docs(01-01): complete current engine audit plan

This commit is contained in:
shen
2026-05-21 21:56:54 +08:00
parent 743988a78b
commit cc2573552c
4 changed files with 532 additions and 39 deletions
@@ -0,0 +1,409 @@
# Phase 1 Audit: Current Reflowable Engine, Boundaries, and Stable Contracts
**Phase:** 01-current-engine-boundaries
**Date:** 2026-05-21
**Purpose:** 固化当前 `.textReflowable` 旧引擎的真实调用链、模式分流边界、以及本次重构不能碰的外层契约。
## 1. Executive Conclusion
当前仓库并不是“所有 EPUB 都走 `WKWebView`”。真实结构是三分流:
1. `webFixedLayout`Fixed Layout EPUB,继续走 `WKWebView`
2. `webInteractive`:带脚本/多媒体/iframe/表单等交互内容的 EPUB,继续走 `WKWebView`
3. `textReflowable`:普通 reflowable EPUB,走 **DTCoreText -> `NSAttributedString` -> CoreText 分页 -> 原生文本页视图**
因此,本次“旧引擎直接演进”的主战场已经存在,且就在 `EPUBTextRendering`。Phase 2/3 不需要新起一套阅读器,也不需要改 `RDReaderView`;要做的是升级当前 native reflowable 的 typesetter、属性体系和分页能力。
## 2. Entry Path: URL -> Reader Controller
入口在 `Sources/RDReaderView/RDURLReaderController.swift`
### 2.1 `.epub` 入口
`RDURLReaderController.embedReaderController()` 对扩展名做分支:
- 扩展名是 `epub`
- 直接构造 `RDEPUBReaderController(epubURL:configuration:)`
- 把 reader controller 作为子控制器嵌入当前界面
这里没有直接决定 native 还是 web;它只负责把 `.epub` 交给 `RDEPUBReaderController`
### 2.2 `.txt` 入口
TXT 则走 `RDPlainTextBookBuilder` 构造 `RDEPUBTextBook`,再以 external TextBook 模式交给 `RDEPUBReaderController`。这条链路与 EPUB 的原生文本展示在 UI 层会合,但不参与 EPUB reading profile 判定。
## 3. Reading Profile Split
模式分流在 `Sources/RDReaderView/EPUBCore/RDEPUBParser+ReadingProfile.swift`
### 3.1 判定规则
`RDEPUBParser.readingProfile()` 的真实逻辑是:
- `metadata.layout == .fixed` -> `.webFixedLayout`
- 否则,如果 `hasInteractiveContent() == true` -> `.webInteractive`
- 否则 -> `.textReflowable`
### 3.2 `hasInteractiveContent()` 的判定来源
`hasInteractiveContent()` 会检查两类信号:
- manifest 中是否出现 JS 相关 media type 或 `scripted` property
- spine HTML 中是否出现交互模式特征
具体 regex 关注:
- `<script>`
- `<iframe>`
- `<video>`
- `<audio>`
- `<canvas>`
- `<svg>`
- `<form>`
- `onload` / `onclick` / `touchstart` 等内联事件
- `hype_generated_script`
- `swiper`
- `webview`
这意味着 fixed layout 与 interactive EPUB 的 WebView 边界不是概念约束,而是已经在 parser 层具备明确判定逻辑。
## 4. `.textReflowable` Real Call Chain
当前 native reflowable 的真实调用链如下:
```text
RDURLReaderController
-> RDEPUBReaderController(epubURL:)
-> RDEPUBParser
-> RDEPUBPublication
-> RDEPUBParser.readingProfile()
-> .textReflowable
-> RDEPUBReaderController.paginatePublication()
-> resolvedTextRenderer()
-> RDEPUBDTCoreTextRenderer
-> RDEPUBTextBookBuilder.build()
-> renderChapter(html:baseURL:style:)
-> RDEPUBRenderedChapterContent
-> ss_pageRanges(size:)
-> RDEPUBTextBook / RDEPUBTextChapter / RDEPUBTextPage
-> RDEPUBTextContentView
-> RDReaderView
```
### 4.1 Branch point in `RDEPUBReaderController`
`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift``paginatePublication(restoreLocation:)` 是当前核心分支点:
- 如果 `publication.readingProfile == .textReflowable`
- 构造 renderer`resolvedTextRenderer()`
- 当前默认返回 `RDEPUBDTCoreTextRenderer()`
- 构造 builder`RDEPUBTextBookBuilder(renderer:)`
- 计算 `pageSize``RDEPUBTextRenderStyle`
- 在后台线程执行 `builder.build(...)`
- 完成后 `applyTextBook(...)`,最终由 `RDReaderView` 刷新内容
- 如果不是 `.textReflowable`
- fixed layout:直接创建 fixed snapshot
- reflowable-web / interactive:使用 `RDEPUBPaginator`
结论:native reflowable 与 WebView 路径已经在 reader controller 内部彻底分开。
## 5. Render Stage: `RDEPUBDTCoreTextRenderer`
`Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift` 是当前原生章节渲染入口。
### 5.1 输入
`renderChapter(html:baseURL:style:)` 接收:
- `html: String`
- `baseURL: URL?`
- `style: RDEPUBTextRenderStyle`
### 5.2 主要处理步骤
1. `RDEPUBTextRendererSupport.injectFragmentMarkers(into:)`
- 先往 HTML 注入 fragment marker
- 为后续目录/定位/fragment offset 映射保留锚点
2. 把 HTML 转为 UTF-8 `Data`
3. 调用 `DTHTMLAttributedStringBuilder`
- 通过 `makeAttributedString(from:baseURL:style:)`
- options 由 `dtOptions(baseURL:style:)` 提供
4. 如果 DTCoreText 构建失败
- 回退到 `fallbackRenderedContent(for:style:)`
5. 对生成的 attributed string 做后处理
- `extractFragmentOffsets(from:)`
- `normalizeReadingAttributes(in:style:)`
6. 返回 `RDEPUBRenderedChapterContent`
- `attributedString`
- `fragmentOffsets`
### 5.3 当前 `dtOptions` 的能力边界
当前 options 只有基础排版参数:
- `DTDefaultFontFamily`
- `DTDefaultFontName`
- `DTDefaultFontSize`
- `DTDefaultLineHeightMultiplier`
- `DTDefaultTextColor`
- `NSBaseURLDocumentOption`
- `DTUseiOS6Attributes`
这说明当前旧引擎的 renderer 仍然是“简单 DTCoreText builder + 少量 options”模型,还没有形成 WXRead 风格的多层 stylesheet / 自定义属性 / 页面级后处理体系。
## 6. Pagination Stage: `RDEPUBTextBookBuilder`
`Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift` 当前负责把章节渲染结果组织为 book/chapter/page 结构。
### 6.1 Chapter iteration
`build(parser:publication:pageSize:style:)` 会:
- 遍历 `publication.spine.enumerated()`
- 只处理 `linear == true`
- 只处理 `html` / `xhtml` 类型
- 从 parser 读取 `rawHTML`
- 计算章节标题
- `normalizeHTML(rawHTML)`
- 调用 renderer 生成 `RDEPUBRenderedChapterContent`
### 6.2 Base URL behavior
builder 把每个 spine item 的章节目录作为 `baseURL`
- `parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent()`
这意味着当前图片/CSS/相对资源的解析锚点已经是“章节所在目录”,这是 Phase 2 引入更完整 CSS 层和资源解析时必须保留的基础约束。
### 6.3 Data model produced
builder 生成以下原生分页数据结构:
- `RDEPUBTextBook`
- `chapters: [RDEPUBTextChapter]`
- `pages: [RDEPUBTextPage]`
- `RDEPUBTextChapter`
- `chapterIndex`
- `spineIndex`
- `href`
- `title`
- `attributedContent`
- `fragmentOffsets`
- `pages`
- `RDEPUBTextPage`
- `absolutePageIndex`
- `chapterIndex`
- `spineIndex`
- `href`
- `chapterTitle`
- `pageIndexInChapter`
- `totalPagesInChapter`
- `content`
- `contentRange`
- `pageStartOffset`
- `pageEndOffset`
这些模型就是当前原生引擎与 UI/状态层的契约面。
## 7. Current Core Pagination Primitive
当前分页核心在 `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift`
- 扩展方法:`NSAttributedString.ss_pageRanges(size:)`
- 底层实现:
- `CTFramesetterCreateWithAttributedString`
- `CTFramesetterCreateFrame`
- `CTFrameGetVisibleStringRange`
处理循环逻辑是:
1. 从当前 `location` 创建 frame
2.`visibleRange`
3. 把这个 range 记录为一页
4. `location += visibleRange.length`
5. 重复直到结束
### 7.1 Current limitation
这是一种典型的“可见字符串范围切页”模型,优点是简单、稳定、易于接回现有 `RDEPUBTextBook` 结构;缺点是:
- 没有块级避免断页策略
- 没有页面级语义结构
- 没有自定义 layout frame / layouter
- 很难表达复杂图片、表格、背景、分页约束
因此,`ss_pageRanges(size:)` 明确是后续需要被增强或替换的核心点之一。
## 8. Presentation Stage: `RDEPUBTextContentView`
`Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift` 是当前 native text page 的展示层。
### 8.1 Responsibilities
它负责:
- 接收单页 `RDEPUBTextPage`
-`page.content` 赋给 `UITextView`
- 叠加高亮和搜索高亮
- 展示页码
- 输出 selection 和 annotation menu action
### 8.2 Why this matters
这说明当前 `.textReflowable` 路径已经和以下阅读器能力发生绑定:
- selection
- highlight
- annotate
- search highlight
- page number display
因此,如果后续升级分页器或页面语义,必须继续维护 `RDEPUBTextPage``RDEPUBTextContentView` 的兼容面,而不是绕开它另起视图层。
## 9. Location / Highlight / Search Compatibility Chain
当前原生文本路径不只是“能显示文本”,还承担已有 reader 能力链路:
### 9.1 Location mapping
`RDEPUBTextBook` 提供:
- `pageNumber(for:resolver:bookIdentifier:)`
- `location(forPageNumber:bookIdentifier:)`
它依赖:
- `fragmentOffsets`
- `pageStartOffset`
- `pageEndOffset`
- chapter-level `attributedContent.length`
### 9.2 Highlight and selection transport
`RDEPUBTextContentView` 会把选区转成:
- `RDEPUBSelection`
- `RDEPUBTextOffsetRangeInfo`
并通过 `RDEPUBReaderController` 的 text selection normalization / persistence 链路继续工作。
### 9.3 Search compatibility
`RDEPUBTextContentView.applySearchHighlights(...)` 也是基于页内 offset 重叠来计算高亮区域。
结论:`RDEPUBTextBook``RDEPUBTextPage` 不是单纯的 UI DTO,而是 reader state、annotation、search、location mapping 的核心兼容层。
## 10. What Still Uses `WKWebView`
下列链路必须继续视为 WebView 范围:
### 10.1 Fixed Layout
`Sources/RDReaderView/EPUBCore/RDEPUBWebView+FixedLayout.swift`
- `loadFixedSpread(...)`
- `handleFixedLayoutLoad(...)`
职责:
- 把 fixed spread 渲染成 HTML
- 通过 `WKWebView.loadHTMLString(...)` 加载
- 处理 fixed spread 资源与 ready fallback
### 10.2 Interactive / web pagination support
`Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift`
职责:
- 使用离屏、非持久化 `WKWebView`
- 按 spine item 加载 HTML
- 测量文档页数
- 为 WebView 路径生成分页信息
尽管它名字叫 paginator,但它不是当前 native `.textReflowable` 的分页核心;它是 WebView 渲染/测量体系的一部分。
### 10.3 Web reflowable presentation
`Sources/RDReaderView/EPUBCore/RDEPUBWebView+Reflowable.swift`
职责:
- 构造 `RDEPUBReflowableRenderRequest`
- 对当前 HTML 文档应用 presentation script
- 处理 page index / target location / highlights 的 WebView 表示
这条链路应该保留给 `.webInteractive` 以及任何必须留在 WebView 的内容,不应误认为是当前 native reflowable 主路径。
## 11. Stable Container Contract: `RDReaderView`
`Sources/RDReaderView/RDReaderView.swift` 是当前阅读容器的稳定外壳。
### 11.1 What it owns
它负责:
- `pageCurl`
- `horizontalScroll`
- `verticalScroll`
- RTL / LTR 翻页方向
- landscape dual-page
- cover page pairing
- orientation change handling
- page container reuse和 data source protocol
### 11.2 Why Phase 1 must keep it unchanged
roadmap 已经把 `RDReaderView` 排除在本次改造范围外,而源码也支持这一结论:
- `RDEPUBReaderController` 只把页面数据和内容视图交给 `RDReaderView`
- native text 和 web content 都通过相同的 container contract 接入
- 改它会把内核升级变成“内核 + 容器 + 手势 + 双页策略”的多变量回归
结论:**`RDReaderView` 不修改** 是正确的硬约束,不只是项目管理偏好,而是当前架构稳定性的必要条件。
## 12. Boundary and Exclusion Rules for Next Phases
后续 phase 应默认遵守以下边界:
1. `RDEPUBParser.readingProfile()` 三分流保持不变
2. `webFixedLayout` 继续走 `WKWebView`
3. `webInteractive` 继续走 `WKWebView`
4. `.textReflowable` 继续以 `EPUBTextRendering` 为主战场
5. `RDReaderView` 不修改
6. `RDEPUBTextBook` / `RDEPUBTextChapter` / `RDEPUBTextPage` 是兼容层,后续升级不能轻易破坏
7. `fragmentOffsets`、offset-based location mapping、高亮/搜索 range transport 必须继续可用
## 13. Direct Refactor Entry Points
基于当前代码,后续“旧引擎直接演进”的直接触点应当是:
- `RDEPUBDTCoreTextRenderer`
- 从简单 `dtOptions` 升级为更完整的 stylesheet / HTML preprocessing / attribute pipeline
- `RDEPUBTextBookBuilder`
- 继续保留 chapter -> rendered content -> page model 的骨架
- 后续承接更丰富页面语义、附件/块元素信息
- `RDEPUBTextPaginationSupport`
- 从单纯 `CTFrameGetVisibleStringRange` 切页,升级到更强分页策略
- `RDEPUBTextPage` / `RDEPUBTextChapter`
- 承接页面级元数据,而不是只保存纯 range
- `RDEPUBReaderController`
- 保持 branch point 与 reader integration,不把它变成新的排版实现层
## 14. Final Audit Result
Phase 1 的事实基线可以归纳为一句话:
> 当前旧引擎已经拥有一条完整的 native `.textReflowable` 路径,真正需要演进的是 `EPUBTextRendering` 内部的 renderer / page model / pagination semanticsFixed Layout、交互式 EPUB 以及 `RDReaderView` 容器都应保持边界不变。