mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 21:01:22 +08:00
支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。 修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。 更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
142 lines
7.2 KiB
Swift
142 lines
7.2 KiB
Swift
import Foundation
|
||
import Testing
|
||
@testable import LegadoCore
|
||
|
||
struct ReplaceRuleExecutionTests {
|
||
private func process(_ content: String, rules: [ReplaceRule], title: String = "第十二章") -> ContentProcessingReport {
|
||
let book = Book(bookUrl: "book", name: "测试书", origin: "https://source.test")
|
||
let chapter = BookChapter(url: "chapter", title: title, bookUrl: book.bookUrl, index: 11)
|
||
return ContentSanitizer().process(book: book, chapter: chapter, content: content, rules: rules,
|
||
removeDuplicateTitle: false, resegment: false)
|
||
}
|
||
|
||
@Test func evaluatesJavaScriptForEveryMatchIncludingZeroWidthMatches() {
|
||
let width = ReplaceRule(id: 1, name: "全角", pattern: "[A-Z0-9]",
|
||
replacement: "@js:String.fromCharCode(result.charCodeAt(0) - 65248)")
|
||
var punctuation = ReplaceRule(id: 2, name: "标点", pattern: #"(?<=说道)(?=“)|\.{3,}"#,
|
||
replacement: #"@js:if (result === '') { ':' } else { '…' }"#)
|
||
punctuation.sortOrder = 1
|
||
let result = process("ABC123\n他说道“等待...”", rules: [punctuation, width])
|
||
#expect(result.content == "ABC123\n他说道:“等待…”")
|
||
#expect(result.effectiveRuleIDs == [1, 2])
|
||
#expect(result.diagnostics.isEmpty)
|
||
}
|
||
|
||
@Test func bindsBookAndChapterAndSharesOnlyJavaVariablesBetweenMatches() {
|
||
let rule = ReplaceRule(id: 1, name: "上下文", pattern: "[ab]", replacement: """
|
||
@js:n = typeof n === 'undefined' ? 1 : n + 1;
|
||
java.put('count', String(Number(java.get('count') || 0) + 1));
|
||
[book.name, book.getName(), chapter.title, chapter.getIndex(), result, n, java.get('count')].join(':')
|
||
""")
|
||
let report = process("a b", rules: [rule])
|
||
#expect(report.content == "测试书:测试书:第十二章:11:a:1:1 测试书:测试书:第十二章:11:b:1:2")
|
||
#expect(report.diagnostics.isEmpty)
|
||
// A new application of the same rule must not inherit another chapter's variables.
|
||
#expect(process("a", rules: [rule]).content == "测试书:测试书:第十二章:11:a:1:1")
|
||
}
|
||
|
||
@Test(arguments: [
|
||
(#"'$1'"#, "x"),
|
||
(#"String.fromCharCode(92) + 'n'"#, #"\n"#),
|
||
("null", "null"),
|
||
("undefined", "null"),
|
||
("['a', 'b']", "a,b"),
|
||
("123", "123.0"),
|
||
("false", "false")
|
||
])
|
||
func convertsScriptCompletionAndPreservesReplacementEscaping(_ sample: (String, String)) {
|
||
let rule = ReplaceRule(id: 1, name: "返回值", pattern: "(x)", replacement: "@js:" + sample.0)
|
||
let report = process("x", rules: [rule])
|
||
#expect(report.content == sample.1)
|
||
#expect(report.diagnostics.isEmpty)
|
||
}
|
||
|
||
@Test func failedScriptDiscardsAllPartialReplacementsAndLaterRulesStillRun() {
|
||
let failing = ReplaceRule(id: 1, name: "失败", pattern: "[ab]",
|
||
replacement: "@js:if (result === 'b') { throw new Error('bad match') }; 'changed'")
|
||
var following = ReplaceRule(id: 2, name: "后续", pattern: "tail", replacement: "后续成功")
|
||
following.sortOrder = 1
|
||
let report = process("ab tail", rules: [failing, following])
|
||
#expect(report.content == "ab 后续成功")
|
||
#expect(report.effectiveRuleIDs == [2])
|
||
#expect(report.diagnostics.count == 1)
|
||
#expect(report.diagnostics.first?.code == "rule.malformed")
|
||
}
|
||
|
||
@Test func skipsDisabledExcludedAndUnmatchedScriptsAndKeepsLiteralReplacementsLiteral() {
|
||
var excluded = ReplaceRule(id: 1, name: "排除", pattern: "x", replacement: "@js:throw new Error('excluded')")
|
||
excluded.excludeScope = "测试书"
|
||
var disabled = excluded
|
||
disabled.id = 2
|
||
disabled.excludeScope = nil
|
||
disabled.isEnabled = false
|
||
let unmatched = ReplaceRule(id: 3, name: "未匹配", pattern: "z", replacement: "@js:throw new Error('unmatched')")
|
||
var literal = ReplaceRule(id: 4, name: "普通文本", pattern: "x", replacement: "@js:'literal'")
|
||
literal.isRegex = false
|
||
let report = process("x", rules: [excluded, disabled, unmatched, literal])
|
||
#expect(report.content == "@js:'literal'")
|
||
#expect(report.diagnostics.isEmpty)
|
||
}
|
||
|
||
@Test func preservesLastNonblankTitleButAllowsDeletingBody() {
|
||
var rename = ReplaceRule(id: 1, name: "标题", pattern: "十二", replacement: "12")
|
||
rename.scopeTitle = true
|
||
rename.scopeContent = false
|
||
var erase = ReplaceRule(id: 2, name: "空白", pattern: "^.+$", replacement: "@js:' \\n'")
|
||
erase.scopeTitle = true
|
||
erase.sortOrder = 1
|
||
let report = process("正文", rules: [erase, rename])
|
||
#expect(report.title == "第12章")
|
||
#expect(report.content == " \n")
|
||
#expect(report.effectiveRuleIDs == [1, 2])
|
||
erase.replacement = ""
|
||
erase.scopeContent = false
|
||
let titleOnly = process("正文", rules: [erase])
|
||
#expect(titleOnly.title == "第十二章")
|
||
#expect(titleOnly.effectiveRuleIDs.isEmpty)
|
||
}
|
||
|
||
@Test(arguments: [
|
||
(#"(?:\n[^\n]+){0,15}$"#, "正文\nPS:说明\n感谢支持", "正文"),
|
||
(#"^(?:[^\n\w]\w+){0,2}"#, ",第一章,开始\n正文", "\n正文"),
|
||
(#""((?:[\w,。]|‘\w+’|\b[%\+\-])+)["]"#, ""你好,世界。"", "“你好,世界。”"),
|
||
(#"(?:[+*]|\\)+"#, #"+*\"#, "")
|
||
])
|
||
func acceptsBoundedDelimitedAndEscapedQuantifiers(_ sample: (String, String, String)) throws {
|
||
let replacement = sample.0.hasPrefix(""") ? "“$1”" : ""
|
||
#expect(try RegexEvaluator().replacingStrict(pattern: sample.0, in: sample.1, with: replacement) == sample.2)
|
||
}
|
||
|
||
@Test(arguments: ["(a+)+$", #"(\w*)*"#, "(?:[ab]+){2,}"])
|
||
func retainsChecksForClearlyNestedUnboundedRepetition(_ pattern: String) {
|
||
#expect(throws: RuleEngineError.self) {
|
||
try RegexEvaluator().replacingStrict(pattern: pattern, in: "aaa!", with: "changed")
|
||
}
|
||
}
|
||
|
||
@Test func dynamicReplacementRespectsMatchOutputAndWholeOperationTimeLimits() throws {
|
||
let limited = RegexEvaluator(limits: .init(maximumInputLength: 4, maximumMatches: 2))
|
||
#expect(throws: RuleEngineError.self) {
|
||
try limited.replacingStrict(pattern: "a", in: "aaa", replacement: { _ in "b" })
|
||
}
|
||
#expect(try limited.replacingStrict(pattern: "a", in: "aa", replacement: { _ in "b" }) == "bb")
|
||
// Include untouched tail in the output limit, not only replaced spans.
|
||
#expect(throws: RuleEngineError.self) {
|
||
try limited.replacingStrict(pattern: "a", in: "abcd", replacement: { _ in "1234567" })
|
||
}
|
||
#expect(throws: RuleEngineError.self) {
|
||
try limited.replacingStrict(pattern: "a", in: "a", timeoutMilliseconds: 1, replacement: { _ in
|
||
Thread.sleep(forTimeInterval: 0.02)
|
||
return "b"
|
||
})
|
||
}
|
||
}
|
||
|
||
@Test func rejectsKnownUnboundedJavaScriptAndReportsErrorWithoutChangingInput() {
|
||
let rule = ReplaceRule(id: 1, name: "循环", pattern: "x", replacement: "@js:while(true){}")
|
||
let report = process("x", rules: [rule])
|
||
#expect(report.content == "x")
|
||
#expect(report.diagnostics.first?.code == "rule.resource_limit")
|
||
}
|
||
}
|