# 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` | 错误类型定义 |