feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化

- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
This commit is contained in:
shenlei
2026-06-22 20:26:34 +08:00
parent f50495ad91
commit c65c190b71
178 changed files with 11380 additions and 6728 deletions
@@ -1,18 +1,8 @@
// RDEPUBParser+Package.swift
// OPFPackage Document Spine
// OPFPackageParserDelegateSAX metadata/manifest/spine
// RDEPUBSpineItem rendition:layoutpage-spread
// XMLNameXMLSectionXMLParserContext XML
import Foundation
extension RDEPUBParser {
/// OPF spine
/// manifest hreffixed/reflowablepage-spread
/// - Parameters:
/// - packageDocument: OPF metadatamanifestspineReferences
/// - 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 metadatamanifestspine 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-paginatedpage-spread-left
var properties: [String]
/// //
var pageSpread: RDEPUBPageSpread?
}
/// OPF SAX
/// metadataidentifier/title/creator/language/rendition:layout
/// manifestitem spineitemref
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:layoutrendition:spreadidentifier
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)
}