@graph-artifact/core 0.1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/ThemeContext.d.ts +47 -0
- package/dist/ThemeContext.js +81 -0
- package/dist/components/DiagramCanvas.d.ts +8 -0
- package/dist/components/DiagramCanvas.js +19 -0
- package/dist/components/GraphCanvas.d.ts +9 -0
- package/dist/components/GraphCanvas.js +104 -0
- package/dist/components/NodeDetail.d.ts +9 -0
- package/dist/components/NodeDetail.js +127 -0
- package/dist/components/edges/RoutedEdge.d.ts +11 -0
- package/dist/components/edges/RoutedEdge.js +199 -0
- package/dist/components/nodes/ClassNode.d.ts +5 -0
- package/dist/components/nodes/ClassNode.js +62 -0
- package/dist/components/nodes/EntityNode.d.ts +5 -0
- package/dist/components/nodes/EntityNode.js +57 -0
- package/dist/components/nodes/FlowNode.d.ts +5 -0
- package/dist/components/nodes/FlowNode.js +144 -0
- package/dist/components/nodes/SequenceNodes.d.ts +33 -0
- package/dist/components/nodes/SequenceNodes.js +205 -0
- package/dist/components/nodes/StateNode.d.ts +5 -0
- package/dist/components/nodes/StateNode.js +71 -0
- package/dist/components/nodes/SubgraphNode.d.ts +5 -0
- package/dist/components/nodes/SubgraphNode.js +16 -0
- package/dist/config.d.ts +138 -0
- package/dist/config.js +165 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.js +7 -0
- package/dist/diagrams/detect.d.ts +2 -0
- package/dist/diagrams/detect.js +8 -0
- package/dist/diagrams/plugins.d.ts +2 -0
- package/dist/diagrams/plugins.js +45 -0
- package/dist/diagrams/registry.d.ts +3 -0
- package/dist/diagrams/registry.js +13 -0
- package/dist/diagrams/types.d.ts +7 -0
- package/dist/diagrams/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/layout/dagre/index.d.ts +31 -0
- package/dist/layout/dagre/index.js +224 -0
- package/dist/layout/dagre/nodeSizing.d.ts +32 -0
- package/dist/layout/dagre/nodeSizing.js +202 -0
- package/dist/layout/edges/buildEdges.d.ts +18 -0
- package/dist/layout/edges/buildEdges.js +405 -0
- package/dist/layout/edges/classify.d.ts +13 -0
- package/dist/layout/edges/classify.js +36 -0
- package/dist/layout/edges/diamondHandles.d.ts +23 -0
- package/dist/layout/edges/diamondHandles.js +108 -0
- package/dist/layout/edges/index.d.ts +10 -0
- package/dist/layout/edges/index.js +8 -0
- package/dist/layout/edges/paths.d.ts +57 -0
- package/dist/layout/edges/paths.js +279 -0
- package/dist/layout/index.d.ts +59 -0
- package/dist/layout/index.js +131 -0
- package/dist/layout/intersect/circle.d.ts +2 -0
- package/dist/layout/intersect/circle.js +14 -0
- package/dist/layout/intersect/diamond.d.ts +9 -0
- package/dist/layout/intersect/diamond.js +21 -0
- package/dist/layout/intersect/index.d.ts +17 -0
- package/dist/layout/intersect/index.js +28 -0
- package/dist/layout/intersect/rect.d.ts +10 -0
- package/dist/layout/intersect/rect.js +31 -0
- package/dist/layout/intersect/rectRounded.d.ts +20 -0
- package/dist/layout/intersect/rectRounded.js +48 -0
- package/dist/layout/mindmapLayout.d.ts +13 -0
- package/dist/layout/mindmapLayout.js +299 -0
- package/dist/layout/sequenceLayout.d.ts +24 -0
- package/dist/layout/sequenceLayout.js +414 -0
- package/dist/layout/subgraph.d.ts +26 -0
- package/dist/layout/subgraph.js +63 -0
- package/dist/layout/types.d.ts +34 -0
- package/dist/layout/types.js +8 -0
- package/dist/parsers/classDiagram.d.ts +2 -0
- package/dist/parsers/classDiagram.js +105 -0
- package/dist/parsers/er.d.ts +2 -0
- package/dist/parsers/er.js +97 -0
- package/dist/parsers/flowchart.d.ts +2 -0
- package/dist/parsers/flowchart.js +191 -0
- package/dist/parsers/helpers.d.ts +4 -0
- package/dist/parsers/helpers.js +8 -0
- package/dist/parsers/index.d.ts +7 -0
- package/dist/parsers/index.js +19 -0
- package/dist/parsers/mindmap.d.ts +2 -0
- package/dist/parsers/mindmap.js +124 -0
- package/dist/parsers/sequence.d.ts +18 -0
- package/dist/parsers/sequence.js +196 -0
- package/dist/parsers/state.d.ts +2 -0
- package/dist/parsers/state.js +68 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +9 -0
- package/dist/reactDefaults.d.ts +5 -0
- package/dist/reactDefaults.js +37 -0
- package/dist/renderMarkdown.d.ts +9 -0
- package/dist/renderMarkdown.js +103 -0
- package/dist/swagger.d.ts +113 -0
- package/dist/swagger.js +551 -0
- package/dist/theme/dark.d.ts +8 -0
- package/dist/theme/dark.js +190 -0
- package/dist/theme/index.d.ts +18 -0
- package/dist/theme/index.js +29 -0
- package/dist/theme/light.d.ts +8 -0
- package/dist/theme/light.js +190 -0
- package/dist/theme/types.d.ts +97 -0
- package/dist/theme/types.js +7 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.js +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { prep } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Maps Mermaid ER cardinality symbols to semantic values.
|
|
4
|
+
*
|
|
5
|
+
* Symbols (from Mermaid's JISON grammar):
|
|
6
|
+
* || → ONLY_ONE
|
|
7
|
+
* o| → ZERO_OR_ONE
|
|
8
|
+
* |o → ZERO_OR_ONE
|
|
9
|
+
* o{ → ZERO_OR_MORE
|
|
10
|
+
* }o → ZERO_OR_MORE
|
|
11
|
+
* |{ → ONE_OR_MORE
|
|
12
|
+
* }| → ONE_OR_MORE
|
|
13
|
+
*/
|
|
14
|
+
function parseCardinality(symbol) {
|
|
15
|
+
switch (symbol) {
|
|
16
|
+
case '||': return 'ONLY_ONE';
|
|
17
|
+
case 'o|':
|
|
18
|
+
case '|o': return 'ZERO_OR_ONE';
|
|
19
|
+
case 'o{':
|
|
20
|
+
case '}o': return 'ZERO_OR_MORE';
|
|
21
|
+
case '|{':
|
|
22
|
+
case '}|': return 'ONE_OR_MORE';
|
|
23
|
+
default: return 'ONLY_ONE';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function parseErDiagram(syntax) {
|
|
27
|
+
const lines = prep(syntax);
|
|
28
|
+
if (!lines.some(l => /^erDiagram$/i.test(l)))
|
|
29
|
+
return null;
|
|
30
|
+
const nodeMap = new Map();
|
|
31
|
+
const edges = [];
|
|
32
|
+
const entityAttrs = new Map();
|
|
33
|
+
let currentEntity = null;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (/^erDiagram$/i.test(line))
|
|
36
|
+
continue;
|
|
37
|
+
// Relationship: ENTITY1 <cardL><relType><cardR> ENTITY2 : label
|
|
38
|
+
// cardL/cardR are 1-2 chars from |o{}
|
|
39
|
+
// relType is -- (identifying) or .. (non-identifying)
|
|
40
|
+
const rel = line.match(/^(\w+)\s+([|o}{]{1,2})(-{2}|\.{2})([|o}{]{1,2})\s+(\w+)\s*:\s*(.+)/);
|
|
41
|
+
if (rel) {
|
|
42
|
+
const src = rel[1];
|
|
43
|
+
const leftCard = rel[2];
|
|
44
|
+
const rightCard = rel[4];
|
|
45
|
+
const tgt = rel[5];
|
|
46
|
+
const label = rel[6].trim().replace(/^["']|["']$/g, '');
|
|
47
|
+
if (!nodeMap.has(src))
|
|
48
|
+
nodeMap.set(src, { id: src, label: src, shape: 'rect', attributes: [] });
|
|
49
|
+
if (!nodeMap.has(tgt))
|
|
50
|
+
nodeMap.set(tgt, { id: tgt, label: tgt, shape: 'rect', attributes: [] });
|
|
51
|
+
edges.push({
|
|
52
|
+
source: src,
|
|
53
|
+
target: tgt,
|
|
54
|
+
label,
|
|
55
|
+
// Keep token assignment aligned with source syntax adjacency:
|
|
56
|
+
// left token belongs to source entity, right token belongs to target entity.
|
|
57
|
+
cardSource: parseCardinality(leftCard),
|
|
58
|
+
cardTarget: parseCardinality(rightCard),
|
|
59
|
+
cardSourceToken: leftCard,
|
|
60
|
+
cardTargetToken: rightCard,
|
|
61
|
+
});
|
|
62
|
+
currentEntity = null;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Entity block open
|
|
66
|
+
const blockOpen = line.match(/^(\w+)\s*\{$/);
|
|
67
|
+
if (blockOpen) {
|
|
68
|
+
const name = blockOpen[1];
|
|
69
|
+
if (!nodeMap.has(name))
|
|
70
|
+
nodeMap.set(name, { id: name, label: name, shape: 'rect', attributes: [] });
|
|
71
|
+
currentEntity = name;
|
|
72
|
+
if (!entityAttrs.has(name))
|
|
73
|
+
entityAttrs.set(name, []);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (line === '}') {
|
|
77
|
+
currentEntity = null;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Attribute line: type name PK/FK/UK
|
|
81
|
+
if (currentEntity) {
|
|
82
|
+
const attr = line.match(/^(\w+)\s+(\w+)(?:\s+(PK|FK|UK))?/);
|
|
83
|
+
if (attr) {
|
|
84
|
+
const marker = attr[3] ? ` ${attr[3]}` : '';
|
|
85
|
+
entityAttrs.get(currentEntity)?.push(`${attr[1]} ${attr[2]}${marker}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (nodeMap.size === 0)
|
|
90
|
+
return null;
|
|
91
|
+
for (const [id, attrs] of entityAttrs) {
|
|
92
|
+
const node = nodeMap.get(id);
|
|
93
|
+
if (node)
|
|
94
|
+
node.attributes = attrs;
|
|
95
|
+
}
|
|
96
|
+
return { kind: 'graph', direction: 'TB', diagramType: 'er', nodes: Array.from(nodeMap.values()), edges };
|
|
97
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { prep, cleanLabel } from './helpers.js';
|
|
2
|
+
export function parseFlowchart(syntax) {
|
|
3
|
+
const lines = prep(syntax);
|
|
4
|
+
const declLine = lines.find(l => /^(graph|flowchart)\s+(TB|TD|LR|BT|RL)/i.test(l));
|
|
5
|
+
if (!declLine)
|
|
6
|
+
return null;
|
|
7
|
+
const dirMatch = declLine.match(/^(?:graph|flowchart)\s+(TB|TD|LR|BT|RL)/i);
|
|
8
|
+
const direction = (dirMatch?.[1]?.toUpperCase() === 'TD' ? 'TB' : dirMatch?.[1]?.toUpperCase() ?? 'TB');
|
|
9
|
+
const nodeMap = new Map();
|
|
10
|
+
const edges = [];
|
|
11
|
+
const subgraphs = [];
|
|
12
|
+
// Tracks which subgraph owns each node (first assignment wins)
|
|
13
|
+
const nodeOwner = new Map();
|
|
14
|
+
// Subgraph parsing stack
|
|
15
|
+
const subgraphStack = [];
|
|
16
|
+
function addToCurrentSubgraph(nodeId) {
|
|
17
|
+
if (subgraphStack.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
if (nodeOwner.has(nodeId))
|
|
20
|
+
return; // already claimed by another subgraph
|
|
21
|
+
const current = subgraphStack[subgraphStack.length - 1];
|
|
22
|
+
if (!current.nodeIds.includes(nodeId)) {
|
|
23
|
+
current.nodeIds.push(nodeId);
|
|
24
|
+
nodeOwner.set(nodeId, current.id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (/^(graph|flowchart)\s/i.test(line))
|
|
29
|
+
continue;
|
|
30
|
+
if (/^(classDef|class|style|click)\s/i.test(line))
|
|
31
|
+
continue;
|
|
32
|
+
if (/^direction\s/i.test(line))
|
|
33
|
+
continue;
|
|
34
|
+
// Subgraph start
|
|
35
|
+
const subgraphMatch = line.match(/^subgraph\s+([^\s[]+)(?:\s*\[([^\]]*)\])?(?:\s+(.*))?/i);
|
|
36
|
+
if (subgraphMatch) {
|
|
37
|
+
const id = subgraphMatch[1];
|
|
38
|
+
const label = subgraphMatch[2]
|
|
39
|
+
? cleanLabel(subgraphMatch[2])
|
|
40
|
+
: subgraphMatch[3]
|
|
41
|
+
? cleanLabel(subgraphMatch[3])
|
|
42
|
+
: id;
|
|
43
|
+
subgraphStack.push({ id, label, nodeIds: [] });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Subgraph end
|
|
47
|
+
if (/^end$/i.test(line)) {
|
|
48
|
+
const completed = subgraphStack.pop();
|
|
49
|
+
if (completed)
|
|
50
|
+
subgraphs.push(completed);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const edgeResult = parseEdgeLine(line, nodeMap);
|
|
54
|
+
if (edgeResult) {
|
|
55
|
+
edges.push(...edgeResult);
|
|
56
|
+
// Track nodes in current subgraph — only claim unclaimed nodes
|
|
57
|
+
for (const edge of edgeResult) {
|
|
58
|
+
addToCurrentSubgraph(edge.source);
|
|
59
|
+
addToCurrentSubgraph(edge.target);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const node = parseNodeDef(line);
|
|
64
|
+
if (node) {
|
|
65
|
+
if (!nodeMap.has(node.id))
|
|
66
|
+
nodeMap.set(node.id, node);
|
|
67
|
+
addToCurrentSubgraph(node.id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Mermaid allows edges to/from subgraph IDs. Our internal graph expects
|
|
71
|
+
// edge endpoints to resolve to concrete node IDs, so map subgraph endpoints
|
|
72
|
+
// to representative member nodes.
|
|
73
|
+
const subgraphMap = new Map(subgraphs.map((sg) => [sg.id, sg]));
|
|
74
|
+
// Remove placeholder bare nodes that were introduced by edge parsing
|
|
75
|
+
// before we encountered `subgraph <id> ... end`.
|
|
76
|
+
for (const sg of subgraphs) {
|
|
77
|
+
nodeMap.delete(sg.id);
|
|
78
|
+
}
|
|
79
|
+
const resolvedEdges = [];
|
|
80
|
+
for (const edge of edges) {
|
|
81
|
+
const source = resolveFlowEndpoint(edge.source, 'source', nodeMap, subgraphMap);
|
|
82
|
+
const target = resolveFlowEndpoint(edge.target, 'target', nodeMap, subgraphMap);
|
|
83
|
+
if (!source || !target)
|
|
84
|
+
continue;
|
|
85
|
+
resolvedEdges.push({ ...edge, source, target });
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
kind: 'graph',
|
|
89
|
+
direction,
|
|
90
|
+
diagramType: 'flowchart',
|
|
91
|
+
nodes: Array.from(nodeMap.values()),
|
|
92
|
+
edges: resolvedEdges,
|
|
93
|
+
subgraphs: subgraphs.length > 0 ? subgraphs : undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function resolveFlowEndpoint(endpoint, role, nodeMap, subgraphMap) {
|
|
97
|
+
const sg = subgraphMap.get(endpoint);
|
|
98
|
+
if (sg && sg.nodeIds.length > 0) {
|
|
99
|
+
// Heuristic anchor selection:
|
|
100
|
+
// - source endpoint (subgraph -> node): use the last member encountered
|
|
101
|
+
// - target endpoint (node -> subgraph): use the first member encountered
|
|
102
|
+
return role === 'source'
|
|
103
|
+
? sg.nodeIds[sg.nodeIds.length - 1]
|
|
104
|
+
: sg.nodeIds[0];
|
|
105
|
+
}
|
|
106
|
+
if (nodeMap.has(endpoint))
|
|
107
|
+
return endpoint;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// ─── Arrow Classification ─────────────────────────────────────────────────────
|
|
111
|
+
function classifyArrow(arrow) {
|
|
112
|
+
if (/^={2,3}>$/.test(arrow))
|
|
113
|
+
return 'thick';
|
|
114
|
+
if (/^-?\.+-?>$/.test(arrow))
|
|
115
|
+
return 'dashed';
|
|
116
|
+
return 'solid';
|
|
117
|
+
}
|
|
118
|
+
// ─── Edge Parsing ─────────────────────────────────────────────────────────────
|
|
119
|
+
function parseEdgeLine(line, nodeMap) {
|
|
120
|
+
if (!/(--+>|=+>|-?\.{1,2}-?>|--+|==[^=])/.test(line))
|
|
121
|
+
return null;
|
|
122
|
+
const results = [];
|
|
123
|
+
let prevNodeId = null;
|
|
124
|
+
let lastArrowType = 'solid';
|
|
125
|
+
let lastLabel;
|
|
126
|
+
// Split on arrow tokens: -->, ===>, -.-> AND ==label==> (thick with inline label)
|
|
127
|
+
// Also matches .-> and ..-> (dashed arrows without leading hyphen)
|
|
128
|
+
for (const rawPart of line.split(/(--+>|={2,3}>|-?\.+-?>|==[^=].*?=+>)/)) {
|
|
129
|
+
const part = rawPart.trim();
|
|
130
|
+
if (!part)
|
|
131
|
+
continue;
|
|
132
|
+
// Check for thick edge with inline label: ==label==>
|
|
133
|
+
const thickLabel = part.match(/^==\s*([^=].*?)\s*=+>$/);
|
|
134
|
+
if (thickLabel) {
|
|
135
|
+
lastArrowType = 'thick';
|
|
136
|
+
lastLabel = thickLabel[1].trim();
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Check if this part is a plain arrow token
|
|
140
|
+
if (/^(--+>|={2,3}>|-?\.+-?>|--+)$/.test(part)) {
|
|
141
|
+
lastArrowType = classifyArrow(part);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let label = lastLabel;
|
|
145
|
+
lastLabel = undefined;
|
|
146
|
+
let nodePart = part;
|
|
147
|
+
const lb = nodePart.match(/^\|([^|]*)\|\s*(.*)/);
|
|
148
|
+
if (lb) {
|
|
149
|
+
label = lb[1].trim();
|
|
150
|
+
nodePart = lb[2];
|
|
151
|
+
}
|
|
152
|
+
const la = nodePart.match(/(.*?)\s*\|([^|]*)\|$/);
|
|
153
|
+
if (la && !label) {
|
|
154
|
+
nodePart = la[1];
|
|
155
|
+
label = la[2].trim();
|
|
156
|
+
}
|
|
157
|
+
const node = parseNodeDef(nodePart.trim());
|
|
158
|
+
if (!node)
|
|
159
|
+
continue;
|
|
160
|
+
if (!nodeMap.has(node.id))
|
|
161
|
+
nodeMap.set(node.id, node);
|
|
162
|
+
if (prevNodeId) {
|
|
163
|
+
results.push({ source: prevNodeId, target: node.id, label, edgeType: lastArrowType });
|
|
164
|
+
}
|
|
165
|
+
prevNodeId = node.id;
|
|
166
|
+
}
|
|
167
|
+
return results.length > 0 ? results : null;
|
|
168
|
+
}
|
|
169
|
+
// ─── Node Definition Parsing ──────────────────────────────────────────────────
|
|
170
|
+
function parseNodeDef(text) {
|
|
171
|
+
const t = text.trim();
|
|
172
|
+
if (!t)
|
|
173
|
+
return null;
|
|
174
|
+
const patterns = [
|
|
175
|
+
[/^([A-Za-z0-9_]+)\s*\{([^}]*)\}/, 'diamond'],
|
|
176
|
+
[/^([A-Za-z0-9_]+)\s*\(\(([^)]*)\)\)/, 'circle'],
|
|
177
|
+
[/^([A-Za-z0-9_]+)\s*\(\[([^\]]*)\]\)/, 'stadium'],
|
|
178
|
+
[/^([A-Za-z0-9_]+)\s*\[\(([^)]*)\)\]/, 'cylinder'],
|
|
179
|
+
[/^([A-Za-z0-9_]+)\s*\(([^)]*)\)/, 'round'],
|
|
180
|
+
[/^([A-Za-z0-9_]+)\s*\[([^\]]*)\]/, 'rect'],
|
|
181
|
+
];
|
|
182
|
+
for (const [regex, shape] of patterns) {
|
|
183
|
+
const m = t.match(regex);
|
|
184
|
+
if (m)
|
|
185
|
+
return { id: m[1], label: cleanLabel(m[2]), shape };
|
|
186
|
+
}
|
|
187
|
+
const bare = t.match(/^([A-Za-z0-9_]+)$/);
|
|
188
|
+
if (bare)
|
|
189
|
+
return { id: bare[1], label: bare[1], shape: 'rect' };
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Trim lines, strip blanks and comments */
|
|
2
|
+
export function prep(syntax) {
|
|
3
|
+
return syntax.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('%%'));
|
|
4
|
+
}
|
|
5
|
+
/** Strip surrounding quotes and HTML entities */
|
|
6
|
+
export function cleanLabel(raw) {
|
|
7
|
+
return raw.replace(/^["']|["']$/g, '').replace(/#quot;/g, '"').trim();
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ParsedDiagram } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse diagram syntax into a React Flow–compatible graph.
|
|
4
|
+
*
|
|
5
|
+
* Tries each parser in order. Returns null for unsupported types.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseMermaid(syntax: string): ParsedDiagram | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { detectDiagramPlugin } from '../diagrams/detect.js';
|
|
2
|
+
function normalizeMermaidSyntax(syntax) {
|
|
3
|
+
const trimmed = syntax.trim();
|
|
4
|
+
// Accept fenced mermaid blocks from LLM/chat outputs.
|
|
5
|
+
const fenced = trimmed.match(/^```(?:mermaid)?\s*([\s\S]*?)\s*```$/i);
|
|
6
|
+
return fenced ? fenced[1].trim() : syntax;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse diagram syntax into a React Flow–compatible graph.
|
|
10
|
+
*
|
|
11
|
+
* Tries each parser in order. Returns null for unsupported types.
|
|
12
|
+
*/
|
|
13
|
+
export function parseMermaid(syntax) {
|
|
14
|
+
const normalized = normalizeMermaidSyntax(syntax);
|
|
15
|
+
const plugin = detectDiagramPlugin(normalized);
|
|
16
|
+
if (!plugin)
|
|
17
|
+
return null;
|
|
18
|
+
return plugin.parse(normalized);
|
|
19
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cleanLabel } from './helpers.js';
|
|
2
|
+
function parseLines(syntax) {
|
|
3
|
+
const out = [];
|
|
4
|
+
for (const raw of syntax.split('\n')) {
|
|
5
|
+
if (!raw.trim())
|
|
6
|
+
continue;
|
|
7
|
+
if (raw.trimStart().startsWith('%%'))
|
|
8
|
+
continue;
|
|
9
|
+
const leading = raw.match(/^\s*/)?.[0] ?? '';
|
|
10
|
+
const indent = leading.replace(/\t/g, ' ').length;
|
|
11
|
+
const text = raw.trim();
|
|
12
|
+
out.push({ indent, text });
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function normalizeMindmapText(text) {
|
|
17
|
+
// Remove Mermaid mindmap decorators/classes/icons if present
|
|
18
|
+
return text
|
|
19
|
+
.replace(/::icon\([^)]*\)/g, '')
|
|
20
|
+
.replace(/:::[A-Za-z0-9_-]+/g, '')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
function parseNodeToken(raw) {
|
|
24
|
+
const text = normalizeMindmapText(raw);
|
|
25
|
+
// Explicit id + shaped label forms, e.g. a((Root)), b[Item], c(Item)
|
|
26
|
+
const withShape = text.match(/^([A-Za-z0-9_-]+)\s*(\(\(.+\)\)|\(.+\)|\[.+\]|\{\{.+\}\})$/);
|
|
27
|
+
if (withShape) {
|
|
28
|
+
const idHint = withShape[1];
|
|
29
|
+
const shapePart = withShape[2];
|
|
30
|
+
if (shapePart.startsWith('((') && shapePart.endsWith('))')) {
|
|
31
|
+
return { idHint, label: cleanLabel(shapePart.slice(2, -2)), shape: 'circle' };
|
|
32
|
+
}
|
|
33
|
+
if (shapePart.startsWith('(') && shapePart.endsWith(')')) {
|
|
34
|
+
return { idHint, label: cleanLabel(shapePart.slice(1, -1)), shape: 'round' };
|
|
35
|
+
}
|
|
36
|
+
if (shapePart.startsWith('[') && shapePart.endsWith(']')) {
|
|
37
|
+
return { idHint, label: cleanLabel(shapePart.slice(1, -1)), shape: 'rect' };
|
|
38
|
+
}
|
|
39
|
+
if (shapePart.startsWith('{{') && shapePart.endsWith('}}')) {
|
|
40
|
+
return { idHint, label: cleanLabel(shapePart.slice(2, -2)), shape: 'diamond' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (text.startsWith('((') && text.endsWith('))')) {
|
|
44
|
+
return { idHint: cleanLabel(text.slice(2, -2)), label: cleanLabel(text.slice(2, -2)), shape: 'circle' };
|
|
45
|
+
}
|
|
46
|
+
if (text.startsWith('(') && text.endsWith(')')) {
|
|
47
|
+
return { idHint: cleanLabel(text.slice(1, -1)), label: cleanLabel(text.slice(1, -1)), shape: 'round' };
|
|
48
|
+
}
|
|
49
|
+
if (text.startsWith('[') && text.endsWith(']')) {
|
|
50
|
+
return { idHint: cleanLabel(text.slice(1, -1)), label: cleanLabel(text.slice(1, -1)), shape: 'rect' };
|
|
51
|
+
}
|
|
52
|
+
if (text.startsWith('{{') && text.endsWith('}}')) {
|
|
53
|
+
return { idHint: cleanLabel(text.slice(2, -2)), label: cleanLabel(text.slice(2, -2)), shape: 'diamond' };
|
|
54
|
+
}
|
|
55
|
+
// Bare text or id-only node
|
|
56
|
+
const bare = cleanLabel(text);
|
|
57
|
+
return { idHint: bare, label: bare, shape: 'rect' };
|
|
58
|
+
}
|
|
59
|
+
function slug(input) {
|
|
60
|
+
const s = input.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
61
|
+
return s || 'node';
|
|
62
|
+
}
|
|
63
|
+
export function parseMindmap(syntax) {
|
|
64
|
+
const lines = parseLines(syntax);
|
|
65
|
+
if (lines.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
if (!/^mindmap\b/i.test(lines[0].text))
|
|
68
|
+
return null;
|
|
69
|
+
const nodeMap = new Map();
|
|
70
|
+
const edges = [];
|
|
71
|
+
const stack = [];
|
|
72
|
+
const idCounts = new Map();
|
|
73
|
+
let nextBranchIndex = 0;
|
|
74
|
+
const makeUniqueId = (hint) => {
|
|
75
|
+
const base = slug(hint);
|
|
76
|
+
const current = idCounts.get(base) ?? 0;
|
|
77
|
+
idCounts.set(base, current + 1);
|
|
78
|
+
return current === 0 ? base : `${base}_${current}`;
|
|
79
|
+
};
|
|
80
|
+
for (let i = 1; i < lines.length; i++) {
|
|
81
|
+
const { indent, text } = lines[i];
|
|
82
|
+
let nodeText = text;
|
|
83
|
+
if (/^root\b/i.test(nodeText)) {
|
|
84
|
+
nodeText = nodeText.replace(/^root\b/i, '').trim();
|
|
85
|
+
if (!nodeText)
|
|
86
|
+
nodeText = 'Root';
|
|
87
|
+
}
|
|
88
|
+
const parsed = parseNodeToken(nodeText);
|
|
89
|
+
const id = makeUniqueId(parsed.idHint || parsed.label);
|
|
90
|
+
nodeMap.set(id, {
|
|
91
|
+
id,
|
|
92
|
+
label: parsed.label,
|
|
93
|
+
shape: parsed.shape,
|
|
94
|
+
});
|
|
95
|
+
while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
|
|
96
|
+
stack.pop();
|
|
97
|
+
}
|
|
98
|
+
const parent = stack[stack.length - 1];
|
|
99
|
+
const depth = parent ? parent.depth + 1 : 0;
|
|
100
|
+
const branchIndex = parent
|
|
101
|
+
? (depth === 1 ? nextBranchIndex++ : parent.branchIndex)
|
|
102
|
+
: -1;
|
|
103
|
+
if (parent) {
|
|
104
|
+
edges.push({
|
|
105
|
+
source: parent.id,
|
|
106
|
+
target: id,
|
|
107
|
+
edgeType: 'solid',
|
|
108
|
+
noArrow: true,
|
|
109
|
+
treeDepth: depth,
|
|
110
|
+
branchIndex,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
stack.push({ indent, id, depth, branchIndex });
|
|
114
|
+
}
|
|
115
|
+
if (nodeMap.size === 0)
|
|
116
|
+
return null;
|
|
117
|
+
return {
|
|
118
|
+
kind: 'graph',
|
|
119
|
+
direction: 'LR',
|
|
120
|
+
diagramType: 'mindmap',
|
|
121
|
+
nodes: Array.from(nodeMap.values()),
|
|
122
|
+
edges,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequence.ts — Parser for Mermaid sequenceDiagram syntax.
|
|
3
|
+
*
|
|
4
|
+
* Extracts participants, messages, notes, and control blocks (alt/loop/opt/etc.)
|
|
5
|
+
* into a structured SequenceData object. The parsed result uses diagramType 'sequence'
|
|
6
|
+
* and carries data in the `sequence` field rather than nodes/edges.
|
|
7
|
+
*
|
|
8
|
+
* Supported syntax:
|
|
9
|
+
* - participant/actor declarations
|
|
10
|
+
* - Solid arrows: ->>, ->>
|
|
11
|
+
* - Dashed arrows: -->>, -->>
|
|
12
|
+
* - Open arrows: ->, -->
|
|
13
|
+
* - Notes: Note left/right/over
|
|
14
|
+
* - Blocks: alt/else/opt/loop/par/critical/break/rect
|
|
15
|
+
* - autonumber
|
|
16
|
+
*/
|
|
17
|
+
import type { ParsedGraph } from '../types.js';
|
|
18
|
+
export declare function parseSequenceDiagram(syntax: string): ParsedGraph | null;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequence.ts — Parser for Mermaid sequenceDiagram syntax.
|
|
3
|
+
*
|
|
4
|
+
* Extracts participants, messages, notes, and control blocks (alt/loop/opt/etc.)
|
|
5
|
+
* into a structured SequenceData object. The parsed result uses diagramType 'sequence'
|
|
6
|
+
* and carries data in the `sequence` field rather than nodes/edges.
|
|
7
|
+
*
|
|
8
|
+
* Supported syntax:
|
|
9
|
+
* - participant/actor declarations
|
|
10
|
+
* - Solid arrows: ->>, ->>
|
|
11
|
+
* - Dashed arrows: -->>, -->>
|
|
12
|
+
* - Open arrows: ->, -->
|
|
13
|
+
* - Notes: Note left/right/over
|
|
14
|
+
* - Blocks: alt/else/opt/loop/par/critical/break/rect
|
|
15
|
+
* - autonumber
|
|
16
|
+
*/
|
|
17
|
+
import { prep } from './helpers.js';
|
|
18
|
+
// ─── Arrow Classification ─────────────────────────────────────────────────────
|
|
19
|
+
const ARROW_PATTERNS = [
|
|
20
|
+
[/^-->>$/, 'dashed'], // dashed with arrowhead
|
|
21
|
+
[/^->>$/, 'solid'], // solid with arrowhead
|
|
22
|
+
[/^-->$/, 'dashed_open'], // dashed open (no arrowhead)
|
|
23
|
+
[/^->$/, 'solid_open'], // solid open (no arrowhead)
|
|
24
|
+
[/^--x$/i, 'dashed'], // dashed with cross
|
|
25
|
+
[/^-x$/i, 'solid'], // solid with cross
|
|
26
|
+
[/^--\)$/, 'dashed'], // dashed async
|
|
27
|
+
[/^-\)$/, 'solid'], // solid async
|
|
28
|
+
];
|
|
29
|
+
function classifyArrow(arrow) {
|
|
30
|
+
for (const [regex, type] of ARROW_PATTERNS) {
|
|
31
|
+
if (regex.test(arrow))
|
|
32
|
+
return type;
|
|
33
|
+
}
|
|
34
|
+
return 'solid';
|
|
35
|
+
}
|
|
36
|
+
// ─── Block Type Detection ──────────────────────────────────────────────────────
|
|
37
|
+
const BLOCK_KEYWORDS = [
|
|
38
|
+
'alt', 'else', 'opt', 'loop', 'par', 'critical', 'break', 'rect',
|
|
39
|
+
];
|
|
40
|
+
function isBlockStart(line) {
|
|
41
|
+
const lower = line.toLowerCase();
|
|
42
|
+
for (const keyword of BLOCK_KEYWORDS) {
|
|
43
|
+
if (keyword === 'else')
|
|
44
|
+
continue; // 'else' is handled as section within alt
|
|
45
|
+
if (lower.startsWith(keyword + ' ') || lower === keyword) {
|
|
46
|
+
const label = line.slice(keyword.length).trim();
|
|
47
|
+
return { type: keyword, label };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// ─── Main Parser ──────────────────────────────────────────────────────────────
|
|
53
|
+
export function parseSequenceDiagram(syntax) {
|
|
54
|
+
const lines = prep(syntax);
|
|
55
|
+
if (!lines.some(l => /^sequenceDiagram$/i.test(l)))
|
|
56
|
+
return null;
|
|
57
|
+
const participantMap = new Map();
|
|
58
|
+
const messages = [];
|
|
59
|
+
const blocks = [];
|
|
60
|
+
const notes = [];
|
|
61
|
+
let autonumber = false;
|
|
62
|
+
// Participant ordering — tracks discovery order
|
|
63
|
+
const participantOrder = [];
|
|
64
|
+
function ensureParticipant(id, label, type = 'participant') {
|
|
65
|
+
if (participantMap.has(id))
|
|
66
|
+
return;
|
|
67
|
+
participantMap.set(id, { id, label: label ?? id, type });
|
|
68
|
+
participantOrder.push(id);
|
|
69
|
+
}
|
|
70
|
+
// Block stack for nested alt/loop/opt etc.
|
|
71
|
+
const blockStack = [];
|
|
72
|
+
// Message arrow regex: captures participant names with possible spaces
|
|
73
|
+
// Format: Alice->>Bob: Hello OR Alice -->> Bob : Hello
|
|
74
|
+
const messageRegex = /^(.+?)\s*(-->>|--x|->>|-->|->|-x|--\)|-\))\s*(.+?)\s*:\s*(.*)$/i;
|
|
75
|
+
// Participant/actor declaration regex
|
|
76
|
+
const participantDeclRegex = /^(participant|actor)\s+(\S+)(?:\s+as\s+(.+))?$/i;
|
|
77
|
+
// Note regex
|
|
78
|
+
const noteRegex = /^note\s+(left of|right of|over)\s+(.+?)\s*:\s*(.+)$/i;
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
// Skip the declaration line
|
|
81
|
+
if (/^sequenceDiagram$/i.test(line))
|
|
82
|
+
continue;
|
|
83
|
+
// autonumber
|
|
84
|
+
if (/^autonumber$/i.test(line)) {
|
|
85
|
+
autonumber = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Skip directives and style commands
|
|
89
|
+
if (/^(title|%%|activate|deactivate|destroy)\s/i.test(line))
|
|
90
|
+
continue;
|
|
91
|
+
if (/^(title|activate|deactivate|destroy)$/i.test(line))
|
|
92
|
+
continue;
|
|
93
|
+
// Participant/actor declaration
|
|
94
|
+
const participantMatch = line.match(participantDeclRegex);
|
|
95
|
+
if (participantMatch) {
|
|
96
|
+
const type = participantMatch[1].toLowerCase();
|
|
97
|
+
const id = participantMatch[2];
|
|
98
|
+
const label = participantMatch[3]?.trim() ?? id;
|
|
99
|
+
ensureParticipant(id, label, type);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Note
|
|
103
|
+
const noteMatch = line.match(noteRegex);
|
|
104
|
+
if (noteMatch) {
|
|
105
|
+
const placementStr = noteMatch[1].toLowerCase();
|
|
106
|
+
const participantStr = noteMatch[2].trim();
|
|
107
|
+
const text = noteMatch[3].trim();
|
|
108
|
+
let placement;
|
|
109
|
+
if (placementStr === 'left of')
|
|
110
|
+
placement = 'left';
|
|
111
|
+
else if (placementStr === 'right of')
|
|
112
|
+
placement = 'right';
|
|
113
|
+
else
|
|
114
|
+
placement = 'over';
|
|
115
|
+
// "over Alice,Bob" → multiple participants
|
|
116
|
+
const participantIds = participantStr.split(',').map(p => p.trim());
|
|
117
|
+
for (const pid of participantIds)
|
|
118
|
+
ensureParticipant(pid);
|
|
119
|
+
notes.push({
|
|
120
|
+
text,
|
|
121
|
+
placement,
|
|
122
|
+
participants: participantIds,
|
|
123
|
+
messageIndex: messages.length,
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Block end
|
|
128
|
+
if (/^end$/i.test(line)) {
|
|
129
|
+
const completed = blockStack.pop();
|
|
130
|
+
if (completed) {
|
|
131
|
+
blocks.push({
|
|
132
|
+
type: completed.type,
|
|
133
|
+
label: completed.label,
|
|
134
|
+
startIndex: completed.startIndex,
|
|
135
|
+
endIndex: messages.length,
|
|
136
|
+
sections: completed.sections.length > 0 ? completed.sections : undefined,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Else section (within alt/par)
|
|
142
|
+
if (/^else\b/i.test(line)) {
|
|
143
|
+
const label = line.slice(4).trim();
|
|
144
|
+
if (blockStack.length > 0) {
|
|
145
|
+
const current = blockStack[blockStack.length - 1];
|
|
146
|
+
current.sections.push({ label, startIndex: messages.length });
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// Block start (alt/opt/loop/par/critical/break/rect)
|
|
151
|
+
const blockStart = isBlockStart(line);
|
|
152
|
+
if (blockStart) {
|
|
153
|
+
blockStack.push({
|
|
154
|
+
type: blockStart.type,
|
|
155
|
+
label: blockStart.label,
|
|
156
|
+
startIndex: messages.length,
|
|
157
|
+
sections: [],
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Message
|
|
162
|
+
const messageMatch = line.match(messageRegex);
|
|
163
|
+
if (messageMatch) {
|
|
164
|
+
const from = messageMatch[1].trim();
|
|
165
|
+
const arrow = messageMatch[2].trim();
|
|
166
|
+
const to = messageMatch[3].trim();
|
|
167
|
+
const label = messageMatch[4].trim();
|
|
168
|
+
ensureParticipant(from);
|
|
169
|
+
ensureParticipant(to);
|
|
170
|
+
messages.push({
|
|
171
|
+
from,
|
|
172
|
+
to,
|
|
173
|
+
label,
|
|
174
|
+
arrowType: classifyArrow(arrow),
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Need at least some participants or messages
|
|
180
|
+
if (participantMap.size === 0 && messages.length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
return {
|
|
183
|
+
kind: 'graph',
|
|
184
|
+
direction: 'TB',
|
|
185
|
+
diagramType: 'sequence',
|
|
186
|
+
nodes: [],
|
|
187
|
+
edges: [],
|
|
188
|
+
sequence: {
|
|
189
|
+
participants: participantOrder.map(id => participantMap.get(id)),
|
|
190
|
+
messages,
|
|
191
|
+
blocks: blocks.length > 0 ? blocks : undefined,
|
|
192
|
+
notes: notes.length > 0 ? notes : undefined,
|
|
193
|
+
autonumber,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|