feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化

- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
This commit is contained in:
shenlei
2026-06-22 20:26:34 +08:00
parent f50495ad91
commit c65c190b71
178 changed files with 11380 additions and 6728 deletions
+642
View File
@@ -0,0 +1,642 @@
# 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` 管理导航状态 |