chore: checkpoint current milestone work

This commit is contained in:
shen
2026-05-22 13:28:53 +08:00
parent 6d196d64e5
commit 5698aeaead
68 changed files with 5694 additions and 3291 deletions
+18 -6
View File
@@ -1,5 +1,4 @@
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
platform :ios, '15.6'
target 'ReadViewDemo' do
# Comment the next line if you don't want to use dynamic frameworks
@@ -9,9 +8,22 @@ target 'ReadViewDemo' do
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
end
apply_settings = lambda do |config|
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6'
end
# Pods project (Pods.xcodeproj)
installer.pods_project.build_configurations.each { |c| apply_settings.call(c) }
installer.pods_project.targets.each { |t| t.build_configurations.each { |c| apply_settings.call(c) } }
# User project(s) that integrate Pods (e.g. ReadViewDemo)
installer.aggregate_targets.each do |at|
p = at.user_project
next unless p
p.build_configurations.each { |c| apply_settings.call(c) }
p.targets.each { |t| t.build_configurations.each { |c| apply_settings.call(c) } }
p.save
end
end
+1 -1
View File
@@ -48,6 +48,6 @@ SPEC CHECKSUMS:
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: c9ace43f8ce76d66d3a7c91dea869911207c0354
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
COCOAPODS: 1.16.2
+1 -1
View File
@@ -48,6 +48,6 @@ SPEC CHECKSUMS:
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: c9ace43f8ce76d66d3a7c91dea869911207c0354
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
COCOAPODS: 1.16.2
File diff suppressed because it is too large Load Diff
@@ -8,78 +8,56 @@
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>DTCoreText.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>DTFoundation.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>Pods-ReadViewDemo.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>RDReaderView-RDReaderViewAssets.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>5</integer>
</dict>
<key>RDReaderView.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>4</integer>
</dict>
<key>SSAlertSwift.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>8</integer>
</dict>
<key>SnapKit-SnapKit_Privacy.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>7</integer>
</dict>
<key>SnapKit.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>6</integer>
</dict>
<key>ZIPFoundation-ZIPFoundation_Privacy.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>10</integer>
</dict>
<key>ZIPFoundation.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>9</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
@@ -64,7 +64,6 @@
423988C9F7F09A1B78D97CC1 /* Pods-ReadViewDemo.debug.xcconfig */,
3A43AED288BFCA3ADBA97DD7 /* Pods-ReadViewDemo.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -214,8 +213,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MG4Z7FU83W;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ReadViewDemo/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -223,13 +226,16 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fs.ReadViewDemo;
PRODUCT_BUNDLE_IDENTIFIER = cn.shen.ReadViewDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = TouchLinked_dev;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@@ -246,8 +252,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MG4Z7FU83W;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ReadViewDemo/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -255,13 +265,16 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fs.ReadViewDemo;
PRODUCT_BUNDLE_IDENTIFIER = cn.shen.ReadViewDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = TouchLinked_dev;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@@ -309,7 +322,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -324,7 +337,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.4;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@@ -372,7 +385,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -381,7 +394,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.4;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
+443 -1
View File
@@ -8,6 +8,57 @@ final class ViewController: UIViewController {
let fileURL: URL
}
private enum ValidationCategory: String, CaseIterable {
case textNovel = "TXT/小说"
case textRich = "复杂图文"
case webFixed = "Fixed/互动"
case plainText = "TXT"
var sortOrder: Int {
switch self {
case .textNovel: return 0
case .textRich: return 1
case .webFixed: return 2
case .plainText: return 3
}
}
}
private struct BookValidationReport {
let title: String
let category: ValidationCategory
let profile: String
let passed: Bool
let notes: [String]
}
private struct ResourceValidationSummary {
let checkedCount: Int
let passedCount: Int
let failedBooks: [String]
let matrixLines: [String]
let diagnosticLines: [String]
let rerunHint: String
var statusText: String {
var lines: [String] = []
if checkedCount == 0 {
lines.append("样本验证:未发现可验证样本")
} else if failedBooks.isEmpty {
lines.append("样本验证:\(passedCount)/\(checkedCount) 通过")
} else {
let titles = failedBooks.prefix(2).joined(separator: "")
lines.append("样本验证:\(passedCount)/\(checkedCount) 通过,失败样本:\(titles)")
}
lines.append(contentsOf: matrixLines.prefix(3))
lines.append(contentsOf: diagnosticLines.prefix(2))
if !rerunHint.isEmpty {
lines.append(rerunHint)
}
return lines.joined(separator: "\n")
}
}
private let statusLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
@@ -39,6 +90,12 @@ final class ViewController: UIViewController {
}()
private var books: [DemoBook] = []
private var validationSummary: ResourceValidationSummary?
private var validationTask: Task<Void, Never>?
deinit {
validationTask?.cancel()
}
override func viewDidLoad() {
super.viewDidLoad()
@@ -75,6 +132,7 @@ final class ViewController: UIViewController {
books = discoverBooks()
tableView.reloadData()
updateChrome()
validateSampleBooks()
}
private func updateChrome() {
@@ -82,7 +140,14 @@ final class ViewController: UIViewController {
statusLabel.text = "ReadViewSDK Demo\nbook 目录下未找到 txt 或 epub 文件"
tableView.backgroundView = emptyStateLabel
} else {
statusLabel.text = "ReadViewSDK Demo\n已发现 \(books.count) 本本地图书,点击即可进入阅读器"
var lines = [
"ReadViewSDK Demo",
"已发现 \(books.count) 本本地图书,点击即可进入阅读器"
]
if let validationSummary {
lines.append(validationSummary.statusText)
}
statusLabel.text = lines.joined(separator: "\n")
tableView.backgroundView = nil
}
}
@@ -126,6 +191,362 @@ final class ViewController: UIViewController {
}
}
private func validateSampleBooks() {
validationTask?.cancel()
guard !books.isEmpty else {
validationSummary = nil
updateChrome()
return
}
let pageSize = currentReaderPageSize()
let style = currentReaderTextStyle()
validationTask = Task.detached(priority: .utility) { [books] in
let summary = Self.validateBooks(books, pageSize: pageSize, style: style)
await MainActor.run {
print("[ReadViewDemo] \(summary.statusText)")
self.validationSummary = summary
self.updateChrome()
}
}
}
nonisolated private static func validateBooks(
_ books: [DemoBook],
pageSize: CGSize,
style: RDEPUBTextRenderStyle
) -> ResourceValidationSummary {
var checkedCount = 0
var passedCount = 0
var failedBooks: [String] = []
var reports: [BookValidationReport] = []
var diagnosticLines: [String] = []
for book in books {
let result = validateBook(book, pageSize: pageSize, style: style)
checkedCount += 1
if result.report.passed {
passedCount += 1
} else {
failedBooks.append(book.title)
}
reports.append(result.report)
diagnosticLines.append(contentsOf: result.diagnostics)
}
return ResourceValidationSummary(
checkedCount: checkedCount,
passedCount: passedCount,
failedBooks: failedBooks,
matrixLines: makeMatrixLines(from: reports),
diagnosticLines: diagnosticLines,
rerunHint: "复现入口:启动 Demo 查看摘要,打开样本书后再验证搜索 / 高亮 / 主题字号切换"
)
}
nonisolated private static func validateBook(
_ book: DemoBook,
pageSize: CGSize,
style: RDEPUBTextRenderStyle
) -> (report: BookValidationReport, diagnostics: [String]) {
let fileExtension = book.fileURL.pathExtension.lowercased()
if fileExtension == "txt" {
let builder = RDPlainTextBookBuilder()
do {
let textBook = try builder.build(textFileURL: book.fileURL, pageSize: pageSize, style: style)
let passed = !textBook.pages.isEmpty && !textBook.chapters.isEmpty
let report = BookValidationReport(
title: book.title,
category: .plainText,
profile: "txt",
passed: passed,
notes: [
"章节 \(textBook.chapters.count)",
"页数 \(textBook.pages.count)"
]
)
let diagnostics = [
"TXT 验证:\(book.title) · chapters \(textBook.chapters.count) · pages \(textBook.pages.count)"
]
return (report, diagnostics)
} catch {
let report = BookValidationReport(
title: book.title,
category: .plainText,
profile: "txt",
passed: false,
notes: [error.localizedDescription]
)
return (report, ["TXT 验证失败:\(book.title) · \(error.localizedDescription)"])
}
}
let parser = RDEPUBParser()
do {
try parser.parse(epubURL: book.fileURL)
let publication = parser.makePublication()
switch publication.readingProfile {
case .textReflowable:
return validateReflowableBook(
book,
parser: parser,
publication: publication,
pageSize: pageSize,
style: style
)
case .webFixedLayout, .webInteractive:
return validateWebBook(
book,
publication: publication,
viewportSize: pageSize
)
}
} catch {
let report = BookValidationReport(
title: book.title,
category: .textRich,
profile: "parse-failed",
passed: false,
notes: [error.localizedDescription]
)
return (report, ["EPUB 验证失败:\(book.title) · \(error.localizedDescription)"])
}
}
nonisolated private static func validateReflowableBook(
_ book: DemoBook,
parser: RDEPUBParser,
publication: RDEPUBPublication,
pageSize: CGSize,
style: RDEPUBTextRenderStyle
) -> (report: BookValidationReport, diagnostics: [String]) {
let builder = RDEPUBTextBookBuilder()
do {
let textBook = try builder.build(
parser: parser,
publication: publication,
pageSize: pageSize,
style: style
)
let missingResources = builder.lastBuildResourceDiagnostics.filter { !$0.existsOnDisk }
let diagnostics = builder.lastBuildPaginationDiagnostics
let attachmentPages = diagnostics.reduce(0) { $0 + $1.attachmentPageCount }
let category: ValidationCategory = attachmentPages > 0 ? .textRich : .textNovel
let passed = !textBook.pages.isEmpty && missingResources.isEmpty
var reportNotes = [
"章节 \(textBook.chapters.count)",
"页数 \(textBook.pages.count)"
]
if attachmentPages > 0 {
reportNotes.append("attachment 页 \(attachmentPages)")
}
var summaryLines: [String] = []
if let paginationSummary = makePaginationSummary(diagnostics, title: book.title) {
summaryLines.append("分页诊断:\(paginationSummary)")
}
if let restoreSummary = makeRestoreSummary(
parser: parser,
publication: publication,
textBook: textBook,
pageSize: pageSize,
style: style,
title: book.title
) {
summaryLines.append("恢复诊断:\(restoreSummary)")
}
let report = BookValidationReport(
title: book.title,
category: category,
profile: publication.readingProfile.rawValue,
passed: passed,
notes: reportNotes
)
return (report, summaryLines)
} catch {
let category: ValidationCategory = book.title.contains("凡人") ? .textNovel : .textRich
let report = BookValidationReport(
title: book.title,
category: category,
profile: publication.readingProfile.rawValue,
passed: false,
notes: [error.localizedDescription]
)
return (report, ["EPUB 验证失败:\(book.title) · \(error.localizedDescription)"])
}
}
nonisolated private static func validateWebBook(
_ book: DemoBook,
publication: RDEPUBPublication,
viewportSize: CGSize
) -> (report: BookValidationReport, diagnostics: [String]) {
let linearItems = publication.spine.filter(\.linear)
let missingFiles = linearItems.filter {
guard let normalizedHref = publication.resourceResolver.normalizedHref($0.href) else {
return true
}
return publication.resourceResolver.fileURL(forRelativePath: normalizedHref) == nil
}
let defaultConfiguration = RDEPUBReaderConfiguration.default
let preferences = RDEPUBPreferences(
fontSize: defaultConfiguration.fontSize,
lineHeightMultiple: defaultConfiguration.lineHeightMultiple,
reflowableContentInsets: defaultConfiguration.reflowableContentInsets,
fixedContentInset: defaultConfiguration.fixedContentInset,
fixedLayoutFit: defaultConfiguration.fixedLayoutFit,
fixedLayoutSpreadMode: defaultConfiguration.fixedLayoutSpreadMode
)
let spreadCount = publication.layout == .fixed
? publication.makeFixedSpreads(
preferences: preferences,
viewportSize: viewportSize
).count
: 0
let passed = !linearItems.isEmpty && missingFiles.isEmpty && (publication.layout != .fixed || spreadCount > 0)
let category: ValidationCategory = .webFixed
let report = BookValidationReport(
title: book.title,
category: category,
profile: publication.readingProfile.rawValue,
passed: passed,
notes: [
"spine \(linearItems.count)",
publication.layout == .fixed ? "spread \(spreadCount)" : "interactive"
]
)
let diagnostic = "Web 路径验证:\(book.title) · profile \(publication.readingProfile.rawValue) · spine \(linearItems.count) · missing \(missingFiles.count)"
return (report, [diagnostic])
}
nonisolated private static func makeMatrixLines(from reports: [BookValidationReport]) -> [String] {
let grouped = Dictionary(grouping: reports, by: \.category)
return ValidationCategory.allCases.compactMap { category in
guard let items = grouped[category], !items.isEmpty else { return nil }
let passed = items.filter(\.passed).count
let notes = items.prefix(2).map { "\($0.title)(\($0.profile))" }.joined(separator: "")
return "矩阵[\(category.rawValue)] \(passed)/\(items.count) · \(notes)"
}
}
nonisolated private static func makePaginationSummary(
_ diagnostics: [RDEPUBTextChapterPaginationDiagnostic],
title: String
) -> String? {
guard !diagnostics.isEmpty else { return nil }
let attachmentPages = diagnostics.reduce(0) { $0 + $1.attachmentPageCount }
let semanticBreakPages = diagnostics.reduce(0) { $0 + $1.blockAdjustedPageCount }
let breakReasonCounts = diagnostics
.flatMap(\.breakReasons)
.reduce(into: [RDEPUBTextPageBreakReason: Int]()) { counts, reason in
counts[reason, default: 0] += 1
}
let orderedReasons = breakReasonCounts
.sorted { lhs, rhs in
if lhs.value == rhs.value {
return lhs.key.rawValue < rhs.key.rawValue
}
return lhs.value > rhs.value
}
.map { "\($0.key.rawValue):\($0.value)" }
.joined(separator: ", ")
let note = diagnostics
.flatMap(\.sampleNotes)
.first(where: { $0.contains("attachment") || $0.contains("block") || $0.contains("page break") })
var parts = [
"\(title)",
"章节 \(diagnostics.count)",
"attachment 页 \(attachmentPages)",
"semantic break 页 \(semanticBreakPages)",
orderedReasons.isEmpty ? nil : "reasons [\(orderedReasons)]"
].compactMap { $0 }
if let note {
parts.append(note)
}
return parts.joined(separator: " · ")
}
nonisolated private static func makeRestoreSummary(
parser: RDEPUBParser,
publication: RDEPUBPublication,
textBook: RDEPUBTextBook,
pageSize: CGSize,
style: RDEPUBTextRenderStyle,
title: String
) -> String? {
guard textBook.pages.count > 1 else { return nil }
let targetPageNumber = min(max(textBook.pages.count / 2, 1), textBook.pages.count)
guard let restoreLocation = textBook.location(forPageNumber: targetPageNumber, bookIdentifier: publication.metadata.identifier),
let baseResolvedPage = textBook.pageNumber(
for: restoreLocation,
resolver: publication.resourceResolver,
bookIdentifier: publication.metadata.identifier
) else {
return nil
}
let alternateFont = UIFont.systemFont(ofSize: style.font.pointSize + 2)
let alternateLineSpacing = style.lineSpacing + max(alternateFont.lineHeight * 0.15, 2)
let alternateStyle = RDEPUBTextRenderStyle(
font: alternateFont,
lineSpacing: alternateLineSpacing,
textColor: style.textColor,
backgroundColor: style.backgroundColor
)
let themeStyle = RDEPUBTextRenderStyle(
font: style.font,
lineSpacing: style.lineSpacing,
textColor: .white,
backgroundColor: .black
)
let builder = RDEPUBTextBookBuilder()
guard let alternateBook = try? builder.build(
parser: parser,
publication: publication,
pageSize: pageSize,
style: alternateStyle
), let alternatePageNumber = alternateBook.pageNumber(
for: restoreLocation,
resolver: publication.resourceResolver,
bookIdentifier: publication.metadata.identifier
), let alternateResolvedLocation = alternateBook.location(
forPageNumber: alternatePageNumber,
bookIdentifier: publication.metadata.identifier
) else {
return nil
}
let themeBuilder = RDEPUBTextBookBuilder()
let themeBook = try? themeBuilder.build(
parser: parser,
publication: publication,
pageSize: pageSize,
style: themeStyle
)
let themePageNumber = themeBook?.pageNumber(
for: restoreLocation,
resolver: publication.resourceResolver,
bookIdentifier: publication.metadata.identifier
)
let hrefStable = (publication.resourceResolver.normalizedHref(alternateResolvedLocation.href) ?? alternateResolvedLocation.href) ==
(publication.resourceResolver.normalizedHref(restoreLocation.href) ?? restoreLocation.href)
let progressionDelta = abs(alternateResolvedLocation.navigationProgression - restoreLocation.navigationProgression)
let themeStable = themePageNumber == baseResolvedPage
return [
title,
"base \(baseResolvedPage)",
"font-shift \(alternatePageNumber)",
"theme-stable \(themeStable ? "yes" : "no")",
"href-stable \(hrefStable ? "yes" : "no")",
String(format: "progression-delta %.3f", progressionDelta)
].joined(separator: " · ")
}
private func openBook(_ book: DemoBook) {
let controller = RDURLReaderController(bookURL: book.fileURL)
controller.title = book.title
@@ -141,6 +562,27 @@ final class ViewController: UIViewController {
present(hostController, animated: true)
}
private func currentReaderPageSize() -> CGSize {
let viewportSize = UIScreen.main.bounds.size
let insets = RDEPUBReaderConfiguration.default.reflowableContentInsets
return CGSize(
width: max(viewportSize.width - insets.left - insets.right, 1),
height: max(viewportSize.height - insets.top - insets.bottom, 1)
)
}
private func currentReaderTextStyle() -> RDEPUBTextRenderStyle {
let configuration = RDEPUBReaderConfiguration.default
let font = UIFont.systemFont(ofSize: configuration.fontSize)
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
return RDEPUBTextRenderStyle(
font: font,
lineSpacing: lineSpacing,
textColor: configuration.theme.contentTextColor,
backgroundColor: configuration.theme.contentBackgroundColor
)
}
}
extension ViewController: UITableViewDataSource {
@@ -0,0 +1,11 @@
第一章 回归验证
这是 ReadViewSDK 的 Phase 5 纯文本验证样本。
它用于覆盖 RDURLReaderController 与 RDPlainTextBookBuilder 的 TXT 路径,
确认纯文本分页、打开主流程,以及后续 reader 壳层回归不会失效。
第二章 稳定性
如果这个样本能被成功发现、分页并进入阅读器,
就说明 TXT 支持仍然保留在当前收敛后的架构中。