// RDEPUBParser.swift // EPUB 解析器入口 // 解析链路:epubURL → ZIP 解压 → container.xml → OPF → spine/TOC // 是 EPUBCore Publication 层的核心,解析完成后通过 makePublication() 生成 RDEPUBPublication。 // 具体解析逻辑分散在 +Archive、+Package、+TOC、+Resources、+ReadingProfile 扩展中。 import Foundation /// EPUB 解析器,执行从 epub 文件到出版物数据的完整解析流程 /// 主链路:epubURL → extractArchiveIfNeeded → container.xml → OPF → metadata/manifest/spine/TOC public final class RDEPUBParser { /// 出版物元数据(标题、作者、语言、布局模式等) public internal(set) var metadata = RDEPUBMetadata() /// 资源清单字典(key 为 manifest item ID) public internal(set) var manifest: [String: RDEPUBManifestItem] = [:] /// 阅读顺序列表(按 spine 排列的资源项) public internal(set) var spine: [RDEPUBSpineItem] = [] /// 目录树(从 NCX 或 Nav Document 解析得到) public internal(set) var tableOfContents: [EPUBTableOfContentsItem] = [] /// EPUB 解压后的根目录路径 public internal(set) var extractionRootURL: URL? /// OPF 文件的完整路径 public internal(set) var opfURL: URL? /// OPF 文件所在目录的 URL(用于解析相对路径) public var opfDirectoryURL: URL? { opfURL?.deletingLastPathComponent() } public init() {} /// 基于当前解析结果创建 RDEPUBPublication(门面对象) public func makePublication() -> RDEPUBPublication { RDEPUBPublication(parser: self) } /// 完整解析 EPUB 文件:解压 → container.xml → OPF /// - Parameter epubURL: EPUB 文件的本地路径 public func parse(epubURL: URL) throws { reset() let extractionURL = try extractArchiveIfNeeded(epubURL: epubURL) extractionRootURL = extractionURL let containerURL = extractionURL.appendingPathComponent("META-INF/container.xml") guard FileManager.default.fileExists(atPath: containerURL.path) else { throw RDEPUBParserError.missingContainerXML } let rootFilePath = try parseContainerRootFile(at: containerURL) let packageURL = extractionURL.appendingPathComponent(rootFilePath) guard FileManager.default.fileExists(atPath: packageURL.path) else { throw RDEPUBParserError.invalidRootFilePath(rootFilePath) } try parseOPF(at: packageURL) } /// 解析 OPF 文件:metadata/manifest/spine/TOC /// - Parameter opfURL: OPF 文件路径 public func parseOPF(at opfURL: URL) throws { resetPublicationState() guard let parser = XMLParser(contentsOf: opfURL) else { throw RDEPUBParserError.invalidXML(opfURL) } let delegate = OPFPackageParserDelegate() parser.shouldProcessNamespaces = false parser.delegate = delegate guard parser.parse() else { throw parser.parserError ?? RDEPUBParserError.invalidXML(opfURL) } let packageDocument = delegate.packageDocument() self.opfURL = opfURL self.metadata = packageDocument.metadata self.manifest = Dictionary(uniqueKeysWithValues: packageDocument.manifest.map { ($0.id, $0) }) self.spine = try buildSpine(from: packageDocument, opfURL: opfURL) self.tableOfContents = parseTOC(from: packageDocument, opfURL: opfURL) } /// 获取已解析的目录 public func parseTOC() -> [EPUBTableOfContentsItem] { tableOfContents } /// 解析指定的 Navigation Document 文件 public func parseNavDocument(_ navURL: URL, baseURL: URL? = nil) -> [EPUBTableOfContentsItem] { parseNavDocumentItems(at: navURL, baseURL: baseURL ?? navURL.deletingLastPathComponent()) } }