@graphty/graph-io 0.3.18 → 0.3.19

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 (118) hide show
  1. package/README.md +37 -2
  2. package/dist/chunks/{escape-D-gZWO26.js → escape-CWExcecC.js} +3 -3
  3. package/dist/chunks/{escape-D-gZWO26.js.map → escape-CWExcecC.js.map} +1 -1
  4. package/dist/chunks/exporter-CbMZyVt-.js +735 -0
  5. package/dist/chunks/exporter-CbMZyVt-.js.map +1 -0
  6. package/dist/chunks/importer-BAP4PrxR.js +2194 -0
  7. package/dist/chunks/importer-BAP4PrxR.js.map +1 -0
  8. package/dist/chunks/{importer-Br_QeAeE.js → importer-BNFuV1-K.js} +5 -4
  9. package/dist/chunks/{importer-Br_QeAeE.js.map → importer-BNFuV1-K.js.map} +1 -1
  10. package/dist/chunks/{importer-d0uQxFp6.js → importer-BVGtU1NA.js} +5 -4
  11. package/dist/chunks/{importer-d0uQxFp6.js.map → importer-BVGtU1NA.js.map} +1 -1
  12. package/dist/chunks/{importer-DHagxvDD.js → importer-BW-Ft2ps.js} +5 -4
  13. package/dist/chunks/{importer-DHagxvDD.js.map → importer-BW-Ft2ps.js.map} +1 -1
  14. package/dist/chunks/{importer-Du5crN9l.js → importer-Bm1_5vPS.js} +275 -82
  15. package/dist/chunks/importer-Bm1_5vPS.js.map +1 -0
  16. package/dist/chunks/{importer-aNJfe0qu.js → importer-ByPGO-09.js} +2 -2
  17. package/dist/chunks/{importer-aNJfe0qu.js.map → importer-ByPGO-09.js.map} +1 -1
  18. package/dist/chunks/importer-DOepkgnG.js +1713 -0
  19. package/dist/chunks/importer-DOepkgnG.js.map +1 -0
  20. package/dist/chunks/json-elements-CZY1wiZh.js +779 -0
  21. package/dist/chunks/json-elements-CZY1wiZh.js.map +1 -0
  22. package/dist/chunks/{records-Bk9jgodz.js → records-BzNicMsf.js} +2 -2
  23. package/dist/chunks/{records-Bk9jgodz.js.map → records-BzNicMsf.js.map} +1 -1
  24. package/dist/chunks/{report-BOk0p5y8.js → report-BcWboivV.js} +95 -95
  25. package/dist/chunks/{report-BOk0p5y8.js.map → report-BcWboivV.js.map} +1 -1
  26. package/dist/chunks/weights-CwISIpCP.js +176 -0
  27. package/dist/chunks/weights-CwISIpCP.js.map +1 -0
  28. package/dist/chunks/{writer-GAdltGmC.js → writer-DQiKgQJc.js} +8 -179
  29. package/dist/chunks/writer-DQiKgQJc.js.map +1 -0
  30. package/dist/csv.js +5 -4
  31. package/dist/csv.js.map +1 -1
  32. package/dist/cx.d.ts +1 -0
  33. package/dist/cx.js +6 -0
  34. package/dist/cx.js.map +1 -0
  35. package/dist/cx2.d.ts +1 -0
  36. package/dist/cx2.js +10 -0
  37. package/dist/cx2.js.map +1 -0
  38. package/dist/dot.js +1 -1
  39. package/dist/gexf.js +15 -4
  40. package/dist/gexf.js.map +1 -1
  41. package/dist/gml.js +5 -4
  42. package/dist/gml.js.map +1 -1
  43. package/dist/graph-io.js +145 -389
  44. package/dist/graph-io.js.map +1 -1
  45. package/dist/graphml.js +1 -1
  46. package/dist/json.js +1 -1
  47. package/dist/neo4j.js +5 -4
  48. package/dist/neo4j.js.map +1 -1
  49. package/dist/obo.js +1 -1
  50. package/dist/pajek.js +1 -1
  51. package/dist/src/common/json-elements.d.ts +286 -0
  52. package/dist/src/common/json-elements.d.ts.map +1 -0
  53. package/dist/src/common/json-elements.js +926 -0
  54. package/dist/src/common/json-elements.js.map +1 -0
  55. package/dist/src/formats/cx/importer.d.ts +133 -0
  56. package/dist/src/formats/cx/importer.d.ts.map +1 -0
  57. package/dist/src/formats/cx/importer.js +2220 -0
  58. package/dist/src/formats/cx/importer.js.map +1 -0
  59. package/dist/src/formats/cx/index.d.ts +7 -0
  60. package/dist/src/formats/cx/index.d.ts.map +1 -0
  61. package/dist/src/formats/cx/index.js +7 -0
  62. package/dist/src/formats/cx/index.js.map +1 -0
  63. package/dist/src/formats/cx2/exporter.d.ts +59 -0
  64. package/dist/src/formats/cx2/exporter.d.ts.map +1 -0
  65. package/dist/src/formats/cx2/exporter.js +864 -0
  66. package/dist/src/formats/cx2/exporter.js.map +1 -0
  67. package/dist/src/formats/cx2/importer.d.ts +169 -0
  68. package/dist/src/formats/cx2/importer.d.ts.map +1 -0
  69. package/dist/src/formats/cx2/importer.js +1652 -0
  70. package/dist/src/formats/cx2/importer.js.map +1 -0
  71. package/dist/src/formats/cx2/index.d.ts +7 -0
  72. package/dist/src/formats/cx2/index.d.ts.map +1 -0
  73. package/dist/src/formats/cx2/index.js +7 -0
  74. package/dist/src/formats/cx2/index.js.map +1 -0
  75. package/dist/src/formats/gexf/exporter.d.ts +2 -0
  76. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  77. package/dist/src/formats/gexf/exporter.js +6 -0
  78. package/dist/src/formats/gexf/exporter.js.map +1 -1
  79. package/dist/src/formats/gexf/importer.d.ts +2 -4
  80. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  81. package/dist/src/formats/gexf/importer.js +2 -4
  82. package/dist/src/formats/gexf/importer.js.map +1 -1
  83. package/dist/src/formats/gexf/index.d.ts +1 -1
  84. package/dist/src/formats/gexf/index.js +2 -2
  85. package/dist/src/formats/gexf/index.js.map +1 -1
  86. package/dist/src/formats/gml/exporter.js +1 -1
  87. package/dist/src/formats/gml/exporter.js.map +1 -1
  88. package/dist/src/formats/json/importer.d.ts.map +1 -1
  89. package/dist/src/formats/json/importer.js +6 -104
  90. package/dist/src/formats/json/importer.js.map +1 -1
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.d.ts.map +1 -1
  93. package/dist/src/index.js +2 -0
  94. package/dist/src/index.js.map +1 -1
  95. package/dist/src/registry.d.ts.map +1 -1
  96. package/dist/src/registry.js +5 -0
  97. package/dist/src/registry.js.map +1 -1
  98. package/dist/src/sniff.d.ts +1 -1
  99. package/dist/src/sniff.d.ts.map +1 -1
  100. package/dist/src/sniff.js +2 -0
  101. package/dist/src/sniff.js.map +1 -1
  102. package/package.json +11 -1
  103. package/src/common/json-elements.ts +1147 -0
  104. package/src/formats/cx/importer.ts +2733 -0
  105. package/src/formats/cx/index.ts +7 -0
  106. package/src/formats/cx2/exporter.ts +1036 -0
  107. package/src/formats/cx2/importer.ts +2187 -0
  108. package/src/formats/cx2/index.ts +7 -0
  109. package/src/formats/gexf/exporter.ts +11 -0
  110. package/src/formats/gexf/importer.ts +2 -2
  111. package/src/formats/gexf/index.ts +1 -1
  112. package/src/formats/gml/exporter.ts +1 -1
  113. package/src/formats/json/importer.ts +6 -105
  114. package/src/index.ts +10 -0
  115. package/src/registry.ts +5 -0
  116. package/src/sniff.ts +14 -1
  117. package/dist/chunks/importer-Du5crN9l.js.map +0 -1
  118. package/dist/chunks/writer-GAdltGmC.js.map +0 -1
@@ -0,0 +1,2220 @@
1
+ /**
2
+ * The CX version 1 importer (design/graph-io/cytoscape-and-obo/design.md section 1.2; the feature
3
+ * and error inventory is research-cx.md): the JSON exchange format of NDEx and Cytoscape's CX
4
+ * support. A CX document is an array of aspect fragments (`nodes`, `edges`, `nodeAttributes`,
5
+ * `cartesianLayout`, `cySubNetworks`, `cyGroups`, `cyVisualProperties`, ...) that refer to each
6
+ * other by integer ids, in any order.
7
+ *
8
+ * The document is read through the streaming scanner of common/json-elements.ts, so its text is
9
+ * never held as one string; the parsed aspect elements are collected and each graph is built at the
10
+ * end, because a reference may precede what it names.
11
+ *
12
+ * Mapping: every edge is directed; ids follow the CX id rule (design section 1.0.2); `n` is the
13
+ * `name` column (the label), `r` is `represents`, `i` is `interaction`; attributes become columns
14
+ * typed by their `d` (Cytoscape's value rule: "" and "null" are unset, "NaN" is NaN in a double); a
15
+ * subnetwork's values are the unscoped ones and its own (`s`), its own winning. A collection (several
16
+ * `cySubNetworks`) holds one graph per subnetwork: `importAll()` returns them, `import()` reads the
17
+ * one `graphIndex` / `graphName` picks, `listGraphs()` lists them. Positions come from the
18
+ * subnetwork's view, y flipped to y-up, `z` into the `z` column; `cyGroups` give `parent` (or
19
+ * `parents`); per-element visual properties are one column per property (origin namespace
20
+ * "cx.bypass") and style rules are kept verbatim in `meta.extra.cx` and not applied
21
+ * (W_STYLES_NOT_IMPORTED, issue #706); provenance aspects become extension tables and list columns.
22
+ */
23
+ import { INVALID_INDEX, } from "@graphty/graph-format";
24
+ import { declareResolved, uniqueColumnName } from "../../common/attributes.js";
25
+ import { AMBIGUOUS_GRAPH_NAME_CODE, ASPECT_ORDER_CODE, BAD_ASPECT_BLOCK_CODE, BAD_VALUE_CODE, COLUMN_RENAMED_CODE, COUNT_MISMATCH_CODE, DANGLING_REFERENCE_CODE, DIRECTION_FORCED_CODE, DIRECTION_REFUSED_CODE, DUPLICATE_ATTRIBUTE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, GRAPH_NOT_FOUND_CODE, ID_MERGED_CODE, ID_TEXT_TYPE_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, NO_GRAPH_CODE, OPTION_IGNORED_CODE, PARENT_CYCLE_CODE, PRECISION_CODE, ROLE_TAKEN_CODE, SINK_OPTION_CODE, STATUS_FAILED_CODE, STATUS_WARNING_CODE, STYLES_NOT_IMPORTED_CODE, SYNTAX_CODE, TOO_LARGE_CODE, UNKNOWN_ATTR_TYPE_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, WIDENED_CODE, } from "../../common/codes.js";
26
+ import { DirectionResolver } from "../../common/direction.js";
27
+ import { IdCoercer } from "../../common/ids.js";
28
+ import { textChunks, throwIfAborted } from "../../common/input.js";
29
+ import { cxId, CxStructure, declareFresh, ExactInteger, flipY, inexactLiteral, isRecord, JsonScanError, plainJson, positionDecl, reportTooDeep, scanAspects, zDecl, } from "../../common/json-elements.js";
30
+ import { chooseGraph, reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
31
+ import { ImportReportBuilder } from "../../common/report.js";
32
+ import { weightFromValue } from "../../common/weights.js";
33
+ import { zAsOption } from "../cx2/importer.js";
34
+ /** The format name. */
35
+ const CX_FORMAT = "cx";
36
+ /** The origin namespace of the per-element visual property columns. */
37
+ const CX_BYPASS_NAMESPACE = "cx.bypass";
38
+ /**
39
+ * The issue codes the CX importer records (design section 1.2), by name: the codes shared with
40
+ * the other importers (src/common/codes.ts) and the CX-specific ones. A key is the code without
41
+ * its severity and format prefixes.
42
+ */
43
+ export const CX_ISSUE = Object.freeze({
44
+ /** The input is empty (fatal). */
45
+ EMPTY_INPUT: EMPTY_INPUT_CODE,
46
+ /** The text is not JSON (fatal). */
47
+ SYNTAX: SYNTAX_CODE,
48
+ /** Invalid UTF-8 (fatal). */
49
+ INVALID_UTF8: INVALID_UTF8_CODE,
50
+ /** Invalid bytes in the encoding a BOM or the encoding option chose (fatal). */
51
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
52
+ /** Bytes that are not UTF-8 were read as windows-1252. */
53
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
54
+ /** An encoding the platform cannot decode was ignored. */
55
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
56
+ /** The document is not a CX array, or it is CX2 (fatal; the message names CX2). */
57
+ NOT_CX: "E_CX_NOT_CX",
58
+ /** numberVerification holds another value than 2^48 - 1, or comes twice. */
59
+ NUMBER_VERIFICATION: "W_CX_NUMBER_VERIFICATION",
60
+ /** An old Cytoscape aspect name (visualProperties, subNetworks, ...) read under its cy name. */
61
+ OLD_ASPECT_NAME: "W_CX_OLD_ASPECT_NAME",
62
+ /** A cyGroups group whose id is not a node: the group node is added. */
63
+ GROUP_NODE_ADDED: "W_CX_GROUP_NODE_ADDED",
64
+ /** Nodes or edges of the root network that no subnetwork holds: Cytoscape shows them in no network; not read. */
65
+ ROOT_ONLY: "W_CX_ROOT_ONLY",
66
+ /** The producer marked the document as failed (fatal). */
67
+ STATUS_FAILED: STATUS_FAILED_CODE,
68
+ /** The producer marked the document as successful with an error text. */
69
+ STATUS_WARNING: STATUS_WARNING_CODE,
70
+ /** A member of the array that is not a one-key aspect block, or an element that is not an object. */
71
+ BAD_ASPECT_BLOCK: BAD_ASPECT_BLOCK_CODE,
72
+ /** An aspect after the post-metadata or after the status, a third metaData. */
73
+ ASPECT_ORDER: ASPECT_ORDER_CODE,
74
+ /** A metaData element count disagrees with what was read. */
75
+ COUNT_MISMATCH: COUNT_MISMATCH_CODE,
76
+ /** A value that does not parse as its data type; the cell is unset. */
77
+ BAD_VALUE: BAD_VALUE_CODE,
78
+ /** One attribute name with several data types: the column takes the wider one. */
79
+ WIDENED: WIDENED_CODE,
80
+ /** A data type CX does not define; the value is kept as text. */
81
+ UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
82
+ /** An attribute, layout, bypass, subnetwork member, view or provenance entry naming nothing. */
83
+ DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
84
+ /** The same attribute twice on one element (or n and a different name attribute); the later wins. */
85
+ DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
86
+ /** A group member naming no node. */
87
+ UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
88
+ /** A group membership that would close a parent cycle; dropped. */
89
+ PARENT_CYCLE: PARENT_CYCLE_CODE,
90
+ /** The style rules of cyVisualProperties are not applied (issue #706). */
91
+ STYLES_NOT_IMPORTED: STYLES_NOT_IMPORTED_CODE,
92
+ /** A node without an @id. */
93
+ MISSING_ID: MISSING_ID_CODE,
94
+ /** An edge without s or t. */
95
+ MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
96
+ /** A node id declared twice; the second merges into the first. */
97
+ DUPLICATE_NODE: DUPLICATE_NODE_CODE,
98
+ /** An edge id declared twice; the second edge is skipped. */
99
+ DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
100
+ /** An id that is not an integer. */
101
+ INVALID_ID: "E_INVALID_ID",
102
+ /** An edge endpoint naming no node of the graph (addMissingNodes false, the default). */
103
+ UNKNOWN_NODE: "E_UNKNOWN_NODE",
104
+ /** A weight that is not a number. */
105
+ INVALID_WEIGHT: "E_INVALID_WEIGHT",
106
+ /** An id spelled as a string or a non-integer literal; read as the integer. */
107
+ ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
108
+ /** An integer beyond 2^53: an id kept as its digits, a value stored as the nearest f64. */
109
+ PRECISION: PRECISION_CODE,
110
+ /** Two id texts merged under `ids: "number"`. */
111
+ ID_MERGED: ID_MERGED_CODE,
112
+ /** A collection read by import(): the other subnetworks are skipped. */
113
+ MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
114
+ /** graphIndex or graphName names no subnetwork (fatal). */
115
+ GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
116
+ /** graphName names several subnetworks (fatal). */
117
+ AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
118
+ /** The input holds no graph (fatal). */
119
+ NO_GRAPH: NO_GRAPH_CODE,
120
+ /** A column renamed because its name was taken. */
121
+ COLUMN_RENAMED: COLUMN_RENAMED_CODE,
122
+ /** A column that lost its role because another column holds it. */
123
+ ROLE_TAKEN: ROLE_TAKEN_CODE,
124
+ /** The sink refused the direction. */
125
+ DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
126
+ /** Edges forced to the policy's direction. */
127
+ DIRECTION_FORCED: DIRECTION_FORCED_CODE,
128
+ /** A common option CX has no use for. */
129
+ OPTION_IGNORED: OPTION_IGNORED_CODE,
130
+ /** A builder option the caller's sink does not honour. */
131
+ SINK_OPTION: SINK_OPTION_CODE,
132
+ /** The input is beyond a size limit (fatal). */
133
+ TOO_LARGE: TOO_LARGE_CODE,
134
+ });
135
+ const USED_OPTIONS = new Set([
136
+ "ids",
137
+ "addMissingNodes",
138
+ "duplicateEdges",
139
+ "selfLoops",
140
+ "onMixedDirection",
141
+ "weightFrom",
142
+ "weightDtype",
143
+ "long",
144
+ "errorLimit",
145
+ "signal",
146
+ "onProgress",
147
+ ]);
148
+ /** CX has no undirected edge: defaultDirected never applies (reported W_OPTION_IGNORED). */
149
+ const FORMAT_DEFAULTS = {
150
+ ids: "keep",
151
+ defaultDirected: true,
152
+ weightFrom: "weight",
153
+ addMissingNodes: false,
154
+ };
155
+ const ABORT_CHECK_INTERVAL = 64;
156
+ /** The numberVerification values a reader accepts: 2^48 - 1 and Java's Long.MAX_VALUE. */
157
+ const NUMBER_VERIFICATION_VALUES = new Set(["281474976710655", "9223372036854775807"]);
158
+ /** Old Cytoscape aspect names and the names they are read under. */
159
+ const OLD_ASPECT_NAMES = {
160
+ visualProperties: "cyVisualProperties",
161
+ subNetworks: "cySubNetworks",
162
+ networkRelations: "cyNetworkRelations",
163
+ hiddenAttributes: "cyHiddenAttributes",
164
+ };
165
+ /** The aspects the importer reads into the graph (everything else is kept verbatim). */
166
+ const READ_ASPECTS = new Set([
167
+ "nodes",
168
+ "edges",
169
+ "nodeAttributes",
170
+ "edgeAttributes",
171
+ "networkAttributes",
172
+ "cartesianLayout",
173
+ "cySubNetworks",
174
+ "cyNetworkRelations",
175
+ "cyViews",
176
+ "cyGroups",
177
+ "cyVisualProperties",
178
+ "cyTableColumn",
179
+ "citations",
180
+ "supports",
181
+ "nodeCitations",
182
+ "edgeCitations",
183
+ "nodeSupports",
184
+ "edgeSupports",
185
+ "functionTerms",
186
+ "reifiedEdges",
187
+ "numberVerification",
188
+ "metaData",
189
+ "status",
190
+ ]);
191
+ /** The aspects also kept verbatim in meta.extra.cx although they are read. */
192
+ const ALSO_KEPT = new Set([
193
+ "cySubNetworks",
194
+ "cyNetworkRelations",
195
+ "cyViews",
196
+ "cyVisualProperties",
197
+ "cyTableColumn",
198
+ "numberVerification",
199
+ "metaData",
200
+ ]);
201
+ /** Cytoscape's table-cell style aspects (both spellings): style rules, kept and reported, never read. */
202
+ const TABLE_STYLE_ASPECTS = ["tableVisualProperties", "cyTableVisualProperties"];
203
+ /** The first keys that make a head CX for sure, and the other aspect names a CX document may start with. */
204
+ const CX_FIRST_KEYS = ["numberVerification", "metaData"];
205
+ const CX_ASPECT_KEYS = [
206
+ "nodes",
207
+ "edges",
208
+ "nodeAttributes",
209
+ "edgeAttributes",
210
+ "networkAttributes",
211
+ "cartesianLayout",
212
+ "@context",
213
+ "cySubNetworks",
214
+ "subNetworks",
215
+ "cyNetworkRelations",
216
+ "networkRelations",
217
+ "cyVisualProperties",
218
+ "visualProperties",
219
+ "cyHiddenAttributes",
220
+ "hiddenAttributes",
221
+ "cyTableColumn",
222
+ "cyGroups",
223
+ "cyViews",
224
+ "ndexStatus",
225
+ "provenanceHistory",
226
+ "citations",
227
+ "supports",
228
+ "functionTerms",
229
+ ];
230
+ /** The CX data types, with the Java reader's float aliases. */
231
+ const SCALARS = {
232
+ string: "string",
233
+ boolean: "boolean",
234
+ integer: "integer",
235
+ long: "long",
236
+ double: "double",
237
+ float: "double",
238
+ };
239
+ /**
240
+ * Resolve a `d` value.
241
+ * @param d - the data type (undefined: string)
242
+ * @returns the type, or null when CX does not define it
243
+ */
244
+ function cxType(d) {
245
+ if (d === undefined || d === null) {
246
+ return { scalar: "string", list: false };
247
+ }
248
+ if (typeof d !== "string") {
249
+ return null;
250
+ }
251
+ const list = d.startsWith("list_of_");
252
+ const scalar = SCALARS[list ? d.slice("list_of_".length) : d];
253
+ return scalar === undefined ? null : { scalar, list };
254
+ }
255
+ /**
256
+ * The type text of a type.
257
+ * @param type - the type
258
+ * @returns `list_of_double`, `string`, ...
259
+ */
260
+ function typeText(type) {
261
+ return type.list ? `list_of_${type.scalar}` : type.scalar;
262
+ }
263
+ /** The widening rank of the scalar types (design 5.1 order; boolean and numbers widen to string). */
264
+ const RANK = { boolean: 0, integer: 1, long: 2, double: 3, string: 4 };
265
+ /**
266
+ * Widen two types to one that holds both.
267
+ * @param a - the type so far
268
+ * @param b - another type
269
+ * @returns the wider type, or null when they cannot share a column (a list and a scalar)
270
+ */
271
+ function widenType(a, b) {
272
+ if (a.list !== b.list) {
273
+ return null;
274
+ }
275
+ if (a.scalar === b.scalar) {
276
+ return a;
277
+ }
278
+ const numeric = (s) => s === "integer" || s === "long" || s === "double";
279
+ if (numeric(a.scalar) && numeric(b.scalar)) {
280
+ return RANK[a.scalar] > RANK[b.scalar] ? a : b;
281
+ }
282
+ return { scalar: "string", list: a.list };
283
+ }
284
+ /** What parseValue() returns for a value that does not parse. */
285
+ const BAD = Symbol("bad");
286
+ /** What parseValue() returns for a value Cytoscape reads as null ("", "null"). */
287
+ const UNSET = Symbol("unset");
288
+ const I32_MIN = -2147483648;
289
+ const I32_MAX = 2147483647;
290
+ const INTEGER_TEXT = /^-?[0-9]+$/;
291
+ const DECIMAL_TEXT = /^-?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?$/;
292
+ /**
293
+ * Whether a text is Cytoscape's null: the empty string or "null" in any case.
294
+ * @param text - the text
295
+ * @returns true for a null text
296
+ */
297
+ function isNullText(text) {
298
+ return text === "" || text.toLowerCase() === "null";
299
+ }
300
+ /**
301
+ * Parse one scalar value (or list item) of a CX attribute by its type, with Cytoscape's value
302
+ * rule: "" and "null" are unset, "NaN" is NaN in a double (unset in an integer or long), native JSON
303
+ * numbers and booleans are accepted, anything else that does not parse is BAD.
304
+ * @param value - the value (never null)
305
+ * @param scalar - the type
306
+ * @param long - the importer's `long` option
307
+ * @param onPrecision - called for a long beyond 2^53
308
+ * @returns the value, UNSET or BAD
309
+ */
310
+ function parseScalar(value, scalar, long, onPrecision) {
311
+ if (value instanceof ExactInteger) {
312
+ if (scalar === "string" || (scalar === "long" && long === "string")) {
313
+ return value.digits;
314
+ }
315
+ if (scalar === "long" || scalar === "double") {
316
+ if (scalar === "long") {
317
+ onPrecision(value.digits);
318
+ }
319
+ return Number(value.digits);
320
+ }
321
+ return BAD;
322
+ }
323
+ if (typeof value === "number" || typeof value === "boolean") {
324
+ return parseNative(value, scalar, long);
325
+ }
326
+ if (typeof value !== "string") {
327
+ return BAD;
328
+ }
329
+ if (scalar === "string") {
330
+ return value;
331
+ }
332
+ if (isNullText(value)) {
333
+ return UNSET;
334
+ }
335
+ switch (scalar) {
336
+ case "boolean": {
337
+ const lower = value.toLowerCase();
338
+ if (lower === "true" || lower === "false") {
339
+ return lower === "true";
340
+ }
341
+ return BAD;
342
+ }
343
+ case "integer":
344
+ case "long": {
345
+ if (value === "NaN") {
346
+ return UNSET;
347
+ }
348
+ if (!INTEGER_TEXT.test(value)) {
349
+ return BAD;
350
+ }
351
+ const n = Number(value);
352
+ if (scalar === "integer") {
353
+ return n >= I32_MIN && n <= I32_MAX ? n : BAD;
354
+ }
355
+ if (long === "string") {
356
+ return value;
357
+ }
358
+ if (!Number.isSafeInteger(n)) {
359
+ onPrecision(value);
360
+ }
361
+ return n;
362
+ }
363
+ default: {
364
+ if (value === "NaN" || value === "nan") {
365
+ return NaN;
366
+ }
367
+ if (value === "Infinity" || value === "-Infinity") {
368
+ return Number(value);
369
+ }
370
+ return DECIMAL_TEXT.test(value) ? Number(value) : BAD;
371
+ }
372
+ }
373
+ }
374
+ /**
375
+ * Parse a native JSON number or boolean by a type.
376
+ * @param value - the value
377
+ * @param scalar - the type
378
+ * @param long - the importer's `long` option
379
+ * @returns the value or BAD
380
+ */
381
+ function parseNative(value, scalar, long) {
382
+ switch (scalar) {
383
+ case "string":
384
+ return String(value);
385
+ case "boolean":
386
+ return typeof value === "boolean" ? value : BAD;
387
+ case "integer":
388
+ return typeof value === "number" && Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX
389
+ ? value
390
+ : BAD;
391
+ case "long":
392
+ if (typeof value !== "number" || !Number.isInteger(value)) {
393
+ return BAD;
394
+ }
395
+ return long === "string" ? String(value) : value;
396
+ default:
397
+ return typeof value === "number" ? value : BAD;
398
+ }
399
+ }
400
+ /**
401
+ * Parse an attribute value by its type: a scalar, or a list whose items parse one by one (a null
402
+ * item is NaN in a double list and BAD elsewhere).
403
+ * @param value - the value (never null)
404
+ * @param type - the type
405
+ * @param long - the importer's `long` option
406
+ * @param onPrecision - called for a long beyond 2^53
407
+ * @returns the value, UNSET or BAD
408
+ */
409
+ function parseValue(value, type, long, onPrecision) {
410
+ if (Array.isArray(value) !== type.list) {
411
+ return BAD;
412
+ }
413
+ if (!type.list) {
414
+ return parseScalar(value, type.scalar, long, onPrecision);
415
+ }
416
+ const out = [];
417
+ for (const item of value) {
418
+ let parsed = item === null ? UNSET : parseScalar(item, type.scalar, long, onPrecision);
419
+ if (parsed === UNSET) {
420
+ parsed = type.scalar === "double" ? NaN : BAD;
421
+ }
422
+ if (parsed === BAD) {
423
+ return BAD;
424
+ }
425
+ out.push(parsed);
426
+ }
427
+ return out;
428
+ }
429
+ /**
430
+ * The column dtype of a type.
431
+ * @param scalar - the scalar type
432
+ * @param long - the importer's `long` option
433
+ * @returns the dtype
434
+ */
435
+ function scalarDtype(scalar, long) {
436
+ switch (scalar) {
437
+ case "boolean":
438
+ return "bool";
439
+ case "integer":
440
+ return "i32";
441
+ case "long":
442
+ return long === "string" ? "string" : "f64";
443
+ case "double":
444
+ return "f64";
445
+ default:
446
+ return "string";
447
+ }
448
+ }
449
+ /**
450
+ * A parsed value converted to the column's type (a narrower value in a widened column).
451
+ * @param value - the parsed value
452
+ * @param type - the column's type
453
+ * @returns the value to store
454
+ */
455
+ function toColumn(value, type) {
456
+ if (type.scalar !== "string") {
457
+ return value;
458
+ }
459
+ if (type.list) {
460
+ return value.map((item) => (typeof item === "string" ? item : String(item)));
461
+ }
462
+ return typeof value === "string" ? value : String(value);
463
+ }
464
+ /**
465
+ * A short JSON rendering of a value for a message.
466
+ * @param value - the value
467
+ * @returns the text, at most 60 characters
468
+ */
469
+ function shown(value) {
470
+ if (value instanceof ExactInteger) {
471
+ return value.digits;
472
+ }
473
+ let text;
474
+ try {
475
+ text = JSON.stringify(value) ?? String(value);
476
+ }
477
+ catch {
478
+ text = String(value);
479
+ }
480
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
481
+ }
482
+ /**
483
+ * The elements of a read aspect.
484
+ * @param doc - the document
485
+ * @param name - the aspect
486
+ * @returns the elements, empty when absent
487
+ */
488
+ function aspect(doc, name) {
489
+ return doc.aspects.get(name) ?? [];
490
+ }
491
+ /**
492
+ * The inexact-literal bits of an element.
493
+ * @param text - the element's JSON text
494
+ * @param keys - the id keys to check, bit 1, 2, 4 in order
495
+ * @returns the bits
496
+ */
497
+ function inexactBits(text, keys) {
498
+ if (!/[0-9][.eE]/.test(text)) {
499
+ return 0;
500
+ }
501
+ let bits = 0;
502
+ keys.forEach((key, i) => {
503
+ if (inexactLiteral(text, key)) {
504
+ bits |= 1 << i;
505
+ }
506
+ });
507
+ return bits;
508
+ }
509
+ /**
510
+ * The attribute map of an attribute aspect.
511
+ * @param doc - the document
512
+ * @param name - the aspect name
513
+ * @returns the map, or null for another aspect
514
+ */
515
+ function attributeTable(doc, name) {
516
+ switch (name) {
517
+ case "nodeAttributes":
518
+ return doc.attributes.node;
519
+ case "edgeAttributes":
520
+ return doc.attributes.edge;
521
+ case "networkAttributes":
522
+ return doc.attributes.network;
523
+ default:
524
+ return null;
525
+ }
526
+ }
527
+ /**
528
+ * Check a numberVerification element: 2^48 - 1 (or Java's Long.MAX_VALUE), and only one.
529
+ * @param value - the element
530
+ * @param count - how many numberVerification elements were read, this one included
531
+ * @param report - the report
532
+ * @param line - its line
533
+ */
534
+ function checkNumberVerification(value, count, report, line) {
535
+ const raw = isRecord(value) ? value.longNumber : undefined;
536
+ const digits = raw instanceof ExactInteger ? raw.digits : String(raw);
537
+ if (count > 1 || !NUMBER_VERIFICATION_VALUES.has(digits)) {
538
+ report.warning("validation-error", CX_ISSUE.NUMBER_VERIFICATION, count > 1
539
+ ? "a second numberVerification element; ignored"
540
+ : `numberVerification holds ${shown(raw === undefined ? null : digits)}, not 281474976710655`, { line, element: "numberVerification" });
541
+ }
542
+ }
543
+ /**
544
+ * Read the whole document through the streaming scanner, checking its structure.
545
+ * @param input - the input
546
+ * @param report - the report
547
+ * @param options - the resolved options
548
+ * @returns what was read
549
+ */
550
+ async function readDocument(input, report, options) {
551
+ const structure = new CxStructure(report);
552
+ const doc = {
553
+ aspects: new Map(),
554
+ attributes: { node: new Map(), edge: new Map(), network: new Map() },
555
+ kept: new Map(),
556
+ structure,
557
+ };
558
+ let sinceCheck = 0;
559
+ let verifications = 0;
560
+ const canonical = (name, line) => {
561
+ const renamed = OLD_ASPECT_NAMES[name];
562
+ if (renamed === undefined) {
563
+ return name;
564
+ }
565
+ report.warnOnce("coercion", CX_ISSUE.OLD_ASPECT_NAME, `the old aspect name "${name}" is read as "${renamed}"`, { line, element: name }, `${CX_ISSUE.OLD_ASPECT_NAME}:${name}`);
566
+ return renamed;
567
+ };
568
+ const collect = (name, value, text, line, exact) => {
569
+ structure.element(name, value, line);
570
+ if (ALSO_KEPT.has(name) || !READ_ASPECTS.has(name)) {
571
+ if (!doc.kept.has(name)) {
572
+ doc.kept.set(name, []);
573
+ }
574
+ doc.kept.get(name)?.push(exact ? plainJson(value) : value);
575
+ }
576
+ if (!READ_ASPECTS.has(name) || name === "metaData" || name === "status") {
577
+ return;
578
+ }
579
+ if (name === "numberVerification") {
580
+ checkNumberVerification(value, ++verifications, report, line);
581
+ return;
582
+ }
583
+ let inexact = 0;
584
+ if (name === "nodes") {
585
+ inexact = inexactBits(text, ["@id"]);
586
+ }
587
+ else if (name === "edges") {
588
+ inexact = inexactBits(text, ["@id", "s", "t"]);
589
+ }
590
+ const held = { value, line, inexact };
591
+ const table = attributeTable(doc, name);
592
+ if (table !== null) {
593
+ if (!isRecord(value) || typeof value.n !== "string") {
594
+ report.error("missing-value", MISSING_ID_CODE, `a ${name} element has no attribute name n; skipped`, {
595
+ line,
596
+ element: name,
597
+ });
598
+ return;
599
+ }
600
+ const key = value.n;
601
+ if (!table.has(key)) {
602
+ table.set(key, []);
603
+ }
604
+ table.get(key)?.push(held);
605
+ return;
606
+ }
607
+ if (!doc.aspects.has(name)) {
608
+ doc.aspects.set(name, []);
609
+ }
610
+ doc.aspects.get(name)?.push(held);
611
+ };
612
+ try {
613
+ for await (const event of scanAspects(textChunks(input, report, options))) {
614
+ if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
615
+ sinceCheck = 0;
616
+ throwIfAborted(options.signal);
617
+ }
618
+ switch (event.kind) {
619
+ case "root":
620
+ report.fail(CX_ISSUE.NOT_CX, `a CX document is a JSON array of aspects; found ${isRecord(event.value) ? "an object" : shown(event.value)}`, { line: event.line });
621
+ break;
622
+ case "member": {
623
+ const { value } = event;
624
+ if (isRecord(value) && "CXVersion" in value) {
625
+ report.fail(CX_ISSUE.NOT_CX, `the document is CX2 (CXVersion ${shown(value.CXVersion)}), not CX version 1; read it with the cx2 importer`, { line: event.line });
626
+ }
627
+ const keys = isRecord(value) ? Object.keys(value) : [];
628
+ if (isRecord(value) && keys.length === 1 && isRecord(value[keys[0]])) {
629
+ const name = canonical(keys[0], event.line);
630
+ structure.block(name, event.line);
631
+ collect(name, value[keys[0]], "", event.line, event.exact);
632
+ break;
633
+ }
634
+ report.error("parse-error", BAD_ASPECT_BLOCK_CODE, `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`, { line: event.line, element: keys[0] ?? null });
635
+ break;
636
+ }
637
+ case "block":
638
+ structure.block(canonical(event.aspect, event.line), event.line);
639
+ break;
640
+ case "element": {
641
+ const name = OLD_ASPECT_NAMES[event.aspect] ?? event.aspect;
642
+ collect(name, event.value, event.text, event.line, event.exact);
643
+ break;
644
+ }
645
+ case "deep":
646
+ reportTooDeep(report, event);
647
+ break;
648
+ case "extraKeys":
649
+ report.error("parse-error", BAD_ASPECT_BLOCK_CODE, `the "${event.aspect}" fragment holds more keys (${event.keys.join(", ")}); a fragment has one key, the others are skipped`, { line: event.line, element: event.aspect });
650
+ break;
651
+ default:
652
+ break;
653
+ }
654
+ }
655
+ }
656
+ catch (err) {
657
+ if (err instanceof JsonScanError) {
658
+ report.fail(err.empty ? EMPTY_INPUT_CODE : SYNTAX_CODE, err.message, { line: err.line });
659
+ }
660
+ throw err;
661
+ }
662
+ structure.checkCounts();
663
+ return doc;
664
+ }
665
+ /**
666
+ * An id of a reference, or null when it is not one.
667
+ * @param raw - the parsed reference
668
+ * @returns the id
669
+ */
670
+ function refId(raw) {
671
+ try {
672
+ return cxId(raw).id;
673
+ }
674
+ catch {
675
+ return null;
676
+ }
677
+ }
678
+ /**
679
+ * A member list: the ids of an array, or null for "all".
680
+ * @param raw - the list
681
+ * @returns the set, or null for every element
682
+ */
683
+ function memberSet(raw) {
684
+ if (raw === "all") {
685
+ return null;
686
+ }
687
+ const out = new Set();
688
+ if (Array.isArray(raw)) {
689
+ for (const item of raw) {
690
+ const id = refId(item);
691
+ if (id !== null) {
692
+ out.add(id);
693
+ }
694
+ }
695
+ }
696
+ return out;
697
+ }
698
+ /**
699
+ * The graphs of a document, in the order Cytoscape opens them.
700
+ * @param doc - the document
701
+ * @returns one plan per graph (at least one)
702
+ */
703
+ function graphPlans(doc) {
704
+ const subs = new Map();
705
+ for (const { value } of aspect(doc, "cySubNetworks")) {
706
+ const id = isRecord(value) ? refId(value["@id"]) : null;
707
+ if (id !== null && isRecord(value) && !subs.has(id)) {
708
+ subs.set(id, { nodes: memberSet(value.nodes), edges: memberSet(value.edges) });
709
+ }
710
+ }
711
+ const relationNames = new Map();
712
+ const order = [];
713
+ const views = new Map();
714
+ const addView = (sub, view) => {
715
+ const list = views.get(sub) ?? [];
716
+ if (!list.includes(view)) {
717
+ list.push(view);
718
+ }
719
+ views.set(sub, list);
720
+ };
721
+ for (const { value } of aspect(doc, "cyNetworkRelations")) {
722
+ if (!isRecord(value)) {
723
+ continue;
724
+ }
725
+ const child = refId(value.c);
726
+ if (child === null) {
727
+ continue;
728
+ }
729
+ if (value.r === "view") {
730
+ const parent = refId(value.p);
731
+ if (parent !== null) {
732
+ addView(parent, child);
733
+ }
734
+ continue;
735
+ }
736
+ if (typeof value.name === "string") {
737
+ relationNames.set(child, value.name);
738
+ }
739
+ if (subs.has(child) && !order.includes(child)) {
740
+ order.push(child);
741
+ }
742
+ }
743
+ for (const { value } of aspect(doc, "cyViews")) {
744
+ const view = isRecord(value) ? refId(value["@id"]) : null;
745
+ const sub = isRecord(value) ? refId(value.s) : null;
746
+ if (view !== null && sub !== null) {
747
+ addView(sub, view);
748
+ }
749
+ }
750
+ for (const id of subs.keys()) {
751
+ if (!order.includes(id)) {
752
+ order.push(id);
753
+ }
754
+ }
755
+ const networkName = (sub) => {
756
+ let found = null;
757
+ for (const { value } of doc.attributes.network.get("name") ?? []) {
758
+ if (!isRecord(value) || typeof value.v !== "string") {
759
+ continue;
760
+ }
761
+ const scope = value.s === undefined ? null : refId(value.s);
762
+ if (scope === sub) {
763
+ return value.v;
764
+ }
765
+ if (scope === null) {
766
+ found ?? (found = value.v);
767
+ }
768
+ }
769
+ return found;
770
+ };
771
+ if (order.length <= 1) {
772
+ const sub = order.length === 1 ? order[0] : null;
773
+ const members = sub === null ? undefined : subs.get(sub);
774
+ let graphViews = sub === null ? [] : (views.get(sub) ?? []);
775
+ if (graphViews.length === 0) {
776
+ graphViews = [...new Set([...views.values()].flat())];
777
+ }
778
+ if (graphViews.length === 0) {
779
+ graphViews = layoutViews(doc);
780
+ }
781
+ return [
782
+ {
783
+ index: 0,
784
+ name: (sub === null ? null : (relationNames.get(sub) ?? null)) ?? networkName(sub),
785
+ subnetwork: sub,
786
+ nodes: members?.nodes ?? null,
787
+ edges: members?.edges ?? null,
788
+ views: graphViews,
789
+ },
790
+ ];
791
+ }
792
+ return order.map((sub, index) => ({
793
+ index,
794
+ name: relationNames.get(sub) ?? networkName(sub),
795
+ subnetwork: sub,
796
+ nodes: subs.get(sub)?.nodes ?? null,
797
+ edges: subs.get(sub)?.edges ?? null,
798
+ views: views.get(sub) ?? [],
799
+ }));
800
+ }
801
+ /**
802
+ * The distinct views the layout entries name, in order of appearance.
803
+ * @param doc - the document
804
+ * @returns the view ids
805
+ */
806
+ function layoutViews(doc) {
807
+ const out = [];
808
+ for (const { value } of aspect(doc, "cartesianLayout")) {
809
+ const view = isRecord(value) && value.view !== undefined ? refId(value.view) : null;
810
+ if (view !== null && !out.includes(view)) {
811
+ out.push(view);
812
+ }
813
+ }
814
+ return out;
815
+ }
816
+ /**
817
+ * How many elements a member set holds.
818
+ * @param members - the set, or null for every element
819
+ * @param total - the element count of the root network
820
+ * @returns the count
821
+ */
822
+ function memberCount(members, total) {
823
+ return members === null ? total : members.size;
824
+ }
825
+ /** Reads one graph of a collected document into a sink. */
826
+ class CxReader {
827
+ /**
828
+ * Create the reader.
829
+ * @param sink - the sink
830
+ * @param report - the report
831
+ * @param options - the resolved options
832
+ * @param zAs - where z goes
833
+ * @param doc - the collected document
834
+ * @param plan - the graph to read
835
+ */
836
+ constructor(sink, report, options, zAs, doc, plan) {
837
+ /** Every node id of the root network. */
838
+ this.rootNodes = new Set();
839
+ /** Every edge id of the root network. */
840
+ this.rootEdges = new Set();
841
+ /** CX node id -> sink node index, for this graph. */
842
+ this.nodeRows = new Map();
843
+ /** CX edge id -> sink edge index, for this graph. */
844
+ this.edgeRows = new Map();
845
+ /** The subnetwork ids of the document. */
846
+ this.subnetworks = new Set();
847
+ this.dangling = new Map();
848
+ this.nameHandle = INVALID_INDEX;
849
+ this.representsHandle = INVALID_INDEX;
850
+ this.interactionHandle = INVALID_INDEX;
851
+ this.edgeIdHandle = INVALID_INDEX;
852
+ this.sinceCheck = 0;
853
+ this.idType = "integer";
854
+ this.weighted = false;
855
+ /** The core field values written per row (n, r), to detect a differing name attribute. */
856
+ this.coreValues = new Map();
857
+ /** The weight attribute values of the edges, by edge id (built once). */
858
+ this.weights = null;
859
+ this.sink = sink;
860
+ this.report = report;
861
+ this.options = options;
862
+ this.zAs = zAs;
863
+ this.doc = doc;
864
+ this.plan = plan;
865
+ this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
866
+ this.coercer = new IdCoercer(options.ids);
867
+ for (const { value } of aspect(doc, "nodes")) {
868
+ const id = isRecord(value) ? refId(value["@id"]) : null;
869
+ if (id !== null) {
870
+ this.rootNodes.add(id);
871
+ }
872
+ }
873
+ for (const { value } of aspect(doc, "edges")) {
874
+ const id = isRecord(value) ? refId(value["@id"]) : null;
875
+ if (id !== null) {
876
+ this.rootEdges.add(id);
877
+ }
878
+ }
879
+ for (const { value } of aspect(doc, "cySubNetworks")) {
880
+ const id = isRecord(value) ? refId(value["@id"]) : null;
881
+ if (id !== null) {
882
+ this.subnetworks.add(id);
883
+ }
884
+ }
885
+ }
886
+ /** Build the graph. */
887
+ build() {
888
+ const { report } = this;
889
+ this.direction.setHeader(true);
890
+ this.checkMembers();
891
+ this.checkRootOnly();
892
+ const nodeColumns = this.declareAttributes("node");
893
+ this.readNodes();
894
+ this.readGroups();
895
+ this.writeAttributes("node", nodeColumns);
896
+ this.readLayout();
897
+ const edgeColumns = this.declareAttributes("edge");
898
+ this.readEdges();
899
+ this.writeAttributes("edge", edgeColumns);
900
+ this.readNetworkAttributes();
901
+ this.readVisualProperties();
902
+ this.readProvenance();
903
+ for (const [kind, count] of this.dangling) {
904
+ report.warning("validation-error", DANGLING_REFERENCE_CODE, `${count} ${kind}(s) name nothing; ignored`, {
905
+ element: kind,
906
+ });
907
+ }
908
+ this.setMeta();
909
+ }
910
+ // ------------------------------------------------------------ helpers
911
+ /**
912
+ * Count a reference that names nothing.
913
+ * @param kind - what referred
914
+ */
915
+ dangle(kind) {
916
+ this.dangling.set(kind, (this.dangling.get(kind) ?? 0) + 1);
917
+ }
918
+ /** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
919
+ checkAbort() {
920
+ if (++this.sinceCheck >= ABORT_CHECK_INTERVAL) {
921
+ this.sinceCheck = 0;
922
+ throwIfAborted(this.options.signal);
923
+ }
924
+ }
925
+ /**
926
+ * Whether a node id belongs to this graph.
927
+ * @param id - the CX id
928
+ * @returns true for a member
929
+ */
930
+ inGraph(id) {
931
+ return this.plan.nodes === null || this.plan.nodes.has(id);
932
+ }
933
+ /**
934
+ * Whether a scoped value applies to this graph: unscoped, or scoped to its subnetwork. A scope
935
+ * naming no subnetwork is counted as dangling.
936
+ * @param scope - the element's s
937
+ * @returns 1 for an unscoped value, 2 for this graph's own, 0 for another graph's
938
+ */
939
+ scopeOf(scope) {
940
+ if (scope === undefined || scope === null) {
941
+ return 1;
942
+ }
943
+ const id = refId(scope);
944
+ if (id !== null && id === this.plan.subnetwork) {
945
+ return 2;
946
+ }
947
+ if (id === null || !this.subnetworks.has(id)) {
948
+ this.dangle("attribute subnetwork reference");
949
+ }
950
+ return 0;
951
+ }
952
+ /**
953
+ * The id of an element, by the CX id rule and the `ids` option.
954
+ * @param raw - the parsed id
955
+ * @param inexact - whether the literal was not a plain integer
956
+ * @param element - the element name for issues
957
+ * @param edgeId - whether this is an edge's own id (stored in the f64 id column, not kept as digits)
958
+ * @returns the id, or null when it was reported
959
+ */
960
+ idOf(raw, inexact, element, edgeId = false) {
961
+ try {
962
+ const parsed = cxId(raw, inexact);
963
+ if (parsed.note === "text") {
964
+ this.report.warnOnce("coercion", ID_TEXT_TYPE_CODE, `${element}: the id ${shown(raw)} is not written as an integer; read as ${String(parsed.id)}`, { element });
965
+ }
966
+ else if (parsed.note === "precision" && edgeId) {
967
+ // the edge is told apart by its digits, but the id column is f64
968
+ this.report.warnOnce("precision", PRECISION_CODE, `${element}: the edge id ${String(parsed.id)} is beyond 2^53; the id column holds the nearest double`, { element }, `${PRECISION_CODE}:edge`);
969
+ }
970
+ else if (parsed.note === "precision") {
971
+ this.idType = "mixed";
972
+ this.report.warnOnce("precision", PRECISION_CODE, `${element}: the id ${String(parsed.id)} is beyond 2^53; kept as its digits (a string id)`, { element });
973
+ }
974
+ return this.options.ids === "keep" ? parsed.id : this.coercer.value(parsed.id);
975
+ }
976
+ catch (err) {
977
+ this.report.recordError(err, { element });
978
+ return null;
979
+ }
980
+ }
981
+ /**
982
+ * Record a long value beyond 2^53 (once).
983
+ * @param name - the attribute
984
+ * @param digits - the value
985
+ */
986
+ precision(name, digits) {
987
+ this.report.warnOnce("precision", PRECISION_CODE, `"${name}": ${digits} is beyond 2^53; stored as the nearest double`, { element: name }, `${PRECISION_CODE}:value`);
988
+ }
989
+ /** Count the subnetwork members that name no node or edge. */
990
+ checkMembers() {
991
+ for (const id of this.plan.nodes ?? []) {
992
+ if (!this.rootNodes.has(id)) {
993
+ this.dangle("subnetwork node member");
994
+ }
995
+ }
996
+ for (const id of this.plan.edges ?? []) {
997
+ if (!this.rootEdges.has(id)) {
998
+ this.dangle("subnetwork edge member");
999
+ }
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Report the root network's nodes and edges that no subnetwork holds (W_CX_ROOT_ONLY): they
1004
+ * belong to no graph Cytoscape opens, so no graph of this file reads them.
1005
+ */
1006
+ checkRootOnly() {
1007
+ const subs = aspect(this.doc, "cySubNetworks");
1008
+ if (subs.length === 0) {
1009
+ return;
1010
+ }
1011
+ const held = { nodes: new Set(), edges: new Set() };
1012
+ const all = { nodes: false, edges: false };
1013
+ for (const { value } of subs) {
1014
+ if (!isRecord(value)) {
1015
+ continue;
1016
+ }
1017
+ for (const key of ["nodes", "edges"]) {
1018
+ const members = memberSet(value[key]);
1019
+ if (members === null) {
1020
+ all[key] = true;
1021
+ }
1022
+ else {
1023
+ members.forEach((id) => held[key].add(id));
1024
+ }
1025
+ }
1026
+ }
1027
+ const lost = (key, root) => all[key] ? 0 : [...root].filter((id) => !held[key].has(id)).length;
1028
+ const nodes = lost("nodes", this.rootNodes);
1029
+ const edges = lost("edges", this.rootEdges);
1030
+ if (nodes + edges > 0) {
1031
+ this.report.warning("unsupported", CX_ISSUE.ROOT_ONLY, `${nodes} node(s) and ${edges} edge(s) of the root network belong to no subnetwork (cySubNetworks); they are not read`, { element: "cySubNetworks" });
1032
+ }
1033
+ }
1034
+ /**
1035
+ * The handle of a structural column, declared on first use.
1036
+ * @param domain - node or edge
1037
+ * @param current - the handle so far
1038
+ * @param decl - the declaration
1039
+ * @returns the handle
1040
+ */
1041
+ structural(domain, current, decl) {
1042
+ return current === INVALID_INDEX ? declareResolved(this.sink, domain, decl, this.report).handle : current;
1043
+ }
1044
+ // ------------------------------------------------------------ attribute columns
1045
+ /**
1046
+ * Type and declare the attribute columns of a table for this graph: each name's type is the
1047
+ * wider of the types its applicable elements and its cyTableColumn entries give (W_WIDENED when
1048
+ * they differ); a name with a type CX does not define keeps its values as text.
1049
+ * @param domain - node or edge
1050
+ * @returns the columns by attribute name
1051
+ */
1052
+ declareAttributes(domain) {
1053
+ const columns = new Map();
1054
+ const declared = this.tableColumns(domain === "node" ? "node_table" : "edge_table");
1055
+ const names = new Set([...declared.keys(), ...this.doc.attributes[domain].keys()]);
1056
+ for (const name of names) {
1057
+ if (name === "" || (domain === "edge" && name === this.options.weightFrom)) {
1058
+ continue;
1059
+ }
1060
+ const types = [];
1061
+ let unknown = false;
1062
+ const tableType = declared.get(name);
1063
+ if (tableType !== undefined) {
1064
+ types.push(tableType);
1065
+ }
1066
+ for (const { value } of this.doc.attributes[domain].get(name) ?? []) {
1067
+ if (!isRecord(value) || this.scopePeek(value.s) === 0) {
1068
+ continue;
1069
+ }
1070
+ const type = cxType(value.d);
1071
+ if (type === null) {
1072
+ unknown = true;
1073
+ continue;
1074
+ }
1075
+ if (!types.some((t) => t.scalar === type.scalar && t.list === type.list)) {
1076
+ types.push(type);
1077
+ }
1078
+ }
1079
+ if (unknown) {
1080
+ this.report.warnOnce("unsupported", UNKNOWN_ATTR_TYPE_CODE, `a ${domain} attribute "${name}" declares a data type CX does not define; its values are kept as text`, { element: name }, `${UNKNOWN_ATTR_TYPE_CODE}:${domain}:${name}`);
1081
+ types.push({ scalar: "string", list: false });
1082
+ }
1083
+ let type = types[0] ?? { scalar: "string", list: false };
1084
+ for (const other of types.slice(1)) {
1085
+ type = type === null ? null : widenType(type, other);
1086
+ }
1087
+ if (types.length > 1) {
1088
+ this.report.warnOnce("coercion", WIDENED_CODE, `the ${domain} attribute "${name}" has the data types ${types.map(typeText).join(", ")}; the column holds ${type === null ? "their values as JSON" : typeText(type)}`, { element: name }, `${WIDENED_CODE}:${domain}:${name}`);
1089
+ }
1090
+ if (this.isStructural(domain, name)) {
1091
+ columns.set(name, {
1092
+ name,
1093
+ type: { scalar: "string", list: false },
1094
+ handle: INVALID_INDEX,
1095
+ });
1096
+ continue;
1097
+ }
1098
+ columns.set(name, { name, type, handle: this.declareColumn(domain, name, type) });
1099
+ }
1100
+ return columns;
1101
+ }
1102
+ /**
1103
+ * Whether an attribute name is the column of a core field (n / r on nodes, i on edges).
1104
+ * @param domain - node or edge
1105
+ * @param name - the attribute name
1106
+ * @returns true for name, represents and interaction
1107
+ */
1108
+ isStructural(domain, name) {
1109
+ return domain === "node" ? name === "name" || name === "represents" : name === "interaction";
1110
+ }
1111
+ /**
1112
+ * The scope of a value without counting a dangling one (for typing, which runs before the values).
1113
+ * @param scope - the element's s
1114
+ * @returns as scopeOf()
1115
+ */
1116
+ scopePeek(scope) {
1117
+ if (scope === undefined || scope === null) {
1118
+ return 1;
1119
+ }
1120
+ const id = refId(scope);
1121
+ return id !== null && id === this.plan.subnetwork ? 2 : 0;
1122
+ }
1123
+ /**
1124
+ * The column types cyTableColumn declares for one table and this graph.
1125
+ * @param table - node_table, edge_table or network_table
1126
+ * @returns name -> type
1127
+ */
1128
+ tableColumns(table) {
1129
+ const out = new Map();
1130
+ for (const { value } of aspect(this.doc, "cyTableColumn")) {
1131
+ if (!isRecord(value) || value.applies_to !== table || typeof value.n !== "string") {
1132
+ continue;
1133
+ }
1134
+ if (this.scopePeek(value.s) === 0) {
1135
+ continue;
1136
+ }
1137
+ const type = cxType(value.d);
1138
+ if (type !== null) {
1139
+ out.set(value.n, type);
1140
+ }
1141
+ }
1142
+ return out;
1143
+ }
1144
+ /**
1145
+ * Declare one attribute column.
1146
+ * @param domain - node or edge
1147
+ * @param name - the attribute name
1148
+ * @param type - the type, or null for a json column
1149
+ * @returns the handle
1150
+ */
1151
+ declareColumn(domain, name, type) {
1152
+ const { long } = this.options;
1153
+ const decl = type === null
1154
+ ? { name, dtype: "json", nullable: true, origin: { format: CX_FORMAT, id: null } }
1155
+ : {
1156
+ name,
1157
+ dtype: type.list ? "list" : scalarDtype(type.scalar, long),
1158
+ ...(type.list ? { itemDtype: scalarDtype(type.scalar, long) } : {}),
1159
+ nullable: true,
1160
+ origin: { format: CX_FORMAT, id: null, type: typeText(type) },
1161
+ };
1162
+ return declareResolved(this.sink, domain, decl, this.report).handle;
1163
+ }
1164
+ /**
1165
+ * Write the attribute values of a table: per element, its own subnetwork's value beats the
1166
+ * unscoped one; a repeated value of the same scope is the later one (W_DUPLICATE_ATTRIBUTE
1167
+ * when they differ).
1168
+ * @param domain - node or edge
1169
+ * @param columns - the columns from declareAttributes()
1170
+ */
1171
+ writeAttributes(domain, columns) {
1172
+ const rows = domain === "node" ? this.nodeRows : this.edgeRows;
1173
+ const root = domain === "node" ? this.rootNodes : this.rootEdges;
1174
+ for (const column of columns.values()) {
1175
+ const chosen = new Map();
1176
+ for (const held of this.doc.attributes[domain].get(column.name) ?? []) {
1177
+ this.checkAbort();
1178
+ const { value, line } = held;
1179
+ if (!isRecord(value)) {
1180
+ this.report.error("parse-error", BAD_ASPECT_BLOCK_CODE, `a ${domain}Attributes element is not an object`, {
1181
+ line,
1182
+ element: `${domain}Attributes`,
1183
+ });
1184
+ continue;
1185
+ }
1186
+ const scope = this.scopeOf(value.s);
1187
+ if (scope === 0) {
1188
+ continue;
1189
+ }
1190
+ const target = refId(value.po);
1191
+ const row = target === null
1192
+ ? undefined
1193
+ : rows.get(this.options.ids === "keep" ? target : this.coercer.value(target));
1194
+ if (row === undefined) {
1195
+ if (target === null || !root.has(target)) {
1196
+ this.dangle(`${domain} attribute target`);
1197
+ }
1198
+ continue;
1199
+ }
1200
+ if (value.v === undefined) {
1201
+ this.report.error("missing-value", BAD_VALUE_CODE, `a ${domain}Attributes element for "${column.name}" has no v; skipped`, {
1202
+ line,
1203
+ element: column.name,
1204
+ });
1205
+ continue;
1206
+ }
1207
+ if (value.v === null) {
1208
+ continue;
1209
+ }
1210
+ const previous = chosen.get(row);
1211
+ if (previous !== undefined && previous.scope > scope) {
1212
+ continue;
1213
+ }
1214
+ if (previous?.scope === scope &&
1215
+ JSON.stringify(previous.value) !== JSON.stringify(plainJson(value.v))) {
1216
+ this.report.warnOnce("validation-error", DUPLICATE_ATTRIBUTE_CODE, `${domain} ${shown(value.po)} has the attribute "${column.name}" twice; the later value wins`, { line, element: column.name }, `${DUPLICATE_ATTRIBUTE_CODE}:${domain}:${column.name}`);
1217
+ }
1218
+ chosen.set(row, { scope, value: value.v, d: value.d, line });
1219
+ }
1220
+ for (const [row, { value, d, line }] of chosen) {
1221
+ this.writeCell(domain, column, row, value, d, line);
1222
+ }
1223
+ }
1224
+ }
1225
+ /**
1226
+ * Parse and write one attribute cell. The core fields' attributes (name, represents,
1227
+ * interaction) go into their columns; a name attribute that differs from n wins with
1228
+ * W_DUPLICATE_ATTRIBUTE.
1229
+ * @param domain - node or edge
1230
+ * @param column - the column
1231
+ * @param row - the row
1232
+ * @param raw - the value
1233
+ * @param d - the data type the value was written with
1234
+ * @param line - its line
1235
+ */
1236
+ writeCell(domain, column, row, raw, d, line) {
1237
+ const element = `${domain} attribute "${column.name}"`;
1238
+ let value;
1239
+ if (column.type === null) {
1240
+ value = plainJson(raw, (digits) => {
1241
+ this.precision(column.name, digits);
1242
+ });
1243
+ }
1244
+ else {
1245
+ const own = cxType(d) ?? { scalar: "string", list: false };
1246
+ const parsed = parseValue(raw, own, this.options.long, (digits) => {
1247
+ this.precision(column.name, digits);
1248
+ });
1249
+ if (parsed === UNSET) {
1250
+ return;
1251
+ }
1252
+ if (parsed === BAD) {
1253
+ this.report.error("validation-error", BAD_VALUE_CODE, `${element}: ${shown(raw)} is not a ${typeText(own)}; the cell is unset`, { line, element: column.name });
1254
+ return;
1255
+ }
1256
+ value = toColumn(parsed, column.type);
1257
+ }
1258
+ let { handle } = column;
1259
+ if (this.isStructural(domain, column.name)) {
1260
+ handle = this.coreColumn(domain, column.name);
1261
+ const prior = this.coreValue(domain, row, column.name);
1262
+ if (prior !== undefined && prior !== value) {
1263
+ this.report.warnOnce("validation-error", DUPLICATE_ATTRIBUTE_CODE, `a ${domain} has "${column.name}" both as its core field and as an attribute with another value; the attribute wins`, { line, element: column.name }, `${DUPLICATE_ATTRIBUTE_CODE}:core:${column.name}`);
1264
+ }
1265
+ }
1266
+ try {
1267
+ if (domain === "node") {
1268
+ this.sink.setNodeValue(handle, row, value);
1269
+ }
1270
+ else {
1271
+ this.sink.setEdgeValue(handle, row, value);
1272
+ }
1273
+ }
1274
+ catch (err) {
1275
+ this.report.recordError(err, { line, element: column.name });
1276
+ }
1277
+ }
1278
+ /**
1279
+ * The core field value of a row.
1280
+ * @param domain - node or edge
1281
+ * @param row - the row
1282
+ * @param name - name, represents or interaction
1283
+ * @returns the value, or undefined
1284
+ */
1285
+ coreValue(domain, row, name) {
1286
+ return this.coreValues.get(`${domain}:${name}`)?.get(row);
1287
+ }
1288
+ /**
1289
+ * The column of a core field.
1290
+ * @param domain - node or edge
1291
+ * @param name - name, represents or interaction
1292
+ * @returns the handle
1293
+ */
1294
+ coreColumn(domain, name) {
1295
+ if (domain === "edge") {
1296
+ this.interactionHandle = this.structural("edge", this.interactionHandle, {
1297
+ name: "interaction",
1298
+ dtype: "string",
1299
+ nullable: true,
1300
+ origin: { format: CX_FORMAT, id: "i", type: "string" },
1301
+ });
1302
+ return this.interactionHandle;
1303
+ }
1304
+ if (name === "name") {
1305
+ this.nameHandle = this.structural("node", this.nameHandle, {
1306
+ name: "name",
1307
+ dtype: "string",
1308
+ role: "label",
1309
+ nullable: true,
1310
+ origin: { format: CX_FORMAT, id: "n", type: "string" },
1311
+ });
1312
+ return this.nameHandle;
1313
+ }
1314
+ this.representsHandle = this.structural("node", this.representsHandle, {
1315
+ name: "represents",
1316
+ dtype: "string",
1317
+ nullable: true,
1318
+ origin: { format: CX_FORMAT, id: "r", type: "string" },
1319
+ });
1320
+ return this.representsHandle;
1321
+ }
1322
+ // ------------------------------------------------------------ nodes, groups, layout
1323
+ /** Read the nodes of this graph. */
1324
+ readNodes() {
1325
+ const { report, sink } = this;
1326
+ for (const held of aspect(this.doc, "nodes")) {
1327
+ this.checkAbort();
1328
+ const { value, line } = held;
1329
+ if (!isRecord(value)) {
1330
+ report.error("parse-error", BAD_ASPECT_BLOCK_CODE, `a nodes element is ${shown(value)}, not an object`, {
1331
+ line,
1332
+ element: "nodes",
1333
+ });
1334
+ report.counts.skippedNodes++;
1335
+ continue;
1336
+ }
1337
+ if (value["@id"] === undefined || value["@id"] === null) {
1338
+ report.error("missing-value", MISSING_ID_CODE, "a node has no @id", { line, element: "nodes" });
1339
+ report.counts.skippedNodes++;
1340
+ continue;
1341
+ }
1342
+ const element = `node ${shown(value["@id"])}`;
1343
+ const id = this.idOf(value["@id"], (held.inexact & 1) !== 0, element);
1344
+ if (id === null) {
1345
+ report.counts.skippedNodes++;
1346
+ continue;
1347
+ }
1348
+ if (!this.inGraph(id)) {
1349
+ continue;
1350
+ }
1351
+ let row = this.nodeRows.get(id);
1352
+ if (row !== undefined) {
1353
+ report.warning("merged", DUPLICATE_NODE_CODE, `${element} is declared more than once; its fields are merged (the later values win)`, { line, element });
1354
+ }
1355
+ else {
1356
+ try {
1357
+ row = sink.addNode(id);
1358
+ }
1359
+ catch (err) {
1360
+ report.recordError(err, { line, element });
1361
+ report.counts.skippedNodes++;
1362
+ continue;
1363
+ }
1364
+ this.nodeRows.set(id, row);
1365
+ report.counts.nodes++;
1366
+ }
1367
+ this.writeCore("node", row, "name", value.n, element, line);
1368
+ this.writeCore("node", row, "represents", value.r, element, line);
1369
+ }
1370
+ }
1371
+ /**
1372
+ * Write a core string field (n, r, i).
1373
+ * @param domain - node or edge
1374
+ * @param row - the row
1375
+ * @param name - the column
1376
+ * @param raw - the value
1377
+ * @param element - the element name
1378
+ * @param line - its line
1379
+ */
1380
+ writeCore(domain, row, name, raw, element, line) {
1381
+ if (raw === undefined || raw === null) {
1382
+ return;
1383
+ }
1384
+ if (typeof raw !== "string") {
1385
+ this.report.error("validation-error", BAD_VALUE_CODE, `${element}: ${name} is ${shown(raw)}, not a string`, {
1386
+ line,
1387
+ element,
1388
+ });
1389
+ return;
1390
+ }
1391
+ const handle = this.coreColumn(domain, name);
1392
+ if (domain === "node") {
1393
+ this.sink.setNodeValue(handle, row, raw);
1394
+ }
1395
+ else {
1396
+ this.sink.setEdgeValue(handle, row, raw);
1397
+ }
1398
+ const key = `${domain}:${name}`;
1399
+ let values = this.coreValues.get(key);
1400
+ if (values === undefined) {
1401
+ values = new Map();
1402
+ this.coreValues.set(key, values);
1403
+ }
1404
+ values.set(row, raw);
1405
+ }
1406
+ /**
1407
+ * Read cyGroups: a group's members get the group node as their parent (parents when a node is in
1408
+ * several groups); a group whose id is not a node gets one (W_CX_GROUP_NODE_ADDED); collapsed is
1409
+ * a bool column on the group node.
1410
+ */
1411
+ readGroups() {
1412
+ const groups = aspect(this.doc, "cyGroups");
1413
+ if (groups.length === 0) {
1414
+ return;
1415
+ }
1416
+ const parents = new Map();
1417
+ const collapsed = [];
1418
+ for (const { value, line } of groups) {
1419
+ if (!isRecord(value)) {
1420
+ continue;
1421
+ }
1422
+ const id = refId(value["@id"]);
1423
+ if (id === null) {
1424
+ this.report.error("missing-value", MISSING_ID_CODE, "a cyGroups element has no @id", {
1425
+ line,
1426
+ element: "cyGroups",
1427
+ });
1428
+ continue;
1429
+ }
1430
+ let groupRow = this.nodeRows.get(id);
1431
+ if (groupRow === undefined) {
1432
+ if (this.rootNodes.has(id) || !this.hasMemberIn(value.nodes)) {
1433
+ continue;
1434
+ }
1435
+ try {
1436
+ groupRow = this.sink.addNode(id);
1437
+ }
1438
+ catch (err) {
1439
+ this.report.recordError(err, { line, element: String(id) });
1440
+ continue;
1441
+ }
1442
+ this.nodeRows.set(id, groupRow);
1443
+ this.report.counts.nodes++;
1444
+ this.report.warning("coercion", CX_ISSUE.GROUP_NODE_ADDED, `group ${String(id)} is not a node; its group node is added`, { line, element: String(id) });
1445
+ }
1446
+ if (typeof value.n === "string" && this.coreValue("node", groupRow, "name") === undefined) {
1447
+ this.writeCore("node", groupRow, "name", value.n, `group ${String(id)}`, line);
1448
+ }
1449
+ if (typeof value.collapsed === "boolean") {
1450
+ collapsed.push([groupRow, value.collapsed]);
1451
+ }
1452
+ for (const raw of Array.isArray(value.nodes) ? value.nodes : []) {
1453
+ const member = refId(raw);
1454
+ const row = member === null ? undefined : this.nodeRows.get(member);
1455
+ if (row === undefined) {
1456
+ if (member === null || !this.rootNodes.has(member)) {
1457
+ this.report.error("missing-value", UNKNOWN_PARENT_CODE, `group ${String(id)}: the member ${shown(raw)} is not a node`, { line, element: String(id) });
1458
+ }
1459
+ continue;
1460
+ }
1461
+ if (this.closesCycle(parents, row, groupRow)) {
1462
+ this.report.error("validation-error", PARENT_CYCLE_CODE, `group ${String(id)}: making ${shown(raw)} a member would close a parent cycle; that membership is dropped`, { line, element: String(id) });
1463
+ continue;
1464
+ }
1465
+ const list = parents.get(row) ?? [];
1466
+ if (!list.includes(groupRow)) {
1467
+ list.push(groupRow);
1468
+ }
1469
+ parents.set(row, list);
1470
+ }
1471
+ }
1472
+ this.writeParents(parents);
1473
+ if (collapsed.length > 0) {
1474
+ const { handle } = declareResolved(this.sink, "node", {
1475
+ name: "collapsed",
1476
+ dtype: "bool",
1477
+ nullable: true,
1478
+ origin: { format: CX_FORMAT, namespace: "cyGroups" },
1479
+ }, this.report);
1480
+ for (const [row, flag] of collapsed) {
1481
+ this.sink.setNodeValue(handle, row, flag);
1482
+ }
1483
+ }
1484
+ }
1485
+ /**
1486
+ * Whether a member list names a node of this graph.
1487
+ * @param raw - the list
1488
+ * @returns true when one member is in the graph
1489
+ */
1490
+ hasMemberIn(raw) {
1491
+ return (Array.isArray(raw) &&
1492
+ raw.some((item) => {
1493
+ const id = refId(item);
1494
+ return id !== null && this.nodeRows.has(id);
1495
+ }));
1496
+ }
1497
+ /**
1498
+ * Whether making `child` a member of `group` closes a cycle: the group already is, through its
1499
+ * own parents, below the child.
1500
+ * @param parents - the memberships so far
1501
+ * @param child - the member row
1502
+ * @param group - the group row
1503
+ * @returns true for a cycle
1504
+ */
1505
+ closesCycle(parents, child, group) {
1506
+ const stack = [group];
1507
+ const seen = new Set();
1508
+ while (stack.length > 0) {
1509
+ const row = stack.pop();
1510
+ if (row === child) {
1511
+ return true;
1512
+ }
1513
+ if (seen.has(row)) {
1514
+ continue;
1515
+ }
1516
+ seen.add(row);
1517
+ stack.push(...(parents.get(row) ?? []));
1518
+ }
1519
+ return false;
1520
+ }
1521
+ /**
1522
+ * Write the group memberships: a parent column, or a parents list column when a node is in
1523
+ * several groups.
1524
+ * @param parents - member row -> group rows
1525
+ */
1526
+ writeParents(parents) {
1527
+ if (parents.size === 0) {
1528
+ return;
1529
+ }
1530
+ const several = [...parents.values()].some((list) => list.length > 1);
1531
+ const decl = several
1532
+ ? { name: "parents", dtype: "list", itemDtype: "u32", role: "parents", refersTo: "node", nullable: true }
1533
+ : { name: "parent", dtype: "u32", role: "parent", refersTo: "node", nullable: true };
1534
+ const { handle } = declareResolved(this.sink, "node", decl, this.report);
1535
+ for (const [row, list] of parents) {
1536
+ this.sink.setNodeValue(handle, row, several ? list : list[0]);
1537
+ }
1538
+ }
1539
+ /**
1540
+ * Read cartesianLayout for this graph's views: the first view is the position (y flipped), z the
1541
+ * z column, every other view a position@n column.
1542
+ */
1543
+ readLayout() {
1544
+ const entries = aspect(this.doc, "cartesianLayout");
1545
+ if (entries.length === 0) {
1546
+ return;
1547
+ }
1548
+ const { views } = this.plan;
1549
+ const knownViews = this.allViews();
1550
+ const columns = new Map();
1551
+ const zHandle = { current: INVALID_INDEX };
1552
+ const seen = new Set();
1553
+ const point = [0, 0, 0];
1554
+ for (const { value, line } of entries) {
1555
+ this.checkAbort();
1556
+ if (!isRecord(value)) {
1557
+ continue;
1558
+ }
1559
+ const view = value.view === undefined || value.view === null ? null : refId(value.view);
1560
+ if (view !== null && knownViews.size > 0 && !knownViews.has(view)) {
1561
+ this.dangle("layout view reference");
1562
+ continue;
1563
+ }
1564
+ const slot = view === null ? 0 : views.indexOf(view);
1565
+ if (slot < 0) {
1566
+ continue;
1567
+ }
1568
+ if (typeof value.x !== "number" || typeof value.y !== "number") {
1569
+ this.report.error("validation-error", BAD_VALUE_CODE, `a cartesianLayout element for node ${shown(value.node)} has no numeric x and y; skipped`, { line, element: "cartesianLayout" });
1570
+ continue;
1571
+ }
1572
+ const node = refId(value.node);
1573
+ const row = node === null ? undefined : this.nodeRows.get(node);
1574
+ if (row === undefined) {
1575
+ if (node === null || !this.rootNodes.has(node)) {
1576
+ this.dangle("layout entry");
1577
+ }
1578
+ continue;
1579
+ }
1580
+ const key = `${slot}:${row}`;
1581
+ if (seen.has(key)) {
1582
+ this.report.warnOnce("validation-error", DUPLICATE_ATTRIBUTE_CODE, `node ${shown(value.node)} has two layout entries for one view; the later wins`, { line, element: "cartesianLayout" }, `${DUPLICATE_ATTRIBUTE_CODE}:layout`);
1583
+ }
1584
+ seen.add(key);
1585
+ const z = typeof value.z === "number" ? value.z : null;
1586
+ point[0] = value.x;
1587
+ point[1] = flipY(value.y);
1588
+ point[2] = slot === 0 && this.zAs === "position" && z !== null ? z : 0;
1589
+ this.sink.setNodeValue(this.layoutColumn(columns, slot, views), row, point);
1590
+ if (slot === 0 && z !== null && this.zAs === "column") {
1591
+ zHandle.current = this.structural("node", zHandle.current, zDecl(CX_FORMAT));
1592
+ this.sink.setNodeValue(zHandle.current, row, z);
1593
+ }
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Every view id the document names.
1598
+ * @returns the set (empty when the document names none)
1599
+ */
1600
+ allViews() {
1601
+ const out = new Set();
1602
+ for (const { value } of aspect(this.doc, "cyNetworkRelations")) {
1603
+ if (isRecord(value) && value.r === "view") {
1604
+ const id = refId(value.c);
1605
+ if (id !== null) {
1606
+ out.add(id);
1607
+ }
1608
+ }
1609
+ }
1610
+ for (const { value } of aspect(this.doc, "cyViews")) {
1611
+ const id = isRecord(value) ? refId(value["@id"]) : null;
1612
+ if (id !== null) {
1613
+ out.add(id);
1614
+ }
1615
+ }
1616
+ return out;
1617
+ }
1618
+ /**
1619
+ * The position column of a view slot, declared on first use.
1620
+ * @param columns - slot -> handle
1621
+ * @param slot - 0 for the position, n - 1 for position@n
1622
+ * @param views - the graph's views
1623
+ * @returns the handle
1624
+ */
1625
+ layoutColumn(columns, slot, views) {
1626
+ let handle = columns.get(slot);
1627
+ if (handle === undefined) {
1628
+ const base = positionDecl(CX_FORMAT, this.zAs === "position" && slot === 0 ? 3 : 2);
1629
+ const decl = slot === 0
1630
+ ? base
1631
+ : {
1632
+ ...base,
1633
+ name: `position@${slot + 1}`,
1634
+ role: undefined,
1635
+ origin: { format: CX_FORMAT, namespace: "cytoscape", id: String(views[slot]) },
1636
+ };
1637
+ ({ handle } = declareResolved(this.sink, "node", decl, this.report));
1638
+ columns.set(slot, handle);
1639
+ }
1640
+ return handle;
1641
+ }
1642
+ // ------------------------------------------------------------ edges
1643
+ /** Read the edges of this graph. */
1644
+ readEdges() {
1645
+ const { report, sink } = this;
1646
+ const members = this.plan.edges;
1647
+ for (const held of aspect(this.doc, "edges")) {
1648
+ this.checkAbort();
1649
+ const { value, line } = held;
1650
+ if (!isRecord(value)) {
1651
+ report.error("parse-error", BAD_ASPECT_BLOCK_CODE, `an edges element is ${shown(value)}, not an object`, {
1652
+ line,
1653
+ element: "edges",
1654
+ });
1655
+ report.counts.skippedEdges++;
1656
+ continue;
1657
+ }
1658
+ if (value["@id"] === undefined || value["@id"] === null) {
1659
+ report.error("missing-value", MISSING_ID_CODE, "an edge has no @id", { line, element: "edges" });
1660
+ report.counts.skippedEdges++;
1661
+ continue;
1662
+ }
1663
+ const element = `edge ${shown(value["@id"])}`;
1664
+ const id = this.idOf(value["@id"], (held.inexact & 1) !== 0, element, true);
1665
+ if (id === null) {
1666
+ report.counts.skippedEdges++;
1667
+ continue;
1668
+ }
1669
+ if (members !== null && !members.has(id)) {
1670
+ continue;
1671
+ }
1672
+ if (value.s === undefined || value.s === null || value.t === undefined || value.t === null) {
1673
+ report.error("missing-value", MISSING_ENDPOINT_CODE, `${element} has no ${value.s === undefined || value.s === null ? "s" : "t"}`, {
1674
+ line,
1675
+ element,
1676
+ });
1677
+ report.counts.skippedEdges++;
1678
+ continue;
1679
+ }
1680
+ const s = this.idOf(value.s, (held.inexact & 2) !== 0, element);
1681
+ const t = s === null ? null : this.idOf(value.t, (held.inexact & 4) !== 0, element);
1682
+ if (s === null || t === null) {
1683
+ report.counts.skippedEdges++;
1684
+ continue;
1685
+ }
1686
+ if (this.edgeRows.has(id)) {
1687
+ report.error("validation-error", DUPLICATE_EDGE_ID_CODE, `${element} is declared more than once; the later edge is skipped`, { line, element });
1688
+ report.counts.skippedEdges++;
1689
+ continue;
1690
+ }
1691
+ if (!this.endpoint(s, element, line) || !this.endpoint(t, element, line)) {
1692
+ report.counts.skippedEdges++;
1693
+ continue;
1694
+ }
1695
+ let weight;
1696
+ try {
1697
+ weight = this.weightOf(id);
1698
+ }
1699
+ catch (err) {
1700
+ report.recordError(err, { line, element });
1701
+ report.counts.skippedEdges++;
1702
+ continue;
1703
+ }
1704
+ const before = sink.edgeCount;
1705
+ let edge;
1706
+ try {
1707
+ edge = this.direction.addEdge(s, t, "directed", weight, { line, element });
1708
+ }
1709
+ catch (err) {
1710
+ report.recordError(err, { line, element });
1711
+ report.counts.skippedEdges++;
1712
+ continue;
1713
+ }
1714
+ if (weight !== undefined) {
1715
+ this.weighted = true;
1716
+ }
1717
+ report.counts.edges += sink.edgeCount - before;
1718
+ this.edgeRows.set(id, edge);
1719
+ this.edgeIdHandle = this.structural("edge", this.edgeIdHandle, {
1720
+ name: uniqueColumnName("id", "cx", (n) => sink.edgeColumn(n) !== INVALID_INDEX),
1721
+ dtype: "f64",
1722
+ role: "id",
1723
+ nullable: true,
1724
+ });
1725
+ sink.setEdgeValue(this.edgeIdHandle, edge, typeof id === "number" ? id : Number(id));
1726
+ this.writeCore("edge", edge, "interaction", value.i, element, line);
1727
+ }
1728
+ }
1729
+ /**
1730
+ * Whether an endpoint is a node of this graph; under addMissingNodes it is created.
1731
+ * @param id - the endpoint
1732
+ * @param element - the edge name
1733
+ * @param line - its line
1734
+ * @returns false when the edge is to be skipped (reported)
1735
+ */
1736
+ endpoint(id, element, line) {
1737
+ if (this.nodeRows.has(id)) {
1738
+ return true;
1739
+ }
1740
+ if (!this.options.addMissingNodes) {
1741
+ this.report.error("missing-value", CX_ISSUE.UNKNOWN_NODE, `${element}: the endpoint ${String(id)} is not a node of the graph; the edge is skipped`, { line, element });
1742
+ return false;
1743
+ }
1744
+ try {
1745
+ this.nodeRows.set(id, this.sink.addNode(id));
1746
+ }
1747
+ catch (err) {
1748
+ this.report.recordError(err, { line, element });
1749
+ return false;
1750
+ }
1751
+ this.report.counts.nodes++;
1752
+ return true;
1753
+ }
1754
+ /**
1755
+ * The weight of an edge: its weightFrom attribute, parsed as a number.
1756
+ * @param id - the edge id
1757
+ * @returns the weight, or undefined; E_INVALID_WEIGHT for a non-number
1758
+ */
1759
+ weightOf(id) {
1760
+ const { weightFrom } = this.options;
1761
+ if (weightFrom === null) {
1762
+ return undefined;
1763
+ }
1764
+ if (this.weights === null) {
1765
+ this.weights = new Map();
1766
+ for (const { value } of this.doc.attributes.edge.get(weightFrom) ?? []) {
1767
+ if (isRecord(value) && this.scopePeek(value.s) !== 0) {
1768
+ const po = refId(value.po);
1769
+ if (po !== null && value.v !== undefined && value.v !== null) {
1770
+ this.weights.set(po, plainJson(value.v));
1771
+ }
1772
+ }
1773
+ }
1774
+ }
1775
+ const raw = this.weights.get(id);
1776
+ if (typeof raw === "string" && isNullText(raw)) {
1777
+ return undefined;
1778
+ }
1779
+ return weightFromValue(raw);
1780
+ }
1781
+ // ------------------------------------------------------------ network, visual properties, provenance
1782
+ /** Write the network attributes (unscoped and this subnetwork's, its own winning) as graph columns. */
1783
+ readNetworkAttributes() {
1784
+ for (const [name, elements] of this.doc.attributes.network) {
1785
+ let chosen = null;
1786
+ let chosenScope = 0;
1787
+ for (const { value } of elements) {
1788
+ if (!isRecord(value)) {
1789
+ continue;
1790
+ }
1791
+ const scope = this.scopeOf(value.s);
1792
+ if (scope !== 0 && scope >= chosenScope) {
1793
+ chosen = value;
1794
+ chosenScope = scope;
1795
+ }
1796
+ }
1797
+ if (chosen === null || chosen.v === undefined || chosen.v === null || name === "") {
1798
+ continue;
1799
+ }
1800
+ const type = cxType(chosen.d);
1801
+ if (type === null) {
1802
+ this.report.warnOnce("unsupported", UNKNOWN_ATTR_TYPE_CODE, `the network attribute "${name}" declares a data type CX does not define; kept as text`, { element: name }, `${UNKNOWN_ATTR_TYPE_CODE}:network:${name}`);
1803
+ }
1804
+ const effective = type ?? { scalar: "string", list: false };
1805
+ const parsed = parseValue(chosen.v, effective, this.options.long, (digits) => {
1806
+ this.precision(name, digits);
1807
+ });
1808
+ if (parsed === UNSET) {
1809
+ continue;
1810
+ }
1811
+ if (parsed === BAD) {
1812
+ this.report.error("validation-error", BAD_VALUE_CODE, `the network attribute "${name}" is ${shown(chosen.v)}, not a ${typeText(effective)}; unset`, { element: name });
1813
+ continue;
1814
+ }
1815
+ if ((name === "name" || name === "description") && typeof parsed === "string") {
1816
+ continue;
1817
+ }
1818
+ const scalar = scalarDtype(effective.scalar, this.options.long);
1819
+ try {
1820
+ this.sink.setGraphValue(name, parsed, {
1821
+ dtype: effective.list ? "list" : scalar,
1822
+ ...(effective.list ? { itemDtype: scalar } : {}),
1823
+ origin: { format: CX_FORMAT, id: null, type: typeText(effective) },
1824
+ });
1825
+ }
1826
+ catch (err) {
1827
+ this.report.recordError(err, { element: name });
1828
+ }
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Read cyVisualProperties for this graph's views: per-element and network values become columns
1833
+ * (origin namespace cx.bypass); defaults, mappings and dependencies are style rules, kept and not
1834
+ * applied.
1835
+ */
1836
+ readVisualProperties() {
1837
+ const entries = aspect(this.doc, "cyVisualProperties");
1838
+ // Cytoscape's table-cell styles: a visual property aspect graph-io keeps but does not read
1839
+ const tableStyles = TABLE_STYLE_ASPECTS.reduce((n, name) => n + (this.doc.kept.get(name)?.length ?? 0), 0);
1840
+ // the graph's own view (its first): the one its positions come from; the others stay in meta.extra.cx
1841
+ const views = new Set(this.plan.views.slice(0, 1));
1842
+ const columns = { node: new Map(), edge: new Map() };
1843
+ const network = new Map();
1844
+ let defaults = 0;
1845
+ let mappings = 0;
1846
+ let dependencies = 0;
1847
+ for (const { value } of entries) {
1848
+ if (!isRecord(value)) {
1849
+ continue;
1850
+ }
1851
+ const view = value.view === undefined || value.view === null ? null : refId(value.view);
1852
+ if (view !== null && views.size > 0 && !views.has(view)) {
1853
+ continue;
1854
+ }
1855
+ const properties = isRecord(value.properties) ? value.properties : {};
1856
+ mappings += isRecord(value.mappings) ? Object.keys(value.mappings).length : 0;
1857
+ dependencies += isRecord(value.dependencies) ? Object.keys(value.dependencies).length : 0;
1858
+ switch (value.properties_of) {
1859
+ case "nodes":
1860
+ case "edges": {
1861
+ const domain = value.properties_of === "nodes" ? "node" : "edge";
1862
+ const target = refId(value.applies_to);
1863
+ const row = target === null ? undefined : (domain === "node" ? this.nodeRows : this.edgeRows).get(target);
1864
+ if (row === undefined) {
1865
+ if (target === null || !(domain === "node" ? this.rootNodes : this.rootEdges).has(target)) {
1866
+ this.dangle(`${domain} visual property entry`);
1867
+ }
1868
+ continue;
1869
+ }
1870
+ for (const [property, raw] of Object.entries(properties)) {
1871
+ const list = columns[domain].get(property) ?? [];
1872
+ list.push([row, typeof raw === "string" ? raw : JSON.stringify(raw)]);
1873
+ columns[domain].set(property, list);
1874
+ }
1875
+ break;
1876
+ }
1877
+ case "network":
1878
+ for (const [property, raw] of Object.entries(properties)) {
1879
+ network.set(property, typeof raw === "string" ? raw : JSON.stringify(raw));
1880
+ }
1881
+ break;
1882
+ default:
1883
+ defaults += Object.keys(properties).length;
1884
+ break;
1885
+ }
1886
+ }
1887
+ for (const domain of ["node", "edge"]) {
1888
+ for (const [property, cells] of columns[domain]) {
1889
+ const handle = declareFresh(this.sink, domain, {
1890
+ name: property,
1891
+ dtype: "string",
1892
+ nullable: true,
1893
+ origin: { format: CX_FORMAT, id: null, namespace: CX_BYPASS_NAMESPACE },
1894
+ }, this.report);
1895
+ for (const [row, text] of cells) {
1896
+ if (domain === "node") {
1897
+ this.sink.setNodeValue(handle, row, text);
1898
+ }
1899
+ else {
1900
+ this.sink.setEdgeValue(handle, row, text);
1901
+ }
1902
+ }
1903
+ }
1904
+ }
1905
+ for (const [property, text] of network) {
1906
+ try {
1907
+ this.sink.setGraphValue(property, text, {
1908
+ dtype: "string",
1909
+ origin: { format: CX_FORMAT, id: null, namespace: CX_BYPASS_NAMESPACE },
1910
+ });
1911
+ }
1912
+ catch (err) {
1913
+ this.report.recordError(err, { element: property });
1914
+ }
1915
+ }
1916
+ if (defaults + mappings + dependencies + tableStyles > 0) {
1917
+ const parts = [
1918
+ `cyVisualProperties: ${defaults} default(s), ${mappings} mapping(s), ${dependencies} dependenc(ies)`,
1919
+ ];
1920
+ if (tableStyles > 0) {
1921
+ parts.push(`${tableStyles} table style element(s)`);
1922
+ }
1923
+ this.report.warning("unsupported", STYLES_NOT_IMPORTED_CODE, `the file's style rules are not applied (${parts.join("; ")}); they are kept in meta.extra.cx (style import is issue #706)`, { element: "cyVisualProperties" });
1924
+ }
1925
+ }
1926
+ /**
1927
+ * Read the provenance aspects: citations and supports as extension tables cx:citations and
1928
+ * cx:supports, the node / edge links to them as list columns, functionTerms as a json column,
1929
+ * reifiedEdges as a u32 column referring to edges.
1930
+ */
1931
+ readProvenance() {
1932
+ for (const name of ["citations", "supports"]) {
1933
+ this.extensionTable(`cx:${name}`, aspect(this.doc, name));
1934
+ }
1935
+ for (const [aspectName, domain, column] of [
1936
+ ["nodeCitations", "node", "citations"],
1937
+ ["edgeCitations", "edge", "citations"],
1938
+ ["nodeSupports", "node", "supports"],
1939
+ ["edgeSupports", "edge", "supports"],
1940
+ ]) {
1941
+ this.linkColumn(aspectName, domain, column);
1942
+ }
1943
+ const terms = [];
1944
+ for (const { value } of aspect(this.doc, "functionTerms")) {
1945
+ if (!isRecord(value)) {
1946
+ continue;
1947
+ }
1948
+ const po = refId(value.po);
1949
+ const row = po === null ? undefined : this.nodeRows.get(po);
1950
+ if (row === undefined) {
1951
+ if (po === null || !this.rootNodes.has(po)) {
1952
+ this.dangle("functionTerms entry");
1953
+ }
1954
+ continue;
1955
+ }
1956
+ const { po: _po, ...term } = value;
1957
+ terms.push([row, plainJson(term)]);
1958
+ }
1959
+ this.writeColumn("node", "functionTerm", "json", null, terms);
1960
+ const reified = [];
1961
+ for (const { value } of aspect(this.doc, "reifiedEdges")) {
1962
+ if (!isRecord(value)) {
1963
+ continue;
1964
+ }
1965
+ const node = refId(value.node);
1966
+ const edge = refId(value.edge);
1967
+ const row = node === null ? undefined : this.nodeRows.get(node);
1968
+ const edgeRow = edge === null ? undefined : this.edgeRows.get(edge);
1969
+ if (row === undefined || edgeRow === undefined) {
1970
+ if (node === null || !this.rootNodes.has(node) || edge === null || !this.rootEdges.has(edge)) {
1971
+ this.dangle("reifiedEdges entry");
1972
+ }
1973
+ continue;
1974
+ }
1975
+ reified.push([row, edgeRow]);
1976
+ }
1977
+ if (reified.length > 0) {
1978
+ const { handle } = declareResolved(this.sink, "node", {
1979
+ name: "reifiedEdge",
1980
+ dtype: "u32",
1981
+ refersTo: "edge",
1982
+ nullable: true,
1983
+ origin: { format: CX_FORMAT, namespace: "reifiedEdges" },
1984
+ }, this.report);
1985
+ for (const [row, edge] of reified) {
1986
+ this.sink.setNodeValue(handle, row, edge);
1987
+ }
1988
+ }
1989
+ }
1990
+ /**
1991
+ * An extension table of a provenance aspect: an f64 `id` column and one json column per field.
1992
+ * @param name - the table name
1993
+ * @param elements - the elements
1994
+ */
1995
+ extensionTable(name, elements) {
1996
+ const records = elements.map((h) => h.value).filter(isRecord);
1997
+ if (records.length === 0) {
1998
+ return;
1999
+ }
2000
+ const fields = [...new Set(records.flatMap((r) => Object.keys(r)).filter((k) => k !== "@id"))];
2001
+ const decls = [
2002
+ { name: "id", dtype: "f64", nullable: true },
2003
+ ...fields.map((field) => ({ name: field, dtype: "json", nullable: true })),
2004
+ ];
2005
+ const table = this.sink.addExtensionTable(name, decls);
2006
+ for (const record of records) {
2007
+ const id = refId(record["@id"]);
2008
+ this.sink.addExtensionRow(table, [
2009
+ id === null ? null : Number(id),
2010
+ ...fields.map((field) => (record[field] === undefined ? null : plainJson(record[field]))),
2011
+ ]);
2012
+ }
2013
+ }
2014
+ /**
2015
+ * A list column of the citation or support ids a node or edge links to.
2016
+ * @param aspectName - nodeCitations, edgeCitations, nodeSupports or edgeSupports
2017
+ * @param domain - node or edge
2018
+ * @param column - citations or supports
2019
+ */
2020
+ linkColumn(aspectName, domain, column) {
2021
+ const rows = domain === "node" ? this.nodeRows : this.edgeRows;
2022
+ const root = domain === "node" ? this.rootNodes : this.rootEdges;
2023
+ const links = new Map();
2024
+ for (const { value } of aspect(this.doc, aspectName)) {
2025
+ if (!isRecord(value)) {
2026
+ continue;
2027
+ }
2028
+ const targets = Array.isArray(value.po) ? value.po : [value.po];
2029
+ const ids = (Array.isArray(value[column]) ? value[column] : [])
2030
+ .map(refId)
2031
+ .filter((id) => id !== null)
2032
+ .map(Number);
2033
+ for (const raw of targets) {
2034
+ const target = refId(raw);
2035
+ const row = target === null ? undefined : rows.get(target);
2036
+ if (row === undefined) {
2037
+ if (target === null || !root.has(target)) {
2038
+ this.dangle(`${aspectName} entry`);
2039
+ }
2040
+ continue;
2041
+ }
2042
+ links.set(row, [...(links.get(row) ?? []), ...ids]);
2043
+ }
2044
+ }
2045
+ this.writeColumn(domain, column, "list", "f64", [...links]);
2046
+ }
2047
+ /**
2048
+ * Declare a column and write its cells (nothing when there are none).
2049
+ * @param domain - node or edge
2050
+ * @param name - the column name
2051
+ * @param dtype - the dtype
2052
+ * @param itemDtype - the list item dtype, or null
2053
+ * @param cells - row and value pairs
2054
+ */
2055
+ writeColumn(domain, name, dtype, itemDtype, cells) {
2056
+ if (cells.length === 0) {
2057
+ return;
2058
+ }
2059
+ const decl = {
2060
+ name,
2061
+ dtype,
2062
+ ...(itemDtype === null ? {} : { itemDtype }),
2063
+ nullable: true,
2064
+ origin: { format: CX_FORMAT, namespace: "provenance" },
2065
+ };
2066
+ const { handle } = declareResolved(this.sink, domain, decl, this.report);
2067
+ for (const [row, value] of cells) {
2068
+ if (domain === "node") {
2069
+ this.sink.setNodeValue(handle, row, value);
2070
+ }
2071
+ else {
2072
+ this.sink.setEdgeValue(handle, row, value);
2073
+ }
2074
+ }
2075
+ }
2076
+ /** Record the metadata: source format, name, description, id type, the kept aspects. */
2077
+ setMeta() {
2078
+ const describe = (key) => {
2079
+ let found = null;
2080
+ for (const { value } of this.doc.attributes.network.get(key) ?? []) {
2081
+ if (isRecord(value) && typeof value.v === "string") {
2082
+ const scope = this.scopePeek(value.s);
2083
+ if (scope === 2) {
2084
+ return value.v;
2085
+ }
2086
+ if (scope === 1) {
2087
+ found ?? (found = value.v);
2088
+ }
2089
+ }
2090
+ }
2091
+ return found;
2092
+ };
2093
+ const extra = Object.fromEntries(this.doc.kept);
2094
+ const groups = aspect(this.doc, "cyGroups").map((h) => plainJson(h.value));
2095
+ if (groups.length > 0) {
2096
+ extra.groups = groups;
2097
+ }
2098
+ if (this.plan.subnetwork !== null) {
2099
+ extra.subnetwork = this.plan.subnetwork;
2100
+ }
2101
+ const { weightFrom } = this.options;
2102
+ const name = this.plan.name ?? describe("name");
2103
+ const description = describe("description");
2104
+ const patch = {
2105
+ sourceFormat: CX_FORMAT,
2106
+ sourceVersion: "1",
2107
+ idType: this.idType,
2108
+ ...(name === null ? {} : { name }),
2109
+ ...(description === null ? {} : { description }),
2110
+ ...(this.weighted && weightFrom !== null
2111
+ ? { weightOrigin: { format: CX_FORMAT, id: null, title: weightFrom, type: "double", namespace: null } }
2112
+ : {}),
2113
+ extra: { cx: extra },
2114
+ };
2115
+ this.sink.setMeta(patch);
2116
+ }
2117
+ }
2118
+ // ============================================================ the plugin
2119
+ /**
2120
+ * Read a document, then build the graphs `pick` selects.
2121
+ * @param input - the input
2122
+ * @param options - the caller's options
2123
+ * @param sinkFor - the sink of graph i, or null to skip it
2124
+ * @param pick - which graph indexes to build, given the plans and the first report
2125
+ * @returns one report per built graph
2126
+ */
2127
+ async function run(input, options, sinkFor, pick) {
2128
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2129
+ const zAs = zAsOption(options?.zAs);
2130
+ const first = new ImportReportBuilder(CX_FORMAT, resolved.errorLimit);
2131
+ reportUnusedOptions(options, first, USED_OPTIONS);
2132
+ const doc = await readDocument(input, first, resolved);
2133
+ const plans = graphPlans(doc);
2134
+ const chosen = pick(plans, first);
2135
+ const reports = [];
2136
+ chosen.forEach((index, k) => {
2137
+ const report = k === 0 ? first : new ImportReportBuilder(CX_FORMAT, resolved.errorLimit);
2138
+ const sink = sinkFor(index);
2139
+ reportSinkOptions(sink, options, report, true);
2140
+ new CxReader(sink, report, resolved, zAs, doc, plans[index]).build();
2141
+ throwIfAborted(resolved.signal);
2142
+ reports.push(report.finish());
2143
+ });
2144
+ return reports;
2145
+ }
2146
+ /**
2147
+ * The CX version 1 importer plugin (design section 1.2).
2148
+ */
2149
+ export const cxImporter = Object.freeze({
2150
+ format: CX_FORMAT,
2151
+ extensions: Object.freeze([".cx"]),
2152
+ mimeTypes: Object.freeze(["application/json"]),
2153
+ /**
2154
+ * Confidence that the head is CX version 1: 0.95 when a top-level array starts with an object
2155
+ * whose first key is numberVerification or metaData (Cytoscape's file filter), 0.7 for another
2156
+ * CX aspect name.
2157
+ * @param head - the first bytes
2158
+ * @returns the confidence
2159
+ */
2160
+ sniff(head) {
2161
+ const text = new TextDecoder("utf-8").decode(head.subarray(0, 1024)).replace(/^\uFEFF/, "");
2162
+ const match = /^\s*\[\s*\{\s*"((?:[^"\\]|\\.)*)"\s*:/.exec(text);
2163
+ if (match === null) {
2164
+ return 0;
2165
+ }
2166
+ if (CX_FIRST_KEYS.includes(match[1])) {
2167
+ return 0.95;
2168
+ }
2169
+ return CX_ASPECT_KEYS.includes(match[1]) ? 0.7 : 0;
2170
+ },
2171
+ /**
2172
+ * Read one graph of a CX document into the sink: the root network, or the subnetwork
2173
+ * graphIndex / graphName picks (the first by default; W_MULTIPLE_GRAPHS names the others).
2174
+ * @param input - the text, bytes or stream
2175
+ * @param sink - the sink
2176
+ * @param options - format-specific and common options
2177
+ * @returns the import report; ImportError on a fatal error or beyond the error limit
2178
+ */
2179
+ async import(input, sink, options) {
2180
+ const [report] = await run(input, options, () => sink, (plans, first) => {
2181
+ const index = chooseGraph(plans.map((p) => p.name), options, first);
2182
+ if (plans.length > 1) {
2183
+ first.warning("unsupported", MULTIPLE_GRAPHS_CODE, `the collection holds ${plans.length} subnetworks; graph ${index} is read and ${plans.length - 1} skipped (importAll() reads every one)`);
2184
+ }
2185
+ return [index];
2186
+ });
2187
+ return report;
2188
+ },
2189
+ /**
2190
+ * Read every graph of a CX document: one per subnetwork of a collection, in the order of
2191
+ * cyNetworkRelations; the root network for any other document.
2192
+ * @param input - the text, bytes or stream
2193
+ * @param sinkFor - the sink of the graph with this index, called before its first push
2194
+ * @param options - format-specific and common options
2195
+ * @returns one report per graph
2196
+ */
2197
+ importAll(input, sinkFor, options) {
2198
+ return run(input, options, sinkFor, (plans) => plans.map((p) => p.index));
2199
+ },
2200
+ /**
2201
+ * List the graphs of a CX document with their names and member counts.
2202
+ * @param input - the text, bytes or stream
2203
+ * @param options - format-specific and common options
2204
+ * @returns one listing per graph
2205
+ */
2206
+ async listGraphs(input, options) {
2207
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2208
+ const report = new ImportReportBuilder(CX_FORMAT, resolved.errorLimit);
2209
+ const doc = await readDocument(input, report, resolved);
2210
+ const nodes = aspect(doc, "nodes").length;
2211
+ const edges = aspect(doc, "edges").length;
2212
+ return graphPlans(doc).map((plan) => Object.freeze({
2213
+ index: plan.index,
2214
+ name: plan.name,
2215
+ nodes: memberCount(plan.nodes, nodes),
2216
+ edges: memberCount(plan.edges, edges),
2217
+ }));
2218
+ },
2219
+ });
2220
+ //# sourceMappingURL=importer.js.map