// RDEPUBResourceResolver.swift // EPUB 资源解析器 // 封装 RDEPUBParser 的资源查询能力,提供 href 规范化、 // spine 索引查找、位置规范化等高层接口。是 Services 层的核心组件, // 被 RDEPUBPublication 和 RDEPUBReadingSession 广泛使用。 import Foundation /// EPUB 资源解析器,负责 URL 转换、href 规范化和位置解析 public final class RDEPUBResourceResolver { /// 底层解析器引用 private let parser: RDEPUBParser public init(parser: RDEPUBParser) { self.parser = parser } /// OPF 文件所在目录的 URL public var opfDirectoryURL: URL? { parser.opfDirectoryURL } /// 将相对路径转换为本地文件 URL public func fileURL(forRelativePath relativePath: String) -> URL? { parser.fileURL(forRelativePath: relativePath) } /// 将相对路径转换为 ss-reader://book/ 协议 URL public func resourceURL(forRelativePath relativePath: String) -> URL? { parser.resourceURL(forRelativePath: relativePath) } /// 将 ss-reader://book/ 协议 URL 还原为本地文件 URL public func fileURL(forResourceURL resourceURL: URL) -> URL? { parser.fileURL(forResourceURL: resourceURL) } /// 规范化 href:基于 OPF 目录和可选的 spine 索引,将相对路径解析为标准化的 OPF 内路径 public func normalizedHref(_ href: String, relativeToSpineIndex spineIndex: Int? = nil) -> String? { guard let opfDirectoryURL else { return href.components(separatedBy: "#").first } let pathPart = href.components(separatedBy: "#").first ?? href if pathPart.isEmpty { guard let spineIndex, parser.spine.indices.contains(spineIndex) else { return nil } return parser.spine[spineIndex].href.components(separatedBy: "#").first } let baseURL: URL if let spineIndex, parser.spine.indices.contains(spineIndex) { baseURL = opfDirectoryURL .appendingPathComponent(parser.spine[spineIndex].href) .deletingLastPathComponent() } else { baseURL = opfDirectoryURL } guard let resolvedURL = URL(string: pathPart, relativeTo: baseURL)?.standardizedFileURL else { return pathPart } let opfPath = opfDirectoryURL.standardizedFileURL.path + "/" let resolvedPath = resolvedURL.path if resolvedPath.hasPrefix(opfPath) { return String(resolvedPath.dropFirst(opfPath.count)) } return pathPart } /// 规范化 href:基于另一个 href 作为基准路径 public func normalizedHref(_ href: String, relativeToHref baseHref: String) -> String? { guard let opfDirectoryURL else { return href.components(separatedBy: "#").first } let pathPart = href.components(separatedBy: "#").first ?? href let basePath = baseHref.components(separatedBy: "#").first ?? baseHref let baseURL = opfDirectoryURL .appendingPathComponent(basePath) .deletingLastPathComponent() guard let resolvedURL = URL(string: pathPart, relativeTo: baseURL)?.standardizedFileURL else { return pathPart } let opfPath = opfDirectoryURL.standardizedFileURL.path + "/" if resolvedURL.path.hasPrefix(opfPath) { return String(resolvedURL.path.dropFirst(opfPath.count)) } return pathPart } /// 将相对于另一个 href 的引用转换为本地文件 URL public func fileURL(forReference href: String, relativeToHref baseHref: String) -> URL? { guard let normalizedHref = normalizedHref(href, relativeToHref: baseHref) else { return nil } return fileURL(forRelativePath: normalizedHref) } /// 规范化位置对象:解析 href、保留 progression 和 fragment public func normalizedLocation( _ location: RDEPUBLocation, relativeToSpineIndex spineIndex: Int? = nil, bookIdentifier: String? ) -> RDEPUBLocation? { let rawHref = location.href let hrefParts = rawHref.split(separator: "#", maxSplits: 1, omittingEmptySubsequences: false) let pathPart = hrefParts.first.map(String.init) ?? rawHref let fragment = location.fragment ?? (hrefParts.count > 1 ? String(hrefParts[1]) : nil) let normalizedTargetHref: String if pathPart.isEmpty { guard let spineIndex, parser.spine.indices.contains(spineIndex) else { return nil } normalizedTargetHref = parser.spine[spineIndex].href.components(separatedBy: "#").first ?? parser.spine[spineIndex].href } else { guard let href = normalizedHref(pathPart, relativeToSpineIndex: spineIndex) else { return nil } normalizedTargetHref = href } return RDEPUBLocation( bookIdentifier: bookIdentifier, href: normalizedTargetHref, progression: location.progression, lastProgression: location.lastProgression, fragment: fragment, rangeAnchor: location.rangeAnchor ) } /// 通过规范化 href 查找对应的 spine 索引 public func spineIndex(forNormalizedHref normalizedHref: String) -> Int? { parser.spine.firstIndex { item in self.normalizedHref(item.href) == normalizedHref } } /// 通过位置查找对应的 spine 索引 public func spineIndex(for location: RDEPUBLocation) -> Int? { guard let normalizedHref = normalizedHref(location.href) else { return nil } return spineIndex(forNormalizedHref: normalizedHref) } /// 通过 spine 索引获取 href public func href(forSpineIndex spineIndex: Int) -> String? { guard parser.spine.indices.contains(spineIndex) else { return nil } return parser.spine[spineIndex].href } /// 通过 spine 索引获取标题 public func title(forSpineIndex spineIndex: Int) -> String? { guard parser.spine.indices.contains(spineIndex) else { return nil } return parser.spine[spineIndex].title } /// 通过 href 查询 manifest 项 public func manifestItem(forHref href: String) -> RDEPUBManifestItem? { let targetHref = normalizedHref(href) ?? href.components(separatedBy: "#").first ?? href return parser.manifest.values.first { normalizedHref($0.href) == targetHref } } }