|
| 1 | +import { cssId } from "./random/cssId"; |
| 2 | +import { ensureNonNullable } from "~/lib/ensureNonNullable"; |
| 3 | +import type { Theme } from "~/lib/theme"; |
| 4 | +import { $theme } from "~/stores/theme"; |
| 5 | +import { createIntersectionObserverAtom } from "~/lib/nanostores/createIntersectionObserverAtom"; |
| 6 | +import { effect } from "~/lib/nanostores/effect"; |
| 7 | + |
| 8 | +interface MermaidOptions { |
| 9 | + id: string; |
| 10 | + container: HTMLElement; |
| 11 | + graph: string; |
| 12 | + theme: Theme; |
| 13 | +} |
| 14 | + |
| 15 | +async function renderMermaid({ id, container, graph, theme }: MermaidOptions) { |
| 16 | + const { default: mermaid } = await import("mermaid"); |
| 17 | + |
| 18 | + try { |
| 19 | + mermaid.initialize({ |
| 20 | + startOnLoad: false, |
| 21 | + securityLevel: "loose", |
| 22 | + fontFamily: "inherit", |
| 23 | + themeCSS: "margin: 1.5rem auto 0;", |
| 24 | + theme: theme === "dark" ? "dark" : "default", |
| 25 | + }); |
| 26 | + |
| 27 | + container.innerHTML = (await mermaid.render(id, graph)).svg; |
| 28 | + } catch (error) { |
| 29 | + // eslint-disable-next-line no-console -- show error |
| 30 | + console.error("Error while rendering mermaid", error); |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +function upsertGraphContainer(element: HTMLDivElement) { |
| 35 | + let container: HTMLDivElement | null = element.querySelector(".mermaid-graph"); |
| 36 | + |
| 37 | + if (!container) { |
| 38 | + container = document.createElement("div"); |
| 39 | + container.classList.add("mermaid-graph"); |
| 40 | + element.appendChild(container); |
| 41 | + } |
| 42 | + |
| 43 | + return container; |
| 44 | +} |
| 45 | + |
| 46 | +export function processMermaidExpressiveCodeBlock() { |
| 47 | + const expressiveCodeBlocks = document.querySelectorAll<HTMLDivElement>(".expressive-code"); |
| 48 | + |
| 49 | + for (const codeBlock of expressiveCodeBlocks) { |
| 50 | + const { $atom: $diagramIsVisible, intersectionObserver } = createIntersectionObserverAtom({ |
| 51 | + element: codeBlock, |
| 52 | + initialValue: false, |
| 53 | + mapper: (entry) => entry.isIntersecting, |
| 54 | + observerOptions: { |
| 55 | + rootMargin: "0px 0px 100% 0px", // Trigger when it is on the next screen |
| 56 | + }, |
| 57 | + }); |
| 58 | + const id = cssId(); |
| 59 | + |
| 60 | + effect([$diagramIsVisible, $theme], (diagramIsVisible, theme) => { |
| 61 | + if (!diagramIsVisible) { |
| 62 | + return; |
| 63 | + } |
| 64 | + |
| 65 | + // Disconnect the observer once the diagram is visible |
| 66 | + // $diagramIsVisible will never change after this point |
| 67 | + intersectionObserver?.disconnect(); |
| 68 | + |
| 69 | + const container = upsertGraphContainer(codeBlock); |
| 70 | + const copyButton = ensureNonNullable( |
| 71 | + codeBlock.querySelector<HTMLButtonElement>(".copy button"), |
| 72 | + ); |
| 73 | + const graphData = (copyButton.dataset.code ?? "") |
| 74 | + .replace(/\u007F/g, "\n") |
| 75 | + .replaceAll("\\n", "\n"); |
| 76 | + |
| 77 | + void renderMermaid({ id, theme, graph: graphData, container }); |
| 78 | + }); |
| 79 | + } |
| 80 | +} |
0 commit comments