@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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/ThemeContext.d.ts +47 -0
  4. package/dist/ThemeContext.js +81 -0
  5. package/dist/components/DiagramCanvas.d.ts +8 -0
  6. package/dist/components/DiagramCanvas.js +19 -0
  7. package/dist/components/GraphCanvas.d.ts +9 -0
  8. package/dist/components/GraphCanvas.js +104 -0
  9. package/dist/components/NodeDetail.d.ts +9 -0
  10. package/dist/components/NodeDetail.js +127 -0
  11. package/dist/components/edges/RoutedEdge.d.ts +11 -0
  12. package/dist/components/edges/RoutedEdge.js +199 -0
  13. package/dist/components/nodes/ClassNode.d.ts +5 -0
  14. package/dist/components/nodes/ClassNode.js +62 -0
  15. package/dist/components/nodes/EntityNode.d.ts +5 -0
  16. package/dist/components/nodes/EntityNode.js +57 -0
  17. package/dist/components/nodes/FlowNode.d.ts +5 -0
  18. package/dist/components/nodes/FlowNode.js +144 -0
  19. package/dist/components/nodes/SequenceNodes.d.ts +33 -0
  20. package/dist/components/nodes/SequenceNodes.js +205 -0
  21. package/dist/components/nodes/StateNode.d.ts +5 -0
  22. package/dist/components/nodes/StateNode.js +71 -0
  23. package/dist/components/nodes/SubgraphNode.d.ts +5 -0
  24. package/dist/components/nodes/SubgraphNode.js +16 -0
  25. package/dist/config.d.ts +138 -0
  26. package/dist/config.js +165 -0
  27. package/dist/core.d.ts +12 -0
  28. package/dist/core.js +7 -0
  29. package/dist/diagrams/detect.d.ts +2 -0
  30. package/dist/diagrams/detect.js +8 -0
  31. package/dist/diagrams/plugins.d.ts +2 -0
  32. package/dist/diagrams/plugins.js +45 -0
  33. package/dist/diagrams/registry.d.ts +3 -0
  34. package/dist/diagrams/registry.js +13 -0
  35. package/dist/diagrams/types.d.ts +7 -0
  36. package/dist/diagrams/types.js +1 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +3 -0
  39. package/dist/layout/dagre/index.d.ts +31 -0
  40. package/dist/layout/dagre/index.js +224 -0
  41. package/dist/layout/dagre/nodeSizing.d.ts +32 -0
  42. package/dist/layout/dagre/nodeSizing.js +202 -0
  43. package/dist/layout/edges/buildEdges.d.ts +18 -0
  44. package/dist/layout/edges/buildEdges.js +405 -0
  45. package/dist/layout/edges/classify.d.ts +13 -0
  46. package/dist/layout/edges/classify.js +36 -0
  47. package/dist/layout/edges/diamondHandles.d.ts +23 -0
  48. package/dist/layout/edges/diamondHandles.js +108 -0
  49. package/dist/layout/edges/index.d.ts +10 -0
  50. package/dist/layout/edges/index.js +8 -0
  51. package/dist/layout/edges/paths.d.ts +57 -0
  52. package/dist/layout/edges/paths.js +279 -0
  53. package/dist/layout/index.d.ts +59 -0
  54. package/dist/layout/index.js +131 -0
  55. package/dist/layout/intersect/circle.d.ts +2 -0
  56. package/dist/layout/intersect/circle.js +14 -0
  57. package/dist/layout/intersect/diamond.d.ts +9 -0
  58. package/dist/layout/intersect/diamond.js +21 -0
  59. package/dist/layout/intersect/index.d.ts +17 -0
  60. package/dist/layout/intersect/index.js +28 -0
  61. package/dist/layout/intersect/rect.d.ts +10 -0
  62. package/dist/layout/intersect/rect.js +31 -0
  63. package/dist/layout/intersect/rectRounded.d.ts +20 -0
  64. package/dist/layout/intersect/rectRounded.js +48 -0
  65. package/dist/layout/mindmapLayout.d.ts +13 -0
  66. package/dist/layout/mindmapLayout.js +299 -0
  67. package/dist/layout/sequenceLayout.d.ts +24 -0
  68. package/dist/layout/sequenceLayout.js +414 -0
  69. package/dist/layout/subgraph.d.ts +26 -0
  70. package/dist/layout/subgraph.js +63 -0
  71. package/dist/layout/types.d.ts +34 -0
  72. package/dist/layout/types.js +8 -0
  73. package/dist/parsers/classDiagram.d.ts +2 -0
  74. package/dist/parsers/classDiagram.js +105 -0
  75. package/dist/parsers/er.d.ts +2 -0
  76. package/dist/parsers/er.js +97 -0
  77. package/dist/parsers/flowchart.d.ts +2 -0
  78. package/dist/parsers/flowchart.js +191 -0
  79. package/dist/parsers/helpers.d.ts +4 -0
  80. package/dist/parsers/helpers.js +8 -0
  81. package/dist/parsers/index.d.ts +7 -0
  82. package/dist/parsers/index.js +19 -0
  83. package/dist/parsers/mindmap.d.ts +2 -0
  84. package/dist/parsers/mindmap.js +124 -0
  85. package/dist/parsers/sequence.d.ts +18 -0
  86. package/dist/parsers/sequence.js +196 -0
  87. package/dist/parsers/state.d.ts +2 -0
  88. package/dist/parsers/state.js +68 -0
  89. package/dist/react.d.ts +7 -0
  90. package/dist/react.js +9 -0
  91. package/dist/reactDefaults.d.ts +5 -0
  92. package/dist/reactDefaults.js +37 -0
  93. package/dist/renderMarkdown.d.ts +9 -0
  94. package/dist/renderMarkdown.js +103 -0
  95. package/dist/swagger.d.ts +113 -0
  96. package/dist/swagger.js +551 -0
  97. package/dist/theme/dark.d.ts +8 -0
  98. package/dist/theme/dark.js +190 -0
  99. package/dist/theme/index.d.ts +18 -0
  100. package/dist/theme/index.js +29 -0
  101. package/dist/theme/light.d.ts +8 -0
  102. package/dist/theme/light.js +190 -0
  103. package/dist/theme/types.d.ts +97 -0
  104. package/dist/theme/types.js +7 -0
  105. package/dist/types.d.ts +235 -0
  106. package/dist/types.js +1 -0
  107. 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,2 @@
1
+ import type { ParsedGraph } from '../types.js';
2
+ export declare function parseFlowchart(syntax: string): ParsedGraph | null;
@@ -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,4 @@
1
+ /** Trim lines, strip blanks and comments */
2
+ export declare function prep(syntax: string): string[];
3
+ /** Strip surrounding quotes and HTML entities */
4
+ export declare function cleanLabel(raw: string): string;
@@ -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,2 @@
1
+ import type { ParsedGraph } from '../types.js';
2
+ export declare function parseMindmap(syntax: string): ParsedGraph | null;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { ParsedGraph } from '../types.js';
2
+ export declare function parseStateDiagram(syntax: string): ParsedGraph | null;