feat(frontend): in-book text search core (TDD)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user