flowchart-sequence-designer 1.0.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.js ADDED
@@ -0,0 +1,817 @@
1
+ // src/core/model.ts
2
+ var Model = class _Model {
3
+ data;
4
+ /**
5
+ * Create an empty model.
6
+ *
7
+ * @param type Top-level kind — `flowchart` or `sequence`.
8
+ * @param title Optional human-readable title.
9
+ * @param variant Optional UI variant (flowchart models only).
10
+ */
11
+ constructor(type, title, variant) {
12
+ this.data = { type, ...variant ? { variant } : {}, title, nodes: [], edges: [], actors: [], messages: [] };
13
+ }
14
+ /**
15
+ * Rehydrate a `Model` from a previously serialized `DiagramModel`. The
16
+ * incoming data is deep-cloned, so future mutations on the returned `Model`
17
+ * do not affect the caller's object.
18
+ */
19
+ static fromData(data) {
20
+ const m = new _Model(data.type, data.title, data.variant);
21
+ m.data = structuredClone(data);
22
+ return m;
23
+ }
24
+ /** Set the UI variant. No-op semantics for sequence models. */
25
+ setVariant(variant) {
26
+ this.data.variant = variant;
27
+ return this;
28
+ }
29
+ /**
30
+ * Append a node. Throws if a node with the same id already exists. The
31
+ * input is shallow-cloned, so later mutations of the caller's object do
32
+ * not leak in.
33
+ */
34
+ addNode(node) {
35
+ if (this.data.nodes.find((n) => n.id === node.id)) {
36
+ throw new Error(`Node with id "${node.id}" already exists`);
37
+ }
38
+ this.data.nodes.push({ ...node });
39
+ return this;
40
+ }
41
+ /**
42
+ * Patch an existing node in place. Throws if the id is not found. The id
43
+ * field itself cannot be patched — to rename, remove + re-add.
44
+ */
45
+ updateNode(id, patch) {
46
+ const node = this.data.nodes.find((n) => n.id === id);
47
+ if (!node) throw new Error(`Node "${id}" not found`);
48
+ Object.assign(node, patch);
49
+ return this;
50
+ }
51
+ /**
52
+ * Remove a node and every edge that referenced it as `from` or `to`. Safe
53
+ * to call on a missing id (no-op).
54
+ */
55
+ removeNode(id) {
56
+ this.data.nodes = this.data.nodes.filter((n) => n.id !== id);
57
+ this.data.edges = this.data.edges.filter((e) => e.from !== id && e.to !== id);
58
+ return this;
59
+ }
60
+ /**
61
+ * Append an edge. Throws on duplicate id or if either endpoint references
62
+ * an unknown node — the model never holds dangling edges from this entry
63
+ * point. (Importers can still construct dangling edges; call `validate()`
64
+ * to detect them.)
65
+ */
66
+ addEdge(edge) {
67
+ if (this.data.edges.find((e) => e.id === edge.id)) {
68
+ throw new Error(`Edge with id "${edge.id}" already exists`);
69
+ }
70
+ if (!this.data.nodes.find((n) => n.id === edge.from)) {
71
+ throw new Error(`Edge "${edge.id}" references unknown source node "${edge.from}"`);
72
+ }
73
+ if (!this.data.nodes.find((n) => n.id === edge.to)) {
74
+ throw new Error(`Edge "${edge.id}" references unknown target node "${edge.to}"`);
75
+ }
76
+ this.data.edges.push({ ...edge });
77
+ return this;
78
+ }
79
+ /**
80
+ * Surface structural problems without throwing. Returns an array of
81
+ * `ValidationError`s; empty array means the model is well-formed. Used by
82
+ * the editor's status banner and by external tooling.
83
+ */
84
+ validate() {
85
+ const errors = [];
86
+ const nodeIds = /* @__PURE__ */ new Set();
87
+ for (const n of this.data.nodes) {
88
+ if (nodeIds.has(n.id)) errors.push({ kind: "duplicate-node-id", id: n.id, message: `Duplicate node id "${n.id}"` });
89
+ nodeIds.add(n.id);
90
+ }
91
+ const edgeIds = /* @__PURE__ */ new Set();
92
+ for (const e of this.data.edges) {
93
+ if (edgeIds.has(e.id)) errors.push({ kind: "duplicate-edge-id", id: e.id, message: `Duplicate edge id "${e.id}"` });
94
+ edgeIds.add(e.id);
95
+ if (!nodeIds.has(e.from)) errors.push({ kind: "dangling-from", id: e.id, message: `Edge "${e.id}" references unknown source node "${e.from}"` });
96
+ if (!nodeIds.has(e.to)) errors.push({ kind: "dangling-to", id: e.id, message: `Edge "${e.id}" references unknown target node "${e.to}"` });
97
+ }
98
+ return errors;
99
+ }
100
+ /** Remove an edge by id. Safe to call on a missing id (no-op). */
101
+ removeEdge(id) {
102
+ this.data.edges = this.data.edges.filter((e) => e.id !== id);
103
+ return this;
104
+ }
105
+ /** Append a sequence actor. Duplicate names are silently ignored. */
106
+ addActor(name) {
107
+ if (!this.data.actors.includes(name)) {
108
+ this.data.actors.push(name);
109
+ }
110
+ return this;
111
+ }
112
+ /**
113
+ * Append a sequence message. The actors referenced by `from`/`to` are not
114
+ * validated here — callers are expected to register them via `addActor()`
115
+ * first.
116
+ */
117
+ addMessage(message) {
118
+ this.data.messages.push({ ...message });
119
+ return this;
120
+ }
121
+ /**
122
+ * Return a deep-cloned plain `DiagramModel`. Safe to mutate by the caller;
123
+ * mutations do not flow back into this `Model`.
124
+ */
125
+ toJSON() {
126
+ return structuredClone(this.data);
127
+ }
128
+ };
129
+
130
+ // src/core/ids.ts
131
+ function nextId(prefix, existing) {
132
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ const re = new RegExp(`^${escaped}(\\d+)$`);
134
+ let max = 0;
135
+ for (const item of existing) {
136
+ const match = re.exec(item.id);
137
+ if (match) {
138
+ const n = parseInt(match[1], 10);
139
+ if (n > max) max = n;
140
+ }
141
+ }
142
+ return `${prefix}${max + 1}`;
143
+ }
144
+
145
+ // src/exporters/mermaid.ts
146
+ var SHAPE_OPEN = {
147
+ rectangle: "[",
148
+ diamond: "{",
149
+ circle: "((",
150
+ parallelogram: "[/"
151
+ };
152
+ var SHAPE_CLOSE = {
153
+ rectangle: "]",
154
+ diamond: "}",
155
+ circle: "))",
156
+ parallelogram: "/]"
157
+ };
158
+ function formatNode(node) {
159
+ const shape = node.shape ?? "rectangle";
160
+ const open = SHAPE_OPEN[shape] ?? "[";
161
+ const close = SHAPE_CLOSE[shape] ?? "]";
162
+ return ` ${node.id}${open}"${node.label}"${close}`;
163
+ }
164
+ function edgeArrow(edge) {
165
+ const style = edge.style ?? "solid";
166
+ const arrowhead = edge.arrowhead ?? "arrow";
167
+ if (style === "dashed" || style === "dotted") return arrowhead === "none" ? "-.-" : "-.->";
168
+ return arrowhead === "none" ? "---" : "-->";
169
+ }
170
+ function formatEdge(edge) {
171
+ const arrow = edgeArrow(edge);
172
+ return edge.label ? ` ${edge.from} ${arrow}|"${edge.label}"| ${edge.to}` : ` ${edge.from} ${arrow} ${edge.to}`;
173
+ }
174
+ function exportFlowchart(model) {
175
+ const lines = ["graph TD"];
176
+ if (model.title) lines.unshift(`---
177
+ title: ${model.title}
178
+ ---`);
179
+ for (const node of model.nodes) lines.push(formatNode(node));
180
+ for (const edge of model.edges) lines.push(formatEdge(edge));
181
+ return lines.join("\n");
182
+ }
183
+ function msgArrow(msg) {
184
+ return msg.style === "dashed" ? "-->>" : "->>";
185
+ }
186
+ function exportSequence(model) {
187
+ const lines = ["sequenceDiagram"];
188
+ if (model.title) lines.unshift(`---
189
+ title: ${model.title}
190
+ ---`);
191
+ for (const actor of model.actors ?? []) lines.push(` participant ${actor}`);
192
+ for (const msg of model.messages ?? []) {
193
+ lines.push(` ${msg.from}${msgArrow(msg)}${msg.to}: ${msg.label}`);
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+ function toMermaid(model) {
198
+ return model.type === "sequence" ? exportSequence(model) : exportFlowchart(model);
199
+ }
200
+
201
+ // src/exporters/plantuml.ts
202
+ function nodeShape(node) {
203
+ switch (node.shape) {
204
+ case "diamond":
205
+ return ["<>", "<>"];
206
+ case "circle":
207
+ return ["(", ")"];
208
+ case "parallelogram":
209
+ return ["/", "/"];
210
+ default:
211
+ return ["[", "]"];
212
+ }
213
+ }
214
+ function exportFlowchart2(model) {
215
+ const lines = ["@startuml"];
216
+ if (model.title) lines.push(`title ${model.title}`);
217
+ lines.push("");
218
+ for (const node of model.nodes) {
219
+ const [open, close] = nodeShape(node);
220
+ lines.push(`state "${node.label}" as ${node.id} ${open}${close}`);
221
+ }
222
+ lines.push("");
223
+ for (const edge of model.edges) {
224
+ const arrow = edge.style === "dashed" ? "-[dashed]->" : edge.style === "dotted" ? "-[dotted]->" : "-->";
225
+ const label = edge.label ? ` : ${edge.label}` : "";
226
+ lines.push(`${edge.from} ${arrow} ${edge.to}${label}`);
227
+ }
228
+ lines.push("@enduml");
229
+ return lines.join("\n");
230
+ }
231
+ function msgArrow2(msg) {
232
+ return msg.style === "dashed" ? "-->" : "->";
233
+ }
234
+ function exportSequence2(model) {
235
+ const lines = ["@startuml"];
236
+ if (model.title) lines.push(`title ${model.title}`);
237
+ lines.push("");
238
+ for (const actor of model.actors ?? []) {
239
+ lines.push(`participant ${actor}`);
240
+ }
241
+ lines.push("");
242
+ for (const msg of model.messages ?? []) {
243
+ lines.push(`${msg.from} ${msgArrow2(msg)} ${msg.to} : ${msg.label}`);
244
+ }
245
+ lines.push("@enduml");
246
+ return lines.join("\n");
247
+ }
248
+ function toPlantUML(model) {
249
+ return model.type === "sequence" ? exportSequence2(model) : exportFlowchart2(model);
250
+ }
251
+
252
+ // src/exporters/json.ts
253
+ function toJSON(model) {
254
+ return JSON.stringify(model, null, 2);
255
+ }
256
+
257
+ // src/exporters/svg.ts
258
+ var NODE_H = 48;
259
+ var Q_BASE_H = 68;
260
+ var Q_ANS_ROW_H = 80;
261
+ var Q_CARD_PAD = 8;
262
+ var MIN_NODE_W = 120;
263
+ var MAX_NODE_W = 320;
264
+ var MIN_Q_W = 220;
265
+ var PADDING = 48;
266
+ var H_GAP = 80;
267
+ var V_GAP = 96;
268
+ function estimateTextW(text, pxPerChar = 7.5) {
269
+ return text.length * pxPerChar;
270
+ }
271
+ function nodeWidth(label) {
272
+ return Math.min(MAX_NODE_W, Math.max(MIN_NODE_W, Math.ceil(estimateTextW(label) + 48)));
273
+ }
274
+ function answerCardW(ans) {
275
+ return Math.max(86, Math.ceil(Math.max(estimateTextW(ans, 7.5) + 20, 56) + 32));
276
+ }
277
+ function questionNodeW(node) {
278
+ const answers = node.metadata?.answers ?? [];
279
+ const headerW = estimateTextW(node.label, 8) + 80;
280
+ if (answers.length === 0) return Math.max(MIN_Q_W, Math.ceil(headerW));
281
+ const cardsW = answers.reduce((s, a) => s + answerCardW(a), 0) + (answers.length - 1) * Q_CARD_PAD + 2 * Q_CARD_PAD;
282
+ return Math.max(MIN_Q_W, Math.ceil(Math.max(headerW, cardsW)));
283
+ }
284
+ function questionNodeH(answers) {
285
+ return Q_BASE_H + (answers.length === 0 ? 48 : Q_ANS_ROW_H);
286
+ }
287
+ function bezierPath(x1, y1, x2, y2) {
288
+ const dy = y2 - y1;
289
+ const dyAbs = Math.abs(dy);
290
+ const dxAbs = Math.abs(x2 - x1);
291
+ const base = dy > 0 ? dyAbs * 0.55 : Math.max(90, dyAbs * 0.5 + dxAbs * 0.28);
292
+ const curve = Math.max(36, Math.min(220, base));
293
+ return `M ${x1} ${y1} C ${x1} ${y1 + curve}, ${x2} ${y2 - curve}, ${x2} ${y2}`;
294
+ }
295
+ function isQuestion(node, variant) {
296
+ return variant === "question" && !!node.metadata?.answers;
297
+ }
298
+ function computeLayout(model) {
299
+ const boxes = /* @__PURE__ */ new Map();
300
+ const sized = model.nodes.map((n) => {
301
+ const w = isQuestion(n, model.variant) ? questionNodeW(n) : nodeWidth(n.label);
302
+ const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
303
+ return { node: n, w, h };
304
+ });
305
+ const allPositioned = sized.every((s) => typeof s.node.x === "number" && typeof s.node.y === "number");
306
+ if (allPositioned) {
307
+ for (const s of sized) {
308
+ boxes.set(s.node.id, { x: s.node.x, y: s.node.y, w: s.w, h: s.h });
309
+ }
310
+ return boxes;
311
+ }
312
+ const inDeg = new Map(model.nodes.map((n) => [n.id, 0]));
313
+ for (const e of model.edges) inDeg.set(e.to, (inDeg.get(e.to) ?? 0) + 1);
314
+ const layers = /* @__PURE__ */ new Map();
315
+ const queue = model.nodes.filter((n) => (inDeg.get(n.id) ?? 0) === 0).map((n) => n.id);
316
+ for (const id of queue) layers.set(id, 0);
317
+ let head = 0;
318
+ while (head < queue.length) {
319
+ const cur = queue[head++];
320
+ const layer = layers.get(cur) ?? 0;
321
+ for (const e of model.edges) {
322
+ if (e.from === cur) {
323
+ const next = layers.get(e.to) ?? -1;
324
+ if (next < layer + 1) {
325
+ layers.set(e.to, layer + 1);
326
+ queue.push(e.to);
327
+ }
328
+ }
329
+ }
330
+ }
331
+ model.nodes.forEach((n) => {
332
+ if (!layers.has(n.id)) layers.set(n.id, 0);
333
+ });
334
+ const byLayer = /* @__PURE__ */ new Map();
335
+ for (const s of sized) {
336
+ const layer = layers.get(s.node.id) ?? 0;
337
+ if (!byLayer.has(layer)) byLayer.set(layer, []);
338
+ byLayer.get(layer).push(s);
339
+ }
340
+ let y = PADDING;
341
+ for (const layer of [...byLayer.keys()].sort((a, b) => a - b)) {
342
+ const row = byLayer.get(layer);
343
+ let x = PADDING;
344
+ let maxH = 0;
345
+ for (const s of row) {
346
+ boxes.set(s.node.id, { x, y, w: s.w, h: s.h });
347
+ x += s.w + H_GAP;
348
+ maxH = Math.max(maxH, s.h);
349
+ }
350
+ y += maxH + V_GAP;
351
+ }
352
+ return boxes;
353
+ }
354
+ function escapeXML(s) {
355
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
356
+ }
357
+ var COLORS = {
358
+ bg: "#fafbfc",
359
+ dot: "#dbe3ee",
360
+ nodeFill: "#ffffff",
361
+ nodeStroke: "#cbd5e1",
362
+ edge: "#94a3b8",
363
+ text: "#1e293b",
364
+ textSub: "#94a3b8",
365
+ amber: "#d97706",
366
+ amberSoft: "#fef9ee",
367
+ amberLine: "#fde68a",
368
+ amberCardBg: "#fffdf7"
369
+ };
370
+ function renderStandardNode(node, box) {
371
+ const cx = box.x + box.w / 2;
372
+ const cy = box.y + box.h / 2;
373
+ 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}">${escapeXML(node.label)}</text>`;
375
+ let shapeEl = "";
376
+ if (shape === "diamond") {
377
+ const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
378
+ shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
379
+ } else if (shape === "circle") {
380
+ const r = Math.min(box.w, box.h) / 2 - 1;
381
+ shapeEl = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
382
+ } else if (shape === "parallelogram") {
383
+ const pts = `${box.x + 14},${box.y} ${box.x + box.w},${box.y} ${box.x + box.w - 14},${box.y + box.h} ${box.x},${box.y + box.h}`;
384
+ shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
385
+ } else {
386
+ shapeEl = `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
387
+ }
388
+ return shapeEl + label;
389
+ }
390
+ function renderQuestionNode(node, box) {
391
+ const answers = node.metadata?.answers ?? [];
392
+ const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
393
+ const x = box.x, y = box.y, w = box.w, h = box.h;
394
+ const parts = [];
395
+ parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`);
396
+ parts.push(`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`);
397
+ parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`);
398
+ parts.push(`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`);
399
+ parts.push(`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`);
400
+ parts.push(`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`);
401
+ parts.push(`<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>`);
402
+ parts.push(`<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}">${escapeXML(node.label)}</text>`);
403
+ parts.push(`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
404
+ if (answers.length === 0) {
405
+ parts.push(`<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>`);
406
+ } else {
407
+ const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
408
+ answers.forEach((ans, i) => {
409
+ const prevW = answers.slice(0, i).reduce((s, a) => s + answerCardW(a) + Q_CARD_PAD, 0);
410
+ const cW = answerCardW(ans);
411
+ const cardX = x + Q_CARD_PAD + prevW;
412
+ const cardY = y + Q_BASE_H + 7;
413
+ const cardH = Q_ANS_ROW_H - 20;
414
+ const cx = cardX + cW / 2;
415
+ const letter = i < 26 ? letters[i] : `${i + 1}`;
416
+ const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
417
+ const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
418
+ parts.push(`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
419
+ parts.push(`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`);
420
+ parts.push(`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${escapeXML(letter)}</text>`);
421
+ parts.push(`<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">${escapeXML(displayAns)}</text>`);
422
+ });
423
+ }
424
+ return parts.join("");
425
+ }
426
+ function renderEdge(edge, boxes, variant, nodes) {
427
+ const fromBox = boxes.get(edge.from);
428
+ const toBox = boxes.get(edge.to);
429
+ if (!fromBox || !toBox) return "";
430
+ let x1, y1;
431
+ const fromNode = nodes.find((n) => n.id === edge.from);
432
+ if (fromNode && isQuestion(fromNode, variant)) {
433
+ const answers = fromNode.metadata?.answers ?? [];
434
+ const idx = answers.indexOf(edge.label ?? "");
435
+ if (idx >= 0) {
436
+ const prevW = answers.slice(0, idx).reduce((s, a) => s + answerCardW(a) + Q_CARD_PAD, 0);
437
+ const cW = answerCardW(answers[idx]);
438
+ x1 = fromBox.x + Q_CARD_PAD + prevW + cW / 2;
439
+ y1 = fromBox.y + Q_BASE_H + Q_ANS_ROW_H - 8;
440
+ } else {
441
+ x1 = fromBox.x + fromBox.w / 2;
442
+ y1 = fromBox.y + fromBox.h;
443
+ }
444
+ } else {
445
+ x1 = fromBox.x + fromBox.w / 2;
446
+ y1 = fromBox.y + fromBox.h;
447
+ }
448
+ const x2 = toBox.x + toBox.w / 2;
449
+ const y2 = toBox.y;
450
+ const dash = edge.style === "dashed" ? ' stroke-dasharray="6,4"' : edge.style === "dotted" ? ' stroke-dasharray="2,3"' : "";
451
+ const marker = edge.arrowhead === "none" ? "" : ' marker-end="url(#arrow)"';
452
+ const d = bezierPath(x1, y1, x2, y2);
453
+ let out = `<path d="${d}" fill="none" stroke="${COLORS.edge}" stroke-width="1.5"${dash}${marker}/>`;
454
+ if (edge.label) {
455
+ const midX = (x1 + x2) / 2;
456
+ const midY = (y1 + y2) / 2;
457
+ const labelW = estimateTextW(edge.label, 7) + 14;
458
+ 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}">${escapeXML(edge.label)}</text>`;
460
+ }
461
+ return out;
462
+ }
463
+ function toSVG(model) {
464
+ const boxes = computeLayout(model);
465
+ let maxX = 0, maxY = 0;
466
+ for (const b of boxes.values()) {
467
+ maxX = Math.max(maxX, b.x + b.w);
468
+ maxY = Math.max(maxY, b.y + b.h);
469
+ }
470
+ const width = maxX + PADDING;
471
+ const height = maxY + PADDING + (model.title ? 32 : 0);
472
+ const defs = [
473
+ `<defs>`,
474
+ `<pattern id="dotgrid" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">`,
475
+ `<circle cx="12" cy="12" r="1.1" fill="${COLORS.dot}"/>`,
476
+ `</pattern>`,
477
+ `<filter id="nodeShadow" x="-20%" y="-20%" width="140%" height="140%">`,
478
+ `<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="rgba(15,23,42,0.09)"/>`,
479
+ `</filter>`,
480
+ `<marker id="arrow" markerWidth="9" markerHeight="7" refX="8.5" refY="3.5" orient="auto" markerUnits="strokeWidth">`,
481
+ `<path d="M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z" fill="${COLORS.edge}"/>`,
482
+ `</marker>`,
483
+ `</defs>`
484
+ ].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}">${escapeXML(model.title)}</text>` : "";
486
+ const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
487
+ const nodes = model.nodes.map((n) => {
488
+ const b = boxes.get(n.id);
489
+ return isQuestion(n, model.variant) ? renderQuestionNode(n, b) : renderStandardNode(n, b);
490
+ }).join("\n");
491
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
492
+ ${defs}
493
+ <rect width="${width}" height="${height}" fill="${COLORS.bg}"/>
494
+ <rect width="${width}" height="${height}" fill="url(#dotgrid)"/>
495
+ ${titleEl}
496
+ ${edges}
497
+ ${nodes}
498
+ </svg>`;
499
+ }
500
+ async function toPNG(model) {
501
+ if (typeof document === "undefined") {
502
+ throw new Error("toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js.");
503
+ }
504
+ const svg = toSVG(model);
505
+ const blob = new Blob([svg], { type: "image/svg+xml" });
506
+ const url = URL.createObjectURL(blob);
507
+ return new Promise((resolve, reject) => {
508
+ const img = new Image();
509
+ img.onload = () => {
510
+ const canvas = document.createElement("canvas");
511
+ const scale = window.devicePixelRatio || 2;
512
+ canvas.width = img.naturalWidth * scale;
513
+ canvas.height = img.naturalHeight * scale;
514
+ const ctx = canvas.getContext("2d");
515
+ ctx.scale(scale, scale);
516
+ ctx.drawImage(img, 0, 0);
517
+ URL.revokeObjectURL(url);
518
+ canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")), "image/png");
519
+ };
520
+ img.onerror = () => {
521
+ URL.revokeObjectURL(url);
522
+ reject(new Error("SVG image load failed"));
523
+ };
524
+ img.src = url;
525
+ });
526
+ }
527
+
528
+ // src/core/flowchart.ts
529
+ var FlowchartBuilder = class {
530
+ model;
531
+ /** @param title Optional human-readable diagram title. */
532
+ constructor(title) {
533
+ this.model = new Model("flowchart", title);
534
+ }
535
+ /**
536
+ * Append a node. Defaults `shape` to `rectangle` when omitted from `options`.
537
+ * Throws on duplicate id.
538
+ */
539
+ node(id, label, options = {}) {
540
+ this.model.addNode({ id, label, shape: options.shape ?? "rectangle", ...options });
541
+ return this;
542
+ }
543
+ /**
544
+ * Append an edge with an auto-generated id. The id is derived from the
545
+ * current edge list to avoid collisions with imported models.
546
+ */
547
+ edge(from, to, options = {}) {
548
+ this.model.addEdge({ id: nextId("e", this.model.toJSON().edges), from, to, ...options });
549
+ return this;
550
+ }
551
+ /** Remove a node and every edge that references it. */
552
+ removeNode(id) {
553
+ this.model.removeNode(id);
554
+ return this;
555
+ }
556
+ /** Remove an edge by id. */
557
+ removeEdge(id) {
558
+ this.model.removeEdge(id);
559
+ return this;
560
+ }
561
+ /** Patch an existing node. See `Model.updateNode`. */
562
+ updateNode(id, patch) {
563
+ this.model.updateNode(id, patch);
564
+ return this;
565
+ }
566
+ /** Return the underlying `Model` for advanced operations or validation. */
567
+ getModel() {
568
+ return this.model;
569
+ }
570
+ /** Serialize as Mermaid `flowchart TD` source. */
571
+ toMermaid() {
572
+ return toMermaid(this.model.toJSON());
573
+ }
574
+ /** Serialize as PlantUML activity-diagram source. */
575
+ toPlantUML() {
576
+ return toPlantUML(this.model.toJSON());
577
+ }
578
+ /** Serialize as the package's JSON shape (full round-trip fidelity). */
579
+ toJSON() {
580
+ return toJSON(this.model.toJSON());
581
+ }
582
+ /** Render to a standalone SVG string. */
583
+ toSVG() {
584
+ return toSVG(this.model.toJSON());
585
+ }
586
+ /** Render to a PNG `Blob`. Browser-only (uses the Canvas API). */
587
+ toPNG() {
588
+ return toPNG(this.model.toJSON());
589
+ }
590
+ };
591
+ function flowchart(title) {
592
+ return new FlowchartBuilder(title);
593
+ }
594
+
595
+ // src/core/sequence.ts
596
+ var SequenceBuilder = class {
597
+ model;
598
+ /** @param title Optional human-readable diagram title. */
599
+ constructor(title) {
600
+ this.model = new Model("sequence", title);
601
+ }
602
+ /** Register an actor. Duplicates are silently ignored. */
603
+ actor(name) {
604
+ this.model.addActor(name);
605
+ return this;
606
+ }
607
+ /**
608
+ * Append a message. Both endpoints are auto-registered as actors if not
609
+ * already present. The id is derived from the current message list.
610
+ */
611
+ message(from, to, label, options = {}) {
612
+ this.model.addActor(from);
613
+ this.model.addActor(to);
614
+ const messages = this.model.toJSON().messages ?? [];
615
+ this.model.addMessage({ id: nextId("m", messages), from, to, label, style: options.style ?? "solid" });
616
+ return this;
617
+ }
618
+ /** Convenience for a `dashed`-style return message. */
619
+ replyMessage(from, to, label) {
620
+ return this.message(from, to, label, { style: "dashed" });
621
+ }
622
+ /** Return the underlying `Model` for advanced operations or validation. */
623
+ getModel() {
624
+ return this.model;
625
+ }
626
+ /** Serialize as Mermaid `sequenceDiagram` source. */
627
+ toMermaid() {
628
+ return toMermaid(this.model.toJSON());
629
+ }
630
+ /** Serialize as PlantUML sequence-diagram source. */
631
+ toPlantUML() {
632
+ return toPlantUML(this.model.toJSON());
633
+ }
634
+ /** Serialize as the package's JSON shape (full round-trip fidelity). */
635
+ toJSON() {
636
+ return toJSON(this.model.toJSON());
637
+ }
638
+ };
639
+ function sequence(title) {
640
+ return new SequenceBuilder(title);
641
+ }
642
+
643
+ // src/importers/mermaid.ts
644
+ function parseNodeDecl(raw) {
645
+ const patterns = [
646
+ [/^(\w+)\{\{?"?(.+?)"?\}?\}$/, "diamond"],
647
+ [/^(\w+)\(\("?(.+?)"?\)\)$/, "circle"],
648
+ [/^(\w+)\[\/(.+?)\/\]$/, "parallelogram"],
649
+ [/^(\w+)\[["']?(.+?)["']?\]$/, "rectangle"],
650
+ [/^(\w+)\("?(.+?)"?\)$/, "rectangle"]
651
+ ];
652
+ for (const [re, shape] of patterns) {
653
+ const m = raw.match(re);
654
+ if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
655
+ }
656
+ return null;
657
+ }
658
+ var EDGE_RE = /^(.+?)\s*(-\.->|-\.-|-->|---)(?:\|(.+?)\|)?\s*(.+)$/;
659
+ function detectStyle(connector) {
660
+ return connector.startsWith("-.") ? "dashed" : "solid";
661
+ }
662
+ function detectArrowhead(connector) {
663
+ return connector.endsWith(">") ? "arrow" : "none";
664
+ }
665
+ function parseFlowchart(lines) {
666
+ const model = new Model("flowchart");
667
+ const nodeMap = /* @__PURE__ */ new Map();
668
+ const groupStack = [];
669
+ const ensureNode = (id, group) => {
670
+ if (!nodeMap.has(id)) {
671
+ nodeMap.set(id, true);
672
+ const metadata = group ? { group } : void 0;
673
+ model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
674
+ }
675
+ };
676
+ for (const line of lines) {
677
+ const trimmed = line.trim();
678
+ 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 ")) continue;
680
+ const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
681
+ if (subgraphOpen) {
682
+ groupStack.push(subgraphOpen[1]);
683
+ continue;
684
+ }
685
+ if (/^end\b/i.test(trimmed)) {
686
+ groupStack.pop();
687
+ continue;
688
+ }
689
+ const currentGroup = groupStack[groupStack.length - 1];
690
+ const edgeMatch = trimmed.match(EDGE_RE);
691
+ if (edgeMatch) {
692
+ const fromRaw = edgeMatch[1].trim();
693
+ const connector = edgeMatch[2];
694
+ const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
695
+ const toRaw = edgeMatch[4].trim();
696
+ const style = detectStyle(connector);
697
+ const arrowhead = detectArrowhead(connector);
698
+ const fromNode = parseNodeDecl(fromRaw);
699
+ const toNode = parseNodeDecl(toRaw);
700
+ if (fromNode && !nodeMap.has(fromNode.id)) {
701
+ nodeMap.set(fromNode.id, true);
702
+ const metadata = currentGroup ? { group: currentGroup } : void 0;
703
+ model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
704
+ } else if (!fromNode) {
705
+ ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
706
+ }
707
+ if (toNode && !nodeMap.has(toNode.id)) {
708
+ nodeMap.set(toNode.id, true);
709
+ const metadata = currentGroup ? { group: currentGroup } : void 0;
710
+ model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
711
+ } else if (!toNode) {
712
+ ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
713
+ }
714
+ const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
715
+ const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
716
+ model.addEdge({
717
+ id: nextId("e", model.toJSON().edges),
718
+ from: fromId,
719
+ to: toId,
720
+ ...label ? { label } : {},
721
+ style,
722
+ ...arrowhead === "none" ? { arrowhead } : {}
723
+ });
724
+ continue;
725
+ }
726
+ const nodeDecl = parseNodeDecl(trimmed);
727
+ if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
728
+ nodeMap.set(nodeDecl.id, true);
729
+ const metadata = currentGroup ? { group: currentGroup } : void 0;
730
+ model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
731
+ }
732
+ }
733
+ return model;
734
+ }
735
+ function parseSequence(lines, title) {
736
+ const model = new Model("sequence", title);
737
+ for (const line of lines) {
738
+ const trimmed = line.trim();
739
+ if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
740
+ const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
741
+ if (participantMatch) {
742
+ model.addActor(participantMatch[1].trim());
743
+ continue;
744
+ }
745
+ const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
746
+ if (actorMatch) {
747
+ model.addActor(actorMatch[1].trim());
748
+ continue;
749
+ }
750
+ const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
751
+ if (msgMatch) {
752
+ const from = msgMatch[1].trim();
753
+ const arrow = msgMatch[2];
754
+ const to = msgMatch[3].trim();
755
+ const label = msgMatch[4].trim();
756
+ model.addActor(from);
757
+ model.addActor(to);
758
+ const messages = model.toJSON().messages ?? [];
759
+ model.addMessage({ id: nextId("m", messages), from, to, label, style: arrow.startsWith("--") ? "dashed" : "solid" });
760
+ }
761
+ }
762
+ return model;
763
+ }
764
+ function fromMermaid(mermaid) {
765
+ const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
766
+ const rawLines = cleaned.split("\n");
767
+ let startIdx = 0;
768
+ let title;
769
+ if (rawLines[0]?.trim() === "---") {
770
+ const endFm = rawLines.findIndex((l, i) => i > 0 && l.trim() === "---");
771
+ if (endFm !== -1) {
772
+ const fmLines = rawLines.slice(1, endFm);
773
+ for (const fl of fmLines) {
774
+ const tm = fl.match(/^title:\s*(.+)$/);
775
+ if (tm) title = tm[1].trim();
776
+ }
777
+ startIdx = endFm + 1;
778
+ }
779
+ }
780
+ const lines = rawLines.slice(startIdx);
781
+ const firstContent = lines.find((l) => l.trim());
782
+ if (firstContent?.trim().startsWith("sequenceDiagram")) {
783
+ const m2 = parseSequence(lines, title);
784
+ return m2;
785
+ }
786
+ const m = parseFlowchart(lines);
787
+ if (title) {
788
+ const data = m.toJSON();
789
+ data.title = title;
790
+ return Model.fromData(data);
791
+ }
792
+ return m;
793
+ }
794
+
795
+ // src/importers/json.ts
796
+ function fromJSON(json) {
797
+ const data = typeof json === "string" ? JSON.parse(json) : json;
798
+ if (!data.type || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
799
+ throw new Error("Invalid DiagramModel JSON");
800
+ }
801
+ return Model.fromData(data);
802
+ }
803
+ export {
804
+ FlowchartBuilder,
805
+ Model,
806
+ SequenceBuilder,
807
+ flowchart,
808
+ fromJSON,
809
+ fromMermaid,
810
+ sequence,
811
+ toJSON,
812
+ toMermaid,
813
+ toPNG,
814
+ toPlantUML,
815
+ toSVG
816
+ };
817
+ //# sourceMappingURL=index.js.map