import Foundation public final class RDEPUBResourceResolver { private let parser: RDEPUBParser public init(parser: RDEPUBParser) { self.parser = parser } public var opfDirectoryURL: URL? { parser.opfDirectoryURL } public func fileURL(forRelativePath relativePath: String) -> URL? { parser.fileURL(forRelativePath: relativePath) } public func resourceURL(forRelativePath relativePath: String) -> URL? { parser.resourceURL(forRelativePath: relativePath) } public func fileURL(forResourceURL resourceURL: URL) -> URL? { parser.fileURL(forResourceURL: resourceURL) } /// 是否配置了加密资源数据提供者 public var hasResourceDataProvider: Bool { parser.resourceDataProvider != nil } /// 仅经 provider 获取解密数据;无 provider 或 provider 判定无需解密时返回 nil public func providedResourceData(at fileURL: URL) -> Data? { parser.providedResourceData(at: fileURL) } /// 统一资源读取入口:优先 provider 解密,其次磁盘明文 public func resourceData(at fileURL: URL) -> Data? { parser.resourceData(at: fileURL) } 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 } 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 } public func fileURL(forReference href: String, relativeToHref baseHref: String) -> URL? { guard let normalizedHref = normalizedHref(href, relativeToHref: baseHref) else { return nil } return fileURL(forRelativePath: normalizedHref) } 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, cfi: location.cfi, lastCFI: location.lastCFI, rangeCFI: location.rangeCFI ) } public func spineIndex(forNormalizedHref normalizedHref: String) -> Int? { parser.spine.firstIndex { item in self.normalizedHref(item.href) == normalizedHref } } public func spineIndex(for location: RDEPUBLocation) -> Int? { guard let normalizedHref = normalizedHref(location.href) else { return nil } return spineIndex(forNormalizedHref: normalizedHref) } public func href(forSpineIndex spineIndex: Int) -> String? { guard parser.spine.indices.contains(spineIndex) else { return nil } return parser.spine[spineIndex].href } public func title(forSpineIndex spineIndex: Int) -> String? { guard parser.spine.indices.contains(spineIndex) else { return nil } return parser.spine[spineIndex].title } 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 } } }