@graphty/graph-io 0.3.40 → 0.3.41

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 (189) hide show
  1. package/dist/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
  2. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
  3. package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
  4. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
  5. package/dist/chunks/{escape-BJRLd3vJ.js → escape-DBb8zuNE.js} +2 -2
  6. package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-DBb8zuNE.js.map} +1 -1
  7. package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
  8. package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
  9. package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-BqtmlxNr.js} +545 -459
  10. package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
  11. package/dist/chunks/{exporter-6ocL_9x_.js → exporter-CS35R6Fg.js} +4 -4
  12. package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-CS35R6Fg.js.map} +1 -1
  13. package/dist/chunks/{exporter-BVXnV9-9.js → exporter-D8r1N2dU.js} +28 -16
  14. package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
  15. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
  16. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
  17. package/dist/chunks/{importer-CPqnIY8m.js → importer-BKC7AmCA.js} +42 -29
  18. package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
  19. package/dist/chunks/{importer-D_srHyj6.js → importer-BdWHmlmG.js} +4 -4
  20. package/dist/chunks/{importer-D_srHyj6.js.map → importer-BdWHmlmG.js.map} +1 -1
  21. package/dist/chunks/{importer-AjwOJrn2.js → importer-BfbzWk6k.js} +202 -181
  22. package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
  23. package/dist/chunks/{importer-BpuS3J8C.js → importer-BmaZcskq.js} +73 -60
  24. package/dist/chunks/importer-BmaZcskq.js.map +1 -0
  25. package/dist/chunks/{importer-DEp_fYjf.js → importer-DviEsHy4.js} +92 -64
  26. package/dist/chunks/importer-DviEsHy4.js.map +1 -0
  27. package/dist/chunks/{importer-DcKB97kW.js → importer-S-abxJHn.js} +5 -5
  28. package/dist/chunks/{importer-DcKB97kW.js.map → importer-S-abxJHn.js.map} +1 -1
  29. package/dist/chunks/{importer-BJlGAOht.js → importer-r29cWnEf.js} +3 -3
  30. package/dist/chunks/{importer-BJlGAOht.js.map → importer-r29cWnEf.js.map} +1 -1
  31. package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DdF-etPT.js} +3 -3
  32. package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DdF-etPT.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
  34. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
  35. package/dist/chunks/{records-r3SFHz11.js → records-Uc3LkABx.js} +15 -12
  36. package/dist/chunks/records-Uc3LkABx.js.map +1 -0
  37. package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
  38. package/dist/chunks/text-Bh9SaAob.js.map +1 -0
  39. package/dist/csv.js +67 -49
  40. package/dist/csv.js.map +1 -1
  41. package/dist/cx.js +1 -1
  42. package/dist/cx2.js +2 -2
  43. package/dist/cys.js +1 -1
  44. package/dist/dot.js +1 -1
  45. package/dist/gexf.js +195 -97
  46. package/dist/gexf.js.map +1 -1
  47. package/dist/gml.js +170 -149
  48. package/dist/gml.js.map +1 -1
  49. package/dist/graph-io.js +26 -26
  50. package/dist/graphml.js +2 -2
  51. package/dist/json.js +1 -1
  52. package/dist/neo4j.js +103 -65
  53. package/dist/neo4j.js.map +1 -1
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/attributes.d.ts.map +1 -1
  57. package/dist/src/common/attributes.js +34 -9
  58. package/dist/src/common/attributes.js.map +1 -1
  59. package/dist/src/common/input.d.ts.map +1 -1
  60. package/dist/src/common/input.js +180 -102
  61. package/dist/src/common/input.js.map +1 -1
  62. package/dist/src/common/text.d.ts +7 -0
  63. package/dist/src/common/text.d.ts.map +1 -1
  64. package/dist/src/common/text.js +15 -7
  65. package/dist/src/common/text.js.map +1 -1
  66. package/dist/src/common/zip.d.ts.map +1 -1
  67. package/dist/src/common/zip.js +36 -19
  68. package/dist/src/common/zip.js.map +1 -1
  69. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  70. package/dist/src/formats/csv/exporter.js +77 -25
  71. package/dist/src/formats/csv/exporter.js.map +1 -1
  72. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  73. package/dist/src/formats/csv/importer.js +16 -6
  74. package/dist/src/formats/csv/importer.js.map +1 -1
  75. package/dist/src/formats/csv/records.d.ts.map +1 -1
  76. package/dist/src/formats/csv/records.js +18 -10
  77. package/dist/src/formats/csv/records.js.map +1 -1
  78. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  79. package/dist/src/formats/cys/tables.js +11 -8
  80. package/dist/src/formats/cys/tables.js.map +1 -1
  81. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  82. package/dist/src/formats/gexf/exporter.js +65 -37
  83. package/dist/src/formats/gexf/exporter.js.map +1 -1
  84. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  85. package/dist/src/formats/gexf/importer.js +138 -50
  86. package/dist/src/formats/gexf/importer.js.map +1 -1
  87. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/gml/exporter.js +51 -35
  89. package/dist/src/formats/gml/exporter.js.map +1 -1
  90. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  91. package/dist/src/formats/gml/importer.js +103 -58
  92. package/dist/src/formats/gml/importer.js.map +1 -1
  93. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  94. package/dist/src/formats/gml/syntax.js +17 -8
  95. package/dist/src/formats/gml/syntax.js.map +1 -1
  96. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/graphml/exporter.js +34 -24
  98. package/dist/src/formats/graphml/exporter.js.map +1 -1
  99. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  100. package/dist/src/formats/graphml/importer.js +17 -13
  101. package/dist/src/formats/graphml/importer.js.map +1 -1
  102. package/dist/src/formats/graphml/tree.js +23 -14
  103. package/dist/src/formats/graphml/tree.js.map +1 -1
  104. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  105. package/dist/src/formats/graphml/yfiles.js +22 -10
  106. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  107. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  108. package/dist/src/formats/json/exporter.js +38 -21
  109. package/dist/src/formats/json/exporter.js.map +1 -1
  110. package/dist/src/formats/json/importer.d.ts.map +1 -1
  111. package/dist/src/formats/json/importer.js +536 -343
  112. package/dist/src/formats/json/importer.js.map +1 -1
  113. package/dist/src/formats/json/obographs.js +29 -17
  114. package/dist/src/formats/json/obographs.js.map +1 -1
  115. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  116. package/dist/src/formats/neo4j/exporter.js +73 -43
  117. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  118. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  119. package/dist/src/formats/neo4j/header.js +37 -16
  120. package/dist/src/formats/neo4j/header.js.map +1 -1
  121. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  122. package/dist/src/formats/neo4j/importer.js +21 -11
  123. package/dist/src/formats/neo4j/importer.js.map +1 -1
  124. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  125. package/dist/src/formats/obo/exporter.js +18 -7
  126. package/dist/src/formats/obo/exporter.js.map +1 -1
  127. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  128. package/dist/src/formats/pajek/exporter.js +142 -73
  129. package/dist/src/formats/pajek/exporter.js.map +1 -1
  130. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  131. package/dist/src/formats/pajek/importer.js +26 -15
  132. package/dist/src/formats/pajek/importer.js.map +1 -1
  133. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  134. package/dist/src/formats/pajek/syntax.js +85 -58
  135. package/dist/src/formats/pajek/syntax.js.map +1 -1
  136. package/dist/src/formats/xgmml/emit.d.ts +7 -0
  137. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  138. package/dist/src/formats/xgmml/emit.js +79 -40
  139. package/dist/src/formats/xgmml/emit.js.map +1 -1
  140. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  141. package/dist/src/formats/xgmml/exporter.js +38 -12
  142. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  143. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  144. package/dist/src/formats/xgmml/importer.js +38 -18
  145. package/dist/src/formats/xgmml/importer.js.map +1 -1
  146. package/dist/src/sniff.js +109 -57
  147. package/dist/src/sniff.js.map +1 -1
  148. package/dist/xgmml.js +2 -2
  149. package/package.json +2 -2
  150. package/src/common/attributes.ts +59 -19
  151. package/src/common/input.ts +216 -102
  152. package/src/common/text.ts +24 -15
  153. package/src/common/zip.ts +47 -18
  154. package/src/formats/csv/exporter.ts +103 -39
  155. package/src/formats/csv/importer.ts +17 -6
  156. package/src/formats/csv/records.ts +19 -10
  157. package/src/formats/cys/tables.ts +11 -10
  158. package/src/formats/gexf/exporter.ts +67 -36
  159. package/src/formats/gexf/importer.ts +173 -63
  160. package/src/formats/gml/exporter.ts +78 -82
  161. package/src/formats/gml/importer.ts +119 -68
  162. package/src/formats/gml/syntax.ts +18 -8
  163. package/src/formats/graphml/exporter.ts +44 -24
  164. package/src/formats/graphml/importer.ts +18 -11
  165. package/src/formats/graphml/tree.ts +22 -12
  166. package/src/formats/graphml/yfiles.ts +23 -10
  167. package/src/formats/json/exporter.ts +40 -21
  168. package/src/formats/json/importer.ts +701 -385
  169. package/src/formats/json/obographs.ts +35 -15
  170. package/src/formats/neo4j/exporter.ts +79 -41
  171. package/src/formats/neo4j/header.ts +50 -16
  172. package/src/formats/neo4j/importer.ts +21 -10
  173. package/src/formats/obo/exporter.ts +34 -17
  174. package/src/formats/pajek/exporter.ts +180 -117
  175. package/src/formats/pajek/importer.ts +27 -15
  176. package/src/formats/pajek/syntax.ts +99 -58
  177. package/src/formats/xgmml/emit.ts +88 -40
  178. package/src/formats/xgmml/exporter.ts +40 -12
  179. package/src/formats/xgmml/importer.ts +45 -23
  180. package/src/sniff.ts +115 -59
  181. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  182. package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
  183. package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
  184. package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
  185. package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
  186. package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
  187. package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
  188. package/dist/chunks/records-r3SFHz11.js.map +0 -1
  189. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -1794,100 +1794,141 @@ function importAdjacency(ctx, root) {
1794
1794
  ctx.setHeader(directed);
1795
1795
  // adjacency_data writes the graph dict as a list of [key, value] pairs
1796
1796
  ctx.writeGraphDict(isPairList(root.graph) ? Object.fromEntries(root.graph) : root.graph, "graph");
1797
- for (const key of Object.keys(root)) {
1798
- if (key !== "nodes" && key !== "adjacency" && key !== "directed" && key !== "multigraph" && key !== "graph") {
1799
- report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${describe(root[key])}) is not read; dropped`, {
1800
- element: key,
1801
- });
1802
- }
1803
- }
1797
+ reportUnreadAdjacencyKeys(ctx, root);
1804
1798
  const lists = adjacency ?? [];
1805
1799
  ctx.sink.reserve(nodes.length, lists.reduce((sum, list) => sum + (Array.isArray(list) ? list.length : 0), 0));
1806
1800
  const idKey = ctx.json.nodeIdKey ?? "id";
1807
1801
  ctx.reportNodeIdFrom("adjacency", `the ${JSON.stringify(idKey)} key`);
1808
- const owners = [];
1809
- for (let i = 0; i < nodes.length; i++) {
1810
- const element = `nodes[${i}]`;
1811
- const record = nodes[i];
1812
- let pushed = null;
1813
- if (!isJsonObject(record)) {
1814
- ctx.badElement("node", element);
1815
- }
1816
- else {
1817
- const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
1818
- if (id === null) {
1819
- ctx.countSkipped("node");
1820
- }
1821
- else {
1822
- const index = ctx.pushNode(id, element);
1823
- if (index >= 0) {
1824
- pushed = id;
1825
- writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
1826
- }
1827
- }
1828
- }
1829
- owners.push(pushed);
1830
- }
1802
+ const owners = nodes.map((record, i) => pushAdjacencyNode(ctx, record, `nodes[${i}]`, idKey));
1831
1803
  throwIfAborted(ctx.options.signal);
1832
- const kind = ctx.uniformKind();
1833
- const { weightFrom } = ctx.options;
1834
- // undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
1835
- const pending = new Map();
1836
- let disagreeing = 0;
1804
+ const reading = {
1805
+ owners,
1806
+ nodeCount: nodes.length,
1807
+ directed,
1808
+ kind: ctx.uniformKind(),
1809
+ idKey,
1810
+ // undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
1811
+ pending: new Map(),
1812
+ disagreeing: 0,
1813
+ };
1837
1814
  for (let i = 0; i < lists.length; i++) {
1838
- const element = `adjacency[${i}]`;
1839
- const list = lists[i];
1840
- if (!Array.isArray(list)) {
1841
- report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not an array`, { element });
1842
- continue;
1843
- }
1844
- const source = i < owners.length ? owners[i] : null;
1845
- if (source === null) {
1846
- const why = i < nodes.length ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
1847
- report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`, {
1848
- element,
1815
+ readAdjacencyList(ctx, reading, i, lists[i]);
1816
+ }
1817
+ reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, reading.disagreeing, reading.pending);
1818
+ ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
1819
+ }
1820
+ /** The top-level keys of an adjacency_data document that are read. */
1821
+ const ADJACENCY_DOCUMENT_KEYS = new Set(["nodes", "adjacency", "directed", "multigraph", "graph"]);
1822
+ /**
1823
+ * Report the top-level keys of an adjacency_data document that are not read.
1824
+ * @param ctx - the context
1825
+ * @param root - the document
1826
+ */
1827
+ function reportUnreadAdjacencyKeys(ctx, root) {
1828
+ for (const key of Object.keys(root)) {
1829
+ if (!ADJACENCY_DOCUMENT_KEYS.has(key)) {
1830
+ ctx.report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${describe(root[key])}) is not read; dropped`, {
1831
+ element: key,
1849
1832
  });
1850
- report.counts.skippedEdges += list.length;
1851
- continue;
1852
1833
  }
1853
- for (let j = 0; j < list.length; j++) {
1854
- const entry = `${element}[${j}]`;
1855
- const record = list[j];
1856
- if (!isJsonObject(record)) {
1857
- ctx.badElement("edge", entry);
1858
- continue;
1859
- }
1860
- if (!hasKey(record, idKey)) {
1861
- ctx.missingEndpoint(entry, idKey);
1862
- continue;
1863
- }
1864
- try {
1865
- const target = ctx.coerceId(record[idKey], `${entry}.${idKey}`);
1866
- if (target === null) {
1867
- ctx.countSkipped("edge");
1868
- continue;
1869
- }
1870
- if (!directed) {
1871
- const mirror = mirroredEntry(pending, source, target, record, idKey);
1872
- disagreeing += mirror === "disagrees" ? 1 : 0;
1873
- if (mirror !== "new") {
1874
- continue;
1875
- }
1876
- }
1877
- const edge = ctx.pushEdge(source, target, kind, ctx.weightOf(record), entry);
1878
- for (const key of Object.keys(record)) {
1879
- if (key !== idKey && key !== weightFrom) {
1880
- ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
1881
- }
1882
- }
1834
+ }
1835
+ }
1836
+ /**
1837
+ * Push one node record of an adjacency_data document.
1838
+ * @param ctx - the context
1839
+ * @param record - the node record
1840
+ * @param element - the element name
1841
+ * @param idKey - the key that holds the id
1842
+ * @returns the node's id, or null when it was skipped
1843
+ */
1844
+ function pushAdjacencyNode(ctx, record, element, idKey) {
1845
+ if (!isJsonObject(record)) {
1846
+ ctx.badElement("node", element);
1847
+ return null;
1848
+ }
1849
+ const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
1850
+ if (id === null) {
1851
+ ctx.countSkipped("node");
1852
+ return null;
1853
+ }
1854
+ const index = ctx.pushNode(id, element);
1855
+ if (index < 0) {
1856
+ return null;
1857
+ }
1858
+ writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
1859
+ return id;
1860
+ }
1861
+ /**
1862
+ * Read one adjacency list: the edges of nodes[i]. A list whose owner was skipped is skipped whole.
1863
+ * @param ctx - the context
1864
+ * @param reading - the read's state
1865
+ * @param i - the list's position
1866
+ * @param list - the list
1867
+ */
1868
+ function readAdjacencyList(ctx, reading, i, list) {
1869
+ const { report } = ctx;
1870
+ const element = `adjacency[${i}]`;
1871
+ if (!Array.isArray(list)) {
1872
+ report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not an array`, { element });
1873
+ return;
1874
+ }
1875
+ const source = i < reading.owners.length ? reading.owners[i] : null;
1876
+ if (source === null) {
1877
+ const why = i < reading.nodeCount ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
1878
+ report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`, {
1879
+ element,
1880
+ });
1881
+ report.counts.skippedEdges += list.length;
1882
+ return;
1883
+ }
1884
+ for (let j = 0; j < list.length; j++) {
1885
+ readAdjacencyEntry(ctx, reading, source, list[j], element, `${element}[${j}]`);
1886
+ }
1887
+ }
1888
+ /**
1889
+ * Read one adjacency entry as an edge from the list's owner, unless it is the mirror of an
1890
+ * undirected edge already read.
1891
+ * @param ctx - the context
1892
+ * @param reading - the read's state
1893
+ * @param source - the list's owner
1894
+ * @param record - the entry
1895
+ * @param element - the list's element name (attribute issues name it)
1896
+ * @param entry - the entry's element name
1897
+ */
1898
+ function readAdjacencyEntry(ctx, reading, source, record, element, entry) {
1899
+ const { idKey } = reading;
1900
+ if (!isJsonObject(record)) {
1901
+ ctx.badElement("edge", entry);
1902
+ return;
1903
+ }
1904
+ if (!hasKey(record, idKey)) {
1905
+ ctx.missingEndpoint(entry, idKey);
1906
+ return;
1907
+ }
1908
+ try {
1909
+ const target = ctx.coerceId(record[idKey], `${entry}.${idKey}`);
1910
+ if (target === null) {
1911
+ ctx.countSkipped("edge");
1912
+ return;
1913
+ }
1914
+ if (!reading.directed) {
1915
+ const mirror = mirroredEntry(reading.pending, source, target, record, idKey);
1916
+ reading.disagreeing += mirror === "disagrees" ? 1 : 0;
1917
+ if (mirror !== "new") {
1918
+ return;
1883
1919
  }
1884
- catch (err) {
1885
- ctx.skip(err, "edge", entry);
1920
+ }
1921
+ const edge = ctx.pushEdge(source, target, reading.kind, ctx.weightOf(record), entry);
1922
+ const { weightFrom } = ctx.options;
1923
+ for (const key of Object.keys(record)) {
1924
+ if (key !== idKey && key !== weightFrom) {
1925
+ ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
1886
1926
  }
1887
1927
  }
1888
1928
  }
1889
- reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, disagreeing, pending);
1890
- ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
1929
+ catch (err) {
1930
+ ctx.skip(err, "edge", entry);
1931
+ }
1891
1932
  }
1892
1933
  /**
1893
1934
  * Report what an adjacency_data document leaves inconsistent: nodes without an adjacency list
@@ -2040,26 +2081,8 @@ function importGraphology(ctx, root) {
2040
2081
  element: "options",
2041
2082
  });
2042
2083
  }
2043
- let type;
2044
- if (options.type === "directed" || options.type === "undirected" || options.type === "mixed") {
2045
- ({ type } = options);
2046
- }
2047
- else {
2048
- if (options.type !== undefined && options.type !== null) {
2049
- report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`, { element: "options.type" });
2050
- }
2051
- type = "mixed";
2052
- }
2053
- let directed;
2054
- if (type === "mixed") {
2055
- const objects = edges.filter((edge) => isJsonObject(edge));
2056
- const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
2057
- directed = objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
2058
- }
2059
- else {
2060
- directed = type === "directed";
2061
- }
2062
- ctx.setHeader(directed);
2084
+ const type = graphologyType(ctx, options);
2085
+ ctx.setHeader(graphologyDirected(ctx, type, edges));
2063
2086
  const multi = hasKey(options, "multi") ? flagOf(options.multi, "options.multi", false, report) : null;
2064
2087
  const allowSelfLoops = hasKey(options, "allowSelfLoops")
2065
2088
  ? flagOf(options.allowSelfLoops, "options.allowSelfLoops", true, report)
@@ -2068,76 +2091,147 @@ function importGraphology(ctx, root) {
2068
2091
  ctx.reportNodeIdFrom("graphology", "the node key");
2069
2092
  ctx.sink.reserve(nodes.length, edges.length);
2070
2093
  for (let i = 0; i < nodes.length; i++) {
2071
- const element = `nodes[${i}]`;
2072
- const record = nodes[i];
2073
- if (!isJsonObject(record)) {
2074
- ctx.badElement("node", element);
2075
- continue;
2076
- }
2077
- const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
2078
- if (id === null) {
2079
- ctx.countSkipped("node");
2080
- continue;
2081
- }
2082
- pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
2094
+ pushGraphologyNode(ctx, nodes[i], `nodes[${i}]`);
2083
2095
  }
2084
2096
  throwIfAborted(ctx.options.signal);
2085
- const ids = ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true);
2086
- // what the edges do against the declared options: graphology itself refuses such a document
2087
- const violations = { selfLoops: 0, parallels: 0, flags: 0 };
2088
- const pairs = multi === false ? new Set() : null;
2097
+ const reading = {
2098
+ ids: ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true),
2099
+ type,
2100
+ allowSelfLoops,
2101
+ // what the edges do against the declared options: graphology itself refuses such a document
2102
+ violations: { selfLoops: 0, parallels: 0, flags: 0 },
2103
+ pairs: multi === false ? new Set() : null,
2104
+ };
2089
2105
  for (let i = 0; i < edges.length; i++) {
2090
- const element = `edges[${i}]`;
2091
- const record = edges[i];
2092
- if (!isJsonObject(record)) {
2093
- ctx.badElement("edge", element);
2094
- continue;
2095
- }
2096
- if (!present(record, "source") || !present(record, "target")) {
2097
- ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
2098
- continue;
2106
+ readGraphologyEdge(ctx, reading, edges[i], `edges[${i}]`);
2107
+ }
2108
+ reportGraphologyViolations(ctx, reading.violations, type);
2109
+ ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
2110
+ }
2111
+ /**
2112
+ * A graphology document's `options.type`; anything else is reported and read as "mixed".
2113
+ * @param ctx - the context
2114
+ * @param options - the options object
2115
+ * @returns the type
2116
+ */
2117
+ function graphologyType(ctx, options) {
2118
+ if (options.type === "directed" || options.type === "undirected" || options.type === "mixed") {
2119
+ return options.type;
2120
+ }
2121
+ if (options.type !== undefined && options.type !== null) {
2122
+ ctx.report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`, { element: "options.type" });
2123
+ }
2124
+ return "mixed";
2125
+ }
2126
+ /**
2127
+ * The header direction of a graphology document: its type, or for "mixed" directed unless every
2128
+ * edge object says undirected (the dialect default when there is none).
2129
+ * @param ctx - the context
2130
+ * @param type - options.type
2131
+ * @param edges - the edge records
2132
+ * @returns whether the header is directed
2133
+ */
2134
+ function graphologyDirected(ctx, type, edges) {
2135
+ if (type !== "mixed") {
2136
+ return type === "directed";
2137
+ }
2138
+ const objects = edges.filter((edge) => isJsonObject(edge));
2139
+ const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
2140
+ return objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
2141
+ }
2142
+ /**
2143
+ * Push one graphology node record: `key` the id, `attributes` the columns.
2144
+ * @param ctx - the context
2145
+ * @param record - the node record
2146
+ * @param element - the element name
2147
+ */
2148
+ function pushGraphologyNode(ctx, record, element) {
2149
+ if (!isJsonObject(record)) {
2150
+ ctx.badElement("node", element);
2151
+ return;
2152
+ }
2153
+ const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
2154
+ if (id === null) {
2155
+ ctx.countSkipped("node");
2156
+ return;
2157
+ }
2158
+ pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
2159
+ }
2160
+ /**
2161
+ * Read one graphology edge record, counting what it does against the declared options.
2162
+ * @param ctx - the context
2163
+ * @param reading - the read's state
2164
+ * @param record - the edge record
2165
+ * @param element - the element name
2166
+ */
2167
+ function readGraphologyEdge(ctx, reading, record, element) {
2168
+ if (!isJsonObject(record)) {
2169
+ ctx.badElement("edge", element);
2170
+ return;
2171
+ }
2172
+ if (!present(record, "source") || !present(record, "target")) {
2173
+ ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
2174
+ return;
2175
+ }
2176
+ try {
2177
+ const u = ctx.coerceId(record.source, `${element}.source`);
2178
+ const v = ctx.coerceId(record.target, `${element}.target`);
2179
+ if (u === null || v === null) {
2180
+ ctx.countSkipped("edge");
2181
+ return;
2099
2182
  }
2100
- try {
2101
- const u = ctx.coerceId(record.source, `${element}.source`);
2102
- const v = ctx.coerceId(record.target, `${element}.target`);
2103
- if (u === null || v === null) {
2104
- ctx.countSkipped("edge");
2105
- continue;
2106
- }
2107
- let kind;
2108
- if (type === "mixed") {
2109
- kind = flagOf(record.undirected, `${element}.undirected`, false, report) ? "undirected" : "directed";
2110
- }
2111
- else {
2112
- kind = type;
2113
- if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
2114
- violations.flags++;
2115
- }
2116
- }
2117
- const { attributes: rawAttributes } = record;
2118
- if (rawAttributes !== undefined && rawAttributes !== null && !isJsonObject(rawAttributes)) {
2119
- report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: attributes must be an object, found ${describe(rawAttributes)}`, { element });
2120
- }
2121
- const attributes = isJsonObject(rawAttributes) ? rawAttributes : {};
2122
- const idValue = ctx.edgeIdValue(ids, record.key);
2123
- const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
2124
- ctx.setEdgeId(ids, edge, idValue);
2125
- ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
2126
- if (allowSelfLoops === false && u === v) {
2127
- violations.selfLoops++;
2128
- }
2129
- if (pairs !== null) {
2130
- const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
2131
- violations.parallels += pairs.has(pair) ? 1 : 0;
2132
- pairs.add(pair);
2133
- }
2183
+ const kind = graphologyEdgeKind(ctx, reading, record, element);
2184
+ const attributes = graphologyEdgeAttributes(ctx, record, element);
2185
+ const idValue = ctx.edgeIdValue(reading.ids, record.key);
2186
+ const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
2187
+ ctx.setEdgeId(reading.ids, edge, idValue);
2188
+ ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
2189
+ if (reading.allowSelfLoops === false && u === v) {
2190
+ reading.violations.selfLoops++;
2134
2191
  }
2135
- catch (err) {
2136
- ctx.skip(err, "edge", element);
2192
+ if (reading.pairs !== null) {
2193
+ const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
2194
+ reading.violations.parallels += reading.pairs.has(pair) ? 1 : 0;
2195
+ reading.pairs.add(pair);
2137
2196
  }
2138
2197
  }
2139
- reportGraphologyViolations(ctx, violations, type);
2140
- ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
2198
+ catch (err) {
2199
+ ctx.skip(err, "edge", element);
2200
+ }
2201
+ }
2202
+ /**
2203
+ * The direction of one graphology edge: its `undirected` flag under type "mixed", else the type
2204
+ * (a contrary flag is counted as a violation).
2205
+ * @param ctx - the context
2206
+ * @param reading - the read's state
2207
+ * @param record - the edge record
2208
+ * @param element - the element name
2209
+ * @returns the edge's kind
2210
+ */
2211
+ function graphologyEdgeKind(ctx, reading, record, element) {
2212
+ const { type } = reading;
2213
+ if (type === "mixed") {
2214
+ return flagOf(record.undirected, `${element}.undirected`, false, ctx.report) ? "undirected" : "directed";
2215
+ }
2216
+ if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
2217
+ reading.violations.flags++;
2218
+ }
2219
+ return type;
2220
+ }
2221
+ /**
2222
+ * The `attributes` object of a graphology edge; any other non-null value is reported and read as
2223
+ * no attributes.
2224
+ * @param ctx - the context
2225
+ * @param record - the edge record
2226
+ * @param element - the element name
2227
+ * @returns the attributes
2228
+ */
2229
+ function graphologyEdgeAttributes(ctx, record, element) {
2230
+ const { attributes } = record;
2231
+ if (attributes !== undefined && attributes !== null && !isJsonObject(attributes)) {
2232
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: attributes must be an object, found ${describe(attributes)}`, { element });
2233
+ }
2234
+ return isJsonObject(attributes) ? attributes : {};
2141
2235
  }
2142
2236
  /**
2143
2237
  * Report the edges that contradict a graphology document's declared options (W_JSON_INCONSISTENT,
@@ -2212,21 +2306,7 @@ function importJgf(ctx, root) {
2212
2306
  report.fail(JSON_ISSUE.SHAPE, `graph.nodes must be an object keyed by id or an array, found ${describe(nodesRaw)}`);
2213
2307
  }
2214
2308
  const nodeRecords = Array.isArray(nodesRaw) ? nodesRaw : Object.values(nodesRaw ?? {});
2215
- let directed;
2216
- if (typeof graph.directed === "boolean") {
2217
- ({ directed } = graph);
2218
- }
2219
- else if (ctx.explicitDefaultDirected) {
2220
- directed = ctx.options.defaultDirected;
2221
- }
2222
- else {
2223
- if (graph.directed !== undefined && graph.directed !== null) {
2224
- report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `graph.directed is ${describe(graph.directed)}, not a boolean`, { element: "graph.directed" });
2225
- }
2226
- // the spec default is true; a file whose every edge says directed: false is read as undirected
2227
- const objects = edges.filter((edge) => isJsonObject(edge));
2228
- directed = objects.length === 0 || !objects.every((edge) => edge.directed === false);
2229
- }
2309
+ const directed = jgfDirected(ctx, graph, edges);
2230
2310
  ctx.setHeader(directed);
2231
2311
  ctx.writeGraphDict(graph.metadata, "graph.metadata");
2232
2312
  const shape = {
@@ -2240,44 +2320,99 @@ function importJgf(ctx, root) {
2240
2320
  const labelColumn = ctx.nodes.declareIf({ name: "label", dtype: "string", role: "label", nullable: true }, anyHas(nodeRecords, "label"));
2241
2321
  if (isJsonObject(nodesRaw)) {
2242
2322
  for (const key of Object.keys(nodesRaw)) {
2243
- const element = `nodes[${JSON.stringify(key)}]`;
2244
- const record = nodesRaw[key];
2245
- if (record !== null && !isJsonObject(record)) {
2246
- ctx.badElement("node", element);
2247
- continue;
2248
- }
2249
- const id = ctx.coerceId(key, element);
2250
- if (id === null) {
2251
- ctx.countSkipped("node");
2252
- continue;
2253
- }
2254
- if (record !== null && hasKey(record, "id") && record.id !== key) {
2255
- report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element}: the inner id ${describe(record.id)} differs from the key; the key is the id, the inner one is kept as id${SUFFIX.element}`, { element });
2256
- }
2257
- pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
2323
+ pushJgfKeyedNode(ctx, key, nodesRaw[key], labelColumn);
2258
2324
  }
2259
2325
  }
2260
2326
  else {
2261
2327
  for (let i = 0; i < nodeRecords.length; i++) {
2262
- const element = `nodes[${i}]`;
2263
- const record = nodeRecords[i];
2264
- if (!isJsonObject(record)) {
2265
- ctx.badElement("node", element);
2266
- continue;
2267
- }
2268
- const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
2269
- if (id === null) {
2270
- ctx.countSkipped("node");
2271
- continue;
2272
- }
2273
- pushJgfNode(ctx, id, record, labelColumn, element);
2328
+ pushJgfListedNode(ctx, nodeRecords[i], labelColumn, `nodes[${i}]`);
2274
2329
  }
2275
2330
  }
2276
2331
  throwIfAborted(ctx.options.signal);
2332
+ const writeJgfEdge = jgfEdgeWriter(ctx, edges, hyperedges);
2333
+ for (let i = 0; i < edges.length; i++) {
2334
+ readJgfEdge(ctx, edges[i], `edges[${i}]`, directed, writeJgfEdge);
2335
+ }
2336
+ importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
2337
+ ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
2338
+ }
2339
+ /**
2340
+ * The header direction of a JGF graph: `directed`, else the defaultDirected option when given,
2341
+ * else the spec default true unless every edge says directed: false.
2342
+ * @param ctx - the context
2343
+ * @param graph - the graph object
2344
+ * @param edges - the edge records
2345
+ * @returns whether the header is directed
2346
+ */
2347
+ function jgfDirected(ctx, graph, edges) {
2348
+ if (typeof graph.directed === "boolean") {
2349
+ return graph.directed;
2350
+ }
2351
+ if (ctx.explicitDefaultDirected) {
2352
+ return ctx.options.defaultDirected;
2353
+ }
2354
+ if (graph.directed !== undefined && graph.directed !== null) {
2355
+ ctx.report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `graph.directed is ${describe(graph.directed)}, not a boolean`, { element: "graph.directed" });
2356
+ }
2357
+ // the spec default is true; a file whose every edge says directed: false is read as undirected
2358
+ const objects = edges.filter((edge) => isJsonObject(edge));
2359
+ return objects.length === 0 || !objects.every((edge) => edge.directed === false);
2360
+ }
2361
+ /**
2362
+ * Push one node of a JGF v2 `nodes` object, keyed by its id (a null record is a node with nothing).
2363
+ * @param ctx - the context
2364
+ * @param key - the node's key
2365
+ * @param record - the node record
2366
+ * @param labelColumn - the label column
2367
+ */
2368
+ function pushJgfKeyedNode(ctx, key, record, labelColumn) {
2369
+ const element = `nodes[${JSON.stringify(key)}]`;
2370
+ if (record !== null && !isJsonObject(record)) {
2371
+ ctx.badElement("node", element);
2372
+ return;
2373
+ }
2374
+ const id = ctx.coerceId(key, element);
2375
+ if (id === null) {
2376
+ ctx.countSkipped("node");
2377
+ return;
2378
+ }
2379
+ if (record !== null && hasKey(record, "id") && record.id !== key) {
2380
+ ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element}: the inner id ${describe(record.id)} differs from the key; the key is the id, the inner one is kept as id${SUFFIX.element}`, { element });
2381
+ }
2382
+ pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
2383
+ }
2384
+ /**
2385
+ * Push one node of a JGF v1 `nodes` array, with its `id`.
2386
+ * @param ctx - the context
2387
+ * @param record - the node record
2388
+ * @param labelColumn - the label column
2389
+ * @param element - the element name
2390
+ */
2391
+ function pushJgfListedNode(ctx, record, labelColumn, element) {
2392
+ if (!isJsonObject(record)) {
2393
+ ctx.badElement("node", element);
2394
+ return;
2395
+ }
2396
+ const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
2397
+ if (id === null) {
2398
+ ctx.countSkipped("node");
2399
+ return;
2400
+ }
2401
+ pushJgfNode(ctx, id, record, labelColumn, element);
2402
+ }
2403
+ /**
2404
+ * The writer of JGF edges: declares the edge id, relation and label columns the edges and
2405
+ * hyperedges need, and returns the function that pushes one edge into them.
2406
+ * @param ctx - the context
2407
+ * @param edges - the edge records
2408
+ * @param hyperedges - the hyperedge records
2409
+ * @returns the edge writer
2410
+ */
2411
+ function jgfEdgeWriter(ctx, edges, hyperedges) {
2277
2412
  const ids = ctx.declareEdgeIds([...edges, ...hyperedges], (edge) => edge.id, "id", false);
2278
2413
  const relationColumn = ctx.edges.declareIf({ name: "relation", dtype: "string", role: "kind", nullable: true }, anyHas(edges, "relation") || anyHas(hyperedges, "relation"));
2279
2414
  const edgeLabelColumn = ctx.edges.declareIf({ name: "label", dtype: "string", role: "label", nullable: true }, anyHas(edges, "label") || anyHas(hyperedges, "label"));
2280
- const writeJgfEdge = (record, u, v, kind, element, structural = JGF_EDGE_KEYS) => {
2415
+ return (record, u, v, kind, element, structural = JGF_EDGE_KEYS) => {
2281
2416
  const { metadata } = record;
2282
2417
  const dict = isJsonObject(metadata) ? metadata : {};
2283
2418
  const idValue = ctx.edgeIdValue(ids, record.id);
@@ -2286,39 +2421,43 @@ function importJgf(ctx, root) {
2286
2421
  ctx.writeStringField(ctx.edges, relationColumn, edge, record.relation, "relation", element);
2287
2422
  ctx.writeStringField(ctx.edges, edgeLabelColumn, edge, record.label, "label", element);
2288
2423
  if (metadata !== undefined && metadata !== null && !isJsonObject(metadata)) {
2289
- report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
2424
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
2290
2425
  element,
2291
2426
  });
2292
2427
  }
2293
2428
  ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom, element);
2294
2429
  };
2295
- for (let i = 0; i < edges.length; i++) {
2296
- const element = `edges[${i}]`;
2297
- const record = edges[i];
2298
- if (!isJsonObject(record)) {
2299
- ctx.badElement("edge", element);
2300
- continue;
2301
- }
2302
- if (!present(record, "source") || !present(record, "target")) {
2303
- ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
2304
- continue;
2305
- }
2306
- try {
2307
- const u = ctx.coerceId(record.source, `${element}.source`);
2308
- const v = ctx.coerceId(record.target, `${element}.target`);
2309
- if (u === null || v === null) {
2310
- ctx.countSkipped("edge");
2311
- continue;
2312
- }
2313
- const edgeDirected = flagOf(record.directed, `${element}.directed`, directed, report);
2314
- writeJgfEdge(record, u, v, edgeDirected ? "directed" : "undirected", element);
2315
- }
2316
- catch (err) {
2317
- ctx.skip(err, "edge", element);
2430
+ }
2431
+ /**
2432
+ * Read one JGF edge record: its ends and its own `directed` flag (the header's by default).
2433
+ * @param ctx - the context
2434
+ * @param record - the edge record
2435
+ * @param element - the element name
2436
+ * @param directed - the header direction
2437
+ * @param writeJgfEdge - the edge writer
2438
+ */
2439
+ function readJgfEdge(ctx, record, element, directed, writeJgfEdge) {
2440
+ if (!isJsonObject(record)) {
2441
+ ctx.badElement("edge", element);
2442
+ return;
2443
+ }
2444
+ if (!present(record, "source") || !present(record, "target")) {
2445
+ ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
2446
+ return;
2447
+ }
2448
+ try {
2449
+ const u = ctx.coerceId(record.source, `${element}.source`);
2450
+ const v = ctx.coerceId(record.target, `${element}.target`);
2451
+ if (u === null || v === null) {
2452
+ ctx.countSkipped("edge");
2453
+ return;
2318
2454
  }
2455
+ const edgeDirected = flagOf(record.directed, `${element}.directed`, directed, ctx.report);
2456
+ writeJgfEdge(record, u, v, edgeDirected ? "directed" : "undirected", element);
2457
+ }
2458
+ catch (err) {
2459
+ ctx.skip(err, "edge", element);
2319
2460
  }
2320
- importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
2321
- ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
2322
2461
  }
2323
2462
  /**
2324
2463
  * The graph object of a JGF document: `graph`, or the graph of `graphs` that graphIndex /
@@ -2626,111 +2765,150 @@ function distinctMembers(ctx, members, element) {
2626
2765
  * @param root - the document
2627
2766
  */
2628
2767
  function importCytoscape(ctx, root) {
2629
- const { report } = ctx;
2630
- let elements;
2631
- let extra;
2632
- if (Array.isArray(root)) {
2633
- elements = root;
2634
- }
2635
- else if (isJsonObject(root)) {
2636
- ({ elements } = root);
2637
- ctx.writeGraphDict(root.data, "data");
2638
- const rest = {};
2639
- for (const key of Object.keys(root)) {
2640
- if (key !== "elements" && key !== "data") {
2641
- rest[key] = root[key];
2642
- }
2643
- }
2644
- if (Object.keys(rest).length > 0) {
2645
- extra = rest;
2646
- }
2647
- }
2648
- else {
2649
- report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
2650
- }
2768
+ const { elements, extra } = cytoscapeRoot(ctx, root);
2651
2769
  const { nodes, edges } = cytoscapeSections(ctx, elements);
2652
2770
  ctx.setHeader(ctx.defaultDirected("cytoscape"));
2653
2771
  ctx.reportNodeIdFrom("cytoscape", "data.id");
2654
2772
  ctx.sink.reserve(nodes.length, edges.length);
2655
2773
  const dataHas = (items, key) => items.some((item) => isJsonObject(item) && isJsonObject(item.data) && item.data[key] !== undefined);
2656
- const positionColumn = ctx.nodes.declareIf({
2657
- name: POSITION_COLUMN,
2658
- dtype: "f64",
2659
- components: 3,
2660
- role: "position",
2661
- mutable: true,
2662
- nullable: true,
2663
- // 3D layouts written by Cytoscape-compatible tools carry a z
2664
- extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
2665
- origin: { format: "json", namespace: "cytoscape" },
2666
- }, anyHas(nodes, "position"));
2667
- const classesColumn = ctx.nodes.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(nodes, "classes"));
2774
+ const reading = {
2775
+ positionColumn: ctx.nodes.declareIf({
2776
+ name: POSITION_COLUMN,
2777
+ dtype: "f64",
2778
+ components: 3,
2779
+ role: "position",
2780
+ mutable: true,
2781
+ nullable: true,
2782
+ // 3D layouts written by Cytoscape-compatible tools carry a z
2783
+ extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
2784
+ origin: { format: "json", namespace: "cytoscape" },
2785
+ }, anyHas(nodes, "position")),
2786
+ classesColumn: ctx.nodes.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(nodes, "classes")),
2787
+ parents: [],
2788
+ point: [0, 0, 0],
2789
+ };
2668
2790
  const parentColumn = ctx.nodes.declareIf({ name: PARENT_COLUMN, dtype: "u32", role: "parent", refersTo: "node", nullable: true }, dataHas(nodes, "parent"));
2669
2791
  const edgeClassesColumn = ctx.edges.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(edges, "classes"));
2670
- const parents = [];
2671
- const point = [0, 0, 0];
2672
2792
  for (let i = 0; i < nodes.length; i++) {
2673
- const element = `nodes[${i}]`;
2674
- const record = nodes[i];
2675
- if (!isJsonObject(record) || !isJsonObject(record.data)) {
2676
- ctx.badElement("node", element, "an element with a data object");
2677
- continue;
2793
+ readCytoscapeNode(ctx, reading, nodes[i], `nodes[${i}]`);
2794
+ }
2795
+ resolveCytoscapeParents(ctx, reading.parents, parentColumn);
2796
+ throwIfAborted(ctx.options.signal);
2797
+ importCytoscapeEdges(ctx, edges, edgeClassesColumn);
2798
+ ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
2799
+ }
2800
+ /**
2801
+ * The elements of a Cytoscape document (the document itself when it is an array) and, for an
2802
+ * object, its graph `data` written and its other top-level keys kept.
2803
+ * @param ctx - the context
2804
+ * @param root - the document
2805
+ * @returns the elements and the other top-level keys, if any
2806
+ */
2807
+ function cytoscapeRoot(ctx, root) {
2808
+ if (Array.isArray(root)) {
2809
+ return { elements: root };
2810
+ }
2811
+ if (!isJsonObject(root)) {
2812
+ ctx.report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
2813
+ }
2814
+ ctx.writeGraphDict(root.data, "data");
2815
+ const rest = {};
2816
+ for (const key of Object.keys(root)) {
2817
+ if (key !== "elements" && key !== "data") {
2818
+ rest[key] = root[key];
2678
2819
  }
2679
- const { data } = record;
2680
- const id = ctx.coerceId(hasKey(data, "id") ? data.id : undefined, element);
2681
- if (id === null) {
2682
- ctx.countSkipped("node");
2820
+ }
2821
+ return Object.keys(rest).length > 0 ? { elements: root.elements, extra: rest } : { elements: root.elements };
2822
+ }
2823
+ /**
2824
+ * Read one Cytoscape node element: `data.id` the id, the other `data` keys attributes (`parent`
2825
+ * kept for later), `position`, `classes` and the element-level keys.
2826
+ * @param ctx - the context
2827
+ * @param reading - the read's state
2828
+ * @param record - the element
2829
+ * @param element - the element name
2830
+ */
2831
+ function readCytoscapeNode(ctx, reading, record, element) {
2832
+ const { report } = ctx;
2833
+ if (!isJsonObject(record) || !isJsonObject(record.data)) {
2834
+ ctx.badElement("node", element, "an element with a data object");
2835
+ return;
2836
+ }
2837
+ const { data } = record;
2838
+ const id = ctx.coerceId(hasKey(data, "id") ? data.id : undefined, element);
2839
+ if (id === null) {
2840
+ ctx.countSkipped("node");
2841
+ return;
2842
+ }
2843
+ const index = ctx.pushNode(id, element);
2844
+ if (index < 0) {
2845
+ return;
2846
+ }
2847
+ if (present(data, "source") && present(data, "target")) {
2848
+ report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`, { element });
2849
+ }
2850
+ try {
2851
+ writeCytoscapeData(ctx, reading, index, data, element);
2852
+ writeCytoscapePosition(ctx, reading, index, record.position, element);
2853
+ writeClasses(ctx, ctx.nodes, reading.classesColumn, index, record.classes, element);
2854
+ writeElementKeys(ctx.nodes, index, record, element);
2855
+ }
2856
+ catch (err) {
2857
+ report.recordError(err, { element: String(id) });
2858
+ }
2859
+ }
2860
+ /**
2861
+ * Write a Cytoscape node's `data` keys as attributes; `parent` is kept as a link to resolve later.
2862
+ * @param ctx - the context
2863
+ * @param reading - the read's state
2864
+ * @param index - the node index
2865
+ * @param data - the element's data object
2866
+ * @param element - the element name
2867
+ */
2868
+ function writeCytoscapeData(ctx, reading, index, data, element) {
2869
+ for (const key of Object.keys(data)) {
2870
+ if (key === "id") {
2683
2871
  continue;
2684
2872
  }
2685
- const index = ctx.pushNode(id, element);
2686
- if (index < 0) {
2873
+ if (key !== "parent") {
2874
+ ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
2687
2875
  continue;
2688
2876
  }
2689
- if (present(data, "source") && present(data, "target")) {
2690
- report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`, { element });
2691
- }
2692
- try {
2693
- for (const key of Object.keys(data)) {
2694
- if (key === "id") {
2695
- continue;
2696
- }
2697
- if (key === "parent") {
2698
- const raw = data.parent;
2699
- if (raw !== undefined && raw !== null) {
2700
- const parent = ctx.coerceId(raw, `${element}.data.parent`);
2701
- if (parent !== null) {
2702
- parents.push({ index, parent, element });
2703
- }
2704
- }
2705
- continue;
2706
- }
2707
- ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
2708
- }
2709
- const { position } = record;
2710
- if (position !== undefined && position !== null) {
2711
- const z = isJsonObject(position) ? (position.z ?? 0) : 0;
2712
- if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
2713
- point[0] = position.x;
2714
- // Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
2715
- point[1] = flipY(position.y);
2716
- point[2] = z;
2717
- ctx.nodes.set(positionColumn, index, point);
2718
- }
2719
- else {
2720
- report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y (and z) that are finite numbers`, { element });
2721
- }
2722
- }
2723
- writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
2724
- writeElementKeys(ctx.nodes, index, record, element);
2877
+ const raw = data.parent;
2878
+ if (raw === undefined || raw === null) {
2879
+ continue;
2725
2880
  }
2726
- catch (err) {
2727
- report.recordError(err, { element: String(id) });
2881
+ const parent = ctx.coerceId(raw, `${element}.data.parent`);
2882
+ if (parent !== null) {
2883
+ reading.parents.push({ index, parent, element });
2728
2884
  }
2729
2885
  }
2730
- resolveCytoscapeParents(ctx, parents, parentColumn);
2731
- throwIfAborted(ctx.options.signal);
2732
- importCytoscapeEdges(ctx, edges, edgeClassesColumn);
2733
- ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
2886
+ }
2887
+ /**
2888
+ * Write a Cytoscape node's `position` ({ x, y, z? }, y flipped to grow upward); any other value is
2889
+ * reported.
2890
+ * @param ctx - the context
2891
+ * @param reading - the read's state
2892
+ * @param index - the node index
2893
+ * @param position - the element's position
2894
+ * @param element - the element name
2895
+ */
2896
+ function writeCytoscapePosition(ctx, reading, index, position, element) {
2897
+ if (position === undefined || position === null) {
2898
+ return;
2899
+ }
2900
+ const z = isJsonObject(position) ? (position.z ?? 0) : 0;
2901
+ if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
2902
+ const { point } = reading;
2903
+ point[0] = position.x;
2904
+ // Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
2905
+ point[1] = flipY(position.y);
2906
+ point[2] = z;
2907
+ ctx.nodes.set(reading.positionColumn, index, point);
2908
+ }
2909
+ else {
2910
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y (and z) that are finite numbers`, { element });
2911
+ }
2734
2912
  }
2735
2913
  /**
2736
2914
  * Set the parent of each Cytoscape node once every node is known (a parent may come later): an
@@ -2819,28 +2997,43 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
2819
2997
  ctx.missingEndpoint(element, `data.${present(data, "source") ? "target" : "source"}`);
2820
2998
  continue;
2821
2999
  }
2822
- try {
2823
- const u = ctx.coerceId(data.source, `${element}.data.source`);
2824
- const v = ctx.coerceId(data.target, `${element}.data.target`);
2825
- if (u === null || v === null) {
2826
- ctx.countSkipped("edge");
2827
- continue;
2828
- }
2829
- const idValue = ctx.edgeIdValue(ids, data.id);
2830
- const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(data), element);
2831
- ctx.setEdgeId(ids, edge, idValue);
2832
- const { weightFrom } = ctx.options;
2833
- for (const key of Object.keys(data)) {
2834
- if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
2835
- ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
2836
- }
2837
- }
2838
- writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
2839
- writeElementKeys(ctx.edges, edge, record, element);
3000
+ importCytoscapeEdge(ctx, { ids, kind, edgeClassesColumn }, record, data, element);
3001
+ }
3002
+ }
3003
+ /**
3004
+ * Read one Cytoscape edge element whose data object holds both endpoints.
3005
+ * @param ctx - the context
3006
+ * @param shared - the edge ids, the uniform kind and the classes column of the whole section
3007
+ * @param shared.ids - the declared edge ids
3008
+ * @param shared.kind - the uniform edge kind
3009
+ * @param shared.edgeClassesColumn - the edge classes column, or INVALID_INDEX
3010
+ * @param record - the edge element
3011
+ * @param data - its data object
3012
+ * @param element - the element's name in messages
3013
+ */
3014
+ function importCytoscapeEdge(ctx, shared, record, data, element) {
3015
+ const { ids, kind, edgeClassesColumn } = shared;
3016
+ try {
3017
+ const u = ctx.coerceId(data.source, `${element}.data.source`);
3018
+ const v = ctx.coerceId(data.target, `${element}.data.target`);
3019
+ if (u === null || v === null) {
3020
+ ctx.countSkipped("edge");
3021
+ return;
2840
3022
  }
2841
- catch (err) {
2842
- ctx.skip(err, "edge", element);
3023
+ const idValue = ctx.edgeIdValue(ids, data.id);
3024
+ const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(data), element);
3025
+ ctx.setEdgeId(ids, edge, idValue);
3026
+ const { weightFrom } = ctx.options;
3027
+ for (const key of Object.keys(data)) {
3028
+ if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
3029
+ ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
3030
+ }
2843
3031
  }
3032
+ writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
3033
+ writeElementKeys(ctx.edges, edge, record, element);
3034
+ }
3035
+ catch (err) {
3036
+ ctx.skip(err, "edge", element);
2844
3037
  }
2845
3038
  }
2846
3039
  /**