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 模式:只缓存页范围,不缓存富文本) /// - 性能采样(记录每章渲染/分页耗时) /// - 尾页规范化(丢弃纯空白尾页、合并过短尾页)