import Foundation public enum RDEPUBTextPageBreakReason: String, Codable, Equatable { /// 章节自然结束 case chapterEnd /// 达到帧容量限制 case frameLimit /// 块级元素边界(如段落、标题) case blockBoundary /// 附件(图片等)边界 case attachmentBoundary /// 语义边界(如列表、引用块) case semanticBoundary } /// 文本附件类型 public enum RDEPUBTextAttachmentKind: String, Codable, Equatable { /// 图片附件 case image /// 通用附件 case generic } /// 文本页元数据,记录分页时的上下文信息 /// 用于调试分页问题和优化分页质量 public struct RDEPUBTextPageMetadata: Codable, Equatable { /// 分页截断原因 public var breakReason: RDEPUBTextPageBreakReason /// 本页覆盖的块级元素范围 public var blockRange: NSRange? /// 本页包含的附件范围列表 public var attachmentRanges: [NSRange] /// 附件类型列表 public var attachmentKinds: [RDEPUBTextAttachmentKind] /// 块级元素类型列表 public var blockKinds: [RDEPUBTextBlockKind] /// 语义提示信息 public var semanticHints: [RDEPUBTextSemanticHint] /// 附件布局位置信息 public var attachmentPlacements: [RDEPUBTextAttachmentPlacement] /// 本页末尾的 fragment ID(用于恢复位置) public var trailingFragmentID: String? /// 诊断信息列表(调试用) public var diagnostics: [String] public init( breakReason: RDEPUBTextPageBreakReason, blockRange: NSRange? = nil, attachmentRanges: [NSRange] = [], attachmentKinds: [RDEPUBTextAttachmentKind] = [], blockKinds: [RDEPUBTextBlockKind] = [], semanticHints: [RDEPUBTextSemanticHint] = [], attachmentPlacements: [RDEPUBTextAttachmentPlacement] = [], trailingFragmentID: String? = nil, diagnostics: [String] = [] ) { self.breakReason = breakReason self.blockRange = blockRange self.attachmentRanges = attachmentRanges self.attachmentKinds = attachmentKinds self.blockKinds = blockKinds self.semanticHints = semanticHints self.attachmentPlacements = attachmentPlacements self.trailingFragmentID = trailingFragmentID self.diagnostics = diagnostics } } /// 高亮模型,表示用户在阅读中标记的文本片段 public struct EPUBChapterInfo: Codable, Equatable { /// 对应的 spine 索引 public var spineIndex: Int /// 章节标题 public var title: String /// 该章节的总页数 public var pageCount: Int public init(spineIndex: Int, title: String, pageCount: Int) { self.spineIndex = spineIndex self.title = title self.pageCount = pageCount } } /// 页面模型,描述单页的完整信息 /// 由 RDEPUBReadingSession 根据分页结果构建,是翻页容器层的数据单元 public struct EPUBPage: Codable, Equatable { /// 对应的 spine 索引 public var spineIndex: Int /// 章节索引(从 0 开始的章节序号) public var chapterIndex: Int /// 页在章节中的索引(从 0 开始) public var pageIndexInChapter: Int /// 该章节的总页数 public var totalPagesInChapter: Int /// 章节标题 public var chapterTitle: String /// 固定版式 Spread 数据(仅 fixed layout 书籍有值) public var fixedSpread: EPUBFixedSpread? public init( spineIndex: Int, chapterIndex: Int, pageIndexInChapter: Int, totalPagesInChapter: Int, chapterTitle: String, fixedSpread: EPUBFixedSpread? ) { self.spineIndex = spineIndex self.chapterIndex = chapterIndex self.pageIndexInChapter = pageIndexInChapter self.totalPagesInChapter = totalPagesInChapter self.chapterTitle = chapterTitle self.fixedSpread = fixedSpread } } /// 固定版式 Spread 中的单个资源 public struct EPUBFixedSpreadResource: Codable, Equatable { /// 对应的 spine 索引 public var spineIndex: Int /// 资源路径 public var href: String /// 资源标题 public var title: String /// 在 spread 中的位置偏好 public var pageSpread: RDEPUBPageSpread? public init(spineIndex: Int, href: String, title: String, pageSpread: RDEPUBPageSpread? = nil) { self.spineIndex = spineIndex self.href = href self.title = title self.pageSpread = pageSpread } } /// 固定版式 Spread(双页展开)模型 /// 将 1-2 个 spine 资源组合为一个显示单元 public struct EPUBFixedSpread: Codable, Equatable { /// 该 spread 包含的资源列表(通常 1-2 个) public var resources: [EPUBFixedSpreadResource] public init(resources: [EPUBFixedSpreadResource]) { self.resources = resources } /// 主资源(列表中的第一个),用于标识和定位 public var primaryResource: EPUBFixedSpreadResource { resources.first ?? EPUBFixedSpreadResource(spineIndex: 0, href: "", title: "") } /// 判断指定 href 的资源是否属于此 spread /// - Parameters: /// - normalizedHref: 标准化后的 href /// - normalizer: href 标准化函数 /// - Returns: 是否包含该资源 public func contains(normalizedHref: String, normalizer: (String) -> String?) -> Bool { resources.contains { resource in normalizer(resource.href) == normalizedHref } } /// 根据 spine 列表和 spread 设置生成 Spread 数组 /// 根据 pageSpread 属性(left/right/center)决定资源配对 /// - Parameters: /// - spine: spine 项列表 /// - spreadEnabled: 是否启用 spread 模式 /// - Returns: spread 数组 public static func makeSpreads(spine: [RDEPUBSpineItem], spreadEnabled: Bool) -> [EPUBFixedSpread] { let resources = spine.enumerated().map { index, item in EPUBFixedSpreadResource( spineIndex: index, href: item.href, title: item.title, pageSpread: item.pageSpread ) } guard spreadEnabled, resources.count > 1 else { return resources.map { EPUBFixedSpread(resources: [$0]) } } var spreads: [EPUBFixedSpread] = [] var cursor = 0 while cursor < resources.count { let current = resources[cursor] guard cursor + 1 < resources.count else { spreads.append(EPUBFixedSpread(resources: [current])) break } let next = resources[cursor + 1] if shouldPair(current: current, next: next) { spreads.append(EPUBFixedSpread(resources: [current, next])) cursor += 2 } else { spreads.append(EPUBFixedSpread(resources: [current])) cursor += 1 } } return spreads } /// 便捷方法:从 parser 构建 spread public static func makeSpreads(parser: RDEPUBParser, spreadEnabled: Bool) -> [EPUBFixedSpread] { makeSpreads(spine: parser.spine, spreadEnabled: spreadEnabled) } /// 判断两个资源是否应该配对为一个 spread /// center 类型不参与配对,left+right 或 right+left 配对 private static func shouldPair(current: EPUBFixedSpreadResource, next: EPUBFixedSpreadResource) -> Bool { if current.pageSpread == .center || next.pageSpread == .center { return false } switch (current.pageSpread, next.pageSpread) { case (.left, .right), (.right, .left): return true case (.left, .left), (.right, .right): return false default: return true } } }