@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,8 @@
1
+ /**
2
+ * The `@graphty/graph-io/graphml` subpath: the GraphML importer and exporter (design section 8.2),
3
+ * their option types and their issue and loss-note code tables.
4
+ */
5
+
6
+ export { GRAPHML_ISSUE, GRAPHML_LOSS } from "./constants.js";
7
+ export { graphmlExporter, type GraphmlExportOptions } from "./exporter.js";
8
+ export { graphmlImporter, type GraphmlImportOptions } from "./importer.js";
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Nested XML inside a GraphML `<data>` element (yFiles / yEd `y:ShapeNode`, `y:PolyLineEdge`,
3
+ * ...) as a JSON tree, the value of a `json` column with `origin.namespace: "yfiles"` (design
4
+ * section 8.5: structure preserved, not byte-exact). The shape follows the fast-xml-parser
5
+ * convention graphty-element's parser already navigates: an element is an object whose
6
+ * `@_<name>` keys are its attributes, whose `#text` key is its non-whitespace character data
7
+ * when it also has attributes or children, and whose other keys are its child elements (an
8
+ * array when a name repeats); an element with neither attributes nor children is its text
9
+ * (an empty string for an empty element).
10
+ *
11
+ * The builder consumes the tokenizer events of one subtree; the writer produces the XML text of
12
+ * a tree again, so a yFiles column re-exports as the same nested markup.
13
+ */
14
+
15
+ import { GraphFormatError } from "@graphty/graph-format";
16
+
17
+ import { escapeXmlAttribute, escapeXmlText } from "../../common/escape.js";
18
+ import { isWhitespace, isXmlName } from "../../common/xml.js";
19
+
20
+ /** The prefix of an attribute key in a tree object. */
21
+ const ATTRIBUTE_PREFIX = "@_";
22
+
23
+ /** The key of an element's character data when it also has attributes or children. */
24
+ const TEXT_KEY = "#text";
25
+
26
+ /** A tree object: attributes, text and child elements by name. */
27
+ type XmlTreeObject = Record<string, unknown>;
28
+
29
+ /** A frame of the builder: one open element. */
30
+ interface Frame {
31
+ readonly name: string;
32
+ readonly node: XmlTreeObject;
33
+ hasAttrs: boolean;
34
+ hasChildren: boolean;
35
+ text: string;
36
+ }
37
+
38
+ /**
39
+ * Builds the tree of one subtree from start / end / text events. The outermost frame is the
40
+ * `<data>` element itself; `finish()` returns its content: the object of its children, or its
41
+ * text when it has none.
42
+ */
43
+ export class XmlTreeBuilder {
44
+ private readonly frames: Frame[] = [];
45
+
46
+ private readonly root: Frame;
47
+
48
+ /** Create a builder whose implicit root is the containing element. */
49
+ constructor() {
50
+ this.root = { name: "", node: {}, hasAttrs: false, hasChildren: false, text: "" };
51
+ }
52
+
53
+ /**
54
+ * Whether the builder is inside a child element (as opposed to at the root level).
55
+ * @returns true while at least one child element is open
56
+ */
57
+ get open(): boolean {
58
+ return this.frames.length > 0;
59
+ }
60
+
61
+ /**
62
+ * An element starts.
63
+ * @param name - the element name as written
64
+ * @param attrs - its attributes
65
+ */
66
+ start(name: string, attrs: ReadonlyMap<string, string>): void {
67
+ const node: XmlTreeObject = {};
68
+ let hasAttrs = false;
69
+ for (const [key, value] of attrs) {
70
+ node[ATTRIBUTE_PREFIX + key] = value;
71
+ hasAttrs = true;
72
+ }
73
+ this.frames.push({ name, node, hasAttrs, hasChildren: false, text: "" });
74
+ }
75
+
76
+ /**
77
+ * Character data inside the current element.
78
+ * @param text - the text
79
+ */
80
+ text(text: string): void {
81
+ const frame = this.frames.length > 0 ? this.frames[this.frames.length - 1] : this.root;
82
+ frame.text += text;
83
+ }
84
+
85
+ /** The current element ends; its value is attached to its parent. */
86
+ end(): void {
87
+ const frame = this.frames.pop();
88
+ if (frame === undefined) {
89
+ throw new GraphFormatError("E_COLUMN_TYPE", "XmlTreeBuilder.end() without a matching start()", {
90
+ reason: "tree builder underflow",
91
+ });
92
+ }
93
+ const parent = this.frames.length > 0 ? this.frames[this.frames.length - 1] : this.root;
94
+ attachChild(parent, frame.name, valueOf(frame));
95
+ }
96
+
97
+ /**
98
+ * The content of the containing element: its text when it has no child elements, otherwise
99
+ * the object of its children (with `#text` for non-whitespace mixed content).
100
+ * @returns the value to store in the json column
101
+ */
102
+ finish(): unknown {
103
+ if (this.frames.length > 0) {
104
+ throw new GraphFormatError("E_COLUMN_TYPE", "XmlTreeBuilder.finish() with open elements", {
105
+ reason: "tree builder open",
106
+ });
107
+ }
108
+ const { root } = this;
109
+ if (!root.hasChildren) {
110
+ return root.text;
111
+ }
112
+ if (!isWhitespace(root.text)) {
113
+ root.node[TEXT_KEY] = root.text;
114
+ }
115
+ return root.node;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * The value of a finished element: its text when it has neither attributes nor children,
121
+ * else its node with `#text` set to non-whitespace text.
122
+ * @param frame - the finished frame
123
+ * @returns the value
124
+ */
125
+ function valueOf(frame: Frame): unknown {
126
+ if (!frame.hasAttrs && !frame.hasChildren) {
127
+ return frame.text;
128
+ }
129
+ if (!isWhitespace(frame.text)) {
130
+ frame.node[TEXT_KEY] = frame.text;
131
+ }
132
+ return frame.node;
133
+ }
134
+
135
+ /**
136
+ * Attach a child value under a name, turning a repeated name into an array.
137
+ * @param parent - the parent frame
138
+ * @param name - the child element name
139
+ * @param value - the child value
140
+ */
141
+ function attachChild(parent: Frame, name: string, value: unknown): void {
142
+ parent.hasChildren = true;
143
+ const existing = parent.node[name];
144
+ if (existing === undefined) {
145
+ parent.node[name] = value;
146
+ } else if (Array.isArray(existing)) {
147
+ existing.push(value);
148
+ } else {
149
+ parent.node[name] = [existing, value];
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Whether a value is a tree the writer can serialise: a string, number, boolean or null (text),
155
+ * an array of such trees, or an object whose keys are `#text`, `@_<Name>` or `<Name>` with tree
156
+ * values.
157
+ * @param value - the value
158
+ * @returns true when writeXmlTree() accepts it
159
+ */
160
+ export function isXmlTree(value: unknown): boolean {
161
+ return treeProblem(value) === null;
162
+ }
163
+
164
+ /**
165
+ * Why a value is not a serialisable tree.
166
+ * @param value - the value
167
+ * @returns a message, or null when the value is a tree
168
+ */
169
+ export function treeProblem(value: unknown): string | null {
170
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
171
+ return null;
172
+ }
173
+ if (Array.isArray(value)) {
174
+ for (const item of value) {
175
+ if (Array.isArray(item)) {
176
+ return "nested arrays cannot be written as elements";
177
+ }
178
+ const problem = treeProblem(item);
179
+ if (problem !== null) {
180
+ return problem;
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+ if (typeof value !== "object") {
186
+ return `a ${typeof value} cannot be written as XML`;
187
+ }
188
+ for (const [key, child] of Object.entries(value)) {
189
+ if (key === TEXT_KEY) {
190
+ if (child !== null && typeof child === "object") {
191
+ return "#text must be a scalar";
192
+ }
193
+ continue;
194
+ }
195
+ if (key.startsWith(ATTRIBUTE_PREFIX)) {
196
+ const name = key.slice(ATTRIBUTE_PREFIX.length);
197
+ if (!isXmlName(name)) {
198
+ return `"${name}" is not an XML attribute name`;
199
+ }
200
+ if (child !== null && typeof child === "object") {
201
+ return `attribute ${name} must be a scalar`;
202
+ }
203
+ continue;
204
+ }
205
+ if (!isXmlName(key)) {
206
+ return `"${key}" is not an XML element name`;
207
+ }
208
+ const problem = treeProblem(child);
209
+ if (problem !== null) {
210
+ return problem;
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * Write the content of a tree as XML text: the children (and mixed text) of the containing
218
+ * element. Leaf elements are written inline so their text stays exact; elements with children
219
+ * are broken over lines and indented.
220
+ * @param value - the tree (the content of a `<data>` element)
221
+ * @param indent - the indentation of the containing element's children
222
+ * @param out - receives the text parts
223
+ */
224
+ export function writeXmlTree(value: unknown, indent: string, out: string[]): void {
225
+ const problem = treeProblem(value);
226
+ if (problem !== null) {
227
+ throw new GraphFormatError("E_COLUMN_TYPE", `a yfiles value cannot be written as XML: ${problem}`, {
228
+ reason: "xml tree",
229
+ });
230
+ }
231
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
232
+ out.push(escapeXmlText(scalarText(value)));
233
+ return;
234
+ }
235
+ const object = value as XmlTreeObject;
236
+ let text: string | null = null;
237
+ for (const [key, child] of Object.entries(object)) {
238
+ if (key === TEXT_KEY) {
239
+ text = scalarText(child);
240
+ } else if (!key.startsWith(ATTRIBUTE_PREFIX)) {
241
+ writeElements(key, child, indent, out);
242
+ }
243
+ }
244
+ if (text !== null) {
245
+ out.push(escapeXmlText(text));
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Write one element (or, for an array, one element per item).
251
+ * @param name - the element name
252
+ * @param value - the element value
253
+ * @param indent - the indentation of this element
254
+ * @param out - receives the text parts
255
+ */
256
+ function writeElements(name: string, value: unknown, indent: string, out: string[]): void {
257
+ if (Array.isArray(value)) {
258
+ for (const item of value) {
259
+ writeElements(name, item, indent, out);
260
+ }
261
+ return;
262
+ }
263
+ if (value === null || typeof value !== "object") {
264
+ const text = scalarText(value);
265
+ out.push(text.length === 0 ? `\n${indent}<${name}/>` : `\n${indent}<${name}>${escapeXmlText(text)}</${name}>`);
266
+ return;
267
+ }
268
+ const object = value as XmlTreeObject;
269
+ let attrs = "";
270
+ let text: string | null = null;
271
+ let hasChildren = false;
272
+ for (const [key, child] of Object.entries(object)) {
273
+ if (key === TEXT_KEY) {
274
+ text = scalarText(child);
275
+ } else if (key.startsWith(ATTRIBUTE_PREFIX)) {
276
+ attrs += ` ${key.slice(ATTRIBUTE_PREFIX.length)}="${escapeXmlAttribute(scalarText(child))}"`;
277
+ } else {
278
+ hasChildren = true;
279
+ }
280
+ }
281
+ if (!hasChildren) {
282
+ out.push(
283
+ text === null || text.length === 0
284
+ ? `\n${indent}<${name}${attrs}/>`
285
+ : `\n${indent}<${name}${attrs}>${escapeXmlText(text)}</${name}>`,
286
+ );
287
+ return;
288
+ }
289
+ out.push(`\n${indent}<${name}${attrs}>`);
290
+ const inner = `${indent} `;
291
+ for (const [key, child] of Object.entries(object)) {
292
+ if (key !== TEXT_KEY && !key.startsWith(ATTRIBUTE_PREFIX)) {
293
+ writeElements(key, child, inner, out);
294
+ }
295
+ }
296
+ if (text !== null) {
297
+ out.push(escapeXmlText(text));
298
+ }
299
+ out.push(`\n${indent}</${name}>`);
300
+ }
301
+
302
+ /**
303
+ * The text of a scalar tree value.
304
+ * @param value - a string, number, boolean or null
305
+ * @returns the text; an empty string for null
306
+ */
307
+ function scalarText(value: unknown): string {
308
+ switch (typeof value) {
309
+ case "string":
310
+ return value;
311
+ case "number":
312
+ case "boolean":
313
+ case "bigint":
314
+ return String(value);
315
+ default:
316
+ return "";
317
+ }
318
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * What the JSON importer and exporter share (design sections 8.2 and 8.5): the dialect names, the
3
+ * reserved `meta.extra.json` shape record the importer writes and the exporter reads back, the
4
+ * per-dialect capability tables, the fixed key sets of each dialect and the small JSON value
5
+ * helpers both sides use.
6
+ *
7
+ * Dialects: NetworkX node-link (`nodes` + `links` / `edges`, graph-level `directed` / `multigraph`
8
+ * / `graph`), the d3 lineage of the same shape (`links`, `name` ids, integer index endpoints),
9
+ * JSON Graph Format v2 (`graph.nodes` keyed by id, per-edge `directed`, hyperedges), Cytoscape.js
10
+ * elements (`data.id` / `data.source` / `data.target`, `position`, `classes`, `data.parent`),
11
+ * graphology serialisation (`key` / `attributes`, `undirected` edges, `options.type` / `multi`) and
12
+ * vis.js (`from` / `to`).
13
+ */
14
+
15
+ import { type GraphMeta } from "@graphty/graph-format";
16
+
17
+ import { capabilities } from "../../common/export.js";
18
+ import { type ExportCapabilities } from "../../types.js";
19
+
20
+ /** The JSON dialects the plugin reads and writes. */
21
+ export type JsonDialect = "node-link" | "d3" | "jgf" | "cytoscape" | "graphology" | "vis";
22
+
23
+ /** Every dialect name, for option checking and messages. */
24
+ export const JSON_DIALECTS: readonly JsonDialect[] = Object.freeze([
25
+ "node-link",
26
+ "d3",
27
+ "jgf",
28
+ "cytoscape",
29
+ "graphology",
30
+ "vis",
31
+ ]);
32
+
33
+ /** The key under `meta.extra` that holds the shape record (design section 8.5). */
34
+ export const META_KEY = "json";
35
+
36
+ /**
37
+ * The shape information the importer records under `meta.extra.json` so the exporter can write the
38
+ * same file back (design section 8.5). Every field is optional because a snapshot may come from
39
+ * another format or from an older record.
40
+ */
41
+ export interface JsonShapeMeta {
42
+ /** The dialect the file was read as. */
43
+ readonly dialect?: JsonDialect | undefined;
44
+ /** node-link / d3: the key the edge array was under ("links" or "edges"). */
45
+ readonly edgesKey?: string | undefined;
46
+ /** node-link / d3: the key holding the node id ("id", "name", ...); null when nodes were positional. */
47
+ readonly nodeIdKey?: string | null | undefined;
48
+ /** node-link / d3: whether edge endpoints were array positions rather than ids. */
49
+ readonly indexLinks?: boolean | undefined;
50
+ /** node-link / d3 / vis: the endpoint keys the file used. */
51
+ readonly sourceKey?: string | undefined;
52
+ /** node-link / d3 / vis: the endpoint keys the file used. */
53
+ readonly targetKey?: string | undefined;
54
+ /** jgf: the graph `id`. */
55
+ readonly id?: string | undefined;
56
+ /** jgf: the graph `type`. */
57
+ readonly type?: string | undefined;
58
+ /** graphology: `options.allowSelfLoops` as declared. */
59
+ readonly allowSelfLoops?: boolean | undefined;
60
+ /** cytoscape: every top-level key besides `elements` and `data` (style, zoom, pan, ...), verbatim. */
61
+ readonly cytoscape?: Readonly<Record<string, unknown>> | undefined;
62
+ }
63
+
64
+ /**
65
+ * Whether a value is a plain JSON object (not null, not an array).
66
+ * @param value - any value
67
+ * @returns true for an object that is not an array
68
+ */
69
+ export function isJsonObject(value: unknown): value is Record<string, unknown> {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+
73
+ /**
74
+ * Whether a record has an own property; JSON.parse output is checked with hasOwnProperty so a key
75
+ * named "constructor" or "__proto__" is never read from the prototype.
76
+ * @param record - the record
77
+ * @param key - the key
78
+ * @returns true when the key is an own property
79
+ */
80
+ export function hasKey(record: Readonly<Record<string, unknown>>, key: string): boolean {
81
+ return Object.prototype.hasOwnProperty.call(record, key);
82
+ }
83
+
84
+ /**
85
+ * Whether a text names a dialect.
86
+ * @param value - any value
87
+ * @returns true for one of JSON_DIALECTS
88
+ */
89
+ export function isJsonDialect(value: unknown): value is JsonDialect {
90
+ return typeof value === "string" && (JSON_DIALECTS as readonly string[]).includes(value);
91
+ }
92
+
93
+ /**
94
+ * The dialect of a parsed JSON document, by the shape rules of design section 8.2 (Cytoscape:
95
+ * `elements` or a top-level array of `{ data }` elements; JGF: `graph.nodes` / `graph.edges` or
96
+ * `graphs[]`; graphology: `options.type` / `options.multi`, `key` nodes without `id`, edges with
97
+ * `undirected` or an `attributes` record; vis: edges with `from` / `to`; d3: `links` without
98
+ * `directed` / `multigraph` / `graph`; else node-link). Pure: the importer wraps it with its issue
99
+ * codes, the registry's sniff() uses it on a head that parses as a whole document.
100
+ * @param root - the parsed document
101
+ * @returns the dialect, or null when the document is not a graph document in any dialect
102
+ */
103
+ export function sniffJsonDialect(root: unknown): JsonDialect | null {
104
+ if (Array.isArray(root)) {
105
+ const first = firstJsonObject(root);
106
+ return root.length === 0 || (first !== null && isJsonObject(first.data)) ? "cytoscape" : null;
107
+ }
108
+ if (!isJsonObject(root)) {
109
+ return null;
110
+ }
111
+ if (hasKey(root, "elements")) {
112
+ return "cytoscape";
113
+ }
114
+ if (isJsonObject(root.graph) && (hasKey(root.graph, "nodes") || hasKey(root.graph, "edges"))) {
115
+ return "jgf";
116
+ }
117
+ if (Array.isArray(root.graphs)) {
118
+ return "jgf";
119
+ }
120
+ if (!hasKey(root, "nodes") && !hasKey(root, "edges") && !hasKey(root, "links")) {
121
+ return null;
122
+ }
123
+ const firstNode = firstJsonObject(root.nodes);
124
+ const firstEdge = firstJsonObject(hasKey(root, "edges") ? root.edges : root.links);
125
+ if (isJsonObject(root.options) && (hasKey(root.options, "type") || hasKey(root.options, "multi"))) {
126
+ return "graphology";
127
+ }
128
+ if (firstNode !== null && hasKey(firstNode, "key") && !hasKey(firstNode, "id")) {
129
+ return "graphology";
130
+ }
131
+ if (firstEdge !== null && !hasKey(firstEdge, "from")) {
132
+ if (hasKey(firstEdge, "undirected") || isJsonObject(firstEdge.attributes)) {
133
+ return "graphology";
134
+ }
135
+ }
136
+ if (firstEdge !== null && hasKey(firstEdge, "from") && hasKey(firstEdge, "to") && !hasKey(firstEdge, "source")) {
137
+ return "vis";
138
+ }
139
+ const bare = !hasKey(root, "directed") && !hasKey(root, "multigraph") && !hasKey(root, "graph");
140
+ return bare && hasKey(root, "links") ? "d3" : "node-link";
141
+ }
142
+
143
+ /**
144
+ * The first object of an array, or null.
145
+ * @param value - maybe an array
146
+ * @returns the first element that is an object
147
+ */
148
+ function firstJsonObject(value: unknown): Record<string, unknown> | null {
149
+ if (!Array.isArray(value)) {
150
+ return null;
151
+ }
152
+ const found: unknown = value.find((item) => isJsonObject(item));
153
+ return isJsonObject(found) ? found : null;
154
+ }
155
+
156
+ /**
157
+ * The shape record of a snapshot's metadata, or an empty record when the snapshot did not come
158
+ * from the JSON importer.
159
+ * @param meta - the snapshot's metadata
160
+ * @returns the fields found under `meta.extra.json`, each only when it has the expected type
161
+ */
162
+ export function shapeMetaOf(meta: GraphMeta): JsonShapeMeta {
163
+ const raw: unknown = meta.extra[META_KEY];
164
+ if (!isJsonObject(raw)) {
165
+ return {};
166
+ }
167
+ const text = (key: string): string | undefined => (typeof raw[key] === "string" ? raw[key] : undefined);
168
+ const flag = (key: string): boolean | undefined => (typeof raw[key] === "boolean" ? raw[key] : undefined);
169
+ const { nodeIdKey } = raw;
170
+ return {
171
+ dialect: isJsonDialect(raw.dialect) ? raw.dialect : undefined,
172
+ edgesKey: text("edgesKey"),
173
+ nodeIdKey: nodeIdKey === null || typeof nodeIdKey === "string" ? nodeIdKey : undefined,
174
+ indexLinks: flag("indexLinks"),
175
+ sourceKey: text("sourceKey"),
176
+ targetKey: text("targetKey"),
177
+ id: text("id"),
178
+ type: text("type"),
179
+ allowSelfLoops: flag("allowSelfLoops"),
180
+ cytoscape: isJsonObject(raw.cytoscape) ? raw.cytoscape : undefined,
181
+ };
182
+ }
183
+
184
+ /** The direction a dialect assumes when the file declares none (design section 8.4, `defaultDirected`). */
185
+ export const DIALECT_DEFAULT_DIRECTED: Readonly<Record<JsonDialect, boolean>> = Object.freeze({
186
+ "node-link": false,
187
+ d3: false,
188
+ jgf: true,
189
+ cytoscape: true,
190
+ graphology: true,
191
+ vis: false,
192
+ });
193
+
194
+ /**
195
+ * The dtypes a JSON dialect keeps as declared: JSON declares no types, so a value reads back as
196
+ * what the importer's inference gives it (design section 5.1: integral numbers i32, other numbers
197
+ * f64, booleans, strings). f32 values re-read as f64 differ from the f32 ones, u32 / u8 come back
198
+ * i32, dict comes back string, a multi-component or list value comes back json.
199
+ */
200
+ const JSON_DTYPES = Object.freeze(["f64", "i32", "bool", "string"] as const);
201
+
202
+ const COMMON = {
203
+ multiEdges: true,
204
+ selfLoops: true,
205
+ idCharset: "any",
206
+ dtypes: JSON_DTYPES,
207
+ components: false,
208
+ lists: false,
209
+ json: true,
210
+ defaults: false,
211
+ options: false,
212
+ temporal: "none",
213
+ positions: false,
214
+ viz: false,
215
+ } as const;
216
+
217
+ const TABLES: Readonly<Record<JsonDialect, ExportCapabilities>> = Object.freeze({
218
+ "node-link": capabilities({
219
+ ...COMMON,
220
+ mixedDirection: false,
221
+ edgeIds: "none",
222
+ hierarchy: false,
223
+ graphAttributes: true,
224
+ }),
225
+ d3: capabilities({
226
+ ...COMMON,
227
+ mixedDirection: false,
228
+ edgeIds: "none",
229
+ hierarchy: false,
230
+ graphAttributes: false,
231
+ }),
232
+ jgf: capabilities({
233
+ ...COMMON,
234
+ mixedDirection: true,
235
+ edgeIds: "optional",
236
+ hierarchy: false,
237
+ graphAttributes: true,
238
+ }),
239
+ cytoscape: capabilities({
240
+ ...COMMON,
241
+ positions: true,
242
+ mixedDirection: false,
243
+ edgeIds: "required",
244
+ hierarchy: true,
245
+ graphAttributes: true,
246
+ }),
247
+ graphology: capabilities({
248
+ ...COMMON,
249
+ mixedDirection: true,
250
+ edgeIds: "optional",
251
+ hierarchy: false,
252
+ graphAttributes: true,
253
+ }),
254
+ vis: capabilities({
255
+ ...COMMON,
256
+ mixedDirection: false,
257
+ edgeIds: "optional",
258
+ hierarchy: false,
259
+ graphAttributes: false,
260
+ }),
261
+ });
262
+
263
+ /**
264
+ * What one dialect can express (design section 8.5): positions and visual columns are written as
265
+ * plain attributes (an array for a multi-component column) and read back without their role,
266
+ * except Cytoscape's `position` object; containment only as the Cytoscape `data.parent`; mixed
267
+ * direction only where the dialect carries a per-edge flag (JGF `directed`, graphology
268
+ * `undirected`); edge ids where the dialect has a slot (not node-link / d3); nothing temporal;
269
+ * graph attributes everywhere but d3 (the bare shape) and vis.
270
+ * @param dialect - the dialect
271
+ * @returns its frozen capability table
272
+ */
273
+ export function dialectCapabilities(dialect: JsonDialect): ExportCapabilities {
274
+ return TABLES[dialect];
275
+ }
276
+
277
+ /** The Cytoscape element-level keys (everything else on an element lives under `data`). */
278
+ export const CYTOSCAPE_ELEMENT_KEYS: ReadonlySet<string> = new Set([
279
+ "selected",
280
+ "selectable",
281
+ "locked",
282
+ "grabbable",
283
+ "pannable",
284
+ "removed",
285
+ "scratch",
286
+ "renderedPosition",
287
+ ]);
288
+
289
+ /** The Cytoscape element keys the importer maps structurally rather than to columns. */
290
+ export const CYTOSCAPE_STRUCTURAL_KEYS: ReadonlySet<string> = new Set(["data", "group", "position", "classes"]);
291
+
292
+ /**
293
+ * The suffix the importer appends to a column name that collides with a structural column or a
294
+ * reserved key of its dialect (design section 5.6 names collisions deterministically); the exporter
295
+ * strips it when the value goes back to the level the suffix names.
296
+ */
297
+ export const SUFFIX = Object.freeze({
298
+ /** A Cytoscape `data` key or a JGF / graphology attribute that collides with a structural column. */
299
+ data: "#data",
300
+ /** An element-level key the dialect does not define (Cytoscape, JGF, graphology). */
301
+ element: "#element",
302
+ });
303
+
304
+ /** The default source keys of a node-link edge record, tried in order (as fromRecords does). */
305
+ export const NODE_LINK_SOURCE_KEYS: readonly string[] = Object.freeze(["source", "src", "from"]);
306
+
307
+ /** The default target keys of a node-link edge record, tried in order. */
308
+ export const NODE_LINK_TARGET_KEYS: readonly string[] = Object.freeze(["target", "dst", "to"]);
309
+
310
+ /** The name of the position column every JSON dialect writes / reads (design section 5.2). */
311
+ export const POSITION_COLUMN = "position";
312
+
313
+ /** The name of the Cytoscape classes column (a list of strings with role "classes"). */
314
+ export const CLASSES_COLUMN = "classes";
315
+
316
+ /** The name of the Cytoscape parent column (u32 refersTo node, role "parent"). */
317
+ export const PARENT_COLUMN = "parent";