// lib/editor/search/search_snippet.dart // // Pure snippet extraction for library-wide full-text search (F8 — the user's #1 // named differentiator). Given a source string (a PDF text page, a typed text // box, or OCR'd handwriting) and a query, produce a windowed excerpt centered on // the first match with the match offset preserved, so the results list can show // "…context **match** context…" and jump to the hit. // // The FTS index / ranking lives in the DB (search_indexer); THIS is the pure, // storage-free excerpt math, fully unit-tested. /// A windowed excerpt around a query match. class Snippet { const Snippet({ required this.text, required this.matchStart, required this.matchLength, required this.truncatedStart, required this.truncatedEnd, }); /// The excerpt (a substring of the source). final String text; /// Offset of the match WITHIN [text]. final int matchStart; /// Length of the matched run. final int matchLength; /// True when [text] begins before the source start was reached (show a /// leading ellipsis). final bool truncatedStart; /// True when [text] ends before the source end (show a trailing ellipsis). final bool truncatedEnd; /// Convenience: the matched substring. String get match => text.substring(matchStart, matchStart + matchLength); @override String toString() => '${truncatedStart ? '…' : ''}$text${truncatedEnd ? '…' : ''}' ' [match @$matchStart+$matchLength]'; } /// First case-insensitive match of [query] in [source], as a snippet of up to /// roughly [window] characters centered on the match. Returns null when [query] /// is empty or absent. The full match is always included even if longer than /// [window]. Snippet? snippetFor(String source, String query, {int window = 80}) { if (query.isEmpty || source.isEmpty) return null; assert(window >= 0); final matchIndex = source.toLowerCase().indexOf(query.toLowerCase()); if (matchIndex < 0) return null; final matchLen = query.length; final contextEach = ((window - matchLen) ~/ 2).clamp(0, window); var start = matchIndex - contextEach; if (start < 0) start = 0; var end = matchIndex + matchLen + contextEach; if (end > source.length) end = source.length; return Snippet( text: source.substring(start, end), matchStart: matchIndex - start, matchLength: matchLen, truncatedStart: start > 0, truncatedEnd: end < source.length, ); }