源码注释: - 为 ~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)
|
|
}
|
|
}
|
|
|