Skip to content
200 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge main into Artifacts Phase 21import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { type Block, type Span, blocks, inline, plainText, safeHref } from "./chat-markdown.ts";
5
6/** Every link anywhere in parsed blocks. */
7function links(list: Block[]): string[] {
8 const out: string[] = [];
9 const walk = (spans: Span[]) => {
10 for (const span of spans) {
11 if (span.t === "link") out.push(span.href);
12 if ("c" in span) walk(span.c);
13 }
14 };
15 const each = (block: Block) => {
16 if (block.t === "p") block.lines.forEach(walk);
17 else if (block.t === "heading") walk(block.c);
18 else if (block.t === "quote") block.c.forEach(each);
19 else if (block.t === "list") block.items.forEach((item) => item.forEach(each));
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.20 else if (block.t === "table") [block.head, ...block.rows].forEach((row) => row.forEach(walk));
Merge main into Artifacts Phase 221 };
22 list.forEach(each);
23 return out;
24}
25
26test("formatting: bold, italic, strikethrough and code, as people and agents write them", () => {
27 assert.deepEqual(inline("**bold** __also__ *it* _it_ ~~gone~~ ~gone~ `x`"), [
28 { t: "strong", c: [{ t: "text", v: "bold" }] },
29 { t: "text", v: " " },
30 { t: "strong", c: [{ t: "text", v: "also" }] },
31 { t: "text", v: " " },
32 { t: "em", c: [{ t: "text", v: "it" }] },
33 { t: "text", v: " " },
34 { t: "em", c: [{ t: "text", v: "it" }] },
35 { t: "text", v: " " },
36 { t: "del", c: [{ t: "text", v: "gone" }] },
37 { t: "text", v: " " },
38 { t: "del", c: [{ t: "text", v: "gone" }] },
39 { t: "text", v: " " },
40 { t: "code", v: "x" },
41 ]);
42 assert.deepEqual(inline("**_both_**"), [{ t: "strong", c: [{ t: "em", c: [{ t: "text", v: "both" }] }] }]);
43 assert.deepEqual(inline("``a ` b``"), [{ t: "code", v: "a ` b" }]);
44});
45
46test("words that only look like formatting stay words", () => {
47 assert.deepEqual(inline("snake_case_name and 2*3*4"), [{ t: "text", v: "snake_case_name and 2*3*4" }]);
48 assert.deepEqual(inline("about ~5 min, ~/src"), [{ t: "text", v: "about ~5 min, ~/src" }]);
49 assert.deepEqual(inline("\\*not\\* \\_em\\_ \\`code\\` \\@nobody"), [{ t: "text", v: "*not* _em_ `code` @nobody" }]);
50 assert.deepEqual(inline("C:\\Users\\me"), [{ t: "text", v: "C:\\Users\\me" }]);
51});
52
53test("mentions, channels and references; never inside code or a link's text", () => {
54 assert.deepEqual(inline("ask @reviewer about #12, g1t#3 in #web"), [
55 { t: "text", v: "ask " },
56 { t: "mention", name: "reviewer" },
57 { t: "text", v: " about " },
58 { t: "ref", repo: null, number: 12 },
59 { t: "text", v: ", " },
60 { t: "ref", repo: "g1t", number: 3 },
61 { t: "text", v: " in " },
62 { t: "channel", name: "web" },
63 ]);
64 assert.deepEqual(inline("`@reviewer #12`"), [{ t: "code", v: "@reviewer #12" }]);
65 assert.deepEqual(inline("[ping @ana](https://x.example)"), [{ t: "link", href: "https://x.example", c: [{ t: "text", v: "ping @ana" }] }]);
66 assert.deepEqual(inline("me@example.com"), [{ t: "text", v: "me@example.com" }]);
67});
68
69test("links go only to the web, mail or this site", () => {
70 assert.deepEqual(inline("[docs](https://g1t.sh/docs) and https://g1t.sh/a."), [
71 { t: "link", href: "https://g1t.sh/docs", c: [{ t: "text", v: "docs" }] },
72 { t: "text", v: " and " },
73 { t: "link", href: "https://g1t.sh/a", c: [{ t: "text", v: "https://g1t.sh/a" }] },
74 { t: "text", v: "." },
75 ]);
76 assert.deepEqual(inline("<https://g1t.sh>"), [{ t: "link", href: "https://g1t.sh", c: [{ t: "text", v: "https://g1t.sh" }] }]);
77 assert.deepEqual(inline("https://en.wikipedia.org/wiki/Foo_(bar)"), [
78 { t: "link", href: "https://en.wikipedia.org/wiki/Foo_(bar)", c: [{ t: "text", v: "https://en.wikipedia.org/wiki/Foo_(bar)" }] },
79 ]);
80 assert.equal(safeHref("/acme/web/pull/3"), "/acme/web/pull/3");
81 assert.equal(safeHref("mailto:me@example.com"), "mailto:me@example.com");
82});
83
84test("XSS: script links, raw HTML and nested tricks are text", () => {
85 const evil = [
86 "[x](javascript:alert(1))",
87 "[x](JaVaScRiPt:alert(1))",
88 "[x]( javascript:alert(1))",
89 "[x](java\tscript:alert(1))",
90 "[x](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)",
91 "[x](vbscript:msgbox(1))",
92 "[x](//evil.example)",
93 "[x](/\\evil.example)",
94 "[x](<javascript:alert(1)>)",
95 "![x](javascript:alert(1))",
96 "<javascript:alert(1)>",
97 "[**[x](javascript:alert(1))**](javascript:alert(2))",
98 "[a](https://ok.example)[b](javascript:alert(1))",
99 "javascript:alert(1)",
100 ];
101 for (const text of evil) {
102 for (const href of links(blocks(text))) assert.ok(/^(https?:\/\/|mailto:|\/(?![/\\]))/i.test(href), `${text} linked to ${href}`);
103 }
104 assert.equal(safeHref("javascript:alert(1)"), null);
105 assert.equal(safeHref("//evil.example"), null);
106 assert.equal(safeHref("/\\evil.example"), null);
107 assert.equal(safeHref("https://ok.example/\u0000x"), null);
108 assert.equal(safeHref("https://ok.example\\@evil.example"), null);
109
110 // HTML is never markup: it comes through as the text it is.
111 const html = blocks('<img src=x onerror="alert(1)"><script>alert(1)</script>\n<a href="javascript:alert(1)">x</a>');
112 assert.deepEqual(html, [
113 {
114 t: "p",
115 lines: [[{ t: "text", v: '<img src=x onerror="alert(1)"><script>alert(1)</script>' }], [{ t: "text", v: '<a href="javascript:alert(1)">x</a>' }]],
116 },
117 ]);
118 // An image is a link to it, never an image.
119 assert.deepEqual(inline("![logo](https://x.example/a.png)"), [{ t: "link", href: "https://x.example/a.png", c: [{ t: "text", v: "logo" }] }]);
120});
121
122test("blocks: paragraphs keep their line breaks; code, lists, quotes, headings and rules", () => {
123 const parsed = blocks("Plan:\n- one\n- two\n\n```ts\nconst a = 1;\n```\n> quoted\nlast");
124 assert.deepEqual(
125 parsed.map((block) => block.t),
126 ["p", "list", "code", "quote", "p"],
127 );
128 assert.deepEqual(parsed[2], { t: "code", lang: "ts", v: "const a = 1;" });
129 assert.deepEqual(blocks("a\nb"), [{ t: "p", lines: [[{ t: "text", v: "a" }], [{ t: "text", v: "b" }]] }]);
130 assert.deepEqual(blocks("## Summary\n---\n#general"), [
131 { t: "heading", level: 2, c: [{ t: "text", v: "Summary" }] },
132 { t: "hr" },
133 { t: "p", lines: [[{ t: "channel", name: "general" }]] },
134 ]);
135 // A fence inside a fence, with a longer fence around it.
136 assert.deepEqual(blocks("````md\n```\ninner\n```\n````"), [{ t: "code", lang: "md", v: "```\ninner\n```" }]);
137 // An unclosed fence runs to the end.
138 assert.deepEqual(blocks("```\nopen"), [{ t: "code", lang: null, v: "open" }]);
139});
140
141test("lists: numbered from where they start, nested by indenting, items with more than a line", () => {
142 const [list] = blocks("3. three\n4. four\n - deeper\n more\n5. five");
143 assert.equal(list!.t, "list");
144 if (list!.t !== "list") return;
145 assert.equal(list.ordered, true);
146 assert.equal(list.start, 3);
147 assert.equal(list.items.length, 3);
148 const nested = list.items[1]![1]!;
149 assert.equal(nested.t, "list");
150 if (nested.t === "list") assert.deepEqual(nested.items[0], [{ t: "p", lines: [[{ t: "text", v: "deeper" }], [{ t: "text", v: "more" }]] }]);
151 // A year at the start of a line is not a list.
152 assert.deepEqual(blocks("We grew in\n2019. Then again.").map((b) => b.t), ["p"]);
153 // Blank lines between items keep one list.
154 assert.equal(blocks("- a\n\n- b").length, 1);
155 // A quote holds blocks of its own.
156 const [quote] = blocks("> - a\n> - b");
157 assert.ok(quote!.t === "quote" && quote.c[0]!.t === "list");
158});
159
160test("deep nesting is bounded", () => {
161 const deep = blocks(">".repeat(200) + " x");
162 assert.equal(deep.length, 1);
163 const list = blocks(Array.from({ length: 50 }, (_, i) => `${" ".repeat(i)}- ${i}`).join("\n"));
164 assert.equal(list.length, 1);
165});
166
167test("plain text: the words without the marks, for previews", () => {
168 assert.equal(plainText("**bold** and `code`\n\nnext [link](https://x.example)"), "bold and code next link");
169 assert.equal(plainText("## Release\n- one\n- **two**\n\n> said\n---\n1. first"), "Release one two said 1. first");
170 assert.equal(plainText("ping @ana in #web about g1t#3, snake_case_name"), "ping @ana in #web about g1t#3, snake_case_name");
171 assert.equal(plainText("\\*literal\\* ~~gone~~ ![logo](https://x.example/a.png)"), "*literal* gone logo");
172 assert.equal(plainText("```ts\nconst a = 1;\n```"), "const a = 1;");
173 assert.equal(plainText("<b>hi</b>"), "<b>hi</b>");
174 assert.equal(plainText("x".repeat(300), 140).length, 140);
175 assert.ok(plainText("x".repeat(300), 140).endsWith("…"));
176});
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.177
178test("tables: a header, a --- row with its alignment, then rows", () => {
179 const [table, after] = blocks("| Session | Length | Notes |\n| --- | ---: | :-: |\n| 1. Welcome | 45 min | `web` \\| `api` |\n| 2. Queue | 30 min |\n\nAfter.");
180 assert.ok(table!.t === "table");
181 assert.deepEqual(table.align, [null, "right", "center"]);
182 assert.deepEqual(table.head, [[{ t: "text", v: "Session" }], [{ t: "text", v: "Length" }], [{ t: "text", v: "Notes" }]]);
183 assert.equal(table.rows.length, 2);
184 // An escaped pipe stays in its cell; a short row is filled to the header's width.
185 assert.deepEqual(table.rows[0]![2], [{ t: "code", v: "web" }, { t: "text", v: " | " }, { t: "code", v: "api" }]);
186 assert.deepEqual(table.rows[1]![2], []);
187 assert.equal(after!.t, "p");
188 // Without outer pipes, and right after a paragraph.
189 assert.deepEqual(blocks("Plan:\na | b\n--|--\n1 | 2").map((b) => b.t), ["p", "table"]);
190 // A pipe inside backticks is not a column.
191 const [code] = blocks("| a |\n| - |\n| `x | y` |");
192 assert.ok(code!.t === "table" && code.rows[0]!.length === 1);
193 // Not tables: a rule under text, columns that don't match, a line with a pipe alone.
194 assert.deepEqual(blocks("a | b\n---").map((b) => b.t), ["p", "hr"]);
195 assert.deepEqual(blocks("| a | b |\n| --- |\n| 1 | 2 |").map((b) => b.t), ["p"]);
196 assert.deepEqual(blocks("either | or").map((b) => b.t), ["p"]);
197 // Links in cells are checked like any other.
198 assert.deepEqual(links(blocks("| x |\n| - |\n| [ok](https://x.example) [no](javascript:alert(1)) |")), ["https://x.example"]);
199 assert.equal(plainText("| a | b |\n|---|---|\n| 1 | 2 |"), "a · b 1 · 2");
200});

This file's history is long; its oldest lines are credited to the oldest commit read.