Skip to content

feat: add mermaid diagram rendering support#5269

Draft
marcoscaceres wants to merge 2 commits into
speced:mainfrom
marcoscaceres:feat/diagrams
Draft

feat: add mermaid diagram rendering support#5269
marcoscaceres wants to merge 2 commits into
speced:mainfrom
marcoscaceres:feat/diagrams

Conversation

@marcoscaceres
Copy link
Copy Markdown
Contributor

Summary

  • Adds mermaid diagram rendering for <pre class="mermaid"> blocks inside <figure> elements
  • Lazy-loads mermaid from a separate build chunk (zero cost if unused)
  • Flip-card UI: hover reveals toolbar with "Mermaid" label, source toggle (</>), and clipboard copy
  • Error display: red-themed card with line-numbered source, inline error pointer, and ReSpec pill reporting
  • Supports i18n (6 languages), dark mode, prefers-reduced-motion, touch/mobile, and print

Details

  • Theme configurable via respecConfig.mermaid.theme (default: neutral)
  • Warns if diagram is not wrapped in a <figure> with <figcaption>
  • Shares clipboard infrastructure with WebIDL/CDDL copy buttons
  • Uses hyperHTML tagged templates for DOM construction (consistent with codebase)
  • CSS uses show/hide for prefers-reduced-motion instead of instant 3D flip
  • Print: hides toolbar and back face, neutralizes 3D transforms

Test plan

  • 11 new integration tests covering rendering, toolbar, source preservation, errors, theme config, linting, highlight exclusion, and runtime injection
  • Full test suite passes (1085 tests, 0 failures)
  • Visual verification in Safari: diagrams, flip animation, error display, error source with line highlighting
  • Build succeeds (pnpm build:w3c)
  • ESLint + Prettier pass (pre-commit hooks)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/core/diagrams.js Outdated
Comment on lines +143 to +148
${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>
Comment thread src/core/diagrams.js
Comment on lines +248 to +255
${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>
Comment thread src/core/diagrams.js Outdated
<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>
Comment on lines +50 to +54
/** @type {NodeListOf<HTMLElement>} */
const diagrams = document.querySelectorAll("pre.mermaid, pre.jake-diagram");

const offendingElements = [...diagrams].filter(pre => !pre.closest("figure"));

Comment thread src/core/diagrams.js Outdated
Comment on lines +293 to +301
const figure = pre.closest("figure");

if (!figure) {
showWarning(l10n.msg_no_figure, name, {
hint: l10n.hint_no_figure,
elements: [pre],
});
}

Comment thread src/core/diagrams.js
Comment on lines +294 to +303

if (!figure) {
showWarning(l10n.msg_no_figure, name, {
hint: l10n.hint_no_figure,
elements: [pre],
});
}

const figcaption = figure?.querySelector("figcaption") ?? null;

Comment on lines +41 to +61
/**
* @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,
});
}
Comment thread tests/spec/core/diagrams-spec.js Outdated
Comment on lines +157 to +172
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();
});
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/core/diagrams.js Outdated
Comment on lines +92 to +105
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;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8bbda05: replaced Promise.withResolvers() with new Promise() for broader browser compatibility.

Comment thread profiles/w3c.js
Comment on lines 28 to 32
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"),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +50 to +56
/** @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");
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants