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) matches.append( RDEPUBSearchMatch( href: normalizedHref, progression: progression, previewText: previewText(in: source, matchRange: foundRange), localMatchIndex: localMatchIndex, rangeLocation: foundRange.location, rangeLength: foundRange.length, rangeAnchor: chapterData.rangeAnchor(for: foundRange) ) ) 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) } }