@markdstage/markdstage 3.1.0 → 3.3.0

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.
@@ -0,0 +1,1248 @@
1
+ import {
2
+ MAX_CONNECTOR_POINTS,
3
+ MAX_GROUP_DEPTH,
4
+ MAX_SCENE_NODES,
5
+ MAX_TEXT_PARAGRAPHS,
6
+ MAX_TEXT_RUNS,
7
+ createScene,
8
+ normalizeScene,
9
+ validateScene,
10
+ } from "./scene-graph.mjs";
11
+
12
+ export const DEFAULT_MERMAID_SCENE_OPTIONS = Object.freeze({
13
+ path: "mermaid.svg",
14
+ simplifyTolerance: 2,
15
+ sampleStep: 4,
16
+ });
17
+
18
+ const SVG_NS = "http://www.w3.org/2000/svg";
19
+ const SHAPE_TAGS = new Set(["rect", "circle", "ellipse", "polygon"]);
20
+ const IGNORED_TAGS = new Set([
21
+ "defs",
22
+ "desc",
23
+ "feDropShadow",
24
+ "filter",
25
+ "linearGradient",
26
+ "marker",
27
+ "metadata",
28
+ "script",
29
+ "stop",
30
+ "style",
31
+ "title",
32
+ ]);
33
+ const VISUAL_TAGS = new Set([
34
+ "circle",
35
+ "ellipse",
36
+ "foreignObject",
37
+ "image",
38
+ "line",
39
+ "path",
40
+ "polygon",
41
+ "polyline",
42
+ "rect",
43
+ "text",
44
+ "use",
45
+ ]);
46
+
47
+ function finiteNumberOr(value, fallback) {
48
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
49
+ }
50
+
51
+ function nonEmptyStringOr(value, fallback) {
52
+ return typeof value === "string" && value ? value : fallback;
53
+ }
54
+
55
+ function roundedMetric(value) {
56
+ return Math.round((Number(value) || 0) * 10) / 10;
57
+ }
58
+
59
+ function definedEntries(object) {
60
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
61
+ }
62
+
63
+ function tagName(element) {
64
+ return element?.tagName || "";
65
+ }
66
+
67
+ function localName(element) {
68
+ return element?.localName || tagName(element);
69
+ }
70
+
71
+ function hasClass(element, name) {
72
+ return Boolean(element?.classList?.contains(name));
73
+ }
74
+
75
+ function firstElementChild(element) {
76
+ return [...(element?.children || [])].find((child) => child.nodeType === 1) || null;
77
+ }
78
+
79
+ function directChildren(element, selector = null) {
80
+ const children = [...(element?.children || [])].filter((child) => child.nodeType === 1);
81
+ return selector ? children.filter((child) => child.matches(selector)) : children;
82
+ }
83
+
84
+ function pointKey(point) {
85
+ return `${roundedMetric(point.x)},${roundedMetric(point.y)}`;
86
+ }
87
+
88
+ function parsePoints(points) {
89
+ if (Array.isArray(points)) {
90
+ return points
91
+ .map((point) => ({ x: Number(point.x), y: Number(point.y) }))
92
+ .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
93
+ }
94
+ return String(points || "")
95
+ .trim()
96
+ .split(/\s+/)
97
+ .map((pair) => {
98
+ const [x, y] = pair.split(",").map(Number);
99
+ return { x, y };
100
+ })
101
+ .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
102
+ }
103
+
104
+ export function polygonPointsSignature(points, precision = 2) {
105
+ const factor = 10 ** precision;
106
+ return parsePoints(points)
107
+ .map((point) => `${Math.round(point.x * factor) / factor},${Math.round(point.y * factor) / factor}`)
108
+ .join(" ");
109
+ }
110
+
111
+ function normalizePolygon(points) {
112
+ const parsed = parsePoints(points);
113
+ if (!parsed.length) return [];
114
+ const minX = Math.min(...parsed.map((point) => point.x));
115
+ const minY = Math.min(...parsed.map((point) => point.y));
116
+ const width = Math.max(...parsed.map((point) => point.x)) - minX || 1;
117
+ const height = Math.max(...parsed.map((point) => point.y)) - minY || 1;
118
+ return parsed.map((point) => ({
119
+ x: Math.round(((point.x - minX) / width) * 1000) / 1000,
120
+ y: Math.round(((point.y - minY) / height) * 1000) / 1000,
121
+ }));
122
+ }
123
+
124
+ function closeToPoint(point, x, y, tolerance) {
125
+ return Math.abs(point.x - x) <= tolerance && Math.abs(point.y - y) <= tolerance;
126
+ }
127
+
128
+ export function classifyPolygonPreset(points, options = {}) {
129
+ const fallbackPreset = options.fallbackPreset === null ? null : nonEmptyStringOr(options.fallbackPreset, "rect");
130
+ const tolerance = finiteNumberOr(options.tolerance, 0.04);
131
+ const normalized = normalizePolygon(points);
132
+ if (normalized.length === 4) {
133
+ const [a, b, c, d] = normalized;
134
+ if (
135
+ closeToPoint(a, 0.5, 1, tolerance) &&
136
+ closeToPoint(b, 1, 0.5, tolerance) &&
137
+ closeToPoint(c, 0.5, 0, tolerance) &&
138
+ closeToPoint(d, 0, 0.5, tolerance)
139
+ ) {
140
+ return "diamond";
141
+ }
142
+ if (
143
+ closeToPoint(a, 0, 1, tolerance) &&
144
+ closeToPoint(b, 0.8, 1, tolerance) &&
145
+ closeToPoint(c, 1, 0, tolerance) &&
146
+ closeToPoint(d, 0.2, 0, tolerance)
147
+ ) {
148
+ return "parallelogram";
149
+ }
150
+ }
151
+ if (normalized.length === 6) {
152
+ const [a, b, c, d, e, f] = normalized;
153
+ if (a.x > 0 && a.x < 0.5 && closeToPoint(a, a.x, 1, tolerance) &&
154
+ closeToPoint(b, 1 - a.x, 1, tolerance) && closeToPoint(c, 1, 0.5, tolerance) &&
155
+ closeToPoint(d, b.x, 0, tolerance) && closeToPoint(e, a.x, 0, tolerance) &&
156
+ closeToPoint(f, 0, 0.5, tolerance)) return "hexagon";
157
+ }
158
+ if (normalized.length === 3 &&
159
+ normalized.some((point) => closeToPoint(point, 0.5, 0, tolerance)) &&
160
+ normalized.some((point) => closeToPoint(point, 0, 1, tolerance)) &&
161
+ normalized.some((point) => closeToPoint(point, 1, 1, tolerance))) return "triangle";
162
+ return fallbackPreset;
163
+ }
164
+
165
+ export function markerIdToArrow(value) {
166
+ const text = String(value || "").trim();
167
+ const marker = /^url\(["']?[^"')]*#([^"')]+)["']?\)$/i.exec(text)?.[1] || text.replace(/^#/, "");
168
+ if (!marker) return "none";
169
+ if (/point(?:Start|End)(?:-margin)?$/i.test(marker)) return "triangle";
170
+ if (/circle(?:Start|End)(?:-margin)?$/i.test(marker)) return "oval";
171
+ if (/diamond(?:Start|End)(?:-margin)?$/i.test(marker)) return "diamond";
172
+ if (/arrow(?:Start|End)(?:-margin)?$/i.test(marker)) return "arrow";
173
+ if (/-arrowhead$/.test(marker)) return "triangle";
174
+ if (/-openarrowhead$/.test(marker)) return "arrow";
175
+ return "none";
176
+ }
177
+
178
+ function distanceToSegment(point, start, end) {
179
+ const dx = end.x - start.x;
180
+ const dy = end.y - start.y;
181
+ if (dx === 0 && dy === 0) return Math.hypot(point.x - start.x, point.y - start.y);
182
+ const t = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)));
183
+ return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy));
184
+ }
185
+
186
+ export function simplifyPolyline(points, tolerance = DEFAULT_MERMAID_SCENE_OPTIONS.simplifyTolerance) {
187
+ const parsed = parsePoints(points);
188
+ let simplified = parsed.filter((point, index) => index === 0 || pointKey(point) !== pointKey(parsed[index - 1]));
189
+ const threshold = Math.max(0, finiteNumberOr(tolerance, DEFAULT_MERMAID_SCENE_OPTIONS.simplifyTolerance));
190
+ let changed = true;
191
+ while (changed && simplified.length > 2) {
192
+ changed = false;
193
+ const next = [simplified[0]];
194
+ for (let index = 1; index < simplified.length - 1; index += 1) {
195
+ const previous = next[next.length - 1];
196
+ const current = simplified[index];
197
+ const following = simplified[index + 1];
198
+ if (distanceToSegment(current, previous, following) <= threshold) {
199
+ changed = true;
200
+ } else {
201
+ next.push(current);
202
+ }
203
+ }
204
+ next.push(simplified[simplified.length - 1]);
205
+ simplified = next;
206
+ }
207
+ return simplified;
208
+ }
209
+
210
+ function normalizeColor(value, resolveColor = (entry) => entry) {
211
+ const resolved = resolveColor(value);
212
+ const text = String(resolved || "").trim();
213
+ if (!text || text === "none" || text === "transparent" || text === "rgba(0, 0, 0, 0)") return null;
214
+ return text;
215
+ }
216
+
217
+ function parseMetric(value) {
218
+ const number = Number.parseFloat(String(value ?? ""));
219
+ return Number.isFinite(number) ? number : undefined;
220
+ }
221
+
222
+ function dashToSceneDash(value) {
223
+ const text = String(value || "").trim();
224
+ if (!text || text === "none" || text === "0" || text === "0px") return "solid";
225
+ const values = text
226
+ .split(/[,\s]+/)
227
+ .map((part) => Number.parseFloat(part))
228
+ .filter(Number.isFinite);
229
+ if (values.length && values.every((value) => value === 0)) return "solid";
230
+ if (values.length >= 2 && values[0] <= 2 && values[1] >= values[0] * 2) return "dot";
231
+ if (values.length >= 2) return "dash";
232
+ if (values.length === 1 && values[0] <= 2) return "dot";
233
+ return "dash";
234
+ }
235
+
236
+ function normalizeAlignment(value) {
237
+ const text = String(value || "").toLowerCase();
238
+ if (text === "left" || text === "center" || text === "right" || text === "justify") return text;
239
+ if (text === "end") return "right";
240
+ return "center";
241
+ }
242
+
243
+ export function cssStyleToSceneStyle(style = {}, options = {}) {
244
+ const resolveColor = typeof options.resolveColor === "function" ? options.resolveColor : (value) => value;
245
+ const strokeWidth = parseMetric(style.strokeWidth);
246
+ const opacity = Number.parseFloat(style.opacity);
247
+ const cornerRadius = parseMetric(style.cornerRadius ?? style.rx);
248
+ return definedEntries({
249
+ fill: style.fill !== undefined ? normalizeColor(style.fill, resolveColor) : undefined,
250
+ stroke: style.stroke !== undefined ? normalizeColor(style.stroke, resolveColor) : undefined,
251
+ strokeWidth,
252
+ dash: style.dash !== undefined || style.strokeDasharray !== undefined
253
+ ? dashToSceneDash(style.dash ?? style.strokeDasharray)
254
+ : undefined,
255
+ opacity: Number.isFinite(opacity) ? Math.max(0, Math.min(1, opacity)) : undefined,
256
+ cornerRadius,
257
+ });
258
+ }
259
+
260
+ export function primaryFontFamily(value) {
261
+ // getComputedStyle returns the whole CSS stack, but PowerPoint needs one literal typeface.
262
+ const first = String(value ?? "").split(",")[0].trim().replace(/^["']|["']$/g, "");
263
+ return first || undefined;
264
+ }
265
+
266
+ export function textToSceneText(text, style = {}, options = {}) {
267
+ const resolveColor = typeof options.resolveColor === "function" ? options.resolveColor : (value) => value;
268
+ const lines = String(text ?? "")
269
+ .replace(/\r\n?/g, "\n")
270
+ .split("\n")
271
+ .map((line) => line.trim())
272
+ .filter(Boolean);
273
+ const paragraphs = (lines.length ? lines : [""]).map((line) => {
274
+ const fontWeight = parseMetric(style.fontWeight);
275
+ return {
276
+ alignment: normalizeAlignment(style.textAlign || style.alignment),
277
+ runs: [
278
+ definedEntries({
279
+ text: line,
280
+ fontSize: parseMetric(style.fontSize),
281
+ fontFace: primaryFontFamily(style.fontFamily),
282
+ fontWeight,
283
+ bold: Number.isFinite(fontWeight) ? fontWeight >= 600 : undefined,
284
+ italic: String(style.fontStyle || "").toLowerCase() === "italic" || undefined,
285
+ color: style.color !== undefined ? normalizeColor(style.color, resolveColor) : undefined,
286
+ opacity: Number.isFinite(Number(style.opacity)) ? Math.max(0, Math.min(1, Number(style.opacity))) : undefined,
287
+ }),
288
+ ],
289
+ };
290
+ });
291
+ return { paragraphs };
292
+ }
293
+
294
+ function countSceneNodes(nodes, depth = 1) {
295
+ let count = 0;
296
+ let maxDepth = depth;
297
+ let connectorPoints = 0;
298
+ let paragraphs = 0;
299
+ let runs = 0;
300
+ const visitText = (text) => {
301
+ if (!text?.paragraphs) return;
302
+ paragraphs += text.paragraphs.length;
303
+ runs += text.paragraphs.reduce((total, paragraph) => total + (paragraph.runs?.length || 0), 0);
304
+ };
305
+ for (const node of nodes || []) {
306
+ count += 1;
307
+ maxDepth = Math.max(maxDepth, depth);
308
+ visitText(node.text);
309
+ visitText(node.label?.text);
310
+ if (node.kind === "connector") connectorPoints = Math.max(connectorPoints, node.points?.length || 0);
311
+ if (node.kind === "group") {
312
+ const nested = countSceneNodes(node.children, depth + 1);
313
+ count += nested.count;
314
+ maxDepth = Math.max(maxDepth, nested.maxDepth);
315
+ connectorPoints = Math.max(connectorPoints, nested.connectorPoints);
316
+ paragraphs += nested.paragraphs;
317
+ runs += nested.runs;
318
+ }
319
+ }
320
+ return { count, maxDepth, connectorPoints, paragraphs, runs };
321
+ }
322
+
323
+ export function fallbackSceneForReason(scene, reason, bounds = {}) {
324
+ const width = finiteNumberOr(scene?.width, finiteNumberOr(bounds.width, 0));
325
+ const height = finiteNumberOr(scene?.height, finiteNumberOr(bounds.height, 0));
326
+ return createScene({
327
+ width,
328
+ height,
329
+ source: { kind: "mermaid", path: nonEmptyStringOr(scene?.source?.path, DEFAULT_MERMAID_SCENE_OPTIONS.path) },
330
+ nodes: [
331
+ {
332
+ kind: "fallback",
333
+ sourcePath: "svg",
334
+ z: 0,
335
+ bounds: {
336
+ x: finiteNumberOr(bounds.x, 0),
337
+ y: finiteNumberOr(bounds.y, 0),
338
+ width,
339
+ height,
340
+ },
341
+ reason,
342
+ },
343
+ ],
344
+ });
345
+ }
346
+
347
+ export function enforceSceneLimits(scene, options = {}) {
348
+ const stats = countSceneNodes(scene?.nodes || []);
349
+ const reasons = [];
350
+ if (stats.count > MAX_SCENE_NODES) reasons.push(`scene node count exceeds ${MAX_SCENE_NODES}`);
351
+ if (stats.maxDepth > MAX_GROUP_DEPTH) reasons.push(`group depth exceeds ${MAX_GROUP_DEPTH}`);
352
+ if (stats.connectorPoints > MAX_CONNECTOR_POINTS) reasons.push(`connector points exceed ${MAX_CONNECTOR_POINTS}`);
353
+ if (stats.paragraphs > MAX_TEXT_PARAGRAPHS) reasons.push(`text paragraphs exceed ${MAX_TEXT_PARAGRAPHS}`);
354
+ if (stats.runs > MAX_TEXT_RUNS) reasons.push(`text runs exceed ${MAX_TEXT_RUNS}`);
355
+ if (!reasons.length) return { scene, diagnostics: [] };
356
+ const reason = nonEmptyStringOr(options.reason, `mermaid-scene-limit-exceeded: ${reasons.join(", ")}`);
357
+ return {
358
+ scene: fallbackSceneForReason(scene, reason, options.bounds),
359
+ diagnostics: [{ path: "scene", kind: "fallback", reason }],
360
+ };
361
+ }
362
+
363
+ function normalizeOptions(options = {}) {
364
+ return {
365
+ path: nonEmptyStringOr(options.path, DEFAULT_MERMAID_SCENE_OPTIONS.path),
366
+ deck: options.deck || null,
367
+ resolveColor: typeof options.resolveColor === "function" ? options.resolveColor : (value) => value,
368
+ simplifyTolerance: finiteNumberOr(options.simplifyTolerance, DEFAULT_MERMAID_SCENE_OPTIONS.simplifyTolerance),
369
+ sampleStep: finiteNumberOr(options.sampleStep, DEFAULT_MERMAID_SCENE_OPTIONS.sampleStep),
370
+ includeSourceElements: options.includeSourceElements === true,
371
+ sourceElements: new Map(),
372
+ };
373
+ }
374
+
375
+ function boundsOf(element, deck) {
376
+ const rect = element.getBoundingClientRect();
377
+ const deckRect = deck.getBoundingClientRect();
378
+ return {
379
+ x: roundedMetric(rect.left - deckRect.left),
380
+ y: roundedMetric(rect.top - deckRect.top),
381
+ width: roundedMetric(rect.width),
382
+ height: roundedMetric(rect.height),
383
+ };
384
+ }
385
+
386
+ function svgSize(svg, deck) {
387
+ const rect = boundsOf(svg, deck);
388
+ if (rect.width > 0 && rect.height > 0) return { width: rect.width, height: rect.height };
389
+ const [, , width, height] = String(svg.getAttribute("viewBox") || "")
390
+ .trim()
391
+ .split(/[\s,]+/)
392
+ .map(Number);
393
+ return {
394
+ width: Number.isFinite(width) ? width : 0,
395
+ height: Number.isFinite(height) ? height : 0,
396
+ };
397
+ }
398
+
399
+ function computedSvgStyle(element, options) {
400
+ const style = getComputedStyle(element);
401
+ return cssStyleToSceneStyle({
402
+ fill: style.fill,
403
+ stroke: style.stroke,
404
+ strokeWidth: Number.parseFloat(style.strokeWidth) * elementScale(element),
405
+ strokeDasharray: style.strokeDasharray,
406
+ opacity: effectiveOpacity(element),
407
+ rx: element.getAttribute("rx"),
408
+ }, options);
409
+ }
410
+
411
+ function computedTextStyle(element, options) {
412
+ const style = getComputedStyle(element);
413
+ return {
414
+ fontFamily: style.fontFamily,
415
+ fontSize: roundedMetric(Number.parseFloat(style.fontSize) * elementScale(element)),
416
+ fontWeight: style.fontWeight,
417
+ fontStyle: style.fontStyle,
418
+ color: element.namespaceURI === SVG_NS ? style.fill : style.color,
419
+ textAlign: element.namespaceURI === SVG_NS
420
+ ? ({ start: "left", middle: "center", end: "right" }[style.textAnchor] || "left")
421
+ : style.textAlign,
422
+ opacity: effectiveOpacity(element),
423
+ resolveColor: options.resolveColor,
424
+ };
425
+ }
426
+
427
+ function elementScale(element) {
428
+ const svgElement = element.namespaceURI === SVG_NS ? element : element.closest("foreignObject");
429
+ const matrix = svgElement?.getScreenCTM?.();
430
+ return matrix ? Math.hypot(matrix.a, matrix.b) : 1;
431
+ }
432
+
433
+ function effectiveOpacity(element) {
434
+ let opacity = 1;
435
+ for (let current = element; current; current = current.parentElement) {
436
+ const computed = Number.parseFloat(getComputedStyle(current).opacity);
437
+ opacity *= Number.isFinite(computed) ? computed : 1;
438
+ if (localName(current) === "svg") break;
439
+ }
440
+ return opacity;
441
+ }
442
+
443
+ function labelInfo(root, selector, deck, options) {
444
+ const label = root.querySelector(selector);
445
+ const text = label?.innerText?.trim() || label?.textContent?.trim() || "";
446
+ if (!text) return null;
447
+ return {
448
+ text: structuredLabelText(label, options),
449
+ bounds: boundsOf(label, deck),
450
+ };
451
+ }
452
+
453
+ function structuredLabelText(element, options) {
454
+ const paragraphs = [];
455
+ let runs = [];
456
+ const flush = () => {
457
+ if (!runs.length) return;
458
+ runs[0].text = runs[0].text.trimStart();
459
+ runs[runs.length - 1].text = runs[runs.length - 1].text.trimEnd();
460
+ if (runs.some((run) => run.text)) {
461
+ paragraphs.push({ alignment: normalizeAlignment(computedTextStyle(element, options).textAlign), runs });
462
+ }
463
+ runs = [];
464
+ };
465
+ const walk = (node) => {
466
+ if (node.nodeType === 3) {
467
+ const text = (node.textContent || "").replace(/\s+/g, " ");
468
+ if (text) {
469
+ const run = textToSceneText("x", computedTextStyle(node.parentElement, options), options).paragraphs[0].runs[0];
470
+ runs.push({ ...run, text });
471
+ }
472
+ return;
473
+ }
474
+ const tag = localName(node);
475
+ if (IGNORED_TAGS.has(tag)) return;
476
+ if (tag === "br") { flush(); return; }
477
+ const block = tag === "p" || tag === "div";
478
+ const newSvgLine = tag === "tspan" && (
479
+ Number.parseFloat(node.getAttribute("dy")) !== 0 && Number.isFinite(Number.parseFloat(node.getAttribute("dy"))) ||
480
+ node.hasAttribute("y") && node.getAttribute("y") !== element.getAttribute("y")
481
+ );
482
+ if (newSvgLine) flush();
483
+ if (block) flush();
484
+ for (const child of node.childNodes || []) walk(child);
485
+ if (block) flush();
486
+ };
487
+ walk(element);
488
+ flush();
489
+ return { paragraphs: paragraphs.length ? paragraphs : textToSceneText("", computedTextStyle(element, options), options).paragraphs };
490
+ }
491
+
492
+ function fallbackNode(element, z, deck, reason, sourcePath) {
493
+ let bounds = boundsOf(element, deck);
494
+ if (["path", "line", "polyline"].includes(localName(element))) {
495
+ const style = getComputedStyle(element);
496
+ const marked = [style.markerStart, style.markerMid, style.markerEnd].some((value) => value && value !== "none");
497
+ const padding = Math.max(1, Number.parseFloat(getComputedStyle(element).strokeWidth) || 0,
498
+ marked ? 20 : 0) * elementScale(element);
499
+ bounds = { x: bounds.x - padding, y: bounds.y - padding,
500
+ width: bounds.width + 2 * padding, height: bounds.height + 2 * padding };
501
+ }
502
+ return {
503
+ kind: "fallback",
504
+ id: element.getAttribute?.("id") || undefined,
505
+ sourcePath,
506
+ z,
507
+ bounds,
508
+ reason,
509
+ meta: { mermaid: { tag: tagName(element), class: element.getAttribute?.("class") || "" } },
510
+ };
511
+ }
512
+
513
+ function rectPreset(shape) {
514
+ const rx = Number.parseFloat(shape.getAttribute("rx") || "0");
515
+ const ry = Number.parseFloat(shape.getAttribute("ry") || "0");
516
+ return rx > 0 || ry > 0 ? "roundedRect" : "rect";
517
+ }
518
+
519
+ function shapePresetFor(shape) {
520
+ if (localName(shape) === "rect") return rectPreset(shape);
521
+ if (localName(shape) === "circle" || localName(shape) === "ellipse") return "ellipse";
522
+ if (localName(shape) === "polygon") return classifyPolygonPreset(shape.getAttribute("points"), { fallbackPreset: null });
523
+ return null;
524
+ }
525
+
526
+ function relativeBounds(bounds, origin) {
527
+ return { ...bounds, x: bounds.x - origin.x, y: bounds.y - origin.y };
528
+ }
529
+
530
+ function unsupportedVisualEffect(element, descendants = true) {
531
+ return [element, ...(descendants ? element.querySelectorAll("*") : [])].some((child) => {
532
+ const style = getComputedStyle(child);
533
+ return (style.filter && style.filter !== "none") ||
534
+ (style.clipPath && style.clipPath !== "none") ||
535
+ (style.maskImage && style.maskImage !== "none") ||
536
+ (localName(child) === "g" && Number.parseFloat(style.opacity) < 1 &&
537
+ child.querySelectorAll([...VISUAL_TAGS].join(",")).length > 1) ||
538
+ Number.parseFloat(style.fillOpacity) < 1 || Number.parseFloat(style.strokeOpacity) < 1 ||
539
+ /url\(/i.test(`${style.fill} ${style.stroke}`);
540
+ });
541
+ }
542
+
543
+ function connectorMarkers(element) {
544
+ const style = getComputedStyle(element);
545
+ const start = style.markerStart || element.getAttribute("marker-start");
546
+ const end = style.markerEnd || element.getAttribute("marker-end");
547
+ return {
548
+ arrowStart: markerIdToArrow(start),
549
+ arrowEnd: markerIdToArrow(end),
550
+ unsupported: [start, end].some((value) => value && value !== "none" && markerIdToArrow(value) === "none") ||
551
+ Boolean(style.markerMid && style.markerMid !== "none"),
552
+ };
553
+ }
554
+
555
+ function nodeText(group, deck, options) {
556
+ const label = labelInfo(group, "span.nodeLabel, text", deck, options);
557
+ return {
558
+ ...(label ? { text: label.text } : {}),
559
+ textLayout: { alignment: "center", verticalAlignment: "middle", textWrap: "none" },
560
+ };
561
+ }
562
+
563
+ function paintedPathStyle(paths, options) {
564
+ const fill = paths.find((path) => getComputedStyle(path).fill !== "none") || paths[0];
565
+ const stroke = paths.find((path) => getComputedStyle(path).stroke !== "none") || fill;
566
+ return { ...computedSvgStyle(stroke, options), fill: computedSvgStyle(fill, options).fill };
567
+ }
568
+
569
+ function compatiblePathPaint(paths, options) {
570
+ const style = paintedPathStyle(paths, options);
571
+ return style.dash === "solid" && paths.every((path) => {
572
+ const paint = computedSvgStyle(path, options);
573
+ return paint.opacity === style.opacity &&
574
+ (!paint.fill || paint.fill === style.fill) &&
575
+ (!paint.stroke || (paint.stroke === style.stroke && paint.strokeWidth === style.strokeWidth &&
576
+ paint.dash === style.dash));
577
+ });
578
+ }
579
+
580
+ function compositeGroup(sourcePath, z, bounds, children, shape) {
581
+ return {
582
+ kind: "group", sourcePath, z, bounds,
583
+ children: children.map((child, index) => ({ ...child, z: z + (index + 1) / (children.length + 1) })),
584
+ style: { fill: null, stroke: null, strokeWidth: 0 },
585
+ meta: { mermaid: { kind: "node", shape } },
586
+ };
587
+ }
588
+
589
+ function stadiumParts(shape, group, sourcePath, z, deck, options) {
590
+ const bounds = boundsOf(shape, deck);
591
+ const style = paintedPathStyle(directChildren(shape, "path"), options);
592
+ if (!opaqueCompositeStyle(style) || !compatiblePathPaint(directChildren(shape, "path"), options)) {
593
+ return fallbackNode(group, z, deck, "unsupported-mermaid-composite-paint", sourcePath);
594
+ }
595
+ const radius = bounds.height / 2;
596
+ const children = [0, bounds.width - bounds.height].map((x, index) => ({
597
+ kind: "shape", sourcePath: `${sourcePath}.parts[${index}]`, z: index,
598
+ bounds: { x, y: 0, width: bounds.height, height: bounds.height },
599
+ preset: "ellipse", style,
600
+ }));
601
+ children.push({
602
+ kind: "shape", sourcePath: `${sourcePath}.parts[2]`, z: 2,
603
+ bounds: { x: radius, y: 0, width: bounds.width - bounds.height, height: bounds.height },
604
+ preset: "rect", style: { ...style, stroke: null, strokeWidth: 0 },
605
+ });
606
+ for (const y of [0, bounds.height]) children.push({
607
+ kind: "connector", sourcePath: `${sourcePath}.parts[${children.length}]`, z: children.length,
608
+ points: [{ x: radius, y }, { x: bounds.width - radius, y }], style: { ...style, fill: null },
609
+ });
610
+ const label = labelInfo(group, "span.nodeLabel, text", deck, options);
611
+ if (label) children.push({
612
+ kind: "text", sourcePath: `${sourcePath}.label`, z: children.length,
613
+ bounds: relativeBounds(label.bounds, bounds), text: label.text,
614
+ textLayout: { alignment: "center", verticalAlignment: "middle", textWrap: "none" },
615
+ });
616
+ return compositeGroup(sourcePath, z, bounds, children, "stadium");
617
+ }
618
+
619
+ function opaqueCompositeStyle(style) {
620
+ return style.fill && (style.opacity === undefined || style.opacity === 1) &&
621
+ style.dash === "solid" && !/^rgba\(/i.test(style.fill);
622
+ }
623
+
624
+ function isStadiumPath(path) {
625
+ const box = path.getBBox();
626
+ const radius = box.height / 2;
627
+ if (radius <= 0 || box.width < box.height) return false;
628
+ const length = path.getTotalLength();
629
+ const start = path.getPointAtLength(0);
630
+ const end = path.getPointAtLength(length);
631
+ if (Math.hypot(start.x - end.x, start.y - end.y) > 0.01) return false;
632
+ for (let index = 0; index < 64; index += 1) {
633
+ const point = path.getPointAtLength(length * index / 64);
634
+ const x = point.x - box.x;
635
+ const y = point.y - box.y;
636
+ const distance = x < radius ? Math.abs(Math.hypot(x - radius, y - radius) - radius)
637
+ : x > box.width - radius ? Math.abs(Math.hypot(x - box.width + radius, y - radius) - radius)
638
+ : Math.min(Math.abs(y), Math.abs(y - box.height));
639
+ if (distance > 0.25) return false;
640
+ }
641
+ return true;
642
+ }
643
+
644
+ function matchingOutline(path, reference, distanceFromOutline) {
645
+ const bounds = path.getBBox();
646
+ const expected = reference.getBBox();
647
+ const screen = path.getScreenCTM();
648
+ const referenceScreen = reference.getScreenCTM();
649
+ if (["a", "b", "c", "d", "e", "f"].some((key) => Math.abs(screen[key] - referenceScreen[key]) > 0.001) ||
650
+ ["x", "y", "width", "height"].some((key) => Math.abs(bounds[key] - expected[key]) > 0.25)) return false;
651
+ const length = path.getTotalLength();
652
+ if (!Number.isFinite(length) || length <= 0) return false;
653
+ for (let index = 0; index <= 128; index += 1) {
654
+ const point = path.getPointAtLength(length * index / 128);
655
+ if (distanceFromOutline(point, expected) > 0.25) return false;
656
+ }
657
+ return true;
658
+ }
659
+
660
+ function matchingStadiumOutline(path, reference) {
661
+ return matchingOutline(path, reference, (point, box) => {
662
+ const radius = box.height / 2;
663
+ const x = point.x - box.x;
664
+ const y = point.y - box.y;
665
+ return x < radius ? Math.abs(Math.hypot(x - radius, y - radius) - radius)
666
+ : x > box.width - radius ? Math.abs(Math.hypot(x - box.width + radius, y - radius) - radius)
667
+ : Math.min(Math.abs(y), Math.abs(y - box.height));
668
+ });
669
+ }
670
+
671
+ function cylinderParts(path, group, sourcePath, z, deck, options) {
672
+ const d = path.getAttribute("d") || "";
673
+ // The pinned renderer draws a cylinder with move, arc, arc, line, arc, line.
674
+ if (d.replace(/[-+]?(?:\d*\.?\d+)(?:e[-+]?\d+)?|[\s,]/gi, "") !== "Maalal") return null;
675
+ const numbers = d.match(/[-+]?(?:\d*\.?\d+)(?:e[-+]?\d+)?/gi)?.map(Number) || [];
676
+ if (numbers.length !== 27) return null;
677
+ const [x, ry, rx] = numbers;
678
+ if ([2, 9, 18].some((offset) => numbers[offset + 2] !== 0 ||
679
+ numbers[offset + 3] !== 0 || numbers[offset + 4] !== 0)) return null;
680
+ if (x !== 0 || rx <= 0 || ry <= 0 ||
681
+ numbers[3] !== ry || numbers[7] !== 2 * rx || numbers[8] !== 0 ||
682
+ numbers[9] !== rx || numbers[10] !== ry || numbers[14] !== -2 * rx ||
683
+ numbers[15] !== 0 || numbers[16] !== 0 || numbers[18] !== rx ||
684
+ numbers[19] !== ry || numbers[23] !== 2 * rx || numbers[24] !== 0 ||
685
+ numbers[25] !== 0 || numbers[26] !== -numbers[17]) return null;
686
+ const bounds = boundsOf(path, deck);
687
+ const capHeight = 2 * ry * bounds.height / path.getBBox().height;
688
+ if (capHeight <= 0 || capHeight >= bounds.height) return null;
689
+ const style = computedSvgStyle(path, options);
690
+ if (!opaqueCompositeStyle(style)) return null;
691
+ const children = [];
692
+ const addShape = (preset, partBounds, partStyle) => children.push({
693
+ kind: "shape", sourcePath: `${sourcePath}.parts[${children.length}]`, z: children.length,
694
+ preset, bounds: partBounds, style: partStyle,
695
+ });
696
+ addShape("ellipse", { x: 0, y: bounds.height - capHeight, width: bounds.width, height: capHeight }, style);
697
+ addShape("rect", { x: 0, y: capHeight / 2, width: bounds.width, height: bounds.height - capHeight },
698
+ { ...style, stroke: null, strokeWidth: 0 });
699
+ for (const side of [0, bounds.width]) children.push({
700
+ kind: "connector", sourcePath: `${sourcePath}.parts[${children.length}]`, z: children.length,
701
+ points: [{ x: side, y: capHeight / 2 }, { x: side, y: bounds.height - capHeight / 2 }],
702
+ style: { ...style, fill: null },
703
+ });
704
+ addShape("ellipse", { x: 0, y: 0, width: bounds.width, height: capHeight }, style);
705
+ const label = labelInfo(group, "span.nodeLabel, text", deck, options);
706
+ if (label) children.push({
707
+ kind: "text", sourcePath: `${sourcePath}.label`, z: children.length,
708
+ bounds: relativeBounds(label.bounds, bounds), text: label.text,
709
+ textLayout: { alignment: "center", verticalAlignment: "middle", textWrap: "none" },
710
+ });
711
+ return compositeGroup(sourcePath, z, bounds, children, "cylinder");
712
+ }
713
+
714
+ function nodeShape(group, sourceIndex, z, deck, options) {
715
+ const sourcePath = `nodes[${sourceIndex}]`;
716
+ if (unsupportedVisualEffect(group) || group.querySelector("img, image, svg, .katex, use")) {
717
+ return fallbackNode(group, z, deck, "unsupported-mermaid-node-content", sourcePath);
718
+ }
719
+ const shape = directChildren(group).find((child) => hasClass(child, "label-container"));
720
+ if (!shape) return fallbackNode(group, z, deck, "unsupported-mermaid-node-structure", `nodes[${sourceIndex}]`);
721
+ if (directChildren(group).some((child) => child !== shape && !hasClass(child, "label"))) {
722
+ return fallbackNode(group, z, deck, "unsupported-mermaid-node-shape", sourcePath);
723
+ }
724
+ if (localName(shape) === "path") {
725
+ const cylinder = cylinderParts(shape, group, sourcePath, z, deck, options);
726
+ if (cylinder) return cylinder;
727
+ }
728
+ if (localName(shape) === "g") {
729
+ const circles = directChildren(shape, "circle");
730
+ if (circles.length === 2 && directChildren(shape).length === 2 &&
731
+ hasClass(circles[0], "outer-circle") && hasClass(circles[1], "inner-circle")) {
732
+ const bounds = boundsOf(shape, deck);
733
+ return compositeGroup(sourcePath, z, bounds, circles.map((circle, index) => ({
734
+ kind: "shape", sourcePath: `${sourcePath}.circles[${index}]`, z: index,
735
+ bounds: relativeBounds(boundsOf(circle, deck), bounds), preset: "ellipse",
736
+ style: computedSvgStyle(circle, options),
737
+ ...(index === 1 ? nodeText(group, deck, options) : {}),
738
+ })), "double-circle");
739
+ }
740
+ const paths = directChildren(shape, "path");
741
+ if (paths.length === 2 && directChildren(shape).length === 2 && isStadiumPath(paths[0]) &&
742
+ matchingStadiumOutline(paths[1], paths[0])) {
743
+ return stadiumParts(shape, group, sourcePath, z, deck, options);
744
+ }
745
+ }
746
+ const preset = shapePresetFor(shape);
747
+ if (!preset || directChildren(group).some((child) => child !== shape && !hasClass(child, "label"))) {
748
+ return fallbackNode(group, z, deck, "unsupported-mermaid-node-shape", sourcePath);
749
+ }
750
+ const label = labelInfo(group, "span.nodeLabel, text", deck, options);
751
+ return definedEntries({
752
+ kind: "shape",
753
+ id: group.getAttribute("id") || undefined,
754
+ sourcePath: `nodes[${sourceIndex}]`,
755
+ z,
756
+ bounds: boundsOf(shape, deck),
757
+ preset,
758
+ style: computedSvgStyle(shape, options),
759
+ text: label?.text,
760
+ textLayout: {
761
+ alignment: "center",
762
+ verticalAlignment: "middle",
763
+ textWrap: "none",
764
+ },
765
+ meta: {
766
+ mermaid: definedEntries({
767
+ kind: "node",
768
+ tag: localName(shape),
769
+ polygonSignature: localName(shape) === "polygon" ? polygonPointsSignature(shape.getAttribute("points")) : undefined,
770
+ }),
771
+ },
772
+ });
773
+ }
774
+
775
+ function clusterGroup(group, sourceIndex, z, deck, options) {
776
+ const rect = directChildren(group, "rect")[0];
777
+ if (!rect) return fallbackNode(group, z, deck, "unsupported-mermaid-cluster-structure", `clusters[${sourceIndex}]`);
778
+ if (unsupportedVisualEffect(group) || group.querySelector("img, image, svg, .katex, use") ||
779
+ directChildren(group).some((child) => child !== rect && !hasClass(child, "cluster-label"))) {
780
+ return fallbackNode(group, z, deck, "unsupported-mermaid-cluster-content", `clusters[${sourceIndex}]`);
781
+ }
782
+ const label = labelInfo(group, "span.nodeLabel", deck, options);
783
+ return definedEntries({
784
+ kind: "group",
785
+ id: group.getAttribute("id") || undefined,
786
+ sourcePath: `clusters[${sourceIndex}]`,
787
+ z,
788
+ bounds: boundsOf(rect, deck),
789
+ children: [],
790
+ style: computedSvgStyle(rect, options),
791
+ text: label?.text,
792
+ textLayout: {
793
+ alignment: "center",
794
+ verticalAlignment: "top",
795
+ textWrap: "none",
796
+ textInsets: { left: 4, top: 4, right: 4, bottom: 4 },
797
+ },
798
+ meta: { mermaid: { kind: "cluster" } },
799
+ });
800
+ }
801
+
802
+ function screenPoint(svg, point, deck) {
803
+ const matrix = svg.getScreenCTM();
804
+ const transformed = matrix ? new DOMPoint(point.x, point.y).matrixTransform(matrix) : point;
805
+ const rect = deck.getBoundingClientRect();
806
+ return {
807
+ x: roundedMetric(transformed.x - rect.left),
808
+ y: roundedMetric(transformed.y - rect.top),
809
+ };
810
+ }
811
+
812
+ function sampledPathPoints(path, svg, deck, options) {
813
+ const total = path.getTotalLength();
814
+ const sampleCount = Math.max(2, Math.round(total / Math.max(1, options.sampleStep)) + 1);
815
+ if (!Number.isFinite(total) || sampleCount > MAX_SCENE_NODES) throw new Error("path sampling limit exceeded");
816
+ const raw = [];
817
+ for (let index = 0; index < sampleCount; index += 1) {
818
+ const distance = total * (index / (sampleCount - 1));
819
+ raw.push(screenPoint(path, path.getPointAtLength(distance), deck));
820
+ }
821
+ return {
822
+ raw,
823
+ simplified: simplifyPolyline(raw, options.simplifyTolerance),
824
+ };
825
+ }
826
+
827
+ function connectorPath(path, sourceIndex, z, svg, deck, options, edgeLabels) {
828
+ try {
829
+ const markers = connectorMarkers(path);
830
+ if (unsupportedVisualEffect(path) || markers.unsupported) {
831
+ return fallbackNode(path, z, deck, "unsupported-mermaid-edge-style", `edges[${sourceIndex}]`);
832
+ }
833
+ // Sampling across multiple subpaths joins disconnected strokes with invented lines.
834
+ const commands = (path.getAttribute("d") || "").match(/[a-df-z]/gi) || [];
835
+ if (commands.filter((command) => command.toLowerCase() === "m").length !== 1 ||
836
+ commands.some((command) => command.toLowerCase() === "z")) {
837
+ return fallbackNode(path, z, deck, "unsupported-mermaid-edge-path", `edges[${sourceIndex}]`);
838
+ }
839
+ const points = sampledPathPoints(path, svg, deck, options);
840
+ const id = path.getAttribute("data-id") || path.getAttribute("id") || "";
841
+ return definedEntries({
842
+ kind: "connector",
843
+ id: path.getAttribute("id") || undefined,
844
+ sourcePath: `edges[${sourceIndex}]`,
845
+ z,
846
+ points: points.simplified,
847
+ style: computedSvgStyle(path, options),
848
+ arrowStart: markers.arrowStart,
849
+ arrowEnd: markers.arrowEnd,
850
+ meta: {
851
+ mermaid: {
852
+ kind: "edge",
853
+ id,
854
+ rawPointCount: points.raw.length,
855
+ simplifiedPointCount: points.simplified.length,
856
+ },
857
+ },
858
+ });
859
+ } catch (error) {
860
+ return fallbackNode(path, z, deck, `unsupported-mermaid-edge-path: ${error?.message || "path sampling failed"}`, `edges[${sourceIndex}]`);
861
+ }
862
+ }
863
+
864
+ function readEdgeLabels(root, deck, options, consumed) {
865
+ const labels = new Map();
866
+ for (const [index, group] of [...root.querySelectorAll(":scope > g.edgeLabels > g.edgeLabel")].entries()) {
867
+ if (consumed?.has(group.parentElement)) continue;
868
+ consumed?.add(group);
869
+ const labelGroup = group.querySelector(":scope > g.label");
870
+ const id = labelGroup?.getAttribute("data-id") || "";
871
+ let key = id || `unidentified-${index}`;
872
+ while (labels.has(key)) key += `-${index}`;
873
+ const sourcePath = `edgeLabels[${key}]`;
874
+ options.sourceElements?.set(sourcePath, group);
875
+ if (group.querySelector("img, image, svg, .katex, use, path, line, polygon, polyline, circle, ellipse, rect") ||
876
+ unsupportedVisualEffect(group)) {
877
+ labels.set(key, { fallback: fallbackNode(group, 0, deck,
878
+ "unsupported-mermaid-edge-label", sourcePath) });
879
+ continue;
880
+ }
881
+ const label = labelInfo(group, "span.edgeLabel, text", deck, options);
882
+ const background = group.querySelector(".edgeLabel p, span.edgeLabel, rect");
883
+ const style = background && getComputedStyle(background);
884
+ if (label && label.bounds.width > 0 && label.bounds.height > 0) {
885
+ labels.set(key, { ...label, fill: normalizeColor(style?.backgroundColor === "rgba(0, 0, 0, 0)"
886
+ ? (background?.localName === "rect" ? style.fill : null) : style?.backgroundColor, options.resolveColor) });
887
+ } else if (group.textContent.trim() || [...group.querySelectorAll("rect, path, image, use")].some(isVisibleUnknown)) {
888
+ labels.set(key, { fallback: fallbackNode(group, 0, deck, "unsupported-mermaid-edge-label", sourcePath) });
889
+ }
890
+ }
891
+ return labels;
892
+ }
893
+
894
+ function isKnownContainer(element) {
895
+ return (
896
+ hasClass(element, "root") ||
897
+ hasClass(element, "clusters") ||
898
+ hasClass(element, "edgePaths") ||
899
+ hasClass(element, "edgeLabels") ||
900
+ hasClass(element, "edgeLabel") ||
901
+ hasClass(element, "label") ||
902
+ hasClass(element, "nodes")
903
+ );
904
+ }
905
+
906
+ function isVisibleUnknown(element) {
907
+ if (IGNORED_TAGS.has(tagName(element)) || IGNORED_TAGS.has(localName(element))) return false;
908
+ if (isKnownContainer(element)) return false;
909
+ if (!VISUAL_TAGS.has(localName(element)) && localName(element) !== "g") return false;
910
+ const rect = element.getBoundingClientRect();
911
+ return rect.width > 0 || rect.height > 0;
912
+ }
913
+
914
+ function collectUnexpectedVisuals(container, deck, startZ, sourcePath, consumed = new Set(), options = {}) {
915
+ const fallbacks = [];
916
+ const walk = (element) => {
917
+ if (consumed.has(element)) return;
918
+ if (IGNORED_TAGS.has(tagName(element)) || IGNORED_TAGS.has(localName(element))) return;
919
+ if (getComputedStyle(element).display === "none") return;
920
+ const containsConsumed = [...consumed].some((child) => element.contains(child));
921
+ if (!containsConsumed && isVisibleUnknown(element)) {
922
+ const path = `${sourcePath}.unknown[${fallbacks.length}]`;
923
+ options.sourceElements?.set(path, element);
924
+ fallbacks.push(fallbackNode(element, startZ + fallbacks.length, deck, "unsupported-mermaid-svg-element", path));
925
+ return;
926
+ }
927
+ for (const child of directChildren(element)) walk(child);
928
+ };
929
+ for (const child of directChildren(container)) walk(child);
930
+ return fallbacks;
931
+ }
932
+
933
+ function appendEdgeLabels(nodes, labels) {
934
+ for (const [id, label] of labels) {
935
+ if (label.fallback) nodes.push({ ...label.fallback, z: nodes.length });
936
+ else nodes.push({
937
+ kind: "shape", sourcePath: `edgeLabels[${id}]`, z: nodes.length,
938
+ bounds: label.bounds, preset: "rect", style: { fill: label.fill, stroke: null, strokeWidth: 0 },
939
+ text: label.text,
940
+ textLayout: { alignment: "center", verticalAlignment: "middle", textWrap: "none" },
941
+ meta: { mermaid: { kind: "edge-label", edgeId: id } },
942
+ });
943
+ }
944
+ }
945
+
946
+ function diagramScene(svg, size, options, nodes) {
947
+ return {
948
+ scene: createScene({
949
+ ...size, source: { kind: "mermaid", path: options.path },
950
+ accessibility: { title: svg.getAttribute("aria-label") || svg.getAttribute("aria-roledescription") || "Mermaid" },
951
+ nodes,
952
+ }),
953
+ diagnostics: [],
954
+ };
955
+ }
956
+
957
+ function unsupportedContainers(root, deck, nodes, options) {
958
+ const consumed = new Set();
959
+ for (const [index, container] of directChildren(root, "g").entries()) {
960
+ if (!isKnownContainer(container) || !unsupportedVisualEffect(container, false)) continue;
961
+ const sourcePath = `root.containers[${index}]`;
962
+ consumed.add(container);
963
+ options.sourceElements.set(sourcePath, container);
964
+ nodes.push(fallbackNode(container, nodes.length, deck, "unsupported-mermaid-container-style", sourcePath));
965
+ }
966
+ return consumed;
967
+ }
968
+
969
+ function preservePaintOrder(nodes, sourceElements) {
970
+ nodes.sort((left, right) => {
971
+ const a = sourceElements.get(left.sourcePath);
972
+ const b = sourceElements.get(right.sourcePath);
973
+ if (!a || !b || a === b) return left.z - right.z;
974
+ const position = a.compareDocumentPosition(b);
975
+ return position & 4 ? -1 : position & 2 ? 1 : left.z - right.z;
976
+ });
977
+ const assignZ = (node, z, span) => {
978
+ node.z = z;
979
+ node.children?.forEach((child, index, children) =>
980
+ assignZ(child, z + (index + 1) * span / (children.length + 1), span / (children.length + 1)));
981
+ };
982
+ nodes.forEach((node, index) => assignZ(node, index, 1));
983
+ }
984
+
985
+ function measuredText(element, sourcePath, z, deck, options) {
986
+ return {
987
+ kind: "text", sourcePath, z, bounds: boundsOf(element, deck),
988
+ text: structuredLabelText(element, options),
989
+ textLayout: { alignment: "center", verticalAlignment: "middle", textWrap: "none" },
990
+ };
991
+ }
992
+
993
+ function sequenceScene(svg, deck, size, options) {
994
+ const nodes = [];
995
+ const walk = (element) => {
996
+ const tag = localName(element);
997
+ if (IGNORED_TAGS.has(tag)) return;
998
+ if (getComputedStyle(element).display === "none") return;
999
+ const sourcePath = `sequence[${nodes.length}]`;
1000
+ options.sourceElements?.set(sourcePath, element);
1001
+ if (unsupportedVisualEffect(element, tag !== "g")) {
1002
+ nodes.push(fallbackNode(element, nodes.length, deck, "unsupported-mermaid-sequence-style", sourcePath));
1003
+ return;
1004
+ }
1005
+ if (tag === "g") {
1006
+ for (const child of directChildren(element)) walk(child);
1007
+ return;
1008
+ }
1009
+ if (tag === "rect" && /^(?:actor|activation\d+|note)(?:\s|$)/.test(element.getAttribute("class") || "")) {
1010
+ nodes.push({ kind: "shape", sourcePath, z: nodes.length, bounds: boundsOf(element, deck),
1011
+ preset: rectPreset(element), style: computedSvgStyle(element, options) });
1012
+ return;
1013
+ }
1014
+ if (tag === "text" && /^(?:actor|messageText|noteText)(?:\s|$)/.test(element.getAttribute("class") || "") &&
1015
+ !element.querySelector(":not(tspan)")) {
1016
+ nodes.push(measuredText(element, sourcePath, nodes.length, deck, options));
1017
+ return;
1018
+ }
1019
+ const markers = tag === "line" ? connectorMarkers(element) : null;
1020
+ if (tag === "line" && /^(?:actor-line|messageLine\d+)(?:\s|$)/.test(element.getAttribute("class") || "") &&
1021
+ !markers.unsupported) {
1022
+ nodes.push({
1023
+ kind: "connector", sourcePath, z: nodes.length,
1024
+ points: [1, 2].map((index) => screenPoint(element, {
1025
+ x: Number(element.getAttribute(`x${index}`)), y: Number(element.getAttribute(`y${index}`)),
1026
+ }, deck)),
1027
+ style: computedSvgStyle(element, options),
1028
+ arrowStart: markers.arrowStart,
1029
+ arrowEnd: markers.arrowEnd,
1030
+ });
1031
+ return;
1032
+ }
1033
+ if (VISUAL_TAGS.has(tag)) nodes.push(fallbackNode(element, nodes.length, deck,
1034
+ "unsupported-mermaid-sequence-element", sourcePath));
1035
+ };
1036
+ for (const child of directChildren(svg)) walk(child);
1037
+ return diagramScene(svg, size, options, nodes);
1038
+ }
1039
+
1040
+ function isRectanglePath(path) {
1041
+ const d = path?.getAttribute("d") || "";
1042
+ if (d.replace(/[-+]?(?:\d*\.?\d+)(?:e[-+]?\d+)?|[\s,]/gi, "") !== "MLLL") return false;
1043
+ const values = d.match(/[-+]?(?:\d*\.?\d+)(?:e[-+]?\d+)?/gi)?.map(Number) || [];
1044
+ return values.length === 8 && values[0] === values[6] && values[2] === values[4] &&
1045
+ values[1] === values[3] && values[5] === values[7];
1046
+ }
1047
+
1048
+ function classParts(group, outline, paths, options) {
1049
+ if (paths.length !== 2 || directChildren(outline).length !== 2 || !isRectanglePath(paths[0]) ||
1050
+ !compatiblePathPaint(paths, options) || !matchingOutline(paths[1], paths[0], (point, box) =>
1051
+ Math.min(Math.abs(point.x - box.x), Math.abs(point.x - box.x - box.width),
1052
+ Math.abs(point.y - box.y), Math.abs(point.y - box.y - box.height)))) return null;
1053
+ const labels = [...group.querySelectorAll("span.nodeLabel, text")]
1054
+ .filter((label) => !label.parentElement.closest("span.nodeLabel, text"));
1055
+ const dividers = [...group.querySelectorAll(":scope > g.divider > path")];
1056
+ const covered = new Set([...paths, ...labels, ...dividers]);
1057
+ for (const visual of group.querySelectorAll([...VISUAL_TAGS].join(","))) {
1058
+ if (covered.has(visual) || labels.some((label) => label.contains(visual))) continue;
1059
+ if (localName(visual) === "foreignObject" && labels.some((label) => visual.contains(label))) {
1060
+ const extra = visual.cloneNode(true);
1061
+ extra.querySelectorAll("span.nodeLabel, text").forEach((label) => label.remove());
1062
+ if (!extra.textContent.trim()) continue;
1063
+ }
1064
+ return null;
1065
+ }
1066
+ return { labels, dividers };
1067
+ }
1068
+
1069
+ function classScene(svg, root, deck, size, options) {
1070
+ const nodes = [];
1071
+ const consumed = unsupportedContainers(root, deck, nodes, options);
1072
+ for (const [index, path] of [...root.querySelectorAll(":scope > g.edgePaths > path.relation")].entries()) {
1073
+ if (consumed.has(path.parentElement)) continue;
1074
+ consumed.add(path);
1075
+ options.sourceElements?.set(`edges[${index}]`, path);
1076
+ nodes.push(connectorPath(path, index, nodes.length, svg, deck, options, new Map()));
1077
+ }
1078
+ appendEdgeLabels(nodes, readEdgeLabels(root, deck, options, consumed));
1079
+ for (const [index, group] of [...root.querySelectorAll(":scope > g.nodes > g.node")].entries()) {
1080
+ if (consumed.has(group.parentElement)) continue;
1081
+ consumed.add(group);
1082
+ const sourcePath = `classes[${index}]`;
1083
+ options.sourceElements?.set(sourcePath, group);
1084
+ const outline = group.querySelector(":scope > g.label-container");
1085
+ const paths = outline ? directChildren(outline, "path") : [];
1086
+ const children = directChildren(group);
1087
+ const parts = outline && classParts(group, outline, paths, options);
1088
+ if (unsupportedVisualEffect(group) || !parts ||
1089
+ group.querySelector("img, image, svg, .katex, use") ||
1090
+ children.some((child) => child !== outline &&
1091
+ !["annotation-group", "label-group", "members-group", "methods-group", "divider"].some((name) => hasClass(child, name)))) {
1092
+ nodes.push(fallbackNode(group, nodes.length, deck, "unsupported-mermaid-class-node", sourcePath));
1093
+ continue;
1094
+ }
1095
+ nodes.push({
1096
+ kind: "shape", sourcePath, z: nodes.length, bounds: boundsOf(outline, deck),
1097
+ preset: "rect", style: paintedPathStyle(paths, options),
1098
+ });
1099
+ options.sourceElements.set(sourcePath, outline);
1100
+ for (const [labelIndex, label] of parts.labels.entries()) {
1101
+ options.sourceElements?.set(`${sourcePath}.labels[${labelIndex}]`, label);
1102
+ nodes.push(measuredText(label, `${sourcePath}.labels[${labelIndex}]`, nodes.length, deck, options));
1103
+ }
1104
+ for (const [dividerIndex, divider] of parts.dividers.entries()) {
1105
+ options.sourceElements?.set(`${sourcePath}.dividers[${dividerIndex}]`, divider);
1106
+ const box = divider.getBBox();
1107
+ if (box.height > 0.1) {
1108
+ nodes.push(fallbackNode(divider, nodes.length, deck, "unsupported-mermaid-class-divider",
1109
+ `${sourcePath}.dividers[${dividerIndex}]`));
1110
+ } else {
1111
+ nodes.push({
1112
+ kind: "connector", sourcePath: `${sourcePath}.dividers[${dividerIndex}]`, z: nodes.length,
1113
+ points: [{ x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }].map((point) => screenPoint(divider, point, deck)),
1114
+ style: computedSvgStyle(divider, options),
1115
+ });
1116
+ }
1117
+ }
1118
+ }
1119
+ nodes.push(...collectUnexpectedVisuals(root, deck, nodes.length, "root", consumed, options));
1120
+ nodes.push(...collectUnexpectedVisuals(svg, deck, nodes.length, "svg", new Set([root]), options));
1121
+ return diagramScene(svg, size, options, nodes);
1122
+ }
1123
+
1124
+ function sceneFromSvg(svg, options) {
1125
+ options.sourceElements?.set("svg", svg);
1126
+ const deck = options.deck || svg.closest(".deck") || svg.parentElement || svg;
1127
+ const size = svgSize(svg, deck);
1128
+ const root = svg.querySelector("g.root");
1129
+ const nodes = [];
1130
+ const elements = svg.querySelectorAll("*");
1131
+ if (elements.length > MAX_SCENE_NODES * 10 || [...elements].some((element) => {
1132
+ if (!VISUAL_TAGS.has(localName(element)) || element.closest("defs, marker")) return false;
1133
+ const matrix = element.getScreenCTM?.();
1134
+ return matrix && (Math.abs(matrix.b) > 0.001 || Math.abs(matrix.c) > 0.001 || matrix.a <= 0 || matrix.d <= 0);
1135
+ })) {
1136
+ const reason = elements.length > MAX_SCENE_NODES * 10
1137
+ ? "mermaid-scene-limit-exceeded: SVG element count" : "unsupported-mermaid-svg-transform";
1138
+ return { scene: fallbackSceneForReason(createScene({ ...size, source: { kind: "mermaid", path: options.path } }), reason,
1139
+ boundsOf(svg, deck)), diagnostics: [{ path: "svg", kind: "fallback", reason }] };
1140
+ }
1141
+ const diagramType = svg.getAttribute("aria-roledescription");
1142
+ const outerElements = [svg];
1143
+ for (let element = root; element && element !== svg; element = element.parentElement) outerElements.push(element);
1144
+ if (outerElements.some((element) => unsupportedVisualEffect(element, false))) {
1145
+ const reason = "unsupported-mermaid-svg-style";
1146
+ return { scene: fallbackSceneForReason(createScene({ ...size, source: { kind: "mermaid", path: options.path } }),
1147
+ reason, boundsOf(svg, deck)), diagnostics: [{ path: "svg", kind: "fallback", reason }] };
1148
+ }
1149
+ if (diagramType === "sequence") return sequenceScene(svg, deck, size, options);
1150
+ if (root && diagramType === "class") return classScene(svg, root, deck, size, options);
1151
+ if (!root || !hasClass(svg, "flowchart")) {
1152
+ return {
1153
+ scene: fallbackSceneForReason(
1154
+ createScene({ width: size.width, height: size.height, source: { kind: "mermaid", path: options.path }, nodes: [] }),
1155
+ "unsupported-mermaid-svg-structure",
1156
+ boundsOf(svg, deck),
1157
+ ),
1158
+ diagnostics: [{ path: "svg", kind: "fallback", reason: "unsupported-mermaid-svg-structure" }],
1159
+ };
1160
+ }
1161
+
1162
+ const consumed = unsupportedContainers(root, deck, nodes, options);
1163
+ const edgeLabels = readEdgeLabels(root, deck, options, consumed);
1164
+ for (const [clusterIndex, cluster] of [...root.querySelectorAll(":scope > g.clusters > g.cluster")].entries()) {
1165
+ if (consumed.has(cluster.parentElement)) continue;
1166
+ consumed.add(cluster);
1167
+ options.sourceElements?.set(`clusters[${clusterIndex}]`, cluster);
1168
+ nodes.push(clusterGroup(cluster, clusterIndex, nodes.length, deck, options));
1169
+ }
1170
+ for (const [edgeIndex, path] of [...root.querySelectorAll(":scope > g.edgePaths > path.flowchart-link")].entries()) {
1171
+ if (consumed.has(path.parentElement)) continue;
1172
+ consumed.add(path);
1173
+ options.sourceElements?.set(`edges[${edgeIndex}]`, path);
1174
+ nodes.push(connectorPath(path, edgeIndex, nodes.length, svg, deck, options, edgeLabels));
1175
+ }
1176
+ appendEdgeLabels(nodes, edgeLabels);
1177
+ for (const [nodeIndex, node] of [...root.querySelectorAll(":scope > g.nodes > g.node")].entries()) {
1178
+ if (consumed.has(node.parentElement)) continue;
1179
+ consumed.add(node);
1180
+ options.sourceElements?.set(`nodes[${nodeIndex}]`, node);
1181
+ nodes.push(nodeShape(node, nodeIndex, nodes.length, deck, options));
1182
+ }
1183
+ nodes.push(...collectUnexpectedVisuals(root, deck, nodes.length, "root", consumed, options));
1184
+ nodes.push(...collectUnexpectedVisuals(svg, deck, nodes.length, "svg", new Set([root]), options));
1185
+
1186
+ return {
1187
+ scene: createScene({
1188
+ width: size.width,
1189
+ height: size.height,
1190
+ source: { kind: "mermaid", path: options.path },
1191
+ accessibility: definedEntries({
1192
+ title: svg.getAttribute("aria-label") || svg.getAttribute("aria-roledescription") || undefined,
1193
+ }),
1194
+ nodes,
1195
+ }),
1196
+ diagnostics: [],
1197
+ };
1198
+ }
1199
+
1200
+ // includeSourceElements exposes DOM references alongside, never inside, the serializable scene.
1201
+ export function mermaidSvgToScene(svg, options = {}) {
1202
+ const normalizedOptions = normalizeOptions(options);
1203
+ const fallbackBounds = (() => {
1204
+ try {
1205
+ const deck = normalizedOptions.deck || svg?.closest?.(".deck") || svg?.parentElement || svg;
1206
+ const size = svg && deck ? svgSize(svg, deck) : { width: 0, height: 0 };
1207
+ return svg && deck ? boundsOf(svg, deck) : { x: 0, y: 0, width: size.width, height: size.height };
1208
+ } catch (_) {
1209
+ return { x: 0, y: 0, width: 0, height: 0 };
1210
+ }
1211
+ })();
1212
+ try {
1213
+ const result = sceneFromSvg(svg, normalizedOptions);
1214
+ preservePaintOrder(result.scene.nodes, normalizedOptions.sourceElements);
1215
+ const limited = enforceSceneLimits(result.scene, { bounds: fallbackBounds });
1216
+ const normalized = normalizeScene(limited.scene);
1217
+ const diagnostics = [...result.diagnostics, ...limited.diagnostics, ...normalized.diagnostics];
1218
+ for (const node of normalized.scene.nodes) {
1219
+ if (node.kind === "fallback" && !diagnostics.some((entry) => entry.path === node.sourcePath && entry.reason === node.reason)) {
1220
+ diagnostics.push({ path: node.sourcePath, kind: "fallback", reason: node.reason });
1221
+ }
1222
+ }
1223
+ validateScene(normalized.scene);
1224
+ return {
1225
+ scene: normalized.scene, diagnostics,
1226
+ ...(normalizedOptions.includeSourceElements ? { sourceElements: normalizedOptions.sourceElements } : {}),
1227
+ };
1228
+ } catch (error) {
1229
+ const reason = `mermaid-scene-adapter-failed: ${error?.message || "unknown error"}`;
1230
+ const scene = fallbackSceneForReason(
1231
+ createScene({
1232
+ width: fallbackBounds.width,
1233
+ height: fallbackBounds.height,
1234
+ source: { kind: "mermaid", path: normalizedOptions.path },
1235
+ nodes: [],
1236
+ }),
1237
+ reason,
1238
+ fallbackBounds,
1239
+ );
1240
+ const normalized = normalizeScene(scene);
1241
+ validateScene(normalized.scene);
1242
+ return {
1243
+ scene: normalized.scene,
1244
+ diagnostics: [{ path: "svg", kind: "fallback", reason }, ...normalized.diagnostics],
1245
+ ...(normalizedOptions.includeSourceElements ? { sourceElements: normalizedOptions.sourceElements } : {}),
1246
+ };
1247
+ }
1248
+ }