@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.
- package/LICENSE +4 -0
- package/README.md +5 -0
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +59 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/formats/dot.d.ts +23 -0
- package/dist/formats/dot.d.ts.map +1 -0
- package/dist/formats/dot.js +541 -0
- package/dist/formats/dot.js.map +1 -0
- package/dist/formats/index.d.ts +6 -0
- package/dist/formats/index.d.ts.map +1 -0
- package/dist/formats/index.js +6 -0
- package/dist/formats/index.js.map +1 -0
- package/dist/formats/mermaid.d.ts +29 -0
- package/dist/formats/mermaid.d.ts.map +1 -0
- package/dist/formats/mermaid.js +1051 -0
- package/dist/formats/mermaid.js.map +1 -0
- package/dist/formats/plantuml.d.ts +20 -0
- package/dist/formats/plantuml.d.ts.map +1 -0
- package/dist/formats/plantuml.js +271 -0
- package/dist/formats/plantuml.js.map +1 -0
- package/dist/ids.d.ts +36 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +69 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/index.d.ts +13 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +415 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/printer/index.d.ts +47 -0
- package/dist/printer/index.d.ts.map +1 -0
- package/dist/printer/index.js +392 -0
- package/dist/printer/index.js.map +1 -0
- package/dist/strings.d.ts +17 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +48 -0
- package/dist/strings.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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
|