feat: render LaTeX, custom HTML and diagrams in the transcript (#19)

This commit was merged in pull request #19.
This commit is contained in:
2026-10-07 07:26:40 +00:00
parent 550a2bbefe
commit a804afd56a
12 changed files with 2543 additions and 30 deletions
+81
View File
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import { sanitizeInlineHtml } from "../lib/html";
describe("sanitizeInlineHtml", () => {
it("keeps the tags a rich text paragraph draws", () => {
expect(sanitizeInlineHtml("<b>bold</b> and <i>italic</i>")).toBe("<b>bold</b> and <i>italic</i>");
expect(sanitizeInlineHtml("x<sup>2</sup> and a<sub>i</sub>")).toBe("x<sup>2</sup> and a<sub>i</sub>");
});
it("keeps a style declaration it can use and drops the rest", () => {
const input = '<span style="color:#ff0000;font-weight:bold;position:absolute">x</span>';
expect(sanitizeInlineHtml(input)).toBe('<span style="color:#ff0000;font-weight:bold">x</span>');
});
it("drops a style value that reaches outside the document", () => {
expect(sanitizeInlineHtml('<span style="background:url(https://x/y.png)">x</span>')).toBe(
"<span>x</span>",
);
expect(sanitizeInlineHtml('<span style="background:#fff">x</span>')).toBe(
'<span style="background:#fff">x</span>',
);
});
it("drops an event handler", () => {
expect(sanitizeInlineHtml('<span onclick="steal()">x</span>')).toBe("<span>x</span>");
});
it("removes a script with the code inside it", () => {
expect(sanitizeInlineHtml("<script>alert(1)</script>ok")).toBe("ok");
expect(sanitizeInlineHtml("<style>p{color:red}</style>ok")).toBe("ok");
expect(sanitizeInlineHtml('<iframe src="https://evil.example"></iframe>ok')).toBe("ok");
expect(sanitizeInlineHtml("<script>alert(1)")).toBe("");
});
it("removes an element with no content of its own, and only the tag", () => {
expect(sanitizeInlineHtml("<input type=text>still here")).toBe("still here");
expect(sanitizeInlineHtml('<meta charset="utf-8">still here')).toBe("still here");
expect(sanitizeInlineHtml("a<br>b")).toBe("a<br>b");
});
it("shows a tag it cannot draw rather than swallowing it", () => {
expect(sanitizeInlineHtml("<foo>bar</foo>")).toBe("&lt;foo&gt;bar&lt;/foo&gt;");
});
it("turns an anchor into the one form that is clickable", () => {
expect(sanitizeInlineHtml('See <a href="https://example.com">the docs</a>.')).toBe(
"See [the docs](https://example.com).",
);
// The destination keeps its query string rather than arriving as `&amp;`.
expect(sanitizeInlineHtml('<a href="https://x/?a=1&b=2">q</a>')).toBe("[q](https://x/?a=1&b=2)");
});
it("keeps the text of an anchor that points somewhere unsafe", () => {
expect(sanitizeInlineHtml('<a href="javascript:alert(1)">click</a>')).toBe("click");
});
it("filters an image source and leaves the tag itself where it is", () => {
// The one place an image written as HTML is loaded for real is the extractor
// for an HTML block, so the tag stays and only its source is judged.
expect(sanitizeInlineHtml('<img src="https://x/y.png" alt="a chart">')).toBe(
'<img src="https://x/y.png" alt="a chart">',
);
expect(sanitizeInlineHtml('<img src="file:///tmp/a.png">')).toBe('<img src="file:///tmp/a.png">');
expect(sanitizeInlineHtml('<img src="javascript:alert(1)">')).toBe("<img>");
expect(sanitizeInlineHtml('<img src="data:image/svg+xml;base64,PHN2Zy8+">')).toBe(
'<img src="data:image/svg+xml;base64,PHN2Zy8+">',
);
});
it("leaves a backtick span exactly as written", () => {
expect(sanitizeInlineHtml("Use `<b>bold</b>` for that.")).toBe("Use `<b>bold</b>` for that.");
});
it("removes a comment", () => {
expect(sanitizeInlineHtml("a<!-- hidden -->b")).toBe("ab");
});
it("leaves a less-than sign that is not a tag alone", () => {
expect(sanitizeInlineHtml("a < b and 3>2")).toBe("a < b and 3>2");
});
});