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.js
CHANGED
|
@@ -9,7 +9,15 @@ var Model = class _Model {
|
|
|
9
9
|
* @param variant Optional UI variant (flowchart models only).
|
|
10
10
|
*/
|
|
11
11
|
constructor(type, title, variant) {
|
|
12
|
-
this.data = {
|
|
12
|
+
this.data = {
|
|
13
|
+
type,
|
|
14
|
+
...variant ? { variant } : {},
|
|
15
|
+
title,
|
|
16
|
+
nodes: [],
|
|
17
|
+
edges: [],
|
|
18
|
+
actors: [],
|
|
19
|
+
messages: []
|
|
20
|
+
};
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
23
|
* Rehydrate a `Model` from a previously serialized `DiagramModel`. The
|
|
@@ -45,7 +53,8 @@ var Model = class _Model {
|
|
|
45
53
|
updateNode(id, patch) {
|
|
46
54
|
const node = this.data.nodes.find((n) => n.id === id);
|
|
47
55
|
if (!node) throw new Error(`Node "${id}" not found`);
|
|
48
|
-
|
|
56
|
+
const { __proto__, constructor, ...safe } = patch;
|
|
57
|
+
Object.assign(node, safe);
|
|
49
58
|
return this;
|
|
50
59
|
}
|
|
51
60
|
/**
|
|
@@ -85,15 +94,35 @@ var Model = class _Model {
|
|
|
85
94
|
const errors = [];
|
|
86
95
|
const nodeIds = /* @__PURE__ */ new Set();
|
|
87
96
|
for (const n of this.data.nodes) {
|
|
88
|
-
if (nodeIds.has(n.id))
|
|
97
|
+
if (nodeIds.has(n.id))
|
|
98
|
+
errors.push({
|
|
99
|
+
kind: "duplicate-node-id",
|
|
100
|
+
id: n.id,
|
|
101
|
+
message: `Duplicate node id "${n.id}"`
|
|
102
|
+
});
|
|
89
103
|
nodeIds.add(n.id);
|
|
90
104
|
}
|
|
91
105
|
const edgeIds = /* @__PURE__ */ new Set();
|
|
92
106
|
for (const e of this.data.edges) {
|
|
93
|
-
if (edgeIds.has(e.id))
|
|
107
|
+
if (edgeIds.has(e.id))
|
|
108
|
+
errors.push({
|
|
109
|
+
kind: "duplicate-edge-id",
|
|
110
|
+
id: e.id,
|
|
111
|
+
message: `Duplicate edge id "${e.id}"`
|
|
112
|
+
});
|
|
94
113
|
edgeIds.add(e.id);
|
|
95
|
-
if (!nodeIds.has(e.from))
|
|
96
|
-
|
|
114
|
+
if (!nodeIds.has(e.from))
|
|
115
|
+
errors.push({
|
|
116
|
+
kind: "dangling-from",
|
|
117
|
+
id: e.id,
|
|
118
|
+
message: `Edge "${e.id}" references unknown source node "${e.from}"`
|
|
119
|
+
});
|
|
120
|
+
if (!nodeIds.has(e.to))
|
|
121
|
+
errors.push({
|
|
122
|
+
kind: "dangling-to",
|
|
123
|
+
id: e.id,
|
|
124
|
+
message: `Edge "${e.id}" references unknown target node "${e.to}"`
|
|
125
|
+
});
|
|
97
126
|
}
|
|
98
127
|
return errors;
|
|
99
128
|
}
|
|
@@ -302,7 +331,9 @@ function computeLayout(model) {
|
|
|
302
331
|
const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
|
|
303
332
|
return { node: n, w, h };
|
|
304
333
|
});
|
|
305
|
-
const allPositioned = sized.every(
|
|
334
|
+
const allPositioned = sized.every(
|
|
335
|
+
(s) => typeof s.node.x === "number" && typeof s.node.y === "number"
|
|
336
|
+
);
|
|
306
337
|
if (allPositioned) {
|
|
307
338
|
for (const s of sized) {
|
|
308
339
|
boxes.set(s.node.id, { x: s.node.x, y: s.node.y, w: s.w, h: s.h });
|
|
@@ -352,7 +383,15 @@ function computeLayout(model) {
|
|
|
352
383
|
return boxes;
|
|
353
384
|
}
|
|
354
385
|
function escapeXML(s) {
|
|
355
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
386
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
387
|
+
}
|
|
388
|
+
function sanitizeForSVG(s) {
|
|
389
|
+
let clean = s;
|
|
390
|
+
clean = clean.replace(/<\/?[a-zA-Z][^>]*>/g, "");
|
|
391
|
+
clean = clean.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
|
|
392
|
+
clean = clean.replace(/\bon[a-z]+\s*=/gi, "");
|
|
393
|
+
clean = clean.replace(/\x00/g, "");
|
|
394
|
+
return escapeXML(clean);
|
|
356
395
|
}
|
|
357
396
|
var COLORS = {
|
|
358
397
|
bg: "#fafbfc",
|
|
@@ -371,8 +410,8 @@ function renderStandardNode(node, box) {
|
|
|
371
410
|
const cx = box.x + box.w / 2;
|
|
372
411
|
const cy = box.y + box.h / 2;
|
|
373
412
|
const shape = node.shape ?? "rectangle";
|
|
374
|
-
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}">${
|
|
375
|
-
let shapeEl
|
|
413
|
+
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>`;
|
|
414
|
+
let shapeEl;
|
|
376
415
|
if (shape === "diamond") {
|
|
377
416
|
const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
|
|
378
417
|
shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
|
|
@@ -392,17 +431,37 @@ function renderQuestionNode(node, box) {
|
|
|
392
431
|
const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
393
432
|
const x = box.x, y = box.y, w = box.w, h = box.h;
|
|
394
433
|
const parts = [];
|
|
395
|
-
parts.push(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
parts.push(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
parts.push(
|
|
402
|
-
|
|
403
|
-
|
|
434
|
+
parts.push(
|
|
435
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`
|
|
436
|
+
);
|
|
437
|
+
parts.push(
|
|
438
|
+
`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`
|
|
439
|
+
);
|
|
440
|
+
parts.push(
|
|
441
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`
|
|
442
|
+
);
|
|
443
|
+
parts.push(
|
|
444
|
+
`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`
|
|
445
|
+
);
|
|
446
|
+
parts.push(
|
|
447
|
+
`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`
|
|
448
|
+
);
|
|
449
|
+
parts.push(
|
|
450
|
+
`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`
|
|
451
|
+
);
|
|
452
|
+
parts.push(
|
|
453
|
+
`<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>`
|
|
454
|
+
);
|
|
455
|
+
parts.push(
|
|
456
|
+
`<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>`
|
|
457
|
+
);
|
|
458
|
+
parts.push(
|
|
459
|
+
`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
|
|
460
|
+
);
|
|
404
461
|
if (answers.length === 0) {
|
|
405
|
-
parts.push(
|
|
462
|
+
parts.push(
|
|
463
|
+
`<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>`
|
|
464
|
+
);
|
|
406
465
|
} else {
|
|
407
466
|
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
408
467
|
answers.forEach((ans, i) => {
|
|
@@ -415,10 +474,18 @@ function renderQuestionNode(node, box) {
|
|
|
415
474
|
const letter = i < 26 ? letters[i] : `${i + 1}`;
|
|
416
475
|
const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
|
|
417
476
|
const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
|
|
418
|
-
parts.push(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
parts.push(
|
|
477
|
+
parts.push(
|
|
478
|
+
`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
|
|
479
|
+
);
|
|
480
|
+
parts.push(
|
|
481
|
+
`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`
|
|
482
|
+
);
|
|
483
|
+
parts.push(
|
|
484
|
+
`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${sanitizeForSVG(letter)}</text>`
|
|
485
|
+
);
|
|
486
|
+
parts.push(
|
|
487
|
+
`<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>`
|
|
488
|
+
);
|
|
422
489
|
});
|
|
423
490
|
}
|
|
424
491
|
return parts.join("");
|
|
@@ -456,7 +523,7 @@ function renderEdge(edge, boxes, variant, nodes) {
|
|
|
456
523
|
const midY = (y1 + y2) / 2;
|
|
457
524
|
const labelW = estimateTextW(edge.label, 7) + 14;
|
|
458
525
|
out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
|
|
459
|
-
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}">${
|
|
526
|
+
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>`;
|
|
460
527
|
}
|
|
461
528
|
return out;
|
|
462
529
|
}
|
|
@@ -482,7 +549,7 @@ function toSVG(model) {
|
|
|
482
549
|
`</marker>`,
|
|
483
550
|
`</defs>`
|
|
484
551
|
].join("");
|
|
485
|
-
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}">${
|
|
552
|
+
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>` : "";
|
|
486
553
|
const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
|
|
487
554
|
const nodes = model.nodes.map((n) => {
|
|
488
555
|
const b = boxes.get(n.id);
|
|
@@ -499,7 +566,9 @@ ${nodes}
|
|
|
499
566
|
}
|
|
500
567
|
async function toPNG(model) {
|
|
501
568
|
if (typeof document === "undefined") {
|
|
502
|
-
throw new Error(
|
|
569
|
+
throw new Error(
|
|
570
|
+
"toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js."
|
|
571
|
+
);
|
|
503
572
|
}
|
|
504
573
|
const svg = toSVG(model);
|
|
505
574
|
const blob = new Blob([svg], { type: "image/svg+xml" });
|
|
@@ -515,7 +584,10 @@ async function toPNG(model) {
|
|
|
515
584
|
ctx.scale(scale, scale);
|
|
516
585
|
ctx.drawImage(img, 0, 0);
|
|
517
586
|
URL.revokeObjectURL(url);
|
|
518
|
-
canvas.toBlob(
|
|
587
|
+
canvas.toBlob(
|
|
588
|
+
(b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")),
|
|
589
|
+
"image/png"
|
|
590
|
+
);
|
|
519
591
|
};
|
|
520
592
|
img.onerror = () => {
|
|
521
593
|
URL.revokeObjectURL(url);
|
|
@@ -612,7 +684,13 @@ var SequenceBuilder = class {
|
|
|
612
684
|
this.model.addActor(from);
|
|
613
685
|
this.model.addActor(to);
|
|
614
686
|
const messages = this.model.toJSON().messages ?? [];
|
|
615
|
-
this.model.addMessage({
|
|
687
|
+
this.model.addMessage({
|
|
688
|
+
id: nextId("m", messages),
|
|
689
|
+
from,
|
|
690
|
+
to,
|
|
691
|
+
label,
|
|
692
|
+
style: options.style ?? "solid"
|
|
693
|
+
});
|
|
616
694
|
return this;
|
|
617
695
|
}
|
|
618
696
|
/** Convenience for a `dashed`-style return message. */
|
|
@@ -640,6 +718,31 @@ function sequence(title) {
|
|
|
640
718
|
return new SequenceBuilder(title);
|
|
641
719
|
}
|
|
642
720
|
|
|
721
|
+
// src/core/sanitize.ts
|
|
722
|
+
var MAX_LABEL_LENGTH = 2e3;
|
|
723
|
+
var MAX_NODES = 500;
|
|
724
|
+
var MAX_EDGES = 2e3;
|
|
725
|
+
var MAX_ACTORS = 100;
|
|
726
|
+
var MAX_MESSAGES = 2e3;
|
|
727
|
+
var MAX_IMPORT_LENGTH = 2 * 1024 * 1024;
|
|
728
|
+
function sanitizeLabel(raw) {
|
|
729
|
+
let s = raw;
|
|
730
|
+
s = s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
731
|
+
s = s.replace(/<\/?[a-zA-Z][^>]*>/g, "");
|
|
732
|
+
s = s.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
|
|
733
|
+
s = s.replace(/\bon[a-z]+\s*=/gi, "");
|
|
734
|
+
if (s.length > MAX_LABEL_LENGTH) {
|
|
735
|
+
s = s.slice(0, MAX_LABEL_LENGTH);
|
|
736
|
+
}
|
|
737
|
+
return s;
|
|
738
|
+
}
|
|
739
|
+
function sanitizeURL(url) {
|
|
740
|
+
const trimmed = url.trim();
|
|
741
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#")) return trimmed;
|
|
742
|
+
if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) return trimmed;
|
|
743
|
+
return void 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
643
746
|
// src/importers/mermaid.ts
|
|
644
747
|
function parseNodeDecl(raw) {
|
|
645
748
|
const patterns = [
|
|
@@ -651,7 +754,7 @@ function parseNodeDecl(raw) {
|
|
|
651
754
|
];
|
|
652
755
|
for (const [re, shape] of patterns) {
|
|
653
756
|
const m = raw.match(re);
|
|
654
|
-
if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
|
|
757
|
+
if (m) return { id: m[1], label: sanitizeLabel(m[2].replace(/^["']|["']$/g, "")), shape };
|
|
655
758
|
}
|
|
656
759
|
return null;
|
|
657
760
|
}
|
|
@@ -666,8 +769,11 @@ function parseFlowchart(lines) {
|
|
|
666
769
|
const model = new Model("flowchart");
|
|
667
770
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
668
771
|
const groupStack = [];
|
|
772
|
+
let edgeCount = 0;
|
|
669
773
|
const ensureNode = (id, group) => {
|
|
670
774
|
if (!nodeMap.has(id)) {
|
|
775
|
+
if (nodeMap.size >= MAX_NODES)
|
|
776
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
671
777
|
nodeMap.set(id, true);
|
|
672
778
|
const metadata = group ? { group } : void 0;
|
|
673
779
|
model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
|
|
@@ -676,7 +782,8 @@ function parseFlowchart(lines) {
|
|
|
676
782
|
for (const line of lines) {
|
|
677
783
|
const trimmed = line.trim();
|
|
678
784
|
if (!trimmed) continue;
|
|
679
|
-
if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
|
|
785
|
+
if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
|
|
786
|
+
continue;
|
|
680
787
|
const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
|
|
681
788
|
if (subgraphOpen) {
|
|
682
789
|
groupStack.push(subgraphOpen[1]);
|
|
@@ -692,12 +799,15 @@ function parseFlowchart(lines) {
|
|
|
692
799
|
const fromRaw = edgeMatch[1].trim();
|
|
693
800
|
const connector = edgeMatch[2];
|
|
694
801
|
const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
|
|
802
|
+
const sanitizedLabel = label ? sanitizeLabel(label) : void 0;
|
|
695
803
|
const toRaw = edgeMatch[4].trim();
|
|
696
804
|
const style = detectStyle(connector);
|
|
697
805
|
const arrowhead = detectArrowhead(connector);
|
|
698
806
|
const fromNode = parseNodeDecl(fromRaw);
|
|
699
807
|
const toNode = parseNodeDecl(toRaw);
|
|
700
808
|
if (fromNode && !nodeMap.has(fromNode.id)) {
|
|
809
|
+
if (nodeMap.size >= MAX_NODES)
|
|
810
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
701
811
|
nodeMap.set(fromNode.id, true);
|
|
702
812
|
const metadata = currentGroup ? { group: currentGroup } : void 0;
|
|
703
813
|
model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
|
|
@@ -705,19 +815,24 @@ function parseFlowchart(lines) {
|
|
|
705
815
|
ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
|
|
706
816
|
}
|
|
707
817
|
if (toNode && !nodeMap.has(toNode.id)) {
|
|
818
|
+
if (nodeMap.size >= MAX_NODES)
|
|
819
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
708
820
|
nodeMap.set(toNode.id, true);
|
|
709
821
|
const metadata = currentGroup ? { group: currentGroup } : void 0;
|
|
710
822
|
model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
|
|
711
823
|
} else if (!toNode) {
|
|
712
824
|
ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
|
|
713
825
|
}
|
|
826
|
+
if (edgeCount >= MAX_EDGES)
|
|
827
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_EDGES} edges`);
|
|
828
|
+
edgeCount++;
|
|
714
829
|
const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
|
|
715
830
|
const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
|
|
716
831
|
model.addEdge({
|
|
717
832
|
id: nextId("e", model.toJSON().edges),
|
|
718
833
|
from: fromId,
|
|
719
834
|
to: toId,
|
|
720
|
-
...
|
|
835
|
+
...sanitizedLabel ? { label: sanitizedLabel } : {},
|
|
721
836
|
style,
|
|
722
837
|
...arrowhead === "none" ? { arrowhead } : {}
|
|
723
838
|
});
|
|
@@ -725,6 +840,8 @@ function parseFlowchart(lines) {
|
|
|
725
840
|
}
|
|
726
841
|
const nodeDecl = parseNodeDecl(trimmed);
|
|
727
842
|
if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
|
|
843
|
+
if (nodeMap.size >= MAX_NODES)
|
|
844
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
|
|
728
845
|
nodeMap.set(nodeDecl.id, true);
|
|
729
846
|
const metadata = currentGroup ? { group: currentGroup } : void 0;
|
|
730
847
|
model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
|
|
@@ -734,34 +851,56 @@ function parseFlowchart(lines) {
|
|
|
734
851
|
}
|
|
735
852
|
function parseSequence(lines, title) {
|
|
736
853
|
const model = new Model("sequence", title);
|
|
854
|
+
let actorCount = 0;
|
|
855
|
+
let messageCount = 0;
|
|
856
|
+
const safeAddActor = (name) => {
|
|
857
|
+
const safeName = sanitizeLabel(name);
|
|
858
|
+
if (!model.toJSON().actors?.includes(safeName)) {
|
|
859
|
+
if (actorCount >= MAX_ACTORS)
|
|
860
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_ACTORS} actors`);
|
|
861
|
+
actorCount++;
|
|
862
|
+
}
|
|
863
|
+
model.addActor(safeName);
|
|
864
|
+
return safeName;
|
|
865
|
+
};
|
|
737
866
|
for (const line of lines) {
|
|
738
867
|
const trimmed = line.trim();
|
|
739
868
|
if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
|
|
740
869
|
const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
|
|
741
870
|
if (participantMatch) {
|
|
742
|
-
|
|
871
|
+
safeAddActor(participantMatch[1].trim());
|
|
743
872
|
continue;
|
|
744
873
|
}
|
|
745
874
|
const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
|
|
746
875
|
if (actorMatch) {
|
|
747
|
-
|
|
876
|
+
safeAddActor(actorMatch[1].trim());
|
|
748
877
|
continue;
|
|
749
878
|
}
|
|
750
879
|
const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
|
|
751
880
|
if (msgMatch) {
|
|
752
|
-
const from = msgMatch[1].trim();
|
|
881
|
+
const from = safeAddActor(msgMatch[1].trim());
|
|
753
882
|
const arrow = msgMatch[2];
|
|
754
|
-
const to = msgMatch[3].trim();
|
|
755
|
-
const label = msgMatch[4].trim();
|
|
756
|
-
|
|
757
|
-
|
|
883
|
+
const to = safeAddActor(msgMatch[3].trim());
|
|
884
|
+
const label = sanitizeLabel(msgMatch[4].trim());
|
|
885
|
+
if (messageCount >= MAX_MESSAGES)
|
|
886
|
+
throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_MESSAGES} messages`);
|
|
887
|
+
messageCount++;
|
|
758
888
|
const messages = model.toJSON().messages ?? [];
|
|
759
|
-
model.addMessage({
|
|
889
|
+
model.addMessage({
|
|
890
|
+
id: nextId("m", messages),
|
|
891
|
+
from,
|
|
892
|
+
to,
|
|
893
|
+
label,
|
|
894
|
+
style: arrow.startsWith("--") ? "dashed" : "solid"
|
|
895
|
+
});
|
|
760
896
|
}
|
|
761
897
|
}
|
|
762
898
|
return model;
|
|
763
899
|
}
|
|
764
900
|
function fromMermaid(mermaid) {
|
|
901
|
+
if (mermaid.length > MAX_IMPORT_LENGTH) {
|
|
902
|
+
throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
|
|
903
|
+
}
|
|
765
904
|
const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
|
|
766
905
|
const rawLines = cleaned.split("\n");
|
|
767
906
|
let startIdx = 0;
|
|
@@ -793,10 +932,74 @@ function fromMermaid(mermaid) {
|
|
|
793
932
|
}
|
|
794
933
|
|
|
795
934
|
// src/importers/json.ts
|
|
935
|
+
function stripDangerousKeys(obj) {
|
|
936
|
+
if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
|
|
937
|
+
if (obj !== null && typeof obj === "object") {
|
|
938
|
+
const clean = {};
|
|
939
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
940
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
941
|
+
clean[key] = stripDangerousKeys(val);
|
|
942
|
+
}
|
|
943
|
+
return clean;
|
|
944
|
+
}
|
|
945
|
+
return obj;
|
|
946
|
+
}
|
|
796
947
|
function fromJSON(json) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
948
|
+
if (typeof json === "string" && json.length > MAX_IMPORT_LENGTH) {
|
|
949
|
+
throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
|
|
950
|
+
}
|
|
951
|
+
const raw = typeof json === "string" ? JSON.parse(json) : json;
|
|
952
|
+
const data = stripDangerousKeys(raw);
|
|
953
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
954
|
+
throw new Error("Invalid DiagramModel JSON: expected an object");
|
|
955
|
+
}
|
|
956
|
+
if (data.type !== "flowchart" && data.type !== "sequence") {
|
|
957
|
+
throw new Error(`Invalid DiagramModel JSON: unknown type "${data.type}"`);
|
|
958
|
+
}
|
|
959
|
+
if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
|
|
960
|
+
throw new Error("Invalid DiagramModel JSON: nodes and edges must be arrays");
|
|
961
|
+
}
|
|
962
|
+
if (data.nodes.length > MAX_NODES) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
`Import aborted: diagram has ${data.nodes.length} nodes, maximum is ${MAX_NODES}`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (data.edges.length > MAX_EDGES) {
|
|
968
|
+
throw new Error(
|
|
969
|
+
`Import aborted: diagram has ${data.edges.length} edges, maximum is ${MAX_EDGES}`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
if (data.actors && data.actors.length > MAX_ACTORS) {
|
|
973
|
+
throw new Error(
|
|
974
|
+
`Import aborted: diagram has ${data.actors.length} actors, maximum is ${MAX_ACTORS}`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
if (data.messages && data.messages.length > MAX_MESSAGES) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Import aborted: diagram has ${data.messages.length} messages, maximum is ${MAX_MESSAGES}`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
for (const node of data.nodes) {
|
|
983
|
+
if (typeof node !== "object" || node === null || typeof node.id !== "string" || typeof node.label !== "string") {
|
|
984
|
+
throw new Error("Invalid DiagramModel JSON: each node must have string id and label");
|
|
985
|
+
}
|
|
986
|
+
node.label = sanitizeLabel(node.label);
|
|
987
|
+
}
|
|
988
|
+
for (const edge of data.edges) {
|
|
989
|
+
if (typeof edge !== "object" || edge === null || typeof edge.id !== "string" || typeof edge.from !== "string" || typeof edge.to !== "string") {
|
|
990
|
+
throw new Error("Invalid DiagramModel JSON: each edge must have string id, from, and to");
|
|
991
|
+
}
|
|
992
|
+
if (edge.label) edge.label = sanitizeLabel(edge.label);
|
|
993
|
+
}
|
|
994
|
+
if (data.actors) {
|
|
995
|
+
data.actors = data.actors.map((a) => typeof a === "string" ? sanitizeLabel(a) : a);
|
|
996
|
+
}
|
|
997
|
+
if (data.messages) {
|
|
998
|
+
for (const msg of data.messages) {
|
|
999
|
+
if (typeof msg === "object" && msg !== null && typeof msg.label === "string") {
|
|
1000
|
+
msg.label = sanitizeLabel(msg.label);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
800
1003
|
}
|
|
801
1004
|
return Model.fromData(data);
|
|
802
1005
|
}
|
|
@@ -807,6 +1010,8 @@ export {
|
|
|
807
1010
|
flowchart,
|
|
808
1011
|
fromJSON,
|
|
809
1012
|
fromMermaid,
|
|
1013
|
+
sanitizeLabel,
|
|
1014
|
+
sanitizeURL,
|
|
810
1015
|
sequence,
|
|
811
1016
|
toJSON,
|
|
812
1017
|
toMermaid,
|