mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 21:01:22 +08:00
feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性
支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。 修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。 更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
@@ -17,6 +17,48 @@ public struct JSONPathEvaluator: Sendable {
|
||||
|
||||
public init() {}
|
||||
|
||||
// Legado's JSON rules also allow literal templates such as /book/{$.id}.
|
||||
// Balance braces and quoted strings instead of stopping at the first }.
|
||||
func evaluateRule(_ rule: String, in root: JSONValue) throws -> [JSONValue] {
|
||||
var cursor = rule.startIndex
|
||||
var output = ""
|
||||
var foundTemplate = false
|
||||
while let opening = rule.range(of: "{$.", range: cursor..<rule.endIndex) {
|
||||
foundTemplate = true
|
||||
output += rule[cursor..<opening.lowerBound]
|
||||
let start = rule.index(after: opening.lowerBound)
|
||||
var end = start
|
||||
var depth = 1
|
||||
var quote: Character?
|
||||
var escaped = false
|
||||
while end < rule.endIndex {
|
||||
let character = rule[end]
|
||||
if escaped { escaped = false }
|
||||
else if character == "\\" { escaped = true }
|
||||
else if let activeQuote = quote {
|
||||
if character == activeQuote { quote = nil }
|
||||
} else if character == "\"" || character == "'" { quote = character }
|
||||
else if character == "{" { depth += 1 }
|
||||
else if character == "}" {
|
||||
depth -= 1
|
||||
if depth == 0 { break }
|
||||
}
|
||||
end = rule.index(after: end)
|
||||
}
|
||||
guard end < rule.endIndex else {
|
||||
throw RuleEngineError.malformedRule(rule: rule, detail: "JSONPath 模板大括号未闭合")
|
||||
}
|
||||
let values = try evaluate(path: String(rule[start..<end]), in: root)
|
||||
output += RuleEvaluation(values: values).strings.joined(separator: "\n")
|
||||
cursor = rule.index(after: end)
|
||||
}
|
||||
if foundTemplate {
|
||||
output += rule[cursor...]
|
||||
return [.string(output)]
|
||||
}
|
||||
return try evaluate(path: rule.hasPrefix("$") ? rule : "$." + rule, in: root)
|
||||
}
|
||||
|
||||
public func evaluate(path: String, in root: JSONValue) throws -> [JSONValue] {
|
||||
let segments = try parse(path)
|
||||
var current = [root]
|
||||
@@ -59,7 +101,10 @@ public struct JSONPathEvaluator: Sendable {
|
||||
guard next < rule.endIndex else {
|
||||
throw RuleEngineError.malformedRule(rule: path, detail: "JSONPath 不能以点结尾")
|
||||
}
|
||||
if rule[next] == "." {
|
||||
if rule[next] == "[" {
|
||||
// Legacy sources use $.[*] as well as $[*].
|
||||
index = next
|
||||
} else if rule[next] == "." {
|
||||
index = rule.index(after: next)
|
||||
let parsed = try parseDotSelector(rule, from: index, original: path)
|
||||
segments.append(.recursive(parsed.selector))
|
||||
|
||||
@@ -84,7 +84,8 @@ public extension JSONValue {
|
||||
if value is NSNull { return .null }
|
||||
if let value = value as? JSONValue { return value }
|
||||
if let value = value as? String { return .string(value) }
|
||||
if let value = value as? Bool { return .bool(value) }
|
||||
// NSNumber(0/1) also casts to Bool. Inspect its CF type first so
|
||||
// JavaScript page arithmetic and numeric IDs remain numbers.
|
||||
if let value = value as? NSNumber {
|
||||
if CFGetTypeID(value) == CFBooleanGetTypeID() { return .bool(value.boolValue) }
|
||||
return .number(value.doubleValue)
|
||||
|
||||
@@ -208,6 +208,33 @@ public struct JavaScriptRuleEvaluator: Sendable {
|
||||
script: String,
|
||||
result: JSONValue,
|
||||
executionContext: RuleExecutionContext
|
||||
) throws -> JavaScriptRuleResult {
|
||||
try evaluate(script: script, result: result, executionContext: executionContext,
|
||||
virtualMachine: JSVirtualMachine(), stringifyResult: false)
|
||||
}
|
||||
|
||||
/// A rule shares java.get/put storage, but each match gets fresh JS globals.
|
||||
/// Keep this closure on the synchronous replacement call's thread.
|
||||
func replacementEvaluator(script: String, book: Book, chapter: BookChapter) -> (String) throws -> String {
|
||||
var virtualMachine: JSVirtualMachine?
|
||||
var executionContext = RuleExecutionContext(book: book, chapter: chapter)
|
||||
return { match in
|
||||
if virtualMachine == nil { virtualMachine = JSVirtualMachine() }
|
||||
let result = try autoreleasepool {
|
||||
try evaluate(script: script, result: .string(match), executionContext: executionContext,
|
||||
virtualMachine: virtualMachine!, stringifyResult: true)
|
||||
}
|
||||
executionContext.variables = result.variables
|
||||
return result.value.stringValue
|
||||
}
|
||||
}
|
||||
|
||||
private func evaluate(
|
||||
script: String,
|
||||
result: JSONValue,
|
||||
executionContext: RuleExecutionContext,
|
||||
virtualMachine: JSVirtualMachine,
|
||||
stringifyResult: Bool
|
||||
) throws -> JavaScriptRuleResult {
|
||||
guard script.utf8.count <= maximumScriptLength else {
|
||||
throw RuleEngineError.resourceLimit(rule: script, detail: "JavaScript 规则超过长度上限")
|
||||
@@ -215,8 +242,7 @@ public struct JavaScriptRuleEvaluator: Sendable {
|
||||
try rejectUnboundedExecution(script)
|
||||
|
||||
let box = JSVariableBox(executionContext.variables)
|
||||
let vm = JSVirtualMachine()
|
||||
guard let context = JSContext(virtualMachine: vm) else {
|
||||
guard let context = JSContext(virtualMachine: virtualMachine) else {
|
||||
throw RuleEngineError.invalidInput(rule: script, expected: "无法创建 JavaScriptCore 上下文")
|
||||
}
|
||||
var exceptionMessage: String?
|
||||
@@ -271,7 +297,28 @@ public struct JavaScriptRuleEvaluator: Sendable {
|
||||
let normalized = evaluated.flatMap {
|
||||
context.objectForKeyedSubscript("__legadoUnboxString")?.call(withArguments: [$0])
|
||||
}
|
||||
let value = JSONValue.bridge(normalized?.toObject())
|
||||
let value: JSONValue
|
||||
if stringifyResult {
|
||||
// Rhino unwraps undefined to null before RegexExtensions.kt calls
|
||||
// toString(). Preserve strings directly, rather than JSON encoding.
|
||||
if let evaluated, !evaluated.isUndefined, !evaluated.isNull {
|
||||
let text = evaluated.toString() ?? "null"
|
||||
if evaluated.isNumber {
|
||||
// The reference runs Rhino in interpreted mode, yielding JVM Doubles.
|
||||
let number = evaluated.toDouble()
|
||||
value = .string(number.isFinite ? String(number) : text)
|
||||
} else {
|
||||
value = .string(text)
|
||||
}
|
||||
} else {
|
||||
value = .string("null")
|
||||
}
|
||||
} else {
|
||||
value = JSONValue.bridge(normalized?.toObject())
|
||||
}
|
||||
if let exceptionMessage {
|
||||
throw RuleEngineError.malformedRule(rule: script, detail: "JavaScript 执行失败:\(exceptionMessage)")
|
||||
}
|
||||
guard value.stringValue.utf8.count <= maximumResultLength else {
|
||||
throw RuleEngineError.resourceLimit(rule: script, detail: "JavaScript 结果超过长度上限")
|
||||
}
|
||||
@@ -974,53 +1021,48 @@ public struct JavaScriptRuleEvaluator: Sendable {
|
||||
return resolveJavaScriptRuleURL(direct.stringValue, enabled: isURL, context: executionContext)
|
||||
}
|
||||
|
||||
let plan = try RuleExpressionParser().parse(trimmedRule)
|
||||
let jsonContent: JSONValue?
|
||||
if case .string(let text) = content { jsonContent = try? JSONValue.decode(data: Data(text.utf8)) }
|
||||
else { jsonContent = content }
|
||||
let plan = try RuleExpressionParser().parse(trimmedRule, inputLooksLikeJSON: jsonContent != nil)
|
||||
var branchResults: [[String]] = []
|
||||
for branch in plan.branches {
|
||||
var values: [String]
|
||||
switch branch.mode {
|
||||
case .css:
|
||||
values = try HTMLRuleEvaluator().extractCSS(
|
||||
branch.expression,
|
||||
html: content.stringValue,
|
||||
baseURL: executionContext.baseURL,
|
||||
isFragment: executionContext.isHTMLFragment
|
||||
)
|
||||
case .xpath:
|
||||
values = try HTMLRuleEvaluator().extractXPath(branch.expression, html: content.stringValue, isFragment: executionContext.isHTMLFragment)
|
||||
case .jsonPath:
|
||||
let json: JSONValue
|
||||
if case .string(let text) = content {
|
||||
json = try JSONValue.decode(data: Data(text.utf8))
|
||||
} else {
|
||||
json = content
|
||||
}
|
||||
let path = branch.expression.hasPrefix("$") ? branch.expression : "." + branch.expression
|
||||
let normalizedPath = path.hasPrefix("$") ? path : "$" + path
|
||||
values = try JSONPathEvaluator().evaluate(path: normalizedPath, in: json)
|
||||
.map(\.stringValue)
|
||||
case .regex:
|
||||
values = try RegexEvaluator().captureGroups(pattern: branch.expression, input: content.stringValue)
|
||||
.map { $0.count > 1 ? $0.dropFirst().joined() : $0.first ?? "" }
|
||||
case .javaScript:
|
||||
values = [try evaluate(
|
||||
script: branch.expression,
|
||||
result: content,
|
||||
executionContext: executionContext
|
||||
).value.stringValue]
|
||||
case .webJavaScript:
|
||||
throw RuleEngineError.unsupportedRule(rule: branch.expression, feature: "java.getString WebView JavaScript")
|
||||
}
|
||||
if let pattern = branch.replacePattern, !pattern.isEmpty {
|
||||
values = try values.map {
|
||||
try RegexEvaluator().replacing(
|
||||
pattern: pattern,
|
||||
in: $0,
|
||||
with: branch.replacement,
|
||||
firstOnly: branch.replaceFirst
|
||||
if branch.expression.isEmpty {
|
||||
values = [content.stringValue]
|
||||
} else {
|
||||
switch branch.mode {
|
||||
case .css:
|
||||
values = try HTMLRuleEvaluator().extractCSS(
|
||||
branch.expression,
|
||||
html: content.stringValue,
|
||||
baseURL: executionContext.baseURL,
|
||||
isFragment: executionContext.isHTMLFragment
|
||||
)
|
||||
case .xpath:
|
||||
values = try HTMLRuleEvaluator().extractXPath(branch.expression, html: content.stringValue, isFragment: executionContext.isHTMLFragment)
|
||||
case .jsonPath:
|
||||
let json: JSONValue
|
||||
if case .string(let text) = content {
|
||||
json = try JSONValue.decode(data: Data(text.utf8))
|
||||
} else {
|
||||
json = content
|
||||
}
|
||||
values = RuleEvaluation(values: try JSONPathEvaluator().evaluateRule(branch.expression, in: json)).strings
|
||||
case .regex:
|
||||
values = try RegexEvaluator().captureGroups(pattern: branch.expression, input: content.stringValue)
|
||||
.map { $0.count > 1 ? $0.dropFirst().joined() : $0.first ?? "" }
|
||||
case .javaScript:
|
||||
values = [try evaluate(
|
||||
script: branch.expression,
|
||||
result: content,
|
||||
executionContext: executionContext
|
||||
).value.stringValue]
|
||||
case .webJavaScript:
|
||||
throw RuleEngineError.unsupportedRule(rule: branch.expression, feature: "java.getString WebView JavaScript")
|
||||
}
|
||||
}
|
||||
if plan.combination == .fallback, values.joined(separator: "\n").isEmpty { values = [] }
|
||||
branchResults.append(values)
|
||||
if plan.combination == .fallback, !values.isEmpty { break }
|
||||
}
|
||||
@@ -1037,7 +1079,11 @@ public struct JavaScriptRuleEvaluator: Sendable {
|
||||
branchResults.compactMap { $0.indices.contains(index) ? $0[index] : nil }
|
||||
}
|
||||
}
|
||||
return resolveJavaScriptRuleURL(combined.joined(separator: "\n"), enabled: isURL, context: executionContext)
|
||||
var result = combined.joined(separator: "\n")
|
||||
if let branch = plan.branches.first, let pattern = branch.replacePattern, !pattern.isEmpty {
|
||||
result = try RegexEvaluator().replacing(pattern: pattern, in: result, with: branch.replacement, firstOnly: branch.replaceFirst)
|
||||
}
|
||||
return resolveJavaScriptRuleURL(result, enabled: isURL, context: executionContext)
|
||||
}
|
||||
|
||||
private func resolveJavaScriptRuleURL(
|
||||
|
||||
@@ -112,6 +112,25 @@ public struct RegexEvaluator: Sendable {
|
||||
|
||||
public func replacingStrict(pattern: String, in input: String, with replacement: String,
|
||||
timeoutMilliseconds: Int64 = 3_000) throws -> String {
|
||||
try replacingStrict(pattern: pattern, in: input, timeoutMilliseconds: timeoutMilliseconds) { expression, match in
|
||||
expression.replacementString(for: match, in: input, offset: 0, template: replacement)
|
||||
}
|
||||
}
|
||||
|
||||
/// Legado's JS replacement still expands capture references, while quoting
|
||||
/// backslashes returned by the script (quoteReplacementJs in the reference).
|
||||
func replacingStrict(pattern: String, in input: String, timeoutMilliseconds: Int64 = 3_000,
|
||||
replacement: (String) throws -> String) throws -> String {
|
||||
let original = input as NSString
|
||||
return try replacingStrict(pattern: pattern, in: input, timeoutMilliseconds: timeoutMilliseconds) { expression, match in
|
||||
let template = try replacement(original.substring(with: match.range))
|
||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||
return expression.replacementString(for: match, in: input, offset: 0, template: template)
|
||||
}
|
||||
}
|
||||
|
||||
private func replacingStrict(pattern: String, in input: String, timeoutMilliseconds: Int64,
|
||||
replacement: (NSRegularExpression, NSTextCheckingResult) throws -> String) throws -> String {
|
||||
guard pattern.count <= limits.maximumPatternLength, input.utf16.count <= limits.maximumInputLength,
|
||||
!hasObviousNestedQuantifier(pattern) else {
|
||||
throw RuleEngineError.resourceLimit(rule: pattern, detail: "正则超过资源限制或包含高风险嵌套量词")
|
||||
@@ -145,30 +164,51 @@ public struct RegexEvaluator: Sendable {
|
||||
throw RuleEngineError.resourceLimit(rule: pattern, detail: "正则替换超时(\(milliseconds) ms)")
|
||||
}
|
||||
let untouched = original.substring(with: NSRange(location: offset, length: match.range.location - offset))
|
||||
let replaced = expression.replacementString(for: match, in: input, offset: 0, template: replacement)
|
||||
output += untouched + replaced
|
||||
let replaced = try replacement(expression, match)
|
||||
guard DispatchTime.now().uptimeNanoseconds < deadline else {
|
||||
throw RuleEngineError.resourceLimit(rule: pattern, detail: "正则替换超时(\(milliseconds) ms)")
|
||||
}
|
||||
outputLength += untouched.utf16.count + replaced.utf16.count
|
||||
offset = NSMaxRange(match.range)
|
||||
guard outputLength <= limits.maximumInputLength * 2 else {
|
||||
throw RuleEngineError.resourceLimit(rule: pattern, detail: "替换结果超过长度限制")
|
||||
}
|
||||
output += untouched + replaced
|
||||
}
|
||||
output += original.substring(from: offset)
|
||||
let tail = original.substring(from: offset)
|
||||
guard outputLength + tail.utf16.count <= limits.maximumInputLength * 2 else {
|
||||
throw RuleEngineError.resourceLimit(rule: pattern, detail: "替换结果超过长度限制")
|
||||
}
|
||||
output += tail
|
||||
return output
|
||||
}
|
||||
|
||||
private func hasObviousNestedQuantifier(_ pattern: String) -> Bool {
|
||||
let risky = #"\(((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*)\)([+*]|\{[^}]+\})"#
|
||||
guard let expression = try? NSRegularExpression(pattern: risky) else { return false }
|
||||
return expression.matches(in: pattern, range: NSRange(pattern.startIndex..., in: pattern)).contains { match in
|
||||
guard let innerRange = Range(match.range(at: 1), in: pattern),
|
||||
let outerRange = Range(match.range(at: 2), in: pattern) else { return true }
|
||||
let inner = String(pattern[innerRange])
|
||||
let outer = String(pattern[outerRange])
|
||||
if outer.range(of: #"^\{[1-9]\}$"#, options: .regularExpression) != nil { return false }
|
||||
if !pattern.contains("(?s"),
|
||||
inner.range(of: #"^(?:\\n[^\[\]()*+?\\]*\.\*|\.\+\\n)$"#, options: .regularExpression) != nil { return false }
|
||||
return true
|
||||
// Reject clear cases such as (a+)+ and (\w*)*. A '+' inside a character
|
||||
// class/escape is not a quantifier, and bounded or delimited repeated
|
||||
// groups are not proof of catastrophic backtracking. Other expressions
|
||||
// remain protected by enumerateMatches' progress/deadline checks.
|
||||
let atom = #"^(?:\?:)?(?:\\[dDsSwW]|\\[^pPkKgG0-9]|\[(?:\\.|[^\]\\])*\]|[^\[\]()\\?*+{}|^$])(?:[+*]|\{[0-9]+,\})\??$"#
|
||||
guard let repeatedAtom = try? NSRegularExpression(pattern: atom),
|
||||
let unbounded = try? NSRegularExpression(pattern: #"^(?:[+*]|\{[0-9]+,\})(?!\+)"#) else { return false }
|
||||
var groups: [String.Index] = []
|
||||
var classDepth = 0
|
||||
var escaped = false
|
||||
for index in pattern.indices {
|
||||
let character = pattern[index]
|
||||
if escaped { escaped = false; continue }
|
||||
if character == "\\" { escaped = true; continue }
|
||||
if character == "[" { classDepth += 1; continue }
|
||||
if character == "]", classDepth > 0 { classDepth -= 1; continue }
|
||||
guard classDepth == 0 else { continue }
|
||||
if character == "(" { groups.append(pattern.index(after: index)) }
|
||||
if character == ")", let start = groups.popLast() {
|
||||
let inner = String(pattern[start..<index])
|
||||
let suffix = String(pattern[pattern.index(after: index)...])
|
||||
if repeatedAtom.firstMatch(in: inner, range: NSRange(inner.startIndex..., in: inner)) != nil,
|
||||
unbounded.firstMatch(in: suffix, range: NSRange(suffix.startIndex..., in: suffix)) != nil { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ public struct RuleEngine: Sendable {
|
||||
return literalEvaluation(rendered.rule, context: rendered.context)
|
||||
}
|
||||
if rendered.context.variables != context.variables {
|
||||
var result = await evaluatePrepared(rendered.rule, in: input, context: rendered.context)
|
||||
var result = await evaluatePrepared(rendered.rule, in: input, listMode: listMode, context: rendered.context)
|
||||
result.variables = rendered.context.variables.merging(result.variables) { _, new in new }
|
||||
return result
|
||||
}
|
||||
return await evaluatePrepared(rendered.rule, in: input, context: rendered.context)
|
||||
return await evaluatePrepared(rendered.rule, in: input, listMode: listMode, context: rendered.context)
|
||||
} catch let error as RuleEngineError {
|
||||
return .init(diagnostics: [error.diagnostic], variables: context.variables)
|
||||
} catch {
|
||||
@@ -72,6 +72,7 @@ public struct RuleEngine: Sendable {
|
||||
private func evaluatePrepared(
|
||||
_ rule: String,
|
||||
in input: JSONValue,
|
||||
listMode: Bool,
|
||||
context: RuleExecutionContext
|
||||
) async -> RuleEvaluation {
|
||||
do {
|
||||
@@ -80,7 +81,8 @@ public struct RuleEngine: Sendable {
|
||||
var diagnostics: [RuleDiagnostic] = []
|
||||
for branch in plan.branches {
|
||||
do {
|
||||
let values = try await evaluate(branch, in: input, context: context)
|
||||
var values = try await evaluate(branch, in: input, context: context)
|
||||
if plan.combination == .fallback, isEmpty(values, listMode: listMode) { values = [] }
|
||||
results.append(values)
|
||||
if plan.combination == .fallback, !values.isEmpty { break }
|
||||
} catch let error as RuleEngineError {
|
||||
@@ -97,7 +99,7 @@ public struct RuleEngine: Sendable {
|
||||
}
|
||||
}
|
||||
return .init(
|
||||
values: combine(results, with: plan.combination),
|
||||
values: try applyingReplacement(to: combine(results, with: plan.combination), plan: plan, listMode: listMode),
|
||||
diagnostics: diagnostics,
|
||||
variables: context.variables
|
||||
)
|
||||
@@ -174,6 +176,11 @@ public struct RuleEngine: Sendable {
|
||||
listMode: Bool,
|
||||
context: RuleExecutionContext
|
||||
) async -> RuleEvaluation {
|
||||
if let root = try? JSONValue.decode(data: Data(input.utf8)),
|
||||
let plan = try? parser.parse(rule, inputLooksLikeJSON: true),
|
||||
plan.branches.allSatisfy({ $0.mode == .jsonPath }) {
|
||||
return await evaluatePrepared(rule, in: root, listMode: listMode, context: context)
|
||||
}
|
||||
if rule.hasPrefix(":"), rule.count > 1 {
|
||||
do {
|
||||
let patterns = rule.dropFirst().components(separatedBy: "&&").filter { !$0.isEmpty }
|
||||
@@ -217,7 +224,7 @@ public struct RuleEngine: Sendable {
|
||||
}
|
||||
}
|
||||
return .init(
|
||||
values: combine(results, with: plan.combination),
|
||||
values: try applyingReplacement(to: combine(results, with: plan.combination), plan: plan, listMode: listMode),
|
||||
diagnostics: diagnostics,
|
||||
variables: context.variables
|
||||
)
|
||||
@@ -241,15 +248,11 @@ public struct RuleEngine: Sendable {
|
||||
try Task.checkCancellation()
|
||||
let rawValues: [JSONValue]
|
||||
if branch.expression.isEmpty {
|
||||
return try [JSONValue.string(regex.replacing(
|
||||
pattern: branch.replacePattern ?? "", in: input.stringValue,
|
||||
with: branch.replacement, firstOnly: branch.replaceFirst
|
||||
))]
|
||||
return [input]
|
||||
}
|
||||
switch branch.mode {
|
||||
case .jsonPath:
|
||||
let path = branch.expression.hasPrefix("$") ? branch.expression : "$.\(branch.expression)"
|
||||
rawValues = try jsonPath.evaluate(path: path, in: input)
|
||||
rawValues = try jsonPath.evaluateRule(branch.expression, in: input)
|
||||
case .regex:
|
||||
let matches = try regex.captureGroups(pattern: branch.expression, input: input.stringValue)
|
||||
rawValues = matches.map { .array($0.map(JSONValue.string)) }
|
||||
@@ -266,15 +269,7 @@ public struct RuleEngine: Sendable {
|
||||
case .webJavaScript:
|
||||
throw RuleEngineError.unsupportedRule(rule: branch.expression, feature: "WKWebView JavaScript")
|
||||
}
|
||||
guard let pattern = branch.replacePattern, !pattern.isEmpty else { return rawValues }
|
||||
return try rawValues.map {
|
||||
.string(try regex.replacing(
|
||||
pattern: pattern,
|
||||
in: $0.stringValue,
|
||||
with: branch.replacement,
|
||||
firstOnly: branch.replaceFirst
|
||||
))
|
||||
}
|
||||
return rawValues
|
||||
}
|
||||
|
||||
private func evaluateTextBranch(
|
||||
@@ -287,10 +282,7 @@ public struct RuleEngine: Sendable {
|
||||
try Task.checkCancellation()
|
||||
let raw: [String]
|
||||
if branch.expression.isEmpty {
|
||||
return try [regex.replacing(
|
||||
pattern: branch.replacePattern ?? "", in: input,
|
||||
with: branch.replacement, firstOnly: branch.replaceFirst
|
||||
)]
|
||||
return [input]
|
||||
}
|
||||
switch branch.mode {
|
||||
case .css:
|
||||
@@ -308,8 +300,7 @@ public struct RuleEngine: Sendable {
|
||||
} catch {
|
||||
throw RuleEngineError.invalidInput(rule: branch.expression, expected: "JSON")
|
||||
}
|
||||
let path = branch.expression.hasPrefix("$") ? branch.expression : "$.\(branch.expression)"
|
||||
raw = try jsonPath.evaluate(path: path, in: root).map(\.stringValue)
|
||||
raw = try jsonPath.evaluateRule(branch.expression, in: root).map(\.stringValue)
|
||||
case .regex:
|
||||
raw = try regex.captureGroups(pattern: branch.expression, input: input).map { captures in
|
||||
captures.count > 1 ? captures.dropFirst().joined() : captures.first ?? ""
|
||||
@@ -323,14 +314,22 @@ public struct RuleEngine: Sendable {
|
||||
case .webJavaScript:
|
||||
throw RuleEngineError.unsupportedRule(rule: branch.expression, feature: "WKWebView JavaScript")
|
||||
}
|
||||
guard let pattern = branch.replacePattern, !pattern.isEmpty else { return raw }
|
||||
return try raw.map {
|
||||
try regex.replacing(
|
||||
pattern: pattern,
|
||||
in: $0,
|
||||
with: branch.replacement,
|
||||
firstOnly: branch.replaceFirst
|
||||
)
|
||||
return raw
|
||||
}
|
||||
|
||||
private func isEmpty(_ values: [JSONValue], listMode: Bool) -> Bool {
|
||||
let result = RuleEvaluation(values: values)
|
||||
return listMode
|
||||
? result.listItems(rule: "").filter { $0 != .null }.isEmpty
|
||||
: result.strings.joined(separator: "\n").isEmpty
|
||||
}
|
||||
|
||||
private func applyingReplacement(to values: [JSONValue], plan: RulePlan, listMode: Bool) throws -> [JSONValue] {
|
||||
guard let branch = plan.branches.first, let pattern = branch.replacePattern, !pattern.isEmpty else { return values }
|
||||
let strings = RuleEvaluation(values: values).strings
|
||||
let inputs = listMode ? strings : [strings.joined(separator: "\n")]
|
||||
return try inputs.map {
|
||||
.string(try regex.replacing(pattern: pattern, in: $0, with: branch.replacement, firstOnly: branch.replaceFirst))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,12 @@ public struct RuleExpressionParser: Sendable {
|
||||
guard !normalized.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
throw RuleEngineError.malformedRule(rule: source, detail: "规则不能为空")
|
||||
}
|
||||
let split = try splitAtTopLevel(normalized)
|
||||
let branches = try split.parts.map { try parseBranch($0, inputLooksLikeJSON: inputLooksLikeJSON) }
|
||||
// ## belongs to the whole selector stage. Regex/replacement text may
|
||||
// contain quotes, brackets and && that are not selector operators.
|
||||
let replacementStart = normalized.range(of: "##")?.lowerBound ?? normalized.endIndex
|
||||
let split = try splitAtTopLevel(String(normalized[..<replacementStart]))
|
||||
let suffix = String(normalized[replacementStart...])
|
||||
let branches = try split.parts.map { try parseBranch($0 + suffix, inputLooksLikeJSON: inputLooksLikeJSON) }
|
||||
return .init(source: source, combination: split.combination, branches: branches)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user