@graphty/graph-io 0.0.0 → 0.2.1

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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -28
  3. package/dist/chunks/children-CL3Cy0ez.js +238 -0
  4. package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
  5. package/dist/chunks/escape-DyI8JofU.js +938 -0
  6. package/dist/chunks/escape-DyI8JofU.js.map +1 -0
  7. package/dist/chunks/importer-CQnJuWJw.js +2987 -0
  8. package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
  9. package/dist/chunks/importer-CpCpfbxr.js +2015 -0
  10. package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
  11. package/dist/chunks/importer-DbnGYr3_.js +2342 -0
  12. package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
  13. package/dist/chunks/importer-GozH8DkN.js +3050 -0
  14. package/dist/chunks/importer-GozH8DkN.js.map +1 -0
  15. package/dist/chunks/records-CGpxszm1.js +605 -0
  16. package/dist/chunks/records-CGpxszm1.js.map +1 -0
  17. package/dist/chunks/text-CajMdVFy.js +189 -0
  18. package/dist/chunks/text-CajMdVFy.js.map +1 -0
  19. package/dist/chunks/writer-DxSKC7TL.js +2842 -0
  20. package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
  21. package/dist/csv.d.ts +1 -0
  22. package/dist/csv.js +1702 -0
  23. package/dist/csv.js.map +1 -0
  24. package/dist/dot.d.ts +1 -0
  25. package/dist/dot.js +8 -0
  26. package/dist/dot.js.map +1 -0
  27. package/dist/gexf.d.ts +1 -0
  28. package/dist/gexf.js +3466 -0
  29. package/dist/gexf.js.map +1 -0
  30. package/dist/gml.d.ts +1 -0
  31. package/dist/gml.js +2647 -0
  32. package/dist/gml.js.map +1 -0
  33. package/dist/graph-io.d.ts +1 -0
  34. package/dist/graph-io.js +790 -0
  35. package/dist/graph-io.js.map +1 -0
  36. package/dist/graphml.d.ts +1 -0
  37. package/dist/graphml.js +8 -0
  38. package/dist/graphml.js.map +1 -0
  39. package/dist/json.d.ts +1 -0
  40. package/dist/json.js +11 -0
  41. package/dist/json.js.map +1 -0
  42. package/dist/neo4j.d.ts +1 -0
  43. package/dist/neo4j.js +2046 -0
  44. package/dist/neo4j.js.map +1 -0
  45. package/dist/pajek.d.ts +1 -0
  46. package/dist/pajek.js +8 -0
  47. package/dist/pajek.js.map +1 -0
  48. package/dist/src/children.d.ts +134 -0
  49. package/dist/src/children.d.ts.map +1 -0
  50. package/dist/src/children.js +274 -0
  51. package/dist/src/children.js.map +1 -0
  52. package/dist/src/common/attributes.d.ts +229 -0
  53. package/dist/src/common/attributes.d.ts.map +1 -0
  54. package/dist/src/common/attributes.js +368 -0
  55. package/dist/src/common/attributes.js.map +1 -0
  56. package/dist/src/common/codes.d.ts +105 -0
  57. package/dist/src/common/codes.d.ts.map +1 -0
  58. package/dist/src/common/codes.js +107 -0
  59. package/dist/src/common/codes.js.map +1 -0
  60. package/dist/src/common/declared-types.d.ts +84 -0
  61. package/dist/src/common/declared-types.d.ts.map +1 -0
  62. package/dist/src/common/declared-types.js +326 -0
  63. package/dist/src/common/declared-types.js.map +1 -0
  64. package/dist/src/common/direction.d.ts +206 -0
  65. package/dist/src/common/direction.d.ts.map +1 -0
  66. package/dist/src/common/direction.js +370 -0
  67. package/dist/src/common/direction.js.map +1 -0
  68. package/dist/src/common/escape.d.ts +92 -0
  69. package/dist/src/common/escape.d.ts.map +1 -0
  70. package/dist/src/common/escape.js +212 -0
  71. package/dist/src/common/escape.js.map +1 -0
  72. package/dist/src/common/export.d.ts +249 -0
  73. package/dist/src/common/export.d.ts.map +1 -0
  74. package/dist/src/common/export.js +594 -0
  75. package/dist/src/common/export.js.map +1 -0
  76. package/dist/src/common/format.d.ts +59 -0
  77. package/dist/src/common/format.d.ts.map +1 -0
  78. package/dist/src/common/format.js +106 -0
  79. package/dist/src/common/format.js.map +1 -0
  80. package/dist/src/common/ids.d.ts +83 -0
  81. package/dist/src/common/ids.d.ts.map +1 -0
  82. package/dist/src/common/ids.js +158 -0
  83. package/dist/src/common/ids.js.map +1 -0
  84. package/dist/src/common/input.d.ts +100 -0
  85. package/dist/src/common/input.d.ts.map +1 -0
  86. package/dist/src/common/input.js +335 -0
  87. package/dist/src/common/input.js.map +1 -0
  88. package/dist/src/common/lists.d.ts +34 -0
  89. package/dist/src/common/lists.d.ts.map +1 -0
  90. package/dist/src/common/lists.js +185 -0
  91. package/dist/src/common/lists.js.map +1 -0
  92. package/dist/src/common/options.d.ts +108 -0
  93. package/dist/src/common/options.d.ts.map +1 -0
  94. package/dist/src/common/options.js +265 -0
  95. package/dist/src/common/options.js.map +1 -0
  96. package/dist/src/common/report.d.ts +187 -0
  97. package/dist/src/common/report.d.ts.map +1 -0
  98. package/dist/src/common/report.js +274 -0
  99. package/dist/src/common/report.js.map +1 -0
  100. package/dist/src/common/temporal.d.ts +71 -0
  101. package/dist/src/common/temporal.d.ts.map +1 -0
  102. package/dist/src/common/temporal.js +266 -0
  103. package/dist/src/common/temporal.js.map +1 -0
  104. package/dist/src/common/text.d.ts +104 -0
  105. package/dist/src/common/text.d.ts.map +1 -0
  106. package/dist/src/common/text.js +255 -0
  107. package/dist/src/common/text.js.map +1 -0
  108. package/dist/src/common/weights.d.ts +77 -0
  109. package/dist/src/common/weights.d.ts.map +1 -0
  110. package/dist/src/common/weights.js +156 -0
  111. package/dist/src/common/weights.js.map +1 -0
  112. package/dist/src/common/writer.d.ts +51 -0
  113. package/dist/src/common/writer.d.ts.map +1 -0
  114. package/dist/src/common/writer.js +108 -0
  115. package/dist/src/common/writer.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +245 -0
  117. package/dist/src/common/xml.d.ts.map +1 -0
  118. package/dist/src/common/xml.js +942 -0
  119. package/dist/src/common/xml.js.map +1 -0
  120. package/dist/src/formats/csv/exporter.d.ts +70 -0
  121. package/dist/src/formats/csv/exporter.d.ts.map +1 -0
  122. package/dist/src/formats/csv/exporter.js +682 -0
  123. package/dist/src/formats/csv/exporter.js.map +1 -0
  124. package/dist/src/formats/csv/header.d.ts +66 -0
  125. package/dist/src/formats/csv/header.d.ts.map +1 -0
  126. package/dist/src/formats/csv/header.js +152 -0
  127. package/dist/src/formats/csv/header.js.map +1 -0
  128. package/dist/src/formats/csv/importer.d.ts +82 -0
  129. package/dist/src/formats/csv/importer.d.ts.map +1 -0
  130. package/dist/src/formats/csv/importer.js +849 -0
  131. package/dist/src/formats/csv/importer.js.map +1 -0
  132. package/dist/src/formats/csv/index.d.ts +60 -0
  133. package/dist/src/formats/csv/index.d.ts.map +1 -0
  134. package/dist/src/formats/csv/index.js +63 -0
  135. package/dist/src/formats/csv/index.js.map +1 -0
  136. package/dist/src/formats/csv/records.d.ts +188 -0
  137. package/dist/src/formats/csv/records.d.ts.map +1 -0
  138. package/dist/src/formats/csv/records.js +702 -0
  139. package/dist/src/formats/csv/records.js.map +1 -0
  140. package/dist/src/formats/csv/values.d.ts +105 -0
  141. package/dist/src/formats/csv/values.d.ts.map +1 -0
  142. package/dist/src/formats/csv/values.js +192 -0
  143. package/dist/src/formats/csv/values.js.map +1 -0
  144. package/dist/src/formats/dot/exporter.d.ts +52 -0
  145. package/dist/src/formats/dot/exporter.d.ts.map +1 -0
  146. package/dist/src/formats/dot/exporter.js +836 -0
  147. package/dist/src/formats/dot/exporter.js.map +1 -0
  148. package/dist/src/formats/dot/importer.d.ts +102 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -0
  150. package/dist/src/formats/dot/importer.js +1291 -0
  151. package/dist/src/formats/dot/importer.js.map +1 -0
  152. package/dist/src/formats/dot/index.d.ts +7 -0
  153. package/dist/src/formats/dot/index.d.ts.map +1 -0
  154. package/dist/src/formats/dot/index.js +7 -0
  155. package/dist/src/formats/dot/index.js.map +1 -0
  156. package/dist/src/formats/dot/names.d.ts +29 -0
  157. package/dist/src/formats/dot/names.d.ts.map +1 -0
  158. package/dist/src/formats/dot/names.js +28 -0
  159. package/dist/src/formats/dot/names.js.map +1 -0
  160. package/dist/src/formats/dot/tokenizer.d.ts +114 -0
  161. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
  162. package/dist/src/formats/dot/tokenizer.js +341 -0
  163. package/dist/src/formats/dot/tokenizer.js.map +1 -0
  164. package/dist/src/formats/gexf/exporter.d.ts +56 -0
  165. package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/exporter.js +1395 -0
  167. package/dist/src/formats/gexf/exporter.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +73 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -0
  170. package/dist/src/formats/gexf/importer.js +1880 -0
  171. package/dist/src/formats/gexf/importer.js.map +1 -0
  172. package/dist/src/formats/gexf/index.d.ts +96 -0
  173. package/dist/src/formats/gexf/index.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/index.js +97 -0
  175. package/dist/src/formats/gexf/index.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts +135 -0
  177. package/dist/src/formats/gexf/schema.d.ts.map +1 -0
  178. package/dist/src/formats/gexf/schema.js +323 -0
  179. package/dist/src/formats/gexf/schema.js.map +1 -0
  180. package/dist/src/formats/gml/exporter.d.ts +69 -0
  181. package/dist/src/formats/gml/exporter.d.ts.map +1 -0
  182. package/dist/src/formats/gml/exporter.js +1093 -0
  183. package/dist/src/formats/gml/exporter.js.map +1 -0
  184. package/dist/src/formats/gml/importer.d.ts +66 -0
  185. package/dist/src/formats/gml/importer.d.ts.map +1 -0
  186. package/dist/src/formats/gml/importer.js +1331 -0
  187. package/dist/src/formats/gml/importer.js.map +1 -0
  188. package/dist/src/formats/gml/index.d.ts +85 -0
  189. package/dist/src/formats/gml/index.d.ts.map +1 -0
  190. package/dist/src/formats/gml/index.js +88 -0
  191. package/dist/src/formats/gml/index.js.map +1 -0
  192. package/dist/src/formats/gml/syntax.d.ts +186 -0
  193. package/dist/src/formats/gml/syntax.d.ts.map +1 -0
  194. package/dist/src/formats/gml/syntax.js +467 -0
  195. package/dist/src/formats/gml/syntax.js.map +1 -0
  196. package/dist/src/formats/graphml/constants.d.ts +169 -0
  197. package/dist/src/formats/graphml/constants.d.ts.map +1 -0
  198. package/dist/src/formats/graphml/constants.js +165 -0
  199. package/dist/src/formats/graphml/constants.js.map +1 -0
  200. package/dist/src/formats/graphml/exporter.d.ts +34 -0
  201. package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
  202. package/dist/src/formats/graphml/exporter.js +1176 -0
  203. package/dist/src/formats/graphml/exporter.js.map +1 -0
  204. package/dist/src/formats/graphml/importer.d.ts +31 -0
  205. package/dist/src/formats/graphml/importer.d.ts.map +1 -0
  206. package/dist/src/formats/graphml/importer.js +1607 -0
  207. package/dist/src/formats/graphml/importer.js.map +1 -0
  208. package/dist/src/formats/graphml/index.d.ts +8 -0
  209. package/dist/src/formats/graphml/index.d.ts.map +1 -0
  210. package/dist/src/formats/graphml/index.js +8 -0
  211. package/dist/src/formats/graphml/index.js.map +1 -0
  212. package/dist/src/formats/graphml/tree.d.ts +72 -0
  213. package/dist/src/formats/graphml/tree.d.ts.map +1 -0
  214. package/dist/src/formats/graphml/tree.js +290 -0
  215. package/dist/src/formats/graphml/tree.js.map +1 -0
  216. package/dist/src/formats/json/dialect.d.ts +125 -0
  217. package/dist/src/formats/json/dialect.d.ts.map +1 -0
  218. package/dist/src/formats/json/dialect.js +262 -0
  219. package/dist/src/formats/json/dialect.js.map +1 -0
  220. package/dist/src/formats/json/exporter.d.ts +89 -0
  221. package/dist/src/formats/json/exporter.d.ts.map +1 -0
  222. package/dist/src/formats/json/exporter.js +1358 -0
  223. package/dist/src/formats/json/exporter.js.map +1 -0
  224. package/dist/src/formats/json/importer.d.ts +108 -0
  225. package/dist/src/formats/json/importer.d.ts.map +1 -0
  226. package/dist/src/formats/json/importer.js +1838 -0
  227. package/dist/src/formats/json/importer.js.map +1 -0
  228. package/dist/src/formats/json/index.d.ts +8 -0
  229. package/dist/src/formats/json/index.d.ts.map +1 -0
  230. package/dist/src/formats/json/index.js +8 -0
  231. package/dist/src/formats/json/index.js.map +1 -0
  232. package/dist/src/formats/neo4j/exporter.d.ts +68 -0
  233. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
  234. package/dist/src/formats/neo4j/exporter.js +1055 -0
  235. package/dist/src/formats/neo4j/exporter.js.map +1 -0
  236. package/dist/src/formats/neo4j/header.d.ts +52 -0
  237. package/dist/src/formats/neo4j/header.d.ts.map +1 -0
  238. package/dist/src/formats/neo4j/header.js +131 -0
  239. package/dist/src/formats/neo4j/header.js.map +1 -0
  240. package/dist/src/formats/neo4j/importer.d.ts +73 -0
  241. package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
  242. package/dist/src/formats/neo4j/importer.js +932 -0
  243. package/dist/src/formats/neo4j/importer.js.map +1 -0
  244. package/dist/src/formats/neo4j/index.d.ts +79 -0
  245. package/dist/src/formats/neo4j/index.d.ts.map +1 -0
  246. package/dist/src/formats/neo4j/index.js +83 -0
  247. package/dist/src/formats/neo4j/index.js.map +1 -0
  248. package/dist/src/formats/pajek/exporter.d.ts +58 -0
  249. package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
  250. package/dist/src/formats/pajek/exporter.js +825 -0
  251. package/dist/src/formats/pajek/exporter.js.map +1 -0
  252. package/dist/src/formats/pajek/importer.d.ts +88 -0
  253. package/dist/src/formats/pajek/importer.d.ts.map +1 -0
  254. package/dist/src/formats/pajek/importer.js +1047 -0
  255. package/dist/src/formats/pajek/importer.js.map +1 -0
  256. package/dist/src/formats/pajek/index.d.ts +7 -0
  257. package/dist/src/formats/pajek/index.d.ts.map +1 -0
  258. package/dist/src/formats/pajek/index.js +7 -0
  259. package/dist/src/formats/pajek/index.js.map +1 -0
  260. package/dist/src/formats/pajek/syntax.d.ts +112 -0
  261. package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
  262. package/dist/src/formats/pajek/syntax.js +269 -0
  263. package/dist/src/formats/pajek/syntax.js.map +1 -0
  264. package/dist/src/index.d.ts +35 -0
  265. package/dist/src/index.d.ts.map +1 -0
  266. package/dist/src/index.js +39 -0
  267. package/dist/src/index.js.map +1 -0
  268. package/dist/src/registry.d.ts +207 -0
  269. package/dist/src/registry.d.ts.map +1 -0
  270. package/dist/src/registry.js +481 -0
  271. package/dist/src/registry.js.map +1 -0
  272. package/dist/src/sniff.d.ts +104 -0
  273. package/dist/src/sniff.d.ts.map +1 -0
  274. package/dist/src/sniff.js +357 -0
  275. package/dist/src/sniff.js.map +1 -0
  276. package/dist/src/types.d.ts +238 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +29 -0
  279. package/dist/src/types.js.map +1 -0
  280. package/dist/tsconfig.build.tsbuildinfo +1 -0
  281. package/package.json +122 -7
  282. package/src/children.ts +335 -0
  283. package/src/common/attributes.ts +520 -0
  284. package/src/common/codes.ts +153 -0
  285. package/src/common/declared-types.ts +374 -0
  286. package/src/common/direction.ts +518 -0
  287. package/src/common/escape.ts +231 -0
  288. package/src/common/export.ts +817 -0
  289. package/src/common/format.ts +111 -0
  290. package/src/common/ids.ts +176 -0
  291. package/src/common/input.ts +378 -0
  292. package/src/common/lists.ts +196 -0
  293. package/src/common/options.ts +377 -0
  294. package/src/common/report.ts +352 -0
  295. package/src/common/temporal.ts +302 -0
  296. package/src/common/text.ts +294 -0
  297. package/src/common/weights.ts +202 -0
  298. package/src/common/writer.ts +123 -0
  299. package/src/common/xml.ts +1053 -0
  300. package/src/formats/csv/exporter.ts +894 -0
  301. package/src/formats/csv/header.ts +172 -0
  302. package/src/formats/csv/importer.ts +1104 -0
  303. package/src/formats/csv/index.ts +88 -0
  304. package/src/formats/csv/records.ts +813 -0
  305. package/src/formats/csv/values.ts +224 -0
  306. package/src/formats/dot/exporter.ts +1014 -0
  307. package/src/formats/dot/importer.ts +1549 -0
  308. package/src/formats/dot/index.ts +7 -0
  309. package/src/formats/dot/names.ts +40 -0
  310. package/src/formats/dot/tokenizer.ts +384 -0
  311. package/src/formats/gexf/exporter.ts +1696 -0
  312. package/src/formats/gexf/importer.ts +2333 -0
  313. package/src/formats/gexf/index.ts +142 -0
  314. package/src/formats/gexf/schema.ts +361 -0
  315. package/src/formats/gml/exporter.ts +1404 -0
  316. package/src/formats/gml/importer.ts +1591 -0
  317. package/src/formats/gml/index.ts +128 -0
  318. package/src/formats/gml/syntax.ts +545 -0
  319. package/src/formats/graphml/constants.ts +225 -0
  320. package/src/formats/graphml/exporter.ts +1458 -0
  321. package/src/formats/graphml/importer.ts +2027 -0
  322. package/src/formats/graphml/index.ts +8 -0
  323. package/src/formats/graphml/tree.ts +318 -0
  324. package/src/formats/json/dialect.ts +317 -0
  325. package/src/formats/json/exporter.ts +1616 -0
  326. package/src/formats/json/importer.ts +2271 -0
  327. package/src/formats/json/index.ts +8 -0
  328. package/src/formats/neo4j/exporter.ts +1287 -0
  329. package/src/formats/neo4j/header.ts +156 -0
  330. package/src/formats/neo4j/importer.ts +1220 -0
  331. package/src/formats/neo4j/index.ts +116 -0
  332. package/src/formats/pajek/exporter.ts +1000 -0
  333. package/src/formats/pajek/importer.ts +1311 -0
  334. package/src/formats/pajek/index.ts +7 -0
  335. package/src/formats/pajek/syntax.ts +307 -0
  336. package/src/index.ts +244 -0
  337. package/src/registry.ts +617 -0
  338. package/src/sniff.ts +397 -0
  339. package/src/types.ts +262 -0
@@ -0,0 +1,1395 @@
1
+ /**
2
+ * The GEXF exporter (design sections 8.5, 3.6, 3.7, 5.1, 5.10; research note 07 section 9): writes
3
+ * GEXF 1.3 by default (1.2 on request), restoring declared attributes from `origin` (id, title,
4
+ * type, defaults, options), the viz namespace from the position / color / size / shape /
5
+ * thickness role columns, containment from the parent / parents columns, element lifetimes from
6
+ * the temporal role columns (with the text companions of design section 5.1), and dynamic
7
+ * attribute values from the temporal extension tables of design section 5.10. Expanded
8
+ * mixed-direction pairs are folded back through the `pair` / `directed` / `mutual` roles into one
9
+ * undirected or mutual edge; weights are written for explicit rows only (the role-weight column's
10
+ * validity, design section 3.7). check() lists every loss before anything is written: the generic
11
+ * capability gaps (W_OPEN_INTERVAL in 1.3, u32 / u8 dtypes, strides, nested json, graph
12
+ * attributes, foreign extension tables) and the GEXF-specific ones (1.2 has no `kind`, no
13
+ * timestamps and no typed lists; role columns of an unexpected shape; roles the format cannot
14
+ * carry; attribute titles the importer would rename).
15
+ */
16
+ import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
17
+ import { mapDeclaredType } from "../../common/declared-types.js";
18
+ import { pairFolding } from "../../common/direction.js";
19
+ import { escapeXmlAttribute, escapeXmlText } from "../../common/escape.js";
20
+ import { capabilities, checkCapabilities, LOSS, sanitizeIds } from "../../common/export.js";
21
+ import { formatF32, formatF64, formatInteger } from "../../common/format.js";
22
+ import { isCanonicalIntegerText } from "../../common/ids.js";
23
+ import { joinListText } from "../../common/lists.js";
24
+ import { resolveExportOptions } from "../../common/options.js";
25
+ import { formatTemporal, formatTimeValue } from "../../common/temporal.js";
26
+ import { explicitWeights } from "../../common/weights.js";
27
+ import { encodeChunks, joinText } from "../../common/writer.js";
28
+ import { xmlIllegalTextNotes } from "../../common/xml.js";
29
+ import { canonicalScalarType, GEXF_FORMAT, GEXF_NAMESPACES, isListType, isScalarType, NODE_COLUMNS, OPEN_END, OPEN_START, parseTemporalTableName, RESERVED_EDGE_NAMES, RESERVED_NODE_NAMES, TEMPORAL_COLUMNS, VIZ_NAMESPACE, } from "./schema.js";
30
+ /** LossNote codes specific to the GEXF exporter, next to the shared LOSS codes. */
31
+ export const GEXF_LOSS = Object.freeze({
32
+ /** GEXF 1.2 has no parallel-edge `kind`; the kind column is dropped. */
33
+ KIND_DROPPED: "W_GEXF_KIND_DROPPED",
34
+ /** GEXF 1.2 has no timestamps; a timestamp becomes a closed interval [t, t]. */
35
+ TIMESTAMP_AS_INTERVAL: "W_TIMESTAMP_AS_INTERVAL",
36
+ /** GEXF 1.2 liststring items are separated by `|`; an item containing one cannot be split back. */
37
+ LIST_SEPARATOR: "W_LIST_SEPARATOR",
38
+ /** A role column of a shape GEXF cannot map (a string `start`, a 2-component color); written as a plain attribute. */
39
+ ROLE_SHAPE: "W_ROLE_SHAPE",
40
+ /** A temporal extension table without the element / start / end / value columns of design section 5.10. */
41
+ TEMPORAL_TABLE_SHAPE: "W_TEMPORAL_TABLE_SHAPE",
42
+ /** An attribute whose title the importer would rename on re-import (a reserved name). */
43
+ ATTRIBUTE_RENAMED: "W_ATTRIBUTE_RENAMED",
44
+ /** A cell, default or option the declared type cannot express (skipped). */
45
+ VALUE_UNWRITABLE: "W_VALUE_UNWRITABLE",
46
+ /** A declared type the target version lacks (1.2: date, dateTime, typed lists...); the canonical type is written. */
47
+ DECLARED_TYPE: "W_DECLARED_TYPE",
48
+ /** A node id whose text reads back as the other type under the canonical rule (design section 4.1): a non-integer number, a string of integer text. */
49
+ ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
50
+ /** A viz role column (position, color, size, thickness) that is not f32; the importer reads viz values as f32. */
51
+ VIZ_DTYPE: "W_GEXF_VIZ_DTYPE",
52
+ /** A plain `weight` edge column reads back as THE weight (the importer's weightFrom default). */
53
+ WEIGHT_KEY_CLASH: LOSS.WEIGHT_KEY_CLASH,
54
+ /** A dict column without declared options gains one from its dictionary on re-import. */
55
+ OPTIONS_GAINED: LOSS.OPTIONS_GAINED,
56
+ /** A string cell holding a character XML 1.0 forbids; export() throws E_COLUMN_TYPE. */
57
+ XML_ILLEGAL_CHAR: LOSS.XML_ILLEGAL_CHAR,
58
+ });
59
+ const DTYPES_KEPT = ["f32", "f64", "i32", "bool", "dict", "string"];
60
+ /** GEXF 1.3: everything the model has but nested json, strides, graph attributes and open intervals. */
61
+ const CAPABILITIES_1_3 = capabilities({
62
+ mixedDirection: true,
63
+ multiEdges: true,
64
+ selfLoops: true,
65
+ edgeIds: "optional",
66
+ idCharset: "any",
67
+ dtypes: DTYPES_KEPT,
68
+ lists: true,
69
+ defaults: true,
70
+ options: true,
71
+ hierarchy: true,
72
+ temporal: "dynamic-values",
73
+ positions: true,
74
+ viz: true,
75
+ });
76
+ /** GEXF 1.2: no parallel edges, required edge ids, `liststring` only. */
77
+ const CAPABILITIES_1_2 = capabilities({
78
+ ...CAPABILITIES_1_3,
79
+ multiEdges: false,
80
+ edgeIds: "required",
81
+ });
82
+ const STRUCTURAL_ROLES = new Set(["directed", "pair", "mutual", "weight", "timeText"]);
83
+ /** The attribute title the importer reads THE weight from by default (its weightFrom default). */
84
+ const DEFAULT_WEIGHT_TITLE = "weight";
85
+ const NUMERIC_DTYPES = new Set(["f32", "f64", "i32", "u32", "u8"]);
86
+ const TEXT_DTYPES = new Set(["string", "dict"]);
87
+ /**
88
+ * Resolve the format-specific options.
89
+ * @param options - the caller's options
90
+ * @returns the version to write
91
+ */
92
+ function resolveVersion(options) {
93
+ const version = options?.version;
94
+ if (version === undefined) {
95
+ return "1.3";
96
+ }
97
+ if (version !== "1.2" && version !== "1.3") {
98
+ throw new GraphFormatError("E_UNSUPPORTED", `option version: ${JSON.stringify(version)} is not "1.2" or "1.3"`, {
99
+ option: "version",
100
+ found: version,
101
+ supported: ["1.2", "1.3"],
102
+ });
103
+ }
104
+ return version;
105
+ }
106
+ /**
107
+ * Decide everything about an export: the capability notes, the role columns of each domain, the
108
+ * attribute declarations and their formatters, the temporal tables and the graph header values.
109
+ * @param snapshot - the snapshot
110
+ * @param options - the caller's options
111
+ * @returns the plan
112
+ */
113
+ function planExport(snapshot, options) {
114
+ const version = resolveVersion(options);
115
+ const resolved = resolveExportOptions(options);
116
+ const caps = version === "1.2" ? CAPABILITIES_1_2 : CAPABILITIES_1_3;
117
+ const notes = [
118
+ ...checkCapabilities(snapshot, caps, resolved, {
119
+ openIntervals: version === "1.2",
120
+ temporalText: true,
121
+ positionDtype: "f32",
122
+ roles: MAPPED_ROLES,
123
+ roleNames: ROLE_NAMES,
124
+ }),
125
+ ];
126
+ const note = (code, message, column = null, count = null) => {
127
+ notes.push(Object.freeze({ code, message, column, count }));
128
+ };
129
+ notes.push(...xmlIllegalTextNotes(snapshot));
130
+ let typeChanges = 0;
131
+ for (let i = 0; i < snapshot.nodeCount; i++) {
132
+ const id = snapshot.ids.idOf(i);
133
+ if (typeof id === "number" ? !Number.isSafeInteger(id) : isCanonicalIntegerText(id)) {
134
+ typeChanges++;
135
+ }
136
+ }
137
+ if (typeChanges > 0) {
138
+ note(GEXF_LOSS.ID_TEXT_TYPE, `${typeChanges} node id(s) change type when read back under ids: "canonical" (string ids that are integer text, non-integer numbers); the file's idtype is not honoured by the importer`, null, typeChanges);
139
+ }
140
+ const tables = collectTemporalTables(snapshot, version, note);
141
+ const nodeRoles = collectRoles(snapshot.nodes, "node", note);
142
+ const edgeRoles = collectRoles(snapshot.edges, "edge", note);
143
+ const nodeAttrs = collectAttributes(snapshot, "node", nodeRoles, tables.get("node") ?? new Map(), version, note);
144
+ const edgeAttrs = collectAttributes(snapshot, "edge", edgeRoles, tables.get("edge") ?? new Map(), version, note);
145
+ const temporal = hasLifetime(nodeRoles) ||
146
+ hasLifetime(edgeRoles) ||
147
+ nodeAttrs.some((a) => a.dynamic) ||
148
+ edgeAttrs.some((a) => a.dynamic);
149
+ if (version === "1.2") {
150
+ for (const [domain, roles] of [
151
+ ["node", nodeRoles],
152
+ ["edge", edgeRoles],
153
+ ]) {
154
+ if (roles.timestamp !== null || roles.timestamps !== null) {
155
+ note(GEXF_LOSS.TIMESTAMP_AS_INTERVAL, `${domain} timestamps are written as closed intervals; GEXF 1.2 has no timestamp representation`, (roles.timestamp ?? roles.timestamps)?.meta.name ?? null, null);
156
+ }
157
+ }
158
+ if (edgeRoles.kind !== null) {
159
+ note(GEXF_LOSS.KIND_DROPPED, `edge column "${edgeRoles.kind.meta.name}" (kind) cannot be written; GEXF 1.2 has no edge kind`, edgeRoles.kind.meta.name, edgeRoles.kind.length - edgeRoles.kind.nullCount);
160
+ }
161
+ }
162
+ const { meta } = snapshot;
163
+ let { timeRepresentation } = meta;
164
+ if (timeRepresentation === null && temporal) {
165
+ const timestampOnly = (nodeRoles.timestamp !== null || edgeRoles.timestamp !== null) &&
166
+ nodeRoles.start === null &&
167
+ nodeRoles.end === null &&
168
+ edgeRoles.start === null &&
169
+ edgeRoles.end === null;
170
+ timeRepresentation = timestampOnly ? "timestamp" : null;
171
+ }
172
+ return {
173
+ version,
174
+ options: resolved,
175
+ notes,
176
+ nodeRoles,
177
+ edgeRoles,
178
+ nodeAttrs,
179
+ edgeAttrs,
180
+ timeFormat: meta.timeFormat,
181
+ temporal,
182
+ timeRepresentation,
183
+ };
184
+ }
185
+ /**
186
+ * Whether a domain carries element lifetimes.
187
+ * @param roles - the domain's role columns
188
+ * @returns true when any temporal role column exists
189
+ */
190
+ function hasLifetime(roles) {
191
+ return (roles.start !== null ||
192
+ roles.end !== null ||
193
+ roles.timestamp !== null ||
194
+ roles.spells !== null ||
195
+ roles.timestamps !== null);
196
+ }
197
+ /**
198
+ * Whether a column is a scalar numeric column.
199
+ * @param column - the column
200
+ * @returns true for f32 / f64 / i32 / u32 / u8 with one component
201
+ */
202
+ function isNumericScalar(column) {
203
+ return NUMERIC_DTYPES.has(column.dtype) && column.meta.components === 1;
204
+ }
205
+ /**
206
+ * Whether a column is a numeric column with a given stride.
207
+ * @param column - the column
208
+ * @param components - the accepted strides
209
+ * @returns true when numeric with one of the strides
210
+ */
211
+ function isNumericVector(column, components) {
212
+ return NUMERIC_DTYPES.has(column.dtype) && components.includes(column.meta.components);
213
+ }
214
+ /**
215
+ * Whether a column holds text (string or dict).
216
+ * @param column - the column
217
+ * @returns true for string / dict
218
+ */
219
+ function isText(column) {
220
+ return TEXT_DTYPES.has(column.dtype);
221
+ }
222
+ /**
223
+ * Whether a column is a list of numbers with a given item stride.
224
+ * @param column - the column
225
+ * @param itemComponents - the required item stride
226
+ * @returns true for a numeric list of that stride
227
+ */
228
+ function isNumericList(column, itemComponents) {
229
+ return (column.dtype === "list" &&
230
+ column.meta.itemDtype !== null &&
231
+ NUMERIC_DTYPES.has(column.meta.itemDtype) &&
232
+ (column.meta.itemComponents ?? 1) === itemComponents);
233
+ }
234
+ /**
235
+ * Whether a role column has the shape its GEXF field needs.
236
+ * @param role - the role
237
+ * @param column - the column
238
+ * @param domain - node or edge
239
+ * @returns true when writable
240
+ */
241
+ function roleShapeOk(role, column, domain) {
242
+ switch (role) {
243
+ case "label":
244
+ case "shape":
245
+ return isText(column);
246
+ case "kind":
247
+ return domain === "edge" && isText(column);
248
+ case "id":
249
+ return domain === "edge" && (isText(column) || isNumericScalar(column));
250
+ case "start":
251
+ case "end":
252
+ case "timestamp":
253
+ case "open":
254
+ return isNumericScalar(column);
255
+ case "size":
256
+ return domain === "node" && isNumericScalar(column);
257
+ case "thickness":
258
+ return domain === "edge" && isNumericScalar(column);
259
+ case "spells":
260
+ return isNumericList(column, 2);
261
+ case "timestamps":
262
+ return isNumericList(column, 1);
263
+ case "position":
264
+ return domain === "node" && isNumericVector(column, [2, 3]);
265
+ case "color":
266
+ return isNumericVector(column, [3, 4]);
267
+ case "parent":
268
+ return domain === "node" && column.dtype === "u32" && column.meta.components === 1;
269
+ case "parents":
270
+ return domain === "node" && column.dtype === "list" && column.meta.itemDtype === "u32";
271
+ default:
272
+ return false;
273
+ }
274
+ }
275
+ const VIZ_NUMERIC_ROLES = new Set(["position", "color", "size", "thickness"]);
276
+ const MAPPED_ROLES = new Set([
277
+ "label",
278
+ "id",
279
+ "kind",
280
+ "start",
281
+ "end",
282
+ "timestamp",
283
+ "spells",
284
+ "timestamps",
285
+ "open",
286
+ "position",
287
+ "color",
288
+ "size",
289
+ "shape",
290
+ "thickness",
291
+ "parent",
292
+ "parents",
293
+ ]);
294
+ /** The column name the importer gives each mapped role on re-import (design section 5.6 fixed names). */
295
+ const ROLE_NAMES = Object.freeze({
296
+ label: NODE_COLUMNS.label,
297
+ id: "id",
298
+ kind: "kind",
299
+ start: NODE_COLUMNS.start,
300
+ end: NODE_COLUMNS.end,
301
+ timestamp: NODE_COLUMNS.timestamp,
302
+ spells: NODE_COLUMNS.spells,
303
+ timestamps: NODE_COLUMNS.timestamps,
304
+ open: NODE_COLUMNS.open,
305
+ position: NODE_COLUMNS.position,
306
+ color: NODE_COLUMNS.color,
307
+ size: NODE_COLUMNS.size,
308
+ shape: NODE_COLUMNS.shape,
309
+ thickness: "thickness",
310
+ parent: NODE_COLUMNS.parent,
311
+ parents: NODE_COLUMNS.parents,
312
+ });
313
+ /**
314
+ * Find the role columns of a table; a role column of the wrong shape is noted and left to the
315
+ * attribute pass.
316
+ * @param table - the node or edge table
317
+ * @param domain - node or edge
318
+ * @param note - the note recorder
319
+ * @returns the role columns
320
+ */
321
+ function collectRoles(table, domain, note) {
322
+ const companions = new Map();
323
+ const roles = {
324
+ label: null,
325
+ id: null,
326
+ kind: null,
327
+ start: null,
328
+ end: null,
329
+ timestamp: null,
330
+ spells: null,
331
+ timestamps: null,
332
+ open: null,
333
+ position: null,
334
+ color: null,
335
+ size: null,
336
+ shape: null,
337
+ shapeUri: null,
338
+ thickness: null,
339
+ parent: null,
340
+ parents: null,
341
+ companions,
342
+ };
343
+ for (const column of table) {
344
+ const { meta } = column;
345
+ const forName = meta.extra.for;
346
+ if ((meta.role === "timeText" || typeof forName === "string") && column.dtype === "string") {
347
+ if (typeof forName === "string") {
348
+ companions.set(forName, column);
349
+ }
350
+ continue;
351
+ }
352
+ if (domain === "node" && meta.name === NODE_COLUMNS.shapeUri && meta.origin?.namespace === VIZ_NAMESPACE) {
353
+ roles.shapeUri = column.dtype === "string" ? column : null;
354
+ continue;
355
+ }
356
+ const { role } = meta;
357
+ if (role === null || !MAPPED_ROLES.has(role)) {
358
+ continue;
359
+ }
360
+ if (!roleShapeOk(role, column, domain)) {
361
+ note(GEXF_LOSS.ROLE_SHAPE, `${domain} column "${meta.name}" (${role}) has a shape GEXF cannot map (${describeShape(column)}); written as a plain attribute`, meta.name, null);
362
+ // the generic check skips role columns; report what the attribute path loses
363
+ const set = column.length - column.nullCount;
364
+ if (NUMERIC_DTYPES.has(column.dtype) && meta.components > 1) {
365
+ note(LOSS.COMPONENTS, `${domain} column "${meta.name}" has ${meta.components} components; written as a list`, meta.name, set);
366
+ }
367
+ if (column.dtype === "json") {
368
+ note(LOSS.JSON, `${domain} column "${meta.name}" holds nested values; written as JSON text`, meta.name, set);
369
+ }
370
+ if (column.dtype === "u32" || column.dtype === "u8") {
371
+ note(LOSS.DTYPE, `${domain} column "${meta.name}" is ${column.dtype}; the format cannot keep that dtype`, meta.name, set);
372
+ }
373
+ continue;
374
+ }
375
+ if (VIZ_NUMERIC_ROLES.has(role) && column.dtype !== "f32") {
376
+ note(GEXF_LOSS.VIZ_DTYPE, `${domain} column "${meta.name}" (${role}) is ${column.dtype}; viz values read back as f32`, meta.name, column.length - column.nullCount);
377
+ }
378
+ roles[role] = column;
379
+ }
380
+ return roles;
381
+ }
382
+ /**
383
+ * A short description of a column's shape for messages.
384
+ * @param column - the column
385
+ * @returns "dtype x components" or "list of item"
386
+ */
387
+ function describeShape(column) {
388
+ if (column.dtype === "list") {
389
+ return `list of ${column.meta.itemDtype ?? "?"} x ${column.meta.itemComponents ?? 1}`;
390
+ }
391
+ return `${column.dtype} x ${column.meta.components}`;
392
+ }
393
+ /**
394
+ * Resolve every temporal extension table (design section 5.10) into its columns, grouped by
395
+ * domain and keyed by the static column's name; a table without the required columns is noted.
396
+ * @param snapshot - the snapshot
397
+ * @param version - the target version
398
+ * @param note - the note recorder
399
+ * @returns tables by domain, then by column name
400
+ */
401
+ function collectTemporalTables(snapshot, version, note) {
402
+ const out = new Map();
403
+ for (const [name, table] of snapshot.extensions) {
404
+ const parsed = parseTemporalTableName(name);
405
+ if (parsed === null) {
406
+ continue;
407
+ }
408
+ const element = table.get(TEMPORAL_COLUMNS.element);
409
+ const start = table.get(TEMPORAL_COLUMNS.start);
410
+ const end = table.get(TEMPORAL_COLUMNS.end);
411
+ const value = table.get(TEMPORAL_COLUMNS.value);
412
+ if (element === null ||
413
+ element.dtype !== "u32" ||
414
+ start === null ||
415
+ !isNumericScalar(start) ||
416
+ end === null ||
417
+ !isNumericScalar(end) ||
418
+ value === null) {
419
+ note(GEXF_LOSS.TEMPORAL_TABLE_SHAPE, `extension table "${name}" lacks the element / start / end / value columns and cannot be written`, name, table.rowCount);
420
+ continue;
421
+ }
422
+ const rows = new Map();
423
+ for (let r = 0; r < table.rowCount; r++) {
424
+ if (!element.isSet(r)) {
425
+ continue;
426
+ }
427
+ const e = element.value(r);
428
+ const list = rows.get(e);
429
+ if (list === undefined) {
430
+ rows.set(e, [r]);
431
+ }
432
+ else {
433
+ list.push(r);
434
+ }
435
+ }
436
+ const typed = attributeType(value, version);
437
+ const open = optionalNumeric(table.get(TEMPORAL_COLUMNS.open));
438
+ if (version === "1.3" && open !== null && open.length - open.nullCount > 0) {
439
+ note(LOSS.OPEN_INTERVAL, `dynamic values of "${name}" carry open intervals, which GEXF 1.3 cannot write`, name, open.length - open.nullCount);
440
+ }
441
+ const resolved = {
442
+ element,
443
+ start,
444
+ end,
445
+ value,
446
+ startText: optionalText(table.get(TEMPORAL_COLUMNS.startText)),
447
+ endText: optionalText(table.get(TEMPORAL_COLUMNS.endText)),
448
+ valueText: isTemporalType(typed.type) ? optionalText(table.get(TEMPORAL_COLUMNS.valueText)) : null,
449
+ open,
450
+ rows,
451
+ format: typed.format,
452
+ };
453
+ let byDomain = out.get(parsed.domain);
454
+ if (byDomain === undefined) {
455
+ byDomain = new Map();
456
+ out.set(parsed.domain, byDomain);
457
+ }
458
+ byDomain.set(parsed.column, resolved);
459
+ }
460
+ return out;
461
+ }
462
+ /**
463
+ * A string column, or null.
464
+ * @param column - the column or null
465
+ * @returns the column when it is a string column
466
+ */
467
+ function optionalText(column) {
468
+ return column !== null && column.dtype === "string" ? column : null;
469
+ }
470
+ /**
471
+ * A numeric scalar column, or null.
472
+ * @param column - the column or null
473
+ * @returns the column when numeric
474
+ */
475
+ function optionalNumeric(column) {
476
+ return column !== null && isNumericScalar(column) ? column : null;
477
+ }
478
+ /**
479
+ * The GEXF type of a column and the formatter of its values: the declared `origin.type` when the
480
+ * version knows it and it agrees with the dtype, the canonical type of the dtype otherwise; lists
481
+ * become `list<item>` (1.3) or `liststring` (1.2); a stride column becomes a list of its lanes;
482
+ * json becomes a string of JSON text.
483
+ * @param column - the column
484
+ * @param version - the target version
485
+ * @returns the type text, whether it is a list, and the formatter
486
+ */
487
+ function attributeType(column, version) {
488
+ const { meta } = column;
489
+ const originType = meta.origin?.type ?? null;
490
+ if (meta.dtype === "list" || (NUMERIC_DTYPES.has(meta.dtype) && meta.components > 1)) {
491
+ const itemDtype = meta.dtype === "list" ? (meta.itemDtype ?? "string") : meta.dtype;
492
+ let itemType = canonicalScalarType(itemDtype);
493
+ if (originType !== null &&
494
+ meta.dtype === "list" &&
495
+ isListType(originType, "1.3") &&
496
+ agrees(originType, "list", itemDtype)) {
497
+ itemType = originType.slice(4);
498
+ }
499
+ if (version === "1.2") {
500
+ const itemFormat = scalarFormatter(itemType, itemDtype);
501
+ return {
502
+ type: "liststring",
503
+ list: true,
504
+ itemType: "string",
505
+ format: listFormatter(itemFormat, version),
506
+ dropped: itemType === "string" ? null : `list${itemType}`,
507
+ };
508
+ }
509
+ return {
510
+ type: `list${itemType}`,
511
+ list: true,
512
+ itemType,
513
+ format: listFormatter(scalarFormatter(itemType, itemDtype), version),
514
+ dropped: null,
515
+ };
516
+ }
517
+ let type = canonicalScalarType(meta.dtype);
518
+ let dropped = null;
519
+ const temporal = originType === null || meta.dtype !== "f64" ? null : gexfTemporalType(originType);
520
+ if (temporal !== null) {
521
+ // a date / dateTime column of any format (Neo4j spells them date / datetime /
522
+ // localdatetime) keeps its temporal type, so the text companion is written back
523
+ if (isScalarType(temporal, version)) {
524
+ type = temporal;
525
+ }
526
+ else {
527
+ dropped = temporal;
528
+ }
529
+ }
530
+ else if (originType !== null && agrees(originType, meta.dtype, null)) {
531
+ if (isScalarType(originType, version)) {
532
+ type = originType;
533
+ }
534
+ else if (isScalarType(originType, "1.3")) {
535
+ dropped = originType;
536
+ }
537
+ }
538
+ return { type, list: false, itemType: type, format: scalarFormatter(type, meta.dtype), dropped };
539
+ }
540
+ /**
541
+ * The GEXF temporal type a declared origin type of any format maps to: `date` for a date,
542
+ * `dateTime` for a date-time with or without zone (GEXF's own spellings, Neo4j's `date` /
543
+ * `datetime` / `localdatetime`), null for a non-temporal type or a time of day (GEXF has none).
544
+ * @param originType - the declared type text
545
+ * @returns "date", "dateTime" or null
546
+ */
547
+ function gexfTemporalType(originType) {
548
+ switch (originType.toLowerCase()) {
549
+ case "date":
550
+ return "date";
551
+ case "datetime":
552
+ case "localdatetime":
553
+ return "dateTime";
554
+ default:
555
+ return null;
556
+ }
557
+ }
558
+ /**
559
+ * Whether a declared GEXF type maps back to a dtype (so the exporter may restore it verbatim).
560
+ * @param type - the declared type text
561
+ * @param dtype - the column dtype
562
+ * @param itemDtype - the item dtype of a list column, or null
563
+ * @returns true when mapDeclaredType agrees
564
+ */
565
+ function agrees(type, dtype, itemDtype) {
566
+ const spec = mapDeclaredType("gexf", type, "f64");
567
+ if (spec === null) {
568
+ return false;
569
+ }
570
+ if (dtype === "list") {
571
+ return spec.list && spec.itemDtype === itemDtype;
572
+ }
573
+ if (spec.list) {
574
+ return false;
575
+ }
576
+ // a dict column is a string column with options
577
+ return spec.dtype === dtype || (dtype === "dict" && spec.dtype === "string");
578
+ }
579
+ /**
580
+ * The formatter of one scalar value under a GEXF type.
581
+ * @param type - the GEXF scalar type text
582
+ * @param dtype - the column (or item) dtype the values come from
583
+ * @returns the formatter
584
+ */
585
+ function scalarFormatter(type, dtype) {
586
+ switch (type) {
587
+ case "boolean":
588
+ return (value) => {
589
+ if (typeof value === "boolean") {
590
+ return value ? "true" : "false";
591
+ }
592
+ if (typeof value !== "number") {
593
+ return null;
594
+ }
595
+ return value !== 0 ? "true" : "false";
596
+ };
597
+ case "integer":
598
+ case "long":
599
+ case "byte":
600
+ case "short":
601
+ case "biginteger":
602
+ return (value) => {
603
+ if (typeof value === "number") {
604
+ return Number.isInteger(value) ? formatInteger(value) : formatF64(value);
605
+ }
606
+ return typeof value === "string" ? value : null;
607
+ };
608
+ case "float":
609
+ return (value) => (typeof value === "number" ? formatF32(value) : null);
610
+ case "double":
611
+ case "bigdecimal":
612
+ return (value) => {
613
+ if (typeof value === "number") {
614
+ return dtype === "f32" ? formatF32(value) : formatF64(value);
615
+ }
616
+ return typeof value === "string" ? value : null;
617
+ };
618
+ case "date":
619
+ case "dateTime":
620
+ return (value) => {
621
+ if (typeof value === "number") {
622
+ return formatTemporal(value, type);
623
+ }
624
+ return typeof value === "string" ? value : null;
625
+ };
626
+ default:
627
+ return (value) => {
628
+ switch (typeof value) {
629
+ case "string":
630
+ return value;
631
+ case "number":
632
+ return dtype === "f32" ? formatF32(value) : formatF64(value);
633
+ case "boolean":
634
+ return value ? "true" : "false";
635
+ case "object":
636
+ return value === null ? null : JSON.stringify(value);
637
+ default:
638
+ return null;
639
+ }
640
+ };
641
+ }
642
+ }
643
+ /**
644
+ * The formatter of a list value: items through the item formatter, joined per version.
645
+ * @param item - the item formatter
646
+ * @param version - the target version (1.3 brackets, 1.2 pipes)
647
+ * @returns the formatter; null when any item cannot be written
648
+ */
649
+ function listFormatter(item, version) {
650
+ return (value) => {
651
+ if (!Array.isArray(value) && !ArrayBuffer.isView(value)) {
652
+ return null;
653
+ }
654
+ const items = [];
655
+ for (const entry of Array.from(value)) {
656
+ const text = item(entry);
657
+ if (text === null) {
658
+ return null;
659
+ }
660
+ items.push(text);
661
+ }
662
+ return joinListText(items, version === "1.2" ? "pipe" : "gexf");
663
+ };
664
+ }
665
+ /**
666
+ * Decide the `<attribute>` declarations of a domain: every column that is neither structural, a
667
+ * companion nor a mapped role column, in declaration order, plus the temporal tables without a
668
+ * static column (the dynamic weight) as dynamic attributes.
669
+ * @param snapshot - the snapshot
670
+ * @param domain - node or edge
671
+ * @param roles - the domain's role columns
672
+ * @param tables - the domain's temporal tables by column name
673
+ * @param version - the target version
674
+ * @param note - the note recorder
675
+ * @returns the declarations
676
+ */
677
+ function collectAttributes(snapshot, domain, roles, tables, version, note) {
678
+ const table = domain === "node" ? snapshot.nodes : snapshot.edges;
679
+ const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
680
+ const roleColumns = new Set();
681
+ for (const value of Object.values(roles)) {
682
+ if (value !== null && !(value instanceof Map)) {
683
+ roleColumns.add(value);
684
+ }
685
+ }
686
+ const usedIds = new Set();
687
+ const specs = [];
688
+ const usedTables = new Set();
689
+ const declare = (column, name, origin, typed, dynamic, temporal) => {
690
+ const id = uniqueId(origin.id ?? name, name, usedIds);
691
+ const title = origin.title ?? name;
692
+ if (typed.dropped !== null) {
693
+ note(GEXF_LOSS.DECLARED_TYPE, `${domain} column "${name}" is declared ${typed.dropped}, which GEXF ${version} lacks; written as ${typed.type}`, name, null);
694
+ }
695
+ if (domain === "edge" && column !== null && column.meta.role === null && title === DEFAULT_WEIGHT_TITLE) {
696
+ note(GEXF_LOSS.WEIGHT_KEY_CLASH, `edge column "${name}" is written as an attribute titled "${title}", which the importer reads as THE weight (weightFrom); it reads back as the weight, not as a column`, name, column.length - column.nullCount);
697
+ }
698
+ const reimportName = reserved.has(title) ? `${title}#${id}` : title;
699
+ if (reimportName !== name) {
700
+ note(GEXF_LOSS.ATTRIBUTE_RENAMED, `${domain} column "${name}" is written with title "${title}" and reads back as "${reimportName}"`, name, null);
701
+ }
702
+ const { defaultText, optionsText } = column === null
703
+ ? { defaultText: null, optionsText: null }
704
+ : declaredTexts(column, domain, typed, version, note);
705
+ const companion = column === null ? null : (roles.companions.get(name) ?? null);
706
+ if (companion !== null && !isTemporalType(typed.type)) {
707
+ note(LOSS.TEMPORAL_TEXT, `${domain} column "${companion.meta.name}" (the lexical form of "${name}") cannot be written: "${name}" is written as ${typed.type}, not a GEXF temporal type`, companion.meta.name, companion.length - companion.nullCount);
708
+ }
709
+ specs.push({
710
+ id,
711
+ title,
712
+ type: typed.type,
713
+ column,
714
+ companion: isTemporalType(typed.type) ? companion : null,
715
+ format: typed.format,
716
+ defaultText,
717
+ optionsText,
718
+ dynamic,
719
+ table: temporal,
720
+ });
721
+ };
722
+ for (const column of table) {
723
+ const { meta } = column;
724
+ if (roleColumns.has(column) || (meta.role !== null && STRUCTURAL_ROLES.has(meta.role))) {
725
+ continue;
726
+ }
727
+ if (typeof meta.extra.for === "string" && column.dtype === "string") {
728
+ continue;
729
+ }
730
+ if (domain === "node" && column === roles.shapeUri) {
731
+ continue;
732
+ }
733
+ const temporal = tables.get(meta.name) ?? null;
734
+ if (temporal !== null) {
735
+ usedTables.add(meta.name);
736
+ }
737
+ declare(column, meta.name, { id: meta.origin?.id ?? null, title: meta.origin?.title ?? null }, attributeType(column, version), meta.dynamic || temporal !== null, temporal);
738
+ }
739
+ for (const [name, temporal] of tables) {
740
+ if (usedTables.has(name)) {
741
+ continue;
742
+ }
743
+ const { origin } = temporal.value.meta;
744
+ const { weightOrigin } = snapshot.meta;
745
+ const weightId = domain === "edge" && weightOrigin !== null && (weightOrigin.title ?? weightOrigin.id) === name
746
+ ? weightOrigin.id
747
+ : null;
748
+ declare(null, name, { id: origin?.id ?? weightId, title: null }, attributeType(temporal.value, version), true, temporal);
749
+ }
750
+ return specs;
751
+ }
752
+ /**
753
+ * The `<default>` and `<options>` texts of a declared attribute, with the notes about what cannot
754
+ * be written as the declared type, about a dictionary written as options, and about 1.2 list items
755
+ * holding the `|` separator.
756
+ * @param column - the column
757
+ * @param domain - node or edge
758
+ * @param typed - the attribute's GEXF type and formatter
759
+ * @param version - the target version
760
+ * @param note - the note recorder
761
+ * @returns the default and options texts (null when absent or unwritable)
762
+ */
763
+ function declaredTexts(column, domain, typed, version, note) {
764
+ const { meta } = column;
765
+ const { name } = meta;
766
+ let defaultText = null;
767
+ let optionsText = null;
768
+ if (meta.default !== undefined) {
769
+ defaultText = typed.format(meta.default);
770
+ if (defaultText === null) {
771
+ note(GEXF_LOSS.VALUE_UNWRITABLE, `${domain} column "${name}": the default cannot be written as ${typed.type}`, name, null);
772
+ }
773
+ }
774
+ const options = meta.options ?? (column.dtype === "dict" ? column.dictionary : null);
775
+ if (meta.options === null && options !== null && options.length > 0) {
776
+ note(GEXF_LOSS.OPTIONS_GAINED, `${domain} column "${name}" declares no options; its dictionary is written as <options> and reads back as declared options`, name, null);
777
+ }
778
+ if (options !== null) {
779
+ const itemFormat = typed.list ? scalarFormatter(typed.itemType, meta.itemDtype ?? "string") : typed.format;
780
+ const texts = [];
781
+ let ok = true;
782
+ for (const option of options) {
783
+ const text = itemFormat(option);
784
+ if (text === null) {
785
+ ok = false;
786
+ break;
787
+ }
788
+ texts.push(text);
789
+ }
790
+ if (ok) {
791
+ optionsText = joinListText(texts, version === "1.2" ? "pipe" : "gexf");
792
+ }
793
+ else {
794
+ note(GEXF_LOSS.VALUE_UNWRITABLE, `${domain} column "${name}": the options cannot be written as ${typed.type}`, name, null);
795
+ }
796
+ }
797
+ if (version === "1.2" && column.dtype === "list") {
798
+ let count = 0;
799
+ for (let r = 0; r < column.length; r++) {
800
+ if (column.isSet(r) && listItems(column, r).some((item) => String(item).includes("|"))) {
801
+ count++;
802
+ }
803
+ }
804
+ if (count > 0) {
805
+ note(GEXF_LOSS.LIST_SEPARATOR, `${domain} column "${name}": ${count} row(s) hold an item containing "|", the 1.2 list separator`, name, count);
806
+ }
807
+ }
808
+ return { defaultText, optionsText };
809
+ }
810
+ /**
811
+ * Whether a GEXF type is a temporal one whose values have text companions.
812
+ * @param type - the type text
813
+ * @returns true for date / dateTime
814
+ */
815
+ function isTemporalType(type) {
816
+ return type === "date" || type === "dateTime";
817
+ }
818
+ /**
819
+ * A unique attribute id within a class.
820
+ * @param preferred - the id to try first (the origin id or the name)
821
+ * @param name - the column name, tried second
822
+ * @param used - ids already taken
823
+ * @returns a free id, recorded as used
824
+ */
825
+ function uniqueId(preferred, name, used) {
826
+ const candidates = [preferred, name];
827
+ for (const candidate of candidates) {
828
+ if (candidate.length > 0 && !used.has(candidate)) {
829
+ used.add(candidate);
830
+ return candidate;
831
+ }
832
+ }
833
+ for (let n = 2;; n++) {
834
+ const candidate = `${name}#${n}`;
835
+ if (!used.has(candidate)) {
836
+ used.add(candidate);
837
+ return candidate;
838
+ }
839
+ }
840
+ }
841
+ // ============================================================ writing
842
+ /**
843
+ * The text of a time bound: the companion text when present, the formatted number otherwise.
844
+ * @param column - the numeric column
845
+ * @param companion - its text companion, or null
846
+ * @param row - the row
847
+ * @param timeFormat - the graph's timeformat
848
+ * @returns the text
849
+ */
850
+ function timeText(column, companion, row, timeFormat) {
851
+ if (companion !== null && companion.isSet(row)) {
852
+ return companion.value(row);
853
+ }
854
+ return formatTimeValue(column.value(row), timeFormat);
855
+ }
856
+ /**
857
+ * The `start` / `end` / `timestamp` / open attributes of one element.
858
+ * @param roles - the domain's role columns
859
+ * @param row - the element's row
860
+ * @param plan - the plan
861
+ * @returns the attribute text, starting with a space when non-empty
862
+ */
863
+ function lifetimeAttrs(roles, row, plan) {
864
+ let out = "";
865
+ const { timeFormat, version } = plan;
866
+ let bits = 0;
867
+ if (version === "1.2" && roles.open !== null && roles.open.isSet(row)) {
868
+ bits = roles.open.value(row);
869
+ }
870
+ const write = (name, column, open) => {
871
+ if (column === null || !column.isSet(row)) {
872
+ return;
873
+ }
874
+ const value = column.value(row);
875
+ if (!Number.isFinite(value)) {
876
+ return;
877
+ }
878
+ const text = timeText(column, roles.companions.get(column.meta.name) ?? null, row, timeFormat);
879
+ // GEXF 1.2: `startopen` / `endopen` hold the time of a non-inclusive bound (dynamics.xsd
880
+ // time-type) and replace `start` / `end`
881
+ out += ` ${open ? `${name}open` : name}="${escapeXmlAttribute(text)}"`;
882
+ };
883
+ if (roles.timestamp !== null && roles.timestamp.isSet(row)) {
884
+ if (version === "1.3" && plan.timeRepresentation === "timestamp") {
885
+ write("timestamp", roles.timestamp, false);
886
+ }
887
+ else {
888
+ write("start", roles.timestamp, false);
889
+ write("end", roles.timestamp, false);
890
+ }
891
+ }
892
+ else {
893
+ write("start", roles.start, (bits & OPEN_START) !== 0);
894
+ write("end", roles.end, (bits & OPEN_END) !== 0);
895
+ }
896
+ if (version === "1.3" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
897
+ const items = listItems(roles.timestamps, row).map((t) => formatTimeValue(t, timeFormat));
898
+ out += ` timestamps="${escapeXmlAttribute(`<[${items.join(", ")}]>`)}"`;
899
+ }
900
+ return out;
901
+ }
902
+ /**
903
+ * The `<spells>` element of one element: its spells column, plus (1.2) its timestamps as [t, t].
904
+ * @param roles - the domain's role columns
905
+ * @param row - the element's row
906
+ * @param plan - the plan
907
+ * @param indent - the indentation of the element
908
+ * @returns the lines, or an empty string
909
+ */
910
+ function spellsElement(roles, row, plan, indent) {
911
+ const pairs = [];
912
+ if (roles.spells !== null && roles.spells.isSet(row)) {
913
+ for (const pair of listItems(roles.spells, row)) {
914
+ const [s, e] = Array.from(pair);
915
+ pairs.push([s, e]);
916
+ }
917
+ }
918
+ if (plan.version === "1.2" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
919
+ for (const t of listItems(roles.timestamps, row)) {
920
+ pairs.push([t, t]);
921
+ }
922
+ }
923
+ if (pairs.length === 0) {
924
+ return "";
925
+ }
926
+ let out = `${indent}<spells>\n`;
927
+ for (const [s, e] of pairs) {
928
+ let attrs = "";
929
+ if (Number.isFinite(s)) {
930
+ attrs += ` start="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
931
+ }
932
+ if (Number.isFinite(e)) {
933
+ attrs += ` end="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
934
+ }
935
+ out += `${indent} <spell${attrs}/>\n`;
936
+ }
937
+ return `${out}${indent}</spells>\n`;
938
+ }
939
+ /**
940
+ * The `<attvalues>` element of one node or edge: static cells and dynamic rows.
941
+ * @param attrs - the domain's declarations
942
+ * @param row - the element's row
943
+ * @param plan - the plan
944
+ * @param indent - the indentation of the element
945
+ * @returns the lines, or an empty string
946
+ */
947
+ function attvaluesElement(attrs, row, plan, indent) {
948
+ let out = "";
949
+ for (const spec of attrs) {
950
+ const { column } = spec;
951
+ if (column !== null && column.isSet(row)) {
952
+ let text;
953
+ if (spec.companion !== null && spec.companion.isSet(row)) {
954
+ text = spec.companion.value(row);
955
+ }
956
+ else {
957
+ text = spec.format(cellValue(column, row));
958
+ }
959
+ if (text !== null) {
960
+ out += `${indent} <attvalue for="${escapeXmlAttribute(spec.id)}" value="${escapeXmlAttribute(text)}"/>\n`;
961
+ }
962
+ }
963
+ const { table } = spec;
964
+ if (table === null) {
965
+ continue;
966
+ }
967
+ const rows = table.rows.get(row);
968
+ if (rows === undefined) {
969
+ continue;
970
+ }
971
+ for (const r of rows) {
972
+ if (!table.value.isSet(r)) {
973
+ continue;
974
+ }
975
+ let text;
976
+ if (table.valueText !== null && table.valueText.isSet(r)) {
977
+ text = table.valueText.value(r);
978
+ }
979
+ else {
980
+ text = table.format(cellValue(table.value, r));
981
+ }
982
+ if (text === null) {
983
+ continue;
984
+ }
985
+ out += `${indent} <attvalue for="${escapeXmlAttribute(spec.id)}" value="${escapeXmlAttribute(text)}"${timedAttrs(table, r, plan)}/>\n`;
986
+ }
987
+ }
988
+ return out.length === 0 ? "" : `${indent}<attvalues>\n${out}${indent}</attvalues>\n`;
989
+ }
990
+ /**
991
+ * The time bounds of one temporal table row as attributes.
992
+ * @param table - the table
993
+ * @param r - the row
994
+ * @param plan - the plan
995
+ * @returns the attribute text, starting with a space when non-empty
996
+ */
997
+ function timedAttrs(table, r, plan) {
998
+ const start = table.start.isSet(r) ? table.start.value(r) : -Infinity;
999
+ const end = table.end.isSet(r) ? table.end.value(r) : Infinity;
1000
+ let out = "";
1001
+ if (plan.version === "1.3" && plan.timeRepresentation === "timestamp" && start === end && Number.isFinite(start)) {
1002
+ return ` timestamp="${escapeXmlAttribute(timeText(table.start, table.startText, r, plan.timeFormat))}"`;
1003
+ }
1004
+ let bits = 0;
1005
+ if (plan.version === "1.2" && table.open !== null && table.open.isSet(r)) {
1006
+ bits = table.open.value(r);
1007
+ }
1008
+ if (Number.isFinite(start)) {
1009
+ const name = (bits & OPEN_START) !== 0 ? "startopen" : "start";
1010
+ out += ` ${name}="${escapeXmlAttribute(timeText(table.start, table.startText, r, plan.timeFormat))}"`;
1011
+ }
1012
+ if (Number.isFinite(end)) {
1013
+ const name = (bits & OPEN_END) !== 0 ? "endopen" : "end";
1014
+ out += ` ${name}="${escapeXmlAttribute(timeText(table.end, table.endText, r, plan.timeFormat))}"`;
1015
+ }
1016
+ return out;
1017
+ }
1018
+ /**
1019
+ * The value of a set cell as a plain JS value: lists through sliceOf, json through values, a
1020
+ * stride column as an array of lanes.
1021
+ * @param column - the column
1022
+ * @param row - the row
1023
+ * @returns the value
1024
+ */
1025
+ function cellValue(column, row) {
1026
+ switch (column.dtype) {
1027
+ case "list":
1028
+ return column.sliceOf(row);
1029
+ case "json":
1030
+ return column.values[row];
1031
+ default: {
1032
+ const value = column.value(row);
1033
+ return ArrayBuffer.isView(value) ? Array.from(value) : value;
1034
+ }
1035
+ }
1036
+ }
1037
+ /**
1038
+ * Whether `viz:position` gets a z attribute: always for a 3-d source, and for a 2-d source
1039
+ * (`extra.sourceDims === 2`) only when some node has since been given a non-zero z.
1040
+ * @param position - the position role column, or null
1041
+ * @returns true when z is written
1042
+ */
1043
+ function positionWritesZ(position) {
1044
+ if (position === null || position.meta.components < 3) {
1045
+ return false;
1046
+ }
1047
+ if (position.meta.extra.sourceDims !== 2) {
1048
+ return true;
1049
+ }
1050
+ const { components } = position.meta;
1051
+ let data;
1052
+ switch (position.dtype) {
1053
+ case "f32":
1054
+ case "f64":
1055
+ case "i32":
1056
+ case "u32":
1057
+ case "u8":
1058
+ ({ data } = position);
1059
+ break;
1060
+ default:
1061
+ // A non-numeric position column has no z lane to inspect; write z so nothing is lost.
1062
+ return true;
1063
+ }
1064
+ for (let i = 0; i < position.length; i++) {
1065
+ if (position.isSet(i) && data[i * components + 2] !== 0) {
1066
+ return true;
1067
+ }
1068
+ }
1069
+ return false;
1070
+ }
1071
+ /**
1072
+ * The items of a set row of a list column.
1073
+ * @param column - a list column
1074
+ * @param row - the row
1075
+ * @returns the items; empty when the column is not a list
1076
+ */
1077
+ function listItems(column, row) {
1078
+ return column.dtype === "list" ? column.sliceOf(row) : [];
1079
+ }
1080
+ /**
1081
+ * The viz elements of one node or edge.
1082
+ * @param roles - the domain's role columns
1083
+ * @param row - the element's row
1084
+ * @param indent - the indentation
1085
+ * @param writeZ - whether viz:position gets its z attribute
1086
+ * @returns the lines, or an empty string
1087
+ */
1088
+ function vizElements(roles, row, indent, writeZ) {
1089
+ let out = "";
1090
+ const { color } = roles;
1091
+ if (color !== null && color.isSet(row)) {
1092
+ const lanes = Array.from(color.value(row));
1093
+ const scale = color.dtype === "u8" ? 1 : 255;
1094
+ const channel = (v) => String(Math.max(0, Math.min(255, Math.round(v * scale))));
1095
+ let attrs = ` r="${channel(lanes[0])}" g="${channel(lanes[1])}" b="${channel(lanes[2])}"`;
1096
+ if (lanes.length > 3) {
1097
+ const a = color.dtype === "u8" ? lanes[3] / 255 : lanes[3];
1098
+ if (a !== 1) {
1099
+ attrs += ` a="${formatF32(Math.fround(a))}"`;
1100
+ }
1101
+ }
1102
+ out += `${indent}<viz:color${attrs}/>\n`;
1103
+ }
1104
+ const { position } = roles;
1105
+ if (position !== null && position.isSet(row)) {
1106
+ const lanes = Array.from(position.value(row));
1107
+ const fmt = position.dtype === "f32" ? formatF32 : formatF64;
1108
+ let attrs = ` x="${fmt(lanes[0])}" y="${fmt(lanes[1])}"`;
1109
+ if (lanes.length > 2 && writeZ) {
1110
+ attrs += ` z="${fmt(lanes[2])}"`;
1111
+ }
1112
+ out += `${indent}<viz:position${attrs}/>\n`;
1113
+ }
1114
+ for (const [name, column] of [
1115
+ ["size", roles.size],
1116
+ ["thickness", roles.thickness],
1117
+ ]) {
1118
+ if (column !== null && column.isSet(row)) {
1119
+ const value = column.value(row);
1120
+ const text = column.dtype === "f32" ? formatF32(value) : formatF64(value);
1121
+ out += `${indent}<viz:${name} value="${escapeXmlAttribute(text)}"/>\n`;
1122
+ }
1123
+ }
1124
+ const { shape } = roles;
1125
+ if (shape !== null && shape.isSet(row)) {
1126
+ let attrs = ` value="${escapeXmlAttribute(String(shape.value(row)))}"`;
1127
+ if (roles.shapeUri !== null && roles.shapeUri.isSet(row)) {
1128
+ attrs += ` uri="${escapeXmlAttribute(roles.shapeUri.value(row))}"`;
1129
+ }
1130
+ out += `${indent}<viz:shape${attrs}/>\n`;
1131
+ }
1132
+ return out;
1133
+ }
1134
+ /**
1135
+ * The `<attributes>` groups of one class.
1136
+ * @param cls - node or edge
1137
+ * @param specs - the declarations
1138
+ * @param indent - the indentation
1139
+ * @returns the lines, or an empty string
1140
+ */
1141
+ function attributesGroups(cls, specs, indent) {
1142
+ let out = "";
1143
+ for (const mode of ["static", "dynamic"]) {
1144
+ const group = specs.filter((s) => s.dynamic === (mode === "dynamic"));
1145
+ if (group.length === 0) {
1146
+ continue;
1147
+ }
1148
+ out += `${indent}<attributes class="${cls}" mode="${mode}">\n`;
1149
+ for (const spec of group) {
1150
+ const head = `${indent} <attribute id="${escapeXmlAttribute(spec.id)}" title="${escapeXmlAttribute(spec.title)}" type="${spec.type}"`;
1151
+ if (spec.defaultText === null && spec.optionsText === null) {
1152
+ out += `${head}/>\n`;
1153
+ continue;
1154
+ }
1155
+ out += `${head}>\n`;
1156
+ if (spec.defaultText !== null) {
1157
+ out += `${indent} <default>${escapeXmlText(spec.defaultText)}</default>\n`;
1158
+ }
1159
+ if (spec.optionsText !== null) {
1160
+ out += `${indent} <options>${escapeXmlText(spec.optionsText)}</options>\n`;
1161
+ }
1162
+ out += `${indent} </attribute>\n`;
1163
+ }
1164
+ out += `${indent}</attributes>\n`;
1165
+ }
1166
+ return out;
1167
+ }
1168
+ /**
1169
+ * The `type` of a logical edge after folding an expanded pair (design section 3.6).
1170
+ * @param snapshot - the snapshot
1171
+ * @param e - the logical edge index
1172
+ * @param folding - the pair-folding view (mutual pairs fold: GEXF has the mutual type)
1173
+ * @returns the edge type, or null when the edge is the mirror half of a pair
1174
+ */
1175
+ function edgeType(snapshot, e, folding) {
1176
+ if (!snapshot.directed) {
1177
+ return "undirected";
1178
+ }
1179
+ if (folding.folded(e)) {
1180
+ return null;
1181
+ }
1182
+ if (!folding.sourceDirected(e)) {
1183
+ return "undirected";
1184
+ }
1185
+ if (folding.mateOf(e) !== INVALID_INDEX || folding.isMutual(e)) {
1186
+ return "mutual";
1187
+ }
1188
+ return "directed";
1189
+ }
1190
+ /**
1191
+ * Write the document as text parts.
1192
+ * @param snapshot - the snapshot
1193
+ * @param options - the caller's options
1194
+ * @yields one part per element or line group
1195
+ * @returns nothing
1196
+ */
1197
+ function* writeGexf(snapshot, options) {
1198
+ const plan = planExport(snapshot, options);
1199
+ const { version } = plan;
1200
+ const ids = sanitizeIds(snapshot, "any", plan.options.sanitizeIds);
1201
+ const idText = (index) => escapeXmlAttribute(String(ids.idAt(index)));
1202
+ const ns = GEXF_NAMESPACES[version];
1203
+ const { meta } = snapshot;
1204
+ yield '<?xml version="1.0" encoding="UTF-8"?>\n';
1205
+ yield `<gexf xmlns="${ns.gexf}" xmlns:viz="${ns.viz}" version="${version}">\n`;
1206
+ yield metaElement(meta);
1207
+ yield graphStart(snapshot, plan);
1208
+ yield attributesGroups("node", plan.nodeAttrs, " ");
1209
+ yield attributesGroups("edge", plan.edgeAttrs, " ");
1210
+ // nodes
1211
+ const { nodeRoles } = plan;
1212
+ const writeZ = positionWritesZ(nodeRoles.position);
1213
+ yield ` <nodes count="${snapshot.nodeCount}">\n`;
1214
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1215
+ let attrs = ` id="${idText(i)}"`;
1216
+ if (nodeRoles.label !== null && nodeRoles.label.isSet(i)) {
1217
+ attrs += ` label="${escapeXmlAttribute(String(nodeRoles.label.value(i)))}"`;
1218
+ }
1219
+ if (nodeRoles.parent !== null && nodeRoles.parent.isSet(i)) {
1220
+ attrs += ` pid="${idText(nodeRoles.parent.value(i))}"`;
1221
+ }
1222
+ attrs += lifetimeAttrs(nodeRoles, i, plan);
1223
+ let body = attvaluesElement(plan.nodeAttrs, i, plan, " ");
1224
+ if (nodeRoles.parents !== null && nodeRoles.parents.isSet(i)) {
1225
+ const parents = listItems(nodeRoles.parents, i);
1226
+ if (parents.length > 0) {
1227
+ body += " <parents>\n";
1228
+ for (const p of parents) {
1229
+ body += ` <parent for="${idText(p)}"/>\n`;
1230
+ }
1231
+ body += " </parents>\n";
1232
+ }
1233
+ }
1234
+ body += spellsElement(nodeRoles, i, plan, " ");
1235
+ body += vizElements(nodeRoles, i, " ", writeZ);
1236
+ yield body.length === 0 ? ` <node${attrs}/>\n` : ` <node${attrs}>\n${body} </node>\n`;
1237
+ }
1238
+ yield " </nodes>\n";
1239
+ // edges: the count is decided in a cheap first pass (no strings built) so the section
1240
+ // streams element by element instead of buffering every edge
1241
+ const { edgeRoles } = plan;
1242
+ const edgeList = snapshot.edgeList();
1243
+ const folding = pairFolding(snapshot, { foldMutual: true });
1244
+ const weights = explicitWeights(snapshot);
1245
+ const defaultType = snapshot.directed ? "directed" : "undirected";
1246
+ let written = 0;
1247
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1248
+ if (edgeType(snapshot, e, folding) !== null) {
1249
+ written++;
1250
+ }
1251
+ }
1252
+ yield ` <edges count="${written}">\n`;
1253
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1254
+ const type = edgeType(snapshot, e, folding);
1255
+ if (type === null) {
1256
+ continue;
1257
+ }
1258
+ let attrs = "";
1259
+ if (edgeRoles.id !== null && edgeRoles.id.isSet(e)) {
1260
+ attrs += ` id="${escapeXmlAttribute(String(edgeRoles.id.value(e)))}"`;
1261
+ }
1262
+ else if (version === "1.2") {
1263
+ attrs += ` id="e${e}"`;
1264
+ }
1265
+ attrs += ` source="${idText(edgeList.src[e])}" target="${idText(edgeList.dst[e])}"`;
1266
+ if (type !== defaultType) {
1267
+ attrs += ` type="${type}"`;
1268
+ }
1269
+ const weight = weights.text(e);
1270
+ if (weight !== null) {
1271
+ attrs += ` weight="${weight}"`;
1272
+ }
1273
+ if (edgeRoles.label !== null && edgeRoles.label.isSet(e)) {
1274
+ attrs += ` label="${escapeXmlAttribute(String(edgeRoles.label.value(e)))}"`;
1275
+ }
1276
+ if (version === "1.3" && edgeRoles.kind !== null && edgeRoles.kind.isSet(e)) {
1277
+ attrs += ` kind="${escapeXmlAttribute(String(edgeRoles.kind.value(e)))}"`;
1278
+ }
1279
+ attrs += lifetimeAttrs(edgeRoles, e, plan);
1280
+ let body = attvaluesElement(plan.edgeAttrs, e, plan, " ");
1281
+ body += spellsElement(edgeRoles, e, plan, " ");
1282
+ body += vizElements(edgeRoles, e, " ", false);
1283
+ yield body.length === 0 ? ` <edge${attrs}/>\n` : ` <edge${attrs}>\n${body} </edge>\n`;
1284
+ }
1285
+ yield " </edges>\n";
1286
+ yield " </graph>\n";
1287
+ yield "</gexf>\n";
1288
+ }
1289
+ /**
1290
+ * The `<meta>` element.
1291
+ * @param meta - the graph meta
1292
+ * @returns the lines, or an empty string when nothing is set
1293
+ */
1294
+ function metaElement(meta) {
1295
+ let body = "";
1296
+ if (meta.creator !== null) {
1297
+ body += ` <creator>${escapeXmlText(meta.creator)}</creator>\n`;
1298
+ }
1299
+ if (meta.description !== null) {
1300
+ body += ` <description>${escapeXmlText(meta.description)}</description>\n`;
1301
+ }
1302
+ if (meta.keywords.length > 0) {
1303
+ body += ` <keywords>${escapeXmlText(meta.keywords.join(", "))}</keywords>\n`;
1304
+ }
1305
+ const modified = meta.modified === null ? "" : ` lastmodifieddate="${escapeXmlAttribute(meta.modified)}"`;
1306
+ if (body.length === 0 && modified.length === 0) {
1307
+ return "";
1308
+ }
1309
+ return body.length === 0 ? ` <meta${modified}/>\n` : ` <meta${modified}>\n${body} </meta>\n`;
1310
+ }
1311
+ /**
1312
+ * The `<graph>` start tag with its header attributes.
1313
+ * @param snapshot - the snapshot
1314
+ * @param plan - the plan
1315
+ * @returns the line
1316
+ */
1317
+ function graphStart(snapshot, plan) {
1318
+ const { meta } = snapshot;
1319
+ let attrs = ` defaultedgetype="${snapshot.directed ? "directed" : "undirected"}"`;
1320
+ let mode = meta.mode ?? "static";
1321
+ if (plan.temporal && mode === "static") {
1322
+ mode = "dynamic";
1323
+ }
1324
+ if (plan.version === "1.2" && mode === "slice") {
1325
+ mode = "dynamic";
1326
+ }
1327
+ attrs += ` mode="${mode}"`;
1328
+ let { idType } = meta;
1329
+ if (idType === null) {
1330
+ const { kind } = snapshot.ids;
1331
+ if (kind === "string") {
1332
+ idType = "string";
1333
+ }
1334
+ else if (kind !== "mixed") {
1335
+ idType = "integer";
1336
+ }
1337
+ }
1338
+ if (idType === "integer" || idType === "string") {
1339
+ attrs += ` idtype="${idType}"`;
1340
+ }
1341
+ if (plan.timeFormat !== null) {
1342
+ attrs += ` timeformat="${plan.timeFormat}"`;
1343
+ }
1344
+ else if (plan.temporal) {
1345
+ attrs += ' timeformat="double"';
1346
+ }
1347
+ if (plan.version === "1.3" && plan.timeRepresentation !== null) {
1348
+ attrs += ` timerepresentation="${plan.timeRepresentation}"`;
1349
+ }
1350
+ const gexfExtra = meta.extra.gexf;
1351
+ if (typeof gexfExtra === "object" && gexfExtra !== null) {
1352
+ for (const key of ["start", "end", "timestamp"]) {
1353
+ const value = gexfExtra[key];
1354
+ if (typeof value === "string") {
1355
+ attrs += ` ${key}="${escapeXmlAttribute(value)}"`;
1356
+ }
1357
+ }
1358
+ }
1359
+ return ` <graph${attrs}>\n`;
1360
+ }
1361
+ /** The GEXF exporter (design section 8.5); `capabilities` describes the default 1.3 output. */
1362
+ export const gexfExporter = Object.freeze({
1363
+ format: GEXF_FORMAT,
1364
+ capabilities: CAPABILITIES_1_3,
1365
+ /**
1366
+ * Pre-flight: what export() would lose.
1367
+ * @param snapshot - the snapshot
1368
+ * @param options - format-specific and common options
1369
+ * @returns the loss notes, empty when the export is exact
1370
+ */
1371
+ check(snapshot, options) {
1372
+ return Object.freeze(planExport(snapshot, options).notes);
1373
+ },
1374
+ /**
1375
+ * Write the snapshot as UTF-8 chunks.
1376
+ * @param snapshot - the snapshot
1377
+ * @param options - format-specific and common options
1378
+ * @returns the chunks
1379
+ */
1380
+ export(snapshot, options) {
1381
+ return encodeChunks(writeGexf(snapshot, options));
1382
+ },
1383
+ /**
1384
+ * Write the snapshot as one string.
1385
+ * @param snapshot - the snapshot
1386
+ * @param options - format-specific and common options
1387
+ * @returns the document
1388
+ */
1389
+ async exportToString(snapshot, options) {
1390
+ return joinText(writeGexf(snapshot, options));
1391
+ },
1392
+ });
1393
+ /** The capabilities of a 1.2 export, for callers that pass `version: "1.2"`. */
1394
+ export const GEXF_1_2_CAPABILITIES = CAPABILITIES_1_2;
1395
+ //# sourceMappingURL=exporter.js.map