@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.
- package/dist-cjs/blueprint.js +17 -0
- package/dist-cjs/blueprint.js.map +7 -0
- package/dist-cjs/colors.js +173 -0
- package/dist-cjs/colors.js.map +7 -0
- package/dist-cjs/createMermaidDiagram.js +144 -0
- package/dist-cjs/createMermaidDiagram.js.map +7 -0
- package/dist-cjs/flowchartDiagram.js +202 -0
- package/dist-cjs/flowchartDiagram.js.map +7 -0
- package/dist-cjs/index.d.ts +114 -0
- package/dist-cjs/index.js +34 -0
- package/dist-cjs/index.js.map +7 -0
- package/dist-cjs/renderBlueprint.js +314 -0
- package/dist-cjs/renderBlueprint.js.map +7 -0
- package/dist-cjs/sequenceDiagram.js +686 -0
- package/dist-cjs/sequenceDiagram.js.map +7 -0
- package/dist-cjs/stateDiagram.js +373 -0
- package/dist-cjs/stateDiagram.js.map +7 -0
- package/dist-cjs/svgParsing.js +187 -0
- package/dist-cjs/svgParsing.js.map +7 -0
- package/dist-cjs/utils.js +75 -0
- package/dist-cjs/utils.js.map +7 -0
- package/dist-esm/blueprint.mjs +1 -0
- package/dist-esm/blueprint.mjs.map +7 -0
- package/dist-esm/colors.mjs +153 -0
- package/dist-esm/colors.mjs.map +7 -0
- package/dist-esm/createMermaidDiagram.mjs +114 -0
- package/dist-esm/createMermaidDiagram.mjs.map +7 -0
- package/dist-esm/flowchartDiagram.mjs +188 -0
- package/dist-esm/flowchartDiagram.mjs.map +7 -0
- package/dist-esm/index.d.mts +114 -0
- package/dist-esm/index.mjs +14 -0
- package/dist-esm/index.mjs.map +7 -0
- package/dist-esm/renderBlueprint.mjs +298 -0
- package/dist-esm/renderBlueprint.mjs.map +7 -0
- package/dist-esm/sequenceDiagram.mjs +666 -0
- package/dist-esm/sequenceDiagram.mjs.map +7 -0
- package/dist-esm/stateDiagram.mjs +359 -0
- package/dist-esm/stateDiagram.mjs.map +7 -0
- package/dist-esm/svgParsing.mjs +167 -0
- package/dist-esm/svgParsing.mjs.map +7 -0
- package/dist-esm/utils.mjs +55 -0
- package/dist-esm/utils.mjs.map +7 -0
- package/package.json +62 -0
- package/src/blueprint.ts +75 -0
- package/src/colors.ts +215 -0
- package/src/createMermaidDiagram.test.ts +31 -0
- package/src/createMermaidDiagram.ts +155 -0
- package/src/flowchartDiagram.ts +232 -0
- package/src/index.ts +18 -0
- package/src/mermaidDiagrams.test.ts +880 -0
- package/src/renderBlueprint.ts +373 -0
- package/src/sequenceDiagram.ts +851 -0
- package/src/stateDiagram.ts +477 -0
- package/src/svgParsing.ts +240 -0
- 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": "AAsBA,SAAS,eAAe,MAA2B;AAClD,MAAI,CAAC,KAAM,QAAO,EAAE,GAAG,GAAG,GAAG,EAAE;AAG/B,QAAM,iBAAiB,KAAK,MAAM,iDAAiD;AACnF,MAAI,CAAC,eAAgB,QAAO,EAAE,GAAG,GAAG,GAAG,EAAE;AACzC,SAAO,EAAE,GAAG,WAAW,eAAe,CAAC,CAAC,GAAG,GAAG,WAAW,eAAe,CAAC,CAAC,EAAE;AAC7E;AAEO,SAAS,wBAAwB,IAAmB;AAC1D,MAAI,IAAI;AACR,MAAI,IAAI;AACR,MAAI,MAAsB,GAAG;AAC7B,SAAO,KAAK;AACX,UAAM,kBAAkB,eAAe,IAAI,aAAa,WAAW,CAAC;AACpE,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AACrB,UAAM,IAAI;AAAA,EACX;AACA,SAAO,EAAE,GAAG,EAAE;AACf;AAMA,SAAS,kBAAkB,SAA4C;AACtE,QAAM,UAAU,QAAQ,cAAc,kBAAkB;AACxD,MAAI,SAAS;AACZ,QAAI;AACH,YAAM,OAAQ,QAA+B,QAAQ;AACrD,UAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,EAAG,QAAO,EAAE,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO;AAAA,IAC/E,QAAQ;AAAA,IAER;AAAA,EACD;AAEA,MAAI;AACH,UAAM,OAAQ,QAA+B,QAAQ;AACrD,QAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,EAAG,QAAO,EAAE,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO;AAAA,EAC/E,QAAQ;AAAA,EAER;AAEA,QAAM,OAAO,QAAQ,cAAc,MAAM;AACzC,MAAI,MAAM;AACT,UAAM,IAAI,WAAW,KAAK,aAAa,OAAO,KAAK,GAAG;AACtD,UAAM,IAAI,WAAW,KAAK,aAAa,QAAQ,KAAK,GAAG;AACvD,QAAI,IAAI,KAAK,IAAI,EAAG,QAAO,EAAE,GAAG,EAAE;AAAA,EACnC;AACA,QAAM,OAAO,QAAQ,cAAc,SAAS;AAC5C,MAAI,MAAM;AACT,UAAM,OAAO,KAAK,aAAa,QAAQ,KAAK,IAC1C,KAAK,EACL,MAAM,KAAK,EACX,IAAI,CAAC,aAAa,SAAS,MAAM,GAAG,EAAE,IAAI,MAAM,CAAC;AACnD,QAAI,OAAO;AACX,QAAI,OAAO;AACX,QAAI,OAAO;AACX,QAAI,OAAO;AACX,eAAW,CAAC,IAAI,EAAE,KAAK,KAAK;AAC3B,aAAO,KAAK,IAAI,MAAM,EAAE;AACxB,aAAO,KAAK,IAAI,MAAM,EAAE;AACxB,aAAO,KAAK,IAAI,MAAM,EAAE;AACxB,aAAO,KAAK,IAAI,MAAM,EAAE;AAAA,IACzB;AACA,QAAI,OAAO,QAAQ,OAAO,KAAM,QAAO,EAAE,GAAG,OAAO,MAAM,GAAG,OAAO,KAAK;AAAA,EACzE;AACA,QAAM,SAAS,QAAQ,cAAc,QAAQ;AAC7C,MAAI,QAAQ;AACX,UAAM,IAAI,WAAW,OAAO,aAAa,GAAG,KAAK,GAAG;AACpD,QAAI,IAAI,EAAG,QAAO,EAAE,GAAG,IAAI,GAAG,GAAG,IAAI,EAAE;AAAA,EACxC;AACA,QAAM,UAAU,QAAQ,cAAc,SAAS;AAC/C,MAAI,SAAS;AACZ,UAAM,IAAI,WAAW,QAAQ,aAAa,IAAI,KAAK,GAAG,IAAI;AAC1D,UAAM,IAAI,WAAW,QAAQ,aAAa,IAAI,KAAK,GAAG,IAAI;AAC1D,QAAI,IAAI,KAAK,IAAI,EAAG,QAAO,EAAE,GAAG,EAAE;AAAA,EACnC;AACA,SAAO,EAAE,GAAG,GAAG,GAAG,EAAE;AACrB;AAEO,SAAS,kBACf,MACA,UACA,UAC0B;AAC1B,QAAM,MAAM,oBAAI,IAAwB;AACxC,aAAW,WAAW,KAAK,iBAAiB,QAAQ,GAAG;AACtD,UAAM,QAAQ,QAAQ,aAAa,IAAI,KAAK;AAC5C,UAAM,KAAK,SAAS,KAAK;AACzB,UAAM,OAAO,eAAe,QAAQ,aAAa,WAAW,CAAC;AAC7D,UAAM,WAAW,wBAAwB,OAAO;AAChD,UAAM,EAAE,GAAG,EAAE,IAAI,kBAAkB,OAAO;AAC1C,QAAI,IAAI,IAAI;AAAA,MACX;AAAA,MACA,QAAQ,EAAE,GAAG,SAAS,IAAI,KAAK,GAAG,GAAG,SAAS,IAAI,KAAK,EAAE;AAAA,MACzD,OAAO;AAAA,MACP,QAAQ;AAAA,IACT,CAAC;AAAA,EACF;AACA,SAAO;AACR;AAEO,SAAS,qBAAqB,MAAe,UAA8C;AACjG,QAAM,MAAM,oBAAI,IAA2B;AAC3C,aAAW,WAAW,KAAK,iBAAiB,QAAQ,GAAG;AACtD,UAAM,KAAK,QAAQ,aAAa,IAAI,KAAK;AACzC,UAAM,OAAO,QAAQ,cAAc,MAAM;AACzC,QAAI,CAAC,KAAM;AACX,UAAM,KAAK,WAAW,KAAK,aAAa,GAAG,KAAK,GAAG;AACnD,UAAM,KAAK,WAAW,KAAK,aAAa,GAAG,KAAK,GAAG;AACnD,UAAM,IAAI,WAAW,KAAK,aAAa,OAAO,KAAK,GAAG;AACtD,UAAM,IAAI,WAAW,KAAK,aAAa,QAAQ,KAAK,GAAG;AACvD,UAAM,OAAO,eAAe,QAAQ,aAAa,WAAW,CAAC;AAC7D,UAAM,WAAW,wBAAwB,OAAO;AAChD,QAAI,IAAI,IAAI;AAAA,MACX;AAAA,MACA,SAAS,EAAE,GAAG,SAAS,IAAI,KAAK,IAAI,IAAI,GAAG,SAAS,IAAI,KAAK,IAAI,GAAG;AAAA,MACpE,OAAO;AAAA,MACP,QAAQ;AAAA,IACT,CAAC;AAAA,EACF;AACA,SAAO;AACR;AAsBO,SAAS,0BAA0B,MAAe,QAAoC;AAC5F,QAAM,MAAoB,CAAC;AAC3B,aAAW,QAAQ,KAAK,iBAAiB,mBAAmB,GAAG;AAC9D,UAAM,SAAS,KAAK,aAAa,SAAS,KAAK,KAAK,aAAa,IAAI,KAAK;AAC1E,UAAM,aAAa,KAAK,aAAa,aAAa;AAClD,QAAI,CAAC,WAAY;AACjB,UAAM,SAAS,OAAO,MAAM;AAC5B,QAAI,CAAC,OAAQ;AACb,QAAI;AACH,YAAM,SAAS,KAAK,MAAM,KAAK,UAAU,CAAC;AAC1C,YAAM,WAAW,wBAAwB,IAAe;AACxD,iBAAW,SAAS,QAAQ;AAC3B,cAAM,KAAK,SAAS;AACpB,cAAM,KAAK,SAAS;AAAA,MACrB;AACA,UAAI,KAAK,EAAE,OAAO,OAAO,OAAO,KAAK,OAAO,KAAK,OAAO,CAAC;AAAA,IAC1D,QAAQ;AAAA,IAER;AAAA,EACD;AACA,SAAO;AACR;AAKO,SAAS,wBACf,OACA,UACoB;AACpB,QAAM,MAAM,oBAAI,IAAkB;AAClC,aAAW,CAAC,IAAI,IAAI,KAAK,OAAO;AAC/B,QAAI,IAAI,IAAI,EAAE,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,OAAO,EAAE,CAAC;AAAA,EACnD;AACA,aAAW,CAAC,IAAI,OAAO,KAAK,UAAU;AACrC,QAAI,IAAI,IAAI;AAAA,MACX,GAAG,QAAQ,QAAQ,IAAI,QAAQ,QAAQ;AAAA,MACvC,GAAG,QAAQ,QAAQ,IAAI,QAAQ,SAAS;AAAA,IACzC,CAAC;AAAA,EACF;AACA,SAAO;AACR;AAMO,SAAS,YACf,OACA,UACA,OACA,OACO;AACP,aAAW,CAAC,EAAE,IAAI,KAAK,OAAO;AAC7B,SAAK,OAAO,KAAK;AACjB,SAAK,OAAO,KAAK;AACjB,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EAChB;AACA,aAAW,CAAC,EAAE,OAAO,KAAK,UAAU;AACnC,YAAQ,QAAQ,KAAK;AACrB,YAAQ,QAAQ,KAAK;AACrB,YAAQ,SAAS;AACjB,YAAQ,UAAU;AAAA,EACnB;AACA,aAAW,QAAQ,OAAO;AACzB,eAAW,SAAS,KAAK,QAAQ;AAChC,YAAM,KAAK;AACX,YAAM,KAAK;AAAA,IACZ;AAAA,EACD;AACD;",
|
|
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
|
+
}
|
package/src/blueprint.ts
ADDED
|
@@ -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
|
+
})
|