- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
129 lines
5.8 KiB
Swift
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
|
|
}
|
|
}
|