docs: 补充注释、修正过时文档、清理重复内容
源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 文本书籍构建器,负责将 EPUB publication 渲染、分页并组装为 `RDEPUBTextBook`。
|
||||
public final class RDEPUBTextBookBuilder {
|
||||
private let renderer: RDEPUBTextRenderer
|
||||
private let cache: RDEPUBTextBookCache?
|
||||
@@ -19,6 +21,11 @@ public final class RDEPUBTextBookBuilder {
|
||||
/// 最后一次构建的缓存命中/未命中统计
|
||||
public private(set) var lastBuildCacheStats: (hits: Int, misses: Int) = (0, 0)
|
||||
|
||||
/// 创建书籍构建器。
|
||||
/// - Parameters:
|
||||
/// - renderer: 文本渲染器
|
||||
/// - cache: 分页缓存(可选)
|
||||
/// - layoutConfig: 页面布局配置
|
||||
public init(
|
||||
renderer: RDEPUBTextRenderer,
|
||||
cache: RDEPUBTextBookCache? = nil,
|
||||
|
||||
@@ -181,13 +181,3 @@ public struct RDEPUBTextBook {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 分页书籍构建器
|
||||
|
||||
/// 从 EPUB publication 构建分页书籍模型的核心构建器。
|
||||
///
|
||||
/// 渲染链路:遍历 spine → 渲染每章 HTML → 分页 → 合并/规范化尾页 → 构建 RDEPUBTextBook
|
||||
///
|
||||
/// 特性:
|
||||
/// - 支持分页缓存(WXRead 模式:只缓存页范围,不缓存富文本)
|
||||
/// - 性能采样(记录每章渲染/分页耗时)
|
||||
/// - 尾页规范化(丢弃纯空白尾页、合并过短尾页)
|
||||
|
||||
+20
@@ -1,5 +1,9 @@
|
||||
// RDEPUBTextBuildPipelineInterfaces.swift
|
||||
// EPUB 文本构建管线接口定义,包括全书构建、章节渲染与分页管线。
|
||||
|
||||
import UIKit
|
||||
|
||||
/// EPUB 全书文本构建接口,将 EPUB 出版物解析为可分页的文本书籍。
|
||||
protocol RDEPUBTextBookBuilding {
|
||||
func build(
|
||||
parser: RDEPUBParser,
|
||||
@@ -9,25 +13,41 @@ protocol RDEPUBTextBookBuilding {
|
||||
) throws -> RDEPUBTextBook
|
||||
}
|
||||
|
||||
/// 章节渲染管线,将章节渲染请求委托给底层文本渲染器。
|
||||
struct RDEPUBChapterRenderPipeline {
|
||||
private let renderer: RDEPUBTextRenderer
|
||||
|
||||
/// 初始化渲染管线。
|
||||
/// - Parameter renderer: 文本渲染器实例
|
||||
init(renderer: RDEPUBTextRenderer) {
|
||||
self.renderer = renderer
|
||||
}
|
||||
|
||||
/// 渲染单个章节,返回渲染后的富文本内容。
|
||||
/// - Parameter request: 章节渲染请求
|
||||
/// - Returns: 渲染完成的章节内容
|
||||
func render(_ request: RDEPUBTextChapterRenderRequest) throws -> RDEPUBRenderedChapterContent {
|
||||
try renderer.renderChapter(request: request)
|
||||
}
|
||||
}
|
||||
|
||||
/// 章节分页管线,将渲染后的富文本按页面尺寸拆分为文本帧数组。
|
||||
struct RDEPUBChapterPaginationPipeline {
|
||||
private let frameFactory: RDEPUBPageFrameBuilding
|
||||
|
||||
/// 初始化分页管线。
|
||||
/// - Parameter frameFactory: 页面帧工厂,默认使用 CoreText 实现
|
||||
init(frameFactory: RDEPUBPageFrameBuilding = RDEPUBCoreTextPageFrameFactory()) {
|
||||
self.frameFactory = frameFactory
|
||||
}
|
||||
|
||||
/// 将富文本内容分页,返回页面帧数组。
|
||||
/// - Parameters:
|
||||
/// - content: 待分页的富文本
|
||||
/// - pageSize: 页面尺寸
|
||||
/// - config: 排版配置
|
||||
/// - fragmentOffsets: fragment 锚点偏移映射
|
||||
/// - Returns: 页面帧数组
|
||||
func frames(
|
||||
for content: NSAttributedString,
|
||||
pageSize: CGSize,
|
||||
|
||||
+23
@@ -1,15 +1,30 @@
|
||||
// RDEPUBTextPaginationInterfaces.swift
|
||||
// EPUB 分页接口定义,包括断页决策、章节数页和页面帧构建协议。
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: - 分页接口定义
|
||||
|
||||
/// 单次断页决策,记录断页位置、原因及诊断信息。
|
||||
struct RDEPUBPageBreakDecision {
|
||||
/// 本页在富文本中的字符范围
|
||||
var range: NSRange
|
||||
/// 断页原因
|
||||
var reason: RDEPUBTextPageBreakReason
|
||||
/// 分页过程中的诊断日志
|
||||
var diagnostics: [String]
|
||||
}
|
||||
|
||||
/// 章节页数计算接口,将富文本按页面尺寸拆分为断页决策序列。
|
||||
protocol RDEPUBChapterPageCounting {
|
||||
/// 计算章节中每页的断页位置。
|
||||
/// - Parameters:
|
||||
/// - attributedString: 章节富文本
|
||||
/// - pageSize: 页面尺寸
|
||||
/// - config: 排版配置
|
||||
/// - fragmentOffsets: fragment 锚点偏移映射
|
||||
/// - Returns: 断页决策数组,每个元素对应一页
|
||||
func pageRanges(
|
||||
for attributedString: NSAttributedString,
|
||||
pageSize: CGSize,
|
||||
@@ -18,7 +33,15 @@ protocol RDEPUBChapterPageCounting {
|
||||
) -> [RDEPUBPageBreakDecision]
|
||||
}
|
||||
|
||||
/// 页面帧构建接口,将富文本转换为可渲染的页面帧数组。
|
||||
protocol RDEPUBPageFrameBuilding {
|
||||
/// 将富文本构建为页面帧数组。
|
||||
/// - Parameters:
|
||||
/// - attributedString: 待分页的富文本
|
||||
/// - pageSize: 页面尺寸
|
||||
/// - config: 排版配置
|
||||
/// - fragmentOffsets: fragment 锚点偏移映射
|
||||
/// - Returns: 页面帧数组
|
||||
func makeFrames(
|
||||
attributedString: NSAttributedString,
|
||||
pageSize: CGSize,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// RDEPUBTextPositionConverter.swift
|
||||
// EPUB 全书文本位置转换器,在多种坐标体系之间进行双向映射。
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 对标 WXRead `WREpubPositionConverter` 的全书位置转换器。
|
||||
@@ -8,44 +11,74 @@ import Foundation
|
||||
/// - 全书字符偏移
|
||||
/// - 页码 / `RDEPUBLocation`
|
||||
public struct RDEPUBTextPositionConverter {
|
||||
/// 当前转换器所关联的 EPUB 文本对象。
|
||||
public let book: RDEPUBTextBook
|
||||
|
||||
/// 创建指定 EPUB 文本对象的位置转换器。
|
||||
/// - Parameter book: 要关联的 EPUB 文本对象。
|
||||
public init(book: RDEPUBTextBook) {
|
||||
self.book = book
|
||||
}
|
||||
|
||||
/// 全书字符总数(跨所有文件累计)。
|
||||
public var totalCharacterCount: Int {
|
||||
book.indexTable.totalCharacterCount
|
||||
}
|
||||
|
||||
/// 将语义锚点转换为全书字符偏移。
|
||||
/// - Parameter anchor: 由文件索引、行号、列号组成的语义锚点。
|
||||
/// - Returns: 对应的全书字符偏移量。
|
||||
public func globalIndex(for anchor: RDEPUBTextAnchor) -> Int {
|
||||
book.indexTable.globalIndex(for: anchor)
|
||||
}
|
||||
|
||||
/// 将语义范围锚点转换为全书字符范围。
|
||||
/// - Parameter rangeAnchor: 由起止锚点组成的语义范围。
|
||||
/// - Returns: 对应的全书字符范围(`NSRange`)。
|
||||
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
|
||||
book.indexTable.globalRange(for: rangeAnchor)
|
||||
}
|
||||
|
||||
/// 根据全书字符偏移查找所属文件的索引。
|
||||
/// - Parameter index: 全书字符偏移量。
|
||||
/// - Returns: 对应的文件索引,超出范围时返回 `nil`。
|
||||
public func fileIndex(forCharacterPosition index: Int) -> Int? {
|
||||
book.indexTable.fileIndex(forCharacterPosition: index)
|
||||
}
|
||||
|
||||
/// 将全书字符偏移转换为指定文件内的本地偏移。
|
||||
/// - Parameters:
|
||||
/// - fileIndex: 目标文件索引。
|
||||
/// - index: 全书字符偏移量。
|
||||
/// - Returns: 文件内的本地字符偏移,越界时返回 `nil`。
|
||||
public func localOffsetInFile(at fileIndex: Int, forGlobalPosition index: Int) -> Int? {
|
||||
book.indexTable.localOffsetInFile(at: fileIndex, forGlobalPosition: index)
|
||||
}
|
||||
|
||||
/// 将全书字符偏移转换为语义锚点。
|
||||
/// - Parameter index: 全书字符偏移量。
|
||||
/// - Returns: 对应的语义锚点,无效位置时返回 `nil`。
|
||||
public func anchor(forCharacterPosition index: Int) -> RDEPUBTextAnchor? {
|
||||
book.indexTable.anchor(forGlobalIndex: index)
|
||||
}
|
||||
|
||||
/// 将 `RDEPUBLocation` 转换为语义锚点。
|
||||
/// - Parameter location: EPUB 位置对象。
|
||||
/// - Returns: 对应的语义锚点,无法映射时返回 `nil`。
|
||||
public func anchor(for location: RDEPUBLocation) -> RDEPUBTextAnchor? {
|
||||
book.indexTable.anchor(for: location)
|
||||
}
|
||||
|
||||
/// 根据语义锚点获取页码(从 1 开始)。
|
||||
/// - Parameter anchor: 由文件索引、行号、列号组成的语义锚点。
|
||||
/// - Returns: 对应的页码,无法定位时返回 `nil`。
|
||||
public func pageNumber(for anchor: RDEPUBTextAnchor) -> Int? {
|
||||
book.indexTable.pageNumber(for: anchor, in: book).map { $0 + 1 }
|
||||
}
|
||||
|
||||
/// 根据全书字符偏移获取页码(从 1 开始)。
|
||||
/// - Parameter index: 全书字符偏移量。
|
||||
/// - Returns: 对应的页码,无效位置时返回 `nil`。
|
||||
public func pageNumber(forCharacterPosition index: Int) -> Int? {
|
||||
guard let anchor = anchor(forCharacterPosition: index) else {
|
||||
return nil
|
||||
@@ -53,6 +86,11 @@ public struct RDEPUBTextPositionConverter {
|
||||
return pageNumber(for: anchor)
|
||||
}
|
||||
|
||||
/// 将语义锚点转换为可序列化的 `RDEPUBLocation`。
|
||||
/// - Parameters:
|
||||
/// - anchor: 由文件索引、行号、列号组成的语义锚点。
|
||||
/// - bookIdentifier: 书籍标识符,会写入 location 的 `bookId` 字段。
|
||||
/// - Returns: 对应的 `RDEPUBLocation`,无法映射时返回 `nil`。
|
||||
public func location(
|
||||
for anchor: RDEPUBTextAnchor,
|
||||
bookIdentifier: String?
|
||||
@@ -63,6 +101,11 @@ public struct RDEPUBTextPositionConverter {
|
||||
return book.indexTable.location(for: anchor, in: chapter, bookIdentifier: bookIdentifier)
|
||||
}
|
||||
|
||||
/// 将语义范围锚点转换为可序列化的 `RDEPUBLocation`。
|
||||
/// - Parameters:
|
||||
/// - rangeAnchor: 由起止锚点组成的语义范围。
|
||||
/// - bookIdentifier: 书籍标识符,会写入 location 的 `bookId` 字段。
|
||||
/// - Returns: 对应的 `RDEPUBLocation`,无法映射时返回 `nil`。
|
||||
public func location(
|
||||
for rangeAnchor: RDEPUBTextRangeAnchor,
|
||||
bookIdentifier: String?
|
||||
|
||||
@@ -4,6 +4,7 @@ import UIKit
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
/// 附件布局规范化器,负责缩放脚注、封面和通用图片的显示尺寸。
|
||||
struct RDEPUBAttachmentNormalizer {
|
||||
/// 脚注附件日志已输出标记(避免重复日志)
|
||||
private static var didLogFootnoteAttachment = false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import UIKit
|
||||
import CoreText
|
||||
|
||||
/// 字体规范化器,负责注册 EPUB 嵌入字体并映射到用户设置字体。
|
||||
struct RDEPUBFontNormalizer {
|
||||
/// 已注册字体资源,避免重复调用 CTFontManager。
|
||||
private static var registeredFontPaths = Set<String>()
|
||||
@@ -52,6 +53,8 @@ struct RDEPUBFontNormalizer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 若字体尚未注册,则通过 CTFontManager 注册。
|
||||
/// - Parameter fileURL: 字体文件 URL
|
||||
static func registerFontIfNeeded(at fileURL: URL) {
|
||||
let standardizedPath = fileURL.standardizedFileURL.path
|
||||
guard !registeredFontPaths.contains(standardizedPath) else { return }
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
/// RDEPUBFragmentMarkerInjector - Fragment 标记注入与偏移量提取
|
||||
import Foundation
|
||||
|
||||
/// Fragment 标记注入器,负责在 HTML 中注入 fragment 锚点标记并从渲染结果中提取偏移量。
|
||||
///
|
||||
/// 工作流程:
|
||||
/// 1. `process` 阶段:将 HTML 中的 `id` 属性元素转为 `${id=xxx}` 文本标记
|
||||
/// 2. `extractOffsets` 阶段:从 NSAttributedString 中扫描标记,记录偏移量后删除标记
|
||||
struct RDEPUBFragmentMarkerInjector: RDEPUBTypesettingStage {
|
||||
/// 将 HTML 中的 id 元素注入 fragment 文本标记,供后续偏移量提取使用。
|
||||
func process(_ html: String, context: RDEPUBTypesettingInput) -> String {
|
||||
Self.injectFragmentMarkers(into: html)
|
||||
}
|
||||
|
||||
/// 从渲染后的富文本中扫描 `${id=xxx}` 标记,提取 fragment ID 到字符偏移量的映射。
|
||||
func extractOffsets(from attributedString: NSMutableAttributedString) -> [String: Int] {
|
||||
Self.extractFragmentOffsets(from: attributedString)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
/// HTML 规范化器,清理冗余字符并规范化附件 HTML 标记。
|
||||
struct RDEPUBHTMLNormalizer: RDEPUBTypesettingStage {
|
||||
/// 执行 HTML 规范化处理。
|
||||
/// - Parameters:
|
||||
/// - html: 原始 HTML
|
||||
/// - context: 排版上下文
|
||||
/// - Returns: 规范化后的 HTML
|
||||
func process(_ html: String, context: RDEPUBTypesettingInput) -> String {
|
||||
Self.normalizeHTML(html)
|
||||
}
|
||||
|
||||
+6
@@ -1,11 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
/// 渲染诊断收集器,扫描 HTML 中的资源引用并生成诊断信息。
|
||||
struct RDEPUBRenderDiagnosticsCollector {
|
||||
/// 外部样式表链接正则
|
||||
private static let stylesheetLinkPattern = #"<link\b[^>]*rel\s*=\s*["'][^"']*stylesheet[^"']*["'][^>]*href\s*=\s*["']([^"']+)["'][^>]*>"#
|
||||
/// 图片源地址正则
|
||||
private static let imageSourcePattern = #"<img\b[^>]*src\s*=\s*["']([^"']+)["'][^>]*>"#
|
||||
|
||||
/// 收集 HTML 中图片资源的引用诊断。
|
||||
/// - Parameters:
|
||||
/// - html: 待扫描的 HTML
|
||||
/// - input: 排版输入上下文
|
||||
/// - Returns: 资源引用诊断列表
|
||||
func collect(
|
||||
in html: String,
|
||||
input: RDEPUBTypesettingInput
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
/// RDEPUBSemanticMarkerInjector - 分页语义标记注入与应用
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// 语义标记注入器,为 HTML 标签注入分页语义信息(块级类型、分页提示、附件放置方式等)。
|
||||
///
|
||||
/// 分两阶段工作:
|
||||
/// 1. HTML 阶段(`process`):解析标签并注入 `${rd-sem-start/end}` 语义标记
|
||||
/// 2. 渲染后阶段(`apply`):将标记解析为 NSAttributedString 属性
|
||||
struct RDEPUBSemanticMarkerInjector: RDEPUBTypesettingStage {
|
||||
/// 语义标记正则(${rd-sem-start:...} / ${rd-sem-end:...})
|
||||
private static let semanticMarkerPattern = #"\$\{rd-sem-(start|end):([^}]+)\}"#
|
||||
|
||||
/// 为 HTML 标签注入 `${rd-sem-start/end}` 语义标记,标记块级元素类型和分页提示。
|
||||
func process(_ html: String, context: RDEPUBTypesettingInput) -> String {
|
||||
Self.injectPaginationSemanticMarkers(into: html)
|
||||
}
|
||||
|
||||
/// 解析渲染后富文本中的语义标记,将其转为 NSAttributedString 属性并删除标记文本。
|
||||
func apply(to attributedString: NSMutableAttributedString) {
|
||||
Self.applyPaginationSemantics(in: attributedString)
|
||||
}
|
||||
@@ -323,6 +331,7 @@ struct RDEPUBSemanticMarkerInjector: RDEPUBTypesettingStage {
|
||||
|
||||
// MARK: - 内部类型
|
||||
|
||||
/// 单个 HTML 元素的分页语义信息,由标签名和属性推断而来。
|
||||
struct RDPaginationSemantics {
|
||||
var id: String
|
||||
var blockKind: RDEPUBTextBlockKind?
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import UIKit
|
||||
|
||||
/// 样式表组合结果,包含合成后的 HTML、CSS 层列表及诊断信息。
|
||||
struct RDEPUBStyleSheetComposition {
|
||||
/// 合成后的 HTML 文本
|
||||
var html: String
|
||||
/// 五层 CSS 样式层(default/replace/dark/epub/user)
|
||||
var layers: [RDEPUBTextStyleSheetLayer]
|
||||
/// 内联后的 CSS 内容
|
||||
var inlinedCSS: String
|
||||
/// 资源引用诊断列表
|
||||
var diagnostics: [RDEPUBTextResourceReferenceDiagnostic]
|
||||
}
|
||||
|
||||
/// EPUB 样式表合成器,负责组装五层 CSS 并注入 HTML。
|
||||
struct RDEPUBStyleSheetComposer {
|
||||
/// 将 CSS 层注入 HTML,返回合成结果。
|
||||
/// - Parameters:
|
||||
/// - html: 原始 HTML
|
||||
/// - input: 排版输入上下文
|
||||
/// - Returns: 包含合成 HTML、CSS 层和诊断信息的结果
|
||||
func compose(html: String, input: RDEPUBTypesettingInput) -> RDEPUBStyleSheetComposition {
|
||||
let stylesheetHrefReplacements = RDEPUBRenderDiagnosticsCollector.inlineLinkedStyleSheets(
|
||||
in: html,
|
||||
@@ -84,8 +95,11 @@ struct RDEPUBStyleSheetComposer {
|
||||
|
||||
// MARK: - Style 注入
|
||||
|
||||
/// `<style>` 标签的注入位置。
|
||||
enum StyleInjectionPosition {
|
||||
/// 注入到 `<head>` 标签之后
|
||||
case headStart
|
||||
/// 注入到 `</head>` 标签之前
|
||||
case headEnd
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
import UIKit
|
||||
|
||||
/// 排版输入参数,包含章节 HTML、样式和布局配置。
|
||||
struct RDEPUBTypesettingInput {
|
||||
/// 章节相对路径
|
||||
var href: String
|
||||
/// 章节标题
|
||||
var title: String
|
||||
/// 原始 HTML 内容
|
||||
var rawHTML: String
|
||||
/// HTML 基础 URL,用于解析相对路径
|
||||
var baseURL: URL?
|
||||
/// 渲染样式
|
||||
var style: RDEPUBTextRenderStyle
|
||||
/// 资源解析器
|
||||
var resourceResolver: RDEPUBResourceResolver?
|
||||
/// 内容语言代码(如 zh-CN)
|
||||
var contentLanguageCode: String?
|
||||
/// 页面尺寸
|
||||
var pageSize: CGSize?
|
||||
/// 页面布局配置
|
||||
var layoutConfig: RDEPUBTextLayoutConfig?
|
||||
}
|
||||
|
||||
/// 排版输出结果,包含渲染请求和诊断信息。
|
||||
struct RDEPUBTypesettingOutput {
|
||||
/// 章节渲染请求
|
||||
var request: RDEPUBTextChapterRenderRequest
|
||||
/// 资源引用诊断列表
|
||||
var diagnostics: [RDEPUBTextResourceReferenceDiagnostic]
|
||||
}
|
||||
|
||||
/// 排版阶段协议,定义 HTML 处理管线中的单个步骤。
|
||||
protocol RDEPUBTypesettingStage {
|
||||
/// 处理 HTML 并返回转换后的结果。
|
||||
/// - Parameters:
|
||||
/// - html: 输入 HTML
|
||||
/// - context: 排版上下文
|
||||
/// - Returns: 处理后的 HTML
|
||||
func process(_ html: String, context: RDEPUBTypesettingInput) -> String
|
||||
}
|
||||
|
||||
/// 排版管线,依次执行 HTML 规范化、语义标记、样式合成、字体注册和诊断收集。
|
||||
struct RDEPUBTextTypesetterPipeline {
|
||||
/// 从排版输入构建渲染请求。
|
||||
/// - Parameter input: 排版输入参数
|
||||
/// - Returns: 包含渲染请求和诊断信息的输出
|
||||
func makeRequest(from input: RDEPUBTypesettingInput) -> RDEPUBTypesettingOutput {
|
||||
let htmlNormalizer = RDEPUBHTMLNormalizer()
|
||||
let semanticMarkerInjector = RDEPUBSemanticMarkerInjector()
|
||||
|
||||
Reference in New Issue
Block a user