@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
@@ -0,0 +1,1614 @@
1
+ import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
2
+ import { a3 as SINK_OPTION_CODE, V as OPTION_IGNORED_CODE, a2 as ROLE_TAKEN_CODE, $ as RENAMED_CODE, v as ID_MERGED_CODE, D as DANGLING_REFERENCE_CODE, af as UNKNOWN_ELEMENT_CODE, m as DUPLICATE_ATTRIBUTE_CODE, p as DUPLICATE_NODE_CODE, e as BAD_VALUE_CODE, N as MISSING_ID_CODE, ag as UNKNOWN_ENCODING_CODE, s as ENCODING_FALLBACK_CODE, z as INVALID_ENCODING_CODE, F as INVALID_UTF8_CODE, r as EMPTY_INPUT_CODE, aD as resolveImportOptions, I as ImportReportBuilder, aA as reportSinkOptions, aB as reportUnusedOptions, L as LineReader, a as throwIfAborted, as as declareResolved, J as IdCoercer, q as DirectionResolver } from "./report-BOk0p5y8.js";
3
+ import { S as SYNONYM_SCOPES, O as OBO_NODE_COLUMNS, o as oboColumnDecl, P as PLACEHOLDER_COLUMN } from "./ontology-BnrJ4I98.js";
4
+ const ESCAPES = Object.freeze({
5
+ n: "\n",
6
+ t: " ",
7
+ // the guides read `\W` as a space; the 1.4 BNF (and fastobo) as the letter W (design 4.1)
8
+ W: " "
9
+ });
10
+ function unescapeObo(text) {
11
+ if (!text.includes("\\")) {
12
+ return text;
13
+ }
14
+ let out = "";
15
+ for (let i = 0; i < text.length; i++) {
16
+ const ch = text[i];
17
+ if (ch !== "\\") {
18
+ out += ch;
19
+ continue;
20
+ }
21
+ i++;
22
+ if (i < text.length) {
23
+ const next = text[i];
24
+ out += ESCAPES[next] ?? next;
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ function isEscaped(text, index) {
30
+ let run = 0;
31
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i--) {
32
+ run++;
33
+ }
34
+ return run % 2 === 1;
35
+ }
36
+ function endsWithContinuation(line) {
37
+ return line.endsWith("\\") && !isEscaped(line, line.length - 1);
38
+ }
39
+ function splitTagValue(line) {
40
+ for (let i = 0; i < line.length; i++) {
41
+ if (line[i] === "\\") {
42
+ i++;
43
+ continue;
44
+ }
45
+ if (line[i] === ":") {
46
+ return { tag: unescapeObo(line.slice(0, i).trim()), rest: line.slice(i + 1).trimStart() };
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ function stripComment(rest) {
52
+ let quoted = false;
53
+ for (let i = 0; i < rest.length; i++) {
54
+ const ch = rest[i];
55
+ if (ch === "\\") {
56
+ i++;
57
+ continue;
58
+ }
59
+ const atStart = i === 0 || rest[i - 1] === " " || rest[i - 1] === " ";
60
+ if (ch === '"' && (quoted || atStart)) {
61
+ quoted = !quoted;
62
+ } else if (ch === "!" && !quoted && atStart) {
63
+ return rest.slice(0, i).trimEnd();
64
+ }
65
+ }
66
+ return rest.trimEnd();
67
+ }
68
+ function splitQualifiers(value) {
69
+ let rest = value;
70
+ let qualifiers = null;
71
+ for (; ; ) {
72
+ if (!rest.endsWith("}") || isEscaped(rest, rest.length - 1)) {
73
+ return { value: rest, qualifiers, badBlock: null };
74
+ }
75
+ const open = openingBrace(rest);
76
+ const parsed = open < 0 ? null : parseQualifiers(rest.slice(open + 1, -1));
77
+ if (parsed === null) {
78
+ return { value: rest, qualifiers, badBlock: open < 0 ? rest : rest.slice(open) };
79
+ }
80
+ qualifiers = qualifiers === null ? parsed : mergeQualifiers(parsed, qualifiers);
81
+ rest = rest.slice(0, open).trimEnd();
82
+ }
83
+ }
84
+ function openingBrace(value) {
85
+ let quoted = false;
86
+ let open = -1;
87
+ for (let i = 0; i < value.length - 1; i++) {
88
+ const ch = value[i];
89
+ if (ch === "\\") {
90
+ i++;
91
+ continue;
92
+ }
93
+ if (ch === '"') {
94
+ quoted = !quoted;
95
+ } else if (!quoted && ch === "{") {
96
+ open = i;
97
+ } else if (!quoted && ch === "}") {
98
+ open = -1;
99
+ }
100
+ }
101
+ return quoted ? -1 : open;
102
+ }
103
+ function mergeQualifiers(first, second) {
104
+ const out = { ...first };
105
+ for (const [name, value] of Object.entries(second)) {
106
+ addQualifier(out, name, value);
107
+ }
108
+ return out;
109
+ }
110
+ function addQualifier(record, name, value) {
111
+ if (!Object.prototype.hasOwnProperty.call(record, name)) {
112
+ record[name] = value;
113
+ return;
114
+ }
115
+ const before = record[name];
116
+ record[name] = [...Array.isArray(before) ? before : [before], ...Array.isArray(value) ? value : [value]];
117
+ }
118
+ function parseQualifiers(inner) {
119
+ const out = /* @__PURE__ */ Object.create(null);
120
+ let i = 0;
121
+ const n = inner.length;
122
+ let any = false;
123
+ while (i < n) {
124
+ while (i < n && (inner[i] === " " || inner[i] === " " || inner[i] === ",")) {
125
+ i++;
126
+ }
127
+ if (i >= n) {
128
+ break;
129
+ }
130
+ const eq = inner.indexOf("=", i);
131
+ if (eq < 0) {
132
+ return null;
133
+ }
134
+ const name = unescapeObo(inner.slice(i, eq).trim());
135
+ if (name.length === 0 || name.includes('"')) {
136
+ return null;
137
+ }
138
+ i = eq + 1;
139
+ while (i < n && (inner[i] === " " || inner[i] === " ")) {
140
+ i++;
141
+ }
142
+ let value;
143
+ if (inner[i] === '"') {
144
+ const end = closingQuote(inner, i + 1);
145
+ if (end < 0) {
146
+ return null;
147
+ }
148
+ value = unescapeObo(inner.slice(i + 1, end));
149
+ i = end + 1;
150
+ } else {
151
+ let end = i;
152
+ while (end < n && inner[end] !== ",") {
153
+ end += inner[end] === "\\" ? 2 : 1;
154
+ }
155
+ value = unescapeObo(inner.slice(i, end).trim());
156
+ i = end;
157
+ }
158
+ addQualifier(out, name, value);
159
+ any = true;
160
+ }
161
+ return any ? { ...out } : null;
162
+ }
163
+ function closingQuote(text, from) {
164
+ for (let i = from; i < text.length; i++) {
165
+ if (text[i] === "\\") {
166
+ i++;
167
+ } else if (text[i] === '"') {
168
+ return i;
169
+ }
170
+ }
171
+ return -1;
172
+ }
173
+ function tokenize(value) {
174
+ const tokens = [];
175
+ const n = value.length;
176
+ let i = 0;
177
+ while (i < n) {
178
+ const ch = value[i];
179
+ if (ch === " " || ch === " ") {
180
+ i++;
181
+ continue;
182
+ }
183
+ if (ch === '"') {
184
+ const end2 = closingQuote(value, i + 1);
185
+ const stop = end2 < 0 ? n : end2;
186
+ let after = stop + 1;
187
+ if (end2 >= 0 && value[after] === "@") {
188
+ while (after < n && value[after] !== " " && value[after] !== " ") {
189
+ after++;
190
+ }
191
+ }
192
+ const suffix = end2 < 0 ? "" : value.slice(stop + 1, after);
193
+ tokens.push({
194
+ kind: "quoted",
195
+ text: unescapeObo(value.slice(i + 1, stop)) + suffix,
196
+ unterminated: end2 < 0
197
+ });
198
+ i = after;
199
+ continue;
200
+ }
201
+ if (ch === "[") {
202
+ const end2 = closingBracket(value, i + 1);
203
+ const stop = end2 < 0 ? n : end2;
204
+ tokens.push({ kind: "list", text: value.slice(i + 1, stop), unterminated: end2 < 0 });
205
+ i = stop + 1;
206
+ continue;
207
+ }
208
+ let end = i;
209
+ while (end < n && value[end] !== " " && value[end] !== " ") {
210
+ end += value[end] === "\\" ? 2 : 1;
211
+ }
212
+ tokens.push({ kind: "word", text: unescapeObo(value.slice(i, Math.min(end, n))), unterminated: false });
213
+ i = end;
214
+ }
215
+ return tokens;
216
+ }
217
+ function closingBracket(text, from) {
218
+ let quoted = false;
219
+ for (let i = from; i < text.length; i++) {
220
+ const ch = text[i];
221
+ if (ch === "\\") {
222
+ i++;
223
+ } else if (ch === '"') {
224
+ quoted = !quoted;
225
+ } else if (ch === "]" && !quoted) {
226
+ return i;
227
+ }
228
+ }
229
+ return -1;
230
+ }
231
+ function parseXref(raw) {
232
+ const { value, qualifiers } = splitQualifiers(raw.trim());
233
+ let quote = -1;
234
+ for (let i = 0; i < value.length; i++) {
235
+ if (value[i] === "\\") {
236
+ i++;
237
+ } else if (value[i] === '"') {
238
+ quote = i;
239
+ break;
240
+ }
241
+ }
242
+ const id = unescapeObo((quote < 0 ? value : value.slice(0, quote)).trim());
243
+ if (id.length === 0) {
244
+ return null;
245
+ }
246
+ let description = null;
247
+ if (quote >= 0) {
248
+ const end = closingQuote(value, quote + 1);
249
+ description = unescapeObo(value.slice(quote + 1, end < 0 ? value.length : end));
250
+ }
251
+ return { id, description, qualifiers };
252
+ }
253
+ function parseXrefList(inner) {
254
+ const out = [];
255
+ let quoted = false;
256
+ let depth = 0;
257
+ let start = 0;
258
+ const flush = (end) => {
259
+ const xref = parseXref(inner.slice(start, end));
260
+ if (xref !== null) {
261
+ out.push(xref);
262
+ }
263
+ };
264
+ for (let i = 0; i < inner.length; i++) {
265
+ const ch = inner[i];
266
+ if (ch === "\\") {
267
+ i++;
268
+ } else if (ch === '"') {
269
+ quoted = !quoted;
270
+ } else if (!quoted && ch === "{") {
271
+ depth++;
272
+ } else if (!quoted && ch === "}") {
273
+ depth = Math.max(0, depth - 1);
274
+ } else if (!quoted && depth === 0 && ch === ",") {
275
+ flush(i);
276
+ start = i + 1;
277
+ }
278
+ }
279
+ flush(inner.length);
280
+ return out;
281
+ }
282
+ function hasStrayBrace(value) {
283
+ let quoted = false;
284
+ let listed = false;
285
+ for (let i = 0; i < value.length; i++) {
286
+ const ch = value[i];
287
+ if (ch === "\\") {
288
+ i++;
289
+ } else if (ch === '"') {
290
+ quoted = !quoted;
291
+ } else if (!quoted && (ch === "[" || ch === "]")) {
292
+ listed = ch === "[";
293
+ } else if (!quoted && !listed && (ch === "{" || ch === "}")) {
294
+ return true;
295
+ }
296
+ }
297
+ return false;
298
+ }
299
+ const OBO_ISSUE = Object.freeze({
300
+ /** The input is empty or whitespace (fatal). */
301
+ EMPTY_INPUT: EMPTY_INPUT_CODE,
302
+ /** The input holds invalid UTF-8 (fatal). */
303
+ INVALID_UTF8: INVALID_UTF8_CODE,
304
+ /** Invalid bytes in the encoding a BOM or the encoding option chose (fatal). */
305
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
306
+ /** Bytes that are not UTF-8 were read as windows-1252. */
307
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
308
+ /** A declared encoding the platform cannot decode was ignored. */
309
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
310
+ /** A frame without an `id` clause; the frame is skipped. */
311
+ MISSING_ID: MISSING_ID_CODE,
312
+ /** A clause whose value cannot be read (a boolean other than true / false, a relationship with one or three values); the clause is skipped. */
313
+ BAD_VALUE: BAD_VALUE_CODE,
314
+ /** Two frames with one id were merged (spec 4.1.1). */
315
+ DUPLICATE_NODE: DUPLICATE_NODE_CODE,
316
+ /** A single-valued tag given twice for one id (a cardinality violation); the first is kept. */
317
+ DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
318
+ /** An unknown tag (kept in `obo.unrecognized`) or frame type (skipped), once per name. */
319
+ UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
320
+ /** A target no frame declares: a placeholder node was made, or the edge dropped under addMissingNodes false. */
321
+ DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
322
+ /** A line without a colon, an unterminated quote, a def without its xref list, a qualifier block that does not parse, an unescaped brace. */
323
+ SYNTAX: "W_OBO_SYNTAX",
324
+ /** The frame's `id` is not its first clause; it is used anyway. */
325
+ ID_NOT_FIRST: "W_OBO_ID_NOT_FIRST",
326
+ /** A synonym without a scope in a file that does not say 1.2, or with a scope that is not one of the four. */
327
+ SYNONYM_SCOPE: "W_OBO_SYNONYM_SCOPE",
328
+ /** A relation, subset or synonym type that nothing declares. */
329
+ UNDECLARED: "W_OBO_UNDECLARED",
330
+ /** One id for a Term and a Typedef (or an Instance); the Term (the first node frame) is the node. */
331
+ ID_KIND_CLASH: "W_OBO_ID_KIND_CLASH",
332
+ /** Fewer than two `intersection_of` or `union_of` clauses on a frame. */
333
+ CARDINALITY: "W_OBO_CARDINALITY",
334
+ /** An obsolete term with is_a / relationship, or replaced_by / consider on a term that is not obsolete. */
335
+ OBSOLETION: "W_OBO_OBSOLETION",
336
+ /** An OBO 1.0 / 1.2 tag read as its 1.4 meaning (exact_synonym, xref_analog, use_term, typeref, version). */
337
+ DEPRECATED_TAG: "W_OBO_DEPRECATED_TAG",
338
+ /** A backslash line continuation (deprecated in 1.4). */
339
+ DEPRECATED_SYNTAX: "W_OBO_DEPRECATED_SYNTAX",
340
+ /** A header clause kept in `meta.extra.obo.header` whose meaning is not applied (import, id-mapping, the treat-xrefs macros, owl-axioms). */
341
+ HEADER_NOT_APPLIED: "W_OBO_HEADER_NOT_APPLIED",
342
+ /** Obsolete terms and their edges left out under obsolete: "drop". */
343
+ OBSOLETE_DROPPED: "W_OBO_OBSOLETE_DROPPED",
344
+ /** Two distinct id texts became one number under ids "number". */
345
+ ID_MERGED: ID_MERGED_CODE,
346
+ /** A vocabulary column renamed `<name>#obo` because the sink already holds the name. */
347
+ COLUMN_RENAMED: RENAMED_CODE,
348
+ /** A vocabulary column declared without its role because the sink already holds it. */
349
+ ROLE_TAKEN: ROLE_TAKEN_CODE,
350
+ /** A common option the format has no use for (defaultDirected, weightFrom, nodeIdFrom, ...). */
351
+ OPTION_IGNORED: OPTION_IGNORED_CODE,
352
+ /** A builder-policy option the caller passed that the caller's sink does not use. */
353
+ SINK_OPTION: SINK_OPTION_CODE
354
+ });
355
+ const USED_OPTIONS = /* @__PURE__ */ new Set([
356
+ "ids",
357
+ "addMissingNodes",
358
+ "duplicateEdges",
359
+ "selfLoops",
360
+ "onMixedDirection",
361
+ "weightDtype",
362
+ "errorLimit",
363
+ "signal",
364
+ "onProgress",
365
+ "encoding"
366
+ ]);
367
+ const DEFAULTS = { ids: "keep", defaultDirected: true, weightFrom: null };
368
+ const ABORT_CHECK_INTERVAL = 64;
369
+ const SNIFF_BYTES = 4096;
370
+ const FRAME_KINDS = /* @__PURE__ */ new Set(["Term", "Typedef", "Instance"]);
371
+ const NOT_APPLIED_HEADER = /^(import|typeref|id-mapping|default-relationship-id-prefix|owl-axioms|treat-xrefs-as-.*)$/;
372
+ const DEPRECATED_HEADER = /* @__PURE__ */ new Map([
373
+ ["typeref", "import"],
374
+ ["version", "data-version"]
375
+ ]);
376
+ const DEPRECATED_TAGS = /* @__PURE__ */ new Map([
377
+ ["exact_synonym", ["synonym", "EXACT"]],
378
+ ["narrow_synonym", ["synonym", "NARROW"]],
379
+ ["broad_synonym", ["synonym", "BROAD"]],
380
+ ["related_synonym", ["synonym", "RELATED"]],
381
+ ["xref_analog", ["xref", null]],
382
+ ["xref_unk", ["xref", null]],
383
+ ["xref_unknown", ["xref", null]],
384
+ ["use_term", ["consider", null]]
385
+ ]);
386
+ const BUILTIN_RELATIONS = /* @__PURE__ */ new Set([
387
+ "is_a",
388
+ "instance_of",
389
+ "disjoint_from",
390
+ "inverse_of",
391
+ "union_of",
392
+ "intersection_of"
393
+ ]);
394
+ const TEXT_TAGS = /* @__PURE__ */ new Set([
395
+ "name",
396
+ "namespace",
397
+ "comment",
398
+ "created_by",
399
+ "creation_date",
400
+ "domain",
401
+ "range",
402
+ "inverse_of"
403
+ ]);
404
+ const BOOL_TAGS = new Set(
405
+ Object.keys(OBO_NODE_COLUMNS).filter((name) => OBO_NODE_COLUMNS[name].dtype === "bool")
406
+ );
407
+ const ID_LIST_TAGS = /* @__PURE__ */ new Set([
408
+ "alt_id",
409
+ "subset",
410
+ "replaced_by",
411
+ "consider",
412
+ "union_of",
413
+ "equivalent_to",
414
+ "disjoint_from",
415
+ "transitive_over",
416
+ "disjoint_over"
417
+ ]);
418
+ const TYPEDEF_TAGS = /* @__PURE__ */ new Set([
419
+ "domain",
420
+ "range",
421
+ "inverse_of",
422
+ "transitive_over",
423
+ "disjoint_over",
424
+ "holds_over_chain",
425
+ "equivalent_to_chain",
426
+ "expand_assertion_to",
427
+ "expand_expression_to",
428
+ "is_cyclic",
429
+ "is_reflexive",
430
+ "is_symmetric",
431
+ "is_anti_symmetric",
432
+ "is_asymmetric",
433
+ "is_transitive",
434
+ "is_functional",
435
+ "is_inverse_functional",
436
+ "is_metadata_tag",
437
+ "is_class_level"
438
+ ]);
439
+ function resolveOboOptions(options) {
440
+ const obsolete = options?.obsolete ?? "keep";
441
+ const typedefs = options?.typedefs ?? "metadata";
442
+ if (obsolete !== "keep" && obsolete !== "drop") {
443
+ throw unsupported("obsolete", obsolete, ["keep", "drop"]);
444
+ }
445
+ if (typedefs !== "metadata" && typedefs !== "nodes") {
446
+ throw unsupported("typedefs", typedefs, ["metadata", "nodes"]);
447
+ }
448
+ return { obsolete, typedefs };
449
+ }
450
+ function unsupported(option, found, supported) {
451
+ return new GraphFormatError(
452
+ "E_UNSUPPORTED",
453
+ `option ${option}: ${JSON.stringify(found)} is not one of ${supported.join(", ")}`,
454
+ {
455
+ option,
456
+ found,
457
+ supported
458
+ }
459
+ );
460
+ }
461
+ class OboReader {
462
+ /**
463
+ * Create a reader.
464
+ * @param report - the report
465
+ * @param signal - the cancellation signal
466
+ */
467
+ constructor(report, signal) {
468
+ this.header = {};
469
+ this.version = null;
470
+ this.defaultNamespace = null;
471
+ this.subsets = /* @__PURE__ */ new Set();
472
+ this.synonymTypes = /* @__PURE__ */ new Map();
473
+ this.nodes = /* @__PURE__ */ new Map();
474
+ this.typedefs = /* @__PURE__ */ new Map();
475
+ this.tallies = /* @__PURE__ */ new Map();
476
+ this.frame = null;
477
+ this.unknownFrames = [];
478
+ this.framesSinceCheck = 0;
479
+ this.significant = false;
480
+ this.report = report;
481
+ this.signal = signal;
482
+ }
483
+ /**
484
+ * Read one logical line.
485
+ * @param text - the line, continuation joined
486
+ * @param line - its 1-based number
487
+ */
488
+ line(text, line) {
489
+ const t = text.trim();
490
+ if (t.length === 0) {
491
+ return;
492
+ }
493
+ this.significant = true;
494
+ if (t.startsWith("!")) {
495
+ return;
496
+ }
497
+ const header = /^\[([^\]]*)\]\s*(?:!.*)?$/.exec(t);
498
+ if (header !== null) {
499
+ this.startFrame(header[1].trim(), line);
500
+ return;
501
+ }
502
+ const tv = splitTagValue(stripComment(t));
503
+ if (tv === null) {
504
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, "a line without a colon was skipped", "line", line);
505
+ return;
506
+ }
507
+ const clause = { tag: tv.tag, value: stripComment(tv.rest), line };
508
+ if (this.frame === null) {
509
+ this.headerClause(clause);
510
+ } else {
511
+ this.frame.clauses.push(clause);
512
+ }
513
+ }
514
+ /**
515
+ * Start a frame, finishing the one before.
516
+ * @param name - the frame type
517
+ * @param line - the header's line
518
+ */
519
+ startFrame(name, line) {
520
+ this.finishFrame();
521
+ const kind = FRAME_KINDS.has(name) ? name : null;
522
+ if (kind === null) {
523
+ this.tally(
524
+ "validation-error",
525
+ OBO_ISSUE.UNKNOWN_ELEMENT,
526
+ "frames of an unknown type are not nodes; kept in meta.extra.obo.unknownFrames",
527
+ `[${name}]`,
528
+ line
529
+ );
530
+ }
531
+ this.frame = { kind, name, line, clauses: [] };
532
+ }
533
+ /**
534
+ * Record one header clause.
535
+ * @param clause - the clause
536
+ */
537
+ headerClause(clause) {
538
+ (this.header[clause.tag] ??= []).push(clause.value);
539
+ const { tag } = clause;
540
+ const tokens = tokenize(splitQualifiers(clause.value).value);
541
+ const first = tokens.length > 0 && tokens[0].kind === "word" ? tokens[0].text : null;
542
+ if (tag === "format-version") {
543
+ this.version ??= unescapeObo(clause.value).trim();
544
+ } else if (tag === "default-namespace") {
545
+ this.defaultNamespace ??= first;
546
+ } else if (tag === "subsetdef" && first !== null) {
547
+ this.subsets.add(first);
548
+ } else if (tag === "synonymtypedef" && first !== null) {
549
+ const scope = tokens.slice(1).find((t) => t.kind === "word" && SYNONYM_SCOPES.has(t.text));
550
+ this.synonymTypes.set(first, scope?.text ?? null);
551
+ }
552
+ const now = DEPRECATED_HEADER.get(tag);
553
+ if (now !== void 0) {
554
+ this.tally("coercion", OBO_ISSUE.DEPRECATED_TAG, `the 1.0 header tag is read as ${now}`, tag, clause.line);
555
+ }
556
+ if (NOT_APPLIED_HEADER.test(tag)) {
557
+ this.tally(
558
+ "unsupported",
559
+ OBO_ISSUE.HEADER_NOT_APPLIED,
560
+ "kept in meta.extra.obo.header, not applied",
561
+ tag,
562
+ clause.line
563
+ );
564
+ }
565
+ }
566
+ /** Finish the frame being read: find its id and merge its clauses into the record of that id. */
567
+ finishFrame() {
568
+ const { frame } = this;
569
+ this.frame = null;
570
+ if (frame === null) {
571
+ return;
572
+ }
573
+ if (frame.kind === null) {
574
+ const clauses = {};
575
+ for (const clause of frame.clauses) {
576
+ (clauses[clause.tag] ??= []).push(clause.value);
577
+ }
578
+ this.unknownFrames.push({ type: frame.name, clauses });
579
+ return;
580
+ }
581
+ if (++this.framesSinceCheck >= ABORT_CHECK_INTERVAL) {
582
+ this.framesSinceCheck = 0;
583
+ throwIfAborted(this.signal);
584
+ }
585
+ const idAt = frame.clauses.findIndex((c) => c.tag === "id");
586
+ const id = idAt < 0 ? "" : unescapeObo(splitQualifiers(frame.clauses[idAt].value).value).trim();
587
+ if (id.length === 0) {
588
+ this.report.error(
589
+ "missing-value",
590
+ OBO_ISSUE.MISSING_ID,
591
+ `a [${frame.kind}] frame has no id; it is skipped`,
592
+ {
593
+ line: frame.line
594
+ }
595
+ );
596
+ this.report.counts.skippedNodes++;
597
+ return;
598
+ }
599
+ if (idAt > 0) {
600
+ this.tally(
601
+ "validation-error",
602
+ OBO_ISSUE.ID_NOT_FIRST,
603
+ "the id clause is not the frame's first; it is used",
604
+ "id",
605
+ frame.clauses[idAt].line
606
+ );
607
+ }
608
+ const record = this.recordFor(frame.kind, id, frame.line);
609
+ for (let i = 0; i < frame.clauses.length; i++) {
610
+ const clause = frame.clauses[i];
611
+ if (clause.tag === "id") {
612
+ if (i !== idAt) {
613
+ this.tally(
614
+ "validation-error",
615
+ OBO_ISSUE.DUPLICATE_ATTRIBUTE,
616
+ "a frame names a second id; the first is kept",
617
+ "id",
618
+ clause.line
619
+ );
620
+ }
621
+ continue;
622
+ }
623
+ this.applyClause(record, clause);
624
+ }
625
+ if (record.kind === "Typedef") {
626
+ record.raw.id ??= [id];
627
+ }
628
+ }
629
+ /**
630
+ * The record a frame merges into: the existing one of its id, or a new one.
631
+ * @param kind - the frame type
632
+ * @param id - the id
633
+ * @param line - the frame's line
634
+ * @returns the record
635
+ */
636
+ recordFor(kind, id, line) {
637
+ const map = kind === "Typedef" ? this.typedefs : this.nodes;
638
+ const existing = map.get(id);
639
+ if (existing !== void 0) {
640
+ if (existing.kind !== kind) {
641
+ this.tally(
642
+ "validation-error",
643
+ OBO_ISSUE.ID_KIND_CLASH,
644
+ `an id names both a ${existing.kind} and an ${kind}; the ${existing.kind} is the node`,
645
+ id,
646
+ line
647
+ );
648
+ } else {
649
+ this.report.warning(
650
+ "merged",
651
+ OBO_ISSUE.DUPLICATE_NODE,
652
+ `two [${kind}] frames have the id ${id}; they are merged (spec 4.1.1)`,
653
+ {
654
+ line,
655
+ element: id
656
+ }
657
+ );
658
+ }
659
+ return existing;
660
+ }
661
+ const record = {
662
+ id,
663
+ kind,
664
+ line,
665
+ values: /* @__PURE__ */ new Map(),
666
+ lists: /* @__PURE__ */ new Map(),
667
+ edges: [],
668
+ seen: /* @__PURE__ */ new Set(),
669
+ raw: {}
670
+ };
671
+ map.set(id, record);
672
+ return record;
673
+ }
674
+ /**
675
+ * A clause's tag (a 1.0 tag read as its current one), the synonym scope that tag implies, its
676
+ * qualifier block and its value, with the syntax warnings about stray braces recorded.
677
+ * @param clause - the clause
678
+ * @returns the parts
679
+ */
680
+ clauseParts(clause) {
681
+ let { tag } = clause;
682
+ let scope = null;
683
+ const deprecated = DEPRECATED_TAGS.get(tag);
684
+ if (deprecated !== void 0) {
685
+ this.tally(
686
+ "coercion",
687
+ OBO_ISSUE.DEPRECATED_TAG,
688
+ `the 1.0 tag is read as ${deprecated[0]}`,
689
+ tag,
690
+ clause.line
691
+ );
692
+ [tag, scope] = deprecated;
693
+ }
694
+ const split = splitQualifiers(clause.value);
695
+ const { qualifiers } = split;
696
+ if (split.badBlock !== null) {
697
+ this.tally(
698
+ "parse-error",
699
+ OBO_ISSUE.SYNTAX,
700
+ "a closing brace that is not a qualifier block was kept as text",
701
+ tag,
702
+ clause.line
703
+ );
704
+ } else if (hasStrayBrace(split.value)) {
705
+ this.tally(
706
+ "parse-error",
707
+ OBO_ISSUE.SYNTAX,
708
+ "an unescaped brace inside a value was kept as text",
709
+ tag,
710
+ clause.line
711
+ );
712
+ }
713
+ return { tag, scope, qualifiers, value: split.value };
714
+ }
715
+ /**
716
+ * A def, expand_assertion_to or expand_expression_to clause: quoted text and its xref list.
717
+ * @param record - the record
718
+ * @param tag - the tag
719
+ * @param value - the clause value
720
+ * @param tokens - its tokens
721
+ * @param qualifiers - its qualifier block
722
+ * @param line - its line
723
+ */
724
+ definition(record, tag, value, tokens, qualifiers, line) {
725
+ if (tag === "def" && record.values.has("def")) {
726
+ this.single(record, "def", null, line);
727
+ return;
728
+ }
729
+ const text = this.quotedWithXrefs(record, tag, value, tokens, line);
730
+ if (tag === "def") {
731
+ this.single(record, "def", text.text, line, () => {
732
+ record.values.set("def.xrefs", text.xrefs);
733
+ });
734
+ this.extraQualifiers(record, tag, text.text, qualifiers);
735
+ } else {
736
+ this.push(record, tag, withQualifiers({ template: text.text, xrefs: text.xrefs }, qualifiers));
737
+ }
738
+ }
739
+ /**
740
+ * Apply one clause to a record.
741
+ * @param record - the record
742
+ * @param clause - the clause
743
+ */
744
+ applyClause(record, clause) {
745
+ const key = `${clause.tag}\0${clause.value}`;
746
+ if (record.seen.has(key)) {
747
+ return;
748
+ }
749
+ record.seen.add(key);
750
+ if (record.kind === "Typedef") {
751
+ (record.raw[clause.tag] ??= []).push(clause.value);
752
+ }
753
+ const { tag, scope, qualifiers, value } = this.clauseParts(clause);
754
+ const where = { line: clause.line, element: record.id };
755
+ if (TYPEDEF_TAGS.has(tag) && record.kind !== "Typedef") {
756
+ this.unrecognized(record, clause);
757
+ return;
758
+ }
759
+ if (TEXT_TAGS.has(tag)) {
760
+ this.single(record, tag, unescapeObo(value).trim(), clause.line);
761
+ this.extraQualifiers(record, tag, unescapeObo(value).trim(), qualifiers);
762
+ return;
763
+ }
764
+ if (BOOL_TAGS.has(tag)) {
765
+ const text = unescapeObo(value).trim();
766
+ if (text !== "true" && text !== "false") {
767
+ this.report.error(
768
+ "validation-error",
769
+ OBO_ISSUE.BAD_VALUE,
770
+ `${tag}: ${JSON.stringify(text)} is not true or false; the clause is skipped`,
771
+ where
772
+ );
773
+ return;
774
+ }
775
+ this.single(record, tag, text === "true", clause.line);
776
+ this.extraQualifiers(record, tag, text, qualifiers);
777
+ return;
778
+ }
779
+ const tokens = tokenize(value);
780
+ if (ID_LIST_TAGS.has(tag)) {
781
+ const id = this.oneWord(tokens, tag, where);
782
+ if (id !== null) {
783
+ this.push(record, tag, id);
784
+ this.extraQualifiers(record, tag, id, qualifiers);
785
+ if (tag === "subset" && !this.subsets.has(id)) {
786
+ this.tally(
787
+ "validation-error",
788
+ OBO_ISSUE.UNDECLARED,
789
+ "a subset no subsetdef declares",
790
+ id,
791
+ clause.line
792
+ );
793
+ }
794
+ }
795
+ return;
796
+ }
797
+ switch (tag) {
798
+ case "is_a":
799
+ case "instance_of": {
800
+ if (tag === "instance_of" && record.kind !== "Instance") {
801
+ this.unrecognized(record, clause);
802
+ return;
803
+ }
804
+ const target = this.oneWord(tokens, tag, where);
805
+ if (target !== null) {
806
+ record.edges.push({ relation: tag, target, qualifiers, line: clause.line });
807
+ }
808
+ return;
809
+ }
810
+ case "relationship": {
811
+ if (tokens.length !== 2 || tokens.some((t) => t.kind !== "word")) {
812
+ this.report.error(
813
+ "validation-error",
814
+ OBO_ISSUE.BAD_VALUE,
815
+ `relationship needs a relation and a target, found ${JSON.stringify(unescapeObo(value))}; the clause is skipped`,
816
+ where
817
+ );
818
+ return;
819
+ }
820
+ record.edges.push({ relation: tokens[0].text, target: tokens[1].text, qualifiers, line: clause.line });
821
+ return;
822
+ }
823
+ case "intersection_of": {
824
+ if (tokens.length < 1 || tokens.length > 2 || tokens.some((t) => t.kind !== "word")) {
825
+ this.report.error(
826
+ "validation-error",
827
+ OBO_ISSUE.BAD_VALUE,
828
+ `intersection_of needs a class, or a relation and a class, found ${JSON.stringify(unescapeObo(value))}; the clause is skipped`,
829
+ where
830
+ );
831
+ return;
832
+ }
833
+ const item = tokens.length === 1 ? { relation: null, target: tokens[0].text } : { relation: tokens[0].text, target: tokens[1].text };
834
+ this.push(record, tag, withQualifiers(item, qualifiers));
835
+ return;
836
+ }
837
+ case "holds_over_chain":
838
+ case "equivalent_to_chain": {
839
+ if (tokens.length !== 2 || tokens.some((t) => t.kind !== "word")) {
840
+ this.report.error(
841
+ "validation-error",
842
+ OBO_ISSUE.BAD_VALUE,
843
+ `${tag} needs two relations; the clause is skipped`,
844
+ where
845
+ );
846
+ return;
847
+ }
848
+ this.push(record, tag, [tokens[0].text, tokens[1].text]);
849
+ this.extraQualifiers(record, tag, `${tokens[0].text} ${tokens[1].text}`, qualifiers);
850
+ return;
851
+ }
852
+ case "def":
853
+ case "expand_assertion_to":
854
+ case "expand_expression_to":
855
+ this.definition(record, tag, value, tokens, qualifiers, clause.line);
856
+ return;
857
+ case "synonym":
858
+ this.synonym(record, tokens, scope, qualifiers, clause.line);
859
+ return;
860
+ case "xref": {
861
+ const xref = parseXref(value);
862
+ if (xref === null) {
863
+ this.report.error(
864
+ "validation-error",
865
+ OBO_ISSUE.BAD_VALUE,
866
+ "xref has no id; the clause is skipped",
867
+ where
868
+ );
869
+ return;
870
+ }
871
+ this.push(record, "xref", xref.id);
872
+ this.describe(record, [xref]);
873
+ this.extraQualifiers(record, tag, xref.id, qualifiers ?? xref.qualifiers);
874
+ return;
875
+ }
876
+ case "property_value":
877
+ this.propertyValue(record, tokens, qualifiers, where);
878
+ return;
879
+ default:
880
+ this.unrecognized(record, clause);
881
+ }
882
+ }
883
+ /**
884
+ * Read a value that must be one word.
885
+ * @param tokens - the value's tokens
886
+ * @param tag - the tag, for the message
887
+ * @param where - the location
888
+ * @returns the word, or null after reporting E_BAD_VALUE
889
+ */
890
+ oneWord(tokens, tag, where) {
891
+ if (tokens.length === 1 && tokens[0].kind === "word") {
892
+ return tokens[0].text;
893
+ }
894
+ const found = tokens.map((t) => t.text).join(" ");
895
+ this.report.error(
896
+ "validation-error",
897
+ OBO_ISSUE.BAD_VALUE,
898
+ `${tag} needs one id, found ${JSON.stringify(found)}; the clause is skipped`,
899
+ where
900
+ );
901
+ return null;
902
+ }
903
+ /**
904
+ * Read `"text" [xrefs]` (def, expand_*), reporting the forms the grammar does not allow.
905
+ * @param record - the record (xref descriptions go to its `xref.descriptions`)
906
+ * @param tag - the tag
907
+ * @param value - the raw value
908
+ * @param tokens - its tokens
909
+ * @param line - the line
910
+ * @returns the text and the xref ids
911
+ */
912
+ quotedWithXrefs(record, tag, value, tokens, line) {
913
+ if (tokens.length === 0 || tokens[0].kind !== "quoted") {
914
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, "the text is not quoted; the whole value is kept", tag, line);
915
+ return { text: unescapeObo(value).trim(), xrefs: [] };
916
+ }
917
+ if (tokens[0].unterminated) {
918
+ this.tally(
919
+ "parse-error",
920
+ OBO_ISSUE.SYNTAX,
921
+ "an unterminated quote; the text runs to the end of the line",
922
+ tag,
923
+ line
924
+ );
925
+ return { text: tokens[0].text, xrefs: [] };
926
+ }
927
+ const list = tokens[1];
928
+ if (list?.kind !== "list") {
929
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, "the xref list is missing", tag, line);
930
+ return { text: tokens[0].text, xrefs: [] };
931
+ }
932
+ const xrefs = parseXrefList(list.text);
933
+ this.describe(record, xrefs);
934
+ this.xrefQualifiers(record, tag, xrefs);
935
+ return { text: tokens[0].text, xrefs: xrefs.map((x) => x.id) };
936
+ }
937
+ /**
938
+ * Read a synonym: `"text" SCOPE? TYPE? [xrefs]` (the scope optional in 1.2).
939
+ * @param record - the record
940
+ * @param tokens - the value's tokens
941
+ * @param fixedScope - the scope of a 1.0 tag (exact_synonym), or null
942
+ * @param qualifiers - the clause's qualifiers
943
+ * @param line - the line
944
+ */
945
+ synonym(record, tokens, fixedScope, qualifiers, line) {
946
+ if (tokens.length === 0 || tokens[0].kind !== "quoted") {
947
+ this.report.error(
948
+ "validation-error",
949
+ OBO_ISSUE.BAD_VALUE,
950
+ "a synonym needs its quoted text; the clause is skipped",
951
+ {
952
+ line,
953
+ element: record.id
954
+ }
955
+ );
956
+ return;
957
+ }
958
+ if (tokens[0].unterminated) {
959
+ this.tally(
960
+ "parse-error",
961
+ OBO_ISSUE.SYNTAX,
962
+ "an unterminated quote; the text runs to the end of the line",
963
+ "synonym",
964
+ line
965
+ );
966
+ }
967
+ const words = tokens.slice(1).filter((t) => t.kind === "word").map((t) => t.text);
968
+ const list = tokens.slice(1).find((t) => t.kind === "list");
969
+ let scope = fixedScope;
970
+ let type = null;
971
+ if (fixedScope === null) {
972
+ if (words.length > 0 && SYNONYM_SCOPES.has(words[0])) {
973
+ [scope] = words;
974
+ type = words[1] ?? null;
975
+ } else if (words.length > 0 && this.synonymTypes.has(words[0])) {
976
+ [type] = words;
977
+ scope = "RELATED";
978
+ } else if (words.length > 0) {
979
+ [type] = words;
980
+ this.tally(
981
+ "validation-error",
982
+ OBO_ISSUE.SYNONYM_SCOPE,
983
+ `a scope that is not EXACT, BROAD, NARROW or RELATED was kept as the type, scope null`,
984
+ words[0],
985
+ line
986
+ );
987
+ } else {
988
+ scope = "RELATED";
989
+ }
990
+ if (scope === "RELATED" && (words.length === 0 || words[0] === type) && !this.allowsMissingScope()) {
991
+ this.tally(
992
+ "validation-error",
993
+ OBO_ISSUE.SYNONYM_SCOPE,
994
+ "a synonym without a scope (1.2 only) is read as RELATED",
995
+ "synonym",
996
+ line
997
+ );
998
+ }
999
+ }
1000
+ if (type !== null) {
1001
+ const declared = this.synonymTypes.get(type);
1002
+ if (declared === void 0) {
1003
+ if (scope !== null) {
1004
+ this.tally(
1005
+ "validation-error",
1006
+ OBO_ISSUE.UNDECLARED,
1007
+ "a synonym type no synonymtypedef declares",
1008
+ type,
1009
+ line
1010
+ );
1011
+ }
1012
+ } else if (declared !== null) {
1013
+ scope = declared;
1014
+ }
1015
+ }
1016
+ if (words.length > 2 || list === void 0) {
1017
+ this.tally(
1018
+ "parse-error",
1019
+ OBO_ISSUE.SYNTAX,
1020
+ list === void 0 ? "a synonym without its xref list" : "extra words after the synonym type",
1021
+ "synonym",
1022
+ line
1023
+ );
1024
+ }
1025
+ const xrefs = list === void 0 ? [] : parseXrefList(list.text);
1026
+ this.describe(record, xrefs);
1027
+ this.xrefQualifiers(record, "synonym", xrefs);
1028
+ this.push(
1029
+ record,
1030
+ "synonym",
1031
+ withQualifiers({ text: tokens[0].text, scope, type, xrefs: xrefs.map((x) => x.id) }, qualifiers)
1032
+ );
1033
+ }
1034
+ /**
1035
+ * Whether the file's version lets a synonym omit its scope (1.0 and 1.2).
1036
+ * @returns true for a 1.2 or 1.0 file
1037
+ */
1038
+ allowsMissingScope() {
1039
+ return this.version !== null && /^(1\.[0-2]|GO_1\.[0-2])\b/.test(this.version);
1040
+ }
1041
+ /**
1042
+ * Read a property_value: `relation "literal" datatype` or `relation id`.
1043
+ * @param record - the record
1044
+ * @param tokens - the value's tokens
1045
+ * @param qualifiers - the clause's qualifiers
1046
+ * @param where - the location
1047
+ */
1048
+ propertyValue(record, tokens, qualifiers, where) {
1049
+ if (tokens.length < 2 || tokens.length > 3 || tokens[0].kind !== "word" || tokens[1].kind === "list") {
1050
+ this.report.error(
1051
+ "validation-error",
1052
+ OBO_ISSUE.BAD_VALUE,
1053
+ "property_value needs a relation and a value; the clause is skipped",
1054
+ where
1055
+ );
1056
+ return;
1057
+ }
1058
+ const datatype = tokens.length === 3 ? tokens[2].text : null;
1059
+ this.push(
1060
+ record,
1061
+ "property_value",
1062
+ withQualifiers({ relation: tokens[0].text, value: tokens[1].text, datatype }, qualifiers)
1063
+ );
1064
+ }
1065
+ /**
1066
+ * Set a single-valued column, keeping the first value of an id.
1067
+ * @param record - the record
1068
+ * @param column - the column
1069
+ * @param value - the value
1070
+ * @param line - the line
1071
+ * @param also - more to set together with the value
1072
+ */
1073
+ single(record, column, value, line, also) {
1074
+ if (record.values.has(column)) {
1075
+ this.tally(
1076
+ "validation-error",
1077
+ OBO_ISSUE.DUPLICATE_ATTRIBUTE,
1078
+ "a single-valued tag given twice for one id; the first is kept",
1079
+ column,
1080
+ line
1081
+ );
1082
+ return;
1083
+ }
1084
+ record.values.set(column, value);
1085
+ also?.();
1086
+ }
1087
+ /**
1088
+ * Append to a list column, without repeats (merged frames take the union).
1089
+ * @param record - the record
1090
+ * @param column - the column
1091
+ * @param value - the item
1092
+ */
1093
+ push(record, column, value) {
1094
+ let list = record.lists.get(column);
1095
+ if (list === void 0) {
1096
+ list = [];
1097
+ record.lists.set(column, list);
1098
+ }
1099
+ if (typeof value === "object" || !list.includes(value)) {
1100
+ list.push(value);
1101
+ }
1102
+ }
1103
+ /**
1104
+ * Record xref descriptions in `xref.descriptions`.
1105
+ * @param record - the record
1106
+ * @param xrefs - the xrefs
1107
+ */
1108
+ describe(record, xrefs) {
1109
+ for (const xref of xrefs) {
1110
+ if (xref.description !== null) {
1111
+ const map = record.values.get("xref.descriptions") ?? {};
1112
+ map[xref.id] ??= xref.description;
1113
+ record.values.set("xref.descriptions", map);
1114
+ }
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Keep the per-xref qualifiers of an xref list (OBO 1.2: `[A:1 {source="x"}]`) in
1119
+ * `obo.qualifiers` under `<tag>.xrefs`.
1120
+ * @param record - the record
1121
+ * @param tag - the clause's tag
1122
+ * @param xrefs - the list's xrefs
1123
+ */
1124
+ xrefQualifiers(record, tag, xrefs) {
1125
+ for (const xref of xrefs) {
1126
+ this.extraQualifiers(record, `${tag}.xrefs`, xref.id, xref.qualifiers);
1127
+ }
1128
+ }
1129
+ /**
1130
+ * Keep the qualifiers of a clause whose column has no place for them in `obo.qualifiers`.
1131
+ * @param record - the record
1132
+ * @param tag - the tag
1133
+ * @param value - the clause's value as stored
1134
+ * @param qualifiers - the qualifiers, or null
1135
+ */
1136
+ extraQualifiers(record, tag, value, qualifiers) {
1137
+ if (qualifiers === null) {
1138
+ return;
1139
+ }
1140
+ const map = record.values.get("obo.qualifiers") ?? {};
1141
+ (map[tag] ??= []).push({ value, qualifiers });
1142
+ record.values.set("obo.qualifiers", map);
1143
+ }
1144
+ /**
1145
+ * Keep an unknown clause in `obo.unrecognized`.
1146
+ * @param record - the record
1147
+ * @param clause - the clause
1148
+ */
1149
+ unrecognized(record, clause) {
1150
+ const map = record.values.get("obo.unrecognized") ?? {};
1151
+ (map[clause.tag] ??= []).push(unescapeObo(clause.value));
1152
+ record.values.set("obo.unrecognized", map);
1153
+ this.tally(
1154
+ "validation-error",
1155
+ OBO_ISSUE.UNKNOWN_ELEMENT,
1156
+ "an unknown tag was kept in obo.unrecognized",
1157
+ clause.tag,
1158
+ clause.line
1159
+ );
1160
+ }
1161
+ /**
1162
+ * Count a warning, recorded once per code and element at the end.
1163
+ * @param category - the category
1164
+ * @param code - the code
1165
+ * @param what - what happened
1166
+ * @param element - the tag, id or name it is about
1167
+ * @param line - the first line
1168
+ */
1169
+ tally(category, code, what, element, line) {
1170
+ const key = `${code}\0${element}\0${what}`;
1171
+ const found = this.tallies.get(key);
1172
+ if (found === void 0) {
1173
+ this.tallies.set(key, { category, code, element, what, line, count: 1 });
1174
+ } else {
1175
+ found.count++;
1176
+ }
1177
+ }
1178
+ /** Record the counted warnings, in the order they were first seen. */
1179
+ flushTallies() {
1180
+ for (const t of this.tallies.values()) {
1181
+ this.report.warning(
1182
+ t.category,
1183
+ t.code,
1184
+ `${t.element}: ${t.what} (${t.count} time(s), first at line ${t.line})`,
1185
+ {
1186
+ line: t.line,
1187
+ element: t.element
1188
+ }
1189
+ );
1190
+ }
1191
+ this.tallies.clear();
1192
+ }
1193
+ /**
1194
+ * The header as GraphMeta fields (design 4.2): name from `ontology`, the version, `created` from
1195
+ * the `dd:MM:yyyy HH:mm` date, `creator` from `saved-by`.
1196
+ * @returns the metadata patch, without `extra`
1197
+ */
1198
+ meta() {
1199
+ const first = (tag) => {
1200
+ const values = this.header[tag];
1201
+ return values === void 0 ? null : unescapeObo(values[0]).trim();
1202
+ };
1203
+ return {
1204
+ name: first("ontology"),
1205
+ sourceFormat: "obo",
1206
+ sourceVersion: this.version,
1207
+ created: oboDate(first("date")),
1208
+ creator: first("saved-by")
1209
+ };
1210
+ }
1211
+ /**
1212
+ * The raw header clauses.
1213
+ * @returns the header by tag
1214
+ */
1215
+ headerRecord() {
1216
+ return this.header;
1217
+ }
1218
+ /**
1219
+ * The namespace a frame without one gets.
1220
+ * @returns the default namespace, or null
1221
+ */
1222
+ namespaceDefault() {
1223
+ return this.defaultNamespace;
1224
+ }
1225
+ }
1226
+ function withQualifiers(item, qualifiers) {
1227
+ return qualifiers === null ? item : { ...item, qualifiers };
1228
+ }
1229
+ function oboDate(text) {
1230
+ const m = text === null ? null : /^(\d{2}):(\d{2}):(\d{4})\s+(\d{2}):(\d{2})$/.exec(text);
1231
+ if (m === null) {
1232
+ return null;
1233
+ }
1234
+ const [, dd, mm, yyyy, hh, min] = m;
1235
+ if (Number(mm) < 1 || Number(mm) > 12 || Number(dd) < 1 || Number(dd) > 31 || Number(hh) > 23 || Number(min) > 59) {
1236
+ return null;
1237
+ }
1238
+ return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
1239
+ }
1240
+ function planGraph(reader, report, addMissingNodes, obo) {
1241
+ const records = [...reader.nodes.values()];
1242
+ for (const [id, typedef] of reader.typedefs) {
1243
+ const node = reader.nodes.get(id);
1244
+ if (node !== void 0) {
1245
+ reader.tally(
1246
+ "validation-error",
1247
+ OBO_ISSUE.ID_KIND_CLASH,
1248
+ `an id names both a ${node.kind} and a Typedef; the ${node.kind} is the node`,
1249
+ id,
1250
+ typedef.line
1251
+ );
1252
+ } else if (obo.typedefs === "nodes") {
1253
+ records.push(typedef);
1254
+ }
1255
+ }
1256
+ for (const record of [...reader.nodes.values(), ...reader.typedefs.values()]) {
1257
+ for (const tag of ["intersection_of", "union_of"]) {
1258
+ if (record.lists.get(tag)?.length === 1) {
1259
+ reader.tally(
1260
+ "validation-error",
1261
+ OBO_ISSUE.CARDINALITY,
1262
+ "a single clause (a class expression needs two or more)",
1263
+ tag,
1264
+ record.line
1265
+ );
1266
+ }
1267
+ }
1268
+ }
1269
+ const dropped = /* @__PURE__ */ new Set();
1270
+ const kept = /* @__PURE__ */ new Set();
1271
+ for (const record of records) {
1272
+ const obsolete = record.values.get("is_obsolete") === true;
1273
+ (obo.obsolete === "drop" && obsolete ? dropped : kept).add(record.id);
1274
+ if (obsolete && record.edges.length > 0) {
1275
+ reader.tally(
1276
+ "validation-error",
1277
+ OBO_ISSUE.OBSOLETION,
1278
+ "an obsolete term has is_a or relationship clauses; they are kept",
1279
+ "is_obsolete",
1280
+ record.edges[0].line
1281
+ );
1282
+ }
1283
+ if (!obsolete && (record.lists.has("replaced_by") || record.lists.has("consider"))) {
1284
+ reader.tally(
1285
+ "validation-error",
1286
+ OBO_ISSUE.OBSOLETION,
1287
+ "replaced_by or consider on a term that is not obsolete; kept",
1288
+ "replaced_by",
1289
+ record.line
1290
+ );
1291
+ }
1292
+ }
1293
+ const declared = new Set(reader.typedefs.keys());
1294
+ for (const typedef of reader.typedefs.values()) {
1295
+ for (const xref of typedef.lists.get("xref") ?? []) {
1296
+ declared.add(xref);
1297
+ }
1298
+ }
1299
+ const dangling = [];
1300
+ const danglingSet = /* @__PURE__ */ new Set();
1301
+ let danglingEdges = 0;
1302
+ let droppedEdges = 0;
1303
+ const edges = [];
1304
+ for (const record of records) {
1305
+ for (const edge of record.edges) {
1306
+ if (dropped.has(record.id) || dropped.has(edge.target)) {
1307
+ droppedEdges++;
1308
+ continue;
1309
+ }
1310
+ if (!BUILTIN_RELATIONS.has(edge.relation) && !declared.has(edge.relation)) {
1311
+ reader.tally(
1312
+ "validation-error",
1313
+ OBO_ISSUE.UNDECLARED,
1314
+ "a relation no Typedef declares",
1315
+ edge.relation,
1316
+ edge.line
1317
+ );
1318
+ }
1319
+ if (!kept.has(edge.target)) {
1320
+ if (!danglingSet.has(edge.target)) {
1321
+ danglingSet.add(edge.target);
1322
+ dangling.push(edge.target);
1323
+ }
1324
+ if (!addMissingNodes) {
1325
+ danglingEdges++;
1326
+ report.counts.skippedEdges++;
1327
+ continue;
1328
+ }
1329
+ }
1330
+ edges.push({ source: record.id, edge });
1331
+ }
1332
+ }
1333
+ reader.flushTallies();
1334
+ reportDangling(records, dangling, addMissingNodes, danglingEdges, report);
1335
+ if (dropped.size > 0) {
1336
+ report.warning(
1337
+ "merged",
1338
+ OBO_ISSUE.OBSOLETE_DROPPED,
1339
+ `${dropped.size} obsolete term(s) and ${droppedEdges} edge(s) to or from them were left out (obsolete: "drop")`,
1340
+ { element: [...dropped][0] }
1341
+ );
1342
+ }
1343
+ return {
1344
+ nodes: records.filter((record) => kept.has(record.id)),
1345
+ placeholders: addMissingNodes ? dangling : [],
1346
+ edges
1347
+ };
1348
+ }
1349
+ function reportDangling(records, dangling, addMissingNodes, droppedEdges, report) {
1350
+ if (dangling.length === 0) {
1351
+ return;
1352
+ }
1353
+ const altOf = /* @__PURE__ */ new Map();
1354
+ for (const record of records) {
1355
+ for (const alt of record.lists.get("alt_id") ?? []) {
1356
+ altOf.set(alt, record.id);
1357
+ }
1358
+ }
1359
+ const shown = dangling.slice(0, 5).map((id) => {
1360
+ const primary = altOf.get(id);
1361
+ return primary === void 0 ? id : `${id} (an alt_id of ${primary})`;
1362
+ });
1363
+ const more = dangling.length > 5 ? `, ... (${dangling.length - 5} more)` : "";
1364
+ const action = addMissingNodes ? "each became a placeholder node (graphty.placeholder)" : `${droppedEdges} edge(s) to them were dropped (addMissingNodes false)`;
1365
+ report.warning(
1366
+ "validation-error",
1367
+ OBO_ISSUE.DANGLING_REFERENCE,
1368
+ `${dangling.length} target(s) no frame declares: ${shown.join(", ")}${more}; ${action}`,
1369
+ { element: dangling[0] }
1370
+ );
1371
+ }
1372
+ class Pusher {
1373
+ /**
1374
+ * Create a pusher.
1375
+ * @param sink - the sink
1376
+ * @param report - the report
1377
+ * @param options - the resolved common options
1378
+ */
1379
+ constructor(sink, report, options) {
1380
+ this.since = 0;
1381
+ this.sink = sink;
1382
+ this.report = report;
1383
+ this.coercer = new IdCoercer(options.ids);
1384
+ this.signal = options.signal;
1385
+ }
1386
+ /**
1387
+ * An id text under the `ids` rule, reporting a merge.
1388
+ * @param text - the id as written
1389
+ * @returns the node id
1390
+ */
1391
+ id(text) {
1392
+ const id = this.coercer.text(text);
1393
+ const merge = this.coercer.lastMerge;
1394
+ if (merge !== null) {
1395
+ this.report.warning(
1396
+ "coercion",
1397
+ OBO_ISSUE.ID_MERGED,
1398
+ `id text ${JSON.stringify(merge.text)} merged with ${JSON.stringify(merge.previousText)} as ${merge.id}`,
1399
+ { element: text }
1400
+ );
1401
+ }
1402
+ return id;
1403
+ }
1404
+ /**
1405
+ * Add a node, counting it, or record why it was refused.
1406
+ * @param text - the id as written
1407
+ * @param line - the line, when known
1408
+ * @returns the node index, or -1 when refused
1409
+ */
1410
+ node(text, line) {
1411
+ this.tick();
1412
+ try {
1413
+ const id = this.id(text);
1414
+ const known = this.sink.indexOf(id) !== INVALID_INDEX;
1415
+ const index = this.sink.addNode(id);
1416
+ if (!known) {
1417
+ this.report.counts.nodes++;
1418
+ }
1419
+ return index;
1420
+ } catch (err) {
1421
+ this.report.recordError(err, { line, element: text });
1422
+ this.report.counts.skippedNodes++;
1423
+ return -1;
1424
+ }
1425
+ }
1426
+ /** Check the cancellation signal every ABORT_CHECK_INTERVAL pushes. */
1427
+ tick() {
1428
+ if (++this.since >= ABORT_CHECK_INTERVAL) {
1429
+ this.since = 0;
1430
+ throwIfAborted(this.signal);
1431
+ }
1432
+ }
1433
+ }
1434
+ function writeGraph(reader, sink, report, options, obo) {
1435
+ const plan = planGraph(reader, report, options.addMissingNodes, obo);
1436
+ const push = new Pusher(sink, report, options);
1437
+ const namespace = reader.namespaceDefault();
1438
+ const used = /* @__PURE__ */ new Set(["type"]);
1439
+ for (const record of plan.nodes) {
1440
+ for (const name of [...record.values.keys(), ...record.lists.keys()]) {
1441
+ used.add(name);
1442
+ }
1443
+ }
1444
+ if (namespace !== null) {
1445
+ used.add("namespace");
1446
+ }
1447
+ const columns = /* @__PURE__ */ new Map();
1448
+ for (const name of plan.nodes.length > 0 ? Object.keys(OBO_NODE_COLUMNS) : []) {
1449
+ if (used.has(name)) {
1450
+ columns.set(name, declareResolved(sink, "node", oboColumnDecl("node", name), report).handle);
1451
+ }
1452
+ }
1453
+ sink.reserve(plan.nodes.length + plan.placeholders.length, plan.edges.length);
1454
+ for (const record of plan.nodes) {
1455
+ const index = push.node(record.id, record.line);
1456
+ if (index < 0) {
1457
+ continue;
1458
+ }
1459
+ const cells = [["type", record.kind], ...record.values, ...record.lists];
1460
+ if (namespace !== null && !record.values.has("namespace")) {
1461
+ cells.push(["namespace", namespace]);
1462
+ }
1463
+ for (const [name, value] of cells) {
1464
+ const handle = columns.get(name);
1465
+ if (handle !== void 0) {
1466
+ sink.setNodeValue(handle, index, value);
1467
+ }
1468
+ }
1469
+ }
1470
+ if (plan.placeholders.length > 0) {
1471
+ const placeholder = declareResolved(sink, "node", oboColumnDecl("node", PLACEHOLDER_COLUMN), report).handle;
1472
+ for (const text of plan.placeholders) {
1473
+ const index = push.node(text);
1474
+ if (index >= 0) {
1475
+ sink.setNodeValue(placeholder, index, true);
1476
+ }
1477
+ }
1478
+ }
1479
+ writeEdges(plan, push, options);
1480
+ }
1481
+ function writeEdges(plan, push, options) {
1482
+ const { sink, report } = push;
1483
+ const direction = new DirectionResolver(sink, report, options.onMixedDirection);
1484
+ direction.setHeader(true);
1485
+ if (plan.edges.length === 0) {
1486
+ return;
1487
+ }
1488
+ const relation = declareResolved(sink, "edge", oboColumnDecl("edge", "relation"), report).handle;
1489
+ const qualifiers = plan.edges.some((e) => e.edge.qualifiers !== null) ? declareResolved(sink, "edge", oboColumnDecl("edge", "qualifiers"), report).handle : null;
1490
+ for (const { source, edge } of plan.edges) {
1491
+ push.tick();
1492
+ const before = sink.edgeCount;
1493
+ try {
1494
+ const e = direction.addEdge(push.id(source), push.id(edge.target), "directed", void 0, {
1495
+ line: edge.line,
1496
+ element: source
1497
+ });
1498
+ report.counts.edges += sink.edgeCount - before;
1499
+ if (e < 0) {
1500
+ continue;
1501
+ }
1502
+ sink.setEdgeValue(relation, e, edge.relation);
1503
+ if (qualifiers !== null && edge.qualifiers !== null) {
1504
+ sink.setEdgeValue(qualifiers, e, edge.qualifiers);
1505
+ }
1506
+ } catch (err) {
1507
+ report.recordError(err, { line: edge.line, element: source });
1508
+ report.counts.skippedEdges++;
1509
+ }
1510
+ }
1511
+ }
1512
+ function sniffObo(head) {
1513
+ let text = new TextDecoder("utf-8").decode(head.subarray(0, SNIFF_BYTES));
1514
+ if (text.charCodeAt(0) === 65279) {
1515
+ text = text.slice(1);
1516
+ }
1517
+ const lines = text.split(/\r\n|\r|\n|\f/);
1518
+ if (lines.length > 1 && head.byteLength >= SNIFF_BYTES) {
1519
+ lines.pop();
1520
+ }
1521
+ let tagLines = 0;
1522
+ for (const raw of lines) {
1523
+ const line = raw.trim();
1524
+ if (line.length === 0 || line.startsWith("!")) {
1525
+ continue;
1526
+ }
1527
+ if (/^\[(Term|Typedef|Instance)\]\s*(!.*)?$/.test(line)) {
1528
+ return 0.9;
1529
+ }
1530
+ if (tagLines === 0 && /^format-version\s*:/.test(line)) {
1531
+ return 0.9;
1532
+ }
1533
+ if (/^[A-Za-z][A-Za-z0-9_-]*\s*:(\s|$)/.test(line)) {
1534
+ tagLines++;
1535
+ continue;
1536
+ }
1537
+ return 0;
1538
+ }
1539
+ return tagLines > 0 ? 0.4 : 0;
1540
+ }
1541
+ const oboImporter = Object.freeze({
1542
+ format: "obo",
1543
+ extensions: Object.freeze([".obo"]),
1544
+ mimeTypes: Object.freeze(["text/obo", "application/obo"]),
1545
+ /**
1546
+ * Confidence that the head is an OBO file.
1547
+ * @param head - the first bytes
1548
+ * @returns the confidence
1549
+ */
1550
+ sniff(head) {
1551
+ return sniffObo(head);
1552
+ },
1553
+ /**
1554
+ * Read an OBO file into the sink.
1555
+ * @param input - the text, bytes or stream
1556
+ * @param sink - the sink
1557
+ * @param options - format-specific and common options
1558
+ * @returns the import report; ImportError on a fatal error or beyond the error limit
1559
+ */
1560
+ async import(input, sink, options) {
1561
+ const resolved = resolveImportOptions(options, DEFAULTS);
1562
+ const obo = resolveOboOptions(options);
1563
+ const report = new ImportReportBuilder("obo", resolved.errorLimit);
1564
+ reportSinkOptions(sink, options, report, true);
1565
+ reportUnusedOptions(options, report, USED_OPTIONS);
1566
+ const reader = new OboReader(report, resolved.signal);
1567
+ const lines = new LineReader(input, report, resolved);
1568
+ let pending = null;
1569
+ for await (const physical of lines) {
1570
+ const { line } = lines;
1571
+ const text = pending === null ? physical : pending + physical;
1572
+ pending = null;
1573
+ if (endsWithContinuation(text)) {
1574
+ reader.tally(
1575
+ "coercion",
1576
+ OBO_ISSUE.DEPRECATED_SYNTAX,
1577
+ "a backslash line continuation was joined (deprecated in 1.4)",
1578
+ "\\",
1579
+ line
1580
+ );
1581
+ pending = text.slice(0, -1);
1582
+ continue;
1583
+ }
1584
+ for (const piece of text.includes("\f") ? text.split("\f") : [text]) {
1585
+ reader.line(piece, line);
1586
+ }
1587
+ }
1588
+ if (pending !== null) {
1589
+ reader.line(pending, lines.line);
1590
+ }
1591
+ reader.finishFrame();
1592
+ if (!reader.significant) {
1593
+ report.fail(OBO_ISSUE.EMPTY_INPUT, "the input is empty");
1594
+ }
1595
+ throwIfAborted(resolved.signal);
1596
+ writeGraph(reader, sink, report, resolved, obo);
1597
+ const typedefs = {};
1598
+ for (const [id, record] of reader.typedefs) {
1599
+ typedefs[id] = record.raw;
1600
+ }
1601
+ const extra = { header: reader.headerRecord(), typedefs };
1602
+ if (reader.unknownFrames.length > 0) {
1603
+ extra.unknownFrames = reader.unknownFrames;
1604
+ }
1605
+ sink.setMeta({ ...reader.meta(), extra: { obo: extra } });
1606
+ throwIfAborted(resolved.signal);
1607
+ return report.finish();
1608
+ }
1609
+ });
1610
+ export {
1611
+ OBO_ISSUE as O,
1612
+ oboImporter as o
1613
+ };
1614
+ //# sourceMappingURL=importer-aNJfe0qu.js.map