Q145: chat messages render GitHub-flavored markdown with code blocks, tables and lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a89d1e883
commit
8a06f2ff77
6 files changed
+1086
-39
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { MessageMarkdown } from "../../src/renderer/src/pages/chat/MessageMarkdown";
|
||||
|
||||
const render = (body: string, mentions: Array<{ start: number; end: number }> = []) => renderToStaticMarkup(createElement(MessageMarkdown, {
|
||||
body,
|
||||
mentions,
|
||||
renderMention: (index: number) => createElement("b", { "data-chip": index }, "CHIP"),
|
||||
}));
|
||||
|
||||
describe("message markdown", () => {
|
||||
test("renders code blocks, tables and nested lists instead of raw markup", () => {
|
||||
const html = render("Plan:\n\n```ts\nconst a = 1;\n```\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\n- one\n - nested\n- two");
|
||||
expect(html).toContain("<pre");
|
||||
expect(html).toContain("const a = 1;");
|
||||
expect(html).toContain("<table");
|
||||
expect(html).toContain("<td>2</td>");
|
||||
expect(html).toMatch(/<li>one\s*<ul[^>]*>\s*<li>nested<\/li>/);
|
||||
expect(html).not.toContain("```");
|
||||
});
|
||||
|
||||
test("keeps line breaks, drops raw HTML and images, and opens links outside", () => {
|
||||
const html = render("line one\nline two <script>alert(1)</script>  [site](https://e.com)");
|
||||
expect(html).toContain("line one\nline two");
|
||||
expect(html).not.toContain("<script");
|
||||
expect(html).not.toContain("<img");
|
||||
expect(html).toContain('href="https://e.com"');
|
||||
});
|
||||
|
||||
test("turns mention ranges into chips, but not inside code", () => {
|
||||
const body = "Hi @Dana see `@Dana` and **@Dana**";
|
||||
const at = (from: number) => ({ start: body.indexOf("@Dana", from), end: body.indexOf("@Dana", from) + 5 });
|
||||
const first = at(0);
|
||||
const inCode = at(first.end);
|
||||
const bold = at(inCode.end);
|
||||
const html = render(body, [first, inCode, bold]);
|
||||
expect(html.match(/CHIP/g)).toHaveLength(2);
|
||||
expect(html).toContain("<code");
|
||||
expect(html).toMatch(/<code[^>]*>@Dana<\/code>/);
|
||||
expect(html).toMatch(/<strong><b data-chip="2">CHIP<\/b><\/strong>/);
|
||||
});
|
||||
|
||||
test("finds mentions in text that entities or escapes changed", () => {
|
||||
for (const body of ["Use & @Dana now", "Stars \\* and @Dana"]) {
|
||||
const start = body.indexOf("@Dana");
|
||||
expect(render(body, [{ start, end: start + 5 }]).match(/CHIP/g)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("chips the mention the server resolved, not a look-alike copy before it", () => {
|
||||
for (const body of ["@Dana and @Dana", "\\@Dana and @Dana", "x@Dana and @Dana"]) {
|
||||
const start = body.lastIndexOf("@Dana");
|
||||
const html = render(body, [{ start, end: start + 5 }]);
|
||||
expect(html.match(/CHIP/g)).toHaveLength(1);
|
||||
expect(html).toMatch(/@Dana and <b data-chip="0">CHIP<\/b>/);
|
||||
}
|
||||
});
|
||||
|
||||
test("a mention inside a link URL stays the URL's text", () => {
|
||||
const body = "[profile](https://e.com/@Dana)";
|
||||
const start = body.indexOf("@Dana");
|
||||
expect(render(body, [{ start, end: start + 5 }])).toContain('href="https://e.com/@Dana"');
|
||||
});
|
||||
|
||||
test("an entity in the body cannot forge a mention chip", () => {
|
||||
for (const forged of ["0", "@0"]) {
|
||||
expect(render(`Hello ${forged}`).match(/CHIP/g)).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user