import Foundation public enum RDEPUBLayout: String, Codable { case reflowable case fixed } public enum RDEPUBReadingProfile: String, Codable { case webInteractive case webFixedLayout case textReflowable } public enum RDEPUBReadingProgression: String, Codable { case ltr case rtl case auto } public enum RDEPUBPageSpread: String, Codable { case left case right case center } /// EPUB 渲染朝向(rendition:orientation / iBooks display-options)。 public enum RDEPUBOrientation: String, Codable { case portrait case landscape case auto } public struct RDEPUBMetadata: Codable, Equatable { public var identifier: String? public var title: String public var author: String? public var language: String? public var version: String? public var layout: RDEPUBLayout public var spread: String? public var readingProgression: RDEPUBReadingProgression /// 渲染朝向;nil = 未声明(不锁定,跟随系统)。 /// 来源优先级:OPF rendition:orientation > META-INF/com.apple.ibooks.display-options.xml。 public var orientation: RDEPUBOrientation? public init( identifier: String? = nil, title: String = "", author: String? = nil, language: String? = nil, version: String? = nil, layout: RDEPUBLayout = .reflowable, spread: String? = nil, readingProgression: RDEPUBReadingProgression = .auto, orientation: RDEPUBOrientation? = nil ) { self.identifier = identifier self.title = title self.author = author self.language = language self.version = version self.layout = layout self.spread = spread self.readingProgression = readingProgression self.orientation = orientation } } public struct RDEPUBManifestItem: Codable, Equatable { public var id: String public var href: String public var mediaType: String public var properties: [String] public var fallback: String? public var mediaOverlay: String? public var title: String? public init( id: String, href: String, mediaType: String, properties: [String] = [], fallback: String? = nil, mediaOverlay: String? = nil, title: String? = nil ) { self.id = id self.href = href self.mediaType = mediaType self.properties = properties self.fallback = fallback self.mediaOverlay = mediaOverlay self.title = title } public var isNavigationDocument: Bool { properties.contains("nav") } public var isNCX: Bool { mediaType == "application/x-dtbncx+xml" } } public struct RDEPUBSpineItem: Codable, Equatable { public var idref: String public var href: String public var mediaType: String public var title: String public var linear: Bool public var properties: [String] public var pageSpread: RDEPUBPageSpread? public var layout: RDEPUBLayout? public init( idref: String, href: String, mediaType: String, title: String, linear: Bool = true, properties: [String] = [], pageSpread: RDEPUBPageSpread? = nil, layout: RDEPUBLayout? = nil ) { self.idref = idref self.href = href self.mediaType = mediaType self.title = title self.linear = linear self.properties = properties self.pageSpread = pageSpread self.layout = layout } } public struct EPUBTableOfContentsItem: Codable, Equatable { public var title: String public var href: String public var children: [EPUBTableOfContentsItem] public init(title: String, href: String, children: [EPUBTableOfContentsItem] = []) { self.title = title self.href = href self.children = children } } public enum RDEPUBParserError: LocalizedError { case archiveOpenFailed(URL) case missingContainerXML case missingRootFile case invalidRootFilePath(String) case invalidXML(URL) case missingManifestItem(idref: String) case emptySpine case invalidArchiveEntryPath(String) public var errorDescription: String? { switch self { case .archiveOpenFailed(let url): return "无法打开 EPUB 压缩包: \(url.lastPathComponent)" case .missingContainerXML: return "EPUB 缺少 META-INF/container.xml" case .missingRootFile: return "container.xml 中未找到 rootfile" case .invalidRootFilePath(let path): return "OPF 路径无效: \(path)" case .invalidXML(let url): return "XML 解析失败: \(url.lastPathComponent)" case .missingManifestItem(let idref): return "spine itemref 找不到对应 manifest 项: \(idref)" case .emptySpine: return "OPF spine 为空" case .invalidArchiveEntryPath(let path): return "归档条目路径非法: \(path)" } } }