# EPUBCore 功能实现逻辑 ## 1. 范围与目标 - 代码范围:`Sources/RDReaderView/EPUBCore/`(30 个 Swift 文件 + 2 个资源文件) - 目标:说明 EPUBCore 如何完成 EPUB 解析、资源定位、阅读会话管理、离屏分页、JS 桥接渲染和全文搜索。 - 主链路关键词:`epubURL -> RDEPUBParser.parse -> container.xml -> OPF -> spine/TOC -> RDEPUBPublication -> RDEPUBReadingSession -> RDEPUBWebView/Paginator -> 分页/渲染`。 ## 2. 关键对象职责 ### 2.1 解析器 `RDEPUBParser` - 文件:`EPUBCore/RDEPUBParser.swift` + 5 个扩展文件 - 入口方法:`parse(epubURL:)` - 职责: - ZIP 解压到缓存目录(`RDEPUBParser+Archive.swift`) - 解析 `META-INF/container.xml` 定位 OPF 根文件 - 解析 OPF 文档提取 metadata / manifest / spine(`RDEPUBParser+Package.swift`) - 解析目录支持 NCX(EPUB 2)和 Nav Document(EPUB 3)(`RDEPUBParser+TOC.swift`) - 判断阅读配置文件类型(`RDEPUBParser+ReadingProfile.swift`) - 资源路径转换:相对路径 ↔ 文件 URL ↔ `ss-reader://` 自定义 scheme URL(`RDEPUBParser+Resources.swift`) ### 2.2 Publication 门面 `RDEPUBPublication` - 文件:`EPUBCore/RDEPUBPublication.swift` - 职责: - 包装 `RDEPUBParser` + `RDEPUBResourceResolver`,对外暴露只读接口 - 提供 metadata、manifest、spine、TOC、layout、readingProfile、readingProgression - 计算 fixed layout 的 spread 组合:`makeFixedSpreads(preferences:viewportSize:)` ### 2.3 阅读会话 `RDEPUBReadingSession` - 文件:`EPUBCore/RDEPUBReadingSession.swift` + `RDEPUBNavigatorState.swift` - 职责: - 管理导航状态机:`initializing -> loading -> idle <-> jumping/moving/repaginating` - 维护活跃/暂存分页快照(`activePages` / `activeChapters` / `stagedPages` / `stagedChapters`) - 管理待定导航目标(`pendingNavigationLocation` / `pendingNavigationPageNum`) - 更新阅读上下文(`currentReadingContext`:location + viewport + pageNumber + chapterIndex) - 构建分页快照:`makePaginationSnapshot(pageCounts:preferences:layoutContext:)` ### 2.4 WebView 渲染层 `RDEPUBWebView` - 文件:`EPUBCore/RDEPUBWebView.swift` + 5 个扩展文件 - 职责: - 内部持有 WKWebView,配置自定义 scheme handler 和 JS 桥接 - 可重排内容加载:`loadPage(parser:spineIndex:pageIndex:...)`(`+Reflowable.swift`) - 固定版式内容加载:`loadFixedSpread(parser:spread:...)`(`+FixedLayout.swift`) - 处理 JS 桥接消息和链接导航拦截(`+JavaScriptBridge.swift`) - 搜索高亮装饰(`+Search.swift`) - 定义 `RDEPUBWebViewDelegate` 协议(6 个回调方法) ### 2.5 离屏分页器 `RDEPUBPaginator` - 文件:`EPUBCore/RDEPUBPaginator.swift` - 职责: - 创建隐藏的 WKWebView 加载每个 spine 项的 HTML - 注入分页 CSS,运行 JS 测量 `scrollWidth` - 计算 `ceil(scrollWidth / viewportWidth)` 作为页数 - 多次测量取最大值(延迟 0ms / 80ms / 180ms)确保布局稳定 ### 2.6 资源解析 `RDEPUBResourceResolver` + `RDEPUBResourceURLSchemeHandler` - 文件:`EPUBCore/RDEPUBResourceResolver.swift`、`EPUBCore/RDEPUBResourceURLSchemeHandler.swift` - 职责: - `RDEPUBResourceResolver`:对外门面,提供 href 标准化、spine 索引查找、Location 构建 - `RDEPUBResourceURLSchemeHandler`:实现 `WKURLSchemeHandler`,将 `ss-reader://book/` 映射到解压后的文件系统路径,缺失的可选资源(字体/图片/CSS/JS)返回空 200 ### 2.7 JS 桥接 - 文件:`EPUBCore/RDEPUBJavaScriptBridge.swift`、`Resources/epub-bridge.js` - 6 种消息类型(JS → Swift): - `ssReaderProgressionChanged`:阅读进度变化 - `ssReaderSelectionChanged`:文本选择变化 - `ssReaderInternalLink`:内部链接点击 - `ssReaderExternalLink`:外部链接点击 - `ssReaderJSError`:JS 错误 - `ssReaderFixedLayoutReady`:固定版式渲染完成 - JS 端 `window.RDReaderBridge` 暴露:`applyPagination`、`setPageMetrics`、`scrollToPage`、`scrollToLocation`、`setHighlights`、`clearHighlights`、`reportProgression`、`selectionPayload` ### 2.8 搜索引擎 `RDEPUBHTMLSearchEngine` - 文件:`EPUBCore/RDEPUBSearchEngine.swift`、`EPUBCore/RDEPUBSearchModels.swift` - 职责: - 将 HTML 转为纯文本(NSAttributedString 或正则兜底) - 执行大小写不敏感的全文搜索 - 返回 `RDEPUBSearchMatch` 数组(含 progression 偏移和预览文本) ### 2.9 配置与样式 - `RDEPUBPreferences`(`EPUBCore/RDEPUBPreferences.swift`):用户阅读偏好(字号、行高、内容边距、主题色、固定版式适配模式),构建 `RDEPUBPresentationStyle` 和 `RDEPUBRenderRequest` - `RDEPUBNavigatorLayoutContext`(`EPUBCore/RDEPUBNavigatorLayoutContext.swift`):容器布局上下文(容器尺寸、每屏页数、安全区域、设备类型) - `RDEPUBStyleSheetBuilder`(`EPUBCore/RDEPUBStyleSheetBuilder.swift`):生成分页 CSS、渲染 CSS 和测量 JS 脚本 - `RDEPUBFixedLayoutTemplate`(`EPUBCore/RDEPUBFixedLayoutTemplate.swift`):生成固定版式 HTML 模板 ## 3. 主流程(代码级) ### 3.1 EPUB 解析全流程 1. 入口:`RDEPUBParser.parse(epubURL:)`。 2. 调用 `extractArchiveIfNeeded(epubURL:)`: - 缓存路径:`~/Library/Caches/ssreaderview-epub/--/` - 若目录已存在则跳过解压 - 否则用 ZIPFoundation 解压 ZIP 到缓存目录 3. 解析 `META-INF/container.xml`: - `ContainerXMLParserDelegate`(SAX)找到第一个 `` 的 `full-path` 属性 4. 解析 OPF 文档: - `OPFPackageParserDelegate`(SAX)分段解析 metadata / manifest / spine - 提取 `` 版本和 unique-identifier - 提取 `` / `` / `` / `` - 提取 `` 判断 fixed/reflowable - 提取 manifest 每个 `` 为 `RDEPUBManifestItem` - 提取 spine 每个 `` 为 `OPFSpineReference` 5. 构建 spine:`buildSpine(from:opfURL:)` - 将 spine reference 匹配到 manifest item - 标准化 href(相对于 OPF 目录) - 确定每项的有效 layout(显式属性覆盖出版级设置) - 返回 `[RDEPUBSpineItem]`,空 spine 抛 `emptySpine` 错误 6. 解析目录:`parseTOC(from:opfURL:)` - 优先 NCX(`NCXParserDelegate`),其次 Nav Document(`NavDocumentParserDelegate`) - 兜底使用 spine 标题 - href 标准化保留 fragment 标识符 7. 创建 Publication:`makePublication()` → `RDEPUBPublication(parser: self)` ### 3.2 阅读配置文件判断 - 入口:`RDEPUBParser+ReadingProfile.swift` - 判断逻辑: - 扫描 manifest 检查是否有 `scripted` 属性的项 - 扫描 HTML body 检查是否有 `script`、`iframe`、`video` 等交互元素 - 结果: - `.webFixedLayout`:固定版式 EPUB(漫画、绘本)— WKWebView 渲染 - `.webInteractive`:可重排 + 交互脚本 EPUB — WKWebView 渲染 - `.textReflowable`:纯文本可重排 EPUB — DTCoreText 渲染 ### 3.3 离屏分页测量 1. 入口:`RDEPUBPaginator.calculate(parser:hostingView:presentation:completion:)` 2. 初始化所有 spine 项页数为 `[1, 1, ..., 1]` 3. 固定版式直接返回(每 spread = 1 页) 4. 顺序遍历可渲染的 HTML/XHTML spine 项 5. 对每项: - `webView.loadFileURL(...)` 加载文件 - `didFinish` 后启动多次测量(`scheduleMeasurementPass`) - 3 次测量延迟 [0ms, 80ms, 180ms] - 每次执行 `measurementScript`:注入分页 CSS → 测量 `scrollWidth` → 返回 `Math.max(1, Math.ceil(totalWidth / viewportWidth))` - 取多次测量的最大值 6. 全部完成后回调 `completion([Int])` 页数数组 7. `activeSessionID`(UUID)防止过期回调污染结果 ### 3.4 WebView 可重排内容加载 1. 入口:`RDEPUBWebView.loadPage(parser:spineIndex:pageIndex:...)` 2. 计算 `loadSignature`(包含 publication key、spine index、href、viewport、字号、行高、主题色、目标位置、高亮等) 3. 若签名与 `currentLoadSignature` 相同且已渲染/正在加载,跳过重复请求 4. 若签名匹配但文档 URL 已加载,仅调用 `applyPresentation()` 重新应用样式 5. 否则 `handleReflowableLoad(...)` 执行完整加载: - 读取 HTML 内容 - 调用 `applyPresentationScript(for:)` 生成 JS - JS 执行:`RDReaderBridge.applyPagination(css)` → `setPageMetrics` → 清除并重新应用高亮 → 滚动到目标位置 → 报告进度 → 应用搜索装饰 ### 3.5 固定版式内容加载 1. 入口:`RDEPUBWebView.loadFixedSpread(parser:spread:...)` 2. `RDEPUBFixedLayoutTemplate.html(for:publication:)` 从模板生成 HTML,包含 spread 中每个资源的 `