@graphty/graph-io 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/README.md +80 -2
  2. package/dist/chunks/{escape-D-gZWO26.js → escape-B-tkk_Cl.js} +74 -16
  3. package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
  4. package/dist/chunks/{writer-GAdltGmC.js → export-Bh60Dv-n.js} +7 -252
  5. package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
  6. package/dist/chunks/exporter-BGrsWamJ.js +736 -0
  7. package/dist/chunks/exporter-BGrsWamJ.js.map +1 -0
  8. package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
  9. package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
  10. package/dist/chunks/{importer-Br_QeAeE.js → importer-BWY2FFc5.js} +28 -6
  11. package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
  12. package/dist/chunks/importer-BaQpCEcJ.js +2194 -0
  13. package/dist/chunks/importer-BaQpCEcJ.js.map +1 -0
  14. package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
  15. package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
  16. package/dist/chunks/{importer-aNJfe0qu.js → importer-DEcKhsmQ.js} +2 -2
  17. package/dist/chunks/{importer-aNJfe0qu.js.map → importer-DEcKhsmQ.js.map} +1 -1
  18. package/dist/chunks/{importer-DHagxvDD.js → importer-DIrbbnAf.js} +6 -4
  19. package/dist/chunks/{importer-DHagxvDD.js.map → importer-DIrbbnAf.js.map} +1 -1
  20. package/dist/chunks/importer-D_e7e7LX.js +3055 -0
  21. package/dist/chunks/importer-D_e7e7LX.js.map +1 -0
  22. package/dist/chunks/{importer-Du5crN9l.js → importer-Wv4C4gaF.js} +278 -82
  23. package/dist/chunks/importer-Wv4C4gaF.js.map +1 -0
  24. package/dist/chunks/importer-vi3bSdtw.js +1713 -0
  25. package/dist/chunks/importer-vi3bSdtw.js.map +1 -0
  26. package/dist/chunks/{importer-d0uQxFp6.js → importer-zGLo8Dg_.js} +6 -4
  27. package/dist/chunks/{importer-d0uQxFp6.js.map → importer-zGLo8Dg_.js.map} +1 -1
  28. package/dist/chunks/json-elements-DDS8N2Dd.js +779 -0
  29. package/dist/chunks/json-elements-DDS8N2Dd.js.map +1 -0
  30. package/dist/chunks/{records-Bk9jgodz.js → records-dbRkxwaq.js} +2 -2
  31. package/dist/chunks/{records-Bk9jgodz.js.map → records-dbRkxwaq.js.map} +1 -1
  32. package/dist/chunks/{report-BOk0p5y8.js → report-B1z4WT9e.js} +143 -111
  33. package/dist/chunks/{report-BOk0p5y8.js.map → report-B1z4WT9e.js.map} +1 -1
  34. package/dist/chunks/weights-Dzba96G3.js +176 -0
  35. package/dist/chunks/weights-Dzba96G3.js.map +1 -0
  36. package/dist/chunks/writer-C7flM-Ih.js +77 -0
  37. package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
  38. package/dist/csv.js +6 -4
  39. package/dist/csv.js.map +1 -1
  40. package/dist/cx.d.ts +1 -0
  41. package/dist/cx.js +6 -0
  42. package/dist/cx.js.map +1 -0
  43. package/dist/cx2.d.ts +1 -0
  44. package/dist/cx2.js +10 -0
  45. package/dist/cx2.js.map +1 -0
  46. package/dist/cys.d.ts +1 -0
  47. package/dist/cys.js +6 -0
  48. package/dist/cys.js.map +1 -0
  49. package/dist/dot.js +1 -1
  50. package/dist/gexf.js +16 -4
  51. package/dist/gexf.js.map +1 -1
  52. package/dist/gml.js +13 -14
  53. package/dist/gml.js.map +1 -1
  54. package/dist/graph-io.js +302 -399
  55. package/dist/graph-io.js.map +1 -1
  56. package/dist/graphml.js +1 -1
  57. package/dist/json.js +1 -1
  58. package/dist/neo4j.js +6 -4
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +1 -1
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/common/cell-budget.d.ts +84 -0
  63. package/dist/src/common/cell-budget.d.ts.map +1 -0
  64. package/dist/src/common/cell-budget.js +138 -0
  65. package/dist/src/common/cell-budget.js.map +1 -0
  66. package/dist/src/common/input.d.ts +10 -0
  67. package/dist/src/common/input.d.ts.map +1 -1
  68. package/dist/src/common/input.js +39 -0
  69. package/dist/src/common/input.js.map +1 -1
  70. package/dist/src/common/json-elements.d.ts +286 -0
  71. package/dist/src/common/json-elements.d.ts.map +1 -0
  72. package/dist/src/common/json-elements.js +926 -0
  73. package/dist/src/common/json-elements.js.map +1 -0
  74. package/dist/src/common/options.d.ts +6 -0
  75. package/dist/src/common/options.d.ts.map +1 -1
  76. package/dist/src/common/options.js +1 -1
  77. package/dist/src/common/options.js.map +1 -1
  78. package/dist/src/common/xml.d.ts +37 -3
  79. package/dist/src/common/xml.d.ts.map +1 -1
  80. package/dist/src/common/xml.js +86 -6
  81. package/dist/src/common/xml.js.map +1 -1
  82. package/dist/src/common/zip.d.ts +92 -0
  83. package/dist/src/common/zip.d.ts.map +1 -0
  84. package/dist/src/common/zip.js +399 -0
  85. package/dist/src/common/zip.js.map +1 -0
  86. package/dist/src/formats/cx/importer.d.ts +133 -0
  87. package/dist/src/formats/cx/importer.d.ts.map +1 -0
  88. package/dist/src/formats/cx/importer.js +2220 -0
  89. package/dist/src/formats/cx/importer.js.map +1 -0
  90. package/dist/src/formats/cx/index.d.ts +7 -0
  91. package/dist/src/formats/cx/index.d.ts.map +1 -0
  92. package/dist/src/formats/cx/index.js +7 -0
  93. package/dist/src/formats/cx/index.js.map +1 -0
  94. package/dist/src/formats/cx2/exporter.d.ts +59 -0
  95. package/dist/src/formats/cx2/exporter.d.ts.map +1 -0
  96. package/dist/src/formats/cx2/exporter.js +864 -0
  97. package/dist/src/formats/cx2/exporter.js.map +1 -0
  98. package/dist/src/formats/cx2/importer.d.ts +169 -0
  99. package/dist/src/formats/cx2/importer.d.ts.map +1 -0
  100. package/dist/src/formats/cx2/importer.js +1652 -0
  101. package/dist/src/formats/cx2/importer.js.map +1 -0
  102. package/dist/src/formats/cx2/index.d.ts +7 -0
  103. package/dist/src/formats/cx2/index.d.ts.map +1 -0
  104. package/dist/src/formats/cx2/index.js +7 -0
  105. package/dist/src/formats/cx2/index.js.map +1 -0
  106. package/dist/src/formats/cys/constants.d.ts +68 -0
  107. package/dist/src/formats/cys/constants.d.ts.map +1 -0
  108. package/dist/src/formats/cys/constants.js +76 -0
  109. package/dist/src/formats/cys/constants.js.map +1 -0
  110. package/dist/src/formats/cys/importer.d.ts +38 -0
  111. package/dist/src/formats/cys/importer.d.ts.map +1 -0
  112. package/dist/src/formats/cys/importer.js +830 -0
  113. package/dist/src/formats/cys/importer.js.map +1 -0
  114. package/dist/src/formats/cys/index.d.ts +7 -0
  115. package/dist/src/formats/cys/index.d.ts.map +1 -0
  116. package/dist/src/formats/cys/index.js +7 -0
  117. package/dist/src/formats/cys/index.js.map +1 -0
  118. package/dist/src/formats/cys/session.d.ts +132 -0
  119. package/dist/src/formats/cys/session.d.ts.map +1 -0
  120. package/dist/src/formats/cys/session.js +315 -0
  121. package/dist/src/formats/cys/session.js.map +1 -0
  122. package/dist/src/formats/cys/tables.d.ts +90 -0
  123. package/dist/src/formats/cys/tables.d.ts.map +1 -0
  124. package/dist/src/formats/cys/tables.js +293 -0
  125. package/dist/src/formats/cys/tables.js.map +1 -0
  126. package/dist/src/formats/dot/exporter.d.ts +2 -0
  127. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  128. package/dist/src/formats/dot/exporter.js +12 -0
  129. package/dist/src/formats/dot/exporter.js.map +1 -1
  130. package/dist/src/formats/dot/importer.js +6 -2
  131. package/dist/src/formats/dot/importer.js.map +1 -1
  132. package/dist/src/formats/gexf/exporter.d.ts +2 -0
  133. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  134. package/dist/src/formats/gexf/exporter.js +6 -0
  135. package/dist/src/formats/gexf/exporter.js.map +1 -1
  136. package/dist/src/formats/gexf/importer.d.ts +0 -2
  137. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  138. package/dist/src/formats/gexf/importer.js +1 -3
  139. package/dist/src/formats/gexf/importer.js.map +1 -1
  140. package/dist/src/formats/gexf/index.d.ts +1 -1
  141. package/dist/src/formats/gexf/index.js +2 -2
  142. package/dist/src/formats/gexf/index.js.map +1 -1
  143. package/dist/src/formats/gml/exporter.d.ts +0 -8
  144. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  145. package/dist/src/formats/gml/exporter.js +8 -18
  146. package/dist/src/formats/gml/exporter.js.map +1 -1
  147. package/dist/src/formats/json/importer.d.ts.map +1 -1
  148. package/dist/src/formats/json/importer.js +6 -104
  149. package/dist/src/formats/json/importer.js.map +1 -1
  150. package/dist/src/formats/xgmml/columns.d.ts +152 -0
  151. package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
  152. package/dist/src/formats/xgmml/columns.js +593 -0
  153. package/dist/src/formats/xgmml/columns.js.map +1 -0
  154. package/dist/src/formats/xgmml/constants.d.ts +196 -0
  155. package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
  156. package/dist/src/formats/xgmml/constants.js +197 -0
  157. package/dist/src/formats/xgmml/constants.js.map +1 -0
  158. package/dist/src/formats/xgmml/document.d.ts +361 -0
  159. package/dist/src/formats/xgmml/document.d.ts.map +1 -0
  160. package/dist/src/formats/xgmml/document.js +695 -0
  161. package/dist/src/formats/xgmml/document.js.map +1 -0
  162. package/dist/src/formats/xgmml/emit.d.ts +341 -0
  163. package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
  164. package/dist/src/formats/xgmml/emit.js +1275 -0
  165. package/dist/src/formats/xgmml/emit.js.map +1 -0
  166. package/dist/src/formats/xgmml/exporter.d.ts +24 -0
  167. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
  168. package/dist/src/formats/xgmml/exporter.js +999 -0
  169. package/dist/src/formats/xgmml/exporter.js.map +1 -0
  170. package/dist/src/formats/xgmml/importer.d.ts +58 -0
  171. package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
  172. package/dist/src/formats/xgmml/importer.js +360 -0
  173. package/dist/src/formats/xgmml/importer.js.map +1 -0
  174. package/dist/src/formats/xgmml/index.d.ts +8 -0
  175. package/dist/src/formats/xgmml/index.d.ts.map +1 -0
  176. package/dist/src/formats/xgmml/index.js +8 -0
  177. package/dist/src/formats/xgmml/index.js.map +1 -0
  178. package/dist/src/formats/xgmml/values.d.ts +60 -0
  179. package/dist/src/formats/xgmml/values.d.ts.map +1 -0
  180. package/dist/src/formats/xgmml/values.js +148 -0
  181. package/dist/src/formats/xgmml/values.js.map +1 -0
  182. package/dist/src/index.d.ts +4 -0
  183. package/dist/src/index.d.ts.map +1 -1
  184. package/dist/src/index.js +4 -0
  185. package/dist/src/index.js.map +1 -1
  186. package/dist/src/registry.d.ts +7 -0
  187. package/dist/src/registry.d.ts.map +1 -1
  188. package/dist/src/registry.js +30 -10
  189. package/dist/src/registry.js.map +1 -1
  190. package/dist/src/sniff.d.ts +1 -1
  191. package/dist/src/sniff.d.ts.map +1 -1
  192. package/dist/src/sniff.js +4 -0
  193. package/dist/src/sniff.js.map +1 -1
  194. package/dist/xgmml.d.ts +1 -0
  195. package/dist/xgmml.js +9 -0
  196. package/dist/xgmml.js.map +1 -0
  197. package/package.json +25 -2
  198. package/src/common/cell-budget.ts +170 -0
  199. package/src/common/input.ts +40 -0
  200. package/src/common/json-elements.ts +1147 -0
  201. package/src/common/options.ts +1 -1
  202. package/src/common/xml.ts +127 -6
  203. package/src/common/zip.ts +472 -0
  204. package/src/formats/cx/importer.ts +2733 -0
  205. package/src/formats/cx/index.ts +7 -0
  206. package/src/formats/cx2/exporter.ts +1036 -0
  207. package/src/formats/cx2/importer.ts +2187 -0
  208. package/src/formats/cx2/index.ts +7 -0
  209. package/src/formats/cys/constants.ts +99 -0
  210. package/src/formats/cys/importer.ts +1100 -0
  211. package/src/formats/cys/index.ts +7 -0
  212. package/src/formats/cys/session.ts +428 -0
  213. package/src/formats/cys/tables.ts +400 -0
  214. package/src/formats/dot/exporter.ts +17 -0
  215. package/src/formats/dot/importer.ts +6 -2
  216. package/src/formats/gexf/exporter.ts +11 -0
  217. package/src/formats/gexf/importer.ts +1 -2
  218. package/src/formats/gexf/index.ts +1 -1
  219. package/src/formats/gml/exporter.ts +8 -19
  220. package/src/formats/json/importer.ts +6 -105
  221. package/src/formats/xgmml/columns.ts +747 -0
  222. package/src/formats/xgmml/constants.ts +265 -0
  223. package/src/formats/xgmml/document.ts +975 -0
  224. package/src/formats/xgmml/emit.ts +1563 -0
  225. package/src/formats/xgmml/exporter.ts +1215 -0
  226. package/src/formats/xgmml/importer.ts +491 -0
  227. package/src/formats/xgmml/index.ts +8 -0
  228. package/src/formats/xgmml/values.ts +183 -0
  229. package/src/index.ts +19 -0
  230. package/src/registry.ts +46 -15
  231. package/src/sniff.ts +18 -1
  232. package/dist/chunks/escape-D-gZWO26.js.map +0 -1
  233. package/dist/chunks/importer-Br_QeAeE.js.map +0 -1
  234. package/dist/chunks/importer-Du5crN9l.js.map +0 -1
  235. package/dist/chunks/writer-GAdltGmC.js.map +0 -1
@@ -0,0 +1,3055 @@
1
+ import { INVALID_INDEX, GraphFormatError } from "@graphty/graph-format";
2
+ import { af as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, a as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, O as OPTION_IGNORED_CODE, I as ID_MERGED_CODE, R as ROLE_TAKEN_CODE, C as COLUMN_RENAMED_CODE, A as AMBIGUOUS_GRAPH_NAME_CODE, G as GRAPH_NOT_FOUND_CODE, M as MULTIPLE_GRAPHS_CODE, au as STRAY_TEXT_CODE, Y as UNKNOWN_ELEMENT_CODE, k as UNKNOWN_ATTR_TYPE_CODE, am as EQUATION_AS_TEXT_CODE, h as PARENT_CYCLE_CODE, U as UNKNOWN_PARENT_CODE, i as DUPLICATE_ATTRIBUTE_CODE, j as DANGLING_REFERENCE_CODE, P as PRECISION_CODE, W as WIDENED_CODE, B as BAD_VALUE_CODE, c as DUPLICATE_EDGE_ID_CODE, d as DUPLICATE_NODE_CODE, e as MISSING_ENDPOINT_CODE, f as MISSING_ID_CODE, N as NO_GRAPH_CODE, q as UNKNOWN_ENCODING_CODE, E as ENCODING_FALLBACK_CODE, r as INVALID_ENCODING_CODE, s as INVALID_UTF8_CODE, u as EMPTY_INPUT_CODE, aw as XML_SYNTAX_CODE, a3 as STORAGE_CLASS_CODE, a7 as WEIGHT_KEY_CLASH_CODE, a6 as ROLE_ASSUMED_CODE, a5 as COLUMN_RENAMED_LOSS_CODE, b as ID_TEXT_TYPE_CODE, aa as MUTUAL_EXPANDED_CODE, ab as PARENTS_DROPPED_CODE, ac as ROLE_DROPPED_CODE, ad as TEMPORAL_TEXT_DROPPED_CODE, ae as TEMPORAL_DROPPED_CODE, a0 as XML_ILLEGAL_CHAR_CODE, Q as uniqueColumnName, L as declareResolved, _ as DictHeuristic, K as IdCoercer, J as DirectionResolver, y as throwIfAborted, F as reportSinkOptions, z as reportUnusedOptions, H as chooseGraph, v as resolveImportOptions, w as ImportReportBuilder, x as textChunks } from "./report-B1z4WT9e.js";
3
+ import { l as localName, a as escapeXmlText, g as isWhitespace, e as escapeXmlAttribute, t as tokenizeXml, f as xmlDeclaredEncoding, X as XmlSyntaxError } from "./escape-B-tkk_Cl.js";
4
+ import { p as parseWeightText } from "./weights-Dzba96G3.js";
5
+ import { p as parseTextCell, i as inferTextDtype } from "./text-Dr0Ifpag.js";
6
+ const FORMAT = "xgmml";
7
+ const EXTENSIONS = Object.freeze([".xgmml", ".xml"]);
8
+ const MIME_TYPES = Object.freeze(["application/xgmml", "text/xgmml", "text/xgmml+xml"]);
9
+ const XGMML_NAMESPACE = "http://www.cs.rpi.edu/XGMML";
10
+ const CY_NAMESPACE = "http://www.cytoscape.org";
11
+ const XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
12
+ const META_KEY = "xgmml";
13
+ const LABEL_COLUMN = "label";
14
+ const EDGE_ID_COLUMN = "id";
15
+ const POSITION_COLUMN = "position";
16
+ const Z_COLUMN = "z";
17
+ const GRAPHICS_COLUMN = "graphics";
18
+ const PARENT_COLUMN = "parent";
19
+ const PARENTS_COLUMN = "parents";
20
+ const SUBGRAPH_COLUMN = "xgmml.subgraph";
21
+ const NESTED_NETWORK_COLUMN = "cytoscape.nestedNetwork";
22
+ const NETWORK_POINTER_COLUMN = "xgmml.networkPointer";
23
+ const NETWORKS_COLUMN = "xgmml.networks";
24
+ const INTERACTION_COLUMN = "interaction";
25
+ const XGMML_ORIGIN_NAMESPACE = "xgmml";
26
+ const CYTOSCAPE_ORIGIN_NAMESPACE = "cytoscape";
27
+ const XGMML_ISSUE = Object.freeze({
28
+ /** Fatal: the input is not well-formed XML. */
29
+ XML_SYNTAX: XML_SYNTAX_CODE,
30
+ /** Fatal: the input is empty or whitespace only. */
31
+ EMPTY_INPUT: EMPTY_INPUT_CODE,
32
+ /** Fatal: the input holds invalid UTF-8. */
33
+ INVALID_UTF8: INVALID_UTF8_CODE,
34
+ /** Fatal: bytes invalid in the encoding a BOM, a declaration or the option chose. */
35
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
36
+ /** Undeclared non-UTF-8 bytes were read as windows-1252. */
37
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
38
+ /** A declared encoding the platform cannot decode was ignored. */
39
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
40
+ /** Fatal: the root element is not `<graph>` (an XHTML page embedding one, a GraphML file). */
41
+ NO_GRAPH: NO_GRAPH_CODE,
42
+ /** Fatal: a session view document (`cy:view="1"`): view SUIDs and no topology. */
43
+ VIEW_DOCUMENT: "E_XGMML_VIEW_DOCUMENT",
44
+ /** A node with neither an id nor a label; it is skipped with its subtree. */
45
+ MISSING_ID: MISSING_ID_CODE,
46
+ /** A node without an id; its label is used as the id. */
47
+ ID_FROM_LABEL: "W_XGMML_ID_FROM_LABEL",
48
+ /** An edge without a source or a target that no label alias resolves. */
49
+ MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
50
+ /** An edge endpoint that names no node (addMissingNodes false); the edge is skipped. */
51
+ UNKNOWN_NODE: "E_UNKNOWN_NODE",
52
+ /** Endpoints resolved through Cytoscape's `"a (pp) b"` label aliases; interactions filled from labels. */
53
+ LABEL_ALIAS: "W_XGMML_LABEL_ALIAS",
54
+ /** A node id declared twice; the declarations are merged. */
55
+ DUPLICATE_NODE: DUPLICATE_NODE_CODE,
56
+ /** An edge id used twice; the second edge is skipped. */
57
+ DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
58
+ /** A meta-edge the 2.x writer repeats inside a group; the copy is dropped. */
59
+ GROUP_DUPLICATE_EDGE: "W_XGMML_GROUP_DUPLICATE_EDGE",
60
+ /** A `directed` or `cy:directed` value other than 0 / 1. */
61
+ BAD_DIRECTED: "W_XGMML_BAD_DIRECTED",
62
+ /** A `documentVersion` that does not parse; the dialect is chosen from the content. */
63
+ DOCUMENT_VERSION: "W_XGMML_DOCUMENT_VERSION",
64
+ /** A root `<graph>` with neither the XGMML namespace nor an XGMML DOCTYPE. */
65
+ NO_NAMESPACE: "W_XGMML_NO_NAMESPACE",
66
+ /** A malformed `<att>`: a list with a value, a scalar with child atts, an att with no name. */
67
+ BAD_ATT: "W_XGMML_BAD_ATT",
68
+ /** A bare `&` read as `&amp;` under repairBareAmpersands. */
69
+ AMPERSAND_REPAIRED: "W_XGMML_AMPERSAND_REPAIRED",
70
+ /** Two surrogate character references joined under pairSurrogateReferences. */
71
+ SURROGATE_PAIRED: "W_XGMML_SURROGATE_PAIRED",
72
+ /** An empty list whose element type nothing states; a list of strings is assumed. */
73
+ EMPTY_LIST_TYPE: "W_XGMML_EMPTY_LIST_TYPE",
74
+ /** A record list, a list of lists, a 2.x map or foreign XML in an att, kept as json. */
75
+ RECORD_LIST: "W_XGMML_RECORD_LIST",
76
+ /** A pointer into another file (`file.xgmml#id`); kept as text. */
77
+ CROSS_FILE_REFERENCE: "W_XGMML_CROSS_FILE_REFERENCE",
78
+ /** A graph nested in an edge's att; there is no model for it. */
79
+ EDGE_NESTED_GRAPH: "W_XGMML_EDGE_NESTED_GRAPH",
80
+ /** Elements of a session network declared outside every registered subnetwork. */
81
+ ROOT_ONLY_ELEMENTS: "W_XGMML_ROOT_ONLY_ELEMENTS",
82
+ /** A value that does not parse as its declared type; the cell is unset. */
83
+ BAD_VALUE: BAD_VALUE_CODE,
84
+ /** A column widened because its values or declared types disagree. */
85
+ WIDENED: WIDENED_CODE,
86
+ /** A long or real beyond what a double holds exactly. */
87
+ PRECISION: PRECISION_CODE,
88
+ /** A reference (xlink:href, nested-network pointer) that names nothing. */
89
+ DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
90
+ /** The same attribute twice on one element; the later value wins. */
91
+ DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
92
+ /** A group membership naming no node. */
93
+ UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
94
+ /** A group membership that would close a parent cycle; that link is dropped. */
95
+ PARENT_CYCLE: PARENT_CYCLE_CODE,
96
+ /** A Cytoscape formula kept as its text. */
97
+ EQUATION_AS_TEXT: EQUATION_AS_TEXT_CODE,
98
+ /** An att type XGMML and Cytoscape do not define; kept as text. */
99
+ UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
100
+ /** An element XGMML does not define at that place; skipped with its subtree. */
101
+ UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
102
+ /** Character data where XGMML allows only elements, or inside an att. */
103
+ STRAY_TEXT: STRAY_TEXT_CODE,
104
+ /** A session network document holds several registered networks; one was read. */
105
+ MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
106
+ /** graphIndex / graphName names no network (fatal). */
107
+ GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
108
+ /** graphName names several networks (fatal). */
109
+ AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
110
+ /** A column renamed `<name>#<n>` because its name was taken. */
111
+ COLUMN_RENAMED: COLUMN_RENAMED_CODE,
112
+ /** A column declared without its role because the table already holds it. */
113
+ ROLE_TAKEN: ROLE_TAKEN_CODE,
114
+ /** Two id texts merged into one number under ids "number". */
115
+ ID_MERGED: ID_MERGED_CODE,
116
+ /** An option the format has no use for. */
117
+ OPTION_IGNORED: OPTION_IGNORED_CODE,
118
+ /** A builder-policy option the sink does not honour. */
119
+ SINK_OPTION: SINK_OPTION_CODE,
120
+ /** The sink refused the file's direction. */
121
+ DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
122
+ /** Edges forced to the policy's direction. */
123
+ DIRECTION_FORCED: DIRECTION_FORCED_CODE,
124
+ /** A mixed file under onMixedDirection "error" (fatal). */
125
+ MIXED_DIRECTION: MIXED_DIRECTION_CODE
126
+ });
127
+ const XGMML_LOSS = Object.freeze({
128
+ /** A json or nested-list column written as a string att. */
129
+ JSON_AS_STRING: "W_XGMML_JSON_AS_STRING",
130
+ /** A dtype written as a wider Cytoscape type (f32 as Double, u32 above i32 as Long, u8 as Integer, dict as String). */
131
+ WIDENED_TYPE: "W_XGMML_WIDENED_TYPE",
132
+ /** A string holding a character XML 1.0 cannot carry; export() throws. */
133
+ XML_ILLEGAL_CHAR: XML_ILLEGAL_CHAR_CODE,
134
+ /** A temporal column written as plain numbers. */
135
+ TEMPORAL_DROPPED: TEMPORAL_DROPPED_CODE,
136
+ /** A temporal text companion written as a plain string att. */
137
+ TEMPORAL_TEXT_DROPPED: TEMPORAL_TEXT_DROPPED_CODE,
138
+ /** A role the format has no slot for, written as a plain att. */
139
+ ROLE_DROPPED: ROLE_DROPPED_CODE,
140
+ /** A parents column not written because the snapshot also has a parent column. */
141
+ PARENTS_DROPPED: PARENTS_DROPPED_CODE,
142
+ /** A mutual pair written as two directed edges. */
143
+ MUTUAL_EXPANDED: MUTUAL_EXPANDED_CODE,
144
+ /** Node ids whose text reads back as the other type. */
145
+ ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
146
+ /** An edge id column of another dtype written as text; it reads back as strings. */
147
+ EDGE_ID_TEXT: "W_XGMML_EDGE_ID_TEXT",
148
+ /** Strings holding a literal backslash-n or backslash-t read back as newline / tab (Cytoscape's escapes). */
149
+ BACKSLASH_ESCAPE: "W_XGMML_BACKSLASH_ESCAPE",
150
+ /** A position that cannot be written as it is: another shape, a non-finite coordinate, a z read back in the z column. */
151
+ POSITION: "W_XGMML_POSITION",
152
+ /** Nodes whose parent chain never reaches a root are written at the top level. */
153
+ PARENT_CYCLE: "W_XGMML_PARENT_CYCLE",
154
+ /** A column named like a column the importer owns reads back renamed. */
155
+ COLUMN_NAME_CHANGED: COLUMN_RENAMED_LOSS_CODE,
156
+ /** A role-less column written into a slot reads back with the slot's role. */
157
+ ROLE_ASSUMED: ROLE_ASSUMED_CODE,
158
+ /** A plain `weight` edge column reads back as THE weight. */
159
+ WEIGHT_KEY_CLASH: WEIGHT_KEY_CLASH_CODE,
160
+ /** A string / dict column that reads back as the other storage class. */
161
+ STORAGE_CLASS_CHANGED: STORAGE_CLASS_CODE,
162
+ /** Edge labels shaped `a (i) b` read back with an `interaction` column (Cytoscape's label alias). */
163
+ INTERACTION_FROM_LABEL: "W_XGMML_INTERACTION_FROM_LABEL"
164
+ });
165
+ const NODE_READ = /* @__PURE__ */ new Set(["id", "label", "name", "xlink:href", "cy:nodeId"]);
166
+ const EDGE_READ = /* @__PURE__ */ new Set([
167
+ "id",
168
+ "label",
169
+ "name",
170
+ "source",
171
+ "target",
172
+ "weight",
173
+ "cy:directed",
174
+ "xlink:href",
175
+ "cy:edgeId"
176
+ ]);
177
+ const ATT_READ = /* @__PURE__ */ new Set([
178
+ "name",
179
+ "label",
180
+ "type",
181
+ "value",
182
+ "cy:type",
183
+ "cy:elementType",
184
+ "cy:hidden",
185
+ "cy:equation",
186
+ "cy:editable"
187
+ ]);
188
+ const TRUE_TEXT = /^(1|true|yes)$/i;
189
+ function isCyTrue(text) {
190
+ return text !== null && text !== void 0 && TRUE_TEXT.test(text.trim());
191
+ }
192
+ function normaliseName(name, xlinkPrefixes) {
193
+ const colon = name.indexOf(":");
194
+ if (colon > 0 && xlinkPrefixes.has(name.slice(0, colon))) {
195
+ return `xlink:${name.slice(colon + 1)}`;
196
+ }
197
+ return name;
198
+ }
199
+ class XgmmlParser {
200
+ /**
201
+ * Create a parser.
202
+ * @param report - the report issues are recorded in
203
+ */
204
+ constructor(report) {
205
+ this.frames = [];
206
+ this.xlinkPrefixes = /* @__PURE__ */ new Set(["xlink"]);
207
+ this.rootGraph = null;
208
+ this.nodes = [];
209
+ this.edges = [];
210
+ this.graphs = [];
211
+ this.cytoscape = false;
212
+ this.xgmmlNamespace = false;
213
+ this.rootPrefix = null;
214
+ this.rdf = {};
215
+ this.rdfXml = null;
216
+ this.rdfElement = null;
217
+ this.report = report;
218
+ }
219
+ /**
220
+ * The parsed document.
221
+ * @returns the document; E_NO_GRAPH (fatal) when no root graph was read
222
+ */
223
+ document() {
224
+ const root = this.rootGraph;
225
+ if (root === null) {
226
+ return this.report.fail(XGMML_ISSUE.NO_GRAPH, "the document has no <graph> element");
227
+ }
228
+ return {
229
+ root,
230
+ nodes: this.nodes,
231
+ edges: this.edges,
232
+ graphs: this.graphs,
233
+ cytoscape: this.cytoscape,
234
+ xgmmlNamespace: this.xgmmlNamespace,
235
+ rdf: this.rdf,
236
+ rdfXml: this.rdfXml
237
+ };
238
+ }
239
+ /**
240
+ * An element starts.
241
+ * @param rawName - the element name as written
242
+ * @param rawAttrs - its attributes
243
+ * @param line - the line
244
+ */
245
+ start(rawName, rawAttrs, line) {
246
+ const top = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
247
+ if (top === null) {
248
+ this.startRoot(rawName, rawAttrs, line);
249
+ return;
250
+ }
251
+ if (top.kind === "skip") {
252
+ this.frames.push(top);
253
+ return;
254
+ }
255
+ if (top.kind === "capture") {
256
+ this.capture(top, rawName, rawAttrs);
257
+ return;
258
+ }
259
+ const attrs = this.normalise(rawAttrs);
260
+ const name = localName(rawName);
261
+ switch (top.kind) {
262
+ case "graph":
263
+ this.startInGraph(top.graph, rawName, name, attrs, line);
264
+ return;
265
+ case "node":
266
+ this.startInElement(top.node, rawName, name, attrs, line);
267
+ return;
268
+ case "edge":
269
+ this.startInElement(top.edge, rawName, name, attrs, line);
270
+ return;
271
+ case "att":
272
+ this.startInAtt(top, rawName, name, attrs, line);
273
+ return;
274
+ case "graphics":
275
+ this.startInGraphics(top.target, rawName, name, attrs, line);
276
+ return;
277
+ case "line":
278
+ if (name === "point") {
279
+ top.points.push(Object.fromEntries(attrs));
280
+ this.frames.push({ kind: "skip" });
281
+ return;
282
+ }
283
+ this.unknownElement(rawName, line);
284
+ return;
285
+ default: {
286
+ const kind = top;
287
+ throw new Error(`unknown XGMML frame ${String(kind)}`);
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * An element ends.
293
+ * @param name - the element name
294
+ * @param _line - the line
295
+ */
296
+ end(name, _line) {
297
+ const frame = this.frames.pop();
298
+ const below = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
299
+ switch (frame?.kind) {
300
+ case "capture":
301
+ frame.att.xml = `${frame.att.xml ?? ""}</${name}>`;
302
+ frame.depth--;
303
+ if (frame.depth > 0) {
304
+ this.frames.push(frame);
305
+ } else if (frame.att.name === "networkMetadata") {
306
+ this.rdfXml = frame.att.xml;
307
+ }
308
+ this.rdfElement = null;
309
+ return;
310
+ case "att":
311
+ if (below?.kind === "graphics") {
312
+ this.graphicsAtt(below.target, frame.att);
313
+ }
314
+ return;
315
+ case "line":
316
+ if (below?.kind === "graphics") {
317
+ below.target.graphics = { ...below.target.graphics ?? {}, Line: frame.points };
318
+ }
319
+ return;
320
+ }
321
+ }
322
+ /**
323
+ * Character data.
324
+ * @param text - the text
325
+ * @param line - the line
326
+ */
327
+ text(text, line) {
328
+ const top = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
329
+ if (top === null || top.kind === "skip") {
330
+ return;
331
+ }
332
+ if (top.kind === "capture") {
333
+ top.att.xml = `${top.att.xml ?? ""}${escapeXmlText(text)}`;
334
+ this.captureRdfText(text);
335
+ return;
336
+ }
337
+ if (isWhitespace(text)) {
338
+ return;
339
+ }
340
+ if (top.kind === "att") {
341
+ top.att.text += text;
342
+ }
343
+ this.report.warnOnce(
344
+ "validation-error",
345
+ XGMML_ISSUE.STRAY_TEXT,
346
+ top.kind === "att" ? "text inside <att> is ignored; an XGMML value is the value attribute" : "text where XGMML allows only elements was ignored",
347
+ { line }
348
+ );
349
+ }
350
+ // ------------------------------------------------------------------ structure
351
+ /**
352
+ * The XML attribute map with every XLink prefix written `xlink:`.
353
+ * @param attrs - the attributes as written
354
+ * @returns the normalised map
355
+ */
356
+ normalise(attrs) {
357
+ const out = /* @__PURE__ */ new Map();
358
+ for (const [key2, value] of attrs) {
359
+ out.set(normaliseName(key2, this.xlinkPrefixes), value);
360
+ }
361
+ return out;
362
+ }
363
+ /**
364
+ * The root element: a `<graph>`, else the document is not XGMML.
365
+ * @param rawName - the name as written
366
+ * @param rawAttrs - its attributes
367
+ * @param line - the line
368
+ */
369
+ startRoot(rawName, rawAttrs, line) {
370
+ const local = localName(rawName);
371
+ const prefix = rawName.includes(":") ? rawName.slice(0, rawName.indexOf(":")) : null;
372
+ for (const [key2, value] of rawAttrs) {
373
+ if (key2.startsWith("xmlns:") && value === "http://www.w3.org/1999/xlink") {
374
+ this.xlinkPrefixes.add(key2.slice(6));
375
+ }
376
+ if (key2 === "xmlns:cy" || key2.startsWith("cy:")) {
377
+ this.cytoscape = true;
378
+ }
379
+ }
380
+ this.rootPrefix = prefix;
381
+ const nsKey = prefix === null ? "xmlns" : `xmlns:${prefix}`;
382
+ this.xgmmlNamespace = rawAttrs.get(nsKey) === XGMML_NAMESPACE;
383
+ if (local !== "graph") {
384
+ this.report.fail(
385
+ XGMML_ISSUE.NO_GRAPH,
386
+ `the root element is <${rawName}>, not <graph>; an XGMML graph embedded in another document is not read`,
387
+ { line }
388
+ );
389
+ }
390
+ const graph = this.newGraph(this.normalise(rawAttrs), line, null, null);
391
+ this.rootGraph = graph;
392
+ this.frames.push({ kind: "graph", graph });
393
+ }
394
+ /**
395
+ * A graph record, registered in the document.
396
+ * @param attrs - its attributes
397
+ * @param line - the line
398
+ * @param owner - the node whose att holds it, or null
399
+ * @param parent - the graph it is nested in, or null
400
+ * @returns the record
401
+ */
402
+ newGraph(attrs, line, owner, parent) {
403
+ const graph = {
404
+ id: attrs.get("id") ?? null,
405
+ label: attrs.get("label") ?? null,
406
+ href: attrs.get("xlink:href") ?? null,
407
+ registered: attrs.get("cy:registered") ?? null,
408
+ attrs,
409
+ atts: [],
410
+ graphics: null,
411
+ owner,
412
+ parent,
413
+ members: [],
414
+ subgraphs: [],
415
+ line
416
+ };
417
+ if (!this.cytoscape) {
418
+ for (const key2 of attrs.keys()) {
419
+ if (key2.startsWith("cy:")) {
420
+ this.cytoscape = true;
421
+ }
422
+ }
423
+ }
424
+ this.graphs.push(graph);
425
+ return graph;
426
+ }
427
+ /**
428
+ * A child of `<graph>`.
429
+ * @param graph - the graph
430
+ * @param rawName - the name as written
431
+ * @param name - its local name
432
+ * @param attrs - its attributes
433
+ * @param line - the line
434
+ */
435
+ startInGraph(graph, rawName, name, attrs, line) {
436
+ switch (name) {
437
+ case "node":
438
+ this.beginNode(graph, attrs, line);
439
+ return;
440
+ case "edge":
441
+ this.beginEdge(graph, attrs, line);
442
+ return;
443
+ case "att": {
444
+ const att = this.newAtt(attrs, line);
445
+ graph.atts.push(att);
446
+ this.beginAtt(att, attrs, "graph");
447
+ return;
448
+ }
449
+ case "graphics":
450
+ this.beginGraphics(graph, attrs);
451
+ return;
452
+ default:
453
+ this.unknownElement(rawName, line);
454
+ }
455
+ }
456
+ /**
457
+ * A child of `<node>` or `<edge>`.
458
+ * @param element - the node or edge
459
+ * @param rawName - the name as written
460
+ * @param name - its local name
461
+ * @param attrs - its attributes
462
+ * @param line - the line
463
+ */
464
+ startInElement(element, rawName, name, attrs, line) {
465
+ switch (name) {
466
+ case "att": {
467
+ const att = this.newAtt(attrs, line);
468
+ element.atts.push(att);
469
+ this.beginAtt(att, attrs, element.kind);
470
+ return;
471
+ }
472
+ case "graphics":
473
+ this.beginGraphics(element, attrs);
474
+ return;
475
+ default:
476
+ this.unknownElement(rawName, line);
477
+ }
478
+ }
479
+ /**
480
+ * A child of `<att>`: a child att, a nested graph, or foreign XML.
481
+ * @param frame - the att frame
482
+ * @param rawName - the name as written
483
+ * @param name - its local name
484
+ * @param attrs - its attributes
485
+ * @param line - the line
486
+ */
487
+ startInAtt(frame, rawName, name, attrs, line) {
488
+ const { att } = frame;
489
+ const xgmml = this.isXgmmlName(rawName);
490
+ if (name === "att" && xgmml) {
491
+ const child = this.newAtt(attrs, line);
492
+ att.children.push(child);
493
+ this.beginAtt(child, attrs, frame.owner === "graphics" ? "graphics" : "att");
494
+ return;
495
+ }
496
+ if (name === "graph" && xgmml) {
497
+ att.hasGraph = true;
498
+ this.beginNestedGraph(frame, attrs, line);
499
+ return;
500
+ }
501
+ att.xml ??= "";
502
+ const capture = { kind: "capture", att, depth: 0 };
503
+ this.frames.push(capture);
504
+ this.capture(capture, rawName, attrs);
505
+ }
506
+ /**
507
+ * Whether an element name inside an att is XGMML's own (unprefixed, or with the root's prefix,
508
+ * as in a document whose root is `<xgmml:graph>`) rather than foreign XML such as `rdf:RDF`.
509
+ * @param rawName - the name as written
510
+ * @returns true for an XGMML element name
511
+ */
512
+ isXgmmlName(rawName) {
513
+ const colon = rawName.indexOf(":");
514
+ return colon < 0 || this.rootPrefix !== null && rawName.slice(0, colon) === this.rootPrefix;
515
+ }
516
+ /**
517
+ * A `<graph>` inside an `<att>`: a subnetwork of the root, a group or nested-network pointer
518
+ * of a node, or (in an edge) a graph with no model.
519
+ * @param frame - the att frame
520
+ * @param attrs - the graph's attributes
521
+ * @param line - the line
522
+ */
523
+ beginNestedGraph(frame, attrs, line) {
524
+ const holder = this.attHolder();
525
+ if (holder === null || holder.kind === "edge" || frame.owner === "graphics") {
526
+ this.report.warnOnce(
527
+ "unsupported",
528
+ XGMML_ISSUE.EDGE_NESTED_GRAPH,
529
+ "a graph nested in an edge's (or a graphics) att has no model and was skipped with its content",
530
+ { line }
531
+ );
532
+ this.frames.push({ kind: "skip" });
533
+ return;
534
+ }
535
+ if (holder.kind === "node") {
536
+ const graph2 = this.newGraph(attrs, line, holder.node, holder.node.graph);
537
+ holder.node.nested.push(graph2);
538
+ this.frames.push({ kind: "graph", graph: graph2 });
539
+ return;
540
+ }
541
+ const graph = this.newGraph(attrs, line, null, holder.graph);
542
+ holder.graph.subgraphs.push(graph);
543
+ this.frames.push({ kind: "graph", graph });
544
+ }
545
+ /**
546
+ * The innermost node, edge or graph frame below the open atts.
547
+ * @returns the frame, or null
548
+ */
549
+ attHolder() {
550
+ for (let i = this.frames.length - 1; i >= 0; i--) {
551
+ const frame = this.frames[i];
552
+ if (frame.kind === "graph" || frame.kind === "node" || frame.kind === "edge") {
553
+ return frame;
554
+ }
555
+ }
556
+ return null;
557
+ }
558
+ /**
559
+ * Report an element XGMML does not define here and skip its subtree.
560
+ * @param rawName - the name as written
561
+ * @param line - the line
562
+ */
563
+ unknownElement(rawName, line) {
564
+ this.report.warnOnce(
565
+ "unsupported",
566
+ XGMML_ISSUE.UNKNOWN_ELEMENT,
567
+ `element <${rawName}> is not XGMML here and was skipped with its content`,
568
+ { line, element: rawName },
569
+ `${XGMML_ISSUE.UNKNOWN_ELEMENT}:${rawName}`
570
+ );
571
+ this.frames.push({ kind: "skip" });
572
+ }
573
+ // ------------------------------------------------------------------ nodes and edges
574
+ /**
575
+ * Open a `<node>`.
576
+ * @param graph - the graph it is in
577
+ * @param attrs - its attributes
578
+ * @param line - the line
579
+ */
580
+ beginNode(graph, attrs, line) {
581
+ const href = attrs.get("xlink:href") ?? null;
582
+ let id = attrs.get("id") ?? null;
583
+ const labelAttr = attrs.get("label") ?? null;
584
+ const nameAttr = attrs.get("name") ?? null;
585
+ const label = labelAttr ?? nameAttr;
586
+ if (href === null && id === null) {
587
+ if (label === null || label.length === 0) {
588
+ this.report.counts.skippedNodes++;
589
+ this.report.error("missing-value", XGMML_ISSUE.MISSING_ID, "<node> without an id or a label", {
590
+ line
591
+ });
592
+ this.frames.push({ kind: "skip" });
593
+ return;
594
+ }
595
+ this.report.warning(
596
+ "validation-error",
597
+ XGMML_ISSUE.ID_FROM_LABEL,
598
+ `<node> without an id: its label "${label}" is used as the id`,
599
+ { line, element: label }
600
+ );
601
+ id = label;
602
+ }
603
+ const node = {
604
+ kind: "node",
605
+ id: href === null ? id : null,
606
+ href,
607
+ label,
608
+ name: labelAttr !== null ? nameAttr : null,
609
+ xmlAttrs: otherAttributes(attrs, NODE_READ),
610
+ atts: [],
611
+ graphics: null,
612
+ x: null,
613
+ y: null,
614
+ z: null,
615
+ nested: [],
616
+ graph,
617
+ line,
618
+ viewId: attrs.get("cy:nodeId") ?? null
619
+ };
620
+ graph.members.push(node);
621
+ if (href === null) {
622
+ this.nodes.push(node);
623
+ }
624
+ this.frames.push({ kind: "node", node });
625
+ }
626
+ /**
627
+ * Open an `<edge>`.
628
+ * @param graph - the graph it is in
629
+ * @param attrs - its attributes
630
+ * @param line - the line
631
+ */
632
+ beginEdge(graph, attrs, line) {
633
+ const href = attrs.get("xlink:href") ?? null;
634
+ const edge = {
635
+ kind: "edge",
636
+ id: href === null ? attrs.get("id") ?? null : null,
637
+ href,
638
+ source: attrs.get("source") ?? null,
639
+ target: attrs.get("target") ?? null,
640
+ label: attrs.get("label") ?? null,
641
+ name: attrs.get("name") ?? null,
642
+ weight: attrs.get("weight") ?? null,
643
+ directed: attrs.get("cy:directed") ?? null,
644
+ xmlAttrs: otherAttributes(attrs, EDGE_READ),
645
+ atts: [],
646
+ graphics: null,
647
+ graph,
648
+ line,
649
+ viewId: attrs.get("cy:edgeId") ?? null
650
+ };
651
+ graph.members.push(edge);
652
+ if (href === null) {
653
+ this.edges.push(edge);
654
+ }
655
+ this.frames.push({ kind: "edge", edge });
656
+ }
657
+ // ------------------------------------------------------------------ atts
658
+ /**
659
+ * An att record from its XML attributes.
660
+ * @param attrs - the attributes
661
+ * @param line - the line
662
+ * @returns the record
663
+ */
664
+ newAtt(attrs, line) {
665
+ const extra = {};
666
+ for (const [key2, value] of attrs) {
667
+ if (!ATT_READ.has(key2) && key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
668
+ extra[key2] = value;
669
+ }
670
+ }
671
+ return {
672
+ name: attrs.get("name") ?? attrs.get("label") ?? null,
673
+ type: attrs.get("type") ?? null,
674
+ cyType: attrs.get("cy:type") ?? null,
675
+ elementType: attrs.get("cy:elementType") ?? null,
676
+ value: attrs.get("value") ?? null,
677
+ hidden: isCyTrue(attrs.get("cy:hidden")),
678
+ equation: isCyTrue(attrs.get("cy:equation")),
679
+ children: [],
680
+ extra,
681
+ xml: null,
682
+ hasGraph: false,
683
+ text: "",
684
+ line
685
+ };
686
+ }
687
+ /**
688
+ * Open an `<att>`.
689
+ * @param att - the record
690
+ * @param _attrs - its attributes
691
+ * @param owner - what holds it
692
+ */
693
+ beginAtt(att, _attrs, owner) {
694
+ this.frames.push({ kind: "att", att, owner });
695
+ }
696
+ /**
697
+ * Serialize one start tag of foreign XML into the capturing att.
698
+ * @param frame - the capture frame
699
+ * @param rawName - the name as written
700
+ * @param attrs - its attributes
701
+ */
702
+ capture(frame, rawName, attrs) {
703
+ let tag = `<${rawName}`;
704
+ for (const [key2, value] of attrs) {
705
+ tag += ` ${key2}="${escapeXmlAttribute(value)}"`;
706
+ }
707
+ frame.att.xml = `${frame.att.xml ?? ""}${tag}>`;
708
+ frame.depth++;
709
+ if (frame.att.name === "networkMetadata") {
710
+ this.rdfElement = localName(rawName);
711
+ }
712
+ }
713
+ /**
714
+ * Keep the text of an RDF metadata field.
715
+ * @param text - the text
716
+ */
717
+ captureRdfText(text) {
718
+ if (this.rdfElement !== null && !isWhitespace(text)) {
719
+ this.rdf[this.rdfElement] = (this.rdf[this.rdfElement] ?? "") + text;
720
+ }
721
+ }
722
+ // ------------------------------------------------------------------ graphics
723
+ /**
724
+ * Open a `<graphics>`: x, y and z (draft and Cytoscape) are read as coordinates; every other
725
+ * attribute is kept as written, merged key by key with an earlier `<graphics>`.
726
+ * @param target - the node, edge or graph
727
+ * @param attrs - its attributes
728
+ */
729
+ beginGraphics(target, attrs) {
730
+ const graphics = { ...target.graphics ?? {} };
731
+ for (const [key2, value] of attrs) {
732
+ if ("x" in target && (key2 === "x" || key2 === "y" || key2 === "z")) {
733
+ target[key2] = value;
734
+ } else if (key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
735
+ graphics[key2] = value;
736
+ }
737
+ }
738
+ target.graphics = graphics;
739
+ this.frames.push({ kind: "graphics", target });
740
+ }
741
+ /**
742
+ * A child of `<graphics>`: a visual-property att, a `<center>` or a `<Line>`.
743
+ * @param target - the graphics target
744
+ * @param rawName - the name as written
745
+ * @param name - its local name
746
+ * @param attrs - its attributes
747
+ * @param line - the line
748
+ */
749
+ startInGraphics(target, rawName, name, attrs, line) {
750
+ switch (name) {
751
+ case "att": {
752
+ this.frames.push({ kind: "att", att: this.newAtt(attrs, line), owner: "graphics" });
753
+ return;
754
+ }
755
+ case "center":
756
+ if ("x" in target) {
757
+ target.x ??= attrs.get("x") ?? null;
758
+ target.y ??= attrs.get("y") ?? null;
759
+ target.z ??= attrs.get("z") ?? null;
760
+ }
761
+ this.frames.push({ kind: "skip" });
762
+ return;
763
+ case "Line":
764
+ case "line":
765
+ this.frames.push({ kind: "line", points: [] });
766
+ return;
767
+ default:
768
+ this.unknownElement(rawName, line);
769
+ }
770
+ }
771
+ /**
772
+ * Keep a visual-property att of a `<graphics>` in its record, as written. A session view's
773
+ * `<att name="z">` repeats z and is read as z when the attributes gave none.
774
+ * @param target - the graphics target
775
+ * @param att - the att
776
+ */
777
+ graphicsAtt(target, att) {
778
+ if (att.name === "z" && "z" in target && att.children.length === 0) {
779
+ target.z ??= att.value;
780
+ return;
781
+ }
782
+ if (att.name === null) {
783
+ this.report.warnOnce(
784
+ "validation-error",
785
+ XGMML_ISSUE.BAD_ATT,
786
+ "a graphics <att> without a name was skipped",
787
+ { line: att.line },
788
+ `${XGMML_ISSUE.BAD_ATT}:graphics-name`
789
+ );
790
+ return;
791
+ }
792
+ target.graphics = { ...target.graphics ?? {}, [att.name]: attJson(att) };
793
+ }
794
+ }
795
+ function isListLike(att) {
796
+ const names = new Set(att.children.map((c) => c.name));
797
+ if (names.size !== 1) {
798
+ return false;
799
+ }
800
+ const [only] = names;
801
+ return only === null || only === att.name || att.children.length > 1;
802
+ }
803
+ function attJson(att) {
804
+ if (att.children.length === 0) {
805
+ if (att.value !== null) {
806
+ return att.value;
807
+ }
808
+ return Object.keys(att.extra).length > 0 ? { ...att.extra } : null;
809
+ }
810
+ if (isListLike(att)) {
811
+ return att.children.map(attJson);
812
+ }
813
+ const out = {};
814
+ for (const child of att.children) {
815
+ out[child.name ?? ""] = attJson(child);
816
+ }
817
+ return out;
818
+ }
819
+ function otherAttributes(attrs, read) {
820
+ const out = [];
821
+ for (const [key2, value] of attrs) {
822
+ if (!read.has(key2) && key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
823
+ out.push([key2, value]);
824
+ }
825
+ }
826
+ return out;
827
+ }
828
+ const CY_TYPES = {
829
+ string: "string",
830
+ double: "real",
831
+ integer: "int",
832
+ long: "long",
833
+ boolean: "bool",
834
+ list: "list"
835
+ };
836
+ const XGMML_TYPES = {
837
+ string: "string",
838
+ real: "real",
839
+ integer: "int",
840
+ boolean: "bool",
841
+ list: "list",
842
+ map: "map"
843
+ };
844
+ function attType(att) {
845
+ let kind;
846
+ let declared = null;
847
+ if (att.cyType !== null && att.cyType.length > 0) {
848
+ kind = CY_TYPES[att.cyType.trim().toLowerCase()];
849
+ declared = att.cyType;
850
+ }
851
+ if (kind === void 0 && att.type !== null && att.type.length > 0) {
852
+ kind = XGMML_TYPES[att.type.trim().toLowerCase()];
853
+ declared = att.type;
854
+ }
855
+ if (kind === void 0) {
856
+ const unknown = declared ?? null;
857
+ return { kind: "string", declared, unknown };
858
+ }
859
+ if (kind === "int" && att.cyType === null && att.name?.endsWith(".SUID") === true) {
860
+ kind = "long";
861
+ }
862
+ return { kind, declared, unknown: null };
863
+ }
864
+ function elementKind(text) {
865
+ if (text === null) {
866
+ return null;
867
+ }
868
+ const kind = CY_TYPES[text.trim().toLowerCase()] ?? XGMML_TYPES[text.trim().toLowerCase()];
869
+ return kind === void 0 || kind === "list" || kind === "map" ? null : kind;
870
+ }
871
+ function widenScalar(a, b) {
872
+ if (a === b) {
873
+ return a;
874
+ }
875
+ if (a === "string" || b === "string" || a === "bool" || b === "bool") {
876
+ return "string";
877
+ }
878
+ if (a === "real" || b === "real") {
879
+ return "real";
880
+ }
881
+ return "long";
882
+ }
883
+ const INTEGER_TEXT = /^[+-]?[0-9]+$/;
884
+ const REAL_TEXT = /^[+-]?(NaN|Infinity|([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?)$/;
885
+ const I32_MIN = -2147483648;
886
+ const I32_MAX = 2147483647;
887
+ function parseScalar(text, kind, unescape) {
888
+ switch (kind) {
889
+ case "string":
890
+ return { value: unescape ? unescapeCytoscape(text) : text };
891
+ case "bool": {
892
+ const t = text.trim().toLowerCase();
893
+ if (t === "1" || t === "true" || t === "yes") {
894
+ return { value: true };
895
+ }
896
+ if (t === "0" || t === "false" || t === "no") {
897
+ return { value: false };
898
+ }
899
+ return null;
900
+ }
901
+ case "int":
902
+ case "long": {
903
+ const t = text.trim();
904
+ if (!INTEGER_TEXT.test(t)) {
905
+ return null;
906
+ }
907
+ const n = Number(t);
908
+ if (kind === "int" && (n < I32_MIN || n > I32_MAX)) {
909
+ return { value: n, overflow: Number.isSafeInteger(n) ? "i32" : "precision" };
910
+ }
911
+ return { value: n, overflow: Number.isSafeInteger(n) ? void 0 : "precision" };
912
+ }
913
+ case "real": {
914
+ const t = text.trim();
915
+ if (!REAL_TEXT.test(t)) {
916
+ return null;
917
+ }
918
+ const n = Number(t);
919
+ const overflow = !Number.isFinite(n) && !/Infinity|NaN/.test(t) ? "precision" : void 0;
920
+ return { value: n, overflow };
921
+ }
922
+ default: {
923
+ const name = kind;
924
+ throw new Error(`unknown scalar kind ${String(name)}`);
925
+ }
926
+ }
927
+ }
928
+ function unescapeCytoscape(text) {
929
+ return text.includes("\\") ? text.replace(/\\t/g, " ").replace(/\\n/g, "\n") : text;
930
+ }
931
+ const DTYPES = {
932
+ bool: "bool",
933
+ int: "i32",
934
+ long: "f64",
935
+ real: "f64",
936
+ string: "string"
937
+ };
938
+ class ColumnSet {
939
+ /**
940
+ * Create a column set.
941
+ * @param domain - the table
942
+ * @param report - the report
943
+ * @param options - the options
944
+ */
945
+ constructor(domain, report, options) {
946
+ this.plans = /* @__PURE__ */ new Map();
947
+ this.domain = domain;
948
+ this.report = report;
949
+ this.options = options;
950
+ }
951
+ /**
952
+ * Whether a column of that name (any namespace) was collected.
953
+ * @param name - the name
954
+ * @returns true when some row has a value or the column was declared
955
+ */
956
+ has(name) {
957
+ return this.plans.has(key(null, name));
958
+ }
959
+ /**
960
+ * Add one att of a row: parse it by its declared kind (E_BAD_VALUE for a value that does not
961
+ * parse; the cell stays unset). A second value of the same column on one row wins with
962
+ * W_DUPLICATE_ATTRIBUTE.
963
+ * @param row - the row
964
+ * @param att - the att
965
+ * @param where - the element, for issues
966
+ */
967
+ addAtt(row, att, where) {
968
+ const { name } = att;
969
+ if (name === null || name.length === 0) {
970
+ this.badAtt("an <att> without a name was skipped", att, "name");
971
+ return;
972
+ }
973
+ const plan = this.plan(name, att.namespace ?? null);
974
+ const type = attType(att);
975
+ plan.hidden ||= att.hidden;
976
+ plan.suid ||= name.endsWith(".SUID");
977
+ if (type.declared !== null) {
978
+ plan.declaredType ??= type.declared;
979
+ }
980
+ const at = { line: att.line, element: where.element ?? name };
981
+ if (type.unknown !== null) {
982
+ this.report.warnOnce(
983
+ "unsupported",
984
+ XGMML_ISSUE.UNKNOWN_ATTR_TYPE,
985
+ `att type "${type.unknown}" of "${name}" is not an XGMML or Cytoscape type; its values are kept as text`,
986
+ at,
987
+ `${XGMML_ISSUE.UNKNOWN_ATTR_TYPE}:${this.domain}:${name}`
988
+ );
989
+ }
990
+ let cell;
991
+ if (att.equation) {
992
+ plan.equation = true;
993
+ this.report.warnOnce(
994
+ "unsupported",
995
+ XGMML_ISSUE.EQUATION_AS_TEXT,
996
+ `"${name}" holds Cytoscape formulas; each is kept as its text, never evaluated`,
997
+ at,
998
+ `${XGMML_ISSUE.EQUATION_AS_TEXT}:${this.domain}:${name}`
999
+ );
1000
+ cell = att.value === null ? null : scalarCell("string", att.value, att.value, true);
1001
+ if (cell !== null) {
1002
+ plan.declared.add("string");
1003
+ }
1004
+ } else if (type.kind === "list") {
1005
+ cell = this.listCell(att, name, plan, at);
1006
+ plan.declared.add("list");
1007
+ } else if (type.kind === "map") {
1008
+ cell = jsonCell(recordOf(att));
1009
+ this.recordList(`"${name}" is a 2.x map; it is kept as a json record`, at);
1010
+ plan.declared.add("json");
1011
+ } else {
1012
+ cell = this.scalarAtt(att, type.kind, plan, at);
1013
+ }
1014
+ if (cell !== null) {
1015
+ this.set(plan, row, cell, at);
1016
+ }
1017
+ }
1018
+ /**
1019
+ * Add one value read by the 5.1 text grammar (an XML attribute of an element).
1020
+ * @param row - the row
1021
+ * @param name - the column name
1022
+ * @param text - the text
1023
+ * @param where - the element, for issues
1024
+ */
1025
+ addText(row, name, text, where) {
1026
+ const plan = this.plan(name, null);
1027
+ const value = parseTextCell(text);
1028
+ const kind = textKind(inferTextDtype(text));
1029
+ this.set(plan, row, { kind, value, text, items: [], itemKind: null, typed: false }, where);
1030
+ }
1031
+ /**
1032
+ * Add one value of a known kind (a typed table cell of a session).
1033
+ * @param row - the row
1034
+ * @param name - the column name
1035
+ * @param value - the value
1036
+ * @param where - the element, for issues
1037
+ */
1038
+ addJson(row, name, value, where) {
1039
+ this.set(this.plan(name, null), row, jsonCell(value), where);
1040
+ }
1041
+ /**
1042
+ * Declare every collected column on the sink and write its values.
1043
+ * @param sink - the sink
1044
+ * @param rowIndex - the sink index of a row (node or edge index), or -1 when the row was not added
1045
+ */
1046
+ write(sink, rowIndex) {
1047
+ const taken = new Set(this.options.reserved);
1048
+ const ordered = [...this.plans.values()].sort(
1049
+ (a, b) => Number(a.namespace !== null) - Number(b.namespace !== null)
1050
+ );
1051
+ for (const plan of ordered) {
1052
+ let { name } = plan;
1053
+ if (taken.has(name)) {
1054
+ name = uniqueColumnName(plan.name, plan.namespace, (n) => taken.has(n));
1055
+ this.report.warning(
1056
+ "coercion",
1057
+ XGMML_ISSUE.COLUMN_RENAMED,
1058
+ `${this.domain} attribute "${plan.name}" renamed to "${name}": the name was taken`,
1059
+ { element: plan.name }
1060
+ );
1061
+ }
1062
+ taken.add(name);
1063
+ this.writePlan(sink, plan, name, rowIndex);
1064
+ }
1065
+ }
1066
+ // ------------------------------------------------------------------ cells
1067
+ /**
1068
+ * The plan of a column, created on first use.
1069
+ * @param name - the attribute name
1070
+ * @param namespace - the table namespace, or null
1071
+ * @returns the plan
1072
+ */
1073
+ plan(name, namespace) {
1074
+ const k = key(namespace, name);
1075
+ let plan = this.plans.get(k);
1076
+ if (plan === void 0) {
1077
+ plan = {
1078
+ name,
1079
+ namespace,
1080
+ cells: /* @__PURE__ */ new Map(),
1081
+ declared: /* @__PURE__ */ new Set(),
1082
+ lines: /* @__PURE__ */ new Map(),
1083
+ declaredType: null,
1084
+ hidden: false,
1085
+ equation: false,
1086
+ suid: false,
1087
+ overflow: false,
1088
+ precision: false
1089
+ };
1090
+ this.plans.set(k, plan);
1091
+ }
1092
+ return plan;
1093
+ }
1094
+ /**
1095
+ * Store a cell, reporting a second value on the same row.
1096
+ * @param plan - the column
1097
+ * @param row - the row
1098
+ * @param cell - the cell
1099
+ * @param where - the location
1100
+ */
1101
+ set(plan, row, cell, where) {
1102
+ if (plan.cells.has(row)) {
1103
+ this.report.warning(
1104
+ "validation-error",
1105
+ XGMML_ISSUE.DUPLICATE_ATTRIBUTE,
1106
+ `"${plan.name}" is given twice on one ${this.domain}; the later value is kept`,
1107
+ { line: where.line ?? null, element: where.element ?? plan.name }
1108
+ );
1109
+ }
1110
+ plan.cells.set(row, cell);
1111
+ if (where.line !== void 0 && where.line !== null) {
1112
+ plan.lines.set(row, where.line);
1113
+ }
1114
+ }
1115
+ /**
1116
+ * Parse a scalar att.
1117
+ * @param att - the att
1118
+ * @param kind - the declared kind
1119
+ * @param plan - the column
1120
+ * @param at - the location
1121
+ * @returns the cell, or null when unset or invalid
1122
+ */
1123
+ scalarAtt(att, kind, plan, at) {
1124
+ if (att.children.length > 0) {
1125
+ this.badAtt(`the ${kind} att "${att.name ?? ""}" holds child atts; they are ignored`, att, "children");
1126
+ }
1127
+ if (att.xml !== null) {
1128
+ this.recordList(`"${att.name ?? ""}" holds foreign XML; it is kept as text in a json column`, at);
1129
+ plan.declared.add("json");
1130
+ return jsonCell(att.xml);
1131
+ }
1132
+ if (att.value === null) {
1133
+ plan.declared.add(kind);
1134
+ return null;
1135
+ }
1136
+ const parsed = parseScalar(att.value, kind, this.options.unescape);
1137
+ if (parsed === null || parsed.overflow !== void 0 && kind === "int" && exactInteger(att.cyType)) {
1138
+ this.badValue(att.value, kind, at);
1139
+ return null;
1140
+ }
1141
+ plan.declared.add(kind);
1142
+ if (parsed.overflow === "i32") {
1143
+ plan.overflow = true;
1144
+ } else if (parsed.overflow === "precision") {
1145
+ plan.precision = true;
1146
+ plan.overflow ||= kind === "int";
1147
+ }
1148
+ return scalarCell(kind, parsed.value, att.value, true);
1149
+ }
1150
+ /**
1151
+ * Parse a list att: items by `cy:elementType`, else by the widened kinds of the children.
1152
+ * A list of lists, a list with records, or a list of named fields is a json value.
1153
+ * @param att - the att
1154
+ * @param name - the column name
1155
+ * @param plan - the column (an item beyond i32 widens it, as a scalar does)
1156
+ * @param at - the location
1157
+ * @returns the cell
1158
+ */
1159
+ listCell(att, name, plan, at) {
1160
+ if (att.value !== null) {
1161
+ this.badAtt(`the list att "${name}" has a value; it is ignored`, att, "list-value");
1162
+ }
1163
+ const { children } = att;
1164
+ const names = new Set(children.map((c) => c.name).filter((n) => n !== null && n !== name));
1165
+ if (names.size > 1 || children.some((c) => c.children.length > 0 || c.xml !== null)) {
1166
+ this.recordList(`"${name}" is a record or a list of lists; it is kept as json`, at);
1167
+ return jsonCell(names.size > 1 ? recordOf(att) : children.map(jsonOfAtt));
1168
+ }
1169
+ let itemKind = elementKind(att.elementType);
1170
+ if (itemKind === null) {
1171
+ const kinds = /* @__PURE__ */ new Set();
1172
+ for (const child of children) {
1173
+ const type = attType(child);
1174
+ const kind = type.kind === "list" || type.kind === "map" ? "string" : type.kind;
1175
+ kinds.add(kind);
1176
+ itemKind = itemKind === null ? kind : widenScalar(itemKind, kind);
1177
+ }
1178
+ if (kinds.size > 1) {
1179
+ this.report.warnOnce(
1180
+ "coercion",
1181
+ XGMML_ISSUE.WIDENED,
1182
+ `the items of list "${name}" declare different types; they are read as ${itemKind ?? "string"}`,
1183
+ at,
1184
+ `${XGMML_ISSUE.WIDENED}:${this.domain}:${name}`
1185
+ );
1186
+ }
1187
+ }
1188
+ const items = [];
1189
+ for (const child of children) {
1190
+ if (child.value === null) {
1191
+ continue;
1192
+ }
1193
+ const parsed = parseScalar(child.value, itemKind ?? "string", this.options.unescape);
1194
+ const exact = exactInteger(att.elementType) || exactInteger(child.cyType);
1195
+ if (parsed === null || parsed.overflow !== void 0 && itemKind === "int" && exact) {
1196
+ this.badValue(child.value, itemKind ?? "string", { line: child.line, element: at.element });
1197
+ continue;
1198
+ }
1199
+ if (parsed.overflow === "i32") {
1200
+ plan.overflow = true;
1201
+ } else if (parsed.overflow === "precision") {
1202
+ plan.precision = true;
1203
+ plan.overflow ||= itemKind === "int";
1204
+ }
1205
+ items.push({ value: parsed.value, text: child.value });
1206
+ }
1207
+ return { kind: "list", value: null, text: "", items, itemKind, typed: true };
1208
+ }
1209
+ // ------------------------------------------------------------------ decision and writing
1210
+ /**
1211
+ * Decide one column's dtype, declare it and write its values.
1212
+ * @param sink - the sink
1213
+ * @param plan - the column
1214
+ * @param name - the name to declare it under
1215
+ * @param rowIndex - the row to sink index map
1216
+ */
1217
+ writePlan(sink, plan, name, rowIndex) {
1218
+ const cells = [...plan.cells.values()];
1219
+ const decision = this.decide(plan, cells);
1220
+ const decl = {
1221
+ name,
1222
+ dtype: decision.dtype,
1223
+ nullable: true,
1224
+ origin: {
1225
+ format: FORMAT,
1226
+ id: null,
1227
+ title: name === plan.name ? null : plan.name,
1228
+ type: originType(plan, decision),
1229
+ namespace: plan.namespace ?? (plan.hidden ? CYTOSCAPE_ORIGIN_NAMESPACE : null)
1230
+ }
1231
+ };
1232
+ if (decision.itemDtype !== null) {
1233
+ decl.itemDtype = decision.itemDtype;
1234
+ }
1235
+ const extra = {};
1236
+ if (plan.hidden) {
1237
+ extra.hidden = true;
1238
+ }
1239
+ if (plan.suid) {
1240
+ extra.suidReference = true;
1241
+ }
1242
+ if (plan.equation) {
1243
+ extra.equation = true;
1244
+ }
1245
+ if (Object.keys(extra).length > 0) {
1246
+ decl.extra = extra;
1247
+ }
1248
+ const where = { element: name };
1249
+ if (this.domain === "graph") {
1250
+ const cell = plan.cells.get(0);
1251
+ try {
1252
+ sink.setGraphValue(name, cell === void 0 ? void 0 : convert(cell, decision), decl);
1253
+ } catch (err) {
1254
+ this.report.recordError(err, where);
1255
+ }
1256
+ return;
1257
+ }
1258
+ let handle;
1259
+ try {
1260
+ ({ handle } = declareResolved(sink, this.domain, decl, this.report, where));
1261
+ } catch (err) {
1262
+ this.report.recordError(err, where);
1263
+ return;
1264
+ }
1265
+ for (const [row, cell] of plan.cells) {
1266
+ const index = rowIndex(row);
1267
+ if (index < 0) {
1268
+ continue;
1269
+ }
1270
+ const value = convert(cell, decision);
1271
+ try {
1272
+ if (this.domain === "node") {
1273
+ sink.setNodeValue(handle, index, value);
1274
+ } else {
1275
+ sink.setEdgeValue(handle, index, value);
1276
+ }
1277
+ } catch (err) {
1278
+ this.report.recordError(err, { line: plan.lines.get(row) ?? null, element: name });
1279
+ }
1280
+ }
1281
+ }
1282
+ /**
1283
+ * The dtype of a column from its cells, with the widening and precision warnings.
1284
+ * @param plan - the column
1285
+ * @param cells - its cells
1286
+ * @returns the decision
1287
+ */
1288
+ decide(plan, cells) {
1289
+ const where = { element: plan.name };
1290
+ const typedKinds = new Set(cells.filter((c) => c.typed).map((c) => c.kind));
1291
+ let widened = typedKinds.size > 1 || plan.declared.size > 1;
1292
+ let decision;
1293
+ if (cells.some((c) => c.kind === "json") || cells.some((c) => c.kind === "list") && cells.some((c) => c.kind !== "list")) {
1294
+ decision = { dtype: "json", itemDtype: null, scalar: null, item: null, long: false };
1295
+ } else if (cells.length > 0 && cells.every((c) => c.kind === "list")) {
1296
+ let item = null;
1297
+ for (const cell of cells) {
1298
+ if (cell.itemKind !== null) {
1299
+ widened ||= item !== null && item !== cell.itemKind;
1300
+ item = item === null ? cell.itemKind : widenScalar(item, cell.itemKind);
1301
+ }
1302
+ }
1303
+ if (item === null) {
1304
+ this.report.warnOnce(
1305
+ "coercion",
1306
+ XGMML_ISSUE.EMPTY_LIST_TYPE,
1307
+ `list "${plan.name}" has no element type and no items; it is a list of strings`,
1308
+ where,
1309
+ `${XGMML_ISSUE.EMPTY_LIST_TYPE}:${this.domain}:${plan.name}`
1310
+ );
1311
+ item = "string";
1312
+ }
1313
+ if (plan.overflow && item === "int") {
1314
+ item = "real";
1315
+ widened = true;
1316
+ }
1317
+ decision = { dtype: "list", itemDtype: DTYPES[item], scalar: null, item, long: item === "long" };
1318
+ } else {
1319
+ decision = this.decideScalar(plan, cells);
1320
+ widened ||= plan.overflow && decision.scalar === "real" && plan.declared.has("int");
1321
+ }
1322
+ if (widened) {
1323
+ this.report.warnOnce(
1324
+ "coercion",
1325
+ XGMML_ISSUE.WIDENED,
1326
+ `${this.domain} column "${plan.name}" was widened to ${decision.dtype}: its values or declared types disagree`,
1327
+ where,
1328
+ `${XGMML_ISSUE.WIDENED}:${this.domain}:${plan.name}`
1329
+ );
1330
+ }
1331
+ if (plan.precision) {
1332
+ this.report.warnOnce(
1333
+ "precision",
1334
+ XGMML_ISSUE.PRECISION,
1335
+ `values of "${plan.name}" beyond what a double holds exactly are stored as the nearest double (or infinity)`,
1336
+ where,
1337
+ `${XGMML_ISSUE.PRECISION}:${this.domain}:${plan.name}`
1338
+ );
1339
+ }
1340
+ return decision;
1341
+ }
1342
+ /**
1343
+ * The scalar decision of a column.
1344
+ * @param plan - the column
1345
+ * @param cells - its cells
1346
+ * @returns the decision
1347
+ */
1348
+ decideScalar(plan, cells) {
1349
+ let scalar = null;
1350
+ for (const cell of cells) {
1351
+ const kind = cell.kind;
1352
+ scalar = scalar === null ? kind : widenScalar(scalar, kind);
1353
+ }
1354
+ if (scalar === null) {
1355
+ const declared = [...plan.declared].find((k) => k in DTYPES);
1356
+ scalar = declared ?? "string";
1357
+ }
1358
+ if (scalar === "int" && plan.overflow) {
1359
+ scalar = "real";
1360
+ }
1361
+ const long = scalar === "long";
1362
+ if (long && this.options.long === "string") {
1363
+ return { dtype: "string", itemDtype: null, scalar: "string", item: null, long: true };
1364
+ }
1365
+ let dtype = DTYPES[scalar];
1366
+ if (scalar === "string") {
1367
+ const heuristic = new DictHeuristic();
1368
+ for (const cell of cells) {
1369
+ if (heuristic.observe(cell.text)) {
1370
+ break;
1371
+ }
1372
+ }
1373
+ dtype = heuristic.decide();
1374
+ }
1375
+ return { dtype, itemDtype: null, scalar, item: null, long };
1376
+ }
1377
+ /**
1378
+ * Report a malformed att, once per kind of defect.
1379
+ * @param message - the message
1380
+ * @param att - the att
1381
+ * @param kind - the defect, for the once key
1382
+ */
1383
+ badAtt(message, att, kind) {
1384
+ this.report.warnOnce(
1385
+ "parse-error",
1386
+ XGMML_ISSUE.BAD_ATT,
1387
+ message,
1388
+ { line: att.line, element: att.name },
1389
+ `${XGMML_ISSUE.BAD_ATT}:${kind}`
1390
+ );
1391
+ }
1392
+ /**
1393
+ * Report a value that does not parse as its declared kind (an error; the cell stays unset).
1394
+ * @param text - the value
1395
+ * @param kind - the kind
1396
+ * @param at - the location
1397
+ */
1398
+ badValue(text, kind, at) {
1399
+ this.report.error(
1400
+ "validation-error",
1401
+ XGMML_ISSUE.BAD_VALUE,
1402
+ `value ${JSON.stringify(text)} is not a valid ${KIND_NAMES[kind]}; the cell is left unset`,
1403
+ at
1404
+ );
1405
+ }
1406
+ /**
1407
+ * Report a record list, list of lists, map or foreign XML kept as json, once.
1408
+ * @param message - the message
1409
+ * @param at - the location
1410
+ */
1411
+ recordList(message, at) {
1412
+ this.report.warnOnce("coercion", XGMML_ISSUE.RECORD_LIST, message, at);
1413
+ }
1414
+ }
1415
+ function exactInteger(cyType) {
1416
+ return cyType?.trim().toLowerCase() === "integer";
1417
+ }
1418
+ function originType(plan, decision) {
1419
+ if (plan.equation) {
1420
+ return "equation";
1421
+ }
1422
+ return decision.long ? "long" : plan.declaredType;
1423
+ }
1424
+ const KIND_NAMES = {
1425
+ bool: "boolean (1, 0, true, false, yes, no)",
1426
+ int: "integer",
1427
+ long: "long integer",
1428
+ real: "real number",
1429
+ string: "string"
1430
+ };
1431
+ function key(namespace, name) {
1432
+ return namespace === null ? name : `${namespace}\0${name}`;
1433
+ }
1434
+ function textKind(dtype) {
1435
+ switch (dtype) {
1436
+ case "bool":
1437
+ return "bool";
1438
+ case "i32":
1439
+ return "int";
1440
+ case "f64":
1441
+ return "real";
1442
+ default:
1443
+ return "string";
1444
+ }
1445
+ }
1446
+ function scalarCell(kind, value, text, typed) {
1447
+ return { kind, value, text, items: [], itemKind: null, typed };
1448
+ }
1449
+ function jsonCell(value) {
1450
+ return { kind: "json", value, text: JSON.stringify(value), items: [], itemKind: null, typed: true };
1451
+ }
1452
+ function recordOf(att) {
1453
+ const out = {};
1454
+ for (const child of att.children) {
1455
+ out[child.name ?? ""] = jsonOfAtt(child);
1456
+ }
1457
+ return out;
1458
+ }
1459
+ function jsonOfAtt(att) {
1460
+ if (att.xml !== null) {
1461
+ return att.xml;
1462
+ }
1463
+ if (att.children.length === 0) {
1464
+ return att.value;
1465
+ }
1466
+ return isListLike(att) ? att.children.map(jsonOfAtt) : recordOf(att);
1467
+ }
1468
+ function convert(cell, decision) {
1469
+ if (decision.dtype === "json") {
1470
+ if (cell.kind === "json") {
1471
+ return cell.value;
1472
+ }
1473
+ return cell.kind === "list" ? cell.items.map((i) => i.value) : cell.value;
1474
+ }
1475
+ if (decision.dtype === "list") {
1476
+ return cell.items.map((item) => convertScalar(item.value, item.text, decision.item ?? "string"));
1477
+ }
1478
+ return convertScalar(
1479
+ cell.value,
1480
+ cell.text,
1481
+ decision.long && decision.dtype === "string" ? "string" : decision.scalar ?? "string"
1482
+ );
1483
+ }
1484
+ function convertScalar(value, text, kind) {
1485
+ switch (kind) {
1486
+ case "bool":
1487
+ return value;
1488
+ case "int":
1489
+ case "long":
1490
+ case "real":
1491
+ return Number(value);
1492
+ default:
1493
+ return typeof value === "string" ? value : text;
1494
+ }
1495
+ }
1496
+ const META_ATTS = /* @__PURE__ */ new Set(["documentVersion", "networkMetadata"]);
1497
+ const GRAPH_STRUCTURE = /* @__PURE__ */ new Set([
1498
+ "id",
1499
+ "label",
1500
+ "directed",
1501
+ "xlink:href",
1502
+ "cy:documentVersion",
1503
+ "cy:view",
1504
+ "cy:registered",
1505
+ "cy:networkId",
1506
+ "cy:visualStyle",
1507
+ "cy:rendererId"
1508
+ ]);
1509
+ const ABORT_CHECK_INTERVAL = 64;
1510
+ const ALIAS_SPLIT = /[()]/;
1511
+ function dialectOf(doc, report) {
1512
+ const { root } = doc;
1513
+ let versionText = root.attrs.get("cy:documentVersion") ?? null;
1514
+ for (const att of root.atts) {
1515
+ if (att.name === "documentVersion" && att.value !== null) {
1516
+ versionText ??= att.value;
1517
+ }
1518
+ }
1519
+ let version = 0;
1520
+ if (versionText !== null) {
1521
+ version = Number(versionText.trim());
1522
+ if (!Number.isFinite(version) || versionText.trim().length === 0) {
1523
+ report.warning(
1524
+ "validation-error",
1525
+ XGMML_ISSUE.DOCUMENT_VERSION,
1526
+ `documentVersion "${versionText}" does not parse; the dialect is chosen from the content`,
1527
+ { line: root.line }
1528
+ );
1529
+ version = 0;
1530
+ }
1531
+ }
1532
+ const registered = root.subgraphs.some((g) => isCyTrue(g.registered));
1533
+ return {
1534
+ versionText,
1535
+ version,
1536
+ cytoscape: doc.cytoscape,
1537
+ cy2: doc.cytoscape && version < 3,
1538
+ session: root.registered !== null && !isCyTrue(root.registered) && registered,
1539
+ view: isCyTrue(root.attrs.get("cy:view"))
1540
+ };
1541
+ }
1542
+ function graphsOf(doc, dialect) {
1543
+ return dialect.session ? doc.root.subgraphs.filter((g) => isCyTrue(g.registered)) : [doc.root];
1544
+ }
1545
+ function localRef(href) {
1546
+ return href.startsWith("#") ? href.slice(1) : null;
1547
+ }
1548
+ function membersOf(doc, graph) {
1549
+ if (graph === null) {
1550
+ return { nodes: doc.nodes, edges: doc.edges, dangling: 0 };
1551
+ }
1552
+ const nodeById = indexById(doc.nodes);
1553
+ const edgeById = indexById(doc.edges);
1554
+ const nodes = [];
1555
+ const edges = [];
1556
+ let dangling = 0;
1557
+ for (const member of graph.members) {
1558
+ if (member.href === null) {
1559
+ if (member.kind === "node") {
1560
+ nodes.push(member);
1561
+ } else {
1562
+ edges.push(member);
1563
+ }
1564
+ continue;
1565
+ }
1566
+ const id = localRef(member.href);
1567
+ let target;
1568
+ if (id !== null) {
1569
+ target = member.kind === "node" ? nodeById.get(id) : edgeById.get(id);
1570
+ }
1571
+ if (target === void 0) {
1572
+ dangling++;
1573
+ } else if (target.kind === "node") {
1574
+ nodes.push(target);
1575
+ } else {
1576
+ edges.push(target);
1577
+ }
1578
+ }
1579
+ return { nodes, edges, dangling };
1580
+ }
1581
+ function indexById(records) {
1582
+ const out = /* @__PURE__ */ new Map();
1583
+ for (const record of records) {
1584
+ if (record.id !== null && !out.has(record.id)) {
1585
+ out.set(record.id, record);
1586
+ }
1587
+ }
1588
+ return out;
1589
+ }
1590
+ class XgmmlEmitter {
1591
+ /**
1592
+ * Create an emitter for one graph.
1593
+ * @param doc - the document
1594
+ * @param dialect - its dialect
1595
+ * @param sink - the sink
1596
+ * @param report - the report
1597
+ * @param options - the resolved common options
1598
+ * @param settings - the XGMML options
1599
+ * @param extras - what a session adds
1600
+ */
1601
+ constructor(doc, dialect, sink, report, options, settings, extras = {}) {
1602
+ this.rows = /* @__PURE__ */ new Map();
1603
+ this.rowList = [];
1604
+ this.edgeIndex = [];
1605
+ this.sinceCheck = 0;
1606
+ this.aliasResolved = 0;
1607
+ this.aliasInteractions = 0;
1608
+ this.weighted = false;
1609
+ this.directions = /* @__PURE__ */ new Map();
1610
+ this.labelRows = null;
1611
+ this.doc = doc;
1612
+ this.dialect = dialect;
1613
+ this.sink = sink;
1614
+ this.report = report;
1615
+ this.options = options;
1616
+ this.settings = settings;
1617
+ this.extras = extras;
1618
+ this.coercer = new IdCoercer(options.ids);
1619
+ this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
1620
+ const base = { unescape: settings.cytoscapeEscapes, long: options.long };
1621
+ this.nodeColumns = new ColumnSet("node", report, {
1622
+ ...base,
1623
+ reserved: /* @__PURE__ */ new Set([
1624
+ LABEL_COLUMN,
1625
+ POSITION_COLUMN,
1626
+ Z_COLUMN,
1627
+ GRAPHICS_COLUMN,
1628
+ PARENT_COLUMN,
1629
+ PARENTS_COLUMN,
1630
+ SUBGRAPH_COLUMN,
1631
+ NESTED_NETWORK_COLUMN,
1632
+ NETWORK_POINTER_COLUMN,
1633
+ NETWORKS_COLUMN
1634
+ ])
1635
+ });
1636
+ this.edgeColumns = new ColumnSet("edge", report, {
1637
+ ...base,
1638
+ reserved: /* @__PURE__ */ new Set([EDGE_ID_COLUMN, LABEL_COLUMN, GRAPHICS_COLUMN])
1639
+ });
1640
+ this.graphColumns = new ColumnSet("graph", report, { ...base, reserved: /* @__PURE__ */ new Set([GRAPHICS_COLUMN]) });
1641
+ }
1642
+ /**
1643
+ * Push the graph: nodes, containment, positions, edges, columns, metadata.
1644
+ * @param graph - the subnetwork, or null for the whole document
1645
+ */
1646
+ emit(graph) {
1647
+ const members = membersOf(this.doc, graph);
1648
+ if (members.dangling > 0) {
1649
+ this.report.warning(
1650
+ "validation-error",
1651
+ XGMML_ISSUE.DANGLING_REFERENCE,
1652
+ `${this.prefix()}${members.dangling} xlink:href member reference(s) name no node or edge of the document`,
1653
+ { line: graph?.line ?? null }
1654
+ );
1655
+ }
1656
+ const header = this.headerDirected();
1657
+ this.direction.setHeader(this.sinkDirection(members.edges, header), { line: this.doc.root.line });
1658
+ this.addNodes(members.nodes);
1659
+ if (graph === null) {
1660
+ this.checkReferences();
1661
+ }
1662
+ const containment = new Containment(this);
1663
+ containment.resolve(this.rowList, this.groupTest());
1664
+ this.writeNodeExtras(graph);
1665
+ this.addEdges(members.edges, header);
1666
+ this.finishAliases();
1667
+ this.writeGraph(graph);
1668
+ const nodeIndex = (row) => this.rowList[row].index;
1669
+ this.nodeColumns.write(this.sink, nodeIndex);
1670
+ this.edgeColumns.write(this.sink, (row) => this.edgeIndex[row] ?? -1);
1671
+ containment.write();
1672
+ this.writeMeta(graph);
1673
+ throwIfAborted(this.options.signal);
1674
+ }
1675
+ // ------------------------------------------------------------------ shared helpers (also used by Containment)
1676
+ /**
1677
+ * The sink.
1678
+ * @returns the sink
1679
+ */
1680
+ get target() {
1681
+ return this.sink;
1682
+ }
1683
+ /**
1684
+ * The report.
1685
+ * @returns the report
1686
+ */
1687
+ get issues() {
1688
+ return this.report;
1689
+ }
1690
+ /**
1691
+ * The row of a node id in this graph.
1692
+ * @param id - the id text
1693
+ * @returns the row, or undefined
1694
+ */
1695
+ rowOf(id) {
1696
+ return this.rows.get(id);
1697
+ }
1698
+ /**
1699
+ * The network name a cross-file pointer names, when a session can tell.
1700
+ * @param href - the pointer
1701
+ * @returns the name, or null
1702
+ */
1703
+ resolvePointer(href) {
1704
+ return this.extras.resolvePointer?.(href) ?? null;
1705
+ }
1706
+ /**
1707
+ * Hand a group member the graph does not hold to the session, when one is reading.
1708
+ * @param group - the group node's id
1709
+ * @param member - the member's id
1710
+ * @returns true when the session took it
1711
+ */
1712
+ missingMember(group, member) {
1713
+ if (this.extras.onMissingMember === void 0) {
1714
+ return false;
1715
+ }
1716
+ this.extras.onMissingMember(group, member);
1717
+ return true;
1718
+ }
1719
+ /**
1720
+ * The first graph of the document with an id.
1721
+ * @param id - the id
1722
+ * @returns the graph, or undefined
1723
+ */
1724
+ graphById(id) {
1725
+ return this.doc.graphs.find((g) => g.id === id);
1726
+ }
1727
+ /**
1728
+ * The prefix of every message (the session entry), or "".
1729
+ * @returns the prefix
1730
+ */
1731
+ prefix() {
1732
+ return this.extras.entry === void 0 ? "" : `${this.extras.entry}: `;
1733
+ }
1734
+ /**
1735
+ * Check the cancellation signal every few dozen elements.
1736
+ */
1737
+ checkAbort() {
1738
+ if (++this.sinceCheck >= ABORT_CHECK_INTERVAL) {
1739
+ this.sinceCheck = 0;
1740
+ throwIfAborted(this.options.signal);
1741
+ }
1742
+ }
1743
+ /**
1744
+ * Declare a column the importer owns on first use (the 5.6 rename rule applies).
1745
+ * @param domain - node or edge
1746
+ * @param decl - the declaration
1747
+ * @returns the handle, or null when the sink refused it (recorded)
1748
+ */
1749
+ declare(domain, decl) {
1750
+ try {
1751
+ return declareResolved(this.sink, domain, decl, this.report, { element: decl.name }).handle;
1752
+ } catch (err) {
1753
+ this.report.recordError(err, { element: decl.name });
1754
+ return null;
1755
+ }
1756
+ }
1757
+ // ------------------------------------------------------------------ nodes
1758
+ /**
1759
+ * The header direction: the root's `directed` (0 by the DTD), else the defaultDirected option.
1760
+ * @returns whether the graph is directed by default
1761
+ */
1762
+ headerDirected() {
1763
+ const text = this.doc.root.attrs.get("directed");
1764
+ if (text === void 0) {
1765
+ return this.options.defaultDirected;
1766
+ }
1767
+ return this.flag(text, this.options.defaultDirected, { line: this.doc.root.line, element: "directed" });
1768
+ }
1769
+ /**
1770
+ * The direction of one edge: its cy:directed, else the header.
1771
+ * @param record - the edge
1772
+ * @param header - the header direction
1773
+ * @returns whether the edge is directed
1774
+ */
1775
+ edgeDirection(record, header) {
1776
+ let directed = this.directions.get(record);
1777
+ if (directed === void 0) {
1778
+ directed = record.directed === null ? header : this.flag(record.directed, header, { line: record.line, element: record.id ?? record.label });
1779
+ this.directions.set(record, directed);
1780
+ }
1781
+ return directed;
1782
+ }
1783
+ /**
1784
+ * The direction the sink starts with: the one every edge has when they all agree (a
1785
+ * Cytoscape file writes cy:directed on every edge and may omit the root attribute), else the
1786
+ * header, so a graph of one direction is never expanded into a mixed one.
1787
+ * @param edges - the edges of the graph
1788
+ * @param header - the header direction
1789
+ * @returns the sink direction
1790
+ */
1791
+ sinkDirection(edges, header) {
1792
+ let first = null;
1793
+ for (const edge of edges) {
1794
+ const directed = this.edgeDirection(edge, header);
1795
+ if (first === null) {
1796
+ first = directed;
1797
+ } else if (first !== directed) {
1798
+ return header;
1799
+ }
1800
+ }
1801
+ return first ?? header;
1802
+ }
1803
+ /**
1804
+ * A 0 / 1 direction flag: true, false, yes and no are accepted with a warning; anything else
1805
+ * falls back.
1806
+ * @param text - the text
1807
+ * @param fallback - the direction for an unreadable value
1808
+ * @param where - the location
1809
+ * @returns the direction
1810
+ */
1811
+ flag(text, fallback, where) {
1812
+ const t = text.trim();
1813
+ if (t === "1" || t === "0") {
1814
+ return t === "1";
1815
+ }
1816
+ const lower = t.toLowerCase();
1817
+ const readable = lower === "true" || lower === "yes" || lower === "false" || lower === "no";
1818
+ this.report.warnOnce(
1819
+ "validation-error",
1820
+ XGMML_ISSUE.BAD_DIRECTED,
1821
+ readable ? `${this.prefix()}directed="${text}" is not 0 or 1; read as ${lower === "true" || lower === "yes" ? "1" : "0"}` : `${this.prefix()}directed="${text}" is not 0 or 1; read as ${fallback ? "directed" : "undirected"}`,
1822
+ where,
1823
+ `${XGMML_ISSUE.BAD_DIRECTED}:${text}`
1824
+ );
1825
+ return readable ? lower === "true" || lower === "yes" : fallback;
1826
+ }
1827
+ /**
1828
+ * Add every node record to the sink (one node per id; later declarations merge).
1829
+ * @param records - the node records
1830
+ */
1831
+ addNodes(records) {
1832
+ for (const record of records) {
1833
+ const { id } = record;
1834
+ if (id === null) {
1835
+ continue;
1836
+ }
1837
+ const existing = this.rows.get(id);
1838
+ if (existing !== void 0) {
1839
+ existing.records.push(record);
1840
+ this.report.warning(
1841
+ "validation-error",
1842
+ XGMML_ISSUE.DUPLICATE_NODE,
1843
+ `${this.prefix()}node "${id}" is declared twice; the declarations are merged`,
1844
+ { line: record.line, element: id }
1845
+ );
1846
+ continue;
1847
+ }
1848
+ const row = { id, records: [record], index: -1, row: this.rowList.length };
1849
+ try {
1850
+ row.index = this.sink.addNode(this.nodeId(id, record.line));
1851
+ this.report.counts.nodes++;
1852
+ } catch (err) {
1853
+ this.report.counts.skippedNodes++;
1854
+ this.report.recordError(err, { line: record.line, element: id });
1855
+ }
1856
+ this.rows.set(id, row);
1857
+ this.rowList.push(row);
1858
+ this.checkAbort();
1859
+ }
1860
+ }
1861
+ /**
1862
+ * The coerced id of a node text.
1863
+ * @param text - the id text
1864
+ * @param line - the line
1865
+ * @returns the id
1866
+ */
1867
+ nodeId(text, line) {
1868
+ const id = this.coercer.text(text);
1869
+ const merge = this.coercer.lastMerge;
1870
+ if (merge !== null) {
1871
+ this.report.warning(
1872
+ "coercion",
1873
+ XGMML_ISSUE.ID_MERGED,
1874
+ `id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`,
1875
+ { line, element: text }
1876
+ );
1877
+ }
1878
+ return id;
1879
+ }
1880
+ /**
1881
+ * Whether a node's nested graph is a group (containment) rather than a nested-network
1882
+ * pointer: any graph in the 1.0 draft dialect, a 2.x node with `__groupState`, a 3.x node with
1883
+ * `__isGroup` true, or a node the session lists.
1884
+ * @returns the test
1885
+ */
1886
+ groupTest() {
1887
+ const { groupNodes } = this.extras;
1888
+ const { cytoscape } = this.dialect;
1889
+ return (node) => {
1890
+ if (!cytoscape) {
1891
+ return true;
1892
+ }
1893
+ if (node.id !== null && groupNodes?.has(node.id) === true) {
1894
+ return true;
1895
+ }
1896
+ return node.atts.some((a) => a.name === "__groupState" || a.name === "__isGroup" && isCyTrue(a.value));
1897
+ };
1898
+ }
1899
+ /**
1900
+ * W_DANGLING_REFERENCE for the `xlink:href` members of a generic document's non-group graphs
1901
+ * that name no node or edge (a group's are E_UNKNOWN_PARENT, reported by the containment).
1902
+ */
1903
+ checkReferences() {
1904
+ const isGroup = this.groupTest();
1905
+ const edgeIds = new Set(this.doc.edges.map((e) => e.id));
1906
+ let dangling = 0;
1907
+ for (const graph of this.doc.graphs) {
1908
+ if (graph.owner !== null && isGroup(graph.owner)) {
1909
+ continue;
1910
+ }
1911
+ for (const member of graph.members) {
1912
+ if (member.href === null) {
1913
+ continue;
1914
+ }
1915
+ const id = localRef(member.href);
1916
+ const known = id !== null && (member.kind === "node" ? this.rows.has(id) : edgeIds.has(id));
1917
+ if (!known) {
1918
+ dangling++;
1919
+ }
1920
+ }
1921
+ }
1922
+ if (dangling > 0) {
1923
+ this.report.warning(
1924
+ "validation-error",
1925
+ XGMML_ISSUE.DANGLING_REFERENCE,
1926
+ `${this.prefix()}${dangling} xlink:href reference(s) name no node or edge of the document`
1927
+ );
1928
+ }
1929
+ }
1930
+ /**
1931
+ * Write the per-node values: label, attributes, XML attributes, graphics, positions, nested
1932
+ * network pointers and subgraph memberships.
1933
+ * @param graph - the subnetwork, or null
1934
+ */
1935
+ writeNodeExtras(graph) {
1936
+ const labels = [];
1937
+ const positions = new Positions(this, this.dialect.cytoscape ? this.settings.zAs : "position");
1938
+ const graphics = new JsonColumn(this, "node", GRAPHICS_COLUMN, XGMML_ORIGIN_NAMESPACE, "graphics");
1939
+ const pointers = new Pointers(this, this.groupTest());
1940
+ for (const row of this.rowList) {
1941
+ const where = { line: row.records[0].line, element: row.id };
1942
+ const seen = /* @__PURE__ */ new Set();
1943
+ for (const record of row.records) {
1944
+ labels[row.row] ??= record.label ?? void 0;
1945
+ this.addElementColumns(this.nodeColumns, row.row, record, seen, where);
1946
+ positions.add(row, record);
1947
+ if (record.graphics !== null && Object.keys(record.graphics).length > 0) {
1948
+ graphics.set(row.index, record.graphics);
1949
+ }
1950
+ pointers.add(row, record);
1951
+ }
1952
+ this.checkAbort();
1953
+ }
1954
+ this.writeLabels("node", labels, (row) => this.rowList[row].index);
1955
+ positions.write();
1956
+ pointers.write();
1957
+ if (graph === null) {
1958
+ this.writeNetworks();
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Add an element's XML attributes (text grammar) and atts to a column set; atts of a later
1963
+ * duplicate declaration only fill names the earlier ones left unset.
1964
+ * @param columns - the column set
1965
+ * @param row - the row
1966
+ * @param record - the node or edge
1967
+ * @param seen - the names set by earlier declarations of the same node
1968
+ * @param where - the location
1969
+ */
1970
+ addElementColumns(columns, row, record, seen, where) {
1971
+ const merging = seen.size > 0;
1972
+ const names = /* @__PURE__ */ new Set();
1973
+ if (record.name !== null) {
1974
+ if (!(merging && seen.has("name"))) {
1975
+ columns.addText(row, "name", record.name, where);
1976
+ }
1977
+ names.add("name");
1978
+ }
1979
+ for (const [key2, value] of record.xmlAttrs) {
1980
+ if (!(merging && seen.has(key2))) {
1981
+ columns.addText(row, key2, value, where);
1982
+ }
1983
+ names.add(key2);
1984
+ }
1985
+ for (const att of record.atts) {
1986
+ if (this.skipAtt(record, att) || merging && att.name !== null && seen.has(att.name)) {
1987
+ continue;
1988
+ }
1989
+ if (record.kind === "node" && att.name === "__isGroup" && record.nested.length > 0 && isCyTrue(att.value)) {
1990
+ continue;
1991
+ }
1992
+ if (att.hasGraph && att.name === null) {
1993
+ continue;
1994
+ }
1995
+ columns.addAtt(row, att, where);
1996
+ if (att.name !== null) {
1997
+ names.add(att.name);
1998
+ }
1999
+ }
2000
+ for (const name of names) {
2001
+ seen.add(name);
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Whether an att is not a column: an edge's weight att under weightFrom.
2006
+ * @param record - the element
2007
+ * @param att - the att
2008
+ * @returns true to skip it
2009
+ */
2010
+ skipAtt(record, att) {
2011
+ return record.kind === "edge" && record.weight === null && att.name === this.options.weightFrom;
2012
+ }
2013
+ /**
2014
+ * Write a label column (role label).
2015
+ * @param domain - node or edge
2016
+ * @param labels - the label of each row
2017
+ * @param index - the sink index of a row
2018
+ */
2019
+ writeLabels(domain, labels, index) {
2020
+ if (!labels.some((l) => l !== void 0)) {
2021
+ return;
2022
+ }
2023
+ const handle = this.declare(domain, {
2024
+ name: LABEL_COLUMN,
2025
+ dtype: "string",
2026
+ role: "label",
2027
+ nullable: true,
2028
+ origin: { format: FORMAT, id: null, title: null, type: "label", namespace: null }
2029
+ });
2030
+ if (handle === null) {
2031
+ return;
2032
+ }
2033
+ labels.forEach((label, row) => {
2034
+ const i = index(row);
2035
+ if (label !== void 0 && i >= 0) {
2036
+ if (domain === "node") {
2037
+ this.sink.setNodeValue(handle, i, label);
2038
+ } else {
2039
+ this.sink.setEdgeValue(handle, i, label);
2040
+ }
2041
+ }
2042
+ });
2043
+ }
2044
+ /** The `xgmml.networks` membership of nodes in root-level subgraphs of a generic document. */
2045
+ writeNetworks() {
2046
+ const { subgraphs } = this.doc.root;
2047
+ if (subgraphs.length === 0) {
2048
+ return;
2049
+ }
2050
+ const member = /* @__PURE__ */ new Map();
2051
+ for (const graph of subgraphs) {
2052
+ const name = graph.id ?? graph.label ?? "";
2053
+ for (const m of graph.members) {
2054
+ const id = m.kind === "node" ? m.id ?? (m.href === null ? null : localRef(m.href)) : null;
2055
+ const row = id === null ? void 0 : this.rows.get(id);
2056
+ if (row !== void 0 && row.index >= 0) {
2057
+ const list = member.get(row.index) ?? [];
2058
+ list.push(name);
2059
+ member.set(row.index, list);
2060
+ }
2061
+ }
2062
+ }
2063
+ if (member.size === 0) {
2064
+ return;
2065
+ }
2066
+ const handle = this.declare("node", {
2067
+ name: NETWORKS_COLUMN,
2068
+ dtype: "list",
2069
+ itemDtype: "string",
2070
+ nullable: true,
2071
+ origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: XGMML_ORIGIN_NAMESPACE }
2072
+ });
2073
+ if (handle !== null) {
2074
+ for (const [index, list] of member) {
2075
+ this.sink.setNodeValue(handle, index, list);
2076
+ }
2077
+ }
2078
+ }
2079
+ // ------------------------------------------------------------------ edges
2080
+ /**
2081
+ * Push every edge record.
2082
+ * @param records - the edge records
2083
+ * @param header - the header direction
2084
+ */
2085
+ addEdges(records, header) {
2086
+ const ids = /* @__PURE__ */ new Set();
2087
+ const dedupe = this.dialect.cy2 && this.doc.nodes.some((n) => n.nested.length > 0) ? /* @__PURE__ */ new Set() : null;
2088
+ const labels = [];
2089
+ const edgeIds = [];
2090
+ const graphics = [];
2091
+ let row = 0;
2092
+ for (const record of records) {
2093
+ const where = { line: record.line, element: record.id ?? record.label };
2094
+ const key2 = record.id ?? record.label;
2095
+ if (dedupe !== null && key2 !== null) {
2096
+ if (dedupe.has(key2)) {
2097
+ this.report.warnOnce(
2098
+ "merged",
2099
+ XGMML_ISSUE.GROUP_DUPLICATE_EDGE,
2100
+ `${this.prefix()}the Cytoscape 2.x writer repeats the edges of a group; each repeat is dropped`,
2101
+ where
2102
+ );
2103
+ continue;
2104
+ }
2105
+ dedupe.add(key2);
2106
+ }
2107
+ const index = this.addEdge(record, header, ids, where);
2108
+ this.checkAbort();
2109
+ if (index < 0) {
2110
+ continue;
2111
+ }
2112
+ this.edgeIndex[row] = index;
2113
+ labels[row] = record.label ?? void 0;
2114
+ edgeIds[row] = record.id ?? void 0;
2115
+ this.addElementColumns(this.edgeColumns, row, record, /* @__PURE__ */ new Set(), where);
2116
+ this.fillInteraction(record, row, where);
2117
+ if (record.graphics !== null && Object.keys(record.graphics).length > 0) {
2118
+ graphics.push([index, record.graphics]);
2119
+ }
2120
+ row++;
2121
+ }
2122
+ this.writeEdgeIds(edgeIds);
2123
+ this.writeLabels("edge", labels, (r) => this.edgeIndex[r] ?? -1);
2124
+ const column = new JsonColumn(this, "edge", GRAPHICS_COLUMN, XGMML_ORIGIN_NAMESPACE, "graphics");
2125
+ for (const [index, value] of graphics) {
2126
+ column.set(index, value);
2127
+ }
2128
+ }
2129
+ /**
2130
+ * Push one edge: endpoints (with label aliases), duplicate id check, direction and weight.
2131
+ * @param record - the edge
2132
+ * @param header - the header direction
2133
+ * @param ids - the edge ids used so far
2134
+ * @param where - the location
2135
+ * @returns the edge index, or -1 when it was skipped
2136
+ */
2137
+ addEdge(record, header, ids, where) {
2138
+ const alias = this.settings.labelAliases ? aliasesOf(record.label) : null;
2139
+ let source;
2140
+ let target;
2141
+ try {
2142
+ source = this.endpoint(record.source, alias?.[0] ?? null, "source", where);
2143
+ target = this.endpoint(record.target, alias?.[2] ?? null, "target", where);
2144
+ } catch (err) {
2145
+ this.report.counts.skippedEdges++;
2146
+ this.report.recordError(err, where);
2147
+ return -1;
2148
+ }
2149
+ if (source === null || target === null) {
2150
+ this.report.counts.skippedEdges++;
2151
+ return -1;
2152
+ }
2153
+ if (record.id !== null && ids.has(record.id)) {
2154
+ this.report.counts.skippedEdges++;
2155
+ this.report.error(
2156
+ "validation-error",
2157
+ XGMML_ISSUE.DUPLICATE_EDGE_ID,
2158
+ `${this.prefix()}edge id "${record.id}" is used twice; the second edge is skipped`,
2159
+ where
2160
+ );
2161
+ return -1;
2162
+ }
2163
+ const directed = this.edgeDirection(record, header);
2164
+ const kind = directed ? "directed" : "undirected";
2165
+ let weight;
2166
+ try {
2167
+ weight = this.weightOf(record);
2168
+ const before = this.sink.edgeCount;
2169
+ const e = this.direction.addEdge(source, target, kind, weight, where);
2170
+ this.report.counts.edges += this.sink.edgeCount - before;
2171
+ if (record.id !== null) {
2172
+ ids.add(record.id);
2173
+ }
2174
+ return e;
2175
+ } catch (err) {
2176
+ this.report.counts.skippedEdges++;
2177
+ this.report.recordError(err, where);
2178
+ return -1;
2179
+ }
2180
+ }
2181
+ /**
2182
+ * The weight of an edge: its `weight` XML attribute (or the att weightFrom names).
2183
+ * @param record - the edge
2184
+ * @returns the weight, or undefined
2185
+ */
2186
+ weightOf(record) {
2187
+ const from = this.options.weightFrom;
2188
+ if (from === null) {
2189
+ return void 0;
2190
+ }
2191
+ let text = from === "weight" ? record.weight : null;
2192
+ if (text === null) {
2193
+ text = record.atts.find((a) => a.name === from && a.children.length === 0)?.value ?? null;
2194
+ }
2195
+ if (text === null) {
2196
+ return void 0;
2197
+ }
2198
+ const weight = parseWeightText(text);
2199
+ this.weighted ||= weight !== void 0;
2200
+ return weight;
2201
+ }
2202
+ /**
2203
+ * Resolve one endpoint: a node of this graph by id, else (under labelAliases) the label alias
2204
+ * by id or by unique node label; a missing endpoint is E_MISSING_ENDPOINT, an unknown one
2205
+ * E_UNKNOWN_NODE unless addMissingNodes adds it.
2206
+ * @param text - the source or target text, or null
2207
+ * @param alias - the label alias, or null
2208
+ * @param side - which end
2209
+ * @param where - the location
2210
+ * @returns the node id, or null when the edge cannot be added
2211
+ */
2212
+ endpoint(text, alias, side, where) {
2213
+ if (text !== null) {
2214
+ const row = this.rows.get(text);
2215
+ if (row !== void 0 && row.index >= 0) {
2216
+ return this.coercer.text(text);
2217
+ }
2218
+ }
2219
+ if (alias !== null) {
2220
+ const resolved = this.aliasRow(alias);
2221
+ if (resolved !== null) {
2222
+ this.aliasResolved++;
2223
+ return this.coercer.text(resolved.id);
2224
+ }
2225
+ }
2226
+ if (text === null) {
2227
+ this.report.error(
2228
+ "missing-value",
2229
+ XGMML_ISSUE.MISSING_ENDPOINT,
2230
+ `${this.prefix()}<edge> without a ${side}`,
2231
+ where
2232
+ );
2233
+ return null;
2234
+ }
2235
+ if (this.options.addMissingNodes) {
2236
+ const id = this.coercer.text(text);
2237
+ if (this.sink.indexOf(id) === INVALID_INDEX) {
2238
+ this.sink.addNode(id);
2239
+ this.report.counts.nodes++;
2240
+ }
2241
+ return id;
2242
+ }
2243
+ this.report.error(
2244
+ "validation-error",
2245
+ XGMML_ISSUE.UNKNOWN_NODE,
2246
+ `${this.prefix()}edge ${side} "${text}" is not a node of this graph; the edge is skipped`,
2247
+ where
2248
+ );
2249
+ return null;
2250
+ }
2251
+ /**
2252
+ * The node a label alias names: by id, else by a label only one node has.
2253
+ * @param alias - the alias
2254
+ * @returns the row, or null
2255
+ */
2256
+ aliasRow(alias) {
2257
+ const byId = this.rows.get(alias);
2258
+ if (byId !== void 0 && byId.index >= 0) {
2259
+ return byId;
2260
+ }
2261
+ if (this.labelRows === null) {
2262
+ this.labelRows = /* @__PURE__ */ new Map();
2263
+ for (const row of this.rowList) {
2264
+ const { label } = row.records[0];
2265
+ if (label !== null && row.index >= 0) {
2266
+ this.labelRows.set(label, this.labelRows.has(label) ? null : row);
2267
+ }
2268
+ }
2269
+ }
2270
+ return this.labelRows.get(alias) ?? null;
2271
+ }
2272
+ /**
2273
+ * Fill the interaction of an edge without one from its label alias, as Cytoscape does.
2274
+ * @param record - the edge
2275
+ * @param row - the edge row
2276
+ * @param where - the location
2277
+ */
2278
+ fillInteraction(record, row, where) {
2279
+ if (!this.settings.labelAliases || this.dialect.session || record.atts.some((a) => a.name === INTERACTION_COLUMN)) {
2280
+ return;
2281
+ }
2282
+ const alias = aliasesOf(record.label);
2283
+ if (alias !== null) {
2284
+ this.edgeColumns.addText(row, INTERACTION_COLUMN, alias[1], where);
2285
+ this.aliasInteractions++;
2286
+ }
2287
+ }
2288
+ /** Report the label alias uses, once. */
2289
+ finishAliases() {
2290
+ if (this.aliasResolved === 0 && this.aliasInteractions === 0) {
2291
+ return;
2292
+ }
2293
+ this.report.warning(
2294
+ "coercion",
2295
+ XGMML_ISSUE.LABEL_ALIAS,
2296
+ `${this.prefix()}Cytoscape label aliases ("a (pp) b"): ${this.aliasResolved} endpoint(s) resolved, ${this.aliasInteractions} interaction(s) filled from edge labels`
2297
+ );
2298
+ }
2299
+ /**
2300
+ * Write the edge `id` column (role id, unique).
2301
+ * @param ids - the id of each row
2302
+ */
2303
+ writeEdgeIds(ids) {
2304
+ if (!ids.some((id) => id !== void 0)) {
2305
+ return;
2306
+ }
2307
+ const handle = this.declare("edge", {
2308
+ name: EDGE_ID_COLUMN,
2309
+ dtype: "string",
2310
+ role: "id",
2311
+ unique: true,
2312
+ nullable: true,
2313
+ origin: { format: FORMAT, id: null, title: null, type: "id", namespace: null }
2314
+ });
2315
+ if (handle === null) {
2316
+ return;
2317
+ }
2318
+ ids.forEach((id, row) => {
2319
+ const e = this.edgeIndex[row];
2320
+ if (id !== void 0 && e !== void 0) {
2321
+ try {
2322
+ this.sink.setEdgeValue(handle, e, id);
2323
+ } catch (err) {
2324
+ this.report.recordError(err, { element: id });
2325
+ }
2326
+ }
2327
+ });
2328
+ }
2329
+ // ------------------------------------------------------------------ graph and metadata
2330
+ /**
2331
+ * The graph table: the graph's atts and other XML attributes, and its graphics.
2332
+ * @param graph - the subnetwork, or null for the root
2333
+ */
2334
+ writeGraph(graph) {
2335
+ const g = graph ?? this.doc.root;
2336
+ const where = { line: g.line, element: g.id };
2337
+ if (graph === null) {
2338
+ for (const [key2, value] of g.attrs) {
2339
+ if (!GRAPH_STRUCTURE.has(key2) && key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
2340
+ this.graphColumns.addText(0, key2, value, where);
2341
+ }
2342
+ }
2343
+ }
2344
+ for (const att of g.atts) {
2345
+ if (att.name !== null && META_ATTS.has(att.name)) {
2346
+ continue;
2347
+ }
2348
+ if (att.hasGraph && att.name === null) {
2349
+ continue;
2350
+ }
2351
+ this.graphColumns.addAtt(0, att, where);
2352
+ }
2353
+ this.graphColumns.write(this.sink, () => 0);
2354
+ const json = { ...this.extras.graphJson ?? {} };
2355
+ if (g.graphics !== null && Object.keys(g.graphics).length > 0) {
2356
+ json[GRAPHICS_COLUMN] = g.graphics;
2357
+ }
2358
+ for (const [name, value] of Object.entries(json)) {
2359
+ this.sink.setGraphValue(name, value, {
2360
+ name,
2361
+ dtype: "json",
2362
+ nullable: true,
2363
+ origin: { format: FORMAT, id: null, title: null, type: name, namespace: XGMML_ORIGIN_NAMESPACE }
2364
+ });
2365
+ }
2366
+ }
2367
+ /**
2368
+ * The graph metadata: name, description and date from the RDF metadata, the format, version
2369
+ * and what the exporter needs.
2370
+ * @param graph - the subnetwork, or null
2371
+ */
2372
+ writeMeta(graph) {
2373
+ const g = graph ?? this.doc.root;
2374
+ const { rdf } = this.doc;
2375
+ const name = this.extras.graphName ?? g.label ?? rdf.title ?? g.id ?? void 0;
2376
+ this.sink.setMeta({
2377
+ name: name ?? void 0,
2378
+ description: rdf.description ?? void 0,
2379
+ created: rdf.date ?? void 0,
2380
+ sourceFormat: this.extras.sourceFormat ?? FORMAT,
2381
+ sourceVersion: this.dialect.versionText ?? void 0,
2382
+ weightOrigin: this.weighted ? { format: FORMAT, id: "weight", title: null, type: "weight", namespace: null } : void 0,
2383
+ extra: {
2384
+ [META_KEY]: {
2385
+ graphId: g.id,
2386
+ directed: this.doc.root.attrs.get("directed") ?? null,
2387
+ rdf: Object.keys(rdf).length > 0 ? { ...rdf } : null
2388
+ },
2389
+ ...this.extras.metaExtra ?? {}
2390
+ }
2391
+ });
2392
+ }
2393
+ }
2394
+ function aliasesOf(label) {
2395
+ if (label === null) {
2396
+ return null;
2397
+ }
2398
+ const parts = label.split(ALIAS_SPLIT);
2399
+ return parts.length === 3 ? [parts[0].trim(), parts[1], parts[2].trim()] : null;
2400
+ }
2401
+ class JsonColumn {
2402
+ /**
2403
+ * Create the column.
2404
+ * @param emitter - the emitter
2405
+ * @param domain - node or edge
2406
+ * @param name - the column name
2407
+ * @param namespace - its origin namespace
2408
+ * @param type - its origin type
2409
+ * @param dtype - json (default) or string
2410
+ */
2411
+ constructor(emitter, domain, name, namespace, type, dtype = "json") {
2412
+ this.emitter = emitter;
2413
+ this.domain = domain;
2414
+ this.name = name;
2415
+ this.namespace = namespace;
2416
+ this.type = type;
2417
+ this.dtype = dtype;
2418
+ }
2419
+ /**
2420
+ * Set one value.
2421
+ * @param index - the node or edge index
2422
+ * @param value - the value
2423
+ */
2424
+ set(index, value) {
2425
+ if (index < 0) {
2426
+ return;
2427
+ }
2428
+ if (this.handle === void 0) {
2429
+ this.handle = this.emitter.declare(this.domain, {
2430
+ name: this.name,
2431
+ dtype: this.dtype,
2432
+ nullable: true,
2433
+ origin: { format: FORMAT, id: null, title: null, type: this.type, namespace: this.namespace }
2434
+ });
2435
+ }
2436
+ if (this.handle === null) {
2437
+ return;
2438
+ }
2439
+ const sink = this.emitter.target;
2440
+ if (this.domain === "node") {
2441
+ sink.setNodeValue(this.handle, index, value);
2442
+ } else {
2443
+ sink.setEdgeValue(this.handle, index, value);
2444
+ }
2445
+ }
2446
+ }
2447
+ class Positions {
2448
+ /**
2449
+ * Create the collector.
2450
+ * @param emitter - the emitter
2451
+ * @param zAs - where z goes
2452
+ */
2453
+ constructor(emitter, zAs) {
2454
+ this.emitter = emitter;
2455
+ this.zAs = zAs;
2456
+ this.values = /* @__PURE__ */ new Map();
2457
+ this.zValues = /* @__PURE__ */ new Map();
2458
+ this.hasZ = false;
2459
+ }
2460
+ /**
2461
+ * Read one node record's coordinates.
2462
+ * @param row - the node row
2463
+ * @param record - the record
2464
+ */
2465
+ add(row, record) {
2466
+ if (row.index < 0 || record.x === null && record.y === null && record.z === null) {
2467
+ return;
2468
+ }
2469
+ const where = { line: record.line, element: row.id };
2470
+ const x = this.number(record.x, where);
2471
+ const y = this.number(record.y, where);
2472
+ const z = this.number(record.z, where);
2473
+ if (z !== null) {
2474
+ this.hasZ = true;
2475
+ if (this.zAs === "column") {
2476
+ this.zValues.set(row.index, z);
2477
+ }
2478
+ }
2479
+ if (x !== null && y !== null) {
2480
+ const flipped = y === 0 ? 0 : -y;
2481
+ this.values.set(row.index, [x, flipped, this.zAs === "position" ? z ?? 0 : 0]);
2482
+ }
2483
+ }
2484
+ /**
2485
+ * A coordinate.
2486
+ * @param text - the text, or null
2487
+ * @param where - the location
2488
+ * @returns the number, or null when absent or invalid (recorded)
2489
+ */
2490
+ number(text, where) {
2491
+ if (text === null) {
2492
+ return null;
2493
+ }
2494
+ const n = Number(text.trim());
2495
+ if (text.trim().length === 0 || !Number.isFinite(n)) {
2496
+ this.emitter.issues.error(
2497
+ "validation-error",
2498
+ XGMML_ISSUE.BAD_VALUE,
2499
+ `${this.emitter.prefix()}graphics coordinate "${text}" is not a number`,
2500
+ where
2501
+ );
2502
+ return null;
2503
+ }
2504
+ return n;
2505
+ }
2506
+ /** Declare and write the position and z columns. */
2507
+ write() {
2508
+ const sink = this.emitter.target;
2509
+ if (this.values.size > 0) {
2510
+ const handle = this.emitter.declare("node", {
2511
+ name: POSITION_COLUMN,
2512
+ dtype: "f32",
2513
+ components: 3,
2514
+ role: "position",
2515
+ nullable: true,
2516
+ origin: { format: FORMAT, id: null, title: null, type: "graphics", namespace: XGMML_ORIGIN_NAMESPACE },
2517
+ extra: { sourceDims: this.zAs === "position" && this.hasZ ? 3 : 2, units: "file" }
2518
+ });
2519
+ if (handle !== null) {
2520
+ for (const [index, xyz] of this.values) {
2521
+ sink.setNodeValue(handle, index, xyz);
2522
+ }
2523
+ }
2524
+ }
2525
+ if (this.zValues.size > 0) {
2526
+ const handle = this.emitter.declare("node", {
2527
+ name: Z_COLUMN,
2528
+ dtype: "f64",
2529
+ nullable: true,
2530
+ origin: {
2531
+ format: FORMAT,
2532
+ id: null,
2533
+ title: null,
2534
+ type: "graphics",
2535
+ namespace: CYTOSCAPE_ORIGIN_NAMESPACE
2536
+ }
2537
+ });
2538
+ if (handle !== null) {
2539
+ for (const [index, z] of this.zValues) {
2540
+ sink.setNodeValue(handle, index, z);
2541
+ }
2542
+ }
2543
+ }
2544
+ }
2545
+ }
2546
+ class Pointers {
2547
+ /**
2548
+ * Create the collector.
2549
+ * @param emitter - the emitter
2550
+ * @param isGroup - the group test
2551
+ */
2552
+ constructor(emitter, isGroup) {
2553
+ this.emitter = emitter;
2554
+ this.isGroup = isGroup;
2555
+ this.names = /* @__PURE__ */ new Map();
2556
+ this.crossFile = /* @__PURE__ */ new Map();
2557
+ this.dangling = 0;
2558
+ }
2559
+ /**
2560
+ * Read one node record's pointers.
2561
+ * @param row - the node row
2562
+ * @param record - the record
2563
+ */
2564
+ add(row, record) {
2565
+ if (record.nested.length === 0 || this.isGroup(record) || row.index < 0) {
2566
+ return;
2567
+ }
2568
+ for (const graph of record.nested) {
2569
+ if (graph.href === null) {
2570
+ this.names.set(row.index, graph.label ?? graph.id ?? "");
2571
+ continue;
2572
+ }
2573
+ const id = localRef(graph.href);
2574
+ if (id === null) {
2575
+ const resolved = this.emitter.resolvePointer(graph.href);
2576
+ if (resolved !== null) {
2577
+ this.names.set(row.index, resolved);
2578
+ } else {
2579
+ this.crossFile.set(row.index, graph.href);
2580
+ }
2581
+ continue;
2582
+ }
2583
+ const target = this.emitter.graphById(id);
2584
+ if (target === void 0) {
2585
+ this.dangling++;
2586
+ } else {
2587
+ this.names.set(row.index, target.label ?? target.id ?? id);
2588
+ }
2589
+ }
2590
+ }
2591
+ /** Write the pointer columns and their warnings. */
2592
+ write() {
2593
+ const prefix = this.emitter.prefix();
2594
+ if (this.dangling > 0) {
2595
+ this.emitter.issues.warning(
2596
+ "validation-error",
2597
+ XGMML_ISSUE.DANGLING_REFERENCE,
2598
+ `${prefix}${this.dangling} nested-network pointer(s) name no graph of the document`
2599
+ );
2600
+ }
2601
+ const names = new JsonColumn(
2602
+ this.emitter,
2603
+ "node",
2604
+ NESTED_NETWORK_COLUMN,
2605
+ CYTOSCAPE_ORIGIN_NAMESPACE,
2606
+ "nestedNetwork",
2607
+ "string"
2608
+ );
2609
+ for (const [index, name] of this.names) {
2610
+ names.set(index, name);
2611
+ }
2612
+ if (this.crossFile.size > 0) {
2613
+ this.emitter.issues.warning(
2614
+ "unsupported",
2615
+ XGMML_ISSUE.CROSS_FILE_REFERENCE,
2616
+ `${prefix}${this.crossFile.size} nested-network pointer(s) point into another file; each is kept as text in ${NETWORK_POINTER_COLUMN}`
2617
+ );
2618
+ const column = new JsonColumn(
2619
+ this.emitter,
2620
+ "node",
2621
+ NETWORK_POINTER_COLUMN,
2622
+ XGMML_ORIGIN_NAMESPACE,
2623
+ "xlink:href",
2624
+ "string"
2625
+ );
2626
+ for (const [index, href] of this.crossFile) {
2627
+ column.set(index, href);
2628
+ }
2629
+ }
2630
+ }
2631
+ }
2632
+ class Containment {
2633
+ /**
2634
+ * Create the resolver.
2635
+ * @param emitter - the emitter
2636
+ */
2637
+ constructor(emitter) {
2638
+ this.emitter = emitter;
2639
+ this.parents = /* @__PURE__ */ new Map();
2640
+ this.subgraphs = /* @__PURE__ */ new Map();
2641
+ }
2642
+ /**
2643
+ * Collect the memberships of every group node of the graph.
2644
+ * @param rows - the node rows
2645
+ * @param isGroup - the group test
2646
+ */
2647
+ resolve(rows, isGroup) {
2648
+ let unknown = 0;
2649
+ for (const row of rows) {
2650
+ for (const record of row.records) {
2651
+ if (row.index < 0 || record.nested.length === 0 || !isGroup(record)) {
2652
+ continue;
2653
+ }
2654
+ for (const graph of record.nested) {
2655
+ const atts = graph.atts.filter((a) => a.name !== null);
2656
+ if (graph.id !== null || graph.label !== null || atts.length > 0) {
2657
+ this.subgraphs.set(row.index, {
2658
+ id: graph.id,
2659
+ label: graph.label,
2660
+ atts: Object.fromEntries(atts.map((a) => [a.name, attJson(a)]))
2661
+ });
2662
+ }
2663
+ for (const member of graph.members) {
2664
+ if (member.kind !== "node") {
2665
+ continue;
2666
+ }
2667
+ const id = member.id ?? (member.href === null ? null : localRef(member.href));
2668
+ const child = id === null ? void 0 : this.emitter.rowOf(id);
2669
+ if (child === void 0) {
2670
+ if (id === null || !this.emitter.missingMember(row.id, id)) {
2671
+ unknown++;
2672
+ }
2673
+ continue;
2674
+ }
2675
+ if (child.index >= 0) {
2676
+ this.link(child.index, row.index, member.line);
2677
+ }
2678
+ }
2679
+ }
2680
+ }
2681
+ }
2682
+ if (unknown > 0) {
2683
+ this.emitter.issues.error(
2684
+ "missing-value",
2685
+ XGMML_ISSUE.UNKNOWN_PARENT,
2686
+ `${this.emitter.prefix()}${unknown} group member(s) name no node of this graph`
2687
+ );
2688
+ }
2689
+ }
2690
+ /**
2691
+ * Add one membership unless it closes a parent cycle.
2692
+ * @param child - the member's index
2693
+ * @param parent - the group node's index
2694
+ * @param line - the line
2695
+ */
2696
+ link(child, parent, line) {
2697
+ if (child === parent || this.isAncestor(child, parent)) {
2698
+ this.emitter.issues.error(
2699
+ "validation-error",
2700
+ XGMML_ISSUE.PARENT_CYCLE,
2701
+ `${this.emitter.prefix()}a group membership would make a node its own ancestor; that membership is dropped`,
2702
+ { line }
2703
+ );
2704
+ return;
2705
+ }
2706
+ const list = this.parents.get(child) ?? [];
2707
+ if (!list.includes(parent)) {
2708
+ list.push(parent);
2709
+ }
2710
+ this.parents.set(child, list);
2711
+ }
2712
+ /**
2713
+ * Whether a node is an ancestor of another through the memberships so far.
2714
+ * @param ancestor - the candidate ancestor
2715
+ * @param node - the node
2716
+ * @returns true when following parents from node reaches ancestor
2717
+ */
2718
+ isAncestor(ancestor, node) {
2719
+ const stack = [node];
2720
+ const seen = /* @__PURE__ */ new Set();
2721
+ while (stack.length > 0) {
2722
+ const n = stack.pop();
2723
+ if (n === ancestor) {
2724
+ return true;
2725
+ }
2726
+ if (seen.has(n)) {
2727
+ continue;
2728
+ }
2729
+ seen.add(n);
2730
+ stack.push(...this.parents.get(n) ?? []);
2731
+ }
2732
+ return false;
2733
+ }
2734
+ /** Declare and write the parent (or parents) and subgraph columns. */
2735
+ write() {
2736
+ const sink = this.emitter.target;
2737
+ if (this.parents.size > 0) {
2738
+ const multi = [...this.parents.values()].some((list) => list.length > 1);
2739
+ const handle = this.emitter.declare("node", {
2740
+ name: multi ? PARENTS_COLUMN : PARENT_COLUMN,
2741
+ dtype: multi ? "list" : "u32",
2742
+ itemDtype: multi ? "u32" : void 0,
2743
+ role: multi ? "parents" : "parent",
2744
+ refersTo: "node",
2745
+ nullable: true,
2746
+ origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: null }
2747
+ });
2748
+ if (handle !== null) {
2749
+ for (const [child, list] of this.parents) {
2750
+ sink.setNodeValue(handle, child, multi ? list : list[0]);
2751
+ }
2752
+ }
2753
+ }
2754
+ if (this.subgraphs.size > 0) {
2755
+ const column = new JsonColumn(this.emitter, "node", SUBGRAPH_COLUMN, XGMML_ORIGIN_NAMESPACE, "graph");
2756
+ for (const [index, value] of this.subgraphs) {
2757
+ column.set(index, value);
2758
+ }
2759
+ }
2760
+ }
2761
+ }
2762
+ const FORMAT_DEFAULTS = {
2763
+ ids: "keep",
2764
+ defaultDirected: false,
2765
+ weightFrom: "weight",
2766
+ addMissingNodes: false
2767
+ };
2768
+ const USED_OPTIONS = /* @__PURE__ */ new Set([
2769
+ "ids",
2770
+ "addMissingNodes",
2771
+ "duplicateEdges",
2772
+ "selfLoops",
2773
+ "onMixedDirection",
2774
+ "defaultDirected",
2775
+ "weightFrom",
2776
+ "weightDtype",
2777
+ "long",
2778
+ "errorLimit",
2779
+ "signal",
2780
+ "onProgress",
2781
+ "encoding"
2782
+ ]);
2783
+ const SNIFF_BYTES = 4096;
2784
+ const HEAD_CHARS = 2048;
2785
+ const XGMML_DOCTYPE = /<!DOCTYPE\s+graph\s[^<>]*xgmml\.dtd/i;
2786
+ function resolveSettings(options, cytoscape) {
2787
+ const zAs = options?.zAs ?? "column";
2788
+ if (zAs !== "column" && zAs !== "position") {
2789
+ throw new GraphFormatError(
2790
+ "E_UNSUPPORTED",
2791
+ `option zAs: ${JSON.stringify(zAs)} is not "column" or "position"`,
2792
+ {
2793
+ option: "zAs",
2794
+ found: zAs
2795
+ }
2796
+ );
2797
+ }
2798
+ return {
2799
+ labelAliases: booleanOption("labelAliases", options?.labelAliases, cytoscape),
2800
+ cytoscapeEscapes: booleanOption("cytoscapeEscapes", options?.cytoscapeEscapes, cytoscape),
2801
+ zAs
2802
+ };
2803
+ }
2804
+ function booleanOption(name, value, fallback) {
2805
+ if (value === void 0) {
2806
+ return fallback;
2807
+ }
2808
+ if (typeof value !== "boolean") {
2809
+ throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${JSON.stringify(value)} is not a boolean`, {
2810
+ option: name,
2811
+ found: value
2812
+ });
2813
+ }
2814
+ return value;
2815
+ }
2816
+ async function parseXgmml(input, report, common, options) {
2817
+ const repairs = {
2818
+ bareAmpersand: booleanOption("repairBareAmpersands", options?.repairBareAmpersands, false) ? (line) => {
2819
+ report.warning("parse-error", XGMML_ISSUE.AMPERSAND_REPAIRED, "a bare & was read as &amp;", { line });
2820
+ } : void 0,
2821
+ surrogatePair: booleanOption("pairSurrogateReferences", options?.pairSurrogateReferences, false) ? (line) => {
2822
+ report.warning(
2823
+ "parse-error",
2824
+ XGMML_ISSUE.SURROGATE_PAIRED,
2825
+ "two surrogate character references were joined into one character",
2826
+ { line }
2827
+ );
2828
+ } : void 0
2829
+ };
2830
+ const parser = new XgmmlParser(report);
2831
+ const seen = { head: "", content: false };
2832
+ try {
2833
+ await tokenizeXml(
2834
+ watch(textChunks(input, report, { ...common, declaredEncoding: xmlDeclaredEncoding }), seen),
2835
+ parser,
2836
+ repairs
2837
+ );
2838
+ } catch (err) {
2839
+ if (err instanceof XmlSyntaxError) {
2840
+ if (!seen.content) {
2841
+ report.fail(XGMML_ISSUE.EMPTY_INPUT, "the input is empty");
2842
+ }
2843
+ report.fail(XGMML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
2844
+ }
2845
+ throw err;
2846
+ }
2847
+ const doc = parser.document();
2848
+ if (!doc.xgmmlNamespace && !XGMML_DOCTYPE.test(seen.head)) {
2849
+ report.warning(
2850
+ "validation-error",
2851
+ XGMML_ISSUE.NO_NAMESPACE,
2852
+ "the root <graph> has neither the XGMML namespace nor an XGMML DOCTYPE; it is read as XGMML",
2853
+ { line: doc.root.line }
2854
+ );
2855
+ }
2856
+ return doc;
2857
+ }
2858
+ async function* watch(chunks, seen) {
2859
+ for await (const chunk of chunks) {
2860
+ if (seen.head.length < HEAD_CHARS) {
2861
+ seen.head += chunk.slice(0, HEAD_CHARS - seen.head.length);
2862
+ }
2863
+ seen.content ||= !isWhitespace(chunk);
2864
+ yield chunk;
2865
+ }
2866
+ }
2867
+ async function prepare(input, sink, options) {
2868
+ const common = resolveImportOptions(options, FORMAT_DEFAULTS);
2869
+ const report = new ImportReportBuilder(FORMAT, common.errorLimit);
2870
+ if (sink !== null) {
2871
+ reportSinkOptions(sink, options, report, true);
2872
+ reportUnusedOptions(options, report, USED_OPTIONS);
2873
+ }
2874
+ resolveSettings(options, false);
2875
+ const doc = await parseXgmml(input, report, common, options);
2876
+ const dialect = dialectOf(doc, report);
2877
+ if (dialect.view) {
2878
+ report.fail(
2879
+ XGMML_ISSUE.VIEW_DOCUMENT,
2880
+ "this is a Cytoscape session view file (cy:view): it holds positions keyed by view ids and no topology; open the session (.cys) instead",
2881
+ { line: doc.root.line }
2882
+ );
2883
+ }
2884
+ return {
2885
+ report,
2886
+ common,
2887
+ doc,
2888
+ dialect,
2889
+ settings: resolveSettings(options, doc.cytoscape),
2890
+ graphs: graphsOf(doc, dialect)
2891
+ };
2892
+ }
2893
+ function emitOne(prepared, graph, sink, extras) {
2894
+ const { doc, dialect, report, common, settings } = prepared;
2895
+ const emitter = new XgmmlEmitter(doc, dialect, sink, report, common, settings, extras);
2896
+ emitter.emit(dialect.session ? graph : null);
2897
+ if (dialect.session) {
2898
+ reportRootOnly(prepared);
2899
+ }
2900
+ throwIfAborted(common.signal);
2901
+ return report.finish();
2902
+ }
2903
+ function reportRootOnly(prepared) {
2904
+ const held = /* @__PURE__ */ new Set();
2905
+ for (const graph of prepared.graphs) {
2906
+ const members = membersOf(prepared.doc, graph);
2907
+ members.nodes.forEach((n) => held.add(n));
2908
+ members.edges.forEach((e) => held.add(e));
2909
+ }
2910
+ const nodes = prepared.doc.nodes.filter((n) => !held.has(n)).length;
2911
+ const edges = prepared.doc.edges.filter((e) => !held.has(e)).length;
2912
+ if (nodes + edges > 0) {
2913
+ prepared.report.warning(
2914
+ "unsupported",
2915
+ XGMML_ISSUE.ROOT_ONLY_ELEMENTS,
2916
+ `${nodes} node(s) and ${edges} edge(s) belong to no registered network (group meta-edges, collapsed group members) and were not read`
2917
+ );
2918
+ }
2919
+ }
2920
+ async function importXgmml(input, sink, options) {
2921
+ const prepared = await prepare(input, sink, options);
2922
+ const index = chooseGraph(
2923
+ prepared.graphs.map((g) => graphName(g, prepared.doc)),
2924
+ options,
2925
+ prepared.report
2926
+ );
2927
+ if (prepared.graphs.length > 1) {
2928
+ prepared.report.warning(
2929
+ "unsupported",
2930
+ XGMML_ISSUE.MULTIPLE_GRAPHS,
2931
+ `the session network document holds ${prepared.graphs.length} registered networks; ${prepared.graphs.length - 1} were not read (use importAll, graphIndex or graphName)`
2932
+ );
2933
+ }
2934
+ return emitOne(prepared, prepared.graphs[index], sink);
2935
+ }
2936
+ async function importAllXgmml(input, sinkFor, options) {
2937
+ const bytes = await reread(input);
2938
+ const first = await prepare(bytes, null, options);
2939
+ const reports = [];
2940
+ for (let i = 0; i < first.graphs.length; i++) {
2941
+ const sink = sinkFor(i);
2942
+ const prepared = i === 0 ? first : await prepare(bytes, null, options);
2943
+ reportSinkOptions(sink, options, prepared.report, true);
2944
+ reportUnusedOptions(options, prepared.report, USED_OPTIONS);
2945
+ reports.push(emitOne(prepared, prepared.graphs[i], sink));
2946
+ }
2947
+ return reports;
2948
+ }
2949
+ async function listXgmmlGraphs(input, options) {
2950
+ const prepared = await prepare(input, null, options);
2951
+ return prepared.graphs.map((graph, index) => {
2952
+ const members = membersOf(prepared.doc, prepared.dialect.session ? graph : null);
2953
+ return Object.freeze({
2954
+ index,
2955
+ name: graphName(graph, prepared.doc),
2956
+ nodes: new Set(members.nodes.map((n) => n.id)).size,
2957
+ edges: members.edges.length
2958
+ });
2959
+ });
2960
+ }
2961
+ function graphName(graph, doc) {
2962
+ return graph.label ?? (graph === doc.root ? doc.rdf.title ?? null : null) ?? graph.id ?? null;
2963
+ }
2964
+ async function reread(input) {
2965
+ if (typeof input === "string" || input instanceof Uint8Array) {
2966
+ return input;
2967
+ }
2968
+ const parts = [];
2969
+ const iterable = typeof input.getReader === "function" ? streamIterable(input) : input;
2970
+ for await (const chunk of iterable) {
2971
+ parts.push(chunk);
2972
+ }
2973
+ if (parts.every((p) => typeof p === "string")) {
2974
+ return parts.join("");
2975
+ }
2976
+ const bytes = parts.map((p) => typeof p === "string" ? new TextEncoder().encode(p) : p);
2977
+ const out = new Uint8Array(bytes.reduce((n, b) => n + b.byteLength, 0));
2978
+ let at = 0;
2979
+ for (const b of bytes) {
2980
+ out.set(b, at);
2981
+ at += b.byteLength;
2982
+ }
2983
+ return out;
2984
+ }
2985
+ async function* streamIterable(stream) {
2986
+ const reader = stream.getReader();
2987
+ try {
2988
+ for (; ; ) {
2989
+ const { done, value } = await reader.read();
2990
+ if (done) {
2991
+ return;
2992
+ }
2993
+ yield value;
2994
+ }
2995
+ } finally {
2996
+ reader.releaseLock();
2997
+ }
2998
+ }
2999
+ function sniffXgmml(head) {
3000
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(head.subarray(0, SNIFF_BYTES));
3001
+ if (!/^\uFEFF?\s*</.test(text)) {
3002
+ return 0;
3003
+ }
3004
+ if (XGMML_DOCTYPE.test(text)) {
3005
+ return 0.95;
3006
+ }
3007
+ const root = /<(?!\?|!)([A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/.exec(text.replace(/<!--[\s\S]*?-->/g, ""));
3008
+ if (root === null || root[2] !== "graph") {
3009
+ return 0;
3010
+ }
3011
+ const tagEnd = text.indexOf(">", root.index);
3012
+ const tag = text.slice(root.index, tagEnd < 0 ? void 0 : tagEnd);
3013
+ return tag.includes("http://www.cs.rpi.edu/XGMML") ? 0.95 : 0.5;
3014
+ }
3015
+ const xgmmlImporter = Object.freeze({
3016
+ format: FORMAT,
3017
+ extensions: EXTENSIONS,
3018
+ mimeTypes: MIME_TYPES,
3019
+ sniff: sniffXgmml,
3020
+ import: importXgmml,
3021
+ importAll: importAllXgmml,
3022
+ listGraphs: listXgmmlGraphs
3023
+ });
3024
+ export {
3025
+ CY_NAMESPACE as C,
3026
+ EDGE_ID_COLUMN as E,
3027
+ FORMAT as F,
3028
+ GRAPHICS_COLUMN as G,
3029
+ INTERACTION_COLUMN as I,
3030
+ LABEL_COLUMN as L,
3031
+ META_KEY as M,
3032
+ NESTED_NETWORK_COLUMN as N,
3033
+ POSITION_COLUMN as P,
3034
+ SUBGRAPH_COLUMN as S,
3035
+ XGMML_ISSUE as X,
3036
+ Z_COLUMN as Z,
3037
+ XGMML_LOSS as a,
3038
+ XLINK_NAMESPACE as b,
3039
+ XGMML_NAMESPACE as c,
3040
+ aliasesOf as d,
3041
+ NETWORK_POINTER_COLUMN as e,
3042
+ NETWORKS_COLUMN as f,
3043
+ PARENT_COLUMN as g,
3044
+ PARENTS_COLUMN as h,
3045
+ XGMML_ORIGIN_NAMESPACE as i,
3046
+ CYTOSCAPE_ORIGIN_NAMESPACE as j,
3047
+ isCyTrue as k,
3048
+ XgmmlEmitter as l,
3049
+ membersOf as m,
3050
+ dialectOf as n,
3051
+ parseXgmml as p,
3052
+ resolveSettings as r,
3053
+ xgmmlImporter as x
3054
+ };
3055
+ //# sourceMappingURL=importer-D_e7e7LX.js.map