The transcript rendered whatever the model wrote as markdown, and three
things a model writes are not markdown the renderer draws.
LaTeX. Vicinae has no maths block, so `$x^2$` reached the pane as
literal TeX. `latex.ts` converts the subset models actually write into
inline markup the pane can draw: `sup`, `sub`, `b` and `i` are available
because `MdParagraph.qml` is a rich text `TextEdit` and
`markdown-model.cpp` passes `CMARK_NODE_HTML_INLINE` through untouched.
It is a linearisation rather than a typesetter, because the pane is a
launcher-width column: `\frac{a}{b}` becomes `a/b` and `\sqrt{x+1}`
becomes `√(x+1)`, with a Unicode script wherever one exists. A lone `$`
pair only counts when the inside looks like maths, so `$5 and $10` and
`cd $A; echo $B` are left as written.
Custom HTML. Raw HTML is the only route past GFM, and it is also a hole,
so `html.ts` filters it: the tags and style declarations the renderer
acts on are kept, `script` and `style` go with their content, an event
handler, a `url()` in a style value and a `javascript:` destination are
dropped, and a tag that is not on the list is shown as text rather than
swallowed. An `<a href>` becomes a markdown link, the only form Vicinae
makes clickable. A well-formed HTML block is moved into a paragraph,
because the block path keeps only the words of one.
Everything else. `code.ts` now fences a paragraph drawn with box
characters, since a paragraph wraps and a diagram that reflows says
nothing, and renames a fence label KSyntaxHighlighting would print but
never resolve.
The pass lives in `render.ts`, is plain TypeScript with no Vicinae
imports, and the default system prompt now says what the window draws.
82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
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("<foo>bar</foo>");
|
|
});
|
|
|
|
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 `&`.
|
|
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");
|
|
});
|
|
});
|