115 lines
2.5 KiB
Go
115 lines
2.5 KiB
Go
package contentreviewser
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
|
||
"91porn-server/models/v/contentreviewmod"
|
||
)
|
||
|
||
const (
|
||
markPrefix = "<mark>"
|
||
markSuffix = "</mark>"
|
||
)
|
||
|
||
// allTerms 把快照展开为一组待匹配的词
|
||
func allTerms(snaps []contentreviewmod.SensitiveWordSnap) []string {
|
||
seen := make(map[string]struct{})
|
||
out := make([]string, 0, len(snaps))
|
||
for _, s := range snaps {
|
||
if s.Word == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[s.Word]; ok {
|
||
continue
|
||
}
|
||
seen[s.Word] = struct{}{}
|
||
out = append(out, s.Word)
|
||
}
|
||
// 长词优先,避免短词先替换破坏长词的匹配
|
||
sort.Slice(out, func(i, j int) bool {
|
||
return len(out[i]) > len(out[j])
|
||
})
|
||
return out
|
||
}
|
||
|
||
// findHits 返回 input 中命中的词(去重,保持长→短顺序)
|
||
func findHits(input string, terms []string) []string {
|
||
if input == "" || len(terms) == 0 {
|
||
return nil
|
||
}
|
||
hits := make([]string, 0)
|
||
seen := make(map[string]struct{})
|
||
for _, t := range terms {
|
||
if t == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[t]; ok {
|
||
continue
|
||
}
|
||
if strings.Contains(input, t) {
|
||
seen[t] = struct{}{}
|
||
hits = append(hits, t)
|
||
}
|
||
}
|
||
return hits
|
||
}
|
||
|
||
// highlight 用 <mark>...</mark> 包裹命中的词
|
||
// hits 已按长度降序;为避免再次包裹已 mark 的内容,分段替换
|
||
func highlight(input string, hits []string) string {
|
||
if input == "" || len(hits) == 0 {
|
||
return input
|
||
}
|
||
result := input
|
||
for _, h := range hits {
|
||
if h == "" {
|
||
continue
|
||
}
|
||
result = replaceNotInMark(result, h)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// replaceNotInMark 在 input 中把 term 全部替换为 <mark>term</mark>,
|
||
// 但跳过已经包在 <mark>...</mark> 内的片段(避免嵌套)
|
||
func replaceNotInMark(input, term string) string {
|
||
if term == "" {
|
||
return input
|
||
}
|
||
var b strings.Builder
|
||
i := 0
|
||
for i < len(input) {
|
||
// 已经在 mark 块里,整段原样输出
|
||
if strings.HasPrefix(input[i:], markPrefix) {
|
||
end := strings.Index(input[i:], markSuffix)
|
||
if end < 0 {
|
||
b.WriteString(input[i:])
|
||
return b.String()
|
||
}
|
||
b.WriteString(input[i : i+end+len(markSuffix)])
|
||
i += end + len(markSuffix)
|
||
continue
|
||
}
|
||
if strings.HasPrefix(input[i:], term) {
|
||
b.WriteString(markPrefix)
|
||
b.WriteString(term)
|
||
b.WriteString(markSuffix)
|
||
i += len(term)
|
||
continue
|
||
}
|
||
b.WriteByte(input[i])
|
||
i++
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// matchAndHighlight 在 input 上一步到位:返回命中词列表 + 高亮后的文本
|
||
func matchAndHighlight(input string, terms []string) ([]string, string) {
|
||
hits := findHits(input, terms)
|
||
if len(hits) == 0 {
|
||
return nil, input
|
||
}
|
||
return hits, highlight(input, hits)
|
||
}
|