@graphty/graph-io 0.0.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -28
  3. package/dist/chunks/children-CL3Cy0ez.js +238 -0
  4. package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
  5. package/dist/chunks/escape-DyI8JofU.js +938 -0
  6. package/dist/chunks/escape-DyI8JofU.js.map +1 -0
  7. package/dist/chunks/importer-CQnJuWJw.js +2987 -0
  8. package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
  9. package/dist/chunks/importer-CpCpfbxr.js +2015 -0
  10. package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
  11. package/dist/chunks/importer-DbnGYr3_.js +2342 -0
  12. package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
  13. package/dist/chunks/importer-GozH8DkN.js +3050 -0
  14. package/dist/chunks/importer-GozH8DkN.js.map +1 -0
  15. package/dist/chunks/records-CGpxszm1.js +605 -0
  16. package/dist/chunks/records-CGpxszm1.js.map +1 -0
  17. package/dist/chunks/text-CajMdVFy.js +189 -0
  18. package/dist/chunks/text-CajMdVFy.js.map +1 -0
  19. package/dist/chunks/writer-DxSKC7TL.js +2842 -0
  20. package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
  21. package/dist/csv.d.ts +1 -0
  22. package/dist/csv.js +1702 -0
  23. package/dist/csv.js.map +1 -0
  24. package/dist/dot.d.ts +1 -0
  25. package/dist/dot.js +8 -0
  26. package/dist/dot.js.map +1 -0
  27. package/dist/gexf.d.ts +1 -0
  28. package/dist/gexf.js +3466 -0
  29. package/dist/gexf.js.map +1 -0
  30. package/dist/gml.d.ts +1 -0
  31. package/dist/gml.js +2647 -0
  32. package/dist/gml.js.map +1 -0
  33. package/dist/graph-io.d.ts +1 -0
  34. package/dist/graph-io.js +790 -0
  35. package/dist/graph-io.js.map +1 -0
  36. package/dist/graphml.d.ts +1 -0
  37. package/dist/graphml.js +8 -0
  38. package/dist/graphml.js.map +1 -0
  39. package/dist/json.d.ts +1 -0
  40. package/dist/json.js +11 -0
  41. package/dist/json.js.map +1 -0
  42. package/dist/neo4j.d.ts +1 -0
  43. package/dist/neo4j.js +2046 -0
  44. package/dist/neo4j.js.map +1 -0
  45. package/dist/pajek.d.ts +1 -0
  46. package/dist/pajek.js +8 -0
  47. package/dist/pajek.js.map +1 -0
  48. package/dist/src/children.d.ts +134 -0
  49. package/dist/src/children.d.ts.map +1 -0
  50. package/dist/src/children.js +274 -0
  51. package/dist/src/children.js.map +1 -0
  52. package/dist/src/common/attributes.d.ts +229 -0
  53. package/dist/src/common/attributes.d.ts.map +1 -0
  54. package/dist/src/common/attributes.js +368 -0
  55. package/dist/src/common/attributes.js.map +1 -0
  56. package/dist/src/common/codes.d.ts +105 -0
  57. package/dist/src/common/codes.d.ts.map +1 -0
  58. package/dist/src/common/codes.js +107 -0
  59. package/dist/src/common/codes.js.map +1 -0
  60. package/dist/src/common/declared-types.d.ts +84 -0
  61. package/dist/src/common/declared-types.d.ts.map +1 -0
  62. package/dist/src/common/declared-types.js +326 -0
  63. package/dist/src/common/declared-types.js.map +1 -0
  64. package/dist/src/common/direction.d.ts +206 -0
  65. package/dist/src/common/direction.d.ts.map +1 -0
  66. package/dist/src/common/direction.js +370 -0
  67. package/dist/src/common/direction.js.map +1 -0
  68. package/dist/src/common/escape.d.ts +92 -0
  69. package/dist/src/common/escape.d.ts.map +1 -0
  70. package/dist/src/common/escape.js +212 -0
  71. package/dist/src/common/escape.js.map +1 -0
  72. package/dist/src/common/export.d.ts +249 -0
  73. package/dist/src/common/export.d.ts.map +1 -0
  74. package/dist/src/common/export.js +594 -0
  75. package/dist/src/common/export.js.map +1 -0
  76. package/dist/src/common/format.d.ts +59 -0
  77. package/dist/src/common/format.d.ts.map +1 -0
  78. package/dist/src/common/format.js +106 -0
  79. package/dist/src/common/format.js.map +1 -0
  80. package/dist/src/common/ids.d.ts +83 -0
  81. package/dist/src/common/ids.d.ts.map +1 -0
  82. package/dist/src/common/ids.js +158 -0
  83. package/dist/src/common/ids.js.map +1 -0
  84. package/dist/src/common/input.d.ts +100 -0
  85. package/dist/src/common/input.d.ts.map +1 -0
  86. package/dist/src/common/input.js +335 -0
  87. package/dist/src/common/input.js.map +1 -0
  88. package/dist/src/common/lists.d.ts +34 -0
  89. package/dist/src/common/lists.d.ts.map +1 -0
  90. package/dist/src/common/lists.js +185 -0
  91. package/dist/src/common/lists.js.map +1 -0
  92. package/dist/src/common/options.d.ts +108 -0
  93. package/dist/src/common/options.d.ts.map +1 -0
  94. package/dist/src/common/options.js +265 -0
  95. package/dist/src/common/options.js.map +1 -0
  96. package/dist/src/common/report.d.ts +187 -0
  97. package/dist/src/common/report.d.ts.map +1 -0
  98. package/dist/src/common/report.js +274 -0
  99. package/dist/src/common/report.js.map +1 -0
  100. package/dist/src/common/temporal.d.ts +71 -0
  101. package/dist/src/common/temporal.d.ts.map +1 -0
  102. package/dist/src/common/temporal.js +266 -0
  103. package/dist/src/common/temporal.js.map +1 -0
  104. package/dist/src/common/text.d.ts +104 -0
  105. package/dist/src/common/text.d.ts.map +1 -0
  106. package/dist/src/common/text.js +255 -0
  107. package/dist/src/common/text.js.map +1 -0
  108. package/dist/src/common/weights.d.ts +77 -0
  109. package/dist/src/common/weights.d.ts.map +1 -0
  110. package/dist/src/common/weights.js +156 -0
  111. package/dist/src/common/weights.js.map +1 -0
  112. package/dist/src/common/writer.d.ts +51 -0
  113. package/dist/src/common/writer.d.ts.map +1 -0
  114. package/dist/src/common/writer.js +108 -0
  115. package/dist/src/common/writer.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +245 -0
  117. package/dist/src/common/xml.d.ts.map +1 -0
  118. package/dist/src/common/xml.js +942 -0
  119. package/dist/src/common/xml.js.map +1 -0
  120. package/dist/src/formats/csv/exporter.d.ts +70 -0
  121. package/dist/src/formats/csv/exporter.d.ts.map +1 -0
  122. package/dist/src/formats/csv/exporter.js +682 -0
  123. package/dist/src/formats/csv/exporter.js.map +1 -0
  124. package/dist/src/formats/csv/header.d.ts +66 -0
  125. package/dist/src/formats/csv/header.d.ts.map +1 -0
  126. package/dist/src/formats/csv/header.js +152 -0
  127. package/dist/src/formats/csv/header.js.map +1 -0
  128. package/dist/src/formats/csv/importer.d.ts +82 -0
  129. package/dist/src/formats/csv/importer.d.ts.map +1 -0
  130. package/dist/src/formats/csv/importer.js +849 -0
  131. package/dist/src/formats/csv/importer.js.map +1 -0
  132. package/dist/src/formats/csv/index.d.ts +60 -0
  133. package/dist/src/formats/csv/index.d.ts.map +1 -0
  134. package/dist/src/formats/csv/index.js +63 -0
  135. package/dist/src/formats/csv/index.js.map +1 -0
  136. package/dist/src/formats/csv/records.d.ts +188 -0
  137. package/dist/src/formats/csv/records.d.ts.map +1 -0
  138. package/dist/src/formats/csv/records.js +702 -0
  139. package/dist/src/formats/csv/records.js.map +1 -0
  140. package/dist/src/formats/csv/values.d.ts +105 -0
  141. package/dist/src/formats/csv/values.d.ts.map +1 -0
  142. package/dist/src/formats/csv/values.js +192 -0
  143. package/dist/src/formats/csv/values.js.map +1 -0
  144. package/dist/src/formats/dot/exporter.d.ts +52 -0
  145. package/dist/src/formats/dot/exporter.d.ts.map +1 -0
  146. package/dist/src/formats/dot/exporter.js +836 -0
  147. package/dist/src/formats/dot/exporter.js.map +1 -0
  148. package/dist/src/formats/dot/importer.d.ts +102 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -0
  150. package/dist/src/formats/dot/importer.js +1291 -0
  151. package/dist/src/formats/dot/importer.js.map +1 -0
  152. package/dist/src/formats/dot/index.d.ts +7 -0
  153. package/dist/src/formats/dot/index.d.ts.map +1 -0
  154. package/dist/src/formats/dot/index.js +7 -0
  155. package/dist/src/formats/dot/index.js.map +1 -0
  156. package/dist/src/formats/dot/names.d.ts +29 -0
  157. package/dist/src/formats/dot/names.d.ts.map +1 -0
  158. package/dist/src/formats/dot/names.js +28 -0
  159. package/dist/src/formats/dot/names.js.map +1 -0
  160. package/dist/src/formats/dot/tokenizer.d.ts +114 -0
  161. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
  162. package/dist/src/formats/dot/tokenizer.js +341 -0
  163. package/dist/src/formats/dot/tokenizer.js.map +1 -0
  164. package/dist/src/formats/gexf/exporter.d.ts +56 -0
  165. package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/exporter.js +1395 -0
  167. package/dist/src/formats/gexf/exporter.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +73 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -0
  170. package/dist/src/formats/gexf/importer.js +1880 -0
  171. package/dist/src/formats/gexf/importer.js.map +1 -0
  172. package/dist/src/formats/gexf/index.d.ts +96 -0
  173. package/dist/src/formats/gexf/index.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/index.js +97 -0
  175. package/dist/src/formats/gexf/index.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts +135 -0
  177. package/dist/src/formats/gexf/schema.d.ts.map +1 -0
  178. package/dist/src/formats/gexf/schema.js +323 -0
  179. package/dist/src/formats/gexf/schema.js.map +1 -0
  180. package/dist/src/formats/gml/exporter.d.ts +69 -0
  181. package/dist/src/formats/gml/exporter.d.ts.map +1 -0
  182. package/dist/src/formats/gml/exporter.js +1093 -0
  183. package/dist/src/formats/gml/exporter.js.map +1 -0
  184. package/dist/src/formats/gml/importer.d.ts +66 -0
  185. package/dist/src/formats/gml/importer.d.ts.map +1 -0
  186. package/dist/src/formats/gml/importer.js +1331 -0
  187. package/dist/src/formats/gml/importer.js.map +1 -0
  188. package/dist/src/formats/gml/index.d.ts +85 -0
  189. package/dist/src/formats/gml/index.d.ts.map +1 -0
  190. package/dist/src/formats/gml/index.js +88 -0
  191. package/dist/src/formats/gml/index.js.map +1 -0
  192. package/dist/src/formats/gml/syntax.d.ts +186 -0
  193. package/dist/src/formats/gml/syntax.d.ts.map +1 -0
  194. package/dist/src/formats/gml/syntax.js +467 -0
  195. package/dist/src/formats/gml/syntax.js.map +1 -0
  196. package/dist/src/formats/graphml/constants.d.ts +169 -0
  197. package/dist/src/formats/graphml/constants.d.ts.map +1 -0
  198. package/dist/src/formats/graphml/constants.js +165 -0
  199. package/dist/src/formats/graphml/constants.js.map +1 -0
  200. package/dist/src/formats/graphml/exporter.d.ts +34 -0
  201. package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
  202. package/dist/src/formats/graphml/exporter.js +1176 -0
  203. package/dist/src/formats/graphml/exporter.js.map +1 -0
  204. package/dist/src/formats/graphml/importer.d.ts +31 -0
  205. package/dist/src/formats/graphml/importer.d.ts.map +1 -0
  206. package/dist/src/formats/graphml/importer.js +1607 -0
  207. package/dist/src/formats/graphml/importer.js.map +1 -0
  208. package/dist/src/formats/graphml/index.d.ts +8 -0
  209. package/dist/src/formats/graphml/index.d.ts.map +1 -0
  210. package/dist/src/formats/graphml/index.js +8 -0
  211. package/dist/src/formats/graphml/index.js.map +1 -0
  212. package/dist/src/formats/graphml/tree.d.ts +72 -0
  213. package/dist/src/formats/graphml/tree.d.ts.map +1 -0
  214. package/dist/src/formats/graphml/tree.js +290 -0
  215. package/dist/src/formats/graphml/tree.js.map +1 -0
  216. package/dist/src/formats/json/dialect.d.ts +125 -0
  217. package/dist/src/formats/json/dialect.d.ts.map +1 -0
  218. package/dist/src/formats/json/dialect.js +262 -0
  219. package/dist/src/formats/json/dialect.js.map +1 -0
  220. package/dist/src/formats/json/exporter.d.ts +89 -0
  221. package/dist/src/formats/json/exporter.d.ts.map +1 -0
  222. package/dist/src/formats/json/exporter.js +1358 -0
  223. package/dist/src/formats/json/exporter.js.map +1 -0
  224. package/dist/src/formats/json/importer.d.ts +108 -0
  225. package/dist/src/formats/json/importer.d.ts.map +1 -0
  226. package/dist/src/formats/json/importer.js +1838 -0
  227. package/dist/src/formats/json/importer.js.map +1 -0
  228. package/dist/src/formats/json/index.d.ts +8 -0
  229. package/dist/src/formats/json/index.d.ts.map +1 -0
  230. package/dist/src/formats/json/index.js +8 -0
  231. package/dist/src/formats/json/index.js.map +1 -0
  232. package/dist/src/formats/neo4j/exporter.d.ts +68 -0
  233. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
  234. package/dist/src/formats/neo4j/exporter.js +1055 -0
  235. package/dist/src/formats/neo4j/exporter.js.map +1 -0
  236. package/dist/src/formats/neo4j/header.d.ts +52 -0
  237. package/dist/src/formats/neo4j/header.d.ts.map +1 -0
  238. package/dist/src/formats/neo4j/header.js +131 -0
  239. package/dist/src/formats/neo4j/header.js.map +1 -0
  240. package/dist/src/formats/neo4j/importer.d.ts +73 -0
  241. package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
  242. package/dist/src/formats/neo4j/importer.js +932 -0
  243. package/dist/src/formats/neo4j/importer.js.map +1 -0
  244. package/dist/src/formats/neo4j/index.d.ts +79 -0
  245. package/dist/src/formats/neo4j/index.d.ts.map +1 -0
  246. package/dist/src/formats/neo4j/index.js +83 -0
  247. package/dist/src/formats/neo4j/index.js.map +1 -0
  248. package/dist/src/formats/pajek/exporter.d.ts +58 -0
  249. package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
  250. package/dist/src/formats/pajek/exporter.js +825 -0
  251. package/dist/src/formats/pajek/exporter.js.map +1 -0
  252. package/dist/src/formats/pajek/importer.d.ts +88 -0
  253. package/dist/src/formats/pajek/importer.d.ts.map +1 -0
  254. package/dist/src/formats/pajek/importer.js +1047 -0
  255. package/dist/src/formats/pajek/importer.js.map +1 -0
  256. package/dist/src/formats/pajek/index.d.ts +7 -0
  257. package/dist/src/formats/pajek/index.d.ts.map +1 -0
  258. package/dist/src/formats/pajek/index.js +7 -0
  259. package/dist/src/formats/pajek/index.js.map +1 -0
  260. package/dist/src/formats/pajek/syntax.d.ts +112 -0
  261. package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
  262. package/dist/src/formats/pajek/syntax.js +269 -0
  263. package/dist/src/formats/pajek/syntax.js.map +1 -0
  264. package/dist/src/index.d.ts +35 -0
  265. package/dist/src/index.d.ts.map +1 -0
  266. package/dist/src/index.js +39 -0
  267. package/dist/src/index.js.map +1 -0
  268. package/dist/src/registry.d.ts +207 -0
  269. package/dist/src/registry.d.ts.map +1 -0
  270. package/dist/src/registry.js +481 -0
  271. package/dist/src/registry.js.map +1 -0
  272. package/dist/src/sniff.d.ts +104 -0
  273. package/dist/src/sniff.d.ts.map +1 -0
  274. package/dist/src/sniff.js +357 -0
  275. package/dist/src/sniff.js.map +1 -0
  276. package/dist/src/types.d.ts +238 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +29 -0
  279. package/dist/src/types.js.map +1 -0
  280. package/dist/tsconfig.build.tsbuildinfo +1 -0
  281. package/package.json +122 -7
  282. package/src/children.ts +335 -0
  283. package/src/common/attributes.ts +520 -0
  284. package/src/common/codes.ts +153 -0
  285. package/src/common/declared-types.ts +374 -0
  286. package/src/common/direction.ts +518 -0
  287. package/src/common/escape.ts +231 -0
  288. package/src/common/export.ts +817 -0
  289. package/src/common/format.ts +111 -0
  290. package/src/common/ids.ts +176 -0
  291. package/src/common/input.ts +378 -0
  292. package/src/common/lists.ts +196 -0
  293. package/src/common/options.ts +377 -0
  294. package/src/common/report.ts +352 -0
  295. package/src/common/temporal.ts +302 -0
  296. package/src/common/text.ts +294 -0
  297. package/src/common/weights.ts +202 -0
  298. package/src/common/writer.ts +123 -0
  299. package/src/common/xml.ts +1053 -0
  300. package/src/formats/csv/exporter.ts +894 -0
  301. package/src/formats/csv/header.ts +172 -0
  302. package/src/formats/csv/importer.ts +1104 -0
  303. package/src/formats/csv/index.ts +88 -0
  304. package/src/formats/csv/records.ts +813 -0
  305. package/src/formats/csv/values.ts +224 -0
  306. package/src/formats/dot/exporter.ts +1014 -0
  307. package/src/formats/dot/importer.ts +1549 -0
  308. package/src/formats/dot/index.ts +7 -0
  309. package/src/formats/dot/names.ts +40 -0
  310. package/src/formats/dot/tokenizer.ts +384 -0
  311. package/src/formats/gexf/exporter.ts +1696 -0
  312. package/src/formats/gexf/importer.ts +2333 -0
  313. package/src/formats/gexf/index.ts +142 -0
  314. package/src/formats/gexf/schema.ts +361 -0
  315. package/src/formats/gml/exporter.ts +1404 -0
  316. package/src/formats/gml/importer.ts +1591 -0
  317. package/src/formats/gml/index.ts +128 -0
  318. package/src/formats/gml/syntax.ts +545 -0
  319. package/src/formats/graphml/constants.ts +225 -0
  320. package/src/formats/graphml/exporter.ts +1458 -0
  321. package/src/formats/graphml/importer.ts +2027 -0
  322. package/src/formats/graphml/index.ts +8 -0
  323. package/src/formats/graphml/tree.ts +318 -0
  324. package/src/formats/json/dialect.ts +317 -0
  325. package/src/formats/json/exporter.ts +1616 -0
  326. package/src/formats/json/importer.ts +2271 -0
  327. package/src/formats/json/index.ts +8 -0
  328. package/src/formats/neo4j/exporter.ts +1287 -0
  329. package/src/formats/neo4j/header.ts +156 -0
  330. package/src/formats/neo4j/importer.ts +1220 -0
  331. package/src/formats/neo4j/index.ts +116 -0
  332. package/src/formats/pajek/exporter.ts +1000 -0
  333. package/src/formats/pajek/importer.ts +1311 -0
  334. package/src/formats/pajek/index.ts +7 -0
  335. package/src/formats/pajek/syntax.ts +307 -0
  336. package/src/index.ts +244 -0
  337. package/src/registry.ts +617 -0
  338. package/src/sniff.ts +397 -0
  339. package/src/types.ts +262 -0
@@ -0,0 +1,682 @@
1
+ /**
2
+ * The CSV / TSV exporter (design section 8.5; research note 07 section 9): writes the edge table
3
+ * (`Source,Target,Type,Id,Label,Weight,<attributes>` in the Gephi dialect, `source,target,weight,
4
+ * <attributes>` in the generic one) or, with `table: "nodes"`, the node table (`Id,Label,
5
+ * <attributes>`), RFC 4180 quoted, one row per logical edge with expanded pairs folded back
6
+ * through the `pair` role column.
7
+ *
8
+ * What survives a re-import exactly: ids (as text under the canonical rule), topology and
9
+ * orientation, explicit weights (blank cells for defaulted ones), the per-row direction of the
10
+ * Gephi dialect, edge ids and labels (string), bool / i32 / f64 / string columns (an f64 value is
11
+ * written with a decimal point so it reads back as f64), low-cardinality dicts, and set empty
12
+ * strings (the quoted empty cell; an unset cell is written as nothing). check() reports everything
13
+ * else: the generic dialect drops direction, mutual pairs become two directed rows, lists and json
14
+ * values are written as text, non-finite numbers do not read back, attribute names that collide
15
+ * with the reserved headers are not written, node attributes are written by the node table only,
16
+ * and the edge table carries neither isolated nodes nor the node order.
17
+ */
18
+ import { GraphFormatError } from "@graphty/graph-format";
19
+ import { DICT_SAMPLE_ROWS, DictHeuristic } from "../../common/attributes.js";
20
+ import { pairFolding } from "../../common/direction.js";
21
+ import { quoteCsvCell } from "../../common/escape.js";
22
+ import { capabilities, checkCapabilities, countMixedEdges, LOSS } from "../../common/export.js";
23
+ import { formatDecimal, formatInteger } from "../../common/format.js";
24
+ import { canonicalId } from "../../common/ids.js";
25
+ import { joinListText } from "../../common/lists.js";
26
+ import { resolveExportOptions } from "../../common/options.js";
27
+ import { inferTextDtype } from "../../common/text.js";
28
+ import { explicitWeights } from "../../common/weights.js";
29
+ import { encodeChunks, joinText } from "../../common/writer.js";
30
+ import { EDGE_ID_NAMES, findColumn, LABEL_NAMES } from "./header.js";
31
+ /** The CSV loss-note codes of check(); the shared ones are LOSS's. */
32
+ export const CSV_LOSS = Object.freeze({
33
+ /** Two node ids share one text (a number and a string); export() throws E_INVALID_ID. */
34
+ ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
35
+ /** Ids whose text reads back as the other type under the canonical rule. */
36
+ ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
37
+ /** The generic dialect has no direction column; an undirected or mixed graph reads back as directed. */
38
+ DIRECTION_DROPPED: "W_CSV_DIRECTION_DROPPED",
39
+ /** Mutual pairs are written as two directed rows. */
40
+ MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
41
+ /** An attribute column named like a reserved header is not written. */
42
+ RESERVED_NAME: "W_CSV_RESERVED_NAME",
43
+ /** A column without a role that the importer gives one back by its name. */
44
+ ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
45
+ /** A role column (id, label) whose name the importer does not recognise; the role is lost. */
46
+ ROLE_NAME: "W_CSV_ROLE_NAME",
47
+ /** A role column (id, label) that is not string / dict reads back as string. */
48
+ TEXT_ROLE: "W_CSV_TEXT_ROLE",
49
+ /** NaN / Infinity in a numeric column read back as text. */
50
+ NONFINITE: "W_CSV_NONFINITE",
51
+ /** A text column whose every value reads back as a number or boolean. */
52
+ TEXT_INFERRED: LOSS.TEXT_INFERRED,
53
+ /** A dict column whose cardinality makes the importer read it back as string, or the reverse. */
54
+ STORAGE_CLASS_CHANGED: LOSS.STORAGE_CLASS,
55
+ /** Node attributes are written by a `table: "nodes"` export only. */
56
+ NODE_TABLE: "W_CSV_NODE_TABLE",
57
+ /** The edge table carries no node without an edge: isolated nodes vanish on re-import. */
58
+ ISOLATED_NODES: "W_CSV_ISOLATED_NODES",
59
+ /** The edge table lists nodes by first appearance; the node order (and indices) change on re-import. */
60
+ NODE_ORDER: "W_CSV_NODE_ORDER",
61
+ });
62
+ /** What the CSV format keeps as declared. */
63
+ export const CSV_CAPABILITIES = capabilities({
64
+ mixedDirection: true,
65
+ multiEdges: true,
66
+ selfLoops: true,
67
+ edgeIds: "optional",
68
+ idCharset: "any",
69
+ dtypes: ["bool", "i32", "f64", "string", "dict"],
70
+ });
71
+ const DIALECTS = {
72
+ gephi: { source: "Source", target: "Target", type: "Type", id: "Id", label: "Label", weight: "Weight" },
73
+ generic: { source: "source", target: "target", type: null, id: "id", label: "label", weight: "weight" },
74
+ };
75
+ /** The roles the tables have a slot for (the id and label headers); every other role is reported. */
76
+ const KEPT_ROLES = new Set(["id", "label"]);
77
+ /** Roles the edge and node tables never write as attributes. */
78
+ const SKIPPED_ROLES = new Set([
79
+ "directed",
80
+ "pair",
81
+ "mutual",
82
+ "weight",
83
+ "timeText",
84
+ "position",
85
+ "color",
86
+ "size",
87
+ "shape",
88
+ "thickness",
89
+ "parent",
90
+ "parents",
91
+ "start",
92
+ "end",
93
+ "timestamp",
94
+ "timestamps",
95
+ "spells",
96
+ "open",
97
+ ]);
98
+ /**
99
+ * Apply the defaults of the CSV export options and check every value.
100
+ * @param options - the caller's options
101
+ * @returns the resolved options; E_UNSUPPORTED for a value outside its set
102
+ */
103
+ function resolveCsvExportOptions(options) {
104
+ const o = options ?? {};
105
+ if (o.dialect !== undefined && o.dialect !== "gephi" && o.dialect !== "generic") {
106
+ throw new GraphFormatError("E_UNSUPPORTED", 'option dialect: expected "gephi" or "generic"', {
107
+ option: "dialect",
108
+ found: o.dialect,
109
+ });
110
+ }
111
+ if (o.table !== undefined && o.table !== "edges" && o.table !== "nodes") {
112
+ throw new GraphFormatError("E_UNSUPPORTED", 'option table: expected "edges" or "nodes"', {
113
+ option: "table",
114
+ found: o.table,
115
+ });
116
+ }
117
+ if (o.delimiter !== undefined &&
118
+ (typeof o.delimiter !== "string" ||
119
+ o.delimiter.length !== 1 ||
120
+ o.delimiter === '"' ||
121
+ o.delimiter === "\n" ||
122
+ o.delimiter === "\r")) {
123
+ throw new GraphFormatError("E_UNSUPPORTED", "option delimiter: expected one character other than a quote or a line break", { option: "delimiter", found: o.delimiter });
124
+ }
125
+ if (o.newline !== undefined && o.newline !== "\n" && o.newline !== "\r\n") {
126
+ throw new GraphFormatError("E_UNSUPPORTED", "option newline: expected LF or CRLF", {
127
+ option: "newline",
128
+ found: o.newline,
129
+ });
130
+ }
131
+ if (o.header !== undefined && typeof o.header !== "boolean") {
132
+ throw new GraphFormatError("E_UNSUPPORTED", "option header: expected a boolean", {
133
+ option: "header",
134
+ found: o.header,
135
+ });
136
+ }
137
+ return {
138
+ dialect: DIALECTS[o.dialect ?? "gephi"],
139
+ table: o.table ?? "edges",
140
+ delimiter: o.delimiter ?? ",",
141
+ newline: o.newline ?? "\n",
142
+ header: o.header ?? true,
143
+ };
144
+ }
145
+ /**
146
+ * One scalar (or one list item) as text by its dtype.
147
+ * @param dtype - the scalar dtype
148
+ * @param value - the value
149
+ * @returns the text
150
+ */
151
+ function scalarText(dtype, value) {
152
+ switch (dtype) {
153
+ case "bool":
154
+ return value === true ? "true" : "false";
155
+ case "f32":
156
+ case "f64":
157
+ return typeof value === "number" ? formatDecimal(value, dtype) : String(value);
158
+ case "i32":
159
+ case "u32":
160
+ case "u8":
161
+ return typeof value === "number" ? formatInteger(value) : String(value);
162
+ case "string":
163
+ case "dict":
164
+ return typeof value === "string" ? value : String(value);
165
+ case "json":
166
+ return JSON.stringify(value) ?? "";
167
+ default:
168
+ return String(value);
169
+ }
170
+ }
171
+ /**
172
+ * The text of one cell: null for an unset row (written as nothing between the delimiters);
173
+ * components and list items joined by `;`.
174
+ * @param column - the column
175
+ * @param row - the row
176
+ * @returns the cell text, or null
177
+ */
178
+ function cellText(column, row) {
179
+ if (!column.isSet(row)) {
180
+ return null;
181
+ }
182
+ switch (column.dtype) {
183
+ case "list": {
184
+ const items = column.sliceOf(row);
185
+ const { dtype } = column.child;
186
+ return joinListText(items.map((item) => scalarText(dtype, item)), "semicolon");
187
+ }
188
+ case "json":
189
+ return JSON.stringify(column.values[row]) ?? "";
190
+ case "bool":
191
+ case "string":
192
+ case "dict":
193
+ return scalarText(column.dtype, column.value(row));
194
+ default: {
195
+ const { components } = column.meta;
196
+ const { data } = column;
197
+ if (components === 1) {
198
+ return scalarText(column.dtype, data[row]);
199
+ }
200
+ const parts = [];
201
+ for (let k = 0; k < components; k++) {
202
+ parts.push(scalarText(column.dtype, data[row * components + k]));
203
+ }
204
+ return parts.join(";");
205
+ }
206
+ }
207
+ }
208
+ /**
209
+ * Build the plan of an export: resolve options, choose the rows and columns, and collect the
210
+ * CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
211
+ * every written cell and are collected for check() only; export() needs the plan and the fatal
212
+ * id-collision note alone.
213
+ * @param snapshot - the snapshot
214
+ * @param options - the caller's options
215
+ * @param values - whether to scan the values for notes
216
+ * @returns the plan
217
+ */
218
+ /**
219
+ * The id notes: text collisions are fatal (export() throws E_INVALID_ID); type changes under the
220
+ * canonical re-read are reported.
221
+ * @param snapshot - the snapshot
222
+ * @param values - whether the values are inspected (false for a plan that writes nothing)
223
+ * @param note - the note recorder
224
+ */
225
+ function idNotes(snapshot, values, note) {
226
+ const { ids } = snapshot;
227
+ if (ids.kind === "mixed") {
228
+ const seen = new Set();
229
+ let collisions = 0;
230
+ for (let i = 0; i < ids.size; i++) {
231
+ const text = String(ids.idOf(i));
232
+ if (seen.has(text)) {
233
+ collisions++;
234
+ }
235
+ else {
236
+ seen.add(text);
237
+ }
238
+ }
239
+ if (collisions > 0) {
240
+ note(CSV_LOSS.ID_TEXT_COLLISION, `${collisions} node id(s) share their text with another id (a number and a string); export() will throw E_INVALID_ID`, null, collisions);
241
+ }
242
+ }
243
+ if (values && ids.kind !== "identity" && ids.kind !== "dense") {
244
+ let changed = 0;
245
+ for (let i = 0; i < ids.size; i++) {
246
+ const id = ids.idOf(i);
247
+ if (typeof canonicalId(String(id)) !== typeof id) {
248
+ changed++;
249
+ }
250
+ }
251
+ if (changed > 0) {
252
+ note(CSV_LOSS.ID_TEXT_TYPE, `${changed} node id(s) read back as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`, null, changed);
253
+ }
254
+ }
255
+ }
256
+ function planExport(snapshot, options, values) {
257
+ const common = resolveExportOptions(options);
258
+ const csv = resolveCsvExportOptions(options);
259
+ const notes = [];
260
+ const note = (code, message, column = null, count = null) => {
261
+ notes.push(Object.freeze({ code, message, column, count }));
262
+ };
263
+ const { ids } = snapshot;
264
+ const idText = (index) => String(ids.idOf(index));
265
+ idNotes(snapshot, values, note);
266
+ // edge rows: fold the mirrors of undirected pairs; mutual pairs stay two directed rows
267
+ const folding = pairFolding(snapshot);
268
+ const edgeRows = [];
269
+ for (let e = 0; e < snapshot.edgeCount; e++) {
270
+ if (!folding.folded(e)) {
271
+ edgeRows.push(e);
272
+ }
273
+ }
274
+ if (csv.dialect.type === null) {
275
+ const mixed = countMixedEdges(snapshot);
276
+ if (!snapshot.directed) {
277
+ note(CSV_LOSS.DIRECTION_DROPPED, `the generic dialect has no direction column; ${snapshot.edgeCount} undirected edge(s) read back as directed unless the importer is told otherwise`, null, snapshot.edgeCount);
278
+ }
279
+ else if (mixed > 0) {
280
+ note(CSV_LOSS.DIRECTION_DROPPED, `the generic dialect has no direction column; ${mixed} undirected edge(s) of a mixed graph read back as one directed edge each`, null, mixed);
281
+ }
282
+ }
283
+ if (folding.mutualCount > 0) {
284
+ note(CSV_LOSS.MUTUAL_EXPANDED, `${folding.mutualCount} mutual pair(s) are written as two directed rows; the mutual mark is lost`, null, folding.mutualCount);
285
+ }
286
+ if (csv.table === "edges") {
287
+ noteNodeCoverage(snapshot, edgeRows, note);
288
+ }
289
+ // weights
290
+ const weights = explicitWeights(snapshot);
291
+ // the edge table's columns
292
+ const edgeId = snapshot.edges.byRole("id");
293
+ const edgeLabel = snapshot.edges.byRole("label");
294
+ const edgeReserved = new Set([csv.dialect.source, csv.dialect.target, csv.dialect.weight].map(lower));
295
+ if (csv.dialect.type !== null) {
296
+ edgeReserved.add(lower(csv.dialect.type));
297
+ }
298
+ const edgeColumns = attributeColumns(snapshot.edges, "edge", edgeReserved, edgeId, edgeLabel, csv.table === "edges" ? note : null);
299
+ if (csv.table === "edges" && values) {
300
+ checkRoleColumn(edgeId, "id", EDGE_ID_NAMES, "edge", edgeRows, note);
301
+ checkRoleColumn(edgeLabel, "label", LABEL_NAMES, "edge", edgeRows, note);
302
+ for (const { column } of edgeColumns) {
303
+ checkValues(column, "edge", edgeRows, note);
304
+ }
305
+ }
306
+ // the node table's columns
307
+ const nodeLabel = snapshot.nodes.byRole("label");
308
+ const nodeReserved = new Set([lower(csv.dialect.id)]);
309
+ const nodeRows = [];
310
+ if (csv.table === "nodes" && values) {
311
+ for (let i = 0; i < snapshot.nodeCount; i++) {
312
+ nodeRows.push(i);
313
+ }
314
+ }
315
+ const nodeColumns = attributeColumns(snapshot.nodes, "node", nodeReserved, null, nodeLabel, csv.table === "nodes" ? note : null);
316
+ if (csv.table === "nodes") {
317
+ if (values) {
318
+ checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
319
+ for (const { column } of nodeColumns) {
320
+ checkValues(column, "node", nodeRows, note);
321
+ }
322
+ }
323
+ }
324
+ else {
325
+ const written = nodeColumns.length + (nodeLabel === null ? 0 : 1);
326
+ if (written > 0) {
327
+ note(CSV_LOSS.NODE_TABLE, `${written} node column(s) are written by a table: "nodes" export only`, null, written);
328
+ }
329
+ }
330
+ return {
331
+ csv,
332
+ common,
333
+ notes,
334
+ edgeRows,
335
+ folding,
336
+ edgeId,
337
+ edgeLabel,
338
+ weights,
339
+ edgeColumns,
340
+ nodeLabel,
341
+ nodeColumns,
342
+ idText,
343
+ };
344
+ }
345
+ /**
346
+ * The edge table is not a node carrier (research note 07 section 2.6: node attributes and node
347
+ * presence come from a separate node table): a node without an edge is not written at all, and
348
+ * the importer creates nodes in the order the edge rows first mention them. Both are reported so
349
+ * check() predicts the ids and the order a re-import gives back (design section 8.5).
350
+ * @param snapshot - the snapshot
351
+ * @param edgeRows - the edge rows written
352
+ * @param note - the recorder
353
+ */
354
+ function noteNodeCoverage(snapshot, edgeRows, note) {
355
+ const { nodeCount } = snapshot;
356
+ if (nodeCount === 0) {
357
+ return;
358
+ }
359
+ const list = snapshot.edgeList();
360
+ const seen = new Uint8Array(nodeCount);
361
+ let next = 0;
362
+ let reordered = 0;
363
+ const mention = (index) => {
364
+ if (seen[index] === 1) {
365
+ return;
366
+ }
367
+ seen[index] = 1;
368
+ if (index !== next) {
369
+ reordered++;
370
+ }
371
+ next++;
372
+ };
373
+ for (const e of edgeRows) {
374
+ mention(list.src[e]);
375
+ mention(list.dst[e]);
376
+ }
377
+ const isolated = nodeCount - next;
378
+ if (isolated > 0) {
379
+ note(CSV_LOSS.ISOLATED_NODES, `${isolated} node(s) have no edge and cannot be written by the edge table; write the node table (table: "nodes") to keep them`, null, isolated);
380
+ }
381
+ if (reordered > 0) {
382
+ note(CSV_LOSS.NODE_ORDER, `${reordered} node(s) are first mentioned by an edge row out of index order; a re-import numbers nodes by first appearance`, null, reordered);
383
+ }
384
+ }
385
+ /**
386
+ * Lower-case a header name for reserved-name comparisons.
387
+ * @param name - the name
388
+ * @returns the lower-case name
389
+ */
390
+ function lower(name) {
391
+ return name.toLowerCase();
392
+ }
393
+ /**
394
+ * The attribute columns of a table that are written, in declaration order, skipping the role
395
+ * columns handled elsewhere, the roles the format cannot hold, and names that collide with the
396
+ * reserved headers (reported).
397
+ * @param table - the node or edge table
398
+ * @param domain - node or edge, for messages
399
+ * @param reserved - lower-case header names the dialect writes for structure
400
+ * @param idColumn - the table's id role column, or null
401
+ * @param labelColumn - the table's label role column, or null
402
+ * @param note - the recorder, or null when this table's notes are not wanted
403
+ * @returns the columns and their headers
404
+ */
405
+ function attributeColumns(table, domain, reserved, idColumn, labelColumn, note) {
406
+ const out = [];
407
+ for (const column of table) {
408
+ const { name, role } = column.meta;
409
+ if (column === idColumn || column === labelColumn) {
410
+ continue;
411
+ }
412
+ if (role !== null && SKIPPED_ROLES.has(role)) {
413
+ continue;
414
+ }
415
+ const low = lower(name);
416
+ const set = column.length - column.nullCount;
417
+ const isIdName = domain === "edge" ? findColumn([name], EDGE_ID_NAMES) >= 0 : false;
418
+ if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
419
+ note?.(CSV_LOSS.RESERVED_NAME, `${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`, name, set);
420
+ continue;
421
+ }
422
+ if (findColumn([name], LABEL_NAMES) >= 0) {
423
+ if (labelColumn !== null) {
424
+ note?.(CSV_LOSS.RESERVED_NAME, `${domain} column "${name}" is not written: the name is reserved for the label column`, name, set);
425
+ continue;
426
+ }
427
+ note?.(CSV_LOSS.ROLE_ASSUMED, `${domain} column "${name}" reads back with the label role (string)`, name, set);
428
+ }
429
+ out.push({ column, header: name });
430
+ }
431
+ return out;
432
+ }
433
+ /**
434
+ * Notes about a role column (id or label): a name the importer does not map to the role, and a
435
+ * dtype other than string / dict (read back as string).
436
+ * @param column - the role column, or null
437
+ * @param role - the role
438
+ * @param names - the header names the importer maps to the role
439
+ * @param domain - node or edge
440
+ * @param rows - the rows written
441
+ * @param note - the recorder
442
+ */
443
+ function checkRoleColumn(column, role, names, domain, rows, note) {
444
+ if (column === null) {
445
+ return;
446
+ }
447
+ const { name } = column.meta;
448
+ if (findColumn([name], names) < 0) {
449
+ note(CSV_LOSS.ROLE_NAME, `${domain} column "${name}" (${role}) is written under its name, which the importer does not map to the ${role} role`, name, countSet(column, rows));
450
+ }
451
+ if (column.dtype !== "string" && column.dtype !== "dict") {
452
+ note(CSV_LOSS.TEXT_ROLE, `${domain} column "${name}" (${role}) is ${column.dtype}; it reads back as string`, name, countSet(column, rows));
453
+ }
454
+ checkValues(column, domain, rows, note, true);
455
+ }
456
+ /**
457
+ * Set rows among the rows written.
458
+ * @param column - the column
459
+ * @param rows - the rows written
460
+ * @returns the count
461
+ */
462
+ function countSet(column, rows) {
463
+ let count = 0;
464
+ for (const row of rows) {
465
+ if (column.isSet(row)) {
466
+ count++;
467
+ }
468
+ }
469
+ return count;
470
+ }
471
+ const WIDENING = { bool: 0, i32: 1, f64: 2, string: 3 };
472
+ const TEXT_DTYPES = ["bool", "i32", "f64", "string"];
473
+ /**
474
+ * Value-level notes of one written column over the rows written: non-finite numbers (text on
475
+ * re-import), a text column whose values all read back as numbers or booleans, and the dict
476
+ * heuristic's verdict when it differs from the dtype. A set empty string is written as the quoted
477
+ * empty cell and reads back exactly.
478
+ * @param column - the column
479
+ * @param domain - node or edge
480
+ * @param rows - the rows written
481
+ * @param note - the recorder
482
+ * @param roleColumn - whether the column is declared string on re-import (no inference)
483
+ */
484
+ function checkValues(column, domain, rows, note, roleColumn = false) {
485
+ const { name, dtype } = column.meta;
486
+ const label = `${domain} column "${name}"`;
487
+ if (column.dtype === "f32" || column.dtype === "f64") {
488
+ let nonFinite = 0;
489
+ const { components } = column.meta;
490
+ const { data } = column;
491
+ for (const row of rows) {
492
+ if (!column.isSet(row)) {
493
+ continue;
494
+ }
495
+ for (let k = 0; k < components; k++) {
496
+ if (!Number.isFinite(data[row * components + k])) {
497
+ nonFinite++;
498
+ break;
499
+ }
500
+ }
501
+ }
502
+ if (nonFinite > 0) {
503
+ note(CSV_LOSS.NONFINITE, `${label}: ${nonFinite} non-finite value(s) read back as text`, name, nonFinite);
504
+ }
505
+ return;
506
+ }
507
+ if (dtype !== "string" && dtype !== "dict") {
508
+ return;
509
+ }
510
+ let widest = -1;
511
+ let candidate = true;
512
+ let set = 0;
513
+ const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
514
+ for (const row of rows) {
515
+ if (!column.isSet(row)) {
516
+ continue;
517
+ }
518
+ const text = column.value(row);
519
+ if (typeof text !== "string") {
520
+ continue;
521
+ }
522
+ set++;
523
+ const kind = inferTextDtype(text);
524
+ widest = Math.max(widest, WIDENING[kind]);
525
+ if (!heuristic.decided) {
526
+ if (kind !== "string") {
527
+ candidate = false;
528
+ }
529
+ heuristic.observe(text);
530
+ }
531
+ }
532
+ if (roleColumn || widest < 0) {
533
+ return;
534
+ }
535
+ const readsAsDict = candidate && heuristic.decide() === "dict";
536
+ if (widest < WIDENING.string && !readsAsDict) {
537
+ note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[widest]}`, name, set);
538
+ return;
539
+ }
540
+ if (dtype === "dict" && !readsAsDict) {
541
+ note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as string (cardinality too high for a dict)`, name, null);
542
+ }
543
+ else if (dtype === "string" && readsAsDict) {
544
+ note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
545
+ }
546
+ }
547
+ /**
548
+ * The Type cell of an edge row.
549
+ * @param snapshot - the snapshot
550
+ * @param plan - the plan
551
+ * @param e - the edge
552
+ * @returns "Directed" or "Undirected"
553
+ */
554
+ function typeText(snapshot, plan, e) {
555
+ if (!snapshot.directed || !plan.folding.sourceDirected(e)) {
556
+ return "Undirected";
557
+ }
558
+ return "Directed";
559
+ }
560
+ /**
561
+ * The lines of the export, one string per row (terminator included).
562
+ * @param snapshot - the snapshot
563
+ * @param plan - the plan
564
+ * @yields one line at a time
565
+ * @returns nothing
566
+ */
567
+ function* lines(snapshot, plan) {
568
+ const { csv } = plan;
569
+ const { delimiter, newline } = csv;
570
+ const quote = (text) => quoteCsvCell(text, delimiter);
571
+ const cell = (column, row) => {
572
+ const text = cellText(column, row);
573
+ return text === null ? "" : quote(text);
574
+ };
575
+ if (csv.table === "nodes") {
576
+ const headers = [csv.dialect.id];
577
+ if (plan.nodeLabel !== null) {
578
+ headers.push(plan.nodeLabel.meta.name);
579
+ }
580
+ for (const { header } of plan.nodeColumns) {
581
+ headers.push(header);
582
+ }
583
+ if (csv.header) {
584
+ yield headers.map(quote).join(delimiter) + newline;
585
+ }
586
+ for (let i = 0; i < snapshot.nodeCount; i++) {
587
+ const cells = [quote(plan.idText(i))];
588
+ if (plan.nodeLabel !== null) {
589
+ cells.push(cell(plan.nodeLabel, i));
590
+ }
591
+ for (const { column } of plan.nodeColumns) {
592
+ cells.push(cell(column, i));
593
+ }
594
+ yield cells.join(delimiter) + newline;
595
+ }
596
+ return;
597
+ }
598
+ const headers = [csv.dialect.source, csv.dialect.target];
599
+ if (csv.dialect.type !== null) {
600
+ headers.push(csv.dialect.type);
601
+ }
602
+ if (plan.edgeId !== null) {
603
+ headers.push(plan.edgeId.meta.name);
604
+ }
605
+ if (plan.edgeLabel !== null) {
606
+ headers.push(plan.edgeLabel.meta.name);
607
+ }
608
+ if (plan.weights.weighted) {
609
+ headers.push(csv.dialect.weight);
610
+ }
611
+ for (const { header } of plan.edgeColumns) {
612
+ headers.push(header);
613
+ }
614
+ if (csv.header) {
615
+ yield headers.map(quote).join(delimiter) + newline;
616
+ }
617
+ const list = snapshot.edgeList();
618
+ for (const e of plan.edgeRows) {
619
+ const cells = [quote(plan.idText(list.src[e])), quote(plan.idText(list.dst[e]))];
620
+ if (csv.dialect.type !== null) {
621
+ cells.push(typeText(snapshot, plan, e));
622
+ }
623
+ if (plan.edgeId !== null) {
624
+ cells.push(cell(plan.edgeId, e));
625
+ }
626
+ if (plan.edgeLabel !== null) {
627
+ cells.push(cell(plan.edgeLabel, e));
628
+ }
629
+ if (plan.weights.weighted) {
630
+ cells.push(plan.weights.text(e) ?? "");
631
+ }
632
+ for (const { column } of plan.edgeColumns) {
633
+ cells.push(cell(column, e));
634
+ }
635
+ yield cells.join(delimiter) + newline;
636
+ }
637
+ }
638
+ /**
639
+ * Plan an export and refuse it when a fatal note is present (E_INVALID_ID for id text collisions),
640
+ * before anything is written.
641
+ * @param snapshot - the snapshot
642
+ * @param options - the caller's options
643
+ * @returns the plan
644
+ */
645
+ function prepare(snapshot, options) {
646
+ const plan = planExport(snapshot, options, false);
647
+ const collision = plan.notes.find((n) => n.code === CSV_LOSS.ID_TEXT_COLLISION);
648
+ if (collision !== undefined) {
649
+ throw new GraphFormatError("E_INVALID_ID", collision.message, {
650
+ reason: "collision",
651
+ count: collision.count,
652
+ });
653
+ }
654
+ return plan;
655
+ }
656
+ /**
657
+ * Pre-flight: what a CSV export would lose. The generic notes describe the snapshot as a whole
658
+ * against the format; the CSV notes concern the table selected by `table`.
659
+ * @param snapshot - the snapshot
660
+ * @param options - CSV and common options
661
+ * @returns the notes, empty when the export is exact
662
+ */
663
+ function check(snapshot, options) {
664
+ const plan = planExport(snapshot, options, true);
665
+ return Object.freeze([
666
+ ...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, { roles: KEPT_ROLES }),
667
+ ...plan.notes,
668
+ ]);
669
+ }
670
+ /** The CSV / TSV exporter plugin (subpath `@graphty/graph-io/csv`). */
671
+ export const csvExporter = Object.freeze({
672
+ format: "csv",
673
+ capabilities: CSV_CAPABILITIES,
674
+ check,
675
+ export(snapshot, options) {
676
+ return encodeChunks(lines(snapshot, prepare(snapshot, options)));
677
+ },
678
+ exportToString(snapshot, options) {
679
+ return joinText(lines(snapshot, prepare(snapshot, options)));
680
+ },
681
+ });
682
+ //# sourceMappingURL=exporter.js.map