源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
184 lines
7.3 KiB
Swift
184 lines
7.3 KiB
Swift
import UIKit
|
||
|
||
// MARK: - 章节分页诊断数据
|
||
|
||
/// 单个章节的分页诊断信息,用于调试和质量检测。
|
||
/// 记录页数、分页原因、附件/块级元素统计等。
|
||
public struct RDEPUBTextChapterPaginationDiagnostic: Equatable {
|
||
public var href: String
|
||
public var title: String
|
||
public var pageCount: Int
|
||
/// 每页的分页原因列表
|
||
public var breakReasons: [RDEPUBTextPageBreakReason]
|
||
/// 包含附件的页数
|
||
public var attachmentPageCount: Int
|
||
/// 因块级/附件边界调整而分页的页数
|
||
public var blockAdjustedPageCount: Int
|
||
public var blockKinds: [RDEPUBTextBlockKind]
|
||
public var semanticHints: [RDEPUBTextSemanticHint]
|
||
public var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
|
||
/// 采样诊断日志(最多 4 条)
|
||
public var sampleNotes: [String]
|
||
}
|
||
|
||
// MARK: - 页面数据模型
|
||
|
||
/// 分页后的单页数据,包含全书绝对页码、所属章节、内容范围等。
|
||
///
|
||
/// 每个 `RDEPUBTextPage` 由 `RDEPUBTextLayoutFrame` 生成,
|
||
/// `contentRange` 标记了该页在章节富文本中的字符范围。
|
||
public struct RDEPUBTextPage: Equatable {
|
||
/// 全书绝对页码(从 0 开始)
|
||
public var absolutePageIndex: Int
|
||
public var chapterIndex: Int
|
||
public var spineIndex: Int
|
||
public var href: String
|
||
public var chapterTitle: String
|
||
/// 该页在所属章节中的相对页码(从 0 开始)
|
||
public var pageIndexInChapter: Int
|
||
public var totalPagesInChapter: Int
|
||
/// 所属章节的完整富文本内容(用于跨页查询)
|
||
public var chapterContent: NSAttributedString
|
||
/// 该页的富文本片段
|
||
public var content: NSAttributedString
|
||
/// 该页在章节富文本中的范围
|
||
public var contentRange: NSRange
|
||
public var pageStartOffset: Int
|
||
public var pageEndOffset: Int
|
||
/// 页的元数据(分页原因、附件、语义标记等)
|
||
public var metadata: RDEPUBTextPageMetadata
|
||
}
|
||
|
||
// MARK: - 章节数据模型
|
||
|
||
/// 渲染并分页后的单个章节,包含完整富文本和分页结果。
|
||
public struct RDEPUBTextChapter: Equatable {
|
||
public var chapterIndex: Int
|
||
public var spineIndex: Int
|
||
public var href: String
|
||
public var title: String
|
||
/// 章节完整富文本内容
|
||
public var attributedContent: NSAttributedString
|
||
/// fragment ID → 字符偏移量映射(用于锚点定位)
|
||
public var fragmentOffsets: [String: Int]
|
||
public var pageBreakReasons: [RDEPUBTextPageBreakReason]
|
||
public var pages: [RDEPUBTextPage]
|
||
}
|
||
|
||
// MARK: - 分页书籍模型
|
||
|
||
/// 整本 EPUB 的分页书籍模型,包含所有章节、页面和全局索引表。
|
||
///
|
||
/// 这是 EPUBTextRendering 层的最终产物,由 `RDEPUBTextBookBuilder.build()` 生成。
|
||
/// 通过 `chapterData(for:)` 或 `chapterData(atChapterIndex:)` 获取 `RDEPUBChapterData` 进行查询。
|
||
public struct RDEPUBTextBook {
|
||
public var chapters: [RDEPUBTextChapter]
|
||
public var pages: [RDEPUBTextPage]
|
||
/// 全局索引表:fileIndex/row/column → 绝对字符偏移量
|
||
public let indexTable: RDEPUBTextIndexTable
|
||
/// 对标 WXRead 的位置转换器(文件位置 <-> 全书字符位置 <-> 页码)
|
||
public var positionConverter: RDEPUBTextPositionConverter {
|
||
RDEPUBTextPositionConverter(book: self)
|
||
}
|
||
|
||
public init(chapters: [RDEPUBTextChapter], pages: [RDEPUBTextPage]) {
|
||
self.chapters = chapters
|
||
self.pages = pages
|
||
self.indexTable = RDEPUBTextIndexTable(chapters: chapters)
|
||
}
|
||
|
||
public static func == (lhs: RDEPUBTextBook, rhs: RDEPUBTextBook) -> Bool {
|
||
lhs.chapters == rhs.chapters && lhs.pages == rhs.pages
|
||
}
|
||
|
||
/// 按 href 获取章节的数据访问层(包含索引表)
|
||
public func chapterData(for href: String) -> RDEPUBChapterData? {
|
||
guard let chapter = chapters.first(where: { $0.href == href }) else { return nil }
|
||
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
|
||
}
|
||
|
||
/// 按 spine 索引获取章节的数据访问层。
|
||
public func chapterData(forSpineIndex spineIndex: Int) -> RDEPUBChapterData? {
|
||
guard let chapter = chapters.first(where: { $0.spineIndex == spineIndex }) else { return nil }
|
||
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
|
||
}
|
||
|
||
/// 按章节序号获取章节的数据访问层
|
||
public func chapterData(atChapterIndex index: Int) -> RDEPUBChapterData? {
|
||
guard chapters.indices.contains(index) else { return nil }
|
||
return RDEPUBChapterData(chapter: chapters[index], indexTable: indexTable)
|
||
}
|
||
|
||
/// 按绝对页码(从 1 开始)获取章节的数据访问层。
|
||
public func chapterData(forPageNumber pageNumber: Int) -> RDEPUBChapterData? {
|
||
guard let page = page(at: pageNumber) else { return nil }
|
||
return chapterData(forSpineIndex: page.spineIndex)
|
||
}
|
||
|
||
/// 根据持久化位置解析所属章节的数据访问层。
|
||
public func chapterData(
|
||
for location: RDEPUBLocation,
|
||
resolver: RDEPUBResourceResolver,
|
||
bookIdentifier: String?
|
||
) -> RDEPUBChapterData? {
|
||
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier) else {
|
||
return nil
|
||
}
|
||
return chapterData(for: normalizedLocation.href)
|
||
}
|
||
|
||
/// 全书章节信息快照;对标 WXRead 由章节模型直接提供章节元数据。
|
||
public var chapterInfos: [EPUBChapterInfo] {
|
||
chapters.map { chapter in
|
||
EPUBChapterInfo(
|
||
spineIndex: chapter.spineIndex,
|
||
title: chapter.title,
|
||
pageCount: chapter.pages.count
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 按页码(从 1 开始)获取对应页面
|
||
public func page(at pageNumber: Int) -> RDEPUBTextPage? {
|
||
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else {
|
||
return nil
|
||
}
|
||
return pages[pageNumber - 1]
|
||
}
|
||
|
||
/// 根据持久化位置计算对应页码(从 1 开始)。
|
||
///
|
||
/// 解析优先级:
|
||
/// 1. rangeAnchor 锚点定位
|
||
/// 2. fragment 片段 ID
|
||
/// 3. navigationProgression 进度百分比回退
|
||
public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? {
|
||
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier),
|
||
let chapterData = chapterData(for: normalizedLocation.href) else {
|
||
return nil
|
||
}
|
||
|
||
if let anchor = normalizedLocation.rangeAnchor?.start,
|
||
let page = positionConverter.pageNumber(for: anchor) {
|
||
return page
|
||
}
|
||
|
||
if let anchor = indexTable.anchor(for: normalizedLocation),
|
||
let page = positionConverter.pageNumber(for: anchor) {
|
||
return page
|
||
}
|
||
|
||
return chapterData.pageNumber(for: normalizedLocation)
|
||
}
|
||
|
||
/// 根据页码生成持久化位置(RDEPUBLocation),包含起止锚点
|
||
public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
|
||
guard let chapterData = chapterData(forPageNumber: pageNumber),
|
||
let page = page(at: pageNumber) else {
|
||
return nil
|
||
}
|
||
return chapterData.location(forPage: page, bookIdentifier: bookIdentifier)
|
||
}
|
||
}
|
||
|