@diagrammo/dgmo 0.15.1 → 0.17.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 (122) hide show
  1. package/README.md +9 -9
  2. package/dist/advanced.cjs +612 -734
  3. package/dist/advanced.d.cts +42 -36
  4. package/dist/advanced.d.ts +42 -36
  5. package/dist/advanced.js +612 -733
  6. package/dist/auto.cjs +508 -620
  7. package/dist/auto.js +105 -105
  8. package/dist/auto.mjs +508 -620
  9. package/dist/cli.cjs +144 -144
  10. package/dist/editor.cjs +8 -9
  11. package/dist/editor.js +8 -9
  12. package/dist/highlight.cjs +8 -9
  13. package/dist/highlight.js +8 -9
  14. package/dist/index.cjs +497 -608
  15. package/dist/index.js +497 -608
  16. package/dist/internal.cjs +612 -734
  17. package/dist/internal.d.cts +42 -36
  18. package/dist/internal.d.ts +42 -36
  19. package/dist/internal.js +612 -733
  20. package/dist/pert.d.cts +2 -2
  21. package/dist/pert.d.ts +2 -2
  22. package/docs/language-reference.md +97 -84
  23. package/docs/migration-sequence-color-to-tags.md +1 -1
  24. package/gallery/fixtures/area.dgmo +3 -3
  25. package/gallery/fixtures/bar-stacked.dgmo +5 -5
  26. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  27. package/gallery/fixtures/c4-full.dgmo +8 -8
  28. package/gallery/fixtures/class-full.dgmo +2 -2
  29. package/gallery/fixtures/doughnut.dgmo +6 -6
  30. package/gallery/fixtures/flowchart-colors.dgmo +3 -3
  31. package/gallery/fixtures/function.dgmo +3 -3
  32. package/gallery/fixtures/gantt-full.dgmo +9 -9
  33. package/gallery/fixtures/gantt.dgmo +7 -7
  34. package/gallery/fixtures/infra-full.dgmo +6 -6
  35. package/gallery/fixtures/infra.dgmo +2 -2
  36. package/gallery/fixtures/kanban.dgmo +9 -9
  37. package/gallery/fixtures/line.dgmo +2 -2
  38. package/gallery/fixtures/multi-line.dgmo +3 -3
  39. package/gallery/fixtures/org-full.dgmo +6 -6
  40. package/gallery/fixtures/quadrant.dgmo +2 -2
  41. package/gallery/fixtures/sankey.dgmo +9 -9
  42. package/gallery/fixtures/scatter.dgmo +3 -3
  43. package/gallery/fixtures/sequence-tags-protocols.dgmo +11 -11
  44. package/gallery/fixtures/sequence-tags.dgmo +10 -10
  45. package/gallery/fixtures/sequence.dgmo +4 -4
  46. package/gallery/fixtures/sitemap-full.dgmo +7 -7
  47. package/gallery/fixtures/slope.dgmo +5 -5
  48. package/gallery/fixtures/spr-eras.dgmo +9 -9
  49. package/gallery/fixtures/timeline.dgmo +3 -3
  50. package/gallery/fixtures/venn.dgmo +3 -3
  51. package/package.json +7 -3
  52. package/src/advanced.ts +0 -1
  53. package/src/auto/index.ts +2 -2
  54. package/src/boxes-and-lines/layout.ts +1 -2
  55. package/src/boxes-and-lines/renderer.ts +5 -1
  56. package/src/c4/parser.ts +2 -2
  57. package/src/c4/renderer.ts +15 -8
  58. package/src/chart.ts +18 -9
  59. package/src/class/parser.ts +8 -7
  60. package/src/class/renderer.ts +17 -6
  61. package/src/cli.ts +8 -8
  62. package/src/completion.ts +14 -17
  63. package/src/cycle/parser.ts +15 -1
  64. package/src/cycle/renderer.ts +6 -3
  65. package/src/d3.ts +88 -49
  66. package/src/diagnostics.ts +20 -0
  67. package/src/echarts.ts +28 -11
  68. package/src/editor/dgmo.grammar +1 -3
  69. package/src/editor/dgmo.grammar.d.ts +1 -1
  70. package/src/editor/dgmo.grammar.js +8 -8
  71. package/src/editor/dgmo.grammar.terms.js +11 -12
  72. package/src/editor/highlight-api.ts +0 -1
  73. package/src/editor/highlight.ts +0 -1
  74. package/src/er/parser.ts +19 -12
  75. package/src/er/renderer.ts +19 -7
  76. package/src/gantt/parser.ts +1 -1
  77. package/src/gantt/renderer.ts +7 -4
  78. package/src/graph/flowchart-parser.ts +18 -84
  79. package/src/graph/flowchart-renderer.ts +6 -8
  80. package/src/graph/layout.ts +0 -2
  81. package/src/graph/state-parser.ts +17 -62
  82. package/src/graph/state-renderer.ts +3 -8
  83. package/src/infra/parser.ts +21 -11
  84. package/src/infra/renderer.ts +8 -6
  85. package/src/journey-map/parser.ts +11 -4
  86. package/src/journey-map/renderer.ts +3 -1
  87. package/src/kanban/parser.ts +11 -7
  88. package/src/kanban/renderer.ts +3 -1
  89. package/src/mindmap/parser.ts +4 -5
  90. package/src/mindmap/renderer.ts +2 -1
  91. package/src/org/parser.ts +3 -3
  92. package/src/org/renderer.ts +4 -3
  93. package/src/pert/analyzer.ts +10 -10
  94. package/src/pert/layout.ts +1 -1
  95. package/src/pert/parser.ts +8 -8
  96. package/src/pert/renderer.ts +7 -2
  97. package/src/pert/types.ts +1 -1
  98. package/src/pyramid/parser.ts +13 -1
  99. package/src/raci/parser.ts +42 -12
  100. package/src/raci/renderer.ts +2 -1
  101. package/src/raci/types.ts +4 -3
  102. package/src/ring/parser.ts +13 -1
  103. package/src/sequence/parser.ts +81 -23
  104. package/src/sequence/participant-inference.ts +18 -181
  105. package/src/sequence/renderer.ts +48 -137
  106. package/src/sitemap/layout.ts +0 -2
  107. package/src/sitemap/parser.ts +12 -38
  108. package/src/sitemap/renderer.ts +13 -13
  109. package/src/sitemap/types.ts +0 -1
  110. package/src/tech-radar/parser.ts +2 -2
  111. package/src/tech-radar/renderer.ts +5 -3
  112. package/src/tech-radar/types.ts +2 -0
  113. package/src/utils/arrows.ts +3 -28
  114. package/src/utils/extract-alias.ts +1 -1
  115. package/src/utils/inline-markdown.ts +1 -1
  116. package/src/utils/legend-d3.ts +12 -6
  117. package/src/utils/legend-layout.ts +1 -1
  118. package/src/utils/legend-types.ts +1 -1
  119. package/src/utils/parsing.ts +64 -35
  120. package/src/utils/tag-groups.ts +98 -18
  121. package/src/utils/time-ticks.ts +1 -1
  122. package/src/wireframe/parser.ts +3 -3
@@ -49,7 +49,6 @@ const PARTICIPANT_BOX_HEIGHT = 50;
49
49
  const TOP_MARGIN = 20;
50
50
  const TITLE_HEIGHT = 30;
51
51
  const PARTICIPANT_Y_OFFSET = 10;
52
- const SERVICE_BORDER_RADIUS = 10;
53
52
  const MESSAGE_START_OFFSET = 30;
54
53
  const LIFELINE_TAIL = 30;
55
54
  const ARROWHEAD_SIZE = 8;
@@ -180,25 +179,6 @@ function renderRectParticipant(
180
179
  .attr('stroke-width', SW);
181
180
  }
182
181
 
183
- function renderServiceParticipant(
184
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
185
- palette: PaletteColors,
186
- isDark: boolean,
187
- color?: string,
188
- solid?: boolean
189
- ): void {
190
- g.append('rect')
191
- .attr('x', -W / 2)
192
- .attr('y', 0)
193
- .attr('width', W)
194
- .attr('height', H)
195
- .attr('rx', SERVICE_BORDER_RADIUS)
196
- .attr('ry', SERVICE_BORDER_RADIUS)
197
- .attr('fill', fill(palette, isDark, color, solid))
198
- .attr('stroke', stroke(palette, color))
199
- .attr('stroke-width', SW);
200
- }
201
-
202
182
  function renderActorParticipant(
203
183
  g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
204
184
  palette: PaletteColors,
@@ -443,99 +423,6 @@ function renderCacheParticipant(
443
423
  .attr('stroke-dasharray', dash);
444
424
  }
445
425
 
446
- function renderNetworkingParticipant(
447
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
448
- palette: PaletteColors,
449
- isDark: boolean,
450
- color?: string,
451
- solid?: boolean
452
- ): void {
453
- // Hexagon fitting within W x H
454
- const inset = 16;
455
- const points = [
456
- `${-W / 2 + inset},0`,
457
- `${W / 2 - inset},0`,
458
- `${W / 2},${H / 2}`,
459
- `${W / 2 - inset},${H}`,
460
- `${-W / 2 + inset},${H}`,
461
- `${-W / 2},${H / 2}`,
462
- ].join(' ');
463
- g.append('polygon')
464
- .attr('points', points)
465
- .attr('fill', fill(palette, isDark, color, solid))
466
- .attr('stroke', stroke(palette, color))
467
- .attr('stroke-width', SW);
468
- }
469
-
470
- function renderFrontendParticipant(
471
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
472
- palette: PaletteColors,
473
- isDark: boolean,
474
- color?: string,
475
- solid?: boolean
476
- ): void {
477
- // Monitor shape fitting within W x H
478
- const screenH = H - 10;
479
- const s = stroke(palette, color);
480
- g.append('rect')
481
- .attr('x', -W / 2)
482
- .attr('y', 0)
483
- .attr('width', W)
484
- .attr('height', screenH)
485
- .attr('rx', 3)
486
- .attr('ry', 3)
487
- .attr('fill', fill(palette, isDark, color, solid))
488
- .attr('stroke', s)
489
- .attr('stroke-width', SW);
490
- // Stand
491
- g.append('line')
492
- .attr('x1', 0)
493
- .attr('y1', screenH)
494
- .attr('x2', 0)
495
- .attr('y2', H - 2)
496
- .attr('stroke', s)
497
- .attr('stroke-width', SW);
498
- // Base
499
- g.append('line')
500
- .attr('x1', -14)
501
- .attr('y1', H - 2)
502
- .attr('x2', 14)
503
- .attr('y2', H - 2)
504
- .attr('stroke', s)
505
- .attr('stroke-width', SW);
506
- }
507
-
508
- function renderExternalParticipant(
509
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
510
- palette: PaletteColors,
511
- isDark: boolean,
512
- color?: string,
513
- solid?: boolean
514
- ): void {
515
- // Dashed border rectangle
516
- g.append('rect')
517
- .attr('x', -W / 2)
518
- .attr('y', 0)
519
- .attr('width', W)
520
- .attr('height', H)
521
- .attr('rx', 2)
522
- .attr('ry', 2)
523
- .attr('fill', fill(palette, isDark, color, solid))
524
- .attr('stroke', stroke(palette, color))
525
- .attr('stroke-width', SW)
526
- .attr('stroke-dasharray', '6 3');
527
- }
528
-
529
- function renderGatewayParticipant(
530
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
531
- palette: PaletteColors,
532
- isDark: boolean,
533
- color?: string,
534
- _solid?: boolean
535
- ): void {
536
- renderRectParticipant(g, palette, isDark, color);
537
- }
538
-
539
426
  // ============================================================
540
427
  // Collapsible Section Support
541
428
  // ============================================================
@@ -1528,7 +1415,7 @@ export function renderSequenceDiagram(
1528
1415
  // Ensure contentBottomY accounts for all note extents
1529
1416
  const lastStep = renderSteps[renderSteps.length - 1];
1530
1417
  const lastIsSelfCall =
1531
- lastStep && lastStep.type === 'call' && lastStep.from === lastStep.to;
1418
+ lastStep?.type === 'call' && lastStep.from === lastStep.to;
1532
1419
  const lastStepTrailing = lastIsSelfCall ? SELF_CALL_HEIGHT + 25 : stepSpacing;
1533
1420
  let contentBottomY =
1534
1421
  renderSteps.length > 0
@@ -1968,6 +1855,13 @@ export function renderSequenceDiagram(
1968
1855
  // FRAME_PADDING_TOP declared earlier (near BLOCK_HEADER_SPACE)
1969
1856
  const FRAME_PADDING_BOTTOM = 15;
1970
1857
  const FRAME_LABEL_HEIGHT = 18;
1858
+ // Self-loop projects ACTIVATION_WIDTH/2 + SELF_CALL_WIDTH (=35) past the
1859
+ // lifeline; FRAME_PADDING_X (=30) leaves no breathing room. When a block
1860
+ // contains a self-arrow, extend the frame on the loop's side so the loop
1861
+ // sits comfortably inside.
1862
+ const SELF_ARROW_PROJECTION = ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH;
1863
+ const SELF_ARROW_FRAME_PAD = 10;
1864
+ const frameRightmostX = Math.max(...Array.from(participantX.values()));
1971
1865
 
1972
1866
  // Collect message indices from an element subtree
1973
1867
  const collectMsgIndices = (els: SequenceElement[]): number[] => {
@@ -2063,14 +1957,46 @@ export function renderSequenceDiagram(
2063
1957
  }
2064
1958
  }
2065
1959
 
2066
- const frameX = minPX - FRAME_PADDING_X;
1960
+ // Self-arrow geometry: extend frame on the loop's side so loops sit
1961
+ // comfortably inside, and extend vertically if the block's last step
1962
+ // is a self-call (whose loop drops SELF_CALL_HEIGHT below stepY).
1963
+ let extraLeft = 0;
1964
+ let extraRight = 0;
1965
+ let maxStepIsSelfCall = false;
1966
+ for (const mi of allIndices) {
1967
+ const m = messages[mi];
1968
+ if (m.from === m.to) {
1969
+ const px = participantX.get(m.from);
1970
+ if (px !== undefined) {
1971
+ const flipLeft = px === frameRightmostX;
1972
+ if (flipLeft) {
1973
+ const loopMin = px - SELF_ARROW_PROJECTION;
1974
+ const need =
1975
+ minPX - FRAME_PADDING_X - loopMin + SELF_ARROW_FRAME_PAD;
1976
+ if (need > 0) extraLeft = Math.max(extraLeft, need);
1977
+ } else {
1978
+ const loopMax = px + SELF_ARROW_PROJECTION;
1979
+ const need =
1980
+ loopMax - (maxPX + FRAME_PADDING_X) + SELF_ARROW_FRAME_PAD;
1981
+ if (need > 0) extraRight = Math.max(extraRight, need);
1982
+ }
1983
+ }
1984
+ if (msgToLastStep.get(mi) === maxStep) {
1985
+ maxStepIsSelfCall = true;
1986
+ }
1987
+ }
1988
+ }
1989
+
1990
+ const frameX = minPX - FRAME_PADDING_X - extraLeft;
2067
1991
  const frameY = stepY(minStep) - FRAME_PADDING_TOP;
2068
- const frameW = maxPX - minPX + FRAME_PADDING_X * 2;
1992
+ const frameW =
1993
+ maxPX - minPX + FRAME_PADDING_X * 2 + extraLeft + extraRight;
2069
1994
  const frameH =
2070
1995
  stepY(maxStep) -
2071
1996
  stepY(minStep) +
2072
1997
  FRAME_PADDING_TOP +
2073
- FRAME_PADDING_BOTTOM;
1998
+ FRAME_PADDING_BOTTOM +
1999
+ (maxStepIsSelfCall ? SELF_CALL_HEIGHT : 0);
2074
2000
 
2075
2001
  // Frame border
2076
2002
  svg
@@ -2093,7 +2019,7 @@ export function renderSequenceDiagram(
2093
2019
  x: frameX + 6,
2094
2020
  y: frameY + FRAME_LABEL_HEIGHT - 4,
2095
2021
  text: `${el.type} ${el.label}`,
2096
- bold: true,
2022
+ bold: false,
2097
2023
  italic: false,
2098
2024
  blockLine: el.lineNumber,
2099
2025
  });
@@ -2121,7 +2047,7 @@ export function renderSequenceDiagram(
2121
2047
  y: dividerY + 14,
2122
2048
  text: `else if ${branchData.label}`,
2123
2049
  bold: false,
2124
- italic: true,
2050
+ italic: false,
2125
2051
  blockLine: branchData.lineNumber,
2126
2052
  });
2127
2053
  }
@@ -2150,7 +2076,7 @@ export function renderSequenceDiagram(
2150
2076
  y: dividerY + 14,
2151
2077
  text: 'else',
2152
2078
  bold: false,
2153
- italic: true,
2079
+ italic: false,
2154
2080
  blockLine: el.elseLineNumber,
2155
2081
  });
2156
2082
  }
@@ -2292,7 +2218,7 @@ export function renderSequenceDiagram(
2292
2218
 
2293
2219
  // Render section dividers
2294
2220
  const leftmostX = Math.min(...Array.from(participantX.values()));
2295
- const rightmostX = Math.max(...Array.from(participantX.values()));
2221
+ const rightmostX = frameRightmostX;
2296
2222
  const sectionLineX1 = leftmostX - PARTICIPANT_BOX_WIDTH / 2 - 10;
2297
2223
  const sectionLineX2 = rightmostX + PARTICIPANT_BOX_WIDTH / 2 + 10;
2298
2224
 
@@ -2762,7 +2688,7 @@ export function renderSequenceDiagram(
2762
2688
  const legendConfig: LegendConfig = {
2763
2689
  groups: resolvedGroups,
2764
2690
  position: { placement: 'top-center', titleRelation: 'below-title' },
2765
- mode: 'fixed',
2691
+ mode: 'preview',
2766
2692
  };
2767
2693
  const legendState: LegendState = {
2768
2694
  activeGroup: activeTagGroup ?? null,
@@ -2853,27 +2779,12 @@ function renderParticipant(
2853
2779
  case 'database':
2854
2780
  renderDatabaseParticipant(g, palette, isDark, color, solid);
2855
2781
  break;
2856
- case 'service':
2857
- renderServiceParticipant(g, palette, isDark, color, solid);
2858
- break;
2859
2782
  case 'queue':
2860
2783
  renderQueueParticipant(g, palette, isDark, color, solid);
2861
2784
  break;
2862
2785
  case 'cache':
2863
2786
  renderCacheParticipant(g, palette, isDark, color, solid);
2864
2787
  break;
2865
- case 'networking':
2866
- renderNetworkingParticipant(g, palette, isDark, color, solid);
2867
- break;
2868
- case 'frontend':
2869
- renderFrontendParticipant(g, palette, isDark, color, solid);
2870
- break;
2871
- case 'external':
2872
- renderExternalParticipant(g, palette, isDark, color, solid);
2873
- break;
2874
- case 'gateway':
2875
- renderGatewayParticipant(g, palette, isDark, color, solid);
2876
- break;
2877
2788
  default:
2878
2789
  renderRectParticipant(g, palette, isDark, color, solid);
2879
2790
  break;
@@ -41,7 +41,6 @@ export interface SitemapLayoutEdge {
41
41
  targetId: string;
42
42
  points: { x: number; y: number }[];
43
43
  label?: string;
44
- color?: string;
45
44
  lineNumber: number;
46
45
  /** True for edges deferred from dagre (container endpoints) — use linear curve */
47
46
  deferred?: boolean;
@@ -652,7 +651,6 @@ export function layoutSitemap(
652
651
  targetId: edge.targetId,
653
652
  points,
654
653
  label: edge.label,
655
- color: edge.color,
656
654
  lineNumber: edge.lineNumber,
657
655
  deferred: deferredSet.has(i) || undefined,
658
656
  });
@@ -3,7 +3,6 @@
3
3
  // ============================================================
4
4
 
5
5
  import type { PaletteColors } from '../palettes';
6
- import { resolveColorWithDiagnostic } from '../colors';
7
6
  import type { DgmoError } from '../diagnostics';
8
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
9
8
  import { normalizeName } from '../utils/name-normalize';
@@ -20,7 +19,6 @@ import {
20
19
  measureIndent,
21
20
  extractColor,
22
21
  parsePipeMetadata,
23
- inferArrowColor,
24
22
  MULTIPLE_PIPE_ERROR,
25
23
  parseFirstLine,
26
24
  OPTION_NOCOLON_RE,
@@ -39,10 +37,11 @@ const CONTAINER_RE = /^\[([^\]]+)\]\s*(?:\|\s*(.+))?$/;
39
37
  const METADATA_RE = /^([^:]+):\s*(.+)$/;
40
38
 
41
39
  /**
42
- * Arrow line: `-label->`, `-(color)->`, `-label(color)->`, `->` followed by target label.
43
- * Captures: [1] label, [2] color, [3] target
40
+ * Arrow line: `-label->` or `->` followed by target label.
41
+ * Edges have no color slot (spec §1.7).
42
+ * Captures: [1] label, [2] target
44
43
  */
45
- const ARROW_RE = /^-([^(>][^(>]*?)?\s*(?:\(([^)]+)\))?\s*->\s*(.+)$/;
44
+ const ARROW_RE = /^-([^>][^>]*?)?\s*->\s*(.+)$/;
46
45
  const BARE_ARROW_RE = /^->\s*(.+)$/;
47
46
 
48
47
  // ============================================================
@@ -51,12 +50,11 @@ const BARE_ARROW_RE = /^->\s*(.+)$/;
51
50
 
52
51
  function parseArrowLine(
53
52
  trimmed: string,
54
- palette: PaletteColors | undefined,
55
- lineNumber: number,
56
- diagnostics: DgmoError[]
53
+ _palette: PaletteColors | undefined,
54
+ _lineNumber: number,
55
+ _diagnostics: DgmoError[]
57
56
  ): {
58
57
  label?: string;
59
- color?: string;
60
58
  target: string;
61
59
  targetIsGroup: boolean;
62
60
  } | null {
@@ -71,33 +69,14 @@ function parseArrowLine(
71
69
  };
72
70
  }
73
71
 
74
- // Labeled/colored arrow: -label(color)-> Target
72
+ // Labeled arrow: -label-> Target
75
73
  const arrowMatch = trimmed.match(ARROW_RE);
76
74
  if (arrowMatch) {
77
75
  const label = arrowMatch[1]?.trim() || undefined;
78
- let color = arrowMatch[2]
79
- ? resolveColorWithDiagnostic(
80
- arrowMatch[2].trim(),
81
- lineNumber,
82
- diagnostics,
83
- palette
84
- )
85
- : undefined;
86
- if (label && !color) {
87
- const inferred = inferArrowColor(label);
88
- if (inferred)
89
- color = resolveColorWithDiagnostic(
90
- inferred,
91
- lineNumber,
92
- diagnostics,
93
- palette
94
- );
95
- }
96
- const rawTarget = arrowMatch[3].trim();
76
+ const rawTarget = arrowMatch[2].trim();
97
77
  const groupMatch = rawTarget.match(/^\[(.+)\]$/);
98
78
  return {
99
79
  label,
100
- color,
101
80
  target: groupMatch ? groupMatch[1].trim() : rawTarget,
102
81
  targetIsGroup: !!groupMatch,
103
82
  };
@@ -183,7 +162,7 @@ export function parseSitemap(
183
162
  result.diagnostics.push(makeDgmoError(line, message, 'warning'));
184
163
  };
185
164
 
186
- if (!content || !content.trim()) {
165
+ if (!content?.trim()) {
187
166
  return fail(0, 'No content provided');
188
167
  }
189
168
 
@@ -216,7 +195,6 @@ export function parseSitemap(
216
195
  targetLabel: string;
217
196
  targetIsGroup: boolean;
218
197
  label?: string;
219
- color?: string;
220
198
  lineNumber: number;
221
199
  }[] = [];
222
200
 
@@ -309,7 +287,7 @@ export function parseSitemap(
309
287
  }
310
288
  }
311
289
 
312
- // Tag group entries (indented Value(color) under tag heading)
290
+ // Tag group entries (indented `Value color` under tag heading; §1.5)
313
291
  // First entry is the default unless another is marked `default`
314
292
  if (currentTagGroup && !contentStarted) {
315
293
  const indent = measureIndent(line);
@@ -319,7 +297,7 @@ export function parseSitemap(
319
297
  if (!color) {
320
298
  pushError(
321
299
  lineNumber,
322
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
300
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
323
301
  );
324
302
  continue;
325
303
  }
@@ -365,7 +343,6 @@ export function parseSitemap(
365
343
  targetLabel: arrowInfo.target,
366
344
  targetIsGroup: arrowInfo.targetIsGroup,
367
345
  label: arrowInfo.label,
368
- color: arrowInfo.color,
369
346
  lineNumber,
370
347
  });
371
348
  }
@@ -486,7 +463,6 @@ export function parseSitemap(
486
463
  sourceId: arrow.sourceNode.id,
487
464
  targetId: aliasHit,
488
465
  label: arrow.label,
489
- color: arrow.color,
490
466
  lineNumber: arrow.lineNumber,
491
467
  });
492
468
  continue;
@@ -508,7 +484,6 @@ export function parseSitemap(
508
484
  sourceId: arrow.sourceNode.id,
509
485
  targetId: targetContainer.id,
510
486
  label: arrow.label,
511
- color: arrow.color,
512
487
  lineNumber: arrow.lineNumber,
513
488
  });
514
489
  } else {
@@ -526,7 +501,6 @@ export function parseSitemap(
526
501
  sourceId: arrow.sourceNode.id,
527
502
  targetId: targetNode.id,
528
503
  label: arrow.label,
529
- color: arrow.color,
530
504
  lineNumber: arrow.lineNumber,
531
505
  });
532
506
  }
@@ -116,7 +116,8 @@ export function renderSitemap(
116
116
  onClickItem?: (lineNumber: number) => void,
117
117
  exportDims?: { width?: number; height?: number },
118
118
  activeTagGroup?: string | null,
119
- hiddenAttributes?: Set<string>
119
+ hiddenAttributes?: Set<string>,
120
+ exportMode?: boolean
120
121
  ): void {
121
122
  // Clear existing content
122
123
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -184,11 +185,9 @@ export function renderSitemap(
184
185
  .attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
185
186
  .attr('fill', palette.textMuted);
186
187
 
187
- // Colored arrowheads
188
+ // Edges have no color slot (spec §1.7); keep empty set so the marker-setup
189
+ // loop is a no-op but the symbol stays available for future color sources.
188
190
  const edgeColors = new Set<string>();
189
- for (const edge of layout.edges) {
190
- if (edge.color) edgeColors.add(edge.color);
191
- }
192
191
  for (const color of edgeColors) {
193
192
  const id = `sm-arrow-${color.replace('#', '')}`;
194
193
  defs
@@ -379,10 +378,8 @@ export function renderSitemap(
379
378
  .attr('class', 'sitemap-edge-group')
380
379
  .attr('data-line-number', String(edge.lineNumber));
381
380
 
382
- const edgeColor = edge.color ?? palette.textMuted;
383
- const markerId = edge.color
384
- ? `sm-arrow-${edge.color.replace('#', '')}`
385
- : 'sm-arrow';
381
+ const edgeColor = palette.textMuted;
382
+ const markerId = 'sm-arrow';
386
383
 
387
384
  const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
388
385
  const pathD = gen(edge.points);
@@ -602,7 +599,8 @@ export function renderSitemap(
602
599
  isDark,
603
600
  activeTagGroup,
604
601
  undefined,
605
- hiddenAttributes
602
+ hiddenAttributes,
603
+ exportMode
606
604
  );
607
605
  }
608
606
 
@@ -647,7 +645,8 @@ export function renderSitemap(
647
645
  isDark,
648
646
  activeTagGroup,
649
647
  width,
650
- hiddenAttributes
648
+ hiddenAttributes,
649
+ exportMode
651
650
  );
652
651
  }
653
652
  }
@@ -663,7 +662,8 @@ function renderLegend(
663
662
  isDark: boolean,
664
663
  activeTagGroup?: string | null,
665
664
  fixedWidth?: number,
666
- hiddenAttributes?: Set<string>
665
+ hiddenAttributes?: Set<string>,
666
+ exportMode?: boolean
667
667
  ): void {
668
668
  if (legendGroups.length === 0) return;
669
669
 
@@ -678,7 +678,7 @@ function renderLegend(
678
678
  const legendConfig: LegendConfig = {
679
679
  groups,
680
680
  position: { placement: 'top-center', titleRelation: 'below-title' },
681
- mode: 'fixed',
681
+ mode: exportMode ? 'export' : 'preview',
682
682
  capsulePillAddonWidth: eyeAddonWidth,
683
683
  };
684
684
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
@@ -22,7 +22,6 @@ export interface SitemapEdge {
22
22
  sourceId: string;
23
23
  targetId: string;
24
24
  label?: string;
25
- color?: string;
26
25
  lineNumber: number;
27
26
  }
28
27
 
@@ -62,7 +62,7 @@ export function parseTechRadar(content: string): ParsedTechRadar {
62
62
  result.diagnostics.push(makeDgmoError(line, message, 'warning'));
63
63
  };
64
64
 
65
- if (!content || !content.trim()) {
65
+ if (!content?.trim()) {
66
66
  return fail(0, 'No content provided');
67
67
  }
68
68
 
@@ -115,7 +115,7 @@ export function parseTechRadar(content: string): ParsedTechRadar {
115
115
  // --- First line: chart type + title ---
116
116
  if (!headerParsed) {
117
117
  const firstLine = parseFirstLine(trimmed);
118
- if (firstLine && firstLine.chartType === 'tech-radar') {
118
+ if (firstLine?.chartType === 'tech-radar') {
119
119
  result.title = firstLine.title ?? '';
120
120
  result.titleLineNumber = lineNumber;
121
121
  headerParsed = true;
@@ -163,7 +163,7 @@ export function renderTechRadar(
163
163
  },
164
164
  ],
165
165
  position: { placement: 'top-center', titleRelation: 'below-title' },
166
- mode: 'fixed',
166
+ mode: options?.exportMode ? 'export' : 'preview',
167
167
  controlsGroup: {
168
168
  toggles: [
169
169
  {
@@ -1195,7 +1195,8 @@ export function renderTechRadarForExport(
1195
1195
  palette: PaletteColors,
1196
1196
  isDark: boolean,
1197
1197
  exportDims?: D3ExportDimensions,
1198
- viewState?: CompactViewState
1198
+ viewState?: CompactViewState,
1199
+ exportMode?: boolean
1199
1200
  ): void {
1200
1201
  renderTechRadar(
1201
1202
  container,
@@ -1204,6 +1205,7 @@ export function renderTechRadarForExport(
1204
1205
  isDark,
1205
1206
  undefined,
1206
1207
  exportDims,
1207
- viewState
1208
+ viewState,
1209
+ { exportMode }
1208
1210
  );
1209
1211
  }
@@ -78,4 +78,6 @@ export interface TechRadarRenderOptions {
78
78
  onLegendGroupToggle?: (groupName: string) => void;
79
79
  /** Active line from the editor cursor — triggers popover/expansion for that blip. */
80
80
  activeLine?: number | null;
81
+ /** True when rendering for export (PNG/SVG/PDF) — controls whether collapsed legend pills and cog are stripped. */
82
+ exportMode?: boolean;
81
83
  }
@@ -22,7 +22,6 @@
22
22
 
23
23
  import type { DgmoError } from '../diagnostics';
24
24
  import { makeDgmoError } from '../diagnostics';
25
- import { RECOGNIZED_COLOR_NAMES } from '../colors';
26
25
 
27
26
  interface ParsedArrow {
28
27
  from: string;
@@ -140,10 +139,9 @@ export interface ParseInArrowLabelResult {
140
139
  *
141
140
  * This helper is intentionally chart-agnostic: it operates on an already
142
141
  * extracted label string, leaving each chart's existing arrow-finding
143
- * tokenization in place. TD-11 color-parens is handled inside the
144
- * flowchart and state `parseArrowToken` functions because those are the
145
- * only charts that interpret `-(color)->` as a colored edge; they use
146
- * `matchColorParens()` from this module for the shared lookup.
142
+ * tokenization in place. Edges no longer have a color slot on any chart
143
+ * type (see spec §1.7 "Edge color is not a feature"); arrow content is
144
+ * pure label text.
147
145
  */
148
146
  export function parseInArrowLabel(
149
147
  rawLabel: string,
@@ -162,29 +160,6 @@ export function parseInArrowLabel(
162
160
  return { label: trimmed, diagnostics };
163
161
  }
164
162
 
165
- // ============================================================
166
- // matchColorParens — shared TD-11 helper for flowchart and state
167
- // ============================================================
168
-
169
- /**
170
- * Test whether a string matches the TD-11 color-parens form `(colorName)`
171
- * where `colorName` is one of the 11 recognized palette color names from
172
- * `src/colors.ts:RECOGNIZED_COLOR_NAMES`. Returns the lowercase color name
173
- * on a match, or `null` on fall-through (whole string becomes a label).
174
- *
175
- * Used by flowchart and state parsers to keep the color-parens recognition
176
- * rule in one place — do NOT re-implement the regex in chart parsers.
177
- */
178
- export function matchColorParens(content: string): string | null {
179
- const m = content.match(/^\(([A-Za-z]+)\)$/);
180
- if (!m) return null;
181
- const candidate = m[1].toLowerCase();
182
- if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(candidate)) {
183
- return candidate;
184
- }
185
- return null;
186
- }
187
-
188
163
  // Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
189
164
  const SYNC_LABELED_RE = /^(.+?)\s*-(.+)->\s*(.+)$/;
190
165
  const ASYNC_LABELED_RE = /^(.+?)\s*~(.+)~>\s*(.+)$/;
@@ -32,7 +32,7 @@ export const ALIAS_TOKEN_RE = /^[A-Za-z][A-Za-z0-9_]{0,11}$/;
32
32
  * DGMO grammar keywords with all chart-type tokens (per F5).
33
33
  *
34
34
  * Articles (`a`, `an`, `the`) are intentionally NOT reserved — the
35
- * spec's worked examples use `Alice is a service as a`, where `a`
35
+ * spec's worked examples use `Alice is an actor as a`, where `a`
36
36
  * is a perfectly fine single-letter alias. Reserving English
37
37
  * articles would break the canonical example pattern.
38
38
  */
@@ -2,7 +2,7 @@
2
2
  // Inline Markdown — shared parsing + SVG rendering for text fields
3
3
  // ============================================================
4
4
 
5
- import * as d3Selection from 'd3-selection';
5
+ import type * as d3Selection from 'd3-selection';
6
6
  import type { PaletteColors } from '../palettes';
7
7
  import { safeHref } from './safe-href';
8
8
 
@@ -46,7 +46,14 @@ export function renderLegendD3(
46
46
  let currentState = { ...state };
47
47
  let currentLayout: LegendLayout;
48
48
 
49
- const legendG = container.append('g').attr('class', 'dgmo-legend');
49
+ const legendG = container
50
+ .append('g')
51
+ .attr('class', 'dgmo-legend')
52
+ .attr('data-legend-title-relation', config.position.titleRelation)
53
+ .attr(
54
+ 'data-legend-capsule-addon-width',
55
+ String(config.capsulePillAddonWidth ?? 0)
56
+ );
50
57
 
51
58
  function render() {
52
59
  currentLayout = computeLegendLayout(config, currentState, width);
@@ -270,11 +277,10 @@ function renderPill(
270
277
  groupBg: string,
271
278
  callbacks?: LegendCallbacks
272
279
  ): void {
273
- // Collapsed tag-group pills survive static export so readers see
274
- // that the diagram has tag dimensions even when no group is active.
275
- // (Per spec §1.3 "Coloring is opt-in" exports default to collapsed
276
- // pills, no node coloring.) Interactive controls keep
277
- // `data-export-ignore` separately.
280
+ // Collapsed tag-group pills are hidden in export mode
281
+ // (`LegendConfig.mode === 'export'`) the layout engine filters them
282
+ // in `computeLegendLayout`. See
283
+ // tech-spec-hide-inactive-tag-pills-in-exports.md.
278
284
  const g = parent
279
285
  .append('g')
280
286
  .attr('transform', `translate(${pill.x},${pill.y})`)