// Package markdown 服务端 Markdown 渲染(goldmark + bluemonday), // 仅用于 RSS / meta 摘要;库中始终只存 Markdown 原文(§7.5)。 package markdown import ( "bytes" "html" "regexp" "strings" "unicode/utf8" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" ) var engine = goldmark.New( goldmark.WithExtensions(extension.GFM), // 默认 html.WithUnsafe=false:原始 HTML 与危险 URL 被转义(XSS 安全默认), // 再经 bluemonday 白名单清洗双保险。 ) // ugcPolicy RSS description 用白名单清洗(双保险,与 goldmark 转义叠加)。 var ugcPolicy = bluemonday.UGCPolicy() // strictPolicy 摘要提取用:剥掉全部标签。 var strictPolicy = bluemonday.StrictPolicy() // Render 将 Markdown 渲染为清洗后的 HTML。 func Render(source string) string { var buf bytes.Buffer if err := engine.Convert([]byte(source), &buf); err != nil { // goldmark 对任意输入不产生错误;兜底按纯文本转义。 return html.EscapeString(source) } return ugcPolicy.Sanitize(buf.String()) } var wsRe = regexp.MustCompile(`\s+`) // Summary 从 Markdown 源提取纯文本摘要:渲染→剥标签→反转义→压空白→截断。 func Summary(source string, limit int) string { if limit <= 0 { return "" } text := strictPolicy.Sanitize(Render(source)) text = html.UnescapeString(text) text = strings.TrimSpace(wsRe.ReplaceAllString(text, " ")) if r := utf8.RuneCountInString(text); r > limit { runes := []rune(text) return string(runes[:limit]) + "…" } return text }