mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 21:01:22 +08:00
feat: open-source 小浣熊阅读 for iOS
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import LegadoCore
|
||||
|
||||
struct LocalBookServiceTests {
|
||||
@Test func decodesDefaultTocRuleWithMissingReplacement() throws {
|
||||
let data = Data(#"{"id":-1,"name":"目录","rule":"^第.+章$","serialNumber":0,"enable":true}"#.utf8)
|
||||
let value = try JSONDecoder().decode(TxtTocRule.self, from: data)
|
||||
#expect(value.replacement == "")
|
||||
#expect(value.example == nil)
|
||||
#expect(value.enable)
|
||||
}
|
||||
|
||||
@Test func importsUTF8TextSplitsChaptersAndReadsContent() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "测试书.txt")
|
||||
let sourceData = Data("序言\n第一章 相遇\n这是第一章正文\n第二章 远行\n这是第二章正文".utf8)
|
||||
try sourceData.write(to: source)
|
||||
|
||||
let service = LocalBookService()
|
||||
let report = try service.importTextFile(at: source, into: root.appending(path: "library"))
|
||||
|
||||
#expect(report.book.origin == "local")
|
||||
#expect(report.book.charset == "UTF-8")
|
||||
#expect(report.chapters.map(\.title) == ["前言", "第一章 相遇", "第二章 远行"])
|
||||
#expect(report.chapters[1].url == "82e56047094237f3")
|
||||
let firstTitle = try #require(sourceData.range(of: Data("第一章 相遇".utf8)))
|
||||
let secondTitle = try #require(sourceData.range(of: Data("第二章 远行".utf8)))
|
||||
#expect(report.chapters[1].start == Int64(firstTitle.upperBound))
|
||||
#expect(report.chapters[1].end == Int64(secondTitle.lowerBound))
|
||||
#expect(report.chapters.map(\.wordCount) == ["3字", "9字", "8字"])
|
||||
#expect(report.book.wordCount == "20字")
|
||||
#expect(report.book.intro == "序言\n")
|
||||
let content = try service.loadContent(book: report.book, chapter: report.chapters[1])
|
||||
#expect(content.content == " 这是第一章正文\n")
|
||||
#expect(FileManager.default.fileExists(atPath: try #require(URL(string: report.book.bookUrl)).path))
|
||||
}
|
||||
|
||||
@Test func createsSingleChapterWhenNoHeadingExists() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "短篇.txt")
|
||||
try Data("这是一个没有章节标题的短篇。".utf8).write(to: source)
|
||||
|
||||
let report = try LocalBookService().importTextFile(at: source, into: root.appending(path: "library"))
|
||||
|
||||
#expect(report.chapters.count == 1)
|
||||
#expect(report.chapters[0].title == "短篇")
|
||||
#expect(try LocalBookService().loadContent(book: report.book, chapter: report.chapters[0]).content == "这是一个没有章节标题的短篇。")
|
||||
}
|
||||
|
||||
@Test func splitsLongHeadinglessTextAtTenKiBThreshold() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "无目录长文.txt")
|
||||
let paragraph = String(repeating: "a", count: 10_500)
|
||||
try Data("\(paragraph)\n\(paragraph)\n尾声".utf8).write(to: source)
|
||||
|
||||
let report = try LocalBookService(tocRules: []).importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library")
|
||||
)
|
||||
|
||||
#expect(report.chapters.count == 2)
|
||||
#expect(report.chapters.map(\.title) == ["第1章(1)", "第1章(2)"])
|
||||
#expect(report.chapters[0].end == report.chapters[1].start)
|
||||
#expect(report.book.wordCount == "2.1万字")
|
||||
#expect(report.book.intro == nil)
|
||||
}
|
||||
|
||||
@Test func numbersHeadinglessChaptersBy512KBBlock() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "多块无目录.txt")
|
||||
let paragraph = String(repeating: "a", count: 10_500) + "\n"
|
||||
try Data(String(repeating: paragraph, count: 55).utf8).write(to: source)
|
||||
|
||||
let report = try LocalBookService(tocRules: []).importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library")
|
||||
)
|
||||
|
||||
#expect(report.chapters.first?.title == "第1章(1)")
|
||||
#expect(report.chapters.contains { $0.title.hasPrefix("第2章(") })
|
||||
for index in 1..<report.chapters.count {
|
||||
#expect(report.chapters[index - 1].end == report.chapters[index].start)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func selectsCustomTocRuleAndRunsTitleScript() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "自定义目录.txt")
|
||||
let body = String(repeating: "正文", count: 650)
|
||||
try Data("卷1 开始\n\(body)\n卷2 继续\n\(body)\n卷3 结束\n\(body)".utf8).write(to: source)
|
||||
let rule = TxtTocRule(
|
||||
name: "卷标题",
|
||||
rule: #"(?m)^卷\d+[^\r\n]*$"#,
|
||||
replacement: "result.replace('卷', '第')",
|
||||
serialNumber: 1
|
||||
)
|
||||
|
||||
let report = try LocalBookService(tocRules: [rule]).importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library")
|
||||
)
|
||||
|
||||
#expect(report.book.tocUrl == rule.rule + TxtTocRuleSelector.separator + rule.replacement)
|
||||
#expect(report.chapters.map(\.title) == ["第1 开始", "第2 继续", "第3 结束"])
|
||||
}
|
||||
|
||||
@Test func tocRuleSelectorKeepsEarlierRuleUnlessLaterWinsByMoreThanTwo() {
|
||||
let body = String(repeating: "字", count: 1_100)
|
||||
let content = "A1\n\(body)\nA2\n\(body)\nA3\n\(body)\nB1\n\(body)\nB2\n\(body)\nB3\n\(body)\nB4"
|
||||
let first = TxtTocRule(id: 1, name: "A", rule: #"(?m)^A\d$"#, serialNumber: 1)
|
||||
let second = TxtTocRule(id: 2, name: "B", rule: #"(?m)^B\d$"#, serialNumber: 2)
|
||||
|
||||
#expect(TxtTocRuleSelector.select(content: content, rules: [first, second])?.id == first.id)
|
||||
}
|
||||
|
||||
@Test func titleScriptPutVolumeInsertsVolumeChapter() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "分卷.txt")
|
||||
let body = String(repeating: "正文", count: 650)
|
||||
try Data("章1\n\(body)\n章2\n\(body)\n章3\n\(body)".utf8).write(to: source)
|
||||
let rule = TxtTocRule(
|
||||
name: "脚本分卷",
|
||||
rule: #"(?m)^章\d$"#,
|
||||
replacement: "if (index === 2) java.putVolume('第二卷'); result;",
|
||||
serialNumber: 1
|
||||
)
|
||||
|
||||
let report = try LocalBookService(tocRules: [rule]).importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library")
|
||||
)
|
||||
|
||||
#expect(report.chapters.map(\.title) == ["章1", "第二卷", "章2", "章3"])
|
||||
#expect(report.chapters.map(\.isVolume) == [false, true, false, false])
|
||||
#expect(report.chapters[1].start == report.chapters[1].end)
|
||||
}
|
||||
|
||||
@Test func scansHeadingsAcross512KBBlocksAndReadsByByteRange() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "跨块.txt")
|
||||
let longBody = String(repeating: "段落内容\n", count: 50_000)
|
||||
let sourceData = Data("第一章 开始\n\(longBody)第二章 结束\n结尾".utf8)
|
||||
try sourceData.write(to: source)
|
||||
|
||||
let report = try LocalBookService().importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library")
|
||||
)
|
||||
let secondTitle = try #require(sourceData.range(of: Data("第二章 结束".utf8)))
|
||||
|
||||
#expect(report.chapters.map(\.title) == ["第一章 开始", "第二章 结束"])
|
||||
#expect(report.chapters[0].end == Int64(secondTitle.lowerBound))
|
||||
#expect(report.chapters[1].start == Int64(secondTitle.upperBound))
|
||||
#expect(try LocalBookService().loadContent(book: report.book, chapter: report.chapters[1]).content == " 结尾")
|
||||
}
|
||||
|
||||
@Test func preservesUTF16EndianBoundariesAcross512KBBlocks() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let text = "第一章 开始\n\(String(repeating: "段落内容\n", count: 70_000))第二章 结束\n结尾"
|
||||
let fixtures: [(String, String.Encoding, [UInt8], String)] = [
|
||||
("LE", .utf16LittleEndian, [0xFF, 0xFE], "UTF-16LE"),
|
||||
("BE", .utf16BigEndian, [0xFE, 0xFF], "UTF-16BE")
|
||||
]
|
||||
|
||||
for (suffix, encoding, bom, charset) in fixtures {
|
||||
let source = root.appending(path: "UTF16\(suffix).txt")
|
||||
var sourceData = Data(bom)
|
||||
sourceData.append(try #require(text.data(using: encoding)))
|
||||
try sourceData.write(to: source)
|
||||
let report = try LocalBookService().importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library-\(suffix)")
|
||||
)
|
||||
let titleData = try #require("第二章 结束".data(using: encoding))
|
||||
let titleRange = try #require(sourceData.range(of: titleData))
|
||||
let second = try #require(report.chapters.first(where: { $0.title == "第二章 结束" }))
|
||||
|
||||
#expect(report.book.charset == charset)
|
||||
#expect(second.start == Int64(titleRange.upperBound))
|
||||
#expect(try LocalBookService().loadContent(book: report.book, chapter: second).content == " 结尾")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func splitsOversizedTocChapterIntoVolumeAndUpdatesFollowingScriptState() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
let source = root.appending(path: "超长章节.txt")
|
||||
let longBody = String(repeating: "正文内容\n", count: 12_000)
|
||||
try Data("第一章\n\(longBody)第二章\n结尾".utf8).write(to: source)
|
||||
let rule = TxtTocRule(
|
||||
name: "长章脚本",
|
||||
rule: #"(?m)^第[一二]章$"#,
|
||||
replacement: "result === '第二章' ? lastVolumeTitle + '|' + index + '|' + result : result",
|
||||
serialNumber: 1
|
||||
)
|
||||
|
||||
let report = try LocalBookService(tocRules: [rule]).importTextFile(
|
||||
at: source,
|
||||
into: root.appending(path: "library")
|
||||
)
|
||||
|
||||
#expect(report.chapters.first?.title == "第一章")
|
||||
#expect(report.chapters.first?.isVolume == true)
|
||||
#expect(report.chapters.dropFirst().contains { $0.title == "第一章(1)" })
|
||||
#expect(report.chapters.last?.title.hasPrefix("第一章|") == true)
|
||||
#expect(report.chapters.last?.title.hasSuffix("|第二章") == true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user