// Mermaid diagrams in Markdown, drawn in the browser by Pele // (https://pele.run). The server sends a ```mermaid block as code, so a // page without scripts still shows its source; this swaps each block for // its diagram, loading Pele, and each kind of diagram, only once a page // has one. A diagram that cannot be drawn shows its source again. import type { Mounted, MountOptions } from 'pele/lazy'; const BLOCKS = 'pre > code.language-mermaid'; /** Anyone with a repository writes diagrams, so their links are not vouched for. */ const OPTIONS: MountOptions = { linkRel: 'noopener nofollow ugc' }; /** * An action drawing the Mermaid blocks in its element, again as the HTML it * shows changes. The element is marked `data-drawing` until they are drawn: * what the screenshots wait for. */ export function diagrams(el: HTMLElement, _html: string) { let mounted: Mounted[] = []; let run = 0; const clear = () => { run++; mounted.forEach((m) => m.destroy()); mounted = []; }; async function draw(id: number) { const blocks = [...el.querySelectorAll(BLOCKS)]; if (!blocks.length) return; let pele: typeof import('pele/lazy'); try { pele = await import('pele/lazy'); } catch { // Not loaded: the sources stay shown. return; } for (const code of blocks) { const pre = code.parentElement; if (id !== run || !pre?.isConnected) return; const box = document.createElement('div'); box.className = 'diagram'; pre.replaceWith(box); let m: Mounted | undefined; try { m = await pele.mountAsync(box, code.textContent ?? '', OPTIONS); } catch { // Its kind of diagram failed to load. } if (m && m.error === undefined && id === run) { mounted.push(m); continue; } // Not drawn, or no longer wanted: the source again. m?.destroy(); box.replaceWith(pre); } } function start() { const id = run; el.dataset.drawing = ''; // After the HTML it is given is in place. queueMicrotask(() => draw(id).finally(() => { if (id === run) delete el.dataset.drawing; }) ); } start(); return { update() { clear(); start(); }, destroy: clear }; }