@diagrammo/dgmo 0.15.0 → 0.16.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 (127) hide show
  1. package/README.md +23 -10
  2. package/dist/advanced.cjs +53094 -0
  3. package/dist/advanced.d.cts +4690 -0
  4. package/dist/advanced.d.ts +4690 -0
  5. package/dist/advanced.js +52849 -0
  6. package/dist/auto.cjs +2298 -2069
  7. package/dist/auto.js +132 -109
  8. package/dist/auto.mjs +2294 -2065
  9. package/dist/cli.cjs +175 -152
  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 +2281 -2048
  15. package/dist/index.d.cts +45 -1
  16. package/dist/index.d.ts +45 -1
  17. package/dist/index.js +2276 -2044
  18. package/dist/internal.cjs +2064 -1831
  19. package/dist/internal.d.cts +113 -113
  20. package/dist/internal.d.ts +113 -113
  21. package/dist/internal.js +2059 -1826
  22. package/dist/pert.cjs +325 -0
  23. package/dist/pert.d.cts +542 -0
  24. package/dist/pert.d.ts +542 -0
  25. package/dist/pert.js +294 -0
  26. package/docs/language-reference.md +83 -66
  27. package/gallery/fixtures/area.dgmo +3 -3
  28. package/gallery/fixtures/bar-stacked.dgmo +5 -5
  29. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  30. package/gallery/fixtures/c4-full.dgmo +8 -8
  31. package/gallery/fixtures/class-full.dgmo +2 -2
  32. package/gallery/fixtures/doughnut.dgmo +6 -6
  33. package/gallery/fixtures/flowchart-colors.dgmo +3 -3
  34. package/gallery/fixtures/function.dgmo +3 -3
  35. package/gallery/fixtures/gantt-full.dgmo +9 -9
  36. package/gallery/fixtures/gantt.dgmo +7 -7
  37. package/gallery/fixtures/infra-full.dgmo +6 -6
  38. package/gallery/fixtures/infra.dgmo +2 -2
  39. package/gallery/fixtures/kanban.dgmo +9 -9
  40. package/gallery/fixtures/line.dgmo +2 -2
  41. package/gallery/fixtures/multi-line.dgmo +3 -3
  42. package/gallery/fixtures/org-full.dgmo +6 -6
  43. package/gallery/fixtures/quadrant.dgmo +2 -2
  44. package/gallery/fixtures/sankey.dgmo +9 -9
  45. package/gallery/fixtures/scatter.dgmo +3 -3
  46. package/gallery/fixtures/sequence-tags-protocols.dgmo +8 -8
  47. package/gallery/fixtures/sequence-tags.dgmo +7 -7
  48. package/gallery/fixtures/sitemap-full.dgmo +7 -7
  49. package/gallery/fixtures/slope.dgmo +5 -5
  50. package/gallery/fixtures/spr-eras.dgmo +9 -9
  51. package/gallery/fixtures/timeline.dgmo +3 -3
  52. package/gallery/fixtures/venn.dgmo +3 -3
  53. package/package.json +28 -3
  54. package/src/advanced.ts +730 -0
  55. package/src/auto/index.ts +14 -13
  56. package/src/boxes-and-lines/layout.ts +481 -445
  57. package/src/boxes-and-lines/renderer.ts +5 -1
  58. package/src/c4/parser.ts +8 -8
  59. package/src/c4/renderer.ts +15 -8
  60. package/src/chart-types.ts +0 -5
  61. package/src/chart.ts +18 -9
  62. package/src/class/parser.ts +8 -15
  63. package/src/class/renderer.ts +17 -6
  64. package/src/cli.ts +15 -13
  65. package/src/completion-types.ts +28 -0
  66. package/src/completion.ts +28 -21
  67. package/src/cycle/layout.ts +2 -2
  68. package/src/cycle/parser.ts +14 -0
  69. package/src/cycle/renderer.ts +6 -3
  70. package/src/d3.ts +1537 -1164
  71. package/src/echarts.ts +37 -20
  72. package/src/editor/dgmo.grammar +1 -3
  73. package/src/editor/dgmo.grammar.js +8 -8
  74. package/src/editor/dgmo.grammar.terms.js +11 -12
  75. package/src/editor/highlight-api.ts +0 -1
  76. package/src/editor/highlight.ts +0 -1
  77. package/src/er/parser.ts +19 -20
  78. package/src/er/renderer.ts +20 -8
  79. package/src/gantt/calculator.ts +1 -11
  80. package/src/gantt/parser.ts +17 -17
  81. package/src/gantt/renderer.ts +9 -6
  82. package/src/graph/flowchart-parser.ts +19 -85
  83. package/src/graph/flowchart-renderer.ts +4 -9
  84. package/src/graph/layout.ts +0 -2
  85. package/src/graph/state-parser.ts +17 -62
  86. package/src/graph/state-renderer.ts +4 -9
  87. package/src/index.ts +17 -1
  88. package/src/infra/parser.ts +40 -30
  89. package/src/infra/renderer.ts +9 -6
  90. package/src/internal.ts +9 -721
  91. package/src/journey-map/parser.ts +10 -3
  92. package/src/journey-map/renderer.ts +3 -1
  93. package/src/kanban/parser.ts +12 -8
  94. package/src/kanban/renderer.ts +3 -1
  95. package/src/mindmap/layout.ts +1 -1
  96. package/src/mindmap/parser.ts +3 -3
  97. package/src/mindmap/renderer.ts +2 -1
  98. package/src/org/parser.ts +3 -3
  99. package/src/org/renderer.ts +5 -4
  100. package/src/pert/layout.ts +1 -1
  101. package/src/pert/monte-carlo.ts +2 -2
  102. package/src/pert/parser.ts +10 -10
  103. package/src/pert/renderer.ts +7 -2
  104. package/src/pert/types.ts +1 -1
  105. package/src/pyramid/parser.ts +12 -0
  106. package/src/raci/parser.ts +44 -14
  107. package/src/raci/renderer.ts +3 -2
  108. package/src/raci/types.ts +4 -3
  109. package/src/ring/parser.ts +12 -0
  110. package/src/sequence/parser.ts +15 -9
  111. package/src/sequence/renderer.ts +2 -5
  112. package/src/sitemap/layout.ts +0 -2
  113. package/src/sitemap/parser.ts +12 -38
  114. package/src/sitemap/renderer.ts +13 -13
  115. package/src/sitemap/types.ts +0 -1
  116. package/src/tech-radar/interactive.ts +1 -1
  117. package/src/tech-radar/renderer.ts +6 -4
  118. package/src/tech-radar/types.ts +2 -0
  119. package/src/utils/arrows.ts +3 -28
  120. package/src/utils/legend-d3.ts +12 -6
  121. package/src/utils/legend-layout.ts +1 -1
  122. package/src/utils/legend-types.ts +1 -1
  123. package/src/utils/parsing.ts +64 -35
  124. package/src/utils/tag-groups.ts +109 -30
  125. package/src/wireframe/layout.ts +11 -7
  126. package/src/wireframe/parser.ts +4 -4
  127. package/src/wireframe/renderer.ts +5 -2
@@ -215,7 +215,10 @@ const IS_A_PATTERN = /^([^:]+?)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
215
215
  const POSITION_ONLY_PATTERN = /^([^:]+?)\s+position\s+(-?\d+)$/i;
216
216
 
217
217
  // Colored participant declaration — e.g. "Tapin2(green)", "API(blue)"
218
- const COLORED_PARTICIPANT_PATTERN = /^(\S+?)\(([^)]+)\)\s*$/;
218
+ // Scoped to recognized 11-name palette colors only (§1.5) so legitimate
219
+ // `funcCall(arg)` lines don't trigger the legacy-color diagnostic.
220
+ const COLORED_PARTICIPANT_PATTERN =
221
+ /^(\S+?)\((red|orange|yellow|green|blue|purple|teal|cyan|gray|black|white)\)\s*$/;
219
222
 
220
223
  // Group heading pattern — "[Backend]", "[Backend] | t: Product"
221
224
  // Group 1: name (no ] or | inside brackets), Group 2: color in parens, Group 3: after-bracket text
@@ -678,7 +681,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
678
681
  if (groupColor) {
679
682
  pushWarning(
680
683
  lineNumber,
681
- `(${groupColor}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
684
+ `'(${groupColor})' parens-color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
682
685
  );
683
686
  }
684
687
  contentStarted = true;
@@ -765,7 +768,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
765
768
  continue;
766
769
  }
767
770
 
768
- // Tag group entries (indented Value(color) under tag heading)
771
+ // Tag group entries (indented Value color under tag heading)
769
772
  // First entry is the default unless another is marked `default`
770
773
  if (currentTagGroup && !contentStarted && measureIndent(raw) > 0) {
771
774
  const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
@@ -778,7 +781,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
778
781
  if (!color) {
779
782
  pushError(
780
783
  lineNumber,
781
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
784
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
782
785
  );
783
786
  continue;
784
787
  }
@@ -807,11 +810,14 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
807
810
  blockStack.pop();
808
811
  }
809
812
  const labelRaw = sectionMatch[1].trim();
810
- const colorMatch = labelRaw.match(/^(.+?)\(([^)]+)\)$/);
813
+ // Scoped to recognized 11-name palette colors only (§1.5).
814
+ const colorMatch = labelRaw.match(
815
+ /^(.+?)\((red|orange|yellow|green|blue|purple|teal|cyan|gray|black|white)\)$/
816
+ );
811
817
  if (colorMatch) {
812
818
  pushWarning(
813
819
  lineNumber,
814
- `(${colorMatch[2].trim()}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
820
+ `'(${colorMatch[2]})' parens-color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
815
821
  );
816
822
  }
817
823
  contentStarted = true;
@@ -1016,8 +1022,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1016
1022
  continue;
1017
1023
  }
1018
1024
 
1019
- // Colored participant declaration — "Name(color)" at any level
1020
- // Color syntax is deprecated emit warning and register without color
1025
+ // Legacy `Name(color)` participant declaration at any level (§1.5 hard
1026
+ // break). Scoped to the 11-name palette so `funcCall(arg)` doesn't trip.
1021
1027
  const { core: colorCore, meta: colorMeta } = splitPipe(trimmed, lineNumber);
1022
1028
  const coloredMatch = colorCore.match(COLORED_PARTICIPANT_PATTERN);
1023
1029
  if (coloredMatch && !ARROW_PATTERN.test(colorCore)) {
@@ -1025,7 +1031,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1025
1031
  const color = coloredMatch[2].trim();
1026
1032
  pushError(
1027
1033
  lineNumber,
1028
- `'${id}(${color})' syntax is no longer supported — use 'tag:' groups for coloring`
1034
+ `'${id}(${color})' parens-color syntax is no longer supported — use 'tag:' groups for coloring`
1029
1035
  );
1030
1036
  contentStarted = true;
1031
1037
  const key = addParticipant(id, lineNumber, { metadata: colorMeta });
@@ -949,9 +949,6 @@ export function renderSequenceDiagram(
949
949
  const messages = collapsed ? collapsed.messages : parsed.messages;
950
950
  const elements = collapsed ? collapsed.elements : parsed.elements;
951
951
  const groups = collapsed ? collapsed.groups : parsed.groups;
952
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
953
- const collapsedGroupIds = collapsed?.collapsedGroupIds ?? new Map();
954
-
955
952
  const collapsedSections = options?.collapsedSections;
956
953
 
957
954
  const sourceParticipants = collapsed
@@ -1006,7 +1003,7 @@ export function renderSequenceDiagram(
1006
1003
  const charsForWidth = (maxW: number): number =>
1007
1004
  Math.floor((maxW - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
1008
1005
 
1009
- const activationsOff = parsedOptions.activations?.toLowerCase() === 'off';
1006
+ const activationsOff = parsedOptions['activations']?.toLowerCase() === 'off';
1010
1007
 
1011
1008
  // Tag resolution — shared utility handles priority chain:
1012
1009
  // programmatic override → diagram-level active-tag → auto-activate first group
@@ -2765,7 +2762,7 @@ export function renderSequenceDiagram(
2765
2762
  const legendConfig: LegendConfig = {
2766
2763
  groups: resolvedGroups,
2767
2764
  position: { placement: 'top-center', titleRelation: 'below-title' },
2768
- mode: 'fixed',
2765
+ mode: 'preview',
2769
2766
  };
2770
2767
  const legendState: LegendState = {
2771
2768
  activeGroup: activeTagGroup ?? null,
@@ -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
  };
@@ -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
  }
@@ -566,7 +540,7 @@ export function parseSitemap(
566
540
  function parseNodeLabel(
567
541
  trimmed: string,
568
542
  lineNumber: number,
569
- palette: PaletteColors | undefined,
543
+ _palette: PaletteColors | undefined,
570
544
  counter: number,
571
545
  metaAliasMap: Map<string, string> = new Map(),
572
546
  warnFn?: (line: number, msg: string) => void,
@@ -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
 
@@ -188,7 +188,7 @@ function renderQuarterCircle(
188
188
  width: number,
189
189
  height: number,
190
190
  mutedColor: string,
191
- tooltip: HTMLDivElement,
191
+ _tooltip: HTMLDivElement,
192
192
  rootContainer: HTMLElement,
193
193
  onClickItem?: (lineNumber: number) => void
194
194
  ): void {
@@ -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
  {
@@ -920,7 +920,7 @@ import type { TechRadarBlip } from './types';
920
920
 
921
921
  function createBlipPopover(
922
922
  container: HTMLElement,
923
- palette: PaletteColors,
923
+ _palette: PaletteColors,
924
924
  isDark: boolean
925
925
  ): HTMLDivElement {
926
926
  container.style.position = 'relative';
@@ -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*(.+)$/;
@@ -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})`)
@@ -235,7 +235,7 @@ export function computeLegendLayout(
235
235
  containerWidth: number
236
236
  ): LegendLayout {
237
237
  const { groups, controls: configControls, mode } = config;
238
- const isExport = mode === 'inline';
238
+ const isExport = mode === 'export';
239
239
 
240
240
  // Filter groups for export: only active group shown
241
241
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
@@ -37,7 +37,7 @@ export interface LegendPosition {
37
37
  titleRelation: 'below-title' | 'inline-with-title';
38
38
  }
39
39
 
40
- export type LegendMode = 'fixed' | 'inline';
40
+ export type LegendMode = 'preview' | 'export';
41
41
 
42
42
  export type LegendControlExportBehavior = 'include' | 'strip' | 'static';
43
43
 
@@ -4,10 +4,18 @@
4
4
  * pipe-metadata parsing.
5
5
  */
6
6
 
7
- import { resolveColor, resolveColorWithDiagnostic } from '../colors';
7
+ import {
8
+ RECOGNIZED_COLOR_NAMES,
9
+ resolveColor,
10
+ resolveColorWithDiagnostic,
11
+ } from '../colors';
8
12
  import type { DgmoError } from '../diagnostics';
9
13
  import type { PaletteColors } from '../palettes';
10
14
 
15
+ const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
16
+ RECOGNIZED_COLOR_NAMES
17
+ );
18
+
11
19
  // ── All known chart types ────────────────────────────────────
12
20
  /** Complete set of recognized chart type identifiers. */
13
21
  export const ALL_CHART_TYPES = new Set([
@@ -87,27 +95,47 @@ export function measureIndent(line: string): number {
87
95
  return indent;
88
96
  }
89
97
 
90
- /** Matches a trailing `(colorName)` suffix on a label. */
91
- export const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
92
-
93
- /** Extract an optional trailing color suffix from a label, resolving via palette. */
98
+ /**
99
+ * Trailing-token color rule (see docs/dgmo-language-spec.md §1.5).
100
+ *
101
+ * Caller contract: `label` must be a pre-split LABEL REGION the parser is
102
+ * responsible for stripping structural terminators (`as <alias>`, `| pipe
103
+ * metadata`, numeric values, date ranges, brackets, arrow constructs) BEFORE
104
+ * invoking this function. The color rule operates only on what remains.
105
+ *
106
+ * Algorithm: split the label on whitespace; if the final token is exactly one
107
+ * of `RECOGNIZED_COLOR_NAMES` (case-sensitive, lowercase only), peel it off
108
+ * as color and return the rest as the label. Otherwise the entire input
109
+ * stays as the label, no color.
110
+ *
111
+ * Case-sensitivity is deliberate: it provides the escape hatch (`Red`,
112
+ * `Yellow`, `Green` stay as labels — useful for traffic-light tag groups).
113
+ *
114
+ * Returns `{ label, color? }` where `color` is the palette-resolved hex string
115
+ * (or undefined if no color word matched).
116
+ */
94
117
  export function extractColor(
95
118
  label: string,
96
119
  palette?: PaletteColors,
97
120
  diagnostics?: DgmoError[],
98
121
  line?: number
99
122
  ): { label: string; color?: string } {
100
- const m = label.match(COLOR_SUFFIX_RE);
101
- if (!m) return { label };
102
- const colorName = m[1].trim();
123
+ const lastSpaceIdx = Math.max(
124
+ label.lastIndexOf(' '),
125
+ label.lastIndexOf('\t')
126
+ );
127
+ if (lastSpaceIdx < 0) return { label };
128
+ const trailing = label.substring(lastSpaceIdx + 1);
129
+ // Case-sensitive lowercase match against the closed 11-name palette.
130
+ if (!RECOGNIZED_COLOR_SET.has(trailing)) return { label };
103
131
  let color: string | undefined;
104
132
  if (diagnostics && line !== undefined) {
105
- color = resolveColorWithDiagnostic(colorName, line, diagnostics, palette);
133
+ color = resolveColorWithDiagnostic(trailing, line, diagnostics, palette);
106
134
  } else {
107
- color = resolveColor(colorName, palette) ?? undefined;
135
+ color = resolveColor(trailing, palette) ?? undefined;
108
136
  }
109
137
  return {
110
- label: label.substring(0, m.index!).trim(),
138
+ label: label.substring(0, lastSpaceIdx).trimEnd(),
111
139
  color,
112
140
  };
113
141
  }
@@ -457,31 +485,32 @@ export function parseSeriesNames(
457
485
  }
458
486
 
459
487
  /**
460
- * Infer arrow color from label text.
461
- * Returns a named palette color or undefined if no inference applies.
462
- * Case-insensitive, exact match only (not prefix/substring).
488
+ * Peel a trailing recognized color name from a label region, returning the
489
+ * raw color name (not a resolved hex). Used by chart types that pair the
490
+ * universal trailing-token shortcut with their own pipe-metadata `color: …`
491
+ * long form (cycle, pyramid, ring, raci, boxes-and-lines).
492
+ *
493
+ * Caller contract: `label` must already have pipe metadata stripped — this
494
+ * function operates only on the label region.
495
+ *
496
+ * Returns `{ label, colorName? }`. If the trailing token is not a recognized
497
+ * lowercase color, returns the original label and `colorName: undefined`.
463
498
  */
464
- export function inferArrowColor(label: string): string | undefined {
465
- const lower = label.toLowerCase();
466
- // Green: positive/affirmative
467
- if (
468
- lower === 'yes' ||
469
- lower === 'success' ||
470
- lower === 'ok' ||
471
- lower === 'true'
472
- )
473
- return 'green';
474
- // Red: negative/failure
475
- if (
476
- lower === 'no' ||
477
- lower === 'fail' ||
478
- lower === 'error' ||
479
- lower === 'false'
480
- )
481
- return 'red';
482
- // Orange: uncertain/warning
483
- if (lower === 'maybe' || lower === 'warning') return 'orange';
484
- return undefined;
499
+ export function peelTrailingColorName(label: string): {
500
+ label: string;
501
+ colorName?: string;
502
+ } {
503
+ const lastSpaceIdx = Math.max(
504
+ label.lastIndexOf(' '),
505
+ label.lastIndexOf('\t')
506
+ );
507
+ if (lastSpaceIdx < 0) return { label };
508
+ const trailing = label.substring(lastSpaceIdx + 1);
509
+ if (!RECOGNIZED_COLOR_SET.has(trailing)) return { label };
510
+ return {
511
+ label: label.substring(0, lastSpaceIdx).trimEnd(),
512
+ colorName: trailing,
513
+ };
485
514
  }
486
515
 
487
516
  /** Error message for multiple pipes on a single line. */