@graphty/graph-io 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -28
  3. package/dist/chunks/children-CL3Cy0ez.js +238 -0
  4. package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
  5. package/dist/chunks/escape-DyI8JofU.js +938 -0
  6. package/dist/chunks/escape-DyI8JofU.js.map +1 -0
  7. package/dist/chunks/importer-CQnJuWJw.js +2987 -0
  8. package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
  9. package/dist/chunks/importer-CpCpfbxr.js +2015 -0
  10. package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
  11. package/dist/chunks/importer-DbnGYr3_.js +2342 -0
  12. package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
  13. package/dist/chunks/importer-GozH8DkN.js +3050 -0
  14. package/dist/chunks/importer-GozH8DkN.js.map +1 -0
  15. package/dist/chunks/records-CGpxszm1.js +605 -0
  16. package/dist/chunks/records-CGpxszm1.js.map +1 -0
  17. package/dist/chunks/text-CajMdVFy.js +189 -0
  18. package/dist/chunks/text-CajMdVFy.js.map +1 -0
  19. package/dist/chunks/writer-DxSKC7TL.js +2842 -0
  20. package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
  21. package/dist/csv.d.ts +1 -0
  22. package/dist/csv.js +1702 -0
  23. package/dist/csv.js.map +1 -0
  24. package/dist/dot.d.ts +1 -0
  25. package/dist/dot.js +8 -0
  26. package/dist/dot.js.map +1 -0
  27. package/dist/gexf.d.ts +1 -0
  28. package/dist/gexf.js +3466 -0
  29. package/dist/gexf.js.map +1 -0
  30. package/dist/gml.d.ts +1 -0
  31. package/dist/gml.js +2647 -0
  32. package/dist/gml.js.map +1 -0
  33. package/dist/graph-io.d.ts +1 -0
  34. package/dist/graph-io.js +790 -0
  35. package/dist/graph-io.js.map +1 -0
  36. package/dist/graphml.d.ts +1 -0
  37. package/dist/graphml.js +8 -0
  38. package/dist/graphml.js.map +1 -0
  39. package/dist/json.d.ts +1 -0
  40. package/dist/json.js +11 -0
  41. package/dist/json.js.map +1 -0
  42. package/dist/neo4j.d.ts +1 -0
  43. package/dist/neo4j.js +2046 -0
  44. package/dist/neo4j.js.map +1 -0
  45. package/dist/pajek.d.ts +1 -0
  46. package/dist/pajek.js +8 -0
  47. package/dist/pajek.js.map +1 -0
  48. package/dist/src/children.d.ts +134 -0
  49. package/dist/src/children.d.ts.map +1 -0
  50. package/dist/src/children.js +274 -0
  51. package/dist/src/children.js.map +1 -0
  52. package/dist/src/common/attributes.d.ts +229 -0
  53. package/dist/src/common/attributes.d.ts.map +1 -0
  54. package/dist/src/common/attributes.js +368 -0
  55. package/dist/src/common/attributes.js.map +1 -0
  56. package/dist/src/common/codes.d.ts +105 -0
  57. package/dist/src/common/codes.d.ts.map +1 -0
  58. package/dist/src/common/codes.js +107 -0
  59. package/dist/src/common/codes.js.map +1 -0
  60. package/dist/src/common/declared-types.d.ts +84 -0
  61. package/dist/src/common/declared-types.d.ts.map +1 -0
  62. package/dist/src/common/declared-types.js +326 -0
  63. package/dist/src/common/declared-types.js.map +1 -0
  64. package/dist/src/common/direction.d.ts +206 -0
  65. package/dist/src/common/direction.d.ts.map +1 -0
  66. package/dist/src/common/direction.js +370 -0
  67. package/dist/src/common/direction.js.map +1 -0
  68. package/dist/src/common/escape.d.ts +92 -0
  69. package/dist/src/common/escape.d.ts.map +1 -0
  70. package/dist/src/common/escape.js +212 -0
  71. package/dist/src/common/escape.js.map +1 -0
  72. package/dist/src/common/export.d.ts +249 -0
  73. package/dist/src/common/export.d.ts.map +1 -0
  74. package/dist/src/common/export.js +594 -0
  75. package/dist/src/common/export.js.map +1 -0
  76. package/dist/src/common/format.d.ts +59 -0
  77. package/dist/src/common/format.d.ts.map +1 -0
  78. package/dist/src/common/format.js +106 -0
  79. package/dist/src/common/format.js.map +1 -0
  80. package/dist/src/common/ids.d.ts +83 -0
  81. package/dist/src/common/ids.d.ts.map +1 -0
  82. package/dist/src/common/ids.js +158 -0
  83. package/dist/src/common/ids.js.map +1 -0
  84. package/dist/src/common/input.d.ts +100 -0
  85. package/dist/src/common/input.d.ts.map +1 -0
  86. package/dist/src/common/input.js +335 -0
  87. package/dist/src/common/input.js.map +1 -0
  88. package/dist/src/common/lists.d.ts +34 -0
  89. package/dist/src/common/lists.d.ts.map +1 -0
  90. package/dist/src/common/lists.js +185 -0
  91. package/dist/src/common/lists.js.map +1 -0
  92. package/dist/src/common/options.d.ts +108 -0
  93. package/dist/src/common/options.d.ts.map +1 -0
  94. package/dist/src/common/options.js +265 -0
  95. package/dist/src/common/options.js.map +1 -0
  96. package/dist/src/common/report.d.ts +187 -0
  97. package/dist/src/common/report.d.ts.map +1 -0
  98. package/dist/src/common/report.js +274 -0
  99. package/dist/src/common/report.js.map +1 -0
  100. package/dist/src/common/temporal.d.ts +71 -0
  101. package/dist/src/common/temporal.d.ts.map +1 -0
  102. package/dist/src/common/temporal.js +266 -0
  103. package/dist/src/common/temporal.js.map +1 -0
  104. package/dist/src/common/text.d.ts +104 -0
  105. package/dist/src/common/text.d.ts.map +1 -0
  106. package/dist/src/common/text.js +255 -0
  107. package/dist/src/common/text.js.map +1 -0
  108. package/dist/src/common/weights.d.ts +77 -0
  109. package/dist/src/common/weights.d.ts.map +1 -0
  110. package/dist/src/common/weights.js +156 -0
  111. package/dist/src/common/weights.js.map +1 -0
  112. package/dist/src/common/writer.d.ts +51 -0
  113. package/dist/src/common/writer.d.ts.map +1 -0
  114. package/dist/src/common/writer.js +108 -0
  115. package/dist/src/common/writer.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +245 -0
  117. package/dist/src/common/xml.d.ts.map +1 -0
  118. package/dist/src/common/xml.js +942 -0
  119. package/dist/src/common/xml.js.map +1 -0
  120. package/dist/src/formats/csv/exporter.d.ts +70 -0
  121. package/dist/src/formats/csv/exporter.d.ts.map +1 -0
  122. package/dist/src/formats/csv/exporter.js +682 -0
  123. package/dist/src/formats/csv/exporter.js.map +1 -0
  124. package/dist/src/formats/csv/header.d.ts +66 -0
  125. package/dist/src/formats/csv/header.d.ts.map +1 -0
  126. package/dist/src/formats/csv/header.js +152 -0
  127. package/dist/src/formats/csv/header.js.map +1 -0
  128. package/dist/src/formats/csv/importer.d.ts +82 -0
  129. package/dist/src/formats/csv/importer.d.ts.map +1 -0
  130. package/dist/src/formats/csv/importer.js +849 -0
  131. package/dist/src/formats/csv/importer.js.map +1 -0
  132. package/dist/src/formats/csv/index.d.ts +60 -0
  133. package/dist/src/formats/csv/index.d.ts.map +1 -0
  134. package/dist/src/formats/csv/index.js +63 -0
  135. package/dist/src/formats/csv/index.js.map +1 -0
  136. package/dist/src/formats/csv/records.d.ts +188 -0
  137. package/dist/src/formats/csv/records.d.ts.map +1 -0
  138. package/dist/src/formats/csv/records.js +702 -0
  139. package/dist/src/formats/csv/records.js.map +1 -0
  140. package/dist/src/formats/csv/values.d.ts +105 -0
  141. package/dist/src/formats/csv/values.d.ts.map +1 -0
  142. package/dist/src/formats/csv/values.js +192 -0
  143. package/dist/src/formats/csv/values.js.map +1 -0
  144. package/dist/src/formats/dot/exporter.d.ts +52 -0
  145. package/dist/src/formats/dot/exporter.d.ts.map +1 -0
  146. package/dist/src/formats/dot/exporter.js +836 -0
  147. package/dist/src/formats/dot/exporter.js.map +1 -0
  148. package/dist/src/formats/dot/importer.d.ts +102 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -0
  150. package/dist/src/formats/dot/importer.js +1291 -0
  151. package/dist/src/formats/dot/importer.js.map +1 -0
  152. package/dist/src/formats/dot/index.d.ts +7 -0
  153. package/dist/src/formats/dot/index.d.ts.map +1 -0
  154. package/dist/src/formats/dot/index.js +7 -0
  155. package/dist/src/formats/dot/index.js.map +1 -0
  156. package/dist/src/formats/dot/names.d.ts +29 -0
  157. package/dist/src/formats/dot/names.d.ts.map +1 -0
  158. package/dist/src/formats/dot/names.js +28 -0
  159. package/dist/src/formats/dot/names.js.map +1 -0
  160. package/dist/src/formats/dot/tokenizer.d.ts +114 -0
  161. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
  162. package/dist/src/formats/dot/tokenizer.js +341 -0
  163. package/dist/src/formats/dot/tokenizer.js.map +1 -0
  164. package/dist/src/formats/gexf/exporter.d.ts +56 -0
  165. package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/exporter.js +1395 -0
  167. package/dist/src/formats/gexf/exporter.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +73 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -0
  170. package/dist/src/formats/gexf/importer.js +1880 -0
  171. package/dist/src/formats/gexf/importer.js.map +1 -0
  172. package/dist/src/formats/gexf/index.d.ts +96 -0
  173. package/dist/src/formats/gexf/index.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/index.js +97 -0
  175. package/dist/src/formats/gexf/index.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts +135 -0
  177. package/dist/src/formats/gexf/schema.d.ts.map +1 -0
  178. package/dist/src/formats/gexf/schema.js +323 -0
  179. package/dist/src/formats/gexf/schema.js.map +1 -0
  180. package/dist/src/formats/gml/exporter.d.ts +69 -0
  181. package/dist/src/formats/gml/exporter.d.ts.map +1 -0
  182. package/dist/src/formats/gml/exporter.js +1093 -0
  183. package/dist/src/formats/gml/exporter.js.map +1 -0
  184. package/dist/src/formats/gml/importer.d.ts +66 -0
  185. package/dist/src/formats/gml/importer.d.ts.map +1 -0
  186. package/dist/src/formats/gml/importer.js +1331 -0
  187. package/dist/src/formats/gml/importer.js.map +1 -0
  188. package/dist/src/formats/gml/index.d.ts +85 -0
  189. package/dist/src/formats/gml/index.d.ts.map +1 -0
  190. package/dist/src/formats/gml/index.js +88 -0
  191. package/dist/src/formats/gml/index.js.map +1 -0
  192. package/dist/src/formats/gml/syntax.d.ts +186 -0
  193. package/dist/src/formats/gml/syntax.d.ts.map +1 -0
  194. package/dist/src/formats/gml/syntax.js +467 -0
  195. package/dist/src/formats/gml/syntax.js.map +1 -0
  196. package/dist/src/formats/graphml/constants.d.ts +169 -0
  197. package/dist/src/formats/graphml/constants.d.ts.map +1 -0
  198. package/dist/src/formats/graphml/constants.js +165 -0
  199. package/dist/src/formats/graphml/constants.js.map +1 -0
  200. package/dist/src/formats/graphml/exporter.d.ts +34 -0
  201. package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
  202. package/dist/src/formats/graphml/exporter.js +1176 -0
  203. package/dist/src/formats/graphml/exporter.js.map +1 -0
  204. package/dist/src/formats/graphml/importer.d.ts +31 -0
  205. package/dist/src/formats/graphml/importer.d.ts.map +1 -0
  206. package/dist/src/formats/graphml/importer.js +1607 -0
  207. package/dist/src/formats/graphml/importer.js.map +1 -0
  208. package/dist/src/formats/graphml/index.d.ts +8 -0
  209. package/dist/src/formats/graphml/index.d.ts.map +1 -0
  210. package/dist/src/formats/graphml/index.js +8 -0
  211. package/dist/src/formats/graphml/index.js.map +1 -0
  212. package/dist/src/formats/graphml/tree.d.ts +72 -0
  213. package/dist/src/formats/graphml/tree.d.ts.map +1 -0
  214. package/dist/src/formats/graphml/tree.js +290 -0
  215. package/dist/src/formats/graphml/tree.js.map +1 -0
  216. package/dist/src/formats/json/dialect.d.ts +125 -0
  217. package/dist/src/formats/json/dialect.d.ts.map +1 -0
  218. package/dist/src/formats/json/dialect.js +262 -0
  219. package/dist/src/formats/json/dialect.js.map +1 -0
  220. package/dist/src/formats/json/exporter.d.ts +89 -0
  221. package/dist/src/formats/json/exporter.d.ts.map +1 -0
  222. package/dist/src/formats/json/exporter.js +1358 -0
  223. package/dist/src/formats/json/exporter.js.map +1 -0
  224. package/dist/src/formats/json/importer.d.ts +108 -0
  225. package/dist/src/formats/json/importer.d.ts.map +1 -0
  226. package/dist/src/formats/json/importer.js +1838 -0
  227. package/dist/src/formats/json/importer.js.map +1 -0
  228. package/dist/src/formats/json/index.d.ts +8 -0
  229. package/dist/src/formats/json/index.d.ts.map +1 -0
  230. package/dist/src/formats/json/index.js +8 -0
  231. package/dist/src/formats/json/index.js.map +1 -0
  232. package/dist/src/formats/neo4j/exporter.d.ts +68 -0
  233. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
  234. package/dist/src/formats/neo4j/exporter.js +1055 -0
  235. package/dist/src/formats/neo4j/exporter.js.map +1 -0
  236. package/dist/src/formats/neo4j/header.d.ts +52 -0
  237. package/dist/src/formats/neo4j/header.d.ts.map +1 -0
  238. package/dist/src/formats/neo4j/header.js +131 -0
  239. package/dist/src/formats/neo4j/header.js.map +1 -0
  240. package/dist/src/formats/neo4j/importer.d.ts +73 -0
  241. package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
  242. package/dist/src/formats/neo4j/importer.js +932 -0
  243. package/dist/src/formats/neo4j/importer.js.map +1 -0
  244. package/dist/src/formats/neo4j/index.d.ts +79 -0
  245. package/dist/src/formats/neo4j/index.d.ts.map +1 -0
  246. package/dist/src/formats/neo4j/index.js +83 -0
  247. package/dist/src/formats/neo4j/index.js.map +1 -0
  248. package/dist/src/formats/pajek/exporter.d.ts +58 -0
  249. package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
  250. package/dist/src/formats/pajek/exporter.js +825 -0
  251. package/dist/src/formats/pajek/exporter.js.map +1 -0
  252. package/dist/src/formats/pajek/importer.d.ts +88 -0
  253. package/dist/src/formats/pajek/importer.d.ts.map +1 -0
  254. package/dist/src/formats/pajek/importer.js +1047 -0
  255. package/dist/src/formats/pajek/importer.js.map +1 -0
  256. package/dist/src/formats/pajek/index.d.ts +7 -0
  257. package/dist/src/formats/pajek/index.d.ts.map +1 -0
  258. package/dist/src/formats/pajek/index.js +7 -0
  259. package/dist/src/formats/pajek/index.js.map +1 -0
  260. package/dist/src/formats/pajek/syntax.d.ts +112 -0
  261. package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
  262. package/dist/src/formats/pajek/syntax.js +269 -0
  263. package/dist/src/formats/pajek/syntax.js.map +1 -0
  264. package/dist/src/index.d.ts +35 -0
  265. package/dist/src/index.d.ts.map +1 -0
  266. package/dist/src/index.js +39 -0
  267. package/dist/src/index.js.map +1 -0
  268. package/dist/src/registry.d.ts +207 -0
  269. package/dist/src/registry.d.ts.map +1 -0
  270. package/dist/src/registry.js +481 -0
  271. package/dist/src/registry.js.map +1 -0
  272. package/dist/src/sniff.d.ts +104 -0
  273. package/dist/src/sniff.d.ts.map +1 -0
  274. package/dist/src/sniff.js +357 -0
  275. package/dist/src/sniff.js.map +1 -0
  276. package/dist/src/types.d.ts +238 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +29 -0
  279. package/dist/src/types.js.map +1 -0
  280. package/dist/tsconfig.build.tsbuildinfo +1 -0
  281. package/package.json +122 -7
  282. package/src/children.ts +335 -0
  283. package/src/common/attributes.ts +520 -0
  284. package/src/common/codes.ts +153 -0
  285. package/src/common/declared-types.ts +374 -0
  286. package/src/common/direction.ts +518 -0
  287. package/src/common/escape.ts +231 -0
  288. package/src/common/export.ts +817 -0
  289. package/src/common/format.ts +111 -0
  290. package/src/common/ids.ts +176 -0
  291. package/src/common/input.ts +378 -0
  292. package/src/common/lists.ts +196 -0
  293. package/src/common/options.ts +377 -0
  294. package/src/common/report.ts +352 -0
  295. package/src/common/temporal.ts +302 -0
  296. package/src/common/text.ts +294 -0
  297. package/src/common/weights.ts +202 -0
  298. package/src/common/writer.ts +123 -0
  299. package/src/common/xml.ts +1053 -0
  300. package/src/formats/csv/exporter.ts +894 -0
  301. package/src/formats/csv/header.ts +172 -0
  302. package/src/formats/csv/importer.ts +1104 -0
  303. package/src/formats/csv/index.ts +88 -0
  304. package/src/formats/csv/records.ts +813 -0
  305. package/src/formats/csv/values.ts +224 -0
  306. package/src/formats/dot/exporter.ts +1014 -0
  307. package/src/formats/dot/importer.ts +1549 -0
  308. package/src/formats/dot/index.ts +7 -0
  309. package/src/formats/dot/names.ts +40 -0
  310. package/src/formats/dot/tokenizer.ts +384 -0
  311. package/src/formats/gexf/exporter.ts +1696 -0
  312. package/src/formats/gexf/importer.ts +2333 -0
  313. package/src/formats/gexf/index.ts +142 -0
  314. package/src/formats/gexf/schema.ts +361 -0
  315. package/src/formats/gml/exporter.ts +1404 -0
  316. package/src/formats/gml/importer.ts +1591 -0
  317. package/src/formats/gml/index.ts +128 -0
  318. package/src/formats/gml/syntax.ts +545 -0
  319. package/src/formats/graphml/constants.ts +225 -0
  320. package/src/formats/graphml/exporter.ts +1458 -0
  321. package/src/formats/graphml/importer.ts +2027 -0
  322. package/src/formats/graphml/index.ts +8 -0
  323. package/src/formats/graphml/tree.ts +318 -0
  324. package/src/formats/json/dialect.ts +317 -0
  325. package/src/formats/json/exporter.ts +1616 -0
  326. package/src/formats/json/importer.ts +2271 -0
  327. package/src/formats/json/index.ts +8 -0
  328. package/src/formats/neo4j/exporter.ts +1287 -0
  329. package/src/formats/neo4j/header.ts +156 -0
  330. package/src/formats/neo4j/importer.ts +1220 -0
  331. package/src/formats/neo4j/index.ts +116 -0
  332. package/src/formats/pajek/exporter.ts +1000 -0
  333. package/src/formats/pajek/importer.ts +1311 -0
  334. package/src/formats/pajek/index.ts +7 -0
  335. package/src/formats/pajek/syntax.ts +307 -0
  336. package/src/index.ts +244 -0
  337. package/src/registry.ts +617 -0
  338. package/src/sniff.ts +397 -0
  339. package/src/types.ts +262 -0
@@ -0,0 +1,1176 @@
1
+ /**
2
+ * The GraphML exporter (design section 8.5; research note 07 section 9): `<key>` declarations
3
+ * regenerated from ColumnMeta.origin when the column came from GraphML (key id, attr.name,
4
+ * attr.type, yfiles.type) and derived from the dtype otherwise; node ids sanitised to NMTOKEN
5
+ * (`sanitizeIds: "error"` refuses, `"mangle"` rewrites and keeps the original in a
6
+ * `graphty:originalId` data attribute the importer restores); mixed direction written with
7
+ * `edgedefault` plus per-edge `directed` attributes, folding expanded pairs back through the
8
+ * `pair` / `directed` role columns; explicit weights only (the role-weight column's validity);
9
+ * containment (`parent` role) as nested graphs; yFiles json columns as nested XML again.
10
+ *
11
+ * check() lists every loss before anything is written: the generic capability gaps of
12
+ * checkCapabilities() (dict / u32 / u8 dtypes, lists, components, options, positions, visual and
13
+ * temporal roles, extension tables) plus the GraphML-specific ones (mutual edges written as
14
+ * undirected, json columns without a yfiles origin written as JSON text, multi-parent columns,
15
+ * containment order, ids that change type under the canonical rule, edge ids that read back as
16
+ * strings, roles GraphML cannot express, yfiles values that are not serialisable trees).
17
+ */
18
+ import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
19
+ import { childrenCsr } from "../../children.js";
20
+ import { pairFolding } from "../../common/direction.js";
21
+ import { escapeXmlAttribute, escapeXmlText } from "../../common/escape.js";
22
+ import { capabilities, checkCapabilities, isNmtoken, LOSS, mangleNmtoken, sanitizeIds, } from "../../common/export.js";
23
+ import { formatF32, formatF64, formatInteger } from "../../common/format.js";
24
+ import { isCanonicalIntegerText } from "../../common/ids.js";
25
+ import { resolveExportOptions } from "../../common/options.js";
26
+ import { explicitWeights } from "../../common/weights.js";
27
+ import { encodeChunks, joinText } from "../../common/writer.js";
28
+ import { xmlIllegalTextNotes } from "../../common/xml.js";
29
+ import { EDGE_ID_COLUMN, FORMAT, GRAPHML_LOSS, GRAPHML_NAMESPACE, LABEL_COLUMN, META_KEY, ORIGINAL_ID_ATTRIBUTE, PARENT_COLUMN, RESERVED_EDGE_NAMES, RESERVED_NODE_NAMES, SCHEMA_LOCATION, SOURCE_PORT_COLUMN, TARGET_PORT_COLUMN, XSI_NAMESPACE, YFILES_NAMESPACE, } from "./constants.js";
30
+ import { treeProblem, writeXmlTree } from "./tree.js";
31
+ /** What GraphML keeps as declared (research note 07 section 9). */
32
+ const CAPABILITIES = capabilities({
33
+ mixedDirection: true,
34
+ multiEdges: true,
35
+ selfLoops: true,
36
+ edgeIds: "optional",
37
+ idCharset: "nmtoken",
38
+ dtypes: ["bool", "i32", "f32", "f64", "string"],
39
+ components: false,
40
+ lists: false,
41
+ json: false,
42
+ defaults: true,
43
+ options: false,
44
+ hierarchy: true,
45
+ temporal: "none",
46
+ graphAttributes: true,
47
+ positions: false,
48
+ viz: false,
49
+ });
50
+ /** Roles handled structurally (never written as a key) or reported by checkCapabilities() and not written. */
51
+ const STRUCTURAL_ROLES = new Set([
52
+ "directed",
53
+ "pair",
54
+ "mutual",
55
+ "weight",
56
+ "timeText",
57
+ "originalId",
58
+ ]);
59
+ const DROPPED_ROLES = new Set([
60
+ "position",
61
+ "color",
62
+ "size",
63
+ "shape",
64
+ "thickness",
65
+ "start",
66
+ "end",
67
+ "timestamp",
68
+ "timestamps",
69
+ "spells",
70
+ "open",
71
+ ]);
72
+ /**
73
+ * The roles GraphML has a slot for (checkCapabilities() reports every other role as lost): the
74
+ * label key, the edge `id`, `sourceport` and `targetport` attributes and nested graphs.
75
+ */
76
+ const SLOT_ROLES = new Set(["label", "id", "sourcePort", "targetPort", "parent"]);
77
+ /** The slot roles written structurally per domain (never as a key); a label is a key titled `label`. */
78
+ const SLOT_ROLES_BY_DOMAIN = {
79
+ graph: new Set(),
80
+ node: new Set(["parent"]),
81
+ edge: new Set(["id", "sourcePort", "targetPort"]),
82
+ };
83
+ /** The names the importer gives the slot columns (design section 5.6), for the name-change notes. */
84
+ const ROLE_NAMES = Object.freeze({
85
+ label: LABEL_COLUMN,
86
+ id: EDGE_ID_COLUMN,
87
+ sourcePort: SOURCE_PORT_COLUMN,
88
+ targetPort: TARGET_PORT_COLUMN,
89
+ parent: PARENT_COLUMN,
90
+ });
91
+ /** The attr.name of the weight key when the weight did not come from GraphML (the importer's weightFrom default). */
92
+ const DEFAULT_WEIGHT_NAME = "weight";
93
+ /** The GraphML attr.type values and the dtype each maps to, for restoring origin.type. */
94
+ const TYPE_DTYPES = {
95
+ boolean: "bool",
96
+ int: "i32",
97
+ long: "f64",
98
+ float: "f32",
99
+ double: "f64",
100
+ string: "string",
101
+ };
102
+ const I32_MAX = 2147483647;
103
+ /** The yfiles.type written for a yfiles json column whose origin records none. */
104
+ const DEFAULT_YFILES_TYPES = {
105
+ node: "nodegraphics",
106
+ edge: "edgegraphics",
107
+ graph: "resources",
108
+ };
109
+ /**
110
+ * Resolve the format-specific options.
111
+ * @param options - the caller's options
112
+ * @returns the pretty flag and the edgedefault override
113
+ */
114
+ function resolveFormatOptions(options) {
115
+ const pretty = options?.pretty ?? true;
116
+ if (typeof pretty !== "boolean") {
117
+ throw new GraphFormatError("E_UNSUPPORTED", "option pretty must be a boolean", {
118
+ option: "pretty",
119
+ found: typeof pretty,
120
+ });
121
+ }
122
+ const edgedefault = options?.edgedefault ?? null;
123
+ if (edgedefault !== null && edgedefault !== "directed" && edgedefault !== "undirected") {
124
+ throw new GraphFormatError("E_UNSUPPORTED", `option edgedefault: ${JSON.stringify(edgedefault)} is not "directed" or "undirected"`, { option: "edgedefault", found: edgedefault });
125
+ }
126
+ return { pretty, edgedefault };
127
+ }
128
+ /**
129
+ * The importer's `meta.extra.graphml`, when the snapshot has one of the expected shape.
130
+ * @param snapshot - the snapshot
131
+ * @returns the meta, or null
132
+ */
133
+ function graphmlMetaOf(snapshot) {
134
+ const value = snapshot.meta.extra[META_KEY];
135
+ if (typeof value !== "object" || value === null) {
136
+ return null;
137
+ }
138
+ const record = value;
139
+ const graphId = typeof record.graphId === "string" ? record.graphId : null;
140
+ const edgedefault = record.edgedefault === "directed" || record.edgedefault === "undirected" ? record.edgedefault : null;
141
+ const namespaces = {};
142
+ if (typeof record.namespaces === "object" && record.namespaces !== null) {
143
+ for (const [prefix, uri] of Object.entries(record.namespaces)) {
144
+ if (typeof uri === "string") {
145
+ namespaces[prefix] = uri;
146
+ }
147
+ }
148
+ }
149
+ return { graphId, edgedefault, namespaces };
150
+ }
151
+ /**
152
+ * Build the plan: classify every column, assign key ids, decide the edgedefault and the
153
+ * containment order, and collect every loss note.
154
+ * @param snapshot - the snapshot
155
+ * @param options - the resolved common options
156
+ * @param format - the resolved format options
157
+ * @returns the plan
158
+ */
159
+ function planExport(snapshot, options, format) {
160
+ // the generic json note is replaced by planTable()'s (yfiles trees are kept, other json is text)
161
+ const notes = checkCapabilities(snapshot, CAPABILITIES, options, {
162
+ roles: SLOT_ROLES,
163
+ roleNames: ROLE_NAMES,
164
+ }).filter((n) => n.code !== LOSS.JSON);
165
+ const note = (code, message, column = null, count = null) => {
166
+ notes.push(Object.freeze({ code, message, column, count }));
167
+ };
168
+ notes.push(...xmlIllegalTextNotes(snapshot));
169
+ const meta = graphmlMetaOf(snapshot);
170
+ const keyIds = new KeyIds();
171
+ const graphColumns = planTable(snapshot.graph, "graph", notes, note);
172
+ const nodeColumns = planTable(snapshot.nodes, "node", notes, note);
173
+ const edgeColumns = planTable(snapshot.edges, "edge", notes, note);
174
+ const keys = planKeys([...graphColumns, ...nodeColumns, ...edgeColumns], keyIds);
175
+ reservedNameNotes(nodeColumns, RESERVED_NODE_NAMES, note);
176
+ reservedNameNotes(edgeColumns, RESERVED_EDGE_NAMES, note);
177
+ const weight = planWeight(snapshot, edgeColumns, keyIds, keys, note);
178
+ // ids
179
+ const idTypeChanges = countIdTypeChanges(snapshot);
180
+ if (idTypeChanges > 0) {
181
+ note(GRAPHML_LOSS.ID_TEXT_TYPE, `${idTypeChanges} node id(s) change type when read back under ids: "canonical" (string ids that are integer text, non-integer numbers)`, null, idTypeChanges);
182
+ }
183
+ const edgeIdColumn = snapshot.edges.byRole("id");
184
+ if (edgeIdColumn !== null) {
185
+ planEdgeIds(snapshot, edgeIdColumn, options, note);
186
+ }
187
+ const unrepresentable = countUnrepresentableNodeIds(snapshot);
188
+ const needsOriginalIdKey = unrepresentable > 0 && options.sanitizeIds === "mangle";
189
+ const originalIdKey = needsOriginalIdKey ? keyIds.next(null) : null;
190
+ // direction: an undirected pair folds to one undirected edge, a mutual pair too (the mark is lost)
191
+ const folding = pairFolding(snapshot, { foldMutual: true });
192
+ if (folding.mutualCount > 0) {
193
+ note(GRAPHML_LOSS.MUTUAL_AS_UNDIRECTED, `${folding.mutualCount} mutual pair(s) are written as undirected edges; the mutual mark is lost`, null, folding.mutualCount);
194
+ }
195
+ const edgedefault = planEdgedefault(snapshot, folding, format, meta);
196
+ // containment
197
+ const hierarchy = planHierarchy(snapshot);
198
+ if (hierarchy.reordered) {
199
+ note(GRAPHML_LOSS.HIERARCHY_REORDERED, "nodes are written in containment order (children nested under their parent); node indices change after a round trip", hierarchy.parent?.meta.name ?? null);
200
+ }
201
+ if (hierarchy.unreachable > 0) {
202
+ note(GRAPHML_LOSS.PARENT_CYCLE, `${hierarchy.unreachable} node(s) whose parent chain never reaches a root are written at the top level`, hierarchy.parent?.meta.name ?? null, hierarchy.unreachable);
203
+ }
204
+ return {
205
+ options,
206
+ pretty: format.pretty,
207
+ notes,
208
+ keys,
209
+ graphColumns,
210
+ nodeColumns,
211
+ edgeColumns,
212
+ weight,
213
+ originalIdKey,
214
+ edgedefault,
215
+ folding,
216
+ edgeIdColumn,
217
+ sourcePortColumn: snapshot.edges.byRole("sourcePort"),
218
+ targetPortColumn: snapshot.edges.byRole("targetPort"),
219
+ hierarchy,
220
+ meta,
221
+ needsOriginalIdKey,
222
+ };
223
+ }
224
+ /**
225
+ * A plain column titled like one of the importer's XML-derived columns (`id`, `sourceport`,
226
+ * `targetport` on edges, `parent` on nodes) reads back renamed `<name>#<key id>` (design section
227
+ * 5.6); the note says so.
228
+ * @param plans - the column plans of one domain, key ids assigned
229
+ * @param reserved - the reserved names of that domain
230
+ * @param note - the note recorder
231
+ */
232
+ function reservedNameNotes(plans, reserved, note) {
233
+ for (const plan of plans) {
234
+ const { column, attrName, keyId } = plan;
235
+ if (attrName === null || !reserved.has(attrName)) {
236
+ continue;
237
+ }
238
+ note(LOSS.COLUMN_NAME_CHANGED, `${plan.domain} column "${column.meta.name}" is titled like the importer's ${attrName} column and reads back as "${attrName}#${keyId}"`, column.meta.name, column.length - column.nullCount);
239
+ }
240
+ }
241
+ /** The key ids handed out so far: a preferred NMTOKEN when free, else the next free `d<n>`. */
242
+ class KeyIds {
243
+ constructor() {
244
+ this.used = new Set();
245
+ this.counter = 0;
246
+ }
247
+ /**
248
+ * Take a key id.
249
+ * @param preferred - the id to keep when it is an NMTOKEN not handed out yet, or null
250
+ * @returns the id
251
+ */
252
+ next(preferred) {
253
+ if (preferred !== null && isNmtoken(preferred) && !this.used.has(preferred)) {
254
+ this.used.add(preferred);
255
+ return preferred;
256
+ }
257
+ for (;;) {
258
+ const candidate = `d${this.counter}`;
259
+ this.counter++;
260
+ if (!this.used.has(candidate)) {
261
+ this.used.add(candidate);
262
+ return candidate;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ /**
268
+ * The `<key>` elements: columns sharing a GraphML origin.id across domains become one for="all"
269
+ * key when their name, type and default agree; every other column is its own key.
270
+ * @param all - the column plans of the three domains, in writing order
271
+ * @param keyIds - the key id allocator
272
+ * @returns the keys, in declaration order
273
+ */
274
+ function planKeys(all, keyIds) {
275
+ const keys = [];
276
+ const byOrigin = new Map();
277
+ for (const plan of all) {
278
+ const { origin } = plan.column.meta;
279
+ if (origin !== null && origin.format === FORMAT && origin.id !== null) {
280
+ const group = byOrigin.get(origin.id) ?? [];
281
+ group.push(plan);
282
+ byOrigin.set(origin.id, group);
283
+ }
284
+ }
285
+ const planned = new Set();
286
+ for (const plan of all) {
287
+ if (planned.has(plan)) {
288
+ continue;
289
+ }
290
+ const { origin } = plan.column.meta;
291
+ const group = origin !== null && origin.format === FORMAT && origin.id !== null
292
+ ? (byOrigin.get(origin.id) ?? [plan])
293
+ : [plan];
294
+ const shared = group.length > 1 && group.every((other) => other === plan || sameKey(plan, other));
295
+ const members = shared ? group : [plan];
296
+ const id = keyIds.next(origin !== null && origin.format === FORMAT ? origin.id : null);
297
+ for (const member of members) {
298
+ member.keyId = id;
299
+ planned.add(member);
300
+ }
301
+ const domains = members.map((member) => member.domain);
302
+ // a yfiles key carries attr.name only when the column name is not the key id (yEd writes none)
303
+ const yfilesName = plan.column.meta.origin?.title ?? plan.column.meta.name;
304
+ let { attrName } = plan;
305
+ if (plan.yfiles) {
306
+ attrName = yfilesName === id ? null : yfilesName;
307
+ }
308
+ keys.push({
309
+ id,
310
+ domains: domains.length > 1 ? ["graph", "node", "edge"] : domains,
311
+ attrName,
312
+ attrType: plan.attrType,
313
+ yfilesType: plan.yfilesType,
314
+ column: plan.column,
315
+ });
316
+ }
317
+ return keys;
318
+ }
319
+ /**
320
+ * The weight key of a weighted snapshot (its attr.name and attr.type restored from
321
+ * meta.weightOrigin when the weight came from GraphML), and the note for an edge column the
322
+ * importer would read as THE weight: every edge key titled like the weight key (`weight` by
323
+ * default) is a weight key on import, so such a column reads back as the weight, not as a column.
324
+ * @param snapshot - the snapshot
325
+ * @param edgeColumns - the edge column plans
326
+ * @param keyIds - the key id allocator
327
+ * @param keys - receives the weight key
328
+ * @param note - the note recorder
329
+ * @returns the weight plan, or null for an unweighted snapshot
330
+ */
331
+ function planWeight(snapshot, edgeColumns, keyIds, keys, note) {
332
+ const weights = explicitWeights(snapshot);
333
+ const origin = snapshot.meta.weightOrigin;
334
+ const fromGraphml = weights.weighted && origin !== null && origin.format === FORMAT;
335
+ const attrName = fromGraphml && origin.title !== null ? origin.title : DEFAULT_WEIGHT_NAME;
336
+ for (const plan of edgeColumns) {
337
+ if (plan.attrName === attrName) {
338
+ note(LOSS.WEIGHT_KEY_CLASH, `edge column "${plan.column.meta.name}" is written as a key titled "${attrName}", which the importer reads as THE weight (weightFrom); it reads back as the weight, not as a column`, plan.column.meta.name, plan.column.length - plan.column.nullCount);
339
+ }
340
+ }
341
+ if (!weights.weighted) {
342
+ return null;
343
+ }
344
+ let attrType = "double";
345
+ if (fromGraphml && origin.type !== null) {
346
+ const declared = origin.type.trim().toLowerCase();
347
+ if (declared === "float" || declared === "double" || declared === "long" || declared === "int") {
348
+ attrType = declared;
349
+ }
350
+ }
351
+ if ((attrType === "int" || attrType === "long") && !weightsIntegral(snapshot, weights)) {
352
+ // a declared integer weight only survives when every explicit weight is integral
353
+ attrType = "double";
354
+ }
355
+ const id = keyIds.next(fromGraphml ? origin.id : null);
356
+ keys.push({ id, domains: ["edge"], attrName, attrType, yfilesType: null, column: null });
357
+ return { keyId: id, attrType, weights };
358
+ }
359
+ /**
360
+ * The notes about the edge id column: a numeric column reads back as string; ids outside the
361
+ * NMTOKEN charset are refused or mangled per sanitizeIds.
362
+ * @param snapshot - the snapshot
363
+ * @param column - the edge id column
364
+ * @param options - the resolved options
365
+ * @param note - the note recorder
366
+ */
367
+ function planEdgeIds(snapshot, column, options, note) {
368
+ if (column.dtype !== "string" && column.dtype !== "dict") {
369
+ note(GRAPHML_LOSS.EDGE_ID_TEXT, `edge id column "${column.meta.name}" is ${column.dtype}; GraphML edge ids read back as strings`, column.meta.name, snapshot.edgeCount - column.nullCount);
370
+ }
371
+ const bad = countBadEdgeIds(column);
372
+ if (bad === 0) {
373
+ return;
374
+ }
375
+ if (options.sanitizeIds === "mangle") {
376
+ note(LOSS.ID_MANGLED, `${bad} edge id(s) outside the nmtoken charset are rewritten`, column.meta.name, bad);
377
+ }
378
+ else {
379
+ note(LOSS.ID_CHARSET, `${bad} edge id(s) outside the nmtoken charset; export() will throw unless sanitizeIds is "mangle"`, column.meta.name, bad);
380
+ }
381
+ }
382
+ /**
383
+ * The top-level edgedefault: the override, else undirected for an undirected snapshot, else
384
+ * directed when no written edge is undirected, else the one the importer recorded, else the
385
+ * majority direction.
386
+ * @param snapshot - the snapshot
387
+ * @param folding - the pair folding
388
+ * @param format - the resolved format options
389
+ * @param meta - the importer's meta, or null
390
+ * @returns the edgedefault
391
+ */
392
+ function planEdgedefault(snapshot, folding, format, meta) {
393
+ if (format.edgedefault !== null) {
394
+ return format.edgedefault;
395
+ }
396
+ if (!snapshot.directed) {
397
+ return "undirected";
398
+ }
399
+ let directedCount = 0;
400
+ let undirectedCount = 0;
401
+ for (let e = 0; e < snapshot.edgeCount; e++) {
402
+ if (folding.folded(e)) {
403
+ continue;
404
+ }
405
+ if (isDirectedEdge(folding, e)) {
406
+ directedCount++;
407
+ }
408
+ else {
409
+ undirectedCount++;
410
+ }
411
+ }
412
+ if (undirectedCount === 0) {
413
+ return "directed";
414
+ }
415
+ const recorded = meta === null ? null : meta.edgedefault;
416
+ if (recorded !== null) {
417
+ return recorded;
418
+ }
419
+ return undirectedCount > directedCount ? "undirected" : "directed";
420
+ }
421
+ /**
422
+ * Whether two column plans can share one for="all" key.
423
+ * @param a - one plan
424
+ * @param b - another
425
+ * @returns true when name, type and default agree
426
+ */
427
+ function sameKey(a, b) {
428
+ return (a.attrName === b.attrName &&
429
+ a.attrType === b.attrType &&
430
+ a.yfilesType === b.yfilesType &&
431
+ a.domain !== b.domain &&
432
+ JSON.stringify(a.column.meta.default ?? null) === JSON.stringify(b.column.meta.default ?? null));
433
+ }
434
+ /**
435
+ * Classify the columns of one table: which are written as keys and what each loses. A label
436
+ * column is written into the label slot (a key titled `label`, which the importer reads back
437
+ * with the role) unless the table holds another column of that name, in which case it keeps
438
+ * its own title and the role is lost.
439
+ * @param table - the table
440
+ * @param domain - its domain
441
+ * @param notes - the notes so far (the generic name-change note of a label column that cannot take the slot is withdrawn)
442
+ * @param note - the note recorder
443
+ * @returns the plans of the written columns, in declaration order
444
+ */
445
+ function planTable(table, domain, notes, note) {
446
+ const plans = [];
447
+ const columns = [...table];
448
+ const names = new Set(columns.map((column) => column.meta.name));
449
+ for (const column of columns) {
450
+ const { meta } = column;
451
+ const { role, name } = meta;
452
+ if (role === "parents") {
453
+ note(GRAPHML_LOSS.PARENTS_DROPPED, `${domain} column "${name}" (parents) cannot be written: nested graphs hold one parent per node`, name, column.length - column.nullCount);
454
+ continue;
455
+ }
456
+ if (role !== null &&
457
+ (STRUCTURAL_ROLES.has(role) || DROPPED_ROLES.has(role) || SLOT_ROLES_BY_DOMAIN[domain].has(role))) {
458
+ continue;
459
+ }
460
+ if (role !== null && role !== "label" && SLOT_ROLES.has(role)) {
461
+ // the slot belongs to another domain (an edge id on a node, a port on a graph)
462
+ withdrawNameChange(notes, name);
463
+ note(LOSS.ROLE, `${domain} column "${name}" (${role}) is written as a plain attribute; GraphML has no ${role} slot for a ${domain} and the role is lost`, name, column.length - column.nullCount);
464
+ }
465
+ const yfiles = meta.dtype === "json" && meta.origin?.namespace === "yfiles";
466
+ if (meta.dtype === "json" && !yfiles) {
467
+ note(LOSS.JSON, `${domain} column "${name}" holds nested values; written as JSON text, which reads back as string`, name, column.length - column.nullCount);
468
+ }
469
+ if (yfiles) {
470
+ const bad = countBadTrees(column);
471
+ if (bad > 0) {
472
+ note(GRAPHML_LOSS.YFILES_TREE, `${bad} value(s) of yfiles column "${name}" are not XML trees; export() will throw E_COLUMN_TYPE`, name, bad);
473
+ }
474
+ }
475
+ const { origin } = meta;
476
+ const fromGraphml = origin !== null && origin.format === FORMAT;
477
+ let attrName = null;
478
+ if (!yfiles) {
479
+ attrName = fromGraphml && origin.title !== null ? origin.title : name;
480
+ if (role === "label" && domain !== "graph" && attrName !== LABEL_COLUMN) {
481
+ attrName = labelSlot(column, domain, names, notes, note);
482
+ }
483
+ else if (role === null && domain !== "graph" && attrName === LABEL_COLUMN) {
484
+ note(LOSS.ROLE_ASSUMED, `${domain} column "${name}" has no role but is titled "${LABEL_COLUMN}", which the importer reads back with the label role`, name, column.length - column.nullCount);
485
+ }
486
+ }
487
+ plans.push({
488
+ column,
489
+ domain,
490
+ keyId: "",
491
+ attrType: yfiles ? null : attrTypeFor(column),
492
+ attrName,
493
+ yfilesType: yfiles ? (origin?.type ?? DEFAULT_YFILES_TYPES[domain]) : null,
494
+ yfiles,
495
+ });
496
+ }
497
+ return plans;
498
+ }
499
+ /**
500
+ * The title a label column not named `label` is written under: the label slot when the table has
501
+ * no other column of that name (checkCapabilities() has noted the name change), else its own
502
+ * name with the role lost (the name-change note is withdrawn and a role note recorded).
503
+ * @param column - the label column
504
+ * @param domain - its domain
505
+ * @param names - the column names of the table
506
+ * @param notes - the notes so far
507
+ * @param note - the note recorder
508
+ * @returns the attr.name
509
+ */
510
+ function labelSlot(column, domain, names, notes, note) {
511
+ const { name } = column.meta;
512
+ if (!names.has(LABEL_COLUMN)) {
513
+ return LABEL_COLUMN;
514
+ }
515
+ withdrawNameChange(notes, name);
516
+ note(LOSS.ROLE, `${domain} column "${name}" (label) is written as a plain attribute: the label slot (a key titled "${LABEL_COLUMN}") is taken by column "${LABEL_COLUMN}" and the role is lost`, name, column.length - column.nullCount);
517
+ return name;
518
+ }
519
+ /**
520
+ * Withdraw the name-change note checkCapabilities() recorded for a role column that does not
521
+ * take the slot after all.
522
+ * @param notes - the notes so far
523
+ * @param name - the column name
524
+ */
525
+ function withdrawNameChange(notes, name) {
526
+ const at = notes.findIndex((n) => n.code === LOSS.COLUMN_NAME_CHANGED && n.column === name);
527
+ if (at >= 0) {
528
+ notes.splice(at, 1);
529
+ }
530
+ }
531
+ /**
532
+ * The attr.type a column is declared with: the GraphML origin.type when it still describes the
533
+ * column, else the type of the dtype (u32 / u8 as int or long, dict as string, list / json /
534
+ * multi-component as string holding JSON text).
535
+ * @param column - the column
536
+ * @returns the attr.type text
537
+ */
538
+ function attrTypeFor(column) {
539
+ const { meta } = column;
540
+ const { origin } = meta;
541
+ const declared = origin !== null && origin.format === FORMAT && origin.type !== null ? origin.type : null;
542
+ if (meta.components > 1) {
543
+ return "string";
544
+ }
545
+ switch (meta.dtype) {
546
+ case "bool":
547
+ return "boolean";
548
+ case "i32":
549
+ return "int";
550
+ case "f32":
551
+ return "float";
552
+ case "f64": {
553
+ if (declared !== null && declared.trim().toLowerCase() === "long" && columnIntegral(column)) {
554
+ return "long";
555
+ }
556
+ return "double";
557
+ }
558
+ case "u8":
559
+ return "int";
560
+ case "u32":
561
+ return columnMax(column) <= I32_MAX ? "int" : "long";
562
+ case "string":
563
+ if (declared !== null && TYPE_DTYPES[declared.trim().toLowerCase()] === undefined) {
564
+ // an unknown declared type kept as text (W_UNKNOWN_ATTR_TYPE on import) is restored as declared
565
+ return declared;
566
+ }
567
+ return "string";
568
+ case "dict":
569
+ case "list":
570
+ case "json":
571
+ return "string";
572
+ default: {
573
+ const { dtype } = meta;
574
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${String(dtype)}`, { dtype });
575
+ }
576
+ }
577
+ }
578
+ /**
579
+ * Whether every set value of a numeric column is an integer.
580
+ * @param column - an f64 column
581
+ * @returns true when all set values are integral
582
+ */
583
+ function columnIntegral(column) {
584
+ if (column.dtype !== "f64") {
585
+ return false;
586
+ }
587
+ const { data } = column;
588
+ for (let r = 0; r < column.length; r++) {
589
+ if (column.isSet(r) && !Number.isInteger(data[r])) {
590
+ return false;
591
+ }
592
+ }
593
+ return true;
594
+ }
595
+ /**
596
+ * The largest set value of a u32 column.
597
+ * @param column - a u32 column
598
+ * @returns the maximum, 0 for an empty column
599
+ */
600
+ function columnMax(column) {
601
+ if (column.dtype !== "u32") {
602
+ return 0;
603
+ }
604
+ let max = 0;
605
+ const { data } = column;
606
+ for (let r = 0; r < column.length; r++) {
607
+ if (column.isSet(r) && data[r] > max) {
608
+ max = data[r];
609
+ }
610
+ }
611
+ return max;
612
+ }
613
+ /**
614
+ * Whether every explicit weight is integral (so a declared int / long weight key survives).
615
+ * @param snapshot - the snapshot
616
+ * @param weights - the explicit weights
617
+ * @returns true when all explicit weights are integers
618
+ */
619
+ function weightsIntegral(snapshot, weights) {
620
+ for (let e = 0; e < snapshot.edgeCount; e++) {
621
+ if (weights.isExplicit(e) && !Number.isInteger(weights.value(e))) {
622
+ return false;
623
+ }
624
+ }
625
+ return true;
626
+ }
627
+ /**
628
+ * How many values of a yfiles column are not serialisable trees.
629
+ * @param column - a json column
630
+ * @returns the count
631
+ */
632
+ function countBadTrees(column) {
633
+ if (column.dtype !== "json") {
634
+ return 0;
635
+ }
636
+ let bad = 0;
637
+ for (let r = 0; r < column.length; r++) {
638
+ if (column.isSet(r) && treeProblem(column.values[r]) !== null) {
639
+ bad++;
640
+ }
641
+ }
642
+ return bad;
643
+ }
644
+ /**
645
+ * Node ids that read back as another type under the canonical rule: string ids that are
646
+ * canonical integer text, and numbers that are not safe integers.
647
+ * @param snapshot - the snapshot
648
+ * @returns the count
649
+ */
650
+ function countIdTypeChanges(snapshot) {
651
+ const { ids } = snapshot;
652
+ if (ids.kind === "identity" || ids.kind === "dense") {
653
+ return 0;
654
+ }
655
+ let count = 0;
656
+ for (let i = 0; i < ids.size; i++) {
657
+ const id = ids.idOf(i);
658
+ if (typeof id === "string" ? isNmtoken(id) && isCanonicalIntegerText(id) : !Number.isSafeInteger(id)) {
659
+ count++;
660
+ }
661
+ }
662
+ return count;
663
+ }
664
+ /**
665
+ * Node ids the nmtoken charset cannot hold.
666
+ * @param snapshot - the snapshot
667
+ * @returns the count
668
+ */
669
+ function countUnrepresentableNodeIds(snapshot) {
670
+ const { ids } = snapshot;
671
+ if (ids.kind === "identity" || ids.kind === "dense") {
672
+ return 0;
673
+ }
674
+ let count = 0;
675
+ for (let i = 0; i < ids.size; i++) {
676
+ if (!isNmtoken(String(ids.idOf(i)))) {
677
+ count++;
678
+ }
679
+ }
680
+ return count;
681
+ }
682
+ /**
683
+ * Edge ids that are not NMTOKENs.
684
+ * @param column - the edge id column
685
+ * @returns the count
686
+ */
687
+ function countBadEdgeIds(column) {
688
+ let bad = 0;
689
+ for (let e = 0; e < column.length; e++) {
690
+ if (column.isSet(e) && !isNmtoken(edgeIdText(column, e))) {
691
+ bad++;
692
+ }
693
+ }
694
+ return bad;
695
+ }
696
+ /**
697
+ * The text of an edge id cell.
698
+ * @param column - the edge id column
699
+ * @param e - the edge
700
+ * @returns the id as text
701
+ */
702
+ function edgeIdText(column, e) {
703
+ const value = column.value(e);
704
+ if (typeof value === "string") {
705
+ return value;
706
+ }
707
+ if (typeof value === "number") {
708
+ return Number.isInteger(value) ? formatInteger(value) : formatF64(value);
709
+ }
710
+ return String(value);
711
+ }
712
+ /**
713
+ * Whether a logical edge is written as directed: never in an undirected snapshot; never for a
714
+ * half of an expanded pair (an undirected or a mutual source edge, whose halves the resolver
715
+ * flags directed); otherwise per the directed role column, with directed as the default.
716
+ * @param folding - the pair folding
717
+ * @param e - the edge
718
+ * @returns false for a paired edge or an edge flagged undirected
719
+ */
720
+ function isDirectedEdge(folding, e) {
721
+ return folding.mateOf(e) === INVALID_INDEX && folding.sourceDirected(e);
722
+ }
723
+ /**
724
+ * The containment order: the children CSR over the parent column (src/children.ts), roots in
725
+ * index order, unreachable nodes (cycles) forced to the top level.
726
+ * @param snapshot - the snapshot
727
+ * @returns the hierarchy
728
+ */
729
+ function planHierarchy(snapshot) {
730
+ const parent = snapshot.nodes.byRole("parent");
731
+ const csr = childrenCsr(snapshot, { column: parent !== null && parent.dtype === "u32" ? parent : null });
732
+ const walk = csr.depthFirst();
733
+ // the top-level nodes in written order: the roots, then the cycle members forced out
734
+ const roots = walk.order.filter((u) => walk.depth[u] === 0);
735
+ return {
736
+ parent: csr.column,
737
+ roots,
738
+ childStart: csr.rowPtr,
739
+ childList: csr.children,
740
+ reordered: walk.reordered,
741
+ unreachable: csr.unreachable,
742
+ };
743
+ }
744
+ // ============================================================ writing
745
+ /**
746
+ * The text of one set cell for a `<data>` element or a `<default>`.
747
+ * @param column - the column
748
+ * @param row - the row
749
+ * @param attrType - the declared attr.type
750
+ * @returns the text
751
+ */
752
+ function cellText(column, row, attrType) {
753
+ if (column.meta.components > 1) {
754
+ const value = column.value(row);
755
+ return JSON.stringify(Array.from(value));
756
+ }
757
+ switch (column.dtype) {
758
+ case "bool":
759
+ return column.value(row) === true ? "true" : "false";
760
+ case "i32":
761
+ case "u32":
762
+ case "u8":
763
+ return String(column.data[row]);
764
+ case "f32":
765
+ return formatF32(column.data[row]);
766
+ case "f64": {
767
+ const value = column.data[row];
768
+ return (attrType === "long" || attrType === "int") && Number.isInteger(value)
769
+ ? formatInteger(value)
770
+ : formatF64(value);
771
+ }
772
+ case "string":
773
+ return column.valueAt(row);
774
+ case "dict":
775
+ return column.dictionary[column.codes[row]];
776
+ case "list":
777
+ return JSON.stringify(Array.from(column.sliceOf(row)));
778
+ case "json":
779
+ return JSON.stringify(column.values[row]) ?? "";
780
+ default: {
781
+ const dtype = column;
782
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${String(dtype)}`, {});
783
+ }
784
+ }
785
+ }
786
+ /**
787
+ * The text of a declared default.
788
+ * @param column - the column
789
+ * @param attrType - the declared attr.type
790
+ * @returns the text, or null when the column has no default
791
+ */
792
+ function defaultText(column, attrType) {
793
+ const value = column.meta.default;
794
+ if (value === undefined) {
795
+ return null;
796
+ }
797
+ switch (typeof value) {
798
+ case "boolean":
799
+ return value ? "true" : "false";
800
+ case "number":
801
+ if (column.dtype === "f32") {
802
+ return formatF32(value);
803
+ }
804
+ return (attrType === "long" || attrType === "int") && Number.isInteger(value)
805
+ ? formatInteger(value)
806
+ : formatF64(value);
807
+ case "string":
808
+ return value;
809
+ default:
810
+ return JSON.stringify(value) ?? "";
811
+ }
812
+ }
813
+ /**
814
+ * Write one `<data>` element.
815
+ * @param plan - the column plan
816
+ * @param row - the row
817
+ * @param indent - the indentation
818
+ * @param out - receives the parts
819
+ */
820
+ function writeData(plan, row, indent, out) {
821
+ const { column } = plan;
822
+ if (!column.isSet(row)) {
823
+ return;
824
+ }
825
+ if (plan.yfiles && column.dtype === "json") {
826
+ writeTreeData(plan.keyId, column.values[row], indent, out);
827
+ return;
828
+ }
829
+ out.push(`\n${indent}<data key="${plan.keyId}">${escapeXmlText(cellText(column, row, plan.attrType))}</data>`);
830
+ }
831
+ /**
832
+ * Write a `<data>` holding a yfiles tree.
833
+ * @param keyId - the key id
834
+ * @param value - the tree
835
+ * @param indent - the indentation
836
+ * @param out - receives the parts
837
+ */
838
+ function writeTreeData(keyId, value, indent, out) {
839
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
840
+ const parts = [];
841
+ writeXmlTree(value, indent, parts);
842
+ out.push(`\n${indent}<data key="${keyId}">${parts.join("")}</data>`);
843
+ return;
844
+ }
845
+ out.push(`\n${indent}<data key="${keyId}">`);
846
+ writeXmlTree(value, `${indent} `, out);
847
+ out.push(`\n${indent}</data>`);
848
+ }
849
+ /**
850
+ * The `<key>` element of a plan.
851
+ * @param key - the key plan
852
+ * @param indent - the indentation
853
+ * @returns the element text
854
+ */
855
+ function keyElement(key, indent) {
856
+ const domain = key.domains.length === 3 ? "all" : key.domains[0];
857
+ let text = `${indent}<key id="${escapeXmlAttribute(key.id)}" for="${domain}"`;
858
+ if (key.attrName !== null) {
859
+ text += ` attr.name="${escapeXmlAttribute(key.attrName)}"`;
860
+ }
861
+ if (key.attrType !== null) {
862
+ text += ` attr.type="${escapeXmlAttribute(key.attrType)}"`;
863
+ }
864
+ if (key.yfilesType !== null) {
865
+ text += ` yfiles.type="${escapeXmlAttribute(key.yfilesType)}"`;
866
+ }
867
+ const { column } = key;
868
+ const desc = column?.meta.extra.desc;
869
+ const hasDefault = column !== null && column.meta.default !== undefined;
870
+ if (column === null || (typeof desc !== "string" && !hasDefault)) {
871
+ return `${text}/>\n`;
872
+ }
873
+ const parts = [`${text}>`];
874
+ const inner = `${indent} `;
875
+ if (typeof desc === "string") {
876
+ parts.push(`\n${inner}<desc>${escapeXmlText(desc)}</desc>`);
877
+ }
878
+ if (hasDefault) {
879
+ if (key.yfilesType !== null) {
880
+ const value = column.meta.default;
881
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
882
+ parts.push(`\n${inner}<default>`);
883
+ writeXmlTree(value, `${inner} `, parts);
884
+ parts.push(`\n${inner}</default>`);
885
+ }
886
+ else {
887
+ const tree = [];
888
+ writeXmlTree(value, inner, tree);
889
+ parts.push(`\n${inner}<default>${tree.join("")}</default>`);
890
+ }
891
+ }
892
+ else {
893
+ parts.push(`\n${inner}<default>${escapeXmlText(defaultText(column, key.attrType) ?? "")}</default>`);
894
+ }
895
+ }
896
+ parts.push(`\n${indent}</key>\n`);
897
+ return parts.join("");
898
+ }
899
+ /**
900
+ * Sanitised edge ids: NMTOKENs as they are; the rest refused or mangled per sanitizeIds.
901
+ * @param column - the edge id column, or null
902
+ * @param mode - the resolved sanitizeIds option
903
+ * @param edgeCount - the edge count
904
+ * @returns the id text per edge (null for edges without one)
905
+ */
906
+ function sanitizeEdgeIds(column, mode, edgeCount) {
907
+ const out = new Array(edgeCount).fill(null);
908
+ if (column === null) {
909
+ return out;
910
+ }
911
+ const used = new Set();
912
+ const bad = [];
913
+ for (let e = 0; e < edgeCount; e++) {
914
+ if (!column.isSet(e)) {
915
+ continue;
916
+ }
917
+ const text = edgeIdText(column, e);
918
+ if (isNmtoken(text)) {
919
+ out[e] = text;
920
+ used.add(text);
921
+ }
922
+ else {
923
+ bad.push(e);
924
+ }
925
+ }
926
+ if (bad.length > 0 && mode === "error") {
927
+ throw new GraphFormatError("E_INVALID_ID", `${bad.length} edge id(s) cannot be written as nmtoken (first: ${JSON.stringify(edgeIdText(column, bad[0]))} at edge ${bad[0]}); pass sanitizeIds: "mangle" to rewrite them`, { reason: "charset", charset: "nmtoken", count: bad.length, edge: bad[0] });
928
+ }
929
+ for (const e of bad) {
930
+ const base = mangleNmtoken(edgeIdText(column, e));
931
+ let candidate = base;
932
+ for (let k = 2; used.has(candidate); k++) {
933
+ candidate = `${base}_${k}`;
934
+ }
935
+ used.add(candidate);
936
+ out[e] = candidate;
937
+ }
938
+ return out;
939
+ }
940
+ /**
941
+ * The document as text parts.
942
+ * @param snapshot - the snapshot
943
+ * @param plan - the plan
944
+ * @yields one element (or a group of small ones) at a time
945
+ * @returns nothing
946
+ */
947
+ function* writeGraphml(snapshot, plan) {
948
+ const ids = sanitizeIds(snapshot, "nmtoken", plan.options.sanitizeIds);
949
+ const edgeIds = sanitizeEdgeIds(plan.edgeIdColumn, plan.options.sanitizeIds, snapshot.edgeCount);
950
+ const i1 = plan.pretty ? " " : "";
951
+ const i2 = plan.pretty ? " " : "";
952
+ const i3 = plan.pretty ? " " : "";
953
+ const step = plan.pretty ? " " : "";
954
+ yield '<?xml version="1.0" encoding="UTF-8"?>\n';
955
+ let root = `<graphml xmlns="${GRAPHML_NAMESPACE}"`;
956
+ const namespaces = { ...(plan.meta?.namespaces ?? {}) };
957
+ if ((plan.nodeColumns.some((c) => c.yfiles) ||
958
+ plan.edgeColumns.some((c) => c.yfiles) ||
959
+ plan.graphColumns.some((c) => c.yfiles)) &&
960
+ !Object.values(namespaces).includes(YFILES_NAMESPACE)) {
961
+ namespaces.y = YFILES_NAMESPACE;
962
+ }
963
+ if (!Object.values(namespaces).includes(XSI_NAMESPACE)) {
964
+ namespaces.xsi = XSI_NAMESPACE;
965
+ }
966
+ for (const [prefix, uri] of Object.entries(namespaces)) {
967
+ root += ` xmlns:${prefix}="${escapeXmlAttribute(uri)}"`;
968
+ }
969
+ const xsiPrefix = Object.entries(namespaces).find(([, uri]) => uri === XSI_NAMESPACE)?.[0] ?? "xsi";
970
+ root += ` ${xsiPrefix}:schemaLocation="${SCHEMA_LOCATION}">\n`;
971
+ yield root;
972
+ for (const key of plan.keys) {
973
+ yield keyElement(key, i1);
974
+ }
975
+ if (plan.originalIdKey !== null) {
976
+ yield `${i1}<key id="${plan.originalIdKey}" for="node" attr.name="${ORIGINAL_ID_ATTRIBUTE}" attr.type="string"/>\n`;
977
+ }
978
+ const graphId = plan.meta?.graphId ?? "G";
979
+ yield `${i1}<graph id="${escapeXmlAttribute(graphId)}" edgedefault="${plan.edgedefault}">`;
980
+ const { description } = snapshot.meta;
981
+ if (description !== null) {
982
+ yield `\n${i2}<desc>${escapeXmlText(description)}</desc>`;
983
+ }
984
+ const graphParts = [];
985
+ for (const column of plan.graphColumns) {
986
+ writeData(column, 0, i2, graphParts);
987
+ }
988
+ if (graphParts.length > 0) {
989
+ yield graphParts.join("");
990
+ }
991
+ // nodes in containment order, children nested under their parent
992
+ const { hierarchy } = plan;
993
+ const nested = hierarchy.childList.length > 0;
994
+ const stackNode = [];
995
+ const stackCursor = [];
996
+ const written = nested ? new Uint8Array(snapshot.nodeCount) : null;
997
+ for (const rootNode of hierarchy.roots) {
998
+ yield* writeNodeSubtree(rootNode, plan, ids, i2, step, stackNode, stackCursor, written);
999
+ }
1000
+ // edges: primaries only, in index order
1001
+ const list = snapshot.edgeList();
1002
+ const { folding } = plan;
1003
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1004
+ if (folding.folded(e)) {
1005
+ continue;
1006
+ }
1007
+ const parts = [`\n${i2}<edge`];
1008
+ const id = edgeIds[e];
1009
+ if (id !== null) {
1010
+ parts.push(` id="${escapeXmlAttribute(id)}"`);
1011
+ }
1012
+ parts.push(` source="${escapeXmlAttribute(String(ids.idAt(list.src[e])))}" target="${escapeXmlAttribute(String(ids.idAt(list.dst[e])))}"`);
1013
+ const directed = snapshot.directed && isDirectedEdge(folding, e);
1014
+ if (directed !== (plan.edgedefault === "directed")) {
1015
+ parts.push(` directed="${directed ? "true" : "false"}"`);
1016
+ }
1017
+ const sourcePort = portText(plan.sourcePortColumn, e);
1018
+ if (sourcePort !== null) {
1019
+ parts.push(` sourceport="${escapeXmlAttribute(sourcePort)}"`);
1020
+ }
1021
+ const targetPort = portText(plan.targetPortColumn, e);
1022
+ if (targetPort !== null) {
1023
+ parts.push(` targetport="${escapeXmlAttribute(targetPort)}"`);
1024
+ }
1025
+ const open = parts.length;
1026
+ if (plan.weight !== null) {
1027
+ const { keyId, attrType } = plan.weight;
1028
+ const weight = plan.weight.weights.text(e, attrType === "int" || attrType === "long");
1029
+ if (weight !== null) {
1030
+ parts.push(`\n${i3}<data key="${keyId}">${escapeXmlText(weight)}</data>`);
1031
+ }
1032
+ }
1033
+ for (const column of plan.edgeColumns) {
1034
+ writeData(column, e, i3, parts);
1035
+ }
1036
+ if (parts.length === open) {
1037
+ parts.push("/>");
1038
+ }
1039
+ else {
1040
+ parts.splice(open, 0, ">");
1041
+ parts.push(`\n${i2}</edge>`);
1042
+ }
1043
+ yield parts.join("");
1044
+ }
1045
+ yield `\n${i1}</graph>\n</graphml>\n`;
1046
+ }
1047
+ /**
1048
+ * The text of a port reference cell.
1049
+ * @param column - the sourcePort / targetPort column, or null
1050
+ * @param e - the edge
1051
+ * @returns the port name, or null when unset
1052
+ */
1053
+ function portText(column, e) {
1054
+ if (column === null || !column.isSet(e)) {
1055
+ return null;
1056
+ }
1057
+ const value = column.value(e);
1058
+ return typeof value === "string" ? value : String(value);
1059
+ }
1060
+ /**
1061
+ * Write a top-level node and, nested, every node contained in it (iteratively, so a deep chain
1062
+ * cannot overflow the call stack).
1063
+ * @param root - the top-level node
1064
+ * @param plan - the plan
1065
+ * @param ids - the sanitised ids
1066
+ * @param indent - the indentation of top-level nodes
1067
+ * @param step - one indentation step
1068
+ * @param stackNode - a reusable stack of open nodes
1069
+ * @param stackCursor - a reusable stack of child cursors
1070
+ * @param written - nodes already written (null when nothing is nested)
1071
+ * @yields the node elements
1072
+ * @returns nothing
1073
+ */
1074
+ function* writeNodeSubtree(root, plan, ids, indent, step, stackNode, stackCursor, written) {
1075
+ const { hierarchy } = plan;
1076
+ const hasChildren = (u) => hierarchy.childStart[u + 1] > hierarchy.childStart[u];
1077
+ const openNode = (u, depth) => {
1078
+ const pad = indent + step.repeat(depth * 2);
1079
+ const parts = [`\n${pad}<node id="${escapeXmlAttribute(String(ids.idAt(u)))}"`];
1080
+ const open = parts.length;
1081
+ if (plan.originalIdKey !== null && ids.isChanged(u)) {
1082
+ parts.push(`\n${pad}${step}<data key="${plan.originalIdKey}">${escapeXmlText(String(ids.originalAt(u)))}</data>`);
1083
+ }
1084
+ for (const column of plan.nodeColumns) {
1085
+ writeData(column, u, pad + step, parts);
1086
+ }
1087
+ if (hasChildren(u)) {
1088
+ parts.splice(open, 0, ">");
1089
+ parts.push(`\n${pad}${step}<graph id="${escapeXmlAttribute(`${String(ids.idAt(u))}:`)}" edgedefault="${plan.edgedefault}">`);
1090
+ return parts.join("");
1091
+ }
1092
+ if (parts.length === open) {
1093
+ parts.push("/>");
1094
+ }
1095
+ else {
1096
+ parts.splice(open, 0, ">");
1097
+ parts.push(`\n${pad}</node>`);
1098
+ }
1099
+ return parts.join("");
1100
+ };
1101
+ const closeNode = (depth) => {
1102
+ const pad = indent + step.repeat(depth * 2);
1103
+ return `\n${pad}${step}</graph>\n${pad}</node>`;
1104
+ };
1105
+ if (written !== null) {
1106
+ written[root] = 1;
1107
+ }
1108
+ yield openNode(root, 0);
1109
+ if (!hasChildren(root)) {
1110
+ return;
1111
+ }
1112
+ stackNode.push(root);
1113
+ stackCursor.push(hierarchy.childStart[root]);
1114
+ while (stackNode.length > 0) {
1115
+ const top = stackNode.length - 1;
1116
+ const u = stackNode[top];
1117
+ const cursor = stackCursor[top];
1118
+ if (cursor >= hierarchy.childStart[u + 1]) {
1119
+ stackNode.pop();
1120
+ stackCursor.pop();
1121
+ yield closeNode(top);
1122
+ continue;
1123
+ }
1124
+ stackCursor[top] = cursor + 1;
1125
+ const v = hierarchy.childList[cursor];
1126
+ if (written !== null && written[v] === 1) {
1127
+ continue;
1128
+ }
1129
+ if (written !== null) {
1130
+ written[v] = 1;
1131
+ }
1132
+ yield openNode(v, top + 1);
1133
+ if (hasChildren(v)) {
1134
+ stackNode.push(v);
1135
+ stackCursor.push(hierarchy.childStart[v]);
1136
+ }
1137
+ }
1138
+ }
1139
+ /**
1140
+ * The GraphML exporter (design section 8.5).
1141
+ */
1142
+ export const graphmlExporter = Object.freeze({
1143
+ format: FORMAT,
1144
+ capabilities: CAPABILITIES,
1145
+ /**
1146
+ * Pre-flight: every loss the export would incur.
1147
+ * @param snapshot - the snapshot
1148
+ * @param options - format-specific and common options
1149
+ * @returns the notes
1150
+ */
1151
+ check(snapshot, options) {
1152
+ const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
1153
+ return Object.freeze(plan.notes);
1154
+ },
1155
+ /**
1156
+ * Write the document as UTF-8 chunks.
1157
+ * @param snapshot - the snapshot
1158
+ * @param options - format-specific and common options
1159
+ * @returns the chunks
1160
+ */
1161
+ export(snapshot, options) {
1162
+ const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
1163
+ return encodeChunks(writeGraphml(snapshot, plan));
1164
+ },
1165
+ /**
1166
+ * Write the document as one string.
1167
+ * @param snapshot - the snapshot
1168
+ * @param options - format-specific and common options
1169
+ * @returns the document
1170
+ */
1171
+ exportToString(snapshot, options) {
1172
+ const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
1173
+ return joinText(writeGraphml(snapshot, plan));
1174
+ },
1175
+ });
1176
+ //# sourceMappingURL=exporter.js.map