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

22 KiB
Raw Permalink Blame History

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 文件中提取结构化数据。

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

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

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

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

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

public struct EPUBTableOfContentsItem: Codable, Equatable {
    public var title: String               // 目录标题
    public var href: String                // 链接地址
    public var children: [EPUBTableOfContentsItem]  // 子目录(支持多级)
}

3.5 枚举类型

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

聚合 RDEPUBParserRDEPUBResourceResolver,提供统一的出版物访问接口。

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

管理阅读过程中的分页快照、阅读位置和待导航状态。

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

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 两种渲染模式。

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

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 消息类型

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 关键方法

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 索引查找。

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 解压目录流式读取资源文件。

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 测量每章的页数。

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

protocol RDEPUBSearchEngine {
    func search(keyword: String) -> [RDEPUBSearchMatch]
}

RDEPUBHTMLSearchEngine

基于 HTML 的搜索引擎,逐章加载 HTML → 转为纯文本 → 关键词匹配。

final class RDEPUBHTMLSearchEngine: RDEPUBSearchEngine {
    init(parser: RDEPUBParser, publication: RDEPUBPublication)
    func search(keyword: String) -> [RDEPUBSearchMatch]
}

RDEPUBTextSearchEngineEPUBTextRendering 模块)

基于原生文本的搜索引擎,在 NSAttributedString 上直接搜索,性能更好。

final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
    init(textBook: RDEPUBTextBook, publication: RDEPUBPublication)
    func search(keyword: String) -> [RDEPUBSearchMatch]
}

12. CFI 子系统

文件: CFI/RDEPUBCFI*.swift13 个文件)

实现 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

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

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

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

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 资源文件加载。

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 模式 RDEPUBParserRDEPUBPublication 构建链
Strategy 模式 RDEPUBSearchEngine 协议HTML 和 Text 两种实现
URL Scheme 拦截 RDEPUBResourceURLSchemeHandler 自定义协议
消息桥接 RDEPUBJavaScriptBridge Native ↔ JS 双向通信
快照管理 RDEPUBReadingSession staged/active 双快照
状态机 RDEPUBNavigatorState 管理导航状态