- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
598 lines
20 KiB
Markdown
598 lines
20 KiB
Markdown
# 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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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/RDEpubReaderView/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` | 错误类型定义 |
|