@oml/markdown 0.9.0 → 0.11.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.
Files changed (37) hide show
  1. package/out/md/md-executor.js +2 -9
  2. package/out/md/md-executor.js.map +1 -1
  3. package/out/md/md-runtime.js +2 -26
  4. package/out/md/md-runtime.js.map +1 -1
  5. package/out/renderers/chart-renderer.js +72 -4
  6. package/out/renderers/chart-renderer.js.map +1 -1
  7. package/out/renderers/diagram-renderer.js +738 -252
  8. package/out/renderers/diagram-renderer.js.map +1 -1
  9. package/out/renderers/graph-renderer.js +5 -9
  10. package/out/renderers/graph-renderer.js.map +1 -1
  11. package/out/renderers/renderer.d.ts +3 -0
  12. package/out/renderers/renderer.js +53 -0
  13. package/out/renderers/renderer.js.map +1 -1
  14. package/out/renderers/table-renderer.d.ts +8 -1
  15. package/out/renderers/table-renderer.js +22 -1
  16. package/out/renderers/table-renderer.js.map +1 -1
  17. package/out/renderers/text-renderer.d.ts +0 -1
  18. package/out/renderers/text-renderer.js +2 -9
  19. package/out/renderers/text-renderer.js.map +1 -1
  20. package/out/renderers/wikilink-utils.js +3 -10
  21. package/out/renderers/wikilink-utils.js.map +1 -1
  22. package/out/static/browser-runtime.bundle.js +1591 -6408
  23. package/out/static/browser-runtime.bundle.js.map +4 -4
  24. package/out/static/browser-runtime.js +3 -0
  25. package/out/static/browser-runtime.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/md/md-executor.ts +2 -9
  28. package/src/md/md-runtime.ts +2 -28
  29. package/src/renderers/chart-renderer.ts +93 -2
  30. package/src/renderers/diagram-renderer.ts +799 -258
  31. package/src/renderers/graph-renderer.ts +5 -9
  32. package/src/renderers/renderer.ts +66 -0
  33. package/src/renderers/table-renderer.ts +39 -3
  34. package/src/renderers/text-renderer.ts +2 -7
  35. package/src/renderers/wikilink-utils.ts +4 -10
  36. package/src/static/browser-runtime.ts +3 -0
  37. package/src/static/markdown-webview.css +1361 -0
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
  import 'reflect-metadata';
3
- import { CanvasMarkdownBlockRenderer } from './renderer.js';
3
+ import { CanvasMarkdownBlockRenderer, displayLabelFromIri } from './renderer.js';
4
4
  const D = 'http://opencaesar.io/oml/diagram#';
5
5
  const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
6
6
  const TYPE_IRIS = {
@@ -55,12 +55,15 @@ export class DiagramMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
55
55
  const tripleIndex = indexTriples(rows);
56
56
  const stylesheet = parseDiagramStylesheet(result.options);
57
57
  const compiled = compileDiagramGraph(tripleIndex, stylesheet);
58
- const layoutOptions = resolveDagreLayoutOptions(result.options);
58
+ const diagramStyle = resolveElementStyle('diagram', 'diagram', [], {}, stylesheet);
59
+ const layoutOptions = resolveDagreLayoutOptions(diagramStyle);
60
+ const rootSpacing = resolveRootSpacing(diagramStyle);
59
61
  if (compiled.nodes.length === 0) {
60
62
  container.appendChild(this.createMessageContainer('No diagram nodes were inferred from the diagram namespace triples.'));
61
63
  return container;
62
64
  }
63
65
  void renderWithX6(canvas, baseId, compiled, layoutOptions, {
66
+ rootSpacing,
64
67
  downloadSvg: (content) => this.requestTextFileDownload(content, 'diagram', 'svg'),
65
68
  }).catch((error) => {
66
69
  const detail = error instanceof Error ? error.message : String(error);
@@ -76,7 +79,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
76
79
  return;
77
80
  }
78
81
  const GraphCtor = await loadX6GraphCtor();
79
- const layout = await layoutGraphDagre(graph, layoutOptions);
82
+ const layout = await layoutGraphDagre(graph, layoutOptions, actions.rootSpacing);
80
83
  const minHeight = asFiniteNumber(parseCssPixels(liveCanvas.style.getPropertyValue('--oml-diagram-min-height')), numericCanvasMinHeight(undefined));
81
84
  const desiredHeight = Math.max(minHeight, Math.ceil(layout.contentHeight + 12));
82
85
  setLockedCanvasHeight(liveCanvas, desiredHeight, minHeight);
@@ -84,30 +87,23 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
84
87
  let isResizingCanvas = false;
85
88
  const isPortPointerTarget = (event) => {
86
89
  const target = event?.target;
87
- if (target?.closest('.x6-port, .x6-port-body, .oml-port-body')) {
90
+ if (target?.closest('.x6-port, .x6-port-body, .oml-port-body, [port]')) {
88
91
  return true;
89
92
  }
90
93
  const path = typeof event?.composedPath === 'function' ? event.composedPath() : [];
91
94
  return path.some((entry) => (entry instanceof Element
92
95
  && (entry.classList.contains('x6-port')
93
96
  || entry.classList.contains('x6-port-body')
94
- || entry.classList.contains('oml-port-body'))));
95
- };
96
- const findPortIdFromTarget = (target) => {
97
- if (!(target instanceof Element))
98
- return undefined;
99
- const portHost = target.closest('.x6-port');
100
- if (!portHost)
101
- return undefined;
102
- return portHost.getAttribute('port')
103
- ?? portHost.getAttribute('data-port-id')
104
- ?? undefined;
97
+ || entry.classList.contains('oml-port-body')
98
+ || entry.hasAttribute('port'))));
105
99
  };
106
100
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
107
101
  const isCompartmentNode = (node) => {
108
102
  const nodeId = String(node?.id ?? '');
109
103
  return nodeById.get(nodeId)?.kind === 'Compartment';
110
104
  };
105
+ const boundPortContainers = new WeakSet();
106
+ let nodeTransform;
111
107
  const graphView = new GraphCtor({
112
108
  container: liveCanvas,
113
109
  autoResize: false,
@@ -121,7 +117,10 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
121
117
  },
122
118
  connecting: {
123
119
  router: 'normal',
124
- connector: 'rounded',
120
+ connector: {
121
+ name: 'jumpover',
122
+ args: { size: 5 },
123
+ },
125
124
  allowBlank: false,
126
125
  allowNode: false,
127
126
  allowPort: false,
@@ -137,13 +136,65 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
137
136
  edgeMovable: false,
138
137
  vertexMovable: false,
139
138
  arrowheadMovable: false,
140
- labelMovable: false,
139
+ labelMovable: true,
141
140
  };
142
141
  },
143
142
  background: {
144
143
  color: CSS_CANVAS_BACKGROUND,
145
144
  },
145
+ onPortRendered: ({ port, node, container }) => {
146
+ if (boundPortContainers.has(container)) {
147
+ return;
148
+ }
149
+ boundPortContainers.add(container);
150
+ container.setAttribute('data-port-id', String(port.id));
151
+ container.style.cursor = 'grab';
152
+ container.style.touchAction = 'none';
153
+ container.addEventListener('pointerdown', (event) => {
154
+ if (typeof graphView.cleanSelection === 'function') {
155
+ graphView.cleanSelection();
156
+ }
157
+ nodeTransform.clearWidgets();
158
+ const owner = graphView.getCellById(String(node.id));
159
+ if (!owner) {
160
+ return;
161
+ }
162
+ startPortDrag(event.clientX, event.clientY, owner, String(port.id), event.pointerId, container);
163
+ event.preventDefault();
164
+ event.stopPropagation();
165
+ if (typeof event.stopImmediatePropagation === 'function') {
166
+ event.stopImmediatePropagation();
167
+ }
168
+ if (typeof container.setPointerCapture === 'function') {
169
+ try {
170
+ container.setPointerCapture(event.pointerId);
171
+ }
172
+ catch {
173
+ // Ignore capture failures; window listeners still handle the drag.
174
+ }
175
+ }
176
+ });
177
+ },
178
+ });
179
+ const x6Mod = await import('@antv/x6');
180
+ const TransformCtor = x6Mod.Transform;
181
+ if (typeof TransformCtor !== 'function') {
182
+ throw new Error('X6 Transform plugin is unavailable in @antv/x6');
183
+ }
184
+ nodeTransform = new TransformCtor({
185
+ resizing: {
186
+ enabled: (node) => node?.getData?.()?.kind === 'Node',
187
+ minWidth: 48,
188
+ minHeight: 32,
189
+ orthogonal: true,
190
+ restrict: false,
191
+ autoScroll: false,
192
+ preserveAspectRatio: false,
193
+ allowReverse: false,
194
+ },
195
+ rotating: false,
146
196
  });
197
+ graphView.use(nodeTransform);
147
198
  const toPlainRect = (value) => {
148
199
  if (!value)
149
200
  return undefined;
@@ -178,12 +229,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
178
229
  for (const list of portsByOwner.values()) {
179
230
  list.sort((a, b) => a.id.localeCompare(b.id));
180
231
  }
181
- const ownerByPortId = new Map();
182
- for (const [ownerId, ports] of portsByOwner.entries()) {
183
- for (const port of ports) {
184
- ownerByPortId.set(port.id, ownerId);
185
- }
186
- }
232
+ const portPlacementById = computeDefaultPortPlacements(graph, nodeById, portsByOwner, layout.boxes);
187
233
  const ordered = [...graph.nodes]
188
234
  .filter((node) => node.kind !== 'Port')
189
235
  .sort((a, b) => nodeDepth(a.id, nodeById) - nodeDepth(b.id, nodeById));
@@ -194,19 +240,35 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
194
240
  const labelText = node.labels.length > 0 ? node.labels.join('\n') : localName(node.id);
195
241
  const resolvedStyle = node.style;
196
242
  const resolvedShape = resolveRenderNodeShape(resolvedStyle);
243
+ const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
197
244
  const x = box.x;
198
245
  const y = box.y;
199
246
  const ownerPorts = portsByOwner.get(node.id) ?? [];
200
- const portItems = ownerPorts.map((port, index) => ({
201
- id: port.id,
202
- group: 'boundary',
203
- args: {
204
- x: box.width,
205
- y: ((index + 1) * box.height) / (ownerPorts.length + 1),
206
- },
207
- attrs: resolvePortAttrs(port.style, port.classes, port.labels[0] ?? 'Port'),
208
- }));
209
- const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
247
+ const portItems = ownerPorts.map((port) => {
248
+ const placement = portPlacementById.get(port.id) ?? { side: 'right', ratio: 0.5 };
249
+ const ratio = clamp(placement.ratio, 0.05, 0.95);
250
+ const position = placement.side === 'left' || placement.side === 'right'
251
+ ? {
252
+ x: placement.side === 'left' ? 0 : box.width,
253
+ y: ratio * box.height,
254
+ }
255
+ : {
256
+ x: ratio * box.width,
257
+ y: placement.side === 'top' ? 0 : box.height,
258
+ };
259
+ const portText = port.labels[0];
260
+ return {
261
+ id: port.id,
262
+ group: 'boundary',
263
+ args: {
264
+ x: position.x,
265
+ y: position.y,
266
+ side: placement.side,
267
+ ratio,
268
+ },
269
+ attrs: resolvePortAttrs(port.style, port.classes, portText, placement.side, typeof bodyAttrs.stroke === 'string' ? bodyAttrs.stroke : undefined),
270
+ };
271
+ });
210
272
  const iconPathSelectors = iconPathAttrs.map((_, index) => `iconPath${index}`);
211
273
  graphView.addNode({
212
274
  id: node.id,
@@ -310,7 +372,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
310
372
  },
311
373
  items: portItems,
312
374
  } : undefined,
313
- zIndex: 10,
375
+ zIndex: 50,
314
376
  data: {
315
377
  kind: node.kind,
316
378
  ownerId: node.parentId,
@@ -457,11 +519,32 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
457
519
  let maxBottom = Number.NEGATIVE_INFINITY;
458
520
  const childCells = [];
459
521
  const childDebug = [];
522
+ const childGeometryBounds = (child) => {
523
+ if (!child || typeof child.size !== 'function') {
524
+ return undefined;
525
+ }
526
+ const size = child.size();
527
+ const width = Number(size?.width);
528
+ const height = Number(size?.height);
529
+ let position;
530
+ if (typeof child.getPosition === 'function') {
531
+ position = child.getPosition();
532
+ }
533
+ else {
534
+ position = child.position;
535
+ }
536
+ const x = Number(position?.x);
537
+ const y = Number(position?.y);
538
+ if (![x, y, width, height].every(Number.isFinite)) {
539
+ return undefined;
540
+ }
541
+ return { x, y, width, height };
542
+ };
460
543
  for (const childId of childIds) {
461
544
  const child = graphView.getCellById(childId);
462
- if (!child || typeof child.getBBox !== 'function')
545
+ const absBBox = childGeometryBounds(child);
546
+ if (!absBBox)
463
547
  continue;
464
- const absBBox = child.getBBox();
465
548
  const relLeft = absBBox.x - containerBBox.x;
466
549
  const relTop = absBBox.y - containerBBox.y;
467
550
  minLeft = Math.min(minLeft, relLeft);
@@ -493,10 +576,11 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
493
576
  // container origin (without moving its embedded children, which keep their
494
577
  // absolute positions) and expand the size to compensate so the opposite edge
495
578
  // is preserved.
496
- // topPadding is the reserved header/label area; children must stay below it.
497
- const topPadding = containerSpec.contentTopPadding;
498
- const shiftX = minLeft < 0 ? minLeft : 0;
499
- const shiftY = minTop < topPadding ? minTop - topPadding : 0;
579
+ const spacing = resolveBoxSpacing(containerSpec.style, 0);
580
+ const minInsetX = spacing.marginLeft + spacing.paddingLeft;
581
+ const minInsetY = spacing.marginTop + spacing.paddingTop;
582
+ const shiftX = minLeft < minInsetX ? minLeft - minInsetX : 0;
583
+ const shiftY = minTop < minInsetY ? minTop - minInsetY : 0;
500
584
  // Dimensions needed relative to the (possibly shifted) new origin.
501
585
  const baseWidth = containerSize.width - shiftX;
502
586
  const baseHeight = containerSize.height - shiftY;
@@ -509,7 +593,8 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
509
593
  childIds,
510
594
  containerBBox: toPlainRect(containerBBox),
511
595
  containerSize: toPlainRect({ x: 0, y: 0, ...containerSize }),
512
- topPadding,
596
+ minInsetX,
597
+ minInsetY,
513
598
  bounds: { minLeft, minTop, maxRight, maxBottom },
514
599
  shift: { shiftX, shiftY },
515
600
  childDebug,
@@ -526,6 +611,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
526
611
  to: { width: neededWidth, height: neededHeight },
527
612
  });
528
613
  container.resize(neededWidth, neededHeight);
614
+ syncOwnedPortPositions(containerId);
529
615
  }
530
616
  else {
531
617
  logResize('container-no-resize', { containerId });
@@ -610,6 +696,48 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
610
696
  return;
611
697
  growAncestorContainers(node);
612
698
  });
699
+ const syncOwnedPortPositions = (ownerId) => {
700
+ const owner = graphView.getCellById(ownerId);
701
+ if (!owner || typeof owner.size !== 'function' || typeof owner.getPorts !== 'function' || typeof owner.setPortProp !== 'function') {
702
+ return;
703
+ }
704
+ const size = owner.size();
705
+ const portIds = owner.getPorts()
706
+ .map((port) => String(port?.id ?? ''))
707
+ .filter((portId) => portId.length > 0);
708
+ for (const portId of portIds) {
709
+ const placement = portPlacementById.get(portId) ?? { side: 'right', ratio: 0.5 };
710
+ const ratio = clamp(placement.ratio, 0.05, 0.95);
711
+ const nextArgs = placement.side === 'left' || placement.side === 'right'
712
+ ? {
713
+ x: placement.side === 'left' ? 0 : size.width,
714
+ y: ratio * size.height,
715
+ side: placement.side,
716
+ ratio,
717
+ }
718
+ : {
719
+ x: ratio * size.width,
720
+ y: placement.side === 'top' ? 0 : size.height,
721
+ side: placement.side,
722
+ ratio,
723
+ };
724
+ owner.setPortProp(portId, {
725
+ args: nextArgs,
726
+ label: {
727
+ position: resolveBorderPortLabelPosition(placement.side),
728
+ },
729
+ });
730
+ }
731
+ };
732
+ graphView.on('node:resizing', ({ node }) => {
733
+ syncOwnedPortPositions(String(node?.id ?? ''));
734
+ });
735
+ graphView.on('node:resized', ({ node, options }) => {
736
+ if (options?.silent)
737
+ return;
738
+ syncOwnedPortPositions(String(node?.id ?? ''));
739
+ growAncestorContainers(node);
740
+ });
613
741
  const edgeLabelPosition = (placement) => {
614
742
  if (placement === 'begin')
615
743
  return 0.15;
@@ -617,105 +745,104 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
617
745
  return 0.85;
618
746
  return 0.5;
619
747
  };
620
- const endpointOwnerId = (nodeId) => {
621
- const node = nodeById.get(nodeId);
622
- if (!node)
623
- return undefined;
624
- return node.kind === 'Port' ? node.parentId : node.id;
625
- };
626
- const undirectedPairKey = (a, b) => (a < b ? `${a}<->${b}` : `${b}<->${a}`);
627
- const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
748
+ const directedPairKey = (sourceId, targetId) => `${sourceId}=>${targetId}`;
749
+ const undirectedPairKey = (leftId, rightId) => (leftId < rightId ? `${leftId}<=>${rightId}` : `${rightId}<=>${leftId}`);
750
+ const edgeIdsByPair = new Map();
628
751
  const undirectedEdgeIdsByPair = new Map();
629
752
  for (const edge of graph.edges) {
630
- const sourceOwner = endpointOwnerId(edge.sourceId);
631
- const targetOwner = endpointOwnerId(edge.targetId);
632
- if (!sourceOwner || !targetOwner || sourceOwner === targetOwner)
753
+ if (!edge.sourceId || !edge.targetId)
633
754
  continue;
634
- const key = undirectedPairKey(sourceOwner, targetOwner);
635
- const ids = undirectedEdgeIdsByPair.get(key) ?? [];
755
+ const key = directedPairKey(edge.sourceId, edge.targetId);
756
+ const ids = edgeIdsByPair.get(key) ?? [];
636
757
  ids.push(edge.id);
637
- undirectedEdgeIdsByPair.set(key, ids);
638
- }
639
- const fanningVertexForEdge = (edge) => {
640
- const sourceOwner = endpointOwnerId(edge.sourceId);
641
- const targetOwner = endpointOwnerId(edge.targetId);
642
- if (!sourceOwner || !targetOwner || sourceOwner === targetOwner) {
643
- return undefined;
644
- }
645
- const edgeIds = undirectedEdgeIdsByPair.get(undirectedPairKey(sourceOwner, targetOwner)) ?? [];
646
- if (edgeIds.length <= 1) {
758
+ edgeIdsByPair.set(key, ids);
759
+ const undirectedKey = undirectedPairKey(edge.sourceId, edge.targetId);
760
+ const pairIds = undirectedEdgeIdsByPair.get(undirectedKey) ?? [];
761
+ pairIds.push(edge.id);
762
+ undirectedEdgeIdsByPair.set(undirectedKey, pairIds);
763
+ }
764
+ const edgePointForEndpoint = (endpointId) => {
765
+ const endpoint = nodeById.get(endpointId);
766
+ if (!endpoint) {
647
767
  return undefined;
648
768
  }
649
- const orderedEdgeIds = [...edgeIds].sort((leftId, rightId) => {
650
- const leftEdge = edgeById.get(leftId);
651
- const rightEdge = edgeById.get(rightId);
652
- if (!leftEdge || !rightEdge)
653
- return leftId.localeCompare(rightId);
654
- const leftSource = endpointOwnerId(leftEdge.sourceId);
655
- const leftTarget = endpointOwnerId(leftEdge.targetId);
656
- const rightSource = endpointOwnerId(rightEdge.sourceId);
657
- const rightTarget = endpointOwnerId(rightEdge.targetId);
658
- const leftForward = leftSource && leftTarget ? (leftSource < leftTarget ? 0 : 1) : 0;
659
- const rightForward = rightSource && rightTarget ? (rightSource < rightTarget ? 0 : 1) : 0;
660
- if (leftForward !== rightForward)
661
- return leftForward - rightForward;
662
- return leftId.localeCompare(rightId);
663
- });
664
- const [pairA, pairB] = sourceOwner < targetOwner
665
- ? [sourceOwner, targetOwner]
666
- : [targetOwner, sourceOwner];
667
- const forwardIds = [];
668
- const reverseIds = [];
669
- for (const edgeId of orderedEdgeIds) {
670
- const pairEdge = edgeById.get(edgeId);
671
- if (!pairEdge)
672
- continue;
673
- const pairSource = endpointOwnerId(pairEdge.sourceId);
674
- const pairTarget = endpointOwnerId(pairEdge.targetId);
675
- if (pairSource === pairA && pairTarget === pairB) {
676
- forwardIds.push(edgeId);
769
+ if (endpoint.kind === 'Port' && endpoint.parentId) {
770
+ const ownerBox = layout.boxes.get(endpoint.parentId);
771
+ const placement = portPlacementById.get(endpoint.id);
772
+ if (!ownerBox || !placement) {
773
+ return undefined;
774
+ }
775
+ const ratio = clamp(placement.ratio, 0.05, 0.95);
776
+ if (placement.side === 'left') {
777
+ return { x: ownerBox.x, y: ownerBox.y + (ownerBox.height * ratio) };
677
778
  }
678
- else if (pairSource === pairB && pairTarget === pairA) {
679
- reverseIds.push(edgeId);
779
+ if (placement.side === 'right') {
780
+ return { x: ownerBox.x + ownerBox.width, y: ownerBox.y + (ownerBox.height * ratio) };
680
781
  }
782
+ if (placement.side === 'top') {
783
+ return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y };
784
+ }
785
+ return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y + ownerBox.height };
681
786
  }
682
- // Only fan when both directions exist for the same endpoint pair.
683
- if (forwardIds.length === 0 || reverseIds.length === 0) {
787
+ const box = layout.boxes.get(endpointId);
788
+ if (!box) {
684
789
  return undefined;
685
790
  }
686
- let directionSign = 0;
687
- let laneIndex = 0;
688
- let laneCount = 0;
689
- if (sourceOwner === pairA && targetOwner === pairB) {
690
- directionSign = 1;
691
- laneIndex = forwardIds.indexOf(edge.id);
692
- laneCount = forwardIds.length;
693
- }
694
- else if (sourceOwner === pairB && targetOwner === pairA) {
695
- directionSign = -1;
696
- laneIndex = reverseIds.indexOf(edge.id);
697
- laneCount = reverseIds.length;
791
+ return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
792
+ };
793
+ const fanningVertexForEdge = (edge) => {
794
+ if (!edge.sourceId || !edge.targetId) {
795
+ return undefined;
698
796
  }
699
- if (directionSign === 0 || laneIndex < 0 || laneCount <= 0) {
797
+ const undirectedIds = undirectedEdgeIdsByPair.get(undirectedPairKey(edge.sourceId, edge.targetId)) ?? [];
798
+ if (undirectedIds.length <= 1) {
700
799
  return undefined;
701
800
  }
702
- const pairABox = layout.boxes.get(pairA);
703
- const pairBBox = layout.boxes.get(pairB);
704
- if (!pairABox || !pairBBox) {
801
+ const sourcePoint = edgePointForEndpoint(edge.sourceId);
802
+ const targetPoint = edgePointForEndpoint(edge.targetId);
803
+ if (!sourcePoint || !targetPoint) {
705
804
  return undefined;
706
805
  }
707
- const sx = pairABox.x + (pairABox.width / 2);
708
- const sy = pairABox.y + (pairABox.height / 2);
709
- const tx = pairBBox.x + (pairBBox.width / 2);
710
- const ty = pairBBox.y + (pairBBox.height / 2);
806
+ const sx = sourcePoint.x;
807
+ const sy = sourcePoint.y;
808
+ const tx = targetPoint.x;
809
+ const ty = targetPoint.y;
711
810
  const dx = tx - sx;
712
811
  const dy = ty - sy;
713
812
  const len = Math.hypot(dx, dy);
714
813
  if (!Number.isFinite(len) || len < 1) {
715
814
  return undefined;
716
815
  }
717
- const laneOffset = (laneIndex - ((laneCount - 1) / 2));
718
- const offset = (directionSign * 16) + (laneOffset * 10);
816
+ const forwardIds = (edgeIdsByPair.get(directedPairKey(edge.sourceId, edge.targetId)) ?? [])
817
+ .slice()
818
+ .sort((left, right) => left.localeCompare(right));
819
+ const reverseIds = (edgeIdsByPair.get(directedPairKey(edge.targetId, edge.sourceId)) ?? [])
820
+ .slice()
821
+ .sort((left, right) => left.localeCompare(right));
822
+ let offset = 0;
823
+ if (forwardIds.length > 0 && reverseIds.length > 0) {
824
+ const forwardIndex = forwardIds.indexOf(edge.id);
825
+ const reverseIndex = reverseIds.indexOf(edge.id);
826
+ if (forwardIndex >= 0) {
827
+ const laneOffset = forwardIndex - ((forwardIds.length - 1) / 2);
828
+ offset = 16 + (laneOffset * 10);
829
+ }
830
+ else if (reverseIndex >= 0) {
831
+ const laneOffset = reverseIndex - ((reverseIds.length - 1) / 2);
832
+ offset = -16 + (laneOffset * 10);
833
+ }
834
+ else {
835
+ return undefined;
836
+ }
837
+ }
838
+ else {
839
+ const laneIndex = forwardIds.indexOf(edge.id);
840
+ if (laneIndex < 0 || forwardIds.length <= 1) {
841
+ return undefined;
842
+ }
843
+ const laneOffset = laneIndex - ((forwardIds.length - 1) / 2);
844
+ offset = laneOffset * 16;
845
+ }
719
846
  if (Math.abs(offset) < 0.01) {
720
847
  return undefined;
721
848
  }
@@ -750,8 +877,8 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
750
877
  id: edge.id,
751
878
  source: resolveEndpoint(edge.sourceId),
752
879
  target: resolveEndpoint(edge.targetId),
753
- router: { name: 'normal' },
754
- connector: { name: 'rounded' },
880
+ router: resolveEdgeRouter(resolvedStyle),
881
+ connector: resolveEdgeConnector(resolvedStyle),
755
882
  attrs: {
756
883
  line: lineAttrs,
757
884
  },
@@ -768,7 +895,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
768
895
  labelBody: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
769
896
  },
770
897
  })),
771
- zIndex: 5,
898
+ zIndex: 50,
772
899
  });
773
900
  }
774
901
  // Compartments are structural containers: do not drag them; select parent instead.
@@ -806,56 +933,120 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
806
933
  e.preventDefault();
807
934
  e.stopPropagation();
808
935
  });
809
- const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
936
+ const sidePriority = (side) => {
937
+ switch (side) {
938
+ case 'left':
939
+ return 0;
940
+ case 'top':
941
+ return 1;
942
+ case 'right':
943
+ return 2;
944
+ case 'bottom':
945
+ return 3;
946
+ }
947
+ };
948
+ const projectPortPlacement = (node, clientX, clientY, preferredSide) => {
949
+ if (!node || typeof node.size !== 'function')
950
+ return undefined;
951
+ const size = node.size();
952
+ const position = typeof node.getPosition === 'function'
953
+ ? node.getPosition()
954
+ : { x: Number(node?.position?.x ?? 0), y: Number(node?.position?.y ?? 0) };
955
+ const localPoint = typeof graphView.clientToGraph === 'function'
956
+ ? graphView.clientToGraph(clientX, clientY)
957
+ : { x: clientX, y: clientY };
958
+ const localX = clamp(localPoint.x - position.x, 0, size.width);
959
+ const localY = clamp(localPoint.y - position.y, 0, size.height);
960
+ const candidates = [
961
+ { side: 'left', distance: localX },
962
+ { side: 'right', distance: size.width - localX },
963
+ { side: 'top', distance: localY },
964
+ { side: 'bottom', distance: size.height - localY },
965
+ ];
966
+ candidates.sort((left, right) => left.distance - right.distance || sidePriority(left.side) - sidePriority(right.side));
967
+ const closest = candidates[0];
968
+ const preferredCandidate = preferredSide
969
+ ? candidates.find((candidate) => candidate.side === preferredSide)
970
+ : undefined;
971
+ const hysteresis = 8;
972
+ const side = preferredCandidate && preferredSide && (preferredCandidate.distance - closest.distance) <= hysteresis
973
+ ? preferredSide
974
+ : closest.side;
975
+ const ratio = side === 'left' || side === 'right'
976
+ ? (size.height > 0 ? localY / size.height : 0.5)
977
+ : (size.width > 0 ? localX / size.width : 0.5);
978
+ return {
979
+ side,
980
+ ratio: Math.max(0.05, Math.min(0.95, ratio)),
981
+ };
982
+ };
810
983
  const onPointerMove = (event) => {
811
- if (!activePortDrag)
984
+ if (!activePortDrag || event.pointerId !== activePortDrag.pointerId)
812
985
  return;
813
986
  const node = graphView.getCellById(activePortDrag.nodeId);
814
987
  if (!node || typeof node.size !== 'function' || typeof node.getPort !== 'function' || typeof node.setPortProp !== 'function')
815
988
  return;
989
+ const placement = projectPortPlacement(node, event.clientX, event.clientY, activePortDrag.side);
990
+ if (!placement)
991
+ return;
992
+ if (placement.side === activePortDrag.side && Math.abs(placement.ratio - activePortDrag.ratio) < 0.005) {
993
+ return;
994
+ }
816
995
  const size = node.size();
817
- const deltaY = event.clientY - activePortDrag.startClientY;
818
- const nextY = clamp(activePortDrag.startPortY + deltaY, 4, Math.max(4, size.height - 4));
996
+ const nextX = placement.side === 'left'
997
+ ? 0
998
+ : placement.side === 'right'
999
+ ? size.width
1000
+ : placement.ratio * size.width;
1001
+ const nextY = placement.side === 'top'
1002
+ ? 0
1003
+ : placement.side === 'bottom'
1004
+ ? size.height
1005
+ : placement.ratio * size.height;
1006
+ node.setPortProp(activePortDrag.portId, 'args/x', nextX);
819
1007
  node.setPortProp(activePortDrag.portId, 'args/y', nextY);
1008
+ node.setPortProp(activePortDrag.portId, 'args/side', placement.side);
1009
+ node.setPortProp(activePortDrag.portId, 'args/ratio', placement.ratio);
1010
+ if (placement.side !== activePortDrag.side) {
1011
+ node.setPortProp(activePortDrag.portId, 'label/position', resolveBorderPortLabelPosition(placement.side));
1012
+ }
1013
+ activePortDrag.side = placement.side;
1014
+ activePortDrag.ratio = placement.ratio;
1015
+ portPlacementById.set(activePortDrag.portId, placement);
820
1016
  };
821
- const onPointerUp = () => {
1017
+ const onPointerUp = (event) => {
1018
+ if (!activePortDrag || event.pointerId !== activePortDrag.pointerId)
1019
+ return;
1020
+ if (typeof activePortDrag.container.releasePointerCapture === 'function') {
1021
+ try {
1022
+ activePortDrag.container.releasePointerCapture(activePortDrag.pointerId);
1023
+ }
1024
+ catch {
1025
+ // Ignore capture release failures.
1026
+ }
1027
+ }
822
1028
  activePortDrag = undefined;
823
1029
  };
824
1030
  window.addEventListener('pointermove', onPointerMove);
825
1031
  window.addEventListener('pointerup', onPointerUp);
826
- const startPortDrag = (clientY, node, portId) => {
1032
+ const startPortDrag = (clientX, clientY, node, portId, pointerId, container) => {
827
1033
  if (!node || typeof node.getPort !== 'function')
828
1034
  return;
829
- const existing = node.getPort(portId);
830
- const startPortY = typeof existing?.args?.y === 'number' ? existing.args.y : (node.size().height / 2);
1035
+ const placement = projectPortPlacement(node, clientX, clientY, portPlacementById.get(portId)?.side ?? 'right')
1036
+ ?? portPlacementById.get(portId)
1037
+ ?? { side: 'right', ratio: 0.5 };
831
1038
  activePortDrag = {
832
1039
  nodeId: String(node.id),
833
1040
  portId: String(portId),
834
- startClientY: clientY,
835
- startPortY,
1041
+ pointerId,
1042
+ side: placement.side,
1043
+ ratio: placement.ratio,
1044
+ container,
836
1045
  };
837
1046
  };
838
- const onCanvasPointerDown = (event) => {
839
- if (!isPortPointerTarget(event))
840
- return;
841
- const portId = findPortIdFromTarget(event.target);
842
- if (!portId)
843
- return;
844
- const ownerId = ownerByPortId.get(portId);
845
- if (!ownerId)
846
- return;
847
- const node = graphView.getCellById(ownerId);
848
- startPortDrag(event.clientY, node, portId);
849
- event.preventDefault();
850
- event.stopPropagation();
851
- if (typeof event.stopImmediatePropagation === 'function')
852
- event.stopImmediatePropagation();
853
- };
854
- liveCanvas.addEventListener('pointerdown', onCanvasPointerDown, true);
855
1047
  liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
856
1048
  window.removeEventListener('pointermove', onPointerMove);
857
1049
  window.removeEventListener('pointerup', onPointerUp);
858
- liveCanvas.removeEventListener('pointerdown', onCanvasPointerDown, true);
859
1050
  }, { once: true });
860
1051
  const resultContainer = liveCanvas.closest('.oml-md-result');
861
1052
  const resizeObserver = new ResizeObserver(() => {
@@ -1238,7 +1429,7 @@ async function loadDagreLib() {
1238
1429
  dagreLib = mod.default ?? mod;
1239
1430
  return dagreLib;
1240
1431
  }
1241
- async function layoutGraphDagre(graph, options) {
1432
+ async function layoutGraphDagre(graph, options, rootSpacing) {
1242
1433
  const dagre = await loadDagreLib();
1243
1434
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
1244
1435
  const layoutNodes = graph.nodes.filter((node) => node.kind !== 'Port');
@@ -1275,8 +1466,6 @@ async function layoutGraphDagre(graph, options) {
1275
1466
  stack: {
1276
1467
  direction: styleLayout.direction === 'horizontal' ? 'horizontal' : 'vertical',
1277
1468
  gap: clampLayoutNumber(styleLayout.gap, 0, 400, 0),
1278
- marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, 0),
1279
- marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, 0),
1280
1469
  stretch: typeof styleLayout.stretch === 'boolean' ? styleLayout.stretch : true,
1281
1470
  },
1282
1471
  };
@@ -1296,33 +1485,43 @@ async function layoutGraphDagre(graph, options) {
1296
1485
  rankdir,
1297
1486
  nodesep: clampLayoutNumber(styleLayout.nodesep, 0, 400, options.nodesep),
1298
1487
  ranksep: clampLayoutNumber(styleLayout.ranksep, 0, 500, options.ranksep),
1299
- marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, options.marginx),
1300
- marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, options.marginy),
1301
1488
  },
1302
1489
  };
1303
1490
  };
1304
1491
  const childPositionById = new Map();
1492
+ const branchChildForParent = (parentId, nodeId) => {
1493
+ let cursorId = nodeId;
1494
+ while (cursorId) {
1495
+ const node = nodeById.get(cursorId);
1496
+ if (!node) {
1497
+ return undefined;
1498
+ }
1499
+ if (node.parentId === parentId) {
1500
+ return node.id;
1501
+ }
1502
+ cursorId = node.parentId;
1503
+ }
1504
+ return undefined;
1505
+ };
1305
1506
  const edgesBetweenChildren = (parentId, childSet) => {
1306
1507
  const seen = new Set();
1307
1508
  const links = [];
1308
1509
  for (const edge of graph.edges) {
1309
1510
  const sourceId = endpointOwner(edge.sourceId);
1310
1511
  const targetId = endpointOwner(edge.targetId);
1311
- if (!sourceId || !targetId || sourceId === targetId)
1512
+ if (!sourceId || !targetId)
1312
1513
  continue;
1313
- if (!childSet.has(sourceId) || !childSet.has(targetId))
1514
+ const sourceBranchId = branchChildForParent(parentId, sourceId);
1515
+ const targetBranchId = branchChildForParent(parentId, targetId);
1516
+ if (!sourceBranchId || !targetBranchId || sourceBranchId === targetBranchId)
1314
1517
  continue;
1315
- const sourceNode = nodeById.get(sourceId);
1316
- const targetNode = nodeById.get(targetId);
1317
- if (!sourceNode || !targetNode)
1518
+ if (!childSet.has(sourceBranchId) || !childSet.has(targetBranchId))
1318
1519
  continue;
1319
- if (sourceNode.parentId !== parentId || targetNode.parentId !== parentId)
1320
- continue;
1321
- const key = `${sourceId}=>${targetId}`;
1520
+ const key = `${sourceBranchId}=>${targetBranchId}`;
1322
1521
  if (seen.has(key))
1323
1522
  continue;
1324
1523
  seen.add(key);
1325
- links.push({ source: sourceId, target: targetId });
1524
+ links.push({ source: sourceBranchId, target: targetBranchId });
1326
1525
  }
1327
1526
  return links;
1328
1527
  };
@@ -1335,8 +1534,8 @@ async function layoutGraphDagre(graph, options) {
1335
1534
  return { width: 0, height: 0 };
1336
1535
  }
1337
1536
  const parent = parentId ? nodeById.get(parentId) : undefined;
1338
- const topPadding = parent?.contentTopPadding ?? 0;
1339
1537
  const localLayout = layoutOptionsForParent(parentId);
1538
+ const spacing = parent ? resolveBoxSpacing(parent.style, 0) : rootSpacing;
1340
1539
  let totalWidth = 0;
1341
1540
  let totalHeight = 0;
1342
1541
  if (localLayout.type === 'stack') {
@@ -1344,13 +1543,13 @@ async function layoutGraphDagre(graph, options) {
1344
1543
  const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
1345
1544
  const maxChildWidth = childNodes.reduce((max, child) => Math.max(max, child.width), 0);
1346
1545
  const maxChildHeight = childNodes.reduce((max, child) => Math.max(max, child.height), 0);
1347
- const parentContentWidth = Math.max(0, (parent?.width ?? 0) - (local.marginx * 2));
1348
- const parentContentHeight = Math.max(0, (parent?.height ?? 0) - topPadding - (local.marginy * 2));
1546
+ const parentContentWidth = Math.max(0, (parent?.width ?? 0) - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
1547
+ const parentContentHeight = Math.max(0, (parent?.height ?? 0) - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
1349
1548
  const availableWidth = Math.max(maxChildWidth, parentContentWidth);
1350
1549
  const availableHeight = Math.max(maxChildHeight, parentContentHeight);
1351
1550
  const isSingleChild = childNodes.length === 1;
1352
- let cursorX = local.marginx;
1353
- let cursorY = topPadding + local.marginy;
1551
+ let cursorX = spacing.marginLeft + spacing.paddingLeft;
1552
+ let cursorY = spacing.marginTop + spacing.paddingTop;
1354
1553
  for (const child of childNodes) {
1355
1554
  if (local.stretch && isSingleChild) {
1356
1555
  child.width = Math.max(0, availableWidth);
@@ -1373,14 +1572,14 @@ async function layoutGraphDagre(graph, options) {
1373
1572
  if (local.direction === 'vertical') {
1374
1573
  const contentHeight = Math.max(0, childNodes.reduce((sum, child) => sum + child.height, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
1375
1574
  const contentWidth = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.width), 0));
1376
- totalWidth = (local.marginx * 2) + contentWidth;
1377
- totalHeight = topPadding + (local.marginy * 2) + contentHeight;
1575
+ totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
1576
+ totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
1378
1577
  }
1379
1578
  else {
1380
1579
  const contentWidth = Math.max(0, childNodes.reduce((sum, child) => sum + child.width, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
1381
1580
  const contentHeight = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.height), 0));
1382
- totalWidth = (local.marginx * 2) + contentWidth;
1383
- totalHeight = topPadding + (local.marginy * 2) + contentHeight;
1581
+ totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
1582
+ totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
1384
1583
  }
1385
1584
  }
1386
1585
  else {
@@ -1438,22 +1637,18 @@ async function layoutGraphDagre(graph, options) {
1438
1637
  const left = laid.x - (width / 2);
1439
1638
  const top = laid.y - (height / 2);
1440
1639
  childPositionById.set(childId, {
1441
- x: localOptions.marginx + (left - minX),
1442
- y: topPadding + localOptions.marginy + (top - minY),
1640
+ x: spacing.marginLeft + spacing.paddingLeft + (left - minX),
1641
+ y: spacing.marginTop + spacing.paddingTop + (top - minY),
1443
1642
  });
1444
1643
  }
1445
1644
  const contentWidth = Math.max(0, maxX - minX);
1446
1645
  const contentHeight = Math.max(0, maxY - minY);
1447
- totalWidth = (localOptions.marginx * 2) + contentWidth;
1448
- totalHeight = topPadding + (localOptions.marginy * 2) + contentHeight;
1646
+ totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
1647
+ totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
1449
1648
  }
1450
1649
  if (parent) {
1451
- if (!parent.hasExplicitWidth) {
1452
- parent.width = Math.max(parent.width, totalWidth);
1453
- }
1454
- if (!parent.hasExplicitHeight) {
1455
- parent.height = Math.max(parent.height, totalHeight);
1456
- }
1650
+ parent.width = Math.max(parent.width, totalWidth);
1651
+ parent.height = Math.max(parent.height, totalHeight);
1457
1652
  }
1458
1653
  return { width: totalWidth, height: totalHeight };
1459
1654
  };
@@ -1471,9 +1666,9 @@ async function layoutGraphDagre(graph, options) {
1471
1666
  if (localLayout.type === 'stack') {
1472
1667
  const local = localLayout.stack;
1473
1668
  const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
1474
- const topPadding = parent.contentTopPadding;
1475
- const contentWidth = Math.max(0, parent.width - (local.marginx * 2));
1476
- const contentHeight = Math.max(0, parent.height - topPadding - (local.marginy * 2));
1669
+ const spacing = resolveBoxSpacing(parent.style, 0);
1670
+ const contentWidth = Math.max(0, parent.width - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
1671
+ const contentHeight = Math.max(0, parent.height - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
1477
1672
  const isSingleChild = childNodes.length === 1;
1478
1673
  if (local.stretch) {
1479
1674
  for (const child of childNodes) {
@@ -1490,8 +1685,8 @@ async function layoutGraphDagre(graph, options) {
1490
1685
  }
1491
1686
  }
1492
1687
  }
1493
- let cursorX = local.marginx;
1494
- let cursorY = topPadding + local.marginy;
1688
+ let cursorX = spacing.marginLeft + spacing.paddingLeft;
1689
+ let cursorY = spacing.marginTop + spacing.paddingTop;
1495
1690
  for (const child of childNodes) {
1496
1691
  childPositionById.set(child.id, { x: cursorX, y: cursorY });
1497
1692
  if (local.direction === 'vertical') {
@@ -1692,9 +1887,6 @@ function compileDiagramGraph(index, stylesheet) {
1692
1887
  children: [],
1693
1888
  width: 160,
1694
1889
  height: 70,
1695
- hasExplicitWidth: false,
1696
- hasExplicitHeight: false,
1697
- contentTopPadding: 0,
1698
1890
  });
1699
1891
  }
1700
1892
  const validParent = (child, parent) => {
@@ -1777,9 +1969,6 @@ function compileDiagramGraph(index, stylesheet) {
1777
1969
  const estimated = estimateSize(node.kind, baseLabel, node.labels.length, node.style);
1778
1970
  node.width = estimated.width;
1779
1971
  node.height = estimated.height;
1780
- node.hasExplicitWidth = estimated.hasExplicitWidth;
1781
- node.hasExplicitHeight = estimated.hasExplicitHeight;
1782
- node.contentTopPadding = resolveContainerTopPadding(node, baseLabel);
1783
1972
  }
1784
1973
  for (const edge of edges) {
1785
1974
  edge.style = styleFor('edge', edge.id, edge.classes, edge.properties);
@@ -1807,65 +1996,42 @@ function compileDiagramGraph(index, stylesheet) {
1807
1996
  function estimateSize(kind, label, labelCount, style) {
1808
1997
  const styledWidth = toPositiveNumber(style.width);
1809
1998
  const styledHeight = toPositiveNumber(style.height);
1810
- if (styledWidth && styledHeight) {
1811
- return { width: styledWidth, height: styledHeight, hasExplicitWidth: true, hasExplicitHeight: true };
1812
- }
1999
+ const attrs = extractStyleAttrs(style);
2000
+ const labelAttrs = asRecord(attrs.label);
2001
+ const labelDisplay = typeof labelAttrs?.display === 'string' ? labelAttrs.display.trim().toLowerCase() : '';
2002
+ const labelOpacity = toNonNegativeNumber(labelAttrs?.opacity);
2003
+ const labelVisible = labelDisplay !== 'none' && (labelOpacity === undefined || labelOpacity > 0);
2004
+ const effectiveLabel = labelVisible ? label : '';
2005
+ const effectiveLabelCount = labelVisible ? labelCount : 0;
1813
2006
  if (kind === 'Port') {
1814
2007
  return {
1815
- width: styledWidth ?? 14,
1816
- height: styledHeight ?? 14,
1817
- hasExplicitWidth: styledWidth !== undefined,
1818
- hasExplicitHeight: styledHeight !== undefined,
2008
+ width: Math.max(14, styledWidth ?? 0),
2009
+ height: Math.max(14, styledHeight ?? 0),
1819
2010
  };
1820
2011
  }
1821
- const textWidth = Math.max(24, label.length * 7);
1822
- const baseWidth = Math.max(72, Math.min(280, textWidth + 26));
2012
+ const textWidth = effectiveLabel.length > 0 ? Math.max(24, effectiveLabel.length * 7) : 0;
2013
+ const baseWidth = effectiveLabel.length > 0 ? Math.max(72, Math.min(280, textWidth + 26)) : 0;
1823
2014
  if (kind === 'Compartment') {
1824
- const size = { width: baseWidth, height: Math.max(44, 24 + labelCount * 16) };
2015
+ const size = {
2016
+ width: baseWidth,
2017
+ height: effectiveLabelCount > 0 ? Math.max(44, 24 + effectiveLabelCount * 16) : 0,
2018
+ };
1825
2019
  return {
1826
- width: styledWidth ?? size.width,
1827
- height: styledHeight ?? size.height,
1828
- hasExplicitWidth: styledWidth !== undefined,
1829
- hasExplicitHeight: styledHeight !== undefined,
2020
+ width: Math.max(size.width, styledWidth ?? 0),
2021
+ height: Math.max(size.height, styledHeight ?? 0),
1830
2022
  };
1831
2023
  }
1832
- const size = { width: baseWidth, height: Math.max(36, 20 + labelCount * 16) };
2024
+ const size = {
2025
+ width: baseWidth,
2026
+ height: effectiveLabelCount > 0 ? Math.max(36, 20 + effectiveLabelCount * 16) : 0,
2027
+ };
1833
2028
  return {
1834
- width: styledWidth ?? size.width,
1835
- height: styledHeight ?? size.height,
1836
- hasExplicitWidth: styledWidth !== undefined,
1837
- hasExplicitHeight: styledHeight !== undefined,
2029
+ width: Math.max(size.width, styledWidth ?? 0),
2030
+ height: Math.max(size.height, styledHeight ?? 0),
1838
2031
  };
1839
2032
  }
1840
- function resolveContainerTopPadding(node, labelText) {
1841
- if (node.children.length === 0) {
1842
- return 0;
1843
- }
1844
- const explicitPadding = toNonNegativeNumber(node.style.paddingTop);
1845
- if (explicitPadding !== undefined) {
1846
- return Math.ceil(explicitPadding);
1847
- }
1848
- const attrs = extractStyleAttrs(node.style);
1849
- const label = asRecord(attrs.label);
1850
- if (label?.display === 'none') {
1851
- return 0;
1852
- }
1853
- const labelOpacity = toNonNegativeNumber(label?.opacity);
1854
- if (labelOpacity !== undefined && labelOpacity <= 0) {
1855
- return 0;
1856
- }
1857
- const fontSize = toPositiveNumber(label?.fontSize) ?? 12;
1858
- const lineCount = Math.max(1, labelText.split('\n').filter((line) => line.trim().length > 0).length);
1859
- return Math.ceil((fontSize * 1.2) * lineCount);
1860
- }
1861
2033
  function localName(value) {
1862
- const hash = value.lastIndexOf('#');
1863
- if (hash >= 0 && hash < value.length - 1)
1864
- return value.slice(hash + 1);
1865
- const slash = value.lastIndexOf('/');
1866
- if (slash >= 0 && slash < value.length - 1)
1867
- return value.slice(slash + 1);
1868
- return value;
2034
+ return displayLabelFromIri(value);
1869
2035
  }
1870
2036
  function indexTriples(rows) {
1871
2037
  const bySubject = new Map();
@@ -1913,9 +2079,7 @@ function resolveDagreLayoutOptions(options) {
1913
2079
  : 'LR';
1914
2080
  const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
1915
2081
  const ranksep = clampLayoutNumber(layout.ranksep, 0, 500, 64);
1916
- const marginx = clampLayoutNumber(layout.marginx, 0, 200, 16);
1917
- const marginy = clampLayoutNumber(layout.marginy, 0, 200, 16);
1918
- return { rankdir, nodesep, ranksep, marginx, marginy };
2082
+ return { rankdir, nodesep, ranksep };
1919
2083
  }
1920
2084
  function clampLayoutNumber(value, min, max, fallback) {
1921
2085
  if (typeof value !== 'number' || !Number.isFinite(value)) {
@@ -1923,6 +2087,121 @@ function clampLayoutNumber(value, min, max, fallback) {
1923
2087
  }
1924
2088
  return Math.max(min, Math.min(max, Math.round(value)));
1925
2089
  }
2090
+ function resolveRootSpacing(options) {
2091
+ return readBoxSpacing(options, {
2092
+ marginTop: 16,
2093
+ marginBottom: 16,
2094
+ marginLeft: 16,
2095
+ marginRight: 16,
2096
+ paddingTop: 0,
2097
+ paddingBottom: 0,
2098
+ paddingLeft: 0,
2099
+ paddingRight: 0,
2100
+ });
2101
+ }
2102
+ function resolveBoxSpacing(style, fallbackMargin) {
2103
+ return readBoxSpacing(style, {
2104
+ marginTop: fallbackMargin,
2105
+ marginBottom: fallbackMargin,
2106
+ marginLeft: fallbackMargin,
2107
+ marginRight: fallbackMargin,
2108
+ paddingTop: 0,
2109
+ paddingBottom: 0,
2110
+ paddingLeft: 0,
2111
+ paddingRight: 0,
2112
+ });
2113
+ }
2114
+ function readBoxSpacing(style, defaults) {
2115
+ const source = style ?? {};
2116
+ return {
2117
+ ...readCssBoxSpacing(source.margin, 'margin', defaults),
2118
+ ...readCssBoxSpacing(source.padding, 'padding', defaults),
2119
+ };
2120
+ }
2121
+ function readCssBoxSpacing(value, kind, defaults) {
2122
+ const sides = parseCssBoxShorthand(value);
2123
+ const isMargin = kind === 'margin';
2124
+ if (isMargin) {
2125
+ return {
2126
+ marginTop: sides?.top ?? defaults.marginTop,
2127
+ marginRight: sides?.right ?? defaults.marginRight,
2128
+ marginBottom: sides?.bottom ?? defaults.marginBottom,
2129
+ marginLeft: sides?.left ?? defaults.marginLeft,
2130
+ paddingTop: defaults.paddingTop,
2131
+ paddingRight: defaults.paddingRight,
2132
+ paddingBottom: defaults.paddingBottom,
2133
+ paddingLeft: defaults.paddingLeft,
2134
+ };
2135
+ }
2136
+ return {
2137
+ marginTop: defaults.marginTop,
2138
+ marginRight: defaults.marginRight,
2139
+ marginBottom: defaults.marginBottom,
2140
+ marginLeft: defaults.marginLeft,
2141
+ paddingTop: sides?.top ?? defaults.paddingTop,
2142
+ paddingRight: sides?.right ?? defaults.paddingRight,
2143
+ paddingBottom: sides?.bottom ?? defaults.paddingBottom,
2144
+ paddingLeft: sides?.left ?? defaults.paddingLeft,
2145
+ };
2146
+ }
2147
+ function parseCssBoxShorthand(value) {
2148
+ const values = Array.isArray(value)
2149
+ ? value
2150
+ : typeof value === 'string'
2151
+ ? (value.includes(',') ? undefined : value.trim().split(/\s+/).filter((token) => token.length > 0))
2152
+ : typeof value === 'number'
2153
+ ? [value]
2154
+ : undefined;
2155
+ if (!values || values.length === 0 || values.length > 4) {
2156
+ return undefined;
2157
+ }
2158
+ const parsed = values.map((entry) => {
2159
+ if (typeof entry === 'number' && Number.isFinite(entry)) {
2160
+ return entry;
2161
+ }
2162
+ if (typeof entry === 'string') {
2163
+ const next = Number.parseFloat(entry);
2164
+ if (Number.isFinite(next)) {
2165
+ return next;
2166
+ }
2167
+ }
2168
+ return undefined;
2169
+ });
2170
+ if (parsed.some((entry) => entry === undefined)) {
2171
+ return undefined;
2172
+ }
2173
+ const clampCssBoxValue = (entry) => {
2174
+ if (entry === undefined)
2175
+ return undefined;
2176
+ return Math.max(0, Math.min(200, Math.round(entry)));
2177
+ };
2178
+ if (parsed.length === 1) {
2179
+ const single = clampCssBoxValue(parsed[0]);
2180
+ return single === undefined ? undefined : { top: single, right: single, bottom: single, left: single };
2181
+ }
2182
+ if (parsed.length === 2) {
2183
+ const vertical = clampCssBoxValue(parsed[0]);
2184
+ const horizontal = clampCssBoxValue(parsed[1]);
2185
+ if (vertical === undefined || horizontal === undefined)
2186
+ return undefined;
2187
+ return { top: vertical, right: horizontal, bottom: vertical, left: horizontal };
2188
+ }
2189
+ if (parsed.length === 3) {
2190
+ const top = clampCssBoxValue(parsed[0]);
2191
+ const horizontal = clampCssBoxValue(parsed[1]);
2192
+ const bottom = clampCssBoxValue(parsed[2]);
2193
+ if (top === undefined || horizontal === undefined || bottom === undefined)
2194
+ return undefined;
2195
+ return { top, right: horizontal, bottom, left: horizontal };
2196
+ }
2197
+ const top = clampCssBoxValue(parsed[0]);
2198
+ const right = clampCssBoxValue(parsed[1]);
2199
+ const bottom = clampCssBoxValue(parsed[2]);
2200
+ const left = clampCssBoxValue(parsed[3]);
2201
+ if (top === undefined || right === undefined || bottom === undefined || left === undefined)
2202
+ return undefined;
2203
+ return { top, right, bottom, left };
2204
+ }
1926
2205
  function parseDiagramStylesheet(options) {
1927
2206
  const stylesheet = options?.stylesheet;
1928
2207
  if (!Array.isArray(stylesheet)) {
@@ -1949,7 +2228,7 @@ function parseDiagramStylesheet(options) {
1949
2228
  return rules;
1950
2229
  }
1951
2230
  function parseStyleSelector(selector) {
1952
- const match = /^\s*(node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
2231
+ const match = /^\s*(diagram|node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
1953
2232
  if (!match)
1954
2233
  return undefined;
1955
2234
  return {
@@ -1962,7 +2241,13 @@ function parseStyleSelector(selector) {
1962
2241
  // These pass through as top-level keys so callers can read style.layout, style.width, etc.
1963
2242
  const OML_PASSTHROUGH_STYLE_KEYS = new Set([
1964
2243
  'layout', 'shape', 'width', 'height',
2244
+ 'margin', 'padding', 'router', 'connector',
2245
+ ]);
2246
+ const LEGACY_BOX_SPACING_KEYS = new Set([
2247
+ 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
1965
2248
  'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
2249
+ 'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
2250
+ 'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
1966
2251
  ]);
1967
2252
  // Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
1968
2253
  // 'label' and 'icon' cover nodes, compartments, ports, and edges.
@@ -1984,6 +2269,9 @@ function normalizeDiagramStyle(elementKind, raw) {
1984
2269
  const key = rawKey.trim();
1985
2270
  if (!key || value === undefined || value === null)
1986
2271
  continue;
2272
+ if (LEGACY_BOX_SPACING_KEYS.has(key)) {
2273
+ continue;
2274
+ }
1987
2275
  // OML node-level keys: pass through to the top level of the normalized style object
1988
2276
  if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) {
1989
2277
  passthrough[key] = value;
@@ -2260,6 +2548,17 @@ function resolveEdgeLineAttrs(style) {
2260
2548
  ...(line ?? {}),
2261
2549
  };
2262
2550
  }
2551
+ function resolveEdgeRouter(style) {
2552
+ const router = asRecord(style.router);
2553
+ return router ?? { name: 'normal' };
2554
+ }
2555
+ function resolveEdgeConnector(style) {
2556
+ const connector = asRecord(style.connector);
2557
+ return connector ?? {
2558
+ name: 'jumpover',
2559
+ args: { size: 5 },
2560
+ };
2561
+ }
2263
2562
  function resolveEdgeLabelAttrs(style, placement, text) {
2264
2563
  const attrs = extractStyleAttrs(style);
2265
2564
  const base = asRecord(attrs.label);
@@ -2282,17 +2581,41 @@ function resolveEdgeLabelBodyAttrs(style, placement) {
2282
2581
  fillOpacity: 0.9,
2283
2582
  stroke: 'none',
2284
2583
  strokeWidth: 0,
2584
+ pointerEvents: 'all',
2285
2585
  ...(base ?? {}),
2286
2586
  ...(specific ?? {}),
2287
2587
  };
2288
2588
  }
2289
- function resolvePortAttrs(style, classes, text) {
2589
+ function resolvePortAttrs(style, classes, text, side = 'right', ownerStroke) {
2290
2590
  const attrs = extractStyleAttrs(style);
2291
2591
  const body = asRecord(attrs.body);
2292
2592
  const icon = asRecord(attrs.icon);
2293
2593
  const label = asRecord(attrs.label);
2294
2594
  const imageUrl = extractImageHrefFromIcon(icon);
2295
- return {
2595
+ const labelPosition = side === 'left'
2596
+ ? {
2597
+ textAnchor: 'end',
2598
+ x: -10,
2599
+ dy: '0.9em',
2600
+ }
2601
+ : side === 'top'
2602
+ ? {
2603
+ textAnchor: 'middle',
2604
+ x: 0,
2605
+ dy: '-0.3em',
2606
+ }
2607
+ : side === 'bottom'
2608
+ ? {
2609
+ textAnchor: 'middle',
2610
+ x: 0,
2611
+ dy: '1.4em',
2612
+ }
2613
+ : {
2614
+ textAnchor: 'start',
2615
+ x: 10,
2616
+ dy: '0.9em',
2617
+ };
2618
+ const result = {
2296
2619
  body: {
2297
2620
  width: 12,
2298
2621
  height: 12,
@@ -2300,7 +2623,7 @@ function resolvePortAttrs(style, classes, text) {
2300
2623
  y: -6,
2301
2624
  class: ['oml-port-body', ...classes].join(' '),
2302
2625
  magnet: false,
2303
- stroke: CSS_FOCUS_BORDER,
2626
+ stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
2304
2627
  strokeWidth: 1,
2305
2628
  fill: CSS_EDITOR_BACKGROUND,
2306
2629
  ...(body ?? {}),
@@ -2315,17 +2638,180 @@ function resolvePortAttrs(style, classes, text) {
2315
2638
  ...(icon ?? {}),
2316
2639
  ...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
2317
2640
  },
2318
- label: {
2641
+ };
2642
+ if (text) {
2643
+ result.label = {
2319
2644
  text,
2320
2645
  fill: CSS_EDITOR_FOREGROUND,
2321
2646
  fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
2322
2647
  fontSize: 12,
2323
- textAnchor: 'start',
2324
- x: 10,
2325
- dy: '0.9em',
2648
+ ...labelPosition,
2326
2649
  ...(label ?? {}),
2327
- },
2650
+ };
2651
+ }
2652
+ return result;
2653
+ }
2654
+ function resolveBorderPortLabelPosition(side) {
2655
+ return { name: side };
2656
+ }
2657
+ function clamp(value, min, max) {
2658
+ return Math.max(min, Math.min(max, value));
2659
+ }
2660
+ function computeDefaultPortPlacements(graph, nodeById, portsByOwner, boxes) {
2661
+ const placements = new Map();
2662
+ const ownerCenter = (nodeId) => {
2663
+ const box = boxes.get(nodeId);
2664
+ if (!box) {
2665
+ return undefined;
2666
+ }
2667
+ return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
2328
2668
  };
2669
+ const portIdsByOwner = new Map();
2670
+ for (const [ownerId, ports] of portsByOwner.entries()) {
2671
+ portIdsByOwner.set(ownerId, new Set(ports.map((port) => port.id)));
2672
+ }
2673
+ const peerPortIdsByPort = new Map();
2674
+ const peerCentersByPort = new Map();
2675
+ for (const edge of graph.edges) {
2676
+ const source = nodeById.get(edge.sourceId);
2677
+ const target = nodeById.get(edge.targetId);
2678
+ const sourceOwnerId = source?.kind === 'Port' ? source.parentId : source?.id;
2679
+ const targetOwnerId = target?.kind === 'Port' ? target.parentId : target?.id;
2680
+ if (!sourceOwnerId || !targetOwnerId || sourceOwnerId === targetOwnerId) {
2681
+ continue;
2682
+ }
2683
+ const sourcePeer = ownerCenter(targetOwnerId);
2684
+ const targetPeer = ownerCenter(sourceOwnerId);
2685
+ if (source?.kind === 'Port' && sourcePeer) {
2686
+ const peers = peerCentersByPort.get(source.id) ?? [];
2687
+ peers.push(sourcePeer);
2688
+ peerCentersByPort.set(source.id, peers);
2689
+ if (target?.kind === 'Port') {
2690
+ const peerPortIds = peerPortIdsByPort.get(source.id) ?? [];
2691
+ peerPortIds.push(target.id);
2692
+ peerPortIdsByPort.set(source.id, peerPortIds);
2693
+ }
2694
+ }
2695
+ if (target?.kind === 'Port' && targetPeer) {
2696
+ const peers = peerCentersByPort.get(target.id) ?? [];
2697
+ peers.push(targetPeer);
2698
+ peerCentersByPort.set(target.id, peers);
2699
+ if (source?.kind === 'Port') {
2700
+ const peerPortIds = peerPortIdsByPort.get(target.id) ?? [];
2701
+ peerPortIds.push(source.id);
2702
+ peerPortIdsByPort.set(target.id, peerPortIds);
2703
+ }
2704
+ }
2705
+ }
2706
+ const anchorFor = (ownerId, side, ratio) => {
2707
+ const box = boxes.get(ownerId);
2708
+ if (!box) {
2709
+ return undefined;
2710
+ }
2711
+ const clampedRatio = clamp(ratio, 0.05, 0.95);
2712
+ if (side === 'left') {
2713
+ return { x: box.x, y: box.y + (box.height * clampedRatio) };
2714
+ }
2715
+ if (side === 'right') {
2716
+ return { x: box.x + box.width, y: box.y + (box.height * clampedRatio) };
2717
+ }
2718
+ if (side === 'top') {
2719
+ return { x: box.x + (box.width * clampedRatio), y: box.y };
2720
+ }
2721
+ return { x: box.x + (box.width * clampedRatio), y: box.y + box.height };
2722
+ };
2723
+ const currentAnchorForPort = (portId) => {
2724
+ const port = nodeById.get(portId);
2725
+ if (!port?.parentId) {
2726
+ return undefined;
2727
+ }
2728
+ const placement = placements.get(portId) ?? { side: 'right', ratio: 0.5 };
2729
+ return anchorFor(port.parentId, placement.side, placement.ratio);
2730
+ };
2731
+ const peerAnchorsForPort = (portId) => {
2732
+ const peerAnchors = [];
2733
+ for (const peerPortId of peerPortIdsByPort.get(portId) ?? []) {
2734
+ const anchor = currentAnchorForPort(peerPortId);
2735
+ if (anchor) {
2736
+ peerAnchors.push(anchor);
2737
+ }
2738
+ }
2739
+ for (const peerCenter of peerCentersByPort.get(portId) ?? []) {
2740
+ peerAnchors.push(peerCenter);
2741
+ }
2742
+ return peerAnchors;
2743
+ };
2744
+ const candidateCost = (ownerId, side, portId) => {
2745
+ const owner = ownerCenter(ownerId);
2746
+ const candidate = anchorFor(ownerId, side, 0.5);
2747
+ if (!owner || !candidate) {
2748
+ return Number.POSITIVE_INFINITY;
2749
+ }
2750
+ const peerAnchors = peerAnchorsForPort(portId);
2751
+ if (peerAnchors.length === 0) {
2752
+ return side === 'right' ? 0 : 1;
2753
+ }
2754
+ let cost = 0;
2755
+ for (const peer of peerAnchors) {
2756
+ cost += Math.abs(candidate.x - peer.x) + Math.abs(candidate.y - peer.y);
2757
+ }
2758
+ return cost;
2759
+ };
2760
+ const sideOrder = ['right', 'left', 'top', 'bottom'];
2761
+ const primaryCoordinate = (side, portId) => {
2762
+ const peerAnchors = peerAnchorsForPort(portId);
2763
+ if (peerAnchors.length === 0) {
2764
+ return 0;
2765
+ }
2766
+ const values = peerAnchors.map((peer) => ((side === 'left' || side === 'right') ? peer.y : peer.x));
2767
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
2768
+ };
2769
+ for (const [ownerId, ports] of portsByOwner.entries()) {
2770
+ const count = ports.length;
2771
+ for (let index = 0; index < count; index += 1) {
2772
+ placements.set(ports[index].id, {
2773
+ side: 'right',
2774
+ ratio: count <= 0 ? 0.5 : ((index + 1) / (count + 1)),
2775
+ });
2776
+ }
2777
+ const ownerPortIds = portIdsByOwner.get(ownerId) ?? new Set();
2778
+ for (let pass = 0; pass < 2; pass += 1) {
2779
+ for (const port of ports) {
2780
+ let bestSide = 'right';
2781
+ let bestCost = Number.POSITIVE_INFINITY;
2782
+ for (const side of sideOrder) {
2783
+ const cost = candidateCost(ownerId, side, port.id);
2784
+ if (cost < bestCost || (cost === bestCost && sideOrder.indexOf(side) < sideOrder.indexOf(bestSide))) {
2785
+ bestCost = cost;
2786
+ bestSide = side;
2787
+ }
2788
+ }
2789
+ const existing = placements.get(port.id) ?? { ratio: 0.5, side: bestSide };
2790
+ placements.set(port.id, { side: bestSide, ratio: existing.ratio });
2791
+ }
2792
+ const sideGroups = new Map([
2793
+ ['left', []],
2794
+ ['right', []],
2795
+ ['top', []],
2796
+ ['bottom', []],
2797
+ ]);
2798
+ for (const port of ports) {
2799
+ sideGroups.get(placements.get(port.id)?.side ?? 'right')?.push(port);
2800
+ }
2801
+ for (const [side, sidePorts] of sideGroups.entries()) {
2802
+ sidePorts.sort((left, right) => (primaryCoordinate(side, left.id) - primaryCoordinate(side, right.id)
2803
+ || left.id.localeCompare(right.id)));
2804
+ for (let index = 0; index < sidePorts.length; index += 1) {
2805
+ placements.set(sidePorts[index].id, {
2806
+ side,
2807
+ ratio: (index + 1) / (sidePorts.length + 1),
2808
+ });
2809
+ }
2810
+ }
2811
+ void ownerPortIds;
2812
+ }
2813
+ }
2814
+ return placements;
2329
2815
  }
2330
2816
  function toPositiveNumber(value) {
2331
2817
  if (typeof value === 'number' && Number.isFinite(value) && value > 0) {