This repository has no description
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("")).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("")).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("")).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("")).toContain('src="https://tangled.org/a.png"');
106 expect(camo("")).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});