@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.
@@ -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
+ }