package rule import ( "encoding/json" "sort" "strconv" "strings" ) // 本文件对应 AnalyzeByJSonPath.kt(Jayway JsonPath 语义,Go 侧用 // PaesslerAG/jsonpath 实现)。 type jsonAnalyzer struct { root any } func newJSONAnalyzer(doc any) *jsonAnalyzer { switch t := doc.(type) { case string: var v any if err := json.Unmarshal([]byte(t), &v); err != nil { return &jsonAnalyzer{root: nil} } return &jsonAnalyzer{root: v} default: return &jsonAnalyzer{root: doc} } } // jsonRead 对应 ctx.read(rule):路径求值,失败返回 nil。 func jsonRead(root any, path string) any { if root == nil || path == "" { return nil } v, err := jsonpathGet(normalizeJSONPath(path), root) if err != nil { return nil } return v } // normalizeJSONPath 把 legado(Jayway)允许、而 PaesslerAG/jsonpath 拒绝的写法 // 归一化成合法路径,共两处差异: // // 1. 缺根 `$`:Jayway 要求路径以 `$`/`@` 开头,legado 书源却常直接写 // `results.list`、`results.list.[*]`。PaesslerAG 对「不含 `[]`/`*` 的裸路径」 // 恰好能容忍(内部按 `$.` 解析),但一旦出现 `[*]` 就报 // `unexpected "*" while scanning extensions`。这里统一补根。 // 2. 点后紧跟方括号:`results.list.[*]`、`$.a.[0]`、`$.a.['k']`。Jayway 里 // `.[` 等价于 `[`,PaesslerAG 会报 `unexpected "[" while scanning field`。 // // 两处都会让整条规则静默返回空——表现为「书源明明有效却搜不出任何结果」 // (拷贝轻小说源的搜索列表规则就是 `results.list.[*]`)。 func normalizeJSONPath(path string) string { p := strings.TrimSpace(path) if p == "" { return path } switch { case strings.HasPrefix(p, "$"), strings.HasPrefix(p, "@"): // 已有根,保持 case strings.HasPrefix(p, "."): p = "$" + p // `..a` 这类递归写法,补 `$` 而不是 `$.` default: p = "$." + p } if !strings.Contains(p, ".[") { return p } var b strings.Builder b.Grow(len(p)) for i := 0; i < len(p); i++ { // 丢掉 `[` 前多余的点;递归下降 `..[` 保持原样。 if p[i] == '.' && i+1 < len(p) && p[i+1] == '[' && i > 0 && p[i-1] != '.' { continue } b.WriteByte(p[i]) } return b.String() } // jsonValueToString 对应 Kotlin 的 ob.toString() / joinToString("\n")。 // 列表按 "\n" 拼接(legado 的 getString 就是这么做的);对象走 Java 的 Map.toString // 形态(见 javaValueString),书源的正则大量依赖这个形态。 func jsonValueToString(ob any) string { switch t := ob.(type) { case nil: return "" case string: return t case []any: parts := make([]string, len(t)) for i, e := range t { parts[i] = jsonValueToString(e) } return strings.Join(parts, "\n") default: return javaValueString(t) } } // javaValueString 按 Java 的 Map/List toString 形态输出 JSON 值。 // // 书源规则里的正则就是照着这个形态写的,最典型的是拷贝漫画的作者规则 // `$.author##.*name=(.*?)\,.*##$1`:它期望拿到 `{name=岸本斉史, alias=…}`。 // 之前这里用 Go 的 fmt 输出 `map[alias:… name:…]`,`name=` 根本匹配不到, // 作者字段就退化成一整串 map 文本。 // // 已知差异:Java 的 LinkedTreeMap 保留 JSON 里的键顺序,而 Go 的 // map[string]interface{} 不保序(JSONPath 库也只认这个类型),这里按 **键排序** // 输出以保证确定性。绝大多数「取某个键」的正则不受影响,但依赖原始键序的正则 // (如 `.*name=(.*?)\,` 且 name 是最后一个键)仍可能与阅读 App 的结果不同。 func javaValueString(v any) string { switch t := v.(type) { case nil: return "null" case string: return t case bool: return strconv.FormatBool(t) case float64: return strconv.FormatFloat(t, 'f', -1, 64) case int: return strconv.Itoa(t) case int64: return strconv.FormatInt(t, 10) case json.Number: return t.String() case []any: parts := make([]string, len(t)) for i, e := range t { parts[i] = javaValueString(e) } return "[" + strings.Join(parts, ", ") + "]" case map[string]any: keys := make([]string, 0, len(t)) for k := range t { keys = append(keys, k) } sort.Strings(keys) var sb strings.Builder sb.WriteByte('{') for i, k := range keys { if i > 0 { sb.WriteString(", ") } sb.WriteString(k) sb.WriteByte('=') sb.WriteString(javaValueString(t[k])) } sb.WriteByte('}') return sb.String() default: return anyToString(v) } } // getString 对应 AnalyzeByJSonPath.getString。 func (a *jsonAnalyzer) getString(rule string) string { if rule == "" { return "" } ra := NewRuleAnalyzer(rule, true) rules := ra.SplitRule("&&", "||") if len(rules) == 1 { ra.ReSetPos() result := ra.InnerRule("{$.", 1, 1, func(inner string) string { return a.getString(inner) }) if result == "" { result = jsonValueToString(jsonRead(a.root, rule)) } return result } var textList []string for _, rl := range rules { temp := a.getString(rl) if temp != "" { textList = append(textList, temp) if ra.ElementsType() == "||" { break } } } return strings.Join(textList, "\n") } // getStringList 对应 AnalyzeByJSonPath.getStringList。 func (a *jsonAnalyzer) getStringList(rule string) []string { var result []string if rule == "" { return result } ra := NewRuleAnalyzer(rule, true) rules := ra.SplitRule("&&", "||", "%%") if len(rules) == 1 { ra.ReSetPos() st := ra.InnerRule("{$.", 1, 1, func(inner string) string { return a.getString(inner) }) if st == "" { ob := jsonRead(a.root, rule) if ob == nil { return result } if lst, ok := ob.([]any); ok { for _, o := range lst { result = append(result, jsonValueToString(o)) } } else { result = append(result, jsonValueToString(ob)) } } else { result = append(result, st) } return result } var results [][]string for _, rl := range rules { temp := a.getStringList(rl) if len(temp) > 0 { results = append(results, temp) if ra.ElementsType() == "||" { break } } } if len(results) > 0 { if ra.ElementsType() == "%%" { for i := 0; i < len(results[0]); i++ { for _, temp := range results { if i < len(temp) { result = append(result, temp[i]) } } } } else { for _, temp := range results { result = append(result, temp...) } } } return result } // getObject 对应 getObject:直接返回求值结果。 func (a *jsonAnalyzer) getObject(rule string) any { return jsonRead(a.root, rule) } // getList 对应 getList:要求路径结果为数组(对应 jayway read, // 非数组时 legado 侧捕获异常返回空列表)。 func (a *jsonAnalyzer) getList(rule string) []any { var result []any if rule == "" { return result } ra := NewRuleAnalyzer(rule, true) rules := ra.SplitRule("&&", "||", "%%") if len(rules) == 1 { ob := jsonRead(a.root, rules[0]) if lst, ok := ob.([]any); ok { return lst } return result } var results [][]any for _, rl := range rules { temp := a.getList(rl) if len(temp) > 0 { results = append(results, temp) if ra.ElementsType() == "||" { break } } } if len(results) > 0 { if ra.ElementsType() == "%%" { for i := 0; i < len(results[0]); i++ { for _, temp := range results { if i < len(temp) { result = append(result, temp[i]) } } } } else { for _, temp := range results { result = append(result, temp...) } } } return result }