@graphty/graph-io 0.3.17 → 0.3.18

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 (91) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -3
  3. package/dist/chunks/{escape-scjHxjpr.js → escape-D-gZWO26.js} +3 -2
  4. package/dist/chunks/{escape-scjHxjpr.js.map → escape-D-gZWO26.js.map} +1 -1
  5. package/dist/chunks/{importer-Xg07gk7p.js → importer-Br_QeAeE.js} +4 -3
  6. package/dist/chunks/{importer-Xg07gk7p.js.map → importer-Br_QeAeE.js.map} +1 -1
  7. package/dist/chunks/{importer-ruMWWvVs.js → importer-DHagxvDD.js} +4 -3
  8. package/dist/chunks/{importer-ruMWWvVs.js.map → importer-DHagxvDD.js.map} +1 -1
  9. package/dist/chunks/{importer-B6rKRxzv.js → importer-Du5crN9l.js} +509 -25
  10. package/dist/chunks/importer-Du5crN9l.js.map +1 -0
  11. package/dist/chunks/importer-aNJfe0qu.js +1614 -0
  12. package/dist/chunks/importer-aNJfe0qu.js.map +1 -0
  13. package/dist/chunks/{importer-BmOl9gLW.js → importer-d0uQxFp6.js} +4 -3
  14. package/dist/chunks/{importer-BmOl9gLW.js.map → importer-d0uQxFp6.js.map} +1 -1
  15. package/dist/chunks/ontology-BnrJ4I98.js +113 -0
  16. package/dist/chunks/ontology-BnrJ4I98.js.map +1 -0
  17. package/dist/chunks/{records-DSpbTE5s.js → records-Bk9jgodz.js} +2 -2
  18. package/dist/chunks/{records-DSpbTE5s.js.map → records-Bk9jgodz.js.map} +1 -1
  19. package/dist/chunks/{writer-DHHfHn11.js → report-BOk0p5y8.js} +97 -919
  20. package/dist/chunks/report-BOk0p5y8.js.map +1 -0
  21. package/dist/chunks/writer-GAdltGmC.js +827 -0
  22. package/dist/chunks/writer-GAdltGmC.js.map +1 -0
  23. package/dist/csv.js +4 -3
  24. package/dist/csv.js.map +1 -1
  25. package/dist/dot.js +1 -1
  26. package/dist/gexf.js +3 -2
  27. package/dist/gexf.js.map +1 -1
  28. package/dist/gml.js +3 -2
  29. package/dist/gml.js.map +1 -1
  30. package/dist/graph-io.js +160 -141
  31. package/dist/graph-io.js.map +1 -1
  32. package/dist/graphml.js +1 -1
  33. package/dist/json.js +1 -1
  34. package/dist/neo4j.js +4 -3
  35. package/dist/neo4j.js.map +1 -1
  36. package/dist/obo.d.ts +1 -0
  37. package/dist/obo.js +6 -0
  38. package/dist/obo.js.map +1 -0
  39. package/dist/pajek.js +1 -1
  40. package/dist/src/common/ontology.d.ts +59 -0
  41. package/dist/src/common/ontology.d.ts.map +1 -0
  42. package/dist/src/common/ontology.js +147 -0
  43. package/dist/src/common/ontology.js.map +1 -0
  44. package/dist/src/formats/json/dialect.d.ts +8 -6
  45. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  46. package/dist/src/formats/json/dialect.js +26 -3
  47. package/dist/src/formats/json/dialect.js.map +1 -1
  48. package/dist/src/formats/json/importer.d.ts +324 -7
  49. package/dist/src/formats/json/importer.d.ts.map +1 -1
  50. package/dist/src/formats/json/importer.js +153 -22
  51. package/dist/src/formats/json/importer.js.map +1 -1
  52. package/dist/src/formats/json/obographs.d.ts +21 -0
  53. package/dist/src/formats/json/obographs.d.ts.map +1 -0
  54. package/dist/src/formats/json/obographs.js +476 -0
  55. package/dist/src/formats/json/obographs.js.map +1 -0
  56. package/dist/src/formats/obo/importer.d.ts +90 -0
  57. package/dist/src/formats/obo/importer.d.ts.map +1 -0
  58. package/dist/src/formats/obo/importer.js +1248 -0
  59. package/dist/src/formats/obo/importer.js.map +1 -0
  60. package/dist/src/formats/obo/index.d.ts +7 -0
  61. package/dist/src/formats/obo/index.d.ts.map +1 -0
  62. package/dist/src/formats/obo/index.js +7 -0
  63. package/dist/src/formats/obo/index.js.map +1 -0
  64. package/dist/src/formats/obo/syntax.d.ts +121 -0
  65. package/dist/src/formats/obo/syntax.d.ts.map +1 -0
  66. package/dist/src/formats/obo/syntax.js +424 -0
  67. package/dist/src/formats/obo/syntax.js.map +1 -0
  68. package/dist/src/index.d.ts +1 -0
  69. package/dist/src/index.d.ts.map +1 -1
  70. package/dist/src/index.js +1 -0
  71. package/dist/src/index.js.map +1 -1
  72. package/dist/src/registry.d.ts.map +1 -1
  73. package/dist/src/registry.js +3 -1
  74. package/dist/src/registry.js.map +1 -1
  75. package/dist/src/sniff.d.ts +1 -1
  76. package/dist/src/sniff.d.ts.map +1 -1
  77. package/dist/src/sniff.js +23 -3
  78. package/dist/src/sniff.js.map +1 -1
  79. package/package.json +6 -1
  80. package/src/common/ontology.ts +169 -0
  81. package/src/formats/json/dialect.ts +37 -7
  82. package/src/formats/json/importer.ts +204 -27
  83. package/src/formats/json/obographs.ts +563 -0
  84. package/src/formats/obo/importer.ts +1695 -0
  85. package/src/formats/obo/index.ts +7 -0
  86. package/src/formats/obo/syntax.ts +466 -0
  87. package/src/index.ts +1 -0
  88. package/src/registry.ts +3 -1
  89. package/src/sniff.ts +35 -5
  90. package/dist/chunks/importer-B6rKRxzv.js.map +0 -1
  91. 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,15 @@ 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";
65
69
  import {
70
+ chooseGraph,
66
71
  type ImportFormatDefaults,
67
72
  reportSinkOptions,
68
73
  reportUnusedOptions,
@@ -72,7 +77,14 @@ import {
72
77
  } from "../../common/options.js";
73
78
  import { ImportReportBuilder } from "../../common/report.js";
74
79
  import { weightFromValue } from "../../common/weights.js";
75
- import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
80
+ import {
81
+ type CommonImportOptions,
82
+ type GraphChoiceOptions,
83
+ type GraphImporter,
84
+ type GraphListing,
85
+ type ImportInput,
86
+ type ImportReport,
87
+ } from "../../types.js";
76
88
  import {
77
89
  CLASSES_COLUMN,
78
90
  CYTOSCAPE_ELEMENT_KEYS,
@@ -92,9 +104,14 @@ import {
92
104
  sniffJsonDialect,
93
105
  SUFFIX,
94
106
  } from "./dialect.js";
107
+ import { importObographs } from "./obographs.js";
95
108
 
96
- /** The format-specific options of the JSON importer. */
97
- export interface JsonImportOptions {
109
+ /**
110
+ * The format-specific options of the JSON importer. `graphIndex` / `graphName` choose one graph of
111
+ * a JGF or OBO Graphs `graphs` array (the first by default); a graph's name is its `id`, else its
112
+ * label.
113
+ */
114
+ export interface JsonImportOptions extends GraphChoiceOptions {
98
115
  /** The dialect to read; "auto" (default) sniffs the parsed document. */
99
116
  dialect?: JsonImportDialect | "auto" | undefined;
100
117
  /**
@@ -113,8 +130,18 @@ export interface JsonImportOptions {
113
130
  * every endpoint is an integer below the node count and no node id is a number.
114
131
  */
115
132
  indexLinks?: boolean | "auto" | undefined;
116
- /** jgf: which graph of a `graphs` array to read; 0 by default. */
117
- graphIndex?: number | undefined;
133
+ /**
134
+ * obographs: "curie" (default) reads `http://purl.obolibrary.org/obo/GO_0008150` as `GO:0008150`
135
+ * and `.../obo/go#regulates` as `regulates`, the identifiers the `.obo` file of the same ontology
136
+ * writes; "iri" keeps every IRI as written.
137
+ */
138
+ oboIds?: "curie" | "iri" | undefined;
139
+ /**
140
+ * obographs: "metadata" (default) keeps PROPERTY nodes and their subPropertyOf / inverseOf edges
141
+ * in `meta.extra.obographs`, as the OBO importer keeps `[Typedef]` frames; "nodes" makes them
142
+ * nodes and edges.
143
+ */
144
+ typedefs?: "metadata" | "nodes" | undefined;
118
145
  /**
119
146
  * node-link / d3 / vis / graphology: where the node array is, as a dotted path of object keys
120
147
  * from the document root (`"data.nodes"`); the object holding it is read as the graph record
@@ -171,8 +198,18 @@ export const JSON_ISSUE = Object.freeze({
171
198
  ID_MERGED: ID_MERGED_CODE,
172
199
  /** Edge ids of mixed JSON types were stored as text. */
173
200
  EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
174
- /** A JGF `graphs` array holds more than one graph; only `graphIndex` is read. */
201
+ /** A JGF or OBO Graphs `graphs` array holds more than one graph; only the chosen one is read. */
175
202
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
203
+ /** `graphIndex` is beyond the `graphs` array, or `graphName` names none of its graphs (fatal). */
204
+ GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
205
+ /** `graphName` names more than one graph of the `graphs` array (fatal). */
206
+ AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
207
+ /** obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
208
+ OBOGRAPHS_SUBJ: "W_JSON_OBOGRAPHS_SUBJ",
209
+ /** obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under addMissingNodes false). */
210
+ DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
211
+ /** The document is longer than one JavaScript string can hold (fatal; category unsupported). */
212
+ TOO_LARGE: TOO_LARGE_CODE,
176
213
  /** JGF hyperedges under the "error" policy. */
177
214
  HYPEREDGE: HYPEREDGE_CODE,
178
215
  /** JGF hyperedges skipped under the default "skip" policy. */
@@ -258,7 +295,7 @@ const GRAPHOLOGY_EDGE_KEYS: ReadonlySet<string> = new Set(["key", "source", "tar
258
295
  const VIS_SOURCE_KEYS: readonly string[] = Object.freeze(["from"]);
259
296
  const VIS_TARGET_KEYS: readonly string[] = Object.freeze(["to"]);
260
297
 
261
- type JsonRecord = Record<string, unknown>;
298
+ export type JsonRecord = Record<string, unknown>;
262
299
 
263
300
  /** An edge id column declared from a scan of the file's edge ids. */
264
301
  interface EdgeIdColumn {
@@ -286,6 +323,12 @@ interface ResolvedJsonOptions {
286
323
  readonly targetKey: string | null;
287
324
  readonly indexLinks: boolean | "auto";
288
325
  readonly graphIndex: number;
326
+ /** The caller's graphIndex / graphName, as given, for chooseGraph(). */
327
+ readonly choice: GraphChoiceOptions;
328
+ /** obographs: CURIE or IRI ids. */
329
+ readonly oboIds: "curie" | "iri";
330
+ /** obographs: PROPERTY nodes as metadata or as nodes. */
331
+ readonly typedefs: "metadata" | "nodes";
289
332
  /** The dotted path segments of the node array, or null for the root's own nodes key. */
290
333
  readonly nodesPath: readonly string[] | null;
291
334
  /** The dotted path segments of the edge array, or null for the edges / links key beside the nodes. */
@@ -313,6 +356,14 @@ function resolveJsonOptions(options: (JsonImportOptions & CommonImportOptions) |
313
356
  if (!Number.isInteger(graphIndex) || graphIndex < 0) {
314
357
  throw unsupportedOption("graphIndex", graphIndex, ["a non-negative integer"]);
315
358
  }
359
+ const oboIds = o.oboIds ?? "curie";
360
+ if (oboIds !== "curie" && oboIds !== "iri") {
361
+ throw unsupportedOption("oboIds", oboIds, ["curie", "iri"]);
362
+ }
363
+ const typedefs = o.typedefs ?? "metadata";
364
+ if (typedefs !== "metadata" && typedefs !== "nodes") {
365
+ throw unsupportedOption("typedefs", typedefs, ["metadata", "nodes"]);
366
+ }
316
367
  const nodesPath = pathOption("nodesPath", o.nodesPath);
317
368
  const edgesPath = pathOption("edgesPath", o.edgesPath);
318
369
  if ((nodesPath !== null || edgesPath !== null) && dialect !== "auto" && !PATH_DIALECTS.has(dialect)) {
@@ -326,6 +377,9 @@ function resolveJsonOptions(options: (JsonImportOptions & CommonImportOptions) |
326
377
  targetKey: keyOption("targetKey", o.targetKey),
327
378
  indexLinks,
328
379
  graphIndex,
380
+ choice: { graphIndex: o.graphIndex, graphName: o.graphName },
381
+ oboIds,
382
+ typedefs,
329
383
  nodesPath,
330
384
  edgesPath,
331
385
  };
@@ -666,7 +720,7 @@ class AttributeWriter {
666
720
  /**
667
721
  * Everything one import call shares between the dialect readers.
668
722
  */
669
- class ImportContext {
723
+ export class ImportContext {
670
724
  readonly sink: GraphSink;
671
725
 
672
726
  readonly report: ImportReportBuilder;
@@ -2339,38 +2393,140 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
2339
2393
  }
2340
2394
 
2341
2395
  /**
2342
- * The graph object of a JGF document: `graph`, or `graphs[graphIndex]`.
2396
+ * The graph object of a JGF document: `graph`, or the graph of `graphs` that graphIndex /
2397
+ * graphName choose (chooseGraph()).
2343
2398
  * @param ctx - the context
2344
2399
  * @param root - the document
2345
2400
  * @returns the graph object; the import fails when there is none
2346
2401
  */
2347
2402
  function jgfGraphOf(ctx: ImportContext, root: JsonRecord): JsonRecord {
2403
+ return isJsonObject(root.graph) ? root.graph : chosenGraph(ctx, root, "JGF");
2404
+ }
2405
+
2406
+ /**
2407
+ * The graph of a `graphs` array (JGF, OBO Graphs) that graphIndex / graphName choose, with
2408
+ * W_MULTIPLE_GRAPHS when the others are skipped; importAll() reads graphs[graphIndex].
2409
+ * @param ctx - the context
2410
+ * @param root - the document
2411
+ * @param what - the dialect's name, for the messages
2412
+ * @returns the graph object; the import fails when there is none
2413
+ */
2414
+ export function chosenGraph(ctx: ImportContext, root: JsonRecord, what: string): JsonRecord {
2348
2415
  const { report } = ctx;
2349
- if (isJsonObject(root.graph)) {
2350
- return root.graph;
2351
- }
2352
2416
  const graphs = arraySection(root.graphs, "graphs", report) ?? [];
2353
2417
  if (graphs.length === 0) {
2354
- report.fail(JSON_ISSUE.SHAPE, "a JGF document needs a graph object or a non-empty graphs array");
2418
+ report.fail(JSON_ISSUE.SHAPE, `a ${what} document needs a graph object or a non-empty graphs array`);
2355
2419
  }
2420
+ const index =
2421
+ ctx.json.all === true ? ctx.json.graphIndex : chooseGraph(graphs.map(graphNameOf), ctx.json.choice, report);
2356
2422
  if (graphs.length > 1 && ctx.json.all !== true) {
2357
2423
  report.warning(
2358
2424
  "unsupported",
2359
2425
  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`,
2426
+ `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped), importAll() reads every one`,
2361
2427
  { element: "graphs" },
2362
2428
  );
2363
2429
  }
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];
2430
+ const graph = graphs[index];
2368
2431
  if (!isJsonObject(graph)) {
2369
- return report.fail(JSON_ISSUE.SHAPE, `graphs[${ctx.json.graphIndex}] is not an object`);
2432
+ return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
2370
2433
  }
2371
2434
  return graph;
2372
2435
  }
2373
2436
 
2437
+ /**
2438
+ * The name a graph of a `graphs` array is listed and chosen by: its `id`, else its `label` (JGF)
2439
+ * or `lbl` (OBO Graphs).
2440
+ * @param graph - the graph
2441
+ * @returns the name, or null
2442
+ */
2443
+ function graphNameOf(graph: unknown): string | null {
2444
+ if (!isJsonObject(graph)) {
2445
+ return null;
2446
+ }
2447
+ for (const key of ["id", "label", "lbl"]) {
2448
+ if (typeof graph[key] === "string") {
2449
+ return graph[key];
2450
+ }
2451
+ }
2452
+ return null;
2453
+ }
2454
+
2455
+ /**
2456
+ * How many elements a nodes or edges section holds: an array's length, an object's key count, 0
2457
+ * when absent, null for anything else.
2458
+ * @param section - the section
2459
+ * @returns the count, or null
2460
+ */
2461
+ function countOf(section: unknown): number | null {
2462
+ if (section === undefined || section === null) {
2463
+ return 0;
2464
+ }
2465
+ if (Array.isArray(section)) {
2466
+ return section.length;
2467
+ }
2468
+ return isJsonObject(section) ? Object.keys(section).length : null;
2469
+ }
2470
+
2471
+ /**
2472
+ * The graphs of a parsed document, for listGraphs(): each entry of a JGF or OBO Graphs `graphs`
2473
+ * array with its name and counts; any other document holds one graph.
2474
+ * @param root - the parsed document
2475
+ * @param dialect - its dialect
2476
+ * @returns the listings
2477
+ */
2478
+ function listingsOf(root: unknown, dialect: JsonImportDialect): GraphListing[] {
2479
+ if ((dialect === "jgf" || dialect === "obographs") && isJsonObject(root)) {
2480
+ if (Array.isArray(root.graphs) && !isJsonObject(root.graph)) {
2481
+ return root.graphs.map((graph: unknown, index) => ({
2482
+ index,
2483
+ name: graphNameOf(graph),
2484
+ nodes: isJsonObject(graph) ? countOf(graph.nodes) : null,
2485
+ edges: isJsonObject(graph) ? countOf(graph.edges) : null,
2486
+ }));
2487
+ }
2488
+ if (isJsonObject(root.graph)) {
2489
+ const { graph } = root;
2490
+ return [{ index: 0, name: graphNameOf(graph), nodes: countOf(graph.nodes), edges: countOf(graph.edges) }];
2491
+ }
2492
+ }
2493
+ return [{ index: 0, name: null, nodes: null, edges: null }];
2494
+ }
2495
+
2496
+ /** The longest string V8 makes (2^29 - 24 UTF-16 code units); a longer document cannot be one JSON.parse input. */
2497
+ const MAX_TEXT_LENGTH = 2 ** 29 - 24;
2498
+
2499
+ /**
2500
+ * Read the whole input as one string, failing with E_TOO_LARGE (category unsupported) before the
2501
+ * join when it is longer than one JavaScript string can hold (OBO Graphs files such as
2502
+ * ncbitaxon.json are; design 7.1 defers streaming the JSON reader).
2503
+ * @param input - the input
2504
+ * @param report - the report
2505
+ * @param options - cancellation, progress and encoding
2506
+ * @returns the text
2507
+ */
2508
+ async function readJsonText(
2509
+ input: ImportInput,
2510
+ report: ImportReportBuilder,
2511
+ options: ResolvedImportOptions,
2512
+ ): Promise<string> {
2513
+ if (typeof input === "string") {
2514
+ return readText(input, report, options);
2515
+ }
2516
+ const parts: string[] = [];
2517
+ let length = 0;
2518
+ for await (const chunk of textChunks(input, report, options)) {
2519
+ length += chunk.length;
2520
+ if (length > MAX_TEXT_LENGTH) {
2521
+ const message = `the document is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`;
2522
+ report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
2523
+ throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
2524
+ }
2525
+ parts.push(chunk);
2526
+ }
2527
+ return parts.length === 1 ? parts[0] : parts.join("");
2528
+ }
2529
+
2374
2530
  /**
2375
2531
  * Push a JGF node: `label` into the declared column, `metadata` as attributes.
2376
2532
  * @param ctx - the context
@@ -2841,12 +2997,32 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2841
2997
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2842
2998
  const json = resolveJsonOptions(options);
2843
2999
  const report = new ImportReportBuilder("json", resolved.errorLimit);
2844
- const text = await readText(input, report, resolved);
3000
+ const text = await readJsonText(input, report, resolved);
2845
3001
  const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2846
3002
  readGraph(root, dialect, sink, report, resolved, json, options);
2847
3003
  return report.finish();
2848
3004
  },
2849
3005
 
3006
+ /**
3007
+ * List the graphs of a JSON document without importing them: each entry of a JGF or OBO
3008
+ * Graphs `graphs` array with its name (`id`, else its label) and node and edge counts; any
3009
+ * other document holds one graph.
3010
+ * @param input - the text, bytes or stream
3011
+ * @param options - format-specific and common options
3012
+ * @returns one listing per graph
3013
+ */
3014
+ async listGraphs(
3015
+ input: ImportInput,
3016
+ options?: JsonImportOptions & CommonImportOptions,
3017
+ ): Promise<readonly GraphListing[]> {
3018
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
3019
+ const json = resolveJsonOptions(options);
3020
+ const report = new ImportReportBuilder("json", resolved.errorLimit);
3021
+ const text = await readJsonText(input, report, resolved);
3022
+ const { root, dialect } = documentOf(parseDocument(text, report), json, report);
3023
+ return listingsOf(root, dialect);
3024
+ },
3025
+
2850
3026
  /**
2851
3027
  * Read every graph of a JSON document: each entry of a JGF `graphs` array into its own sink;
2852
3028
  * any other document holds one graph.
@@ -2863,12 +3039,9 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2863
3039
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2864
3040
  const json = resolveJsonOptions(options);
2865
3041
  const first = new ImportReportBuilder("json", resolved.errorLimit);
2866
- const text = await readText(input, first, resolved);
3042
+ const text = await readJsonText(input, first, resolved);
2867
3043
  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;
3044
+ const graphs = listingsOf(root, dialect).length;
2872
3045
  const reports: ImportReport[] = [];
2873
3046
  for (let i = 0; i < Math.max(graphs, 1); i++) {
2874
3047
  const report = i === 0 ? first : new ImportReportBuilder("json", resolved.errorLimit);
@@ -2899,7 +3072,8 @@ function readGraph(
2899
3072
  options: (JsonImportOptions & CommonImportOptions) | undefined,
2900
3073
  ): void {
2901
3074
  const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== undefined);
2902
- reportSinkOptions(sink, options, report);
3075
+ // the obographs reader refuses missing endpoints itself, so addMissingNodes false holds on any sink
3076
+ reportSinkOptions(sink, options, report, dialect === "obographs");
2903
3077
  reportUnusedOptions(options, report, USED_OPTIONS);
2904
3078
  if (dialect === "cytoscape") {
2905
3079
  importCytoscape(ctx, root);
@@ -2929,6 +3103,9 @@ function readGraph(
2929
3103
  case "tree":
2930
3104
  importTree(ctx, doc);
2931
3105
  break;
3106
+ case "obographs":
3107
+ importObographs(ctx, doc);
3108
+ break;
2932
3109
  default: {
2933
3110
  const name: string = dialect;
2934
3111
  throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {