@diagrammo/dgmo 0.8.2 → 0.8.4
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 +185 -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 +189 -194
- package/dist/editor.cjs +336 -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 +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- 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 +8 -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 +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- 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.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- 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 +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
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
|
|
|
@@ -185,21 +192,24 @@ const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
|
|
|
185
192
|
// Arrow pattern for sequence inference — detects any arrow form
|
|
186
193
|
const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
|
|
187
194
|
|
|
188
|
-
// Note patterns — colon-free syntax
|
|
195
|
+
// Note patterns — colon-free syntax only
|
|
189
196
|
// Single-line: "note text", "note left text", "note right of X text", "note left X text"
|
|
190
197
|
// Multi-line: "note", "note right", "note right of X", "note left X" (body indented below)
|
|
191
|
-
// Also supports legacy colon syntax: "note: text", "note right of X: text"
|
|
192
198
|
//
|
|
193
199
|
// The colon-free positioned form requires participant resolution — the parser
|
|
194
200
|
// already has participant collection infrastructure, so we match the general
|
|
195
201
|
// structure here and resolve participant vs text in the parsing logic.
|
|
196
|
-
const NOTE_SINGLE_COLON = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*:\s*(.+)$/i;
|
|
197
202
|
const NOTE_BARE = /^note\s+(.+)$/i;
|
|
198
|
-
const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s
|
|
203
|
+
const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*$/i;
|
|
199
204
|
|
|
200
205
|
/** Result of parseNoteLine — indicates what the parser should do. */
|
|
201
206
|
type NoteParseResult =
|
|
202
|
-
| {
|
|
207
|
+
| {
|
|
208
|
+
kind: 'single';
|
|
209
|
+
position: 'right' | 'left';
|
|
210
|
+
participantId: string;
|
|
211
|
+
text: string;
|
|
212
|
+
}
|
|
203
213
|
| { kind: 'multi-head'; position: 'right' | 'left'; participantId: string }
|
|
204
214
|
| { kind: 'skip' }
|
|
205
215
|
| null; // not a note line at all
|
|
@@ -208,42 +218,30 @@ type NoteParseResult =
|
|
|
208
218
|
* Parse a note line, resolving participant names from the known participants list.
|
|
209
219
|
*
|
|
210
220
|
* Supports:
|
|
211
|
-
* - `note
|
|
212
|
-
* - `note left of X
|
|
213
|
-
* - `note right
|
|
214
|
-
* - `note right of X
|
|
221
|
+
* - `note text` — default position (right), last msg sender
|
|
222
|
+
* - `note left of X text` / `note left X text`
|
|
223
|
+
* - `note right` — multi-line head
|
|
224
|
+
* - `note right of X` / `note left X` — multi-line head
|
|
215
225
|
* - Quoted participant: `note left "Auth Service" text`
|
|
216
226
|
*/
|
|
217
227
|
function parseNoteLine(
|
|
218
228
|
trimmed: string,
|
|
219
229
|
participants: SequenceParticipant[],
|
|
220
|
-
lastMsgFrom: string | null
|
|
230
|
+
lastMsgFrom: string | null
|
|
221
231
|
): NoteParseResult {
|
|
222
232
|
const lower = trimmed.toLowerCase();
|
|
223
233
|
if (!lower.startsWith('note')) return null;
|
|
224
234
|
// Must be exactly "note" or "note " — not "notebook" etc.
|
|
225
|
-
if (trimmed.length > 4 && trimmed[4] !== ' '
|
|
226
|
-
|
|
227
|
-
// 1. Try legacy colon-based syntax first
|
|
228
|
-
const colonMatch = trimmed.match(NOTE_SINGLE_COLON);
|
|
229
|
-
if (colonMatch) {
|
|
230
|
-
const position = (colonMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
|
|
231
|
-
let participantId = colonMatch[2] || null;
|
|
232
|
-
if (!participantId) {
|
|
233
|
-
if (!lastMsgFrom) return { kind: 'skip' };
|
|
234
|
-
participantId = lastMsgFrom;
|
|
235
|
-
}
|
|
236
|
-
if (!participants.some((p) => p.id === participantId)) return { kind: 'skip' };
|
|
237
|
-
return { kind: 'single', position, participantId, text: colonMatch[3].trim() };
|
|
238
|
-
}
|
|
235
|
+
if (trimmed.length > 4 && trimmed[4] !== ' ') return null;
|
|
239
236
|
|
|
240
|
-
//
|
|
237
|
+
// 1. Try multi-line head (no text after note): `note`, `note right`, `note right of X`, `note left X`
|
|
241
238
|
// NOTE: NOTE_MULTI's (.+?) can greedily capture "participant text" as one group.
|
|
242
|
-
// Only trust this match if the captured participant actually exists. Otherwise,
|
|
243
|
-
// through to the bare-note handler which does proper participant-aware splitting.
|
|
239
|
+
// Only trust this match if the captured participant actually exists. Otherwise,
|
|
240
|
+
// fall through to the bare-note handler which does proper participant-aware splitting.
|
|
244
241
|
const multiMatch = trimmed.match(NOTE_MULTI);
|
|
245
242
|
if (multiMatch) {
|
|
246
|
-
const position =
|
|
243
|
+
const position =
|
|
244
|
+
(multiMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
|
|
247
245
|
let participantId = multiMatch[2] || null;
|
|
248
246
|
if (!participantId) {
|
|
249
247
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
@@ -255,7 +253,7 @@ function parseNoteLine(
|
|
|
255
253
|
// Participant not found — fall through to bare-note handler for proper resolution
|
|
256
254
|
}
|
|
257
255
|
|
|
258
|
-
//
|
|
256
|
+
// 2. Bare note: `note text` or `note left [of] X text`
|
|
259
257
|
const bareMatch = trimmed.match(NOTE_BARE);
|
|
260
258
|
if (bareMatch) {
|
|
261
259
|
const rest = bareMatch[1].trim();
|
|
@@ -277,7 +275,8 @@ function parseNoteLine(
|
|
|
277
275
|
if (!afterPos) {
|
|
278
276
|
// Just `note left` or `note right` — multi-line head
|
|
279
277
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
280
|
-
if (!participants.some((p) => p.id === lastMsgFrom))
|
|
278
|
+
if (!participants.some((p) => p.id === lastMsgFrom))
|
|
279
|
+
return { kind: 'skip' };
|
|
281
280
|
return { kind: 'multi-head', position, participantId: lastMsgFrom };
|
|
282
281
|
}
|
|
283
282
|
|
|
@@ -285,10 +284,19 @@ function parseNoteLine(
|
|
|
285
284
|
const resolved = resolveParticipantAndText(afterPos, participants);
|
|
286
285
|
if (resolved) {
|
|
287
286
|
if (resolved.text) {
|
|
288
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
kind: 'single',
|
|
289
|
+
position,
|
|
290
|
+
participantId: resolved.participantId,
|
|
291
|
+
text: resolved.text,
|
|
292
|
+
};
|
|
289
293
|
} else {
|
|
290
294
|
// No text after participant — multi-line head
|
|
291
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
kind: 'multi-head',
|
|
297
|
+
position,
|
|
298
|
+
participantId: resolved.participantId,
|
|
299
|
+
};
|
|
292
300
|
}
|
|
293
301
|
}
|
|
294
302
|
|
|
@@ -299,14 +307,26 @@ function parseNoteLine(
|
|
|
299
307
|
|
|
300
308
|
// Without `of`, treat remaining text as note content on the last-msg sender
|
|
301
309
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
302
|
-
if (!participants.some((p) => p.id === lastMsgFrom))
|
|
303
|
-
|
|
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
|
+
};
|
|
304
318
|
}
|
|
305
319
|
|
|
306
320
|
// Plain `note text` — default position, last msg sender
|
|
307
321
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
308
|
-
if (!participants.some((p) => p.id === lastMsgFrom))
|
|
309
|
-
|
|
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
|
+
};
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
return null;
|
|
@@ -319,7 +339,7 @@ function parseNoteLine(
|
|
|
319
339
|
*/
|
|
320
340
|
function resolveParticipantAndText(
|
|
321
341
|
input: string,
|
|
322
|
-
participants: SequenceParticipant[]
|
|
342
|
+
participants: SequenceParticipant[]
|
|
323
343
|
): { participantId: string; text: string } | null {
|
|
324
344
|
// Handle quoted participant: `"Auth Service" text`
|
|
325
345
|
if (input.startsWith('"') || input.startsWith("'")) {
|
|
@@ -422,12 +442,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
422
442
|
const aliasMap = new Map<string, string>();
|
|
423
443
|
|
|
424
444
|
/** Split pipe metadata from a line: "core | k: v" → { core, meta } */
|
|
425
|
-
const splitPipe = (
|
|
445
|
+
const splitPipe = (
|
|
446
|
+
text: string,
|
|
447
|
+
ln?: number
|
|
448
|
+
): { core: string; meta?: Record<string, string> } => {
|
|
426
449
|
const idx = text.indexOf('|');
|
|
427
450
|
if (idx < 0) return { core: text };
|
|
428
451
|
const core = text.substring(0, idx).trimEnd();
|
|
429
452
|
const segments = text.substring(idx).split('|');
|
|
430
|
-
const warnFn =
|
|
453
|
+
const warnFn =
|
|
454
|
+
ln != null ? () => pushError(ln, MULTIPLE_PIPE_ERROR) : undefined;
|
|
431
455
|
const meta = parsePipeMetadata(segments, aliasMap, warnFn);
|
|
432
456
|
return Object.keys(meta).length > 0 ? { core, meta } : { core };
|
|
433
457
|
};
|
|
@@ -475,12 +499,17 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
475
499
|
const afterBracket = groupMatch[3]?.trim() || '';
|
|
476
500
|
if (afterBracket.startsWith('|')) {
|
|
477
501
|
const segments = afterBracket.split('|');
|
|
478
|
-
const meta = parsePipeMetadata(segments, aliasMap, () =>
|
|
502
|
+
const meta = parsePipeMetadata(segments, aliasMap, () =>
|
|
503
|
+
pushError(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
504
|
+
);
|
|
479
505
|
if (Object.keys(meta).length > 0) groupMeta = meta;
|
|
480
506
|
}
|
|
481
507
|
|
|
482
508
|
if (groupColor) {
|
|
483
|
-
pushWarning(
|
|
509
|
+
pushWarning(
|
|
510
|
+
lineNumber,
|
|
511
|
+
`(${groupColor}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
|
|
512
|
+
);
|
|
484
513
|
}
|
|
485
514
|
contentStarted = true;
|
|
486
515
|
activeGroup = {
|
|
@@ -499,9 +528,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
499
528
|
if (fallbackMatch && fallbackMatch[1].includes('|')) {
|
|
500
529
|
const rawInside = fallbackMatch[1];
|
|
501
530
|
const pipeIdx = rawInside.indexOf('|');
|
|
502
|
-
const cleanName = rawInside
|
|
531
|
+
const cleanName = rawInside
|
|
532
|
+
.substring(0, pipeIdx)
|
|
533
|
+
.trim()
|
|
534
|
+
.replace(/\([^)]*\)$/, '')
|
|
535
|
+
.trim();
|
|
503
536
|
const metaPart = rawInside.substring(pipeIdx).trim();
|
|
504
|
-
pushError(
|
|
537
|
+
pushError(
|
|
538
|
+
lineNumber,
|
|
539
|
+
`Pipe metadata must go outside brackets — use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
|
|
540
|
+
);
|
|
505
541
|
continue;
|
|
506
542
|
}
|
|
507
543
|
}
|
|
@@ -512,7 +548,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
512
548
|
const name = legacyMatch[1].trim();
|
|
513
549
|
const color = legacyMatch[2]?.trim();
|
|
514
550
|
const suggestion = color ? `[${name}(${color})]` : `[${name}]`;
|
|
515
|
-
pushError(
|
|
551
|
+
pushError(
|
|
552
|
+
lineNumber,
|
|
553
|
+
`'## ${name}' group syntax is no longer supported. Use '${suggestion}' instead`
|
|
554
|
+
);
|
|
516
555
|
continue;
|
|
517
556
|
}
|
|
518
557
|
|
|
@@ -531,9 +570,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
531
570
|
}
|
|
532
571
|
|
|
533
572
|
// ---- Tag group handling ----
|
|
534
|
-
// Tag block heading: "tag
|
|
573
|
+
// Tag block heading: "tag Name [alias X]"
|
|
535
574
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
536
|
-
if (tagBlockMatch
|
|
575
|
+
if (tagBlockMatch) {
|
|
537
576
|
if (contentStarted) {
|
|
538
577
|
pushError(lineNumber, 'Tag groups must appear before sequence content');
|
|
539
578
|
continue;
|
|
@@ -545,7 +584,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
545
584
|
lineNumber,
|
|
546
585
|
};
|
|
547
586
|
if (tagBlockMatch.alias) {
|
|
548
|
-
aliasMap.set(
|
|
587
|
+
aliasMap.set(
|
|
588
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
589
|
+
tagBlockMatch.name.toLowerCase()
|
|
590
|
+
);
|
|
549
591
|
}
|
|
550
592
|
result.tagGroups.push(currentTagGroup);
|
|
551
593
|
continue;
|
|
@@ -556,7 +598,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
556
598
|
if (currentTagGroup && !contentStarted && measureIndent(raw) > 0) {
|
|
557
599
|
const { label, color } = extractColor(trimmed);
|
|
558
600
|
if (!color) {
|
|
559
|
-
pushError(
|
|
601
|
+
pushError(
|
|
602
|
+
lineNumber,
|
|
603
|
+
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
|
|
604
|
+
);
|
|
560
605
|
continue;
|
|
561
606
|
}
|
|
562
607
|
// First entry is the default
|
|
@@ -585,7 +630,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
585
630
|
const labelRaw = sectionMatch[1].trim();
|
|
586
631
|
const colorMatch = labelRaw.match(/^(.+?)\(([^)]+)\)$/);
|
|
587
632
|
if (colorMatch) {
|
|
588
|
-
pushWarning(
|
|
633
|
+
pushWarning(
|
|
634
|
+
lineNumber,
|
|
635
|
+
`(${colorMatch[2].trim()}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`
|
|
636
|
+
);
|
|
589
637
|
}
|
|
590
638
|
contentStarted = true;
|
|
591
639
|
const section: SequenceSection = {
|
|
@@ -601,37 +649,39 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
601
649
|
// Parse header key: value lines (always top-level)
|
|
602
650
|
// Skip 'note' lines — parsed in the indent-aware section below
|
|
603
651
|
const colonIndex = trimmed.indexOf(':');
|
|
604
|
-
if (
|
|
652
|
+
if (
|
|
653
|
+
colonIndex > 0 &&
|
|
654
|
+
!trimmed.includes('->') &&
|
|
655
|
+
!trimmed.includes('~>') &&
|
|
656
|
+
!trimmed.includes('<-') &&
|
|
657
|
+
!trimmed.includes('<~') &&
|
|
658
|
+
!trimmed.includes('|')
|
|
659
|
+
) {
|
|
605
660
|
const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
|
|
606
661
|
if (key === 'note' || key.startsWith('note ')) {
|
|
607
662
|
// Fall through to indent-aware note parsing below
|
|
608
663
|
} else {
|
|
609
|
-
|
|
664
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
610
665
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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;
|
|
615
673
|
}
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
674
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
675
|
+
if (key === 'title') {
|
|
676
|
+
result.title = value;
|
|
677
|
+
result.titleLineNumber = lineNumber;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
624
680
|
|
|
625
|
-
|
|
626
|
-
result.
|
|
627
|
-
result.titleLineNumber = lineNumber;
|
|
681
|
+
// Store other options
|
|
682
|
+
result.options[key] = value;
|
|
628
683
|
continue;
|
|
629
684
|
}
|
|
630
|
-
|
|
631
|
-
// Store other options
|
|
632
|
-
result.options[key] = value;
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
685
|
}
|
|
636
686
|
|
|
637
687
|
// Parse space-separated options (no colon): `activations off`, `no-activations`, `active-tag Priority`
|
|
@@ -642,7 +692,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
642
692
|
const base = optLower.substring(3);
|
|
643
693
|
if (KNOWN_SEQ_BOOLEANS.has(base)) {
|
|
644
694
|
if (contentStarted) {
|
|
645
|
-
pushError(
|
|
695
|
+
pushError(
|
|
696
|
+
lineNumber,
|
|
697
|
+
`Options like '${trimmed}' must appear before the first message or declaration`
|
|
698
|
+
);
|
|
646
699
|
continue;
|
|
647
700
|
}
|
|
648
701
|
result.options[base] = 'off';
|
|
@@ -656,7 +709,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
656
709
|
const optVal = spaceMatch[2].trim();
|
|
657
710
|
if (KNOWN_SEQ_OPTIONS.has(optKey) || KNOWN_SEQ_BOOLEANS.has(optKey)) {
|
|
658
711
|
if (contentStarted) {
|
|
659
|
-
pushError(
|
|
712
|
+
pushError(
|
|
713
|
+
lineNumber,
|
|
714
|
+
`Options like '${trimmed}' must appear before the first message or declaration`
|
|
715
|
+
);
|
|
660
716
|
continue;
|
|
661
717
|
}
|
|
662
718
|
result.options[optKey] = optVal;
|
|
@@ -666,8 +722,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
666
722
|
}
|
|
667
723
|
|
|
668
724
|
// Parse "Name is a type [aka Alias]" declarations (always top-level)
|
|
725
|
+
// Skip lines starting with 'note' — handled by note parsing below
|
|
669
726
|
const { core: isACore, meta: isAMeta } = splitPipe(trimmed, lineNumber);
|
|
670
|
-
const isAMatch =
|
|
727
|
+
const isAMatch = !/^note(\s|$)/i.test(trimmed)
|
|
728
|
+
? isACore.match(IS_A_PATTERN)
|
|
729
|
+
: null;
|
|
671
730
|
if (isAMatch) {
|
|
672
731
|
contentStarted = true;
|
|
673
732
|
const id = isAMatch[1];
|
|
@@ -703,7 +762,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
703
762
|
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
704
763
|
const existingGroup = participantGroupMap.get(id);
|
|
705
764
|
if (existingGroup) {
|
|
706
|
-
pushError(
|
|
765
|
+
pushError(
|
|
766
|
+
lineNumber,
|
|
767
|
+
`Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
|
|
768
|
+
);
|
|
707
769
|
} else {
|
|
708
770
|
activeGroup.participantIds.push(id);
|
|
709
771
|
participantGroupMap.set(id, activeGroup.name);
|
|
@@ -734,7 +796,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
734
796
|
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
735
797
|
const existingGroup = participantGroupMap.get(id);
|
|
736
798
|
if (existingGroup) {
|
|
737
|
-
pushError(
|
|
799
|
+
pushError(
|
|
800
|
+
lineNumber,
|
|
801
|
+
`Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
|
|
802
|
+
);
|
|
738
803
|
} else {
|
|
739
804
|
activeGroup.participantIds.push(id);
|
|
740
805
|
participantGroupMap.set(id, activeGroup.name);
|
|
@@ -750,7 +815,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
750
815
|
if (coloredMatch && !ARROW_PATTERN.test(colorCore)) {
|
|
751
816
|
const id = coloredMatch[1];
|
|
752
817
|
const color = coloredMatch[2].trim();
|
|
753
|
-
pushError(
|
|
818
|
+
pushError(
|
|
819
|
+
lineNumber,
|
|
820
|
+
`'${id}(${color})' syntax is no longer supported — use 'tag:' groups for coloring`
|
|
821
|
+
);
|
|
754
822
|
contentStarted = true;
|
|
755
823
|
if (!result.participants.some((p) => p.id === id)) {
|
|
756
824
|
result.participants.push({
|
|
@@ -764,7 +832,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
764
832
|
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
765
833
|
const existingGroup = participantGroupMap.get(id);
|
|
766
834
|
if (existingGroup) {
|
|
767
|
-
pushError(
|
|
835
|
+
pushError(
|
|
836
|
+
lineNumber,
|
|
837
|
+
`Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
|
|
838
|
+
);
|
|
768
839
|
} else {
|
|
769
840
|
activeGroup.participantIds.push(id);
|
|
770
841
|
participantGroupMap.set(id, activeGroup.name);
|
|
@@ -778,7 +849,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
778
849
|
{
|
|
779
850
|
const { core: bareCore, meta: bareMeta } = splitPipe(trimmed, lineNumber);
|
|
780
851
|
const inGroup = activeGroup && measureIndent(raw) > 0;
|
|
781
|
-
if (
|
|
852
|
+
if (
|
|
853
|
+
/^\S+$/.test(bareCore) &&
|
|
854
|
+
!ARROW_PATTERN.test(bareCore) &&
|
|
855
|
+
(inGroup || !contentStarted || bareMeta)
|
|
856
|
+
) {
|
|
782
857
|
contentStarted = true;
|
|
783
858
|
const id = bareCore;
|
|
784
859
|
if (!result.participants.some((p) => p.id === id)) {
|
|
@@ -793,7 +868,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
793
868
|
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
794
869
|
const existingGroup = participantGroupMap.get(id);
|
|
795
870
|
if (existingGroup) {
|
|
796
|
-
pushError(
|
|
871
|
+
pushError(
|
|
872
|
+
lineNumber,
|
|
873
|
+
`Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`
|
|
874
|
+
);
|
|
797
875
|
} else {
|
|
798
876
|
activeGroup.participantIds.push(id);
|
|
799
877
|
participantGroupMap.set(id, activeGroup.name);
|
|
@@ -897,9 +975,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
897
975
|
}
|
|
898
976
|
|
|
899
977
|
// ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
|
|
900
|
-
const bidiPlainMatch = arrowCore.match(
|
|
901
|
-
/^(.+?)\s*(?:<->|<~>)\s*(.+)/
|
|
902
|
-
);
|
|
978
|
+
const bidiPlainMatch = arrowCore.match(/^(.+?)\s*(?:<->|<~>)\s*(.+)/);
|
|
903
979
|
if (bidiPlainMatch) {
|
|
904
980
|
pushError(
|
|
905
981
|
lineNumber,
|
|
@@ -1016,14 +1092,23 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1016
1092
|
// Parse 'else if <label>' keyword (must come before bare 'else')
|
|
1017
1093
|
const elseIfMatch = trimmed.match(/^else\s+if\s+(.+)$/i);
|
|
1018
1094
|
if (elseIfMatch) {
|
|
1019
|
-
if (
|
|
1095
|
+
if (
|
|
1096
|
+
blockStack.length > 0 &&
|
|
1097
|
+
blockStack[blockStack.length - 1].indent === indent
|
|
1098
|
+
) {
|
|
1020
1099
|
const top = blockStack[blockStack.length - 1];
|
|
1021
1100
|
if (top.block.type === 'parallel') {
|
|
1022
|
-
pushError(
|
|
1101
|
+
pushError(
|
|
1102
|
+
lineNumber,
|
|
1103
|
+
"parallel blocks don't support else if — list all concurrent messages directly inside the block"
|
|
1104
|
+
);
|
|
1023
1105
|
continue;
|
|
1024
1106
|
}
|
|
1025
1107
|
if (top.block.type === 'if') {
|
|
1026
|
-
const branch: ElseIfBranch = {
|
|
1108
|
+
const branch: ElseIfBranch = {
|
|
1109
|
+
label: elseIfMatch[1].trim(),
|
|
1110
|
+
children: [],
|
|
1111
|
+
};
|
|
1027
1112
|
if (!top.block.elseIfBranches) top.block.elseIfBranches = [];
|
|
1028
1113
|
top.block.elseIfBranches.push(branch);
|
|
1029
1114
|
top.activeElseIfBranch = branch;
|
|
@@ -1035,10 +1120,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1035
1120
|
|
|
1036
1121
|
// Parse 'else' keyword (only applies to 'if' blocks)
|
|
1037
1122
|
if (trimmed.toLowerCase() === 'else') {
|
|
1038
|
-
if (
|
|
1123
|
+
if (
|
|
1124
|
+
blockStack.length > 0 &&
|
|
1125
|
+
blockStack[blockStack.length - 1].indent === indent
|
|
1126
|
+
) {
|
|
1039
1127
|
const top = blockStack[blockStack.length - 1];
|
|
1040
1128
|
if (top.block.type === 'parallel') {
|
|
1041
|
-
pushError(
|
|
1129
|
+
pushError(
|
|
1130
|
+
lineNumber,
|
|
1131
|
+
"parallel blocks don't support else — list all concurrent messages directly inside the block"
|
|
1132
|
+
);
|
|
1042
1133
|
continue;
|
|
1043
1134
|
}
|
|
1044
1135
|
if (top.block.type === 'if') {
|
|
@@ -1049,14 +1140,17 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1049
1140
|
continue;
|
|
1050
1141
|
}
|
|
1051
1142
|
|
|
1052
|
-
// ---- Note parsing (
|
|
1143
|
+
// ---- Note parsing (space-separated only) ----
|
|
1053
1144
|
// Strategy:
|
|
1054
|
-
// 1. Try
|
|
1055
|
-
// 2.
|
|
1056
|
-
// 3.
|
|
1057
|
-
// 4. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
|
|
1145
|
+
// 1. Try bare note: `note text` — position defaults, text is everything after `note`
|
|
1146
|
+
// 2. For positioned: `note left [of] X text` — needs participant lookup to split name vs text
|
|
1147
|
+
// 3. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
|
|
1058
1148
|
{
|
|
1059
|
-
const noteParsed = parseNoteLine(
|
|
1149
|
+
const noteParsed = parseNoteLine(
|
|
1150
|
+
trimmed,
|
|
1151
|
+
result.participants,
|
|
1152
|
+
lastMsgFrom
|
|
1153
|
+
);
|
|
1060
1154
|
if (noteParsed) {
|
|
1061
1155
|
if (noteParsed.kind === 'single') {
|
|
1062
1156
|
const note: SequenceNote = {
|
|
@@ -1098,6 +1192,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1098
1192
|
continue;
|
|
1099
1193
|
}
|
|
1100
1194
|
}
|
|
1195
|
+
|
|
1196
|
+
// Catch-all: nothing matched this line
|
|
1197
|
+
pushWarning(lineNumber, `Unexpected line: '${trimmed}'.`);
|
|
1101
1198
|
}
|
|
1102
1199
|
|
|
1103
1200
|
// Validate: if no explicit chart line, check for arrow-based inference
|
|
@@ -1136,7 +1233,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1136
1233
|
|
|
1137
1234
|
for (const p of result.participants) {
|
|
1138
1235
|
if (!usedIds.has(p.id)) {
|
|
1139
|
-
pushWarning(
|
|
1236
|
+
pushWarning(
|
|
1237
|
+
p.lineNumber,
|
|
1238
|
+
`Participant "${p.label}" is declared but never used in any message or note`
|
|
1239
|
+
);
|
|
1140
1240
|
}
|
|
1141
1241
|
}
|
|
1142
1242
|
}
|
|
@@ -1144,21 +1244,30 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1144
1244
|
// Warn about empty groups
|
|
1145
1245
|
for (const group of result.groups) {
|
|
1146
1246
|
if (group.participantIds.length === 0) {
|
|
1147
|
-
pushWarning(
|
|
1247
|
+
pushWarning(
|
|
1248
|
+
group.lineNumber,
|
|
1249
|
+
`Empty group '${group.name}' — did you mean '== ${group.name} ==' for a section divider?`
|
|
1250
|
+
);
|
|
1148
1251
|
}
|
|
1149
1252
|
}
|
|
1150
1253
|
|
|
1151
1254
|
// Validate tag group values on participants and messages
|
|
1152
1255
|
if (result.tagGroups.length > 0) {
|
|
1153
|
-
const entities: Array<{
|
|
1256
|
+
const entities: Array<{
|
|
1257
|
+
metadata: Record<string, string>;
|
|
1258
|
+
lineNumber: number;
|
|
1259
|
+
}> = [];
|
|
1154
1260
|
for (const p of result.participants) {
|
|
1155
|
-
if (p.metadata)
|
|
1261
|
+
if (p.metadata)
|
|
1262
|
+
entities.push({ metadata: p.metadata, lineNumber: p.lineNumber });
|
|
1156
1263
|
}
|
|
1157
1264
|
for (const m of result.messages) {
|
|
1158
|
-
if (m.metadata)
|
|
1265
|
+
if (m.metadata)
|
|
1266
|
+
entities.push({ metadata: m.metadata, lineNumber: m.lineNumber });
|
|
1159
1267
|
}
|
|
1160
1268
|
for (const g of result.groups) {
|
|
1161
|
-
if (g.metadata)
|
|
1269
|
+
if (g.metadata)
|
|
1270
|
+
entities.push({ metadata: g.metadata, lineNumber: g.lineNumber });
|
|
1162
1271
|
}
|
|
1163
1272
|
validateTagValues(entities, result.tagGroups, pushWarning, suggest);
|
|
1164
1273
|
}
|