import { describe, expect, it } from "vitest"; import { isMarkdownFile } from "./format"; import { renderMarkdown } from "./markdown"; import type { MarkupContext } from "./paths"; const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main", host: "tangled.org" }; const render = (source: string, overrides: Partial = {}) => renderMarkdown(source, { ...ctx, ...overrides }); describe("isMarkdownFile", () => { it("knows the markdown extensions from the plain text ones", () => { expect(isMarkdownFile("README.md")).toBe(true); expect(isMarkdownFile("readme.MARKDOWN")).toBe(true); expect(isMarkdownFile("readme.mkd")).toBe(true); expect(isMarkdownFile("README")).toBe(false); expect(isMarkdownFile("README.rst")).toBe(false); }); }); describe("renderMarkdown", () => { it("renders gfm tables and strikethrough", () => { const html = render("| a | b |\n| - | - |\n| 1 | 2 |\n\n~~gone~~"); expect(html).toContain(""); expect(html).toContain("gone"); }); it("gives headings github's slugs and an anchor link", () => { const html = render("## Hello, World!"); expect(html).toContain('id="hello-world"'); expect(html).toContain('#'); }); it("points relative links at the file browser", () => { expect(render("[docs](./docs/setup.md)")).toContain( 'href="/ada.test/infra/tree/main/docs/setup.md"' ); }); it("resolves a link against the directory the document is in", () => { const html = render("[sibling](../other.md)", { dir: "docs/guide" }); expect(html).toContain('href="/ada.test/infra/tree/main/docs/other.md"'); }); it("treats an absolute path as repo relative, not host relative", () => { expect(render("[root](/LICENSE)", { dir: "docs" })).toContain( 'href="/ada.test/infra/tree/main/LICENSE"' ); }); it("keeps a fragment on a rewritten link", () => { expect(render("[part](./setup.md#install)")).toContain( 'href="/ada.test/infra/tree/main/setup.md#install"' ); }); it("leaves fragments, mail and absolute urls alone", () => { const html = render("[a](#top) [b](mailto:ada@test) [c](https://example.com)"); expect(html).toContain('href="#top"'); expect(html).toContain('href="mailto:ada@test"'); expect(html).toContain('href="https://example.com"'); }); it("marks off-site links as untrusted", () => { expect(render("[c](https://example.com)")).toContain('rel="nofollow noopener noreferrer"'); expect(render("[a](./x.md)")).not.toContain("nofollow"); }); it("points relative images at the raw file, including raw html ones", () => { expect(render("![logo](assets/logo.png)")).toContain( 'src="/ada.test/infra/raw/main/assets/logo.png"' ); expect(render('logo')).toContain( 'src="/ada.test/infra/raw/main/assets/logo.png"' ); }); it("rewrites every candidate in a srcset", () => { const html = render(''); expect(html).toContain( 'srcset="/ada.test/infra/raw/main/a.png 1x, /ada.test/infra/raw/main/b.png 2x"' ); }); it("leaves off-site images alone when there is no camo", () => { expect(render("![x](https://example.com/a.png)")).toContain('src="https://example.com/a.png"'); }); describe("with camo", () => { const camo = (source: string) => render(source, { camo: true }); // hex of https://example.com/a.png, which is what camo signs const hex = "68747470733a2f2f6578616d706c652e636f6d2f612e706e67"; it("sends an off-site image to the route that signs for camo", () => { expect(camo("![x](https://example.com/a.png)")).toContain(`src="/camo/${hex}"`); }); it("proxies raw html images and srcset candidates too", () => { expect(camo('')).toContain(`src="/camo/${hex}"`); expect(camo('')).toContain( `srcset="/camo/${hex} 2x"` ); }); it("leaves our own images and repo files direct", () => { expect(camo("![x](https://tangled.org/a.png)")).toContain('src="https://tangled.org/a.png"'); expect(camo("![x](assets/a.png)")).toContain('src="/ada.test/infra/raw/main/assets/a.png"'); }); it("does not proxy links, only what the page loads by itself", () => { expect(camo("[x](https://example.com/a.png)")).toContain('href="https://example.com/a.png"'); }); }); it("links a bare handle to its profile", () => { const html = render("thanks @ada.test for the fix"); expect(html).toContain('@ada.test'); }); it("leaves a handle inside a link label as text", () => { expect(render("[ask @ada.test](https://example.com)")).not.toContain("mention"); }); it("does not read an email address as a mention", () => { expect(render("mail ada@ada.test now")).not.toContain("mention"); }); it("shortens our own commit urls to a sha", () => { const url = "https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8"; expect(render(url)).toContain("0c4d0e9b"); }); it("leaves a commit url that has its own label", () => { const html = render( "[the fix](https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8)" ); expect(html).toContain("the fix"); expect(html).not.toContain(""); }); it("leaves another host's commit urls as urls", () => { const url = "https://github.com/ada/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8"; expect(render(url)).not.toContain(""); }); it("renders task lists as disabled checkboxes", () => { const html = render("- [x] done\n- [ ] not"); expect(html).toContain('type="checkbox"'); expect(html).toContain('checked="checked"'); expect(html).toContain('disabled="disabled"'); }); it("renders footnotes with their backlinks", () => { const html = render("a claim[^1]\n\n[^1]: the source"); expect(html).toContain('class="footnote-ref"'); expect(html).toContain('class="footnote-backref"'); expect(html).toContain("the source"); }); it("renders github style alerts", () => { const html = render("> [!WARNING]\n> careful"); expect(html).toContain('class="markdown-alert markdown-alert-warning"'); expect(html).toContain("careful"); }); it("renders emoji shortcodes", () => { expect(render("ship it :tada:")).toContain("🎉"); }); it("keeps the language on a fenced block", () => { expect(render("```rust\nfn main() {}\n```")).toContain('class="language-rust"'); }); it("strips scripts, event handlers and javascript urls", () => { expect(render("")).not.toContain("alert"); expect(render('')).not.toContain("onerror"); // markdown-it refuses the destination outright, so this stays plain text expect(render("[x](javascript:alert(1))")).not.toContain("x')).not.toContain("data:"); }); it("drops classes it does not recognise", () => { expect(render('
boo
')).not.toContain("inset-0"); }); it("drops inputs that are not task list checkboxes", () => { expect(render('')).not.toContain(" { const html = render( '

infra

\n\n
more\n\nhidden\n\n
' ); expect(html).toContain('
'); expect(html).toContain("
more"); }); });