mirror of
https://github.com/docker/docs.git
synced 2026-10-23 18:09:02 +03:00
ux: improve Mermaid rendering and theme switching (#26304)
## Summary **Problem:** Mermaid diagrams briefly showed their raw source before the SVG appeared, stayed on the wrong palette after a light/dark theme toggle, and could leave Mermaid's error graphic in the page when a diagram failed to parse. **Why this change:** Diagrams are used more often in the docs, so the renderer needs to match the site theme and avoid flashing source text while SVGs load. **Solution:** Replace `mermaid.run()` with a controlled `mermaid.render()` loop that caches each block's source, skips stale async results, re-renders when the `<html>` dark class changes, suppresses Mermaid's default error SVG, and hides `pre.mermaid` until an SVG is present (or a render error sets `data-error` so the source can still be inspected). @netlify /get-started/docker-concepts/the-basics/what-is-a-registry/ Preview: https://deploy-preview-26304--docsdocker.netlify.app/get-started/docker-concepts/the-basics/what-is-a-registry/ Example of pre-fix behavior: https://deploy-preview-26283--docsdocker.netlify.app/accounts/organization/ Generated by Cursor --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -127,4 +127,14 @@
|
||||
@apply inline-block rounded-t-sm px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-900;
|
||||
@apply dark:text-gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mermaid blocks hold the diagram source until mermaid.js replaces it with
|
||||
an SVG. Hide the source while that happens, unless rendering failed.
|
||||
|
||||
Kept outside @layer components on purpose: the element also carries the
|
||||
`flex` utility, and Tailwind's utilities layer outranks components
|
||||
regardless of selector specificity. An unlayered rule wins over both. */
|
||||
pre.mermaid:not(:has(svg)):not([data-error]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,86 @@
|
||||
import mermaid from 'mermaid'
|
||||
import mermaid from "mermaid";
|
||||
|
||||
const isDark = () => document.documentElement.classList.contains('dark')
|
||||
const SELECTOR = "pre.mermaid";
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
theme: isDark() ? 'dark' : 'default',
|
||||
})
|
||||
const isDark = () => document.documentElement.classList.contains("dark");
|
||||
|
||||
const render = () => {
|
||||
mermaid.run({ querySelector: 'pre.mermaid' })
|
||||
}
|
||||
// Rendering replaces the element content with an SVG, so the diagram source
|
||||
// is captured once per element and reused for every redraw.
|
||||
const sources = new WeakMap();
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', render)
|
||||
const getSource = (el) => {
|
||||
let source = sources.get(el);
|
||||
if (source === undefined) {
|
||||
// Same treatment mermaid.run() gives element content: decode HTML
|
||||
// entities, but keep <br/> tags because labels use them for line breaks.
|
||||
const decoder = document.createElement("textarea");
|
||||
decoder.innerHTML = el.innerHTML;
|
||||
source = decoder.value.replace(/<br\s*\/?>/gi, "<br/>").trim();
|
||||
sources.set(el, source);
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
// Incremented on every render request. A render that is no longer the latest
|
||||
// stops before writing to the page, so a slow render cannot overwrite a
|
||||
// newer theme.
|
||||
let generation = 0;
|
||||
let nextId = 0;
|
||||
let renderedDark;
|
||||
|
||||
const render = async () => {
|
||||
const current = ++generation;
|
||||
const dark = isDark();
|
||||
renderedDark = dark;
|
||||
|
||||
const elements = Array.from(document.querySelectorAll(SELECTOR));
|
||||
if (elements.length === 0) return;
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "strict",
|
||||
theme: dark ? "dark" : "default",
|
||||
// On a syntax error, render() would otherwise leave Mermaid's error
|
||||
// graphic in a temporary element at the end of <body>. The failing
|
||||
// element shows its source instead (see the catch below).
|
||||
suppressErrorRendering: true,
|
||||
});
|
||||
|
||||
for (const el of elements) {
|
||||
const source = getSource(el);
|
||||
try {
|
||||
// mermaid.render() returns the SVG as a string instead of writing it
|
||||
// into the element, so an existing diagram stays on screen until its
|
||||
// replacement is ready.
|
||||
const { svg, bindFunctions } = await mermaid.render(
|
||||
`mermaid-${nextId++}`,
|
||||
source,
|
||||
);
|
||||
if (current !== generation) return;
|
||||
el.innerHTML = svg;
|
||||
bindFunctions?.(el);
|
||||
delete el.dataset.error;
|
||||
} catch (error) {
|
||||
if (current !== generation) return;
|
||||
// Unhide the element so the source and the console error can be
|
||||
// matched up.
|
||||
el.dataset.error = "";
|
||||
console.error("Mermaid failed to render a diagram", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", render);
|
||||
} else {
|
||||
render()
|
||||
render();
|
||||
}
|
||||
|
||||
// theme.js and the header theme button both set the light/dark class on
|
||||
// <html>. Redraw with the matching Mermaid theme whenever it changes.
|
||||
new MutationObserver(() => {
|
||||
if (isDark() !== renderedDark) render();
|
||||
}).observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user