GenerateSearchableText由正则改为goldmark的AST
This commit is contained in:
+70
-91
@@ -3,12 +3,13 @@ package utils
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
goldmarkhtml "github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
// markdownEngine 全局 Markdown 渲染引擎
|
||||
@@ -22,6 +23,10 @@ var markdownEngine = goldmark.New(
|
||||
),
|
||||
)
|
||||
|
||||
// searchableTextParser 复用同一套扩展配置,但只做解析,不渲染。
|
||||
// 与 markdownEngine 共享 goldmark 实例的 Parser,避免重复构造解析器开销。
|
||||
var searchableTextParser = markdownEngine.Parser()
|
||||
|
||||
// RenderMarkdown 将 Markdown 文本渲染为 HTML
|
||||
func RenderMarkdown(source string) template.HTML {
|
||||
var buf bytes.Buffer
|
||||
@@ -32,102 +37,76 @@ func RenderMarkdown(source string) template.HTML {
|
||||
return template.HTML(buf.String())
|
||||
}
|
||||
|
||||
// 全局预编译正则(保持不变,但移除冗余)
|
||||
var (
|
||||
// 块级语法
|
||||
reHeading = regexp.MustCompile(`(?m)^\s*#{1,6}\s+`)
|
||||
// 列表
|
||||
reUnorderedList = regexp.MustCompile(`(?m)^\s*[-*+]\s+`)
|
||||
// 有序列表
|
||||
reOrderedList = regexp.MustCompile(`(?m)^\s*\d+[.)]\s+`)
|
||||
// 块引用
|
||||
reBlockquote = regexp.MustCompile(`(?m)^\s*>\s+`)
|
||||
// 分割线
|
||||
reHr = regexp.MustCompile(`(?m)^\s*[-*_]{3,}\s*$`)
|
||||
|
||||
// 引用定义
|
||||
reFootnoteDef = regexp.MustCompile(`(?m)^\[\^.+?\]:\s.*$`)
|
||||
reRefDef = regexp.MustCompile(`(?m)^\s*\[.+?\]:\s*(.*)$`)
|
||||
|
||||
// 图片(全部移除)
|
||||
reImage = regexp.MustCompile(`!\[.*?\]\(.*?\)`)
|
||||
reRefImage = regexp.MustCompile(`!\[.*?\](\[.*?\])?`)
|
||||
|
||||
// 内联链接 [text](url) → 保留 text
|
||||
reLink = regexp.MustCompile(`\[([^\]]*)\]\([^)]*\)`)
|
||||
// 引用式链接 [text][id] 或 [text][] → 保留 text
|
||||
reRefLink = regexp.MustCompile(`\[([^\]]*)\](\[[^\]]*\])?`)
|
||||
|
||||
// 行内标记(加粗、斜体、删除线)
|
||||
reBoldStar = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
reBoldUnd = regexp.MustCompile(`__(.+?)__`)
|
||||
reItalicStar = regexp.MustCompile(`\*(.+?)\*`) // 注意:必须在粗体之后处理
|
||||
reItalicUnd = regexp.MustCompile(`_(.+?)_`) // 同上
|
||||
reStrike = regexp.MustCompile(`~~(.+?)~~`) // 删除线
|
||||
|
||||
// 为保留代码内容(可能含关键词),此处只移除反引号,但代码块可能多行,我们保留内容。
|
||||
reCodeInline = regexp.MustCompile("`([^`]*)`")
|
||||
reCodeBlock = regexp.MustCompile("(?s)```(.*?)```")
|
||||
|
||||
// HTML 标签
|
||||
reHTMLTag = regexp.MustCompile(`<[^>]+>`)
|
||||
|
||||
// 表格分隔线(移除 --- 行,保留内容)
|
||||
reTableSep = regexp.MustCompile(`(?m)^\s*\|?\s*[-:]+[-| :]+\s*\|?\s*$`)
|
||||
|
||||
// 多余空白
|
||||
reMultiSpace = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// GenerateSearchableText 提取 Markdown 中的可见纯文本
|
||||
// GenerateSearchableText 提取 Markdown 中的可见纯文本(基于 goldmark AST)
|
||||
func GenerateSearchableText(raw string) string {
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
text := raw
|
||||
|
||||
// 1. 全角空格转半角
|
||||
text = strings.ReplaceAll(text, "\u3000", " ")
|
||||
// 全角空格转半角
|
||||
raw = strings.ReplaceAll(raw, "\u3000", " ")
|
||||
|
||||
// 2. 移除块级标记(标题、列表、引用、分割线)
|
||||
text = reHeading.ReplaceAllString(text, "")
|
||||
text = reUnorderedList.ReplaceAllString(text, "")
|
||||
text = reOrderedList.ReplaceAllString(text, "")
|
||||
text = reBlockquote.ReplaceAllString(text, "")
|
||||
text = reHr.ReplaceAllString(text, "")
|
||||
source := []byte(raw)
|
||||
doc := searchableTextParser.Parse(text.NewReader(source))
|
||||
|
||||
// 3. 移除引用定义行
|
||||
text = reFootnoteDef.ReplaceAllString(text, "")
|
||||
text = reRefDef.ReplaceAllString(text, "")
|
||||
var sb strings.Builder
|
||||
extractSearchableText(doc, source, &sb)
|
||||
|
||||
// 4. 移除图片(所有形式)
|
||||
text = reImage.ReplaceAllString(text, "")
|
||||
text = reRefImage.ReplaceAllString(text, "")
|
||||
|
||||
// 5. 移除代码块(整块移除,因为代码块内容通常不参与自然语言搜索)
|
||||
text = reCodeBlock.ReplaceAllString(text, "$1")
|
||||
|
||||
// 6. 处理链接(先引用式后内联)
|
||||
text = reLink.ReplaceAllString(text, "$1")
|
||||
text = reRefLink.ReplaceAllString(text, "$1")
|
||||
|
||||
// 7. 移除行内代码反引号(保留代码内容)
|
||||
text = reCodeInline.ReplaceAllString(text, "")
|
||||
|
||||
// 8. 移除粗体、斜体、删除线标记(保留文字)
|
||||
text = reBoldStar.ReplaceAllString(text, "$1")
|
||||
text = reBoldUnd.ReplaceAllString(text, "$1")
|
||||
text = reItalicStar.ReplaceAllString(text, "$1")
|
||||
text = reItalicUnd.ReplaceAllString(text, "$1")
|
||||
text = reStrike.ReplaceAllString(text, "$1")
|
||||
|
||||
// 9. 移除表格分隔线(保留表格内容)
|
||||
text = reTableSep.ReplaceAllString(text, "")
|
||||
|
||||
// 10. 移除 HTML 标签
|
||||
text = reHTMLTag.ReplaceAllString(text, "")
|
||||
|
||||
// 11. 压缩空白
|
||||
text = reMultiSpace.ReplaceAllString(text, " ")
|
||||
return strings.TrimSpace(text)
|
||||
// 压缩空白(等价于原 reMultiSpace + TrimSpace)
|
||||
return strings.Join(strings.Fields(sb.String()), " ")
|
||||
}
|
||||
|
||||
// extractSearchableText 深度优先遍历 AST,把可见文本写入 sb
|
||||
func extractSearchableText(node ast.Node, source []byte, sb *strings.Builder) {
|
||||
switch n := node.(type) {
|
||||
case *ast.Image:
|
||||
// 图片:整棵子树跳过(含 alt 文本),对应原 reImage / reRefImage
|
||||
return
|
||||
|
||||
case *ast.HTMLBlock, *ast.RawHTML:
|
||||
// 原始 HTML:跳过,对应原 reHTMLTag
|
||||
return
|
||||
|
||||
case *ast.AutoLink:
|
||||
// 自动链接 <url> / <email>:保留 URL / 邮箱
|
||||
// n.URL(source) 返回链接目标;邮箱自动链接会带 mailto: 前缀,这里去掉
|
||||
url := string(n.URL(source))
|
||||
url = strings.TrimPrefix(url, "mailto:")
|
||||
sb.WriteString(url)
|
||||
sb.WriteByte(' ')
|
||||
return
|
||||
|
||||
case *ast.FencedCodeBlock, *ast.CodeBlock:
|
||||
lines := n.Lines()
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
seg := lines.At(i)
|
||||
sb.Write(seg.Value(source))
|
||||
}
|
||||
sb.WriteByte(' ')
|
||||
return
|
||||
|
||||
case *ast.Text:
|
||||
sb.Write(n.Segment.Value(source))
|
||||
if n.SoftLineBreak() || n.HardLineBreak() {
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
return
|
||||
|
||||
case *ast.String:
|
||||
// 代码块 / 部分扩展产生的字符串节点
|
||||
sb.Write(n.Value)
|
||||
return
|
||||
}
|
||||
|
||||
// 递归子节点
|
||||
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
extractSearchableText(child, source, sb)
|
||||
}
|
||||
|
||||
// 块级元素结束后补一个空格,避免相邻段落/列表项文本粘连
|
||||
switch node.Kind() {
|
||||
case ast.KindParagraph, ast.KindHeading, ast.KindListItem,
|
||||
ast.KindBlockquote, ast.KindList:
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user