Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions rivet-core/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,15 @@ pub fn render_markdown(input: &str) -> String {
in_mermaid = false;
Some(Event::Html(MERMAID_CLOSE.into()))
}
// Inside a mermaid block, pass text through as-is (pulldown-cmark
// emits the fenced body as Event::Text segments).
Event::Text(_) if in_mermaid => Some(event),
// Inside a mermaid block, emit the fenced body verbatim. pulldown-cmark
// delivers it as Event::Text segments, but `html::push_html` HTML-entity-
// escapes Text events — turning mermaid's core `-->` arrow into `-->`
// and `<|--` into `&lt;|--`, which mermaid.js then rejects with "Syntax
// error in text". Re-tag the segments as Event::Html so they pass through
// unescaped (mermaid diagram source contains no HTML tags; a `</pre>`
// smuggled inside a ```mermaid block would at worst end the block early,
// and `<script>` etc. are still stripped by sanitize_html below).
Event::Text(t) if in_mermaid => Some(Event::Html(t)),
// Drop all other raw HTML events for XSS defence.
Event::Html(_) | Event::InlineHtml(_) => None,
other => Some(other),
Expand Down Expand Up @@ -444,4 +450,42 @@ mod tests {
"sentinel label must not leak, got: {html}"
);
}

// Regression: mermaid body must NOT be HTML-entity-escaped. `-->` is core
// mermaid syntax (flowchart/stateDiagram arrows); if it renders as `--&gt;`
// mermaid.js fails with "Syntax error in text". Reported against v0.6.0/v0.8.0.
// rivet: verifies REQ-032
#[test]
fn mermaid_body_not_html_escaped() {
let input = "```mermaid\nstateDiagram-v2\n [*] --> init_mode\n init_mode --> run_mode : wake\n```";
let html = render_markdown(input);
assert!(
html.contains("[*] --> init_mode"),
"arrow must survive verbatim, not as --&gt;, got: {html}"
);
assert!(
html.contains("init_mode --> run_mode : wake"),
"arrow must survive verbatim, got: {html}"
);
assert!(
!html.contains("--&gt;"),
"mermaid body must not be HTML-entity-escaped, got: {html}"
);
// The whole diagram is one logical block inside <pre class="mermaid">,
// newlines preserved, no paragraph splitting.
assert!(
html.contains("<pre class=\"mermaid\">stateDiagram-v2"),
"got: {html}"
);
}

// Class-diagram arrows use `<` and `>` too (`<|--`, `*--`, `o--`); those
// must survive verbatim as well.
// rivet: verifies REQ-032
#[test]
fn mermaid_class_diagram_arrows_not_escaped() {
let html = render_markdown("```mermaid\nclassDiagram\n Animal <|-- Dog\n```");
assert!(html.contains("Animal <|-- Dog"), "got: {html}");
assert!(!html.contains("&lt;|--"), "got: {html}");
}
}
48 changes: 48 additions & 0 deletions tests/playwright/mermaid.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test, expect } from "@playwright/test";

// Regression coverage for the fenced-mermaid HTML-escaping bug (reported
// against v0.6.0 and v0.8.0): `render_markdown` passed the body of a
// ```mermaid fenced block through pulldown-cmark's `html::push_html`, which
// HTML-entity-escapes Text events — turning mermaid's core `-->` arrow into
// `--&gt;` (and class-diagram `<|--` into `&lt;|--`). mermaid.js then failed
// with "Syntax error in text" and the diagram never rendered.
//
// ARCH-CORE-001 in artifacts/architecture.yaml has a real ```mermaid
// `flowchart LR` with `-->` arrows in its description, so it doubles as the
// end-to-end fixture.

test.describe("Mermaid diagram rendering", () => {
test("fenced mermaid body is emitted verbatim — arrows not HTML-escaped", async ({
request,
}) => {
// Raw HTML response: no JS runs, so mermaid.js hasn't transformed the
// <pre> yet — we see exactly what render_markdown produced.
const res = await request.get("/artifacts/ARCH-CORE-001");
expect(res.status()).toBe(200);
const html = await res.text();
expect(html).toContain('<pre class="mermaid">');
// The flowchart arrow must survive verbatim.
expect(html).toContain("Config --> Store");
// ...and must NOT be entity-escaped anywhere in the page.
expect(html).not.toContain("--&gt;");
});

test("mermaid.js renders the diagram to SVG with no syntax error", async ({
page,
}) => {
await page.goto("/artifacts/ARCH-CORE-001");

// mermaid (startOnLoad) parses the .mermaid element's text and replaces
// it with an inline <svg>. mermaid SVGs carry an aria-roledescription
// like "flowchart-v2"; matching either that or `.mermaid svg` is robust
// across the wrapper shape mermaid uses.
const rendered = page
.locator(".mermaid svg, svg[aria-roledescription]")
.first();
await expect(rendered).toBeVisible({ timeout: 15_000 });

// On a parse failure mermaid injects a visible "Syntax error in text"
// box instead of the diagram — assert that never happens.
await expect(page.locator("body")).not.toContainText("Syntax error");
});
});
Loading