Files
ReadViewSDK/.planning/phases/01-current-engine-boundaries/01-reflowable-audit.md
T
2026-05-21 21:56:54 +08:00

13 KiB
Raw Blame History

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. webFixedLayoutFixed 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 的真实调用链如下:

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.swiftpaginatePublication(restoreLocation:) 是当前核心分支点:

  • 如果 publication.readingProfile == .textReflowable

    • 构造 rendererresolvedTextRenderer()
    • 当前默认返回 RDEPUBDTCoreTextRenderer()
    • 构造 builderRDEPUBTextBookBuilder(renderer:)
    • 计算 pageSizeRDEPUBTextRenderStyle
    • 在后台线程执行 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

因此,如果后续升级分页器或页面语义,必须继续维护 RDEPUBTextPageRDEPUBTextContentView 的兼容面,而不是绕开它另起视图层。

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 重叠来计算高亮区域。

结论:RDEPUBTextBookRDEPUBTextPage 不是单纯的 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 容器都应保持边界不变。