@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,975 @@
1
+ /**
2
+ * The XGMML document model: one pass of the shared XML tokenizer turns an XGMML document into
3
+ * plain records (graphs, nodes, edges, atts, graphics) that keep everything the file says, with
4
+ * forward references (`xlink:href`, an edge before its nodes) left as ids. The importer
5
+ * (importer.ts) resolves the records into a sink once the outermost `</graph>` has closed, as
6
+ * Cytoscape does, and the `.cys` importer reuses the same records for the network and view files
7
+ * of a session.
8
+ *
9
+ * It recognises the five shapes of `research-xgmml.md` section 2: the 1.0 draft, the Cytoscape
10
+ * 2.x export and session, the 3.x export, the 3.x session network file (root `cy:registered="0"`)
11
+ * and the 3.x session view file (root `cy:view="1"`).
12
+ */
13
+
14
+ import { escapeXmlAttribute, escapeXmlText } from "../../common/escape.js";
15
+ import { type ImportReportBuilder } from "../../common/report.js";
16
+ import { isWhitespace, localName, type XmlHandler } from "../../common/xml.js";
17
+ import { XGMML_ISSUE, XGMML_NAMESPACE } from "./constants.js";
18
+
19
+ /** One `<att>` and what it holds. */
20
+ export interface AttRec {
21
+ /** The `name` (else `label`) attribute, or null. */
22
+ name: string | null;
23
+ /** The XGMML `type` as written, or null. */
24
+ readonly type: string | null;
25
+ /** Cytoscape's `cy:type`, or null. */
26
+ readonly cyType: string | null;
27
+ /** Cytoscape's `cy:elementType` of a list, or null. */
28
+ readonly elementType: string | null;
29
+ /** The `value` attribute, or null when absent. */
30
+ readonly value: string | null;
31
+ /** `cy:hidden` is 1, true or yes. */
32
+ readonly hidden: boolean;
33
+ /** `cy:equation` is 1, true or yes: the value is a formula. */
34
+ readonly equation: boolean;
35
+ /** The child atts (list items, record fields, graphics properties). */
36
+ readonly children: AttRec[];
37
+ /** The XML attributes besides the ones above (a 2.x bend handle's `x` and `y`). */
38
+ readonly extra: Record<string, string>;
39
+ /** Foreign XML inside the att (RDF), serialized, or null. */
40
+ xml: string | null;
41
+ /** Whether the att held a `<graph>`. */
42
+ hasGraph: boolean;
43
+ /** Non-whitespace character data inside the att. */
44
+ text: string;
45
+ /** The line of the `<att`. */
46
+ readonly line: number;
47
+ /** The table namespace of a `.cys` column, or null for an att of the document. */
48
+ readonly namespace?: string | null | undefined;
49
+ }
50
+
51
+ /** A `<node>`. */
52
+ export interface NodeRec {
53
+ readonly kind: "node";
54
+ /** The id (the label when the id is missing), or null for an `xlink:href` reference. */
55
+ readonly id: string | null;
56
+ /** The `xlink:href` of a reference to a node declared elsewhere, or null. */
57
+ readonly href: string | null;
58
+ /** The `label` attribute (else `name`), or null. */
59
+ readonly label: string | null;
60
+ /** The `name` attribute when a label is also given, or null. */
61
+ readonly name: string | null;
62
+ /** The other XML attributes, as written. */
63
+ readonly xmlAttrs: [string, string][];
64
+ /** The atts, in document order. */
65
+ readonly atts: AttRec[];
66
+ /** The graphics values besides x, y and z, or null without `<graphics>`. */
67
+ graphics: Record<string, unknown> | null;
68
+ /** The x, y and z texts of the graphics (or its `<center>`), or null. */
69
+ x: string | null;
70
+ y: string | null;
71
+ z: string | null;
72
+ /** The graphs nested in the node's atts. */
73
+ readonly nested: GraphRec[];
74
+ /** The graph that declares (or references) the node. */
75
+ readonly graph: GraphRec;
76
+ /** The line of the `<node`. */
77
+ readonly line: number;
78
+ /** A session view's `cy:nodeId`, or null. */
79
+ readonly viewId: string | null;
80
+ }
81
+
82
+ /** An `<edge>`. */
83
+ export interface EdgeRec {
84
+ readonly kind: "edge";
85
+ /** The id attribute, or null. */
86
+ readonly id: string | null;
87
+ /** The `xlink:href` of a reference to an edge declared elsewhere, or null. */
88
+ readonly href: string | null;
89
+ /** The source and target texts, or null when missing. */
90
+ readonly source: string | null;
91
+ readonly target: string | null;
92
+ /** The label attribute, or null. */
93
+ readonly label: string | null;
94
+ /** The `name` attribute, or null. */
95
+ readonly name: string | null;
96
+ /** The `weight` attribute, or null. */
97
+ readonly weight: string | null;
98
+ /** The `cy:directed` attribute, or null. */
99
+ readonly directed: string | null;
100
+ /** The other XML attributes, as written. */
101
+ readonly xmlAttrs: [string, string][];
102
+ /** The atts, in document order. */
103
+ readonly atts: AttRec[];
104
+ /** The graphics values, or null. */
105
+ graphics: Record<string, unknown> | null;
106
+ /** The graph that declares (or references) the edge. */
107
+ readonly graph: GraphRec;
108
+ /** The line of the `<edge`. */
109
+ readonly line: number;
110
+ /** A session view's `cy:edgeId`, or null. */
111
+ readonly viewId: string | null;
112
+ }
113
+
114
+ /** A `<graph>`: the root, a subnetwork, a group or a nested-network pointer. */
115
+ export interface GraphRec {
116
+ /** The id attribute, or null. */
117
+ readonly id: string | null;
118
+ /** The label attribute, or null. */
119
+ readonly label: string | null;
120
+ /** The `xlink:href` of a pointer to a graph written elsewhere, or null. */
121
+ readonly href: string | null;
122
+ /** `cy:registered` as written, or null. */
123
+ readonly registered: string | null;
124
+ /** Every XML attribute, as written. */
125
+ readonly attrs: ReadonlyMap<string, string>;
126
+ /** The graph-level atts. */
127
+ readonly atts: AttRec[];
128
+ /** The graph's `<graphics>` values, or null. */
129
+ graphics: Record<string, unknown> | null;
130
+ /** The node whose att holds this graph, or null. */
131
+ readonly owner: NodeRec | null;
132
+ /** The graph this one is nested in, or null for the root. */
133
+ readonly parent: GraphRec | null;
134
+ /** The nodes and edges directly in this graph (declared or referenced), in document order. */
135
+ readonly members: (NodeRec | EdgeRec)[];
136
+ /** The graphs held by this graph's own atts (subnetworks of the root). */
137
+ readonly subgraphs: GraphRec[];
138
+ /** The line of the `<graph`. */
139
+ readonly line: number;
140
+ }
141
+
142
+ /** A parsed document. */
143
+ export interface XgmmlDocument {
144
+ /** The root graph. */
145
+ readonly root: GraphRec;
146
+ /** Every declared node (not references), in document order. */
147
+ readonly nodes: NodeRec[];
148
+ /** Every declared edge, in document order. */
149
+ readonly edges: EdgeRec[];
150
+ /** Every graph, root first. */
151
+ readonly graphs: GraphRec[];
152
+ /** The `cy` prefix is bound, or a `cy:` attribute is used: Cytoscape wrote the file. */
153
+ readonly cytoscape: boolean;
154
+ /** The root is in the XGMML namespace (default or prefixed). */
155
+ readonly xgmmlNamespace: boolean;
156
+ /** The RDF network metadata fields (`dc:title`, ...) by local name. */
157
+ readonly rdf: Record<string, string>;
158
+ /** The serialized `networkMetadata` RDF, or null. */
159
+ readonly rdfXml: string | null;
160
+ }
161
+
162
+ /** What the parser keeps about one open element. */
163
+ type Frame =
164
+ | { readonly kind: "graph"; readonly graph: GraphRec }
165
+ | { readonly kind: "node"; readonly node: NodeRec }
166
+ | { readonly kind: "edge"; readonly edge: EdgeRec }
167
+ | { readonly kind: "att"; readonly att: AttRec; readonly owner: "graph" | "node" | "edge" | "graphics" | "att" }
168
+ | { readonly kind: "graphics"; readonly target: GraphicsTarget }
169
+ | { readonly kind: "line"; readonly points: Record<string, string>[] }
170
+ | { readonly kind: "capture"; readonly att: AttRec; depth: number }
171
+ | { readonly kind: "skip" };
172
+
173
+ /** What a `<graphics>` writes into. */
174
+ interface GraphicsTarget {
175
+ graphics: Record<string, unknown> | null;
176
+ x?: string | null;
177
+ y?: string | null;
178
+ z?: string | null;
179
+ }
180
+
181
+ /** The XML attributes the parser reads on `<node>`. */
182
+ const NODE_READ: ReadonlySet<string> = new Set(["id", "label", "name", "xlink:href", "cy:nodeId"]);
183
+
184
+ /** The XML attributes the parser reads on `<edge>`. */
185
+ const EDGE_READ: ReadonlySet<string> = new Set([
186
+ "id",
187
+ "label",
188
+ "name",
189
+ "source",
190
+ "target",
191
+ "weight",
192
+ "cy:directed",
193
+ "xlink:href",
194
+ "cy:edgeId",
195
+ ]);
196
+
197
+ /** The XML attributes an `<att>` uses as its own; any other is kept in `extra`. */
198
+ const ATT_READ: ReadonlySet<string> = new Set([
199
+ "name",
200
+ "label",
201
+ "type",
202
+ "value",
203
+ "cy:type",
204
+ "cy:elementType",
205
+ "cy:hidden",
206
+ "cy:equation",
207
+ "cy:editable",
208
+ ]);
209
+
210
+ /** Cytoscape's spellings of true (`1`, `true`, `yes`, any case). */
211
+ const TRUE_TEXT = /^(1|true|yes)$/i;
212
+
213
+ /**
214
+ * Whether a Cytoscape flag text is true.
215
+ * @param text - the text, or undefined
216
+ * @returns true for 1, true or yes in any case
217
+ */
218
+ export function isCyTrue(text: string | null | undefined): boolean {
219
+ return text !== null && text !== undefined && TRUE_TEXT.test(text.trim());
220
+ }
221
+
222
+ /**
223
+ * The name an XML attribute is known by, its `xlink` prefix normalised whatever prefix the file
224
+ * binds XLink to (galFiltered.xgmml uses `ns1`).
225
+ * @param name - the attribute name as written
226
+ * @param xlinkPrefixes - the prefixes bound to the XLink namespace
227
+ * @returns the name with `xlink:` for XLink attributes
228
+ */
229
+ function normaliseName(name: string, xlinkPrefixes: ReadonlySet<string>): string {
230
+ const colon = name.indexOf(":");
231
+ if (colon > 0 && xlinkPrefixes.has(name.slice(0, colon))) {
232
+ return `xlink:${name.slice(colon + 1)}`;
233
+ }
234
+ return name;
235
+ }
236
+
237
+ /**
238
+ * The tokenizer handler that builds an XgmmlDocument.
239
+ */
240
+ export class XgmmlParser implements XmlHandler {
241
+ private readonly report: ImportReportBuilder;
242
+
243
+ private readonly frames: Frame[] = [];
244
+
245
+ private readonly xlinkPrefixes = new Set<string>(["xlink"]);
246
+
247
+ private rootGraph: GraphRec | null = null;
248
+
249
+ private readonly nodes: NodeRec[] = [];
250
+
251
+ private readonly edges: EdgeRec[] = [];
252
+
253
+ private readonly graphs: GraphRec[] = [];
254
+
255
+ private cytoscape = false;
256
+
257
+ private xgmmlNamespace = false;
258
+
259
+ /** The prefix of the root element (`xgmml` in `<xgmml:graph>`), or null when it has none. */
260
+ private rootPrefix: string | null = null;
261
+
262
+ private readonly rdf: Record<string, string> = {};
263
+
264
+ private rdfXml: string | null = null;
265
+
266
+ /**
267
+ * Create a parser.
268
+ * @param report - the report issues are recorded in
269
+ */
270
+ constructor(report: ImportReportBuilder) {
271
+ this.report = report;
272
+ }
273
+
274
+ /**
275
+ * The parsed document.
276
+ * @returns the document; E_NO_GRAPH (fatal) when no root graph was read
277
+ */
278
+ document(): XgmmlDocument {
279
+ const root = this.rootGraph;
280
+ if (root === null) {
281
+ return this.report.fail(XGMML_ISSUE.NO_GRAPH, "the document has no <graph> element");
282
+ }
283
+ return {
284
+ root,
285
+ nodes: this.nodes,
286
+ edges: this.edges,
287
+ graphs: this.graphs,
288
+ cytoscape: this.cytoscape,
289
+ xgmmlNamespace: this.xgmmlNamespace,
290
+ rdf: this.rdf,
291
+ rdfXml: this.rdfXml,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * An element starts.
297
+ * @param rawName - the element name as written
298
+ * @param rawAttrs - its attributes
299
+ * @param line - the line
300
+ */
301
+ start(rawName: string, rawAttrs: ReadonlyMap<string, string>, line: number): void {
302
+ const top = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
303
+ if (top === null) {
304
+ this.startRoot(rawName, rawAttrs, line);
305
+ return;
306
+ }
307
+ if (top.kind === "skip") {
308
+ this.frames.push(top);
309
+ return;
310
+ }
311
+ if (top.kind === "capture") {
312
+ this.capture(top, rawName, rawAttrs);
313
+ return;
314
+ }
315
+ const attrs = this.normalise(rawAttrs);
316
+ const name = localName(rawName);
317
+ switch (top.kind) {
318
+ case "graph":
319
+ this.startInGraph(top.graph, rawName, name, attrs, line);
320
+ return;
321
+ case "node":
322
+ this.startInElement(top.node, rawName, name, attrs, line);
323
+ return;
324
+ case "edge":
325
+ this.startInElement(top.edge, rawName, name, attrs, line);
326
+ return;
327
+ case "att":
328
+ this.startInAtt(top, rawName, name, attrs, line);
329
+ return;
330
+ case "graphics":
331
+ this.startInGraphics(top.target, rawName, name, attrs, line);
332
+ return;
333
+ case "line":
334
+ if (name === "point") {
335
+ top.points.push(Object.fromEntries(attrs));
336
+ this.frames.push({ kind: "skip" });
337
+ return;
338
+ }
339
+ this.unknownElement(rawName, line);
340
+ return;
341
+ default: {
342
+ const kind: never = top;
343
+ throw new Error(`unknown XGMML frame ${String(kind)}`);
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * An element ends.
350
+ * @param name - the element name
351
+ * @param _line - the line
352
+ */
353
+ end(name: string, _line: number): void {
354
+ const frame = this.frames.pop();
355
+ const below = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
356
+ switch (frame?.kind) {
357
+ case "capture":
358
+ frame.att.xml = `${frame.att.xml ?? ""}</${name}>`;
359
+ frame.depth--;
360
+ if (frame.depth > 0) {
361
+ this.frames.push(frame);
362
+ } else if (frame.att.name === "networkMetadata") {
363
+ this.rdfXml = frame.att.xml;
364
+ }
365
+ this.rdfElement = null;
366
+ return;
367
+ case "att":
368
+ if (below?.kind === "graphics") {
369
+ this.graphicsAtt(below.target, frame.att);
370
+ }
371
+ return;
372
+ case "line":
373
+ if (below?.kind === "graphics") {
374
+ below.target.graphics = { ...(below.target.graphics ?? {}), Line: frame.points };
375
+ }
376
+ return;
377
+ default:
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Character data.
383
+ * @param text - the text
384
+ * @param line - the line
385
+ */
386
+ text(text: string, line: number): void {
387
+ const top = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
388
+ if (top === null || top.kind === "skip") {
389
+ return;
390
+ }
391
+ if (top.kind === "capture") {
392
+ top.att.xml = `${top.att.xml ?? ""}${escapeXmlText(text)}`;
393
+ this.captureRdfText(text);
394
+ return;
395
+ }
396
+ if (isWhitespace(text)) {
397
+ return;
398
+ }
399
+ if (top.kind === "att") {
400
+ top.att.text += text;
401
+ }
402
+ this.report.warnOnce(
403
+ "validation-error",
404
+ XGMML_ISSUE.STRAY_TEXT,
405
+ top.kind === "att"
406
+ ? "text inside <att> is ignored; an XGMML value is the value attribute"
407
+ : "text where XGMML allows only elements was ignored",
408
+ { line },
409
+ );
410
+ }
411
+
412
+ // ------------------------------------------------------------------ structure
413
+
414
+ /**
415
+ * The XML attribute map with every XLink prefix written `xlink:`.
416
+ * @param attrs - the attributes as written
417
+ * @returns the normalised map
418
+ */
419
+ private normalise(attrs: ReadonlyMap<string, string>): Map<string, string> {
420
+ const out = new Map<string, string>();
421
+ for (const [key, value] of attrs) {
422
+ out.set(normaliseName(key, this.xlinkPrefixes), value);
423
+ }
424
+ return out;
425
+ }
426
+
427
+ /**
428
+ * The root element: a `<graph>`, else the document is not XGMML.
429
+ * @param rawName - the name as written
430
+ * @param rawAttrs - its attributes
431
+ * @param line - the line
432
+ */
433
+ private startRoot(rawName: string, rawAttrs: ReadonlyMap<string, string>, line: number): void {
434
+ const local = localName(rawName);
435
+ const prefix = rawName.includes(":") ? rawName.slice(0, rawName.indexOf(":")) : null;
436
+ for (const [key, value] of rawAttrs) {
437
+ if (key.startsWith("xmlns:") && value === "http://www.w3.org/1999/xlink") {
438
+ this.xlinkPrefixes.add(key.slice(6));
439
+ }
440
+ if (key === "xmlns:cy" || key.startsWith("cy:")) {
441
+ this.cytoscape = true;
442
+ }
443
+ }
444
+ this.rootPrefix = prefix;
445
+ const nsKey = prefix === null ? "xmlns" : `xmlns:${prefix}`;
446
+ this.xgmmlNamespace = rawAttrs.get(nsKey) === XGMML_NAMESPACE;
447
+ if (local !== "graph") {
448
+ this.report.fail(
449
+ XGMML_ISSUE.NO_GRAPH,
450
+ `the root element is <${rawName}>, not <graph>; an XGMML graph embedded in another document is not read`,
451
+ { line },
452
+ );
453
+ }
454
+ const graph = this.newGraph(this.normalise(rawAttrs), line, null, null);
455
+ this.rootGraph = graph;
456
+ this.frames.push({ kind: "graph", graph });
457
+ }
458
+
459
+ /**
460
+ * A graph record, registered in the document.
461
+ * @param attrs - its attributes
462
+ * @param line - the line
463
+ * @param owner - the node whose att holds it, or null
464
+ * @param parent - the graph it is nested in, or null
465
+ * @returns the record
466
+ */
467
+ private newGraph(
468
+ attrs: ReadonlyMap<string, string>,
469
+ line: number,
470
+ owner: NodeRec | null,
471
+ parent: GraphRec | null,
472
+ ): GraphRec {
473
+ const graph: GraphRec = {
474
+ id: attrs.get("id") ?? null,
475
+ label: attrs.get("label") ?? null,
476
+ href: attrs.get("xlink:href") ?? null,
477
+ registered: attrs.get("cy:registered") ?? null,
478
+ attrs,
479
+ atts: [],
480
+ graphics: null,
481
+ owner,
482
+ parent,
483
+ members: [],
484
+ subgraphs: [],
485
+ line,
486
+ };
487
+ if (!this.cytoscape) {
488
+ for (const key of attrs.keys()) {
489
+ if (key.startsWith("cy:")) {
490
+ this.cytoscape = true;
491
+ }
492
+ }
493
+ }
494
+ this.graphs.push(graph);
495
+ return graph;
496
+ }
497
+
498
+ /**
499
+ * A child of `<graph>`.
500
+ * @param graph - the graph
501
+ * @param rawName - the name as written
502
+ * @param name - its local name
503
+ * @param attrs - its attributes
504
+ * @param line - the line
505
+ */
506
+ private startInGraph(
507
+ graph: GraphRec,
508
+ rawName: string,
509
+ name: string,
510
+ attrs: Map<string, string>,
511
+ line: number,
512
+ ): void {
513
+ switch (name) {
514
+ case "node":
515
+ this.beginNode(graph, attrs, line);
516
+ return;
517
+ case "edge":
518
+ this.beginEdge(graph, attrs, line);
519
+ return;
520
+ case "att": {
521
+ const att = this.newAtt(attrs, line);
522
+ graph.atts.push(att);
523
+ this.beginAtt(att, attrs, "graph");
524
+ return;
525
+ }
526
+ case "graphics":
527
+ this.beginGraphics(graph, attrs);
528
+ return;
529
+ default:
530
+ this.unknownElement(rawName, line);
531
+ }
532
+ }
533
+
534
+ /**
535
+ * A child of `<node>` or `<edge>`.
536
+ * @param element - the node or edge
537
+ * @param rawName - the name as written
538
+ * @param name - its local name
539
+ * @param attrs - its attributes
540
+ * @param line - the line
541
+ */
542
+ private startInElement(
543
+ element: NodeRec | EdgeRec,
544
+ rawName: string,
545
+ name: string,
546
+ attrs: Map<string, string>,
547
+ line: number,
548
+ ): void {
549
+ switch (name) {
550
+ case "att": {
551
+ const att = this.newAtt(attrs, line);
552
+ element.atts.push(att);
553
+ this.beginAtt(att, attrs, element.kind);
554
+ return;
555
+ }
556
+ case "graphics":
557
+ this.beginGraphics(element, attrs);
558
+ return;
559
+ default:
560
+ this.unknownElement(rawName, line);
561
+ }
562
+ }
563
+
564
+ /**
565
+ * A child of `<att>`: a child att, a nested graph, or foreign XML.
566
+ * @param frame - the att frame
567
+ * @param rawName - the name as written
568
+ * @param name - its local name
569
+ * @param attrs - its attributes
570
+ * @param line - the line
571
+ */
572
+ private startInAtt(
573
+ frame: Extract<Frame, { kind: "att" }>,
574
+ rawName: string,
575
+ name: string,
576
+ attrs: Map<string, string>,
577
+ line: number,
578
+ ): void {
579
+ const { att } = frame;
580
+ const xgmml = this.isXgmmlName(rawName);
581
+ if (name === "att" && xgmml) {
582
+ const child = this.newAtt(attrs, line);
583
+ att.children.push(child);
584
+ this.beginAtt(child, attrs, frame.owner === "graphics" ? "graphics" : "att");
585
+ return;
586
+ }
587
+ if (name === "graph" && xgmml) {
588
+ att.hasGraph = true;
589
+ this.beginNestedGraph(frame, attrs, line);
590
+ return;
591
+ }
592
+ // a second foreign element of the same att is appended to the first
593
+ att.xml ??= "";
594
+ const capture: Extract<Frame, { kind: "capture" }> = { kind: "capture", att, depth: 0 };
595
+ this.frames.push(capture);
596
+ this.capture(capture, rawName, attrs);
597
+ }
598
+
599
+ /**
600
+ * Whether an element name inside an att is XGMML's own (unprefixed, or with the root's prefix,
601
+ * as in a document whose root is `<xgmml:graph>`) rather than foreign XML such as `rdf:RDF`.
602
+ * @param rawName - the name as written
603
+ * @returns true for an XGMML element name
604
+ */
605
+ private isXgmmlName(rawName: string): boolean {
606
+ const colon = rawName.indexOf(":");
607
+ return colon < 0 || (this.rootPrefix !== null && rawName.slice(0, colon) === this.rootPrefix);
608
+ }
609
+
610
+ /**
611
+ * A `<graph>` inside an `<att>`: a subnetwork of the root, a group or nested-network pointer
612
+ * of a node, or (in an edge) a graph with no model.
613
+ * @param frame - the att frame
614
+ * @param attrs - the graph's attributes
615
+ * @param line - the line
616
+ */
617
+ private beginNestedGraph(frame: Extract<Frame, { kind: "att" }>, attrs: Map<string, string>, line: number): void {
618
+ const holder = this.attHolder();
619
+ if (holder === null || holder.kind === "edge" || frame.owner === "graphics") {
620
+ this.report.warnOnce(
621
+ "unsupported",
622
+ XGMML_ISSUE.EDGE_NESTED_GRAPH,
623
+ "a graph nested in an edge's (or a graphics) att has no model and was skipped with its content",
624
+ { line },
625
+ );
626
+ this.frames.push({ kind: "skip" });
627
+ return;
628
+ }
629
+ if (holder.kind === "node") {
630
+ const graph = this.newGraph(attrs, line, holder.node, holder.node.graph);
631
+ holder.node.nested.push(graph);
632
+ this.frames.push({ kind: "graph", graph });
633
+ return;
634
+ }
635
+ const graph = this.newGraph(attrs, line, null, holder.graph);
636
+ holder.graph.subgraphs.push(graph);
637
+ this.frames.push({ kind: "graph", graph });
638
+ }
639
+
640
+ /**
641
+ * The innermost node, edge or graph frame below the open atts.
642
+ * @returns the frame, or null
643
+ */
644
+ private attHolder(): Extract<Frame, { kind: "graph" | "node" | "edge" }> | null {
645
+ for (let i = this.frames.length - 1; i >= 0; i--) {
646
+ const frame = this.frames[i];
647
+ if (frame.kind === "graph" || frame.kind === "node" || frame.kind === "edge") {
648
+ return frame;
649
+ }
650
+ }
651
+ return null;
652
+ }
653
+
654
+ /**
655
+ * Report an element XGMML does not define here and skip its subtree.
656
+ * @param rawName - the name as written
657
+ * @param line - the line
658
+ */
659
+ private unknownElement(rawName: string, line: number): void {
660
+ this.report.warnOnce(
661
+ "unsupported",
662
+ XGMML_ISSUE.UNKNOWN_ELEMENT,
663
+ `element <${rawName}> is not XGMML here and was skipped with its content`,
664
+ { line, element: rawName },
665
+ `${XGMML_ISSUE.UNKNOWN_ELEMENT}:${rawName}`,
666
+ );
667
+ this.frames.push({ kind: "skip" });
668
+ }
669
+
670
+ // ------------------------------------------------------------------ nodes and edges
671
+
672
+ /**
673
+ * Open a `<node>`.
674
+ * @param graph - the graph it is in
675
+ * @param attrs - its attributes
676
+ * @param line - the line
677
+ */
678
+ private beginNode(graph: GraphRec, attrs: Map<string, string>, line: number): void {
679
+ const href = attrs.get("xlink:href") ?? null;
680
+ let id = attrs.get("id") ?? null;
681
+ const labelAttr = attrs.get("label") ?? null;
682
+ const nameAttr = attrs.get("name") ?? null;
683
+ const label = labelAttr ?? nameAttr;
684
+ if (href === null && id === null) {
685
+ if (label === null || label.length === 0) {
686
+ this.report.counts.skippedNodes++;
687
+ this.report.error("missing-value", XGMML_ISSUE.MISSING_ID, "<node> without an id or a label", {
688
+ line,
689
+ });
690
+ this.frames.push({ kind: "skip" });
691
+ return;
692
+ }
693
+ this.report.warning(
694
+ "validation-error",
695
+ XGMML_ISSUE.ID_FROM_LABEL,
696
+ `<node> without an id: its label "${label}" is used as the id`,
697
+ { line, element: label },
698
+ );
699
+ id = label;
700
+ }
701
+ const node: NodeRec = {
702
+ kind: "node",
703
+ id: href === null ? id : null,
704
+ href,
705
+ label,
706
+ name: labelAttr !== null ? nameAttr : null,
707
+ xmlAttrs: otherAttributes(attrs, NODE_READ),
708
+ atts: [],
709
+ graphics: null,
710
+ x: null,
711
+ y: null,
712
+ z: null,
713
+ nested: [],
714
+ graph,
715
+ line,
716
+ viewId: attrs.get("cy:nodeId") ?? null,
717
+ };
718
+ graph.members.push(node);
719
+ if (href === null) {
720
+ this.nodes.push(node);
721
+ }
722
+ this.frames.push({ kind: "node", node });
723
+ }
724
+
725
+ /**
726
+ * Open an `<edge>`.
727
+ * @param graph - the graph it is in
728
+ * @param attrs - its attributes
729
+ * @param line - the line
730
+ */
731
+ private beginEdge(graph: GraphRec, attrs: Map<string, string>, line: number): void {
732
+ const href = attrs.get("xlink:href") ?? null;
733
+ const edge: EdgeRec = {
734
+ kind: "edge",
735
+ id: href === null ? (attrs.get("id") ?? null) : null,
736
+ href,
737
+ source: attrs.get("source") ?? null,
738
+ target: attrs.get("target") ?? null,
739
+ label: attrs.get("label") ?? null,
740
+ name: attrs.get("name") ?? null,
741
+ weight: attrs.get("weight") ?? null,
742
+ directed: attrs.get("cy:directed") ?? null,
743
+ xmlAttrs: otherAttributes(attrs, EDGE_READ),
744
+ atts: [],
745
+ graphics: null,
746
+ graph,
747
+ line,
748
+ viewId: attrs.get("cy:edgeId") ?? null,
749
+ };
750
+ graph.members.push(edge);
751
+ if (href === null) {
752
+ this.edges.push(edge);
753
+ }
754
+ this.frames.push({ kind: "edge", edge });
755
+ }
756
+
757
+ // ------------------------------------------------------------------ atts
758
+
759
+ /**
760
+ * An att record from its XML attributes.
761
+ * @param attrs - the attributes
762
+ * @param line - the line
763
+ * @returns the record
764
+ */
765
+ private newAtt(attrs: Map<string, string>, line: number): AttRec {
766
+ const extra: Record<string, string> = {};
767
+ for (const [key, value] of attrs) {
768
+ if (!ATT_READ.has(key) && key !== "xmlns" && !key.startsWith("xmlns:")) {
769
+ extra[key] = value;
770
+ }
771
+ }
772
+ return {
773
+ name: attrs.get("name") ?? attrs.get("label") ?? null,
774
+ type: attrs.get("type") ?? null,
775
+ cyType: attrs.get("cy:type") ?? null,
776
+ elementType: attrs.get("cy:elementType") ?? null,
777
+ value: attrs.get("value") ?? null,
778
+ hidden: isCyTrue(attrs.get("cy:hidden")),
779
+ equation: isCyTrue(attrs.get("cy:equation")),
780
+ children: [],
781
+ extra,
782
+ xml: null,
783
+ hasGraph: false,
784
+ text: "",
785
+ line,
786
+ };
787
+ }
788
+
789
+ /**
790
+ * Open an `<att>`.
791
+ * @param att - the record
792
+ * @param _attrs - its attributes
793
+ * @param owner - what holds it
794
+ */
795
+ private beginAtt(
796
+ att: AttRec,
797
+ _attrs: Map<string, string>,
798
+ owner: "graph" | "node" | "edge" | "graphics" | "att",
799
+ ): void {
800
+ this.frames.push({ kind: "att", att, owner });
801
+ }
802
+
803
+ /**
804
+ * Serialize one start tag of foreign XML into the capturing att.
805
+ * @param frame - the capture frame
806
+ * @param rawName - the name as written
807
+ * @param attrs - its attributes
808
+ */
809
+ private capture(
810
+ frame: Extract<Frame, { kind: "capture" }>,
811
+ rawName: string,
812
+ attrs: ReadonlyMap<string, string>,
813
+ ): void {
814
+ let tag = `<${rawName}`;
815
+ for (const [key, value] of attrs) {
816
+ tag += ` ${key}="${escapeXmlAttribute(value)}"`;
817
+ }
818
+ frame.att.xml = `${frame.att.xml ?? ""}${tag}>`;
819
+ frame.depth++;
820
+ if (frame.att.name === "networkMetadata") {
821
+ this.rdfElement = localName(rawName);
822
+ }
823
+ }
824
+
825
+ /** The local name of the RDF element whose text is being read, or null. */
826
+ private rdfElement: string | null = null;
827
+
828
+ /**
829
+ * Keep the text of an RDF metadata field.
830
+ * @param text - the text
831
+ */
832
+ private captureRdfText(text: string): void {
833
+ if (this.rdfElement !== null && !isWhitespace(text)) {
834
+ this.rdf[this.rdfElement] = (this.rdf[this.rdfElement] ?? "") + text;
835
+ }
836
+ }
837
+
838
+ // ------------------------------------------------------------------ graphics
839
+
840
+ /**
841
+ * Open a `<graphics>`: x, y and z (draft and Cytoscape) are read as coordinates; every other
842
+ * attribute is kept as written, merged key by key with an earlier `<graphics>`.
843
+ * @param target - the node, edge or graph
844
+ * @param attrs - its attributes
845
+ */
846
+ private beginGraphics(target: GraphicsTarget, attrs: Map<string, string>): void {
847
+ const graphics: Record<string, unknown> = { ...(target.graphics ?? {}) };
848
+ for (const [key, value] of attrs) {
849
+ if ("x" in target && (key === "x" || key === "y" || key === "z")) {
850
+ target[key] = value;
851
+ } else if (key !== "xmlns" && !key.startsWith("xmlns:")) {
852
+ graphics[key] = value;
853
+ }
854
+ }
855
+ target.graphics = graphics;
856
+ this.frames.push({ kind: "graphics", target });
857
+ }
858
+
859
+ /**
860
+ * A child of `<graphics>`: a visual-property att, a `<center>` or a `<Line>`.
861
+ * @param target - the graphics target
862
+ * @param rawName - the name as written
863
+ * @param name - its local name
864
+ * @param attrs - its attributes
865
+ * @param line - the line
866
+ */
867
+ private startInGraphics(
868
+ target: GraphicsTarget,
869
+ rawName: string,
870
+ name: string,
871
+ attrs: Map<string, string>,
872
+ line: number,
873
+ ): void {
874
+ switch (name) {
875
+ case "att": {
876
+ this.frames.push({ kind: "att", att: this.newAtt(attrs, line), owner: "graphics" });
877
+ return;
878
+ }
879
+ case "center":
880
+ if ("x" in target) {
881
+ target.x ??= attrs.get("x") ?? null;
882
+ target.y ??= attrs.get("y") ?? null;
883
+ target.z ??= attrs.get("z") ?? null;
884
+ }
885
+ this.frames.push({ kind: "skip" });
886
+ return;
887
+ case "Line":
888
+ case "line":
889
+ this.frames.push({ kind: "line", points: [] });
890
+ return;
891
+ default:
892
+ this.unknownElement(rawName, line);
893
+ }
894
+ }
895
+
896
+ /**
897
+ * Keep a visual-property att of a `<graphics>` in its record, as written. A session view's
898
+ * `<att name="z">` repeats z and is read as z when the attributes gave none.
899
+ * @param target - the graphics target
900
+ * @param att - the att
901
+ */
902
+ private graphicsAtt(target: GraphicsTarget, att: AttRec): void {
903
+ if (att.name === "z" && "z" in target && att.children.length === 0) {
904
+ target.z ??= att.value;
905
+ return;
906
+ }
907
+ if (att.name === null) {
908
+ this.report.warnOnce(
909
+ "validation-error",
910
+ XGMML_ISSUE.BAD_ATT,
911
+ "a graphics <att> without a name was skipped",
912
+ { line: att.line },
913
+ `${XGMML_ISSUE.BAD_ATT}:graphics-name`,
914
+ );
915
+ return;
916
+ }
917
+ target.graphics = { ...(target.graphics ?? {}), [att.name]: attJson(att) };
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Whether an att's children are list items rather than named fields: they have no names, repeat
923
+ * the parent's name (Cytoscape 3 lists), or are several that share one name (2.x bend handles).
924
+ * @param att - the att
925
+ * @returns true for a list
926
+ */
927
+ export function isListLike(att: AttRec): boolean {
928
+ const names = new Set(att.children.map((c) => c.name));
929
+ if (names.size !== 1) {
930
+ return false;
931
+ }
932
+ const [only] = names;
933
+ return only === null || only === att.name || att.children.length > 1;
934
+ }
935
+
936
+ /**
937
+ * A graphics att as plain data, values as written: an att with children is an array when they
938
+ * are list items (isListLike), else a record by name; a childless att is its value,
939
+ * else its other XML attributes (a 2.x bend handle's x and y), else null.
940
+ * @param att - the att
941
+ * @returns the value
942
+ */
943
+ export function attJson(att: AttRec): unknown {
944
+ if (att.children.length === 0) {
945
+ if (att.value !== null) {
946
+ return att.value;
947
+ }
948
+ return Object.keys(att.extra).length > 0 ? { ...att.extra } : null;
949
+ }
950
+ if (isListLike(att)) {
951
+ return att.children.map(attJson);
952
+ }
953
+ const out: Record<string, unknown> = {};
954
+ for (const child of att.children) {
955
+ out[child.name ?? ""] = attJson(child);
956
+ }
957
+ return out;
958
+ }
959
+
960
+ /**
961
+ * The XML attributes of an element the parser does not read itself, without namespace
962
+ * declarations.
963
+ * @param attrs - the attributes
964
+ * @param read - the names the parser reads
965
+ * @returns the others, in document order
966
+ */
967
+ function otherAttributes(attrs: ReadonlyMap<string, string>, read: ReadonlySet<string>): [string, string][] {
968
+ const out: [string, string][] = [];
969
+ for (const [key, value] of attrs) {
970
+ if (!read.has(key) && key !== "xmlns" && !key.startsWith("xmlns:")) {
971
+ out.push([key, value]);
972
+ }
973
+ }
974
+ return out;
975
+ }