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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 thread src/core/diagrams.js
Comment thread src/core/diagrams.js Outdated
Comment thread src/core/linter-rules/no-uncaptioned-diagram.js Outdated
Comment thread src/core/diagrams.js Outdated
Comment thread src/core/diagrams.js
Comment thread src/core/linter-rules/no-uncaptioned-diagram.js Outdated
Comment thread tests/spec/core/diagrams-spec.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 thread profiles/w3c.js
Comment thread src/core/linter-rules/no-uncaptioned-diagram.js Outdated
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.
Comment thread src/core/clipboard.js
export function createCopyButton(headerSelector, title = "Copy to clipboard") {
export function createCopyButton(
headerSelector,
title = "Copy to clipboard",

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.

We should probably localize this.

Comment thread src/core/clipboard.js
}
button.addEventListener("click", () => {
const pre = button.closest("pre");
const containerSel = button.dataset.copyContainer;

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.

Nit: avoid abbreviations in variable names.

Comment thread src/core/clipboard.js
document.querySelectorAll(".respec-button-copy-paste").forEach(function(btn) {
btn.addEventListener("click", function() {
var pre = this.closest("pre");
var containerSel = this.dataset.copyContainer;

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.

nit: use let or const where possible (applies throughout this PR.

document.querySelectorAll(".diagram-flip-btn").forEach(function(btn) {
btn.addEventListener("click", function(e) {
e.stopPropagation();
var container = btn.closest(".diagram-container");

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.

nit: use let or const. Same with other vars.

Comment thread src/core/diagrams.js
// @ts-ignore
if (self.respecMermaid) return true;
} catch {
// fallback to next URL

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.

Nit: probably console.warn here so we can at least see what's failing.

Comment thread src/core/diagrams.js
};
script.onerror = event => {
script.remove();
reject(new Error(`Failed to load: ${url}`, { cause: event }));

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.

Should this be event or event.error?

Comment thread src/core/diagrams.js
</button>
${copyBtn}
</header>
<div class="diagram-flip">

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.

Nit: can we use more semantic markup here?

@@ -0,0 +1,329 @@
/* --- Diagrams --- */

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.

Can this file CSS be improved, consolidated, or simplified in any way?

Comment thread worker/respec-mermaid.js
* @param {{ id: string }} options
* @returns {Promise<{ svg: string } | { error: string }>}
*/
async function render(source, { id }) {

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.

Is there a less ugly way to do this? What is here makes sense, but seems like a bit of a hack.

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