- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
522 lines
17 KiB
Markdown
522 lines
17 KiB
Markdown
# EPUBTextRendering 模块代码级参考文档
|
||
|
||
> 最后更新:2026-06-18
|
||
|
||
---
|
||
|
||
## 1. 模块概述
|
||
|
||
`EPUBTextRendering` 是 ReadViewSDK 的**文本渲染层**,负责将 EPUB HTML 内容转换为原生 `NSAttributedString`,执行排版分页,并构建全书文本模型。它位于 EPUBCore 之上、EPUBUI 之下,是 `textReflowable` 阅读配置文件的核心引擎。
|
||
|
||
**文件清单(~30 个 Swift 文件):**
|
||
|
||
| 子系统 | 核心文件 | 职责 |
|
||
|--------|----------|------|
|
||
| **排版管线** | `Typesetter/RDEPUBTypesettingPipeline.swift` | HTML → 标记化 HTML 的多阶段管线 |
|
||
| **文本渲染** | `RDEPUBTextRenderer.swift`, `RDEPUBDTCoreTextRenderer.swift` | HTML → NSAttributedString 转换 |
|
||
| **分页引擎** | `Pagination/*.swift` | CoreText 排版、页帧计算、分页策略 |
|
||
| **构建管线** | `BuildPipeline/*.swift` | 全书构建(渲染 → 分页 → 缓存) |
|
||
| **搜索** | `RDEPUBTextSearchEngine.swift` | 基于原生文本的全文搜索 |
|
||
| **索引** | `RDEPUBTextIndexTable.swift` | 全书字符偏移索引、CFI 映射 |
|
||
| **位置转换** | `RDEPUBTextPositionConverter.swift` | 字符偏移 ↔ 页码转换 |
|
||
| **章节数据** | `RDEPUBChapterData.swift` | 章节级数据访问封装 |
|
||
|
||
---
|
||
|
||
## 2. 排版管线(Typesetter)
|
||
|
||
### 2.1 RDEPUBTextTypesettingPipeline
|
||
|
||
**文件:** `Typesetter/RDEPUBTypesettingPipeline.swift`
|
||
|
||
多阶段 HTML 处理管线,将原始 HTML 转换为可渲染的标记化 HTML。
|
||
|
||
```swift
|
||
struct RDEPUBTextTypesetterPipeline {
|
||
func makeRequest(from input: RDEPUBTypesettingInput) -> RDEPUBTypesettingOutput
|
||
}
|
||
```
|
||
|
||
**管线阶段:**
|
||
|
||
```
|
||
原始 HTML
|
||
│
|
||
▼
|
||
RDEPUBHTMLNormalizer → 清理 HTML(移除脚本、修复标签)
|
||
│
|
||
▼
|
||
RDEPUBSemanticMarkerInjector → 注入语义标记(段落、列表、表格等)
|
||
│
|
||
▼
|
||
RDEPUBCFIMarkerInjector → 注入 CFI 定位标记
|
||
│
|
||
▼
|
||
RDEPUBStyleSheetComposer → 合并样式表(内联 CSS + 兼容性处理)
|
||
│
|
||
▼
|
||
RDEPUBFontNormalizer → 注册嵌入字体、字体回退
|
||
│
|
||
▼
|
||
RDEPUBFragmentMarkerInjector → 注入片段标记(用于锚点定位)
|
||
│
|
||
▼
|
||
标记化 HTML + 诊断报告
|
||
```
|
||
|
||
### 2.2 管线输入/输出
|
||
|
||
```swift
|
||
struct RDEPUBTypesettingInput {
|
||
var href: String // 章节 href
|
||
var spineIndex: Int?
|
||
var title: String // 章节标题
|
||
var rawHTML: String // 原始 HTML
|
||
var baseURL: URL? // 资源基础 URL
|
||
var style: RDEPUBTextRenderStyle // 渲染样式
|
||
var resourceResolver: RDEPUBResourceResolver?
|
||
var contentLanguageCode: String?
|
||
var pageSize: CGSize?
|
||
var layoutConfig: RDEPUBTextLayoutConfig?
|
||
}
|
||
|
||
struct RDEPUBTypesettingOutput {
|
||
var request: RDEPUBTextChapterRenderRequest // 渲染请求
|
||
var diagnostics: [RDEPUBTextResourceReferenceDiagnostic] // 资源引用诊断
|
||
var styleCompatibilityReport: RDEPUBCSSCompatibilityReport // CSS 兼容性报告
|
||
}
|
||
```
|
||
|
||
### 2.3 各阶段处理器
|
||
|
||
| 处理器 | 文件 | 职责 |
|
||
|--------|------|------|
|
||
| `RDEPUBHTMLNormalizer` | `RDEPUBHTMLNormalizer.swift` | 清理 HTML:移除 `<script>`、修复未闭合标签、规范化编码 |
|
||
| `RDEPUBSemanticMarkerInjector` | `RDEPUBSemanticMarkerInjector.swift` | 为块级元素注入 `data-rd-block-kind` 属性 |
|
||
| `RDEPUBCFIMarkerInjector` | `RDEPUBCFIMarkerInjector.swift` | 为文本节点注入 `data-rd-cfi` 属性 |
|
||
| `RDEPUBStyleSheetComposer` | `RDEPUBStyleSheetComposer.swift` | 合并 EPUB 原始 CSS + SDK 样式 + 兼容性补丁 |
|
||
| `RDEPUBFontNormalizer` | `RDEPUBFontNormalizer.swift` | 检测并注册 `@font-face` 嵌入字体 |
|
||
| `RDEPUBFragmentMarkerInjector` | `RDEPUBFragmentMarkerInjector.swift` | 为 `id` 属性注入片段偏移标记 |
|
||
| `RDEPUBAttachmentNormalizer` | `RDEPUBAttachmentNormalizer.swift` | 规范化 `<img>`、`<svg>` 等附件元素 |
|
||
| `RDEPUBRenderDiagnosticsCollector` | `RDEPUBRenderDiagnosticsCollector.swift` | 收集渲染诊断信息(缺失资源、不支持的特性) |
|
||
|
||
### 2.4 CSS 兼容性层
|
||
|
||
**文件:** `Typesetter/Compatibility/`
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `RDEPUBCSSCompatibilityLayer.swift` | CSS 兼容性补丁(webkit 前缀、不支持属性替换) |
|
||
| `RDEPUBFontFallbackResolver.swift` | 字体回退链解析 |
|
||
| `RDEPUBStyleCompatibilityModels.swift` | 兼容性报告模型 |
|
||
|
||
---
|
||
|
||
## 3. 文本渲染器
|
||
|
||
### 3.1 RDEPUBTextRenderer(协议)
|
||
|
||
**文件:** `RDEPUBTextRenderer.swift`
|
||
|
||
```swift
|
||
public protocol RDEPUBTextRenderer {
|
||
/// 将 HTML 渲染为 NSAttributedString
|
||
func render(html: String, baseURL: URL?, style: RDEPUBTextRenderStyle) -> NSAttributedString
|
||
}
|
||
```
|
||
|
||
### 3.2 RDEPUBDTCoreTextRenderer
|
||
|
||
**文件:** `RDEPUBDTCoreTextRenderer.swift`
|
||
|
||
基于 DTCoreText 的渲染器实现,将 HTML 转换为 `NSAttributedString`。
|
||
|
||
```swift
|
||
public final class RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
|
||
public init()
|
||
public func render(html: String, baseURL: URL?, style: RDEPUBTextRenderStyle) -> NSAttributedString
|
||
}
|
||
```
|
||
|
||
### 3.3 渲染样式
|
||
|
||
```swift
|
||
public struct RDEPUBTextRenderStyle {
|
||
public var font: UIFont // 字体
|
||
public var lineSpacing: CGFloat // 行间距
|
||
public var textColor: UIColor? // 文字颜色
|
||
public var backgroundColor: UIColor? // 背景颜色
|
||
}
|
||
```
|
||
|
||
### 3.4 NSAttributedString 扩展键
|
||
|
||
```swift
|
||
extension NSAttributedString.Key {
|
||
static let rdPageBlockRange // 块级元素范围
|
||
static let rdPageBlockIndex // 块索引
|
||
static let rdPageFragmentID // 片段 ID
|
||
static let rdPageAttachmentKind // 附件类型
|
||
static let rdPageBlockKind // 块类型(paragraph/list/table/code/blockquote/attachment/generic)
|
||
static let rdPageSemanticHints // 语义提示(avoidPageBreakInside/keepWithNext/pageBreakBefore/After)
|
||
static let rdPageAttachmentPlacement // 附件放置方式(inline/baseline/centered)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 分页引擎
|
||
|
||
### 4.1 RDEPUBTextLayouter
|
||
|
||
**文件:** `Pagination/RDEPUBTextLayouter.swift`
|
||
|
||
分页的核心入口,组合 `RDEPUBCoreTextPageFrameFactory` 和 `RDEPUBChapterPageCounter`。
|
||
|
||
```swift
|
||
struct RDEPUBTextLayouter {
|
||
init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default)
|
||
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame]
|
||
}
|
||
```
|
||
|
||
### 4.2 RDEPUBTextLayoutConfig
|
||
|
||
```swift
|
||
public struct RDEPUBTextLayoutConfig: Equatable {
|
||
public var frameWidth: CGFloat // 帧宽度
|
||
public var frameHeight: CGFloat // 帧高度
|
||
public var edgeInsets: UIEdgeInsets // 内边距
|
||
public var numberOfColumns: Int // 列数
|
||
public var columnGap: CGFloat // 列间距
|
||
public var avoidOrphans: Bool // 避免孤行
|
||
public var avoidWidows: Bool // 避免寡行
|
||
public var avoidPageBreakInsideEnabled: Bool // 避免块内分页
|
||
public var hyphenation: Bool // 连字符断词
|
||
|
||
public static let `default`: RDEPUBTextLayoutConfig
|
||
}
|
||
```
|
||
|
||
### 4.3 RDEPUBCoreTextPageFrameFactory
|
||
|
||
**文件:** `Pagination/RDEPUBCoreTextPageFrameFactory.swift`
|
||
|
||
使用 CoreText 排版引擎,将 `NSAttributedString` 排列到指定尺寸的帧中。
|
||
|
||
```swift
|
||
struct RDEPUBCoreTextPageFrameFactory {
|
||
init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig)
|
||
func makeFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame]
|
||
}
|
||
```
|
||
|
||
### 4.4 RDEPUBChapterPageCounter
|
||
|
||
**文件:** `Pagination/RDEPUBChapterPageCounter.swift`
|
||
|
||
基于 `RDEPUBCoreTextPageFrameFactory` 计算页范围。
|
||
|
||
```swift
|
||
struct RDEPUBChapterPageCounter {
|
||
init(factory: RDEPUBCoreTextPageFrameFactory)
|
||
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame]
|
||
}
|
||
```
|
||
|
||
### 4.5 RDEPUBTextLayoutFrame
|
||
|
||
**文件:** `Pagination/RDEPUBTextLayoutFrame.swift`
|
||
|
||
表示一个排版帧(一页的内容范围)。
|
||
|
||
```swift
|
||
public struct RDEPUBTextLayoutFrame {
|
||
public let range: NSRange // 字符范围
|
||
public let frameIndex: Int // 帧索引
|
||
public let diagnostics: [String] // 诊断信息
|
||
}
|
||
```
|
||
|
||
### 4.6 RDEPUBPageBreakPolicy
|
||
|
||
**文件:** `Pagination/RDEPUBPageBreakPolicy.swift`
|
||
|
||
分页策略,决定在何处断页。
|
||
|
||
```swift
|
||
struct RDEPUBPageBreakPolicy {
|
||
/// 检查指定位置是否允许分页
|
||
func canBreak(at offset: Int, in attributedString: NSAttributedString) -> Bool
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 构建管线
|
||
|
||
### 5.1 RDEPUBTextBookBuilder
|
||
|
||
**文件:** `BuildPipeline/RDEPUBTextBookBuilder.swift`
|
||
|
||
全书构建的核心类,串联渲染、分页、缓存和诊断。
|
||
|
||
```swift
|
||
public final class RDEPUBTextBookBuilder {
|
||
public init(renderer: RDEPUBTextRenderer, cache: RDEPUBTextBookCache?, layoutConfig: RDEPUBTextLayoutConfig)
|
||
|
||
/// 构建全书文本模型
|
||
public func build(
|
||
parser: RDEPUBParser,
|
||
publication: RDEPUBPublication,
|
||
pageSize: CGSize,
|
||
style: RDEPUBTextRenderStyle
|
||
) throws -> RDEPUBTextBook
|
||
|
||
// 诊断信息
|
||
public private(set) var lastBuildResourceDiagnostics: [RDEPUBTextResourceReferenceDiagnostic]
|
||
public private(set) var lastBuildPaginationDiagnostics: [RDEPUBTextChapterPaginationDiagnostic]
|
||
public private(set) var lastBuildPerformanceSamples: [RDEPUBTextPerformanceSample]
|
||
public private(set) var lastBuildCacheStats: (hits: Int, misses: Int)
|
||
}
|
||
```
|
||
|
||
**构建流程:**
|
||
|
||
```
|
||
RDEPUBParser + RDEPUBPublication
|
||
│
|
||
▼ 遍历 spine(linear 章节)
|
||
│
|
||
├── 检查缓存(RDEPUBPaginationCacheCoordinator)
|
||
│ ├── 命中 → 直接使用缓存的分页结果
|
||
│ └── 未命中 → 执行渲染 + 分页
|
||
│
|
||
├── RDEPUBChapterRenderPipeline(渲染单章)
|
||
│ └── RDEPUBTextTypesetterPipeline → RDEPUBTextRenderer
|
||
│
|
||
├── RDEPUBChapterPaginationPipeline(分页单章)
|
||
│ └── RDEPUBTextLayouter → [RDEPUBTextLayoutFrame]
|
||
│
|
||
├── RDEPUBChapterTailNormalizer(章节尾部规范化)
|
||
│
|
||
└── 组装 RDEPUBTextChapter + RDEPUBTextPage
|
||
│
|
||
▼
|
||
RDEPUBTextBook(全书模型)
|
||
```
|
||
|
||
### 5.2 辅助组件
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `RDEPUBChapterRenderPipeline.swift` | 单章渲染管线 |
|
||
| `RDEPUBChapterPaginationPipeline.swift` | 单章分页管线 |
|
||
| `RDEPUBChapterTailNormalizer.swift` | 章节尾部空白处理 |
|
||
| `RDEPUBPaginationCacheCoordinator.swift` | 分页缓存协调 |
|
||
| `RDEPUBTextBookCache.swift` | 分页缓存存储 |
|
||
| `RDEPUBTextPerformanceSampler.swift` | 性能采样 |
|
||
| `RDEPUBBuildDiagnosticsReporter.swift` | 构建诊断报告 |
|
||
| `RDEPUBTextBuildPipelineInterfaces.swift` | 管线接口定义 |
|
||
|
||
---
|
||
|
||
## 6. 全书模型
|
||
|
||
### 6.1 RDEPUBTextBook
|
||
|
||
**文件:** `BuildPipeline/RDEPUBTextBookModels.swift`
|
||
|
||
全书文本模型,包含所有章节和页面。
|
||
|
||
```swift
|
||
public struct RDEPUBTextBook {
|
||
public var chapters: [RDEPUBTextChapter] // 所有章节
|
||
public var pages: [RDEPUBTextPage] // 所有页面(扁平化)
|
||
public let indexTable: RDEPUBTextIndexTable // 索引表
|
||
public var positionConverter: RDEPUBTextPositionConverter
|
||
|
||
// 章节查询
|
||
public func chapterData(for href: String) -> RDEPUBChapterData?
|
||
public func chapterData(forSpineIndex spineIndex: Int) -> RDEPUBChapterData?
|
||
public func chapterData(atChapterIndex index: Int) -> RDEPUBChapterData?
|
||
public func chapterData(forPageNumber pageNumber: Int) -> RDEPUBChapterData?
|
||
|
||
// 页码查询
|
||
public func page(at pageNumber: Int) -> RDEPUBTextPage?
|
||
public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int?
|
||
public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation?
|
||
}
|
||
```
|
||
|
||
### 6.2 RDEPUBTextChapter
|
||
|
||
```swift
|
||
public struct RDEPUBTextChapter: Equatable {
|
||
public var chapterIndex: Int // 章节索引
|
||
public var spineIndex: Int // spine 索引
|
||
public var href: String // 章节 href
|
||
public var title: String // 章节标题
|
||
public var attributedContent: NSAttributedString // 章节完整富文本
|
||
public var fragmentOffsets: [String: Int] // 片段 ID → 字符偏移
|
||
public var cfiMap: RDEPUBCFIMap? // CFI 映射
|
||
public var pageBreakReasons: [RDEPUBTextPageBreakReason] // 分页原因
|
||
public var pages: [RDEPUBTextPage] // 章节内页面
|
||
}
|
||
```
|
||
|
||
### 6.3 RDEPUBTextPage
|
||
|
||
```swift
|
||
public struct RDEPUBTextPage: Equatable {
|
||
public var absolutePageIndex: Int // 全书绝对页码
|
||
public var chapterIndex: Int // 章节索引
|
||
public var spineIndex: Int // spine 索引
|
||
public var href: String // 章节 href
|
||
public var chapterTitle: String // 章节标题
|
||
public var pageIndexInChapter: Int // 章节内页码
|
||
public var totalPagesInChapter: Int // 章节总页数
|
||
public var chapterContent: NSAttributedString // 章节完整内容
|
||
public var content: NSAttributedString // 页面内容(子范围)
|
||
public var contentRange: NSRange // 在章节内容中的范围
|
||
public var pageStartOffset: Int // 起始偏移
|
||
public var pageEndOffset: Int // 结束偏移
|
||
public var metadata: RDEPUBTextPageMetadata // 页元数据
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 索引与位置转换
|
||
|
||
### 7.1 RDEPUBTextIndexTable
|
||
|
||
**文件:** `RDEPUBTextIndexTable.swift`
|
||
|
||
全书字符偏移索引,支持快速查找。
|
||
|
||
```swift
|
||
public struct RDEPUBTextIndexTable {
|
||
public let chapterStartOffsets: [Int] // 各章节起始偏移
|
||
public let chapterLengths: [Int] // 各章节长度
|
||
public let hrefToChapterIndex: [String: Int] // href → 章节索引
|
||
public let hrefToFileIndex: [String: Int] // href → 文件索引
|
||
public let fragmentOffsetsByHref: [String: [String: Int]] // 片段偏移
|
||
public let fileRowColumnMap: [Int: [RDEPUBRowColumnIndex]] // 行列映射
|
||
public let fileCFIMap: [Int: RDEPUBCFIMap] // CFI 映射
|
||
public var totalCharacterCount: Int // 总字符数
|
||
|
||
public init(chapters: [RDEPUBTextChapter])
|
||
}
|
||
```
|
||
|
||
### 7.2 RDEPUBTextPositionConverter
|
||
|
||
**文件:** `RDEPUBTextPositionConverter.swift`
|
||
|
||
字符偏移 ↔ 页码双向转换。
|
||
|
||
```swift
|
||
public struct RDEPUBTextPositionConverter {
|
||
init(book: RDEPUBTextBook)
|
||
|
||
/// 字符偏移 → 页码
|
||
public func pageNumber(for offset: Int) -> Int?
|
||
|
||
/// 页码 → 字符偏移范围
|
||
public func offsetRange(forPageNumber pageNumber: Int) -> NSRange?
|
||
}
|
||
```
|
||
|
||
### 7.3 RDEPUBChapterData
|
||
|
||
**文件:** `RDEPUBChapterData.swift`
|
||
|
||
章节级数据访问封装。
|
||
|
||
```swift
|
||
public struct RDEPUBChapterData {
|
||
public let chapter: RDEPUBTextChapter
|
||
public let indexTable: RDEPUBTextIndexTable
|
||
|
||
/// 位置 → 页码
|
||
public func pageNumber(for location: RDEPUBLocation) -> Int?
|
||
|
||
/// 页面 → 位置
|
||
public func location(for page: RDEPUBTextPage, bookIdentifier: String?) -> RDEPUBLocation?
|
||
|
||
/// 字符范围 → 范围锚点
|
||
public func rangeAnchor(for range: NSRange) -> RDEPUBRangeAnchor
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 搜索引擎
|
||
|
||
**文件:** `RDEPUBTextSearchEngine.swift`
|
||
|
||
基于原生 `NSAttributedString` 的搜索引擎,比 HTML 搜索更高效。
|
||
|
||
```swift
|
||
final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||
init(textBook: RDEPUBTextBook, publication: RDEPUBPublication)
|
||
func search(keyword: String) -> [RDEPUBSearchMatch]
|
||
}
|
||
```
|
||
|
||
搜索结果包含:href、progression、previewText、rangeAnchor、cfi 等定位信息。
|
||
|
||
---
|
||
|
||
## 9. 纯文本构建器
|
||
|
||
**文件:** `RDEpubPlainTextBookBuilder.swift`
|
||
|
||
从纯文本(.txt)文件构建 `RDEPUBTextBook`,用于支持 TXT 格式阅读。
|
||
|
||
---
|
||
|
||
## 10. 设计模式总结
|
||
|
||
| 模式 | 应用 |
|
||
|------|------|
|
||
| **管线模式** | `RDEPUBTextTypesettingPipeline` 多阶段 HTML 处理 |
|
||
| **策略模式** | `RDEPUBTextRenderer` 协议,`RDEPUBDTCoreTextRenderer` 实现 |
|
||
| **Builder 模式** | `RDEPUBTextBookBuilder` 全书构建 |
|
||
| **缓存协调** | `RDEPUBPaginationCacheCoordinator` 分页结果缓存 |
|
||
| **索引表** | `RDEPUBTextIndexTable` 全书字符偏移快速查找 |
|
||
| **关注点分离** | 渲染/分页/缓存/诊断各司其职 |
|
||
|
||
---
|
||
|
||
## 11. 数据流图
|
||
|
||
```
|
||
EPUB HTML 文件
|
||
│
|
||
▼
|
||
RDEPUBTypesettingPipeline
|
||
│ HTMLNormalizer → SemanticMarker → CFI → StyleSheet → Font → Fragment
|
||
▼
|
||
标记化 HTML + CSS
|
||
│
|
||
▼
|
||
RDEPUBDTCoreTextRenderer
|
||
│ DTCoreText HTML → NSAttributedString
|
||
▼
|
||
NSAttributedString(带语义属性)
|
||
│
|
||
▼
|
||
RDEPUBTextLayouter
|
||
│ CoreText 排版 → 帧计算 → 分页策略
|
||
▼
|
||
[RDEPUBTextLayoutFrame](页面范围列表)
|
||
│
|
||
▼
|
||
RDEPUBTextChapter + RDEPUBTextPage
|
||
│
|
||
▼
|
||
RDEPUBTextBook(全书模型 + 索引表)
|
||
```
|