@diagrammo/dgmo 0.15.1 → 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 (109) hide show
  1. package/README.md +9 -9
  2. package/dist/advanced.cjs +479 -454
  3. package/dist/advanced.d.cts +34 -35
  4. package/dist/advanced.d.ts +34 -35
  5. package/dist/advanced.js +479 -453
  6. package/dist/auto.cjs +374 -352
  7. package/dist/auto.js +103 -103
  8. package/dist/auto.mjs +374 -352
  9. package/dist/cli.cjs +140 -140
  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 +365 -342
  15. package/dist/index.js +365 -342
  16. package/dist/internal.cjs +479 -454
  17. package/dist/internal.d.cts +34 -35
  18. package/dist/internal.d.ts +34 -35
  19. package/dist/internal.js +479 -453
  20. package/dist/pert.d.cts +2 -2
  21. package/dist/pert.d.ts +2 -2
  22. package/docs/language-reference.md +83 -66
  23. package/gallery/fixtures/area.dgmo +3 -3
  24. package/gallery/fixtures/bar-stacked.dgmo +5 -5
  25. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  26. package/gallery/fixtures/c4-full.dgmo +8 -8
  27. package/gallery/fixtures/class-full.dgmo +2 -2
  28. package/gallery/fixtures/doughnut.dgmo +6 -6
  29. package/gallery/fixtures/flowchart-colors.dgmo +3 -3
  30. package/gallery/fixtures/function.dgmo +3 -3
  31. package/gallery/fixtures/gantt-full.dgmo +9 -9
  32. package/gallery/fixtures/gantt.dgmo +7 -7
  33. package/gallery/fixtures/infra-full.dgmo +6 -6
  34. package/gallery/fixtures/infra.dgmo +2 -2
  35. package/gallery/fixtures/kanban.dgmo +9 -9
  36. package/gallery/fixtures/line.dgmo +2 -2
  37. package/gallery/fixtures/multi-line.dgmo +3 -3
  38. package/gallery/fixtures/org-full.dgmo +6 -6
  39. package/gallery/fixtures/quadrant.dgmo +2 -2
  40. package/gallery/fixtures/sankey.dgmo +9 -9
  41. package/gallery/fixtures/scatter.dgmo +3 -3
  42. package/gallery/fixtures/sequence-tags-protocols.dgmo +8 -8
  43. package/gallery/fixtures/sequence-tags.dgmo +7 -7
  44. package/gallery/fixtures/sitemap-full.dgmo +7 -7
  45. package/gallery/fixtures/slope.dgmo +5 -5
  46. package/gallery/fixtures/spr-eras.dgmo +9 -9
  47. package/gallery/fixtures/timeline.dgmo +3 -3
  48. package/gallery/fixtures/venn.dgmo +3 -3
  49. package/package.json +1 -1
  50. package/src/advanced.ts +0 -1
  51. package/src/boxes-and-lines/renderer.ts +5 -1
  52. package/src/c4/parser.ts +1 -1
  53. package/src/c4/renderer.ts +15 -8
  54. package/src/chart.ts +18 -9
  55. package/src/class/parser.ts +7 -6
  56. package/src/class/renderer.ts +17 -6
  57. package/src/cli.ts +6 -6
  58. package/src/completion.ts +13 -3
  59. package/src/cycle/parser.ts +14 -0
  60. package/src/cycle/renderer.ts +6 -3
  61. package/src/d3.ts +86 -46
  62. package/src/echarts.ts +26 -9
  63. package/src/editor/dgmo.grammar +1 -3
  64. package/src/editor/dgmo.grammar.js +8 -8
  65. package/src/editor/dgmo.grammar.terms.js +11 -12
  66. package/src/editor/highlight-api.ts +0 -1
  67. package/src/editor/highlight.ts +0 -1
  68. package/src/er/parser.ts +18 -11
  69. package/src/er/renderer.ts +19 -7
  70. package/src/gantt/parser.ts +1 -1
  71. package/src/gantt/renderer.ts +7 -4
  72. package/src/graph/flowchart-parser.ts +18 -84
  73. package/src/graph/flowchart-renderer.ts +3 -8
  74. package/src/graph/layout.ts +0 -2
  75. package/src/graph/state-parser.ts +17 -62
  76. package/src/graph/state-renderer.ts +3 -8
  77. package/src/infra/parser.ts +21 -11
  78. package/src/infra/renderer.ts +7 -4
  79. package/src/journey-map/parser.ts +10 -3
  80. package/src/journey-map/renderer.ts +3 -1
  81. package/src/kanban/parser.ts +10 -6
  82. package/src/kanban/renderer.ts +3 -1
  83. package/src/mindmap/parser.ts +2 -2
  84. package/src/mindmap/renderer.ts +2 -1
  85. package/src/org/parser.ts +2 -2
  86. package/src/org/renderer.ts +4 -3
  87. package/src/pert/parser.ts +7 -7
  88. package/src/pert/renderer.ts +7 -2
  89. package/src/pert/types.ts +1 -1
  90. package/src/pyramid/parser.ts +12 -0
  91. package/src/raci/parser.ts +40 -10
  92. package/src/raci/renderer.ts +2 -1
  93. package/src/raci/types.ts +4 -3
  94. package/src/ring/parser.ts +12 -0
  95. package/src/sequence/parser.ts +15 -9
  96. package/src/sequence/renderer.ts +1 -1
  97. package/src/sitemap/layout.ts +0 -2
  98. package/src/sitemap/parser.ts +11 -37
  99. package/src/sitemap/renderer.ts +13 -13
  100. package/src/sitemap/types.ts +0 -1
  101. package/src/tech-radar/renderer.ts +5 -3
  102. package/src/tech-radar/types.ts +2 -0
  103. package/src/utils/arrows.ts +3 -28
  104. package/src/utils/legend-d3.ts +12 -6
  105. package/src/utils/legend-layout.ts +1 -1
  106. package/src/utils/legend-types.ts +1 -1
  107. package/src/utils/parsing.ts +64 -35
  108. package/src/utils/tag-groups.ts +98 -18
  109. package/src/wireframe/parser.ts +2 -2
@@ -2024,7 +2024,8 @@ function renderLegend(
2024
2024
  palette: PaletteColors,
2025
2025
  isDark: boolean,
2026
2026
  activeGroup: string | null,
2027
- playback?: InfraPlaybackState
2027
+ playback?: InfraPlaybackState,
2028
+ exportMode = false
2028
2029
  ) {
2029
2030
  if (legendGroups.length === 0 && !playback) return;
2030
2031
 
@@ -2049,7 +2050,7 @@ function renderLegend(
2049
2050
  const legendConfig: LegendConfig = {
2050
2051
  groups: allGroups,
2051
2052
  position: { placement: 'top-center', titleRelation: 'below-title' },
2052
- mode: 'fixed',
2053
+ mode: exportMode ? 'export' : 'preview',
2053
2054
  showEmptyGroups: true,
2054
2055
  };
2055
2056
  const legendState: LegendState = { activeGroup };
@@ -2394,7 +2395,8 @@ export function renderInfra(
2394
2395
  palette,
2395
2396
  isDark,
2396
2397
  activeGroup ?? null,
2397
- playback ?? undefined
2398
+ playback ?? undefined,
2399
+ exportMode
2398
2400
  );
2399
2401
  // Re-enable pointer events on interactive legend elements
2400
2402
  legendSvg
@@ -2410,7 +2412,8 @@ export function renderInfra(
2410
2412
  palette,
2411
2413
  isDark,
2412
2414
  activeGroup ?? null,
2413
- playback ?? undefined
2415
+ playback ?? undefined,
2416
+ exportMode
2414
2417
  );
2415
2418
  }
2416
2419
  }
@@ -1,4 +1,5 @@
1
1
  import type { PaletteColors } from '../palettes';
2
+ import { resolveColorWithDiagnostic } from '../colors';
2
3
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
3
4
  import {
4
5
  matchTagBlockHeading,
@@ -138,8 +139,14 @@ export function parseJourneyMap(
138
139
  const key = part.substring(0, colonIdx).trim().toLowerCase();
139
140
  const value = part.substring(colonIdx + 1).trim();
140
141
  if (key === 'color') {
141
- const resolved = extractColor(`x(${value})`, palette);
142
- personaColor = resolved.color;
142
+ // Resolve the color name directly (no synthetic parens wrap).
143
+ personaColor =
144
+ resolveColorWithDiagnostic(
145
+ value,
146
+ lineNumber,
147
+ result.diagnostics,
148
+ palette
149
+ ) ?? undefined;
143
150
  }
144
151
  }
145
152
  }
@@ -209,7 +216,7 @@ export function parseJourneyMap(
209
216
  if (!color) {
210
217
  warn(
211
218
  lineNumber,
212
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
219
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
213
220
  );
214
221
  continue;
215
222
  }
@@ -36,6 +36,7 @@ export interface JourneyMapInteractiveOptions {
36
36
  collapsedPhases?: Set<string>;
37
37
  /** Called when a phase is toggled */
38
38
  onPhaseToggle?: (phaseName: string) => void;
39
+ exportMode?: boolean;
39
40
  }
40
41
 
41
42
  // ============================================================
@@ -313,7 +314,7 @@ export function renderJourneyMap(
313
314
  titleRelation: 'inline-with-title',
314
315
  },
315
316
  titleWidth: 0,
316
- mode: exportDims ? 'inline' : 'fixed',
317
+ mode: options?.exportMode ? 'export' : 'preview',
317
318
  };
318
319
 
319
320
  const legendState: LegendState = { activeGroup: effectiveActiveGroup };
@@ -1559,6 +1560,7 @@ export function renderJourneyMapForExport(
1559
1560
  const container = document.createElement('div');
1560
1561
  renderJourneyMap(container, parsed, palette, isDark, {
1561
1562
  exportDims: { width: layout.totalWidth, height: layout.totalHeight },
1563
+ exportMode: true,
1562
1564
  });
1563
1565
 
1564
1566
  const svgEl = container.querySelector('svg');
@@ -26,10 +26,11 @@ import type {
26
26
  // Regex patterns
27
27
  // ============================================================
28
28
 
29
- // [Column Name], [Column Name](color), [Column Name] as <alias>, [Column Name] | wip: 3, etc.
29
+ // [Column Name], [Column Name] color, [Column Name] as <alias>, [Column Name] | wip: 3, etc.
30
+ // Universal §1.5 trailing-token: color is a bare token after `]`.
30
31
  // Captures: [1]=label [2]=color [3]=alias (TD-18) [4]=pipe meta
31
32
  const COLUMN_RE =
32
- /^\[(.+?)\](?:\s*\(([^)]+)\))?(?:\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11}))?\s*(?:\|\s*(.+))?$/;
33
+ /^\[(.+?)\](?:\s+(\S+))?(?:\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11}))?\s*(?:\|\s*(.+))?$/;
33
34
  // Legacy delimiter
34
35
  const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
35
36
 
@@ -180,7 +181,7 @@ export function parseKanban(
180
181
  }
181
182
  }
182
183
 
183
- // Tag group entries (indented Value(color) under tag heading)
184
+ // Tag group entries (indented Value color under tag heading)
184
185
  // First entry is the default unless another is marked `default`
185
186
  if (currentTagGroup && !contentStarted) {
186
187
  const indent = measureIndent(line);
@@ -190,7 +191,7 @@ export function parseKanban(
190
191
  if (!color) {
191
192
  warn(
192
193
  lineNumber,
193
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
194
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
194
195
  );
195
196
  continue;
196
197
  }
@@ -247,9 +248,12 @@ export function parseKanban(
247
248
 
248
249
  columnCounter++;
249
250
  const colName = columnMatch[1].trim();
250
- const colColor = columnMatch[2]
251
+ // Trailing token after `]` must be a recognized color word (§1.5).
252
+ // If it isn't, the line is malformed — emit the standard diagnostic.
253
+ const rawTrailing = columnMatch[2]?.trim();
254
+ const colColor = rawTrailing
251
255
  ? resolveColorWithDiagnostic(
252
- columnMatch[2].trim(),
256
+ rawTrailing,
253
257
  lineNumber,
254
258
  result.diagnostics,
255
259
  palette
@@ -37,6 +37,7 @@ interface KanbanInteractiveOptions {
37
37
  collapsedLanes?: Set<string>;
38
38
  collapsedColumns?: Set<string>;
39
39
  compactMeta?: boolean;
40
+ exportMode?: boolean;
40
41
  }
41
42
 
42
43
  // ============================================================
@@ -330,7 +331,7 @@ export function renderKanban(
330
331
  const legendConfig: LegendConfig = {
331
332
  groups: parsed.tagGroups,
332
333
  position: { placement: 'top-center', titleRelation: 'inline-with-title' },
333
- mode: exportDims ? 'inline' : 'fixed',
334
+ mode: options?.exportMode ? 'export' : 'preview',
334
335
  };
335
336
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
336
337
  const legendG = svg
@@ -682,6 +683,7 @@ export function renderKanbanForExport(
682
683
  const container = document.createElement('div');
683
684
  renderKanban(container, parsed, palette, isDark, {
684
685
  exportDims: { width: layout.totalWidth, height: layout.totalHeight },
686
+ exportMode: true,
685
687
  });
686
688
 
687
689
  const svgEl = container.querySelector('svg');
@@ -179,7 +179,7 @@ export function parseMindmap(
179
179
  }
180
180
  }
181
181
 
182
- // Tag group entries (indented Value(color) under tag heading)
182
+ // Tag group entries (indented Value color under tag heading)
183
183
  if (currentTagGroup && !contentStarted) {
184
184
  const indent = measureIndent(line);
185
185
  if (indent > 0) {
@@ -188,7 +188,7 @@ export function parseMindmap(
188
188
  if (!color) {
189
189
  pushError(
190
190
  lineNumber,
191
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
191
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
192
192
  );
193
193
  continue;
194
194
  }
@@ -95,6 +95,7 @@ export function renderMindmap(
95
95
  onToggleDescriptions?: (active: boolean) => void;
96
96
  controlsExpanded?: boolean;
97
97
  onToggleControlsExpand?: () => void;
98
+ exportMode?: boolean;
98
99
  }
99
100
  ): void {
100
101
  const isExport = !!exportDims;
@@ -234,7 +235,7 @@ export function renderMindmap(
234
235
  };
235
236
  }),
236
237
  position: { placement: 'top-center', titleRelation: 'below-title' },
237
- mode: 'fixed',
238
+ mode: options?.exportMode ? 'export' : 'preview',
238
239
  controlsGroup: controlsToggles,
239
240
  };
240
241
  const legendState: LegendState = {
package/src/org/parser.ts CHANGED
@@ -235,7 +235,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
235
235
  }
236
236
  }
237
237
 
238
- // Tag group entries (indented Value(color) under tag heading)
238
+ // Tag group entries (indented Value color under tag heading)
239
239
  // First entry is the default unless another is marked `default`
240
240
  if (currentTagGroup && !contentStarted) {
241
241
  const indent = measureIndent(line);
@@ -245,7 +245,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
245
245
  if (!color) {
246
246
  pushError(
247
247
  lineNumber,
248
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
248
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
249
249
  );
250
250
  continue;
251
251
  }
@@ -110,7 +110,8 @@ export function renderOrg(
110
110
  exportDims?: { width?: number; height?: number },
111
111
  activeTagGroup?: string | null,
112
112
  hiddenAttributes?: Set<string>,
113
- ancestorPath?: AncestorInfo[]
113
+ ancestorPath?: AncestorInfo[],
114
+ exportMode?: boolean
114
115
  ): void {
115
116
  // Clear existing content
116
117
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -759,7 +760,7 @@ export function renderOrg(
759
760
  },
760
761
  ],
761
762
  position: { placement: 'top-center', titleRelation: 'below-title' },
762
- mode: 'fixed',
763
+ mode: exportMode ? 'export' : 'preview',
763
764
  };
764
765
  const singleState: LegendState = { activeGroup: lg.name };
765
766
  const groupG = legendParentBase
@@ -783,7 +784,7 @@ export function renderOrg(
783
784
  const legendConfig: LegendConfig = {
784
785
  groups,
785
786
  position: { placement: 'top-center', titleRelation: 'below-title' },
786
- mode: 'fixed',
787
+ mode: exportMode ? 'export' : 'preview',
787
788
  capsulePillAddonWidth: eyeAddonWidth,
788
789
  };
789
790
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
@@ -448,7 +448,7 @@ export interface ParsePertOptions {
448
448
  now?: Date;
449
449
  /**
450
450
  * Active palette — used when resolving color names on `tag` entries
451
- * (e.g. `High(red)` → palette.colors.red). Optional; when omitted the
451
+ * (e.g. `High red` → palette.colors.red). Optional; when omitted the
452
452
  * universal default color map is used.
453
453
  */
454
454
  palette?: PaletteColors;
@@ -503,7 +503,7 @@ export function parsePert(
503
503
 
504
504
  /**
505
505
  * Tag groups declared at the top of the diagram. A `tag …` heading
506
- * opens a block; entries (indented `Value(color)` lines) accumulate
506
+ * opens a block; entries (indented `Value color` lines) accumulate
507
507
  * until the first non-tag content line closes it.
508
508
  */
509
509
  const tagGroups: TagGroup[] = [];
@@ -576,7 +576,7 @@ export function parsePert(
576
576
  // layer is responsible for routing.
577
577
  }
578
578
 
579
- // ── Tag-block phase. `tag Priority as p\n High(red)\n Low(green)`
579
+ // ── Tag-block phase. `tag Priority as p\n High red\n Low green`
580
580
  // lives BEFORE diagram content; once any group / activity / arrow
581
581
  // is seen, `contentStarted` flips and further `tag …` headings
582
582
  // emit an error.
@@ -599,7 +599,7 @@ export function parsePert(
599
599
  );
600
600
  }
601
601
  tagGroups.push(currentTagGroup);
602
- // Inline values (e.g. `tag Priority as p Low(green), High(red)`).
602
+ // Inline values (e.g. `tag Priority as p Low green, High red`).
603
603
  if (tagBlockMatch.inlineValues) {
604
604
  for (const raw of tagBlockMatch.inlineValues) {
605
605
  const { text, isDefault } = stripDefaultModifier(raw);
@@ -612,7 +612,7 @@ export function parsePert(
612
612
  if (!color) {
613
613
  warn(
614
614
  lineNumber,
615
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
615
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
616
616
  );
617
617
  continue;
618
618
  }
@@ -624,7 +624,7 @@ export function parsePert(
624
624
  }
625
625
  continue;
626
626
  }
627
- // Indented `Value(color)` entry under an open tag block.
627
+ // Indented `Value color` entry under an open tag block.
628
628
  if (currentTagGroup && indent > 0) {
629
629
  const { text, isDefault } = stripDefaultModifier(trimmed);
630
630
  const { label, color } = extractColor(
@@ -636,7 +636,7 @@ export function parsePert(
636
636
  if (!color) {
637
637
  warn(
638
638
  lineNumber,
639
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
639
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
640
640
  );
641
641
  continue;
642
642
  }
@@ -388,6 +388,8 @@ export interface PertRenderOptions {
388
388
  * through to the parsed `active-tag` directive.
389
389
  */
390
390
  activeTagOverride?: string | null;
391
+ /** True when rendering for export — strips collapsed pills and cog from legend. */
392
+ exportMode?: boolean;
391
393
  }
392
394
 
393
395
  export function renderPert(
@@ -568,6 +570,7 @@ export function renderPert(
568
570
  y: tagLegendY,
569
571
  width: exportWidth,
570
572
  activeGroup: tagLegendActive,
573
+ exportMode: options.exportMode,
571
574
  });
572
575
  }
573
576
 
@@ -681,6 +684,7 @@ export function renderPertForExport(
681
684
  title: hasTitle ? parsed.title : null,
682
685
  subtitle: resolved.projectSubtitle,
683
686
  exportDims: { width: exportWidth, height: exportHeight },
687
+ exportMode: true,
684
688
  });
685
689
  const svgEl = container.querySelector('svg');
686
690
  if (!svgEl) return '';
@@ -2990,6 +2994,7 @@ interface TagLegendArgs {
2990
2994
  y: number;
2991
2995
  width: number;
2992
2996
  activeGroup: string | null;
2997
+ exportMode?: boolean;
2993
2998
  }
2994
2999
 
2995
3000
  /**
@@ -3008,7 +3013,7 @@ function renderTagLegendRow(
3008
3013
  ): void {
3009
3014
  if (resolved.tagGroups.length === 0) return;
3010
3015
 
3011
- const { x, y, width, activeGroup } = args;
3016
+ const { x, y, width, activeGroup, exportMode } = args;
3012
3017
  const groups = resolved.tagGroups.map((g) => ({
3013
3018
  name: g.name,
3014
3019
  entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
@@ -3024,7 +3029,7 @@ function renderTagLegendRow(
3024
3029
  {
3025
3030
  groups,
3026
3031
  position: { placement: 'top-center', titleRelation: 'below-title' },
3027
- mode: 'fixed',
3032
+ mode: exportMode ? 'export' : 'preview',
3028
3033
  },
3029
3034
  { activeGroup },
3030
3035
  palette,
package/src/pert/types.ts CHANGED
@@ -215,7 +215,7 @@ export interface ParsedPert {
215
215
  groups: PertGroup[];
216
216
  /**
217
217
  * Tag groups declared at the top of the diagram (`tag Priority as p
218
- * High(red), Low(green)`). Drive node fill via `resolveTagColor()`.
218
+ * High red, Low green`). Drive node fill via `resolveTagColor()`.
219
219
  * Empty when no `tag` blocks are declared.
220
220
  */
221
221
  tagGroups: TagGroup[];
@@ -7,6 +7,7 @@ import {
7
7
  measureIndent,
8
8
  parseFirstLine,
9
9
  parsePipeMetadata,
10
+ peelTrailingColorName,
10
11
  tryParseSharedOption,
11
12
  } from '../utils/parsing';
12
13
  import type { ParsedPyramid, PyramidLayer } from './types';
@@ -146,6 +147,17 @@ export function parsePyramid(content: string): ParsedPyramid {
146
147
  continue;
147
148
  }
148
149
 
150
+ // Universal trailing-token shortcut: `Label color` is equivalent to
151
+ // `Label | color: <name>` when color is the only metadata key (§1.5).
152
+ if (!color) {
153
+ const { label: stripped, colorName: shortcutColor } =
154
+ peelTrailingColorName(label);
155
+ if (shortcutColor) {
156
+ color = shortcutColor;
157
+ label = stripped;
158
+ }
159
+ }
160
+
149
161
  currentLayer = {
150
162
  label,
151
163
  lineNumber: lineNum,
@@ -24,6 +24,7 @@ import {
24
24
  measureIndent,
25
25
  parseFirstLine,
26
26
  parsePipeMetadata,
27
+ peelTrailingColorName,
27
28
  OPTION_NOCOLON_RE,
28
29
  tryParseSharedOption,
29
30
  } from '../utils/parsing';
@@ -82,10 +83,10 @@ const KNOWN_BOOLEANS = new Set<string>([
82
83
  ...Object.keys(VARIANT_LOCK_DIRECTIVES),
83
84
  ]);
84
85
 
85
- // Allow optional trailing `| key: value, ...` after the bracket,
86
- // e.g. `[Voyage] | color: blue` matches the modern dgmo idiom
87
- // (cycle / pyramid / ring / journey-map / boxes-and-lines).
88
- const PHASE_RE = /^\[(.+?)\]\s*(?:\|\s*(.+))?\s*$/;
86
+ // Allow optional trailing color shortcut and/or pipe metadata after the
87
+ // bracket: `[Voyage] blue | desc: …` (per §1.5 universal trailing-token
88
+ // rule + the modern cycle/pyramid/ring/journey-map/b&l idiom).
89
+ const PHASE_RE = /^\[(.+?)\](?:\s+(\S+))?(?:\s*\|\s*(.+))?\s*$/;
89
90
  const ROLE_ASSIGNMENT_RE = /^([^:]+):\s*(.*)$/;
90
91
 
91
92
  /**
@@ -382,10 +383,10 @@ export function parseRaci(
382
383
  // Strip a possible trailing comma (user habit tolerance,
383
384
  // matches `collectIndentedValues`).
384
385
  const stripped = nextTrim.replace(/,\s*$/, '');
385
- // Optional pipe metadata: `Cap | color: blue` — matches
386
- // every modern chart-type's per-element styling form.
386
+ // Optional pipe metadata: `Cap | color: blue` — long form.
387
+ // Optional trailing-token shortcut: `Cap blue` — short form (§1.5).
387
388
  const segments = stripped.split('|').map((s) => s.trim());
388
- const roleLabel = segments[0] ?? '';
389
+ let roleLabel = segments[0] ?? '';
389
390
  let roleColor: string | undefined;
390
391
  if (segments.length > 1) {
391
392
  const meta = parsePipeMetadata(segments);
@@ -398,6 +399,20 @@ export function parseRaci(
398
399
  );
399
400
  }
400
401
  }
402
+ // Apply shortcut only when pipe metadata didn't already set color.
403
+ if (!roleColor) {
404
+ const { label: stripLabel, colorName: shortcutColor } =
405
+ peelTrailingColorName(roleLabel);
406
+ if (shortcutColor) {
407
+ roleColor = resolveColorWithDiagnostic(
408
+ shortcutColor,
409
+ j + 1,
410
+ result.diagnostics,
411
+ palette
412
+ );
413
+ roleLabel = stripLabel;
414
+ }
415
+ }
401
416
  if (roleLabel) getOrAddRole(roleLabel, j + 1, roleColor);
402
417
  }
403
418
  i = j - 1; // outer loop's i++ lands on the first non-block line
@@ -469,10 +484,13 @@ export function parseRaci(
469
484
  errorAt(lineNumber, 'Phase label is empty.');
470
485
  continue;
471
486
  }
472
- // Optional pipe metadata: `[Voyage] | color: blue`.
487
+ // PHASE_RE captures: 1=label, 2=optional trailing-token color, 3=pipe meta.
488
+ // Long pipe form (`[Voyage] | color: blue`) wins over the shortcut.
473
489
  let phaseColor: string | undefined;
474
- if (phaseMatch[2]) {
475
- const meta = parsePipeMetadata(['', phaseMatch[2]]);
490
+ const trailingToken = phaseMatch[2];
491
+ const pipeMeta = phaseMatch[3];
492
+ if (pipeMeta) {
493
+ const meta = parsePipeMetadata(['', pipeMeta]);
476
494
  if (meta['color']) {
477
495
  phaseColor = resolveColorWithDiagnostic(
478
496
  meta['color'],
@@ -482,6 +500,18 @@ export function parseRaci(
482
500
  );
483
501
  }
484
502
  }
503
+ if (!phaseColor && trailingToken) {
504
+ // Trailing token must be a recognized color word, or it's a parse error.
505
+ const { colorName } = peelTrailingColorName(`x ${trailingToken}`);
506
+ if (colorName) {
507
+ phaseColor = resolveColorWithDiagnostic(
508
+ colorName,
509
+ lineNumber,
510
+ result.diagnostics,
511
+ palette
512
+ );
513
+ }
514
+ }
485
515
  currentPhase = {
486
516
  id: normalizeName(display),
487
517
  displayName: display,
@@ -602,7 +602,8 @@ export function renderRaci(
602
602
  parsed.roles.forEach((roleId, i) => {
603
603
  const cx = roleX(i) + COLUMN_INSET;
604
604
  const cw = roleColW - 2 * COLUMN_INSET;
605
- // Per-role color from `Cap(blue)` syntax. When the user provides
605
+ // Per-role color from `Cap blue` trailing-token (or `Cap | color: blue`)
606
+ // syntax. When the user provides
606
607
  // one, it wins; otherwise rotate through marker-safe accents so
607
608
  // each column has a subtle visual identity instead of every column
608
609
  // reading as the same neutral gray.
package/src/raci/types.ts CHANGED
@@ -67,9 +67,10 @@ export interface ParsedRaci {
67
67
  /** Display name for each role (parallel to `roles`). */
68
68
  roleDisplayNames: string[];
69
69
  /**
70
- * Optional per-role palette color from `Cap(blue)` suffix in the
71
- * roles block. Parallel to `roles`; entries default to `undefined`
72
- * (renderer falls back to the neutral column tint).
70
+ * Optional per-role palette color from the `Cap blue` trailing-token
71
+ * suffix in the roles block (or the long pipe form `Cap | color: blue`).
72
+ * Parallel to `roles`; entries default to `undefined` (renderer falls
73
+ * back to the neutral column tint).
73
74
  */
74
75
  roleColors: Array<string | undefined>;
75
76
  phases: RaciPhase[];
@@ -8,6 +8,7 @@ import {
8
8
  measureIndent,
9
9
  parseFirstLine,
10
10
  parsePipeMetadata,
11
+ peelTrailingColorName,
11
12
  tryParseSharedOption,
12
13
  PIPE_KEY_VALUE_PREFIX_RE,
13
14
  PIPE_LIKELY_STRUCTURED_TAIL_RE,
@@ -175,6 +176,17 @@ export function parseRing(content: string): ParsedRing {
175
176
  continue;
176
177
  }
177
178
 
179
+ // Universal trailing-token shortcut: `Label color` equivalent to
180
+ // `Label | color: <name>` when color is not already set (§1.5).
181
+ if (!color) {
182
+ const { label: stripped, colorName: shortcutColor } =
183
+ peelTrailingColorName(label);
184
+ if (shortcutColor) {
185
+ color = shortcutColor;
186
+ label = stripped;
187
+ }
188
+ }
189
+
178
190
  currentLayer = {
179
191
  label,
180
192
  lineNumber: lineNum,
@@ -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 });
@@ -2762,7 +2762,7 @@ export function renderSequenceDiagram(
2762
2762
  const legendConfig: LegendConfig = {
2763
2763
  groups: resolvedGroups,
2764
2764
  position: { placement: 'top-center', titleRelation: 'below-title' },
2765
- mode: 'fixed',
2765
+ mode: 'preview',
2766
2766
  };
2767
2767
  const legendState: LegendState = {
2768
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
  });