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 integrate | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { type Block, type Span, blocks, inline, plainText, safeHref } from "./chat-markdown.ts"; | |
| 5 | ||
| 6 | /** Every link anywhere in parsed blocks. */ | |
| 7 | function 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 | ||
| 25 | test("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 | ||
| 45 | test("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 | ||
| 52 | test("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 | ||
| 68 | test("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 | ||
| 83 | test("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 | ")", | |
| 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(""), [{ t: "link", href: "https://x.example/a.png", c: [{ t: "text", v: "logo" }] }]); | |
| 119 | }); | |
| 120 | ||
| 121 | test("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 | ||
| 140 | test("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 | ||
| 159 | test("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 | ||
| 166 | test("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~~ "), "*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.