@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,1880 @@
1
+ /**
2
+ * The GEXF importer (design sections 8.4, 8.6, 5.1, 5.6, 5.10; research note 07 section 2.1):
3
+ * GEXF 1.1, 1.2 and 1.3 documents read in one SAX-style pass over the shared streaming XML
4
+ * tokenizer (src/common/xml.ts) and pushed into the sink one scalar at a time, so a document of
5
+ * any size is never held in memory. Declared attributes become typed columns as they are read
6
+ * (defaults, options, list types, temporal types with their text companions declared on the first
7
+ * value whose text is not canonical); the viz namespace becomes the position / color / size /
8
+ * shape / thickness role columns with origin.namespace "viz"; `pid`, nested `<nodes>` and
9
+ * `<parents>` become the parent / parents columns, resolved once every node is declared so a
10
+ * forward reference is legal; element lifetimes (`start` / `end` / `timestamp`, `<spells>`, 1.3
11
+ * `timestamps` / `intervals`, 1.2 `startopen` / `endopen` holding the open bound's time) become
12
+ * the temporal role columns; dynamic attribute values go into the temporal extension tables of
13
+ * design section 5.10; the graph header's `mode`, `timeformat`, `timerepresentation` and `idtype`
14
+ * land in the graph meta (`idtype` is informational: Gephi writes `idtype="string"` for every
15
+ * file, so ids follow the `ids` rule, canonical by default).
16
+ *
17
+ * Direction follows design section 8.4: `defaultedgetype` (undirected when absent, per the spec and
18
+ * the `defaultDirected` option) sets the sink's direction once, and every per-edge `type` override,
19
+ * `mutual` included, goes through the common DirectionResolver and the `onMixedDirection` policy.
20
+ * The weight is the `weight` XML attribute, overridden by a static value of the edge attribute
21
+ * titled `weightFrom` ("weight" by default), applied with `setEdgeWeight` when that value arrives;
22
+ * timed values of that attribute form the `temporal:edge:weight` table (design section 5.10). Ids
23
+ * are coerced with the "canonical" rule of design section 4.1 by default. Edges may not reference
24
+ * undeclared nodes unless `addMissingNodes` is set (the GEXF default is false, design section 8.4).
25
+ */
26
+ import { GraphFormatError, INVALID_INDEX, MAX_COUNT, } from "@graphty/graph-format";
27
+ import { declareAttribute, declareCompanion, declareResolved, losesPrecision, parseDeclaredTemporal, parseDeclaredValue, PRECISION_CODE, } from "../../common/attributes.js";
28
+ import { COUNT_HINT_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, ID_MERGED_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, NO_GRAPH_CODE, STRAY_TEXT_CODE, UNKNOWN_ELEMENT_CODE, UNKNOWN_PARENT_CODE, XML_SYNTAX_CODE, } from "../../common/codes.js";
29
+ import { parseDecimalText } from "../../common/declared-types.js";
30
+ import { DirectionResolver } from "../../common/direction.js";
31
+ import { IdCoercer } from "../../common/ids.js";
32
+ import { textChunks, throwIfAborted } from "../../common/input.js";
33
+ import { splitListText } from "../../common/lists.js";
34
+ import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
35
+ import { ImportReportBuilder } from "../../common/report.js";
36
+ import { parseTimeText, timeTextCompanion } from "../../common/temporal.js";
37
+ import { isWeightField, parseWeightText } from "../../common/weights.js";
38
+ import { isWhitespace, localName, tokenizeXml, XmlSyntaxError } from "../../common/xml.js";
39
+ import { EDGE_DECLS, GEXF_FORMAT, GRAPH_MODES, NODE_DECLS, OPEN_END, OPEN_START, RESERVED_EDGE_NAMES, RESERVED_NODE_NAMES, temporalTableDecls, temporalTableName, TIME_FORMATS, TIME_REPRESENTATIONS, } from "./schema.js";
40
+ /** Issue code: the document is not a GEXF document (no `<gexf>` root). */
41
+ export const NOT_GEXF_CODE = "E_NOT_GEXF";
42
+ /** Issue code: the graph declares edges but no `<nodes>` section. */
43
+ export const MISSING_NODES_CODE = "E_GEXF_MISSING_NODES";
44
+ /** Issue code: an edge `type` outside directed / undirected / mutual. */
45
+ export const EDGE_TYPE_CODE = "E_GEXF_EDGE_TYPE";
46
+ /** Issue code: an `<attributes>` group without a class, or with an unknown one. */
47
+ export const ATTRIBUTES_CLASS_CODE = "E_GEXF_ATTRIBUTES_CLASS";
48
+ /** Issue code: an `<attribute>` without an id. */
49
+ export const ATTRIBUTE_ID_CODE = "E_GEXF_ATTRIBUTE_ID";
50
+ /** Issue code: a graph header value (`defaultedgetype`, `mode`, `timeformat`, ...) outside its set. */
51
+ export const HEADER_VALUE_CODE = "W_GEXF_HEADER_VALUE";
52
+ /** Issue code: a second `<attribute>` with the same id in the same class (the first is kept). */
53
+ export const DUPLICATE_ATTRIBUTE_CODE = "W_GEXF_DUPLICATE_ATTRIBUTE";
54
+ /** Issue code: an `<attribute>` without a type (read as string). */
55
+ export const ATTRIBUTE_TYPE_CODE = "W_GEXF_ATTRIBUTE_TYPE";
56
+ /** Issue code: an `<attvalue>` naming an attribute the document never declares (once per id). */
57
+ export const UNKNOWN_ATTRIBUTE_CODE = "W_GEXF_UNKNOWN_ATTRIBUTE";
58
+ /** Issue code: an `<attvalue>` without a `for` or a `value`. */
59
+ export const ATTVALUE_SHAPE_CODE = "W_GEXF_ATTVALUE_SHAPE";
60
+ /** Issue code: a timed value on an attribute declared in a static group (stored as dynamic anyway). */
61
+ export const TIMED_STATIC_CODE = "W_GEXF_TIMED_VALUE_ON_STATIC";
62
+ /** Issue code: the `weight` XML attribute is present but `weightFrom` is null. */
63
+ export const WEIGHT_IGNORED_CODE = "W_GEXF_WEIGHT_IGNORED";
64
+ /** Issue code: viz elements skipped under `viz: false`. */
65
+ export const VIZ_SKIPPED_CODE = "W_GEXF_VIZ_SKIPPED";
66
+ /** Issue code: a 1.2 dynamic viz element (its bounds are dropped, the value kept). */
67
+ export const VIZ_DYNAMIC_CODE = "W_GEXF_VIZ_DYNAMIC_DROPPED";
68
+ /** Issue code: `startopen` / `endopen` on a `<spell>` (the spells column has no open bits). */
69
+ export const SPELL_OPEN_CODE = "W_GEXF_SPELL_OPEN_DROPPED";
70
+ /** Issue code: a viz element with a value that does not parse (the element is skipped). */
71
+ export const VIZ_VALUE_CODE = "W_GEXF_VIZ_VALUE";
72
+ /** Issue code: both `start` and `startopen` (or `end` and `endopen`) on one element; the closed bound wins. */
73
+ export const OPEN_BOUND_CONFLICT_CODE = "W_GEXF_OPEN_BOUND_CONFLICT";
74
+ export { DUPLICATE_NODE_CODE, ID_MERGED_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, NO_GRAPH_CODE, UNKNOWN_PARENT_CODE, };
75
+ const EDGE_TYPES = new Set(["directed", "undirected", "mutual"]);
76
+ const ABORT_CHECK_INTERVAL = 64;
77
+ /** The XML attributes the importer reads on `<node>`; any other one is reported once. */
78
+ const NODE_ATTRIBUTES = new Set([
79
+ "id",
80
+ "label",
81
+ "pid",
82
+ "start",
83
+ "end",
84
+ "timestamp",
85
+ "startopen",
86
+ "endopen",
87
+ "timestamps",
88
+ "intervals",
89
+ ]);
90
+ /** The XML attributes the importer reads on `<edge>`; any other one is reported once. */
91
+ const EDGE_ATTRIBUTES = new Set([
92
+ "id",
93
+ "source",
94
+ "target",
95
+ "type",
96
+ "label",
97
+ "weight",
98
+ "kind",
99
+ "start",
100
+ "end",
101
+ "timestamp",
102
+ "startopen",
103
+ "endopen",
104
+ "timestamps",
105
+ "intervals",
106
+ ]);
107
+ /** The common options the GEXF importer reads (the rest is reported by reportUnusedOptions). */
108
+ const USED_OPTIONS = new Set([
109
+ "ids",
110
+ "addMissingNodes",
111
+ "duplicateEdges",
112
+ "selfLoops",
113
+ "onMixedDirection",
114
+ "defaultDirected",
115
+ "weightFrom",
116
+ "weightDtype",
117
+ "long",
118
+ "errorLimit",
119
+ "signal",
120
+ "onProgress",
121
+ ]);
122
+ /** A column declared on first use, through the design section 5.6 rule. */
123
+ class LazyColumn {
124
+ /**
125
+ * Create a lazily declared column.
126
+ * @param domain - node or edge
127
+ * @param decl - the declaration
128
+ */
129
+ constructor(domain, decl) {
130
+ this.handle = INVALID_INDEX;
131
+ this.domain = domain;
132
+ this.decl = decl;
133
+ }
134
+ /**
135
+ * The handle, declaring the column on the first call (renamed or without its role when the
136
+ * sink already holds the name or the role, reported).
137
+ * @param sink - the sink
138
+ * @param report - the report
139
+ * @param where - the location of the first use
140
+ * @returns the handle
141
+ */
142
+ get(sink, report, where) {
143
+ if (this.handle === INVALID_INDEX) {
144
+ this.handle = declareResolved(sink, this.domain, this.decl, report, where).handle;
145
+ }
146
+ return this.handle;
147
+ }
148
+ /**
149
+ * Write one cell, declaring the column first when needed.
150
+ * @param sink - the sink
151
+ * @param report - the report
152
+ * @param index - the row
153
+ * @param value - the value
154
+ * @param where - the location, for a declaration issue
155
+ */
156
+ set(sink, report, index, value, where) {
157
+ const handle = this.get(sink, report, where);
158
+ if (this.domain === "node") {
159
+ sink.setNodeValue(handle, index, value);
160
+ }
161
+ else {
162
+ sink.setEdgeValue(handle, index, value);
163
+ }
164
+ }
165
+ }
166
+ /** A temporal role column with its lazily declared text companion. */
167
+ class TemporalColumn {
168
+ /**
169
+ * Create the pair.
170
+ * @param domain - node or edge
171
+ * @param decl - the numeric column's declaration
172
+ */
173
+ constructor(domain, decl) {
174
+ this.companion = INVALID_INDEX;
175
+ this.domain = domain;
176
+ this.name = decl.name;
177
+ this.column = new LazyColumn(domain, decl);
178
+ }
179
+ /**
180
+ * Write a parsed time value and, when its text must be kept, the companion cell.
181
+ * @param sink - the sink
182
+ * @param report - the report
183
+ * @param index - the row
184
+ * @param value - the parsed value
185
+ * @param where - the location, for a declaration issue
186
+ */
187
+ set(sink, report, index, value, where) {
188
+ this.column.set(sink, report, index, value.value, where);
189
+ if (value.text === null) {
190
+ return;
191
+ }
192
+ if (this.companion === INVALID_INDEX) {
193
+ this.companion = declareCompanion(sink, this.domain, timeTextCompanion(this.name), report);
194
+ }
195
+ if (this.domain === "node") {
196
+ sink.setNodeValue(this.companion, index, value.text);
197
+ }
198
+ else {
199
+ sink.setEdgeValue(this.companion, index, value.text);
200
+ }
201
+ }
202
+ }
203
+ /** Where the reader is in the document. */
204
+ var Ctx;
205
+ (function (Ctx) {
206
+ Ctx[Ctx["Gexf"] = 0] = "Gexf";
207
+ Ctx[Ctx["Meta"] = 1] = "Meta";
208
+ Ctx[Ctx["MetaText"] = 2] = "MetaText";
209
+ Ctx[Ctx["Graph"] = 3] = "Graph";
210
+ Ctx[Ctx["Attributes"] = 4] = "Attributes";
211
+ Ctx[Ctx["Attribute"] = 5] = "Attribute";
212
+ Ctx[Ctx["AttributeText"] = 6] = "AttributeText";
213
+ Ctx[Ctx["Nodes"] = 7] = "Nodes";
214
+ Ctx[Ctx["Node"] = 8] = "Node";
215
+ Ctx[Ctx["Attvalues"] = 9] = "Attvalues";
216
+ Ctx[Ctx["Parents"] = 10] = "Parents";
217
+ Ctx[Ctx["Spells"] = 11] = "Spells";
218
+ Ctx[Ctx["Edges"] = 12] = "Edges";
219
+ Ctx[Ctx["Edge"] = 13] = "Edge";
220
+ Ctx[Ctx["Skip"] = 14] = "Skip";
221
+ })(Ctx || (Ctx = {}));
222
+ /**
223
+ * The event handler that turns the tokenizer's events into sink calls: one import call.
224
+ */
225
+ class GexfReader {
226
+ /**
227
+ * Set up one import.
228
+ * @param sink - the sink
229
+ * @param report - the report
230
+ * @param options - the resolved common options
231
+ * @param viz - whether viz elements are imported
232
+ */
233
+ constructor(sink, report, options, viz) {
234
+ this.ctx = [];
235
+ this.timeFormat = null;
236
+ this.defaultKind = "undirected";
237
+ this.meta = { sourceFormat: GEXF_FORMAT };
238
+ this.metaText = "";
239
+ this.metaField = null;
240
+ this.nodeAttrs = new Map();
241
+ this.edgeAttrs = new Map();
242
+ this.declaredNodeNames = new Set();
243
+ this.declaredEdgeNames = new Set();
244
+ /** The edge attribute that is THE weight (its title equals weightFrom), or null. */
245
+ this.weightAttr = null;
246
+ this.parentColumn = new LazyColumn("node", NODE_DECLS.parent);
247
+ this.parentsColumn = new LazyColumn("node", NODE_DECLS.parents);
248
+ this.positionColumn = null;
249
+ this.sizeColumn = new LazyColumn("node", NODE_DECLS.size);
250
+ this.shapeUriColumn = new LazyColumn("node", NODE_DECLS.shapeUri);
251
+ this.thicknessColumn = new LazyColumn("edge", EDGE_DECLS.thickness);
252
+ this.edgeIdColumn = new LazyColumn("edge", EDGE_DECLS.id);
253
+ this.edgeKindColumn = new LazyColumn("edge", EDGE_DECLS.kind);
254
+ this.edgeIds = new Set();
255
+ this.parentRefs = [];
256
+ this.parentsRefs = [];
257
+ this.unknownAttributes = new Set();
258
+ this.nodeStack = [];
259
+ this.edge = null;
260
+ /** The one edge frame, reused for every `<edge>` (see EdgeFrame). */
261
+ this.edgeFrame = {
262
+ index: INVALID_INDEX,
263
+ mirror: INVALID_INDEX,
264
+ where: { line: 0, element: null },
265
+ };
266
+ /** The one `<attvalue>` location, reused per value (its fields are copied into every issue). */
267
+ this.valueWhere = { line: 0, element: null };
268
+ this.attributeGroup = null;
269
+ this.pendingAttribute = null;
270
+ this.attributeText = null;
271
+ this.attributeTextValue = "";
272
+ this.spellPairs = null;
273
+ this.spellsWhere = null;
274
+ this.parentIds = null;
275
+ this.nodesSeen = false;
276
+ this.graphSeen = false;
277
+ this.elementsSinceCheck = 0;
278
+ this.sink = sink;
279
+ this.report = report;
280
+ this.options = options;
281
+ this.viz = viz;
282
+ this.coercer = new IdCoercer(options.ids);
283
+ this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
284
+ this.nodeColumns = {
285
+ label: new LazyColumn("node", NODE_DECLS.label),
286
+ start: new TemporalColumn("node", NODE_DECLS.start),
287
+ end: new TemporalColumn("node", NODE_DECLS.end),
288
+ timestamp: new TemporalColumn("node", NODE_DECLS.timestamp),
289
+ spells: new LazyColumn("node", NODE_DECLS.spells),
290
+ timestamps: new LazyColumn("node", NODE_DECLS.timestamps),
291
+ open: new LazyColumn("node", NODE_DECLS.open),
292
+ color: new LazyColumn("node", NODE_DECLS.color),
293
+ shape: new LazyColumn("node", NODE_DECLS.shape),
294
+ };
295
+ this.edgeColumns = {
296
+ label: new LazyColumn("edge", EDGE_DECLS.label),
297
+ start: new TemporalColumn("edge", EDGE_DECLS.start),
298
+ end: new TemporalColumn("edge", EDGE_DECLS.end),
299
+ timestamp: new TemporalColumn("edge", EDGE_DECLS.timestamp),
300
+ spells: new LazyColumn("edge", EDGE_DECLS.spells),
301
+ timestamps: new LazyColumn("edge", EDGE_DECLS.timestamps),
302
+ open: new LazyColumn("edge", EDGE_DECLS.open),
303
+ color: new LazyColumn("edge", EDGE_DECLS.color),
304
+ shape: new LazyColumn("edge", EDGE_DECLS.shape),
305
+ };
306
+ }
307
+ // ---------------------------------------------------------------- events
308
+ start(name, attrs, line) {
309
+ const parent = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
310
+ const local = localName(name);
311
+ switch (parent) {
312
+ case null:
313
+ if (local !== "gexf") {
314
+ this.report.fail(NOT_GEXF_CODE, `the root element is <${name}>, not <gexf>`, { line });
315
+ }
316
+ this.readRoot(attrs);
317
+ this.ctx.push(Ctx.Gexf);
318
+ return;
319
+ case Ctx.Gexf:
320
+ this.startInGexf(name, local, attrs, line);
321
+ return;
322
+ case Ctx.Meta:
323
+ this.startInMeta(name, local, line);
324
+ return;
325
+ case Ctx.Graph:
326
+ this.startInGraph(name, local, attrs, line);
327
+ return;
328
+ case Ctx.Attributes:
329
+ this.startInAttributes(name, local, attrs, line);
330
+ return;
331
+ case Ctx.Attribute:
332
+ this.startInAttribute(name, local, line);
333
+ return;
334
+ case Ctx.Nodes:
335
+ this.startInNodes(name, local, attrs, line);
336
+ return;
337
+ case Ctx.Node:
338
+ this.startInNode(name, local, attrs, line);
339
+ return;
340
+ case Ctx.Attvalues:
341
+ this.startInAttvalues(name, local, attrs, line);
342
+ return;
343
+ case Ctx.Parents:
344
+ this.startInParents(name, local, attrs, line);
345
+ return;
346
+ case Ctx.Spells:
347
+ this.startInSpells(name, local, attrs, line);
348
+ return;
349
+ case Ctx.Edges:
350
+ this.startInEdges(name, local, attrs, line);
351
+ return;
352
+ case Ctx.Edge:
353
+ this.startInEdge(name, local, attrs, line);
354
+ return;
355
+ case Ctx.MetaText:
356
+ case Ctx.AttributeText:
357
+ case Ctx.Skip:
358
+ this.ctx.push(Ctx.Skip);
359
+ return;
360
+ default:
361
+ this.unknownElement(name, line);
362
+ }
363
+ }
364
+ /**
365
+ * An element ends.
366
+ * @param _name - the element name
367
+ * @param _line - the line (unused: issues of an element cite its start line)
368
+ */
369
+ end(_name, _line) {
370
+ const ctx = this.ctx.pop();
371
+ switch (ctx) {
372
+ case Ctx.Meta:
373
+ this.finishMeta();
374
+ return;
375
+ case Ctx.MetaText:
376
+ this.finishMetaText();
377
+ return;
378
+ case Ctx.Graph:
379
+ this.resolveParents();
380
+ return;
381
+ case Ctx.Attributes:
382
+ this.attributeGroup = null;
383
+ return;
384
+ case Ctx.Attribute:
385
+ this.finishAttribute();
386
+ return;
387
+ case Ctx.AttributeText:
388
+ this.finishAttributeText();
389
+ return;
390
+ case Ctx.Node:
391
+ this.nodeStack.pop();
392
+ return;
393
+ case Ctx.Edge:
394
+ this.edge = null;
395
+ return;
396
+ case Ctx.Parents:
397
+ this.finishParents();
398
+ return;
399
+ case Ctx.Spells:
400
+ this.finishSpells();
401
+ return;
402
+ case Ctx.Gexf:
403
+ case Ctx.Nodes:
404
+ case Ctx.Edges:
405
+ case Ctx.Attvalues:
406
+ case Ctx.Skip:
407
+ case undefined:
408
+ return;
409
+ default: {
410
+ const value = ctx;
411
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown reader state ${String(value)}`, {});
412
+ }
413
+ }
414
+ }
415
+ /**
416
+ * Character data: kept inside `<creator>` / `<description>` / `<keywords>` and `<default>` /
417
+ * `<options>`, ignored elsewhere (reported once when it is not whitespace).
418
+ * @param text - the text
419
+ * @param line - the line
420
+ */
421
+ text(text, line) {
422
+ const ctx = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
423
+ switch (ctx) {
424
+ case Ctx.MetaText:
425
+ this.metaText += text;
426
+ return;
427
+ case Ctx.AttributeText:
428
+ this.attributeTextValue += text;
429
+ return;
430
+ case Ctx.Skip:
431
+ return;
432
+ default:
433
+ if (!isWhitespace(text)) {
434
+ this.report.warnOnce("validation-error", STRAY_TEXT_CODE, "text where GEXF allows only elements was ignored", { line });
435
+ }
436
+ }
437
+ }
438
+ /**
439
+ * Apply what the root and `<meta>` gave to the sink; called once the graph's header is read.
440
+ */
441
+ finish() {
442
+ if (!this.graphSeen) {
443
+ this.report.fail(NO_GRAPH_CODE, "the document has no <graph> element");
444
+ }
445
+ }
446
+ // ---------------------------------------------------------------- root, meta, graph
447
+ /**
448
+ * Record the root's version.
449
+ * @param attrs - the `<gexf>` attributes
450
+ */
451
+ readRoot(attrs) {
452
+ const version = attrs.get("version");
453
+ if (version !== undefined && version.length > 0) {
454
+ this.meta.sourceVersion = version;
455
+ }
456
+ }
457
+ /**
458
+ * A child of `<gexf>`.
459
+ * @param name - the element name
460
+ * @param local - its local name
461
+ * @param attrs - its attributes
462
+ * @param line - the line
463
+ */
464
+ startInGexf(name, local, attrs, line) {
465
+ switch (local) {
466
+ case "meta": {
467
+ const modified = attrs.get("lastmodifieddate");
468
+ if (modified !== undefined && modified.length > 0) {
469
+ this.meta.modified = modified;
470
+ }
471
+ this.ctx.push(Ctx.Meta);
472
+ return;
473
+ }
474
+ case "graph":
475
+ if (this.graphSeen) {
476
+ this.report.fail(NO_GRAPH_CODE, "a second <graph> element; a GEXF document has one graph", {
477
+ line,
478
+ });
479
+ }
480
+ this.graphSeen = true;
481
+ this.readHeader(attrs, line);
482
+ this.ctx.push(Ctx.Graph);
483
+ return;
484
+ default:
485
+ this.unknownElement(name, line);
486
+ }
487
+ }
488
+ /**
489
+ * A child of `<meta>`.
490
+ * @param name - the element name
491
+ * @param local - its local name
492
+ * @param line - the line
493
+ */
494
+ startInMeta(name, local, line) {
495
+ if (local === "creator" || local === "description" || local === "keywords") {
496
+ this.metaField = local;
497
+ this.metaText = "";
498
+ this.ctx.push(Ctx.MetaText);
499
+ return;
500
+ }
501
+ this.unknownElement(name, line);
502
+ }
503
+ /** Close a `<creator>` / `<description>` / `<keywords>`. */
504
+ finishMetaText() {
505
+ const text = this.metaText;
506
+ switch (this.metaField) {
507
+ case "creator":
508
+ this.meta.creator = text;
509
+ break;
510
+ case "description":
511
+ this.meta.description = text;
512
+ break;
513
+ case "keywords":
514
+ this.meta.keywords = text
515
+ .split(",")
516
+ .map((k) => k.trim())
517
+ .filter((k) => k.length > 0);
518
+ break;
519
+ default:
520
+ break;
521
+ }
522
+ this.metaField = null;
523
+ this.metaText = "";
524
+ }
525
+ /** Close `<meta>`: nothing more to do (the patch is applied at `<graph>`). */
526
+ finishMeta() {
527
+ // the meta patch is applied when the graph header is read
528
+ }
529
+ /**
530
+ * Read `<graph defaultedgetype mode timeformat timerepresentation idtype start end timestamp>`
531
+ * into the meta and the importer's own state, apply the meta patch and set the sink's
532
+ * direction; an unknown value is a warning and the default applies.
533
+ * @param attrs - the `<graph>` attributes
534
+ * @param line - the line
535
+ */
536
+ readHeader(attrs, line) {
537
+ const where = { line };
538
+ const patch = this.meta;
539
+ const edgeType = attrs.get("defaultedgetype");
540
+ if (edgeType === undefined) {
541
+ this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
542
+ }
543
+ else if (EDGE_TYPES.has(edgeType)) {
544
+ this.defaultKind = edgeType;
545
+ }
546
+ else {
547
+ this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
548
+ this.warnHeader("defaultedgetype", edgeType, where);
549
+ }
550
+ const mode = attrs.get("mode") ?? attrs.get("type");
551
+ if (mode !== undefined) {
552
+ if (GRAPH_MODES.has(mode)) {
553
+ patch.mode = mode;
554
+ }
555
+ else {
556
+ this.warnHeader("mode", mode, where);
557
+ }
558
+ }
559
+ const timeFormat = attrs.get("timeformat");
560
+ if (timeFormat !== undefined) {
561
+ if (TIME_FORMATS.has(timeFormat)) {
562
+ this.timeFormat = timeFormat;
563
+ patch.timeFormat = this.timeFormat;
564
+ }
565
+ else {
566
+ this.warnHeader("timeformat", timeFormat, where);
567
+ }
568
+ }
569
+ const representation = attrs.get("timerepresentation");
570
+ if (representation !== undefined) {
571
+ if (TIME_REPRESENTATIONS.has(representation)) {
572
+ patch.timeRepresentation = representation;
573
+ }
574
+ else {
575
+ this.warnHeader("timerepresentation", representation, where);
576
+ }
577
+ }
578
+ const idType = attrs.get("idtype");
579
+ if (idType !== undefined) {
580
+ if (idType === "string") {
581
+ // informational only: Gephi writes idtype="string" unconditionally, so honouring it
582
+ // would turn every Gephi export's integer ids into text; the canonical rule of
583
+ // design section 4.1 applies and the exporter's check() reports W_ID_TEXT_TYPE
584
+ patch.idType = "string";
585
+ }
586
+ else if (idType === "integer" || idType === "long") {
587
+ patch.idType = "integer";
588
+ }
589
+ else {
590
+ this.warnHeader("idtype", idType, where);
591
+ }
592
+ }
593
+ const extra = {};
594
+ for (const key of ["start", "end", "timestamp"]) {
595
+ const value = attrs.get(key);
596
+ if (value !== undefined) {
597
+ extra[key] = value;
598
+ }
599
+ }
600
+ if (Object.keys(extra).length > 0) {
601
+ patch.extra = { gexf: extra };
602
+ }
603
+ this.sink.setMeta(patch);
604
+ this.resolver.setHeader(this.defaultKind !== "undirected", where);
605
+ }
606
+ /**
607
+ * Record a header value outside its set.
608
+ * @param name - the attribute name
609
+ * @param value - the value found
610
+ * @param where - the header line
611
+ */
612
+ warnHeader(name, value, where) {
613
+ this.report.warning("validation-error", HEADER_VALUE_CODE, `<graph ${name}="${value}"> is not a known value; the default applies`, { ...where, element: name });
614
+ }
615
+ /**
616
+ * A child of `<graph>`.
617
+ * @param name - the element name
618
+ * @param local - its local name
619
+ * @param attrs - its attributes
620
+ * @param line - the line
621
+ */
622
+ startInGraph(name, local, attrs, line) {
623
+ switch (local) {
624
+ case "attributes":
625
+ this.beginAttributes(attrs, line);
626
+ return;
627
+ case "nodes":
628
+ this.nodesSeen = true;
629
+ this.reserve(attrs, "node", line);
630
+ this.ctx.push(Ctx.Nodes);
631
+ return;
632
+ case "edges":
633
+ if (!this.nodesSeen) {
634
+ this.report.error("validation-error", MISSING_NODES_CODE, "the graph declares <edges> but no <nodes>", { line });
635
+ }
636
+ this.resolveParents();
637
+ this.reserve(attrs, "edge", line);
638
+ this.ctx.push(Ctx.Edges);
639
+ return;
640
+ default:
641
+ this.unknownElement(name, line);
642
+ }
643
+ }
644
+ /**
645
+ * Pass a section's `count` hint to the sink; a hint the sink cannot reserve is a warning.
646
+ * @param attrs - the `<nodes>` or `<edges>` attributes
647
+ * @param domain - which count it is
648
+ * @param line - the line
649
+ */
650
+ reserve(attrs, domain, line) {
651
+ const text = attrs.get("count");
652
+ if (text === undefined) {
653
+ return;
654
+ }
655
+ const count = Number(text);
656
+ if (!Number.isInteger(count) || count < 0 || count > MAX_COUNT) {
657
+ this.report.warning("validation-error", COUNT_HINT_CODE, `<${domain}s count="${text}"> is not a count the sink can reserve; ignored`, { line, element: "count" });
658
+ return;
659
+ }
660
+ if (count === 0) {
661
+ return;
662
+ }
663
+ try {
664
+ if (domain === "node") {
665
+ this.sink.reserve(count, undefined);
666
+ }
667
+ else {
668
+ this.sink.reserve(undefined, count);
669
+ }
670
+ }
671
+ catch (err) {
672
+ if (!(err instanceof GraphFormatError)) {
673
+ throw err;
674
+ }
675
+ this.report.warning("validation-error", COUNT_HINT_CODE, `count hint ${text} refused: ${err.message}`, {
676
+ line,
677
+ element: "count",
678
+ });
679
+ }
680
+ }
681
+ // ---------------------------------------------------------------- attribute declarations
682
+ /**
683
+ * Open an `<attributes class mode>` group.
684
+ * @param attrs - the group's attributes
685
+ * @param line - the line
686
+ */
687
+ beginAttributes(attrs, line) {
688
+ const cls = attrs.get("class");
689
+ const where = { line };
690
+ let domain = null;
691
+ if (cls === "node" || cls === "edge") {
692
+ domain = cls;
693
+ }
694
+ else {
695
+ this.report.error("validation-error", ATTRIBUTES_CLASS_CODE, cls === undefined
696
+ ? "<attributes> without a class"
697
+ : `<attributes class="${cls}"> is neither node nor edge`, where);
698
+ }
699
+ const mode = attrs.get("mode");
700
+ if (mode !== undefined && mode !== "static" && mode !== "dynamic") {
701
+ this.warnHeader("mode", mode, where);
702
+ }
703
+ this.attributeGroup = { domain, dynamic: mode === "dynamic" };
704
+ this.ctx.push(Ctx.Attributes);
705
+ }
706
+ /**
707
+ * A child of `<attributes>`.
708
+ * @param name - the element name
709
+ * @param local - its local name
710
+ * @param attrs - its attributes
711
+ * @param line - the line
712
+ */
713
+ startInAttributes(name, local, attrs, line) {
714
+ if (local !== "attribute") {
715
+ this.unknownElement(name, line);
716
+ return;
717
+ }
718
+ const group = this.attributeGroup;
719
+ if (group === null || group.domain === null) {
720
+ // the group's class was refused; its attributes are dropped with it
721
+ this.ctx.push(Ctx.Skip);
722
+ return;
723
+ }
724
+ this.pendingAttribute = {
725
+ domain: group.domain,
726
+ dynamic: group.dynamic,
727
+ attrs,
728
+ line,
729
+ defaultText: null,
730
+ optionsText: null,
731
+ };
732
+ this.ctx.push(Ctx.Attribute);
733
+ }
734
+ /**
735
+ * A child of `<attribute>`: `<default>` or `<options>`.
736
+ * @param name - the element name
737
+ * @param local - its local name
738
+ * @param line - the line
739
+ */
740
+ startInAttribute(name, local, line) {
741
+ if (local === "default" || local === "options") {
742
+ this.attributeText = local;
743
+ this.attributeTextValue = "";
744
+ this.ctx.push(Ctx.AttributeText);
745
+ return;
746
+ }
747
+ this.unknownElement(name, line);
748
+ }
749
+ /** Close a `<default>` / `<options>`. */
750
+ finishAttributeText() {
751
+ const pending = this.pendingAttribute;
752
+ if (pending !== null) {
753
+ if (this.attributeText === "default") {
754
+ pending.defaultText = this.attributeTextValue;
755
+ }
756
+ else if (this.attributeText === "options") {
757
+ pending.optionsText = this.attributeTextValue;
758
+ }
759
+ }
760
+ this.attributeText = null;
761
+ this.attributeTextValue = "";
762
+ }
763
+ /** Close an `<attribute>`: declare it. */
764
+ finishAttribute() {
765
+ const pending = this.pendingAttribute;
766
+ this.pendingAttribute = null;
767
+ if (pending === null) {
768
+ return;
769
+ }
770
+ try {
771
+ this.declareOne(pending);
772
+ }
773
+ catch (err) {
774
+ this.report.recordError(err, { line: pending.line, element: pending.attrs.get("id") ?? null });
775
+ }
776
+ }
777
+ /**
778
+ * Declare one `<attribute id title type>` with its `<default>` and `<options>` (design
779
+ * sections 5.1, 5.5, 5.6). The edge attribute titled `weightFrom` is the weight and gets no
780
+ * column; its declaration is recorded in meta.weightOrigin.
781
+ * @param pending - the attribute as read
782
+ */
783
+ declareOne(pending) {
784
+ const { report, sink, options } = this;
785
+ const { domain, dynamic, attrs } = pending;
786
+ const id = attrs.get("id");
787
+ const where = { line: pending.line, element: id ?? null };
788
+ if (id === undefined) {
789
+ report.error("validation-error", ATTRIBUTE_ID_CODE, "<attribute> without an id", where);
790
+ return;
791
+ }
792
+ const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
793
+ if (tracked.has(id) || (domain === "edge" && this.weightAttr !== null && this.weightAttr.id === id)) {
794
+ report.warning("validation-error", DUPLICATE_ATTRIBUTE_CODE, `${domain} attribute "${id}" is declared twice; the first declaration is kept`, where);
795
+ return;
796
+ }
797
+ const title = attrs.get("title") ?? null;
798
+ const type = attrs.get("type") ?? null;
799
+ if (type === null) {
800
+ report.warning("validation-error", ATTRIBUTE_TYPE_CODE, `attribute "${id}" declares no type; read as string`, where);
801
+ }
802
+ const name = title !== null && title.length > 0 ? title : id;
803
+ if (domain === "edge" && isWeightField(name, options.weightFrom)) {
804
+ const declared = declareAttribute({ format: GEXF_FORMAT, id, title, type, long: options.long });
805
+ for (const issue of declared.issues) {
806
+ report.warning(issue.category, issue.code, issue.message, where);
807
+ }
808
+ this.weightAttr = { id, spec: declared.spec, table: INVALID_INDEX };
809
+ sink.setMeta({ weightOrigin: { format: GEXF_FORMAT, id, title, type, namespace: null } });
810
+ return;
811
+ }
812
+ const declaredNames = domain === "node" ? this.declaredNodeNames : this.declaredEdgeNames;
813
+ const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
814
+ const taken = (candidate) => reserved.has(candidate) || declaredNames.has(candidate);
815
+ const input = {
816
+ format: GEXF_FORMAT,
817
+ id,
818
+ title,
819
+ type,
820
+ defaultText: pending.defaultText,
821
+ optionsText: pending.optionsText,
822
+ listSyntax: "gexf",
823
+ dynamic,
824
+ long: options.long,
825
+ taken,
826
+ };
827
+ const declared = declareAttribute(input);
828
+ for (const issue of declared.issues) {
829
+ report.warning(issue.category, issue.code, issue.message, where);
830
+ }
831
+ const resolved = declareResolved(sink, domain, declared.decl, report, where);
832
+ declaredNames.add(resolved.decl.name);
833
+ let companionDecl = null;
834
+ if (declared.companion !== null) {
835
+ companionDecl = {
836
+ ...declared.companion,
837
+ name: `${resolved.decl.name}.text`,
838
+ extra: { for: resolved.decl.name },
839
+ };
840
+ declaredNames.add(companionDecl.name);
841
+ }
842
+ const valueDecl = {
843
+ dtype: resolved.decl.dtype,
844
+ itemDtype: resolved.decl.itemDtype,
845
+ options: resolved.decl.options,
846
+ origin: resolved.decl.origin,
847
+ };
848
+ tracked.set(id, {
849
+ id,
850
+ name: resolved.decl.name,
851
+ spec: declared.spec,
852
+ listSyntax: declared.listSyntax,
853
+ handle: resolved.handle,
854
+ companionDecl,
855
+ companion: INVALID_INDEX,
856
+ dynamic,
857
+ valueDecl,
858
+ table: INVALID_INDEX,
859
+ });
860
+ }
861
+ // ---------------------------------------------------------------- nodes
862
+ /**
863
+ * A child of `<nodes>`.
864
+ * @param name - the element name
865
+ * @param local - its local name
866
+ * @param attrs - its attributes
867
+ * @param line - the line
868
+ */
869
+ startInNodes(name, local, attrs, line) {
870
+ if (local !== "node") {
871
+ this.unknownElement(name, line);
872
+ return;
873
+ }
874
+ this.beginNode(attrs, line);
875
+ }
876
+ /**
877
+ * Push one `<node>`.
878
+ * @param attrs - its attributes
879
+ * @param line - the line
880
+ */
881
+ beginNode(attrs, line) {
882
+ const { report, sink } = this;
883
+ this.checkAbort();
884
+ const enclosing = this.nodeStack.length === 0 ? INVALID_INDEX : this.nodeStack[this.nodeStack.length - 1].index;
885
+ const idText = attrs.get("id");
886
+ const where = { line, element: idText ?? attrs.get("label") ?? null };
887
+ this.checkAttributeNames(attrs, NODE_ATTRIBUTES, "node", where);
888
+ if (idText === undefined) {
889
+ report.error("missing-value", MISSING_ID_CODE, "<node> without an id", where);
890
+ report.counts.skippedNodes++;
891
+ this.nodeStack.push({ index: INVALID_INDEX, where });
892
+ this.ctx.push(Ctx.Node);
893
+ return;
894
+ }
895
+ let index;
896
+ try {
897
+ const id = this.coerceId(idText, where);
898
+ const existing = sink.indexOf(id);
899
+ if (existing !== INVALID_INDEX) {
900
+ report.warning("validation-error", DUPLICATE_NODE_CODE, `node "${idText}" is declared more than once; later attributes overwrite`, where);
901
+ index = existing;
902
+ }
903
+ else {
904
+ index = sink.addNode(id);
905
+ report.counts.nodes++;
906
+ }
907
+ }
908
+ catch (err) {
909
+ report.recordError(err, where);
910
+ report.counts.skippedNodes++;
911
+ this.nodeStack.push({ index: INVALID_INDEX, where });
912
+ this.ctx.push(Ctx.Node);
913
+ return;
914
+ }
915
+ this.nodeStack.push({ index, where });
916
+ this.ctx.push(Ctx.Node);
917
+ try {
918
+ const label = attrs.get("label");
919
+ if (label !== undefined) {
920
+ this.nodeColumns.label.set(sink, report, index, label, where);
921
+ }
922
+ const pid = attrs.get("pid");
923
+ if (pid !== undefined) {
924
+ this.parentRefs.push({
925
+ child: index,
926
+ parentId: this.coerceId(pid, where),
927
+ parentIndex: INVALID_INDEX,
928
+ where,
929
+ });
930
+ }
931
+ else if (enclosing !== INVALID_INDEX) {
932
+ this.parentRefs.push({ child: index, parentId: null, parentIndex: enclosing, where });
933
+ }
934
+ this.writeLifetime(attrs, index, this.nodeColumns, where);
935
+ }
936
+ catch (err) {
937
+ report.recordError(err, where);
938
+ }
939
+ }
940
+ /**
941
+ * A child of `<node>`.
942
+ * @param name - the element name
943
+ * @param local - its local name
944
+ * @param attrs - its attributes
945
+ * @param line - the line
946
+ */
947
+ startInNode(name, local, attrs, line) {
948
+ const frame = this.nodeStack[this.nodeStack.length - 1];
949
+ if (frame.index === INVALID_INDEX && local !== "nodes") {
950
+ // the node was skipped; its children are too
951
+ this.ctx.push(Ctx.Skip);
952
+ return;
953
+ }
954
+ switch (local) {
955
+ case "attvalues":
956
+ this.ctx.push(Ctx.Attvalues);
957
+ return;
958
+ case "parents":
959
+ this.parentIds = [];
960
+ this.ctx.push(Ctx.Parents);
961
+ return;
962
+ case "spells":
963
+ this.spellPairs = [];
964
+ this.spellsWhere = frame.where;
965
+ this.ctx.push(Ctx.Spells);
966
+ return;
967
+ case "nodes":
968
+ this.reserve(attrs, "node", line);
969
+ this.ctx.push(Ctx.Nodes);
970
+ return;
971
+ case "color":
972
+ case "position":
973
+ case "size":
974
+ case "shape":
975
+ this.writeViz(name, local, attrs, "node", frame.index, frame.where);
976
+ this.ctx.push(Ctx.Skip);
977
+ return;
978
+ default:
979
+ this.unknownElement(name, line);
980
+ }
981
+ }
982
+ /**
983
+ * A `<parent for>` inside `<parents>`.
984
+ * @param name - the element name
985
+ * @param local - its local name
986
+ * @param attrs - its attributes
987
+ * @param line - the line
988
+ */
989
+ startInParents(name, local, attrs, line) {
990
+ if (local !== "parent") {
991
+ this.unknownElement(name, line);
992
+ return;
993
+ }
994
+ this.ctx.push(Ctx.Skip);
995
+ const frame = this.nodeStack[this.nodeStack.length - 1];
996
+ const text = attrs.get("for");
997
+ if (text === undefined) {
998
+ this.report.warning("missing-value", ATTVALUE_SHAPE_CODE, "<parent> without a for attribute", frame.where);
999
+ return;
1000
+ }
1001
+ try {
1002
+ this.parentIds?.push(this.coerceId(text, frame.where));
1003
+ }
1004
+ catch (err) {
1005
+ this.report.recordError(err, frame.where);
1006
+ }
1007
+ }
1008
+ /** Close `<parents>`: queue the list for resolution. */
1009
+ finishParents() {
1010
+ const frame = this.nodeStack[this.nodeStack.length - 1];
1011
+ if (this.parentIds !== null && frame.index !== INVALID_INDEX) {
1012
+ this.parentsRefs.push({ child: frame.index, ids: this.parentIds, where: frame.where });
1013
+ }
1014
+ this.parentIds = null;
1015
+ }
1016
+ /**
1017
+ * Resolve every `pid`, nested and `<parents>` reference read so far now that every node is
1018
+ * declared (design section 8.4); an unknown parent is a missing-value error.
1019
+ */
1020
+ resolveParents() {
1021
+ const { sink, report } = this;
1022
+ const refs = this.parentRefs;
1023
+ const lists = this.parentsRefs;
1024
+ this.parentRefs = [];
1025
+ this.parentsRefs = [];
1026
+ for (const ref of refs) {
1027
+ let parent = ref.parentIndex;
1028
+ if (ref.parentId !== null) {
1029
+ parent = sink.indexOf(ref.parentId);
1030
+ if (parent === INVALID_INDEX) {
1031
+ report.error("missing-value", UNKNOWN_PARENT_CODE, `pid "${String(ref.parentId)}" names a node the document does not declare`, ref.where);
1032
+ continue;
1033
+ }
1034
+ }
1035
+ try {
1036
+ this.parentColumn.set(sink, report, ref.child, parent, ref.where);
1037
+ }
1038
+ catch (err) {
1039
+ report.recordError(err, ref.where);
1040
+ }
1041
+ }
1042
+ for (const ref of lists) {
1043
+ const indices = [];
1044
+ for (const id of ref.ids) {
1045
+ const parent = sink.indexOf(id);
1046
+ if (parent === INVALID_INDEX) {
1047
+ report.error("missing-value", UNKNOWN_PARENT_CODE, `<parent for="${String(id)}"> names a node the document does not declare`, ref.where);
1048
+ continue;
1049
+ }
1050
+ indices.push(parent);
1051
+ }
1052
+ try {
1053
+ this.parentsColumn.set(sink, report, ref.child, indices, ref.where);
1054
+ }
1055
+ catch (err) {
1056
+ report.recordError(err, ref.where);
1057
+ }
1058
+ }
1059
+ }
1060
+ // ---------------------------------------------------------------- edges
1061
+ /**
1062
+ * A child of `<edges>`.
1063
+ * @param name - the element name
1064
+ * @param local - its local name
1065
+ * @param attrs - its attributes
1066
+ * @param line - the line
1067
+ */
1068
+ startInEdges(name, local, attrs, line) {
1069
+ if (local !== "edge") {
1070
+ this.unknownElement(name, line);
1071
+ return;
1072
+ }
1073
+ this.beginEdge(attrs, line);
1074
+ }
1075
+ /**
1076
+ * Push one `<edge>`.
1077
+ * @param attrs - its attributes
1078
+ * @param line - the line
1079
+ */
1080
+ beginEdge(attrs, line) {
1081
+ const { report, sink, options } = this;
1082
+ this.checkAbort();
1083
+ this.ctx.push(Ctx.Edge);
1084
+ this.edge = null;
1085
+ const sourceText = attrs.get("source");
1086
+ const targetText = attrs.get("target");
1087
+ const idText = attrs.get("id");
1088
+ const { where } = this.edgeFrame;
1089
+ where.line = line;
1090
+ where.element = idText ?? edgeElement(sourceText, targetText);
1091
+ this.checkAttributeNames(attrs, EDGE_ATTRIBUTES, "edge", where);
1092
+ if (sourceText === undefined || targetText === undefined) {
1093
+ report.error("missing-value", MISSING_ENDPOINT_CODE, `<edge> without a ${sourceText === undefined ? "source" : "target"}`, where);
1094
+ report.counts.skippedEdges++;
1095
+ return;
1096
+ }
1097
+ let kind = this.defaultKind;
1098
+ const type = attrs.get("type");
1099
+ if (type !== undefined) {
1100
+ if (!EDGE_TYPES.has(type)) {
1101
+ report.error("validation-error", EDGE_TYPE_CODE, `edge type "${type}" is not directed, undirected or mutual`, where);
1102
+ report.counts.skippedEdges++;
1103
+ return;
1104
+ }
1105
+ kind = type;
1106
+ }
1107
+ if (idText !== undefined) {
1108
+ if (this.edgeIds.has(idText)) {
1109
+ report.error("validation-error", DUPLICATE_EDGE_ID_CODE, `edge id "${idText}" is declared more than once; the edge is skipped`, where);
1110
+ report.counts.skippedEdges++;
1111
+ return;
1112
+ }
1113
+ this.edgeIds.add(idText);
1114
+ }
1115
+ let index;
1116
+ try {
1117
+ const source = this.coerceId(sourceText, where);
1118
+ const target = this.coerceId(targetText, where);
1119
+ if (!options.addMissingNodes) {
1120
+ if (sink.indexOf(source) === INVALID_INDEX) {
1121
+ throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${sourceText}"`, {
1122
+ id: source,
1123
+ });
1124
+ }
1125
+ if (sink.indexOf(target) === INVALID_INDEX) {
1126
+ throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${targetText}"`, {
1127
+ id: target,
1128
+ });
1129
+ }
1130
+ }
1131
+ const weight = this.xmlWeight(attrs, where);
1132
+ const before = sink.edgeCount;
1133
+ index = this.resolver.addEdge(source, target, kind, weight, where);
1134
+ report.counts.edges += sink.edgeCount - before;
1135
+ }
1136
+ catch (err) {
1137
+ report.recordError(err, where);
1138
+ report.counts.skippedEdges++;
1139
+ return;
1140
+ }
1141
+ const frame = this.edgeFrame;
1142
+ frame.index = index;
1143
+ frame.mirror = this.resolver.lastMirror;
1144
+ this.edge = frame;
1145
+ try {
1146
+ if (idText !== undefined) {
1147
+ this.edgeIdColumn.set(sink, report, index, idText, where);
1148
+ }
1149
+ const label = attrs.get("label");
1150
+ if (label !== undefined) {
1151
+ this.edgeColumns.label.set(sink, report, index, label, where);
1152
+ }
1153
+ const edgeKind = attrs.get("kind");
1154
+ if (edgeKind !== undefined) {
1155
+ this.edgeKindColumn.set(sink, report, index, edgeKind, where);
1156
+ }
1157
+ this.writeLifetime(attrs, index, this.edgeColumns, where);
1158
+ }
1159
+ catch (err) {
1160
+ report.recordError(err, where);
1161
+ }
1162
+ }
1163
+ /**
1164
+ * The weight argument of one edge from its `weight` XML attribute; undefined when absent (the
1165
+ * sink records a defaulted weight, design section 3.7).
1166
+ * @param attrs - the edge's attributes
1167
+ * @param where - the edge's location
1168
+ * @returns the weight, or undefined
1169
+ */
1170
+ xmlWeight(attrs, where) {
1171
+ const text = attrs.get("weight");
1172
+ if (text === undefined) {
1173
+ return undefined;
1174
+ }
1175
+ if (this.options.weightFrom === null) {
1176
+ this.report.warnOnce("coercion", WEIGHT_IGNORED_CODE, "the weight attribute is ignored because weightFrom is null; edges are unweighted", where);
1177
+ return undefined;
1178
+ }
1179
+ return parseWeightText(text);
1180
+ }
1181
+ /**
1182
+ * A child of `<edge>`.
1183
+ * @param name - the element name
1184
+ * @param local - its local name
1185
+ * @param attrs - its attributes
1186
+ * @param line - the line
1187
+ */
1188
+ startInEdge(name, local, attrs, line) {
1189
+ const { edge } = this;
1190
+ if (edge === null) {
1191
+ this.ctx.push(Ctx.Skip);
1192
+ return;
1193
+ }
1194
+ switch (local) {
1195
+ case "attvalues":
1196
+ this.ctx.push(Ctx.Attvalues);
1197
+ return;
1198
+ case "spells":
1199
+ this.spellPairs = [];
1200
+ this.spellsWhere = edge.where;
1201
+ this.ctx.push(Ctx.Spells);
1202
+ return;
1203
+ case "color":
1204
+ case "thickness":
1205
+ case "shape":
1206
+ this.writeViz(name, local, attrs, "edge", edge.index, edge.where);
1207
+ this.ctx.push(Ctx.Skip);
1208
+ return;
1209
+ default:
1210
+ this.unknownElement(name, line);
1211
+ }
1212
+ }
1213
+ // ---------------------------------------------------------------- attribute values
1214
+ /**
1215
+ * An `<attvalue>` of the open node or edge; a value that does not parse is recorded and skipped
1216
+ * without losing the element or its other values.
1217
+ * @param name - the element name
1218
+ * @param local - its local name
1219
+ * @param attrs - its attributes
1220
+ * @param line - the line
1221
+ */
1222
+ startInAttvalues(name, local, attrs, line) {
1223
+ if (local !== "attvalue") {
1224
+ this.unknownElement(name, line);
1225
+ return;
1226
+ }
1227
+ this.ctx.push(Ctx.Skip);
1228
+ const { report, edge } = this;
1229
+ const domain = edge === null ? "node" : "edge";
1230
+ let index;
1231
+ let element;
1232
+ if (edge !== null) {
1233
+ ({ index } = edge);
1234
+ element = edge.where.element ?? null;
1235
+ }
1236
+ else {
1237
+ const frame = this.nodeStack[this.nodeStack.length - 1];
1238
+ ({ index } = frame);
1239
+ element = frame.where.element ?? null;
1240
+ }
1241
+ if (index === INVALID_INDEX) {
1242
+ return;
1243
+ }
1244
+ const forId = attrs.get("for");
1245
+ const { valueWhere } = this;
1246
+ valueWhere.line = line;
1247
+ valueWhere.element = element;
1248
+ if (forId === undefined) {
1249
+ report.warning("missing-value", ATTVALUE_SHAPE_CODE, "<attvalue> without a for attribute", valueWhere);
1250
+ return;
1251
+ }
1252
+ const value = attrs.get("value");
1253
+ if (value === undefined) {
1254
+ report.warning("missing-value", ATTVALUE_SHAPE_CODE, `<attvalue for="${forId}"> without a value attribute`, valueWhere);
1255
+ return;
1256
+ }
1257
+ try {
1258
+ if (domain === "edge" && this.weightAttr !== null && this.weightAttr.id === forId) {
1259
+ if (isTimed(attrs)) {
1260
+ this.writeTimedWeight(attrs, value, index, valueWhere);
1261
+ }
1262
+ else {
1263
+ this.applyStaticWeight(value, valueWhere);
1264
+ }
1265
+ return;
1266
+ }
1267
+ const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
1268
+ const attribute = tracked.get(forId);
1269
+ if (attribute === undefined) {
1270
+ const key = `${domain}:${forId}`;
1271
+ if (!this.unknownAttributes.has(key)) {
1272
+ this.unknownAttributes.add(key);
1273
+ report.warning("missing-value", UNKNOWN_ATTRIBUTE_CODE, `<attvalue for="${forId}"> names an undeclared ${domain} attribute; its values are dropped`, { ...valueWhere, element: forId });
1274
+ }
1275
+ return;
1276
+ }
1277
+ if (isTimed(attrs)) {
1278
+ this.writeTimedValue(attribute, domain, attrs, value, index, valueWhere);
1279
+ }
1280
+ else {
1281
+ this.writeStaticValue(attribute, domain, value, index, valueWhere);
1282
+ }
1283
+ }
1284
+ catch (err) {
1285
+ report.recordError(err, { ...valueWhere, element: `${element ?? "?"}[${forId}]` });
1286
+ }
1287
+ }
1288
+ /**
1289
+ * A static value of the attribute titled `weightFrom` overrides the XML weight of the open
1290
+ * edge (and of its mirror half, design section 3.6).
1291
+ * @param text - the value text
1292
+ * @param where - the value's location
1293
+ */
1294
+ applyStaticWeight(text, where) {
1295
+ const { edge, sink } = this;
1296
+ if (edge === null) {
1297
+ return;
1298
+ }
1299
+ const weight = parseWeightText(text);
1300
+ if (weight === undefined) {
1301
+ throw new GraphFormatError("E_INVALID_WEIGHT", "an edge weight cannot be blank", {
1302
+ value: text,
1303
+ line: where.line ?? null,
1304
+ });
1305
+ }
1306
+ sink.setEdgeWeight(edge.index, weight);
1307
+ if (edge.mirror !== INVALID_INDEX) {
1308
+ sink.setEdgeWeight(edge.mirror, weight);
1309
+ }
1310
+ }
1311
+ /**
1312
+ * Parse a value by its declaration and write the cell (and the temporal companion, declared
1313
+ * on the first value whose text is not canonical, design section 5.1).
1314
+ * @param attribute - the declaration
1315
+ * @param domain - node or edge
1316
+ * @param text - the value text
1317
+ * @param index - the element's index
1318
+ * @param where - the value's location
1319
+ */
1320
+ writeStaticValue(attribute, domain, text, index, where) {
1321
+ const { spec } = attribute;
1322
+ if (spec.temporal !== null && !spec.list) {
1323
+ const parsed = parseDeclaredTemporal(text, spec);
1324
+ this.setCell(domain, attribute.handle, index, parsed.value);
1325
+ if (parsed.text !== null && attribute.companionDecl !== null) {
1326
+ if (attribute.companion === INVALID_INDEX) {
1327
+ attribute.companion = declareCompanion(this.sink, domain, attribute.companionDecl, this.report);
1328
+ }
1329
+ this.setCell(domain, attribute.companion, index, parsed.text);
1330
+ }
1331
+ return;
1332
+ }
1333
+ const value = parseDeclaredValue(text, spec, attribute.listSyntax);
1334
+ if (losesPrecision(spec, text)) {
1335
+ this.report.warning("precision", PRECISION_CODE, `long value ${text.trim()} of "${attribute.name}" exceeds 2^53 and was rounded`, where);
1336
+ }
1337
+ this.setCell(domain, attribute.handle, index, value);
1338
+ }
1339
+ /**
1340
+ * Append a timed value to the attribute's temporal table (design section 5.10), creating the
1341
+ * table on first use.
1342
+ * @param attribute - the declaration
1343
+ * @param domain - node or edge
1344
+ * @param attrs - the `<attvalue>` attributes carrying the bounds
1345
+ * @param text - the value text
1346
+ * @param index - the element's index
1347
+ * @param where - the value's location
1348
+ */
1349
+ writeTimedValue(attribute, domain, attrs, text, index, where) {
1350
+ const { sink, report } = this;
1351
+ if (!attribute.dynamic) {
1352
+ report.warnOnce("validation-error", TIMED_STATIC_CODE, `attribute "${attribute.name}" is declared static but has timed values; they are stored as dynamic`, where);
1353
+ }
1354
+ const { spec } = attribute;
1355
+ const temporalValue = spec.temporal !== null && !spec.list;
1356
+ if (attribute.table === INVALID_INDEX) {
1357
+ attribute.table = sink.addExtensionTable(temporalTableName(domain, attribute.name), temporalTableDecls(domain, attribute.valueDecl, temporalValue));
1358
+ }
1359
+ let value;
1360
+ let valueText = null;
1361
+ if (temporalValue) {
1362
+ const parsed = parseDeclaredTemporal(text, spec);
1363
+ ({ value } = parsed);
1364
+ valueText = parsed.text;
1365
+ }
1366
+ else {
1367
+ value = parseDeclaredValue(text, spec, attribute.listSyntax);
1368
+ if (losesPrecision(spec, text)) {
1369
+ report.warning("precision", PRECISION_CODE, `long value ${text.trim()} of "${attribute.name}" exceeds 2^53 and was rounded`, where);
1370
+ }
1371
+ }
1372
+ const row = this.temporalRow(attrs, index, value, where);
1373
+ if (temporalValue) {
1374
+ row.push(valueText ?? undefined);
1375
+ }
1376
+ sink.addExtensionRow(attribute.table, row);
1377
+ }
1378
+ /**
1379
+ * Append a timed weight to the `temporal:edge:weight` table (design section 5.10).
1380
+ * @param attrs - the `<attvalue>` attributes carrying the bounds
1381
+ * @param text - the value text
1382
+ * @param index - the edge index
1383
+ * @param where - the value's location
1384
+ */
1385
+ writeTimedWeight(attrs, text, index, where) {
1386
+ const { weightAttr } = this;
1387
+ if (weightAttr === null) {
1388
+ return;
1389
+ }
1390
+ const { sink } = this;
1391
+ if (weightAttr.table === INVALID_INDEX) {
1392
+ weightAttr.table = sink.addExtensionTable(temporalTableName("edge", this.options.weightFrom ?? "weight"), temporalTableDecls("edge", {
1393
+ dtype: "f64",
1394
+ origin: { format: GEXF_FORMAT, id: weightAttr.id, type: weightAttr.spec.declared },
1395
+ }, false));
1396
+ }
1397
+ const value = parseWeightText(text);
1398
+ if (value === undefined) {
1399
+ throw new GraphFormatError("E_INVALID_WEIGHT", "a timed edge weight cannot be blank", {
1400
+ value: text,
1401
+ line: where.line ?? null,
1402
+ });
1403
+ }
1404
+ sink.addExtensionRow(weightAttr.table, this.temporalRow(attrs, index, value, where));
1405
+ }
1406
+ /**
1407
+ * The row of a temporal table for one timed value: element, bounds, value, bound texts, open bits.
1408
+ * @param attrs - the attributes carrying `start` / `end` / `timestamp` / `startopen` / `endopen`
1409
+ * @param index - the element's index
1410
+ * @param value - the parsed value
1411
+ * @param where - the value's location
1412
+ * @returns the row, without the optional value text
1413
+ */
1414
+ temporalRow(attrs, index, value, where) {
1415
+ const bounds = this.readBounds(attrs, where);
1416
+ return [
1417
+ index,
1418
+ bounds.start.value,
1419
+ bounds.end.value,
1420
+ value,
1421
+ bounds.start.text ?? undefined,
1422
+ bounds.end.text ?? undefined,
1423
+ bounds.open === 0 ? undefined : bounds.open,
1424
+ ];
1425
+ }
1426
+ /**
1427
+ * The interval of an element or timed value: `timestamp`, or `start` / `startopen` and `end` /
1428
+ * `endopen` (GEXF 1.2: the open form carries the bound's time and marks the bound open, design
1429
+ * section 5.1); missing bounds are unbounded.
1430
+ * @param attrs - the attributes
1431
+ * @param where - the location, for the conflict warning
1432
+ * @returns the bounds and the open bits
1433
+ */
1434
+ readBounds(attrs, where) {
1435
+ let start = { value: -Infinity, text: null };
1436
+ let end = { value: Infinity, text: null };
1437
+ let open = 0;
1438
+ const timestamp = attrs.get("timestamp");
1439
+ if (timestamp !== undefined) {
1440
+ start = parseTimeText(timestamp, this.timeFormat);
1441
+ end = start;
1442
+ }
1443
+ const startText = attrs.get("start");
1444
+ const startOpen = attrs.get("startopen");
1445
+ if (startText !== undefined) {
1446
+ start = parseTimeText(startText, this.timeFormat);
1447
+ if (startOpen !== undefined) {
1448
+ this.warnOpenConflict("start", where);
1449
+ }
1450
+ }
1451
+ else if (startOpen !== undefined) {
1452
+ start = parseTimeText(startOpen, this.timeFormat);
1453
+ open |= OPEN_START;
1454
+ }
1455
+ const endText = attrs.get("end");
1456
+ const endOpen = attrs.get("endopen");
1457
+ if (endText !== undefined) {
1458
+ end = parseTimeText(endText, this.timeFormat);
1459
+ if (endOpen !== undefined) {
1460
+ this.warnOpenConflict("end", where);
1461
+ }
1462
+ }
1463
+ else if (endOpen !== undefined) {
1464
+ end = parseTimeText(endOpen, this.timeFormat);
1465
+ open |= OPEN_END;
1466
+ }
1467
+ return { start, end, open };
1468
+ }
1469
+ /**
1470
+ * Record a `start` next to a `startopen` (or `end` next to `endopen`), which the 1.2 primer forbids.
1471
+ * @param bound - which bound
1472
+ * @param where - the location
1473
+ */
1474
+ warnOpenConflict(bound, where) {
1475
+ this.report.warnOnce("validation-error", OPEN_BOUND_CONFLICT_CODE, `both ${bound} and ${bound}open are given; the closed bound is kept`, where);
1476
+ }
1477
+ // ---------------------------------------------------------------- lifetimes
1478
+ /**
1479
+ * Write an element's lifetime: `start` / `end` / `timestamp` (with text companions), the 1.2
1480
+ * `startopen` / `endopen` bounds and bits, and the 1.3 `timestamps` / `intervals` attributes.
1481
+ * @param attrs - the node or edge attributes
1482
+ * @param index - its index
1483
+ * @param columns - the domain's columns
1484
+ * @param where - the element's location
1485
+ */
1486
+ writeLifetime(attrs, index, columns, where) {
1487
+ const { sink, report } = this;
1488
+ if (attrs.has("start") ||
1489
+ attrs.has("end") ||
1490
+ attrs.has("timestamp") ||
1491
+ attrs.has("startopen") ||
1492
+ attrs.has("endopen")) {
1493
+ const bounds = this.readBounds(attrs, where);
1494
+ const timestamp = attrs.get("timestamp");
1495
+ if (timestamp !== undefined) {
1496
+ columns.timestamp.set(sink, report, index, bounds.start, where);
1497
+ }
1498
+ if (attrs.has("start") || attrs.has("startopen")) {
1499
+ columns.start.set(sink, report, index, bounds.start, where);
1500
+ }
1501
+ if (attrs.has("end") || attrs.has("endopen")) {
1502
+ columns.end.set(sink, report, index, bounds.end, where);
1503
+ }
1504
+ if (bounds.open !== 0) {
1505
+ columns.open.set(sink, report, index, bounds.open, where);
1506
+ }
1507
+ }
1508
+ const timestamps = attrs.get("timestamps");
1509
+ if (timestamps !== undefined) {
1510
+ const items = splitListText(stripAngles(timestamps), "brackets");
1511
+ columns.timestamps.set(sink, report, index, items.map((item) => parseTimeText(item, this.timeFormat).value), where);
1512
+ }
1513
+ const intervals = attrs.get("intervals");
1514
+ if (intervals !== undefined) {
1515
+ columns.spells.set(sink, report, index, this.parseIntervals(intervals, where), where);
1516
+ }
1517
+ }
1518
+ /**
1519
+ * Parse a 1.3 `intervals="<[s, e]; [s, e]>"` attribute into spell pairs.
1520
+ * @param text - the attribute text
1521
+ * @param where - the element's location, for the error
1522
+ * @returns the pairs
1523
+ */
1524
+ parseIntervals(text, where) {
1525
+ const pairs = [];
1526
+ for (const part of stripAngles(text).split(";")) {
1527
+ if (part.trim().length === 0) {
1528
+ continue;
1529
+ }
1530
+ const items = splitListText(part, "brackets");
1531
+ if (items.length !== 2) {
1532
+ throw new GraphFormatError("E_COLUMN_TYPE", `interval "${part.trim()}" is not a [start, end] pair`, {
1533
+ value: part,
1534
+ line: where.line ?? null,
1535
+ });
1536
+ }
1537
+ pairs.push([
1538
+ parseTimeText(items[0], this.timeFormat).value,
1539
+ parseTimeText(items[1], this.timeFormat).value,
1540
+ ]);
1541
+ }
1542
+ return pairs;
1543
+ }
1544
+ /**
1545
+ * A `<spell>` inside `<spells>`.
1546
+ * @param name - the element name
1547
+ * @param local - its local name
1548
+ * @param attrs - its attributes
1549
+ * @param line - the line
1550
+ */
1551
+ startInSpells(name, local, attrs, line) {
1552
+ if (local !== "spell") {
1553
+ this.unknownElement(name, line);
1554
+ return;
1555
+ }
1556
+ this.ctx.push(Ctx.Skip);
1557
+ const where = this.spellsWhere ?? { line };
1558
+ try {
1559
+ const bounds = this.readBounds(attrs, where);
1560
+ if (bounds.open !== 0) {
1561
+ this.report.warnOnce("unsupported", SPELL_OPEN_CODE, "startopen / endopen on a <spell> cannot be kept; the spell is stored closed", where);
1562
+ }
1563
+ this.spellPairs?.push([bounds.start.value, bounds.end.value]);
1564
+ }
1565
+ catch (err) {
1566
+ this.report.recordError(err, where);
1567
+ }
1568
+ }
1569
+ /** Close `<spells>`: write the pairs into the open element's spells column. */
1570
+ finishSpells() {
1571
+ const pairs = this.spellPairs;
1572
+ const where = this.spellsWhere ?? undefined;
1573
+ this.spellPairs = null;
1574
+ this.spellsWhere = null;
1575
+ if (pairs === null) {
1576
+ return;
1577
+ }
1578
+ try {
1579
+ if (this.edge !== null) {
1580
+ this.edgeColumns.spells.set(this.sink, this.report, this.edge.index, pairs, where);
1581
+ }
1582
+ else {
1583
+ const frame = this.nodeStack[this.nodeStack.length - 1];
1584
+ if (frame.index !== INVALID_INDEX) {
1585
+ this.nodeColumns.spells.set(this.sink, this.report, frame.index, pairs, where);
1586
+ }
1587
+ }
1588
+ }
1589
+ catch (err) {
1590
+ this.report.recordError(err, where);
1591
+ }
1592
+ }
1593
+ // ---------------------------------------------------------------- viz
1594
+ /**
1595
+ * Write one viz element into its role column; a value that does not parse is a recorded
1596
+ * warning and the element is skipped.
1597
+ * @param name - the element name as written
1598
+ * @param local - its local name
1599
+ * @param attrs - its attributes
1600
+ * @param domain - node or edge
1601
+ * @param index - the element's index
1602
+ * @param where - the element's location
1603
+ */
1604
+ writeViz(name, local, attrs, domain, index, where) {
1605
+ const { sink, report } = this;
1606
+ if (!this.viz) {
1607
+ report.warnOnce("unsupported", VIZ_SKIPPED_CODE, "viz elements are skipped (option viz: false)", where);
1608
+ return;
1609
+ }
1610
+ if (isTimed(attrs)) {
1611
+ report.warnOnce("unsupported", VIZ_DYNAMIC_CODE, "dynamic viz elements are not supported; the bounds are dropped and the last value kept", where);
1612
+ }
1613
+ const columns = domain === "node" ? this.nodeColumns : this.edgeColumns;
1614
+ try {
1615
+ switch (local) {
1616
+ case "color":
1617
+ columns.color.set(sink, report, index, parseColor(attrs), where);
1618
+ break;
1619
+ case "position": {
1620
+ const x = vizNumber(attrs, "x");
1621
+ const y = vizNumber(attrs, "y");
1622
+ const z = attrs.has("z") ? vizNumber(attrs, "z") : 0;
1623
+ this.positionColumn ?? (this.positionColumn = new LazyColumn("node", {
1624
+ ...NODE_DECLS.position,
1625
+ extra: { ...NODE_DECLS.position.extra, sourceDims: attrs.has("z") ? 3 : 2 },
1626
+ }));
1627
+ this.positionColumn.set(sink, report, index, [x, y, z], where);
1628
+ break;
1629
+ }
1630
+ case "size":
1631
+ this.sizeColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
1632
+ break;
1633
+ case "thickness":
1634
+ this.thicknessColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
1635
+ break;
1636
+ case "shape": {
1637
+ const value = attrs.get("value");
1638
+ if (value === undefined) {
1639
+ throw new GraphFormatError("E_COLUMN_TYPE", "<viz:shape> without a value", { element: name });
1640
+ }
1641
+ columns.shape.set(sink, report, index, value, where);
1642
+ const uri = attrs.get("uri");
1643
+ if (domain === "node" && uri !== undefined) {
1644
+ this.shapeUriColumn.set(sink, report, index, uri, where);
1645
+ }
1646
+ break;
1647
+ }
1648
+ default:
1649
+ break;
1650
+ }
1651
+ }
1652
+ catch (err) {
1653
+ if (!(err instanceof GraphFormatError) || err.code !== "E_COLUMN_TYPE") {
1654
+ throw err;
1655
+ }
1656
+ report.warning("validation-error", VIZ_VALUE_CODE, `<${name}> skipped: ${err.message}`, where);
1657
+ }
1658
+ }
1659
+ // ---------------------------------------------------------------- helpers
1660
+ /**
1661
+ * Report an element the schema does not define at this place and skip its subtree.
1662
+ * @param name - the element name
1663
+ * @param line - the line
1664
+ */
1665
+ unknownElement(name, line) {
1666
+ this.report.warnOnce("unsupported", UNKNOWN_ELEMENT_CODE, `element <${name}> is not GEXF and was skipped`, {
1667
+ line,
1668
+ element: name,
1669
+ });
1670
+ this.ctx.push(Ctx.Skip);
1671
+ }
1672
+ /**
1673
+ * Report XML attributes the importer does not read (once per attribute name and domain).
1674
+ * @param attrs - the element's attributes
1675
+ * @param known - the attribute names read
1676
+ * @param domain - node or edge
1677
+ * @param where - the element's location
1678
+ */
1679
+ checkAttributeNames(attrs, known, domain, where) {
1680
+ for (const key of attrs.keys()) {
1681
+ if (!known.has(key)) {
1682
+ const seen = `${domain}@${key}`;
1683
+ if (!this.unknownAttributes.has(seen)) {
1684
+ this.unknownAttributes.add(seen);
1685
+ this.report.warning("unsupported", UNKNOWN_ELEMENT_CODE, `attribute ${key} of <${domain}> is not GEXF and was ignored`, { ...where, element: key });
1686
+ }
1687
+ }
1688
+ }
1689
+ }
1690
+ /**
1691
+ * Write one cell of a domain.
1692
+ * @param domain - node or edge
1693
+ * @param handle - the column
1694
+ * @param index - the row
1695
+ * @param value - the value
1696
+ */
1697
+ setCell(domain, handle, index, value) {
1698
+ if (domain === "node") {
1699
+ this.sink.setNodeValue(handle, index, value);
1700
+ }
1701
+ else {
1702
+ this.sink.setEdgeValue(handle, index, value);
1703
+ }
1704
+ }
1705
+ /**
1706
+ * Coerce an id text, recording a merge under ids "number".
1707
+ * @param text - the id text
1708
+ * @param where - the location, for the merge warning
1709
+ * @returns the id
1710
+ */
1711
+ coerceId(text, where) {
1712
+ const id = this.coercer.text(text);
1713
+ const merge = this.coercer.lastMerge;
1714
+ if (merge !== null) {
1715
+ this.report.warning("coercion", ID_MERGED_CODE, `id text "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids "number"`, where);
1716
+ }
1717
+ return id;
1718
+ }
1719
+ /** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
1720
+ checkAbort() {
1721
+ if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
1722
+ this.elementsSinceCheck = 0;
1723
+ throwIfAborted(this.options.signal);
1724
+ }
1725
+ }
1726
+ }
1727
+ /**
1728
+ * The element text of an edge without an id, for issues.
1729
+ * @param source - the source text, or undefined
1730
+ * @param target - the target text, or undefined
1731
+ * @returns `source->target`
1732
+ */
1733
+ function edgeElement(source, target) {
1734
+ return `${source ?? "?"}->${target ?? "?"}`;
1735
+ }
1736
+ /**
1737
+ * Whether an element carries time bounds.
1738
+ * @param attrs - the element's attributes
1739
+ * @returns true when start, end, timestamp or an open bound is present
1740
+ */
1741
+ function isTimed(attrs) {
1742
+ return (attrs.has("start") ||
1743
+ attrs.has("end") ||
1744
+ attrs.has("timestamp") ||
1745
+ attrs.has("startopen") ||
1746
+ attrs.has("endopen"));
1747
+ }
1748
+ /**
1749
+ * Remove the `<` `>` wrapper of a 1.3 `timestamps` / `intervals` attribute value.
1750
+ * @param text - the attribute text
1751
+ * @returns the inner text
1752
+ */
1753
+ function stripAngles(text) {
1754
+ let inner = text.trim();
1755
+ if (inner.startsWith("<")) {
1756
+ inner = inner.slice(1);
1757
+ }
1758
+ if (inner.endsWith(">")) {
1759
+ inner = inner.slice(0, -1);
1760
+ }
1761
+ return inner;
1762
+ }
1763
+ /**
1764
+ * A numeric viz attribute.
1765
+ * @param attrs - the element's attributes
1766
+ * @param name - the attribute name
1767
+ * @returns the number; E_COLUMN_TYPE when absent or not a number
1768
+ */
1769
+ function vizNumber(attrs, name) {
1770
+ const text = attrs.get(name);
1771
+ if (text === undefined) {
1772
+ throw new GraphFormatError("E_COLUMN_TYPE", `viz element without a ${name} attribute`, { attribute: name });
1773
+ }
1774
+ const value = parseDecimalText(text);
1775
+ if (Number.isNaN(value)) {
1776
+ throw new GraphFormatError("E_COLUMN_TYPE", `viz ${name}="${text}" is not a number`, { attribute: name });
1777
+ }
1778
+ return value;
1779
+ }
1780
+ /**
1781
+ * Parse `<viz:color r g b a hex>` into rgba in 0..1 (design section 5.5: role color, f32 x4).
1782
+ * @param attrs - the element's attributes
1783
+ * @returns [r, g, b, a]
1784
+ */
1785
+ function parseColor(attrs) {
1786
+ let r;
1787
+ let g;
1788
+ let b;
1789
+ const hex = attrs.get("hex");
1790
+ if (hex !== undefined && !attrs.has("r")) {
1791
+ const digits = hex.startsWith("#") ? hex.slice(1) : hex;
1792
+ if (!/^[0-9a-fA-F]{6}$/.test(digits)) {
1793
+ throw new GraphFormatError("E_COLUMN_TYPE", `viz hex="${hex}" is not a #RRGGBB colour`, {
1794
+ attribute: "hex",
1795
+ });
1796
+ }
1797
+ r = Number.parseInt(digits.slice(0, 2), 16);
1798
+ g = Number.parseInt(digits.slice(2, 4), 16);
1799
+ b = Number.parseInt(digits.slice(4, 6), 16);
1800
+ }
1801
+ else {
1802
+ r = vizNumber(attrs, "r");
1803
+ g = vizNumber(attrs, "g");
1804
+ b = vizNumber(attrs, "b");
1805
+ }
1806
+ const a = attrs.has("a") ? vizNumber(attrs, "a") : 1;
1807
+ return [r / 255, g / 255, b / 255, a];
1808
+ }
1809
+ /**
1810
+ * Confidence that a document head is GEXF.
1811
+ * @param head - the first bytes
1812
+ * @returns 1 for a `<gexf` tag, 0.8 for a gexf.net namespace, 0 otherwise
1813
+ */
1814
+ function sniffGexf(head) {
1815
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(head);
1816
+ if (/<(\w+:)?gexf[\s>]/.test(text)) {
1817
+ return 1;
1818
+ }
1819
+ return text.includes("gexf.net") ? 0.8 : 0;
1820
+ }
1821
+ /**
1822
+ * Validate the format-specific options.
1823
+ * @param options - the caller's options
1824
+ * @returns whether viz elements are imported
1825
+ */
1826
+ function resolveGexfOptions(options) {
1827
+ const viz = options?.viz;
1828
+ if (viz === undefined) {
1829
+ return true;
1830
+ }
1831
+ if (typeof viz !== "boolean") {
1832
+ throw new GraphFormatError("E_UNSUPPORTED", `option viz: ${typeof viz} is not a boolean`, {
1833
+ option: "viz",
1834
+ found: viz,
1835
+ });
1836
+ }
1837
+ return viz;
1838
+ }
1839
+ /** The GEXF importer (design section 8.4). */
1840
+ export const gexfImporter = Object.freeze({
1841
+ format: GEXF_FORMAT,
1842
+ extensions: Object.freeze([".gexf"]),
1843
+ mimeTypes: Object.freeze(["application/gexf+xml", "application/xml", "text/xml"]),
1844
+ sniff: sniffGexf,
1845
+ /**
1846
+ * Read a GEXF document into the sink in one streaming pass.
1847
+ * @param input - the document as text, bytes, a stream or chunks
1848
+ * @param sink - the sink
1849
+ * @param options - format-specific and common options
1850
+ * @returns the report
1851
+ */
1852
+ async import(input, sink, options) {
1853
+ const resolved = resolveImportOptions(options, {
1854
+ ids: "canonical",
1855
+ defaultDirected: false,
1856
+ weightFrom: "weight",
1857
+ addMissingNodes: false,
1858
+ });
1859
+ const viz = resolveGexfOptions(options);
1860
+ const report = new ImportReportBuilder(GEXF_FORMAT, resolved.errorLimit);
1861
+ reportSinkOptions(sink, options, report, true);
1862
+ // nodeIdFrom is among the options reportUnusedOptions() reports: GEXF ids are mandatory
1863
+ reportUnusedOptions(options, report, USED_OPTIONS);
1864
+ const reader = new GexfReader(sink, report, resolved, viz);
1865
+ try {
1866
+ await tokenizeXml(textChunks(input, report, resolved), reader);
1867
+ }
1868
+ catch (err) {
1869
+ if (err instanceof XmlSyntaxError) {
1870
+ report.fail(XML_SYNTAX_CODE, err.message, { line: err.line });
1871
+ }
1872
+ throw err;
1873
+ }
1874
+ reader.finish();
1875
+ // an abort raised during the last few elements (after the last periodic check) still rejects
1876
+ throwIfAborted(resolved.signal);
1877
+ return report.finish();
1878
+ },
1879
+ });
1880
+ //# sourceMappingURL=importer.js.map