@diagrammo/dgmo 0.0.1
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/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/index.cjs +6698 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +685 -0
- package/dist/index.d.ts +685 -0
- package/dist/index.js +6611 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/chartjs.ts +784 -0
- package/src/colors.ts +75 -0
- package/src/d3.ts +5021 -0
- package/src/dgmo-mermaid.ts +247 -0
- package/src/dgmo-router.ts +77 -0
- package/src/echarts.ts +1207 -0
- package/src/index.ts +126 -0
- package/src/palettes/bold.ts +59 -0
- package/src/palettes/catppuccin.ts +76 -0
- package/src/palettes/color-utils.ts +191 -0
- package/src/palettes/gruvbox.ts +77 -0
- package/src/palettes/index.ts +35 -0
- package/src/palettes/mermaid-bridge.ts +220 -0
- package/src/palettes/nord.ts +59 -0
- package/src/palettes/one-dark.ts +62 -0
- package/src/palettes/registry.ts +92 -0
- package/src/palettes/rose-pine.ts +76 -0
- package/src/palettes/solarized.ts +69 -0
- package/src/palettes/tokyo-night.ts +78 -0
- package/src/palettes/types.ts +67 -0
- package/src/sequence/parser.ts +531 -0
- package/src/sequence/participant-inference.ts +178 -0
- package/src/sequence/renderer.ts +1487 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Palette Type Definitions
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Color definitions for a single mode (light or dark).
|
|
7
|
+
* 10 semantic UI colors + 9 named accent colors = 19 total.
|
|
8
|
+
*/
|
|
9
|
+
export interface PaletteColors {
|
|
10
|
+
// ── Surface hierarchy ──────────────────────────────────
|
|
11
|
+
/** Main background (#eceff4 light / #2e3440 dark for Nord) */
|
|
12
|
+
bg: string;
|
|
13
|
+
/** Cards, panels (#e5e9f0 / #3b4252) */
|
|
14
|
+
surface: string;
|
|
15
|
+
/** Popovers, dropdowns (#e5e9f0 / #434c5e) */
|
|
16
|
+
overlay: string;
|
|
17
|
+
/** Borders, dividers, muted (#d8dee9 / #4c566a) */
|
|
18
|
+
border: string;
|
|
19
|
+
|
|
20
|
+
// ── Text hierarchy ─────────────────────────────────────
|
|
21
|
+
/** Primary text (#2e3440 / #eceff4) */
|
|
22
|
+
text: string;
|
|
23
|
+
/** Secondary/diminished text (#4c566a / #d8dee9) */
|
|
24
|
+
textMuted: string;
|
|
25
|
+
|
|
26
|
+
// ── Semantic accents ───────────────────────────────────
|
|
27
|
+
/** Primary accent — buttons, links */
|
|
28
|
+
primary: string;
|
|
29
|
+
/** Secondary accent */
|
|
30
|
+
secondary: string;
|
|
31
|
+
/** Tertiary accent */
|
|
32
|
+
accent: string;
|
|
33
|
+
/** Error/danger */
|
|
34
|
+
destructive: string;
|
|
35
|
+
|
|
36
|
+
// ── Named accent colors ────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Used for: inline annotations (red), pie charts, cScale,
|
|
39
|
+
* series rotation, journey actors, Gantt tasks.
|
|
40
|
+
*/
|
|
41
|
+
colors: {
|
|
42
|
+
red: string;
|
|
43
|
+
orange: string;
|
|
44
|
+
yellow: string;
|
|
45
|
+
green: string;
|
|
46
|
+
blue: string;
|
|
47
|
+
purple: string;
|
|
48
|
+
teal: string;
|
|
49
|
+
cyan: string;
|
|
50
|
+
gray: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Complete palette definition. One object per color scheme.
|
|
56
|
+
* This is what palette authors create — the single artifact for NFR1.
|
|
57
|
+
*/
|
|
58
|
+
export interface PaletteConfig {
|
|
59
|
+
/** Registry key: 'nord', 'solarized', 'catppuccin' */
|
|
60
|
+
id: string;
|
|
61
|
+
/** Display name: 'Nord', 'Solarized', 'Catppuccin' */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Light mode color definitions */
|
|
64
|
+
light: PaletteColors;
|
|
65
|
+
/** Dark mode color definitions */
|
|
66
|
+
dark: PaletteColors;
|
|
67
|
+
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Sequence Diagram Parser (.dgmo format)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { inferParticipantType } from './participant-inference';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Participant types that can be declared via "Name is a type" syntax.
|
|
9
|
+
*/
|
|
10
|
+
export type ParticipantType =
|
|
11
|
+
| 'default'
|
|
12
|
+
| 'service'
|
|
13
|
+
| 'database'
|
|
14
|
+
| 'actor'
|
|
15
|
+
| 'queue'
|
|
16
|
+
| 'cache'
|
|
17
|
+
| 'gateway'
|
|
18
|
+
| 'external'
|
|
19
|
+
| 'networking'
|
|
20
|
+
| 'frontend';
|
|
21
|
+
|
|
22
|
+
const VALID_PARTICIPANT_TYPES: ReadonlySet<string> = new Set([
|
|
23
|
+
'service',
|
|
24
|
+
'database',
|
|
25
|
+
'actor',
|
|
26
|
+
'queue',
|
|
27
|
+
'cache',
|
|
28
|
+
'gateway',
|
|
29
|
+
'external',
|
|
30
|
+
'networking',
|
|
31
|
+
'frontend',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A declared or inferred participant in the sequence diagram.
|
|
36
|
+
*/
|
|
37
|
+
export interface SequenceParticipant {
|
|
38
|
+
/** Internal identifier (e.g. "AuthService") */
|
|
39
|
+
id: string;
|
|
40
|
+
/** Display label — uses aka alias if provided, otherwise id */
|
|
41
|
+
label: string;
|
|
42
|
+
/** Participant shape type */
|
|
43
|
+
type: ParticipantType;
|
|
44
|
+
/** Source line number (1-based) */
|
|
45
|
+
lineNumber: number;
|
|
46
|
+
/** Explicit layout position override (0-based from left, negative from right) */
|
|
47
|
+
position?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A message between two participants.
|
|
52
|
+
* Placeholder for future stories — included in the interface now for completeness.
|
|
53
|
+
*/
|
|
54
|
+
export interface SequenceMessage {
|
|
55
|
+
from: string;
|
|
56
|
+
to: string;
|
|
57
|
+
label: string;
|
|
58
|
+
returnLabel?: string;
|
|
59
|
+
lineNumber: number;
|
|
60
|
+
async?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A conditional or loop block in the sequence diagram.
|
|
65
|
+
*/
|
|
66
|
+
export interface SequenceBlock {
|
|
67
|
+
kind: 'block';
|
|
68
|
+
type: 'if' | 'loop' | 'parallel';
|
|
69
|
+
label: string;
|
|
70
|
+
children: SequenceElement[];
|
|
71
|
+
elseChildren: SequenceElement[];
|
|
72
|
+
lineNumber: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A labeled horizontal divider between message phases.
|
|
77
|
+
*/
|
|
78
|
+
export interface SequenceSection {
|
|
79
|
+
kind: 'section';
|
|
80
|
+
label: string;
|
|
81
|
+
color?: string;
|
|
82
|
+
lineNumber: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SequenceElement = SequenceMessage | SequenceBlock | SequenceSection;
|
|
86
|
+
|
|
87
|
+
export function isSequenceBlock(el: SequenceElement): el is SequenceBlock {
|
|
88
|
+
return 'kind' in el && (el as SequenceBlock).kind === 'block';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isSequenceSection(el: SequenceElement): el is SequenceSection {
|
|
92
|
+
return 'kind' in el && (el as SequenceSection).kind === 'section';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A named group of participants rendered as a labeled box.
|
|
97
|
+
*/
|
|
98
|
+
export interface SequenceGroup {
|
|
99
|
+
name: string;
|
|
100
|
+
color?: string;
|
|
101
|
+
participantIds: string[];
|
|
102
|
+
lineNumber: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parsed result from a .dgmo sequence diagram.
|
|
107
|
+
*/
|
|
108
|
+
export interface ParsedSequenceDgmo {
|
|
109
|
+
title: string | null;
|
|
110
|
+
participants: SequenceParticipant[];
|
|
111
|
+
messages: SequenceMessage[];
|
|
112
|
+
elements: SequenceElement[];
|
|
113
|
+
groups: SequenceGroup[];
|
|
114
|
+
sections: SequenceSection[];
|
|
115
|
+
options: Record<string, string>;
|
|
116
|
+
error: string | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// "Name is a type" pattern — e.g. "AuthService is a service"
|
|
120
|
+
// Remainder after type is parsed separately for aka/position modifiers
|
|
121
|
+
const IS_A_PATTERN = /^(\S+)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
|
|
122
|
+
|
|
123
|
+
// Standalone "Name position N" pattern — e.g. "DB position -1"
|
|
124
|
+
const POSITION_ONLY_PATTERN = /^(\S+)\s+position\s+(-?\d+)$/i;
|
|
125
|
+
|
|
126
|
+
// Group heading pattern — "## Backend" or "## Backend(blue)"
|
|
127
|
+
const GROUP_HEADING_PATTERN = /^##\s+(\S+?)(?:\((\w+)\))?$/;
|
|
128
|
+
|
|
129
|
+
// Section divider pattern — "== Label ==" or "== Label(color) =="
|
|
130
|
+
const SECTION_PATTERN = /^==\s+(.+?)\s*==$/;
|
|
131
|
+
|
|
132
|
+
// Arrow pattern for sequence inference — "A -> B: message" or "A ~> B: message"
|
|
133
|
+
const ARROW_PATTERN = /\S+\s*(?:->|~>)\s*\S+/;
|
|
134
|
+
|
|
135
|
+
// <- return syntax: "Login <- 200 OK"
|
|
136
|
+
const ARROW_RETURN_PATTERN = /^(.+?)\s*<-\s*(.+)$/;
|
|
137
|
+
|
|
138
|
+
// UML method(args): returnType syntax: "getUser(id): UserObj"
|
|
139
|
+
const UML_RETURN_PATTERN = /^(\w+\([^)]*\))\s*:\s*(.+)$/;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract return label from a message label string.
|
|
143
|
+
* Priority: `<-` syntax first, then UML `method(): return` syntax.
|
|
144
|
+
*/
|
|
145
|
+
function parseReturnLabel(rawLabel: string): {
|
|
146
|
+
label: string;
|
|
147
|
+
returnLabel?: string;
|
|
148
|
+
} {
|
|
149
|
+
if (!rawLabel) return { label: '' };
|
|
150
|
+
|
|
151
|
+
// Check <- syntax first
|
|
152
|
+
const arrowReturn = rawLabel.match(ARROW_RETURN_PATTERN);
|
|
153
|
+
if (arrowReturn) {
|
|
154
|
+
return { label: arrowReturn[1].trim(), returnLabel: arrowReturn[2].trim() };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check UML method(args): returnType syntax
|
|
158
|
+
const umlReturn = rawLabel.match(UML_RETURN_PATTERN);
|
|
159
|
+
if (umlReturn) {
|
|
160
|
+
return { label: umlReturn[1].trim(), returnLabel: umlReturn[2].trim() };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { label: rawLabel };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Measure leading whitespace of a line, normalizing tabs to 4 spaces.
|
|
168
|
+
*/
|
|
169
|
+
function measureIndent(line: string): number {
|
|
170
|
+
let indent = 0;
|
|
171
|
+
for (const ch of line) {
|
|
172
|
+
if (ch === ' ') indent++;
|
|
173
|
+
else if (ch === '\t') indent += 4;
|
|
174
|
+
else break;
|
|
175
|
+
}
|
|
176
|
+
return indent;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse a .dgmo file with `chart: sequence` into a structured representation.
|
|
181
|
+
*/
|
|
182
|
+
export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
183
|
+
const result: ParsedSequenceDgmo = {
|
|
184
|
+
title: null,
|
|
185
|
+
participants: [],
|
|
186
|
+
messages: [],
|
|
187
|
+
elements: [],
|
|
188
|
+
groups: [],
|
|
189
|
+
sections: [],
|
|
190
|
+
options: {},
|
|
191
|
+
error: null,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (!content || !content.trim()) {
|
|
195
|
+
result.error = 'Empty content';
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const lines = content.split('\n');
|
|
200
|
+
let hasExplicitChart = false;
|
|
201
|
+
|
|
202
|
+
// Group parsing state — tracks the active ## group heading
|
|
203
|
+
let activeGroup: SequenceGroup | null = null;
|
|
204
|
+
|
|
205
|
+
// Block parsing state
|
|
206
|
+
const blockStack: {
|
|
207
|
+
block: SequenceBlock;
|
|
208
|
+
indent: number;
|
|
209
|
+
inElse: boolean;
|
|
210
|
+
}[] = [];
|
|
211
|
+
const currentContainer = (): SequenceElement[] => {
|
|
212
|
+
if (blockStack.length === 0) return result.elements;
|
|
213
|
+
const top = blockStack[blockStack.length - 1];
|
|
214
|
+
return top.inElse ? top.block.elseChildren : top.block.children;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < lines.length; i++) {
|
|
218
|
+
const raw = lines[i];
|
|
219
|
+
const trimmed = raw.trim();
|
|
220
|
+
const lineNumber = i + 1;
|
|
221
|
+
|
|
222
|
+
// Skip empty lines
|
|
223
|
+
if (!trimmed) {
|
|
224
|
+
activeGroup = null;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Parse group heading — must be checked before comment skip since ## starts with #
|
|
229
|
+
const groupMatch = trimmed.match(GROUP_HEADING_PATTERN);
|
|
230
|
+
if (groupMatch) {
|
|
231
|
+
activeGroup = {
|
|
232
|
+
name: groupMatch[1],
|
|
233
|
+
color: groupMatch[2] || undefined,
|
|
234
|
+
participantIds: [],
|
|
235
|
+
lineNumber,
|
|
236
|
+
};
|
|
237
|
+
result.groups.push(activeGroup);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Close active group on non-indented, non-group lines
|
|
242
|
+
if (activeGroup && measureIndent(raw) === 0) {
|
|
243
|
+
activeGroup = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Skip comments
|
|
247
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
|
|
248
|
+
|
|
249
|
+
// Parse section dividers — "== Label ==" or "== Label(color) =="
|
|
250
|
+
const sectionMatch = trimmed.match(SECTION_PATTERN);
|
|
251
|
+
if (sectionMatch) {
|
|
252
|
+
const labelRaw = sectionMatch[1].trim();
|
|
253
|
+
const colorMatch = labelRaw.match(/^(.+?)\((\w+)\)$/);
|
|
254
|
+
const section: SequenceSection = {
|
|
255
|
+
kind: 'section',
|
|
256
|
+
label: colorMatch ? colorMatch[1].trim() : labelRaw,
|
|
257
|
+
color: colorMatch ? colorMatch[2] : undefined,
|
|
258
|
+
lineNumber,
|
|
259
|
+
};
|
|
260
|
+
result.sections.push(section);
|
|
261
|
+
currentContainer().push(section);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Parse header key: value lines (always top-level)
|
|
266
|
+
const colonIndex = trimmed.indexOf(':');
|
|
267
|
+
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>')) {
|
|
268
|
+
const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
|
|
269
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
270
|
+
|
|
271
|
+
if (key === 'chart') {
|
|
272
|
+
hasExplicitChart = true;
|
|
273
|
+
if (value.toLowerCase() !== 'sequence') {
|
|
274
|
+
result.error = `Expected chart type "sequence", got "${value}"`;
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (key === 'title') {
|
|
281
|
+
result.title = value;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Store other options
|
|
286
|
+
result.options[key] = value;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Parse "Name is a type [aka Alias]" declarations (always top-level)
|
|
291
|
+
const isAMatch = trimmed.match(IS_A_PATTERN);
|
|
292
|
+
if (isAMatch) {
|
|
293
|
+
const id = isAMatch[1];
|
|
294
|
+
const typeStr = isAMatch[2].toLowerCase();
|
|
295
|
+
const remainder = isAMatch[3]?.trim() || '';
|
|
296
|
+
|
|
297
|
+
const participantType: ParticipantType = VALID_PARTICIPANT_TYPES.has(
|
|
298
|
+
typeStr
|
|
299
|
+
)
|
|
300
|
+
? (typeStr as ParticipantType)
|
|
301
|
+
: 'default';
|
|
302
|
+
|
|
303
|
+
// Parse modifiers from remainder: aka ALIAS, position N
|
|
304
|
+
const akaMatch = remainder.match(
|
|
305
|
+
/\baka\s+(.+?)(?:\s+position\s+-?\d+\s*$|$)/i
|
|
306
|
+
);
|
|
307
|
+
const posMatch = remainder.match(/\bposition\s+(-?\d+)/i);
|
|
308
|
+
const alias = akaMatch ? akaMatch[1].trim() : null;
|
|
309
|
+
const position = posMatch ? parseInt(posMatch[1], 10) : undefined;
|
|
310
|
+
|
|
311
|
+
// Avoid duplicate participant declarations
|
|
312
|
+
if (!result.participants.some((p) => p.id === id)) {
|
|
313
|
+
result.participants.push({
|
|
314
|
+
id,
|
|
315
|
+
label: alias || id,
|
|
316
|
+
type: participantType,
|
|
317
|
+
lineNumber,
|
|
318
|
+
...(position !== undefined ? { position } : {}),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// Track group membership
|
|
322
|
+
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
323
|
+
activeGroup.participantIds.push(id);
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Parse standalone "Name position N" (no "is a" type)
|
|
329
|
+
const posOnlyMatch = trimmed.match(POSITION_ONLY_PATTERN);
|
|
330
|
+
if (posOnlyMatch) {
|
|
331
|
+
const id = posOnlyMatch[1];
|
|
332
|
+
const position = parseInt(posOnlyMatch[2], 10);
|
|
333
|
+
|
|
334
|
+
if (!result.participants.some((p) => p.id === id)) {
|
|
335
|
+
result.participants.push({
|
|
336
|
+
id,
|
|
337
|
+
label: id,
|
|
338
|
+
type: inferParticipantType(id),
|
|
339
|
+
lineNumber,
|
|
340
|
+
position,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// Track group membership
|
|
344
|
+
if (activeGroup && !activeGroup.participantIds.includes(id)) {
|
|
345
|
+
activeGroup.participantIds.push(id);
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Bare participant name inside an active group (single identifier on an indented line)
|
|
351
|
+
if (activeGroup && measureIndent(raw) > 0 && /^\S+$/.test(trimmed)) {
|
|
352
|
+
const id = trimmed;
|
|
353
|
+
if (!result.participants.some((p) => p.id === id)) {
|
|
354
|
+
result.participants.push({
|
|
355
|
+
id,
|
|
356
|
+
label: id,
|
|
357
|
+
type: inferParticipantType(id),
|
|
358
|
+
lineNumber,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (!activeGroup.participantIds.includes(id)) {
|
|
362
|
+
activeGroup.participantIds.push(id);
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---- Indent-aware parsing for messages and block keywords ----
|
|
368
|
+
const indent = measureIndent(raw);
|
|
369
|
+
|
|
370
|
+
// Close blocks whose scope has ended (indent decreased)
|
|
371
|
+
while (blockStack.length > 0) {
|
|
372
|
+
const top = blockStack[blockStack.length - 1];
|
|
373
|
+
if (indent > top.indent) break;
|
|
374
|
+
if (
|
|
375
|
+
indent === top.indent &&
|
|
376
|
+
trimmed.toLowerCase() === 'else' &&
|
|
377
|
+
top.block.type === 'if'
|
|
378
|
+
)
|
|
379
|
+
break;
|
|
380
|
+
blockStack.pop();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Parse message lines first — arrows take priority over keywords
|
|
384
|
+
// Detect async prefix: "async A -> B: msg"
|
|
385
|
+
let isAsync = false;
|
|
386
|
+
let arrowLine = trimmed;
|
|
387
|
+
const asyncPrefixMatch = trimmed.match(/^async\s+(.+)$/i);
|
|
388
|
+
if (asyncPrefixMatch) {
|
|
389
|
+
isAsync = true;
|
|
390
|
+
arrowLine = asyncPrefixMatch[1];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Match ~> (async arrow) or -> (sync arrow)
|
|
394
|
+
const asyncArrowMatch = arrowLine.match(
|
|
395
|
+
/^(\S+)\s*~>\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
396
|
+
);
|
|
397
|
+
const syncArrowMatch = arrowLine.match(
|
|
398
|
+
/^(\S+)\s*->\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
399
|
+
);
|
|
400
|
+
const arrowMatch = asyncArrowMatch || syncArrowMatch;
|
|
401
|
+
if (asyncArrowMatch) isAsync = true;
|
|
402
|
+
|
|
403
|
+
if (arrowMatch) {
|
|
404
|
+
const from = arrowMatch[1];
|
|
405
|
+
const to = arrowMatch[2];
|
|
406
|
+
const rawLabel = arrowMatch[3]?.trim() || '';
|
|
407
|
+
|
|
408
|
+
// Extract return label — skip for async messages
|
|
409
|
+
const { label, returnLabel } = isAsync
|
|
410
|
+
? { label: rawLabel, returnLabel: undefined }
|
|
411
|
+
: parseReturnLabel(rawLabel);
|
|
412
|
+
|
|
413
|
+
const msg: SequenceMessage = {
|
|
414
|
+
from,
|
|
415
|
+
to,
|
|
416
|
+
label,
|
|
417
|
+
returnLabel,
|
|
418
|
+
lineNumber,
|
|
419
|
+
...(isAsync ? { async: true } : {}),
|
|
420
|
+
};
|
|
421
|
+
result.messages.push(msg);
|
|
422
|
+
currentContainer().push(msg);
|
|
423
|
+
|
|
424
|
+
// Auto-register participants from message usage with type inference
|
|
425
|
+
if (!result.participants.some((p) => p.id === from)) {
|
|
426
|
+
result.participants.push({
|
|
427
|
+
id: from,
|
|
428
|
+
label: from,
|
|
429
|
+
type: inferParticipantType(from),
|
|
430
|
+
lineNumber,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (!result.participants.some((p) => p.id === to)) {
|
|
434
|
+
result.participants.push({
|
|
435
|
+
id: to,
|
|
436
|
+
label: to,
|
|
437
|
+
type: inferParticipantType(to),
|
|
438
|
+
lineNumber,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Parse 'if <label>' block keyword
|
|
445
|
+
const ifMatch = trimmed.match(/^if\s+(.+)$/i);
|
|
446
|
+
if (ifMatch) {
|
|
447
|
+
const block: SequenceBlock = {
|
|
448
|
+
kind: 'block',
|
|
449
|
+
type: 'if',
|
|
450
|
+
label: ifMatch[1].trim(),
|
|
451
|
+
children: [],
|
|
452
|
+
elseChildren: [],
|
|
453
|
+
lineNumber,
|
|
454
|
+
};
|
|
455
|
+
currentContainer().push(block);
|
|
456
|
+
blockStack.push({ block, indent, inElse: false });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Parse 'loop <label>' block keyword
|
|
461
|
+
const loopMatch = trimmed.match(/^loop\s+(.+)$/i);
|
|
462
|
+
if (loopMatch) {
|
|
463
|
+
const block: SequenceBlock = {
|
|
464
|
+
kind: 'block',
|
|
465
|
+
type: 'loop',
|
|
466
|
+
label: loopMatch[1].trim(),
|
|
467
|
+
children: [],
|
|
468
|
+
elseChildren: [],
|
|
469
|
+
lineNumber,
|
|
470
|
+
};
|
|
471
|
+
currentContainer().push(block);
|
|
472
|
+
blockStack.push({ block, indent, inElse: false });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Parse 'parallel [label]' block keyword
|
|
477
|
+
const parallelMatch = trimmed.match(/^parallel(?:\s+(.+))?$/i);
|
|
478
|
+
if (parallelMatch) {
|
|
479
|
+
const block: SequenceBlock = {
|
|
480
|
+
kind: 'block',
|
|
481
|
+
type: 'parallel',
|
|
482
|
+
label: parallelMatch[1]?.trim() || '',
|
|
483
|
+
children: [],
|
|
484
|
+
elseChildren: [],
|
|
485
|
+
lineNumber,
|
|
486
|
+
};
|
|
487
|
+
currentContainer().push(block);
|
|
488
|
+
blockStack.push({ block, indent, inElse: false });
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Parse 'else' keyword (only applies to 'if' blocks)
|
|
493
|
+
if (trimmed.toLowerCase() === 'else') {
|
|
494
|
+
if (
|
|
495
|
+
blockStack.length > 0 &&
|
|
496
|
+
blockStack[blockStack.length - 1].indent === indent &&
|
|
497
|
+
blockStack[blockStack.length - 1].block.type === 'if'
|
|
498
|
+
) {
|
|
499
|
+
blockStack[blockStack.length - 1].inElse = true;
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Validate: if no explicit chart line, check for arrow-based inference
|
|
506
|
+
if (!hasExplicitChart && result.messages.length === 0) {
|
|
507
|
+
// Check if raw content has arrow patterns for inference
|
|
508
|
+
const hasArrows = lines.some((line) => ARROW_PATTERN.test(line.trim()));
|
|
509
|
+
if (!hasArrows) {
|
|
510
|
+
result.error =
|
|
511
|
+
'No "chart: sequence" header and no sequence content detected';
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Detect whether raw content looks like a sequence diagram.
|
|
521
|
+
* Used by the chart type inference logic.
|
|
522
|
+
*/
|
|
523
|
+
export function looksLikeSequence(content: string): boolean {
|
|
524
|
+
if (!content) return false;
|
|
525
|
+
const lines = content.split('\n');
|
|
526
|
+
return lines.some((line) => {
|
|
527
|
+
const trimmed = line.trim();
|
|
528
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('//')) return false;
|
|
529
|
+
return ARROW_PATTERN.test(trimmed);
|
|
530
|
+
});
|
|
531
|
+
}
|