feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,10 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
/// EPUB 文本搜索引擎:在分页书籍中执行全文搜索。
|
||||
///
|
||||
/// 实现 `RDEPUBSearchEngine` 协议,遍历所有章节的富文本内容,
|
||||
/// 使用大小写不敏感匹配查找关键词,返回搜索结果列表。
|
||||
/// 每个搜索结果包含进度位置、预览文本和语义锚点。
|
||||
final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
private let textBook: RDEPUBTextBook
|
||||
private let publication: RDEPUBPublication
|
||||
@@ -14,14 +9,6 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
self.publication = publication
|
||||
}
|
||||
|
||||
/// 在全书中搜索关键词,返回所有匹配结果。
|
||||
///
|
||||
/// 搜索流程:
|
||||
/// 1. 遍历所有章节
|
||||
/// 2. 在每章的富文本字符串中执行大小写不敏感搜索
|
||||
/// 3. 计算匹配位置的阅读进度(progression)
|
||||
/// 4. 生成预览文本(匹配位置前后各 12 个字符)
|
||||
/// 5. 生成语义锚点(用于跨设备/跨字体精确定位)
|
||||
func search(keyword: String) -> [RDEPUBSearchMatch] {
|
||||
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedKeyword.isEmpty else {
|
||||
@@ -49,6 +36,7 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
|
||||
let progressionDenominator = max(fullLength - 1, 1)
|
||||
let progression = Double(foundRange.location) / Double(progressionDenominator)
|
||||
let rangeAnchor = chapterData.rangeAnchor(for: foundRange)
|
||||
matches.append(
|
||||
RDEPUBSearchMatch(
|
||||
href: normalizedHref,
|
||||
@@ -57,7 +45,9 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
localMatchIndex: localMatchIndex,
|
||||
rangeLocation: foundRange.location,
|
||||
rangeLength: foundRange.length,
|
||||
rangeAnchor: chapterData.rangeAnchor(for: foundRange)
|
||||
rangeAnchor: rangeAnchor,
|
||||
cfi: chapterData.indexTable.cfi(for: rangeAnchor.start)?.rawValue,
|
||||
rangeCFI: chapterData.indexTable.cfiRange(for: rangeAnchor)?.rawValue
|
||||
)
|
||||
)
|
||||
|
||||
@@ -73,7 +63,6 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
return matches
|
||||
}
|
||||
|
||||
/// 生成搜索结果的预览文本:匹配位置前后各取 12 个字符
|
||||
private func previewText(in text: NSString, matchRange: NSRange) -> String {
|
||||
let previewRadius = 12
|
||||
let start = max(matchRange.location - previewRadius, 0)
|
||||
@@ -82,13 +71,16 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
return text.substring(with: range).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
/// 无 publication 时的搜索(用于 txt 等外部文本文件)
|
||||
static func searchWithoutPublication(textBook: RDEPUBTextBook, keyword: String) -> [RDEPUBSearchMatch] {
|
||||
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedKeyword.isEmpty else { return [] }
|
||||
|
||||
var matches: [RDEPUBSearchMatch] = []
|
||||
for chapter in textBook.chapters {
|
||||
let chapterData = RDEPUBChapterData(
|
||||
chapter: chapter,
|
||||
indexTable: RDEPUBTextIndexTable(chapters: [chapter])
|
||||
)
|
||||
let source = chapter.attributedContent.string as NSString
|
||||
let fullLength = source.length
|
||||
guard fullLength > 0 else { continue }
|
||||
@@ -102,6 +94,9 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
|
||||
let progressionDenominator = max(fullLength - 1, 1)
|
||||
let progression = Double(foundRange.location) / Double(progressionDenominator)
|
||||
let rangeAnchor = chapterData.rangeAnchor(for: foundRange)
|
||||
let cfi = chapterData.indexTable.cfi(for: rangeAnchor.start)
|
||||
let cfiRange = chapterData.indexTable.cfiRange(for: rangeAnchor)
|
||||
let previewRadius = 12
|
||||
let previewStart = max(foundRange.location - previewRadius, 0)
|
||||
let previewEnd = min(foundRange.location + foundRange.length + previewRadius, fullLength)
|
||||
@@ -116,7 +111,9 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
localMatchIndex: localMatchIndex,
|
||||
rangeLocation: foundRange.location,
|
||||
rangeLength: foundRange.length,
|
||||
rangeAnchor: nil
|
||||
rangeAnchor: rangeAnchor,
|
||||
cfi: cfi?.rawValue ?? cfiRange?.start.rawValue,
|
||||
rangeCFI: cfiRange?.rawValue
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user