ReadViewSDK/_ssoft-output/implementation-artifacts/spec-cfi-11-issues-fix.md
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

9.6 KiB
Raw Blame History

title type created baseline_commit status context
CFI 模块 11 项问题修复 bugfix 2026-06-18 6ed3fcb done
Doc/CFI_ISSUES_REVIEW.md

意图

问题: CFI 模块存在 11 个已验证的代码问题涵盖数据一致性风险rawValue 短路、偏移计算错误UTF-16/Character 混用)、健壮性缺陷(正则解析 HTML、token 宽泛匹配、转义缺失)和代码质量问题(重复扩展、线性扫描、混合错误处理)。

方法: 按 Phase 1→2→3 顺序修复全部 11 个问题,保持 API 向后兼容。高优先级问题从根源修正数据模型,中优先级加固边界处理,低优先级提取共享代码和优化性能。

边界与约束

始终:

  • 保持 public API 签名不变,仅修改内部实现和 internal/private 代码
  • 所有偏移计算统一使用 UTF-16 基准(与 NSRange/NSString 一致)
  • 修复必须保持现有正确行为的测试不回归

先询问:

  • 是否为 #3content path 硬编码)引入新的 contentPath 参数到 makeOffsetCFI 公开 API
  • 是否为 #10线性扫描markers[RDEPUBCFIMarker] 改为含索引字典的结构

永不:

  • 不引入新的外部依赖(如 libxml2、DTCoreText
  • 不改变 CFI 字符串的序列化格式
  • 不修改 RDEPUBCFIMarkerRDEPUBCFITokenAnchor 等 Codable 结构的字段名(保持序列化兼容)

代码映射

  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFI.swift -- #1 rawValue 存储模型
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift -- #1 rawValue 短路序列化;#9 nilIfEmpty
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift -- #3 content path 硬编码;#8 end offset 边界
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift -- #2 偏移混用;#5 HTML 正则;#9 nilIfEmpty
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift -- #2 calibratedOffset#7 token 匹配
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift -- #4 结构假设
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift -- #6 text assertion 转义;#11 parent 静默失败;#9 nilIfEmpty
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift -- #10 线性扫描
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift -- #9 nilIfEmpty
  • Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift -- #9 nilIfEmpty

任务与验收

Phase 1 — 高优先级(影响正确性):

  • RDEPUBCFI.swift + RDEPUBCFISerializer.swift -- 将 rawValue 从存储属性改为 computed property基于组件实时序列化移除 Generator 中的 rawValue="" workaround -- #1 rawValue 短路导致序列化返回过期值
  • RDEPUBCFIDOMPathBuilder.swift -- 将 tokenSamples() 的 offset 从 Character 索引改为 UTF-16 索引,确保与 normalizedToChapterOffsets 数组下标对齐;同步修正 calibratedOffset 中 NSString 搜索与 Character 偏移的一致性 -- #2 UTF-16/Character 偏移混用导致 emoji/CJK 扩展字符定位错误
  • RDEPUBCFIGenerator.swift -- 为 makeOffsetCFI 添加可选 contentPath 参数(默认仍为 /4/2makeOffsetRangeCFI 透传该参数 -- #3 content path 硬编码 /4/2 在非标准 DOM 结构下定位失败

Phase 2 — 中优先级(影响健壮性):

  • RDEPUBCFIResolver.swift -- 用 steps.count >= 3 ? steps[2] : steps.last 替换 last(where:) 查找 manifest 步骤;用 steps.count >= 2 ? steps[1] : nil 替换 dropFirst().last;将 idAssertion?.isEmpty == false 改为 idAssertion?.nilIfEmpty != nil -- #4 Resolver 假设不健壮
  • RDEPUBCFIDOMPathBuilder.swift -- 在正则匹配前先移除 HTML 注释 <!--...--> 和 CDATA <![CDATA[...]]> 内容;对属性值中的 > 增加 hasSuffix("/>") 对自闭合标签的精确判断 -- #5 HTML 正则脆弱
  • RDEPUBCFIParser.swift -- 在 parseTextAssertion 中实现反斜杠转义处理:用扫描替代 firstIndex/lastIndex,遇到 \ 时跳过下一个字符 -- #6 text assertion 不处理转义
  • RDEPUBCFIRecoveryEngine.swift -- 将双向 contains 匹配改为前缀+后缀匹配,设置最小 token 长度阈值 ≥ 4要求 token 与 exact 长度比在 0.3~3.0 范围内 -- #7 token 匹配过于宽泛
  • RDEPUBCFIGenerator.swift -- 在 makeOffsetRangeCFI 入口添加 precondition(startOffset <= endOffset) 或交换两者确保 start ≤ end -- #8 反向 range 退化为点

Phase 3 — 低优先级(代码质量):

  • 新建 Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIUtilities.swift -- 提取共享 internal extension String { var nilIfEmpty } -- #9 四处重复的 private nilIfEmpty
  • RDEPUBCFIMap.swift -- 添加 lazy var markerByPath: [RDEPUBCFIPath: Int] 索引字典,将 marker(matching:) 查找改为 O(1) -- #10 线性扫描性能瓶颈
  • RDEPUBCFIParser.swift -- 将 parent: try? parse(parentRaw) 改为 parent: try parse(parentRaw),统一抛出错误;或三者均改为 try? 并在 RDEPUBCFIRange 初始化中加注释说明容错策略 -- #11 parent 静默失败混合错误处理

验收标准:

  • 含 emoji😀)的文本在 CFI 恢复时定位偏移正确
  • 修改 rawValue 后序列化返回新值而非旧值
  • makeOffsetCFI 接受自定义 contentPath 时能正确定位嵌套文本节点
  • parseTextAssertion 正确解析含 \[ \] 转义的 CFI
  • makeOffsetRangeCFI(startOffset: 10, endOffset: 5) 不会静默产生退化 range
  • marker(matching:) 在 500+ markers 场景下性能优于 O(n)

规范变更日志

设计说明

#1 rawValue 改为 computed property 当前 RDEPUBCFI 同时存储 rawValue 和组件,序列化时优先返回 rawValue 导致数据不一致。改为 computed property 后,rawValue 始终由 RDEPUBCFISerializer.serialize(self) 计算得出,消除缓存过期风险。由于 RDEPUBCFI 是 struct 且 rawValue 原本就参与 Codable,需同步调整 Codable 实现。

#2 偏移统一: tokenSamples() 当前用 Array(normalizedText) 生成 Character 索引,但 normalizedToChapterOffsets 数组按 UTF-16 code unit 索引构建。修正方式tokenSamples 改为基于 UTF-16 偏移生成 token使用 nsSource.length 迭代而非 Swift Character 迭代。

#5 HTML 预处理: 不引入外部 HTML 解析器,而是在正则匹配前用简单的字符串操作移除注释和 CDATA<!--...--><![CDATA[...]]>。这是最小侵入的修复,覆盖最常见的边界情况。

验证

命令:

  • swift build -- 预期:编译成功,无错误无警告
  • swift test -- 预期:所有现有测试通过(如有 CFI 相关测试,需覆盖 emoji 和转义场景)

手动检查:

  • 确认 RDEPUBCFIUtilities.swift 中只有一个 internal nilIfEmpty 扩展
  • 确认 RDEPUBCFI.rawValue 在 struct 定义中为 computed property 而非 stored property
  • 确认 tokenSamples 返回的 offset 类型与 normalizedToChapterOffsets 索引一致(均为 UTF-16 基准)

Suggested Review Order

数据模型核心rawValue → computed property

偏移计算统一UTF-16/Character 修复)

解析与健壮性修复

代码质量与性能