@tldraw/mermaid 4.6.0-internal.c7df3c92455a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist-cjs/blueprint.js +17 -0
  2. package/dist-cjs/blueprint.js.map +7 -0
  3. package/dist-cjs/colors.js +173 -0
  4. package/dist-cjs/colors.js.map +7 -0
  5. package/dist-cjs/createMermaidDiagram.js +144 -0
  6. package/dist-cjs/createMermaidDiagram.js.map +7 -0
  7. package/dist-cjs/flowchartDiagram.js +202 -0
  8. package/dist-cjs/flowchartDiagram.js.map +7 -0
  9. package/dist-cjs/index.d.ts +114 -0
  10. package/dist-cjs/index.js +34 -0
  11. package/dist-cjs/index.js.map +7 -0
  12. package/dist-cjs/renderBlueprint.js +314 -0
  13. package/dist-cjs/renderBlueprint.js.map +7 -0
  14. package/dist-cjs/sequenceDiagram.js +686 -0
  15. package/dist-cjs/sequenceDiagram.js.map +7 -0
  16. package/dist-cjs/stateDiagram.js +373 -0
  17. package/dist-cjs/stateDiagram.js.map +7 -0
  18. package/dist-cjs/svgParsing.js +187 -0
  19. package/dist-cjs/svgParsing.js.map +7 -0
  20. package/dist-cjs/utils.js +75 -0
  21. package/dist-cjs/utils.js.map +7 -0
  22. package/dist-esm/blueprint.mjs +1 -0
  23. package/dist-esm/blueprint.mjs.map +7 -0
  24. package/dist-esm/colors.mjs +153 -0
  25. package/dist-esm/colors.mjs.map +7 -0
  26. package/dist-esm/createMermaidDiagram.mjs +114 -0
  27. package/dist-esm/createMermaidDiagram.mjs.map +7 -0
  28. package/dist-esm/flowchartDiagram.mjs +188 -0
  29. package/dist-esm/flowchartDiagram.mjs.map +7 -0
  30. package/dist-esm/index.d.mts +114 -0
  31. package/dist-esm/index.mjs +14 -0
  32. package/dist-esm/index.mjs.map +7 -0
  33. package/dist-esm/renderBlueprint.mjs +298 -0
  34. package/dist-esm/renderBlueprint.mjs.map +7 -0
  35. package/dist-esm/sequenceDiagram.mjs +666 -0
  36. package/dist-esm/sequenceDiagram.mjs.map +7 -0
  37. package/dist-esm/stateDiagram.mjs +359 -0
  38. package/dist-esm/stateDiagram.mjs.map +7 -0
  39. package/dist-esm/svgParsing.mjs +167 -0
  40. package/dist-esm/svgParsing.mjs.map +7 -0
  41. package/dist-esm/utils.mjs +55 -0
  42. package/dist-esm/utils.mjs.map +7 -0
  43. package/package.json +62 -0
  44. package/src/blueprint.ts +75 -0
  45. package/src/colors.ts +215 -0
  46. package/src/createMermaidDiagram.test.ts +31 -0
  47. package/src/createMermaidDiagram.ts +155 -0
  48. package/src/flowchartDiagram.ts +232 -0
  49. package/src/index.ts +18 -0
  50. package/src/mermaidDiagrams.test.ts +880 -0
  51. package/src/renderBlueprint.ts +373 -0
  52. package/src/sequenceDiagram.ts +851 -0
  53. package/src/stateDiagram.ts +477 -0
  54. package/src/svgParsing.ts +240 -0
  55. package/src/utils.ts +73 -0
@@ -0,0 +1,167 @@
1
+ function parseTranslate(attr) {
2
+ if (!attr) return { x: 0, y: 0 };
3
+ const translateMatch = attr.match(/translate\(\s*([\d.e+-]+)[,\s]+([\d.e+-]+)\s*\)/);
4
+ if (!translateMatch) return { x: 0, y: 0 };
5
+ return { x: parseFloat(translateMatch[1]), y: parseFloat(translateMatch[2]) };
6
+ }
7
+ function getAccumulatedTranslate(el) {
8
+ let x = 0;
9
+ let y = 0;
10
+ let cur = el.parentElement;
11
+ while (cur) {
12
+ const parentTranslate = parseTranslate(cur.getAttribute("transform"));
13
+ x += parentTranslate.x;
14
+ y += parentTranslate.y;
15
+ cur = cur.parentElement;
16
+ }
17
+ return { x, y };
18
+ }
19
+ function getNodeDimensions(groupEl) {
20
+ const shapeEl = groupEl.querySelector(".label-container");
21
+ if (shapeEl) {
22
+ try {
23
+ const bbox = shapeEl.getBBox();
24
+ if (bbox.width > 0 && bbox.height > 0) return { w: bbox.width, h: bbox.height };
25
+ } catch {
26
+ }
27
+ }
28
+ try {
29
+ const bbox = groupEl.getBBox();
30
+ if (bbox.width > 0 && bbox.height > 0) return { w: bbox.width, h: bbox.height };
31
+ } catch {
32
+ }
33
+ const rect = groupEl.querySelector("rect");
34
+ if (rect) {
35
+ const w = parseFloat(rect.getAttribute("width") || "0");
36
+ const h = parseFloat(rect.getAttribute("height") || "0");
37
+ if (w > 0 && h > 0) return { w, h };
38
+ }
39
+ const poly = groupEl.querySelector("polygon");
40
+ if (poly) {
41
+ const pts = (poly.getAttribute("points") || "").trim().split(/\s+/).map((pointStr) => pointStr.split(",").map(Number));
42
+ let minX = Infinity;
43
+ let maxX = -Infinity;
44
+ let minY = Infinity;
45
+ let maxY = -Infinity;
46
+ for (const [px, py] of pts) {
47
+ minX = Math.min(minX, px);
48
+ maxX = Math.max(maxX, px);
49
+ minY = Math.min(minY, py);
50
+ maxY = Math.max(maxY, py);
51
+ }
52
+ if (maxX > minX && maxY > minY) return { w: maxX - minX, h: maxY - minY };
53
+ }
54
+ const circle = groupEl.querySelector("circle");
55
+ if (circle) {
56
+ const r = parseFloat(circle.getAttribute("r") || "0");
57
+ if (r > 0) return { w: r * 2, h: r * 2 };
58
+ }
59
+ const ellipse = groupEl.querySelector("ellipse");
60
+ if (ellipse) {
61
+ const w = parseFloat(ellipse.getAttribute("rx") || "0") * 2;
62
+ const h = parseFloat(ellipse.getAttribute("ry") || "0") * 2;
63
+ if (w > 0 && h > 0) return { w, h };
64
+ }
65
+ return { w: 0, h: 0 };
66
+ }
67
+ function parseNodesFromSvg(root, selector, idParser) {
68
+ const out = /* @__PURE__ */ new Map();
69
+ for (const groupEl of root.querySelectorAll(selector)) {
70
+ const rawId = groupEl.getAttribute("id") || "";
71
+ const id = idParser(rawId);
72
+ const self = parseTranslate(groupEl.getAttribute("transform"));
73
+ const ancestor = getAccumulatedTranslate(groupEl);
74
+ const { w, h } = getNodeDimensions(groupEl);
75
+ out.set(id, {
76
+ id,
77
+ center: { x: ancestor.x + self.x, y: ancestor.y + self.y },
78
+ width: w,
79
+ height: h
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+ function parseClustersFromSvg(root, selector) {
85
+ const out = /* @__PURE__ */ new Map();
86
+ for (const groupEl of root.querySelectorAll(selector)) {
87
+ const id = groupEl.getAttribute("id") || "";
88
+ const rect = groupEl.querySelector("rect");
89
+ if (!rect) continue;
90
+ const rx = parseFloat(rect.getAttribute("x") || "0");
91
+ const ry = parseFloat(rect.getAttribute("y") || "0");
92
+ const w = parseFloat(rect.getAttribute("width") || "0");
93
+ const h = parseFloat(rect.getAttribute("height") || "0");
94
+ const self = parseTranslate(groupEl.getAttribute("transform"));
95
+ const ancestor = getAccumulatedTranslate(groupEl);
96
+ out.set(id, {
97
+ id,
98
+ topLeft: { x: ancestor.x + self.x + rx, y: ancestor.y + self.y + ry },
99
+ width: w,
100
+ height: h
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+ function parseAllEdgePointsFromSvg(root, parser) {
106
+ const out = [];
107
+ for (const path of root.querySelectorAll("path[data-points]")) {
108
+ const dataId = path.getAttribute("data-id") || path.getAttribute("id") || "";
109
+ const dataPoints = path.getAttribute("data-points");
110
+ if (!dataPoints) continue;
111
+ const parsed = parser(dataId);
112
+ if (!parsed) continue;
113
+ try {
114
+ const points = JSON.parse(atob(dataPoints));
115
+ const ancestor = getAccumulatedTranslate(path);
116
+ for (const point of points) {
117
+ point.x += ancestor.x;
118
+ point.y += ancestor.y;
119
+ }
120
+ out.push({ start: parsed.start, end: parsed.end, points });
121
+ } catch {
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ function buildNodeCentersFromSvg(nodes, clusters) {
127
+ const out = /* @__PURE__ */ new Map();
128
+ for (const [id, node] of nodes) {
129
+ out.set(id, { x: node.center.x, y: node.center.y });
130
+ }
131
+ for (const [id, cluster] of clusters) {
132
+ out.set(id, {
133
+ x: cluster.topLeft.x + cluster.width / 2,
134
+ y: cluster.topLeft.y + cluster.height / 2
135
+ });
136
+ }
137
+ return out;
138
+ }
139
+ function scaleLayout(nodes, clusters, edges, scale) {
140
+ for (const [, node] of nodes) {
141
+ node.center.x *= scale;
142
+ node.center.y *= scale;
143
+ node.width *= scale;
144
+ node.height *= scale;
145
+ }
146
+ for (const [, cluster] of clusters) {
147
+ cluster.topLeft.x *= scale;
148
+ cluster.topLeft.y *= scale;
149
+ cluster.width *= scale;
150
+ cluster.height *= scale;
151
+ }
152
+ for (const edge of edges) {
153
+ for (const point of edge.points) {
154
+ point.x *= scale;
155
+ point.y *= scale;
156
+ }
157
+ }
158
+ }
159
+ export {
160
+ buildNodeCentersFromSvg,
161
+ getAccumulatedTranslate,
162
+ parseAllEdgePointsFromSvg,
163
+ parseClustersFromSvg,
164
+ parseNodesFromSvg,
165
+ scaleLayout
166
+ };
167
+ //# sourceMappingURL=svgParsing.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/svgParsing.ts"],
4
+ "sourcesContent": ["export interface Vec2 {\n\tx: number\n\ty: number\n}\n\nexport interface ParsedNode {\n\tid: string\n\tcenter: Vec2\n\twidth: number\n\theight: number\n}\n\nexport interface ParsedCluster {\n\tid: string\n\ttopLeft: Vec2\n\twidth: number\n\theight: number\n}\n\ntype NodeIdParser = (domId: string) => string\ntype EdgeIdParser = (dataId: string) => { start: string; end: string } | null\n\nfunction parseTranslate(attr: string | null): Vec2 {\n\tif (!attr) return { x: 0, y: 0 }\n\t// Matches SVG translate transforms, e.g. transform=\"translate(123.45, 67.8)\".\n\t// Handles scientific notation (1.2e+3). Group 1 = x offset, group 2 = y offset.\n\tconst translateMatch = attr.match(/translate\\(\\s*([\\d.e+-]+)[,\\s]+([\\d.e+-]+)\\s*\\)/)\n\tif (!translateMatch) return { x: 0, y: 0 }\n\treturn { x: parseFloat(translateMatch[1]), y: parseFloat(translateMatch[2]) }\n}\n\nexport function getAccumulatedTranslate(el: Element): Vec2 {\n\tlet x = 0\n\tlet y = 0\n\tlet cur: Element | null = el.parentElement\n\twhile (cur) {\n\t\tconst parentTranslate = parseTranslate(cur.getAttribute('transform'))\n\t\tx += parentTranslate.x\n\t\ty += parentTranslate.y\n\t\tcur = cur.parentElement\n\t}\n\treturn { x, y }\n}\n\n/**\n * Extract element dimensions from a live SVG element using getBBox(),\n * falling back to attribute parsing for non-browser environments (jsdom).\n */\nfunction getNodeDimensions(groupEl: Element): { w: number; h: number } {\n\tconst shapeEl = groupEl.querySelector('.label-container')\n\tif (shapeEl) {\n\t\ttry {\n\t\t\tconst bbox = (shapeEl as SVGGraphicsElement).getBBox()\n\t\t\tif (bbox.width > 0 && bbox.height > 0) return { w: bbox.width, h: bbox.height }\n\t\t} catch {\n\t\t\t/* fall through */\n\t\t}\n\t}\n\n\ttry {\n\t\tconst bbox = (groupEl as SVGGraphicsElement).getBBox()\n\t\tif (bbox.width > 0 && bbox.height > 0) return { w: bbox.width, h: bbox.height }\n\t} catch {\n\t\t/* fall through */\n\t}\n\n\tconst rect = groupEl.querySelector('rect')\n\tif (rect) {\n\t\tconst w = parseFloat(rect.getAttribute('width') || '0')\n\t\tconst h = parseFloat(rect.getAttribute('height') || '0')\n\t\tif (w > 0 && h > 0) return { w, h }\n\t}\n\tconst poly = groupEl.querySelector('polygon')\n\tif (poly) {\n\t\tconst pts = (poly.getAttribute('points') || '')\n\t\t\t.trim()\n\t\t\t.split(/\\s+/)\n\t\t\t.map((pointStr) => pointStr.split(',').map(Number))\n\t\tlet minX = Infinity\n\t\tlet maxX = -Infinity\n\t\tlet minY = Infinity\n\t\tlet maxY = -Infinity\n\t\tfor (const [px, py] of pts) {\n\t\t\tminX = Math.min(minX, px)\n\t\t\tmaxX = Math.max(maxX, px)\n\t\t\tminY = Math.min(minY, py)\n\t\t\tmaxY = Math.max(maxY, py)\n\t\t}\n\t\tif (maxX > minX && maxY > minY) return { w: maxX - minX, h: maxY - minY }\n\t}\n\tconst circle = groupEl.querySelector('circle')\n\tif (circle) {\n\t\tconst r = parseFloat(circle.getAttribute('r') || '0')\n\t\tif (r > 0) return { w: r * 2, h: r * 2 }\n\t}\n\tconst ellipse = groupEl.querySelector('ellipse')\n\tif (ellipse) {\n\t\tconst w = parseFloat(ellipse.getAttribute('rx') || '0') * 2\n\t\tconst h = parseFloat(ellipse.getAttribute('ry') || '0') * 2\n\t\tif (w > 0 && h > 0) return { w, h }\n\t}\n\treturn { w: 0, h: 0 }\n}\n\nexport function parseNodesFromSvg(\n\troot: Element,\n\tselector: string,\n\tidParser: NodeIdParser\n): Map<string, ParsedNode> {\n\tconst out = new Map<string, ParsedNode>()\n\tfor (const groupEl of root.querySelectorAll(selector)) {\n\t\tconst rawId = groupEl.getAttribute('id') || ''\n\t\tconst id = idParser(rawId)\n\t\tconst self = parseTranslate(groupEl.getAttribute('transform'))\n\t\tconst ancestor = getAccumulatedTranslate(groupEl)\n\t\tconst { w, h } = getNodeDimensions(groupEl)\n\t\tout.set(id, {\n\t\t\tid,\n\t\t\tcenter: { x: ancestor.x + self.x, y: ancestor.y + self.y },\n\t\t\twidth: w,\n\t\t\theight: h,\n\t\t})\n\t}\n\treturn out\n}\n\nexport function parseClustersFromSvg(root: Element, selector: string): Map<string, ParsedCluster> {\n\tconst out = new Map<string, ParsedCluster>()\n\tfor (const groupEl of root.querySelectorAll(selector)) {\n\t\tconst id = groupEl.getAttribute('id') || ''\n\t\tconst rect = groupEl.querySelector('rect')\n\t\tif (!rect) continue\n\t\tconst rx = parseFloat(rect.getAttribute('x') || '0')\n\t\tconst ry = parseFloat(rect.getAttribute('y') || '0')\n\t\tconst w = parseFloat(rect.getAttribute('width') || '0')\n\t\tconst h = parseFloat(rect.getAttribute('height') || '0')\n\t\tconst self = parseTranslate(groupEl.getAttribute('transform'))\n\t\tconst ancestor = getAccumulatedTranslate(groupEl)\n\t\tout.set(id, {\n\t\t\tid,\n\t\t\ttopLeft: { x: ancestor.x + self.x + rx, y: ancestor.y + self.y + ry },\n\t\t\twidth: w,\n\t\t\theight: h,\n\t\t})\n\t}\n\treturn out\n}\n\nexport interface ParsedEdge {\n\tstart: string\n\tend: string\n\tpoints: Vec2[]\n}\n\n/**\n * Pre-parsed SVG layout for flowchart and state diagram converters.\n * Contains already-scaled node, cluster, and edge data.\n */\nexport interface ParsedDiagramLayout {\n\tnodes: Map<string, ParsedNode>\n\tclusters: Map<string, ParsedCluster>\n\tedges: ParsedEdge[]\n}\n\n/**\n * Parse every SVG edge path in DOM order (matching mermaid's edge list order).\n * Unlike the old per-pair map, this preserves all parallel edges individually.\n */\nexport function parseAllEdgePointsFromSvg(root: Element, parser: EdgeIdParser): ParsedEdge[] {\n\tconst out: ParsedEdge[] = []\n\tfor (const path of root.querySelectorAll('path[data-points]')) {\n\t\tconst dataId = path.getAttribute('data-id') || path.getAttribute('id') || ''\n\t\tconst dataPoints = path.getAttribute('data-points')\n\t\tif (!dataPoints) continue\n\t\tconst parsed = parser(dataId)\n\t\tif (!parsed) continue\n\t\ttry {\n\t\t\tconst points = JSON.parse(atob(dataPoints))\n\t\t\tconst ancestor = getAccumulatedTranslate(path as Element)\n\t\t\tfor (const point of points) {\n\t\t\t\tpoint.x += ancestor.x\n\t\t\t\tpoint.y += ancestor.y\n\t\t\t}\n\t\t\tout.push({ start: parsed.start, end: parsed.end, points })\n\t\t} catch {\n\t\t\t/* ignore malformed data */\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * Build a map of node/cluster id \u2192 center (for flowchart and state diagram edge matching).\n */\nexport function buildNodeCentersFromSvg(\n\tnodes: Map<string, ParsedNode>,\n\tclusters: Map<string, ParsedCluster>\n): Map<string, Vec2> {\n\tconst out = new Map<string, Vec2>()\n\tfor (const [id, node] of nodes) {\n\t\tout.set(id, { x: node.center.x, y: node.center.y })\n\t}\n\tfor (const [id, cluster] of clusters) {\n\t\tout.set(id, {\n\t\t\tx: cluster.topLeft.x + cluster.width / 2,\n\t\t\ty: cluster.topLeft.y + cluster.height / 2,\n\t\t})\n\t}\n\treturn out\n}\n\n// ---------------------------------------------------------------------------\n// Layout scaling and bounds\n// ---------------------------------------------------------------------------\n\nexport function scaleLayout(\n\tnodes: Map<string, ParsedNode>,\n\tclusters: Map<string, ParsedCluster>,\n\tedges: ParsedEdge[],\n\tscale: number\n): void {\n\tfor (const [, node] of nodes) {\n\t\tnode.center.x *= scale\n\t\tnode.center.y *= scale\n\t\tnode.width *= scale\n\t\tnode.height *= scale\n\t}\n\tfor (const [, cluster] of clusters) {\n\t\tcluster.topLeft.x *= scale\n\t\tcluster.topLeft.y *= scale\n\t\tcluster.width *= scale\n\t\tcluster.height *= scale\n\t}\n\tfor (const edge of edges) {\n\t\tfor (const point of edge.points) {\n\t\t\tpoint.x *= scale\n\t\t\tpoint.y *= scale\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,55 @@
1
+ const BEND_SCALE = -1.8;
2
+ const MAX_ARROW_BEND = 200;
3
+ function getArrowBend(edgeData) {
4
+ const points = edgeData.points;
5
+ if (points.length < 2) {
6
+ return 0;
7
+ }
8
+ const start = points[0];
9
+ const end = points[points.length - 1];
10
+ const dx = end.x - start.x;
11
+ const dy = end.y - start.y;
12
+ const chordLength = Math.sqrt(dx * dx + dy * dy);
13
+ if (chordLength === 0) return 0;
14
+ let maxDistance = 0;
15
+ for (let i = 1; i < points.length - 1; i++) {
16
+ const distance = ((points[i].x - start.x) * dy - (points[i].y - start.y) * dx) / chordLength;
17
+ if (Math.abs(distance) > Math.abs(maxDistance)) {
18
+ maxDistance = distance;
19
+ }
20
+ }
21
+ const bend = maxDistance * BEND_SCALE;
22
+ return Math.max(-MAX_ARROW_BEND, Math.min(MAX_ARROW_BEND, bend));
23
+ }
24
+ function sanitizeDiagramText(text) {
25
+ if (typeof text !== "string") return "";
26
+ const doc = new DOMParser().parseFromString(text.replace(/<br\s*\/?>/gi, "\n"), "text/html");
27
+ return (doc.body.textContent ?? "").trim();
28
+ }
29
+ const LAYOUT_SCALE = 1.25;
30
+ function orderTopDown(items, getId, getParentId) {
31
+ const byId = new Map(items.map((item) => [getId(item), item]));
32
+ const visited = /* @__PURE__ */ new Set();
33
+ const result = [];
34
+ function visit(id) {
35
+ if (visited.has(id)) return;
36
+ visited.add(id);
37
+ const item = byId.get(id);
38
+ if (item) result.push(item);
39
+ for (const child of items) {
40
+ if (getParentId(child) === id) visit(getId(child));
41
+ }
42
+ }
43
+ for (const item of items) {
44
+ const parentId = getParentId(item);
45
+ if (!parentId || !byId.has(parentId)) visit(getId(item));
46
+ }
47
+ return result;
48
+ }
49
+ export {
50
+ LAYOUT_SCALE,
51
+ getArrowBend,
52
+ orderTopDown,
53
+ sanitizeDiagramText
54
+ };
55
+ //# sourceMappingURL=utils.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/utils.ts"],
4
+ "sourcesContent": ["const BEND_SCALE = -1.8\nconst MAX_ARROW_BEND = 200\n\n/**\n * Extrapolate a bend value for tldraw arrows from Mermaid edge path waypoints.\n * Uses perpendicular distance from chord to mid-points, scaled and clamped.\n */\nexport function getArrowBend(edgeData: { points: { x: number; y: number }[] }) {\n\tconst points = edgeData.points\n\n\tif (points.length < 2) {\n\t\treturn 0\n\t}\n\n\tconst start = points[0]\n\tconst end = points[points.length - 1]\n\tconst dx = end.x - start.x\n\tconst dy = end.y - start.y\n\tconst chordLength = Math.sqrt(dx * dx + dy * dy)\n\n\tif (chordLength === 0) return 0\n\n\tlet maxDistance = 0\n\tfor (let i = 1; i < points.length - 1; i++) {\n\t\tconst distance = ((points[i].x - start.x) * dy - (points[i].y - start.y) * dx) / chordLength\n\t\tif (Math.abs(distance) > Math.abs(maxDistance)) {\n\t\t\tmaxDistance = distance\n\t\t}\n\t}\n\n\tconst bend = maxDistance * BEND_SCALE\n\treturn Math.max(-MAX_ARROW_BEND, Math.min(MAX_ARROW_BEND, bend))\n}\n\n/** Normalize HTML line breaks to newlines, decode HTML entities, and trim. */\nexport function sanitizeDiagramText(text: string): string {\n\tif (typeof text !== 'string') return ''\n\tconst doc = new DOMParser().parseFromString(text.replace(/<br\\s*\\/?>/gi, '\\n'), 'text/html')\n\treturn (doc.body.textContent ?? '').trim()\n}\n\n/** Scale factor applied to parsed SVG layout (nodes, clusters, edges). */\nexport const LAYOUT_SCALE = 1.25\n\n/**\n * Order items top-down by parent relationship so parents are visited before children.\n * Works for subgraphs (FlowSubGraph[]) and compound state IDs (string[]).\n */\nexport function orderTopDown<T>(\n\titems: T[],\n\tgetId: (item: T) => string,\n\tgetParentId: (item: T) => string | undefined\n): T[] {\n\tconst byId = new Map(items.map((item) => [getId(item), item]))\n\tconst visited = new Set<string>()\n\tconst result: T[] = []\n\n\tfunction visit(id: string) {\n\t\tif (visited.has(id)) return\n\t\tvisited.add(id)\n\t\tconst item = byId.get(id)\n\t\tif (item) result.push(item)\n\t\tfor (const child of items) {\n\t\t\tif (getParentId(child) === id) visit(getId(child))\n\t\t}\n\t}\n\n\tfor (const item of items) {\n\t\tconst parentId = getParentId(item)\n\t\tif (!parentId || !byId.has(parentId)) visit(getId(item))\n\t}\n\treturn result\n}\n"],
5
+ "mappings": "AAAA,MAAM,aAAa;AACnB,MAAM,iBAAiB;AAMhB,SAAS,aAAa,UAAkD;AAC9E,QAAM,SAAS,SAAS;AAExB,MAAI,OAAO,SAAS,GAAG;AACtB,WAAO;AAAA,EACR;AAEA,QAAM,QAAQ,OAAO,CAAC;AACtB,QAAM,MAAM,OAAO,OAAO,SAAS,CAAC;AACpC,QAAM,KAAK,IAAI,IAAI,MAAM;AACzB,QAAM,KAAK,IAAI,IAAI,MAAM;AACzB,QAAM,cAAc,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;AAE/C,MAAI,gBAAgB,EAAG,QAAO;AAE9B,MAAI,cAAc;AAClB,WAAS,IAAI,GAAG,IAAI,OAAO,SAAS,GAAG,KAAK;AAC3C,UAAM,aAAa,OAAO,CAAC,EAAE,IAAI,MAAM,KAAK,MAAM,OAAO,CAAC,EAAE,IAAI,MAAM,KAAK,MAAM;AACjF,QAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,WAAW,GAAG;AAC/C,oBAAc;AAAA,IACf;AAAA,EACD;AAEA,QAAM,OAAO,cAAc;AAC3B,SAAO,KAAK,IAAI,CAAC,gBAAgB,KAAK,IAAI,gBAAgB,IAAI,CAAC;AAChE;AAGO,SAAS,oBAAoB,MAAsB;AACzD,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAM,MAAM,IAAI,UAAU,EAAE,gBAAgB,KAAK,QAAQ,gBAAgB,IAAI,GAAG,WAAW;AAC3F,UAAQ,IAAI,KAAK,eAAe,IAAI,KAAK;AAC1C;AAGO,MAAM,eAAe;AAMrB,SAAS,aACf,OACA,OACA,aACM;AACN,QAAM,OAAO,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC;AAC7D,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,SAAc,CAAC;AAErB,WAAS,MAAM,IAAY;AAC1B,QAAI,QAAQ,IAAI,EAAE,EAAG;AACrB,YAAQ,IAAI,EAAE;AACd,UAAM,OAAO,KAAK,IAAI,EAAE;AACxB,QAAI,KAAM,QAAO,KAAK,IAAI;AAC1B,eAAW,SAAS,OAAO;AAC1B,UAAI,YAAY,KAAK,MAAM,GAAI,OAAM,MAAM,KAAK,CAAC;AAAA,IAClD;AAAA,EACD;AAEA,aAAW,QAAQ,OAAO;AACzB,UAAM,WAAW,YAAY,IAAI;AACjC,QAAI,CAAC,YAAY,CAAC,KAAK,IAAI,QAAQ,EAAG,OAAM,MAAM,IAAI,CAAC;AAAA,EACxD;AACA,SAAO;AACR;",
6
+ "names": []
7
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@tldraw/mermaid",
3
+ "description": "Mermaid diagram to tldraw shape conversion.",
4
+ "version": "4.6.0-internal.c7df3c92455a",
5
+ "author": {
6
+ "name": "tldraw Inc.",
7
+ "email": "hello@tldraw.com"
8
+ },
9
+ "homepage": "https://tldraw.dev",
10
+ "license": "SEE LICENSE IN LICENSE.md",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/tldraw/tldraw"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/tldraw/tldraw/issues"
17
+ },
18
+ "keywords": [
19
+ "tldraw",
20
+ "mermaid",
21
+ "diagram",
22
+ "canvas",
23
+ "infinite"
24
+ ],
25
+ "main": "dist-cjs/index.js",
26
+ "files": [
27
+ "dist-esm",
28
+ "dist-cjs",
29
+ "src"
30
+ ],
31
+ "scripts": {
32
+ "test-ci": "yarn run -T vitest run --passWithNoTests",
33
+ "test": "yarn run -T vitest --passWithNoTests",
34
+ "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
35
+ "build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
36
+ "build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
37
+ "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
38
+ "postpack": "../../internal/scripts/postpack.sh",
39
+ "pack-tarball": "yarn pack",
40
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
41
+ },
42
+ "dependencies": {
43
+ "@tldraw/tlschema": "4.6.0-internal.c7df3c92455a",
44
+ "@tldraw/utils": "4.6.0-internal.c7df3c92455a",
45
+ "mermaid": "11.12.2"
46
+ },
47
+ "peerDependencies": {
48
+ "tldraw": "4.6.0-internal.c7df3c92455a"
49
+ },
50
+ "devDependencies": {
51
+ "lazyrepo": "0.0.0-alpha.27",
52
+ "vitest": "^3.2.4"
53
+ },
54
+ "module": "dist-esm/index.mjs",
55
+ "source": "src/index.ts",
56
+ "exports": {
57
+ ".": {
58
+ "import": "./dist-esm/index.mjs",
59
+ "require": "./dist-cjs/index.js"
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,75 @@
1
+ import type {
2
+ TLArrowShapeArrowheadStyle,
3
+ TLDefaultColorStyle,
4
+ TLDefaultDashStyle,
5
+ TLDefaultFillStyle,
6
+ TLDefaultHorizontalAlignStyle,
7
+ TLDefaultSizeStyle,
8
+ TLDefaultVerticalAlignStyle,
9
+ TLGeoShapeGeoStyle,
10
+ } from 'tldraw'
11
+
12
+ /**
13
+ * An intermediate representation of a parsed mermaid diagram as abstract nodes,
14
+ * edges, and lines with layout positions and tldraw style props. Produced by
15
+ * the diagram-specific converters and consumed by `renderBlueprint` to create
16
+ * actual tldraw shapes on the canvas.
17
+ *
18
+ * @public
19
+ */
20
+ export interface DiagramMermaidBlueprint {
21
+ nodes: MermaidBlueprintGeoNode[]
22
+ edges: MermaidBlueprintEdge[]
23
+ lines?: MermaidBlueprintLineNode[]
24
+ groups?: string[][]
25
+ }
26
+
27
+ /** @public */
28
+ export interface MermaidBlueprintGeoNode {
29
+ id: string
30
+ x: number
31
+ y: number
32
+ w: number
33
+ h: number
34
+ geo: TLGeoShapeGeoStyle
35
+ parentId?: string
36
+ label?: string
37
+ fill?: TLDefaultFillStyle
38
+ color?: TLDefaultColorStyle
39
+ dash?: TLDefaultDashStyle
40
+ size?: TLDefaultSizeStyle
41
+ align?: TLDefaultHorizontalAlignStyle
42
+ verticalAlign?: TLDefaultVerticalAlignStyle
43
+ }
44
+
45
+ /** @public */
46
+ export interface MermaidBlueprintEdge {
47
+ startNodeId: string
48
+ endNodeId: string
49
+ label?: string
50
+ bend: number
51
+ arrowheadEnd?: TLArrowShapeArrowheadStyle
52
+ arrowheadStart?: TLArrowShapeArrowheadStyle
53
+ dash?: TLDefaultDashStyle
54
+ size?: TLDefaultSizeStyle
55
+ color?: TLDefaultColorStyle
56
+ anchorStartY?: number
57
+ anchorEndY?: number
58
+ isExact?: boolean
59
+ isPrecise?: boolean
60
+ isExactEnd?: boolean
61
+ isPreciseEnd?: boolean
62
+ decoration?: { type: 'autonumber'; value: string }
63
+ }
64
+
65
+ /** @public */
66
+ export interface MermaidBlueprintLineNode {
67
+ id: string
68
+ x: number
69
+ y: number
70
+ endX?: number
71
+ endY: number
72
+ dash?: TLDefaultDashStyle
73
+ size?: TLDefaultSizeStyle
74
+ color?: TLDefaultColorStyle
75
+ }
package/src/colors.ts ADDED
@@ -0,0 +1,215 @@
1
+ import { defaultColorNames, DefaultColorThemePalette } from '@tldraw/tlschema'
2
+ import { TLDefaultColorStyle, TLDefaultDashStyle, TLDefaultSizeStyle } from 'tldraw'
3
+
4
+ type Color = [number, number, number, number]
5
+
6
+ export interface ParsedNodeColors {
7
+ fillColor?: TLDefaultColorStyle
8
+ strokeColor?: TLDefaultColorStyle
9
+ }
10
+
11
+ /**
12
+ * Build a map of node id → parsed fill/stroke colors from Mermaid's classDef definitions.
13
+ *
14
+ * Uses the structured data from `db.getClasses()` and each node's `classes`
15
+ * array. For each node, looks up its applied classDef styles and maps fill and
16
+ * stroke independently to the nearest tldraw palette color.
17
+ */
18
+ export function buildClassDefColorMap(
19
+ classDefs: Map<string, { styles: string[] }>,
20
+ items: Iterable<[string, { classes?: string[] }]>
21
+ ): Map<string, ParsedNodeColors> {
22
+ const result = new Map<string, ParsedNodeColors>()
23
+ if (classDefs.size === 0) return result
24
+
25
+ for (const [nodeId, item] of items) {
26
+ if (!item.classes || item.classes.length === 0) continue
27
+
28
+ for (const className of item.classes) {
29
+ const classDef = classDefs.get(className)
30
+ if (!classDef || classDef.styles.length === 0) continue
31
+
32
+ const props = parseCssProps(classDef.styles)
33
+ const fill = toColor(props.get('fill'))
34
+ const stroke = toColor(props.get('stroke'))
35
+
36
+ if (!fill && !stroke) continue
37
+
38
+ const colors: ParsedNodeColors = {}
39
+ if (fill) colors.fillColor = nearestTldrawColor(fill)
40
+ if (stroke) colors.strokeColor = nearestTldrawColor(stroke)
41
+ result.set(nodeId, colors)
42
+ break
43
+ }
44
+ }
45
+
46
+ return result
47
+ }
48
+
49
+ export function parseRgbToTldrawColor(
50
+ text: string
51
+ ): { color: TLDefaultColorStyle; hasAlpha: boolean } | null {
52
+ const color = toColor(text)
53
+ if (!color) return null
54
+ return { color: nearestTldrawColor(color), hasAlpha: color[3] < 255 }
55
+ }
56
+
57
+ interface ParsedCssOverrides {
58
+ color?: TLDefaultColorStyle
59
+ dashOverride?: TLDefaultDashStyle
60
+ sizeOverride?: TLDefaultSizeStyle
61
+ }
62
+
63
+ function parseCssProps(styles: string[]): Map<string, string> {
64
+ const props = new Map<string, string>()
65
+ for (const entry of styles) {
66
+ for (const part of entry.split(';')) {
67
+ const colon = part.indexOf(':')
68
+ if (colon < 0) continue
69
+ const key = part.slice(0, colon).trim().toLowerCase()
70
+ const value = part.slice(colon + 1).trim()
71
+ if (key && value) props.set(key, value)
72
+ }
73
+ }
74
+ return props
75
+ }
76
+
77
+ /**
78
+ * Parse a Mermaid CSS style array from an edge (FlowEdge.style / linkStyle)
79
+ * and return tldraw-compatible overrides.
80
+ */
81
+ export function parseCssStyles(styles: string[] | undefined): ParsedCssOverrides {
82
+ if (!styles || styles.length === 0) return {}
83
+
84
+ const props = parseCssProps(styles)
85
+ const result: ParsedCssOverrides = {}
86
+
87
+ const stroke = toColor(props.get('stroke'))
88
+ if (stroke) {
89
+ result.color = nearestTldrawColor(stroke)
90
+ }
91
+
92
+ if (props.has('stroke-dasharray')) {
93
+ result.dashOverride = 'dashed'
94
+ }
95
+
96
+ const strokeWidth = props.get('stroke-width')
97
+ if (strokeWidth) {
98
+ const pixels = parseFloat(strokeWidth)
99
+ if (Number.isFinite(pixels)) {
100
+ if (pixels <= 1) result.sizeOverride = 's'
101
+ else if (pixels <= 2) result.sizeOverride = 'm'
102
+ else result.sizeOverride = 'l'
103
+ }
104
+ }
105
+
106
+ return result
107
+ }
108
+
109
+ /**
110
+ * Parse inline `style nodeId fill:…,stroke:…` directives from a FlowVertex.styles
111
+ * array and return fill and stroke as independent tldraw colors.
112
+ */
113
+ export function parseNodeInlineColor(styles: string[] | undefined): ParsedNodeColors | undefined {
114
+ if (!styles || styles.length === 0) return undefined
115
+
116
+ const props = parseCssProps(styles)
117
+ const fill = toColor(props.get('fill'))
118
+ const stroke = toColor(props.get('stroke'))
119
+
120
+ if (!fill && !stroke) return undefined
121
+
122
+ const colors: ParsedNodeColors = {}
123
+ if (fill) colors.fillColor = nearestTldrawColor(fill)
124
+ if (stroke) colors.strokeColor = nearestTldrawColor(stroke)
125
+ return colors
126
+ }
127
+
128
+ function parseHexToRgb(hex: string): [number, number, number] | null {
129
+ const stripped = hex.replace(/^#/, '')
130
+ if (stripped.length === 3 || stripped.length === 4) {
131
+ return [
132
+ parseInt(stripped[0] + stripped[0], 16),
133
+ parseInt(stripped[1] + stripped[1], 16),
134
+ parseInt(stripped[2] + stripped[2], 16),
135
+ ]
136
+ }
137
+ if (stripped.length === 6 || stripped.length === 8) {
138
+ return [
139
+ parseInt(stripped.slice(0, 2), 16),
140
+ parseInt(stripped.slice(2, 4), 16),
141
+ parseInt(stripped.slice(4, 6), 16),
142
+ ]
143
+ }
144
+ return null
145
+ }
146
+
147
+ const TLDRAW_PALETTE: [TLDefaultColorStyle, number, number, number][] = defaultColorNames.map(
148
+ (name) => {
149
+ const { solid } = DefaultColorThemePalette.lightMode[name]
150
+ const rgb = parseHexToRgb(solid)!
151
+ return [name, rgb[0], rgb[1], rgb[2]]
152
+ }
153
+ )
154
+
155
+ /** Map an arbitrary Color tuple to the nearest tldraw named color (best-effort). */
156
+ function nearestTldrawColor(rgb: Color): TLDefaultColorStyle {
157
+ let [r, g, b] = rgb
158
+
159
+ const max = Math.max(r, g, b)
160
+ const min = Math.min(r, g, b)
161
+ const lightness = (max + min) / 2 / 255
162
+ const chroma = max - min
163
+
164
+ // For very light pastels, strip the white base and amplify the
165
+ // chromatic signal so the distance metric can see the hue.
166
+ if (lightness > 0.75 && chroma > 5) {
167
+ const target = 200
168
+ r = Math.round(((r - min) / chroma) * target)
169
+ g = Math.round(((g - min) / chroma) * target)
170
+ b = Math.round(((b - min) / chroma) * target)
171
+ }
172
+
173
+ let best: TLDefaultColorStyle = 'black'
174
+ let bestDistance = Infinity
175
+ for (const [name, red, green, blue] of TLDRAW_PALETTE) {
176
+ // "Redmean" weighted Euclidean distance (Compuphase approximation).
177
+ // Weights RGB channels by the average red value of the two colors to
178
+ // approximate human perception, which is more sensitive to green and
179
+ // varies in red/blue sensitivity depending on the color's warmth.
180
+ const rMean = (r + red) / 2
181
+ const dR = r - red
182
+ const dG = g - green
183
+ const dB = b - blue
184
+ const distance = (2 + rMean / 256) * dR * dR + 4 * dG * dG + (2 + (255 - rMean) / 256) * dB * dB
185
+ if (distance < bestDistance) {
186
+ bestDistance = distance
187
+ best = name
188
+ }
189
+ }
190
+ return best
191
+ }
192
+
193
+ function toColor(value: string | undefined): Color | undefined {
194
+ if (!value) return undefined
195
+
196
+ const trimmed = value.trim()
197
+ if (!trimmed || trimmed === 'none' || trimmed === 'transparent') return undefined
198
+
199
+ if (trimmed.startsWith('rgb')) {
200
+ const match = trimmed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/)
201
+ if (!match) return undefined
202
+ return [
203
+ parseInt(match[1], 10),
204
+ parseInt(match[2], 10),
205
+ parseInt(match[3], 10),
206
+ match[4] !== undefined ? Math.round(parseFloat(match[4]) * 255) : 255,
207
+ ]
208
+ }
209
+ if (trimmed.startsWith('#')) {
210
+ const rgb = parseHexToRgb(trimmed)
211
+ if (!rgb) return undefined
212
+ return [rgb[0], rgb[1], rgb[2], 255]
213
+ }
214
+ return undefined
215
+ }
@@ -0,0 +1,31 @@
1
+ vi.mock('tldraw', () => ({
2
+ createShapeId: () => `shape:mock-0` as any,
3
+ toRichText: (text: string) => ({
4
+ type: 'doc',
5
+ content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
6
+ }),
7
+ Vec: {
8
+ Min: (a: { x: number; y: number }, b: { x: number; y: number }) => ({
9
+ x: Math.min(a.x, b.x),
10
+ y: Math.min(a.y, b.y),
11
+ }),
12
+ },
13
+ }))
14
+
15
+ import { createMermaidDiagram, MermaidDiagramError } from './createMermaidDiagram'
16
+
17
+ describe('createMermaidDiagram', () => {
18
+ it('throws MermaidDiagramError for invalid input', async () => {
19
+ const editor = {} as any
20
+
21
+ await expect(
22
+ createMermaidDiagram(editor, 'not a diagram at all', {
23
+ blueprintRender: { position: { x: 0, y: 0 }, centerOnPosition: false },
24
+ })
25
+ ).rejects.toThrow(MermaidDiagramError)
26
+
27
+ await expect(createMermaidDiagram(editor, 'not a diagram at all')).rejects.toMatchObject({
28
+ type: 'parse',
29
+ })
30
+ })
31
+ })