@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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/index.d.mts +967 -0
- package/dist/index.mjs +4966 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
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
|