@diagrammo/dgmo 0.4.2 → 0.4.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/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +197 -154
- package/dist/index.cjs +8647 -3447
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -58
- package/dist/index.d.ts +503 -58
- package/dist/index.js +8379 -3200
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +578 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1553 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sharing.ts +8 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
package/src/sequence/parser.ts
CHANGED
|
@@ -6,7 +6,9 @@ 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 } from '../utils/parsing';
|
|
9
|
+
import { measureIndent, extractColor, parsePipeMetadata } from '../utils/parsing';
|
|
10
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
11
|
+
import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Participant types that can be declared via "Name is a type" syntax.
|
|
@@ -49,6 +51,8 @@ export interface SequenceParticipant {
|
|
|
49
51
|
lineNumber: number;
|
|
50
52
|
/** Explicit layout position override (0-based from left, negative from right) */
|
|
51
53
|
position?: number;
|
|
54
|
+
/** Pipe-delimited tag metadata (e.g. `| role: Gateway`) */
|
|
55
|
+
metadata?: Record<string, string>;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
@@ -60,6 +64,8 @@ export interface SequenceMessage {
|
|
|
60
64
|
label: string;
|
|
61
65
|
lineNumber: number;
|
|
62
66
|
async?: boolean;
|
|
67
|
+
/** Pipe-delimited tag metadata (e.g. `| c: Caching`) */
|
|
68
|
+
metadata?: Record<string, string>;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
@@ -86,7 +92,6 @@ export interface SequenceBlock {
|
|
|
86
92
|
export interface SequenceSection {
|
|
87
93
|
kind: 'section';
|
|
88
94
|
label: string;
|
|
89
|
-
color?: string;
|
|
90
95
|
lineNumber: number;
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -125,9 +130,10 @@ export function isSequenceNote(el: SequenceElement): el is SequenceNote {
|
|
|
125
130
|
*/
|
|
126
131
|
export interface SequenceGroup {
|
|
127
132
|
name: string;
|
|
128
|
-
color?: string;
|
|
129
133
|
participantIds: string[];
|
|
130
134
|
lineNumber: number;
|
|
135
|
+
/** Pipe-delimited tag metadata (e.g. `[Backend | t: Product]`) */
|
|
136
|
+
metadata?: Record<string, string>;
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
/**
|
|
@@ -141,6 +147,7 @@ export interface ParsedSequenceDgmo {
|
|
|
141
147
|
elements: SequenceElement[];
|
|
142
148
|
groups: SequenceGroup[];
|
|
143
149
|
sections: SequenceSection[];
|
|
150
|
+
tagGroups: TagGroup[];
|
|
144
151
|
options: Record<string, string>;
|
|
145
152
|
diagnostics: DgmoError[];
|
|
146
153
|
error: string | null;
|
|
@@ -153,8 +160,13 @@ const IS_A_PATTERN = /^(\S+)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
|
|
|
153
160
|
// Standalone "Name position N" pattern — e.g. "DB position -1"
|
|
154
161
|
const POSITION_ONLY_PATTERN = /^(\S+)\s+position\s+(-?\d+)$/i;
|
|
155
162
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
163
|
+
// Colored participant declaration — e.g. "Tapin2(green)", "API(blue)"
|
|
164
|
+
const COLORED_PARTICIPANT_PATTERN = /^(\S+?)\(([^)]+)\)\s*$/;
|
|
165
|
+
|
|
166
|
+
// Group heading pattern — "[Backend]", "[API Services(blue)]", "[Backend(#hex)]"
|
|
167
|
+
const GROUP_HEADING_PATTERN = /^\[(.+?)(?:\(([^)]+)\))?\]\s*$/;
|
|
168
|
+
// Legacy ## syntax — detect and emit migration error
|
|
169
|
+
const LEGACY_GROUP_PATTERN = /^##\s+(.+?)(?:\(([^)]+)\))?\s*$/;
|
|
158
170
|
|
|
159
171
|
// Section divider pattern — "== Label ==", "== Label(color) ==", or "== Label" (trailing == optional)
|
|
160
172
|
const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
|
|
@@ -178,6 +190,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
178
190
|
elements: [],
|
|
179
191
|
groups: [],
|
|
180
192
|
sections: [],
|
|
193
|
+
tagGroups: [],
|
|
181
194
|
options: {},
|
|
182
195
|
diagnostics: [],
|
|
183
196
|
error: null,
|
|
@@ -197,6 +210,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
197
210
|
if (!result.error) result.error = formatDgmoError(diag);
|
|
198
211
|
};
|
|
199
212
|
|
|
213
|
+
/** Push a non-fatal warning (does not set result.error). */
|
|
214
|
+
const pushWarning = (line: number, message: string): void => {
|
|
215
|
+
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
216
|
+
};
|
|
217
|
+
|
|
200
218
|
if (!content || !content.trim()) {
|
|
201
219
|
return fail(0, 'Empty content');
|
|
202
220
|
}
|
|
@@ -205,12 +223,26 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
205
223
|
let hasExplicitChart = false;
|
|
206
224
|
let contentStarted = false;
|
|
207
225
|
|
|
208
|
-
// Group parsing state — tracks the active
|
|
226
|
+
// Group parsing state — tracks the active [Group] heading
|
|
209
227
|
let activeGroup: SequenceGroup | null = null;
|
|
210
228
|
|
|
211
229
|
// Track participant → group name for duplicate membership detection
|
|
212
230
|
const participantGroupMap = new Map<string, string>();
|
|
213
231
|
|
|
232
|
+
// Tag group parsing state
|
|
233
|
+
let currentTagGroup: TagGroup | null = null;
|
|
234
|
+
const aliasMap = new Map<string, string>();
|
|
235
|
+
|
|
236
|
+
/** Split pipe metadata from a line: "core | k: v" → { core, meta } */
|
|
237
|
+
const splitPipe = (text: string): { core: string; meta?: Record<string, string> } => {
|
|
238
|
+
const idx = text.indexOf('|');
|
|
239
|
+
if (idx < 0) return { core: text };
|
|
240
|
+
const core = text.substring(0, idx).trimEnd();
|
|
241
|
+
const segments = text.substring(idx).split('|');
|
|
242
|
+
const meta = parsePipeMetadata(segments, aliasMap);
|
|
243
|
+
return Object.keys(meta).length > 0 ? { core, meta } : { core };
|
|
244
|
+
};
|
|
245
|
+
|
|
214
246
|
// Block parsing state
|
|
215
247
|
const blockStack: {
|
|
216
248
|
block: SequenceBlock;
|
|
@@ -236,28 +268,58 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
236
268
|
// Skip empty lines
|
|
237
269
|
if (!trimmed) {
|
|
238
270
|
activeGroup = null;
|
|
271
|
+
currentTagGroup = null;
|
|
239
272
|
continue;
|
|
240
273
|
}
|
|
241
274
|
|
|
242
|
-
// Parse group heading —
|
|
275
|
+
// Parse group heading — [Group Name] or [Group Name(color)] or [Group | k: v]
|
|
243
276
|
const groupMatch = trimmed.match(GROUP_HEADING_PATTERN);
|
|
244
277
|
if (groupMatch) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
278
|
+
let groupName = groupMatch[1].trim();
|
|
279
|
+
let groupColor = groupMatch[2]?.trim();
|
|
280
|
+
let groupMeta: Record<string, string> | undefined;
|
|
281
|
+
|
|
282
|
+
// Check for pipe metadata inside brackets
|
|
283
|
+
const gpipeIdx = groupName.indexOf('|');
|
|
284
|
+
if (gpipeIdx >= 0) {
|
|
285
|
+
const nameAndColor = groupName.substring(0, gpipeIdx).trimEnd();
|
|
286
|
+
const segments = groupName.substring(gpipeIdx).split('|');
|
|
287
|
+
const meta = parsePipeMetadata(segments, aliasMap);
|
|
288
|
+
if (Object.keys(meta).length > 0) groupMeta = meta;
|
|
289
|
+
// Re-extract color from name part
|
|
290
|
+
const colorSuffix = nameAndColor.match(/^(.+?)\(([^)]+)\)$/);
|
|
291
|
+
if (colorSuffix) {
|
|
292
|
+
groupName = colorSuffix[1].trim();
|
|
293
|
+
groupColor = colorSuffix[2].trim();
|
|
294
|
+
} else {
|
|
295
|
+
groupName = nameAndColor;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (groupColor) {
|
|
300
|
+
pushWarning(lineNumber, `(${groupColor}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`);
|
|
249
301
|
}
|
|
250
302
|
contentStarted = true;
|
|
251
303
|
activeGroup = {
|
|
252
|
-
name:
|
|
253
|
-
color: groupColor || undefined,
|
|
304
|
+
name: groupName,
|
|
254
305
|
participantIds: [],
|
|
255
306
|
lineNumber,
|
|
307
|
+
...(groupMeta ? { metadata: groupMeta } : {}),
|
|
256
308
|
};
|
|
257
309
|
result.groups.push(activeGroup);
|
|
258
310
|
continue;
|
|
259
311
|
}
|
|
260
312
|
|
|
313
|
+
// Reject legacy ## group syntax with migration hint
|
|
314
|
+
if (trimmed.match(LEGACY_GROUP_PATTERN)) {
|
|
315
|
+
const legacyMatch = trimmed.match(LEGACY_GROUP_PATTERN)!;
|
|
316
|
+
const name = legacyMatch[1].trim();
|
|
317
|
+
const color = legacyMatch[2]?.trim();
|
|
318
|
+
const suggestion = color ? `[${name}(${color})]` : `[${name}]`;
|
|
319
|
+
pushError(lineNumber, `'## ${name}' group syntax is no longer supported. Use '${suggestion}' instead`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
261
323
|
// Close active group on non-indented, non-group lines
|
|
262
324
|
if (activeGroup && measureIndent(raw) === 0) {
|
|
263
325
|
activeGroup = null;
|
|
@@ -266,12 +328,56 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
266
328
|
// Skip comments — only // is supported
|
|
267
329
|
if (trimmed.startsWith('//')) continue;
|
|
268
330
|
|
|
269
|
-
// Reject # as comment syntax
|
|
270
|
-
if (trimmed.startsWith('#')
|
|
271
|
-
pushError(lineNumber, 'Use // for comments
|
|
331
|
+
// Reject # as comment syntax
|
|
332
|
+
if (trimmed.startsWith('#')) {
|
|
333
|
+
pushError(lineNumber, 'Use // for comments');
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- Tag group handling ----
|
|
338
|
+
// Tag block heading: "tag: Name [alias X]"
|
|
339
|
+
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
340
|
+
if (tagBlockMatch && !tagBlockMatch.deprecated) {
|
|
341
|
+
if (contentStarted) {
|
|
342
|
+
pushError(lineNumber, 'Tag groups must appear before sequence content');
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
currentTagGroup = {
|
|
346
|
+
name: tagBlockMatch.name,
|
|
347
|
+
alias: tagBlockMatch.alias,
|
|
348
|
+
entries: [],
|
|
349
|
+
lineNumber,
|
|
350
|
+
};
|
|
351
|
+
if (tagBlockMatch.alias) {
|
|
352
|
+
aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
|
|
353
|
+
}
|
|
354
|
+
result.tagGroups.push(currentTagGroup);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Tag group entries (indented Value(color) [default] under tag: heading)
|
|
359
|
+
if (currentTagGroup && !contentStarted && measureIndent(raw) > 0) {
|
|
360
|
+
const isDefault = /\bdefault\s*$/.test(trimmed);
|
|
361
|
+
const entryText = isDefault
|
|
362
|
+
? trimmed.replace(/\s+default\s*$/, '').trim()
|
|
363
|
+
: trimmed;
|
|
364
|
+
const { label, color } = extractColor(entryText);
|
|
365
|
+
if (!color) {
|
|
366
|
+
pushError(lineNumber, `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (isDefault) {
|
|
370
|
+
currentTagGroup.defaultValue = label;
|
|
371
|
+
}
|
|
372
|
+
currentTagGroup.entries.push({ value: label, color, lineNumber });
|
|
272
373
|
continue;
|
|
273
374
|
}
|
|
274
375
|
|
|
376
|
+
// Non-indented line after tag group — close it
|
|
377
|
+
if (currentTagGroup) {
|
|
378
|
+
currentTagGroup = null;
|
|
379
|
+
}
|
|
380
|
+
|
|
275
381
|
// Parse section dividers — "== Label ==" or "== Label(color) =="
|
|
276
382
|
// Close blocks first — sections at indent 0 should not nest inside blocks
|
|
277
383
|
const sectionMatch = trimmed.match(SECTION_PATTERN);
|
|
@@ -284,15 +390,13 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
284
390
|
}
|
|
285
391
|
const labelRaw = sectionMatch[1].trim();
|
|
286
392
|
const colorMatch = labelRaw.match(/^(.+?)\(([^)]+)\)$/);
|
|
287
|
-
if (colorMatch
|
|
288
|
-
|
|
289
|
-
continue;
|
|
393
|
+
if (colorMatch) {
|
|
394
|
+
pushWarning(lineNumber, `(${colorMatch[2].trim()}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`);
|
|
290
395
|
}
|
|
291
396
|
contentStarted = true;
|
|
292
397
|
const section: SequenceSection = {
|
|
293
398
|
kind: 'section',
|
|
294
399
|
label: colorMatch ? colorMatch[1].trim() : labelRaw,
|
|
295
|
-
color: colorMatch ? colorMatch[2].trim() : undefined,
|
|
296
400
|
lineNumber,
|
|
297
401
|
};
|
|
298
402
|
result.sections.push(section);
|
|
@@ -303,7 +407,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
303
407
|
// Parse header key: value lines (always top-level)
|
|
304
408
|
// Skip 'note' lines — parsed in the indent-aware section below
|
|
305
409
|
const colonIndex = trimmed.indexOf(':');
|
|
306
|
-
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>') && !trimmed.includes('<-') && !trimmed.includes('<~')) {
|
|
410
|
+
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>') && !trimmed.includes('<-') && !trimmed.includes('<~') && !trimmed.includes('|')) {
|
|
307
411
|
const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
|
|
308
412
|
if (key === 'note' || key.startsWith('note ')) {
|
|
309
413
|
// Fall through to indent-aware note parsing below
|
|
@@ -337,7 +441,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
337
441
|
}
|
|
338
442
|
|
|
339
443
|
// Parse "Name is a type [aka Alias]" declarations (always top-level)
|
|
340
|
-
const
|
|
444
|
+
const { core: isACore, meta: isAMeta } = splitPipe(trimmed);
|
|
445
|
+
const isAMatch = isACore.match(IS_A_PATTERN);
|
|
341
446
|
if (isAMatch) {
|
|
342
447
|
contentStarted = true;
|
|
343
448
|
const id = isAMatch[1];
|
|
@@ -366,6 +471,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
366
471
|
type: participantType,
|
|
367
472
|
lineNumber,
|
|
368
473
|
...(position !== undefined ? { position } : {}),
|
|
474
|
+
...(isAMeta ? { metadata: isAMeta } : {}),
|
|
369
475
|
});
|
|
370
476
|
}
|
|
371
477
|
// Track group membership
|
|
@@ -382,7 +488,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
382
488
|
}
|
|
383
489
|
|
|
384
490
|
// Parse standalone "Name position N" (no "is a" type)
|
|
385
|
-
const
|
|
491
|
+
const { core: posCore, meta: posMeta } = splitPipe(trimmed);
|
|
492
|
+
const posOnlyMatch = posCore.match(POSITION_ONLY_PATTERN);
|
|
386
493
|
if (posOnlyMatch) {
|
|
387
494
|
contentStarted = true;
|
|
388
495
|
const id = posOnlyMatch[1];
|
|
@@ -395,6 +502,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
395
502
|
type: inferParticipantType(id),
|
|
396
503
|
lineNumber,
|
|
397
504
|
position,
|
|
505
|
+
...(posMeta ? { metadata: posMeta } : {}),
|
|
398
506
|
});
|
|
399
507
|
}
|
|
400
508
|
// Track group membership
|
|
@@ -410,19 +518,25 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
410
518
|
continue;
|
|
411
519
|
}
|
|
412
520
|
|
|
413
|
-
//
|
|
414
|
-
|
|
521
|
+
// Colored participant declaration — "Name(color)" at any level
|
|
522
|
+
// Color syntax is deprecated — emit warning and register without color
|
|
523
|
+
const { core: colorCore, meta: colorMeta } = splitPipe(trimmed);
|
|
524
|
+
const coloredMatch = colorCore.match(COLORED_PARTICIPANT_PATTERN);
|
|
525
|
+
if (coloredMatch && !ARROW_PATTERN.test(colorCore)) {
|
|
526
|
+
const id = coloredMatch[1];
|
|
527
|
+
const color = coloredMatch[2].trim();
|
|
528
|
+
pushWarning(lineNumber, `(${color}) color syntax removed from sequence diagrams — use 'tag:' groups for coloring`);
|
|
415
529
|
contentStarted = true;
|
|
416
|
-
const id = trimmed;
|
|
417
530
|
if (!result.participants.some((p) => p.id === id)) {
|
|
418
531
|
result.participants.push({
|
|
419
532
|
id,
|
|
420
533
|
label: id,
|
|
421
534
|
type: inferParticipantType(id),
|
|
422
535
|
lineNumber,
|
|
536
|
+
...(colorMeta ? { metadata: colorMeta } : {}),
|
|
423
537
|
});
|
|
424
538
|
}
|
|
425
|
-
if (!activeGroup.participantIds.includes(id)) {
|
|
539
|
+
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
426
540
|
const existingGroup = participantGroupMap.get(id);
|
|
427
541
|
if (existingGroup) {
|
|
428
542
|
pushError(lineNumber, `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`);
|
|
@@ -434,6 +548,36 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
434
548
|
continue;
|
|
435
549
|
}
|
|
436
550
|
|
|
551
|
+
// Bare participant name — either inside an active group (indented) or top-level declaration
|
|
552
|
+
// Supports pipe metadata: " API | c: Gateway" or "Tapin2 | l:Park"
|
|
553
|
+
{
|
|
554
|
+
const { core: bareCore, meta: bareMeta } = splitPipe(trimmed);
|
|
555
|
+
const inGroup = activeGroup && measureIndent(raw) > 0;
|
|
556
|
+
if (/^\S+$/.test(bareCore) && !ARROW_PATTERN.test(bareCore) && (inGroup || !contentStarted || bareMeta)) {
|
|
557
|
+
contentStarted = true;
|
|
558
|
+
const id = bareCore;
|
|
559
|
+
if (!result.participants.some((p) => p.id === id)) {
|
|
560
|
+
result.participants.push({
|
|
561
|
+
id,
|
|
562
|
+
label: id,
|
|
563
|
+
type: inferParticipantType(id),
|
|
564
|
+
lineNumber,
|
|
565
|
+
...(bareMeta ? { metadata: bareMeta } : {}),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
569
|
+
const existingGroup = participantGroupMap.get(id);
|
|
570
|
+
if (existingGroup) {
|
|
571
|
+
pushError(lineNumber, `Participant '${id}' is already in group '${existingGroup}' — participants can only belong to one group`);
|
|
572
|
+
} else {
|
|
573
|
+
activeGroup.participantIds.push(id);
|
|
574
|
+
participantGroupMap.set(id, activeGroup.name);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
437
581
|
// ---- Indent-aware parsing for messages and block keywords ----
|
|
438
582
|
const indent = measureIndent(raw);
|
|
439
583
|
|
|
@@ -452,9 +596,12 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
452
596
|
blockStack.pop();
|
|
453
597
|
}
|
|
454
598
|
|
|
599
|
+
// Split pipe metadata before arrow parsing (arrows use $ anchor)
|
|
600
|
+
const { core: arrowCore, meta: arrowMeta } = splitPipe(trimmed);
|
|
601
|
+
|
|
455
602
|
// Parse message lines first — arrows take priority over keywords
|
|
456
603
|
// Reject "async" keyword prefix — use ~> instead
|
|
457
|
-
const asyncPrefixMatch =
|
|
604
|
+
const asyncPrefixMatch = arrowCore.match(/^async\s+(.+)$/i);
|
|
458
605
|
if (asyncPrefixMatch && ARROW_PATTERN.test(asyncPrefixMatch[1])) {
|
|
459
606
|
pushError(lineNumber, 'Use ~> for async messages: A ~> B: message');
|
|
460
607
|
continue;
|
|
@@ -462,7 +609,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
462
609
|
|
|
463
610
|
// ---- Labeled arrows: -label->, ~label~> ----
|
|
464
611
|
// Must be checked BEFORE plain arrow patterns to avoid partial matches
|
|
465
|
-
const labeledArrow = parseArrow(
|
|
612
|
+
const labeledArrow = parseArrow(arrowCore);
|
|
466
613
|
if (labeledArrow && 'error' in labeledArrow) {
|
|
467
614
|
pushError(lineNumber, labeledArrow.error);
|
|
468
615
|
continue;
|
|
@@ -478,6 +625,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
478
625
|
label,
|
|
479
626
|
lineNumber,
|
|
480
627
|
...(isAsync ? { async: true } : {}),
|
|
628
|
+
...(arrowMeta ? { metadata: arrowMeta } : {}),
|
|
481
629
|
};
|
|
482
630
|
result.messages.push(msg);
|
|
483
631
|
currentContainer().push(msg);
|
|
@@ -503,10 +651,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
503
651
|
}
|
|
504
652
|
|
|
505
653
|
// ---- Error: old colon-postfix syntax (A -> B: msg) ----
|
|
506
|
-
const colonPostfixSync =
|
|
654
|
+
const colonPostfixSync = arrowCore.match(
|
|
507
655
|
/^(\S+)\s*->\s*([^\s:]+)\s*:\s*(.+)$/
|
|
508
656
|
);
|
|
509
|
-
const colonPostfixAsync =
|
|
657
|
+
const colonPostfixAsync = arrowCore.match(
|
|
510
658
|
/^(\S+)\s*~>\s*([^\s:]+)\s*:\s*(.+)$/
|
|
511
659
|
);
|
|
512
660
|
const colonPostfix = colonPostfixSync || colonPostfixAsync;
|
|
@@ -524,7 +672,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
524
672
|
}
|
|
525
673
|
|
|
526
674
|
// ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
|
|
527
|
-
const bidiPlainMatch =
|
|
675
|
+
const bidiPlainMatch = arrowCore.match(
|
|
528
676
|
/^(\S+)\s*(?:<->|<~>)\s*(\S+)/
|
|
529
677
|
);
|
|
530
678
|
if (bidiPlainMatch) {
|
|
@@ -536,8 +684,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
536
684
|
}
|
|
537
685
|
|
|
538
686
|
// ---- Deprecated bare return arrows: A <- B, A <~ B ----
|
|
539
|
-
const bareReturnSync =
|
|
540
|
-
const bareReturnAsync =
|
|
687
|
+
const bareReturnSync = arrowCore.match(/^(\S+)\s+<-\s+(\S+)$/);
|
|
688
|
+
const bareReturnAsync = arrowCore.match(/^(\S+)\s+<~\s+(\S+)$/);
|
|
541
689
|
const bareReturn = bareReturnSync || bareReturnAsync;
|
|
542
690
|
if (bareReturn) {
|
|
543
691
|
const to = bareReturn[1];
|
|
@@ -550,8 +698,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
550
698
|
}
|
|
551
699
|
|
|
552
700
|
// ---- Bare (unlabeled) call arrows: A -> B, A ~> B ----
|
|
553
|
-
const bareCallSync =
|
|
554
|
-
const bareCallAsync =
|
|
701
|
+
const bareCallSync = arrowCore.match(/^(\S+)\s*->\s*(\S+)$/);
|
|
702
|
+
const bareCallAsync = arrowCore.match(/^(\S+)\s*~>\s*(\S+)$/);
|
|
555
703
|
const bareCall = bareCallSync || bareCallAsync;
|
|
556
704
|
if (bareCall) {
|
|
557
705
|
contentStarted = true;
|
|
@@ -565,6 +713,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
565
713
|
label: '',
|
|
566
714
|
lineNumber,
|
|
567
715
|
...(bareCallAsync ? { async: true } : {}),
|
|
716
|
+
...(arrowMeta ? { metadata: arrowMeta } : {}),
|
|
568
717
|
};
|
|
569
718
|
result.messages.push(msg);
|
|
570
719
|
currentContainer().push(msg);
|
|
@@ -747,10 +896,6 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
747
896
|
}
|
|
748
897
|
}
|
|
749
898
|
|
|
750
|
-
const warn = (line: number, message: string): void => {
|
|
751
|
-
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
752
|
-
};
|
|
753
|
-
|
|
754
899
|
// Warn about unused participants (only when the diagram has messages)
|
|
755
900
|
if (result.messages.length > 0) {
|
|
756
901
|
const usedIds = new Set<string>();
|
|
@@ -778,7 +923,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
778
923
|
|
|
779
924
|
for (const p of result.participants) {
|
|
780
925
|
if (!usedIds.has(p.id)) {
|
|
781
|
-
|
|
926
|
+
pushWarning(p.lineNumber, `Participant "${p.label}" is declared but never used in any message or note`);
|
|
782
927
|
}
|
|
783
928
|
}
|
|
784
929
|
}
|
|
@@ -786,8 +931,23 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
786
931
|
// Warn about empty groups
|
|
787
932
|
for (const group of result.groups) {
|
|
788
933
|
if (group.participantIds.length === 0) {
|
|
789
|
-
|
|
934
|
+
pushWarning(group.lineNumber, `Group "${group.name}" has no participants`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Validate tag group values on participants and messages
|
|
939
|
+
if (result.tagGroups.length > 0) {
|
|
940
|
+
const entities: Array<{ metadata: Record<string, string>; lineNumber: number }> = [];
|
|
941
|
+
for (const p of result.participants) {
|
|
942
|
+
if (p.metadata) entities.push({ metadata: p.metadata, lineNumber: p.lineNumber });
|
|
943
|
+
}
|
|
944
|
+
for (const m of result.messages) {
|
|
945
|
+
if (m.metadata) entities.push({ metadata: m.metadata, lineNumber: m.lineNumber });
|
|
946
|
+
}
|
|
947
|
+
for (const g of result.groups) {
|
|
948
|
+
if (g.metadata) entities.push({ metadata: g.metadata, lineNumber: g.lineNumber });
|
|
790
949
|
}
|
|
950
|
+
validateTagValues(entities, result.tagGroups, pushWarning, suggest);
|
|
791
951
|
}
|
|
792
952
|
|
|
793
953
|
return result;
|