feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
This commit is contained in:
92
frontend/lib/bookSearch.test.ts
Normal file
92
frontend/lib/bookSearch.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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, "航天飞机"), []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user