@graphty/graph-io 0.3.39 → 0.3.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/dist/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
  2. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
  3. package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
  4. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
  5. package/dist/chunks/{escape-BpYMQZVa.js → escape-DBb8zuNE.js} +4 -3
  6. package/dist/chunks/escape-DBb8zuNE.js.map +1 -0
  7. package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
  8. package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
  9. package/dist/chunks/{exporter-CNJZF_BN.js → exporter-BqtmlxNr.js} +545 -459
  10. package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
  11. package/dist/chunks/{exporter-BqKs1Q12.js → exporter-CS35R6Fg.js} +6 -5
  12. package/dist/chunks/exporter-CS35R6Fg.js.map +1 -0
  13. package/dist/chunks/{exporter-DXqCPNrI.js → exporter-D8r1N2dU.js} +67 -42
  14. package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
  15. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
  16. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
  17. package/dist/chunks/{importer-CSt-gaYE.js → importer-BKC7AmCA.js} +42 -29
  18. package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
  19. package/dist/chunks/{importer-DvFznt5-.js → importer-BdWHmlmG.js} +112 -62
  20. package/dist/chunks/importer-BdWHmlmG.js.map +1 -0
  21. package/dist/chunks/{importer-CqgYq4Ld.js → importer-BfbzWk6k.js} +202 -181
  22. package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
  23. package/dist/chunks/{importer-PVXkeoaS.js → importer-BmaZcskq.js} +79 -66
  24. package/dist/chunks/importer-BmaZcskq.js.map +1 -0
  25. package/dist/chunks/{importer-B7Ir269L.js → importer-DviEsHy4.js} +118 -72
  26. package/dist/chunks/importer-DviEsHy4.js.map +1 -0
  27. package/dist/chunks/{importer--tQQsY4W.js → importer-S-abxJHn.js} +5 -5
  28. package/dist/chunks/{importer--tQQsY4W.js.map → importer-S-abxJHn.js.map} +1 -1
  29. package/dist/chunks/{importer-BfJ1jnNr.js → importer-r29cWnEf.js} +28 -13
  30. package/dist/chunks/importer-r29cWnEf.js.map +1 -0
  31. package/dist/chunks/{json-elements-CXs4z1iL.js → json-elements-DdF-etPT.js} +7 -5
  32. package/dist/chunks/{json-elements-CXs4z1iL.js.map → json-elements-DdF-etPT.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
  34. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
  35. package/dist/chunks/{records-C9jd7yLD.js → records-Uc3LkABx.js} +150 -106
  36. package/dist/chunks/records-Uc3LkABx.js.map +1 -0
  37. package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
  38. package/dist/chunks/text-Bh9SaAob.js.map +1 -0
  39. package/dist/csv.js +67 -49
  40. package/dist/csv.js.map +1 -1
  41. package/dist/cx.js +1 -1
  42. package/dist/cx2.js +2 -2
  43. package/dist/cys.js +1 -1
  44. package/dist/dot.js +1 -1
  45. package/dist/gexf.js +199 -99
  46. package/dist/gexf.js.map +1 -1
  47. package/dist/gml.js +170 -149
  48. package/dist/gml.js.map +1 -1
  49. package/dist/graph-io.js +26 -26
  50. package/dist/graphml.js +2 -2
  51. package/dist/json.js +1 -1
  52. package/dist/neo4j.js +103 -65
  53. package/dist/neo4j.js.map +1 -1
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/attributes.d.ts.map +1 -1
  57. package/dist/src/common/attributes.js +34 -9
  58. package/dist/src/common/attributes.js.map +1 -1
  59. package/dist/src/common/input.d.ts.map +1 -1
  60. package/dist/src/common/input.js +180 -102
  61. package/dist/src/common/input.js.map +1 -1
  62. package/dist/src/common/json-elements.d.ts.map +1 -1
  63. package/dist/src/common/json-elements.js +4 -2
  64. package/dist/src/common/json-elements.js.map +1 -1
  65. package/dist/src/common/text.d.ts +7 -0
  66. package/dist/src/common/text.d.ts.map +1 -1
  67. package/dist/src/common/text.js +15 -7
  68. package/dist/src/common/text.js.map +1 -1
  69. package/dist/src/common/xml.d.ts.map +1 -1
  70. package/dist/src/common/xml.js +2 -1
  71. package/dist/src/common/xml.js.map +1 -1
  72. package/dist/src/common/zip.d.ts.map +1 -1
  73. package/dist/src/common/zip.js +36 -19
  74. package/dist/src/common/zip.js.map +1 -1
  75. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  76. package/dist/src/formats/csv/exporter.js +77 -25
  77. package/dist/src/formats/csv/exporter.js.map +1 -1
  78. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  79. package/dist/src/formats/csv/importer.js +16 -6
  80. package/dist/src/formats/csv/importer.js.map +1 -1
  81. package/dist/src/formats/csv/records.d.ts.map +1 -1
  82. package/dist/src/formats/csv/records.js +156 -110
  83. package/dist/src/formats/csv/records.js.map +1 -1
  84. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  85. package/dist/src/formats/cx/importer.js +108 -53
  86. package/dist/src/formats/cx/importer.js.map +1 -1
  87. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/cx2/exporter.js +2 -1
  89. package/dist/src/formats/cx2/exporter.js.map +1 -1
  90. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  91. package/dist/src/formats/cx2/importer.js +28 -6
  92. package/dist/src/formats/cx2/importer.js.map +1 -1
  93. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  94. package/dist/src/formats/cys/tables.js +11 -8
  95. package/dist/src/formats/cys/tables.js.map +1 -1
  96. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/exporter.js +69 -39
  98. package/dist/src/formats/gexf/exporter.js.map +1 -1
  99. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  100. package/dist/src/formats/gexf/importer.js +138 -50
  101. package/dist/src/formats/gexf/importer.js.map +1 -1
  102. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  103. package/dist/src/formats/gml/exporter.js +51 -35
  104. package/dist/src/formats/gml/exporter.js.map +1 -1
  105. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  106. package/dist/src/formats/gml/importer.js +103 -58
  107. package/dist/src/formats/gml/importer.js.map +1 -1
  108. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  109. package/dist/src/formats/gml/syntax.js +17 -8
  110. package/dist/src/formats/gml/syntax.js.map +1 -1
  111. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/exporter.js +36 -25
  113. package/dist/src/formats/graphml/exporter.js.map +1 -1
  114. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  115. package/dist/src/formats/graphml/importer.js +20 -15
  116. package/dist/src/formats/graphml/importer.js.map +1 -1
  117. package/dist/src/formats/graphml/tree.js +23 -14
  118. package/dist/src/formats/graphml/tree.js.map +1 -1
  119. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  120. package/dist/src/formats/graphml/yfiles.js +22 -10
  121. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  122. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  123. package/dist/src/formats/json/exporter.js +38 -21
  124. package/dist/src/formats/json/exporter.js.map +1 -1
  125. package/dist/src/formats/json/importer.d.ts.map +1 -1
  126. package/dist/src/formats/json/importer.js +536 -343
  127. package/dist/src/formats/json/importer.js.map +1 -1
  128. package/dist/src/formats/json/obographs.js +29 -17
  129. package/dist/src/formats/json/obographs.js.map +1 -1
  130. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  131. package/dist/src/formats/neo4j/exporter.js +73 -43
  132. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  133. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  134. package/dist/src/formats/neo4j/header.js +37 -16
  135. package/dist/src/formats/neo4j/header.js.map +1 -1
  136. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  137. package/dist/src/formats/neo4j/importer.js +21 -11
  138. package/dist/src/formats/neo4j/importer.js.map +1 -1
  139. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  140. package/dist/src/formats/obo/exporter.js +18 -7
  141. package/dist/src/formats/obo/exporter.js.map +1 -1
  142. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  143. package/dist/src/formats/pajek/exporter.js +142 -73
  144. package/dist/src/formats/pajek/exporter.js.map +1 -1
  145. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  146. package/dist/src/formats/pajek/importer.js +26 -15
  147. package/dist/src/formats/pajek/importer.js.map +1 -1
  148. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  149. package/dist/src/formats/pajek/syntax.js +85 -58
  150. package/dist/src/formats/pajek/syntax.js.map +1 -1
  151. package/dist/src/formats/xgmml/emit.d.ts +13 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +112 -50
  154. package/dist/src/formats/xgmml/emit.js.map +1 -1
  155. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  156. package/dist/src/formats/xgmml/exporter.js +92 -34
  157. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  158. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  159. package/dist/src/formats/xgmml/importer.js +38 -18
  160. package/dist/src/formats/xgmml/importer.js.map +1 -1
  161. package/dist/src/sniff.js +109 -57
  162. package/dist/src/sniff.js.map +1 -1
  163. package/dist/xgmml.js +2 -2
  164. package/package.json +2 -2
  165. package/src/common/attributes.ts +59 -19
  166. package/src/common/input.ts +216 -102
  167. package/src/common/json-elements.ts +4 -2
  168. package/src/common/text.ts +24 -15
  169. package/src/common/xml.ts +2 -1
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +103 -39
  172. package/src/formats/csv/importer.ts +17 -6
  173. package/src/formats/csv/records.ts +162 -109
  174. package/src/formats/cx/importer.ts +140 -58
  175. package/src/formats/cx2/exporter.ts +2 -1
  176. package/src/formats/cx2/importer.ts +34 -10
  177. package/src/formats/cys/tables.ts +11 -10
  178. package/src/formats/gexf/exporter.ts +71 -38
  179. package/src/formats/gexf/importer.ts +173 -63
  180. package/src/formats/gml/exporter.ts +78 -82
  181. package/src/formats/gml/importer.ts +119 -68
  182. package/src/formats/gml/syntax.ts +18 -8
  183. package/src/formats/graphml/exporter.ts +46 -27
  184. package/src/formats/graphml/importer.ts +21 -13
  185. package/src/formats/graphml/tree.ts +22 -12
  186. package/src/formats/graphml/yfiles.ts +23 -10
  187. package/src/formats/json/exporter.ts +40 -21
  188. package/src/formats/json/importer.ts +701 -385
  189. package/src/formats/json/obographs.ts +35 -15
  190. package/src/formats/neo4j/exporter.ts +79 -41
  191. package/src/formats/neo4j/header.ts +50 -16
  192. package/src/formats/neo4j/importer.ts +21 -10
  193. package/src/formats/obo/exporter.ts +34 -17
  194. package/src/formats/pajek/exporter.ts +180 -117
  195. package/src/formats/pajek/importer.ts +27 -15
  196. package/src/formats/pajek/syntax.ts +99 -58
  197. package/src/formats/xgmml/emit.ts +122 -50
  198. package/src/formats/xgmml/exporter.ts +103 -35
  199. package/src/formats/xgmml/importer.ts +45 -23
  200. package/src/sniff.ts +115 -59
  201. package/dist/chunks/escape-BpYMQZVa.js.map +0 -1
  202. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  203. package/dist/chunks/exporter-BqKs1Q12.js.map +0 -1
  204. package/dist/chunks/exporter-CNJZF_BN.js.map +0 -1
  205. package/dist/chunks/exporter-DXqCPNrI.js.map +0 -1
  206. package/dist/chunks/importer-B7Ir269L.js.map +0 -1
  207. package/dist/chunks/importer-BfJ1jnNr.js.map +0 -1
  208. package/dist/chunks/importer-CSt-gaYE.js.map +0 -1
  209. package/dist/chunks/importer-CqgYq4Ld.js.map +0 -1
  210. package/dist/chunks/importer-DvFznt5-.js.map +0 -1
  211. package/dist/chunks/importer-PVXkeoaS.js.map +0 -1
  212. package/dist/chunks/records-C9jd7yLD.js.map +0 -1
  213. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -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 => {
@@ -1418,9 +1438,8 @@ function* writeNodeSubtree(
1418
1438
  }
1419
1439
  if (hasChildren(u)) {
1420
1440
  parts.splice(open, 0, ">");
1421
- parts.push(
1422
- `\n${pad}${step}<graph id="${escapeXmlAttribute(`${String(ids.idAt(u))}:`)}" edgedefault="${plan.edgedefault}">`,
1423
- );
1441
+ const graphId = `${String(ids.idAt(u))}:`;
1442
+ parts.push(`\n${pad}${step}<graph id="${escapeXmlAttribute(graphId)}" edgedefault="${plan.edgedefault}">`);
1424
1443
  return parts.join("");
1425
1444
  }
1426
1445
  if (parts.length === open) {
@@ -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
  }
@@ -2462,9 +2452,10 @@ class GraphmlReader implements XmlHandler {
2462
2452
  */
2463
2453
  private expandStar(hyperedge: HyperedgeState, where: Where): void {
2464
2454
  this.hyperedgeCount++;
2465
- let hubId = hyperedge.id ?? `hyperedge${this.hyperedgeCount}`;
2455
+ const baseId = hyperedge.id ?? `hyperedge${this.hyperedgeCount}`;
2456
+ let hubId = baseId;
2466
2457
  for (let n = 2; this.sink.indexOf(hubId) !== INVALID_INDEX; n++) {
2467
- hubId = `${hyperedge.id ?? `hyperedge${this.hyperedgeCount}`}#${n}`;
2458
+ hubId = `${baseId}#${n}`;
2468
2459
  }
2469
2460
  const hub = this.sink.addNode(hubId);
2470
2461
  this.declared.add(hub);
@@ -2728,3 +2719,20 @@ function unknownElementMessage(name: string): string {
2728
2719
  ? `element <${name}> is not allowed at this place in a GraphML file and was skipped`
2729
2720
  : `element <${name}> is not GraphML and was skipped`;
2730
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
+ }
@@ -270,18 +270,7 @@ function writeElements(name: string, value: unknown, indent: string, out: string
270
270
  return;
271
271
  }
272
272
  const object = value as XmlTreeObject;
273
- let attrs = "";
274
- let text: string | null = null;
275
- let hasChildren = false;
276
- for (const [key, child] of Object.entries(object)) {
277
- if (key === TEXT_KEY) {
278
- text = scalarText(child);
279
- } else if (key.startsWith(ATTRIBUTE_PREFIX)) {
280
- attrs += ` ${key.slice(ATTRIBUTE_PREFIX.length)}="${escapeXmlAttribute(scalarText(child))}"`;
281
- } else {
282
- hasChildren = true;
283
- }
284
- }
273
+ const { attrs, text, hasChildren } = scanElement(object);
285
274
  if (!hasChildren) {
286
275
  out.push(
287
276
  text === null || text.length === 0
@@ -303,6 +292,27 @@ function writeElements(name: string, value: unknown, indent: string, out: string
303
292
  out.push(`\n${indent}</${name}>`);
304
293
  }
305
294
 
295
+ /**
296
+ * Split an element object into its attribute text, its text content and whether it has children.
297
+ * @param object - the element object
298
+ * @returns the attribute text, the text content (null when absent) and the children flag
299
+ */
300
+ function scanElement(object: XmlTreeObject): { attrs: string; text: string | null; hasChildren: boolean } {
301
+ let attrs = "";
302
+ let text: string | null = null;
303
+ let hasChildren = false;
304
+ for (const [key, child] of Object.entries(object)) {
305
+ if (key === TEXT_KEY) {
306
+ text = scalarText(child);
307
+ } else if (key.startsWith(ATTRIBUTE_PREFIX)) {
308
+ attrs += ` ${key.slice(ATTRIBUTE_PREFIX.length)}="${escapeXmlAttribute(scalarText(child))}"`;
309
+ } else {
310
+ hasChildren = true;
311
+ }
312
+ }
313
+ return { attrs, text, hasChildren };
314
+ }
315
+
306
316
  /**
307
317
  * The text of a scalar tree value.
308
318
  * @param value - a string, number, boolean or null
@@ -109,16 +109,7 @@ export function staleGraphicsRows(column: Column, table: Iterable<Column>, domai
109
109
  }
110
110
  let stale = 0;
111
111
  for (let r = 0; r < column.length; r++) {
112
- const wants = new Set<string>();
113
- for (const tree of trees) {
114
- const want = graphicsValues(domain, tree.values[r]).find(([f]) => f === field);
115
- if (want !== undefined) {
116
- wants.add(JSON.stringify(want[1]));
117
- }
118
- }
119
- if (wants.size === 0) {
120
- wants.add(JSON.stringify(undefined));
121
- }
112
+ const wants = wantedGraphics(trees, domain, field, r);
122
113
  const value = column.value(r);
123
114
  const have = column.meta.components > 1 && value !== undefined ? Array.from(value as ArrayLike<number>) : value;
124
115
  if (!wants.has(JSON.stringify(have))) {
@@ -128,6 +119,28 @@ export function staleGraphicsRows(column: Column, table: Iterable<Column>, domai
128
119
  return stale;
129
120
  }
130
121
 
122
+ /**
123
+ * The serialized values the trees want for one row of a mapped column.
124
+ * @param trees - the yFiles tree columns of the table
125
+ * @param domain - node or edge
126
+ * @param field - the mapped field name
127
+ * @param r - the row
128
+ * @returns the wanted values as JSON text; just `undefined`'s when no tree wants one
129
+ */
130
+ function wantedGraphics(trees: JsonColumn[], domain: "node" | "edge", field: string, r: number): Set<string> {
131
+ const wants = new Set<string>();
132
+ for (const tree of trees) {
133
+ const want = graphicsValues(domain, tree.values[r]).find(([f]) => f === field);
134
+ if (want !== undefined) {
135
+ wants.add(JSON.stringify(want[1]));
136
+ }
137
+ }
138
+ if (wants.size === 0) {
139
+ wants.add(JSON.stringify(undefined));
140
+ }
141
+ return wants;
142
+ }
143
+
131
144
  /**
132
145
  * The mapped values of one `<data>` tree of a yFiles key: a ShapeNode's for a node, a
133
146
  * PolyLineEdge's for an edge; nothing for any other graphics (GenericNode, BezierEdge, ...).
@@ -1609,7 +1609,6 @@ function* writeCytoscape(snapshot: GraphSnapshot, p: Plan): Generator<string, vo
1609
1609
  const c: Cursor = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
1610
1610
  const { w } = c;
1611
1611
  const parentSlots: ReadonlySet<Slot> = new Set(["parent"]);
1612
- const positionSlots: ReadonlySet<Slot> = new Set(["cytoscapePosition"]);
1613
1612
  w.open("{");
1614
1613
  w.key("elements");
1615
1614
  w.open("{");
@@ -1625,18 +1624,7 @@ function* writeCytoscape(snapshot: GraphSnapshot, p: Plan): Generator<string, vo
1625
1624
  writeSlots(c, p.nodes, i, parentSlots);
1626
1625
  writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
1627
1626
  w.close("}");
1628
- for (const item of p.nodes) {
1629
- if (positionSlots.has(item.slot) && item.column.isSet(i)) {
1630
- const value = item.column.value(i) as ArrayLike<number>;
1631
- const f32 = item.column.dtype === "f32";
1632
- w.key("position");
1633
- w.open("{");
1634
- w.member("x", numberText(value[0], f32, c.nonfinite));
1635
- // stored y-up; Cytoscape's y grows downward
1636
- w.member("y", numberText(flipY(value[1]), f32, c.nonfinite));
1637
- w.close("}");
1638
- }
1639
- }
1627
+ writeCytoscapePosition(c, p.nodes, i);
1640
1628
  writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
1641
1629
  w.close("}");
1642
1630
  yield w.take();
@@ -1673,18 +1661,49 @@ function* writeCytoscape(snapshot: GraphSnapshot, p: Plan): Generator<string, vo
1673
1661
  w.key("data");
1674
1662
  writeGraphObject(c, p);
1675
1663
  }
1676
- const extra = resolved.shape.cytoscape;
1677
- if (extra !== undefined) {
1678
- for (const key of Object.keys(extra)) {
1679
- if (key !== "elements" && key !== "data") {
1680
- w.member(key, valueText(extra[key], false, c.nonfinite));
1681
- }
1682
- }
1683
- }
1664
+ writeCytoscapeExtra(c, resolved.shape.cytoscape);
1684
1665
  w.close("}");
1685
1666
  yield w.take();
1686
1667
  }
1687
1668
 
1669
+ /**
1670
+ * Write a node's `position` member (Cytoscape.js), when its position is set.
1671
+ * @param c - the cursor
1672
+ * @param items - the node plan items
1673
+ * @param i - the node index
1674
+ */
1675
+ function writeCytoscapePosition(c: Cursor, items: readonly ColumnPlan[], i: number): void {
1676
+ const { w } = c;
1677
+ for (const item of items) {
1678
+ if (item.slot === "cytoscapePosition" && item.column.isSet(i)) {
1679
+ const value = item.column.value(i) as ArrayLike<number>;
1680
+ const f32 = item.column.dtype === "f32";
1681
+ w.key("position");
1682
+ w.open("{");
1683
+ w.member("x", numberText(value[0], f32, c.nonfinite));
1684
+ // stored y-up; Cytoscape's y grows downward
1685
+ w.member("y", numberText(flipY(value[1]), f32, c.nonfinite));
1686
+ w.close("}");
1687
+ }
1688
+ }
1689
+ }
1690
+
1691
+ /**
1692
+ * Write the extra top-level members of a Cytoscape.js document, beyond `elements` and `data`.
1693
+ * @param c - the cursor
1694
+ * @param extra - the extra members, when any
1695
+ */
1696
+ function writeCytoscapeExtra(c: Cursor, extra: Readonly<Record<string, unknown>> | undefined): void {
1697
+ if (extra === undefined) {
1698
+ return;
1699
+ }
1700
+ for (const key of Object.keys(extra)) {
1701
+ if (key !== "elements" && key !== "data") {
1702
+ c.w.member(key, valueText(extra[key], false, c.nonfinite));
1703
+ }
1704
+ }
1705
+ }
1706
+
1688
1707
  /**
1689
1708
  * A generator of canonical `e<index>` edge ids for the edges without one (design section 4.6),
1690
1709
  * skipping any text a node id or an explicit edge id already uses (Cytoscape ids share one