@diagrammo/dgmo 0.2.22 → 0.2.24

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.
@@ -0,0 +1,86 @@
1
+ // ============================================================
2
+ // C4 Architecture Diagram — Types
3
+ // ============================================================
4
+
5
+ import type { OrgTagGroup, OrgTagEntry } from '../org/parser';
6
+ import type { DgmoError } from '../diagnostics';
7
+
8
+ // Re-export tag types for convenience
9
+ export type { OrgTagGroup as C4TagGroup, OrgTagEntry as C4TagEntry };
10
+
11
+ // ── String unions ────────────────────────────────────────────
12
+
13
+ export type C4ElementType = 'person' | 'system' | 'container' | 'component';
14
+
15
+ export type C4Shape =
16
+ | 'default'
17
+ | 'database'
18
+ | 'cache'
19
+ | 'queue'
20
+ | 'cloud'
21
+ | 'external';
22
+
23
+ export type C4ArrowType =
24
+ | 'sync'
25
+ | 'async'
26
+ | 'bidirectional'
27
+ | 'bidirectional-async';
28
+
29
+ // ── Relationships ────────────────────────────────────────────
30
+
31
+ export interface C4Relationship {
32
+ target: string;
33
+ label?: string;
34
+ technology?: string;
35
+ arrowType: C4ArrowType;
36
+ lineNumber: number;
37
+ }
38
+
39
+ // ── Groups ───────────────────────────────────────────────────
40
+
41
+ export interface C4Group {
42
+ name: string;
43
+ children: C4Element[];
44
+ lineNumber: number;
45
+ }
46
+
47
+ // ── Elements ─────────────────────────────────────────────────
48
+
49
+ export interface C4Element {
50
+ name: string;
51
+ type: C4ElementType;
52
+ shape: C4Shape;
53
+ metadata: Record<string, string>;
54
+ children: C4Element[];
55
+ groups: C4Group[];
56
+ relationships: C4Relationship[];
57
+ importPath?: string;
58
+ lineNumber: number;
59
+ sectionHeader?: 'containers' | 'components';
60
+ sectionHeaderLineNumber?: number;
61
+ }
62
+
63
+ // ── Deployment ───────────────────────────────────────────────
64
+
65
+ export interface C4DeploymentNode {
66
+ name: string;
67
+ metadata: Record<string, string>;
68
+ shape: C4Shape;
69
+ children: C4DeploymentNode[];
70
+ containerRefs: string[];
71
+ lineNumber: number;
72
+ }
73
+
74
+ // ── Parsed result ────────────────────────────────────────────
75
+
76
+ export interface ParsedC4 {
77
+ title: string | null;
78
+ titleLineNumber: number | null;
79
+ options: Record<string, string>;
80
+ tagGroups: OrgTagGroup[];
81
+ elements: C4Element[];
82
+ relationships: C4Relationship[];
83
+ deployment: C4DeploymentNode[];
84
+ diagnostics: DgmoError[];
85
+ error: string | null;
86
+ }
@@ -53,7 +53,7 @@ function modifierColor(modifier: ClassModifier | undefined, palette: PaletteColo
53
53
 
54
54
  function nodeFill(palette: PaletteColors, isDark: boolean, modifier: ClassModifier | undefined, nodeColor?: string, colorOff?: boolean): string {
55
55
  const color = nodeColor ?? modifierColor(modifier, palette, colorOff);
56
- return mix(color, isDark ? palette.surface : palette.bg, 20);
56
+ return mix(color, isDark ? palette.surface : palette.bg, 25);
57
57
  }
58
58
 
59
59
  function nodeStroke(palette: PaletteColors, modifier: ClassModifier | undefined, nodeColor?: string, colorOff?: boolean): string {
@@ -137,7 +137,7 @@ export function renderClassDiagram(
137
137
  const scaledW = diagramW * scale;
138
138
  const scaledH = diagramH * scale;
139
139
  const offsetX = (width - scaledW) / 2;
140
- const offsetY = titleHeight + (availH - scaledH) / 2;
140
+ const offsetY = titleHeight + DIAGRAM_PADDING;
141
141
 
142
142
  const svg = d3Selection
143
143
  .select(container)
package/src/cli.ts CHANGED
@@ -31,16 +31,19 @@ function printHelp(): void {
31
31
  Render a .dgmo file to PNG (default) or SVG.
32
32
 
33
33
  Options:
34
- -o <file> Output file (default: <input>.png in cwd)
35
- Format inferred from extension: .svg → SVG, else PNG
36
- Use -o url to output a shareable diagrammo.app URL
37
- With stdin and no -o, PNG is written to stdout
38
- --theme <theme> Theme: ${THEMES.join(', ')} (default: light)
39
- --palette <name> Palette: ${PALETTES.join(', ')} (default: nord)
40
- --no-branding Omit diagrammo.app branding from exports
41
- --copy Copy URL to clipboard (only with -o url)
42
- --help Show this help
43
- --version Show version`);
34
+ -o <file> Output file (default: <input>.png in cwd)
35
+ Format inferred from extension: .svg → SVG, else PNG
36
+ Use -o url to output a shareable diagrammo.app URL
37
+ With stdin and no -o, PNG is written to stdout
38
+ --theme <theme> Theme: ${THEMES.join(', ')} (default: light)
39
+ --palette <name> Palette: ${PALETTES.join(', ')} (default: nord)
40
+ --c4-level <level> C4 render level: context (default), containers, components, deployment
41
+ --c4-system <name> System to drill into (with --c4-level containers or components)
42
+ --c4-container <name> Container to drill into (with --c4-level components)
43
+ --no-branding Omit diagrammo.app branding from exports
44
+ --copy Copy URL to clipboard (only with -o url)
45
+ --help Show this help
46
+ --version Show version`);
44
47
  }
45
48
 
46
49
  function printVersion(): void {
@@ -59,6 +62,9 @@ function parseArgs(argv: string[]): {
59
62
  version: boolean;
60
63
  noBranding: boolean;
61
64
  copy: boolean;
65
+ c4Level: 'context' | 'containers' | 'components' | 'deployment';
66
+ c4System: string | undefined;
67
+ c4Container: string | undefined;
62
68
  } {
63
69
  const result = {
64
70
  input: undefined as string | undefined,
@@ -69,6 +75,9 @@ function parseArgs(argv: string[]): {
69
75
  version: false,
70
76
  noBranding: false,
71
77
  copy: false,
78
+ c4Level: 'context' as 'context' | 'containers' | 'components' | 'deployment',
79
+ c4System: undefined as string | undefined,
80
+ c4Container: undefined as string | undefined,
72
81
  };
73
82
 
74
83
  const args = argv.slice(2); // skip node + script
@@ -106,6 +115,22 @@ function parseArgs(argv: string[]): {
106
115
  }
107
116
  result.palette = val;
108
117
  i++;
118
+ } else if (arg === '--c4-level') {
119
+ const val = args[++i];
120
+ if (val !== 'context' && val !== 'containers' && val !== 'components' && val !== 'deployment') {
121
+ console.error(
122
+ `Error: Invalid C4 level "${val}". Valid levels: context, containers, components, deployment`
123
+ );
124
+ process.exit(1);
125
+ }
126
+ result.c4Level = val;
127
+ i++;
128
+ } else if (arg === '--c4-system') {
129
+ result.c4System = args[++i];
130
+ i++;
131
+ } else if (arg === '--c4-container') {
132
+ result.c4Container = args[++i];
133
+ i++;
109
134
  } else if (arg === '--no-branding') {
110
135
  result.noBranding = true;
111
136
  i++;
@@ -304,10 +329,29 @@ async function main(): Promise<void> {
304
329
  console.error(`\u2716 ${formatDgmoError(e)}`);
305
330
  }
306
331
 
332
+ // Validate C4 options
333
+ if (opts.c4Level === 'containers' && !opts.c4System) {
334
+ console.error('Error: --c4-system is required when --c4-level is containers');
335
+ process.exit(1);
336
+ }
337
+ if (opts.c4Level === 'components') {
338
+ if (!opts.c4System) {
339
+ console.error('Error: --c4-system is required when --c4-level is components');
340
+ process.exit(1);
341
+ }
342
+ if (!opts.c4Container) {
343
+ console.error('Error: --c4-container is required when --c4-level is components');
344
+ process.exit(1);
345
+ }
346
+ }
347
+
307
348
  const svg = await render(content, {
308
349
  theme: opts.theme,
309
350
  palette: opts.palette,
310
351
  branding: !opts.noBranding,
352
+ c4Level: opts.c4Level,
353
+ c4System: opts.c4System,
354
+ c4Container: opts.c4Container,
311
355
  });
312
356
 
313
357
  if (!svg) {
package/src/d3.ts CHANGED
@@ -429,15 +429,15 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
429
429
  if (result.type === 'timeline') {
430
430
  // Duration event: 2026-07-15->30d: description (d=days, w=weeks, m=months, y=years)
431
431
  // Supports decimals up to 2 places (e.g., 1.25y = 1 year 3 months)
432
- // Supports uncertain end with ? prefix (e.g., ->?3m fades out the last 20%)
432
+ // Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
433
433
  const durationMatch = line.match(
434
- /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\?)?(\d+(?:\.\d{1,2})?)([dwmy])\s*:\s*(.+)$/
434
+ /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\d+(?:\.\d{1,2})?)([dwmy])(\?)?\s*:\s*(.+)$/
435
435
  );
436
436
  if (durationMatch) {
437
437
  const startDate = durationMatch[1];
438
- const uncertain = durationMatch[2] === '?';
439
- const amount = parseFloat(durationMatch[3]);
440
- const unit = durationMatch[4] as 'd' | 'w' | 'm' | 'y';
438
+ const uncertain = durationMatch[4] === '?';
439
+ const amount = parseFloat(durationMatch[2]);
440
+ const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y';
441
441
  const endDate = addDurationToDate(startDate, amount, unit);
442
442
  result.timelineEvents.push({
443
443
  date: startDate,
@@ -450,18 +450,18 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
450
450
  continue;
451
451
  }
452
452
 
453
- // Range event: 1655->1667: description (supports uncertain end: 1655->?1667)
453
+ // Range event: 1655->1667: description (supports uncertain end: 1655->1667?)
454
454
  const rangeMatch = line.match(
455
- /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\?)?(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*:\s*(.+)$/
455
+ /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\d{4}(?:-\d{2})?(?:-\d{2})?)(\?)?\s*:\s*(.+)$/
456
456
  );
457
457
  if (rangeMatch) {
458
458
  result.timelineEvents.push({
459
459
  date: rangeMatch[1],
460
- endDate: rangeMatch[3],
460
+ endDate: rangeMatch[2],
461
461
  label: rangeMatch[4].trim(),
462
462
  group: currentTimelineGroup,
463
463
  lineNumber,
464
- uncertain: rangeMatch[2] === '?',
464
+ uncertain: rangeMatch[3] === '?',
465
465
  });
466
466
  continue;
467
467
  }
@@ -5263,7 +5263,7 @@ export async function renderD3ForExport(
5263
5263
  activeTagGroup?: string | null;
5264
5264
  hiddenAttributes?: Set<string>;
5265
5265
  },
5266
- options?: { branding?: boolean }
5266
+ options?: { branding?: boolean; c4Level?: 'context' | 'containers' | 'components' | 'deployment'; c4System?: string; c4Container?: string }
5267
5267
  ): Promise<string> {
5268
5268
  // Flowchart and org chart use their own parser pipelines — intercept before parseD3()
5269
5269
  const { parseDgmoChartType } = await import('./dgmo-router');
@@ -5512,6 +5512,144 @@ export async function renderD3ForExport(
5512
5512
  }
5513
5513
  }
5514
5514
 
5515
+ if (detectedType === 'initiative-status') {
5516
+ const { parseInitiativeStatus } = await import('./initiative-status/parser');
5517
+ const { layoutInitiativeStatus } = await import('./initiative-status/layout');
5518
+ const { renderInitiativeStatus } = await import('./initiative-status/renderer');
5519
+
5520
+ const isDark = theme === 'dark';
5521
+ const { getPalette } = await import('./palettes');
5522
+ const effectivePalette =
5523
+ palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
5524
+
5525
+ const isParsed = parseInitiativeStatus(content);
5526
+ if (isParsed.error || isParsed.nodes.length === 0) return '';
5527
+
5528
+ const isLayout = layoutInitiativeStatus(isParsed);
5529
+ const PADDING = 20;
5530
+ const titleOffset = isParsed.title ? 40 : 0;
5531
+ const exportWidth = isLayout.width + PADDING * 2;
5532
+ const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
5533
+
5534
+ const container = document.createElement('div');
5535
+ container.style.width = `${exportWidth}px`;
5536
+ container.style.height = `${exportHeight}px`;
5537
+ container.style.position = 'absolute';
5538
+ container.style.left = '-9999px';
5539
+ document.body.appendChild(container);
5540
+
5541
+ try {
5542
+ renderInitiativeStatus(
5543
+ container,
5544
+ isParsed,
5545
+ isLayout,
5546
+ effectivePalette,
5547
+ isDark,
5548
+ undefined,
5549
+ { width: exportWidth, height: exportHeight }
5550
+ );
5551
+
5552
+ const svgEl = container.querySelector('svg');
5553
+ if (!svgEl) return '';
5554
+
5555
+ if (theme === 'transparent') {
5556
+ svgEl.style.background = 'none';
5557
+ } else if (!svgEl.style.background) {
5558
+ svgEl.style.background = effectivePalette.bg;
5559
+ }
5560
+
5561
+ svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
5562
+ svgEl.style.fontFamily = FONT_FAMILY;
5563
+
5564
+ const svgHtml = svgEl.outerHTML;
5565
+ if (options?.branding !== false) {
5566
+ const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
5567
+ return injectBranding(svgHtml, brandColor);
5568
+ }
5569
+ return svgHtml;
5570
+ } finally {
5571
+ document.body.removeChild(container);
5572
+ }
5573
+ }
5574
+
5575
+ if (detectedType === 'c4') {
5576
+ const { parseC4 } = await import('./c4/parser');
5577
+ const { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment } = await import('./c4/layout');
5578
+ const { renderC4Context, renderC4Containers } = await import('./c4/renderer');
5579
+
5580
+ const isDark = theme === 'dark';
5581
+ const { getPalette } = await import('./palettes');
5582
+ const effectivePalette =
5583
+ palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
5584
+
5585
+ const c4Parsed = parseC4(content, effectivePalette);
5586
+ if (c4Parsed.error || c4Parsed.elements.length === 0) return '';
5587
+
5588
+ // Container/component-level rendering
5589
+ const c4Level = options?.c4Level ?? 'context';
5590
+ const c4System = options?.c4System;
5591
+ const c4Container = options?.c4Container;
5592
+
5593
+ const c4Layout = c4Level === 'deployment'
5594
+ ? layoutC4Deployment(c4Parsed)
5595
+ : c4Level === 'components' && c4System && c4Container
5596
+ ? layoutC4Components(c4Parsed, c4System, c4Container)
5597
+ : c4Level === 'containers' && c4System
5598
+ ? layoutC4Containers(c4Parsed, c4System)
5599
+ : layoutC4Context(c4Parsed);
5600
+
5601
+ if (c4Layout.nodes.length === 0) return '';
5602
+
5603
+ const PADDING = 20;
5604
+ const titleOffset = c4Parsed.title ? 40 : 0;
5605
+ const exportWidth = c4Layout.width + PADDING * 2;
5606
+ const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
5607
+
5608
+ const container = document.createElement('div');
5609
+ container.style.width = `${exportWidth}px`;
5610
+ container.style.height = `${exportHeight}px`;
5611
+ container.style.position = 'absolute';
5612
+ container.style.left = '-9999px';
5613
+ document.body.appendChild(container);
5614
+
5615
+ try {
5616
+ const renderFn = c4Level === 'deployment' || (c4Level === 'components' && c4System && c4Container) || (c4Level === 'containers' && c4System)
5617
+ ? renderC4Containers
5618
+ : renderC4Context;
5619
+
5620
+ renderFn(
5621
+ container,
5622
+ c4Parsed,
5623
+ c4Layout,
5624
+ effectivePalette,
5625
+ isDark,
5626
+ undefined,
5627
+ { width: exportWidth, height: exportHeight }
5628
+ );
5629
+
5630
+ const svgEl = container.querySelector('svg');
5631
+ if (!svgEl) return '';
5632
+
5633
+ if (theme === 'transparent') {
5634
+ svgEl.style.background = 'none';
5635
+ } else if (!svgEl.style.background) {
5636
+ svgEl.style.background = effectivePalette.bg;
5637
+ }
5638
+
5639
+ svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
5640
+ svgEl.style.fontFamily = FONT_FAMILY;
5641
+
5642
+ const svgHtml = svgEl.outerHTML;
5643
+ if (options?.branding !== false) {
5644
+ const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
5645
+ return injectBranding(svgHtml, brandColor);
5646
+ }
5647
+ return svgHtml;
5648
+ } finally {
5649
+ document.body.removeChild(container);
5650
+ }
5651
+ }
5652
+
5515
5653
  if (detectedType === 'flowchart') {
5516
5654
  const { parseFlowchart } = await import('./graph/flowchart-parser');
5517
5655
  const { layoutGraph } = await import('./graph/layout');
@@ -11,6 +11,8 @@ import { parseEChart } from './echarts';
11
11
  import { parseD3 } from './d3';
12
12
  import { parseOrg, looksLikeOrg } from './org/parser';
13
13
  import { parseKanban } from './kanban/parser';
14
+ import { parseC4 } from './c4/parser';
15
+ import { looksLikeInitiativeStatus, parseInitiativeStatus } from './initiative-status/parser';
14
16
  import type { DgmoError } from './diagnostics';
15
17
 
16
18
  /**
@@ -58,6 +60,8 @@ export const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
58
60
  er: 'd3',
59
61
  org: 'd3',
60
62
  kanban: 'd3',
63
+ c4: 'd3',
64
+ 'initiative-status': 'd3',
61
65
  };
62
66
 
63
67
  /**
@@ -89,6 +93,7 @@ export function parseDgmoChartType(content: string): string | null {
89
93
  if (looksLikeFlowchart(content)) return 'flowchart';
90
94
  if (looksLikeClassDiagram(content)) return 'class';
91
95
  if (looksLikeERDiagram(content)) return 'er';
96
+ if (looksLikeInitiativeStatus(content)) return 'initiative-status';
92
97
  if (looksLikeOrg(content)) return 'org';
93
98
 
94
99
  return null;
@@ -142,6 +147,14 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
142
147
  const parsed = parseKanban(content);
143
148
  return { diagnostics: parsed.diagnostics };
144
149
  }
150
+ if (chartType === 'c4') {
151
+ const parsed = parseC4(content);
152
+ return { diagnostics: parsed.diagnostics };
153
+ }
154
+ if (chartType === 'initiative-status') {
155
+ const parsed = parseInitiativeStatus(content);
156
+ return { diagnostics: parsed.diagnostics };
157
+ }
145
158
  if (STANDARD_CHART_TYPES.has(chartType)) {
146
159
  const parsed = parseChart(content);
147
160
  return { diagnostics: parsed.diagnostics };
@@ -233,7 +233,7 @@ export function renderERDiagram(
233
233
  const scaledW = diagramW * scale;
234
234
  const scaledH = diagramH * scale;
235
235
  const offsetX = (width - scaledW) / 2;
236
- const offsetY = titleHeight + (availH - scaledH) / 2;
236
+ const offsetY = titleHeight + DIAGRAM_PADDING;
237
237
 
238
238
  const svg = d3Selection
239
239
  .select(container)
@@ -369,7 +369,7 @@ export function renderERDiagram(
369
369
 
370
370
  const w = node.width;
371
371
  const h = node.height;
372
- const fill = mix(nodeColor, isDark ? palette.surface : palette.bg, 20);
372
+ const fill = mix(nodeColor, isDark ? palette.surface : palette.bg, 25);
373
373
  const stroke = nodeColor;
374
374
 
375
375
  // Outer rectangle
@@ -259,7 +259,7 @@ export function renderFlowchart(
259
259
  const scaledW = diagramW * scale;
260
260
  const scaledH = diagramH * scale;
261
261
  const offsetX = (width - scaledW) / 2;
262
- const offsetY = titleHeight + (availH - scaledH) / 2;
262
+ const offsetY = titleHeight + DIAGRAM_PADDING;
263
263
 
264
264
  // Create SVG
265
265
  const svg = d3Selection
package/src/index.ts CHANGED
@@ -155,6 +155,60 @@ export type {
155
155
  export { computeCardMove, computeCardArchive, isArchiveColumn } from './kanban/mutations';
156
156
  export { renderKanban, renderKanbanForExport } from './kanban/renderer';
157
157
 
158
+ export { parseC4 } from './c4/parser';
159
+ export type {
160
+ ParsedC4,
161
+ C4Element,
162
+ C4ElementType,
163
+ C4Shape,
164
+ C4ArrowType,
165
+ C4Relationship,
166
+ C4Group,
167
+ C4DeploymentNode,
168
+ C4TagGroup,
169
+ C4TagEntry,
170
+ } from './c4/types';
171
+
172
+ export { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment, rollUpContextRelationships } from './c4/layout';
173
+ export type {
174
+ C4LayoutResult,
175
+ C4LayoutNode,
176
+ C4LayoutEdge,
177
+ C4LayoutBoundary,
178
+ C4LegendGroup,
179
+ C4LegendEntry,
180
+ ContextRelationship,
181
+ } from './c4/layout';
182
+
183
+ export {
184
+ renderC4Context,
185
+ renderC4ContextForExport,
186
+ renderC4Containers,
187
+ renderC4ContainersForExport,
188
+ renderC4ComponentsForExport,
189
+ renderC4Deployment,
190
+ renderC4DeploymentForExport,
191
+ } from './c4/renderer';
192
+
193
+ export { parseInitiativeStatus, looksLikeInitiativeStatus } from './initiative-status/parser';
194
+ export type {
195
+ ParsedInitiativeStatus,
196
+ ISNode,
197
+ ISEdge,
198
+ ISGroup,
199
+ InitiativeStatus,
200
+ } from './initiative-status/types';
201
+
202
+ export { layoutInitiativeStatus } from './initiative-status/layout';
203
+ export type {
204
+ ISLayoutResult,
205
+ ISLayoutNode,
206
+ ISLayoutEdge,
207
+ ISLayoutGroup,
208
+ } from './initiative-status/layout';
209
+
210
+ export { renderInitiativeStatus, renderInitiativeStatusForExport } from './initiative-status/renderer';
211
+
158
212
  export { collapseOrgTree } from './org/collapse';
159
213
  export type { CollapsedOrgResult } from './org/collapse';
160
214