@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
@@ -6,7 +6,14 @@ import { inferParticipantType } from './participant-inference';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
8
  import { parseArrow } from '../utils/arrows';
9
- import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_ERROR, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
9
+ import {
10
+ measureIndent,
11
+ extractColor,
12
+ parsePipeMetadata,
13
+ MULTIPLE_PIPE_ERROR,
14
+ parseFirstLine,
15
+ OPTION_NOCOLON_RE,
16
+ } from '../utils/parsing';
10
17
  import type { TagGroup } from '../utils/tag-groups';
11
18
  import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
12
19
 
@@ -197,7 +204,12 @@ const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*$/i;
197
204
 
198
205
  /** Result of parseNoteLine — indicates what the parser should do. */
199
206
  type NoteParseResult =
200
- | { kind: 'single'; position: 'right' | 'left'; participantId: string; text: string }
207
+ | {
208
+ kind: 'single';
209
+ position: 'right' | 'left';
210
+ participantId: string;
211
+ text: string;
212
+ }
201
213
  | { kind: 'multi-head'; position: 'right' | 'left'; participantId: string }
202
214
  | { kind: 'skip' }
203
215
  | null; // not a note line at all
@@ -215,7 +227,7 @@ type NoteParseResult =
215
227
  function parseNoteLine(
216
228
  trimmed: string,
217
229
  participants: SequenceParticipant[],
218
- lastMsgFrom: string | null,
230
+ lastMsgFrom: string | null
219
231
  ): NoteParseResult {
220
232
  const lower = trimmed.toLowerCase();
221
233
  if (!lower.startsWith('note')) return null;
@@ -228,7 +240,8 @@ function parseNoteLine(
228
240
  // fall through to the bare-note handler which does proper participant-aware splitting.
229
241
  const multiMatch = trimmed.match(NOTE_MULTI);
230
242
  if (multiMatch) {
231
- const position = (multiMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
243
+ const position =
244
+ (multiMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
232
245
  let participantId = multiMatch[2] || null;
233
246
  if (!participantId) {
234
247
  if (!lastMsgFrom) return { kind: 'skip' };
@@ -262,7 +275,8 @@ function parseNoteLine(
262
275
  if (!afterPos) {
263
276
  // Just `note left` or `note right` — multi-line head
264
277
  if (!lastMsgFrom) return { kind: 'skip' };
265
- if (!participants.some((p) => p.id === lastMsgFrom)) return { kind: 'skip' };
278
+ if (!participants.some((p) => p.id === lastMsgFrom))
279
+ return { kind: 'skip' };
266
280
  return { kind: 'multi-head', position, participantId: lastMsgFrom };
267
281
  }
268
282
 
@@ -270,10 +284,19 @@ function parseNoteLine(
270
284
  const resolved = resolveParticipantAndText(afterPos, participants);
271
285
  if (resolved) {
272
286
  if (resolved.text) {
273
- return { kind: 'single', position, participantId: resolved.participantId, text: resolved.text };
287
+ return {
288
+ kind: 'single',
289
+ position,
290
+ participantId: resolved.participantId,
291
+ text: resolved.text,
292
+ };
274
293
  } else {
275
294
  // No text after participant — multi-line head
276
- return { kind: 'multi-head', position, participantId: resolved.participantId };
295
+ return {
296
+ kind: 'multi-head',
297
+ position,
298
+ participantId: resolved.participantId,
299
+ };
277
300
  }
278
301
  }
279
302
 
@@ -284,14 +307,26 @@ function parseNoteLine(
284
307
 
285
308
  // Without `of`, treat remaining text as note content on the last-msg sender
286
309
  if (!lastMsgFrom) return { kind: 'skip' };
287
- if (!participants.some((p) => p.id === lastMsgFrom)) return { kind: 'skip' };
288
- return { kind: 'single', position, participantId: lastMsgFrom, text: afterPos };
310
+ if (!participants.some((p) => p.id === lastMsgFrom))
311
+ return { kind: 'skip' };
312
+ return {
313
+ kind: 'single',
314
+ position,
315
+ participantId: lastMsgFrom,
316
+ text: afterPos,
317
+ };
289
318
  }
290
319
 
291
320
  // Plain `note text` — default position, last msg sender
292
321
  if (!lastMsgFrom) return { kind: 'skip' };
293
- if (!participants.some((p) => p.id === lastMsgFrom)) return { kind: 'skip' };
294
- return { kind: 'single', position: 'right', participantId: lastMsgFrom, text: rest };
322
+ if (!participants.some((p) => p.id === lastMsgFrom))
323
+ return { kind: 'skip' };
324
+ return {
325
+ kind: 'single',
326
+ position: 'right',
327
+ participantId: lastMsgFrom,
328
+ text: rest,
329
+ };
295
330
  }
296
331
 
297
332
  return null;
@@ -304,7 +339,7 @@ function parseNoteLine(
304
339
  */
305
340
  function resolveParticipantAndText(
306
341
  input: string,
307
- participants: SequenceParticipant[],
342
+ participants: SequenceParticipant[]
308
343
  ): { participantId: string; text: string } | null {
309
344
  // Handle quoted participant: `"Auth Service" text`
310
345
  if (input.startsWith('"') || input.startsWith("'")) {
@@ -407,12 +442,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
407
442
  const aliasMap = new Map<string, string>();
408
443
 
409
444
  /** Split pipe metadata from a line: "core | k: v" → { core, meta } */
410
- const splitPipe = (text: string, ln?: number): { core: string; meta?: Record<string, string> } => {
445
+ const splitPipe = (
446
+ text: string,
447
+ ln?: number
448
+ ): { core: string; meta?: Record<string, string> } => {
411
449
  const idx = text.indexOf('|');
412
450
  if (idx < 0) return { core: text };
413
451
  const core = text.substring(0, idx).trimEnd();
414
452
  const segments = text.substring(idx).split('|');
415
- const warnFn = ln != null ? () => pushError(ln, MULTIPLE_PIPE_ERROR) : undefined;
453
+ const warnFn =
454
+ ln != null ? () => pushError(ln, MULTIPLE_PIPE_ERROR) : undefined;
416
455
  const meta = parsePipeMetadata(segments, aliasMap, warnFn);
417
456
  return Object.keys(meta).length > 0 ? { core, meta } : { core };
418
457
  };
@@ -460,12 +499,17 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
460
499
  const afterBracket = groupMatch[3]?.trim() || '';
461
500
  if (afterBracket.startsWith('|')) {
462
501
  const segments = afterBracket.split('|');
463
- const meta = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
502
+ const meta = parsePipeMetadata(segments, aliasMap, () =>
503
+ pushError(lineNumber, MULTIPLE_PIPE_ERROR)
504
+ );
464
505
  if (Object.keys(meta).length > 0) groupMeta = meta;
465
506
  }
466
507
 
467
508
  if (groupColor) {
468
- pushWarning(lineNumber, `(${groupColor}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`);
509
+ pushWarning(
510
+ lineNumber,
511
+ `(${groupColor}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
512
+ );
469
513
  }
470
514
  contentStarted = true;
471
515
  activeGroup = {
@@ -484,9 +528,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
484
528
  if (fallbackMatch && fallbackMatch[1].includes('|')) {
485
529
  const rawInside = fallbackMatch[1];
486
530
  const pipeIdx = rawInside.indexOf('|');
487
- const cleanName = rawInside.substring(0, pipeIdx).trim().replace(/\([^)]*\)$/, '').trim();
531
+ const cleanName = rawInside
532
+ .substring(0, pipeIdx)
533
+ .trim()
534
+ .replace(/\([^)]*\)$/, '')
535
+ .trim();
488
536
  const metaPart = rawInside.substring(pipeIdx).trim();
489
- pushError(lineNumber, `Pipe metadata must go outside brackets — use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`);
537
+ pushError(
538
+ lineNumber,
539
+ `Pipe metadata must go outside brackets — use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
540
+ );
490
541
  continue;
491
542
  }
492
543
  }
@@ -497,7 +548,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
497
548
  const name = legacyMatch[1].trim();
498
549
  const color = legacyMatch[2]?.trim();
499
550
  const suggestion = color ? `[${name}(${color})]` : `[${name}]`;
500
- pushError(lineNumber, `'## ${name}' group syntax is no longer supported. Use '${suggestion}' instead`);
551
+ pushError(
552
+ lineNumber,
553
+ `'## ${name}' group syntax is no longer supported. Use '${suggestion}' instead`
554
+ );
501
555
  continue;
502
556
  }
503
557
 
@@ -530,7 +584,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
530
584
  lineNumber,
531
585
  };
532
586
  if (tagBlockMatch.alias) {
533
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
587
+ aliasMap.set(
588
+ tagBlockMatch.alias.toLowerCase(),
589
+ tagBlockMatch.name.toLowerCase()
590
+ );
534
591
  }
535
592
  result.tagGroups.push(currentTagGroup);
536
593
  continue;
@@ -541,7 +598,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
541
598
  if (currentTagGroup && !contentStarted && measureIndent(raw) > 0) {
542
599
  const { label, color } = extractColor(trimmed);
543
600
  if (!color) {
544
- pushError(lineNumber, `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`);
601
+ pushError(
602
+ lineNumber,
603
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
604
+ );
545
605
  continue;
546
606
  }
547
607
  // First entry is the default
@@ -570,7 +630,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
570
630
  const labelRaw = sectionMatch[1].trim();
571
631
  const colorMatch = labelRaw.match(/^(.+?)\(([^)]+)\)$/);
572
632
  if (colorMatch) {
573
- pushWarning(lineNumber, `(${colorMatch[2].trim()}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`);
633
+ pushWarning(
634
+ lineNumber,
635
+ `(${colorMatch[2].trim()}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
636
+ );
574
637
  }
575
638
  contentStarted = true;
576
639
  const section: SequenceSection = {
@@ -586,28 +649,38 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
586
649
  // Parse header key: value lines (always top-level)
587
650
  // Skip 'note' lines — parsed in the indent-aware section below
588
651
  const colonIndex = trimmed.indexOf(':');
589
- if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>') && !trimmed.includes('<-') && !trimmed.includes('<~') && !trimmed.includes('|')) {
652
+ if (
653
+ colonIndex > 0 &&
654
+ !trimmed.includes('->') &&
655
+ !trimmed.includes('~>') &&
656
+ !trimmed.includes('<-') &&
657
+ !trimmed.includes('<~') &&
658
+ !trimmed.includes('|')
659
+ ) {
590
660
  const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
591
661
  if (key === 'note' || key.startsWith('note ')) {
592
662
  // Fall through to indent-aware note parsing below
593
663
  } else {
594
- const value = trimmed.substring(colonIndex + 1).trim();
664
+ const value = trimmed.substring(colonIndex + 1).trim();
595
665
 
596
- // Enforce headers-before-content
597
- if (contentStarted) {
598
- pushError(lineNumber, `Options like '${key}: ${value}' must appear before the first message or declaration`);
599
- continue;
600
- }
666
+ // Enforce headers-before-content
667
+ if (contentStarted) {
668
+ pushError(
669
+ lineNumber,
670
+ `Options like '${key}: ${value}' must appear before the first message or declaration`
671
+ );
672
+ continue;
673
+ }
601
674
 
602
- if (key === 'title') {
603
- result.title = value;
604
- result.titleLineNumber = lineNumber;
605
- continue;
606
- }
675
+ if (key === 'title') {
676
+ result.title = value;
677
+ result.titleLineNumber = lineNumber;
678
+ continue;
679
+ }
607
680
 
608
- // Store other options
609
- result.options[key] = value;
610
- continue;
681
+ // Store other options
682
+ result.options[key] = value;
683
+ continue;
611
684
  }
612
685
  }
613
686
 
@@ -619,7 +692,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
619
692
  const base = optLower.substring(3);
620
693
  if (KNOWN_SEQ_BOOLEANS.has(base)) {
621
694
  if (contentStarted) {
622
- pushError(lineNumber, `Options like '${trimmed}' must appear before the first message or declaration`);
695
+ pushError(
696
+ lineNumber,
697
+ `Options like '${trimmed}' must appear before the first message or declaration`
698
+ );
623
699
  continue;
624
700
  }
625
701
  result.options[base] = 'off';
@@ -633,7 +709,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
633
709
  const optVal = spaceMatch[2].trim();
634
710
  if (KNOWN_SEQ_OPTIONS.has(optKey) || KNOWN_SEQ_BOOLEANS.has(optKey)) {
635
711
  if (contentStarted) {
636
- pushError(lineNumber, `Options like '${trimmed}' must appear before the first message or declaration`);
712
+ pushError(
713
+ lineNumber,
714
+ `Options like '${trimmed}' must appear before the first message or declaration`
715
+ );
637
716
  continue;
638
717
  }
639
718
  result.options[optKey] = optVal;
@@ -645,7 +724,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
645
724
  // Parse "Name is a type [aka Alias]" declarations (always top-level)
646
725
  // Skip lines starting with 'note' — handled by note parsing below
647
726
  const { core: isACore, meta: isAMeta } = splitPipe(trimmed, lineNumber);
648
- const isAMatch = !/^note(\s|$)/i.test(trimmed) ? isACore.match(IS_A_PATTERN) : null;
727
+ const isAMatch = !/^note(\s|$)/i.test(trimmed)
728
+ ? isACore.match(IS_A_PATTERN)
729
+ : null;
649
730
  if (isAMatch) {
650
731
  contentStarted = true;
651
732
  const id = isAMatch[1];
@@ -681,7 +762,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
681
762
  if (activeGroup && !activeGroup.participantIds.includes(id)) {
682
763
  const existingGroup = participantGroupMap.get(id);
683
764
  if (existingGroup) {
684
- pushError(lineNumber, `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`);
765
+ pushError(
766
+ lineNumber,
767
+ `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
768
+ );
685
769
  } else {
686
770
  activeGroup.participantIds.push(id);
687
771
  participantGroupMap.set(id, activeGroup.name);
@@ -712,7 +796,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
712
796
  if (activeGroup && !activeGroup.participantIds.includes(id)) {
713
797
  const existingGroup = participantGroupMap.get(id);
714
798
  if (existingGroup) {
715
- pushError(lineNumber, `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`);
799
+ pushError(
800
+ lineNumber,
801
+ `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
802
+ );
716
803
  } else {
717
804
  activeGroup.participantIds.push(id);
718
805
  participantGroupMap.set(id, activeGroup.name);
@@ -728,7 +815,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
728
815
  if (coloredMatch && !ARROW_PATTERN.test(colorCore)) {
729
816
  const id = coloredMatch[1];
730
817
  const color = coloredMatch[2].trim();
731
- pushError(lineNumber, `'${id}(${color})' syntax is no longer supported — use 'tag:' groups for coloring`);
818
+ pushError(
819
+ lineNumber,
820
+ `'${id}(${color})' syntax is no longer supported — use 'tag:' groups for coloring`
821
+ );
732
822
  contentStarted = true;
733
823
  if (!result.participants.some((p) => p.id === id)) {
734
824
  result.participants.push({
@@ -742,7 +832,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
742
832
  if (activeGroup && !activeGroup.participantIds.includes(id)) {
743
833
  const existingGroup = participantGroupMap.get(id);
744
834
  if (existingGroup) {
745
- pushError(lineNumber, `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`);
835
+ pushError(
836
+ lineNumber,
837
+ `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
838
+ );
746
839
  } else {
747
840
  activeGroup.participantIds.push(id);
748
841
  participantGroupMap.set(id, activeGroup.name);
@@ -756,7 +849,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
756
849
  {
757
850
  const { core: bareCore, meta: bareMeta } = splitPipe(trimmed, lineNumber);
758
851
  const inGroup = activeGroup && measureIndent(raw) > 0;
759
- if (/^\S+$/.test(bareCore) && !ARROW_PATTERN.test(bareCore) && (inGroup || !contentStarted || bareMeta)) {
852
+ if (
853
+ /^\S+$/.test(bareCore) &&
854
+ !ARROW_PATTERN.test(bareCore) &&
855
+ (inGroup || !contentStarted || bareMeta)
856
+ ) {
760
857
  contentStarted = true;
761
858
  const id = bareCore;
762
859
  if (!result.participants.some((p) => p.id === id)) {
@@ -771,7 +868,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
771
868
  if (activeGroup && !activeGroup.participantIds.includes(id)) {
772
869
  const existingGroup = participantGroupMap.get(id);
773
870
  if (existingGroup) {
774
- pushError(lineNumber, `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`);
871
+ pushError(
872
+ lineNumber,
873
+ `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
874
+ );
775
875
  } else {
776
876
  activeGroup.participantIds.push(id);
777
877
  participantGroupMap.set(id, activeGroup.name);
@@ -875,9 +975,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
875
975
  }
876
976
 
877
977
  // ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
878
- const bidiPlainMatch = arrowCore.match(
879
- /^(.+?)\s*(?:<->|<~>)\s*(.+)/
880
- );
978
+ const bidiPlainMatch = arrowCore.match(/^(.+?)\s*(?:<->|<~>)\s*(.+)/);
881
979
  if (bidiPlainMatch) {
882
980
  pushError(
883
981
  lineNumber,
@@ -994,14 +1092,23 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
994
1092
  // Parse 'else if <label>' keyword (must come before bare 'else')
995
1093
  const elseIfMatch = trimmed.match(/^else\s+if\s+(.+)$/i);
996
1094
  if (elseIfMatch) {
997
- if (blockStack.length > 0 && blockStack[blockStack.length - 1].indent === indent) {
1095
+ if (
1096
+ blockStack.length > 0 &&
1097
+ blockStack[blockStack.length - 1].indent === indent
1098
+ ) {
998
1099
  const top = blockStack[blockStack.length - 1];
999
1100
  if (top.block.type === 'parallel') {
1000
- pushError(lineNumber, "parallel blocks don't support else if — list all concurrent messages directly inside the block");
1101
+ pushError(
1102
+ lineNumber,
1103
+ "parallel blocks don't support else if — list all concurrent messages directly inside the block"
1104
+ );
1001
1105
  continue;
1002
1106
  }
1003
1107
  if (top.block.type === 'if') {
1004
- const branch: ElseIfBranch = { label: elseIfMatch[1].trim(), children: [] };
1108
+ const branch: ElseIfBranch = {
1109
+ label: elseIfMatch[1].trim(),
1110
+ children: [],
1111
+ };
1005
1112
  if (!top.block.elseIfBranches) top.block.elseIfBranches = [];
1006
1113
  top.block.elseIfBranches.push(branch);
1007
1114
  top.activeElseIfBranch = branch;
@@ -1013,10 +1120,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1013
1120
 
1014
1121
  // Parse 'else' keyword (only applies to 'if' blocks)
1015
1122
  if (trimmed.toLowerCase() === 'else') {
1016
- if (blockStack.length > 0 && blockStack[blockStack.length - 1].indent === indent) {
1123
+ if (
1124
+ blockStack.length > 0 &&
1125
+ blockStack[blockStack.length - 1].indent === indent
1126
+ ) {
1017
1127
  const top = blockStack[blockStack.length - 1];
1018
1128
  if (top.block.type === 'parallel') {
1019
- pushError(lineNumber, "parallel blocks don't support else — list all concurrent messages directly inside the block");
1129
+ pushError(
1130
+ lineNumber,
1131
+ "parallel blocks don't support else — list all concurrent messages directly inside the block"
1132
+ );
1020
1133
  continue;
1021
1134
  }
1022
1135
  if (top.block.type === 'if') {
@@ -1033,7 +1146,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1033
1146
  // 2. For positioned: `note left [of] X text` — needs participant lookup to split name vs text
1034
1147
  // 3. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
1035
1148
  {
1036
- const noteParsed = parseNoteLine(trimmed, result.participants, lastMsgFrom);
1149
+ const noteParsed = parseNoteLine(
1150
+ trimmed,
1151
+ result.participants,
1152
+ lastMsgFrom
1153
+ );
1037
1154
  if (noteParsed) {
1038
1155
  if (noteParsed.kind === 'single') {
1039
1156
  const note: SequenceNote = {
@@ -1075,6 +1192,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1075
1192
  continue;
1076
1193
  }
1077
1194
  }
1195
+
1196
+ // Catch-all: nothing matched this line
1197
+ pushWarning(lineNumber, `Unexpected line: '${trimmed}'.`);
1078
1198
  }
1079
1199
 
1080
1200
  // Validate: if no explicit chart line, check for arrow-based inference
@@ -1113,7 +1233,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1113
1233
 
1114
1234
  for (const p of result.participants) {
1115
1235
  if (!usedIds.has(p.id)) {
1116
- pushWarning(p.lineNumber, `Participant "${p.label}" is declared but never used in any message or note`);
1236
+ pushWarning(
1237
+ p.lineNumber,
1238
+ `Participant "${p.label}" is declared but never used in any message or note`
1239
+ );
1117
1240
  }
1118
1241
  }
1119
1242
  }
@@ -1121,21 +1244,30 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1121
1244
  // Warn about empty groups
1122
1245
  for (const group of result.groups) {
1123
1246
  if (group.participantIds.length === 0) {
1124
- pushWarning(group.lineNumber, `Empty group '${group.name}' — did you mean '== ${group.name} ==' for a section divider?`);
1247
+ pushWarning(
1248
+ group.lineNumber,
1249
+ `Empty group '${group.name}' — did you mean '== ${group.name} ==' for a section divider?`
1250
+ );
1125
1251
  }
1126
1252
  }
1127
1253
 
1128
1254
  // Validate tag group values on participants and messages
1129
1255
  if (result.tagGroups.length > 0) {
1130
- const entities: Array<{ metadata: Record<string, string>; lineNumber: number }> = [];
1256
+ const entities: Array<{
1257
+ metadata: Record<string, string>;
1258
+ lineNumber: number;
1259
+ }> = [];
1131
1260
  for (const p of result.participants) {
1132
- if (p.metadata) entities.push({ metadata: p.metadata, lineNumber: p.lineNumber });
1261
+ if (p.metadata)
1262
+ entities.push({ metadata: p.metadata, lineNumber: p.lineNumber });
1133
1263
  }
1134
1264
  for (const m of result.messages) {
1135
- if (m.metadata) entities.push({ metadata: m.metadata, lineNumber: m.lineNumber });
1265
+ if (m.metadata)
1266
+ entities.push({ metadata: m.metadata, lineNumber: m.lineNumber });
1136
1267
  }
1137
1268
  for (const g of result.groups) {
1138
- if (g.metadata) entities.push({ metadata: g.metadata, lineNumber: g.lineNumber });
1269
+ if (g.metadata)
1270
+ entities.push({ metadata: g.metadata, lineNumber: g.lineNumber });
1139
1271
  }
1140
1272
  validateTagValues(entities, result.tagGroups, pushWarning, suggest);
1141
1273
  }