@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,836 @@
1
+ /**
2
+ * The DOT / Graphviz exporter (design section 8.5; research note 07 section 9). Writes one
3
+ * `[strict] graph | digraph [name] { ... }` with the graph attributes first, every node in index
4
+ * order (a node statement carrying its set cells as attributes, or a `subgraph` block for a cluster
5
+ * container node with its members nested inside), then every logical edge in index order with its
6
+ * explicit weight, `key` (the edge id role), ports and attributes.
7
+ *
8
+ * What the format keeps and what check() reports: any id and any text (DOT quotes everything
9
+ * except a text ending in a backslash, which the lexer cannot read back); bool / i32 / f64 cells
10
+ * (f64 written with a decimal point so the dtype survives re-import; other numeric dtypes are
11
+ * written and read back inferred); containment through the parent role (clusters); graph
12
+ * attributes; node positions as `pos="x,y"`. Mixed direction is folded per onMixedDirection
13
+ * (DOT has none); lists, json, defaults, options, temporal and visual roles cannot be written.
14
+ */
15
+ import { GraphFormatError } from "@graphty/graph-format";
16
+ import { childrenCsr } from "../../children.js";
17
+ import { pairFolding } from "../../common/direction.js";
18
+ import { isWritableDotText, quoteDotId } from "../../common/escape.js";
19
+ import { capabilities, checkCapabilities, countMixedEdges, LOSS, sanitizeIds } from "../../common/export.js";
20
+ import { formatDecimal, formatF32, formatF64, formatInteger } from "../../common/format.js";
21
+ import { canonicalId } from "../../common/ids.js";
22
+ import { resolveExportOptions } from "../../common/options.js";
23
+ import { inferTextDtype } from "../../common/text.js";
24
+ import { explicitWeights } from "../../common/weights.js";
25
+ import { encodeChunks, joinText } from "../../common/writer.js";
26
+ import { CLUSTER_COLUMN, DOT_FORMAT, DOT_META_KEY, KEY_ATTRIBUTE, LABEL_ATTRIBUTE, PARENT_COLUMN, POS_ATTRIBUTE, SOURCE_PORT_COLUMN, TARGET_PORT_COLUMN, } from "./names.js";
27
+ /** The LossNote codes of the DOT exporter; the shared ones are LOSS's. */
28
+ export const DOT_LOSS = Object.freeze({
29
+ /** An id, name or text with a backslash before a quote or at its end cannot be written as a DOT quoted string; export() throws. */
30
+ TRAILING_BACKSLASH: "E_DOT_TRAILING_BACKSLASH",
31
+ /** A non-finite f32 / f64 cell has no numeric DOT spelling and reads back as text. */
32
+ NON_FINITE: "W_DOT_NON_FINITE",
33
+ /** Text cells that look like numbers or booleans read back as such (DOT attribute values are untyped). */
34
+ TEXT_INFERRED: LOSS.TEXT_INFERRED,
35
+ /** A plain column named like an attribute the exporter writes for a role (weight, key, pos) is not written. */
36
+ ATTRIBUTE_CLASH: "W_DOT_ATTRIBUTE_CLASH",
37
+ /** A mutual pair is written as two directed edges. */
38
+ MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
39
+ /** A parents (multi-parent) column cannot be written; DOT clusters nest. */
40
+ PARENTS_DROPPED: LOSS.PARENTS,
41
+ /** A position column that is not a node column of 2 or 3 components is not written. */
42
+ POSITION_SHAPE: "W_DOT_POSITION_SHAPE",
43
+ /** An id whose text reads back as the other type under ids: "canonical" (1.5 as text, "1" as 1). */
44
+ ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
45
+ /** A declared column whose every row is unset is not written (DOT writes cells, never declarations). */
46
+ EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
47
+ /** A role-less column named `label` reads back with the label role. */
48
+ ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
49
+ });
50
+ /** The roles DOT has a slot for (the label attribute, key, ports, clusters); every other role is reported. */
51
+ const KEPT_ROLES = new Set(["label", "id", "sourcePort", "targetPort", "parent"]);
52
+ /** The column name the importer gives each mapped role on re-import. */
53
+ const ROLE_NAMES = Object.freeze({
54
+ label: LABEL_ATTRIBUTE,
55
+ id: KEY_ATTRIBUTE,
56
+ sourcePort: SOURCE_PORT_COLUMN,
57
+ targetPort: TARGET_PORT_COLUMN,
58
+ parent: PARENT_COLUMN,
59
+ position: POS_ATTRIBUTE,
60
+ });
61
+ const CAPABILITIES = capabilities({
62
+ mixedDirection: false,
63
+ multiEdges: true,
64
+ selfLoops: true,
65
+ edgeIds: "optional",
66
+ idCharset: "any",
67
+ dtypes: ["bool", "i32", "f64", "string"],
68
+ components: false,
69
+ lists: false,
70
+ json: false,
71
+ defaults: false,
72
+ options: false,
73
+ hierarchy: true,
74
+ temporal: "none",
75
+ graphAttributes: true,
76
+ positions: true,
77
+ viz: false,
78
+ });
79
+ const DEFAULT_INDENT = " ";
80
+ const CLUSTER_PREFIX = "cluster";
81
+ const COMPASS_POINTS = new Set(["n", "ne", "e", "se", "s", "sw", "w", "nw", "c", "_"]);
82
+ /** Roles never written as attributes, per domain. */
83
+ const SKIPPED_NODE_ROLES = new Set([
84
+ "parent",
85
+ "parents",
86
+ "position",
87
+ "color",
88
+ "size",
89
+ "shape",
90
+ "thickness",
91
+ "start",
92
+ "end",
93
+ "timestamp",
94
+ "timestamps",
95
+ "spells",
96
+ "open",
97
+ "timeText",
98
+ "directed",
99
+ "pair",
100
+ "mutual",
101
+ "weight",
102
+ ]);
103
+ const SKIPPED_EDGE_ROLES = new Set([
104
+ "id",
105
+ "weight",
106
+ "directed",
107
+ "pair",
108
+ "mutual",
109
+ "sourcePort",
110
+ "targetPort",
111
+ "position",
112
+ "color",
113
+ "size",
114
+ "shape",
115
+ "thickness",
116
+ "start",
117
+ "end",
118
+ "timestamp",
119
+ "timestamps",
120
+ "spells",
121
+ "open",
122
+ "timeText",
123
+ "parent",
124
+ "parents",
125
+ ]);
126
+ const SKIPPED_GRAPH_ROLES = new Set(["position", "color", "size", "shape", "thickness"]);
127
+ /** Text columns whose values are read back as text regardless of their spelling (no TEXT_INFERRED note). */
128
+ const TEXT_ROLES = new Set(["label", "id", "sourcePort", "targetPort"]);
129
+ /**
130
+ * The exporter plugin for DOT / Graphviz text (design section 12.4).
131
+ */
132
+ export const dotExporter = Object.freeze({
133
+ format: DOT_FORMAT,
134
+ capabilities: CAPABILITIES,
135
+ /**
136
+ * Pre-flight: every loss the DOT text would incur, without writing anything.
137
+ * @param snapshot - the snapshot to check
138
+ * @param options - format-specific and common options
139
+ * @returns the notes, empty when the export is exact
140
+ */
141
+ check(snapshot, options) {
142
+ const resolved = resolveExportOptions(options);
143
+ const notes = checkCapabilities(snapshot, CAPABILITIES, resolved, {
144
+ positionDtype: "f32",
145
+ roles: KEPT_ROLES,
146
+ roleNames: ROLE_NAMES,
147
+ });
148
+ const plan = new ExportPlan(snapshot, resolved, options);
149
+ return [...notes, ...plan.notes()];
150
+ },
151
+ /**
152
+ * Write the snapshot as UTF-8 chunks.
153
+ * @param snapshot - the snapshot to write
154
+ * @param options - format-specific and common options
155
+ * @returns the encoded chunks
156
+ */
157
+ export(snapshot, options) {
158
+ return encodeChunks(writeDot(snapshot, options));
159
+ },
160
+ /**
161
+ * Write the snapshot as one string.
162
+ * @param snapshot - the snapshot to write
163
+ * @param options - format-specific and common options
164
+ * @returns the whole document
165
+ */
166
+ exportToString(snapshot, options) {
167
+ return joinText(writeDot(snapshot, options));
168
+ },
169
+ });
170
+ /**
171
+ * The text parts of a DOT document.
172
+ * @param snapshot - the snapshot
173
+ * @param options - the caller's options
174
+ * @yields one statement (or header / footer) at a time
175
+ * @returns nothing
176
+ */
177
+ function* writeDot(snapshot, options) {
178
+ const resolved = resolveExportOptions(options);
179
+ const plan = new ExportPlan(snapshot, resolved, options);
180
+ plan.refuse();
181
+ yield* plan.write();
182
+ }
183
+ /**
184
+ * Whether a text ends in a backslash, which a DOT quoted string cannot carry (the lexer would read
185
+ * the backslash and the closing quote as an escaped quote; common/escape.ts isWritableDotText).
186
+ * @param text - the text
187
+ * @returns true when unwritable
188
+ */
189
+ function endsWithBackslash(text) {
190
+ return !isWritableDotText(text);
191
+ }
192
+ /**
193
+ * Whether a text is an HTML string (balanced angle brackets around the whole text), which DOT
194
+ * writes bare as `<...>`.
195
+ * @param text - the text
196
+ * @returns true for an HTML string
197
+ */
198
+ function isHtmlString(text) {
199
+ if (text.length < 2 || !text.startsWith("<") || !text.endsWith(">")) {
200
+ return false;
201
+ }
202
+ let depth = 0;
203
+ for (let i = 0; i < text.length; i++) {
204
+ const c = text.charCodeAt(i);
205
+ if (c === 0x3c) {
206
+ depth++;
207
+ }
208
+ else if (c === 0x3e) {
209
+ depth--;
210
+ if (depth === 0 && i !== text.length - 1) {
211
+ return false;
212
+ }
213
+ }
214
+ }
215
+ return depth === 0;
216
+ }
217
+ /**
218
+ * Write a text as a DOT ID: an HTML string bare, everything else through quoteDotId.
219
+ * @param text - the text
220
+ * @returns the DOT ID
221
+ */
222
+ function writeId(text) {
223
+ return isHtmlString(text) ? text : quoteDotId(text);
224
+ }
225
+ /**
226
+ * Write a port text (`f0`, `f0:ne`, `ne`) as its DOT spelling after the endpoint.
227
+ * @param port - the port text
228
+ * @returns `:port[:compass]`
229
+ */
230
+ function writePort(port) {
231
+ const colon = port.lastIndexOf(":");
232
+ if (colon > 0 && COMPASS_POINTS.has(port.slice(colon + 1))) {
233
+ return `:${quoteDotId(port.slice(0, colon))}:${port.slice(colon + 1)}`;
234
+ }
235
+ return `:${quoteDotId(port)}`;
236
+ }
237
+ /**
238
+ * Whether a column can be written as attribute text: every scalar dtype; lists and json cannot.
239
+ * @param column - the column
240
+ * @returns true when writable
241
+ */
242
+ function isTextualDtype(column) {
243
+ return column.dtype !== "list" && column.dtype !== "json";
244
+ }
245
+ /**
246
+ * Everything one export needs to know about a snapshot, computed once and shared by check() and
247
+ * write(): the output direction, the columns written per table, the cluster structure, and the
248
+ * format-specific loss notes.
249
+ */
250
+ class ExportPlan {
251
+ /**
252
+ * Plan one export.
253
+ * @param snapshot - the snapshot
254
+ * @param resolved - the resolved common options
255
+ * @param options - the caller's options (format-specific fields read here)
256
+ */
257
+ constructor(snapshot, resolved, options) {
258
+ this.extraNotes = [];
259
+ this.snapshot = snapshot;
260
+ this.resolved = resolved;
261
+ this.indent = options?.indent ?? DEFAULT_INDENT;
262
+ this.name = options?.name === undefined ? snapshot.meta.name : options.name;
263
+ this.strict = options?.strict ?? isStrictMeta(snapshot);
264
+ this.mixed = countMixedEdges(snapshot);
265
+ if (this.mixed > 0 && resolved.onMixedDirection !== "error") {
266
+ this.directed = resolved.onMixedDirection === "directed";
267
+ }
268
+ else {
269
+ this.directed = snapshot.directed;
270
+ }
271
+ const { nodes, edges, graph } = snapshot;
272
+ this.position = nodes.byRole("position");
273
+ this.positionDims = positionDims(this.position);
274
+ this.parent = nodes.byRole("parent");
275
+ const cluster = nodes.get(CLUSTER_COLUMN);
276
+ this.cluster = cluster !== null && cluster.dtype === "bool" ? cluster : null;
277
+ this.edgeId = edges.byRole("id");
278
+ this.weights = explicitWeights(snapshot);
279
+ this.folding = pairFolding(snapshot);
280
+ this.sourcePort = edges.byRole("sourcePort");
281
+ this.targetPort = edges.byRole("targetPort");
282
+ this.nodeColumns = [...nodes].filter((c) => this.writesNodeColumn(c));
283
+ this.edgeColumns = [...edges].filter((c) => this.writesEdgeColumn(c));
284
+ this.graphColumns = [...graph].filter((c) => isTextualDtype(c) && (c.meta.role === null || !SKIPPED_GRAPH_ROLES.has(c.meta.role)));
285
+ this.children = childrenCsr(snapshot, {
286
+ column: this.parent !== null && this.parent.dtype === "u32" ? this.parent : null,
287
+ });
288
+ }
289
+ /**
290
+ * Whether a node column is written as node attributes.
291
+ * @param column - the column
292
+ * @returns true when written
293
+ */
294
+ writesNodeColumn(column) {
295
+ const { name, role } = column.meta;
296
+ if (!isTextualDtype(column) || name === CLUSTER_COLUMN) {
297
+ return false;
298
+ }
299
+ if (role !== null && SKIPPED_NODE_ROLES.has(role)) {
300
+ return false;
301
+ }
302
+ if (name === POS_ATTRIBUTE && role !== "position") {
303
+ this.clash("node", name, "the position role is written as pos");
304
+ return false;
305
+ }
306
+ return true;
307
+ }
308
+ /**
309
+ * Whether an edge column is written as edge attributes.
310
+ * @param column - the column
311
+ * @returns true when written
312
+ */
313
+ writesEdgeColumn(column) {
314
+ const { name, role } = column.meta;
315
+ if (!isTextualDtype(column)) {
316
+ return false;
317
+ }
318
+ if (role !== null && SKIPPED_EDGE_ROLES.has(role)) {
319
+ return false;
320
+ }
321
+ if (name === KEY_ATTRIBUTE && this.edgeId !== null) {
322
+ this.clash("edge", name, "the edge id role is written as key");
323
+ return false;
324
+ }
325
+ if (name === "weight") {
326
+ this.clash("edge", name, "the weight is written as weight");
327
+ return false;
328
+ }
329
+ return true;
330
+ }
331
+ /**
332
+ * Record a column not written because its name is an attribute the exporter writes for a role.
333
+ * @param domain - the table
334
+ * @param name - the column name
335
+ * @param reason - why
336
+ */
337
+ clash(domain, name, reason) {
338
+ this.extraNotes.push(Object.freeze({
339
+ code: DOT_LOSS.ATTRIBUTE_CLASH,
340
+ message: `${domain} column "${name}" has no role and is not written: ${reason}`,
341
+ column: name,
342
+ count: null,
343
+ }));
344
+ }
345
+ /**
346
+ * The format-specific loss notes (the generic ones come from checkCapabilities).
347
+ * @returns the notes
348
+ */
349
+ notes() {
350
+ const notes = [...this.extraNotes];
351
+ const note = (code, message, column = null, count = null) => {
352
+ notes.push(Object.freeze({ code, message, column, count }));
353
+ };
354
+ const { snapshot } = this;
355
+ const unwritable = this.countUnwritable();
356
+ if (unwritable > 0) {
357
+ note(DOT_LOSS.TRAILING_BACKSLASH, `${unwritable} id(s), name(s) or text value(s) hold a backslash before a quote or at the end, which a DOT quoted string cannot carry; export() will throw`, null, unwritable);
358
+ }
359
+ for (const [label, columns] of [
360
+ ["node", this.nodeColumns],
361
+ ["edge", this.edgeColumns],
362
+ ["graph", this.graphColumns],
363
+ ]) {
364
+ for (const column of columns) {
365
+ if (label !== "graph" && column.nullCount === column.length) {
366
+ note(DOT_LOSS.EMPTY_COLUMN_DROPPED, `${label} column "${column.meta.name}" has no set cell and is not written: DOT writes cells, never declarations`, column.meta.name, 0);
367
+ continue;
368
+ }
369
+ if (label !== "graph" &&
370
+ column.meta.role === null &&
371
+ column.meta.name === LABEL_ATTRIBUTE &&
372
+ (column.dtype === "string" || column.dtype === "dict")) {
373
+ note(DOT_LOSS.ROLE_ASSUMED, `${label} column "${column.meta.name}" reads back with the label role`, column.meta.name, column.length - column.nullCount);
374
+ }
375
+ if (column.dtype === "f32" || column.dtype === "f64") {
376
+ const bad = countNonFinite(column);
377
+ if (bad > 0) {
378
+ note(DOT_LOSS.NON_FINITE, `${label} column "${column.meta.name}" holds ${bad} non-finite value(s) with no numeric DOT spelling; they read back as text`, column.meta.name, bad);
379
+ }
380
+ }
381
+ if ((column.dtype === "string" || column.dtype === "dict") &&
382
+ (column.meta.role === null || !TEXT_ROLES.has(column.meta.role))) {
383
+ const typed = countTypedLookingText(column);
384
+ if (typed > 0) {
385
+ note(DOT_LOSS.TEXT_INFERRED, `${label} column "${column.meta.name}" holds ${typed} text value(s) that look like numbers or booleans; DOT values are untyped and they read back as such`, column.meta.name, typed);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ const mutual = this.folding.mutualCount;
391
+ if (mutual > 0) {
392
+ note(DOT_LOSS.MUTUAL_EXPANDED, `${mutual} mutual pair(s) are written as two directed edges each; the mutual mark is lost`, null, mutual);
393
+ }
394
+ const parents = snapshot.nodes.byRole("parents");
395
+ if (parents !== null) {
396
+ note(DOT_LOSS.PARENTS_DROPPED, `node column "${parents.meta.name}" (parents) cannot be written: a DOT node is in one cluster`, parents.meta.name, parents.length - parents.nullCount);
397
+ }
398
+ if (this.position !== null && this.position.meta.components !== 2 && this.position.meta.components !== 3) {
399
+ note(DOT_LOSS.POSITION_SHAPE, `node column "${this.position.meta.name}" (position) has ${this.position.meta.components} components and is not written; pos takes 2 or 3`, this.position.meta.name, this.position.length - this.position.nullCount);
400
+ }
401
+ for (const [label, table] of [
402
+ ["edge", snapshot.edges],
403
+ ["graph", snapshot.graph],
404
+ ]) {
405
+ const column = table.byRole("position");
406
+ if (column !== null) {
407
+ note(DOT_LOSS.POSITION_SHAPE, `${label} column "${column.meta.name}" (position) is not written; only node positions map to pos`, column.meta.name, column.length - column.nullCount);
408
+ }
409
+ }
410
+ const textIds = countTextIds(snapshot);
411
+ if (textIds > 0) {
412
+ note(DOT_LOSS.ID_TEXT_TYPE, `${textIds} 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, textIds);
413
+ }
414
+ return notes;
415
+ }
416
+ /**
417
+ * Throw for the conditions check() reports as errors: mixed direction under "error", and an
418
+ * unwritable id / name / text.
419
+ */
420
+ refuse() {
421
+ if (this.mixed > 0 && this.resolved.onMixedDirection === "error") {
422
+ throw new GraphFormatError("E_DIRECTED", `${this.mixed} undirected edge(s) in a directed graph; DOT has no mixed direction and onMixedDirection is "error"`, { reason: LOSS.MIXED_DIRECTION_ERROR, count: this.mixed });
423
+ }
424
+ const first = this.firstUnwritable();
425
+ if (first !== null) {
426
+ throw new GraphFormatError(first.kind === "id" ? "E_INVALID_ID" : "E_COLUMN_TYPE", `${first.kind} ${JSON.stringify(first.text)} ends in a backslash, which a DOT quoted string cannot carry`, { reason: "trailing backslash", kind: first.kind, value: first.text });
427
+ }
428
+ sanitizeIds(this.snapshot, CAPABILITIES.idCharset, this.resolved.sanitizeIds);
429
+ }
430
+ /**
431
+ * How many ids, names and text values end in a backslash.
432
+ * @returns the count
433
+ */
434
+ countUnwritable() {
435
+ let count = 0;
436
+ this.forEachText((text) => {
437
+ if (endsWithBackslash(text)) {
438
+ count++;
439
+ }
440
+ });
441
+ return count;
442
+ }
443
+ /**
444
+ * The first id, name or text value ending in a backslash.
445
+ * @returns what it is and its text, or null
446
+ */
447
+ firstUnwritable() {
448
+ let found = null;
449
+ this.forEachText((text, kind) => {
450
+ if (found === null && endsWithBackslash(text)) {
451
+ found = { kind, text };
452
+ }
453
+ });
454
+ return found;
455
+ }
456
+ /**
457
+ * Visit every text the document writes as a quoted string: the graph name, node ids, column
458
+ * names and string / dict cells.
459
+ * @param visit - the visitor
460
+ */
461
+ forEachText(visit) {
462
+ const { snapshot } = this;
463
+ if (this.name !== null) {
464
+ visit(this.name, "graph name");
465
+ }
466
+ for (let i = 0; i < snapshot.nodeCount; i++) {
467
+ const id = snapshot.ids.idOf(i);
468
+ if (typeof id === "string") {
469
+ visit(id, "id");
470
+ }
471
+ }
472
+ for (const columns of [this.nodeColumns, this.edgeColumns, this.graphColumns]) {
473
+ for (const column of columns) {
474
+ visit(column.meta.name, "attribute name");
475
+ if (column.dtype === "string" || column.dtype === "dict") {
476
+ for (let r = 0; r < column.length; r++) {
477
+ if (column.isSet(r)) {
478
+ visit(column.value(r), "value");
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+ for (const column of [this.edgeId, this.sourcePort, this.targetPort]) {
485
+ if (column !== null && (column.dtype === "string" || column.dtype === "dict")) {
486
+ for (let r = 0; r < column.length; r++) {
487
+ if (column.isSet(r)) {
488
+ visit(column.value(r), "value");
489
+ }
490
+ }
491
+ }
492
+ }
493
+ }
494
+ /**
495
+ * The document, one statement per part.
496
+ * @yields the parts
497
+ * @returns nothing
498
+ */
499
+ *write() {
500
+ const { snapshot, indent } = this;
501
+ const keyword = this.directed ? "digraph" : "graph";
502
+ const name = this.name === null ? "" : ` ${quoteDotId(this.name)}`;
503
+ yield `${this.strict ? "strict " : ""}${keyword}${name} {\n`;
504
+ const graphAttributes = this.attributesOf(this.graphColumns, 0);
505
+ if (graphAttributes.length > 0) {
506
+ yield `${indent}graph [${graphAttributes.join(", ")}];\n`;
507
+ }
508
+ const emitted = new Uint8Array(snapshot.nodeCount);
509
+ for (let i = 0; i < snapshot.nodeCount; i++) {
510
+ yield* this.writeNode(i, 1, emitted);
511
+ }
512
+ yield* this.writeEdges();
513
+ yield "}\n";
514
+ }
515
+ /**
516
+ * Write one node (and, for a container, its cluster block with the members nested).
517
+ * @param i - the node index
518
+ * @param depth - the nesting depth
519
+ * @param emitted - which nodes were written already
520
+ * @yields the statements
521
+ * @returns nothing
522
+ */
523
+ *writeNode(i, depth, emitted) {
524
+ if (emitted[i] === 1) {
525
+ return;
526
+ }
527
+ emitted[i] = 1;
528
+ const pad = this.indent.repeat(depth);
529
+ const idText = this.idText(i);
530
+ const kids = this.children.childrenOf(i);
531
+ const marked = this.cluster !== null && this.cluster.isSet(i) && this.cluster.value(i) === true;
532
+ if (kids.length === 0 && !marked) {
533
+ yield `${pad}${this.nodeStatement(i, idText)};\n`;
534
+ return;
535
+ }
536
+ if (!marked) {
537
+ // a real node that is also a parent: its cells are node attributes, the cluster only groups
538
+ yield `${pad}${this.nodeStatement(i, idText)};\n`;
539
+ }
540
+ const inner = `${pad}${this.indent}`;
541
+ yield `${pad}subgraph ${writeId(idText)} {\n`;
542
+ if (!idText.startsWith(CLUSTER_PREFIX)) {
543
+ yield `${inner}cluster=true;\n`;
544
+ }
545
+ if (marked) {
546
+ const attributes = this.nodeAttributes(i);
547
+ if (attributes.length > 0) {
548
+ yield `${inner}graph [${attributes.join(", ")}];\n`;
549
+ }
550
+ }
551
+ for (const child of kids) {
552
+ if (emitted[child] === 1) {
553
+ yield `${inner}${writeId(this.idText(child))};\n`;
554
+ }
555
+ else {
556
+ yield* this.writeNode(child, depth + 1, emitted);
557
+ }
558
+ }
559
+ yield `${pad}}\n`;
560
+ }
561
+ /**
562
+ * A node statement without its terminator.
563
+ * @param i - the node index
564
+ * @param idText - the id text
565
+ * @returns `id` or `id [attrs]`
566
+ */
567
+ nodeStatement(i, idText) {
568
+ const attributes = this.nodeAttributes(i);
569
+ return attributes.length === 0 ? writeId(idText) : `${writeId(idText)} [${attributes.join(", ")}]`;
570
+ }
571
+ /**
572
+ * The attributes of a node: its set cells plus `pos` from the position column.
573
+ * @param i - the node index
574
+ * @returns the attribute texts
575
+ */
576
+ nodeAttributes(i) {
577
+ const attributes = this.attributesOf(this.nodeColumns, i);
578
+ const pos = this.positionText(i);
579
+ if (pos !== null) {
580
+ attributes.push(`${POS_ATTRIBUTE}=${quoteDotId(pos)}`);
581
+ }
582
+ return attributes;
583
+ }
584
+ /**
585
+ * The `pos` text of a node, or null when it has none.
586
+ * @param i - the node index
587
+ * @returns `x,y` or `x,y,z`
588
+ */
589
+ positionText(i) {
590
+ const column = this.position;
591
+ if (column === null || !column.isSet(i)) {
592
+ return null;
593
+ }
594
+ if (column.dtype !== "f32" && column.dtype !== "f64" && column.dtype !== "i32") {
595
+ return null;
596
+ }
597
+ const { components } = column.meta;
598
+ if (components !== 2 && components !== 3) {
599
+ return null;
600
+ }
601
+ const format = column.dtype === "f32" ? formatF32 : formatF64;
602
+ const base = i * components;
603
+ const { data } = column;
604
+ const parts = [format(data[base]), format(data[base + 1])];
605
+ if (this.positionDims === 3) {
606
+ parts.push(format(components === 3 ? data[base + 2] : 0));
607
+ }
608
+ return parts.join(",");
609
+ }
610
+ /**
611
+ * The `name=value` attributes of one row over a column list (set cells only).
612
+ * @param columns - the columns
613
+ * @param row - the row
614
+ * @returns the attribute texts
615
+ */
616
+ attributesOf(columns, row) {
617
+ const out = [];
618
+ for (const column of columns) {
619
+ if (!column.isSet(row)) {
620
+ continue;
621
+ }
622
+ const text = cellText(column, row);
623
+ if (text !== null) {
624
+ out.push(`${quoteDotId(column.meta.name)}=${writeId(text)}`);
625
+ }
626
+ }
627
+ return out;
628
+ }
629
+ /**
630
+ * Every logical edge in index order, mirror halves of expanded undirected pairs folded away.
631
+ * @yields the edge statements
632
+ * @returns nothing
633
+ */
634
+ *writeEdges() {
635
+ const { snapshot, indent } = this;
636
+ const list = snapshot.edgeList();
637
+ const op = this.directed ? "->" : "--";
638
+ for (let e = 0; e < snapshot.edgeCount; e++) {
639
+ if (this.folding.folded(e)) {
640
+ continue;
641
+ }
642
+ const attributes = this.attributesOf(this.edgeColumns, e);
643
+ const weight = this.weights.text(e);
644
+ if (weight !== null) {
645
+ attributes.unshift(`weight=${quoteDotId(weight)}`);
646
+ }
647
+ const key = this.keyText(e);
648
+ if (key !== null) {
649
+ attributes.unshift(`${KEY_ATTRIBUTE}=${writeId(key)}`);
650
+ }
651
+ const source = `${writeId(this.idText(list.src[e]))}${this.portText(this.sourcePort, e)}`;
652
+ const target = `${writeId(this.idText(list.dst[e]))}${this.portText(this.targetPort, e)}`;
653
+ const tail = attributes.length === 0 ? "" : ` [${attributes.join(", ")}]`;
654
+ yield `${indent}${source} ${op} ${target}${tail};\n`;
655
+ }
656
+ }
657
+ /**
658
+ * The edge id of an edge as `key` text, or null.
659
+ * @param e - the edge index
660
+ * @returns the text
661
+ */
662
+ keyText(e) {
663
+ const column = this.edgeId;
664
+ if (column === null || !column.isSet(e)) {
665
+ return null;
666
+ }
667
+ if (column.dtype === "f32" || column.dtype === "f64") {
668
+ return formatF64(column.data[e]);
669
+ }
670
+ return cellText(column, e);
671
+ }
672
+ /**
673
+ * The port suffix of an endpoint.
674
+ * @param column - the port column, or null
675
+ * @param e - the edge index
676
+ * @returns `:port` or ""
677
+ */
678
+ portText(column, e) {
679
+ if (column === null || !column.isSet(e)) {
680
+ return "";
681
+ }
682
+ const text = cellText(column, e);
683
+ return text === null || text.length === 0 ? "" : writePort(text);
684
+ }
685
+ /**
686
+ * The id text of a node.
687
+ * @param i - the node index
688
+ * @returns String(id)
689
+ */
690
+ idText(i) {
691
+ return idToText(this.snapshot.ids.idOf(i));
692
+ }
693
+ }
694
+ /**
695
+ * The text of a node id: the string itself, or the shortest decimal of a number.
696
+ * @param id - the id
697
+ * @returns the text
698
+ */
699
+ function idToText(id) {
700
+ return typeof id === "string" ? id : formatF64(id);
701
+ }
702
+ /**
703
+ * Whether the snapshot's meta records a strict graph.
704
+ * @param snapshot - the snapshot
705
+ * @returns true when meta.extra.dot.strict is true
706
+ */
707
+ function isStrictMeta(snapshot) {
708
+ const dot = snapshot.meta.extra[DOT_META_KEY];
709
+ return typeof dot === "object" && dot !== null && dot.strict === true;
710
+ }
711
+ /**
712
+ * The dimensions `pos` is written with: what the importer recorded in extra.sourceDims, else 2
713
+ * for a 2-component column and 3 otherwise.
714
+ * @param column - the position column, or null
715
+ * @returns 2 or 3
716
+ */
717
+ function positionDims(column) {
718
+ if (column === null) {
719
+ return 2;
720
+ }
721
+ const dims = column.meta.extra.sourceDims;
722
+ if (dims === 2 || dims === 3) {
723
+ return dims;
724
+ }
725
+ return column.meta.components === 2 ? 2 : 3;
726
+ }
727
+ /**
728
+ * The attribute text of one set cell, or null for a dtype the format cannot write.
729
+ * @param column - the column
730
+ * @param row - the row
731
+ * @returns the text
732
+ */
733
+ function cellText(column, row) {
734
+ switch (column.dtype) {
735
+ case "bool":
736
+ return column.value(row) === true ? "true" : "false";
737
+ case "i32":
738
+ case "u32":
739
+ case "u8":
740
+ return numericText(column, row, formatInteger);
741
+ case "f32":
742
+ return numericText(column, row, (v) => formatDecimal(v, "f32"));
743
+ case "f64":
744
+ return numericText(column, row, (v) => formatDecimal(v, "f64"));
745
+ case "dict":
746
+ case "string":
747
+ return column.value(row) ?? "";
748
+ case "list":
749
+ case "json":
750
+ return null;
751
+ default: {
752
+ const name = column.dtype;
753
+ throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
754
+ }
755
+ }
756
+ }
757
+ /**
758
+ * The text of a numeric cell: one number, or the `components` numbers joined by commas.
759
+ * @param column - a numeric column
760
+ * @param row - the row
761
+ * @param format - the per-number formatter
762
+ * @returns the text
763
+ */
764
+ function numericText(column, row, format) {
765
+ const { components } = column.meta;
766
+ if (components === 1) {
767
+ return format(column.data[row]);
768
+ }
769
+ const parts = [];
770
+ for (let k = 0; k < components; k++) {
771
+ parts.push(format(column.data[row * components + k]));
772
+ }
773
+ return parts.join(",");
774
+ }
775
+ /**
776
+ * How many set cells of an f32 / f64 column are non-finite.
777
+ * @param column - the column
778
+ * @returns the count
779
+ */
780
+ function countNonFinite(column) {
781
+ if (column.dtype !== "f32" && column.dtype !== "f64") {
782
+ return 0;
783
+ }
784
+ const { components } = column.meta;
785
+ let count = 0;
786
+ for (let r = 0; r < column.length; r++) {
787
+ if (!column.isSet(r)) {
788
+ continue;
789
+ }
790
+ for (let k = 0; k < components; k++) {
791
+ if (!Number.isFinite(column.data[r * components + k])) {
792
+ count++;
793
+ break;
794
+ }
795
+ }
796
+ }
797
+ return count;
798
+ }
799
+ /**
800
+ * How many set cells of a text column read back as something other than text under the 5.1 grammar.
801
+ * @param column - a string or dict column
802
+ * @returns the count
803
+ */
804
+ function countTypedLookingText(column) {
805
+ if (column.dtype !== "string" && column.dtype !== "dict") {
806
+ return 0;
807
+ }
808
+ let count = 0;
809
+ for (let r = 0; r < column.length; r++) {
810
+ if (column.isSet(r) && inferTextDtype(column.value(r) ?? "") !== "string") {
811
+ count++;
812
+ }
813
+ }
814
+ return count;
815
+ }
816
+ /**
817
+ * How many ids read back as the other type under the canonical rule: numeric ids that are not
818
+ * canonical integer text (read back as strings) and string ids that are (read back as numbers).
819
+ * @param snapshot - the snapshot
820
+ * @returns the count
821
+ */
822
+ function countTextIds(snapshot) {
823
+ const { ids } = snapshot;
824
+ if (ids.kind === "identity" || ids.kind === "dense") {
825
+ return 0;
826
+ }
827
+ let count = 0;
828
+ for (let i = 0; i < ids.size; i++) {
829
+ const id = ids.idOf(i);
830
+ if (canonicalId(idToText(id)) !== id) {
831
+ count++;
832
+ }
833
+ }
834
+ return count;
835
+ }
836
+ //# sourceMappingURL=exporter.js.map