@coral-viz/language 0.2.3

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.
Files changed (47) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +5 -0
  3. package/dist/diagnostics.d.ts +81 -0
  4. package/dist/diagnostics.d.ts.map +1 -0
  5. package/dist/diagnostics.js +59 -0
  6. package/dist/diagnostics.js.map +1 -0
  7. package/dist/formats/dot.d.ts +23 -0
  8. package/dist/formats/dot.d.ts.map +1 -0
  9. package/dist/formats/dot.js +541 -0
  10. package/dist/formats/dot.js.map +1 -0
  11. package/dist/formats/index.d.ts +6 -0
  12. package/dist/formats/index.d.ts.map +1 -0
  13. package/dist/formats/index.js +6 -0
  14. package/dist/formats/index.js.map +1 -0
  15. package/dist/formats/mermaid.d.ts +29 -0
  16. package/dist/formats/mermaid.d.ts.map +1 -0
  17. package/dist/formats/mermaid.js +1051 -0
  18. package/dist/formats/mermaid.js.map +1 -0
  19. package/dist/formats/plantuml.d.ts +20 -0
  20. package/dist/formats/plantuml.d.ts.map +1 -0
  21. package/dist/formats/plantuml.js +271 -0
  22. package/dist/formats/plantuml.js.map +1 -0
  23. package/dist/ids.d.ts +36 -0
  24. package/dist/ids.d.ts.map +1 -0
  25. package/dist/ids.js +69 -0
  26. package/dist/ids.js.map +1 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/parser/index.d.ts +13 -0
  32. package/dist/parser/index.d.ts.map +1 -0
  33. package/dist/parser/index.js +415 -0
  34. package/dist/parser/index.js.map +1 -0
  35. package/dist/printer/index.d.ts +47 -0
  36. package/dist/printer/index.d.ts.map +1 -0
  37. package/dist/printer/index.js +392 -0
  38. package/dist/printer/index.js.map +1 -0
  39. package/dist/strings.d.ts +17 -0
  40. package/dist/strings.d.ts.map +1 -0
  41. package/dist/strings.js +48 -0
  42. package/dist/strings.js.map +1 -0
  43. package/dist/types.d.ts +40 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +2 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +64 -0
@@ -0,0 +1,1051 @@
1
+ /**
2
+ * Mermaid Diagram Importer
3
+ *
4
+ * Converts Mermaid diagram syntax to Graph-IR.
5
+ *
6
+ * Supported diagram types (Mermaid v11.12.2):
7
+ * - flowchart/graph
8
+ * - sequenceDiagram
9
+ * - classDiagram
10
+ * - stateDiagram / stateDiagram-v2
11
+ * - erDiagram
12
+ * - timeline
13
+ * - block-beta
14
+ * - packet-beta
15
+ * - kanban
16
+ * - architecture-beta
17
+ *
18
+ * See SPECS.md for version tracking and feature status.
19
+ */
20
+ import { finishParse, parseDiagnostic } from '../diagnostics.js';
21
+ // Diagram type detection patterns
22
+ const DIAGRAM_PATTERNS = {
23
+ flowchart: /^(flowchart|graph)\s+(TB|TD|BT|LR|RL)?/i,
24
+ sequence: /^sequenceDiagram/i,
25
+ class: /^classDiagram/i,
26
+ state: /^stateDiagram(-v2)?/i,
27
+ er: /^erDiagram/i,
28
+ timeline: /^timeline/i,
29
+ block: /^block-beta/i,
30
+ packet: /^packet-beta/i,
31
+ kanban: /^kanban/i,
32
+ architecture: /^architecture-beta/i,
33
+ };
34
+ // Unsupported diagram types (for clear error messages)
35
+ const UNSUPPORTED_DIAGRAMS = {
36
+ gantt: 'Gantt charts',
37
+ pie: 'Pie charts',
38
+ quadrantChart: 'Quadrant charts',
39
+ requirementDiagram: 'Requirement diagrams',
40
+ gitGraph: 'Git graphs',
41
+ C4Context: 'C4 diagrams',
42
+ mindmap: 'Mind maps',
43
+ journey: 'User journey diagrams',
44
+ zenuml: 'ZenUML diagrams',
45
+ sankey: 'Sankey diagrams',
46
+ xychart: 'XY charts',
47
+ radar: 'Radar charts',
48
+ treemap: 'Treemaps',
49
+ };
50
+ /**
51
+ * Parse Mermaid diagram syntax into Graph-IR
52
+ */
53
+ export function parseMermaid(source, options = {}) {
54
+ const trimmed = source.trim();
55
+ if (!trimmed) {
56
+ return finishParse([
57
+ parseDiagnostic({
58
+ severity: 'error',
59
+ code: 'mermaid.input.unparsed',
60
+ message: 'Empty input',
61
+ position: { line: 1, column: 0, offset: 0 },
62
+ }),
63
+ ]);
64
+ }
65
+ const lines = trimmed.split('\n');
66
+ const firstLine = lines[0].trim();
67
+ // Detect diagram type
68
+ const diagramType = detectDiagramType(firstLine);
69
+ if (!diagramType) {
70
+ // Check if it's an unsupported type
71
+ const unsupportedType = detectUnsupportedType(firstLine);
72
+ if (unsupportedType) {
73
+ return finishParse([
74
+ parseDiagnostic({
75
+ severity: 'error',
76
+ code: 'mermaid.diagram.unsupported',
77
+ message: `Unsupported Mermaid diagram type: ${unsupportedType}. Supported types: flowchart, sequenceDiagram, classDiagram, stateDiagram, erDiagram, timeline, block-beta, packet-beta, kanban, architecture-beta`,
78
+ position: { line: 1, column: 0, offset: 0 },
79
+ }),
80
+ ]);
81
+ }
82
+ return finishParse([
83
+ parseDiagnostic({
84
+ severity: 'error',
85
+ code: 'mermaid.diagram.unparsed',
86
+ message: 'Could not detect Mermaid diagram type. Line must start with a valid diagram keyword.',
87
+ position: { line: 1, column: 0, offset: 0 },
88
+ }),
89
+ ]);
90
+ }
91
+ // Parse based on diagram type
92
+ switch (diagramType.type) {
93
+ case 'flowchart':
94
+ return parseFlowchart(lines, diagramType.match, options);
95
+ case 'sequence':
96
+ return parseSequence(lines, options);
97
+ case 'class':
98
+ return parseClass(lines, options);
99
+ case 'state':
100
+ return parseState(lines, options);
101
+ case 'er':
102
+ return parseER(lines, options);
103
+ case 'timeline':
104
+ return parseTimeline(lines, options);
105
+ case 'block':
106
+ return parseBlock(lines, options);
107
+ case 'packet':
108
+ return parsePacket(lines, options);
109
+ case 'kanban':
110
+ return parseKanban(lines, options);
111
+ case 'architecture':
112
+ return parseArchitecture(lines, options);
113
+ default:
114
+ return finishParse([
115
+ parseDiagnostic({
116
+ severity: 'error',
117
+ code: 'mermaid.diagram.unsupported',
118
+ message: `Parser not implemented for: ${diagramType.type}`,
119
+ position: { line: 1, column: 0, offset: 0 },
120
+ }),
121
+ ]);
122
+ }
123
+ }
124
+ function detectDiagramType(firstLine) {
125
+ for (const [type, pattern] of Object.entries(DIAGRAM_PATTERNS)) {
126
+ const match = firstLine.match(pattern);
127
+ if (match) {
128
+ return { type, match };
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function detectUnsupportedType(firstLine) {
134
+ for (const [keyword, description] of Object.entries(UNSUPPORTED_DIAGRAMS)) {
135
+ if (firstLine.toLowerCase().startsWith(keyword.toLowerCase())) {
136
+ return description;
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+ function parseFlowchart(lines, headerMatch, options) {
142
+ const ctx = {
143
+ nodeMap: new Map(),
144
+ edges: [],
145
+ edgeCounter: 0,
146
+ diagnostics: [],
147
+ currentLine: 0,
148
+ shapeMap: new Map(),
149
+ };
150
+ const direction = headerMatch[2]?.toUpperCase();
151
+ const layoutOptions = {
152
+ direction: directionMap[direction] || 'DOWN',
153
+ };
154
+ let subgraphStack = [];
155
+ for (let i = 1; i < lines.length; i++) {
156
+ ctx.currentLine = i + 1;
157
+ const line = lines[i].trim();
158
+ if (!line || line.startsWith('%%'))
159
+ continue;
160
+ // Subgraph start
161
+ const subgraphMatch = line.match(/^subgraph\s+(\w+)(?:\s*\[([^\]]+)\])?\s*$/);
162
+ if (subgraphMatch) {
163
+ subgraphStack.push({
164
+ id: subgraphMatch[1],
165
+ label: subgraphMatch[2],
166
+ nodes: [],
167
+ });
168
+ continue;
169
+ }
170
+ // Subgraph end
171
+ if (line === 'end' && subgraphStack.length > 0) {
172
+ const subgraph = subgraphStack.pop();
173
+ const groupNode = {
174
+ id: subgraph.id,
175
+ type: 'group',
176
+ label: subgraph.label || subgraph.id,
177
+ children: subgraph.nodes.map(nodeId => {
178
+ const node = ctx.nodeMap.get(nodeId);
179
+ node.parent = subgraph.id;
180
+ return node;
181
+ }),
182
+ };
183
+ ctx.nodeMap.set(subgraph.id, groupNode);
184
+ continue;
185
+ }
186
+ // Parse edges or nodes
187
+ const edgeParsed = parseFlowchartEdgeLine(line, ctx);
188
+ if (edgeParsed) {
189
+ if (subgraphStack.length > 0) {
190
+ const current = subgraphStack[subgraphStack.length - 1];
191
+ for (const nodeId of edgeParsed.nodeIds) {
192
+ if (!current.nodes.includes(nodeId)) {
193
+ current.nodes.push(nodeId);
194
+ }
195
+ }
196
+ }
197
+ continue;
198
+ }
199
+ const nodeParsed = parseFlowchartNodeLine(line, ctx);
200
+ if (nodeParsed) {
201
+ if (subgraphStack.length > 0) {
202
+ const current = subgraphStack[subgraphStack.length - 1];
203
+ if (!current.nodes.includes(nodeParsed)) {
204
+ current.nodes.push(nodeParsed);
205
+ }
206
+ }
207
+ continue;
208
+ }
209
+ // Nothing matched. Previously this line was skipped in silence.
210
+ ctx.diagnostics.push(parseDiagnostic({
211
+ severity: 'info',
212
+ code: 'mermaid.line.unparsed',
213
+ message: `Line not recognised by the flowchart parser: ${line}`,
214
+ position: { line: ctx.currentLine, column: 0, offset: 0 },
215
+ source: line,
216
+ }));
217
+ }
218
+ const nodes = [];
219
+ for (const node of ctx.nodeMap.values()) {
220
+ if (!node.parent)
221
+ nodes.push(node);
222
+ }
223
+ return finishParse(ctx.diagnostics, {
224
+ version: '1.0.0',
225
+ id: options.graphId || 'mermaid-flowchart',
226
+ name: options.graphName,
227
+ nodes,
228
+ edges: ctx.edges,
229
+ layoutOptions,
230
+ });
231
+ }
232
+ const directionMap = {
233
+ TB: 'DOWN', TD: 'DOWN', BT: 'UP', LR: 'RIGHT', RL: 'LEFT',
234
+ };
235
+ // Flowchart node shapes: [pattern, nodeType, mermaidShape]
236
+ const FLOWCHART_SHAPES = [
237
+ [/^\[\(([^\)]*)\)\]$/, 'database', 'database'], // [(text)]
238
+ [/^\(\[([^\]]*)\]\)$/, 'service', 'stadium'], // ([text])
239
+ [/^\[\[([^\]]*)\]\]$/, 'module', 'subroutine'], // [[text]]
240
+ [/^\(\(\(([^\)]*)\)\)\)$/, 'actor', 'doublecircle'], // (((text)))
241
+ [/^\(\(([^\)]*)\)\)$/, 'service', 'circle'], // ((text))
242
+ [/^\(([^\)]*)\)$/, 'service', 'round'], // (text)
243
+ [/^\{([^\}]*)\}$/, 'module', 'diamond'], // {text}
244
+ [/^\{\{([^\}]*)\}\}$/, 'group', 'hexagon'], // {{text}}
245
+ [/^\[([^\]]*)\]$/, 'service', 'box'], // [text]
246
+ [/^>([^\]]*)\]$/, 'service', 'asymmetric'], // >text]
247
+ ];
248
+ function parseFlowchartNodeLine(line, ctx) {
249
+ const nodePattern = /^(\w+)(.*)$/;
250
+ const match = line.match(nodePattern);
251
+ if (!match)
252
+ return null;
253
+ const id = match[1];
254
+ const rest = match[2].trim();
255
+ if (!rest || ctx.nodeMap.has(id)) {
256
+ if (!ctx.nodeMap.has(id)) {
257
+ ctx.nodeMap.set(id, { id, type: 'service', label: id });
258
+ }
259
+ return id;
260
+ }
261
+ let type = 'service';
262
+ let label = id;
263
+ let matchedShape = false;
264
+ for (const [pattern, nodeType, mermaidShape] of FLOWCHART_SHAPES) {
265
+ const shapeMatch = rest.match(pattern);
266
+ if (shapeMatch) {
267
+ type = nodeType;
268
+ label = shapeMatch[1] || id;
269
+ matchedShape = true;
270
+ if (mermaidShape)
271
+ ctx.shapeMap.set(id, mermaidShape);
272
+ break;
273
+ }
274
+ }
275
+ // A bare identifier is a node. Text following it must be a recognised
276
+ // Mermaid shape; otherwise accepting only the first word hides bad output.
277
+ if (rest && !matchedShape)
278
+ return null;
279
+ ctx.nodeMap.set(id, { id, type, label });
280
+ return id;
281
+ }
282
+ // Edge patterns for flowchart
283
+ const EDGE_ARROWS = [
284
+ { pattern: /-->/, style: undefined },
285
+ { pattern: /---/, style: { targetArrow: 'none' } },
286
+ { pattern: /-\.->/, style: { lineStyle: 'dashed' } },
287
+ { pattern: /==>/, style: { lineStyle: 'solid' } },
288
+ { pattern: /--o/, style: { targetArrow: 'circle' } },
289
+ { pattern: /--x/, style: { targetArrow: 'none' } },
290
+ { pattern: /~~~/, style: { lineStyle: 'dotted' } },
291
+ ];
292
+ // Node shape patterns for the two-pass tokenizer
293
+ const NODE_SHAPE_PATTERNS = [
294
+ [/^\[\(([^\)]*)\)\]/, 'database'], // [(text)]
295
+ [/^\(\[([^\]]*)\]\)/, 'stadium'], // ([text])
296
+ [/^\[\[([^\]]*)\]\]/, 'subroutine'], // [[text]]
297
+ [/^\(\(\(([^\)]*)\)\)\)/, 'doublecircle'], // (((text)))
298
+ [/^\(\(([^\)]*)\)\)/, 'circle'], // ((text))
299
+ [/^\(([^\)]*)\)/, 'round'], // (text)
300
+ [/^\{\{([^\}]*)\}\}/, 'hexagon'], // {{text}}
301
+ [/^\{([^\}]*)\}/, 'diamond'], // {text}
302
+ [/^\[([^\]]*)\]/, 'box'], // [text]
303
+ ];
304
+ function tokenizeNode(text) {
305
+ const idMatch = text.match(/^(\w+)/);
306
+ if (!idMatch)
307
+ return null;
308
+ const id = idMatch[1];
309
+ const afterId = text.slice(id.length);
310
+ for (const [pattern, shape] of NODE_SHAPE_PATTERNS) {
311
+ const shapeMatch = afterId.match(pattern);
312
+ if (shapeMatch) {
313
+ return { id, consumed: id.length + shapeMatch[0].length, label: shapeMatch[1], shape };
314
+ }
315
+ }
316
+ return { id, consumed: id.length };
317
+ }
318
+ function tokenizeArrow(text) {
319
+ const arrowMatch = text.match(/^(-.->|-->|===|==>|---|--o|--x|~~~)/);
320
+ if (!arrowMatch)
321
+ return null;
322
+ const arrowType = arrowMatch[1];
323
+ let consumed = arrowType.length;
324
+ // Check for edge label: |label text|
325
+ let label;
326
+ const rest = text.slice(consumed);
327
+ const labelMatch = rest.match(/^\|([^|]*)\|/);
328
+ if (labelMatch) {
329
+ label = labelMatch[1];
330
+ consumed += labelMatch[0].length;
331
+ }
332
+ const arrowDef = EDGE_ARROWS.find(a => a.pattern.test(arrowType));
333
+ return { consumed, label, style: arrowDef?.style };
334
+ }
335
+ function parseFlowchartEdgeLine(line, ctx) {
336
+ // Two-pass tokenizer: alternate node → arrow → node → arrow → ...
337
+ // Handles chains like: A[label] -->|text| B{shape} -.-> C
338
+ const parts = [];
339
+ const arrows = [];
340
+ let pos = 0;
341
+ const trimmed = line.trimStart();
342
+ pos = 0;
343
+ // Parse first node
344
+ const firstNode = tokenizeNode(trimmed);
345
+ if (!firstNode)
346
+ return null;
347
+ parts.push(firstNode);
348
+ pos = firstNode.consumed;
349
+ // Alternate: skip whitespace → arrow → skip whitespace → node
350
+ while (pos < trimmed.length) {
351
+ // Skip whitespace
352
+ while (pos < trimmed.length && /\s/.test(trimmed[pos]))
353
+ pos++;
354
+ if (pos >= trimmed.length)
355
+ break;
356
+ const arrow = tokenizeArrow(trimmed.slice(pos));
357
+ if (!arrow)
358
+ break;
359
+ pos += arrow.consumed;
360
+ // Skip whitespace
361
+ while (pos < trimmed.length && /\s/.test(trimmed[pos]))
362
+ pos++;
363
+ if (pos >= trimmed.length)
364
+ break;
365
+ const node = tokenizeNode(trimmed.slice(pos));
366
+ if (!node)
367
+ break;
368
+ parts.push(node);
369
+ arrows.push({ label: arrow.label, style: arrow.style });
370
+ pos += node.consumed;
371
+ }
372
+ if (parts.length < 2 || arrows.length === 0)
373
+ return null;
374
+ const nodeIds = [];
375
+ for (const part of parts) {
376
+ nodeIds.push(part.id);
377
+ if (!ctx.nodeMap.has(part.id)) {
378
+ ctx.nodeMap.set(part.id, {
379
+ id: part.id,
380
+ type: shapeToType(part.shape),
381
+ label: part.label || part.id,
382
+ });
383
+ }
384
+ else if (part.label) {
385
+ const existing = ctx.nodeMap.get(part.id);
386
+ existing.label = part.label;
387
+ if (part.shape)
388
+ existing.type = shapeToType(part.shape);
389
+ }
390
+ // Track shape from tokenizeNode path
391
+ if (part.shape)
392
+ ctx.shapeMap.set(part.id, part.shape);
393
+ }
394
+ for (let i = 0; i < arrows.length && i < parts.length - 1; i++) {
395
+ const sourceId = parts[i].id;
396
+ const targetId = parts[i + 1].id;
397
+ const edge = {
398
+ id: `edge_${++ctx.edgeCounter}`,
399
+ source: sourceId,
400
+ target: targetId,
401
+ };
402
+ if (arrows[i].label)
403
+ edge.label = arrows[i].label;
404
+ if (arrows[i].style)
405
+ edge.style = arrows[i].style;
406
+ // Map edge labels to ports when source is a diamond (decision)
407
+ if (ctx.shapeMap.get(sourceId) === 'diamond' && edge.label) {
408
+ const normalizedLabel = edge.label.toLowerCase().trim();
409
+ if (['yes', 'y', 'true'].includes(normalizedLabel)) {
410
+ edge.sourcePort = 'yes';
411
+ }
412
+ else if (['no', 'n', 'false'].includes(normalizedLabel)) {
413
+ edge.sourcePort = 'no';
414
+ }
415
+ else {
416
+ edge.sourcePort = 'alt';
417
+ }
418
+ }
419
+ // Set targetPort for non-diamond targets
420
+ if (ctx.shapeMap.get(sourceId) === 'diamond' && edge.label) {
421
+ if (ctx.shapeMap.get(targetId) !== 'diamond') {
422
+ edge.targetPort = 'in';
423
+ }
424
+ }
425
+ ctx.edges.push(edge);
426
+ }
427
+ return { nodeIds };
428
+ }
429
+ function shapeToType(shape) {
430
+ switch (shape) {
431
+ case 'database': return 'database';
432
+ case 'doublecircle':
433
+ case 'circle': return 'actor';
434
+ case 'diamond':
435
+ case 'subroutine': return 'module';
436
+ case 'hexagon': return 'group';
437
+ default: return 'service';
438
+ }
439
+ }
440
+ // ============================================================================
441
+ // Sequence Diagram Parser
442
+ // ============================================================================
443
+ function parseSequence(lines, options) {
444
+ const nodes = [];
445
+ const edges = [];
446
+ const nodeMap = new Map();
447
+ let edgeCounter = 0;
448
+ for (let i = 1; i < lines.length; i++) {
449
+ const line = lines[i].trim();
450
+ if (!line || line.startsWith('%%'))
451
+ continue;
452
+ // Participant/Actor declaration
453
+ const participantMatch = line.match(/^(participant|actor)\s+(\w+)(?:\s+as\s+(.+))?$/i);
454
+ if (participantMatch) {
455
+ const type = participantMatch[1].toLowerCase() === 'actor' ? 'actor' : 'service';
456
+ const id = participantMatch[2];
457
+ const label = participantMatch[3] || id;
458
+ if (!nodeMap.has(id)) {
459
+ const node = { id, type, label };
460
+ nodeMap.set(id, node);
461
+ nodes.push(node);
462
+ }
463
+ continue;
464
+ }
465
+ // Message: A->>B: text or A-->>B: text etc
466
+ const messageMatch = line.match(/^(\w+)\s*(--?>?>|--?>>|<<--?>>?|-x|--x|-\)|--\))\s*(\w+)\s*:\s*(.*)$/);
467
+ if (messageMatch) {
468
+ const [, source, arrow, target, label] = messageMatch;
469
+ // Ensure nodes exist
470
+ for (const id of [source, target]) {
471
+ if (!nodeMap.has(id)) {
472
+ const node = { id, type: 'service', label: id };
473
+ nodeMap.set(id, node);
474
+ nodes.push(node);
475
+ }
476
+ }
477
+ const edge = {
478
+ id: `msg_${++edgeCounter}`,
479
+ source,
480
+ target,
481
+ label,
482
+ type: 'message',
483
+ };
484
+ // Set style based on arrow
485
+ if (arrow.includes('--')) {
486
+ edge.style = { lineStyle: 'dashed' };
487
+ }
488
+ if (arrow.includes('x')) {
489
+ edge.style = { ...edge.style, targetArrow: 'none' };
490
+ }
491
+ edges.push(edge);
492
+ }
493
+ }
494
+ return finishParse([], {
495
+ version: '1.0.0',
496
+ id: options.graphId || 'mermaid-sequence',
497
+ name: options.graphName,
498
+ nodes,
499
+ edges,
500
+ metadata: { custom: { diagramType: 'sequence' } },
501
+ });
502
+ }
503
+ // ============================================================================
504
+ // Class Diagram Parser
505
+ // ============================================================================
506
+ function parseClass(lines, options) {
507
+ const nodes = [];
508
+ const edges = [];
509
+ const nodeMap = new Map();
510
+ let edgeCounter = 0;
511
+ let currentClass = null;
512
+ let inClassBody = false;
513
+ for (let i = 1; i < lines.length; i++) {
514
+ const line = lines[i].trim();
515
+ if (!line || line.startsWith('%%'))
516
+ continue;
517
+ // Class with body start
518
+ const classBodyMatch = line.match(/^class\s+(\w+)(?:\["([^"]+)"\])?\s*\{$/);
519
+ if (classBodyMatch) {
520
+ const id = classBodyMatch[1];
521
+ const label = classBodyMatch[2] || id;
522
+ currentClass = { id, type: 'module', label, properties: { members: [] } };
523
+ nodeMap.set(id, currentClass);
524
+ nodes.push(currentClass);
525
+ inClassBody = true;
526
+ continue;
527
+ }
528
+ // End of class body
529
+ if (line === '}' && inClassBody) {
530
+ currentClass = null;
531
+ inClassBody = false;
532
+ continue;
533
+ }
534
+ // Member inside class body
535
+ if (inClassBody && currentClass) {
536
+ currentClass.properties.members.push(line);
537
+ continue;
538
+ }
539
+ // Simple class declaration
540
+ const simpleClassMatch = line.match(/^class\s+(\w+)(?:\["([^"]+)"\])?$/);
541
+ if (simpleClassMatch) {
542
+ const id = simpleClassMatch[1];
543
+ const label = simpleClassMatch[2] || id;
544
+ if (!nodeMap.has(id)) {
545
+ const node = { id, type: 'module', label };
546
+ nodeMap.set(id, node);
547
+ nodes.push(node);
548
+ }
549
+ continue;
550
+ }
551
+ // Relationship: ClassA <|-- ClassB, ClassA *-- ClassB, etc.
552
+ const relationMatch = line.match(/^(\w+)\s*(<\|--|<\|\.\.|\*--|o--|-->|--|\.\.\>|\.\.\|>|\.\.)\s*(\w+)(?:\s*:\s*(.+))?$/);
553
+ if (relationMatch) {
554
+ const [, classA, rel, classB, label] = relationMatch;
555
+ for (const id of [classA, classB]) {
556
+ if (!nodeMap.has(id)) {
557
+ const node = { id, type: 'module', label: id };
558
+ nodeMap.set(id, node);
559
+ nodes.push(node);
560
+ }
561
+ }
562
+ const relTypes = {
563
+ '<|--': 'inheritance',
564
+ '<|..': 'realization',
565
+ '*--': 'composition',
566
+ 'o--': 'aggregation',
567
+ '-->': 'association',
568
+ '--': 'link',
569
+ '..>': 'dependency',
570
+ '..|>': 'realization',
571
+ '..': 'dashed_link',
572
+ };
573
+ const edge = {
574
+ id: `rel_${++edgeCounter}`,
575
+ source: classB,
576
+ target: classA,
577
+ type: relTypes[rel] || 'association',
578
+ };
579
+ if (label)
580
+ edge.label = label;
581
+ if (rel.includes('..'))
582
+ edge.style = { lineStyle: 'dashed' };
583
+ edges.push(edge);
584
+ }
585
+ }
586
+ return finishParse([], {
587
+ version: '1.0.0',
588
+ id: options.graphId || 'mermaid-class',
589
+ name: options.graphName,
590
+ nodes,
591
+ edges,
592
+ metadata: { custom: { diagramType: 'class' } },
593
+ });
594
+ }
595
+ // ============================================================================
596
+ // State Diagram Parser
597
+ // ============================================================================
598
+ function parseState(lines, options) {
599
+ const nodes = [];
600
+ const edges = [];
601
+ const nodeMap = new Map();
602
+ let edgeCounter = 0;
603
+ for (let i = 1; i < lines.length; i++) {
604
+ const line = lines[i].trim();
605
+ if (!line || line.startsWith('%%') || line.startsWith('direction'))
606
+ continue;
607
+ // State declaration with description
608
+ const stateMatch = line.match(/^state\s+"?(\w+)"?(?:\s*:\s*(.+))?$/);
609
+ if (stateMatch) {
610
+ const id = stateMatch[1];
611
+ const description = stateMatch[2];
612
+ if (!nodeMap.has(id)) {
613
+ const node = { id, type: 'module', label: id };
614
+ if (description)
615
+ node.description = description;
616
+ nodeMap.set(id, node);
617
+ nodes.push(node);
618
+ }
619
+ continue;
620
+ }
621
+ // Transition: State1 --> State2 : label
622
+ const transitionMatch = line.match(/^(\[\*\]|\w+)\s*-->\s*(\[\*\]|\w+)(?:\s*:\s*(.+))?$/);
623
+ if (transitionMatch) {
624
+ const [, source, target, label] = transitionMatch;
625
+ const sourceId = source === '[*]' ? '_start_' : source;
626
+ const targetId = target === '[*]' ? '_end_' : target;
627
+ for (const [id, type] of [[sourceId, source === '[*]' ? 'actor' : 'module'], [targetId, target === '[*]' ? 'actor' : 'module']]) {
628
+ if (!nodeMap.has(id)) {
629
+ const node = {
630
+ id,
631
+ type: type,
632
+ label: id === '_start_' ? 'Start' : id === '_end_' ? 'End' : id,
633
+ };
634
+ nodeMap.set(id, node);
635
+ nodes.push(node);
636
+ }
637
+ }
638
+ const edge = {
639
+ id: `trans_${++edgeCounter}`,
640
+ source: sourceId,
641
+ target: targetId,
642
+ type: 'transition',
643
+ };
644
+ if (label)
645
+ edge.label = label;
646
+ edges.push(edge);
647
+ }
648
+ }
649
+ return finishParse([], {
650
+ version: '1.0.0',
651
+ id: options.graphId || 'mermaid-state',
652
+ name: options.graphName,
653
+ nodes,
654
+ edges,
655
+ metadata: { custom: { diagramType: 'state' } },
656
+ });
657
+ }
658
+ // ============================================================================
659
+ // ER Diagram Parser
660
+ // ============================================================================
661
+ function parseER(lines, options) {
662
+ const nodes = [];
663
+ const edges = [];
664
+ const nodeMap = new Map();
665
+ let edgeCounter = 0;
666
+ let currentEntity = null;
667
+ for (let i = 1; i < lines.length; i++) {
668
+ const line = lines[i].trim();
669
+ if (!line || line.startsWith('%%'))
670
+ continue;
671
+ // Entity with attributes block start
672
+ const entityBlockMatch = line.match(/^(\w+)\s*\{$/);
673
+ if (entityBlockMatch) {
674
+ const id = entityBlockMatch[1];
675
+ currentEntity = { id, type: 'database', label: id, properties: { attributes: [] } };
676
+ nodeMap.set(id, currentEntity);
677
+ nodes.push(currentEntity);
678
+ continue;
679
+ }
680
+ // End of entity block
681
+ if (line === '}' && currentEntity) {
682
+ currentEntity = null;
683
+ continue;
684
+ }
685
+ // Attribute inside entity
686
+ if (currentEntity && !line.includes('||') && !line.includes('}')) {
687
+ currentEntity.properties.attributes.push(line);
688
+ continue;
689
+ }
690
+ // Relationship: ENTITY1 ||--o{ ENTITY2 : "label"
691
+ const relMatch = line.match(/^(\w+)\s*(\|o|\|\||o\{|\}\||\|{|}o|o\|)\s*--\s*(\|o|\|\||o\{|\}\||\|{|}o|o\|)\s*(\w+)\s*:\s*"?([^"]+)"?$/);
692
+ if (relMatch) {
693
+ const [, entity1, card1, card2, entity2, label] = relMatch;
694
+ for (const id of [entity1, entity2]) {
695
+ if (!nodeMap.has(id)) {
696
+ const node = { id, type: 'database', label: id };
697
+ nodeMap.set(id, node);
698
+ nodes.push(node);
699
+ }
700
+ }
701
+ const edge = {
702
+ id: `rel_${++edgeCounter}`,
703
+ source: entity1,
704
+ target: entity2,
705
+ type: 'relationship',
706
+ label,
707
+ properties: {
708
+ sourceCardinality: card1,
709
+ targetCardinality: card2,
710
+ },
711
+ };
712
+ edges.push(edge);
713
+ }
714
+ }
715
+ return finishParse([], {
716
+ version: '1.0.0',
717
+ id: options.graphId || 'mermaid-er',
718
+ name: options.graphName,
719
+ nodes,
720
+ edges,
721
+ metadata: { custom: { diagramType: 'er' } },
722
+ });
723
+ }
724
+ // ============================================================================
725
+ // Timeline Parser
726
+ // ============================================================================
727
+ function parseTimeline(lines, options) {
728
+ const nodes = [];
729
+ const edges = [];
730
+ let nodeCounter = 0;
731
+ let currentSection = null;
732
+ let previousPeriod = null;
733
+ for (let i = 1; i < lines.length; i++) {
734
+ const line = lines[i].trim();
735
+ if (!line || line.startsWith('%%'))
736
+ continue;
737
+ // Title
738
+ if (line.startsWith('title '))
739
+ continue;
740
+ // Section
741
+ const sectionMatch = line.match(/^section\s+(.+)$/);
742
+ if (sectionMatch) {
743
+ currentSection = {
744
+ id: `section_${++nodeCounter}`,
745
+ type: 'group',
746
+ label: sectionMatch[1],
747
+ children: [],
748
+ };
749
+ nodes.push(currentSection);
750
+ continue;
751
+ }
752
+ // Time period with events
753
+ const periodMatch = line.match(/^([^:]+)\s*:\s*(.+)$/);
754
+ if (periodMatch) {
755
+ const period = periodMatch[1].trim();
756
+ const events = periodMatch[2].split(':').map(e => e.trim());
757
+ const periodNode = {
758
+ id: `period_${++nodeCounter}`,
759
+ type: 'module',
760
+ label: period,
761
+ properties: { events },
762
+ };
763
+ if (currentSection) {
764
+ periodNode.parent = currentSection.id;
765
+ currentSection.children.push(periodNode);
766
+ }
767
+ else {
768
+ nodes.push(periodNode);
769
+ }
770
+ if (previousPeriod) {
771
+ edges.push({
772
+ id: `seq_${nodeCounter}`,
773
+ source: previousPeriod,
774
+ target: periodNode.id,
775
+ type: 'sequence',
776
+ });
777
+ }
778
+ previousPeriod = periodNode.id;
779
+ }
780
+ }
781
+ return finishParse([], {
782
+ version: '1.0.0',
783
+ id: options.graphId || 'mermaid-timeline',
784
+ name: options.graphName,
785
+ nodes,
786
+ edges,
787
+ metadata: { custom: { diagramType: 'timeline' } },
788
+ });
789
+ }
790
+ // ============================================================================
791
+ // Block Diagram Parser
792
+ // ============================================================================
793
+ function parseBlock(lines, options) {
794
+ const nodes = [];
795
+ const edges = [];
796
+ const nodeMap = new Map();
797
+ let edgeCounter = 0;
798
+ for (let i = 1; i < lines.length; i++) {
799
+ const line = lines[i].trim();
800
+ if (!line || line.startsWith('%%') || line.startsWith('columns'))
801
+ continue;
802
+ // Block with edge
803
+ const edgeMatch = line.match(/^(\w+)(?:\["([^"]+)"\])?\s*(-->|---)\s*(?:\|"([^"]+)"\|)?\s*(\w+)(?:\["([^"]+)"\])?$/);
804
+ if (edgeMatch) {
805
+ const [, id1, label1, arrow, edgeLabel, id2, label2] = edgeMatch;
806
+ for (const [id, label] of [[id1, label1], [id2, label2]]) {
807
+ if (!nodeMap.has(id)) {
808
+ const node = { id: id, type: 'service', label: (label || id) };
809
+ nodeMap.set(id, node);
810
+ nodes.push(node);
811
+ }
812
+ }
813
+ const edge = {
814
+ id: `edge_${++edgeCounter}`,
815
+ source: id1,
816
+ target: id2,
817
+ };
818
+ if (edgeLabel)
819
+ edge.label = edgeLabel;
820
+ if (arrow === '---')
821
+ edge.style = { targetArrow: 'none' };
822
+ edges.push(edge);
823
+ continue;
824
+ }
825
+ // Simple block
826
+ const blockMatch = line.match(/^(\w+)(?:\["([^"]+)"\])?(?::(\d+))?$/);
827
+ if (blockMatch) {
828
+ const [, id, label, span] = blockMatch;
829
+ if (!nodeMap.has(id)) {
830
+ const node = { id, type: 'service', label: label || id };
831
+ if (span)
832
+ node.properties = { columnSpan: parseInt(span) };
833
+ nodeMap.set(id, node);
834
+ nodes.push(node);
835
+ }
836
+ }
837
+ }
838
+ return finishParse([], {
839
+ version: '1.0.0',
840
+ id: options.graphId || 'mermaid-block',
841
+ name: options.graphName,
842
+ nodes,
843
+ edges,
844
+ metadata: { custom: { diagramType: 'block' } },
845
+ });
846
+ }
847
+ // ============================================================================
848
+ // Packet Diagram Parser
849
+ // ============================================================================
850
+ function parsePacket(lines, options) {
851
+ const nodes = [];
852
+ let fieldCounter = 0;
853
+ let currentBit = 0;
854
+ for (let i = 1; i < lines.length; i++) {
855
+ const line = lines[i].trim();
856
+ if (!line || line.startsWith('%%'))
857
+ continue;
858
+ // Range syntax: 0-15: "Header"
859
+ const rangeMatch = line.match(/^(\d+)-(\d+)\s*:\s*"([^"]+)"$/);
860
+ if (rangeMatch) {
861
+ const [, start, end, label] = rangeMatch;
862
+ const node = {
863
+ id: `field_${++fieldCounter}`,
864
+ type: 'module',
865
+ label,
866
+ properties: {
867
+ bitStart: parseInt(start),
868
+ bitEnd: parseInt(end),
869
+ bitWidth: parseInt(end) - parseInt(start) + 1,
870
+ },
871
+ };
872
+ nodes.push(node);
873
+ continue;
874
+ }
875
+ // Relative syntax: +8: "Field"
876
+ const relativeMatch = line.match(/^\+(\d+)\s*:\s*"([^"]+)"$/);
877
+ if (relativeMatch) {
878
+ const [, width, label] = relativeMatch;
879
+ const bitWidth = parseInt(width);
880
+ const node = {
881
+ id: `field_${++fieldCounter}`,
882
+ type: 'module',
883
+ label,
884
+ properties: {
885
+ bitStart: currentBit,
886
+ bitEnd: currentBit + bitWidth - 1,
887
+ bitWidth,
888
+ },
889
+ };
890
+ nodes.push(node);
891
+ currentBit += bitWidth;
892
+ }
893
+ }
894
+ return finishParse([], {
895
+ version: '1.0.0',
896
+ id: options.graphId || 'mermaid-packet',
897
+ name: options.graphName,
898
+ nodes,
899
+ edges: [],
900
+ metadata: { custom: { diagramType: 'packet' } },
901
+ });
902
+ }
903
+ // ============================================================================
904
+ // Kanban Parser
905
+ // ============================================================================
906
+ function parseKanban(lines, options) {
907
+ const nodes = [];
908
+ let currentColumn = null;
909
+ for (let i = 1; i < lines.length; i++) {
910
+ const line = lines[i];
911
+ const trimmed = line.trim();
912
+ if (!trimmed || trimmed.startsWith('%%'))
913
+ continue;
914
+ const indent = line.search(/\S/);
915
+ // Column (less indented than tasks)
916
+ const columnMatch = trimmed.match(/^(\w+)\[([^\]]+)\]$/);
917
+ if (columnMatch && indent <= 4) {
918
+ currentColumn = {
919
+ id: columnMatch[1],
920
+ type: 'group',
921
+ label: columnMatch[2],
922
+ children: [],
923
+ };
924
+ nodes.push(currentColumn);
925
+ continue;
926
+ }
927
+ // Task (more indented than column)
928
+ const taskMatch = trimmed.match(/^(\w+)\[([^\]]+)\](?:@\{([^}]+)\})?$/);
929
+ if (taskMatch && currentColumn && indent > 4) {
930
+ const [, id, label, metaStr] = taskMatch;
931
+ const task = {
932
+ id,
933
+ type: 'service',
934
+ label,
935
+ parent: currentColumn.id,
936
+ };
937
+ if (metaStr) {
938
+ const metadata = {};
939
+ const pairs = metaStr.split(',');
940
+ for (const pair of pairs) {
941
+ const [key, value] = pair.split(':').map(s => s.trim().replace(/"/g, ''));
942
+ if (key && value)
943
+ metadata[key] = value;
944
+ }
945
+ task.properties = { metadata };
946
+ }
947
+ currentColumn.children.push(task);
948
+ }
949
+ }
950
+ return finishParse([], {
951
+ version: '1.0.0',
952
+ id: options.graphId || 'mermaid-kanban',
953
+ name: options.graphName,
954
+ nodes,
955
+ edges: [],
956
+ metadata: { custom: { diagramType: 'kanban' } },
957
+ });
958
+ }
959
+ // ============================================================================
960
+ // Architecture Diagram Parser
961
+ // ============================================================================
962
+ function parseArchitecture(lines, options) {
963
+ const nodes = [];
964
+ const edges = [];
965
+ const nodeMap = new Map();
966
+ let edgeCounter = 0;
967
+ for (let i = 1; i < lines.length; i++) {
968
+ const line = lines[i].trim();
969
+ if (!line || line.startsWith('%%'))
970
+ continue;
971
+ // Group: group id(icon)[title] in parent
972
+ const groupMatch = line.match(/^group\s+(\w+)(?:\(([^)]*)\))?\[([^\]]+)\](?:\s+in\s+(\w+))?$/);
973
+ if (groupMatch) {
974
+ const [, id, icon, label, parentId] = groupMatch;
975
+ const node = { id, type: 'group', label, children: [] };
976
+ if (icon)
977
+ node.properties = { icon };
978
+ if (parentId)
979
+ node.parent = parentId;
980
+ nodeMap.set(id, node);
981
+ if (!parentId)
982
+ nodes.push(node);
983
+ else {
984
+ const parent = nodeMap.get(parentId);
985
+ if (parent?.children)
986
+ parent.children.push(node);
987
+ }
988
+ continue;
989
+ }
990
+ // Service: service id(icon)[title] in parent
991
+ const serviceMatch = line.match(/^service\s+(\w+)(?:\(([^)]*)\))?\[([^\]]+)\](?:\s+in\s+(\w+))?$/);
992
+ if (serviceMatch) {
993
+ const [, id, icon, label, parentId] = serviceMatch;
994
+ const node = { id, type: 'service', label };
995
+ if (icon)
996
+ node.properties = { icon };
997
+ if (parentId)
998
+ node.parent = parentId;
999
+ nodeMap.set(id, node);
1000
+ if (!parentId)
1001
+ nodes.push(node);
1002
+ else {
1003
+ const parent = nodeMap.get(parentId);
1004
+ if (parent?.children)
1005
+ parent.children.push(node);
1006
+ }
1007
+ continue;
1008
+ }
1009
+ // Junction: junction id in parent
1010
+ const junctionMatch = line.match(/^junction\s+(\w+)(?:\s+in\s+(\w+))?$/);
1011
+ if (junctionMatch) {
1012
+ const [, id, parentId] = junctionMatch;
1013
+ const node = { id, type: 'module', label: id };
1014
+ if (parentId)
1015
+ node.parent = parentId;
1016
+ nodeMap.set(id, node);
1017
+ if (!parentId)
1018
+ nodes.push(node);
1019
+ continue;
1020
+ }
1021
+ // Edge: id:T <--> B:id or id:L --> R:id
1022
+ const edgeMatch = line.match(/^(\w+)(?:\{(\w+)\})?:([TBLR])\s*(<)?--?(>)?\s*([TBLR]):(\w+)(?:\{(\w+)\})?$/);
1023
+ if (edgeMatch) {
1024
+ const [, src, srcGroup, srcSide, leftArrow, rightArrow, tgtSide, tgt, tgtGroup] = edgeMatch;
1025
+ const edge = {
1026
+ id: `edge_${++edgeCounter}`,
1027
+ source: src,
1028
+ target: tgt,
1029
+ properties: {
1030
+ sourceSide: srcSide,
1031
+ targetSide: tgtSide,
1032
+ bidirectional: !!leftArrow && !!rightArrow,
1033
+ },
1034
+ };
1035
+ if (srcGroup)
1036
+ edge.properties.sourceGroup = srcGroup;
1037
+ if (tgtGroup)
1038
+ edge.properties.targetGroup = tgtGroup;
1039
+ edges.push(edge);
1040
+ }
1041
+ }
1042
+ return finishParse([], {
1043
+ version: '1.0.0',
1044
+ id: options.graphId || 'mermaid-architecture',
1045
+ name: options.graphName,
1046
+ nodes,
1047
+ edges,
1048
+ metadata: { custom: { diagramType: 'architecture' } },
1049
+ });
1050
+ }
1051
+ //# sourceMappingURL=mermaid.js.map