Skip to content
83 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { type DocNode, docToMarkdown, escapeText, markdownToDoc } from "./chat-compose.ts";
5
6const text = (value: string, ...marks: (string | { type: string; attrs?: Record<string, unknown> })[]): DocNode => ({
7 type: "text",
8 text: value,
9 ...(marks.length ? { marks: marks.map((mark) => (typeof mark === "string" ? { type: mark } : mark)) } : {}),
10});
11const p = (...content: DocNode[]): DocNode => (content.length ? { type: "paragraph", content } : { type: "paragraph" });
12const doc = (...content: DocNode[]): DocNode => ({ type: "doc", content });
13
14test("formatting is written as Markdown", () => {
15 assert.equal(
16 docToMarkdown(doc(p(text("bold", "bold"), text(" "), text("it", "italic"), text(" "), text("gone", "strike"), text(" "), text("a*b", "code")))),
17 "**bold** _it_ ~~gone~~ `a*b`",
18 );
19 // Marks that overlap open and close once.
20 assert.equal(docToMarkdown(doc(p(text("a", "bold"), text("b", "bold", "italic"), text("c", "bold")))), "**a_b_c**");
21 // Spaces at a mark's edges sit outside it.
22 assert.equal(docToMarkdown(doc(p(text("say "), text(" hi ", "bold"), text("there")))), "say **hi** there");
23 assert.equal(docToMarkdown(doc(p(text("tick ` in", "code")))), "`` tick ` in ``");
24});
25
26test("links: with their text, or bare when the text is the address", () => {
27 const link = (href: string) => ({ type: "link", attrs: { href } });
28 assert.equal(docToMarkdown(doc(p(text("the docs", link("https://g1t.sh/docs"))))), "[the docs](https://g1t.sh/docs)");
29 assert.equal(docToMarkdown(doc(p(text("https://g1t.sh", link("https://g1t.sh"))))), "https://g1t.sh");
30 assert.equal(docToMarkdown(doc(p(text("x", link("https://a.example/a b"))))), "[x](<https://a.example/a%20b>)");
31});
32
33test("each paragraph is a line; an empty one is a blank line", () => {
34 assert.equal(docToMarkdown(doc(p(text("one")), p(text("two")), p(), p(text("three")))), "one\ntwo\n\nthree");
35 assert.equal(docToMarkdown(doc(p(), p(text("x")), p())), "x");
36});
37
38test("text that only looks like Markdown is escaped, and mentions stay mentions", () => {
39 assert.equal(escapeText("2*3 and snake_case and _edge_ and [x] and `t` and ~5"), "2\\*3 and snake_case and \\_edge\\_ and \\[x\\] and \\`t\\` and \\~5");
40 assert.equal(docToMarkdown(doc(p(text("- not a list")), p(text("1. not either")), p(text("# nor this")), p(text("> or this")))), "\\- not a list\n1\\. not either\n\\# nor this\n\\> or this");
41 assert.equal(docToMarkdown(doc(p(text("ask @ana in #web about #12")))), "ask @ana in #web about #12");
42});
43
44test("lists, quotes and code blocks", () => {
45 const item = (...content: DocNode[]): DocNode => ({ type: "listItem", content });
46 assert.equal(
47 docToMarkdown(
48 doc(
49 p(text("Plan:")),
50 { type: "bulletList", content: [item(p(text("one"))), item(p(text("two")), { type: "orderedList", attrs: { start: 1 }, content: [item(p(text("deep")))] })] },
51 p(text("after")),
52 ),
53 ),
54 "Plan:\n\n- one\n- two\n 1. deep\n\nafter",
55 );
56 assert.equal(docToMarkdown(doc({ type: "orderedList", attrs: { start: 3 }, content: [item(p(text("c"))), item(p(text("d")))] })), "3. c\n4. d");
57 assert.equal(docToMarkdown(doc({ type: "blockquote", content: [p(text("quoted")), p(), p(text("more"))] })), "> quoted\n>\n> more");
58 assert.equal(docToMarkdown(doc({ type: "codeBlock", attrs: { language: "ts" }, content: [{ type: "text", text: "const a = 1;\n\n\nconst b = `x`;" }] })), "```ts\nconst a = 1;\n\n\nconst b = `x`;\n```");
59 assert.equal(docToMarkdown(doc({ type: "codeBlock", attrs: { language: null }, content: [{ type: "text", text: "```\nfence\n```" }] })), "````\n```\nfence\n```\n````");
60});
61
62test("Markdown comes back into the composer as it was written", () => {
63 const samples = [
64 "**bold** _it_ ~~gone~~ `code`",
65 "one\ntwo\n\nthree",
66 "Plan:\n\n- one\n- two\n 1. deep\n\nafter",
67 "3. c\n4. d",
68 "> quoted\n>\n> more",
69 "```ts\nconst a = 1;\n\nconst b = 2;\n```",
70 "[the docs](https://g1t.sh/docs) and https://g1t.sh",
71 "ask @ana in #web about g1t#12",
72 "\\- not a list and 2\\*3 and snake_case",
73 "**a _b_ c**",
74 ];
75 for (const sample of samples) assert.equal(docToMarkdown(markdownToDoc(sample)), sample, sample);
76});
77
78test("Markdown the composer cannot write still comes in", () => {
79 // A heading is a bold line; a rule is left out; an image is a link.
80 assert.equal(docToMarkdown(markdownToDoc("## Notes\n---\ntext")), "**Notes**\ntext");
81 assert.equal(docToMarkdown(markdownToDoc("*star em* and __strong__ and ~one~")), "_star em_ and **strong** and ~~one~~");
82 assert.deepEqual(markdownToDoc(""), { type: "doc", content: [{ type: "paragraph" }] });
83});