mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 12:51:22 +08:00
feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性
支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。 修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。 更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import LegadoCore
|
||||
|
||||
// All responses and hosts are synthetic; these tests never contact source websites.
|
||||
struct SampledSourceRegressionTests {
|
||||
@Test func JavaScriptNumbersZeroAndOneRemainNumeric() throws {
|
||||
let evaluator = JavaScriptRuleEvaluator()
|
||||
let value = try evaluator.evaluate(script: "({zero: 0, one: 1, yes: true, no: false, list: [0, 1, true]})", result: .null, executionContext: .init()).value
|
||||
#expect(value["zero"] == .number(0))
|
||||
#expect(value["one"] == .number(1))
|
||||
#expect(value["yes"] == .bool(true))
|
||||
#expect(value["no"] == .bool(false))
|
||||
#expect(value["list"] == .array([.number(0), .number(1), .bool(true)]))
|
||||
}
|
||||
|
||||
@Test func JavaScriptGetStringUsesTheSameJSONTemplateAndReplacementSemantics() async throws {
|
||||
let input = try JSONValue.decode(data: Data(#"{"id":42,"name":"测试小说","alternate":"备用小说"}"#.utf8))
|
||||
let value = await RuleEngine().evaluate("@js:java.getString('/book/{$.id}') + ':' + java.getString('$.name||$.alternate##小说')", in: input)
|
||||
#expect(value.diagnostics.isEmpty)
|
||||
#expect(value.strings == ["/book/42:测试"])
|
||||
}
|
||||
|
||||
@Test func legacyJSONTemplatesKeepEachSearchBookURLDistinct() async throws {
|
||||
var source = BookSource(bookSourceUrl: "https://fixture.invalid", bookSourceName: "旧式 JSON 模板")
|
||||
source.searchUrl = "/search?word={{key}}"
|
||||
source.ruleSearch = .init(bookList: "data.bookList", name: "name", bookUrl: "/pc/book/{$.id}/catalog")
|
||||
let outcome = await BookSearchService(transport: SampledResponseTransport(body:
|
||||
#"{"data":{"bookList":[{"id":42,"name":"测试甲"},{"id":43,"name":"测试乙"}]}}"#
|
||||
)).search(keyword: "测试", page: 1, source: source)
|
||||
#expect(outcome.diagnostics.isEmpty)
|
||||
#expect(outcome.books.map(\.bookURL) == ["https://fixture.invalid/pc/book/42/catalog", "https://fixture.invalid/pc/book/43/catalog"])
|
||||
}
|
||||
|
||||
@Test func legacyJSONRootWildcardAndRecursiveURLTemplate() async throws {
|
||||
var source = BookSource(bookSourceUrl: "https://fixture.invalid", bookSourceName: "旧式 JSON 列表")
|
||||
source.searchUrl = "/search?word={{key}}"
|
||||
source.ruleSearch = .init(bookList: "$.[*]", name: "$..name", bookUrl: "https://fixture.invalid{$..url}")
|
||||
let outcome = await BookSearchService(transport: SampledResponseTransport(body:
|
||||
#"[{"name":"测试甲","url":"/book/42"},{"name":"测试乙","url":"/book/43"}]"#
|
||||
)).search(keyword: "测试", page: 1, source: source)
|
||||
#expect(outcome.diagnostics.isEmpty)
|
||||
#expect(outcome.books.map(\.bookURL) == ["https://fixture.invalid/book/42", "https://fixture.invalid/book/43"])
|
||||
}
|
||||
|
||||
@Test func legacyJSONTemplatesRespectQuotedBracesAndReplacement() async throws {
|
||||
let input = try JSONValue.decode(data: Data(#"{"id":42,"words":1200,"meta":{"label}":"玄幻"},"tags":["甲","乙"]}"#.utf8))
|
||||
let value = await RuleEngine().evaluate(#"{$.meta['label}']}/{$.words}字/{$.tags[*]}##1200##1.2千"#, in: input)
|
||||
#expect(value.diagnostics.isEmpty)
|
||||
#expect(value.strings == ["玄幻/1.2千字/甲\n乙"])
|
||||
let fromText = await RuleEngine().evaluate("/book/{$.id}", inText: input.stringValue)
|
||||
#expect(fromText.diagnostics.isEmpty)
|
||||
#expect(fromText.strings == ["/book/42"])
|
||||
}
|
||||
|
||||
@Test func JSONFallbackSkipsEmptyArraysAndEmptyNames() async throws {
|
||||
let input = try JSONValue.decode(data: Data(#"{"empty":[],"books":[{"name":"备用书"}],"name":"","alternate":"备用书","zero":0}"#.utf8))
|
||||
for useText in [false, true] {
|
||||
let list = if useText {
|
||||
await RuleEngine().evaluate("$.empty||$.books", inText: input.stringValue, listMode: true)
|
||||
} else {
|
||||
await RuleEngine().evaluate("$.empty||$.books", in: input, listMode: true)
|
||||
}
|
||||
#expect(list.diagnostics.isEmpty)
|
||||
#expect(list.listItems(rule: "$.empty||$.books").count == 1)
|
||||
let name = await RuleEngine().evaluate("$.name||$.alternate", in: input)
|
||||
#expect(name.strings == ["备用书"])
|
||||
let zero = await RuleEngine().evaluate("$.zero||$.alternate", in: input)
|
||||
#expect(zero.strings == ["0"])
|
||||
}
|
||||
}
|
||||
|
||||
@Test func replacementAppliesToTheCombinedSelectorResult() async throws {
|
||||
// Search names in the sampled fallback sources and multi-part metadata
|
||||
// use one trailing ## expression for the entire selector combination.
|
||||
let input = try JSONValue.decode(data: Data(#"{"name":"测试小说","fallback":"备用小说","a":"A1","b":"B2"}"#.utf8))
|
||||
let fallback = await RuleEngine().evaluate("$.name||$.fallback##小说", in: input)
|
||||
#expect(fallback.strings == ["测试"])
|
||||
let combined = await RuleEngine().evaluate("$.a&&$.b##[0-9]", in: input)
|
||||
#expect(combined.strings.joined(separator: "\n") == "A\nB")
|
||||
let anchored = await RuleEngine().evaluate(#"$.a&&$.b##^A1\nB2$##整段"#, in: input)
|
||||
#expect(anchored.strings == ["整段"])
|
||||
let list = await RuleEngine().evaluate("$.a&&$.b##[0-9]", in: input, listMode: true)
|
||||
#expect(list.strings == ["A", "B"])
|
||||
let html = await RuleEngine().evaluate("h1@text||h2@text##小说", inText: "<h1>测试小说</h1>")
|
||||
#expect(html.strings == ["测试"])
|
||||
}
|
||||
|
||||
@Test func replacementTextIsNotParsedAsSelectorSyntax() async {
|
||||
let result = await RuleEngine().evaluate(#"h1@text##["']|[()]"#, inText: "<h1>书名(测试)'</h1>")
|
||||
#expect(result.diagnostics.isEmpty)
|
||||
#expect(result.strings == ["书名测试"])
|
||||
let replacement = await RuleEngine().evaluate("h1@text##书##A&&B", inText: "<h1>书</h1>")
|
||||
#expect(replacement.diagnostics.isEmpty)
|
||||
#expect(replacement.strings == ["A&&B"])
|
||||
}
|
||||
}
|
||||
|
||||
private struct SampledResponseTransport: HTTPTransport {
|
||||
let body: String
|
||||
func data(for request: URLRequest) async throws -> HTTPResponse {
|
||||
.init(data: Data(body.utf8), statusCode: 200, headers: [:], finalURL: try #require(request.url))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user