@slxu/graphsx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parser.js ADDED
@@ -0,0 +1,1463 @@
1
+ import { GraphDslError } from "./errors.js";
2
+ import {
3
+ ADDRESS_LITERAL,
4
+ EXPRESSION_LITERAL,
5
+ POINT_LITERAL,
6
+ REF_LITERAL,
7
+ TEMPLATE_LITERAL,
8
+ evaluateExpression,
9
+ isAddress,
10
+ isAddressLiteral,
11
+ isExpressionLiteral,
12
+ isPointLiteral,
13
+ isRefLiteral,
14
+ isTemplateLiteral,
15
+ pointLiteral,
16
+ pointExpressionNumber,
17
+ substitutePointExpression,
18
+ templateLiteral
19
+ } from "./literals.js";
20
+ import { parseMarkup } from "./markup.js";
21
+ import { buildPlotModel } from "./plot.js";
22
+
23
+ const BUILTIN_SHAPE_TAGS = new Map([
24
+ ["Rect", "rect"],
25
+ ["rect", "rect"],
26
+ ["Rec", "rect"],
27
+ ["rec", "rect"],
28
+ ["Circle", "circle"],
29
+ ["circle", "circle"],
30
+ ["Circ", "circle"],
31
+ ["circ", "circle"],
32
+ ["Point", "point"],
33
+ ["point", "point"],
34
+ ["Anchor", "point"],
35
+ ["anchor", "point"]
36
+ ]);
37
+ const PLOT_TAGS = new Set(["Plot"]);
38
+ const EDGE_TAGS = new Set(["Link"]);
39
+ const PATH_TAGS = new Set(["Path", "path"]);
40
+ const PORT_TAGS = new Set(["Port", "Leg"]);
41
+ const STYLE_TAGS = new Set(["Style"]);
42
+ const REPEAT_TAGS = new Set(["Repeat"]);
43
+ const SIDE_ATTRS = ["left", "right", "top", "bottom"];
44
+ const SIDE_ANGLES = {
45
+ left: 180,
46
+ right: 0,
47
+ top: -90,
48
+ bottom: 90
49
+ };
50
+ export { GraphDslError, parseMarkup };
51
+
52
+ export function parseGraphs(source) {
53
+ const roots = parseMarkup(source).filter((node) => node.type === "element");
54
+ const graphs = roots.filter((node) => node.name === "Graph");
55
+
56
+ if (graphs.length !== roots.length) {
57
+ throw new GraphDslError("Top-level elements must be <Graph>");
58
+ }
59
+
60
+ return graphs.map(buildGraphModel);
61
+ }
62
+
63
+ export function parseGraph(source) {
64
+ const graphs = parseGraphs(source);
65
+
66
+ if (graphs.length !== 1) {
67
+ throw new GraphDslError(`Expected exactly one <Graph>, found ${graphs.length}`);
68
+ }
69
+
70
+ return graphs[0];
71
+ }
72
+
73
+ export function buildGraphModel(graphElement) {
74
+ assertElement(graphElement, "Graph");
75
+ graphElement = expandRepeats(graphElement);
76
+
77
+ const shapeElements = graphElement.children.filter(isElementNamed("Shape"));
78
+ assertUniqueShapeIds(shapeElements);
79
+ const shapes = resolveShapeDefinitions(shapeElements);
80
+ const styles = buildStyles(graphElement.children.filter(isStyleElement));
81
+ assertKnownChildren(graphElement, shapes, { allowStyle: true });
82
+ for (const shape of shapes.values()) {
83
+ assertKnownChildren(shape, shapes);
84
+ validateShapeDefinitionIds(shape, shapes);
85
+ }
86
+
87
+ const nodes = graphElement.children.filter((node) => isNodeElement(node, shapes)).map((node) => {
88
+ return buildNode(node, shapes, styles, { x: 0, y: 0, namespace: "" });
89
+ });
90
+
91
+ const edges = graphElement.children.filter(isEdgeElement).map((edge) => buildEdge(edge, styles));
92
+ const paths = graphElement.children.filter(isPathElement).map((path) => buildPath(path, styles, { x: 0, y: 0 }));
93
+ const graph = {
94
+ type: "graph",
95
+ attrs: { ...graphElement.attrs },
96
+ styles: Object.fromEntries(styles),
97
+ shapes: Object.fromEntries([...shapes].map(([id, shape]) => [id, describeShape(shape)])),
98
+ nodes,
99
+ edges,
100
+ paths
101
+ };
102
+
103
+ validateUniqueIds(graph);
104
+ applyLayout(graph);
105
+ resolveGraphAddresses(graph, { assertEdges: false });
106
+ applyTransforms(graph);
107
+ applyPlacements(graph);
108
+ resolveGraphAddresses(graph);
109
+ return graph;
110
+ }
111
+
112
+ function buildNode(nodeElement, shapes, styles, context) {
113
+ const normalized = normalizeNodeElement(nodeElement, shapes, styles);
114
+ const id = requiredAttr(nodeElement, "id");
115
+ const positioned = hasExplicitPosition(nodeElement.attrs);
116
+ const x = coordinateAttr(normalized.attrs, "x", 0) + context.x;
117
+ const y = coordinateAttr(normalized.attrs, "y", 0) + context.y;
118
+ const base = {
119
+ id: context.namespace ? `${context.namespace}.${id}` : id,
120
+ localId: id,
121
+ shape: normalized.shape,
122
+ x,
123
+ y,
124
+ positioned,
125
+ attrs: normalized.attrs,
126
+ legs: {},
127
+ children: [],
128
+ edges: [],
129
+ paths: [],
130
+ plot: normalized.plot ?? null
131
+ };
132
+
133
+ if (shapes.has(normalized.shape)) {
134
+ return buildGroupedNode(base, shapes.get(normalized.shape), shapes, styles);
135
+ }
136
+
137
+ assertUniqueChildIds(nodeElement.children.filter(isPortElement), "port", ` on "${base.id}"`);
138
+ for (const legElement of nodeElement.children.filter(isPortElement)) {
139
+ const leg = buildLeg(legElement, base, styles);
140
+ base.legs[leg.id] = leg;
141
+ }
142
+ addDefaultPorts(base);
143
+
144
+ return base;
145
+ }
146
+
147
+ function buildGroupedNode(instance, shapeElement, shapes, styles) {
148
+ shapeElement = substituteShapeProps(shapeElement, instance.attrs);
149
+ assertUniqueChildIds(shapeElement.children.filter(isPortElement), "port", ` on "${instance.id}"`);
150
+
151
+ const childContext = {
152
+ x: instance.x,
153
+ y: instance.y,
154
+ namespace: instance.id
155
+ };
156
+
157
+ instance.children = shapeElement.children
158
+ .filter((child) => isNodeElement(child, shapes))
159
+ .map((child) => buildNode(child, shapes, styles, childContext));
160
+
161
+ instance.edges = shapeElement.children
162
+ .filter(isEdgeElement)
163
+ .map((edge) => prefixGroupedEdge(buildEdge(edge, styles), instance.id));
164
+
165
+ instance.paths = shapeElement.children
166
+ .filter(isPathElement)
167
+ .map((path) => buildPath(path, styles, childContext));
168
+
169
+ for (const legElement of shapeElement.children.filter(isPortElement)) {
170
+ const leg = buildLeg(legElement, instance, styles);
171
+ if (legElement.attrs.target) {
172
+ leg.target = `${instance.id}.${legElement.attrs.target}`;
173
+ }
174
+ instance.legs[leg.id] = leg;
175
+ }
176
+
177
+ return instance;
178
+ }
179
+
180
+ function buildLeg(legElement, node, styles) {
181
+ const id = requiredAttr(legElement, "id");
182
+ const side = resolveSide(legElement.attrs);
183
+ const [explicitX, explicitY] = resolvePortCoordinates(legElement.attrs);
184
+ const relative = resolveLegPosition(node, side, explicitX, explicitY);
185
+
186
+ return {
187
+ id,
188
+ side,
189
+ angle: resolvePortAngle(legElement.attrs, side),
190
+ x: node.x + relative.x,
191
+ y: node.y + relative.y,
192
+ relative,
193
+ attrs: resolveStyledAttrs(legElement.attrs, styles)
194
+ };
195
+ }
196
+
197
+ function addDefaultPorts(node) {
198
+ if (node.shape === "point") {
199
+ if (!node.legs.center) {
200
+ node.legs.center = {
201
+ id: "center",
202
+ side: null,
203
+ angle: 0,
204
+ x: node.x,
205
+ y: node.y,
206
+ relative: { x: 0, y: 0 },
207
+ auto: true,
208
+ attrs: { id: "center" }
209
+ };
210
+ }
211
+ return;
212
+ }
213
+
214
+ if (node.shape !== "rect" && node.shape !== "circle" && node.shape !== "plot") return;
215
+
216
+ for (const side of SIDE_ATTRS) {
217
+ if (node.legs[side]) continue;
218
+ const relative = resolveLegPosition(node, side, null, null);
219
+ node.legs[side] = {
220
+ id: side,
221
+ side,
222
+ angle: SIDE_ANGLES[side],
223
+ x: node.x + relative.x,
224
+ y: node.y + relative.y,
225
+ relative,
226
+ auto: true,
227
+ attrs: { id: side, [side]: true }
228
+ };
229
+ }
230
+ }
231
+
232
+ function resolveLegPosition(node, side, explicitX, explicitY) {
233
+ if (explicitX != null || explicitY != null) {
234
+ return { x: explicitX ?? 0, y: explicitY ?? 0 };
235
+ }
236
+
237
+ if (node.shape === "point") {
238
+ return { x: 0, y: 0 };
239
+ }
240
+
241
+ if (node.shape === "circle") {
242
+ const r = numberAttr({ attrs: node.attrs }, "r", 0);
243
+ const circle = {
244
+ left: { x: -r, y: 0 },
245
+ right: { x: r, y: 0 },
246
+ top: { x: 0, y: -r },
247
+ bottom: { x: 0, y: r }
248
+ };
249
+ return circle[side] ?? { x: 0, y: 0 };
250
+ }
251
+
252
+ const w = numberAttr({ attrs: node.attrs }, "w", 0);
253
+ const h = numberAttr({ attrs: node.attrs }, "h", 0);
254
+ const rect = {
255
+ left: { x: 0, y: h / 2 },
256
+ right: { x: w, y: h / 2 },
257
+ top: { x: w / 2, y: 0 },
258
+ bottom: { x: w / 2, y: h }
259
+ };
260
+
261
+ return rect[side] ?? { x: w / 2, y: h / 2 };
262
+ }
263
+
264
+ function buildEdge(edgeElement, styles) {
265
+ return {
266
+ from: endpointAttr(edgeElement, "from"),
267
+ to: endpointAttr(edgeElement, "to"),
268
+ attrs: resolveStyledAttrs(edgeElement.attrs, styles)
269
+ };
270
+ }
271
+
272
+ function buildPath(pathElement, styles, context) {
273
+ const attrs = resolveStyledAttrs(pathElement.attrs, styles);
274
+ return {
275
+ id: attrs.id ?? null,
276
+ x: attrs.points == null ? context.x : 0,
277
+ y: attrs.points == null ? context.y : 0,
278
+ points: normalizePathPoints(attrs.points, context),
279
+ attrs
280
+ };
281
+ }
282
+
283
+ function normalizePathPoints(points, context) {
284
+ if (points == null) return null;
285
+ if (!Array.isArray(points)) {
286
+ throw new GraphDslError("\"points\" must be an array of [x, y] pairs");
287
+ }
288
+ return points.map((point) => {
289
+ if (!Array.isArray(point) || point.length < 2) {
290
+ throw new GraphDslError("\"points\" must be an array of [x, y] pairs");
291
+ }
292
+ const x = Number(point[0]);
293
+ const y = Number(point[1]);
294
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
295
+ throw new GraphDslError("Path points must be numbers");
296
+ }
297
+ return { x: x + context.x, y: y + context.y };
298
+ });
299
+ }
300
+
301
+ function prefixGroupedEdge(edge, namespace) {
302
+ return {
303
+ ...edge,
304
+ from: `${namespace}.${edge.from}`,
305
+ to: `${namespace}.${edge.to}`
306
+ };
307
+ }
308
+
309
+ function applyLayout(graph) {
310
+ const layout = graph.attrs.layout;
311
+ if (!layout || layout === "manual") return;
312
+
313
+ if (layout === "row" || layout === "column") {
314
+ applyFlowLayout(graph, layout);
315
+ return;
316
+ }
317
+
318
+ if (layout === "dag" || layout === "auto") {
319
+ applyDagLayout(graph);
320
+ }
321
+ }
322
+
323
+ function applyFlowLayout(graph, layout) {
324
+ const gap = numberFromAttrs(graph.attrs, "gap", 120);
325
+ const originX = numberFromAttrs(graph.attrs, "x", 100);
326
+ const originY = numberFromAttrs(graph.attrs, "y", 100);
327
+ let cursor = 0;
328
+
329
+ for (const node of graph.nodes) {
330
+ if (!node.positioned) {
331
+ moveNodeTo(node, layout === "row" ? originX + cursor : originX, layout === "column" ? originY + cursor : originY);
332
+ }
333
+ const size = nodeSize(node);
334
+ cursor += (layout === "row" ? size.w : size.h) + gap;
335
+ }
336
+ }
337
+
338
+ function applyDagLayout(graph) {
339
+ const direction = graph.attrs.direction ?? "right";
340
+ const rankGap = numberFromAttrs(graph.attrs, "rankGap", numberFromAttrs(graph.attrs, "gap", 180));
341
+ const nodeGap = numberFromAttrs(graph.attrs, "nodeGap", 90);
342
+ const originX = numberFromAttrs(graph.attrs, "x", 100);
343
+ const originY = numberFromAttrs(graph.attrs, "y", 100);
344
+ const order = new Map(graph.nodes.map((node, index) => [node.id, index]));
345
+ const ids = new Set(graph.nodes.map((node) => node.id));
346
+ const outgoing = new Map([...ids].map((id) => [id, []]));
347
+ const indegree = new Map([...ids].map((id) => [id, 0]));
348
+
349
+ for (const edge of graph.edges) {
350
+ const from = rootAddress(edge.from);
351
+ const to = rootAddress(edge.to);
352
+ if (!ids.has(from) || !ids.has(to) || from === to) continue;
353
+ outgoing.get(from).push(to);
354
+ indegree.set(to, indegree.get(to) + 1);
355
+ }
356
+
357
+ const queue = [...ids]
358
+ .filter((id) => indegree.get(id) === 0)
359
+ .sort((a, b) => order.get(a) - order.get(b));
360
+ const layer = new Map([...ids].map((id) => [id, 0]));
361
+
362
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
363
+ const id = queue[cursor];
364
+ for (const next of outgoing.get(id)) {
365
+ layer.set(next, Math.max(layer.get(next), layer.get(id) + 1));
366
+ indegree.set(next, indegree.get(next) - 1);
367
+ if (indegree.get(next) === 0) {
368
+ queue.push(next);
369
+ }
370
+ }
371
+ }
372
+
373
+ const layers = new Map();
374
+ for (const node of graph.nodes) {
375
+ const rank = layer.get(node.id) ?? 0;
376
+ if (!layers.has(rank)) layers.set(rank, []);
377
+ layers.get(rank).push(node);
378
+ }
379
+
380
+ for (const [rank, nodes] of layers) {
381
+ nodes.sort((a, b) => order.get(a.id) - order.get(b.id));
382
+ nodes.forEach((node, index) => {
383
+ if (node.positioned) return;
384
+ const point = orientedPoint(direction, originX, originY, rank * rankGap, index * nodeGap);
385
+ moveNodeTo(node, point.x, point.y);
386
+ });
387
+ }
388
+ }
389
+
390
+ function orientedPoint(direction, originX, originY, main, cross) {
391
+ if (direction === "left") return { x: originX - main, y: originY + cross };
392
+ if (direction === "down") return { x: originX + cross, y: originY + main };
393
+ if (direction === "up") return { x: originX + cross, y: originY - main };
394
+ return { x: originX + main, y: originY + cross };
395
+ }
396
+
397
+ function moveNodeTo(node, x, y) {
398
+ moveNodeBy(node, x - node.x, y - node.y);
399
+ }
400
+
401
+ function moveNodeBy(node, dx, dy) {
402
+ if (node.transform) {
403
+ node.transform.e += dx;
404
+ node.transform.f += dy;
405
+ } else {
406
+ node.x += dx;
407
+ node.y += dy;
408
+ }
409
+ for (const leg of Object.values(node.legs)) {
410
+ leg.x += dx;
411
+ leg.y += dy;
412
+ }
413
+ for (const child of node.children) {
414
+ moveNodeBy(child, dx, dy);
415
+ }
416
+ for (const path of node.paths ?? []) {
417
+ movePathBy(path, dx, dy);
418
+ }
419
+ }
420
+
421
+ function movePathBy(path, dx, dy) {
422
+ if (path.transform) {
423
+ path.transform.e += dx;
424
+ path.transform.f += dy;
425
+ return;
426
+ }
427
+ if (Array.isArray(path.points)) {
428
+ for (const point of path.points) {
429
+ point.x += dx;
430
+ point.y += dy;
431
+ }
432
+ return;
433
+ }
434
+ path.x = (path.x ?? 0) + dx;
435
+ path.y = (path.y ?? 0) + dy;
436
+ }
437
+
438
+ function applyTransforms(graph) {
439
+ for (const node of graph.nodes) {
440
+ applyNodeTransforms(node);
441
+ }
442
+ for (const path of graph.paths ?? []) {
443
+ applyPathOwnTransform(path);
444
+ }
445
+ }
446
+
447
+ function applyNodeTransforms(node) {
448
+ for (const child of node.children) {
449
+ applyNodeTransforms(child);
450
+ }
451
+ for (const path of node.paths ?? []) {
452
+ applyPathOwnTransform(path);
453
+ }
454
+
455
+ const matrix = nodeTransformMatrix(node);
456
+ if (!matrix) return;
457
+
458
+ if (node.children.length > 0 || (node.paths?.length ?? 0) > 0) {
459
+ transformNodeContents(node, matrix);
460
+ } else {
461
+ node.transform = composeMatrix(matrix, node.transform);
462
+ transformNodeLegs(node, matrix);
463
+ }
464
+ }
465
+
466
+ function transformNodeContents(node, matrix) {
467
+ for (const child of node.children) {
468
+ transformNodeTree(child, matrix);
469
+ }
470
+ for (const path of node.paths ?? []) {
471
+ transformPath(path, matrix);
472
+ }
473
+ transformNodeLegs(node, matrix);
474
+ }
475
+
476
+ function transformNodeTree(node, matrix) {
477
+ node.transform = composeMatrix(matrix, node.transform);
478
+ transformNodeLegs(node, matrix);
479
+ for (const path of node.paths ?? []) {
480
+ transformPath(path, matrix);
481
+ }
482
+ for (const child of node.children) {
483
+ transformNodeTree(child, matrix);
484
+ }
485
+ }
486
+
487
+ function transformNodeLegs(node, matrix) {
488
+ for (const leg of Object.values(node.legs)) {
489
+ const point = transformPoint(matrix, leg);
490
+ leg.x = point.x;
491
+ leg.y = point.y;
492
+ leg.angle = transformAngle(matrix, leg.angle ?? 0);
493
+ leg.relative = {
494
+ x: leg.x - node.x,
495
+ y: leg.y - node.y
496
+ };
497
+ }
498
+ }
499
+
500
+ function applyPathOwnTransform(path) {
501
+ const matrix = attrsTransformMatrix(path.attrs, pathOrigin(path));
502
+ if (matrix) {
503
+ transformPath(path, matrix);
504
+ }
505
+ }
506
+
507
+ function transformPath(path, matrix) {
508
+ if (Array.isArray(path.points)) {
509
+ path.points = path.points.map((point) => transformPoint(matrix, point));
510
+ return;
511
+ }
512
+ path.transform = composeMatrix(matrix, path.transform);
513
+ }
514
+
515
+ function nodeTransformMatrix(node) {
516
+ return attrsTransformMatrix(node.attrs, nodeTransformOrigin(node));
517
+ }
518
+
519
+ function attrsTransformMatrix(attrs, origin) {
520
+ const rotate = attrs.rotate ?? attrs.rotation;
521
+ const hasRotate = rotate != null && Number(rotate) !== 0;
522
+ const hasFlipX = booleanAttr(attrs.flipX ?? attrs.flipx, false);
523
+ const hasFlipY = booleanAttr(attrs.flipY ?? attrs.flipy, false);
524
+ if (!hasRotate && !hasFlipX && !hasFlipY) return null;
525
+
526
+ const angle = rotate == null ? 0 : Number(rotate);
527
+ if (!Number.isFinite(angle)) {
528
+ throw new GraphDslError("\"rotate\" must be a number");
529
+ }
530
+ return transformMatrix(origin.x, origin.y, angle, hasFlipX, hasFlipY);
531
+ }
532
+
533
+ function nodeTransformOrigin(node) {
534
+ if (Array.isArray(node.attrs.origin)) {
535
+ return {
536
+ x: node.x + (optionalNumber(node.attrs.origin[0]) ?? 0),
537
+ y: node.y + (optionalNumber(node.attrs.origin[1]) ?? 0)
538
+ };
539
+ }
540
+
541
+ const anchor = transformAnchor(node);
542
+ if (anchor) {
543
+ return { x: anchor.x, y: anchor.y };
544
+ }
545
+
546
+ if (node.children.length > 0 || (node.paths?.length ?? 0) > 0) {
547
+ return { x: node.x, y: node.y };
548
+ }
549
+
550
+ const box = untransformedNodeBox(node);
551
+ return { x: box.cx, y: box.cy };
552
+ }
553
+
554
+ function transformAnchor(node) {
555
+ if (node.attrs.anchor == null) return null;
556
+ return findPortInNode(node, String(node.attrs.anchor));
557
+ }
558
+
559
+ function pathOrigin(path) {
560
+ if (Array.isArray(path.attrs.origin)) {
561
+ return {
562
+ x: (path.x ?? 0) + (optionalNumber(path.attrs.origin[0]) ?? 0),
563
+ y: (path.y ?? 0) + (optionalNumber(path.attrs.origin[1]) ?? 0)
564
+ };
565
+ }
566
+ return { x: path.x ?? 0, y: path.y ?? 0 };
567
+ }
568
+
569
+ function transformMatrix(ox, oy, rotate, flipX, flipY) {
570
+ const radians = rotate * Math.PI / 180;
571
+ const cos = Math.cos(radians);
572
+ const sin = Math.sin(radians);
573
+ const sx = flipX ? -1 : 1;
574
+ const sy = flipY ? -1 : 1;
575
+ const a = cos * sx;
576
+ const b = sin * sx;
577
+ const c = -sin * sy;
578
+ const d = cos * sy;
579
+ return {
580
+ a,
581
+ b,
582
+ c,
583
+ d,
584
+ e: ox - a * ox - c * oy,
585
+ f: oy - b * ox - d * oy
586
+ };
587
+ }
588
+
589
+ function composeMatrix(outer, inner) {
590
+ if (!inner) return { ...outer };
591
+ return {
592
+ a: outer.a * inner.a + outer.c * inner.b,
593
+ b: outer.b * inner.a + outer.d * inner.b,
594
+ c: outer.a * inner.c + outer.c * inner.d,
595
+ d: outer.b * inner.c + outer.d * inner.d,
596
+ e: outer.a * inner.e + outer.c * inner.f + outer.e,
597
+ f: outer.b * inner.e + outer.d * inner.f + outer.f
598
+ };
599
+ }
600
+
601
+ function transformPoint(matrix, point) {
602
+ return {
603
+ x: matrix.a * point.x + matrix.c * point.y + matrix.e,
604
+ y: matrix.b * point.x + matrix.d * point.y + matrix.f
605
+ };
606
+ }
607
+
608
+ function transformAngle(matrix, angle) {
609
+ const radians = Number(angle) * Math.PI / 180;
610
+ const x = Math.cos(radians);
611
+ const y = Math.sin(radians);
612
+ const tx = matrix.a * x + matrix.c * y;
613
+ const ty = matrix.b * x + matrix.d * y;
614
+ return normalizeAngle(Math.atan2(ty, tx) * 180 / Math.PI);
615
+ }
616
+
617
+ function normalizeAngle(angle) {
618
+ const normalized = ((angle % 360) + 360) % 360;
619
+ return Math.abs(normalized - 360) < 1e-9 ? 0 : normalized;
620
+ }
621
+
622
+ function untransformedNodeBox(node) {
623
+ if (node.shape === "point") {
624
+ return {
625
+ minX: node.x,
626
+ minY: node.y,
627
+ maxX: node.x,
628
+ maxY: node.y,
629
+ cx: node.x,
630
+ cy: node.y
631
+ };
632
+ }
633
+
634
+ if (node.shape === "circle") {
635
+ const r = numberFromAttrs(node.attrs, "r", 28);
636
+ return {
637
+ minX: node.x - r,
638
+ minY: node.y - r,
639
+ maxX: node.x + r,
640
+ maxY: node.y + r,
641
+ cx: node.x,
642
+ cy: node.y
643
+ };
644
+ }
645
+
646
+ const w = numberFromAttrs(node.attrs, "w", 100);
647
+ const h = numberFromAttrs(node.attrs, "h", 60);
648
+ return {
649
+ minX: node.x,
650
+ minY: node.y,
651
+ maxX: node.x + w,
652
+ maxY: node.y + h,
653
+ cx: node.x + w / 2,
654
+ cy: node.y + h / 2
655
+ };
656
+ }
657
+
658
+ function nodeSize(node) {
659
+ if (node.shape === "point") {
660
+ return { w: 0, h: 0 };
661
+ }
662
+
663
+ if (node.shape === "circle") {
664
+ const r = numberFromAttrs(node.attrs, "r", 28);
665
+ return { w: r * 2, h: r * 2 };
666
+ }
667
+ return {
668
+ w: numberFromAttrs(node.attrs, "w", 100),
669
+ h: numberFromAttrs(node.attrs, "h", 60)
670
+ };
671
+ }
672
+
673
+ function rootAddress(address) {
674
+ return String(address).split(".")[0];
675
+ }
676
+
677
+ function nodeAddress(address) {
678
+ return String(address).split(".").slice(0, -1).join(".");
679
+ }
680
+
681
+ function resolveGraphAddresses(graph, options = {}) {
682
+ const ports = indexPorts(graph.nodes);
683
+
684
+ for (const node of flattenNodes(graph.nodes)) {
685
+ for (const leg of Object.values(node.legs)) {
686
+ if (!leg.target) continue;
687
+ const target = ports.get(leg.target);
688
+ if (!target) {
689
+ throw new GraphDslError(`Unknown port address "${leg.target}"`);
690
+ }
691
+ leg.x = target.x;
692
+ leg.y = target.y;
693
+ leg.side = leg.side ?? target.side;
694
+ if (leg.attrs.angle == null && leg.attrs.side == null && !SIDE_ATTRS.some((side) => leg.attrs[side] === true)) {
695
+ leg.angle = target.angle;
696
+ }
697
+ leg.relative = {
698
+ x: target.x - node.x,
699
+ y: target.y - node.y
700
+ };
701
+ leg.attrs = inheritTargetPortAttrs(target.attrs, leg.attrs, leg.id);
702
+ }
703
+ }
704
+
705
+ if (options.assertEdges !== false) {
706
+ for (const edge of allEdges(graph)) {
707
+ assertPortAddress(edge.from, ports);
708
+ assertPortAddress(edge.to, ports);
709
+ }
710
+ }
711
+ }
712
+
713
+ function applyPlacements(graph) {
714
+ const pending = new Set(flattenNodes(graph.nodes).filter(hasPlacementRef));
715
+
716
+ while (pending.size > 0) {
717
+ const ports = indexPorts(graph.nodes);
718
+ let progressed = false;
719
+
720
+ for (const node of [...pending]) {
721
+ const targetAddress = placementDependency(node.attrs.at);
722
+ const dependency = targetAddress ? nodeAddress(targetAddress) : null;
723
+ if (dependency && isPendingDependency(dependency, pending, node)) {
724
+ continue;
725
+ }
726
+
727
+ placeNodeAtReference(node, ports);
728
+ pending.delete(node);
729
+ progressed = true;
730
+ }
731
+
732
+ if (!progressed) {
733
+ const ids = [...pending].map((node) => node.id).join(", ");
734
+ throw new GraphDslError(`Cyclic or unresolved placement reference involving ${ids}`);
735
+ }
736
+ }
737
+ }
738
+
739
+ function hasPlacementRef(node) {
740
+ return placementAddress(node.attrs.at) != null;
741
+ }
742
+
743
+ function isPendingDependency(dependency, pending, node) {
744
+ if (dependency === node.id || node.id.startsWith(`${dependency}.`)) {
745
+ return false;
746
+ }
747
+ for (const pendingNode of pending) {
748
+ if (pendingNode === node) continue;
749
+ if (pendingNode.id === dependency || dependency.startsWith(`${pendingNode.id}.`)) {
750
+ return true;
751
+ }
752
+ }
753
+ return false;
754
+ }
755
+
756
+ function placeNodeAtReference(node, ports) {
757
+ const target = placementPoint(node.attrs.at, ports);
758
+ if (!target) return;
759
+
760
+ const anchor = placementAnchor(node, ports);
761
+ moveNodeBy(node, target.x - anchor.x, target.y - anchor.y);
762
+ }
763
+
764
+ function placementDependency(value) {
765
+ const expression = placementExpression(value);
766
+ return expression?.address ?? null;
767
+ }
768
+
769
+ function placementPoint(value, ports) {
770
+ const expression = placementExpression(value);
771
+ if (!expression) return null;
772
+
773
+ const port = ports.get(expression.address);
774
+ if (!port) {
775
+ throw new GraphDslError(`Unknown placement port "${expression.address}"`);
776
+ }
777
+
778
+ return expression.offsets.reduce((point, offset) => {
779
+ const x = pointExpressionNumber(offset.x, expression);
780
+ const y = pointExpressionNumber(offset.y, expression);
781
+ return {
782
+ x: point.x + x,
783
+ y: point.y + y
784
+ };
785
+ }, { x: port.x, y: port.y });
786
+ }
787
+
788
+ function placementExpression(value) {
789
+ if (isPointLiteral(value)) return value[POINT_LITERAL];
790
+ if (isAddressLiteral(value)) return { address: value[ADDRESS_LITERAL], offsets: [] };
791
+ if (typeof value === "string" && isAddress(value)) return { address: value, offsets: [] };
792
+ return null;
793
+ }
794
+
795
+ function placementAnchor(node, ports) {
796
+ if (node.attrs.anchor == null) {
797
+ return { x: node.x, y: node.y };
798
+ }
799
+
800
+ const anchor = String(node.attrs.anchor);
801
+ const port = findPortInNode(node, anchor, ports);
802
+ if (!port) {
803
+ throw new GraphDslError(`Unknown anchor port "${anchor}" on "${node.id}"`);
804
+ }
805
+ return port;
806
+ }
807
+
808
+ function findPortInNode(node, anchor, ports = null) {
809
+ if (!anchor.includes(".") && node.legs[anchor]) {
810
+ return node.legs[anchor];
811
+ }
812
+
813
+ const address = `${node.id}.${anchor}`;
814
+ if (ports) {
815
+ return ports.get(address) ?? null;
816
+ }
817
+
818
+ for (const candidate of flattenNodes([node])) {
819
+ for (const [id, leg] of Object.entries(candidate.legs)) {
820
+ if (`${candidate.id}.${id}` === address) {
821
+ return leg;
822
+ }
823
+ }
824
+ }
825
+ return null;
826
+ }
827
+
828
+ function placementAddress(value) {
829
+ return placementDependency(value);
830
+ }
831
+
832
+ function indexPorts(nodes) {
833
+ const ports = new Map();
834
+ for (const node of flattenNodes(nodes)) {
835
+ for (const [id, leg] of Object.entries(node.legs)) {
836
+ ports.set(`${node.id}.${id}`, leg);
837
+ }
838
+ }
839
+ return ports;
840
+ }
841
+
842
+ function inheritTargetPortAttrs(targetAttrs, publicAttrs, publicId) {
843
+ const attrs = {
844
+ ...targetAttrs,
845
+ ...publicAttrs,
846
+ id: publicId
847
+ };
848
+ if (targetAttrs.style || publicAttrs.style) {
849
+ attrs.style = {
850
+ ...(targetAttrs.style ?? {}),
851
+ ...(publicAttrs.style ?? {})
852
+ };
853
+ }
854
+ return attrs;
855
+ }
856
+
857
+ function flattenNodes(nodes) {
858
+ return nodes.flatMap((node) => [node, ...flattenNodes(node.children)]);
859
+ }
860
+
861
+ function allEdges(graph) {
862
+ return [
863
+ ...graph.edges,
864
+ ...graph.nodes.flatMap((node) => nodeEdges(node))
865
+ ];
866
+ }
867
+
868
+ function nodeEdges(node) {
869
+ return [
870
+ ...node.edges,
871
+ ...node.children.flatMap((child) => nodeEdges(child))
872
+ ];
873
+ }
874
+
875
+ function assertPortAddress(address, ports) {
876
+ if (!ports.has(address)) {
877
+ throw new GraphDslError(`Unknown port address "${address}"`);
878
+ }
879
+ }
880
+
881
+ function assertUniqueShapeIds(shapeElements) {
882
+ const seen = new Set();
883
+ for (const shape of shapeElements) {
884
+ const id = requiredAttr(shape, "id");
885
+ if (seen.has(id)) {
886
+ throw new GraphDslError(`Duplicate shape id "${id}"`);
887
+ }
888
+ seen.add(id);
889
+ }
890
+ }
891
+
892
+ function resolveShapeDefinitions(shapeElements) {
893
+ const rawShapes = new Map(shapeElements.map((shape) => [requiredAttr(shape, "id"), shape]));
894
+ const resolved = new Map();
895
+ const resolving = new Set();
896
+
897
+ const resolve = (id) => {
898
+ if (resolved.has(id)) return resolved.get(id);
899
+ const shape = rawShapes.get(id);
900
+ if (!shape) {
901
+ throw new GraphDslError(`Unknown parent shape "${id}"`);
902
+ }
903
+ if (resolving.has(id)) {
904
+ throw new GraphDslError(`Cyclic shape inheritance involving "${id}"`);
905
+ }
906
+
907
+ resolving.add(id);
908
+ const parentId = shape.attrs.from;
909
+ let result = shape;
910
+ if (parentId != null) {
911
+ if (!rawShapes.has(parentId)) {
912
+ throw new GraphDslError(`Unknown parent shape "${parentId}"`);
913
+ }
914
+ const parent = resolve(parentId);
915
+ result = mergeShapeDefinition(parent, shape);
916
+ }
917
+ resolving.delete(id);
918
+ resolved.set(id, result);
919
+ return result;
920
+ };
921
+
922
+ for (const id of rawShapes.keys()) {
923
+ resolve(id);
924
+ }
925
+ return resolved;
926
+ }
927
+
928
+ function mergeShapeDefinition(parent, child) {
929
+ return {
930
+ ...child,
931
+ attrs: {
932
+ ...parent.attrs,
933
+ ...child.attrs,
934
+ id: child.attrs.id
935
+ },
936
+ children: [
937
+ ...parent.children,
938
+ ...child.children
939
+ ]
940
+ };
941
+ }
942
+
943
+ function validateShapeDefinitionIds(shape, shapes) {
944
+ const id = requiredAttr(shape, "id");
945
+ assertUniqueChildIds(shape.children.filter((child) => isNodeElement(child, shapes)), "child node", ` in shape "${id}"`);
946
+ assertUniqueChildIds(shape.children.filter(isPortElement), "port", ` in shape "${id}"`);
947
+ assertUniqueChildIds(shape.children.filter((child) => isPathElement(child) && child.attrs.id != null), "path", ` in shape "${id}"`);
948
+ }
949
+
950
+ function assertUniqueChildIds(elements, label, suffix = "") {
951
+ const seen = new Set();
952
+ for (const element of elements) {
953
+ const id = requiredAttr(element, "id");
954
+ if (seen.has(id)) {
955
+ throw new GraphDslError(`Duplicate ${label} id "${id}"${suffix}`);
956
+ }
957
+ seen.add(id);
958
+ }
959
+ }
960
+
961
+ function validateUniqueIds(graph) {
962
+ assertUniqueNodeIds(graph.nodes);
963
+ for (const node of flattenNodes(graph.nodes)) {
964
+ assertUniquePortIds(node);
965
+ }
966
+ assertUniquePathIds(flattenModelPaths(graph));
967
+ }
968
+
969
+ function assertUniqueNodeIds(nodes) {
970
+ const seen = new Set();
971
+ for (const node of flattenNodes(nodes)) {
972
+ if (seen.has(node.id)) {
973
+ throw new GraphDslError(`Duplicate node id "${node.id}"`);
974
+ }
975
+ seen.add(node.id);
976
+ }
977
+ }
978
+
979
+ function assertUniquePortIds(node) {
980
+ const seen = new Set();
981
+ for (const id of Object.keys(node.legs)) {
982
+ if (seen.has(id)) {
983
+ throw new GraphDslError(`Duplicate port id "${id}" on "${node.id}"`);
984
+ }
985
+ seen.add(id);
986
+ }
987
+ }
988
+
989
+ function assertUniquePathIds(paths) {
990
+ const seen = new Set();
991
+ for (const path of paths) {
992
+ if (path.id == null) continue;
993
+ if (seen.has(path.id)) {
994
+ throw new GraphDslError(`Duplicate path id "${path.id}"`);
995
+ }
996
+ seen.add(path.id);
997
+ }
998
+ }
999
+
1000
+ function flattenModelPaths(graph) {
1001
+ return [
1002
+ ...(graph.paths ?? []),
1003
+ ...(graph.nodes ?? []).flatMap((node) => flattenNodePaths(node))
1004
+ ];
1005
+ }
1006
+
1007
+ function flattenNodePaths(node) {
1008
+ return [
1009
+ ...(node.paths ?? []),
1010
+ ...node.children.flatMap((child) => flattenNodePaths(child))
1011
+ ];
1012
+ }
1013
+
1014
+ function describeShape(shapeElement) {
1015
+ return {
1016
+ id: requiredAttr(shapeElement, "id"),
1017
+ attrs: { ...shapeElement.attrs },
1018
+ nodes: shapeElement.children
1019
+ .filter((node) => node.type === "element" && !isEdgeElement(node) && !isPathElement(node) && !isPortElement(node))
1020
+ .map((node) => node.attrs.id),
1021
+ paths: shapeElement.children.filter(isPathElement).map((path) => path.attrs.id).filter(Boolean),
1022
+ legs: shapeElement.children.filter(isPortElement).map((leg) => leg.attrs.id)
1023
+ };
1024
+ }
1025
+
1026
+ function assertElement(node, name) {
1027
+ if (!node || node.type !== "element" || node.name !== name) {
1028
+ throw new GraphDslError(`Expected <${name}>`);
1029
+ }
1030
+ }
1031
+
1032
+ function isElementNamed(name) {
1033
+ return (node) => node.type === "element" && node.name === name;
1034
+ }
1035
+
1036
+ function isNodeElement(node, shapes) {
1037
+ return node.type === "element" && (BUILTIN_SHAPE_TAGS.has(node.name) || shapes.has(node.name) || isPlotElement(node));
1038
+ }
1039
+
1040
+ function isEdgeElement(node) {
1041
+ return node.type === "element" && EDGE_TAGS.has(node.name);
1042
+ }
1043
+
1044
+ function isPathElement(node) {
1045
+ return node.type === "element" && PATH_TAGS.has(node.name);
1046
+ }
1047
+
1048
+ function isPortElement(node) {
1049
+ return node.type === "element" && PORT_TAGS.has(node.name);
1050
+ }
1051
+
1052
+ function isPlotElement(node) {
1053
+ return node.type === "element" && PLOT_TAGS.has(node.name);
1054
+ }
1055
+
1056
+ function isStyleElement(node) {
1057
+ return node.type === "element" && STYLE_TAGS.has(node.name);
1058
+ }
1059
+
1060
+ function isRepeatElement(node) {
1061
+ return node.type === "element" && REPEAT_TAGS.has(node.name);
1062
+ }
1063
+
1064
+ function normalizeNodeElement(nodeElement, shapes, styles) {
1065
+ if (isPlotElement(nodeElement)) {
1066
+ const attrs = resolveStyledAttrs({ ...nodeElement.attrs, shape: "plot" }, styles);
1067
+ normalizeBoxAttrs(attrs);
1068
+ return {
1069
+ shape: "plot",
1070
+ attrs,
1071
+ plot: buildPlotModel({
1072
+ ...nodeElement,
1073
+ attrs,
1074
+ children: nodeElement.children.filter((child) => !isPortElement(child))
1075
+ })
1076
+ };
1077
+ }
1078
+
1079
+ const shape = BUILTIN_SHAPE_TAGS.get(nodeElement.name) ?? nodeElement.name;
1080
+ if (!BUILTIN_SHAPE_TAGS.has(nodeElement.name) && !shapes.has(nodeElement.name)) {
1081
+ throw new GraphDslError(`Unknown shape tag <${nodeElement.name}>`);
1082
+ }
1083
+
1084
+ const defaults = shapes.get(nodeElement.name)?.attrs ?? {};
1085
+ const attrs = resolveStyledAttrs({ ...defaults, ...nodeElement.attrs, shape }, styles);
1086
+ normalizeBoxAttrs(attrs);
1087
+
1088
+ return { shape, attrs };
1089
+ }
1090
+
1091
+ function normalizeBoxAttrs(attrs) {
1092
+ if (Array.isArray(attrs.at)) {
1093
+ attrs.x = attrs.at[0] ?? 0;
1094
+ attrs.y = attrs.at[1] ?? 0;
1095
+ }
1096
+ if (Array.isArray(attrs.size)) {
1097
+ attrs.w = attrs.size[0] ?? attrs.w;
1098
+ attrs.h = attrs.size[1] ?? attrs.h;
1099
+ }
1100
+ if (attrs.width != null) attrs.w = attrs.width;
1101
+ if (attrs.height != null) attrs.h = attrs.height;
1102
+ }
1103
+
1104
+ function hasExplicitPosition(attrs) {
1105
+ return attrs.at != null || attrs.x != null || attrs.y != null;
1106
+ }
1107
+
1108
+ function coordinateAttr(attrs, name, fallback) {
1109
+ return numberAttr({ attrs }, name, fallback);
1110
+ }
1111
+
1112
+ function resolveSide(attrs) {
1113
+ if (attrs.side) return attrs.side;
1114
+ return SIDE_ATTRS.find((side) => attrs[side] === true) ?? null;
1115
+ }
1116
+
1117
+ function resolvePortAngle(attrs, side) {
1118
+ if (attrs.angle != null) {
1119
+ const angle = Number(attrs.angle);
1120
+ if (!Number.isFinite(angle)) {
1121
+ throw new GraphDslError("\"angle\" must be a number");
1122
+ }
1123
+ return angle;
1124
+ }
1125
+ return SIDE_ANGLES[side] ?? 0;
1126
+ }
1127
+
1128
+ function resolvePortCoordinates(attrs) {
1129
+ if (Array.isArray(attrs.at)) {
1130
+ return [optionalNumber(attrs.at[0]), optionalNumber(attrs.at[1])];
1131
+ }
1132
+ return [optionalNumber(attrs.x), optionalNumber(attrs.y)];
1133
+ }
1134
+
1135
+ function assertKnownChildren(element, shapes, options = {}) {
1136
+ for (const child of element.children) {
1137
+ if (child.type !== "element") continue;
1138
+ if (
1139
+ child.name === "Shape" ||
1140
+ (options.allowStyle && isStyleElement(child)) ||
1141
+ isNodeElement(child, shapes) ||
1142
+ isEdgeElement(child) ||
1143
+ isPathElement(child) ||
1144
+ isPortElement(child)
1145
+ ) {
1146
+ continue;
1147
+ }
1148
+ throw new GraphDslError(`Unknown tag <${child.name}>`);
1149
+ }
1150
+ }
1151
+
1152
+ function expandRepeats(element) {
1153
+ return expandElement(element, new Map(), { x: 0, y: 0 });
1154
+ }
1155
+
1156
+ function expandElement(element, scope, offset) {
1157
+ if (isRepeatElement(element)) {
1158
+ return expandRepeat(element, scope, offset);
1159
+ }
1160
+
1161
+ const attrs = offsetPositionAttrs(substituteAttrs(element.attrs, scope), element.name, offset);
1162
+ const childOffset = isPositionableElementName(element.name) ? { x: 0, y: 0 } : offset;
1163
+ const children = element.children.flatMap((child) => {
1164
+ if (child.type !== "element") return child;
1165
+ const expanded = expandElement(child, scope, childOffset);
1166
+ return Array.isArray(expanded) ? expanded : [expanded];
1167
+ });
1168
+
1169
+ return { ...element, attrs, children };
1170
+ }
1171
+
1172
+ function expandRepeat(element, scope, offset) {
1173
+ const count = numberAttr(element, "count", numberAttr(element, "n", 0));
1174
+ const variable = element.attrs.as ?? "i";
1175
+ const step = Array.isArray(element.attrs.step) ? element.attrs.step : [0, 0];
1176
+ const dx = optionalNumber(step[0]) ?? 0;
1177
+ const dy = optionalNumber(step[1]) ?? 0;
1178
+ const expanded = [];
1179
+
1180
+ for (let index = 0; index < count; index += 1) {
1181
+ const nextScope = new Map(scope);
1182
+ nextScope.set(variable, index);
1183
+ const nextOffset = {
1184
+ x: offset.x + dx * index,
1185
+ y: offset.y + dy * index
1186
+ };
1187
+
1188
+ for (const child of element.children) {
1189
+ if (child.type !== "element") continue;
1190
+ const item = expandElement(child, nextScope, nextOffset);
1191
+ expanded.push(...(Array.isArray(item) ? item : [item]));
1192
+ }
1193
+ }
1194
+
1195
+ return expanded;
1196
+ }
1197
+
1198
+ function substituteAttrs(attrs, scope) {
1199
+ return Object.fromEntries(Object.entries(attrs).map(([key, value]) => {
1200
+ return [key, substituteValue(value, scope)];
1201
+ }));
1202
+ }
1203
+
1204
+ function substituteValue(value, scope) {
1205
+ if (isRefLiteral(value)) {
1206
+ return scope.has(value[REF_LITERAL]) ? scope.get(value[REF_LITERAL]) : value;
1207
+ }
1208
+ if (isPointLiteral(value)) {
1209
+ return pointLiteral(substitutePointExpression(value[POINT_LITERAL], (item) => substituteValue(item, scope)));
1210
+ }
1211
+ if (isExpressionLiteral(value)) {
1212
+ const result = evaluateExpression(value[EXPRESSION_LITERAL], scope, { strict: false });
1213
+ return result.resolved ? result.value : value;
1214
+ }
1215
+ if (isTemplateLiteral(value)) {
1216
+ const rendered = renderTemplateLiteral(value[TEMPLATE_LITERAL], scope, { strict: false });
1217
+ return rendered.complete ? rendered.value : templateLiteral(rendered.value);
1218
+ }
1219
+ if (typeof value === "string") {
1220
+ return substituteTemplate(value, scope);
1221
+ }
1222
+ if (Array.isArray(value)) {
1223
+ return value.map((item) => substituteValue(item, scope));
1224
+ }
1225
+ if (value && typeof value === "object") {
1226
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
1227
+ return [key, substituteValue(item, scope)];
1228
+ }));
1229
+ }
1230
+ return value;
1231
+ }
1232
+
1233
+ function substituteShapeProps(element, attrs) {
1234
+ const scope = new Map(Object.entries(attrs));
1235
+ return substituteElementProps(element, scope);
1236
+ }
1237
+
1238
+ function substituteElementProps(element, scope) {
1239
+ return {
1240
+ ...element,
1241
+ attrs: substitutePropAttrs(element.attrs, scope),
1242
+ children: element.children.map((child) => {
1243
+ if (child.type !== "element") return child;
1244
+ return substituteElementProps(child, scope);
1245
+ })
1246
+ };
1247
+ }
1248
+
1249
+ function substitutePropAttrs(attrs, scope) {
1250
+ return Object.fromEntries(Object.entries(attrs).map(([key, value]) => {
1251
+ return [key, substitutePropValue(value, scope)];
1252
+ }));
1253
+ }
1254
+
1255
+ function substitutePropValue(value, scope) {
1256
+ if (isRefLiteral(value)) {
1257
+ const name = value[REF_LITERAL];
1258
+ if (!scope.has(name)) {
1259
+ throw new GraphDslError(`Unknown shape prop "${name}"`);
1260
+ }
1261
+ return scope.get(name);
1262
+ }
1263
+ if (isPointLiteral(value)) {
1264
+ return pointLiteral(substitutePointExpression(value[POINT_LITERAL], (item) => substitutePropValue(item, scope)));
1265
+ }
1266
+ if (isExpressionLiteral(value)) {
1267
+ return evaluateExpression(value[EXPRESSION_LITERAL], scope, { strict: true }).value;
1268
+ }
1269
+ if (isTemplateLiteral(value)) {
1270
+ const rendered = renderTemplateLiteral(value[TEMPLATE_LITERAL], scope, { strict: true });
1271
+ return rendered.value;
1272
+ }
1273
+ if (Array.isArray(value)) {
1274
+ return value.map((item) => substitutePropValue(item, scope));
1275
+ }
1276
+ if (value && typeof value === "object") {
1277
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
1278
+ return [key, substitutePropValue(item, scope)];
1279
+ }));
1280
+ }
1281
+ return value;
1282
+ }
1283
+
1284
+ function substituteTemplate(source, scope) {
1285
+ return source.replace(/\{([^{}]+)\}/g, (match, expression, offset) => {
1286
+ const values = expression.split(",").map((term) => evaluateTemplateTerm(term.trim(), scope));
1287
+ if (!values.every((value) => value.resolved)) return match;
1288
+
1289
+ const replacement = values.map((value) => value.value).join(",");
1290
+ return source[offset - 1] === "_" || source[offset - 1] === "^" ? `{${replacement}}` : replacement;
1291
+ });
1292
+ }
1293
+
1294
+ function renderTemplateLiteral(source, scope, options) {
1295
+ const consumed = new Set();
1296
+ let complete = true;
1297
+ const value = source.replace(/\$\{([^{}]+)\}/g, (match, expression) => {
1298
+ const result = evaluateTemplateTerm(expression.trim(), scope, options);
1299
+ if (!result.resolved) {
1300
+ if (options.strict) {
1301
+ throw new GraphDslError(`Unknown template variable "${expression.trim()}"`);
1302
+ }
1303
+ complete = false;
1304
+ return match;
1305
+ }
1306
+ consumed.add(result.name);
1307
+ return result.value;
1308
+ });
1309
+
1310
+ return { value, consumed, complete };
1311
+ }
1312
+
1313
+ function evaluateTemplateTerm(term, scope, options = {}) {
1314
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(term)) {
1315
+ if (scope.has(term)) {
1316
+ return { resolved: true, value: scope.get(term) };
1317
+ }
1318
+ if (options.strict) {
1319
+ throw new GraphDslError(`Unknown template variable "${term}"`);
1320
+ }
1321
+ return { resolved: false };
1322
+ }
1323
+ return evaluateExpression(term, scope, options);
1324
+ }
1325
+
1326
+ function offsetPositionAttrs(attrs, name, offset) {
1327
+ if (!isPositionableElementName(name) || (offset.x === 0 && offset.y === 0)) {
1328
+ return attrs;
1329
+ }
1330
+
1331
+ if (Array.isArray(attrs.at)) {
1332
+ return {
1333
+ ...attrs,
1334
+ at: [
1335
+ offsetNumber(attrs.at[0], offset.x),
1336
+ offsetNumber(attrs.at[1], offset.y),
1337
+ ...attrs.at.slice(2)
1338
+ ]
1339
+ };
1340
+ }
1341
+
1342
+ if (name === "Path" || name === "path") {
1343
+ return {
1344
+ ...attrs,
1345
+ points: offsetPathPoints(attrs.points, offset)
1346
+ };
1347
+ }
1348
+
1349
+ return {
1350
+ ...attrs,
1351
+ x: offsetNumber(attrs.x, offset.x),
1352
+ y: offsetNumber(attrs.y, offset.y)
1353
+ };
1354
+ }
1355
+
1356
+ function isPositionableElementName(name) {
1357
+ return (
1358
+ !STYLE_TAGS.has(name) &&
1359
+ !REPEAT_TAGS.has(name) &&
1360
+ name !== "Graph" &&
1361
+ name !== "Shape" &&
1362
+ !EDGE_TAGS.has(name)
1363
+ );
1364
+ }
1365
+
1366
+ function offsetPathPoints(points, offset) {
1367
+ if (!Array.isArray(points)) return points;
1368
+ return points.map((point) => {
1369
+ if (!Array.isArray(point)) return point;
1370
+ return [
1371
+ offsetNumber(point[0], offset.x),
1372
+ offsetNumber(point[1], offset.y),
1373
+ ...point.slice(2)
1374
+ ];
1375
+ });
1376
+ }
1377
+
1378
+ function offsetNumber(value, offset) {
1379
+ return (value == null ? 0 : Number(value)) + offset;
1380
+ }
1381
+
1382
+ function buildStyles(styleElements) {
1383
+ return new Map(styleElements.map((element) => {
1384
+ const id = requiredAttr(element, "id");
1385
+ return [id, styleAttrs(element.attrs)];
1386
+ }));
1387
+ }
1388
+
1389
+ function resolveStyledAttrs(attrs, styles) {
1390
+ const namedStyle = attrs.useStyle == null ? {} : lookupStyle(styles, attrs.useStyle);
1391
+ const style = {
1392
+ ...namedStyle,
1393
+ ...styleAttrs(attrs.style ?? {})
1394
+ };
1395
+ if (Object.keys(style).length === 0) {
1396
+ return { ...attrs };
1397
+ }
1398
+ return {
1399
+ ...attrs,
1400
+ style
1401
+ };
1402
+ }
1403
+
1404
+ function lookupStyle(styles, id) {
1405
+ if (!styles.has(id)) {
1406
+ throw new GraphDslError(`Unknown style "${id}"`);
1407
+ }
1408
+ return styles.get(id);
1409
+ }
1410
+
1411
+ function styleAttrs(attrs) {
1412
+ const { id, useStyle, style, ...rest } = attrs;
1413
+ return { ...rest, ...(style && typeof style === "object" ? style : {}) };
1414
+ }
1415
+
1416
+ function requiredAttr(element, name) {
1417
+ if (element.attrs[name] == null || element.attrs[name] === "") {
1418
+ throw new GraphDslError(`<${element.name}> requires "${name}"`);
1419
+ }
1420
+ return element.attrs[name];
1421
+ }
1422
+
1423
+ function endpointAttr(element, name) {
1424
+ const value = requiredAttr(element, name);
1425
+ if (typeof value === "string") return value;
1426
+ throw new GraphDslError(`<${element.name}> "${name}" must be a quoted port address like "A.right"`);
1427
+ }
1428
+
1429
+ function numberAttr(element, name, fallback) {
1430
+ const value = element.attrs[name];
1431
+ if (value == null) return fallback;
1432
+ const number = Number(value);
1433
+ if (!Number.isFinite(number)) {
1434
+ throw new GraphDslError(`"${name}" must be a number`);
1435
+ }
1436
+ return number;
1437
+ }
1438
+
1439
+ function numberFromAttrs(attrs, name, fallback) {
1440
+ const value = attrs[name];
1441
+ if (value == null) return fallback;
1442
+ const number = Number(value);
1443
+ if (!Number.isFinite(number)) {
1444
+ throw new GraphDslError(`"${name}" must be a number`);
1445
+ }
1446
+ return number;
1447
+ }
1448
+
1449
+ function optionalNumber(value) {
1450
+ if (value == null) return null;
1451
+ const number = Number(value);
1452
+ if (!Number.isFinite(number)) {
1453
+ throw new GraphDslError("Leg coordinates must be numbers");
1454
+ }
1455
+ return number;
1456
+ }
1457
+
1458
+ function booleanAttr(value, fallback) {
1459
+ if (value == null) return fallback;
1460
+ if (value === false || value === "false") return false;
1461
+ if (value === true || value === "true") return true;
1462
+ return Boolean(value);
1463
+ }