提问背景 Question:
按下面的来改,先不要管代码高亮的事情了。你看看下面的代码。
// 2. 百科详情页 Handler
// 路由配置:r.GET("/baike/article/:id", ai.Baike_detail)
func Baike_detail(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
articleIDStr := ps.ByName("id")
articleID, err := strconv.Atoi(articleIDStr)
if err != nil {
http.NotFound(w, r)
return
}
// 1. 查询文章主体
var a Article
var contentRaw string
var createdAtInt int64
row := fun.DB.QueryRow(
"SELECT id, question, title, description, content, click_count, like_count, created_at FROM cms_article WHERE id = ? AND status = 1",
articleID,
)
err = row.Scan(&a.ID, &a.Question, &a.Title, &a.Description, &contentRaw, &a.ClickCount, &a.LikeCount, &createdAtInt)
if err != nil {
if err == sql.ErrNoRows {
http.NotFound(w, r)
} else {
http.Error(w, "数据库错误: "+err.Error(), http.StatusInternalServerError)
}
return
}
//a.Content = template.HTML(contentRaw) // 转换为非转义 HTML,直接渲染
a.Content = ConvertMarkdown(contentRaw) //解析Markdown并转义template.HTML
a.CreatedAt = time.Unix(createdAtInt, 0).Format("2006-01-02 15:04")
// 2. 异步增加浏览量(避免阻塞主渲染)
go func(id int) {
_, _ = fun.DB.Exec("UPDATE cms_article SET click_count = click_count + 1 WHERE id = ?", id)
}(articleID)
// 3. 查询当前文章的 Tags
var tags []Tag
tagRows, err := fun.DB.Query(`
SELECT t.id, t.name
FROM cms_tag t
INNER JOIN cms_article_tag at ON t.id = at.tag_id
WHERE at.article_id = ?`,
articleID,
)
if err == nil {
for tagRows.Next() {
var t Tag
if err := tagRows.Scan(&t.ID, &t.Name); err == nil {
tags = append(tags, t)
}
}
tagRows.Close()
}
data := BaikeDetailData{
Article: a,
Tags: tags,
}
// 4. 渲染模板
path := filepath.Join(fun.AppPath, "/templates/ai/baike_detail.html")
mb, err := fun.TManager.Get(path)
if err != nil {
http.Error(w, "详情模板渲染失败: "+err.Error(), http.StatusInternalServerError)
return
}
err = mb.Execute(w, data)
if err != nil {
fun.WriteLog("渲染百科详情页模板报错: %s", err)
}
}
func ConvertMarkdown(mdContent string) template.HTML {
var buf bytes.Buffer
if err := goldmark.Convert([]byte(mdContent), &buf); err != nil {
return template.HTML(mdContent) // 如果解析失败,回退返回原始文本
}
return template.HTML(buf.String())
}
感觉是\n没有处理好,在交给md解析之前,是不是应该先处理什么东西。换行吗?
解决Markdown字面量\n导致解析失效方案
你的直觉非常精准!这确实是导致 Markdown 渲染完全失效的根本原因。
1. 为什么会这样?
在数据传输(特别是 JSON 格式传输)、API 响应或从某些不规范的数据库存储中读取时,真正的换行符(如回车 )往往会被转义为字面量的两个字符:\(反斜杠)和 n(字母n)。
- 真正的换行符:在计算机中是一个不可见的控制字符(ASCII 码 10)。
- 字面量
:是两个普通的可见字符。
Goldmark 作为标准的 Markdown 解析器,必须依赖真正的换行符来区分行。例如,只有在换行后紧跟 ### 或 -,它才会将其解析为标题或列表。如果中间夹杂的是字面量的 ,解析器会认为整篇文档只有一条极长的“单行文本”,从而将所有 Markdown 标记直接原文输出,导致排版彻底崩溃。
2. 后端解决方案:归一化处理
在将文本交给 Goldmark 之前,我们应当在 Go 后端对字面量 进行预处理,将其替换为真正的换行符。同时,为了保险起见,建议引入 strings 包,并同时兼容处理 Windows 的回车换行符(`
`)。
请修改你的 ConvertMarkdown 函数,代码如下:
import (
"bytes"
"html/template"
"strings"
"github.com/yuin/goldmark"
)
func ConvertMarkdown(mdContent string) template.HTML {
// 1. 将字面量的 "\
" (反斜杠+n) 替换为真正的换行符 "
"
mdContent = strings.ReplaceAll(mdContent, "\
", "
")
// 2. 兼容性处理:将字面量的 "\
\
" 替换为真正的 "
"
mdContent = strings.ReplaceAll(mdContent, "\
\
", "
")
// 3. 兼容性处理:将 Windows 换行符统一替换为 Linux 换行符(可选,防患未然)
mdContent = strings.ReplaceAll(mdContent, "
", "
")
var buf bytes.Buffer
if err := goldmark.Convert([]byte(mdContent), &buf); err != nil {
return template.HTML(mdContent) // 如果解析失败,回退返回原始文本
}
return template.HTML(buf.String())
}
3. 修改后的效果对照
做出此修改后:
- `
核心食材将会变成物理换行,Goldmark 会正确识别###并将其渲染为核心食材`。
- `
- 优质皮蛋
将会换行并被正确识别为列表优质皮蛋`。
- 原来夹杂在文本中的强转义标签如
<strong>也将正常生效,或者你可以直接使用标准的 Markdown 语法**粗体**,两者均能完美呈现。