|
| 1 | +import { |
| 2 | + ArrowPathRoundedSquareIcon, |
| 3 | + MagnifyingGlassMinusIcon, |
| 4 | + MagnifyingGlassPlusIcon, |
| 5 | +} from "@heroicons/react/24/outline"; |
| 6 | +import Panzoom from "@panzoom/panzoom"; |
| 7 | +import mermaid from "mermaid"; |
| 8 | +import { useEffect, useId, useRef, useState } from "react"; |
| 9 | +import { useDebouncedEffect } from "../find/useDebounce"; |
| 10 | +import { ToolTip } from "../gui/Tooltip"; |
| 11 | + |
| 12 | +const MINIMUM_ZOOM_STEP = 0.05; |
| 13 | + |
| 14 | +const MERMAID_THEME_COLORS = { |
| 15 | + background: "#1e1e1e", |
| 16 | + primaryColor: "#4d8bf0", |
| 17 | + primaryTextColor: "#ffffff", |
| 18 | + primaryBorderColor: "#4d8bf0", |
| 19 | + secondaryColor: "#3a6db3", |
| 20 | + secondaryTextColor: "#ffffff", |
| 21 | + secondaryBorderColor: "#3a6db3", |
| 22 | + tertiaryColor: "#59bc89", |
| 23 | + tertiaryTextColor: "#ffffff", |
| 24 | + tertiaryBorderColor: "#59bc89", |
| 25 | + noteBkgColor: "#2d2d2d", |
| 26 | + noteTextColor: "#e6e6e6", |
| 27 | + noteBorderColor: "#555555", |
| 28 | + lineColor: "#8c8c8c", |
| 29 | + textColor: "#e6e6e6", |
| 30 | + mainBkg: "#252525", |
| 31 | + errorBkgColor: "#f44336", |
| 32 | + errorTextColor: "#ffffff", |
| 33 | + nodeBorder: "#555555", |
| 34 | + clusterBkg: "#2a2a2a", |
| 35 | + clusterBorder: "#555555", |
| 36 | + defaultLinkColor: "#8c8c8c", |
| 37 | + titleColor: "#e6e6e6", |
| 38 | + edgeLabelBackground: "#252525", |
| 39 | + activeTaskBkgColor: "#4caf50", |
| 40 | + activeTaskBorderColor: "#388e3c", |
| 41 | + doneTaskBkgColor: "#388e3c", |
| 42 | + doneTaskBorderColor: "#2e7d32", |
| 43 | + critBkgColor: "#f44336", |
| 44 | + critBorderColor: "#d32f2f", |
| 45 | + taskTextColor: "#e6e6e6", |
| 46 | + taskTextOutsideColor: "#e6e6e6", |
| 47 | + taskTextLightColor: "#b3b3b3", |
| 48 | + sectionBkgColor: "#2a2a2a", |
| 49 | + altSectionBkgColor: "#303030", |
| 50 | + sectionBkgColor2: "#252525", |
| 51 | + excludeBkgColor: "#2d2d2d", |
| 52 | + fillType0: "#264f78", |
| 53 | + fillType1: "#3a6db3", |
| 54 | + fillType2: "#59bc89", |
| 55 | + fillType3: "#4d8bf0", |
| 56 | + fillType4: "#3a6db3", |
| 57 | + fillType5: "#264f78", |
| 58 | + fillType6: "#59bc89", |
| 59 | + fillType7: "#4d8bf0", |
| 60 | +}; |
| 61 | + |
| 62 | +mermaid.initialize({ |
| 63 | + startOnLoad: false, |
| 64 | + securityLevel: "loose", |
| 65 | + theme: "dark", |
| 66 | + themeVariables: { |
| 67 | + ...MERMAID_THEME_COLORS, |
| 68 | + fontSize: "14px", |
| 69 | + fontFamily: "var(--vscode-font-family)", |
| 70 | + }, |
| 71 | +}); |
| 72 | + |
| 73 | +export default function MermaidDiagram({ code }: { code: string }) { |
| 74 | + const mermaidRenderContainerRef = useRef<HTMLDivElement>(null); |
| 75 | + const zoomInButtonRef = useRef<SVGSVGElement>(null); |
| 76 | + const zoomOutButtonRef = useRef<SVGSVGElement>(null); |
| 77 | + const resetZoomButtonRef = useRef<SVGSVGElement>(null); |
| 78 | + |
| 79 | + const zoomInButtonId = useId(); |
| 80 | + const zoomOutButtonId = useId(); |
| 81 | + const resetZoomButtonId = useId(); |
| 82 | + |
| 83 | + const [isLoading, setIsLoading] = useState(false); |
| 84 | + const [error, setError] = useState(""); |
| 85 | + |
| 86 | + useEffect(() => { |
| 87 | + setIsLoading(true); |
| 88 | + }, [code]); |
| 89 | + |
| 90 | + useDebouncedEffect( |
| 91 | + () => { |
| 92 | + if (mermaidRenderContainerRef.current) { |
| 93 | + mermaidRenderContainerRef.current.innerHTML = ""; |
| 94 | + } |
| 95 | + const diagramId = `mermaid-id-${Math.random().toString(36).substring(2)}`; |
| 96 | + mermaid |
| 97 | + .parse(code) |
| 98 | + .then(() => mermaid.render(diagramId, code)) |
| 99 | + .then(({ svg }) => { |
| 100 | + if (mermaidRenderContainerRef.current) { |
| 101 | + mermaidRenderContainerRef.current.innerHTML = svg; |
| 102 | + setError(""); |
| 103 | + const panzoom = Panzoom(mermaidRenderContainerRef.current, { |
| 104 | + step: MINIMUM_ZOOM_STEP, |
| 105 | + }); |
| 106 | + zoomInButtonRef.current?.addEventListener("click", () => |
| 107 | + panzoom.zoomIn({ step: MINIMUM_ZOOM_STEP * 4 }), |
| 108 | + ); |
| 109 | + zoomOutButtonRef.current?.addEventListener("click", () => |
| 110 | + panzoom.zoomOut({ step: MINIMUM_ZOOM_STEP * 4 }), |
| 111 | + ); |
| 112 | + resetZoomButtonRef.current?.addEventListener("click", () => |
| 113 | + panzoom.reset(), |
| 114 | + ); |
| 115 | + mermaidRenderContainerRef.current.parentElement?.addEventListener( |
| 116 | + "wheel", |
| 117 | + (event) => { |
| 118 | + if (!event.shiftKey) return; |
| 119 | + panzoom.zoomWithWheel(event); |
| 120 | + }, |
| 121 | + ); |
| 122 | + } |
| 123 | + }) |
| 124 | + .catch((err) => { |
| 125 | + if (err.message) { |
| 126 | + setError(err.message); |
| 127 | + } else { |
| 128 | + setError( |
| 129 | + "Unknown error when parsing or rendering the Mermaid diagram.", |
| 130 | + ); |
| 131 | + } |
| 132 | + }) |
| 133 | + .finally(() => { |
| 134 | + setIsLoading(false); |
| 135 | + }); |
| 136 | + }, |
| 137 | + 500, |
| 138 | + [code], |
| 139 | + ); |
| 140 | + |
| 141 | + return ( |
| 142 | + <> |
| 143 | + {isLoading && ( |
| 144 | + <div className="text-vsc-foreground text-sm">Generating diagram...</div> |
| 145 | + )} |
| 146 | + {!!error ? ( |
| 147 | + <div className="text-error whitespace-pre text-sm">{error}</div> |
| 148 | + ) : ( |
| 149 | + <div className="relative"> |
| 150 | + <div className="absolute right-0 z-10 m-2 flex items-center gap-x-1"> |
| 151 | + <MagnifyingGlassPlusIcon |
| 152 | + data-tooltip-id={zoomInButtonId} |
| 153 | + ref={zoomInButtonRef} |
| 154 | + className="h-4 w-4 cursor-pointer" |
| 155 | + /> |
| 156 | + <ToolTip id={zoomInButtonId}>Zoom In</ToolTip> |
| 157 | + <MagnifyingGlassMinusIcon |
| 158 | + data-tooltip-id={zoomOutButtonId} |
| 159 | + ref={zoomOutButtonRef} |
| 160 | + className="h-4 w-4 cursor-pointer" |
| 161 | + /> |
| 162 | + <ToolTip id={zoomOutButtonId}>Zoom Out</ToolTip> |
| 163 | + <ArrowPathRoundedSquareIcon |
| 164 | + data-tooltip-id={resetZoomButtonId} |
| 165 | + ref={resetZoomButtonRef} |
| 166 | + className="h-4 w-4 cursor-pointer" |
| 167 | + /> |
| 168 | + <ToolTip id={resetZoomButtonId}>Reset Zoom</ToolTip> |
| 169 | + </div> |
| 170 | + <div |
| 171 | + className="flex min-h-5 justify-center" |
| 172 | + ref={mermaidRenderContainerRef} |
| 173 | + /> |
| 174 | + </div> |
| 175 | + )} |
| 176 | + </> |
| 177 | + ); |
| 178 | +} |
0 commit comments