@diagrammo/dgmo 0.8.3 → 0.8.5

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 (122) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +452 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +188 -185
  9. package/dist/editor.cjs +338 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. package/src/utils/parsing.ts +75 -31
package/src/c4/parser.ts CHANGED
@@ -39,7 +39,8 @@ const ELEMENT_RE = /^(person|system|container|component)\s+(.+)$/i;
39
39
  const IS_A_RE = /\s+is\s+a(?:n)?\s+(\w+)\s*$/i;
40
40
 
41
41
  /** Matches `Name is a <type>` declarations (new preferred syntax) */
42
- const C4_IS_A_RE = /^([^:]+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b(.*)$/i;
42
+ const C4_IS_A_RE =
43
+ /^([^:]+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b(.*)$/i;
43
44
 
44
45
  /** Matches relationship arrows: `->`, `~>`, `<->`, `<~>` */
45
46
  const RELATIONSHIP_RE = /^(<?-?>|<?~?>)\s*(.+)$/;
@@ -63,7 +64,6 @@ const METADATA_RE = /^([a-z][a-z0-9-]*):\s+(.+)$/i;
63
64
  // Helpers
64
65
  // ============================================================
65
66
 
66
-
67
67
  const VALID_ELEMENT_TYPES = new Set<string>([
68
68
  'person',
69
69
  'system',
@@ -81,14 +81,10 @@ const VALID_SHAPES = new Set<string>([
81
81
  ]);
82
82
 
83
83
  /** Known top-level option keys for C4 diagrams. */
84
- const KNOWN_C4_OPTIONS = new Set<string>([
85
- 'layout',
86
- ]);
84
+ const KNOWN_C4_OPTIONS = new Set<string>(['layout']);
87
85
 
88
86
  /** Known C4 boolean options (bare keyword = on). */
89
- const KNOWN_C4_BOOLEANS = new Set<string>([
90
- 'direction-tb',
91
- ]);
87
+ const KNOWN_C4_BOOLEANS = new Set<string>(['direction-tb']);
92
88
 
93
89
  const ALL_CHART_TYPES = [
94
90
  'c4',
@@ -110,9 +106,7 @@ const ALL_CHART_TYPES = [
110
106
  ];
111
107
 
112
108
  /** Map from ParticipantType inference → C4Shape */
113
- function participantTypeToC4Shape(
114
- pType: string,
115
- ): C4Shape {
109
+ function participantTypeToC4Shape(pType: string): C4Shape {
116
110
  switch (pType) {
117
111
  case 'database':
118
112
  return 'database';
@@ -155,7 +149,6 @@ function parseArrowType(arrow: string): C4ArrowType | null {
155
149
  }
156
150
  }
157
151
 
158
-
159
152
  // ============================================================
160
153
  // Stack entry types
161
154
  // ============================================================
@@ -196,10 +189,7 @@ type StackEntry =
196
189
  // Parser
197
190
  // ============================================================
198
191
 
199
- export function parseC4(
200
- content: string,
201
- palette?: PaletteColors,
202
- ): ParsedC4 {
192
+ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
203
193
  const result: ParsedC4 = {
204
194
  title: null,
205
195
  titleLineNumber: null,
@@ -212,10 +202,15 @@ export function parseC4(
212
202
  error: null,
213
203
  };
214
204
 
215
- const pushError = (line: number, message: string, severity: 'error' | 'warning' = 'error'): void => {
205
+ const pushError = (
206
+ line: number,
207
+ message: string,
208
+ severity: 'error' | 'warning' = 'error'
209
+ ): void => {
216
210
  const diag = makeDgmoError(line, message, severity);
217
211
  result.diagnostics.push(diag);
218
- if (!result.error && severity === 'error') result.error = formatDgmoError(diag);
212
+ if (!result.error && severity === 'error')
213
+ result.error = formatDgmoError(diag);
219
214
  };
220
215
 
221
216
  const fail = (line: number, message: string): ParsedC4 => {
@@ -297,7 +292,10 @@ export function parseC4(
297
292
  lineNumber,
298
293
  };
299
294
  if (tagBlockMatch.alias) {
300
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
295
+ aliasMap.set(
296
+ tagBlockMatch.alias.toLowerCase(),
297
+ tagBlockMatch.name.toLowerCase()
298
+ );
301
299
  }
302
300
  result.tagGroups.push(currentTagGroup);
303
301
  continue;
@@ -329,7 +327,7 @@ export function parseC4(
329
327
  if (!color) {
330
328
  pushError(
331
329
  lineNumber,
332
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
330
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
333
331
  );
334
332
  continue;
335
333
  }
@@ -344,7 +342,7 @@ export function parseC4(
344
342
  });
345
343
  continue;
346
344
  }
347
- currentTagGroup = null;
345
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
348
346
  }
349
347
 
350
348
  // --- Content phase ---
@@ -367,7 +365,10 @@ export function parseC4(
367
365
  }
368
366
 
369
367
  // Check for top-level non-deployment content (section ended)
370
- if (indent === 0 && (C4_IS_A_RE.test(trimmed) || ELEMENT_RE.test(trimmed))) {
368
+ if (
369
+ indent === 0 &&
370
+ (C4_IS_A_RE.test(trimmed) || ELEMENT_RE.test(trimmed))
371
+ ) {
371
372
  inDeployment = false;
372
373
  // Fall through to element parsing below
373
374
  } else {
@@ -377,10 +378,13 @@ export function parseC4(
377
378
  const refName = refMatch[1].trim();
378
379
  if (deployStack.length > 0) {
379
380
  deployStack[deployStack.length - 1].node.containerRefs.push(
380
- refName,
381
+ refName
381
382
  );
382
383
  } else {
383
- pushError(lineNumber, `"container ${refName}" must be inside a deployment node`);
384
+ pushError(
385
+ lineNumber,
386
+ `"container ${refName}" must be inside a deployment node`
387
+ );
384
388
  }
385
389
  continue;
386
390
  }
@@ -388,8 +392,13 @@ export function parseC4(
388
392
  // Otherwise it's a deployment node (possibly with pipe metadata)
389
393
  const segments = trimmed.split('|').map((s) => s.trim());
390
394
  const nodeName = segments[0];
391
- const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
392
- const shape = inferC4Shape(nodeName, metadata.tech ?? metadata.technology);
395
+ const metadata = parsePipeMetadata(segments, aliasMap, () =>
396
+ pushError(lineNumber, MULTIPLE_PIPE_ERROR)
397
+ );
398
+ const shape = inferC4Shape(
399
+ nodeName,
400
+ metadata.tech ?? metadata.technology
401
+ );
393
402
 
394
403
  const dNode: C4DeploymentNode = {
395
404
  name: nodeName,
@@ -423,8 +432,9 @@ export function parseC4(
423
432
  // containers / components must be inside an element
424
433
  const parentEntry = findParentElement(indent, stack);
425
434
  if (parentEntry) {
426
- parentEntry.element.sectionHeader =
427
- sectionType as 'containers' | 'components';
435
+ parentEntry.element.sectionHeader = sectionType as
436
+ | 'containers'
437
+ | 'components';
428
438
  parentEntry.element.sectionHeaderLineNumber = lineNumber;
429
439
  stack.push({
430
440
  kind: 'section',
@@ -433,10 +443,7 @@ export function parseC4(
433
443
  indent,
434
444
  });
435
445
  } else {
436
- pushError(
437
- lineNumber,
438
- `"${sectionType}" must be inside an element`,
439
- );
446
+ pushError(lineNumber, `"${sectionType}" must be inside an element`);
440
447
  }
441
448
  continue;
442
449
  }
@@ -493,9 +500,15 @@ export function parseC4(
493
500
  if (!rawLabel) break; // empty label — fall through to plain arrow
494
501
 
495
502
  // Reject bidirectional arrows
496
- if (arrowType === 'bidirectional' || arrowType === 'bidirectional-async') {
503
+ if (
504
+ arrowType === 'bidirectional' ||
505
+ arrowType === 'bidirectional-async'
506
+ ) {
497
507
  const source = findParentElement(indent, stack)?.element.name ?? '?';
498
- pushError(lineNumber, `Bidirectional arrows are no longer supported. Replace with two separate arrows:\n -${rawLabel}-> ${targetBody}\n ${targetBody} -${rawLabel}-> ${source}`);
508
+ pushError(
509
+ lineNumber,
510
+ `Bidirectional arrows are no longer supported. Replace with two separate arrows:\n -${rawLabel}-> ${targetBody}\n ${targetBody} -${rawLabel}-> ${source}`
511
+ );
499
512
  labeledHandled = true;
500
513
  break;
501
514
  }
@@ -552,11 +565,17 @@ export function parseC4(
552
565
  const arrowType = parseArrowType(relMatch[1]);
553
566
  if (arrowType) {
554
567
  // Reject bidirectional arrows
555
- if (arrowType === 'bidirectional' || arrowType === 'bidirectional-async') {
568
+ if (
569
+ arrowType === 'bidirectional' ||
570
+ arrowType === 'bidirectional-async'
571
+ ) {
556
572
  const arrow = relMatch[1];
557
573
  const target = relMatch[2].trim();
558
574
  const source = findParentElement(indent, stack)?.element.name ?? '?';
559
- pushError(lineNumber, `'${arrow}' bidirectional arrows are no longer supported. Replace with two separate arrows:\n -> ${target}\n ${target} -> ${source}`);
575
+ pushError(
576
+ lineNumber,
577
+ `'${arrow}' bidirectional arrows are no longer supported. Replace with two separate arrows:\n -> ${target}\n ${target} -> ${source}`
578
+ );
560
579
  continue;
561
580
  }
562
581
 
@@ -605,14 +624,22 @@ export function parseC4(
605
624
  let segments: string[];
606
625
  if (remainderTrimmed.startsWith('|')) {
607
626
  // remainder has pipe metadata: "| tech: PostgreSQL, team: Data"
608
- segments = ['', ...remainderTrimmed.substring(1).split('|').map((s) => s.trim())];
627
+ segments = [
628
+ '',
629
+ ...remainderTrimmed
630
+ .substring(1)
631
+ .split('|')
632
+ .map((s) => s.trim()),
633
+ ];
609
634
  } else {
610
635
  segments = [remainderTrimmed];
611
636
  }
612
637
 
613
638
  // Check for additional `is a <shape>` in the name (e.g., already stripped by C4_IS_A_RE won't happen,
614
639
  // but handle remainder like "is a cylinder" after type)
615
- const remainderIsA = remainderTrimmed.match(/^\s*is\s+a(?:n)?\s+(\w+)\s*(.*)$/i);
640
+ const remainderIsA = remainderTrimmed.match(
641
+ /^\s*is\s+a(?:n)?\s+(\w+)\s*(.*)$/i
642
+ );
616
643
  if (remainderIsA) {
617
644
  const shapeName = remainderIsA[1].toLowerCase();
618
645
  if (VALID_SHAPES.has(shapeName)) {
@@ -620,13 +647,19 @@ export function parseC4(
620
647
  } else {
621
648
  pushError(
622
649
  lineNumber,
623
- `Unknown shape "${remainderIsA[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`,
650
+ `Unknown shape "${remainderIsA[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
624
651
  );
625
652
  }
626
653
  // Re-parse remainder after shape
627
654
  const afterShape = remainderIsA[2].trim();
628
655
  if (afterShape.startsWith('|')) {
629
- segments = ['', ...afterShape.substring(1).split('|').map((s) => s.trim())];
656
+ segments = [
657
+ '',
658
+ ...afterShape
659
+ .substring(1)
660
+ .split('|')
661
+ .map((s) => s.trim()),
662
+ ];
630
663
  } else {
631
664
  segments = [afterShape];
632
665
  }
@@ -641,13 +674,15 @@ export function parseC4(
641
674
  } else {
642
675
  pushError(
643
676
  lineNumber,
644
- `Unknown shape "${nameIsAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`,
677
+ `Unknown shape "${nameIsAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
645
678
  );
646
679
  }
647
680
  namePart = namePart.substring(0, nameIsAMatch.index!).trim();
648
681
  }
649
682
 
650
- const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
683
+ const metadata = parsePipeMetadata(segments, aliasMap, () =>
684
+ pushError(lineNumber, MULTIPLE_PIPE_ERROR)
685
+ );
651
686
 
652
687
  const shape =
653
688
  explicitShape ??
@@ -669,7 +704,7 @@ export function parseC4(
669
704
  if (existingLine !== undefined) {
670
705
  pushError(
671
706
  lineNumber,
672
- `Duplicate element name "${namePart}" (first defined on line ${existingLine})`,
707
+ `Duplicate element name "${namePart}" (first defined on line ${existingLine})`
673
708
  );
674
709
  } else {
675
710
  knownNames.set(namePart.toLowerCase(), lineNumber);
@@ -683,7 +718,7 @@ export function parseC4(
683
718
  const elementMatch = trimmed.match(ELEMENT_RE);
684
719
  if (elementMatch) {
685
720
  const elementType = elementMatch[1].toLowerCase() as C4ElementType;
686
- let nameAndRest = elementMatch[2];
721
+ const nameAndRest = elementMatch[2];
687
722
 
688
723
  // Split on pipe for inline metadata
689
724
  const segments = nameAndRest.split('|').map((s) => s.trim());
@@ -699,7 +734,7 @@ export function parseC4(
699
734
  } else {
700
735
  pushError(
701
736
  lineNumber,
702
- `Unknown shape "${isAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`,
737
+ `Unknown shape "${isAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
703
738
  );
704
739
  }
705
740
  namePart = namePart.substring(0, isAMatch.index!).trim();
@@ -708,10 +743,12 @@ export function parseC4(
708
743
  // Emit deprecation error with migration hint
709
744
  pushError(
710
745
  lineNumber,
711
- `'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`,
746
+ `'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`
712
747
  );
713
748
 
714
- const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
749
+ const metadata = parsePipeMetadata(segments, aliasMap, () =>
750
+ pushError(lineNumber, MULTIPLE_PIPE_ERROR)
751
+ );
715
752
 
716
753
  // Determine shape: explicit > inference
717
754
  const shape =
@@ -734,7 +771,7 @@ export function parseC4(
734
771
  if (existingLine !== undefined) {
735
772
  pushError(
736
773
  lineNumber,
737
- `Duplicate element name "${namePart}" (first defined on line ${existingLine})`,
774
+ `Duplicate element name "${namePart}" (first defined on line ${existingLine})`
738
775
  );
739
776
  } else {
740
777
  knownNames.set(namePart.toLowerCase(), lineNumber);
@@ -798,7 +835,7 @@ export function parseC4(
798
835
  /** Find the nearest parent element entry on the stack at shallower indent. */
799
836
  function findParentElement(
800
837
  indent: number,
801
- stack: StackEntry[],
838
+ stack: StackEntry[]
802
839
  ): ElementStackEntry | null {
803
840
  for (let i = stack.length - 1; i >= 0; i--) {
804
841
  const entry = stack[i];
@@ -824,7 +861,7 @@ function attachElement(
824
861
  element: C4Element,
825
862
  indent: number,
826
863
  stack: StackEntry[],
827
- result: ParsedC4,
864
+ result: ParsedC4
828
865
  ): void {
829
866
  // Find the immediate context: group, section, or parent element
830
867
  let attached = false;
@@ -866,7 +903,11 @@ function attachElement(
866
903
  function validateRelationshipTargets(
867
904
  result: ParsedC4,
868
905
  knownNames: Map<string, number>,
869
- pushWarning: (line: number, message: string, severity?: 'error' | 'warning') => void,
906
+ pushWarning: (
907
+ line: number,
908
+ message: string,
909
+ severity?: 'error' | 'warning'
910
+ ) => void
870
911
  ): void {
871
912
  function walkRels(elements: C4Element[]) {
872
913
  for (const el of elements) {
@@ -875,7 +916,7 @@ function validateRelationshipTargets(
875
916
  pushWarning(
876
917
  rel.lineNumber,
877
918
  `Relationship target "${rel.target}" not found`,
878
- 'warning',
919
+ 'warning'
879
920
  );
880
921
  }
881
922
  }
@@ -893,7 +934,7 @@ function validateRelationshipTargets(
893
934
  pushWarning(
894
935
  rel.lineNumber,
895
936
  `Relationship target "${rel.target}" not found`,
896
- 'warning',
937
+ 'warning'
897
938
  );
898
939
  }
899
940
  }
@@ -902,7 +943,11 @@ function validateRelationshipTargets(
902
943
  function validateDeploymentRefs(
903
944
  result: ParsedC4,
904
945
  knownNames: Map<string, number>,
905
- pushWarning: (line: number, message: string, severity?: 'error' | 'warning') => void,
946
+ pushWarning: (
947
+ line: number,
948
+ message: string,
949
+ severity?: 'error' | 'warning'
950
+ ) => void
906
951
  ): void {
907
952
  function walkDeploy(nodes: C4DeploymentNode[]) {
908
953
  for (const node of nodes) {
@@ -911,7 +956,7 @@ function validateDeploymentRefs(
911
956
  pushWarning(
912
957
  node.lineNumber,
913
958
  `Deployment reference "container ${ref}" not found`,
914
- 'warning',
959
+ 'warning'
915
960
  );
916
961
  }
917
962
  }
package/src/chart.ts CHANGED
@@ -21,9 +21,9 @@ export interface ChartDataPoint {
21
21
  }
22
22
 
23
23
  export interface ChartEra {
24
- start: string; // exact category label, e.g. "'77"
25
- end: string; // exact category label, e.g. "'81"
26
- label: string; // display name, e.g. "Carter"
24
+ start: string; // exact category label, e.g. "'77"
25
+ end: string; // exact category label, e.g. "'81"
26
+ label: string; // display name, e.g. "Carter"
27
27
  color: string | null; // resolved CSS color, or null → palette default
28
28
  lineNumber: number;
29
29
  }
@@ -62,7 +62,11 @@ export interface ParsedChart {
62
62
  import { resolveColor } from './colors';
63
63
  import type { PaletteColors } from './palettes';
64
64
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
65
- import { extractColor, normalizeGroupedNumber, parseFirstLine, parseSeriesNames } from './utils/parsing';
65
+ import {
66
+ extractColor,
67
+ parseFirstLine,
68
+ parseSeriesNames,
69
+ } from './utils/parsing';
66
70
 
67
71
  // ============================================================
68
72
  // Parser
@@ -85,15 +89,20 @@ const TYPE_ALIASES: Record<string, ChartType> = {
85
89
 
86
90
  /** Known option keywords for the simple chart parser. */
87
91
  const KNOWN_OPTIONS = new Set([
88
- 'chart', 'title', 'series', 'xlabel', 'ylabel', 'label',
89
- 'no-label-name', 'no-label-value', 'no-label-percent',
92
+ 'chart',
93
+ 'title',
94
+ 'series',
95
+ 'x-label',
96
+ 'y-label',
97
+ 'label',
98
+ 'no-label-name',
99
+ 'no-label-value',
100
+ 'no-label-percent',
90
101
  'color',
91
102
  ]);
92
103
 
93
104
  /** Known boolean options for the simple chart parser. */
94
- const KNOWN_BOOLEANS = new Set([
95
- 'orientation-horizontal',
96
- ]);
105
+ const KNOWN_BOOLEANS = new Set(['orientation-horizontal']);
97
106
 
98
107
  /**
99
108
  * Parses the simple chart text format into a structured object.
@@ -114,7 +123,12 @@ export function parseChart(
114
123
  ): ParsedChart {
115
124
  const lines = content.split('\n');
116
125
  const parsedEras: ChartEra[] = [];
117
- const rawEras: { start: string; afterArrow: string; color: string | null; lineNumber: number }[] = [];
126
+ const rawEras: {
127
+ start: string;
128
+ afterArrow: string;
129
+ color: string | null;
130
+ lineNumber: number;
131
+ }[] = [];
118
132
  const result: ParsedChart = {
119
133
  type: 'bar',
120
134
  data: [],
@@ -141,7 +155,12 @@ export function parseChart(
141
155
 
142
156
  // Reject legacy ## section headers
143
157
  if (/^#{2,}\s+/.test(trimmed)) {
144
- result.diagnostics.push(makeDgmoError(lineNumber, `'${trimmed}' — ## syntax is no longer supported. Use [Group] containers instead`));
158
+ result.diagnostics.push(
159
+ makeDgmoError(
160
+ lineNumber,
161
+ `'${trimmed}' — ## syntax is no longer supported. Use [Group] containers instead`
162
+ )
163
+ );
145
164
  continue;
146
165
  }
147
166
 
@@ -171,7 +190,11 @@ export function parseChart(
171
190
  }
172
191
  // If the first line is a single word (no spaces, no colon, no numbers),
173
192
  // treat it as an unrecognized chart type rather than falling through
174
- if (!trimmed.includes(' ') && !trimmed.includes(':') && !/\d/.test(trimmed)) {
193
+ if (
194
+ !trimmed.includes(' ') &&
195
+ !trimmed.includes(':') &&
196
+ !/\d/.test(trimmed)
197
+ ) {
175
198
  let msg = `Unsupported chart type: ${trimmed}. Supported types: ${[...VALID_TYPES].join(', ')}.`;
176
199
  const hint = suggest(trimmed.toLowerCase(), [...VALID_TYPES]);
177
200
  if (hint) msg += ` ${hint}`;
@@ -181,7 +204,9 @@ export function parseChart(
181
204
  }
182
205
 
183
206
  // Era line: era Day 1 -> Day 3 Rough Seas (blue) — colon-free
184
- const eraMatch = trimmed.match(/^era\s+(.+?)\s*->\s*(.+?)(?:\s*\(([^)]+)\))?\s*$/);
207
+ const eraMatch = trimmed.match(
208
+ /^era\s+(.+?)\s*->\s*(.+?)(?:\s*\(([^)]+)\))?\s*$/
209
+ );
185
210
  if (eraMatch) {
186
211
  // Store start and raw afterArrow — resolved against data labels after parsing
187
212
  const afterArrow = eraMatch[2].trim();
@@ -199,7 +224,9 @@ export function parseChart(
199
224
 
200
225
  // Extract first token to check for known options
201
226
  const spaceIdx = trimmed.indexOf(' ');
202
- const firstToken = (spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed).toLowerCase();
227
+ const firstToken = (
228
+ spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed
229
+ ).toLowerCase();
203
230
 
204
231
  // Bare boolean options (e.g. orientation-horizontal)
205
232
  if (KNOWN_BOOLEANS.has(firstToken) && spaceIdx < 0) {
@@ -233,13 +260,13 @@ export function parseChart(
233
260
  continue;
234
261
  }
235
262
 
236
- if (firstToken === 'xlabel') {
263
+ if (firstToken === 'x-label') {
237
264
  result.xlabel = value;
238
265
  result.xlabelLineNumber = lineNumber;
239
266
  continue;
240
267
  }
241
268
 
242
- if (firstToken === 'ylabel') {
269
+ if (firstToken === 'y-label') {
243
270
  result.ylabel = value;
244
271
  result.ylabelLineNumber = lineNumber;
245
272
  continue;
@@ -264,15 +291,25 @@ export function parseChart(
264
291
  result.seriesNames = parsed.names;
265
292
  result.seriesNameLineNumbers = parsed.nameLineNumbers;
266
293
  }
267
- if (parsed.nameColors.some(Boolean)) result.seriesNameColors = parsed.nameColors;
294
+ if (parsed.nameColors.some(Boolean))
295
+ result.seriesNameColors = parsed.nameColors;
268
296
  continue;
269
297
  }
270
298
  }
271
299
 
272
300
  // Bare boolean options: no-label-name, no-label-value, no-label-percent
273
- if (firstToken === 'no-label-name') { result.noLabelName = true; continue; }
274
- if (firstToken === 'no-label-value') { result.noLabelValue = true; continue; }
275
- if (firstToken === 'no-label-percent') { result.noLabelPercent = true; continue; }
301
+ if (firstToken === 'no-label-name') {
302
+ result.noLabelName = true;
303
+ continue;
304
+ }
305
+ if (firstToken === 'no-label-value') {
306
+ result.noLabelValue = true;
307
+ continue;
308
+ }
309
+ if (firstToken === 'no-label-percent') {
310
+ result.noLabelPercent = true;
311
+ continue;
312
+ }
276
313
 
277
314
  // Bare "series" keyword with no value — collect indented names
278
315
  if (firstToken === 'series' && spaceIdx === -1) {
@@ -284,7 +321,8 @@ export function parseChart(
284
321
  result.seriesNames = parsed.names;
285
322
  result.seriesNameLineNumbers = parsed.nameLineNumbers;
286
323
  }
287
- if (parsed.nameColors.some(Boolean)) result.seriesNameColors = parsed.nameColors;
324
+ if (parsed.nameColors.some(Boolean))
325
+ result.seriesNameColors = parsed.nameColors;
288
326
  continue;
289
327
  }
290
328
 
@@ -295,7 +333,10 @@ export function parseChart(
295
333
  const multiValue = (result.seriesNames?.length ?? 0) >= 2;
296
334
  const dataValues = parseDataRowValues(trimmed, { multiValue });
297
335
  if (dataValues) {
298
- const { label: rawLabel, color: pointColor } = extractColor(dataValues.label, palette);
336
+ const { label: rawLabel, color: pointColor } = extractColor(
337
+ dataValues.label,
338
+ palette
339
+ );
299
340
  const [first, ...rest] = dataValues.values;
300
341
  result.data.push({
301
342
  label: rawLabel,
@@ -304,7 +345,14 @@ export function parseChart(
304
345
  ...(pointColor && { color: pointColor }),
305
346
  lineNumber,
306
347
  });
348
+ continue;
307
349
  }
350
+
351
+ // Catch-all: nothing matched this line
352
+ let msg = `Unexpected line: '${trimmed}'.`;
353
+ const hint = suggest(firstToken, [...KNOWN_OPTIONS, ...KNOWN_BOOLEANS]);
354
+ if (hint) msg += ` ${hint}`;
355
+ result.diagnostics.push(makeDgmoError(lineNumber, msg, 'warning'));
308
356
  }
309
357
 
310
358
  // Resolve raw eras against known data labels (longest-prefix match for multi-word labels)
@@ -329,7 +377,13 @@ export function parseChart(
329
377
  end = words[0];
330
378
  label = words.slice(1).join(' ');
331
379
  }
332
- parsedEras.push({ start: raw.start, end, label, color: raw.color, lineNumber: raw.lineNumber });
380
+ parsedEras.push({
381
+ start: raw.start,
382
+ end,
383
+ label,
384
+ color: raw.color,
385
+ lineNumber: raw.lineNumber,
386
+ });
333
387
  }
334
388
 
335
389
  // Eras are only valid for line, multi-line (aliased to 'line'), and area chart types
@@ -353,7 +407,10 @@ export function parseChart(
353
407
  }
354
408
 
355
409
  if (!result.error && result.type === 'bar-stacked' && !result.seriesNames) {
356
- setChartError(1, 'Chart type "bar-stacked" requires multiple series names. Use: series Name1, Name2, Name3');
410
+ setChartError(
411
+ 1,
412
+ 'Chart type "bar-stacked" requires multiple series names. Use: series Name1, Name2, Name3'
413
+ );
357
414
  }
358
415
 
359
416
  if (!result.error && result.seriesNames) {
@@ -361,7 +418,10 @@ export function parseChart(
361
418
  for (const dp of result.data) {
362
419
  const actualCount = 1 + (dp.extraValues?.length ?? 0);
363
420
  if (actualCount !== expectedCount) {
364
- warn(dp.lineNumber, `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} values.`);
421
+ warn(
422
+ dp.lineNumber,
423
+ `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} values.`
424
+ );
365
425
  }
366
426
  }
367
427
  // Filter out mismatched data points so renderers get clean data
@@ -395,7 +455,7 @@ export function parseChart(
395
455
  */
396
456
  export function parseDataRowValues(
397
457
  line: string,
398
- options?: { multiValue?: boolean },
458
+ options?: { multiValue?: boolean }
399
459
  ): { label: string; values: number[] } | null {
400
460
  // First, normalize comma-grouped numbers: replace patterns like "1,087" with "1087"
401
461
  // We need to be careful: commas also separate multi-values.
@@ -423,7 +483,6 @@ export function parseDataRowValues(
423
483
  const prevMatch = prevSeg.match(/(\d{1,3})$/);
424
484
  if (prevMatch) {
425
485
  // Tentatively merge and validate
426
- const mergedTail = prevMatch[1] + ',' + seg;
427
486
  // Build full token by looking at what's left in normalized
428
487
  // Simple approach: just merge
429
488
  normalized[normalized.length - 1] = prevSeg + seg;
@@ -465,7 +524,11 @@ export function parseDataRowValues(
465
524
  const lastSpaceIdx = firstPart.lastIndexOf(' ');
466
525
  if (lastSpaceIdx >= 0) {
467
526
  const possibleFirstVal = firstPart.substring(lastSpaceIdx + 1).trim();
468
- if (possibleFirstVal && !isNaN(parseFloat(possibleFirstVal)) && isFinite(Number(possibleFirstVal))) {
527
+ if (
528
+ possibleFirstVal &&
529
+ !isNaN(parseFloat(possibleFirstVal)) &&
530
+ isFinite(Number(possibleFirstVal))
531
+ ) {
469
532
  const label = firstPart.substring(0, lastSpaceIdx).trim();
470
533
  if (label) {
471
534
  const values = [parseFloat(possibleFirstVal)];