import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { markdownToSearchText, searchBook, type BookChapterIndex } from "./bookSearch.ts"; describe("markdownToSearchText", () => { it("条目卡指令保留值、剥离标记符号", () => { const md = `[entries] ## 安全气囊 @tags 汽车 | 安全 | 主:被动 @summary 气囊需配合安全带使用。 备注 :: 详见召回公告 @source NHTSA 报告 | https://a.gov/x [/entries]`; const text = markdownToSearchText(md); assert.ok(text.includes("安全气囊")); assert.ok(text.includes("汽车")); assert.ok(text.includes("主:被动")); assert.ok(text.includes("气囊需配合安全带使用。")); assert.ok(text.includes("备注 详见召回公告")); assert.ok(text.includes("NHTSA 报告")); assert.ok(!text.includes("@tags")); assert.ok(!text.includes("@summary")); assert.ok(!text.includes("@source")); assert.ok(!text.includes("::")); assert.ok(!text.includes("[entries")); assert.ok(!text.includes("https://a.gov/x")); }); it("锁定的 hide 块整体不可搜,未锁块保留正文", () => { const md = `公开段落内容。 [hide login] 登录后可见的秘密 [/hide] [hide points=10 locked] 积分秘密内容 [/hide]`; const text = markdownToSearchText(md); assert.ok(text.includes("公开段落内容")); assert.ok(text.includes("登录后可见的秘密")); assert.ok(!text.includes("积分秘密内容")); }); it("剥离代码围栏与行内 markdown 语法但保留代码与锚文本", () => { const md = "见 [官网说明](https://x.com) 与 `const x = 1`,\n\n```ts\ncodeInside();\n```"; const text = markdownToSearchText(md); assert.ok(text.includes("官网说明")); assert.ok(!text.includes("https://x.com")); assert.ok(text.includes("const x = 1")); assert.ok(text.includes("codeInside();")); assert.ok(!text.includes("```")); }); }); describe("searchBook", () => { const chapters: BookChapterIndex[] = [ { no: 1, title: "第一章", plain: "今天天气很好,适合出门散步。路边有一朵花。" }, { no: 2, title: "第二章", plain: "第二章没有关键词。" }, { no: 3, title: "第三章", plain: "再次提到天气,天气真的不错。" }, ]; it("按章聚合命中数与片段,大小写不敏感", () => { const hits = searchBook(chapters, "天气"); assert.equal(hits.length, 2); assert.deepEqual( hits.map((h) => [h.no, h.count]), [ [1, 1], [3, 2], ] ); assert.equal(hits[1].snippets[0].match, "天气"); assert.ok(hits[1].snippets[0].after.includes(",天气真的")); }); it("片段最多生成 SNIPPET_LIMIT 个,但 count 为真实总数", () => { const cs: BookChapterIndex[] = [{ no: 1, title: "t", plain: "天气 天气 天气 天气" }]; const hits = searchBook(cs, "天气"); assert.equal(hits[0].count, 4); assert.equal(hits[0].snippets.length, 3); assert.deepEqual(hits[0].snippets.map((s) => s.ordinal), [0, 1, 2]); }); it("空关键词返回空", () => { assert.deepEqual(searchBook(chapters, " "), []); }); it("无命中返回空数组", () => { assert.deepEqual(searchBook(chapters, "航天飞机"), []); }); });