@kekonic/diagrams-core 1.0.0-rc.4

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.mjs ADDED
@@ -0,0 +1,4966 @@
1
+ //#region src/types/branch.ts
2
+ /**
3
+ * Classify an edge label into a yes/no/neutral branch cue.
4
+ * Only clear affirmative/negative tokens — use `branch: yes|no` for outcome wording.
5
+ */
6
+ function classifyBranch(label) {
7
+ if (!label) return "neutral";
8
+ const normalized = label.trim().toLowerCase();
9
+ if (/^(yes|y|true|ok)\b/.test(normalized) || normalized === "yes") return "yes";
10
+ if (/^(no|n|false)\b/.test(normalized) || normalized === "no") return "no";
11
+ if (/\byes\b/.test(normalized) && !/\bno\b/.test(normalized)) return "yes";
12
+ if (/\bno\b/.test(normalized) && !/\byes\b/.test(normalized)) return "no";
13
+ return "neutral";
14
+ }
15
+ function normalizeBranch(value) {
16
+ if (value === "yes" || value === "no" || value === "neutral") return value;
17
+ }
18
+ //#endregion
19
+ //#region src/animation/infer-auto-animation.ts
20
+ const DEFAULT_FLOW_MS$1 = 500;
21
+ const DEFAULT_PULSE_MS$1 = 650;
22
+ /** Dwell pulse for ordinary nodes between hops. */
23
+ const DEFAULT_NODE_PULSE_MS = 450;
24
+ const DEFAULT_WAIT_MS = 600;
25
+ const DEFAULT_CHAPTER_MS = 350;
26
+ /** Soft cap so dense exclusive-branch DAGs stay watchable. */
27
+ const MAX_AUTO_CHAPTERS = 12;
28
+ /**
29
+ * Infer Automatic animation.
30
+ *
31
+ * - **Exclusive** fan-out (choice nodes / yes·no branches) → one chapter per alternative.
32
+ * - **Parallel** fan-out (everything else) → `parallel` flow cues in a single story.
33
+ */
34
+ function inferAutoAnimation(graph) {
35
+ if (graph.diagramKind === "sequence" && graph.sequence) return inferSequenceAutoAnimation(graph);
36
+ const steps = planAutoWalk(graph);
37
+ if (steps.length === 0) return null;
38
+ const cues = [];
39
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
40
+ let chapterIndex = -1;
41
+ const pulseFor = (nodeId) => {
42
+ const node = nodeById.get(nodeId);
43
+ return {
44
+ op: "pulse",
45
+ targets: [{
46
+ type: "node",
47
+ id: nodeId
48
+ }],
49
+ durationMs: node?.kind === "choice" ? DEFAULT_PULSE_MS$1 : DEFAULT_NODE_PULSE_MS
50
+ };
51
+ };
52
+ for (const step of steps) {
53
+ if (step.kind === "chapter") {
54
+ chapterIndex += 1;
55
+ cues.push({
56
+ op: "dim",
57
+ targets: [{ type: "all" }]
58
+ });
59
+ if (chapterIndex > 0) cues.push({
60
+ op: "wait",
61
+ durationMs: DEFAULT_CHAPTER_MS
62
+ });
63
+ continue;
64
+ }
65
+ if (step.kind === "enter") {
66
+ cues.push({
67
+ op: "activate",
68
+ targets: [{
69
+ type: "node",
70
+ id: step.nodeId
71
+ }]
72
+ });
73
+ cues.push(pulseFor(step.nodeId));
74
+ continue;
75
+ }
76
+ if (step.kind === "enter-many") {
77
+ const targets = step.nodeIds.map((id) => ({
78
+ type: "node",
79
+ id
80
+ }));
81
+ cues.push({
82
+ op: "activate",
83
+ targets
84
+ });
85
+ cues.push({
86
+ op: "parallel",
87
+ cues: step.nodeIds.map((id) => pulseFor(id))
88
+ });
89
+ continue;
90
+ }
91
+ if (step.kind === "edge") {
92
+ cues.push({
93
+ op: "flow",
94
+ path: [step.from, step.to],
95
+ durationMs: DEFAULT_FLOW_MS$1
96
+ });
97
+ continue;
98
+ }
99
+ if (step.edges.length === 1) {
100
+ const e = step.edges[0];
101
+ cues.push({
102
+ op: "flow",
103
+ path: [e.from, e.to],
104
+ durationMs: DEFAULT_FLOW_MS$1
105
+ });
106
+ } else cues.push({
107
+ op: "parallel",
108
+ cues: step.edges.map((e) => ({
109
+ op: "flow",
110
+ path: [e.from, e.to],
111
+ durationMs: DEFAULT_FLOW_MS$1
112
+ }))
113
+ });
114
+ }
115
+ cues.push({
116
+ op: "wait",
117
+ durationMs: DEFAULT_WAIT_MS
118
+ });
119
+ return {
120
+ id: "auto",
121
+ name: "Automatic",
122
+ loop: false,
123
+ cues,
124
+ source: "auto"
125
+ };
126
+ }
127
+ /**
128
+ * Chaptered walk: exclusive alternatives restart as chapters; concurrent
129
+ * fan-out becomes parallel edge steps within a chapter.
130
+ */
131
+ function planAutoWalk(graph) {
132
+ const chapters = enumerateChapterEdgeSets(graph);
133
+ if (chapters.length === 0) return [];
134
+ const steps = [];
135
+ for (const chapter of chapters) {
136
+ steps.push({ kind: "chapter" });
137
+ steps.push(...wavefrontSteps(chapter.startId, chapter.edges));
138
+ }
139
+ return steps;
140
+ }
141
+ /**
142
+ * Exclusive alternatives as node sequences (parallel siblings linearized in
143
+ * wavefront order). Prefer {@link planAutoWalk} for playback structure.
144
+ */
145
+ function enumerateAutoPaths(graph) {
146
+ return enumerateChapterEdgeSets(graph).map((ch) => linearizeChapter(ch.startId, ch.edges));
147
+ }
148
+ /** First-visit node order across Automatic chapters. */
149
+ function traceDeclarationPath(graph) {
150
+ const seen = /* @__PURE__ */ new Set();
151
+ const path = [];
152
+ for (const step of planAutoWalk(graph)) if (step.kind === "enter") {
153
+ if (seen.has(step.nodeId)) continue;
154
+ seen.add(step.nodeId);
155
+ path.push(step.nodeId);
156
+ } else if (step.kind === "enter-many") for (const id of step.nodeIds) {
157
+ if (seen.has(id)) continue;
158
+ seen.add(id);
159
+ path.push(id);
160
+ }
161
+ return path;
162
+ }
163
+ function enumerateChapterEdgeSets(graph) {
164
+ if (graph.nodes.length === 0) return [];
165
+ const nodeIds = new Set(graph.nodes.map((n) => n.id));
166
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
167
+ const outs = buildOuts(graph, nodeIds);
168
+ const indegree = buildIndegree(graph, nodeIds);
169
+ const sources = graph.nodes.filter((n) => (indegree.get(n.id) ?? 0) === 0);
170
+ const startIds = sources.length > 0 ? sources.map((n) => n.id) : [graph.nodes[0].id];
171
+ const primary = startIds[0];
172
+ const chapters = [];
173
+ const pushChapter = (startId, edges) => {
174
+ if (chapters.length >= MAX_AUTO_CHAPTERS) return;
175
+ const key = `${startId}|${edges.map((e) => `${e.from}>${e.to}`).join(";")}`;
176
+ if (chapters.some((c) => `${c.startId}|${c.edges.map((e) => `${e.from}>${e.to}`).join(";")}` === key)) return;
177
+ chapters.push({
178
+ startId,
179
+ edges: dedupeEdges(edges)
180
+ });
181
+ };
182
+ for (const edgeSet of edgeSetsFrom(primary, /* @__PURE__ */ new Set(), nodeById, outs)) {
183
+ pushChapter(primary, edgeSet);
184
+ if (chapters.length >= MAX_AUTO_CHAPTERS) break;
185
+ }
186
+ const covered = /* @__PURE__ */ new Set();
187
+ for (const ch of chapters) {
188
+ covered.add(ch.startId);
189
+ for (const e of ch.edges) {
190
+ covered.add(e.from);
191
+ covered.add(e.to);
192
+ }
193
+ }
194
+ for (const sourceId of startIds.slice(1)) {
195
+ if (chapters.length >= MAX_AUTO_CHAPTERS) break;
196
+ const side = [];
197
+ const seen = /* @__PURE__ */ new Set();
198
+ let nodeId = sourceId;
199
+ while (nodeId && !seen.has(nodeId)) {
200
+ seen.add(nodeId);
201
+ if (seen.size > 1 && covered.has(nodeId)) break;
202
+ const nexts = outs.get(nodeId) ?? [];
203
+ if (nexts.length === 0) break;
204
+ const out = nexts[0];
205
+ side.push({
206
+ from: nodeId,
207
+ to: out.to
208
+ });
209
+ nodeId = out.to;
210
+ }
211
+ pushChapter(sourceId, side);
212
+ }
213
+ for (const node of graph.nodes) {
214
+ if (covered.has(node.id)) continue;
215
+ if (chapters.some((ch) => ch.startId === node.id || ch.edges.some((e) => e.from === node.id || e.to === node.id))) continue;
216
+ if (chapters.length >= MAX_AUTO_CHAPTERS) break;
217
+ const sets = edgeSetsFrom(node.id, /* @__PURE__ */ new Set(), nodeById, outs);
218
+ if (sets.length === 0) pushChapter(node.id, []);
219
+ else for (const edgeSet of sets) pushChapter(node.id, edgeSet);
220
+ }
221
+ return chapters;
222
+ }
223
+ /**
224
+ * Edge-set alternatives from `nodeId`.
225
+ * Exclusive fan-out → multiple sets; parallel fan-out → one set (cartesian if
226
+ * parallel arms themselves contain exclusive forks).
227
+ */
228
+ function edgeSetsFrom(nodeId, trail, nodeById, outs) {
229
+ if (trail.has(nodeId)) return [[]];
230
+ const nextTrail = new Set(trail);
231
+ nextTrail.add(nodeId);
232
+ const nexts = outs.get(nodeId) ?? [];
233
+ if (nexts.length === 0) return [[]];
234
+ if (isExclusiveFanOut(nodeById.get(nodeId), nexts)) {
235
+ const result = [];
236
+ for (const out of nexts) {
237
+ const hop = {
238
+ from: nodeId,
239
+ to: out.to
240
+ };
241
+ for (const suffix of edgeSetsFrom(out.to, nextTrail, nodeById, outs)) result.push([hop, ...suffix]);
242
+ }
243
+ return result.length > 0 ? result : [[]];
244
+ }
245
+ return cartesianConcat(nexts.map((out) => {
246
+ const hop = {
247
+ from: nodeId,
248
+ to: out.to
249
+ };
250
+ return edgeSetsFrom(out.to, nextTrail, nodeById, outs).map((suffix) => [hop, ...suffix]);
251
+ }));
252
+ }
253
+ function cartesianConcat(branches) {
254
+ if (branches.length === 0) return [[]];
255
+ let acc = [[]];
256
+ for (const alts of branches) {
257
+ const next = [];
258
+ for (const prefix of acc) for (const alt of alts) next.push([...prefix, ...alt]);
259
+ acc = next;
260
+ if (acc.length >= MAX_AUTO_CHAPTERS) break;
261
+ }
262
+ return acc;
263
+ }
264
+ function wavefrontSteps(startId, edges) {
265
+ const steps = [];
266
+ const remaining = new Set(edges.map((e) => keyOf(e)));
267
+ const edgeByKey = new Map(edges.map((e) => [keyOf(e), e]));
268
+ const reached = /* @__PURE__ */ new Set([startId]);
269
+ steps.push({
270
+ kind: "enter",
271
+ nodeId: startId
272
+ });
273
+ let guard = edges.length + 2;
274
+ while (remaining.size > 0 && guard-- > 0) {
275
+ const ready = [];
276
+ for (const key of remaining) {
277
+ const e = edgeByKey.get(key);
278
+ if (reached.has(e.from)) ready.push(e);
279
+ }
280
+ if (ready.length === 0) break;
281
+ if (ready.length === 1) {
282
+ const e = ready[0];
283
+ steps.push({
284
+ kind: "edge",
285
+ from: e.from,
286
+ to: e.to
287
+ });
288
+ } else steps.push({
289
+ kind: "parallel-edges",
290
+ edges: ready
291
+ });
292
+ const newly = [];
293
+ for (const e of ready) {
294
+ remaining.delete(keyOf(e));
295
+ if (!reached.has(e.to)) {
296
+ reached.add(e.to);
297
+ newly.push(e.to);
298
+ }
299
+ }
300
+ const uniqueNew = [...new Set(newly)];
301
+ if (uniqueNew.length === 1) steps.push({
302
+ kind: "enter",
303
+ nodeId: uniqueNew[0]
304
+ });
305
+ else if (uniqueNew.length > 1) steps.push({
306
+ kind: "enter-many",
307
+ nodeIds: uniqueNew
308
+ });
309
+ }
310
+ return steps;
311
+ }
312
+ function linearizeChapter(startId, edges) {
313
+ const ids = [];
314
+ const seen = /* @__PURE__ */ new Set();
315
+ for (const step of wavefrontSteps(startId, edges)) if (step.kind === "enter") {
316
+ if (!seen.has(step.nodeId)) {
317
+ seen.add(step.nodeId);
318
+ ids.push(step.nodeId);
319
+ }
320
+ } else if (step.kind === "enter-many") for (const id of step.nodeIds) {
321
+ if (seen.has(id)) continue;
322
+ seen.add(id);
323
+ ids.push(id);
324
+ }
325
+ return ids;
326
+ }
327
+ /** Choice diamonds / yes·no forks are mutually exclusive stories. */
328
+ function isExclusiveFanOut(node, outs) {
329
+ if (outs.length < 2) return false;
330
+ if (node?.kind === "choice") return true;
331
+ const branches = new Set(outs.map((o) => o.edge.branch ?? classifyBranch(o.edge.label)));
332
+ return branches.has("yes") && branches.has("no");
333
+ }
334
+ function buildOuts(graph, nodeIds) {
335
+ const outs = /* @__PURE__ */ new Map();
336
+ for (const edge of graph.edges) {
337
+ if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) continue;
338
+ const list = outs.get(edge.from);
339
+ if (list) list.push({
340
+ to: edge.to,
341
+ edge
342
+ });
343
+ else outs.set(edge.from, [{
344
+ to: edge.to,
345
+ edge
346
+ }]);
347
+ }
348
+ return outs;
349
+ }
350
+ function buildIndegree(graph, nodeIds) {
351
+ const indegree = /* @__PURE__ */ new Map();
352
+ for (const id of nodeIds) indegree.set(id, 0);
353
+ for (const edge of graph.edges) {
354
+ if (!nodeIds.has(edge.to)) continue;
355
+ indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
356
+ }
357
+ return indegree;
358
+ }
359
+ function dedupeEdges(edges) {
360
+ const seen = /* @__PURE__ */ new Set();
361
+ const out = [];
362
+ for (const e of edges) {
363
+ const k = keyOf(e);
364
+ if (seen.has(k)) continue;
365
+ seen.add(k);
366
+ out.push(e);
367
+ }
368
+ return out;
369
+ }
370
+ function keyOf(e) {
371
+ return `${e.from}\0${e.to}`;
372
+ }
373
+ /** Participants with an open activation bar covering this message order. */
374
+ function liveParticipantsAt(activations, order) {
375
+ const ids = /* @__PURE__ */ new Set();
376
+ for (const a of activations) if (a.startOrder <= order && order <= a.endOrder) ids.add(a.participantId);
377
+ return [...ids];
378
+ }
379
+ /**
380
+ * Walk sequence messages in order.
381
+ * Emphasis follows SequenceIR activation spans: when a participant deactivates,
382
+ * we dim and re-activate only the still-live set (plus the hop endpoints).
383
+ */
384
+ function inferSequenceAutoAnimation(graph) {
385
+ const seq = graph.sequence;
386
+ if (!seq || seq.messages.length === 0) return null;
387
+ const cues = [];
388
+ let prevEmphasisKey = "";
389
+ for (const msg of seq.messages) {
390
+ if (!msg.from || !msg.to || msg.kind === "destroy") continue;
391
+ const live = liveParticipantsAt(seq.activations, msg.order);
392
+ const emphasis = new Set(live);
393
+ emphasis.add(msg.from);
394
+ emphasis.add(msg.to);
395
+ const emphasisKey = [...emphasis].sort().join("\0");
396
+ if (emphasisKey !== prevEmphasisKey) {
397
+ cues.push({
398
+ op: "dim",
399
+ targets: [{ type: "all" }]
400
+ });
401
+ cues.push({
402
+ op: "activate",
403
+ targets: [...emphasis].map((id) => ({
404
+ type: "node",
405
+ id
406
+ }))
407
+ });
408
+ prevEmphasisKey = emphasisKey;
409
+ }
410
+ cues.push({
411
+ op: "flow",
412
+ path: [msg.from, msg.to],
413
+ durationMs: DEFAULT_FLOW_MS$1,
414
+ edgeId: msg.id,
415
+ edgeIds: [msg.id]
416
+ });
417
+ cues.push({
418
+ op: "pulse",
419
+ targets: [{
420
+ type: "node",
421
+ id: msg.to
422
+ }],
423
+ durationMs: DEFAULT_NODE_PULSE_MS
424
+ });
425
+ }
426
+ cues.push({
427
+ op: "wait",
428
+ durationMs: DEFAULT_WAIT_MS
429
+ });
430
+ return {
431
+ id: "auto",
432
+ name: "Automatic",
433
+ loop: false,
434
+ cues,
435
+ source: "auto"
436
+ };
437
+ }
438
+ //#endregion
439
+ //#region src/animation/bind-sequence-flow-edge-ids.ts
440
+ /**
441
+ * Resolve the concrete edge/message id for hop `i` of a flow cue.
442
+ * `edgeIds` is authoritative and index-aligned with hops; lone `edgeId` only
443
+ * covers hop 0 (single-hop convenience / auto-anim).
444
+ */
445
+ function flowHopEdgeId(hopIndex, refs) {
446
+ if (refs.edgeIds && hopIndex < refs.edgeIds.length) return refs.edgeIds[hopIndex];
447
+ if (hopIndex === 0) return refs.edgeId;
448
+ }
449
+ /** Attach concrete sequence message ids to authored `flow` cues (in place-safe copy). */
450
+ function bindSequenceFlowEdgeIds(graph, cues) {
451
+ const seq = graph.sequence;
452
+ if (!seq) return cues;
453
+ const messages = seq.messages.filter((m) => m.from && m.to && m.kind !== "destroy").slice().sort((a, b) => a.order - b.order);
454
+ const used = /* @__PURE__ */ new Set();
455
+ const take = (from, to) => {
456
+ for (const m of messages) {
457
+ if (used.has(m.id) || m.from !== from || m.to !== to) continue;
458
+ used.add(m.id);
459
+ return m.id;
460
+ }
461
+ };
462
+ const mapCue = (cue) => {
463
+ if (cue.op === "parallel") return {
464
+ ...cue,
465
+ cues: cue.cues.map(mapCue)
466
+ };
467
+ if (cue.op !== "flow") return cue;
468
+ if (cue.edgeId || cue.edgeIds && cue.edgeIds.length > 0) return cue;
469
+ if (cue.path.length < 2) return cue;
470
+ const hops = cue.path.length - 1;
471
+ const edgeIds = [];
472
+ for (let i = 0; i < hops; i++) {
473
+ const id = take(cue.path[i], cue.path[i + 1]);
474
+ if (!id) return cue;
475
+ edgeIds.push(id);
476
+ }
477
+ return {
478
+ ...cue,
479
+ edgeId: edgeIds[0],
480
+ edgeIds
481
+ };
482
+ };
483
+ return cues.map(mapCue);
484
+ }
485
+ //#endregion
486
+ //#region src/animation/index.ts
487
+ /** Slugify an authored animation name into a stable id. */
488
+ function animationIdFromName(name) {
489
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "animation";
490
+ }
491
+ //#endregion
492
+ //#region src/types/cardinality.ts
493
+ const ALIASES = {
494
+ "1": "one",
495
+ one: "one",
496
+ exactlyone: "one",
497
+ "0..1": "zeroOrOne",
498
+ "0:1": "zeroOrOne",
499
+ "01": "zeroOrOne",
500
+ zeroorone: "zeroOrOne",
501
+ optional: "zeroOrOne",
502
+ "1..n": "oneOrMany",
503
+ "1..*": "oneOrMany",
504
+ "1:n": "oneOrMany",
505
+ "1:*": "oneOrMany",
506
+ n: "oneOrMany",
507
+ many: "oneOrMany",
508
+ "+": "oneOrMany",
509
+ oneormany: "oneOrMany",
510
+ "0..n": "zeroOrMany",
511
+ "0..*": "zeroOrMany",
512
+ "0:n": "zeroOrMany",
513
+ "0:*": "zeroOrMany",
514
+ "*": "zeroOrMany",
515
+ zeroormany: "zeroOrMany"
516
+ };
517
+ function normalizeToken(raw) {
518
+ const key = raw.trim().toLowerCase().replace(/\s+/g, "");
519
+ return ALIASES[key];
520
+ }
521
+ /**
522
+ * Parse cardinality from a property or label.
523
+ *
524
+ * Forms:
525
+ * - `"1:N"` / `"1:0..N"` / `"0..1:N"` (from:to)
526
+ * - Mermaid-ish `"||--o{"` / `"}o--||"`
527
+ * - Single side `"N"` (applied to target; source defaults to `one`)
528
+ */
529
+ function parseCardinality(raw) {
530
+ if (raw == null || raw === "") return void 0;
531
+ if (typeof raw !== "string" && typeof raw !== "number" && typeof raw !== "boolean") return;
532
+ const text = String(raw).trim();
533
+ if (!text) return void 0;
534
+ const mermaid = parseMermaidCardinality(text);
535
+ if (mermaid) return mermaid;
536
+ const parts = text.split(/[:|]/).map((p) => p.trim()).filter(Boolean);
537
+ if (parts.length >= 2) {
538
+ const from = normalizeToken(parts[0]);
539
+ const to = normalizeToken(parts[1]);
540
+ if (from && to) return {
541
+ from,
542
+ to
543
+ };
544
+ }
545
+ const single = normalizeToken(text);
546
+ if (single) return {
547
+ from: "one",
548
+ to: single
549
+ };
550
+ }
551
+ /**
552
+ * True when a label only encodes cardinality (crow's-foot already conveys it).
553
+ * Shared by finalize (label placement) and SVG (hide duplicate text).
554
+ */
555
+ function isPureCardinalityLabel(label) {
556
+ if (!label) return true;
557
+ const t = label.trim();
558
+ if (!t) return true;
559
+ if (parseMermaidCardinality(t)) return true;
560
+ if (/^(?:1|0\.\.1|1\.\.N|0\.\.N|N|\*):\s*(?:1|0\.\.1|1\.\.N|0\.\.N|N|\*)$/i.test(t)) return true;
561
+ if (/^(?:\d+\.\.[Nn*]|\d+:[Nn*]|\*|[Nn]|0\.\.[Nn*])$/i.test(t)) return true;
562
+ if (!parseCardinality(t)) return false;
563
+ return !/[a-z]/i.test(t.replace(/n/gi, ""));
564
+ }
565
+ function cardinalityLabel(c) {
566
+ return `${formatSide(c.from)}:${formatSide(c.to)}`;
567
+ }
568
+ function parseMermaidCardinality(text) {
569
+ const match = text.replace(/\s+/g, "").match(/^([|}{o]+)--([|}{o]+)$/);
570
+ if (!match) return void 0;
571
+ const from = mermaidEnd(match[1]);
572
+ const to = mermaidEnd(match[2]);
573
+ if (!from || !to) return void 0;
574
+ return {
575
+ from,
576
+ to
577
+ };
578
+ }
579
+ function mermaidEnd(token) {
580
+ if (token === "}o" || token === "o{") return "zeroOrMany";
581
+ if (token === "}|" || token === "|{") return "oneOrMany";
582
+ if (token === "||") return "one";
583
+ if (token === "o|" || token === "|o") return "zeroOrOne";
584
+ if (token === "|") return "one";
585
+ if (token === "o") return "zeroOrOne";
586
+ }
587
+ function formatSide(c) {
588
+ switch (c) {
589
+ case "one": return "1";
590
+ case "zeroOrOne": return "0..1";
591
+ case "oneOrMany": return "1..N";
592
+ case "zeroOrMany": return "0..N";
593
+ }
594
+ }
595
+ /** Default IE cardinality for a FK from parent → child. */
596
+ function fkCardinality(fkNotNull) {
597
+ return {
598
+ from: fkNotNull ? "one" : "zeroOrOne",
599
+ to: "zeroOrMany"
600
+ };
601
+ }
602
+ //#endregion
603
+ //#region src/types/table.ts
604
+ const FLAG_TOKENS = /* @__PURE__ */ new Set([
605
+ "PK",
606
+ "PRIMARY",
607
+ "FK",
608
+ "FOREIGN",
609
+ "UK",
610
+ "UNIQUE",
611
+ "NN",
612
+ "NOTNULL",
613
+ "NOT_NULL"
614
+ ]);
615
+ /**
616
+ * Parse a compact column spec string into a structured column.
617
+ *
618
+ * Accepted forms (whitespace-separated):
619
+ * - `"id PK uuid"`
620
+ * - `"email text UK NN"`
621
+ * - `"customer_id : uuid FK"`
622
+ * - `"customer_id FK uuid -> customers.id"`
623
+ * - `"status text NN // order lifecycle"` (note after //)
624
+ *
625
+ * Flags: PK, FK, UK/UNIQUE, NN/NOT NULL/NOTNULL (case-insensitive).
626
+ */
627
+ function parseTableColumnSpec(raw) {
628
+ let trimmed = raw.trim();
629
+ if (!trimmed) return null;
630
+ let note;
631
+ const noteSplit = trimmed.split(/\s+\/\/\s+/);
632
+ if (noteSplit.length > 1) {
633
+ trimmed = noteSplit[0].trim();
634
+ note = noteSplit.slice(1).join(" // ").trim() || void 0;
635
+ }
636
+ let references;
637
+ const refMatch = trimmed.match(/^(.*?)\s*->\s*([A-Za-z_][\w-]*)\.([A-Za-z_][\w-]*)\s*$/);
638
+ if (refMatch) {
639
+ trimmed = refMatch[1].trim();
640
+ references = {
641
+ table: refMatch[2],
642
+ column: refMatch[3]
643
+ };
644
+ } else {
645
+ const refWord = trimmed.match(/^(.*?)\s+(?:ref|references)\s+([A-Za-z_][\w-]*)\.([A-Za-z_][\w-]*)\s*$/i);
646
+ if (refWord) {
647
+ trimmed = refWord[1].trim();
648
+ references = {
649
+ table: refWord[2],
650
+ column: refWord[3]
651
+ };
652
+ }
653
+ }
654
+ const parts = trimmed.replace(/\s*:\s*/g, " ").split(/\s+/).filter(Boolean);
655
+ if (parts.length === 0) return null;
656
+ const name = parts[0];
657
+ const keys = [];
658
+ let notNull = false;
659
+ const typeParts = [];
660
+ for (let i = 1; i < parts.length; i++) {
661
+ const token = parts[i];
662
+ const upper = token.toUpperCase();
663
+ if (upper === "NOT" && parts[i + 1]?.toUpperCase() === "NULL") {
664
+ notNull = true;
665
+ i++;
666
+ continue;
667
+ }
668
+ switch (upper) {
669
+ case "PK":
670
+ case "PRIMARY":
671
+ if (!keys.includes("pk")) keys.push("pk");
672
+ break;
673
+ case "FK":
674
+ case "FOREIGN":
675
+ if (!keys.includes("fk")) keys.push("fk");
676
+ break;
677
+ case "UK":
678
+ case "UNIQUE":
679
+ if (!keys.includes("uk")) keys.push("uk");
680
+ break;
681
+ case "NN":
682
+ case "NOTNULL":
683
+ case "NOT_NULL":
684
+ notNull = true;
685
+ break;
686
+ default:
687
+ if (!FLAG_TOKENS.has(upper)) typeParts.push(token);
688
+ break;
689
+ }
690
+ }
691
+ if (references && !keys.includes("fk")) keys.push("fk");
692
+ return {
693
+ name,
694
+ type: typeParts.length ? typeParts.join(" ") : void 0,
695
+ keys,
696
+ notNull: notNull || void 0,
697
+ references,
698
+ note
699
+ };
700
+ }
701
+ function parseTableColumns(raw) {
702
+ if (!Array.isArray(raw)) return [];
703
+ const columns = [];
704
+ for (const item of raw) {
705
+ const col = parseTableColumnSpec(String(item));
706
+ if (col) columns.push(col);
707
+ }
708
+ return columns;
709
+ }
710
+ function findColumnIndex(columns, name) {
711
+ if (!columns) return -1;
712
+ return columns.findIndex((c) => c.name === name);
713
+ }
714
+ /**
715
+ * Format a structured column as a highlightable line:
716
+ * `customer_id: uuid FK NN -> customers.id // buyer`
717
+ */
718
+ function formatTableColumnLine(col) {
719
+ const flags = [];
720
+ for (const key of col.keys) flags.push(key.toUpperCase());
721
+ if (col.notNull) flags.push("NN");
722
+ const rhs = [];
723
+ if (col.type) rhs.push(col.type);
724
+ rhs.push(...flags);
725
+ if (col.references) rhs.push(`-> ${col.references.table}.${col.references.column}`);
726
+ let line = col.name;
727
+ if (rhs.length) line += `: ${rhs.join(" ")}`;
728
+ if (col.note) line += ` // ${col.note}`;
729
+ return line;
730
+ }
731
+ //#endregion
732
+ //#region src/types/sequence.ts
733
+ /** Map authored keyword (including aliases) → canonical operator. */
734
+ const SEQUENCE_FRAGMENT_ALIASES = {
735
+ alt: "alternate",
736
+ alternate: "alternate",
737
+ opt: "optional",
738
+ optional: "optional",
739
+ loop: "loop",
740
+ par: "parallel",
741
+ parallel: "parallel",
742
+ critical: "critical",
743
+ break: "break",
744
+ group: "section",
745
+ section: "section",
746
+ box: "section"
747
+ };
748
+ /** Title-case label painted on the fragment frame (newcomer-friendly). */
749
+ function sequenceFragmentDisplayName(operator) {
750
+ switch (operator) {
751
+ case "alternate": return "Alternate";
752
+ case "optional": return "Optional";
753
+ case "loop": return "Loop";
754
+ case "parallel": return "Parallel";
755
+ case "critical": return "Critical";
756
+ case "break": return "Break";
757
+ case "section": return "Section";
758
+ }
759
+ }
760
+ function normalizeSequenceFragmentOperator(word) {
761
+ return SEQUENCE_FRAGMENT_ALIASES[word.toLowerCase()];
762
+ }
763
+ //#endregion
764
+ //#region src/parser/edge-ops.ts
765
+ /** Edge operators — longest first so `<->` / `<..` / `<~` / `-->` win over shorter prefixes. */
766
+ const EDGE_OPS = [
767
+ "<->",
768
+ "<..",
769
+ "<~",
770
+ "<=",
771
+ "<-",
772
+ "x-",
773
+ "=>",
774
+ "~>",
775
+ "..>",
776
+ "-->",
777
+ "-x",
778
+ "->",
779
+ "--"
780
+ ];
781
+ /** Escape for TextMate / Shiki character classes (e.g. `.` in `..>`). */
782
+ function edgeOpsPattern() {
783
+ return EDGE_OPS.map((op) => op.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
784
+ }
785
+ //#endregion
786
+ //#region src/parser/lexer.ts
787
+ /** Soft keywords — statement intros; lexer emits them as identifiers. */
788
+ const STATEMENT_KEYWORDS = /* @__PURE__ */ new Set([
789
+ "diagram",
790
+ "state",
791
+ "sequence",
792
+ "kdiagram",
793
+ "group",
794
+ "boundary",
795
+ "zone",
796
+ "swimlane",
797
+ "direction",
798
+ "density",
799
+ "layout",
800
+ "edges",
801
+ "render",
802
+ "presentation",
803
+ "animation",
804
+ "style",
805
+ "is",
806
+ "for",
807
+ "edge",
808
+ "svg",
809
+ "activate",
810
+ "deactivate",
811
+ "create",
812
+ "destroy",
813
+ "note",
814
+ "autonumber",
815
+ "alt",
816
+ "alternate",
817
+ "else",
818
+ "otherwise",
819
+ "opt",
820
+ "optional",
821
+ "loop",
822
+ "par",
823
+ "parallel",
824
+ "and",
825
+ "also",
826
+ "critical",
827
+ "break",
828
+ "section",
829
+ "box",
830
+ "divider"
831
+ ]);
832
+ function tokenize(source, diagnostics = []) {
833
+ const tokens = [];
834
+ let i = 0;
835
+ let line = 1;
836
+ let column = 1;
837
+ const makeRange = (start, end) => {
838
+ const startLine = source.slice(0, start).split("\n").length;
839
+ const startCol = start - (source.lastIndexOf("\n", start - 1) + 1) + 1;
840
+ const endLine = source.slice(0, end).split("\n").length;
841
+ const endCol = end - (source.lastIndexOf("\n", end - 1) + 1) + 1;
842
+ return {
843
+ start: {
844
+ line: startLine,
845
+ column: startCol,
846
+ offset: start
847
+ },
848
+ end: {
849
+ line: endLine,
850
+ column: endCol,
851
+ offset: end
852
+ }
853
+ };
854
+ };
855
+ while (i < source.length) {
856
+ const start = i;
857
+ if (source[i] === "\n") {
858
+ tokens.push({
859
+ type: "newline",
860
+ value: "\n",
861
+ range: makeRange(start, ++i)
862
+ });
863
+ line++;
864
+ column = 1;
865
+ continue;
866
+ }
867
+ if (source[i] === " " || source[i] === " " || source[i] === "\r" || source[i] === ";") {
868
+ i++;
869
+ column++;
870
+ continue;
871
+ }
872
+ if (source.slice(i, i + 2) === "//") {
873
+ i += 2;
874
+ const commentStart = i;
875
+ while (i < source.length && source[i] !== "\n") i++;
876
+ tokens.push({
877
+ type: "comment",
878
+ value: source.slice(commentStart, i).trim(),
879
+ range: makeRange(start, i)
880
+ });
881
+ continue;
882
+ }
883
+ if (source.slice(i, i + 2) === "/*") {
884
+ i += 2;
885
+ while (i < source.length && source.slice(i, i + 2) !== "*/") i++;
886
+ if (i < source.length) i += 2;
887
+ continue;
888
+ }
889
+ if (source[i] === "[") {
890
+ tokens.push({
891
+ type: "lbracket",
892
+ value: "[",
893
+ range: makeRange(start, ++i)
894
+ });
895
+ column++;
896
+ continue;
897
+ }
898
+ if (source[i] === "]") {
899
+ tokens.push({
900
+ type: "rbracket",
901
+ value: "]",
902
+ range: makeRange(start, ++i)
903
+ });
904
+ column++;
905
+ continue;
906
+ }
907
+ if (source[i] === "(") {
908
+ tokens.push({
909
+ type: "lparen",
910
+ value: "(",
911
+ range: makeRange(start, ++i)
912
+ });
913
+ column++;
914
+ continue;
915
+ }
916
+ if (source[i] === ")") {
917
+ tokens.push({
918
+ type: "rparen",
919
+ value: ")",
920
+ range: makeRange(start, ++i)
921
+ });
922
+ column++;
923
+ continue;
924
+ }
925
+ if (source[i] === "{") {
926
+ tokens.push({
927
+ type: "lbrace",
928
+ value: "{",
929
+ range: makeRange(start, ++i)
930
+ });
931
+ column++;
932
+ continue;
933
+ }
934
+ if (source[i] === "}") {
935
+ tokens.push({
936
+ type: "rbrace",
937
+ value: "}",
938
+ range: makeRange(start, ++i)
939
+ });
940
+ column++;
941
+ continue;
942
+ }
943
+ if (source[i] === ":") {
944
+ tokens.push({
945
+ type: "colon",
946
+ value: ":",
947
+ range: makeRange(start, ++i)
948
+ });
949
+ column++;
950
+ continue;
951
+ }
952
+ if (source[i] === ",") {
953
+ tokens.push({
954
+ type: "comma",
955
+ value: ",",
956
+ range: makeRange(start, ++i)
957
+ });
958
+ column++;
959
+ continue;
960
+ }
961
+ if (source[i] === "*") {
962
+ tokens.push({
963
+ type: "star",
964
+ value: "*",
965
+ range: makeRange(start, ++i)
966
+ });
967
+ column++;
968
+ continue;
969
+ }
970
+ if (source[i] === "#") {
971
+ let j = i + 1;
972
+ while (j < source.length && /[0-9a-fA-F]/.test(source[j])) j++;
973
+ const hexLen = j - i - 1;
974
+ if (hexLen === 3 || hexLen === 4 || hexLen === 6 || hexLen === 8) {
975
+ tokens.push({
976
+ type: "identifier",
977
+ value: source.slice(i, j),
978
+ range: makeRange(start, j)
979
+ });
980
+ column += j - i;
981
+ i = j;
982
+ continue;
983
+ }
984
+ diagnostics.push({
985
+ severity: "error",
986
+ code: "FM006",
987
+ message: `Unexpected character "#"`,
988
+ range: makeRange(start, start + 1),
989
+ hint: "Use a valid hex color (#rgb, #rrggbb, #rgba, or #rrggbbaa)"
990
+ });
991
+ i++;
992
+ column++;
993
+ continue;
994
+ }
995
+ let matchedEdge = false;
996
+ for (const op of EDGE_OPS) {
997
+ if (source.slice(i, i + op.length) !== op) continue;
998
+ if (op === "--") {
999
+ const next = source[i + 2];
1000
+ if (next && /[A-Za-z0-9_-]/.test(next)) continue;
1001
+ }
1002
+ tokens.push({
1003
+ type: "edgeOp",
1004
+ value: op,
1005
+ range: makeRange(start, i + op.length)
1006
+ });
1007
+ i += op.length;
1008
+ column += op.length;
1009
+ matchedEdge = true;
1010
+ break;
1011
+ }
1012
+ if (matchedEdge) continue;
1013
+ if (source[i] === ".") {
1014
+ tokens.push({
1015
+ type: "dot",
1016
+ value: ".",
1017
+ range: makeRange(start, ++i)
1018
+ });
1019
+ column++;
1020
+ continue;
1021
+ }
1022
+ if (source[i] === "\"") {
1023
+ i++;
1024
+ let value = "";
1025
+ while (i < source.length && source[i] !== "\"") {
1026
+ if (source[i] === "\\" && i + 1 < source.length) value += source[++i];
1027
+ else value += source[i];
1028
+ i++;
1029
+ }
1030
+ if (i >= source.length || source[i] !== "\"") diagnostics.push({
1031
+ severity: "error",
1032
+ code: "FM007",
1033
+ message: "Unclosed string literal",
1034
+ range: makeRange(start, i)
1035
+ });
1036
+ else i++;
1037
+ tokens.push({
1038
+ type: "string",
1039
+ value,
1040
+ range: makeRange(start, i)
1041
+ });
1042
+ continue;
1043
+ }
1044
+ if (/[0-9]/.test(source[i])) {
1045
+ let value = "";
1046
+ while (i < source.length && /[0-9]/.test(source[i])) value += source[i++];
1047
+ if (source[i] === "." && /[0-9]/.test(source[i + 1] ?? "")) {
1048
+ value += source[i++];
1049
+ while (i < source.length && /[0-9]/.test(source[i])) value += source[i++];
1050
+ }
1051
+ tokens.push({
1052
+ type: "number",
1053
+ value,
1054
+ range: makeRange(start, i)
1055
+ });
1056
+ continue;
1057
+ }
1058
+ if (source[i] === "-" && source[i + 1] === "-" && /[a-zA-Z_]/.test(source[i + 2] ?? "")) {
1059
+ let value = "--";
1060
+ i += 2;
1061
+ while (i < source.length && /[a-zA-Z0-9_-]/.test(source[i])) value += source[i++];
1062
+ tokens.push({
1063
+ type: "identifier",
1064
+ value,
1065
+ range: makeRange(start, i)
1066
+ });
1067
+ continue;
1068
+ }
1069
+ if (/[a-zA-Z_]/.test(source[i])) {
1070
+ let value = "";
1071
+ while (i < source.length && /[a-zA-Z0-9_-]/.test(source[i])) value += source[i++];
1072
+ tokens.push({
1073
+ type: "identifier",
1074
+ value,
1075
+ range: makeRange(start, i)
1076
+ });
1077
+ continue;
1078
+ }
1079
+ diagnostics.push({
1080
+ severity: "error",
1081
+ code: "FM006",
1082
+ message: `Unexpected character "${source[i]}"`,
1083
+ range: makeRange(start, start + 1)
1084
+ });
1085
+ i++;
1086
+ column++;
1087
+ }
1088
+ tokens.push({
1089
+ type: "eof",
1090
+ value: "",
1091
+ range: makeRange(source.length, source.length)
1092
+ });
1093
+ return tokens;
1094
+ }
1095
+ //#endregion
1096
+ //#region src/parser/parser.ts
1097
+ function parse(source) {
1098
+ const diagnostics = [];
1099
+ return new Parser(tokenize(source, diagnostics), diagnostics, source).parseDocument();
1100
+ }
1101
+ var Parser = class {
1102
+ constructor(tokens, diagnostics, source) {
1103
+ this.tokens = tokens;
1104
+ this.diagnostics = diagnostics;
1105
+ this.source = source;
1106
+ this.pos = 0;
1107
+ }
1108
+ peek(offset = 0) {
1109
+ return this.tokens[this.pos + offset] ?? this.tokens[this.tokens.length - 1];
1110
+ }
1111
+ advance() {
1112
+ return this.tokens[this.pos++] ?? this.tokens[this.tokens.length - 1];
1113
+ }
1114
+ at(type, value) {
1115
+ const t = this.peek();
1116
+ return t.type === type && (value === void 0 || t.value === value);
1117
+ }
1118
+ /** Soft keyword / identifier word match. */
1119
+ atWord(value) {
1120
+ const t = this.peek();
1121
+ return t.type === "identifier" && t.value === value;
1122
+ }
1123
+ skipNewlines() {
1124
+ this.skipTrivia();
1125
+ }
1126
+ /** Skip newlines and line comments (trivia). */
1127
+ skipTrivia() {
1128
+ while (this.at("newline") || this.at("comment")) this.advance();
1129
+ }
1130
+ error(code, message, range, hint) {
1131
+ this.diagnostics.push({
1132
+ severity: "error",
1133
+ code,
1134
+ message,
1135
+ range,
1136
+ hint
1137
+ });
1138
+ }
1139
+ parseDocument() {
1140
+ this.skipNewlines();
1141
+ let version;
1142
+ if (this.atWord("kdiagram")) {
1143
+ this.advance();
1144
+ const num = this.advance();
1145
+ if (num.type === "number") version = Number(num.value);
1146
+ this.skipNewlines();
1147
+ }
1148
+ const body = [];
1149
+ while (!this.at("eof")) {
1150
+ this.skipNewlines();
1151
+ if (this.at("eof")) break;
1152
+ if (this.atWord("diagram")) body.push(this.parseDiagram("flow"));
1153
+ else if (this.atWord("state")) body.push(this.parseDiagram("state"));
1154
+ else if (this.atWord("sequence")) body.push(this.parseSequence());
1155
+ else {
1156
+ this.error("FM001", `Unexpected token "${this.peek().value}"`, this.peek().range);
1157
+ this.advance();
1158
+ }
1159
+ this.skipNewlines();
1160
+ }
1161
+ return {
1162
+ type: "Document",
1163
+ version,
1164
+ body,
1165
+ diagnostics: this.diagnostics
1166
+ };
1167
+ }
1168
+ parseSequence() {
1169
+ const start = this.peek().range.start;
1170
+ this.advance();
1171
+ this.skipTrivia();
1172
+ let name;
1173
+ if (this.at("string")) {
1174
+ name = this.advance().value;
1175
+ this.skipTrivia();
1176
+ }
1177
+ if (!this.at("lbrace")) this.error("FM002", "Expected \"{\" to open sequence", this.peek().range);
1178
+ else this.advance();
1179
+ const statements = [];
1180
+ while (!this.at("rbrace") && !this.at("eof")) {
1181
+ this.skipNewlines();
1182
+ if (this.at("rbrace") || this.at("eof")) break;
1183
+ const stmt = this.parseSequenceStatement();
1184
+ if (!stmt) continue;
1185
+ if (Array.isArray(stmt)) statements.push(...stmt);
1186
+ else statements.push(stmt);
1187
+ }
1188
+ if (!this.at("rbrace")) this.error("FM002", "Expected \"}\" to close sequence", this.peek().range);
1189
+ else this.advance();
1190
+ const end = this.tokens[Math.min(this.pos, this.tokens.length - 1)].range.end;
1191
+ return {
1192
+ type: "Sequence",
1193
+ name,
1194
+ statements,
1195
+ range: {
1196
+ start,
1197
+ end
1198
+ }
1199
+ };
1200
+ }
1201
+ parseSequenceStatement() {
1202
+ if (this.at("identifier") && this.peek(1).type === "colon") {
1203
+ const id = this.advance();
1204
+ this.advance();
1205
+ return this.parseNode(id.value, id.range);
1206
+ }
1207
+ if (this.atWord("activate")) return this.parseSequenceActivate();
1208
+ if (this.atWord("deactivate")) return this.parseSequenceDeactivate();
1209
+ if (this.atWord("create")) return this.parseSequenceCreate();
1210
+ if (this.atWord("destroy")) return this.parseSequenceDestroy();
1211
+ if (this.atWord("note")) return this.parseSequenceNote();
1212
+ if (this.atWord("autonumber")) return {
1213
+ type: "SequenceAutonumber",
1214
+ range: this.advance().range
1215
+ };
1216
+ if (this.atWord("divider")) return this.parseSequenceDivider();
1217
+ if (this.atWord("alt") || this.atWord("alternate") || this.atWord("opt") || this.atWord("optional") || this.atWord("loop") || this.atWord("par") || this.atWord("parallel") || this.atWord("critical") || this.atWord("break") || this.atWord("group") || this.atWord("section") || this.atWord("box")) return this.parseSequenceFragment();
1218
+ if (this.atWord("style")) return this.parseStyle();
1219
+ if (this.atWord("direction") || this.atWord("density")) return this.parseDirective();
1220
+ if (this.atWord("layout")) return this.parseLayoutBlock();
1221
+ if (this.atWord("edges")) return this.parseEdgePolicyBlock();
1222
+ if (this.atWord("render")) return this.parseRenderBlock();
1223
+ if (this.atWord("presentation")) return this.parsePresentationBlock();
1224
+ if (this.atWord("animation")) return this.parseAnimationBlock();
1225
+ if (this.at("star") && this.peek(1).type === "edgeOp") {
1226
+ const star = this.advance();
1227
+ return this.parseEdgeChain("*", void 0, star.range);
1228
+ }
1229
+ if (this.at("identifier")) {
1230
+ if (this.looksLikeStyleRef()) return this.parseStyleRefStatement();
1231
+ const id = this.advance();
1232
+ if (this.at("dot") || this.at("edgeOp")) {
1233
+ const from = this.parseQualifiedName(id.value, id.range);
1234
+ return this.parseEdgeChain(from.nodeId, from.column, id.range);
1235
+ }
1236
+ this.error("FM003", `Unexpected identifier "${id.value}"`, id.range);
1237
+ return null;
1238
+ }
1239
+ this.error("FM004", `Unexpected token "${this.peek().value}"`, this.peek().range);
1240
+ this.advance();
1241
+ return null;
1242
+ }
1243
+ parseSequenceActivate() {
1244
+ const startTok = this.advance();
1245
+ const idTok = this.advance();
1246
+ if (idTok.type !== "identifier") {
1247
+ this.error("FM140", "Expected participant id after activate", idTok.range);
1248
+ return {
1249
+ type: "SequenceActivate",
1250
+ participantId: "",
1251
+ range: startTok.range
1252
+ };
1253
+ }
1254
+ return {
1255
+ type: "SequenceActivate",
1256
+ participantId: idTok.value,
1257
+ range: {
1258
+ start: startTok.range.start,
1259
+ end: idTok.range.end
1260
+ }
1261
+ };
1262
+ }
1263
+ parseSequenceDeactivate() {
1264
+ const startTok = this.advance();
1265
+ const idTok = this.advance();
1266
+ if (idTok.type !== "identifier") {
1267
+ this.error("FM141", "Expected participant id after deactivate", idTok.range);
1268
+ return {
1269
+ type: "SequenceDeactivate",
1270
+ participantId: "",
1271
+ range: startTok.range
1272
+ };
1273
+ }
1274
+ return {
1275
+ type: "SequenceDeactivate",
1276
+ participantId: idTok.value,
1277
+ range: {
1278
+ start: startTok.range.start,
1279
+ end: idTok.range.end
1280
+ }
1281
+ };
1282
+ }
1283
+ parseSequenceCreate() {
1284
+ const startTok = this.advance();
1285
+ this.skipTrivia();
1286
+ if (!(this.at("identifier") && this.peek(1).type === "colon")) {
1287
+ this.error("FM142", "Expected `create id: kind \"Label\"`", this.peek().range);
1288
+ return {
1289
+ type: "SequenceCreate",
1290
+ node: {
1291
+ type: "Node",
1292
+ id: "",
1293
+ kind: "participant",
1294
+ properties: {},
1295
+ styleRefs: [],
1296
+ range: startTok.range
1297
+ },
1298
+ range: startTok.range
1299
+ };
1300
+ }
1301
+ const id = this.advance();
1302
+ this.advance();
1303
+ const node = this.parseNode(id.value, id.range);
1304
+ return {
1305
+ type: "SequenceCreate",
1306
+ node,
1307
+ range: {
1308
+ start: startTok.range.start,
1309
+ end: node.range.end
1310
+ }
1311
+ };
1312
+ }
1313
+ parseSequenceDestroy() {
1314
+ const startTok = this.advance();
1315
+ const idTok = this.advance();
1316
+ if (idTok.type !== "identifier") {
1317
+ this.error("FM143", "Expected participant id after destroy", idTok.range);
1318
+ return {
1319
+ type: "SequenceDestroy",
1320
+ participantId: "",
1321
+ range: startTok.range
1322
+ };
1323
+ }
1324
+ return {
1325
+ type: "SequenceDestroy",
1326
+ participantId: idTok.value,
1327
+ range: {
1328
+ start: startTok.range.start,
1329
+ end: idTok.range.end
1330
+ }
1331
+ };
1332
+ }
1333
+ parseSequenceNote() {
1334
+ const startTok = this.advance();
1335
+ this.skipTrivia();
1336
+ let placement = "over";
1337
+ if (this.atWord("over")) {
1338
+ this.advance();
1339
+ placement = "over";
1340
+ } else if (this.atWord("left")) {
1341
+ this.advance();
1342
+ this.skipTrivia();
1343
+ if (this.atWord("of")) this.advance();
1344
+ placement = "left";
1345
+ } else if (this.atWord("right")) {
1346
+ this.advance();
1347
+ this.skipTrivia();
1348
+ if (this.atWord("of")) this.advance();
1349
+ placement = "right";
1350
+ } else this.error("FM144", "Expected note placement (over | left of | right of)", this.peek().range);
1351
+ this.skipTrivia();
1352
+ const participantIds = [];
1353
+ while (this.at("identifier")) {
1354
+ participantIds.push(this.advance().value);
1355
+ this.skipTrivia();
1356
+ if (this.at("comma")) {
1357
+ this.advance();
1358
+ this.skipTrivia();
1359
+ continue;
1360
+ }
1361
+ break;
1362
+ }
1363
+ let text = "";
1364
+ if (this.at("string")) text = this.advance().value;
1365
+ else this.error("FM145", "Expected note text string", this.peek().range);
1366
+ return {
1367
+ type: "SequenceNote",
1368
+ placement,
1369
+ participantIds,
1370
+ text,
1371
+ range: {
1372
+ start: startTok.range.start,
1373
+ end: this.peek(-1)?.range.end ?? startTok.range.end
1374
+ }
1375
+ };
1376
+ }
1377
+ parseSequenceDivider() {
1378
+ const startTok = this.advance();
1379
+ this.skipTrivia();
1380
+ let label;
1381
+ if (this.at("string")) label = this.advance().value;
1382
+ return {
1383
+ type: "SequenceDivider",
1384
+ label,
1385
+ range: {
1386
+ start: startTok.range.start,
1387
+ end: this.peek(-1)?.range.end ?? startTok.range.end
1388
+ }
1389
+ };
1390
+ }
1391
+ parseSequenceFragment() {
1392
+ const startTok = this.peek();
1393
+ const operator = normalizeSequenceFragmentOperator(startTok.value) ?? "section";
1394
+ this.advance();
1395
+ this.skipTrivia();
1396
+ let label;
1397
+ if (this.at("string")) {
1398
+ label = this.advance().value;
1399
+ this.skipTrivia();
1400
+ }
1401
+ const styleRefs = this.parseOptionalIsStyleRefs();
1402
+ this.skipTrivia();
1403
+ const operands = [];
1404
+ const first = this.parseSequenceFragmentBody(label, styleRefs);
1405
+ operands.push(first);
1406
+ while (operator === "alternate" && (this.atWord("else") || this.atWord("otherwise")) || operator === "parallel" && (this.atWord("and") || this.atWord("also"))) {
1407
+ this.advance();
1408
+ this.skipTrivia();
1409
+ let opLabel;
1410
+ if (this.at("string")) {
1411
+ opLabel = this.advance().value;
1412
+ this.skipTrivia();
1413
+ }
1414
+ const opStyleRefs = this.parseOptionalIsStyleRefs();
1415
+ this.skipTrivia();
1416
+ operands.push(this.parseSequenceFragmentBody(opLabel, opStyleRefs));
1417
+ }
1418
+ return {
1419
+ type: "SequenceFragment",
1420
+ operator,
1421
+ label,
1422
+ styleRefs,
1423
+ operands,
1424
+ range: {
1425
+ start: startTok.range.start,
1426
+ end: this.peek(-1)?.range.end ?? startTok.range.end
1427
+ }
1428
+ };
1429
+ }
1430
+ parseSequenceFragmentBody(label, styleRefs = []) {
1431
+ const start = this.peek().range.start;
1432
+ if (!this.at("lbrace")) {
1433
+ this.error("FM146", "Expected \"{\" to open fragment body", this.peek().range);
1434
+ return {
1435
+ label,
1436
+ styleRefs,
1437
+ statements: [],
1438
+ range: this.peek().range
1439
+ };
1440
+ }
1441
+ this.advance();
1442
+ const statements = [];
1443
+ while (!this.at("rbrace") && !this.at("eof")) {
1444
+ this.skipNewlines();
1445
+ if (this.at("rbrace") || this.at("eof")) break;
1446
+ const stmt = this.parseSequenceStatement();
1447
+ if (!stmt) continue;
1448
+ if (Array.isArray(stmt)) statements.push(...stmt);
1449
+ else statements.push(stmt);
1450
+ }
1451
+ if (!this.at("rbrace")) this.error("FM146", "Expected \"}\" to close fragment body", this.peek().range);
1452
+ else this.advance();
1453
+ return {
1454
+ label,
1455
+ styleRefs,
1456
+ statements,
1457
+ range: {
1458
+ start,
1459
+ end: this.peek(-1)?.range.end ?? start
1460
+ }
1461
+ };
1462
+ }
1463
+ parseDiagram(diagramKind) {
1464
+ const start = this.peek().range.start;
1465
+ this.advance();
1466
+ this.skipTrivia();
1467
+ let name;
1468
+ if (this.at("string")) {
1469
+ name = this.advance().value;
1470
+ this.skipTrivia();
1471
+ }
1472
+ if (!this.at("lbrace")) this.error("FM002", `Expected "{" to open ${diagramKind} diagram`, this.peek().range);
1473
+ else this.advance();
1474
+ const statements = [];
1475
+ while (!this.at("rbrace") && !this.at("eof")) {
1476
+ this.skipNewlines();
1477
+ if (this.at("rbrace") || this.at("eof")) break;
1478
+ const stmt = this.parseStatement(true);
1479
+ if (!stmt) continue;
1480
+ if (Array.isArray(stmt)) statements.push(...stmt);
1481
+ else statements.push(stmt);
1482
+ }
1483
+ if (!this.at("rbrace")) this.error("FM002", "Expected \"}\" to close diagram", this.peek().range);
1484
+ else this.advance();
1485
+ const end = this.tokens[Math.min(this.pos, this.tokens.length - 1)].range.end;
1486
+ return {
1487
+ type: "Diagram",
1488
+ diagramKind,
1489
+ name,
1490
+ statements,
1491
+ range: {
1492
+ start,
1493
+ end
1494
+ }
1495
+ };
1496
+ }
1497
+ parseStatement(inGroup = false) {
1498
+ if (this.at("identifier") && this.peek(1).type === "colon") {
1499
+ if (inGroup && isGroupLayoutHint(this.peek().value)) return this.parseDirective();
1500
+ const id = this.advance();
1501
+ this.advance();
1502
+ return this.parseNode(id.value, id.range);
1503
+ }
1504
+ if (this.atWord("group") || this.atWord("boundary") || this.atWord("zone") || this.atWord("swimlane")) return this.parseGroup();
1505
+ if (this.atWord("style")) return this.parseStyle();
1506
+ if (this.atWord("direction") || this.atWord("density")) return this.parseDirective();
1507
+ if (this.atWord("layout")) return this.parseLayoutBlock();
1508
+ if (this.atWord("edges")) return this.parseEdgePolicyBlock();
1509
+ if (this.atWord("render")) return this.parseRenderBlock();
1510
+ if (this.atWord("presentation")) return this.parsePresentationBlock();
1511
+ if (this.atWord("animation")) return this.parseAnimationBlock();
1512
+ if (this.at("identifier")) {
1513
+ if (this.looksLikeStyleRef()) return this.parseStyleRefStatement();
1514
+ if (inGroup && this.looksLikeGroupMember()) return this.parseGroupMemberList();
1515
+ const id = this.advance();
1516
+ if (this.at("dot") || this.at("edgeOp")) {
1517
+ const from = this.parseQualifiedName(id.value, id.range);
1518
+ return this.parseEdgeChain(from.nodeId, from.column, id.range);
1519
+ }
1520
+ this.error("FM003", `Unexpected identifier "${id.value}"`, id.range);
1521
+ return null;
1522
+ }
1523
+ this.error("FM004", `Unexpected token "${this.peek().value}"`, this.peek().range);
1524
+ this.advance();
1525
+ return null;
1526
+ }
1527
+ parseNode(id, idRange) {
1528
+ const start = idRange.start;
1529
+ const kindTok = this.advance();
1530
+ const kind = kindTok.value;
1531
+ let label;
1532
+ if (this.at("string")) label = this.advance().value;
1533
+ const styleRefs = this.parseOptionalIsStyleRefs();
1534
+ const properties = this.parseOptionalProperties();
1535
+ styleRefs.push(...this.parseOptionalIsStyleRefs());
1536
+ const end = this.peek(-1)?.range.end ?? kindTok.range.end;
1537
+ return {
1538
+ type: "Node",
1539
+ id,
1540
+ kind,
1541
+ label,
1542
+ properties,
1543
+ styleRefs,
1544
+ range: {
1545
+ start,
1546
+ end
1547
+ }
1548
+ };
1549
+ }
1550
+ parseOptionalIsStyleRefs() {
1551
+ const refs = [];
1552
+ while (this.atWord("is")) {
1553
+ this.advance();
1554
+ const styleTok = this.advance();
1555
+ if (styleTok.type !== "identifier") {
1556
+ this.error("FM008", "Expected style name after \"is\"", styleTok.range);
1557
+ break;
1558
+ }
1559
+ refs.push(styleTok.value);
1560
+ }
1561
+ return refs;
1562
+ }
1563
+ parseQualifiedName(firstId, _firstRange) {
1564
+ if (!this.at("dot")) return { nodeId: firstId };
1565
+ this.advance();
1566
+ const colTok = this.advance();
1567
+ if (colTok.type !== "identifier") {
1568
+ this.error("FM007", "Expected column name after '.'", colTok.range);
1569
+ return { nodeId: firstId };
1570
+ }
1571
+ return {
1572
+ nodeId: firstId,
1573
+ column: colTok.value
1574
+ };
1575
+ }
1576
+ parseEdgeChain(from, fromColumn, fromRange) {
1577
+ const edges = [];
1578
+ let currentFrom = from;
1579
+ let currentFromColumn = fromColumn;
1580
+ let chainStart = fromRange.start;
1581
+ while (this.at("edgeOp")) {
1582
+ const opTok = this.advance();
1583
+ let toNodeId;
1584
+ let toColumn;
1585
+ let toRange;
1586
+ if (this.at("star")) {
1587
+ const star = this.advance();
1588
+ toNodeId = "*";
1589
+ toRange = star.range;
1590
+ } else {
1591
+ const toTok = this.advance();
1592
+ if (toTok.type !== "identifier") {
1593
+ this.error("FM005", "Expected target node id after edge operator", toTok.range, "Add a target node id after the edge operator.");
1594
+ break;
1595
+ }
1596
+ const to = this.parseQualifiedName(toTok.value, toTok.range);
1597
+ toNodeId = to.nodeId;
1598
+ toColumn = to.column;
1599
+ toRange = toTok.range;
1600
+ }
1601
+ let label;
1602
+ if (this.at("string")) label = this.advance().value;
1603
+ const properties = this.parseOptionalProperties();
1604
+ const end = this.peek(-1)?.range.end ?? toRange.end;
1605
+ edges.push({
1606
+ type: "Edge",
1607
+ from: currentFrom,
1608
+ to: toNodeId,
1609
+ fromColumn: currentFromColumn,
1610
+ toColumn,
1611
+ op: opTok.value,
1612
+ label,
1613
+ properties,
1614
+ styleRefs: [],
1615
+ range: {
1616
+ start: chainStart,
1617
+ end
1618
+ }
1619
+ });
1620
+ currentFrom = toNodeId;
1621
+ currentFromColumn = toColumn;
1622
+ chainStart = toRange.start;
1623
+ if (this.atWord("is")) {
1624
+ this.advance();
1625
+ const styleTok = this.advance();
1626
+ edges[edges.length - 1].styleRefs.push(styleTok.value);
1627
+ }
1628
+ }
1629
+ return edges;
1630
+ }
1631
+ looksLikeStyleRef() {
1632
+ let p = this.pos;
1633
+ while (this.tokens[p]?.type === "identifier") {
1634
+ p++;
1635
+ if (this.tokens[p]?.type === "comma") {
1636
+ p++;
1637
+ continue;
1638
+ }
1639
+ break;
1640
+ }
1641
+ const t = this.tokens[p];
1642
+ return t?.type === "identifier" && t.value === "is";
1643
+ }
1644
+ looksLikeGroupMember() {
1645
+ if (!this.at("identifier")) return false;
1646
+ const next = this.peek(1);
1647
+ if (next.type === "colon" || next.type === "edgeOp" || next.type === "dot") return false;
1648
+ if (next.type === "identifier" && next.value === "is") return false;
1649
+ return true;
1650
+ }
1651
+ parseStyleRefStatement() {
1652
+ const start = this.peek().range.start;
1653
+ const targetIds = [this.advance().value];
1654
+ while (this.at("comma")) {
1655
+ this.advance();
1656
+ targetIds.push(this.advance().value);
1657
+ }
1658
+ this.advance();
1659
+ return {
1660
+ type: "StyleRef",
1661
+ targetIds,
1662
+ styleName: this.advance().value,
1663
+ range: {
1664
+ start,
1665
+ end: this.peek(-1)?.range.end ?? start
1666
+ }
1667
+ };
1668
+ }
1669
+ parseGroupMemberList() {
1670
+ const start = this.peek().range.start;
1671
+ const nodeIds = [this.advance().value];
1672
+ while (this.at("comma")) {
1673
+ this.advance();
1674
+ nodeIds.push(this.advance().value);
1675
+ }
1676
+ return {
1677
+ type: "GroupMember",
1678
+ nodeIds,
1679
+ range: {
1680
+ start,
1681
+ end: this.peek(-1)?.range.end ?? start
1682
+ }
1683
+ };
1684
+ }
1685
+ parseGroup() {
1686
+ const start = this.peek().range.start;
1687
+ const kindTok = this.advance();
1688
+ const groupKind = kindTok.value;
1689
+ let id;
1690
+ let label;
1691
+ if (this.at("identifier")) id = this.advance().value;
1692
+ if (this.at("string")) label = this.advance().value;
1693
+ const properties = {};
1694
+ if (this.at("lbrace")) {
1695
+ this.advance();
1696
+ const statements = [];
1697
+ while (!this.at("rbrace") && !this.at("eof")) {
1698
+ this.skipNewlines();
1699
+ if (this.at("rbrace") || this.at("eof")) break;
1700
+ const stmt = this.parseStatement(true);
1701
+ if (!stmt) continue;
1702
+ if (Array.isArray(stmt)) statements.push(...stmt);
1703
+ else statements.push(stmt);
1704
+ }
1705
+ if (this.at("rbrace")) this.advance();
1706
+ const end = this.peek(-1)?.range.end ?? kindTok.range.end;
1707
+ return {
1708
+ type: "Group",
1709
+ id,
1710
+ label,
1711
+ groupKind,
1712
+ statements,
1713
+ properties,
1714
+ range: {
1715
+ start,
1716
+ end
1717
+ }
1718
+ };
1719
+ }
1720
+ this.error("FM006", "Expected { after group declaration", this.peek().range);
1721
+ return {
1722
+ type: "Group",
1723
+ id,
1724
+ label,
1725
+ groupKind,
1726
+ statements: [],
1727
+ properties,
1728
+ range: {
1729
+ start,
1730
+ end: kindTok.range.end
1731
+ }
1732
+ };
1733
+ }
1734
+ parseStyle() {
1735
+ const start = this.advance().range.start;
1736
+ const nameTok = this.advance();
1737
+ const name = nameTok.value;
1738
+ let target = "node";
1739
+ if (this.atWord("for")) {
1740
+ this.advance();
1741
+ if (this.atWord("edge")) {
1742
+ this.advance();
1743
+ target = "edge";
1744
+ } else if (this.atWord("fragment")) {
1745
+ this.advance();
1746
+ target = "fragment";
1747
+ }
1748
+ }
1749
+ const properties = this.parseBlockProperties();
1750
+ return {
1751
+ type: "Style",
1752
+ name,
1753
+ target,
1754
+ properties,
1755
+ range: {
1756
+ start,
1757
+ end: this.peek(-1)?.range.end ?? nameTok.range.end
1758
+ }
1759
+ };
1760
+ }
1761
+ parseDirective() {
1762
+ const keywordTok = this.advance();
1763
+ const start = keywordTok.range.start;
1764
+ const name = keywordTok.value;
1765
+ let value;
1766
+ if (this.at("colon")) this.advance();
1767
+ if (this.at("lbracket")) value = this.parseIdentifierList();
1768
+ else if (!this.at("newline") && !this.at("comment") && !this.at("rbrace") && !this.at("eof")) value = this.parsePropertyValue();
1769
+ return {
1770
+ type: "Directive",
1771
+ name,
1772
+ value,
1773
+ range: {
1774
+ start,
1775
+ end: this.peek(-1)?.range.end ?? keywordTok.range.end
1776
+ }
1777
+ };
1778
+ }
1779
+ parseLayoutBlock() {
1780
+ const start = this.advance().range.start;
1781
+ return {
1782
+ type: "LayoutBlock",
1783
+ properties: this.parseBlockProperties(),
1784
+ range: {
1785
+ start,
1786
+ end: this.peek(-1)?.range.end ?? {
1787
+ line: 1,
1788
+ column: 1,
1789
+ offset: 0
1790
+ }
1791
+ }
1792
+ };
1793
+ }
1794
+ parseEdgePolicyBlock() {
1795
+ const start = this.advance().range.start;
1796
+ return {
1797
+ type: "EdgePolicyBlock",
1798
+ properties: this.parseBlockProperties(),
1799
+ range: {
1800
+ start,
1801
+ end: this.peek(-1)?.range.end ?? {
1802
+ line: 1,
1803
+ column: 1,
1804
+ offset: 0
1805
+ }
1806
+ }
1807
+ };
1808
+ }
1809
+ parseRenderBlock() {
1810
+ const start = this.advance().range.start;
1811
+ return {
1812
+ type: "RenderBlock",
1813
+ properties: this.at("lbrace") ? this.parseBlockProperties() : {},
1814
+ range: {
1815
+ start,
1816
+ end: this.peek(-1)?.range.end ?? {
1817
+ line: 1,
1818
+ column: 1,
1819
+ offset: 0
1820
+ }
1821
+ }
1822
+ };
1823
+ }
1824
+ parsePresentationBlock() {
1825
+ const start = this.advance().range.start;
1826
+ return {
1827
+ type: "PresentationBlock",
1828
+ properties: this.at("lbrace") ? this.parseBlockProperties() : {},
1829
+ range: {
1830
+ start,
1831
+ end: this.peek(-1)?.range.end ?? {
1832
+ line: 1,
1833
+ column: 1,
1834
+ offset: 0
1835
+ }
1836
+ }
1837
+ };
1838
+ }
1839
+ parseAnimationBlock() {
1840
+ const startTok = this.advance();
1841
+ this.skipTrivia();
1842
+ let name = "Animation";
1843
+ if (this.at("string")) {
1844
+ name = this.advance().value;
1845
+ this.skipTrivia();
1846
+ } else this.error("FM120", "Expected quoted animation name after `animation`", this.peek().range);
1847
+ if (!this.at("lbrace")) {
1848
+ this.error("FM121", "Expected \"{\" to open animation block", this.peek().range);
1849
+ return {
1850
+ type: "AnimationBlock",
1851
+ name,
1852
+ cues: [],
1853
+ range: {
1854
+ start: startTok.range.start,
1855
+ end: startTok.range.end
1856
+ }
1857
+ };
1858
+ }
1859
+ this.advance();
1860
+ const cues = [];
1861
+ while (!this.at("rbrace") && !this.at("eof")) {
1862
+ this.skipTrivia();
1863
+ if (this.at("rbrace") || this.at("eof")) break;
1864
+ const cue = this.parseAnimationCue();
1865
+ if (cue) cues.push(cue);
1866
+ }
1867
+ if (this.at("rbrace")) this.advance();
1868
+ else this.error("FM121", "Expected \"}\" to close animation block", this.peek().range);
1869
+ return {
1870
+ type: "AnimationBlock",
1871
+ name,
1872
+ cues,
1873
+ range: {
1874
+ start: startTok.range.start,
1875
+ end: this.peek(-1)?.range.end ?? startTok.range.end
1876
+ }
1877
+ };
1878
+ }
1879
+ parseAnimationCue() {
1880
+ const start = this.peek().range;
1881
+ if (this.atWord("loop")) {
1882
+ this.advance();
1883
+ return {
1884
+ type: "loop",
1885
+ range: start
1886
+ };
1887
+ }
1888
+ if (this.atWord("wait")) {
1889
+ this.advance();
1890
+ return {
1891
+ type: "wait",
1892
+ durationMs: this.parseDurationMs() ?? 400,
1893
+ range: {
1894
+ start: start.start,
1895
+ end: this.peek(-1)?.range.end ?? start.end
1896
+ }
1897
+ };
1898
+ }
1899
+ if (this.atWord("parallel")) {
1900
+ this.advance();
1901
+ this.skipTrivia();
1902
+ if (!this.at("lbrace")) {
1903
+ this.error("FM121", "Expected \"{\" to open parallel animation group", this.peek().range);
1904
+ return null;
1905
+ }
1906
+ this.advance();
1907
+ const cues = [];
1908
+ while (!this.at("rbrace") && !this.at("eof")) {
1909
+ this.skipTrivia();
1910
+ if (this.at("rbrace") || this.at("eof")) break;
1911
+ if (this.atWord("parallel")) {
1912
+ this.error("FM130", "Nested parallel groups are not supported", this.peek().range, "Keep parallel groups one level deep");
1913
+ this.advance();
1914
+ continue;
1915
+ }
1916
+ if (this.atWord("loop")) {
1917
+ this.error("FM129", "`loop` belongs on the animation block, not inside parallel", this.peek().range);
1918
+ this.advance();
1919
+ continue;
1920
+ }
1921
+ const cue = this.parseAnimationCue();
1922
+ if (cue) cues.push(cue);
1923
+ }
1924
+ if (this.at("rbrace")) this.advance();
1925
+ else this.error("FM121", "Expected \"}\" to close parallel group", this.peek().range);
1926
+ return {
1927
+ type: "parallel",
1928
+ cues,
1929
+ range: {
1930
+ start: start.start,
1931
+ end: this.peek(-1)?.range.end ?? start.end
1932
+ }
1933
+ };
1934
+ }
1935
+ if (this.atWord("dim") || this.atWord("activate") || this.atWord("pulse")) {
1936
+ const op = this.advance().value;
1937
+ const targets = this.parseAnimationTargets();
1938
+ let durationMs;
1939
+ if (op === "pulse" && this.atWord("for")) {
1940
+ this.advance();
1941
+ durationMs = this.parseDurationMs();
1942
+ }
1943
+ const range = {
1944
+ start: start.start,
1945
+ end: this.peek(-1)?.range.end ?? start.end
1946
+ };
1947
+ if (op === "pulse") return {
1948
+ type: "pulse",
1949
+ targets,
1950
+ durationMs,
1951
+ range
1952
+ };
1953
+ return {
1954
+ type: op,
1955
+ targets,
1956
+ range
1957
+ };
1958
+ }
1959
+ if (this.atWord("flow")) {
1960
+ this.advance();
1961
+ const path = this.parseAnimationPath();
1962
+ let durationMs;
1963
+ if (this.atWord("for")) {
1964
+ this.advance();
1965
+ durationMs = this.parseDurationMs();
1966
+ }
1967
+ return {
1968
+ type: "flow",
1969
+ path,
1970
+ durationMs,
1971
+ range: {
1972
+ start: start.start,
1973
+ end: this.peek(-1)?.range.end ?? start.end
1974
+ }
1975
+ };
1976
+ }
1977
+ this.error("FM122", `Unexpected animation cue "${this.peek().value}"`, this.peek().range, "Expected dim, activate, pulse, flow, wait, parallel, or loop");
1978
+ this.advance();
1979
+ return null;
1980
+ }
1981
+ parseAnimationTargets() {
1982
+ const targets = [];
1983
+ for (;;) {
1984
+ this.skipTrivia();
1985
+ if (this.at("star")) {
1986
+ this.advance();
1987
+ targets.push({ type: "all" });
1988
+ } else if (this.at("identifier")) {
1989
+ const from = this.advance().value;
1990
+ if (this.at("edgeOp") && this.peek().value === "->") {
1991
+ this.advance();
1992
+ if (!this.at("identifier")) {
1993
+ this.error("FM123", "Expected node id after `->` in animation target", this.peek().range);
1994
+ break;
1995
+ }
1996
+ const to = this.advance().value;
1997
+ targets.push({
1998
+ type: "edge",
1999
+ from,
2000
+ to
2001
+ });
2002
+ } else targets.push({
2003
+ type: "node",
2004
+ id: from
2005
+ });
2006
+ } else break;
2007
+ this.skipTrivia();
2008
+ if (this.at("comma")) {
2009
+ this.advance();
2010
+ continue;
2011
+ }
2012
+ break;
2013
+ }
2014
+ if (targets.length === 0) this.error("FM124", "Expected animation target (* or node id)", this.peek().range);
2015
+ return targets;
2016
+ }
2017
+ parseAnimationPath() {
2018
+ const path = [];
2019
+ if (!this.at("identifier")) {
2020
+ this.error("FM125", "Expected node id to start flow path", this.peek().range);
2021
+ return path;
2022
+ }
2023
+ path.push(this.advance().value);
2024
+ while (this.at("edgeOp") && this.peek().value === "->") {
2025
+ this.advance();
2026
+ if (!this.at("identifier")) {
2027
+ this.error("FM125", "Expected node id after `->` in flow path", this.peek().range);
2028
+ break;
2029
+ }
2030
+ path.push(this.advance().value);
2031
+ }
2032
+ if (path.length < 2) this.error("FM126", "flow path needs at least two node ids (a -> b)", this.peek().range);
2033
+ return path;
2034
+ }
2035
+ /** Parse `800ms`, `1.5s`, or bare number (milliseconds). */
2036
+ parseDurationMs() {
2037
+ if (!this.at("number")) {
2038
+ this.error("FM127", "Expected duration (e.g. 800ms or 1.5s)", this.peek().range);
2039
+ return;
2040
+ }
2041
+ const n = Number(this.advance().value);
2042
+ if (!Number.isFinite(n)) return void 0;
2043
+ if (this.at("identifier") && (this.peek().value === "ms" || this.peek().value === "s")) return this.advance().value === "s" ? Math.round(n * 1e3) : Math.round(n);
2044
+ return Math.round(n);
2045
+ }
2046
+ parseBlockProperties() {
2047
+ const props = {};
2048
+ if (!this.at("lbrace")) return props;
2049
+ this.advance();
2050
+ while (!this.at("rbrace") && !this.at("eof")) {
2051
+ this.skipTrivia();
2052
+ if (this.at("identifier")) {
2053
+ const key = this.advance().value;
2054
+ if (this.at("colon")) this.advance();
2055
+ if (key === "columns" && this.at("lbrace")) {
2056
+ props[key] = this.parseColumnsBlock();
2057
+ continue;
2058
+ }
2059
+ if (this.at("lbracket")) {
2060
+ props[key] = this.parseIdentifierList();
2061
+ continue;
2062
+ }
2063
+ if (this.at("newline") || this.at("comment") || this.at("rbrace") || this.at("eof")) continue;
2064
+ props[key] = key.startsWith("--") || key === "iconColor" ? this.parseCssValue() : this.parsePropertyValue();
2065
+ } else if (!this.at("rbrace")) this.advance();
2066
+ }
2067
+ if (this.at("rbrace")) this.advance();
2068
+ return props;
2069
+ }
2070
+ /**
2071
+ * Property values: numbers, booleans, strings, identifiers,
2072
+ * and qualified ids like `lucide:user` / `logos:aws`.
2073
+ */
2074
+ parsePropertyValue() {
2075
+ const val = this.advance();
2076
+ if (val.type === "number") return Number(val.value);
2077
+ if (val.value === "true" || val.value === "false") return val.value === "true";
2078
+ if (val.type === "string") return val.value;
2079
+ let text = val.value;
2080
+ while (this.at("colon") && this.peek(1).type === "identifier") {
2081
+ this.advance();
2082
+ text += `:${this.advance().value}`;
2083
+ }
2084
+ return text;
2085
+ }
2086
+ /**
2087
+ * CSS custom-property values on a style/node — slice source through end of line so
2088
+ * `#hex`, `var(...)`, and `color-mix(...)` keep spaces and punctuation.
2089
+ */
2090
+ parseCssValue() {
2091
+ if (this.at("string")) return this.advance().value;
2092
+ if (this.at("newline") || this.at("comment") || this.at("rbrace") || this.at("eof")) return "";
2093
+ const start = this.peek().range.start.offset;
2094
+ let end = start;
2095
+ while (!this.at("newline") && !this.at("comment") && !this.at("rbrace") && !this.at("eof")) end = this.advance().range.end.offset;
2096
+ return this.source.slice(start, end).trim();
2097
+ }
2098
+ /**
2099
+ * Parse `columns { name: type FLAGS … }` into compact column specs
2100
+ * (same strings `parseTableColumnSpec` already understands).
2101
+ */
2102
+ parseColumnsBlock() {
2103
+ const cols = [];
2104
+ if (!this.at("lbrace")) return cols;
2105
+ this.advance();
2106
+ while (!this.at("rbrace") && !this.at("eof")) {
2107
+ this.skipTrivia();
2108
+ if (this.at("rbrace") || this.at("eof")) break;
2109
+ if (this.peek().type !== "identifier") {
2110
+ this.error("FM113", "Expected column name in columns block", this.peek().range);
2111
+ this.advance();
2112
+ continue;
2113
+ }
2114
+ const nameTok = this.advance();
2115
+ if (this.at("colon")) this.advance();
2116
+ const parts = [];
2117
+ while (!this.at("newline") && !this.at("comment") && !this.at("rbrace") && !this.at("eof") && !this.at("comma")) {
2118
+ const t = this.advance();
2119
+ parts.push(t.value);
2120
+ }
2121
+ let note;
2122
+ if (this.at("comment")) note = this.advance().value || void 0;
2123
+ if (this.at("comma")) this.advance();
2124
+ const body = joinColumnParts(parts);
2125
+ const spec = note ? `${nameTok.value}: ${body} // ${note}` : `${nameTok.value}: ${body}`;
2126
+ cols.push(spec.trim());
2127
+ }
2128
+ if (this.at("rbrace")) this.advance();
2129
+ return cols;
2130
+ }
2131
+ parseOptionalProperties() {
2132
+ if (!this.at("lbrace")) return {};
2133
+ return this.parseBlockProperties();
2134
+ }
2135
+ parseIdentifierList() {
2136
+ const items = [];
2137
+ if (!this.at("lbracket")) return items;
2138
+ this.advance();
2139
+ while (!this.at("rbracket") && !this.at("eof")) {
2140
+ this.skipTrivia();
2141
+ if (this.at("rbracket")) break;
2142
+ items.push(this.advance().value);
2143
+ if (this.at("comma")) this.advance();
2144
+ }
2145
+ if (this.at("rbracket")) this.advance();
2146
+ return items;
2147
+ }
2148
+ };
2149
+ /** Join column RHS tokens so `customers . id` becomes `customers.id`. */
2150
+ function joinColumnParts(parts) {
2151
+ if (parts.length === 0) return "";
2152
+ let out = "";
2153
+ for (const part of parts) {
2154
+ if (part === ".") {
2155
+ out = out.trimEnd() + ".";
2156
+ continue;
2157
+ }
2158
+ if (out.endsWith(".")) {
2159
+ out += part;
2160
+ continue;
2161
+ }
2162
+ out += out ? ` ${part}` : part;
2163
+ }
2164
+ return out.trim();
2165
+ }
2166
+ const GROUP_LAYOUT_HINTS = /* @__PURE__ */ new Set([
2167
+ "padding",
2168
+ "arrange",
2169
+ "align",
2170
+ "gap",
2171
+ "columns",
2172
+ "rows",
2173
+ "column",
2174
+ "row",
2175
+ "span",
2176
+ "colSpan",
2177
+ "rowSpan",
2178
+ "chrome",
2179
+ "icon",
2180
+ "iconPaint",
2181
+ "iconColor"
2182
+ ]);
2183
+ function isGroupLayoutHint(name) {
2184
+ return GROUP_LAYOUT_HINTS.has(name);
2185
+ }
2186
+ //#endregion
2187
+ //#region src/types/presentation.ts
2188
+ /** Props removed from the presentation DSL — emit a diagnostic instead of ignoring. */
2189
+ const REMOVED_PRESENTATION_PROPS = [
2190
+ "preset",
2191
+ "background",
2192
+ "grid",
2193
+ "frame",
2194
+ "legend",
2195
+ "nodeGradient",
2196
+ "groupHeaders"
2197
+ ];
2198
+ function normalizePadding(spec, fallback = 0) {
2199
+ if (spec == null) return {
2200
+ top: fallback,
2201
+ right: fallback,
2202
+ bottom: fallback,
2203
+ left: fallback
2204
+ };
2205
+ if (typeof spec === "number") return {
2206
+ top: spec,
2207
+ right: spec,
2208
+ bottom: spec,
2209
+ left: spec
2210
+ };
2211
+ return {
2212
+ top: spec.top ?? fallback,
2213
+ right: spec.right ?? fallback,
2214
+ bottom: spec.bottom ?? fallback,
2215
+ left: spec.left ?? fallback
2216
+ };
2217
+ }
2218
+ const BASE_DEFAULTS = {
2219
+ title: false,
2220
+ labelCase: "as-authored",
2221
+ showKindSubtitles: false,
2222
+ showEndpoints: false,
2223
+ groupAccent: false,
2224
+ clampLabels: true
2225
+ };
2226
+ function resolveTitle(title, graphTitle) {
2227
+ if (title === false || title == null) return false;
2228
+ if (title === true || title === "auto") {
2229
+ if (!graphTitle) return false;
2230
+ return {
2231
+ text: graphTitle,
2232
+ align: "start"
2233
+ };
2234
+ }
2235
+ if (typeof title === "object") {
2236
+ const text = title.text ?? graphTitle;
2237
+ if (!text) return false;
2238
+ return {
2239
+ text,
2240
+ subtitle: title.subtitle,
2241
+ align: title.align ?? "start"
2242
+ };
2243
+ }
2244
+ return false;
2245
+ }
2246
+ /** Merge explicit options onto chromeless defaults. */
2247
+ function resolvePresentation(options, graphTitle) {
2248
+ const merged = {
2249
+ ...BASE_DEFAULTS,
2250
+ ...options
2251
+ };
2252
+ return {
2253
+ title: resolveTitle(merged.title, graphTitle),
2254
+ padding: normalizePadding(merged.padding, 0),
2255
+ contentPadding: normalizePadding(merged.contentPadding, 0),
2256
+ labelCase: "as-authored",
2257
+ showKindSubtitles: merged.showKindSubtitles === true,
2258
+ showEndpoints: merged.showEndpoints === true,
2259
+ groupAccent: merged.groupAccent === true,
2260
+ clampLabels: merged.clampLabels !== false
2261
+ };
2262
+ }
2263
+ /** Parse a DSL property map into PresentationOptions. */
2264
+ function presentationFromProperties(props) {
2265
+ const out = {};
2266
+ const str = (key) => {
2267
+ const v = props[key];
2268
+ return typeof v === "string" ? v : void 0;
2269
+ };
2270
+ const bool = (key) => {
2271
+ const v = props[key];
2272
+ if (typeof v === "boolean") return v;
2273
+ if (v === "true") return true;
2274
+ if (v === "false") return false;
2275
+ };
2276
+ const num = (key) => {
2277
+ const v = props[key];
2278
+ return typeof v === "number" ? v : void 0;
2279
+ };
2280
+ const titleVal = props.title;
2281
+ const titleSubtitle = str("titleSubtitle");
2282
+ const titleAlign = str("titleAlign");
2283
+ if (titleVal === false || titleVal === "false" || titleVal === "none") out.title = false;
2284
+ else if (titleVal === true || titleVal === "true" || titleVal === "auto") if (titleSubtitle || titleAlign) out.title = {
2285
+ subtitle: titleSubtitle,
2286
+ align: titleAlign
2287
+ };
2288
+ else out.title = titleVal === "auto" ? "auto" : true;
2289
+ else if (typeof titleVal === "string") out.title = {
2290
+ text: titleVal,
2291
+ subtitle: titleSubtitle,
2292
+ align: titleAlign
2293
+ };
2294
+ else if (titleSubtitle || titleAlign) out.title = {
2295
+ subtitle: titleSubtitle,
2296
+ align: titleAlign
2297
+ };
2298
+ const padding = num("padding");
2299
+ if (padding != null) out.padding = padding;
2300
+ const contentPadding = num("contentPadding");
2301
+ if (contentPadding != null) out.contentPadding = contentPadding;
2302
+ out.labelCase = "as-authored";
2303
+ const showKindSubtitles = bool("showKindSubtitles");
2304
+ if (showKindSubtitles != null) out.showKindSubtitles = showKindSubtitles;
2305
+ const showEndpoints = bool("showEndpoints");
2306
+ if (showEndpoints != null) out.showEndpoints = showEndpoints;
2307
+ if (props.groupAccent === "auto") out.groupAccent = true;
2308
+ else {
2309
+ const b = bool("groupAccent");
2310
+ if (b != null) out.groupAccent = b;
2311
+ }
2312
+ const clampLabels = bool("clampLabels");
2313
+ if (clampLabels != null) out.clampLabels = clampLabels;
2314
+ return out;
2315
+ }
2316
+ /**
2317
+ * Pick the case policy for a concrete label.
2318
+ * Quoted / authored labels are never rewritten.
2319
+ */
2320
+ function displayLabelCase(_labelAuthored, _policy) {
2321
+ return "as-authored";
2322
+ }
2323
+ function formatLabelText(text, _policy) {
2324
+ return text;
2325
+ }
2326
+ /** Shallow-merge presentation layers (later wins). */
2327
+ function mergePresentationOptions(...layers) {
2328
+ let result;
2329
+ for (const layer of layers) {
2330
+ if (!layer) continue;
2331
+ result = {
2332
+ ...result,
2333
+ ...layer
2334
+ };
2335
+ }
2336
+ return result;
2337
+ }
2338
+ //#endregion
2339
+ //#region src/types/shapes.ts
2340
+ /**
2341
+ * Shared shape id vocabulary — leaf types used by kinds, compile, and geometry.
2342
+ * Geometry implementations live in @kekonic/diagrams-geometry; this module
2343
+ * only names the ids so core can validate without depending on renderers.
2344
+ */
2345
+ const BUILTIN_SHAPE_IDS = [
2346
+ "rectangle",
2347
+ "rounded",
2348
+ "pill",
2349
+ "circle",
2350
+ "ellipse",
2351
+ "diamond",
2352
+ "hexagon",
2353
+ "triangle",
2354
+ "parallelogram",
2355
+ "trapezoid",
2356
+ "document",
2357
+ "folded-document",
2358
+ "cylinder",
2359
+ "cloud",
2360
+ "person",
2361
+ "queue",
2362
+ "stream",
2363
+ "table",
2364
+ "boundary"
2365
+ ];
2366
+ const SHAPE_ALIASES = {
2367
+ note: "folded-document",
2368
+ card: "rounded",
2369
+ round: "rounded",
2370
+ roundedbox: "rounded",
2371
+ rect: "rectangle",
2372
+ box: "rectangle",
2373
+ cyl: "cylinder",
2374
+ hex: "hexagon",
2375
+ diam: "diamond",
2376
+ capsule: "pill",
2377
+ stadium: "pill",
2378
+ pipe: "queue",
2379
+ log: "stream"
2380
+ };
2381
+ function normalizeShapeId(shape) {
2382
+ if (!shape) return "rounded";
2383
+ const key = String(shape).trim().toLowerCase();
2384
+ if (!key) return "rounded";
2385
+ return SHAPE_ALIASES[key] ?? key;
2386
+ }
2387
+ function isKnownShapeId(shape) {
2388
+ if (!shape) return false;
2389
+ const id = normalizeShapeId(shape);
2390
+ return BUILTIN_SHAPE_IDS.includes(id);
2391
+ }
2392
+ function listBuiltinShapeIds() {
2393
+ return BUILTIN_SHAPE_IDS;
2394
+ }
2395
+ //#endregion
2396
+ //#region src/compiler/kinds.ts
2397
+ function defineKind(id, spec) {
2398
+ const caps = new Set(spec.capabilities ?? []);
2399
+ if (spec.shape === "rounded" || spec.shape === "rectangle" || spec.shape === "table" || spec.shape === "boundary") caps.add("card");
2400
+ const shapeId = normalizeShapeId(spec.shape);
2401
+ const classNames = [
2402
+ `flow-node-${id}`,
2403
+ `flow-shape-${shapeId}`,
2404
+ ...spec.extraClasses ?? []
2405
+ ];
2406
+ return {
2407
+ shape: shapeId,
2408
+ icon: spec.icon,
2409
+ subtitle: spec.subtitle,
2410
+ classNames: [...new Set(classNames)],
2411
+ cssVars: spec.cssVars ?? {},
2412
+ defaultMinWidth: spec.min ?? 120,
2413
+ defaultMaxWidth: spec.max ?? 240,
2414
+ defaultDepth: spec.depth ?? 1,
2415
+ category: spec.category,
2416
+ capabilities: [...caps]
2417
+ };
2418
+ }
2419
+ function titleCaseKind(kind) {
2420
+ const words = kind.replace(/[_-]+/g, " ").trim();
2421
+ if (!words) return "Node";
2422
+ return words.replace(/\b\w/g, (c) => c.toUpperCase());
2423
+ }
2424
+ function sanitizeKindClass(kind) {
2425
+ return kind.replace(/[^a-z0-9-]/gi, "") || "generic";
2426
+ }
2427
+ /**
2428
+ * Built-in semantic node kinds.
2429
+ * Geometry is selected via `shape`; meaning via kind + theme (+ optional authored `icon`).
2430
+ * Do not invent a unique SVG engine per kind — map onto the shared geometry library.
2431
+ */
2432
+ const BUILTIN_KINDS = {
2433
+ note: defineKind("note", {
2434
+ shape: "folded-document",
2435
+ subtitle: "Note",
2436
+ category: "general",
2437
+ min: 120,
2438
+ max: 280,
2439
+ capabilities: ["annotation"]
2440
+ }),
2441
+ label: defineKind("label", {
2442
+ shape: "rounded",
2443
+ subtitle: "Label",
2444
+ category: "general",
2445
+ min: 64,
2446
+ max: 200,
2447
+ capabilities: ["annotation", "symbolic"]
2448
+ }),
2449
+ group: defineKind("group", {
2450
+ shape: "boundary",
2451
+ subtitle: "Group",
2452
+ category: "general",
2453
+ min: 160,
2454
+ max: 400,
2455
+ capabilities: ["boundary"]
2456
+ }),
2457
+ image: defineKind("image", {
2458
+ shape: "rectangle",
2459
+ subtitle: "Image",
2460
+ category: "general",
2461
+ min: 80,
2462
+ max: 320
2463
+ }),
2464
+ icon: defineKind("icon", {
2465
+ shape: "rounded",
2466
+ subtitle: "Icon",
2467
+ category: "general",
2468
+ min: 56,
2469
+ max: 96,
2470
+ capabilities: ["symbolic", "icon-only"]
2471
+ }),
2472
+ start: defineKind("start", {
2473
+ shape: "pill",
2474
+ subtitle: "Start",
2475
+ category: "flowchart",
2476
+ min: 88,
2477
+ max: 160,
2478
+ capabilities: ["symbolic"]
2479
+ }),
2480
+ end: defineKind("end", {
2481
+ shape: "pill",
2482
+ subtitle: "End",
2483
+ category: "flowchart",
2484
+ min: 88,
2485
+ max: 160,
2486
+ capabilities: ["symbolic"]
2487
+ }),
2488
+ process: defineKind("process", {
2489
+ shape: "rounded",
2490
+ subtitle: "Process",
2491
+ category: "flowchart",
2492
+ min: 128,
2493
+ max: 260
2494
+ }),
2495
+ task: defineKind("task", {
2496
+ shape: "rounded",
2497
+ subtitle: "Task",
2498
+ category: "flowchart",
2499
+ min: 120,
2500
+ max: 240
2501
+ }),
2502
+ subprocess: defineKind("subprocess", {
2503
+ shape: "rounded",
2504
+ subtitle: "Subprocess",
2505
+ category: "flowchart",
2506
+ min: 140,
2507
+ max: 280
2508
+ }),
2509
+ choice: defineKind("choice", {
2510
+ shape: "diamond",
2511
+ subtitle: "Decision",
2512
+ category: "flowchart",
2513
+ min: 180,
2514
+ max: 360
2515
+ }),
2516
+ decision: defineKind("decision", {
2517
+ shape: "diamond",
2518
+ subtitle: "Decision",
2519
+ category: "flowchart",
2520
+ min: 180,
2521
+ max: 360,
2522
+ extraClasses: ["flow-node-choice"]
2523
+ }),
2524
+ io: defineKind("io", {
2525
+ shape: "parallelogram",
2526
+ subtitle: "Input / Output",
2527
+ category: "flowchart",
2528
+ min: 120,
2529
+ max: 240
2530
+ }),
2531
+ document: defineKind("document", {
2532
+ shape: "document",
2533
+ subtitle: "Document",
2534
+ category: "flowchart",
2535
+ min: 120,
2536
+ max: 240
2537
+ }),
2538
+ preparation: defineKind("preparation", {
2539
+ shape: "hexagon",
2540
+ subtitle: "Preparation",
2541
+ category: "flowchart",
2542
+ min: 120,
2543
+ max: 220
2544
+ }),
2545
+ manual: defineKind("manual", {
2546
+ shape: "trapezoid",
2547
+ subtitle: "Manual",
2548
+ category: "flowchart",
2549
+ min: 120,
2550
+ max: 240
2551
+ }),
2552
+ connector: defineKind("connector", {
2553
+ shape: "circle",
2554
+ subtitle: "Connector",
2555
+ category: "flowchart",
2556
+ min: 36,
2557
+ max: 56,
2558
+ capabilities: ["symbolic"]
2559
+ }),
2560
+ user: defineKind("user", {
2561
+ shape: "person",
2562
+ subtitle: "User",
2563
+ category: "architecture",
2564
+ icon: "user",
2565
+ min: 100,
2566
+ max: 220,
2567
+ extraClasses: ["flow-node-user"]
2568
+ }),
2569
+ person: defineKind("person", {
2570
+ shape: "person",
2571
+ subtitle: "Person",
2572
+ category: "architecture",
2573
+ icon: "user",
2574
+ min: 100,
2575
+ max: 220,
2576
+ extraClasses: ["flow-node-user"]
2577
+ }),
2578
+ actor: defineKind("actor", {
2579
+ shape: "rounded",
2580
+ subtitle: "Actor",
2581
+ category: "architecture",
2582
+ icon: "user",
2583
+ min: 120,
2584
+ max: 240,
2585
+ extraClasses: ["flow-node-actor"],
2586
+ capabilities: ["sequence-lifeline"]
2587
+ }),
2588
+ participant: defineKind("participant", {
2589
+ shape: "rounded",
2590
+ subtitle: "Participant",
2591
+ category: "architecture",
2592
+ min: 120,
2593
+ max: 240,
2594
+ extraClasses: ["flow-node-participant"],
2595
+ capabilities: ["sequence-lifeline"]
2596
+ }),
2597
+ system: defineKind("system", {
2598
+ shape: "rounded",
2599
+ subtitle: "System",
2600
+ category: "architecture",
2601
+ min: 176,
2602
+ max: 340
2603
+ }),
2604
+ external: defineKind("external", {
2605
+ shape: "rectangle",
2606
+ subtitle: "External",
2607
+ category: "architecture",
2608
+ min: 120,
2609
+ max: 260
2610
+ }),
2611
+ container: defineKind("container", {
2612
+ shape: "rounded",
2613
+ subtitle: "Container",
2614
+ category: "architecture",
2615
+ min: 148,
2616
+ max: 300
2617
+ }),
2618
+ component: defineKind("component", {
2619
+ shape: "rounded",
2620
+ subtitle: "Component",
2621
+ category: "architecture",
2622
+ min: 120,
2623
+ max: 240
2624
+ }),
2625
+ service: defineKind("service", {
2626
+ shape: "rounded",
2627
+ subtitle: "Service",
2628
+ category: "architecture",
2629
+ min: 128,
2630
+ max: 280
2631
+ }),
2632
+ application: defineKind("application", {
2633
+ shape: "rounded",
2634
+ subtitle: "Application",
2635
+ category: "architecture",
2636
+ min: 140,
2637
+ max: 280,
2638
+ extraClasses: ["flow-node-service"]
2639
+ }),
2640
+ module: defineKind("module", {
2641
+ shape: "rounded",
2642
+ subtitle: "Module",
2643
+ category: "architecture",
2644
+ min: 120,
2645
+ max: 240,
2646
+ extraClasses: ["flow-node-service"]
2647
+ }),
2648
+ package: defineKind("package", {
2649
+ shape: "folded-document",
2650
+ subtitle: "Package",
2651
+ category: "architecture",
2652
+ min: 120,
2653
+ max: 240
2654
+ }),
2655
+ deployment: defineKind("deployment", {
2656
+ shape: "rectangle",
2657
+ subtitle: "Deployment",
2658
+ category: "architecture",
2659
+ min: 140,
2660
+ max: 280,
2661
+ extraClasses: ["flow-node-service"]
2662
+ }),
2663
+ server: defineKind("server", {
2664
+ shape: "rectangle",
2665
+ subtitle: "Server",
2666
+ category: "infrastructure",
2667
+ min: 120,
2668
+ max: 240,
2669
+ extraClasses: ["flow-node-service"]
2670
+ }),
2671
+ database: defineKind("database", {
2672
+ shape: "cylinder",
2673
+ subtitle: "Database",
2674
+ category: "infrastructure",
2675
+ min: 100,
2676
+ max: 200,
2677
+ capabilities: ["datastore"]
2678
+ }),
2679
+ cache: defineKind("cache", {
2680
+ shape: "cylinder",
2681
+ subtitle: "Cache",
2682
+ category: "infrastructure",
2683
+ min: 100,
2684
+ max: 200,
2685
+ capabilities: ["datastore"],
2686
+ extraClasses: ["flow-node-database"]
2687
+ }),
2688
+ store: defineKind("store", {
2689
+ shape: "cylinder",
2690
+ subtitle: "Data store",
2691
+ category: "infrastructure",
2692
+ min: 100,
2693
+ max: 220,
2694
+ capabilities: ["datastore"],
2695
+ extraClasses: ["flow-node-database"]
2696
+ }),
2697
+ bucket: defineKind("bucket", {
2698
+ shape: "cylinder",
2699
+ subtitle: "Object store",
2700
+ category: "infrastructure",
2701
+ min: 100,
2702
+ max: 220,
2703
+ capabilities: ["datastore"],
2704
+ extraClasses: ["flow-node-database"]
2705
+ }),
2706
+ queue: defineKind("queue", {
2707
+ shape: "queue",
2708
+ subtitle: "Queue",
2709
+ category: "infrastructure",
2710
+ min: 100,
2711
+ max: 180,
2712
+ capabilities: ["messaging"]
2713
+ }),
2714
+ topic: defineKind("topic", {
2715
+ shape: "stream",
2716
+ subtitle: "Topic",
2717
+ category: "infrastructure",
2718
+ min: 100,
2719
+ max: 220,
2720
+ capabilities: ["messaging"],
2721
+ extraClasses: ["flow-node-stream"]
2722
+ }),
2723
+ stream: defineKind("stream", {
2724
+ shape: "stream",
2725
+ subtitle: "Stream",
2726
+ category: "infrastructure",
2727
+ min: 120,
2728
+ max: 260,
2729
+ capabilities: ["messaging"]
2730
+ }),
2731
+ gateway: defineKind("gateway", {
2732
+ shape: "hexagon",
2733
+ subtitle: "Gateway",
2734
+ category: "infrastructure",
2735
+ min: 110,
2736
+ max: 200
2737
+ }),
2738
+ loadbalancer: defineKind("loadbalancer", {
2739
+ shape: "hexagon",
2740
+ subtitle: "Load balancer",
2741
+ category: "infrastructure",
2742
+ min: 120,
2743
+ max: 220,
2744
+ extraClasses: ["flow-node-gateway"]
2745
+ }),
2746
+ cloud: defineKind("cloud", {
2747
+ shape: "cloud",
2748
+ subtitle: "Cloud",
2749
+ category: "infrastructure",
2750
+ min: 140,
2751
+ max: 260
2752
+ }),
2753
+ firewall: defineKind("firewall", {
2754
+ shape: "rectangle",
2755
+ subtitle: "Firewall",
2756
+ category: "infrastructure",
2757
+ min: 100,
2758
+ max: 200,
2759
+ extraClasses: ["flow-node-gateway"]
2760
+ }),
2761
+ zone: defineKind("zone", {
2762
+ shape: "boundary",
2763
+ subtitle: "Zone",
2764
+ category: "infrastructure",
2765
+ min: 180,
2766
+ max: 400,
2767
+ capabilities: ["boundary"]
2768
+ }),
2769
+ broker: defineKind("broker", {
2770
+ shape: "hexagon",
2771
+ subtitle: "Broker",
2772
+ category: "messaging",
2773
+ min: 110,
2774
+ max: 200,
2775
+ capabilities: ["messaging"]
2776
+ }),
2777
+ event: defineKind("event", {
2778
+ shape: "pill",
2779
+ subtitle: "Event",
2780
+ category: "messaging",
2781
+ min: 90,
2782
+ max: 180,
2783
+ capabilities: ["messaging", "symbolic"]
2784
+ }),
2785
+ command: defineKind("command", {
2786
+ shape: "rounded",
2787
+ subtitle: "Command",
2788
+ category: "messaging",
2789
+ min: 110,
2790
+ max: 220,
2791
+ capabilities: ["messaging"],
2792
+ extraClasses: ["flow-node-service"]
2793
+ }),
2794
+ message: defineKind("message", {
2795
+ shape: "document",
2796
+ subtitle: "Message",
2797
+ category: "messaging",
2798
+ min: 100,
2799
+ max: 200,
2800
+ capabilities: ["messaging"]
2801
+ }),
2802
+ worker: defineKind("worker", {
2803
+ shape: "rounded",
2804
+ subtitle: "Worker",
2805
+ category: "messaging",
2806
+ min: 110,
2807
+ max: 200,
2808
+ extraClasses: ["flow-node-service"]
2809
+ }),
2810
+ producer: defineKind("producer", {
2811
+ shape: "rounded",
2812
+ subtitle: "Producer",
2813
+ category: "messaging",
2814
+ min: 110,
2815
+ max: 200,
2816
+ capabilities: ["messaging"],
2817
+ extraClasses: ["flow-node-service"]
2818
+ }),
2819
+ consumer: defineKind("consumer", {
2820
+ shape: "rounded",
2821
+ subtitle: "Consumer",
2822
+ category: "messaging",
2823
+ min: 110,
2824
+ max: 200,
2825
+ capabilities: ["messaging"],
2826
+ extraClasses: ["flow-node-service"]
2827
+ }),
2828
+ dlq: defineKind("dlq", {
2829
+ shape: "queue",
2830
+ subtitle: "Dead-letter queue",
2831
+ category: "messaging",
2832
+ min: 110,
2833
+ max: 200,
2834
+ capabilities: ["messaging"],
2835
+ extraClasses: ["flow-node-queue", "flow-node-warning"]
2836
+ }),
2837
+ job: defineKind("job", {
2838
+ shape: "rounded",
2839
+ subtitle: "Job",
2840
+ category: "messaging",
2841
+ min: 110,
2842
+ max: 200,
2843
+ extraClasses: ["flow-node-worker"]
2844
+ }),
2845
+ table: defineKind("table", {
2846
+ shape: "cylinder",
2847
+ subtitle: "Table",
2848
+ category: "modeling",
2849
+ min: 160,
2850
+ max: 320,
2851
+ capabilities: ["datastore", "erd-table"]
2852
+ }),
2853
+ entity: defineKind("entity", {
2854
+ shape: "cylinder",
2855
+ subtitle: "Entity",
2856
+ category: "modeling",
2857
+ min: 160,
2858
+ max: 320,
2859
+ capabilities: ["datastore", "erd-table"],
2860
+ extraClasses: ["flow-node-table"]
2861
+ }),
2862
+ class: defineKind("class", {
2863
+ shape: "table",
2864
+ subtitle: "Class",
2865
+ category: "modeling",
2866
+ min: 160,
2867
+ max: 320,
2868
+ capabilities: ["erd-table"],
2869
+ extraClasses: ["flow-node-table"]
2870
+ }),
2871
+ interface: defineKind("interface", {
2872
+ shape: "rounded",
2873
+ subtitle: "Interface",
2874
+ category: "modeling",
2875
+ min: 120,
2876
+ max: 240,
2877
+ extraClasses: ["flow-node-service"]
2878
+ }),
2879
+ aggregate: defineKind("aggregate", {
2880
+ shape: "rounded",
2881
+ subtitle: "Aggregate",
2882
+ category: "modeling",
2883
+ min: 140,
2884
+ max: 280,
2885
+ extraClasses: ["flow-node-service"]
2886
+ }),
2887
+ projection: defineKind("projection", {
2888
+ shape: "rounded",
2889
+ subtitle: "Projection",
2890
+ category: "modeling",
2891
+ min: 140,
2892
+ max: 280,
2893
+ extraClasses: ["flow-node-service"]
2894
+ }),
2895
+ policy: defineKind("policy", {
2896
+ shape: "hexagon",
2897
+ subtitle: "Policy",
2898
+ category: "modeling",
2899
+ min: 120,
2900
+ max: 220,
2901
+ extraClasses: ["flow-node-gateway"]
2902
+ }),
2903
+ state: defineKind("state", {
2904
+ shape: "rounded",
2905
+ subtitle: "State",
2906
+ category: "state",
2907
+ min: 100,
2908
+ max: 200
2909
+ }),
2910
+ initial: defineKind("initial", {
2911
+ shape: "circle",
2912
+ subtitle: "Initial",
2913
+ category: "state",
2914
+ min: 28,
2915
+ max: 40,
2916
+ capabilities: ["symbolic"]
2917
+ }),
2918
+ final: defineKind("final", {
2919
+ shape: "circle",
2920
+ subtitle: "Final",
2921
+ category: "state",
2922
+ min: 32,
2923
+ max: 48,
2924
+ capabilities: ["symbolic"]
2925
+ }),
2926
+ junction: defineKind("junction", {
2927
+ shape: "circle",
2928
+ subtitle: "Junction",
2929
+ category: "state",
2930
+ min: 24,
2931
+ max: 36,
2932
+ capabilities: ["symbolic"]
2933
+ }),
2934
+ fork: defineKind("fork", {
2935
+ shape: "rectangle",
2936
+ subtitle: "Fork",
2937
+ category: "state",
2938
+ min: 80,
2939
+ max: 160,
2940
+ capabilities: ["symbolic"]
2941
+ }),
2942
+ join: defineKind("join", {
2943
+ shape: "rectangle",
2944
+ subtitle: "Join",
2945
+ category: "state",
2946
+ min: 80,
2947
+ max: 160,
2948
+ capabilities: ["symbolic"]
2949
+ }),
2950
+ success: defineKind("success", {
2951
+ shape: "rounded",
2952
+ subtitle: "Success",
2953
+ category: "flowchart",
2954
+ min: 128,
2955
+ max: 260
2956
+ }),
2957
+ warning: defineKind("warning", {
2958
+ shape: "rounded",
2959
+ subtitle: "Manual review",
2960
+ category: "flowchart",
2961
+ min: 128,
2962
+ max: 260
2963
+ }),
2964
+ failure: defineKind("failure", {
2965
+ shape: "rounded",
2966
+ subtitle: "Failure",
2967
+ category: "flowchart",
2968
+ min: 120,
2969
+ max: 240,
2970
+ extraClasses: ["flow-node-warning"]
2971
+ }),
2972
+ commit: defineKind("commit", {
2973
+ shape: "circle",
2974
+ subtitle: "Commit",
2975
+ category: "git",
2976
+ min: 28,
2977
+ max: 44,
2978
+ capabilities: ["symbolic"]
2979
+ }),
2980
+ branch: defineKind("branch", {
2981
+ shape: "pill",
2982
+ subtitle: "Branch",
2983
+ category: "git",
2984
+ min: 72,
2985
+ max: 160,
2986
+ capabilities: ["symbolic"]
2987
+ }),
2988
+ tag: defineKind("tag", {
2989
+ shape: "pill",
2990
+ subtitle: "Tag",
2991
+ category: "git",
2992
+ min: 64,
2993
+ max: 140,
2994
+ capabilities: ["symbolic"]
2995
+ })
2996
+ };
2997
+ for (const [id, spec] of Object.entries({
2998
+ rectangle: {
2999
+ shape: "rectangle",
3000
+ subtitle: "Rectangle",
3001
+ category: "shape",
3002
+ min: 100,
3003
+ max: 240
3004
+ },
3005
+ rounded: {
3006
+ shape: "rounded",
3007
+ subtitle: "Rounded",
3008
+ category: "shape",
3009
+ min: 100,
3010
+ max: 240
3011
+ },
3012
+ pill: {
3013
+ shape: "pill",
3014
+ subtitle: "Pill",
3015
+ category: "shape",
3016
+ min: 88,
3017
+ max: 180,
3018
+ capabilities: ["symbolic"]
3019
+ },
3020
+ circle: {
3021
+ shape: "circle",
3022
+ subtitle: "Circle",
3023
+ category: "shape",
3024
+ min: 36,
3025
+ max: 72,
3026
+ capabilities: ["symbolic"]
3027
+ },
3028
+ ellipse: {
3029
+ shape: "ellipse",
3030
+ subtitle: "Ellipse",
3031
+ category: "shape",
3032
+ min: 100,
3033
+ max: 200
3034
+ },
3035
+ diamond: {
3036
+ shape: "diamond",
3037
+ subtitle: "Diamond",
3038
+ category: "shape",
3039
+ min: 120,
3040
+ max: 280
3041
+ },
3042
+ hexagon: {
3043
+ shape: "hexagon",
3044
+ subtitle: "Hexagon",
3045
+ category: "shape",
3046
+ min: 110,
3047
+ max: 220
3048
+ },
3049
+ triangle: {
3050
+ shape: "triangle",
3051
+ subtitle: "Triangle",
3052
+ category: "shape",
3053
+ min: 64,
3054
+ max: 140,
3055
+ capabilities: ["symbolic"]
3056
+ },
3057
+ parallelogram: {
3058
+ shape: "parallelogram",
3059
+ subtitle: "Parallelogram",
3060
+ category: "shape",
3061
+ min: 120,
3062
+ max: 240
3063
+ },
3064
+ trapezoid: {
3065
+ shape: "trapezoid",
3066
+ subtitle: "Trapezoid",
3067
+ category: "shape",
3068
+ min: 120,
3069
+ max: 240
3070
+ },
3071
+ "folded-document": {
3072
+ shape: "folded-document",
3073
+ subtitle: "Folded document",
3074
+ category: "shape",
3075
+ min: 120,
3076
+ max: 240,
3077
+ capabilities: ["annotation"]
3078
+ },
3079
+ cylinder: {
3080
+ shape: "cylinder",
3081
+ subtitle: "Cylinder",
3082
+ category: "shape",
3083
+ min: 100,
3084
+ max: 200,
3085
+ capabilities: ["datastore"]
3086
+ },
3087
+ boundary: {
3088
+ shape: "boundary",
3089
+ subtitle: "Boundary",
3090
+ category: "shape",
3091
+ min: 160,
3092
+ max: 400,
3093
+ capabilities: ["boundary"]
3094
+ }
3095
+ })) if (!(id in BUILTIN_KINDS)) BUILTIN_KINDS[id] = defineKind(id, spec);
3096
+ const GENERIC_DEFAULTS = {
3097
+ shape: "rounded",
3098
+ subtitle: "Node",
3099
+ classNames: ["flow-node-generic"],
3100
+ cssVars: {},
3101
+ defaultMinWidth: 100,
3102
+ defaultMaxWidth: 200,
3103
+ defaultDepth: 1,
3104
+ category: "general",
3105
+ capabilities: ["card"]
3106
+ };
3107
+ function geometryDefaultsFor(shapeId) {
3108
+ const existing = BUILTIN_KINDS[shapeId];
3109
+ if (existing) return existing;
3110
+ return defineKind(shapeId, {
3111
+ shape: shapeId,
3112
+ subtitle: titleCaseKind(shapeId),
3113
+ category: "shape"
3114
+ });
3115
+ }
3116
+ function getKindDefaults(kind) {
3117
+ const defaults = BUILTIN_KINDS[kind];
3118
+ if (defaults) return {
3119
+ defaults,
3120
+ isBuiltin: true
3121
+ };
3122
+ const shapeId = normalizeShapeId(kind);
3123
+ if (shapeId !== kind && isKnownShapeId(shapeId)) return {
3124
+ defaults: geometryDefaultsFor(shapeId),
3125
+ isBuiltin: true
3126
+ };
3127
+ if (BUILTIN_SHAPE_IDS.includes(kind)) return {
3128
+ defaults: geometryDefaultsFor(kind),
3129
+ isBuiltin: true
3130
+ };
3131
+ return {
3132
+ defaults: {
3133
+ ...GENERIC_DEFAULTS,
3134
+ subtitle: titleCaseKind(kind),
3135
+ classNames: [`flow-node-${sanitizeKindClass(kind)}`, "flow-node-generic"]
3136
+ },
3137
+ isBuiltin: false
3138
+ };
3139
+ }
3140
+ function isBuiltinKind(kind) {
3141
+ return getKindDefaults(kind).isBuiltin;
3142
+ }
3143
+ /**
3144
+ * True when `kind` names a built-in shape (or alias): `diamond`, `hex`, `cloud`, …
3145
+ * Semantic kinds that share a shape id (`cloud`, `document`, `person`, `queue`) count —
3146
+ * they are usable as both meaning and geometry. ERD `table` is excluded.
3147
+ */
3148
+ function isGeometryKind(kind) {
3149
+ const shapeId = normalizeShapeId(kind);
3150
+ if (!isKnownShapeId(shapeId) || shapeId === "table") return false;
3151
+ if (shapeId !== kind) return true;
3152
+ return kind === shapeId && BUILTIN_SHAPE_IDS.includes(kind);
3153
+ }
3154
+ function kindHasCapability(kind, capability) {
3155
+ return getKindDefaults(kind).defaults.capabilities.includes(capability);
3156
+ }
3157
+ /** Human-readable kind subtitle for presentation chrome. */
3158
+ function kindSubtitle(kind) {
3159
+ return getKindDefaults(kind).defaults.subtitle;
3160
+ }
3161
+ function listKindsByCategory(category) {
3162
+ return Object.entries(BUILTIN_KINDS).filter(([, d]) => d.category === category).map(([id]) => id).sort();
3163
+ }
3164
+ /**
3165
+ * Shape ids usable directly as kinds (excludes ERD `table`, which stays semantic-only).
3166
+ * Includes semantic synonyms that share a shape id (`cloud`, `document`, `person`, `queue`).
3167
+ */
3168
+ function listGeometryKinds() {
3169
+ return BUILTIN_SHAPE_IDS.filter((id) => id !== "table" && id in BUILTIN_KINDS).sort();
3170
+ }
3171
+ const BUILTIN_KIND_LIST = Object.keys(BUILTIN_KINDS).sort();
3172
+ const BUILTIN_KIND_CATALOG = BUILTIN_KINDS;
3173
+ //#endregion
3174
+ //#region src/compiler/direction.ts
3175
+ /** Normalize DSL direction aliases to canonical values (§6.5). */
3176
+ function normalizeDirection(value) {
3177
+ switch (value.toLowerCase()) {
3178
+ case "lr":
3179
+ case "right": return "LR";
3180
+ case "rl": return "RL";
3181
+ case "td":
3182
+ case "tb":
3183
+ case "down": return "TD";
3184
+ case "bt": return "BT";
3185
+ default: return value.toUpperCase();
3186
+ }
3187
+ }
3188
+ //#endregion
3189
+ //#region src/compiler/compile-animations.ts
3190
+ const DEFAULT_PULSE_MS = 400;
3191
+ const DEFAULT_FLOW_MS = 600;
3192
+ function compileAnimationBlocks(blocks, nodes, diagnostics) {
3193
+ const usedIds = /* @__PURE__ */ new Set();
3194
+ const animations = [];
3195
+ for (const block of blocks) {
3196
+ let id = animationIdFromName(block.name);
3197
+ if (usedIds.has(id)) {
3198
+ let n = 2;
3199
+ while (usedIds.has(`${id}-${n}`)) n++;
3200
+ id = `${id}-${n}`;
3201
+ }
3202
+ usedIds.add(id);
3203
+ let loop = false;
3204
+ const cues = [];
3205
+ for (const cueAst of block.cues) {
3206
+ if (cueAst.type === "loop") {
3207
+ loop = true;
3208
+ continue;
3209
+ }
3210
+ const cue = compileCue(cueAst, nodes, diagnostics);
3211
+ if (cue) cues.push(cue);
3212
+ }
3213
+ const source = cues.length === 0 ? "auto" : "authored";
3214
+ animations.push({
3215
+ id,
3216
+ name: block.name,
3217
+ loop,
3218
+ cues,
3219
+ source
3220
+ });
3221
+ }
3222
+ return animations;
3223
+ }
3224
+ function compileCue(cue, nodes, diagnostics) {
3225
+ switch (cue.type) {
3226
+ case "dim":
3227
+ case "activate": return {
3228
+ op: cue.type,
3229
+ targets: resolveTargets(cue.targets, nodes, diagnostics, cue.range)
3230
+ };
3231
+ case "pulse": return {
3232
+ op: "pulse",
3233
+ targets: resolveTargets(cue.targets, nodes, diagnostics, cue.range),
3234
+ durationMs: cue.durationMs ?? DEFAULT_PULSE_MS
3235
+ };
3236
+ case "flow":
3237
+ for (const id of cue.path) if (!nodes.has(id)) diagnostics.push({
3238
+ severity: "error",
3239
+ code: "FM128",
3240
+ message: `Unknown node "${id}" in animation flow path`,
3241
+ range: cue.range
3242
+ });
3243
+ return {
3244
+ op: "flow",
3245
+ path: cue.path,
3246
+ durationMs: cue.durationMs ?? DEFAULT_FLOW_MS
3247
+ };
3248
+ case "wait": return {
3249
+ op: "wait",
3250
+ durationMs: cue.durationMs
3251
+ };
3252
+ case "parallel": {
3253
+ const children = [];
3254
+ for (const child of cue.cues) {
3255
+ if (child.type === "loop") {
3256
+ diagnostics.push({
3257
+ severity: "warning",
3258
+ code: "FM129",
3259
+ message: "`loop` inside parallel is ignored — place it on the animation block",
3260
+ range: child.range
3261
+ });
3262
+ continue;
3263
+ }
3264
+ if (child.type === "parallel") {
3265
+ diagnostics.push({
3266
+ severity: "error",
3267
+ code: "FM130",
3268
+ message: "Nested parallel groups are not supported",
3269
+ range: child.range
3270
+ });
3271
+ continue;
3272
+ }
3273
+ const compiled = compileCue(child, nodes, diagnostics);
3274
+ if (compiled) children.push(compiled);
3275
+ }
3276
+ return {
3277
+ op: "parallel",
3278
+ cues: children
3279
+ };
3280
+ }
3281
+ case "loop": return null;
3282
+ }
3283
+ }
3284
+ function resolveTargets(targets, nodes, diagnostics, range) {
3285
+ const out = [];
3286
+ for (const t of targets) {
3287
+ if (t.type === "all") {
3288
+ out.push({ type: "all" });
3289
+ continue;
3290
+ }
3291
+ if (t.type === "node") {
3292
+ if (!nodes.has(t.id)) diagnostics.push({
3293
+ severity: "error",
3294
+ code: "FM128",
3295
+ message: `Unknown node "${t.id}" in animation`,
3296
+ range
3297
+ });
3298
+ out.push({
3299
+ type: "node",
3300
+ id: t.id
3301
+ });
3302
+ continue;
3303
+ }
3304
+ if (!nodes.has(t.from) || !nodes.has(t.to)) diagnostics.push({
3305
+ severity: "error",
3306
+ code: "FM128",
3307
+ message: `Unknown edge endpoints "${t.from} -> ${t.to}" in animation`,
3308
+ range
3309
+ });
3310
+ out.push({
3311
+ type: "edge",
3312
+ from: t.from,
3313
+ to: t.to
3314
+ });
3315
+ }
3316
+ return out;
3317
+ }
3318
+ //#endregion
3319
+ //#region src/compiler/compile-sequence.ts
3320
+ function edgeOpToSequenceKind(op, from, to) {
3321
+ if (from === "*") return "found";
3322
+ if (to === "*") return "lost";
3323
+ switch (op) {
3324
+ case "-->": return "return";
3325
+ case "~>":
3326
+ case "<~":
3327
+ case "=>":
3328
+ case "<=": return "async";
3329
+ case "-x":
3330
+ case "x-": return "failure";
3331
+ default: return "sync";
3332
+ }
3333
+ }
3334
+ function collectStyleRefsFromProperties$1(properties) {
3335
+ const raw = properties.styles ?? properties.style;
3336
+ if (Array.isArray(raw)) return raw.map(String);
3337
+ if (typeof raw === "string") return [raw];
3338
+ return [];
3339
+ }
3340
+ function collectUnresolvedVars$1(properties) {
3341
+ const vars = {};
3342
+ for (const [key, value] of Object.entries(properties)) {
3343
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") continue;
3344
+ if (key.startsWith("--") || key === "iconColor") {
3345
+ if (key === "iconColor") vars["--icon-color"] = String(value);
3346
+ else if (key.startsWith("--")) vars[key] = String(value);
3347
+ }
3348
+ }
3349
+ return vars;
3350
+ }
3351
+ function materializeNode(stmt, nodes, diagnostics) {
3352
+ if (nodes.has(stmt.id)) {
3353
+ diagnostics.push({
3354
+ severity: "error",
3355
+ code: "FM101",
3356
+ message: `Duplicate participant id "${stmt.id}"`,
3357
+ range: stmt.range
3358
+ });
3359
+ return nodes.get(stmt.id) ?? null;
3360
+ }
3361
+ const { defaults, isBuiltin } = getKindDefaults(stmt.kind);
3362
+ if (!isBuiltin) diagnostics.push({
3363
+ severity: "warning",
3364
+ code: "FM102",
3365
+ message: `Unknown node kind "${stmt.kind}" — using generic defaults`,
3366
+ range: stmt.range,
3367
+ hint: "Any kind can be a sequence lifeline; prefer participant, actor, or architecture kinds."
3368
+ });
3369
+ const rawShape = stmt.properties.shape != null ? String(stmt.properties.shape) : void 0;
3370
+ const shapeOverride = rawShape ? normalizeShapeId(rawShape) : void 0;
3371
+ if (rawShape && !isKnownShapeId(shapeOverride)) diagnostics.push({
3372
+ severity: "warning",
3373
+ code: "FM111",
3374
+ message: `Unknown shape "${rawShape}"`,
3375
+ range: stmt.range
3376
+ });
3377
+ const styleRefs = [...collectStyleRefsFromProperties$1(stmt.properties), ...stmt.styleRefs];
3378
+ const unresolvedVars = {
3379
+ ...defaults.cssVars,
3380
+ ...collectUnresolvedVars$1(stmt.properties)
3381
+ };
3382
+ const rawIcon = stmt.properties.icon != null ? String(stmt.properties.icon) : void 0;
3383
+ const icon = rawIcon === "none" ? void 0 : rawIcon !== void 0 ? rawIcon : defaults.shape === "person" ? void 0 : defaults.icon;
3384
+ const authoredPaint = stmt.properties.iconPaint != null ? String(stmt.properties.iconPaint).toLowerCase() : void 0;
3385
+ const iconPaint = authoredPaint === "theme" || authoredPaint === "brand" ? authoredPaint : void 0;
3386
+ const iconColor = stmt.properties.iconColor != null && String(stmt.properties.iconColor).length > 0 ? String(stmt.properties.iconColor) : void 0;
3387
+ const nodeVars = {
3388
+ ...unresolvedVars,
3389
+ ...iconColor ? { "--icon-color": iconColor } : {}
3390
+ };
3391
+ const node = {
3392
+ id: stmt.id,
3393
+ label: stmt.label ?? stmt.id,
3394
+ labelAuthored: stmt.label != null,
3395
+ kind: stmt.kind,
3396
+ shape: shapeOverride ?? defaults.shape,
3397
+ icon,
3398
+ iconPaint,
3399
+ iconColor,
3400
+ minWidth: defaults.defaultMinWidth,
3401
+ maxWidth: defaults.defaultMaxWidth,
3402
+ depth: defaults.defaultDepth,
3403
+ styleRefs,
3404
+ unresolvedVars: Object.keys(nodeVars).length ? nodeVars : void 0,
3405
+ sourceRange: stmt.range
3406
+ };
3407
+ nodes.set(stmt.id, node);
3408
+ return node;
3409
+ }
3410
+ function nextOrder(ctx) {
3411
+ const o = ctx.order;
3412
+ ctx.order += 1;
3413
+ return o;
3414
+ }
3415
+ function ensureParticipant(ctx, id, range) {
3416
+ if (id === "*" || !id) return;
3417
+ if (!ctx.nodes.has(id)) ctx.diagnostics.push({
3418
+ severity: "error",
3419
+ code: "FM147",
3420
+ message: `Unknown participant "${id}" — declare with \`id: kind "Label"\` before use`,
3421
+ range: range ?? {
3422
+ start: {
3423
+ line: 1,
3424
+ column: 1,
3425
+ offset: 0
3426
+ },
3427
+ end: {
3428
+ line: 1,
3429
+ column: 1,
3430
+ offset: 0
3431
+ }
3432
+ }
3433
+ });
3434
+ else if (!ctx.participantOrder.includes(id)) ctx.participantOrder.push(id);
3435
+ }
3436
+ function pushMessage(ctx, partial, edgeMeta) {
3437
+ const order = nextOrder(ctx);
3438
+ const id = partial.id ?? `msg${ctx.edgeCounter++}`;
3439
+ const msg = {
3440
+ ...partial,
3441
+ id,
3442
+ order
3443
+ };
3444
+ ctx.messages.push(msg);
3445
+ const styleRefs = edgeMeta?.styleRefs ?? [];
3446
+ const branch = edgeMeta?.branch ?? (msg.label != null ? classifyBranch(msg.label) : void 0);
3447
+ if (msg.from && msg.to && msg.from !== "*" && msg.to !== "*") ctx.edges.push({
3448
+ id,
3449
+ from: msg.from,
3450
+ to: msg.to,
3451
+ label: msg.label,
3452
+ labelAuthored: msg.labelAuthored,
3453
+ kind: msg.kind === "async" ? "eventual" : msg.kind === "failure" ? "failure" : msg.kind === "return" ? "dependency" : msg.kind === "destroy" ? "failure" : "sync",
3454
+ arrows: "end",
3455
+ styleRefs,
3456
+ branch: branch === "neutral" ? void 0 : branch,
3457
+ sequenceOrder: order,
3458
+ sequenceKind: msg.kind,
3459
+ sourceRange: msg.sourceRange
3460
+ });
3461
+ return msg;
3462
+ }
3463
+ function closeActivation(ctx, participantId, endOrder) {
3464
+ const stack = ctx.openActivations.get(participantId);
3465
+ if (!stack || stack.length === 0) return;
3466
+ const open = stack.pop();
3467
+ ctx.activations.push({
3468
+ id: open.id,
3469
+ participantId,
3470
+ startOrder: open.startOrder,
3471
+ endOrder,
3472
+ sourceRange: open.range
3473
+ });
3474
+ }
3475
+ function openActivation(ctx, participantId, startOrder, range) {
3476
+ const stack = ctx.openActivations.get(participantId) ?? [];
3477
+ stack.push({
3478
+ id: `act${ctx.nextActId++}`,
3479
+ startOrder,
3480
+ range
3481
+ });
3482
+ ctx.openActivations.set(participantId, stack);
3483
+ }
3484
+ function collectSequenceStatements(statements, ctx) {
3485
+ for (const stmt of statements) switch (stmt.type) {
3486
+ case "Node":
3487
+ materializeNode(stmt, ctx.nodes, ctx.diagnostics);
3488
+ if (!ctx.participantOrder.includes(stmt.id)) ctx.participantOrder.push(stmt.id);
3489
+ break;
3490
+ case "Edge": {
3491
+ const from = stmt.from;
3492
+ const to = stmt.to;
3493
+ ensureParticipant(ctx, from, stmt.range);
3494
+ ensureParticipant(ctx, to, stmt.range);
3495
+ const kind = edgeOpToSequenceKind(stmt.op, from, to);
3496
+ const styleRefs = [...collectStyleRefsFromProperties$1(stmt.properties), ...stmt.styleRefs];
3497
+ const branch = normalizeBranch(stmt.properties.branch) ?? (stmt.label != null ? classifyBranch(stmt.label) : void 0);
3498
+ const msg = pushMessage(ctx, {
3499
+ from: from === "*" ? null : from,
3500
+ to: to === "*" ? null : to,
3501
+ kind,
3502
+ label: stmt.label,
3503
+ labelAuthored: stmt.label != null,
3504
+ sourceRange: stmt.range
3505
+ }, {
3506
+ styleRefs,
3507
+ branch
3508
+ });
3509
+ if (kind === "sync" && msg.to) {
3510
+ openActivation(ctx, msg.to, msg.order, stmt.range);
3511
+ if (msg.from) ctx.callStack.push({
3512
+ messageId: msg.id,
3513
+ to: msg.to,
3514
+ from: msg.from
3515
+ });
3516
+ } else if (kind === "return" && msg.from) {
3517
+ const idx = [...ctx.callStack].reverse().findIndex((c) => c.to === msg.from);
3518
+ if (idx >= 0) {
3519
+ const real = ctx.callStack.length - 1 - idx;
3520
+ msg.replyTo = ctx.callStack[real].messageId;
3521
+ ctx.callStack.splice(real, 1);
3522
+ }
3523
+ closeActivation(ctx, msg.from, msg.order);
3524
+ }
3525
+ break;
3526
+ }
3527
+ case "SequenceActivate":
3528
+ ensureParticipant(ctx, stmt.participantId, stmt.range);
3529
+ openActivation(ctx, stmt.participantId, ctx.order, stmt.range);
3530
+ break;
3531
+ case "SequenceDeactivate":
3532
+ ensureParticipant(ctx, stmt.participantId, stmt.range);
3533
+ closeActivation(ctx, stmt.participantId, Math.max(0, ctx.order - 1));
3534
+ break;
3535
+ case "SequenceCreate": {
3536
+ materializeNode(stmt.node, ctx.nodes, ctx.diagnostics);
3537
+ if (!ctx.participantOrder.includes(stmt.node.id)) ctx.participantOrder.push(stmt.node.id);
3538
+ const prev = ctx.messages.at(-1);
3539
+ pushMessage(ctx, {
3540
+ from: prev?.to ?? prev?.from ?? null,
3541
+ to: stmt.node.id,
3542
+ kind: "create",
3543
+ label: stmt.node.label,
3544
+ labelAuthored: stmt.node.label != null,
3545
+ sourceRange: stmt.range
3546
+ });
3547
+ break;
3548
+ }
3549
+ case "SequenceDestroy":
3550
+ ensureParticipant(ctx, stmt.participantId, stmt.range);
3551
+ pushMessage(ctx, {
3552
+ from: stmt.participantId,
3553
+ to: stmt.participantId,
3554
+ kind: "destroy",
3555
+ sourceRange: stmt.range
3556
+ });
3557
+ closeActivation(ctx, stmt.participantId, ctx.order - 1);
3558
+ break;
3559
+ case "SequenceNote": {
3560
+ for (const id of stmt.participantIds) ensureParticipant(ctx, id, stmt.range);
3561
+ const order = nextOrder(ctx);
3562
+ ctx.notes.push({
3563
+ id: `note${ctx.nextNoteId++}`,
3564
+ order,
3565
+ placement: stmt.placement,
3566
+ participantIds: stmt.participantIds,
3567
+ text: stmt.text,
3568
+ sourceRange: stmt.range
3569
+ });
3570
+ break;
3571
+ }
3572
+ case "SequenceDivider": {
3573
+ const order = nextOrder(ctx);
3574
+ ctx.dividers.push({
3575
+ id: `div${ctx.nextDivId++}`,
3576
+ order,
3577
+ label: stmt.label,
3578
+ sourceRange: stmt.range
3579
+ });
3580
+ break;
3581
+ }
3582
+ case "SequenceAutonumber":
3583
+ ctx.autonumber = true;
3584
+ break;
3585
+ case "SequenceFragment":
3586
+ collectFragment(stmt, ctx);
3587
+ break;
3588
+ case "Style":
3589
+ ctx.styles.push({
3590
+ name: stmt.name,
3591
+ target: stmt.target,
3592
+ properties: Object.fromEntries(Object.entries(stmt.properties).map(([k, v]) => [k, String(v)]))
3593
+ });
3594
+ break;
3595
+ case "StyleRef":
3596
+ for (const id of stmt.targetIds) {
3597
+ const node = ctx.nodes.get(id);
3598
+ if (node && !node.styleRefs.includes(stmt.styleName)) node.styleRefs.push(stmt.styleName);
3599
+ }
3600
+ break;
3601
+ case "Directive":
3602
+ case "LayoutBlock":
3603
+ case "EdgePolicyBlock":
3604
+ case "RenderBlock":
3605
+ case "PresentationBlock":
3606
+ case "AnimationBlock": break;
3607
+ default: break;
3608
+ }
3609
+ }
3610
+ function collectFragment(stmt, ctx) {
3611
+ const fragId = `frag${ctx.nextFragId++}`;
3612
+ const startOrder = ctx.order;
3613
+ const operands = [];
3614
+ for (const op of stmt.operands) {
3615
+ const opStart = ctx.order;
3616
+ const childFragsStart = ctx.fragments.length;
3617
+ collectSequenceStatements(op.statements, ctx);
3618
+ const nested = ctx.fragments.splice(childFragsStart);
3619
+ const opEnd = Math.max(opStart, ctx.order - 1);
3620
+ operands.push({
3621
+ label: op.label,
3622
+ styleRefs: [...op.styleRefs],
3623
+ startOrder: opStart,
3624
+ endOrder: opEnd,
3625
+ children: nested
3626
+ });
3627
+ }
3628
+ const endOrder = Math.max(startOrder, ctx.order - 1);
3629
+ ctx.fragments.push({
3630
+ id: fragId,
3631
+ operator: stmt.operator,
3632
+ label: stmt.label,
3633
+ styleRefs: [...stmt.styleRefs],
3634
+ unresolvedVars: {},
3635
+ startOrder,
3636
+ endOrder,
3637
+ operands,
3638
+ sourceRange: stmt.range
3639
+ });
3640
+ }
3641
+ function collectAnimationBlocks$1(statements) {
3642
+ const blocks = [];
3643
+ for (const stmt of statements) {
3644
+ if (stmt.type === "AnimationBlock") blocks.push(stmt);
3645
+ if (stmt.type === "SequenceFragment") for (const op of stmt.operands) blocks.push(...collectAnimationBlocks$1(op.statements));
3646
+ }
3647
+ return blocks;
3648
+ }
3649
+ function finalizeOpenActivations(ctx) {
3650
+ const lastOrder = Math.max(0, ctx.order - 1);
3651
+ for (const [participantId, stack] of ctx.openActivations) while (stack.length) {
3652
+ const open = stack.pop();
3653
+ ctx.activations.push({
3654
+ id: open.id,
3655
+ participantId,
3656
+ startOrder: open.startOrder,
3657
+ endOrder: lastOrder,
3658
+ sourceRange: open.range
3659
+ });
3660
+ }
3661
+ }
3662
+ /** Compile a `sequence { … }` document body into GraphModel + SequenceIR. */
3663
+ function compileSequence(ast, diagramIndex = 0) {
3664
+ const diagnostics = [...ast.diagnostics];
3665
+ const top = ast.body[diagramIndex];
3666
+ if (!top || top.type !== "Sequence") {
3667
+ diagnostics.push({
3668
+ severity: "error",
3669
+ code: "FM100",
3670
+ message: "No sequence diagram found at index",
3671
+ range: {
3672
+ start: {
3673
+ line: 1,
3674
+ column: 1,
3675
+ offset: 0
3676
+ },
3677
+ end: {
3678
+ line: 1,
3679
+ column: 1,
3680
+ offset: 0
3681
+ }
3682
+ }
3683
+ });
3684
+ return {
3685
+ graph: {
3686
+ id: "empty",
3687
+ diagramKind: "sequence",
3688
+ nodes: [],
3689
+ edges: [],
3690
+ groups: [],
3691
+ styles: [],
3692
+ animations: [],
3693
+ diagnostics
3694
+ },
3695
+ layoutHints: {},
3696
+ routingHints: {},
3697
+ renderHints: {},
3698
+ diagnostics
3699
+ };
3700
+ }
3701
+ const seq = top;
3702
+ const ctx = {
3703
+ nodes: /* @__PURE__ */ new Map(),
3704
+ edges: [],
3705
+ styles: [],
3706
+ diagnostics,
3707
+ participantOrder: [],
3708
+ messages: [],
3709
+ activations: [],
3710
+ fragments: [],
3711
+ notes: [],
3712
+ dividers: [],
3713
+ autonumber: false,
3714
+ order: 0,
3715
+ edgeCounter: 0,
3716
+ openActivations: /* @__PURE__ */ new Map(),
3717
+ callStack: [],
3718
+ nextActId: 0,
3719
+ nextFragId: 0,
3720
+ nextNoteId: 0,
3721
+ nextDivId: 0
3722
+ };
3723
+ collectSequenceStatements(seq.statements, ctx);
3724
+ finalizeOpenActivations(ctx);
3725
+ const sequence = {
3726
+ autonumber: ctx.autonumber,
3727
+ messages: ctx.messages,
3728
+ activations: ctx.activations,
3729
+ fragments: ctx.fragments,
3730
+ notes: ctx.notes,
3731
+ dividers: ctx.dividers,
3732
+ participantOrder: ctx.participantOrder
3733
+ };
3734
+ const graph = {
3735
+ id: seq.name ? seq.name.toLowerCase().replace(/\s+/g, "-") : "sequence",
3736
+ title: seq.name,
3737
+ diagramKind: "sequence",
3738
+ sequence,
3739
+ nodes: ctx.participantOrder.map((id) => ctx.nodes.get(id)).filter(Boolean),
3740
+ edges: ctx.edges,
3741
+ groups: [],
3742
+ styles: ctx.styles,
3743
+ animations: compileAnimationBlocks(collectAnimationBlocks$1(seq.statements), ctx.nodes, diagnostics),
3744
+ diagnostics
3745
+ };
3746
+ const hints = extractSequenceHints(seq);
3747
+ diagnostics.push(...hints.diagnostics);
3748
+ const { diagnostics: _d, ...hintFields } = hints;
3749
+ hintFields.layoutHints = {
3750
+ ...hintFields.layoutHints,
3751
+ direction: hintFields.layoutHints.direction ?? "TD"
3752
+ };
3753
+ return {
3754
+ graph,
3755
+ ...hintFields,
3756
+ diagnostics
3757
+ };
3758
+ }
3759
+ function extractSequenceHints(seq) {
3760
+ const layoutHints = {};
3761
+ const routingHints = {};
3762
+ const renderHints = {};
3763
+ const diagnostics = [];
3764
+ let presentationLayers = [];
3765
+ for (const stmt of seq.statements) {
3766
+ if (stmt.type === "Directive") {
3767
+ if (stmt.name === "direction" && stmt.value) layoutHints.direction = normalizeDirection(String(stmt.value));
3768
+ if (stmt.name === "density" && stmt.value) {
3769
+ const d = String(stmt.value);
3770
+ if (d === "compact" || d === "normal" || d === "spacious") layoutHints.density = d;
3771
+ }
3772
+ }
3773
+ if (stmt.type === "LayoutBlock") {
3774
+ if (stmt.properties.direction) layoutHints.direction = normalizeDirection(String(stmt.properties.direction));
3775
+ if (stmt.properties.density) {
3776
+ const d = String(stmt.properties.density);
3777
+ if (d === "compact" || d === "normal" || d === "spacious") layoutHints.density = d;
3778
+ }
3779
+ }
3780
+ if (stmt.type === "EdgePolicyBlock") {
3781
+ if (stmt.properties.route) routingHints.route = String(stmt.properties.route);
3782
+ }
3783
+ if (stmt.type === "RenderBlock") {
3784
+ if (stmt.properties.theme) renderHints.theme = String(stmt.properties.theme);
3785
+ }
3786
+ if (stmt.type === "PresentationBlock") {
3787
+ const layer = presentationFromProperties(stmt.properties);
3788
+ if (layer) presentationLayers.push(layer);
3789
+ }
3790
+ }
3791
+ const merged = mergePresentationOptions(...presentationLayers);
3792
+ if (merged) renderHints.presentation = merged;
3793
+ return {
3794
+ layoutHints,
3795
+ routingHints,
3796
+ renderHints,
3797
+ diagnostics
3798
+ };
3799
+ }
3800
+ //#endregion
3801
+ //#region src/compiler/compile.ts
3802
+ function suggestKindName(kind) {
3803
+ const target = kind.toLowerCase();
3804
+ let best;
3805
+ let bestDist = 3;
3806
+ for (const candidate of BUILTIN_KIND_LIST) {
3807
+ const d = levenshtein(target, candidate.toLowerCase());
3808
+ if (d < bestDist) {
3809
+ bestDist = d;
3810
+ best = candidate;
3811
+ }
3812
+ }
3813
+ return best;
3814
+ }
3815
+ function levenshtein(a, b) {
3816
+ const m = a.length;
3817
+ const n = b.length;
3818
+ const dp = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0));
3819
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
3820
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
3821
+ for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) {
3822
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3823
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
3824
+ }
3825
+ return dp[m][n];
3826
+ }
3827
+ function collectStyleRefsFromProperties(properties) {
3828
+ const refs = [];
3829
+ const styles = properties.styles;
3830
+ if (Array.isArray(styles)) for (const s of styles) refs.push(String(s));
3831
+ return refs;
3832
+ }
3833
+ function collectUnresolvedVars(properties) {
3834
+ const vars = {};
3835
+ for (const [k, v] of Object.entries(properties)) if (k.startsWith("--")) vars[k] = String(v);
3836
+ return Object.keys(vars).length ? vars : void 0;
3837
+ }
3838
+ function edgeOpToKind(op) {
3839
+ switch (op) {
3840
+ case "=>":
3841
+ case "<=": return "async";
3842
+ case "~>":
3843
+ case "<~": return "eventual";
3844
+ case "..>":
3845
+ case "<..":
3846
+ case "-->": return "dependency";
3847
+ case "-x":
3848
+ case "x-": return "failure";
3849
+ case "--": return "association";
3850
+ default: return "sync";
3851
+ }
3852
+ }
3853
+ /** Reverse ops are authored target-first; compile swaps endpoints so routing stays from→to. */
3854
+ function edgeOpIsReverse(op) {
3855
+ return op === "<-" || op === "<=" || op === "<.." || op === "x-" || op === "<~";
3856
+ }
3857
+ function edgeOpArrows(op) {
3858
+ if (op === "<->") return "both";
3859
+ if (op === "--") return "none";
3860
+ return "end";
3861
+ }
3862
+ function normalizePriority(value) {
3863
+ if (value === "low" || value === "normal" || value === "high") return value;
3864
+ }
3865
+ /** Along-path label bias — `labelPosition` or alias `labelAt`. */
3866
+ function normalizeLabelPosition(value) {
3867
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return;
3868
+ const raw = String(value).trim().toLowerCase();
3869
+ if (raw === "start" || raw === "begin" || raw === "source") return "start";
3870
+ if (raw === "middle" || raw === "mid" || raw === "center") return "middle";
3871
+ if (raw === "end" || raw === "target") return "end";
3872
+ }
3873
+ function normalizeGroupLayout(value) {
3874
+ if (value === "auto" || value === "flat" || value === "compound" || value === "swimlane") return value;
3875
+ }
3876
+ /** Normalize DSL nodePlacement to public values (no obscure ELK nicknames). */
3877
+ function normalizeNodePlacement(value) {
3878
+ switch (value) {
3879
+ case "straight":
3880
+ case "balanced":
3881
+ case "basic": return value;
3882
+ default: return;
3883
+ }
3884
+ }
3885
+ function normalizeDensity(value) {
3886
+ if (value === "compact" || value === "normal" || value === "spacious") return value;
3887
+ }
3888
+ function isRemovedDensityAlias(value) {
3889
+ return value === "mid" || value === "comfortable" || value === "roomy";
3890
+ }
3891
+ /** Prefer `gaps`; accept singular `gap`. */
3892
+ function normalizeCrossings(value) {
3893
+ if (value === "none" || value === "gaps" || value === "jumps" || value === "smart") return value;
3894
+ if (value === "gap") return "gaps";
3895
+ }
3896
+ function normalizeBool(value) {
3897
+ if (value === true || value === false) return value;
3898
+ if (value === "true") return true;
3899
+ if (value === "false") return false;
3900
+ }
3901
+ function optionalNumber(value) {
3902
+ if (value == null || value === "") return void 0;
3903
+ const n = Number(value);
3904
+ return Number.isFinite(n) ? n : void 0;
3905
+ }
3906
+ /** Presentation scale — clamp so layout stays sane. */
3907
+ function normalizeScale(value) {
3908
+ const n = optionalNumber(value);
3909
+ if (n == null || n <= 0) return void 0;
3910
+ return Math.min(2, Math.max(.75, n));
3911
+ }
3912
+ function collectStatements(statements, ctx) {
3913
+ for (const stmt of statements) switch (stmt.type) {
3914
+ case "Node": {
3915
+ if (ctx.nodes.has(stmt.id)) {
3916
+ ctx.diagnostics.push({
3917
+ severity: "error",
3918
+ code: "FM101",
3919
+ message: `Duplicate node id "${stmt.id}"`,
3920
+ range: stmt.range
3921
+ });
3922
+ break;
3923
+ }
3924
+ const { defaults, isBuiltin } = getKindDefaults(stmt.kind);
3925
+ if (!isBuiltin) {
3926
+ const suggestion = suggestKindName(stmt.kind);
3927
+ ctx.diagnostics.push({
3928
+ severity: "warning",
3929
+ code: "FM102",
3930
+ message: `Unknown node kind "${stmt.kind}" — using generic defaults`,
3931
+ range: stmt.range,
3932
+ hint: suggestion ? `Did you mean "${suggestion}"?` : "See built-in kinds (service, database, event, process, …) or keep an open kind."
3933
+ });
3934
+ }
3935
+ const columns = parseTableColumns(stmt.properties.columns);
3936
+ const hasErdColumns = columns.length > 0;
3937
+ const rawShape = stmt.properties.shape != null ? String(stmt.properties.shape) : void 0;
3938
+ const shapeOverride = rawShape ? normalizeShapeId(rawShape) : void 0;
3939
+ if (rawShape && !isKnownShapeId(shapeOverride)) ctx.diagnostics.push({
3940
+ severity: "warning",
3941
+ code: "FM111",
3942
+ message: `Unknown shape "${rawShape}" — rendering falls back to rounded unless a custom shape is registered`,
3943
+ range: stmt.range,
3944
+ hint: `Known shapes: rectangle, rounded, pill, diamond, hexagon, cylinder, queue, cloud, document, …`
3945
+ });
3946
+ const erdKind = defaults.capabilities.includes("erd-table") || stmt.kind === "table" || stmt.kind === "entity";
3947
+ const shape = shapeOverride ?? (hasErdColumns && erdKind ? "table" : defaults.shape);
3948
+ const styleRefs = [...collectStyleRefsFromProperties(stmt.properties), ...stmt.styleRefs];
3949
+ const unresolvedVars = {
3950
+ ...defaults.cssVars,
3951
+ ...collectUnresolvedVars(stmt.properties)
3952
+ };
3953
+ const erdMinWidth = hasErdColumns ? Math.max(defaults.defaultMinWidth, 200) : defaults.defaultMinWidth;
3954
+ const erdMaxWidth = hasErdColumns ? Math.max(defaults.defaultMaxWidth, 360) : defaults.defaultMaxWidth;
3955
+ const rawIcon = stmt.properties.icon != null ? String(stmt.properties.icon) : void 0;
3956
+ const icon = rawIcon === "none" ? void 0 : rawIcon !== void 0 ? rawIcon : shape === "person" ? void 0 : defaults.icon;
3957
+ const authoredPaint = stmt.properties.iconPaint != null ? String(stmt.properties.iconPaint).toLowerCase() : void 0;
3958
+ const iconPaint = authoredPaint === "theme" || authoredPaint === "brand" ? authoredPaint : void 0;
3959
+ const iconColor = stmt.properties.iconColor != null && String(stmt.properties.iconColor).length > 0 ? String(stmt.properties.iconColor) : void 0;
3960
+ const nodeVars = {
3961
+ ...unresolvedVars,
3962
+ ...iconColor ? { "--icon-color": iconColor } : {}
3963
+ };
3964
+ const parentGroupId = ctx.groupStack.at(-1);
3965
+ ctx.nodes.set(stmt.id, {
3966
+ id: stmt.id,
3967
+ label: stmt.label ?? stmt.id,
3968
+ labelAuthored: stmt.label != null,
3969
+ kind: stmt.kind,
3970
+ shape,
3971
+ icon,
3972
+ iconPaint,
3973
+ iconColor,
3974
+ groupId: parentGroupId,
3975
+ styleRefs,
3976
+ unresolvedVars: Object.keys(nodeVars).length ? nodeVars : void 0,
3977
+ note: stmt.properties.note != null ? String(stmt.properties.note) : void 0,
3978
+ showSubtitle: stmt.properties.subtitle === true,
3979
+ subtitle: typeof stmt.properties.subtitle === "string" && stmt.properties.subtitle.length > 0 ? String(stmt.properties.subtitle) : void 0,
3980
+ technology: stmt.properties.technology != null && String(stmt.properties.technology).length > 0 ? String(stmt.properties.technology) : void 0,
3981
+ description: stmt.properties.description != null && String(stmt.properties.description).length > 0 ? String(stmt.properties.description) : void 0,
3982
+ columns: hasErdColumns ? columns : void 0,
3983
+ minWidth: stmt.properties.minWidth ?? erdMinWidth,
3984
+ maxWidth: stmt.properties.maxWidth ?? erdMaxWidth,
3985
+ depth: stmt.properties.depth ?? defaults.defaultDepth,
3986
+ scale: normalizeScale(stmt.properties.scale),
3987
+ sourceRange: stmt.range
3988
+ });
3989
+ if (parentGroupId) ctx.groups.find((g) => g.id === parentGroupId)?.members?.push({
3990
+ kind: "node",
3991
+ id: stmt.id
3992
+ });
3993
+ break;
3994
+ }
3995
+ case "Edge": {
3996
+ ctx.edgeCounter++;
3997
+ const fromColumn = stmt.fromColumn ?? (stmt.properties.fromColumn != null ? String(stmt.properties.fromColumn) : stmt.properties.fromCol != null ? String(stmt.properties.fromCol) : void 0);
3998
+ const toColumn = stmt.toColumn ?? (stmt.properties.toColumn != null ? String(stmt.properties.toColumn) : stmt.properties.toCol != null ? String(stmt.properties.toCol) : void 0);
3999
+ const cardinality = parseCardinality(stmt.properties.cardinality ?? stmt.properties.rel) ?? parseCardinality(stmt.label) ?? (fromColumn || toColumn ? {
4000
+ from: "one",
4001
+ to: "zeroOrMany"
4002
+ } : void 0);
4003
+ const branch = normalizeBranch(stmt.properties.branch) ?? (cardinality && isPureCardinalityLabel(stmt.label) ? void 0 : classifyBranch(stmt.label));
4004
+ const identifying = normalizeBool(stmt.properties.identifying);
4005
+ const rawIcon = stmt.properties.icon != null ? String(stmt.properties.icon) : void 0;
4006
+ const icon = rawIcon && rawIcon !== "none" ? rawIcon : void 0;
4007
+ const authoredPaint = stmt.properties.iconPaint != null ? String(stmt.properties.iconPaint).toLowerCase() : void 0;
4008
+ const iconPaint = authoredPaint === "theme" || authoredPaint === "brand" ? authoredPaint : void 0;
4009
+ const iconColor = stmt.properties.iconColor != null && String(stmt.properties.iconColor).length > 0 ? String(stmt.properties.iconColor) : void 0;
4010
+ const reverse = edgeOpIsReverse(stmt.op);
4011
+ const arrowsProp = String(stmt.properties.arrows ?? "").toLowerCase();
4012
+ const arrows = arrowsProp === "both" || arrowsProp === "start" || arrowsProp === "end" || arrowsProp === "none" ? arrowsProp : edgeOpArrows(stmt.op);
4013
+ ctx.edges.push({
4014
+ id: `e${ctx.edgeCounter}`,
4015
+ from: reverse ? stmt.to : stmt.from,
4016
+ to: reverse ? stmt.from : stmt.to,
4017
+ fromColumn: reverse ? toColumn : fromColumn,
4018
+ toColumn: reverse ? fromColumn : toColumn,
4019
+ label: stmt.label,
4020
+ labelAuthored: stmt.label != null,
4021
+ kind: edgeOpToKind(stmt.op),
4022
+ arrows,
4023
+ styleRefs: [...collectStyleRefsFromProperties(stmt.properties), ...stmt.styleRefs],
4024
+ icon,
4025
+ iconPaint,
4026
+ iconColor,
4027
+ priority: normalizePriority(stmt.properties.priority),
4028
+ labelPosition: normalizeLabelPosition(stmt.properties.labelPosition ?? stmt.properties.labelAt),
4029
+ branch,
4030
+ cardinality,
4031
+ identifying: identifying ?? void 0,
4032
+ sourceRange: stmt.range
4033
+ });
4034
+ break;
4035
+ }
4036
+ case "Group": {
4037
+ const groupId = stmt.id ?? `group_${ctx.groups.length + 1}`;
4038
+ const hints = extractGroupArrangeHints(stmt.statements);
4039
+ const rawIcon = hints.icon;
4040
+ const icon = rawIcon && rawIcon !== "none" ? rawIcon : void 0;
4041
+ const group = {
4042
+ id: groupId,
4043
+ label: stmt.label ?? groupId,
4044
+ labelAuthored: stmt.label != null,
4045
+ kind: stmt.groupKind,
4046
+ parentId: ctx.groupStack.at(-1),
4047
+ nodeIds: [],
4048
+ childGroupIds: [],
4049
+ members: [],
4050
+ styleRefs: [],
4051
+ paddingHint: hints.paddingHint,
4052
+ chrome: hints.chrome,
4053
+ icon,
4054
+ iconPaint: hints.iconPaint,
4055
+ iconColor: hints.iconColor,
4056
+ arrange: hints.arrange,
4057
+ align: hints.align,
4058
+ gap: hints.gap,
4059
+ columns: hints.columns,
4060
+ rows: hints.rows,
4061
+ column: hints.column,
4062
+ row: hints.row,
4063
+ span: hints.span,
4064
+ colSpan: hints.colSpan,
4065
+ rowSpan: hints.rowSpan,
4066
+ cellArrange: hints.cellArrange
4067
+ };
4068
+ if (ctx.groupStack.length > 0) {
4069
+ const parent = ctx.groups.find((g) => g.id === ctx.groupStack.at(-1));
4070
+ parent?.childGroupIds.push(groupId);
4071
+ parent?.members?.push({
4072
+ kind: "group",
4073
+ id: groupId
4074
+ });
4075
+ }
4076
+ ctx.groups.push(group);
4077
+ ctx.groupStack.push(groupId);
4078
+ collectStatements(stmt.statements, ctx);
4079
+ for (const n of ctx.nodes.values()) if (n.groupId === groupId && !group.nodeIds.includes(n.id)) group.nodeIds.push(n.id);
4080
+ if (group.arrange === "stack" && group.childGroupIds.length === 0 && group.cellArrange == null) {
4081
+ group.cellArrange = "stack";
4082
+ group.arrange = void 0;
4083
+ }
4084
+ ctx.groupStack.pop();
4085
+ break;
4086
+ }
4087
+ case "Style": {
4088
+ const props = {};
4089
+ for (const [k, v] of Object.entries(stmt.properties)) props[k] = String(v);
4090
+ ctx.styles.push({
4091
+ name: stmt.name,
4092
+ target: stmt.target,
4093
+ properties: props
4094
+ });
4095
+ break;
4096
+ }
4097
+ case "StyleRef": {
4098
+ const styleDef = ctx.styles.find((s) => s.name === stmt.styleName);
4099
+ for (const id of stmt.targetIds) {
4100
+ const node = ctx.nodes.get(id);
4101
+ if (node) {
4102
+ if (!node.styleRefs.includes(stmt.styleName)) node.styleRefs.push(stmt.styleName);
4103
+ } else ctx.diagnostics.push({
4104
+ severity: "warning",
4105
+ code: "FM105",
4106
+ message: styleDef?.target === "edge" ? `Style "${stmt.styleName}" targets edges — use \`a -> b is ${stmt.styleName}\` or styles: […] on the edge` : `Style ref "${stmt.styleName}" references unknown node "${id}"`,
4107
+ range: stmt.range
4108
+ });
4109
+ }
4110
+ break;
4111
+ }
4112
+ case "GroupMember": {
4113
+ const groupId = ctx.groupStack.at(-1);
4114
+ if (!groupId) break;
4115
+ const group = ctx.groups.find((g) => g.id === groupId);
4116
+ for (const nodeId of stmt.nodeIds) {
4117
+ const node = ctx.nodes.get(nodeId);
4118
+ if (node) {
4119
+ node.groupId = groupId;
4120
+ if (group && !group.nodeIds.includes(nodeId)) group.nodeIds.push(nodeId);
4121
+ if (group && !group.members?.some((m) => m.kind === "node" && m.id === nodeId)) group.members?.push({
4122
+ kind: "node",
4123
+ id: nodeId
4124
+ });
4125
+ } else ctx.pendingGroupMembers.push({
4126
+ nodeId,
4127
+ groupId,
4128
+ range: stmt.range
4129
+ });
4130
+ }
4131
+ break;
4132
+ }
4133
+ default: break;
4134
+ }
4135
+ }
4136
+ function extractGroupPaddingHint(statements) {
4137
+ for (const stmt of statements) if (stmt.type === "Directive" && stmt.name === "padding" && stmt.value != null) return String(stmt.value);
4138
+ }
4139
+ function normalizeGroupChrome(value) {
4140
+ if (value === true || value === "true" || value === "on") return true;
4141
+ if (value === false || value === "false" || value === "none" || value === "off") return false;
4142
+ }
4143
+ function normalizeRegionArrange(value) {
4144
+ if (value === "stack" || value === "row" || value === "grid") return value;
4145
+ }
4146
+ function normalizeRegionAlign(value) {
4147
+ if (value === "stretch" || value === "start" || value === "center" || value === "end") return value;
4148
+ }
4149
+ function normalizeCellArrange(value) {
4150
+ if (value === "flow" || value === "pack" || value === "stack") return value;
4151
+ }
4152
+ function normalizeTrackSpec(value) {
4153
+ if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
4154
+ if (typeof value === "string") {
4155
+ const n = Number(value);
4156
+ if (Number.isFinite(n) && n >= 1) return Math.floor(n);
4157
+ return;
4158
+ }
4159
+ if (Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string")) return value.map(String);
4160
+ }
4161
+ function normalizeTrackRef(value) {
4162
+ if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
4163
+ if (typeof value === "string") {
4164
+ const n = Number(value);
4165
+ if (Number.isFinite(n) && n >= 1 && String(n) === value.trim()) return Math.floor(n);
4166
+ if (value.trim()) return value.trim();
4167
+ }
4168
+ }
4169
+ function normalizePositiveInt(value) {
4170
+ const n = typeof value === "number" ? value : Number(value);
4171
+ if (!Number.isFinite(n) || n < 1) return void 0;
4172
+ return Math.floor(n);
4173
+ }
4174
+ function normalizeGap(value) {
4175
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4176
+ if (typeof value === "string" && value.trim()) {
4177
+ const n = Number(value);
4178
+ if (Number.isFinite(n)) return n;
4179
+ const named = value.trim();
4180
+ if (named === "compact" || named === "normal" || named === "spacious") return named;
4181
+ return;
4182
+ }
4183
+ }
4184
+ function extractGroupArrangeHints(statements) {
4185
+ const hints = { paddingHint: extractGroupPaddingHint(statements) };
4186
+ for (const stmt of statements) {
4187
+ if (stmt.type !== "Directive" || stmt.value == null) continue;
4188
+ switch (stmt.name) {
4189
+ case "chrome": {
4190
+ const chrome = normalizeGroupChrome(stmt.value);
4191
+ if (chrome != null) hints.chrome = chrome;
4192
+ break;
4193
+ }
4194
+ case "icon":
4195
+ hints.icon = String(stmt.value);
4196
+ break;
4197
+ case "iconPaint": {
4198
+ const paint = String(stmt.value).toLowerCase();
4199
+ if (paint === "theme" || paint === "brand") hints.iconPaint = paint;
4200
+ break;
4201
+ }
4202
+ case "iconColor": {
4203
+ const color = String(stmt.value).trim();
4204
+ if (color.length > 0) hints.iconColor = color;
4205
+ break;
4206
+ }
4207
+ case "arrange": {
4208
+ const region = normalizeRegionArrange(stmt.value);
4209
+ const cell = normalizeCellArrange(stmt.value);
4210
+ if (region) hints.arrange = region;
4211
+ else if (cell) hints.cellArrange = cell;
4212
+ break;
4213
+ }
4214
+ case "align": {
4215
+ const align = normalizeRegionAlign(stmt.value);
4216
+ if (align) hints.align = align;
4217
+ break;
4218
+ }
4219
+ case "gap": {
4220
+ const gap = normalizeGap(stmt.value);
4221
+ if (gap != null) hints.gap = gap;
4222
+ break;
4223
+ }
4224
+ case "columns": {
4225
+ const tracks = normalizeTrackSpec(stmt.value);
4226
+ if (tracks != null) hints.columns = tracks;
4227
+ break;
4228
+ }
4229
+ case "rows": {
4230
+ const tracks = normalizeTrackSpec(stmt.value);
4231
+ if (tracks != null) hints.rows = tracks;
4232
+ break;
4233
+ }
4234
+ case "column": {
4235
+ const ref = normalizeTrackRef(stmt.value);
4236
+ if (ref != null) hints.column = ref;
4237
+ break;
4238
+ }
4239
+ case "row": {
4240
+ const ref = normalizeTrackRef(stmt.value);
4241
+ if (ref != null) hints.row = ref;
4242
+ break;
4243
+ }
4244
+ case "span": {
4245
+ const n = normalizePositiveInt(stmt.value);
4246
+ if (n != null) hints.span = n;
4247
+ break;
4248
+ }
4249
+ case "colSpan": {
4250
+ const n = normalizePositiveInt(stmt.value);
4251
+ if (n != null) hints.colSpan = n;
4252
+ break;
4253
+ }
4254
+ case "rowSpan": {
4255
+ const n = normalizePositiveInt(stmt.value);
4256
+ if (n != null) hints.rowSpan = n;
4257
+ break;
4258
+ }
4259
+ default: break;
4260
+ }
4261
+ }
4262
+ return hints;
4263
+ }
4264
+ function removedPresentationDiagnostics(props, range) {
4265
+ const out = [];
4266
+ for (const key of REMOVED_PRESENTATION_PROPS) {
4267
+ if (!(key in props)) continue;
4268
+ out.push({
4269
+ severity: "warning",
4270
+ code: "FM210",
4271
+ message: `presentation.${key} was removed — SVG is chromeless by default; set title, legend, padding, groupAccent, etc. explicitly`,
4272
+ range,
4273
+ hint: key === "preset" ? "Presets (document/slide/print/embed) no longer exist." : `Drop ${key}; backgrounds, grids, and frames are not part of the SVG embed model.`
4274
+ });
4275
+ }
4276
+ return out;
4277
+ }
4278
+ function extractHints(diagram) {
4279
+ const layoutHints = {};
4280
+ const routingHints = {};
4281
+ const renderHints = {};
4282
+ const diagnostics = [];
4283
+ let presentationLayers = [];
4284
+ for (const stmt of diagram.statements) {
4285
+ if (stmt.type === "Directive") {
4286
+ if (stmt.name === "direction" && stmt.value) layoutHints.direction = normalizeDirection(String(stmt.value));
4287
+ if (stmt.name === "density" && stmt.value) {
4288
+ if (isRemovedDensityAlias(stmt.value)) diagnostics.push({
4289
+ severity: "error",
4290
+ code: "FM112",
4291
+ message: `density "${String(stmt.value)}" is not supported`,
4292
+ range: stmt.range,
4293
+ hint: "Use density compact, normal, or spacious"
4294
+ });
4295
+ const density = normalizeDensity(stmt.value);
4296
+ if (density) layoutHints.density = density;
4297
+ }
4298
+ }
4299
+ if (stmt.type === "LayoutBlock") {
4300
+ if (stmt.properties.direction) layoutHints.direction = normalizeDirection(String(stmt.properties.direction));
4301
+ if (stmt.properties.density) {
4302
+ if (isRemovedDensityAlias(stmt.properties.density)) diagnostics.push({
4303
+ severity: "error",
4304
+ code: "FM112",
4305
+ message: `density "${String(stmt.properties.density)}" is not supported`,
4306
+ range: stmt.range,
4307
+ hint: "Use density compact, normal, or spacious"
4308
+ });
4309
+ const density = normalizeDensity(stmt.properties.density);
4310
+ if (density) layoutHints.density = density;
4311
+ }
4312
+ if (stmt.properties.spacingScale != null) {
4313
+ const spacingScale = optionalNumber(stmt.properties.spacingScale);
4314
+ if (spacingScale != null) layoutHints.spacingScale = spacingScale;
4315
+ }
4316
+ const groupLayout = normalizeGroupLayout(stmt.properties.groupLayout);
4317
+ if (groupLayout) layoutHints.groupLayout = groupLayout;
4318
+ const nodePlacement = normalizeNodePlacement(stmt.properties.nodePlacement);
4319
+ if (nodePlacement) layoutHints.nodePlacement = nodePlacement;
4320
+ const considerModelOrder = normalizeBool(stmt.properties.considerModelOrder);
4321
+ if (considerModelOrder != null) layoutHints.considerModelOrder = considerModelOrder;
4322
+ const groupGap = optionalNumber(stmt.properties.groupGap);
4323
+ if (groupGap != null) layoutHints.groupGap = groupGap;
4324
+ const edgeNodeSpacing = optionalNumber(stmt.properties.edgeNodeSpacing);
4325
+ if (edgeNodeSpacing != null) layoutHints.edgeNodeSpacing = edgeNodeSpacing;
4326
+ const edgeEdgeSpacing = optionalNumber(stmt.properties.edgeEdgeSpacing);
4327
+ if (edgeEdgeSpacing != null) layoutHints.edgeEdgeSpacing = edgeEdgeSpacing;
4328
+ const edgeLabelSpacing = optionalNumber(stmt.properties.edgeLabelSpacing);
4329
+ if (edgeLabelSpacing != null) layoutHints.edgeLabelSpacing = edgeLabelSpacing;
4330
+ const arrange = normalizeRegionArrange(stmt.properties.arrange);
4331
+ if (arrange) layoutHints.arrange = arrange;
4332
+ const align = normalizeRegionAlign(stmt.properties.align);
4333
+ if (align) layoutHints.align = align;
4334
+ const gap = normalizeGap(stmt.properties.gap);
4335
+ if (gap != null) layoutHints.gap = gap;
4336
+ const columns = normalizeTrackSpec(stmt.properties.columns);
4337
+ if (columns != null) layoutHints.columns = columns;
4338
+ const rows = normalizeTrackSpec(stmt.properties.rows);
4339
+ if (rows != null) layoutHints.rows = rows;
4340
+ const edgeGap = stmt.properties.edgeGap != null ? String(stmt.properties.edgeGap) : "";
4341
+ if (edgeGap === "tight" || edgeGap === "normal" || edgeGap === "wide") {
4342
+ const p = {
4343
+ tight: {
4344
+ edgeNodeSpacing: 28,
4345
+ edgeEdgeSpacing: 14,
4346
+ edgeLabelSpacing: 10
4347
+ },
4348
+ normal: {
4349
+ edgeNodeSpacing: 36,
4350
+ edgeEdgeSpacing: 28,
4351
+ edgeLabelSpacing: 16
4352
+ },
4353
+ wide: {
4354
+ edgeNodeSpacing: 72,
4355
+ edgeEdgeSpacing: 56,
4356
+ edgeLabelSpacing: 28
4357
+ }
4358
+ }[edgeGap];
4359
+ if (layoutHints.edgeNodeSpacing == null) layoutHints.edgeNodeSpacing = p.edgeNodeSpacing;
4360
+ if (layoutHints.edgeEdgeSpacing == null) layoutHints.edgeEdgeSpacing = p.edgeEdgeSpacing;
4361
+ if (layoutHints.edgeLabelSpacing == null) layoutHints.edgeLabelSpacing = p.edgeLabelSpacing;
4362
+ }
4363
+ }
4364
+ if (stmt.type === "EdgePolicyBlock") {
4365
+ if (stmt.properties.route) routingHints.route = String(stmt.properties.route);
4366
+ if (stmt.properties.crossings) {
4367
+ const crossings = normalizeCrossings(stmt.properties.crossings);
4368
+ if (crossings) routingHints.crossings = crossings;
4369
+ }
4370
+ if (stmt.properties.cornerRadius != null) routingHints.cornerRadius = Number(stmt.properties.cornerRadius);
4371
+ if (stmt.properties.parallel) routingHints.parallel = String(stmt.properties.parallel);
4372
+ if (stmt.properties.arrowheads != null) routingHints.arrowheads = stmt.properties.arrowheads !== false;
4373
+ }
4374
+ if (stmt.type === "PresentationBlock") {
4375
+ diagnostics.push(...removedPresentationDiagnostics(stmt.properties, stmt.range));
4376
+ presentationLayers.push(presentationFromProperties(stmt.properties));
4377
+ }
4378
+ if (stmt.type === "RenderBlock") {
4379
+ if (stmt.properties.theme) renderHints.theme = String(stmt.properties.theme);
4380
+ if (stmt.properties.shadows != null) renderHints.shadows = stmt.properties.shadows !== false;
4381
+ if (stmt.properties.roundedCorners != null) renderHints.roundedCorners = stmt.properties.roundedCorners !== false;
4382
+ const presentationPreset = stmt.properties.presentation;
4383
+ if (typeof presentationPreset === "string") diagnostics.push({
4384
+ severity: "warning",
4385
+ code: "FM210",
4386
+ message: `render.presentation: "${presentationPreset}" presets were removed — SVG is chromeless by default`,
4387
+ range: stmt.range,
4388
+ hint: "Use presentation { title, padding, groupAccent, … } for opt-in chrome."
4389
+ });
4390
+ else {
4391
+ diagnostics.push(...removedPresentationDiagnostics(stmt.properties, stmt.range));
4392
+ presentationLayers.push(presentationFromProperties(stmt.properties));
4393
+ }
4394
+ }
4395
+ }
4396
+ const mergedPresentation = mergePresentationOptions(...presentationLayers);
4397
+ if (mergedPresentation) renderHints.presentation = mergedPresentation;
4398
+ return {
4399
+ layoutHints,
4400
+ routingHints,
4401
+ renderHints,
4402
+ diagnostics
4403
+ };
4404
+ }
4405
+ function compile(ast, diagramIndex = 0) {
4406
+ const top = ast.body[diagramIndex];
4407
+ if (top?.type === "Sequence") return compileSequence(ast, diagramIndex);
4408
+ const diagnostics = [...ast.diagnostics];
4409
+ const diagram = top;
4410
+ if (!diagram || diagram.type !== "Diagram") {
4411
+ diagnostics.push({
4412
+ severity: "error",
4413
+ code: "FM100",
4414
+ message: "No diagram found in document",
4415
+ range: {
4416
+ start: {
4417
+ line: 1,
4418
+ column: 1,
4419
+ offset: 0
4420
+ },
4421
+ end: {
4422
+ line: 1,
4423
+ column: 1,
4424
+ offset: 0
4425
+ }
4426
+ }
4427
+ });
4428
+ return {
4429
+ graph: {
4430
+ id: "empty",
4431
+ nodes: [],
4432
+ edges: [],
4433
+ groups: [],
4434
+ styles: [],
4435
+ animations: [],
4436
+ diagnostics
4437
+ },
4438
+ layoutHints: {},
4439
+ routingHints: {},
4440
+ renderHints: {},
4441
+ diagnostics
4442
+ };
4443
+ }
4444
+ const nodes = /* @__PURE__ */ new Map();
4445
+ const edges = [];
4446
+ const groups = [];
4447
+ const styles = [];
4448
+ const pendingGroupMembers = [];
4449
+ collectStatements(diagram.statements, {
4450
+ nodes,
4451
+ edges,
4452
+ groups,
4453
+ styles,
4454
+ diagnostics,
4455
+ groupStack: [],
4456
+ edgeCounter: 0,
4457
+ pendingGroupMembers
4458
+ });
4459
+ for (const pending of pendingGroupMembers) {
4460
+ const node = nodes.get(pending.nodeId);
4461
+ const group = groups.find((g) => g.id === pending.groupId);
4462
+ if (node && group) {
4463
+ node.groupId = pending.groupId;
4464
+ if (!group.nodeIds.includes(pending.nodeId)) group.nodeIds.push(pending.nodeId);
4465
+ if (!group.members?.some((m) => m.kind === "node" && m.id === pending.nodeId)) group.members?.push({
4466
+ kind: "node",
4467
+ id: pending.nodeId
4468
+ });
4469
+ } else diagnostics.push({
4470
+ severity: "warning",
4471
+ code: "FM106",
4472
+ message: `Group membership references unknown node "${pending.nodeId}"`,
4473
+ range: pending.range
4474
+ });
4475
+ }
4476
+ for (const edge of edges) {
4477
+ if (!nodes.has(edge.from)) diagnostics.push({
4478
+ severity: "error",
4479
+ code: "FM103",
4480
+ message: `Edge references unknown node "${edge.from}"`,
4481
+ range: edge.sourceRange ?? diagram.range
4482
+ });
4483
+ if (!nodes.has(edge.to)) diagnostics.push({
4484
+ severity: "error",
4485
+ code: "FM104",
4486
+ message: `Edge references unknown node "${edge.to}"`,
4487
+ range: edge.sourceRange ?? diagram.range
4488
+ });
4489
+ }
4490
+ let edgeCounter = edges.length;
4491
+ for (const node of nodes.values()) {
4492
+ if (!node.columns) continue;
4493
+ for (const col of node.columns) {
4494
+ const ref = col.references;
4495
+ if (!ref) continue;
4496
+ const target = nodes.get(ref.table);
4497
+ if (!target) {
4498
+ diagnostics.push({
4499
+ severity: "error",
4500
+ code: "FM107",
4501
+ message: `FK "${node.id}.${col.name}" references unknown table "${ref.table}"`,
4502
+ range: node.sourceRange ?? diagram.range
4503
+ });
4504
+ continue;
4505
+ }
4506
+ if (findColumnIndex(target.columns, ref.column) < 0 && target.columns?.length) diagnostics.push({
4507
+ severity: "warning",
4508
+ code: "FM108",
4509
+ message: `FK "${node.id}.${col.name}" references unknown column "${ref.table}.${ref.column}"`,
4510
+ range: node.sourceRange ?? diagram.range
4511
+ });
4512
+ const card = fkCardinality(col.notNull);
4513
+ const existing = edges.find((e) => e.from === ref.table && e.to === node.id && (e.fromColumn == null || e.fromColumn === ref.column) && (e.toColumn == null || e.toColumn === col.name));
4514
+ if (existing) {
4515
+ existing.fromColumn ??= ref.column;
4516
+ existing.toColumn ??= col.name;
4517
+ existing.cardinality ??= card;
4518
+ if (existing.identifying == null) existing.identifying = false;
4519
+ continue;
4520
+ }
4521
+ const reverse = edges.find((e) => e.from === node.id && e.to === ref.table && (e.fromColumn == null || e.fromColumn === col.name) && (e.toColumn == null || e.toColumn === ref.column));
4522
+ if (reverse) {
4523
+ reverse.fromColumn ??= col.name;
4524
+ reverse.toColumn ??= ref.column;
4525
+ reverse.cardinality ??= {
4526
+ from: card.to,
4527
+ to: card.from
4528
+ };
4529
+ if (reverse.identifying == null) reverse.identifying = false;
4530
+ continue;
4531
+ }
4532
+ edgeCounter++;
4533
+ edges.push({
4534
+ id: `e${edgeCounter}`,
4535
+ from: ref.table,
4536
+ to: node.id,
4537
+ fromColumn: ref.column,
4538
+ toColumn: col.name,
4539
+ kind: "sync",
4540
+ styleRefs: [],
4541
+ cardinality: card,
4542
+ identifying: false,
4543
+ sourceRange: node.sourceRange
4544
+ });
4545
+ }
4546
+ }
4547
+ for (const edge of edges) {
4548
+ const fromNode = nodes.get(edge.from);
4549
+ const toNode = nodes.get(edge.to);
4550
+ if (edge.fromColumn && fromNode) {
4551
+ if (findColumnIndex(fromNode.columns, edge.fromColumn) < 0) diagnostics.push({
4552
+ severity: "warning",
4553
+ code: "FM109",
4554
+ message: `Edge column "${edge.from}.${edge.fromColumn}" not found on table`,
4555
+ range: edge.sourceRange ?? diagram.range
4556
+ });
4557
+ }
4558
+ if (edge.toColumn && toNode) {
4559
+ if (findColumnIndex(toNode.columns, edge.toColumn) < 0) diagnostics.push({
4560
+ severity: "warning",
4561
+ code: "FM110",
4562
+ message: `Edge column "${edge.to}.${edge.toColumn}" not found on table`,
4563
+ range: edge.sourceRange ?? diagram.range
4564
+ });
4565
+ }
4566
+ if (edge.fromColumn && edge.toColumn && !parseCardinality(edge.label)) {
4567
+ const toFk = toNode?.columns?.find((c) => c.name === edge.toColumn && c.keys.includes("fk"));
4568
+ const fromFk = fromNode?.columns?.find((c) => c.name === edge.fromColumn && c.keys.includes("fk"));
4569
+ const refined = toFk ? fkCardinality(toFk.notNull) : fromFk ? (() => {
4570
+ const c = fkCardinality(fromFk.notNull);
4571
+ return {
4572
+ from: c.to,
4573
+ to: c.from
4574
+ };
4575
+ })() : void 0;
4576
+ if (refined) {
4577
+ const looksLikeGenericDefault = edge.cardinality?.from === "one" && edge.cardinality?.to === "zeroOrMany";
4578
+ if (!edge.cardinality || looksLikeGenericDefault) edge.cardinality = refined;
4579
+ }
4580
+ }
4581
+ if (edge.identifying == null && (edge.fromColumn || edge.toColumn) && fromNode?.shape === "table" && toNode?.shape === "table") edge.identifying = false;
4582
+ }
4583
+ if (diagram.diagramKind === "state") {
4584
+ const initialNodes = [...nodes.values()].filter((node) => node.kind === "initial");
4585
+ const finalNodes = [...nodes.values()].filter((node) => node.kind === "final");
4586
+ if (initialNodes.length !== 1) diagnostics.push({
4587
+ severity: initialNodes.length === 0 ? "warning" : "error",
4588
+ code: "FM150",
4589
+ message: initialNodes.length === 0 ? "State diagram has no initial pseudostate" : "State diagram must have exactly one initial pseudostate",
4590
+ range: initialNodes[1]?.sourceRange ?? diagram.range,
4591
+ hint: "Declare one symbolic entry node, for example `entry: initial \"Start\"`."
4592
+ });
4593
+ if (finalNodes.length === 0) diagnostics.push({
4594
+ severity: "warning",
4595
+ code: "FM151",
4596
+ message: "State diagram has no final state",
4597
+ range: diagram.range,
4598
+ hint: "Declare at least one terminal node, for example `done: final \"Completed\"`."
4599
+ });
4600
+ for (const node of initialNodes) {
4601
+ const incoming = edges.filter((edge) => edge.to === node.id);
4602
+ const outgoing = edges.filter((edge) => edge.from === node.id);
4603
+ if (incoming.length > 0) diagnostics.push({
4604
+ severity: "error",
4605
+ code: "FM152",
4606
+ message: `Initial pseudostate "${node.id}" cannot have incoming transitions`,
4607
+ range: incoming[0]?.sourceRange ?? node.sourceRange ?? diagram.range
4608
+ });
4609
+ if (outgoing.length !== 1) diagnostics.push({
4610
+ severity: "warning",
4611
+ code: "FM153",
4612
+ message: `Initial pseudostate "${node.id}" should have exactly one outgoing transition`,
4613
+ range: node.sourceRange ?? diagram.range
4614
+ });
4615
+ }
4616
+ for (const node of finalNodes) {
4617
+ const outgoing = edges.find((edge) => edge.from === node.id);
4618
+ if (outgoing) diagnostics.push({
4619
+ severity: "error",
4620
+ code: "FM154",
4621
+ message: `Final state "${node.id}" cannot have outgoing transitions`,
4622
+ range: outgoing.sourceRange ?? node.sourceRange ?? diagram.range
4623
+ });
4624
+ }
4625
+ }
4626
+ const graph = {
4627
+ id: diagram.name ? diagram.name.toLowerCase().replace(/\s+/g, "-") : "diagram",
4628
+ title: diagram.name,
4629
+ diagramKind: diagram.diagramKind,
4630
+ nodes: [...nodes.values()],
4631
+ edges,
4632
+ groups,
4633
+ styles,
4634
+ animations: compileAnimationBlocks(collectAnimationBlocks(diagram.statements), nodes, diagnostics),
4635
+ diagnostics
4636
+ };
4637
+ const hints = extractHints(diagram);
4638
+ diagnostics.push(...hints.diagnostics);
4639
+ const { diagnostics: _hintDiags, ...hintFields } = hints;
4640
+ if (diagram.diagramKind === "state") hintFields.layoutHints = {
4641
+ ...hintFields.layoutHints,
4642
+ direction: hintFields.layoutHints.direction ?? "TD"
4643
+ };
4644
+ return {
4645
+ graph,
4646
+ ...hintFields,
4647
+ diagnostics
4648
+ };
4649
+ }
4650
+ function collectAnimationBlocks(statements) {
4651
+ const blocks = [];
4652
+ for (const stmt of statements) {
4653
+ if (stmt.type === "AnimationBlock") blocks.push(stmt);
4654
+ if (stmt.type === "Group") blocks.push(...collectAnimationBlocks(stmt.statements));
4655
+ }
4656
+ return blocks;
4657
+ }
4658
+ //#endregion
4659
+ //#region src/format/print.ts
4660
+ const INDENT = " ";
4661
+ function formatSource(source) {
4662
+ const ast = parse(source);
4663
+ if (ast.diagnostics.some((d) => d.severity === "error")) return source.trimEnd() + "\n";
4664
+ return printDocument(ast);
4665
+ }
4666
+ function printDocument(ast) {
4667
+ const parts = [];
4668
+ if (ast.version != null) parts.push(`kdiagram ${ast.version}`);
4669
+ parts.push(...ast.body.map(printTopLevel));
4670
+ return parts.join("\n\n") + "\n";
4671
+ }
4672
+ function printTopLevel(node) {
4673
+ if (node.type === "Sequence") return printSequence(node);
4674
+ return printDiagram(node);
4675
+ }
4676
+ function printDiagram(diagram) {
4677
+ const keyword = diagram.diagramKind === "state" ? "state" : "diagram";
4678
+ const lines = [diagram.name != null ? `${keyword} "${diagram.name}" {` : `${keyword} {`];
4679
+ const policy = diagram.statements.filter(isDiagramPolicyStatement);
4680
+ const content = diagram.statements.filter((statement) => !isDiagramPolicyStatement(statement));
4681
+ for (const statement of policy) printStatement(statement, 1, lines);
4682
+ if (policy.length > 0 && content.length > 0) lines.push("");
4683
+ let previousContent;
4684
+ for (const statement of content) {
4685
+ const adjacentInSource = previousContent != null && diagram.statements.indexOf(statement) === diagram.statements.indexOf(previousContent) + 1;
4686
+ if (previousContent && adjacentInSource && hasBlankLineBetween(previousContent, statement)) lines.push("");
4687
+ printStatement(statement, 1, lines);
4688
+ previousContent = statement;
4689
+ }
4690
+ lines.push("}");
4691
+ return lines.join("\n");
4692
+ }
4693
+ function isDiagramPolicyStatement(statement) {
4694
+ return diagramPolicyRank(statement) < Number.POSITIVE_INFINITY;
4695
+ }
4696
+ function diagramPolicyRank(statement) {
4697
+ if (statement.type === "Directive" && statement.name === "direction") return 0;
4698
+ if (statement.type === "Directive" && statement.name === "density") return 1;
4699
+ if (statement.type === "LayoutBlock") return 2;
4700
+ if (statement.type === "EdgePolicyBlock") return 3;
4701
+ if (statement.type === "RenderBlock") return 4;
4702
+ if (statement.type === "PresentationBlock") return 5;
4703
+ return Number.POSITIVE_INFINITY;
4704
+ }
4705
+ function printSequence(seq) {
4706
+ const lines = [seq.name != null ? `sequence "${seq.name}" {` : "sequence {"];
4707
+ printSeparated(seq.statements, lines, (stmt) => printSequenceStatement(stmt, 1, lines));
4708
+ lines.push("}");
4709
+ return lines.join("\n");
4710
+ }
4711
+ function printSequenceStatement(stmt, depth, lines) {
4712
+ const pad = INDENT.repeat(depth);
4713
+ switch (stmt.type) {
4714
+ case "SequenceActivate":
4715
+ lines.push(`${pad}activate ${stmt.participantId}`);
4716
+ break;
4717
+ case "SequenceDeactivate":
4718
+ lines.push(`${pad}deactivate ${stmt.participantId}`);
4719
+ break;
4720
+ case "SequenceCreate": {
4721
+ const n = stmt.node;
4722
+ const isSuffix = n.styleRefs.map((s) => ` is ${s}`).join("");
4723
+ printPropsBlock(`${pad}create ${n.id}: ${n.kind}${n.label ? ` "${n.label}"` : ""}${isSuffix}`, n.properties, depth, lines);
4724
+ break;
4725
+ }
4726
+ case "SequenceDestroy":
4727
+ lines.push(`${pad}destroy ${stmt.participantId}`);
4728
+ break;
4729
+ case "SequenceNote": {
4730
+ const ids = stmt.participantIds.join(", ");
4731
+ const place = stmt.placement === "over" ? `over ${ids}` : stmt.placement === "left" ? `left of ${ids}` : `right of ${ids}`;
4732
+ lines.push(`${pad}note ${place} "${stmt.text}"`);
4733
+ break;
4734
+ }
4735
+ case "SequenceDivider":
4736
+ lines.push(stmt.label ? `${pad}divider "${stmt.label}"` : `${pad}divider`);
4737
+ break;
4738
+ case "SequenceAutonumber":
4739
+ lines.push(`${pad}autonumber`);
4740
+ break;
4741
+ case "SequenceFragment": {
4742
+ const label = stmt.label ? ` "${stmt.label}"` : "";
4743
+ for (let i = 0; i < stmt.operands.length; i++) {
4744
+ const op = stmt.operands[i];
4745
+ const keyword = i === 0 ? stmt.operator : stmt.operator === "parallel" ? "and" : "else";
4746
+ const opLabel = i === 0 ? label : op.label ? ` "${op.label}"` : "";
4747
+ const styleSuffix = op.styleRefs.map((s) => ` is ${s}`).join("");
4748
+ lines.push(`${pad}${keyword}${opLabel}${styleSuffix} {`);
4749
+ printSeparated(op.statements, lines, (inner) => printSequenceStatement(inner, depth + 1, lines));
4750
+ lines.push(`${pad}}`);
4751
+ }
4752
+ break;
4753
+ }
4754
+ default:
4755
+ printStatement(stmt, depth, lines);
4756
+ break;
4757
+ }
4758
+ }
4759
+ function printStatement(stmt, depth, lines) {
4760
+ const pad = INDENT.repeat(depth);
4761
+ switch (stmt.type) {
4762
+ case "Directive":
4763
+ lines.push(`${pad}${printDirective(stmt)}`);
4764
+ break;
4765
+ case "Node": {
4766
+ const isSuffix = stmt.styleRefs.map((s) => ` is ${s}`).join("");
4767
+ printPropsBlock(`${pad}${stmt.id}: ${stmt.kind}${stmt.label ? ` "${stmt.label}"` : ""}${isSuffix}`, stmt.properties, depth, lines);
4768
+ break;
4769
+ }
4770
+ case "Edge": {
4771
+ const from = stmt.fromColumn ? `${stmt.from}.${stmt.fromColumn}` : stmt.from;
4772
+ const to = stmt.toColumn ? `${stmt.to}.${stmt.toColumn}` : stmt.to;
4773
+ const label = stmt.label ? ` "${stmt.label}"` : "";
4774
+ const isSuffix = stmt.styleRefs.map((s) => ` is ${s}`).join("");
4775
+ printPropsBlock(`${pad}${from} ${stmt.op} ${to}${label}`, stmt.properties, depth, lines, isSuffix);
4776
+ break;
4777
+ }
4778
+ case "Group":
4779
+ printGroup(stmt, depth, lines);
4780
+ break;
4781
+ case "Style": {
4782
+ const forTarget = stmt.target === "edge" ? " for edge" : stmt.target === "fragment" ? " for fragment" : "";
4783
+ printPropertyBlock(`${pad}style ${stmt.name}${forTarget}`, stmt.properties, depth, lines);
4784
+ break;
4785
+ }
4786
+ case "StyleRef":
4787
+ lines.push(`${pad}${stmt.targetIds.join(", ")} is ${stmt.styleName}`);
4788
+ break;
4789
+ case "GroupMember":
4790
+ lines.push(`${pad}${stmt.nodeIds.join(", ")}`);
4791
+ break;
4792
+ case "LayoutBlock":
4793
+ printPropertyBlock(`${pad}layout`, stmt.properties, depth, lines);
4794
+ break;
4795
+ case "EdgePolicyBlock":
4796
+ printPropertyBlock(`${pad}edges`, stmt.properties, depth, lines);
4797
+ break;
4798
+ case "RenderBlock":
4799
+ printPropertyBlock(`${pad}render`, stmt.properties, depth, lines);
4800
+ break;
4801
+ case "PresentationBlock":
4802
+ printPropertyBlock(`${pad}presentation`, stmt.properties, depth, lines);
4803
+ break;
4804
+ case "AnimationBlock":
4805
+ lines.push(`${pad}animation "${stmt.name}" {`);
4806
+ printSeparated(stmt.cues, lines, (cue) => {
4807
+ for (const line of formatAnimationCueLines(cue)) lines.push(line ? `${pad}${INDENT}${line}` : "");
4808
+ });
4809
+ lines.push(`${pad}}`);
4810
+ break;
4811
+ }
4812
+ }
4813
+ function printDirective(stmt) {
4814
+ const value = Array.isArray(stmt.value) ? stmt.value.join(", ") : stmt.value ?? "";
4815
+ return stmt.name === "direction" || stmt.name === "density" ? `${stmt.name} ${value}` : `${stmt.name}: ${value}`;
4816
+ }
4817
+ function formatAnimationCueLines(cue) {
4818
+ if (cue.type === "parallel") {
4819
+ const lines = ["parallel {"];
4820
+ let previous;
4821
+ for (const child of cue.cues) {
4822
+ if (previous && hasBlankLineBetween(previous, child)) lines.push("");
4823
+ for (const line of formatAnimationCueLines(child)) lines.push(line ? `${INDENT}${line}` : "");
4824
+ previous = child;
4825
+ }
4826
+ lines.push("}");
4827
+ return lines;
4828
+ }
4829
+ return [formatAnimationCue(cue)];
4830
+ }
4831
+ function formatAnimationCue(cue) {
4832
+ switch (cue.type) {
4833
+ case "loop": return "loop";
4834
+ case "wait": return `wait ${formatDuration(cue.durationMs)}`;
4835
+ case "dim":
4836
+ case "activate": return `${cue.type} ${cue.targets.map(formatAnimationTarget).join(", ")}`;
4837
+ case "pulse": {
4838
+ const dur = cue.durationMs != null ? ` for ${formatDuration(cue.durationMs)}` : "";
4839
+ return `pulse ${cue.targets.map(formatAnimationTarget).join(", ")}${dur}`;
4840
+ }
4841
+ case "flow": {
4842
+ const dur = cue.durationMs != null ? ` for ${formatDuration(cue.durationMs)}` : "";
4843
+ return `flow ${cue.path.join(" -> ")}${dur}`;
4844
+ }
4845
+ case "parallel": return "parallel { … }";
4846
+ }
4847
+ }
4848
+ function formatAnimationTarget(t) {
4849
+ if (t.type === "all") return "*";
4850
+ if (t.type === "node") return t.id;
4851
+ return `${t.from} -> ${t.to}`;
4852
+ }
4853
+ function formatDuration(ms) {
4854
+ if (ms >= 1e3 && ms % 100 === 0) return `${ms / 1e3}s`;
4855
+ return `${ms}ms`;
4856
+ }
4857
+ function printPropsBlock(head, properties, depth, lines, tail = "") {
4858
+ const pad = INDENT.repeat(depth);
4859
+ const entries = Object.entries(properties);
4860
+ if (entries.length === 0) {
4861
+ lines.push(`${head}${tail}`);
4862
+ return;
4863
+ }
4864
+ if (entries.length === 1 && entries[0][0] !== "columns") {
4865
+ const [key, value] = entries[0];
4866
+ lines.push(`${head} { ${key}: ${formatValue(value)} }${tail}`);
4867
+ return;
4868
+ }
4869
+ lines.push(`${head} {`);
4870
+ for (const [k, v] of entries) {
4871
+ if (k === "columns" && Array.isArray(v)) {
4872
+ lines.push(`${pad}${INDENT}columns {`);
4873
+ for (const raw of v) {
4874
+ const col = parseTableColumnSpec(String(raw));
4875
+ const line = col ? formatTableColumnLine(col) : String(raw);
4876
+ lines.push(`${pad}${INDENT}${INDENT}${line}`);
4877
+ }
4878
+ lines.push(`${pad}${INDENT}}`);
4879
+ continue;
4880
+ }
4881
+ lines.push(`${pad}${INDENT}${k}: ${formatValue(v)}`);
4882
+ }
4883
+ lines.push(`${pad}}${tail}`);
4884
+ }
4885
+ function printGroup(group, depth, lines) {
4886
+ const pad = INDENT.repeat(depth);
4887
+ const id = group.id ? `${group.id} ` : "";
4888
+ const label = group.label != null ? `"${group.label}" ` : "";
4889
+ lines.push(`${pad}${group.groupKind} ${id}${label}{`);
4890
+ printSeparated(group.statements, lines, (stmt) => printStatement(stmt, depth + 1, lines));
4891
+ lines.push(`${pad}}`);
4892
+ }
4893
+ function printPropertyBlock(head, properties, depth, lines) {
4894
+ const entries = Object.entries(properties);
4895
+ if (entries.length === 0) {
4896
+ lines.push(`${head} {}`);
4897
+ return;
4898
+ }
4899
+ if (entries.length === 1) {
4900
+ const [key, value] = entries[0];
4901
+ lines.push(`${head} { ${key}: ${formatValue(value)} }`);
4902
+ return;
4903
+ }
4904
+ const pad = INDENT.repeat(depth);
4905
+ lines.push(`${head} {`);
4906
+ for (const [key, value] of entries) lines.push(`${pad}${INDENT}${key}: ${formatValue(value)}`);
4907
+ lines.push(`${pad}}`);
4908
+ }
4909
+ function printSeparated(items, lines, print) {
4910
+ let previous;
4911
+ for (const item of items) {
4912
+ if (previous && hasBlankLineBetween(previous, item)) lines.push("");
4913
+ print(item);
4914
+ previous = item;
4915
+ }
4916
+ }
4917
+ function hasBlankLineBetween(previous, next) {
4918
+ return next.range.start.line > previous.range.end.line + 1;
4919
+ }
4920
+ function formatValue(v) {
4921
+ if (Array.isArray(v)) return `[${v.map((x) => `"${x}"`).join(", ")}]`;
4922
+ if (typeof v === "string") return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(v) ? v : JSON.stringify(v);
4923
+ return String(v);
4924
+ }
4925
+ //#endregion
4926
+ //#region src/types/graph.ts
4927
+ function mergeOptions(defaults, ...layers) {
4928
+ const result = { ...defaults };
4929
+ for (const layer of layers) {
4930
+ if (!layer) continue;
4931
+ for (const [k, v] of Object.entries(layer)) if (v !== void 0) result[k] = v;
4932
+ }
4933
+ return result;
4934
+ }
4935
+ //#endregion
4936
+ //#region src/types/geometry.ts
4937
+ function rectCenter(r) {
4938
+ return {
4939
+ x: r.x + r.width / 2,
4940
+ y: r.y + r.height / 2
4941
+ };
4942
+ }
4943
+ function rectRight(r) {
4944
+ return r.x + r.width;
4945
+ }
4946
+ function rectBottom(r) {
4947
+ return r.y + r.height;
4948
+ }
4949
+ function rectsOverlap(a, b, gap = 0) {
4950
+ return !(a.x + a.width + gap <= b.x || b.x + b.width + gap <= a.x || a.y + a.height + gap <= b.y || b.y + b.height + gap <= a.y);
4951
+ }
4952
+ function expandRect(r, pad) {
4953
+ return {
4954
+ x: r.x - pad,
4955
+ y: r.y - pad,
4956
+ width: r.width + pad * 2,
4957
+ height: r.height + pad * 2
4958
+ };
4959
+ }
4960
+ function manhattan(a, b) {
4961
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4962
+ }
4963
+ //#endregion
4964
+ export { BUILTIN_KIND_CATALOG, BUILTIN_KIND_LIST, BUILTIN_SHAPE_IDS, EDGE_OPS, REMOVED_PRESENTATION_PROPS, SEQUENCE_FRAGMENT_ALIASES, STATEMENT_KEYWORDS, animationIdFromName, bindSequenceFlowEdgeIds, cardinalityLabel, classifyBranch, compile, displayLabelCase, edgeOpsPattern, enumerateAutoPaths, expandRect, findColumnIndex, fkCardinality, flowHopEdgeId, formatLabelText, formatSource, formatTableColumnLine, getKindDefaults, inferAutoAnimation, isBuiltinKind, isGeometryKind, isKnownShapeId, isPureCardinalityLabel, kindHasCapability, kindSubtitle, listBuiltinShapeIds, listGeometryKinds, listKindsByCategory, manhattan, mergeOptions, mergePresentationOptions, normalizeBranch, normalizeSequenceFragmentOperator, normalizeShapeId, parse, parseCardinality, parseTableColumnSpec, parseTableColumns, planAutoWalk, presentationFromProperties, rectBottom, rectCenter, rectRight, rectsOverlap, resolvePresentation, sequenceFragmentDisplayName, traceDeclarationPath };
4965
+
4966
+ //# sourceMappingURL=index.mjs.map