ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextSearchEngine.swift
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

129 lines
5.8 KiB
Swift

import Foundation
final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
private let textBook: RDEPUBTextBook
private let publication: RDEPUBPublication
init(textBook: RDEPUBTextBook, publication: RDEPUBPublication) {
self.textBook = textBook
self.publication = publication
}
func search(keyword: String) -> [RDEPUBSearchMatch] {
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedKeyword.isEmpty else {
return []
}
var matches: [RDEPUBSearchMatch] = []
for chapter in textBook.chapters {
guard let chapterData = textBook.chapterData(for: chapter.href) else { continue }
let source = chapter.attributedContent.string as NSString
let fullLength = source.length
guard fullLength > 0 else {
continue
}
let normalizedHref = publication.resourceResolver.normalizedHref(chapter.href) ?? chapter.href
var localMatchIndex = 0
var searchRange = NSRange(location: 0, length: fullLength)
while searchRange.length > 0 {
let foundRange = source.range(of: normalizedKeyword, options: [.caseInsensitive], range: searchRange)
guard foundRange.location != NSNotFound else {
break
}
let progressionDenominator = max(fullLength - 1, 1)
let progression = Double(foundRange.location) / Double(progressionDenominator)
let rangeAnchor = chapterData.rangeAnchor(for: foundRange)
matches.append(
RDEPUBSearchMatch(
href: normalizedHref,
progression: progression,
previewText: previewText(in: source, matchRange: foundRange),
localMatchIndex: localMatchIndex,
rangeLocation: foundRange.location,
rangeLength: foundRange.length,
rangeAnchor: rangeAnchor,
cfi: chapterData.indexTable.cfi(for: rangeAnchor.start)?.rawValue,
rangeCFI: chapterData.indexTable.cfiRange(for: rangeAnchor)?.rawValue
)
)
localMatchIndex += 1
let nextLocation = foundRange.location + max(foundRange.length, 1)
if nextLocation >= fullLength {
break
}
searchRange = NSRange(location: nextLocation, length: fullLength - nextLocation)
}
}
return matches
}
private func previewText(in text: NSString, matchRange: NSRange) -> String {
let previewRadius = 12
let start = max(matchRange.location - previewRadius, 0)
let end = min(matchRange.location + matchRange.length + previewRadius, text.length)
let range = NSRange(location: start, length: max(end - start, 0))
return text.substring(with: range).trimmingCharacters(in: .whitespacesAndNewlines)
}
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 }
var localMatchIndex = 0
var searchRange = NSRange(location: 0, length: fullLength)
while searchRange.length > 0 {
let foundRange = source.range(of: normalizedKeyword, options: [.caseInsensitive], range: searchRange)
guard foundRange.location != NSNotFound else { break }
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)
let previewRange = NSRange(location: previewStart, length: max(previewEnd - previewStart, 0))
let previewText = source.substring(with: previewRange).trimmingCharacters(in: .whitespacesAndNewlines)
matches.append(
RDEPUBSearchMatch(
href: chapter.href,
progression: progression,
previewText: previewText,
localMatchIndex: localMatchIndex,
rangeLocation: foundRange.location,
rangeLength: foundRange.length,
rangeAnchor: rangeAnchor,
cfi: cfi?.rawValue ?? cfiRange?.start.rawValue,
rangeCFI: cfiRange?.rawValue
)
)
localMatchIndex += 1
let nextLocation = foundRange.location + max(foundRange.length, 1)
if nextLocation >= fullLength { break }
searchRange = NSRange(location: nextLocation, length: fullLength - nextLocation)
}
}
return matches
}
}