ReadViewSDK/.planning/phases/01-current-engine-boundaries/01-RESEARCH.md

225 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: 对齐现状、边界与重构切入点 - Research
**Researched:** 2026-05-21
**Domain:** iOS EPUB reader architecture / DTCoreText-based native reflowable rendering
**Confidence:** HIGH
<user_constraints>
## User Constraints (from CONTEXT.md)
No user constraints - all decisions at the agent's discretion.
</user_constraints>
<architectural_responsibility_map>
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| EPUB URL entry and format routing | Browser/Client | — | `RDURLReaderController` decides epub vs txt and embeds the correct reader controller. |
| EPUB structure parsing and reading-profile split | Browser/Client | — | `RDEPUBParser` and `RDEPUBPublication` classify `.textReflowable` vs `webFixedLayout` / `webInteractive`. |
| Reflowable native chapter rendering | Browser/Client | — | `RDEPUBDTCoreTextRenderer` and `RDEPUBTextBookBuilder` own HTML to `NSAttributedString` and page slicing. |
| Fixed-layout and interactive content rendering | Browser/Client | — | `RDEPUBWebView` and `RDEPUBPaginator` keep `WKWebView` as the rendering and measurement path. |
| Page container and interaction shell | Browser/Client | — | `RDReaderView` is the stable presentation container that Phase 1 must not modify. |
</architectural_responsibility_map>
<research_summary>
## Summary
当前仓库已经存在一条独立的原生 reflowable 路径,而不是“所有 EPUB 都走 WebView”。`RDEPUBReaderController.paginatePublication` 会在 `publication.readingProfile == .textReflowable` 时改走 `RDEPUBTextBookBuilder`,使用 `RDEPUBDTCoreTextRenderer` 把章节 HTML 转成 `NSAttributedString`,再通过 `ss_pageRanges(size:)` 基于 CoreText 可见范围切页。该路径最终产出 `RDEPUBTextBook` / `RDEPUBTextPage`,并由 `RDEPUBTextContentView` 在现有 `RDReaderView` 容器内展示。
Fixed Layout 与交互式 EPUB 的边界已经在代码里清晰存在。`RDEPUBParser.readingProfile()` 将 `metadata.layout == .fixed` 归类为 `.webFixedLayout`,并把包含脚本/多媒体/iframe/form 等交互内容的资源归类为 `.webInteractive`。这两类内容继续走 `RDEPUBWebView` / `RDEPUBPaginator` / `WKWebView` 体系,不应被纳入本次原生重构。
WXRead 参考资料说明,目标不是“再包一层轻量 renderer”而是把当前 DTCoreText 路径升级为完整的 typesetter + layouter + layout frame 体系:五层 CSS 级联、自定义 DTCoreText 属性键、块级避免断页、页面级元数据、图片与背景等页面语义。对本仓库来说,最稳妥的切入点是保留 `RDURLReaderController -> RDEPUBReaderController -> RDReaderView` 外层契约不变,把增强集中在 `EPUBTextRendering` 内部及其与 reader state 的对接点。
**Primary recommendation:** Phase 1 应先把当前 `.textReflowable` 调用链、WebView 边界、兼容性约束和可演进触点写成明确文档,再以这些文档作为 Phase 2/3 的实现基线。
</research_summary>
<standard_stack>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| UIKit | iOS 15+ SDK | Reader container, controllers, text view presentation | 现有 reader UI 与 `RDReaderView` 全部建立在 UIKit 上。 |
| CoreText | System | Native pagination and text measurement | 当前 `ss_pageRanges(size:)` 直接使用 `CTFramesetterCreateFrame` / `CTFrameGetVisibleStringRange`。 |
| DTCoreText | 1.6.28 | HTML/CSS to `NSAttributedString` | 当前原生 reflowable 渲染核心已经依赖它,后续定制也必须围绕它展开。 |
| WebKit | System | Fixed-layout / interactive EPUB rendering and pagination measurement | 非 reflowable-native 路径仍依赖 `WKWebView`。 |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| DTFoundation | 1.7.19 | DTCoreText supporting parser/runtime pieces | DTCoreText HTML 解析与附件支持所需。 |
| ZIPFoundation | 0.9.20 | EPUB archive handling | EPUB 解压与资源访问基础设施。 |
| SnapKit | 5.7.1 | UI layout helper | Reader UI 周边布局已有依赖,但不是本次内核切入点。 |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Directly evolving `EPUBTextRendering` | A second native engine alongside the existing one | 会增加长期双轨维护成本,并破坏 roadmap 已锁定的“旧引擎直接演进”原则。 |
| DTCoreText-based native rendering | Full `WKWebView` for all EPUB modes | 无法满足 WXRead-style page metadata / page-frame semantics / native selection control goals。 |
| Keeping `RDReaderView` stable | Reworking page container and gesture stack together | 改动半径过大Phase 1 明确要求把翻页容器排除出本次重构。 |
**Installation:**
```bash
pod install
```
</standard_stack>
<architecture_patterns>
## Architecture Patterns
### System Architecture Diagram
```text
RDURLReaderController
-> RDEPUBReaderController
-> RDEPUBParser.parse()
-> RDEPUBPublication / readingProfile()
-> textReflowable
-> RDEPUBTextBookBuilder
-> RDEPUBDTCoreTextRenderer
-> DTHTMLAttributedStringBuilder
-> NSAttributedString
-> RDEPUBTextPaginationSupport.ss_pageRanges()
-> RDEPUBTextBook / RDEPUBTextPage
-> RDEPUBTextContentView
-> RDReaderView
-> webFixedLayout
-> RDEPUBWebView.loadFixedSpread()
-> RDReaderView
-> webInteractive
-> RDEPUBPaginator + RDEPUBWebView.loadPage()
-> RDReaderView
```
### Recommended Project Structure
```text
Sources/RDReaderView/
├── EPUBCore/ # Parser, publication, web pagination, render requests
├── EPUBTextRendering/ # Native reflowable rendering and pagination
├── EPUBUI/ # Reader controller, content views, settings
└── RDReaderView.swift # Stable page container and gestures
```
### Pattern 1: Preserve the outer reader contract
**What:** Keep `RDURLReaderController`, `RDEPUBReaderController`, persistence, location mapping, and `RDReaderView` as the stable shell while replacing internals inside `EPUBTextRendering`.
**When to use:** Any phase that upgrades the reflowable engine without expanding scope into the page container.
**Example:** `RDEPUBReaderController.paginatePublication` already branches into text vs web rendering without changing the reader container contract.
### Pattern 2: Keep rendering and presentation separate
**What:** Produce rich `RDEPUBTextBook` / `RDEPUBTextPage` models first, then let `RDEPUBTextContentView` render those models.
**When to use:** When introducing page-level metadata, selection offsets, image semantics, or new pagination logic.
**Example:** `RDEPUBTextBookBuilder` owns chapter rendering and page slicing, while `RDEPUBTextContentView` only paints attributed substrings and overlays highlights/search state.
### Pattern 3: Branch by publication profile early
**What:** Decide native reflowable vs fixed vs interactive before rendering work starts.
**When to use:** Any future entry-point or repagination logic.
**Example:** `RDEPUBParser.readingProfile()` uses `layout == .fixed` and interactive-content detection to keep `WKWebView` modes out of the native path.
### Anti-Patterns to Avoid
- **Touching `RDReaderView` during engine migration:** Phase 1 explicitly excludes page curl / scroll / gesture container changes.
- **Building a parallel renderer shell:** The roadmap already rejects a “new engine beside old engine” approach.
- **Mixing WebView-only semantics into the native path:** `textReflowable` should evolve around `NSAttributedString` and CoreText, not around hidden `WKWebView` dependencies.
</architecture_patterns>
<dont_hand_roll>
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| HTML parsing | A brand-new EPUB HTML parser | DTCoreText builder extension points | 仓库已经围绕 DTCoreText 建立基础能力,重写 parser 成本高且风险大。 |
| EPUB mode classification | Ad-hoc view-layer checks | `RDEPUBParser.readingProfile()` | 当前 profile 分流已经覆盖 fixed / interactive / text reflowable。 |
| Reader shell replacement | A new page container | Existing `RDReaderView` contract | 现有 container 已承担翻页模式、双页与方向逻辑,超出本次范围。 |
| Location/highlight transport | A second mapping format | Existing `RDEPUBLocation` + `RDEPUBTextOffsetRangeInfo` | 这些类型已被 highlights / search / restore location 链路依赖。 |
**Key insight:** 应该复用现有阅读器契约和数据模型,把新增复杂度集中在旧引擎内部,而不是在外围重造一层基础设施。
</dont_hand_roll>
<common_pitfalls>
## Common Pitfalls
### Pitfall 1: Confusing native reflowable with the WebView paginator
**What goes wrong:** 把 `RDEPUBPaginator` 当成所有 reflowable 的基础,导致 Phase 2/3 方案偏向继续强化 WebView。
**Why it happens:** 当前工程同时存在 `textReflowable` 和基于 `WKWebView` 的分页/展示路径。
**How to avoid:** 在所有 Phase 1 输出里明确:`RDEPUBPaginator` 服务于 fixed 或 interactive/web paths原生重构主战场是 `EPUBTextRendering`
**Warning signs:** 计划任务把 `RDEPUBPaginator` 作为 CSS 五层或页面元数据的主要落点。
### Pitfall 2: Breaking `RDReaderView` assumptions while changing pagination
**What goes wrong:** 新分页结果不再满足现有 page index / container reuse / orientation repagination 约定。
**Why it happens:** 容易把引擎升级和容器升级绑在一起处理。
**How to avoid:**`RDEPUBTextBook.pages` / `RDEPUBTextPage` 视为 Phase 2/3 的兼容面,并在策略中显式约束 `RDReaderView` 不改。
**Warning signs:** 方案需要修改 `RDReaderView.DataSource`、翻页方向、双页逻辑或 content view reuse 协议。
### Pitfall 3: Introducing richer layout semantics without preserving location mapping
**What goes wrong:** 新分页器能排版复杂元素,但阅读位置、高亮、搜索结果定位失效。
**Why it happens:** 只关注排版帧,不关注 `pageStartOffset/pageEndOffset``RDEPUBLocation` 的传递。
**How to avoid:** 把 offset continuity、fragment offset、selection range transport 作为 Phase 2/3 的第一类兼容目标。
**Warning signs:** 设计里没有说明新页面元数据如何回写 `RDEPUBTextBook`、`RDEPUBSelection`、`RDEPUBHighlight`。
</common_pitfalls>
<code_examples>
## Code Examples
### Current reflowable split
```swift
if publication.readingProfile == .textReflowable {
let renderer = resolvedTextRenderer()
let builder = RDEPUBTextBookBuilder(renderer: renderer)
let textBook = try builder.build(
parser: parser,
publication: publication,
pageSize: pageSize,
style: renderStyle
)
}
```
### Current fixed vs interactive boundary
```swift
public func readingProfile() -> RDEPUBReadingProfile {
if metadata.layout == .fixed {
return .webFixedLayout
}
return hasInteractiveContent() ? .webInteractive : .textReflowable
}
```
### Current native pagination primitive
```swift
while visibleRange.location + visibleRange.length < length {
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), path, nil)
visibleRange = CTFrameGetVisibleStringRange(frame)
ranges.append(NSRange(location: location, length: visibleRange.length))
location += visibleRange.length
}
```
</code_examples>
<sota_updates>
## State of the Art (2024-2025)
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Generic HTML to attributed string only | HTML to attributed string plus page-level layout semantics | 已反映在 WXRead 参考资料中 | 需要把 CSS/HTML 处理与分页语义一起设计,而不是只换 builder options。 |
| Simple visible-range page slicing | Block-aware layout frames and custom pagination metadata | 已反映在 WXRead 参考资料中 | 当前 `CTFrameGetVisibleStringRange` 模式会成为主要替换对象。 |
| Styling as a small option bag | Multi-layer stylesheet cascade | 已反映在 WXRead 参考资料中 | `dtOptions` 需要被更明确的 stylesheet pipeline 取代或扩展。 |
**New tools/patterns to consider:**
- DTCoreText attribute extension points: 适合承接 WXRead 风格自定义属性键。
- Page-level layout model: 需要在 `RDEPUBTextChapter` / `RDEPUBTextPage` 附近引入更丰富的元数据,而不是只保存范围。
**Deprecated/outdated:**
- “Simple DTCoreText renderer + pageRanges” 作为长期方案:对复杂块元素、页面语义和 CSS 分层都不够。
- “为原生重构同时替换翻页容器”:与当前范围控制相冲突。
</sota_updates>
<open_questions>
## Open Questions
- `LegacyRDReaderController/RDEPUBTextPaging.swift` 与新 `EPUBTextRendering` 之间哪些实现差异仍值得迁移,哪些应直接废弃,需要在 Phase 1 执行阶段做一次并列审计。
- `Doc/FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md` 与当前源码是否已经出现偏差,需要在 Plan 01-02 中显式核对。
- 未来自定义 DTCoreText 属性应优先挂在 `NSAttributedString`、页模型,还是单独的 layout frame 对象上,需要在策略文档中做出建议但不提前定版实现。
</open_questions>