ReadViewSDK/.planning/phases/01-current-engine-boundaries/01-reflowable-audit.md

410 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 容器都应保持边界不变。