@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,1358 @@
1
+ /**
2
+ * The JSON exporter (design section 8.5): one GraphExporter with a `dialect` option writing
3
+ * NetworkX node-link (the default), d3 (`links`, optional index endpoints), JSON Graph Format v2,
4
+ * Cytoscape.js elements, graphology serialisation or vis.js. The dialect defaults to the one the
5
+ * importer recorded under `meta.extra.json` so a JSON file re-exported as JSON keeps its shape,
6
+ * and to node-link for a snapshot from any other source.
7
+ *
8
+ * check() runs the generic capability pre-flight against the dialect's table and appends the
9
+ * dialect-specific notes (non-finite numbers written as null, JGF id stringification / collisions
10
+ * / node order, direction dropped by Cytoscape and vis, mutual pairs expanded, reserved key
11
+ * collisions, positions written as plain attributes, a Cytoscape `parents` column) before anything
12
+ * is written; the same column plan drives write(), so the notes and the output agree.
13
+ *
14
+ * Every column without a structural slot in the dialect is written as a plain attribute under its
15
+ * column name; an f32 value is written as the shortest decimal that round-trips through
16
+ * Math.fround (design section 3.7); a multi-component value is an array; a list is an array; a json
17
+ * value is its JSON text; an unset row writes no key (design section 5.3). Non-finite numbers,
18
+ * which JSON cannot carry, become null and are reported.
19
+ */
20
+ import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
21
+ import { pairFolding } from "../../common/direction.js";
22
+ import { checkCapabilities, countMixedEdges, LOSS } from "../../common/export.js";
23
+ import { formatF32 } from "../../common/format.js";
24
+ import { resolveExportOptions } from "../../common/options.js";
25
+ import { explicitWeights } from "../../common/weights.js";
26
+ import { encodeChunks, joinText } from "../../common/writer.js";
27
+ import { CYTOSCAPE_ELEMENT_KEYS, DIALECT_DEFAULT_DIRECTED, dialectCapabilities, isJsonDialect, JSON_DIALECTS, shapeMetaOf, SUFFIX, } from "./dialect.js";
28
+ /**
29
+ * The LossNote codes of the JSON exporter's check(): the dialect-specific ones and, aliased, the
30
+ * shared ones it records (`LOSS` holds the rest of the generic pre-flight's codes). A key is the
31
+ * code without its severity prefix.
32
+ */
33
+ export const JSON_LOSS = Object.freeze({
34
+ /** Non-finite numbers (columns, weights) are written as null. */
35
+ NONFINITE_AS_NULL: "W_NONFINITE_AS_NULL",
36
+ /** Cytoscape, vis and d3 carry no direction; the file re-imports with the dialect's default direction. */
37
+ DIRECTION_DROPPED: "W_DIRECTION_DROPPED",
38
+ /** GEXF mutual pairs are written as two directed edges. */
39
+ MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
40
+ /** JGF keys its nodes by string; numeric ids re-import as text unless ids: "canonical". */
41
+ NUMERIC_IDS_STRINGIFIED: "W_NUMERIC_IDS_STRINGIFIED",
42
+ /** JGF: two ids have the same text; export() throws E_INVALID_ID. */
43
+ ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
44
+ /** JGF: integer-like id keys are enumerated first and ascending by JSON parsers; node order changes on re-import. */
45
+ NODE_ORDER: "W_NODE_ORDER",
46
+ /** A column named like a reserved key of the dialect (id, source, target, ...) is skipped. */
47
+ RESERVED_KEY: "W_RESERVED_KEY",
48
+ /** A plain edge column named like the weight key reads back as THE weight (or is skipped when weights are written). */
49
+ WEIGHT_KEY_CLASH: LOSS.WEIGHT_KEY_CLASH,
50
+ /** A position column without a slot (every dialect but Cytoscape) is a plain array attribute; the role is lost. */
51
+ POSITIONS_DROPPED: LOSS.POSITIONS,
52
+ /** Cytoscape positions are 2D; non-zero z values are dropped. */
53
+ POSITION_Z_DROPPED: "W_POSITION_Z_DROPPED",
54
+ /** Cytoscape has a single parent; a `parents` list column cannot be written. */
55
+ PARENTS_DROPPED: LOSS.PARENTS,
56
+ /** node-link / d3 have no edge id slot; the id column is written as a plain attribute. */
57
+ EDGE_IDS_DROPPED: LOSS.EDGE_IDS_DROPPED,
58
+ /** An f64 column of integral values reads back as i32 (JSON declares no types). */
59
+ INTEGRAL_F64_AS_I32: LOSS.INTEGRAL_F64,
60
+ /** A role column the dialect has no slot for is a plain attribute; the role is lost. */
61
+ ROLE_DROPPED: LOSS.ROLE,
62
+ /** A column without a set cell is not written (JSON declares no columns). */
63
+ EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
64
+ });
65
+ /** The roles each dialect has a slot for (every other role column is a plain attribute, reported by checkCapabilities()). */
66
+ const SLOT_ROLES = Object.freeze({
67
+ "node-link": new Set(),
68
+ d3: new Set(),
69
+ jgf: new Set(["label", "kind"]),
70
+ cytoscape: new Set(["classes"]),
71
+ graphology: new Set(),
72
+ vis: new Set(),
73
+ });
74
+ /** The roles the pre-flight treats structurally; never written as attributes. */
75
+ const STRUCTURAL_ROLES = new Set([
76
+ "directed",
77
+ "pair",
78
+ "mutual",
79
+ "weight",
80
+ "timeText",
81
+ "originalId",
82
+ ]);
83
+ /** The roles no JSON dialect can carry; skipped (checkCapabilities reports them). */
84
+ const TEMPORAL_ROLES = new Set(["start", "end", "timestamp", "timestamps", "spells", "open"]);
85
+ /** The dialects whose attributes live in a nested dict (data / metadata / attributes). */
86
+ const NESTED_DIALECTS = new Set(["jgf", "cytoscape", "graphology"]);
87
+ /**
88
+ * Resolve the options against the snapshot's recorded shape.
89
+ * @param snapshot - the snapshot
90
+ * @param options - the caller's options
91
+ * @returns the resolved options; E_UNSUPPORTED for a bad value
92
+ */
93
+ function resolve(snapshot, options) {
94
+ const o = options ?? {};
95
+ const common = resolveExportOptions(options);
96
+ const shape = shapeMetaOf(snapshot.meta);
97
+ let dialect;
98
+ if (o.dialect === undefined) {
99
+ dialect = shape.dialect ?? "node-link";
100
+ }
101
+ else if (isJsonDialect(o.dialect)) {
102
+ ({ dialect } = o);
103
+ }
104
+ else {
105
+ throw unsupported("dialect", o.dialect, JSON_DIALECTS);
106
+ }
107
+ const indent = o.indent ?? 0;
108
+ if (!Number.isInteger(indent) || indent < 0 || indent > 16) {
109
+ throw unsupported("indent", indent, ["an integer 0..16"]);
110
+ }
111
+ const sameDialect = shape.dialect === dialect;
112
+ const recorded = (value, fallback) => sameDialect && value !== undefined ? value : fallback;
113
+ const edgesKey = keyOption("edgesKey", o.edgesKey) ?? recorded(shape.edgesKey, dialect === "d3" ? "links" : "edges");
114
+ let nodeIdKey;
115
+ if (o.nodeIdKey !== undefined) {
116
+ nodeIdKey = keyOption("nodeIdKey", o.nodeIdKey);
117
+ }
118
+ else if (sameDialect && shape.nodeIdKey !== undefined) {
119
+ ({ nodeIdKey } = shape);
120
+ }
121
+ else {
122
+ nodeIdKey = "id";
123
+ }
124
+ let indexLinks = o.indexLinks ?? recorded(shape.indexLinks, false);
125
+ if (typeof indexLinks !== "boolean") {
126
+ throw unsupported("indexLinks", indexLinks, ["true", "false"]);
127
+ }
128
+ if (nodeIdKey === null) {
129
+ // positional nodes have no id to write; endpoints must be positions
130
+ indexLinks = true;
131
+ }
132
+ const sourceKey = keyOption("sourceKey", o.sourceKey) ?? recorded(shape.sourceKey, dialect === "vis" ? "from" : "source");
133
+ const targetKey = keyOption("targetKey", o.targetKey) ?? recorded(shape.targetKey, dialect === "vis" ? "to" : "target");
134
+ const { weightOrigin } = snapshot.meta;
135
+ const weightKey = keyOption("weightKey", o.weightKey) ??
136
+ (weightOrigin !== null && weightOrigin.format === "json" && weightOrigin.id !== null
137
+ ? weightOrigin.id
138
+ : "weight");
139
+ return { common, dialect, indent, edgesKey, nodeIdKey, indexLinks, sourceKey, targetKey, weightKey, shape };
140
+ }
141
+ /**
142
+ * Check a key-valued option.
143
+ * @param name - the option name
144
+ * @param value - the caller's value
145
+ * @returns the key, or null when absent
146
+ */
147
+ function keyOption(name, value) {
148
+ if (value === undefined) {
149
+ return null;
150
+ }
151
+ if (typeof value !== "string" || value.length === 0) {
152
+ throw unsupported(name, value, ["a non-empty key"]);
153
+ }
154
+ return value;
155
+ }
156
+ /**
157
+ * The E_UNSUPPORTED error of a bad option.
158
+ * @param name - the option name
159
+ * @param found - the value
160
+ * @param supported - what is accepted
161
+ * @returns the error
162
+ */
163
+ function unsupported(name, found, supported) {
164
+ const shown = typeof found === "string" ? JSON.stringify(found) : String(found);
165
+ return new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${shown} is not one of ${supported.join(", ")}`, {
166
+ option: name,
167
+ found: typeof found === "string" || typeof found === "number" ? found : typeof found,
168
+ supported: [...supported],
169
+ });
170
+ }
171
+ // ============================================================ value formatting
172
+ /**
173
+ * The JSON text of a finite or non-finite number; non-finite values become null and are counted.
174
+ * @param value - the number
175
+ * @param f32 - whether the value came from an f32 store (shortest fround-round-trip text)
176
+ * @param nonfinite - the counter of null-ed values
177
+ * @returns the JSON text
178
+ */
179
+ function numberText(value, f32, nonfinite) {
180
+ if (!Number.isFinite(value)) {
181
+ nonfinite.count++;
182
+ return "null";
183
+ }
184
+ if (f32) {
185
+ return formatF32(value);
186
+ }
187
+ return Object.is(value, -0) ? "0" : String(value);
188
+ }
189
+ /**
190
+ * The JSON text of any cell value: a number per its dtype, a boolean, a string, an array (a
191
+ * multi-component subarray or a list row), or a json value.
192
+ * @param value - the value
193
+ * @param f32 - whether numbers come from an f32 store
194
+ * @param nonfinite - the counter of null-ed values
195
+ * @returns the JSON text
196
+ */
197
+ function valueText(value, f32, nonfinite) {
198
+ switch (typeof value) {
199
+ case "number":
200
+ return numberText(value, f32, nonfinite);
201
+ case "boolean":
202
+ return value ? "true" : "false";
203
+ case "string":
204
+ return JSON.stringify(value);
205
+ case "undefined":
206
+ return "null";
207
+ default:
208
+ break;
209
+ }
210
+ if (value === null) {
211
+ return "null";
212
+ }
213
+ if (Array.isArray(value) || ArrayBuffer.isView(value)) {
214
+ const items = Array.from(value, (item) => valueText(item, f32, nonfinite));
215
+ return `[${items.join(",")}]`;
216
+ }
217
+ const text = JSON.stringify(value);
218
+ return text === undefined ? "null" : text;
219
+ }
220
+ /**
221
+ * The JSON text of one set cell.
222
+ * @param column - the column
223
+ * @param row - the row
224
+ * @param ids - the snapshot's id map (for refersTo node columns)
225
+ * @param nonfinite - the counter of null-ed values
226
+ * @returns the JSON text
227
+ */
228
+ function cellText(column, row, ids, nonfinite) {
229
+ switch (column.dtype) {
230
+ case "list":
231
+ return valueText(column.sliceOf(row), column.child.dtype === "f32", nonfinite);
232
+ case "json":
233
+ return valueText(column.values[row], false, nonfinite);
234
+ case "u32":
235
+ if (column.meta.refersTo === "node" && column.meta.components === 1) {
236
+ const index = column.data[row];
237
+ return index === INVALID_INDEX ? "null" : JSON.stringify(ids.idOf(index));
238
+ }
239
+ return valueText(column.value(row), false, nonfinite);
240
+ case "f32":
241
+ return valueText(column.value(row), true, nonfinite);
242
+ default:
243
+ return valueText(column.value(row), false, nonfinite);
244
+ }
245
+ }
246
+ /**
247
+ * How many cells of a column hold a non-finite number (f32 / f64 columns, their components and
248
+ * lists of them).
249
+ * @param column - the column
250
+ * @returns the count
251
+ */
252
+ function countNonFinite(column) {
253
+ let data;
254
+ switch (column.dtype) {
255
+ case "f32":
256
+ case "f64":
257
+ ({ data } = column);
258
+ break;
259
+ case "list":
260
+ if (column.child.dtype !== "f32" && column.child.dtype !== "f64") {
261
+ return 0;
262
+ }
263
+ ({ data } = column.child);
264
+ break;
265
+ default:
266
+ return 0;
267
+ }
268
+ let count = 0;
269
+ for (let i = 0; i < data.length; i++) {
270
+ if (!Number.isFinite(data[i])) {
271
+ count++;
272
+ }
273
+ }
274
+ return count;
275
+ }
276
+ // ============================================================ the writer
277
+ /**
278
+ * Emits JSON tokens with optional indentation, accumulating text parts the generator yields one
279
+ * element at a time.
280
+ */
281
+ class JsonWriter {
282
+ /**
283
+ * Create a writer.
284
+ * @param indent - spaces per level; 0 for compact output
285
+ */
286
+ constructor(indent) {
287
+ this.parts = [];
288
+ this.depth = 0;
289
+ this.firstAtDepth = [];
290
+ this.indent = indent;
291
+ }
292
+ /**
293
+ * Open an object or an array.
294
+ * @param bracket - "{" or "["
295
+ */
296
+ open(bracket) {
297
+ this.parts.push(bracket);
298
+ this.depth++;
299
+ this.firstAtDepth.push(true);
300
+ }
301
+ /**
302
+ * Close the innermost object or array.
303
+ * @param bracket - "}" or "]"
304
+ */
305
+ close(bracket) {
306
+ const wasEmpty = this.firstAtDepth.pop() ?? true;
307
+ this.depth--;
308
+ if (!wasEmpty) {
309
+ this.parts.push(this.newline());
310
+ }
311
+ this.parts.push(bracket);
312
+ }
313
+ /**
314
+ * Start an object member: the separator and the quoted key.
315
+ * @param name - the key
316
+ */
317
+ key(name) {
318
+ this.separator();
319
+ this.parts.push(JSON.stringify(name), this.indent > 0 ? ": " : ":");
320
+ }
321
+ /** Start an array element: the separator only. */
322
+ item() {
323
+ this.separator();
324
+ }
325
+ /**
326
+ * Append raw JSON text (a value).
327
+ * @param text - the text
328
+ */
329
+ raw(text) {
330
+ this.parts.push(text);
331
+ }
332
+ /**
333
+ * Write a member with a value in one call.
334
+ * @param name - the key
335
+ * @param text - the value's JSON text
336
+ */
337
+ member(name, text) {
338
+ this.key(name);
339
+ this.parts.push(text);
340
+ }
341
+ /**
342
+ * Hand out the text accumulated since the last take.
343
+ * @returns the text
344
+ */
345
+ take() {
346
+ const text = this.parts.length === 1 ? this.parts[0] : this.parts.join("");
347
+ this.parts = [];
348
+ return text;
349
+ }
350
+ /** The comma and newline before a member or element. */
351
+ separator() {
352
+ const top = this.firstAtDepth.length - 1;
353
+ if (this.firstAtDepth[top]) {
354
+ this.firstAtDepth[top] = false;
355
+ }
356
+ else {
357
+ this.parts.push(",");
358
+ }
359
+ this.parts.push(this.newline());
360
+ }
361
+ /**
362
+ * A newline plus the current indentation, or nothing in compact mode.
363
+ * @returns the text
364
+ */
365
+ newline() {
366
+ return this.indent > 0 ? `\n${" ".repeat(this.depth * this.indent)}` : "";
367
+ }
368
+ }
369
+ /**
370
+ * Build the column plan and the notes for one dialect.
371
+ * @param snapshot - the snapshot
372
+ * @param resolved - the resolved options
373
+ * @returns the plan
374
+ */
375
+ function plan(snapshot, resolved) {
376
+ const { dialect } = resolved;
377
+ const caps = dialectCapabilities(dialect);
378
+ // the generic position and edge-id notes are replaced by planTable()'s (both are written as
379
+ // plain attributes and read back without their role)
380
+ const notes = checkCapabilities(snapshot, caps, resolved.common, {
381
+ roles: SLOT_ROLES[dialect],
382
+ positionDtype: "f32",
383
+ }).filter((n) => n.code !== LOSS.POSITIONS && n.code !== LOSS.EDGE_IDS_DROPPED);
384
+ const note = (code, message, column = null, count = null) => {
385
+ notes.push(Object.freeze({ code, message, column, count }));
386
+ };
387
+ const ctx = {
388
+ snapshot,
389
+ resolved,
390
+ dialect,
391
+ caps,
392
+ notes,
393
+ note,
394
+ reserved: reservedKeys(snapshot, resolved),
395
+ nonfinite: { count: 0 },
396
+ };
397
+ const nodes = planTable(ctx, snapshot.nodes, "node");
398
+ const edges = planTable(ctx, snapshot.edges, "edge");
399
+ const graph = caps.graphAttributes ? planTable(ctx, snapshot.graph, "graph") : [];
400
+ withdrawSlotNotes(notes, [...nodes, ...edges]);
401
+ const weights = planWeights(ctx, edges);
402
+ if (ctx.nonfinite.count > 0) {
403
+ note(JSON_LOSS.NONFINITE_AS_NULL, `${ctx.nonfinite.count} non-finite number(s) are written as null`, null, ctx.nonfinite.count);
404
+ }
405
+ const { folding, directed } = planDirection(ctx);
406
+ const multigraph = snapshot.flags.multigraph || snapshot.meta.declaredMultigraph === true;
407
+ if (dialect === "jgf") {
408
+ jgfIdNotes(snapshot, note);
409
+ }
410
+ const edgeIds = snapshot.edges.byRole("id");
411
+ if (dialect === "cytoscape" && edgeIds !== null && edgeIds.nullCount > 0) {
412
+ note(LOSS.EDGE_IDS_GENERATED, `${edgeIds.nullCount} edge(s) have no id; canonical e<index> ids are generated for them`, edgeIds.meta.name, edgeIds.nullCount);
413
+ }
414
+ return { resolved, notes, nodes, edges, graph, edgeIds, weights, folding, directed, multigraph };
415
+ }
416
+ /**
417
+ * Withdraw the generic list note of a Cytoscape classes column: the slot writes the list as the
418
+ * `classes` string and the importer reads it back as the list it was.
419
+ * @param notes - the notes so far
420
+ * @param plans - the node and edge plans
421
+ */
422
+ function withdrawSlotNotes(notes, plans) {
423
+ for (const plan of plans) {
424
+ if (plan.slot !== "classes") {
425
+ continue;
426
+ }
427
+ const at = notes.findIndex((n) => n.code === LOSS.LIST && n.column === plan.column.meta.name);
428
+ if (at >= 0) {
429
+ notes.splice(at, 1);
430
+ }
431
+ }
432
+ }
433
+ /**
434
+ * The output keys each dialect reserves for its structure (a column of that name cannot be
435
+ * written as an attribute): the id and endpoint keys, and the weight key of a weighted snapshot.
436
+ * @param snapshot - the snapshot
437
+ * @param resolved - the resolved options
438
+ * @returns the reserved keys per table
439
+ */
440
+ function reservedKeys(snapshot, resolved) {
441
+ const { dialect } = resolved;
442
+ const node = new Set();
443
+ const edge = new Set(snapshot.flags.weighted ? [resolved.weightKey] : []);
444
+ const graph = new Set();
445
+ switch (dialect) {
446
+ case "node-link":
447
+ case "d3":
448
+ if (resolved.nodeIdKey !== null) {
449
+ node.add(resolved.nodeIdKey);
450
+ }
451
+ edge.add(resolved.sourceKey).add(resolved.targetKey);
452
+ break;
453
+ case "vis":
454
+ node.add(resolved.nodeIdKey ?? "id");
455
+ edge.add(resolved.sourceKey).add(resolved.targetKey);
456
+ break;
457
+ case "cytoscape":
458
+ node.add("id").add("parent");
459
+ edge.add("id").add("source").add("target");
460
+ break;
461
+ case "jgf":
462
+ case "graphology":
463
+ break;
464
+ default: {
465
+ const name = dialect;
466
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, { option: "dialect", found: name });
467
+ }
468
+ }
469
+ return { node, edge, graph };
470
+ }
471
+ /**
472
+ * Classify the columns of one table: the slot each is written into and what it loses on the way
473
+ * (JSON declares no types, so the notes name what the importer's inference reads back).
474
+ * @param ctx - the plan context
475
+ * @param table - the table
476
+ * @param domain - its domain
477
+ * @returns the plans of the written columns, in declaration order
478
+ */
479
+ function planTable(ctx, table, domain) {
480
+ const { dialect, note } = ctx;
481
+ const plans = [];
482
+ const reserved = ctx.reserved[domain];
483
+ const used = new Set();
484
+ for (const column of table) {
485
+ const { meta } = column;
486
+ const { role } = meta;
487
+ const setRows = column.length - column.nullCount;
488
+ if (role !== null && (STRUCTURAL_ROLES.has(role) || TEMPORAL_ROLES.has(role))) {
489
+ continue;
490
+ }
491
+ if (domain !== "graph") {
492
+ ctx.nonfinite.count += countNonFinite(column);
493
+ }
494
+ const slotted = planSlot(ctx, column, domain);
495
+ if (slotted !== null) {
496
+ if (slotted !== SKIPPED) {
497
+ plans.push(slotted);
498
+ }
499
+ continue;
500
+ }
501
+ let key = meta.name;
502
+ let slot = "attribute";
503
+ if (NESTED_DIALECTS.has(dialect) && domain !== "graph") {
504
+ if (key.endsWith(SUFFIX.element)) {
505
+ key = key.slice(0, -SUFFIX.element.length);
506
+ slot = "element";
507
+ }
508
+ else if (dialect === "cytoscape" && CYTOSCAPE_ELEMENT_KEYS.has(key)) {
509
+ slot = "element";
510
+ }
511
+ else if (key.endsWith(SUFFIX.data)) {
512
+ key = key.slice(0, -SUFFIX.data.length);
513
+ }
514
+ }
515
+ if (slot === "attribute" && reserved.has(key)) {
516
+ note(JSON_LOSS.RESERVED_KEY, `${domain} column "${meta.name}" cannot be written: "${key}" is a reserved key of ${dialect}`, meta.name, setRows);
517
+ continue;
518
+ }
519
+ const slotKey = `${slot}:${key}`;
520
+ if (used.has(slotKey)) {
521
+ note(JSON_LOSS.RESERVED_KEY, `${domain} column "${meta.name}" cannot be written: key "${key}" is already used by another column`, meta.name, setRows);
522
+ continue;
523
+ }
524
+ used.add(slotKey);
525
+ if (setRows === 0 && domain !== "graph") {
526
+ note(LOSS.EMPTY_COLUMN, `${domain} column "${meta.name}" has no set cell and is not written: JSON writes values, never declarations`, meta.name, 0);
527
+ }
528
+ inferenceNotes(ctx, column, domain, setRows);
529
+ plans.push({ column, key, slot });
530
+ }
531
+ return plans;
532
+ }
533
+ /** planSlot()'s answer for a role column handled (or dropped) without a plan of its own. */
534
+ const SKIPPED = Symbol("skipped");
535
+ /**
536
+ * The structural slot of a role column in the dialect, when it has one: Cytoscape's parent,
537
+ * position and classes, JGF's label and relation; the id-role edge column is written
538
+ * structurally (data.id / key / id) where the dialect has an edge id and as a plain attribute
539
+ * elsewhere; a position column without a slot is a plain array attribute.
540
+ * @param ctx - the plan context
541
+ * @param column - the column
542
+ * @param domain - its domain
543
+ * @returns the plan, SKIPPED for a column written structurally or dropped, null for a plain attribute
544
+ */
545
+ function planSlot(ctx, column, domain) {
546
+ const { dialect, note } = ctx;
547
+ const { meta } = column;
548
+ const { role } = meta;
549
+ const setRows = column.length - column.nullCount;
550
+ if (role === "parent" || role === "parents") {
551
+ if (dialect === "cytoscape" && domain === "node") {
552
+ if (role === "parent" && column.dtype === "u32" && meta.refersTo === "node") {
553
+ return { column, key: "parent", slot: "parent" };
554
+ }
555
+ note(LOSS.PARENTS, `node column "${meta.name}" (${role}) cannot be written: Cytoscape has a single parent`, meta.name, setRows);
556
+ }
557
+ return SKIPPED;
558
+ }
559
+ if (role === "position" && domain === "node") {
560
+ if (dialect === "cytoscape" && (column.dtype === "f32" || column.dtype === "f64") && meta.components >= 2) {
561
+ const z = countNonZeroZ(column);
562
+ if (z > 0) {
563
+ note(JSON_LOSS.POSITION_Z_DROPPED, `${z} position(s) have a non-zero z; Cytoscape positions are 2D`, meta.name, z);
564
+ }
565
+ return { column, key: "position", slot: "cytoscapePosition" };
566
+ }
567
+ note(LOSS.POSITIONS, `node column "${meta.name}" (position) is written as a plain array attribute; its role is lost on re-import`, meta.name, setRows);
568
+ return { column, key: meta.name, slot: "position" };
569
+ }
570
+ if (role === "id" && domain === "edge") {
571
+ if (dialect === "node-link" || dialect === "d3") {
572
+ note(LOSS.EDGE_IDS_DROPPED, `edge column "${meta.name}" (id) is written as a plain attribute; ${dialect} has no edge id`, meta.name, setRows);
573
+ return null;
574
+ }
575
+ return SKIPPED;
576
+ }
577
+ if (dialect === "cytoscape" && role === "classes" && column.dtype === "list" && column.child.dtype === "string") {
578
+ return { column, key: "classes", slot: "classes" };
579
+ }
580
+ if (dialect === "jgf" && role === "label" && (column.dtype === "string" || column.dtype === "dict")) {
581
+ return { column, key: "label", slot: "label" };
582
+ }
583
+ if (dialect === "jgf" &&
584
+ role === "kind" &&
585
+ domain === "edge" &&
586
+ (column.dtype === "string" || column.dtype === "dict")) {
587
+ return { column, key: "relation", slot: "relation" };
588
+ }
589
+ return null;
590
+ }
591
+ /**
592
+ * What the importer's inference (design section 5.1: JSON numbers are i32 when integral, else
593
+ * f64; strings are strings; arrays and objects are json) changes about a written attribute
594
+ * column beyond what checkCapabilities() already reported for its dtype: an f64 column whose set
595
+ * values are all integers reads back as i32.
596
+ * @param ctx - the plan context
597
+ * @param column - the column
598
+ * @param domain - its domain
599
+ * @param setRows - the set rows
600
+ */
601
+ function inferenceNotes(ctx, column, domain, setRows) {
602
+ if (column.dtype !== "f64" || column.meta.components > 1 || setRows === 0) {
603
+ return;
604
+ }
605
+ const { data } = column;
606
+ for (let r = 0; r < column.length; r++) {
607
+ if (column.isSet(r) && !Number.isInteger(data[r])) {
608
+ return;
609
+ }
610
+ }
611
+ ctx.note(LOSS.INTEGRAL_F64, `${domain} column "${column.meta.name}" is f64 with integral values only; JSON declares no types and it reads back as i32`, column.meta.name, setRows);
612
+ }
613
+ /**
614
+ * The explicit weights and their JSON text (non-finite values become null and are counted).
615
+ * @param ctx - the plan context
616
+ * @param edges - the edge column plans (for the weight-key clash note)
617
+ * @returns the weight text function
618
+ */
619
+ function planWeights(ctx, edges) {
620
+ const { snapshot, nonfinite } = ctx;
621
+ const weights = explicitWeights(snapshot);
622
+ if (!weights.weighted) {
623
+ const clash = edges.find((p) => p.slot === "attribute" && p.key === ctx.resolved.weightKey);
624
+ if (clash !== undefined) {
625
+ ctx.note(LOSS.WEIGHT_KEY_CLASH, `edge column "${clash.column.meta.name}" is written under "${clash.key}", 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);
626
+ }
627
+ return () => null;
628
+ }
629
+ for (let e = 0; e < snapshot.edgeCount; e++) {
630
+ if (weights.isExplicit(e) && !Number.isFinite(weights.value(e))) {
631
+ nonfinite.count++;
632
+ }
633
+ }
634
+ return (e) => {
635
+ if (!weights.isExplicit(e)) {
636
+ return null;
637
+ }
638
+ return Number.isFinite(weights.value(e)) ? weights.text(e) : "null";
639
+ };
640
+ }
641
+ /**
642
+ * The direction decisions: the pair folding (mutual pairs are written as two directed edges and
643
+ * reported), the file-level direction under the onMixedDirection policy of a dialect without a
644
+ * per-edge flag, and the note of the dialects that carry no direction at all.
645
+ * @param ctx - the plan context
646
+ * @returns the folding and the file-level direction
647
+ */
648
+ function planDirection(ctx) {
649
+ const { snapshot, dialect, caps, note } = ctx;
650
+ const folding = pairFolding(snapshot);
651
+ if (folding.mutualCount > 0) {
652
+ note(LOSS.MUTUAL_EXPANDED, `${folding.mutualCount} mutual pair(s) are written as two directed edges; the mutual mark is lost`, null, folding.mutualCount);
653
+ }
654
+ let { directed } = snapshot;
655
+ if (!caps.mixedDirection &&
656
+ countMixedEdges(snapshot) > 0 &&
657
+ ctx.resolved.common.onMixedDirection === "undirected") {
658
+ directed = false;
659
+ }
660
+ if (dialect === "cytoscape" || dialect === "vis" || dialect === "d3") {
661
+ const assumed = DIALECT_DEFAULT_DIRECTED[dialect];
662
+ if (directed !== assumed) {
663
+ note(JSON_LOSS.DIRECTION_DROPPED, `${dialect} has no direction flag; the ${directed ? "directed" : "undirected"} graph re-imports as ${assumed ? "directed" : "undirected"} unless defaultDirected is passed`);
664
+ }
665
+ }
666
+ return { folding, directed };
667
+ }
668
+ /**
669
+ * How many rows of a position column have a non-zero third component.
670
+ * @param column - an f32 / f64 column with at least two components
671
+ * @returns the count
672
+ */
673
+ function countNonZeroZ(column) {
674
+ if ((column.dtype !== "f32" && column.dtype !== "f64") || column.meta.components < 3) {
675
+ return 0;
676
+ }
677
+ const { components } = column.meta;
678
+ let count = 0;
679
+ for (let r = 0; r < column.length; r++) {
680
+ if (column.isSet(r) && column.data[r * components + 2] !== 0) {
681
+ count++;
682
+ }
683
+ }
684
+ return count;
685
+ }
686
+ /**
687
+ * The JGF id notes: numeric ids become string keys, colliding texts are refused, and integer-like
688
+ * keys are enumerated first and ascending by every JSON parser so the node order may change.
689
+ * @param snapshot - the snapshot
690
+ * @param note - the recorder
691
+ */
692
+ function jgfIdNotes(snapshot, note) {
693
+ const { ids } = snapshot;
694
+ const seen = new Set();
695
+ let numeric = 0;
696
+ let collisions = 0;
697
+ let indexLike = 0;
698
+ let lastIndex = -1;
699
+ let ordered = true;
700
+ let sawOther = false;
701
+ for (let i = 0; i < ids.size; i++) {
702
+ const id = ids.idOf(i);
703
+ const text = String(id);
704
+ if (typeof id === "number") {
705
+ numeric++;
706
+ }
707
+ if (seen.has(text)) {
708
+ collisions++;
709
+ }
710
+ seen.add(text);
711
+ if (isArrayIndexKey(text)) {
712
+ indexLike++;
713
+ const n = Number(text);
714
+ if (sawOther || n <= lastIndex) {
715
+ ordered = false;
716
+ }
717
+ lastIndex = n;
718
+ }
719
+ else {
720
+ sawOther = true;
721
+ }
722
+ }
723
+ if (numeric > 0) {
724
+ note(JSON_LOSS.NUMERIC_IDS_STRINGIFIED, `${numeric} numeric node id(s) become JGF object keys (strings); pass ids: "canonical" on re-import`, null, numeric);
725
+ }
726
+ if (collisions > 0) {
727
+ note(JSON_LOSS.ID_TEXT_COLLISION, `${collisions} node id(s) share their text with another id; export() will throw`, null, collisions);
728
+ }
729
+ if (!ordered) {
730
+ note(JSON_LOSS.NODE_ORDER, `${indexLike} integer-like node id(s) are enumerated first and ascending by JSON parsers; node order changes on re-import`, null, indexLike);
731
+ }
732
+ }
733
+ /**
734
+ * Whether a key is an array index in the JS sense (enumerated before other keys, ascending).
735
+ * @param text - the key
736
+ * @returns true for canonical integer text below 2^32 - 1
737
+ */
738
+ function isArrayIndexKey(text) {
739
+ return /^(0|[1-9][0-9]*)$/.test(text) && Number(text) < 4294967295;
740
+ }
741
+ // ============================================================ writing
742
+ /**
743
+ * The text parts of the document.
744
+ * @param snapshot - the snapshot
745
+ * @param p - the plan
746
+ * @yields one part per header, node, edge and footer
747
+ * @returns nothing
748
+ */
749
+ function* write(snapshot, p) {
750
+ const { dialect } = p.resolved;
751
+ if (p.notes.some((n) => n.code === LOSS.MIXED_DIRECTION_ERROR)) {
752
+ throw new GraphFormatError("E_DIRECTED", `${dialect} has no mixed direction; pass onMixedDirection "directed" or "undirected"`, { reason: "mixed direction", dialect });
753
+ }
754
+ if (p.notes.some((n) => n.code === JSON_LOSS.ID_TEXT_COLLISION)) {
755
+ throw new GraphFormatError("E_INVALID_ID", "two node ids have the same text; JGF keys nodes by text", {
756
+ reason: "collision",
757
+ });
758
+ }
759
+ switch (dialect) {
760
+ case "node-link":
761
+ case "d3":
762
+ yield* writeNodeLink(snapshot, p);
763
+ return;
764
+ case "vis":
765
+ yield* writeVis(snapshot, p);
766
+ return;
767
+ case "graphology":
768
+ yield* writeGraphology(snapshot, p);
769
+ return;
770
+ case "jgf":
771
+ yield* writeJgf(snapshot, p);
772
+ return;
773
+ case "cytoscape":
774
+ yield* writeCytoscape(snapshot, p);
775
+ return;
776
+ default: {
777
+ const name = dialect;
778
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, { option: "dialect", found: name });
779
+ }
780
+ }
781
+ }
782
+ /**
783
+ * Write the members of one row's planned columns into the current object, attributes only.
784
+ * @param c - the cursor
785
+ * @param plans - the table plan
786
+ * @param row - the row
787
+ * @param slots - which slots to write
788
+ */
789
+ function writeSlots(c, plans, row, slots) {
790
+ for (const item of plans) {
791
+ if (!slots.has(item.slot) || !item.column.isSet(row)) {
792
+ continue;
793
+ }
794
+ let text;
795
+ switch (item.slot) {
796
+ case "position": {
797
+ const value = item.column.value(row);
798
+ const dims = dimsOf(item.column);
799
+ const values = Array.from(value).slice(0, dims);
800
+ text = valueText(values, item.column.dtype === "f32", c.nonfinite);
801
+ break;
802
+ }
803
+ case "classes":
804
+ text = JSON.stringify(item.column.sliceOf(row).join(" "));
805
+ break;
806
+ default:
807
+ text = cellText(item.column, row, c.ids, c.nonfinite);
808
+ break;
809
+ }
810
+ c.w.member(item.key, text);
811
+ }
812
+ }
813
+ const ATTRIBUTE_SLOTS = new Set(["attribute", "position"]);
814
+ const ELEMENT_SLOTS = new Set(["element", "classes"]);
815
+ /**
816
+ * The number of components a position column writes: `extra.sourceDims` when recorded, else all.
817
+ * @param column - the position column
818
+ * @returns 2 or 3 (or the component count)
819
+ */
820
+ function dimsOf(column) {
821
+ const dims = column.meta.extra.sourceDims;
822
+ if (typeof dims === "number" && dims >= 1 && dims <= column.meta.components) {
823
+ return dims;
824
+ }
825
+ return column.meta.components;
826
+ }
827
+ /**
828
+ * Whether any planned column of a slot set has a value in a row (to omit an empty nested dict).
829
+ * @param plans - the table plan
830
+ * @param row - the row
831
+ * @param slots - the slots
832
+ * @returns true when something would be written
833
+ */
834
+ function anySet(plans, row, slots) {
835
+ return plans.some((item) => slots.has(item.slot) && item.column.isSet(row));
836
+ }
837
+ /**
838
+ * The JSON text of a node id.
839
+ * @param id - the id
840
+ * @returns the text
841
+ */
842
+ function idText(id) {
843
+ return typeof id === "number" && Object.is(id, -0) ? "0" : JSON.stringify(id);
844
+ }
845
+ /**
846
+ * The edge id text of an edge, or null when none is set.
847
+ * @param p - the plan
848
+ * @param e - the edge
849
+ * @param c - the cursor
850
+ * @returns the text, or null
851
+ */
852
+ function edgeIdText(p, e, c) {
853
+ const column = p.edgeIds;
854
+ if (column === null || !column.isSet(e)) {
855
+ return null;
856
+ }
857
+ return cellText(column, e, c.ids, c.nonfinite);
858
+ }
859
+ /**
860
+ * Whether an edge is written, and as which kind, under the plan's direction policy.
861
+ * @param p - the plan
862
+ * @param e - the edge
863
+ * @returns "skip" for a folded mirror, "undirected" for a pair primary, "directed" otherwise
864
+ */
865
+ function edgeDisposition(p, e) {
866
+ const { folding } = p;
867
+ if (folding.folded(e)) {
868
+ return "skip";
869
+ }
870
+ if (!p.directed) {
871
+ return "undirected";
872
+ }
873
+ if (folding.sourceDirected(e)) {
874
+ return "directed";
875
+ }
876
+ // a source-undirected edge (pair primary or expanded self-loop): a dialect with a per-edge flag
877
+ // keeps it; one without writes it per the policy (the pair folds to one directed edge)
878
+ if (dialectCapabilities(p.resolved.dialect).mixedDirection) {
879
+ return "undirected";
880
+ }
881
+ return p.resolved.common.onMixedDirection === "directed" ? "directed" : "undirected";
882
+ }
883
+ /**
884
+ * The graph-level attribute object.
885
+ * @param c - the cursor
886
+ * @param p - the plan
887
+ */
888
+ function writeGraphObject(c, p) {
889
+ c.w.open("{");
890
+ for (const item of p.graph) {
891
+ if (item.column.isSet(0)) {
892
+ c.w.member(item.key, cellText(item.column, 0, c.ids, c.nonfinite));
893
+ }
894
+ }
895
+ c.w.close("}");
896
+ }
897
+ /**
898
+ * The endpoint texts of an edge: ids, or array positions under index links.
899
+ * @param snapshot - the snapshot
900
+ * @param p - the plan
901
+ * @param e - the edge
902
+ * @returns the source and target texts
903
+ */
904
+ function endpoints(snapshot, p, e) {
905
+ const list = snapshot.edgeList();
906
+ const u = list.src[e];
907
+ const v = list.dst[e];
908
+ if (p.resolved.indexLinks) {
909
+ return [String(u), String(v)];
910
+ }
911
+ return [idText(snapshot.ids.idOf(u)), idText(snapshot.ids.idOf(v))];
912
+ }
913
+ /**
914
+ * Write a node-link / d3 document.
915
+ * @param snapshot - the snapshot
916
+ * @param p - the plan
917
+ * @yields the parts
918
+ * @returns nothing
919
+ */
920
+ function* writeNodeLink(snapshot, p) {
921
+ const { resolved } = p;
922
+ const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
923
+ const { w } = c;
924
+ w.open("{");
925
+ if (resolved.dialect !== "d3") {
926
+ // d3 is the bare shape: no direction, multigraph or graph keys (the importer sniffs it by their absence)
927
+ w.member("directed", p.directed ? "true" : "false");
928
+ w.member("multigraph", p.multigraph ? "true" : "false");
929
+ w.key("graph");
930
+ writeGraphObject(c, p);
931
+ }
932
+ w.key("nodes");
933
+ w.open("[");
934
+ yield w.take();
935
+ for (let i = 0; i < snapshot.nodeCount; i++) {
936
+ w.item();
937
+ w.open("{");
938
+ if (resolved.nodeIdKey !== null) {
939
+ w.member(resolved.nodeIdKey, idText(snapshot.ids.idOf(i)));
940
+ }
941
+ writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
942
+ w.close("}");
943
+ yield w.take();
944
+ }
945
+ w.close("]");
946
+ w.key(resolved.edgesKey);
947
+ w.open("[");
948
+ for (let e = 0; e < snapshot.edgeCount; e++) {
949
+ const disposition = edgeDisposition(p, e);
950
+ if (disposition === "skip") {
951
+ continue;
952
+ }
953
+ const [s, t] = endpoints(snapshot, p, e);
954
+ w.item();
955
+ w.open("{");
956
+ w.member(resolved.sourceKey, s);
957
+ w.member(resolved.targetKey, t);
958
+ const weight = p.weights(e);
959
+ if (weight !== null) {
960
+ w.member(resolved.weightKey, weight);
961
+ }
962
+ writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
963
+ w.close("}");
964
+ yield w.take();
965
+ }
966
+ w.close("]");
967
+ w.close("}");
968
+ yield w.take();
969
+ }
970
+ /**
971
+ * Write a vis.js document.
972
+ * @param snapshot - the snapshot
973
+ * @param p - the plan
974
+ * @yields the parts
975
+ * @returns nothing
976
+ */
977
+ function* writeVis(snapshot, p) {
978
+ const { resolved } = p;
979
+ const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
980
+ const { w } = c;
981
+ const nodeIdKey = resolved.nodeIdKey ?? "id";
982
+ w.open("{");
983
+ w.key("nodes");
984
+ w.open("[");
985
+ yield w.take();
986
+ for (let i = 0; i < snapshot.nodeCount; i++) {
987
+ w.item();
988
+ w.open("{");
989
+ w.member(nodeIdKey, idText(snapshot.ids.idOf(i)));
990
+ writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
991
+ w.close("}");
992
+ yield w.take();
993
+ }
994
+ w.close("]");
995
+ w.key("edges");
996
+ w.open("[");
997
+ for (let e = 0; e < snapshot.edgeCount; e++) {
998
+ if (edgeDisposition(p, e) === "skip") {
999
+ continue;
1000
+ }
1001
+ const list = snapshot.edgeList();
1002
+ w.item();
1003
+ w.open("{");
1004
+ const id = edgeIdText(p, e, c);
1005
+ if (id !== null) {
1006
+ w.member("id", id);
1007
+ }
1008
+ w.member(resolved.sourceKey, idText(snapshot.ids.idOf(list.src[e])));
1009
+ w.member(resolved.targetKey, idText(snapshot.ids.idOf(list.dst[e])));
1010
+ const weight = p.weights(e);
1011
+ if (weight !== null) {
1012
+ w.member(resolved.weightKey, weight);
1013
+ }
1014
+ writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
1015
+ w.close("}");
1016
+ yield w.take();
1017
+ }
1018
+ w.close("]");
1019
+ w.close("}");
1020
+ yield w.take();
1021
+ }
1022
+ /**
1023
+ * Write a graphology serialisation.
1024
+ * @param snapshot - the snapshot
1025
+ * @param p - the plan
1026
+ * @yields the parts
1027
+ * @returns nothing
1028
+ */
1029
+ function* writeGraphology(snapshot, p) {
1030
+ const { resolved } = p;
1031
+ const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
1032
+ const { w } = c;
1033
+ let type;
1034
+ if (!p.directed) {
1035
+ type = "undirected";
1036
+ }
1037
+ else {
1038
+ type = countMixedEdges(snapshot) > 0 ? "mixed" : "directed";
1039
+ }
1040
+ w.open("{");
1041
+ w.key("attributes");
1042
+ writeGraphObject(c, p);
1043
+ w.key("options");
1044
+ w.open("{");
1045
+ w.member("type", JSON.stringify(type));
1046
+ // the shape the importer read is written back as read: `multi` and `allowSelfLoops` only when
1047
+ // the source declared them (or the graph needs them)
1048
+ if (snapshot.meta.declaredMultigraph !== null || p.multigraph) {
1049
+ w.member("multi", p.multigraph ? "true" : "false");
1050
+ }
1051
+ if (resolved.shape.allowSelfLoops !== undefined) {
1052
+ w.member("allowSelfLoops", !resolved.shape.allowSelfLoops && snapshot.selfLoopCount === 0 ? "false" : "true");
1053
+ }
1054
+ w.close("}");
1055
+ w.key("nodes");
1056
+ w.open("[");
1057
+ yield w.take();
1058
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1059
+ w.item();
1060
+ w.open("{");
1061
+ w.member("key", idText(snapshot.ids.idOf(i)));
1062
+ if (anySet(p.nodes, i, ATTRIBUTE_SLOTS)) {
1063
+ w.key("attributes");
1064
+ w.open("{");
1065
+ writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
1066
+ w.close("}");
1067
+ }
1068
+ writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
1069
+ w.close("}");
1070
+ yield w.take();
1071
+ }
1072
+ w.close("]");
1073
+ w.key("edges");
1074
+ w.open("[");
1075
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1076
+ const disposition = edgeDisposition(p, e);
1077
+ if (disposition === "skip") {
1078
+ continue;
1079
+ }
1080
+ const list = snapshot.edgeList();
1081
+ w.item();
1082
+ w.open("{");
1083
+ const id = edgeIdText(p, e, c);
1084
+ if (id !== null) {
1085
+ w.member("key", id);
1086
+ }
1087
+ w.member("source", idText(snapshot.ids.idOf(list.src[e])));
1088
+ w.member("target", idText(snapshot.ids.idOf(list.dst[e])));
1089
+ if (type === "mixed" && disposition === "undirected") {
1090
+ w.member("undirected", "true");
1091
+ }
1092
+ const weight = p.weights(e);
1093
+ if (weight !== null || anySet(p.edges, e, ATTRIBUTE_SLOTS)) {
1094
+ w.key("attributes");
1095
+ w.open("{");
1096
+ if (weight !== null) {
1097
+ w.member(resolved.weightKey, weight);
1098
+ }
1099
+ writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
1100
+ w.close("}");
1101
+ }
1102
+ writeSlots(c, p.edges, e, ELEMENT_SLOTS);
1103
+ w.close("}");
1104
+ yield w.take();
1105
+ }
1106
+ w.close("]");
1107
+ w.close("}");
1108
+ yield w.take();
1109
+ }
1110
+ /**
1111
+ * Write a JSON Graph Format v2 document.
1112
+ * @param snapshot - the snapshot
1113
+ * @param p - the plan
1114
+ * @yields the parts
1115
+ * @returns nothing
1116
+ */
1117
+ function* writeJgf(snapshot, p) {
1118
+ const { resolved } = p;
1119
+ const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
1120
+ const { w } = c;
1121
+ const labelSlots = new Set(["label"]);
1122
+ const relationSlots = new Set(["relation"]);
1123
+ w.open("{");
1124
+ w.key("graph");
1125
+ w.open("{");
1126
+ if (resolved.shape.id !== undefined) {
1127
+ w.member("id", JSON.stringify(resolved.shape.id));
1128
+ }
1129
+ if (snapshot.meta.name !== null) {
1130
+ w.member("label", JSON.stringify(snapshot.meta.name));
1131
+ }
1132
+ if (resolved.shape.type !== undefined) {
1133
+ w.member("type", JSON.stringify(resolved.shape.type));
1134
+ }
1135
+ w.member("directed", p.directed ? "true" : "false");
1136
+ if (p.graph.some((item) => item.column.isSet(0))) {
1137
+ w.key("metadata");
1138
+ writeGraphObject(c, p);
1139
+ }
1140
+ w.key("nodes");
1141
+ w.open("{");
1142
+ yield w.take();
1143
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1144
+ w.key(String(snapshot.ids.idOf(i)));
1145
+ w.open("{");
1146
+ writeSlots(c, p.nodes, i, labelSlots);
1147
+ if (anySet(p.nodes, i, ATTRIBUTE_SLOTS)) {
1148
+ w.key("metadata");
1149
+ w.open("{");
1150
+ writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
1151
+ w.close("}");
1152
+ }
1153
+ writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
1154
+ w.close("}");
1155
+ yield w.take();
1156
+ }
1157
+ w.close("}");
1158
+ w.key("edges");
1159
+ w.open("[");
1160
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1161
+ const disposition = edgeDisposition(p, e);
1162
+ if (disposition === "skip") {
1163
+ continue;
1164
+ }
1165
+ const list = snapshot.edgeList();
1166
+ w.item();
1167
+ w.open("{");
1168
+ const id = edgeIdText(p, e, c);
1169
+ if (id !== null) {
1170
+ w.member("id", id);
1171
+ }
1172
+ w.member("source", JSON.stringify(String(snapshot.ids.idOf(list.src[e]))));
1173
+ w.member("target", JSON.stringify(String(snapshot.ids.idOf(list.dst[e]))));
1174
+ writeSlots(c, p.edges, e, relationSlots);
1175
+ if (p.directed && disposition === "undirected") {
1176
+ w.member("directed", "false");
1177
+ }
1178
+ writeSlots(c, p.edges, e, labelSlots);
1179
+ const weight = p.weights(e);
1180
+ if (weight !== null || anySet(p.edges, e, ATTRIBUTE_SLOTS)) {
1181
+ w.key("metadata");
1182
+ w.open("{");
1183
+ if (weight !== null) {
1184
+ w.member(resolved.weightKey, weight);
1185
+ }
1186
+ writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
1187
+ w.close("}");
1188
+ }
1189
+ writeSlots(c, p.edges, e, ELEMENT_SLOTS);
1190
+ w.close("}");
1191
+ yield w.take();
1192
+ }
1193
+ w.close("]");
1194
+ w.close("}");
1195
+ w.close("}");
1196
+ yield w.take();
1197
+ }
1198
+ /**
1199
+ * Write Cytoscape.js elements.
1200
+ * @param snapshot - the snapshot
1201
+ * @param p - the plan
1202
+ * @yields the parts
1203
+ * @returns nothing
1204
+ */
1205
+ function* writeCytoscape(snapshot, p) {
1206
+ const { resolved } = p;
1207
+ const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
1208
+ const { w } = c;
1209
+ const parentSlots = new Set(["parent"]);
1210
+ const positionSlots = new Set(["cytoscapePosition"]);
1211
+ w.open("{");
1212
+ w.key("elements");
1213
+ w.open("{");
1214
+ w.key("nodes");
1215
+ w.open("[");
1216
+ yield w.take();
1217
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1218
+ w.item();
1219
+ w.open("{");
1220
+ w.key("data");
1221
+ w.open("{");
1222
+ w.member("id", idText(snapshot.ids.idOf(i)));
1223
+ writeSlots(c, p.nodes, i, parentSlots);
1224
+ writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
1225
+ w.close("}");
1226
+ for (const item of p.nodes) {
1227
+ if (positionSlots.has(item.slot) && item.column.isSet(i)) {
1228
+ const value = item.column.value(i);
1229
+ const f32 = item.column.dtype === "f32";
1230
+ w.key("position");
1231
+ w.open("{");
1232
+ w.member("x", numberText(value[0], f32, c.nonfinite));
1233
+ w.member("y", numberText(value[1], f32, c.nonfinite));
1234
+ w.close("}");
1235
+ }
1236
+ }
1237
+ writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
1238
+ w.close("}");
1239
+ yield w.take();
1240
+ }
1241
+ w.close("]");
1242
+ w.key("edges");
1243
+ w.open("[");
1244
+ const generated = edgeIdGenerator(snapshot, p);
1245
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1246
+ if (edgeDisposition(p, e) === "skip") {
1247
+ continue;
1248
+ }
1249
+ const list = snapshot.edgeList();
1250
+ w.item();
1251
+ w.open("{");
1252
+ w.key("data");
1253
+ w.open("{");
1254
+ w.member("id", edgeIdText(p, e, c) ?? generated(e));
1255
+ w.member("source", idText(snapshot.ids.idOf(list.src[e])));
1256
+ w.member("target", idText(snapshot.ids.idOf(list.dst[e])));
1257
+ const weight = p.weights(e);
1258
+ if (weight !== null) {
1259
+ w.member(resolved.weightKey, weight);
1260
+ }
1261
+ writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
1262
+ w.close("}");
1263
+ writeSlots(c, p.edges, e, ELEMENT_SLOTS);
1264
+ w.close("}");
1265
+ yield w.take();
1266
+ }
1267
+ w.close("]");
1268
+ w.close("}");
1269
+ if (p.graph.some((item) => item.column.isSet(0))) {
1270
+ w.key("data");
1271
+ writeGraphObject(c, p);
1272
+ }
1273
+ const extra = resolved.shape.cytoscape;
1274
+ if (extra !== undefined) {
1275
+ for (const key of Object.keys(extra)) {
1276
+ if (key !== "elements" && key !== "data") {
1277
+ w.member(key, valueText(extra[key], false, c.nonfinite));
1278
+ }
1279
+ }
1280
+ }
1281
+ w.close("}");
1282
+ yield w.take();
1283
+ }
1284
+ /**
1285
+ * A generator of canonical `e<index>` edge ids for the edges without one (design section 4.6),
1286
+ * skipping any text a node id or an explicit edge id already uses (Cytoscape ids share one
1287
+ * namespace).
1288
+ * @param snapshot - the snapshot
1289
+ * @param p - the plan
1290
+ * @returns the id text of an edge without an explicit id
1291
+ */
1292
+ function edgeIdGenerator(snapshot, p) {
1293
+ const used = new Set();
1294
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1295
+ used.add(String(snapshot.ids.idOf(i)));
1296
+ }
1297
+ const column = p.edgeIds;
1298
+ if (column !== null) {
1299
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1300
+ if (column.isSet(e)) {
1301
+ used.add(String(column.value(e)));
1302
+ }
1303
+ }
1304
+ }
1305
+ return (e) => {
1306
+ let candidate = `e${e}`;
1307
+ for (let k = 2; used.has(candidate); k++) {
1308
+ candidate = `e${e}_${k}`;
1309
+ }
1310
+ used.add(candidate);
1311
+ return JSON.stringify(candidate);
1312
+ };
1313
+ }
1314
+ // ============================================================ the plugin
1315
+ /**
1316
+ * The JSON exporter plugin (design section 8.5). `capabilities` is the node-link table (the
1317
+ * default dialect); check() applies the table of the dialect actually selected.
1318
+ */
1319
+ export const jsonExporter = Object.freeze({
1320
+ format: "json",
1321
+ capabilities: dialectCapabilities("node-link"),
1322
+ /**
1323
+ * Pre-flight: every loss of the selected dialect, without writing anything.
1324
+ * @param snapshot - the snapshot
1325
+ * @param options - format-specific and common options
1326
+ * @returns the notes, empty when the export is exact
1327
+ */
1328
+ check(snapshot, options) {
1329
+ return Object.freeze([...plan(snapshot, resolve(snapshot, options)).notes]);
1330
+ },
1331
+ /**
1332
+ * Write the document as UTF-8 chunks.
1333
+ * @param snapshot - the snapshot
1334
+ * @param options - format-specific and common options
1335
+ * @returns the chunks
1336
+ */
1337
+ export(snapshot, options) {
1338
+ return encodeChunks(write(snapshot, plan(snapshot, resolve(snapshot, options))));
1339
+ },
1340
+ /**
1341
+ * Write the document as one string.
1342
+ * @param snapshot - the snapshot
1343
+ * @param options - format-specific and common options
1344
+ * @returns the document
1345
+ */
1346
+ exportToString(snapshot, options) {
1347
+ return joinText(write(snapshot, plan(snapshot, resolve(snapshot, options))));
1348
+ },
1349
+ });
1350
+ /**
1351
+ * The capability table of one dialect, for callers that pick a dialect before check().
1352
+ * @param dialect - the dialect
1353
+ * @returns the frozen table
1354
+ */
1355
+ export function jsonCapabilities(dialect) {
1356
+ return dialectCapabilities(dialect);
1357
+ }
1358
+ //# sourceMappingURL=exporter.js.map