@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 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
- Includes traversal (BFS, DFS), pathfinding (shortest path, simple paths, all-pairs shortest paths), 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.
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
- **Supported formats:** Cytoscape.js JSON, D3.js JSON, JSON Graph Format, GEXF, GraphML, GML, TGF, DOT, Mermaid (flowchart, state, sequence, class, ER, mindmap, block), adjacency list, and edge list.
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
- - [GraphML](./src/formats/graphml/README.md)
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
@@ -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
- 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 MermaidBlockGraph, type MermaidClassGraph, type MermaidERGraph, type MermaidFlowchartGraph, 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, fromMermaidMindmap, fromMermaidSequence, fromMermaidState, mermaidBlockConverter, mermaidClassConverter, mermaidERConverter, mermaidFlowchartConverter, mermaidMindmapConverter, mermaidSequenceConverter, mermaidStateConverter, toMermaidBlock, toMermaidClass, toMermaidER, toMermaidFlowchart, toMermaidMindmap, toMermaidSequence, toMermaidState };
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 parseArrow(arrow) {
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 MESSAGE_RE = /^(\S+?)\s*(<<-->>|<<->>|-->>|-->|--x|--\)|->>|->|-x|-\))\s*(\S+?)\s*:\s*(.*)$/;
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
- let targetId = msgMatch[3];
350
- const messageText = msgMatch[4].trim();
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 = parseArrow(arrowStr);
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 parseERRelationship(symbol) {
2013
- if (symbol.length < 6) return null;
2014
- const left = symbol.slice(0, 2);
2015
- const mid = symbol.slice(2, 4);
2016
- const right = symbol.slice(4, 6);
2017
- const srcCard = LEFT_CARDINALITY[left];
2018
- const tgtCard = RIGHT_CARDINALITY[right];
2019
- if (!srcCard || !tgtCard) return null;
2020
- let identifying;
2021
- if (mid === "--") identifying = true;
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+([|}{o.][|}{o.][-.][-.][|}{o.][|}{o.])\s+(\S+)\s*:\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 = parseERRelationship(symbol);
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
- export { fromMermaidBlock, fromMermaidClass, fromMermaidER, fromMermaidFlowchart, fromMermaidMindmap, fromMermaidSequence, fromMermaidState, mermaidBlockConverter, mermaidClassConverter, mermaidERConverter, mermaidFlowchartConverter, mermaidMindmapConverter, mermaidSequenceConverter, mermaidStateConverter, toMermaidBlock, toMermaidClass, toMermaidER, toMermaidFlowchart, toMermaidMindmap, toMermaidSequence, toMermaidState };
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 };
@@ -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.0",
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",
@@ -55,7 +55,14 @@
55
55
  ]
56
56
  },
57
57
  "label": {
58
- "type": "string"
58
+ "anyOf": [
59
+ {
60
+ "type": "string"
61
+ },
62
+ {
63
+ "type": "null"
64
+ }
65
+ ]
59
66
  },
60
67
  "data": {},
61
68
  "x": {
@@ -30,7 +30,14 @@
30
30
  ]
31
31
  },
32
32
  "label": {
33
- "type": "string"
33
+ "anyOf": [
34
+ {
35
+ "type": "string"
36
+ },
37
+ {
38
+ "type": "null"
39
+ }
40
+ ]
34
41
  },
35
42
  "data": {},
36
43
  "x": {