@graphty/graph-io 0.3.40 → 0.3.41

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 (189) hide show
  1. package/dist/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
  2. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
  3. package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
  4. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
  5. package/dist/chunks/{escape-BJRLd3vJ.js → escape-DBb8zuNE.js} +2 -2
  6. package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-DBb8zuNE.js.map} +1 -1
  7. package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
  8. package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
  9. package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-BqtmlxNr.js} +545 -459
  10. package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
  11. package/dist/chunks/{exporter-6ocL_9x_.js → exporter-CS35R6Fg.js} +4 -4
  12. package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-CS35R6Fg.js.map} +1 -1
  13. package/dist/chunks/{exporter-BVXnV9-9.js → exporter-D8r1N2dU.js} +28 -16
  14. package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
  15. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
  16. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
  17. package/dist/chunks/{importer-CPqnIY8m.js → importer-BKC7AmCA.js} +42 -29
  18. package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
  19. package/dist/chunks/{importer-D_srHyj6.js → importer-BdWHmlmG.js} +4 -4
  20. package/dist/chunks/{importer-D_srHyj6.js.map → importer-BdWHmlmG.js.map} +1 -1
  21. package/dist/chunks/{importer-AjwOJrn2.js → importer-BfbzWk6k.js} +202 -181
  22. package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
  23. package/dist/chunks/{importer-BpuS3J8C.js → importer-BmaZcskq.js} +73 -60
  24. package/dist/chunks/importer-BmaZcskq.js.map +1 -0
  25. package/dist/chunks/{importer-DEp_fYjf.js → importer-DviEsHy4.js} +92 -64
  26. package/dist/chunks/importer-DviEsHy4.js.map +1 -0
  27. package/dist/chunks/{importer-DcKB97kW.js → importer-S-abxJHn.js} +5 -5
  28. package/dist/chunks/{importer-DcKB97kW.js.map → importer-S-abxJHn.js.map} +1 -1
  29. package/dist/chunks/{importer-BJlGAOht.js → importer-r29cWnEf.js} +3 -3
  30. package/dist/chunks/{importer-BJlGAOht.js.map → importer-r29cWnEf.js.map} +1 -1
  31. package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DdF-etPT.js} +3 -3
  32. package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DdF-etPT.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
  34. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
  35. package/dist/chunks/{records-r3SFHz11.js → records-Uc3LkABx.js} +15 -12
  36. package/dist/chunks/records-Uc3LkABx.js.map +1 -0
  37. package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
  38. package/dist/chunks/text-Bh9SaAob.js.map +1 -0
  39. package/dist/csv.js +67 -49
  40. package/dist/csv.js.map +1 -1
  41. package/dist/cx.js +1 -1
  42. package/dist/cx2.js +2 -2
  43. package/dist/cys.js +1 -1
  44. package/dist/dot.js +1 -1
  45. package/dist/gexf.js +195 -97
  46. package/dist/gexf.js.map +1 -1
  47. package/dist/gml.js +170 -149
  48. package/dist/gml.js.map +1 -1
  49. package/dist/graph-io.js +26 -26
  50. package/dist/graphml.js +2 -2
  51. package/dist/json.js +1 -1
  52. package/dist/neo4j.js +103 -65
  53. package/dist/neo4j.js.map +1 -1
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/attributes.d.ts.map +1 -1
  57. package/dist/src/common/attributes.js +34 -9
  58. package/dist/src/common/attributes.js.map +1 -1
  59. package/dist/src/common/input.d.ts.map +1 -1
  60. package/dist/src/common/input.js +180 -102
  61. package/dist/src/common/input.js.map +1 -1
  62. package/dist/src/common/text.d.ts +7 -0
  63. package/dist/src/common/text.d.ts.map +1 -1
  64. package/dist/src/common/text.js +15 -7
  65. package/dist/src/common/text.js.map +1 -1
  66. package/dist/src/common/zip.d.ts.map +1 -1
  67. package/dist/src/common/zip.js +36 -19
  68. package/dist/src/common/zip.js.map +1 -1
  69. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  70. package/dist/src/formats/csv/exporter.js +77 -25
  71. package/dist/src/formats/csv/exporter.js.map +1 -1
  72. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  73. package/dist/src/formats/csv/importer.js +16 -6
  74. package/dist/src/formats/csv/importer.js.map +1 -1
  75. package/dist/src/formats/csv/records.d.ts.map +1 -1
  76. package/dist/src/formats/csv/records.js +18 -10
  77. package/dist/src/formats/csv/records.js.map +1 -1
  78. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  79. package/dist/src/formats/cys/tables.js +11 -8
  80. package/dist/src/formats/cys/tables.js.map +1 -1
  81. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  82. package/dist/src/formats/gexf/exporter.js +65 -37
  83. package/dist/src/formats/gexf/exporter.js.map +1 -1
  84. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  85. package/dist/src/formats/gexf/importer.js +138 -50
  86. package/dist/src/formats/gexf/importer.js.map +1 -1
  87. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/gml/exporter.js +51 -35
  89. package/dist/src/formats/gml/exporter.js.map +1 -1
  90. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  91. package/dist/src/formats/gml/importer.js +103 -58
  92. package/dist/src/formats/gml/importer.js.map +1 -1
  93. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  94. package/dist/src/formats/gml/syntax.js +17 -8
  95. package/dist/src/formats/gml/syntax.js.map +1 -1
  96. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/graphml/exporter.js +34 -24
  98. package/dist/src/formats/graphml/exporter.js.map +1 -1
  99. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  100. package/dist/src/formats/graphml/importer.js +17 -13
  101. package/dist/src/formats/graphml/importer.js.map +1 -1
  102. package/dist/src/formats/graphml/tree.js +23 -14
  103. package/dist/src/formats/graphml/tree.js.map +1 -1
  104. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  105. package/dist/src/formats/graphml/yfiles.js +22 -10
  106. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  107. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  108. package/dist/src/formats/json/exporter.js +38 -21
  109. package/dist/src/formats/json/exporter.js.map +1 -1
  110. package/dist/src/formats/json/importer.d.ts.map +1 -1
  111. package/dist/src/formats/json/importer.js +536 -343
  112. package/dist/src/formats/json/importer.js.map +1 -1
  113. package/dist/src/formats/json/obographs.js +29 -17
  114. package/dist/src/formats/json/obographs.js.map +1 -1
  115. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  116. package/dist/src/formats/neo4j/exporter.js +73 -43
  117. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  118. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  119. package/dist/src/formats/neo4j/header.js +37 -16
  120. package/dist/src/formats/neo4j/header.js.map +1 -1
  121. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  122. package/dist/src/formats/neo4j/importer.js +21 -11
  123. package/dist/src/formats/neo4j/importer.js.map +1 -1
  124. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  125. package/dist/src/formats/obo/exporter.js +18 -7
  126. package/dist/src/formats/obo/exporter.js.map +1 -1
  127. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  128. package/dist/src/formats/pajek/exporter.js +142 -73
  129. package/dist/src/formats/pajek/exporter.js.map +1 -1
  130. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  131. package/dist/src/formats/pajek/importer.js +26 -15
  132. package/dist/src/formats/pajek/importer.js.map +1 -1
  133. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  134. package/dist/src/formats/pajek/syntax.js +85 -58
  135. package/dist/src/formats/pajek/syntax.js.map +1 -1
  136. package/dist/src/formats/xgmml/emit.d.ts +7 -0
  137. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  138. package/dist/src/formats/xgmml/emit.js +79 -40
  139. package/dist/src/formats/xgmml/emit.js.map +1 -1
  140. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  141. package/dist/src/formats/xgmml/exporter.js +38 -12
  142. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  143. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  144. package/dist/src/formats/xgmml/importer.js +38 -18
  145. package/dist/src/formats/xgmml/importer.js.map +1 -1
  146. package/dist/src/sniff.js +109 -57
  147. package/dist/src/sniff.js.map +1 -1
  148. package/dist/xgmml.js +2 -2
  149. package/package.json +2 -2
  150. package/src/common/attributes.ts +59 -19
  151. package/src/common/input.ts +216 -102
  152. package/src/common/text.ts +24 -15
  153. package/src/common/zip.ts +47 -18
  154. package/src/formats/csv/exporter.ts +103 -39
  155. package/src/formats/csv/importer.ts +17 -6
  156. package/src/formats/csv/records.ts +19 -10
  157. package/src/formats/cys/tables.ts +11 -10
  158. package/src/formats/gexf/exporter.ts +67 -36
  159. package/src/formats/gexf/importer.ts +173 -63
  160. package/src/formats/gml/exporter.ts +78 -82
  161. package/src/formats/gml/importer.ts +119 -68
  162. package/src/formats/gml/syntax.ts +18 -8
  163. package/src/formats/graphml/exporter.ts +44 -24
  164. package/src/formats/graphml/importer.ts +18 -11
  165. package/src/formats/graphml/tree.ts +22 -12
  166. package/src/formats/graphml/yfiles.ts +23 -10
  167. package/src/formats/json/exporter.ts +40 -21
  168. package/src/formats/json/importer.ts +701 -385
  169. package/src/formats/json/obographs.ts +35 -15
  170. package/src/formats/neo4j/exporter.ts +79 -41
  171. package/src/formats/neo4j/header.ts +50 -16
  172. package/src/formats/neo4j/importer.ts +21 -10
  173. package/src/formats/obo/exporter.ts +34 -17
  174. package/src/formats/pajek/exporter.ts +180 -117
  175. package/src/formats/pajek/importer.ts +27 -15
  176. package/src/formats/pajek/syntax.ts +99 -58
  177. package/src/formats/xgmml/emit.ts +88 -40
  178. package/src/formats/xgmml/exporter.ts +40 -12
  179. package/src/formats/xgmml/importer.ts +45 -23
  180. package/src/sniff.ts +115 -59
  181. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  182. package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
  183. package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
  184. package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
  185. package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
  186. package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
  187. package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
  188. package/dist/chunks/records-r3SFHz11.js.map +0 -1
  189. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -237,19 +237,7 @@ export function membersOf(
237
237
  const edges: EdgeRec[] = [];
238
238
  let dangling = 0;
239
239
  for (const member of graph.members) {
240
- if (member.href === null) {
241
- if (member.kind === "node") {
242
- nodes.push(member);
243
- } else {
244
- edges.push(member);
245
- }
246
- continue;
247
- }
248
- const id = localRef(member.href);
249
- let target: NodeRec | EdgeRec | undefined;
250
- if (id !== null) {
251
- target = member.kind === "node" ? nodeById.get(id) : edgeById.get(id);
252
- }
240
+ const target = resolveMember(member, nodeById, edgeById);
253
241
  if (target === undefined) {
254
242
  dangling++;
255
243
  } else if (target.kind === "node") {
@@ -261,6 +249,29 @@ export function membersOf(
261
249
  return { nodes, edges, dangling };
262
250
  }
263
251
 
252
+ /**
253
+ * The record a graph member stands for: itself when declared in place, else the node or edge its
254
+ * local `#id` reference names.
255
+ * @param member - the member
256
+ * @param nodeById - the first node declaration of each id
257
+ * @param edgeById - the first edge declaration of each id
258
+ * @returns the record, or undefined for a cross-file, malformed or dangling reference
259
+ */
260
+ function resolveMember(
261
+ member: NodeRec | EdgeRec,
262
+ nodeById: ReadonlyMap<string, NodeRec>,
263
+ edgeById: ReadonlyMap<string, EdgeRec>,
264
+ ): NodeRec | EdgeRec | undefined {
265
+ if (member.href === null) {
266
+ return member;
267
+ }
268
+ const id = localRef(member.href);
269
+ if (id === null) {
270
+ return undefined;
271
+ }
272
+ return member.kind === "node" ? nodeById.get(id) : edgeById.get(id);
273
+ }
274
+
264
275
  /**
265
276
  * The first declaration of each id.
266
277
  * @param records - node or edge records
@@ -276,6 +287,42 @@ function indexById<T extends NodeRec | EdgeRec>(records: readonly T[]): Map<stri
276
287
  return out;
277
288
  }
278
289
 
290
+ /**
291
+ * Whether an edge repeats one already seen, remembering it otherwise: a repeat has the same id,
292
+ * or without one the same label and endpoints.
293
+ * @param seen - the keys seen so far, or null when repeats are kept
294
+ * @param record - the edge
295
+ * @returns true for a repeat
296
+ */
297
+ function isRepeat(seen: Set<string> | null, record: EdgeRec): boolean {
298
+ if (seen === null) {
299
+ return false;
300
+ }
301
+ const key =
302
+ record.id ?? (record.label === null ? null : JSON.stringify([record.label, record.source, record.target]));
303
+ if (key === null) {
304
+ return false;
305
+ }
306
+ if (seen.has(key)) {
307
+ return true;
308
+ }
309
+ seen.add(key);
310
+ return false;
311
+ }
312
+
313
+ /**
314
+ * Whether a graph's att is one of its values: not RDF metadata, not the unnamed holder of a
315
+ * nested graph.
316
+ * @param att - the att
317
+ * @returns true to write it to the graph table
318
+ */
319
+ function isGraphValueAtt(att: AttRec): boolean {
320
+ if (att.name !== null && META_ATTS.has(att.name)) {
321
+ return false;
322
+ }
323
+ return !(att.hasGraph && att.name === null);
324
+ }
325
+
279
326
  /** Per-node state while emitting. */
280
327
  interface NodeRow {
281
328
  readonly id: string;
@@ -731,16 +778,7 @@ export class XgmmlEmitter {
731
778
  if (graph.owner !== null && isGroup(graph.owner)) {
732
779
  continue;
733
780
  }
734
- for (const member of graph.members) {
735
- if (member.href === null) {
736
- continue;
737
- }
738
- const id = localRef(member.href);
739
- const known = id !== null && (member.kind === "node" ? this.rows.has(id) : edgeIds.has(id));
740
- if (!known) {
741
- dangling++;
742
- }
743
- }
781
+ dangling += this.danglingReferences(graph, edgeIds);
744
782
  }
745
783
  if (dangling > 0) {
746
784
  this.report.warning(
@@ -751,6 +789,27 @@ export class XgmmlEmitter {
751
789
  }
752
790
  }
753
791
 
792
+ /**
793
+ * How many `xlink:href` members of a graph name no node or edge of the document.
794
+ * @param graph - the graph
795
+ * @param edgeIds - the id of every edge
796
+ * @returns the count
797
+ */
798
+ private danglingReferences(graph: GraphRec, edgeIds: ReadonlySet<string | null>): number {
799
+ let dangling = 0;
800
+ for (const member of graph.members) {
801
+ if (member.href === null) {
802
+ continue;
803
+ }
804
+ const id = localRef(member.href);
805
+ const known = id !== null && (member.kind === "node" ? this.rows.has(id) : edgeIds.has(id));
806
+ if (!known) {
807
+ dangling++;
808
+ }
809
+ }
810
+ return dangling;
811
+ }
812
+
754
813
  /**
755
814
  * Write the per-node values: label, attributes, XML attributes, graphics, positions, nested
756
815
  * network pointers and subgraph memberships.
@@ -966,17 +1025,10 @@ export class XgmmlEmitter {
966
1025
  this.decideNameLabel("edge", records);
967
1026
  for (const record of records) {
968
1027
  const where = { line: record.line, element: record.id ?? record.label };
969
- // a repeat has the same id, or without one the same label and endpoints
970
- const key =
971
- record.id ??
972
- (record.label === null ? null : JSON.stringify([record.label, record.source, record.target]));
973
- if (dedupe !== null && key !== null) {
974
- if (dedupe.has(key)) {
975
- repeats++;
976
- firstRepeat ??= where;
977
- continue;
978
- }
979
- dedupe.add(key);
1028
+ if (isRepeat(dedupe, record)) {
1029
+ repeats++;
1030
+ firstRepeat ??= where;
1031
+ continue;
980
1032
  }
981
1033
  const index = this.addEdge(record, header, ids, where);
982
1034
  this.checkAbort();
@@ -1244,13 +1296,9 @@ export class XgmmlEmitter {
1244
1296
  }
1245
1297
  }
1246
1298
  for (const att of g.atts) {
1247
- if (att.name !== null && META_ATTS.has(att.name)) {
1248
- continue;
1249
- }
1250
- if (att.hasGraph && att.name === null) {
1251
- continue;
1299
+ if (isGraphValueAtt(att)) {
1300
+ this.graphColumns.addAtt(0, att, where);
1252
1301
  }
1253
- this.graphColumns.addAtt(0, att, where);
1254
1302
  }
1255
1303
  this.graphColumns.write(this.sink, () => 0);
1256
1304
  const json: Record<string, unknown> = { ...this.extras.graphJson };
@@ -394,6 +394,17 @@ function slotOf(column: Column, domain: "graph" | "node" | "edge", hasLabelRole:
394
394
  if (domain !== "node") {
395
395
  return "att";
396
396
  }
397
+ return nodeSlotOf(column);
398
+ }
399
+
400
+ /**
401
+ * Where a node column goes when it is no XML attribute, label or graphics: one of the node-only
402
+ * slots, else a typed att.
403
+ * @param column - the column
404
+ * @returns the slot
405
+ */
406
+ function nodeSlotOf(column: Column): ColumnWrite["kind"] {
407
+ const { meta } = column;
397
408
  if (meta.name === Z_COLUMN && meta.dtype === "f64" && meta.role === null) {
398
409
  return "z";
399
410
  }
@@ -1004,18 +1015,7 @@ function cellOf(column: Column | null, row: number): unknown {
1004
1015
  * @returns the coordinates as XML attributes
1005
1016
  */
1006
1017
  function coordsOf(plan: Plan, zColumn: Column | null, node: number): [string, string][] {
1007
- const out: [string, string][] = [];
1008
- const xyz = cellOf(plan.position, node);
1009
- if (xyz !== null) {
1010
- const [x, y, z] = Array.from(xyz as ArrayLike<number>);
1011
- const fmt = plan.position?.dtype === "f32" ? formatF32 : formatF64;
1012
- if (Number.isFinite(x) && Number.isFinite(y)) {
1013
- out.push(["x", fmt(x)], ["y", fmt(y === 0 ? 0 : -y)]);
1014
- if (zColumn === null && z !== undefined && z !== 0 && Number.isFinite(z)) {
1015
- out.push(["z", fmt(z)]);
1016
- }
1017
- }
1018
- }
1018
+ const out = positionCoordsOf(plan, zColumn, node);
1019
1019
  const z = cellOf(zColumn, node);
1020
1020
  if (typeof z === "number") {
1021
1021
  out.push(["z", formatF64(z)]);
@@ -1023,6 +1023,34 @@ function coordsOf(plan: Plan, zColumn: Column | null, node: number): [string, st
1023
1023
  return out;
1024
1024
  }
1025
1025
 
1026
+ /**
1027
+ * The graphics coordinates a node's position gives: x, y negated back to screen coordinates, and
1028
+ * z when no z column carries it.
1029
+ * @param plan - the plan
1030
+ * @param zColumn - the z column, or null
1031
+ * @param node - the node
1032
+ * @returns the coordinates as XML attributes (none for an unset or non-finite position)
1033
+ */
1034
+ function positionCoordsOf(plan: Plan, zColumn: Column | null, node: number): [string, string][] {
1035
+ const xyz = cellOf(plan.position, node);
1036
+ if (xyz === null) {
1037
+ return [];
1038
+ }
1039
+ const [x, y, z] = Array.from(xyz as ArrayLike<number>);
1040
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
1041
+ return [];
1042
+ }
1043
+ const fmt = plan.position?.dtype === "f32" ? formatF32 : formatF64;
1044
+ const out: [string, string][] = [
1045
+ ["x", fmt(x)],
1046
+ ["y", fmt(y === 0 ? 0 : -y)],
1047
+ ];
1048
+ if (zColumn === null && z !== undefined && z !== 0 && Number.isFinite(z)) {
1049
+ out.push(["z", fmt(z)]);
1050
+ }
1051
+ return out;
1052
+ }
1053
+
1026
1054
  /**
1027
1055
  * The group marker and the nested `<graph>` of a group node: its own id, label and atts, and an
1028
1056
  * `xlink:href` to each child.
@@ -174,23 +174,7 @@ export async function parseXgmml(
174
174
  common: ResolvedImportOptions,
175
175
  options: XgmmlImportOptions | undefined,
176
176
  ): Promise<XgmmlDocument> {
177
- const repairs: XmlRepairs = {
178
- bareAmpersand: booleanOption("repairBareAmpersands", options?.repairBareAmpersands, false)
179
- ? (line): void => {
180
- report.warning("parse-error", XGMML_ISSUE.AMPERSAND_REPAIRED, "a bare & was read as &amp;", { line });
181
- }
182
- : undefined,
183
- surrogatePair: booleanOption("pairSurrogateReferences", options?.pairSurrogateReferences, false)
184
- ? (line): void => {
185
- report.warning(
186
- "parse-error",
187
- XGMML_ISSUE.SURROGATE_PAIRED,
188
- "two surrogate character references were joined into one character",
189
- { line },
190
- );
191
- }
192
- : undefined,
193
- };
177
+ const repairs = repairsOf(options, report);
194
178
  const parser = new XgmmlParser(report);
195
179
  const seen = { content: false };
196
180
  try {
@@ -200,12 +184,7 @@ export async function parseXgmml(
200
184
  repairs,
201
185
  );
202
186
  } catch (err) {
203
- if (err instanceof XmlSyntaxError) {
204
- if (!seen.content) {
205
- report.fail(XGMML_ISSUE.EMPTY_INPUT, "the input is empty");
206
- }
207
- report.fail(XGMML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
208
- }
187
+ failOnSyntax(err, seen.content, report);
209
188
  throw err;
210
189
  }
211
190
  const doc = parser.document();
@@ -220,6 +199,49 @@ export async function parseXgmml(
220
199
  return doc;
221
200
  }
222
201
 
202
+ /**
203
+ * The XML repairs the options switch on, each reporting where it repaired.
204
+ * @param options - the XGMML options
205
+ * @param report - the report
206
+ * @returns the repairs
207
+ */
208
+ function repairsOf(options: XgmmlImportOptions | undefined, report: ImportReportBuilder): XmlRepairs {
209
+ return {
210
+ bareAmpersand: booleanOption("repairBareAmpersands", options?.repairBareAmpersands, false)
211
+ ? (line): void => {
212
+ report.warning("parse-error", XGMML_ISSUE.AMPERSAND_REPAIRED, "a bare & was read as &amp;", { line });
213
+ }
214
+ : undefined,
215
+ surrogatePair: booleanOption("pairSurrogateReferences", options?.pairSurrogateReferences, false)
216
+ ? (line): void => {
217
+ report.warning(
218
+ "parse-error",
219
+ XGMML_ISSUE.SURROGATE_PAIRED,
220
+ "two surrogate character references were joined into one character",
221
+ { line },
222
+ );
223
+ }
224
+ : undefined,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Fail the report on an XML syntax error: an input with no content is empty, any other is
230
+ * malformed. Any other error is left to the caller.
231
+ * @param err - the error the tokenizer threw
232
+ * @param content - whether non-whitespace text was seen
233
+ * @param report - the report
234
+ */
235
+ function failOnSyntax(err: unknown, content: boolean, report: ImportReportBuilder): void {
236
+ if (!(err instanceof XmlSyntaxError)) {
237
+ return;
238
+ }
239
+ if (!content) {
240
+ report.fail(XGMML_ISSUE.EMPTY_INPUT, "the input is empty");
241
+ }
242
+ report.fail(XGMML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
243
+ }
244
+
223
245
  /**
224
246
  * Pass text chunks through while noting non-whitespace content.
225
247
  * @param chunks - the chunks
package/src/sniff.ts CHANGED
@@ -356,40 +356,57 @@ function skeletonOf(text: string): unknown {
356
356
  }
357
357
  const root: Record<string, unknown> = {};
358
358
  for (const key of rootKeys) {
359
- switch (key) {
360
- case "graph":
361
- case "options":
362
- root[key] = objectOf(keys.get(key));
363
- break;
364
- case "graphs": {
365
- // the keys of any node or edge of the first graph: OBO Graphs or JGF (design 1.6)
366
- const nodes = keys.get("graphs[0].nodes[]");
367
- const edges = keys.get("graphs[0].edges[]");
368
- const graph: Record<string, unknown> = {};
369
- if (nodes !== undefined) {
370
- graph.nodes = [objectOf(nodes)];
371
- }
372
- if (edges !== undefined) {
373
- graph.edges = [objectOf(edges)];
374
- }
375
- root[key] = nodes === undefined && edges === undefined ? [] : [graph];
376
- break;
377
- }
378
- case "nodes":
379
- case "edges":
380
- case "links": {
381
- const first = keys.get(`${key}[0]`);
382
- root[key] = first === undefined ? [] : [objectOf(first)];
383
- break;
384
- }
385
- default:
386
- root[key] = true;
387
- break;
388
- }
359
+ root[key] = placeholderOf(key, keys);
389
360
  }
390
361
  return root;
391
362
  }
392
363
 
364
+ /**
365
+ * The placeholder value of a root key: the shape the dialect rule tests that key for.
366
+ * @param key - the root key
367
+ * @param keys - the key sets by path
368
+ * @returns the placeholder
369
+ */
370
+ function placeholderOf(key: string, keys: ReadonlyMap<KeyPath, Set<string>>): unknown {
371
+ switch (key) {
372
+ case "graph":
373
+ case "options":
374
+ return objectOf(keys.get(key));
375
+ case "graphs":
376
+ return graphsPlaceholder(keys);
377
+ case "nodes":
378
+ case "edges":
379
+ case "links": {
380
+ const first = keys.get(`${key}[0]`);
381
+ return first === undefined ? [] : [objectOf(first)];
382
+ }
383
+ default:
384
+ return true;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * The placeholder of `graphs`: the keys of any node or edge of the first graph, which tell OBO
390
+ * Graphs from JGF (design 1.6).
391
+ * @param keys - the key sets by path
392
+ * @returns `[]` when neither was seen, else one graph holding them
393
+ */
394
+ function graphsPlaceholder(keys: ReadonlyMap<KeyPath, Set<string>>): unknown[] {
395
+ const nodes = keys.get("graphs[0].nodes[]");
396
+ const edges = keys.get("graphs[0].edges[]");
397
+ if (nodes === undefined && edges === undefined) {
398
+ return [];
399
+ }
400
+ const graph: Record<string, unknown> = {};
401
+ if (nodes !== undefined) {
402
+ graph.nodes = [objectOf(nodes)];
403
+ }
404
+ if (edges !== undefined) {
405
+ graph.edges = [objectOf(edges)];
406
+ }
407
+ return [graph];
408
+ }
409
+
393
410
  /**
394
411
  * An object holding placeholder values for a key set: `{}` for the keys the rule tests with
395
412
  * isJsonObject (`attributes`, `data`), `true` otherwise.
@@ -424,34 +441,6 @@ function scanKeys(text: string): Map<KeyPath, Set<string>> {
424
441
  const counts: number[] = [];
425
442
  let pendingKey: string | null = null;
426
443
  let expectingKey = false;
427
- const pathOf = (): KeyPath | null => {
428
- const depth = kinds.length;
429
- if (depth === 1) {
430
- return kinds[0] === "object" ? "" : null;
431
- }
432
- if (depth === 2 && kinds[0] === "array" && kinds[1] === "object" && labels[1] === "0") {
433
- return "nodes[0]";
434
- }
435
- if (depth === 2 && kinds[0] === "object" && kinds[1] === "object") {
436
- return labels[1] === "graph" || labels[1] === "options" ? labels[1] : null;
437
- }
438
- if (
439
- depth === 5 &&
440
- kinds.join() === "object,array,object,array,object" &&
441
- labels[1] === "graphs" &&
442
- labels[2] === "0" &&
443
- (labels[3] === "nodes" || labels[3] === "edges")
444
- ) {
445
- return labels[3] === "nodes" ? "graphs[0].nodes[]" : "graphs[0].edges[]";
446
- }
447
- if (depth === 3 && kinds[0] === "object" && kinds[1] === "array" && kinds[2] === "object") {
448
- const section = labels[1];
449
- if ((section === "nodes" || section === "edges" || section === "links") && labels[2] === "0") {
450
- return `${section}[0]`;
451
- }
452
- }
453
- return null;
454
- };
455
444
  let i = 0;
456
445
  const n = text.length;
457
446
  while (i < n) {
@@ -487,7 +476,7 @@ function scanKeys(text: string): Map<KeyPath, Set<string>> {
487
476
  pendingKey = null;
488
477
  } else if (ch === ":") {
489
478
  if (pendingKey !== null) {
490
- const path = pathOf();
479
+ const path = keyPathOf(kinds, labels);
491
480
  if (path !== null) {
492
481
  let set = found.get(path);
493
482
  if (set === undefined) {
@@ -510,6 +499,73 @@ function scanKeys(text: string): Map<KeyPath, Set<string>> {
510
499
  return found;
511
500
  }
512
501
 
502
+ /**
503
+ * The watched path of the innermost open object, from the container stack.
504
+ * @param kinds - the kind of each open container, outermost first
505
+ * @param labels - the key or index each open container sits under
506
+ * @returns the path, or null when keys there are not watched
507
+ */
508
+ function keyPathOf(kinds: readonly ("object" | "array")[], labels: readonly string[]): KeyPath | null {
509
+ switch (kinds.length) {
510
+ case 1:
511
+ return kinds[0] === "object" ? "" : null;
512
+ case 2:
513
+ return nestedObjectPath(kinds, labels);
514
+ case 3:
515
+ return sectionElementPath(kinds, labels);
516
+ case 5:
517
+ return graphsMemberPath(kinds, labels);
518
+ default:
519
+ return null;
520
+ }
521
+ }
522
+
523
+ /**
524
+ * At depth 2: the first element of a top-level array, or the `graph` / `options` object.
525
+ * @param kinds - the kind of each open container, outermost first
526
+ * @param labels - the key or index each open container sits under
527
+ * @returns the path, or null
528
+ */
529
+ function nestedObjectPath(kinds: readonly ("object" | "array")[], labels: readonly string[]): KeyPath | null {
530
+ if (kinds[0] === "array" && kinds[1] === "object" && labels[1] === "0") {
531
+ return "nodes[0]";
532
+ }
533
+ if (kinds[0] === "object" && kinds[1] === "object") {
534
+ return labels[1] === "graph" || labels[1] === "options" ? labels[1] : null;
535
+ }
536
+ return null;
537
+ }
538
+
539
+ /**
540
+ * At depth 3: the first element of the root's `nodes`, `edges` or `links` array.
541
+ * @param kinds - the kind of each open container, outermost first
542
+ * @param labels - the key or index each open container sits under
543
+ * @returns the path, or null
544
+ */
545
+ function sectionElementPath(kinds: readonly ("object" | "array")[], labels: readonly string[]): KeyPath | null {
546
+ if (kinds[0] !== "object" || kinds[1] !== "array" || kinds[2] !== "object" || labels[2] !== "0") {
547
+ return null;
548
+ }
549
+ const section = labels[1];
550
+ return section === "nodes" || section === "edges" || section === "links" ? `${section}[0]` : null;
551
+ }
552
+
553
+ /**
554
+ * At depth 5: any node or edge of the first graph of the root's `graphs` array.
555
+ * @param kinds - the kind of each open container, outermost first
556
+ * @param labels - the key or index each open container sits under
557
+ * @returns the path, or null
558
+ */
559
+ function graphsMemberPath(kinds: readonly ("object" | "array")[], labels: readonly string[]): KeyPath | null {
560
+ if (kinds.join() !== "object,array,object,array,object" || labels[1] !== "graphs" || labels[2] !== "0") {
561
+ return null;
562
+ }
563
+ if (labels[3] === "nodes") {
564
+ return "graphs[0].nodes[]";
565
+ }
566
+ return labels[3] === "edges" ? "graphs[0].edges[]" : null;
567
+ }
568
+
513
569
  /**
514
570
  * The index of the closing quote of a JSON string starting after its opening quote.
515
571
  * @param text - the text