- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
110 lines
4.5 KiB
Swift
110 lines
4.5 KiB
Swift
// RDEPUBParser+Resources.swift
|
||
// EPUB 资源查询与 URL 解析
|
||
// 提供按 ID、href 查询 manifest 项,spine 索引与 href 互转,
|
||
// 文件路径与 ss-reader:// 协议 URL 互转,封面图提取,HTML 内容读取等能力。
|
||
|
||
import Foundation
|
||
import UIKit
|
||
|
||
extension RDEPUBParser {
|
||
/// 按 ID 查询 manifest 项
|
||
public func manifestItem(for id: String) -> RDEPUBManifestItem? {
|
||
manifest[id]
|
||
}
|
||
|
||
/// 按 href 查询 manifest 项(忽略 fragment)
|
||
public func manifestItem(forHref href: String) -> RDEPUBManifestItem? {
|
||
let target = href.components(separatedBy: "#").first ?? href
|
||
return manifest.values.first { item in
|
||
normalize(href: item.href, relativeTo: opfDirectoryURL ?? URL(fileURLWithPath: "/")) == target
|
||
}
|
||
}
|
||
|
||
/// 通过 spine 索引获取 href
|
||
public func href(forSpineIndex index: Int) -> String? {
|
||
guard spine.indices.contains(index) else { return nil }
|
||
return spine[index].href
|
||
}
|
||
|
||
/// 将相对路径转换为本地文件 URL,并校验是否在解压根目录内(防路径遍历攻击)
|
||
public func fileURL(forRelativePath relativePath: String) -> URL? {
|
||
guard let opfDirectoryURL else { return nil }
|
||
let path = relativePath.components(separatedBy: "#").first ?? relativePath
|
||
guard !path.isEmpty else { return nil }
|
||
let resolvedURL = URL(fileURLWithPath: path, relativeTo: opfDirectoryURL).standardizedFileURL
|
||
guard let rootURL = extractionRootURL?.standardizedFileURL else {
|
||
return resolvedURL
|
||
}
|
||
guard resolvedURL.path.hasPrefix(rootURL.path) else {
|
||
return nil
|
||
}
|
||
return resolvedURL
|
||
}
|
||
|
||
/// 将相对路径转换为 ss-reader://book/ 协议的 URL(用于 WebView 加载资源)
|
||
public func resourceURL(forRelativePath relativePath: String) -> URL? {
|
||
let normalizedPath = relativePath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||
guard !normalizedPath.isEmpty else { return nil }
|
||
guard let encodedPath = normalizedPath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
||
return nil
|
||
}
|
||
var components = URLComponents()
|
||
components.scheme = RDEPUBResourceURLSchemeHandler.scheme
|
||
components.host = RDEPUBResourceURLSchemeHandler.host
|
||
components.percentEncodedPath = "/" + encodedPath
|
||
return components.url
|
||
}
|
||
|
||
/// 将 ss-reader://book/ 协议的 URL 还原为本地文件 URL
|
||
public func fileURL(forResourceURL resourceURL: URL) -> URL? {
|
||
guard resourceURL.scheme == RDEPUBResourceURLSchemeHandler.scheme,
|
||
resourceURL.host == RDEPUBResourceURLSchemeHandler.host else {
|
||
return nil
|
||
}
|
||
let relativePath = resourceURL.path.removingPercentEncoding?.trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
|
||
return fileURL(forRelativePath: relativePath)
|
||
}
|
||
|
||
/// 提取封面图片(优先查找含 "cover-image" 属性或 ID 含 "cover" 的 manifest 项)
|
||
public func coverImage() -> UIImage? {
|
||
let coverCandidates = manifest.values.filter { item in
|
||
item.properties.contains("cover-image") || item.id.lowercased().contains("cover")
|
||
}
|
||
for item in coverCandidates {
|
||
if let fileURL = fileURL(forRelativePath: item.href),
|
||
let image = UIImage(contentsOfFile: fileURL.path) {
|
||
return image
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// 通过 spine 索引读取 HTML 内容
|
||
public func htmlString(forSpineIndex index: Int) -> String? {
|
||
guard spine.indices.contains(index) else {
|
||
return nil
|
||
}
|
||
return htmlString(forRelativePath: spine[index].href)
|
||
}
|
||
|
||
/// 通过相对路径读取 HTML 内容
|
||
public func htmlString(forRelativePath relativePath: String) -> String? {
|
||
guard let fileURL = fileURL(forRelativePath: relativePath) else {
|
||
return nil
|
||
}
|
||
return try? String(contentsOf: fileURL)
|
||
}
|
||
|
||
/// 将 href 相对于目录 URL 解析为标准化的相对路径
|
||
func normalize(href: String, relativeTo directoryURL: URL) -> String {
|
||
guard let resolvedURL = URL(string: href, relativeTo: directoryURL)?.standardizedFileURL else {
|
||
return href
|
||
}
|
||
let prefix = directoryURL.standardizedFileURL.path + "/"
|
||
if resolvedURL.path.hasPrefix(prefix) {
|
||
return String(resolvedURL.path.dropFirst(prefix.count))
|
||
}
|
||
return href
|
||
}
|
||
}
|