@graphty/graph-io 0.3.39 → 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 (213) 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-BpYMQZVa.js → escape-DBb8zuNE.js} +4 -3
  6. package/dist/chunks/escape-DBb8zuNE.js.map +1 -0
  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-CNJZF_BN.js → exporter-BqtmlxNr.js} +545 -459
  10. package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
  11. package/dist/chunks/{exporter-BqKs1Q12.js → exporter-CS35R6Fg.js} +6 -5
  12. package/dist/chunks/exporter-CS35R6Fg.js.map +1 -0
  13. package/dist/chunks/{exporter-DXqCPNrI.js → exporter-D8r1N2dU.js} +67 -42
  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-CSt-gaYE.js → importer-BKC7AmCA.js} +42 -29
  18. package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
  19. package/dist/chunks/{importer-DvFznt5-.js → importer-BdWHmlmG.js} +112 -62
  20. package/dist/chunks/importer-BdWHmlmG.js.map +1 -0
  21. package/dist/chunks/{importer-CqgYq4Ld.js → importer-BfbzWk6k.js} +202 -181
  22. package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
  23. package/dist/chunks/{importer-PVXkeoaS.js → importer-BmaZcskq.js} +79 -66
  24. package/dist/chunks/importer-BmaZcskq.js.map +1 -0
  25. package/dist/chunks/{importer-B7Ir269L.js → importer-DviEsHy4.js} +118 -72
  26. package/dist/chunks/importer-DviEsHy4.js.map +1 -0
  27. package/dist/chunks/{importer--tQQsY4W.js → importer-S-abxJHn.js} +5 -5
  28. package/dist/chunks/{importer--tQQsY4W.js.map → importer-S-abxJHn.js.map} +1 -1
  29. package/dist/chunks/{importer-BfJ1jnNr.js → importer-r29cWnEf.js} +28 -13
  30. package/dist/chunks/importer-r29cWnEf.js.map +1 -0
  31. package/dist/chunks/{json-elements-CXs4z1iL.js → json-elements-DdF-etPT.js} +7 -5
  32. package/dist/chunks/{json-elements-CXs4z1iL.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-C9jd7yLD.js → records-Uc3LkABx.js} +150 -106
  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 +199 -99
  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/json-elements.d.ts.map +1 -1
  63. package/dist/src/common/json-elements.js +4 -2
  64. package/dist/src/common/json-elements.js.map +1 -1
  65. package/dist/src/common/text.d.ts +7 -0
  66. package/dist/src/common/text.d.ts.map +1 -1
  67. package/dist/src/common/text.js +15 -7
  68. package/dist/src/common/text.js.map +1 -1
  69. package/dist/src/common/xml.d.ts.map +1 -1
  70. package/dist/src/common/xml.js +2 -1
  71. package/dist/src/common/xml.js.map +1 -1
  72. package/dist/src/common/zip.d.ts.map +1 -1
  73. package/dist/src/common/zip.js +36 -19
  74. package/dist/src/common/zip.js.map +1 -1
  75. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  76. package/dist/src/formats/csv/exporter.js +77 -25
  77. package/dist/src/formats/csv/exporter.js.map +1 -1
  78. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  79. package/dist/src/formats/csv/importer.js +16 -6
  80. package/dist/src/formats/csv/importer.js.map +1 -1
  81. package/dist/src/formats/csv/records.d.ts.map +1 -1
  82. package/dist/src/formats/csv/records.js +156 -110
  83. package/dist/src/formats/csv/records.js.map +1 -1
  84. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  85. package/dist/src/formats/cx/importer.js +108 -53
  86. package/dist/src/formats/cx/importer.js.map +1 -1
  87. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/cx2/exporter.js +2 -1
  89. package/dist/src/formats/cx2/exporter.js.map +1 -1
  90. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  91. package/dist/src/formats/cx2/importer.js +28 -6
  92. package/dist/src/formats/cx2/importer.js.map +1 -1
  93. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  94. package/dist/src/formats/cys/tables.js +11 -8
  95. package/dist/src/formats/cys/tables.js.map +1 -1
  96. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/exporter.js +69 -39
  98. package/dist/src/formats/gexf/exporter.js.map +1 -1
  99. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  100. package/dist/src/formats/gexf/importer.js +138 -50
  101. package/dist/src/formats/gexf/importer.js.map +1 -1
  102. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  103. package/dist/src/formats/gml/exporter.js +51 -35
  104. package/dist/src/formats/gml/exporter.js.map +1 -1
  105. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  106. package/dist/src/formats/gml/importer.js +103 -58
  107. package/dist/src/formats/gml/importer.js.map +1 -1
  108. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  109. package/dist/src/formats/gml/syntax.js +17 -8
  110. package/dist/src/formats/gml/syntax.js.map +1 -1
  111. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/exporter.js +36 -25
  113. package/dist/src/formats/graphml/exporter.js.map +1 -1
  114. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  115. package/dist/src/formats/graphml/importer.js +20 -15
  116. package/dist/src/formats/graphml/importer.js.map +1 -1
  117. package/dist/src/formats/graphml/tree.js +23 -14
  118. package/dist/src/formats/graphml/tree.js.map +1 -1
  119. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  120. package/dist/src/formats/graphml/yfiles.js +22 -10
  121. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  122. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  123. package/dist/src/formats/json/exporter.js +38 -21
  124. package/dist/src/formats/json/exporter.js.map +1 -1
  125. package/dist/src/formats/json/importer.d.ts.map +1 -1
  126. package/dist/src/formats/json/importer.js +536 -343
  127. package/dist/src/formats/json/importer.js.map +1 -1
  128. package/dist/src/formats/json/obographs.js +29 -17
  129. package/dist/src/formats/json/obographs.js.map +1 -1
  130. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  131. package/dist/src/formats/neo4j/exporter.js +73 -43
  132. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  133. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  134. package/dist/src/formats/neo4j/header.js +37 -16
  135. package/dist/src/formats/neo4j/header.js.map +1 -1
  136. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  137. package/dist/src/formats/neo4j/importer.js +21 -11
  138. package/dist/src/formats/neo4j/importer.js.map +1 -1
  139. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  140. package/dist/src/formats/obo/exporter.js +18 -7
  141. package/dist/src/formats/obo/exporter.js.map +1 -1
  142. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  143. package/dist/src/formats/pajek/exporter.js +142 -73
  144. package/dist/src/formats/pajek/exporter.js.map +1 -1
  145. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  146. package/dist/src/formats/pajek/importer.js +26 -15
  147. package/dist/src/formats/pajek/importer.js.map +1 -1
  148. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  149. package/dist/src/formats/pajek/syntax.js +85 -58
  150. package/dist/src/formats/pajek/syntax.js.map +1 -1
  151. package/dist/src/formats/xgmml/emit.d.ts +13 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +112 -50
  154. package/dist/src/formats/xgmml/emit.js.map +1 -1
  155. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  156. package/dist/src/formats/xgmml/exporter.js +92 -34
  157. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  158. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  159. package/dist/src/formats/xgmml/importer.js +38 -18
  160. package/dist/src/formats/xgmml/importer.js.map +1 -1
  161. package/dist/src/sniff.js +109 -57
  162. package/dist/src/sniff.js.map +1 -1
  163. package/dist/xgmml.js +2 -2
  164. package/package.json +2 -2
  165. package/src/common/attributes.ts +59 -19
  166. package/src/common/input.ts +216 -102
  167. package/src/common/json-elements.ts +4 -2
  168. package/src/common/text.ts +24 -15
  169. package/src/common/xml.ts +2 -1
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +103 -39
  172. package/src/formats/csv/importer.ts +17 -6
  173. package/src/formats/csv/records.ts +162 -109
  174. package/src/formats/cx/importer.ts +140 -58
  175. package/src/formats/cx2/exporter.ts +2 -1
  176. package/src/formats/cx2/importer.ts +34 -10
  177. package/src/formats/cys/tables.ts +11 -10
  178. package/src/formats/gexf/exporter.ts +71 -38
  179. package/src/formats/gexf/importer.ts +173 -63
  180. package/src/formats/gml/exporter.ts +78 -82
  181. package/src/formats/gml/importer.ts +119 -68
  182. package/src/formats/gml/syntax.ts +18 -8
  183. package/src/formats/graphml/exporter.ts +46 -27
  184. package/src/formats/graphml/importer.ts +21 -13
  185. package/src/formats/graphml/tree.ts +22 -12
  186. package/src/formats/graphml/yfiles.ts +23 -10
  187. package/src/formats/json/exporter.ts +40 -21
  188. package/src/formats/json/importer.ts +701 -385
  189. package/src/formats/json/obographs.ts +35 -15
  190. package/src/formats/neo4j/exporter.ts +79 -41
  191. package/src/formats/neo4j/header.ts +50 -16
  192. package/src/formats/neo4j/importer.ts +21 -10
  193. package/src/formats/obo/exporter.ts +34 -17
  194. package/src/formats/pajek/exporter.ts +180 -117
  195. package/src/formats/pajek/importer.ts +27 -15
  196. package/src/formats/pajek/syntax.ts +99 -58
  197. package/src/formats/xgmml/emit.ts +122 -50
  198. package/src/formats/xgmml/exporter.ts +103 -35
  199. package/src/formats/xgmml/importer.ts +45 -23
  200. package/src/sniff.ts +115 -59
  201. package/dist/chunks/escape-BpYMQZVa.js.map +0 -1
  202. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  203. package/dist/chunks/exporter-BqKs1Q12.js.map +0 -1
  204. package/dist/chunks/exporter-CNJZF_BN.js.map +0 -1
  205. package/dist/chunks/exporter-DXqCPNrI.js.map +0 -1
  206. package/dist/chunks/importer-B7Ir269L.js.map +0 -1
  207. package/dist/chunks/importer-BfJ1jnNr.js.map +0 -1
  208. package/dist/chunks/importer-CSt-gaYE.js.map +0 -1
  209. package/dist/chunks/importer-CqgYq4Ld.js.map +0 -1
  210. package/dist/chunks/importer-DvFznt5-.js.map +0 -1
  211. package/dist/chunks/importer-PVXkeoaS.js.map +0 -1
  212. package/dist/chunks/records-C9jd7yLD.js.map +0 -1
  213. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -1328,20 +1328,15 @@ class GexfReader implements XmlHandler {
1328
1328
  }
1329
1329
 
1330
1330
  /**
1331
- * Declare one `<attribute id title type>` with its `<default>` and `<options>` (design
1332
- * sections 5.1, 5.5, 5.6). The edge attribute titled `weightFrom` is the weight and gets no
1333
- * column; its declaration is recorded in meta.weightOrigin.
1334
- * @param pending - the attribute as read
1331
+ * Whether an attribute was already declared (warning that the first declaration is kept), and
1332
+ * a warning when values that use it came before its declaration.
1333
+ * @param domain - node or edge
1334
+ * @param id - the attribute id
1335
+ * @param where - the report location
1336
+ * @returns true when the declaration is a duplicate and must be skipped
1335
1337
  */
1336
- private declareOne(pending: PendingAttribute): void {
1337
- const { report, sink, options } = this;
1338
- const { domain, dynamic, attrs } = pending;
1339
- const id = attrs.get("id");
1340
- const where = { line: pending.line, element: id ?? null };
1341
- if (id === undefined) {
1342
- report.error("validation-error", ATTRIBUTE_ID_CODE, "<attribute> without an id", where);
1343
- return;
1344
- }
1338
+ private isDuplicateAttribute(domain: "node" | "edge", id: string, where: IssueLocation): boolean {
1339
+ const { report } = this;
1345
1340
  const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
1346
1341
  if (tracked.has(id) || (domain === "edge" && this.weightAttr !== null && this.weightAttr.id === id)) {
1347
1342
  report.warning(
@@ -1350,7 +1345,7 @@ class GexfReader implements XmlHandler {
1350
1345
  `${domain} attribute "${id}" is declared twice; the first declaration is kept`,
1351
1346
  where,
1352
1347
  );
1353
- return;
1348
+ return true;
1354
1349
  }
1355
1350
  if (this.unknownAttributes.has(`${domain}:${id}`)) {
1356
1351
  report.warning(
@@ -1360,6 +1355,68 @@ class GexfReader implements XmlHandler {
1360
1355
  where,
1361
1356
  );
1362
1357
  }
1358
+ return false;
1359
+ }
1360
+
1361
+ /**
1362
+ * Whether an edge attribute becomes THE weight: the first attribute of the weight's title does
1363
+ * (declared here), a later one only warns and stays a plain column.
1364
+ * @param attribute - the attribute's domain, id, display name, title and type
1365
+ * @param attribute.domain - node or edge
1366
+ * @param attribute.id - the attribute id
1367
+ * @param attribute.name - the column name
1368
+ * @param attribute.title - the declared title, or null
1369
+ * @param attribute.type - the declared type, or null
1370
+ * @param where - the report location
1371
+ * @returns true when the attribute was declared as the weight
1372
+ */
1373
+ private claimsWeight(
1374
+ attribute: { domain: "node" | "edge"; id: string; name: string; title: string | null; type: string | null },
1375
+ where: IssueLocation,
1376
+ ): boolean {
1377
+ const { report, sink, options } = this;
1378
+ const { domain, id, name, title, type } = attribute;
1379
+ if (domain !== "edge" || !isWeightField(name, options.weightFrom)) {
1380
+ return false;
1381
+ }
1382
+ if (this.weightAttr !== null) {
1383
+ // the first attribute of that title stays THE weight; this one becomes a plain column
1384
+ report.warning(
1385
+ "validation-error",
1386
+ DUPLICATE_ATTRIBUTE_CODE,
1387
+ `edge attribute "${id}" is also titled ${name}; attribute "${this.weightAttr.id}" is the weight, "${id}" is kept as a column`,
1388
+ where,
1389
+ );
1390
+ return false;
1391
+ }
1392
+ const declared = declareAttribute({ format: GEXF_FORMAT, id, title, type, long: options.long });
1393
+ for (const issue of declared.issues) {
1394
+ report.warning(issue.category, issue.code, issue.message, where);
1395
+ }
1396
+ this.weightAttr = { id, spec: declared.spec, table: INVALID_INDEX as ExtensionHandle };
1397
+ sink.setMeta({ weightOrigin: { format: GEXF_FORMAT, id, title, type, namespace: null } });
1398
+ return true;
1399
+ }
1400
+
1401
+ /**
1402
+ * Declare one `<attribute id title type>` with its `<default>` and `<options>` (design
1403
+ * sections 5.1, 5.5, 5.6). The edge attribute titled `weightFrom` is the weight and gets no
1404
+ * column; its declaration is recorded in meta.weightOrigin.
1405
+ * @param pending - the attribute as read
1406
+ */
1407
+ private declareOne(pending: PendingAttribute): void {
1408
+ const { report, sink, options } = this;
1409
+ const { domain, dynamic, attrs } = pending;
1410
+ const id = attrs.get("id");
1411
+ const where = { line: pending.line, element: id ?? null };
1412
+ if (id === undefined) {
1413
+ report.error("validation-error", ATTRIBUTE_ID_CODE, "<attribute> without an id", where);
1414
+ return;
1415
+ }
1416
+ const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
1417
+ if (this.isDuplicateAttribute(domain, id, where)) {
1418
+ return;
1419
+ }
1363
1420
  const title = attrs.get("title") ?? null;
1364
1421
  const type = attrs.get("type") ?? null;
1365
1422
  if (type === null) {
@@ -1371,21 +1428,7 @@ class GexfReader implements XmlHandler {
1371
1428
  );
1372
1429
  }
1373
1430
  const name = title !== null && title.length > 0 ? title : id;
1374
- if (domain === "edge" && this.weightAttr !== null && isWeightField(name, options.weightFrom)) {
1375
- // the first attribute of that title stays THE weight; this one becomes a plain column
1376
- report.warning(
1377
- "validation-error",
1378
- DUPLICATE_ATTRIBUTE_CODE,
1379
- `edge attribute "${id}" is also titled ${name}; attribute "${this.weightAttr.id}" is the weight, "${id}" is kept as a column`,
1380
- where,
1381
- );
1382
- } else if (domain === "edge" && isWeightField(name, options.weightFrom)) {
1383
- const declared = declareAttribute({ format: GEXF_FORMAT, id, title, type, long: options.long });
1384
- for (const issue of declared.issues) {
1385
- report.warning(issue.category, issue.code, issue.message, where);
1386
- }
1387
- this.weightAttr = { id, spec: declared.spec, table: INVALID_INDEX as ExtensionHandle };
1388
- sink.setMeta({ weightOrigin: { format: GEXF_FORMAT, id, title, type, namespace: null } });
1431
+ if (this.claimsWeight({ domain, id, name, title, type }, where)) {
1389
1432
  return;
1390
1433
  }
1391
1434
  const declaredNames = domain === "node" ? this.declaredNodeNames : this.declaredEdgeNames;
@@ -1717,6 +1760,15 @@ class GexfReader implements XmlHandler {
1717
1760
  report.recordError(err, ref.where);
1718
1761
  }
1719
1762
  }
1763
+ this.resolveParentsLists(lists);
1764
+ }
1765
+
1766
+ /**
1767
+ * Resolve the `<parents>` lists read so far into the parents column.
1768
+ * @param lists - the queued `<parents>` lists
1769
+ */
1770
+ private resolveParentsLists(lists: readonly (typeof this.parentsRefs)[number][]): void {
1771
+ const { sink, report } = this;
1720
1772
  for (const ref of lists) {
1721
1773
  const indices: number[] = [];
1722
1774
  for (const id of ref.ids) {
@@ -1763,7 +1815,7 @@ class GexfReader implements XmlHandler {
1763
1815
  * @param line - the line
1764
1816
  */
1765
1817
  private beginEdge(attrs: ReadonlyMap<string, string>, line: number): void {
1766
- const { report, sink, options } = this;
1818
+ const { report, sink } = this;
1767
1819
  this.checkAbort();
1768
1820
  this.ctx.push(Ctx.Edge);
1769
1821
  this.edge = null;
@@ -1816,18 +1868,7 @@ class GexfReader implements XmlHandler {
1816
1868
  try {
1817
1869
  const source = this.coerceId(sourceText, where);
1818
1870
  const target = this.coerceId(targetText, where);
1819
- if (!options.addMissingNodes) {
1820
- if (sink.indexOf(source) === INVALID_INDEX) {
1821
- throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${sourceText}"`, {
1822
- id: source,
1823
- });
1824
- }
1825
- if (sink.indexOf(target) === INVALID_INDEX) {
1826
- throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${targetText}"`, {
1827
- id: target,
1828
- });
1829
- }
1830
- }
1871
+ this.requireDeclaredEndpoints(source, target, sourceText, targetText);
1831
1872
  const weight = this.xmlWeight(attrs, where);
1832
1873
  const before = sink.edgeCount;
1833
1874
  index = this.resolver.addEdge(source, target, kind, weight, where);
@@ -1846,6 +1887,47 @@ class GexfReader implements XmlHandler {
1846
1887
  frame.mirror = this.resolver.lastMirror;
1847
1888
  frame.xmlWeight = attrs.get("weight") ?? null;
1848
1889
  this.edge = frame;
1890
+ this.writeEdgeColumns(attrs, index, idText, where);
1891
+ }
1892
+
1893
+ /**
1894
+ * Throw when addMissingNodes is off and an endpoint names an undeclared node.
1895
+ * @param source - the coerced source id
1896
+ * @param target - the coerced target id
1897
+ * @param sourceText - the source as written
1898
+ * @param targetText - the target as written
1899
+ */
1900
+ private requireDeclaredEndpoints(source: NodeId, target: NodeId, sourceText: string, targetText: string): void {
1901
+ const { sink, options } = this;
1902
+ if (options.addMissingNodes) {
1903
+ return;
1904
+ }
1905
+ if (sink.indexOf(source) === INVALID_INDEX) {
1906
+ throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${sourceText}"`, {
1907
+ id: source,
1908
+ });
1909
+ }
1910
+ if (sink.indexOf(target) === INVALID_INDEX) {
1911
+ throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${targetText}"`, {
1912
+ id: target,
1913
+ });
1914
+ }
1915
+ }
1916
+
1917
+ /**
1918
+ * Write the id, label, kind and lifetime columns of an added edge; a failure is reported.
1919
+ * @param attrs - the edge's attributes
1920
+ * @param index - the edge's index
1921
+ * @param idText - the edge's id, or undefined
1922
+ * @param where - the edge's location
1923
+ */
1924
+ private writeEdgeColumns(
1925
+ attrs: ReadonlyMap<string, string>,
1926
+ index: number,
1927
+ idText: string | undefined,
1928
+ where: IssueLocation,
1929
+ ): void {
1930
+ const { report, sink } = this;
1849
1931
  try {
1850
1932
  if (idText !== undefined) {
1851
1933
  this.edgeIdColumn.set(sink, report, index, idText, where);
@@ -2555,35 +2637,18 @@ class GexfReader implements XmlHandler {
2555
2637
  case "color":
2556
2638
  columns.color.set(sink, report, index, parseColor(attrs), where);
2557
2639
  break;
2558
- case "position": {
2559
- const x = vizNumber(attrs, "x");
2560
- const y = vizNumber(attrs, "y");
2561
- const z = attrs.has("z") ? vizNumber(attrs, "z") : 0;
2562
- this.positionColumn ??= new LazyColumn("node", {
2563
- ...NODE_DECLS.position,
2564
- extra: { ...NODE_DECLS.position.extra, sourceDims: attrs.has("z") ? 3 : 2 },
2565
- });
2566
- this.positionColumn.set(sink, report, index, [x, y, z], where);
2640
+ case "position":
2641
+ this.writePosition(attrs, index, where);
2567
2642
  break;
2568
- }
2569
2643
  case "size":
2570
2644
  this.sizeColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
2571
2645
  break;
2572
2646
  case "thickness":
2573
2647
  this.thicknessColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
2574
2648
  break;
2575
- case "shape": {
2576
- const value = attrs.get("value");
2577
- if (value === undefined) {
2578
- throw new GraphFormatError("E_COLUMN_TYPE", "<viz:shape> without a value", { element: name });
2579
- }
2580
- columns.shape.set(sink, report, index, value, where);
2581
- const uri = attrs.get("uri");
2582
- if (domain === "node" && uri !== undefined) {
2583
- this.shapeUriColumn.set(sink, report, index, uri, where);
2584
- }
2649
+ case "shape":
2650
+ this.writeShape(name, attrs, domain, index, where);
2585
2651
  break;
2586
- }
2587
2652
  default:
2588
2653
  break;
2589
2654
  }
@@ -2595,6 +2660,51 @@ class GexfReader implements XmlHandler {
2595
2660
  }
2596
2661
  }
2597
2662
 
2663
+ /**
2664
+ * Write a `<viz:position>`.
2665
+ * @param attrs - its attributes
2666
+ * @param index - the node's index
2667
+ * @param where - the node's location
2668
+ */
2669
+ private writePosition(attrs: ReadonlyMap<string, string>, index: number, where: IssueLocation): void {
2670
+ const x = vizNumber(attrs, "x");
2671
+ const y = vizNumber(attrs, "y");
2672
+ const z = attrs.has("z") ? vizNumber(attrs, "z") : 0;
2673
+ this.positionColumn ??= new LazyColumn("node", {
2674
+ ...NODE_DECLS.position,
2675
+ extra: { ...NODE_DECLS.position.extra, sourceDims: attrs.has("z") ? 3 : 2 },
2676
+ });
2677
+ this.positionColumn.set(this.sink, this.report, index, [x, y, z], where);
2678
+ }
2679
+
2680
+ /**
2681
+ * Write a `<viz:shape>` and, on a node, its `uri`.
2682
+ * @param name - the element name as written
2683
+ * @param attrs - its attributes
2684
+ * @param domain - node or edge
2685
+ * @param index - the element's index
2686
+ * @param where - the element's location
2687
+ */
2688
+ private writeShape(
2689
+ name: string,
2690
+ attrs: ReadonlyMap<string, string>,
2691
+ domain: "node" | "edge",
2692
+ index: number,
2693
+ where: IssueLocation,
2694
+ ): void {
2695
+ const { sink, report } = this;
2696
+ const columns = domain === "node" ? this.nodeColumns : this.edgeColumns;
2697
+ const value = attrs.get("value");
2698
+ if (value === undefined) {
2699
+ throw new GraphFormatError("E_COLUMN_TYPE", "<viz:shape> without a value", { element: name });
2700
+ }
2701
+ columns.shape.set(sink, report, index, value, where);
2702
+ const uri = attrs.get("uri");
2703
+ if (domain === "node" && uri !== undefined) {
2704
+ this.shapeUriColumn.set(sink, report, index, uri, where);
2705
+ }
2706
+ }
2707
+
2598
2708
  // ---------------------------------------------------------------- helpers
2599
2709
 
2600
2710
  /**
@@ -420,6 +420,20 @@ function note(code: string, message: string, column: string | null = null, count
420
420
  return Object.freeze({ code, message, column, count });
421
421
  }
422
422
 
423
+ /**
424
+ * Count a kind of json value once per row.
425
+ * @param seen - the flags already counted for this row
426
+ * @param flag - the flag of the kind
427
+ * @param stats - the counters
428
+ * @param counter - the counter of the kind
429
+ */
430
+ function countOnce(seen: SeenFlags, flag: keyof SeenFlags, stats: JsonStats, counter: keyof JsonStats): void {
431
+ if (!seen[flag]) {
432
+ seen[flag] = true;
433
+ stats[counter]++;
434
+ }
435
+ }
436
+
423
437
  /**
424
438
  * Inspect a JSON value for what GML cannot write exactly.
425
439
  * @param value - the value
@@ -429,24 +443,15 @@ function note(code: string, message: string, column: string | null = null, count
429
443
  */
430
444
  function inspectJson(value: unknown, stats: JsonStats, inArray: boolean, seen: SeenFlags): void {
431
445
  if (value === null) {
432
- if (!seen.nulls) {
433
- seen.nulls = true;
434
- stats.nulls++;
435
- }
446
+ countOnce(seen, "nulls", stats, "nulls");
436
447
  return;
437
448
  }
438
449
  switch (typeof value) {
439
450
  case "number":
440
- if (!seen.numbers) {
441
- seen.numbers = true;
442
- stats.numbers++;
443
- }
451
+ countOnce(seen, "numbers", stats, "numbers");
444
452
  return;
445
453
  case "boolean":
446
- if (!seen.booleans) {
447
- seen.booleans = true;
448
- stats.booleans++;
449
- }
454
+ countOnce(seen, "booleans", stats, "booleans");
450
455
  return;
451
456
  case "string":
452
457
  return;
@@ -454,9 +459,8 @@ function inspectJson(value: unknown, stats: JsonStats, inArray: boolean, seen: S
454
459
  break;
455
460
  }
456
461
  if (Array.isArray(value)) {
457
- if (inArray && !seen.nested) {
458
- seen.nested = true;
459
- stats.nestedArrays++;
462
+ if (inArray) {
463
+ countOnce(seen, "nested", stats, "nestedArrays");
460
464
  }
461
465
  for (const item of value) {
462
466
  inspectJson(item, stats, true, seen);
@@ -472,6 +476,29 @@ function inspectJson(value: unknown, stats: JsonStats, inArray: boolean, seen: S
472
476
  }
473
477
  }
474
478
 
479
+ /**
480
+ * A row count in words.
481
+ * @param count - the count
482
+ * @returns the count and the word row, pluralised
483
+ */
484
+ function rowCount(count: number): string {
485
+ return `${count} row${plural(count)}`;
486
+ }
487
+
488
+ /**
489
+ * Record a note for a counter that is not zero.
490
+ * @param notes - where to record
491
+ * @param count - the counter
492
+ * @param code - the note code
493
+ * @param text - the note message
494
+ * @param column - the column name
495
+ */
496
+ function pushCounted(notes: LossNote[], count: number, code: string, text: string, column: string): void {
497
+ if (count > 0) {
498
+ notes.push(note(code, text, column, count));
499
+ }
500
+ }
501
+
475
502
  /**
476
503
  * The GML-specific notes of one table's json columns.
477
504
  * @param columns - the written columns
@@ -487,73 +514,42 @@ function jsonNotes(columns: readonly WrittenColumn[], label: string, plan: GmlEx
487
514
  }
488
515
  const { name } = column.meta;
489
516
  const where = `${label} column "${name}"`;
490
- if (stats.numbers > 0) {
491
- notes.push(
492
- note(
493
- RECORD_NUMBER_TYPE_CODE,
494
- `${where} holds numbers in ${stats.numbers} row${plural(stats.numbers)}; GML records cannot keep the int / real distinction`,
495
- name,
496
- stats.numbers,
497
- ),
498
- );
499
- }
500
- if (stats.booleans > 0) {
501
- notes.push(
502
- note(
503
- RECORD_BOOLEAN_CODE,
504
- `${where} holds booleans in ${stats.booleans} row${plural(stats.booleans)}; written as 1 / 0`,
505
- name,
506
- stats.booleans,
507
- ),
508
- );
509
- }
510
- if (stats.nulls > 0) {
511
- notes.push(
512
- note(
513
- RECORD_NULL_CODE,
514
- `${where} holds nulls in ${stats.nulls} row${plural(stats.nulls)}; GML has no null, the key is omitted`,
515
- name,
516
- stats.nulls,
517
- ),
518
- );
519
- }
520
- if (stats.arrays > 0) {
521
- notes.push(
522
- note(
523
- JSON_ARRAY_CODE,
524
- `${where} holds arrays as values in ${stats.arrays} row${plural(stats.arrays)}; written as repeated keys, they re-import as a list`,
525
- name,
526
- stats.arrays,
527
- ),
528
- );
529
- }
530
- if (stats.nestedArrays > 0) {
531
- notes.push(
532
- note(
533
- NESTED_ARRAY_CODE,
534
- `${where} holds arrays nested in arrays in ${stats.nestedArrays} row${plural(stats.nestedArrays)}; GML cannot write them and the save fails`,
535
- name,
536
- stats.nestedArrays,
537
- ),
538
- );
539
- }
540
- if (stats.invalidKeys > 0) {
541
- notes.push(
542
- plan.mangle
543
- ? note(
544
- KEY_MANGLED_CODE,
545
- `${where}: ${stats.invalidKeys} record key${plural(stats.invalidKeys)} ${agree(stats.invalidKeys, "is", "are")} not GML keys and are rewritten`,
546
- name,
547
- stats.invalidKeys,
548
- )
549
- : note(
550
- INVALID_KEY_CODE,
551
- `${where}: ${stats.invalidKeys} record key${plural(stats.invalidKeys)} ${agree(stats.invalidKeys, "is", "are")} not GML keys; the save fails unless sanitizeKeys is "mangle"`,
552
- name,
553
- stats.invalidKeys,
554
- ),
555
- );
556
- }
517
+ const counted = (count: number, code: string, text: string): void => {
518
+ pushCounted(notes, count, code, text, name);
519
+ };
520
+ counted(
521
+ stats.numbers,
522
+ RECORD_NUMBER_TYPE_CODE,
523
+ `${where} holds numbers in ${rowCount(stats.numbers)}; GML records cannot keep the int / real distinction`,
524
+ );
525
+ counted(
526
+ stats.booleans,
527
+ RECORD_BOOLEAN_CODE,
528
+ `${where} holds booleans in ${rowCount(stats.booleans)}; written as 1 / 0`,
529
+ );
530
+ counted(
531
+ stats.nulls,
532
+ RECORD_NULL_CODE,
533
+ `${where} holds nulls in ${rowCount(stats.nulls)}; GML has no null, the key is omitted`,
534
+ );
535
+ counted(
536
+ stats.arrays,
537
+ JSON_ARRAY_CODE,
538
+ `${where} holds arrays as values in ${rowCount(stats.arrays)}; written as repeated keys, they re-import as a list`,
539
+ );
540
+ counted(
541
+ stats.nestedArrays,
542
+ NESTED_ARRAY_CODE,
543
+ `${where} holds arrays nested in arrays in ${rowCount(stats.nestedArrays)}; GML cannot write them and the save fails`,
544
+ );
545
+ const keys = `${stats.invalidKeys} record key${plural(stats.invalidKeys)} ${agree(stats.invalidKeys, "is", "are")} not GML keys`;
546
+ counted(
547
+ stats.invalidKeys,
548
+ plan.mangle ? KEY_MANGLED_CODE : INVALID_KEY_CODE,
549
+ plan.mangle
550
+ ? `${where}: ${keys} and are rewritten`
551
+ : `${where}: ${keys}; the save fails unless sanitizeKeys is "mangle"`,
552
+ );
557
553
  }
558
554
  }
559
555