- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
22 KiB
标准级 EPUB 定位与兼容能力开发蓝图
最后更新:2026-06-18 适用范围:
EPUBCore/CFI、EPUBCore/Notes、EPUBTextRendering、EPUBUI目标:把当前阅读器从“主流文本书可用”推进到“商业级 EPUB 阅读器必须具备的定位与兼容能力”。
1. 文档目标
本文不是概念方案,而是可直接拆任务、排期、开发和回归的实施蓝图,覆盖三项“必须做”的能力:
EPUB CFI标准级定位与持久化- 脚注 / 尾注弹层阅读
- 嵌入字体回退 + CSS 兼容层
其中 EPUB CFI 按完整版路线执行,核心要求是:
- 不依赖
id也能做章节内精确 DOM 定位 - 支持“阅读位置 -> CFI”和“CFI -> 阅读位置”的双向恢复
- 在换字号、换字体、换行距、重新分页后仍能稳定恢复
- 为高亮、书签、搜索命中、脚注锚点、未来跨设备同步提供统一锚点
2. 当前基线
当前仓库已经有一部分基础设施,不是从零开始:
2.1 已有能力
- 已有
RDEPUBLocation.cfi / lastCFI / rangeCFI - 已有
RDEPUBCFIParser / Serializer / Resolver / Generator - 已有
RDEPUBCFIMap和章节级marker - 已有基于文本节点路径的初版
cfiMap构建 - 已有脚注检测、解析和弹层 UI 的第一版入口
- 已有 CSS 兼容层与字体 fallback resolver 的第一版模型
2.2 当前仍然不够的地方
当前实现更像“标准级路线的 Phase 0.5”,还差以下闭环:
- 无
id节点的 DOM 路径恢复还只是“文本对齐驱动”,未达到标准级双向校准 CFI -> 章内偏移仍主要依赖单个 marker 命中,缺少区间级、断言级回退链- 还没有持久化“DOM 恢复所需的结构指纹”,章节缓存命中后无法做更强恢复
- 脚注弹层还没有完整纳入 CFI 锚点、回跳和高亮链路
- CSS 兼容层仍偏“样式修正函数”,还不是完整的兼容策略包
- 缺少样书库、诊断报告和标准级回归基线
3. 为什么要做“标准级无 id 节点双向恢复”
3.1 要解决的真实问题
如果只用 href + progression 或 href + chapterOffset:
- 用户改字号、字体、行距、分栏后,位置会漂
- 高亮恢复会落到错误字词
- 搜索命中重开书后会跳偏
- 脚注回跳在长章节里不稳定
- 将来做跨设备同步时,同步点不可复用
3.2 做到标准级后的收益
- 阅读位置恢复从“章节大致位置”升级到“字符级稳定锚点”
- 高亮、批注、书签、搜索、脚注全部共用同一套定位协议
- 章节重排后仍能尽可能落到同一语义位置
- 对不规范 EPUB 的容错能力显著提升
- 后续可以继续扩展到 WebView 路径、CFI 导出、跨端同步
4. 目标能力定义
4.1 CFI 能力分级
L1 基础可用
- 能解析 / 序列化单点 CFI 与 Range CFI
- 能持久化阅读位置和高亮范围
- 对带
id的节点定位稳定
L2 当前已接近
- 能为文本节点生成路径
- 能在章节内做初步文本节点映射
- 能在部分重排场景下恢复位置
L3 本文目标:标准级
- 无
id节点可精确恢复 - 位置恢复有多级回退链
- 断言、结构指纹、上下文窗口共同参与校准
- 章节缓存可直接携带恢复元数据
- 所有消费方统一走
CFI主链路
5. 总体架构
建议把定位系统拆成四层:
CFI Syntax LayerDOM Anchor Extraction LayerRecovery & Calibration LayerConsumer Integration Layer
5.1 CFI Syntax Layer
职责:
- 负责解析、序列化、生成、Range 拼装
- 不关心具体渲染结果
对应目录:
Sources/RDReaderView/EPUBCore/CFI/
5.2 DOM Anchor Extraction Layer
职责:
- 从原始 HTML 建立 DOM 路径
- 为文本节点、fragment、note anchor 建立索引
- 产出章节级恢复元数据
对应目录:
Sources/RDReaderView/EPUBCore/CFI/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/
5.3 Recovery & Calibration Layer
职责:
- 输入
CFI,输出稳定的章节字符偏移 - 输入字符偏移,输出稳定
CFI - 在 DOM、文本、分页变动时完成校准与回退
对应目录:
Sources/RDReaderView/EPUBTextRendering/Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/
5.4 Consumer Integration Layer
职责:
- 阅读位置恢复
- 高亮 / 批注 / 书签
- 搜索命中
- 脚注弹层与回跳
对应目录:
Sources/RDReaderView/EPUBUI/
6. 核心数据结构
6.1 在现有模型上补强,不推翻
RDEPUBCFIMap
现有结构:
public struct RDEPUBCFIMap: Codable, Equatable {
public var href: String
public var markers: [RDEPUBCFIMarker]
public var textAssertions: [String: RDEPUBCFITextAssertion]
}
建议扩展为:
public struct RDEPUBCFIMap: Codable, Equatable {
public var href: String
public var renderVersion: Int
public var domVersion: Int
public var markers: [RDEPUBCFIMarker]
public var textAssertions: [String: RDEPUBCFITextAssertion]
public var pathRanges: [RDEPUBCFIPathRange]
public var recoveryMetadata: RDEPUBCFIRecoveryMetadata
}
新增原因:
markers适合单点命中,但不足以做标准级回退- 需要显式记录“某个 DOM 路径覆盖哪段文本”
- 需要缓存 DOM 恢复辅助信息,避免每次重扫 HTML
RDEPUBCFIMarker
建议扩展字段:
public struct RDEPUBCFIMarker: Codable, Equatable {
public var cfiPath: RDEPUBCFIPath
public var chapterOffset: Int?
public var fragmentID: String?
public var textNodeLength: Int?
public var textNodeChecksum: UInt64?
public var normalizedTextPreview: String?
public var domSiblingSignature: String?
}
用途:
textNodeChecksum用于节点文本快速比对normalizedTextPreview用于短窗口断言domSiblingSignature用于路径偏移时的邻接恢复
新增 RDEPUBCFIPathRange
public struct RDEPUBCFIPathRange: Codable, Equatable {
public var cfiPath: RDEPUBCFIPath
public var startOffset: Int
public var endOffset: Int
public var textNodeLength: Int
}
用途:
- 表示某个文本节点在章节纯文本中的覆盖区间
- 支撑“按区间查找最近路径”
- 支撑
chapterOffset -> nearest CFI path
新增 RDEPUBCFIRecoveryMetadata
public struct RDEPUBCFIRecoveryMetadata: Codable, Equatable {
public var domFingerprint: String
public var normalizedTextChecksum: String
public var tokenIndex: [RDEPUBCFITokenAnchor]
public var fragmentPathMap: [String: RDEPUBCFIPath]
}
用途:
domFingerprint判断原始 HTML 是否变化normalizedTextChecksum判断章节标准化文本是否变化tokenIndex为无id恢复提供次级定位锚fragmentPathMap保留锚点和脚注入口
新增 RDEPUBCFITokenAnchor
public struct RDEPUBCFITokenAnchor: Codable, Equatable {
public var token: String
public var occurrence: Int
public var chapterOffset: Int
public var cfiPath: RDEPUBCFIPath
}
用途:
- 当文本节点路径不再精确命中时,用稀疏 token 锚做恢复
- 对长章节做局部二次定位
7. 标准级无 id 节点双向恢复算法
7.1 正向:阅读位置 -> CFI
输入:
fileIndexchapterOffsetRDEPUBCFIMap
输出:
- 标准
CFI - 如有需要,附带 text assertion
算法顺序:
- 在
pathRanges中查找覆盖chapterOffset的文本节点 - 计算该节点内的
localOffset - 生成
contentPath + characterOffset - 从前后文提取 assertion
- 若该节点不存在,则回退到最近
fragment - 若
fragment也缺失,则退化为 offset-backed CFI
关键要求
chapterOffset不能直接依赖页码- assertion 必须来自标准化文本,而不是原始 HTML 片段
- 对选区范围,
startCFI / endCFI必须分别独立生成,不能只存一个起点
7.2 逆向:CFI -> 阅读位置
输入:
RDEPUBCFIRDEPUBCFIMapchapterText
输出:
chapterOffset- 必要时输出
confidence
建议新增恢复结果:
public struct RDEPUBCFIRecoveryResult: Equatable {
public enum Confidence: Int {
case exactPath
case assertionCalibrated
case siblingRecovered
case tokenRecovered
case fragmentFallback
case offsetFallback
}
public var chapterOffset: Int
public var confidence: Confidence
}
恢复链路必须固定为:
exact-pathcfiPath直接命中 marker 或 pathRange
assertion-calibrated- 路径命中后,用 text assertion 微调偏移
sibling-recovered- 路径不命中时,用父路径 + 兄弟签名查找邻近文本节点
token-recovered- 使用 token anchor 在局部窗口重定位
fragment-fallback- 回退到最近 fragment 起点
offset-fallback- 使用旧式 chapterOffset 或 progression 兜底
这里的关键不是“某一步一定成功”,而是恢复链必须稳定、可解释、可诊断。
8. 为什么要加“结构指纹 + token 锚”
只做文本节点路径有两个问题:
- 某些 EPUB 会在无
id场景下插入大量包装标签,导致路径整体漂移 - 某些章节文本重复度高,单靠短 assertion 可能落到错误位置
所以标准级方案需要两层辅助:
8.1 结构指纹
针对每个文本节点记录:
- 父路径
- 左右兄弟摘要
- 标签名序列
- 局部文本 checksum
这样即便 cfiPath 因中间插入一个 wrapper 节点失效,也能在兄弟集合里恢复出最可能目标。
8.2 token 锚
在章节标准化文本中抽样稀疏 token,例如:
- 每
N个词或每M个中文字符窗口抽一个 token - 每个 token 记录其 occurrence、offset 和所属
cfiPath
当路径和断言都不稳时:
- 先用 token 锁定大致位置
- 再在局部窗口里做文本节点回溯
这就是“无 id 节点也能做标准级恢复”的核心。
9. 分阶段实施
Phase 1:结构补强与缓存升级
目标
让章节缓存携带足够的 DOM 恢复元数据,为后续标准级恢复打底。
需要改的模块
Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swiftSources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swiftSources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swiftSources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swiftSources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterSummaryDiskCache.swiftSources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift
具体任务
- 扩展
RDEPUBCFIMap、RDEPUBCFIMarker - 新增
RDEPUBCFIPathRange - 新增
RDEPUBCFIRecoveryMetadata - 在章节构建阶段产出
pathRanges / tokenIndex / domFingerprint - 升级 chapter summary schema version
- 为缓存加版本兼容分支,旧缓存 miss 后自动重建
验收
- 新章节缓存可落盘完整
cfiMap - 旧缓存不会引发崩溃
- 二次打开时不需要重新扫描整章 HTML 才能恢复定位
Phase 2:标准级 CFI -> offset 恢复链
目标
把当前“marker 命中 + chapterOffset 回退”的恢复方式升级为多级恢复链。
需要改的模块
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextIndexTable.swiftSources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterOffsetMap.swiftSources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift- 建议新增:
Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift
具体任务
- 抽出
RDEPUBCFIRecoveryEngine - 输出
RDEPUBCFIRecoveryResult - 实现六级恢复链
- 为每次恢复产出
confidence - 为 diagnostics 埋点:
- exact-path 命中率
- assertion 校准命中率
- token 恢复命中率
- offset 回退比例
验收
- 无
id文本节点可稳定恢复 - 换字号 / 字体 / 行距后,阅读位置恢复成功率显著提升
- 高亮与搜索命中恢复不再大面积偏移
Phase 3:标准级 offset -> CFI 生成链
目标
让所有阅读位置、选区、高亮和搜索结果都优先生成标准文本节点 CFI。
需要改的模块
Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swiftSources/RDReaderView/EPUBTextRendering/RDEPUBTextIndexTable.swiftSources/RDReaderView/EPUBTextRendering/RDEPUBChapterData.swiftSources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swiftSources/RDReaderView/EPUBTextRendering/RDEPUBTextSearchEngine.swift
具体任务
chapterOffset -> pathRange精确映射- 统一单点 CFI 与范围 CFI 生成
- 所有消费方优先写入
cfi/rangeCFI - 保留
fragment/progression作为兜底兼容字段
验收
- 书签、新建高亮、搜索命中都能生成标准 CFI
- 相同文本范围在重新分页后仍能正确恢复
Phase 4:脚注 / 尾注弹层闭环
目标
把脚注从“跳出当前阅读流”改为“就地预览 + 可回跳”。
需要改的模块
Sources/RDReaderView/EPUBCore/Notes/RDEPUBNoteDetector.swiftSources/RDReaderView/EPUBCore/Notes/RDEPUBNoteResolver.swiftSources/RDReaderView/EPUBCore/Notes/RDEPUBNoteModels.swiftSources/RDReaderView/EPUBUI/Notes/RDEPUBNotePopupCoordinator.swiftSources/RDReaderView/EPUBUI/Notes/RDEPUBNotePopupViewController.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift
具体任务
- 完善脚注链接识别:
epub:type=noterefrole=doc-noterefhref=#footnote-*- 章节内 / 跨章节 note link
- note target 解析后落为
CFI - 弹层展示注释正文,而不是强制整章跳转
- 弹层内支持:
- 查看上下文
- 跳到原文
- 跳到注释原位置
- 建立“入口位置 CFI -> 弹层 -> 回跳位置 CFI”的闭环
验收
- 点击脚注不打断当前阅读主流程
- 长注释可滚动
- 关闭弹层后能回到点击时的位置
- 跨章节尾注也能稳定打开
Phase 5:样式兼容层与字体回退闭环
目标
减少“某些 EPUB 打开排版异常”的商业级缺陷。
需要改的模块
Sources/RDReaderView/EPUBTextRendering/Typesetter/Compatibility/RDEPUBStyleCompatibilityModels.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/Compatibility/RDEPUBCSSCompatibilityLayer.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/Compatibility/RDEPUBFontFallbackResolver.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBStyleSheetComposer.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBHTMLNormalizer.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBRenderDiagnosticsCollector.swift
具体任务
- 建立 CSS 兼容策略包:
- 非法
font-family归一化 - 极端
line-height修正 - 过大
margin/padding收口 - 表格 / 图片 / SVG 溢出保护
- 嵌套
white-space异常归一化
- 非法
- 建立字体 fallback 链:
- 书内嵌入字体
- 用户选中字體
- 语言 fallback
- 系统兜底字体
- diagnostics 输出:
- 缺失字体
- 被修正的 CSS 规则
- 可能导致排版异常的资源
验收
- 缺字库 EPUB 不崩溃、不出现大面积 tofu
- 非法 CSS 不导致正文不可读
- diagnostics 可定位问题书源
10. 关键实现细节
10.1 DOM 路径构建规则
- 元素节点使用偶数 step
- 文本节点使用奇数 step
- 忽略注释节点、doctype、处理指令
script/style默认不参与文本定位ruby、rt、rp需要单独定义标准化策略,避免正文偏移
10.2 标准化文本规则必须固定
以下规则必须全链路共用同一份实现,否则 CFI 会漂:
- HTML entity 解码
- 连续空白压缩
- 换行归一化
- 零宽字符处理
nbsp处理- 附件占位字符策略
- 中文全角 / 半角是否归一
建议把规则集中到一个入口,避免 Builder、Search、Selection 各写一套。
10.3 renderSignature 与 domFingerprint 分工
renderSignature- 描述“分页语义”
- 用于判定页图、chapter summary、layout cache 是否可复用
domFingerprint- 描述“章节 DOM 结构”
- 用于判定
cfiMap是否可复用
这两个概念不能混用。
10.4 本地缓存策略
建议缓存分三层:
raw parse cache- OPF、manifest、spine、resource bytes
chapter summary cache- 章节文本、fragmentOffsets、cfiMap、pathRanges、tokenIndex
page map / runtime cache- 与具体排版参数绑定
原则:
- 字号、字体、行距变化时,第三层必须失效
- 第二层尽量复用
cfiMap属于第二层,不应因单纯重排被清空
11. 文件改动清单
11.1 必改文件
CFI 核心
Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swiftSources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swiftSources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swiftSources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift- 新增
Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift
构建与缓存
Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swiftSources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swiftSources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swiftSources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterSummaryDiskCache.swift
定位消费方
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextIndexTable.swiftSources/RDReaderView/EPUBTextRendering/RDEPUBChapterData.swiftSources/RDReaderView/EPUBTextRendering/RDEPUBTextSearchEngine.swiftSources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swiftSources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentView.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swiftSources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterOffsetMap.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLocationCoordinator.swift
脚注
Sources/RDReaderView/EPUBCore/Notes/RDEPUBNoteDetector.swiftSources/RDReaderView/EPUBCore/Notes/RDEPUBNoteResolver.swiftSources/RDReaderView/EPUBUI/Notes/RDEPUBNotePopupCoordinator.swiftSources/RDReaderView/EPUBUI/Notes/RDEPUBNotePopupViewController.swift
样式兼容
Sources/RDReaderView/EPUBTextRendering/Typesetter/Compatibility/RDEPUBStyleCompatibilityModels.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/Compatibility/RDEPUBCSSCompatibilityLayer.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/Compatibility/RDEPUBFontFallbackResolver.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBStyleSheetComposer.swift
11.2 建议新增测试
ReadViewDemo/ReadViewDemoTests/CFI/RDEPUBCFIRecoveryEngineTests.swiftReadViewDemo/ReadViewDemoTests/CFI/RDEPUBCFITextNodeMapBuilderTests.swiftReadViewDemo/ReadViewDemoTests/Notes/RDEPUBNoteResolverTests.swiftReadViewDemo/ReadViewDemoTests/Compatibility/RDEPUBCSSCompatibilityLayerTests.swiftReadViewDemo/ReadViewDemoUITests/ReaderUITests/LocationPersistenceTests.swiftReadViewDemo/ReadViewDemoUITests/ReaderUITests/FootnotePopupTests.swift
12. 回归样书与验收用例
至少准备以下样书:
- 标准文本 EPUB,节点
id完整 - 无
id文本 EPUB - 重复文本很多的长章节 EPUB
- 脚注密集型 EPUB
- 嵌入字体缺失或损坏 EPUB
- CSS 激进覆盖型 EPUB
- 跨章节尾注 EPUB
关键验收:
- 阅读到某章第 5 页,改字号后仍落在同一阅读语义位置
- 高亮一段正文,改字体后还能精确回到同一段
- 搜索命中结果重开书后可稳定定位
- 点击脚注弹层展示,不强制整章跳转
- 关闭脚注后回到点击前位置
- 不规范 CSS 的书籍仍可阅读
- 缺失嵌入字体时能稳定 fallback
13. 性能与风险要求
性能目标
- 单章节
cfiMap构建增量耗时 P50< 12ms,P95< 35ms CFI -> offset恢复耗时 P50< 2ms,P95< 8ms- 二次打开时不因 CFI 恢复而重新解析整本书
主要风险
- 章节标准化文本规则不统一,导致生成和恢复不一致
- token 锚过密,导致缓存膨胀
- 断言窗口过短,在重复文本章节误命中
- 兼容层修正规则过激,伤及正常书籍排版
对应策略:
- 文本标准化统一收口
- token 锚做稀疏抽样并设章节上限
- diagnostics 输出恢复置信度
- CSS 修正规则全部可开关并支持样书回归
14. 推荐排期
建议按 4 个迭代执行:
- 第 1 迭代
- Phase 1
- Phase 2
- 第 2 迭代
- Phase 3
- 核心位置恢复 UI 回归
- 第 3 迭代
- Phase 4
- 脚注体验打磨
- 第 4 迭代
- Phase 5
- 样书库、diagnostics、稳定性回归
15. 最终交付定义
完成本蓝图后,阅读器至少应达到以下标准:
- 阅读位置、高亮、书签、搜索命中全部优先基于标准 CFI
- 无
id节点的 EPUB 仍能做精确字符级恢复 - 脚注 / 尾注不再破坏主阅读流
- 字体缺失和异常 CSS 不再轻易把正文排坏
- 章节缓存、页图缓存、定位缓存职责清晰,二次打开可复用
这时阅读器才算从“功能可用”进入“商业级可发布”的基线。