@diagrammo/dgmo 0.26.0 → 0.27.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 (136) hide show
  1. package/README.md +3 -3
  2. package/dist/advanced.cjs +4182 -2704
  3. package/dist/advanced.d.cts +266 -58
  4. package/dist/advanced.d.ts +266 -58
  5. package/dist/advanced.js +4182 -2698
  6. package/dist/auto.cjs +4042 -2581
  7. package/dist/auto.js +124 -122
  8. package/dist/auto.mjs +4042 -2581
  9. package/dist/cli.cjs +172 -170
  10. package/dist/editor.cjs +4 -0
  11. package/dist/editor.js +4 -0
  12. package/dist/highlight.cjs +4 -0
  13. package/dist/highlight.js +4 -0
  14. package/dist/index.cjs +4067 -2583
  15. package/dist/index.d.cts +33 -8
  16. package/dist/index.d.ts +33 -8
  17. package/dist/index.js +4067 -2583
  18. package/dist/internal.cjs +4182 -2704
  19. package/dist/internal.d.cts +266 -58
  20. package/dist/internal.d.ts +266 -58
  21. package/dist/internal.js +4182 -2698
  22. package/dist/map-data/PROVENANCE.json +1 -1
  23. package/dist/map-data/airport-collisions.json +1 -0
  24. package/dist/map-data/airports.json +1 -0
  25. package/docs/language-reference.md +68 -18
  26. package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
  27. package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
  28. package/gallery/fixtures/map-region-values.dgmo +13 -0
  29. package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
  30. package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
  31. package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
  32. package/package.json +1 -1
  33. package/src/advanced.ts +1 -6
  34. package/src/auto/index.ts +1 -1
  35. package/src/boxes-and-lines/layout.ts +146 -26
  36. package/src/boxes-and-lines/parser.ts +43 -8
  37. package/src/boxes-and-lines/renderer.ts +223 -96
  38. package/src/boxes-and-lines/types.ts +9 -2
  39. package/src/c4/layout.ts +14 -32
  40. package/src/c4/parser.ts +9 -5
  41. package/src/c4/renderer.ts +34 -39
  42. package/src/class/layout.ts +118 -18
  43. package/src/class/parser.ts +35 -0
  44. package/src/class/renderer.ts +58 -2
  45. package/src/class/types.ts +3 -0
  46. package/src/cli.ts +4 -4
  47. package/src/completion.ts +26 -12
  48. package/src/cycle/layout.ts +55 -72
  49. package/src/cycle/renderer.ts +11 -6
  50. package/src/d3.ts +78 -117
  51. package/src/diagnostics.ts +16 -0
  52. package/src/echarts.ts +46 -33
  53. package/src/editor/keywords.ts +4 -0
  54. package/src/er/layout.ts +114 -22
  55. package/src/er/parser.ts +28 -0
  56. package/src/er/renderer.ts +55 -2
  57. package/src/er/types.ts +3 -0
  58. package/src/gantt/renderer.ts +46 -38
  59. package/src/gantt/resolver.ts +9 -2
  60. package/src/graph/edge-spline.ts +29 -0
  61. package/src/graph/flowchart-parser.ts +34 -1
  62. package/src/graph/flowchart-renderer.ts +78 -64
  63. package/src/graph/layout.ts +206 -23
  64. package/src/graph/notes.ts +21 -0
  65. package/src/graph/state-parser.ts +26 -1
  66. package/src/graph/state-renderer.ts +78 -64
  67. package/src/graph/types.ts +13 -0
  68. package/src/index.ts +1 -1
  69. package/src/infra/layout.ts +46 -26
  70. package/src/infra/renderer.ts +16 -7
  71. package/src/journey-map/layout.ts +38 -49
  72. package/src/journey-map/renderer.ts +22 -45
  73. package/src/kanban/renderer.ts +15 -6
  74. package/src/label-layout.ts +3 -3
  75. package/src/map/completion.ts +77 -22
  76. package/src/map/context-labels.ts +57 -12
  77. package/src/map/data/PROVENANCE.json +1 -1
  78. package/src/map/data/airport-collisions.json +1 -0
  79. package/src/map/data/airports.json +1 -0
  80. package/src/map/data/types.ts +19 -0
  81. package/src/map/layout.ts +1196 -90
  82. package/src/map/legend-band.ts +2 -2
  83. package/src/map/load-data.ts +10 -1
  84. package/src/map/parser.ts +61 -32
  85. package/src/map/renderer.ts +284 -12
  86. package/src/map/resolved-types.ts +15 -1
  87. package/src/map/resolver.ts +132 -12
  88. package/src/map/types.ts +28 -8
  89. package/src/migrate/embedded.ts +9 -7
  90. package/src/mindmap/text-wrap.ts +13 -14
  91. package/src/org/layout.ts +19 -17
  92. package/src/org/renderer.ts +11 -4
  93. package/src/palettes/color-utils.ts +82 -21
  94. package/src/palettes/index.ts +0 -19
  95. package/src/palettes/registry.ts +1 -1
  96. package/src/palettes/types.ts +2 -2
  97. package/src/pert/layout.ts +48 -40
  98. package/src/pert/renderer.ts +30 -43
  99. package/src/pyramid/renderer.ts +4 -5
  100. package/src/raci/renderer.ts +34 -68
  101. package/src/render.ts +1 -1
  102. package/src/ring/renderer.ts +1 -2
  103. package/src/sequence/parser.ts +100 -22
  104. package/src/sequence/renderer.ts +75 -50
  105. package/src/sitemap/layout.ts +27 -19
  106. package/src/sitemap/renderer.ts +12 -5
  107. package/src/tech-radar/renderer.ts +11 -35
  108. package/src/utils/arrow-markers.ts +51 -0
  109. package/src/utils/fit-canvas.ts +64 -0
  110. package/src/utils/legend-constants.ts +8 -54
  111. package/src/utils/legend-d3.ts +10 -7
  112. package/src/utils/legend-layout.ts +7 -4
  113. package/src/utils/legend-types.ts +10 -4
  114. package/src/utils/note-box/constants.ts +25 -0
  115. package/src/utils/note-box/index.ts +11 -0
  116. package/src/utils/note-box/metrics.ts +90 -0
  117. package/src/utils/note-box/svg.ts +331 -0
  118. package/src/utils/notes/bounds.ts +30 -0
  119. package/src/utils/notes/build.ts +131 -0
  120. package/src/utils/notes/index.ts +18 -0
  121. package/src/utils/notes/model.ts +19 -0
  122. package/src/utils/notes/parse.ts +131 -0
  123. package/src/utils/notes/place.ts +177 -0
  124. package/src/utils/notes/resolve.ts +88 -0
  125. package/src/utils/number-format.ts +36 -0
  126. package/src/utils/parsing.ts +41 -0
  127. package/src/utils/reserved-key-registry.ts +4 -0
  128. package/src/utils/text-measure.ts +122 -0
  129. package/src/wireframe/layout.ts +4 -2
  130. package/src/wireframe/renderer.ts +8 -6
  131. package/src/palettes/dracula.ts +0 -68
  132. package/src/palettes/gruvbox.ts +0 -85
  133. package/src/palettes/monokai.ts +0 -68
  134. package/src/palettes/one-dark.ts +0 -70
  135. package/src/palettes/rose-pine.ts +0 -84
  136. package/src/palettes/solarized.ts +0 -77
@@ -0,0 +1,16 @@
1
+ map Smuggler's Run
2
+
3
+ tag Leg as l
4
+ Sail blue
5
+ March green
6
+ Row orange
7
+
8
+ poi 20.05 -72.82 as tor label: Tortuga
9
+ poi 23.13 -82.38 as hav label: Havana
10
+ poi 18.02 -76.79 as cove label: Pirate's Cove
11
+ poi 25.06 -77.35 as nas label: Nassau
12
+
13
+ tor ~> hav l: Sail
14
+ hav ~> cove l: Sail
15
+ cove -> nas l: March
16
+ nas ~> tor l: Row
@@ -0,0 +1,12 @@
1
+ map Sister Ports of the Spanish Main
2
+ flow-metric Shared cargo runs
3
+
4
+ poi Tortuga
5
+ poi Port Royal
6
+ poi Nassau
7
+ poi Havana
8
+
9
+ Tortuga -ferry- Port Royal value: 18
10
+ Port Royal ~cable~ Nassau
11
+ Nassau -- Havana
12
+ Havana -ships-> Tortuga value: 30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/advanced.ts CHANGED
@@ -585,6 +585,7 @@ export type {
585
585
  BoundaryTopology,
586
586
  RegionName,
587
587
  RegionNames,
588
+ AirportData,
588
589
  } from './map/data/types';
589
590
 
590
591
  export type { RaciDragSource, RaciInteractionHandlers } from './raci';
@@ -728,18 +729,12 @@ export {
728
729
  contrastText,
729
730
  // Palette definitions
730
731
  nordPalette,
731
- solarizedPalette,
732
732
  catppuccinPalette,
733
- rosePinePalette,
734
- gruvboxPalette,
735
733
  tokyoNightPalette,
736
- oneDarkPalette,
737
734
  atlasPalette,
738
735
  blueprintPalette,
739
736
  slatePalette,
740
737
  tidewaterPalette,
741
- draculaPalette,
742
- monokaiPalette,
743
738
  } from './palettes';
744
739
 
745
740
  export type { PaletteConfig, PaletteColors } from './palettes';
package/src/auto/index.ts CHANGED
@@ -46,7 +46,7 @@ declare const __DGMO_VERSION__: string;
46
46
 
47
47
  const DEFAULTS: Required<AutoConfig> = {
48
48
  theme: 'auto',
49
- palette: 'nord',
49
+ palette: 'slate',
50
50
  showSource: true,
51
51
  showEditorLink: true,
52
52
  };
@@ -10,6 +10,13 @@
10
10
 
11
11
  import ELK from 'elkjs/lib/elk.bundled.js';
12
12
  import type { ParsedBoxesAndLines, BLNode, BLGroup } from './types';
13
+ import { measureText, wrapTextToWidth } from '../utils/text-measure';
14
+ import {
15
+ resolveNotes,
16
+ buildPlacedNotes,
17
+ noteCanvasShift,
18
+ type PlacedNote,
19
+ } from '../utils/notes';
13
20
 
14
21
  // ── Constants ──────────────────────────────────────────────
15
22
  const MARGIN = 40;
@@ -31,6 +38,11 @@ const MAX_DESC_LINES = 6;
31
38
  const MAX_LABEL_LINES = 3;
32
39
  const LABEL_LINE_HEIGHT = 1.3;
33
40
  const LABEL_PAD = 12;
41
+ // Bottom value-row reserved on a DESCRIBED node under `show-values`: a thin
42
+ // divider + a "Metric: value" footer line (replaces the old corner badge).
43
+ const VALUE_ROW_FONT = 11;
44
+ const VALUE_ROW_H =
45
+ SEPARATOR_GAP + VALUE_ROW_FONT * DESC_LINE_HEIGHT + DESC_PADDING;
34
46
 
35
47
  // ── Result types ───────────────────────────────────────────
36
48
 
@@ -40,6 +52,8 @@ export interface BLLayoutNode {
40
52
  readonly y: number;
41
53
  readonly width: number;
42
54
  readonly height: number;
55
+ /** A note floated beside this box (never moves the box). */
56
+ readonly note?: PlacedNote;
43
57
  }
44
58
 
45
59
  export interface BLLayoutEdge {
@@ -113,15 +127,14 @@ function estimateLabelLines(label: string, nodeWidth = NODE_WIDTH): number {
113
127
  if (!part) continue;
114
128
  words.push(...splitCamelCase(part));
115
129
  }
130
+ const maxTextWidth = nodeWidth - 24;
116
131
  for (let fontSize = 13; fontSize >= 9; fontSize--) {
117
- const charWidth = fontSize * 0.6;
118
- const maxChars = Math.floor((nodeWidth - 24) / charWidth);
119
- if (maxChars < 2) continue;
132
+ if (maxTextWidth < measureText('MM', fontSize)) continue;
120
133
  let lines = 1;
121
134
  let current = '';
122
135
  for (const word of words) {
123
136
  const test = current ? `${current} ${word}` : word;
124
- if (test.length <= maxChars) {
137
+ if (measureText(test, fontSize) <= maxTextWidth) {
125
138
  current = test;
126
139
  } else {
127
140
  lines++;
@@ -133,35 +146,31 @@ function estimateLabelLines(label: string, nodeWidth = NODE_WIDTH): number {
133
146
  return MAX_LABEL_LINES;
134
147
  }
135
148
 
136
- function computeNodeSize(node: BLNode): { width: number; height: number } {
149
+ function computeNodeSize(
150
+ node: BLNode,
151
+ reserveValueRow: boolean
152
+ ): { width: number; height: number } {
137
153
  if (!node.description || node.description.length === 0) {
138
154
  return { width: NODE_WIDTH, height: NODE_HEIGHT };
139
155
  }
140
156
  const w = DESC_NODE_WIDTH;
141
157
  const labelLines = estimateLabelLines(node.label, w);
142
158
  const labelHeight = labelLines * 13 * LABEL_LINE_HEIGHT + LABEL_PAD;
143
- const charsPerLine = Math.floor((w - 24) / (DESC_FONT_SIZE * 0.6));
159
+ const maxTextWidth = w - 24;
144
160
  let totalRenderedLines = 0;
145
161
  for (const line of node.description) {
146
- if (line.length <= charsPerLine) {
162
+ if (measureText(line, DESC_FONT_SIZE) <= maxTextWidth) {
147
163
  totalRenderedLines += 1;
148
164
  } else {
149
- const words = line.split(/\s+/);
150
- let current = '';
151
- let lineCount = 0;
152
- for (const word of words) {
153
- const fitted =
154
- word.length > charsPerLine ? word.slice(0, charsPerLine) : word;
155
- const test = current ? `${current} ${fitted}` : fitted;
156
- if (test.length <= charsPerLine) {
157
- current = test;
158
- } else {
159
- if (current) lineCount++;
160
- current = fitted;
165
+ // Hard-break long words to match the renderer's slicing behaviour.
166
+ totalRenderedLines += wrapTextToWidth(
167
+ line,
168
+ DESC_FONT_SIZE,
169
+ maxTextWidth,
170
+ {
171
+ hardBreak: true,
161
172
  }
162
- }
163
- if (current) lineCount++;
164
- totalRenderedLines += lineCount;
173
+ ).length;
165
174
  }
166
175
  }
167
176
  totalRenderedLines = Math.min(totalRenderedLines, MAX_DESC_LINES);
@@ -172,7 +181,8 @@ function computeNodeSize(node: BLNode): { width: number; height: number } {
172
181
  SEPARATOR_GAP +
173
182
  DESC_PADDING +
174
183
  descriptionHeight +
175
- DESC_PADDING;
184
+ DESC_PADDING +
185
+ (reserveValueRow ? VALUE_ROW_H : 0);
176
186
  return { width: w, height: Math.max(NODE_HEIGHT, totalHeight) };
177
187
  }
178
188
 
@@ -394,7 +404,10 @@ export async function layoutBoxesAndLines(
394
404
  collapsedChildCounts: Map<string, number>;
395
405
  originalGroups: readonly BLGroup[];
396
406
  },
397
- layoutOptions?: { hideDescriptions?: boolean }
407
+ layoutOptions?: {
408
+ hideDescriptions?: boolean;
409
+ collapsedNotes?: ReadonlySet<number>;
410
+ }
398
411
  ): Promise<BLLayoutResult> {
399
412
  const hideDescriptions = layoutOptions?.hideDescriptions ?? false;
400
413
  const direction = parsed.direction === 'TB' ? 'DOWN' : 'RIGHT';
@@ -423,7 +436,7 @@ export async function layoutBoxesAndLines(
423
436
  for (const node of parsed.nodes) {
424
437
  const size = hideDescriptions
425
438
  ? { width: NODE_WIDTH, height: NODE_HEIGHT }
426
- : computeNodeSize(node);
439
+ : computeNodeSize(node, parsed.showValues === true);
427
440
  nodeSizes.set(node.label, size);
428
441
  if (!hideDescriptions && node.description && node.description.length > 0) {
429
442
  maxDescHeight = Math.max(maxDescHeight, size.height);
@@ -728,5 +741,112 @@ export async function layoutBoxesAndLines(
728
741
  bestScore = s;
729
742
  }
730
743
  }
731
- return best;
744
+
745
+ return attachNotes(best, parsed, layoutOptions?.collapsedNotes);
746
+ }
747
+
748
+ /**
749
+ * Float notes beside their boxes on the chosen layout (runs after variant
750
+ * selection — notes don't affect scoring). `no-notes` opts out. A note placed
751
+ * above/left can land off-canvas, so the whole layout is shifted to fit.
752
+ * Un-annotated diagrams are returned unchanged (min coords stay ≥ 0).
753
+ */
754
+ function attachNotes(
755
+ layout: BLLayoutResult,
756
+ parsed: ParsedBoxesAndLines,
757
+ collapsedNotes?: ReadonlySet<number>
758
+ ): BLLayoutResult {
759
+ const notesSuppressed = parsed.options?.['no-notes'] === 'on';
760
+ const noteByNode =
761
+ notesSuppressed || !parsed.notes
762
+ ? new Map()
763
+ : resolveNotes(
764
+ parsed.notes,
765
+ parsed.nodes.map((n) => ({ id: n.label, label: n.label }))
766
+ );
767
+ if (noteByNode.size === 0) return layout;
768
+
769
+ const placed = buildPlacedNotes(
770
+ layout.nodes.map((n) => ({
771
+ id: n.label,
772
+ x: n.x,
773
+ y: n.y,
774
+ width: n.width,
775
+ height: n.height,
776
+ })),
777
+ noteByNode,
778
+ parsed.direction === 'TB' ? 'TB' : 'LR',
779
+ collapsedNotes
780
+ );
781
+ const notedNodes: BLLayoutNode[] = layout.nodes.map((n) => {
782
+ const note = placed.get(n.label);
783
+ return note ? { ...n, note } : n;
784
+ });
785
+
786
+ // Content bbox over nodes (+ their floated notes) and groups — matches the
787
+ // prior max-extent computation plus the notes.
788
+ let bbMinX = Infinity;
789
+ let bbMinY = Infinity;
790
+ let bbMaxX = -Infinity;
791
+ let bbMaxY = -Infinity;
792
+ const extend = (l: number, t: number, r: number, b: number): void => {
793
+ if (l < bbMinX) bbMinX = l;
794
+ if (t < bbMinY) bbMinY = t;
795
+ if (r > bbMaxX) bbMaxX = r;
796
+ if (b > bbMaxY) bbMaxY = b;
797
+ };
798
+ for (const n of notedNodes) {
799
+ extend(
800
+ n.x - n.width / 2,
801
+ n.y - n.height / 2,
802
+ n.x + n.width / 2,
803
+ n.y + n.height / 2
804
+ );
805
+ if (n.note && !n.note.collapsed) {
806
+ extend(
807
+ n.x + n.note.x,
808
+ n.y + n.note.y,
809
+ n.x + n.note.x + n.note.width,
810
+ n.y + n.note.y + n.note.height
811
+ );
812
+ }
813
+ }
814
+ for (const grp of layout.groups) {
815
+ extend(
816
+ grp.x - grp.width / 2,
817
+ grp.y - grp.height / 2,
818
+ grp.x + grp.width / 2,
819
+ grp.y + grp.height / 2
820
+ );
821
+ }
822
+ if (!Number.isFinite(bbMinX)) return { ...layout, nodes: notedNodes };
823
+
824
+ const { shiftX, shiftY } = noteCanvasShift(bbMinX, bbMinY);
825
+ const shifted = shiftX !== 0 || shiftY !== 0;
826
+ const finalNodes = shifted
827
+ ? notedNodes.map((n) => ({ ...n, x: n.x + shiftX, y: n.y + shiftY }))
828
+ : notedNodes;
829
+ const finalEdges = shifted
830
+ ? layout.edges.map((e) => ({
831
+ ...e,
832
+ points: e.points.map((pt) => ({ x: pt.x + shiftX, y: pt.y + shiftY })),
833
+ ...(e.labelX !== undefined && { labelX: e.labelX + shiftX }),
834
+ ...(e.labelY !== undefined && { labelY: e.labelY + shiftY }),
835
+ }))
836
+ : layout.edges;
837
+ const finalGroups = shifted
838
+ ? layout.groups.map((grp) => ({
839
+ ...grp,
840
+ x: grp.x + shiftX,
841
+ y: grp.y + shiftY,
842
+ }))
843
+ : layout.groups;
844
+
845
+ return {
846
+ nodes: finalNodes,
847
+ edges: finalEdges,
848
+ groups: finalGroups,
849
+ width: bbMaxX + shiftX + MARGIN,
850
+ height: bbMaxY + shiftY + MARGIN,
851
+ };
732
852
  }
@@ -26,7 +26,7 @@ import {
26
26
  extractColor,
27
27
  parseFirstLine,
28
28
  OPTION_NOCOLON_RE,
29
- peelTrailingColorName,
29
+ peelRampColors,
30
30
  splitNameAndMeta,
31
31
  tryParseSharedOption,
32
32
  warnUnknownMetaKeys,
@@ -35,6 +35,8 @@ import {
35
35
  BOXES_AND_LINES_REGISTRY,
36
36
  withTagAliases,
37
37
  } from '../utils/reserved-key-registry';
38
+ import { tryCollectNote, resolveNotes, type DiagramNote } from '../utils/notes';
39
+ import type { PaletteColors } from '../palettes';
38
40
 
39
41
  const MAX_GROUP_DEPTH = 2;
40
42
 
@@ -113,8 +115,12 @@ type MutBLGroup = Omit<Writable<BLGroup>, 'metadata' | 'children'> & {
113
115
  children: string[];
114
116
  };
115
117
 
116
- export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
118
+ export function parseBoxesAndLines(
119
+ content: string,
120
+ palette?: PaletteColors
121
+ ): ParsedBoxesAndLines {
117
122
  const options: Record<string, string> = {};
123
+ const notes: DiagramNote[] = [];
118
124
  const initialHiddenTagValues = new Map<string, Set<string>>();
119
125
  const nodes: MutBLNode[] = [];
120
126
  const edges: MutBLEdge[] = [];
@@ -305,11 +311,10 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
305
311
  const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
306
312
  if (metricMatch) {
307
313
  // Regex capture group present after successful match.
308
- const { label, colorName } = peelTrailingColorName(
309
- metricMatch[1]!.trim()
310
- );
314
+ const { label, low, high } = peelRampColors(metricMatch[1]!.trim());
311
315
  result.boxMetric = label;
312
- if (colorName !== undefined) result.boxMetricColor = colorName;
316
+ if (high !== undefined) result.boxMetricColor = high;
317
+ if (low !== undefined) result.boxMetricLowColor = low;
313
318
  continue;
314
319
  }
315
320
  if (/^show-values$/i.test(trimmed)) {
@@ -336,6 +341,24 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
336
341
  }
337
342
  }
338
343
 
344
+ // Note annotation (top-level): `note <Box> [inline body]` + an optional
345
+ // indented body. Checked before tag/group/node/edge matching so a note is
346
+ // never mistaken for a box; gated to indent 0. `note -> X` is excluded.
347
+ if (indent === 0) {
348
+ const noteResult = tryCollectNote(
349
+ lines,
350
+ i,
351
+ indent,
352
+ palette,
353
+ result.diagnostics
354
+ );
355
+ if (noteResult) {
356
+ if (noteResult.note) notes.push(noteResult.note);
357
+ i = noteResult.lastIndex;
358
+ continue;
359
+ }
360
+ }
361
+
339
362
  // Tag group heading — must be checked BEFORE group/node/edge matching
340
363
  const tagBlockMatch = matchTagBlockHeading(trimmed);
341
364
  if (tagBlockMatch && indent === 0) {
@@ -721,6 +744,17 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
721
744
  }
722
745
  result.edges = validEdges;
723
746
 
747
+ // Resolve note refs against box labels (forward refs OK). The id→note
748
+ // binding is recomputed in layout; this pass surfaces diagnostics.
749
+ if (notes.length > 0) {
750
+ result.notes = notes;
751
+ resolveNotes(
752
+ notes,
753
+ result.nodes.map((n) => ({ id: n.label, label: n.label })),
754
+ result.diagnostics
755
+ );
756
+ }
757
+
724
758
  // Post-parse: inject default tag metadata and validate tag values
725
759
  if (result.tagGroups.length > 0) {
726
760
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
@@ -1007,8 +1041,9 @@ function parseEdgeLine(
1007
1041
  };
1008
1042
  }
1009
1043
 
1010
- // Check for labeled arrow: `Source -label-> Target`
1011
- const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
1044
+ // Check for labeled arrow: `Source -label-> Target` (label lazy → split
1045
+ // at the first arrow, consistent with the other parsers).
1046
+ const labeledMatch = trimmed.match(/^(.+?)\s+-(.+?)->\s*(.+)$/);
1012
1047
  if (labeledMatch) {
1013
1048
  // Regex capture groups present after successful match.
1014
1049
  const rawSource = labeledMatch[1]!.trim();