1// Docusaurus's Mermaid component, swizzled so diagrams are drawn in 012's2// palette: Mermaid's base theme, its variables read from the colors3// custom.css defines for the color mode shown, so the palette has one4// home. Mermaid needs literal colors, not CSS variables, which is why5// they are read from the page rather than passed through. It draws with6// its own hook rather than Docusaurus's, which can leave a diagram's box7// empty when the color mode changes quickly (see draw).89import React, {useEffect, useMemo, useRef, useState, type ReactNode} from 'react';10import ErrorBoundary from '@docusaurus/ErrorBoundary';11import {ErrorBoundaryErrorMessageFallback, useColorMode} from '@docusaurus/theme-common';12import {MermaidContainerClassName} from '@docusaurus/theme-mermaid/client';13import {loadMermaid} from '@docusaurus/theme-mermaid/lib/client/loadMermaid.js';14import type {MermaidConfig, RenderResult} from 'mermaid';1516type Props = {value: string};1718// The custom properties the diagrams are drawn with, as custom.css19// names them without the leading dashes.20const vars = [21 'ansi-black',22 'ansi-red',23 'ansi-green',24 'ansi-yellow',25 'ansi-blue',26 'ansi-magenta',27 'ansi-cyan',28 'ansi-bright-black',29 'term-bg',30 'term-fg',31 'role-accent-bg',32 'role-accent-fg',33 'role-header-bg',34 'role-header-fg',35 'role-muted',36 'role-frame',37 'role-grid',38 'role-panel-bg',39 'font-mono',40] as const;4142type Palette = Record<(typeof vars)[number], string>;4344function readPalette(): Palette {45 const style = getComputedStyle(document.documentElement);46 const p = {} as Palette;47 for (const v of vars) {48 p[v] = style.getPropertyValue(`--${v}`).trim();49 }50 return p;51}5253// Nodes are cells: the panel's background framed in the grid's frame54// color. Actors and composite states are header bands, notes the cyan55// of where you are, and lines the muted text color.56function themeVariables(p: Palette, dark: boolean): Record<string, unknown> {57 return {58 darkMode: dark,59 background: p['term-bg'],60 fontFamily: p['font-mono'],61 fontSize: '14px',62 // Square, flat boxes, as 012's frames are: no rounding, shadow or63 // gradient.64 radius: 0,65 dropShadow: 'none',66 useGradient: false,67 primaryColor: p['role-panel-bg'],68 primaryTextColor: p['term-fg'],69 primaryBorderColor: p['role-frame'],70 secondaryColor: p['role-header-bg'],71 secondaryTextColor: p['role-header-fg'],72 secondaryBorderColor: p['role-frame'],73 tertiaryColor: p['term-bg'],74 tertiaryTextColor: p['term-fg'],75 tertiaryBorderColor: p['role-grid'],76 textColor: p['term-fg'],77 lineColor: p['role-muted'],78 arrowheadColor: p['role-muted'],79 edgeLabelBackground: p['term-bg'],80 clusterBkg: p['term-bg'],81 clusterBorder: p['role-frame'],82 titleColor: p['term-fg'],83 noteBkgColor: p['role-accent-bg'],84 noteTextColor: p['role-accent-fg'],85 noteBorderColor: p['role-accent-bg'],86 actorBkg: p['role-header-bg'],87 actorTextColor: p['role-header-fg'],88 actorBorder: p['role-header-bg'],89 actorLineColor: p['role-frame'],90 signalColor: p['term-fg'],91 signalTextColor: p['term-fg'],92 labelBoxBkgColor: p['role-header-bg'],93 labelBoxBorderColor: p['role-header-bg'],94 labelTextColor: p['role-header-fg'],95 loopTextColor: p['term-fg'],96 activationBkgColor: p['role-panel-bg'],97 activationBorderColor: p['role-frame'],98 sequenceNumberColor: p['role-accent-fg'],99 stateBkg: p['role-panel-bg'],100 stateLabelColor: p['term-fg'],101 compositeBackground: p['term-bg'],102 compositeTitleBackground: p['role-header-bg'],103 compositeBorder: p['role-frame'],104 altBackground: p['role-panel-bg'],105 transitionColor: p['role-muted'],106 transitionLabelColor: p['term-fg'],107 labelBackgroundColor: p['term-bg'],108 specialStateColor: p['term-fg'],109 innerEndBackground: p['role-accent-bg'],110 errorBkgColor: p['ansi-red'],111 errorTextColor: p['term-bg'],112 };113}114115function useMermaidConfig(): MermaidConfig | undefined {116 // The mode is read from the page's data-theme attribute, which is set117 // before colorMode changes. colorMode only makes this render again: while118 // the page hydrates it reports the default mode first, and a config119 // made from it would draw every diagram twice at once, the two drawings120 // clobbering each other. On the server there is no page, and Mermaid121 // draws only in the browser.122 useColorMode();123 const mode = typeof document === 'undefined' ? undefined : document.documentElement.dataset.theme;124 return useMemo(() => {125 if (mode === undefined) {126 return undefined;127 }128 const palette = readPalette();129 // The font is also set outside the theme: diagrams that size boxes130 // by measuring their text (sequence diagrams) measure it in these,131 // and text measured in another font overflows its box.132 return {133 startOnLoad: false,134 theme: 'base',135 fontFamily: palette['font-mono'],136 fontSize: 14,137 themeVariables: themeVariables(palette, mode === 'dark'),138 flowchart: {curve: 'linear', htmlLabels: true},139 } as MermaidConfig;140 }, [mode]);141}142143function MermaidRenderResult({renderResult}: {renderResult: RenderResult}): ReactNode {144 const ref = useRef<HTMLDivElement>(null);145 useEffect(() => {146 if (ref.current) {147 renderResult.bindFunctions?.(ref.current);148 }149 }, [renderResult]);150 return (151 <div152 ref={ref}153 className={`${MermaidContainerClassName} mermaid-012`}154 // eslint-disable-next-line react/no-danger155 dangerouslySetInnerHTML={{__html: renderResult.svg}}156 />157 );158}159160// Mermaid is one object with one configuration, and it draws a diagram161// through an element of the page named by the id it is given. So each162// drawing gets its configuration set just before it and runs after the163// one before has finished, and gets an id of its own: under the id of164// the drawing shown, Mermaid would take that one off the page first, and165// a drawing identical to the one React last put there (the color mode166// switched twice quickly) would leave the box empty.167let drawing: Promise<unknown> = Promise.resolve();168let drawn = 0;169170function draw(text: string, config: MermaidConfig): Promise<RenderResult> {171 const id = `mermaid-svg-${++drawn}`;172 const result = drawing.then(async () => {173 const mermaid = await loadMermaid();174 mermaid.initialize(config);175 try {176 return await mermaid.render(id, text);177 } catch (e) {178 // On an error Mermaid leaves its error drawing on the page.179 document.querySelector(`#d${id}`)?.remove();180 throw e;181 }182 });183 drawing = result.catch(() => undefined);184 return result;185}186187// The drawing for the latest configuration: one asked for before the188// color mode changed again is dropped when it arrives. An error goes to189// the error boundary, which shows it in place of the diagram.190function useDrawing(text: string, config: MermaidConfig | undefined): RenderResult | null {191 const [result, setResult] = useState<RenderResult | null>(null);192 useEffect(() => {193 if (config === undefined) {194 return undefined;195 }196 let live = true;197 draw(text, config).then(198 (r) => live && setResult(r),199 (e: unknown) =>200 live &&201 setResult(() => {202 throw e;203 }),204 );205 return () => {206 live = false;207 };208 }, [text, config]);209 return result;210}211212function MermaidRenderer({value}: Props): ReactNode {213 const config = useMermaidConfig();214 const renderResult = useDrawing(value, config);215 if (renderResult === null) {216 return null;217 }218 return <MermaidRenderResult renderResult={renderResult} />;219}220221// Mermaid sizes boxes by measuring their text, so it draws once the222// site's fonts have loaded; measured in a fallback font, text overflows.223function useFontsReady(): boolean {224 const [ready, setReady] = useState(false);225 useEffect(() => {226 let live = true;227 document.fonts.ready.then(() => live && setReady(true));228 return () => {229 live = false;230 };231 }, []);232 return ready;233}234235export default function Mermaid(props: Props): ReactNode {236 const ready = useFontsReady();237 return (238 <ErrorBoundary fallback={(params) => <ErrorBoundaryErrorMessageFallback {...params} />}>239 {ready && <MermaidRenderer {...props} />}240 </ErrorBoundary>241 );242}