import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { parseEntriesBody, parseEntrySource, serializeEntriesBlock, wrapImplicitEntries, } from "./entryBlocks.ts"; import { splitContentSegments } from "./directiveBlocks.ts"; import { buildTocFromSegments, extractToc } from "./toc.ts"; describe("parseEntrySource", () => { it("最后一个 | 后为 http(s) 时拆分文本与链接", () => { assert.deepEqual(parseEntrySource("NHTSA (2024). Report | https://a.gov/x"), { text: "NHTSA (2024). Report", url: "https://a.gov/x", }); }); it("无链接或非 http 后缀时整行作为文本", () => { assert.deepEqual(parseEntrySource("WHO 口径"), { text: "WHO 口径" }); assert.deepEqual(parseEntrySource("标题 | 备注说明"), { text: "标题 | 备注说明" }); }); it("兼容全角竖线", () => { assert.deepEqual(parseEntrySource("来源|https://a.gov/x"), { text: "来源", url: "https://a.gov/x", }); }); }); describe("parseEntriesBody", () => { it("完整条目:标题/标签/摘要/字段/来源", () => { const items = parseEntriesBody( [ "## 系安全带,前排后排都系", "@tags 性价比:极高 | 证据:A级 | 不花钱", "@summary 坐前排系上安全带,概率大约降一半。", "成本 :: 不花钱。每次上车多花 2 秒。", "收益 :: NHTSA 估计致命伤风险下降 45%。", "@source NHTSA (2024). Report | https://a.gov/x", "@source WHO (2025). Fact sheet", ].join("\n") ); assert.equal(items.length, 1); assert.deepEqual(items[0], { title: "系安全带,前排后排都系", tags: ["性价比:极高", "证据:A级", "不花钱"], summary: "坐前排系上安全带,概率大约降一半。", fields: [ { label: "成本", value: "不花钱。每次上车多花 2 秒。" }, { label: "收益", value: "NHTSA 估计致命伤风险下降 45%。" }, ], sources: [ { text: "NHTSA (2024). Report", url: "https://a.gov/x" }, { text: "WHO (2025). Fact sheet" }, ], }); }); it("多条目按标题切分;标题后裸文本计入摘要", () => { const items = parseEntriesBody( [ "## 甲", "第一段摘要。", "第二行摘要。", "成本 :: 10 元。", "## 乙", "@tags 快", ].join("\n") ); assert.equal(items.length, 2); assert.equal(items[0].title, "甲"); assert.equal(items[0].summary, "第一段摘要。\n第二行摘要。"); assert.deepEqual(items[0].fields, [{ label: "成本", value: "10 元。" }]); assert.equal(items[1].title, "乙"); assert.deepEqual(items[1].tags, ["快"]); }); it("字段值多行追加到最后一个字段;无条目的内容丢弃", () => { const items = parseEntriesBody( [ "块首无标题行应丢弃", "## 甲", "成本 :: 第一行", "续行仍属成本", "@tags x", ].join("\n") ); assert.equal(items.length, 1); assert.equal(items[0].fields[0].value, "第一行\n续行仍属成本"); }); it("空块返回空数组", () => { assert.deepEqual(parseEntriesBody(""), []); }); it("@plain 标记无标题头,序列化往返保留", () => { const items = parseEntriesBody(["## 甲", "@plain", "@tags x", "成本 :: 1"].join("\n")); assert.equal(items[0].plain, true); const round = parseEntriesBody( serializeEntriesBlock(items).slice("[entries]\n".length, -"\n[/entries]".length) ); assert.equal(round[0].plain, true); const normal = parseEntriesBody(["## 甲", "@tags x"].join("\n")); assert.equal(normal[0].plain, undefined); }); }); describe("serializeEntriesBlock", () => { it("往返:序列化后可重新解析", () => { const src = parseEntriesBody( [ "## 系安全带", "@tags 性价比:极高 | 不花钱", "@summary 概率降一半。", "成本 :: 不花钱。", "@source NHTSA | https://a.gov/x", ].join("\n") ); const round = parseEntriesBody(serializeEntriesBlock(src).slice("[entries]\n".length, -"\n[/entries]".length)); assert.deepEqual(round, src); }); }); describe("splitContentSegments 集成 [entries]", () => { it("条目块单独成段,前后 md 保留", () => { const segs = splitContentSegments( ["前文", "", "[entries]", "## 甲", "@tags x", "[/entries]", "", "后文"].join("\n") ); assert.deepEqual( segs.map((s) => s.type), ["md", "entries", "md"] ); if (segs[1].type === "entries") { assert.equal(segs[1].items[0].title, "甲"); } else { assert.fail("第二段应为 entries"); } }); it("代码围栏内的块标记与未闭合块按普通 md 处理", () => { const fenced = splitContentSegments(["```", "[entries]", "```"].join("\n")); assert.deepEqual(fenced.map((s) => s.type), ["md"]); const unclosed = splitContentSegments(["[entries]", "## 甲"].join("\n")); assert.deepEqual(unclosed.map((s) => s.type), ["md"]); }); }); describe("extractToc 跳过 entries 块", () => { it("块内标题不进目录,块外锚点不错位", () => { const toc = extractToc( ["## 外部一", "[entries]", "## 卡片标题", "[/entries]", "### 外部二"].join("\n") ); assert.deepEqual( toc.map((t) => `${t.level}:${t.text}:${t.id}`), ["2:外部一:h-0", "3:外部二:h-1"] ); }); }); describe("wrapImplicitEntries(书级条目卡模式)", () => { it("标题 + 指令行自动包壳;普通小节与正文不受影响", () => { const src = [ "# 第一章", "", "## 说明小节", "这是普通段落,不是条目。", "", "## 系安全带", "@tags 性价比:极高", "@summary 概率降一半。", "成本 :: 不花钱。", "", "普通正文段落,出现在条目之后。", ].join("\n"); const wrapped = wrapImplicitEntries(src); const segs = splitContentSegments(wrapped, { entriesAuto: false }); assert.deepEqual( segs.map((s) => s.type), ["md", "entries", "md"] ); if (segs[1].type === "entries") { assert.equal(segs[1].items.length, 1); assert.equal(segs[1].items[0].title, "系安全带"); assert.deepEqual(segs[1].items[0].tags, ["性价比:极高"]); assert.deepEqual(segs[1].items[0].fields, [{ label: "成本", value: "不花钱。" }]); } else { assert.fail("隐式条目应解析为 entries 段"); } // 普通小节标题与条目后正文都按普通 md 渲染 if (segs[0].type !== "md" || segs[2].type !== "md") assert.fail("前后应为 md 段"); assert.ok(segs[0].text.includes("## 说明小节")); assert.ok(segs[2].text.includes("普通正文段落")); }); it("空行后仍是指令行时条目继续;空行后普通文本则条目收尾", () => { const src = [ "## 甲", "@tags a", "", "备注 :: 续字段。", "", "收尾后的普通段落。", ].join("\n"); const items = parseEntriesBody( wrapImplicitEntries(src) .split("[entries]\n")[1] ?.split("\n[/entries]")[0] ?? "" ); assert.equal(items.length, 1); assert.deepEqual(items[0].tags, ["a"]); assert.deepEqual(items[0].fields, [{ label: "备注", value: "续字段。" }]); }); it("显式 [entries] 块与 [entries:off]、时间线块原样跳过", () => { const src = [ "[entries]", "## 显式条目", "@tags x", "[/entries]", "", "[entries:off]", "## off 内标题", "@tags 不应识别为条目", "[/entries]", "", "## 隐式条目", "@summary 摘要。", ].join("\n"); const segs = splitContentSegments(src, { entriesAuto: true }); assert.deepEqual( segs.map((s) => s.type), ["entries", "md", "entries"] ); if (segs[0].type === "entries") assert.equal(segs[0].items[0].title, "显式条目"); else assert.fail("显式块应保持 entries"); // off 段内容原样按普通 md 渲染(含 off 内字面标记行) if (segs[1].type !== "md") assert.fail("off 段应为 md"); assert.ok(segs[1].text.includes("## off 内标题")); assert.ok(segs[1].text.includes("@tags 不应识别为条目")); if (segs[2].type === "entries") assert.equal(segs[2].items[0].title, "隐式条目"); else assert.fail("off 之后的条目应正常识别"); }); it("幂等:包壳结果重复执行不变", () => { const src = [ "## 系安全带", "@tags 钱=0", "成本 :: 不花钱。", "", "## 隐式条目", "@tags x", ].join("\n"); const once = wrapImplicitEntries(src); assert.equal(wrapImplicitEntries(once), once); }); it("条目标题进卡后目录不再收录,块外标题锚点不漂移", () => { const src = [ "## 前章", "", "### 系安全带", "@summary 降一半。", "成本 :: 不花钱。", "", "### 后节", ].join("\n"); const toc = extractToc(wrapImplicitEntries(src)); assert.deepEqual( toc.map((t) => `${t.level}:${t.text}`), ["2:前章", "3:后节"] ); }); }); describe("buildTocFromSegments(条目卡模式目录)", () => { it("md 标题与条目标题按渲染顺序编入目录并分配锚点", () => { const src = [ "开头说明。", "", "## 系安全带", "@tags 钱=0", "成本 :: 不花钱。", "", "### 小节标题", ].join("\n"); const segs = splitContentSegments(wrapImplicitEntries(src)); let n = 0; const { items, mdHeadIds, entriesTitleIds } = buildTocFromSegments(segs, () => `e-1-${++n}`); assert.deepEqual( items.map((t) => `${t.level}:${t.text}`), ["2:系安全带", "3:小节标题"] ); assert.deepEqual(mdHeadIds, ["e-1-2"]); assert.equal(entriesTitleIds[0], undefined); assert.deepEqual(entriesTitleIds[1], ["e-1-1"]); }); }); describe("注释标签写法不再识别(已收敛为一套 @ 指令写法)", () => { const USER_SAMPLE = [ "", "- 成本:不花钱。", "- 说人话:死亡风险降一半。", ].join("\n"); it("entriesAuto 开启时注释标签 + 字段列表仍保持普通 Markdown", () => { const segs = splitContentSegments(USER_SAMPLE, { entriesAuto: true }); assert.deepEqual(segs.map((s) => s.type), ["md"]); if (segs[0].type !== "md") assert.fail("应为 md 段"); assert.ok(segs[0].text.includes("- 成本:不花钱。")); }); }); describe("端到端:@ 指令写法(安全带条目)", () => { it("标题/@tags/@summary/字段/多来源完整成卡", () => { const src = [ "### 系安全带,前排后排都系", "@tags 钱=0 | 时间=少 | 毅力=否 | 收益=大 | 口径=死亡率 | 证据:A", "@summary 坐前排系上安全带,出车祸时被撞死的概率大约降一半。", "成本 :: 不花钱。每次上车多花 2 秒。", "收益 :: NHTSA 估计致命伤风险下降 45%。", "备注 :: 后排也要系。", "@source NHTSA (2024). Occupant Protection (DOT HS 813 573) | https://crashstats.nhtsa.dot.gov/Api/Public/ViewPublication/813573", "@source WHO (2025). Road traffic injuries fact sheet | https://www.who.int/news-room/fact-sheets/detail/road-traffic-injuries", ].join("\n"); const segs = splitContentSegments(src, { entriesAuto: true }); assert.deepEqual(segs.map((s) => s.type), ["entries"]); const it = segs[0].items[0]; assert.equal(it.title, "系安全带,前排后排都系"); assert.equal(it.summary, "坐前排系上安全带,出车祸时被撞死的概率大约降一半。"); assert.deepEqual(it.tags, [ "钱=0", "时间=少", "毅力=否", "收益=大", "口径=死亡率", "证据:A", ]); assert.deepEqual(it.fields, [ { label: "成本", value: "不花钱。每次上车多花 2 秒。" }, { label: "收益", value: "NHTSA 估计致命伤风险下降 45%。" }, { label: "备注", value: "后排也要系。" }, ]); assert.deepEqual(it.sources, [ { text: "NHTSA (2024). Occupant Protection (DOT HS 813 573)", url: "https://crashstats.nhtsa.dot.gov/Api/Public/ViewPublication/813573", }, { text: "WHO (2025). Road traffic injuries fact sheet", url: "https://www.who.int/news-room/fact-sheets/detail/road-traffic-injuries", }, ]); }); });