@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
@@ -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
 
@@ -518,31 +518,50 @@ class GmlImport {
518
518
  if (key === "graph" && !record && scalarGraph < 0) {
519
519
  scalarGraph = v;
520
520
  }
521
- if (key === "graph" && record) {
522
- if (this.graphCount++ === this.which) {
523
- this.graphOpen = v;
524
- this.scanGraph(v);
525
- }
526
- } else if (key === "Creator" && !record && this.creatorToken < 0) {
527
- this.creatorToken = v;
528
- } else if (key === "Version" && !record && this.versionToken < 0) {
529
- this.versionToken = v;
530
- } else {
531
- this.observe(this.topSchema, "graph", key, v, TOP_SEQ);
532
- }
521
+ this.scanTopPair(key, record, v);
533
522
  }
534
523
  if (this.graphOpen < 0) {
535
- if (scalarGraph >= 0) {
536
- this.report.fail(
537
- NO_GRAPH_CODE,
538
- `the graph key at line ${t.line[scalarGraph]} holds ${describeValue(t, scalarGraph)}, not a [ ... ] block; the input contains no graph block`,
539
- { line: t.line[scalarGraph] },
540
- );
524
+ this.failNoGraph(t, scalarGraph);
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Handle one top-level pair other than a scalar `graph`.
530
+ * @param key - the key
531
+ * @param record - whether the value is a `[ ... ]` block
532
+ * @param v - the value token
533
+ */
534
+ private scanTopPair(key: string, record: boolean, v: number): void {
535
+ if (key === "graph" && record) {
536
+ if (this.graphCount++ === this.which) {
537
+ this.graphOpen = v;
538
+ this.scanGraph(v);
541
539
  }
542
- this.report.fail(NO_GRAPH_CODE, "the input contains no graph [ ... ] block", { line: null });
540
+ } else if (key === "Creator" && !record && this.creatorToken < 0) {
541
+ this.creatorToken = v;
542
+ } else if (key === "Version" && !record && this.versionToken < 0) {
543
+ this.versionToken = v;
544
+ } else {
545
+ this.observe(this.topSchema, "graph", key, v, TOP_SEQ);
543
546
  }
544
547
  }
545
548
 
549
+ /**
550
+ * Fail because the input holds no graph block.
551
+ * @param t - the tokens
552
+ * @param scalarGraph - the token of a scalar `graph` key, or -1
553
+ */
554
+ private failNoGraph(t: ReturnType<GmlImport["requireTokens"]>, scalarGraph: number): void {
555
+ if (scalarGraph >= 0) {
556
+ this.report.fail(
557
+ NO_GRAPH_CODE,
558
+ `the graph key at line ${t.line[scalarGraph]} holds ${describeValue(t, scalarGraph)}, not a [ ... ] block; the input contains no graph block`,
559
+ { line: t.line[scalarGraph] },
560
+ );
561
+ }
562
+ this.report.fail(NO_GRAPH_CODE, "the input contains no graph [ ... ] block", { line: null });
563
+ }
564
+
546
565
  /**
547
566
  * Walk the graph block for the schema.
548
567
  * @param open - the index of the block's `[`
@@ -592,31 +611,43 @@ class GmlImport {
592
611
  const close = t.match[open];
593
612
  const seq = this.seq++;
594
613
  const schema = domain === "node" ? this.nodeSchema : this.edgeSchema;
595
- const { weightFrom, restoreMangledIds } = this.options;
596
614
  for (let p = open + 1; p < close; p = t.nextPair(p)) {
597
615
  const key = t.textOf(p);
598
616
  const v = p + 1;
599
- if (domain === "node") {
600
- if (key === "id") {
601
- continue;
602
- }
603
- if (key === ORIGINAL_ID_KEY && restoreMangledIds) {
604
- this.hasOriginalId = true;
605
- continue;
606
- }
607
- } else {
608
- if (key === "source" || key === "target" || key === "directed") {
609
- continue;
610
- }
611
- if (key === weightFrom) {
612
- this.weightKinds |= kindBit(t, v);
613
- continue;
614
- }
617
+ if (this.skipsKey(domain, key, v)) {
618
+ continue;
615
619
  }
616
620
  this.observe(schema, domain, key, v, seq);
617
621
  }
618
622
  }
619
623
 
624
+ /**
625
+ * Whether a key of a node or edge block is structural (id, endpoints, weight, original id), not
626
+ * a column; notes the original id and the weight's kinds on the way.
627
+ * @param domain - node or edge
628
+ * @param key - the key
629
+ * @param v - the value token
630
+ * @returns true when the key is not a column
631
+ */
632
+ private skipsKey(domain: "node" | "edge", key: string, v: number): boolean {
633
+ const { weightFrom, restoreMangledIds } = this.options;
634
+ if (domain === "node") {
635
+ if (key === ORIGINAL_ID_KEY && restoreMangledIds) {
636
+ this.hasOriginalId = true;
637
+ return true;
638
+ }
639
+ return key === "id";
640
+ }
641
+ if (key === "source" || key === "target" || key === "directed") {
642
+ return true;
643
+ }
644
+ if (key === weightFrom) {
645
+ this.weightKinds |= kindBit(this.requireTokens(), v);
646
+ return true;
647
+ }
648
+ return false;
649
+ }
650
+
620
651
  /**
621
652
  * Record one value of one key in a table's schema.
622
653
  * @param schema - the table's schema
@@ -816,26 +847,12 @@ class GmlImport {
816
847
  }
817
848
 
818
849
  /**
819
- * The plan of one schema entry.
850
+ * Warn about a key that mixes numbers and strings, or holds integers beyond 2^53.
820
851
  * @param domain - the table
821
852
  * @param entry - the schema entry
822
- * @param top - whether the key is a top-level one (graph table)
823
- * @returns the plan
853
+ * @param asText - whether the integers are kept as text
824
854
  */
825
- private planOf(domain: "node" | "edge" | "graph", entry: KeySchema, top = false): ColumnPlan {
826
- // long: "string" keeps every digit of an integer key that holds values beyond 2^53, as text
827
- const asText = entry.unsafe && this.options.long === "string" && kindOf(entry) === "int";
828
- const kind = asText ? "string" : kindOf(entry);
829
- let scalar: ScalarDtype = dtypeOfKind(kind, entry);
830
- if (
831
- scalar === "string" &&
832
- !entry.list &&
833
- entry.dict !== null &&
834
- entry.kinds === KIND_STRING &&
835
- entry.dict.decide() === "dict"
836
- ) {
837
- scalar = "dict";
838
- }
855
+ private warnKinds(domain: "node" | "edge" | "graph", entry: KeySchema, asText: boolean): void {
839
856
  if (
840
857
  (entry.kinds & KIND_STRING) !== 0 &&
841
858
  (entry.kinds & (KIND_INT | KIND_REAL)) !== 0 &&
@@ -858,6 +875,54 @@ class GmlImport {
858
875
  `${PRECISION_CODE}:${domain}:${entry.key}`,
859
876
  );
860
877
  }
878
+ }
879
+
880
+ /**
881
+ * The plan of a node `graphics` record: the plan plus its position and rest columns.
882
+ * @param plan - the record's plan
883
+ * @param entry - the schema entry
884
+ * @returns the plan with its position (and rest) plans
885
+ */
886
+ private graphicsPlan(plan: ColumnPlan, entry: KeySchema): ColumnPlan {
887
+ const taken = (candidate: string): boolean =>
888
+ this.nodeSchema.has(candidate) || this.sink.nodeColumn(candidate) !== INVALID_INDEX;
889
+ const position: ColumnPlan = {
890
+ ...plan,
891
+ name: uniqueColumnName("position", "graphics", taken),
892
+ dtype: "f64",
893
+ itemDtype: null,
894
+ kind: "real",
895
+ list: false,
896
+ role: "position",
897
+ };
898
+ const rest: ColumnPlan | null = entry.rest
899
+ ? { ...plan, dtype: "json", itemDtype: null, kind: "record", list: false }
900
+ : null;
901
+ return { ...plan, position, rest };
902
+ }
903
+
904
+ /**
905
+ * The plan of one schema entry.
906
+ * @param domain - the table
907
+ * @param entry - the schema entry
908
+ * @param top - whether the key is a top-level one (graph table)
909
+ * @returns the plan
910
+ */
911
+ private planOf(domain: "node" | "edge" | "graph", entry: KeySchema, top = false): ColumnPlan {
912
+ // long: "string" keeps every digit of an integer key that holds values beyond 2^53, as text
913
+ const asText = entry.unsafe && this.options.long === "string" && kindOf(entry) === "int";
914
+ const kind = asText ? "string" : kindOf(entry);
915
+ let scalar: ScalarDtype = dtypeOfKind(kind, entry);
916
+ if (
917
+ scalar === "string" &&
918
+ !entry.list &&
919
+ entry.dict !== null &&
920
+ entry.kinds === KIND_STRING &&
921
+ entry.dict.decide() === "dict"
922
+ ) {
923
+ scalar = "dict";
924
+ }
925
+ this.warnKinds(domain, entry, asText);
861
926
  const name = domain === "graph" ? this.uniqueGraphName(entry.key) : entry.key;
862
927
  let itemDtype: ScalarDtype | null = null;
863
928
  if (entry.list) {
@@ -880,21 +945,7 @@ class GmlImport {
880
945
  seq: Number.NEGATIVE_INFINITY,
881
946
  };
882
947
  if (domain === "node" && entry.key === "graphics" && this.positions && (entry.kinds & KIND_RECORD) !== 0) {
883
- const taken = (candidate: string): boolean =>
884
- this.nodeSchema.has(candidate) || this.sink.nodeColumn(candidate) !== INVALID_INDEX;
885
- const position: ColumnPlan = {
886
- ...plan,
887
- name: uniqueColumnName("position", "graphics", taken),
888
- dtype: "f64",
889
- itemDtype: null,
890
- kind: "real",
891
- list: false,
892
- role: "position",
893
- };
894
- const rest: ColumnPlan | null = entry.rest
895
- ? { ...plan, dtype: "json", itemDtype: null, kind: "record", list: false }
896
- : null;
897
- return { ...plan, position, rest };
948
+ return this.graphicsPlan(plan, entry);
898
949
  }
899
950
  return plan;
900
951
  }
@@ -583,14 +583,7 @@ export function parseRecord(tokens: GmlTokens, open: number): Record<string, unk
583
583
  const key = tokens.textOf(p);
584
584
  const v = p + 1;
585
585
  if (tokens.kind[v] === TOKEN_STRING) {
586
- const text = tokens.stringOf(v);
587
- if (text === LIST_START_MARKER) {
588
- frame.record[key] = [];
589
- frame.listKeys ??= new Set();
590
- frame.listKeys.add(key);
591
- continue;
592
- }
593
- storeValue(frame, key, text === EMPTY_LIST_TEXT || text === EMPTY_TUPLE_TEXT ? [] : text);
586
+ storeString(frame, key, tokens.stringOf(v));
594
587
  } else if (tokens.kind[v] === TOKEN_OPEN) {
595
588
  stack.push({
596
589
  record: Object.create(null) as Record<string, unknown>,
@@ -606,6 +599,23 @@ export function parseRecord(tokens: GmlTokens, open: number): Record<string, unk
606
599
  return root;
607
600
  }
608
601
 
602
+ /**
603
+ * Store a string value: the list-start marker opens an empty list for the key, the empty list and
604
+ * tuple texts store an empty list, anything else stores the text.
605
+ * @param frame - the open record
606
+ * @param key - the key
607
+ * @param text - the string
608
+ */
609
+ function storeString(frame: RecordFrame, key: string, text: string): void {
610
+ if (text === LIST_START_MARKER) {
611
+ frame.record[key] = [];
612
+ frame.listKeys ??= new Set();
613
+ frame.listKeys.add(key);
614
+ return;
615
+ }
616
+ storeValue(frame, key, text === EMPTY_LIST_TEXT || text === EMPTY_TUPLE_TEXT ? [] : text);
617
+ }
618
+
609
619
  /**
610
620
  * Store one value under a key: appended to the key's list when the key is a list, paired with the
611
621
  * earlier value when the key repeats, set otherwise.
@@ -1153,6 +1153,30 @@ function writeTreeData(keyId: string, value: unknown, indent: string, out: strin
1153
1153
  out.push(`\n${indent}</data>`);
1154
1154
  }
1155
1155
 
1156
+ /**
1157
+ * Append the `<default>` element of a key.
1158
+ * @param key - the key
1159
+ * @param column - the key's column
1160
+ * @param inner - the indentation of the element
1161
+ * @param parts - where to append
1162
+ */
1163
+ function pushDefault(key: KeyPlan, column: Column, inner: string, parts: string[]): void {
1164
+ if (key.yfilesType === null) {
1165
+ parts.push(`\n${inner}<default>${escapeXmlText(defaultText(column, key.attrType) ?? "")}</default>`);
1166
+ return;
1167
+ }
1168
+ const value = column.meta.default;
1169
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1170
+ parts.push(`\n${inner}<default>`);
1171
+ writeXmlTree(value, `${inner} `, parts);
1172
+ parts.push(`\n${inner}</default>`);
1173
+ } else {
1174
+ const tree: string[] = [];
1175
+ writeXmlTree(value, inner, tree);
1176
+ parts.push(`\n${inner}<default>${tree.join("")}</default>`);
1177
+ }
1178
+ }
1179
+
1156
1180
  /**
1157
1181
  * The `<key>` element of a plan.
1158
1182
  * @param key - the key plan
@@ -1183,20 +1207,7 @@ function keyElement(key: KeyPlan, indent: string): string {
1183
1207
  parts.push(`\n${inner}<desc>${escapeXmlText(desc)}</desc>`);
1184
1208
  }
1185
1209
  if (hasDefault) {
1186
- if (key.yfilesType !== null) {
1187
- const value = column.meta.default;
1188
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1189
- parts.push(`\n${inner}<default>`);
1190
- writeXmlTree(value, `${inner} `, parts);
1191
- parts.push(`\n${inner}</default>`);
1192
- } else {
1193
- const tree: string[] = [];
1194
- writeXmlTree(value, inner, tree);
1195
- parts.push(`\n${inner}<default>${tree.join("")}</default>`);
1196
- }
1197
- } else {
1198
- parts.push(`\n${inner}<default>${escapeXmlText(defaultText(column, key.attrType) ?? "")}</default>`);
1199
- }
1210
+ pushDefault(key, column, inner, parts);
1200
1211
  }
1201
1212
  parts.push(`\n${indent}</key>\n`);
1202
1213
  return parts.join("");
@@ -1307,11 +1318,13 @@ function* writeGraphml(snapshot: GraphSnapshot, plan: Plan): Generator<string, v
1307
1318
  // nodes in containment order, children nested under their parent
1308
1319
  const { hierarchy } = plan;
1309
1320
  const nested = hierarchy.childList.length > 0;
1310
- const stackNode: number[] = [];
1311
- const stackCursor: number[] = [];
1312
- const written = nested ? new Uint8Array(snapshot.nodeCount) : null;
1321
+ const walk: SubtreeWalk = {
1322
+ stackNode: [],
1323
+ stackCursor: [],
1324
+ written: nested ? new Uint8Array(snapshot.nodeCount) : null,
1325
+ };
1313
1326
  for (const rootNode of hierarchy.roots) {
1314
- yield* writeNodeSubtree(rootNode, plan, ids, i2, step, stackNode, stackCursor, written);
1327
+ yield* writeNodeSubtree(rootNode, plan, ids, i2, step, walk);
1315
1328
  }
1316
1329
 
1317
1330
  // edges: primaries only, in index order
@@ -1378,6 +1391,16 @@ function portText(column: Column | null, e: number): string | null {
1378
1391
  return typeof value === "string" ? value : String(value);
1379
1392
  }
1380
1393
 
1394
+ /** The state a subtree walk reuses from one top-level node to the next. */
1395
+ interface SubtreeWalk {
1396
+ /** A reusable stack of open nodes. */
1397
+ readonly stackNode: number[];
1398
+ /** A reusable stack of child cursors. */
1399
+ readonly stackCursor: number[];
1400
+ /** Nodes already written (null when nothing is nested). */
1401
+ readonly written: Uint8Array | null;
1402
+ }
1403
+
1381
1404
  /**
1382
1405
  * Write a top-level node and, nested, every node contained in it (iteratively, so a deep chain
1383
1406
  * cannot overflow the call stack).
@@ -1386,9 +1409,7 @@ function portText(column: Column | null, e: number): string | null {
1386
1409
  * @param ids - the sanitised ids
1387
1410
  * @param indent - the indentation of top-level nodes
1388
1411
  * @param step - one indentation step
1389
- * @param stackNode - a reusable stack of open nodes
1390
- * @param stackCursor - a reusable stack of child cursors
1391
- * @param written - nodes already written (null when nothing is nested)
1412
+ * @param walk - the reusable walk state
1392
1413
  * @yields the node elements
1393
1414
  * @returns nothing
1394
1415
  */
@@ -1398,10 +1419,9 @@ function* writeNodeSubtree(
1398
1419
  ids: SanitizedIds,
1399
1420
  indent: string,
1400
1421
  step: string,
1401
- stackNode: number[],
1402
- stackCursor: number[],
1403
- written: Uint8Array | null,
1422
+ walk: SubtreeWalk,
1404
1423
  ): Generator<string, void, undefined> {
1424
+ const { stackNode, stackCursor, written } = walk;
1405
1425
  const { hierarchy } = plan;
1406
1426
  const hasChildren = (u: number): boolean => hierarchy.childStart[u + 1] > hierarchy.childStart[u];
1407
1427
  const openNode = (u: number, depth: number): string => {
@@ -1126,9 +1126,6 @@ class GraphmlReader implements XmlHandler {
1126
1126
  known: ReadonlySet<string>,
1127
1127
  line: number,
1128
1128
  ): void {
1129
- if (attrs.size === 0) {
1130
- return;
1131
- }
1132
1129
  for (const name of attrs.keys()) {
1133
1130
  if (known.has(name) || name === "xmlns" || /^(xmlns|xml|xsi):/.test(name)) {
1134
1131
  continue;
@@ -1150,14 +1147,7 @@ class GraphmlReader implements XmlHandler {
1150
1147
  if (names.size > MAX_UNKNOWN_ATTRIBUTE_NAMES && !hint) {
1151
1148
  continue;
1152
1149
  }
1153
- let message: string;
1154
- if (hint) {
1155
- message = `the <${kind}> parse hint ${name} is ignored`;
1156
- } else if (SCHEMA_ATTRIBUTES.has(name)) {
1157
- message = `the <${kind}> attribute ${name} is not kept`;
1158
- } else {
1159
- message = `the <${kind}> attribute ${name} is not a GraphML attribute; it is not kept`;
1160
- }
1150
+ const message = unreadAttributeMessage(kind, name, hint);
1161
1151
  this.report.warning("unsupported", code, message, { line, element: name });
1162
1152
  }
1163
1153
  }
@@ -2729,3 +2719,20 @@ function unknownElementMessage(name: string): string {
2729
2719
  ? `element <${name}> is not allowed at this place in a GraphML file and was skipped`
2730
2720
  : `element <${name}> is not GraphML and was skipped`;
2731
2721
  }
2722
+
2723
+ /**
2724
+ * The warning text for an attribute the importer does not keep.
2725
+ * @param kind - the element name
2726
+ * @param name - the attribute name
2727
+ * @param hint - whether the attribute is a `parse.*` hint
2728
+ * @returns the message
2729
+ */
2730
+ function unreadAttributeMessage(kind: string, name: string, hint: boolean): string {
2731
+ if (hint) {
2732
+ return `the <${kind}> parse hint ${name} is ignored`;
2733
+ }
2734
+ if (SCHEMA_ATTRIBUTES.has(name)) {
2735
+ return `the <${kind}> attribute ${name} is not kept`;
2736
+ }
2737
+ return `the <${kind}> attribute ${name} is not a GraphML attribute; it is not kept`;
2738
+ }