Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | 6x 6x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x | import {useEffect, useState} from 'react';
import {useDarkMode} from '../../hooks/useDarkMode.js';
import type {IDiagramRendererProps} from './rendererRegistry.js';
/**
* Mermaid, rendered in the browser.
*
* Three properties are deliberate:
*
* - **Only its own text is trusted, and even that is sanitised.** Mermaid runs
* with `securityLevel: 'strict'`, and the labels arriving here have already
* been made label-safe where they were written (see the service's renderer).
* A diagram drawn from another process's messages is not a place to be
* relaxed about markup.
* - **A diagram that cannot be drawn is shown, not swallowed.** Mermaid throws
* on a notation it does not recognise; the answer is the text as it came,
* because a broken diagram the reader can see is evidence and a blank panel is
* not.
* - **Mermaid is loaded when a diagram is drawn, not when the library is
* imported.** It is a large parser for a feature most pages never show, so it
* stays out of the bundle until something needs it.
* - **Every render attempt gets its own element id.** Mermaid draws into an element
* it finds by id, so a second attempt that reuses the first one's id draws into the
* element React is already showing — and it resolves with an *empty* svg, which the
* setState below then wrote over a perfectly good diagram: a capture-time race that
* produced a zero-height element carrying the text and none of the drawing (F-202).
* The dependency list can re-run the effect (a theme change does), so "once per
* component instance" was not the same thing as "once per render".
*/
let initialised = false;
let sequence = 0;
/** What has been drawn for one diagram, or how drawing it failed. */
interface IRenderState {
diagram: string;
svg?: string;
failed?: boolean;
}
export default function MermaidRenderer({diagram, className}: IDiagramRendererProps) {
const dark = useDarkMode();
const [state, setState] = useState<IRenderState>(() => ({diagram}));
useEffect(() => {
let cancelled = false;
void (async (): Promise<void> => {
try {
const mermaid = (await import('mermaid')).default;
if (!initialised) {
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme: dark ? 'dark' : 'neutral',
});
initialised = true;
}
const {svg} = await mermaid.render(`blong-diagram-${(sequence += 1)}`, diagram);
if (cancelled) return;
// A render that answers with nothing is a failed render, and it is said
// so rather than written over the drawing: an element carrying the text
// and no svg is a diagram that looks captured and is empty (F-191).
setState(svg.length > 0 ? {diagram, svg} : {diagram, failed: true});
} catch (error) {
// Said out loud, not only drawn. The fallback shows the reader the
// text; this line tells whoever has to fix it *why* mermaid refused —
// a chunk that never loaded, a notation it does not know, a render
// that threw. Without it, a failed diagram and a pending one are both
// "an element that never appears", which is how a page that renders
// nothing costs an afternoon.
console.error('[blong] diagram render failed', {diagram, error});
if (!cancelled) setState({diagram, failed: true});
}
})();
return () => {
cancelled = true;
};
}, [diagram, dark]);
// Derived rather than reset in the effect: a diagram the state does not
// describe is pending, whatever the state says about the previous one, so a
// change of diagram needs no state of its own and no extra render pass.
const current: IRenderState = state.diagram === diagram ? state : {diagram};
Eif (current.failed || diagram.length === 0) {
return (
<pre className={className} data-diagram-fallback="true">
{diagram}
</pre>
);
}
if (current.svg === undefined) {
return <div className={className} data-diagram-pending="true" />;
}
return (
<div
className={className}
data-diagram-rendered="true"
// The source text stays reachable, because the two artifacts a diagram
// is worth are the picture and the text: a spec can screenshot the
// drawing and still write the mermaid it was drawn from, which an
// injected SVG no longer contains.
data-diagram-text={diagram}
// Safe by construction, not by assumption: mermaid renders with
// `securityLevel: 'strict'`, which sanitises the SVG it produces, and
// the labels in it were sanitised where they were written.
// cspell:disable-next-line -- the rule's name is not a word
// eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml
dangerouslySetInnerHTML={{__html: current.svg}}
/>
);
}
|