@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
@@ -309,6 +309,7 @@ interface BLRenderOptions {
309
309
  controlsExpanded?: boolean;
310
310
  onToggleDescriptions?: (active: boolean) => void;
311
311
  onToggleControlsExpand?: () => void;
312
+ exportMode?: boolean;
312
313
  }
313
314
 
314
315
  export function renderBoxesAndLines(
@@ -328,6 +329,7 @@ export function renderBoxesAndLines(
328
329
  controlsExpanded,
329
330
  onToggleDescriptions,
330
331
  onToggleControlsExpand,
332
+ exportMode = false,
331
333
  } = options ?? {};
332
334
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
333
335
 
@@ -974,7 +976,7 @@ export function renderBoxesAndLines(
974
976
  const legendConfig: LegendConfig = {
975
977
  groups: parsed.tagGroups,
976
978
  position: { placement: 'top-center', titleRelation: 'below-title' },
977
- mode: 'fixed',
979
+ mode: exportMode ? 'export' : 'preview',
978
980
  controlsGroup,
979
981
  };
980
982
  const legendState: LegendState = {
@@ -1017,11 +1019,13 @@ export function renderBoxesAndLinesForExport(
1017
1019
  exportDims?: { width: number; height: number };
1018
1020
  activeTagGroup?: string | null;
1019
1021
  hiddenTagValues?: Map<string, Set<string>>;
1022
+ exportMode?: boolean;
1020
1023
  }
1021
1024
  ): void {
1022
1025
  renderBoxesAndLines(container, parsed, layout, palette, isDark, {
1023
1026
  exportDims: options?.exportDims,
1024
1027
  activeTagGroup: options?.activeTagGroup,
1025
1028
  hiddenTagValues: options?.hiddenTagValues,
1029
+ exportMode: options?.exportMode,
1026
1030
  });
1027
1031
  }
package/src/c4/parser.ts CHANGED
@@ -350,7 +350,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
350
350
  if (!color) {
351
351
  pushError(
352
352
  lineNumber,
353
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
353
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
354
354
  );
355
355
  continue;
356
356
  }
@@ -421,7 +421,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
421
421
  );
422
422
  const shape = inferC4Shape(
423
423
  nodeName,
424
- metadata.tech ?? metadata.technology
424
+ metadata['tech'] ?? metadata['technology']
425
425
  );
426
426
 
427
427
  const dNode: C4DeploymentNode = {
@@ -555,11 +555,11 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
555
555
  // parsePipeMetadata expects segments split by |; first segment is pre-pipe
556
556
  const meta = parsePipeMetadata(['', metaPart], metaAliasMap);
557
557
  // tech/technology on pipe overrides [tech] in label
558
- if (meta.tech) {
559
- technology = meta.tech;
558
+ if (meta['tech']) {
559
+ technology = meta['tech'];
560
560
  }
561
- if (meta.technology) {
562
- technology = meta.technology;
561
+ if (meta['technology']) {
562
+ technology = meta['technology'];
563
563
  }
564
564
  }
565
565
 
@@ -729,7 +729,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
729
729
 
730
730
  const shape =
731
731
  explicitShape ??
732
- inferC4Shape(namePart, metadata.tech ?? metadata.technology);
732
+ inferC4Shape(namePart, metadata['tech'] ?? metadata['technology']);
733
733
 
734
734
  // Extract description from pipe metadata into dedicated field
735
735
  let isADescription: string[] | undefined;
@@ -806,7 +806,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
806
806
  // Determine shape: explicit > inference
807
807
  const shape =
808
808
  explicitShape ??
809
- inferC4Shape(namePart, metadata.tech ?? metadata.technology);
809
+ inferC4Shape(namePart, metadata['tech'] ?? metadata['technology']);
810
810
 
811
811
  // Extract description from pipe metadata into dedicated field
812
812
  let prefixDescription: string[] | undefined;
@@ -232,7 +232,8 @@ export function renderC4Context(
232
232
  isDark: boolean,
233
233
  onClickItem?: (lineNumber: number) => void,
234
234
  exportDims?: { width?: number; height?: number },
235
- activeTagGroup?: string | null
235
+ activeTagGroup?: string | null,
236
+ exportMode?: boolean
236
237
  ): void {
237
238
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
238
239
 
@@ -636,7 +637,8 @@ export function renderC4Context(
636
637
  palette,
637
638
  isDark,
638
639
  activeTagGroup,
639
- fixedLegend ? width : null
640
+ fixedLegend ? width : null,
641
+ exportMode
640
642
  );
641
643
  }
642
644
  }
@@ -1259,7 +1261,8 @@ function renderLegend(
1259
1261
  palette: PaletteColors,
1260
1262
  isDark: boolean,
1261
1263
  activeTagGroup?: string | null,
1262
- fixedWidth?: number | null
1264
+ fixedWidth?: number | null,
1265
+ exportMode?: boolean
1263
1266
  ): void {
1264
1267
  const groups = layout.legend.map((g) => ({
1265
1268
  name: g.name,
@@ -1268,7 +1271,7 @@ function renderLegend(
1268
1271
  const legendConfig: LegendConfig = {
1269
1272
  groups,
1270
1273
  position: { placement: 'top-center', titleRelation: 'below-title' },
1271
- mode: 'fixed',
1274
+ mode: exportMode ? 'export' : 'preview',
1272
1275
  };
1273
1276
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
1274
1277
  const containerWidth = fixedWidth ?? layout.width;
@@ -1300,7 +1303,8 @@ export function renderC4Containers(
1300
1303
  isDark: boolean,
1301
1304
  onClickItem?: (lineNumber: number) => void,
1302
1305
  exportDims?: { width?: number; height?: number },
1303
- activeTagGroup?: string | null
1306
+ activeTagGroup?: string | null,
1307
+ exportMode?: boolean
1304
1308
  ): void {
1305
1309
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
1306
1310
 
@@ -1805,7 +1809,8 @@ export function renderC4Containers(
1805
1809
  palette,
1806
1810
  isDark,
1807
1811
  activeTagGroup,
1808
- fixedLegend ? width : null
1812
+ fixedLegend ? width : null,
1813
+ exportMode
1809
1814
  );
1810
1815
  }
1811
1816
  }
@@ -1933,7 +1938,8 @@ export function renderC4Deployment(
1933
1938
  isDark: boolean,
1934
1939
  onClickItem?: (lineNumber: number) => void,
1935
1940
  exportDims?: { width?: number; height?: number },
1936
- activeTagGroup?: string | null
1941
+ activeTagGroup?: string | null,
1942
+ exportMode?: boolean
1937
1943
  ): void {
1938
1944
  renderC4Containers(
1939
1945
  container,
@@ -1943,7 +1949,8 @@ export function renderC4Deployment(
1943
1949
  isDark,
1944
1950
  onClickItem,
1945
1951
  exportDims,
1946
- activeTagGroup
1952
+ activeTagGroup,
1953
+ exportMode
1947
1954
  );
1948
1955
  }
1949
1956
 
@@ -490,8 +490,3 @@ export const chartTypes: readonly ChartTypeMeta[] = [
490
490
  fallback: true,
491
491
  },
492
492
  ] as const;
493
-
494
- // Chart-type ids currently flagged as beta. Renderers/UIs use this set to
495
- // surface a "β BETA" badge next to titles, nav entries, and template tiles.
496
- // Promote/demote by editing this set + the corresponding registry titles.
497
- export const BETA_CHART_IDS: ReadonlySet<string> = new Set(['c4', 'venn']);
package/src/chart.ts CHANGED
@@ -63,7 +63,7 @@ export interface ParsedChart {
63
63
  // Colors
64
64
  // ============================================================
65
65
 
66
- import { resolveColorWithDiagnostic } from './colors';
66
+ import { resolveColorWithDiagnostic, RECOGNIZED_COLOR_NAMES } from './colors';
67
67
  import type { PaletteColors } from './palettes';
68
68
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
69
69
  import {
@@ -212,21 +212,30 @@ export function parseChart(
212
212
  // Fall through — first line might be a data row or option
213
213
  }
214
214
 
215
- // Era line: era Day 1 -> Day 3 Rough Seas (blue) — colon-free
216
- const eraMatch = trimmed.match(
217
- /^era\s+(.+?)\s*->\s*(.+?)(?:\s*\(([^)]+)\))?\s*$/
218
- );
215
+ // Era line 1.5 trailing-token):
216
+ // `era Day 1 -> Day 3 Rough Seas` (no color)
217
+ // `era Day 1 -> Day 3 Rough Seas blue` (trailing color word)
218
+ // Color (if any) is the last whitespace-delimited token of the label.
219
+ const eraMatch = trimmed.match(/^era\s+(.+?)\s*->\s*(.+?)\s*$/);
219
220
  if (eraMatch) {
220
- // Store start and raw afterArrow — resolved against data labels after parsing
221
221
  const afterArrow = eraMatch[2].trim();
222
222
  const spaceIdx = afterArrow.indexOf(' ');
223
223
  if (spaceIdx >= 0) {
224
+ // Peel trailing-token color off the after-arrow label region.
225
+ const lastSpaceIdx = afterArrow.lastIndexOf(' ');
226
+ const trailing = afterArrow.substring(lastSpaceIdx + 1);
227
+ const hasColor = RECOGNIZED_COLOR_NAMES.includes(
228
+ trailing as (typeof RECOGNIZED_COLOR_NAMES)[number]
229
+ );
230
+ const labelPart = hasColor
231
+ ? afterArrow.substring(0, lastSpaceIdx).trimEnd()
232
+ : afterArrow;
224
233
  rawEras.push({
225
234
  start: eraMatch[1].trim(),
226
- afterArrow,
227
- color: eraMatch[3]
235
+ afterArrow: labelPart,
236
+ color: hasColor
228
237
  ? (resolveColorWithDiagnostic(
229
- eraMatch[3].trim(),
238
+ trailing,
230
239
  lineNumber,
231
240
  result.diagnostics,
232
241
  palette
@@ -35,10 +35,11 @@ function classId(name: string): string {
35
35
  // Regex patterns
36
36
  // ============================================================
37
37
 
38
- // Class declaration: [modifier] ClassName [extends Parent] [implements Interface] (color)
39
- // Multi-word names allowed (`Customer Service`); quote with `"name"` if name
40
- // contains reserved chars. ClassName must start with uppercase to keep the
41
- // convention. Captures (positional):
38
+ // Class declaration: [modifier] ClassName [extends Parent] [implements Interface] [color] [as alias]
39
+ // Color is the universal §1.5 trailing-token form (a bare lowercase palette
40
+ // color word after structural slots). Multi-word names allowed
41
+ // (`Customer Service`); quote with `"name"` if name contains reserved chars.
42
+ // ClassName must start with uppercase. Captures (positional):
42
43
  // 1: modifier (abstract|interface|enum) | undefined
43
44
  // 2: quotedClassName | undefined
44
45
  // 3: bareClassName | undefined
@@ -47,10 +48,10 @@ function classId(name: string): string {
47
48
  // 6: quotedImplements | undefined
48
49
  // 7: bareImplements | undefined
49
50
  // 8: legacy bracket modifier | undefined
50
- // 9: color | undefined
51
+ // 9: color (trailing token; recognized palette word) | undefined
51
52
  // 10: alias literal (TD-18) | undefined
52
53
  const CLASS_DECL_RE =
53
- /^(?:(abstract|interface|enum)\s+)?(?:"([^"]+)"|([A-Z][^":]*?))(?:\s+extends\s+(?:"([^"]+)"|([A-Z][^":]*?)))?(?:\s+implements\s+(?:"([^"]+)"|([A-Z][^":]*?)))?(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?(?:\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11}))?\s*$/;
54
+ /^(?:(abstract|interface|enum)\s+)?(?:"([^"]+)"|([A-Z][^":]*?))(?:\s+extends\s+(?:"([^"]+)"|([A-Z][^":]*?)))?(?:\s+implements\s+(?:"([^"]+)"|([A-Z][^":]*?)))?(?:\s+\[(abstract|interface|enum)\])?(?:\s+(red|orange|yellow|green|blue|purple|teal|cyan|gray|black|white))?(?:\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11}))?\s*$/;
54
55
 
55
56
  // Relationship — arrow syntax (indented under source class).
56
57
  // Arrows: --|> ..|> *-- o-- ..> ->
@@ -184,14 +185,6 @@ export function parseClassDiagram(
184
185
  error: null,
185
186
  };
186
187
 
187
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
188
- const _fail = (line: number, message: string): ParsedClassDiagram => {
189
- const diag = makeDgmoError(line, message);
190
- result.diagnostics.push(diag);
191
- result.error = formatDgmoError(diag);
192
- return result;
193
- };
194
-
195
188
  const classMap = new Map<string, ClassNode>();
196
189
 
197
190
  // Per-parse alias literal → canonical class id (TD-18). Per C8.
@@ -548,7 +541,7 @@ export function looksLikeClassDiagram(content: string): boolean {
548
541
  // Symbol extraction (for completion API)
549
542
  // ============================================================
550
543
 
551
- import type { DiagramSymbols } from '../completion';
544
+ import type { DiagramSymbols } from '../completion-types';
552
545
 
553
546
  /**
554
547
  * Extract class names (entities) from class diagram document text.
@@ -192,7 +192,8 @@ export function renderClassDiagram(
192
192
  isDark: boolean,
193
193
  onClickItem?: (lineNumber: number) => void,
194
194
  exportDims?: { width?: number; height?: number },
195
- legendActive?: boolean | null
195
+ legendActive?: boolean | null,
196
+ exportMode?: boolean
196
197
  ): void {
197
198
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
198
199
 
@@ -376,7 +377,7 @@ export function renderClassDiagram(
376
377
  const legendConfig: LegendConfig = {
377
378
  groups: legendGroups,
378
379
  position: { placement: 'top-center', titleRelation: 'below-title' },
379
- mode: 'fixed',
380
+ mode: exportMode ? 'export' : 'preview',
380
381
  };
381
382
  const legendState: LegendState = {
382
383
  activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null,
@@ -697,10 +698,20 @@ export function renderClassDiagramForExport(
697
698
  legendReserve;
698
699
 
699
700
  return runInExportContainer(exportWidth, exportHeight, (container) => {
700
- renderClassDiagram(container, parsed, layout, palette, isDark, undefined, {
701
- width: exportWidth,
702
- height: exportHeight,
703
- });
701
+ renderClassDiagram(
702
+ container,
703
+ parsed,
704
+ layout,
705
+ palette,
706
+ isDark,
707
+ undefined,
708
+ {
709
+ width: exportWidth,
710
+ height: exportHeight,
711
+ },
712
+ true, // legendActive for export
713
+ true // exportMode
714
+ );
704
715
  return extractExportSvg(container, theme);
705
716
  });
706
717
  }
package/src/cli.ts CHANGED
@@ -179,8 +179,8 @@ palette: catppuccin // override palette
179
179
  // This is a comment (only // syntax — not #)
180
180
  \`\`\`
181
181
 
182
- Inline colors on most elements: append \`(colorname)\` — e.g. \`North(red): 850\`, \`[Process(blue)]\`.
183
- Named colors: \`red\`, \`orange\`, \`yellow\`, \`green\`, \`blue\`, \`purple\`, \`teal\`, \`cyan\`, \`gray\`.
182
+ Inline colors on most elements: append the color name as the trailing token — e.g. \`North red 850\`, \`[Process] blue\`. To use a color word as a literal label, capitalize it (\`Red\` stays as the word Red).
183
+ Named colors: \`red\`, \`orange\`, \`yellow\`, \`green\`, \`blue\`, \`purple\`, \`teal\`, \`cyan\`, \`gray\`, \`black\`, \`white\`.
184
184
 
185
185
  ### sequence (most commonly used)
186
186
 
@@ -234,7 +234,7 @@ North: 850
234
234
  South: 620
235
235
 
236
236
  // line (multi-series)
237
- series: Sales(red), Costs(blue)
237
+ series: Sales red, Costs blue
238
238
  Q1: 100, 50
239
239
  Q2: 120, 55
240
240
 
@@ -295,7 +295,7 @@ API
295
295
  async A -> B: msg ❌ use A ~msg~> B
296
296
  A <- B ❌ left-pointing arrows removed — use B -> A
297
297
  parallel else ❌ not supported — use separate parallel blocks
298
- == Foo(#ff0000) == ❌ hex colors not supported — use named colors: == Foo(red) ==
298
+ == Foo #ff0000 == ❌ hex colors not supported — use named colors: == Foo red ==
299
299
  A -routes to /api-> B ❌ -> inside a label is ambiguous — rephrase the label
300
300
  end ❌ not needed — indentation closes blocks in sequence diagrams
301
301
  \`\`\`
@@ -427,8 +427,8 @@ bar, line, multi-line, area, pie, doughnut, radar, polar-area, bar-stacked, scat
427
427
 
428
428
  - First line: chart type keyword (e.g. \`sequence\`, \`flowchart\`, \`bar\`), optionally followed by a title (\`bar Revenue\`)
429
429
  - \`// comment\` — only \`//\` comments (not \`#\`)
430
- - \`(colorname)\` — inline colors on data series, tag values, kanban columns: \`Label(red) 100\`
431
- - \`series A(red), B(blue)\` — multi-series with colors
430
+ - Trailing color name — inline colors on data series, tag values, kanban columns: \`Label red 100\`
431
+ - \`series A red, B blue\` — multi-series with colors
432
432
 
433
433
  ## Rendering via CLI
434
434
 
@@ -705,7 +705,9 @@ async function main(): Promise<void> {
705
705
 
706
706
  if (opts.cat) {
707
707
  const useColor =
708
- !opts.noColor && !process.env.NO_COLOR && process.stdout.isTTY === true;
708
+ !opts.noColor &&
709
+ !process.env['NO_COLOR'] &&
710
+ process.stdout.isTTY === true;
709
711
 
710
712
  let catContent: string;
711
713
  if (opts.input && opts.input !== '-') {
@@ -822,9 +824,9 @@ async function main(): Promise<void> {
822
824
  }
823
825
  }
824
826
  const mcpServers =
825
- (settings.mcpServers as Record<string, unknown> | undefined) ?? {};
827
+ (settings['mcpServers'] as Record<string, unknown> | undefined) ?? {};
826
828
  mcpServers['dgmo'] = mcpEntry;
827
- settings.mcpServers = mcpServers;
829
+ settings['mcpServers'] = mcpServers;
828
830
  writeFileSync(
829
831
  settingsPath,
830
832
  JSON.stringify(settings, null, 2) + '\n',
@@ -842,9 +844,9 @@ async function main(): Promise<void> {
842
844
  }
843
845
  }
844
846
  const mcpServers =
845
- (mcp.mcpServers as Record<string, unknown> | undefined) ?? {};
847
+ (mcp['mcpServers'] as Record<string, unknown> | undefined) ?? {};
846
848
  mcpServers['dgmo'] = mcpEntry;
847
- mcp.mcpServers = mcpServers;
849
+ mcp['mcpServers'] = mcpServers;
848
850
  writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n', 'utf-8');
849
851
  console.log(
850
852
  `✓ MCP server configured: ${join(process.cwd(), '.mcp.json')}`
@@ -1118,7 +1120,7 @@ async function main(): Promise<void> {
1118
1120
  );
1119
1121
  } else if (os === 'win32') {
1120
1122
  const appData =
1121
- process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
1123
+ process.env['APPDATA'] ?? join(homedir(), 'AppData', 'Roaming');
1122
1124
  configPath = join(appData, 'Claude', 'claude_desktop_config.json');
1123
1125
  } else {
1124
1126
  configPath = join(
@@ -1154,7 +1156,7 @@ async function main(): Promise<void> {
1154
1156
  }
1155
1157
  }
1156
1158
 
1157
- const existingDgmo = config.mcpServers?.dgmo;
1159
+ const existingDgmo = config.mcpServers?.['dgmo'];
1158
1160
  if (existingDgmo && existingDgmo.command === 'dgmo-mcp') {
1159
1161
  console.log(`✓ dgmo MCP server already configured in ${configPath}`);
1160
1162
  } else {
@@ -0,0 +1,28 @@
1
+ // ============================================================
2
+ // Completion / symbol-extraction shared types
3
+ // ============================================================
4
+ //
5
+ // Lives in its own leaf module so chart-type parsers (Class, ER,
6
+ // Flowchart, Infra, Pert) can `import type { DiagramSymbols }` here
7
+ // without depending on completion.ts — which itself imports those
8
+ // parsers' extractSymbols() functions, producing a cycle hub.
9
+ //
10
+ // Keep this file dependency-free.
11
+
12
+ // ChartType is just a string — alias for documentation clarity.
13
+ export type ChartType = string;
14
+
15
+ export interface DiagramSymbols {
16
+ kind: ChartType;
17
+ entities: string[]; // table names, node IDs, class names, etc.
18
+ keywords: string[]; // diagram-specific reserved words
19
+ /**
20
+ * Map of alias-literal → canonical entity name, collected from
21
+ * `Name as <alias>` declarations in the document. Editor surfaces
22
+ * both forms in autocomplete; selecting an alias inserts the alias
23
+ * literal (the alias is input convenience, not a display name).
24
+ */
25
+ aliases?: Record<string, string>;
26
+ }
27
+
28
+ export type ExtractFn = (docText: string) => DiagramSymbols;
package/src/completion.ts CHANGED
@@ -17,29 +17,27 @@ import { extractSymbols as extractInfraSymbols } from './infra/parser';
17
17
  import { extractSymbols as extractClassSymbols } from './class/parser';
18
18
  import { extractPertSymbols } from './pert/parser';
19
19
  import { parseFirstLine, ALL_CHART_TYPES } from './utils/parsing';
20
- import { CHART_TYPE_DESCRIPTIONS } from './dgmo-router';
20
+ import { RECOGNIZED_COLOR_NAMES } from './colors';
21
+
22
+ const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
23
+ RECOGNIZED_COLOR_NAMES
24
+ );
25
+ // Read chart-type descriptions directly from the source-of-truth data
26
+ // module instead of via dgmo-router.ts. dgmo-router imports every
27
+ // parser, and the parsers (Class/ER/Infra/Pert/Flowchart) type-only
28
+ // import DiagramSymbols back from this file — creating a hub of cycles
29
+ // through completion ↔ dgmo-router. Going through chart-types.ts (a
30
+ // leaf module with zero imports) breaks 7 of the 10 known cycles.
31
+ import { chartTypes } from './chart-types';
21
32
 
22
33
  // ============================================================
23
34
  // Symbol extraction
24
35
  // ============================================================
25
36
 
26
- // ChartType is just a string alias here for documentation clarity.
27
- export type ChartType = string;
28
-
29
- export interface DiagramSymbols {
30
- kind: ChartType;
31
- entities: string[]; // table names, node IDs, class names, etc.
32
- keywords: string[]; // diagram-specific reserved words
33
- /**
34
- * Map of alias-literal → canonical entity name, collected from
35
- * `Name as <alias>` declarations in the document. Editor surfaces
36
- * both forms in autocomplete; selecting an alias inserts the alias
37
- * literal (the alias is input convenience, not a display name).
38
- */
39
- aliases?: Record<string, string>;
40
- }
41
-
42
- export type ExtractFn = (docText: string) => DiagramSymbols;
37
+ // Types live in ./completion-types so the chart-type parsers can
38
+ // import them without taking a cycle through this file.
39
+ import type { ChartType, DiagramSymbols, ExtractFn } from './completion-types';
40
+ export type { ChartType, DiagramSymbols, ExtractFn };
43
41
 
44
42
  const extractorRegistry = new Map<ChartType, ExtractFn>();
45
43
 
@@ -575,6 +573,10 @@ for (const [type, spec] of COMPLETION_REGISTRY) {
575
573
  // ============================================================
576
574
 
577
575
  /** All chart types with descriptions, for chart type autocomplete. Excludes `multi-line` alias. */
576
+ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = Object.fromEntries(
577
+ chartTypes.map((c) => [c.id, c.description])
578
+ );
579
+
578
580
  export const CHART_TYPES: ReadonlyArray<{ name: string; description: string }> =
579
581
  [...ALL_CHART_TYPES]
580
582
  .filter((t) => t !== 'multi-line')
@@ -971,10 +973,15 @@ export function extractTagDeclarations(docText: string): Map<string, string[]> {
971
973
  (raw[0] === ' ' || raw[0] === '\t')
972
974
  ) {
973
975
  if (trimmed && !trimmed.startsWith('//')) {
974
- // Strip color annotation: Frontend(blue) → Frontend
975
- const colorIdx = trimmed.indexOf('(');
976
+ // Strip trailing-token color (§1.5): `Frontend blue``Frontend`.
977
+ // Whitespace-split; if the last token is a recognized color word,
978
+ // drop it; otherwise the whole trimmed string is the value.
979
+ const lastSpaceIdx = trimmed.lastIndexOf(' ');
976
980
  const value =
977
- colorIdx > 0 ? trimmed.substring(0, colorIdx).trim() : trimmed;
981
+ lastSpaceIdx > 0 &&
982
+ RECOGNIZED_COLOR_SET.has(trimmed.substring(lastSpaceIdx + 1))
983
+ ? trimmed.substring(0, lastSpaceIdx).trim()
984
+ : trimmed;
978
985
  if (value) currentValues.push(value);
979
986
  }
980
987
  continue;
@@ -863,8 +863,8 @@ function fitToCanvas(
863
863
  nodes: CycleLayoutNode[],
864
864
  edges: CycleLayoutEdge[],
865
865
  parsed: ParsedCycle,
866
- cx: number,
867
- cy: number,
866
+ _cx: number,
867
+ _cy: number,
868
868
  radius: number,
869
869
  width: number,
870
870
  height: number,
@@ -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 { ParsedCycle, CycleNode, CycleEdge } from './types';
@@ -149,6 +150,7 @@ export function parseCycle(content: string): ParsedCycle {
149
150
  }
150
151
 
151
152
  // Parse node: Label | color: blue, span: 3, description: text
153
+ // OR shortcut form (color only): `Label color` (universal §1.5)
152
154
  const pipeIdx = trimmed.indexOf('|');
153
155
  let label: string;
154
156
  let metadata: Record<string, string> = {};
@@ -166,6 +168,18 @@ export function parseCycle(content: string): ParsedCycle {
166
168
  continue;
167
169
  }
168
170
 
171
+ // Universal trailing-token shortcut: if no `| color:` was set explicitly
172
+ // and the label ends in a recognized color word, treat that word as the
173
+ // color and strip it from the label.
174
+ if (!metadata['color']) {
175
+ const { label: stripped, colorName: shortcutColor } =
176
+ peelTrailingColorName(label);
177
+ if (shortcutColor) {
178
+ metadata['color'] = shortcutColor;
179
+ label = stripped;
180
+ }
181
+ }
182
+
169
183
  // Extract known keys from metadata
170
184
  const color = metadata['color'];
171
185
  const spanStr = metadata['span'];
@@ -46,6 +46,7 @@ export interface CycleRenderOptions {
46
46
  controlsExpanded?: boolean;
47
47
  onToggleDescriptions?: (active: boolean) => void;
48
48
  onToggleControlsExpand?: () => void;
49
+ exportMode?: boolean;
49
50
  }
50
51
 
51
52
  /**
@@ -144,7 +145,7 @@ export function renderCycle(
144
145
  const legendConfig: LegendConfig = {
145
146
  groups: [],
146
147
  position: { placement: 'top-center', titleRelation: 'below-title' },
147
- mode: 'fixed',
148
+ mode: renderOptions?.exportMode ? 'export' : 'preview',
148
149
  controlsGroup,
149
150
  };
150
151
  const legendState: LegendState = {
@@ -519,7 +520,8 @@ export function renderCycleForExport(
519
520
  palette: PaletteColors,
520
521
  isDark: boolean,
521
522
  exportDims?: D3ExportDimensions,
522
- viewState?: CompactViewState
523
+ viewState?: CompactViewState,
524
+ exportMode?: boolean
523
525
  ): void {
524
526
  renderCycle(
525
527
  container,
@@ -528,7 +530,8 @@ export function renderCycleForExport(
528
530
  isDark,
529
531
  undefined,
530
532
  exportDims,
531
- viewState
533
+ viewState,
534
+ { exportMode }
532
535
  );
533
536
  }
534
537