@statelyai/graph 0.1.0 → 0.3.0
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/README.md +65 -15
- package/dist/{adjacency-list-CXpOCibq.mjs → adjacency-list-A4_Eiwj3.mjs} +1 -1
- package/dist/{algorithms-R35X6ro4.mjs → algorithms-DBU7nmIV.mjs} +83 -2
- package/dist/algorithms.d.mts +10 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/converter-DnbeyE_p.mjs +33 -0
- package/dist/{edge-list-BRujEnnU.mjs → edge-list-DuHMz8hf.mjs} +1 -1
- package/dist/{adjacency-list-DW-lAUe8.d.mts → formats/adjacency-list/index.d.mts} +3 -3
- package/dist/formats/adjacency-list/index.mjs +3 -0
- package/dist/formats/converter/index.d.mts +27 -0
- package/dist/formats/converter/index.mjs +3 -0
- package/dist/formats/cytoscape/index.d.mts +35 -0
- package/dist/formats/cytoscape/index.mjs +87 -0
- package/dist/formats/d3/index.d.mts +22 -0
- package/dist/formats/d3/index.mjs +65 -0
- package/dist/formats/dot/index.d.mts +9 -0
- package/dist/formats/dot/index.mjs +235 -0
- package/dist/{edge-list-CJmfoNu2.d.mts → formats/edge-list/index.d.mts} +3 -3
- package/dist/formats/edge-list/index.mjs +3 -0
- package/dist/formats/gexf/index.d.mts +9 -0
- package/dist/formats/gexf/index.mjs +249 -0
- package/dist/formats/gml/index.d.mts +9 -0
- package/dist/formats/gml/index.mjs +236 -0
- package/dist/formats/graphml/index.d.mts +9 -0
- package/dist/{graphml-CUTNRXqd.mjs → formats/graphml/index.mjs} +18 -4
- package/dist/formats/jgf/index.d.mts +30 -0
- package/dist/formats/jgf/index.mjs +85 -0
- package/dist/formats/mermaid/index.d.mts +188 -0
- package/dist/formats/mermaid/index.mjs +1898 -0
- package/dist/formats/tgf/index.d.mts +9 -0
- package/dist/formats/tgf/index.mjs +67 -0
- package/dist/index.d.mts +14 -9
- package/dist/index.mjs +5 -9
- package/dist/queries.d.mts +78 -2
- package/dist/queries.mjs +121 -2
- package/dist/{types-XV3S5Jnh.d.mts → types-B6Tpeerk.d.mts} +36 -1
- package/package.json +43 -17
- package/dist/dot-BRtq3e3c.mjs +0 -59
- package/dist/dot-HmJeUMsj.d.mts +0 -6
- package/dist/formats/adjacency-list.d.mts +0 -2
- package/dist/formats/adjacency-list.mjs +0 -3
- package/dist/formats/dot.d.mts +0 -2
- package/dist/formats/dot.mjs +0 -3
- package/dist/formats/edge-list.d.mts +0 -2
- package/dist/formats/edge-list.mjs +0 -3
- package/dist/formats/graphml.d.mts +0 -2
- package/dist/formats/graphml.mjs +0 -3
- package/dist/graphml-CMjPzSfY.d.mts +0 -7
- /package/dist/{indexing-BHg1VhqN.mjs → indexing-BFFVMnjF.mjs} +0 -0
|
@@ -0,0 +1,1898 @@
|
|
|
1
|
+
import { n as createFormatConverter } from "../../converter-DnbeyE_p.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/formats/mermaid/shared.ts
|
|
4
|
+
const MERMAID_TO_DIRECTION = {
|
|
5
|
+
TB: "down",
|
|
6
|
+
TD: "down",
|
|
7
|
+
BT: "up",
|
|
8
|
+
LR: "right",
|
|
9
|
+
RL: "left"
|
|
10
|
+
};
|
|
11
|
+
const DIRECTION_TO_MERMAID = {
|
|
12
|
+
down: "TD",
|
|
13
|
+
up: "BT",
|
|
14
|
+
right: "LR",
|
|
15
|
+
left: "RL"
|
|
16
|
+
};
|
|
17
|
+
/** Escape a label for Mermaid output (quotes special chars). */
|
|
18
|
+
function escapeMermaidLabel(s) {
|
|
19
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "#quot;").replace(/;/g, "#59;").replace(/#(?!quot;|59;|35;)/g, "#35;");
|
|
20
|
+
}
|
|
21
|
+
/** Unescape a Mermaid label back to plain text. */
|
|
22
|
+
function unescapeMermaidLabel(s) {
|
|
23
|
+
return s.replace(/#quot;/g, "\"").replace(/#59;/g, ";").replace(/#35;/g, "#");
|
|
24
|
+
}
|
|
25
|
+
/** Generate a deterministic edge ID from source, target, and index. */
|
|
26
|
+
function generateEdgeId(sourceId, targetId, index) {
|
|
27
|
+
return `${sourceId}-${targetId}-${index}`;
|
|
28
|
+
}
|
|
29
|
+
/** Strip `%%` single-line comments from Mermaid input. */
|
|
30
|
+
function stripComments(input) {
|
|
31
|
+
return input.split("\n").map((line) => {
|
|
32
|
+
const idx = line.indexOf("%%");
|
|
33
|
+
if (idx === -1) return line;
|
|
34
|
+
if ((line.slice(0, idx).match(/"/g) || []).length % 2 !== 0) return line;
|
|
35
|
+
return line.slice(0, idx);
|
|
36
|
+
}).join("\n");
|
|
37
|
+
}
|
|
38
|
+
/** Strip `%%{init: ...}%%` directives and return them separately. */
|
|
39
|
+
function stripDirectives(input) {
|
|
40
|
+
const directives = {};
|
|
41
|
+
return {
|
|
42
|
+
directives,
|
|
43
|
+
cleaned: input.replace(/%%\{[\s]*init[\s]*:[\s]*(.*?)[\s]*\}%%/gs, (_match, json) => {
|
|
44
|
+
try {
|
|
45
|
+
Object.assign(directives, JSON.parse(`{${json}}`));
|
|
46
|
+
} catch {}
|
|
47
|
+
return "";
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Split input into non-empty trimmed lines, stripping comments and directives.
|
|
53
|
+
* Returns the cleaned lines and any extracted directives.
|
|
54
|
+
*/
|
|
55
|
+
function prepareLines(input) {
|
|
56
|
+
const { directives, cleaned } = stripDirectives(input);
|
|
57
|
+
return {
|
|
58
|
+
lines: stripComments(cleaned).split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== ""),
|
|
59
|
+
directives
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Validate input is a non-empty string, throw with prefix if not. */
|
|
63
|
+
function validateInput(input, prefix) {
|
|
64
|
+
if (typeof input !== "string") throw new Error(`${prefix}: expected a string`);
|
|
65
|
+
if (!input.trim()) throw new Error(`${prefix}: input is empty`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/formats/mermaid/sequence.ts
|
|
70
|
+
const ARROW_PATTERNS = [
|
|
71
|
+
["<<-->>", {
|
|
72
|
+
stroke: "dotted",
|
|
73
|
+
arrowType: "filled",
|
|
74
|
+
bidirectional: true
|
|
75
|
+
}],
|
|
76
|
+
["<<->>", {
|
|
77
|
+
stroke: "solid",
|
|
78
|
+
arrowType: "filled",
|
|
79
|
+
bidirectional: true
|
|
80
|
+
}],
|
|
81
|
+
["-->>", {
|
|
82
|
+
stroke: "dotted",
|
|
83
|
+
arrowType: "filled",
|
|
84
|
+
bidirectional: false
|
|
85
|
+
}],
|
|
86
|
+
["-->", {
|
|
87
|
+
stroke: "dotted",
|
|
88
|
+
arrowType: "open",
|
|
89
|
+
bidirectional: false
|
|
90
|
+
}],
|
|
91
|
+
["--x", {
|
|
92
|
+
stroke: "dotted",
|
|
93
|
+
arrowType: "cross",
|
|
94
|
+
bidirectional: false
|
|
95
|
+
}],
|
|
96
|
+
["--)", {
|
|
97
|
+
stroke: "dotted",
|
|
98
|
+
arrowType: "async",
|
|
99
|
+
bidirectional: false
|
|
100
|
+
}],
|
|
101
|
+
["->>", {
|
|
102
|
+
stroke: "solid",
|
|
103
|
+
arrowType: "filled",
|
|
104
|
+
bidirectional: false
|
|
105
|
+
}],
|
|
106
|
+
["->", {
|
|
107
|
+
stroke: "solid",
|
|
108
|
+
arrowType: "open",
|
|
109
|
+
bidirectional: false
|
|
110
|
+
}],
|
|
111
|
+
["-x", {
|
|
112
|
+
stroke: "solid",
|
|
113
|
+
arrowType: "cross",
|
|
114
|
+
bidirectional: false
|
|
115
|
+
}],
|
|
116
|
+
["-)", {
|
|
117
|
+
stroke: "solid",
|
|
118
|
+
arrowType: "async",
|
|
119
|
+
bidirectional: false
|
|
120
|
+
}]
|
|
121
|
+
];
|
|
122
|
+
function parseArrow(arrow) {
|
|
123
|
+
for (const [pattern, info] of ARROW_PATTERNS) if (arrow === pattern) return info;
|
|
124
|
+
}
|
|
125
|
+
const MESSAGE_RE = /^(\S+?)\s*(<<-->>|<<->>|-->>|-->|--x|--\)|->>|->|-x|-\))\s*(\S+?)\s*:\s*(.*)$/;
|
|
126
|
+
function fromMermaidSequence(input) {
|
|
127
|
+
validateInput(input, "Mermaid sequence");
|
|
128
|
+
const { lines } = prepareLines(input);
|
|
129
|
+
const header = lines[0]?.trim();
|
|
130
|
+
if (!header || !header.startsWith("sequenceDiagram")) throw new Error("Mermaid sequence: expected \"sequenceDiagram\" header");
|
|
131
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
132
|
+
const edges = [];
|
|
133
|
+
const blocks = [];
|
|
134
|
+
let autonumber = false;
|
|
135
|
+
let edgeCounter = 0;
|
|
136
|
+
let seqNum = 0;
|
|
137
|
+
const blockStack = [];
|
|
138
|
+
function ensureNode(id, actorType = "participant") {
|
|
139
|
+
if (!nodeMap.has(id)) nodeMap.set(id, {
|
|
140
|
+
type: "node",
|
|
141
|
+
id,
|
|
142
|
+
parentId: null,
|
|
143
|
+
initialNodeId: null,
|
|
144
|
+
label: id,
|
|
145
|
+
data: { actorType }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function addEdge(edge) {
|
|
149
|
+
edges.push(edge);
|
|
150
|
+
if (blockStack.length > 0) {
|
|
151
|
+
const top = blockStack[blockStack.length - 1];
|
|
152
|
+
if (top.branches && top.branches.length > 0) top.branches[top.branches.length - 1].edgeIds.push(edge.id);
|
|
153
|
+
else if (top.options && top.options.length > 0) top.options[top.options.length - 1].edgeIds.push(edge.id);
|
|
154
|
+
else top.edgeIds.push(edge.id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (let i = 1; i < lines.length; i++) {
|
|
158
|
+
const line = lines[i].trim();
|
|
159
|
+
if (!line) continue;
|
|
160
|
+
if (line === "autonumber") {
|
|
161
|
+
autonumber = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const participantMatch = line.match(/^(participant|actor)\s+(\S+?)(?:\s+as\s+(.+))?$/);
|
|
165
|
+
if (participantMatch) {
|
|
166
|
+
const actorType = participantMatch[1];
|
|
167
|
+
const id = participantMatch[2];
|
|
168
|
+
const alias = participantMatch[3]?.trim();
|
|
169
|
+
ensureNode(id, actorType);
|
|
170
|
+
const node = nodeMap.get(id);
|
|
171
|
+
node.data.actorType = actorType;
|
|
172
|
+
if (alias) {
|
|
173
|
+
node.data.alias = alias;
|
|
174
|
+
node.label = alias;
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const createMatch = line.match(/^create\s+(participant|actor)\s+(\S+?)(?:\s+as\s+(.+))?$/);
|
|
179
|
+
if (createMatch) {
|
|
180
|
+
const actorType = createMatch[1];
|
|
181
|
+
const id = createMatch[2];
|
|
182
|
+
const alias = createMatch[3]?.trim();
|
|
183
|
+
ensureNode(id, actorType);
|
|
184
|
+
const node = nodeMap.get(id);
|
|
185
|
+
node.data.created = true;
|
|
186
|
+
if (alias) {
|
|
187
|
+
node.data.alias = alias;
|
|
188
|
+
node.label = alias;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const destroyMatch = line.match(/^destroy\s+(\S+)$/);
|
|
193
|
+
if (destroyMatch) {
|
|
194
|
+
const id = destroyMatch[1];
|
|
195
|
+
ensureNode(id);
|
|
196
|
+
nodeMap.get(id).data.destroyed = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const activateMatch = line.match(/^(activate|deactivate)\s+(\S+)$/);
|
|
200
|
+
if (activateMatch) {
|
|
201
|
+
const kind = activateMatch[1] === "activate" ? "activation" : "deactivation";
|
|
202
|
+
const actorId = activateMatch[2];
|
|
203
|
+
ensureNode(actorId);
|
|
204
|
+
addEdge({
|
|
205
|
+
type: "edge",
|
|
206
|
+
id: generateEdgeId(actorId, actorId, edgeCounter++),
|
|
207
|
+
sourceId: actorId,
|
|
208
|
+
targetId: actorId,
|
|
209
|
+
label: "",
|
|
210
|
+
data: {
|
|
211
|
+
kind,
|
|
212
|
+
...autonumber && { sequenceNumber: ++seqNum }
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (/^(loop|alt|opt|par|critical|break)\s+/.test(line) || line.startsWith("rect ")) {
|
|
218
|
+
const spaceIdx = line.indexOf(" ");
|
|
219
|
+
const keyword = line.slice(0, spaceIdx);
|
|
220
|
+
const label = line.slice(spaceIdx + 1).trim();
|
|
221
|
+
if (keyword === "alt" || keyword === "par" || keyword === "critical") blockStack.push({
|
|
222
|
+
type: keyword,
|
|
223
|
+
label,
|
|
224
|
+
edgeIds: [],
|
|
225
|
+
branches: keyword === "par" ? [{
|
|
226
|
+
label,
|
|
227
|
+
edgeIds: []
|
|
228
|
+
}] : keyword === "alt" ? [{
|
|
229
|
+
label,
|
|
230
|
+
edgeIds: []
|
|
231
|
+
}] : void 0,
|
|
232
|
+
options: keyword === "critical" ? [] : void 0
|
|
233
|
+
});
|
|
234
|
+
else if (keyword === "rect") blockStack.push({
|
|
235
|
+
type: "rect",
|
|
236
|
+
label: "",
|
|
237
|
+
edgeIds: [],
|
|
238
|
+
color: label
|
|
239
|
+
});
|
|
240
|
+
else blockStack.push({
|
|
241
|
+
type: keyword,
|
|
242
|
+
label,
|
|
243
|
+
edgeIds: []
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (line.startsWith("else") || line === "else") {
|
|
248
|
+
if (blockStack.length > 0) {
|
|
249
|
+
const top = blockStack[blockStack.length - 1];
|
|
250
|
+
if (top.branches) {
|
|
251
|
+
const elseLabel = line.length > 4 ? line.slice(5).trim() : void 0;
|
|
252
|
+
top.branches.push({
|
|
253
|
+
label: elseLabel,
|
|
254
|
+
edgeIds: []
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (line.startsWith("and ")) {
|
|
261
|
+
if (blockStack.length > 0) {
|
|
262
|
+
const top = blockStack[blockStack.length - 1];
|
|
263
|
+
if (top.branches) top.branches.push({
|
|
264
|
+
label: line.slice(4).trim(),
|
|
265
|
+
edgeIds: []
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (line.startsWith("option ")) {
|
|
271
|
+
if (blockStack.length > 0) {
|
|
272
|
+
const top = blockStack[blockStack.length - 1];
|
|
273
|
+
if (top.options) top.options.push({
|
|
274
|
+
label: line.slice(7).trim(),
|
|
275
|
+
edgeIds: []
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (line === "end") {
|
|
281
|
+
if (blockStack.length > 0) {
|
|
282
|
+
const finished = blockStack.pop();
|
|
283
|
+
const block = buildBlock(finished);
|
|
284
|
+
if (block) {
|
|
285
|
+
if (blockStack.length > 0) blockStack[blockStack.length - 1].edgeIds.push(...finished.edgeIds, ...finished.branches?.flatMap((b) => b.edgeIds) ?? [], ...finished.options?.flatMap((o) => o.edgeIds) ?? []);
|
|
286
|
+
blocks.push(block);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (/^Note\s+(left|right|over)\s+/i.test(line)) continue;
|
|
292
|
+
const msgMatch = line.match(MESSAGE_RE);
|
|
293
|
+
if (msgMatch) {
|
|
294
|
+
let sourceId = msgMatch[1];
|
|
295
|
+
const arrowStr = msgMatch[2];
|
|
296
|
+
let targetId = msgMatch[3];
|
|
297
|
+
const messageText = msgMatch[4].trim();
|
|
298
|
+
let activationOnTarget = null;
|
|
299
|
+
if (targetId.startsWith("+")) {
|
|
300
|
+
activationOnTarget = "activation";
|
|
301
|
+
targetId = targetId.slice(1);
|
|
302
|
+
} else if (targetId.startsWith("-")) {
|
|
303
|
+
activationOnTarget = "deactivation";
|
|
304
|
+
targetId = targetId.slice(1);
|
|
305
|
+
}
|
|
306
|
+
let activationOnSource = null;
|
|
307
|
+
if (sourceId.endsWith("+")) {
|
|
308
|
+
activationOnSource = "activation";
|
|
309
|
+
sourceId = sourceId.slice(0, -1);
|
|
310
|
+
} else if (sourceId.endsWith("-")) {
|
|
311
|
+
activationOnSource = "deactivation";
|
|
312
|
+
sourceId = sourceId.slice(0, -1);
|
|
313
|
+
}
|
|
314
|
+
ensureNode(sourceId);
|
|
315
|
+
ensureNode(targetId);
|
|
316
|
+
const arrowInfo = parseArrow(arrowStr);
|
|
317
|
+
if (!arrowInfo) continue;
|
|
318
|
+
const edgeId = generateEdgeId(sourceId, targetId, edgeCounter++);
|
|
319
|
+
const data = {
|
|
320
|
+
kind: "message",
|
|
321
|
+
stroke: arrowInfo.stroke,
|
|
322
|
+
arrowType: arrowInfo.arrowType,
|
|
323
|
+
...arrowInfo.bidirectional && { bidirectional: true },
|
|
324
|
+
...autonumber && { sequenceNumber: ++seqNum }
|
|
325
|
+
};
|
|
326
|
+
addEdge({
|
|
327
|
+
type: "edge",
|
|
328
|
+
id: edgeId,
|
|
329
|
+
sourceId,
|
|
330
|
+
targetId,
|
|
331
|
+
label: unescapeMermaidLabel(messageText),
|
|
332
|
+
data
|
|
333
|
+
});
|
|
334
|
+
if (activationOnTarget) addEdge({
|
|
335
|
+
type: "edge",
|
|
336
|
+
id: generateEdgeId(targetId, targetId, edgeCounter++),
|
|
337
|
+
sourceId: targetId,
|
|
338
|
+
targetId,
|
|
339
|
+
label: "",
|
|
340
|
+
data: { kind: activationOnTarget }
|
|
341
|
+
});
|
|
342
|
+
if (activationOnSource) addEdge({
|
|
343
|
+
type: "edge",
|
|
344
|
+
id: generateEdgeId(sourceId, sourceId, edgeCounter++),
|
|
345
|
+
sourceId,
|
|
346
|
+
targetId: sourceId,
|
|
347
|
+
label: "",
|
|
348
|
+
data: { kind: activationOnSource }
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
id: "",
|
|
355
|
+
type: "directed",
|
|
356
|
+
initialNodeId: null,
|
|
357
|
+
nodes: Array.from(nodeMap.values()),
|
|
358
|
+
edges,
|
|
359
|
+
data: {
|
|
360
|
+
diagramType: "sequence",
|
|
361
|
+
...autonumber && { autonumber: true },
|
|
362
|
+
...blocks.length > 0 && { blocks }
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function buildBlock(raw) {
|
|
367
|
+
switch (raw.type) {
|
|
368
|
+
case "loop": return {
|
|
369
|
+
type: "loop",
|
|
370
|
+
label: raw.label,
|
|
371
|
+
edgeIds: raw.edgeIds
|
|
372
|
+
};
|
|
373
|
+
case "alt": return {
|
|
374
|
+
type: "alt",
|
|
375
|
+
label: raw.label,
|
|
376
|
+
branches: raw.branches ?? [{ edgeIds: raw.edgeIds }]
|
|
377
|
+
};
|
|
378
|
+
case "opt": return {
|
|
379
|
+
type: "opt",
|
|
380
|
+
label: raw.label,
|
|
381
|
+
edgeIds: raw.edgeIds
|
|
382
|
+
};
|
|
383
|
+
case "par": return {
|
|
384
|
+
type: "par",
|
|
385
|
+
branches: (raw.branches ?? []).map((b) => ({
|
|
386
|
+
label: b.label ?? "",
|
|
387
|
+
edgeIds: b.edgeIds
|
|
388
|
+
}))
|
|
389
|
+
};
|
|
390
|
+
case "critical": return {
|
|
391
|
+
type: "critical",
|
|
392
|
+
label: raw.label,
|
|
393
|
+
edgeIds: raw.edgeIds,
|
|
394
|
+
...raw.options && raw.options.length > 0 && { options: raw.options }
|
|
395
|
+
};
|
|
396
|
+
case "break": return {
|
|
397
|
+
type: "break",
|
|
398
|
+
label: raw.label,
|
|
399
|
+
edgeIds: raw.edgeIds
|
|
400
|
+
};
|
|
401
|
+
case "rect": return {
|
|
402
|
+
type: "rect",
|
|
403
|
+
color: raw.color ?? "",
|
|
404
|
+
edgeIds: raw.edgeIds
|
|
405
|
+
};
|
|
406
|
+
default: return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const ARROW_MAP = {
|
|
410
|
+
solid: {
|
|
411
|
+
open: "->",
|
|
412
|
+
filled: "->>",
|
|
413
|
+
cross: "-x",
|
|
414
|
+
async: "-)"
|
|
415
|
+
},
|
|
416
|
+
dotted: {
|
|
417
|
+
open: "-->",
|
|
418
|
+
filled: "-->>",
|
|
419
|
+
cross: "--x",
|
|
420
|
+
async: "--)"
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
function toMermaidSequence(graph) {
|
|
424
|
+
const lines = ["sequenceDiagram"];
|
|
425
|
+
if (graph.data?.autonumber) lines.push(" autonumber");
|
|
426
|
+
for (const node of graph.nodes) {
|
|
427
|
+
const d = node.data;
|
|
428
|
+
const keyword = d?.actorType === "actor" ? "actor" : "participant";
|
|
429
|
+
const alias = d?.alias ? ` as ${escapeMermaidLabel(d.alias)}` : "";
|
|
430
|
+
if (d?.created) lines.push(` create ${keyword} ${node.id}${alias}`);
|
|
431
|
+
else lines.push(` ${keyword} ${node.id}${alias}`);
|
|
432
|
+
}
|
|
433
|
+
for (const edge of graph.edges) {
|
|
434
|
+
const d = edge.data;
|
|
435
|
+
if (!d) continue;
|
|
436
|
+
if (d.kind === "activation") {
|
|
437
|
+
lines.push(` activate ${edge.sourceId}`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (d.kind === "deactivation") {
|
|
441
|
+
lines.push(` deactivate ${edge.sourceId}`);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const stroke = d.stroke ?? "solid";
|
|
445
|
+
const arrowType = d.arrowType ?? "filled";
|
|
446
|
+
let arrow;
|
|
447
|
+
if (d.bidirectional) arrow = stroke === "dotted" ? "<<-->>" : "<<->>";
|
|
448
|
+
else arrow = ARROW_MAP[stroke]?.[arrowType] ?? "->>";
|
|
449
|
+
const label = edge.label ? `: ${escapeMermaidLabel(edge.label)}` : ":";
|
|
450
|
+
lines.push(` ${edge.sourceId}${arrow}${edge.targetId}${label}`);
|
|
451
|
+
}
|
|
452
|
+
return lines.join("\n");
|
|
453
|
+
}
|
|
454
|
+
/** Bidirectional converter for Mermaid sequence diagram format. */
|
|
455
|
+
const mermaidSequenceConverter = createFormatConverter(toMermaidSequence, fromMermaidSequence);
|
|
456
|
+
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/formats/mermaid/flowchart.ts
|
|
459
|
+
const SHAPE_OPENERS = [
|
|
460
|
+
[
|
|
461
|
+
"(((",
|
|
462
|
+
")))",
|
|
463
|
+
"double-circle"
|
|
464
|
+
],
|
|
465
|
+
[
|
|
466
|
+
"((",
|
|
467
|
+
"))",
|
|
468
|
+
"circle"
|
|
469
|
+
],
|
|
470
|
+
[
|
|
471
|
+
"([",
|
|
472
|
+
"])",
|
|
473
|
+
"stadium"
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
"[(",
|
|
477
|
+
")]",
|
|
478
|
+
"cylinder"
|
|
479
|
+
],
|
|
480
|
+
[
|
|
481
|
+
"[[",
|
|
482
|
+
"]]",
|
|
483
|
+
"subroutine"
|
|
484
|
+
],
|
|
485
|
+
[
|
|
486
|
+
"{{",
|
|
487
|
+
"}}",
|
|
488
|
+
"hexagon"
|
|
489
|
+
],
|
|
490
|
+
[
|
|
491
|
+
"[/",
|
|
492
|
+
"/]",
|
|
493
|
+
"parallelogram"
|
|
494
|
+
],
|
|
495
|
+
[
|
|
496
|
+
"[\\",
|
|
497
|
+
"\\]",
|
|
498
|
+
"parallelogram-alt"
|
|
499
|
+
],
|
|
500
|
+
[
|
|
501
|
+
"[/",
|
|
502
|
+
"\\]",
|
|
503
|
+
"trapezoid"
|
|
504
|
+
],
|
|
505
|
+
[
|
|
506
|
+
"[\\",
|
|
507
|
+
"/]",
|
|
508
|
+
"trapezoid-alt"
|
|
509
|
+
],
|
|
510
|
+
[
|
|
511
|
+
"(",
|
|
512
|
+
")",
|
|
513
|
+
"rounded"
|
|
514
|
+
],
|
|
515
|
+
[
|
|
516
|
+
"{",
|
|
517
|
+
"}",
|
|
518
|
+
"diamond"
|
|
519
|
+
],
|
|
520
|
+
[
|
|
521
|
+
">",
|
|
522
|
+
"]",
|
|
523
|
+
"asymmetric"
|
|
524
|
+
],
|
|
525
|
+
[
|
|
526
|
+
"[",
|
|
527
|
+
"]",
|
|
528
|
+
"rectangle"
|
|
529
|
+
]
|
|
530
|
+
];
|
|
531
|
+
const SHAPE_TO_BRACKETS$1 = {};
|
|
532
|
+
for (const [opener, closer, name] of SHAPE_OPENERS) SHAPE_TO_BRACKETS$1[name] = [opener, closer];
|
|
533
|
+
function parseNodeDecl(text) {
|
|
534
|
+
for (const [opener, closer, shapeName] of SHAPE_OPENERS) {
|
|
535
|
+
const opIdx = text.indexOf(opener);
|
|
536
|
+
if (opIdx < 0) continue;
|
|
537
|
+
const id = text.slice(0, opIdx);
|
|
538
|
+
if (!id) continue;
|
|
539
|
+
if (!text.endsWith(closer)) continue;
|
|
540
|
+
return {
|
|
541
|
+
id,
|
|
542
|
+
label: text.slice(opIdx + opener.length, text.length - closer.length).trim(),
|
|
543
|
+
shape: shapeName
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (/^[a-zA-Z_][\w]*$/.test(text)) return {
|
|
547
|
+
id: text,
|
|
548
|
+
label: "",
|
|
549
|
+
shape: "rectangle"
|
|
550
|
+
};
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const EDGE_ARROWS = [
|
|
554
|
+
[/^<===>$/, {
|
|
555
|
+
stroke: "thick",
|
|
556
|
+
arrowType: "arrow",
|
|
557
|
+
endMarker: "arrow",
|
|
558
|
+
startMarker: "arrow",
|
|
559
|
+
bidirectional: true
|
|
560
|
+
}],
|
|
561
|
+
[/^<==>$/, {
|
|
562
|
+
stroke: "thick",
|
|
563
|
+
arrowType: "arrow",
|
|
564
|
+
endMarker: "arrow",
|
|
565
|
+
startMarker: "arrow",
|
|
566
|
+
bidirectional: true
|
|
567
|
+
}],
|
|
568
|
+
[/^<-.->$/, {
|
|
569
|
+
stroke: "dotted",
|
|
570
|
+
arrowType: "arrow",
|
|
571
|
+
endMarker: "arrow",
|
|
572
|
+
startMarker: "arrow",
|
|
573
|
+
bidirectional: true
|
|
574
|
+
}],
|
|
575
|
+
[/^<-->$/, {
|
|
576
|
+
stroke: "normal",
|
|
577
|
+
arrowType: "arrow",
|
|
578
|
+
endMarker: "arrow",
|
|
579
|
+
startMarker: "arrow",
|
|
580
|
+
bidirectional: true
|
|
581
|
+
}],
|
|
582
|
+
[/^===>$/, {
|
|
583
|
+
stroke: "thick",
|
|
584
|
+
arrowType: "arrow",
|
|
585
|
+
endMarker: "arrow",
|
|
586
|
+
bidirectional: false
|
|
587
|
+
}],
|
|
588
|
+
[/^==>$/, {
|
|
589
|
+
stroke: "thick",
|
|
590
|
+
arrowType: "arrow",
|
|
591
|
+
endMarker: "arrow",
|
|
592
|
+
bidirectional: false
|
|
593
|
+
}],
|
|
594
|
+
[/^===$/, {
|
|
595
|
+
stroke: "thick",
|
|
596
|
+
arrowType: "none",
|
|
597
|
+
endMarker: "arrow",
|
|
598
|
+
bidirectional: false
|
|
599
|
+
}],
|
|
600
|
+
[/^-\.->$/, {
|
|
601
|
+
stroke: "dotted",
|
|
602
|
+
arrowType: "arrow",
|
|
603
|
+
endMarker: "arrow",
|
|
604
|
+
bidirectional: false
|
|
605
|
+
}],
|
|
606
|
+
[/^\.-\.>$/, {
|
|
607
|
+
stroke: "dotted",
|
|
608
|
+
arrowType: "arrow",
|
|
609
|
+
endMarker: "arrow",
|
|
610
|
+
bidirectional: false
|
|
611
|
+
}],
|
|
612
|
+
[/^-\.-$/, {
|
|
613
|
+
stroke: "dotted",
|
|
614
|
+
arrowType: "none",
|
|
615
|
+
endMarker: "arrow",
|
|
616
|
+
bidirectional: false
|
|
617
|
+
}],
|
|
618
|
+
[/^---o$/, {
|
|
619
|
+
stroke: "normal",
|
|
620
|
+
arrowType: "arrow",
|
|
621
|
+
endMarker: "circle",
|
|
622
|
+
bidirectional: false
|
|
623
|
+
}],
|
|
624
|
+
[/^--o$/, {
|
|
625
|
+
stroke: "normal",
|
|
626
|
+
arrowType: "arrow",
|
|
627
|
+
endMarker: "circle",
|
|
628
|
+
bidirectional: false
|
|
629
|
+
}],
|
|
630
|
+
[/^---x$/, {
|
|
631
|
+
stroke: "normal",
|
|
632
|
+
arrowType: "arrow",
|
|
633
|
+
endMarker: "cross",
|
|
634
|
+
bidirectional: false
|
|
635
|
+
}],
|
|
636
|
+
[/^--x$/, {
|
|
637
|
+
stroke: "normal",
|
|
638
|
+
arrowType: "arrow",
|
|
639
|
+
endMarker: "cross",
|
|
640
|
+
bidirectional: false
|
|
641
|
+
}],
|
|
642
|
+
[/^--->$/, {
|
|
643
|
+
stroke: "normal",
|
|
644
|
+
arrowType: "arrow",
|
|
645
|
+
endMarker: "arrow",
|
|
646
|
+
bidirectional: false
|
|
647
|
+
}],
|
|
648
|
+
[/^-->$/, {
|
|
649
|
+
stroke: "normal",
|
|
650
|
+
arrowType: "arrow",
|
|
651
|
+
endMarker: "arrow",
|
|
652
|
+
bidirectional: false
|
|
653
|
+
}],
|
|
654
|
+
[/^---$/, {
|
|
655
|
+
stroke: "normal",
|
|
656
|
+
arrowType: "none",
|
|
657
|
+
endMarker: "arrow",
|
|
658
|
+
bidirectional: false
|
|
659
|
+
}],
|
|
660
|
+
[/^--$/, {
|
|
661
|
+
stroke: "normal",
|
|
662
|
+
arrowType: "none",
|
|
663
|
+
endMarker: "arrow",
|
|
664
|
+
bidirectional: false
|
|
665
|
+
}]
|
|
666
|
+
];
|
|
667
|
+
function findEdge(line) {
|
|
668
|
+
const pipeMatch = line.match(/^(.+?)\s*([-=.<>ox]+)\|([^|]*)\|\s*(.+)$/);
|
|
669
|
+
if (pipeMatch) {
|
|
670
|
+
const arrowStr = pipeMatch[2];
|
|
671
|
+
for (const [re, info] of EDGE_ARROWS) if (re.test(arrowStr)) return {
|
|
672
|
+
sourceText: pipeMatch[1].trim(),
|
|
673
|
+
targetText: pipeMatch[4].trim(),
|
|
674
|
+
info: {
|
|
675
|
+
...info,
|
|
676
|
+
label: pipeMatch[3].trim()
|
|
677
|
+
},
|
|
678
|
+
rest: ""
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const inlineMatch = line.match(/^(.+?)\s*(--)\s+([^-][^>]*?)\s*(-->)\s*(.+)$/);
|
|
682
|
+
if (inlineMatch) return {
|
|
683
|
+
sourceText: inlineMatch[1].trim(),
|
|
684
|
+
targetText: inlineMatch[5].trim(),
|
|
685
|
+
info: {
|
|
686
|
+
stroke: "normal",
|
|
687
|
+
arrowType: "arrow",
|
|
688
|
+
endMarker: "arrow",
|
|
689
|
+
bidirectional: false,
|
|
690
|
+
label: inlineMatch[3].trim()
|
|
691
|
+
},
|
|
692
|
+
rest: ""
|
|
693
|
+
};
|
|
694
|
+
for (const [re, info] of EDGE_ARROWS) {
|
|
695
|
+
const arrowSource = re.source.replace(/^\^/, "").replace(/\$$/, "");
|
|
696
|
+
const fullRe = /* @__PURE__ */ new RegExp(`^(.+?)\\s*(${arrowSource})\\s*(.+)$`);
|
|
697
|
+
const m = line.match(fullRe);
|
|
698
|
+
if (m) return {
|
|
699
|
+
sourceText: m[1].trim(),
|
|
700
|
+
targetText: m[3].trim(),
|
|
701
|
+
info: {
|
|
702
|
+
...info,
|
|
703
|
+
label: ""
|
|
704
|
+
},
|
|
705
|
+
rest: ""
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
function fromMermaidFlowchart(input) {
|
|
711
|
+
validateInput(input, "Mermaid flowchart");
|
|
712
|
+
const { lines } = prepareLines(input);
|
|
713
|
+
const headerMatch = (lines[0]?.trim())?.match(/^(graph|flowchart)\s+(TD|TB|BT|LR|RL)\s*$/);
|
|
714
|
+
if (!headerMatch) throw new Error("Mermaid flowchart: expected \"graph <direction>\" or \"flowchart <direction>\" header");
|
|
715
|
+
const direction = MERMAID_TO_DIRECTION[headerMatch[2]] ?? "down";
|
|
716
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
717
|
+
const edges = [];
|
|
718
|
+
const classDefs = {};
|
|
719
|
+
const classAssignments = {};
|
|
720
|
+
let edgeCounter = 0;
|
|
721
|
+
const parentStack = [null];
|
|
722
|
+
function ensureNode(id, label, shape) {
|
|
723
|
+
if (!nodeMap.has(id)) nodeMap.set(id, {
|
|
724
|
+
type: "node",
|
|
725
|
+
id,
|
|
726
|
+
parentId: parentStack[parentStack.length - 1],
|
|
727
|
+
initialNodeId: null,
|
|
728
|
+
label: label ?? "",
|
|
729
|
+
data: {},
|
|
730
|
+
...shape && shape !== "rectangle" && { shape }
|
|
731
|
+
});
|
|
732
|
+
const node = nodeMap.get(id);
|
|
733
|
+
if (label && !node.label) node.label = label;
|
|
734
|
+
if (shape && shape !== "rectangle" && !node.shape) node.shape = shape;
|
|
735
|
+
return node;
|
|
736
|
+
}
|
|
737
|
+
for (let i = 1; i < lines.length; i++) {
|
|
738
|
+
const line = lines[i].trim();
|
|
739
|
+
if (!line) continue;
|
|
740
|
+
const subgraphMatch = line.match(/^subgraph\s+(\S+?)(?:\s*\[(.+)\])?\s*$/);
|
|
741
|
+
if (subgraphMatch) {
|
|
742
|
+
const subId = subgraphMatch[1];
|
|
743
|
+
ensureNode(subId, subgraphMatch[2]?.trim() ?? subId);
|
|
744
|
+
parentStack.push(subId);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (/^direction\s+(TD|TB|BT|LR|RL)\s*$/.test(line)) continue;
|
|
748
|
+
if (line === "end") {
|
|
749
|
+
if (parentStack.length > 1) parentStack.pop();
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const classDefMatch = line.match(/^classDef\s+(\S+)\s+(.+)$/);
|
|
753
|
+
if (classDefMatch) {
|
|
754
|
+
const className = classDefMatch[1];
|
|
755
|
+
const propsStr = classDefMatch[2];
|
|
756
|
+
const props = {};
|
|
757
|
+
for (const pair of propsStr.split(",")) {
|
|
758
|
+
const [k, v] = pair.split(":").map((s) => s.trim());
|
|
759
|
+
if (k && v) props[k] = v;
|
|
760
|
+
}
|
|
761
|
+
classDefs[className] = props;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const classAssignMatch = line.match(/^class\s+(.+)\s+(\S+)\s*$/);
|
|
765
|
+
if (classAssignMatch) {
|
|
766
|
+
const nodeIds = classAssignMatch[1].split(",").map((s) => s.trim());
|
|
767
|
+
const className = classAssignMatch[2];
|
|
768
|
+
for (const nid of nodeIds) {
|
|
769
|
+
if (!classAssignments[nid]) classAssignments[nid] = [];
|
|
770
|
+
classAssignments[nid].push(className);
|
|
771
|
+
}
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const styleMatch = line.match(/^style\s+(\S+)\s+(.+)$/);
|
|
775
|
+
if (styleMatch) {
|
|
776
|
+
const nodeId = styleMatch[1];
|
|
777
|
+
const propsStr = styleMatch[2];
|
|
778
|
+
ensureNode(nodeId);
|
|
779
|
+
const node = nodeMap.get(nodeId);
|
|
780
|
+
const style = {};
|
|
781
|
+
for (const pair of propsStr.split(",")) {
|
|
782
|
+
const [k, v] = pair.split(":").map((s) => s.trim());
|
|
783
|
+
if (k && v) {
|
|
784
|
+
if (k === "fill") node.color = v;
|
|
785
|
+
style[k] = v;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (Object.keys(style).length > 0) node.style = {
|
|
789
|
+
...node.style ?? {},
|
|
790
|
+
...style
|
|
791
|
+
};
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (line.startsWith("linkStyle ")) continue;
|
|
795
|
+
const clickMatch = line.match(/^click\s+(\S+)\s+"([^"]*)"(?:\s+"([^"]*)")?\s*$/);
|
|
796
|
+
if (clickMatch) {
|
|
797
|
+
const nodeId = clickMatch[1];
|
|
798
|
+
ensureNode(nodeId);
|
|
799
|
+
const node = nodeMap.get(nodeId);
|
|
800
|
+
node.data.link = clickMatch[2];
|
|
801
|
+
if (clickMatch[3]) node.data.tooltip = clickMatch[3];
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
let remaining = line;
|
|
805
|
+
let foundEdge = false;
|
|
806
|
+
while (remaining) {
|
|
807
|
+
const edgeResult = findEdge(remaining);
|
|
808
|
+
if (!edgeResult) break;
|
|
809
|
+
foundEdge = true;
|
|
810
|
+
const sourceDecl = parseNodeDecl(edgeResult.sourceText);
|
|
811
|
+
if (sourceDecl) ensureNode(sourceDecl.id, sourceDecl.label, sourceDecl.shape);
|
|
812
|
+
const targetDecl = parseNodeDecl(edgeResult.targetText);
|
|
813
|
+
let targetId;
|
|
814
|
+
if (targetDecl) {
|
|
815
|
+
ensureNode(targetDecl.id, targetDecl.label, targetDecl.shape);
|
|
816
|
+
targetId = targetDecl.id;
|
|
817
|
+
remaining = "";
|
|
818
|
+
} else {
|
|
819
|
+
const nextEdge = findEdge(edgeResult.targetText);
|
|
820
|
+
if (nextEdge) {
|
|
821
|
+
const tDecl = parseNodeDecl(nextEdge.sourceText);
|
|
822
|
+
if (tDecl) {
|
|
823
|
+
ensureNode(tDecl.id, tDecl.label, tDecl.shape);
|
|
824
|
+
targetId = tDecl.id;
|
|
825
|
+
} else {
|
|
826
|
+
targetId = nextEdge.sourceText;
|
|
827
|
+
ensureNode(targetId);
|
|
828
|
+
}
|
|
829
|
+
remaining = edgeResult.targetText;
|
|
830
|
+
} else {
|
|
831
|
+
targetId = edgeResult.targetText;
|
|
832
|
+
ensureNode(targetId);
|
|
833
|
+
remaining = "";
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const sourceId = sourceDecl ? sourceDecl.id : edgeResult.sourceText;
|
|
837
|
+
ensureNode(sourceId);
|
|
838
|
+
const edgeId = generateEdgeId(sourceId, targetId, edgeCounter++);
|
|
839
|
+
const info = edgeResult.info;
|
|
840
|
+
edges.push({
|
|
841
|
+
type: "edge",
|
|
842
|
+
id: edgeId,
|
|
843
|
+
sourceId,
|
|
844
|
+
targetId,
|
|
845
|
+
label: info.label ? unescapeMermaidLabel(info.label) : "",
|
|
846
|
+
data: {
|
|
847
|
+
stroke: info.stroke,
|
|
848
|
+
arrowType: info.arrowType,
|
|
849
|
+
...info.endMarker !== "arrow" && { endMarker: info.endMarker },
|
|
850
|
+
...info.startMarker && { startMarker: info.startMarker },
|
|
851
|
+
...info.bidirectional && { bidirectional: true }
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
if (foundEdge) continue;
|
|
856
|
+
const nodeDecl = parseNodeDecl(line);
|
|
857
|
+
if (nodeDecl) {
|
|
858
|
+
ensureNode(nodeDecl.id, nodeDecl.label, nodeDecl.shape);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (line.includes(";")) {
|
|
862
|
+
const stmts = line.split(";").map((s) => s.trim()).filter(Boolean);
|
|
863
|
+
for (const stmt of stmts) {
|
|
864
|
+
const nd = parseNodeDecl(stmt);
|
|
865
|
+
if (nd) ensureNode(nd.id, nd.label, nd.shape);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
for (const [nodeId, classes] of Object.entries(classAssignments)) {
|
|
870
|
+
const node = nodeMap.get(nodeId);
|
|
871
|
+
if (node) {
|
|
872
|
+
node.data.classes = classes;
|
|
873
|
+
for (const cls of classes) {
|
|
874
|
+
const def = classDefs[cls];
|
|
875
|
+
if (def) {
|
|
876
|
+
if (def.fill) node.color = def.fill;
|
|
877
|
+
const style = {};
|
|
878
|
+
for (const [k, v] of Object.entries(def)) style[k] = v;
|
|
879
|
+
if (Object.keys(style).length > 0) node.style = {
|
|
880
|
+
...node.style ?? {},
|
|
881
|
+
...style
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
id: "",
|
|
889
|
+
type: "directed",
|
|
890
|
+
initialNodeId: null,
|
|
891
|
+
nodes: Array.from(nodeMap.values()),
|
|
892
|
+
edges,
|
|
893
|
+
data: {
|
|
894
|
+
diagramType: "flowchart",
|
|
895
|
+
...Object.keys(classDefs).length > 0 && { classDefs }
|
|
896
|
+
},
|
|
897
|
+
direction
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function toMermaidFlowchart(graph) {
|
|
901
|
+
const lines = [`flowchart ${DIRECTION_TO_MERMAID[graph.direction ?? "down"] ?? "TD"}`];
|
|
902
|
+
const gd = graph.data;
|
|
903
|
+
if (gd?.classDefs) for (const [name, props] of Object.entries(gd.classDefs)) {
|
|
904
|
+
const propsStr = Object.entries(props).map(([k, v]) => `${k}:${v}`).join(",");
|
|
905
|
+
lines.push(` classDef ${name} ${propsStr}`);
|
|
906
|
+
}
|
|
907
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const node of graph.nodes) {
|
|
909
|
+
const pid = node.parentId;
|
|
910
|
+
if (!childrenMap.has(pid)) childrenMap.set(pid, []);
|
|
911
|
+
childrenMap.get(pid).push(node);
|
|
912
|
+
}
|
|
913
|
+
const isParent = /* @__PURE__ */ new Set();
|
|
914
|
+
for (const node of graph.nodes) if (childrenMap.has(node.id)) isParent.add(node.id);
|
|
915
|
+
function writeNodes(parentId, indent) {
|
|
916
|
+
const children = childrenMap.get(parentId) ?? [];
|
|
917
|
+
for (const node of children) if (isParent.has(node.id)) {
|
|
918
|
+
const label = node.label ? `[${escapeMermaidLabel(node.label)}]` : "";
|
|
919
|
+
lines.push(`${indent}subgraph ${node.id}${label}`);
|
|
920
|
+
writeNodes(node.id, indent + " ");
|
|
921
|
+
lines.push(`${indent}end`);
|
|
922
|
+
} else {
|
|
923
|
+
const brackets = SHAPE_TO_BRACKETS$1[node.shape ?? "rectangle"] ?? ["[", "]"];
|
|
924
|
+
const label = node.label ? `${brackets[0]}${escapeMermaidLabel(node.label)}${brackets[1]}` : "";
|
|
925
|
+
lines.push(`${indent}${node.id}${label}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
writeNodes(null, " ");
|
|
929
|
+
for (const edge of graph.edges) {
|
|
930
|
+
const d = edge.data;
|
|
931
|
+
let arrow;
|
|
932
|
+
if (d?.bidirectional) arrow = d.stroke === "thick" ? "<==>" : d.stroke === "dotted" ? "<-.->" : "<-->";
|
|
933
|
+
else if (d?.stroke === "thick") arrow = d.arrowType === "none" ? "===" : "==>";
|
|
934
|
+
else if (d?.stroke === "dotted") arrow = d.arrowType === "none" ? "-.-" : "-.->";
|
|
935
|
+
else if (d?.endMarker === "circle") arrow = "--o";
|
|
936
|
+
else if (d?.endMarker === "cross") arrow = "--x";
|
|
937
|
+
else arrow = d?.arrowType === "none" ? "---" : "-->";
|
|
938
|
+
let labelStr = "";
|
|
939
|
+
if (edge.label) labelStr = `|${escapeMermaidLabel(edge.label)}|`;
|
|
940
|
+
lines.push(` ${edge.sourceId} ${arrow}${labelStr} ${edge.targetId}`);
|
|
941
|
+
}
|
|
942
|
+
const classAssignments = /* @__PURE__ */ new Map();
|
|
943
|
+
for (const node of graph.nodes) if (node.data?.classes) for (const cls of node.data.classes) {
|
|
944
|
+
if (!classAssignments.has(cls)) classAssignments.set(cls, []);
|
|
945
|
+
classAssignments.get(cls).push(node.id);
|
|
946
|
+
}
|
|
947
|
+
for (const [cls, nodeIds] of classAssignments) lines.push(` class ${nodeIds.join(",")} ${cls}`);
|
|
948
|
+
for (const node of graph.nodes) if (node.data?.link) {
|
|
949
|
+
const tip = node.data.tooltip ? ` "${escapeMermaidLabel(node.data.tooltip)}"` : "";
|
|
950
|
+
lines.push(` click ${node.id} "${escapeMermaidLabel(node.data.link)}"${tip}`);
|
|
951
|
+
}
|
|
952
|
+
return lines.join("\n");
|
|
953
|
+
}
|
|
954
|
+
/** Bidirectional converter for Mermaid flowchart format. */
|
|
955
|
+
const mermaidFlowchartConverter = createFormatConverter(toMermaidFlowchart, fromMermaidFlowchart);
|
|
956
|
+
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/formats/mermaid/state.ts
|
|
959
|
+
function fromMermaidState(input) {
|
|
960
|
+
validateInput(input, "Mermaid state");
|
|
961
|
+
const { lines } = prepareLines(input);
|
|
962
|
+
const header = lines[0]?.trim();
|
|
963
|
+
if (!header || !header.startsWith("stateDiagram-v2") && !header.startsWith("stateDiagram")) throw new Error("Mermaid state: expected \"stateDiagram\" or \"stateDiagram-v2\" header");
|
|
964
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
965
|
+
const edges = [];
|
|
966
|
+
let edgeCounter = 0;
|
|
967
|
+
let startCounter = 0;
|
|
968
|
+
let endCounter = 0;
|
|
969
|
+
const parentStack = [null];
|
|
970
|
+
function ensureNode(id) {
|
|
971
|
+
if (!nodeMap.has(id)) nodeMap.set(id, {
|
|
972
|
+
type: "node",
|
|
973
|
+
id,
|
|
974
|
+
parentId: parentStack[parentStack.length - 1],
|
|
975
|
+
initialNodeId: null,
|
|
976
|
+
label: id,
|
|
977
|
+
data: {}
|
|
978
|
+
});
|
|
979
|
+
return nodeMap.get(id);
|
|
980
|
+
}
|
|
981
|
+
function resolveStarNode(position) {
|
|
982
|
+
if (position === "source") {
|
|
983
|
+
const id = `[*]_start_${startCounter++}`;
|
|
984
|
+
const node = ensureNode(id);
|
|
985
|
+
node.label = "[*]";
|
|
986
|
+
node.data.isStart = true;
|
|
987
|
+
node.shape = "start";
|
|
988
|
+
return id;
|
|
989
|
+
} else {
|
|
990
|
+
const id = `[*]_end_${endCounter++}`;
|
|
991
|
+
const node = ensureNode(id);
|
|
992
|
+
node.label = "[*]";
|
|
993
|
+
node.data.isEnd = true;
|
|
994
|
+
node.shape = "end";
|
|
995
|
+
return id;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
for (let i = 1; i < lines.length; i++) {
|
|
999
|
+
const line = lines[i].trim();
|
|
1000
|
+
if (!line) continue;
|
|
1001
|
+
if (/^direction\s+(TD|TB|BT|LR|RL)\s*$/.test(line)) continue;
|
|
1002
|
+
const compositeMatch = line.match(/^state\s+(\S+)\s*\{?\s*$/);
|
|
1003
|
+
if (compositeMatch && line.includes("{")) {
|
|
1004
|
+
const stateId = compositeMatch[1];
|
|
1005
|
+
ensureNode(stateId);
|
|
1006
|
+
parentStack.push(stateId);
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
const stereotypeMatch = line.match(/^state\s+(\S+)\s+<<(choice|fork|join)>>\s*$/);
|
|
1010
|
+
if (stereotypeMatch) {
|
|
1011
|
+
const stateId = stereotypeMatch[1];
|
|
1012
|
+
const stateType = stereotypeMatch[2];
|
|
1013
|
+
const node = ensureNode(stateId);
|
|
1014
|
+
node.data.stateType = stateType;
|
|
1015
|
+
node.shape = stateType;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const stateAsMatch = line.match(/^state\s+"([^"]+)"\s+as\s+(\S+)\s*$/);
|
|
1019
|
+
if (stateAsMatch) {
|
|
1020
|
+
const description = stateAsMatch[1];
|
|
1021
|
+
const stateId = stateAsMatch[2];
|
|
1022
|
+
const node = ensureNode(stateId);
|
|
1023
|
+
node.data.description = description;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (line === "}" || line === "end") {
|
|
1027
|
+
if (parentStack.length > 1) parentStack.pop();
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const noteMatch = line.match(/^note\s+(left|right)\s+of\s+(\S+)\s*:\s*(.+)$/i);
|
|
1031
|
+
if (noteMatch) {
|
|
1032
|
+
const position = noteMatch[1].toLowerCase();
|
|
1033
|
+
const stateId = noteMatch[2];
|
|
1034
|
+
const text = noteMatch[3].trim();
|
|
1035
|
+
const node = ensureNode(stateId);
|
|
1036
|
+
if (!node.data.notes) node.data.notes = [];
|
|
1037
|
+
node.data.notes.push({
|
|
1038
|
+
position,
|
|
1039
|
+
text
|
|
1040
|
+
});
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const transMatch = line.match(/^(\S+)\s*-->\s*(\S+)\s*(?::\s*(.+))?$/);
|
|
1044
|
+
if (transMatch) {
|
|
1045
|
+
let sourceId = transMatch[1];
|
|
1046
|
+
let targetId = transMatch[2];
|
|
1047
|
+
const label = transMatch[3]?.trim() ?? "";
|
|
1048
|
+
if (sourceId === "[*]") sourceId = resolveStarNode("source");
|
|
1049
|
+
else ensureNode(sourceId);
|
|
1050
|
+
if (targetId === "[*]") targetId = resolveStarNode("target");
|
|
1051
|
+
else ensureNode(targetId);
|
|
1052
|
+
const edgeId = generateEdgeId(sourceId, targetId, edgeCounter++);
|
|
1053
|
+
edges.push({
|
|
1054
|
+
type: "edge",
|
|
1055
|
+
id: edgeId,
|
|
1056
|
+
sourceId,
|
|
1057
|
+
targetId,
|
|
1058
|
+
label: label ? unescapeMermaidLabel(label) : "",
|
|
1059
|
+
data: {}
|
|
1060
|
+
});
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const descMatch = line.match(/^(\S+)\s*:\s*(.+)$/);
|
|
1064
|
+
if (descMatch) {
|
|
1065
|
+
const stateId = descMatch[1];
|
|
1066
|
+
const description = descMatch[2].trim();
|
|
1067
|
+
const node = ensureNode(stateId);
|
|
1068
|
+
node.data.description = description;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (/^[a-zA-Z_][\w]*$/.test(line)) {
|
|
1072
|
+
ensureNode(line);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
id: "",
|
|
1078
|
+
type: "directed",
|
|
1079
|
+
initialNodeId: null,
|
|
1080
|
+
nodes: Array.from(nodeMap.values()),
|
|
1081
|
+
edges,
|
|
1082
|
+
data: { diagramType: "stateDiagram" }
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function toMermaidState(graph) {
|
|
1086
|
+
const lines = ["stateDiagram-v2"];
|
|
1087
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
1088
|
+
for (const node of graph.nodes) {
|
|
1089
|
+
const pid = node.parentId;
|
|
1090
|
+
if (!childrenMap.has(pid)) childrenMap.set(pid, []);
|
|
1091
|
+
childrenMap.get(pid).push(node);
|
|
1092
|
+
}
|
|
1093
|
+
const isParent = /* @__PURE__ */ new Set();
|
|
1094
|
+
for (const node of graph.nodes) if (childrenMap.has(node.id)) isParent.add(node.id);
|
|
1095
|
+
function writeNodes(parentId, indent) {
|
|
1096
|
+
const children = childrenMap.get(parentId) ?? [];
|
|
1097
|
+
for (const node of children) {
|
|
1098
|
+
if (node.data?.isStart || node.data?.isEnd) continue;
|
|
1099
|
+
if (node.data?.stateType) lines.push(`${indent}state ${node.id} <<${node.data.stateType}>>`);
|
|
1100
|
+
if (node.data?.description) lines.push(`${indent}state "${escapeMermaidLabel(node.data.description)}" as ${node.id}`);
|
|
1101
|
+
if (isParent.has(node.id)) {
|
|
1102
|
+
lines.push(`${indent}state ${node.id} {`);
|
|
1103
|
+
writeNodes(node.id, indent + " ");
|
|
1104
|
+
lines.push(`${indent}}`);
|
|
1105
|
+
}
|
|
1106
|
+
if (node.data?.notes) for (const note of node.data.notes) lines.push(`${indent}note ${note.position} of ${node.id} : ${escapeMermaidLabel(note.text)}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
writeNodes(null, " ");
|
|
1110
|
+
for (const edge of graph.edges) {
|
|
1111
|
+
let sourceId = edge.sourceId;
|
|
1112
|
+
let targetId = edge.targetId;
|
|
1113
|
+
const sourceNode = graph.nodes.find((n) => n.id === sourceId);
|
|
1114
|
+
const targetNode = graph.nodes.find((n) => n.id === targetId);
|
|
1115
|
+
if (sourceNode?.data?.isStart) sourceId = "[*]";
|
|
1116
|
+
if (targetNode?.data?.isEnd) targetId = "[*]";
|
|
1117
|
+
const label = edge.label ? ` : ${escapeMermaidLabel(edge.label)}` : "";
|
|
1118
|
+
lines.push(` ${sourceId} --> ${targetId}${label}`);
|
|
1119
|
+
}
|
|
1120
|
+
return lines.join("\n");
|
|
1121
|
+
}
|
|
1122
|
+
/** Bidirectional converter for Mermaid state diagram format. */
|
|
1123
|
+
const mermaidStateConverter = createFormatConverter(toMermaidState, fromMermaidState);
|
|
1124
|
+
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/formats/mermaid/class-diagram.ts
|
|
1127
|
+
const RELATIONSHIP_ARROWS = [
|
|
1128
|
+
[
|
|
1129
|
+
"<|--",
|
|
1130
|
+
"inheritance",
|
|
1131
|
+
true
|
|
1132
|
+
],
|
|
1133
|
+
[
|
|
1134
|
+
"--|>",
|
|
1135
|
+
"inheritance",
|
|
1136
|
+
false
|
|
1137
|
+
],
|
|
1138
|
+
[
|
|
1139
|
+
"<|..",
|
|
1140
|
+
"realization",
|
|
1141
|
+
true
|
|
1142
|
+
],
|
|
1143
|
+
[
|
|
1144
|
+
"..|>",
|
|
1145
|
+
"realization",
|
|
1146
|
+
false
|
|
1147
|
+
],
|
|
1148
|
+
[
|
|
1149
|
+
"*--",
|
|
1150
|
+
"composition",
|
|
1151
|
+
true
|
|
1152
|
+
],
|
|
1153
|
+
[
|
|
1154
|
+
"--*",
|
|
1155
|
+
"composition",
|
|
1156
|
+
false
|
|
1157
|
+
],
|
|
1158
|
+
[
|
|
1159
|
+
"o--",
|
|
1160
|
+
"aggregation",
|
|
1161
|
+
true
|
|
1162
|
+
],
|
|
1163
|
+
[
|
|
1164
|
+
"--o",
|
|
1165
|
+
"aggregation",
|
|
1166
|
+
false
|
|
1167
|
+
],
|
|
1168
|
+
[
|
|
1169
|
+
"<--",
|
|
1170
|
+
"association",
|
|
1171
|
+
true
|
|
1172
|
+
],
|
|
1173
|
+
[
|
|
1174
|
+
"-->",
|
|
1175
|
+
"association",
|
|
1176
|
+
false
|
|
1177
|
+
],
|
|
1178
|
+
[
|
|
1179
|
+
"<..",
|
|
1180
|
+
"dependency",
|
|
1181
|
+
true
|
|
1182
|
+
],
|
|
1183
|
+
[
|
|
1184
|
+
"..>",
|
|
1185
|
+
"dependency",
|
|
1186
|
+
false
|
|
1187
|
+
],
|
|
1188
|
+
[
|
|
1189
|
+
"--",
|
|
1190
|
+
"link",
|
|
1191
|
+
false
|
|
1192
|
+
],
|
|
1193
|
+
[
|
|
1194
|
+
"..",
|
|
1195
|
+
"dashed",
|
|
1196
|
+
false
|
|
1197
|
+
]
|
|
1198
|
+
];
|
|
1199
|
+
function parseRelationship(line) {
|
|
1200
|
+
for (const [arrow, relationType, reversed] of RELATIONSHIP_ARROWS) {
|
|
1201
|
+
const idx = line.indexOf(arrow);
|
|
1202
|
+
if (idx < 0) continue;
|
|
1203
|
+
let left = line.slice(0, idx).trim();
|
|
1204
|
+
let right = line.slice(idx + arrow.length).trim();
|
|
1205
|
+
let label = "";
|
|
1206
|
+
const colonIdx = right.indexOf(":");
|
|
1207
|
+
if (colonIdx >= 0) {
|
|
1208
|
+
label = right.slice(colonIdx + 1).trim();
|
|
1209
|
+
right = right.slice(0, colonIdx).trim();
|
|
1210
|
+
}
|
|
1211
|
+
let leftCard;
|
|
1212
|
+
let rightCard;
|
|
1213
|
+
const leftCardMatch = left.match(/^"([^"]+)"\s+(.+)$/);
|
|
1214
|
+
if (leftCardMatch) {
|
|
1215
|
+
leftCard = leftCardMatch[1];
|
|
1216
|
+
left = leftCardMatch[2].trim();
|
|
1217
|
+
} else {
|
|
1218
|
+
const leftCardTrail = left.match(/^(.+?)\s+"([^"]+)"$/);
|
|
1219
|
+
if (leftCardTrail) {
|
|
1220
|
+
leftCard = leftCardTrail[2];
|
|
1221
|
+
left = leftCardTrail[1].trim();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const rightCardMatch = right.match(/^"([^"]+)"\s+(.+)$/);
|
|
1225
|
+
if (rightCardMatch) {
|
|
1226
|
+
rightCard = rightCardMatch[1];
|
|
1227
|
+
right = rightCardMatch[2].trim();
|
|
1228
|
+
} else {
|
|
1229
|
+
const rightCardTrail = right.match(/^(.+?)\s+"([^"]+)"$/);
|
|
1230
|
+
if (rightCardTrail) {
|
|
1231
|
+
rightCard = rightCardTrail[2];
|
|
1232
|
+
right = rightCardTrail[1].trim();
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (!left || !right) continue;
|
|
1236
|
+
if (reversed) return {
|
|
1237
|
+
leftClass: right,
|
|
1238
|
+
rightClass: left,
|
|
1239
|
+
relationType,
|
|
1240
|
+
label,
|
|
1241
|
+
leftCard: rightCard,
|
|
1242
|
+
rightCard: leftCard
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
leftClass: left,
|
|
1246
|
+
rightClass: right,
|
|
1247
|
+
relationType,
|
|
1248
|
+
label,
|
|
1249
|
+
leftCard,
|
|
1250
|
+
rightCard
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
function parseMember(line) {
|
|
1256
|
+
const trimmed = line.trim();
|
|
1257
|
+
let visibility = "+";
|
|
1258
|
+
let rest = trimmed;
|
|
1259
|
+
if (/^[+\-#~]/.test(rest)) {
|
|
1260
|
+
visibility = rest[0];
|
|
1261
|
+
rest = rest.slice(1).trim();
|
|
1262
|
+
}
|
|
1263
|
+
const isMethod = rest.includes("(");
|
|
1264
|
+
let name = rest;
|
|
1265
|
+
let type;
|
|
1266
|
+
if (isMethod) {
|
|
1267
|
+
const methodMatch = rest.match(/^(.+\))\s*(.+)?$/);
|
|
1268
|
+
if (methodMatch) {
|
|
1269
|
+
name = methodMatch[1];
|
|
1270
|
+
type = methodMatch[2];
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
const fieldMatch = rest.match(/^(\S+)\s+(\S+)$/);
|
|
1274
|
+
if (fieldMatch) {
|
|
1275
|
+
type = fieldMatch[1];
|
|
1276
|
+
name = fieldMatch[2];
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return {
|
|
1280
|
+
visibility,
|
|
1281
|
+
name,
|
|
1282
|
+
type,
|
|
1283
|
+
isMethod
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function fromMermaidClass(input) {
|
|
1287
|
+
validateInput(input, "Mermaid class");
|
|
1288
|
+
const { lines } = prepareLines(input);
|
|
1289
|
+
const header = lines[0]?.trim();
|
|
1290
|
+
if (!header || !header.startsWith("classDiagram")) throw new Error("Mermaid class: expected \"classDiagram\" header");
|
|
1291
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1292
|
+
const edges = [];
|
|
1293
|
+
let edgeCounter = 0;
|
|
1294
|
+
function ensureNode(id) {
|
|
1295
|
+
if (!nodeMap.has(id)) nodeMap.set(id, {
|
|
1296
|
+
type: "node",
|
|
1297
|
+
id,
|
|
1298
|
+
parentId: null,
|
|
1299
|
+
initialNodeId: null,
|
|
1300
|
+
label: id,
|
|
1301
|
+
data: {}
|
|
1302
|
+
});
|
|
1303
|
+
return nodeMap.get(id);
|
|
1304
|
+
}
|
|
1305
|
+
let currentClass = null;
|
|
1306
|
+
let inClassBlock = false;
|
|
1307
|
+
let braceDepth = 0;
|
|
1308
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1309
|
+
const line = lines[i].trim();
|
|
1310
|
+
if (!line) continue;
|
|
1311
|
+
const classBlockMatch = line.match(/^class\s+(\S+?)(?:~(.+?)~)?\s*\{\s*$/);
|
|
1312
|
+
if (classBlockMatch) {
|
|
1313
|
+
currentClass = classBlockMatch[1];
|
|
1314
|
+
const node = ensureNode(currentClass);
|
|
1315
|
+
if (classBlockMatch[2]) node.data.genericType = classBlockMatch[2];
|
|
1316
|
+
inClassBlock = true;
|
|
1317
|
+
braceDepth = 1;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (inClassBlock && currentClass) {
|
|
1321
|
+
if (line === "}") {
|
|
1322
|
+
braceDepth--;
|
|
1323
|
+
if (braceDepth <= 0) {
|
|
1324
|
+
inClassBlock = false;
|
|
1325
|
+
currentClass = null;
|
|
1326
|
+
}
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const annotMatch = line.match(/^<<(.+)>>$/);
|
|
1330
|
+
if (annotMatch) {
|
|
1331
|
+
nodeMap.get(currentClass).data.annotation = annotMatch[1];
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
const node = nodeMap.get(currentClass);
|
|
1335
|
+
if (!node.data.members) node.data.members = [];
|
|
1336
|
+
node.data.members.push(parseMember(line));
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const classInlineMatch = line.match(/^class\s+(\S+?)(?:~(.+?)~)?\s*$/);
|
|
1340
|
+
if (classInlineMatch) {
|
|
1341
|
+
const node = ensureNode(classInlineMatch[1]);
|
|
1342
|
+
if (classInlineMatch[2]) node.data.genericType = classInlineMatch[2];
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
const annotLineMatch = line.match(/^<<(.+)>>\s+(\S+)\s*$/);
|
|
1346
|
+
if (annotLineMatch) {
|
|
1347
|
+
const node = ensureNode(annotLineMatch[2]);
|
|
1348
|
+
node.data.annotation = annotLineMatch[1];
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
const inlineMemberMatch = line.match(/^(\S+)\s*:\s*(.+)$/);
|
|
1352
|
+
if (inlineMemberMatch) {
|
|
1353
|
+
if (!parseRelationship(line)) {
|
|
1354
|
+
const node = ensureNode(inlineMemberMatch[1]);
|
|
1355
|
+
if (!node.data.members) node.data.members = [];
|
|
1356
|
+
node.data.members.push(parseMember(inlineMemberMatch[2]));
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const rel = parseRelationship(line);
|
|
1361
|
+
if (rel) {
|
|
1362
|
+
ensureNode(rel.leftClass);
|
|
1363
|
+
ensureNode(rel.rightClass);
|
|
1364
|
+
const edgeId = generateEdgeId(rel.leftClass, rel.rightClass, edgeCounter++);
|
|
1365
|
+
edges.push({
|
|
1366
|
+
type: "edge",
|
|
1367
|
+
id: edgeId,
|
|
1368
|
+
sourceId: rel.leftClass,
|
|
1369
|
+
targetId: rel.rightClass,
|
|
1370
|
+
label: rel.label ? unescapeMermaidLabel(rel.label) : "",
|
|
1371
|
+
data: {
|
|
1372
|
+
relationType: rel.relationType,
|
|
1373
|
+
...rel.leftCard && { sourceCardinality: rel.leftCard },
|
|
1374
|
+
...rel.rightCard && { targetCardinality: rel.rightCard }
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
id: "",
|
|
1382
|
+
type: "directed",
|
|
1383
|
+
initialNodeId: null,
|
|
1384
|
+
nodes: Array.from(nodeMap.values()),
|
|
1385
|
+
edges,
|
|
1386
|
+
data: { diagramType: "classDiagram" }
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
const RELATION_TO_ARROW = {
|
|
1390
|
+
inheritance: "--|>",
|
|
1391
|
+
composition: "--*",
|
|
1392
|
+
aggregation: "--o",
|
|
1393
|
+
association: "-->",
|
|
1394
|
+
dependency: "..>",
|
|
1395
|
+
realization: "..|>",
|
|
1396
|
+
link: "--",
|
|
1397
|
+
dashed: ".."
|
|
1398
|
+
};
|
|
1399
|
+
const VISIBILITY_SYMBOLS = {
|
|
1400
|
+
"+": "+",
|
|
1401
|
+
"-": "-",
|
|
1402
|
+
"#": "#",
|
|
1403
|
+
"~": "~"
|
|
1404
|
+
};
|
|
1405
|
+
function toMermaidClass(graph) {
|
|
1406
|
+
const lines = ["classDiagram"];
|
|
1407
|
+
for (const node of graph.nodes) if (node.data?.members && node.data.members.length > 0) {
|
|
1408
|
+
const generic = node.data.genericType ? `~${node.data.genericType}~` : "";
|
|
1409
|
+
lines.push(` class ${node.id}${generic} {`);
|
|
1410
|
+
if (node.data.annotation) lines.push(` <<${node.data.annotation}>>`);
|
|
1411
|
+
for (const m of node.data.members) {
|
|
1412
|
+
const vis = VISIBILITY_SYMBOLS[m.visibility] ?? "+";
|
|
1413
|
+
const typeStr = m.type ? m.isMethod ? ` ${m.type}` : `${m.type} ` : "";
|
|
1414
|
+
if (m.isMethod) lines.push(` ${vis}${m.name}${typeStr}`);
|
|
1415
|
+
else lines.push(` ${vis}${typeStr}${m.name}`);
|
|
1416
|
+
}
|
|
1417
|
+
lines.push(` }`);
|
|
1418
|
+
} else {
|
|
1419
|
+
const generic = node.data?.genericType ? `~${node.data.genericType}~` : "";
|
|
1420
|
+
lines.push(` class ${node.id}${generic}`);
|
|
1421
|
+
if (node.data?.annotation) lines.push(` <<${node.data.annotation}>> ${node.id}`);
|
|
1422
|
+
}
|
|
1423
|
+
for (const edge of graph.edges) {
|
|
1424
|
+
const arrow = RELATION_TO_ARROW[edge.data?.relationType ?? "association"] ?? "-->";
|
|
1425
|
+
const srcCard = edge.data?.sourceCardinality ? `"${edge.data.sourceCardinality}" ` : "";
|
|
1426
|
+
const tgtCard = edge.data?.targetCardinality ? ` "${edge.data.targetCardinality}"` : "";
|
|
1427
|
+
const label = edge.label ? ` : ${escapeMermaidLabel(edge.label)}` : "";
|
|
1428
|
+
lines.push(` ${srcCard}${edge.sourceId} ${arrow} ${edge.targetId}${tgtCard}${label}`);
|
|
1429
|
+
}
|
|
1430
|
+
return lines.join("\n");
|
|
1431
|
+
}
|
|
1432
|
+
/** Bidirectional converter for Mermaid class diagram format. */
|
|
1433
|
+
const mermaidClassConverter = createFormatConverter(toMermaidClass, fromMermaidClass);
|
|
1434
|
+
|
|
1435
|
+
//#endregion
|
|
1436
|
+
//#region src/formats/mermaid/er-diagram.ts
|
|
1437
|
+
const LEFT_CARDINALITY = {
|
|
1438
|
+
"||": "one",
|
|
1439
|
+
"|o": "zero-or-one",
|
|
1440
|
+
"}|": "one-or-more",
|
|
1441
|
+
"}o": "zero-or-more"
|
|
1442
|
+
};
|
|
1443
|
+
const RIGHT_CARDINALITY = {
|
|
1444
|
+
"||": "one",
|
|
1445
|
+
"o|": "zero-or-one",
|
|
1446
|
+
"|{": "one-or-more",
|
|
1447
|
+
"o{": "zero-or-more"
|
|
1448
|
+
};
|
|
1449
|
+
const CARDINALITY_TO_LEFT = {
|
|
1450
|
+
one: "||",
|
|
1451
|
+
"zero-or-one": "|o",
|
|
1452
|
+
"one-or-more": "}|",
|
|
1453
|
+
"zero-or-more": "}o"
|
|
1454
|
+
};
|
|
1455
|
+
const CARDINALITY_TO_RIGHT = {
|
|
1456
|
+
one: "||",
|
|
1457
|
+
"zero-or-one": "o|",
|
|
1458
|
+
"one-or-more": "|{",
|
|
1459
|
+
"zero-or-more": "o{"
|
|
1460
|
+
};
|
|
1461
|
+
function parseERRelationship(symbol) {
|
|
1462
|
+
if (symbol.length < 6) return null;
|
|
1463
|
+
const left = symbol.slice(0, 2);
|
|
1464
|
+
const mid = symbol.slice(2, 4);
|
|
1465
|
+
const right = symbol.slice(4, 6);
|
|
1466
|
+
const srcCard = LEFT_CARDINALITY[left];
|
|
1467
|
+
const tgtCard = RIGHT_CARDINALITY[right];
|
|
1468
|
+
if (!srcCard || !tgtCard) return null;
|
|
1469
|
+
let identifying;
|
|
1470
|
+
if (mid === "--") identifying = true;
|
|
1471
|
+
else if (mid === "..") identifying = false;
|
|
1472
|
+
else return null;
|
|
1473
|
+
return {
|
|
1474
|
+
sourceCardinality: srcCard,
|
|
1475
|
+
targetCardinality: tgtCard,
|
|
1476
|
+
identifying
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const ER_LINE_RE = /^(\S+)\s+([|}{o.][|}{o.][-.][-.][|}{o.][|}{o.])\s+(\S+)\s*:\s*"?([^"]*)"?\s*$/;
|
|
1480
|
+
function fromMermaidER(input) {
|
|
1481
|
+
validateInput(input, "Mermaid ER");
|
|
1482
|
+
const { lines } = prepareLines(input);
|
|
1483
|
+
const header = lines[0]?.trim();
|
|
1484
|
+
if (!header || !header.startsWith("erDiagram")) throw new Error("Mermaid ER: expected \"erDiagram\" header");
|
|
1485
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1486
|
+
const edges = [];
|
|
1487
|
+
let edgeCounter = 0;
|
|
1488
|
+
function ensureNode(id) {
|
|
1489
|
+
if (!nodeMap.has(id)) nodeMap.set(id, {
|
|
1490
|
+
type: "node",
|
|
1491
|
+
id,
|
|
1492
|
+
parentId: null,
|
|
1493
|
+
initialNodeId: null,
|
|
1494
|
+
label: id,
|
|
1495
|
+
data: {}
|
|
1496
|
+
});
|
|
1497
|
+
return nodeMap.get(id);
|
|
1498
|
+
}
|
|
1499
|
+
let currentEntity = null;
|
|
1500
|
+
let inBlock = false;
|
|
1501
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1502
|
+
const line = lines[i].trim();
|
|
1503
|
+
if (!line) continue;
|
|
1504
|
+
const blockMatch = line.match(/^(\S+)\s*\{\s*$/);
|
|
1505
|
+
if (blockMatch) {
|
|
1506
|
+
currentEntity = blockMatch[1];
|
|
1507
|
+
ensureNode(currentEntity);
|
|
1508
|
+
inBlock = true;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (inBlock && currentEntity) {
|
|
1512
|
+
if (line === "}") {
|
|
1513
|
+
inBlock = false;
|
|
1514
|
+
currentEntity = null;
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
const attrMatch = line.match(/^(\S+)\s+(\S+)(?:\s+(PK|FK|UK))?(?:\s+"([^"]*)")?$/);
|
|
1518
|
+
if (attrMatch) {
|
|
1519
|
+
const node = nodeMap.get(currentEntity);
|
|
1520
|
+
if (!node.data.attributes) node.data.attributes = [];
|
|
1521
|
+
node.data.attributes.push({
|
|
1522
|
+
type: attrMatch[1],
|
|
1523
|
+
name: attrMatch[2],
|
|
1524
|
+
...attrMatch[3] && { key: attrMatch[3] },
|
|
1525
|
+
...attrMatch[4] && { comment: attrMatch[4] }
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
const relMatch = line.match(ER_LINE_RE);
|
|
1531
|
+
if (relMatch) {
|
|
1532
|
+
const leftEntity = relMatch[1];
|
|
1533
|
+
const symbol = relMatch[2];
|
|
1534
|
+
const rightEntity = relMatch[3];
|
|
1535
|
+
const label = relMatch[4].trim();
|
|
1536
|
+
ensureNode(leftEntity);
|
|
1537
|
+
ensureNode(rightEntity);
|
|
1538
|
+
const rel = parseERRelationship(symbol);
|
|
1539
|
+
if (rel) {
|
|
1540
|
+
const edgeId = generateEdgeId(leftEntity, rightEntity, edgeCounter++);
|
|
1541
|
+
edges.push({
|
|
1542
|
+
type: "edge",
|
|
1543
|
+
id: edgeId,
|
|
1544
|
+
sourceId: leftEntity,
|
|
1545
|
+
targetId: rightEntity,
|
|
1546
|
+
label: label ? unescapeMermaidLabel(label) : "",
|
|
1547
|
+
data: rel
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (/^[A-Z_][\w]*$/i.test(line)) ensureNode(line);
|
|
1553
|
+
}
|
|
1554
|
+
return {
|
|
1555
|
+
id: "",
|
|
1556
|
+
type: "directed",
|
|
1557
|
+
initialNodeId: null,
|
|
1558
|
+
nodes: Array.from(nodeMap.values()),
|
|
1559
|
+
edges,
|
|
1560
|
+
data: { diagramType: "erDiagram" }
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
function toMermaidER(graph) {
|
|
1564
|
+
const lines = ["erDiagram"];
|
|
1565
|
+
for (const node of graph.nodes) if (node.data?.attributes && node.data.attributes.length > 0) {
|
|
1566
|
+
lines.push(` ${node.id} {`);
|
|
1567
|
+
for (const attr of node.data.attributes) {
|
|
1568
|
+
const keyStr = attr.key ? ` ${attr.key}` : "";
|
|
1569
|
+
const commentStr = attr.comment ? ` "${escapeMermaidLabel(attr.comment)}"` : "";
|
|
1570
|
+
lines.push(` ${attr.type} ${attr.name}${keyStr}${commentStr}`);
|
|
1571
|
+
}
|
|
1572
|
+
lines.push(` }`);
|
|
1573
|
+
}
|
|
1574
|
+
for (const edge of graph.edges) {
|
|
1575
|
+
const d = edge.data;
|
|
1576
|
+
if (!d) continue;
|
|
1577
|
+
const leftMark = CARDINALITY_TO_LEFT[d.sourceCardinality] ?? "||";
|
|
1578
|
+
const rightMark = CARDINALITY_TO_RIGHT[d.targetCardinality] ?? "||";
|
|
1579
|
+
const symbol = `${leftMark}${d.identifying ? "--" : ".."}${rightMark}`;
|
|
1580
|
+
const label = edge.label ? `"${escapeMermaidLabel(edge.label)}"` : "\"\"";
|
|
1581
|
+
lines.push(` ${edge.sourceId} ${symbol} ${edge.targetId} : ${label}`);
|
|
1582
|
+
}
|
|
1583
|
+
return lines.join("\n");
|
|
1584
|
+
}
|
|
1585
|
+
/** Bidirectional converter for Mermaid ER diagram format. */
|
|
1586
|
+
const mermaidERConverter = createFormatConverter(toMermaidER, fromMermaidER);
|
|
1587
|
+
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region src/formats/mermaid/mindmap.ts
|
|
1590
|
+
const SHAPE_PATTERNS = [
|
|
1591
|
+
[
|
|
1592
|
+
"(((",
|
|
1593
|
+
")))",
|
|
1594
|
+
"double-circle"
|
|
1595
|
+
],
|
|
1596
|
+
[
|
|
1597
|
+
"((",
|
|
1598
|
+
"))",
|
|
1599
|
+
"circle"
|
|
1600
|
+
],
|
|
1601
|
+
[
|
|
1602
|
+
"([",
|
|
1603
|
+
"])",
|
|
1604
|
+
"stadium"
|
|
1605
|
+
],
|
|
1606
|
+
[
|
|
1607
|
+
"{{",
|
|
1608
|
+
"}}",
|
|
1609
|
+
"hexagon"
|
|
1610
|
+
],
|
|
1611
|
+
[
|
|
1612
|
+
"(",
|
|
1613
|
+
")",
|
|
1614
|
+
"rounded"
|
|
1615
|
+
],
|
|
1616
|
+
[
|
|
1617
|
+
"[",
|
|
1618
|
+
"]",
|
|
1619
|
+
"rectangle"
|
|
1620
|
+
]
|
|
1621
|
+
];
|
|
1622
|
+
const SHAPE_TO_BRACKETS = {};
|
|
1623
|
+
for (const [opener, closer, name] of SHAPE_PATTERNS) SHAPE_TO_BRACKETS[name] = [opener, closer];
|
|
1624
|
+
function parseNodeText(text) {
|
|
1625
|
+
for (const [opener, closer, shapeName] of SHAPE_PATTERNS) if (text.startsWith(opener) && text.endsWith(closer)) return {
|
|
1626
|
+
label: text.slice(opener.length, text.length - closer.length).trim(),
|
|
1627
|
+
shape: shapeName
|
|
1628
|
+
};
|
|
1629
|
+
return { label: text.trim() };
|
|
1630
|
+
}
|
|
1631
|
+
function fromMermaidMindmap(input) {
|
|
1632
|
+
validateInput(input, "Mermaid mindmap");
|
|
1633
|
+
const { lines } = prepareLines(input);
|
|
1634
|
+
const header = lines[0]?.trim();
|
|
1635
|
+
if (!header || !header.startsWith("mindmap")) throw new Error("Mermaid mindmap: expected \"mindmap\" header");
|
|
1636
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1637
|
+
const edges = [];
|
|
1638
|
+
let nodeCounter = 0;
|
|
1639
|
+
let edgeCounter = 0;
|
|
1640
|
+
const stack = [];
|
|
1641
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1642
|
+
const rawLine = lines[i];
|
|
1643
|
+
if (!rawLine.trim()) continue;
|
|
1644
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
1645
|
+
const content = rawLine.trim();
|
|
1646
|
+
let icon;
|
|
1647
|
+
let cleanContent = content;
|
|
1648
|
+
const iconMatch = content.match(/::icon\(([^)]+)\)/);
|
|
1649
|
+
if (iconMatch) {
|
|
1650
|
+
icon = iconMatch[1];
|
|
1651
|
+
cleanContent = content.replace(/::icon\([^)]+\)/, "").trim();
|
|
1652
|
+
}
|
|
1653
|
+
const { label, shape } = parseNodeText(cleanContent);
|
|
1654
|
+
const id = `mm_${nodeCounter++}`;
|
|
1655
|
+
const node = {
|
|
1656
|
+
type: "node",
|
|
1657
|
+
id,
|
|
1658
|
+
parentId: null,
|
|
1659
|
+
initialNodeId: null,
|
|
1660
|
+
label,
|
|
1661
|
+
data: { ...icon && { icon } },
|
|
1662
|
+
...shape && { shape }
|
|
1663
|
+
};
|
|
1664
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
1665
|
+
if (stack.length > 0) {
|
|
1666
|
+
const parent = stack[stack.length - 1];
|
|
1667
|
+
node.parentId = parent.id;
|
|
1668
|
+
const edgeId = generateEdgeId(parent.id, id, edgeCounter++);
|
|
1669
|
+
edges.push({
|
|
1670
|
+
type: "edge",
|
|
1671
|
+
id: edgeId,
|
|
1672
|
+
sourceId: parent.id,
|
|
1673
|
+
targetId: id,
|
|
1674
|
+
label: "",
|
|
1675
|
+
data: {}
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
nodeMap.set(id, node);
|
|
1679
|
+
stack.push({
|
|
1680
|
+
id,
|
|
1681
|
+
indent
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
id: "",
|
|
1686
|
+
type: "directed",
|
|
1687
|
+
initialNodeId: null,
|
|
1688
|
+
nodes: Array.from(nodeMap.values()),
|
|
1689
|
+
edges,
|
|
1690
|
+
data: { diagramType: "mindmap" }
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function toMermaidMindmap(graph) {
|
|
1694
|
+
const lines = ["mindmap"];
|
|
1695
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
1696
|
+
for (const node of graph.nodes) {
|
|
1697
|
+
const pid = node.parentId;
|
|
1698
|
+
if (!childrenMap.has(pid)) childrenMap.set(pid, []);
|
|
1699
|
+
childrenMap.get(pid).push(node);
|
|
1700
|
+
}
|
|
1701
|
+
function writeNode(nodeId, depth) {
|
|
1702
|
+
const children = childrenMap.get(nodeId) ?? [];
|
|
1703
|
+
for (const child of children) {
|
|
1704
|
+
const indent = " ".repeat(depth + 1);
|
|
1705
|
+
const shape = child.shape;
|
|
1706
|
+
const brackets = shape ? SHAPE_TO_BRACKETS[shape] : void 0;
|
|
1707
|
+
const label = escapeMermaidLabel(child.label);
|
|
1708
|
+
const text = brackets ? `${brackets[0]}${label}${brackets[1]}` : label;
|
|
1709
|
+
let extra = "";
|
|
1710
|
+
if (child.data?.icon) extra = `\n${" ".repeat(depth + 2)}::icon(${child.data.icon})`;
|
|
1711
|
+
lines.push(`${indent}${text}${extra}`);
|
|
1712
|
+
writeNode(child.id, depth + 1);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
writeNode(null, 0);
|
|
1716
|
+
return lines.join("\n");
|
|
1717
|
+
}
|
|
1718
|
+
/** Bidirectional converter for Mermaid mindmap format. */
|
|
1719
|
+
const mermaidMindmapConverter = createFormatConverter(toMermaidMindmap, fromMermaidMindmap);
|
|
1720
|
+
|
|
1721
|
+
//#endregion
|
|
1722
|
+
//#region src/formats/mermaid/block.ts
|
|
1723
|
+
const BLOCK_SHAPES = [
|
|
1724
|
+
[
|
|
1725
|
+
"((",
|
|
1726
|
+
"))",
|
|
1727
|
+
"circle"
|
|
1728
|
+
],
|
|
1729
|
+
[
|
|
1730
|
+
"(",
|
|
1731
|
+
")",
|
|
1732
|
+
"rounded"
|
|
1733
|
+
],
|
|
1734
|
+
[
|
|
1735
|
+
"{{",
|
|
1736
|
+
"}}",
|
|
1737
|
+
"hexagon"
|
|
1738
|
+
],
|
|
1739
|
+
[
|
|
1740
|
+
"{",
|
|
1741
|
+
"}",
|
|
1742
|
+
"diamond"
|
|
1743
|
+
],
|
|
1744
|
+
[
|
|
1745
|
+
"[[",
|
|
1746
|
+
"]]",
|
|
1747
|
+
"subroutine"
|
|
1748
|
+
],
|
|
1749
|
+
[
|
|
1750
|
+
"[",
|
|
1751
|
+
"]",
|
|
1752
|
+
"rectangle"
|
|
1753
|
+
]
|
|
1754
|
+
];
|
|
1755
|
+
function parseBlockNode(text) {
|
|
1756
|
+
for (const [opener, closer, shapeName] of BLOCK_SHAPES) {
|
|
1757
|
+
const idx = text.indexOf(opener);
|
|
1758
|
+
if (idx < 0) continue;
|
|
1759
|
+
const id = text.slice(0, idx).trim();
|
|
1760
|
+
if (!id) continue;
|
|
1761
|
+
if (!text.endsWith(closer)) continue;
|
|
1762
|
+
return {
|
|
1763
|
+
id,
|
|
1764
|
+
label: text.slice(idx + opener.length, text.length - closer.length).trim(),
|
|
1765
|
+
shape: shapeName
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
if (/^[a-zA-Z_][\w]*$/.test(text.trim())) return {
|
|
1769
|
+
id: text.trim(),
|
|
1770
|
+
label: "",
|
|
1771
|
+
shape: void 0
|
|
1772
|
+
};
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
function fromMermaidBlock(input) {
|
|
1776
|
+
validateInput(input, "Mermaid block");
|
|
1777
|
+
const { lines } = prepareLines(input);
|
|
1778
|
+
const header = lines[0]?.trim();
|
|
1779
|
+
if (!header || !header.startsWith("block-beta")) throw new Error("Mermaid block: expected \"block-beta\" header");
|
|
1780
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1781
|
+
const edges = [];
|
|
1782
|
+
let edgeCounter = 0;
|
|
1783
|
+
let columns;
|
|
1784
|
+
const parentStack = [null];
|
|
1785
|
+
let blockCounter = 0;
|
|
1786
|
+
function ensureNode(id, label, shape) {
|
|
1787
|
+
if (!nodeMap.has(id)) nodeMap.set(id, {
|
|
1788
|
+
type: "node",
|
|
1789
|
+
id,
|
|
1790
|
+
parentId: parentStack[parentStack.length - 1],
|
|
1791
|
+
initialNodeId: null,
|
|
1792
|
+
label: label ?? id,
|
|
1793
|
+
data: {},
|
|
1794
|
+
...shape && { shape }
|
|
1795
|
+
});
|
|
1796
|
+
return nodeMap.get(id);
|
|
1797
|
+
}
|
|
1798
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1799
|
+
const line = lines[i].trim();
|
|
1800
|
+
if (!line) continue;
|
|
1801
|
+
const colsMatch = line.match(/^columns\s+(\d+)\s*$/);
|
|
1802
|
+
if (colsMatch) {
|
|
1803
|
+
columns = parseInt(colsMatch[1], 10);
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
if (line === "block" || line.startsWith("block:")) {
|
|
1807
|
+
const blockId = line.includes(":") ? line.split(":")[1].trim() : `__block_${blockCounter++}`;
|
|
1808
|
+
ensureNode(blockId);
|
|
1809
|
+
parentStack.push(blockId);
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (line === "end") {
|
|
1813
|
+
if (parentStack.length > 1) parentStack.pop();
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
const edgeMatch = line.match(/^(\S+)\s*(-->|---|==>|-\.->)\s*(\S+)\s*$/);
|
|
1817
|
+
if (edgeMatch) {
|
|
1818
|
+
const sourceId = edgeMatch[1];
|
|
1819
|
+
const targetId = edgeMatch[3];
|
|
1820
|
+
ensureNode(sourceId);
|
|
1821
|
+
ensureNode(targetId);
|
|
1822
|
+
const edgeId = generateEdgeId(sourceId, targetId, edgeCounter++);
|
|
1823
|
+
edges.push({
|
|
1824
|
+
type: "edge",
|
|
1825
|
+
id: edgeId,
|
|
1826
|
+
sourceId,
|
|
1827
|
+
targetId,
|
|
1828
|
+
label: "",
|
|
1829
|
+
data: {}
|
|
1830
|
+
});
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const spanMatch = line.match(/^(\S+?)(?:\["([^"]*)"\])?:(\d+)\s*$/);
|
|
1834
|
+
if (spanMatch) {
|
|
1835
|
+
const id = spanMatch[1];
|
|
1836
|
+
const label = spanMatch[2] ?? id;
|
|
1837
|
+
const span = parseInt(spanMatch[3], 10);
|
|
1838
|
+
const node = ensureNode(id, label);
|
|
1839
|
+
node.data.span = span;
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
const parts = line.split(/\s+/);
|
|
1843
|
+
let consumed = false;
|
|
1844
|
+
for (const part of parts) {
|
|
1845
|
+
const parsed = parseBlockNode(part);
|
|
1846
|
+
if (parsed) {
|
|
1847
|
+
ensureNode(parsed.id, parsed.label, parsed.shape);
|
|
1848
|
+
consumed = true;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (consumed) continue;
|
|
1852
|
+
const nodeDecl = parseBlockNode(line);
|
|
1853
|
+
if (nodeDecl) ensureNode(nodeDecl.id, nodeDecl.label, nodeDecl.shape);
|
|
1854
|
+
}
|
|
1855
|
+
return {
|
|
1856
|
+
id: "",
|
|
1857
|
+
type: "directed",
|
|
1858
|
+
initialNodeId: null,
|
|
1859
|
+
nodes: Array.from(nodeMap.values()),
|
|
1860
|
+
edges,
|
|
1861
|
+
data: {
|
|
1862
|
+
diagramType: "block",
|
|
1863
|
+
...columns !== void 0 && { columns }
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function toMermaidBlock(graph) {
|
|
1868
|
+
const lines = ["block-beta"];
|
|
1869
|
+
if (graph.data?.columns) lines.push(` columns ${graph.data.columns}`);
|
|
1870
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
1871
|
+
for (const node of graph.nodes) {
|
|
1872
|
+
const pid = node.parentId;
|
|
1873
|
+
if (!childrenMap.has(pid)) childrenMap.set(pid, []);
|
|
1874
|
+
childrenMap.get(pid).push(node);
|
|
1875
|
+
}
|
|
1876
|
+
const isParent = /* @__PURE__ */ new Set();
|
|
1877
|
+
for (const node of graph.nodes) if (childrenMap.has(node.id)) isParent.add(node.id);
|
|
1878
|
+
function writeNodes(parentId, indent) {
|
|
1879
|
+
const children = childrenMap.get(parentId) ?? [];
|
|
1880
|
+
for (const child of children) if (isParent.has(child.id)) {
|
|
1881
|
+
lines.push(`${indent}block:${child.id}`);
|
|
1882
|
+
writeNodes(child.id, indent + " ");
|
|
1883
|
+
lines.push(`${indent}end`);
|
|
1884
|
+
} else {
|
|
1885
|
+
const label = child.label && child.label !== child.id ? `["${escapeMermaidLabel(child.label)}"]` : "";
|
|
1886
|
+
const span = child.data?.span ? `:${child.data.span}` : "";
|
|
1887
|
+
lines.push(`${indent}${child.id}${label}${span}`);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
writeNodes(null, " ");
|
|
1891
|
+
for (const edge of graph.edges) lines.push(` ${edge.sourceId} --> ${edge.targetId}`);
|
|
1892
|
+
return lines.join("\n");
|
|
1893
|
+
}
|
|
1894
|
+
/** Bidirectional converter for Mermaid block diagram format. */
|
|
1895
|
+
const mermaidBlockConverter = createFormatConverter(toMermaidBlock, fromMermaidBlock);
|
|
1896
|
+
|
|
1897
|
+
//#endregion
|
|
1898
|
+
export { fromMermaidBlock, fromMermaidClass, fromMermaidER, fromMermaidFlowchart, fromMermaidMindmap, fromMermaidSequence, fromMermaidState, mermaidBlockConverter, mermaidClassConverter, mermaidERConverter, mermaidFlowchartConverter, mermaidMindmapConverter, mermaidSequenceConverter, mermaidStateConverter, toMermaidBlock, toMermaidClass, toMermaidER, toMermaidFlowchart, toMermaidMindmap, toMermaidSequence, toMermaidState };
|