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
6.3 kB 167 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("links a bare handle to its profile", () => { 85 const html = render("thanks @ada.test for the fix"); 86 expect(html).toContain('<a href="/ada.test" class="mention">@ada.test</a>'); 87 }); 88 89 it("leaves a handle inside a link label as text", () => { 90 expect(render("[ask @ada.test](https://example.com)")).not.toContain("mention"); 91 }); 92 93 it("does not read an email address as a mention", () => { 94 expect(render("mail ada@ada.test now")).not.toContain("mention"); 95 }); 96 97 it("shortens our own commit urls to a sha", () => { 98 const url = 99 "https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8"; 100 expect(render(url)).toContain("<code>0c4d0e9b</code>"); 101 }); 102 103 it("leaves a commit url that has its own label", () => { 104 const html = render( 105 "[the fix](https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8)" 106 ); 107 expect(html).toContain("the fix"); 108 expect(html).not.toContain("<code>"); 109 }); 110 111 it("leaves another host's commit urls as urls", () => { 112 const url = "https://github.com/ada/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8"; 113 expect(render(url)).not.toContain("<code>"); 114 }); 115 116 it("renders task lists as disabled checkboxes", () => { 117 const html = render("- [x] done\n- [ ] not"); 118 expect(html).toContain('type="checkbox"'); 119 expect(html).toContain('checked="checked"'); 120 expect(html).toContain('disabled="disabled"'); 121 }); 122 123 it("renders footnotes with their backlinks", () => { 124 const html = render("a claim[^1]\n\n[^1]: the source"); 125 expect(html).toContain('class="footnote-ref"'); 126 expect(html).toContain('class="footnote-backref"'); 127 expect(html).toContain("the source"); 128 }); 129 130 it("renders github style alerts", () => { 131 const html = render("> [!WARNING]\n> careful"); 132 expect(html).toContain('class="markdown-alert markdown-alert-warning"'); 133 expect(html).toContain("careful"); 134 }); 135 136 it("renders emoji shortcodes", () => { 137 expect(render("ship it :tada:")).toContain("🎉"); 138 }); 139 140 it("keeps the language on a fenced block", () => { 141 expect(render("```rust\nfn main() {}\n```")).toContain('class="language-rust"'); 142 }); 143 144 it("strips scripts, event handlers and javascript urls", () => { 145 expect(render("<script>alert(1)</script>")).not.toContain("alert"); 146 expect(render('<img src="x" onerror="alert(1)">')).not.toContain("onerror"); 147 // markdown-it refuses the destination outright, so this stays plain text 148 expect(render("[x](javascript:alert(1))")).not.toContain("<a"); 149 expect(render('<a href="data:text/html,hi">x</a>')).not.toContain("data:"); 150 }); 151 152 it("drops classes it does not recognise", () => { 153 expect(render('<div class="fixed inset-0 bg-black">boo</div>')).not.toContain("inset-0"); 154 }); 155 156 it("drops inputs that are not task list checkboxes", () => { 157 expect(render('<input type="password" name="p">')).not.toContain("<input"); 158 }); 159 160 it("keeps the html a readme actually uses for layout", () => { 161 const html = render( 162 '<div align="center"><h1>infra</h1></div>\n\n<details><summary>more</summary>\n\nhidden\n\n</details>' 163 ); 164 expect(html).toContain('<div align="center">'); 165 expect(html).toContain("<details><summary>more</summary>"); 166 }); 167});