// 长文的自动摘要与自动封面:站主没填这两项时从正文派生—— // 摘要取正文开头的一段纯文本,封面取正文第一张图。 // 更新路径只在「新值与旧值都为空」时才生成,站主主动清空的要保得住。 package admin import ( "regexp" "strings" "oneblog/internal/model" ) const autoSummaryLen = 110 var ( mdImageRe = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)[^)]*\)`) mdCodeFence = regexp.MustCompile("(?s)```.*?```") mdLinkRe = regexp.MustCompile(`\[([^\]]*)\]\([^)]*\)`) mdHeadingRe = regexp.MustCompile(`(?m)^#{1,6}\s+.*$`) // 标题行整行不进摘要(与文章标题重复) mdNoiseRe = regexp.MustCompile(`(?m)^([>\s*-]+|\d+\.\s)+`) mdEmphasis = strings.NewReplacer("**", "", "__", "", "~~", "", "*", "", "_", "") wsRe = regexp.MustCompile(`\s+`) ) // autoMeta 按需填充 in.Summary / in.CoverURL。 // cur 为更新前的旧文章(新建时传 nil)。 func autoMeta(in *model.PostInput, cur *model.Post) { if in.Kind != model.KindLong || strings.TrimSpace(in.ContentMd) == "" { return } if strings.TrimSpace(in.Summary) == "" && (cur == nil || strings.TrimSpace(cur.Summary) == "") { in.Summary = summarizeMarkdown(in.ContentMd, autoSummaryLen) } if strings.TrimSpace(in.CoverURL) == "" && (cur == nil || strings.TrimSpace(cur.CoverURL) == "") { in.CoverURL = firstImage(in.ContentMd) } } // summarizeMarkdown 剥掉 markdown 语法后取正文开头一段纯文本 func summarizeMarkdown(md string, maxRunes int) string { s := mdCodeFence.ReplaceAllString(md, " ") // 代码块整段不进摘要 s = mdHeadingRe.ReplaceAllString(s, " ") s = mdImageRe.ReplaceAllString(s, " ") s = mdLinkRe.ReplaceAllString(s, "$1") s = mdNoiseRe.ReplaceAllString(s, " ") s = mdEmphasis.Replace(s) s = strings.ReplaceAll(s, "`", "") s = wsRe.ReplaceAllString(s, " ") s = strings.TrimSpace(s) runes := []rune(s) if len(runes) > maxRunes { return string(runes[:maxRunes]) + "…" } return s } // firstImage 取正文第一张图片的地址(markdown 图;http/站内路径均可) func firstImage(md string) string { m := mdImageRe.FindStringSubmatch(md) if m == nil { return "" } u := m[1] if strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "/uploads/") { return u } return "" }