@statelyai/graph 0.13.0 → 2.0.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 +57 -26
- package/dist/{adjacency-list-Ca0VjKIf.mjs → adjacency-list-GeL1Cu-L.mjs} +5 -3
- package/dist/{algorithms-BlM-qoJb.d.mts → algorithms-CsGNehct.d.mts} +137 -2
- package/dist/{algorithms-BNDQcHU3.mjs → algorithms-DF1pSQGv.mjs} +1494 -357
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/{converter-Dspillnn.mjs → converter-DyCJJfTe.mjs} +2 -2
- package/dist/{edge-list-gKe8-iRa.mjs → edge-list-BcZ0h6zz.mjs} +1 -1
- package/dist/format-support.mjs +67 -11
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -60
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +1 -1
- package/dist/formats/cytoscape/index.mjs +5 -3
- package/dist/formats/d2/index.d.mts +109 -0
- package/dist/formats/d2/index.mjs +1100 -0
- package/dist/formats/d3/index.d.mts +2 -2
- package/dist/formats/d3/index.mjs +5 -3
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +24 -8
- package/dist/formats/edge-list/index.d.mts +1 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/elk/index.d.mts +1 -1
- package/dist/formats/elk/index.mjs +23 -16
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +30 -17
- package/dist/formats/gml/index.d.mts +1 -1
- package/dist/formats/gml/index.mjs +22 -13
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +83 -25
- package/dist/formats/jgf/index.d.mts +1 -1
- package/dist/formats/jgf/index.mjs +6 -3
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +57 -20
- package/dist/formats/tgf/index.d.mts +1 -1
- package/dist/formats/tgf/index.mjs +2 -2
- package/dist/formats/xyflow/index.d.mts +1 -1
- package/dist/formats/xyflow/index.mjs +33 -6
- package/dist/index-D51lJnt2.d.mts +61 -0
- package/dist/index-DWmo1mIp.d.mts +697 -0
- package/dist/index.d.mts +6 -631
- package/dist/index.mjs +144 -295
- package/dist/mode-D8OnHFBk.mjs +15 -0
- package/dist/queries-BfXeTXRf.d.mts +547 -0
- package/dist/queries-KirMDR7e.mjs +980 -0
- package/dist/queries.d.mts +1 -514
- package/dist/queries.mjs +1 -766
- package/dist/schemas.d.mts +21 -10
- package/dist/schemas.mjs +35 -86
- package/dist/{types-CnZ01raw.d.mts → types-DNYdIU21.d.mts} +83 -11
- package/dist/validate-TtH-x3JV.mjs +190 -0
- package/package.json +14 -3
- package/schemas/edge.schema.json +11 -0
- package/schemas/graph.schema.json +24 -3
- package/schemas/node.schema.json +6 -0
- package/dist/indexing-DUl3kTqm.mjs +0 -137
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import { n as createFormatConverter } from "../../converter-DyCJJfTe.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/formats/d2/shared.ts
|
|
4
|
+
/** Map a d2 connector glyph to a graph edge mode. */
|
|
5
|
+
const ARROW_TO_MODE = {
|
|
6
|
+
"->": "directed",
|
|
7
|
+
"<-": "directed",
|
|
8
|
+
"--": "undirected",
|
|
9
|
+
"<->": "bidirectional"
|
|
10
|
+
};
|
|
11
|
+
const RESERVED_KEYWORDS = new Set([
|
|
12
|
+
"shape",
|
|
13
|
+
"label",
|
|
14
|
+
"icon",
|
|
15
|
+
"near",
|
|
16
|
+
"tooltip",
|
|
17
|
+
"link",
|
|
18
|
+
"direction",
|
|
19
|
+
"constraint",
|
|
20
|
+
"width",
|
|
21
|
+
"height",
|
|
22
|
+
"top",
|
|
23
|
+
"left",
|
|
24
|
+
"style",
|
|
25
|
+
"class",
|
|
26
|
+
"classes",
|
|
27
|
+
"vars",
|
|
28
|
+
"source-arrowhead",
|
|
29
|
+
"target-arrowhead",
|
|
30
|
+
"grid-rows",
|
|
31
|
+
"grid-columns",
|
|
32
|
+
"grid-gap",
|
|
33
|
+
"vertical-gap",
|
|
34
|
+
"horizontal-gap"
|
|
35
|
+
]);
|
|
36
|
+
/** Style sub-keys that are booleans in d2. */
|
|
37
|
+
const BOOLEAN_STYLE_KEYS = new Set([
|
|
38
|
+
"3d",
|
|
39
|
+
"multiple",
|
|
40
|
+
"double-border",
|
|
41
|
+
"animated",
|
|
42
|
+
"bold",
|
|
43
|
+
"italic",
|
|
44
|
+
"underline",
|
|
45
|
+
"filled",
|
|
46
|
+
"shadow"
|
|
47
|
+
]);
|
|
48
|
+
/** Style sub-keys whose value is numeric in d2. */
|
|
49
|
+
const NUMERIC_STYLE_KEYS = new Set([
|
|
50
|
+
"stroke-width",
|
|
51
|
+
"stroke-dash",
|
|
52
|
+
"opacity",
|
|
53
|
+
"border-radius",
|
|
54
|
+
"font-size"
|
|
55
|
+
]);
|
|
56
|
+
const SAFE_ID = /^[A-Za-z0-9_][A-Za-z0-9_ -]*$/;
|
|
57
|
+
/** Quote a d2 key/id if it contains characters that need escaping. */
|
|
58
|
+
function escapeD2Key(id) {
|
|
59
|
+
if (id === "") return "\"\"";
|
|
60
|
+
if (RESERVED_KEYWORDS.has(id)) return `"${id}"`;
|
|
61
|
+
if (SAFE_ID.test(id) && !id.includes(" ")) return id;
|
|
62
|
+
return `"${id.replace(/"/g, "\\\"")}"`;
|
|
63
|
+
}
|
|
64
|
+
/** Quote a d2 label value if needed (contains `:`, `#`, leading/trailing space, etc.). */
|
|
65
|
+
function escapeD2Label(label) {
|
|
66
|
+
if (label === "") return "\"\"";
|
|
67
|
+
if (/[:;#{}|<>"\n]/.test(label) || label !== label.trim()) return `"${label.replace(/"/g, "\\\"")}"`;
|
|
68
|
+
return label;
|
|
69
|
+
}
|
|
70
|
+
/** Remove surrounding quotes from a d2 string token and unescape. */
|
|
71
|
+
function unquoteD2(s) {
|
|
72
|
+
const t = s.trim();
|
|
73
|
+
if (t.length >= 2 && t[0] === "\"" && t[t.length - 1] === "\"") return t.slice(1, -1).replace(/\\"/g, "\"");
|
|
74
|
+
if (t.length >= 2 && t[0] === "'" && t[t.length - 1] === "'") return t.slice(1, -1);
|
|
75
|
+
return t;
|
|
76
|
+
}
|
|
77
|
+
function validateD2Input(input) {
|
|
78
|
+
if (typeof input !== "string") throw new Error("D2: expected a string");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/formats/d2/parser.ts
|
|
83
|
+
const BLOCK_BOUNDARY_CHARS = new Set([
|
|
84
|
+
"`",
|
|
85
|
+
"'",
|
|
86
|
+
"\"",
|
|
87
|
+
".",
|
|
88
|
+
"*",
|
|
89
|
+
"+",
|
|
90
|
+
"=",
|
|
91
|
+
"-",
|
|
92
|
+
"_"
|
|
93
|
+
]);
|
|
94
|
+
/**
|
|
95
|
+
* Read a block string starting at index `i` (text[i] === '|'). Returns the end
|
|
96
|
+
* index (exclusive) and the raw fence open. d2 block strings are delimited by
|
|
97
|
+
* `|`, optionally with one boundary char (e.g. `` |` `` ... `` `| ``).
|
|
98
|
+
*/
|
|
99
|
+
function readBlockString(text, i) {
|
|
100
|
+
if (text[i] !== "|") return null;
|
|
101
|
+
let open = "|";
|
|
102
|
+
let close = "|";
|
|
103
|
+
const next = text[i + 1];
|
|
104
|
+
if (next !== void 0 && BLOCK_BOUNDARY_CHARS.has(next)) {
|
|
105
|
+
open = "|" + next;
|
|
106
|
+
close = next + "|";
|
|
107
|
+
}
|
|
108
|
+
const contentStart = i + open.length;
|
|
109
|
+
const closeIdx = text.indexOf(close, contentStart);
|
|
110
|
+
if (closeIdx === -1) return null;
|
|
111
|
+
return {
|
|
112
|
+
end: closeIdx + close.length,
|
|
113
|
+
open,
|
|
114
|
+
close
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Split a scope body into top-level statements and comments. */
|
|
118
|
+
function splitStatements(body) {
|
|
119
|
+
const tokens = [];
|
|
120
|
+
let i = 0;
|
|
121
|
+
const len = body.length;
|
|
122
|
+
let buf = "";
|
|
123
|
+
let braceDepth = 0;
|
|
124
|
+
let blockStart = -1;
|
|
125
|
+
const flush = () => {
|
|
126
|
+
const raw = buf;
|
|
127
|
+
buf = "";
|
|
128
|
+
const trimmed = raw.trim();
|
|
129
|
+
if (trimmed === "") {
|
|
130
|
+
blockStart = -1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (blockStart >= 0) {
|
|
134
|
+
const braceIdx = raw.indexOf("{", blockStart);
|
|
135
|
+
const head = raw.slice(0, braceIdx).trim();
|
|
136
|
+
const closeIdx = raw.lastIndexOf("}");
|
|
137
|
+
const block = raw.slice(braceIdx + 1, closeIdx);
|
|
138
|
+
tokens.push({
|
|
139
|
+
type: "stmt",
|
|
140
|
+
head,
|
|
141
|
+
block
|
|
142
|
+
});
|
|
143
|
+
} else tokens.push({
|
|
144
|
+
type: "stmt",
|
|
145
|
+
head: trimmed
|
|
146
|
+
});
|
|
147
|
+
blockStart = -1;
|
|
148
|
+
};
|
|
149
|
+
while (i < len) {
|
|
150
|
+
const ch = body[i];
|
|
151
|
+
if (ch === "#" && braceDepth === 0 && buf.trim() === "") {
|
|
152
|
+
const nl = body.indexOf("\n", i);
|
|
153
|
+
const end = nl === -1 ? len : nl;
|
|
154
|
+
tokens.push({
|
|
155
|
+
type: "comment",
|
|
156
|
+
text: body.slice(i + 1, end).trim()
|
|
157
|
+
});
|
|
158
|
+
i = end + 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ch === "#" && braceDepth === 0 && buf.trim() !== "") {
|
|
162
|
+
const nl = body.indexOf("\n", i);
|
|
163
|
+
i = nl === -1 ? len : nl;
|
|
164
|
+
flush();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === "\"") {
|
|
168
|
+
const endQuote = (() => {
|
|
169
|
+
let j = i + 1;
|
|
170
|
+
while (j < len) {
|
|
171
|
+
if (body[j] === "\\") {
|
|
172
|
+
j += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (body[j] === "\"") return j;
|
|
176
|
+
j++;
|
|
177
|
+
}
|
|
178
|
+
return len - 1;
|
|
179
|
+
})();
|
|
180
|
+
buf += body.slice(i, endQuote + 1);
|
|
181
|
+
i = endQuote + 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ch === "|") {
|
|
185
|
+
const bs = readBlockString(body, i);
|
|
186
|
+
if (bs) {
|
|
187
|
+
buf += body.slice(i, bs.end);
|
|
188
|
+
i = bs.end;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (ch === "{") {
|
|
193
|
+
if (braceDepth === 0) blockStart = buf.length;
|
|
194
|
+
braceDepth++;
|
|
195
|
+
buf += ch;
|
|
196
|
+
i++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (ch === "}") {
|
|
200
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
201
|
+
buf += ch;
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if ((ch === "\n" || ch === ";") && braceDepth === 0) {
|
|
206
|
+
flush();
|
|
207
|
+
i++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
buf += ch;
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
flush();
|
|
214
|
+
return tokens;
|
|
215
|
+
}
|
|
216
|
+
/** Find the top-level connector in a statement head, ignoring quotes/blocks. */
|
|
217
|
+
function findConnector(head) {
|
|
218
|
+
let depth = 0;
|
|
219
|
+
for (let i = 0; i < head.length; i++) {
|
|
220
|
+
const ch = head[i];
|
|
221
|
+
if (ch === "\"") {
|
|
222
|
+
i = skipQuoted(head, i);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (ch === "(") depth++;
|
|
226
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
227
|
+
else if (depth === 0) {
|
|
228
|
+
const m = head.slice(i, i + 3).match(/^(<->|->|<-|--)/);
|
|
229
|
+
if (m) {
|
|
230
|
+
const arrow = m[1];
|
|
231
|
+
return {
|
|
232
|
+
index: i,
|
|
233
|
+
arrow
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function skipQuoted(s, i) {
|
|
241
|
+
let j = i + 1;
|
|
242
|
+
while (j < s.length) {
|
|
243
|
+
if (s[j] === "\\") {
|
|
244
|
+
j += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (s[j] === "\"") return j;
|
|
248
|
+
j++;
|
|
249
|
+
}
|
|
250
|
+
return s.length;
|
|
251
|
+
}
|
|
252
|
+
/** Split `key: value` at the first top-level colon. Returns null if no colon. */
|
|
253
|
+
function splitKeyValue(s) {
|
|
254
|
+
let depth = 0;
|
|
255
|
+
for (let i = 0; i < s.length; i++) {
|
|
256
|
+
const ch = s[i];
|
|
257
|
+
if (ch === "\"") {
|
|
258
|
+
i = skipQuoted(s, i);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch === "(" || ch === "[") depth++;
|
|
262
|
+
else if (ch === ")" || ch === "]") depth = Math.max(0, depth - 1);
|
|
263
|
+
else if (ch === ":" && depth === 0) return {
|
|
264
|
+
key: s.slice(0, i).trim(),
|
|
265
|
+
value: s.slice(i + 1).trim()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Parse a dotted key path into segments, respecting quotes. Segments are
|
|
272
|
+
* returned **raw** (quotes preserved) so callers can distinguish a reserved
|
|
273
|
+
* keyword (`shape`) from a quoted literal node id (`"shape"`). Use
|
|
274
|
+
* {@link unquotePath} to materialize node ids.
|
|
275
|
+
*/
|
|
276
|
+
function parseKeyPath(key) {
|
|
277
|
+
const segs = [];
|
|
278
|
+
let buf = "";
|
|
279
|
+
for (let i = 0; i < key.length; i++) {
|
|
280
|
+
const ch = key[i];
|
|
281
|
+
if (ch === "\"") {
|
|
282
|
+
const end = skipQuoted(key, i);
|
|
283
|
+
buf += key.slice(i, end + 1);
|
|
284
|
+
i = end;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (ch === ".") {
|
|
288
|
+
segs.push(buf.trim());
|
|
289
|
+
buf = "";
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
buf += ch;
|
|
293
|
+
}
|
|
294
|
+
if (buf.trim() !== "") segs.push(buf.trim());
|
|
295
|
+
return segs;
|
|
296
|
+
}
|
|
297
|
+
/** Materialize node-id segments from raw key-path segments (strips quotes). */
|
|
298
|
+
function unquotePath(segs) {
|
|
299
|
+
return segs.map((s) => unquoteD2(s));
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Whether a raw key-path segment denotes a reserved keyword. Quoted segments
|
|
303
|
+
* (e.g. `"shape"`) are literal node ids and never reserved.
|
|
304
|
+
*/
|
|
305
|
+
function isReservedSegment(rawSeg) {
|
|
306
|
+
return RESERVED_KEYWORDS.has(rawSeg);
|
|
307
|
+
}
|
|
308
|
+
function parseLabelBlock(value) {
|
|
309
|
+
const trimmed = value.trim();
|
|
310
|
+
if (!trimmed.startsWith("|")) return { text: unquoteD2(trimmed) };
|
|
311
|
+
const bs = readBlockString(trimmed, 0);
|
|
312
|
+
if (!bs) return { text: unquoteD2(trimmed) };
|
|
313
|
+
let inner = trimmed.slice(bs.open.length, trimmed.length - bs.close.length);
|
|
314
|
+
let kind = "block";
|
|
315
|
+
let lang;
|
|
316
|
+
const tagMatch = inner.match(/^([A-Za-z0-9_+-]+)(\s|\n)/);
|
|
317
|
+
if (tagMatch) {
|
|
318
|
+
const tag = tagMatch[1];
|
|
319
|
+
inner = inner.slice(tagMatch[0].length);
|
|
320
|
+
if (tag === "md") kind = "md";
|
|
321
|
+
else if (tag === "tex" || tag === "latex") kind = "latex";
|
|
322
|
+
else {
|
|
323
|
+
kind = "code";
|
|
324
|
+
lang = tag;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
text: inner.replace(/^\n/, "").replace(/\s+$/, ""),
|
|
329
|
+
labelBlock: {
|
|
330
|
+
kind,
|
|
331
|
+
lang,
|
|
332
|
+
fence: bs.open
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function coerceStyleValue(key, raw) {
|
|
337
|
+
const v = unquoteD2(raw);
|
|
338
|
+
if (BOOLEAN_STYLE_KEYS.has(key)) {
|
|
339
|
+
if (v === "true") return true;
|
|
340
|
+
if (v === "false") return false;
|
|
341
|
+
}
|
|
342
|
+
if (NUMERIC_STYLE_KEYS.has(key)) {
|
|
343
|
+
const n = Number(v);
|
|
344
|
+
if (!Number.isNaN(n)) return n;
|
|
345
|
+
}
|
|
346
|
+
return v;
|
|
347
|
+
}
|
|
348
|
+
function nodeId(path) {
|
|
349
|
+
return path.join(".");
|
|
350
|
+
}
|
|
351
|
+
function ensureNode(ctx, path, declarationForm) {
|
|
352
|
+
const id = nodeId(path);
|
|
353
|
+
let node = ctx.nodes.get(id);
|
|
354
|
+
if (!node) {
|
|
355
|
+
const parentId = path.length > 1 ? nodeId(path.slice(0, -1)) : null;
|
|
356
|
+
if (parentId) ensureNode(ctx, path.slice(0, -1), "dot");
|
|
357
|
+
node = {
|
|
358
|
+
type: "node",
|
|
359
|
+
id,
|
|
360
|
+
parentId,
|
|
361
|
+
label: null,
|
|
362
|
+
data: { declarationForm }
|
|
363
|
+
};
|
|
364
|
+
ctx.nodes.set(id, node);
|
|
365
|
+
registerChildOrder(ctx, parentId, id);
|
|
366
|
+
}
|
|
367
|
+
return node;
|
|
368
|
+
}
|
|
369
|
+
function registerChildOrder(ctx, parentId, childId) {
|
|
370
|
+
if (!parentId) return;
|
|
371
|
+
const parent = ctx.nodes.get(parentId);
|
|
372
|
+
if (!parent) return;
|
|
373
|
+
parent.data.order = parent.data.order ?? [];
|
|
374
|
+
if (!parent.data.order.includes(childId)) parent.data.order.push(childId);
|
|
375
|
+
}
|
|
376
|
+
function applyReserved(node, keyword, rest, value) {
|
|
377
|
+
switch (keyword) {
|
|
378
|
+
case "shape":
|
|
379
|
+
node.shape = unquoteD2(value);
|
|
380
|
+
break;
|
|
381
|
+
case "label": {
|
|
382
|
+
const { text, labelBlock } = parseLabelBlock(value);
|
|
383
|
+
node.label = text;
|
|
384
|
+
if (labelBlock) node.data.labelBlock = labelBlock;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "icon":
|
|
388
|
+
node.data.icon = unquoteD2(value);
|
|
389
|
+
break;
|
|
390
|
+
case "near":
|
|
391
|
+
node.data.near = unquoteD2(value);
|
|
392
|
+
break;
|
|
393
|
+
case "tooltip":
|
|
394
|
+
node.data.tooltip = unquoteD2(value);
|
|
395
|
+
break;
|
|
396
|
+
case "link":
|
|
397
|
+
node.data.link = unquoteD2(value);
|
|
398
|
+
break;
|
|
399
|
+
case "class":
|
|
400
|
+
node.data.classes = unquoteD2(value).split(/[\s;]+/).filter(Boolean);
|
|
401
|
+
break;
|
|
402
|
+
case "width":
|
|
403
|
+
node.width = Number(unquoteD2(value));
|
|
404
|
+
break;
|
|
405
|
+
case "height":
|
|
406
|
+
node.height = Number(unquoteD2(value));
|
|
407
|
+
break;
|
|
408
|
+
case "left":
|
|
409
|
+
node.x = Number(unquoteD2(value));
|
|
410
|
+
break;
|
|
411
|
+
case "top":
|
|
412
|
+
node.y = Number(unquoteD2(value));
|
|
413
|
+
break;
|
|
414
|
+
case "grid-rows":
|
|
415
|
+
node.data.grid = {
|
|
416
|
+
...node.data.grid,
|
|
417
|
+
rows: Number(unquoteD2(value))
|
|
418
|
+
};
|
|
419
|
+
break;
|
|
420
|
+
case "grid-columns":
|
|
421
|
+
node.data.grid = {
|
|
422
|
+
...node.data.grid,
|
|
423
|
+
columns: Number(unquoteD2(value))
|
|
424
|
+
};
|
|
425
|
+
break;
|
|
426
|
+
case "grid-gap":
|
|
427
|
+
node.data.grid = {
|
|
428
|
+
...node.data.grid,
|
|
429
|
+
gap: Number(unquoteD2(value))
|
|
430
|
+
};
|
|
431
|
+
break;
|
|
432
|
+
case "style":
|
|
433
|
+
if (rest.length > 0) {
|
|
434
|
+
const styleKey = rest[0];
|
|
435
|
+
node.style = node.style ?? {};
|
|
436
|
+
node.style[styleKey] = coerceStyleValue(styleKey, value);
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
default:
|
|
440
|
+
node.data.reserved = node.data.reserved ?? {};
|
|
441
|
+
node.data.reserved[[keyword, ...rest].join(".")] = unquoteD2(value);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** Apply a `style: { ... }` block to a node. */
|
|
445
|
+
function applyStyleBlock(node, block) {
|
|
446
|
+
for (const tok of splitStatements(block)) {
|
|
447
|
+
if (tok.type !== "stmt") continue;
|
|
448
|
+
const kv = splitKeyValue(tok.head);
|
|
449
|
+
if (!kv) continue;
|
|
450
|
+
node.style = node.style ?? {};
|
|
451
|
+
node.style[kv.key] = coerceStyleValue(kv.key, kv.value);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** Parse a sql_table / class field declaration into a port. */
|
|
455
|
+
function parseFieldPort(name, value, block, classMember) {
|
|
456
|
+
const data = {};
|
|
457
|
+
if (classMember) data.classMember = true;
|
|
458
|
+
let portName = name;
|
|
459
|
+
const vis = portName.match(/^([+\-#])\s*/);
|
|
460
|
+
if (vis) {
|
|
461
|
+
data.visibility = vis[1];
|
|
462
|
+
portName = portName.slice(vis[0].length);
|
|
463
|
+
}
|
|
464
|
+
const typeName = unquoteD2(value);
|
|
465
|
+
if (typeName) data.typeName = typeName;
|
|
466
|
+
if (block) for (const tok of splitStatements(block)) {
|
|
467
|
+
if (tok.type !== "stmt") continue;
|
|
468
|
+
const kv = splitKeyValue(tok.head);
|
|
469
|
+
if (kv && kv.key === "constraint") data.constraint = unquoteD2(kv.value).replace(/^\[|\]$/g, "").split(/[\s;,]+/).filter(Boolean);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
name: unquoteD2(portName),
|
|
473
|
+
direction: "inout",
|
|
474
|
+
data
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const STRUCTURED_SHAPES$1 = new Set(["sql_table", "class"]);
|
|
478
|
+
/** Parse the body of a structured-shape (sql_table/class) node into ports. */
|
|
479
|
+
function parseStructuredBody(ctx, node, block, classMember) {
|
|
480
|
+
node.ports = node.ports ?? [];
|
|
481
|
+
for (const tok of splitStatements(block)) {
|
|
482
|
+
if (tok.type !== "stmt") continue;
|
|
483
|
+
const kv = splitKeyValue(tok.head);
|
|
484
|
+
if (kv && kv.key === "shape") {
|
|
485
|
+
node.shape = unquoteD2(kv.value);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (kv && (kv.key === "style" || kv.key.startsWith("style."))) {
|
|
489
|
+
applyStyleBlock(node, tok.block ?? kv.value);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const fieldName = kv ? kv.key : tok.head.trim();
|
|
493
|
+
const fieldType = kv ? kv.value : "";
|
|
494
|
+
node.ports.push(parseFieldPort(fieldName, fieldType, tok.block, classMember));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function makeEdge(ctx, sourcePath, targetPath, arrow, label, labelBlock) {
|
|
498
|
+
const resolve = (path) => resolveEndpoint(ctx, path);
|
|
499
|
+
const src = resolve(sourcePath);
|
|
500
|
+
const tgt = resolve(targetPath);
|
|
501
|
+
let sId = src.nodeId;
|
|
502
|
+
let tId = tgt.nodeId;
|
|
503
|
+
let sPort = src.port;
|
|
504
|
+
let tPort = tgt.port;
|
|
505
|
+
if (arrow === "<-") {
|
|
506
|
+
[sId, tId] = [tId, sId];
|
|
507
|
+
[sPort, tPort] = [tPort, sPort];
|
|
508
|
+
}
|
|
509
|
+
const key = `${sId} ${tId}`;
|
|
510
|
+
const count = ctx.edgeCounts.get(key) ?? 0;
|
|
511
|
+
ctx.edgeCounts.set(key, count + 1);
|
|
512
|
+
const id = `${sId}->${tId}#${count}`;
|
|
513
|
+
const data = { arrow };
|
|
514
|
+
if (labelBlock) data.labelBlock = labelBlock;
|
|
515
|
+
const edge = {
|
|
516
|
+
type: "edge",
|
|
517
|
+
id,
|
|
518
|
+
sourceId: sId,
|
|
519
|
+
targetId: tId,
|
|
520
|
+
label,
|
|
521
|
+
data,
|
|
522
|
+
mode: ARROW_TO_MODE[arrow]
|
|
523
|
+
};
|
|
524
|
+
if (sPort) edge.sourcePort = sPort;
|
|
525
|
+
if (tPort) edge.targetPort = tPort;
|
|
526
|
+
return edge;
|
|
527
|
+
}
|
|
528
|
+
function resolveEndpoint(ctx, path) {
|
|
529
|
+
if (path.length >= 2) {
|
|
530
|
+
const maybeNode = nodeId(path.slice(0, -1));
|
|
531
|
+
const node = ctx.nodes.get(maybeNode);
|
|
532
|
+
const portName = path[path.length - 1];
|
|
533
|
+
if (node?.ports?.some((p) => p.name === portName)) return {
|
|
534
|
+
nodeId: maybeNode,
|
|
535
|
+
port: portName
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
ensureNode(ctx, path, "dot");
|
|
539
|
+
return { nodeId: nodeId(path) };
|
|
540
|
+
}
|
|
541
|
+
function parseScope(ctx, body, scopePath, pendingComments) {
|
|
542
|
+
const tokens = splitStatements(body);
|
|
543
|
+
let comments = [...pendingComments];
|
|
544
|
+
for (const tok of tokens) {
|
|
545
|
+
if (tok.type === "comment") {
|
|
546
|
+
comments.push(tok.text);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const head = tok.head;
|
|
550
|
+
if (head.startsWith("...@") || head.startsWith("@")) {
|
|
551
|
+
const importPath = head.replace(/^\.\.\./, "").replace(/^@/, "").trim();
|
|
552
|
+
ctx.graph.data.source = ctx.graph.data.source ?? {};
|
|
553
|
+
ctx.graph.data.source.imports = ctx.graph.data.source.imports ?? [];
|
|
554
|
+
ctx.graph.data.source.imports.push(importPath);
|
|
555
|
+
comments = [];
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (findConnector(head) && scopePath.length >= 0) {
|
|
559
|
+
handleConnection(ctx, head, tok.block, scopePath, comments);
|
|
560
|
+
comments = [];
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const kv = splitKeyValue(head);
|
|
564
|
+
const keyText = kv ? kv.key : head;
|
|
565
|
+
const value = kv ? kv.value : "";
|
|
566
|
+
const rawSegs = parseKeyPath(keyText);
|
|
567
|
+
if (rawSegs.length >= 1 && isReservedSegment(rawSegs[0]) && scopePath.length === 0) {
|
|
568
|
+
handleScopeReserved(ctx, rawSegs, value, tok.block, scopePath);
|
|
569
|
+
comments = [];
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
handleDeclaration(ctx, rawSegs, value, tok.block, scopePath, comments);
|
|
573
|
+
comments = [];
|
|
574
|
+
}
|
|
575
|
+
if (scopePath.length === 0 && comments.length > 0) ctx.graph.data.trailingComments = comments;
|
|
576
|
+
}
|
|
577
|
+
function handleScopeReserved(ctx, segs, value, block, scopePath) {
|
|
578
|
+
const keyword = segs[0];
|
|
579
|
+
if (keyword === "direction" && scopePath.length === 0) {
|
|
580
|
+
const dir = unquoteD2(value);
|
|
581
|
+
if ([
|
|
582
|
+
"up",
|
|
583
|
+
"down",
|
|
584
|
+
"left",
|
|
585
|
+
"right"
|
|
586
|
+
].includes(dir)) ctx.graph.direction = dir;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (keyword === "vars" && block !== void 0) {
|
|
590
|
+
ctx.graph.data.source = ctx.graph.data.source ?? {};
|
|
591
|
+
ctx.graph.data.source.vars = parseVarsBlock(block);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (keyword === "classes" && block !== void 0) {
|
|
595
|
+
ctx.graph.data.source = ctx.graph.data.source ?? {};
|
|
596
|
+
ctx.graph.data.source.classes = parseClassesBlock(block);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (keyword === "style") {
|
|
600
|
+
if (block !== void 0) for (const tok of splitStatements(block)) {
|
|
601
|
+
if (tok.type !== "stmt") continue;
|
|
602
|
+
const kv = splitKeyValue(tok.head);
|
|
603
|
+
if (!kv) continue;
|
|
604
|
+
ctx.graph.style = ctx.graph.style ?? {};
|
|
605
|
+
ctx.graph.style[kv.key] = coerceStyleValue(kv.key, kv.value);
|
|
606
|
+
}
|
|
607
|
+
else if (segs.length > 1) {
|
|
608
|
+
ctx.graph.style = ctx.graph.style ?? {};
|
|
609
|
+
ctx.graph.style[segs[1]] = coerceStyleValue(segs[1], value);
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function parseVarsBlock(block) {
|
|
615
|
+
const out = {};
|
|
616
|
+
for (const tok of splitStatements(block)) {
|
|
617
|
+
if (tok.type !== "stmt") continue;
|
|
618
|
+
const kv = splitKeyValue(tok.head);
|
|
619
|
+
if (kv) out[kv.key] = unquoteD2(kv.value);
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function parseClassesBlock(block) {
|
|
624
|
+
const out = {};
|
|
625
|
+
for (const tok of splitStatements(block)) {
|
|
626
|
+
if (tok.type !== "stmt" || tok.block === void 0) continue;
|
|
627
|
+
const kv = splitKeyValue(tok.head);
|
|
628
|
+
const className = (kv ? kv.key : tok.head).trim();
|
|
629
|
+
const style = {};
|
|
630
|
+
for (const inner of splitStatements(tok.block)) {
|
|
631
|
+
if (inner.type !== "stmt") continue;
|
|
632
|
+
const ikv = splitKeyValue(inner.head);
|
|
633
|
+
if (!ikv) continue;
|
|
634
|
+
const segs = parseKeyPath(ikv.key);
|
|
635
|
+
const styleKey = segs[0] === "style" ? segs[1] : segs[0];
|
|
636
|
+
style[styleKey] = coerceStyleValue(styleKey, ikv.value);
|
|
637
|
+
}
|
|
638
|
+
out[className] = style;
|
|
639
|
+
}
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
function handleConnection(ctx, head, block, scopePath, comments) {
|
|
643
|
+
let working = head;
|
|
644
|
+
const kv = splitKeyValue(working);
|
|
645
|
+
let label = null;
|
|
646
|
+
let labelBlock;
|
|
647
|
+
if (kv && findConnector(kv.value) === null) {
|
|
648
|
+
working = kv.key;
|
|
649
|
+
const parsed = parseLabelBlock(kv.value);
|
|
650
|
+
label = parsed.text || null;
|
|
651
|
+
labelBlock = parsed.labelBlock;
|
|
652
|
+
}
|
|
653
|
+
const parts = [];
|
|
654
|
+
const arrows = [];
|
|
655
|
+
let rest = working.trim();
|
|
656
|
+
rest = rest.replace(/^\((.*)\)\s*(\[\d+\])?$/s, "$1");
|
|
657
|
+
let conn = findConnector(rest);
|
|
658
|
+
let lastIndex = 0;
|
|
659
|
+
while (conn) {
|
|
660
|
+
parts.push({ text: rest.slice(lastIndex, conn.index).trim() });
|
|
661
|
+
arrows.push(conn.arrow);
|
|
662
|
+
rest = rest.slice(conn.index + conn.arrow.length);
|
|
663
|
+
conn = findConnector(rest);
|
|
664
|
+
lastIndex = 0;
|
|
665
|
+
}
|
|
666
|
+
parts.push({ text: rest.trim() });
|
|
667
|
+
for (let k = 0; k < arrows.length; k++) {
|
|
668
|
+
const sourcePath = unquotePath(parseKeyPath(parts[k].text));
|
|
669
|
+
const targetPath = unquotePath(parseKeyPath(parts[k + 1].text));
|
|
670
|
+
const edge = makeEdge(ctx, [...scopePath, ...sourcePath], [...scopePath, ...targetPath], arrows[k], k === arrows.length - 1 ? label : null, k === arrows.length - 1 ? labelBlock : void 0);
|
|
671
|
+
if (comments.length > 0 && k === 0) edge.data.commentsBefore = comments;
|
|
672
|
+
ctx.edges.push(edge);
|
|
673
|
+
registerChildOrder(ctx, scopePath.length ? nodeId(scopePath) : null, edge.id);
|
|
674
|
+
if (block !== void 0 && k === arrows.length - 1) applyEdgeBlock(edge, block);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function applyEdgeBlock(edge, block) {
|
|
678
|
+
for (const tok of splitStatements(block)) {
|
|
679
|
+
if (tok.type !== "stmt") continue;
|
|
680
|
+
const kv = splitKeyValue(tok.head);
|
|
681
|
+
if (!kv) continue;
|
|
682
|
+
const segs = parseKeyPath(kv.key);
|
|
683
|
+
if (segs[0] === "style") {
|
|
684
|
+
edge.style = edge.style ?? {};
|
|
685
|
+
if (segs.length > 1) edge.style[segs[1]] = coerceStyleValue(segs[1], kv.value);
|
|
686
|
+
else if (tok.block !== void 0) for (const inner of splitStatements(tok.block)) {
|
|
687
|
+
if (inner.type !== "stmt") continue;
|
|
688
|
+
const ikv = splitKeyValue(inner.head);
|
|
689
|
+
if (ikv) edge.style[ikv.key] = coerceStyleValue(ikv.key, ikv.value);
|
|
690
|
+
}
|
|
691
|
+
} else if (segs[0] === "source-arrowhead" || segs[0] === "target-arrowhead") {
|
|
692
|
+
const side = segs[0] === "source-arrowhead" ? "sourceArrowhead" : "targetArrowhead";
|
|
693
|
+
edge.data[side] = edge.data[side] ?? {};
|
|
694
|
+
if (segs[1] === "shape") edge.data[side].shape = unquoteD2(kv.value);
|
|
695
|
+
else edge.data[side].label = unquoteD2(kv.value);
|
|
696
|
+
} else if (segs[0] === "label") edge.label = unquoteD2(kv.value);
|
|
697
|
+
else {
|
|
698
|
+
edge.data.reserved = edge.data.reserved ?? {};
|
|
699
|
+
edge.data.reserved[kv.key] = unquoteD2(kv.value);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function handleDeclaration(ctx, rawSegs, value, block, scopePath, comments) {
|
|
704
|
+
const segs = unquotePath(rawSegs);
|
|
705
|
+
const fullPath = [...scopePath, ...segs];
|
|
706
|
+
let reservedAt = -1;
|
|
707
|
+
for (let i = 0; i < rawSegs.length; i++) if (isReservedSegment(rawSegs[i])) {
|
|
708
|
+
reservedAt = i;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
if (reservedAt >= 0) {
|
|
712
|
+
const nodePath = [...scopePath, ...segs.slice(0, reservedAt)];
|
|
713
|
+
if (nodePath.length === 0) return;
|
|
714
|
+
const node$1 = ensureNode(ctx, nodePath, "dot");
|
|
715
|
+
if (comments.length > 0 && !node$1.data.commentsBefore) node$1.data.commentsBefore = comments;
|
|
716
|
+
applyReserved(node$1, rawSegs[reservedAt], segs.slice(reservedAt + 1), value);
|
|
717
|
+
if (rawSegs[reservedAt] === "style" && block !== void 0) applyStyleBlock(node$1, block);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const node = ensureNode(ctx, fullPath, block !== void 0 ? "block" : segs.length > 1 ? "dot" : "block");
|
|
721
|
+
if (comments.length > 0 && !node.data.commentsBefore) node.data.commentsBefore = comments;
|
|
722
|
+
if (value !== "") {
|
|
723
|
+
const { text, labelBlock } = parseLabelBlock(value);
|
|
724
|
+
node.label = text;
|
|
725
|
+
if (labelBlock) node.data.labelBlock = labelBlock;
|
|
726
|
+
}
|
|
727
|
+
if (block !== void 0) {
|
|
728
|
+
const shapeFromBlock = peekShape(block);
|
|
729
|
+
if (shapeFromBlock) node.shape = shapeFromBlock;
|
|
730
|
+
if (node.shape && STRUCTURED_SHAPES$1.has(node.shape)) parseStructuredBody(ctx, node, block, node.shape === "class");
|
|
731
|
+
else parseScope(ctx, block, fullPath, []);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function peekShape(block) {
|
|
735
|
+
for (const tok of splitStatements(block)) {
|
|
736
|
+
if (tok.type !== "stmt") continue;
|
|
737
|
+
const kv = splitKeyValue(tok.head);
|
|
738
|
+
if (kv && parseKeyPath(kv.key).join(".") === "shape") return unquoteD2(kv.value);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Resolve edge endpoints that point at a structured-shape field but were parsed
|
|
743
|
+
* as child nodes because the connection appeared *before* the `sql_table`/`class`
|
|
744
|
+
* declaration (forward reference). Rewrites such endpoints to `sourcePort` /
|
|
745
|
+
* `targetPort` and strips the spurious empty-leaf node that was auto-created.
|
|
746
|
+
*/
|
|
747
|
+
function finalizePortEndpoints(ctx) {
|
|
748
|
+
const childCount = /* @__PURE__ */ new Map();
|
|
749
|
+
for (const n of ctx.nodes.values()) if (n.parentId) childCount.set(n.parentId, (childCount.get(n.parentId) ?? 0) + 1);
|
|
750
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
751
|
+
const resolve = (id, port) => {
|
|
752
|
+
if (port) return [id, port];
|
|
753
|
+
const node = ctx.nodes.get(id);
|
|
754
|
+
if (!node?.parentId) return [id, port];
|
|
755
|
+
const parent = ctx.nodes.get(node.parentId);
|
|
756
|
+
const portName = id.slice(node.parentId.length + 1);
|
|
757
|
+
if (!parent?.ports?.some((p) => p.name === portName)) return [id, port];
|
|
758
|
+
if (!((childCount.get(id) ?? 0) === 0 && !node.shape && !node.style && (node.label == null || node.label === "") && (node.ports?.length ?? 0) === 0)) return [id, port];
|
|
759
|
+
toRemove.add(id);
|
|
760
|
+
return [node.parentId, portName];
|
|
761
|
+
};
|
|
762
|
+
for (const edge of ctx.edges) {
|
|
763
|
+
[edge.sourceId, edge.sourcePort] = resolve(edge.sourceId, edge.sourcePort);
|
|
764
|
+
[edge.targetId, edge.targetPort] = resolve(edge.targetId, edge.targetPort);
|
|
765
|
+
}
|
|
766
|
+
for (const id of toRemove) {
|
|
767
|
+
const node = ctx.nodes.get(id);
|
|
768
|
+
ctx.nodes.delete(id);
|
|
769
|
+
const parent = node?.parentId ? ctx.nodes.get(node.parentId) : void 0;
|
|
770
|
+
if (parent?.data.order) parent.data.order = parent.data.order.filter((x) => x !== id);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function fromD2(input) {
|
|
774
|
+
validateD2Input(input);
|
|
775
|
+
const graph = {
|
|
776
|
+
id: "",
|
|
777
|
+
mode: "directed",
|
|
778
|
+
initialNodeId: null,
|
|
779
|
+
nodes: [],
|
|
780
|
+
edges: [],
|
|
781
|
+
data: { diagramType: "d2" }
|
|
782
|
+
};
|
|
783
|
+
const ctx = {
|
|
784
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
785
|
+
edges: [],
|
|
786
|
+
edgeCounts: /* @__PURE__ */ new Map(),
|
|
787
|
+
graph
|
|
788
|
+
};
|
|
789
|
+
parseScope(ctx, input, [], []);
|
|
790
|
+
finalizePortEndpoints(ctx);
|
|
791
|
+
graph.nodes = [...ctx.nodes.values()];
|
|
792
|
+
graph.edges = ctx.edges;
|
|
793
|
+
if (graph.edges.length > 0) {
|
|
794
|
+
const modes = new Set(graph.edges.map((e) => e.mode));
|
|
795
|
+
if (modes.size === 1 && modes.has("undirected")) graph.mode = "undirected";
|
|
796
|
+
}
|
|
797
|
+
return graph;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/formats/d2/emitter.ts
|
|
802
|
+
const STRUCTURED_SHAPES = new Set(["sql_table", "class"]);
|
|
803
|
+
/**
|
|
804
|
+
* Parser-shaped node data, tolerating graphs not produced by `fromD2` whose
|
|
805
|
+
* `data` may be `undefined`, `null`, or an arbitrary user object.
|
|
806
|
+
*/
|
|
807
|
+
function nodeData(node) {
|
|
808
|
+
return node.data ?? {};
|
|
809
|
+
}
|
|
810
|
+
/** Parser-shaped edge data; see {@link nodeData}. */
|
|
811
|
+
function edgeData(edge) {
|
|
812
|
+
return edge.data ?? {};
|
|
813
|
+
}
|
|
814
|
+
function splitId(id) {
|
|
815
|
+
const out = [];
|
|
816
|
+
let buf = "";
|
|
817
|
+
let inQuote = false;
|
|
818
|
+
for (let i = 0; i < id.length; i++) {
|
|
819
|
+
const ch = id[i];
|
|
820
|
+
if (ch === "\"") inQuote = !inQuote;
|
|
821
|
+
if (ch === "." && !inQuote) {
|
|
822
|
+
out.push(buf);
|
|
823
|
+
buf = "";
|
|
824
|
+
} else buf += ch;
|
|
825
|
+
}
|
|
826
|
+
out.push(buf);
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
function indent(n) {
|
|
830
|
+
return " ".repeat(n);
|
|
831
|
+
}
|
|
832
|
+
function emitLabelBlock(text, block) {
|
|
833
|
+
const open = block.fence;
|
|
834
|
+
const close = open.length > 1 ? open[1] + "|" : "|";
|
|
835
|
+
const tag = block.kind === "md" ? "md" : block.kind === "latex" ? "tex" : block.kind === "code" ? block.lang ?? "" : "";
|
|
836
|
+
return `${open}${tag ? `${tag} ` : ""}${text}${close}`;
|
|
837
|
+
}
|
|
838
|
+
function styleValueToD2(v) {
|
|
839
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
840
|
+
if (typeof v === "number") return String(v);
|
|
841
|
+
return escapeD2Label(v);
|
|
842
|
+
}
|
|
843
|
+
function emitComments(comments, ind, lines) {
|
|
844
|
+
if (!comments) return;
|
|
845
|
+
for (const c of comments) lines.push(`${indent(ind)}#${c ? " " + c.trimStart() : ""}`);
|
|
846
|
+
}
|
|
847
|
+
function nodeAttrLines(node, ind) {
|
|
848
|
+
const lines = [];
|
|
849
|
+
const d = nodeData(node);
|
|
850
|
+
if (node.shape) lines.push(`${indent(ind)}shape: ${escapeD2Label(node.shape)}`);
|
|
851
|
+
if (d.near) lines.push(`${indent(ind)}near: ${escapeD2Label(d.near)}`);
|
|
852
|
+
if (d.icon) lines.push(`${indent(ind)}icon: ${d.icon}`);
|
|
853
|
+
if (d.tooltip) lines.push(`${indent(ind)}tooltip: ${escapeD2Label(d.tooltip)}`);
|
|
854
|
+
if (d.link) lines.push(`${indent(ind)}link: ${escapeD2Label(d.link)}`);
|
|
855
|
+
if (d.classes?.length) lines.push(`${indent(ind)}class: ${d.classes.join(" ")}`);
|
|
856
|
+
if (node.width !== void 0) lines.push(`${indent(ind)}width: ${node.width}`);
|
|
857
|
+
if (node.height !== void 0) lines.push(`${indent(ind)}height: ${node.height}`);
|
|
858
|
+
if (node.x !== void 0) lines.push(`${indent(ind)}left: ${node.x}`);
|
|
859
|
+
if (node.y !== void 0) lines.push(`${indent(ind)}top: ${node.y}`);
|
|
860
|
+
if (d.grid) {
|
|
861
|
+
if (d.grid.rows !== void 0) lines.push(`${indent(ind)}grid-rows: ${d.grid.rows}`);
|
|
862
|
+
if (d.grid.columns !== void 0) lines.push(`${indent(ind)}grid-columns: ${d.grid.columns}`);
|
|
863
|
+
if (d.grid.gap !== void 0) lines.push(`${indent(ind)}grid-gap: ${d.grid.gap}`);
|
|
864
|
+
}
|
|
865
|
+
if (node.style) for (const [k, v] of Object.entries(node.style)) lines.push(`${indent(ind)}style.${k}: ${styleValueToD2(v)}`);
|
|
866
|
+
if (d.reserved) for (const [k, v] of Object.entries(d.reserved)) lines.push(`${indent(ind)}${k}: ${styleValueToD2(v)}`);
|
|
867
|
+
return lines;
|
|
868
|
+
}
|
|
869
|
+
function portLines(node, ind) {
|
|
870
|
+
const lines = [];
|
|
871
|
+
for (const port of node.ports ?? []) {
|
|
872
|
+
const pd = port.data ?? {};
|
|
873
|
+
const name = `${pd.visibility ?? ""}${escapeD2Key(port.name)}`;
|
|
874
|
+
let line = `${indent(ind)}${name}`;
|
|
875
|
+
if (pd.typeName) line += `: ${escapeD2Label(pd.typeName)}`;
|
|
876
|
+
if (pd.constraint?.length) line += ` { constraint: ${pd.constraint.length === 1 ? pd.constraint[0] : `[${pd.constraint.join("; ")}]`} }`;
|
|
877
|
+
lines.push(line);
|
|
878
|
+
}
|
|
879
|
+
return lines;
|
|
880
|
+
}
|
|
881
|
+
function labelHeader(node) {
|
|
882
|
+
if (node.label == null || node.label === "") return "";
|
|
883
|
+
const labelBlock = nodeData(node).labelBlock;
|
|
884
|
+
if (labelBlock) return emitLabelBlock(node.label, labelBlock);
|
|
885
|
+
return escapeD2Label(node.label);
|
|
886
|
+
}
|
|
887
|
+
/** Whether a node carries its own attributes/ports/label (needs a line of its own). */
|
|
888
|
+
function hasOwnContent(node) {
|
|
889
|
+
return nodeAttrLines(node, 0).length > 0 || (node.ports?.length ?? 0) > 0 || node.label != null && node.label !== "";
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* A node is a "pure prefix" — emitted by carrying its dotted path down to
|
|
893
|
+
* descendants rather than as its own block — when it was authored in dot form,
|
|
894
|
+
* has no own content, and has children.
|
|
895
|
+
*/
|
|
896
|
+
function isPurePrefix(ctx, node) {
|
|
897
|
+
const children = ctx.childrenOf.get(node.id) ?? [];
|
|
898
|
+
return nodeData(node).declarationForm !== "block" && children.length > 0 && !hasOwnContent(node);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Emit the items (child nodes + owned edges) of a scope, in `data.order` when
|
|
902
|
+
* present (sequence diagrams), else nodes-then-edges in insertion order.
|
|
903
|
+
* `scopeSegs` is the dotted prefix already implied by enclosing blocks.
|
|
904
|
+
*/
|
|
905
|
+
function emitScope(ctx, container, containerId, scopeSegs, ind, lines) {
|
|
906
|
+
const children = ctx.childrenOf.get(containerId) ?? [];
|
|
907
|
+
const ownedEdges = container ? ctx.edgesByOwner.get(container.id) ?? [] : [];
|
|
908
|
+
const order = container ? nodeData(container).order : void 0;
|
|
909
|
+
if (order) {
|
|
910
|
+
const childById = new Map(children.map((c) => [c.id, c]));
|
|
911
|
+
const edgeById = new Map(ownedEdges.map((e) => [e.id, e]));
|
|
912
|
+
for (const refId of order) {
|
|
913
|
+
const child = childById.get(refId);
|
|
914
|
+
if (child) {
|
|
915
|
+
emitItem(ctx, child, scopeSegs, ind, lines);
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const edge = edgeById.get(refId);
|
|
919
|
+
if (edge && !ctx.emittedEdges.has(edge.id)) emitEdge(ctx, edge, ind, lines, true);
|
|
920
|
+
}
|
|
921
|
+
for (const child of children) if (!order.includes(child.id)) emitItem(ctx, child, scopeSegs, ind, lines);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
for (const child of children) emitItem(ctx, child, scopeSegs, ind, lines);
|
|
925
|
+
}
|
|
926
|
+
/** Emit a single node within a scope, choosing dot-prefix vs block form. */
|
|
927
|
+
function emitItem(ctx, node, scopeSegs, ind, lines) {
|
|
928
|
+
const rel = splitId(node.id).slice(scopeSegs.length).map(escapeD2Key).join(".");
|
|
929
|
+
if (isPurePrefix(ctx, node)) {
|
|
930
|
+
emitScope(ctx, node, node.id, scopeSegs, ind, lines);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
emitComments(nodeData(node).commentsBefore, ind, lines);
|
|
934
|
+
const label = labelHeader(node);
|
|
935
|
+
const structured = node.shape && STRUCTURED_SHAPES.has(node.shape);
|
|
936
|
+
const attrLines = nodeAttrLines(node, ind + 1);
|
|
937
|
+
const children = ctx.childrenOf.get(node.id) ?? [];
|
|
938
|
+
const ports = structured ? portLines(node, ind + 1) : [];
|
|
939
|
+
if (!(attrLines.length > 0 || children.length > 0 || ports.length > 0)) {
|
|
940
|
+
lines.push(`${indent(ind)}${rel}${label ? `: ${label}` : ""}`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
lines.push(`${indent(ind)}${rel}: {`);
|
|
944
|
+
if (label) lines.push(`${indent(ind + 1)}label: ${label}`);
|
|
945
|
+
if (structured) {
|
|
946
|
+
lines.push(`${indent(ind + 1)}shape: ${node.shape}`);
|
|
947
|
+
lines.push(...ports);
|
|
948
|
+
} else {
|
|
949
|
+
lines.push(...attrLines);
|
|
950
|
+
emitScope(ctx, node, node.id, splitId(node.id), ind + 1, lines);
|
|
951
|
+
}
|
|
952
|
+
lines.push(`${indent(ind)}}`);
|
|
953
|
+
}
|
|
954
|
+
function endpointRef(nodeId$1, port, scopeSegs) {
|
|
955
|
+
let ref = splitId(nodeId$1).slice(scopeSegs.length).map(escapeD2Key).join(".");
|
|
956
|
+
if (port) ref += `.${escapeD2Key(port)}`;
|
|
957
|
+
return ref;
|
|
958
|
+
}
|
|
959
|
+
function emitEdge(ctx, edge, ind, lines, scoped) {
|
|
960
|
+
ctx.emittedEdges.add(edge.id);
|
|
961
|
+
const d = edgeData(edge);
|
|
962
|
+
emitComments(d.commentsBefore, ind, lines);
|
|
963
|
+
const arrow = d.arrow ?? modeToArrow(edge);
|
|
964
|
+
const owner = scoped ? ctx.ownerOfEdge.get(edge.id) : void 0;
|
|
965
|
+
const scopeSegs = owner ? splitId(owner) : [];
|
|
966
|
+
let sId = edge.sourceId;
|
|
967
|
+
let tId = edge.targetId;
|
|
968
|
+
let sPort = edge.sourcePort;
|
|
969
|
+
let tPort = edge.targetPort;
|
|
970
|
+
if (arrow === "<-") {
|
|
971
|
+
[sId, tId] = [tId, sId];
|
|
972
|
+
[sPort, tPort] = [tPort, sPort];
|
|
973
|
+
}
|
|
974
|
+
const left = endpointRef(sId, sPort, scopeSegs);
|
|
975
|
+
const right = endpointRef(tId, tPort, scopeSegs);
|
|
976
|
+
let line = `${indent(ind)}${left} ${arrow} ${right}`;
|
|
977
|
+
if (edge.label != null && edge.label !== "") {
|
|
978
|
+
const lbl = d.labelBlock ? emitLabelBlock(edge.label, d.labelBlock) : escapeD2Label(edge.label);
|
|
979
|
+
line += `: ${lbl}`;
|
|
980
|
+
}
|
|
981
|
+
const blockLines = edgeBlockLines(edge);
|
|
982
|
+
if (blockLines.length > 0) {
|
|
983
|
+
lines.push(`${line} {`);
|
|
984
|
+
lines.push(...blockLines.map((l) => `${indent(ind + 1)}${l}`));
|
|
985
|
+
lines.push(`${indent(ind)}}`);
|
|
986
|
+
} else lines.push(line);
|
|
987
|
+
}
|
|
988
|
+
function edgeBlockLines(edge) {
|
|
989
|
+
const out = [];
|
|
990
|
+
if (edge.style) for (const [k, v] of Object.entries(edge.style)) out.push(`style.${k}: ${styleValueToD2(v)}`);
|
|
991
|
+
const d = edgeData(edge);
|
|
992
|
+
const { sourceArrowhead, targetArrowhead } = d;
|
|
993
|
+
if (sourceArrowhead?.shape) out.push(`source-arrowhead.shape: ${sourceArrowhead.shape}`);
|
|
994
|
+
if (targetArrowhead?.shape) out.push(`target-arrowhead.shape: ${targetArrowhead.shape}`);
|
|
995
|
+
if (d.reserved) for (const [k, v] of Object.entries(d.reserved)) out.push(`${k}: ${styleValueToD2(v)}`);
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
function modeToArrow(edge) {
|
|
999
|
+
switch (edge.mode) {
|
|
1000
|
+
case "undirected": return "--";
|
|
1001
|
+
case "bidirectional": return "<->";
|
|
1002
|
+
default: return "->";
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Single pass over container `data.order` lists, building both edge→owner and
|
|
1007
|
+
* owner→edges indexes. An edge "belongs" to a container when that container
|
|
1008
|
+
* references it in its order (sequence diagrams / scoped connections); all
|
|
1009
|
+
* other edges emit at root scope.
|
|
1010
|
+
*/
|
|
1011
|
+
function buildEdgeOwnership(graph) {
|
|
1012
|
+
const edgesByOwner = /* @__PURE__ */ new Map();
|
|
1013
|
+
const ownerOfEdge = /* @__PURE__ */ new Map();
|
|
1014
|
+
const edgeById = new Map(graph.edges.map((e) => [e.id, e]));
|
|
1015
|
+
for (const node of graph.nodes) {
|
|
1016
|
+
const order = nodeData(node).order;
|
|
1017
|
+
if (!order) continue;
|
|
1018
|
+
for (const refId of order) {
|
|
1019
|
+
const edge = edgeById.get(refId);
|
|
1020
|
+
if (!edge) continue;
|
|
1021
|
+
ownerOfEdge.set(refId, node.id);
|
|
1022
|
+
const arr = edgesByOwner.get(node.id) ?? [];
|
|
1023
|
+
arr.push(edge);
|
|
1024
|
+
edgesByOwner.set(node.id, arr);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
edgesByOwner,
|
|
1029
|
+
ownerOfEdge
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
function toD2(graph) {
|
|
1033
|
+
const lines = [];
|
|
1034
|
+
const data = graph.data ?? { diagramType: "d2" };
|
|
1035
|
+
if (data.leadingComments) for (const c of data.leadingComments) lines.push(`#${c ? " " + c.trimStart() : ""}`);
|
|
1036
|
+
if (graph.direction) lines.push(`direction: ${graph.direction}`);
|
|
1037
|
+
if (data.source?.vars && Object.keys(data.source.vars).length > 0) {
|
|
1038
|
+
lines.push("vars: {");
|
|
1039
|
+
for (const [k, v] of Object.entries(data.source.vars)) lines.push(` ${escapeD2Key(k)}: ${styleValueToD2(v)}`);
|
|
1040
|
+
lines.push("}");
|
|
1041
|
+
}
|
|
1042
|
+
if (data.source?.classes && Object.keys(data.source.classes).length > 0) {
|
|
1043
|
+
lines.push("classes: {");
|
|
1044
|
+
for (const [name, style] of Object.entries(data.source.classes)) {
|
|
1045
|
+
lines.push(` ${escapeD2Key(name)}: {`);
|
|
1046
|
+
for (const [k, v] of Object.entries(style)) lines.push(` style.${k}: ${styleValueToD2(v)}`);
|
|
1047
|
+
lines.push(" }");
|
|
1048
|
+
}
|
|
1049
|
+
lines.push("}");
|
|
1050
|
+
}
|
|
1051
|
+
if (data.source?.imports) for (const imp of data.source.imports) lines.push(`@${imp}`);
|
|
1052
|
+
if (graph.style && Object.keys(graph.style).length > 0) {
|
|
1053
|
+
lines.push("style: {");
|
|
1054
|
+
for (const [k, v] of Object.entries(graph.style)) lines.push(` ${k}: ${styleValueToD2(v)}`);
|
|
1055
|
+
lines.push("}");
|
|
1056
|
+
}
|
|
1057
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1058
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
1059
|
+
for (const node of graph.nodes) {
|
|
1060
|
+
nodeById.set(node.id, node);
|
|
1061
|
+
const key = node.parentId ?? null;
|
|
1062
|
+
const arr = childrenOf.get(key) ?? [];
|
|
1063
|
+
arr.push(node);
|
|
1064
|
+
childrenOf.set(key, arr);
|
|
1065
|
+
}
|
|
1066
|
+
const { edgesByOwner, ownerOfEdge } = buildEdgeOwnership(graph);
|
|
1067
|
+
const ctx = {
|
|
1068
|
+
childrenOf,
|
|
1069
|
+
nodeById,
|
|
1070
|
+
edgesByOwner,
|
|
1071
|
+
ownerOfEdge,
|
|
1072
|
+
emittedEdges: /* @__PURE__ */ new Set()
|
|
1073
|
+
};
|
|
1074
|
+
emitScope(ctx, null, null, [], 0, lines);
|
|
1075
|
+
for (const edge of graph.edges) {
|
|
1076
|
+
if (ctx.emittedEdges.has(edge.id)) continue;
|
|
1077
|
+
const owner = ownerOfEdge.get(edge.id);
|
|
1078
|
+
emitEdge(ctx, edge, owner ? splitId(owner).length : 0, lines, true);
|
|
1079
|
+
}
|
|
1080
|
+
if (data.trailingComments) for (const c of data.trailingComments) lines.push(`#${c ? " " + c.trimStart() : ""}`);
|
|
1081
|
+
return lines.join("\n") + "\n";
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
//#endregion
|
|
1085
|
+
//#region src/formats/d2/index.ts
|
|
1086
|
+
/**
|
|
1087
|
+
* Bidirectional converter for [d2](https://d2lang.com/) diagram syntax.
|
|
1088
|
+
*
|
|
1089
|
+
* @example
|
|
1090
|
+
* ```ts
|
|
1091
|
+
* import { d2Converter } from '@statelyai/graph/d2';
|
|
1092
|
+
*
|
|
1093
|
+
* const graph = d2Converter.from('a -> b: hello');
|
|
1094
|
+
* const text = d2Converter.to(graph);
|
|
1095
|
+
* ```
|
|
1096
|
+
*/
|
|
1097
|
+
const d2Converter = createFormatConverter(toD2, fromD2);
|
|
1098
|
+
|
|
1099
|
+
//#endregion
|
|
1100
|
+
export { d2Converter, fromD2, toD2 };
|