mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
194 lines
5.3 KiB
Go
194 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func parseRecognitionWordRules(raw string) []recognitionWordRule {
|
|
var out []recognitionWordRule
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
|
|
continue
|
|
}
|
|
out = append(out, parseRecognitionWordRule(line))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseRecognitionWordRule(line string) recognitionWordRule {
|
|
rule := recognitionWordRule{raw: line}
|
|
for _, part := range strings.Split(line, "&&") {
|
|
part = strings.TrimSpace(part)
|
|
switch {
|
|
case strings.Contains(part, "=>"):
|
|
pieces := strings.SplitN(part, "=>", 2)
|
|
rule.replaceFrom = strings.TrimSpace(pieces[0])
|
|
rule.replaceTo = normalizeRecognitionReplacement(strings.TrimSpace(pieces[1]))
|
|
case strings.Contains(part, "<>") && strings.Contains(part, ">>"):
|
|
beforeAfter := strings.SplitN(part, ">>", 2)
|
|
bounds := strings.SplitN(beforeAfter[0], "<>", 2)
|
|
// A malformed rule without "<>" would otherwise index past the
|
|
// slice; word lists are fetched from the network, so stay defensive.
|
|
if len(bounds) < 2 {
|
|
continue
|
|
}
|
|
rule.offsetLeft = strings.TrimSpace(bounds[0])
|
|
rule.offsetRight = strings.TrimSpace(bounds[1])
|
|
rule.offsetExpr = strings.TrimSpace(beforeAfter[1])
|
|
default:
|
|
rule.block = part
|
|
}
|
|
}
|
|
compileRecognitionWordRule(&rule)
|
|
return rule
|
|
}
|
|
|
|
var recognitionReplacementRE = regexp.MustCompile(`\\([0-9]+)`)
|
|
|
|
func normalizeRecognitionReplacement(value string) string {
|
|
return recognitionReplacementRE.ReplaceAllString(value, "$$$1")
|
|
}
|
|
|
|
// compileRecognitionWordRule pre-compiles every pattern in a rule so the hot
|
|
// clean-query path never recompiles regexes per candidate.
|
|
func compileRecognitionWordRule(rule *recognitionWordRule) {
|
|
if rule == nil {
|
|
return
|
|
}
|
|
if rule.block != "" {
|
|
if re, err := regexp.Compile(rule.block); err == nil {
|
|
rule.blockRE = re
|
|
}
|
|
}
|
|
if rule.replaceFrom != "" {
|
|
if re, err := regexp.Compile(rule.replaceFrom); err == nil {
|
|
rule.replaceRE = re
|
|
}
|
|
}
|
|
if rule.offsetExpr != "" && (rule.offsetLeft != "" || rule.offsetRight != "") {
|
|
if re, err := compileRecognitionOffsetRE(rule.offsetLeft, rule.offsetRight); err == nil {
|
|
rule.offsetRE = re
|
|
}
|
|
}
|
|
}
|
|
|
|
func compileRecognitionOffsetRE(left, right string) (*regexp.Regexp, error) {
|
|
leftPattern := firstNonEmpty(left, `^`)
|
|
rightPattern := firstNonEmpty(right, `$`)
|
|
return regexp.Compile(`(?i)(` + leftPattern + `)(\d{1,5})(` + rightPattern + `)`)
|
|
}
|
|
|
|
func applyRecognitionWordRules(raw string, rules []recognitionWordRule) string {
|
|
out := strings.TrimSpace(raw)
|
|
for _, rule := range rules {
|
|
if rule.block != "" {
|
|
out = applyRecognitionBlock(out, rule)
|
|
}
|
|
if rule.replaceFrom != "" {
|
|
out = applyRecognitionReplace(out, rule)
|
|
}
|
|
if rule.offsetLeft != "" || rule.offsetRight != "" {
|
|
out = applyRecognitionOffset(out, rule)
|
|
}
|
|
}
|
|
return strings.Join(strings.Fields(out), " ")
|
|
}
|
|
|
|
func applyRecognitionBlock(raw string, rule recognitionWordRule) string {
|
|
if rule.blockRE != nil {
|
|
return rule.blockRE.ReplaceAllString(raw, " ")
|
|
}
|
|
return strings.ReplaceAll(raw, rule.block, " ")
|
|
}
|
|
|
|
func applyRecognitionReplace(raw string, rule recognitionWordRule) string {
|
|
if rule.replaceRE != nil {
|
|
return rule.replaceRE.ReplaceAllString(raw, rule.replaceTo)
|
|
}
|
|
return strings.ReplaceAll(raw, rule.replaceFrom, rule.replaceTo)
|
|
}
|
|
|
|
func applyRecognitionOffset(raw string, rule recognitionWordRule) string {
|
|
if strings.TrimSpace(rule.offsetExpr) == "" {
|
|
return raw
|
|
}
|
|
re := rule.offsetRE
|
|
if re == nil {
|
|
compiled, err := compileRecognitionOffsetRE(rule.offsetLeft, rule.offsetRight)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
re = compiled
|
|
}
|
|
return re.ReplaceAllStringFunc(raw, func(match string) string {
|
|
return applyRecognitionOffsetMatch(re, match, rule.offsetExpr)
|
|
})
|
|
}
|
|
|
|
func applyRecognitionOffsetMatch(re *regexp.Regexp, match, expr string) string {
|
|
parts := re.FindStringSubmatch(match)
|
|
if len(parts) < 4 {
|
|
return match
|
|
}
|
|
ep, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return match
|
|
}
|
|
next, ok := evalRecognitionEpisodeExpr(expr, ep)
|
|
if !ok || next < 0 {
|
|
return match
|
|
}
|
|
format := "%d"
|
|
if width := len(parts[2]); width > 1 && width <= 2 {
|
|
format = "%0" + strconv.Itoa(width) + "d"
|
|
}
|
|
return parts[1] + fmt.Sprintf(format, next) + parts[3]
|
|
}
|
|
|
|
func evalRecognitionEpisodeExpr(expr string, ep int) (int, bool) {
|
|
value := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(expr), " ", ""))
|
|
if value == "" || value == "EP" {
|
|
return ep, true
|
|
}
|
|
if n, err := strconv.Atoi(value); err == nil {
|
|
return n, true
|
|
}
|
|
if out, ok := evalRecognitionEpisodeAddSub(value, ep); ok {
|
|
return out, true
|
|
}
|
|
return evalRecognitionEpisodeMul(value, ep)
|
|
}
|
|
|
|
func evalRecognitionEpisodeAddSub(value string, ep int) (int, bool) {
|
|
for _, op := range []string{"+", "-"} {
|
|
if !strings.HasPrefix(value, "EP"+op) {
|
|
continue
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimPrefix(value, "EP"+op))
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
if op == "+" {
|
|
return ep + n, true
|
|
}
|
|
return ep - n, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func evalRecognitionEpisodeMul(value string, ep int) (int, bool) {
|
|
if strings.HasPrefix(value, "EP*") {
|
|
n, err := strconv.Atoi(strings.TrimPrefix(value, "EP*"))
|
|
return ep * n, err == nil
|
|
}
|
|
if strings.HasSuffix(value, "*EP") {
|
|
n, err := strconv.Atoi(strings.TrimSuffix(value, "*EP"))
|
|
return ep * n, err == nil
|
|
}
|
|
return 0, false
|
|
}
|