@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,1404 @@
1
+ /**
2
+ * The GML exporter (design section 8.5, research note 07 sections 2.2 and 9): writes a snapshot in
3
+ * the NetworkX dialect of GML. Node ids must be integers (`idCharset: "integer"`; `sanitizeIds:
4
+ * "mangle"` renumbers the others and keeps the original in a `graphty_originalId` key the importer
5
+ * restores); columns are written per dtype (`int` for the integer dtypes, `real` with a decimal
6
+ * point guaranteed for f32 / f64 so the dtype survives a re-import, quoted strings with `&#NN;`
7
+ * references, repeated keys for lists with the `_networkx_list_start` marker for one-element
8
+ * lists and `"[]"` for empty ones, nested records for json objects); the position role becomes
9
+ * `graphics [ x y z ]` merged with the node's `graphics` json record; graph columns are written
10
+ * inside `graph [ ]` (or at the top level when the importer found them there); `Creator` and
11
+ * `Version` come from the metadata.
12
+ *
13
+ * check() reports, before anything is written, the capability gaps of the common check plus the
14
+ * GML-specific ones: json columns holding numbers (JSON cannot keep the int / real distinction:
15
+ * `W_GML_RECORD_NUMBER_TYPE`), booleans (written 1 / 0) or nulls (omitted), arrays nested in
16
+ * arrays (no GML spelling; export() throws), column names and record keys that are not GML keys
17
+ * or collide with the structural keys (`E_GML_INVALID_KEY` / `E_GML_RESERVED_KEY`, or
18
+ * `W_GML_KEY_MANGLED` under `sanitizeKeys: "mangle"`), and graphics / position overlaps.
19
+ */
20
+
21
+ import {
22
+ type Column,
23
+ type ColumnMeta,
24
+ GraphFormatError,
25
+ type GraphSnapshot,
26
+ type JsonColumn,
27
+ type NodeId,
28
+ } from "@graphty/graph-format";
29
+
30
+ import { DICT_SAMPLE_ROWS, DictHeuristic } from "../../common/attributes.js";
31
+ import { type PairFolding, pairFolding } from "../../common/direction.js";
32
+ import { quoteGmlString } from "../../common/escape.js";
33
+ import {
34
+ capabilities,
35
+ checkCapabilities,
36
+ countMixedEdges,
37
+ LOSS,
38
+ type SanitizedIds,
39
+ sanitizeIds,
40
+ } from "../../common/export.js";
41
+ import { formatGmlReal, formatInteger } from "../../common/format.js";
42
+ import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
43
+ import { type ExplicitWeights, explicitWeights } from "../../common/weights.js";
44
+ import { encodeChunks, joinText } from "../../common/writer.js";
45
+ import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
46
+ import { EMPTY_LIST_TEXT, isGmlKey, LIST_START_MARKER, mangleGmlKey, ORIGINAL_ID_KEY } from "./syntax.js";
47
+
48
+ /** The format-specific options of the GML exporter. */
49
+ export interface GmlExportOptions {
50
+ /**
51
+ * The edge key explicit weights are written under; by default the key the GML importer read
52
+ * them from (`meta.weightOrigin.id` when the snapshot came from GML), else `value`.
53
+ */
54
+ weightKey?: string | undefined;
55
+ /**
56
+ * "error" (default): a column name or record key that is not a GML key (`[A-Za-z][0-9A-Za-z_]*`)
57
+ * or collides with a structural key makes export() throw; "mangle": such keys are rewritten
58
+ * (`.` and other characters become `_`, collisions get a `_2` suffix) and check() reports them.
59
+ */
60
+ sanitizeKeys?: "error" | "mangle" | undefined;
61
+ }
62
+
63
+ /** Loss code: a json column holds numbers; JSON cannot keep GML's int / real distinction (design section 8.5). */
64
+ export const RECORD_NUMBER_TYPE_CODE = "W_GML_RECORD_NUMBER_TYPE";
65
+ /** Loss code: a json column holds booleans, written as 1 / 0. */
66
+ export const RECORD_BOOLEAN_CODE = "W_GML_RECORD_BOOLEAN";
67
+ /** Loss code: a json column holds nulls, which GML cannot write; the key (or the row) is omitted. */
68
+ export const RECORD_NULL_CODE = "W_GML_RECORD_NULL";
69
+ /** Loss code: a json column holds an array inside an array, which GML cannot write; export() throws. */
70
+ export const NESTED_ARRAY_CODE = "E_GML_NESTED_ARRAY";
71
+ /** Loss code: a json column holds arrays as row values; written as repeated keys, they re-import as a list column. */
72
+ export const JSON_ARRAY_CODE = "W_GML_JSON_ARRAY";
73
+ /** Loss code: a column name or record key is not a GML key; export() throws unless sanitizeKeys is "mangle". */
74
+ export const INVALID_KEY_CODE = "E_GML_INVALID_KEY";
75
+ /** Loss code: a column name collides with a structural GML key; export() throws unless sanitizeKeys is "mangle". */
76
+ export const RESERVED_KEY_CODE = "E_GML_RESERVED_KEY";
77
+ /** Loss code: keys rewritten under sanitizeKeys "mangle". */
78
+ export const KEY_MANGLED_CODE = "W_GML_KEY_MANGLED";
79
+ /** Loss code: a position column with more than three components; x, y and z are written. */
80
+ export const POSITION_COMPONENTS_CODE = "W_GML_POSITION_COMPONENTS";
81
+ /** Loss code: a node's graphics record has x / y / z keys the position column replaces. */
82
+ export const GRAPHICS_OVERRIDDEN_CODE = "W_GML_GRAPHICS_OVERRIDDEN";
83
+ /** Loss code: a node's graphics value is not a record and cannot hold the position; export() throws. */
84
+ export const GRAPHICS_CONFLICT_CODE = "E_GML_GRAPHICS_CONFLICT";
85
+
86
+ /** The default weight key (design section 8.4: GML weights are read from `value` by default). */
87
+ export const DEFAULT_WEIGHT_KEY = "value";
88
+
89
+ /** The roles GML has a key for (`label`, the edge `id` and `key`); every other role is reported. */
90
+ const KEPT_ROLES: ReadonlySet<string> = new Set(["label", "id", "key"]);
91
+
92
+ /** The key the importer maps each kept role back from (the column name after re-import). */
93
+ const ROLE_NAMES: Readonly<Record<string, string>> = Object.freeze({
94
+ label: "label",
95
+ id: "id",
96
+ key: "key",
97
+ });
98
+
99
+ const GML_CAPABILITIES: ExportCapabilities = capabilities({
100
+ mixedDirection: false,
101
+ multiEdges: true,
102
+ selfLoops: true,
103
+ edgeIds: "optional",
104
+ idCharset: "integer",
105
+ dtypes: ["i32", "f64", "string", "dict", "json"],
106
+ components: false,
107
+ lists: true,
108
+ json: true,
109
+ defaults: false,
110
+ options: false,
111
+ hierarchy: false,
112
+ temporal: "none",
113
+ graphAttributes: true,
114
+ positions: true,
115
+ viz: false,
116
+ });
117
+
118
+ /** Roles whose columns are never written as attributes (structural, or dropped per the capabilities). */
119
+ const SKIPPED_ROLES: ReadonlySet<string> = new Set([
120
+ "directed",
121
+ "pair",
122
+ "mutual",
123
+ "weight",
124
+ "timeText",
125
+ "originalId",
126
+ "position",
127
+ "color",
128
+ "size",
129
+ "shape",
130
+ "thickness",
131
+ "parent",
132
+ "parents",
133
+ "start",
134
+ "end",
135
+ "timestamp",
136
+ "timestamps",
137
+ "spells",
138
+ "open",
139
+ ]);
140
+
141
+ const NODE_RESERVED: ReadonlySet<string> = new Set(["id"]);
142
+ const EDGE_RESERVED: ReadonlySet<string> = new Set(["source", "target"]);
143
+ const GRAPH_RESERVED: ReadonlySet<string> = new Set(["node", "edge", "directed", "multigraph"]);
144
+ const TOP_RESERVED: ReadonlySet<string> = new Set(["graph"]);
145
+ const NUMBER_TEXT = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
146
+
147
+ /** The resolved options of one export. */
148
+ interface GmlExportPlan {
149
+ readonly common: ResolvedExportOptions;
150
+ readonly weightKey: string;
151
+ readonly mangle: boolean;
152
+ }
153
+
154
+ /** What one json column holds that GML cannot write exactly. */
155
+ interface JsonStats {
156
+ numbers: number;
157
+ booleans: number;
158
+ nulls: number;
159
+ nestedArrays: number;
160
+ arrays: number;
161
+ invalidKeys: number;
162
+ }
163
+
164
+ /** The kinds of value already counted for one row while inspecting a json value. */
165
+ interface SeenFlags {
166
+ numbers: boolean;
167
+ booleans: boolean;
168
+ nulls: boolean;
169
+ nested: boolean;
170
+ }
171
+
172
+ /** The columns of one table selected for writing, with their keys. */
173
+ interface WrittenColumn {
174
+ readonly column: Column;
175
+ readonly key: string;
176
+ }
177
+
178
+ /**
179
+ * Resolve the options of one export call.
180
+ * @param snapshot - the snapshot
181
+ * @param options - the caller's options
182
+ * @returns the plan
183
+ */
184
+ function planOf(snapshot: GraphSnapshot, options: (GmlExportOptions & CommonExportOptions) | undefined): GmlExportPlan {
185
+ const common = resolveExportOptions(options);
186
+ const mode = options?.sanitizeKeys ?? "error";
187
+ if (mode !== "error" && mode !== "mangle") {
188
+ throw new GraphFormatError(
189
+ "E_UNSUPPORTED",
190
+ `option sanitizeKeys: ${JSON.stringify(mode)} is not "error" or "mangle"`,
191
+ {
192
+ option: "sanitizeKeys",
193
+ found: mode,
194
+ },
195
+ );
196
+ }
197
+ let weightKey = options?.weightKey;
198
+ if (weightKey === undefined) {
199
+ const origin = snapshot.meta.weightOrigin;
200
+ weightKey = origin !== null && origin.format === "gml" && origin.id !== null ? origin.id : DEFAULT_WEIGHT_KEY;
201
+ } else if (typeof weightKey !== "string" || !isGmlKey(weightKey)) {
202
+ throw new GraphFormatError("E_UNSUPPORTED", `option weightKey: ${JSON.stringify(weightKey)} is not a GML key`, {
203
+ option: "weightKey",
204
+ found: weightKey,
205
+ });
206
+ }
207
+ return { common, weightKey, mangle: mode === "mangle" };
208
+ }
209
+
210
+ /**
211
+ * Whether a column is written as an attribute (its role is neither structural nor dropped).
212
+ * @param column - the column
213
+ * @returns true when written
214
+ */
215
+ function isWritten(column: Column): boolean {
216
+ const { role } = column.meta;
217
+ return role === null || !SKIPPED_ROLES.has(role);
218
+ }
219
+
220
+ /**
221
+ * The key a column is written under.
222
+ * @param meta - the column metadata
223
+ * @returns origin.id when the column came from GML (the key it was read from), else the name
224
+ */
225
+ function preferredKey(meta: ColumnMeta): string {
226
+ const { origin } = meta;
227
+ return origin !== null && origin.format === "gml" && origin.id !== null ? origin.id : meta.name;
228
+ }
229
+
230
+ /**
231
+ * Whether a snapshot has explicit weights to write.
232
+ * @param snapshot - the snapshot
233
+ * @returns true when weighted
234
+ */
235
+ function hasWeights(snapshot: GraphSnapshot): boolean {
236
+ return snapshot.flags.weighted;
237
+ }
238
+
239
+ /**
240
+ * Whether the graph column is a top-level key (the importer's `extra.gmlTopLevel`).
241
+ * @param column - the column
242
+ * @returns true for a top-level key
243
+ */
244
+ function isTopLevel(column: Column): boolean {
245
+ return column.meta.extra.gmlTopLevel === true;
246
+ }
247
+
248
+ /**
249
+ * The text of a `Version` value: bare when it is a number text, quoted otherwise.
250
+ * @param text - the version text
251
+ * @returns the GML value
252
+ */
253
+ function versionText(text: string): string {
254
+ return NUMBER_TEXT.test(text) ? text : quoteGmlString(text);
255
+ }
256
+
257
+ /**
258
+ * Select the written columns of a table and assign their keys, recording invalid and reserved
259
+ * keys as notes (or rewriting them under mangle).
260
+ * @param table - the columns
261
+ * @param reserved - the structural keys of the table
262
+ * @param label - the table name for messages
263
+ * @param plan - the export plan
264
+ * @param notes - where to record
265
+ * @param filter - an extra selection predicate
266
+ * @returns the written columns with their keys
267
+ */
268
+ function selectColumns(
269
+ table: Iterable<Column>,
270
+ reserved: ReadonlySet<string>,
271
+ label: string,
272
+ plan: GmlExportPlan,
273
+ notes: LossNote[] | null,
274
+ filter: (column: Column) => boolean = (): boolean => true,
275
+ ): WrittenColumn[] {
276
+ const used = new Set<string>(reserved);
277
+ const out: WrittenColumn[] = [];
278
+ for (const column of table) {
279
+ if (!isWritten(column) || !filter(column)) {
280
+ continue;
281
+ }
282
+ const preferred = preferredKey(column.meta);
283
+ let key = preferred;
284
+ const valid = isGmlKey(preferred);
285
+ const taken = used.has(preferred);
286
+ if (!valid || taken) {
287
+ const code = valid ? RESERVED_KEY_CODE : INVALID_KEY_CODE;
288
+ let why = "is not a GML key";
289
+ if (valid) {
290
+ why = reserved.has(preferred) ? "collides with a structural GML key" : "repeats another column's key";
291
+ }
292
+ if (!plan.mangle) {
293
+ if (notes === null) {
294
+ throw new GraphFormatError(
295
+ "E_UNSUPPORTED",
296
+ `${label} column "${column.meta.name}" ${why}; pass sanitizeKeys: "mangle" to rewrite it`,
297
+ {
298
+ reason: "gml key",
299
+ column: column.meta.name,
300
+ key: preferred,
301
+ },
302
+ );
303
+ }
304
+ notes.push(
305
+ note(
306
+ code,
307
+ `${label} column "${column.meta.name}" ${why}; export() will throw unless sanitizeKeys is "mangle"`,
308
+ column.meta.name,
309
+ ),
310
+ );
311
+ continue;
312
+ }
313
+ key = uniqueKey(valid ? preferred : mangleGmlKey(preferred), used);
314
+ notes?.push(
315
+ note(
316
+ KEY_MANGLED_CODE,
317
+ `${label} column "${column.meta.name}" is written as "${key}"`,
318
+ column.meta.name,
319
+ ),
320
+ );
321
+ }
322
+ used.add(key);
323
+ out.push({ column, key });
324
+ }
325
+ return out;
326
+ }
327
+
328
+ /**
329
+ * A key not yet used: the base, else base_2, base_3...
330
+ * @param base - a valid key
331
+ * @param used - the keys taken
332
+ * @returns a free key
333
+ */
334
+ function uniqueKey(base: string, used: ReadonlySet<string>): string {
335
+ let candidate = base;
336
+ for (let k = 2; used.has(candidate); k++) {
337
+ candidate = `${base}_${k}`;
338
+ }
339
+ return candidate;
340
+ }
341
+
342
+ /**
343
+ * Build a frozen note.
344
+ * @param code - the code
345
+ * @param message - the message
346
+ * @param column - the column, or null
347
+ * @param count - the count, or null
348
+ * @returns the note
349
+ */
350
+ function note(code: string, message: string, column: string | null = null, count: number | null = null): LossNote {
351
+ return Object.freeze({ code, message, column, count });
352
+ }
353
+
354
+ /**
355
+ * Inspect a JSON value for what GML cannot write exactly.
356
+ * @param value - the value
357
+ * @param stats - the counters to update (each counted at most once per call for numbers / booleans / nulls)
358
+ * @param inArray - whether the value is an array item
359
+ * @param seen - the flags already counted for this row
360
+ */
361
+ function inspectJson(value: unknown, stats: JsonStats, inArray: boolean, seen: SeenFlags): void {
362
+ if (value === null) {
363
+ if (!seen.nulls) {
364
+ seen.nulls = true;
365
+ stats.nulls++;
366
+ }
367
+ return;
368
+ }
369
+ switch (typeof value) {
370
+ case "number":
371
+ if (!seen.numbers) {
372
+ seen.numbers = true;
373
+ stats.numbers++;
374
+ }
375
+ return;
376
+ case "boolean":
377
+ if (!seen.booleans) {
378
+ seen.booleans = true;
379
+ stats.booleans++;
380
+ }
381
+ return;
382
+ case "string":
383
+ return;
384
+ default:
385
+ break;
386
+ }
387
+ if (Array.isArray(value)) {
388
+ if (inArray && !seen.nested) {
389
+ seen.nested = true;
390
+ stats.nestedArrays++;
391
+ }
392
+ for (const item of value) {
393
+ inspectJson(item, stats, true, seen);
394
+ }
395
+ return;
396
+ }
397
+ const record = value as Record<string, unknown>;
398
+ for (const key of Object.keys(record)) {
399
+ if (!isGmlKey(key)) {
400
+ stats.invalidKeys++;
401
+ }
402
+ inspectJson(record[key], stats, false, seen);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * The GML-specific notes of one table's json columns.
408
+ * @param columns - the written columns
409
+ * @param label - the table name
410
+ * @param plan - the export plan
411
+ * @param notes - where to record
412
+ */
413
+ function jsonNotes(columns: readonly WrittenColumn[], label: string, plan: GmlExportPlan, notes: LossNote[]): void {
414
+ for (const { column } of columns) {
415
+ const stats = jsonStatsOf(column);
416
+ if (stats === null) {
417
+ continue;
418
+ }
419
+ const { name } = column.meta;
420
+ const where = `${label} column "${name}"`;
421
+ if (stats.numbers > 0) {
422
+ notes.push(
423
+ note(
424
+ RECORD_NUMBER_TYPE_CODE,
425
+ `${where} holds numbers in ${stats.numbers} row(s); GML records cannot keep the int / real distinction`,
426
+ name,
427
+ stats.numbers,
428
+ ),
429
+ );
430
+ }
431
+ if (stats.booleans > 0) {
432
+ notes.push(
433
+ note(
434
+ RECORD_BOOLEAN_CODE,
435
+ `${where} holds booleans in ${stats.booleans} row(s); written as 1 / 0`,
436
+ name,
437
+ stats.booleans,
438
+ ),
439
+ );
440
+ }
441
+ if (stats.nulls > 0) {
442
+ notes.push(
443
+ note(
444
+ RECORD_NULL_CODE,
445
+ `${where} holds nulls in ${stats.nulls} row(s); GML has no null, the key is omitted`,
446
+ name,
447
+ stats.nulls,
448
+ ),
449
+ );
450
+ }
451
+ if (stats.arrays > 0) {
452
+ notes.push(
453
+ note(
454
+ JSON_ARRAY_CODE,
455
+ `${where} holds arrays as values in ${stats.arrays} row(s); written as repeated keys, they re-import as a list`,
456
+ name,
457
+ stats.arrays,
458
+ ),
459
+ );
460
+ }
461
+ if (stats.nestedArrays > 0) {
462
+ notes.push(
463
+ note(
464
+ NESTED_ARRAY_CODE,
465
+ `${where} holds arrays nested in arrays in ${stats.nestedArrays} row(s); GML cannot write them and export() will throw`,
466
+ name,
467
+ stats.nestedArrays,
468
+ ),
469
+ );
470
+ }
471
+ if (stats.invalidKeys > 0) {
472
+ notes.push(
473
+ plan.mangle
474
+ ? note(
475
+ KEY_MANGLED_CODE,
476
+ `${where}: ${stats.invalidKeys} record key(s) are not GML keys and are rewritten`,
477
+ name,
478
+ stats.invalidKeys,
479
+ )
480
+ : note(
481
+ INVALID_KEY_CODE,
482
+ `${where}: ${stats.invalidKeys} record key(s) are not GML keys; export() will throw unless sanitizeKeys is "mangle"`,
483
+ name,
484
+ stats.invalidKeys,
485
+ ),
486
+ );
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * The json statistics of a json column, or of a list column with json items; null for other dtypes.
493
+ * @param column - the column
494
+ * @returns the stats, or null
495
+ */
496
+ function jsonStatsOf(column: Column): JsonStats | null {
497
+ const stats: JsonStats = { numbers: 0, booleans: 0, nulls: 0, nestedArrays: 0, arrays: 0, invalidKeys: 0 };
498
+ if (column.dtype === "json") {
499
+ for (let r = 0; r < column.length; r++) {
500
+ if (!column.isSet(r)) {
501
+ continue;
502
+ }
503
+ const value = column.values[r];
504
+ const seen: SeenFlags = { numbers: false, booleans: false, nulls: false, nested: false };
505
+ if (Array.isArray(value)) {
506
+ stats.arrays++;
507
+ for (const item of value) {
508
+ inspectJson(item, stats, true, seen);
509
+ }
510
+ } else {
511
+ inspectJson(value, stats, false, seen);
512
+ }
513
+ }
514
+ return stats;
515
+ }
516
+ if (column.dtype === "list" && column.meta.itemDtype === "json") {
517
+ for (let r = 0; r < column.length; r++) {
518
+ if (!column.isSet(r)) {
519
+ continue;
520
+ }
521
+ const seen: SeenFlags = { numbers: false, booleans: false, nulls: false, nested: false };
522
+ for (const item of column.sliceOf(r)) {
523
+ inspectJson(item, stats, true, seen);
524
+ }
525
+ }
526
+ return stats;
527
+ }
528
+ return null;
529
+ }
530
+
531
+ /**
532
+ * The node position column when it can be mapped to graphics x / y / z: a numeric scalar column
533
+ * with the position role.
534
+ * @param snapshot - the snapshot
535
+ * @returns the column, or null
536
+ */
537
+ function positionColumn(snapshot: GraphSnapshot): Column | null {
538
+ const column = snapshot.nodes.byRole("position");
539
+ if (column === null) {
540
+ return null;
541
+ }
542
+ switch (column.dtype) {
543
+ case "f32":
544
+ case "f64":
545
+ case "i32":
546
+ case "u32":
547
+ case "u8":
548
+ return column;
549
+ default:
550
+ return null;
551
+ }
552
+ }
553
+
554
+ /**
555
+ * The node `graphics` json column, when present.
556
+ * @param snapshot - the snapshot
557
+ * @returns the column, or null
558
+ */
559
+ function graphicsColumn(snapshot: GraphSnapshot): JsonColumn | null {
560
+ return snapshot.nodes.typed("graphics", "json");
561
+ }
562
+
563
+ /**
564
+ * The GML-specific notes about the position / graphics mapping.
565
+ * @param snapshot - the snapshot
566
+ * @param notes - where to record
567
+ */
568
+ function graphicsNotes(snapshot: GraphSnapshot, notes: LossNote[]): void {
569
+ const position = positionColumn(snapshot);
570
+ if (position === null) {
571
+ return;
572
+ }
573
+ if (position.meta.components > 3) {
574
+ notes.push(
575
+ note(
576
+ POSITION_COMPONENTS_CODE,
577
+ `node column "${position.meta.name}" has ${position.meta.components} components; only x, y and z are written`,
578
+ position.meta.name,
579
+ position.length - position.nullCount,
580
+ ),
581
+ );
582
+ }
583
+ const graphics = graphicsColumn(snapshot);
584
+ if (graphics === null) {
585
+ return;
586
+ }
587
+ let overridden = 0;
588
+ let conflicts = 0;
589
+ for (let r = 0; r < snapshot.nodeCount; r++) {
590
+ if (!position.isSet(r) || !graphics.isSet(r)) {
591
+ continue;
592
+ }
593
+ const value = graphics.values[r];
594
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
595
+ conflicts++;
596
+ } else if ("x" in value || "y" in value || "z" in value) {
597
+ overridden++;
598
+ }
599
+ }
600
+ if (overridden > 0) {
601
+ notes.push(
602
+ note(
603
+ GRAPHICS_OVERRIDDEN_CODE,
604
+ `${overridden} node graphics record(s) have x / y / z keys the position column replaces`,
605
+ "graphics",
606
+ overridden,
607
+ ),
608
+ );
609
+ }
610
+ if (conflicts > 0) {
611
+ notes.push(
612
+ note(
613
+ GRAPHICS_CONFLICT_CODE,
614
+ `${conflicts} node graphics value(s) are not records and cannot hold the position; export() will throw`,
615
+ "graphics",
616
+ conflicts,
617
+ ),
618
+ );
619
+ }
620
+ }
621
+
622
+ /**
623
+ * What the importer's own rules change on re-import of the written columns: an all-unset column
624
+ * vanishes (GML writes cells, never declarations), a role-less column named like a role key
625
+ * gains the role, and the dictionary heuristic of design section 5.4 turns a low-cardinality
626
+ * string column into a dict (or a wide dict into a string).
627
+ * @param columns - the written columns
628
+ * @param domain - node or edge
629
+ * @param notes - where to record
630
+ */
631
+ function reimportNotes(columns: readonly WrittenColumn[], domain: "node" | "edge", notes: LossNote[]): void {
632
+ for (const { column, key } of columns) {
633
+ const { meta } = column;
634
+ const set = column.length - column.nullCount;
635
+ if (set === 0) {
636
+ notes.push(
637
+ note(
638
+ LOSS.EMPTY_COLUMN,
639
+ `${domain} column "${meta.name}" has no set cell and is not written: GML writes cells, never declarations`,
640
+ meta.name,
641
+ 0,
642
+ ),
643
+ );
644
+ continue;
645
+ }
646
+ if (meta.role === null && (key === "label" || (domain === "edge" && (key === "id" || key === "key")))) {
647
+ notes.push(
648
+ note(
649
+ LOSS.ROLE_ASSUMED,
650
+ `${domain} column "${meta.name}" is written under "${key}" and reads back with the ${key === "label" ? "label" : key} role`,
651
+ meta.name,
652
+ set,
653
+ ),
654
+ );
655
+ continue;
656
+ }
657
+ if (column.dtype === "list" || !(column.dtype === "string" || column.dtype === "dict")) {
658
+ continue;
659
+ }
660
+ const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
661
+ for (let r = 0; r < column.length && !heuristic.decided; r++) {
662
+ if (column.isSet(r)) {
663
+ heuristic.observe(column.value(r) as string);
664
+ }
665
+ }
666
+ const readsAsDict = heuristic.decide() === "dict";
667
+ if (column.dtype === "string" && readsAsDict) {
668
+ notes.push(
669
+ note(
670
+ LOSS.STORAGE_CLASS,
671
+ `${domain} column "${meta.name}" reads back as dict (low cardinality)`,
672
+ meta.name,
673
+ null,
674
+ ),
675
+ );
676
+ } else if (column.dtype === "dict" && !readsAsDict) {
677
+ notes.push(
678
+ note(
679
+ LOSS.STORAGE_CLASS,
680
+ `${domain} column "${meta.name}" reads back as string (cardinality too high for a dict)`,
681
+ meta.name,
682
+ null,
683
+ ),
684
+ );
685
+ }
686
+ }
687
+ }
688
+
689
+ /**
690
+ * The importer names the graphics-derived position column `position`, or `position#graphics`
691
+ * when a plain `position` key is written next to it (design section 5.6); a position column
692
+ * named otherwise reads back under that name.
693
+ * @param snapshot - the snapshot
694
+ * @param nodeColumns - the written node columns
695
+ * @param notes - where to record
696
+ */
697
+ function positionNameNote(snapshot: GraphSnapshot, nodeColumns: readonly WrittenColumn[], notes: LossNote[]): void {
698
+ const position = positionColumn(snapshot);
699
+ if (position === null) {
700
+ return;
701
+ }
702
+ const { name } = position.meta;
703
+ const plainPosition = nodeColumns.some((c) => c.key === "position");
704
+ const reimportName = plainPosition ? "position#graphics" : "position";
705
+ if (name !== reimportName) {
706
+ notes.push(
707
+ note(
708
+ LOSS.COLUMN_NAME_CHANGED,
709
+ `node column "${name}" (position) is written as the graphics x / y / z keys and reads back as "${reimportName}"`,
710
+ name,
711
+ position.length - position.nullCount,
712
+ ),
713
+ );
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Pre-flight: the common capability notes plus the GML-specific ones.
719
+ * @param snapshot - the snapshot
720
+ * @param options - the options
721
+ * @returns the notes, empty when the export is exact
722
+ */
723
+ function check(snapshot: GraphSnapshot, options?: GmlExportOptions & CommonExportOptions): readonly LossNote[] {
724
+ const plan = planOf(snapshot, options);
725
+ const notes = checkCapabilities(snapshot, GML_CAPABILITIES, plan.common, {
726
+ positionDtype: "f64",
727
+ roles: KEPT_ROLES,
728
+ roleNames: ROLE_NAMES,
729
+ });
730
+ const nodeReserved = new Set(NODE_RESERVED);
731
+ if (
732
+ (plan.common.sanitizeIds === "mangle" && countMangled(snapshot) > 0) ||
733
+ snapshot.nodes.byRole("originalId") !== null
734
+ ) {
735
+ nodeReserved.add(ORIGINAL_ID_KEY);
736
+ }
737
+ const edgeIds = snapshot.edges.byRole("id");
738
+ const edgeReserved = edgeReservedKeys(snapshot, plan, edgeIds);
739
+ const topReserved = topReservedKeys(snapshot);
740
+ const nodeColumns = selectColumns(snapshot.nodes, nodeReserved, "node", plan, notes);
741
+ const edgeColumns = selectColumns(snapshot.edges, edgeReserved, "edge", plan, notes, (c) => c !== edgeIds);
742
+ const graphColumns = selectColumns(snapshot.graph, GRAPH_RESERVED, "graph", plan, notes, (c) => !isTopLevel(c));
743
+ const topColumns = selectColumns(snapshot.graph, topReserved, "top-level", plan, notes, isTopLevel);
744
+ jsonNotes(nodeColumns, "node", plan, notes);
745
+ jsonNotes(edgeColumns, "edge", plan, notes);
746
+ jsonNotes(graphColumns, "graph", plan, notes);
747
+ jsonNotes(topColumns, "top-level", plan, notes);
748
+ graphicsNotes(snapshot, notes);
749
+ positionNameNote(snapshot, nodeColumns, notes);
750
+ reimportNotes(nodeColumns, "node", notes);
751
+ reimportNotes(edgeColumns, "edge", notes);
752
+ if (!hasWeights(snapshot)) {
753
+ const clash = edgeColumns.find((c) => c.key === plan.weightKey);
754
+ if (clash !== undefined) {
755
+ notes.push(
756
+ note(
757
+ LOSS.WEIGHT_KEY_CLASH,
758
+ `edge column "${clash.column.meta.name}" is written under "${plan.weightKey}", the key the importer reads THE weight from; it reads back as the weight, not as a column`,
759
+ clash.column.meta.name,
760
+ clash.column.length - clash.column.nullCount,
761
+ ),
762
+ );
763
+ }
764
+ }
765
+ const folding = pairFolding(snapshot);
766
+ if (folding.mutualCount > 0) {
767
+ notes.push(
768
+ note(
769
+ LOSS.MUTUAL_EXPANDED,
770
+ `${folding.mutualCount} mutual pair(s) are written as two directed edges; the mutual mark is lost`,
771
+ null,
772
+ folding.mutualCount,
773
+ ),
774
+ );
775
+ }
776
+ return Object.freeze(notes);
777
+ }
778
+
779
+ /**
780
+ * The structural keys of the edge table: source, target, the weight key when weights are written,
781
+ * and `id` when a role-id column is written as the edge id.
782
+ * @param snapshot - the snapshot
783
+ * @param plan - the export plan
784
+ * @param edgeIds - the role-id edge column, or null
785
+ * @returns the reserved keys
786
+ */
787
+ function edgeReservedKeys(snapshot: GraphSnapshot, plan: GmlExportPlan, edgeIds: Column | null): Set<string> {
788
+ const reserved = new Set(EDGE_RESERVED);
789
+ if (hasWeights(snapshot)) {
790
+ reserved.add(plan.weightKey);
791
+ }
792
+ if (edgeIds !== null) {
793
+ reserved.add("id");
794
+ }
795
+ return reserved;
796
+ }
797
+
798
+ /**
799
+ * The structural keys of the top level: graph, plus Creator and Version when the metadata writes them.
800
+ * @param snapshot - the snapshot
801
+ * @returns the reserved keys
802
+ */
803
+ function topReservedKeys(snapshot: GraphSnapshot): Set<string> {
804
+ const reserved = new Set(TOP_RESERVED);
805
+ if (snapshot.meta.creator !== null) {
806
+ reserved.add("Creator");
807
+ }
808
+ if (snapshot.meta.sourceFormat === "gml" && snapshot.meta.sourceVersion !== null) {
809
+ reserved.add("Version");
810
+ }
811
+ return reserved;
812
+ }
813
+
814
+ /**
815
+ * How many node ids are not safe integers (what sanitizeIds "mangle" rewrites).
816
+ * @param snapshot - the snapshot
817
+ * @returns the count
818
+ */
819
+ function countMangled(snapshot: GraphSnapshot): number {
820
+ let count = 0;
821
+ for (let i = 0; i < snapshot.nodeCount; i++) {
822
+ const id = snapshot.ids.idOf(i);
823
+ if (typeof id !== "number" || !Number.isSafeInteger(id)) {
824
+ count++;
825
+ }
826
+ }
827
+ return count;
828
+ }
829
+
830
+ // ============================================================ writing
831
+
832
+ /**
833
+ * The GML text of a real: the shortest text of the dtype with a decimal point guaranteed
834
+ * (`2.0`, `1.0e-7`), the NetworkX spellings `+INF` / `-INF` / `NAN` for the non-finite values.
835
+ * @param value - the value
836
+ * @param dtype - the dtype the value came from (f32 uses the fround-shortest text)
837
+ * @returns the text
838
+ */
839
+ export function gmlRealText(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8" = "f64"): string {
840
+ return formatGmlReal(value, dtype);
841
+ }
842
+
843
+ /**
844
+ * The GML text of a number by the dtype and origin of its column: an integer text for the integer
845
+ * dtypes and for an f64 column that came from GML `int` values, a real text otherwise.
846
+ * @param value - the value
847
+ * @param meta - the column metadata
848
+ * @returns the text
849
+ */
850
+ function numberText(value: number, meta: ColumnMeta): string {
851
+ switch (meta.dtype) {
852
+ case "i32":
853
+ case "u32":
854
+ case "u8":
855
+ return formatInteger(value);
856
+ case "f32":
857
+ return gmlRealText(value, "f32");
858
+ default:
859
+ if (meta.origin?.type === "int" && Number.isInteger(value)) {
860
+ return formatInteger(value);
861
+ }
862
+ return gmlRealText(value, "f64");
863
+ }
864
+ }
865
+
866
+ /**
867
+ * The GML text of a JSON scalar inside a record: integers as int, other numbers as real, strings
868
+ * quoted, booleans as 1 / 0.
869
+ * @param value - the scalar
870
+ * @returns the text
871
+ */
872
+ function jsonScalarText(value: number | string | boolean): string {
873
+ switch (typeof value) {
874
+ case "number":
875
+ return Number.isInteger(value) ? formatInteger(value) : gmlRealText(value, "f64");
876
+ case "boolean":
877
+ return value ? "1" : "0";
878
+ default:
879
+ return quoteGmlString(value);
880
+ }
881
+ }
882
+
883
+ /**
884
+ * The GML text of a list item or components lane by the item dtype.
885
+ * @param item - the item
886
+ * @param itemDtype - the list's item dtype
887
+ * @param meta - the list column metadata
888
+ * @returns the text
889
+ */
890
+ function itemText(item: unknown, itemDtype: string, meta: ColumnMeta): string {
891
+ switch (typeof item) {
892
+ case "number":
893
+ switch (itemDtype) {
894
+ case "i32":
895
+ case "u32":
896
+ case "u8":
897
+ return formatInteger(item);
898
+ case "f32":
899
+ return gmlRealText(item, "f32");
900
+ case "json":
901
+ return jsonScalarText(item);
902
+ default:
903
+ return meta.origin?.type === "int" && Number.isInteger(item)
904
+ ? formatInteger(item)
905
+ : gmlRealText(item, "f64");
906
+ }
907
+ case "boolean":
908
+ return item ? "1" : "0";
909
+ case "string":
910
+ return quoteGmlString(item);
911
+ default:
912
+ throw new GraphFormatError(
913
+ "E_COLUMN_TYPE",
914
+ `column "${meta.name}": a ${typeof item} list item cannot be written as GML`,
915
+ {
916
+ column: meta.name,
917
+ found: typeof item,
918
+ },
919
+ );
920
+ }
921
+ }
922
+
923
+ /** Writes lines with indentation into an array of parts. */
924
+ class GmlWriter {
925
+ private readonly lines: string[] = [];
926
+
927
+ private readonly mangle: boolean;
928
+
929
+ /**
930
+ * Create a writer.
931
+ * @param mangle - whether record keys are rewritten rather than refused
932
+ */
933
+ constructor(mangle: boolean) {
934
+ this.mangle = mangle;
935
+ }
936
+
937
+ /**
938
+ * Take the lines written so far as one string.
939
+ * @returns the text; the writer is empty afterwards
940
+ */
941
+ take(): string {
942
+ const text = this.lines.join("");
943
+ this.lines.length = 0;
944
+ return text;
945
+ }
946
+
947
+ /**
948
+ * Write one `key value` line.
949
+ * @param indent - the indentation
950
+ * @param key - the key
951
+ * @param value - the value text
952
+ */
953
+ line(indent: string, key: string, value: string): void {
954
+ this.lines.push(`${indent}${key} ${value}\n`);
955
+ }
956
+
957
+ /**
958
+ * Write a raw line.
959
+ * @param text - the line without its terminator
960
+ */
961
+ raw(text: string): void {
962
+ this.lines.push(`${text}\n`);
963
+ }
964
+
965
+ /**
966
+ * Write one column cell of a row.
967
+ * @param indent - the indentation
968
+ * @param key - the key
969
+ * @param column - the column
970
+ * @param row - the row
971
+ */
972
+ cell(indent: string, key: string, column: Column, row: number): void {
973
+ const { meta, dtype } = column;
974
+ switch (dtype) {
975
+ case "f32":
976
+ case "f64":
977
+ case "i32":
978
+ case "u32":
979
+ case "u8": {
980
+ const { components } = meta;
981
+ if (components === 1) {
982
+ this.line(indent, key, numberText(column.data[row], meta));
983
+ } else {
984
+ for (let k = 0; k < components; k++) {
985
+ this.line(indent, key, numberText(column.data[row * components + k], meta));
986
+ }
987
+ }
988
+ return;
989
+ }
990
+ case "bool":
991
+ this.line(indent, key, column.value(row) === true ? "1" : "0");
992
+ return;
993
+ case "dict":
994
+ case "string":
995
+ this.line(indent, key, quoteGmlString(column.value(row) ?? ""));
996
+ return;
997
+ case "list":
998
+ this.list(indent, key, column.sliceOf(row), meta.itemDtype ?? "string", meta);
999
+ return;
1000
+ case "json":
1001
+ this.json(indent, key, column.values[row], meta.name);
1002
+ return;
1003
+ default: {
1004
+ const name: string = dtype;
1005
+ throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * Write a list as repeated keys with the NetworkX conventions.
1012
+ * @param indent - the indentation
1013
+ * @param key - the key
1014
+ * @param items - the items
1015
+ * @param itemDtype - the item dtype
1016
+ * @param meta - the column metadata
1017
+ */
1018
+ private list(indent: string, key: string, items: readonly unknown[], itemDtype: string, meta: ColumnMeta): void {
1019
+ if (items.length === 0) {
1020
+ this.line(indent, key, quoteGmlString(EMPTY_LIST_TEXT));
1021
+ return;
1022
+ }
1023
+ if (items.length === 1) {
1024
+ this.line(indent, key, quoteGmlString(LIST_START_MARKER));
1025
+ }
1026
+ for (const item of items) {
1027
+ if (itemDtype === "json" && typeof item === "object") {
1028
+ this.json(indent, key, item, meta.name, true);
1029
+ } else {
1030
+ this.line(indent, key, itemText(item, itemDtype, meta));
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ /**
1036
+ * Write a JSON value: a record for an object, repeated keys for an array, a scalar otherwise;
1037
+ * null writes nothing.
1038
+ * @param indent - the indentation
1039
+ * @param key - the key
1040
+ * @param value - the value
1041
+ * @param column - the column name, for errors
1042
+ * @param inArray - whether the value is an array item (an array here has no GML spelling)
1043
+ */
1044
+ private json(indent: string, key: string, value: unknown, column: string, inArray = false): void {
1045
+ if (value === null || value === undefined) {
1046
+ return;
1047
+ }
1048
+ if (Array.isArray(value)) {
1049
+ if (inArray) {
1050
+ throw new GraphFormatError(
1051
+ "E_UNSUPPORTED",
1052
+ `column "${column}": an array nested in an array cannot be written as GML`,
1053
+ {
1054
+ reason: "nested array",
1055
+ column,
1056
+ },
1057
+ );
1058
+ }
1059
+ if (value.length === 0) {
1060
+ this.line(indent, key, quoteGmlString(EMPTY_LIST_TEXT));
1061
+ return;
1062
+ }
1063
+ if (value.length === 1) {
1064
+ this.line(indent, key, quoteGmlString(LIST_START_MARKER));
1065
+ }
1066
+ for (const item of value) {
1067
+ this.json(indent, key, item, column, true);
1068
+ }
1069
+ return;
1070
+ }
1071
+ if (typeof value === "object") {
1072
+ this.record(indent, key, value as Record<string, unknown>, column);
1073
+ return;
1074
+ }
1075
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
1076
+ this.line(indent, key, jsonScalarText(value));
1077
+ return;
1078
+ }
1079
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${column}": a ${typeof value} cannot be written as GML`, {
1080
+ column,
1081
+ found: typeof value,
1082
+ });
1083
+ }
1084
+
1085
+ /**
1086
+ * Write a record `key [ ... ]`.
1087
+ * @param indent - the indentation
1088
+ * @param key - the key
1089
+ * @param record - the object
1090
+ * @param column - the column name, for errors
1091
+ * @param extra - lines to write first (the position of a graphics record), or null
1092
+ */
1093
+ record(
1094
+ indent: string,
1095
+ key: string,
1096
+ record: Record<string, unknown>,
1097
+ column: string,
1098
+ extra: readonly [string, string][] | null = null,
1099
+ ): void {
1100
+ this.raw(`${indent}${key} [`);
1101
+ const inner = `${indent} `;
1102
+ const used = new Set<string>();
1103
+ if (extra !== null) {
1104
+ for (const [k, v] of extra) {
1105
+ this.line(inner, k, v);
1106
+ used.add(k);
1107
+ }
1108
+ }
1109
+ for (const name of Object.keys(record)) {
1110
+ if (used.has(name) && extra !== null && (name === "x" || name === "y" || name === "z")) {
1111
+ continue;
1112
+ }
1113
+ const written = this.recordKey(name, used, column);
1114
+ used.add(written);
1115
+ this.json(inner, written, record[name], column);
1116
+ }
1117
+ this.raw(`${indent}]`);
1118
+ }
1119
+
1120
+ /**
1121
+ * The key a record field is written under.
1122
+ * @param name - the field name
1123
+ * @param used - the keys already written in the record
1124
+ * @param column - the column name, for errors
1125
+ * @returns a valid, unused key; E_UNSUPPORTED for an invalid key unless mangling
1126
+ */
1127
+ private recordKey(name: string, used: ReadonlySet<string>, column: string): string {
1128
+ if (isGmlKey(name) && !used.has(name)) {
1129
+ return name;
1130
+ }
1131
+ if (!this.mangle) {
1132
+ throw new GraphFormatError(
1133
+ "E_UNSUPPORTED",
1134
+ `column "${column}": record key "${name}" ${isGmlKey(name) ? "repeats a key" : "is not a GML key"}; pass sanitizeKeys: "mangle" to rewrite it`,
1135
+ {
1136
+ reason: "gml key",
1137
+ column,
1138
+ key: name,
1139
+ },
1140
+ );
1141
+ }
1142
+ return uniqueKey(isGmlKey(name) ? name : mangleGmlKey(name), used);
1143
+ }
1144
+ }
1145
+
1146
+ /** Everything the writer needs, resolved once before the first line. */
1147
+ interface WriteContext {
1148
+ readonly snapshot: GraphSnapshot;
1149
+ readonly plan: GmlExportPlan;
1150
+ readonly ids: SanitizedIds;
1151
+ readonly writeDirected: boolean;
1152
+ readonly foldPairs: boolean;
1153
+ readonly nodeColumns: readonly WrittenColumn[];
1154
+ readonly edgeColumns: readonly WrittenColumn[];
1155
+ readonly graphColumns: readonly WrittenColumn[];
1156
+ readonly topColumns: readonly WrittenColumn[];
1157
+ readonly position: Column | null;
1158
+ readonly graphics: JsonColumn | null;
1159
+ readonly originalIds: Column | null;
1160
+ readonly edgeIds: Column | null;
1161
+ readonly weights: ExplicitWeights;
1162
+ readonly folding: PairFolding;
1163
+ }
1164
+
1165
+ /**
1166
+ * Resolve everything export() needs, throwing for what check() reported as an error.
1167
+ * @param snapshot - the snapshot
1168
+ * @param options - the options
1169
+ * @returns the context
1170
+ */
1171
+ function contextOf(
1172
+ snapshot: GraphSnapshot,
1173
+ options: (GmlExportOptions & CommonExportOptions) | undefined,
1174
+ ): WriteContext {
1175
+ const plan = planOf(snapshot, options);
1176
+ const ids = sanitizeIds(snapshot, "integer", plan.common.sanitizeIds);
1177
+ const mixed = countMixedEdges(snapshot);
1178
+ if (mixed > 0 && plan.common.onMixedDirection === "error") {
1179
+ throw new GraphFormatError(
1180
+ "E_DIRECTED",
1181
+ `${mixed} undirected edge(s) in a directed graph; GML has no mixed direction (onMixedDirection: "error")`,
1182
+ {
1183
+ reason: "mixed direction",
1184
+ count: mixed,
1185
+ },
1186
+ );
1187
+ }
1188
+ // design 8.5: exporters fold expanded pairs back; under "directed" the folded edge is written
1189
+ // once, as one directed edge, under "undirected" the whole graph is written undirected
1190
+ const foldPairs = mixed > 0;
1191
+ const writeDirected = snapshot.directed && plan.common.onMixedDirection !== "undirected";
1192
+ const originalIds = snapshot.nodes.byRole("originalId");
1193
+ const nodeReserved = new Set(NODE_RESERVED);
1194
+ if (ids.changed > 0 || originalIds !== null) {
1195
+ nodeReserved.add(ORIGINAL_ID_KEY);
1196
+ }
1197
+ const edgeIds = snapshot.edges.byRole("id");
1198
+ const edgeReserved = edgeReservedKeys(snapshot, plan, edgeIds);
1199
+ const topReserved = topReservedKeys(snapshot);
1200
+ return {
1201
+ snapshot,
1202
+ plan,
1203
+ ids,
1204
+ writeDirected,
1205
+ foldPairs,
1206
+ nodeColumns: selectColumns(
1207
+ snapshot.nodes,
1208
+ nodeReserved,
1209
+ "node",
1210
+ plan,
1211
+ null,
1212
+ (c) => !(c.meta.name === "graphics" && c.dtype === "json"),
1213
+ ),
1214
+ edgeColumns: selectColumns(snapshot.edges, edgeReserved, "edge", plan, null, (c) => c !== edgeIds),
1215
+ graphColumns: selectColumns(snapshot.graph, GRAPH_RESERVED, "graph", plan, null, (c) => !isTopLevel(c)),
1216
+ topColumns: selectColumns(snapshot.graph, topReserved, "top-level", plan, null, isTopLevel),
1217
+ position: positionColumn(snapshot),
1218
+ graphics: graphicsColumn(snapshot),
1219
+ originalIds,
1220
+ edgeIds,
1221
+ weights: explicitWeights(snapshot),
1222
+ folding: pairFolding(snapshot),
1223
+ };
1224
+ }
1225
+
1226
+ /**
1227
+ * The GML text of an id written into `graphty_originalId`: quoted for a string, a number text
1228
+ * otherwise (an integer as int, else real, so the importer restores the same typed value).
1229
+ * @param id - the original id
1230
+ * @returns the text
1231
+ */
1232
+ function originalIdText(id: NodeId): string {
1233
+ if (typeof id === "string") {
1234
+ return quoteGmlString(id);
1235
+ }
1236
+ return Number.isSafeInteger(id) ? formatInteger(id) : gmlRealText(id, "f64");
1237
+ }
1238
+
1239
+ /**
1240
+ * The parts of the document.
1241
+ * @param context - the resolved context
1242
+ * @yields one string per header line, node or edge
1243
+ * @returns nothing
1244
+ */
1245
+ function* gmlParts(context: WriteContext): Generator<string, void, undefined> {
1246
+ const { snapshot, plan, ids } = context;
1247
+ const w = new GmlWriter(plan.mangle);
1248
+ const { meta } = snapshot;
1249
+ if (meta.creator !== null) {
1250
+ w.line("", "Creator", quoteGmlString(meta.creator));
1251
+ }
1252
+ if (meta.sourceFormat === "gml" && meta.sourceVersion !== null) {
1253
+ w.line("", "Version", versionText(meta.sourceVersion));
1254
+ }
1255
+ for (const { column, key } of context.topColumns) {
1256
+ if (column.isSet(0)) {
1257
+ w.cell("", key, column, 0);
1258
+ }
1259
+ }
1260
+ w.raw("graph [");
1261
+ w.line(" ", "directed", context.writeDirected ? "1" : "0");
1262
+ const multigraph = meta.declaredMultigraph ?? (snapshot.flags.multigraph ? true : null);
1263
+ if (multigraph !== null) {
1264
+ w.line(" ", "multigraph", multigraph ? "1" : "0");
1265
+ }
1266
+ for (const { column, key } of context.graphColumns) {
1267
+ if (column.isSet(0)) {
1268
+ w.cell(" ", key, column, 0);
1269
+ }
1270
+ }
1271
+ yield w.take();
1272
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1273
+ w.raw(" node [");
1274
+ w.line(" ", "id", formatInteger(ids.idAt(i) as number));
1275
+ if (ids.isChanged(i)) {
1276
+ w.line(" ", ORIGINAL_ID_KEY, originalIdText(ids.originalAt(i)));
1277
+ } else if (context.originalIds !== null && context.originalIds.isSet(i)) {
1278
+ const original = context.originalIds.value(i);
1279
+ if (typeof original === "string" || typeof original === "number") {
1280
+ w.line(" ", ORIGINAL_ID_KEY, originalIdText(original));
1281
+ }
1282
+ }
1283
+ for (const { column, key } of context.nodeColumns) {
1284
+ if (column.isSet(i)) {
1285
+ w.cell(" ", key, column, i);
1286
+ }
1287
+ }
1288
+ writeGraphics(w, context, i);
1289
+ w.raw(" ]");
1290
+ yield w.take();
1291
+ }
1292
+ const el = snapshot.edgeList();
1293
+ const origin = meta.weightOrigin;
1294
+ const integerWeights = origin !== null && origin.type === "int";
1295
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1296
+ if (context.foldPairs && context.folding.folded(e)) {
1297
+ continue;
1298
+ }
1299
+ w.raw(" edge [");
1300
+ w.line(" ", "source", formatInteger(ids.idAt(el.src[e]) as number));
1301
+ w.line(" ", "target", formatInteger(ids.idAt(el.dst[e]) as number));
1302
+ if (context.edgeIds !== null && context.edgeIds.isSet(e)) {
1303
+ w.cell(" ", "id", context.edgeIds, e);
1304
+ }
1305
+ if (context.weights.isExplicit(e)) {
1306
+ const weight = context.weights.value(e);
1307
+ const text =
1308
+ integerWeights && Number.isInteger(weight)
1309
+ ? formatInteger(weight)
1310
+ : gmlRealText(weight, context.weights.dtype);
1311
+ w.line(" ", plan.weightKey, text);
1312
+ }
1313
+ for (const { column, key } of context.edgeColumns) {
1314
+ if (column.isSet(e)) {
1315
+ w.cell(" ", key, column, e);
1316
+ }
1317
+ }
1318
+ w.raw(" ]");
1319
+ yield w.take();
1320
+ }
1321
+ w.raw("]");
1322
+ yield w.take();
1323
+ }
1324
+
1325
+ /**
1326
+ * Write a node's `graphics [ ... ]` record from the position column and the graphics json column.
1327
+ * @param w - the writer
1328
+ * @param context - the context
1329
+ * @param i - the node
1330
+ */
1331
+ function writeGraphics(w: GmlWriter, context: WriteContext, i: number): void {
1332
+ const { position, graphics } = context;
1333
+ const hasPosition = position !== null && position.isSet(i);
1334
+ const graphicsValue = graphics !== null && graphics.isSet(i) ? graphics.values[i] : undefined;
1335
+ if (!hasPosition) {
1336
+ if (graphics !== null && graphicsValue !== undefined) {
1337
+ w.cell(" ", "graphics", graphics, i);
1338
+ }
1339
+ return;
1340
+ }
1341
+ const numeric = position as Column & { readonly data: ArrayLike<number> };
1342
+ const { components } = numeric.meta;
1343
+ const lanes = Math.min(components, 3);
1344
+ const extra: [string, string][] = [];
1345
+ const names = ["x", "y", "z"];
1346
+ for (let k = 0; k < lanes; k++) {
1347
+ extra.push([names[k], numberText(numeric.data[i * components + k], numeric.meta)]);
1348
+ }
1349
+ let record: Record<string, unknown> = {};
1350
+ if (graphicsValue !== undefined) {
1351
+ if (typeof graphicsValue !== "object" || graphicsValue === null || Array.isArray(graphicsValue)) {
1352
+ throw new GraphFormatError(
1353
+ "E_UNSUPPORTED",
1354
+ `node ${i}: the graphics value is not a record and cannot hold the position`,
1355
+ {
1356
+ reason: "graphics conflict",
1357
+ node: i,
1358
+ },
1359
+ );
1360
+ }
1361
+ record = graphicsValue as Record<string, unknown>;
1362
+ }
1363
+ w.record(" ", "graphics", record, "graphics", extra);
1364
+ }
1365
+
1366
+ /** The GML exporter plugin. */
1367
+ export const gmlExporter: GraphExporter<GmlExportOptions> = Object.freeze({
1368
+ format: "gml",
1369
+ capabilities: GML_CAPABILITIES,
1370
+ check,
1371
+ /**
1372
+ * Write the snapshot as UTF-8 chunks; the context is resolved on the first pull, so the
1373
+ * errors check() announced surface from the iteration.
1374
+ * @param snapshot - the snapshot
1375
+ * @param options - the options
1376
+ * @returns the chunks
1377
+ */
1378
+ export(snapshot: GraphSnapshot, options?: GmlExportOptions & CommonExportOptions): AsyncIterable<Uint8Array> {
1379
+ return encodeChunks(lazyParts(snapshot, options));
1380
+ },
1381
+ /**
1382
+ * Write the snapshot as one string.
1383
+ * @param snapshot - the snapshot
1384
+ * @param options - the options
1385
+ * @returns the document
1386
+ */
1387
+ exportToString(snapshot: GraphSnapshot, options?: GmlExportOptions & CommonExportOptions): Promise<string> {
1388
+ return joinText(lazyParts(snapshot, options));
1389
+ },
1390
+ });
1391
+
1392
+ /**
1393
+ * The document parts, with the context resolved when iteration starts.
1394
+ * @param snapshot - the snapshot
1395
+ * @param options - the options
1396
+ * @yields the parts
1397
+ * @returns nothing
1398
+ */
1399
+ function* lazyParts(
1400
+ snapshot: GraphSnapshot,
1401
+ options: (GmlExportOptions & CommonExportOptions) | undefined,
1402
+ ): Generator<string, void, undefined> {
1403
+ yield* gmlParts(contextOf(snapshot, options));
1404
+ }