@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
@@ -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) {
@@ -663,7 +597,7 @@ export function looksLikeFlowchart(content: string): boolean {
663
597
  // Symbol extraction (for completion API)
664
598
  // ============================================================
665
599
 
666
- import type { DiagramSymbols } from '../completion';
600
+ import type { DiagramSymbols } from '../completion-types';
667
601
 
668
602
  // Node ID: identifier at line start followed by a shape delimiter or space (arrow line)
669
603
  const NODE_ID_RE = /^([a-zA-Z_][\w-]*)[\s([</{]/;
@@ -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) {
@@ -623,7 +618,7 @@ export function renderFlowchart(
623
618
  }
624
619
 
625
620
  // Render nodes (top layer)
626
- const colorOff = graph.options?.color === 'off';
621
+ const colorOff = graph.options?.['color'] === 'off';
627
622
  const solid = graph.options?.['solid-fill'] === 'on';
628
623
  for (const node of layout.nodes) {
629
624
  const nodeG = contentG
@@ -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
@@ -431,7 +426,7 @@ export function renderState(
431
426
  }
432
427
 
433
428
  // Render nodes (top layer)
434
- const colorOff = graph.options?.color === 'off';
429
+ const colorOff = graph.options?.['color'] === 'off';
435
430
  const solid = graph.options?.['solid-fill'] === 'on';
436
431
  for (const node of layout.nodes) {
437
432
  const isCollapsedGroup = collapsedGroupIds.has(node.id);
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { render as renderInternal } from './render';
13
13
  import {
14
14
  encodeDiagramUrl as encodeDiagramUrlInternal,
15
15
  decodeDiagramUrl as decodeDiagramUrlInternal,
16
+ type CompactViewState,
16
17
  } from './sharing';
17
18
  import { parseDgmo as validate } from './dgmo-router';
18
19
  import { palettes, getPalette } from './palettes';
@@ -20,6 +21,8 @@ import type { PaletteConfig } from './palettes/types';
20
21
  import type { Theme } from './themes';
21
22
  import { formatDgmoError, type DgmoError } from './diagnostics';
22
23
 
24
+ export type { CompactViewState } from './sharing';
25
+
23
26
  // ============================================================
24
27
  // render(text, options?)
25
28
  // ============================================================
@@ -34,6 +37,12 @@ export interface RenderOptions {
34
37
  * 'throw' — throw an Error with the diagnostics
35
38
  */
36
39
  onError?: 'svg' | 'silent' | 'throw';
40
+ /**
41
+ * Pre-applied interactive view state — collapsed sections/columns,
42
+ * active swimlane tag-group, etc. Used to render a specific view
43
+ * non-interactively (server-side render, share-link decode).
44
+ */
45
+ viewState?: CompactViewState;
37
46
  }
38
47
 
39
48
  export interface RenderResult {
@@ -65,6 +74,7 @@ export async function render(
65
74
  const result = await renderInternal(text, {
66
75
  theme: options?.theme,
67
76
  palette: palette.id,
77
+ viewState: options?.viewState,
68
78
  });
69
79
 
70
80
  const errors = result.diagnostics.filter((d) => d.severity === 'error');
@@ -139,6 +149,11 @@ export interface EncodeDiagramUrlOptions {
139
149
  palette?: PaletteConfig;
140
150
  theme?: Theme;
141
151
  filename?: string;
152
+ /**
153
+ * Initial view state to embed in the URL — re-applied when the link is
154
+ * decoded so recipients open the diagram in the same configuration.
155
+ */
156
+ viewState?: CompactViewState;
142
157
  }
143
158
 
144
159
  /**
@@ -158,6 +173,7 @@ export function encodeDiagramUrl(
158
173
  palette: options?.palette?.id,
159
174
  theme: internalTheme,
160
175
  filename: options?.filename,
176
+ viewState: options?.viewState,
161
177
  });
162
178
  return 'error' in result && result.error ? null : (result.url ?? null);
163
179
  }
@@ -188,7 +204,7 @@ export function decodeDiagramUrl(url: string): DecodedDiagramUrl | null {
188
204
  // Palettes + themes (namespaces)
189
205
  // ============================================================
190
206
 
191
- export { palettes } from './palettes';
207
+ export { palettes, getPalette } from './palettes';
192
208
  export { themes, type Theme } from './themes';
193
209
 
194
210
  // ============================================================
@@ -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-
@@ -351,11 +353,11 @@ export function parseInfra(content: string): ParsedInfra {
351
353
 
352
354
  // animate (default ON) / no-animate
353
355
  if (trimmed === 'animate') {
354
- result.options.animate = 'on';
356
+ result.options['animate'] = 'on';
355
357
  continue;
356
358
  }
357
359
  if (trimmed === 'no-animate') {
358
- result.options.animate = 'off';
360
+ result.options['animate'] = 'off';
359
361
  continue;
360
362
  }
361
363
 
@@ -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,
@@ -550,11 +560,11 @@ export function parseInfra(content: string): ParsedInfra {
550
560
  const pipeMeta = extractPipeMetadata(targetRaw);
551
561
  const targetName = pipeMeta.clean || targetRaw;
552
562
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
553
- const split = pipeMeta.tags.split
554
- ? parseFloat(pipeMeta.tags.split)
563
+ const split = pipeMeta.tags['split']
564
+ ? parseFloat(pipeMeta.tags['split'])
555
565
  : null;
556
- const fanoutRaw = pipeMeta.tags.fanout
557
- ? parseInt(pipeMeta.tags.fanout, 10)
566
+ const fanoutRaw = pipeMeta.tags['fanout']
567
+ ? parseInt(pipeMeta.tags['fanout'], 10)
558
568
  : null;
559
569
  if (fanoutRaw !== null && fanoutRaw < 1) {
560
570
  warn(
@@ -588,11 +598,11 @@ export function parseInfra(content: string): ParsedInfra {
588
598
  const pipeMeta = extractPipeMetadata(targetRaw);
589
599
  const targetName = pipeMeta.clean || targetRaw;
590
600
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
591
- const split = pipeMeta.tags.split
592
- ? parseFloat(pipeMeta.tags.split)
601
+ const split = pipeMeta.tags['split']
602
+ ? parseFloat(pipeMeta.tags['split'])
593
603
  : null;
594
- const fanoutRaw = pipeMeta.tags.fanout
595
- ? parseInt(pipeMeta.tags.fanout, 10)
604
+ const fanoutRaw = pipeMeta.tags['fanout']
605
+ ? parseInt(pipeMeta.tags['fanout'], 10)
596
606
  : null;
597
607
  if (fanoutRaw !== null && fanoutRaw < 1) {
598
608
  warn(
@@ -631,11 +641,11 @@ export function parseInfra(content: string): ParsedInfra {
631
641
  const pipeMeta = extractPipeMetadata(targetRaw);
632
642
  const targetName = pipeMeta.clean || targetRaw;
633
643
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
634
- const split = pipeMeta.tags.split
635
- ? parseFloat(pipeMeta.tags.split)
644
+ const split = pipeMeta.tags['split']
645
+ ? parseFloat(pipeMeta.tags['split'])
636
646
  : null;
637
- const fanoutRaw = pipeMeta.tags.fanout
638
- ? parseInt(pipeMeta.tags.fanout, 10)
647
+ const fanoutRaw = pipeMeta.tags['fanout']
648
+ ? parseInt(pipeMeta.tags['fanout'], 10)
639
649
  : null;
640
650
  if (fanoutRaw !== null && fanoutRaw < 1) {
641
651
  warn(
@@ -669,11 +679,11 @@ export function parseInfra(content: string): ParsedInfra {
669
679
  const pipeMeta = extractPipeMetadata(targetRaw);
670
680
  const targetName = pipeMeta.clean || targetRaw;
671
681
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
672
- const split = pipeMeta.tags.split
673
- ? parseFloat(pipeMeta.tags.split)
682
+ const split = pipeMeta.tags['split']
683
+ ? parseFloat(pipeMeta.tags['split'])
674
684
  : null;
675
- const fanoutRaw = pipeMeta.tags.fanout
676
- ? parseInt(pipeMeta.tags.fanout, 10)
685
+ const fanoutRaw = pipeMeta.tags['fanout']
686
+ ? parseInt(pipeMeta.tags['fanout'], 10)
677
687
  : null;
678
688
  if (fanoutRaw !== null && fanoutRaw < 1) {
679
689
  warn(
@@ -892,7 +902,7 @@ export function parseInfra(content: string): ParsedInfra {
892
902
  // Symbol extraction (for completion API)
893
903
  // ============================================================
894
904
 
895
- import type { DiagramSymbols } from '../completion';
905
+ import type { DiagramSymbols } from '../completion-types';
896
906
 
897
907
  /**
898
908
  * Extract component names (entities) from infra document text.