@diagrammo/dgmo 0.8.23 → 0.8.26

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 (78) hide show
  1. package/.claude/commands/dgmo.md +43 -431
  2. package/.cursorrules +2 -2
  3. package/.windsurfrules +2 -2
  4. package/AGENTS.md +8 -5
  5. package/dist/cli.cjs +119 -114
  6. package/dist/editor.cjs +0 -2
  7. package/dist/editor.cjs.map +1 -1
  8. package/dist/editor.js +0 -2
  9. package/dist/editor.js.map +1 -1
  10. package/dist/highlight.cjs +0 -2
  11. package/dist/highlight.cjs.map +1 -1
  12. package/dist/highlight.js +0 -2
  13. package/dist/highlight.js.map +1 -1
  14. package/dist/index.cjs +719 -281
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +105 -18
  17. package/dist/index.d.ts +105 -18
  18. package/dist/index.js +709 -280
  19. package/dist/index.js.map +1 -1
  20. package/dist/internal.cjs +348 -51
  21. package/dist/internal.cjs.map +1 -1
  22. package/dist/internal.d.cts +93 -5
  23. package/dist/internal.d.ts +93 -5
  24. package/dist/internal.js +334 -38
  25. package/dist/internal.js.map +1 -1
  26. package/docs/guide/chart-area.md +17 -17
  27. package/docs/guide/chart-bar-stacked.md +12 -12
  28. package/docs/guide/chart-doughnut.md +10 -10
  29. package/docs/guide/chart-funnel.md +9 -9
  30. package/docs/guide/chart-heatmap.md +10 -10
  31. package/docs/guide/chart-kanban.md +2 -0
  32. package/docs/guide/chart-line.md +19 -19
  33. package/docs/guide/chart-multi-line.md +16 -16
  34. package/docs/guide/chart-pie.md +11 -11
  35. package/docs/guide/chart-polar-area.md +10 -10
  36. package/docs/guide/chart-radar.md +9 -9
  37. package/docs/guide/chart-scatter.md +24 -27
  38. package/docs/guide/index.md +3 -3
  39. package/docs/language-reference.md +46 -25
  40. package/fonts/Inter-Bold.ttf +0 -0
  41. package/fonts/Inter-Regular.ttf +0 -0
  42. package/fonts/LICENSE-Inter.txt +92 -0
  43. package/gallery/fixtures/bar-stacked.dgmo +12 -6
  44. package/gallery/fixtures/heatmap.dgmo +12 -6
  45. package/gallery/fixtures/multi-line.dgmo +11 -7
  46. package/gallery/fixtures/quadrant.dgmo +8 -8
  47. package/gallery/fixtures/scatter.dgmo +12 -12
  48. package/package.json +10 -3
  49. package/src/boxes-and-lines/parser.ts +13 -2
  50. package/src/boxes-and-lines/renderer.ts +22 -13
  51. package/src/chart-type-scoring.ts +162 -0
  52. package/src/chart-types.ts +437 -0
  53. package/src/cli.ts +147 -66
  54. package/src/completion.ts +0 -4
  55. package/src/d3.ts +40 -2
  56. package/src/dgmo-router.ts +85 -130
  57. package/src/editor/keywords.ts +0 -2
  58. package/src/fonts.ts +3 -2
  59. package/src/gantt/parser.ts +5 -1
  60. package/src/index.ts +24 -1
  61. package/src/infra/parser.ts +1 -1
  62. package/src/internal.ts +6 -2
  63. package/src/journey-map/layout.ts +8 -6
  64. package/src/journey-map/parser.ts +5 -1
  65. package/src/kanban/parser.ts +5 -1
  66. package/src/org/collapse.ts +1 -4
  67. package/src/org/parser.ts +1 -1
  68. package/src/org/renderer.ts +26 -17
  69. package/src/sequence/parser.ts +2 -2
  70. package/src/sequence/participant-inference.ts +0 -1
  71. package/src/sequence/renderer.ts +95 -263
  72. package/src/sharing.ts +0 -1
  73. package/src/sitemap/parser.ts +1 -1
  74. package/src/tech-radar/layout.ts +1 -2
  75. package/src/tech-radar/shared.ts +1 -37
  76. package/src/utils/tag-groups.ts +35 -5
  77. package/src/wireframe/parser.ts +3 -1
  78. package/src/tech-radar/index.ts +0 -14
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { execSync } from 'node:child_process';
4
- import { homedir } from 'node:os';
4
+ import { homedir, platform } from 'node:os';
5
5
  import { resolve, join, basename, extname } from 'node:path';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { Resvg } from '@resvg/resvg-js';
@@ -467,10 +467,6 @@ Options:
467
467
  With stdin and no -o, PNG is written to stdout
468
468
  --theme <theme> Theme: ${THEMES.join(', ')} (default: light)
469
469
  --palette <name> Palette: ${PALETTES.join(', ')} (default: nord)
470
- --c4-level <level> C4 render level: context (default), containers, components, deployment
471
- --c4-system <name> System to drill into (with --c4-level containers or components)
472
- --c4-container <name> Container to drill into (with --c4-level components)
473
- --tag-group <name> Pre-select a tag group for static export coloring
474
470
  --copy Copy URL to clipboard (only with -o url)
475
471
  --json Output structured JSON to stdout
476
472
  --chart-types List all supported chart types
@@ -482,6 +478,11 @@ Options:
482
478
  --install-codex-integration
483
479
  Full Codex CLI setup: write AGENTS.md to the project and configure
484
480
  the dgmo MCP server in .codex/config.toml (project) or ~/.codex/config.toml (global)
481
+ --install-claude-desktop-integration
482
+ Full Claude Desktop setup: install @diagrammo/dgmo-mcp if needed,
483
+ then merge the dgmo MCP entry into Claude Desktop's config file
484
+ (~/Library/Application Support/Claude/claude_desktop_config.json on macOS,
485
+ %APPDATA%/Claude/... on Windows, ~/.config/Claude/... on Linux)
485
486
  --help Show this help
486
487
  --version Show version`);
487
488
  }
@@ -508,10 +509,7 @@ function parseArgs(argv: string[]): {
508
509
  installClaudeSkill: boolean;
509
510
  installClaudeCodeIntegration: boolean;
510
511
  installCodexIntegration: boolean;
511
- c4Level: 'context' | 'containers' | 'components' | 'deployment';
512
- c4System: string | undefined;
513
- c4Container: string | undefined;
514
- tagGroup: string | undefined;
512
+ installClaudeDesktopIntegration: boolean;
515
513
  } {
516
514
  const result = {
517
515
  input: undefined as string | undefined,
@@ -528,14 +526,7 @@ function parseArgs(argv: string[]): {
528
526
  installClaudeSkill: false,
529
527
  installClaudeCodeIntegration: false,
530
528
  installCodexIntegration: false,
531
- c4Level: 'context' as
532
- | 'context'
533
- | 'containers'
534
- | 'components'
535
- | 'deployment',
536
- c4System: undefined as string | undefined,
537
- c4Container: undefined as string | undefined,
538
- tagGroup: undefined as string | undefined,
529
+ installClaudeDesktopIntegration: false,
539
530
  };
540
531
 
541
532
  const args = argv.slice(2); // skip node + script
@@ -579,30 +570,6 @@ function parseArgs(argv: string[]): {
579
570
  }
580
571
  result.palette = val;
581
572
  i++;
582
- } else if (arg === '--c4-level') {
583
- const val = args[++i];
584
- if (
585
- val !== 'context' &&
586
- val !== 'containers' &&
587
- val !== 'components' &&
588
- val !== 'deployment'
589
- ) {
590
- console.error(
591
- `Error: Invalid C4 level "${val}". Valid levels: context, containers, components, deployment`
592
- );
593
- process.exit(1);
594
- }
595
- result.c4Level = val;
596
- i++;
597
- } else if (arg === '--c4-system') {
598
- result.c4System = args[++i];
599
- i++;
600
- } else if (arg === '--c4-container') {
601
- result.c4Container = args[++i];
602
- i++;
603
- } else if (arg === '--tag-group') {
604
- result.tagGroup = args[++i];
605
- i++;
606
573
  } else if (arg === '--json') {
607
574
  result.json = true;
608
575
  i++;
@@ -618,6 +585,9 @@ function parseArgs(argv: string[]): {
618
585
  } else if (arg === '--install-codex-integration') {
619
586
  result.installCodexIntegration = true;
620
587
  i++;
588
+ } else if (arg === '--install-claude-desktop-integration') {
589
+ result.installClaudeDesktopIntegration = true;
590
+ i++;
621
591
  } else if (arg === '--copy') {
622
592
  result.copy = true;
623
593
  i++;
@@ -643,12 +613,19 @@ function inferFormat(outputPath: string | undefined): 'svg' | 'png' | 'url' {
643
613
  return 'png';
644
614
  }
645
615
 
616
+ const BUNDLED_FONTS = [
617
+ join(__dirname, '..', 'fonts', 'Inter-Regular.ttf'),
618
+ join(__dirname, '..', 'fonts', 'Inter-Bold.ttf'),
619
+ ];
620
+
646
621
  function svgToPng(svg: string, background?: string): Buffer {
622
+ const fontFiles = BUNDLED_FONTS.filter((f) => existsSync(f));
647
623
  const resvg = new Resvg(svg, {
648
624
  fitTo: { mode: 'zoom', value: 2 },
649
625
  ...(background ? { background } : {}),
650
626
  font: {
651
- loadSystemFonts: true,
627
+ loadSystemFonts: fontFiles.length === 0,
628
+ ...(fontFiles.length > 0 ? { fontFiles } : {}),
652
629
  defaultFontFamily: DEFAULT_FONT_NAME,
653
630
  sansSerifFamily: DEFAULT_FONT_NAME,
654
631
  },
@@ -715,7 +692,7 @@ async function main(): Promise<void> {
715
692
  } else {
716
693
  for (const id of types) {
717
694
  const desc = CHART_TYPE_DESCRIPTIONS[id];
718
- console.log(desc ? `${id} — ${desc.split(' — ')[1]}` : id);
695
+ console.log(desc ? `${id} — ${desc}` : id);
719
696
  }
720
697
  }
721
698
  return;
@@ -1038,6 +1015,133 @@ async function main(): Promise<void> {
1038
1015
  return;
1039
1016
  }
1040
1017
 
1018
+ if (opts.installClaudeDesktopIntegration) {
1019
+ const ask = (prompt: string): Promise<string> =>
1020
+ new Promise((resolve) => {
1021
+ const rl = createInterface({
1022
+ input: process.stdin,
1023
+ output: process.stdout,
1024
+ });
1025
+ rl.question(prompt, (answer) => {
1026
+ rl.close();
1027
+ resolve(answer);
1028
+ });
1029
+ });
1030
+
1031
+ // Check / install dgmo-mcp binary
1032
+ let dgmoMcpInstalled = false;
1033
+ try {
1034
+ execSync('which dgmo-mcp', { stdio: 'pipe' });
1035
+ dgmoMcpInstalled = true;
1036
+ } catch {
1037
+ /* not found */
1038
+ }
1039
+ if (!dgmoMcpInstalled) {
1040
+ const ans = await ask(
1041
+ '\ndgmo-mcp not found. Install @diagrammo/dgmo-mcp globally now? [Y/n] '
1042
+ );
1043
+ const yes =
1044
+ ans === '' || ans.toLowerCase() === 'y' || ans.toLowerCase() === 'yes';
1045
+ if (yes) {
1046
+ console.log('Installing @diagrammo/dgmo-mcp...');
1047
+ try {
1048
+ execSync('npm install -g @diagrammo/dgmo-mcp', { stdio: 'inherit' });
1049
+ console.log('✓ @diagrammo/dgmo-mcp installed');
1050
+ } catch {
1051
+ console.error('Error: Failed to install @diagrammo/dgmo-mcp.');
1052
+ console.error('Try manually: npm install -g @diagrammo/dgmo-mcp');
1053
+ }
1054
+ } else {
1055
+ console.log(
1056
+ ' Skipped. Install later with: npm install -g @diagrammo/dgmo-mcp'
1057
+ );
1058
+ }
1059
+ } else {
1060
+ console.log('✓ dgmo-mcp already installed');
1061
+ }
1062
+
1063
+ // Resolve the Claude Desktop config path for the current platform.
1064
+ // macOS and Windows use the documented Claude Desktop paths; Linux
1065
+ // doesn't have a first-party build yet, but community installs follow
1066
+ // the XDG config convention.
1067
+ const os = platform();
1068
+ let configPath: string;
1069
+ if (os === 'darwin') {
1070
+ configPath = join(
1071
+ homedir(),
1072
+ 'Library',
1073
+ 'Application Support',
1074
+ 'Claude',
1075
+ 'claude_desktop_config.json'
1076
+ );
1077
+ } else if (os === 'win32') {
1078
+ const appData =
1079
+ process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
1080
+ configPath = join(appData, 'Claude', 'claude_desktop_config.json');
1081
+ } else {
1082
+ configPath = join(
1083
+ homedir(),
1084
+ '.config',
1085
+ 'Claude',
1086
+ 'claude_desktop_config.json'
1087
+ );
1088
+ }
1089
+
1090
+ // Read existing config (or start fresh). Non-JSON contents are treated
1091
+ // as corruption and we bail — the user needs to resolve it manually so
1092
+ // we don't silently overwrite something they care about.
1093
+ type ClaudeDesktopConfig = {
1094
+ mcpServers?: Record<
1095
+ string,
1096
+ { command: string; args?: string[]; env?: Record<string, string> }
1097
+ >;
1098
+ [key: string]: unknown;
1099
+ };
1100
+ let config: ClaudeDesktopConfig = {};
1101
+ if (existsSync(configPath)) {
1102
+ const raw = readFileSync(configPath, 'utf-8');
1103
+ if (raw.trim().length > 0) {
1104
+ try {
1105
+ config = JSON.parse(raw) as ClaudeDesktopConfig;
1106
+ } catch {
1107
+ console.error(
1108
+ `Error: ${configPath} exists but is not valid JSON. Fix it manually and re-run, or remove the file to regenerate.`
1109
+ );
1110
+ process.exit(1);
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ const existingDgmo = config.mcpServers?.dgmo;
1116
+ if (existingDgmo && existingDgmo.command === 'dgmo-mcp') {
1117
+ console.log(`✓ dgmo MCP server already configured in ${configPath}`);
1118
+ } else {
1119
+ if (existingDgmo) {
1120
+ const ans = await ask(
1121
+ `\nA "dgmo" entry already exists in ${configPath}. Overwrite? [y/N] `
1122
+ );
1123
+ if (ans.toLowerCase() !== 'y' && ans.toLowerCase() !== 'yes') {
1124
+ console.log(' Skipped.');
1125
+ return;
1126
+ }
1127
+ }
1128
+ config.mcpServers = {
1129
+ ...(config.mcpServers ?? {}),
1130
+ dgmo: { command: 'dgmo-mcp' },
1131
+ };
1132
+ mkdirSync(join(configPath, '..'), { recursive: true });
1133
+ writeFileSync(
1134
+ configPath,
1135
+ JSON.stringify(config, null, 2) + '\n',
1136
+ 'utf-8'
1137
+ );
1138
+ console.log(`✓ dgmo MCP server configured: ${configPath}`);
1139
+ }
1140
+
1141
+ console.log('\nRestart Claude Desktop to activate the MCP server.');
1142
+ return;
1143
+ }
1144
+
1041
1145
  // Determine input source
1042
1146
  let content: string;
1043
1147
  let inputBasename: string | undefined;
@@ -1195,32 +1299,9 @@ async function main(): Promise<void> {
1195
1299
  }
1196
1300
  }
1197
1301
 
1198
- // Validate C4 options
1199
- if (opts.c4Level === 'containers' && !opts.c4System) {
1200
- exitWithJsonError(
1201
- 'Error: --c4-system is required when --c4-level is containers'
1202
- );
1203
- }
1204
- if (opts.c4Level === 'components') {
1205
- if (!opts.c4System) {
1206
- exitWithJsonError(
1207
- 'Error: --c4-system is required when --c4-level is components'
1208
- );
1209
- }
1210
- if (!opts.c4Container) {
1211
- exitWithJsonError(
1212
- 'Error: --c4-container is required when --c4-level is components'
1213
- );
1214
- }
1215
- }
1216
-
1217
1302
  const { svg } = await render(content, {
1218
1303
  theme: opts.theme,
1219
1304
  palette: opts.palette,
1220
- c4Level: opts.c4Level,
1221
- c4System: opts.c4System,
1222
- c4Container: opts.c4Container,
1223
- tagGroup: opts.tagGroup,
1224
1305
  });
1225
1306
 
1226
1307
  if (!svg) {
package/src/completion.ts CHANGED
@@ -238,10 +238,6 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
238
238
  description: 'Show activation bars',
239
239
  values: ['on', 'off'],
240
240
  },
241
- 'collapse-notes': {
242
- description: 'Collapse note blocks',
243
- values: ['yes', 'no'],
244
- },
245
241
  'active-tag': { description: 'Active tag group name' },
246
242
  }),
247
243
  ],
package/src/d3.ts CHANGED
@@ -1506,8 +1506,15 @@ export function parseVisualization(
1506
1506
  result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1507
1507
  suggest
1508
1508
  );
1509
- validateTagGroupNames(result.timelineTagGroups, (line, msg) =>
1510
- result.diagnostics.push(makeDgmoError(line, msg, 'warning'))
1509
+ validateTagGroupNames(
1510
+ result.timelineTagGroups,
1511
+ (line, msg) =>
1512
+ result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1513
+ (line, msg) => {
1514
+ const diag = makeDgmoError(line, msg);
1515
+ result.diagnostics.push(diag);
1516
+ if (!result.error) result.error = formatDgmoError(diag);
1517
+ }
1511
1518
  );
1512
1519
  for (const group of result.timelineTagGroups) {
1513
1520
  if (!group.defaultValue) continue;
@@ -2674,8 +2681,10 @@ function renderEras(
2674
2681
  eras.forEach((era, i) => {
2675
2682
  const startVal = parseTimelineDate(era.startDate);
2676
2683
  const endVal = parseTimelineDate(era.endDate);
2684
+ if (!Number.isFinite(startVal) || !Number.isFinite(endVal)) return;
2677
2685
  const start = scale(startVal);
2678
2686
  const end = scale(endVal);
2687
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
2679
2688
  const color = era.color || eraColors[i % eraColors.length];
2680
2689
 
2681
2690
  const eraG = g
@@ -2765,7 +2774,9 @@ function renderMarkers(
2765
2774
 
2766
2775
  markers.forEach((marker) => {
2767
2776
  const dateVal = parseTimelineDate(marker.date);
2777
+ if (!Number.isFinite(dateVal)) return;
2768
2778
  const pos = scale(dateVal);
2779
+ if (!Number.isFinite(pos)) return;
2769
2780
  const color = marker.color || defaultColor;
2770
2781
  const lineOpacity = 0.5;
2771
2782
  const diamondSize = 5;
@@ -3473,6 +3484,33 @@ export function renderTimeline(
3473
3484
  latestEndDateStr = ev.endDate ?? ev.date;
3474
3485
  }
3475
3486
  }
3487
+
3488
+ // Eras and markers anchor the time axis — fold their dates into the
3489
+ // domain so out-of-range items still render within the chart.
3490
+ for (const era of timelineEras) {
3491
+ const eraStartNum = parseTimelineDate(era.startDate);
3492
+ const eraEndNum = parseTimelineDate(era.endDate);
3493
+ if (Number.isFinite(eraStartNum) && eraStartNum < minDate) {
3494
+ minDate = eraStartNum;
3495
+ earliestStartDateStr = era.startDate;
3496
+ }
3497
+ if (Number.isFinite(eraEndNum) && eraEndNum > maxDate) {
3498
+ maxDate = eraEndNum;
3499
+ latestEndDateStr = era.endDate;
3500
+ }
3501
+ }
3502
+ for (const marker of timelineMarkers) {
3503
+ const markerNum = parseTimelineDate(marker.date);
3504
+ if (!Number.isFinite(markerNum)) continue;
3505
+ if (markerNum < minDate) {
3506
+ minDate = markerNum;
3507
+ earliestStartDateStr = marker.date;
3508
+ }
3509
+ if (markerNum > maxDate) {
3510
+ maxDate = markerNum;
3511
+ latestEndDateStr = marker.date;
3512
+ }
3513
+ }
3476
3514
  const datePadding = (maxDate - minDate) * 0.05 || 0.5;
3477
3515
 
3478
3516
  const FADE_OPACITY = 0.1;
@@ -26,6 +26,7 @@ import { parsePyramid } from './pyramid/parser';
26
26
  import { parseFirstLine } from './utils/parsing';
27
27
  import { makeDgmoError, suggest } from './diagnostics';
28
28
  import type { DgmoError } from './diagnostics';
29
+ import { chartTypes } from './chart-types';
29
30
 
30
31
  // ============================================================
31
32
  // Content-based chart type inference helpers
@@ -192,129 +193,98 @@ export function isExtendedChartType(chartType: string): boolean {
192
193
  return EXTENDED_CHART_TYPES.has(chartType.toLowerCase());
193
194
  }
194
195
 
195
- /** Standard chart types parsed by parseChart (then rendered via ECharts). Internal use. */
196
- const STANDARD_CHART_TYPES = new Set([
197
- 'bar',
198
- 'line',
199
- 'multi-line',
200
- 'area',
201
- 'pie',
202
- 'doughnut',
203
- 'radar',
204
- 'polar-area',
205
- 'bar-stacked',
206
- ]);
207
-
208
196
  /**
209
- * Returns all supported chart type identifiers.
210
- * Useful for CLI enumeration and autocomplete.
197
+ * Returns all supported chart type identifiers in canonical (tier) order,
198
+ * derived from `chartTypes`. Consumers that need alphabetical order should
199
+ * call `.sort()` explicitly.
211
200
  */
212
201
  export function getAllChartTypes(): string[] {
213
- return [...DATA_CHART_TYPES, ...VISUALIZATION_TYPES, ...DIAGRAM_TYPES];
202
+ return chartTypes.map((c) => c.id);
214
203
  }
215
204
 
216
205
  /**
217
- * Canonical descriptions for every supported chart type. Shared by the CLI
218
- * `--chart-types` flag, the editor autocomplete popup, and the MCP
219
- * `list_chart_types` tool so all three surfaces stay in sync.
206
+ * Canonical descriptions for every supported chart type. Derived from
207
+ * `chartTypes` so there is exactly one place to update when adding a new
208
+ * type. Consumed by the CLI `--chart-types` flag, the editor autocomplete
209
+ * popup, and the MCP `list_chart_types` tool.
220
210
  */
221
- export const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
222
- bar: 'Bar chart — categorical comparisons',
223
- line: 'Line chart — trends over time; supports era bands (era start -> end Label (color)) for annotating named periods',
224
- 'multi-line':
225
- 'Multi-line chart — multiple series trends over time; supports era bands',
226
- area: 'Area chart — filled line chart; supports era bands',
227
- pie: 'Pie chart — part-to-whole proportions',
228
- doughnut: 'Doughnut chart — ring-style pie chart',
229
- radar: 'Radar chart — multi-dimensional metrics',
230
- 'polar-area': 'Polar area chart — radial bar chart',
231
- 'bar-stacked': 'Stacked bar chart — multi-series categorical',
232
- scatter: 'Scatter plot — 2D data points or bubble chart',
233
- sankey: 'Sankey diagram — flow/allocation visualization',
234
- chord: 'Chord diagram — circular flow relationships',
235
- function: 'Function plot — mathematical expressions',
236
- heatmap: 'Heatmap — matrix intensity visualization',
237
- funnel: 'Funnel chart — conversion pipeline',
238
- slope: 'Slope chart — change between two periods',
239
- wordcloud: 'Word cloud — term frequency visualization',
240
- arc: 'Arc diagram — network relationships',
241
- timeline: 'Timeline — events, eras, and date ranges',
242
- venn: 'Venn diagram — set overlaps',
243
- quadrant: 'Quadrant chart — 2x2 positioning matrix',
244
- 'tech-radar':
245
- 'Tech radar — technology adoption quadrants (adopt/trial/assess/hold)',
246
- cycle:
247
- 'Cycle diagram — cyclical process visualization (PDCA, OODA, DevOps loops)',
248
- sequence: 'Sequence diagram — message/interaction flows',
249
- flowchart: 'Flowchart — decision trees and process flows',
250
- class: 'Class diagram — UML class hierarchies',
251
- er: 'ER diagram — database schemas and relationships',
252
- org: 'Org chart — hierarchical tree structures',
253
- kanban: 'Kanban board — task/workflow columns',
254
- c4: 'C4 diagram — system architecture (context, container, component, deployment)',
255
- state: 'State diagram — state machine / lifecycle transitions',
256
- sitemap:
257
- 'Sitemap — navigable UI structure with pages, groups, and cross-link arrows',
258
- infra:
259
- 'Infrastructure diagram — traffic flow with RPS computation, capacity modeling, and latency analysis',
260
- gantt:
261
- 'Gantt chart — project scheduling with task dependencies and milestones',
262
- 'boxes-and-lines':
263
- 'Boxes and lines — general-purpose node-edge diagrams with nested groups, tags, and shape inference',
264
- mindmap: 'Mindmap — radial hierarchy of ideas branching from a central topic',
265
- wireframe:
266
- 'Wireframe — low-fidelity UI layout with panels, controls, and annotations',
267
- 'journey-map':
268
- 'Journey map — user experience flow with emotion scores, phases, and annotations',
269
- pyramid:
270
- 'Pyramid — hierarchical layered pyramid (Maslow, DIKW, learning pyramid); inverted for funnel-of-learning style',
271
- };
272
-
273
- // ECharts-native types parsed by parseExtendedChart
274
- const ECHART_TYPES = new Set([
275
- 'scatter',
276
- 'sankey',
277
- 'chord',
278
- 'function',
279
- 'heatmap',
280
- 'funnel',
281
- ]);
211
+ export const CHART_TYPE_DESCRIPTIONS: Record<string, string> =
212
+ Object.fromEntries(chartTypes.map((c) => [c.id, c.description]));
282
213
 
283
- /** Map chart type strings to their parse function (content → { diagnostics }). */
284
- const PARSE_DISPATCH = new Map<
285
- string,
286
- (content: string) => { diagnostics: DgmoError[] }
287
- >([
288
- ['sequence', (c) => parseSequenceDgmo(c)],
289
- ['flowchart', (c) => parseFlowchart(c)],
290
- ['class', (c) => parseClassDiagram(c)],
291
- ['er', (c) => parseERDiagram(c)],
292
- ['org', (c) => parseOrg(c)],
293
- ['kanban', (c) => parseKanban(c)],
294
- ['c4', (c) => parseC4(c)],
295
- ['state', (c) => parseState(c)],
296
- ['sitemap', (c) => parseSitemap(c)],
297
- ['infra', (c) => parseInfra(c)],
298
- ['gantt', (c) => parseGantt(c)],
299
- ['boxes-and-lines', (c) => parseBoxesAndLines(c)],
300
- ['mindmap', (c) => parseMindmap(c)],
301
- ['wireframe', (c) => parseWireframe(c)],
302
- ['tech-radar', (c) => parseTechRadar(c)],
303
- ['cycle', (c) => parseCycle(c)],
304
- ['journey-map', (c) => parseJourneyMap(c)],
305
- ['pyramid', (c) => parsePyramid(c)],
306
- ]);
214
+ // ============================================================
215
+ // Parser registry single source of truth for id → parser
216
+ // ============================================================
217
+
218
+ type ParseResult = { diagnostics: DgmoError[] };
219
+ type ParseFn = (content: string) => ParseResult;
307
220
 
308
221
  /**
309
- * Parse DGMO content and return diagnostics without rendering.
310
- * Useful for the CLI and editor to surface all errors before attempting render.
222
+ * Maps every chart-type id to the parser that handles it. Adding a new
223
+ * chart type means:
224
+ * 1. Add an entry here.
225
+ * 2. Add an entry to `chartTypes` in `chart-types.ts`.
226
+ *
227
+ * The `chart-types.test.ts` cross-check asserts both sets are identical;
228
+ * forgetting either side trips the test.
311
229
  */
230
+ export const chartTypeParsers: ReadonlyArray<readonly [string, ParseFn]> = [
231
+ // Structured diagrams (direct parsers)
232
+ ['sequence', parseSequenceDgmo],
233
+ ['flowchart', parseFlowchart],
234
+ ['class', parseClassDiagram],
235
+ ['er', parseERDiagram],
236
+ ['state', parseState],
237
+ ['org', parseOrg],
238
+ ['kanban', parseKanban],
239
+ ['c4', parseC4],
240
+ ['sitemap', parseSitemap],
241
+ ['infra', parseInfra],
242
+ ['gantt', parseGantt],
243
+ ['boxes-and-lines', parseBoxesAndLines],
244
+ ['mindmap', parseMindmap],
245
+ ['wireframe', parseWireframe],
246
+ ['tech-radar', parseTechRadar],
247
+ ['cycle', parseCycle],
248
+ ['journey-map', parseJourneyMap],
249
+ ['pyramid', parsePyramid],
250
+
251
+ // Standard ECharts charts (parseChart)
252
+ ['bar', parseChart],
253
+ ['line', parseChart],
254
+ ['multi-line', parseChart],
255
+ ['area', parseChart],
256
+ ['pie', parseChart],
257
+ ['doughnut', parseChart],
258
+ ['radar', parseChart],
259
+ ['polar-area', parseChart],
260
+ ['bar-stacked', parseChart],
261
+
262
+ // Extended ECharts charts (parseExtendedChart)
263
+ ['scatter', parseExtendedChart],
264
+ ['sankey', parseExtendedChart],
265
+ ['chord', parseExtendedChart],
266
+ ['function', parseExtendedChart],
267
+ ['heatmap', parseExtendedChart],
268
+ ['funnel', parseExtendedChart],
269
+
270
+ // D3 visualizations (parseVisualization)
271
+ ['slope', parseVisualization],
272
+ ['wordcloud', parseVisualization],
273
+ ['arc', parseVisualization],
274
+ ['timeline', parseVisualization],
275
+ ['venn', parseVisualization],
276
+ ['quadrant', parseVisualization],
277
+ ];
278
+
279
+ /** Ids in the same order as `chartTypeParsers`; used for cross-checks. */
280
+ export const knownChartTypeIds: readonly string[] = chartTypeParsers.map(
281
+ ([id]) => id
282
+ );
283
+
284
+ const PARSER_BY_ID: Map<string, ParseFn> = new Map(chartTypeParsers);
285
+
312
286
  /** All known chart type names for colon-pattern detection. */
313
- const ALL_KNOWN_TYPES = new Set([
314
- ...DATA_CHART_TYPES,
315
- ...VISUALIZATION_TYPES,
316
- ...DIAGRAM_TYPES,
317
- ]);
287
+ const ALL_KNOWN_TYPES: ReadonlySet<string> = new Set(knownChartTypeIds);
318
288
 
319
289
  /**
320
290
  * Parse DGMO content and return diagnostics without rendering.
@@ -341,31 +311,16 @@ export function parseDgmo(content: string): {
341
311
  };
342
312
  }
343
313
 
344
- const directParser = PARSE_DISPATCH.get(chartType);
345
- if (directParser) {
346
- const result = directParser(content);
347
- return {
348
- diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
349
- chartType,
350
- };
351
- }
352
-
353
- if (STANDARD_CHART_TYPES.has(chartType)) {
354
- const result = parseChart(content);
355
- return {
356
- diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
357
- chartType,
358
- };
359
- }
360
- if (ECHART_TYPES.has(chartType)) {
361
- const result = parseExtendedChart(content);
314
+ const parser = PARSER_BY_ID.get(chartType);
315
+ if (parser) {
316
+ const result = parser(content);
362
317
  return {
363
318
  diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
364
319
  chartType,
365
320
  };
366
321
  }
367
322
 
368
- // Visualization types (slope, wordcloud, arc, timeline, venn, quadrant)
323
+ // Unknown id (defensive): fall through to visualization parser.
369
324
  const result = parseVisualization(content);
370
325
  return {
371
326
  diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
@@ -140,8 +140,6 @@ export const DIRECTIVE_KEYWORDS = new Set([
140
140
  // Sequence
141
141
  'activations',
142
142
  'no-activations',
143
- 'collapse-notes',
144
- 'no-collapse-notes',
145
143
  // Data charts
146
144
  'stacked',
147
145
  'no-label-name',
package/src/fonts.ts CHANGED
@@ -1,2 +1,3 @@
1
- export const FONT_FAMILY = 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif';
2
- export const DEFAULT_FONT_NAME = 'Helvetica';
1
+ export const FONT_FAMILY =
2
+ 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif';
3
+ export const DEFAULT_FONT_NAME = 'Inter';
@@ -926,7 +926,11 @@ export function parseGantt(
926
926
  result.options.sort = 'default';
927
927
  }
928
928
 
929
- validateTagGroupNames(result.tagGroups, warn);
929
+ validateTagGroupNames(result.tagGroups, warn, (line, msg) => {
930
+ const diag = makeDgmoError(line, msg);
931
+ diagnostics.push(diag);
932
+ if (!result.error) result.error = formatDgmoError(diag);
933
+ });
930
934
 
931
935
  // ── Sprint mode detection ──────────────────────────────
932
936
  const hasSprintOption =