@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
@@ -773,12 +773,13 @@ function checkNumberVerification(value: unknown, count: number, report: ImportRe
773
773
  const raw = isRecord(value) ? value.longNumber : undefined;
774
774
  const digits = raw instanceof ExactInteger ? raw.digits : String(raw);
775
775
  if (count > 1 || !NUMBER_VERIFICATION_VALUES.has(digits)) {
776
+ const held = raw === undefined ? null : digits;
776
777
  report.warning(
777
778
  "validation-error",
778
779
  CX_ISSUE.NUMBER_VERIFICATION,
779
780
  count > 1
780
781
  ? "a second numberVerification element; ignored"
781
- : `numberVerification holds ${shown(raw === undefined ? null : digits)}, not 281474976710655`,
782
+ : `numberVerification holds ${shown(held)}, not 281474976710655`,
782
783
  { line, element: "numberVerification" },
783
784
  );
784
785
  }
@@ -993,10 +994,12 @@ async function readDocument(
993
994
  break;
994
995
  }
995
996
  countMember(event.block, null);
997
+ const what =
998
+ keys.length > 1 ? `an object with ${keys.length} keys (${keys.join(", ")})` : shown(value);
996
999
  report.error(
997
1000
  "parse-error",
998
1001
  BAD_ASPECT_BLOCK_CODE,
999
- `a member of the document is ${keys.length > 1 ? `an object with ${keys.length} keys (${keys.join(", ")})` : shown(value)}, not a one-key aspect fragment; skipped`,
1002
+ `a member of the document is ${what}, not a one-key aspect fragment; skipped`,
1000
1003
  { line: event.line, element: keys[0] ?? null },
1001
1004
  );
1002
1005
  break;
@@ -1361,6 +1364,26 @@ function memberCount(members: ReadonlySet<NodeId> | null, total: number): number
1361
1364
 
1362
1365
  // ============================================================ the build
1363
1366
 
1367
+ /** The cyVisualProperties of a graph's view, gathered before they are written. */
1368
+ interface VisualCollection {
1369
+ /** per domain, per property: the [row, text] cells */
1370
+ columns: Record<"node" | "edge", Map<string, [number, string][]>>;
1371
+ /** the network values by property */
1372
+ network: Map<string, string>;
1373
+ defaults: number;
1374
+ mappings: number;
1375
+ dependencies: number;
1376
+ }
1377
+
1378
+ /**
1379
+ * A visual property value as text: a string as it is, anything else as JSON.
1380
+ * @param raw - the value
1381
+ * @returns its text
1382
+ */
1383
+ function visualText(raw: unknown): string {
1384
+ return typeof raw === "string" ? raw : JSON.stringify(raw);
1385
+ }
1386
+
1364
1387
  /** One attribute column of the graph being built. */
1365
1388
  interface AttributeColumn {
1366
1389
  readonly name: string;
@@ -2849,57 +2872,99 @@ class CxReader {
2849
2872
  * applied.
2850
2873
  */
2851
2874
  private readVisualProperties(): void {
2852
- const entries = aspect(this.doc, "cyVisualProperties");
2853
2875
  // Cytoscape's table-cell styles: a visual property aspect graph-io keeps but does not read
2854
2876
  const tableStyles = TABLE_STYLE_ASPECTS.reduce((n, name) => n + (this.doc.kept.get(name)?.length ?? 0), 0);
2855
2877
  // the graph's own view (its first): the one its positions come from; the others stay in meta.extra.cx
2856
2878
  const views = new Set(this.plan.views.slice(0, 1));
2857
- const columns = { node: new Map<string, [number, string][]>(), edge: new Map<string, [number, string][]>() };
2858
- const network = new Map<string, string>();
2859
- let defaults = 0;
2860
- let mappings = 0;
2861
- let dependencies = 0;
2862
- for (const { value } of entries) {
2863
- if (!isRecord(value)) {
2864
- continue;
2865
- }
2866
- const view = value.view === undefined || value.view === null ? null : refId(value.view);
2867
- if (view !== null && views.size > 0 && !views.has(view)) {
2868
- continue;
2879
+ const visual: VisualCollection = {
2880
+ columns: { node: new Map(), edge: new Map() },
2881
+ network: new Map(),
2882
+ defaults: 0,
2883
+ mappings: 0,
2884
+ dependencies: 0,
2885
+ };
2886
+ for (const { value } of aspect(this.doc, "cyVisualProperties")) {
2887
+ if (isRecord(value)) {
2888
+ this.collectVisualEntry(value, views, visual);
2869
2889
  }
2870
- const properties = isRecord(value.properties) ? value.properties : {};
2871
- mappings += isRecord(value.mappings) ? Object.keys(value.mappings).length : 0;
2872
- dependencies += isRecord(value.dependencies) ? Object.keys(value.dependencies).length : 0;
2873
- switch (value.properties_of) {
2874
- case "nodes":
2875
- case "edges": {
2876
- const domain = value.properties_of === "nodes" ? "node" : "edge";
2877
- const target = refId(value.applies_to);
2878
- const rows = domain === "node" ? this.nodeRows : this.edgeRows;
2879
- const row = target === null ? undefined : rows.get(target);
2880
- if (row === undefined) {
2881
- if (target === null || !(domain === "node" ? this.rootNodes : this.rootEdges).has(target)) {
2882
- this.dangle(`${domain} visual property entry`);
2883
- }
2884
- continue;
2885
- }
2886
- for (const [property, raw] of Object.entries(properties)) {
2887
- const list = columns[domain].get(property) ?? [];
2888
- list.push([row, typeof raw === "string" ? raw : JSON.stringify(raw)]);
2889
- columns[domain].set(property, list);
2890
- }
2891
- break;
2890
+ }
2891
+ this.writeBypassColumns(visual.columns);
2892
+ this.writeBypassNetwork(visual.network);
2893
+ this.reportStyleRules(visual, tableStyles);
2894
+ }
2895
+
2896
+ /**
2897
+ * File one cyVisualProperties element of this graph's view into the collection: per-element and
2898
+ * network values by property, defaults, mappings and dependencies counted.
2899
+ * @param value - the element
2900
+ * @param views - the views read (empty: every view)
2901
+ * @param visual - the collection to add to
2902
+ */
2903
+ private collectVisualEntry(
2904
+ value: Record<string, unknown>,
2905
+ views: ReadonlySet<NodeId>,
2906
+ visual: VisualCollection,
2907
+ ): void {
2908
+ const view = value.view === undefined || value.view === null ? null : refId(value.view);
2909
+ if (view !== null && views.size > 0 && !views.has(view)) {
2910
+ return;
2911
+ }
2912
+ const properties = isRecord(value.properties) ? value.properties : {};
2913
+ visual.mappings += isRecord(value.mappings) ? Object.keys(value.mappings).length : 0;
2914
+ visual.dependencies += isRecord(value.dependencies) ? Object.keys(value.dependencies).length : 0;
2915
+ switch (value.properties_of) {
2916
+ case "nodes":
2917
+ this.collectElementProperties("node", value.applies_to, properties, visual.columns);
2918
+ break;
2919
+ case "edges":
2920
+ this.collectElementProperties("edge", value.applies_to, properties, visual.columns);
2921
+ break;
2922
+ case "network":
2923
+ for (const [property, raw] of Object.entries(properties)) {
2924
+ visual.network.set(property, visualText(raw));
2892
2925
  }
2893
- case "network":
2894
- for (const [property, raw] of Object.entries(properties)) {
2895
- network.set(property, typeof raw === "string" ? raw : JSON.stringify(raw));
2896
- }
2897
- break;
2898
- default:
2899
- defaults += Object.keys(properties).length;
2900
- break;
2926
+ break;
2927
+ default:
2928
+ visual.defaults += Object.keys(properties).length;
2929
+ break;
2930
+ }
2931
+ }
2932
+
2933
+ /**
2934
+ * File the visual values of one node or edge under its row; an element not in this graph is
2935
+ * skipped, and reported dangling when it is in no graph of the file.
2936
+ * @param domain - node or edge
2937
+ * @param appliesTo - the element's applies_to
2938
+ * @param properties - its visual properties
2939
+ * @param columns - the per-property cells to add to
2940
+ */
2941
+ private collectElementProperties(
2942
+ domain: "node" | "edge",
2943
+ appliesTo: unknown,
2944
+ properties: Record<string, unknown>,
2945
+ columns: VisualCollection["columns"],
2946
+ ): void {
2947
+ const target = refId(appliesTo);
2948
+ const rows = domain === "node" ? this.nodeRows : this.edgeRows;
2949
+ const row = target === null ? undefined : rows.get(target);
2950
+ if (row === undefined) {
2951
+ if (target === null || !(domain === "node" ? this.rootNodes : this.rootEdges).has(target)) {
2952
+ this.dangle(`${domain} visual property entry`);
2901
2953
  }
2954
+ return;
2902
2955
  }
2956
+ for (const [property, raw] of Object.entries(properties)) {
2957
+ const list = columns[domain].get(property) ?? [];
2958
+ list.push([row, visualText(raw)]);
2959
+ columns[domain].set(property, list);
2960
+ }
2961
+ }
2962
+
2963
+ /**
2964
+ * Write the collected per-element visual values as string columns in the cx.bypass namespace.
2965
+ * @param columns - the per-property cells of each domain
2966
+ */
2967
+ private writeBypassColumns(columns: VisualCollection["columns"]): void {
2903
2968
  for (const domain of ["node", "edge"] as const) {
2904
2969
  for (const [property, cells] of columns[domain]) {
2905
2970
  const handle = declareFresh(
@@ -2922,6 +2987,13 @@ class CxReader {
2922
2987
  }
2923
2988
  }
2924
2989
  }
2990
+ }
2991
+
2992
+ /**
2993
+ * Write the network visual values as graph values in the cx.bypass namespace.
2994
+ * @param network - the value of each property
2995
+ */
2996
+ private writeBypassNetwork(network: ReadonlyMap<string, string>): void {
2925
2997
  for (const [property, text] of network) {
2926
2998
  try {
2927
2999
  this.sink.setGraphValue(property, text, {
@@ -2932,20 +3004,30 @@ class CxReader {
2932
3004
  this.report.recordError(err, { element: property });
2933
3005
  }
2934
3006
  }
2935
- if (defaults + mappings + dependencies + tableStyles > 0) {
2936
- const parts = [
2937
- `cyVisualProperties: ${defaults} default${plural(defaults)}, ${mappings} mapping${plural(mappings)}, ${dependencies} dependenc(ies)`,
2938
- ];
2939
- if (tableStyles > 0) {
2940
- parts.push(`${tableStyles} table style element${plural(tableStyles)}`);
2941
- }
2942
- this.report.warning(
2943
- "unsupported",
2944
- STYLES_NOT_IMPORTED_CODE,
2945
- `the file's style rules are not applied (${parts.join("; ")}); they are kept in meta.extra.cx`,
2946
- { element: "cyVisualProperties" },
2947
- );
3007
+ }
3008
+
3009
+ /**
3010
+ * Warn that the style rules (defaults, mappings, dependencies, table styles) are kept, not applied.
3011
+ * @param visual - the counted rules
3012
+ * @param tableStyles - the number of table style elements
3013
+ */
3014
+ private reportStyleRules(visual: VisualCollection, tableStyles: number): void {
3015
+ const { defaults, mappings, dependencies } = visual;
3016
+ if (defaults + mappings + dependencies + tableStyles === 0) {
3017
+ return;
2948
3018
  }
3019
+ const parts = [
3020
+ `cyVisualProperties: ${defaults} default${plural(defaults)}, ${mappings} mapping${plural(mappings)}, ${dependencies} dependenc(ies)`,
3021
+ ];
3022
+ if (tableStyles > 0) {
3023
+ parts.push(`${tableStyles} table style element${plural(tableStyles)}`);
3024
+ }
3025
+ this.report.warning(
3026
+ "unsupported",
3027
+ STYLES_NOT_IMPORTED_CODE,
3028
+ `the file's style rules are not applied (${parts.join("; ")}); they are kept in meta.extra.cx`,
3029
+ { element: "cyVisualProperties" },
3030
+ );
2949
3031
  }
2950
3032
 
2951
3033
  /**
@@ -304,9 +304,10 @@ function capabilityNotes(
304
304
  continue;
305
305
  }
306
306
  if (gen.code === LOSS.JSON) {
307
+ const what = gen.column === null ? "a column" : `column "${gen.column}"`;
307
308
  note(
308
309
  CX2_LOSS.JSON_AS_STRING,
309
- `${gen.column === null ? "a column" : `column "${gen.column}"`} holds nested values; written as a string attribute holding their JSON text`,
310
+ `${what} holds nested values; written as a string attribute holding their JSON text`,
310
311
  gen.column,
311
312
  gen.count,
312
313
  );
@@ -688,6 +688,25 @@ async function readDocument(
688
688
  return doc;
689
689
  }
690
690
 
691
+ /**
692
+ * Fail the import: the first member of the array is not a descriptor.
693
+ * @param value - the member
694
+ * @param line - its line
695
+ * @param report - the report
696
+ * @returns never: the import fails
697
+ */
698
+ function failNoDescriptor(value: unknown, line: number, report: ImportReportBuilder): never {
699
+ const keys = isRecord(value) ? Object.keys(value) : [];
700
+ const keyList = keys.length > 0 ? ` (keys: ${keys.join(", ")})` : "";
701
+ return report.fail(
702
+ CX2_ISSUE.NO_DESCRIPTOR,
703
+ `the first member of the array is not a descriptor with CXVersion${keyList}`,
704
+ {
705
+ line,
706
+ },
707
+ );
708
+ }
709
+
691
710
  /**
692
711
  * Check the descriptor, the first member of the array.
693
712
  * @param value - the member
@@ -697,12 +716,7 @@ async function readDocument(
697
716
  */
698
717
  function readDescriptor(value: unknown, line: number, doc: Cx2Document, report: ImportReportBuilder): void {
699
718
  if (!isRecord(value) || !("CXVersion" in value)) {
700
- const keys = isRecord(value) ? Object.keys(value) : [];
701
- report.fail(
702
- CX2_ISSUE.NO_DESCRIPTOR,
703
- `the first member of the array is not a descriptor with CXVersion${keys.length > 0 ? ` (keys: ${keys.join(", ")})` : ""}`,
704
- { line },
705
- );
719
+ failNoDescriptor(value, line, report);
706
720
  }
707
721
  const raw = value.CXVersion;
708
722
  const text = typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
@@ -779,10 +793,11 @@ function memberAspect(
779
793
  collect(keys[0], value[keys[0]]);
780
794
  return;
781
795
  }
796
+ const what = keys.length > 1 ? `an object with ${keys.length} keys (${keys.join(", ")})` : shown(value);
782
797
  report.error(
783
798
  "parse-error",
784
799
  BAD_ASPECT_BLOCK_CODE,
785
- `a member of the document is ${keys.length > 1 ? `an object with ${keys.length} keys (${keys.join(", ")})` : shown(value)}, not a one-key aspect block; skipped`,
800
+ `a member of the document is ${what}, not a one-key aspect block; skipped`,
786
801
  { line, element: keys[0] ?? null },
787
802
  );
788
803
  }
@@ -1325,10 +1340,11 @@ class Cx2Reader {
1325
1340
  }
1326
1341
  const other = table.byName.get(alias) ?? table.byAlias.get(alias);
1327
1342
  if (other !== undefined) {
1343
+ const clash = other.name === alias ? "an attribute's name" : `the alias of "${other.name}"`;
1328
1344
  this.report.error(
1329
1345
  "validation-error",
1330
1346
  CX2_ISSUE.ALIAS_CONFLICT,
1331
- `the alias "${alias}" of the ${domain} attribute "${decl.name}" is also ${other.name === alias ? "an attribute's name" : `the alias of "${other.name}"`}; the alias is ignored`,
1347
+ `the alias "${alias}" of the ${domain} attribute "${decl.name}" is also ${clash}; the alias is ignored`,
1332
1348
  { element: decl.name },
1333
1349
  );
1334
1350
  decl.alias = null;
@@ -2259,6 +2275,14 @@ class Cx2Reader {
2259
2275
  }
2260
2276
  }
2261
2277
  }
2278
+ this.checkStatus();
2279
+ structure.checkCounts();
2280
+ }
2281
+
2282
+ /** The status block: present, and exactly one well-formed element. */
2283
+ private checkStatus(): void {
2284
+ const { report } = this;
2285
+ const { structure } = this.doc;
2262
2286
  if (!structure.hasStatus) {
2263
2287
  report.error("validation-error", CX2_ISSUE.NO_STATUS, "the document ends without a status block", {
2264
2288
  element: "status",
@@ -2266,14 +2290,14 @@ class Cx2Reader {
2266
2290
  } else if (!structure.statusWellFormed() || structure.counts.get("status") !== 1) {
2267
2291
  // the specification: exactly one status aspect holding exactly one element
2268
2292
  const count = structure.counts.get("status") ?? 0;
2293
+ const found = count === 1 ? shown(structure.status) : `${count} elements`;
2269
2294
  report.error(
2270
2295
  "validation-error",
2271
2296
  CX2_ISSUE.NO_STATUS,
2272
- `the status is malformed (${count === 1 ? shown(structure.status) : `${count} elements`}): a document ends with one status block holding one object with a boolean success`,
2297
+ `the status is malformed (${found}): a document ends with one status block holding one object with a boolean success`,
2273
2298
  { element: "status" },
2274
2299
  );
2275
2300
  }
2276
- structure.checkCounts();
2277
2301
  }
2278
2302
 
2279
2303
  /** Record the metadata: source format and version, name, description, id type, the kept aspects. */
@@ -217,24 +217,25 @@ export function cellAtt(
217
217
  namespace: string | null,
218
218
  hidden: boolean,
219
219
  ): AttRec | null {
220
+ const cell = { line, namespace, hidden };
220
221
  if (text.startsWith("=")) {
221
- return att(column.name, column.list ? "List" : column.type, null, text, line, namespace, hidden, true);
222
+ return att(column.name, column.list ? "List" : column.type, null, text, true, cell);
222
223
  }
223
224
  if (!column.list) {
224
225
  if (text.length === 0 && column.type !== "String") {
225
226
  return null;
226
227
  }
227
- return att(column.name, column.type, null, text, line, namespace, hidden, false);
228
+ return att(column.name, column.type, null, text, false, cell);
228
229
  }
229
230
  if (text.length === 0 && column.type !== "String") {
230
231
  return null;
231
232
  }
232
- const list = att(column.name, "List", column.type, null, line, namespace, hidden, false);
233
+ const list = att(column.name, "List", column.type, null, false, cell);
233
234
  for (const item of javaSplitLines(text)) {
234
235
  if (item.length === 0 && column.type !== "String") {
235
236
  continue;
236
237
  }
237
- list.children.push(att(column.name, column.type, null, item, line, namespace, hidden, false));
238
+ list.children.push(att(column.name, column.type, null, item, false, cell));
238
239
  }
239
240
  return list;
240
241
  }
@@ -262,10 +263,11 @@ function javaSplitLines(text: string): string[] {
262
263
  * @param cyType - the Cytoscape type
263
264
  * @param elementType - a list's item type, or null
264
265
  * @param value - the value, or null
265
- * @param line - the line
266
- * @param namespace - the table namespace, or null
267
- * @param hidden - a hidden column
268
266
  * @param equation - the value is a formula
267
+ * @param cell - where the cell sits
268
+ * @param cell.line - the line
269
+ * @param cell.namespace - the table namespace, or null
270
+ * @param cell.hidden - a hidden column
269
271
  * @returns the record
270
272
  */
271
273
  function att(
@@ -273,11 +275,10 @@ function att(
273
275
  cyType: string,
274
276
  elementType: string | null,
275
277
  value: string | null,
276
- line: number,
277
- namespace: string | null,
278
- hidden: boolean,
279
278
  equation: boolean,
279
+ cell: { readonly line: number; readonly namespace: string | null; readonly hidden: boolean },
280
280
  ): AttRec {
281
+ const { line, namespace, hidden } = cell;
281
282
  return {
282
283
  name,
283
284
  type: null,
@@ -1225,7 +1225,8 @@ function lifetimeAttrs(roles: RoleColumns, row: number, plan: ExportPlan): strin
1225
1225
  const text = timeText(column, roles.companions.get(column.meta.name) ?? null, row, timeFormat);
1226
1226
  // GEXF 1.2: `startopen` / `endopen` hold the time of a non-inclusive bound (dynamics.xsd
1227
1227
  // time-type) and replace `start` / `end`
1228
- out += ` ${open ? `${name}open` : name}="${escapeXmlAttribute(text)}"`;
1228
+ const attribute = open ? `${name}open` : name;
1229
+ out += ` ${attribute}="${escapeXmlAttribute(text)}"`;
1229
1230
  };
1230
1231
  if (roles.timestamp?.isSet(row)) {
1231
1232
  if (version === "1.3" && plan.timeRepresentation === "timestamp") {
@@ -1240,11 +1241,33 @@ function lifetimeAttrs(roles: RoleColumns, row: number, plan: ExportPlan): strin
1240
1241
  }
1241
1242
  if (version === "1.3" && roles.timestamps?.isSet(row)) {
1242
1243
  const items = listItems(roles.timestamps, row).map((t) => formatTimeValue(t as number, timeFormat));
1243
- out += ` timestamps="${escapeXmlAttribute(`<[${items.join(", ")}]>`)}"`;
1244
+ const stamps = `<[${items.join(", ")}]>`;
1245
+ out += ` timestamps="${escapeXmlAttribute(stamps)}"`;
1244
1246
  }
1245
1247
  return out;
1246
1248
  }
1247
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
+
1248
1271
  /**
1249
1272
  * The `<spells>` element of one element: its spells column, plus (1.2) its timestamps as [t, t].
1250
1273
  * In 1.2 a spell whose spells.open bits mark a bound open writes that bound as `startopen` /
@@ -1274,16 +1297,7 @@ function spellsElement(roles: RoleColumns, row: number, plan: ExportPlan, indent
1274
1297
  }
1275
1298
  let out = `${indent}<spells>\n`;
1276
1299
  for (const [s, e, open] of pairs) {
1277
- let attrs = "";
1278
- if (Number.isFinite(s)) {
1279
- const name = (open & OPEN_START) === 0 ? "start" : "startopen";
1280
- attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
1281
- }
1282
- if (Number.isFinite(e)) {
1283
- const name = (open & OPEN_END) === 0 ? "end" : "endopen";
1284
- attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
1285
- }
1286
- out += `${indent} <spell${attrs}/>\n`;
1300
+ out += `${indent} <spell${spellBounds(s, e, open, plan)}/>\n`;
1287
1301
  }
1288
1302
  return `${out}${indent}</spells>\n`;
1289
1303
  }
@@ -1674,23 +1688,12 @@ function metaElement(meta: GraphSnapshot["meta"]): string {
1674
1688
  }
1675
1689
 
1676
1690
  /**
1677
- * The `<graph>` start tag with its header attributes.
1691
+ * The `idtype` attribute of the `<graph>` tag.
1678
1692
  * @param snapshot - the snapshot
1679
- * @param plan - the plan
1680
- * @returns the line
1693
+ * @returns the attribute text with a leading space, or an empty string
1681
1694
  */
1682
- function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1683
- const { meta } = snapshot;
1684
- let attrs = ` defaultedgetype="${defaultEdgeType(snapshot)}"`;
1685
- let mode = meta.mode ?? "static";
1686
- if (plan.temporal && mode === "static") {
1687
- mode = "dynamic";
1688
- }
1689
- if (plan.version === "1.2" && mode === "slice") {
1690
- mode = "dynamic";
1691
- }
1692
- attrs += ` mode="${mode}"`;
1693
- let { idType } = meta;
1695
+ function graphIdTypeAttr(snapshot: GraphSnapshot): string {
1696
+ let { idType } = snapshot.meta;
1694
1697
  if (idType === null) {
1695
1698
  const { kind } = snapshot.ids;
1696
1699
  if (kind === "string") {
@@ -1699,17 +1702,16 @@ function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1699
1702
  idType = "integer";
1700
1703
  }
1701
1704
  }
1702
- if (idType === "integer" || idType === "string") {
1703
- attrs += ` idtype="${idType}"`;
1704
- }
1705
- if (plan.timeFormat !== null) {
1706
- attrs += ` timeformat="${plan.timeFormat}"`;
1707
- } else if (plan.temporal) {
1708
- attrs += ' timeformat="double"';
1709
- }
1710
- if (plan.version === "1.3" && plan.timeRepresentation !== null) {
1711
- attrs += ` timerepresentation="${plan.timeRepresentation}"`;
1712
- }
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 {
1713
1715
  const timestamp = graphExtraText(snapshot, "timestamp");
1714
1716
  const header: Record<string, string | null> = {
1715
1717
  start: graphExtraText(snapshot, "start"),
@@ -1722,11 +1724,42 @@ function graphStart(snapshot: GraphSnapshot, plan: ExportPlan): string {
1722
1724
  header.end ??= timestamp;
1723
1725
  header.timestamp = null;
1724
1726
  }
1727
+ let attrs = "";
1725
1728
  for (const [key, value] of Object.entries(header)) {
1726
1729
  if (value !== null) {
1727
1730
  attrs += ` ${key}="${escapeXmlAttribute(value)}"`;
1728
1731
  }
1729
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);
1730
1763
  return ` <graph${attrs}>\n`;
1731
1764
  }
1732
1765