Files
jiang13-bbs/frontend/lib/entryBlocks.test.ts
freefire 4f3f3a265c feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块:
- 友链(friendlink)后端处理 + 前端管理页与友链板
- 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot
- 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉
- 排行榜 service 测试、站点页面测试、时间线发布(timeline_release)

其它改动:
- 后端 handlers/services 全量小幅调整
- 前端组件、库函数、URL/品牌/站点 URL 工具更新
- Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
2026-09-30 16:30:07 +08:00

352 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = [
"<!-- 成本标签: 钱=0 时间=少 -->",
"- 成本:不花钱。",
"- 说人话:死亡风险降一半。",
].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",
},
]);
});
});