chore: checkpoint current milestone work
This commit is contained in:
+18
-6
@@ -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
|
||||
|
||||
@@ -48,6 +48,6 @@ SPEC CHECKSUMS:
|
||||
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: c9ace43f8ce76d66d3a7c91dea869911207c0354
|
||||
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
Generated
+1
-1
@@ -48,6 +48,6 @@ SPEC CHECKSUMS:
|
||||
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: c9ace43f8ce76d66d3a7c91dea869911207c0354
|
||||
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
+1464
-1454
File diff suppressed because it is too large
Load Diff
Generated
-22
@@ -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;
|
||||
|
||||
Generated
BIN
Binary file not shown.
@@ -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 支持仍然保留在当前收敛后的架构中。
|
||||
Reference in New Issue
Block a user