13 KiB
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”。真实结构是三分流:
webFixedLayout:Fixed Layout EPUB,继续走WKWebViewwebInteractive:带脚本/多媒体/iframe/表单等交互内容的 EPUB,继续走WKWebViewtextReflowable:普通 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 或
scriptedproperty - spine HTML 中是否出现交互模式特征
具体 regex 关注:
<script><iframe><video><audio><canvas><svg><form>onload/onclick/touchstart等内联事件hype_generated_scriptswiperwebview
这意味着 fixed layout 与 interactive EPUB 的 WebView 边界不是概念约束,而是已经在 parser 层具备明确判定逻辑。
4. .textReflowable Real Call Chain
当前 native reflowable 的真实调用链如下:
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刷新内容
- 构造 renderer:
-
如果不是
.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: StringbaseURL: URL?style: RDEPUBTextRenderStyle
5.2 主要处理步骤
-
RDEPUBTextRendererSupport.injectFragmentMarkers(into:)- 先往 HTML 注入 fragment marker
- 为后续目录/定位/fragment offset 映射保留锚点
-
把 HTML 转为 UTF-8
Data -
调用
DTHTMLAttributedStringBuilder- 通过
makeAttributedString(from:baseURL:style:) - options 由
dtOptions(baseURL:style:)提供
- 通过
-
如果 DTCoreText 构建失败
- 回退到
fallbackRenderedContent(for:style:)
- 回退到
-
对生成的 attributed string 做后处理
extractFragmentOffsets(from:)normalizeReadingAttributes(in:style:)
-
返回
RDEPUBRenderedChapterContentattributedStringfragmentOffsets
5.3 当前 dtOptions 的能力边界
当前 options 只有基础排版参数:
DTDefaultFontFamilyDTDefaultFontNameDTDefaultFontSizeDTDefaultLineHeightMultiplierDTDefaultTextColorNSBaseURLDocumentOptionDTUseiOS6Attributes
这说明当前旧引擎的 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 生成以下原生分页数据结构:
-
RDEPUBTextBookchapters: [RDEPUBTextChapter]pages: [RDEPUBTextPage]
-
RDEPUBTextChapterchapterIndexspineIndexhreftitleattributedContentfragmentOffsetspages
-
RDEPUBTextPageabsolutePageIndexchapterIndexspineIndexhrefchapterTitlepageIndexInChaptertotalPagesInChaptercontentcontentRangepageStartOffsetpageEndOffset
这些模型就是当前原生引擎与 UI/状态层的契约面。
7. Current Core Pagination Primitive
当前分页核心在 Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift:
- 扩展方法:
NSAttributedString.ss_pageRanges(size:) - 底层实现:
CTFramesetterCreateWithAttributedStringCTFramesetterCreateFrameCTFrameGetVisibleStringRange
处理循环逻辑是:
- 从当前
location创建 frame - 取
visibleRange - 把这个 range 记录为一页
location += visibleRange.length- 重复直到结束
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:)
它依赖:
fragmentOffsetspageStartOffsetpageEndOffset- chapter-level
attributedContent.length
9.2 Highlight and selection transport
RDEPUBTextContentView 会把选区转成:
RDEPUBSelectionRDEPUBTextOffsetRangeInfo
并通过 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
它负责:
pageCurlhorizontalScrollverticalScroll- 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 应默认遵守以下边界:
RDEPUBParser.readingProfile()三分流保持不变webFixedLayout继续走WKWebViewwebInteractive继续走WKWebView.textReflowable继续以EPUBTextRendering为主战场RDReaderView不修改RDEPUBTextBook/RDEPUBTextChapter/RDEPUBTextPage是兼容层,后续升级不能轻易破坏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 semantics;Fixed Layout、交互式 EPUB 以及RDReaderView容器都应保持边界不变。