194 lines
7.7 KiB
Swift
194 lines
7.7 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)
|
||
}
|
||
}
|
||
|
||
// MARK: - 分页书籍构建器
|
||
|
||
/// 从 EPUB publication 构建分页书籍模型的核心构建器。
|
||
///
|
||
/// 渲染链路:遍历 spine → 渲染每章 HTML → 分页 → 合并/规范化尾页 → 构建 RDEPUBTextBook
|
||
///
|
||
/// 特性:
|
||
/// - 支持分页缓存(WXRead 模式:只缓存页范围,不缓存富文本)
|
||
/// - 性能采样(记录每章渲染/分页耗时)
|
||
/// - 尾页规范化(丢弃纯空白尾页、合并过短尾页)
|