@graphty/graph-io 0.0.0 → 0.2.0

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