@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,825 @@
1
+ /**
2
+ * The Pajek NET exporter (design section 8.5; research note 07 sections 2.5 and 9): vertices are
3
+ * numbered 1..N in index order (the `dense-1-based` id charset: an id that is not its 1-based
4
+ * index survives as the label only, which check() reports as W_ID_RENUMBERED; under
5
+ * `sanitizeIds: "mangle"` it is also written as a `graphty_originalId` parameter the importer
6
+ * restores under `restoreMangledIds`), the label role
7
+ * column is the label, the position role column the coordinates, a `shape` column the shape
8
+ * keyword, every other writable column a `key value` parameter and the spells role a time
9
+ * interval token. Logical edges are written in index order in runs of `*Arcs` (directed) and
10
+ * `*Edges` (undirected) sections, folding expanded pairs back through the pair role and reading
11
+ * attributes from the primary half, so a mixed file round-trips in its original edge order. The
12
+ * weight is written for edges whose weight was explicit (the role-weight column's validity).
13
+ */
14
+ import { GraphFormatError } from "@graphty/graph-format";
15
+ import { pairFolding } from "../../common/direction.js";
16
+ import { isPajekLabel, quotePajekLabel } from "../../common/escape.js";
17
+ import { capabilities, checkCapabilities, LOSS, sanitizeIds } from "../../common/export.js";
18
+ import { formatDecimal, formatF32, formatF64, formatInteger } from "../../common/format.js";
19
+ import { canonicalId } from "../../common/ids.js";
20
+ import { resolveExportOptions } from "../../common/options.js";
21
+ import { explicitWeights } from "../../common/weights.js";
22
+ import { encodeChunks, joinText } from "../../common/writer.js";
23
+ import { formatIntervals, isParameterKey, LABEL_COLUMN, ORIGINAL_ID_KEY, RELATION_COLUMN, SHAPE_COLUMN, SHAPES, } from "./syntax.js";
24
+ /**
25
+ * The LossNote codes of the Pajek exporter's check(): the Pajek-specific ones and, aliased, the
26
+ * shared ones it records itself (`LOSS` holds the rest of the generic pre-flight's codes). A key
27
+ * is the code without its severity and format prefixes.
28
+ */
29
+ export const PAJEK_LOSS = Object.freeze({
30
+ /** A label or text value holds a double quote or a line break; export() will throw E_UNSUPPORTED. */
31
+ TEXT: "E_PAJEK_TEXT",
32
+ /** A column whose name cannot be a parameter key (whitespace, a quote, numeric, a shape keyword); skipped. */
33
+ KEY_DROPPED: "W_PAJEK_KEY_DROPPED",
34
+ /** A label role column that is not text; its values are written as text and re-import as string. */
35
+ LABEL_AS_TEXT: "W_PAJEK_LABEL_AS_TEXT",
36
+ /** A non-finite f64 value; written as Infinity / NaN text, which re-imports as string. */
37
+ NONFINITE_AS_TEXT: "W_PAJEK_NONFINITE_AS_TEXT",
38
+ /** A mutual pair; written as one undirected edge, the mark lost. */
39
+ MUTUAL_AS_UNDIRECTED: LOSS.MUTUAL_AS_UNDIRECTED,
40
+ /** A start / end / timestamp role column; Pajek intervals are written from the spells role only. */
41
+ TEMPORAL_DROPPED: LOSS.TEMPORAL,
42
+ /** A position column with a stride other than 2 or 3. */
43
+ POSITION_STRIDE: "W_PAJEK_POSITION_STRIDE",
44
+ /** meta.extra.pajek.firstMode is not a count within 0..N; the two-mode header is not written. */
45
+ FIRST_MODE_DROPPED: "W_PAJEK_FIRST_MODE_DROPPED",
46
+ /** A role column Pajek has no slot for (kind, ...) written as a plain parameter; the role is lost. */
47
+ ROLE_DROPPED: LOSS.ROLE,
48
+ /** A `shape` column with a value outside the shape keywords is written as a parameter (a string on re-import). */
49
+ SHAPE_AS_PARAMETER: "W_PAJEK_SHAPE_AS_PARAMETER",
50
+ /** A vertex line with coordinates, a shape or parameters needs a label: the id text is written and reads back as a label. */
51
+ LABEL_GAINED: "W_PAJEK_LABEL_GAINED",
52
+ /** A role-less node column named `label` reads back with the label role (its values become the vertex labels). */
53
+ ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
54
+ /** Under sanitizeIds "mangle": an original id whose text reads back as the other type under ids "canonical". */
55
+ ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
56
+ });
57
+ /** The roles Pajek has a slot for beyond the structural, position and temporal ones. */
58
+ const SLOT_ROLES = new Set(["label"]);
59
+ /** The names the importer gives the slot columns, for the name-change notes. */
60
+ const ROLE_NAMES = Object.freeze({ label: "label" });
61
+ /** The roles the exporter handles structurally rather than as parameters. */
62
+ const STRUCTURAL_ROLES = new Set([
63
+ "directed",
64
+ "pair",
65
+ "mutual",
66
+ "weight",
67
+ "timeText",
68
+ "originalId",
69
+ ]);
70
+ /** The roles the generic checker reports for a format without viz, hierarchy or open-interval support. */
71
+ const CHECKED_ROLES = new Set([
72
+ "color",
73
+ "size",
74
+ "shape",
75
+ "thickness",
76
+ "parent",
77
+ "parents",
78
+ "open",
79
+ ]);
80
+ /** Roles the generic checker lets through under temporal "spells" that Pajek cannot write. */
81
+ const DROPPED_TEMPORAL_ROLES = new Set(["start", "end", "timestamp", "timestamps"]);
82
+ const CAPABILITIES = capabilities({
83
+ mixedDirection: true,
84
+ multiEdges: true,
85
+ selfLoops: true,
86
+ edgeIds: "none",
87
+ idCharset: "dense-1-based",
88
+ dtypes: ["f64", "i32", "bool", "string"],
89
+ temporal: "spells",
90
+ positions: true,
91
+ });
92
+ /**
93
+ * Decide the slot of every column of a table.
94
+ * @param table - the node or edge table
95
+ * @param domain - node or edge
96
+ * @param notes - the note list check() fills; null when writing
97
+ * @returns the plan, in declaration order
98
+ */
99
+ function plan(table, domain, notes) {
100
+ const planned = [];
101
+ const rows = (column) => column.length - column.nullCount;
102
+ for (const column of table) {
103
+ const { meta } = column;
104
+ const { role, name, dtype } = meta;
105
+ if (role === "label" && domain === "node") {
106
+ if (dtype !== "string" && dtype !== "dict" && notes !== null) {
107
+ notes.push(note(PAJEK_LOSS.LABEL_AS_TEXT, `node column "${name}" (label) is ${dtype}; labels are text and re-import as string`, name, rows(column)));
108
+ }
109
+ planned.push({ column, slot: "label" });
110
+ continue;
111
+ }
112
+ if (role === "position") {
113
+ if (domain === "node" && (meta.components === 2 || meta.components === 3)) {
114
+ planned.push({ column, slot: "position" });
115
+ }
116
+ else if (notes !== null && domain === "node") {
117
+ notes.push(note(PAJEK_LOSS.POSITION_STRIDE, `node column "${name}" (position) has ${meta.components} components; Pajek coordinates are x y [z]`, name, rows(column)));
118
+ }
119
+ else if (notes !== null) {
120
+ notes.push(note(LOSS.POSITIONS, `edge column "${name}" (position) cannot be written`, name, rows(column)));
121
+ }
122
+ continue;
123
+ }
124
+ if (role === "spells") {
125
+ if (dtype === "list" &&
126
+ (meta.itemDtype === "f64" || meta.itemDtype === "i32" || meta.itemDtype === "f32")) {
127
+ planned.push({ column, slot: "spells" });
128
+ continue;
129
+ }
130
+ if (notes !== null) {
131
+ notes.push(note(LOSS.SPELLS, `${domain} column "${name}" (spells) is not a list of number pairs; it cannot be written`, name, rows(column)));
132
+ }
133
+ continue;
134
+ }
135
+ if (role !== null && DROPPED_TEMPORAL_ROLES.has(role)) {
136
+ if (notes !== null) {
137
+ notes.push(note(PAJEK_LOSS.TEMPORAL_DROPPED, `${domain} column "${name}" (${role}) cannot be written; Pajek intervals come from the spells role`, name, rows(column)));
138
+ }
139
+ continue;
140
+ }
141
+ if (role !== null && (STRUCTURAL_ROLES.has(role) || CHECKED_ROLES.has(role))) {
142
+ // structural columns are folded into the topology; viz and hierarchy roles are reported by the generic checker
143
+ continue;
144
+ }
145
+ if (domain === "edge" && role === "id") {
146
+ continue;
147
+ }
148
+ if (dtype === "list" || dtype === "json") {
149
+ // reported by the generic checker (lists: false, json: false)
150
+ continue;
151
+ }
152
+ if (domain === "node" && name === SHAPE_COLUMN && (dtype === "dict" || dtype === "string")) {
153
+ if (allShapeKeywords(column)) {
154
+ planned.push({ column, slot: "shape" });
155
+ continue;
156
+ }
157
+ // a value outside the keywords: the whole column is a `shape "<text>"` parameter, which the
158
+ // importer reads back as one string column (a change for a dict column only)
159
+ if (notes !== null && dtype === "dict") {
160
+ notes.push(note(PAJEK_LOSS.SHAPE_AS_PARAMETER, `node column "${name}" holds values outside the Pajek shape keywords; it is written as a parameter and reads back as string`, name, rows(column)));
161
+ }
162
+ planned.push({ column, slot: "param" });
163
+ continue;
164
+ }
165
+ if (domain === "edge" && name === RELATION_COLUMN && (dtype === "dict" || dtype === "string")) {
166
+ planned.push({ column, slot: "relation" });
167
+ continue;
168
+ }
169
+ if (!isParameterKey(name)) {
170
+ if (notes !== null) {
171
+ notes.push(note(PAJEK_LOSS.KEY_DROPPED, `${domain} column "${name}" cannot be a Pajek parameter key; it is not written`, name, rows(column)));
172
+ }
173
+ continue;
174
+ }
175
+ planned.push({ column, slot: "param" });
176
+ }
177
+ return planned;
178
+ }
179
+ /**
180
+ * The notes about vertex labels: Pajek's grammar puts the label second, so a vertex line that
181
+ * carries coordinates, a shape, parameters or an interval needs one, and a node without a label
182
+ * value gets its id text written there (it reads back as a label); a role-less column named
183
+ * `label` reads back with the label role.
184
+ * @param snapshot - the snapshot
185
+ * @param nodePlan - the node column plan
186
+ * @param notes - where to record
187
+ */
188
+ function labelNotes(snapshot, nodePlan, notes) {
189
+ let labels = null;
190
+ let carried = 0;
191
+ for (const { column, slot } of nodePlan) {
192
+ if (slot === "label") {
193
+ labels = column;
194
+ }
195
+ else if (slot !== "skip" && slot !== "relation") {
196
+ carried++;
197
+ }
198
+ }
199
+ const plain = [...snapshot.nodes].find((c) => c.meta.role === null && c.meta.name === LABEL_COLUMN);
200
+ if (plain !== undefined) {
201
+ notes.push(note(PAJEK_LOSS.ROLE_ASSUMED, `node column "${LABEL_COLUMN}" has no role; written as a parameter, it reads back as the vertex label (role label)`, LABEL_COLUMN, plain.length - plain.nullCount));
202
+ }
203
+ if (carried === 0) {
204
+ return;
205
+ }
206
+ let gained = 0;
207
+ for (let i = 0; i < snapshot.nodeCount; i++) {
208
+ if (labels === null || !labels.isSet(i)) {
209
+ gained++;
210
+ }
211
+ }
212
+ if (gained > 0) {
213
+ notes.push(note(PAJEK_LOSS.LABEL_GAINED, `${gained} vertex line(s) carry coordinates, a shape or parameters and need a label; the id text is written there and reads back as a label`, labels === null ? LABEL_COLUMN : labels.meta.name, gained));
214
+ }
215
+ }
216
+ /**
217
+ * Whether every set value of a shape column is a Pajek shape keyword.
218
+ * @param column - the shape column (string or dict)
219
+ * @returns true when the shape slot can hold the column
220
+ */
221
+ function allShapeKeywords(column) {
222
+ for (let r = 0; r < column.length; r++) {
223
+ if (column.isSet(r) && !SHAPES.has(column.value(r))) {
224
+ return false;
225
+ }
226
+ }
227
+ return true;
228
+ }
229
+ /**
230
+ * Build a frozen LossNote.
231
+ * @param code - the code
232
+ * @param message - the message
233
+ * @param column - the column, or null
234
+ * @param count - the count, or null
235
+ * @returns the note
236
+ */
237
+ function note(code, message, column = null, count = null) {
238
+ return Object.freeze({ code, message, column, count });
239
+ }
240
+ /**
241
+ * The text of one numeric cell: integers as digits, f64 with a decimal point guaranteed so the
242
+ * importer's inference keeps the dtype, f32 as the shortest fround-round-trip decimal.
243
+ * @param value - the value
244
+ * @param dtype - the column dtype
245
+ * @returns the text
246
+ */
247
+ function numberText(value, dtype) {
248
+ switch (dtype) {
249
+ case "i32":
250
+ case "u32":
251
+ case "u8":
252
+ return formatInteger(value);
253
+ case "f32":
254
+ return formatF32(value);
255
+ case "f64":
256
+ return formatDecimal(value, "f64");
257
+ default: {
258
+ const name = dtype;
259
+ throw new Error(`unknown numeric dtype ${name}`);
260
+ }
261
+ }
262
+ }
263
+ /**
264
+ * The text of a set cell as a parameter value or label, before quoting.
265
+ * @param column - the column
266
+ * @param row - the row
267
+ * @returns the text
268
+ */
269
+ function cellText(column, row) {
270
+ switch (column.dtype) {
271
+ case "f32":
272
+ case "f64":
273
+ case "i32":
274
+ case "u32":
275
+ case "u8": {
276
+ const value = column.value(row);
277
+ if (typeof value === "number") {
278
+ return numberText(value, column.dtype);
279
+ }
280
+ const parts = [];
281
+ const vector = value;
282
+ for (let k = 0; k < vector.length; k++) {
283
+ parts.push(numberText(vector[k], column.dtype));
284
+ }
285
+ return parts.join(" ");
286
+ }
287
+ case "bool":
288
+ return column.value(row) === true ? "true" : "false";
289
+ case "dict":
290
+ case "string":
291
+ return column.value(row);
292
+ case "list":
293
+ case "json":
294
+ return JSON.stringify(column.value(row));
295
+ default: {
296
+ const name = column.dtype;
297
+ throw new Error(`unknown dtype ${name}`);
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * Count the cells of a plan that cannot be written (text with a quote or line break, non-finite
303
+ * f64) and add the notes.
304
+ * @param planned - the plan
305
+ * @param domain - node or edge
306
+ * @param notes - the note list
307
+ */
308
+ function checkCells(planned, domain, notes) {
309
+ for (const { column, slot } of planned) {
310
+ if (slot === "position" || slot === "spells") {
311
+ continue;
312
+ }
313
+ const { name } = column.meta;
314
+ let badText = 0;
315
+ let nonFinite = 0;
316
+ for (let r = 0; r < column.length; r++) {
317
+ if (!column.isSet(r)) {
318
+ continue;
319
+ }
320
+ if (column.dtype === "f64" || column.dtype === "f32") {
321
+ const value = column.value(r);
322
+ if (typeof value === "number") {
323
+ if (!Number.isFinite(value)) {
324
+ nonFinite++;
325
+ }
326
+ }
327
+ else if (value !== undefined && !Array.from(value).every((v) => Number.isFinite(v))) {
328
+ nonFinite++;
329
+ }
330
+ }
331
+ else if (column.dtype === "string" || column.dtype === "dict") {
332
+ if (!isPajekLabel(column.value(r))) {
333
+ badText++;
334
+ }
335
+ }
336
+ }
337
+ if (badText > 0) {
338
+ notes.push(note(PAJEK_LOSS.TEXT, `${badText} value(s) of ${domain} column "${name}" hold a double quote or a line break; Pajek cannot write them`, name, badText));
339
+ }
340
+ if (nonFinite > 0) {
341
+ notes.push(note(PAJEK_LOSS.NONFINITE_AS_TEXT, `${nonFinite} non-finite value(s) of ${domain} column "${name}" are written as text`, name, nonFinite));
342
+ }
343
+ }
344
+ }
345
+ /**
346
+ * The first-mode count to write in `*Vertices N N1`, from meta.extra.pajek.firstMode.
347
+ * @param snapshot - the snapshot
348
+ * @returns the count, or null when absent or invalid
349
+ */
350
+ function firstModeOf(snapshot) {
351
+ const { pajek } = snapshot.meta.extra;
352
+ if (typeof pajek !== "object" || pajek === null) {
353
+ return null;
354
+ }
355
+ const value = pajek.firstMode;
356
+ if (value === undefined) {
357
+ return null;
358
+ }
359
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= snapshot.nodeCount) {
360
+ return value;
361
+ }
362
+ return NaN;
363
+ }
364
+ /**
365
+ * The label of a vertex: the label column's value when set, the id's text when the id is not
366
+ * the vertex number, otherwise null (no label needed). A label is forced when something follows
367
+ * it on the line, because Pajek's grammar puts the label second.
368
+ * @param labels - the label column, or null
369
+ * @param ids - the sanitised ids
370
+ * @param i - the node index
371
+ * @param needed - whether the line has more after the label
372
+ * @returns the label text, or null
373
+ */
374
+ function labelOf(labels, ids, i, needed) {
375
+ if (labels !== null && labels.isSet(i)) {
376
+ return cellText(labels, i);
377
+ }
378
+ if (needed || ids.isChanged(i)) {
379
+ return idText(ids.originalAt(i));
380
+ }
381
+ return null;
382
+ }
383
+ /**
384
+ * The text of an id.
385
+ * @param id - the id
386
+ * @returns String(id)
387
+ */
388
+ function idText(id) {
389
+ return typeof id === "number" ? formatF64(id) : id;
390
+ }
391
+ /**
392
+ * The spells text of a row, or null when unset or empty.
393
+ * @param column - the spells list column
394
+ * @param row - the row
395
+ * @returns the interval token or null
396
+ */
397
+ function spellsText(column, row) {
398
+ if (column.dtype !== "list" || !column.isSet(row)) {
399
+ return null;
400
+ }
401
+ const items = column.sliceOf(row);
402
+ if (items.length === 0) {
403
+ return null;
404
+ }
405
+ const pairs = [];
406
+ for (const item of items) {
407
+ const pair = item;
408
+ pairs.push([pair[0], pair[1]]);
409
+ }
410
+ return formatIntervals(pairs);
411
+ }
412
+ /**
413
+ * The coordinates text of a vertex, or null when unset.
414
+ * @param column - the position column (2 or 3 components)
415
+ * @param i - the node index
416
+ * @param writeZ - whether z is written for a 3-component column
417
+ * @returns "x y" or "x y z", or null
418
+ */
419
+ function coordinatesText(column, i, writeZ) {
420
+ if (!column.isSet(i)) {
421
+ return null;
422
+ }
423
+ const dtype = column.dtype;
424
+ const vector = column.value(i);
425
+ const x = numberText(vector[0], dtype);
426
+ const y = numberText(vector[1], dtype);
427
+ if (column.meta.components === 3 && writeZ) {
428
+ return `${x} ${y} ${numberText(vector[2], dtype)}`;
429
+ }
430
+ return `${x} ${y}`;
431
+ }
432
+ /**
433
+ * Whether a 3-component position column needs its z written: the importer recorded sourceDims 3,
434
+ * or any set row has a non-zero z (never drop data because of a 2-D hint).
435
+ * @param column - the position column
436
+ * @returns true when z is written
437
+ */
438
+ function needsZ(column) {
439
+ if (column.meta.components !== 3) {
440
+ return false;
441
+ }
442
+ if (column.meta.extra.sourceDims !== 2) {
443
+ return true;
444
+ }
445
+ for (let i = 0; i < column.length; i++) {
446
+ if (column.isSet(i)) {
447
+ const vector = column.value(i);
448
+ if (vector[2] !== 0) {
449
+ return true;
450
+ }
451
+ }
452
+ }
453
+ return false;
454
+ }
455
+ /**
456
+ * The text parts of a Pajek document, one per line.
457
+ * @param snapshot - the snapshot
458
+ * @param options - the resolved format options
459
+ * @param options.networkHeader - whether the `*Network` line is written
460
+ * @param options.mangle - whether the original ids are written as `graphty_originalId` parameters
461
+ * @yields lines with their terminator
462
+ * @returns nothing
463
+ */
464
+ function* writeParts(snapshot, options) {
465
+ const ids = sanitizeIds(snapshot, CAPABILITIES.idCharset, options.mangle ? "mangle" : "error");
466
+ const nodePlan = withoutReservedKey(plan(snapshot.nodes, "node", null), options.mangle);
467
+ const edgePlan = plan(snapshot.edges, "edge", null);
468
+ // fail before writing anything when a text cannot be written (the check() E_ note)
469
+ const preflight = [];
470
+ checkCells(nodePlan, "node", preflight);
471
+ checkCells(edgePlan, "edge", preflight);
472
+ checkIdTexts(snapshot.nodeCount, ids, options.mangle, preflight);
473
+ for (const n of preflight) {
474
+ if (n.code === PAJEK_LOSS.TEXT) {
475
+ throw new GraphFormatError("E_UNSUPPORTED", n.message, {
476
+ reason: "pajek label",
477
+ column: n.column,
478
+ count: n.count,
479
+ });
480
+ }
481
+ }
482
+ if (options.networkHeader && snapshot.meta.name !== null) {
483
+ yield `*Network ${snapshot.meta.name}\n`;
484
+ }
485
+ const firstMode = firstModeOf(snapshot);
486
+ yield firstMode !== null && !Number.isNaN(firstMode)
487
+ ? `*Vertices ${snapshot.nodeCount} ${firstMode}\n`
488
+ : `*Vertices ${snapshot.nodeCount}\n`;
489
+ yield* writeVertices(snapshot, ids, nodePlan, options.mangle);
490
+ yield* writeLines(snapshot, edgePlan);
491
+ }
492
+ /**
493
+ * The node plan without a user column named like the exporter's originalId key, which is
494
+ * reserved under "mangle" (check() reports it as not written).
495
+ * @param nodePlan - the node column plan
496
+ * @param mangle - whether the original ids are written as parameters
497
+ * @returns the plan to write
498
+ */
499
+ function withoutReservedKey(nodePlan, mangle) {
500
+ if (!mangle) {
501
+ return nodePlan;
502
+ }
503
+ return nodePlan.filter((p) => !(p.slot === "param" && p.column.meta.name === ORIGINAL_ID_KEY));
504
+ }
505
+ /**
506
+ * The notes about the id texts a vertex line carries: an id written as a label or, under
507
+ * "mangle", as the `graphty_originalId` parameter must be a Pajek label (E_PAJEK_TEXT otherwise),
508
+ * and under "mangle" an original id whose text reads back as the other type under ids
509
+ * "canonical" is reported (the importer coerces the parameter text like every id cell).
510
+ * @param nodeCount - the node count
511
+ * @param ids - the sanitised ids
512
+ * @param mangle - whether the original ids are written as parameters
513
+ * @param notes - where to record
514
+ */
515
+ function checkIdTexts(nodeCount, ids, mangle, notes) {
516
+ let badText = 0;
517
+ let typeChanged = 0;
518
+ for (let i = 0; i < nodeCount; i++) {
519
+ if (!ids.isChanged(i)) {
520
+ continue;
521
+ }
522
+ const original = ids.originalAt(i);
523
+ const text = idText(original);
524
+ if (!isPajekLabel(text)) {
525
+ badText++;
526
+ }
527
+ else if (mangle && typeof canonicalId(text) !== typeof original) {
528
+ typeChanged++;
529
+ }
530
+ }
531
+ if (badText > 0) {
532
+ notes.push(note(PAJEK_LOSS.TEXT, `${badText} node id(s) hold a double quote or a line break; Pajek cannot write them as labels or parameters`, null, badText));
533
+ }
534
+ if (typeChanged > 0) {
535
+ notes.push(note(PAJEK_LOSS.ID_TEXT_TYPE, `${typeChanged} original id(s) read back from ${ORIGINAL_ID_KEY} as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`, null, typeChanged));
536
+ }
537
+ }
538
+ /**
539
+ * The vertex lines: number, label, coordinates, shape, parameters and interval.
540
+ * @param snapshot - the snapshot
541
+ * @param ids - the sanitised ids
542
+ * @param nodePlan - the node column plan
543
+ * @param mangle - whether a renumbered vertex carries its original id as a parameter
544
+ * @yields one line per vertex
545
+ * @returns nothing
546
+ */
547
+ function* writeVertices(snapshot, ids, nodePlan, mangle) {
548
+ let labels = null;
549
+ let position = null;
550
+ let shape = null;
551
+ let nodeSpells = null;
552
+ const nodeParams = [];
553
+ for (const { column, slot } of nodePlan) {
554
+ switch (slot) {
555
+ case "label":
556
+ labels = column;
557
+ break;
558
+ case "position":
559
+ position = column;
560
+ break;
561
+ case "shape":
562
+ shape = column;
563
+ break;
564
+ case "spells":
565
+ nodeSpells = column;
566
+ break;
567
+ case "param":
568
+ nodeParams.push(column);
569
+ break;
570
+ case "relation":
571
+ case "skip":
572
+ break;
573
+ default: {
574
+ const name = slot;
575
+ throw new Error(`unknown slot ${name}`);
576
+ }
577
+ }
578
+ }
579
+ const writeZ = position !== null && needsZ(position);
580
+ for (let i = 0; i < snapshot.nodeCount; i++) {
581
+ const parts = [];
582
+ if (position !== null) {
583
+ const coordinates = coordinatesText(position, i, writeZ);
584
+ if (coordinates !== null) {
585
+ parts.push(coordinates);
586
+ }
587
+ }
588
+ if (shape !== null && shape.isSet(i)) {
589
+ const keyword = cellText(shape, i);
590
+ if (SHAPES.has(keyword)) {
591
+ parts.push(keyword);
592
+ }
593
+ else {
594
+ parts.push(`${SHAPE_COLUMN} ${quotePajekLabel(keyword)}`);
595
+ }
596
+ }
597
+ for (const column of nodeParams) {
598
+ if (column.isSet(i)) {
599
+ parts.push(`${column.meta.name} ${quotePajekLabel(cellText(column, i))}`);
600
+ }
601
+ }
602
+ if (mangle && ids.isChanged(i)) {
603
+ parts.push(`${ORIGINAL_ID_KEY} ${quotePajekLabel(idText(ids.originalAt(i)))}`);
604
+ }
605
+ if (nodeSpells !== null) {
606
+ const spells = spellsText(nodeSpells, i);
607
+ if (spells !== null) {
608
+ parts.push(spells);
609
+ }
610
+ }
611
+ const label = labelOf(labels, ids, i, parts.length > 0);
612
+ let line = String(i + 1);
613
+ if (label !== null) {
614
+ line += ` ${quotePajekLabel(label)}`;
615
+ }
616
+ if (parts.length > 0) {
617
+ line += ` ${parts.join(" ")}`;
618
+ }
619
+ yield `${line}\n`;
620
+ }
621
+ }
622
+ /**
623
+ * The line sections: runs of `*Arcs` / `*Edges` (with a relation number and name when the edge
624
+ * has one), one line per written edge with its weight, parameters and interval.
625
+ * @param snapshot - the snapshot
626
+ * @param edgePlan - the edge column plan
627
+ * @yields one line per section header and edge
628
+ * @returns nothing
629
+ */
630
+ function* writeLines(snapshot, edgePlan) {
631
+ const folding = pairFolding(snapshot, { foldMutual: true });
632
+ const weights = explicitWeights(snapshot);
633
+ const { src, dst } = snapshot.edgeList();
634
+ let relation = null;
635
+ let edgeSpells = null;
636
+ const edgeParams = [];
637
+ for (const { column, slot } of edgePlan) {
638
+ switch (slot) {
639
+ case "relation":
640
+ relation = column;
641
+ break;
642
+ case "spells":
643
+ edgeSpells = column;
644
+ break;
645
+ case "param":
646
+ edgeParams.push(column);
647
+ break;
648
+ case "label":
649
+ case "position":
650
+ case "shape":
651
+ case "skip":
652
+ break;
653
+ default: {
654
+ const name = slot;
655
+ throw new Error(`unknown slot ${name}`);
656
+ }
657
+ }
658
+ }
659
+ const relationNumbers = new Map();
660
+ let currentKind = null;
661
+ let currentRelation = null;
662
+ let sectionOpen = false;
663
+ for (let e = 0; e < snapshot.edgeCount; e++) {
664
+ if (folding.folded(e)) {
665
+ continue;
666
+ }
667
+ // a mutual pair (design section 3.6): both directions, written once as an undirected edge
668
+ const directed = snapshot.directed && folding.sourceDirected(e) && !folding.isMutual(e);
669
+ const kind = directed ? "arcs" : "edges";
670
+ const relationName = relation !== null && relation.isSet(e) ? cellText(relation, e) : null;
671
+ if (!sectionOpen || kind !== currentKind || relationName !== currentRelation) {
672
+ currentKind = kind;
673
+ currentRelation = relationName;
674
+ sectionOpen = true;
675
+ const keyword = kind === "arcs" ? "*Arcs" : "*Edges";
676
+ if (relationName === null) {
677
+ yield `${keyword}\n`;
678
+ }
679
+ else {
680
+ let number = relationNumbers.get(relationName);
681
+ if (number === undefined) {
682
+ number = relationNumbers.size + 1;
683
+ relationNumbers.set(relationName, number);
684
+ }
685
+ yield `${keyword} :${number} ${quotePajekLabel(relationName)}\n`;
686
+ }
687
+ }
688
+ let line = `${src[e] + 1} ${dst[e] + 1}`;
689
+ const weight = weights.text(e);
690
+ if (weight !== null) {
691
+ line += ` ${weight}`;
692
+ }
693
+ for (const column of edgeParams) {
694
+ if (column.isSet(e)) {
695
+ line += ` ${column.meta.name} ${quotePajekLabel(cellText(column, e))}`;
696
+ }
697
+ }
698
+ if (edgeSpells !== null) {
699
+ const spells = spellsText(edgeSpells, e);
700
+ if (spells !== null) {
701
+ line += ` ${spells}`;
702
+ }
703
+ }
704
+ yield `${line}\n`;
705
+ }
706
+ if (!sectionOpen) {
707
+ yield snapshot.directed ? "*Arcs\n" : "*Edges\n";
708
+ }
709
+ }
710
+ /**
711
+ * Resolve the Pajek export options.
712
+ * @param options - the caller's options
713
+ * @returns the resolved format options; the common options are checked by resolveExportOptions
714
+ */
715
+ function resolvePajekOptions(options) {
716
+ const common = resolveExportOptions(options);
717
+ const value = options?.networkHeader;
718
+ if (value !== undefined && typeof value !== "boolean") {
719
+ throw new GraphFormatError("E_UNSUPPORTED", "option networkHeader: not a boolean", {
720
+ option: "networkHeader",
721
+ found: typeof value,
722
+ });
723
+ }
724
+ return { networkHeader: value ?? false, mangle: common.sanitizeIds === "mangle" };
725
+ }
726
+ /** The Pajek NET exporter. */
727
+ export const pajekExporter = Object.freeze({
728
+ format: "pajek",
729
+ capabilities: CAPABILITIES,
730
+ /**
731
+ * Pre-flight: the generic capability gaps plus the Pajek-specific ones.
732
+ * @param snapshot - the snapshot
733
+ * @param options - export options
734
+ * @returns the loss notes, empty when the export is exact
735
+ */
736
+ check(snapshot, options) {
737
+ const resolved = resolveExportOptions(options);
738
+ const { mangle } = resolvePajekOptions(options);
739
+ const notes = [];
740
+ const planned = plan(snapshot.nodes, "node", notes);
741
+ const nodePlan = withoutReservedKey(planned, mangle);
742
+ const edgePlan = plan(snapshot.edges, "edge", notes);
743
+ const ids = sanitizeIds(snapshot, CAPABILITIES.idCharset, mangle ? "mangle" : "error");
744
+ checkIdTexts(snapshot.nodeCount, ids, mangle, notes);
745
+ if (nodePlan.length !== planned.length) {
746
+ const reserved = planned.find((p) => p.slot === "param" && p.column.meta.name === ORIGINAL_ID_KEY);
747
+ if (reserved !== undefined) {
748
+ notes.push(note(PAJEK_LOSS.KEY_DROPPED, `node column "${ORIGINAL_ID_KEY}" is the parameter the exporter writes the original ids under (sanitizeIds "mangle"); it is not written`, ORIGINAL_ID_KEY, reserved.column.length - reserved.column.nullCount));
749
+ }
750
+ }
751
+ // columns this exporter handles itself although the generic table says otherwise: the spells
752
+ // list (written as an interval token, or reported above with the spells code), the shape
753
+ // keyword and the relation name (dict columns written as keywords)
754
+ const owned = new Set();
755
+ for (const { column, slot } of [...nodePlan, ...edgePlan]) {
756
+ if (slot === "shape" || slot === "relation") {
757
+ owned.add(`${column.meta.domain}:${column.meta.name}`);
758
+ }
759
+ else if (slot === "param" && column.meta.domain === "node" && column.meta.name === SHAPE_COLUMN) {
760
+ // the shape-as-parameter note already says the column reads back as string
761
+ owned.add(`${column.meta.domain}:${column.meta.name}`);
762
+ }
763
+ }
764
+ for (const table of [snapshot.nodes, snapshot.edges]) {
765
+ for (const column of table) {
766
+ if (column.meta.role === "spells") {
767
+ owned.add(`${column.meta.domain}:${column.meta.name}`);
768
+ }
769
+ }
770
+ }
771
+ for (const n of checkCapabilities(snapshot, CAPABILITIES, resolved, {
772
+ roles: SLOT_ROLES,
773
+ roleNames: ROLE_NAMES,
774
+ })) {
775
+ if (n.column !== null && (n.code === LOSS.LIST || n.code === LOSS.DTYPE)) {
776
+ const domain = n.message.startsWith("node column") ? "node" : "edge";
777
+ if (owned.has(`${domain}:${n.column}`)) {
778
+ continue;
779
+ }
780
+ }
781
+ if (n.code === LOSS.COLUMN_NAME_CHANGED && n.message.startsWith("edge column")) {
782
+ // the label slot is a vertex slot; an edge label is a parameter under its own name
783
+ continue;
784
+ }
785
+ notes.push(n);
786
+ }
787
+ labelNotes(snapshot, nodePlan, notes);
788
+ for (const column of snapshot.edges) {
789
+ if (column.meta.role === "label") {
790
+ notes.push(note(LOSS.ROLE, `edge column "${column.meta.name}" (label) is written as a plain parameter; Pajek labels vertices only and the role is lost`, column.meta.name, column.length - column.nullCount));
791
+ }
792
+ }
793
+ checkCells(nodePlan, "node", notes);
794
+ checkCells(edgePlan, "edge", notes);
795
+ const folding = pairFolding(snapshot, { foldMutual: true });
796
+ if (folding.mutualCount > 0) {
797
+ notes.push(note(PAJEK_LOSS.MUTUAL_AS_UNDIRECTED, `${folding.mutualCount} mutual pair(s) are written as undirected edges; the mutual mark is lost`, snapshot.edges.byRole("mutual")?.meta.name ?? null, folding.mutualCount));
798
+ }
799
+ if (Number.isNaN(firstModeOf(snapshot))) {
800
+ notes.push(note(PAJEK_LOSS.FIRST_MODE_DROPPED, "meta.extra.pajek.firstMode is not a count within 0..nodeCount; the two-mode header is not written"));
801
+ }
802
+ return notes;
803
+ },
804
+ /**
805
+ * Write the snapshot as UTF-8 chunks.
806
+ * @param snapshot - the snapshot
807
+ * @param options - export options
808
+ * @returns the chunks
809
+ */
810
+ export(snapshot, options) {
811
+ const resolved = resolvePajekOptions(options);
812
+ return encodeChunks(writeParts(snapshot, resolved));
813
+ },
814
+ /**
815
+ * Write the snapshot as one string.
816
+ * @param snapshot - the snapshot
817
+ * @param options - export options
818
+ * @returns the document
819
+ */
820
+ exportToString(snapshot, options) {
821
+ const resolved = resolvePajekOptions(options);
822
+ return joinText(writeParts(snapshot, resolved));
823
+ },
824
+ });
825
+ //# sourceMappingURL=exporter.js.map