@graphty/graph-io 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +17 -4
  2. package/dist/chunks/{importer-BELrICWM.js → importer-BOxwmef_.js} +352 -19
  3. package/dist/chunks/importer-BOxwmef_.js.map +1 -0
  4. package/dist/graph-io.js +2 -2
  5. package/dist/graph-io.js.map +1 -1
  6. package/dist/json.js +1 -1
  7. package/dist/src/formats/json/dialect.d.ts +21 -4
  8. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  9. package/dist/src/formats/json/dialect.js +29 -4
  10. package/dist/src/formats/json/dialect.js.map +1 -1
  11. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  12. package/dist/src/formats/json/exporter.js +27 -7
  13. package/dist/src/formats/json/exporter.js.map +1 -1
  14. package/dist/src/formats/json/importer.d.ts +17 -4
  15. package/dist/src/formats/json/importer.d.ts.map +1 -1
  16. package/dist/src/formats/json/importer.js +356 -8
  17. package/dist/src/formats/json/importer.js.map +1 -1
  18. package/dist/src/formats/json/index.d.ts +1 -1
  19. package/dist/src/formats/json/index.d.ts.map +1 -1
  20. package/dist/src/formats/json/index.js +1 -1
  21. package/dist/src/formats/json/index.js.map +1 -1
  22. package/dist/src/index.d.ts +1 -1
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/sniff.d.ts +3 -3
  26. package/dist/src/sniff.d.ts.map +1 -1
  27. package/dist/src/sniff.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/common/escape.ts +5 -4
  30. package/src/formats/json/dialect.ts +40 -6
  31. package/src/formats/json/exporter.ts +28 -7
  32. package/src/formats/json/importer.ts +411 -21
  33. package/src/formats/json/index.ts +7 -1
  34. package/src/formats/neo4j/exporter.ts +43 -6
  35. package/src/formats/neo4j/importer.ts +56 -11
  36. package/src/formats/neo4j/index.ts +10 -3
  37. package/src/formats/pajek/exporter.ts +3 -1
  38. package/src/formats/pajek/importer.ts +284 -60
  39. package/src/formats/pajek/syntax.ts +98 -12
  40. package/src/index.ts +2 -0
  41. package/src/sniff.ts +3 -3
  42. package/dist/chunks/importer-BELrICWM.js.map +0 -1
@@ -5,7 +5,13 @@
5
5
  * index endpoints), JSON Graph Format v2 (nodes keyed by id, per-edge `directed`, hyperedges),
6
6
  * Cytoscape.js elements (`data.id` / `data.source` / `data.target`, `position`, `classes`,
7
7
  * `data.parent`), graphology serialisation (`key` / `attributes`, `undirected` edges, `options`)
8
- * and vis.js (`from` / `to`) -- and pushes it scalar by scalar into the sink.
8
+ * vis.js (`from` / `to`), and NetworkX adjacency_data (`nodes` + `adjacency`) and tree_data (nested
9
+ * `id` / `children`) -- and pushes it scalar by scalar into the sink.
10
+ *
11
+ * Python's json module writes the bare tokens NaN, Infinity and -Infinity, which strict JSON
12
+ * refuses; they are read as the JavaScript numbers with one warning. An integer literal beyond
13
+ * 2^53 is read as its exact digits (a string, the canonical id rule of the text formats) rather
14
+ * than a rounded float, again with one warning.
9
15
  *
10
16
  * JSON awaits the whole text (design section 8.4: `JSON.parse` on 100 MB is fine; a streaming
11
17
  * tokeniser is a later improvement). The parsed records are iterated in place; the importer never
@@ -33,7 +39,7 @@ import { readText, throwIfAborted } from "../../common/input.js";
33
39
  import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
34
40
  import { ImportReportBuilder } from "../../common/report.js";
35
41
  import { weightFromValue } from "../../common/weights.js";
36
- import { CLASSES_COLUMN, CYTOSCAPE_ELEMENT_KEYS, CYTOSCAPE_STRUCTURAL_KEYS, DIALECT_DEFAULT_DIRECTED, hasKey, isJsonDialect, isJsonObject, JSON_DIALECTS, META_KEY, NODE_LINK_SOURCE_KEYS, NODE_LINK_TARGET_KEYS, PARENT_COLUMN, POSITION_COLUMN, sniffJsonDialect, SUFFIX, } from "./dialect.js";
42
+ import { CLASSES_COLUMN, CYTOSCAPE_ELEMENT_KEYS, CYTOSCAPE_STRUCTURAL_KEYS, DIALECT_DEFAULT_DIRECTED, hasKey, isJsonImportDialect, isJsonObject, JSON_IMPORT_DIALECTS, META_KEY, NODE_LINK_SOURCE_KEYS, NODE_LINK_TARGET_KEYS, PARENT_COLUMN, POSITION_COLUMN, sniffJsonDialect, SUFFIX, } from "./dialect.js";
37
43
  /**
38
44
  * The issue codes the JSON importer records (design section 8.6), by name: the codes shared with
39
45
  * the other importers (src/common/codes.ts) and the JSON-specific ones. A key is the code without
@@ -96,6 +102,10 @@ export const JSON_ISSUE = Object.freeze({
96
102
  INVALID_ENCODING: INVALID_ENCODING_CODE,
97
103
  /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
98
104
  ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
105
+ /** The document uses the non-standard tokens NaN / Infinity / -Infinity (Python's json writes them); read as numbers. */
106
+ NONSTANDARD_NUMBER: "W_JSON_NONSTANDARD_NUMBER",
107
+ /** Integer literals beyond 2^53 were read as their exact digits (strings), not as rounded numbers. */
108
+ BIG_INTEGER: "W_JSON_BIG_INTEGER",
99
109
  /** A declared encoding the platform cannot decode was ignored. */
100
110
  UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
101
111
  });
@@ -152,8 +162,8 @@ const VIS_TARGET_KEYS = Object.freeze(["to"]);
152
162
  function resolveJsonOptions(options) {
153
163
  const o = options ?? {};
154
164
  const dialect = o.dialect ?? "auto";
155
- if (dialect !== "auto" && !isJsonDialect(dialect)) {
156
- throw unsupportedOption("dialect", dialect, [...JSON_DIALECTS, "auto"]);
165
+ if (dialect !== "auto" && !isJsonImportDialect(dialect)) {
166
+ throw unsupportedOption("dialect", dialect, [...JSON_IMPORT_DIALECTS, "auto"]);
157
167
  }
158
168
  const indexLinks = o.indexLinks ?? "auto";
159
169
  if (indexLinks !== "auto" && typeof indexLinks !== "boolean") {
@@ -730,13 +740,145 @@ function parseDocument(text, report) {
730
740
  if (text.trim().length === 0) {
731
741
  report.fail(JSON_ISSUE.EMPTY_INPUT, "the input is empty");
732
742
  }
743
+ let root;
744
+ let syntaxError = null;
733
745
  try {
734
- return JSON.parse(text);
746
+ root = JSON.parse(text);
735
747
  }
736
748
  catch (err) {
737
- const message = err instanceof Error ? err.message : String(err);
738
- return report.fail(JSON_ISSUE.SYNTAX, `invalid JSON: ${message}`);
749
+ syntaxError = err instanceof Error ? err.message : String(err);
750
+ }
751
+ // the fast path: strict JSON without a digit run long enough to be an unsafe integer
752
+ if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text)) {
753
+ return root;
754
+ }
755
+ const scan = rewriteNumbers(text);
756
+ if (scan.tokens.size > 0 || scan.bigIntegers.length > 0) {
757
+ try {
758
+ root = JSON.parse(scan.text, scan.tokens.size > 0 ? reviveNonstandard : undefined);
759
+ syntaxError = null;
760
+ }
761
+ catch {
762
+ // the rewrite did not make it valid JSON: report the parser's message on the original text
763
+ }
764
+ }
765
+ if (syntaxError !== null) {
766
+ return report.fail(JSON_ISSUE.SYNTAX, `invalid JSON: ${syntaxError}`);
767
+ }
768
+ if (scan.tokens.size > 0) {
769
+ report.warning("coercion", JSON_ISSUE.NONSTANDARD_NUMBER, `the document uses the non-standard token(s) ${[...scan.tokens].join(", ")}, which strict JSON does not allow; read as numbers`);
770
+ }
771
+ if (scan.bigIntegers.length > 0) {
772
+ const shown = scan.bigIntegers.slice(0, BIG_INTEGERS_SHOWN).join(", ");
773
+ const more = scan.bigIntegers.length - BIG_INTEGERS_SHOWN;
774
+ report.warning("precision", JSON_ISSUE.BIG_INTEGER, `${scan.bigIntegers.length} integer(s) beyond 2^53 kept as text so no digit is lost: ${shown}${more > 0 ? ` and ${more} more` : ""}`);
775
+ }
776
+ return root;
777
+ }
778
+ /** A run of 16 digits not inside a fraction: the shortest integer literal that can exceed 2^53 (9007199254740992). */
779
+ const MAYBE_UNSAFE_INTEGER = /(?<![0-9.])[0-9]{16}/;
780
+ /** How many of the integers kept as text the warning lists. */
781
+ const BIG_INTEGERS_SHOWN = 10;
782
+ /**
783
+ * The prefix of the string a non-standard token is rewritten to (a NUL character first, which no
784
+ * sensible attribute value starts with); reviveNonstandard() turns it back into the number.
785
+ */
786
+ const NONSTANDARD_SENTINEL = `${String.fromCharCode(0)}graph-io:`;
787
+ /** The non-standard tokens Python's json module writes, longest first so -Infinity wins over a bare minus. */
788
+ const NONSTANDARD_TOKENS = [
789
+ ["-Infinity", -Infinity],
790
+ ["Infinity", Infinity],
791
+ ["NaN", NaN],
792
+ ];
793
+ /** A JSON integer literal (no fraction, no exponent, no leading zero), as CANONICAL_INTEGER in common/ids.ts. */
794
+ const INTEGER_LITERAL = /^-?(0|[1-9][0-9]*)$/;
795
+ /**
796
+ * Rewrite the numbers JSON.parse cannot read exactly, outside strings and in value positions only:
797
+ * NaN / Infinity / -Infinity become sentinel strings, an integer literal that is not a safe
798
+ * integer becomes a string of its digits. A container stack tells a value position (after `:`,
799
+ * `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays invalid.
800
+ * @param text - the document text
801
+ * @returns the rewritten text, the non-standard tokens seen and the integer literals quoted
802
+ */
803
+ function rewriteNumbers(text) {
804
+ const parts = [];
805
+ const tokens = new Set();
806
+ const bigIntegers = [];
807
+ const arrays = [];
808
+ let expectValue = true;
809
+ let copied = 0;
810
+ let i = 0;
811
+ const n = text.length;
812
+ while (i < n) {
813
+ const ch = text[i];
814
+ if (ch === '"') {
815
+ i++;
816
+ while (i < n && text[i] !== '"') {
817
+ i += text[i] === "\\" ? 2 : 1;
818
+ }
819
+ i++;
820
+ expectValue = false;
821
+ continue;
822
+ }
823
+ if (ch === "{" || ch === "[") {
824
+ arrays.push(ch === "[");
825
+ expectValue = ch === "[";
826
+ }
827
+ else if (ch === "}" || ch === "]") {
828
+ arrays.pop();
829
+ expectValue = false;
830
+ }
831
+ else if (ch === ":") {
832
+ expectValue = true;
833
+ }
834
+ else if (ch === ",") {
835
+ expectValue = arrays.length > 0 && arrays[arrays.length - 1];
836
+ }
837
+ else if (expectValue && ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") {
838
+ expectValue = false;
839
+ const token = NONSTANDARD_TOKENS.find(([word]) => text.startsWith(word, i));
840
+ let end = i;
841
+ let replacement = null;
842
+ if (token !== undefined) {
843
+ end = i + token[0].length;
844
+ tokens.add(token[0]);
845
+ replacement = JSON.stringify(`${NONSTANDARD_SENTINEL}${token[0]}`);
846
+ }
847
+ else if (ch === "-" || (ch >= "0" && ch <= "9")) {
848
+ end = i + 1;
849
+ while (end < n && "0123456789+-.eE".includes(text[end])) {
850
+ end++;
851
+ }
852
+ const literal = text.slice(i, end);
853
+ if (INTEGER_LITERAL.test(literal) && !Number.isSafeInteger(Number(literal))) {
854
+ bigIntegers.push(literal);
855
+ replacement = `"${literal}"`;
856
+ }
857
+ }
858
+ if (replacement !== null) {
859
+ parts.push(text.slice(copied, i), replacement);
860
+ copied = end;
861
+ }
862
+ i = Math.max(end, i + 1);
863
+ continue;
864
+ }
865
+ i++;
866
+ }
867
+ parts.push(text.slice(copied));
868
+ return { text: parts.join(""), tokens, bigIntegers };
869
+ }
870
+ /**
871
+ * The JSON.parse reviver that turns the sentinel strings of rewriteNumbers() back into numbers.
872
+ * @param _key - the member key (unused)
873
+ * @param value - the parsed value
874
+ * @returns the number for a sentinel string, the value otherwise
875
+ */
876
+ function reviveNonstandard(_key, value) {
877
+ if (typeof value === "string" && value.startsWith(NONSTANDARD_SENTINEL)) {
878
+ const found = NONSTANDARD_TOKENS.find(([word]) => word === value.slice(NONSTANDARD_SENTINEL.length));
879
+ return found === undefined ? value : found[1];
739
880
  }
881
+ return value;
740
882
  }
741
883
  /**
742
884
  * The dialect to read: the forced one, else the shape rule of sniffJsonDialect(); a document
@@ -760,7 +902,7 @@ function detectDialect(root, forced, report) {
760
902
  if (!isJsonObject(root)) {
761
903
  return report.fail(JSON_ISSUE.DIALECT, `the document is a JSON ${describe(root)}, not a graph object`);
762
904
  }
763
- return report.fail(JSON_ISSUE.DIALECT, "no known dialect: expected nodes / links / edges (node-link), elements (Cytoscape) or graph (JGF)");
905
+ return report.fail(JSON_ISSUE.DIALECT, "no known dialect: expected nodes / links / edges (node-link), nodes / adjacency (adjacency), children (tree), elements (Cytoscape) or graph (JGF)");
764
906
  }
765
907
  /**
766
908
  * A section that must be an array: fail when it is something else.
@@ -1151,6 +1293,206 @@ function importVis(ctx, root) {
1151
1293
  }
1152
1294
  ctx.setMeta({ dialect: "vis", nodeIdKey, sourceKey: sourceKey ?? undefined, targetKey: targetKey ?? undefined }, ctx.weightOriginPatch());
1153
1295
  }
1296
+ // ============================================================ NetworkX adjacency_data / tree_data
1297
+ /**
1298
+ * Read a NetworkX adjacency_data document: `nodes` as in node-link, and `adjacency[i]` the
1299
+ * neighbour list of `nodes[i]`, one `{ id, key?, ...attributes }` entry per edge. An undirected
1300
+ * file lists every edge from both ends, so an entry whose mirror (the same pair and `key`) was
1301
+ * already read is that edge again and is not pushed twice; a self-loop is listed once.
1302
+ * @param ctx - the context
1303
+ * @param root - the document
1304
+ */
1305
+ function importAdjacency(ctx, root) {
1306
+ const { report } = ctx;
1307
+ const nodes = arraySection(root.nodes, "nodes", report) ?? [];
1308
+ const adjacency = arraySection(root.adjacency, "adjacency", report);
1309
+ if (adjacency === null) {
1310
+ report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no adjacency array; the graph has no edges", {
1311
+ element: "adjacency",
1312
+ });
1313
+ }
1314
+ const directed = flagOf(root.directed, "directed", ctx.defaultDirected("adjacency"), report);
1315
+ const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
1316
+ ctx.setHeader(directed);
1317
+ // adjacency_data writes the graph dict as a list of [key, value] pairs
1318
+ ctx.writeGraphDict(isPairList(root.graph) ? Object.fromEntries(root.graph) : root.graph, "graph");
1319
+ for (const key of Object.keys(root)) {
1320
+ if (key !== "nodes" && key !== "adjacency" && key !== "directed" && key !== "multigraph" && key !== "graph") {
1321
+ report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${describe(root[key])}) is not read; dropped`, {
1322
+ element: key,
1323
+ });
1324
+ }
1325
+ }
1326
+ const lists = adjacency ?? [];
1327
+ ctx.sink.reserve(nodes.length, lists.reduce((sum, list) => sum + (Array.isArray(list) ? list.length : 0), 0));
1328
+ const idKey = ctx.json.nodeIdKey ?? "id";
1329
+ ctx.reportNodeIdFrom("adjacency", `the ${JSON.stringify(idKey)} key`);
1330
+ const owners = [];
1331
+ for (let i = 0; i < nodes.length; i++) {
1332
+ const element = `nodes[${i}]`;
1333
+ const record = nodes[i];
1334
+ let pushed = null;
1335
+ if (!isJsonObject(record)) {
1336
+ ctx.badElement("node", element);
1337
+ }
1338
+ else {
1339
+ const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
1340
+ if (id === null) {
1341
+ ctx.countSkipped("node");
1342
+ }
1343
+ else {
1344
+ const index = ctx.pushNode(id, element);
1345
+ if (index >= 0) {
1346
+ pushed = id;
1347
+ writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== idKey);
1348
+ }
1349
+ }
1350
+ }
1351
+ owners.push(pushed);
1352
+ }
1353
+ throwIfAborted(ctx.options.signal);
1354
+ const kind = ctx.uniformKind();
1355
+ const { weightFrom } = ctx.options;
1356
+ // undirected: entries read once whose mirror is still to come, by pair and key
1357
+ const pending = new Map();
1358
+ for (let i = 0; i < lists.length; i++) {
1359
+ const element = `adjacency[${i}]`;
1360
+ const list = lists[i];
1361
+ if (!Array.isArray(list)) {
1362
+ report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not an array`, { element });
1363
+ continue;
1364
+ }
1365
+ const source = i < owners.length ? owners[i] : null;
1366
+ if (source === null) {
1367
+ const why = i < nodes.length ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
1368
+ report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge(s) are skipped`, {
1369
+ element,
1370
+ });
1371
+ report.counts.skippedEdges += list.length;
1372
+ continue;
1373
+ }
1374
+ for (let j = 0; j < list.length; j++) {
1375
+ const entry = `${element}[${j}]`;
1376
+ const record = list[j];
1377
+ if (!isJsonObject(record)) {
1378
+ ctx.badElement("edge", entry);
1379
+ continue;
1380
+ }
1381
+ if (!hasKey(record, idKey)) {
1382
+ ctx.missingEndpoint(entry, idKey);
1383
+ continue;
1384
+ }
1385
+ try {
1386
+ const target = ctx.coerceId(record[idKey], `${entry}.${idKey}`);
1387
+ if (target === null) {
1388
+ ctx.countSkipped("edge");
1389
+ continue;
1390
+ }
1391
+ if (!directed && isMirroredEntry(pending, source, target, record.key)) {
1392
+ continue;
1393
+ }
1394
+ const edge = ctx.pushEdge(source, target, kind, ctx.weightOf(record), entry);
1395
+ for (const key of Object.keys(record)) {
1396
+ if (key !== idKey && key !== weightFrom) {
1397
+ ctx.edges.write(edge, key, record[key], SUFFIX.data);
1398
+ }
1399
+ }
1400
+ }
1401
+ catch (err) {
1402
+ ctx.skip(err, "edge", entry);
1403
+ }
1404
+ }
1405
+ }
1406
+ ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
1407
+ }
1408
+ /**
1409
+ * Whether an undirected adjacency entry is the mirror of one already read (the same unordered
1410
+ * pair and key), consuming it; otherwise the entry is remembered as awaiting its mirror. A
1411
+ * self-loop is listed once and never awaits one.
1412
+ * @param pending - the entries awaiting their mirror, by owner, other end and key
1413
+ * @param source - the owner of the list
1414
+ * @param target - the entry's node
1415
+ * @param key - the entry's multigraph key, or undefined
1416
+ * @returns true when the entry is a mirror and must not be pushed
1417
+ */
1418
+ function isMirroredEntry(pending, source, target, key) {
1419
+ const a = JSON.stringify(source);
1420
+ const b = JSON.stringify(target);
1421
+ if (a === b) {
1422
+ return false;
1423
+ }
1424
+ const k = JSON.stringify(key ?? null);
1425
+ // an entry is the mirror of one listed by the other end, never of a parallel entry of its own list
1426
+ const mirror = `${b} ${a} ${k}`;
1427
+ const waiting = pending.get(mirror) ?? 0;
1428
+ if (waiting > 0) {
1429
+ pending.set(mirror, waiting - 1);
1430
+ return true;
1431
+ }
1432
+ const own = `${a} ${b} ${k}`;
1433
+ pending.set(own, (pending.get(own) ?? 0) + 1);
1434
+ return false;
1435
+ }
1436
+ /**
1437
+ * Whether a value is a list of [string, value] pairs, the shape adjacency_data gives the graph dict.
1438
+ * @param value - the value
1439
+ * @returns true for an array whose items are all two-element arrays with a string first
1440
+ */
1441
+ function isPairList(value) {
1442
+ return Array.isArray(value) && value.every((p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string");
1443
+ }
1444
+ /**
1445
+ * Read a NetworkX tree_data document: a nested record with the node id, its attributes and a
1446
+ * `children` array of records of the same shape; every child gets a directed edge from its parent.
1447
+ * Walked depth first in document order with an explicit stack. A record without a usable id is
1448
+ * reported and skipped, and its children are still read (as roots, without an edge).
1449
+ * @param ctx - the context
1450
+ * @param root - the root record
1451
+ */
1452
+ function importTree(ctx, root) {
1453
+ const idKey = ctx.json.nodeIdKey ?? "id";
1454
+ ctx.setHeader(ctx.defaultDirected("tree"));
1455
+ ctx.reportNodeIdFrom("tree", `the ${JSON.stringify(idKey)} key`);
1456
+ const kind = ctx.uniformKind();
1457
+ const stack = [
1458
+ { record: root, element: "root", parent: null },
1459
+ ];
1460
+ for (let item = stack.pop(); item !== undefined; item = stack.pop()) {
1461
+ const { record, element, parent } = item;
1462
+ if (!isJsonObject(record)) {
1463
+ ctx.badElement("node", element);
1464
+ continue;
1465
+ }
1466
+ let id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
1467
+ if (id === null) {
1468
+ ctx.countSkipped("node");
1469
+ }
1470
+ else if (ctx.pushNode(id, element) < 0) {
1471
+ id = null;
1472
+ }
1473
+ else {
1474
+ writeFlat(ctx, ctx.nodes, ctx.sink.indexOf(id), record, id, (key) => key !== idKey && key !== "children");
1475
+ if (parent !== null) {
1476
+ try {
1477
+ ctx.pushEdge(parent, id, kind, undefined, element);
1478
+ }
1479
+ catch (err) {
1480
+ ctx.skip(err, "edge", element);
1481
+ }
1482
+ }
1483
+ }
1484
+ const { children } = record;
1485
+ if (Array.isArray(children)) {
1486
+ for (let k = children.length - 1; k >= 0; k--) {
1487
+ stack.push({ record: children[k], element: `${element}.children[${k}]`, parent: id });
1488
+ }
1489
+ }
1490
+ else if (children !== undefined && children !== null) {
1491
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: children must be an array, found ${describe(children)}`, { element });
1492
+ }
1493
+ }
1494
+ ctx.setMeta({});
1495
+ }
1154
1496
  // ============================================================ graphology
1155
1497
  /**
1156
1498
  * Read a graphology serialisation: `options.type` decides the header direction ("mixed" or absent:
@@ -1885,6 +2227,12 @@ function readGraph(root, dialect, sink, report, resolved, json, options) {
1885
2227
  case "vis":
1886
2228
  importVis(ctx, doc);
1887
2229
  break;
2230
+ case "adjacency":
2231
+ importAdjacency(ctx, doc);
2232
+ break;
2233
+ case "tree":
2234
+ importTree(ctx, doc);
2235
+ break;
1888
2236
  default: {
1889
2237
  const name = dialect;
1890
2238
  throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {