This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / markup / markdown.test.ts
7.6 kB 197 lines
1import { describe, expect, it } from "vitest"; 2import { isMarkdownFile } from "./format"; 3import { renderMarkdown } from "./markdown"; 4import type { MarkupContext } from "./paths"; 5 6const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main", host: "tangled.org" }; 7const render = (source: string, overrides: Partial<MarkupContext> = {}) => 8 renderMarkdown(source, { ...ctx, ...overrides }); 9 10describe("isMarkdownFile", () => { 11 it("knows the markdown extensions from the plain text ones", () => { 12 expect(isMarkdownFile("README.md")).toBe(true); 13 expect(isMarkdownFile("readme.MARKDOWN")).toBe(true); 14 expect(isMarkdownFile("readme.mkd")).toBe(true); 15 expect(isMarkdownFile("README")).toBe(false); 16 expect(isMarkdownFile("README.rst")).toBe(false); 17 }); 18}); 19 20describe("renderMarkdown", () => { 21 it("renders gfm tables and strikethrough", () => { 22 const html = render("| a | b |\n| - | - |\n| 1 | 2 |\n\n~~gone~~"); 23 expect(html).toContain("<table>"); 24 expect(html).toContain("<s>gone</s>"); 25 }); 26 27 it("gives headings github's slugs and an anchor link", () => { 28 const html = render("## Hello, World!"); 29 expect(html).toContain('id="hello-world"'); 30 expect(html).toContain('<a class="anchor" href="#hello-world">#</a>'); 31 }); 32 33 it("points relative links at the file browser", () => { 34 expect(render("[docs](./docs/setup.md)")).toContain( 35 'href="/ada.test/infra/tree/main/docs/setup.md"' 36 ); 37 }); 38 39 it("resolves a link against the directory the document is in", () => { 40 const html = render("[sibling](../other.md)", { dir: "docs/guide" }); 41 expect(html).toContain('href="/ada.test/infra/tree/main/docs/other.md"'); 42 }); 43 44 it("treats an absolute path as repo relative, not host relative", () => { 45 expect(render("[root](/LICENSE)", { dir: "docs" })).toContain( 46 'href="/ada.test/infra/tree/main/LICENSE"' 47 ); 48 }); 49 50 it("keeps a fragment on a rewritten link", () => { 51 expect(render("[part](./setup.md#install)")).toContain( 52 'href="/ada.test/infra/tree/main/setup.md#install"' 53 ); 54 }); 55 56 it("leaves fragments, mail and absolute urls alone", () => { 57 const html = render("[a](#top) [b](mailto:ada@test) [c](https://example.com)"); 58 expect(html).toContain('href="#top"'); 59 expect(html).toContain('href="mailto:ada@test"'); 60 expect(html).toContain('href="https://example.com"'); 61 }); 62 63 it("marks off-site links as untrusted", () => { 64 expect(render("[c](https://example.com)")).toContain('rel="nofollow noopener noreferrer"'); 65 expect(render("[a](./x.md)")).not.toContain("nofollow"); 66 }); 67 68 it("points relative images at the raw file, including raw html ones", () => { 69 expect(render("![logo](assets/logo.png)")).toContain( 70 'src="/ada.test/infra/raw/main/assets/logo.png"' 71 ); 72 expect(render('<img src="assets/logo.png" alt="logo">')).toContain( 73 'src="/ada.test/infra/raw/main/assets/logo.png"' 74 ); 75 }); 76 77 it("rewrites every candidate in a srcset", () => { 78 const html = render('<picture><source srcset="a.png 1x, b.png 2x"></picture>'); 79 expect(html).toContain( 80 'srcset="/ada.test/infra/raw/main/a.png 1x, /ada.test/infra/raw/main/b.png 2x"' 81 ); 82 }); 83 84 it("leaves off-site images alone when there is no camo", () => { 85 expect(render("![x](https://example.com/a.png)")).toContain('src="https://example.com/a.png"'); 86 }); 87 88 describe("with camo", () => { 89 const camo = (source: string) => render(source, { camo: true }); 90 // hex of https://example.com/a.png, which is what camo signs 91 const hex = "68747470733a2f2f6578616d706c652e636f6d2f612e706e67"; 92 93 it("sends an off-site image to the route that signs for camo", () => { 94 expect(camo("![x](https://example.com/a.png)")).toContain(`src="/camo/${hex}"`); 95 }); 96 97 it("proxies raw html images and srcset candidates too", () => { 98 expect(camo('<img src="https://example.com/a.png">')).toContain(`src="/camo/${hex}"`); 99 expect(camo('<source srcset="https://example.com/a.png 2x">')).toContain( 100 `srcset="/camo/${hex} 2x"` 101 ); 102 }); 103 104 it("leaves our own images and repo files direct", () => { 105 expect(camo("![x](https://tangled.org/a.png)")).toContain('src="https://tangled.org/a.png"'); 106 expect(camo("![x](assets/a.png)")).toContain('src="/ada.test/infra/raw/main/assets/a.png"'); 107 }); 108 109 it("does not proxy links, only what the page loads by itself", () => { 110 expect(camo("[x](https://example.com/a.png)")).toContain('href="https://example.com/a.png"'); 111 }); 112 }); 113 114 it("links a bare handle to its profile", () => { 115 const html = render("thanks @ada.test for the fix"); 116 expect(html).toContain('<a href="/ada.test" class="mention">@ada.test</a>'); 117 }); 118 119 it("leaves a handle inside a link label as text", () => { 120 expect(render("[ask @ada.test](https://example.com)")).not.toContain("mention"); 121 }); 122 123 it("does not read an email address as a mention", () => { 124 expect(render("mail ada@ada.test now")).not.toContain("mention"); 125 }); 126 127 it("shortens our own commit urls to a sha", () => { 128 const url = 129 "https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8"; 130 expect(render(url)).toContain("<code>0c4d0e9b</code>"); 131 }); 132 133 it("leaves a commit url that has its own label", () => { 134 const html = render( 135 "[the fix](https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8)" 136 ); 137 expect(html).toContain("the fix"); 138 expect(html).not.toContain("<code>"); 139 }); 140 141 it("leaves another host's commit urls as urls", () => { 142 const url = "https://github.com/ada/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8"; 143 expect(render(url)).not.toContain("<code>"); 144 }); 145 146 it("renders task lists as disabled checkboxes", () => { 147 const html = render("- [x] done\n- [ ] not"); 148 expect(html).toContain('type="checkbox"'); 149 expect(html).toContain('checked="checked"'); 150 expect(html).toContain('disabled="disabled"'); 151 }); 152 153 it("renders footnotes with their backlinks", () => { 154 const html = render("a claim[^1]\n\n[^1]: the source"); 155 expect(html).toContain('class="footnote-ref"'); 156 expect(html).toContain('class="footnote-backref"'); 157 expect(html).toContain("the source"); 158 }); 159 160 it("renders github style alerts", () => { 161 const html = render("> [!WARNING]\n> careful"); 162 expect(html).toContain('class="markdown-alert markdown-alert-warning"'); 163 expect(html).toContain("careful"); 164 }); 165 166 it("renders emoji shortcodes", () => { 167 expect(render("ship it :tada:")).toContain("🎉"); 168 }); 169 170 it("keeps the language on a fenced block", () => { 171 expect(render("```rust\nfn main() {}\n```")).toContain('class="language-rust"'); 172 }); 173 174 it("strips scripts, event handlers and javascript urls", () => { 175 expect(render("<script>alert(1)</script>")).not.toContain("alert"); 176 expect(render('<img src="x" onerror="alert(1)">')).not.toContain("onerror"); 177 // markdown-it refuses the destination outright, so this stays plain text 178 expect(render("[x](javascript:alert(1))")).not.toContain("<a"); 179 expect(render('<a href="data:text/html,hi">x</a>')).not.toContain("data:"); 180 }); 181 182 it("drops classes it does not recognise", () => { 183 expect(render('<div class="fixed inset-0 bg-black">boo</div>')).not.toContain("inset-0"); 184 }); 185 186 it("drops inputs that are not task list checkboxes", () => { 187 expect(render('<input type="password" name="p">')).not.toContain("<input"); 188 }); 189 190 it("keeps the html a readme actually uses for layout", () => { 191 const html = render( 192 '<div align="center"><h1>infra</h1></div>\n\n<details><summary>more</summary>\n\nhidden\n\n</details>' 193 ); 194 expect(html).toContain('<div align="center">'); 195 expect(html).toContain("<details><summary>more</summary>"); 196 }); 197});