feat: add mermaid diagram rendering support#5269
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces first-class Mermaid diagram support to ReSpec by adding a new core/diagrams coordinator that detects pre.mermaid blocks, lazy-loads a Mermaid runtime bundle, renders diagrams to SVG, and provides a flip-card UI to view/copy the underlying source, plus linting/styling/test coverage updates.
Changes:
- Added Mermaid rendering pipeline (runtime bundle + coordinator + renderer + runtime flip handler) and associated CSS for interactive diagrams.
- Updated highlighting to exclude Mermaid source blocks and added a new linter rule for diagram placement.
- Added integration tests and wired the new modules into the W3C profile + build/dependency graph.
Reviewed changes
Copilot reviewed 11 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| worker/rollup.config.js | Adds a separate Rollup output to build a dedicated Mermaid runtime bundle. |
| worker/respec-mermaid.js | Implements the Mermaid runtime wrapper (initialize, render) and exposes it on self. |
| src/core/diagrams.js | New coordinator that loads Mermaid, renders SVG, builds UI/error views, injects runtime, and emits warnings/errors. |
| src/core/diagrams/mermaid.js | New “pure renderer” that delegates rendering to the injected runtime. |
| src/core/diagrams-runtime.js | New exported runtime script that wires up flip-button behavior in exported documents. |
| src/styles/diagrams.css.js | New styles for diagram flip-card UI, error presentation, reduced-motion, dark mode, and print. |
| src/core/highlight.js | Excludes .mermaid blocks from syntax highlighting selection. |
| src/core/linter-rules/no-uncaptioned-diagram.js | New linter rule to warn about Mermaid/Jake diagrams outside figures. |
| profiles/w3c.js | Ensures core/diagrams and the new linter rule run in the W3C profile. |
| tests/spec/core/diagrams-spec.js | Adds integration tests for Mermaid rendering, toolbar presence, errors, config, and highlighting behavior. |
| package.json | Adds mermaid dependency. |
| pnpm-lock.yaml | Locks Mermaid and transitive dependencies. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ${copyBtn} | ||
| </header> | ||
| <div class="diagram-flip"> | ||
| <div class="diagram-face diagram-face--front">${{ html: svg }}</div> | ||
| <div class="diagram-face diagram-face--back"> | ||
| <pre><code class="mermaid-source">${source}</code></pre> |
| ${copyBtn} | ||
| </header> | ||
| <div class="diagram-flip"> | ||
| <div class="diagram-face diagram-face--front diagram-error-front"> | ||
| ${l10n.diagram_error} | ||
| </div> | ||
| <div class="diagram-face diagram-face--back"> | ||
| <pre class="nohighlight">${numberedSource}</pre> |
| <div class="diagram-flip"> | ||
| <div class="diagram-face diagram-face--front">${{ html: svg }}</div> | ||
| <div class="diagram-face diagram-face--back"> | ||
| <pre><code class="mermaid-source">${source}</code></pre> |
| /** @type {NodeListOf<HTMLElement>} */ | ||
| const diagrams = document.querySelectorAll("pre.mermaid, pre.jake-diagram"); | ||
|
|
||
| const offendingElements = [...diagrams].filter(pre => !pre.closest("figure")); | ||
|
|
| const figure = pre.closest("figure"); | ||
|
|
||
| if (!figure) { | ||
| showWarning(l10n.msg_no_figure, name, { | ||
| hint: l10n.hint_no_figure, | ||
| elements: [pre], | ||
| }); | ||
| } | ||
|
|
|
|
||
| if (!figure) { | ||
| showWarning(l10n.msg_no_figure, name, { | ||
| hint: l10n.hint_no_figure, | ||
| elements: [pre], | ||
| }); | ||
| } | ||
|
|
||
| const figcaption = figure?.querySelector("figcaption") ?? null; | ||
|
|
| /** | ||
| * @param {Conf} conf | ||
| */ | ||
| export function run(conf) { | ||
| // @ts-expect-error -- LintConfig can be false | ||
| if (!conf.lint?.[ruleName]) { | ||
| return; | ||
| } | ||
|
|
||
| /** @type {NodeListOf<HTMLElement>} */ | ||
| const diagrams = document.querySelectorAll("pre.mermaid, pre.jake-diagram"); | ||
|
|
||
| const offendingElements = [...diagrams].filter(pre => !pre.closest("figure")); | ||
|
|
||
| if (!offendingElements.length) return; | ||
|
|
||
| showWarning(l10n.msg, name, { | ||
| hint: l10n.hint, | ||
| elements: offendingElements, | ||
| }); | ||
| } |
| describe("Highlight exclusion", () => { | ||
| it("does not syntax-highlight the original pre.mermaid", async () => { | ||
| const body = ` | ||
| <figure id="fig-no-hl"> | ||
| <pre class="mermaid"> | ||
| flowchart LR | ||
| A --> B | ||
| </pre> | ||
| <figcaption>No highlight</figcaption> | ||
| </figure> | ||
| `; | ||
| const ops = makeStandardOps(null, body); | ||
| const doc = await makeRSDoc(ops); | ||
| const hljs = doc.querySelector("pre.mermaid code.hljs"); | ||
| expect(hljs).toBeNull(); | ||
| }); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 18 changed files in this pull request and generated 3 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { promise, resolve, reject } = | ||
| /** @type {PromiseWithResolvers<void>} */ (Promise.withResolvers()); | ||
| const script = document.createElement("script"); | ||
| script.src = url; | ||
| script.onload = () => { | ||
| script.remove(); | ||
| resolve(); | ||
| }; | ||
| script.onerror = event => { | ||
| script.remove(); | ||
| reject(new Error(`Failed to load: ${url}`, { cause: event })); | ||
| }; | ||
| document.head.append(script); | ||
| return promise; |
There was a problem hiding this comment.
Fixed in 8bbda05: replaced Promise.withResolvers() with new Promise() for broader browser compatibility.
| import("../src/core/best-practices.js"), | ||
| import("../src/core/figures.js"), | ||
| import("../src/core/linter-rules/no-uncaptioned-diagram.js"), | ||
| import("../src/core/diagrams.js"), | ||
| import("../src/core/tables.js"), |
There was a problem hiding this comment.
Fixed in 8bbda05: moved the caption check into core/diagrams itself (where it runs before rendering replaces the <pre> nodes). The standalone linter rule file is removed, restoring the 'linters last' convention in the profile.
| /** @type {NodeListOf<HTMLElement>} */ | ||
| const diagrams = document.querySelectorAll("pre.mermaid, pre.jake-diagram"); | ||
|
|
||
| const offendingElements = [...diagrams].filter(pre => { | ||
| const figure = pre.closest("figure"); | ||
| return !figure || !figure.querySelector("figcaption"); | ||
| }); |
There was a problem hiding this comment.
Fixed in 8bbda05: the standalone linter rule was removed. The caption check now lives inside core/diagrams and runs before rendering replaces the original <pre> elements. Also removed pre.jake-diagram (not yet supported).
4f54e6a to
f60ebad
Compare
Adds native Mermaid diagram support to ReSpec. Authors write <pre class="mermaid"> inside a <figure>, and ReSpec renders SVG diagrams with a hover-reveal toolbar and 3D flip card for viewing source code. Features: - Lazy-loads mermaid from a separate build chunk (zero cost if unused) - Hover-reveal toolbar: 'Mermaid' label + flip button + copy button - 3D flip card animation (classic CSS preserve-3d pattern) - Copy button uses shared clipboard.js (matches WebIDL/CDDL style) - Error display: red toolbar, source shown by default (flipped state), line-numbered source with CSS Grid, error line highlighted with emoji + red border, parser pointer shown inline - Accessibility: aria-expanded toggles on flip, aria-label updates, visibility:hidden when header is opacity:0, prefers-reduced-motion uses opacity crossfade instead of 3D flip, error-pulse suppressed - Security: htmlLabels:false eliminates foreignObject attack surface, securityLevel:strict with DOMPurify 3.3.1 sanitization - CSS: all colors via custom properties, dark mode, RTL-safe logical properties, WCAG AAA contrast on header, min(20em, 100%) responsive - Figure integration with automatic numbering - i18n support (en, ja, ko, nl, es, fr) - Touch device support (toolbar always visible when hover:none) - Theme configurable via respecConfig.mermaid.theme (default: neutral) - Linter rule: no-uncaptioned-diagram (checks figure + figcaption) - Promise.withResolvers() in loadScript - Promise.allSettled for parallel diagram rendering
f60ebad to
806a3a3
Compare
Move the caption check from a standalone linter rule into the diagrams module itself, where it runs before rendering replaces the pre nodes. This fixes the profile ordering issue (linter rules must be last) and replaces Promise.withResolvers() with new Promise() for browser compat.
Summary
<pre class="mermaid">blocks inside<figure>elements</>), and clipboard copyprefers-reduced-motion, touch/mobile, and printDetails
respecConfig.mermaid.theme(default:neutral)<figure>with<figcaption>prefers-reduced-motioninstead of instant 3D flipTest plan
pnpm build:w3c)