@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
@@ -43,11 +43,14 @@ import {
43
43
 
44
44
  import { uniqueColumnName } from "../../common/attributes.js";
45
45
  import {
46
+ AMBIGUOUS_GRAPH_NAME_CODE,
46
47
  BAD_VALUE_CODE,
48
+ DANGLING_REFERENCE_CODE,
47
49
  DUPLICATE_EDGE_ID_CODE,
48
50
  DUPLICATE_NODE_CODE,
49
51
  EMPTY_INPUT_CODE,
50
52
  ENCODING_FALLBACK_CODE,
53
+ GRAPH_NOT_FOUND_CODE,
51
54
  HYPEREDGE_CODE,
52
55
  INVALID_ENCODING_CODE,
53
56
  INVALID_UTF8_CODE,
@@ -56,13 +59,16 @@ import {
56
59
  MULTIPLE_GRAPHS_CODE,
57
60
  OPTION_IGNORED_CODE,
58
61
  SYNTAX_CODE,
62
+ TOO_LARGE_CODE,
59
63
  UNKNOWN_ENCODING_CODE,
60
64
  UNKNOWN_PARENT_CODE,
61
65
  } from "../../common/codes.js";
62
66
  import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
63
67
  import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
64
- import { readText, throwIfAborted } from "../../common/input.js";
68
+ import { readText, textChunks, throwIfAborted } from "../../common/input.js";
69
+ import { MAYBE_UNSAFE_INTEGER, reviveNonstandard, rewriteNumbers } from "../../common/json-elements.js";
65
70
  import {
71
+ chooseGraph,
66
72
  type ImportFormatDefaults,
67
73
  reportSinkOptions,
68
74
  reportUnusedOptions,
@@ -72,7 +78,15 @@ import {
72
78
  } from "../../common/options.js";
73
79
  import { ImportReportBuilder } from "../../common/report.js";
74
80
  import { weightFromValue } from "../../common/weights.js";
75
- import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
81
+ import { sniffJsonDialectHead } from "../../sniff.js";
82
+ import {
83
+ type CommonImportOptions,
84
+ type GraphChoiceOptions,
85
+ type GraphImporter,
86
+ type GraphListing,
87
+ type ImportInput,
88
+ type ImportReport,
89
+ } from "../../types.js";
76
90
  import {
77
91
  CLASSES_COLUMN,
78
92
  CYTOSCAPE_ELEMENT_KEYS,
@@ -92,9 +106,14 @@ import {
92
106
  sniffJsonDialect,
93
107
  SUFFIX,
94
108
  } from "./dialect.js";
109
+ import { importObographs } from "./obographs.js";
95
110
 
96
- /** The format-specific options of the JSON importer. */
97
- export interface JsonImportOptions {
111
+ /**
112
+ * The format-specific options of the JSON importer. `graphIndex` / `graphName` choose one graph of
113
+ * a JGF or OBO Graphs `graphs` array (the first by default); a graph's name is its `id`, else its
114
+ * label.
115
+ */
116
+ export interface JsonImportOptions extends GraphChoiceOptions {
98
117
  /** The dialect to read; "auto" (default) sniffs the parsed document. */
99
118
  dialect?: JsonImportDialect | "auto" | undefined;
100
119
  /**
@@ -113,8 +132,18 @@ export interface JsonImportOptions {
113
132
  * every endpoint is an integer below the node count and no node id is a number.
114
133
  */
115
134
  indexLinks?: boolean | "auto" | undefined;
116
- /** jgf: which graph of a `graphs` array to read; 0 by default. */
117
- graphIndex?: number | undefined;
135
+ /**
136
+ * obographs: "curie" (default) reads `http://purl.obolibrary.org/obo/GO_0008150` as `GO:0008150`
137
+ * and `.../obo/go#regulates` as `regulates`, the identifiers the `.obo` file of the same ontology
138
+ * writes; "iri" keeps every IRI as written.
139
+ */
140
+ oboIds?: "curie" | "iri" | undefined;
141
+ /**
142
+ * obographs: "metadata" (default) keeps PROPERTY nodes and their subPropertyOf / inverseOf edges
143
+ * in `meta.extra.obographs`, as the OBO importer keeps `[Typedef]` frames; "nodes" makes them
144
+ * nodes and edges.
145
+ */
146
+ typedefs?: "metadata" | "nodes" | undefined;
118
147
  /**
119
148
  * node-link / d3 / vis / graphology: where the node array is, as a dotted path of object keys
120
149
  * from the document root (`"data.nodes"`); the object holding it is read as the graph record
@@ -171,8 +200,18 @@ export const JSON_ISSUE = Object.freeze({
171
200
  ID_MERGED: ID_MERGED_CODE,
172
201
  /** Edge ids of mixed JSON types were stored as text. */
173
202
  EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
174
- /** A JGF `graphs` array holds more than one graph; only `graphIndex` is read. */
203
+ /** A JGF or OBO Graphs `graphs` array holds more than one graph; only the chosen one is read. */
175
204
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
205
+ /** `graphIndex` is beyond the `graphs` array, or `graphName` names none of its graphs (fatal). */
206
+ GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
207
+ /** `graphName` names more than one graph of the `graphs` array (fatal). */
208
+ AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
209
+ /** obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
210
+ OBOGRAPHS_SUBJ: "W_JSON_OBOGRAPHS_SUBJ",
211
+ /** obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under addMissingNodes false). */
212
+ DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
213
+ /** The document is longer than one JavaScript string can hold (fatal; category unsupported). */
214
+ TOO_LARGE: TOO_LARGE_CODE,
176
215
  /** JGF hyperedges under the "error" policy. */
177
216
  HYPEREDGE: HYPEREDGE_CODE,
178
217
  /** JGF hyperedges skipped under the default "skip" policy. */
@@ -258,7 +297,7 @@ const GRAPHOLOGY_EDGE_KEYS: ReadonlySet<string> = new Set(["key", "source", "tar
258
297
  const VIS_SOURCE_KEYS: readonly string[] = Object.freeze(["from"]);
259
298
  const VIS_TARGET_KEYS: readonly string[] = Object.freeze(["to"]);
260
299
 
261
- type JsonRecord = Record<string, unknown>;
300
+ export type JsonRecord = Record<string, unknown>;
262
301
 
263
302
  /** An edge id column declared from a scan of the file's edge ids. */
264
303
  interface EdgeIdColumn {
@@ -286,6 +325,12 @@ interface ResolvedJsonOptions {
286
325
  readonly targetKey: string | null;
287
326
  readonly indexLinks: boolean | "auto";
288
327
  readonly graphIndex: number;
328
+ /** The caller's graphIndex / graphName, as given, for chooseGraph(). */
329
+ readonly choice: GraphChoiceOptions;
330
+ /** obographs: CURIE or IRI ids. */
331
+ readonly oboIds: "curie" | "iri";
332
+ /** obographs: PROPERTY nodes as metadata or as nodes. */
333
+ readonly typedefs: "metadata" | "nodes";
289
334
  /** The dotted path segments of the node array, or null for the root's own nodes key. */
290
335
  readonly nodesPath: readonly string[] | null;
291
336
  /** The dotted path segments of the edge array, or null for the edges / links key beside the nodes. */
@@ -313,6 +358,14 @@ function resolveJsonOptions(options: (JsonImportOptions & CommonImportOptions) |
313
358
  if (!Number.isInteger(graphIndex) || graphIndex < 0) {
314
359
  throw unsupportedOption("graphIndex", graphIndex, ["a non-negative integer"]);
315
360
  }
361
+ const oboIds = o.oboIds ?? "curie";
362
+ if (oboIds !== "curie" && oboIds !== "iri") {
363
+ throw unsupportedOption("oboIds", oboIds, ["curie", "iri"]);
364
+ }
365
+ const typedefs = o.typedefs ?? "metadata";
366
+ if (typedefs !== "metadata" && typedefs !== "nodes") {
367
+ throw unsupportedOption("typedefs", typedefs, ["metadata", "nodes"]);
368
+ }
316
369
  const nodesPath = pathOption("nodesPath", o.nodesPath);
317
370
  const edgesPath = pathOption("edgesPath", o.edgesPath);
318
371
  if ((nodesPath !== null || edgesPath !== null) && dialect !== "auto" && !PATH_DIALECTS.has(dialect)) {
@@ -326,6 +379,9 @@ function resolveJsonOptions(options: (JsonImportOptions & CommonImportOptions) |
326
379
  targetKey: keyOption("targetKey", o.targetKey),
327
380
  indexLinks,
328
381
  graphIndex,
382
+ choice: { graphIndex: o.graphIndex, graphName: o.graphName },
383
+ oboIds,
384
+ typedefs,
329
385
  nodesPath,
330
386
  edgesPath,
331
387
  };
@@ -666,7 +722,7 @@ class AttributeWriter {
666
722
  /**
667
723
  * Everything one import call shares between the dialect readers.
668
724
  */
669
- class ImportContext {
725
+ export class ImportContext {
670
726
  readonly sink: GraphSink;
671
727
 
672
728
  readonly report: ImportReportBuilder;
@@ -1218,113 +1274,9 @@ function parseDocument(text: string, report: ImportReportBuilder): unknown {
1218
1274
  return root;
1219
1275
  }
1220
1276
 
1221
- /** A run of 16 digits not inside a fraction: the shortest integer literal that can exceed 2^53 (9007199254740992). */
1222
- const MAYBE_UNSAFE_INTEGER = /(?<![0-9.])[0-9]{16}/;
1223
-
1224
1277
  /** How many of the integers kept as text the warning lists. */
1225
1278
  const BIG_INTEGERS_SHOWN = 10;
1226
1279
 
1227
- /**
1228
- * The prefix of the string a non-standard token is rewritten to (a NUL character first, which no
1229
- * sensible attribute value starts with); reviveNonstandard() turns it back into the number.
1230
- */
1231
- const NONSTANDARD_SENTINEL = `${String.fromCharCode(0)}graph-io:`;
1232
-
1233
- /** The non-standard tokens Python's json module writes, longest first so -Infinity wins over a bare minus. */
1234
- const NONSTANDARD_TOKENS: readonly (readonly [string, number])[] = [
1235
- ["-Infinity", -Infinity],
1236
- ["Infinity", Infinity],
1237
- ["NaN", NaN],
1238
- ];
1239
-
1240
- /** A JSON integer literal (no fraction, no exponent, no leading zero), as CANONICAL_INTEGER in common/ids.ts. */
1241
- const INTEGER_LITERAL = /^-?(0|[1-9][0-9]*)$/;
1242
-
1243
- /**
1244
- * Rewrite the numbers JSON.parse cannot read exactly, outside strings and in value positions only:
1245
- * NaN / Infinity / -Infinity become sentinel strings, an integer literal that is not a safe
1246
- * integer becomes a string of its digits. A container stack tells a value position (after `:`,
1247
- * `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays invalid.
1248
- * @param text - the document text
1249
- * @returns the rewritten text, the non-standard tokens seen and the integer literals quoted
1250
- */
1251
- function rewriteNumbers(text: string): { text: string; tokens: Set<string>; bigIntegers: string[] } {
1252
- const parts: string[] = [];
1253
- const tokens = new Set<string>();
1254
- const bigIntegers: string[] = [];
1255
- const arrays: boolean[] = [];
1256
- let expectValue = true;
1257
- let copied = 0;
1258
- let i = 0;
1259
- const n = text.length;
1260
- while (i < n) {
1261
- const ch = text[i];
1262
- if (ch === '"') {
1263
- i++;
1264
- while (i < n && text[i] !== '"') {
1265
- i += text[i] === "\\" ? 2 : 1;
1266
- }
1267
- i++;
1268
- expectValue = false;
1269
- continue;
1270
- }
1271
- if (ch === "{" || ch === "[") {
1272
- arrays.push(ch === "[");
1273
- expectValue = ch === "[";
1274
- } else if (ch === "}" || ch === "]") {
1275
- arrays.pop();
1276
- expectValue = false;
1277
- } else if (ch === ":") {
1278
- expectValue = true;
1279
- } else if (ch === ",") {
1280
- expectValue = arrays.length > 0 && arrays[arrays.length - 1];
1281
- } else if (expectValue && ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") {
1282
- expectValue = false;
1283
- const token = NONSTANDARD_TOKENS.find(([word]) => text.startsWith(word, i));
1284
- let end = i;
1285
- let replacement: string | null = null;
1286
- if (token !== undefined) {
1287
- end = i + token[0].length;
1288
- tokens.add(token[0]);
1289
- replacement = JSON.stringify(`${NONSTANDARD_SENTINEL}${token[0]}`);
1290
- } else if (ch === "-" || (ch >= "0" && ch <= "9")) {
1291
- end = i + 1;
1292
- while (end < n && "0123456789+-.eE".includes(text[end])) {
1293
- end++;
1294
- }
1295
- const literal = text.slice(i, end);
1296
- if (INTEGER_LITERAL.test(literal) && !Number.isSafeInteger(Number(literal))) {
1297
- bigIntegers.push(literal);
1298
- replacement = `"${literal}"`;
1299
- }
1300
- }
1301
- if (replacement !== null) {
1302
- parts.push(text.slice(copied, i), replacement);
1303
- copied = end;
1304
- }
1305
- i = Math.max(end, i + 1);
1306
- continue;
1307
- }
1308
- i++;
1309
- }
1310
- parts.push(text.slice(copied));
1311
- return { text: parts.join(""), tokens, bigIntegers };
1312
- }
1313
-
1314
- /**
1315
- * The JSON.parse reviver that turns the sentinel strings of rewriteNumbers() back into numbers.
1316
- * @param _key - the member key (unused)
1317
- * @param value - the parsed value
1318
- * @returns the number for a sentinel string, the value otherwise
1319
- */
1320
- function reviveNonstandard(_key: string, value: unknown): unknown {
1321
- if (typeof value === "string" && value.startsWith(NONSTANDARD_SENTINEL)) {
1322
- const found = NONSTANDARD_TOKENS.find(([word]) => word === value.slice(NONSTANDARD_SENTINEL.length));
1323
- return found === undefined ? value : found[1];
1324
- }
1325
- return value;
1326
- }
1327
-
1328
1280
  /**
1329
1281
  * The dialect to read: the forced one, else the shape rule of sniffJsonDialect(); a document
1330
1282
  * that matches no dialect is a fatal E_JSON_DIALECT.
@@ -2339,38 +2291,140 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
2339
2291
  }
2340
2292
 
2341
2293
  /**
2342
- * The graph object of a JGF document: `graph`, or `graphs[graphIndex]`.
2294
+ * The graph object of a JGF document: `graph`, or the graph of `graphs` that graphIndex /
2295
+ * graphName choose (chooseGraph()).
2343
2296
  * @param ctx - the context
2344
2297
  * @param root - the document
2345
2298
  * @returns the graph object; the import fails when there is none
2346
2299
  */
2347
2300
  function jgfGraphOf(ctx: ImportContext, root: JsonRecord): JsonRecord {
2301
+ return isJsonObject(root.graph) ? root.graph : chosenGraph(ctx, root, "JGF");
2302
+ }
2303
+
2304
+ /**
2305
+ * The graph of a `graphs` array (JGF, OBO Graphs) that graphIndex / graphName choose, with
2306
+ * W_MULTIPLE_GRAPHS when the others are skipped; importAll() reads graphs[graphIndex].
2307
+ * @param ctx - the context
2308
+ * @param root - the document
2309
+ * @param what - the dialect's name, for the messages
2310
+ * @returns the graph object; the import fails when there is none
2311
+ */
2312
+ export function chosenGraph(ctx: ImportContext, root: JsonRecord, what: string): JsonRecord {
2348
2313
  const { report } = ctx;
2349
- if (isJsonObject(root.graph)) {
2350
- return root.graph;
2351
- }
2352
2314
  const graphs = arraySection(root.graphs, "graphs", report) ?? [];
2353
2315
  if (graphs.length === 0) {
2354
- report.fail(JSON_ISSUE.SHAPE, "a JGF document needs a graph object or a non-empty graphs array");
2316
+ report.fail(JSON_ISSUE.SHAPE, `a ${what} document needs a graph object or a non-empty graphs array`);
2355
2317
  }
2318
+ const index =
2319
+ ctx.json.all === true ? ctx.json.graphIndex : chooseGraph(graphs.map(graphNameOf), ctx.json.choice, report);
2356
2320
  if (graphs.length > 1 && ctx.json.all !== true) {
2357
2321
  report.warning(
2358
2322
  "unsupported",
2359
2323
  JSON_ISSUE.MULTIPLE_GRAPHS,
2360
- `the document holds ${graphs.length} graphs; only graphs[${ctx.json.graphIndex}] is read (${graphs.length - 1} skipped), importAll() reads every one`,
2324
+ `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped), importAll() reads every one`,
2361
2325
  { element: "graphs" },
2362
2326
  );
2363
2327
  }
2364
- if (ctx.json.graphIndex >= graphs.length) {
2365
- report.fail(JSON_ISSUE.SHAPE, `graphIndex ${ctx.json.graphIndex} is beyond the ${graphs.length} graph(s)`);
2366
- }
2367
- const graph = graphs[ctx.json.graphIndex];
2328
+ const graph = graphs[index];
2368
2329
  if (!isJsonObject(graph)) {
2369
- return report.fail(JSON_ISSUE.SHAPE, `graphs[${ctx.json.graphIndex}] is not an object`);
2330
+ return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
2370
2331
  }
2371
2332
  return graph;
2372
2333
  }
2373
2334
 
2335
+ /**
2336
+ * The name a graph of a `graphs` array is listed and chosen by: its `id`, else its `label` (JGF)
2337
+ * or `lbl` (OBO Graphs).
2338
+ * @param graph - the graph
2339
+ * @returns the name, or null
2340
+ */
2341
+ function graphNameOf(graph: unknown): string | null {
2342
+ if (!isJsonObject(graph)) {
2343
+ return null;
2344
+ }
2345
+ for (const key of ["id", "label", "lbl"]) {
2346
+ if (typeof graph[key] === "string") {
2347
+ return graph[key];
2348
+ }
2349
+ }
2350
+ return null;
2351
+ }
2352
+
2353
+ /**
2354
+ * How many elements a nodes or edges section holds: an array's length, an object's key count, 0
2355
+ * when absent, null for anything else.
2356
+ * @param section - the section
2357
+ * @returns the count, or null
2358
+ */
2359
+ function countOf(section: unknown): number | null {
2360
+ if (section === undefined || section === null) {
2361
+ return 0;
2362
+ }
2363
+ if (Array.isArray(section)) {
2364
+ return section.length;
2365
+ }
2366
+ return isJsonObject(section) ? Object.keys(section).length : null;
2367
+ }
2368
+
2369
+ /**
2370
+ * The graphs of a parsed document, for listGraphs(): each entry of a JGF or OBO Graphs `graphs`
2371
+ * array with its name and counts; any other document holds one graph.
2372
+ * @param root - the parsed document
2373
+ * @param dialect - its dialect
2374
+ * @returns the listings
2375
+ */
2376
+ function listingsOf(root: unknown, dialect: JsonImportDialect): GraphListing[] {
2377
+ if ((dialect === "jgf" || dialect === "obographs") && isJsonObject(root)) {
2378
+ if (Array.isArray(root.graphs) && !isJsonObject(root.graph)) {
2379
+ return root.graphs.map((graph: unknown, index) => ({
2380
+ index,
2381
+ name: graphNameOf(graph),
2382
+ nodes: isJsonObject(graph) ? countOf(graph.nodes) : null,
2383
+ edges: isJsonObject(graph) ? countOf(graph.edges) : null,
2384
+ }));
2385
+ }
2386
+ if (isJsonObject(root.graph)) {
2387
+ const { graph } = root;
2388
+ return [{ index: 0, name: graphNameOf(graph), nodes: countOf(graph.nodes), edges: countOf(graph.edges) }];
2389
+ }
2390
+ }
2391
+ return [{ index: 0, name: null, nodes: null, edges: null }];
2392
+ }
2393
+
2394
+ /** The longest string V8 makes (2^29 - 24 UTF-16 code units); a longer document cannot be one JSON.parse input. */
2395
+ const MAX_TEXT_LENGTH = 2 ** 29 - 24;
2396
+
2397
+ /**
2398
+ * Read the whole input as one string, failing with E_TOO_LARGE (category unsupported) before the
2399
+ * join when it is longer than one JavaScript string can hold (OBO Graphs files such as
2400
+ * ncbitaxon.json are; design 7.1 defers streaming the JSON reader).
2401
+ * @param input - the input
2402
+ * @param report - the report
2403
+ * @param options - cancellation, progress and encoding
2404
+ * @returns the text
2405
+ */
2406
+ async function readJsonText(
2407
+ input: ImportInput,
2408
+ report: ImportReportBuilder,
2409
+ options: ResolvedImportOptions,
2410
+ ): Promise<string> {
2411
+ if (typeof input === "string") {
2412
+ return readText(input, report, options);
2413
+ }
2414
+ const parts: string[] = [];
2415
+ let length = 0;
2416
+ for await (const chunk of textChunks(input, report, options)) {
2417
+ length += chunk.length;
2418
+ if (length > MAX_TEXT_LENGTH) {
2419
+ const message = `the document is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`;
2420
+ report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
2421
+ throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
2422
+ }
2423
+ parts.push(chunk);
2424
+ }
2425
+ return parts.length === 1 ? parts[0] : parts.join("");
2426
+ }
2427
+
2374
2428
  /**
2375
2429
  * Push a JGF node: `label` into the declared column, `metadata` as attributes.
2376
2430
  * @param ctx - the context
@@ -2823,7 +2877,10 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2823
2877
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
2824
2878
  return 0;
2825
2879
  }
2826
- return SNIFF_KEYS.some((key) => trimmed.includes(key)) ? 0.9 : 0.5;
2880
+ const score = SNIFF_KEYS.some((key) => trimmed.includes(key)) ? 0.9 : 0.5;
2881
+ // a top-level array is a graph only as Cytoscape.js elements (`[{"data": ...}]`): a CX or
2882
+ // CX2 document (`[{"metaData": ...}]`, `[{"CXVersion": ...}]`) is another format's
2883
+ return trimmed.startsWith("[") && sniffJsonDialectHead(trimmed) !== "cytoscape" ? Math.min(score, 0.3) : score;
2827
2884
  },
2828
2885
 
2829
2886
  /**
@@ -2841,12 +2898,32 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2841
2898
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2842
2899
  const json = resolveJsonOptions(options);
2843
2900
  const report = new ImportReportBuilder("json", resolved.errorLimit);
2844
- const text = await readText(input, report, resolved);
2901
+ const text = await readJsonText(input, report, resolved);
2845
2902
  const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2846
2903
  readGraph(root, dialect, sink, report, resolved, json, options);
2847
2904
  return report.finish();
2848
2905
  },
2849
2906
 
2907
+ /**
2908
+ * List the graphs of a JSON document without importing them: each entry of a JGF or OBO
2909
+ * Graphs `graphs` array with its name (`id`, else its label) and node and edge counts; any
2910
+ * other document holds one graph.
2911
+ * @param input - the text, bytes or stream
2912
+ * @param options - format-specific and common options
2913
+ * @returns one listing per graph
2914
+ */
2915
+ async listGraphs(
2916
+ input: ImportInput,
2917
+ options?: JsonImportOptions & CommonImportOptions,
2918
+ ): Promise<readonly GraphListing[]> {
2919
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2920
+ const json = resolveJsonOptions(options);
2921
+ const report = new ImportReportBuilder("json", resolved.errorLimit);
2922
+ const text = await readJsonText(input, report, resolved);
2923
+ const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2924
+ return listingsOf(root, dialect);
2925
+ },
2926
+
2850
2927
  /**
2851
2928
  * Read every graph of a JSON document: each entry of a JGF `graphs` array into its own sink;
2852
2929
  * any other document holds one graph.
@@ -2863,12 +2940,9 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2863
2940
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2864
2941
  const json = resolveJsonOptions(options);
2865
2942
  const first = new ImportReportBuilder("json", resolved.errorLimit);
2866
- const text = await readText(input, first, resolved);
2943
+ const text = await readJsonText(input, first, resolved);
2867
2944
  const { root, dialect } = documentOf(parseDocument(text, first), json, first);
2868
- const graphs =
2869
- dialect === "jgf" && isJsonObject(root) && !isJsonObject(root.graph) && Array.isArray(root.graphs)
2870
- ? root.graphs.length
2871
- : 1;
2945
+ const graphs = listingsOf(root, dialect).length;
2872
2946
  const reports: ImportReport[] = [];
2873
2947
  for (let i = 0; i < Math.max(graphs, 1); i++) {
2874
2948
  const report = i === 0 ? first : new ImportReportBuilder("json", resolved.errorLimit);
@@ -2899,7 +2973,8 @@ function readGraph(
2899
2973
  options: (JsonImportOptions & CommonImportOptions) | undefined,
2900
2974
  ): void {
2901
2975
  const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== undefined);
2902
- reportSinkOptions(sink, options, report);
2976
+ // the obographs reader refuses missing endpoints itself, so addMissingNodes false holds on any sink
2977
+ reportSinkOptions(sink, options, report, dialect === "obographs");
2903
2978
  reportUnusedOptions(options, report, USED_OPTIONS);
2904
2979
  if (dialect === "cytoscape") {
2905
2980
  importCytoscape(ctx, root);
@@ -2929,6 +3004,9 @@ function readGraph(
2929
3004
  case "tree":
2930
3005
  importTree(ctx, doc);
2931
3006
  break;
3007
+ case "obographs":
3008
+ importObographs(ctx, doc);
3009
+ break;
2932
3010
  default: {
2933
3011
  const name: string = dialect;
2934
3012
  throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {