@graphty/graph-io 0.3.19 → 0.3.21

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 (201) hide show
  1. package/README.md +49 -4
  2. package/dist/chunks/{escape-CWExcecC.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-DQiKgQJc.js → export-Bh60Dv-n.js} +5 -79
  5. package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
  6. package/dist/chunks/{exporter-CbMZyVt-.js → exporter-BGrsWamJ.js} +7 -6
  7. package/dist/chunks/{exporter-CbMZyVt-.js.map → exporter-BGrsWamJ.js.map} +1 -1
  8. package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
  9. package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
  10. package/dist/chunks/{importer-BNFuV1-K.js → importer-BWY2FFc5.js} +28 -7
  11. package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
  12. package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
  13. package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
  14. package/dist/chunks/{importer-BAP4PrxR.js → importer-DET1tg6s.js} +30 -20
  15. package/dist/chunks/importer-DET1tg6s.js.map +1 -0
  16. package/dist/chunks/{importer-ByPGO-09.js → importer-DEcKhsmQ.js} +2 -2
  17. package/dist/chunks/{importer-ByPGO-09.js.map → importer-DEcKhsmQ.js.map} +1 -1
  18. package/dist/chunks/{importer-BW-Ft2ps.js → importer-DIrbbnAf.js} +6 -5
  19. package/dist/chunks/{importer-BW-Ft2ps.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-Bm1_5vPS.js → importer-eUREhBGj.js} +11 -8
  23. package/dist/chunks/importer-eUREhBGj.js.map +1 -0
  24. package/dist/chunks/{importer-DOepkgnG.js → importer-vi3bSdtw.js} +4 -4
  25. package/dist/chunks/{importer-DOepkgnG.js.map → importer-vi3bSdtw.js.map} +1 -1
  26. package/dist/chunks/{importer-BVGtU1NA.js → importer-zGLo8Dg_.js} +6 -5
  27. package/dist/chunks/{importer-BVGtU1NA.js.map → importer-zGLo8Dg_.js.map} +1 -1
  28. package/dist/chunks/{json-elements-CZY1wiZh.js → json-elements-DDS8N2Dd.js} +2 -2
  29. package/dist/chunks/{json-elements-CZY1wiZh.js.map → json-elements-DDS8N2Dd.js.map} +1 -1
  30. package/dist/chunks/{records-BzNicMsf.js → records-dbRkxwaq.js} +2 -2
  31. package/dist/chunks/{records-BzNicMsf.js.map → records-dbRkxwaq.js.map} +1 -1
  32. package/dist/chunks/{report-BcWboivV.js → report-B1z4WT9e.js} +143 -111
  33. package/dist/chunks/{report-BcWboivV.js.map → report-B1z4WT9e.js.map} +1 -1
  34. package/dist/chunks/{weights-CwISIpCP.js → weights-Dzba96G3.js} +5 -5
  35. package/dist/chunks/{weights-CwISIpCP.js.map → weights-Dzba96G3.js.map} +1 -1
  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 -5
  39. package/dist/csv.js.map +1 -1
  40. package/dist/cx.js +1 -1
  41. package/dist/cx2.js +2 -2
  42. package/dist/cys.d.ts +1 -0
  43. package/dist/cys.js +6 -0
  44. package/dist/cys.js.map +1 -0
  45. package/dist/dot.js +1 -1
  46. package/dist/gexf.js +5 -4
  47. package/dist/gexf.js.map +1 -1
  48. package/dist/gml.js +12 -14
  49. package/dist/gml.js.map +1 -1
  50. package/dist/graph-io.js +288 -141
  51. package/dist/graph-io.js.map +1 -1
  52. package/dist/graphml.js +1 -1
  53. package/dist/json.js +1 -1
  54. package/dist/neo4j.js +6 -5
  55. package/dist/neo4j.js.map +1 -1
  56. package/dist/obo.js +1 -1
  57. package/dist/pajek.js +1 -1
  58. package/dist/src/common/cell-budget.d.ts +84 -0
  59. package/dist/src/common/cell-budget.d.ts.map +1 -0
  60. package/dist/src/common/cell-budget.js +138 -0
  61. package/dist/src/common/cell-budget.js.map +1 -0
  62. package/dist/src/common/input.d.ts +10 -0
  63. package/dist/src/common/input.d.ts.map +1 -1
  64. package/dist/src/common/input.js +39 -0
  65. package/dist/src/common/input.js.map +1 -1
  66. package/dist/src/common/options.d.ts +6 -0
  67. package/dist/src/common/options.d.ts.map +1 -1
  68. package/dist/src/common/options.js +1 -1
  69. package/dist/src/common/options.js.map +1 -1
  70. package/dist/src/common/xml.d.ts +37 -3
  71. package/dist/src/common/xml.d.ts.map +1 -1
  72. package/dist/src/common/xml.js +86 -6
  73. package/dist/src/common/xml.js.map +1 -1
  74. package/dist/src/common/zip.d.ts +92 -0
  75. package/dist/src/common/zip.d.ts.map +1 -0
  76. package/dist/src/common/zip.js +399 -0
  77. package/dist/src/common/zip.js.map +1 -0
  78. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  79. package/dist/src/formats/cx/importer.js +26 -17
  80. package/dist/src/formats/cx/importer.js.map +1 -1
  81. package/dist/src/formats/cys/constants.d.ts +68 -0
  82. package/dist/src/formats/cys/constants.d.ts.map +1 -0
  83. package/dist/src/formats/cys/constants.js +76 -0
  84. package/dist/src/formats/cys/constants.js.map +1 -0
  85. package/dist/src/formats/cys/importer.d.ts +38 -0
  86. package/dist/src/formats/cys/importer.d.ts.map +1 -0
  87. package/dist/src/formats/cys/importer.js +830 -0
  88. package/dist/src/formats/cys/importer.js.map +1 -0
  89. package/dist/src/formats/cys/index.d.ts +7 -0
  90. package/dist/src/formats/cys/index.d.ts.map +1 -0
  91. package/dist/src/formats/cys/index.js +7 -0
  92. package/dist/src/formats/cys/index.js.map +1 -0
  93. package/dist/src/formats/cys/session.d.ts +132 -0
  94. package/dist/src/formats/cys/session.d.ts.map +1 -0
  95. package/dist/src/formats/cys/session.js +315 -0
  96. package/dist/src/formats/cys/session.js.map +1 -0
  97. package/dist/src/formats/cys/tables.d.ts +90 -0
  98. package/dist/src/formats/cys/tables.d.ts.map +1 -0
  99. package/dist/src/formats/cys/tables.js +293 -0
  100. package/dist/src/formats/cys/tables.js.map +1 -0
  101. package/dist/src/formats/dot/exporter.d.ts +2 -0
  102. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  103. package/dist/src/formats/dot/exporter.js +12 -0
  104. package/dist/src/formats/dot/exporter.js.map +1 -1
  105. package/dist/src/formats/dot/importer.js +6 -2
  106. package/dist/src/formats/dot/importer.js.map +1 -1
  107. package/dist/src/formats/gexf/importer.d.ts +2 -2
  108. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  109. package/dist/src/formats/gexf/importer.js +1 -1
  110. package/dist/src/formats/gexf/importer.js.map +1 -1
  111. package/dist/src/formats/gml/exporter.d.ts +0 -8
  112. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  113. package/dist/src/formats/gml/exporter.js +7 -17
  114. package/dist/src/formats/gml/exporter.js.map +1 -1
  115. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  116. package/dist/src/formats/json/exporter.js +3 -1
  117. package/dist/src/formats/json/exporter.js.map +1 -1
  118. package/dist/src/formats/json/importer.d.ts.map +1 -1
  119. package/dist/src/formats/json/importer.js +3 -2
  120. package/dist/src/formats/json/importer.js.map +1 -1
  121. package/dist/src/formats/xgmml/columns.d.ts +152 -0
  122. package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
  123. package/dist/src/formats/xgmml/columns.js +593 -0
  124. package/dist/src/formats/xgmml/columns.js.map +1 -0
  125. package/dist/src/formats/xgmml/constants.d.ts +196 -0
  126. package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
  127. package/dist/src/formats/xgmml/constants.js +197 -0
  128. package/dist/src/formats/xgmml/constants.js.map +1 -0
  129. package/dist/src/formats/xgmml/document.d.ts +361 -0
  130. package/dist/src/formats/xgmml/document.d.ts.map +1 -0
  131. package/dist/src/formats/xgmml/document.js +695 -0
  132. package/dist/src/formats/xgmml/document.js.map +1 -0
  133. package/dist/src/formats/xgmml/emit.d.ts +341 -0
  134. package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
  135. package/dist/src/formats/xgmml/emit.js +1275 -0
  136. package/dist/src/formats/xgmml/emit.js.map +1 -0
  137. package/dist/src/formats/xgmml/exporter.d.ts +24 -0
  138. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
  139. package/dist/src/formats/xgmml/exporter.js +999 -0
  140. package/dist/src/formats/xgmml/exporter.js.map +1 -0
  141. package/dist/src/formats/xgmml/importer.d.ts +58 -0
  142. package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
  143. package/dist/src/formats/xgmml/importer.js +360 -0
  144. package/dist/src/formats/xgmml/importer.js.map +1 -0
  145. package/dist/src/formats/xgmml/index.d.ts +8 -0
  146. package/dist/src/formats/xgmml/index.d.ts.map +1 -0
  147. package/dist/src/formats/xgmml/index.js +8 -0
  148. package/dist/src/formats/xgmml/index.js.map +1 -0
  149. package/dist/src/formats/xgmml/values.d.ts +60 -0
  150. package/dist/src/formats/xgmml/values.d.ts.map +1 -0
  151. package/dist/src/formats/xgmml/values.js +148 -0
  152. package/dist/src/formats/xgmml/values.js.map +1 -0
  153. package/dist/src/index.d.ts +2 -0
  154. package/dist/src/index.d.ts.map +1 -1
  155. package/dist/src/index.js +2 -0
  156. package/dist/src/index.js.map +1 -1
  157. package/dist/src/registry.d.ts +7 -0
  158. package/dist/src/registry.d.ts.map +1 -1
  159. package/dist/src/registry.js +25 -10
  160. package/dist/src/registry.js.map +1 -1
  161. package/dist/src/sniff.d.ts +1 -1
  162. package/dist/src/sniff.d.ts.map +1 -1
  163. package/dist/src/sniff.js +2 -0
  164. package/dist/src/sniff.js.map +1 -1
  165. package/dist/xgmml.d.ts +1 -0
  166. package/dist/xgmml.js +9 -0
  167. package/dist/xgmml.js.map +1 -0
  168. package/package.json +15 -2
  169. package/src/common/cell-budget.ts +170 -0
  170. package/src/common/input.ts +40 -0
  171. package/src/common/options.ts +1 -1
  172. package/src/common/xml.ts +127 -6
  173. package/src/common/zip.ts +472 -0
  174. package/src/formats/cx/importer.ts +27 -19
  175. package/src/formats/cys/constants.ts +99 -0
  176. package/src/formats/cys/importer.ts +1100 -0
  177. package/src/formats/cys/index.ts +7 -0
  178. package/src/formats/cys/session.ts +428 -0
  179. package/src/formats/cys/tables.ts +400 -0
  180. package/src/formats/dot/exporter.ts +17 -0
  181. package/src/formats/dot/importer.ts +6 -2
  182. package/src/formats/gexf/importer.ts +0 -1
  183. package/src/formats/gml/exporter.ts +7 -18
  184. package/src/formats/json/exporter.ts +3 -1
  185. package/src/formats/json/importer.ts +3 -2
  186. package/src/formats/xgmml/columns.ts +747 -0
  187. package/src/formats/xgmml/constants.ts +265 -0
  188. package/src/formats/xgmml/document.ts +975 -0
  189. package/src/formats/xgmml/emit.ts +1563 -0
  190. package/src/formats/xgmml/exporter.ts +1215 -0
  191. package/src/formats/xgmml/importer.ts +491 -0
  192. package/src/formats/xgmml/index.ts +8 -0
  193. package/src/formats/xgmml/values.ts +183 -0
  194. package/src/index.ts +9 -0
  195. package/src/registry.ts +41 -15
  196. package/src/sniff.ts +5 -1
  197. package/dist/chunks/escape-CWExcecC.js.map +0 -1
  198. package/dist/chunks/importer-BAP4PrxR.js.map +0 -1
  199. package/dist/chunks/importer-BNFuV1-K.js.map +0 -1
  200. package/dist/chunks/importer-Bm1_5vPS.js.map +0 -1
  201. package/dist/chunks/writer-DQiKgQJc.js.map +0 -1
@@ -0,0 +1,1275 @@
1
+ /**
2
+ * Resolving a parsed XGMML document (document.ts) into a sink: which nodes and edges a graph
3
+ * holds, their ids, endpoints, direction, weights, containment, positions, graphics and attribute
4
+ * columns. The `.cys` importer calls the same code with the session's tables and views merged into
5
+ * the records first, so both readers share every rule.
6
+ */
7
+ import { INVALID_INDEX } from "@graphty/graph-format";
8
+ import { declareResolved } from "../../common/attributes.js";
9
+ import { DirectionResolver } from "../../common/direction.js";
10
+ import { IdCoercer } from "../../common/ids.js";
11
+ import { throwIfAborted } from "../../common/input.js";
12
+ import { parseWeightText } from "../../common/weights.js";
13
+ import { ColumnSet } from "./columns.js";
14
+ import { CYTOSCAPE_ORIGIN_NAMESPACE, EDGE_ID_COLUMN, FORMAT, GRAPHICS_COLUMN, INTERACTION_COLUMN, LABEL_COLUMN, META_KEY, NESTED_NETWORK_COLUMN, NETWORK_POINTER_COLUMN, NETWORKS_COLUMN, PARENT_COLUMN, PARENTS_COLUMN, POSITION_COLUMN, SUBGRAPH_COLUMN, XGMML_ISSUE, XGMML_ORIGIN_NAMESPACE, Z_COLUMN, } from "./constants.js";
15
+ import { attJson, isCyTrue, } from "./document.js";
16
+ /** The graph atts that are document metadata, not columns. */
17
+ const META_ATTS = new Set(["documentVersion", "networkMetadata"]);
18
+ /** The root XML attributes that are structure, not graph columns. */
19
+ const GRAPH_STRUCTURE = new Set([
20
+ "id",
21
+ "label",
22
+ "directed",
23
+ "xlink:href",
24
+ "cy:documentVersion",
25
+ "cy:view",
26
+ "cy:registered",
27
+ "cy:networkId",
28
+ "cy:visualStyle",
29
+ "cy:rendererId",
30
+ ]);
31
+ /** Elements between two checks of the cancellation signal. */
32
+ const ABORT_CHECK_INTERVAL = 64;
33
+ /** Cytoscape's label alias shape: `source (interaction) target`. */
34
+ const ALIAS_SPLIT = /[()]/;
35
+ /**
36
+ * The dialect of a document (`research-xgmml.md` section 2).
37
+ * @param doc - the document
38
+ * @param report - the report the unparseable-version warning goes to
39
+ * @returns the dialect
40
+ */
41
+ export function dialectOf(doc, report) {
42
+ const { root } = doc;
43
+ let versionText = root.attrs.get("cy:documentVersion") ?? null;
44
+ for (const att of root.atts) {
45
+ if (att.name === "documentVersion" && att.value !== null) {
46
+ versionText ?? (versionText = att.value);
47
+ }
48
+ }
49
+ let version = 0;
50
+ if (versionText !== null) {
51
+ version = Number(versionText.trim());
52
+ if (!Number.isFinite(version) || versionText.trim().length === 0) {
53
+ report.warning("validation-error", XGMML_ISSUE.DOCUMENT_VERSION, `documentVersion "${versionText}" does not parse; the dialect is chosen from the content`, { line: root.line });
54
+ version = 0;
55
+ }
56
+ }
57
+ const registered = root.subgraphs.some((g) => isCyTrue(g.registered));
58
+ return {
59
+ versionText,
60
+ version,
61
+ cytoscape: doc.cytoscape,
62
+ cy2: doc.cytoscape && version < 3,
63
+ session: root.registered !== null && !isCyTrue(root.registered) && registered,
64
+ view: isCyTrue(root.attrs.get("cy:view")),
65
+ };
66
+ }
67
+ /**
68
+ * The graphs a document offers: the registered subnetworks of a session network document, else
69
+ * the root.
70
+ * @param doc - the document
71
+ * @param dialect - its dialect
72
+ * @returns the graphs, in document order
73
+ */
74
+ export function graphsOf(doc, dialect) {
75
+ return dialect.session ? doc.root.subgraphs.filter((g) => isCyTrue(g.registered)) : [doc.root];
76
+ }
77
+ /**
78
+ * The id a `#id` reference names, or null for a cross-file or malformed reference.
79
+ * @param href - the xlink:href text
80
+ * @returns the id
81
+ */
82
+ function localRef(href) {
83
+ return href.startsWith("#") ? href.slice(1) : null;
84
+ }
85
+ /**
86
+ * The nodes and edges a graph holds: its direct members, references resolved.
87
+ * @param doc - the document
88
+ * @param graph - the graph (a registered subnetwork), or null for the whole document
89
+ * @returns the declared node and edge records it holds, references resolved, in order
90
+ */
91
+ export function membersOf(doc, graph) {
92
+ if (graph === null) {
93
+ return { nodes: doc.nodes, edges: doc.edges, dangling: 0 };
94
+ }
95
+ const nodeById = indexById(doc.nodes);
96
+ const edgeById = indexById(doc.edges);
97
+ const nodes = [];
98
+ const edges = [];
99
+ let dangling = 0;
100
+ for (const member of graph.members) {
101
+ if (member.href === null) {
102
+ if (member.kind === "node") {
103
+ nodes.push(member);
104
+ }
105
+ else {
106
+ edges.push(member);
107
+ }
108
+ continue;
109
+ }
110
+ const id = localRef(member.href);
111
+ let target;
112
+ if (id !== null) {
113
+ target = member.kind === "node" ? nodeById.get(id) : edgeById.get(id);
114
+ }
115
+ if (target === undefined) {
116
+ dangling++;
117
+ }
118
+ else if (target.kind === "node") {
119
+ nodes.push(target);
120
+ }
121
+ else {
122
+ edges.push(target);
123
+ }
124
+ }
125
+ return { nodes, edges, dangling };
126
+ }
127
+ /**
128
+ * The first declaration of each id.
129
+ * @param records - node or edge records
130
+ * @returns id to record
131
+ */
132
+ function indexById(records) {
133
+ const out = new Map();
134
+ for (const record of records) {
135
+ if (record.id !== null && !out.has(record.id)) {
136
+ out.set(record.id, record);
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+ /**
142
+ * Pushes one graph of a document into a sink.
143
+ */
144
+ export class XgmmlEmitter {
145
+ /**
146
+ * Create an emitter for one graph.
147
+ * @param doc - the document
148
+ * @param dialect - its dialect
149
+ * @param sink - the sink
150
+ * @param report - the report
151
+ * @param options - the resolved common options
152
+ * @param settings - the XGMML options
153
+ * @param extras - what a session adds
154
+ */
155
+ constructor(doc, dialect, sink, report, options, settings, extras = {}) {
156
+ this.rows = new Map();
157
+ this.rowList = [];
158
+ this.edgeIndex = [];
159
+ this.sinceCheck = 0;
160
+ this.aliasResolved = 0;
161
+ this.aliasInteractions = 0;
162
+ this.weighted = false;
163
+ /** The direction of each edge, decided once (its warning is recorded once). */
164
+ this.directions = new Map();
165
+ /** Node labels to rows, for label aliases; null marks a label two nodes share. */
166
+ this.labelRows = null;
167
+ this.doc = doc;
168
+ this.dialect = dialect;
169
+ this.sink = sink;
170
+ this.report = report;
171
+ this.options = options;
172
+ this.settings = settings;
173
+ this.extras = extras;
174
+ this.coercer = new IdCoercer(options.ids);
175
+ this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
176
+ const base = { unescape: settings.cytoscapeEscapes, long: options.long };
177
+ this.nodeColumns = new ColumnSet("node", report, {
178
+ ...base,
179
+ reserved: new Set([
180
+ LABEL_COLUMN,
181
+ POSITION_COLUMN,
182
+ Z_COLUMN,
183
+ GRAPHICS_COLUMN,
184
+ PARENT_COLUMN,
185
+ PARENTS_COLUMN,
186
+ SUBGRAPH_COLUMN,
187
+ NESTED_NETWORK_COLUMN,
188
+ NETWORK_POINTER_COLUMN,
189
+ NETWORKS_COLUMN,
190
+ ]),
191
+ });
192
+ this.edgeColumns = new ColumnSet("edge", report, {
193
+ ...base,
194
+ reserved: new Set([EDGE_ID_COLUMN, LABEL_COLUMN, GRAPHICS_COLUMN]),
195
+ });
196
+ this.graphColumns = new ColumnSet("graph", report, { ...base, reserved: new Set([GRAPHICS_COLUMN]) });
197
+ }
198
+ /**
199
+ * Push the graph: nodes, containment, positions, edges, columns, metadata.
200
+ * @param graph - the subnetwork, or null for the whole document
201
+ */
202
+ emit(graph) {
203
+ const members = membersOf(this.doc, graph);
204
+ if (members.dangling > 0) {
205
+ this.report.warning("validation-error", XGMML_ISSUE.DANGLING_REFERENCE, `${this.prefix()}${members.dangling} xlink:href member reference(s) name no node or edge of the document`, { line: graph?.line ?? null });
206
+ }
207
+ const header = this.headerDirected();
208
+ this.direction.setHeader(this.sinkDirection(members.edges, header), { line: this.doc.root.line });
209
+ this.addNodes(members.nodes);
210
+ if (graph === null) {
211
+ this.checkReferences();
212
+ }
213
+ const containment = new Containment(this);
214
+ containment.resolve(this.rowList, this.groupTest());
215
+ this.writeNodeExtras(graph);
216
+ this.addEdges(members.edges, header);
217
+ this.finishAliases();
218
+ this.writeGraph(graph);
219
+ const nodeIndex = (row) => this.rowList[row].index;
220
+ this.nodeColumns.write(this.sink, nodeIndex);
221
+ this.edgeColumns.write(this.sink, (row) => this.edgeIndex[row] ?? -1);
222
+ containment.write();
223
+ this.writeMeta(graph);
224
+ throwIfAborted(this.options.signal);
225
+ }
226
+ // ------------------------------------------------------------------ shared helpers (also used by Containment)
227
+ /**
228
+ * The sink.
229
+ * @returns the sink
230
+ */
231
+ get target() {
232
+ return this.sink;
233
+ }
234
+ /**
235
+ * The report.
236
+ * @returns the report
237
+ */
238
+ get issues() {
239
+ return this.report;
240
+ }
241
+ /**
242
+ * The row of a node id in this graph.
243
+ * @param id - the id text
244
+ * @returns the row, or undefined
245
+ */
246
+ rowOf(id) {
247
+ return this.rows.get(id);
248
+ }
249
+ /**
250
+ * The network name a cross-file pointer names, when a session can tell.
251
+ * @param href - the pointer
252
+ * @returns the name, or null
253
+ */
254
+ resolvePointer(href) {
255
+ return this.extras.resolvePointer?.(href) ?? null;
256
+ }
257
+ /**
258
+ * Hand a group member the graph does not hold to the session, when one is reading.
259
+ * @param group - the group node's id
260
+ * @param member - the member's id
261
+ * @returns true when the session took it
262
+ */
263
+ missingMember(group, member) {
264
+ if (this.extras.onMissingMember === undefined) {
265
+ return false;
266
+ }
267
+ this.extras.onMissingMember(group, member);
268
+ return true;
269
+ }
270
+ /**
271
+ * The first graph of the document with an id.
272
+ * @param id - the id
273
+ * @returns the graph, or undefined
274
+ */
275
+ graphById(id) {
276
+ return this.doc.graphs.find((g) => g.id === id);
277
+ }
278
+ /**
279
+ * The prefix of every message (the session entry), or "".
280
+ * @returns the prefix
281
+ */
282
+ prefix() {
283
+ return this.extras.entry === undefined ? "" : `${this.extras.entry}: `;
284
+ }
285
+ /**
286
+ * Check the cancellation signal every few dozen elements.
287
+ */
288
+ checkAbort() {
289
+ if (++this.sinceCheck >= ABORT_CHECK_INTERVAL) {
290
+ this.sinceCheck = 0;
291
+ throwIfAborted(this.options.signal);
292
+ }
293
+ }
294
+ /**
295
+ * Declare a column the importer owns on first use (the 5.6 rename rule applies).
296
+ * @param domain - node or edge
297
+ * @param decl - the declaration
298
+ * @returns the handle, or null when the sink refused it (recorded)
299
+ */
300
+ declare(domain, decl) {
301
+ try {
302
+ return declareResolved(this.sink, domain, decl, this.report, { element: decl.name }).handle;
303
+ }
304
+ catch (err) {
305
+ this.report.recordError(err, { element: decl.name });
306
+ return null;
307
+ }
308
+ }
309
+ // ------------------------------------------------------------------ nodes
310
+ /**
311
+ * The header direction: the root's `directed` (0 by the DTD), else the defaultDirected option.
312
+ * @returns whether the graph is directed by default
313
+ */
314
+ headerDirected() {
315
+ const text = this.doc.root.attrs.get("directed");
316
+ if (text === undefined) {
317
+ return this.options.defaultDirected;
318
+ }
319
+ return this.flag(text, this.options.defaultDirected, { line: this.doc.root.line, element: "directed" });
320
+ }
321
+ /**
322
+ * The direction of one edge: its cy:directed, else the header.
323
+ * @param record - the edge
324
+ * @param header - the header direction
325
+ * @returns whether the edge is directed
326
+ */
327
+ edgeDirection(record, header) {
328
+ let directed = this.directions.get(record);
329
+ if (directed === undefined) {
330
+ directed =
331
+ record.directed === null
332
+ ? header
333
+ : this.flag(record.directed, header, { line: record.line, element: record.id ?? record.label });
334
+ this.directions.set(record, directed);
335
+ }
336
+ return directed;
337
+ }
338
+ /**
339
+ * The direction the sink starts with: the one every edge has when they all agree (a
340
+ * Cytoscape file writes cy:directed on every edge and may omit the root attribute), else the
341
+ * header, so a graph of one direction is never expanded into a mixed one.
342
+ * @param edges - the edges of the graph
343
+ * @param header - the header direction
344
+ * @returns the sink direction
345
+ */
346
+ sinkDirection(edges, header) {
347
+ let first = null;
348
+ for (const edge of edges) {
349
+ const directed = this.edgeDirection(edge, header);
350
+ if (first === null) {
351
+ first = directed;
352
+ }
353
+ else if (first !== directed) {
354
+ return header;
355
+ }
356
+ }
357
+ return first ?? header;
358
+ }
359
+ /**
360
+ * A 0 / 1 direction flag: true, false, yes and no are accepted with a warning; anything else
361
+ * falls back.
362
+ * @param text - the text
363
+ * @param fallback - the direction for an unreadable value
364
+ * @param where - the location
365
+ * @returns the direction
366
+ */
367
+ flag(text, fallback, where) {
368
+ const t = text.trim();
369
+ if (t === "1" || t === "0") {
370
+ return t === "1";
371
+ }
372
+ const lower = t.toLowerCase();
373
+ const readable = lower === "true" || lower === "yes" || lower === "false" || lower === "no";
374
+ this.report.warnOnce("validation-error", XGMML_ISSUE.BAD_DIRECTED, readable
375
+ ? `${this.prefix()}directed="${text}" is not 0 or 1; read as ${lower === "true" || lower === "yes" ? "1" : "0"}`
376
+ : `${this.prefix()}directed="${text}" is not 0 or 1; read as ${fallback ? "directed" : "undirected"}`, where, `${XGMML_ISSUE.BAD_DIRECTED}:${text}`);
377
+ return readable ? lower === "true" || lower === "yes" : fallback;
378
+ }
379
+ /**
380
+ * Add every node record to the sink (one node per id; later declarations merge).
381
+ * @param records - the node records
382
+ */
383
+ addNodes(records) {
384
+ for (const record of records) {
385
+ const { id } = record;
386
+ if (id === null) {
387
+ continue;
388
+ }
389
+ const existing = this.rows.get(id);
390
+ if (existing !== undefined) {
391
+ existing.records.push(record);
392
+ this.report.warning("validation-error", XGMML_ISSUE.DUPLICATE_NODE, `${this.prefix()}node "${id}" is declared twice; the declarations are merged`, { line: record.line, element: id });
393
+ continue;
394
+ }
395
+ // -1 (not INVALID_INDEX, which is positive) marks a node the sink refused
396
+ const row = { id, records: [record], index: -1, row: this.rowList.length };
397
+ try {
398
+ row.index = this.sink.addNode(this.nodeId(id, record.line));
399
+ this.report.counts.nodes++;
400
+ }
401
+ catch (err) {
402
+ this.report.counts.skippedNodes++;
403
+ this.report.recordError(err, { line: record.line, element: id });
404
+ }
405
+ this.rows.set(id, row);
406
+ this.rowList.push(row);
407
+ this.checkAbort();
408
+ }
409
+ }
410
+ /**
411
+ * The coerced id of a node text.
412
+ * @param text - the id text
413
+ * @param line - the line
414
+ * @returns the id
415
+ */
416
+ nodeId(text, line) {
417
+ const id = this.coercer.text(text);
418
+ const merge = this.coercer.lastMerge;
419
+ if (merge !== null) {
420
+ this.report.warning("coercion", XGMML_ISSUE.ID_MERGED, `id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`, { line, element: text });
421
+ }
422
+ return id;
423
+ }
424
+ /**
425
+ * Whether a node's nested graph is a group (containment) rather than a nested-network
426
+ * pointer: any graph in the 1.0 draft dialect, a 2.x node with `__groupState`, a 3.x node with
427
+ * `__isGroup` true, or a node the session lists.
428
+ * @returns the test
429
+ */
430
+ groupTest() {
431
+ const { groupNodes } = this.extras;
432
+ const { cytoscape } = this.dialect;
433
+ return (node) => {
434
+ if (!cytoscape) {
435
+ return true;
436
+ }
437
+ if (node.id !== null && groupNodes?.has(node.id) === true) {
438
+ return true;
439
+ }
440
+ return node.atts.some((a) => a.name === "__groupState" || (a.name === "__isGroup" && isCyTrue(a.value)));
441
+ };
442
+ }
443
+ /**
444
+ * W_DANGLING_REFERENCE for the `xlink:href` members of a generic document's non-group graphs
445
+ * that name no node or edge (a group's are E_UNKNOWN_PARENT, reported by the containment).
446
+ */
447
+ checkReferences() {
448
+ const isGroup = this.groupTest();
449
+ const edgeIds = new Set(this.doc.edges.map((e) => e.id));
450
+ let dangling = 0;
451
+ for (const graph of this.doc.graphs) {
452
+ if (graph.owner !== null && isGroup(graph.owner)) {
453
+ continue;
454
+ }
455
+ for (const member of graph.members) {
456
+ if (member.href === null) {
457
+ continue;
458
+ }
459
+ const id = localRef(member.href);
460
+ const known = id !== null && (member.kind === "node" ? this.rows.has(id) : edgeIds.has(id));
461
+ if (!known) {
462
+ dangling++;
463
+ }
464
+ }
465
+ }
466
+ if (dangling > 0) {
467
+ this.report.warning("validation-error", XGMML_ISSUE.DANGLING_REFERENCE, `${this.prefix()}${dangling} xlink:href reference(s) name no node or edge of the document`);
468
+ }
469
+ }
470
+ /**
471
+ * Write the per-node values: label, attributes, XML attributes, graphics, positions, nested
472
+ * network pointers and subgraph memberships.
473
+ * @param graph - the subnetwork, or null
474
+ */
475
+ writeNodeExtras(graph) {
476
+ var _a;
477
+ const labels = [];
478
+ const positions = new Positions(this, this.dialect.cytoscape ? this.settings.zAs : "position");
479
+ const graphics = new JsonColumn(this, "node", GRAPHICS_COLUMN, XGMML_ORIGIN_NAMESPACE, "graphics");
480
+ const pointers = new Pointers(this, this.groupTest());
481
+ for (const row of this.rowList) {
482
+ const where = { line: row.records[0].line, element: row.id };
483
+ const seen = new Set();
484
+ for (const record of row.records) {
485
+ labels[_a = row.row] ?? (labels[_a] = record.label ?? undefined);
486
+ this.addElementColumns(this.nodeColumns, row.row, record, seen, where);
487
+ positions.add(row, record);
488
+ if (record.graphics !== null && Object.keys(record.graphics).length > 0) {
489
+ graphics.set(row.index, record.graphics);
490
+ }
491
+ pointers.add(row, record);
492
+ }
493
+ this.checkAbort();
494
+ }
495
+ this.writeLabels("node", labels, (row) => this.rowList[row].index);
496
+ positions.write();
497
+ pointers.write();
498
+ if (graph === null) {
499
+ this.writeNetworks();
500
+ }
501
+ }
502
+ /**
503
+ * Add an element's XML attributes (text grammar) and atts to a column set; atts of a later
504
+ * duplicate declaration only fill names the earlier ones left unset.
505
+ * @param columns - the column set
506
+ * @param row - the row
507
+ * @param record - the node or edge
508
+ * @param seen - the names set by earlier declarations of the same node
509
+ * @param where - the location
510
+ */
511
+ addElementColumns(columns, row, record, seen, where) {
512
+ const merging = seen.size > 0;
513
+ const names = new Set();
514
+ if (record.name !== null) {
515
+ if (!(merging && seen.has("name"))) {
516
+ columns.addText(row, "name", record.name, where);
517
+ }
518
+ names.add("name");
519
+ }
520
+ for (const [key, value] of record.xmlAttrs) {
521
+ if (!(merging && seen.has(key))) {
522
+ columns.addText(row, key, value, where);
523
+ }
524
+ names.add(key);
525
+ }
526
+ for (const att of record.atts) {
527
+ if (this.skipAtt(record, att) || (merging && att.name !== null && seen.has(att.name))) {
528
+ continue;
529
+ }
530
+ if (record.kind === "node" && att.name === "__isGroup" && record.nested.length > 0 && isCyTrue(att.value)) {
531
+ // the group is the containment itself (parent / parents and xgmml.subgraph)
532
+ continue;
533
+ }
534
+ if (att.hasGraph && att.name === null) {
535
+ continue;
536
+ }
537
+ columns.addAtt(row, att, where);
538
+ if (att.name !== null) {
539
+ names.add(att.name);
540
+ }
541
+ }
542
+ for (const name of names) {
543
+ seen.add(name);
544
+ }
545
+ }
546
+ /**
547
+ * Whether an att is not a column: an edge's weight att under weightFrom.
548
+ * @param record - the element
549
+ * @param att - the att
550
+ * @returns true to skip it
551
+ */
552
+ skipAtt(record, att) {
553
+ return record.kind === "edge" && record.weight === null && att.name === this.options.weightFrom;
554
+ }
555
+ /**
556
+ * Write a label column (role label).
557
+ * @param domain - node or edge
558
+ * @param labels - the label of each row
559
+ * @param index - the sink index of a row
560
+ */
561
+ writeLabels(domain, labels, index) {
562
+ if (!labels.some((l) => l !== undefined)) {
563
+ return;
564
+ }
565
+ const handle = this.declare(domain, {
566
+ name: LABEL_COLUMN,
567
+ dtype: "string",
568
+ role: "label",
569
+ nullable: true,
570
+ origin: { format: FORMAT, id: null, title: null, type: "label", namespace: null },
571
+ });
572
+ if (handle === null) {
573
+ return;
574
+ }
575
+ labels.forEach((label, row) => {
576
+ const i = index(row);
577
+ if (label !== undefined && i >= 0) {
578
+ if (domain === "node") {
579
+ this.sink.setNodeValue(handle, i, label);
580
+ }
581
+ else {
582
+ this.sink.setEdgeValue(handle, i, label);
583
+ }
584
+ }
585
+ });
586
+ }
587
+ /** The `xgmml.networks` membership of nodes in root-level subgraphs of a generic document. */
588
+ writeNetworks() {
589
+ const { subgraphs } = this.doc.root;
590
+ if (subgraphs.length === 0) {
591
+ return;
592
+ }
593
+ const member = new Map();
594
+ for (const graph of subgraphs) {
595
+ const name = graph.id ?? graph.label ?? "";
596
+ for (const m of graph.members) {
597
+ const id = m.kind === "node" ? (m.id ?? (m.href === null ? null : localRef(m.href))) : null;
598
+ const row = id === null ? undefined : this.rows.get(id);
599
+ if (row !== undefined && row.index >= 0) {
600
+ const list = member.get(row.index) ?? [];
601
+ list.push(name);
602
+ member.set(row.index, list);
603
+ }
604
+ }
605
+ }
606
+ if (member.size === 0) {
607
+ return;
608
+ }
609
+ const handle = this.declare("node", {
610
+ name: NETWORKS_COLUMN,
611
+ dtype: "list",
612
+ itemDtype: "string",
613
+ nullable: true,
614
+ origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: XGMML_ORIGIN_NAMESPACE },
615
+ });
616
+ if (handle !== null) {
617
+ for (const [index, list] of member) {
618
+ this.sink.setNodeValue(handle, index, list);
619
+ }
620
+ }
621
+ }
622
+ // ------------------------------------------------------------------ edges
623
+ /**
624
+ * Push every edge record.
625
+ * @param records - the edge records
626
+ * @param header - the header direction
627
+ */
628
+ addEdges(records, header) {
629
+ const ids = new Set();
630
+ const dedupe = this.dialect.cy2 && this.doc.nodes.some((n) => n.nested.length > 0) ? new Set() : null;
631
+ const labels = [];
632
+ const edgeIds = [];
633
+ const graphics = [];
634
+ let row = 0;
635
+ for (const record of records) {
636
+ const where = { line: record.line, element: record.id ?? record.label };
637
+ const key = record.id ?? record.label;
638
+ if (dedupe !== null && key !== null) {
639
+ if (dedupe.has(key)) {
640
+ this.report.warnOnce("merged", XGMML_ISSUE.GROUP_DUPLICATE_EDGE, `${this.prefix()}the Cytoscape 2.x writer repeats the edges of a group; each repeat is dropped`, where);
641
+ continue;
642
+ }
643
+ dedupe.add(key);
644
+ }
645
+ const index = this.addEdge(record, header, ids, where);
646
+ this.checkAbort();
647
+ if (index < 0) {
648
+ continue;
649
+ }
650
+ this.edgeIndex[row] = index;
651
+ labels[row] = record.label ?? undefined;
652
+ edgeIds[row] = record.id ?? undefined;
653
+ this.addElementColumns(this.edgeColumns, row, record, new Set(), where);
654
+ this.fillInteraction(record, row, where);
655
+ if (record.graphics !== null && Object.keys(record.graphics).length > 0) {
656
+ graphics.push([index, record.graphics]);
657
+ }
658
+ row++;
659
+ }
660
+ this.writeEdgeIds(edgeIds);
661
+ this.writeLabels("edge", labels, (r) => this.edgeIndex[r] ?? -1);
662
+ const column = new JsonColumn(this, "edge", GRAPHICS_COLUMN, XGMML_ORIGIN_NAMESPACE, "graphics");
663
+ for (const [index, value] of graphics) {
664
+ column.set(index, value);
665
+ }
666
+ }
667
+ /**
668
+ * Push one edge: endpoints (with label aliases), duplicate id check, direction and weight.
669
+ * @param record - the edge
670
+ * @param header - the header direction
671
+ * @param ids - the edge ids used so far
672
+ * @param where - the location
673
+ * @returns the edge index, or -1 when it was skipped
674
+ */
675
+ addEdge(record, header, ids, where) {
676
+ const alias = this.settings.labelAliases ? aliasesOf(record.label) : null;
677
+ let source;
678
+ let target;
679
+ try {
680
+ source = this.endpoint(record.source, alias?.[0] ?? null, "source", where);
681
+ target = this.endpoint(record.target, alias?.[2] ?? null, "target", where);
682
+ }
683
+ catch (err) {
684
+ // an endpoint the id rule or the sink refuses (ids: "number", a caller's sink)
685
+ this.report.counts.skippedEdges++;
686
+ this.report.recordError(err, where);
687
+ return -1;
688
+ }
689
+ if (source === null || target === null) {
690
+ this.report.counts.skippedEdges++;
691
+ return -1;
692
+ }
693
+ if (record.id !== null && ids.has(record.id)) {
694
+ this.report.counts.skippedEdges++;
695
+ this.report.error("validation-error", XGMML_ISSUE.DUPLICATE_EDGE_ID, `${this.prefix()}edge id "${record.id}" is used twice; the second edge is skipped`, where);
696
+ return -1;
697
+ }
698
+ const directed = this.edgeDirection(record, header);
699
+ const kind = directed ? "directed" : "undirected";
700
+ let weight;
701
+ try {
702
+ weight = this.weightOf(record);
703
+ const before = this.sink.edgeCount;
704
+ const e = this.direction.addEdge(source, target, kind, weight, where);
705
+ this.report.counts.edges += this.sink.edgeCount - before;
706
+ if (record.id !== null) {
707
+ ids.add(record.id);
708
+ }
709
+ return e;
710
+ }
711
+ catch (err) {
712
+ this.report.counts.skippedEdges++;
713
+ this.report.recordError(err, where);
714
+ return -1;
715
+ }
716
+ }
717
+ /**
718
+ * The weight of an edge: its `weight` XML attribute (or the att weightFrom names).
719
+ * @param record - the edge
720
+ * @returns the weight, or undefined
721
+ */
722
+ weightOf(record) {
723
+ const from = this.options.weightFrom;
724
+ if (from === null) {
725
+ return undefined;
726
+ }
727
+ let text = from === "weight" ? record.weight : null;
728
+ if (text === null) {
729
+ text = record.atts.find((a) => a.name === from && a.children.length === 0)?.value ?? null;
730
+ }
731
+ if (text === null) {
732
+ return undefined;
733
+ }
734
+ const weight = parseWeightText(text);
735
+ this.weighted || (this.weighted = weight !== undefined);
736
+ return weight;
737
+ }
738
+ /**
739
+ * Resolve one endpoint: a node of this graph by id, else (under labelAliases) the label alias
740
+ * by id or by unique node label; a missing endpoint is E_MISSING_ENDPOINT, an unknown one
741
+ * E_UNKNOWN_NODE unless addMissingNodes adds it.
742
+ * @param text - the source or target text, or null
743
+ * @param alias - the label alias, or null
744
+ * @param side - which end
745
+ * @param where - the location
746
+ * @returns the node id, or null when the edge cannot be added
747
+ */
748
+ endpoint(text, alias, side, where) {
749
+ if (text !== null) {
750
+ const row = this.rows.get(text);
751
+ if (row !== undefined && row.index >= 0) {
752
+ return this.coercer.text(text);
753
+ }
754
+ }
755
+ if (alias !== null) {
756
+ const resolved = this.aliasRow(alias);
757
+ if (resolved !== null) {
758
+ this.aliasResolved++;
759
+ return this.coercer.text(resolved.id);
760
+ }
761
+ }
762
+ if (text === null) {
763
+ this.report.error("missing-value", XGMML_ISSUE.MISSING_ENDPOINT, `${this.prefix()}<edge> without a ${side}`, where);
764
+ return null;
765
+ }
766
+ if (this.options.addMissingNodes) {
767
+ const id = this.coercer.text(text);
768
+ if (this.sink.indexOf(id) === INVALID_INDEX) {
769
+ this.sink.addNode(id);
770
+ this.report.counts.nodes++;
771
+ }
772
+ return id;
773
+ }
774
+ this.report.error("validation-error", XGMML_ISSUE.UNKNOWN_NODE, `${this.prefix()}edge ${side} "${text}" is not a node of this graph; the edge is skipped`, where);
775
+ return null;
776
+ }
777
+ /**
778
+ * The node a label alias names: by id, else by a label only one node has.
779
+ * @param alias - the alias
780
+ * @returns the row, or null
781
+ */
782
+ aliasRow(alias) {
783
+ const byId = this.rows.get(alias);
784
+ if (byId !== undefined && byId.index >= 0) {
785
+ return byId;
786
+ }
787
+ if (this.labelRows === null) {
788
+ this.labelRows = new Map();
789
+ for (const row of this.rowList) {
790
+ const { label } = row.records[0];
791
+ if (label !== null && row.index >= 0) {
792
+ this.labelRows.set(label, this.labelRows.has(label) ? null : row);
793
+ }
794
+ }
795
+ }
796
+ return this.labelRows.get(alias) ?? null;
797
+ }
798
+ /**
799
+ * Fill the interaction of an edge without one from its label alias, as Cytoscape does.
800
+ * @param record - the edge
801
+ * @param row - the edge row
802
+ * @param where - the location
803
+ */
804
+ fillInteraction(record, row, where) {
805
+ if (!this.settings.labelAliases ||
806
+ this.dialect.session ||
807
+ record.atts.some((a) => a.name === INTERACTION_COLUMN)) {
808
+ return;
809
+ }
810
+ const alias = aliasesOf(record.label);
811
+ if (alias !== null) {
812
+ this.edgeColumns.addText(row, INTERACTION_COLUMN, alias[1], where);
813
+ this.aliasInteractions++;
814
+ }
815
+ }
816
+ /** Report the label alias uses, once. */
817
+ finishAliases() {
818
+ if (this.aliasResolved === 0 && this.aliasInteractions === 0) {
819
+ return;
820
+ }
821
+ this.report.warning("coercion", XGMML_ISSUE.LABEL_ALIAS, `${this.prefix()}Cytoscape label aliases ("a (pp) b"): ${this.aliasResolved} endpoint(s) resolved, ${this.aliasInteractions} interaction(s) filled from edge labels`);
822
+ }
823
+ /**
824
+ * Write the edge `id` column (role id, unique).
825
+ * @param ids - the id of each row
826
+ */
827
+ writeEdgeIds(ids) {
828
+ if (!ids.some((id) => id !== undefined)) {
829
+ return;
830
+ }
831
+ const handle = this.declare("edge", {
832
+ name: EDGE_ID_COLUMN,
833
+ dtype: "string",
834
+ role: "id",
835
+ unique: true,
836
+ nullable: true,
837
+ origin: { format: FORMAT, id: null, title: null, type: "id", namespace: null },
838
+ });
839
+ if (handle === null) {
840
+ return;
841
+ }
842
+ ids.forEach((id, row) => {
843
+ const e = this.edgeIndex[row];
844
+ if (id !== undefined && e !== undefined) {
845
+ try {
846
+ this.sink.setEdgeValue(handle, e, id);
847
+ }
848
+ catch (err) {
849
+ this.report.recordError(err, { element: id });
850
+ }
851
+ }
852
+ });
853
+ }
854
+ // ------------------------------------------------------------------ graph and metadata
855
+ /**
856
+ * The graph table: the graph's atts and other XML attributes, and its graphics.
857
+ * @param graph - the subnetwork, or null for the root
858
+ */
859
+ writeGraph(graph) {
860
+ const g = graph ?? this.doc.root;
861
+ const where = { line: g.line, element: g.id };
862
+ if (graph === null) {
863
+ for (const [key, value] of g.attrs) {
864
+ if (!GRAPH_STRUCTURE.has(key) && key !== "xmlns" && !key.startsWith("xmlns:")) {
865
+ this.graphColumns.addText(0, key, value, where);
866
+ }
867
+ }
868
+ }
869
+ for (const att of g.atts) {
870
+ if (att.name !== null && META_ATTS.has(att.name)) {
871
+ continue;
872
+ }
873
+ if (att.hasGraph && att.name === null) {
874
+ continue;
875
+ }
876
+ this.graphColumns.addAtt(0, att, where);
877
+ }
878
+ this.graphColumns.write(this.sink, () => 0);
879
+ const json = { ...(this.extras.graphJson ?? {}) };
880
+ if (g.graphics !== null && Object.keys(g.graphics).length > 0) {
881
+ json[GRAPHICS_COLUMN] = g.graphics;
882
+ }
883
+ for (const [name, value] of Object.entries(json)) {
884
+ this.sink.setGraphValue(name, value, {
885
+ name,
886
+ dtype: "json",
887
+ nullable: true,
888
+ origin: { format: FORMAT, id: null, title: null, type: name, namespace: XGMML_ORIGIN_NAMESPACE },
889
+ });
890
+ }
891
+ }
892
+ /**
893
+ * The graph metadata: name, description and date from the RDF metadata, the format, version
894
+ * and what the exporter needs.
895
+ * @param graph - the subnetwork, or null
896
+ */
897
+ writeMeta(graph) {
898
+ const g = graph ?? this.doc.root;
899
+ const { rdf } = this.doc;
900
+ const name = this.extras.graphName ?? g.label ?? rdf.title ?? g.id ?? undefined;
901
+ this.sink.setMeta({
902
+ name: name ?? undefined,
903
+ description: rdf.description ?? undefined,
904
+ created: rdf.date ?? undefined,
905
+ sourceFormat: this.extras.sourceFormat ?? FORMAT,
906
+ sourceVersion: this.dialect.versionText ?? undefined,
907
+ weightOrigin: this.weighted
908
+ ? { format: FORMAT, id: "weight", title: null, type: "weight", namespace: null }
909
+ : undefined,
910
+ extra: {
911
+ [META_KEY]: {
912
+ graphId: g.id,
913
+ directed: this.doc.root.attrs.get("directed") ?? null,
914
+ rdf: Object.keys(rdf).length > 0 ? { ...rdf } : null,
915
+ },
916
+ ...(this.extras.metaExtra ?? {}),
917
+ },
918
+ });
919
+ }
920
+ }
921
+ /**
922
+ * The three parts of a Cytoscape edge label (`source (interaction) target`).
923
+ * @param label - the label, or null
924
+ * @returns source alias, interaction, target alias; null when the label has another shape
925
+ */
926
+ export function aliasesOf(label) {
927
+ if (label === null) {
928
+ return null;
929
+ }
930
+ const parts = label.split(ALIAS_SPLIT);
931
+ return parts.length === 3 ? [parts[0].trim(), parts[1], parts[2].trim()] : null;
932
+ }
933
+ /**
934
+ * A json column the importer owns, declared on first value.
935
+ */
936
+ class JsonColumn {
937
+ /**
938
+ * Create the column.
939
+ * @param emitter - the emitter
940
+ * @param domain - node or edge
941
+ * @param name - the column name
942
+ * @param namespace - its origin namespace
943
+ * @param type - its origin type
944
+ * @param dtype - json (default) or string
945
+ */
946
+ constructor(emitter, domain, name, namespace, type, dtype = "json") {
947
+ this.emitter = emitter;
948
+ this.domain = domain;
949
+ this.name = name;
950
+ this.namespace = namespace;
951
+ this.type = type;
952
+ this.dtype = dtype;
953
+ }
954
+ /**
955
+ * Set one value.
956
+ * @param index - the node or edge index
957
+ * @param value - the value
958
+ */
959
+ set(index, value) {
960
+ if (index < 0) {
961
+ return;
962
+ }
963
+ if (this.handle === undefined) {
964
+ this.handle = this.emitter.declare(this.domain, {
965
+ name: this.name,
966
+ dtype: this.dtype,
967
+ nullable: true,
968
+ origin: { format: FORMAT, id: null, title: null, type: this.type, namespace: this.namespace },
969
+ });
970
+ }
971
+ if (this.handle === null) {
972
+ return;
973
+ }
974
+ const sink = this.emitter.target;
975
+ if (this.domain === "node") {
976
+ sink.setNodeValue(this.handle, index, value);
977
+ }
978
+ else {
979
+ sink.setEdgeValue(this.handle, index, value);
980
+ }
981
+ }
982
+ }
983
+ /**
984
+ * Node positions: x and y (y negated: Cytoscape's screen y grows down, graph-io's grows up), z
985
+ * as a coordinate in the draft dialect or under zAs "position", else as the `z` column.
986
+ */
987
+ class Positions {
988
+ /**
989
+ * Create the collector.
990
+ * @param emitter - the emitter
991
+ * @param zAs - where z goes
992
+ */
993
+ constructor(emitter, zAs) {
994
+ this.emitter = emitter;
995
+ this.zAs = zAs;
996
+ this.values = new Map();
997
+ this.zValues = new Map();
998
+ this.hasZ = false;
999
+ }
1000
+ /**
1001
+ * Read one node record's coordinates.
1002
+ * @param row - the node row
1003
+ * @param record - the record
1004
+ */
1005
+ add(row, record) {
1006
+ if (row.index < 0 || (record.x === null && record.y === null && record.z === null)) {
1007
+ return;
1008
+ }
1009
+ const where = { line: record.line, element: row.id };
1010
+ const x = this.number(record.x, where);
1011
+ const y = this.number(record.y, where);
1012
+ const z = this.number(record.z, where);
1013
+ if (z !== null) {
1014
+ this.hasZ = true;
1015
+ if (this.zAs === "column") {
1016
+ this.zValues.set(row.index, z);
1017
+ }
1018
+ }
1019
+ if (x !== null && y !== null) {
1020
+ const flipped = y === 0 ? 0 : -y;
1021
+ this.values.set(row.index, [x, flipped, this.zAs === "position" ? (z ?? 0) : 0]);
1022
+ }
1023
+ }
1024
+ /**
1025
+ * A coordinate.
1026
+ * @param text - the text, or null
1027
+ * @param where - the location
1028
+ * @returns the number, or null when absent or invalid (recorded)
1029
+ */
1030
+ number(text, where) {
1031
+ if (text === null) {
1032
+ return null;
1033
+ }
1034
+ const n = Number(text.trim());
1035
+ if (text.trim().length === 0 || !Number.isFinite(n)) {
1036
+ this.emitter.issues.error("validation-error", XGMML_ISSUE.BAD_VALUE, `${this.emitter.prefix()}graphics coordinate "${text}" is not a number`, where);
1037
+ return null;
1038
+ }
1039
+ return n;
1040
+ }
1041
+ /** Declare and write the position and z columns. */
1042
+ write() {
1043
+ const sink = this.emitter.target;
1044
+ if (this.values.size > 0) {
1045
+ const handle = this.emitter.declare("node", {
1046
+ name: POSITION_COLUMN,
1047
+ dtype: "f32",
1048
+ components: 3,
1049
+ role: "position",
1050
+ nullable: true,
1051
+ origin: { format: FORMAT, id: null, title: null, type: "graphics", namespace: XGMML_ORIGIN_NAMESPACE },
1052
+ extra: { sourceDims: this.zAs === "position" && this.hasZ ? 3 : 2, units: "file" },
1053
+ });
1054
+ if (handle !== null) {
1055
+ for (const [index, xyz] of this.values) {
1056
+ sink.setNodeValue(handle, index, xyz);
1057
+ }
1058
+ }
1059
+ }
1060
+ if (this.zValues.size > 0) {
1061
+ const handle = this.emitter.declare("node", {
1062
+ name: Z_COLUMN,
1063
+ dtype: "f64",
1064
+ nullable: true,
1065
+ origin: {
1066
+ format: FORMAT,
1067
+ id: null,
1068
+ title: null,
1069
+ type: "graphics",
1070
+ namespace: CYTOSCAPE_ORIGIN_NAMESPACE,
1071
+ },
1072
+ });
1073
+ if (handle !== null) {
1074
+ for (const [index, z] of this.zValues) {
1075
+ sink.setNodeValue(handle, index, z);
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ /**
1082
+ * Nested-network pointers: a node-nested graph that is not a group names a network.
1083
+ */
1084
+ class Pointers {
1085
+ /**
1086
+ * Create the collector.
1087
+ * @param emitter - the emitter
1088
+ * @param isGroup - the group test
1089
+ */
1090
+ constructor(emitter, isGroup) {
1091
+ this.emitter = emitter;
1092
+ this.isGroup = isGroup;
1093
+ this.names = new Map();
1094
+ this.crossFile = new Map();
1095
+ this.dangling = 0;
1096
+ }
1097
+ /**
1098
+ * Read one node record's pointers.
1099
+ * @param row - the node row
1100
+ * @param record - the record
1101
+ */
1102
+ add(row, record) {
1103
+ if (record.nested.length === 0 || this.isGroup(record) || row.index < 0) {
1104
+ return;
1105
+ }
1106
+ for (const graph of record.nested) {
1107
+ if (graph.href === null) {
1108
+ this.names.set(row.index, graph.label ?? graph.id ?? "");
1109
+ continue;
1110
+ }
1111
+ const id = localRef(graph.href);
1112
+ if (id === null) {
1113
+ const resolved = this.emitter.resolvePointer(graph.href);
1114
+ if (resolved !== null) {
1115
+ this.names.set(row.index, resolved);
1116
+ }
1117
+ else {
1118
+ this.crossFile.set(row.index, graph.href);
1119
+ }
1120
+ continue;
1121
+ }
1122
+ const target = this.emitter.graphById(id);
1123
+ if (target === undefined) {
1124
+ this.dangling++;
1125
+ }
1126
+ else {
1127
+ this.names.set(row.index, target.label ?? target.id ?? id);
1128
+ }
1129
+ }
1130
+ }
1131
+ /** Write the pointer columns and their warnings. */
1132
+ write() {
1133
+ const prefix = this.emitter.prefix();
1134
+ if (this.dangling > 0) {
1135
+ this.emitter.issues.warning("validation-error", XGMML_ISSUE.DANGLING_REFERENCE, `${prefix}${this.dangling} nested-network pointer(s) name no graph of the document`);
1136
+ }
1137
+ const names = new JsonColumn(this.emitter, "node", NESTED_NETWORK_COLUMN, CYTOSCAPE_ORIGIN_NAMESPACE, "nestedNetwork", "string");
1138
+ for (const [index, name] of this.names) {
1139
+ names.set(index, name);
1140
+ }
1141
+ if (this.crossFile.size > 0) {
1142
+ this.emitter.issues.warning("unsupported", XGMML_ISSUE.CROSS_FILE_REFERENCE, `${prefix}${this.crossFile.size} nested-network pointer(s) point into another file; each is kept as text in ${NETWORK_POINTER_COLUMN}`);
1143
+ const column = new JsonColumn(this.emitter, "node", NETWORK_POINTER_COLUMN, XGMML_ORIGIN_NAMESPACE, "xlink:href", "string");
1144
+ for (const [index, href] of this.crossFile) {
1145
+ column.set(index, href);
1146
+ }
1147
+ }
1148
+ }
1149
+ }
1150
+ /**
1151
+ * Group containment: every node (declared or referenced) in a group's nested graph has the group
1152
+ * node as a parent; a node in several groups gets the `parents` list instead of `parent`.
1153
+ */
1154
+ class Containment {
1155
+ /**
1156
+ * Create the resolver.
1157
+ * @param emitter - the emitter
1158
+ */
1159
+ constructor(emitter) {
1160
+ this.emitter = emitter;
1161
+ this.parents = new Map();
1162
+ this.subgraphs = new Map();
1163
+ }
1164
+ /**
1165
+ * Collect the memberships of every group node of the graph.
1166
+ * @param rows - the node rows
1167
+ * @param isGroup - the group test
1168
+ */
1169
+ resolve(rows, isGroup) {
1170
+ let unknown = 0;
1171
+ for (const row of rows) {
1172
+ for (const record of row.records) {
1173
+ if (row.index < 0 || record.nested.length === 0 || !isGroup(record)) {
1174
+ continue;
1175
+ }
1176
+ for (const graph of record.nested) {
1177
+ const atts = graph.atts.filter((a) => a.name !== null);
1178
+ if (graph.id !== null || graph.label !== null || atts.length > 0) {
1179
+ this.subgraphs.set(row.index, {
1180
+ id: graph.id,
1181
+ label: graph.label,
1182
+ atts: Object.fromEntries(atts.map((a) => [a.name, attJson(a)])),
1183
+ });
1184
+ }
1185
+ for (const member of graph.members) {
1186
+ if (member.kind !== "node") {
1187
+ continue;
1188
+ }
1189
+ const id = member.id ?? (member.href === null ? null : localRef(member.href));
1190
+ const child = id === null ? undefined : this.emitter.rowOf(id);
1191
+ if (child === undefined) {
1192
+ if (id === null || !this.emitter.missingMember(row.id, id)) {
1193
+ unknown++;
1194
+ }
1195
+ continue;
1196
+ }
1197
+ if (child.index >= 0) {
1198
+ this.link(child.index, row.index, member.line);
1199
+ }
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+ if (unknown > 0) {
1205
+ this.emitter.issues.error("missing-value", XGMML_ISSUE.UNKNOWN_PARENT, `${this.emitter.prefix()}${unknown} group member(s) name no node of this graph`);
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Add one membership unless it closes a parent cycle.
1210
+ * @param child - the member's index
1211
+ * @param parent - the group node's index
1212
+ * @param line - the line
1213
+ */
1214
+ link(child, parent, line) {
1215
+ if (child === parent || this.isAncestor(child, parent)) {
1216
+ this.emitter.issues.error("validation-error", XGMML_ISSUE.PARENT_CYCLE, `${this.emitter.prefix()}a group membership would make a node its own ancestor; that membership is dropped`, { line });
1217
+ return;
1218
+ }
1219
+ const list = this.parents.get(child) ?? [];
1220
+ if (!list.includes(parent)) {
1221
+ list.push(parent);
1222
+ }
1223
+ this.parents.set(child, list);
1224
+ }
1225
+ /**
1226
+ * Whether a node is an ancestor of another through the memberships so far.
1227
+ * @param ancestor - the candidate ancestor
1228
+ * @param node - the node
1229
+ * @returns true when following parents from node reaches ancestor
1230
+ */
1231
+ isAncestor(ancestor, node) {
1232
+ const stack = [node];
1233
+ const seen = new Set();
1234
+ while (stack.length > 0) {
1235
+ const n = stack.pop();
1236
+ if (n === ancestor) {
1237
+ return true;
1238
+ }
1239
+ if (seen.has(n)) {
1240
+ continue;
1241
+ }
1242
+ seen.add(n);
1243
+ stack.push(...(this.parents.get(n) ?? []));
1244
+ }
1245
+ return false;
1246
+ }
1247
+ /** Declare and write the parent (or parents) and subgraph columns. */
1248
+ write() {
1249
+ const sink = this.emitter.target;
1250
+ if (this.parents.size > 0) {
1251
+ const multi = [...this.parents.values()].some((list) => list.length > 1);
1252
+ const handle = this.emitter.declare("node", {
1253
+ name: multi ? PARENTS_COLUMN : PARENT_COLUMN,
1254
+ dtype: multi ? "list" : "u32",
1255
+ itemDtype: multi ? "u32" : undefined,
1256
+ role: multi ? "parents" : "parent",
1257
+ refersTo: "node",
1258
+ nullable: true,
1259
+ origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: null },
1260
+ });
1261
+ if (handle !== null) {
1262
+ for (const [child, list] of this.parents) {
1263
+ sink.setNodeValue(handle, child, multi ? list : list[0]);
1264
+ }
1265
+ }
1266
+ }
1267
+ if (this.subgraphs.size > 0) {
1268
+ const column = new JsonColumn(this.emitter, "node", SUBGRAPH_COLUMN, XGMML_ORIGIN_NAMESPACE, "graph");
1269
+ for (const [index, value] of this.subgraphs) {
1270
+ column.set(index, value);
1271
+ }
1272
+ }
1273
+ }
1274
+ }
1275
+ //# sourceMappingURL=emit.js.map