import { chapterText, type TxtChapter } from "./chapters"; export interface SearchHit { ch: number; /** 命中在章内文本的起点 */ pos: number; snippet: string; matchStart: number; // snippet 内 matchLen: number; } const CTX = 20; // snippet 前后文 /** 全书线性扫描(文本已分章在内存,量级足够;大小写不敏感,不折叠全半角)。 */ export function searchChapters(text: string, chapters: TxtChapter[], q: string, limit = 500): SearchHit[] { const needle = q.trim().toLowerCase(); if (!needle) return []; const bodies = chapters.map((_, ci) => chapterText(text, chapters, ci).toLowerCase()); const hits: SearchHit[] = []; for (let ci = 0; ci < chapters.length && hits.length < limit; ci++) { const body = bodies[ci]; let from = 0; for (;;) { const at = body.indexOf(needle, from); if (at < 0 || hits.length >= limit) break; const raw = chapterText(text, chapters, ci); const s = Math.max(0, at - CTX); const e = Math.min(raw.length, at + needle.length + CTX); hits.push({ ch: ci, pos: at, snippet: (s > 0 ? "…" : "") + raw.slice(s, e) + (e < raw.length ? "…" : ""), matchStart: at - s + (s > 0 ? 1 : 0), matchLen: needle.length, }); from = at + needle.length; } } return hits; } export function splitHighlight(snippet: string, matchStart: number, matchLen: number) { return { before: snippet.slice(0, matchStart), match: snippet.slice(matchStart, matchStart + matchLen), after: snippet.slice(matchStart + matchLen), }; }