@statelyai/graph 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -42
- package/dist/format-support.mjs +18 -18
- package/dist/formats/cytoscape/index.mjs +8 -2
- package/dist/formats/d3/index.mjs +8 -2
- package/dist/formats/dot/index.mjs +18 -6
- package/dist/formats/gexf/index.mjs +46 -17
- package/dist/formats/gml/index.mjs +6 -0
- package/dist/formats/jgf/index.mjs +8 -2
- package/dist/schemas.d.mts +15 -1
- package/dist/schemas.mjs +34 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: '@statelyai/graph'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
1
|
# @statelyai/graph
|
|
6
2
|
|
|
7
3
|
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.
|
|
@@ -18,14 +14,14 @@ Optional peers are only needed for specific adapters:
|
|
|
18
14
|
|
|
19
15
|
<!-- optional peer dependencies derived from package.json#peerDependencies -->
|
|
20
16
|
|
|
21
|
-
| Package
|
|
22
|
-
|
|
|
17
|
+
| Package | Needed for |
|
|
18
|
+
| ----------------- | --------------------------------------------------- |
|
|
23
19
|
| `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
|
|
24
|
-
| `dotparser`
|
|
25
|
-
| `cytoscape`
|
|
26
|
-
| `d3-force`
|
|
27
|
-
| `elkjs`
|
|
28
|
-
| `zod`
|
|
20
|
+
| `dotparser` | `@statelyai/graph/dot` parsing |
|
|
21
|
+
| `cytoscape` | Cytoscape integration tests and consumer typing |
|
|
22
|
+
| `d3-force` | D3 force integration tests and consumer typing |
|
|
23
|
+
| `elkjs` | `@statelyai/graph/elk` |
|
|
24
|
+
| `zod` | `@statelyai/graph/schemas` |
|
|
29
25
|
|
|
30
26
|
## Highlights
|
|
31
27
|
|
|
@@ -43,7 +39,12 @@ Optional peers are only needed for specific adapters:
|
|
|
43
39
|
Graphs are plain JSON-serializable objects. All operations are standalone functions — no classes, no DOM, no rendering engine.
|
|
44
40
|
|
|
45
41
|
```ts
|
|
46
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
createGraph,
|
|
44
|
+
addNode,
|
|
45
|
+
addEdge,
|
|
46
|
+
getShortestPath,
|
|
47
|
+
} from '@statelyai/graph';
|
|
47
48
|
|
|
48
49
|
const graph = createGraph({
|
|
49
50
|
nodes: [
|
|
@@ -70,12 +71,17 @@ const path = getShortestPath(graph, { from: 'a', to: 'c' });
|
|
|
70
71
|
Look up, add, delete, and update nodes and edges. Query neighbors, predecessors, successors, degree, and more.
|
|
71
72
|
|
|
72
73
|
```ts
|
|
73
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
getNode,
|
|
76
|
+
deleteNode,
|
|
77
|
+
getNeighbors,
|
|
78
|
+
getSources,
|
|
79
|
+
} from '@statelyai/graph';
|
|
74
80
|
|
|
75
|
-
const node = getNode(graph, 'a');
|
|
76
|
-
deleteNode(graph, 'd');
|
|
81
|
+
const node = getNode(graph, 'a'); // lookup by id
|
|
82
|
+
deleteNode(graph, 'd'); // removes node + connected edges
|
|
77
83
|
const neighbors = getNeighbors(graph, 'a'); // adjacent nodes
|
|
78
|
-
const roots = getSources(graph);
|
|
84
|
+
const roots = getSources(graph); // nodes with no incoming edges
|
|
79
85
|
```
|
|
80
86
|
|
|
81
87
|
Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once.
|
|
@@ -96,14 +102,14 @@ const graph = createGraph({
|
|
|
96
102
|
{ id: 'c' },
|
|
97
103
|
],
|
|
98
104
|
edges: [
|
|
99
|
-
{ id: 'e1', sourceId: 'a', targetId: 'b' },
|
|
105
|
+
{ id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1
|
|
100
106
|
{ id: 'e2', sourceId: 'b1', targetId: 'b2' },
|
|
101
|
-
{ id: 'e3', sourceId: 'b', targetId: 'c' },
|
|
107
|
+
{ id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b
|
|
102
108
|
],
|
|
103
109
|
});
|
|
104
110
|
|
|
105
111
|
const children = getChildren(graph, 'b'); // [b1, b2]
|
|
106
|
-
const flat = flatten(graph);
|
|
112
|
+
const flat = flatten(graph); // only leaf nodes, edges resolved
|
|
107
113
|
```
|
|
108
114
|
|
|
109
115
|
## Ports
|
|
@@ -139,6 +145,26 @@ getPorts(graph, 'fetch'); // [{ name: 'result', ... }]
|
|
|
139
145
|
getEdgesByPort(graph, 'render', 'input'); // [e1]
|
|
140
146
|
```
|
|
141
147
|
|
|
148
|
+
## Schema Validation
|
|
149
|
+
|
|
150
|
+
<!-- validation helpers exported from src/schemas.ts -->
|
|
151
|
+
|
|
152
|
+
Use the `@statelyai/graph/schemas` subpath when you want runtime validation or JSON Schema generation.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { GraphSchema, getGraphIssues, isGraph } from '@statelyai/graph/schemas';
|
|
156
|
+
|
|
157
|
+
const unknownValue: unknown = JSON.parse(input);
|
|
158
|
+
|
|
159
|
+
if (isGraph(unknownValue)) {
|
|
160
|
+
// fully typed Graph
|
|
161
|
+
} else {
|
|
162
|
+
console.error(getGraphIssues(unknownValue));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const parsed = GraphSchema.parse(unknownValue);
|
|
166
|
+
```
|
|
167
|
+
|
|
142
168
|
## Algorithms
|
|
143
169
|
|
|
144
170
|
<!-- algorithm functions exported from src/algorithms.ts -->
|
|
@@ -147,27 +173,40 @@ Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, s
|
|
|
147
173
|
|
|
148
174
|
```ts
|
|
149
175
|
import {
|
|
150
|
-
bfs,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
bfs,
|
|
177
|
+
dfs,
|
|
178
|
+
hasPath,
|
|
179
|
+
isAcyclic,
|
|
180
|
+
getShortestPath,
|
|
181
|
+
getCycles,
|
|
182
|
+
getTopologicalSort,
|
|
183
|
+
getConnectedComponents,
|
|
184
|
+
getMinimumSpanningTree,
|
|
185
|
+
getPageRank,
|
|
186
|
+
getLabelPropagationCommunities,
|
|
187
|
+
genGirvanNewmanCommunities,
|
|
188
|
+
getBridges,
|
|
189
|
+
isIsomorphic,
|
|
155
190
|
} from '@statelyai/graph';
|
|
156
191
|
|
|
157
|
-
for (const node of bfs(graph, 'a')) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
for (const node of bfs(graph, 'a')) {
|
|
193
|
+
/* breadth-first */
|
|
194
|
+
}
|
|
195
|
+
for (const node of dfs(graph, 'a')) {
|
|
196
|
+
/* depth-first */
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
hasPath(graph, 'a', 'c'); // reachability
|
|
200
|
+
isAcyclic(graph); // cycle check
|
|
201
|
+
getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path
|
|
202
|
+
getTopologicalSort(graph); // topological order (or null)
|
|
203
|
+
getConnectedComponents(graph); // connected components
|
|
204
|
+
getMinimumSpanningTree(graph, { weight: (e) => e.data?.weight ?? 1 }); // MST
|
|
205
|
+
getPageRank(graph); // link analysis scores
|
|
206
|
+
getLabelPropagationCommunities(graph); // community detection
|
|
207
|
+
[...genGirvanNewmanCommunities(graph)]; // lazy community splits
|
|
208
|
+
getBridges(graph); // bridge edges
|
|
209
|
+
isIsomorphic(graph, otherGraph); // structural equivalence
|
|
171
210
|
```
|
|
172
211
|
|
|
173
212
|
## Diff & Walks
|
|
@@ -205,10 +244,10 @@ import { fromGEXF } from '@statelyai/graph/gexf';
|
|
|
205
244
|
import { toCytoscapeJSON } from '@statelyai/graph/cytoscape';
|
|
206
245
|
import { toD3Graph } from '@statelyai/graph/d3';
|
|
207
246
|
|
|
208
|
-
const dot = toDOT(graph);
|
|
209
|
-
const cytoData = toCytoscapeJSON(graph);
|
|
210
|
-
const d3Data = toD3Graph(graph);
|
|
211
|
-
const imported = fromGEXF(gexfXmlString);
|
|
247
|
+
const dot = toDOT(graph); // Graphviz DOT
|
|
248
|
+
const cytoData = toCytoscapeJSON(graph); // Cytoscape.js JSON
|
|
249
|
+
const d3Data = toD3Graph(graph); // D3.js { nodes, links }
|
|
250
|
+
const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
|
|
212
251
|
```
|
|
213
252
|
|
|
214
253
|
<!-- supported format adapters derived from src/formats/* subdirectories -->
|
|
@@ -224,6 +263,33 @@ const cyto = cytoscapeConverter.to(graph);
|
|
|
224
263
|
const back = cytoscapeConverter.from(cyto);
|
|
225
264
|
```
|
|
226
265
|
|
|
266
|
+
## Format Support
|
|
267
|
+
|
|
268
|
+
<!-- format support matrix derived from src/formats/support.ts -->
|
|
269
|
+
|
|
270
|
+
| Format | Hierarchy | Ports | Visual | Round-trip | Notes |
|
|
271
|
+
| ------------------- | --------- | ------- | ------- | ---------- | -------------------------------------------------------------------------- |
|
|
272
|
+
| `adjacency-list` | none | none | none | partial | Connectivity only; edge metadata is lost. |
|
|
273
|
+
| `cytoscape` | full | full | partial | partial | Ports round-trip through element data. |
|
|
274
|
+
| `d3` | none | full | partial | partial | Ports round-trip through node/link objects. |
|
|
275
|
+
| `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
|
|
276
|
+
| `edge-list` | none | none | none | partial | Endpoints only. |
|
|
277
|
+
| `elk` | full | full | full | partial | Best for layout exchange. |
|
|
278
|
+
| `gexf` | full | full | partial | partial | Ports round-trip via custom attributes. |
|
|
279
|
+
| `gml` | full | full | partial | partial | Ports round-trip through JSON-stringified metadata. |
|
|
280
|
+
| `graphml` | full | full | partial | partial | Ports round-trip through `<data>` fields. |
|
|
281
|
+
| `jgf` | full | full | none | partial | Ports round-trip through metadata. |
|
|
282
|
+
| `tgf` | none | none | none | partial | Minimal ids and labels only. |
|
|
283
|
+
| `xyflow` | none | full | full | partial | Ports map directly to handles. |
|
|
284
|
+
| `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
|
|
285
|
+
| `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
|
|
286
|
+
| `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
|
|
287
|
+
| `mermaid/flowchart` | partial | none | partial | partial | `linkStyle` indices are fragile. |
|
|
288
|
+
| `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
|
|
289
|
+
| `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
|
|
290
|
+
| `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
|
|
291
|
+
| `mermaid/state` | full | none | partial | partial | State notes are still lossy. |
|
|
292
|
+
|
|
227
293
|
Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML) and `dotparser` (DOT). All other formats are dependency-free.
|
|
228
294
|
|
|
229
295
|
Format-specific docs live alongside the source:
|
package/dist/format-support.mjs
CHANGED
|
@@ -21,14 +21,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
21
21
|
features: {
|
|
22
22
|
directed: "full",
|
|
23
23
|
undirected: "full",
|
|
24
|
-
hierarchy: "
|
|
25
|
-
ports: "
|
|
24
|
+
hierarchy: "full",
|
|
25
|
+
ports: "full",
|
|
26
26
|
visual: "partial",
|
|
27
27
|
style: "partial",
|
|
28
28
|
weight: "full",
|
|
29
29
|
roundTrip: "partial"
|
|
30
30
|
},
|
|
31
|
-
notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity."]
|
|
31
|
+
notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity.", "Ports round-trip through element data as `ports`, `sourcePort`, and `targetPort`."]
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
id: "d3",
|
|
@@ -37,13 +37,13 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
37
37
|
directed: "full",
|
|
38
38
|
undirected: "full",
|
|
39
39
|
hierarchy: "none",
|
|
40
|
-
ports: "
|
|
40
|
+
ports: "full",
|
|
41
41
|
visual: "partial",
|
|
42
42
|
style: "none",
|
|
43
43
|
weight: "full",
|
|
44
44
|
roundTrip: "partial"
|
|
45
45
|
},
|
|
46
|
-
notes: ["Targets force-graph structures, not compound graph metadata."]
|
|
46
|
+
notes: ["Targets force-graph structures, not compound graph metadata.", "Ports round-trip through node/link objects."]
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
id: "dot",
|
|
@@ -58,7 +58,7 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
58
58
|
weight: "none",
|
|
59
59
|
roundTrip: "partial"
|
|
60
60
|
},
|
|
61
|
-
notes: ["
|
|
61
|
+
notes: ["Edge port ids round-trip, but compass points and node port definitions are not mapped.", "HTML labels and layout hints beyond `rankdir` are lossy."]
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
id: "edge-list",
|
|
@@ -96,14 +96,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
96
96
|
features: {
|
|
97
97
|
directed: "full",
|
|
98
98
|
undirected: "full",
|
|
99
|
-
hierarchy: "
|
|
100
|
-
ports: "
|
|
99
|
+
hierarchy: "full",
|
|
100
|
+
ports: "full",
|
|
101
101
|
visual: "partial",
|
|
102
102
|
style: "partial",
|
|
103
103
|
weight: "full",
|
|
104
104
|
roundTrip: "partial"
|
|
105
105
|
},
|
|
106
|
-
notes: ["Attribute and viz extensions round-trip partially."]
|
|
106
|
+
notes: ["Attribute and viz extensions round-trip partially.", "Ports round-trip via custom node/edge attributes."]
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
id: "gml",
|
|
@@ -111,14 +111,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
111
111
|
features: {
|
|
112
112
|
directed: "full",
|
|
113
113
|
undirected: "full",
|
|
114
|
-
hierarchy: "
|
|
115
|
-
ports: "
|
|
114
|
+
hierarchy: "full",
|
|
115
|
+
ports: "full",
|
|
116
116
|
visual: "partial",
|
|
117
117
|
style: "partial",
|
|
118
118
|
weight: "full",
|
|
119
119
|
roundTrip: "partial"
|
|
120
120
|
},
|
|
121
|
-
notes: ["GML support focuses on common graph/node/edge attributes."]
|
|
121
|
+
notes: ["GML support focuses on common graph/node/edge attributes.", "Ports round-trip through JSON-stringified node metadata and edge fields."]
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
id: "graphml",
|
|
@@ -126,14 +126,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
126
126
|
features: {
|
|
127
127
|
directed: "full",
|
|
128
128
|
undirected: "full",
|
|
129
|
-
hierarchy: "
|
|
130
|
-
ports: "
|
|
129
|
+
hierarchy: "full",
|
|
130
|
+
ports: "full",
|
|
131
131
|
visual: "partial",
|
|
132
132
|
style: "partial",
|
|
133
133
|
weight: "full",
|
|
134
134
|
roundTrip: "partial"
|
|
135
135
|
},
|
|
136
|
-
notes: ["GraphML attribute fidelity is good, but not every extension is represented."]
|
|
136
|
+
notes: ["GraphML attribute fidelity is good, but not every extension is represented.", "Ports round-trip through node and edge `<data>` fields."]
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
id: "jgf",
|
|
@@ -141,14 +141,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
141
141
|
features: {
|
|
142
142
|
directed: "full",
|
|
143
143
|
undirected: "full",
|
|
144
|
-
hierarchy: "
|
|
145
|
-
ports: "
|
|
144
|
+
hierarchy: "full",
|
|
145
|
+
ports: "full",
|
|
146
146
|
visual: "none",
|
|
147
147
|
style: "none",
|
|
148
148
|
weight: "full",
|
|
149
149
|
roundTrip: "partial"
|
|
150
150
|
},
|
|
151
|
-
notes: ["JGF preserves core graph structure and data, but not layout primitives."]
|
|
151
|
+
notes: ["JGF preserves core graph structure and data, but not layout primitives.", "Ports round-trip through node and edge metadata."]
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
id: "tgf",
|
|
@@ -38,6 +38,7 @@ function toCytoscapeJSON(graph) {
|
|
|
38
38
|
if (n.height !== void 0) data.height = n.height;
|
|
39
39
|
if (n.shape) data.shape = n.shape;
|
|
40
40
|
if (n.color) data.color = n.color;
|
|
41
|
+
if (n.ports !== void 0) data.ports = n.ports;
|
|
41
42
|
const node = { data };
|
|
42
43
|
if (n.x !== void 0 && n.y !== void 0) node.position = {
|
|
43
44
|
x: n.x,
|
|
@@ -54,6 +55,8 @@ function toCytoscapeJSON(graph) {
|
|
|
54
55
|
if (e.label) data.label = e.label;
|
|
55
56
|
if (e.data !== void 0) data.edgeData = e.data;
|
|
56
57
|
if (e.color) data.color = e.color;
|
|
58
|
+
if (e.sourcePort !== void 0) data.sourcePort = e.sourcePort;
|
|
59
|
+
if (e.targetPort !== void 0) data.targetPort = e.targetPort;
|
|
57
60
|
return { data };
|
|
58
61
|
})
|
|
59
62
|
}
|
|
@@ -99,7 +102,8 @@ function fromCytoscapeJSON(cyto) {
|
|
|
99
102
|
...n.data.width !== void 0 && { width: n.data.width },
|
|
100
103
|
...n.data.height !== void 0 && { height: n.data.height },
|
|
101
104
|
...n.data.shape && { shape: n.data.shape },
|
|
102
|
-
...n.data.color && { color: n.data.color }
|
|
105
|
+
...n.data.color && { color: n.data.color },
|
|
106
|
+
...n.data.ports !== void 0 && { ports: n.data.ports }
|
|
103
107
|
})),
|
|
104
108
|
edges: cyto.elements.edges.map((e, i) => ({
|
|
105
109
|
type: "edge",
|
|
@@ -108,7 +112,9 @@ function fromCytoscapeJSON(cyto) {
|
|
|
108
112
|
targetId: e.data.target,
|
|
109
113
|
label: e.data.label ?? "",
|
|
110
114
|
data: e.data.edgeData,
|
|
111
|
-
...e.data.color && { color: e.data.color }
|
|
115
|
+
...e.data.color && { color: e.data.color },
|
|
116
|
+
...e.data.sourcePort !== void 0 && { sourcePort: e.data.sourcePort },
|
|
117
|
+
...e.data.targetPort !== void 0 && { targetPort: e.data.targetPort }
|
|
112
118
|
}))
|
|
113
119
|
};
|
|
114
120
|
}
|
|
@@ -28,6 +28,7 @@ function toD3Graph(graph) {
|
|
|
28
28
|
if (n.y !== void 0) node.y = n.y;
|
|
29
29
|
if (n.color) node.color = n.color;
|
|
30
30
|
if (n.shape) node.shape = n.shape;
|
|
31
|
+
if (n.ports !== void 0) node.ports = n.ports;
|
|
31
32
|
return node;
|
|
32
33
|
}),
|
|
33
34
|
links: graph.edges.map((e) => {
|
|
@@ -39,6 +40,8 @@ function toD3Graph(graph) {
|
|
|
39
40
|
if (e.label) link.label = e.label;
|
|
40
41
|
if (e.data !== void 0) link.data = e.data;
|
|
41
42
|
if (e.color) link.color = e.color;
|
|
43
|
+
if (e.sourcePort !== void 0) link.sourcePort = e.sourcePort;
|
|
44
|
+
if (e.targetPort !== void 0) link.targetPort = e.targetPort;
|
|
42
45
|
return link;
|
|
43
46
|
})
|
|
44
47
|
};
|
|
@@ -75,7 +78,8 @@ function fromD3Graph(d3) {
|
|
|
75
78
|
...n.x !== void 0 && { x: n.x },
|
|
76
79
|
...n.y !== void 0 && { y: n.y },
|
|
77
80
|
...n.color && { color: n.color },
|
|
78
|
-
...n.shape && { shape: n.shape }
|
|
81
|
+
...n.shape && { shape: n.shape },
|
|
82
|
+
...n.ports !== void 0 && { ports: n.ports }
|
|
79
83
|
})),
|
|
80
84
|
edges: d3.links.map((l, i) => ({
|
|
81
85
|
type: "edge",
|
|
@@ -84,7 +88,9 @@ function fromD3Graph(d3) {
|
|
|
84
88
|
targetId: typeof l.target === "string" ? l.target : l.target.id,
|
|
85
89
|
label: l.label ?? "",
|
|
86
90
|
data: l.data,
|
|
87
|
-
...l.color && { color: l.color }
|
|
91
|
+
...l.color && { color: l.color },
|
|
92
|
+
...l.sourcePort !== void 0 && { sourcePort: l.sourcePort },
|
|
93
|
+
...l.targetPort !== void 0 && { targetPort: l.targetPort }
|
|
88
94
|
}))
|
|
89
95
|
};
|
|
90
96
|
}
|
|
@@ -11,6 +11,9 @@ function escapeId(id) {
|
|
|
11
11
|
function escapeLabel(label) {
|
|
12
12
|
return label.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
13
13
|
}
|
|
14
|
+
function formatEndpoint(id, port) {
|
|
15
|
+
return `${escapeId(id)}${port ? `:${escapeId(port)}` : ""}`;
|
|
16
|
+
}
|
|
14
17
|
const DIRECTION_TO_RANKDIR = {
|
|
15
18
|
down: "TB",
|
|
16
19
|
up: "BT",
|
|
@@ -72,7 +75,7 @@ function toDOT(graph) {
|
|
|
72
75
|
if (edge.label) attrs.push(`label="${escapeLabel(edge.label)}"`);
|
|
73
76
|
if (edge.color) attrs.push(`color="${escapeLabel(edge.color)}"`);
|
|
74
77
|
const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
|
|
75
|
-
lines.push(` ${
|
|
78
|
+
lines.push(` ${formatEndpoint(edge.sourceId, edge.sourcePort)} ${edgeOp} ${formatEndpoint(edge.targetId, edge.targetPort)}${attrStr};`);
|
|
76
79
|
}
|
|
77
80
|
lines.push("}");
|
|
78
81
|
return lines.join("\n");
|
|
@@ -95,6 +98,10 @@ const DOT_TO_SHAPE = {
|
|
|
95
98
|
cylinder: "cylinder",
|
|
96
99
|
parallelogram: "parallelogram"
|
|
97
100
|
};
|
|
101
|
+
function getPortId(nodeId) {
|
|
102
|
+
const port = nodeId.port;
|
|
103
|
+
return typeof port?.id === "string" ? port.id : void 0;
|
|
104
|
+
}
|
|
98
105
|
function attrsToMap(attrList) {
|
|
99
106
|
const map = {};
|
|
100
107
|
for (const a of attrList) map[a.id] = String(a.eq);
|
|
@@ -209,24 +216,29 @@ function fromDOT(dot) {
|
|
|
209
216
|
const endpointGroups = [];
|
|
210
217
|
for (const item of stmt.edge_list) if (item.type === "node_id") {
|
|
211
218
|
ensureNode(item.id, parentId, nd);
|
|
212
|
-
endpointGroups.push([
|
|
219
|
+
endpointGroups.push([{
|
|
220
|
+
id: item.id,
|
|
221
|
+
...getPortId(item) && { port: getPortId(item) }
|
|
222
|
+
}]);
|
|
213
223
|
} else if (item.type === "subgraph") {
|
|
214
224
|
walkChildren(item.children, parentId, nd, ed);
|
|
215
225
|
const subNodeIds = getNodeIdsFromSubgraph(item.children);
|
|
216
226
|
for (const subNodeId of subNodeIds) ensureNode(subNodeId, parentId, nd);
|
|
217
|
-
if (subNodeIds.length > 0) endpointGroups.push(subNodeIds);
|
|
227
|
+
if (subNodeIds.length > 0) endpointGroups.push(subNodeIds.map((id) => ({ id })));
|
|
218
228
|
}
|
|
219
229
|
for (let i = 0; i < endpointGroups.length - 1; i++) {
|
|
220
230
|
const left = endpointGroups[i];
|
|
221
231
|
const right = endpointGroups[i + 1];
|
|
222
|
-
for (const
|
|
232
|
+
for (const source of left) for (const target of right) {
|
|
223
233
|
const edge = {
|
|
224
234
|
type: "edge",
|
|
225
235
|
id: `e${edgeIdx++}`,
|
|
226
|
-
sourceId,
|
|
227
|
-
targetId,
|
|
236
|
+
sourceId: source.id,
|
|
237
|
+
targetId: target.id,
|
|
228
238
|
label: mergedEdgeAttrs["label"] ?? "",
|
|
229
239
|
data: void 0,
|
|
240
|
+
...source.port && { sourcePort: source.port },
|
|
241
|
+
...target.port && { targetPort: target.port },
|
|
230
242
|
...mergedEdgeAttrs["color"] && { color: mergedEdgeAttrs["color"] }
|
|
231
243
|
};
|
|
232
244
|
edges.push(edge);
|
|
@@ -23,13 +23,30 @@ function toGEXF(graph) {
|
|
|
23
23
|
"@_id": "a_shape",
|
|
24
24
|
"@_title": "shape",
|
|
25
25
|
"@_type": "string"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"@_id": "a_ports",
|
|
29
|
+
"@_title": "ports",
|
|
30
|
+
"@_type": "string"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
const edgeAttrs = [
|
|
34
|
+
{
|
|
35
|
+
"@_id": "a_edgeData",
|
|
36
|
+
"@_title": "data",
|
|
37
|
+
"@_type": "string"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"@_id": "a_sourcePort",
|
|
41
|
+
"@_title": "sourcePort",
|
|
42
|
+
"@_type": "string"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"@_id": "a_targetPort",
|
|
46
|
+
"@_title": "targetPort",
|
|
47
|
+
"@_type": "string"
|
|
26
48
|
}
|
|
27
49
|
];
|
|
28
|
-
const edgeAttrs = [{
|
|
29
|
-
"@_id": "a_edgeData",
|
|
30
|
-
"@_title": "data",
|
|
31
|
-
"@_type": "string"
|
|
32
|
-
}];
|
|
33
50
|
const nodes = graph.nodes.map((n) => {
|
|
34
51
|
const attvalues = [];
|
|
35
52
|
if (n.parentId) attvalues.push({
|
|
@@ -48,6 +65,10 @@ function toGEXF(graph) {
|
|
|
48
65
|
"@_for": "a_shape",
|
|
49
66
|
"@_value": n.shape
|
|
50
67
|
});
|
|
68
|
+
if (n.ports !== void 0) attvalues.push({
|
|
69
|
+
"@_for": "a_ports",
|
|
70
|
+
"@_value": JSON.stringify(n.ports)
|
|
71
|
+
});
|
|
51
72
|
const node = {
|
|
52
73
|
"@_id": n.id,
|
|
53
74
|
"@_label": n.label || n.id
|
|
@@ -80,6 +101,16 @@ function toGEXF(graph) {
|
|
|
80
101
|
"@_for": "a_edgeData",
|
|
81
102
|
"@_value": JSON.stringify(e.data)
|
|
82
103
|
}] };
|
|
104
|
+
const edgeAttvalues = edge.attvalues?.attvalue ?? [];
|
|
105
|
+
if (e.sourcePort !== void 0) edgeAttvalues.push({
|
|
106
|
+
"@_for": "a_sourcePort",
|
|
107
|
+
"@_value": e.sourcePort
|
|
108
|
+
});
|
|
109
|
+
if (e.targetPort !== void 0) edgeAttvalues.push({
|
|
110
|
+
"@_for": "a_targetPort",
|
|
111
|
+
"@_value": e.targetPort
|
|
112
|
+
});
|
|
113
|
+
if (edgeAttvalues.length > 0) edge.attvalues = { attvalue: edgeAttvalues };
|
|
83
114
|
if (e.color) {
|
|
84
115
|
const hex$1 = e.color.replace("#", "");
|
|
85
116
|
if (hex$1.length === 6) edge["viz:color"] = {
|
|
@@ -90,15 +121,6 @@ function toGEXF(graph) {
|
|
|
90
121
|
}
|
|
91
122
|
return edge;
|
|
92
123
|
});
|
|
93
|
-
const graphData = [];
|
|
94
|
-
if (graph.initialNodeId) graphData.push({
|
|
95
|
-
"@_for": "a_initialNodeId",
|
|
96
|
-
"@_value": graph.initialNodeId
|
|
97
|
-
});
|
|
98
|
-
if (graph.data !== void 0) graphData.push({
|
|
99
|
-
"@_for": "a_data",
|
|
100
|
-
"@_value": JSON.stringify(graph.data)
|
|
101
|
-
});
|
|
102
124
|
const obj = {
|
|
103
125
|
"?xml": {
|
|
104
126
|
"@_version": "1.0",
|
|
@@ -111,6 +133,9 @@ function toGEXF(graph) {
|
|
|
111
133
|
graph: {
|
|
112
134
|
"@_defaultedgetype": graph.type === "directed" ? "directed" : "undirected",
|
|
113
135
|
...graph.id && { "@_id": graph.id },
|
|
136
|
+
...graph.initialNodeId && { "@_initialNodeId": graph.initialNodeId },
|
|
137
|
+
...graph.direction && { "@_direction": graph.direction },
|
|
138
|
+
...graph.data !== void 0 && { "@_data": JSON.stringify(graph.data) },
|
|
114
139
|
attributes: [{
|
|
115
140
|
"@_class": "node",
|
|
116
141
|
attribute: nodeAttrs
|
|
@@ -168,6 +193,7 @@ function fromGEXF(xml) {
|
|
|
168
193
|
data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0
|
|
169
194
|
};
|
|
170
195
|
if (attvals["shape"]) node.shape = attvals["shape"];
|
|
196
|
+
if (attvals["ports"] !== void 0) node.ports = tryParseJSON(attvals["ports"]);
|
|
171
197
|
const pos = n["viz:position"];
|
|
172
198
|
if (pos) {
|
|
173
199
|
node.x = Number(pos["@_x"] ?? 0);
|
|
@@ -199,7 +225,9 @@ function fromGEXF(xml) {
|
|
|
199
225
|
sourceId: String(e["@_source"]),
|
|
200
226
|
targetId: String(e["@_target"]),
|
|
201
227
|
label: e["@_label"] ?? "",
|
|
202
|
-
data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0
|
|
228
|
+
data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0,
|
|
229
|
+
...attvals["sourcePort"] !== void 0 && { sourcePort: attvals["sourcePort"] },
|
|
230
|
+
...attvals["targetPort"] !== void 0 && { targetPort: attvals["targetPort"] }
|
|
203
231
|
};
|
|
204
232
|
const color = e["viz:color"];
|
|
205
233
|
if (color) {
|
|
@@ -213,10 +241,11 @@ function fromGEXF(xml) {
|
|
|
213
241
|
return {
|
|
214
242
|
id: String(graphEl["@_id"] ?? ""),
|
|
215
243
|
type: graphType,
|
|
216
|
-
initialNodeId: null,
|
|
244
|
+
initialNodeId: graphEl["@_initialNodeId"] ?? null,
|
|
217
245
|
nodes,
|
|
218
246
|
edges,
|
|
219
|
-
data: void 0
|
|
247
|
+
data: graphEl["@_data"] !== void 0 ? tryParseJSON(String(graphEl["@_data"])) : void 0,
|
|
248
|
+
...graphEl["@_direction"] && { direction: graphEl["@_direction"] }
|
|
220
249
|
};
|
|
221
250
|
}
|
|
222
251
|
function asArray(val) {
|
|
@@ -40,6 +40,7 @@ function toGML(graph) {
|
|
|
40
40
|
if (node.label) lines.push(`${indent} label ${gmlString(node.label)}`);
|
|
41
41
|
if (node.initialNodeId) lines.push(`${indent} initialNodeId ${gmlString(node.initialNodeId)}`);
|
|
42
42
|
if (node.data !== void 0) lines.push(`${indent} data ${gmlString(JSON.stringify(node.data))}`);
|
|
43
|
+
if (node.ports !== void 0) lines.push(`${indent} ports ${gmlString(JSON.stringify(node.ports))}`);
|
|
43
44
|
if (node.shape) lines.push(`${indent} shape ${gmlString(node.shape)}`);
|
|
44
45
|
if (node.color) lines.push(`${indent} color ${gmlString(node.color)}`);
|
|
45
46
|
if (node.x !== void 0 || node.y !== void 0 || node.width !== void 0 || node.height !== void 0) {
|
|
@@ -63,6 +64,8 @@ function toGML(graph) {
|
|
|
63
64
|
lines.push(` target ${gmlString(edge.targetId)}`);
|
|
64
65
|
if (edge.label) lines.push(` label ${gmlString(edge.label)}`);
|
|
65
66
|
if (edge.data !== void 0) lines.push(` data ${gmlString(JSON.stringify(edge.data))}`);
|
|
67
|
+
if (edge.sourcePort !== void 0) lines.push(` sourcePort ${gmlString(edge.sourcePort)}`);
|
|
68
|
+
if (edge.targetPort !== void 0) lines.push(` targetPort ${gmlString(edge.targetPort)}`);
|
|
66
69
|
if (edge.color) lines.push(` color ${gmlString(edge.color)}`);
|
|
67
70
|
lines.push(" ]");
|
|
68
71
|
}
|
|
@@ -112,6 +115,7 @@ function fromGML(gml) {
|
|
|
112
115
|
initialNodeId: n["initialNodeId"] ?? null,
|
|
113
116
|
label: n["label"] ?? "",
|
|
114
117
|
data: n["data"] !== void 0 ? tryParseJSON(n["data"]) : void 0,
|
|
118
|
+
...n["ports"] !== void 0 && { ports: tryParseJSON(n["ports"]) },
|
|
115
119
|
...n["shape"] && { shape: n["shape"] },
|
|
116
120
|
...n["color"] && { color: n["color"] },
|
|
117
121
|
...gfx?.x !== void 0 && { x: gfx.x },
|
|
@@ -131,6 +135,8 @@ function fromGML(gml) {
|
|
|
131
135
|
targetId: String(e["target"] ?? ""),
|
|
132
136
|
label: e["label"] ?? "",
|
|
133
137
|
data: e["data"] !== void 0 ? tryParseJSON(e["data"]) : void 0,
|
|
138
|
+
...e["sourcePort"] !== void 0 && { sourcePort: String(e["sourcePort"]) },
|
|
139
|
+
...e["targetPort"] !== void 0 && { targetPort: String(e["targetPort"]) },
|
|
134
140
|
...e["color"] && { color: e["color"] }
|
|
135
141
|
});
|
|
136
142
|
return {
|
|
@@ -38,6 +38,7 @@ function toJGF(graph) {
|
|
|
38
38
|
if (n.height !== void 0) meta.height = n.height;
|
|
39
39
|
if (n.shape) meta.shape = n.shape;
|
|
40
40
|
if (n.color) meta.color = n.color;
|
|
41
|
+
if (n.ports !== void 0) meta.ports = n.ports;
|
|
41
42
|
return {
|
|
42
43
|
id: n.id,
|
|
43
44
|
...n.label && { label: n.label },
|
|
@@ -48,6 +49,8 @@ function toJGF(graph) {
|
|
|
48
49
|
const meta = {};
|
|
49
50
|
if (e.data !== void 0) meta.data = e.data;
|
|
50
51
|
if (e.color) meta.color = e.color;
|
|
52
|
+
if (e.sourcePort !== void 0) meta.sourcePort = e.sourcePort;
|
|
53
|
+
if (e.targetPort !== void 0) meta.targetPort = e.targetPort;
|
|
51
54
|
return {
|
|
52
55
|
id: e.id,
|
|
53
56
|
source: e.sourceId,
|
|
@@ -98,7 +101,8 @@ function fromJGF(jgf) {
|
|
|
98
101
|
...n.metadata?.width !== void 0 && { width: n.metadata.width },
|
|
99
102
|
...n.metadata?.height !== void 0 && { height: n.metadata.height },
|
|
100
103
|
...n.metadata?.shape && { shape: n.metadata.shape },
|
|
101
|
-
...n.metadata?.color && { color: n.metadata.color }
|
|
104
|
+
...n.metadata?.color && { color: n.metadata.color },
|
|
105
|
+
...n.metadata?.ports !== void 0 && { ports: n.metadata.ports }
|
|
102
106
|
})),
|
|
103
107
|
edges: g.edges.map((e, i) => ({
|
|
104
108
|
type: "edge",
|
|
@@ -107,7 +111,9 @@ function fromJGF(jgf) {
|
|
|
107
111
|
targetId: e.target,
|
|
108
112
|
label: e.label ?? "",
|
|
109
113
|
data: e.metadata?.data,
|
|
110
|
-
...e.metadata?.color && { color: e.metadata.color }
|
|
114
|
+
...e.metadata?.color && { color: e.metadata.color },
|
|
115
|
+
...e.metadata?.sourcePort !== void 0 && { sourcePort: e.metadata.sourcePort },
|
|
116
|
+
...e.metadata?.targetPort !== void 0 && { targetPort: e.metadata.targetPort }
|
|
111
117
|
}))
|
|
112
118
|
};
|
|
113
119
|
}
|
package/dist/schemas.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { g as GraphNode, p as GraphEdge, u as Graph, y as GraphPort } from "./types-CnZ01raw.mjs";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
|
|
3
4
|
//#region src/schemas.d.ts
|
|
@@ -126,5 +127,18 @@ declare const GraphSchema: z.ZodObject<{
|
|
|
126
127
|
}>>;
|
|
127
128
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
128
129
|
}, z.core.$strip>;
|
|
130
|
+
interface GraphValidationIssue {
|
|
131
|
+
code: string;
|
|
132
|
+
message: string;
|
|
133
|
+
path: Array<string | number>;
|
|
134
|
+
}
|
|
135
|
+
declare function isGraphPort(value: unknown): value is GraphPort;
|
|
136
|
+
declare function isGraphNode(value: unknown): value is GraphNode;
|
|
137
|
+
declare function isGraphEdge(value: unknown): value is GraphEdge;
|
|
138
|
+
declare function isGraph(value: unknown): value is Graph;
|
|
139
|
+
declare function getGraphPortIssues(value: unknown): GraphValidationIssue[];
|
|
140
|
+
declare function getGraphNodeIssues(value: unknown): GraphValidationIssue[];
|
|
141
|
+
declare function getGraphEdgeIssues(value: unknown): GraphValidationIssue[];
|
|
142
|
+
declare function getGraphIssues(value: unknown): GraphValidationIssue[];
|
|
129
143
|
//#endregion
|
|
130
|
-
export { EdgeSchema, GraphSchema, NodeSchema, PortSchema };
|
|
144
|
+
export { EdgeSchema, GraphSchema, GraphValidationIssue, NodeSchema, PortSchema, getGraphEdgeIssues, getGraphIssues, getGraphNodeIssues, getGraphPortIssues, isGraph, isGraphEdge, isGraphNode, isGraphPort };
|
package/dist/schemas.mjs
CHANGED
|
@@ -66,6 +66,39 @@ const GraphSchema = z.object({
|
|
|
66
66
|
]).optional(),
|
|
67
67
|
style: StyleSchema.optional()
|
|
68
68
|
});
|
|
69
|
+
function getValidationIssues(schema, value) {
|
|
70
|
+
const result = schema.safeParse(value);
|
|
71
|
+
if (result.success) return [];
|
|
72
|
+
return result.error.issues.map((issue) => ({
|
|
73
|
+
code: issue.code,
|
|
74
|
+
message: issue.message,
|
|
75
|
+
path: issue.path.map((segment) => typeof segment === "symbol" ? String(segment) : segment)
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
function isGraphPort(value) {
|
|
79
|
+
return PortSchema.safeParse(value).success;
|
|
80
|
+
}
|
|
81
|
+
function isGraphNode(value) {
|
|
82
|
+
return NodeSchema.safeParse(value).success;
|
|
83
|
+
}
|
|
84
|
+
function isGraphEdge(value) {
|
|
85
|
+
return EdgeSchema.safeParse(value).success;
|
|
86
|
+
}
|
|
87
|
+
function isGraph(value) {
|
|
88
|
+
return GraphSchema.safeParse(value).success;
|
|
89
|
+
}
|
|
90
|
+
function getGraphPortIssues(value) {
|
|
91
|
+
return getValidationIssues(PortSchema, value);
|
|
92
|
+
}
|
|
93
|
+
function getGraphNodeIssues(value) {
|
|
94
|
+
return getValidationIssues(NodeSchema, value);
|
|
95
|
+
}
|
|
96
|
+
function getGraphEdgeIssues(value) {
|
|
97
|
+
return getValidationIssues(EdgeSchema, value);
|
|
98
|
+
}
|
|
99
|
+
function getGraphIssues(value) {
|
|
100
|
+
return getValidationIssues(GraphSchema, value);
|
|
101
|
+
}
|
|
69
102
|
|
|
70
103
|
//#endregion
|
|
71
|
-
export { EdgeSchema, GraphSchema, NodeSchema, PortSchema };
|
|
104
|
+
export { EdgeSchema, GraphSchema, NodeSchema, PortSchema, getGraphEdgeIssues, getGraphIssues, getGraphNodeIssues, getGraphPortIssues, isGraph, isGraphEdge, isGraphNode, isGraphPort };
|
package/package.json
CHANGED