ReadViewSDK/Doc/EPUBCore_CODE_REFERENCE.md
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

643 lines
22 KiB
Markdown
Raw Permalink 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.

# EPUBCore 模块代码级参考文档
> 最后更新2026-06-18
---
## 1. 模块概述
`EPUBCore` 是 ReadViewSDK 的**基础层**,负责 EPUB 文件解析、资源管理、WebView 渲染、JavaScript 桥接、CFI 定位、搜索和分页计算。它是上层 EPUBTextRendering 和 EPUBUI 的数据提供者。
**文件清单(~50 个 Swift 文件):**
| 子系统 | 核心文件 | 职责 |
|--------|----------|------|
| **解析器** | `RDEPUBParser*.swift` | EPUB 解压、OPF 解析、TOC 解析、资源提取 |
| **数据模型** | `RDEPUBModels.swift` | Metadata、ManifestItem、SpineItem、TOC 等基础模型 |
| **Publication** | `RDEPUBPublication.swift` | 出版物抽象,聚合 parser 和 resourceResolver |
| **阅读会话** | `RDEPUBReadingSession.swift` | 管理分页快照、阅读位置、待导航状态 |
| **WebView** | `RDEPUBWebView*.swift` | WKWebView 封装,支持 Reflowable 和 Fixed Layout |
| **JS 桥接** | `RDEPUBJavaScriptBridge.swift` | JavaScript 消息协议和脚本注入 |
| **资源解析** | `RDEPUBResourceResolver.swift` | href 归一化、文件路径解析 |
| **URL 方案** | `RDEPUBResourceURLSchemeHandler.swift` | 自定义 `ss-reader://` 协议处理 |
| **分页器** | `RDEPUBPaginator.swift` | WebView 分页计算(按章测量页数) |
| **CFI** | `CFI/RDEPUBCFI*.swift` | EPUB CFI 解析、生成、序列化、恢复 |
| **搜索** | `RDEPUBSearchEngine.swift` | HTML 全文搜索 |
| **样式** | `RDEPUBStyleSheetBuilder.swift` | CSS 注入与分页样式生成 |
| **偏好** | `RDEPUBPreferences.swift` | 阅读偏好设置模型 |
| **资源文件** | `RDEPUBAssetRepository.swift` | JS/CSS/HTML 资源文件加载 |
| **笔记** | `Notes/RDEPUBNote*.swift` | 脚注/尾注检测与解析 |
---
## 2. 解析器子系统
### 2.1 RDEPUBParser
**文件:** `RDEPUBParser.swift` + 扩展文件
EPUB 文件解析的核心类,负责从 .epub 文件中提取结构化数据。
```swift
public final class RDEPUBParser {
public internal(set) var metadata: RDEPUBMetadata // 书籍元数据
public internal(set) var manifest: [String: RDEPUBManifestItem] // 资源清单
public internal(set) var spine: [RDEPUBSpineItem] // 阅读顺序
public internal(set) var tableOfContents: [EPUBTableOfContentsItem] // 目录
public internal(set) var extractionRootURL: URL? // 解压根目录
public internal(set) var opfURL: URL? // OPF 文件路径
public var opfDirectoryURL: URL? // OPF 所在目录
/// 解析 EPUB 文件(解压 + 解析 container.xml + 解析 OPF
public func parse(epubURL: URL) throws
/// 直接解析 OPF 文件
public func parseOPF(at opfURL: URL) throws
/// 解析 Navigation Document 或 NCX
public func parseTOC() -> [EPUBTableOfContentsItem]
/// 从 parser 创建 Publication
public func makePublication() -> RDEPUBPublication
}
```
**扩展文件职责:**
| 文件 | 职责 |
|------|------|
| `RDEPUBParser+Archive.swift` | ZIP 解压ZIPFoundation提取到 Caches 目录 |
| `RDEPUBParser+Package.swift` | OPF XML SAX 解析metadata/manifest/spine |
| `RDEPUBParser+TOC.swift` | Navigation Document 和 NCX 解析 |
| `RDEPUBParser+Resources.swift` | 资源文件读取HTML 字符串、文件 URL |
| `RDEPUBParser+ReadingProfile.swift` | 阅读配置文件检测 |
### 2.2 RDEPUBParserError
```swift
public enum RDEPUBParserError: LocalizedError {
case archiveOpenFailed(URL)
case missingContainerXML
case missingRootFile
case invalidRootFilePath(String)
case invalidXML(URL)
case missingManifestItem(idref: String)
case emptySpine
case invalidArchiveEntryPath(String)
}
```
---
## 3. 数据模型
### 3.1 RDEPUBMetadata
**文件:** `RDEPUBModels.swift`
```swift
public struct RDEPUBMetadata: Codable, Equatable {
public var identifier: String? // ISBN/UUID
public var title: String // 书名
public var author: String? // 作者
public var language: String? // 语言
public var version: String? // EPUB 版本
public var layout: RDEPUBLayout // reflowable / fixed
public var spread: String? // none / auto
public var readingProgression: RDEPUBReadingProgression // ltr / rtl / auto
}
```
### 3.2 RDEPUBManifestItem
```swift
public struct RDEPUBManifestItem: Codable, Equatable {
public var id: String // 资源 ID
public var href: String // 相对路径
public var mediaType: String // MIME 类型
public var properties: [String] // 属性nav, mathml, svg 等)
public var fallback: String? // 回退资源 ID
public var mediaOverlay: String? // 媒体叠加 ID
public var title: String? // 标题
public var isNavigationDocument: Bool // 是否为 Navigation Document
public var isNCX: Bool // 是否为 NCX 文件
}
```
### 3.3 RDEPUBSpineItem
```swift
public struct RDEPUBSpineItem: Codable, Equatable {
public var idref: String // 引用 manifest ID
public var href: String // 解析后的相对路径
public var mediaType: String // MIME 类型
public var title: String // 章节标题
public var linear: Bool // 是否为线性内容
public var properties: [String] // 属性page-spread-left/right
public var pageSpread: RDEPUBPageSpread? // 页面位置
public var layout: RDEPUBLayout? // 章节级布局覆盖
}
```
### 3.4 EPUBTableOfContentsItem
```swift
public struct EPUBTableOfContentsItem: Codable, Equatable {
public var title: String // 目录标题
public var href: String // 链接地址
public var children: [EPUBTableOfContentsItem] // 子目录(支持多级)
}
```
### 3.5 枚举类型
```swift
public enum RDEPUBLayout: String, Codable {
case reflowable // 文本重排
case fixed // 固定布局
}
public enum RDEPUBReadingProfile: String, Codable {
case webInteractive // WebView 交互模式(原 EPUB 渲染)
case webFixedLayout // WebView 固定布局
case textReflowable // 原生文本渲染DTCoreText
}
public enum RDEPUBReadingProgression: String, Codable {
case ltr, rtl, auto
}
public enum RDEPUBPageSpread: String, Codable {
case left, right, center
}
```
---
## 4. Publication
**文件:** `RDEPUBPublication.swift`
聚合 `RDEPUBParser``RDEPUBResourceResolver`,提供统一的出版物访问接口。
```swift
public final class RDEPUBPublication {
public let parser: RDEPUBParser
public let resourceResolver: RDEPUBResourceResolver
public var metadata: RDEPUBMetadata
public var manifest: [String: RDEPUBManifestItem]
public var spine: [RDEPUBSpineItem]
public var tableOfContents: [EPUBTableOfContentsItem]
public var layout: RDEPUBLayout
public var readingProfile: RDEPUBReadingProfile
public var readingProgression: RDEPUBReadingProgression
public var bookIdentifier: String?
/// 判断 Fixed Layout 是否启用双页展开
public func fixedLayoutSpreadEnabled(for preferences: RDEPUBPreferences, viewportSize: CGSize) -> Bool
/// 生成 Fixed Layout 的 Spread 列表
public func makeFixedSpreads(preferences: RDEPUBPreferences, viewportSize: CGSize) -> [EPUBFixedSpread]
}
```
---
## 5. 阅读会话
**文件:** `RDEPUBReadingSession.swift`
管理阅读过程中的分页快照、阅读位置和待导航状态。
```swift
public final class RDEPUBReadingSession {
public typealias PaginationSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
public let publication: RDEPUBPublication
public private(set) var navigatorState: RDEPUBNavigatorState
public private(set) var activePages: [EPUBPage] // 当前活跃页列表
public private(set) var activeChapters: [EPUBChapterInfo] // 当前活跃章节列表
public private(set) var stagedPages: [EPUBPage]? // 暂存页列表(待应用)
public private(set) var stagedChapters: [EPUBChapterInfo]?
public private(set) var currentViewport: RDEPUBViewport?
public private(set) var currentReadingContext: RDEPUBReadingContext?
// 状态管理
public func transition(to state: RDEPUBNavigatorState)
public func resetRuntimeState()
// 快照管理
public func setActiveSnapshot(_ snapshot: PaginationSnapshot)
public func stageSnapshot(_ snapshot: PaginationSnapshot, restoreLocation: RDEPUBLocation?)
public func consumeStagedSnapshotIfAllowed() -> (snapshot: PaginationSnapshot, restoreLocation: RDEPUBLocation?)?
// 导航
public func queueNavigation(to location: RDEPUBLocation, ...) -> Int?
public func updateReadingContext(pageNumber:location:spineIndex:chapterIndex:bookIdentifier:)
public func currentReadingLocation(bookIdentifier: String?) -> RDEPUBLocation?
// 分页快照生成
public func makePaginationSnapshot(pageCounts:preferences:layoutContext:) -> PaginationSnapshot
}
```
### 5.1 RDEPUBNavigatorState
```swift
public enum RDEPUBNavigatorState: String, Codable {
case initializing // 初始化中
case loading // 加载中
case idle // 空闲(可应用快照)
case jumping // 跳转中
case moving // 翻页中
case repaginating // 重新分页中
public var isStableForSnapshotApplication: Bool // 仅 idle 状态为 true
}
```
---
## 6. WebView 子系统
### 6.1 RDEPUBWebView
**文件:** `RDEPUBWebView.swift` + 扩展文件
`UIView` 子类,内部封装 `WKWebView`,支持 Reflowable 和 Fixed Layout 两种渲染模式。
```swift
public final class RDEPUBWebView: UIView {
public weak var delegate: RDEPUBWebViewDelegate?
public var onRendered: (() -> Void)?
var publication: RDEPUBPublication?
var currentRenderRequest: RDEPUBRenderRequest?
var webView: WKWebView?
var schemeHandler: RDEPUBResourceURLSchemeHandler?
var currentSpineIndex: Int
var currentHref: String
var currentPageIndex: Int
var currentTotalPagesInChapter: Int
var viewportSize: CGSize
var currentFontSize: CGFloat
var currentLineHeightMultiple: CGFloat
var targetLocation: RDEPUBLocation?
var pendingHighlights: [RDEPUBHighlight]
}
```
**扩展文件:**
| 文件 | 职责 |
|------|------|
| `RDEPUBWebView+Configuration.swift` | WKWebView 配置scheme handler、user script |
| `RDEPUBWebView+Reflowable.swift` | Reflowable 模式渲染逻辑 |
| `RDEPUBWebView+FixedLayout.swift` | Fixed Layout 模式渲染逻辑 |
| `RDEPUBWebView+JavaScriptBridge.swift` | JS 消息接收与处理 |
| `RDEPUBWebView+Search.swift` | 搜索高亮渲染 |
### 6.2 RDEPUBWebViewDelegate
```swift
public protocol RDEPUBWebViewDelegate: AnyObject {
func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int)
func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int)
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int)
func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL)
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String)
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView)
}
```
---
## 7. JavaScript 桥接
**文件:** `RDEPUBJavaScriptBridge.swift`
定义 Native ↔ WebView 的消息协议和脚本注入。
### 7.1 消息类型
```swift
enum RDEPUBJavaScriptBridgeMessage: String, CaseIterable {
case progressionChanged = "ssReaderProgressionChanged" // 阅读进度变化
case selectionChanged = "ssReaderSelectionChanged" // 文本选择变化
case internalLink = "ssReaderInternalLink" // 内部链接点击
case externalLink = "ssReaderExternalLink" // 外部链接点击
case javaScriptError = "ssReaderJSError" // JS 错误
case fixedLayoutReady = "ssReaderFixedLayoutReady" // Fixed Layout 就绪
}
```
### 7.2 注入的 JS 资源
- `rangy-core.js` — 范围与选择 API
- `rangy-serializer.js` — 选择序列化
- `cssInjector.js` — CSS 注入
- `WeReadApi.js` — 阅读器 API分页、跳转、高亮
- `epub-bridge.js` — 消息桥接
### 7.3 关键方法
```swift
enum RDEPUBJavaScriptBridge {
static var messageNames: [String] // 所有消息名称
static var userScript: String // 注入的 User Script
/// 生成 Reflowable 渲染脚本
static func applyPresentationScript(for request: RDEPUBReflowableRenderRequest) -> String
/// 生成 Fixed Layout 渲染脚本
static func applyFixedPresentationScript(for request: RDEPUBFixedRenderRequest) -> String
}
```
---
## 8. 资源解析
**文件:** `RDEPUBResourceResolver.swift`
负责 href 归一化、文件路径解析和 spine 索引查找。
```swift
public final class RDEPUBResourceResolver {
public var opfDirectoryURL: URL?
/// 将相对路径转为文件 URL
public func fileURL(forRelativePath relativePath: String) -> URL?
/// 将相对路径转为 ss-reader:// 资源 URL
public func resourceURL(forRelativePath relativePath: String) -> URL?
/// href 归一化(去除 fragment解析相对路径
public func normalizedHref(_ href: String, relativeToSpineIndex spineIndex: Int? = nil) -> String?
public func normalizedHref(_ href: String, relativeToHref baseHref: String) -> String?
/// 位置归一化(合并 href + fragment + progression
public func normalizedLocation(_ location: RDEPUBLocation, relativeToSpineIndex spineIndex: Int?, bookIdentifier: String?) -> RDEPUBLocation?
/// 根据 href 查找 spine 索引
public func spineIndex(forNormalizedHref normalizedHref: String) -> Int?
public func spineIndex(for location: RDEPUBLocation) -> Int?
/// 获取 spine 项的 href 和 title
public func href(forSpineIndex spineIndex: Int) -> String?
public func title(forSpineIndex spineIndex: Int) -> String?
}
```
---
## 9. URL 方案处理器
**文件:** `RDEPUBResourceURLSchemeHandler.swift`
实现 `WKURLSchemeHandler`,拦截 `ss-reader://book/` 请求,从 EPUB 解压目录流式读取资源文件。
```swift
public final class RDEPUBResourceURLSchemeHandler: NSObject, WKURLSchemeHandler {
public static let scheme = "ss-reader"
public static let host = "book"
public init(parser: RDEPUBParser)
// WKURLSchemeHandler
public func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask)
public func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask)
}
```
---
## 10. 分页器
**文件:** `RDEPUBPaginator.swift`
使用隐藏的 WKWebView 逐章加载 HTML通过 JS 测量每章的页数。
```swift
public final class RDEPUBPaginator: NSObject {
/// 计算所有章节的页数
public func calculate(
parser: RDEPUBParser,
hostingView: UIView,
presentation: RDEPUBPresentationStyle,
completion: @escaping ([Int]) -> Void
)
/// 计算单个章节的页数
public func calculateSingleChapter(
parser: RDEPUBParser,
spineIndex: Int,
hostingView: UIView,
presentation: RDEPUBPresentationStyle,
completion: @escaping (Int) -> Void
)
}
```
---
## 11. 搜索引擎
**文件:** `RDEPUBSearchEngine.swift`
```swift
protocol RDEPUBSearchEngine {
func search(keyword: String) -> [RDEPUBSearchMatch]
}
```
### RDEPUBHTMLSearchEngine
基于 HTML 的搜索引擎,逐章加载 HTML → 转为纯文本 → 关键词匹配。
```swift
final class RDEPUBHTMLSearchEngine: RDEPUBSearchEngine {
init(parser: RDEPUBParser, publication: RDEPUBPublication)
func search(keyword: String) -> [RDEPUBSearchMatch]
}
```
### RDEPUBTextSearchEngineEPUBTextRendering 模块)
基于原生文本的搜索引擎,在 `NSAttributedString` 上直接搜索,性能更好。
```swift
final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
init(textBook: RDEPUBTextBook, publication: RDEPUBPublication)
func search(keyword: String) -> [RDEPUBSearchMatch]
}
```
---
## 12. CFI 子系统
**文件:** `CFI/RDEPUBCFI*.swift`13 个文件)
实现 EPUB CFICanonical Fragment Identifier标准用于精确定位 EPUB 内容。
| 文件 | 职责 |
|------|------|
| `RDEPUBCFI.swift` | CFI 值类型,核心解析入口 |
| `RDEPUBCFIParser.swift` | CFI 字符串解析器 |
| `RDEPUBCFISerializer.swift` | CFI 序列化为字符串 |
| `RDEPUBCFIPath.swift` | CFI 路径节点 |
| `RDEPUBCFIRange.swift` | CFI 范围(起止点) |
| `RDEPUBCFIResolver.swift` | CFI → DOM 位置解析 |
| `RDEPUBCFIGenerator.swift` | DOM 位置 → CFI 生成 |
| `RDEPUBCFIMap.swift` | 章节级 CFI 映射表 |
| `RDEPUBCFIDOMPathBuilder.swift` | DOM 路径构建 |
| `RDEPUBCFITextAssertion.swift` | 文本断言(偏移校验) |
| `RDEPUBCFIRecoveryEngine.swift` | CFI 恢复引擎(节点变化后重新定位) |
| `RDEPUBCFICompatibility.swift` | CFI 兼容性处理 |
| `RDEPUBCFIError.swift` | 错误类型 |
---
## 13. 样式构建
**文件:** `RDEPUBStyleSheetBuilder.swift`
```swift
public enum RDEPUBStyleSheetBuilder {
/// 向 HTML 注入分页 CSS用于 Paginator 测量)
public static func injectPaginationCSS(into html: String, presentation: RDEPUBPresentationStyle) -> String
/// 生成渲染用 CSSviewport 尺寸、字体、行高、主题色)
public static func renderCSS(for presentation: RDEPUBPresentationStyle) -> String
}
```
---
## 14. 偏好设置
**文件:** `RDEPUBPreferences.swift`
```swift
public struct RDEPUBPreferences: Equatable {
public var fontSize: CGFloat // 字体大小
public var lineHeightMultiple: CGFloat // 行高倍数
public var reflowableContentInsets: UIEdgeInsets // Reflowable 内容边距
public var fixedContentInset: UIEdgeInsets // Fixed Layout 内容边距
public var themeBackgroundColor: String? // 主题背景色CSS
public var themeTextColor: String? // 主题文字色CSS
public var fixedBackgroundColor: String? // Fixed 背景色
public var fixedLayoutFit: RDEPUBFixedLayoutFit // Fixed 适配模式
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode // 双页模式
public var numberOfColumns: Int // 列数
public var columnGap: CGFloat // 列间距
/// 生成 PresentationStyle
public func presentationStyle(viewportSize: CGSize) -> RDEPUBPresentationStyle
/// 生成渲染请求
public func renderRequest(for page: EPUBPage, publication: RDEPUBPublication, viewportSize: CGSize, ...) -> RDEPUBRenderRequest?
}
```
---
## 15. 渲染请求模型
**文件:** `RDEPUBRenderRequest.swift`
```swift
public enum RDEPUBRenderRequest: Equatable {
case reflowable(RDEPUBReflowableRenderRequest)
case fixed(RDEPUBFixedRenderRequest)
public var isFixedLayout: Bool
public var primarySpineIndex: Int
public var primaryHref: String
}
public struct RDEPUBReflowableRenderRequest: Equatable {
public var spineIndex: Int
public var href: String
public var pageIndex: Int
public var totalPagesInChapter: Int
public var presentation: RDEPUBPresentationStyle
public var targetLocation: RDEPUBLocation?
public var highlights: [RDEPUBHighlight]
public var searchPresentation: RDEPUBSearchPresentation?
}
public struct RDEPUBFixedRenderRequest: Equatable {
public var spread: EPUBFixedSpread
public var viewportSize: CGSize
public var contentInset: UIEdgeInsets
public var fit: RDEPUBFixedLayoutFit
}
```
### RDEPUBPresentationStyle
```swift
public struct RDEPUBPresentationStyle: Equatable {
public var viewportSize: CGSize
public var contentInsets: UIEdgeInsets
public var fontSize: CGFloat
public var lineHeightMultiple: CGFloat
public var numberOfColumns: Int
public var columnGap: CGFloat
public var themeBackgroundColor: String?
public var themeTextColor: String?
}
```
---
## 16. 资源仓库
**文件:** `RDEPUBAssetRepository.swift`
管理 SDK 内置的 JS/CSS/HTML 资源文件加载。
```swift
enum RDEPUBAsset: String {
case rangyCoreScript, rangySerializerScript, cssInjectorScript
case weReadAPIScript, bridgeScript, fixedLayoutTemplate
case wxReadDefaultCSS, wxReadReplaceCSS, wxReadDarkCSS, wxReadLatinReplaceCSS
}
enum RDEPUBAssetRepository {
/// 加载资源文件内容,支持模板替换
static func string(for asset: RDEPUBAsset, replacements: [String: String] = [:]) -> String
}
```
---
## 17. 笔记子系统
**文件:** `Notes/RDEPUBNote*.swift`
| 文件 | 职责 |
|------|------|
| `RDEPUBNoteModels.swift` | 脚注/尾注数据模型 |
| `RDEPUBNoteDetector.swift` | 从 HTML 中检测脚注标记 |
| `RDEPUBNoteResolver.swift` | 解析脚注内容 |
---
## 18. 设计模式总结
| 模式 | 应用 |
|------|------|
| **Builder 模式** | `RDEPUBParser``RDEPUBPublication` 构建链 |
| **Strategy 模式** | `RDEPUBSearchEngine` 协议HTML 和 Text 两种实现 |
| **URL Scheme 拦截** | `RDEPUBResourceURLSchemeHandler` 自定义协议 |
| **消息桥接** | `RDEPUBJavaScriptBridge` Native ↔ JS 双向通信 |
| **快照管理** | `RDEPUBReadingSession` staged/active 双快照 |
| **状态机** | `RDEPUBNavigatorState` 管理导航状态 |