flowchart-sequence-designer 1.1.0 → 1.2.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.
- package/dist/index.cjs +250 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +248 -43
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +2840 -1177
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +2853 -1190
- package/dist/ui/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,8 @@ __export(src_exports, {
|
|
|
26
26
|
flowchart: () => flowchart,
|
|
27
27
|
fromJSON: () => fromJSON,
|
|
28
28
|
fromMermaid: () => fromMermaid,
|
|
29
|
+
sanitizeLabel: () => sanitizeLabel,
|
|
30
|
+
sanitizeURL: () => sanitizeURL,
|
|
29
31
|
sequence: () => sequence,
|
|
30
32
|
toJSON: () => toJSON,
|
|
31
33
|
toMermaid: () => toMermaid,
|
|
@@ -46,7 +48,15 @@ var Model = class _Model {
|
|
|
46
48
|
* @param variant Optional UI variant (flowchart models only).
|
|
47
49
|
*/
|
|
48
50
|
constructor(type, title, variant) {
|
|
49
|
-
this.data = {
|
|
51
|
+
this.data = {
|
|
52
|
+
type,
|
|
53
|
+
...variant ? { variant } : {},
|
|
54
|
+
title,
|
|
55
|
+
nodes: [],
|
|
56
|
+
edges: [],
|
|
57
|
+
actors: [],
|
|
58
|
+
messages: []
|
|
59
|
+
};
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
52
62
|
* Rehydrate a `Model` from a previously serialized `DiagramModel`. The
|
|
@@ -82,7 +92,8 @@ var Model = class _Model {
|
|
|
82
92
|
updateNode(id, patch) {
|
|
83
93
|
const node = this.data.nodes.find((n) => n.id === id);
|
|
84
94
|
if (!node) throw new Error(`Node "${id}" not found`);
|
|
85
|
-
|
|
95
|
+
const { __proto__, constructor, ...safe } = patch;
|
|
96
|
+
Object.assign(node, safe);
|
|
86
97
|
return this;
|
|
87
98
|
}
|
|
88
99
|
/**
|
|
@@ -122,15 +133,35 @@ var Model = class _Model {
|
|
|
122
133
|
const errors = [];
|
|
123
134
|
const nodeIds = /* @__PURE__ */ new Set();
|
|
124
135
|
for (const n of this.data.nodes) {
|
|
125
|
-
if (nodeIds.has(n.id))
|
|
136
|
+
if (nodeIds.has(n.id))
|
|
137
|
+
errors.push({
|
|
138
|
+
kind: "duplicate-node-id",
|
|
139
|
+
id: n.id,
|
|
140
|
+
message: `Duplicate node id "${n.id}"`
|
|
141
|
+
});
|
|
126
142
|
nodeIds.add(n.id);
|
|
127
143
|
}
|
|
128
144
|
const edgeIds = /* @__PURE__ */ new Set();
|
|
129
145
|
for (const e of this.data.edges) {
|
|
130
|
-
if (edgeIds.has(e.id))
|
|
146
|
+
if (edgeIds.has(e.id))
|
|
147
|
+
errors.push({
|
|
148
|
+
kind: "duplicate-edge-id",
|
|
149
|
+
id: e.id,
|
|
150
|
+
message: `Duplicate edge id "${e.id}"`
|
|
151
|
+
});
|
|
131
152
|
edgeIds.add(e.id);
|
|
132
|
-
if (!nodeIds.has(e.from))
|
|
133
|
-
|
|
153
|
+
if (!nodeIds.has(e.from))
|
|
154
|
+
errors.push({
|
|
155
|
+
kind: "dangling-from",
|
|
156
|
+
id: e.id,
|
|
157
|
+
message: `Edge "${e.id}" references unknown source node "${e.from}"`
|
|
158
|
+
});
|
|
159
|
+
if (!nodeIds.has(e.to))
|
|
160
|
+
errors.push({
|
|
161
|
+
kind: "dangling-to",
|
|
162
|
+
id: e.id,
|
|
163
|
+
message: `Edge "${e.id}" references unknown target node "${e.to}"`
|
|
164
|
+
});
|
|
134
165
|
}
|
|
135
166
|
return errors;
|
|
136
167
|
}
|
|
@@ -339,7 +370,9 @@ function computeLayout(model) {
|
|
|
339
370
|
const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
|
|
340
371
|
return { node: n, w, h };
|
|
341
372
|
});
|
|
342
|
-
const allPositioned = sized.every(
|
|
373
|
+
const allPositioned = sized.every(
|
|
374
|
+
(s) => typeof s.node.x === "number" && typeof s.node.y === "number"
|
|
375
|
+
);
|
|
343
376
|
if (allPositioned) {
|
|
344
377
|
for (const s of sized) {
|
|
345
378
|
boxes.set(s.node.id, { x: s.node.x, y: s.node.y, w: s.w, h: s.h });
|
|
@@ -389,7 +422,15 @@ function computeLayout(model) {
|
|
|
389
422
|
return boxes;
|
|
390
423
|
}
|
|
391
424
|
function escapeXML(s) {
|
|
392
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
425
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
426
|
+
}
|
|
427
|
+
function sanitizeForSVG(s) {
|
|
428
|
+
let clean = s;
|
|
429
|
+
clean = clean.replace(/<\/?[a-zA-Z][^>]*>/g, "");
|
|
430
|
+
clean = clean.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
|
|
431
|
+
clean = clean.replace(/\bon[a-z]+\s*=/gi, "");
|
|
432
|
+
clean = clean.replace(/\x00/g, "");
|
|
433
|
+
return escapeXML(clean);
|
|
393
434
|
}
|
|
394
435
|
var COLORS = {
|
|
395
436
|
bg: "#fafbfc",
|
|
@@ -408,8 +449,8 @@ function renderStandardNode(node, box) {
|
|
|
408
449
|
const cx = box.x + box.w / 2;
|
|
409
450
|
const cy = box.y + box.h / 2;
|
|
410
451
|
const shape = node.shape ?? "rectangle";
|
|
411
|
-
const label = `<text x="${cx}" y="${cy + 4.5}" text-anchor="middle" font-family="ui-sans-serif,system-ui,-apple-system,sans-serif" font-size="13" font-weight="500" fill="${COLORS.text}">${
|
|
412
|
-
let shapeEl
|
|
452
|
+
const label = `<text x="${cx}" y="${cy + 4.5}" text-anchor="middle" font-family="ui-sans-serif,system-ui,-apple-system,sans-serif" font-size="13" font-weight="500" fill="${COLORS.text}">${sanitizeForSVG(node.label)}</text>`;
|
|
453
|
+
let shapeEl;
|
|
413
454
|
if (shape === "diamond") {
|
|
414
455
|
const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
|
|
415
456
|
shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
|
|
@@ -429,17 +470,37 @@ function renderQuestionNode(node, box) {
|
|
|
429
470
|
const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
430
471
|
const x = box.x, y = box.y, w = box.w, h = box.h;
|
|
431
472
|
const parts = [];
|
|
432
|
-
parts.push(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
parts.push(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
parts.push(
|
|
439
|
-
|
|
440
|
-
|
|
473
|
+
parts.push(
|
|
474
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`
|
|
475
|
+
);
|
|
476
|
+
parts.push(
|
|
477
|
+
`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`
|
|
478
|
+
);
|
|
479
|
+
parts.push(
|
|
480
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`
|
|
481
|
+
);
|
|
482
|
+
parts.push(
|
|
483
|
+
`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`
|
|
484
|
+
);
|
|
485
|
+
parts.push(
|
|
486
|
+
`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`
|
|
487
|
+
);
|
|
488
|
+
parts.push(
|
|
489
|
+
`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`
|
|
490
|
+
);
|
|
491
|
+
parts.push(
|
|
492
|
+
`<text x="${x + 50}" y="${y + 27}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="700" fill="${COLORS.textSub}" letter-spacing="0.6">QUESTION</text>`
|
|
493
|
+
);
|
|
494
|
+
parts.push(
|
|
495
|
+
`<text x="${x + 50}" y="${y + 42}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="13" font-weight="700" fill="${COLORS.text}">${sanitizeForSVG(node.label)}</text>`
|
|
496
|
+
);
|
|
497
|
+
parts.push(
|
|
498
|
+
`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
|
|
499
|
+
);
|
|
441
500
|
if (answers.length === 0) {
|
|
442
|
-
parts.push(
|
|
501
|
+
parts.push(
|
|
502
|
+
`<text x="${x + w / 2}" y="${y + Q_BASE_H + 22}" text-anchor="middle" font-size="10" fill="${COLORS.amber}" opacity="0.4" font-weight="600">No answers yet</text>`
|
|
503
|
+
);
|
|
443
504
|
} else {
|
|
444
505
|
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
445
506
|
answers.forEach((ans, i) => {
|
|
@@ -452,10 +513,18 @@ function renderQuestionNode(node, box) {
|
|
|
452
513
|
const letter = i < 26 ? letters[i] : `${i + 1}`;
|
|
453
514
|
const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
|
|
454
515
|
const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
|
|
455
|
-
parts.push(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
parts.push(
|
|
516
|
+
parts.push(
|
|
517
|
+
`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
|
|
518
|
+
);
|
|
519
|
+
parts.push(
|
|
520
|
+
`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`
|
|
521
|
+
);
|
|
522
|
+
parts.push(
|
|
523
|
+
`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${sanitizeForSVG(letter)}</text>`
|
|
524
|
+
);
|
|
525
|
+
parts.push(
|
|
526
|
+
`<text x="${cx}" y="${cardY + 46}" text-anchor="middle" font-size="11" font-weight="500" fill="#374151" font-family="ui-sans-serif,system-ui,sans-serif">${sanitizeForSVG(displayAns)}</text>`
|
|
527
|
+
);
|
|
459
528
|
});
|
|
460
529
|
}
|
|
461
530
|
return parts.join("");
|
|
@@ -493,7 +562,7 @@ function renderEdge(edge, boxes, variant, nodes) {
|
|
|
493
562
|
const midY = (y1 + y2) / 2;
|
|
494
563
|
const labelW = estimateTextW(edge.label, 7) + 14;
|
|
495
564
|
out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
|
|
496
|
-
out += `<text x="${midX}" y="${midY + 2}" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="11" fill="${COLORS.text}">${
|
|
565
|
+
out += `<text x="${midX}" y="${midY + 2}" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="11" fill="${COLORS.text}">${sanitizeForSVG(edge.label)}</text>`;
|
|
497
566
|
}
|
|
498
567
|
return out;
|
|
499
568
|
}
|
|
@@ -519,7 +588,7 @@ function toSVG(model) {
|
|
|
519
588
|
`</marker>`,
|
|
520
589
|
`</defs>`
|
|
521
590
|
].join("");
|
|
522
|
-
const titleEl = model.title ? `<text x="${width / 2}" y="22" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="15" font-weight="700" fill="${COLORS.text}">${
|
|
591
|
+
const titleEl = model.title ? `<text x="${width / 2}" y="22" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="15" font-weight="700" fill="${COLORS.text}">${sanitizeForSVG(model.title)}</text>` : "";
|
|
523
592
|
const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
|
|
524
593
|
const nodes = model.nodes.map((n) => {
|
|
525
594
|
const b = boxes.get(n.id);
|
|
@@ -536,7 +605,9 @@ ${nodes}
|
|
|
536
605
|
}
|
|
537
606
|
async function toPNG(model) {
|
|
538
607
|
if (typeof document === "undefined") {
|
|
539
|
-
throw new Error(
|
|
608
|
+
throw new Error(
|
|
609
|
+
"toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js."
|
|
610
|
+
);
|
|
540
611
|
}
|
|
541
612
|
const svg = toSVG(model);
|
|
542
613
|
const blob = new Blob([svg], { type: "image/svg+xml" });
|
|
@@ -552,7 +623,10 @@ async function toPNG(model) {
|
|
|
552
623
|
ctx.scale(scale, scale);
|
|
553
624
|
ctx.drawImage(img, 0, 0);
|
|
554
625
|
URL.revokeObjectURL(url);
|
|
555
|
-
canvas.toBlob(
|
|
626
|
+
canvas.toBlob(
|
|
627
|
+
(b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")),
|
|
628
|
+
"image/png"
|
|
629
|
+
);
|
|
556
630
|
};
|
|
557
631
|
img.onerror = () => {
|
|
558
632
|
URL.revokeObjectURL(url);
|
|
@@ -649,7 +723,13 @@ var SequenceBuilder = class {
|
|
|
649
723
|
this.model.addActor(from);
|
|
650
724
|
this.model.addActor(to);
|
|
651
725
|
const messages = this.model.toJSON().messages ?? [];
|
|
652
|
-
this.model.addMessage({
|
|
726
|
+
this.model.addMessage({
|
|
727
|
+
id: nextId("m", messages),
|
|
728
|
+
from,
|
|
729
|
+
to,
|
|
730
|
+
label,
|
|
731
|
+
style: options.style ?? "solid"
|
|
732
|
+
});
|
|
653
733
|
return this;
|
|
654
734
|
}
|
|
655
735
|
/** Convenience for a `dashed`-style return message. */
|
|
@@ -677,6 +757,31 @@ function sequence(title) {
|
|
|
677
757
|
return new SequenceBuilder(title);
|
|
678
758
|
}
|
|
679
759
|
|
|
760
|
+
// src/core/sanitize.ts
|
|
761
|
+
var MAX_LABEL_LENGTH = 2e3;
|
|
762
|
+
var MAX_NODES = 500;
|
|
763
|
+
var MAX_EDGES = 2e3;
|
|
764
|
+
var MAX_ACTORS = 100;
|
|
765
|
+
var MAX_MESSAGES = 2e3;
|
|
766
|
+
var MAX_IMPORT_LENGTH = 2 * 1024 * 1024;
|
|
767
|
+
function sanitizeLabel(raw) {
|
|
768
|
+
let s = raw;
|
|
769
|
+
s = s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
770
|
+
s = s.replace(/<\/?[a-zA-Z][^>]*>/g, "");
|
|
771
|
+
s = s.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
|
|
772
|
+
s = s.replace(/\bon[a-z]+\s*=/gi, "");
|
|
773
|
+
if (s.length > MAX_LABEL_LENGTH) {
|
|
774
|
+
s = s.slice(0, MAX_LABEL_LENGTH);
|
|
775
|
+
}
|
|
776
|
+
return s;
|
|
777
|
+
}
|
|
778
|
+
function sanitizeURL(url) {
|
|
779
|
+
const trimmed = url.trim();
|
|
780
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#")) return trimmed;
|
|
781
|
+
if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) return trimmed;
|
|
782
|
+
return void 0;
|
|
783
|
+
}
|
|
784
|
+
|
|
680
785
|
// src/importers/mermaid.ts
|
|
681
786
|
function parseNodeDecl(raw) {
|
|
682
787
|
const patterns = [
|
|
@@ -688,7 +793,7 @@ function parseNodeDecl(raw) {
|
|
|
688
793
|
];
|
|
689
794
|
for (const [re, shape] of patterns) {
|
|
690
795
|
const m = raw.match(re);
|
|
691
|
-
if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
|
|
796
|
+
if (m) return { id: m[1], label: sanitizeLabel(m[2].replace(/^["']|["']$/g, "")), shape };
|
|
692
797
|
}
|
|
693
798
|
return null;
|
|
694
799
|
}
|
|
@@ -703,8 +808,11 @@ function parseFlowchart(lines) {
|
|
|
703
808
|
const model = new Model("flowchart");
|
|
704
809
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
705
810
|
const groupStack = [];
|
|
811
|
+
let edgeCount = 0;
|
|
706
812
|
const ensureNode = (id, group) => {
|
|
707
813
|
if (!nodeMap.has(id)) {
|
|
814
|
+
if (nodeMap.size >= MAX_NODES)
|
|
815
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
708
816
|
nodeMap.set(id, true);
|
|
709
817
|
const metadata = group ? { group } : void 0;
|
|
710
818
|
model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
|
|
@@ -713,7 +821,8 @@ function parseFlowchart(lines) {
|
|
|
713
821
|
for (const line of lines) {
|
|
714
822
|
const trimmed = line.trim();
|
|
715
823
|
if (!trimmed) continue;
|
|
716
|
-
if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
|
|
824
|
+
if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
|
|
825
|
+
continue;
|
|
717
826
|
const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
|
|
718
827
|
if (subgraphOpen) {
|
|
719
828
|
groupStack.push(subgraphOpen[1]);
|
|
@@ -729,12 +838,15 @@ function parseFlowchart(lines) {
|
|
|
729
838
|
const fromRaw = edgeMatch[1].trim();
|
|
730
839
|
const connector = edgeMatch[2];
|
|
731
840
|
const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
|
|
841
|
+
const sanitizedLabel = label ? sanitizeLabel(label) : void 0;
|
|
732
842
|
const toRaw = edgeMatch[4].trim();
|
|
733
843
|
const style = detectStyle(connector);
|
|
734
844
|
const arrowhead = detectArrowhead(connector);
|
|
735
845
|
const fromNode = parseNodeDecl(fromRaw);
|
|
736
846
|
const toNode = parseNodeDecl(toRaw);
|
|
737
847
|
if (fromNode && !nodeMap.has(fromNode.id)) {
|
|
848
|
+
if (nodeMap.size >= MAX_NODES)
|
|
849
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
738
850
|
nodeMap.set(fromNode.id, true);
|
|
739
851
|
const metadata = currentGroup ? { group: currentGroup } : void 0;
|
|
740
852
|
model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
|
|
@@ -742,19 +854,24 @@ function parseFlowchart(lines) {
|
|
|
742
854
|
ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
|
|
743
855
|
}
|
|
744
856
|
if (toNode && !nodeMap.has(toNode.id)) {
|
|
857
|
+
if (nodeMap.size >= MAX_NODES)
|
|
858
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
745
859
|
nodeMap.set(toNode.id, true);
|
|
746
860
|
const metadata = currentGroup ? { group: currentGroup } : void 0;
|
|
747
861
|
model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
|
|
748
862
|
} else if (!toNode) {
|
|
749
863
|
ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
|
|
750
864
|
}
|
|
865
|
+
if (edgeCount >= MAX_EDGES)
|
|
866
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_EDGES} edges`);
|
|
867
|
+
edgeCount++;
|
|
751
868
|
const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
|
|
752
869
|
const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
|
|
753
870
|
model.addEdge({
|
|
754
871
|
id: nextId("e", model.toJSON().edges),
|
|
755
872
|
from: fromId,
|
|
756
873
|
to: toId,
|
|
757
|
-
...
|
|
874
|
+
...sanitizedLabel ? { label: sanitizedLabel } : {},
|
|
758
875
|
style,
|
|
759
876
|
...arrowhead === "none" ? { arrowhead } : {}
|
|
760
877
|
});
|
|
@@ -762,6 +879,8 @@ function parseFlowchart(lines) {
|
|
|
762
879
|
}
|
|
763
880
|
const nodeDecl = parseNodeDecl(trimmed);
|
|
764
881
|
if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
|
|
882
|
+
if (nodeMap.size >= MAX_NODES)
|
|
883
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
765
884
|
nodeMap.set(nodeDecl.id, true);
|
|
766
885
|
const metadata = currentGroup ? { group: currentGroup } : void 0;
|
|
767
886
|
model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
|
|
@@ -771,34 +890,56 @@ function parseFlowchart(lines) {
|
|
|
771
890
|
}
|
|
772
891
|
function parseSequence(lines, title) {
|
|
773
892
|
const model = new Model("sequence", title);
|
|
893
|
+
let actorCount = 0;
|
|
894
|
+
let messageCount = 0;
|
|
895
|
+
const safeAddActor = (name) => {
|
|
896
|
+
const safeName = sanitizeLabel(name);
|
|
897
|
+
if (!model.toJSON().actors?.includes(safeName)) {
|
|
898
|
+
if (actorCount >= MAX_ACTORS)
|
|
899
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_ACTORS} actors`);
|
|
900
|
+
actorCount++;
|
|
901
|
+
}
|
|
902
|
+
model.addActor(safeName);
|
|
903
|
+
return safeName;
|
|
904
|
+
};
|
|
774
905
|
for (const line of lines) {
|
|
775
906
|
const trimmed = line.trim();
|
|
776
907
|
if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
|
|
777
908
|
const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
|
|
778
909
|
if (participantMatch) {
|
|
779
|
-
|
|
910
|
+
safeAddActor(participantMatch[1].trim());
|
|
780
911
|
continue;
|
|
781
912
|
}
|
|
782
913
|
const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
|
|
783
914
|
if (actorMatch) {
|
|
784
|
-
|
|
915
|
+
safeAddActor(actorMatch[1].trim());
|
|
785
916
|
continue;
|
|
786
917
|
}
|
|
787
918
|
const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
|
|
788
919
|
if (msgMatch) {
|
|
789
|
-
const from = msgMatch[1].trim();
|
|
920
|
+
const from = safeAddActor(msgMatch[1].trim());
|
|
790
921
|
const arrow = msgMatch[2];
|
|
791
|
-
const to = msgMatch[3].trim();
|
|
792
|
-
const label = msgMatch[4].trim();
|
|
793
|
-
|
|
794
|
-
|
|
922
|
+
const to = safeAddActor(msgMatch[3].trim());
|
|
923
|
+
const label = sanitizeLabel(msgMatch[4].trim());
|
|
924
|
+
if (messageCount >= MAX_MESSAGES)
|
|
925
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_MESSAGES} messages`);
|
|
926
|
+
messageCount++;
|
|
795
927
|
const messages = model.toJSON().messages ?? [];
|
|
796
|
-
model.addMessage({
|
|
928
|
+
model.addMessage({
|
|
929
|
+
id: nextId("m", messages),
|
|
930
|
+
from,
|
|
931
|
+
to,
|
|
932
|
+
label,
|
|
933
|
+
style: arrow.startsWith("--") ? "dashed" : "solid"
|
|
934
|
+
});
|
|
797
935
|
}
|
|
798
936
|
}
|
|
799
937
|
return model;
|
|
800
938
|
}
|
|
801
939
|
function fromMermaid(mermaid) {
|
|
940
|
+
if (mermaid.length > MAX_IMPORT_LENGTH) {
|
|
941
|
+
throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
|
|
942
|
+
}
|
|
802
943
|
const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
|
|
803
944
|
const rawLines = cleaned.split("\n");
|
|
804
945
|
let startIdx = 0;
|
|
@@ -830,10 +971,74 @@ function fromMermaid(mermaid) {
|
|
|
830
971
|
}
|
|
831
972
|
|
|
832
973
|
// src/importers/json.ts
|
|
974
|
+
function stripDangerousKeys(obj) {
|
|
975
|
+
if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
|
|
976
|
+
if (obj !== null && typeof obj === "object") {
|
|
977
|
+
const clean = {};
|
|
978
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
979
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
980
|
+
clean[key] = stripDangerousKeys(val);
|
|
981
|
+
}
|
|
982
|
+
return clean;
|
|
983
|
+
}
|
|
984
|
+
return obj;
|
|
985
|
+
}
|
|
833
986
|
function fromJSON(json) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
987
|
+
if (typeof json === "string" && json.length > MAX_IMPORT_LENGTH) {
|
|
988
|
+
throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
|
|
989
|
+
}
|
|
990
|
+
const raw = typeof json === "string" ? JSON.parse(json) : json;
|
|
991
|
+
const data = stripDangerousKeys(raw);
|
|
992
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
993
|
+
throw new Error("Invalid DiagramModel JSON: expected an object");
|
|
994
|
+
}
|
|
995
|
+
if (data.type !== "flowchart" && data.type !== "sequence") {
|
|
996
|
+
throw new Error(`Invalid DiagramModel JSON: unknown type "${data.type}"`);
|
|
997
|
+
}
|
|
998
|
+
if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
|
|
999
|
+
throw new Error("Invalid DiagramModel JSON: nodes and edges must be arrays");
|
|
1000
|
+
}
|
|
1001
|
+
if (data.nodes.length > MAX_NODES) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`Import aborted: diagram has ${data.nodes.length} nodes, maximum is ${MAX_NODES}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (data.edges.length > MAX_EDGES) {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
`Import aborted: diagram has ${data.edges.length} edges, maximum is ${MAX_EDGES}`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
if (data.actors && data.actors.length > MAX_ACTORS) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Import aborted: diagram has ${data.actors.length} actors, maximum is ${MAX_ACTORS}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (data.messages && data.messages.length > MAX_MESSAGES) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
`Import aborted: diagram has ${data.messages.length} messages, maximum is ${MAX_MESSAGES}`
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
for (const node of data.nodes) {
|
|
1022
|
+
if (typeof node !== "object" || node === null || typeof node.id !== "string" || typeof node.label !== "string") {
|
|
1023
|
+
throw new Error("Invalid DiagramModel JSON: each node must have string id and label");
|
|
1024
|
+
}
|
|
1025
|
+
node.label = sanitizeLabel(node.label);
|
|
1026
|
+
}
|
|
1027
|
+
for (const edge of data.edges) {
|
|
1028
|
+
if (typeof edge !== "object" || edge === null || typeof edge.id !== "string" || typeof edge.from !== "string" || typeof edge.to !== "string") {
|
|
1029
|
+
throw new Error("Invalid DiagramModel JSON: each edge must have string id, from, and to");
|
|
1030
|
+
}
|
|
1031
|
+
if (edge.label) edge.label = sanitizeLabel(edge.label);
|
|
1032
|
+
}
|
|
1033
|
+
if (data.actors) {
|
|
1034
|
+
data.actors = data.actors.map((a) => typeof a === "string" ? sanitizeLabel(a) : a);
|
|
1035
|
+
}
|
|
1036
|
+
if (data.messages) {
|
|
1037
|
+
for (const msg of data.messages) {
|
|
1038
|
+
if (typeof msg === "object" && msg !== null && typeof msg.label === "string") {
|
|
1039
|
+
msg.label = sanitizeLabel(msg.label);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
837
1042
|
}
|
|
838
1043
|
return Model.fromData(data);
|
|
839
1044
|
}
|
|
@@ -845,6 +1050,8 @@ function fromJSON(json) {
|
|
|
845
1050
|
flowchart,
|
|
846
1051
|
fromJSON,
|
|
847
1052
|
fromMermaid,
|
|
1053
|
+
sanitizeLabel,
|
|
1054
|
+
sanitizeURL,
|
|
848
1055
|
sequence,
|
|
849
1056
|
toJSON,
|
|
850
1057
|
toMermaid,
|