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