328 lines
9.1 KiB
TypeScript
328 lines
9.1 KiB
TypeScript
import type {
|
|
DocumentMeta,
|
|
PageMeta,
|
|
} from "@silverbulletmd/silverbullet/type/index";
|
|
import { expect, test } from "vitest";
|
|
import {
|
|
buildPageTree,
|
|
collectPlainleafTags,
|
|
isPlainleafFavoritePage,
|
|
pagesInPlainleafFolder,
|
|
plainleafDuplicatePageName,
|
|
plainleafFolderExists,
|
|
plainleafFolderPaths,
|
|
plainleafFrontmatterTags,
|
|
plainleafPageLabel,
|
|
plainleafTaskReference,
|
|
plainleafTaskTitle,
|
|
visiblePlainleafHtmlDocuments,
|
|
visiblePlainleafPages,
|
|
} from "./plainleaf_workspace.tsx";
|
|
|
|
function page(name: string, extra: Partial<PageMeta> = {}): PageMeta {
|
|
return {
|
|
ref: name,
|
|
tag: "page",
|
|
name,
|
|
created: "2026-08-12T00:00:00Z",
|
|
lastModified: "2026-08-12T00:00:00Z",
|
|
perm: "rw",
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function document(
|
|
name: string,
|
|
extension = name.split(".").at(-1) || "",
|
|
): DocumentMeta {
|
|
return {
|
|
ref: name,
|
|
tag: "document",
|
|
name,
|
|
contentType: extension === "html" ? "text/html" : "text/plain",
|
|
created: "2026-08-12T00:00:00Z",
|
|
lastModified: "2026-08-12T00:00:00Z",
|
|
size: 100,
|
|
perm: "rw",
|
|
extension,
|
|
};
|
|
}
|
|
|
|
test("builds nested folders from slash-separated page names", () => {
|
|
const tree = buildPageTree([
|
|
page("Inbox"),
|
|
page("Projects/Plainleaf/Plan"),
|
|
page("Projects/Plainleaf/Research"),
|
|
page("Projects/Website"),
|
|
]);
|
|
|
|
expect(tree.map((node) => [node.kind, node.name])).toEqual([
|
|
["folder", "Projects"],
|
|
["page", "Inbox"],
|
|
]);
|
|
expect(tree[0]).toMatchObject({
|
|
kind: "folder",
|
|
name: "Projects",
|
|
children: [
|
|
{
|
|
kind: "folder",
|
|
name: "Plainleaf",
|
|
children: [
|
|
{ kind: "page", name: "Projects/Plainleaf/Plan", label: "Plan" },
|
|
{
|
|
kind: "page",
|
|
name: "Projects/Plainleaf/Research",
|
|
label: "Research",
|
|
},
|
|
],
|
|
},
|
|
{ kind: "page", name: "Projects/Website", label: "Website" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("keeps a page and a same-named folder as separate nodes", () => {
|
|
const tree = buildPageTree([page("Projects"), page("Projects/Plan")]);
|
|
|
|
expect(tree).toMatchObject([
|
|
{
|
|
kind: "folder",
|
|
name: "Projects",
|
|
children: [{ kind: "page", name: "Projects/Plan" }],
|
|
},
|
|
{ kind: "page", name: "Projects" },
|
|
]);
|
|
});
|
|
|
|
test("mixes editable HTML documents into the same folder tree", () => {
|
|
const tree = buildPageTree([page("项目/计划")], "index", false, [
|
|
document("项目/展示页.html"),
|
|
]);
|
|
|
|
expect(tree).toMatchObject([
|
|
{
|
|
kind: "folder",
|
|
name: "项目",
|
|
children: [
|
|
{
|
|
kind: "page",
|
|
name: "项目/计划",
|
|
label: "计划",
|
|
format: "markdown",
|
|
},
|
|
{
|
|
kind: "page",
|
|
name: "项目/展示页.html",
|
|
label: "展示页",
|
|
format: "html",
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("sorts folders and files by name or latest modification", () => {
|
|
const pages = [
|
|
page("项目/旧计划", { lastModified: "2026-08-10T00:00:00Z" }),
|
|
page("项目/新计划", { lastModified: "2026-08-13T00:00:00Z" }),
|
|
page("归档/记录", { lastModified: "2026-08-11T00:00:00Z" }),
|
|
];
|
|
|
|
expect(
|
|
buildPageTree(pages, "index", false, [], "name-desc").map(
|
|
(node) => node.name,
|
|
),
|
|
).toEqual(["项目", "归档"]);
|
|
const recentTree = buildPageTree(pages, "index", false, [], "updated-desc");
|
|
expect(recentTree.map((node) => node.name)).toEqual(["项目", "归档"]);
|
|
expect(
|
|
recentTree[0].kind === "folder"
|
|
? recentTree[0].children.map((node) => node.name)
|
|
: [],
|
|
).toEqual(["项目/新计划", "项目/旧计划"]);
|
|
});
|
|
|
|
test("shows only user HTML documents in the Plainleaf tree", () => {
|
|
expect(
|
|
visiblePlainleafHtmlDocuments([
|
|
document("网页/介绍.html"),
|
|
document("网页/说明.txt"),
|
|
document("index.html"),
|
|
document("Library/内部.html"),
|
|
document("Repositories/内部.html"),
|
|
document(".plainleaf/history/内部.html"),
|
|
]).map((item) => item.name),
|
|
).toEqual(["网页/介绍.html", "index.html"]);
|
|
});
|
|
|
|
test("selects only Markdown pages below the exact folder prefix", () => {
|
|
expect(
|
|
pagesInPlainleafFolder(
|
|
[
|
|
page("项目/计划"),
|
|
page("项目/子目录/记录"),
|
|
page("项目备份/计划"),
|
|
page("项目/草稿", { _isAspiring: true }),
|
|
],
|
|
"项目",
|
|
),
|
|
).toEqual(["项目/计划", "项目/子目录/记录"]);
|
|
});
|
|
|
|
test("distinguishes a same-named page from an existing folder", () => {
|
|
expect(plainleafFolderExists([page("项目")], "项目")).toBe(false);
|
|
expect(plainleafFolderExists([page("项目/计划")], "项目")).toBe(true);
|
|
expect(plainleafFolderExists([page("项目备份/计划")], "项目")).toBe(false);
|
|
expect(
|
|
plainleafFolderExists([page("项目/草稿", { _isAspiring: true })], "项目"),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("creates a non-conflicting Markdown copy beside the source page", () => {
|
|
const pages = [
|
|
page("项目/计划"),
|
|
page("项目/计划 副本"),
|
|
page("项目/计划 副本 2"),
|
|
];
|
|
expect(plainleafDuplicatePageName("项目/计划", pages)).toBe(
|
|
"项目/计划 副本 3",
|
|
);
|
|
expect(plainleafDuplicatePageName("根目录笔记", pages)).toBe(
|
|
"根目录笔记 副本",
|
|
);
|
|
});
|
|
|
|
test("keeps the HTML extension when creating a copy", () => {
|
|
expect(
|
|
plainleafDuplicatePageName("网页/介绍.html", [
|
|
document("网页/介绍.html"),
|
|
document("网页/介绍 副本.html"),
|
|
]),
|
|
).toBe("网页/介绍 副本 2.html");
|
|
});
|
|
|
|
test("derives all nested destination folders from Markdown page paths", () => {
|
|
expect(
|
|
plainleafFolderPaths([
|
|
page("工作/项目/计划"),
|
|
page("工作/记录"),
|
|
page("个人/日记"),
|
|
page("草稿/隐藏", { _isAspiring: true }),
|
|
]),
|
|
).toEqual(["个人", "工作", "工作/项目"]);
|
|
});
|
|
|
|
test("recognizes both supported favorite tags", () => {
|
|
expect(isPlainleafFavoritePage(page("A", { tags: ["favorite"] }))).toBe(true);
|
|
expect(isPlainleafFavoritePage(page("B", { tags: ["收藏"] }))).toBe(true);
|
|
expect(isPlainleafFavoritePage(page("C", { tags: ["读书"] }))).toBe(false);
|
|
});
|
|
|
|
test("reads only YAML frontmatter tags when updating favorites", () => {
|
|
expect(
|
|
plainleafFrontmatterTags(
|
|
"---\ntitle: 示例\ntags:\n - 工作\n - favorite\n---\n# 正文\n\n#不会写回YAML\n",
|
|
),
|
|
).toEqual(["工作", "favorite"]);
|
|
expect(plainleafFrontmatterTags("# 正文\n\n#favorite\n")).toEqual([]);
|
|
expect(plainleafFrontmatterTags("---\ntags: 工作, 收藏\n---\n正文")).toEqual([
|
|
"工作",
|
|
"收藏",
|
|
]);
|
|
});
|
|
|
|
test("omits aspiring and decorated hidden pages", () => {
|
|
const tree = buildPageTree([
|
|
page("Visible"),
|
|
page("Aspiring", { _isAspiring: true }),
|
|
page("Hidden", { pageDecoration: { hide: true } }),
|
|
]);
|
|
|
|
expect(tree).toMatchObject([{ kind: "page", name: "Visible" }]);
|
|
});
|
|
|
|
test("hides SilverBullet system roots and meta-tagged pages", () => {
|
|
const pages = [
|
|
page("Notes/Visible"),
|
|
page("Library/Std"),
|
|
page("Repositories/Std"),
|
|
page("CONFIG"),
|
|
page("Internal", { tags: ["meta/library"] }),
|
|
];
|
|
|
|
expect(visiblePlainleafPages(pages).map((item) => item.name)).toEqual([
|
|
"Notes/Visible",
|
|
]);
|
|
expect(visiblePlainleafPages(pages, true).map((item) => item.name)).toEqual([
|
|
"Notes/Visible",
|
|
"Library/Std",
|
|
"Repositories/Std",
|
|
"CONFIG",
|
|
"Internal",
|
|
]);
|
|
});
|
|
|
|
test("uses a friendly home label without changing the stored page name", () => {
|
|
expect(plainleafPageLabel("index", "index")).toBe("首页");
|
|
expect(plainleafPageLabel("项目/计划", "index")).toBe("计划");
|
|
expect(plainleafPageLabel("Library/Std", "index")).toBe("内置");
|
|
expect(plainleafPageLabel("CONFIG", "index")).toBe("系统配置");
|
|
|
|
const tree = buildPageTree([page("index")], "index");
|
|
expect(tree).toMatchObject([{ kind: "page", name: "index", label: "首页" }]);
|
|
});
|
|
|
|
test("counts user tags and excludes internal tags", () => {
|
|
const tags = collectPlainleafTags([
|
|
page("A", { tags: ["产品"] }),
|
|
page("B", { tags: ["产品", "灵感"] }),
|
|
page("C", { tags: ["template"] }),
|
|
page("D", { tags: ["产品", "meta/library"] }),
|
|
]);
|
|
|
|
expect(tags).toEqual([
|
|
{ name: "产品", count: 2 },
|
|
{ name: "灵感", count: 1 },
|
|
]);
|
|
expect(
|
|
collectPlainleafTags(
|
|
[page("Library/Test", { tags: ["meta/library"] })],
|
|
true,
|
|
),
|
|
).toEqual([{ name: "meta/library", count: 1 }]);
|
|
});
|
|
|
|
test("qualifies anchor-backed task refs with their source page", () => {
|
|
expect(
|
|
plainleafTaskReference({
|
|
ref: "follow-up",
|
|
page: "项目/计划",
|
|
name: "跟进",
|
|
state: " ",
|
|
done: false,
|
|
}),
|
|
).toBe("项目/计划$follow-up");
|
|
expect(
|
|
plainleafTaskReference({
|
|
ref: "项目/计划@42",
|
|
page: "项目/计划",
|
|
name: "跟进",
|
|
state: " ",
|
|
done: false,
|
|
}),
|
|
).toBe("项目/计划@42");
|
|
});
|
|
|
|
test("separates explicit project links from the editable task title", () => {
|
|
expect(
|
|
plainleafTaskTitle({
|
|
ref: "项目/计划@42",
|
|
page: "项目/计划",
|
|
name: "跟进设计稿 [[项目/Plainleaf]]",
|
|
state: " ",
|
|
done: false,
|
|
links: ["项目/Plainleaf"],
|
|
}),
|
|
).toBe("跟进设计稿");
|
|
});
|