Skip to content
128 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import * as Y from "yjs";
5
6import { parseInline, parseMarkdown, seed } from "./blocks.ts";
7import { documentMarkdown, excerpt, mentionedIds, outline, searchText, topContainers } from "./markdown.ts";
8
9function docFrom(markdown: string) {
10 const doc = new Y.Doc();
11 const fragment = doc.getXmlFragment("document-store");
12 seed(doc, fragment, markdown);
13 return { doc, fragment };
14}
15
16const SAMPLE = `# Release plan
17
18Ship **v2** on _Thursday_ with ~~no~~ \`zero\` downtime. See [the runbook](https://g1t.sh/x).
19
20## Steps
21
221. Freeze main
232. Tag the release
24 - with a signed tag
253. Deploy
26
27- [x] Write notes
28- [ ] Tell #support
29
30> [!WARNING]
31> Migrations run first.
32
33> Quotes stay quotes.
34
35\`\`\`ts
36const a = 1;
37\`\`\`
38
39\`\`\`mermaid
40flowchart LR
41 A --> B
42\`\`\`
43
44$$
45E = mc^2
46$$
47
48| Step | Owner |
49| --- | --- |
50| Tag | Ana |
51
52---
53
54![Diagram](https://g1tusercontent.com/docs/x.png)
55
56<details>
57<summary>More</summary>
58
59Hidden text.
60
61</details>
62`;
63
64test("Markdown survives a trip through the document", () => {
65 const { fragment } = docFrom(SAMPLE);
66 assert.equal(documentMarkdown(fragment), SAMPLE);
67});
68
69test("the document has BlockNote's shape: a group of containers with ids", () => {
70 const { fragment } = docFrom("# Title\n\nText");
71 const group = fragment.get(0) as Y.XmlElement;
72 assert.equal(group.nodeName, "blockGroup");
73 const tops = topContainers(fragment);
74 assert.equal(tops.length, 2);
75 assert.match(String(tops[0]!.getAttribute("id")), /^[0-9a-f-]{36}$/);
76 const heading = tops[0]!.get(0) as Y.XmlElement;
77 assert.equal(heading.nodeName, "heading");
78 assert.equal(heading.getAttribute("level"), 1 as unknown as string);
79 assert.equal(heading.getAttribute("textAlignment"), "left");
80});
81
82test("marks are stored as y-prosemirror stores them", () => {
83 const { fragment } = docFrom("a **b** [c](https://x.y)");
84 const paragraph = topContainers(fragment)[0]!.get(0) as Y.XmlElement;
85 const text = paragraph.get(0) as Y.XmlText;
86 assert.deepEqual(text.toDelta(), [{ insert: "a " }, { insert: "b", attributes: { bold: {} } }, { insert: " " }, { insert: "c", attributes: { link: { href: "https://x.y" } } }]);
87});
88
89test("nested list items become children", () => {
90 const specs = parseMarkdown("- one\n - two\n - three\n- four");
91 assert.equal(specs.length, 2);
92 assert.equal(specs[0]!.children![0]!.children![0]!.type, "bulletListItem");
93});
94
95test("snake_case and URLs are not italicized", () => {
96 assert.deepEqual(parseInline("set max_retry_count to 3"), [{ text: "set max_retry_count to 3" }]);
97});
98
99test("the outline names each top-level block for agents", () => {
100 const { fragment } = docFrom("# A\n\npara\n\n- x\n - y");
101 const blocks = outline(fragment);
102 assert.deepEqual(
103 blocks.map((b) => [b.type, b.level, b.markdown]),
104 [
105 ["heading", 1, "# A"],
106 ["paragraph", null, "para"],
107 ["bulletListItem", null, "- x\n - y"],
108 ],
109 );
110});
111
112test("mentions are found by id; search text drops markup", () => {
113 const doc = new Y.Doc();
114 const fragment = doc.getXmlFragment("document-store");
115 seed(doc, fragment, "hello");
116 doc.transact(() => {
117 const paragraph = topContainers(fragment)[0]!.get(0) as Y.XmlElement;
118 const mention = new Y.XmlElement("mention");
119 mention.setAttribute("kind", "user");
120 mention.setAttribute("id", "usr_1");
121 mention.setAttribute("name", "ana");
122 paragraph.insert(paragraph.length, [mention]);
123 });
124 assert.deepEqual(mentionedIds(fragment), { users: ["usr_1"], agents: [] });
125 assert.equal(documentMarkdown(fragment), "hello@ana\n");
126 assert.equal(searchText("## Steps\n\n- [x] **Ship** [it](https://x)"), "Steps\n Ship it");
127 assert.equal(excerpt("# T\n\n" + "word ".repeat(100), 20).length, 20);
128});