/** * Mermaid diagram enhancement for markdown surfaces: lazily loads the * mermaid runtime from the host vendor route (same origin, no CDN), renders * every fenced ```mermaid code block in place, and re-renders on theme * flips. Framework-free so both the preview panel (React effect) and the * chat transcript observer can drive it over disjoint DOM scopes. * * Failure policy: any load/render failure leaves the original code block * untouched (or restores it verbatim); nothing here throws to the caller. * @module dsh-aionui-panel/client/preview/mermaid */ /** Minimal structural type of the mermaid runtime this module consumes. */ interface MermaidRuntime { initialize: (config: Record) => void render: (id: string, text: string, container?: HTMLElement) => Promise<{ svg: string }> } /** Host-served mermaid IIFE bundle (lib/assets/mermaid.min.js behind the route). */ export const MERMAID_VENDOR_URL = '/aionui-panel/vendor/mermaid.js' /** Lifecycle state stamped on diagram containers (`pending`/`rendering`/`done`). */ const DATA_STATE = 'data-mermaid-state' /** State stamped on a code block once its container exists (`claimed`). */ const DATA_CLAIMED = 'data-mermaid-claimed' /** The verbatim diagram source kept on the container for theme re-renders. */ const DATA_SOURCE = 'data-mermaid-source' /** Marker the preview viewer stamps on its own subtree (chat enhancement skips it). */ export const DATA_MD_SCOPE = 'data-aionui-md-scope' let loadPromise: Promise | undefined /** * Resolve the mermaid global left by the vendor IIFE bundle, or null while * absent. Narrow and defensive: the bundle is a third-party artifact. */ function mermaidGlobal(): MermaidRuntime | null { const candidate = (globalThis as Record).mermaid if (typeof candidate !== 'object' || candidate === null) return null const checked = candidate as Record if (typeof checked.initialize !== 'function' || typeof checked.render !== 'function') return null return checked as unknown as MermaidRuntime } /** * Load the mermaid runtime once per page: injects a