Skip to content

Commit 65defca

Browse files
authored
feat: mermaid@11 & ERD (#95)
1 parent 6c89fa7 commit 65defca

47 files changed

Lines changed: 7117 additions & 706 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@
2626
npm-debug.log*
2727
yarn-debug.log*
2828
yarn-error.log*
29+
30+
.claude
31+
CLAUDE.md

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Unreleased
22

3+
### v2.1.0
4+
5+
- Upgrade mermaid to 11.12.1
6+
- Add ERD support
7+
38
### Features
49

510
- Support ClassDef for styling the nodes by [@ad1992](https://github.com/ad1992) in https://github.com/excalidraw/mermaid-to-excalidraw/pull/71

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@excalidraw/mermaid-to-excalidraw",
3-
"version": "1.1.4",
3+
"version": "2.1.1",
44
"description": "Mermaid to Excalidraw Diagrams",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -11,7 +11,8 @@
1111
"scripts": {
1212
"build": "rimraf -rf ./dist && cross-env tsc -b src",
1313
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
14-
"start": "vite playground",
14+
"dev": "vite playground --mode development",
15+
"start": "vite playground --mode development",
1516
"build:playground": "tsc --noEmit --project ./playground/tsconfig.json && vite build playground",
1617
"preview": "yarn run build:playground && vite preview --outDir ./public",
1718
"test": "vitest",
@@ -20,15 +21,15 @@
2021
},
2122
"dependencies": {
2223
"@excalidraw/markdown-to-text": "0.1.2",
23-
"mermaid": "10.9.4",
24-
"nanoid": "4.0.2",
25-
"react-split": "^2.0.14"
24+
"@mermaid-js/parser": "^0.6.3",
25+
"mermaid": "^11.12.1",
26+
"nanoid": "4.0.2"
2627
},
2728
"devDependencies": {
2829
"@babel/core": "7.12.0",
2930
"@excalidraw/eslint-config": "1.0.3",
30-
"@excalidraw/excalidraw": "0.17.1-7381-cdf6d3e",
31-
"@types/mermaid": "9.2.0",
31+
"@excalidraw/excalidraw": "^0.18.0-816c81c",
32+
"@types/mermaid": "^9.2.0",
3233
"@types/react": "18.2.14",
3334
"@types/react-dom": "18.2.4",
3435
"@typescript-eslint/eslint-plugin": "5.59.9",

playground/CustomTest.tsx

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useState, useEffect } from "react";
12
import { MermaidDiagram } from "./MermaidDiagram.tsx";
23
import type { ActiveTestCaseIndex, MermaidData } from "./index.tsx";
4+
import { usePersistedSectionState } from "./usePersistedSectionState.ts";
35

46
interface CustomTestProps {
57
onChange: (
@@ -10,57 +12,112 @@ interface CustomTestProps {
1012
activeTestCaseIndex: ActiveTestCaseIndex;
1113
}
1214

15+
const STORAGE_KEY = "mermaid-to-excalidraw-definition";
16+
1317
const CustomTest = ({
1418
onChange,
1519
mermaidData,
1620
activeTestCaseIndex,
1721
}: CustomTestProps) => {
1822
const isActive = activeTestCaseIndex === "custom";
23+
const {
24+
isExpanded: isParsedDataExpanded,
25+
handleToggle: handleParsedDataToggle,
26+
} = usePersistedSectionState("custom:parsed-data");
27+
const [textareaValue, setTextareaValue] = useState(() => {
28+
// Load from localStorage on initial mount
29+
try {
30+
return localStorage.getItem(STORAGE_KEY) || "";
31+
} catch {
32+
return "";
33+
}
34+
});
35+
36+
// Update textarea when mermaidData changes from external source
37+
useEffect(() => {
38+
if (mermaidData.definition && activeTestCaseIndex !== "custom") {
39+
setTextareaValue(mermaidData.definition);
40+
}
41+
}, [mermaidData.definition, activeTestCaseIndex]);
42+
1943
return (
2044
<>
2145
<form
46+
className="custom-test-form"
2247
onSubmit={(e) => {
2348
e.preventDefault();
2449

2550
const formData = new FormData(e.target as HTMLFormElement);
51+
const definition = formData.get("mermaid-input")?.toString() || "";
2652

27-
onChange(formData.get("mermaid-input")?.toString() || "", "custom");
53+
// Save to localStorage
54+
try {
55+
localStorage.setItem(STORAGE_KEY, definition);
56+
} catch (error) {
57+
console.error("Failed to save to localStorage:", error);
58+
}
59+
60+
onChange(definition, "custom");
2861
}}
2962
>
63+
<label className="field-label" htmlFor="mermaid-input">
64+
{"Mermaid definition"}
65+
</label>
3066
<textarea
3167
id="mermaid-input"
3268
rows={10}
3369
cols={50}
3470
name="mermaid-input"
71+
value={textareaValue}
3572
onChange={(e) => {
73+
const value = e.target.value;
74+
setTextareaValue(value);
75+
3676
if (!isActive) {
3777
return;
3878
}
3979

40-
onChange(e.target.value, "custom");
80+
onChange(value, "custom");
4181
}}
42-
style={{ marginTop: "1rem" }}
43-
placeholder="Input Mermaid Syntax"
82+
placeholder="Paste or type Mermaid syntax here"
4483
/>
45-
<br />
46-
<button type="submit" id="render-excalidraw-btn">
47-
{"Render to Excalidraw"}
48-
</button>
84+
<div className="custom-test-actions">
85+
<button
86+
className="playground-button"
87+
type="submit"
88+
id="render-excalidraw-btn"
89+
>
90+
{"Render to Excalidraw"}
91+
</button>
92+
<p className="custom-test-hint">
93+
{"The live Excalidraw canvas updates on the right."}
94+
</p>
95+
</div>
4996
</form>
5097

5198
{isActive && (
5299
<>
53-
<MermaidDiagram
54-
definition={mermaidData.definition}
55-
id="custom-diagram"
56-
/>
100+
<section className="custom-preview-card">
101+
<div className="preview-badge">{"Live Mermaid SVG"}</div>
102+
<div className="diagram-preview-surface custom-diagram-surface">
103+
<MermaidDiagram definition={textareaValue} id="custom-diagram" />
104+
</div>
105+
</section>
57106

58-
<details id="parsed-data-details">
107+
<details
108+
id="parsed-data-details"
109+
open={isParsedDataExpanded}
110+
onToggle={handleParsedDataToggle}
111+
>
59112
<summary>{"Parsed data from parseMermaid"}</summary>
60-
<pre id="custom-parsed-data">
61-
{JSON.stringify(mermaidData.output, null, 2)}
62-
</pre>
63-
{mermaidData.error && <div id="error">{mermaidData.error}</div>}
113+
{isParsedDataExpanded ? (
114+
<>
115+
<pre id="custom-parsed-data">
116+
{JSON.stringify(mermaidData.output, null, 2)}
117+
</pre>
118+
{mermaidData.error && <div id="error">{mermaidData.error}</div>}
119+
</>
120+
) : null}
64121
</details>
65122
</>
66123
)}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useEffect, useState } from "react";
2+
import {
3+
convertToExcalidrawElements,
4+
exportToSvg,
5+
} from "@excalidraw/excalidraw";
6+
import { DEFAULT_FONT_SIZE } from "../src/constants";
7+
import { graphToExcalidraw } from "../src/graphToExcalidraw";
8+
import { parseMermaid } from "../src/parseMermaid";
9+
10+
interface ExcalidrawSvgPreviewProps {
11+
definition: string;
12+
}
13+
14+
let previewRenderQueue: Promise<void> = Promise.resolve();
15+
const svgCache = new Map<string, string>();
16+
const inFlightSvgCache = new Map<string, Promise<string>>();
17+
18+
const runSequentially = <T,>(task: () => Promise<T>) => {
19+
const run = previewRenderQueue.then(task, task);
20+
previewRenderQueue = run.then(
21+
() => undefined,
22+
() => undefined
23+
);
24+
return run;
25+
};
26+
27+
const cleanupMermaidTempContainers = () => {
28+
document
29+
.querySelectorAll<HTMLDivElement>(".mermaid-to-excalidraw-svg-container")
30+
.forEach((container) => {
31+
container.remove();
32+
});
33+
};
34+
35+
const generateExcalidrawSvg = async (definition: string): Promise<string> => {
36+
const cachedSvg = svgCache.get(definition);
37+
if (cachedSvg) {
38+
return cachedSvg;
39+
}
40+
41+
const pendingRender = inFlightSvgCache.get(definition);
42+
if (pendingRender) {
43+
return pendingRender;
44+
}
45+
46+
const renderPromise = runSequentially(async () => {
47+
try {
48+
const parsedMermaid = await parseMermaid(definition);
49+
const { elements, files } = graphToExcalidraw(parsedMermaid, {
50+
fontSize: DEFAULT_FONT_SIZE,
51+
});
52+
53+
const svgElement = await exportToSvg({
54+
elements: convertToExcalidrawElements(elements),
55+
appState: {
56+
exportBackground: true,
57+
viewBackgroundColor: "#ffffff",
58+
},
59+
files: files ?? null,
60+
exportPadding: 16,
61+
});
62+
63+
return svgElement.outerHTML;
64+
} finally {
65+
cleanupMermaidTempContainers();
66+
}
67+
})
68+
.then((svg) => {
69+
svgCache.set(definition, svg);
70+
return svg;
71+
})
72+
.finally(() => {
73+
inFlightSvgCache.delete(definition);
74+
});
75+
76+
inFlightSvgCache.set(definition, renderPromise);
77+
return renderPromise;
78+
};
79+
80+
export const ExcalidrawSvgPreview = ({
81+
definition,
82+
}: ExcalidrawSvgPreviewProps) => {
83+
const [svg, setSvg] = useState("");
84+
const [error, setError] = useState<string | null>(null);
85+
const [isLoading, setIsLoading] = useState(false);
86+
87+
useEffect(() => {
88+
let isCancelled = false;
89+
90+
setIsLoading(true);
91+
setError(null);
92+
93+
generateExcalidrawSvg(definition)
94+
.then((renderedSvg) => {
95+
if (!isCancelled) {
96+
setSvg(renderedSvg);
97+
}
98+
})
99+
.catch((err) => {
100+
if (!isCancelled) {
101+
setSvg("");
102+
setError(String(err));
103+
}
104+
})
105+
.finally(() => {
106+
if (!isCancelled) {
107+
setIsLoading(false);
108+
}
109+
});
110+
111+
return () => {
112+
isCancelled = true;
113+
};
114+
}, [definition]);
115+
116+
if (error) {
117+
return <div className="preview-error">{error}</div>;
118+
}
119+
120+
if (isLoading && !svg) {
121+
return (
122+
<div className="preview-loading">{"Rendering Excalidraw SVG..."}</div>
123+
);
124+
}
125+
126+
return (
127+
<div
128+
className="excalidraw-svg-preview"
129+
dangerouslySetInnerHTML={{ __html: svg }}
130+
/>
131+
);
132+
};

0 commit comments

Comments
 (0)