@statelyai/graph 0.11.0 → 0.11.1
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 +26 -6
- package/dist/format-support.mjs +15 -0
- package/dist/formats/graphml/index.mjs +33 -0
- package/dist/formats/mermaid/index.d.mts +46 -2
- package/dist/formats/mermaid/index.mjs +233 -28
- package/dist/schemas.d.mts +2 -2
- package/dist/schemas.mjs +1 -1
- package/package.json +2 -1
- package/schemas/graph.schema.json +8 -1
- package/schemas/node.schema.json +8 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '@statelyai/graph'
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# @statelyai/graph
|
|
2
6
|
|
|
3
7
|
A TypeScript graph library built on plain JSON objects. Supports directed/undirected graphs, hierarchical nodes, graph algorithms, visual properties, and serialization to DOT, GraphML, Mermaid, and more.
|
|
@@ -12,6 +16,8 @@ npm install @statelyai/graph
|
|
|
12
16
|
|
|
13
17
|
Optional peers are only needed for specific adapters:
|
|
14
18
|
|
|
19
|
+
<!-- optional peer dependencies derived from package.json#peerDependencies -->
|
|
20
|
+
|
|
15
21
|
| Package | Needed for |
|
|
16
22
|
| --- | --- |
|
|
17
23
|
| `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
|
|
@@ -135,7 +141,9 @@ getEdgesByPort(graph, 'render', 'input'); // [e1]
|
|
|
135
141
|
|
|
136
142
|
## Algorithms
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
<!-- algorithm functions exported from src/algorithms.ts -->
|
|
145
|
+
|
|
146
|
+
Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, simple paths, all-pairs shortest paths, A*), centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector), community detection (label propagation, Girvan-Newman, greedy modularity, modularity scoring), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, isomorphism, topological sort, minimum spanning tree, and more. Many algorithms have lazy generator variants (`gen*`) for early exit.
|
|
139
147
|
|
|
140
148
|
```ts
|
|
141
149
|
import {
|
|
@@ -203,7 +211,9 @@ const d3Data = toD3Graph(graph); // D3.js { nodes, links }
|
|
|
203
211
|
const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
|
|
204
212
|
```
|
|
205
213
|
|
|
206
|
-
|
|
214
|
+
<!-- supported format adapters derived from src/formats/* subdirectories -->
|
|
215
|
+
|
|
216
|
+
**Supported formats:** Cytoscape.js JSON, D3.js JSON, JSON Graph Format, GEXF, GraphML, GML, TGF, DOT, Mermaid (flowchart, state, sequence, class, ER, mindmap, block, Ishikawa), ELK, xyflow, adjacency list, and edge list.
|
|
207
217
|
|
|
208
218
|
Each bidirectional format also has a converter object:
|
|
209
219
|
|
|
@@ -218,19 +228,27 @@ Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML)
|
|
|
218
228
|
|
|
219
229
|
Format-specific docs live alongside the source:
|
|
220
230
|
|
|
231
|
+
<!-- format README files under src/formats/*/README.md -->
|
|
232
|
+
|
|
233
|
+
- [Adjacency list](./src/formats/adjacency-list/README.md)
|
|
234
|
+
- [Cytoscape](./src/formats/cytoscape/README.md)
|
|
235
|
+
- [D3](./src/formats/d3/README.md)
|
|
221
236
|
- [DOT](./src/formats/dot/README.md)
|
|
222
|
-
- [
|
|
237
|
+
- [Edge list](./src/formats/edge-list/README.md)
|
|
238
|
+
- [ELK](./src/formats/elk/README.md)
|
|
223
239
|
- [GEXF](./src/formats/gexf/README.md)
|
|
224
240
|
- [GML](./src/formats/gml/README.md)
|
|
241
|
+
- [GraphML](./src/formats/graphml/README.md)
|
|
225
242
|
- [JGF](./src/formats/jgf/README.md)
|
|
226
|
-
- [TGF](./src/formats/tgf/README.md)
|
|
227
|
-
- [Cytoscape](./src/formats/cytoscape/README.md)
|
|
228
|
-
- [D3](./src/formats/d3/README.md)
|
|
229
243
|
- [Mermaid](./src/formats/mermaid/README.md)
|
|
244
|
+
- [TGF](./src/formats/tgf/README.md)
|
|
245
|
+
- [xyflow](./src/formats/xyflow/README.md)
|
|
230
246
|
- [Converter helpers](./src/formats/converter/README.md)
|
|
231
247
|
|
|
232
248
|
## Examples
|
|
233
249
|
|
|
250
|
+
<!-- runnable example files under examples/ -->
|
|
251
|
+
|
|
234
252
|
The repo includes runnable examples under [`examples/`](./examples):
|
|
235
253
|
|
|
236
254
|
- [Flow-based math](./examples/flow-based-math.ts) shows ports, topological ordering, and value propagation.
|
|
@@ -238,6 +256,8 @@ The repo includes runnable examples under [`examples/`](./examples):
|
|
|
238
256
|
|
|
239
257
|
## Development
|
|
240
258
|
|
|
259
|
+
<!-- dev commands from package.json#scripts -->
|
|
260
|
+
|
|
241
261
|
```bash
|
|
242
262
|
pnpm install
|
|
243
263
|
pnpm verify
|
package/dist/format-support.mjs
CHANGED
|
@@ -240,6 +240,21 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
240
240
|
},
|
|
241
241
|
notes: ["Index-based `linkStyle` metadata is fragile after graph mutation.", "Mermaid init directives are not fully preserved."]
|
|
242
242
|
},
|
|
243
|
+
{
|
|
244
|
+
id: "mermaid/ishikawa",
|
|
245
|
+
importPath: "@statelyai/graph/mermaid",
|
|
246
|
+
features: {
|
|
247
|
+
directed: "full",
|
|
248
|
+
undirected: "none",
|
|
249
|
+
hierarchy: "full",
|
|
250
|
+
ports: "none",
|
|
251
|
+
visual: "none",
|
|
252
|
+
style: "none",
|
|
253
|
+
weight: "none",
|
|
254
|
+
roundTrip: "partial"
|
|
255
|
+
},
|
|
256
|
+
notes: ["Indentation is preserved as hierarchy; renderer-specific fishbone layout is not represented."]
|
|
257
|
+
},
|
|
243
258
|
{
|
|
244
259
|
id: "mermaid/mindmap",
|
|
245
260
|
importPath: "@statelyai/graph/mermaid",
|
|
@@ -100,6 +100,24 @@ function toGraphML(graph) {
|
|
|
100
100
|
"@_for": "edge",
|
|
101
101
|
"@_attr.name": "weight",
|
|
102
102
|
"@_attr.type": "double"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"@_id": "ports",
|
|
106
|
+
"@_for": "node",
|
|
107
|
+
"@_attr.name": "ports",
|
|
108
|
+
"@_attr.type": "string"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"@_id": "sourcePort",
|
|
112
|
+
"@_for": "edge",
|
|
113
|
+
"@_attr.name": "sourcePort",
|
|
114
|
+
"@_attr.type": "string"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"@_id": "targetPort",
|
|
118
|
+
"@_for": "edge",
|
|
119
|
+
"@_attr.name": "targetPort",
|
|
120
|
+
"@_attr.type": "string"
|
|
103
121
|
}
|
|
104
122
|
];
|
|
105
123
|
const nodes = graph.nodes.map((node) => {
|
|
@@ -148,6 +166,10 @@ function toGraphML(graph) {
|
|
|
148
166
|
"@_key": "color",
|
|
149
167
|
"#text": node.color
|
|
150
168
|
});
|
|
169
|
+
if (node.ports !== void 0) data.push({
|
|
170
|
+
"@_key": "ports",
|
|
171
|
+
"#text": JSON.stringify(node.ports)
|
|
172
|
+
});
|
|
151
173
|
return {
|
|
152
174
|
"@_id": node.id,
|
|
153
175
|
...data.length > 0 && { data }
|
|
@@ -191,6 +213,14 @@ function toGraphML(graph) {
|
|
|
191
213
|
"@_key": "weight",
|
|
192
214
|
"#text": edge.weight
|
|
193
215
|
});
|
|
216
|
+
if (edge.sourcePort !== void 0) data.push({
|
|
217
|
+
"@_key": "sourcePort",
|
|
218
|
+
"#text": edge.sourcePort
|
|
219
|
+
});
|
|
220
|
+
if (edge.targetPort !== void 0) data.push({
|
|
221
|
+
"@_key": "targetPort",
|
|
222
|
+
"#text": edge.targetPort
|
|
223
|
+
});
|
|
194
224
|
return {
|
|
195
225
|
"@_id": edge.id,
|
|
196
226
|
"@_source": edge.sourceId,
|
|
@@ -279,6 +309,7 @@ function fromGraphML(xml) {
|
|
|
279
309
|
if (dataMap.shape !== void 0) node.shape = dataMap.shape;
|
|
280
310
|
if (dataMap.color !== void 0) node.color = dataMap.color;
|
|
281
311
|
if (dataMap.style !== void 0) node.style = tryParseJSON(dataMap.style);
|
|
312
|
+
if (dataMap.ports !== void 0) node.ports = tryParseJSON(dataMap.ports);
|
|
282
313
|
return node;
|
|
283
314
|
});
|
|
284
315
|
const edges = asArray(graphEl.edge).map((edgeEl) => {
|
|
@@ -298,6 +329,8 @@ function fromGraphML(xml) {
|
|
|
298
329
|
if (dataMap.height !== void 0) edge.height = parseNumber(dataMap.height);
|
|
299
330
|
if (dataMap.color !== void 0) edge.color = dataMap.color;
|
|
300
331
|
if (dataMap.style !== void 0) edge.style = tryParseJSON(dataMap.style);
|
|
332
|
+
if (dataMap.sourcePort !== void 0) edge.sourcePort = dataMap.sourcePort;
|
|
333
|
+
if (dataMap.targetPort !== void 0) edge.targetPort = dataMap.targetPort;
|
|
301
334
|
return edge;
|
|
302
335
|
});
|
|
303
336
|
const graph = {
|
|
@@ -19,8 +19,10 @@ interface SequenceNodeData {
|
|
|
19
19
|
interface SequenceEdgeData {
|
|
20
20
|
kind: 'message' | 'activation' | 'deactivation';
|
|
21
21
|
stroke?: 'solid' | 'dotted';
|
|
22
|
-
arrowType?: 'filled' | 'open' | 'cross' | 'async';
|
|
22
|
+
arrowType?: 'filled' | 'open' | 'cross' | 'async' | 'half-top' | 'half-bottom' | 'half-reverse-top' | 'half-reverse-bottom' | 'stick-half-top' | 'stick-half-bottom' | 'stick-half-reverse-top' | 'stick-half-reverse-bottom';
|
|
23
23
|
bidirectional?: boolean;
|
|
24
|
+
centralSource?: boolean;
|
|
25
|
+
centralTarget?: boolean;
|
|
24
26
|
sequenceNumber?: number;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
@@ -396,4 +398,46 @@ declare function toMermaidBlock(graph: MermaidBlockGraph): string;
|
|
|
396
398
|
*/
|
|
397
399
|
declare const mermaidBlockConverter: GraphFormatConverter<string, BlockNodeData, BlockEdgeData, BlockGraphData>;
|
|
398
400
|
//#endregion
|
|
399
|
-
|
|
401
|
+
//#region src/formats/mermaid/ishikawa.d.ts
|
|
402
|
+
interface IshikawaNodeData {
|
|
403
|
+
kind: 'effect' | 'cause';
|
|
404
|
+
}
|
|
405
|
+
interface IshikawaEdgeData {}
|
|
406
|
+
interface IshikawaGraphData {
|
|
407
|
+
diagramType: 'ishikawa';
|
|
408
|
+
}
|
|
409
|
+
type MermaidIshikawaGraph = Graph<IshikawaNodeData, IshikawaEdgeData, IshikawaGraphData>;
|
|
410
|
+
/**
|
|
411
|
+
* Parses a Mermaid Ishikawa diagram string into a Graph.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* const graph = fromMermaidIshikawa(`
|
|
415
|
+
* ishikawa-beta
|
|
416
|
+
* Problem
|
|
417
|
+
* Cause
|
|
418
|
+
* Sub-cause
|
|
419
|
+
* `);
|
|
420
|
+
*/
|
|
421
|
+
declare function fromMermaidIshikawa(input: string): MermaidIshikawaGraph;
|
|
422
|
+
/**
|
|
423
|
+
* Converts an Ishikawa Graph to a Mermaid Ishikawa diagram string.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* const mermaid = toMermaidIshikawa(graph);
|
|
427
|
+
* // "ishikawa-beta\nProblem\n Cause"
|
|
428
|
+
*/
|
|
429
|
+
declare function toMermaidIshikawa(graph: MermaidIshikawaGraph): string;
|
|
430
|
+
/**
|
|
431
|
+
* Bidirectional converter for Mermaid Ishikawa diagram format.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* const graph = mermaidIshikawaConverter.from(`
|
|
435
|
+
* ishikawa-beta
|
|
436
|
+
* Problem
|
|
437
|
+
* Cause
|
|
438
|
+
* `);
|
|
439
|
+
* const str = mermaidIshikawaConverter.to(graph);
|
|
440
|
+
*/
|
|
441
|
+
declare const mermaidIshikawaConverter: GraphFormatConverter<string, IshikawaNodeData, IshikawaEdgeData, IshikawaGraphData>;
|
|
442
|
+
//#endregion
|
|
443
|
+
export { type BlockEdgeData, type BlockGraphData, type BlockNodeData, type ClassEdgeData, type ClassGraphData, type ClassNodeData, type EREdgeData, type ERGraphData, type ERNodeData, type FlowchartEdgeData, type FlowchartGraphData, type FlowchartNodeData, type IshikawaEdgeData, type IshikawaGraphData, type IshikawaNodeData, type MermaidBlockGraph, type MermaidClassGraph, type MermaidERGraph, type MermaidFlowchartGraph, type MermaidIshikawaGraph, type MermaidMindmapGraph, type MermaidSequenceGraph, type MermaidStateGraph, type MindmapEdgeData, type MindmapGraphData, type MindmapNodeData, type SequenceBlock, type SequenceEdgeData, type SequenceGraphData, type SequenceNodeData, type StateEdgeData, type StateGraphData, type StateNodeData, fromMermaidBlock, fromMermaidClass, fromMermaidER, fromMermaidFlowchart, fromMermaidIshikawa, fromMermaidMindmap, fromMermaidSequence, fromMermaidState, mermaidBlockConverter, mermaidClassConverter, mermaidERConverter, mermaidFlowchartConverter, mermaidIshikawaConverter, mermaidMindmapConverter, mermaidSequenceConverter, mermaidStateConverter, toMermaidBlock, toMermaidClass, toMermaidER, toMermaidFlowchart, toMermaidIshikawa, toMermaidMindmap, toMermaidSequence, toMermaidState };
|
|
@@ -98,6 +98,46 @@ const ARROW_PATTERNS = [
|
|
|
98
98
|
arrowType: "async",
|
|
99
99
|
bidirectional: false
|
|
100
100
|
}],
|
|
101
|
+
["--|\\", {
|
|
102
|
+
stroke: "dotted",
|
|
103
|
+
arrowType: "half-top",
|
|
104
|
+
bidirectional: false
|
|
105
|
+
}],
|
|
106
|
+
["--|/", {
|
|
107
|
+
stroke: "dotted",
|
|
108
|
+
arrowType: "half-bottom",
|
|
109
|
+
bidirectional: false
|
|
110
|
+
}],
|
|
111
|
+
["/|--", {
|
|
112
|
+
stroke: "dotted",
|
|
113
|
+
arrowType: "half-reverse-top",
|
|
114
|
+
bidirectional: false
|
|
115
|
+
}],
|
|
116
|
+
["\\|--", {
|
|
117
|
+
stroke: "dotted",
|
|
118
|
+
arrowType: "half-reverse-bottom",
|
|
119
|
+
bidirectional: false
|
|
120
|
+
}],
|
|
121
|
+
["--\\\\", {
|
|
122
|
+
stroke: "dotted",
|
|
123
|
+
arrowType: "stick-half-top",
|
|
124
|
+
bidirectional: false
|
|
125
|
+
}],
|
|
126
|
+
["--//", {
|
|
127
|
+
stroke: "dotted",
|
|
128
|
+
arrowType: "stick-half-bottom",
|
|
129
|
+
bidirectional: false
|
|
130
|
+
}],
|
|
131
|
+
["//--", {
|
|
132
|
+
stroke: "dotted",
|
|
133
|
+
arrowType: "stick-half-reverse-top",
|
|
134
|
+
bidirectional: false
|
|
135
|
+
}],
|
|
136
|
+
["\\\\--", {
|
|
137
|
+
stroke: "dotted",
|
|
138
|
+
arrowType: "stick-half-reverse-bottom",
|
|
139
|
+
bidirectional: false
|
|
140
|
+
}],
|
|
101
141
|
["->>", {
|
|
102
142
|
stroke: "solid",
|
|
103
143
|
arrowType: "filled",
|
|
@@ -117,12 +157,56 @@ const ARROW_PATTERNS = [
|
|
|
117
157
|
stroke: "solid",
|
|
118
158
|
arrowType: "async",
|
|
119
159
|
bidirectional: false
|
|
160
|
+
}],
|
|
161
|
+
["-|\\", {
|
|
162
|
+
stroke: "solid",
|
|
163
|
+
arrowType: "half-top",
|
|
164
|
+
bidirectional: false
|
|
165
|
+
}],
|
|
166
|
+
["-|/", {
|
|
167
|
+
stroke: "solid",
|
|
168
|
+
arrowType: "half-bottom",
|
|
169
|
+
bidirectional: false
|
|
170
|
+
}],
|
|
171
|
+
["/|-", {
|
|
172
|
+
stroke: "solid",
|
|
173
|
+
arrowType: "half-reverse-top",
|
|
174
|
+
bidirectional: false
|
|
175
|
+
}],
|
|
176
|
+
["\\|-", {
|
|
177
|
+
stroke: "solid",
|
|
178
|
+
arrowType: "half-reverse-bottom",
|
|
179
|
+
bidirectional: false
|
|
180
|
+
}],
|
|
181
|
+
["-\\\\", {
|
|
182
|
+
stroke: "solid",
|
|
183
|
+
arrowType: "stick-half-top",
|
|
184
|
+
bidirectional: false
|
|
185
|
+
}],
|
|
186
|
+
["-//", {
|
|
187
|
+
stroke: "solid",
|
|
188
|
+
arrowType: "stick-half-bottom",
|
|
189
|
+
bidirectional: false
|
|
190
|
+
}],
|
|
191
|
+
["//-", {
|
|
192
|
+
stroke: "solid",
|
|
193
|
+
arrowType: "stick-half-reverse-top",
|
|
194
|
+
bidirectional: false
|
|
195
|
+
}],
|
|
196
|
+
["\\\\-", {
|
|
197
|
+
stroke: "solid",
|
|
198
|
+
arrowType: "stick-half-reverse-bottom",
|
|
199
|
+
bidirectional: false
|
|
120
200
|
}]
|
|
121
201
|
];
|
|
122
|
-
function
|
|
202
|
+
function getEscapedRegExp(s) {
|
|
203
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
204
|
+
}
|
|
205
|
+
function getParsedArrow(arrow) {
|
|
123
206
|
for (const [pattern, info] of ARROW_PATTERNS) if (arrow === pattern) return info;
|
|
124
207
|
}
|
|
125
|
-
const
|
|
208
|
+
const ARROW_PATTERN_SOURCE = ARROW_PATTERNS.map(([pattern]) => getEscapedRegExp(pattern)).join("|");
|
|
209
|
+
const MESSAGE_RE = /* @__PURE__ */ new RegExp(`^(\\S+?)\\s*(${ARROW_PATTERN_SOURCE})(\\(\\))?\\s*(\\S+?)\\s*:\\s*(.*)$`);
|
|
126
210
|
/**
|
|
127
211
|
* Parses a Mermaid sequence diagram string into a Graph.
|
|
128
212
|
*
|
|
@@ -346,8 +430,14 @@ function fromMermaidSequence(input) {
|
|
|
346
430
|
if (msgMatch) {
|
|
347
431
|
let sourceId = msgMatch[1];
|
|
348
432
|
const arrowStr = msgMatch[2];
|
|
349
|
-
|
|
350
|
-
|
|
433
|
+
const centralTarget = msgMatch[3] === "()";
|
|
434
|
+
let targetId = msgMatch[4];
|
|
435
|
+
const messageText = msgMatch[5].trim();
|
|
436
|
+
let centralSource = false;
|
|
437
|
+
if (sourceId.endsWith("()")) {
|
|
438
|
+
centralSource = true;
|
|
439
|
+
sourceId = sourceId.slice(0, -2);
|
|
440
|
+
}
|
|
351
441
|
let activationOnTarget = null;
|
|
352
442
|
if (targetId.startsWith("+")) {
|
|
353
443
|
activationOnTarget = "activation";
|
|
@@ -366,7 +456,7 @@ function fromMermaidSequence(input) {
|
|
|
366
456
|
}
|
|
367
457
|
ensureNode(sourceId);
|
|
368
458
|
ensureNode(targetId);
|
|
369
|
-
const arrowInfo =
|
|
459
|
+
const arrowInfo = getParsedArrow(arrowStr);
|
|
370
460
|
if (!arrowInfo) continue;
|
|
371
461
|
const edgeId = generateEdgeId(sourceId, targetId, edgeCounter++);
|
|
372
462
|
const data = {
|
|
@@ -374,6 +464,8 @@ function fromMermaidSequence(input) {
|
|
|
374
464
|
stroke: arrowInfo.stroke,
|
|
375
465
|
arrowType: arrowInfo.arrowType,
|
|
376
466
|
...arrowInfo.bidirectional && { bidirectional: true },
|
|
467
|
+
...centralSource && { centralSource: true },
|
|
468
|
+
...centralTarget && { centralTarget: true },
|
|
377
469
|
...autonumber && { sequenceNumber: ++seqNum }
|
|
378
470
|
};
|
|
379
471
|
addEdge({
|
|
@@ -464,13 +556,29 @@ const ARROW_MAP = {
|
|
|
464
556
|
open: "->",
|
|
465
557
|
filled: "->>",
|
|
466
558
|
cross: "-x",
|
|
467
|
-
async: "-)"
|
|
559
|
+
async: "-)",
|
|
560
|
+
"half-top": "-|\\",
|
|
561
|
+
"half-bottom": "-|/",
|
|
562
|
+
"half-reverse-top": "/|-",
|
|
563
|
+
"half-reverse-bottom": "\\|-",
|
|
564
|
+
"stick-half-top": "-\\\\",
|
|
565
|
+
"stick-half-bottom": "-//",
|
|
566
|
+
"stick-half-reverse-top": "//-",
|
|
567
|
+
"stick-half-reverse-bottom": "\\\\-"
|
|
468
568
|
},
|
|
469
569
|
dotted: {
|
|
470
570
|
open: "-->",
|
|
471
571
|
filled: "-->>",
|
|
472
572
|
cross: "--x",
|
|
473
|
-
async: "--)"
|
|
573
|
+
async: "--)",
|
|
574
|
+
"half-top": "--|\\",
|
|
575
|
+
"half-bottom": "--|/",
|
|
576
|
+
"half-reverse-top": "/|--",
|
|
577
|
+
"half-reverse-bottom": "\\|--",
|
|
578
|
+
"stick-half-top": "--\\\\",
|
|
579
|
+
"stick-half-bottom": "--//",
|
|
580
|
+
"stick-half-reverse-top": "//--",
|
|
581
|
+
"stick-half-reverse-bottom": "\\\\--"
|
|
474
582
|
}
|
|
475
583
|
};
|
|
476
584
|
/**
|
|
@@ -672,8 +780,9 @@ function toMermaidSequence(graph) {
|
|
|
672
780
|
let arrow;
|
|
673
781
|
if (d.bidirectional) arrow = stroke === "dotted" ? "<<-->>" : "<<->>";
|
|
674
782
|
else arrow = ARROW_MAP[stroke]?.[arrowType] ?? "->>";
|
|
783
|
+
if (d.centralTarget) arrow += "()";
|
|
675
784
|
const label = edge.label ? `: ${escapeMermaidLabel(edge.label)}` : ":";
|
|
676
|
-
lines.push(`${indent()}${edge.sourceId}${arrow}${edge.targetId}${label}`);
|
|
785
|
+
lines.push(`${indent()}${edge.sourceId}${d.centralSource ? "()" : ""}${arrow}${edge.targetId}${label}`);
|
|
677
786
|
}
|
|
678
787
|
const afters = afterEdge.get(edge.id);
|
|
679
788
|
if (afters) for (const _ev of afters) {
|
|
@@ -1987,12 +2096,14 @@ const mermaidClassConverter = createFormatConverter(toMermaidClass, fromMermaidC
|
|
|
1987
2096
|
//#region src/formats/mermaid/er-diagram.ts
|
|
1988
2097
|
const LEFT_CARDINALITY = {
|
|
1989
2098
|
"||": "one",
|
|
2099
|
+
"1": "one",
|
|
1990
2100
|
"|o": "zero-or-one",
|
|
1991
2101
|
"}|": "one-or-more",
|
|
1992
2102
|
"}o": "zero-or-more"
|
|
1993
2103
|
};
|
|
1994
2104
|
const RIGHT_CARDINALITY = {
|
|
1995
2105
|
"||": "one",
|
|
2106
|
+
"1": "one",
|
|
1996
2107
|
"o|": "zero-or-one",
|
|
1997
2108
|
"|{": "one-or-more",
|
|
1998
2109
|
"o{": "zero-or-more"
|
|
@@ -2009,25 +2120,18 @@ const CARDINALITY_TO_RIGHT = {
|
|
|
2009
2120
|
"one-or-more": "|{",
|
|
2010
2121
|
"zero-or-more": "o{"
|
|
2011
2122
|
};
|
|
2012
|
-
function
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
else if (mid === "..") identifying = false;
|
|
2023
|
-
else return null;
|
|
2024
|
-
return {
|
|
2025
|
-
sourceCardinality: srcCard,
|
|
2026
|
-
targetCardinality: tgtCard,
|
|
2027
|
-
identifying
|
|
2028
|
-
};
|
|
2123
|
+
function getParsedERRelationship(symbol) {
|
|
2124
|
+
for (const left of Object.keys(LEFT_CARDINALITY).sort((a, b) => b.length - a.length)) for (const mid of ["--", ".."]) for (const right of Object.keys(RIGHT_CARDINALITY).sort((a, b) => b.length - a.length)) {
|
|
2125
|
+
if (symbol !== `${left}${mid}${right}`) continue;
|
|
2126
|
+
return {
|
|
2127
|
+
sourceCardinality: LEFT_CARDINALITY[left],
|
|
2128
|
+
targetCardinality: RIGHT_CARDINALITY[right],
|
|
2129
|
+
identifying: mid === "--"
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
return null;
|
|
2029
2133
|
}
|
|
2030
|
-
const ER_LINE_RE = /^(\S+)\s+([|}{
|
|
2134
|
+
const ER_LINE_RE = /^(\S+)\s+([|}{o1.]{1,2}[-.][-.][|}{o1.]{1,2})\s+(\S+)\s*:\s*"?([^"]*)"?\s*$/;
|
|
2031
2135
|
/**
|
|
2032
2136
|
* Parses a Mermaid ER diagram string into a Graph.
|
|
2033
2137
|
*
|
|
@@ -2099,7 +2203,7 @@ function fromMermaidER(input) {
|
|
|
2099
2203
|
const label = relMatch[4].trim();
|
|
2100
2204
|
ensureNode(leftEntity);
|
|
2101
2205
|
ensureNode(rightEntity);
|
|
2102
|
-
const rel =
|
|
2206
|
+
const rel = getParsedERRelationship(symbol);
|
|
2103
2207
|
if (rel) {
|
|
2104
2208
|
const edgeId = generateEdgeId(leftEntity, rightEntity, edgeCounter++);
|
|
2105
2209
|
edges.push({
|
|
@@ -2532,4 +2636,105 @@ function toMermaidBlock(graph) {
|
|
|
2532
2636
|
const mermaidBlockConverter = createFormatConverter(toMermaidBlock, fromMermaidBlock);
|
|
2533
2637
|
|
|
2534
2638
|
//#endregion
|
|
2535
|
-
|
|
2639
|
+
//#region src/formats/mermaid/ishikawa.ts
|
|
2640
|
+
/**
|
|
2641
|
+
* Parses a Mermaid Ishikawa diagram string into a Graph.
|
|
2642
|
+
*
|
|
2643
|
+
* @example
|
|
2644
|
+
* const graph = fromMermaidIshikawa(`
|
|
2645
|
+
* ishikawa-beta
|
|
2646
|
+
* Problem
|
|
2647
|
+
* Cause
|
|
2648
|
+
* Sub-cause
|
|
2649
|
+
* `);
|
|
2650
|
+
*/
|
|
2651
|
+
function fromMermaidIshikawa(input) {
|
|
2652
|
+
validateInput(input, "Mermaid Ishikawa");
|
|
2653
|
+
const { lines } = prepareLines(input);
|
|
2654
|
+
const header = lines[0]?.trim();
|
|
2655
|
+
if (!header || !header.startsWith("ishikawa-beta")) throw new Error("Mermaid Ishikawa: expected \"ishikawa-beta\" header");
|
|
2656
|
+
const nodes = [];
|
|
2657
|
+
const edges = [];
|
|
2658
|
+
const stack = [];
|
|
2659
|
+
let nodeCounter = 0;
|
|
2660
|
+
let edgeCounter = 0;
|
|
2661
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2662
|
+
const rawLine = lines[i];
|
|
2663
|
+
if (!rawLine.trim()) continue;
|
|
2664
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
2665
|
+
const label = rawLine.trim();
|
|
2666
|
+
const id = `ish_${nodeCounter++}`;
|
|
2667
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
2668
|
+
const parent = stack[stack.length - 1];
|
|
2669
|
+
const node = {
|
|
2670
|
+
type: "node",
|
|
2671
|
+
id,
|
|
2672
|
+
parentId: parent?.id ?? null,
|
|
2673
|
+
initialNodeId: null,
|
|
2674
|
+
label,
|
|
2675
|
+
data: { kind: parent ? "cause" : "effect" }
|
|
2676
|
+
};
|
|
2677
|
+
if (parent) edges.push({
|
|
2678
|
+
type: "edge",
|
|
2679
|
+
id: generateEdgeId(parent.id, id, edgeCounter++),
|
|
2680
|
+
sourceId: parent.id,
|
|
2681
|
+
targetId: id,
|
|
2682
|
+
label: "",
|
|
2683
|
+
data: {}
|
|
2684
|
+
});
|
|
2685
|
+
nodes.push(node);
|
|
2686
|
+
stack.push({
|
|
2687
|
+
id,
|
|
2688
|
+
indent
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
return {
|
|
2692
|
+
id: "",
|
|
2693
|
+
type: "directed",
|
|
2694
|
+
initialNodeId: null,
|
|
2695
|
+
nodes,
|
|
2696
|
+
edges,
|
|
2697
|
+
data: { diagramType: "ishikawa" }
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Converts an Ishikawa Graph to a Mermaid Ishikawa diagram string.
|
|
2702
|
+
*
|
|
2703
|
+
* @example
|
|
2704
|
+
* const mermaid = toMermaidIshikawa(graph);
|
|
2705
|
+
* // "ishikawa-beta\nProblem\n Cause"
|
|
2706
|
+
*/
|
|
2707
|
+
function toMermaidIshikawa(graph) {
|
|
2708
|
+
const lines = ["ishikawa-beta"];
|
|
2709
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
2710
|
+
for (const node of graph.nodes) {
|
|
2711
|
+
const parentId = node.parentId ?? null;
|
|
2712
|
+
const children = childrenMap.get(parentId) ?? [];
|
|
2713
|
+
children.push(node);
|
|
2714
|
+
childrenMap.set(parentId, children);
|
|
2715
|
+
}
|
|
2716
|
+
const addIshikawaNodes = (parentId, depth) => {
|
|
2717
|
+
for (const node of childrenMap.get(parentId) ?? []) {
|
|
2718
|
+
const indent = " ".repeat(depth);
|
|
2719
|
+
lines.push(`${indent}${escapeMermaidLabel(node.label ?? node.id)}`);
|
|
2720
|
+
addIshikawaNodes(node.id, depth + 1);
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
addIshikawaNodes(null, 0);
|
|
2724
|
+
return lines.join("\n");
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Bidirectional converter for Mermaid Ishikawa diagram format.
|
|
2728
|
+
*
|
|
2729
|
+
* @example
|
|
2730
|
+
* const graph = mermaidIshikawaConverter.from(`
|
|
2731
|
+
* ishikawa-beta
|
|
2732
|
+
* Problem
|
|
2733
|
+
* Cause
|
|
2734
|
+
* `);
|
|
2735
|
+
* const str = mermaidIshikawaConverter.to(graph);
|
|
2736
|
+
*/
|
|
2737
|
+
const mermaidIshikawaConverter = createFormatConverter(toMermaidIshikawa, fromMermaidIshikawa);
|
|
2738
|
+
|
|
2739
|
+
//#endregion
|
|
2740
|
+
export { fromMermaidBlock, fromMermaidClass, fromMermaidER, fromMermaidFlowchart, fromMermaidIshikawa, fromMermaidMindmap, fromMermaidSequence, fromMermaidState, mermaidBlockConverter, mermaidClassConverter, mermaidERConverter, mermaidFlowchartConverter, mermaidIshikawaConverter, mermaidMindmapConverter, mermaidSequenceConverter, mermaidStateConverter, toMermaidBlock, toMermaidClass, toMermaidER, toMermaidFlowchart, toMermaidIshikawa, toMermaidMindmap, toMermaidSequence, toMermaidState };
|
package/dist/schemas.d.mts
CHANGED
|
@@ -21,7 +21,7 @@ declare const NodeSchema: z.ZodObject<{
|
|
|
21
21
|
id: z.ZodString;
|
|
22
22
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
23
23
|
initialNodeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
-
label: z.ZodOptional<z.ZodString
|
|
24
|
+
label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
25
|
data: z.ZodAny;
|
|
26
26
|
x: z.ZodOptional<z.ZodNumber>;
|
|
27
27
|
y: z.ZodOptional<z.ZodNumber>;
|
|
@@ -75,7 +75,7 @@ declare const GraphSchema: z.ZodObject<{
|
|
|
75
75
|
id: z.ZodString;
|
|
76
76
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
77
77
|
initialNodeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
78
|
-
label: z.ZodOptional<z.ZodString
|
|
78
|
+
label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
79
79
|
data: z.ZodAny;
|
|
80
80
|
x: z.ZodOptional<z.ZodNumber>;
|
|
81
81
|
y: z.ZodOptional<z.ZodNumber>;
|
package/dist/schemas.mjs
CHANGED
|
@@ -23,7 +23,7 @@ const NodeSchema = z.object({
|
|
|
23
23
|
id: z.string(),
|
|
24
24
|
parentId: z.string().nullable().optional(),
|
|
25
25
|
initialNodeId: z.string().nullable().optional(),
|
|
26
|
-
label: z.string().optional(),
|
|
26
|
+
label: z.string().nullable().optional(),
|
|
27
27
|
data: z.any(),
|
|
28
28
|
x: z.number().optional(),
|
|
29
29
|
y: z.number().optional(),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/graph",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.1",
|
|
5
5
|
"description": "A TypeScript-first graph library with plain JSON-serializable objects",
|
|
6
6
|
"author": "David Khourshid <david@stately.ai>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
"build": "tsdown",
|
|
105
105
|
"bench": "vitest bench --run",
|
|
106
106
|
"dev": "tsdown --watch",
|
|
107
|
+
"fix:generated": "pnpm generate-schema",
|
|
107
108
|
"test": "vitest",
|
|
108
109
|
"typecheck": "tsc --noEmit",
|
|
109
110
|
"check:generated": "tsx scripts/generate-json-schema.ts --check",
|