feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
# CFI 子系统文档
|
||||
|
||||
> 最后更新:2026-06-18
|
||||
|
||||
本文档详细描述 ReadViewSDK 中 EPUB CFI(Canonical Fragment Identifier)子系统的架构、数据模型、解析流程与容错机制。
|
||||
|
||||
---
|
||||
|
||||
## 1. CFI 规范简介
|
||||
|
||||
EPUB CFI 是 EPUB 3 规范定义的标准化片段标识符,用于精确定位 EPUB 内容中的任意位置。其语法形式为:
|
||||
|
||||
```
|
||||
epubcfi(/6/4!ch01.xhtml/4/2/1:3)
|
||||
├──────┘ ├────────┘ ├──────┘ └─┘
|
||||
│ │ │ └─ 字符偏移(characterOffset)
|
||||
│ │ └─ 内容路径(contentPath):定位 DOM 节点
|
||||
│ └─ 包路径(packagePath):定位 OPF manifest 中的资源
|
||||
└─ epubcfi() 包装器
|
||||
```
|
||||
|
||||
关键规则:
|
||||
- **步进(step)**:以 `/` 分隔,偶数索引表示元素节点,奇数索引表示文本节点
|
||||
- **id 断言**:`[id]` 形式,如 `/4[ch01.xhtml]`,用于增强定位鲁棒性
|
||||
- **范围 CFI**:`epubcfi(/parent,/start,/end)` 三段逗号分隔,表示起止范围
|
||||
- **侧偏**:`;s=b`(before)或 `;s=a`(after),指示锚点偏向
|
||||
- **文本断言**:`[prefix,exact,suffix]`,用于断言定位处的文本内容
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心数据模型
|
||||
|
||||
### 2.1 RDEPUBCFI
|
||||
|
||||
顶层 CFI 模型,对应一个完整的 `epubcfi(...)` 字符串。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFI: Codable, Equatable, Hashable {
|
||||
public var rawValue: String // 原始 CFI 字符串
|
||||
public var packagePath: RDEPUBCFIPath // 包路径(定位 XHTML 文件)
|
||||
public var contentPath: RDEPUBCFIPath // 内容路径(定位 DOM 节点)
|
||||
public var characterOffset: Int? // 文本节点内的字符偏移
|
||||
public var sideBias: RDEPUBCFISideBias? // 侧偏方向
|
||||
public var textAssertion: RDEPUBCFITextAssertion? // 文本断言
|
||||
}
|
||||
```
|
||||
|
||||
**侧偏枚举**:
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFISideBias: String, Codable {
|
||||
case before = "b" // 锚点偏向起始侧
|
||||
case after = "a" // 锚点偏向结束侧
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:解析 `epubcfi(/6/4[ch01.xhtml]!/4/2/1:100;s=b)` 后:
|
||||
- `packagePath.steps` = `[Step(index: 6), Step(index: 4, idAssertion: "ch01.xhtml")]`
|
||||
- `contentPath.steps` = `[Step(index: 4), Step(index: 2), Step(index: 1)]`
|
||||
- `characterOffset` = 100
|
||||
- `sideBias` = `.before`
|
||||
|
||||
### 2.2 RDEPUBCFIPath
|
||||
|
||||
路径模型,由一组 `RDEPUBCFIStep` 组成,描述从根节点到目标节点的遍历序列。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIPath: Codable, Equatable, Hashable {
|
||||
public var steps: [RDEPUBCFIStep]
|
||||
|
||||
// 计算两条路径的公共前缀
|
||||
public func commonPrefix(with other: RDEPUBCFIPath) -> RDEPUBCFIPath
|
||||
|
||||
// 去除指定前缀后的剩余路径
|
||||
public func droppingPrefix(_ prefix: RDEPUBCFIPath) -> RDEPUBCFIPath
|
||||
}
|
||||
```
|
||||
|
||||
**RDEPUBCFIStep**:
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIStep: Codable, Equatable, Hashable {
|
||||
public var index: Int // 节点索引(偶数=元素,奇数=文本)
|
||||
public var idAssertion: String? // 可选的 id 断言,如 "ch01.xhtml"
|
||||
}
|
||||
```
|
||||
|
||||
`commonPrefix` 方法在范围 CFI 序列化时用于提取起止点的公共父路径;`droppingPrefix` 用于生成相对于父级的路径片段。
|
||||
|
||||
### 2.3 RDEPUBCFIRange
|
||||
|
||||
范围模型,表示文档中的一个连续区域,由父级 CFI 和起止 CFI 组成。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
|
||||
public var rawValue: String // 原始范围 CFI 字符串
|
||||
public var parent: RDEPUBCFI? // 公共父级(起止共享的前缀路径)
|
||||
public var start: RDEPUBCFI // 起始位置
|
||||
public var end: RDEPUBCFI // 结束位置
|
||||
}
|
||||
```
|
||||
|
||||
序列化时输出格式:`epubcfi(/parent_path,/start_terminal,/end_terminal)`,其中起止路径相对于父级路径输出。
|
||||
|
||||
---
|
||||
|
||||
## 3. 解析与序列化
|
||||
|
||||
### 3.1 RDEPUBCFIParser
|
||||
|
||||
将 CFI 字符串解析为结构化模型。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIParser {
|
||||
// 解析单点 CFI
|
||||
public static func parse(_ rawValue: String?) throws -> RDEPUBCFI
|
||||
|
||||
// 解析范围 CFI
|
||||
public static func parseRange(_ rawValue: String?) throws -> RDEPUBCFIRange
|
||||
}
|
||||
```
|
||||
|
||||
**解析流程**(`parse` 方法):
|
||||
|
||||
1. **去包装**:剥离 `epubcfi(...)` 外层,提取 body
|
||||
2. **拆分包路径与内容路径**:以第一个 `!` 为分隔符
|
||||
3. **解析路径**:按 `/` 分割为 step,每个 step 可含 `[idAssertion]`
|
||||
4. **解析偏移与限定符**:从内容路径中提取 `:offset`、`;s=b/a`、`[textAssertion]`
|
||||
|
||||
**关键细节**:
|
||||
- `firstIndexOutsideBrackets` 方法确保 `:` 分隔符在方括号外才被识别,避免与文本断言中的内容混淆
|
||||
- `parseRange` 要求恰好 3 个逗号分隔部分(parent, start, end),否则抛出 `unsupportedRange` 错误
|
||||
|
||||
**使用示例**:
|
||||
|
||||
```swift
|
||||
let cfi = try RDEPUBCFIParser.parse(
|
||||
"epubcfi(/6/4[ch01.xhtml]!/4/2/1:50;s=a[前缀,目标文本,后缀])"
|
||||
)
|
||||
print(cfi.characterOffset) // Optional(50)
|
||||
print(cfi.sideBias) // Optional(.after)
|
||||
print(cfi.textAssertion?.exact) // Optional("目标文本")
|
||||
```
|
||||
|
||||
### 3.2 RDEPUBCFISerializer
|
||||
|
||||
将 CFI 模型序列化为标准字符串。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFISerializer {
|
||||
// 序列化单点 CFI
|
||||
public static func serialize(_ cfi: RDEPUBCFI) -> String
|
||||
|
||||
// 序列化范围 CFI
|
||||
public static func serializeRange(_ range: RDEPUBCFIRange) -> String
|
||||
|
||||
// 序列化路径为 step 字符串
|
||||
public static func serializePath(_ path: RDEPUBCFIPath) -> String
|
||||
}
|
||||
```
|
||||
|
||||
**序列化逻辑**:
|
||||
- 若 `rawValue` 非空,直接返回(避免重复序列化)
|
||||
- 范围序列化先通过 `canonicalRangeComponents` 提取或推导公共父级,再分别序列化起止终端路径(相对于父级)
|
||||
- 终端路径使用 `droppingPrefix` 去除与父级共享的部分
|
||||
|
||||
**示例**:
|
||||
|
||||
```swift
|
||||
let cfi = RDEPUBCFIGenerator.makeOffsetCFI(
|
||||
href: "chapter1.xhtml", fileIndex: 0, chapterOffset: 120
|
||||
)
|
||||
let serialized = RDEPUBCFISerializer.serialize(cfi)
|
||||
// => "epubcfi(/6/2[chapter1.xhtml]!/4/2:120)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. DOM 解析与生成
|
||||
|
||||
### 4.1 RDEPUBCFIResolver
|
||||
|
||||
将 CFI 解析为可直接用于资源定位的结果。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIResolverResult: Equatable {
|
||||
public var href: String? // XHTML 文件路径(来自 idAssertion)
|
||||
public var fileIndex: Int? // 文件索引
|
||||
public var chapterOffset: Int? // 章节内字符偏移
|
||||
public var fragmentID: String? // 片段 ID(如 #section1)
|
||||
}
|
||||
|
||||
public enum RDEPUBCFIResolver {
|
||||
public static func resolve(_ cfi: RDEPUBCFI) -> RDEPUBCFIResolverResult
|
||||
}
|
||||
```
|
||||
|
||||
**解析规则**:
|
||||
- `href`:从 `packagePath` 中最后一个含 `idAssertion` 的 step 提取
|
||||
- `fileIndex`:`packagePath` 中倒数第二个 step 的 `(index / 2) - 1`
|
||||
- `fragmentID`:从 `contentPath` 中最后一个含 `idAssertion` 的 step 提取
|
||||
- `chapterOffset`:直接取 `cfi.characterOffset`
|
||||
|
||||
**示例**:
|
||||
|
||||
```swift
|
||||
let cfi = try RDEPUBCFIParser.parse("epubcfi(/6/4[ch01.xhtml]!/4/2[chap1]:80)")
|
||||
let result = RDEPUBCFIResolver.resolve(cfi)
|
||||
print(result.href) // Optional("ch01.xhtml")
|
||||
print(result.fileIndex) // Optional(0)
|
||||
print(result.fragmentID) // Optional("chap1")
|
||||
print(result.chapterOffset) // Optional(80)
|
||||
```
|
||||
|
||||
### 4.2 RDEPUBCFIGenerator
|
||||
|
||||
从已知的章节信息生成 CFI。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIGenerator {
|
||||
// 根据偏移量生成单点 CFI
|
||||
public static func makeOffsetCFI(
|
||||
href: String,
|
||||
fileIndex: Int,
|
||||
chapterOffset: Int,
|
||||
fragmentID: String? = nil,
|
||||
sideBias: RDEPUBCFISideBias? = nil,
|
||||
textAssertion: RDEPUBCFITextAssertion? = nil
|
||||
) -> RDEPUBCFI
|
||||
|
||||
// 根据自定义内容路径生成 CFI
|
||||
public static func makeCFI(
|
||||
href: String,
|
||||
fileIndex: Int,
|
||||
contentPath: RDEPUBCFIPath,
|
||||
characterOffset: Int,
|
||||
sideBias: RDEPUBCFISideBias? = nil,
|
||||
textAssertion: RDEPUBCFITextAssertion? = nil
|
||||
) -> RDEPUBCFI
|
||||
|
||||
// 生成范围 CFI(起止偏移量)
|
||||
public static func makeOffsetRangeCFI(
|
||||
href: String,
|
||||
fileIndex: Int,
|
||||
startOffset: Int,
|
||||
endOffset: Int,
|
||||
fragmentID: String? = nil,
|
||||
startTextAssertion: RDEPUBCFITextAssertion? = nil,
|
||||
endTextAssertion: RDEPUBCFITextAssertion? = nil
|
||||
) -> RDEPUBCFIRange
|
||||
}
|
||||
```
|
||||
|
||||
**路径构造规则**:
|
||||
- `packagePath` 固定为 `[Step(index: 6), Step(index: (fileIndex+1)*2, idAssertion: href)]`
|
||||
- `contentPath` 默认为 `[Step(index: 4), Step(index: 2, idAssertion: fragmentID)]`
|
||||
- 范围 CFI 的起始点自动附加 `sideBias: .before`,结束点附加 `sideBias: .after`
|
||||
|
||||
### 4.3 RDEPUBCFIDOMPathBuilder
|
||||
|
||||
从 HTML 源码中提取带有 `id` 属性的元素路径映射。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIDOMPathBuilder {
|
||||
// 从 HTML 中提取 fragmentID -> CFIPath 映射
|
||||
public static func fragmentPaths(in html: String) -> [String: RDEPUBCFIPath]
|
||||
}
|
||||
```
|
||||
|
||||
**实现细节**:
|
||||
- 使用正则表达式匹配 HTML 标签,维护一个模拟 DOM 栈
|
||||
- 每遇到开标签,计算子节点索引(`childIndex * 2`),生成 `RDEPUBCFIStep`
|
||||
- 从标签属性中提取 `id` 或 `xml:id` 作为 `idAssertion`
|
||||
- 识别并跳过 void 元素(`br`、`img`、`input` 等)和可忽略标签(`!doctype`)
|
||||
- 闭标签时弹出栈顶,重置该深度的子节点计数
|
||||
|
||||
---
|
||||
|
||||
## 5. 容错恢复引擎(RDEPUBCFIRecoveryEngine)
|
||||
|
||||
当 CFI 精确定位失败时(如 DOM 结构变更),恢复引擎通过多级降级策略尝试找到最佳匹配位置。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
|
||||
|
||||
### 5.1 恢复置信度
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIRecoveryResult: Equatable {
|
||||
public enum Confidence: Int, Codable {
|
||||
case exactPath // 精确路径匹配
|
||||
case assertionCalibrated // 路径匹配 + 文本断言校准
|
||||
case siblingRecovered // 兄弟节点恢复
|
||||
case tokenRecovered // 词元索引恢复
|
||||
case fragmentFallback // 片段 ID 降级
|
||||
case offsetFallback // 偏移量兜底
|
||||
}
|
||||
public var chapterOffset: Int
|
||||
public var confidence: Confidence
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 恢复策略(按优先级)
|
||||
|
||||
| 优先级 | 策略 | 说明 |
|
||||
|--------|------|------|
|
||||
| 1 | `exactPath` | 在 CFIMap 中查找精确匹配的 marker 或 pathRange |
|
||||
| 2 | `assertionCalibrated` | 精确路径匹配后,使用文本断言在窗口内校准偏移 |
|
||||
| 3 | `siblingRecovered` | 查找同父级的兄弟节点,通过 `siblingScore` 选择最近匹配 |
|
||||
| 4 | `tokenRecovered` | 利用词元索引(token index)在章节文本中搜索匹配 |
|
||||
| 5 | `fragmentFallback` | 回退到 fragment ID 对应的已知偏移量 |
|
||||
| 6 | `offsetFallback` | 使用兜底偏移量 |
|
||||
|
||||
### 5.3 核心算法
|
||||
|
||||
**校准机制**(`calibratedOffset`):
|
||||
- 在给定偏移量附近 ±512 字符窗口内搜索 `textAssertion.exact` 文本
|
||||
- 对每个候选位置计算 `assertionScore`:距离越近分数越低,prefix/suffix 匹配则大幅加分(不匹配 +10000)
|
||||
- 选取得分最低的候选位置
|
||||
|
||||
**兄弟恢复**(`siblingRecovered`):
|
||||
- 查找与目标路径同父级、同深度的已知 marker
|
||||
- 通过 `siblingSignature`(`parent_path#childIndex`)和索引距离计算相似度分数
|
||||
|
||||
**词元恢复**(`tokenRecovered`):
|
||||
- 在预构建的 token index 中查找包含 `textAssertion.exact` 的词元
|
||||
- 对每个候选锚点执行校准,选择校准距离最小的结果
|
||||
|
||||
**调用入口**:
|
||||
|
||||
```swift
|
||||
let result = RDEPUBCFIRecoveryEngine.recover(
|
||||
cfi: someCFI,
|
||||
cfiMap: chapterCFIMap,
|
||||
chapterText: "章节纯文本...",
|
||||
fragmentOffsets: ["section1": 150, "section2": 800],
|
||||
fallbackOffset: 0,
|
||||
lastOffset: 5000
|
||||
)
|
||||
// result?.confidence 反映恢复质量
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 文本断言验证(RDEPUBCFITextAssertion)
|
||||
|
||||
文本断言用于在 CFI 定位后验证所指位置的文本内容是否符合预期,增强定位鲁棒性。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFITextAssertion: Codable, Equatable, Hashable {
|
||||
public var prefix: String? // 目标文本之前的上下文
|
||||
public var exact: String? // 精确匹配的目标文本
|
||||
public var suffix: String? // 目标文本之后的上下文
|
||||
}
|
||||
```
|
||||
|
||||
**在 CFI 中的表示**:`[prefix,exact,suffix]`,位于偏移量和侧偏之后。
|
||||
|
||||
**示例**:
|
||||
|
||||
```swift
|
||||
// CFI: epubcfi(/6/4[ch01.xhtml]!/4/2/1:100;s=a[这是一段,目标文本,后续内容])
|
||||
let assertion = cfi.textAssertion
|
||||
print(assertion?.prefix) // Optional("这是一段")
|
||||
print(assertion?.exact) // Optional("目标文本")
|
||||
print(assertion?.suffix) // Optional("后续内容")
|
||||
```
|
||||
|
||||
**容错引擎中的应用**:
|
||||
- `exact` 用于在窗口内搜索实际文本位置
|
||||
- `prefix` 和 `suffix` 用于对候选位置评分,优先选择上下文都匹配的位置
|
||||
|
||||
---
|
||||
|
||||
## 7. 兼容性处理(RDEPUBCFICompatibility)
|
||||
|
||||
提供宽松解析接口,兼容非标准 CFI 格式。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFICompatibility {
|
||||
// 宽松解析单点 CFI(解析失败返回 nil 而非抛异常)
|
||||
public static func parseLossy(_ rawValue: String?) -> RDEPUBCFI?
|
||||
|
||||
// 宽松解析范围 CFI
|
||||
public static func parseRangeLossy(_ rawValue: String?) -> RDEPUBCFIRange?
|
||||
}
|
||||
```
|
||||
|
||||
**范围 CFI 兼容策略**:
|
||||
- 优先使用标准 `parseRange`(逗号三分段格式)
|
||||
- 若失败,尝试以 `..` 或 `-` 作为分隔符拆分为两个独立 CFI 分别解析
|
||||
- 支持 `epubcfi(...)-epubcfi(...)` 或 `epubcfi(...)..epubcfi(...)` 格式
|
||||
|
||||
**使用场景**:处理第三方生成器或旧版本导出的非标准范围标记。
|
||||
|
||||
---
|
||||
|
||||
## 8. CFI 映射(RDEPUBCFIMap)
|
||||
|
||||
CFI 映射是章节级别的索引结构,将 CFI 路径映射到章节文本偏移量,是容错恢复引擎的核心数据源。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
|
||||
|
||||
### 8.1 RDEPUBCFIMap
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIMap: Codable, Equatable {
|
||||
public var href: String // 章节文件路径
|
||||
public var renderVersion: Int // 渲染版本号
|
||||
public var domVersion: Int // DOM 版本号
|
||||
public var markers: [RDEPUBCFIMarker] // CFI 路径到偏移量的标记列表
|
||||
public var textAssertions: [String: RDEPUBCFITextAssertion] // 文本断言缓存
|
||||
public var pathRanges: [RDEPUBCFIPathRange] // 路径范围列表
|
||||
public var recoveryMetadata: RDEPUBCFIRecoveryMetadata // 恢复元数据
|
||||
|
||||
// 精确匹配 marker
|
||||
public func marker(matching path: RDEPUBCFIPath) -> RDEPUBCFIMarker?
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 RDEPUBCFIMarker
|
||||
|
||||
每个 marker 记录一个 CFI 路径对应的章节信息:
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIMarker: Codable, Equatable {
|
||||
public var cfiPath: RDEPUBCFIPath // CFI 路径
|
||||
public var chapterOffset: Int? // 章节内字符偏移
|
||||
public var fragmentID: String? // 片段 ID
|
||||
public var textNodeLength: Int? // 文本节点长度
|
||||
public var textNodeChecksum: UInt64? // 文本节点校验和(FNV-1a 64)
|
||||
public var normalizedTextPreview: String? // 归一化文本预览(前 24 字符)
|
||||
public var domSiblingSignature: String? // DOM 兄弟签名
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 RDEPUBCFIPathRange
|
||||
|
||||
描述一个 CFI 路径对应的文本偏移范围:
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIPathRange: Codable, Equatable {
|
||||
public var cfiPath: RDEPUBCFIPath // CFI 路径
|
||||
public var startOffset: Int // 范围起始偏移
|
||||
public var endOffset: Int // 范围结束偏移
|
||||
public var textNodeLength: Int // 文本节点长度
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 RDEPUBCFIRecoveryMetadata
|
||||
|
||||
恢复元数据,为容错引擎提供多维度恢复依据:
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIRecoveryMetadata: Codable, Equatable {
|
||||
public var domFingerprint: String // DOM 指纹(SHA-256)
|
||||
public var normalizedTextChecksum: String // 归一化文本校验和
|
||||
public var tokenIndex: [RDEPUBCFITokenAnchor] // 词元锚点索引
|
||||
public var fragmentPathMap: [String: RDEPUBCFIPath] // fragment -> 路径映射
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 RDEPUBCFITokenAnchor
|
||||
|
||||
词元锚点,用于基于内容的恢复:
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFITokenAnchor: Codable, Equatable {
|
||||
public var token: String // 采样词元(12 字符窗口,96 步长,最多 64 个)
|
||||
public var occurrence: Int // 该词元的出现次数
|
||||
public var chapterOffset: Int // 对应的章节偏移
|
||||
public var cfiPath: RDEPUBCFIPath // 对应的 CFI 路径
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 映射构建流程
|
||||
|
||||
`RDEPUBCFITextNodeMapBuilder.makeMap` 方法负责构建完整映射:
|
||||
|
||||
1. **归一化文本**:解码 HTML 实体、合并连续空白、统一空白字符
|
||||
2. **提取 fragment 路径**:通过 `RDEPUBCFIDOMPathBuilder.fragmentPaths` 获取 id -> path 映射
|
||||
3. **构建 markers**:逐标签遍历 HTML,维护 DOM 栈,为每个文本节点创建 marker
|
||||
4. **构建 pathRanges**:将 markers 转换为偏移范围列表
|
||||
5. **构建恢复元数据**:生成 DOM 指纹、文本校验和、词元锚点索引
|
||||
|
||||
---
|
||||
|
||||
## 9. 错误类型(RDEPUBCFIError)
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIError: Error, Equatable {
|
||||
case empty // 输入为空或空白
|
||||
case invalidWrapper(String) // 缺少 epubcfi() 包装
|
||||
case invalidPath(String) // 路径格式无效(不以 / 开头)
|
||||
case unsupportedRange(String) // 范围格式不正确(非三段逗号分隔)
|
||||
}
|
||||
```
|
||||
|
||||
| 错误 | 触发条件 |
|
||||
|------|----------|
|
||||
| `empty` | 输入为 `nil`、空字符串或纯空白 |
|
||||
| `invalidWrapper` | 未以 `epubcfi(` 开头或未以 `)` 结尾 |
|
||||
| `invalidPath` | 路径部分非空且不以 `/` 开头 |
|
||||
| `unsupportedRange` | 范围 CFI 的逗号分隔部分不等于 3 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 使用场景与调用链
|
||||
|
||||
### 10.1 保存阅读位置
|
||||
|
||||
```
|
||||
用户翻页
|
||||
→ RDEPUBCFIGenerator.makeOffsetCFI(href, fileIndex, chapterOffset)
|
||||
→ RDEPUBCFISerializer.serialize(cfi)
|
||||
→ 存储 epubcfi(...) 字符串
|
||||
```
|
||||
|
||||
### 10.2 恢复阅读位置
|
||||
|
||||
```
|
||||
加载 epubcfi(...) 字符串
|
||||
→ RDEPUBCFIParser.parse(rawValue)
|
||||
→ RDEPUBCFIResolver.resolve(cfi)
|
||||
→ 获取 href, fileIndex, chapterOffset
|
||||
→ 若偏移量无效,进入恢复引擎
|
||||
→ RDEPUBCFIRecoveryEngine.recover(cfi, cfiMap, chapterText, ...)
|
||||
→ 依次尝试 exactPath → sibling → token → fragment → offset
|
||||
```
|
||||
|
||||
### 10.3 高亮选中文本
|
||||
|
||||
```
|
||||
用户选择文本范围
|
||||
→ 获取 start/end DOM 位置
|
||||
→ RDEPUBCFIGenerator.makeOffsetRangeCFI(href, fileIndex, startOffset, endOffset)
|
||||
→ RDEPUBCFISerializer.serializeRange(range)
|
||||
→ 存储范围 CFI
|
||||
```
|
||||
|
||||
### 10.4 章节加载时构建索引
|
||||
|
||||
```
|
||||
章节 HTML 加载完成
|
||||
→ RDEPUBCFITextNodeMapBuilder.makeMap(href, rawHTML, chapterText, fragmentOffsets)
|
||||
→ 生成 RDEPUBCFIMap(markers + pathRanges + recoveryMetadata)
|
||||
→ 缓存供后续恢复使用
|
||||
```
|
||||
|
||||
### 10.5 兼容性解析
|
||||
|
||||
```
|
||||
外部导入非标准 CFI
|
||||
→ RDEPUBCFICompatibility.parseLossy(rawValue) // 宽松解析
|
||||
→ RDEPUBCFICompatibility.parseRangeLossy(rawValue) // 支持 .. / - 分隔符
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `RDEPUBCFI.swift` | 顶层 CFI 模型与侧偏枚举 |
|
||||
| `RDEPUBCFIPath.swift` | 路径模型与 step 定义 |
|
||||
| `RDEPUBCFIRange.swift` | 范围模型 |
|
||||
| `RDEPUBCFIParser.swift` | 字符串 → 模型解析 |
|
||||
| `RDEPUBCFISerializer.swift` | 模型 → 字符串序列化 |
|
||||
| `RDEPUBCFIResolver.swift` | CFI → 资源定位结果 |
|
||||
| `RDEPUBCFIGenerator.swift` | 章节信息 → CFI 生成 |
|
||||
| `RDEPUBCFIDOMPathBuilder.swift` | HTML → fragment 路径映射 + 文本节点映射构建 |
|
||||
| `RDEPUBCFIRecoveryEngine.swift` | 多级容错恢复引擎 |
|
||||
| `RDEPUBCFITextAssertion.swift` | 文本断言模型 |
|
||||
| `RDEPUBCFICompatibility.swift` | 非标准格式兼容解析 |
|
||||
| `RDEPUBCFIMap.swift` | CFI 映射、marker、恢复元数据 |
|
||||
| `RDEPUBCFIError.swift` | 错误类型定义 |
|
||||
Reference in New Issue
Block a user