feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,18 +1,8 @@
|
||||
// RDEPUBParser+Package.swift
|
||||
// OPF(Package Document)解析与 Spine 构建
|
||||
// 包含 OPFPackageParserDelegate(SAX 解析 metadata/manifest/spine),
|
||||
// 从解析结果构建 RDEPUBSpineItem 数组,处理 rendition:layout、page-spread 等属性。
|
||||
// 同时定义 XMLName、XMLSection、XMLParserContext 等 XML 解析辅助工具。
|
||||
|
||||
import Foundation
|
||||
|
||||
extension RDEPUBParser {
|
||||
/// 从 OPF 解析结果构建 spine 阅读顺序数组
|
||||
/// 逐项匹配 manifest,解析 href、布局模式(fixed/reflowable)、page-spread 等属性
|
||||
/// - Parameters:
|
||||
/// - packageDocument: OPF 解析结果(含 metadata、manifest、spineReferences)
|
||||
/// - opfURL: OPF 文件路径
|
||||
/// - Returns: spine 项数组
|
||||
|
||||
func buildSpine(from packageDocument: OPFPackageDocument, opfURL: URL) throws -> [RDEPUBSpineItem] {
|
||||
let opfDirectoryURL = opfURL.deletingLastPathComponent()
|
||||
let publicationLayout = packageDocument.metadata.layout
|
||||
@@ -62,47 +52,52 @@ extension RDEPUBParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// OPF 包文档的解析结果,包含 metadata、manifest、spine 引用及 NCX/nav 项
|
||||
struct OPFPackageDocument {
|
||||
var metadata: RDEPUBMetadata
|
||||
var manifest: [RDEPUBManifestItem]
|
||||
var manifestByID: [String: RDEPUBManifestItem]
|
||||
var spineReferences: [OPFSpineReference]
|
||||
/// NCX 目录文件的 manifest 项(EPUB 2)
|
||||
|
||||
var ncxItem: RDEPUBManifestItem?
|
||||
/// Navigation Document 的 manifest 项(EPUB 3)
|
||||
|
||||
var navigationItem: RDEPUBManifestItem?
|
||||
}
|
||||
|
||||
/// OPF 中 spine/itemref 元素的解析结果
|
||||
struct OPFSpineReference {
|
||||
/// 对应 manifest 中的 item id
|
||||
|
||||
var idref: String
|
||||
/// 是否为线性阅读项("no" 表示辅助内容)
|
||||
|
||||
var linear: Bool
|
||||
/// 属性列表(如 rendition:layout-pre-paginated、page-spread-left 等)
|
||||
|
||||
var properties: [String]
|
||||
/// 页面展开方向(左/右/居中)
|
||||
|
||||
var pageSpread: RDEPUBPageSpread?
|
||||
}
|
||||
|
||||
/// OPF 文档的 SAX 解析代理
|
||||
/// 解析 metadata(identifier/title/creator/language/rendition:layout 等)、
|
||||
/// manifest(item 元素)、spine(itemref 元素)三个主要区域
|
||||
final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
|
||||
|
||||
private var metadata = RDEPUBMetadata()
|
||||
|
||||
private var manifestItems: [RDEPUBManifestItem] = []
|
||||
|
||||
private var spineReferences: [OPFSpineReference] = []
|
||||
|
||||
private let xmlContext = XMLParserContext()
|
||||
|
||||
private var uniqueIdentifierID: String?
|
||||
|
||||
private var currentIdentifierElementID: String?
|
||||
|
||||
private var currentMetaProperty: String?
|
||||
|
||||
private var identifierByID: [String: String] = [:]
|
||||
|
||||
private var manifestTitleByID: [String: String] = [:]
|
||||
|
||||
private var currentMetaRefinesID: String?
|
||||
|
||||
private var ncxID: String?
|
||||
|
||||
/// 将解析中间结果组装为 OPFPackageDocument 结构体
|
||||
func packageDocument() -> OPFPackageDocument {
|
||||
if metadata.identifier == nil, let fallbackIdentifier = identifierByID.values.first {
|
||||
metadata.identifier = fallbackIdentifier
|
||||
@@ -251,7 +246,6 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
|
||||
xmlContext.endElement()
|
||||
}
|
||||
|
||||
/// 当前解析所在的 OPF 区域(metadata/manifest/spine/other)
|
||||
private var currentSection: XMLSection {
|
||||
if xmlContext.containsElement(named: "manifest") {
|
||||
return .manifest
|
||||
@@ -265,7 +259,6 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
|
||||
return .other
|
||||
}
|
||||
|
||||
/// 处理 <meta> 元素的文本内容,提取 rendition:layout、rendition:spread、identifier 等
|
||||
private func applyMetaValue(_ value: String) {
|
||||
let normalizedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedValue.isEmpty, let property = currentMetaProperty?.lowercased() else {
|
||||
@@ -290,7 +283,6 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从属性字典中提取 page-spread 信息(支持 properties 和 page-spread 两种写法)
|
||||
private func pageSpread(from attributes: [String: String]) -> RDEPUBPageSpread? {
|
||||
if let properties = attributes["properties"]?.lowercased() {
|
||||
if properties.contains("page-spread-left") {
|
||||
@@ -320,13 +312,11 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 将 rendition:layout 原始值转换为布局枚举
|
||||
private func layout(from rawValue: String) -> RDEPUBLayout {
|
||||
rawValue.lowercased().contains("pre-paginated") ? .fixed : .reflowable
|
||||
}
|
||||
}
|
||||
|
||||
/// OPF 文档的区域枚举,用于区分当前解析位置
|
||||
enum XMLSection {
|
||||
case metadata
|
||||
case manifest
|
||||
@@ -334,14 +324,12 @@ enum XMLSection {
|
||||
case other
|
||||
}
|
||||
|
||||
/// XML 名称解析工具:提取本地名、分词、提取 refines ID
|
||||
enum XMLName {
|
||||
/// 从带命名空间前缀的名称中提取本地名(如 "dc:title" -> "title")
|
||||
|
||||
static func localName(from rawName: String) -> String {
|
||||
rawName.split(separator: ":").last.map(String.init) ?? rawName
|
||||
}
|
||||
|
||||
/// 将空格分隔的属性值拆分为 token 数组
|
||||
static func tokenize(_ rawValue: String?) -> [String] {
|
||||
guard let rawValue else { return [] }
|
||||
return rawValue
|
||||
@@ -349,7 +337,6 @@ enum XMLName {
|
||||
.map(String.init)
|
||||
}
|
||||
|
||||
/// 从 refines 属性值中提取 ID(去掉 "#" 前缀)
|
||||
static func refinedID(from rawValue: String) -> String? {
|
||||
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard trimmed.hasPrefix("#") else {
|
||||
@@ -359,14 +346,12 @@ enum XMLName {
|
||||
}
|
||||
}
|
||||
|
||||
/// XML 解析上下文,维护元素栈和当前文本内容
|
||||
final class XMLParserContext {
|
||||
/// 元素名称栈,用于追踪嵌套层级
|
||||
|
||||
private var elementStack: [String] = []
|
||||
/// 当前元素累积的文本内容
|
||||
|
||||
private var currentCharacters = ""
|
||||
|
||||
/// 去除空白后的当前文本
|
||||
var trimmedCharacters: String {
|
||||
currentCharacters.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user