refactor: 添加中文注释 + 优化模块结构
- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
This commit is contained in:
@@ -1,48 +1,80 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 章节分页诊断数据
|
||||
|
||||
/// 单个章节的分页诊断信息,用于调试和质量检测。
|
||||
/// 记录页数、分页原因、附件/块级元素统计等。
|
||||
public struct RDEPUBTextChapterPaginationDiagnostic: Equatable {
|
||||
public var href: String
|
||||
public var title: String
|
||||
public var pageCount: Int
|
||||
/// 每页的分页原因列表
|
||||
public var breakReasons: [RDEPUBTextPageBreakReason]
|
||||
/// 包含附件的页数
|
||||
public var attachmentPageCount: Int
|
||||
/// 因块级/附件边界调整而分页的页数
|
||||
public var blockAdjustedPageCount: Int
|
||||
public var blockKinds: [RDEPUBTextBlockKind]
|
||||
public var semanticHints: [RDEPUBTextSemanticHint]
|
||||
public var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
|
||||
/// 采样诊断日志(最多 4 条)
|
||||
public var sampleNotes: [String]
|
||||
}
|
||||
|
||||
// MARK: - 页面数据模型
|
||||
|
||||
/// 分页后的单页数据,包含全书绝对页码、所属章节、内容范围等。
|
||||
///
|
||||
/// 每个 `RDEPUBTextPage` 由 `RDEPUBTextLayoutFrame` 生成,
|
||||
/// `contentRange` 标记了该页在章节富文本中的字符范围。
|
||||
public struct RDEPUBTextPage: Equatable {
|
||||
/// 全书绝对页码(从 0 开始)
|
||||
public var absolutePageIndex: Int
|
||||
public var chapterIndex: Int
|
||||
public var spineIndex: Int
|
||||
public var href: String
|
||||
public var chapterTitle: String
|
||||
/// 该页在所属章节中的相对页码(从 0 开始)
|
||||
public var pageIndexInChapter: Int
|
||||
public var totalPagesInChapter: Int
|
||||
/// 所属章节的完整富文本内容(用于跨页查询)
|
||||
public var chapterContent: NSAttributedString
|
||||
/// 该页的富文本片段
|
||||
public var content: NSAttributedString
|
||||
/// 该页在章节富文本中的范围
|
||||
public var contentRange: NSRange
|
||||
public var pageStartOffset: Int
|
||||
public var pageEndOffset: Int
|
||||
/// 页的元数据(分页原因、附件、语义标记等)
|
||||
public var metadata: RDEPUBTextPageMetadata
|
||||
}
|
||||
|
||||
// MARK: - 章节数据模型
|
||||
|
||||
/// 渲染并分页后的单个章节,包含完整富文本和分页结果。
|
||||
public struct RDEPUBTextChapter: Equatable {
|
||||
public var chapterIndex: Int
|
||||
public var spineIndex: Int
|
||||
public var href: String
|
||||
public var title: String
|
||||
/// 章节完整富文本内容
|
||||
public var attributedContent: NSAttributedString
|
||||
/// fragment ID → 字符偏移量映射(用于锚点定位)
|
||||
public var fragmentOffsets: [String: Int]
|
||||
public var pageBreakReasons: [RDEPUBTextPageBreakReason]
|
||||
public var pages: [RDEPUBTextPage]
|
||||
}
|
||||
|
||||
// MARK: - 分页书籍模型
|
||||
|
||||
/// 整本 EPUB 的分页书籍模型,包含所有章节、页面和全局索引表。
|
||||
///
|
||||
/// 这是 EPUBTextRendering 层的最终产物,由 `RDEPUBTextBookBuilder.build()` 生成。
|
||||
/// 通过 `chapterData(for:)` 或 `chapterData(atChapterIndex:)` 获取 `RDEPUBChapterData` 进行查询。
|
||||
public struct RDEPUBTextBook {
|
||||
public var chapters: [RDEPUBTextChapter]
|
||||
public var pages: [RDEPUBTextPage]
|
||||
/// 全局索引表:fileIndex/row/column → 绝对字符偏移量
|
||||
public let indexTable: RDEPUBTextIndexTable
|
||||
|
||||
public init(chapters: [RDEPUBTextChapter], pages: [RDEPUBTextPage]) {
|
||||
@@ -55,16 +87,19 @@ public struct RDEPUBTextBook {
|
||||
lhs.chapters == rhs.chapters && lhs.pages == rhs.pages
|
||||
}
|
||||
|
||||
/// 按 href 获取章节的数据访问层(包含索引表)
|
||||
public func chapterData(for href: String) -> RDEPUBChapterData? {
|
||||
guard let chapter = chapters.first(where: { $0.href == href }) else { return nil }
|
||||
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
|
||||
}
|
||||
|
||||
/// 按章节序号获取章节的数据访问层
|
||||
public func chapterData(atChapterIndex index: Int) -> RDEPUBChapterData? {
|
||||
guard chapters.indices.contains(index) else { return nil }
|
||||
return RDEPUBChapterData(chapter: chapters[index], indexTable: indexTable)
|
||||
}
|
||||
|
||||
/// 按页码(从 1 开始)获取对应页面
|
||||
public func page(at pageNumber: Int) -> RDEPUBTextPage? {
|
||||
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else {
|
||||
return nil
|
||||
@@ -72,6 +107,12 @@ public struct RDEPUBTextBook {
|
||||
return pages[pageNumber - 1]
|
||||
}
|
||||
|
||||
/// 根据持久化位置计算对应页码(从 1 开始)。
|
||||
///
|
||||
/// 解析优先级:
|
||||
/// 1. rangeAnchor 锚点定位
|
||||
/// 2. fragment 片段 ID
|
||||
/// 3. navigationProgression 进度百分比回退
|
||||
public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? {
|
||||
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier),
|
||||
let chapter = chapters.first(where: { $0.href == normalizedLocation.href }) else {
|
||||
@@ -98,6 +139,7 @@ public struct RDEPUBTextBook {
|
||||
return chapter.pages.last.map { $0.absolutePageIndex + 1 }
|
||||
}
|
||||
|
||||
/// 根据页码生成持久化位置(RDEPUBLocation),包含起止锚点
|
||||
public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
|
||||
guard let page = page(at: pageNumber),
|
||||
let chapter = chapters.first(where: { $0.chapterIndex == page.chapterIndex }) else {
|
||||
@@ -121,14 +163,29 @@ public struct RDEPUBTextBook {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 分页书籍构建器
|
||||
|
||||
/// 从 EPUB publication 构建分页书籍模型的核心构建器。
|
||||
///
|
||||
/// 渲染链路:遍历 spine → 渲染每章 HTML → 分页 → 合并/规范化尾页 → 构建 RDEPUBTextBook
|
||||
///
|
||||
/// 特性:
|
||||
/// - 支持分页缓存(WXRead 模式:只缓存页范围,不缓存富文本)
|
||||
/// - 性能采样(记录每章渲染/分页耗时)
|
||||
/// - 尾页规范化(丢弃纯空白尾页、合并过短尾页)
|
||||
/// - 封面章节特殊处理
|
||||
public final class RDEPUBTextBookBuilder {
|
||||
private let renderer: RDEPUBTextRenderer
|
||||
private let cache: RDEPUBTextBookCache?
|
||||
private let sampler: RDEPUBTextPerformanceSampler
|
||||
|
||||
/// 最后一次构建的资源引用诊断(样式表、图片等)
|
||||
public private(set) var lastBuildResourceDiagnostics: [RDEPUBTextResourceReferenceDiagnostic] = []
|
||||
/// 最后一次构建的分页诊断(每章一页的分页原因、附件统计等)
|
||||
public private(set) var lastBuildPaginationDiagnostics: [RDEPUBTextChapterPaginationDiagnostic] = []
|
||||
/// 最后一次构建的性能采样数据
|
||||
public private(set) var lastBuildPerformanceSamples: [RDEPUBTextPerformanceSample] = []
|
||||
/// 最后一次构建的缓存命中/未命中统计
|
||||
public private(set) var lastBuildCacheStats: (hits: Int, misses: Int) = (0, 0)
|
||||
|
||||
public init(renderer: RDEPUBTextRenderer, cache: RDEPUBTextBookCache? = nil) {
|
||||
@@ -137,10 +194,12 @@ public final class RDEPUBTextBookBuilder {
|
||||
self.sampler = RDEPUBTextPerformanceSampler()
|
||||
}
|
||||
|
||||
/// 默认构造器,使用 DTCoreText 渲染器
|
||||
public convenience init() {
|
||||
self.init(renderer: RDEPUBDTCoreTextRenderer())
|
||||
}
|
||||
|
||||
/// 生成最近一次构建的语义摘要,用于 Phase 7 质量检测日志
|
||||
public func phase7SemanticSummary(title: String? = nil) -> String? {
|
||||
guard !lastBuildPaginationDiagnostics.isEmpty else { return nil }
|
||||
|
||||
@@ -164,6 +223,16 @@ public final class RDEPUBTextBookBuilder {
|
||||
return parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
/// 核心构建方法:从 EPUB publication 构建分页书籍。
|
||||
///
|
||||
/// 流程:
|
||||
/// 1. 生成缓存键,尝试加载分页缓存
|
||||
/// 2. 遍历 spine 中的线性 HTML 章节
|
||||
/// 3. 渲染每章 HTML → NSAttributedString
|
||||
/// 4. 跳过空白的封面/扉页章节
|
||||
/// 5. 分页:缓存命中则使用缓存的页范围,否则调用 CoreText 分页引擎
|
||||
/// 6. 尾页规范化:丢弃纯空白尾页、合并过短尾页
|
||||
/// 7. 构建 RDEPUBTextBook 并保存分页缓存
|
||||
public func build(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
@@ -180,7 +249,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
|
||||
let buildStart = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
// Cache lookup — WXRead style: load per-chapter page ranges
|
||||
// 缓存查询 — WXRead 模式:只加载每章的页范围,不缓存富文本
|
||||
let bookID = publication.metadata.identifier ?? publication.metadata.title
|
||||
let cacheKey = makeCacheKey(bookID: bookID, pageSize: pageSize, style: style)
|
||||
let cachedPagination = cacheKey.flatMap { cache?.load(key: $0) }
|
||||
@@ -191,6 +260,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
continue
|
||||
}
|
||||
|
||||
// 从目录表中解析章节标题
|
||||
let chapterTitle = resolvedChapterTitle(for: item, toc: publication.tableOfContents)
|
||||
let request = RDEPUBTextRendererSupport.makeChapterRenderRequest(
|
||||
href: item.href,
|
||||
@@ -201,6 +271,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
resourceResolver: publication.resourceResolver
|
||||
)
|
||||
|
||||
// 渲染 HTML → NSAttributedString
|
||||
let renderStart = CFAbsoluteTimeGetCurrent()
|
||||
let rendered = try renderer.renderChapter(request: request)
|
||||
let renderDuration = CFAbsoluteTimeGetCurrent() - renderStart
|
||||
@@ -211,6 +282,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
if item.href.lowercased().contains("cover") {
|
||||
print("[EPUB][Cover] rendered href=\(item.href) textLength=\(plainText.count) attrLength=\(rendered.attributedString.length) attachments=\(attachmentCount(in: rendered.attributedString))")
|
||||
}
|
||||
// 跳过空白的封面/扉页章节
|
||||
if shouldSkipChapter(item: item, content: rendered.attributedString, text: plainText) {
|
||||
if item.href.lowercased().contains("cover") {
|
||||
print("[EPUB][Cover] skipped href=\(item.href)")
|
||||
@@ -221,10 +293,12 @@ public final class RDEPUBTextBookBuilder {
|
||||
let chapterIndex = chapters.count
|
||||
let content = NSMutableAttributedString(attributedString: rendered.attributedString)
|
||||
|
||||
// 分页:封面图片章节走特殊路径,缓存命中则跳过分页引擎
|
||||
let paginateStart = CFAbsoluteTimeGetCurrent()
|
||||
let layoutFrames: [RDEPUBTextLayoutFrame]
|
||||
let isCacheHit: Bool
|
||||
if isAttachmentOnlyCoverChapter(item: item, content: content, plainText: plainText) {
|
||||
// 纯图片封面:整个内容作为一页
|
||||
layoutFrames = [
|
||||
RDEPUBTextLayoutFrame(
|
||||
contentRange: NSRange(location: 0, length: content.length),
|
||||
@@ -245,7 +319,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
]
|
||||
isCacheHit = false
|
||||
} else if let cached = cachedPagination?[item.href] {
|
||||
// Cache hit: use cached page ranges, skip rd_paginatedFrames
|
||||
// 缓存命中:直接使用缓存的页范围,跳过 CoreText 分页
|
||||
layoutFrames = cached.pageRanges.enumerated().map { idx, range in
|
||||
let breakReason = idx < cached.breakReasons.count ? cached.breakReasons[idx] : .frameLimit
|
||||
return RDEPUBTextLayoutFrame(
|
||||
@@ -263,6 +337,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
isCacheHit = true
|
||||
} else {
|
||||
// 缓存未命中:调用 CoreText 分页引擎
|
||||
layoutFrames = content.length > 0
|
||||
? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
|
||||
: []
|
||||
@@ -270,6 +345,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
let paginateDuration = CFAbsoluteTimeGetCurrent() - paginateStart
|
||||
|
||||
// 尾页规范化:丢弃纯空白尾页、合并过短尾页
|
||||
let normalizedFrames = normalizeTrailingFrames(
|
||||
layoutFrames,
|
||||
content: content,
|
||||
@@ -298,6 +374,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
print("[EPUB][Cover] paginated href=\(item.href) pages=\(effectiveFrames.count) firstRange=\(effectiveFrames.first.map { NSStringFromRange($0.contentRange) } ?? "none")")
|
||||
}
|
||||
|
||||
// 记录性能采样
|
||||
sampler.record(RDEPUBTextPerformanceSample(
|
||||
chapterHref: item.href,
|
||||
renderDuration: renderDuration,
|
||||
@@ -312,6 +389,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
lastBuildCacheStats.misses += 1
|
||||
}
|
||||
|
||||
// 构建页面和章节模型
|
||||
let chapterAttributedContent = content.copy() as! NSAttributedString
|
||||
let pages = effectiveFrames.enumerated().map { localPageIndex, frame in
|
||||
let range = frame.contentRange
|
||||
@@ -369,6 +447,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
|
||||
sampler.totalBuildDuration = CFAbsoluteTimeGetCurrent() - buildStart
|
||||
|
||||
// 保存分页缓存(只缓存页范围和分页原因,不缓存富文本)
|
||||
if let cacheKey {
|
||||
let paginationCache = chapters.map { chapter in
|
||||
let pageRanges = chapter.pages.map(\.contentRange)
|
||||
@@ -390,6 +469,9 @@ public final class RDEPUBTextBookBuilder {
|
||||
return book
|
||||
}
|
||||
|
||||
// MARK: - 章节标题解析
|
||||
|
||||
/// 从目录表中查找章节标题,找不到则回退到 spine item 的 title 或 href
|
||||
private func resolvedChapterTitle(for item: RDEPUBSpineItem, toc: [EPUBTableOfContentsItem]) -> String {
|
||||
if let title = flattenedTOCItems(from: toc).first(where: { tocItem in
|
||||
tocItem.href.components(separatedBy: "#").first == item.href
|
||||
@@ -400,12 +482,16 @@ public final class RDEPUBTextBookBuilder {
|
||||
return trimmedTitle.isEmpty ? item.href : trimmedTitle
|
||||
}
|
||||
|
||||
/// 递归展开嵌套目录为扁平列表
|
||||
private func flattenedTOCItems(from items: [EPUBTableOfContentsItem]) -> [EPUBTableOfContentsItem] {
|
||||
items.flatMap { item in
|
||||
[item] + flattenedTOCItems(from: item.children)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 章节过滤
|
||||
|
||||
/// 判断是否应跳过该章节(空白的封面/扉页,无文本且无附件)
|
||||
private func shouldSkipChapter(item: RDEPUBSpineItem, content: NSAttributedString, text: String) -> Bool {
|
||||
let lowercasedHref = item.href.lowercased()
|
||||
var hasAttachment = false
|
||||
@@ -422,6 +508,9 @@ public final class RDEPUBTextBookBuilder {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - 附件统计
|
||||
|
||||
/// 统计富文本中的附件数量
|
||||
private func attachmentCount(in content: NSAttributedString) -> Int {
|
||||
guard content.length > 0 else { return 0 }
|
||||
var count = 0
|
||||
@@ -433,6 +522,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
return count
|
||||
}
|
||||
|
||||
/// 获取富文本中所有附件的 NSRange 列表
|
||||
private func attachmentRanges(in content: NSAttributedString) -> [NSRange] {
|
||||
guard content.length > 0 else { return [] }
|
||||
var ranges: [NSRange] = []
|
||||
@@ -444,6 +534,9 @@ public final class RDEPUBTextBookBuilder {
|
||||
return ranges
|
||||
}
|
||||
|
||||
// MARK: - 封面章节检测
|
||||
|
||||
/// 判断是否为纯图片封面章节(href 包含 cover 且有附件但几乎无文本)
|
||||
private func isAttachmentOnlyCoverChapter(
|
||||
item: RDEPUBSpineItem,
|
||||
content: NSAttributedString,
|
||||
@@ -455,6 +548,11 @@ public final class RDEPUBTextBookBuilder {
|
||||
return attachmentCount(in: content) > 0 && trimmed.count <= 1
|
||||
}
|
||||
|
||||
// MARK: - 尾页规范化
|
||||
|
||||
/// 规范化分页结果的尾部页面:
|
||||
/// 1. 丢弃纯空白的尾页
|
||||
/// 2. 将过短的尾页(≤2 字符)合并到前一页
|
||||
private func normalizeTrailingFrames(
|
||||
_ frames: [RDEPUBTextLayoutFrame],
|
||||
content: NSAttributedString,
|
||||
@@ -464,6 +562,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
|
||||
var normalized = frames
|
||||
|
||||
// 丢弃纯空白的尾页
|
||||
while let lastFrame = normalized.last,
|
||||
shouldDropWhitespaceOnlyTrailingFrame(lastFrame, in: content) {
|
||||
normalized.removeLast()
|
||||
@@ -476,6 +575,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// 将过短的尾页合并到前一页
|
||||
guard normalized.count > 1,
|
||||
let lastFrame = normalized.last,
|
||||
let previousFrame = normalized.dropLast().last,
|
||||
@@ -489,6 +589,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
return normalized
|
||||
}
|
||||
|
||||
/// 判断尾页是否为纯空白(无可显示字符且无附件)
|
||||
private func shouldDropWhitespaceOnlyTrailingFrame(
|
||||
_ frame: RDEPUBTextLayoutFrame,
|
||||
in content: NSAttributedString
|
||||
@@ -500,6 +601,8 @@ public final class RDEPUBTextBookBuilder {
|
||||
return visibleCharacterCount(in: content, range: frame.contentRange) == 0
|
||||
}
|
||||
|
||||
/// 判断尾页是否过短需要合并到前一页。
|
||||
/// 条件:尾页 ≤ 2 个可见字符,且前一页的字符数是尾页的 8 倍以上(至少 12 个字符)
|
||||
private func shouldMergeShortTrailingFrame(
|
||||
_ trailingFrame: RDEPUBTextLayoutFrame,
|
||||
previousFrame: RDEPUBTextLayoutFrame,
|
||||
@@ -522,6 +625,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
return previousVisibleCount >= max(visibleCount * 8, 12)
|
||||
}
|
||||
|
||||
/// 将尾页合并到前一页,保留尾页的分页原因
|
||||
private func mergeTrailingFrame(
|
||||
_ previousFrame: RDEPUBTextLayoutFrame,
|
||||
with trailingFrame: RDEPUBTextLayoutFrame
|
||||
@@ -547,6 +651,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
/// 统计指定范围内可见字符数(排除空白和控制字符)
|
||||
private func visibleCharacterCount(
|
||||
in content: NSAttributedString,
|
||||
range: NSRange
|
||||
@@ -560,6 +665,9 @@ public final class RDEPUBTextBookBuilder {
|
||||
return filteredScalars.count
|
||||
}
|
||||
|
||||
// MARK: - 集合工具方法
|
||||
|
||||
/// 数组去重(保持顺序)
|
||||
private func uniqueValues<T: Equatable>(from values: [T]) -> [T] {
|
||||
values.reduce(into: [T]()) { result, value in
|
||||
if !result.contains(value) {
|
||||
@@ -568,6 +676,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// NSRange 数组去重(保持顺序)
|
||||
private func uniqueRanges(from ranges: [NSRange]) -> [NSRange] {
|
||||
ranges.reduce(into: [NSRange]()) { result, value in
|
||||
if !result.contains(value) {
|
||||
@@ -576,6 +685,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// 统计指定范围内附件数量
|
||||
private func attachmentCount(in content: NSAttributedString, range: NSRange) -> Int {
|
||||
guard content.length > 0, range.length > 0 else { return 0 }
|
||||
var count = 0
|
||||
@@ -587,6 +697,9 @@ public final class RDEPUBTextBookBuilder {
|
||||
return count
|
||||
}
|
||||
|
||||
// MARK: - 缓存键生成
|
||||
|
||||
/// 生成缓存键:基于书籍 ID、字号、行距、页面尺寸等参数的 SHA256 哈希
|
||||
private func makeCacheKey(
|
||||
bookID: String,
|
||||
pageSize: CGSize,
|
||||
|
||||
Reference in New Issue
Block a user