@graphty/graph-io 0.3.17 → 0.3.19

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 (154) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -3
  3. package/dist/chunks/{escape-scjHxjpr.js → escape-CWExcecC.js} +3 -2
  4. package/dist/chunks/{escape-scjHxjpr.js.map → escape-CWExcecC.js.map} +1 -1
  5. package/dist/chunks/exporter-CbMZyVt-.js +735 -0
  6. package/dist/chunks/exporter-CbMZyVt-.js.map +1 -0
  7. package/dist/chunks/importer-BAP4PrxR.js +2194 -0
  8. package/dist/chunks/importer-BAP4PrxR.js.map +1 -0
  9. package/dist/chunks/{importer-Xg07gk7p.js → importer-BNFuV1-K.js} +5 -3
  10. package/dist/chunks/{importer-Xg07gk7p.js.map → importer-BNFuV1-K.js.map} +1 -1
  11. package/dist/chunks/{importer-BmOl9gLW.js → importer-BVGtU1NA.js} +5 -3
  12. package/dist/chunks/{importer-BmOl9gLW.js.map → importer-BVGtU1NA.js.map} +1 -1
  13. package/dist/chunks/{importer-ruMWWvVs.js → importer-BW-Ft2ps.js} +5 -3
  14. package/dist/chunks/{importer-ruMWWvVs.js.map → importer-BW-Ft2ps.js.map} +1 -1
  15. package/dist/chunks/{importer-B6rKRxzv.js → importer-Bm1_5vPS.js} +781 -104
  16. package/dist/chunks/importer-Bm1_5vPS.js.map +1 -0
  17. package/dist/chunks/importer-ByPGO-09.js +1614 -0
  18. package/dist/chunks/importer-ByPGO-09.js.map +1 -0
  19. package/dist/chunks/importer-DOepkgnG.js +1713 -0
  20. package/dist/chunks/importer-DOepkgnG.js.map +1 -0
  21. package/dist/chunks/json-elements-CZY1wiZh.js +779 -0
  22. package/dist/chunks/json-elements-CZY1wiZh.js.map +1 -0
  23. package/dist/chunks/ontology-BnrJ4I98.js +113 -0
  24. package/dist/chunks/ontology-BnrJ4I98.js.map +1 -0
  25. package/dist/chunks/{records-DSpbTE5s.js → records-BzNicMsf.js} +2 -2
  26. package/dist/chunks/{records-DSpbTE5s.js.map → records-BzNicMsf.js.map} +1 -1
  27. package/dist/chunks/{writer-DHHfHn11.js → report-BcWboivV.js} +106 -928
  28. package/dist/chunks/report-BcWboivV.js.map +1 -0
  29. package/dist/chunks/weights-CwISIpCP.js +176 -0
  30. package/dist/chunks/weights-CwISIpCP.js.map +1 -0
  31. package/dist/chunks/writer-DQiKgQJc.js +656 -0
  32. package/dist/chunks/writer-DQiKgQJc.js.map +1 -0
  33. package/dist/csv.js +5 -3
  34. package/dist/csv.js.map +1 -1
  35. package/dist/cx.d.ts +1 -0
  36. package/dist/cx.js +6 -0
  37. package/dist/cx.js.map +1 -0
  38. package/dist/cx2.d.ts +1 -0
  39. package/dist/cx2.js +10 -0
  40. package/dist/cx2.js.map +1 -0
  41. package/dist/dot.js +1 -1
  42. package/dist/gexf.js +15 -3
  43. package/dist/gexf.js.map +1 -1
  44. package/dist/gml.js +5 -3
  45. package/dist/gml.js.map +1 -1
  46. package/dist/graph-io.js +158 -383
  47. package/dist/graph-io.js.map +1 -1
  48. package/dist/graphml.js +1 -1
  49. package/dist/json.js +1 -1
  50. package/dist/neo4j.js +5 -3
  51. package/dist/neo4j.js.map +1 -1
  52. package/dist/obo.d.ts +1 -0
  53. package/dist/obo.js +6 -0
  54. package/dist/obo.js.map +1 -0
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/json-elements.d.ts +286 -0
  57. package/dist/src/common/json-elements.d.ts.map +1 -0
  58. package/dist/src/common/json-elements.js +926 -0
  59. package/dist/src/common/json-elements.js.map +1 -0
  60. package/dist/src/common/ontology.d.ts +59 -0
  61. package/dist/src/common/ontology.d.ts.map +1 -0
  62. package/dist/src/common/ontology.js +147 -0
  63. package/dist/src/common/ontology.js.map +1 -0
  64. package/dist/src/formats/cx/importer.d.ts +133 -0
  65. package/dist/src/formats/cx/importer.d.ts.map +1 -0
  66. package/dist/src/formats/cx/importer.js +2220 -0
  67. package/dist/src/formats/cx/importer.js.map +1 -0
  68. package/dist/src/formats/cx/index.d.ts +7 -0
  69. package/dist/src/formats/cx/index.d.ts.map +1 -0
  70. package/dist/src/formats/cx/index.js +7 -0
  71. package/dist/src/formats/cx/index.js.map +1 -0
  72. package/dist/src/formats/cx2/exporter.d.ts +59 -0
  73. package/dist/src/formats/cx2/exporter.d.ts.map +1 -0
  74. package/dist/src/formats/cx2/exporter.js +864 -0
  75. package/dist/src/formats/cx2/exporter.js.map +1 -0
  76. package/dist/src/formats/cx2/importer.d.ts +169 -0
  77. package/dist/src/formats/cx2/importer.d.ts.map +1 -0
  78. package/dist/src/formats/cx2/importer.js +1652 -0
  79. package/dist/src/formats/cx2/importer.js.map +1 -0
  80. package/dist/src/formats/cx2/index.d.ts +7 -0
  81. package/dist/src/formats/cx2/index.d.ts.map +1 -0
  82. package/dist/src/formats/cx2/index.js +7 -0
  83. package/dist/src/formats/cx2/index.js.map +1 -0
  84. package/dist/src/formats/gexf/exporter.d.ts +2 -0
  85. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  86. package/dist/src/formats/gexf/exporter.js +6 -0
  87. package/dist/src/formats/gexf/exporter.js.map +1 -1
  88. package/dist/src/formats/gexf/importer.d.ts +2 -4
  89. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  90. package/dist/src/formats/gexf/importer.js +2 -4
  91. package/dist/src/formats/gexf/importer.js.map +1 -1
  92. package/dist/src/formats/gexf/index.d.ts +1 -1
  93. package/dist/src/formats/gexf/index.js +2 -2
  94. package/dist/src/formats/gexf/index.js.map +1 -1
  95. package/dist/src/formats/gml/exporter.js +1 -1
  96. package/dist/src/formats/gml/exporter.js.map +1 -1
  97. package/dist/src/formats/json/dialect.d.ts +8 -6
  98. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  99. package/dist/src/formats/json/dialect.js +26 -3
  100. package/dist/src/formats/json/dialect.js.map +1 -1
  101. package/dist/src/formats/json/importer.d.ts +324 -7
  102. package/dist/src/formats/json/importer.d.ts.map +1 -1
  103. package/dist/src/formats/json/importer.js +159 -126
  104. package/dist/src/formats/json/importer.js.map +1 -1
  105. package/dist/src/formats/json/obographs.d.ts +21 -0
  106. package/dist/src/formats/json/obographs.d.ts.map +1 -0
  107. package/dist/src/formats/json/obographs.js +476 -0
  108. package/dist/src/formats/json/obographs.js.map +1 -0
  109. package/dist/src/formats/obo/importer.d.ts +90 -0
  110. package/dist/src/formats/obo/importer.d.ts.map +1 -0
  111. package/dist/src/formats/obo/importer.js +1248 -0
  112. package/dist/src/formats/obo/importer.js.map +1 -0
  113. package/dist/src/formats/obo/index.d.ts +7 -0
  114. package/dist/src/formats/obo/index.d.ts.map +1 -0
  115. package/dist/src/formats/obo/index.js +7 -0
  116. package/dist/src/formats/obo/index.js.map +1 -0
  117. package/dist/src/formats/obo/syntax.d.ts +121 -0
  118. package/dist/src/formats/obo/syntax.d.ts.map +1 -0
  119. package/dist/src/formats/obo/syntax.js +424 -0
  120. package/dist/src/formats/obo/syntax.js.map +1 -0
  121. package/dist/src/index.d.ts +3 -0
  122. package/dist/src/index.d.ts.map +1 -1
  123. package/dist/src/index.js +3 -0
  124. package/dist/src/index.js.map +1 -1
  125. package/dist/src/registry.d.ts.map +1 -1
  126. package/dist/src/registry.js +8 -1
  127. package/dist/src/registry.js.map +1 -1
  128. package/dist/src/sniff.d.ts +1 -1
  129. package/dist/src/sniff.d.ts.map +1 -1
  130. package/dist/src/sniff.js +25 -3
  131. package/dist/src/sniff.js.map +1 -1
  132. package/package.json +16 -1
  133. package/src/common/json-elements.ts +1147 -0
  134. package/src/common/ontology.ts +169 -0
  135. package/src/formats/cx/importer.ts +2733 -0
  136. package/src/formats/cx/index.ts +7 -0
  137. package/src/formats/cx2/exporter.ts +1036 -0
  138. package/src/formats/cx2/importer.ts +2187 -0
  139. package/src/formats/cx2/index.ts +7 -0
  140. package/src/formats/gexf/exporter.ts +11 -0
  141. package/src/formats/gexf/importer.ts +2 -2
  142. package/src/formats/gexf/index.ts +1 -1
  143. package/src/formats/gml/exporter.ts +1 -1
  144. package/src/formats/json/dialect.ts +37 -7
  145. package/src/formats/json/importer.ts +210 -132
  146. package/src/formats/json/obographs.ts +563 -0
  147. package/src/formats/obo/importer.ts +1695 -0
  148. package/src/formats/obo/index.ts +7 -0
  149. package/src/formats/obo/syntax.ts +466 -0
  150. package/src/index.ts +11 -0
  151. package/src/registry.ts +8 -1
  152. package/src/sniff.ts +48 -5
  153. package/dist/chunks/importer-B6rKRxzv.js.map +0 -1
  154. package/dist/chunks/writer-DHHfHn11.js.map +0 -1
@@ -32,14 +32,17 @@
32
32
  */
33
33
  import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
34
34
  import { uniqueColumnName } from "../../common/attributes.js";
35
- import { BAD_VALUE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, HYPEREDGE_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
35
+ import { AMBIGUOUS_GRAPH_NAME_CODE, BAD_VALUE_CODE, DANGLING_REFERENCE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, GRAPH_NOT_FOUND_CODE, HYPEREDGE_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, TOO_LARGE_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
36
36
  import { DirectionResolver } from "../../common/direction.js";
37
37
  import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
38
- import { readText, throwIfAborted } from "../../common/input.js";
39
- import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
38
+ import { readText, textChunks, throwIfAborted } from "../../common/input.js";
39
+ import { MAYBE_UNSAFE_INTEGER, reviveNonstandard, rewriteNumbers } from "../../common/json-elements.js";
40
+ import { chooseGraph, reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
40
41
  import { ImportReportBuilder } from "../../common/report.js";
41
42
  import { weightFromValue } from "../../common/weights.js";
43
+ import { sniffJsonDialectHead } from "../../sniff.js";
42
44
  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";
45
+ import { importObographs } from "./obographs.js";
43
46
  /**
44
47
  * The issue codes the JSON importer records (design section 8.6), by name: the codes shared with
45
48
  * the other importers (src/common/codes.ts) and the JSON-specific ones. A key is the code without
@@ -80,8 +83,18 @@ export const JSON_ISSUE = Object.freeze({
80
83
  ID_MERGED: ID_MERGED_CODE,
81
84
  /** Edge ids of mixed JSON types were stored as text. */
82
85
  EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
83
- /** A JGF `graphs` array holds more than one graph; only `graphIndex` is read. */
86
+ /** A JGF or OBO Graphs `graphs` array holds more than one graph; only the chosen one is read. */
84
87
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
88
+ /** `graphIndex` is beyond the `graphs` array, or `graphName` names none of its graphs (fatal). */
89
+ GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
90
+ /** `graphName` names more than one graph of the `graphs` array (fatal). */
91
+ AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
92
+ /** obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
93
+ OBOGRAPHS_SUBJ: "W_JSON_OBOGRAPHS_SUBJ",
94
+ /** obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under addMissingNodes false). */
95
+ DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
96
+ /** The document is longer than one JavaScript string can hold (fatal; category unsupported). */
97
+ TOO_LARGE: TOO_LARGE_CODE,
85
98
  /** JGF hyperedges under the "error" policy. */
86
99
  HYPEREDGE: HYPEREDGE_CODE,
87
100
  /** JGF hyperedges skipped under the default "skip" policy. */
@@ -180,6 +193,14 @@ function resolveJsonOptions(options) {
180
193
  if (!Number.isInteger(graphIndex) || graphIndex < 0) {
181
194
  throw unsupportedOption("graphIndex", graphIndex, ["a non-negative integer"]);
182
195
  }
196
+ const oboIds = o.oboIds ?? "curie";
197
+ if (oboIds !== "curie" && oboIds !== "iri") {
198
+ throw unsupportedOption("oboIds", oboIds, ["curie", "iri"]);
199
+ }
200
+ const typedefs = o.typedefs ?? "metadata";
201
+ if (typedefs !== "metadata" && typedefs !== "nodes") {
202
+ throw unsupportedOption("typedefs", typedefs, ["metadata", "nodes"]);
203
+ }
183
204
  const nodesPath = pathOption("nodesPath", o.nodesPath);
184
205
  const edgesPath = pathOption("edgesPath", o.edgesPath);
185
206
  if ((nodesPath !== null || edgesPath !== null) && dialect !== "auto" && !PATH_DIALECTS.has(dialect)) {
@@ -193,6 +214,9 @@ function resolveJsonOptions(options) {
193
214
  targetKey: keyOption("targetKey", o.targetKey),
194
215
  indexLinks,
195
216
  graphIndex,
217
+ choice: { graphIndex: o.graphIndex, graphName: o.graphName },
218
+ oboIds,
219
+ typedefs,
196
220
  nodesPath,
197
221
  edgesPath,
198
222
  };
@@ -493,7 +517,7 @@ class AttributeWriter {
493
517
  /**
494
518
  * Everything one import call shares between the dialect readers.
495
519
  */
496
- class ImportContext {
520
+ export class ImportContext {
497
521
  /**
498
522
  * Create the context.
499
523
  * @param sink - the sink
@@ -938,111 +962,8 @@ function parseDocument(text, report) {
938
962
  }
939
963
  return root;
940
964
  }
941
- /** A run of 16 digits not inside a fraction: the shortest integer literal that can exceed 2^53 (9007199254740992). */
942
- const MAYBE_UNSAFE_INTEGER = /(?<![0-9.])[0-9]{16}/;
943
965
  /** How many of the integers kept as text the warning lists. */
944
966
  const BIG_INTEGERS_SHOWN = 10;
945
- /**
946
- * The prefix of the string a non-standard token is rewritten to (a NUL character first, which no
947
- * sensible attribute value starts with); reviveNonstandard() turns it back into the number.
948
- */
949
- const NONSTANDARD_SENTINEL = `${String.fromCharCode(0)}graph-io:`;
950
- /** The non-standard tokens Python's json module writes, longest first so -Infinity wins over a bare minus. */
951
- const NONSTANDARD_TOKENS = [
952
- ["-Infinity", -Infinity],
953
- ["Infinity", Infinity],
954
- ["NaN", NaN],
955
- ];
956
- /** A JSON integer literal (no fraction, no exponent, no leading zero), as CANONICAL_INTEGER in common/ids.ts. */
957
- const INTEGER_LITERAL = /^-?(0|[1-9][0-9]*)$/;
958
- /**
959
- * Rewrite the numbers JSON.parse cannot read exactly, outside strings and in value positions only:
960
- * NaN / Infinity / -Infinity become sentinel strings, an integer literal that is not a safe
961
- * integer becomes a string of its digits. A container stack tells a value position (after `:`,
962
- * `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays invalid.
963
- * @param text - the document text
964
- * @returns the rewritten text, the non-standard tokens seen and the integer literals quoted
965
- */
966
- function rewriteNumbers(text) {
967
- const parts = [];
968
- const tokens = new Set();
969
- const bigIntegers = [];
970
- const arrays = [];
971
- let expectValue = true;
972
- let copied = 0;
973
- let i = 0;
974
- const n = text.length;
975
- while (i < n) {
976
- const ch = text[i];
977
- if (ch === '"') {
978
- i++;
979
- while (i < n && text[i] !== '"') {
980
- i += text[i] === "\\" ? 2 : 1;
981
- }
982
- i++;
983
- expectValue = false;
984
- continue;
985
- }
986
- if (ch === "{" || ch === "[") {
987
- arrays.push(ch === "[");
988
- expectValue = ch === "[";
989
- }
990
- else if (ch === "}" || ch === "]") {
991
- arrays.pop();
992
- expectValue = false;
993
- }
994
- else if (ch === ":") {
995
- expectValue = true;
996
- }
997
- else if (ch === ",") {
998
- expectValue = arrays.length > 0 && arrays[arrays.length - 1];
999
- }
1000
- else if (expectValue && ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") {
1001
- expectValue = false;
1002
- const token = NONSTANDARD_TOKENS.find(([word]) => text.startsWith(word, i));
1003
- let end = i;
1004
- let replacement = null;
1005
- if (token !== undefined) {
1006
- end = i + token[0].length;
1007
- tokens.add(token[0]);
1008
- replacement = JSON.stringify(`${NONSTANDARD_SENTINEL}${token[0]}`);
1009
- }
1010
- else if (ch === "-" || (ch >= "0" && ch <= "9")) {
1011
- end = i + 1;
1012
- while (end < n && "0123456789+-.eE".includes(text[end])) {
1013
- end++;
1014
- }
1015
- const literal = text.slice(i, end);
1016
- if (INTEGER_LITERAL.test(literal) && !Number.isSafeInteger(Number(literal))) {
1017
- bigIntegers.push(literal);
1018
- replacement = `"${literal}"`;
1019
- }
1020
- }
1021
- if (replacement !== null) {
1022
- parts.push(text.slice(copied, i), replacement);
1023
- copied = end;
1024
- }
1025
- i = Math.max(end, i + 1);
1026
- continue;
1027
- }
1028
- i++;
1029
- }
1030
- parts.push(text.slice(copied));
1031
- return { text: parts.join(""), tokens, bigIntegers };
1032
- }
1033
- /**
1034
- * The JSON.parse reviver that turns the sentinel strings of rewriteNumbers() back into numbers.
1035
- * @param _key - the member key (unused)
1036
- * @param value - the parsed value
1037
- * @returns the number for a sentinel string, the value otherwise
1038
- */
1039
- function reviveNonstandard(_key, value) {
1040
- if (typeof value === "string" && value.startsWith(NONSTANDARD_SENTINEL)) {
1041
- const found = NONSTANDARD_TOKENS.find(([word]) => word === value.slice(NONSTANDARD_SENTINEL.length));
1042
- return found === undefined ? value : found[1];
1043
- }
1044
- return value;
1045
- }
1046
967
  /**
1047
968
  * The dialect to read: the forced one, else the shape rule of sniffJsonDialect(); a document
1048
969
  * that matches no dialect is a fatal E_JSON_DIALECT.
@@ -1906,32 +1827,123 @@ function importJgf(ctx, root) {
1906
1827
  ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
1907
1828
  }
1908
1829
  /**
1909
- * The graph object of a JGF document: `graph`, or `graphs[graphIndex]`.
1830
+ * The graph object of a JGF document: `graph`, or the graph of `graphs` that graphIndex /
1831
+ * graphName choose (chooseGraph()).
1910
1832
  * @param ctx - the context
1911
1833
  * @param root - the document
1912
1834
  * @returns the graph object; the import fails when there is none
1913
1835
  */
1914
1836
  function jgfGraphOf(ctx, root) {
1837
+ return isJsonObject(root.graph) ? root.graph : chosenGraph(ctx, root, "JGF");
1838
+ }
1839
+ /**
1840
+ * The graph of a `graphs` array (JGF, OBO Graphs) that graphIndex / graphName choose, with
1841
+ * W_MULTIPLE_GRAPHS when the others are skipped; importAll() reads graphs[graphIndex].
1842
+ * @param ctx - the context
1843
+ * @param root - the document
1844
+ * @param what - the dialect's name, for the messages
1845
+ * @returns the graph object; the import fails when there is none
1846
+ */
1847
+ export function chosenGraph(ctx, root, what) {
1915
1848
  const { report } = ctx;
1916
- if (isJsonObject(root.graph)) {
1917
- return root.graph;
1918
- }
1919
1849
  const graphs = arraySection(root.graphs, "graphs", report) ?? [];
1920
1850
  if (graphs.length === 0) {
1921
- report.fail(JSON_ISSUE.SHAPE, "a JGF document needs a graph object or a non-empty graphs array");
1851
+ report.fail(JSON_ISSUE.SHAPE, `a ${what} document needs a graph object or a non-empty graphs array`);
1922
1852
  }
1853
+ const index = ctx.json.all === true ? ctx.json.graphIndex : chooseGraph(graphs.map(graphNameOf), ctx.json.choice, report);
1923
1854
  if (graphs.length > 1 && ctx.json.all !== true) {
1924
- report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${ctx.json.graphIndex}] is read (${graphs.length - 1} skipped), importAll() reads every one`, { element: "graphs" });
1925
- }
1926
- if (ctx.json.graphIndex >= graphs.length) {
1927
- report.fail(JSON_ISSUE.SHAPE, `graphIndex ${ctx.json.graphIndex} is beyond the ${graphs.length} graph(s)`);
1855
+ report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped), importAll() reads every one`, { element: "graphs" });
1928
1856
  }
1929
- const graph = graphs[ctx.json.graphIndex];
1857
+ const graph = graphs[index];
1930
1858
  if (!isJsonObject(graph)) {
1931
- return report.fail(JSON_ISSUE.SHAPE, `graphs[${ctx.json.graphIndex}] is not an object`);
1859
+ return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
1932
1860
  }
1933
1861
  return graph;
1934
1862
  }
1863
+ /**
1864
+ * The name a graph of a `graphs` array is listed and chosen by: its `id`, else its `label` (JGF)
1865
+ * or `lbl` (OBO Graphs).
1866
+ * @param graph - the graph
1867
+ * @returns the name, or null
1868
+ */
1869
+ function graphNameOf(graph) {
1870
+ if (!isJsonObject(graph)) {
1871
+ return null;
1872
+ }
1873
+ for (const key of ["id", "label", "lbl"]) {
1874
+ if (typeof graph[key] === "string") {
1875
+ return graph[key];
1876
+ }
1877
+ }
1878
+ return null;
1879
+ }
1880
+ /**
1881
+ * How many elements a nodes or edges section holds: an array's length, an object's key count, 0
1882
+ * when absent, null for anything else.
1883
+ * @param section - the section
1884
+ * @returns the count, or null
1885
+ */
1886
+ function countOf(section) {
1887
+ if (section === undefined || section === null) {
1888
+ return 0;
1889
+ }
1890
+ if (Array.isArray(section)) {
1891
+ return section.length;
1892
+ }
1893
+ return isJsonObject(section) ? Object.keys(section).length : null;
1894
+ }
1895
+ /**
1896
+ * The graphs of a parsed document, for listGraphs(): each entry of a JGF or OBO Graphs `graphs`
1897
+ * array with its name and counts; any other document holds one graph.
1898
+ * @param root - the parsed document
1899
+ * @param dialect - its dialect
1900
+ * @returns the listings
1901
+ */
1902
+ function listingsOf(root, dialect) {
1903
+ if ((dialect === "jgf" || dialect === "obographs") && isJsonObject(root)) {
1904
+ if (Array.isArray(root.graphs) && !isJsonObject(root.graph)) {
1905
+ return root.graphs.map((graph, index) => ({
1906
+ index,
1907
+ name: graphNameOf(graph),
1908
+ nodes: isJsonObject(graph) ? countOf(graph.nodes) : null,
1909
+ edges: isJsonObject(graph) ? countOf(graph.edges) : null,
1910
+ }));
1911
+ }
1912
+ if (isJsonObject(root.graph)) {
1913
+ const { graph } = root;
1914
+ return [{ index: 0, name: graphNameOf(graph), nodes: countOf(graph.nodes), edges: countOf(graph.edges) }];
1915
+ }
1916
+ }
1917
+ return [{ index: 0, name: null, nodes: null, edges: null }];
1918
+ }
1919
+ /** The longest string V8 makes (2^29 - 24 UTF-16 code units); a longer document cannot be one JSON.parse input. */
1920
+ const MAX_TEXT_LENGTH = 2 ** 29 - 24;
1921
+ /**
1922
+ * Read the whole input as one string, failing with E_TOO_LARGE (category unsupported) before the
1923
+ * join when it is longer than one JavaScript string can hold (OBO Graphs files such as
1924
+ * ncbitaxon.json are; design 7.1 defers streaming the JSON reader).
1925
+ * @param input - the input
1926
+ * @param report - the report
1927
+ * @param options - cancellation, progress and encoding
1928
+ * @returns the text
1929
+ */
1930
+ async function readJsonText(input, report, options) {
1931
+ if (typeof input === "string") {
1932
+ return readText(input, report, options);
1933
+ }
1934
+ const parts = [];
1935
+ let length = 0;
1936
+ for await (const chunk of textChunks(input, report, options)) {
1937
+ length += chunk.length;
1938
+ if (length > MAX_TEXT_LENGTH) {
1939
+ const message = `the document is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`;
1940
+ report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
1941
+ throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
1942
+ }
1943
+ parts.push(chunk);
1944
+ }
1945
+ return parts.length === 1 ? parts[0] : parts.join("");
1946
+ }
1935
1947
  /**
1936
1948
  * Push a JGF node: `label` into the declared column, `metadata` as attributes.
1937
1949
  * @param ctx - the context
@@ -2308,7 +2320,10 @@ export const jsonImporter = Object.freeze({
2308
2320
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
2309
2321
  return 0;
2310
2322
  }
2311
- return SNIFF_KEYS.some((key) => trimmed.includes(key)) ? 0.9 : 0.5;
2323
+ const score = SNIFF_KEYS.some((key) => trimmed.includes(key)) ? 0.9 : 0.5;
2324
+ // a top-level array is a graph only as Cytoscape.js elements (`[{"data": ...}]`): a CX or
2325
+ // CX2 document (`[{"metaData": ...}]`, `[{"CXVersion": ...}]`) is another format's
2326
+ return trimmed.startsWith("[") && sniffJsonDialectHead(trimmed) !== "cytoscape" ? Math.min(score, 0.3) : score;
2312
2327
  },
2313
2328
  /**
2314
2329
  * Read a JSON graph document into the sink.
@@ -2321,11 +2336,27 @@ export const jsonImporter = Object.freeze({
2321
2336
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2322
2337
  const json = resolveJsonOptions(options);
2323
2338
  const report = new ImportReportBuilder("json", resolved.errorLimit);
2324
- const text = await readText(input, report, resolved);
2339
+ const text = await readJsonText(input, report, resolved);
2325
2340
  const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2326
2341
  readGraph(root, dialect, sink, report, resolved, json, options);
2327
2342
  return report.finish();
2328
2343
  },
2344
+ /**
2345
+ * List the graphs of a JSON document without importing them: each entry of a JGF or OBO
2346
+ * Graphs `graphs` array with its name (`id`, else its label) and node and edge counts; any
2347
+ * other document holds one graph.
2348
+ * @param input - the text, bytes or stream
2349
+ * @param options - format-specific and common options
2350
+ * @returns one listing per graph
2351
+ */
2352
+ async listGraphs(input, options) {
2353
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2354
+ const json = resolveJsonOptions(options);
2355
+ const report = new ImportReportBuilder("json", resolved.errorLimit);
2356
+ const text = await readJsonText(input, report, resolved);
2357
+ const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2358
+ return listingsOf(root, dialect);
2359
+ },
2329
2360
  /**
2330
2361
  * Read every graph of a JSON document: each entry of a JGF `graphs` array into its own sink;
2331
2362
  * any other document holds one graph.
@@ -2338,11 +2369,9 @@ export const jsonImporter = Object.freeze({
2338
2369
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2339
2370
  const json = resolveJsonOptions(options);
2340
2371
  const first = new ImportReportBuilder("json", resolved.errorLimit);
2341
- const text = await readText(input, first, resolved);
2372
+ const text = await readJsonText(input, first, resolved);
2342
2373
  const { root, dialect } = documentOf(parseDocument(text, first), json, first);
2343
- const graphs = dialect === "jgf" && isJsonObject(root) && !isJsonObject(root.graph) && Array.isArray(root.graphs)
2344
- ? root.graphs.length
2345
- : 1;
2374
+ const graphs = listingsOf(root, dialect).length;
2346
2375
  const reports = [];
2347
2376
  for (let i = 0; i < Math.max(graphs, 1); i++) {
2348
2377
  const report = i === 0 ? first : new ImportReportBuilder("json", resolved.errorLimit);
@@ -2364,7 +2393,8 @@ export const jsonImporter = Object.freeze({
2364
2393
  */
2365
2394
  function readGraph(root, dialect, sink, report, resolved, json, options) {
2366
2395
  const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== undefined);
2367
- reportSinkOptions(sink, options, report);
2396
+ // the obographs reader refuses missing endpoints itself, so addMissingNodes false holds on any sink
2397
+ reportSinkOptions(sink, options, report, dialect === "obographs");
2368
2398
  reportUnusedOptions(options, report, USED_OPTIONS);
2369
2399
  if (dialect === "cytoscape") {
2370
2400
  importCytoscape(ctx, root);
@@ -2394,6 +2424,9 @@ function readGraph(root, dialect, sink, report, resolved, json, options) {
2394
2424
  case "tree":
2395
2425
  importTree(ctx, doc);
2396
2426
  break;
2427
+ case "obographs":
2428
+ importObographs(ctx, doc);
2429
+ break;
2397
2430
  default: {
2398
2431
  const name = dialect;
2399
2432
  throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {