mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
markdown-it's typographer option rewrites characters after parsing, so a literal (c) came out as ©. Agents label steps and options (a), (b), (c) all the time, and an ordinary list rendering as (a), (b), © stops your eye every time. The same option turns ... into an ellipsis, --- into an em dash, and straight quotes into curly ones. Agent output is text people paste back into a shell or a file, so a curly quote in --name="my repo" looks correct on screen and fails when pasted. The markdown file preview had the same problem, showing characters that are not in the file on disk. Every in-app markdown surface now goes through one factory in utils/markdown-parser.ts, which leaves typographer off: agent chat, plan cards, pull request comment bodies and the file preview. The plan card was rendering through its library's built-in parser because it never passed a markdownit prop, and that default has typographer on. Link behaviour is unchanged. The assistant parser keeps its file:// permission and no other surface gains it, and linkify stays on for chat and the default renderer and off for plan cards, matching what each did before. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createMarkdownParser } from "./markdown-parser";
|
|
|
|
// Every string markdown-it's typographer would rewrite, with the character it
|
|
// would rewrite it to. Sourced from markdown-it/lib/rules_core/replacements.js
|
|
// and smartquotes.js. `--flag` is deliberately absent: the en-dash rules need
|
|
// whitespace or a word character on both sides, so a CLI flag after a space is
|
|
// never touched and asserting on it would prove nothing.
|
|
const REWRITTEN_BY_TYPOGRAPHER = [
|
|
"(c)",
|
|
"(C)",
|
|
"(r)",
|
|
"(R)",
|
|
"(tm)",
|
|
"(TM)",
|
|
"(p)",
|
|
"(P)",
|
|
"+-",
|
|
"two dots .. here",
|
|
"wait for it...",
|
|
"what????",
|
|
"stop!!!!",
|
|
"hmm,, ok",
|
|
"a -- b",
|
|
"word--word",
|
|
"a --- b",
|
|
'run --name="my repo"',
|
|
"it's fine",
|
|
];
|
|
|
|
describe("createMarkdownParser", () => {
|
|
it("renders every typographer-rewritten sequence verbatim", () => {
|
|
const parser = createMarkdownParser({ linkify: true });
|
|
|
|
for (const source of REWRITTEN_BY_TYPOGRAPHER) {
|
|
expect(parser.renderInline(source)).toBe(escapeHtml(source));
|
|
}
|
|
});
|
|
|
|
it("would fail if typographer were switched back on", () => {
|
|
// Guards the assertion above: proves the fixture actually exercises the
|
|
// rules, so a future refactor can't quietly make the test vacuous.
|
|
const typographer = createMarkdownParser({ linkify: true });
|
|
typographer.set({ typographer: true });
|
|
|
|
for (const source of REWRITTEN_BY_TYPOGRAPHER) {
|
|
expect(typographer.renderInline(source)).not.toBe(escapeHtml(source));
|
|
}
|
|
});
|
|
|
|
it("rejects file:// links", () => {
|
|
const parser = createMarkdownParser({ linkify: true });
|
|
|
|
expect(parser.render("[open](file:///tmp/a.ts)")).not.toContain("href");
|
|
});
|
|
|
|
it("rejects javascript: links", () => {
|
|
const parser = createMarkdownParser({ linkify: true });
|
|
|
|
expect(parser.render("[x](javascript:alert(1))")).not.toContain("href");
|
|
});
|
|
|
|
it("linkifies bare URLs only when asked", () => {
|
|
expect(createMarkdownParser({ linkify: true }).render("see https://paseo.sh now")).toContain(
|
|
'href="https://paseo.sh"',
|
|
);
|
|
expect(
|
|
createMarkdownParser({ linkify: false }).render("see https://paseo.sh now"),
|
|
).not.toContain("href");
|
|
});
|
|
});
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """);
|
|
}
|