package rule import ( "encoding/json" "fmt" "regexp" "sort" "strings" ) // 本文件对应 AnalyzeRule.kt 中的 SourceRule 内部类与 splitSourceRule。 // Mode 规则模式(对应 AnalyzeRule.Mode)。 type Mode int const ( ModeXPath Mode = iota ModeJson ModeDefault ModeJs ModeRegex ModeWebJs ) var ( jsPatternRe = regexp.MustCompile(`(?i)([\w\W]*?)|@js:([\w\W]*)`) webJsPatternRe = regexp.MustCompile(`(?i)@webjs:([\w\W]{5,})`) putPatternRe = regexp.MustCompile(`(?i)@put:(\{[^}]+?\})`) evalPatternRe = regexp.MustCompile(`(?i)@get:\{[^}]+?\}|\{\{[\w\W]*?\}\}`) regexGroupRe = regexp.MustCompile(`\$\d{1,2}`) ) // makeUpRule 参数类型(对应 SourceRule 的 ruleType 常量)。 const ( paramGet = -2 // @get:{name} paramJs = -1 // {{js}} paramText = 0 // 字面文本 paramGroupN = 1 // >0 为 $N 正则分组引用,typ 即分组序号 ) type ruleParam struct { typ int val string } // SourceRule 是拆分后的单条规则(对应 AnalyzeRule.SourceRule)。 type SourceRule struct { Rule string Mode Mode putMap map[string]string ruleParams []ruleParam } // SplitSourceRule 对应 AnalyzeRule.splitSourceRule(ruleStr, allInOne)。 // contentIsJSON 对应 Kotlin 成员 isJSON(当前内容是否为 JSON)。 // isRegex 对应 AnalyzeRule.isRegex 持久标记,可为 nil。 func SplitSourceRule(ruleStr string, allInOne, contentIsJSON bool, isRegex *bool) []*SourceRule { if ruleStr == "" { return nil } mode := ModeDefault start := 0 if allInOne && strings.HasPrefix(ruleStr, ":") { mode = ModeRegex if isRegex != nil { *isRegex = true } start = 1 } else if isRegex != nil && *isRegex { mode = ModeRegex } type rulePart struct { start int end int rule string mode Mode } var parts []rulePart for _, g := range jsPatternRe.FindAllStringSubmatchIndex(ruleStr, -1) { jsBody := "" if g[2] >= 0 { // group(1): ... jsBody = ruleStr[g[2]:g[3]] } else if g[4] >= 0 { // group(2): @js:... jsBody = ruleStr[g[4]:g[5]] } parts = append(parts, rulePart{g[0], g[1], jsBody, ModeJs}) } for _, g := range webJsPatternRe.FindAllStringSubmatchIndex(ruleStr, -1) { parts = append(parts, rulePart{g[0], g[1], ruleStr[g[2]:g[3]], ModeWebJs}) } sort.Slice(parts, func(i, j int) bool { return parts[i].start < parts[j].start }) var out []*SourceRule for _, p := range parts { if p.start < start { continue } if p.start > start { if tmp := strings.TrimSpace(ruleStr[start:p.start]); tmp != "" { out = append(out, newSourceRule(tmp, mode, contentIsJSON)) } } out = append(out, newSourceRule(p.rule, p.mode, contentIsJSON)) start = p.end } if len(ruleStr) > start { if tmp := strings.TrimSpace(ruleStr[start:]); tmp != "" { out = append(out, newSourceRule(tmp, mode, contentIsJSON)) } } return out } // newSourceRule 对应 SourceRule.init:模式识别 + 分离 put + 拆分 @get/{{}}/$N。 func newSourceRule(ruleStr string, mode Mode, contentIsJSON bool) *SourceRule { sr := &SourceRule{Mode: mode, putMap: map[string]string{}} rule := ruleStr switch { case mode == ModeJs || mode == ModeRegex: // 保持原样 case hasPrefixFold(ruleStr, "@CSS:"): sr.Mode = ModeDefault case strings.HasPrefix(ruleStr, "@@"): sr.Mode = ModeDefault rule = ruleStr[2:] case hasPrefixFold(ruleStr, "@XPath:"): sr.Mode = ModeXPath rule = ruleStr[7:] case hasPrefixFold(ruleStr, "@Json:"): sr.Mode = ModeJson rule = ruleStr[6:] case contentIsJSON || strings.HasPrefix(ruleStr, "$.") || strings.HasPrefix(ruleStr, "$["): sr.Mode = ModeJson case strings.HasPrefix(ruleStr, "/"): sr.Mode = ModeXPath } // 分离 @put:{...} rule = splitPutRule(rule, sr.putMap) sr.Rule = rule sr.splitEvalParams() return sr } func hasPrefixFold(s, prefix string) bool { return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix) } // splitPutRule 对应 splitPutRule:提取并移除 @put:{...} 段。 func splitPutRule(ruleStr string, putMap map[string]string) string { out := ruleStr for _, m := range putPatternRe.FindAllStringSubmatch(ruleStr, -1) { out = strings.Replace(out, m[0], "", 1) parsed := map[string]string{} if err := json.Unmarshal([]byte(m[1]), &parsed); err == nil { for k, v := range parsed { putMap[k] = v } continue } // 宽松解析(对应 GSON lenient):key:val 键值对 pairRe := regexp.MustCompile(`["']?(\w+)["']?\s*:\s*(["']?)([^,{}]*?)\2`) for _, pm := range pairRe.FindAllStringSubmatch(m[1], -1) { putMap[pm[1]] = pm[3] } } return out } // splitEvalParams 对应 init 中 @get/{{ }} 的拆分循环。 func (sr *SourceRule) splitEvalParams() { rule := sr.Rule start := 0 locs := evalPatternRe.FindAllStringIndex(rule, -1) if len(locs) > 0 { firstStart := locs[0][0] prefix := rule[:firstStart] if sr.Mode != ModeJs && sr.Mode != ModeRegex && (firstStart == 0 || !strings.Contains(prefix, "##")) { sr.Mode = ModeRegex } for _, loc := range locs { if loc[0] > start { sr.splitRegex(rule[start:loc[0]]) } tmp := rule[loc[0]:loc[1]] switch { case hasPrefixFold(tmp, "@get:"): sr.ruleParams = append(sr.ruleParams, ruleParam{paramGet, tmp[6 : len(tmp)-1]}) case strings.HasPrefix(tmp, "{{"): sr.ruleParams = append(sr.ruleParams, ruleParam{paramJs, tmp[2 : len(tmp)-2]}) default: sr.splitRegex(tmp) } start = loc[1] } } if len(rule) > start { sr.splitRegex(rule[start:]) } } // splitRegex 对应 SourceRule.splitRegex:拆分 $N 分组引用。 // $N 匹配只作用于 "##" 前的第一段,"##..." 尾部作为字面参数保留, // 由 MakeUpRule 重新按 "##" 切分。 func (sr *SourceRule) splitRegex(ruleStr string) { start := 0 first := strings.Split(ruleStr, "##")[0] locs := regexGroupRe.FindAllStringIndex(first, -1) if len(locs) > 0 { if sr.Mode != ModeJs && sr.Mode != ModeRegex { sr.Mode = ModeRegex } for _, loc := range locs { if loc[0] > start { sr.ruleParams = append(sr.ruleParams, ruleParam{paramText, ruleStr[start:loc[0]]}) } n := 0 fmt.Sscanf(ruleStr[loc[0]+1:loc[1]], "%d", &n) sr.ruleParams = append(sr.ruleParams, ruleParam{n, ruleStr[loc[0]:loc[1]]}) start = loc[1] } } if len(ruleStr) > start { sr.ruleParams = append(sr.ruleParams, ruleParam{paramText, ruleStr[start:]}) } } // ResolvedSourceRule 对应 AnalyzeRule.ResolvedSourceRule。 type ResolvedSourceRule struct { Rule string ReplaceRegex string Replacement string ReplaceFirst bool ParamSize int } // RuleDeps 是 MakeUpRule 解析内嵌 {{...}} 时需要的执行环境。 type RuleDeps struct { // JS 执行 {{js}}(P0 未接入 goja 时返回 ErrJsUnsupported) JS func(js string, result any) (any, error) // Rule 执行 {{@rule}} / {{$.rule}} 形式的规则引用 Rule func(rule string) (string, error) // Get 对应 AnalyzeRule.get(key) Get func(key string) string } // MakeUpRule 对应 SourceRule.makeUpRule(result):替换 @get/{{}}/$N, // 再按 "##" 切分出正则段。 func (sr *SourceRule) MakeUpRule(result any, deps *RuleDeps) (ResolvedSourceRule, error) { resolved := sr.Rule if len(sr.ruleParams) > 0 { var infoVal []byte for i := len(sr.ruleParams) - 1; i >= 0; i-- { p := sr.ruleParams[i] switch { case p.typ >= paramGroupN: // 对应 Kotlin:(result as? List) 成功但越界时不插入任何内容 switch lst := result.(type) { case []string: if len(lst) > p.typ { infoVal = prepend(infoVal, lst[p.typ]) } case []any: if len(lst) > p.typ { if s, ok := lst[p.typ].(string); ok { infoVal = prepend(infoVal, s) } } default: infoVal = prepend(infoVal, p.val) } case p.typ == paramJs: if isRuleString(p.val) { s, err := deps.Rule(p.val) if err != nil { return ResolvedSourceRule{}, err } infoVal = prepend(infoVal, s) } else { v, err := deps.JS(p.val, result) if err != nil { return ResolvedSourceRule{}, err } infoVal = prepend(infoVal, anyToString(v)) } case p.typ == paramGet: infoVal = prepend(infoVal, deps.Get(p.val)) default: infoVal = prepend(infoVal, p.val) } } resolved = string(infoVal) } segs := strings.Split(resolved, "##") r := ResolvedSourceRule{ Rule: strings.TrimSpace(segs[0]), ReplaceFirst: len(segs) > 3, ParamSize: len(sr.ruleParams), } if len(segs) > 1 { r.ReplaceRegex = segs[1] } if len(segs) > 2 { r.Replacement = segs[2] } return r, nil } func prepend(dst []byte, s string) []byte { return append([]byte(s), dst...) } func isRuleString(s string) bool { return strings.HasPrefix(s, "@") || strings.HasPrefix(s, "$.") || strings.HasPrefix(s, "$[") || strings.HasPrefix(s, "//") } // anyToString 对应 Kotlin 值字符串化(整值 Double 去小数,对应 %.0f)。 func anyToString(v any) string { switch t := v.(type) { case nil: return "" case string: return t case float64: if t == float64(int64(t)) { return fmt.Sprintf("%.0f", t) } return fmt.Sprintf("%v", t) default: return fmt.Sprintf("%v", t) } }