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 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 ) } 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 } } }