@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.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +452 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +188 -185
- package/dist/editor.cjs +338 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +307 -0
- package/dist/editor.js.map +1 -0
- package/dist/highlight.cjs +560 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +530 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3467 -1078
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +3466 -1078
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +46 -37
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +9 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +71 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +211 -62
- package/src/completion.ts +378 -183
- package/src/d3.ts +1044 -303
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight-api.ts +444 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +222 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/index.ts +96 -31
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +75 -31
package/src/sequence/parser.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
| {
|
|
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 =
|
|
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))
|
|
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 {
|
|
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 {
|
|
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))
|
|
288
|
-
|
|
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))
|
|
294
|
-
|
|
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 = (
|
|
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 =
|
|
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, () =>
|
|
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(
|
|
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
|
|
531
|
+
const cleanName = rawInside
|
|
532
|
+
.substring(0, pipeIdx)
|
|
533
|
+
.trim()
|
|
534
|
+
.replace(/\([^)]*\)$/, '')
|
|
535
|
+
.trim();
|
|
488
536
|
const metaPart = rawInside.substring(pipeIdx).trim();
|
|
489
|
-
pushError(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
664
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
595
665
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
675
|
+
if (key === 'title') {
|
|
676
|
+
result.title = value;
|
|
677
|
+
result.titleLineNumber = lineNumber;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
607
680
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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 (
|
|
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(
|
|
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 = {
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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<{
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
}
|