import { describe, expect, it } from "vitest";
import { isMarkdownFile } from "#lib/markup/format.ts";
import { renderMarkdown } from "#lib/markup/markdown.ts";
import type { MarkupContext } from "#lib/markup/paths.ts";
const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main", host: "tangled.org" };
const render = (source: string, overrides: Partial<MarkupContext> = {}) =>
renderMarkdown(source, { ...ctx, ...overrides });
describe("isMarkdownFile", () => {
it("knows the markdown extensions from the plain text ones", async () => {
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", async () => {
const html = await render("| a | b |\n| - | - |\n| 1 | 2 |\n\n~~gone~~");
expect(html).toContain("<table>");
expect(html).toContain("<s>gone</s>");
});
it("gives headings github's slugs and an anchor link", async () => {
const html = await render("## Hello, World!");
expect(html).toContain('id="hello-world"');
expect(html).toContain('<a class="anchor" href="#hello-world">#</a>');
});
it("points relative links at the file browser", async () => {
expect(await 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", async () => {
const html = await 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", async () => {
expect(await render("[root](/LICENSE)", { dir: "docs" })).toContain(
'href="/ada.test/infra/tree/main/LICENSE"'
);
});
it("keeps a fragment on a rewritten link", async () => {
expect(await render("[part](./setup.md#install)")).toContain(
'href="/ada.test/infra/tree/main/setup.md#install"'
);
});
it("leaves fragments, mail and absolute urls alone", async () => {
const html = await 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", async () => {
expect(await render("[c](https://example.com)")).toContain(
'rel="nofollow noopener noreferrer"'
);
expect(await render("[a](./x.md)")).not.toContain("nofollow");
});
it("points relative images at the raw file, including raw html ones", async () => {
expect(await render("")).toContain(
'src="/ada.test/infra/raw/main/assets/logo.png"'
);
expect(await render('<img src="assets/logo.png" alt="logo">')).toContain(
'src="/ada.test/infra/raw/main/assets/logo.png"'
);
});
it("loads images lazily and decodes them off the main path, whatever the source says", async () => {
expect(await render("")).toContain('loading="lazy" decoding="async"');
const html = await render('<img src="a.png" loading="eager" decoding="sync">');
expect(html).toContain('loading="lazy"');
expect(html).toContain('decoding="async"');
expect(html).not.toContain("eager");
});
it("rewrites every candidate in a srcset", async () => {
const html = await render('<picture><source srcset="a.png 1x, b.png 2x"></picture>');
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", async () => {
expect(await render("")).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", async () => {
expect(await camo("")).toContain(`src="/camo/${hex}"`);
});
it("proxies raw html images and srcset candidates too", async () => {
expect(await camo('<img src="https://example.com/a.png">')).toContain(
`src="/camo/${hex}"`
);
expect(await camo('<source srcset="https://example.com/a.png 2x">')).toContain(
`srcset="/camo/${hex} 2x"`
);
});
it("leaves our own images and repo files direct", async () => {
expect(await camo("")).toContain(
'src="https://tangled.org/a.png"'
);
expect(await camo("")).toContain(
'src="/ada.test/infra/raw/main/assets/a.png"'
);
});
it("does not proxy links, only what the page loads by itself", async () => {
expect(await camo("[x](https://example.com/a.png)")).toContain(
'href="https://example.com/a.png"'
);
});
});
it("links a bare handle to its profile", async () => {
const html = await render("thanks @ada.test for the fix");
expect(html).toContain('<a href="/ada.test" class="mention">@ada.test</a>');
});
it("leaves a handle inside a link label as text", async () => {
expect(await render("[ask @ada.test](https://example.com)")).not.toContain("mention");
});
it("does not read an email address as a mention", async () => {
expect(await render("mail ada@ada.test now")).not.toContain("mention");
});
it("shortens our own commit urls to a sha", async () => {
const url =
"https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8";
expect(await render(url)).toContain("<code>0c4d0e9b</code>");
});
it("leaves a commit url that has its own label", async () => {
const html = await render(
"[the fix](https://tangled.org/ada.test/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8)"
);
expect(html).toContain("the fix");
expect(html).not.toContain("<code>");
});
it("leaves another host's commit urls as urls", async () => {
const url = "https://github.com/ada/infra/commit/0c4d0e9b07940033721395a434b5873f0fb9e6c8";
expect(await render(url)).not.toContain("<code>");
});
it("renders task lists as disabled checkboxes", async () => {
const html = await 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", async () => {
const html = await 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", async () => {
const html = await render("> [!WARNING]\n> careful");
expect(html).toContain('class="markdown-alert markdown-alert-warning"');
expect(html).toContain("careful");
});
it("renders emoji shortcodes", async () => {
expect(await render("ship it :tada:")).toContain("🎉");
});
it("keeps the language on a fenced block", async () => {
expect(await render("```rust\nfn main() {}\n```")).toContain('class="language-rust"');
});
it("highlights a fenced block but keeps its language class", async () => {
const html = await render("```rust\nfn main() {}\n```");
expect(html).toContain('class="language-rust"');
expect(html).toContain("--diffs-token-");
expect(html).toContain("<span");
});
it("highlights a block that carries source lines", async () => {
const html = await render("```rust\nfn main() {}\n```", { sourceLines: true });
expect(html).toContain('data-line="0"');
expect(html).toContain("--diffs-token-");
});
it("leaves fenced blocks without a known language plain", async () => {
const unknown = await render("```nosuchlang\nhello\n```");
expect(unknown).toContain('class="language-nosuchlang"');
expect(unknown).not.toContain("--diffs-token-");
const plain = await render("```\nhello\n```");
expect(plain).not.toContain("--diffs-token-");
});
it("leaves oversized blocks plain instead of stalling ssr", async () => {
const html = await render("```rust\n" + "x".repeat(210_000) + "\n```");
expect(html).toContain('class="language-rust"');
expect(html).not.toContain("--diffs-token-");
});
it("draws a mermaid block as an svg on the server", async () => {
const html = await render("```mermaid\nflowchart LR\n A --> B\n```");
expect(html).toMatch(/^<div data-mermaid><svg [^]*<\/svg><\/div>/);
expect(html).not.toContain("<pre");
expect(html).not.toContain("--diffs-token-");
});
it("keeps the source line on a drawn mermaid block", async () => {
const html = await render("```mermaid\nflowchart LR\n A --> B\n```", {
sourceLines: true
});
expect(html).toContain('<div data-line="0" data-mermaid><svg ');
});
it("draws the same svg for the same source", async () => {
const source = "```mermaid\nsequenceDiagram\n A->>B: hi\n```";
expect(await render(source)).toBe(await render(source));
});
it("leaves a mermaid block that does not parse as a pre", async () => {
const html = await render("```mermaid\nnot a diagram --> at all\n```", {
sourceLines: true
});
expect(html).toContain(
'<pre data-line="0" class="mermaid">not a diagram --> at all\n</pre>'
);
expect(html).not.toContain("<svg");
});
it("leaves an oversized mermaid block as a pre", async () => {
const label = "x".repeat(20_000);
const html = await render(`\`\`\`mermaid\nflowchart LR\n A[${label}] --> B\n\`\`\``);
expect(html).toContain('<pre class="mermaid">');
expect(html).not.toContain("<svg");
});
it("draws only the first few mermaid blocks of a document", async () => {
const block = "```mermaid\nflowchart LR\n A --> B\n```\n\n";
const html = await render(block.repeat(12));
expect(html.match(/<svg /g)).toHaveLength(10);
expect(html.match(/<pre class="mermaid">/g)).toHaveLength(2);
});
it("stops drawing once a document's mermaid source adds up", async () => {
const label = "x".repeat(15_000);
const block = `\`\`\`mermaid\nflowchart LR\n A[${label}] --> B\n\`\`\`\n\n`;
const html = await render(block.repeat(5));
expect(html.match(/<svg /g)).toHaveLength(3);
expect(html.match(/<pre class="mermaid">/g)).toHaveLength(2);
});
it("does not let a diagram's text become markup", async () => {
const source =
'flowchart LR\n A["<script>alert(1)</script><img src=x onerror=alert(2)>"] --> B';
const html = await render("```mermaid\n" + source + "\n```");
expect(html).toContain("<svg ");
expect(html).not.toContain("<script");
expect(html).not.toContain("<img");
expect(html).not.toContain("onerror=");
});
it("does not draw a mermaid pre that carries markup of its own", async () => {
const html = await render(
'<pre class="mermaid">flowchart LR\n<b onclick="x()">A</b> --> B</pre>'
);
expect(html).not.toContain("<svg");
expect(html).not.toContain("onclick");
});
it("does not let highlighted code smuggle styles past the sanitizer", async () => {
const html = await render("```rust\nfn main() {}\n```");
expect(html).not.toContain("background-color");
expect(html).not.toContain("shiki");
});
it("strips scripts, event handlers and javascript urls", async () => {
expect(await render("<script>alert(1)</script>")).not.toContain("alert");
expect(await render('<img src="x" onerror="alert(1)">')).not.toContain("onerror");
// markdown-it refuses the destination outright, so this stays plain text
expect(await render("[x](javascript:alert(1))")).not.toContain("<a");
expect(await render('<a href="data:text/html,hi">x</a>')).not.toContain("data:");
});
it("drops classes it does not recognise", async () => {
expect(await render('<div class="fixed inset-0 bg-black">boo</div>')).not.toContain(
"inset-0"
);
});
it("drops inputs that are not task list checkboxes", async () => {
expect(await render('<input type="password" name="p">')).not.toContain("<input");
});
it("keeps the html a readme actually uses for layout", async () => {
const html = await render(
'<div align="center"><h1>infra</h1></div>\n\n<details><summary>more</summary>\n\nhidden\n\n</details>'
);
expect(html).toContain('<div align="center">');
expect(html).toContain("<details><summary>more</summary>");
});
it("tags blocks with their source line only when asked", async () => {
const source = "# Title\n\ntext\n\n- one\n- two\n\n```\ncode\n```";
const lines = await render(source, { sourceLines: true });
expect(lines).toContain('<h1 id="title" data-line="0">');
expect(lines).toContain('<p data-line="2">');
expect(lines).toContain('<li data-line="5">');
expect(lines).toContain('data-line="7"');
expect(await render(source)).not.toContain("data-line");
});
it("drops source lines written as raw html", async () => {
expect(await render('<p data-line="9">x</p>')).not.toContain("data-line");
});
});