- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
177 lines
6.5 KiB
Swift
177 lines
6.5 KiB
Swift
// 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
|
|
}
|
|
}
|
|
}
|