@graphty/graph-io 0.3.40 → 0.3.42

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 (210) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
  3. package/dist/chunks/children-CA4coBi0.js.map +1 -0
  4. package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
  5. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
  6. package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
  7. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
  8. package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
  9. package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
  10. package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
  11. package/dist/chunks/export-rKLeGQMo.js.map +1 -0
  12. package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
  13. package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
  14. package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
  15. package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
  16. package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
  17. package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
  18. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
  19. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
  20. package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
  21. package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
  22. package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
  23. package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
  24. package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
  25. package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
  26. package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
  27. package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
  28. package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
  29. package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
  30. package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
  31. package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
  32. package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
  33. package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
  34. package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
  35. package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
  36. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
  37. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
  38. package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
  39. package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
  40. package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
  41. package/dist/chunks/text-DHOJXnED.js.map +1 -0
  42. package/dist/csv.js +323 -241
  43. package/dist/csv.js.map +1 -1
  44. package/dist/cx.js +1 -1
  45. package/dist/cx2.js +2 -2
  46. package/dist/cys.js +1 -1
  47. package/dist/dot.js +1 -1
  48. package/dist/gexf.js +195 -97
  49. package/dist/gexf.js.map +1 -1
  50. package/dist/gml.js +170 -149
  51. package/dist/gml.js.map +1 -1
  52. package/dist/graph-io.js +27 -27
  53. package/dist/graphml.js +2 -2
  54. package/dist/json.js +1 -1
  55. package/dist/neo4j.js +103 -65
  56. package/dist/neo4j.js.map +1 -1
  57. package/dist/obo.js +1 -1
  58. package/dist/pajek.js +1 -1
  59. package/dist/src/children.d.ts.map +1 -1
  60. package/dist/src/children.js +50 -27
  61. package/dist/src/children.js.map +1 -1
  62. package/dist/src/common/attributes.d.ts.map +1 -1
  63. package/dist/src/common/attributes.js +86 -42
  64. package/dist/src/common/attributes.js.map +1 -1
  65. package/dist/src/common/input.d.ts.map +1 -1
  66. package/dist/src/common/input.js +358 -184
  67. package/dist/src/common/input.js.map +1 -1
  68. package/dist/src/common/lists.js +30 -15
  69. package/dist/src/common/lists.js.map +1 -1
  70. package/dist/src/common/text.d.ts +7 -0
  71. package/dist/src/common/text.d.ts.map +1 -1
  72. package/dist/src/common/text.js +15 -7
  73. package/dist/src/common/text.js.map +1 -1
  74. package/dist/src/common/zip.d.ts.map +1 -1
  75. package/dist/src/common/zip.js +36 -19
  76. package/dist/src/common/zip.js.map +1 -1
  77. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  78. package/dist/src/formats/csv/exporter.js +288 -146
  79. package/dist/src/formats/csv/exporter.js.map +1 -1
  80. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  81. package/dist/src/formats/csv/importer.js +129 -80
  82. package/dist/src/formats/csv/importer.js.map +1 -1
  83. package/dist/src/formats/csv/records.d.ts.map +1 -1
  84. package/dist/src/formats/csv/records.js +78 -36
  85. package/dist/src/formats/csv/records.js.map +1 -1
  86. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  87. package/dist/src/formats/cys/tables.js +11 -8
  88. package/dist/src/formats/cys/tables.js.map +1 -1
  89. package/dist/src/formats/dot/tokenizer.d.ts +39 -0
  90. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  91. package/dist/src/formats/dot/tokenizer.js +112 -66
  92. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  93. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  94. package/dist/src/formats/gexf/exporter.js +65 -37
  95. package/dist/src/formats/gexf/exporter.js.map +1 -1
  96. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/importer.js +138 -50
  98. package/dist/src/formats/gexf/importer.js.map +1 -1
  99. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  100. package/dist/src/formats/gml/exporter.js +51 -35
  101. package/dist/src/formats/gml/exporter.js.map +1 -1
  102. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  103. package/dist/src/formats/gml/importer.js +103 -58
  104. package/dist/src/formats/gml/importer.js.map +1 -1
  105. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  106. package/dist/src/formats/gml/syntax.js +17 -8
  107. package/dist/src/formats/gml/syntax.js.map +1 -1
  108. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  109. package/dist/src/formats/graphml/exporter.js +34 -24
  110. package/dist/src/formats/graphml/exporter.js.map +1 -1
  111. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/importer.js +17 -13
  113. package/dist/src/formats/graphml/importer.js.map +1 -1
  114. package/dist/src/formats/graphml/tree.js +23 -14
  115. package/dist/src/formats/graphml/tree.js.map +1 -1
  116. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  117. package/dist/src/formats/graphml/yfiles.js +22 -10
  118. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  119. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  120. package/dist/src/formats/json/dialect.js +54 -20
  121. package/dist/src/formats/json/dialect.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 +7 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +79 -40
  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 +38 -12
  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/children.ts +54 -27
  166. package/src/common/attributes.ts +118 -49
  167. package/src/common/input.ts +439 -197
  168. package/src/common/lists.ts +29 -15
  169. package/src/common/text.ts +24 -15
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +377 -175
  172. package/src/formats/csv/importer.ts +155 -84
  173. package/src/formats/csv/records.ts +90 -35
  174. package/src/formats/cys/tables.ts +11 -10
  175. package/src/formats/dot/tokenizer.ts +118 -68
  176. package/src/formats/gexf/exporter.ts +67 -36
  177. package/src/formats/gexf/importer.ts +173 -63
  178. package/src/formats/gml/exporter.ts +78 -82
  179. package/src/formats/gml/importer.ts +119 -68
  180. package/src/formats/gml/syntax.ts +18 -8
  181. package/src/formats/graphml/exporter.ts +44 -24
  182. package/src/formats/graphml/importer.ts +18 -11
  183. package/src/formats/graphml/tree.ts +22 -12
  184. package/src/formats/graphml/yfiles.ts +23 -10
  185. package/src/formats/json/dialect.ts +61 -22
  186. package/src/formats/json/exporter.ts +40 -21
  187. package/src/formats/json/importer.ts +701 -385
  188. package/src/formats/json/obographs.ts +35 -15
  189. package/src/formats/neo4j/exporter.ts +79 -41
  190. package/src/formats/neo4j/header.ts +50 -16
  191. package/src/formats/neo4j/importer.ts +21 -10
  192. package/src/formats/obo/exporter.ts +34 -17
  193. package/src/formats/pajek/exporter.ts +180 -117
  194. package/src/formats/pajek/importer.ts +27 -15
  195. package/src/formats/pajek/syntax.ts +99 -58
  196. package/src/formats/xgmml/emit.ts +88 -40
  197. package/src/formats/xgmml/exporter.ts +40 -12
  198. package/src/formats/xgmml/importer.ts +45 -23
  199. package/src/sniff.ts +115 -59
  200. package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
  201. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  202. package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
  203. package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
  204. package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
  205. package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
  206. package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
  207. package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
  208. package/dist/chunks/importer-DcKB97kW.js.map +0 -1
  209. package/dist/chunks/records-r3SFHz11.js.map +0 -1
  210. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -87,6 +87,16 @@ function isDigit(c: number): boolean {
87
87
  return c >= 48 && c <= 57;
88
88
  }
89
89
 
90
+ /**
91
+ * Whether a code unit is a blank between tokens: space, tab, vertical tab, form feed, or a U+FEFF
92
+ * after the start (concatenated files), which the shared text check reports.
93
+ * @param c - the code unit
94
+ * @returns true for a blank
95
+ */
96
+ function isBlank(c: number): boolean {
97
+ return c === 0x20 || c === 0x09 || c === 0x0b || c === 0x0c || c === BOM_CODE;
98
+ }
99
+
90
100
  /**
91
101
  * A pull lexer over one DOT document with arbitrary lookahead.
92
102
  * @category Plugin helpers
@@ -247,40 +257,68 @@ export class DotTokenizer {
247
257
  this.pos = i + 1;
248
258
  return { kind: "id", text: out, quoted: true, html: false, line };
249
259
  }
250
- if (c === 0x5c) {
251
- const d = text.charCodeAt(i + 1);
252
- if (d === 0x22) {
253
- out += `${text.slice(segment, i)}"`;
254
- i += 2;
255
- segment = i;
256
- continue;
257
- }
258
- if (d === 0x5c) {
259
- // Graphviz's scanner consumes a backslash pair as one unit (both kept), so the
260
- // second backslash never escapes a closing quote: "x\\" is the text x\\
261
- i += 2;
262
- continue;
263
- }
264
- if (d === 0x0a || d === 0x0d) {
265
- out += text.slice(segment, i);
266
- i += d === 0x0d && text.charCodeAt(i + 2) === 0x0a ? 3 : 2;
267
- this.line++;
268
- segment = i;
269
- continue;
270
- }
271
- i++;
272
- continue;
273
- }
274
- if (c === 0x0a || c === 0x0d) {
275
- if (c === 0x0d && text.charCodeAt(i + 1) === 0x0a) {
276
- i++;
260
+ if (c === 0x5c && this.isQuotedSplice(i)) {
261
+ out += text.slice(segment, i);
262
+ if (text.codePointAt(i + 1) === 0x22) {
263
+ out += '"';
277
264
  }
278
- this.line++;
265
+ i = this.quotedSpliceEnd(i);
266
+ segment = i;
267
+ } else {
268
+ i = this.quotedStep(i, c);
279
269
  }
280
- i++;
281
270
  }
282
271
  }
283
272
 
273
+ /**
274
+ * Whether the backslash at `i` inside a quoted string starts a sequence that changes the decoded
275
+ * text: `\"` (an escaped quote) or a backslash before a line break (a line continuation).
276
+ * @param i - the offset of the backslash
277
+ * @returns true for an escaped quote or a line continuation
278
+ */
279
+ private isQuotedSplice(i: number): boolean {
280
+ const d = this.text.codePointAt(i + 1);
281
+ return d === 0x22 || d === 0x0a || d === 0x0d;
282
+ }
283
+
284
+ /**
285
+ * The offset just past an escaped quote or a line continuation, counting the line a
286
+ * continuation ends.
287
+ * @param i - the offset of the backslash
288
+ * @returns the offset after the sequence
289
+ */
290
+ private quotedSpliceEnd(i: number): number {
291
+ const { text } = this;
292
+ const d = text.charCodeAt(i + 1);
293
+ if (d === 0x22) {
294
+ return i + 2;
295
+ }
296
+ this.line++;
297
+ return d === 0x0d && text.codePointAt(i + 2) === 0x0a ? i + 3 : i + 2;
298
+ }
299
+
300
+ /**
301
+ * Step over one unit of a quoted string kept as written: a backslash pair (consumed as one unit,
302
+ * as Graphviz's scanner does, so the second backslash never escapes a closing quote: "x\\" is
303
+ * the text x\\), a lone backslash, a raw line break (counted) or any other character.
304
+ * @param i - the offset of the unit
305
+ * @param c - the code unit at `i`
306
+ * @returns the offset after the unit
307
+ */
308
+ private quotedStep(i: number, c: number): number {
309
+ const { text } = this;
310
+ if (c === 0x5c) {
311
+ return text.codePointAt(i + 1) === 0x5c ? i + 2 : i + 1;
312
+ }
313
+ if (c === 0x0a) {
314
+ this.line++;
315
+ } else if (c === 0x0d) {
316
+ this.line++;
317
+ return text.codePointAt(i + 1) === 0x0a ? i + 2 : i + 1;
318
+ }
319
+ return i + 1;
320
+ }
321
+
284
322
  /**
285
323
  * Lex an HTML string starting at the opening angle bracket: the text up to the matching
286
324
  * closing bracket, brackets balanced, kept verbatim including the outer pair.
@@ -313,51 +351,63 @@ export class DotTokenizer {
313
351
  */
314
352
  private skipTrivia(): void {
315
353
  const { text } = this;
316
- for (;;) {
317
- if (this.pos >= text.length) {
318
- return;
319
- }
354
+ while (this.pos < text.length) {
320
355
  const c = text.charCodeAt(this.pos);
321
- if (c === 0x0a) {
322
- this.line++;
323
- this.pos++;
324
- this.skipPreprocessorLine();
325
- continue;
326
- }
327
- if (c === 0x0d) {
328
- this.line++;
329
- this.pos += text.charCodeAt(this.pos + 1) === 0x0a ? 2 : 1;
330
- this.skipPreprocessorLine();
331
- continue;
332
- }
333
- if (c === 0x20 || c === 0x09 || c === 0x0b || c === 0x0c) {
334
- this.pos++;
335
- continue;
336
- }
337
- if (c === BOM_CODE) {
338
- // a U+FEFF after the start (concatenated files) is whitespace; the shared text check reports it
356
+ if (isBlank(c)) {
339
357
  this.pos++;
340
- continue;
341
- }
342
- if (c === 0x2f) {
343
- const d = text.charCodeAt(this.pos + 1);
344
- if (d === 0x2f) {
345
- this.skipToLineEnd();
346
- continue;
347
- }
348
- if (d === 0x2a) {
349
- this.skipBlockComment();
350
- continue;
351
- }
352
- }
353
- if (c === 0x23 && this.pos === 0) {
354
- this.skipToLineEnd();
355
- continue;
358
+ } else if (!this.skipBreakOrComment(c)) {
359
+ return;
356
360
  }
357
- return;
358
361
  }
359
362
  }
360
363
 
364
+ /**
365
+ * Skip one line break, comment or leading `#` line at the current position.
366
+ * @param c - the code unit at the current position
367
+ * @returns false when the current position starts a token
368
+ */
369
+ private skipBreakOrComment(c: number): boolean {
370
+ if (c === 0x0a || c === 0x0d) {
371
+ this.skipLineBreak(c);
372
+ return true;
373
+ }
374
+ if (c === 0x2f) {
375
+ return this.skipComment();
376
+ }
377
+ if (c === 0x23 && this.pos === 0) {
378
+ this.skipToLineEnd();
379
+ return true;
380
+ }
381
+ return false;
382
+ }
383
+
384
+ /**
385
+ * Consume the line break at the current position (CR LF as one) and a `#` line after it.
386
+ * @param c - the code unit at the current position, LF or CR
387
+ */
388
+ private skipLineBreak(c: number): void {
389
+ this.line++;
390
+ this.pos += c === 0x0d && this.text.codePointAt(this.pos + 1) === 0x0a ? 2 : 1;
391
+ this.skipPreprocessorLine();
392
+ }
393
+
394
+ /**
395
+ * Skip a `//` or block comment starting at the current `/`.
396
+ * @returns false when the `/` starts no comment
397
+ */
398
+ private skipComment(): boolean {
399
+ const d = this.text.codePointAt(this.pos + 1);
400
+ if (d === 0x2f) {
401
+ this.skipToLineEnd();
402
+ return true;
403
+ }
404
+ if (d === 0x2a) {
405
+ this.skipBlockComment();
406
+ return true;
407
+ }
408
+ return false;
409
+ }
410
+
361
411
  /**
362
412
  * Discard a `#` line when one starts at the current position (the start of a line).
363
413
  */
@@ -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
  /**