feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 文本书籍构建器,负责将 EPUB publication 渲染、分页并组装为 `RDEPUBTextBook`。
|
||||
public final class RDEPUBTextBookBuilder {
|
||||
private let renderer: RDEPUBTextRenderer
|
||||
private let cache: RDEPUBTextBookCache?
|
||||
@@ -12,20 +11,14 @@ public final class RDEPUBTextBookBuilder {
|
||||
private let cacheCoordinator: RDEPUBPaginationCacheCoordinator
|
||||
private let diagnosticsReporter: RDEPUBBuildDiagnosticsReporter
|
||||
|
||||
/// 最后一次构建的资源引用诊断(样式表、图片等)
|
||||
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)
|
||||
|
||||
/// 创建书籍构建器。
|
||||
/// - Parameters:
|
||||
/// - renderer: 文本渲染器
|
||||
/// - cache: 分页缓存(可选)
|
||||
/// - layoutConfig: 页面布局配置
|
||||
public init(
|
||||
renderer: RDEPUBTextRenderer,
|
||||
cache: RDEPUBTextBookCache? = nil,
|
||||
@@ -42,7 +35,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
self.diagnosticsReporter = RDEPUBBuildDiagnosticsReporter()
|
||||
}
|
||||
|
||||
/// 默认构造器,使用 DTCoreText 渲染器
|
||||
public convenience init() {
|
||||
self.init(renderer: RDEPUBDTCoreTextRenderer())
|
||||
}
|
||||
@@ -51,7 +43,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug")
|
||||
}
|
||||
|
||||
/// 生成最近一次构建的语义摘要,用于 Phase 7 质量检测日志
|
||||
public func phase7SemanticSummary(title: String? = nil) -> String? {
|
||||
diagnosticsReporter.phase7SemanticSummary(
|
||||
title: title,
|
||||
@@ -59,16 +50,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
/// 核心构建方法:从 EPUB publication 构建分页书籍。
|
||||
///
|
||||
/// 流程:
|
||||
/// 1. 生成缓存键,尝试加载分页缓存
|
||||
/// 2. 遍历 spine 中的线性 HTML 章节
|
||||
/// 3. 渲染每章 HTML → NSAttributedString
|
||||
/// 4. 跳过空白的封面/扉页章节
|
||||
/// 5. 分页:缓存命中则使用缓存的页范围,否则调用 CoreText 分页引擎
|
||||
/// 6. 尾页规范化:丢弃纯空白尾页、合并过短尾页
|
||||
/// 7. 构建 RDEPUBTextBook 并保存分页缓存
|
||||
public func build(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
@@ -88,7 +69,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
|
||||
let buildStart = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
// 缓存查询 — WXRead 模式:只加载每章的页范围,不缓存富文本
|
||||
let bookID = publication.metadata.identifier ?? publication.metadata.title
|
||||
let cacheKey = cacheCoordinator.cacheKey(bookID: bookID, pageSize: pageSize, style: style)
|
||||
let cachedPagination = cacheCoordinator.load(key: cacheKey)
|
||||
@@ -134,7 +114,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
|
||||
sampler.totalBuildDuration = CFAbsoluteTimeGetCurrent() - buildStart
|
||||
|
||||
// 保存分页缓存(只缓存页范围和分页原因,不缓存富文本)
|
||||
cacheCoordinator.save(chapters: chapters, key: cacheKey)
|
||||
|
||||
#if DEBUG
|
||||
@@ -145,7 +124,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
return book
|
||||
}
|
||||
|
||||
/// 构建指定 spine 章节,用于大书快速首屏和后台增量补齐。
|
||||
public func buildChapter(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
@@ -192,6 +170,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
let request = RDEPUBTextTypesetterPipeline().makeRequest(
|
||||
from: RDEPUBTypesettingInput(
|
||||
href: item.href,
|
||||
spineIndex: spineIndex,
|
||||
title: chapterTitle,
|
||||
rawHTML: rawHTML,
|
||||
baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(),
|
||||
@@ -281,11 +260,13 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
|
||||
let paginateDuration = CFAbsoluteTimeGetCurrent() - paginateStart
|
||||
|
||||
let normalizedFrames = tailNormalizer.normalize(
|
||||
layoutFrames,
|
||||
content: content,
|
||||
href: item.href
|
||||
)
|
||||
|
||||
let effectiveFrames = normalizedFrames.isEmpty && content.length > 0
|
||||
? [
|
||||
RDEPUBTextLayoutFrame(
|
||||
@@ -339,6 +320,12 @@ public final class RDEPUBTextBookBuilder {
|
||||
title: chapterTitle,
|
||||
attributedContent: chapterAttributedContent,
|
||||
fragmentOffsets: rendered.fragmentOffsets,
|
||||
cfiMap: RDEPUBCFITextNodeMapBuilder.makeMap(
|
||||
href: item.href,
|
||||
rawHTML: rawHTML,
|
||||
chapterText: chapterAttributedContent.string,
|
||||
fragmentOffsets: rendered.fragmentOffsets
|
||||
),
|
||||
pageBreakReasons: pages.map(\.metadata.breakReason),
|
||||
pages: pages
|
||||
)
|
||||
@@ -365,9 +352,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -378,16 +362,12 @@ 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
|
||||
@@ -404,9 +384,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - 附件统计
|
||||
|
||||
/// 统计富文本中的附件数量
|
||||
private func attachmentCount(in content: NSAttributedString) -> Int {
|
||||
guard content.length > 0 else { return 0 }
|
||||
var count = 0
|
||||
@@ -418,7 +395,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
return count
|
||||
}
|
||||
|
||||
/// 获取富文本中所有附件的 NSRange 列表
|
||||
private func attachmentRanges(in content: NSAttributedString) -> [NSRange] {
|
||||
guard content.length > 0 else { return [] }
|
||||
var ranges: [NSRange] = []
|
||||
@@ -430,9 +406,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
return ranges
|
||||
}
|
||||
|
||||
// MARK: - 封面章节检测
|
||||
|
||||
/// 判断是否为纯图片封面章节(href 包含 cover 且有附件但几乎无文本)
|
||||
private func isAttachmentOnlyCoverChapter(
|
||||
item: RDEPUBSpineItem,
|
||||
content: NSAttributedString,
|
||||
@@ -445,6 +418,7 @@ public final class RDEPUBTextBookBuilder {
|
||||
}
|
||||
|
||||
private func debugPreview(for content: NSAttributedString, limit: Int) -> String {
|
||||
|
||||
let collapsed = content.string
|
||||
.replacingOccurrences(of: "\n", with: " ")
|
||||
.replacingOccurrences(of: "\r", with: " ")
|
||||
|
||||
Reference in New Issue
Block a user