@graphty/graph-io 0.3.18 → 0.3.20

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 (235) hide show
  1. package/README.md +80 -2
  2. package/dist/chunks/{escape-D-gZWO26.js → escape-B-tkk_Cl.js} +74 -16
  3. package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
  4. package/dist/chunks/{writer-GAdltGmC.js → export-Bh60Dv-n.js} +7 -252
  5. package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
  6. package/dist/chunks/exporter-BGrsWamJ.js +736 -0
  7. package/dist/chunks/exporter-BGrsWamJ.js.map +1 -0
  8. package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
  9. package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
  10. package/dist/chunks/{importer-Br_QeAeE.js → importer-BWY2FFc5.js} +28 -6
  11. package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
  12. package/dist/chunks/importer-BaQpCEcJ.js +2194 -0
  13. package/dist/chunks/importer-BaQpCEcJ.js.map +1 -0
  14. package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
  15. package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
  16. package/dist/chunks/{importer-aNJfe0qu.js → importer-DEcKhsmQ.js} +2 -2
  17. package/dist/chunks/{importer-aNJfe0qu.js.map → importer-DEcKhsmQ.js.map} +1 -1
  18. package/dist/chunks/{importer-DHagxvDD.js → importer-DIrbbnAf.js} +6 -4
  19. package/dist/chunks/{importer-DHagxvDD.js.map → importer-DIrbbnAf.js.map} +1 -1
  20. package/dist/chunks/importer-D_e7e7LX.js +3055 -0
  21. package/dist/chunks/importer-D_e7e7LX.js.map +1 -0
  22. package/dist/chunks/{importer-Du5crN9l.js → importer-Wv4C4gaF.js} +278 -82
  23. package/dist/chunks/importer-Wv4C4gaF.js.map +1 -0
  24. package/dist/chunks/importer-vi3bSdtw.js +1713 -0
  25. package/dist/chunks/importer-vi3bSdtw.js.map +1 -0
  26. package/dist/chunks/{importer-d0uQxFp6.js → importer-zGLo8Dg_.js} +6 -4
  27. package/dist/chunks/{importer-d0uQxFp6.js.map → importer-zGLo8Dg_.js.map} +1 -1
  28. package/dist/chunks/json-elements-DDS8N2Dd.js +779 -0
  29. package/dist/chunks/json-elements-DDS8N2Dd.js.map +1 -0
  30. package/dist/chunks/{records-Bk9jgodz.js → records-dbRkxwaq.js} +2 -2
  31. package/dist/chunks/{records-Bk9jgodz.js.map → records-dbRkxwaq.js.map} +1 -1
  32. package/dist/chunks/{report-BOk0p5y8.js → report-B1z4WT9e.js} +143 -111
  33. package/dist/chunks/{report-BOk0p5y8.js.map → report-B1z4WT9e.js.map} +1 -1
  34. package/dist/chunks/weights-Dzba96G3.js +176 -0
  35. package/dist/chunks/weights-Dzba96G3.js.map +1 -0
  36. package/dist/chunks/writer-C7flM-Ih.js +77 -0
  37. package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
  38. package/dist/csv.js +6 -4
  39. package/dist/csv.js.map +1 -1
  40. package/dist/cx.d.ts +1 -0
  41. package/dist/cx.js +6 -0
  42. package/dist/cx.js.map +1 -0
  43. package/dist/cx2.d.ts +1 -0
  44. package/dist/cx2.js +10 -0
  45. package/dist/cx2.js.map +1 -0
  46. package/dist/cys.d.ts +1 -0
  47. package/dist/cys.js +6 -0
  48. package/dist/cys.js.map +1 -0
  49. package/dist/dot.js +1 -1
  50. package/dist/gexf.js +16 -4
  51. package/dist/gexf.js.map +1 -1
  52. package/dist/gml.js +13 -14
  53. package/dist/gml.js.map +1 -1
  54. package/dist/graph-io.js +302 -399
  55. package/dist/graph-io.js.map +1 -1
  56. package/dist/graphml.js +1 -1
  57. package/dist/json.js +1 -1
  58. package/dist/neo4j.js +6 -4
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +1 -1
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/common/cell-budget.d.ts +84 -0
  63. package/dist/src/common/cell-budget.d.ts.map +1 -0
  64. package/dist/src/common/cell-budget.js +138 -0
  65. package/dist/src/common/cell-budget.js.map +1 -0
  66. package/dist/src/common/input.d.ts +10 -0
  67. package/dist/src/common/input.d.ts.map +1 -1
  68. package/dist/src/common/input.js +39 -0
  69. package/dist/src/common/input.js.map +1 -1
  70. package/dist/src/common/json-elements.d.ts +286 -0
  71. package/dist/src/common/json-elements.d.ts.map +1 -0
  72. package/dist/src/common/json-elements.js +926 -0
  73. package/dist/src/common/json-elements.js.map +1 -0
  74. package/dist/src/common/options.d.ts +6 -0
  75. package/dist/src/common/options.d.ts.map +1 -1
  76. package/dist/src/common/options.js +1 -1
  77. package/dist/src/common/options.js.map +1 -1
  78. package/dist/src/common/xml.d.ts +37 -3
  79. package/dist/src/common/xml.d.ts.map +1 -1
  80. package/dist/src/common/xml.js +86 -6
  81. package/dist/src/common/xml.js.map +1 -1
  82. package/dist/src/common/zip.d.ts +92 -0
  83. package/dist/src/common/zip.d.ts.map +1 -0
  84. package/dist/src/common/zip.js +399 -0
  85. package/dist/src/common/zip.js.map +1 -0
  86. package/dist/src/formats/cx/importer.d.ts +133 -0
  87. package/dist/src/formats/cx/importer.d.ts.map +1 -0
  88. package/dist/src/formats/cx/importer.js +2220 -0
  89. package/dist/src/formats/cx/importer.js.map +1 -0
  90. package/dist/src/formats/cx/index.d.ts +7 -0
  91. package/dist/src/formats/cx/index.d.ts.map +1 -0
  92. package/dist/src/formats/cx/index.js +7 -0
  93. package/dist/src/formats/cx/index.js.map +1 -0
  94. package/dist/src/formats/cx2/exporter.d.ts +59 -0
  95. package/dist/src/formats/cx2/exporter.d.ts.map +1 -0
  96. package/dist/src/formats/cx2/exporter.js +864 -0
  97. package/dist/src/formats/cx2/exporter.js.map +1 -0
  98. package/dist/src/formats/cx2/importer.d.ts +169 -0
  99. package/dist/src/formats/cx2/importer.d.ts.map +1 -0
  100. package/dist/src/formats/cx2/importer.js +1652 -0
  101. package/dist/src/formats/cx2/importer.js.map +1 -0
  102. package/dist/src/formats/cx2/index.d.ts +7 -0
  103. package/dist/src/formats/cx2/index.d.ts.map +1 -0
  104. package/dist/src/formats/cx2/index.js +7 -0
  105. package/dist/src/formats/cx2/index.js.map +1 -0
  106. package/dist/src/formats/cys/constants.d.ts +68 -0
  107. package/dist/src/formats/cys/constants.d.ts.map +1 -0
  108. package/dist/src/formats/cys/constants.js +76 -0
  109. package/dist/src/formats/cys/constants.js.map +1 -0
  110. package/dist/src/formats/cys/importer.d.ts +38 -0
  111. package/dist/src/formats/cys/importer.d.ts.map +1 -0
  112. package/dist/src/formats/cys/importer.js +830 -0
  113. package/dist/src/formats/cys/importer.js.map +1 -0
  114. package/dist/src/formats/cys/index.d.ts +7 -0
  115. package/dist/src/formats/cys/index.d.ts.map +1 -0
  116. package/dist/src/formats/cys/index.js +7 -0
  117. package/dist/src/formats/cys/index.js.map +1 -0
  118. package/dist/src/formats/cys/session.d.ts +132 -0
  119. package/dist/src/formats/cys/session.d.ts.map +1 -0
  120. package/dist/src/formats/cys/session.js +315 -0
  121. package/dist/src/formats/cys/session.js.map +1 -0
  122. package/dist/src/formats/cys/tables.d.ts +90 -0
  123. package/dist/src/formats/cys/tables.d.ts.map +1 -0
  124. package/dist/src/formats/cys/tables.js +293 -0
  125. package/dist/src/formats/cys/tables.js.map +1 -0
  126. package/dist/src/formats/dot/exporter.d.ts +2 -0
  127. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  128. package/dist/src/formats/dot/exporter.js +12 -0
  129. package/dist/src/formats/dot/exporter.js.map +1 -1
  130. package/dist/src/formats/dot/importer.js +6 -2
  131. package/dist/src/formats/dot/importer.js.map +1 -1
  132. package/dist/src/formats/gexf/exporter.d.ts +2 -0
  133. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  134. package/dist/src/formats/gexf/exporter.js +6 -0
  135. package/dist/src/formats/gexf/exporter.js.map +1 -1
  136. package/dist/src/formats/gexf/importer.d.ts +0 -2
  137. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  138. package/dist/src/formats/gexf/importer.js +1 -3
  139. package/dist/src/formats/gexf/importer.js.map +1 -1
  140. package/dist/src/formats/gexf/index.d.ts +1 -1
  141. package/dist/src/formats/gexf/index.js +2 -2
  142. package/dist/src/formats/gexf/index.js.map +1 -1
  143. package/dist/src/formats/gml/exporter.d.ts +0 -8
  144. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  145. package/dist/src/formats/gml/exporter.js +8 -18
  146. package/dist/src/formats/gml/exporter.js.map +1 -1
  147. package/dist/src/formats/json/importer.d.ts.map +1 -1
  148. package/dist/src/formats/json/importer.js +6 -104
  149. package/dist/src/formats/json/importer.js.map +1 -1
  150. package/dist/src/formats/xgmml/columns.d.ts +152 -0
  151. package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
  152. package/dist/src/formats/xgmml/columns.js +593 -0
  153. package/dist/src/formats/xgmml/columns.js.map +1 -0
  154. package/dist/src/formats/xgmml/constants.d.ts +196 -0
  155. package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
  156. package/dist/src/formats/xgmml/constants.js +197 -0
  157. package/dist/src/formats/xgmml/constants.js.map +1 -0
  158. package/dist/src/formats/xgmml/document.d.ts +361 -0
  159. package/dist/src/formats/xgmml/document.d.ts.map +1 -0
  160. package/dist/src/formats/xgmml/document.js +695 -0
  161. package/dist/src/formats/xgmml/document.js.map +1 -0
  162. package/dist/src/formats/xgmml/emit.d.ts +341 -0
  163. package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
  164. package/dist/src/formats/xgmml/emit.js +1275 -0
  165. package/dist/src/formats/xgmml/emit.js.map +1 -0
  166. package/dist/src/formats/xgmml/exporter.d.ts +24 -0
  167. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
  168. package/dist/src/formats/xgmml/exporter.js +999 -0
  169. package/dist/src/formats/xgmml/exporter.js.map +1 -0
  170. package/dist/src/formats/xgmml/importer.d.ts +58 -0
  171. package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
  172. package/dist/src/formats/xgmml/importer.js +360 -0
  173. package/dist/src/formats/xgmml/importer.js.map +1 -0
  174. package/dist/src/formats/xgmml/index.d.ts +8 -0
  175. package/dist/src/formats/xgmml/index.d.ts.map +1 -0
  176. package/dist/src/formats/xgmml/index.js +8 -0
  177. package/dist/src/formats/xgmml/index.js.map +1 -0
  178. package/dist/src/formats/xgmml/values.d.ts +60 -0
  179. package/dist/src/formats/xgmml/values.d.ts.map +1 -0
  180. package/dist/src/formats/xgmml/values.js +148 -0
  181. package/dist/src/formats/xgmml/values.js.map +1 -0
  182. package/dist/src/index.d.ts +4 -0
  183. package/dist/src/index.d.ts.map +1 -1
  184. package/dist/src/index.js +4 -0
  185. package/dist/src/index.js.map +1 -1
  186. package/dist/src/registry.d.ts +7 -0
  187. package/dist/src/registry.d.ts.map +1 -1
  188. package/dist/src/registry.js +30 -10
  189. package/dist/src/registry.js.map +1 -1
  190. package/dist/src/sniff.d.ts +1 -1
  191. package/dist/src/sniff.d.ts.map +1 -1
  192. package/dist/src/sniff.js +4 -0
  193. package/dist/src/sniff.js.map +1 -1
  194. package/dist/xgmml.d.ts +1 -0
  195. package/dist/xgmml.js +9 -0
  196. package/dist/xgmml.js.map +1 -0
  197. package/package.json +25 -2
  198. package/src/common/cell-budget.ts +170 -0
  199. package/src/common/input.ts +40 -0
  200. package/src/common/json-elements.ts +1147 -0
  201. package/src/common/options.ts +1 -1
  202. package/src/common/xml.ts +127 -6
  203. package/src/common/zip.ts +472 -0
  204. package/src/formats/cx/importer.ts +2733 -0
  205. package/src/formats/cx/index.ts +7 -0
  206. package/src/formats/cx2/exporter.ts +1036 -0
  207. package/src/formats/cx2/importer.ts +2187 -0
  208. package/src/formats/cx2/index.ts +7 -0
  209. package/src/formats/cys/constants.ts +99 -0
  210. package/src/formats/cys/importer.ts +1100 -0
  211. package/src/formats/cys/index.ts +7 -0
  212. package/src/formats/cys/session.ts +428 -0
  213. package/src/formats/cys/tables.ts +400 -0
  214. package/src/formats/dot/exporter.ts +17 -0
  215. package/src/formats/dot/importer.ts +6 -2
  216. package/src/formats/gexf/exporter.ts +11 -0
  217. package/src/formats/gexf/importer.ts +1 -2
  218. package/src/formats/gexf/index.ts +1 -1
  219. package/src/formats/gml/exporter.ts +8 -19
  220. package/src/formats/json/importer.ts +6 -105
  221. package/src/formats/xgmml/columns.ts +747 -0
  222. package/src/formats/xgmml/constants.ts +265 -0
  223. package/src/formats/xgmml/document.ts +975 -0
  224. package/src/formats/xgmml/emit.ts +1563 -0
  225. package/src/formats/xgmml/exporter.ts +1215 -0
  226. package/src/formats/xgmml/importer.ts +491 -0
  227. package/src/formats/xgmml/index.ts +8 -0
  228. package/src/formats/xgmml/values.ts +183 -0
  229. package/src/index.ts +19 -0
  230. package/src/registry.ts +46 -15
  231. package/src/sniff.ts +18 -1
  232. package/dist/chunks/escape-D-gZWO26.js.map +0 -1
  233. package/dist/chunks/importer-Br_QeAeE.js.map +0 -1
  234. package/dist/chunks/importer-Du5crN9l.js.map +0 -1
  235. package/dist/chunks/writer-GAdltGmC.js.map +0 -1
@@ -0,0 +1,747 @@
1
+ /**
2
+ * The attribute columns of one XGMML (or `.cys`) import: every att and every untyped XML
3
+ * attribute is parsed by its own declared kind when it is read, then each column's dtype is
4
+ * decided once over all its values (design section 5.1 widening: int and long to f64, numbers
5
+ * and booleans to string, lists and scalars to json), and only then declared on the sink, so a
6
+ * column is never re-typed after rows were written and a caller's sink needs no widening call.
7
+ */
8
+
9
+ import { type ColumnDecl, type Dtype, type GraphSink, type ScalarDtype } from "@graphty/graph-format";
10
+
11
+ import { declareResolved, DictHeuristic, uniqueColumnName } from "../../common/attributes.js";
12
+ import { type ImportReportBuilder, type IssueLocation } from "../../common/report.js";
13
+ import { inferTextDtype, parseTextCell } from "../../common/text.js";
14
+ import { CYTOSCAPE_ORIGIN_NAMESPACE, FORMAT, XGMML_ISSUE } from "./constants.js";
15
+ import { type AttRec, isListLike } from "./document.js";
16
+ import { attType, elementKind, parseScalar, type ScalarKind, widenScalar } from "./values.js";
17
+
18
+ /** A table of the snapshot. */
19
+ type Domain = "node" | "edge" | "graph";
20
+
21
+ /** One item of a list cell. */
22
+ interface Item {
23
+ readonly value: boolean | number | string;
24
+ readonly text: string;
25
+ }
26
+
27
+ /** One parsed value. */
28
+ interface Cell {
29
+ /** The kind the value was parsed as. */
30
+ readonly kind: ScalarKind | "list" | "json";
31
+ /** A scalar's value, a json value, or null for a list. */
32
+ readonly value: unknown;
33
+ /** A scalar's text as written (what a column widened to string keeps). */
34
+ readonly text: string;
35
+ /** A list's items. */
36
+ readonly items: readonly Item[];
37
+ /** A list's item kind, or null for an empty list whose type nothing states. */
38
+ readonly itemKind: ScalarKind | null;
39
+ /** The kind came from a declared type (not the text grammar of an XML attribute). */
40
+ readonly typed: boolean;
41
+ }
42
+
43
+ /** One column being collected. */
44
+ interface Plan {
45
+ readonly name: string;
46
+ readonly namespace: string | null;
47
+ readonly cells: Map<number, Cell>;
48
+ readonly declared: Set<string>;
49
+ readonly lines: Map<number, number>;
50
+ declaredType: string | null;
51
+ hidden: boolean;
52
+ equation: boolean;
53
+ suid: boolean;
54
+ overflow: boolean;
55
+ precision: boolean;
56
+ }
57
+
58
+ /** Options of a column set. */
59
+ interface ColumnSetOptions {
60
+ /** Decode Cytoscape's `\n` / `\t` escapes in string values. */
61
+ readonly unescape: boolean;
62
+ /** The importer's `long` option. */
63
+ readonly long: "f64" | "string";
64
+ /** The column names the importer's own columns hold; an att of one of these names is renamed. */
65
+ readonly reserved: ReadonlySet<string>;
66
+ }
67
+
68
+ /** The dtype of each scalar kind. */
69
+ const DTYPES: Readonly<Record<ScalarKind, ScalarDtype>> = {
70
+ bool: "bool",
71
+ int: "i32",
72
+ long: "f64",
73
+ real: "f64",
74
+ string: "string",
75
+ };
76
+
77
+ /**
78
+ * Collects the attribute values of one table, then declares and writes the columns.
79
+ */
80
+ export class ColumnSet {
81
+ private readonly domain: Domain;
82
+
83
+ private readonly report: ImportReportBuilder;
84
+
85
+ private readonly options: ColumnSetOptions;
86
+
87
+ private readonly plans = new Map<string, Plan>();
88
+
89
+ /**
90
+ * Create a column set.
91
+ * @param domain - the table
92
+ * @param report - the report
93
+ * @param options - the options
94
+ */
95
+ constructor(domain: Domain, report: ImportReportBuilder, options: ColumnSetOptions) {
96
+ this.domain = domain;
97
+ this.report = report;
98
+ this.options = options;
99
+ }
100
+
101
+ /**
102
+ * Whether a column of that name (any namespace) was collected.
103
+ * @param name - the name
104
+ * @returns true when some row has a value or the column was declared
105
+ */
106
+ has(name: string): boolean {
107
+ return this.plans.has(key(null, name));
108
+ }
109
+
110
+ /**
111
+ * Add one att of a row: parse it by its declared kind (E_BAD_VALUE for a value that does not
112
+ * parse; the cell stays unset). A second value of the same column on one row wins with
113
+ * W_DUPLICATE_ATTRIBUTE.
114
+ * @param row - the row
115
+ * @param att - the att
116
+ * @param where - the element, for issues
117
+ */
118
+ addAtt(row: number, att: AttRec, where: IssueLocation): void {
119
+ const { name } = att;
120
+ if (name === null || name.length === 0) {
121
+ this.badAtt("an <att> without a name was skipped", att, "name");
122
+ return;
123
+ }
124
+ const plan = this.plan(name, att.namespace ?? null);
125
+ const type = attType(att);
126
+ plan.hidden ||= att.hidden;
127
+ plan.suid ||= name.endsWith(".SUID");
128
+ if (type.declared !== null) {
129
+ plan.declaredType ??= type.declared;
130
+ }
131
+ const at = { line: att.line, element: where.element ?? name };
132
+ if (type.unknown !== null) {
133
+ this.report.warnOnce(
134
+ "unsupported",
135
+ XGMML_ISSUE.UNKNOWN_ATTR_TYPE,
136
+ `att type "${type.unknown}" of "${name}" is not an XGMML or Cytoscape type; its values are kept as text`,
137
+ at,
138
+ `${XGMML_ISSUE.UNKNOWN_ATTR_TYPE}:${this.domain}:${name}`,
139
+ );
140
+ }
141
+ let cell: Cell | null;
142
+ if (att.equation) {
143
+ plan.equation = true;
144
+ this.report.warnOnce(
145
+ "unsupported",
146
+ XGMML_ISSUE.EQUATION_AS_TEXT,
147
+ `"${name}" holds Cytoscape formulas; each is kept as its text, never evaluated`,
148
+ at,
149
+ `${XGMML_ISSUE.EQUATION_AS_TEXT}:${this.domain}:${name}`,
150
+ );
151
+ cell = att.value === null ? null : scalarCell("string", att.value, att.value, true);
152
+ if (cell !== null) {
153
+ plan.declared.add("string");
154
+ }
155
+ } else if (type.kind === "list") {
156
+ cell = this.listCell(att, name, plan, at);
157
+ plan.declared.add("list");
158
+ } else if (type.kind === "map") {
159
+ cell = jsonCell(recordOf(att));
160
+ this.recordList(`"${name}" is a 2.x map; it is kept as a json record`, at);
161
+ plan.declared.add("json");
162
+ } else {
163
+ cell = this.scalarAtt(att, type.kind, plan, at);
164
+ }
165
+ if (cell !== null) {
166
+ this.set(plan, row, cell, at);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Add one value read by the 5.1 text grammar (an XML attribute of an element).
172
+ * @param row - the row
173
+ * @param name - the column name
174
+ * @param text - the text
175
+ * @param where - the element, for issues
176
+ */
177
+ addText(row: number, name: string, text: string, where: IssueLocation): void {
178
+ const plan = this.plan(name, null);
179
+ const value = parseTextCell(text);
180
+ const kind = textKind(inferTextDtype(text));
181
+ this.set(plan, row, { kind, value, text, items: [], itemKind: null, typed: false }, where);
182
+ }
183
+
184
+ /**
185
+ * Add one value of a known kind (a typed table cell of a session).
186
+ * @param row - the row
187
+ * @param name - the column name
188
+ * @param value - the value
189
+ * @param where - the element, for issues
190
+ */
191
+ addJson(row: number, name: string, value: unknown, where: IssueLocation): void {
192
+ this.set(this.plan(name, null), row, jsonCell(value), where);
193
+ }
194
+
195
+ /**
196
+ * Declare every collected column on the sink and write its values.
197
+ * @param sink - the sink
198
+ * @param rowIndex - the sink index of a row (node or edge index), or -1 when the row was not added
199
+ */
200
+ write(sink: GraphSink, rowIndex: (row: number) => number): void {
201
+ const taken = new Set<string>(this.options.reserved);
202
+ const ordered = [...this.plans.values()].sort(
203
+ (a, b) => Number(a.namespace !== null) - Number(b.namespace !== null),
204
+ );
205
+ for (const plan of ordered) {
206
+ let { name } = plan;
207
+ if (taken.has(name)) {
208
+ name = uniqueColumnName(plan.name, plan.namespace, (n) => taken.has(n));
209
+ this.report.warning(
210
+ "coercion",
211
+ XGMML_ISSUE.COLUMN_RENAMED,
212
+ `${this.domain} attribute "${plan.name}" renamed to "${name}": the name was taken`,
213
+ { element: plan.name },
214
+ );
215
+ }
216
+ taken.add(name);
217
+ this.writePlan(sink, plan, name, rowIndex);
218
+ }
219
+ }
220
+
221
+ // ------------------------------------------------------------------ cells
222
+
223
+ /**
224
+ * The plan of a column, created on first use.
225
+ * @param name - the attribute name
226
+ * @param namespace - the table namespace, or null
227
+ * @returns the plan
228
+ */
229
+ private plan(name: string, namespace: string | null): Plan {
230
+ const k = key(namespace, name);
231
+ let plan = this.plans.get(k);
232
+ if (plan === undefined) {
233
+ plan = {
234
+ name,
235
+ namespace,
236
+ cells: new Map(),
237
+ declared: new Set(),
238
+ lines: new Map(),
239
+ declaredType: null,
240
+ hidden: false,
241
+ equation: false,
242
+ suid: false,
243
+ overflow: false,
244
+ precision: false,
245
+ };
246
+ this.plans.set(k, plan);
247
+ }
248
+ return plan;
249
+ }
250
+
251
+ /**
252
+ * Store a cell, reporting a second value on the same row.
253
+ * @param plan - the column
254
+ * @param row - the row
255
+ * @param cell - the cell
256
+ * @param where - the location
257
+ */
258
+ private set(plan: Plan, row: number, cell: Cell, where: IssueLocation): void {
259
+ if (plan.cells.has(row)) {
260
+ this.report.warning(
261
+ "validation-error",
262
+ XGMML_ISSUE.DUPLICATE_ATTRIBUTE,
263
+ `"${plan.name}" is given twice on one ${this.domain}; the later value is kept`,
264
+ { line: where.line ?? null, element: where.element ?? plan.name },
265
+ );
266
+ }
267
+ plan.cells.set(row, cell);
268
+ if (where.line !== undefined && where.line !== null) {
269
+ plan.lines.set(row, where.line);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Parse a scalar att.
275
+ * @param att - the att
276
+ * @param kind - the declared kind
277
+ * @param plan - the column
278
+ * @param at - the location
279
+ * @returns the cell, or null when unset or invalid
280
+ */
281
+ private scalarAtt(att: AttRec, kind: ScalarKind, plan: Plan, at: IssueLocation): Cell | null {
282
+ if (att.children.length > 0) {
283
+ this.badAtt(`the ${kind} att "${att.name ?? ""}" holds child atts; they are ignored`, att, "children");
284
+ }
285
+ if (att.xml !== null) {
286
+ this.recordList(`"${att.name ?? ""}" holds foreign XML; it is kept as text in a json column`, at);
287
+ plan.declared.add("json");
288
+ return jsonCell(att.xml);
289
+ }
290
+ if (att.value === null) {
291
+ plan.declared.add(kind);
292
+ return null;
293
+ }
294
+ const parsed = parseScalar(att.value, kind, this.options.unescape);
295
+ if (parsed === null || (parsed.overflow !== undefined && kind === "int" && exactInteger(att.cyType))) {
296
+ this.badValue(att.value, kind, at);
297
+ return null;
298
+ }
299
+ plan.declared.add(kind);
300
+ if (parsed.overflow === "i32") {
301
+ plan.overflow = true;
302
+ } else if (parsed.overflow === "precision") {
303
+ plan.precision = true;
304
+ plan.overflow ||= kind === "int";
305
+ }
306
+ return scalarCell(kind, parsed.value, att.value, true);
307
+ }
308
+
309
+ /**
310
+ * Parse a list att: items by `cy:elementType`, else by the widened kinds of the children.
311
+ * A list of lists, a list with records, or a list of named fields is a json value.
312
+ * @param att - the att
313
+ * @param name - the column name
314
+ * @param plan - the column (an item beyond i32 widens it, as a scalar does)
315
+ * @param at - the location
316
+ * @returns the cell
317
+ */
318
+ private listCell(att: AttRec, name: string, plan: Plan, at: IssueLocation): Cell | null {
319
+ if (att.value !== null) {
320
+ this.badAtt(`the list att "${name}" has a value; it is ignored`, att, "list-value");
321
+ }
322
+ const { children } = att;
323
+ const names = new Set(children.map((c) => c.name).filter((n): n is string => n !== null && n !== name));
324
+ if (names.size > 1 || children.some((c) => c.children.length > 0 || c.xml !== null)) {
325
+ this.recordList(`"${name}" is a record or a list of lists; it is kept as json`, at);
326
+ return jsonCell(names.size > 1 ? recordOf(att) : children.map(jsonOfAtt));
327
+ }
328
+ let itemKind = elementKind(att.elementType);
329
+ if (itemKind === null) {
330
+ const kinds = new Set<ScalarKind>();
331
+ for (const child of children) {
332
+ const type = attType(child);
333
+ const kind: ScalarKind = type.kind === "list" || type.kind === "map" ? "string" : type.kind;
334
+ kinds.add(kind);
335
+ itemKind = itemKind === null ? kind : widenScalar(itemKind, kind);
336
+ }
337
+ if (kinds.size > 1) {
338
+ this.report.warnOnce(
339
+ "coercion",
340
+ XGMML_ISSUE.WIDENED,
341
+ `the items of list "${name}" declare different types; they are read as ${itemKind ?? "string"}`,
342
+ at,
343
+ `${XGMML_ISSUE.WIDENED}:${this.domain}:${name}`,
344
+ );
345
+ }
346
+ }
347
+ const items: Item[] = [];
348
+ for (const child of children) {
349
+ if (child.value === null) {
350
+ continue;
351
+ }
352
+ const parsed = parseScalar(child.value, itemKind ?? "string", this.options.unescape);
353
+ const exact = exactInteger(att.elementType) || exactInteger(child.cyType);
354
+ if (parsed === null || (parsed.overflow !== undefined && itemKind === "int" && exact)) {
355
+ this.badValue(child.value, itemKind ?? "string", { line: child.line, element: at.element });
356
+ continue;
357
+ }
358
+ if (parsed.overflow === "i32") {
359
+ plan.overflow = true;
360
+ } else if (parsed.overflow === "precision") {
361
+ plan.precision = true;
362
+ plan.overflow ||= itemKind === "int";
363
+ }
364
+ items.push({ value: parsed.value, text: child.value });
365
+ }
366
+ return { kind: "list", value: null, text: "", items, itemKind, typed: true };
367
+ }
368
+
369
+ // ------------------------------------------------------------------ decision and writing
370
+
371
+ /**
372
+ * Decide one column's dtype, declare it and write its values.
373
+ * @param sink - the sink
374
+ * @param plan - the column
375
+ * @param name - the name to declare it under
376
+ * @param rowIndex - the row to sink index map
377
+ */
378
+ private writePlan(sink: GraphSink, plan: Plan, name: string, rowIndex: (row: number) => number): void {
379
+ const cells = [...plan.cells.values()];
380
+ const decision = this.decide(plan, cells);
381
+ const decl: ColumnDecl = {
382
+ name,
383
+ dtype: decision.dtype,
384
+ nullable: true,
385
+ origin: {
386
+ format: FORMAT,
387
+ id: null,
388
+ title: name === plan.name ? null : plan.name,
389
+ type: originType(plan, decision),
390
+ namespace: plan.namespace ?? (plan.hidden ? CYTOSCAPE_ORIGIN_NAMESPACE : null),
391
+ },
392
+ };
393
+ if (decision.itemDtype !== null) {
394
+ decl.itemDtype = decision.itemDtype;
395
+ }
396
+ const extra: Record<string, unknown> = {};
397
+ if (plan.hidden) {
398
+ extra.hidden = true;
399
+ }
400
+ if (plan.suid) {
401
+ extra.suidReference = true;
402
+ }
403
+ if (plan.equation) {
404
+ extra.equation = true;
405
+ }
406
+ if (Object.keys(extra).length > 0) {
407
+ decl.extra = extra;
408
+ }
409
+ const where = { element: name };
410
+ if (this.domain === "graph") {
411
+ const cell = plan.cells.get(0);
412
+ try {
413
+ sink.setGraphValue(name, cell === undefined ? undefined : convert(cell, decision), decl);
414
+ } catch (err) {
415
+ this.report.recordError(err, where);
416
+ }
417
+ return;
418
+ }
419
+ let handle;
420
+ try {
421
+ ({ handle } = declareResolved(sink, this.domain, decl, this.report, where));
422
+ } catch (err) {
423
+ this.report.recordError(err, where);
424
+ return;
425
+ }
426
+ for (const [row, cell] of plan.cells) {
427
+ const index = rowIndex(row);
428
+ if (index < 0) {
429
+ continue;
430
+ }
431
+ const value = convert(cell, decision);
432
+ try {
433
+ if (this.domain === "node") {
434
+ sink.setNodeValue(handle, index, value);
435
+ } else {
436
+ sink.setEdgeValue(handle, index, value);
437
+ }
438
+ } catch (err) {
439
+ this.report.recordError(err, { line: plan.lines.get(row) ?? null, element: name });
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * The dtype of a column from its cells, with the widening and precision warnings.
446
+ * @param plan - the column
447
+ * @param cells - its cells
448
+ * @returns the decision
449
+ */
450
+ private decide(plan: Plan, cells: readonly Cell[]): Decision {
451
+ const where = { element: plan.name };
452
+ const typedKinds = new Set(cells.filter((c) => c.typed).map((c) => c.kind));
453
+ let widened = typedKinds.size > 1 || plan.declared.size > 1;
454
+ let decision: Decision;
455
+ if (
456
+ cells.some((c) => c.kind === "json") ||
457
+ (cells.some((c) => c.kind === "list") && cells.some((c) => c.kind !== "list"))
458
+ ) {
459
+ decision = { dtype: "json", itemDtype: null, scalar: null, item: null, long: false };
460
+ } else if (cells.length > 0 && cells.every((c) => c.kind === "list")) {
461
+ let item: ScalarKind | null = null;
462
+ for (const cell of cells) {
463
+ if (cell.itemKind !== null) {
464
+ widened ||= item !== null && item !== cell.itemKind;
465
+ item = item === null ? cell.itemKind : widenScalar(item, cell.itemKind);
466
+ }
467
+ }
468
+ if (item === null) {
469
+ this.report.warnOnce(
470
+ "coercion",
471
+ XGMML_ISSUE.EMPTY_LIST_TYPE,
472
+ `list "${plan.name}" has no element type and no items; it is a list of strings`,
473
+ where,
474
+ `${XGMML_ISSUE.EMPTY_LIST_TYPE}:${this.domain}:${plan.name}`,
475
+ );
476
+ item = "string";
477
+ }
478
+ if (plan.overflow && item === "int") {
479
+ item = "real";
480
+ widened = true;
481
+ }
482
+ decision = { dtype: "list", itemDtype: DTYPES[item], scalar: null, item, long: item === "long" };
483
+ } else {
484
+ decision = this.decideScalar(plan, cells);
485
+ widened ||= plan.overflow && decision.scalar === "real" && plan.declared.has("int");
486
+ }
487
+ if (widened) {
488
+ this.report.warnOnce(
489
+ "coercion",
490
+ XGMML_ISSUE.WIDENED,
491
+ `${this.domain} column "${plan.name}" was widened to ${decision.dtype}: its values or declared types disagree`,
492
+ where,
493
+ `${XGMML_ISSUE.WIDENED}:${this.domain}:${plan.name}`,
494
+ );
495
+ }
496
+ if (plan.precision) {
497
+ this.report.warnOnce(
498
+ "precision",
499
+ XGMML_ISSUE.PRECISION,
500
+ `values of "${plan.name}" beyond what a double holds exactly are stored as the nearest double (or infinity)`,
501
+ where,
502
+ `${XGMML_ISSUE.PRECISION}:${this.domain}:${plan.name}`,
503
+ );
504
+ }
505
+ return decision;
506
+ }
507
+
508
+ /**
509
+ * The scalar decision of a column.
510
+ * @param plan - the column
511
+ * @param cells - its cells
512
+ * @returns the decision
513
+ */
514
+ private decideScalar(plan: Plan, cells: readonly Cell[]): Decision {
515
+ let scalar: ScalarKind | null = null;
516
+ for (const cell of cells) {
517
+ const kind = cell.kind as ScalarKind;
518
+ scalar = scalar === null ? kind : widenScalar(scalar, kind);
519
+ }
520
+ if (scalar === null) {
521
+ const declared = [...plan.declared].find((k): k is ScalarKind => k in DTYPES);
522
+ scalar = declared ?? "string";
523
+ }
524
+ if (scalar === "int" && plan.overflow) {
525
+ scalar = "real";
526
+ }
527
+ const long = scalar === "long";
528
+ if (long && this.options.long === "string") {
529
+ return { dtype: "string", itemDtype: null, scalar: "string", item: null, long: true };
530
+ }
531
+ let dtype: Dtype = DTYPES[scalar];
532
+ if (scalar === "string") {
533
+ const heuristic = new DictHeuristic();
534
+ for (const cell of cells) {
535
+ if (heuristic.observe(cell.text)) {
536
+ break;
537
+ }
538
+ }
539
+ dtype = heuristic.decide();
540
+ }
541
+ return { dtype, itemDtype: null, scalar, item: null, long };
542
+ }
543
+
544
+ /**
545
+ * Report a malformed att, once per kind of defect.
546
+ * @param message - the message
547
+ * @param att - the att
548
+ * @param kind - the defect, for the once key
549
+ */
550
+ private badAtt(message: string, att: AttRec, kind: string): void {
551
+ this.report.warnOnce(
552
+ "parse-error",
553
+ XGMML_ISSUE.BAD_ATT,
554
+ message,
555
+ { line: att.line, element: att.name },
556
+ `${XGMML_ISSUE.BAD_ATT}:${kind}`,
557
+ );
558
+ }
559
+
560
+ /**
561
+ * Report a value that does not parse as its declared kind (an error; the cell stays unset).
562
+ * @param text - the value
563
+ * @param kind - the kind
564
+ * @param at - the location
565
+ */
566
+ private badValue(text: string, kind: ScalarKind, at: IssueLocation): void {
567
+ this.report.error(
568
+ "validation-error",
569
+ XGMML_ISSUE.BAD_VALUE,
570
+ `value ${JSON.stringify(text)} is not a valid ${KIND_NAMES[kind]}; the cell is left unset`,
571
+ at,
572
+ );
573
+ }
574
+
575
+ /**
576
+ * Report a record list, list of lists, map or foreign XML kept as json, once.
577
+ * @param message - the message
578
+ * @param at - the location
579
+ */
580
+ private recordList(message: string, at: IssueLocation): void {
581
+ this.report.warnOnce("coercion", XGMML_ISSUE.RECORD_LIST, message, at);
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Whether a Cytoscape type names Java's Integer exactly (`cy:type="Integer"`, a CyCSV
587
+ * `java.lang.Integer` column): a value beyond i32 is then not an Integer at all (Cytoscape cannot
588
+ * write one), where the XGMML `integer` of pre-3.3 Cytoscape also carried Longs and widens.
589
+ * @param cyType - the Cytoscape type, or null
590
+ * @returns true for Integer
591
+ */
592
+ function exactInteger(cyType: string | null): boolean {
593
+ return cyType?.trim().toLowerCase() === "integer";
594
+ }
595
+
596
+ /** A column's decided storage. */
597
+ interface Decision {
598
+ readonly dtype: Dtype;
599
+ readonly itemDtype: ScalarDtype | null;
600
+ readonly scalar: ScalarKind | null;
601
+ readonly item: ScalarKind | null;
602
+ /** The column is a declared long (origin.type "long"). */
603
+ readonly long: boolean;
604
+ }
605
+
606
+ /**
607
+ * The `origin.type` of a column: "equation" for formulas, "long" for a declared long, else the
608
+ * declared type text.
609
+ * @param plan - the column
610
+ * @param decision - its decision
611
+ * @returns the type text, or null
612
+ */
613
+ function originType(plan: Plan, decision: Decision): string | null {
614
+ if (plan.equation) {
615
+ return "equation";
616
+ }
617
+ return decision.long ? "long" : plan.declaredType;
618
+ }
619
+
620
+ /** How a kind is named in messages. */
621
+ const KIND_NAMES: Readonly<Record<ScalarKind, string>> = {
622
+ bool: "boolean (1, 0, true, false, yes, no)",
623
+ int: "integer",
624
+ long: "long integer",
625
+ real: "real number",
626
+ string: "string",
627
+ };
628
+
629
+ /**
630
+ * The plans key of a column.
631
+ * @param namespace - the namespace, or null
632
+ * @param name - the name
633
+ * @returns the key
634
+ */
635
+ function key(namespace: string | null, name: string): string {
636
+ return namespace === null ? name : `${namespace}\u0000${name}`;
637
+ }
638
+
639
+ /**
640
+ * The scalar kind of a text grammar dtype.
641
+ * @param dtype - the dtype
642
+ * @returns the kind
643
+ */
644
+ function textKind(dtype: "bool" | "i32" | "f64" | "string"): ScalarKind {
645
+ switch (dtype) {
646
+ case "bool":
647
+ return "bool";
648
+ case "i32":
649
+ return "int";
650
+ case "f64":
651
+ return "real";
652
+ default:
653
+ return "string";
654
+ }
655
+ }
656
+
657
+ /**
658
+ * A scalar cell.
659
+ * @param kind - its kind
660
+ * @param value - its value
661
+ * @param text - its text as written
662
+ * @param typed - whether a declared type gave the kind
663
+ * @returns the cell
664
+ */
665
+ function scalarCell(kind: ScalarKind, value: unknown, text: string, typed: boolean): Cell {
666
+ return { kind, value, text, items: [], itemKind: null, typed };
667
+ }
668
+
669
+ /**
670
+ * A json cell.
671
+ * @param value - the value
672
+ * @returns the cell
673
+ */
674
+ function jsonCell(value: unknown): Cell {
675
+ return { kind: "json", value, text: JSON.stringify(value), items: [], itemKind: null, typed: true };
676
+ }
677
+
678
+ /**
679
+ * An att with named children as a record of name to value.
680
+ * @param att - the att
681
+ * @returns the record
682
+ */
683
+ function recordOf(att: AttRec): Record<string, unknown> {
684
+ const out: Record<string, unknown> = {};
685
+ for (const child of att.children) {
686
+ out[child.name ?? ""] = jsonOfAtt(child);
687
+ }
688
+ return out;
689
+ }
690
+
691
+ /**
692
+ * An att as a json value: its value text, its children as an array or a record, or its XML.
693
+ * @param att - the att
694
+ * @returns the value
695
+ */
696
+ function jsonOfAtt(att: AttRec): unknown {
697
+ if (att.xml !== null) {
698
+ return att.xml;
699
+ }
700
+ if (att.children.length === 0) {
701
+ return att.value;
702
+ }
703
+ return isListLike(att) ? att.children.map(jsonOfAtt) : recordOf(att);
704
+ }
705
+
706
+ /**
707
+ * A cell as the value its column's dtype stores.
708
+ * @param cell - the cell
709
+ * @param decision - the column's decision
710
+ * @returns the value
711
+ */
712
+ function convert(cell: Cell, decision: Decision): unknown {
713
+ if (decision.dtype === "json") {
714
+ if (cell.kind === "json") {
715
+ return cell.value;
716
+ }
717
+ return cell.kind === "list" ? cell.items.map((i) => i.value) : cell.value;
718
+ }
719
+ if (decision.dtype === "list") {
720
+ return cell.items.map((item) => convertScalar(item.value, item.text, decision.item ?? "string"));
721
+ }
722
+ return convertScalar(
723
+ cell.value,
724
+ cell.text,
725
+ decision.long && decision.dtype === "string" ? "string" : (decision.scalar ?? "string"),
726
+ );
727
+ }
728
+
729
+ /**
730
+ * A scalar as the kind its column stores.
731
+ * @param value - the parsed value
732
+ * @param text - its text as written
733
+ * @param kind - the column's kind
734
+ * @returns the value
735
+ */
736
+ function convertScalar(value: unknown, text: string, kind: ScalarKind): unknown {
737
+ switch (kind) {
738
+ case "bool":
739
+ return value;
740
+ case "int":
741
+ case "long":
742
+ case "real":
743
+ return Number(value);
744
+ default:
745
+ return typeof value === "string" ? value : text;
746
+ }
747
+ }