Skip to content

Commit 7849b48

Browse files
dwelleigorwessel
andauthored
feat: add state diagram support (#96)
Co-authored-by: Igor Wessel <igor_wessel@hotmail.com>
1 parent fe990f9 commit 7849b48

137 files changed

Lines changed: 2091 additions & 86 deletions

File tree

Some content is hidden

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@excalidraw/excalidraw": "^0.18.0-a9ca16e",
3535
"@playwright/test": "^1.58.2",
3636
"@types/mermaid": "^9.2.0",
37+
"@types/node": "^20",
3738
"@types/react": "18.2.14",
3839
"@types/react-dom": "18.2.4",
3940
"@typescript-eslint/eslint-plugin": "5.59.9",

playground/ExcalidrawSvgPreview.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { DEFAULT_FONT_SIZE } from "../src/constants";
77
import { graphToExcalidraw } from "../src/graphToExcalidraw";
88
import { parseMermaid } from "../src/parseMermaid";
9+
import { ensureExcalidrawFontsLoaded } from "./loadExcalidrawFonts";
910

1011
interface ExcalidrawSvgPreviewProps {
1112
definition: string;
@@ -49,6 +50,7 @@ const generateExcalidrawSvg = async (definition: string): Promise<string> => {
4950
const { elements, files } = graphToExcalidraw(parsedMermaid, {
5051
fontSize: DEFAULT_FONT_SIZE,
5152
});
53+
await ensureExcalidrawFontsLoaded();
5254

5355
const svgElement = await exportToSvg({
5456
elements: convertToExcalidrawElements(elements),

playground/ExcalidrawWrapper.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
77
import { graphToExcalidraw } from "../src/graphToExcalidraw";
88
import { DEFAULT_FONT_SIZE } from "../src/constants";
99
import type { MermaidData } from "./";
10+
import { ensureExcalidrawFontsLoaded } from "./loadExcalidrawFonts";
1011

1112
interface ExcalidrawWrapperProps {
1213
mermaidDefinition: MermaidData["definition"];
@@ -25,29 +26,45 @@ const ExcalidrawWrapper = ({
2526
useState<ExcalidrawImperativeAPI | null>(null);
2627

2728
useEffect(() => {
29+
let isCancelled = false;
30+
2831
if (!readyExcalidrawAPI || readyExcalidrawAPI.isDestroyed) {
29-
return;
32+
return undefined;
3033
}
3134

3235
if (mermaidDefinition === "" || mermaidOutput === null) {
3336
readyExcalidrawAPI.resetScene();
34-
return;
37+
return undefined;
3538
}
3639

37-
const { elements, files } = graphToExcalidraw(mermaidOutput, {
38-
fontSize: DEFAULT_FONT_SIZE,
39-
});
40+
void (async () => {
41+
await ensureExcalidrawFontsLoaded();
42+
if (isCancelled || readyExcalidrawAPI.isDestroyed) {
43+
return;
44+
}
4045

41-
readyExcalidrawAPI.updateScene({
42-
elements: convertToExcalidrawElements(elements),
43-
});
44-
readyExcalidrawAPI.scrollToContent(readyExcalidrawAPI.getSceneElements(), {
45-
fitToContent: true,
46-
});
46+
const { elements, files } = graphToExcalidraw(mermaidOutput, {
47+
fontSize: DEFAULT_FONT_SIZE,
48+
});
4749

48-
if (files) {
49-
readyExcalidrawAPI.addFiles(Object.values(files));
50-
}
50+
readyExcalidrawAPI.updateScene({
51+
elements: convertToExcalidrawElements(elements),
52+
});
53+
readyExcalidrawAPI.scrollToContent(
54+
readyExcalidrawAPI.getSceneElements(),
55+
{
56+
fitToContent: true,
57+
}
58+
);
59+
60+
if (files) {
61+
readyExcalidrawAPI.addFiles(Object.values(files));
62+
}
63+
})();
64+
65+
return () => {
66+
isCancelled = true;
67+
};
5168
}, [mermaidDefinition, mermaidOutput, readyExcalidrawAPI]);
5269

5370
return (

playground/SingleTestCase.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { MermaidDiagram } from "./MermaidDiagram";
33
import { ExcalidrawSvgPreview } from "./ExcalidrawSvgPreview";
44

55
export interface TestCase {
6-
type: "class" | "erd" | "flowchart" | "sequence" | "unsupported";
6+
type: "class" | "erd" | "flowchart" | "sequence" | "state" | "unsupported";
77
name: string;
88
definition: string;
99
}

playground/Testcases.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FLOWCHART_DIAGRAM_TESTCASES } from "./testcases/flowchart";
22
import { SEQUENCE_DIAGRAM_TESTCASES } from "./testcases/sequence.ts";
33
import { CLASS_DIAGRAM_TESTCASES } from "./testcases/class.ts";
44
import { ERD_DIAGRAM_TESTCASES } from "./testcases/er.ts";
5+
import { STATE_DIAGRAM_TESTCASES } from "./testcases/state.ts";
56
import { UNSUPPORTED_DIAGRAM_TESTCASES } from "./testcases/unsupported.ts";
67

78
import SingleTestCase, { TestCase } from "./SingleTestCase.tsx";
@@ -119,6 +120,11 @@ const Testcases = ({ onChange, onInsertMermaidSvg }: TestcasesProps) => {
119120
documentationHref:
120121
"https://mermaid.js.org/syntax/entityRelationshipDiagram.html",
121122
},
123+
{
124+
name: "State",
125+
testcases: STATE_DIAGRAM_TESTCASES,
126+
documentationHref: "https://mermaid.js.org/syntax/stateDiagram.html",
127+
},
122128
{
123129
name: "Unsupported",
124130
testcases: UNSUPPORTED_DIAGRAM_TESTCASES,

playground/loadExcalidrawFonts.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { getFontString } from "@excalidraw/common";
2+
import { FONT_FAMILY, Fonts } from "@excalidraw/excalidraw";
3+
4+
let excalidrawFontsReadyPromise: Promise<void> | null = null;
5+
let fontMeasureContext: CanvasRenderingContext2D | null | undefined;
6+
7+
const EXCALIFONT_PROBE_TEXT = "This is the note to the left.";
8+
const EXCALIFONT_PROBE_SIZE = 18;
9+
const EXCALIFONT_METRICS_WAIT_TIMEOUT_MS = 2000;
10+
const EXCALIFONT_METRICS_POLL_INTERVAL_MS = 16;
11+
12+
const getFontMeasureContext = () => {
13+
if (fontMeasureContext !== undefined) {
14+
return fontMeasureContext;
15+
}
16+
17+
try {
18+
fontMeasureContext = document.createElement("canvas").getContext("2d");
19+
} catch {
20+
fontMeasureContext = null;
21+
}
22+
23+
return fontMeasureContext;
24+
};
25+
26+
const measureTextWidth = (font: string, text: string) => {
27+
const context = getFontMeasureContext();
28+
if (!context) {
29+
return null;
30+
}
31+
32+
context.font = font;
33+
return context.measureText(text).width;
34+
};
35+
36+
const wait = (ms: number) =>
37+
new Promise<void>((resolve) => {
38+
window.setTimeout(resolve, ms);
39+
});
40+
41+
const waitForExcalifontMetrics = async () => {
42+
const font = getFontString({
43+
fontSize: EXCALIFONT_PROBE_SIZE,
44+
fontFamily: FONT_FAMILY.Excalifont,
45+
});
46+
47+
await document.fonts.load(font, EXCALIFONT_PROBE_TEXT);
48+
49+
const fallbackWidth = measureTextWidth(
50+
`${EXCALIFONT_PROBE_SIZE}px sans-serif`,
51+
EXCALIFONT_PROBE_TEXT,
52+
);
53+
54+
if (fallbackWidth === null) {
55+
return;
56+
}
57+
58+
const deadline = Date.now() + EXCALIFONT_METRICS_WAIT_TIMEOUT_MS;
59+
while (Date.now() < deadline) {
60+
const excalifontWidth = measureTextWidth(font, EXCALIFONT_PROBE_TEXT);
61+
if (
62+
document.fonts.check(font, EXCALIFONT_PROBE_TEXT) &&
63+
excalifontWidth !== null &&
64+
Math.abs(excalifontWidth - fallbackWidth) > 0.5
65+
) {
66+
return;
67+
}
68+
69+
// `requestAnimationFrame` can stop firing in headless/background tabs,
70+
// which would wedge Playwright visual runs. Poll on wall-clock time instead.
71+
await wait(EXCALIFONT_METRICS_POLL_INTERVAL_MS);
72+
}
73+
};
74+
75+
export const ensureExcalidrawFontsLoaded = () => {
76+
if (typeof window === "undefined") {
77+
return Promise.resolve();
78+
}
79+
80+
if ((window as any).EXCALIDRAW_ASSET_PATH === undefined) {
81+
(window as any).EXCALIDRAW_ASSET_PATH = "/";
82+
}
83+
84+
if (!excalidrawFontsReadyPromise) {
85+
excalidrawFontsReadyPromise = (async () => {
86+
await Fonts.loadElementsFonts([
87+
{
88+
type: "text",
89+
fontFamily: FONT_FAMILY.Excalifont,
90+
text: "preload",
91+
originalText: "preload",
92+
} as any,
93+
]);
94+
await document.fonts.ready;
95+
await waitForExcalifontMetrics();
96+
})();
97+
}
98+
99+
return excalidrawFontsReadyPromise;
100+
};

playground/main.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
33
import App from "./index.tsx";
44
import mermaid from "mermaid";
55
import { DEFAULT_FONT_SIZE, MERMAID_CONFIG } from "../src/constants.ts";
6+
import { ensureExcalidrawFontsLoaded } from "./loadExcalidrawFonts.ts";
67

78
// Initialize Mermaid
89
mermaid.initialize({
@@ -14,8 +15,10 @@ mermaid.initialize({
1415

1516
const root = ReactDOM.createRoot(document.getElementById("root")!);
1617

17-
root.render(
18-
<React.StrictMode>
19-
<App />
20-
</React.StrictMode>
21-
);
18+
void ensureExcalidrawFontsLoaded().finally(() => {
19+
root.render(
20+
<React.StrictMode>
21+
<App />
22+
</React.StrictMode>
23+
);
24+
});

0 commit comments

Comments
 (0)