@graphty/graph-io 0.3.16 → 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 (111) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -3
  3. package/dist/chunks/{escape-D1f9-cwf.js → escape-D-gZWO26.js} +3 -2
  4. package/dist/chunks/{escape-D1f9-cwf.js.map → escape-D-gZWO26.js.map} +1 -1
  5. package/dist/chunks/{importer-D7ZcGCeb.js → importer-Br_QeAeE.js} +4 -3
  6. package/dist/chunks/{importer-D7ZcGCeb.js.map → importer-Br_QeAeE.js.map} +1 -1
  7. package/dist/chunks/{importer-CXEiicAN.js → importer-DHagxvDD.js} +4 -3
  8. package/dist/chunks/{importer-CXEiicAN.js.map → importer-DHagxvDD.js.map} +1 -1
  9. package/dist/chunks/{importer-C7mnGdr_.js → importer-Du5crN9l.js} +510 -26
  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-B8lsjFWx.js → importer-d0uQxFp6.js} +4 -3
  14. package/dist/chunks/{importer-B8lsjFWx.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-IHsCfv7s.js → records-Bk9jgodz.js} +2 -2
  18. package/dist/chunks/{records-IHsCfv7s.js.map → records-Bk9jgodz.js.map} +1 -1
  19. package/dist/chunks/{writer-BtWpUaiH.js → report-BOk0p5y8.js} +181 -912
  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 +191 -134
  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/codes.d.ts +43 -0
  41. package/dist/src/common/codes.d.ts.map +1 -1
  42. package/dist/src/common/codes.js +43 -0
  43. package/dist/src/common/codes.js.map +1 -1
  44. package/dist/src/common/input.d.ts +9 -0
  45. package/dist/src/common/input.d.ts.map +1 -1
  46. package/dist/src/common/input.js +16 -0
  47. package/dist/src/common/input.js.map +1 -1
  48. package/dist/src/common/ontology.d.ts +59 -0
  49. package/dist/src/common/ontology.d.ts.map +1 -0
  50. package/dist/src/common/ontology.js +147 -0
  51. package/dist/src/common/ontology.js.map +1 -0
  52. package/dist/src/common/options.d.ts +12 -1
  53. package/dist/src/common/options.d.ts.map +1 -1
  54. package/dist/src/common/options.js +51 -1
  55. package/dist/src/common/options.js.map +1 -1
  56. package/dist/src/formats/json/dialect.d.ts +8 -6
  57. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  58. package/dist/src/formats/json/dialect.js +26 -3
  59. package/dist/src/formats/json/dialect.js.map +1 -1
  60. package/dist/src/formats/json/importer.d.ts +324 -7
  61. package/dist/src/formats/json/importer.d.ts.map +1 -1
  62. package/dist/src/formats/json/importer.js +154 -23
  63. package/dist/src/formats/json/importer.js.map +1 -1
  64. package/dist/src/formats/json/obographs.d.ts +21 -0
  65. package/dist/src/formats/json/obographs.d.ts.map +1 -0
  66. package/dist/src/formats/json/obographs.js +476 -0
  67. package/dist/src/formats/json/obographs.js.map +1 -0
  68. package/dist/src/formats/obo/importer.d.ts +90 -0
  69. package/dist/src/formats/obo/importer.d.ts.map +1 -0
  70. package/dist/src/formats/obo/importer.js +1248 -0
  71. package/dist/src/formats/obo/importer.js.map +1 -0
  72. package/dist/src/formats/obo/index.d.ts +7 -0
  73. package/dist/src/formats/obo/index.d.ts.map +1 -0
  74. package/dist/src/formats/obo/index.js +7 -0
  75. package/dist/src/formats/obo/index.js.map +1 -0
  76. package/dist/src/formats/obo/syntax.d.ts +121 -0
  77. package/dist/src/formats/obo/syntax.d.ts.map +1 -0
  78. package/dist/src/formats/obo/syntax.js +424 -0
  79. package/dist/src/formats/obo/syntax.js.map +1 -0
  80. package/dist/src/index.d.ts +5 -4
  81. package/dist/src/index.d.ts.map +1 -1
  82. package/dist/src/index.js +4 -3
  83. package/dist/src/index.js.map +1 -1
  84. package/dist/src/registry.d.ts +21 -3
  85. package/dist/src/registry.d.ts.map +1 -1
  86. package/dist/src/registry.js +32 -1
  87. package/dist/src/registry.js.map +1 -1
  88. package/dist/src/sniff.d.ts +1 -1
  89. package/dist/src/sniff.d.ts.map +1 -1
  90. package/dist/src/sniff.js +23 -3
  91. package/dist/src/sniff.js.map +1 -1
  92. package/dist/src/types.d.ts +31 -0
  93. package/dist/src/types.d.ts.map +1 -1
  94. package/dist/src/types.js.map +1 -1
  95. package/package.json +6 -1
  96. package/src/common/codes.ts +58 -0
  97. package/src/common/input.ts +16 -0
  98. package/src/common/ontology.ts +169 -0
  99. package/src/common/options.ts +78 -2
  100. package/src/formats/json/dialect.ts +37 -7
  101. package/src/formats/json/importer.ts +206 -28
  102. package/src/formats/json/obographs.ts +563 -0
  103. package/src/formats/obo/importer.ts +1695 -0
  104. package/src/formats/obo/index.ts +7 -0
  105. package/src/formats/obo/syntax.ts +466 -0
  106. package/src/index.ts +6 -0
  107. package/src/registry.ts +38 -3
  108. package/src/sniff.ts +35 -5
  109. package/src/types.ts +40 -1
  110. package/dist/chunks/importer-C7mnGdr_.js.map +0 -1
  111. package/dist/chunks/writer-BtWpUaiH.js.map +0 -1
@@ -108,6 +108,64 @@ export const BAD_OPTIONS_CODE = "W_BAD_OPTIONS";
108
108
  /** A repeated edge id in a format whose edge ids are unique; the second edge is skipped. */
109
109
  export const DUPLICATE_EDGE_ID_CODE = "E_DUPLICATE_EDGE_ID";
110
110
 
111
+ /** A value does not parse as its declared type; the cell is left unset. */
112
+ export const BAD_VALUE_CODE = "E_BAD_VALUE";
113
+
114
+ /**
115
+ * A column's dtype was widened (design section 5.1) because a later value did not fit: an i32
116
+ * column meeting a value above 2^31, two declared types for one attribute.
117
+ */
118
+ export const WIDENED_CODE = "W_WIDENED";
119
+
120
+ /**
121
+ * A reference that is not a containment link (an attribute's element id, a layout or bypass
122
+ * entry, a subnetwork member, a view's network, an undeclared target) names nothing; reported
123
+ * once per kind with the count.
124
+ */
125
+ export const DANGLING_REFERENCE_CODE = "W_DANGLING_REFERENCE";
126
+
127
+ /** The same attribute twice on one element; one value is kept (the later, unless the format's specification says the first). */
128
+ export const DUPLICATE_ATTRIBUTE_CODE = "W_DUPLICATE_ATTRIBUTE";
129
+
130
+ /** A containment link would close a parent cycle; that one link is dropped. */
131
+ export const PARENT_CYCLE_CODE = "E_PARENT_CYCLE";
132
+
133
+ /** A formula (Cytoscape's `=ABS($x)`) is kept as its text; it is never evaluated. */
134
+ export const EQUATION_AS_TEXT_CODE = "W_EQUATION_AS_TEXT";
135
+
136
+ /**
137
+ * The file's style rules (defaults, mappings, dependencies, visual property aspects) are not
138
+ * applied to the snapshot; recorded once per import, the message names what was not applied.
139
+ */
140
+ export const STYLES_NOT_IMPORTED_CODE = "W_STYLES_NOT_IMPORTED";
141
+
142
+ /** A CX array member that is not a one-key object holding an array or an object; the block is skipped. */
143
+ export const BAD_ASPECT_BLOCK_CODE = "E_BAD_ASPECT_BLOCK";
144
+
145
+ /** A CX aspect out of its place (after the post-metadata, a status that is not last, a block buffered for what it depends on). */
146
+ export const ASPECT_ORDER_CODE = "W_ASPECT_ORDER";
147
+
148
+ /** A declared element count disagrees with what was read. */
149
+ export const COUNT_MISMATCH_CODE = "W_COUNT_MISMATCH";
150
+
151
+ /** The producer marked the document as failed (CX `status.success: false`): it is incomplete (fatal). */
152
+ export const STATUS_FAILED_CODE = "E_STATUS_FAILED";
153
+
154
+ /** The producer marked the document as successful but attached an error text. */
155
+ export const STATUS_WARNING_CODE = "W_STATUS_WARNING";
156
+
157
+ /**
158
+ * The input is beyond a size limit (a zip's uncompressed total or ratio, a document longer than
159
+ * one string); the same string as graph-format's E_TOO_LARGE, recorded with category "unsupported".
160
+ */
161
+ export const TOO_LARGE_CODE = "E_TOO_LARGE";
162
+
163
+ /** `graphIndex` is beyond the graphs of the input, or `graphName` names none of them (fatal). */
164
+ export const GRAPH_NOT_FOUND_CODE = "E_GRAPH_NOT_FOUND";
165
+
166
+ /** `graphName` names more than one graph of the input; the message lists their indexes (fatal). */
167
+ export const AMBIGUOUS_GRAPH_NAME_CODE = "E_AMBIGUOUS_GRAPH_NAME";
168
+
111
169
  // ============================================================ exporter loss notes
112
170
 
113
171
  /** A role column the format has no slot for is written as a plain attribute (the role is lost). */
@@ -262,6 +262,22 @@ function startsWithUtf8(bytes: Uint8Array): boolean {
262
262
  }
263
263
  }
264
264
 
265
+ /**
266
+ * A zip entry name as text (APPNOTE 4.4.17): UTF-8 when the bytes are valid UTF-8, whether or not
267
+ * the entry sets the language-encoding flag (bit 11), else windows-1252 (WHATWG has no CP437, and
268
+ * session entry names are ASCII in practice). Never throws: a name is a matching key, never a path,
269
+ * so an undecodable one still has to name its entry.
270
+ * @param bytes - the name bytes from the central directory or a local header
271
+ * @returns the name
272
+ */
273
+ export function decodeEntryName(bytes: Uint8Array): string {
274
+ try {
275
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
276
+ } catch {
277
+ return new TextDecoder("windows-1252").decode(bytes);
278
+ }
279
+ }
280
+
265
281
  /**
266
282
  * Join byte chunks.
267
283
  * @param chunks - the chunks
@@ -0,0 +1,169 @@
1
+ /**
2
+ * What the OBO importer and the JSON importer's `obographs` dialect share (design section 1.0, the
3
+ * `ontology.ts` building block): the OBO column vocabulary (names, dtypes, roles), so the `.obo` and
4
+ * the `.json` of one ontology give the same columns, and the OBO 1.4 rule for turning the IRIs
5
+ * OBO Graphs writes back into the identifiers the `.obo` file writes (section 4.6).
6
+ */
7
+
8
+ import { type ColumnDecl } from "@graphty/graph-format";
9
+
10
+ /** The dtypes the OBO vocabulary uses. */
11
+ type OboDtype = "string" | "dict" | "bool" | "list" | "json";
12
+
13
+ /** One column of the OBO vocabulary. */
14
+ interface OboColumnSpec {
15
+ /** The dtype. */
16
+ readonly dtype: OboDtype;
17
+ /** The role, when the column carries one. */
18
+ readonly role?: "label" | "kind" | undefined;
19
+ }
20
+
21
+ /**
22
+ * The node columns of the OBO vocabulary, keyed by column name (design section 4.2). The names are
23
+ * the OBO tag names, so a Gene Ontology user finds `namespace`, `def` and `is_obsolete` under the
24
+ * names the GO documentation uses.
25
+ */
26
+ export const OBO_NODE_COLUMNS: Readonly<Record<string, OboColumnSpec>> = Object.freeze({
27
+ type: { dtype: "dict" },
28
+ name: { dtype: "string", role: "label" },
29
+ namespace: { dtype: "dict" },
30
+ def: { dtype: "string" },
31
+ "def.xrefs": { dtype: "list" },
32
+ comment: { dtype: "string" },
33
+ synonym: { dtype: "json" },
34
+ xref: { dtype: "list" },
35
+ "xref.descriptions": { dtype: "json" },
36
+ alt_id: { dtype: "list" },
37
+ subset: { dtype: "list" },
38
+ replaced_by: { dtype: "list" },
39
+ consider: { dtype: "list" },
40
+ is_obsolete: { dtype: "bool" },
41
+ is_anonymous: { dtype: "bool" },
42
+ builtin: { dtype: "bool" },
43
+ created_by: { dtype: "string" },
44
+ creation_date: { dtype: "string" },
45
+ intersection_of: { dtype: "json" },
46
+ union_of: { dtype: "list" },
47
+ equivalent_to: { dtype: "list" },
48
+ disjoint_from: { dtype: "list" },
49
+ property_value: { dtype: "json" },
50
+ // Typedef frames, under typedefs: "nodes"
51
+ domain: { dtype: "string" },
52
+ range: { dtype: "string" },
53
+ inverse_of: { dtype: "string" },
54
+ transitive_over: { dtype: "list" },
55
+ disjoint_over: { dtype: "list" },
56
+ holds_over_chain: { dtype: "json" },
57
+ equivalent_to_chain: { dtype: "json" },
58
+ expand_assertion_to: { dtype: "json" },
59
+ expand_expression_to: { dtype: "json" },
60
+ is_cyclic: { dtype: "bool" },
61
+ is_reflexive: { dtype: "bool" },
62
+ is_symmetric: { dtype: "bool" },
63
+ is_anti_symmetric: { dtype: "bool" },
64
+ is_asymmetric: { dtype: "bool" },
65
+ is_transitive: { dtype: "bool" },
66
+ is_functional: { dtype: "bool" },
67
+ is_inverse_functional: { dtype: "bool" },
68
+ is_metadata_tag: { dtype: "bool" },
69
+ is_class_level: { dtype: "bool" },
70
+ // OBO Graphs only
71
+ propertyType: { dtype: "dict" },
72
+ // what has no column of its own
73
+ "obo.qualifiers": { dtype: "json" },
74
+ "obo.unrecognized": { dtype: "json" },
75
+ });
76
+
77
+ /** The edge columns of the OBO vocabulary. */
78
+ const OBO_EDGE_COLUMNS: Readonly<Record<string, OboColumnSpec>> = Object.freeze({
79
+ relation: { dtype: "dict", role: "kind" },
80
+ qualifiers: { dtype: "json" },
81
+ meta: { dtype: "json" },
82
+ });
83
+
84
+ /** The node column that marks a node made for an undeclared reference (design section 4.2). */
85
+ export const PLACEHOLDER_COLUMN = "graphty.placeholder";
86
+
87
+ /**
88
+ * The declaration of an OBO vocabulary column.
89
+ * @param domain - node or edge
90
+ * @param name - the column name (a key of OBO_NODE_COLUMNS / OBO_EDGE_COLUMNS, or the placeholder column)
91
+ * @returns the declaration, nullable, with origin `{ format: "obo", id: name }`
92
+ */
93
+ export function oboColumnDecl(domain: "node" | "edge", name: string): ColumnDecl {
94
+ if (name === PLACEHOLDER_COLUMN) {
95
+ return { name, dtype: "bool", nullable: true, origin: { format: "graphty", id: "placeholder" } };
96
+ }
97
+ const spec = (domain === "node" ? OBO_NODE_COLUMNS : OBO_EDGE_COLUMNS)[name];
98
+ const decl: ColumnDecl = { name, dtype: spec.dtype, nullable: true, origin: { format: "obo", id: name } };
99
+ if (spec.dtype === "list") {
100
+ decl.itemDtype = "string";
101
+ }
102
+ if (spec.role !== undefined) {
103
+ decl.role = spec.role;
104
+ }
105
+ return decl;
106
+ }
107
+
108
+ /** The OBO PURL base every OBO Foundry IRI starts with. */
109
+ const OBO_PURL = "http://purl.obolibrary.org/obo/";
110
+
111
+ /** The oboInOwl namespace of the OBO-to-OWL mapping's annotation properties. */
112
+ const OBO_IN_OWL = "http://www.geneontology.org/formats/oboInOwl#";
113
+
114
+ /**
115
+ * An IRI as the identifier the `.obo` file writes (the OBO 1.4 mapping, section 5.9, read
116
+ * backwards): `http://purl.obolibrary.org/obo/GO_0008150` is `GO:0008150` (the prefix is the text
117
+ * before the first underscore), and `http://purl.obolibrary.org/obo/go#regulates` (how the OWL
118
+ * translation writes an unprefixed OBO id: subsets, relations, synonym types) is `regulates`.
119
+ * Every other IRI, and an OBO PURL that fits neither form (`.../obo/T/Female`), is kept.
120
+ * @param iri - the IRI
121
+ * @returns the CURIE or local id, or the IRI unchanged
122
+ */
123
+ export function compactOboIri(iri: string): string {
124
+ if (!iri.startsWith(OBO_PURL)) {
125
+ return iri;
126
+ }
127
+ const rest = iri.slice(OBO_PURL.length);
128
+ const hash = rest.indexOf("#");
129
+ if (hash > 0) {
130
+ const local = rest.slice(hash + 1);
131
+ return local.length > 0 && !rest.slice(0, hash).includes("/") ? local : iri;
132
+ }
133
+ const underscore = rest.indexOf("_");
134
+ if (underscore <= 0 || rest.includes("/") || underscore === rest.length - 1) {
135
+ return iri;
136
+ }
137
+ return `${rest.slice(0, underscore)}:${rest.slice(underscore + 1)}`;
138
+ }
139
+
140
+ /** The OBO synonym scopes. */
141
+ export const SYNONYM_SCOPES: ReadonlySet<string> = new Set(["EXACT", "BROAD", "NARROW", "RELATED"]);
142
+
143
+ /**
144
+ * The OBO synonym scope of an OBO Graphs synonym predicate (`hasExactSynonym`, with or without the
145
+ * oboInOwl namespace).
146
+ * @param pred - the predicate
147
+ * @returns the scope, or null for a predicate that is not one of the four
148
+ */
149
+ export function synonymScopeOf(pred: string): string | null {
150
+ const local = pred.startsWith(OBO_IN_OWL) ? pred.slice(OBO_IN_OWL.length) : pred;
151
+ const match = /^has(Exact|Broad|Narrow|Related)Synonym$/.exec(local);
152
+ return match === null ? null : match[1].toUpperCase();
153
+ }
154
+
155
+ /**
156
+ * The OBO tag a `basicPropertyValues` predicate of OBO Graphs came from (the OBO-to-OWL mapping of
157
+ * the tags that are annotations), so the `.json` of an ontology fills the same columns as its
158
+ * `.obo`. `shorthand` is the relation's short name; every predicate not listed is a
159
+ * `property_value`.
160
+ */
161
+ export const OBOGRAPHS_PREDICATE_TAGS: ReadonlyMap<string, string> = new Map([
162
+ [`${OBO_IN_OWL}hasOBONamespace`, "namespace"],
163
+ [`${OBO_IN_OWL}hasAlternativeId`, "alt_id"],
164
+ [`${OBO_IN_OWL}created_by`, "created_by"],
165
+ [`${OBO_IN_OWL}creation_date`, "creation_date"],
166
+ [`${OBO_PURL}IAO_0100001`, "replaced_by"],
167
+ [`${OBO_IN_OWL}consider`, "consider"],
168
+ [`${OBO_IN_OWL}shorthand`, "shorthand"],
169
+ ]);
@@ -7,8 +7,14 @@
7
7
 
8
8
  import { type DuplicatePolicy, GraphFormatError, type GraphSink, type IdCoercion } from "@graphty/graph-format";
9
9
 
10
- import { type CommonExportOptions, type CommonImportOptions } from "../types.js";
11
- import { OPTION_IGNORED_CODE, SINK_OPTION_CODE } from "./codes.js";
10
+ import { type CommonExportOptions, type CommonImportOptions, type GraphChoiceOptions } from "../types.js";
11
+ import {
12
+ AMBIGUOUS_GRAPH_NAME_CODE,
13
+ GRAPH_NOT_FOUND_CODE,
14
+ NO_GRAPH_CODE,
15
+ OPTION_IGNORED_CODE,
16
+ SINK_OPTION_CODE,
17
+ } from "./codes.js";
12
18
  import { canonicalEncoding } from "./input.js";
13
19
  import { type ImportReportBuilder } from "./report.js";
14
20
 
@@ -188,6 +194,76 @@ export function reportUnusedOptions(
188
194
  return recorded;
189
195
  }
190
196
 
197
+ /**
198
+ * The graph an importer reads from an input that holds several: the one `graphIndex` or
199
+ * `graphName` names, else the first. A choice that names no graph is E_GRAPH_NOT_FOUND, a name two
200
+ * graphs share is E_AMBIGUOUS_GRAPH_NAME, and an input with no graph at all is E_NO_GRAPH, each
201
+ * fatal (the report fails with ImportError).
202
+ * @param names - each graph's name (null for an unnamed graph), in document order
203
+ * @param options - the caller's graphIndex / graphName
204
+ * @param report - the report a failure is recorded in
205
+ * @returns the index of the graph to read; E_UNSUPPORTED for an option of the wrong type, or both
206
+ */
207
+ export function chooseGraph(
208
+ names: readonly (string | null)[],
209
+ options: GraphChoiceOptions | undefined,
210
+ report: ImportReportBuilder,
211
+ ): number {
212
+ const { graphIndex, graphName } = options ?? {};
213
+ if (
214
+ graphIndex !== undefined &&
215
+ (typeof graphIndex !== "number" || !Number.isInteger(graphIndex) || graphIndex < 0)
216
+ ) {
217
+ throw new GraphFormatError(
218
+ "E_UNSUPPORTED",
219
+ `option graphIndex: ${describe(graphIndex)} is not a non-negative integer`,
220
+ {
221
+ option: "graphIndex",
222
+ found: graphIndex,
223
+ },
224
+ );
225
+ }
226
+ if (graphName !== undefined && typeof graphName !== "string") {
227
+ throw new GraphFormatError("E_UNSUPPORTED", `option graphName: ${describe(graphName)} is not a string`, {
228
+ option: "graphName",
229
+ found: graphName,
230
+ });
231
+ }
232
+ if (graphIndex !== undefined && graphName !== undefined) {
233
+ throw new GraphFormatError("E_UNSUPPORTED", "options graphIndex and graphName both choose a graph; pass one", {
234
+ option: "graphName",
235
+ found: graphName,
236
+ });
237
+ }
238
+ if (names.length === 0) {
239
+ return report.fail(NO_GRAPH_CODE, "the input holds no graph");
240
+ }
241
+ if (graphName !== undefined) {
242
+ const matches = names.flatMap((name, i) => (name === graphName ? [i] : []));
243
+ if (matches.length > 1) {
244
+ return report.fail(
245
+ AMBIGUOUS_GRAPH_NAME_CODE,
246
+ `graphName ${JSON.stringify(graphName)} names the graphs at indexes ${matches.join(", ")}; pass graphIndex`,
247
+ { element: graphName },
248
+ );
249
+ }
250
+ if (matches.length === 0) {
251
+ return report.fail(
252
+ GRAPH_NOT_FOUND_CODE,
253
+ `graphName ${JSON.stringify(graphName)} names none of the ${names.length} graph(s)`,
254
+ { element: graphName },
255
+ { names: [...names] },
256
+ );
257
+ }
258
+ return matches[0];
259
+ }
260
+ const index = graphIndex ?? 0;
261
+ if (index >= names.length) {
262
+ return report.fail(GRAPH_NOT_FOUND_CODE, `graphIndex ${index} is beyond the ${names.length} graph(s)`);
263
+ }
264
+ return index;
265
+ }
266
+
191
267
  /**
192
268
  * Apply the design section 8.4 defaults to an importer's common options and check every enum
193
269
  * value. Format-specific options in the same object are ignored here.
@@ -32,17 +32,19 @@ export const JSON_DIALECTS: readonly JsonDialect[] = Object.freeze([
32
32
  ]);
33
33
 
34
34
  /**
35
- * The dialects the importer reads: every JsonDialect plus two it only reads, NetworkX
36
- * adjacency_data (`nodes` plus one neighbour list per node under `adjacency`) and tree_data (a
37
- * nested `id` / `children` record). The exporter writes neither; write node-link instead.
35
+ * The dialects the importer reads: every JsonDialect plus three it only reads, NetworkX
36
+ * adjacency_data (`nodes` plus one neighbour list per node under `adjacency`), tree_data (a
37
+ * nested `id` / `children` record) and OBO Graphs (`graphs[]` of `sub` / `pred` / `obj` edges, the
38
+ * JSON form of the Gene Ontology and the OBO Foundry ontologies). The exporter writes none of them.
38
39
  */
39
- export type JsonImportDialect = JsonDialect | "adjacency" | "tree";
40
+ export type JsonImportDialect = JsonDialect | "adjacency" | "tree" | "obographs";
40
41
 
41
42
  /** Every dialect the importer reads, for option checking and messages. */
42
43
  export const JSON_IMPORT_DIALECTS: readonly JsonImportDialect[] = Object.freeze([
43
44
  ...JSON_DIALECTS,
44
45
  "adjacency",
45
46
  "tree",
47
+ "obographs",
46
48
  ]);
47
49
 
48
50
  /** The key under `meta.extra` that holds the shape record (design section 8.5). */
@@ -116,8 +118,9 @@ export function isJsonImportDialect(value: unknown): value is JsonImportDialect
116
118
 
117
119
  /**
118
120
  * The dialect of a parsed JSON document, by the shape rules of design section 8.2 (Cytoscape:
119
- * `elements` or a top-level array of `{ data }` elements; JGF: `graph.nodes` / `graph.edges` or
120
- * `graphs[]`; graphology: `options.type` / `options.multi`, `key` nodes without `id`, edges with
121
+ * `elements` or a top-level array of `{ data }` elements; OBO Graphs: `graphs[]` whose first graph
122
+ * has an edge with `sub` (or `subj`) and `obj` or a `pred`, or a node with `lbl`, `meta` or an OWL
123
+ * `type`; JGF: `graph.nodes` / `graph.edges` or any other `graphs[]`; graphology: `options.type` / `options.multi`, `key` nodes without `id`, edges with
121
124
  * `undirected` or an `attributes` record; vis: edges with `from` / `to`; d3: `links` without
122
125
  * `directed` / `multigraph` / `graph`; NetworkX adjacency_data: `nodes` and `adjacency` without
123
126
  * `links` / `edges`; NetworkX tree_data: `children` without `nodes` / `links` / `edges`; else
@@ -141,7 +144,7 @@ export function sniffJsonDialect(root: unknown): JsonImportDialect | null {
141
144
  return "jgf";
142
145
  }
143
146
  if (Array.isArray(root.graphs)) {
144
- return "jgf";
147
+ return isOboGraph(firstJsonObject(root.graphs)) ? "obographs" : "jgf";
145
148
  }
146
149
  if (!hasKey(root, "edges") && !hasKey(root, "links")) {
147
150
  if (hasKey(root, "nodes") && hasKey(root, "adjacency")) {
@@ -171,6 +174,31 @@ export function sniffJsonDialect(root: unknown): JsonImportDialect | null {
171
174
  return bare && hasKey(root, "links") ? "d3" : "node-link";
172
175
  }
173
176
 
177
+ /** The node types of OBO Graphs (the OWL entity kinds). */
178
+ const OBOGRAPHS_NODE_TYPES: ReadonlySet<unknown> = new Set(["CLASS", "INDIVIDUAL", "PROPERTY"]);
179
+
180
+ /**
181
+ * Whether a graph of a `graphs[]` document is an OBO Graphs graph rather than JGF: anywhere in its
182
+ * nodes or edges, an edge with `sub` (or the outdated `subj`) and `obj`, or a `pred`, or a node
183
+ * with `lbl`, `meta` or a `type` of CLASS / INDIVIDUAL / PROPERTY (design 1.6: looking only at the
184
+ * first node and edge misses graphs with no edges and graphs whose first node has no `lbl`).
185
+ * @param graph - the first graph, or null
186
+ * @returns true for OBO Graphs
187
+ */
188
+ function isOboGraph(graph: Record<string, unknown> | null): boolean {
189
+ if (graph === null) {
190
+ return false;
191
+ }
192
+ const isEdge = (e: unknown): boolean =>
193
+ isJsonObject(e) && ((hasKey(e, "obj") && (hasKey(e, "sub") || hasKey(e, "subj"))) || hasKey(e, "pred"));
194
+ const isNode = (n: unknown): boolean =>
195
+ isJsonObject(n) && (hasKey(n, "lbl") || hasKey(n, "meta") || OBOGRAPHS_NODE_TYPES.has(n.type));
196
+ return (
197
+ (Array.isArray(graph.edges) && graph.edges.some(isEdge)) ||
198
+ (Array.isArray(graph.nodes) && graph.nodes.some(isNode))
199
+ );
200
+ }
201
+
174
202
  /**
175
203
  * The first object of an array, or null.
176
204
  * @param value - maybe an array
@@ -223,6 +251,8 @@ export const DIALECT_DEFAULT_DIRECTED: Readonly<Record<JsonImportDialect, boolea
223
251
  // networkx adjacency_data declares `directed`; tree_graph always builds a DiGraph
224
252
  adjacency: false,
225
253
  tree: true,
254
+ // OBO Graphs edges point from the subject (child) to the object (parent)
255
+ obographs: true,
226
256
  });
227
257
 
228
258
  /**