@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
package/src/er/parser.ts CHANGED
@@ -46,15 +46,15 @@ function tableId(name: string): string {
46
46
  // Regex patterns
47
47
  // ============================================================
48
48
 
49
- // Table declaration: name or name (color) or name | key: value
50
- // Multi-word names allowed; quote `"name with reserved chars"` if the name
51
- // contains pipe / paren / colon. Captures:
49
+ // Table declaration: `name`, `name color` (trailing-token §1.5), or
50
+ // `name | key: value`. Multi-word names allowed; quote `"name with reserved
51
+ // chars"` if the name contains pipe / paren / colon. Captures:
52
52
  // 1: quoted-name content (without surrounding quotes), or undefined
53
53
  // 2: bare-name (trimmed at call site), or undefined
54
- // 3: color (inside parens), or undefined
54
+ // 3: trailing-token color (recognized palette word), or undefined
55
55
  // 4: pipe metadata (without leading `|`), or undefined
56
56
  const TABLE_DECL_RE =
57
- /^(?:"([^"]+)"|([a-zA-Z_][^|":(]*?))(?:\s*\(([^)]+)\))?(?:\s*\|(.+))?$/;
57
+ /^(?:"([^"]+)"|([a-zA-Z_][^|":(]*?))(?:\s+(red|orange|yellow|green|blue|purple|teal|cyan|gray|black|white))?(?:\s*\|(.+))?$/;
58
58
 
59
59
  // Column: name [type] [constraints...] — space-separated, no colon, no brackets
60
60
  // First token is always the name. Second token is the type if it's not a constraint keyword.
@@ -350,7 +350,7 @@ export function parseERDiagram(
350
350
  result.diagnostics.push(
351
351
  makeDgmoError(
352
352
  lineNumber,
353
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
353
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`,
354
354
  'warning'
355
355
  )
356
356
  );
@@ -494,7 +494,7 @@ export function parseERDiagram(
494
494
  if (result.tables.length === 0 && !result.error) {
495
495
  const diag = makeDgmoError(
496
496
  1,
497
- 'No tables found. Add table declarations like "users" or "orders (blue)".'
497
+ 'No tables found. Add table declarations like "users" or "orders blue".'
498
498
  );
499
499
  result.diagnostics.push(diag);
500
500
  result.error = formatDgmoError(diag);
@@ -632,15 +632,22 @@ export function extractSymbols(docText: string): DiagramSymbols {
632
632
  for (const rawLine of docText.split('\n')) {
633
633
  const line = rawLine.trim();
634
634
  if (inMetadata && /^er(\s|$)/i.test(line)) continue;
635
- if (inMetadata && OPTION_NOCOLON_RE.test(line)) continue; // option line
636
- inMetadata = false;
637
- if (line.length === 0) continue;
635
+ // Under §1.5 trailing-token, `Users blue` matches OPTION_NOCOLON_RE
636
+ // (key=Users, value=blue) but is actually a table with a color.
637
+ // Detect tables FIRST so they aren't swallowed by the option fallback.
638
638
  if (/^\s/.test(rawLine)) continue; // indented = column definition, not table
639
+ if (line.length === 0) continue;
639
640
  const m = TABLE_DECL_RE.exec(line);
640
641
  if (m) {
641
642
  const name = (m[1] ?? m[2] ?? '').trim();
642
- if (name) entities.push(name);
643
+ if (name) {
644
+ inMetadata = false;
645
+ entities.push(name);
646
+ continue;
647
+ }
643
648
  }
649
+ if (inMetadata && OPTION_NOCOLON_RE.test(line)) continue; // option line
650
+ inMetadata = false;
644
651
  }
645
652
  return {
646
653
  kind: 'er',
@@ -225,7 +225,8 @@ export function renderERDiagram(
225
225
  exportDims?: { width?: number; height?: number },
226
226
  activeTagGroup?: string | null,
227
227
  /** When false, semantic role colors are suppressed and entities use a neutral color. */
228
- semanticColorsActive?: boolean
228
+ semanticColorsActive?: boolean,
229
+ exportMode?: boolean
229
230
  ): void {
230
231
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
231
232
 
@@ -560,7 +561,7 @@ export function renderERDiagram(
560
561
  const legendConfig: LegendConfig = {
561
562
  groups: parsed.tagGroups,
562
563
  position: { placement: 'top-center', titleRelation: 'below-title' },
563
- mode: 'fixed',
564
+ mode: exportMode ? 'export' : 'preview',
564
565
  };
565
566
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
566
567
  const legendG = svg
@@ -602,7 +603,7 @@ export function renderERDiagram(
602
603
  const legendConfig: LegendConfig = {
603
604
  groups: semanticGroups,
604
605
  position: { placement: 'top-center', titleRelation: 'below-title' },
605
- mode: 'fixed',
606
+ mode: exportMode ? 'export' : 'preview',
606
607
  };
607
608
  const legendState: LegendState = {
608
609
  activeGroup: semanticActive ? 'Role' : null,
@@ -653,10 +654,21 @@ export function renderERDiagramForExport(
653
654
  document.body.appendChild(container);
654
655
 
655
656
  try {
656
- renderERDiagram(container, parsed, layout, palette, isDark, undefined, {
657
- width: exportWidth,
658
- height: exportHeight,
659
- });
657
+ renderERDiagram(
658
+ container,
659
+ parsed,
660
+ layout,
661
+ palette,
662
+ isDark,
663
+ undefined,
664
+ {
665
+ width: exportWidth,
666
+ height: exportHeight,
667
+ },
668
+ undefined,
669
+ undefined,
670
+ true
671
+ );
660
672
 
661
673
  const svgEl = container.querySelector('svg');
662
674
  if (!svgEl) return '';
@@ -380,7 +380,7 @@ export function parseGantt(
380
380
  currentTagGroup = null;
381
381
  // fall through to process this line normally
382
382
  } else {
383
- // Parse tag entry: `Value(color)` or `Value`
383
+ // Parse tag entry: `Value color` or `Value`
384
384
  // First entry is the default unless another is marked `default`
385
385
  if (COMMENT_RE.test(line)) continue;
386
386
  const { text: cleanEntry, isDefault } = stripDefaultModifier(line);
@@ -208,6 +208,7 @@ export interface GanttInteractiveOptions {
208
208
  collapsedLanes?: Set<string>;
209
209
  onToggleLane?: (laneName: string) => void;
210
210
  viewMode?: boolean;
211
+ exportMode?: boolean;
211
212
  }
212
213
 
213
214
  // ── Main Renderer ───────────────────────────────────────────
@@ -439,7 +440,8 @@ export function renderGantt(
439
440
  ).attr('display', active ? null : 'none');
440
441
  }
441
442
  drawLegend();
442
- }
443
+ },
444
+ options?.exportMode ?? false
443
445
  );
444
446
  }
445
447
  }
@@ -1983,7 +1985,8 @@ function renderTagLegend(
1983
1985
  controlsExpanded = false,
1984
1986
  hasDependencies = false,
1985
1987
  dependenciesActive = false,
1986
- onControlsToggle?: (toggleId: string, active: boolean) => void
1988
+ onControlsToggle?: (toggleId: string, active: boolean) => void,
1989
+ exportMode = false
1987
1990
  ): void {
1988
1991
  // Build visible groups: active group expanded + swimlane group as compact pill
1989
1992
  let visibleGroups: TagGroup[];
@@ -2117,7 +2120,7 @@ function renderTagLegend(
2117
2120
  placement: 'top-center' as const,
2118
2121
  titleRelation: 'below-title' as const,
2119
2122
  },
2120
- mode: 'fixed' as const,
2123
+ mode: exportMode ? 'export' : 'preview',
2121
2124
  capsulePillAddonWidth: iconReserve,
2122
2125
  controlsGroup:
2123
2126
  controlsToggles.length > 0 ? { toggles: controlsToggles } : undefined,
@@ -2263,7 +2266,7 @@ function renderTagLegend(
2263
2266
  placement: 'top-center' as const,
2264
2267
  titleRelation: 'below-title' as const,
2265
2268
  },
2266
- mode: 'fixed' as const,
2269
+ mode: exportMode ? 'export' : 'preview',
2267
2270
  controlsGroup: { toggles: controlsToggles },
2268
2271
  };
2269
2272
 
@@ -1,4 +1,3 @@
1
- import { resolveColorWithDiagnostic } from '../colors';
2
1
  import type { DgmoError } from '../diagnostics';
3
2
  import type { PaletteColors } from '../palettes';
4
3
  import {
@@ -8,10 +7,9 @@ import {
8
7
  NAME_DIAGNOSTIC_CODES,
9
8
  nameMergedMessage,
10
9
  } from '../diagnostics';
11
- import { parseInArrowLabel, matchColorParens } from '../utils/arrows';
10
+ import { parseInArrowLabel } from '../utils/arrows';
12
11
  import {
13
12
  measureIndent,
14
- inferArrowColor,
15
13
  parseFirstLine,
16
14
  OPTION_NOCOLON_RE,
17
15
  ALL_CHART_TYPES,
@@ -90,9 +88,8 @@ function parseNodeRef(text: string): NodeRef | null {
90
88
 
91
89
  /**
92
90
  * Split a line into segments around arrow tokens.
93
- * Arrows: `->`, `-label->`, `-(color)->`, `-label(color)->`, and long-dash
94
- * variants like `-->`, `--->`, `--foo--->` (TD-9 longest-match: the arrow
95
- * token is the maximal run of `-+>`).
91
+ * Arrows: `->`, `-label->`, and long-dash variants like `-->`, `--->`,
92
+ * `--foo--->` (TD-9 longest-match: the arrow token is the maximal run of `-+>`).
96
93
  *
97
94
  * Returns alternating: [nodeText, arrowText, nodeText, arrowText, nodeText, ...]
98
95
  * Where arrowText is the synthesized full arrow token like `-yes->` or `->`
@@ -105,7 +102,6 @@ function splitArrows(line: string): string[] {
105
102
  start: number;
106
103
  end: number;
107
104
  label?: string;
108
- color?: string;
109
105
  }[] = [];
110
106
 
111
107
  // Find all arrow tokens. A token is a maximal run of `-+>` (one-or-more
@@ -132,7 +128,6 @@ function splitArrows(line: string): string[] {
132
128
  // the label; the full arrow token runs from opening through `>`.
133
129
  let arrowStart: number;
134
130
  let label: string | undefined;
135
- let color: string | undefined;
136
131
 
137
132
  let openingStart = -1;
138
133
  for (let i = scanFloor; i < runStart; i++) {
@@ -151,16 +146,9 @@ function splitArrows(line: string): string[] {
151
146
  while (openingEnd < runStart && line[openingEnd] === '-') openingEnd++;
152
147
 
153
148
  // Label content = everything between opening run and the arrow run.
154
- const arrowContent = line.substring(openingEnd, runStart);
155
- const colorMatch = arrowContent.match(/\(([^)]+)\)\s*$/);
156
- if (colorMatch) {
157
- color = colorMatch[1].trim();
158
- const labelPart = arrowContent.substring(0, colorMatch.index!).trim();
159
- if (labelPart) label = labelPart;
160
- } else {
161
- const labelPart = arrowContent.trim();
162
- if (labelPart) label = labelPart;
163
- }
149
+ // Edges have no color slot (§1.7); parens stay literal.
150
+ const labelPart = line.substring(openingEnd, runStart).trim();
151
+ if (labelPart) label = labelPart;
164
152
  arrowStart = openingStart;
165
153
  } else {
166
154
  // No opening dash run found. All absorbed leftward dashes belong to
@@ -168,7 +156,7 @@ function splitArrows(line: string): string[] {
168
156
  arrowStart = runStart;
169
157
  }
170
158
 
171
- arrowPositions.push({ start: arrowStart, end: arrowEnd, label, color });
159
+ arrowPositions.push({ start: arrowStart, end: arrowEnd, label });
172
160
  searchFrom = arrowEnd;
173
161
  scanFloor = arrowEnd;
174
162
  }
@@ -179,12 +167,9 @@ function splitArrows(line: string): string[] {
179
167
 
180
168
  // Build segments.
181
169
  //
182
- // NOTE: the synthesized arrow token is always the short form (`->`,
183
- // `-label->`, `-(color)->`). The actual dash run-length (`-->`, `--->`,
184
- // `---->`) seen in the source is collapsed here. If we ever add
185
- // dash-length-sensitive edge styling (e.g. Mermaid-style "long arrow"
186
- // emphasis), thread `arrow.end - arrow.start - label?.length - color?.length`
187
- // through to ArrowInfo so downstream renderers can honor it.
170
+ // NOTE: the synthesized arrow token is always the short form (`->` or
171
+ // `-label->`). The actual dash run-length (`-->`, `--->`, `---->`) seen
172
+ // in the source is collapsed here.
188
173
  let lastIndex = 0;
189
174
  for (let i = 0; i < arrowPositions.length; i++) {
190
175
  const arrow = arrowPositions[i];
@@ -192,12 +177,7 @@ function splitArrows(line: string): string[] {
192
177
  if (beforeText || i === 0) {
193
178
  segments.push(beforeText);
194
179
  }
195
- // Arrow marker
196
- let arrowToken = '->';
197
- if (arrow.label && arrow.color)
198
- arrowToken = `-${arrow.label}(${arrow.color})->`;
199
- else if (arrow.label) arrowToken = `-${arrow.label}->`;
200
- else if (arrow.color) arrowToken = `-(${arrow.color})->`;
180
+ const arrowToken = arrow.label ? `-${arrow.label}->` : '->';
201
181
  segments.push(arrowToken);
202
182
  lastIndex = arrow.end;
203
183
  }
@@ -212,61 +192,23 @@ function splitArrows(line: string): string[] {
212
192
 
213
193
  interface ArrowInfo {
214
194
  label?: string;
215
- color?: string;
216
195
  }
217
196
 
218
197
  function parseArrowToken(
219
198
  token: string,
220
- palette: PaletteColors | undefined,
199
+ _palette: PaletteColors | undefined,
221
200
  lineNumber: number,
222
201
  diagnostics: DgmoError[]
223
202
  ): ArrowInfo {
224
203
  if (token === '->') return {};
225
- // TD-11: `-(X)->` is a color if and only if `X` is one of the 11 recognized
226
- // palette color names. Otherwise the entire `(X)` becomes the label.
227
- // Delegate the recognition rule to the shared `matchColorParens` helper.
228
- const bareParen = token.match(/^-(\([A-Za-z]+\))->$/);
229
- if (bareParen) {
230
- const colorName = matchColorParens(bareParen[1]);
231
- if (colorName) {
232
- return {
233
- color: resolveColorWithDiagnostic(
234
- colorName,
235
- lineNumber,
236
- diagnostics,
237
- palette
238
- ),
239
- };
240
- }
241
- // Unrecognized color name → whole `(X)` is the label (fall through).
242
- }
243
- // -label(color)-> or -label->
244
- const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
204
+ // Edges have no color slot (spec §1.7 "Edge color is not a feature"). The
205
+ // whole content between `-` and `->` is the label, including parens.
206
+ const m = token.match(/^-(.+?)->$/);
245
207
  if (m) {
246
208
  const rawLabel = m[1] ?? '';
247
- // Route label through TD-13/TD-14 validator.
248
209
  const labelResult = parseInArrowLabel(rawLabel, lineNumber);
249
210
  diagnostics.push(...labelResult.diagnostics);
250
- const label = labelResult.label;
251
- let color = m[2]
252
- ? resolveColorWithDiagnostic(
253
- m[2].trim(),
254
- lineNumber,
255
- diagnostics,
256
- palette
257
- )
258
- : undefined;
259
- if (label && !color) {
260
- const inferred = inferArrowColor(label);
261
- if (inferred)
262
- color = resolveColorWithDiagnostic(
263
- inferred,
264
- lineNumber,
265
- diagnostics,
266
- palette
267
- );
268
- }
269
- return { label, color };
211
+ return { label: labelResult.label };
270
212
  }
271
213
  return {};
272
214
  }
@@ -362,15 +304,13 @@ export function parseFlowchart(
362
304
  sourceId: string,
363
305
  targetId: string,
364
306
  lineNumber: number,
365
- label?: string,
366
- color?: string
307
+ label?: string
367
308
  ): void {
368
309
  const edge: GraphEdge = {
369
310
  source: sourceId,
370
311
  target: targetId,
371
312
  lineNumber,
372
313
  ...(label && { label }),
373
- ...(color && { color }),
374
314
  };
375
315
  result.edges.push(edge);
376
316
  }
@@ -471,13 +411,7 @@ export function parseFlowchart(
471
411
  if (pendingArrow !== null) {
472
412
  const sourceId = lastNodeId ?? implicitSourceId;
473
413
  if (sourceId) {
474
- addEdge(
475
- sourceId,
476
- node.id,
477
- lineNumber,
478
- pendingArrow.label,
479
- pendingArrow.color
480
- );
414
+ addEdge(sourceId, node.id, lineNumber, pendingArrow.label);
481
415
  }
482
416
  pendingArrow = null;
483
417
  } else if (lastNodeId === null && implicitSourceId === null) {
@@ -447,11 +447,8 @@ export function renderFlowchart(
447
447
  .attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
448
448
  .attr('fill', palette.textMuted);
449
449
 
450
- // Collect unique edge colors for custom markers
450
+ // Edges have no color slot (§1.7); keep empty set for marker iteration.
451
451
  const edgeColors = new Set<string>();
452
- for (const edge of layout.edges) {
453
- if (edge.color) edgeColors.add(edge.color);
454
- }
455
452
  for (const color of edgeColors) {
456
453
  const id = `fc-arrow-${color.replace('#', '')}`;
457
454
  defs
@@ -569,10 +566,8 @@ export function renderFlowchart(
569
566
  .attr('class', 'fc-edge-group')
570
567
  .attr('data-line-number', String(edge.lineNumber));
571
568
 
572
- const edgeColor = edge.color ?? palette.textMuted;
573
- const markerId = edge.color
574
- ? `fc-arrow-${edge.color.replace('#', '')}`
575
- : 'fc-arrow';
569
+ const edgeColor = palette.textMuted;
570
+ const markerId = 'fc-arrow';
576
571
 
577
572
  const pathD = lineGenerator(edge.points);
578
573
  if (pathD) {
@@ -25,7 +25,6 @@ export interface LayoutEdge {
25
25
  target: string;
26
26
  points: { x: number; y: number }[];
27
27
  label?: string;
28
- color?: string;
29
28
  lineNumber: number;
30
29
  }
31
30
 
@@ -179,7 +178,6 @@ export function layoutGraph(
179
178
  target: edge.target,
180
179
  points: edgeData?.points ?? [],
181
180
  label: edge.label,
182
- color: edge.color,
183
181
  lineNumber: edge.lineNumber,
184
182
  };
185
183
  });
@@ -8,7 +8,7 @@ import {
8
8
  NAME_DIAGNOSTIC_CODES,
9
9
  nameMergedMessage,
10
10
  } from '../diagnostics';
11
- import { parseInArrowLabel, matchColorParens } from '../utils/arrows';
11
+ import { parseInArrowLabel } from '../utils/arrows';
12
12
  import {
13
13
  measureIndent,
14
14
  parseFirstLine,
@@ -26,7 +26,10 @@ import type { ParsedGraph, GraphNode, GraphGroup } from './types';
26
26
  const PSEUDOSTATE_ID = 'pseudostate:[*]';
27
27
  const PSEUDOSTATE_LABEL = '[*]';
28
28
 
29
- const GROUP_BRACKET_RE = /^\[([^\]]+)\](?:\(([^)]+)\))?\s*$/;
29
+ // `[Group]` or `[Group] color` (universal §1.5 trailing-token).
30
+ // Color (group 2) must be a recognized lowercase palette word.
31
+ const GROUP_BRACKET_RE =
32
+ /^\[([^\]]+)\](?:\s+(red|orange|yellow|green|blue|purple|teal|cyan|gray|black|white))?\s*$/;
30
33
 
31
34
  // ============================================================
32
35
  // Arrow splitter
@@ -36,7 +39,7 @@ const GROUP_BRACKET_RE = /^\[([^\]]+)\](?:\(([^)]+)\))?\s*$/;
36
39
  * Split a line on `->` arrows, returning alternating segments:
37
40
  * [nodeText, arrowToken, nodeText, ...]
38
41
  *
39
- * Arrows: `->`, `-label->`, `-(color)->`, `-label(color)->`
42
+ * Arrows: `->`, `-label->`. Edges have no color slot (spec §1.7).
40
43
  */
41
44
  function splitArrows(line: string): string[] {
42
45
  // Mirrors flowchart-parser.ts splitArrows. TD-9 longest-match: arrow token
@@ -46,7 +49,6 @@ function splitArrows(line: string): string[] {
46
49
  start: number;
47
50
  end: number;
48
51
  label?: string;
49
- color?: string;
50
52
  }[] = [];
51
53
 
52
54
  let searchFrom = 0;
@@ -61,7 +63,6 @@ function splitArrows(line: string): string[] {
61
63
 
62
64
  let arrowStart: number;
63
65
  let label: string | undefined;
64
- let color: string | undefined;
65
66
 
66
67
  let openingStart = -1;
67
68
  for (let i = scanFloor; i < runStart; i++) {
@@ -78,22 +79,14 @@ function splitArrows(line: string): string[] {
78
79
  let openingEnd = openingStart;
79
80
  while (openingEnd < runStart && line[openingEnd] === '-') openingEnd++;
80
81
 
81
- const arrowContent = line.substring(openingEnd, runStart);
82
- const colorMatch = arrowContent.match(/\(([^)]+)\)\s*$/);
83
- if (colorMatch) {
84
- color = colorMatch[1].trim();
85
- const labelPart = arrowContent.substring(0, colorMatch.index!).trim();
86
- if (labelPart) label = labelPart;
87
- } else {
88
- const labelPart = arrowContent.trim();
89
- if (labelPart) label = labelPart;
90
- }
82
+ const labelPart = line.substring(openingEnd, runStart).trim();
83
+ if (labelPart) label = labelPart;
91
84
  arrowStart = openingStart;
92
85
  } else {
93
86
  arrowStart = runStart;
94
87
  }
95
88
 
96
- arrowPositions.push({ start: arrowStart, end: arrowEnd, label, color });
89
+ arrowPositions.push({ start: arrowStart, end: arrowEnd, label });
97
90
  searchFrom = arrowEnd;
98
91
  scanFloor = arrowEnd;
99
92
  }
@@ -106,11 +99,7 @@ function splitArrows(line: string): string[] {
106
99
  const beforeText = line.substring(lastIndex, arrow.start).trim();
107
100
  if (beforeText || i === 0) segments.push(beforeText);
108
101
 
109
- let arrowToken = '->';
110
- if (arrow.label && arrow.color)
111
- arrowToken = `-${arrow.label}(${arrow.color})->`;
112
- else if (arrow.label) arrowToken = `-${arrow.label}->`;
113
- else if (arrow.color) arrowToken = `-(${arrow.color})->`;
102
+ const arrowToken = arrow.label ? `-${arrow.label}->` : '->';
114
103
  segments.push(arrowToken);
115
104
  lastIndex = arrow.end;
116
105
  }
@@ -122,49 +111,23 @@ function splitArrows(line: string): string[] {
122
111
 
123
112
  interface ArrowInfo {
124
113
  label?: string;
125
- color?: string;
126
114
  }
127
115
 
128
116
  function parseArrowToken(
129
117
  token: string,
130
- palette: PaletteColors | undefined,
118
+ _palette: PaletteColors | undefined,
131
119
  lineNumber: number,
132
120
  diagnostics: DgmoError[]
133
121
  ): ArrowInfo {
134
122
  if (token === '->') return {};
135
- // TD-11: `-(X)->` is a color if and only if X is a recognized palette
136
- // color; otherwise the whole `(X)` becomes the label. Delegate recognition
137
- // to the shared `matchColorParens` helper.
138
- const bareParen = token.match(/^-(\([A-Za-z]+\))->$/);
139
- if (bareParen) {
140
- const colorName = matchColorParens(bareParen[1]);
141
- if (colorName) {
142
- return {
143
- color: resolveColorWithDiagnostic(
144
- colorName,
145
- lineNumber,
146
- diagnostics,
147
- palette
148
- ),
149
- };
150
- }
151
- // fall through — whole `(X)` becomes label
152
- }
153
- const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
123
+ // Edges have no color slot (§1.7); arrow content between `-` and `->`
124
+ // is pure label text.
125
+ const m = token.match(/^-(.+?)->$/);
154
126
  if (m) {
155
127
  const rawLabel = m[1] ?? '';
156
128
  const labelResult = parseInArrowLabel(rawLabel, lineNumber);
157
129
  diagnostics.push(...labelResult.diagnostics);
158
- const label = labelResult.label;
159
- const color = m[2]
160
- ? resolveColorWithDiagnostic(
161
- m[2].trim(),
162
- lineNumber,
163
- diagnostics,
164
- palette
165
- )
166
- : undefined;
167
- return { label, color };
130
+ return { label: labelResult.label };
168
131
  }
169
132
  return {};
170
133
  }
@@ -292,15 +255,13 @@ export function parseState(
292
255
  sourceId: string,
293
256
  targetId: string,
294
257
  lineNumber: number,
295
- label?: string,
296
- color?: string
258
+ label?: string
297
259
  ): void {
298
260
  result.edges.push({
299
261
  source: sourceId,
300
262
  target: targetId,
301
263
  lineNumber,
302
264
  ...(label && { label }),
303
- ...(color && { color }),
304
265
  });
305
266
  }
306
267
 
@@ -494,13 +455,7 @@ export function parseState(
494
455
  // Use explicit source if available, else implicit from indent
495
456
  const sourceId = lastNodeId ?? implicitSourceId;
496
457
  if (sourceId) {
497
- addEdge(
498
- sourceId,
499
- node.id,
500
- lineNumber,
501
- pendingArrow.label,
502
- pendingArrow.color
503
- );
458
+ addEdge(sourceId, node.id, lineNumber, pendingArrow.label);
504
459
  }
505
460
  pendingArrow = null;
506
461
  }
@@ -144,11 +144,8 @@ export function renderState(
144
144
  .attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
145
145
  .attr('fill', palette.textMuted);
146
146
 
147
- // Custom colored markers
147
+ // Edges have no color slot (§1.7); keep empty set for marker iteration.
148
148
  const edgeColors = new Set<string>();
149
- for (const edge of layout.edges) {
150
- if (edge.color) edgeColors.add(edge.color);
151
- }
152
149
  for (const color of edgeColors) {
153
150
  const id = `st-arrow-${color.replace('#', '')}`;
154
151
  defs
@@ -345,10 +342,8 @@ export function renderState(
345
342
  .attr('class', 'st-edge-group')
346
343
  .attr('data-line-number', String(edge.lineNumber));
347
344
 
348
- const edgeColor = edge.color ?? palette.textMuted;
349
- const markerId = edge.color
350
- ? `st-arrow-${edge.color.replace('#', '')}`
351
- : 'st-arrow';
345
+ const edgeColor = palette.textMuted;
346
+ const markerId = 'st-arrow';
352
347
 
353
348
  if (edge.source === edge.target) {
354
349
  // Self-loop
@@ -14,7 +14,6 @@ import {
14
14
  nameMergedMessage,
15
15
  } from '../diagnostics';
16
16
  import { tryStripDescriptionKeyword } from '../utils/description-helpers';
17
- import { resolveColorWithDiagnostic } from '../colors';
18
17
  import { parseInArrowLabel } from '../utils/arrows';
19
18
  import {
20
19
  measureIndent,
@@ -22,6 +21,7 @@ import {
22
21
  OPTION_NOCOLON_RE,
23
22
  tryParseSharedOption,
24
23
  } from '../utils/parsing';
24
+ import { isRecognizedColorName } from '../colors';
25
25
  import { normalizeName, displayName } from '../utils/name-normalize';
26
26
  import {
27
27
  matchTagBlockHeading,
@@ -62,9 +62,11 @@ const DEPRECATED_FANOUT_RE = /\bx(\d+)\s*$/;
62
62
  const GROUP_RE =
63
63
  /^\[([^\]]+)\]\s*(?:as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*)?(?:\|\s*(.+))?$/;
64
64
 
65
- // Tag value: Name or Name(color)
66
- // Note: `default` keyword removed first value is the default.
67
- const TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?\s*$/;
65
+ // Tag value: `Name` or `Name color` (trailing-token color form). Color is
66
+ // extracted via the shared `extractColor` helper at use-site (see
67
+ // `dgmo/src/utils/parsing.ts:extractColor`), not via this regex. This regex
68
+ // just confirms the line shape is a valid tag value (no reserved sigils).
69
+ const TAG_VALUE_RE = /^(\w[\w\s]+?)\s*$/;
68
70
 
69
71
  // Component line. Accepts either a quoted name ("name with | : reserved chars")
70
72
  // or a bare name (multi-word allowed; must start with letter/underscore so digit-
@@ -445,14 +447,22 @@ export function parseInfra(content: string): ParsedInfra {
445
447
  // Tag value inside tag group — first value is the default unless another is marked `default`
446
448
  if (currentTagGroup && indent > 0) {
447
449
  const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
448
- const tvMatch = cleanEntry.match(TAG_VALUE_RE);
449
- if (tvMatch) {
450
- const valueName = tvMatch[1].trim();
451
- const rawColor = tvMatch[2]?.trim();
452
- if (rawColor) {
453
- // Validate the color name; emit diagnostic if invalid
454
- resolveColorWithDiagnostic(rawColor, lineNumber, result.diagnostics);
450
+ // Trailing-token color (universal rule, §1.5): peel off a lowercase
451
+ // recognized color word from the end of the line. Downstream stores
452
+ // the raw color NAME (not the palette hex) so the renderer can resolve
453
+ // against whichever theme/palette is active at render time.
454
+ const lastSpaceIdx = cleanEntry.lastIndexOf(' ');
455
+ let valueName = cleanEntry;
456
+ let rawColor: string | undefined;
457
+ if (lastSpaceIdx > 0) {
458
+ const trailing = cleanEntry.substring(lastSpaceIdx + 1);
459
+ if (isRecognizedColorName(trailing)) {
460
+ rawColor = trailing;
461
+ valueName = cleanEntry.substring(0, lastSpaceIdx).trimEnd();
455
462
  }
463
+ }
464
+ const tvMatch = valueName.match(TAG_VALUE_RE);
465
+ if (tvMatch || /^\w+$/.test(valueName)) {
456
466
  currentTagGroup.values.push({
457
467
  name: valueName,
458
468
  color: rawColor,