@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
@@ -575,6 +575,22 @@ function isUnset(text: string, quoted: boolean | undefined): boolean {
575
575
  return quoted !== true && isBlank(text);
576
576
  }
577
577
 
578
+ /**
579
+ * The number of set neighbor cells of an adjacency row (every cell after the first).
580
+ * @param row - the cells
581
+ * @param quoted - whether each cell was quoted
582
+ * @returns the set neighbor cells
583
+ */
584
+ function setNeighborCells(row: readonly string[], quoted: readonly boolean[]): number {
585
+ let neighbors = 0;
586
+ for (let k = 1; k < row.length; k++) {
587
+ if (!isUnset(row[k], quoted[k])) {
588
+ neighbors++;
589
+ }
590
+ }
591
+ return neighbors;
592
+ }
593
+
578
594
  /** Rows between two checks of the cancellation signal on an in-memory input. */
579
595
  const ABORT_CHECK_INTERVAL = 64;
580
596
 
@@ -1520,12 +1536,7 @@ class TableReader {
1520
1536
  const { where } = this;
1521
1537
  where.line = line;
1522
1538
  where.element = null;
1523
- let neighbors = 0;
1524
- for (let k = 1; k < row.length; k++) {
1525
- if (!isUnset(row[k], quoted[k])) {
1526
- neighbors++;
1527
- }
1528
- }
1539
+ const neighbors = setNeighborCells(row, quoted);
1529
1540
  if (isUnset(row[0], quoted[0])) {
1530
1541
  report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
1531
1542
  counts.skippedNodes++;
@@ -410,16 +410,7 @@ export function sniffDelimiter(
410
410
  if (rows.length === 0) {
411
411
  continue;
412
412
  }
413
- let total = 0;
414
- let delta = 0;
415
- for (let i = 0; i < rows.length; i++) {
416
- const count = rows[i].length;
417
- total += count;
418
- if (i > 0) {
419
- delta += Math.abs(count - rows[i - 1].length);
420
- }
421
- }
422
- const average = total / rows.length;
413
+ const { average, delta } = rowWidths(rows);
423
414
  if (average > 1.99 && delta < bestDelta) {
424
415
  best = delimiter;
425
416
  bestDelta = delta;
@@ -428,6 +419,24 @@ export function sniffDelimiter(
428
419
  return best;
429
420
  }
430
421
 
422
+ /**
423
+ * The average width of the rows and the sum of the width changes from each row to the next.
424
+ * @param rows - the rows (at least one)
425
+ * @returns the average and the delta
426
+ */
427
+ function rowWidths(rows: readonly (readonly string[])[]): { average: number; delta: number } {
428
+ let total = 0;
429
+ let delta = 0;
430
+ for (let i = 0; i < rows.length; i++) {
431
+ const count = rows[i].length;
432
+ total += count;
433
+ if (i > 0) {
434
+ delta += Math.abs(count - rows[i - 1].length);
435
+ }
436
+ }
437
+ return { average: total / rows.length, delta };
438
+ }
439
+
431
440
  /**
432
441
  * Whether a parsed row is a blank line: one cell holding only whitespace.
433
442
  * @param row - the row
@@ -217,24 +217,25 @@ export function cellAtt(
217
217
  namespace: string | null,
218
218
  hidden: boolean,
219
219
  ): AttRec | null {
220
+ const cell = { line, namespace, hidden };
220
221
  if (text.startsWith("=")) {
221
- return att(column.name, column.list ? "List" : column.type, null, text, line, namespace, hidden, true);
222
+ return att(column.name, column.list ? "List" : column.type, null, text, true, cell);
222
223
  }
223
224
  if (!column.list) {
224
225
  if (text.length === 0 && column.type !== "String") {
225
226
  return null;
226
227
  }
227
- return att(column.name, column.type, null, text, line, namespace, hidden, false);
228
+ return att(column.name, column.type, null, text, false, cell);
228
229
  }
229
230
  if (text.length === 0 && column.type !== "String") {
230
231
  return null;
231
232
  }
232
- const list = att(column.name, "List", column.type, null, line, namespace, hidden, false);
233
+ const list = att(column.name, "List", column.type, null, false, cell);
233
234
  for (const item of javaSplitLines(text)) {
234
235
  if (item.length === 0 && column.type !== "String") {
235
236
  continue;
236
237
  }
237
- list.children.push(att(column.name, column.type, null, item, line, namespace, hidden, false));
238
+ list.children.push(att(column.name, column.type, null, item, false, cell));
238
239
  }
239
240
  return list;
240
241
  }
@@ -262,10 +263,11 @@ function javaSplitLines(text: string): string[] {
262
263
  * @param cyType - the Cytoscape type
263
264
  * @param elementType - a list's item type, or null
264
265
  * @param value - the value, or null
265
- * @param line - the line
266
- * @param namespace - the table namespace, or null
267
- * @param hidden - a hidden column
268
266
  * @param equation - the value is a formula
267
+ * @param cell - where the cell sits
268
+ * @param cell.line - the line
269
+ * @param cell.namespace - the table namespace, or null
270
+ * @param cell.hidden - a hidden column
269
271
  * @returns the record
270
272
  */
271
273
  function att(
@@ -273,11 +275,10 @@ function att(
273
275
  cyType: string,
274
276
  elementType: string | null,
275
277
  value: string | null,
276
- line: number,
277
- namespace: string | null,
278
- hidden: boolean,
279
278
  equation: boolean,
279
+ cell: { readonly line: number; readonly namespace: string | null; readonly hidden: boolean },
280
280
  ): AttRec {
281
+ const { line, namespace, hidden } = cell;
281
282
  return {
282
283
  name,
283
284
  type: null,
@@ -1247,6 +1247,27 @@ function lifetimeAttrs(roles: RoleColumns, row: number, plan: ExportPlan): strin
1247
1247
  return out;
1248
1248
  }
1249
1249
 
1250
+ /**
1251
+ * The `start` and `end` attributes of one `<spell>`.
1252
+ * @param s - the start, or a non-finite value for an unbounded start
1253
+ * @param e - the end, or a non-finite value for an unbounded end
1254
+ * @param open - the open-bound bits
1255
+ * @param plan - the plan
1256
+ * @returns the attribute text, each with a leading space
1257
+ */
1258
+ function spellBounds(s: number, e: number, open: number, plan: ExportPlan): string {
1259
+ let attrs = "";
1260
+ if (Number.isFinite(s)) {
1261
+ const name = (open & OPEN_START) === 0 ? "start" : "startopen";
1262
+ attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
1263
+ }
1264
+ if (Number.isFinite(e)) {
1265
+ const name = (open & OPEN_END) === 0 ? "end" : "endopen";
1266
+ attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
1267
+ }
1268
+ return attrs;
1269
+ }
1270
+
1250
1271
  /**
1251
1272
  * The `<spells>` element of one element: its spells column, plus (1.2) its timestamps as [t, t].
1252
1273
  * In 1.2 a spell whose spells.open bits mark a bound open writes that bound as `startopen` /
@@ -1276,16 +1297,7 @@ function spellsElement(roles: RoleColumns, row: number, plan: ExportPlan, indent
1276
1297
  }
1277
1298
  let out = `${indent}<spells>\n`;
1278
1299
  for (const [s, e, open] of pairs) {
1279
- let attrs = "";
1280
- if (Number.isFinite(s)) {
1281
- const name = (open & OPEN_START) === 0 ? "start" : "startopen";
1282
- attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
1283
- }
1284
- if (Number.isFinite(e)) {
1285
- const name = (open & OPEN_END) === 0 ? "end" : "endopen";
1286
- attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
1287
- }
1288
- out += `${indent} <spell${attrs}/>\n`;
1300
+ out += `${indent} <spell${spellBounds(s, e, open, plan)}/>\n`;
1289
1301
  }
1290
1302
  return `${out}${indent}</spells>\n`;
1291
1303
  }
@@ -1676,23 +1688,12 @@ function metaElement(meta: GraphSnapshot["meta"]): string {
1676
1688
  }
1677
1689
 
1678
1690
  /**
1679
- * The `<graph>` start tag with its header attributes.
1691
+ * The `idtype` attribute of the `<graph>` tag.
1680
1692
  * @param snapshot - the snapshot
1681
- * @param plan - the plan
1682
- * @returns the line
1693
+ * @returns the attribute text with a leading space, or an empty string
1683
1694
  */
1684
- function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1685
- const { meta } = snapshot;
1686
- let attrs = ` defaultedgetype="${defaultEdgeType(snapshot)}"`;
1687
- let mode = meta.mode ?? "static";
1688
- if (plan.temporal && mode === "static") {
1689
- mode = "dynamic";
1690
- }
1691
- if (plan.version === "1.2" && mode === "slice") {
1692
- mode = "dynamic";
1693
- }
1694
- attrs += ` mode="${mode}"`;
1695
- let { idType } = meta;
1695
+ function graphIdTypeAttr(snapshot: GraphSnapshot): string {
1696
+ let { idType } = snapshot.meta;
1696
1697
  if (idType === null) {
1697
1698
  const { kind } = snapshot.ids;
1698
1699
  if (kind === "string") {
@@ -1701,17 +1702,16 @@ function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1701
1702
  idType = "integer";
1702
1703
  }
1703
1704
  }
1704
- if (idType === "integer" || idType === "string") {
1705
- attrs += ` idtype="${idType}"`;
1706
- }
1707
- if (plan.timeFormat !== null) {
1708
- attrs += ` timeformat="${plan.timeFormat}"`;
1709
- } else if (plan.temporal) {
1710
- attrs += ' timeformat="double"';
1711
- }
1712
- if (plan.version === "1.3" && plan.timeRepresentation !== null) {
1713
- attrs += ` timerepresentation="${plan.timeRepresentation}"`;
1714
- }
1705
+ return idType === "integer" || idType === "string" ? ` idtype="${idType}"` : "";
1706
+ }
1707
+
1708
+ /**
1709
+ * The `start`, `end` and `timestamp` attributes of the `<graph>` tag.
1710
+ * @param snapshot - the snapshot
1711
+ * @param plan - the plan
1712
+ * @returns the attribute text with leading spaces, or an empty string
1713
+ */
1714
+ function graphHeaderAttrs(snapshot: GraphSnapshot, plan: ExportPlan): string {
1715
1715
  const timestamp = graphExtraText(snapshot, "timestamp");
1716
1716
  const header: Record<string, string | null> = {
1717
1717
  start: graphExtraText(snapshot, "start"),
@@ -1724,11 +1724,42 @@ function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1724
1724
  header.end ??= timestamp;
1725
1725
  header.timestamp = null;
1726
1726
  }
1727
+ let attrs = "";
1727
1728
  for (const [key, value] of Object.entries(header)) {
1728
1729
  if (value !== null) {
1729
1730
  attrs += ` ${key}="${escapeXmlAttribute(value)}"`;
1730
1731
  }
1731
1732
  }
1733
+ return attrs;
1734
+ }
1735
+
1736
+ /**
1737
+ * The `<graph>` start tag with its header attributes.
1738
+ * @param snapshot - the snapshot
1739
+ * @param plan - the plan
1740
+ * @returns the line
1741
+ */
1742
+ function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1743
+ const { meta } = snapshot;
1744
+ let attrs = ` defaultedgetype="${defaultEdgeType(snapshot)}"`;
1745
+ let mode = meta.mode ?? "static";
1746
+ if (plan.temporal && mode === "static") {
1747
+ mode = "dynamic";
1748
+ }
1749
+ if (plan.version === "1.2" && mode === "slice") {
1750
+ mode = "dynamic";
1751
+ }
1752
+ attrs += ` mode="${mode}"`;
1753
+ attrs += graphIdTypeAttr(snapshot);
1754
+ if (plan.timeFormat !== null) {
1755
+ attrs += ` timeformat="${plan.timeFormat}"`;
1756
+ } else if (plan.temporal) {
1757
+ attrs += ' timeformat="double"';
1758
+ }
1759
+ if (plan.version === "1.3" && plan.timeRepresentation !== null) {
1760
+ attrs += ` timerepresentation="${plan.timeRepresentation}"`;
1761
+ }
1762
+ attrs += graphHeaderAttrs(snapshot, plan);
1732
1763
  return ` <graph${attrs}>\n`;
1733
1764
  }
1734
1765
 
@@ -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
  /**