Skip to content
175 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 branch 'worktree-agent-a1c6f5039da0a4ac2' into integrate1import 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));
20 };
21 list.forEach(each);
22 return out;
23}
24
25test("formatting: bold, italic, strikethrough and code, as people and agents write them", () => {
26 assert.deepEqual(inline("**bold** __also__ *it* _it_ ~~gone~~ ~gone~ `x`"), [
27 { t: "strong", c: [{ t: "text", v: "bold" }] },
28 { t: "text", v: " " },
29 { t: "strong", c: [{ t: "text", v: "also" }] },
30 { t: "text", v: " " },
31 { t: "em", c: [{ t: "text", v: "it" }] },
32 { t: "text", v: " " },
33 { t: "em", c: [{ t: "text", v: "it" }] },
34 { t: "text", v: " " },
35 { t: "del", c: [{ t: "text", v: "gone" }] },
36 { t: "text", v: " " },
37 { t: "del", c: [{ t: "text", v: "gone" }] },
38 { t: "text", v: " " },
39 { t: "code", v: "x" },
40 ]);
41 assert.deepEqual(inline("**_both_**"), [{ t: "strong", c: [{ t: "em", c: [{ t: "text", v: "both" }] }] }]);
42 assert.deepEqual(inline("``a ` b``"), [{ t: "code", v: "a ` b" }]);
43});
44
45test("words that only look like formatting stay words", () => {
46 assert.deepEqual(inline("snake_case_name and 2*3*4"), [{ t: "text", v: "snake_case_name and 2*3*4" }]);
47 assert.deepEqual(inline("about ~5 min, ~/src"), [{ t: "text", v: "about ~5 min, ~/src" }]);
48 assert.deepEqual(inline("\\*not\\* \\_em\\_ \\`code\\` \\@nobody"), [{ t: "text", v: "*not* _em_ `code` @nobody" }]);
49 assert.deepEqual(inline("C:\\Users\\me"), [{ t: "text", v: "C:\\Users\\me" }]);
50});
51
52test("mentions, channels and references; never inside code or a link's text", () => {
53 assert.deepEqual(inline("ask @reviewer about #12, g1t#3 in #web"), [
54 { t: "text", v: "ask " },
55 { t: "mention", name: "reviewer" },
56 { t: "text", v: " about " },
57 { t: "ref", repo: null, number: 12 },
58 { t: "text", v: ", " },
59 { t: "ref", repo: "g1t", number: 3 },
60 { t: "text", v: " in " },
61 { t: "channel", name: "web" },
62 ]);
63 assert.deepEqual(inline("`@reviewer #12`"), [{ t: "code", v: "@reviewer #12" }]);
64 assert.deepEqual(inline("[ping @ana](https://x.example)"), [{ t: "link", href: "https://x.example", c: [{ t: "text", v: "ping @ana" }] }]);
65 assert.deepEqual(inline("me@example.com"), [{ t: "text", v: "me@example.com" }]);
66});
67
68test("links go only to the web, mail or this site", () => {
69 assert.deepEqual(inline("[docs](https://g1t.sh/docs) and https://g1t.sh/a."), [
70 { t: "link", href: "https://g1t.sh/docs", c: [{ t: "text", v: "docs" }] },
71 { t: "text", v: " and " },
72 { t: "link", href: "https://g1t.sh/a", c: [{ t: "text", v: "https://g1t.sh/a" }] },
73 { t: "text", v: "." },
74 ]);
75 assert.deepEqual(inline("<https://g1t.sh>"), [{ t: "link", href: "https://g1t.sh", c: [{ t: "text", v: "https://g1t.sh" }] }]);
76 assert.deepEqual(inline("https://en.wikipedia.org/wiki/Foo_(bar)"), [
77 { t: "link", href: "https://en.wikipedia.org/wiki/Foo_(bar)", c: [{ t: "text", v: "https://en.wikipedia.org/wiki/Foo_(bar)" }] },
78 ]);
79 assert.equal(safeHref("/acme/web/pull/3"), "/acme/web/pull/3");
80 assert.equal(safeHref("mailto:me@example.com"), "mailto:me@example.com");
81});
82
83test("XSS: script links, raw HTML and nested tricks are text", () => {
84 const evil = [
85 "[x](javascript:alert(1))",
86 "[x](JaVaScRiPt:alert(1))",
87 "[x]( javascript:alert(1))",
88 "[x](java\tscript:alert(1))",
89 "[x](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)",
90 "[x](vbscript:msgbox(1))",
91 "[x](//evil.example)",
92 "[x](/\\evil.example)",
93 "[x](<javascript:alert(1)>)",
94 "![x](javascript:alert(1))",
95 "<javascript:alert(1)>",
96 "[**[x](javascript:alert(1))**](javascript:alert(2))",
97 "[a](https://ok.example)[b](javascript:alert(1))",
98 "javascript:alert(1)",
99 ];
100 for (const text of evil) {
101 for (const href of links(blocks(text))) assert.ok(/^(https?:\/\/|mailto:|\/(?![/\\]))/i.test(href), `${text} linked to ${href}`);
102 }
103 assert.equal(safeHref("javascript:alert(1)"), null);
104 assert.equal(safeHref("//evil.example"), null);
105 assert.equal(safeHref("/\\evil.example"), null);
106 assert.equal(safeHref("https://ok.example/\u0000x"), null);
107 assert.equal(safeHref("https://ok.example\\@evil.example"), null);
108
109 // HTML is never markup: it comes through as the text it is.
110 const html = blocks('<img src=x onerror="alert(1)"><script>alert(1)</script>\n<a href="javascript:alert(1)">x</a>');
111 assert.deepEqual(html, [
112 {
113 t: "p",
114 lines: [[{ t: "text", v: '<img src=x onerror="alert(1)"><script>alert(1)</script>' }], [{ t: "text", v: '<a href="javascript:alert(1)">x</a>' }]],
115 },
116 ]);
117 // An image is a link to it, never an image.
118 assert.deepEqual(inline("![logo](https://x.example/a.png)"), [{ t: "link", href: "https://x.example/a.png", c: [{ t: "text", v: "logo" }] }]);
119});
120
121test("blocks: paragraphs keep their line breaks; code, lists, quotes, headings and rules", () => {
122 const parsed = blocks("Plan:\n- one\n- two\n\n```ts\nconst a = 1;\n```\n> quoted\nlast");
123 assert.deepEqual(
124 parsed.map((block) => block.t),
125 ["p", "list", "code", "quote", "p"],
126 );
127 assert.deepEqual(parsed[2], { t: "code", lang: "ts", v: "const a = 1;" });
128 assert.deepEqual(blocks("a\nb"), [{ t: "p", lines: [[{ t: "text", v: "a" }], [{ t: "text", v: "b" }]] }]);
129 assert.deepEqual(blocks("## Summary\n---\n#general"), [
130 { t: "heading", level: 2, c: [{ t: "text", v: "Summary" }] },
131 { t: "hr" },
132 { t: "p", lines: [[{ t: "channel", name: "general" }]] },
133 ]);
134 // A fence inside a fence, with a longer fence around it.
135 assert.deepEqual(blocks("````md\n```\ninner\n```\n````"), [{ t: "code", lang: "md", v: "```\ninner\n```" }]);
136 // An unclosed fence runs to the end.
137 assert.deepEqual(blocks("```\nopen"), [{ t: "code", lang: null, v: "open" }]);
138});
139
140test("lists: numbered from where they start, nested by indenting, items with more than a line", () => {
141 const [list] = blocks("3. three\n4. four\n - deeper\n more\n5. five");
142 assert.equal(list!.t, "list");
143 if (list!.t !== "list") return;
144 assert.equal(list.ordered, true);
145 assert.equal(list.start, 3);
146 assert.equal(list.items.length, 3);
147 const nested = list.items[1]![1]!;
148 assert.equal(nested.t, "list");
149 if (nested.t === "list") assert.deepEqual(nested.items[0], [{ t: "p", lines: [[{ t: "text", v: "deeper" }], [{ t: "text", v: "more" }]] }]);
150 // A year at the start of a line is not a list.
151 assert.deepEqual(blocks("We grew in\n2019. Then again.").map((b) => b.t), ["p"]);
152 // Blank lines between items keep one list.
153 assert.equal(blocks("- a\n\n- b").length, 1);
154 // A quote holds blocks of its own.
155 const [quote] = blocks("> - a\n> - b");
156 assert.ok(quote!.t === "quote" && quote.c[0]!.t === "list");
157});
158
159test("deep nesting is bounded", () => {
160 const deep = blocks(">".repeat(200) + " x");
161 assert.equal(deep.length, 1);
162 const list = blocks(Array.from({ length: 50 }, (_, i) => `${" ".repeat(i)}- ${i}`).join("\n"));
163 assert.equal(list.length, 1);
164});
165
166test("plain text: the words without the marks, for previews", () => {
167 assert.equal(plainText("**bold** and `code`\n\nnext [link](https://x.example)"), "bold and code next link");
168 assert.equal(plainText("## Release\n- one\n- **two**\n\n> said\n---\n1. first"), "Release one two said 1. first");
169 assert.equal(plainText("ping @ana in #web about g1t#3, snake_case_name"), "ping @ana in #web about g1t#3, snake_case_name");
170 assert.equal(plainText("\\*literal\\* ~~gone~~ ![logo](https://x.example/a.png)"), "*literal* gone logo");
171 assert.equal(plainText("```ts\nconst a = 1;\n```"), "const a = 1;");
172 assert.equal(plainText("<b>hi</b>"), "<b>hi</b>");
173 assert.equal(plainText("x".repeat(300), 140).length, 140);
174 assert.ok(plainText("x".repeat(300), 140).endsWith("…"));
175});

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