diff --git a/package.json b/package.json index ecbe031e..17260fda 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@excalidraw/excalidraw": "^0.18.0-a9ca16e", "@playwright/test": "^1.58.2", "@types/mermaid": "^9.2.0", + "@types/node": "^20", "@types/react": "18.2.14", "@types/react-dom": "18.2.4", "@typescript-eslint/eslint-plugin": "5.59.9", diff --git a/playground/ExcalidrawSvgPreview.tsx b/playground/ExcalidrawSvgPreview.tsx index daca5758..f36ccb13 100644 --- a/playground/ExcalidrawSvgPreview.tsx +++ b/playground/ExcalidrawSvgPreview.tsx @@ -6,6 +6,7 @@ import { import { DEFAULT_FONT_SIZE } from "../src/constants"; import { graphToExcalidraw } from "../src/graphToExcalidraw"; import { parseMermaid } from "../src/parseMermaid"; +import { ensureExcalidrawFontsLoaded } from "./loadExcalidrawFonts"; interface ExcalidrawSvgPreviewProps { definition: string; @@ -49,6 +50,7 @@ const generateExcalidrawSvg = async (definition: string): Promise => { const { elements, files } = graphToExcalidraw(parsedMermaid, { fontSize: DEFAULT_FONT_SIZE, }); + await ensureExcalidrawFontsLoaded(); const svgElement = await exportToSvg({ elements: convertToExcalidrawElements(elements), diff --git a/playground/ExcalidrawWrapper.tsx b/playground/ExcalidrawWrapper.tsx index 9f1391c1..c3432703 100644 --- a/playground/ExcalidrawWrapper.tsx +++ b/playground/ExcalidrawWrapper.tsx @@ -7,6 +7,7 @@ import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; import { graphToExcalidraw } from "../src/graphToExcalidraw"; import { DEFAULT_FONT_SIZE } from "../src/constants"; import type { MermaidData } from "./"; +import { ensureExcalidrawFontsLoaded } from "./loadExcalidrawFonts"; interface ExcalidrawWrapperProps { mermaidDefinition: MermaidData["definition"]; @@ -25,29 +26,45 @@ const ExcalidrawWrapper = ({ useState(null); useEffect(() => { + let isCancelled = false; + if (!readyExcalidrawAPI || readyExcalidrawAPI.isDestroyed) { - return; + return undefined; } if (mermaidDefinition === "" || mermaidOutput === null) { readyExcalidrawAPI.resetScene(); - return; + return undefined; } - const { elements, files } = graphToExcalidraw(mermaidOutput, { - fontSize: DEFAULT_FONT_SIZE, - }); + void (async () => { + await ensureExcalidrawFontsLoaded(); + if (isCancelled || readyExcalidrawAPI.isDestroyed) { + return; + } - readyExcalidrawAPI.updateScene({ - elements: convertToExcalidrawElements(elements), - }); - readyExcalidrawAPI.scrollToContent(readyExcalidrawAPI.getSceneElements(), { - fitToContent: true, - }); + const { elements, files } = graphToExcalidraw(mermaidOutput, { + fontSize: DEFAULT_FONT_SIZE, + }); - if (files) { - readyExcalidrawAPI.addFiles(Object.values(files)); - } + readyExcalidrawAPI.updateScene({ + elements: convertToExcalidrawElements(elements), + }); + readyExcalidrawAPI.scrollToContent( + readyExcalidrawAPI.getSceneElements(), + { + fitToContent: true, + } + ); + + if (files) { + readyExcalidrawAPI.addFiles(Object.values(files)); + } + })(); + + return () => { + isCancelled = true; + }; }, [mermaidDefinition, mermaidOutput, readyExcalidrawAPI]); return ( diff --git a/playground/SingleTestCase.tsx b/playground/SingleTestCase.tsx index 72496ae6..b60dce8f 100644 --- a/playground/SingleTestCase.tsx +++ b/playground/SingleTestCase.tsx @@ -3,7 +3,7 @@ import { MermaidDiagram } from "./MermaidDiagram"; import { ExcalidrawSvgPreview } from "./ExcalidrawSvgPreview"; export interface TestCase { - type: "class" | "erd" | "flowchart" | "sequence" | "unsupported"; + type: "class" | "erd" | "flowchart" | "sequence" | "state" | "unsupported"; name: string; definition: string; } diff --git a/playground/Testcases.tsx b/playground/Testcases.tsx index 06571ac0..793421bf 100644 --- a/playground/Testcases.tsx +++ b/playground/Testcases.tsx @@ -2,6 +2,7 @@ import { FLOWCHART_DIAGRAM_TESTCASES } from "./testcases/flowchart"; import { SEQUENCE_DIAGRAM_TESTCASES } from "./testcases/sequence.ts"; import { CLASS_DIAGRAM_TESTCASES } from "./testcases/class.ts"; import { ERD_DIAGRAM_TESTCASES } from "./testcases/er.ts"; +import { STATE_DIAGRAM_TESTCASES } from "./testcases/state.ts"; import { UNSUPPORTED_DIAGRAM_TESTCASES } from "./testcases/unsupported.ts"; import SingleTestCase, { TestCase } from "./SingleTestCase.tsx"; @@ -119,6 +120,11 @@ const Testcases = ({ onChange, onInsertMermaidSvg }: TestcasesProps) => { documentationHref: "https://mermaid.js.org/syntax/entityRelationshipDiagram.html", }, + { + name: "State", + testcases: STATE_DIAGRAM_TESTCASES, + documentationHref: "https://mermaid.js.org/syntax/stateDiagram.html", + }, { name: "Unsupported", testcases: UNSUPPORTED_DIAGRAM_TESTCASES, diff --git a/playground/loadExcalidrawFonts.ts b/playground/loadExcalidrawFonts.ts new file mode 100644 index 00000000..3cf9d304 --- /dev/null +++ b/playground/loadExcalidrawFonts.ts @@ -0,0 +1,100 @@ +import { getFontString } from "@excalidraw/common"; +import { FONT_FAMILY, Fonts } from "@excalidraw/excalidraw"; + +let excalidrawFontsReadyPromise: Promise | null = null; +let fontMeasureContext: CanvasRenderingContext2D | null | undefined; + +const EXCALIFONT_PROBE_TEXT = "This is the note to the left."; +const EXCALIFONT_PROBE_SIZE = 18; +const EXCALIFONT_METRICS_WAIT_TIMEOUT_MS = 2000; +const EXCALIFONT_METRICS_POLL_INTERVAL_MS = 16; + +const getFontMeasureContext = () => { + if (fontMeasureContext !== undefined) { + return fontMeasureContext; + } + + try { + fontMeasureContext = document.createElement("canvas").getContext("2d"); + } catch { + fontMeasureContext = null; + } + + return fontMeasureContext; +}; + +const measureTextWidth = (font: string, text: string) => { + const context = getFontMeasureContext(); + if (!context) { + return null; + } + + context.font = font; + return context.measureText(text).width; +}; + +const wait = (ms: number) => + new Promise((resolve) => { + window.setTimeout(resolve, ms); + }); + +const waitForExcalifontMetrics = async () => { + const font = getFontString({ + fontSize: EXCALIFONT_PROBE_SIZE, + fontFamily: FONT_FAMILY.Excalifont, + }); + + await document.fonts.load(font, EXCALIFONT_PROBE_TEXT); + + const fallbackWidth = measureTextWidth( + `${EXCALIFONT_PROBE_SIZE}px sans-serif`, + EXCALIFONT_PROBE_TEXT, + ); + + if (fallbackWidth === null) { + return; + } + + const deadline = Date.now() + EXCALIFONT_METRICS_WAIT_TIMEOUT_MS; + while (Date.now() < deadline) { + const excalifontWidth = measureTextWidth(font, EXCALIFONT_PROBE_TEXT); + if ( + document.fonts.check(font, EXCALIFONT_PROBE_TEXT) && + excalifontWidth !== null && + Math.abs(excalifontWidth - fallbackWidth) > 0.5 + ) { + return; + } + + // `requestAnimationFrame` can stop firing in headless/background tabs, + // which would wedge Playwright visual runs. Poll on wall-clock time instead. + await wait(EXCALIFONT_METRICS_POLL_INTERVAL_MS); + } +}; + +export const ensureExcalidrawFontsLoaded = () => { + if (typeof window === "undefined") { + return Promise.resolve(); + } + + if ((window as any).EXCALIDRAW_ASSET_PATH === undefined) { + (window as any).EXCALIDRAW_ASSET_PATH = "/"; + } + + if (!excalidrawFontsReadyPromise) { + excalidrawFontsReadyPromise = (async () => { + await Fonts.loadElementsFonts([ + { + type: "text", + fontFamily: FONT_FAMILY.Excalifont, + text: "preload", + originalText: "preload", + } as any, + ]); + await document.fonts.ready; + await waitForExcalifontMetrics(); + })(); + } + + return excalidrawFontsReadyPromise; +}; diff --git a/playground/main.tsx b/playground/main.tsx index bb38d06f..3f1ae482 100644 --- a/playground/main.tsx +++ b/playground/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client"; import App from "./index.tsx"; import mermaid from "mermaid"; import { DEFAULT_FONT_SIZE, MERMAID_CONFIG } from "../src/constants.ts"; +import { ensureExcalidrawFontsLoaded } from "./loadExcalidrawFonts.ts"; // Initialize Mermaid mermaid.initialize({ @@ -14,8 +15,10 @@ mermaid.initialize({ const root = ReactDOM.createRoot(document.getElementById("root")!); -root.render( - - - -); +void ensureExcalidrawFontsLoaded().finally(() => { + root.render( + + + + ); +}); diff --git a/playground/testcases/state.ts b/playground/testcases/state.ts new file mode 100644 index 00000000..b136fa4e --- /dev/null +++ b/playground/testcases/state.ts @@ -0,0 +1,157 @@ +import type { TestCase } from "../SingleTestCase"; + +export const STATE_DIAGRAM_TESTCASES: TestCase[] = [ + { + name: "Simple Transition", + definition: `stateDiagram-v2 + [*] --> Idle + Idle --> Active: Start + Active --> [*] +`, + type: "state", + }, + { + name: "Choice and Notes", + definition: `stateDiagram-v2 + state Decision <> + [*] --> Input + Input --> Decision + note right of Input: Capture payload + Decision --> Accept: valid + Decision --> Reject: invalid +`, + type: "state", + }, + { + name: "Composite State", + definition: `stateDiagram-v2 + [*] --> Session + state Session { + [*] --> Ready + Ready --> Busy + Busy --> Ready + } +`, + type: "state", + }, + { + name: "Composite State Transitions", + definition: `stateDiagram-v2 + [*] --> First + First --> Second + First --> Third + state First { + [*] --> fir + fir --> [*] + } + state Second { + [*] --> sec + sec --> [*] + } + state Third { + [*] --> thi + thi --> [*] + } +`, + type: "state", + }, + { + name: "Nested Composite States", + definition: `stateDiagram-v2 + [*] --> First + state First { + [*] --> Second + state Second { + [*] --> second + second --> Third + state Third { + [*] --> third + third --> [*] + } + } + } +`, + type: "state", + }, + { + name: "Concurrency", + definition: `stateDiagram-v2 + state Active { + [*] --> Left + -- + [*] --> Right + } +`, + type: "state", + }, + { + name: "Fork and Join", + definition: `stateDiagram-v2 + state fork_state <> + [*] --> fork_state + fork_state --> State2 + fork_state --> State3 + + state join_state <> + State2 --> join_state + State3 --> join_state + join_state --> State4 + State4 --> [*] +`, + type: "state", + }, + { + name: "Multiline Notes", + definition: `stateDiagram-v2 + State1: The state with a really long note that should wrap notes + note right of State1 + Important information! + You can write notes. + end note + State1 --> State2 + note left of State2 : This is the note to the left. +`, + type: "state", + }, + { + name: "Styling", + definition: `stateDiagram-v2 + classDef movement fill:#f00,color:white,stroke-width:2px,stroke:yellow + classDef stopped fill:#fff,stroke:#1f1f1f + Still --> Moving + Moving --> Still + class Moving movement + class Still stopped + style Still color:#1f1f1f +`, + type: "state", + }, + { + name: "Direction and Comments", + definition: `stateDiagram-v2 + direction LR + [*] --> A + A --> B + %% nested states may set their own direction + state B { + direction LR + a --> b + } + B --> D +`, + type: "state", + }, + { + name: "Spaces and Inline Styles", + definition: `stateDiagram-v2 + classDef yourState fill:#ffec99,stroke:#c92a2a,color:#1864ab,stroke-width:2px + yswsii: Your state with spaces in it + [*] --> yswsii:::yourState + [*] --> SomeOtherState + SomeOtherState --> YetAnotherState + yswsii --> YetAnotherState + YetAnotherState --> [*] +`, + type: "state", + }, +]; diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 78a9b59a..afdc3507 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -1,5 +1,11 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { cpSync, mkdirSync } from "node:fs"; +import { createRequire } from "node:module"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export default defineConfig({ resolve: { @@ -15,7 +21,31 @@ export default defineConfig({ define: { "process.env.IS_PREACT": JSON.stringify("false"), }, - plugins: [react()], + plugins: [ + react(), + { + name: "copy-excalifont", + configResolved() { + const require_ = createRequire(import.meta.url); + const excalidrawEntry = require_.resolve("@excalidraw/excalidraw"); + const srcFontsDir = resolve( + dirname(excalidrawEntry), + "fonts", + "Excalifont" + ); + const destFontsDir = resolve( + __dirname, + "..", + "public", + "fonts", + "Excalifont" + ); + + mkdirSync(destFontsDir, { recursive: true }); + cpSync(srcFontsDir, destFontsDir, { recursive: true }); + }, + }, + ], server: { port: 3418, open: true, diff --git a/playwright.config.ts b/playwright.config.ts index ef1583bb..d46d2be6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ }, expect: { toHaveScreenshot: { - maxDiffPixels: 0, + maxDiffPixels: 2, }, }, }); diff --git a/src/converter/types/state.ts b/src/converter/types/state.ts new file mode 100644 index 00000000..81616233 --- /dev/null +++ b/src/converter/types/state.ts @@ -0,0 +1,293 @@ +import type { + ExcalidrawElementSkeleton, + ValidContainer, + ValidLinearElement, +} from "@excalidraw/excalidraw/element/transform"; +import type { LocalPoint } from "@excalidraw/excalidraw/math/types"; + +import { GraphConverter } from "../GraphConverter.js"; +import { + computeExcalidrawVertexLabelStyle, + computeExcalidrawVertexStyle, +} from "../helpers.js"; + +import type { State, StateEdge, StateNode } from "../../parser/state.js"; + +const point = (x: number, y: number) => [x, y] as LocalPoint; +const DEFAULT_STATE_NODE_LABEL_FONT_SIZE = 18; +const MIN_STATE_NODE_LABEL_FONT_SIZE = 14; +const STATE_NODE_LABEL_FONT_SIZE_STEP = 2; +const DEFAULT_STATE_FILLED_COLOR = "#000000"; +const STATE_NODE_LABEL_HORIZONTAL_PADDING = 36; + +const STATE_PSEUDO_STATE_SHAPES_WITHOUT_LABEL = new Set([ + "choice", + "fork", + "join", + "stateStart", + "stateEnd", + "divider", +]); + +const getNodeText = (node: StateNode) => { + if (node.shape === "rectWithTitle" && node.description.length) { + return [node.text, ...node.description].join("\n"); + } + + return node.text; +}; + +let labelMeasureContext: CanvasRenderingContext2D | null | undefined; + +const getLabelMeasureContext = () => { + if (labelMeasureContext !== undefined) { + return labelMeasureContext; + } + + if (typeof document === "undefined") { + labelMeasureContext = null; + return labelMeasureContext; + } + + if ( + typeof navigator !== "undefined" && + /jsdom/i.test(navigator.userAgent || "") + ) { + labelMeasureContext = null; + return labelMeasureContext; + } + + try { + labelMeasureContext = document.createElement("canvas").getContext("2d"); + } catch { + labelMeasureContext = null; + } + return labelMeasureContext; +}; + +const measureLabelLineWidthAtFontSize = (line: string, fontSize: number) => { + const context = getLabelMeasureContext(); + if (!context) { + return line.length * fontSize * 0.6; + } + + context.font = `${fontSize}px Excalifont, sans-serif`; + return context.measureText(line).width; +}; + +const getNodeLabelFontSize = (node: StateNode) => { + const text = getNodeText(node); + if (!text || STATE_PSEUDO_STATE_SHAPES_WITHOUT_LABEL.has(node.shape)) { + return DEFAULT_STATE_NODE_LABEL_FONT_SIZE; + } + + if (!text.includes("\n")) { + return DEFAULT_STATE_NODE_LABEL_FONT_SIZE; + } + + const availableWidth = Math.max( + 1, + node.width - STATE_NODE_LABEL_HORIZONTAL_PADDING + ); + const lines = text.split("\n"); + + for ( + let fontSize = DEFAULT_STATE_NODE_LABEL_FONT_SIZE; + fontSize >= MIN_STATE_NODE_LABEL_FONT_SIZE; + fontSize -= STATE_NODE_LABEL_FONT_SIZE_STEP + ) { + const longestLineWidth = Math.max( + ...lines.map((line) => measureLabelLineWidthAtFontSize(line, fontSize)) + ); + + if (longestLineWidth <= availableWidth) { + return fontSize; + } + } + + return MIN_STATE_NODE_LABEL_FONT_SIZE; +}; + +const createLabel = (node: StateNode): ValidContainer["label"] | undefined => { + if (STATE_PSEUDO_STATE_SHAPES_WITHOUT_LABEL.has(node.shape)) { + return undefined; + } + + const text = getNodeText(node); + if (!text) { + return undefined; + } + + return { + text, + fontSize: getNodeLabelFontSize(node), + verticalAlign: + node.shape === "rectWithTitle" || node.shape === "roundedWithTitle" + ? "top" + : "middle", + ...computeExcalidrawVertexLabelStyle(node.labelStyle), + }; +}; + +const createContainerElement = (node: StateNode): ValidContainer => { + const baseStyle = computeExcalidrawVertexStyle(node.containerStyle); + const label = createLabel(node); + const elementType = + node.shape === "choice" + ? "diamond" + : node.shape === "stateStart" || node.shape === "stateEnd" + ? "ellipse" + : "rectangle"; + const hasRoundedCorners = + node.shape === "rect" || + node.shape === "rectWithTitle" || + node.shape === "roundedWithTitle"; + const isFilledStateNode = + node.shape === "stateStart" || + node.shape === "fork" || + node.shape === "join"; + const fillColor = + baseStyle.backgroundColor || + baseStyle.strokeColor || + DEFAULT_STATE_FILLED_COLOR; + const strokeColor = + baseStyle.strokeColor || + baseStyle.backgroundColor || + DEFAULT_STATE_FILLED_COLOR; + + return { + id: node.id, + type: elementType, + x: node.x, + y: node.y, + width: node.width, + height: node.height, + ...(label ? { label } : {}), + ...baseStyle, + ...(hasRoundedCorners ? { roundness: { type: 3 } } : {}), + ...(isFilledStateNode + ? { + backgroundColor: fillColor, + strokeColor, + fillStyle: "solid", + } + : {}), + }; +}; + +const createDividerLineElement = ( + node: StateNode +): ExcalidrawElementSkeleton | null => { + if (!node.dividerLine) { + return null; + } + + const baseStyle = computeExcalidrawVertexStyle(node.containerStyle); + + return { + id: `${node.id}__divider`, + type: "line", + x: node.dividerLine.startX, + y: node.dividerLine.startY, + width: node.dividerLine.endX - node.dividerLine.startX, + height: node.dividerLine.endY - node.dividerLine.startY, + points: [ + point(0, 0), + point( + node.dividerLine.endX - node.dividerLine.startX, + node.dividerLine.endY - node.dividerLine.startY + ), + ], + strokeColor: baseStyle.strokeColor || "#000", + strokeWidth: baseStyle.strokeWidth || 1, + }; +}; + +const createEndStateInnerEllipse = (node: StateNode): ValidContainer => { + const outerStyle = computeExcalidrawVertexStyle(node.containerStyle); + const inset = Math.max(2, Math.min(node.width, node.height) * 0.32); + const fillColor = + node.endInnerColor || + outerStyle.strokeColor || + outerStyle.backgroundColor || + DEFAULT_STATE_FILLED_COLOR; + + return { + id: `${node.id}__inner`, + type: "ellipse", + x: node.x + inset, + y: node.y + inset, + width: Math.max(1, node.width - inset * 2), + height: Math.max(1, node.height - inset * 2), + backgroundColor: fillColor, + strokeColor: fillColor, + fillStyle: "solid", + strokeWidth: 1, + }; +}; + +const createArrowElement = (edge: StateEdge): ValidLinearElement => { + const points = edge.reflectionPoints.map((currentPoint, index, array) => { + const startPoint = array[0]; + if (index === 0) { + return point(0, 0); + } + + return point(currentPoint.x - startPoint.x, currentPoint.y - startPoint.y); + }); + + return { + id: edge.id, + type: "arrow", + x: edge.startX, + y: edge.startY, + width: edge.endX - edge.startX, + height: edge.endY - edge.startY, + points, + strokeColor: edge.strokeColor || "#000", + strokeWidth: edge.strokeWidth || 2, + strokeStyle: edge.strokeStyle || "solid", + endArrowhead: edge.isNoteEdge ? null : "triangle", + roundness: { type: 2 }, + start: { id: edge.start }, + end: { id: edge.end }, + ...(edge.text + ? { + label: { + text: edge.text, + fontSize: 16, + }, + } + : {}), + }; +}; + +export const stateToExcalidrawSkeletonConvertor = new GraphConverter({ + converter: (chart: State) => { + const elements: ExcalidrawElementSkeleton[] = []; + + chart.nodes.forEach((node) => { + if (!node.isRenderable) { + return; + } + + const containerElement = createContainerElement(node); + elements.push(containerElement); + + const dividerLineElement = createDividerLineElement(node); + if (dividerLineElement) { + elements.push(dividerLineElement); + } + + if (node.shape === "stateEnd") { + elements.push(createEndStateInnerEllipse(node)); + } + }); + + chart.edges.forEach((edge) => { + elements.push(createArrowElement(edge)); + }); + + return { elements }; + }, +}); diff --git a/src/graphToExcalidraw.ts b/src/graphToExcalidraw.ts index 83b1606d..47744653 100644 --- a/src/graphToExcalidraw.ts +++ b/src/graphToExcalidraw.ts @@ -9,6 +9,8 @@ import { Class } from "./parser/class.js"; import { classToExcalidrawSkeletonConvertor } from "./converter/types/class.js"; import { ERD } from "./parser/er.js"; import { erToExcalidrawSkeletonConvertor } from "./converter/types/er.js"; +import { State } from "./parser/state.js"; +import { stateToExcalidrawSkeletonConvertor } from "./converter/types/state.js"; import type { LocalPoint } from "@excalidraw/excalidraw/math/types"; import { dedupeConsecutivePoints } from "./utils.js"; @@ -41,7 +43,7 @@ const normalizeLinearElementPoints = ( }; export const graphToExcalidraw = ( - graph: Flowchart | GraphImage | Sequence | Class | ERD, + graph: Flowchart | GraphImage | Sequence | Class | ERD | State, options: ExcalidrawConfig = {} ): MermaidToExcalidrawResult => { const result = (() => { @@ -66,6 +68,10 @@ export const graphToExcalidraw = ( return erToExcalidrawSkeletonConvertor.convert(graph, options); } + case "state": { + return stateToExcalidrawSkeletonConvertor.convert(graph, options); + } + default: { throw new Error( `graphToExcalidraw: unknown graph type "${ diff --git a/src/parseMermaid.ts b/src/parseMermaid.ts index 42acfe37..52688b45 100644 --- a/src/parseMermaid.ts +++ b/src/parseMermaid.ts @@ -3,6 +3,7 @@ import type { MermaidConfig } from "mermaid"; import type { Diagram } from "mermaid/dist/Diagram.js"; import type { FlowDB } from "mermaid/dist/diagrams/flowchart/flowDb.js"; import type { ErDB } from "mermaid/dist/diagrams/er/erDb.js"; +import type { StateDB } from "mermaid/dist/diagrams/state/stateDb.js"; import { GraphImage } from "./interfaces.js"; import { MERMAID_CONFIG } from "./constants.js"; @@ -11,6 +12,7 @@ import { Flowchart, parseMermaidFlowChartDiagram } from "./parser/flowchart.js"; import { Sequence, parseMermaidSequenceDiagram } from "./parser/sequence.js"; import { Class, parseMermaidClassDiagram } from "./parser/class.js"; import { ERD, parseMermaidERDiagram } from "./parser/er.js"; +import { State, parseMermaidStateDiagram } from "./parser/state.js"; import { runMermaidTaskSequentially } from "./mermaidExecutionQueue.js"; // Track initialization state to avoid redundant mermaid.initialize() calls @@ -61,7 +63,7 @@ const convertSvgToGraphImage = (svgContainer: HTMLDivElement) => { export const parseMermaid = async ( definition: string, config: MermaidConfig = MERMAID_CONFIG -): Promise => { +): Promise => { return runMermaidTaskSequentially(async () => { const resolvedFontSize = config.themeVariables?.fontSize ?? MERMAID_CONFIG.themeVariables.fontSize; @@ -115,7 +117,7 @@ export const parseMermaid = async ( // Append SVG to DOM temporarily to allow querying element dimensions/positions svgContainer.innerHTML = svg; - let data: Flowchart | GraphImage | Sequence | Class | ERD; + let data: Flowchart | GraphImage | Sequence | Class | ERD | State; try { switch (diagram.type) { @@ -140,6 +142,14 @@ export const parseMermaid = async ( data = parseMermaidERDiagram(diagram.db as ErDB, svgContainer); break; } + case "state": + case "stateDiagram": { + data = parseMermaidStateDiagram( + diagram.db as StateDB, + svgContainer + ); + break; + } default: { data = convertSvgToGraphImage(svgContainer); } diff --git a/src/parser/state.ts b/src/parser/state.ts new file mode 100644 index 00000000..9f4567f5 --- /dev/null +++ b/src/parser/state.ts @@ -0,0 +1,701 @@ +import { + ContainerStyle, + CONTAINER_STYLE_PROPERTY, + LABEL_STYLE_PROPERTY, + LabelStyle, + Position, +} from "../interfaces.js"; +import { + computeEdgePositions, + entityCodesToText, + getTransformAttr, +} from "../utils.js"; +import { + cleanCSSValue, + isValidCSSColor, + parseCSSDeclarations, +} from "./cssUtils.js"; + +import type { ExcalidrawLinearElement } from "@excalidraw/excalidraw/element/types"; +import type { + Edge as MermaidStateEdge, + NodeData as MermaidStateNode, + StateDB, +} from "mermaid/dist/diagrams/state/stateDb.js"; + +type StateShape = MermaidStateNode["shape"]; + +type DividerLine = { + startX: number; + startY: number; + endX: number; + endY: number; +}; + +export interface StateNode { + id: string; + shape: StateShape; + text: string; + description: string[]; + x: number; + y: number; + width: number; + height: number; + parentId?: string; + position?: string; + containerStyle: ContainerStyle; + labelStyle: LabelStyle; + dividerLine?: DividerLine; + endInnerColor?: string; + isRenderable: boolean; +} + +export interface StateEdge { + id: string; + start: string; + end: string; + text: string; + startX: number; + startY: number; + endX: number; + endY: number; + reflectionPoints: Position[]; + strokeColor?: string; + strokeWidth?: number; + strokeStyle?: ExcalidrawLinearElement["strokeStyle"]; + isNoteEdge?: boolean; +} + +export interface State { + type: "state"; + nodes: StateNode[]; + edges: StateEdge[]; +} + +const isMeaningfulCSSColor = (value?: string | null) => { + const normalizedValue = cleanCSSValue(value || ""); + if (!normalizedValue) { + return false; + } + + if ( + normalizedValue === "none" || + normalizedValue === "transparent" || + normalizedValue === "rgba(0, 0, 0, 0)" || + normalizedValue === "rgba(0,0,0,0)" + ) { + return false; + } + + return isValidCSSColor(normalizedValue); +}; + +const applyContainerStyleProperty = ( + style: ContainerStyle, + property: string, + value: string +) => { + switch (property) { + case CONTAINER_STYLE_PROPERTY.FILL: + case CONTAINER_STYLE_PROPERTY.STROKE: + if (isMeaningfulCSSColor(value)) { + style[property] = cleanCSSValue(value); + } + break; + case CONTAINER_STYLE_PROPERTY.STROKE_WIDTH: + case CONTAINER_STYLE_PROPERTY.STROKE_DASHARRAY: + if (cleanCSSValue(value)) { + style[property] = cleanCSSValue(value); + } + break; + } +}; + +const applyLabelStyleProperty = ( + style: LabelStyle, + property: string, + value: string +) => { + if (property === LABEL_STYLE_PROPERTY.COLOR && isMeaningfulCSSColor(value)) { + style[LABEL_STYLE_PROPERTY.COLOR] = cleanCSSValue(value); + } +}; + +const applyStyleTextToStyles = ( + styleText: string | null | undefined, + containerStyle: ContainerStyle, + labelStyle: LabelStyle +) => { + if (!styleText) { + return; + } + + parseCSSDeclarations(styleText).forEach(({ property, value }) => { + applyContainerStyleProperty(containerStyle, property, value); + applyLabelStyleProperty(labelStyle, property, value); + }); +}; + +const applyStyleTextToLabelStyle = ( + styleText: string | null | undefined, + labelStyle: LabelStyle +) => { + if (!styleText) { + return; + } + + parseCSSDeclarations(styleText).forEach(({ property, value }) => { + if ( + property === CONTAINER_STYLE_PROPERTY.FILL && + isMeaningfulCSSColor(value) + ) { + labelStyle[LABEL_STYLE_PROPERTY.COLOR] = cleanCSSValue(value); + return; + } + + applyLabelStyleProperty(labelStyle, property, value); + }); +}; + +const getExplicitStyleProperties = ( + styleTexts: Array +) => { + const properties = new Set(); + + styleTexts.filter(Boolean).forEach((styleText) => { + parseCSSDeclarations(styleText || "").forEach(({ property }) => { + properties.add(property); + }); + }); + + return properties; +}; + +const applyElementAttributesToContainerStyle = ( + element: Element | null, + containerStyle: ContainerStyle, + explicitProperties: Set +) => { + if (!element) { + return; + } + + const attrs: Array<[CONTAINER_STYLE_PROPERTY, string | null]> = [ + [CONTAINER_STYLE_PROPERTY.FILL, element.getAttribute("fill")], + [CONTAINER_STYLE_PROPERTY.STROKE, element.getAttribute("stroke")], + [ + CONTAINER_STYLE_PROPERTY.STROKE_WIDTH, + element.getAttribute("stroke-width"), + ], + [ + CONTAINER_STYLE_PROPERTY.STROKE_DASHARRAY, + element.getAttribute("stroke-dasharray"), + ], + ]; + + attrs.forEach(([property, rawValue]) => { + if (!explicitProperties.has(property)) { + return; + } + if (containerStyle[property]) { + return; + } + + const value = cleanCSSValue(rawValue || ""); + if (!value) { + return; + } + + applyContainerStyleProperty(containerStyle, property, value); + }); +}; + +const applyElementAttributesToLabelStyle = ( + element: Element | null, + labelStyle: LabelStyle, + explicitProperties: Set +) => { + if (!element) { + return; + } + + const candidates = [ + element, + ...Array.from( + element.querySelectorAll("text, foreignObject, div, span, p") + ), + ]; + + for (const candidate of candidates) { + if (labelStyle[LABEL_STYLE_PROPERTY.COLOR]) { + break; + } + + if ( + explicitProperties.has(LABEL_STYLE_PROPERTY.COLOR) || + explicitProperties.has(CONTAINER_STYLE_PROPERTY.FILL) + ) { + applyStyleTextToLabelStyle(candidate.getAttribute("style"), labelStyle); + if (labelStyle[LABEL_STYLE_PROPERTY.COLOR]) { + break; + } + } + + const color = cleanCSSValue( + candidate.getAttribute("fill") || candidate.getAttribute("color") || "" + ); + if ( + (explicitProperties.has(LABEL_STYLE_PROPERTY.COLOR) || + explicitProperties.has(CONTAINER_STYLE_PROPERTY.FILL)) && + isMeaningfulCSSColor(color) + ) { + labelStyle[LABEL_STYLE_PROPERTY.COLOR] = color; + } + } +}; + +const accumulateTranslation = (node: Element, stopAt?: Element | null) => { + let tx = 0; + let ty = 0; + let current: Element | null = node; + + while (current && current !== stopAt) { + const { transformX, transformY } = getTransformAttr(current); + tx += transformX; + ty += transformY; + current = current.parentElement; + } + + return { tx, ty }; +}; + +const getAbsoluteBounds = (node: SVGGraphicsElement, containerEl: Element) => { + const bbox = node.getBBox(); + const { tx, ty } = accumulateTranslation(node, containerEl); + + return { + x: bbox.x + tx, + y: bbox.y + ty, + width: bbox.width, + height: bbox.height, + }; +}; + +const getDividerLine = ( + nodeEl: Element, + containerEl: Element +): DividerLine | undefined => { + const dividerLine = nodeEl.querySelector("line.divider"); + if (!dividerLine) { + return undefined; + } + + const { tx, ty } = accumulateTranslation(dividerLine, containerEl); + + return { + startX: Number(dividerLine.getAttribute("x1")) + tx, + startY: Number(dividerLine.getAttribute("y1")) + ty, + endX: Number(dividerLine.getAttribute("x2")) + tx, + endY: Number(dividerLine.getAttribute("y2")) + ty, + }; +}; + +const getShapeArea = (element: SVGGraphicsElement) => { + const bounds = element.getBBox(); + return Math.abs(bounds.width * bounds.height); +}; + +const getExplicitStyleValue = (element: Element, property: string) => { + const styleText = element.getAttribute("style"); + if (!styleText) { + return undefined; + } + + const declaration = parseCSSDeclarations(styleText).find( + (entry) => entry.property === property + ); + if (!declaration) { + return undefined; + } + + return cleanCSSValue(declaration.value); +}; + +const pickShapeElement = ( + elements: SVGGraphicsElement[], + strategy: "largest" | "smallest" +) => { + const candidates = elements + .map((element) => ({ element, area: getShapeArea(element) })) + .filter(({ area }) => Number.isFinite(area) && area > 0); + + if (candidates.length === 0) { + return null; + } + + const sortedCandidates = candidates.sort((left, right) => + strategy === "largest" ? right.area - left.area : left.area - right.area + ); + + return sortedCandidates[0].element; +}; + +const getElementShapeColor = ( + element: Element | null, + explicitProperties: Set +) => { + if (!element) { + return undefined; + } + + if ( + !explicitProperties.has(CONTAINER_STYLE_PROPERTY.FILL) && + !explicitProperties.has(CONTAINER_STYLE_PROPERTY.STROKE) + ) { + return undefined; + } + + const fill = cleanCSSValue( + element.getAttribute("fill") || + getExplicitStyleValue(element, CONTAINER_STYLE_PROPERTY.FILL) || + "" + ); + const stroke = cleanCSSValue( + element.getAttribute("stroke") || + getExplicitStyleValue(element, CONTAINER_STYLE_PROPERTY.STROKE) || + "" + ); + + if (isMeaningfulCSSColor(fill)) { + return fill; + } + + if (isMeaningfulCSSColor(stroke)) { + return stroke; + } + + return undefined; +}; + +const getEndStateInnerColor = ( + nodeEl: SVGGraphicsElement, + explicitProperties: Set +) => { + const shapeElements = Array.from( + nodeEl.querySelectorAll("circle, ellipse, path") + ); + const innerShapeElement = pickShapeElement(shapeElements, "smallest"); + + return getElementShapeColor(innerShapeElement, explicitProperties); +}; + +const trimSharedTrailingLineIndentation = (lines: string[]) => { + if (lines.length < 2) { + return lines; + } + + const trailingLines = lines.slice(1); + const commonIndent = trailingLines + .filter((line) => line.trim().length > 0) + .reduce((minIndent, line) => { + const indent = line.match(/^\s*/)?.[0].length ?? 0; + return Math.min(minIndent, indent); + }, Number.POSITIVE_INFINITY); + + if (!Number.isFinite(commonIndent) || commonIndent <= 0) { + return lines.map((line) => line.trimEnd()); + } + + return [ + lines[0].trimEnd(), + ...trailingLines.map((line) => + line.replace(new RegExp(`^\\s{0,${commonIndent}}`), "").trimEnd() + ), + ]; +}; + +const getStateLabelText = (node: MermaidStateNode) => { + const labelLines = Array.isArray(node.label) + ? node.label.map((entry) => entityCodesToText(entry)) + : entityCodesToText(node.label || "").split("\n"); + const labelText = trimSharedTrailingLineIndentation(labelLines).join("\n"); + + return labelText; +}; + +const getStateDescription = (node: MermaidStateNode) => { + if (!node.description) { + return []; + } + + const description = Array.isArray(node.description) + ? node.description + : [node.description]; + + return description + .map((entry) => entityCodesToText(entry)) + .filter((entry) => entry.length > 0); +}; + +const createNodeElementResolver = (containerEl: Element) => { + const usedElements = new Set(); + + const markAndReturn = (element: SVGGraphicsElement | null) => { + if (element) { + usedElements.add(element); + } + + return element; + }; + + const getNextUnusedCandidate = (elements: SVGGraphicsElement[]) => { + const candidate = elements.find((element) => !usedElements.has(element)); + return markAndReturn(candidate || null); + }; + + return (node: MermaidStateNode): SVGGraphicsElement | null => { + const selectors = [ + `[id='${node.domId}']`, + `[id='${node.id}']`, + `[data-id='${node.id}']`, + ]; + + for (const selector of selectors) { + const element = containerEl.querySelector(selector); + if (element) { + return markAndReturn(element); + } + } + + // Mermaid generates random ids for anonymous divider sections during parse + // and regenerates them during render, so exact ids do not always line up. + switch (node.shape) { + case "divider": + return getNextUnusedCandidate( + Array.from( + containerEl.querySelectorAll( + "g.statediagram-cluster-alt" + ) + ) + ); + case "stateStart": + return getNextUnusedCandidate( + Array.from( + containerEl.querySelectorAll("g.node.default") + ).filter((element) => element.querySelector("circle.state-start")) + ); + case "stateEnd": + return getNextUnusedCandidate( + Array.from( + containerEl.querySelectorAll("g.node.default") + ).filter((element) => !element.querySelector("circle.state-start")) + ); + default: + return null; + } + }; +}; + +const findShapeElement = ( + nodeEl: SVGGraphicsElement, + shape: StateShape +): SVGGraphicsElement => { + switch (shape) { + case "roundedWithTitle": + return ( + nodeEl.querySelector("rect.outer") || + nodeEl.querySelector("rect") || + nodeEl + ); + case "divider": + return ( + nodeEl.querySelector("rect.divider") || + nodeEl.querySelector("rect") || + nodeEl + ); + case "rectWithTitle": + return ( + nodeEl.querySelector("rect.outer") || + nodeEl.querySelector("rect") || + nodeEl + ); + case "stateStart": + return ( + pickShapeElement( + Array.from( + nodeEl.querySelectorAll("circle, ellipse, path") + ), + "largest" + ) || nodeEl + ); + case "stateEnd": + return ( + pickShapeElement( + Array.from( + nodeEl.querySelectorAll("circle, ellipse, path") + ), + "largest" + ) || nodeEl + ); + case "choice": + case "fork": + case "join": + case "note": + case "rect": + default: + return ( + nodeEl.querySelector( + "rect, path, circle, ellipse, polygon" + ) || nodeEl + ); + } +}; + +const parseStateNode = ( + node: MermaidStateNode, + containerEl: Element, + resolveNodeElement: ReturnType +): StateNode => { + const nodeEl = resolveNodeElement(node); + if (!nodeEl) { + throw new Error(`State node element not found for "${node.id}"`); + } + + const shapeEl = findShapeElement(nodeEl, node.shape); + const containerStyle: ContainerStyle = {}; + const labelStyle: LabelStyle = {}; + const explicitStyleTexts = [ + node.labelStyle, + ...(node.cssCompiledStyles || []), + ...(node.cssStyles || []), + ]; + const explicitStyleProperties = + getExplicitStyleProperties(explicitStyleTexts); + + explicitStyleTexts.filter(Boolean).forEach((styleText) => { + applyStyleTextToStyles(styleText, containerStyle, labelStyle); + }); + + applyElementAttributesToContainerStyle( + shapeEl, + containerStyle, + explicitStyleProperties + ); + applyElementAttributesToLabelStyle( + nodeEl, + labelStyle, + explicitStyleProperties + ); + + const bounds = getAbsoluteBounds(shapeEl, containerEl); + + return { + id: node.id, + shape: node.shape, + text: getStateLabelText(node), + description: getStateDescription(node), + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + parentId: node.parentId, + position: node.position, + containerStyle, + labelStyle, + dividerLine: + node.shape === "rectWithTitle" + ? getDividerLine(nodeEl, containerEl) + : undefined, + endInnerColor: + node.shape === "stateEnd" + ? getEndStateInnerColor(nodeEl, explicitStyleProperties) + : undefined, + isRenderable: node.shape !== "noteGroup", + }; +}; + +const parseStateEdge = ( + edge: MermaidStateEdge, + containerEl: Element +): StateEdge | null => { + const edgeEl = containerEl.querySelector(`[id='${edge.id}']`); + if (!edgeEl) { + return null; + } + + const { tx, ty } = accumulateTranslation(edgeEl, containerEl); + const edgePositionData = computeEdgePositions( + edgeEl, + { x: tx, y: ty }, + "MCL" + ); + + if (edgePositionData.reflectionPoints.length < 2) { + return null; + } + + const edgeStyle: { + strokeColor?: string; + strokeWidth?: number; + strokeStyle?: ExcalidrawLinearElement["strokeStyle"]; + } = {}; + const applyEdgeStyleProperty = (property: string, value: string) => { + switch (property) { + case CONTAINER_STYLE_PROPERTY.STROKE: + if (isMeaningfulCSSColor(value)) { + edgeStyle.strokeColor = cleanCSSValue(value); + } + break; + case CONTAINER_STYLE_PROPERTY.STROKE_WIDTH: { + const strokeWidth = parseFloat(cleanCSSValue(value)); + if (Number.isFinite(strokeWidth) && strokeWidth > 0) { + edgeStyle.strokeWidth = strokeWidth; + } + break; + } + case CONTAINER_STYLE_PROPERTY.STROKE_DASHARRAY: + if (cleanCSSValue(value)) { + edgeStyle.strokeStyle = "dashed"; + } + break; + } + }; + + [edge.style].filter(Boolean).forEach((styleText) => { + parseCSSDeclarations(styleText || "").forEach(({ property, value }) => { + applyEdgeStyleProperty(property, value); + }); + }); + const isNoteEdge = + edge.arrowhead === "none" || edge.classes?.includes("note-edge"); + + return { + id: edge.id, + start: edge.start, + end: edge.end, + text: entityCodesToText(edge.label || ""), + ...edgePositionData, + strokeColor: edgeStyle.strokeColor, + strokeWidth: edgeStyle.strokeWidth, + strokeStyle: isNoteEdge ? "dashed" : edgeStyle.strokeStyle, + isNoteEdge, + }; +}; + +export const parseMermaidStateDiagram = ( + db: StateDB, + containerEl: Element +): State => { + const { nodes, edges } = db.getData(); + const resolveNodeElement = createNodeElementResolver(containerEl); + + return { + type: "state", + nodes: nodes.map((node) => + parseStateNode(node, containerEl, resolveNodeElement) + ), + edges: edges + .map((edge) => parseStateEdge(edge, containerEl)) + .filter((edge): edge is StateEdge => edge !== null), + }; +}; diff --git a/src/utils.ts b/src/utils.ts index f4a3e647..59be7166 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -154,7 +154,8 @@ interface EdgePositionData { // Compute edge postion start, end and points (reflection points) export const computeEdgePositions = ( pathElement: SVGPathElement, - offset: Position = { x: 0, y: 0 } + offset: Position = { x: 0, y: 0 }, + commandsPattern = "LM" ): EdgePositionData => { // Check if the element is a path else throw an error if (pathElement.tagName.toLowerCase() !== "path") { @@ -169,9 +170,10 @@ export const computeEdgePositions = ( throw new Error('Path element does not contain a "d" attribute'); } - // Split the d attribute based on M (Move To) and L (Line To) commands - // eg "M29.383,38.5L29.383,63.5L29.383,83.2" => ["M29.383,38.5", "L29.383,63.5", "L29.383,83.2"] - const commands = dAttr.split(/(?=[LM])/); + // Split the d attribute based on the supported SVG path commands. + // Ex: "M29.383,38.5L29.383,63.5L29.383,83.2" + // => ["M29.383,38.5", "L29.383,63.5", "L29.383,83.2"] + const commands = dAttr.split(new RegExp(`(?=[${commandsPattern}])`)); // Get the start position from the first commands element => [29.383,38.5] const startPosition = commands[0] @@ -189,11 +191,21 @@ export const computeEdgePositions = ( // These includes the start and end points and also points which are not the same as the previous points const reflectionPoints = commands .map((command) => { + const commandType = command[0]; const coords = command .substring(1) .split(",") .map((coord) => parseFloat(coord)); - return { x: coords[0], y: coords[1] }; + + if (commandType === "C") { + return { + x: coords[4], + y: coords[5], + command: commandType, + }; + } + + return { x: coords[0], y: coords[1], command: commandType }; }) .filter((point, index, array) => { // Always include the last point @@ -206,6 +218,12 @@ export const computeEdgePositions = ( return false; } + // Exclude the second last point for curves because the end point + // already captures the rendered segment end. + if (index === array.length - 2 && point.command === "C") { + return false; + } + // The below check is exclusively for second last point if ( index === array.length - 2 && diff --git a/tests/examples.test.ts b/tests/examples.test.ts index e91988d9..798bb8f8 100644 --- a/tests/examples.test.ts +++ b/tests/examples.test.ts @@ -6,6 +6,7 @@ import { CLASS_DIAGRAM_TESTCASES } from "../playground/testcases/class.ts"; import { ERD_DIAGRAM_TESTCASES } from "../playground/testcases/er.ts"; import { FLOWCHART_DIAGRAM_TESTCASES } from "../playground/testcases/flowchart.ts"; import { SEQUENCE_DIAGRAM_TESTCASES } from "../playground/testcases/sequence.ts"; +import { STATE_DIAGRAM_TESTCASES } from "../playground/testcases/state.ts"; import { UNSUPPORTED_DIAGRAM_TESTCASES } from "../playground/testcases/unsupported.ts"; const PLAYGROUND_TESTCASES = [ @@ -13,6 +14,7 @@ const PLAYGROUND_TESTCASES = [ ...SEQUENCE_DIAGRAM_TESTCASES, ...CLASS_DIAGRAM_TESTCASES, ...ERD_DIAGRAM_TESTCASES, + ...STATE_DIAGRAM_TESTCASES, ...UNSUPPORTED_DIAGRAM_TESTCASES, ]; diff --git a/tests/state.test.ts b/tests/state.test.ts new file mode 100644 index 00000000..de26ab95 --- /dev/null +++ b/tests/state.test.ts @@ -0,0 +1,542 @@ +import { graphToExcalidraw } from "../src/graphToExcalidraw.js"; +import { parseMermaid } from "../src/parseMermaid.js"; + +type BBox = { + x: number; + y: number; + width: number; + height: number; +}; + +const zeroBox = (): BBox => ({ x: 0, y: 0, width: 0, height: 0 }); + +const numberAttr = (element: Element, attribute: string, fallback = 0) => { + const rawValue = element.getAttribute(attribute); + return rawValue === null ? fallback : Number(rawValue); +}; + +const unionBoxes = (boxes: BBox[]): BBox => { + if (boxes.length === 0) { + return zeroBox(); + } + + const minX = Math.min(...boxes.map((box) => box.x)); + const minY = Math.min(...boxes.map((box) => box.y)); + const maxX = Math.max(...boxes.map((box) => box.x + box.width)); + const maxY = Math.max(...boxes.map((box) => box.y + box.height)); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; +}; + +const parsePointsBox = (points: string | null): BBox => { + if (!points) { + return zeroBox(); + } + + const coordinates = points + .trim() + .split(/\s+/) + .map((pair) => pair.split(",").map((value) => Number(value))) + .filter( + (pair): pair is [number, number] => + pair.length === 2 && + Number.isFinite(pair[0]) && + Number.isFinite(pair[1]) + ); + + if (coordinates.length === 0) { + return zeroBox(); + } + + const xs = coordinates.map(([x]) => x); + const ys = coordinates.map(([, y]) => y); + + return { + x: Math.min(...xs), + y: Math.min(...ys), + width: Math.max(...xs) - Math.min(...xs), + height: Math.max(...ys) - Math.min(...ys), + }; +}; + +const parsePathBox = (pathData: string | null): BBox => { + if (!pathData) { + return zeroBox(); + } + + const numericTokens = Array.from( + pathData.matchAll(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi), + (match) => Number(match[0]) + ); + + if (numericTokens.length < 2) { + return zeroBox(); + } + + const coordinates: Array<[number, number]> = []; + for (let index = 0; index < numericTokens.length - 1; index += 2) { + coordinates.push([numericTokens[index], numericTokens[index + 1]]); + } + + const xs = coordinates.map(([x]) => x); + const ys = coordinates.map(([, y]) => y); + + return { + x: Math.min(...xs), + y: Math.min(...ys), + width: Math.max(...xs) - Math.min(...xs), + height: Math.max(...ys) - Math.min(...ys), + }; +}; + +const getMockBBox = (element: SVGElement): BBox => { + const tagName = element.tagName.toLowerCase(); + + switch (tagName) { + case "rect": + case "foreignobject": + case "image": + return { + x: numberAttr(element, "x"), + y: numberAttr(element, "y"), + width: numberAttr(element, "width"), + height: numberAttr(element, "height"), + }; + case "circle": { + const r = numberAttr(element, "r"); + const cx = numberAttr(element, "cx"); + const cy = numberAttr(element, "cy"); + return { x: cx - r, y: cy - r, width: r * 2, height: r * 2 }; + } + case "ellipse": { + const rx = numberAttr(element, "rx"); + const ry = numberAttr(element, "ry"); + const cx = numberAttr(element, "cx"); + const cy = numberAttr(element, "cy"); + return { x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2 }; + } + case "line": { + const x1 = numberAttr(element, "x1"); + const y1 = numberAttr(element, "y1"); + const x2 = numberAttr(element, "x2"); + const y2 = numberAttr(element, "y2"); + return { + x: Math.min(x1, x2), + y: Math.min(y1, y2), + width: Math.abs(x2 - x1), + height: Math.abs(y2 - y1), + }; + } + case "polygon": + case "polyline": + return parsePointsBox(element.getAttribute("points")); + case "path": + return parsePathBox(element.getAttribute("d")); + case "text": + case "tspan": { + const x = numberAttr(element, "x"); + const y = numberAttr(element, "y"); + const text = element.textContent ?? ""; + return { + x, + y, + width: Math.max(text.length * 8, 1), + height: 16, + }; + } + default: { + const childBoxes = Array.from(element.children) + .filter((child): child is SVGElement => child instanceof SVGElement) + .map((child) => getMockBBox(child)) + .filter((box) => box.width > 0 || box.height > 0); + + return unionBoxes(childBoxes); + } + } +}; + +describe("state diagrams", () => { + const originalGetBBox = SVGElement.prototype.getBBox; + const originalGetBoundingClientRect = + SVGElement.prototype.getBoundingClientRect; + const originalGetComputedTextLength = + SVGElement.prototype.getComputedTextLength; + + beforeAll(() => { + Object.defineProperty(SVGElement.prototype, "getBBox", { + configurable: true, + value: function getBBox() { + return getMockBBox(this); + }, + }); + + Object.defineProperty(SVGElement.prototype, "getBoundingClientRect", { + configurable: true, + value: function getBoundingClientRect() { + const box = getMockBBox(this); + return { + ...box, + top: box.y, + right: box.x + box.width, + bottom: box.y + box.height, + left: box.x, + toJSON: () => box, + }; + }, + }); + + Object.defineProperty(SVGElement.prototype, "getComputedTextLength", { + configurable: true, + value: function getComputedTextLength() { + return Math.max((this.textContent ?? "").length * 8, 1); + }, + }); + }); + + afterAll(() => { + Object.defineProperty(SVGElement.prototype, "getBBox", { + configurable: true, + value: originalGetBBox, + }); + + Object.defineProperty(SVGElement.prototype, "getBoundingClientRect", { + configurable: true, + value: originalGetBoundingClientRect, + }); + + Object.defineProperty(SVGElement.prototype, "getComputedTextLength", { + configurable: true, + value: originalGetComputedTextLength, + }); + }); + + it("renders note edges without arrowheads and choice states as diamonds", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + state Decision <> + [*] --> Input + Input --> Decision + note right of Input: Capture payload + Decision --> Accept: valid + Decision --> Reject: invalid`); + + expect(graph.type).toBe("state"); + + const result = graphToExcalidraw(graph); + expect( + result.elements.find((element: any) => element.type === "diamond") + ).toBeTruthy(); + expect( + result.elements.find( + (element: any) => + element.type === "rectangle" && + element.label?.text === "Capture payload" + ) + ).toBeTruthy(); + expect( + result.elements.find((element: any) => element.id === "Input") + ).toMatchObject({ + label: { + fontSize: 18, + }, + }); + expect( + result.elements.find( + (element: any) => + element.type === "arrow" && + element.endArrowhead === null && + element.strokeStyle === "dashed" + ) + ).toBeTruthy(); + expect( + result.elements + .filter( + (element: any) => + element.type === "arrow" && element.endArrowhead === "triangle" + ) + .every((element: any) => element.strokeStyle === "solid") + ).toBe(true); + }); + + it("preserves state class and direct style overrides", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + classDef movement fill:#f00,color:white,font-weight:bold,stroke-width:2px,stroke:yellow + A --> B + class A,B movement + style B fill:#0f0,stroke:#333,stroke-width:4px,color:#00f`); + + expect(graph.type).toBe("state"); + + const result = graphToExcalidraw(graph); + expect( + result.elements.find((element: any) => element.id === "A") + ).toMatchObject({ + backgroundColor: "#f00", + strokeColor: "yellow", + strokeWidth: 2, + label: { + strokeColor: "white", + }, + }); + expect( + result.elements.find((element: any) => element.id === "B") + ).toMatchObject({ + backgroundColor: "#0f0", + strokeColor: "#333", + strokeWidth: 4, + label: { + strokeColor: "#00f", + }, + }); + }); + + it("does not preserve Mermaid default colors for unstyled states and notes", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + [*] --> Idle + Idle --> Active + note right of Active: Default note + Active --> [*]`); + + expect(graph.type).toBe("state"); + + const startNode = graph.nodes.find((node) => node.shape === "stateStart"); + const endNode = graph.nodes.find((node) => node.shape === "stateEnd"); + const result = graphToExcalidraw(graph); + const idle = result.elements.find((element: any) => element.id === "Idle"); + const active = result.elements.find( + (element: any) => element.id === "Active" + ); + const note = result.elements.find( + (element: any) => + element.type === "rectangle" && element.label?.text === "Default note" + ); + const start = result.elements.find( + (element: any) => element.id === startNode?.id + ); + const endInner = result.elements.find( + (element: any) => element.id === `${endNode?.id}__inner` + ); + + expect(idle?.backgroundColor).toBeUndefined(); + expect(idle?.strokeColor).toBeUndefined(); + expect(active?.backgroundColor).toBeUndefined(); + expect(active?.strokeColor).toBeUndefined(); + expect(note?.backgroundColor).toBeUndefined(); + expect(note?.strokeColor).toBeUndefined(); + expect(start).toMatchObject({ + type: "ellipse", + backgroundColor: "#000000", + strokeColor: "#000000", + }); + expect(endInner).toMatchObject({ + type: "ellipse", + backgroundColor: "#000000", + strokeColor: "#000000", + }); + }); + + it("renders titled states with a divider line without grouping", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + state Group { + state "Header" as A + A : body + }`); + + expect(graph.type).toBe("state"); + + const result = graphToExcalidraw(graph); + const group = result.elements.find( + (element: any) => element.id === "Group" + ); + const titledState = result.elements.find( + (element: any) => element.id === "A" + ); + const divider = result.elements.find( + (element: any) => element.id === "A__divider" + ); + + expect(group).toBeTruthy(); + expect(titledState).toMatchObject({ + type: "rectangle", + label: { + text: "Header\nbody", + verticalAlign: "top", + }, + }); + expect(group?.groupIds).toBeUndefined(); + expect(titledState?.groupIds).toBeUndefined(); + expect(titledState?.label?.groupIds).toBeUndefined(); + expect(divider?.groupIds).toBeUndefined(); + }); + + it("renders end states with a colored inner marker", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + [*] --> Active + Active --> [*]`); + + expect(graph.type).toBe("state"); + + const result = graphToExcalidraw(graph); + const endState = result.elements.find( + (element: any) => + element.type === "ellipse" && + !String(element.id).endsWith("__inner") && + element.id !== "[*]_start" + ); + const endStateInner = result.elements.find((element: any) => + String(element.id).endsWith("__inner") + ); + + expect(endState).toBeTruthy(); + expect(endStateInner).toMatchObject({ + type: "ellipse", + }); + expect(endStateInner?.backgroundColor).toBe(endStateInner?.strokeColor); + expect(endStateInner?.backgroundColor).toBeTruthy(); + expect(endStateInner?.width).toBeLessThan(endState?.width ?? Infinity); + expect(endStateInner?.height).toBeLessThan(endState?.height ?? Infinity); + }); + + it("renders fork and join pseudo states as unlabeled bars", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + state fork_state <> + [*] --> fork_state + fork_state --> State2 + fork_state --> State3 + + state join_state <> + State2 --> join_state + State3 --> join_state + join_state --> State4 + State4 --> [*]`); + + expect(graph.type).toBe("state"); + + const forkAndJoinNodes = graph.nodes.filter( + (node) => node.shape === "fork" || node.shape === "join" + ); + expect(forkAndJoinNodes).toHaveLength(2); + + const result = graphToExcalidraw(graph); + for (const node of forkAndJoinNodes) { + const element = result.elements.find( + (entry: any) => entry.id === node.id + ); + expect(element).toMatchObject({ + type: "rectangle", + backgroundColor: "#000000", + strokeColor: "#000000", + }); + expect((element as any)?.label).toBeUndefined(); + expect( + Math.min( + (element as any)?.width ?? Infinity, + (element as any)?.height ?? Infinity + ) + ).toBeLessThanOrEqual(12); + } + }); + + it("renders multiline notes from the docs as note rectangles with dashed note edges", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + State1: The state with a note + note right of State1 + Important information! + You can write notes. + end note + State1 --> State2 + note left of State2 : This is the note to the left.`); + + expect(graph.type).toBe("state"); + + const result = graphToExcalidraw(graph); + const state1Node = graph.nodes.find((node) => node.id === "State1"); + const multilineNoteNode = graph.nodes.find( + (node) => + node.shape === "note" && + node.text === "Important information!\nYou can write notes." + ); + const leftNoteNode = graph.nodes.find( + (node) => + node.shape === "note" && node.text === "This is the note to the left." + ); + const state2Node = graph.nodes.find((node) => node.id === "State2"); + const notedState = result.elements.find( + (element: any) => element.id === "State1" + ); + const multilineNote = result.elements.find( + (element: any) => + element.type === "rectangle" && + typeof element.label?.text === "string" && + element.label.text.includes("Important information!") + ); + const leftNote = result.elements.find( + (element: any) => + element.type === "rectangle" && + element.label?.text === "This is the note to the left." + ); + const state2 = result.elements.find( + (element: any) => element.id === "State2" + ); + + expect( + multilineNote && + multilineNote.label?.text.includes("You can write notes.") + ).toBe(true); + expect(multilineNote?.label?.text).toBe( + "Important information!\nYou can write notes." + ); + expect( + result.elements.find( + (element: any) => + element.type === "rectangle" && + element.label?.text === "This is the note to the left." + ) + ).toBeTruthy(); + expect(notedState?.width).toBe(state1Node?.width); + expect(multilineNote?.width).toBe(multilineNoteNode?.width); + expect(leftNote?.width).toBe(leftNoteNode?.width); + expect(state2?.width).toBe(state2Node?.width); + expect(notedState?.label?.fontSize).toBe(18); + expect(leftNote?.label?.fontSize).toBe(18); + expect(state2?.label?.fontSize).toBe(18); + expect( + result.elements.filter( + (element: any) => + element.type === "arrow" && + element.endArrowhead === null && + element.strokeStyle === "dashed" + ) + ).toHaveLength(2); + }); + + it("supports spaces in state names together with ::: styling", async () => { + const graph = await parseMermaid(`stateDiagram-v2 + classDef yourState fill:#ffec99,stroke:#c92a2a,color:#1864ab,stroke-width:2px + yswsii: Your state with spaces in it + [*] --> yswsii:::yourState + yswsii --> YetAnotherState + YetAnotherState --> [*]`); + + expect(graph.type).toBe("state"); + + const result = graphToExcalidraw(graph); + expect( + result.elements.find((element: any) => element.id === "yswsii") + ).toMatchObject({ + type: "rectangle", + backgroundColor: "#ffec99", + strokeColor: "#c92a2a", + strokeWidth: 2, + label: { + text: "Your state with spaces in it", + fontSize: 18, + strokeColor: "#1864ab", + }, + }); + }); +}); diff --git a/visual-tests/main.ts b/visual-tests/main.ts index fc99a85c..32e8cdb3 100644 --- a/visual-tests/main.ts +++ b/visual-tests/main.ts @@ -2,9 +2,10 @@ import mermaid from "mermaid"; import { convertToExcalidrawElements, exportToSvg, - Fonts, FONT_FAMILY, } from "@excalidraw/excalidraw"; +import { charWidth } from "@excalidraw/element"; +import { getFontString } from "@excalidraw/common"; import { DEFAULT_FONT_SIZE, MERMAID_CONFIG } from "../src/constants"; import { graphToExcalidraw } from "../src/graphToExcalidraw"; @@ -15,7 +16,9 @@ import { FLOWCHART_DIAGRAM_TESTCASES } from "../playground/testcases/flowchart"; import { SEQUENCE_DIAGRAM_TESTCASES } from "../playground/testcases/sequence"; import { CLASS_DIAGRAM_TESTCASES } from "../playground/testcases/class"; import { ERD_DIAGRAM_TESTCASES } from "../playground/testcases/er"; +import { STATE_DIAGRAM_TESTCASES } from "../playground/testcases/state"; import { UNSUPPORTED_DIAGRAM_TESTCASES } from "../playground/testcases/unsupported"; +import { ensureExcalidrawFontsLoaded } from "../playground/loadExcalidrawFonts"; interface TestCase { type: string; @@ -28,6 +31,7 @@ const ALL_TESTCASES: TestCase[] = [ ...SEQUENCE_DIAGRAM_TESTCASES, ...CLASS_DIAGRAM_TESTCASES, ...ERD_DIAGRAM_TESTCASES, + ...STATE_DIAGRAM_TESTCASES, ...UNSUPPORTED_DIAGRAM_TESTCASES, ]; @@ -39,6 +43,8 @@ mermaid.initialize({ const mermaidOutput = document.getElementById("mermaid-output")!; const excalidrawOutput = document.getElementById("excalidraw-output")!; const errorDiv = document.getElementById("error")!; +const SVG_EXPORT_PADDING = 4; +const EXCALIDRAW_SVG_EXPORT_PADDING = 16; let renderCounter = 0; let currentRenderGeneration = 0; @@ -49,6 +55,124 @@ const cleanupMermaidTempContainers = () => { .forEach((el) => el.remove()); }; +const clearExcalidrawTextMeasureCache = ( + elements: ReadonlyArray<{ + fontSize?: unknown; + label?: { fontSize?: unknown }; + }> +) => { + const fontSizes = new Set([DEFAULT_FONT_SIZE]); + + elements.forEach((element) => { + if (typeof element.fontSize === "number") { + fontSizes.add(element.fontSize); + } + if (typeof element.label?.fontSize === "number") { + fontSizes.add(element.label.fontSize); + } + }); + + fontSizes.forEach((fontSize) => { + charWidth.clearCache( + getFontString({ + fontSize, + fontFamily: FONT_FAMILY.Excalifont, + }) + ); + }); +}; + +const getSvgViewBox = (svg: SVGSVGElement) => { + const viewBox = svg.viewBox.baseVal; + if (viewBox && viewBox.width > 0 && viewBox.height > 0) { + return { + x: viewBox.x, + y: viewBox.y, + width: viewBox.width, + height: viewBox.height, + }; + } + + const width = Number(svg.getAttribute("width")) || 0; + const height = Number(svg.getAttribute("height")) || 0; + return { x: 0, y: 0, width, height }; +}; + +const getSvgContentRects = ( + svg: SVGSVGElement, + source: "mermaid" | "excalidraw" +) => { + if (source === "mermaid") { + return Array.from( + svg.querySelectorAll( + "g, path, rect, circle, ellipse, polygon, polyline, line, foreignObject, text" + ) + ) + .filter((element) => !element.closest("defs")) + .map((element) => element.getBoundingClientRect()) + .filter((rect) => rect.width > 0 || rect.height > 0); + } + + return Array.from(svg.children) + .filter( + (element) => + element instanceof SVGGraphicsElement && + element.tagName !== "metadata" && + element.tagName !== "defs" && + !( + element.tagName === "rect" && + element.parentElement === svg && + element.hasAttribute("fill") + ) + ) + .map((element) => element.getBoundingClientRect()) + .filter((rect) => rect.width > 0 || rect.height > 0); +}; + +const cropSvgToRenderedContent = ( + svg: SVGSVGElement, + source: "mermaid" | "excalidraw", + padding = SVG_EXPORT_PADDING +) => { + const contentRects = getSvgContentRects(svg, source); + if (contentRects.length === 0) { + return; + } + + const svgRect = svg.getBoundingClientRect(); + if (!svgRect.width || !svgRect.height) { + return; + } + + const currentViewBox = getSvgViewBox(svg); + if (!currentViewBox.width || !currentViewBox.height) { + return; + } + + const minLeft = Math.min(...contentRects.map((rect) => rect.left)); + const minTop = Math.min(...contentRects.map((rect) => rect.top)); + const maxRight = Math.max(...contentRects.map((rect) => rect.right)); + const maxBottom = Math.max(...contentRects.map((rect) => rect.bottom)); + + const scaleX = currentViewBox.width / svgRect.width; + const scaleY = currentViewBox.height / svgRect.height; + + const nextViewBox = { + x: currentViewBox.x + (minLeft - svgRect.left) * scaleX - padding, + y: currentViewBox.y + (minTop - svgRect.top) * scaleY - padding, + width: (maxRight - minLeft) * scaleX + padding * 2, + height: (maxBottom - minTop) * scaleY + padding * 2, + }; + + svg.setAttribute( + "viewBox", + `${nextViewBox.x} ${nextViewBox.y} ${nextViewBox.width} ${nextViewBox.height}` + ); + svg.setAttribute("width", `${nextViewBox.width}`); + svg.setAttribute("height", `${nextViewBox.height}`); + svg.style.maxWidth = ""; +}; + // ── Render cache (dev only) ── // Stores rendered SVG HTML per test case index so switching is instant. // Cleared on Vite HMR updates. @@ -104,7 +228,7 @@ async function renderTestCase(index: number): Promise { document.body.appendChild(tempContainer); const { svg: mermaidSvg } = await runMermaidTaskSequentially(() => - mermaid.render(renderId, testcase.definition, tempContainer), + mermaid.render(renderId, testcase.definition, tempContainer) ); tempContainer.remove(); if (isStale()) { @@ -113,24 +237,9 @@ async function renderTestCase(index: number): Promise { mermaidOutput.innerHTML = mermaidSvg; - // Mermaid adds internal padding around diagram content. Use getBBox() - // to find the actual content bounds and crop the viewBox to fit tightly. - // Skip for extreme aspect ratios (e.g. Gantt charts) where cropping - // would make the SVG too short when CSS constrains the width. const mRenderedSvg = mermaidOutput.querySelector("svg"); if (mRenderedSvg) { - const PAD = 4; // small breathing room - const bbox = mRenderedSvg.getBBox(); - const vbW = bbox.width + PAD * 2; - const vbH = bbox.height + PAD * 2; - if (vbW / vbH < 10) { - const vbX = bbox.x - PAD; - const vbY = bbox.y - PAD; - mRenderedSvg.setAttribute("viewBox", `${vbX} ${vbY} ${vbW} ${vbH}`); - mRenderedSvg.setAttribute("width", `${vbW}`); - mRenderedSvg.removeAttribute("height"); - mRenderedSvg.style.maxWidth = ""; - } + cropSvgToRenderedContent(mRenderedSvg, "mermaid"); } // Render excalidraw SVG @@ -142,9 +251,13 @@ async function renderTestCase(index: number): Promise { const { elements, files } = graphToExcalidraw(parsed, { fontSize: DEFAULT_FONT_SIZE, }); + await ensureExcalidrawFontsLoaded(); + clearExcalidrawTextMeasureCache(elements); + if (isStale()) { + return; + } const excalidrawElements = convertToExcalidrawElements(elements); - // Fix seeds to make rendering deterministic across runs for (const el of excalidrawElements) { (el as any).seed = 1; @@ -157,13 +270,17 @@ async function renderTestCase(index: number): Promise { viewBackgroundColor: "#ffffff", }, files: files ?? null, - exportPadding: 4, + exportPadding: EXCALIDRAW_SVG_EXPORT_PADDING, }); if (isStale()) { return; } excalidrawOutput.innerHTML = svgElement.outerHTML; + const eRenderedSvg = excalidrawOutput.querySelector("svg"); + if (eRenderedSvg) { + cropSvgToRenderedContent(eRenderedSvg, "excalidraw"); + } // Let SVGs size naturally — CSS flexbox handles panel alignment. @@ -275,32 +392,22 @@ async function renderAllTestCases(): Promise { document.body.appendChild(tempContainer); const { svg: mermaidSvg } = await runMermaidTaskSequentially(() => - mermaid.render(renderId, tc.definition, tempContainer), + mermaid.render(renderId, tc.definition, tempContainer) ); tempContainer.remove(); mOut.innerHTML = mermaidSvg; - // Crop mermaid SVG padding (same as single-case view) const mRenderedSvg = mOut.querySelector("svg"); if (mRenderedSvg) { - const PAD = 4; - const bbox = mRenderedSvg.getBBox(); - const vbW = bbox.width + PAD * 2; - const vbH = bbox.height + PAD * 2; - if (vbW / vbH < 10) { - const vbX = bbox.x - PAD; - const vbY = bbox.y - PAD; - mRenderedSvg.setAttribute("viewBox", `${vbX} ${vbY} ${vbW} ${vbH}`); - mRenderedSvg.setAttribute("width", `${vbW}`); - mRenderedSvg.removeAttribute("height"); - mRenderedSvg.style.maxWidth = ""; - } + cropSvgToRenderedContent(mRenderedSvg, "mermaid"); } const parsed = await parseMermaid(tc.definition); const { elements, files } = graphToExcalidraw(parsed, { fontSize: DEFAULT_FONT_SIZE, }); + await ensureExcalidrawFontsLoaded(); + clearExcalidrawTextMeasureCache(elements); const excalidrawElements = convertToExcalidrawElements(elements); for (const el of excalidrawElements) { (el as any).seed = 1; @@ -312,9 +419,13 @@ async function renderAllTestCases(): Promise { viewBackgroundColor: "#ffffff", }, files: files ?? null, - exportPadding: 4, + exportPadding: EXCALIDRAW_SVG_EXPORT_PADDING, }); eOut.innerHTML = svgElement.outerHTML; + const eRenderedSvg = eOut.querySelector("svg"); + if (eRenderedSvg) { + cropSvgToRenderedContent(eRenderedSvg, "excalidraw"); + } cleanupMermaidTempContainers(); } catch (err) { @@ -342,26 +453,10 @@ function hideAllView(): void { (window as any).TEST_CASE_COUNT = ALL_TESTCASES.length; (window as any).ALL_TESTCASES = ALL_TESTCASES; -// Preload Excalifont before any test case runs. -// convertToExcalidrawElements measures text synchronously via Canvas API, -// so Excalifont must be loaded BEFORE it runs, not after. -// -// Font files are copied from @excalidraw/excalidraw dist into public/fonts/ -// by visual-tests/copy-fonts.mjs (run automatically via the test:visual script). -// EXCALIDRAW_ASSET_PATH tells the Fonts loader to resolve relative font URIs -// (e.g. "./fonts/Excalifont/...") against our public dir instead of esm.sh CDN. +// Keep the visual harness on the same font-loading path as the playground +// preview so text measurement and wrapping stay consistent. (async () => { - (window as any).EXCALIDRAW_ASSET_PATH = "/"; - - await Fonts.loadElementsFonts([ - { - type: "text", - fontFamily: FONT_FAMILY.Excalifont, - text: "preload", - originalText: "preload", - } as any, - ]); - await document.fonts.ready; + await ensureExcalidrawFontsLoaded(); (window as any).__HARNESS_READY__ = true; })(); diff --git a/visual-tests/playground.spec.ts b/visual-tests/playground.spec.ts index 325c8295..dc5e9cba 100644 --- a/visual-tests/playground.spec.ts +++ b/visual-tests/playground.spec.ts @@ -3,14 +3,24 @@ import { FLOWCHART_DIAGRAM_TESTCASES } from "../playground/testcases/flowchart"; import { SEQUENCE_DIAGRAM_TESTCASES } from "../playground/testcases/sequence"; import { CLASS_DIAGRAM_TESTCASES } from "../playground/testcases/class"; import { ERD_DIAGRAM_TESTCASES } from "../playground/testcases/er"; +import { STATE_DIAGRAM_TESTCASES } from "../playground/testcases/state"; import { UNSUPPORTED_DIAGRAM_TESTCASES } from "../playground/testcases/unsupported"; +test.describe.configure({ mode: "serial" }); + const ALL_TESTCASES = [ - ...FLOWCHART_DIAGRAM_TESTCASES.map((tc) => ({ ...tc, chartType: "flowchart" })), + ...FLOWCHART_DIAGRAM_TESTCASES.map((tc) => ({ + ...tc, + chartType: "flowchart", + })), ...SEQUENCE_DIAGRAM_TESTCASES.map((tc) => ({ ...tc, chartType: "sequence" })), ...CLASS_DIAGRAM_TESTCASES.map((tc) => ({ ...tc, chartType: "class" })), ...ERD_DIAGRAM_TESTCASES.map((tc) => ({ ...tc, chartType: "erd" })), - ...UNSUPPORTED_DIAGRAM_TESTCASES.map((tc) => ({ ...tc, chartType: "unsupported" })), + ...STATE_DIAGRAM_TESTCASES.map((tc) => ({ ...tc, chartType: "state" })), + ...UNSUPPORTED_DIAGRAM_TESTCASES.map((tc) => ({ + ...tc, + chartType: "unsupported", + })), ]; function slugify(s: string): string { diff --git a/visual-tests/playground.spec.ts-snapshots/0-flowchart-direction-top-to-down-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/0-flowchart-direction-top-to-down-chromium-linux.png index 1279bb53..88166535 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/0-flowchart-direction-top-to-down-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/0-flowchart-direction-top-to-down-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/1-flowchart-direction-left-to-right-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/1-flowchart-direction-left-to-right-chromium-linux.png index d8e0acf3..89b391e7 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/1-flowchart-direction-left-to-right-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/1-flowchart-direction-left-to-right-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/10-flowchart-parallelogram-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/10-flowchart-parallelogram-chromium-linux.png index fd631238..410a175a 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/10-flowchart-parallelogram-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/10-flowchart-parallelogram-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/100-erd-identifying-and-non-identifying-relationships-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/100-erd-identifying-and-non-identifying-relationships-chromium-linux.png index c337e242..3eafee5b 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/100-erd-identifying-and-non-identifying-relationships-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/100-erd-identifying-and-non-identifying-relationships-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/101-erd-entity-aliases-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/101-erd-entity-aliases-chromium-linux.png index fd8952b6..9e8ce72d 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/101-erd-entity-aliases-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/101-erd-entity-aliases-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/102-erd-attributes-with-keys-and-comments-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/102-erd-attributes-with-keys-and-comments-chromium-linux.png index 2a6fcb77..9db83d04 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/102-erd-attributes-with-keys-and-comments-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/102-erd-attributes-with-keys-and-comments-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/103-erd-direction-left-to-right-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/103-erd-direction-left-to-right-chromium-linux.png index 0947fc5a..64809ffb 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/103-erd-direction-left-to-right-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/103-erd-direction-left-to-right-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/104-erd-class-styling-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/104-erd-class-styling-chromium-linux.png index 30ecb12d..61c1cd1d 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/104-erd-class-styling-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/104-erd-class-styling-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/105-erd-styled-erd-text-colors-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/105-erd-styled-erd-text-colors-chromium-linux.png index 8ea9c113..0fa4d8af 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/105-erd-styled-erd-text-colors-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/105-erd-styled-erd-text-colors-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/106-state-simple-transition-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/106-state-simple-transition-chromium-linux.png new file mode 100644 index 00000000..90282ff1 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/106-state-simple-transition-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/107-state-choice-and-notes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/107-state-choice-and-notes-chromium-linux.png new file mode 100644 index 00000000..c083e0cc Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/107-state-choice-and-notes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/108-state-composite-state-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/108-state-composite-state-chromium-linux.png new file mode 100644 index 00000000..9c98a5a1 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/108-state-composite-state-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/109-state-composite-state-transitions-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/109-state-composite-state-transitions-chromium-linux.png new file mode 100644 index 00000000..be2c2e43 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/109-state-composite-state-transitions-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/11-flowchart-parallelogram-with-alt-text-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/11-flowchart-parallelogram-with-alt-text-chromium-linux.png index 593e19ea..52c70085 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/11-flowchart-parallelogram-with-alt-text-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/11-flowchart-parallelogram-with-alt-text-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/110-state-nested-composite-states-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/110-state-nested-composite-states-chromium-linux.png new file mode 100644 index 00000000..b7f23637 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/110-state-nested-composite-states-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/111-state-concurrency-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/111-state-concurrency-chromium-linux.png new file mode 100644 index 00000000..1fb037a5 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/111-state-concurrency-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/112-state-fork-and-join-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/112-state-fork-and-join-chromium-linux.png new file mode 100644 index 00000000..be9709f5 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/112-state-fork-and-join-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/113-state-multiline-notes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/113-state-multiline-notes-chromium-linux.png new file mode 100644 index 00000000..80fb23f1 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/113-state-multiline-notes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/114-state-styling-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/114-state-styling-chromium-linux.png new file mode 100644 index 00000000..6d41db05 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/114-state-styling-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/115-state-direction-and-comments-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/115-state-direction-and-comments-chromium-linux.png new file mode 100644 index 00000000..2f117e4a Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/115-state-direction-and-comments-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/116-state-spaces-and-inline-styles-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/116-state-spaces-and-inline-styles-chromium-linux.png new file mode 100644 index 00000000..6ffd5854 Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/116-state-spaces-and-inline-styles-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/117-unsupported-gantt-diagram-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/117-unsupported-gantt-diagram-chromium-linux.png new file mode 100644 index 00000000..2b58584f Binary files /dev/null and b/visual-tests/playground.spec.ts-snapshots/117-unsupported-gantt-diagram-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/12-flowchart-trapezoid-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/12-flowchart-trapezoid-chromium-linux.png index a0911460..debef342 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/12-flowchart-trapezoid-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/12-flowchart-trapezoid-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/13-flowchart-trapezoid-alt-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/13-flowchart-trapezoid-alt-chromium-linux.png index 1c8f10b8..d82c1101 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/13-flowchart-trapezoid-alt-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/13-flowchart-trapezoid-alt-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/14-flowchart-double-circle-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/14-flowchart-double-circle-chromium-linux.png index cda74751..f26745db 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/14-flowchart-double-circle-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/14-flowchart-double-circle-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/15-flowchart-a-link-with-arrow-head-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/15-flowchart-a-link-with-arrow-head-chromium-linux.png index d7e3933d..3dc2f284 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/15-flowchart-a-link-with-arrow-head-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/15-flowchart-a-link-with-arrow-head-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/16-flowchart-a-link-with-arrow-head-and-text-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/16-flowchart-a-link-with-arrow-head-and-text-chromium-linux.png index 14d51e0f..25d76c78 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/16-flowchart-a-link-with-arrow-head-and-text-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/16-flowchart-a-link-with-arrow-head-and-text-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/17-flowchart-a-link-with-arrow-head-and-text-using-another-syntax-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/17-flowchart-a-link-with-arrow-head-and-text-using-another-syntax-chromium-linux.png index 9798aba4..a4e7c891 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/17-flowchart-a-link-with-arrow-head-and-text-using-another-syntax-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/17-flowchart-a-link-with-arrow-head-and-text-using-another-syntax-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/18-flowchart-dotted-link-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/18-flowchart-dotted-link-chromium-linux.png index 497f5a6f..411d004e 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/18-flowchart-dotted-link-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/18-flowchart-dotted-link-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/19-flowchart-dotted-link-with-text-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/19-flowchart-dotted-link-with-text-chromium-linux.png index cfed489b..4825f065 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/19-flowchart-dotted-link-with-text-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/19-flowchart-dotted-link-with-text-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/2-flowchart-a-node-with-round-edges-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/2-flowchart-a-node-with-round-edges-chromium-linux.png index 987fa268..6b2425f5 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/2-flowchart-a-node-with-round-edges-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/2-flowchart-a-node-with-round-edges-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/20-flowchart-an-open-link-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/20-flowchart-an-open-link-chromium-linux.png index de7e3366..fb4cbfab 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/20-flowchart-an-open-link-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/20-flowchart-an-open-link-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/21-flowchart-an-open-link-with-text-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/21-flowchart-an-open-link-with-text-chromium-linux.png index 36024d34..dd27ef64 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/21-flowchart-an-open-link-with-text-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/21-flowchart-an-open-link-with-text-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/22-flowchart-an-open-link-with-text-using-another-syntax-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/22-flowchart-an-open-link-with-text-using-another-syntax-chromium-linux.png index 27cdf87a..00190716 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/22-flowchart-an-open-link-with-text-using-another-syntax-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/22-flowchart-an-open-link-with-text-using-another-syntax-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/23-flowchart-thick-link-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/23-flowchart-thick-link-chromium-linux.png index 5950a4f4..2655e84e 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/23-flowchart-thick-link-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/23-flowchart-thick-link-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/24-flowchart-thick-link-with-text-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/24-flowchart-thick-link-with-text-chromium-linux.png index 96539627..4f338f82 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/24-flowchart-thick-link-with-text-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/24-flowchart-thick-link-with-text-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/25-flowchart-chaining-of-links-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/25-flowchart-chaining-of-links-chromium-linux.png index f9a7be54..7e4a097c 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/25-flowchart-chaining-of-links-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/25-flowchart-chaining-of-links-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/26-flowchart-multiple-nodes-links-in-the-same-line-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/26-flowchart-multiple-nodes-links-in-the-same-line-chromium-linux.png index a720d633..a9427a70 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/26-flowchart-multiple-nodes-links-in-the-same-line-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/26-flowchart-multiple-nodes-links-in-the-same-line-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/27-flowchart-multiple-nodes-links-to-describe-a-dependencies-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/27-flowchart-multiple-nodes-links-to-describe-a-dependencies-chromium-linux.png index 280fa91d..c1022b22 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/27-flowchart-multiple-nodes-links-to-describe-a-dependencies-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/27-flowchart-multiple-nodes-links-to-describe-a-dependencies-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/28-flowchart-multiple-nodes-linkes-to-describe-a-dependencies-using-another-syntax-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/28-flowchart-multiple-nodes-linkes-to-describe-a-dependencies-using-another-syntax-chromium-linux.png index 034b9aad..fc161962 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/28-flowchart-multiple-nodes-linkes-to-describe-a-dependencies-using-another-syntax-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/28-flowchart-multiple-nodes-linkes-to-describe-a-dependencies-using-another-syntax-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/29-flowchart-circle-arrow-and-cross-arrow-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/29-flowchart-circle-arrow-and-cross-arrow-chromium-linux.png index 5e307d34..cefb7840 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/29-flowchart-circle-arrow-and-cross-arrow-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/29-flowchart-circle-arrow-and-cross-arrow-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/3-flowchart-a-stadium-shaped-node-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/3-flowchart-a-stadium-shaped-node-chromium-linux.png index bda2ceee..c2ac74e6 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/3-flowchart-a-stadium-shaped-node-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/3-flowchart-a-stadium-shaped-node-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/30-flowchart-multi-directional-arrows-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/30-flowchart-multi-directional-arrows-chromium-linux.png index c19f1767..c64d7b3f 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/30-flowchart-multi-directional-arrows-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/30-flowchart-multi-directional-arrows-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/31-flowchart-special-characters-that-break-syntax-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/31-flowchart-special-characters-that-break-syntax-chromium-linux.png index 2c3d75c8..675520f4 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/31-flowchart-special-characters-that-break-syntax-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/31-flowchart-special-characters-that-break-syntax-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/32-flowchart-entity-codes-to-escape-characters-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/32-flowchart-entity-codes-to-escape-characters-chromium-linux.png index a35750d1..e1f157dc 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/32-flowchart-entity-codes-to-escape-characters-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/32-flowchart-entity-codes-to-escape-characters-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/33-flowchart-subgraphs-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/33-flowchart-subgraphs-chromium-linux.png index 764be036..5ce4a7aa 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/33-flowchart-subgraphs-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/33-flowchart-subgraphs-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/34-flowchart-subgraph-with-explicit-id-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/34-flowchart-subgraph-with-explicit-id-chromium-linux.png index e0c0276e..e5dd46c3 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/34-flowchart-subgraph-with-explicit-id-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/34-flowchart-subgraph-with-explicit-id-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/35-flowchart-links-between-subgraphs-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/35-flowchart-links-between-subgraphs-chromium-linux.png index c1590023..4fb1b97b 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/35-flowchart-links-between-subgraphs-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/35-flowchart-links-between-subgraphs-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/36-flowchart-direction-in-subgraphs-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/36-flowchart-direction-in-subgraphs-chromium-linux.png index c815b77d..b9d03e42 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/36-flowchart-direction-in-subgraphs-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/36-flowchart-direction-in-subgraphs-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/37-flowchart-markdown-strings-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/37-flowchart-markdown-strings-chromium-linux.png index a1aedb69..5e5e6801 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/37-flowchart-markdown-strings-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/37-flowchart-markdown-strings-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/38-flowchart-interaction-using-tooltip-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/38-flowchart-interaction-using-tooltip-chromium-linux.png index f152dced..932104eb 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/38-flowchart-interaction-using-tooltip-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/38-flowchart-interaction-using-tooltip-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/39-flowchart-interaction-using-link-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/39-flowchart-interaction-using-link-chromium-linux.png index 22b3b72d..b500cd1b 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/39-flowchart-interaction-using-link-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/39-flowchart-interaction-using-link-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/4-flowchart-a-node-in-a-subroutine-shape-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/4-flowchart-a-node-in-a-subroutine-shape-chromium-linux.png index f7ce0295..e1bd5fd7 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/4-flowchart-a-node-in-a-subroutine-shape-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/4-flowchart-a-node-in-a-subroutine-shape-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/40-flowchart-comments-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/40-flowchart-comments-chromium-linux.png index 754a846a..7235469d 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/40-flowchart-comments-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/40-flowchart-comments-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/41-flowchart-styling-a-node-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/41-flowchart-styling-a-node-chromium-linux.png index 4a2b83a0..1973fae0 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/41-flowchart-styling-a-node-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/41-flowchart-styling-a-node-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/42-flowchart-styling-a-node-using-class-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/42-flowchart-styling-a-node-using-class-chromium-linux.png index b5c8afd0..0385b248 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/42-flowchart-styling-a-node-using-class-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/42-flowchart-styling-a-node-using-class-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/43-flowchart-styling-subgraphs-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/43-flowchart-styling-subgraphs-chromium-linux.png index 052a51bf..4a1b8223 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/43-flowchart-styling-subgraphs-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/43-flowchart-styling-subgraphs-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/44-flowchart-classes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/44-flowchart-classes-chromium-linux.png index 63d2b4ff..9ce2fb62 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/44-flowchart-classes-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/44-flowchart-classes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/45-flowchart-basic-support-for-fontawesome-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/45-flowchart-basic-support-for-fontawesome-chromium-linux.png index cd757f2b..90391a6c 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/45-flowchart-basic-support-for-fontawesome-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/45-flowchart-basic-support-for-fontawesome-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/46-flowchart-graph-declarations-with-spaces-between-vertices-and-link-and-without-semicolon-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/46-flowchart-graph-declarations-with-spaces-between-vertices-and-link-and-without-semicolon-chromium-linux.png index 425f889b..40464f2a 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/46-flowchart-graph-declarations-with-spaces-between-vertices-and-link-and-without-semicolon-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/46-flowchart-graph-declarations-with-spaces-between-vertices-and-link-and-without-semicolon-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/47-flowchart-complex-case-1-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/47-flowchart-complex-case-1-chromium-linux.png index 460b2ce2..a1d080ee 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/47-flowchart-complex-case-1-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/47-flowchart-complex-case-1-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/48-flowchart-complex-case-2-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/48-flowchart-complex-case-2-chromium-linux.png index 093a50be..3e8f28f1 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/48-flowchart-complex-case-2-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/48-flowchart-complex-case-2-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/49-flowchart-multiple-edges-relations-to-a-single-entity-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/49-flowchart-multiple-edges-relations-to-a-single-entity-chromium-linux.png index 2723d90d..737a5692 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/49-flowchart-multiple-edges-relations-to-a-single-entity-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/49-flowchart-multiple-edges-relations-to-a-single-entity-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/5-flowchart-a-node-in-a-cylindrical-shape-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/5-flowchart-a-node-in-a-cylindrical-shape-chromium-linux.png index 82dade35..f6b359ed 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/5-flowchart-a-node-in-a-cylindrical-shape-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/5-flowchart-a-node-in-a-cylindrical-shape-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/50-flowchart-when-some-edges-aren-t-present-in-dom-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/50-flowchart-when-some-edges-aren-t-present-in-dom-chromium-linux.png index 59a0be36..842821e3 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/50-flowchart-when-some-edges-aren-t-present-in-dom-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/50-flowchart-when-some-edges-aren-t-present-in-dom-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/51-sequence-solid-and-dotted-line-without-arrow-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/51-sequence-solid-and-dotted-line-without-arrow-chromium-linux.png index eddc941b..8b76516c 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/51-sequence-solid-and-dotted-line-without-arrow-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/51-sequence-solid-and-dotted-line-without-arrow-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/52-sequence-solid-and-dotted-line-with-arrow-head-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/52-sequence-solid-and-dotted-line-with-arrow-head-chromium-linux.png index de5e182b..ba639d6a 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/52-sequence-solid-and-dotted-line-with-arrow-head-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/52-sequence-solid-and-dotted-line-with-arrow-head-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/53-sequence-solid-and-dotted-line-with-cross-at-end-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/53-sequence-solid-and-dotted-line-with-cross-at-end-chromium-linux.png index c76e8ad8..611813d0 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/53-sequence-solid-and-dotted-line-with-cross-at-end-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/53-sequence-solid-and-dotted-line-with-cross-at-end-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/54-sequence-solid-and-dotted-line-with-open-arrow-at-end-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/54-sequence-solid-and-dotted-line-with-open-arrow-at-end-chromium-linux.png index 8ab8567f..cde9ce1e 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/54-sequence-solid-and-dotted-line-with-open-arrow-at-end-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/54-sequence-solid-and-dotted-line-with-open-arrow-at-end-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/55-sequence-actor-symbols-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/55-sequence-actor-symbols-chromium-linux.png index f4f05ab5..8bfe2ecb 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/55-sequence-actor-symbols-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/55-sequence-actor-symbols-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/56-sequence-aliases-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/56-sequence-aliases-chromium-linux.png index 8191a183..c30b3e42 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/56-sequence-aliases-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/56-sequence-aliases-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/57-sequence-notes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/57-sequence-notes-chromium-linux.png index c7135f9a..c52d81b6 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/57-sequence-notes-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/57-sequence-notes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/58-sequence-grouping-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/58-sequence-grouping-chromium-linux.png index ced01a94..72463726 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/58-sequence-grouping-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/58-sequence-grouping-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/59-sequence-activations-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/59-sequence-activations-chromium-linux.png index 684a5e5c..a861bc02 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/59-sequence-activations-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/59-sequence-activations-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/6-flowchart-a-node-in-the-form-of-a-circle-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/6-flowchart-a-node-in-the-form-of-a-circle-chromium-linux.png index d70a680e..9b85b06b 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/6-flowchart-a-node-in-the-form-of-a-circle-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/6-flowchart-a-node-in-the-form-of-a-circle-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/60-sequence-loops-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/60-sequence-loops-chromium-linux.png index c0ba3da1..a7bd68d4 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/60-sequence-loops-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/60-sequence-loops-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/61-sequence-alternate-paths-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/61-sequence-alternate-paths-chromium-linux.png index f50a1ed0..f0b6b5b7 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/61-sequence-alternate-paths-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/61-sequence-alternate-paths-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/62-sequence-critical-regions-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/62-sequence-critical-regions-chromium-linux.png index d85c007a..2e09c719 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/62-sequence-critical-regions-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/62-sequence-critical-regions-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/63-sequence-parallel-actions-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/63-sequence-parallel-actions-chromium-linux.png index b926c9fc..a734c6fb 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/63-sequence-parallel-actions-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/63-sequence-parallel-actions-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/64-sequence-break-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/64-sequence-break-chromium-linux.png index d17b99b6..31263c8d 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/64-sequence-break-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/64-sequence-break-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/65-sequence-comments-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/65-sequence-comments-chromium-linux.png index 98f35a89..80ae5d2b 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/65-sequence-comments-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/65-sequence-comments-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/66-sequence-background-hightlights-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/66-sequence-background-hightlights-chromium-linux.png index 05b8986a..6bd6cd97 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/66-sequence-background-hightlights-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/66-sequence-background-hightlights-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/67-sequence-grouping-with-background-highlights-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/67-sequence-grouping-with-background-highlights-chromium-linux.png index b62de5c1..aad1c897 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/67-sequence-grouping-with-background-highlights-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/67-sequence-grouping-with-background-highlights-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/68-sequence-sequence-numbers-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/68-sequence-sequence-numbers-chromium-linux.png index b57a1ac7..4b762196 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/68-sequence-sequence-numbers-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/68-sequence-sequence-numbers-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/69-sequence-entity-codes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/69-sequence-entity-codes-chromium-linux.png index e323dfca..8f16b6f2 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/69-sequence-entity-codes-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/69-sequence-entity-codes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/7-flowchart-a-node-in-an-asymmetric-shape-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/7-flowchart-a-node-in-an-asymmetric-shape-chromium-linux.png index b42d7dc8..92dccfd2 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/7-flowchart-a-node-in-an-asymmetric-shape-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/7-flowchart-a-node-in-an-asymmetric-shape-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/70-sequence-actor-creation-and-destruction-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/70-sequence-actor-creation-and-destruction-chromium-linux.png index 5573435d..53320159 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/70-sequence-actor-creation-and-destruction-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/70-sequence-actor-creation-and-destruction-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/71-class-class-only-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/71-class-class-only-chromium-linux.png index 9d370678..348f4511 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/71-class-class-only-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/71-class-class-only-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/72-class-class-with-relations-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/72-class-class-with-relations-chromium-linux.png index 1c3ed229..2b2af247 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/72-class-class-with-relations-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/72-class-class-with-relations-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/73-class-class-with-labels-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/73-class-class-with-labels-chromium-linux.png index a798d124..6ff70bff 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/73-class-class-with-labels-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/73-class-class-with-labels-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/74-class-class-with-members-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/74-class-class-with-members-chromium-linux.png index f4447148..30e7fecd 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/74-class-class-with-members-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/74-class-class-with-members-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/75-class-class-with-members-using-curly-braces-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/75-class-class-with-members-using-curly-braces-chromium-linux.png index 9be2c73a..b86f08e7 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/75-class-class-with-members-using-curly-braces-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/75-class-class-with-members-using-curly-braces-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/76-class-class-with-members-and-return-type-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/76-class-class-with-members-and-return-type-chromium-linux.png index 50676407..492050fe 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/76-class-class-with-members-and-return-type-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/76-class-class-with-members-and-return-type-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/77-class-class-with-generic-types-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/77-class-class-with-generic-types-chromium-linux.png index 9c411ac1..ef601765 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/77-class-class-with-generic-types-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/77-class-class-with-generic-types-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/78-class-multiple-classes-with-members-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/78-class-multiple-classes-with-members-chromium-linux.png index 1d7c5df5..d75132c9 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/78-class-multiple-classes-with-members-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/78-class-multiple-classes-with-members-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/79-class-class-with-relations-1-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/79-class-class-with-relations-1-chromium-linux.png index 4528578d..3ab7727f 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/79-class-class-with-relations-1-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/79-class-class-with-relations-1-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/80-class-class-with-relations-2-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/80-class-class-with-relations-2-chromium-linux.png index 07c59ff4..ac9c5338 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/80-class-class-with-relations-2-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/80-class-class-with-relations-2-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/81-class-class-with-2-way-relations-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/81-class-class-with-2-way-relations-chromium-linux.png index fb6b3378..2b399655 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/81-class-class-with-2-way-relations-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/81-class-class-with-2-way-relations-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/82-class-class-with-2-way-relations-and-direction-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/82-class-class-with-2-way-relations-and-direction-chromium-linux.png index c8335a34..f1795974 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/82-class-class-with-2-way-relations-and-direction-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/82-class-class-with-2-way-relations-and-direction-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/83-class-class-with-namespace-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/83-class-class-with-namespace-chromium-linux.png index 8d7e0b20..1f58097a 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/83-class-class-with-namespace-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/83-class-class-with-namespace-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/84-class-class-with-cardinality-multiplicity-on-relations-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/84-class-class-with-cardinality-multiplicity-on-relations-chromium-linux.png index ac1a33ec..22fb913b 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/84-class-class-with-cardinality-multiplicity-on-relations-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/84-class-class-with-cardinality-multiplicity-on-relations-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/85-class-annotations-on-classes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/85-class-annotations-on-classes-chromium-linux.png index 4d15052f..e99a1697 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/85-class-annotations-on-classes-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/85-class-annotations-on-classes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/86-class-comments-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/86-class-comments-chromium-linux.png index 9a559a55..e7aeccc8 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/86-class-comments-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/86-class-comments-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/87-class-setting-the-direction-of-diagram-left-to-right-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/87-class-setting-the-direction-of-diagram-left-to-right-chromium-linux.png index aff07733..e30ce675 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/87-class-setting-the-direction-of-diagram-left-to-right-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/87-class-setting-the-direction-of-diagram-left-to-right-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/88-class-setting-the-direction-of-diagram-right-to-left-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/88-class-setting-the-direction-of-diagram-right-to-left-chromium-linux.png index 8a42561d..27a714b6 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/88-class-setting-the-direction-of-diagram-right-to-left-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/88-class-setting-the-direction-of-diagram-right-to-left-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/89-class-setting-the-direction-of-diagram-bottom-to-top-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/89-class-setting-the-direction-of-diagram-bottom-to-top-chromium-linux.png index be484825..13bda11d 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/89-class-setting-the-direction-of-diagram-bottom-to-top-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/89-class-setting-the-direction-of-diagram-bottom-to-top-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/9-flowchart-a-hexagon-node-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/9-flowchart-a-hexagon-node-chromium-linux.png index 7a976598..b347a0dd 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/9-flowchart-a-hexagon-node-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/9-flowchart-a-hexagon-node-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/90-class-setting-the-direction-of-diagram-top-to-bottom-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/90-class-setting-the-direction-of-diagram-top-to-bottom-chromium-linux.png index 125c416d..cbf87f31 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/90-class-setting-the-direction-of-diagram-top-to-bottom-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/90-class-setting-the-direction-of-diagram-top-to-bottom-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/91-class-class-with-notes-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/91-class-class-with-notes-chromium-linux.png index de65f065..abbbefa1 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/91-class-class-with-notes-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/91-class-class-with-notes-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/92-class-class-with-routed-notes-and-mixed-arrowheads-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/92-class-class-with-routed-notes-and-mixed-arrowheads-chromium-linux.png index cf8ed3b1..264593b2 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/92-class-class-with-routed-notes-and-mixed-arrowheads-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/92-class-class-with-routed-notes-and-mixed-arrowheads-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/93-class-classes-with-partial-match-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/93-class-classes-with-partial-match-chromium-linux.png index 08aca621..507e3311 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/93-class-classes-with-partial-match-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/93-class-classes-with-partial-match-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/94-class-class-with-custom-colors-using-style-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/94-class-class-with-custom-colors-using-style-chromium-linux.png index ac564a6a..7d842d93 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/94-class-class-with-custom-colors-using-style-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/94-class-class-with-custom-colors-using-style-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/95-class-styled-class-text-colors-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/95-class-styled-class-text-colors-chromium-linux.png index 797ca560..eb757cda 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/95-class-styled-class-text-colors-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/95-class-styled-class-text-colors-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/96-class-self-refrencing-class-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/96-class-self-refrencing-class-chromium-linux.png index 1bb423e4..da1871b5 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/96-class-self-refrencing-class-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/96-class-self-refrencing-class-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/97-erd-single-entity-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/97-erd-single-entity-chromium-linux.png index bf4696cc..00b58f82 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/97-erd-single-entity-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/97-erd-single-entity-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/98-erd-basic-cardinalities-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/98-erd-basic-cardinalities-chromium-linux.png index 8660b660..8c685f7c 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/98-erd-basic-cardinalities-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/98-erd-basic-cardinalities-chromium-linux.png differ diff --git a/visual-tests/playground.spec.ts-snapshots/99-erd-self-relationship-chromium-linux.png b/visual-tests/playground.spec.ts-snapshots/99-erd-self-relationship-chromium-linux.png index 8c653067..840927e0 100644 Binary files a/visual-tests/playground.spec.ts-snapshots/99-erd-self-relationship-chromium-linux.png and b/visual-tests/playground.spec.ts-snapshots/99-erd-self-relationship-chromium-linux.png differ diff --git a/yarn.lock b/yarn.lock index ddebd6e9..6e5d2fc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2513,6 +2513,13 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node@^20": + version "20.19.37" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.37.tgz#b4fb4033408dd97becce63ec932c9ec57a9e2919" + integrity sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw== + dependencies: + undici-types "~6.21.0" + "@types/prop-types@*": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -5833,6 +5840,11 @@ ufo@^1.5.4: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"