@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,2027 @@
1
+ /**
2
+ * The GraphML importer (design sections 8.4 and 5.1; research note 07 section 2.3): a single-pass
3
+ * SAX-style reader over the streaming XML tokenizer that pushes scalars into the sink as elements
4
+ * close. `<key>` declarations become typed columns up front (boolean / int / long / float / double
5
+ * / string, with `<default>`, for the node, edge and graph domains; `for="all"` declares in all
6
+ * three with the same origin.id); `edgedefault` and per-edge `directed` go through the common
7
+ * DirectionResolver; a nested `<graph>` inside a node becomes the `parent` column (containment,
8
+ * never topology); `<port>` declarations and `<locator>` elements are reported and dropped while
9
+ * `sourceport` / `targetport` edge attributes are kept as role columns; hyperedges follow the
10
+ * `hyperedges` option (refuse, skip with a report entry, star, clique); yFiles `yfiles.type` keys
11
+ * become `json` columns holding the nested XML as a tree (origin.namespace "yfiles"), reported as
12
+ * a loss note because the structure, not the bytes, is preserved.
13
+ *
14
+ * Ids are coerced with the common rule (`ids: "canonical"` by default, design section 4.1); the
15
+ * edge attribute whose `attr.name` is `weightFrom` ("weight" by default) is THE weight and is
16
+ * passed to `addEdge` rather than stored as a column, with its declaration kept in
17
+ * `meta.weightOrigin`; a node key named `graphty:originalId` restores ids the exporter mangled.
18
+ */
19
+
20
+ import {
21
+ type ColumnDecl,
22
+ type ColumnHandle,
23
+ GraphFormatError,
24
+ type GraphSink,
25
+ INVALID_INDEX,
26
+ MAX_COUNT,
27
+ type NodeId,
28
+ } from "@graphty/graph-format";
29
+
30
+ import {
31
+ declareAttribute,
32
+ type DeclaredAttribute,
33
+ declareResolved,
34
+ losesPrecision,
35
+ parseDeclaredValue,
36
+ PRECISION_CODE,
37
+ takenIn,
38
+ uniqueColumnName,
39
+ } from "../../common/attributes.js";
40
+ import { type DeclaredTypeSpec } from "../../common/declared-types.js";
41
+ import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
42
+ import { IdCoercer } from "../../common/ids.js";
43
+ import { textChunks, throwIfAborted } from "../../common/input.js";
44
+ import { type ListSyntax } from "../../common/lists.js";
45
+ import {
46
+ type ImportFormatDefaults,
47
+ reportSinkOptions,
48
+ reportUnusedOptions,
49
+ type ResolvedImportOptions,
50
+ resolveImportOptions,
51
+ } from "../../common/options.js";
52
+ import { ImportReportBuilder } from "../../common/report.js";
53
+ import { isWeightField, parseWeightText } from "../../common/weights.js";
54
+ import { isWhitespace, localName, tokenizeXml, type XmlHandler, XmlSyntaxError } from "../../common/xml.js";
55
+ import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
56
+ import {
57
+ EDGE_ID_COLUMN,
58
+ EXTENSIONS,
59
+ FORMAT,
60
+ GRAPHML_ISSUE,
61
+ GRAPHML_LOSS,
62
+ GRAPHML_NAMESPACE,
63
+ type GraphmlMeta,
64
+ HYPEREDGE_HUB_COLUMN,
65
+ LABEL_COLUMN,
66
+ META_KEY,
67
+ MIME_TYPES,
68
+ ORIGINAL_ID_ATTRIBUTE,
69
+ ORIGINAL_ID_COLUMN,
70
+ PARENT_COLUMN,
71
+ RESERVED_EDGE_NAMES,
72
+ RESERVED_NODE_NAMES,
73
+ SOURCE_PORT_COLUMN,
74
+ TARGET_PORT_COLUMN,
75
+ XSI_NAMESPACE,
76
+ } from "./constants.js";
77
+ import { XmlTreeBuilder } from "./tree.js";
78
+
79
+ /** The format-specific options of the GraphML importer. */
80
+ export interface GraphmlImportOptions {
81
+ /**
82
+ * How keys with a `yfiles.type` (yEd / yFiles graphics) are read: "json" (default) keeps the
83
+ * nested XML of each `<data>` as a json column with origin.namespace "yfiles"; "skip" reports
84
+ * them once and declares nothing.
85
+ */
86
+ yfiles?: "json" | "skip" | undefined;
87
+ }
88
+
89
+ /** The per-format defaults of design section 8.4: text-cell ids, undirected when edgedefault is missing, weight key. */
90
+ const FORMAT_DEFAULTS: ImportFormatDefaults = { ids: "canonical", defaultDirected: false, weightFrom: "weight" };
91
+
92
+ /** Bytes inspected by sniff(). */
93
+ const SNIFF_BYTES = 4096;
94
+
95
+ const YFILES_MODES: ReadonlySet<string> = new Set(["json", "skip"]);
96
+
97
+ /** Elements between two checks of the cancellation signal. */
98
+ const ABORT_CHECK_INTERVAL = 64;
99
+
100
+ /** The common options the GraphML importer reads (the rest is reported by reportUnusedOptions). */
101
+ const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof CommonImportOptions>([
102
+ "ids",
103
+ "addMissingNodes",
104
+ "duplicateEdges",
105
+ "selfLoops",
106
+ "onMixedDirection",
107
+ "defaultDirected",
108
+ "weightFrom",
109
+ "weightDtype",
110
+ "long",
111
+ "restoreMangledIds",
112
+ "hyperedges",
113
+ "errorLimit",
114
+ "signal",
115
+ "onProgress",
116
+ ]);
117
+
118
+ /** Where the reader is, by the innermost open element. */
119
+ const enum Ctx {
120
+ Graphml,
121
+ Desc,
122
+ Key,
123
+ Default,
124
+ Graph,
125
+ Node,
126
+ Edge,
127
+ Hyperedge,
128
+ Data,
129
+ Capture,
130
+ Skip,
131
+ }
132
+
133
+ /** The element domains a key can be declared for. */
134
+ type Domain = "graph" | "node" | "edge";
135
+
136
+ /** A declared column of one domain. */
137
+ interface ColumnTarget {
138
+ /** The column name in its table. */
139
+ readonly name: string;
140
+ /** The handle (node / edge domains); INVALID_INDEX for the graph domain. */
141
+ readonly handle: ColumnHandle;
142
+ /** The declaration (graph domain, re-sent with every setGraphValue). */
143
+ readonly decl: ColumnDecl;
144
+ /** How text values are parsed; null for a json (yfiles) column. */
145
+ readonly spec: DeclaredTypeSpec | null;
146
+ /** The list syntax (unused by GraphML, which has no list type). */
147
+ readonly listSyntax: ListSyntax;
148
+ }
149
+
150
+ /** One `<key>` after `</key>`. */
151
+ interface KeyEntry {
152
+ readonly id: string;
153
+ readonly name: string;
154
+ readonly node: ColumnTarget | null;
155
+ readonly edge: ColumnTarget | null;
156
+ readonly graph: ColumnTarget | null;
157
+ /** The edge domain of this key is THE weight. */
158
+ readonly weight: boolean;
159
+ /** The node domain of this key restores mangled ids. */
160
+ readonly originalId: boolean;
161
+ /** Whether the key holds yFiles nested XML. */
162
+ readonly yfiles: boolean;
163
+ /** Whether data of this key is silently ignored (yfiles: "skip"; already reported). */
164
+ readonly skipped: boolean;
165
+ /** The domains the key was declared for, for the mismatch warning. */
166
+ readonly domains: readonly Domain[];
167
+ }
168
+
169
+ /** A `<key>` while its `<default>` and `<desc>` are being read. */
170
+ interface PendingKey {
171
+ readonly id: string | null;
172
+ readonly attrName: string | null;
173
+ readonly attrType: string | null;
174
+ readonly yfilesType: string | null;
175
+ readonly domains: readonly Domain[];
176
+ readonly line: number;
177
+ desc: string;
178
+ defaultText: string | null;
179
+ defaultTree: unknown;
180
+ }
181
+
182
+ /** A `<node>` while open (nodes nest through nested graphs, so these stack). */
183
+ interface NodeState {
184
+ /** The coerced id, or null when the element is unusable. */
185
+ id: NodeId | null;
186
+ /** The id attribute as written, for the mangled -> original map. */
187
+ idText: string | null;
188
+ /** The node index once added; INVALID_INDEX before. */
189
+ index: number;
190
+ /** The index of the containing node, or INVALID_INDEX at the top level. */
191
+ readonly parent: number;
192
+ /** Data values waiting for the node to be added: handle, value, handle, value... */
193
+ readonly pending: unknown[];
194
+ /** A `graphty:originalId` value, restored as the id. */
195
+ restoredId: string | null;
196
+ failed: boolean;
197
+ readonly line: number;
198
+ }
199
+
200
+ /** The `<edge>` being read. */
201
+ interface EdgeState {
202
+ source: NodeId | null;
203
+ target: NodeId | null;
204
+ id: string | null;
205
+ kind: EdgeKind;
206
+ sourcePort: string | null;
207
+ targetPort: string | null;
208
+ weight: number | undefined;
209
+ readonly pending: unknown[];
210
+ failed: boolean;
211
+ line: number;
212
+ }
213
+
214
+ /** One `<endpoint>` of a hyperedge. */
215
+ interface Endpoint {
216
+ readonly node: NodeId;
217
+ readonly type: "in" | "out" | "undir";
218
+ }
219
+
220
+ /** The `<hyperedge>` being read. */
221
+ interface HyperedgeState {
222
+ id: string | null;
223
+ readonly endpoints: Endpoint[];
224
+ failed: boolean;
225
+ line: number;
226
+ }
227
+
228
+ /** The `<data>` or `<default>` being read. */
229
+ interface ContentState {
230
+ key: KeyEntry | null;
231
+ pendingKey: PendingKey | null;
232
+ domain: Domain;
233
+ text: string;
234
+ tree: XmlTreeBuilder | null;
235
+ line: number;
236
+ }
237
+
238
+ /** Where an issue was found. */
239
+ interface Where {
240
+ /** The line. */
241
+ readonly line: number;
242
+ /** The element, or null. */
243
+ readonly element: string | null;
244
+ }
245
+
246
+ /** One open `<graph>`. */
247
+ interface GraphState {
248
+ readonly directed: boolean;
249
+ readonly parent: number;
250
+ }
251
+
252
+ /**
253
+ * A growable bit set over node indices, marking the indices declared by a `<node>` element so a
254
+ * second declaration is reported and a node created by an edge reference is not.
255
+ */
256
+ class IndexFlags {
257
+ private words = new Uint32Array(64);
258
+
259
+ /**
260
+ * Whether an index is marked.
261
+ * @param index - the node index
262
+ * @returns true when set
263
+ */
264
+ has(index: number): boolean {
265
+ const w = index >>> 5;
266
+ return w < this.words.length && ((this.words[w] >>> (index & 31)) & 1) === 1;
267
+ }
268
+
269
+ /**
270
+ * Mark an index.
271
+ * @param index - the node index
272
+ */
273
+ add(index: number): void {
274
+ const w = index >>> 5;
275
+ if (w >= this.words.length) {
276
+ let size = this.words.length * 2;
277
+ while (size <= w) {
278
+ size *= 2;
279
+ }
280
+ const grown = new Uint32Array(size);
281
+ grown.set(this.words);
282
+ this.words = grown;
283
+ }
284
+ this.words[w] |= 1 << (index & 31);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * The event handler that turns the tokenizer's events into sink calls.
290
+ */
291
+ class GraphmlReader implements XmlHandler {
292
+ private readonly sink: GraphSink;
293
+
294
+ private readonly report: ImportReportBuilder;
295
+
296
+ private readonly options: ResolvedImportOptions;
297
+
298
+ private readonly yfilesMode: "json" | "skip";
299
+
300
+ private readonly coercer: IdCoercer;
301
+
302
+ private readonly direction: DirectionResolver;
303
+
304
+ private readonly ctx: Ctx[] = [];
305
+
306
+ private readonly keys = new Map<string, KeyEntry>();
307
+
308
+ private readonly graphs: GraphState[] = [];
309
+
310
+ private readonly nodes: NodeState[] = [];
311
+
312
+ private readonly declared = new IndexFlags();
313
+
314
+ private readonly edgeIds = new Set<string>();
315
+
316
+ private elementsSinceCheck = 0;
317
+
318
+ /** Mangled id text -> the original id restored from `graphty:originalId`, for edge endpoints. */
319
+ private readonly restored = new Map<string, NodeId>();
320
+
321
+ private readonly graphColumnNames = new Set<string>();
322
+
323
+ private pendingKey: PendingKey | null = null;
324
+
325
+ private edge: EdgeState | null = null;
326
+
327
+ private hyperedge: HyperedgeState | null = null;
328
+
329
+ private readonly content: ContentState = {
330
+ key: null,
331
+ pendingKey: null,
332
+ domain: "node",
333
+ text: "",
334
+ tree: null,
335
+ line: 0,
336
+ };
337
+
338
+ private descTarget: "graph" | "key" | null = null;
339
+
340
+ private descText = "";
341
+
342
+ private description: string | null = null;
343
+
344
+ private graphSeen = false;
345
+
346
+ private graphId: string | null = null;
347
+
348
+ private edgedefault: "directed" | "undirected" | null = null;
349
+
350
+ private namespaces: Record<string, string> = {};
351
+
352
+ private weightOrigin: { id: string; title: string | null; type: string | null } | null = null;
353
+
354
+ private yfilesLossNoted = false;
355
+
356
+ private hyperedgeCount = 0;
357
+
358
+ private edgeIdHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
359
+
360
+ private sourcePortHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
361
+
362
+ private targetPortHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
363
+
364
+ private parentHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
365
+
366
+ private hubHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
367
+
368
+ /**
369
+ * Create a reader for one import call.
370
+ * @param sink - the sink
371
+ * @param report - the report
372
+ * @param options - the resolved common options
373
+ * @param yfilesMode - the resolved yfiles option
374
+ */
375
+ constructor(
376
+ sink: GraphSink,
377
+ report: ImportReportBuilder,
378
+ options: ResolvedImportOptions,
379
+ yfilesMode: "json" | "skip",
380
+ ) {
381
+ this.sink = sink;
382
+ this.report = report;
383
+ this.options = options;
384
+ this.yfilesMode = yfilesMode;
385
+ this.coercer = new IdCoercer(options.ids);
386
+ this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
387
+ }
388
+
389
+ // ------------------------------------------------------------------ tokenizer events
390
+
391
+ /**
392
+ * An element starts.
393
+ * @param name - the element name
394
+ * @param attrs - its attributes
395
+ * @param line - the line
396
+ */
397
+ start(name: string, attrs: ReadonlyMap<string, string>, line: number): void {
398
+ const parent = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
399
+ switch (parent) {
400
+ case Ctx.Capture:
401
+ this.captureStart(name, attrs);
402
+ return;
403
+ case Ctx.Skip:
404
+ case Ctx.Desc:
405
+ this.ctx.push(Ctx.Skip);
406
+ return;
407
+ case Ctx.Data:
408
+ case Ctx.Default:
409
+ this.captureStart(name, attrs);
410
+ return;
411
+ default:
412
+ break;
413
+ }
414
+ const local = localName(name);
415
+ switch (parent) {
416
+ case null:
417
+ if (local !== "graphml") {
418
+ this.report.fail(GRAPHML_ISSUE.NOT_GRAPHML, `the root element is <${name}>, not <graphml>`, {
419
+ line,
420
+ });
421
+ }
422
+ this.readRoot(attrs);
423
+ this.ctx.push(Ctx.Graphml);
424
+ return;
425
+ case Ctx.Graphml:
426
+ this.startInGraphml(name, local, attrs, line);
427
+ return;
428
+ case Ctx.Key:
429
+ this.startInKey(name, local, line);
430
+ return;
431
+ case Ctx.Graph:
432
+ this.startInGraph(name, local, attrs, line);
433
+ return;
434
+ case Ctx.Node:
435
+ this.startInNode(name, local, attrs, line);
436
+ return;
437
+ case Ctx.Edge:
438
+ this.startInEdge(name, local, attrs, line);
439
+ return;
440
+ case Ctx.Hyperedge:
441
+ this.startInHyperedge(name, local, attrs, line);
442
+ return;
443
+ default:
444
+ this.unknownElement(name, line);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * An element ends.
450
+ * @param name - the element name
451
+ * @param _line - the line (unused: issues of an element cite its start line)
452
+ */
453
+ end(name: string, _line: number): void {
454
+ const ctx = this.ctx.pop();
455
+ switch (ctx) {
456
+ case Ctx.Capture:
457
+ this.captureEnd();
458
+ return;
459
+ case Ctx.Skip:
460
+ case Ctx.Graphml:
461
+ case undefined:
462
+ return;
463
+ case Ctx.Desc:
464
+ this.finishDesc();
465
+ return;
466
+ case Ctx.Key:
467
+ this.finishKey();
468
+ return;
469
+ case Ctx.Default:
470
+ this.finishDefault();
471
+ return;
472
+ case Ctx.Data:
473
+ this.finishData();
474
+ return;
475
+ case Ctx.Node:
476
+ this.finishNode();
477
+ return;
478
+ case Ctx.Edge:
479
+ this.finishEdge();
480
+ return;
481
+ case Ctx.Hyperedge:
482
+ this.finishHyperedge();
483
+ return;
484
+ case Ctx.Graph:
485
+ this.graphs.pop();
486
+ return;
487
+ default: {
488
+ const value: never = ctx;
489
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown reader state ${String(value)}`, { name });
490
+ }
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Character data.
496
+ * @param text - the text
497
+ * @param line - the line
498
+ */
499
+ text(text: string, line: number): void {
500
+ const ctx = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
501
+ switch (ctx) {
502
+ case Ctx.Capture:
503
+ this.content.tree?.text(text);
504
+ return;
505
+ case Ctx.Data:
506
+ case Ctx.Default:
507
+ if (this.content.tree === null) {
508
+ this.content.text += text;
509
+ } else {
510
+ this.content.tree.text(text);
511
+ }
512
+ return;
513
+ case Ctx.Desc:
514
+ this.descText += text;
515
+ return;
516
+ case Ctx.Skip:
517
+ return;
518
+ default:
519
+ if (!isWhitespace(text)) {
520
+ this.report.warnOnce(
521
+ "validation-error",
522
+ GRAPHML_ISSUE.STRAY_TEXT,
523
+ "text where GraphML allows only elements was ignored",
524
+ { line },
525
+ );
526
+ }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * After the last event: the document must have held a graph; the metadata is written.
532
+ */
533
+ finish(): void {
534
+ if (!this.graphSeen) {
535
+ this.report.fail(GRAPHML_ISSUE.NO_GRAPH, "the document has no <graph> element");
536
+ }
537
+ const meta: GraphmlMeta = { graphId: this.graphId, edgedefault: this.edgedefault, namespaces: this.namespaces };
538
+ this.sink.setMeta({
539
+ sourceFormat: FORMAT,
540
+ description: this.description ?? undefined,
541
+ weightOrigin:
542
+ this.weightOrigin === null
543
+ ? undefined
544
+ : {
545
+ format: FORMAT,
546
+ id: this.weightOrigin.id,
547
+ title: this.weightOrigin.title,
548
+ type: this.weightOrigin.type,
549
+ namespace: null,
550
+ },
551
+ extra: { [META_KEY]: meta },
552
+ });
553
+ }
554
+
555
+ // ------------------------------------------------------------------ structure
556
+
557
+ /**
558
+ * Record the root element's namespace declarations.
559
+ * @param attrs - the root attributes
560
+ */
561
+ private readRoot(attrs: ReadonlyMap<string, string>): void {
562
+ for (const [key, value] of attrs) {
563
+ if (key.startsWith("xmlns:") && value !== GRAPHML_NAMESPACE && value !== XSI_NAMESPACE) {
564
+ this.namespaces[key.slice(6)] = value;
565
+ }
566
+ }
567
+ }
568
+
569
+ /**
570
+ * A child of `<graphml>`.
571
+ * @param name - the element name
572
+ * @param local - its local name
573
+ * @param attrs - its attributes
574
+ * @param line - the line
575
+ */
576
+ private startInGraphml(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
577
+ switch (local) {
578
+ case "desc":
579
+ this.beginDesc("graph");
580
+ return;
581
+ case "key":
582
+ this.beginKey(attrs, line);
583
+ return;
584
+ case "graph":
585
+ this.beginGraph(attrs, line, INVALID_INDEX);
586
+ return;
587
+ case "data":
588
+ this.beginData(attrs, line, "graph");
589
+ return;
590
+ default:
591
+ this.unknownElement(name, line);
592
+ }
593
+ }
594
+
595
+ /**
596
+ * A child of `<key>`.
597
+ * @param name - the element name
598
+ * @param local - its local name
599
+ * @param line - the line
600
+ */
601
+ private startInKey(name: string, local: string, line: number): void {
602
+ switch (local) {
603
+ case "desc":
604
+ this.beginDesc("key");
605
+ return;
606
+ case "default":
607
+ this.content.key = null;
608
+ this.content.pendingKey = this.pendingKey;
609
+ this.content.text = "";
610
+ this.content.tree = null;
611
+ this.content.line = line;
612
+ this.ctx.push(Ctx.Default);
613
+ return;
614
+ default:
615
+ this.unknownElement(name, line);
616
+ }
617
+ }
618
+
619
+ /**
620
+ * A child of `<graph>`.
621
+ * @param name - the element name
622
+ * @param local - its local name
623
+ * @param attrs - its attributes
624
+ * @param line - the line
625
+ */
626
+ private startInGraph(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
627
+ switch (local) {
628
+ case "desc":
629
+ if (this.graphs.length === 1) {
630
+ this.beginDesc("graph");
631
+ } else {
632
+ this.ctx.push(Ctx.Skip);
633
+ }
634
+ return;
635
+ case "data":
636
+ if (this.graphs.length === 1) {
637
+ this.beginData(attrs, line, "graph");
638
+ } else {
639
+ this.report.warnOnce(
640
+ "unsupported",
641
+ GRAPHML_ISSUE.NESTED_GRAPH_DATA,
642
+ "data of a nested <graph> cannot be kept; only the top-level graph has attributes",
643
+ { line },
644
+ );
645
+ this.ctx.push(Ctx.Skip);
646
+ }
647
+ return;
648
+ case "node":
649
+ this.beginNode(attrs, line);
650
+ return;
651
+ case "edge":
652
+ this.beginEdge(attrs, line);
653
+ return;
654
+ case "hyperedge":
655
+ this.beginHyperedge(attrs, line);
656
+ return;
657
+ case "locator":
658
+ this.report.warnOnce(
659
+ "unsupported",
660
+ GRAPHML_ISSUE.LOCATOR_DROPPED,
661
+ "<locator> references are not followed",
662
+ { line },
663
+ );
664
+ this.ctx.push(Ctx.Skip);
665
+ return;
666
+ default:
667
+ this.unknownElement(name, line);
668
+ }
669
+ }
670
+
671
+ /**
672
+ * A child of `<node>`.
673
+ * @param name - the element name
674
+ * @param local - its local name
675
+ * @param attrs - its attributes
676
+ * @param line - the line
677
+ */
678
+ private startInNode(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
679
+ switch (local) {
680
+ case "desc":
681
+ this.report.warnOnce(
682
+ "unsupported",
683
+ GRAPHML_ISSUE.DESC_DROPPED,
684
+ "node and edge <desc> texts are not kept",
685
+ {
686
+ line,
687
+ },
688
+ );
689
+ this.ctx.push(Ctx.Skip);
690
+ return;
691
+ case "data":
692
+ this.beginData(attrs, line, "node");
693
+ return;
694
+ case "port":
695
+ this.report.warnOnce(
696
+ "unsupported",
697
+ GRAPHML_ISSUE.PORT_DECLARATION,
698
+ "<port> declarations are not kept; sourceport / targetport edge attributes are",
699
+ { line },
700
+ );
701
+ this.ctx.push(Ctx.Skip);
702
+ return;
703
+ case "graph": {
704
+ const node = this.nodes[this.nodes.length - 1];
705
+ this.materializeNode(node);
706
+ this.beginGraph(attrs, line, node.index);
707
+ return;
708
+ }
709
+ case "locator":
710
+ this.report.warnOnce(
711
+ "unsupported",
712
+ GRAPHML_ISSUE.LOCATOR_DROPPED,
713
+ "<locator> references are not followed",
714
+ { line },
715
+ );
716
+ this.ctx.push(Ctx.Skip);
717
+ return;
718
+ default:
719
+ this.unknownElement(name, line);
720
+ }
721
+ }
722
+
723
+ /**
724
+ * A child of `<edge>`.
725
+ * @param name - the element name
726
+ * @param local - its local name
727
+ * @param attrs - its attributes
728
+ * @param line - the line
729
+ */
730
+ private startInEdge(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
731
+ switch (local) {
732
+ case "desc":
733
+ this.report.warnOnce(
734
+ "unsupported",
735
+ GRAPHML_ISSUE.DESC_DROPPED,
736
+ "node and edge <desc> texts are not kept",
737
+ {
738
+ line,
739
+ },
740
+ );
741
+ this.ctx.push(Ctx.Skip);
742
+ return;
743
+ case "data":
744
+ this.beginData(attrs, line, "edge");
745
+ return;
746
+ default:
747
+ this.unknownElement(name, line);
748
+ }
749
+ }
750
+
751
+ /**
752
+ * A child of `<hyperedge>` (under the star / clique policies).
753
+ * @param name - the element name
754
+ * @param local - its local name
755
+ * @param attrs - its attributes
756
+ * @param line - the line
757
+ */
758
+ private startInHyperedge(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
759
+ switch (local) {
760
+ case "endpoint":
761
+ this.readEndpoint(attrs, line);
762
+ this.ctx.push(Ctx.Skip);
763
+ return;
764
+ case "data":
765
+ this.report.warnOnce(
766
+ "unsupported",
767
+ GRAPHML_ISSUE.HYPEREDGE_DATA_DROPPED,
768
+ "hyperedge data are not kept by the star / clique expansions",
769
+ { line },
770
+ );
771
+ this.ctx.push(Ctx.Skip);
772
+ return;
773
+ case "desc":
774
+ this.ctx.push(Ctx.Skip);
775
+ return;
776
+ default:
777
+ this.unknownElement(name, line);
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Report an element the schema does not define at this place and skip its subtree.
783
+ * @param name - the element name
784
+ * @param line - the line
785
+ */
786
+ private unknownElement(name: string, line: number): void {
787
+ this.report.warnOnce(
788
+ "unsupported",
789
+ GRAPHML_ISSUE.UNKNOWN_ELEMENT,
790
+ `element <${name}> is not GraphML and was skipped`,
791
+ {
792
+ line,
793
+ element: name,
794
+ },
795
+ );
796
+ this.ctx.push(Ctx.Skip);
797
+ }
798
+
799
+ /**
800
+ * Open a `<desc>`.
801
+ * @param target - what the text describes
802
+ */
803
+ private beginDesc(target: "graph" | "key"): void {
804
+ this.descTarget = target;
805
+ this.descText = "";
806
+ this.ctx.push(Ctx.Desc);
807
+ }
808
+
809
+ /** Close a `<desc>`. */
810
+ private finishDesc(): void {
811
+ const text = this.descText.trim();
812
+ if (this.descTarget === "key" && this.pendingKey !== null) {
813
+ this.pendingKey.desc = text;
814
+ } else if (this.descTarget === "graph" && text.length > 0) {
815
+ this.description = text;
816
+ }
817
+ this.descTarget = null;
818
+ this.descText = "";
819
+ }
820
+
821
+ // ------------------------------------------------------------------ graphs
822
+
823
+ /**
824
+ * Open a `<graph>`: the top-level one sets the sink's direction (rule 1 of design section
825
+ * 8.4); a nested one records its container as the parent of its nodes.
826
+ * @param attrs - the graph attributes
827
+ * @param line - the line
828
+ * @param parent - the containing node's index, or INVALID_INDEX
829
+ */
830
+ private beginGraph(attrs: ReadonlyMap<string, string>, line: number, parent: number): void {
831
+ const top = this.graphs.length === 0;
832
+ const edgedefault = attrs.get("edgedefault");
833
+ let directed: boolean;
834
+ if (edgedefault === "directed" || edgedefault === "undirected") {
835
+ directed = edgedefault === "directed";
836
+ } else if (edgedefault === undefined) {
837
+ directed = top ? this.options.defaultDirected : this.graphs[this.graphs.length - 1].directed;
838
+ this.report.warning(
839
+ "validation-error",
840
+ GRAPHML_ISSUE.EDGEDEFAULT_MISSING,
841
+ `<graph> has no edgedefault; read as ${directed ? "directed" : "undirected"}`,
842
+ { line },
843
+ );
844
+ } else {
845
+ directed = top ? this.options.defaultDirected : this.graphs[this.graphs.length - 1].directed;
846
+ this.report.error(
847
+ "validation-error",
848
+ GRAPHML_ISSUE.INVALID_EDGEDEFAULT,
849
+ `edgedefault "${edgedefault}" is neither directed nor undirected; read as ${directed ? "directed" : "undirected"}`,
850
+ { line },
851
+ );
852
+ }
853
+ if (top && this.graphSeen) {
854
+ this.report.warnOnce(
855
+ "unsupported",
856
+ GRAPHML_ISSUE.MULTIPLE_GRAPHS,
857
+ "the document holds more than one top-level <graph>; their nodes and edges are merged into one graph",
858
+ { line, element: attrs.get("id") ?? null },
859
+ );
860
+ } else if (top) {
861
+ this.graphSeen = true;
862
+ this.graphId = attrs.get("id") ?? null;
863
+ this.edgedefault = directed ? "directed" : "undirected";
864
+ this.direction.setHeader(directed, { line });
865
+ const nodes = this.hint(attrs.get("parse.nodes"), "parse.nodes", line);
866
+ const edges = this.hint(attrs.get("parse.edges"), "parse.edges", line);
867
+ if (nodes !== null || edges !== null) {
868
+ try {
869
+ this.sink.reserve(nodes ?? undefined, edges ?? undefined);
870
+ } catch (err) {
871
+ if (!(err instanceof GraphFormatError)) {
872
+ throw err;
873
+ }
874
+ this.report.warning(
875
+ "validation-error",
876
+ GRAPHML_ISSUE.COUNT_HINT,
877
+ `count hint refused: ${err.message}`,
878
+ {
879
+ line,
880
+ },
881
+ );
882
+ }
883
+ }
884
+ }
885
+ this.graphs.push({ directed, parent });
886
+ this.ctx.push(Ctx.Graph);
887
+ }
888
+
889
+ /**
890
+ * A `parse.nodes` / `parse.edges` hint the sink can reserve; anything else is reported and ignored.
891
+ * @param text - the attribute text, or undefined
892
+ * @param name - the attribute name
893
+ * @param line - the line
894
+ * @returns the count, or null
895
+ */
896
+ private hint(text: string | undefined, name: string, line: number): number | null {
897
+ if (text === undefined) {
898
+ return null;
899
+ }
900
+ const n = hint(text);
901
+ if (n === null || n > MAX_COUNT) {
902
+ this.report.warning(
903
+ "validation-error",
904
+ GRAPHML_ISSUE.COUNT_HINT,
905
+ `<graph ${name}="${text}"> is not a count the sink can reserve; ignored`,
906
+ { line, element: name },
907
+ );
908
+ return null;
909
+ }
910
+ return n;
911
+ }
912
+
913
+ // ------------------------------------------------------------------ keys
914
+
915
+ /**
916
+ * Open a `<key>`; the column is declared at `</key>` once its default is known.
917
+ * @param attrs - the key attributes
918
+ * @param line - the line
919
+ */
920
+ private beginKey(attrs: ReadonlyMap<string, string>, line: number): void {
921
+ const id = attrs.get("id") ?? null;
922
+ const forText = attrs.get("for") ?? "all";
923
+ let domains: readonly Domain[];
924
+ switch (forText) {
925
+ case "all":
926
+ domains = ["graph", "node", "edge"];
927
+ break;
928
+ case "graph":
929
+ case "node":
930
+ case "edge":
931
+ domains = [forText];
932
+ break;
933
+ case "hyperedge":
934
+ case "port":
935
+ case "endpoint":
936
+ this.report.warning(
937
+ "unsupported",
938
+ GRAPHML_ISSUE.KEY_DOMAIN_UNSUPPORTED,
939
+ `key "${id ?? ""}" is declared for ${forText} elements, which are not kept`,
940
+ { line, element: id },
941
+ );
942
+ domains = [];
943
+ break;
944
+ default:
945
+ this.report.error(
946
+ "validation-error",
947
+ GRAPHML_ISSUE.KEY_FOR_INVALID,
948
+ `key "${id ?? ""}" has an unknown for="${forText}"`,
949
+ { line, element: id },
950
+ );
951
+ domains = [];
952
+ }
953
+ this.pendingKey = {
954
+ id,
955
+ attrName: attrs.get("attr.name") ?? null,
956
+ attrType: attrs.get("attr.type") ?? null,
957
+ yfilesType: attrs.get("yfiles.type") ?? null,
958
+ domains,
959
+ line,
960
+ desc: "",
961
+ defaultText: null,
962
+ defaultTree: undefined,
963
+ };
964
+ this.ctx.push(Ctx.Key);
965
+ }
966
+
967
+ /** Close a `<key>`: declare its columns. */
968
+ private finishKey(): void {
969
+ const key = this.pendingKey;
970
+ this.pendingKey = null;
971
+ if (key === null) {
972
+ return;
973
+ }
974
+ const where = { line: key.line, element: key.id };
975
+ if (key.id === null || key.id.length === 0) {
976
+ this.report.error("validation-error", GRAPHML_ISSUE.KEY_MISSING_ID, "<key> without an id", where);
977
+ return;
978
+ }
979
+ if (this.keys.has(key.id)) {
980
+ this.report.error(
981
+ "validation-error",
982
+ GRAPHML_ISSUE.DUPLICATE_KEY,
983
+ `key "${key.id}" is declared twice`,
984
+ where,
985
+ );
986
+ return;
987
+ }
988
+ const yfiles = key.yfilesType !== null;
989
+ const name = key.attrName !== null && key.attrName.length > 0 ? key.attrName : key.id;
990
+ const skipped = yfiles && this.yfilesMode === "skip";
991
+ if (skipped) {
992
+ this.report.warning(
993
+ "unsupported",
994
+ GRAPHML_ISSUE.YFILES_SKIPPED,
995
+ `yFiles key "${key.id}" (${key.yfilesType ?? ""}) skipped per the yfiles option`,
996
+ where,
997
+ );
998
+ }
999
+ let weight = false;
1000
+ let originalId = false;
1001
+ let node: ColumnTarget | null = null;
1002
+ let edge: ColumnTarget | null = null;
1003
+ let graph: ColumnTarget | null = null;
1004
+ for (const domain of key.domains) {
1005
+ if (skipped) {
1006
+ continue;
1007
+ }
1008
+ if (domain === "edge" && !yfiles && isWeightField(name, this.options.weightFrom)) {
1009
+ weight = true;
1010
+ this.weightOrigin ??= { id: key.id, title: key.attrName, type: key.attrType };
1011
+ continue;
1012
+ }
1013
+ if (domain === "node" && !yfiles && name === ORIGINAL_ID_ATTRIBUTE) {
1014
+ if (this.options.restoreMangledIds) {
1015
+ originalId = true;
1016
+ continue;
1017
+ }
1018
+ node = this.declareOriginalIdColumn(key);
1019
+ continue;
1020
+ }
1021
+ try {
1022
+ const target = yfiles ? this.declareYfiles(key, name, domain) : this.declareTyped(key, name, domain);
1023
+ if (domain === "node") {
1024
+ node = target;
1025
+ } else if (domain === "edge") {
1026
+ edge = target;
1027
+ } else {
1028
+ graph = target;
1029
+ }
1030
+ } catch (err) {
1031
+ this.report.recordError(err, where);
1032
+ }
1033
+ }
1034
+ this.keys.set(key.id, {
1035
+ id: key.id,
1036
+ name,
1037
+ node,
1038
+ edge,
1039
+ graph,
1040
+ weight,
1041
+ originalId,
1042
+ yfiles,
1043
+ skipped,
1044
+ domains: key.domains,
1045
+ });
1046
+ }
1047
+
1048
+ /**
1049
+ * Declare the column of a typed (or untyped, then string) key in one domain.
1050
+ * @param key - the key
1051
+ * @param name - the resolved attribute name
1052
+ * @param domain - the domain
1053
+ * @returns the column target
1054
+ */
1055
+ private declareTyped(key: PendingKey, name: string, domain: Domain): ColumnTarget {
1056
+ const id = key.id ?? "";
1057
+ const declared: DeclaredAttribute = declareAttribute({
1058
+ format: FORMAT,
1059
+ id,
1060
+ title: key.attrName !== null && key.attrName.length > 0 ? key.attrName : null,
1061
+ type: key.attrType,
1062
+ defaultText: key.defaultTree === undefined ? key.defaultText : null,
1063
+ listSyntax: "comma",
1064
+ role: domain !== "graph" && name === LABEL_COLUMN ? "label" : null,
1065
+ long: this.options.long,
1066
+ taken: this.takenIn(domain),
1067
+ });
1068
+ for (const issue of declared.issues) {
1069
+ this.report.warning(issue.category, issue.code, issue.message, { line: key.line, element: id });
1070
+ }
1071
+ if (key.defaultTree !== undefined) {
1072
+ this.report.warning(
1073
+ "validation-error",
1074
+ GRAPHML_ISSUE.DATA_NESTED,
1075
+ `the <default> of key "${id}" holds elements; a ${key.attrType ?? "string"} default must be text`,
1076
+ { line: key.line, element: id },
1077
+ );
1078
+ }
1079
+ const { decl } = declared;
1080
+ if (key.desc.length > 0) {
1081
+ decl.extra = { desc: key.desc };
1082
+ }
1083
+ return this.declareTarget(domain, decl, declared.spec, declared.listSyntax);
1084
+ }
1085
+
1086
+ /**
1087
+ * Declare the json column of a yFiles key in one domain.
1088
+ * @param key - the key
1089
+ * @param name - the resolved attribute name
1090
+ * @param domain - the domain
1091
+ * @returns the column target
1092
+ */
1093
+ private declareYfiles(key: PendingKey, name: string, domain: Domain): ColumnTarget {
1094
+ const id = key.id ?? "";
1095
+ const columnName = uniqueColumnName(name, id, this.takenIn(domain));
1096
+ const decl: ColumnDecl = {
1097
+ name: columnName,
1098
+ dtype: "json",
1099
+ nullable: true,
1100
+ origin: {
1101
+ format: FORMAT,
1102
+ id,
1103
+ title: key.attrName !== null && key.attrName !== columnName ? key.attrName : null,
1104
+ type: key.yfilesType,
1105
+ namespace: "yfiles",
1106
+ },
1107
+ };
1108
+ if (key.defaultTree !== undefined) {
1109
+ decl.default = key.defaultTree;
1110
+ } else if (key.defaultText !== null) {
1111
+ decl.default = key.defaultText;
1112
+ }
1113
+ if (key.desc.length > 0) {
1114
+ decl.extra = { desc: key.desc };
1115
+ }
1116
+ return this.declareTarget(domain, decl, null, "comma");
1117
+ }
1118
+
1119
+ /**
1120
+ * Declare the `graphty.originalId` column that keeps the exporter's original ids when
1121
+ * restoreMangledIds is off.
1122
+ * @param key - the key
1123
+ * @returns the column target
1124
+ */
1125
+ private declareOriginalIdColumn(key: PendingKey): ColumnTarget {
1126
+ const decl: ColumnDecl = {
1127
+ name: uniqueColumnName(ORIGINAL_ID_COLUMN, key.id, this.takenIn("node")),
1128
+ dtype: "string",
1129
+ nullable: true,
1130
+ role: "originalId",
1131
+ origin: { format: FORMAT, id: key.id, title: key.attrName, type: key.attrType, namespace: null },
1132
+ };
1133
+ return this.declareTarget("node", decl, null, "comma");
1134
+ }
1135
+
1136
+ /**
1137
+ * Push a declaration into the sink for one domain through the shared design section 5.6 rule:
1138
+ * a role another column already holds is dropped (reported), a name the sink holds with
1139
+ * another shape is renamed `<name>#<id>` (reported).
1140
+ * @param domain - the domain
1141
+ * @param decl - the declaration
1142
+ * @param spec - the value spec, or null for json
1143
+ * @param listSyntax - the list syntax
1144
+ * @returns the target
1145
+ */
1146
+ private declareTarget(
1147
+ domain: Domain,
1148
+ decl: ColumnDecl,
1149
+ spec: DeclaredTypeSpec | null,
1150
+ listSyntax: ListSyntax,
1151
+ ): ColumnTarget {
1152
+ if (domain === "graph") {
1153
+ this.sink.setGraphValue(decl.name, undefined, decl);
1154
+ this.graphColumnNames.add(decl.name);
1155
+ return { name: decl.name, handle: INVALID_INDEX as ColumnHandle, decl, spec, listSyntax };
1156
+ }
1157
+ const resolved = declareResolved(this.sink, domain, decl, this.report, { element: decl.name });
1158
+ return { name: resolved.decl.name, handle: resolved.handle, decl: resolved.decl, spec, listSyntax };
1159
+ }
1160
+
1161
+ /**
1162
+ * The `taken` predicate of a domain: the reserved XML-derived names plus the sink's columns.
1163
+ * @param domain - the domain
1164
+ * @returns the predicate
1165
+ */
1166
+ private takenIn(domain: Domain): (name: string) => boolean {
1167
+ if (domain === "graph") {
1168
+ return (name: string): boolean => this.graphColumnNames.has(name);
1169
+ }
1170
+ const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
1171
+ const inSink = takenIn(this.sink, domain);
1172
+ return (name: string): boolean => reserved.has(name) || inSink(name);
1173
+ }
1174
+
1175
+ // ------------------------------------------------------------------ data and defaults
1176
+
1177
+ /**
1178
+ * Open a `<data>`.
1179
+ * @param attrs - its attributes
1180
+ * @param line - the line
1181
+ * @param domain - the domain of the element holding it
1182
+ */
1183
+ private beginData(attrs: ReadonlyMap<string, string>, line: number, domain: Domain): void {
1184
+ const keyId = attrs.get("key");
1185
+ const { content } = this;
1186
+ content.key = null;
1187
+ content.pendingKey = null;
1188
+ content.domain = domain;
1189
+ content.text = "";
1190
+ content.tree = null;
1191
+ content.line = line;
1192
+ if (keyId === undefined) {
1193
+ this.report.error("validation-error", GRAPHML_ISSUE.DATA_MISSING_KEY, "<data> without a key attribute", {
1194
+ line,
1195
+ });
1196
+ } else {
1197
+ const key = this.keys.get(keyId);
1198
+ if (key === undefined) {
1199
+ this.report.error(
1200
+ "validation-error",
1201
+ GRAPHML_ISSUE.UNKNOWN_KEY,
1202
+ `<data> references undeclared key "${keyId}"`,
1203
+ { line, element: keyId },
1204
+ );
1205
+ } else if (!key.skipped && !key.domains.includes(domain)) {
1206
+ this.report.warnOnce(
1207
+ "validation-error",
1208
+ GRAPHML_ISSUE.KEY_DOMAIN,
1209
+ `key "${keyId}" is declared for ${key.domains.join(" / ")} elements but used on a ${domain}; ignored`,
1210
+ { line, element: keyId },
1211
+ );
1212
+ } else {
1213
+ content.key = key;
1214
+ }
1215
+ }
1216
+ this.ctx.push(Ctx.Data);
1217
+ }
1218
+
1219
+ /**
1220
+ * A child element inside `<data>` / `<default>`: the content becomes a tree.
1221
+ * @param name - the element name
1222
+ * @param attrs - its attributes
1223
+ */
1224
+ private captureStart(name: string, attrs: ReadonlyMap<string, string>): void {
1225
+ const { content } = this;
1226
+ if (content.tree === null) {
1227
+ content.tree = new XmlTreeBuilder();
1228
+ if (content.text.length > 0) {
1229
+ content.tree.text(content.text);
1230
+ content.text = "";
1231
+ }
1232
+ }
1233
+ content.tree.start(name, attrs);
1234
+ this.ctx.push(Ctx.Capture);
1235
+ }
1236
+
1237
+ /** A captured element ends. */
1238
+ private captureEnd(): void {
1239
+ this.content.tree?.end();
1240
+ }
1241
+
1242
+ /** Close a `<default>`: keep its text or tree for the key. */
1243
+ private finishDefault(): void {
1244
+ const { content } = this;
1245
+ const key = content.pendingKey;
1246
+ if (key === null) {
1247
+ return;
1248
+ }
1249
+ if (content.tree !== null) {
1250
+ key.defaultTree = content.tree.finish();
1251
+ } else {
1252
+ key.defaultText = content.text;
1253
+ }
1254
+ content.tree = null;
1255
+ content.text = "";
1256
+ content.pendingKey = null;
1257
+ }
1258
+
1259
+ /** Close a `<data>`: parse the value and route it to its element. */
1260
+ private finishData(): void {
1261
+ const { content } = this;
1262
+ const { key } = content;
1263
+ const value: unknown = content.tree === null ? content.text : content.tree.finish();
1264
+ content.tree = null;
1265
+ content.text = "";
1266
+ content.key = null;
1267
+ if (key === null || key.skipped) {
1268
+ return;
1269
+ }
1270
+ const where = { line: content.line, element: key.id };
1271
+ switch (content.domain) {
1272
+ case "node": {
1273
+ const node = this.nodes[this.nodes.length - 1];
1274
+ if (key.originalId) {
1275
+ if (typeof value === "string") {
1276
+ node.restoredId = value;
1277
+ }
1278
+ return;
1279
+ }
1280
+ if (key.node === null) {
1281
+ return;
1282
+ }
1283
+ const parsed = this.parseValue(key.node, value, where);
1284
+ if (parsed === undefined) {
1285
+ return;
1286
+ }
1287
+ if (node.index === INVALID_INDEX) {
1288
+ node.pending.push(key.node.handle, parsed);
1289
+ } else if (!node.failed) {
1290
+ this.sink.setNodeValue(key.node.handle, node.index, parsed);
1291
+ }
1292
+ return;
1293
+ }
1294
+ case "edge": {
1295
+ const { edge } = this;
1296
+ if (edge === null) {
1297
+ return;
1298
+ }
1299
+ if (key.weight) {
1300
+ if (typeof value !== "string") {
1301
+ this.report.error(
1302
+ "validation-error",
1303
+ GRAPHML_ISSUE.DATA_NESTED,
1304
+ "a weight value must be text",
1305
+ where,
1306
+ );
1307
+ return;
1308
+ }
1309
+ try {
1310
+ edge.weight = parseWeightText(value);
1311
+ } catch (err) {
1312
+ this.skipEdge(edge);
1313
+ this.report.recordError(err, where);
1314
+ }
1315
+ return;
1316
+ }
1317
+ if (key.edge === null) {
1318
+ return;
1319
+ }
1320
+ const parsed = this.parseValue(key.edge, value, where);
1321
+ if (parsed !== undefined) {
1322
+ edge.pending.push(key.edge.handle, parsed);
1323
+ }
1324
+ return;
1325
+ }
1326
+ case "graph": {
1327
+ if (key.graph === null) {
1328
+ return;
1329
+ }
1330
+ const parsed = this.parseValue(key.graph, value, where);
1331
+ if (parsed !== undefined) {
1332
+ try {
1333
+ this.sink.setGraphValue(key.graph.name, parsed, key.graph.decl);
1334
+ } catch (err) {
1335
+ this.report.recordError(err, where);
1336
+ }
1337
+ }
1338
+ return;
1339
+ }
1340
+ default: {
1341
+ const { domain } = content;
1342
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown data domain ${String(domain)}`, {});
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ /**
1348
+ * Parse the content of a `<data>` by its column's declaration.
1349
+ * @param target - the column
1350
+ * @param value - the text, or the tree of a nested subtree
1351
+ * @param where - the location for issues
1352
+ * @returns the value to write, or undefined when there is none (blank, or an error was recorded)
1353
+ */
1354
+ private parseValue(target: ColumnTarget, value: unknown, where: Where): unknown {
1355
+ if (target.spec === null) {
1356
+ if (!this.yfilesLossNoted) {
1357
+ this.yfilesLossNoted = true;
1358
+ this.report.loss(
1359
+ GRAPHML_LOSS.YFILES_JSON,
1360
+ "yFiles nested XML is kept as a JSON tree: structure preserved, not byte-exact",
1361
+ target.name,
1362
+ );
1363
+ }
1364
+ return value;
1365
+ }
1366
+ if (typeof value !== "string") {
1367
+ this.report.error(
1368
+ "validation-error",
1369
+ GRAPHML_ISSUE.DATA_NESTED,
1370
+ `<data> of the ${target.spec.declared} attribute "${target.name}" holds elements, not text`,
1371
+ where,
1372
+ );
1373
+ return undefined;
1374
+ }
1375
+ const { spec } = target;
1376
+ if (spec.kind !== "string" && isWhitespace(value)) {
1377
+ return undefined;
1378
+ }
1379
+ try {
1380
+ const parsed = parseDeclaredValue(value, spec, target.listSyntax);
1381
+ if (spec.precision && losesPrecision(spec, value)) {
1382
+ this.report.warnOnce(
1383
+ "precision",
1384
+ PRECISION_CODE,
1385
+ `long values of "${target.name}" beyond 2^53 are stored as the nearest double`,
1386
+ { line: where.line, element: target.name },
1387
+ );
1388
+ }
1389
+ return parsed;
1390
+ } catch (err) {
1391
+ this.report.recordError(err, where);
1392
+ return undefined;
1393
+ }
1394
+ }
1395
+
1396
+ // ------------------------------------------------------------------ nodes
1397
+
1398
+ /**
1399
+ * Open a `<node>`; it is added to the sink when its data is complete (or a nested graph
1400
+ * starts), so a `graphty:originalId` value can replace its id.
1401
+ * @param attrs - the node attributes
1402
+ * @param line - the line
1403
+ */
1404
+ private beginNode(attrs: ReadonlyMap<string, string>, line: number): void {
1405
+ const graph = this.graphs[this.graphs.length - 1];
1406
+ const state: NodeState = {
1407
+ id: null,
1408
+ idText: null,
1409
+ index: INVALID_INDEX,
1410
+ parent: graph.parent,
1411
+ pending: [],
1412
+ restoredId: null,
1413
+ failed: false,
1414
+ line,
1415
+ };
1416
+ const idText = attrs.get("id");
1417
+ if (idText === undefined || idText.length === 0) {
1418
+ this.skipNode(state);
1419
+ this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ID, "<node> without an id", { line });
1420
+ } else {
1421
+ try {
1422
+ state.id = this.coercer.text(idText);
1423
+ state.idText = idText;
1424
+ this.reportMerge(idText, line);
1425
+ } catch (err) {
1426
+ this.skipNode(state);
1427
+ this.report.recordError(err, { line, element: idText });
1428
+ }
1429
+ }
1430
+ this.nodes.push(state);
1431
+ this.ctx.push(Ctx.Node);
1432
+ }
1433
+
1434
+ /**
1435
+ * Add a node to the sink and flush its pending values.
1436
+ * @param node - the node state
1437
+ */
1438
+ private materializeNode(node: NodeState): void {
1439
+ if (node.index !== INVALID_INDEX || node.failed) {
1440
+ return;
1441
+ }
1442
+ const id: NodeId | null = node.restoredId ?? node.id;
1443
+ if (id === null) {
1444
+ node.failed = true;
1445
+ return;
1446
+ }
1447
+ if (node.restoredId !== null && node.idText !== null) {
1448
+ this.restored.set(node.idText, node.restoredId);
1449
+ }
1450
+ try {
1451
+ const existing = this.sink.indexOf(id);
1452
+ const index = this.sink.addNode(id);
1453
+ if (existing !== INVALID_INDEX && this.declared.has(index)) {
1454
+ this.report.warning(
1455
+ "validation-error",
1456
+ GRAPHML_ISSUE.DUPLICATE_NODE,
1457
+ `node "${String(id)}" is declared twice; the declarations are merged`,
1458
+ { line: node.line, element: String(id) },
1459
+ );
1460
+ } else {
1461
+ this.report.counts.nodes++;
1462
+ }
1463
+ this.declared.add(index);
1464
+ node.index = index;
1465
+ this.checkAbort();
1466
+ if (node.parent !== INVALID_INDEX) {
1467
+ this.sink.setNodeValue(this.parentColumn(), index, node.parent);
1468
+ }
1469
+ const { pending } = node;
1470
+ for (let i = 0; i < pending.length; i += 2) {
1471
+ this.sink.setNodeValue(pending[i] as ColumnHandle, index, pending[i + 1]);
1472
+ }
1473
+ pending.length = 0;
1474
+ } catch (err) {
1475
+ this.skipNode(node);
1476
+ this.report.recordError(err, { line: node.line, element: String(id) });
1477
+ }
1478
+ }
1479
+
1480
+ /**
1481
+ * Mark a node element skipped (once) before its error is recorded, so the report's counts
1482
+ * include it even when that error is the one that exceeds the limit.
1483
+ * @param node - the node state
1484
+ */
1485
+ private skipNode(node: NodeState): void {
1486
+ if (!node.failed) {
1487
+ node.failed = true;
1488
+ this.report.counts.skippedNodes++;
1489
+ }
1490
+ }
1491
+
1492
+ /** Close a `<node>`. */
1493
+ private finishNode(): void {
1494
+ const node = this.nodes.pop();
1495
+ if (node !== undefined) {
1496
+ this.materializeNode(node);
1497
+ }
1498
+ }
1499
+
1500
+ /**
1501
+ * The `parent` column, declared on first use.
1502
+ * @returns the handle
1503
+ */
1504
+ private parentColumn(): ColumnHandle {
1505
+ if (this.parentHandle === INVALID_INDEX) {
1506
+ this.parentHandle = this.sink.declareNodeColumn({
1507
+ name: PARENT_COLUMN,
1508
+ dtype: "u32",
1509
+ role: "parent",
1510
+ refersTo: "node",
1511
+ nullable: true,
1512
+ origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: null },
1513
+ });
1514
+ }
1515
+ return this.parentHandle;
1516
+ }
1517
+
1518
+ // ------------------------------------------------------------------ edges
1519
+
1520
+ /**
1521
+ * Open an `<edge>`; it is pushed at `</edge>` once its weight and data are known.
1522
+ * @param attrs - the edge attributes
1523
+ * @param line - the line
1524
+ */
1525
+ private beginEdge(attrs: ReadonlyMap<string, string>, line: number): void {
1526
+ const graph = this.graphs[this.graphs.length - 1];
1527
+ const edge: EdgeState = {
1528
+ source: null,
1529
+ target: null,
1530
+ id: attrs.get("id") ?? null,
1531
+ kind: graph.directed ? "directed" : "undirected",
1532
+ sourcePort: attrs.get("sourceport") ?? null,
1533
+ targetPort: attrs.get("targetport") ?? null,
1534
+ weight: undefined,
1535
+ pending: [],
1536
+ failed: false,
1537
+ line,
1538
+ };
1539
+ const where = { line, element: edge.id };
1540
+ const sourceText = attrs.get("source");
1541
+ const targetText = attrs.get("target");
1542
+ if (sourceText === undefined || sourceText.length === 0) {
1543
+ this.skipEdge(edge);
1544
+ this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ENDPOINT, "<edge> without a source", where);
1545
+ }
1546
+ if (targetText === undefined || targetText.length === 0) {
1547
+ this.skipEdge(edge);
1548
+ this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ENDPOINT, "<edge> without a target", where);
1549
+ }
1550
+ if (!edge.failed && sourceText !== undefined && targetText !== undefined) {
1551
+ try {
1552
+ edge.source = this.endpointId(sourceText, line);
1553
+ edge.target = this.endpointId(targetText, line);
1554
+ } catch (err) {
1555
+ this.skipEdge(edge);
1556
+ this.report.recordError(err, where);
1557
+ }
1558
+ }
1559
+ const directedText = attrs.get("directed");
1560
+ if (directedText !== undefined) {
1561
+ if (directedText === "true") {
1562
+ edge.kind = "directed";
1563
+ } else if (directedText === "false") {
1564
+ edge.kind = "undirected";
1565
+ } else {
1566
+ this.skipEdge(edge);
1567
+ this.report.error(
1568
+ "validation-error",
1569
+ GRAPHML_ISSUE.INVALID_DIRECTED,
1570
+ `directed="${directedText}" is neither true nor false`,
1571
+ where,
1572
+ );
1573
+ }
1574
+ }
1575
+ this.edge = edge;
1576
+ this.ctx.push(Ctx.Edge);
1577
+ }
1578
+
1579
+ /**
1580
+ * Close an `<edge>`: push it through the direction resolver and write its values.
1581
+ */
1582
+ private finishEdge(): void {
1583
+ const { edge } = this;
1584
+ this.edge = null;
1585
+ if (edge === null) {
1586
+ return;
1587
+ }
1588
+ if (edge.failed || edge.source === null || edge.target === null) {
1589
+ this.skipEdge(edge);
1590
+ return;
1591
+ }
1592
+ const element = edge.id ?? `${String(edge.source)} -> ${String(edge.target)}`;
1593
+ const where = { line: edge.line, element };
1594
+ try {
1595
+ if (!this.options.addMissingNodes) {
1596
+ this.requireNode(edge.source);
1597
+ this.requireNode(edge.target);
1598
+ }
1599
+ if (edge.id !== null && this.edgeIds.has(edge.id)) {
1600
+ throw new GraphFormatError("E_DUPLICATE_EDGE_ID", `edge id "${edge.id}" is used twice`, {
1601
+ id: edge.id,
1602
+ });
1603
+ }
1604
+ const before = this.sink.edgeCount;
1605
+ this.countEndpoints(edge.source, edge.target);
1606
+ const e = this.direction.addEdge(edge.source, edge.target, edge.kind, edge.weight, where);
1607
+ this.report.counts.edges += this.sink.edgeCount - before;
1608
+ this.checkAbort();
1609
+ if (edge.id !== null) {
1610
+ this.edgeIds.add(edge.id);
1611
+ this.sink.setEdgeValue(this.edgeIdColumn(), e, edge.id);
1612
+ }
1613
+ if (edge.sourcePort !== null) {
1614
+ this.sink.setEdgeValue(this.portColumn("source"), e, edge.sourcePort);
1615
+ }
1616
+ if (edge.targetPort !== null) {
1617
+ this.sink.setEdgeValue(this.portColumn("target"), e, edge.targetPort);
1618
+ }
1619
+ const { pending } = edge;
1620
+ for (let i = 0; i < pending.length; i += 2) {
1621
+ this.sink.setEdgeValue(pending[i] as ColumnHandle, e, pending[i + 1]);
1622
+ }
1623
+ } catch (err) {
1624
+ this.skipEdge(edge);
1625
+ this.report.recordError(err, where);
1626
+ }
1627
+ }
1628
+
1629
+ /**
1630
+ * Mark an edge (or hyperedge) element skipped once, before its error is recorded.
1631
+ * @param edge - the edge state
1632
+ */
1633
+ private skipEdge(edge: EdgeState | HyperedgeState): void {
1634
+ if (!edge.failed) {
1635
+ edge.failed = true;
1636
+ this.report.counts.skippedEdges++;
1637
+ }
1638
+ }
1639
+
1640
+ /**
1641
+ * E_UNKNOWN_NODE when an endpoint was never declared (addMissingNodes false enforced by the
1642
+ * importer, whatever the sink allows).
1643
+ * @param id - the endpoint id
1644
+ */
1645
+ private requireNode(id: NodeId): void {
1646
+ if (this.sink.indexOf(id) === INVALID_INDEX) {
1647
+ throw new GraphFormatError("E_UNKNOWN_NODE", `edge endpoint "${String(id)}" is not a declared node`, {
1648
+ id,
1649
+ });
1650
+ }
1651
+ }
1652
+
1653
+ /**
1654
+ * The edge `id` column (role id, unique), declared on first use.
1655
+ * @returns the handle
1656
+ */
1657
+ private edgeIdColumn(): ColumnHandle {
1658
+ if (this.edgeIdHandle === INVALID_INDEX) {
1659
+ this.edgeIdHandle = this.sink.declareEdgeColumn({
1660
+ name: EDGE_ID_COLUMN,
1661
+ dtype: "string",
1662
+ role: "id",
1663
+ unique: true,
1664
+ nullable: true,
1665
+ origin: { format: FORMAT, id: null, title: null, type: "id", namespace: null },
1666
+ });
1667
+ }
1668
+ return this.edgeIdHandle;
1669
+ }
1670
+
1671
+ /**
1672
+ * The sourceport / targetport column, declared on first use.
1673
+ * @param side - which end
1674
+ * @returns the handle
1675
+ */
1676
+ private portColumn(side: "source" | "target"): ColumnHandle {
1677
+ if (side === "source") {
1678
+ if (this.sourcePortHandle === INVALID_INDEX) {
1679
+ this.sourcePortHandle = this.sink.declareEdgeColumn({
1680
+ name: SOURCE_PORT_COLUMN,
1681
+ dtype: "string",
1682
+ role: "sourcePort",
1683
+ nullable: true,
1684
+ origin: { format: FORMAT, id: null, title: null, type: "sourceport", namespace: null },
1685
+ });
1686
+ }
1687
+ return this.sourcePortHandle;
1688
+ }
1689
+ if (this.targetPortHandle === INVALID_INDEX) {
1690
+ this.targetPortHandle = this.sink.declareEdgeColumn({
1691
+ name: TARGET_PORT_COLUMN,
1692
+ dtype: "string",
1693
+ role: "targetPort",
1694
+ nullable: true,
1695
+ origin: { format: FORMAT, id: null, title: null, type: "targetport", namespace: null },
1696
+ });
1697
+ }
1698
+ return this.targetPortHandle;
1699
+ }
1700
+
1701
+ // ------------------------------------------------------------------ hyperedges
1702
+
1703
+ /**
1704
+ * Open a `<hyperedge>` per the hyperedges option.
1705
+ * @param attrs - its attributes
1706
+ * @param line - the line
1707
+ */
1708
+ private beginHyperedge(attrs: ReadonlyMap<string, string>, line: number): void {
1709
+ const id = attrs.get("id") ?? null;
1710
+ const where = { line, element: id };
1711
+ switch (this.options.hyperedges) {
1712
+ case "error":
1713
+ this.report.error(
1714
+ "unsupported",
1715
+ GRAPHML_ISSUE.HYPEREDGE,
1716
+ 'hyperedge refused (hyperedges: "error")',
1717
+ where,
1718
+ );
1719
+ throw this.report.abort("hyperedge refused", { code: GRAPHML_ISSUE.HYPEREDGE });
1720
+ case "skip":
1721
+ this.report.warnOnce(
1722
+ "unsupported",
1723
+ GRAPHML_ISSUE.HYPEREDGE_SKIPPED,
1724
+ 'hyperedges are skipped (hyperedges: "skip"); use "star" or "clique" to expand them',
1725
+ where,
1726
+ );
1727
+ this.report.counts.skippedEdges++;
1728
+ this.ctx.push(Ctx.Skip);
1729
+ return;
1730
+ case "star":
1731
+ case "clique":
1732
+ this.hyperedge = { id, endpoints: [], failed: false, line };
1733
+ this.ctx.push(Ctx.Hyperedge);
1734
+ return;
1735
+ default: {
1736
+ const policy: never = this.options.hyperedges;
1737
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown hyperedges policy ${String(policy)}`, {
1738
+ option: "hyperedges",
1739
+ });
1740
+ }
1741
+ }
1742
+ }
1743
+
1744
+ /**
1745
+ * An `<endpoint>` of the open hyperedge.
1746
+ * @param attrs - its attributes
1747
+ * @param line - the line
1748
+ */
1749
+ private readEndpoint(attrs: ReadonlyMap<string, string>, line: number): void {
1750
+ const { hyperedge } = this;
1751
+ if (hyperedge === null) {
1752
+ return;
1753
+ }
1754
+ const where = { line, element: hyperedge.id };
1755
+ const nodeText = attrs.get("node");
1756
+ const typeText = attrs.get("type") ?? "undir";
1757
+ if (nodeText === undefined || nodeText.length === 0) {
1758
+ this.skipEdge(hyperedge);
1759
+ this.report.error("missing-value", GRAPHML_ISSUE.HYPEREDGE_ENDPOINT, "<endpoint> without a node", where);
1760
+ return;
1761
+ }
1762
+ if (typeText !== "in" && typeText !== "out" && typeText !== "undir") {
1763
+ this.skipEdge(hyperedge);
1764
+ this.report.error(
1765
+ "validation-error",
1766
+ GRAPHML_ISSUE.HYPEREDGE_ENDPOINT,
1767
+ `endpoint type "${typeText}" is not in, out or undir`,
1768
+ where,
1769
+ );
1770
+ return;
1771
+ }
1772
+ try {
1773
+ const node = this.endpointId(nodeText, line);
1774
+ hyperedge.endpoints.push({ node, type: typeText });
1775
+ } catch (err) {
1776
+ this.skipEdge(hyperedge);
1777
+ this.report.recordError(err, where);
1778
+ }
1779
+ }
1780
+
1781
+ /** Close a `<hyperedge>`: expand it to a star or a clique. */
1782
+ private finishHyperedge(): void {
1783
+ const { hyperedge } = this;
1784
+ this.hyperedge = null;
1785
+ if (hyperedge === null) {
1786
+ return;
1787
+ }
1788
+ if (hyperedge.failed) {
1789
+ return;
1790
+ }
1791
+ const where = { line: hyperedge.line, element: hyperedge.id };
1792
+ try {
1793
+ if (this.options.hyperedges === "star") {
1794
+ this.expandStar(hyperedge, where);
1795
+ } else {
1796
+ this.expandClique(hyperedge, where);
1797
+ }
1798
+ } catch (err) {
1799
+ this.skipEdge(hyperedge);
1800
+ this.report.recordError(err, where);
1801
+ }
1802
+ }
1803
+
1804
+ /**
1805
+ * Star expansion: a synthetic hub node (marked in the `graphty.hyperedge` column) joined to
1806
+ * every endpoint; `in` endpoints point at the hub, `out` endpoints are pointed at by it,
1807
+ * `undir` endpoints are undirected.
1808
+ * @param hyperedge - the hyperedge
1809
+ * @param where - the location for issues
1810
+ */
1811
+ private expandStar(hyperedge: HyperedgeState, where: Where): void {
1812
+ this.hyperedgeCount++;
1813
+ let hubId = hyperedge.id ?? `hyperedge${this.hyperedgeCount}`;
1814
+ for (let n = 2; this.sink.indexOf(hubId) !== INVALID_INDEX; n++) {
1815
+ hubId = `${hyperedge.id ?? `hyperedge${this.hyperedgeCount}`}#${n}`;
1816
+ }
1817
+ const hub = this.sink.addNode(hubId);
1818
+ this.declared.add(hub);
1819
+ this.report.counts.nodes++;
1820
+ if (this.hubHandle === INVALID_INDEX) {
1821
+ this.hubHandle = this.sink.declareNodeColumn({
1822
+ name: HYPEREDGE_HUB_COLUMN,
1823
+ dtype: "bool",
1824
+ nullable: true,
1825
+ origin: { format: FORMAT, id: null, title: null, type: "hyperedge", namespace: null },
1826
+ });
1827
+ }
1828
+ this.sink.setNodeValue(this.hubHandle, hub, true);
1829
+ for (const endpoint of hyperedge.endpoints) {
1830
+ const before = this.sink.edgeCount;
1831
+ switch (endpoint.type) {
1832
+ case "in":
1833
+ this.direction.addEdge(endpoint.node, hubId, "directed", undefined, where);
1834
+ break;
1835
+ case "out":
1836
+ this.direction.addEdge(hubId, endpoint.node, "directed", undefined, where);
1837
+ break;
1838
+ case "undir":
1839
+ this.direction.addEdge(endpoint.node, hubId, "undirected", undefined, where);
1840
+ break;
1841
+ default: {
1842
+ const { type } = endpoint;
1843
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown endpoint type ${String(type)}`, {});
1844
+ }
1845
+ }
1846
+ this.report.counts.edges += this.sink.edgeCount - before;
1847
+ }
1848
+ }
1849
+
1850
+ /**
1851
+ * Clique expansion: every `in` endpoint points at every `out` endpoint; `undir` endpoints are
1852
+ * joined undirected to every other endpoint; two endpoints of the same direction are not joined.
1853
+ * @param hyperedge - the hyperedge
1854
+ * @param where - the location for issues
1855
+ */
1856
+ private expandClique(hyperedge: HyperedgeState, where: Where): void {
1857
+ const { endpoints } = hyperedge;
1858
+ for (let i = 0; i < endpoints.length; i++) {
1859
+ for (let j = i + 1; j < endpoints.length; j++) {
1860
+ const a = endpoints[i];
1861
+ const b = endpoints[j];
1862
+ const before = this.sink.edgeCount;
1863
+ if (a.type === "undir" || b.type === "undir") {
1864
+ this.direction.addEdge(a.node, b.node, "undirected", undefined, where);
1865
+ } else if (a.type === "in" && b.type === "out") {
1866
+ this.direction.addEdge(a.node, b.node, "directed", undefined, where);
1867
+ } else if (a.type === "out" && b.type === "in") {
1868
+ this.direction.addEdge(b.node, a.node, "directed", undefined, where);
1869
+ } else {
1870
+ continue;
1871
+ }
1872
+ this.report.counts.edges += this.sink.edgeCount - before;
1873
+ }
1874
+ }
1875
+ }
1876
+
1877
+ // ------------------------------------------------------------------ options and ids
1878
+
1879
+ /**
1880
+ * Count the endpoints of an edge the file never declared as nodes of the sink (created under
1881
+ * addMissingNodes), so the report describes what the sink holds (design section 8.6).
1882
+ * @param source - the source id
1883
+ * @param target - the target id
1884
+ */
1885
+ private countEndpoints(source: NodeId, target: NodeId): void {
1886
+ if (!this.options.addMissingNodes) {
1887
+ return;
1888
+ }
1889
+ let created = 0;
1890
+ if (this.sink.indexOf(source) === INVALID_INDEX) {
1891
+ created++;
1892
+ }
1893
+ if (source !== target && this.sink.indexOf(target) === INVALID_INDEX) {
1894
+ created++;
1895
+ }
1896
+ this.report.counts.nodes += created;
1897
+ }
1898
+
1899
+ /** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
1900
+ private checkAbort(): void {
1901
+ if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
1902
+ this.elementsSinceCheck = 0;
1903
+ throwIfAborted(this.options.signal);
1904
+ }
1905
+ }
1906
+
1907
+ /**
1908
+ * The id an edge endpoint refers to: the original id when the text is a mangled id restored
1909
+ * from `graphty:originalId` (the exporter writes endpoints with the mangled id), else the
1910
+ * coerced text.
1911
+ * @param text - the source / target / endpoint node text
1912
+ * @param line - the line
1913
+ * @returns the node id
1914
+ */
1915
+ private endpointId(text: string, line: number): NodeId {
1916
+ const restored = this.restored.get(text);
1917
+ if (restored !== undefined) {
1918
+ return restored;
1919
+ }
1920
+ const id = this.coercer.text(text);
1921
+ this.reportMerge(text, line);
1922
+ return id;
1923
+ }
1924
+
1925
+ /**
1926
+ * Report a merge the coercer detected under ids: "number".
1927
+ * @param text - the id text just coerced
1928
+ * @param line - the line
1929
+ */
1930
+ private reportMerge(text: string, line: number): void {
1931
+ const merge = this.coercer.lastMerge;
1932
+ if (merge !== null) {
1933
+ this.report.warning(
1934
+ "coercion",
1935
+ GRAPHML_ISSUE.ID_MERGED,
1936
+ `id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`,
1937
+ { line, element: text },
1938
+ );
1939
+ }
1940
+ }
1941
+ }
1942
+
1943
+ /**
1944
+ * A `parse.nodes` / `parse.edges` hint as a count.
1945
+ * @param text - the attribute text
1946
+ * @returns the count, or null when absent or not a non-negative integer
1947
+ */
1948
+ function hint(text: string | undefined): number | null {
1949
+ if (text === undefined || !/^[0-9]+$/.test(text)) {
1950
+ return null;
1951
+ }
1952
+ const n = Number(text);
1953
+ return Number.isSafeInteger(n) ? n : null;
1954
+ }
1955
+
1956
+ /**
1957
+ * Resolve the format-specific options.
1958
+ * @param options - the caller's options
1959
+ * @returns the yfiles mode
1960
+ */
1961
+ function resolveYfiles(options: GraphmlImportOptions | undefined): "json" | "skip" {
1962
+ const value = options?.yfiles;
1963
+ if (value === undefined) {
1964
+ return "json";
1965
+ }
1966
+ if (typeof value !== "string" || !YFILES_MODES.has(value)) {
1967
+ throw new GraphFormatError("E_UNSUPPORTED", `option yfiles: ${JSON.stringify(value)} is not "json" or "skip"`, {
1968
+ option: "yfiles",
1969
+ found: value,
1970
+ });
1971
+ }
1972
+ return value;
1973
+ }
1974
+
1975
+ /**
1976
+ * Confidence that a head of bytes is GraphML.
1977
+ * @param head - the first bytes
1978
+ * @returns 1 for a `<graphml` root in the GraphML namespace, 0.9 for a `<graphml` root, 0.05 for other XML, 0 otherwise
1979
+ */
1980
+ function sniffGraphml(head: Uint8Array): number {
1981
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(head.subarray(0, SNIFF_BYTES));
1982
+ const root = text.indexOf("<graphml");
1983
+ if (root >= 0) {
1984
+ return text.includes(GRAPHML_NAMESPACE, root) ? 1 : 0.9;
1985
+ }
1986
+ return /^\s*<(\?xml|!--|!DOCTYPE)/.test(text) ? 0.05 : 0;
1987
+ }
1988
+
1989
+ /**
1990
+ * Import GraphML into a sink.
1991
+ * @param input - the document
1992
+ * @param sink - the sink
1993
+ * @param options - format-specific and common options
1994
+ * @returns the report; ImportError when the document cannot be read or the error limit is exceeded
1995
+ */
1996
+ async function importGraphml(
1997
+ input: ImportInput,
1998
+ sink: GraphSink,
1999
+ options?: GraphmlImportOptions & CommonImportOptions,
2000
+ ): Promise<ImportReport> {
2001
+ const common = resolveImportOptions(options, FORMAT_DEFAULTS);
2002
+ const yfilesMode = resolveYfiles(options);
2003
+ const report = new ImportReportBuilder(FORMAT, common.errorLimit);
2004
+ reportSinkOptions(sink, options, report, true);
2005
+ reportUnusedOptions(options, report, USED_OPTIONS);
2006
+ const reader = new GraphmlReader(sink, report, common, yfilesMode);
2007
+ try {
2008
+ await tokenizeXml(textChunks(input, report, common), reader);
2009
+ } catch (err) {
2010
+ if (err instanceof XmlSyntaxError) {
2011
+ report.fail(GRAPHML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
2012
+ }
2013
+ throw err;
2014
+ }
2015
+ reader.finish();
2016
+ throwIfAborted(common.signal);
2017
+ return report.finish();
2018
+ }
2019
+
2020
+ /** The GraphML importer (design section 8.4). */
2021
+ export const graphmlImporter: GraphImporter<GraphmlImportOptions> = Object.freeze({
2022
+ format: FORMAT,
2023
+ extensions: EXTENSIONS,
2024
+ mimeTypes: MIME_TYPES,
2025
+ sniff: sniffGraphml,
2026
+ import: importGraphml,
2027
+ });