@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,1838 @@
1
+ /**
2
+ * The JSON importer (design sections 8.2, 8.4 and 8.5): one GraphImporter that sniffs the dialect
3
+ * of a parsed document -- NetworkX node-link (old `links` and new `edges` key forms, graph-level
4
+ * `directed` / `multigraph` / `graph`), the d3 lineage of the same shape (`name` ids, integer
5
+ * index endpoints), JSON Graph Format v2 (nodes keyed by id, per-edge `directed`, hyperedges),
6
+ * Cytoscape.js elements (`data.id` / `data.source` / `data.target`, `position`, `classes`,
7
+ * `data.parent`), graphology serialisation (`key` / `attributes`, `undirected` edges, `options`)
8
+ * and vis.js (`from` / `to`) -- and pushes it scalar by scalar into the sink.
9
+ *
10
+ * JSON awaits the whole text (design section 8.4: `JSON.parse` on 100 MB is fine; a streaming
11
+ * tokeniser is a later improvement). The parsed records are iterated in place; the importer never
12
+ * builds an intermediate array of node or edge objects. Ids are coerced per `ids` ("keep" by
13
+ * default: JSON values are already typed); node ids that are JSON `true` / `false` / `null` are
14
+ * reported as `unsupported` and coerced with `String(v)` only under `ids: "string"`. Attribute
15
+ * columns are inferred per column by the sink (design section 5.1); the structural fields of a
16
+ * dialect (Cytoscape `position` / `classes` / `parent`, JGF `label` / `relation`, edge ids) are
17
+ * declared up front with their roles when the file uses them. Direction goes through the
18
+ * DirectionResolver of design section 8.4; per-element errors are aggregated into the ImportReport
19
+ * until the error limit (design section 8.6).
20
+ *
21
+ * Fatal errors (ImportError at once): empty input, invalid JSON, an unrecognised top-level shape, a
22
+ * section that is not an array / object. Recoverable errors (an issue, the element skipped): a
23
+ * missing nodes or edges array, a node without an id, an edge without an endpoint, a bad index
24
+ * endpoint, a declared field of the wrong type, an unknown Cytoscape parent, an id the coercion rule
25
+ * rejects.
26
+ */
27
+ import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
28
+ import { uniqueColumnName } from "../../common/attributes.js";
29
+ import { DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE, HYPEREDGE_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
30
+ import { DirectionResolver } from "../../common/direction.js";
31
+ import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
32
+ import { readText, throwIfAborted } from "../../common/input.js";
33
+ import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
34
+ import { ImportReportBuilder } from "../../common/report.js";
35
+ import { weightFromValue } from "../../common/weights.js";
36
+ import { CLASSES_COLUMN, CYTOSCAPE_ELEMENT_KEYS, CYTOSCAPE_STRUCTURAL_KEYS, DIALECT_DEFAULT_DIRECTED, hasKey, isJsonDialect, isJsonObject, JSON_DIALECTS, META_KEY, NODE_LINK_SOURCE_KEYS, NODE_LINK_TARGET_KEYS, PARENT_COLUMN, POSITION_COLUMN, sniffJsonDialect, SUFFIX, } from "./dialect.js";
37
+ /**
38
+ * The issue codes the JSON importer records (design section 8.6), by name: the codes shared with
39
+ * the other importers (src/common/codes.ts) and the JSON-specific ones. A key is the code without
40
+ * its severity and format prefixes.
41
+ */
42
+ export const JSON_ISSUE = Object.freeze({
43
+ /** The text is empty or whitespace (fatal). */
44
+ EMPTY_INPUT: EMPTY_INPUT_CODE,
45
+ /** JSON.parse refused the text (fatal). */
46
+ SYNTAX: SYNTAX_CODE,
47
+ /** No dialect matches the document's top-level shape. */
48
+ DIALECT: "E_JSON_DIALECT",
49
+ /** A section (nodes, edges, elements, graph) has the wrong JSON type. */
50
+ SHAPE: "E_JSON_SHAPE",
51
+ /** A node-link document lacks its nodes or its edges array, or a Cytoscape document its elements. */
52
+ MISSING_SECTION: "E_MISSING_SECTION",
53
+ /** A node record or an element is not an object. */
54
+ BAD_ELEMENT: "E_BAD_ELEMENT",
55
+ /** A node record has no id. */
56
+ MISSING_ID: MISSING_ID_CODE,
57
+ /** A node id is a JSON boolean or null (legal in NetworkX, not a NodeId); coerced only under ids "string". */
58
+ UNSUPPORTED_ID: "E_UNSUPPORTED_ID",
59
+ /** An edge record has no source or no target. */
60
+ MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
61
+ /** An index endpoint is not an integer below the node count, or names a skipped node. */
62
+ BAD_INDEX: "E_BAD_INDEX",
63
+ /** A declared field has the wrong JSON type (JGF label / relation / metadata, Cytoscape position / classes). */
64
+ BAD_VALUE: "E_BAD_VALUE",
65
+ /** A graph-level flag (`directed`, `multigraph`, graphology `options`) has the wrong type; the default is used. */
66
+ BAD_FLAG: "W_BAD_FLAG",
67
+ /** A Cytoscape `data.parent` names an unknown node. */
68
+ UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
69
+ /** A node id repeated by a later record; the records are merged (the later attributes win). */
70
+ DUPLICATE_NODE: DUPLICATE_NODE_CODE,
71
+ /** An edge id (Cytoscape data.id, graphology key, vis id) repeated by a later edge; the edge is skipped. */
72
+ DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
73
+ /** Two distinct id texts merged into one number under ids "number". */
74
+ ID_MERGED: ID_MERGED_CODE,
75
+ /** Edge ids of mixed JSON types were stored as text. */
76
+ EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
77
+ /** A JGF `graphs` array holds more than one graph; only `graphIndex` is read. */
78
+ MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
79
+ /** JGF hyperedges under the "error" policy. */
80
+ HYPEREDGE: HYPEREDGE_CODE,
81
+ /** JGF hyperedges skipped under the default "skip" policy. */
82
+ HYPEREDGES_SKIPPED: "W_HYPEREDGES_SKIPPED",
83
+ /** A JGF hyperedge with neither a nodes array nor source / target arrays. */
84
+ HYPEREDGE_SHAPE: "E_HYPEREDGE_SHAPE",
85
+ /** The nodes have no id key at all; array positions became the ids. */
86
+ POSITIONAL_NODES: "W_POSITIONAL_NODES",
87
+ /** A builder-policy option (addMissingNodes, duplicateEdges, selfLoops, weightDtype) differs from the sink's (the shared W_SINK_OPTION). */
88
+ SINK_OPTION: SINK_OPTION_CODE,
89
+ /** A common option the dialect has no use for (nodeIdFrom outside node-link, long, restoreMangledIds). */
90
+ OPTION_IGNORED: OPTION_IGNORED_CODE,
91
+ });
92
+ /** The common options the JSON importer reads (the rest is reported by reportUnusedOptions). */
93
+ const USED_OPTIONS = new Set([
94
+ "ids",
95
+ "nodeIdFrom",
96
+ "addMissingNodes",
97
+ "duplicateEdges",
98
+ "selfLoops",
99
+ "onMixedDirection",
100
+ "defaultDirected",
101
+ "weightFrom",
102
+ "weightDtype",
103
+ "hyperedges",
104
+ "errorLimit",
105
+ "signal",
106
+ "onProgress",
107
+ ]);
108
+ const FORMAT_DEFAULTS = { ids: "keep", defaultDirected: false, weightFrom: "weight" };
109
+ /** Bytes of the head sniff() inspects. */
110
+ const SNIFF_BYTES = 4096;
111
+ /** Elements pushed between two checks of the cancellation signal (the whole document is one chunk). */
112
+ const ABORT_CHECK_INTERVAL = 64;
113
+ /** The top-level keys whose presence in the head marks a graph document rather than arbitrary JSON. */
114
+ const SNIFF_KEYS = ['"nodes"', '"links"', '"edges"', '"elements"', '"graph"', '"graphs"'];
115
+ const BOM = String.fromCharCode(0xfeff);
116
+ /** The JGF edge keys that are not metadata. */
117
+ const JGF_EDGE_KEYS = new Set([
118
+ "id",
119
+ "source",
120
+ "target",
121
+ "relation",
122
+ "directed",
123
+ "label",
124
+ "metadata",
125
+ ]);
126
+ /** The JGF hyperedge keys that are not metadata. */
127
+ const JGF_HYPEREDGE_KEYS = new Set([...JGF_EDGE_KEYS, "nodes"]);
128
+ /** The JGF node keys that are not metadata. */
129
+ const JGF_NODE_KEYS = new Set(["label", "metadata"]);
130
+ /** The graphology node keys that are not attributes. */
131
+ const GRAPHOLOGY_NODE_KEYS = new Set(["key", "attributes"]);
132
+ /** The graphology edge keys that are not attributes. */
133
+ const GRAPHOLOGY_EDGE_KEYS = new Set(["key", "source", "target", "attributes", "undirected"]);
134
+ /** The vis.js endpoint keys. */
135
+ const VIS_SOURCE_KEYS = Object.freeze(["from"]);
136
+ const VIS_TARGET_KEYS = Object.freeze(["to"]);
137
+ /**
138
+ * Check the format-specific options.
139
+ * @param options - the caller's options
140
+ * @returns the resolved options; E_UNSUPPORTED for a bad value
141
+ */
142
+ function resolveJsonOptions(options) {
143
+ const o = options ?? {};
144
+ const dialect = o.dialect ?? "auto";
145
+ if (dialect !== "auto" && !isJsonDialect(dialect)) {
146
+ throw unsupportedOption("dialect", dialect, [...JSON_DIALECTS, "auto"]);
147
+ }
148
+ const indexLinks = o.indexLinks ?? "auto";
149
+ if (indexLinks !== "auto" && typeof indexLinks !== "boolean") {
150
+ throw unsupportedOption("indexLinks", indexLinks, ["true", "false", "auto"]);
151
+ }
152
+ const graphIndex = o.graphIndex ?? 0;
153
+ if (!Number.isInteger(graphIndex) || graphIndex < 0) {
154
+ throw unsupportedOption("graphIndex", graphIndex, ["a non-negative integer"]);
155
+ }
156
+ return {
157
+ dialect,
158
+ nodeIdKey: keyOption("nodeIdKey", o.nodeIdKey),
159
+ edgesKey: keyOption("edgesKey", o.edgesKey),
160
+ sourceKey: keyOption("sourceKey", o.sourceKey),
161
+ targetKey: keyOption("targetKey", o.targetKey),
162
+ indexLinks,
163
+ graphIndex,
164
+ };
165
+ }
166
+ /**
167
+ * Check a key-valued option.
168
+ * @param name - the option name
169
+ * @param value - the caller's value
170
+ * @returns the key, or null when absent
171
+ */
172
+ function keyOption(name, value) {
173
+ if (value === undefined) {
174
+ return null;
175
+ }
176
+ if (typeof value !== "string" || value.length === 0) {
177
+ throw unsupportedOption(name, value, ["a non-empty key"]);
178
+ }
179
+ return value;
180
+ }
181
+ /**
182
+ * The E_UNSUPPORTED error of a bad format-specific option (the core's convention, as in the common
183
+ * option module).
184
+ * @param name - the option name
185
+ * @param found - the value
186
+ * @param supported - what is accepted
187
+ * @returns the error
188
+ */
189
+ function unsupportedOption(name, found, supported) {
190
+ return new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${describe(found)} is not one of ${supported.join(", ")}`, {
191
+ option: name,
192
+ found: typeof found === "string" ? found : typeof found,
193
+ supported: [...supported],
194
+ });
195
+ }
196
+ /**
197
+ * A short description of a value for messages.
198
+ * @param value - the value
199
+ * @returns JSON for primitives, "array" or the type name otherwise
200
+ */
201
+ function describe(value) {
202
+ if (value === null) {
203
+ return "null";
204
+ }
205
+ if (Array.isArray(value)) {
206
+ return "array";
207
+ }
208
+ switch (typeof value) {
209
+ case "string":
210
+ case "number":
211
+ case "boolean":
212
+ return JSON.stringify(value);
213
+ default:
214
+ return typeof value;
215
+ }
216
+ }
217
+ // ============================================================ attribute writer
218
+ /**
219
+ * Writes inferred attribute cells for one table, caching the handle of every column after its
220
+ * first write so the hot loop never looks a column up by name twice. Names that collide with a
221
+ * structural column declared up front are suffixed deterministically (design section 5.6).
222
+ */
223
+ class AttributeWriter {
224
+ /**
225
+ * Create a writer.
226
+ * @param sink - the sink
227
+ * @param domain - node or edge
228
+ */
229
+ constructor(sink, domain) {
230
+ this.handles = new Map();
231
+ this.reservedNames = new Set();
232
+ this.sink = sink;
233
+ this.domain = domain;
234
+ }
235
+ /**
236
+ * Declare a structural column up front; attribute keys with its name are suffixed from now on.
237
+ * @param decl - the declaration
238
+ * @returns the handle
239
+ */
240
+ declare(decl) {
241
+ this.reservedNames.add(decl.name);
242
+ const handle = this.domain === "node" ? this.sink.declareNodeColumn(decl) : this.sink.declareEdgeColumn(decl);
243
+ this.handles.set(decl.name, handle);
244
+ return handle;
245
+ }
246
+ /**
247
+ * Declare a structural column only when the file uses it.
248
+ * @param decl - the declaration
249
+ * @param present - whether any element carries the field
250
+ * @returns the handle, or INVALID_INDEX when not declared
251
+ */
252
+ declareIf(decl, present) {
253
+ return present ? this.declare(decl) : INVALID_INDEX;
254
+ }
255
+ /**
256
+ * Whether a name is taken in the sink's table (for the deterministic rename rule).
257
+ * @param name - the column name
258
+ * @returns true when a column of that name exists
259
+ */
260
+ taken(name) {
261
+ return this.lookup(name) !== INVALID_INDEX;
262
+ }
263
+ /**
264
+ * Write one attribute cell by its source key; null and undefined leave the row unset.
265
+ * @param row - the node or edge index
266
+ * @param key - the source key
267
+ * @param value - the JSON value
268
+ * @param suffix - the suffix applied when the key collides with a structural column
269
+ */
270
+ write(row, key, value, suffix) {
271
+ if (value === undefined || value === null) {
272
+ return;
273
+ }
274
+ const name = this.reservedNames.has(key) ? `${key}${suffix}` : key;
275
+ const cached = this.handles.get(name);
276
+ if (cached !== undefined) {
277
+ this.set(cached, row, value);
278
+ return;
279
+ }
280
+ this.set(name, row, value);
281
+ const handle = this.lookup(name);
282
+ if (handle !== INVALID_INDEX) {
283
+ this.handles.set(name, handle);
284
+ }
285
+ }
286
+ /**
287
+ * Write through a handle or a name.
288
+ * @param column - the handle or name
289
+ * @param row - the row
290
+ * @param value - the value
291
+ */
292
+ set(column, row, value) {
293
+ if (this.domain === "node") {
294
+ this.sink.setNodeValue(column, row, value);
295
+ }
296
+ else {
297
+ this.sink.setEdgeValue(column, row, value);
298
+ }
299
+ }
300
+ /**
301
+ * Look a column up by name.
302
+ * @param name - the column name
303
+ * @returns the handle, or INVALID_INDEX
304
+ */
305
+ lookup(name) {
306
+ return this.domain === "node" ? this.sink.nodeColumn(name) : this.sink.edgeColumn(name);
307
+ }
308
+ }
309
+ // ============================================================ the import context
310
+ /**
311
+ * Everything one import call shares between the dialect readers.
312
+ */
313
+ class ImportContext {
314
+ /**
315
+ * Create the context.
316
+ * @param sink - the sink
317
+ * @param report - the report
318
+ * @param options - the resolved common options
319
+ * @param json - the resolved format options
320
+ * @param explicitDefaultDirected - whether the caller passed defaultDirected
321
+ */
322
+ constructor(sink, report, options, json, explicitDefaultDirected) {
323
+ /** Nodes and edges pushed since the signal was last checked. */
324
+ this.elementsSinceCheck = 0;
325
+ /** The direction the file declares (or the default), set by setHeader(). */
326
+ this.fileDirected = false;
327
+ this.sink = sink;
328
+ this.report = report;
329
+ this.options = options;
330
+ this.json = json;
331
+ this.ids = new IdCoercer(options.ids);
332
+ this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
333
+ this.nodes = new AttributeWriter(sink, "node");
334
+ this.edges = new AttributeWriter(sink, "edge");
335
+ this.explicitDefaultDirected = explicitDefaultDirected;
336
+ }
337
+ /**
338
+ * Report a `nodeIdFrom` other than "id" for a dialect whose ids are unambiguous.
339
+ * @param dialect - the dialect
340
+ * @param idField - where the dialect's ids come from, for the message
341
+ */
342
+ reportNodeIdFrom(dialect, idField) {
343
+ if (this.options.nodeIdFrom !== "id") {
344
+ this.report.warning("unsupported", JSON_ISSUE.OPTION_IGNORED, `nodeIdFrom "${this.options.nodeIdFrom}" does not apply to ${dialect}; ids are read from ${idField}`, { element: "nodeIdFrom" });
345
+ }
346
+ }
347
+ /**
348
+ * The direction assumed when the file declares none: the caller's `defaultDirected` when given,
349
+ * else the dialect's convention.
350
+ * @param dialect - the dialect
351
+ * @returns the direction
352
+ */
353
+ defaultDirected(dialect) {
354
+ return this.explicitDefaultDirected ? this.options.defaultDirected : DIALECT_DEFAULT_DIRECTED[dialect];
355
+ }
356
+ /**
357
+ * Rule 1 of design section 8.4: set the sink's direction from the file's header (or the
358
+ * dialect's default) before the first edge, remembering the file's direction for uniformKind().
359
+ * @param directed - the file's direction
360
+ */
361
+ setHeader(directed) {
362
+ this.fileDirected = directed;
363
+ this.direction.setHeader(directed);
364
+ }
365
+ /**
366
+ * The edge kind every edge of a dialect without per-edge direction has: the file's direction
367
+ * (the resolver expands it when the sink's direction differs).
368
+ * @returns the kind
369
+ */
370
+ uniformKind() {
371
+ return this.fileDirected ? "directed" : "undirected";
372
+ }
373
+ /**
374
+ * Coerce a node id value per the `ids` option, reporting what cannot be an id. A JSON boolean or
375
+ * null is `unsupported` unless `ids` is "string" (design section 8.5); anything the rule rejects
376
+ * is recorded through the per-element catch.
377
+ * @param raw - the JSON value; undefined when the record has no id key
378
+ * @param element - the element name for the issue
379
+ * @returns the id, or null when the value was reported and the element must be skipped
380
+ */
381
+ coerceId(raw, element) {
382
+ if (raw === undefined) {
383
+ this.report.error("missing-value", JSON_ISSUE.MISSING_ID, `${element} has no id`, { element });
384
+ return null;
385
+ }
386
+ if ((typeof raw === "boolean" || raw === null) && this.options.ids !== "string") {
387
+ const kind = raw === null ? "null" : "boolean";
388
+ this.report.error("unsupported", JSON_ISSUE.UNSUPPORTED_ID, `${element}: a JSON ${kind} is not a node id (pass ids: "string" to coerce it)`, { element });
389
+ return null;
390
+ }
391
+ let id;
392
+ try {
393
+ id = this.ids.value(this.options.ids === "string" && typeof raw === "number" ? String(raw) : raw);
394
+ }
395
+ catch (err) {
396
+ this.report.recordError(err, { element });
397
+ return null;
398
+ }
399
+ const merge = this.ids.lastMerge;
400
+ if (merge !== null) {
401
+ this.report.warning("coercion", JSON_ISSUE.ID_MERGED, `id text ${JSON.stringify(merge.text)} merged with ${JSON.stringify(merge.previousText)} as ${merge.id}`, { element });
402
+ }
403
+ return id;
404
+ }
405
+ /**
406
+ * Coerce an id that must be valid for the caller to proceed (hyperedge members): the rejections
407
+ * of coerceId() are thrown instead of recorded.
408
+ * @param raw - the JSON value
409
+ * @param element - the element name
410
+ * @returns the id
411
+ */
412
+ requireId(raw, element) {
413
+ if ((typeof raw === "boolean" || raw === null) && this.options.ids !== "string") {
414
+ const kind = raw === null ? "null" : "boolean";
415
+ throw new GraphFormatError("E_INVALID_ID", `${element}: a JSON ${kind} is not a node id`, {
416
+ reason: "unsupported id",
417
+ });
418
+ }
419
+ return this.ids.value(this.options.ids === "string" && typeof raw === "number" ? String(raw) : raw);
420
+ }
421
+ /**
422
+ * Add a node, counting it or recording the failure.
423
+ * @param id - the node id
424
+ * @param element - the element name
425
+ * @returns the node index, or -1 when the sink refused the node
426
+ */
427
+ pushNode(id, element) {
428
+ this.checkAbort();
429
+ const existing = this.sink.indexOf(id);
430
+ if (existing !== INVALID_INDEX) {
431
+ this.report.warning("merged", JSON_ISSUE.DUPLICATE_NODE, `${element}: node ${JSON.stringify(id)} already exists; its attributes are merged (the later values win)`, { element });
432
+ return existing;
433
+ }
434
+ let index;
435
+ try {
436
+ index = this.sink.addNode(id);
437
+ }
438
+ catch (err) {
439
+ this.skip(err, "node", element);
440
+ return -1;
441
+ }
442
+ this.report.counts.nodes++;
443
+ return index;
444
+ }
445
+ /**
446
+ * Push one edge through the direction resolver, counting every logical edge the sink gained
447
+ * (both halves of an expanded edge, and the mirrors of an in-place expansion).
448
+ * @param source - the source id
449
+ * @param target - the target id
450
+ * @param kind - the edge's direction in the file
451
+ * @param weight - the weight, or undefined
452
+ * @param element - the element name for issues
453
+ * @returns the primary edge index
454
+ */
455
+ pushEdge(source, target, kind, weight, element) {
456
+ this.checkAbort();
457
+ const before = this.sink.edgeCount;
458
+ // endpoints the sink creates (addMissingNodes) count as nodes too
459
+ let created = this.sink.indexOf(source) === INVALID_INDEX ? 1 : 0;
460
+ if (source !== target && this.sink.indexOf(target) === INVALID_INDEX) {
461
+ created++;
462
+ }
463
+ const edge = this.direction.addEdge(source, target, kind, weight, { element });
464
+ this.report.counts.edges += this.sink.edgeCount - before;
465
+ this.report.counts.nodes += created;
466
+ return edge;
467
+ }
468
+ /**
469
+ * Check the cancellation signal every ABORT_CHECK_INTERVAL pushed elements, so an abort raised
470
+ * while the whole in-memory document is being walked rejects promptly.
471
+ */
472
+ checkAbort() {
473
+ if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
474
+ this.elementsSinceCheck = 0;
475
+ throwIfAborted(this.options.signal);
476
+ }
477
+ }
478
+ /**
479
+ * Record a per-element failure and count the skipped element.
480
+ * @param err - the thrown value
481
+ * @param domain - which counter to bump
482
+ * @param element - the element name
483
+ */
484
+ skip(err, domain, element) {
485
+ this.report.recordError(err, { element });
486
+ if (domain === "node") {
487
+ this.report.counts.skippedNodes++;
488
+ }
489
+ else {
490
+ this.report.counts.skippedEdges++;
491
+ }
492
+ }
493
+ /**
494
+ * Report an element that is not an object and count it as skipped.
495
+ * @param domain - node or edge
496
+ * @param element - the element name
497
+ * @param what - what was expected
498
+ */
499
+ badElement(domain, element, what = "an object") {
500
+ this.report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not ${what}`, { element });
501
+ this.countSkipped(domain);
502
+ }
503
+ /**
504
+ * Count a skipped element whose issue was already recorded.
505
+ * @param domain - node or edge
506
+ */
507
+ countSkipped(domain) {
508
+ if (domain === "node") {
509
+ this.report.counts.skippedNodes++;
510
+ }
511
+ else {
512
+ this.report.counts.skippedEdges++;
513
+ }
514
+ }
515
+ /**
516
+ * Report an edge record without an endpoint and count it as skipped.
517
+ * @param element - the element name
518
+ * @param field - the missing field
519
+ */
520
+ missingEndpoint(element, field) {
521
+ this.report.error("missing-value", JSON_ISSUE.MISSING_ENDPOINT, `${element} has no ${field}`, { element });
522
+ this.report.counts.skippedEdges++;
523
+ }
524
+ /**
525
+ * Record the shape metadata, the source format and further metadata fields in one setMeta()
526
+ * call (the sink replaces `extra` as a whole).
527
+ * @param shape - the shape record
528
+ * @param patch - further metadata fields
529
+ */
530
+ setMeta(shape, patch = {}) {
531
+ const extra = {};
532
+ for (const [key, value] of Object.entries(shape)) {
533
+ if (value !== undefined) {
534
+ extra[key] = value;
535
+ }
536
+ }
537
+ this.sink.setMeta({ sourceFormat: "json", ...patch, extra: { [META_KEY]: extra } });
538
+ }
539
+ /**
540
+ * Record which source field the weight came from, so the exporter writes it back under the same
541
+ * key (design section 3.7, `meta.weightOrigin`).
542
+ * @returns the metadata patch, empty for an unweighted import
543
+ */
544
+ weightOriginPatch() {
545
+ const { weightFrom } = this.options;
546
+ if (weightFrom === null) {
547
+ return {};
548
+ }
549
+ return { weightOrigin: { format: "json", id: weightFrom, title: null, type: null, namespace: null } };
550
+ }
551
+ /**
552
+ * Write the graph-level attributes of a dict (NetworkX `graph`, JGF `metadata`, graphology
553
+ * `attributes`, Cytoscape `data`), one column per key.
554
+ * @param dict - the dict, or anything else (then reported)
555
+ * @param what - the dict's name for the issue
556
+ */
557
+ writeGraphDict(dict, what) {
558
+ if (dict === undefined || dict === null) {
559
+ return;
560
+ }
561
+ if (!isJsonObject(dict)) {
562
+ this.report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `${what} is not an object; ignored`, {
563
+ element: what,
564
+ });
565
+ return;
566
+ }
567
+ for (const key of Object.keys(dict)) {
568
+ const value = dict[key];
569
+ if (value === undefined || value === null) {
570
+ continue;
571
+ }
572
+ try {
573
+ this.sink.setGraphValue(key, value);
574
+ }
575
+ catch (err) {
576
+ this.report.recordError(err, { element: `${what}.${key}` });
577
+ }
578
+ }
579
+ }
580
+ /**
581
+ * Read the weight field of an edge record.
582
+ * @param record - the record holding the attributes
583
+ * @returns the weight, or undefined when absent or null; E_INVALID_WEIGHT otherwise
584
+ */
585
+ weightOf(record) {
586
+ const { weightFrom } = this.options;
587
+ if (weightFrom === null || !hasKey(record, weightFrom)) {
588
+ return undefined;
589
+ }
590
+ return weightFromValue(record[weightFrom]);
591
+ }
592
+ /**
593
+ * Write the attributes of a nested dict plus the element-level keys the dialect does not
594
+ * define (kept with the `#element` suffix).
595
+ * @param writer - the table writer
596
+ * @param row - the row
597
+ * @param record - the element record
598
+ * @param dict - the nested attribute dict
599
+ * @param structural - the element keys that are not attributes
600
+ * @param weightFrom - the weight key to skip in the dict, or null
601
+ */
602
+ writeNested(writer, row, record, dict, structural, weightFrom) {
603
+ for (const key of Object.keys(dict)) {
604
+ if (key !== weightFrom) {
605
+ writer.write(row, key, dict[key], SUFFIX.data);
606
+ }
607
+ }
608
+ for (const key of Object.keys(record)) {
609
+ if (!structural.has(key)) {
610
+ writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data);
611
+ }
612
+ }
613
+ }
614
+ /**
615
+ * Write a spec-typed string field (JGF label / relation), reporting a value of another type.
616
+ * @param writer - the table writer
617
+ * @param column - the declared column, or INVALID_INDEX when the file has no such field
618
+ * @param row - the row
619
+ * @param value - the value
620
+ * @param field - the field name
621
+ * @param element - the element name
622
+ */
623
+ writeStringField(writer, column, row, value, field, element) {
624
+ if (column === INVALID_INDEX || value === undefined || value === null) {
625
+ return;
626
+ }
627
+ if (typeof value !== "string") {
628
+ this.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: ${field} must be a string, found ${describe(value)}`, { element });
629
+ return;
630
+ }
631
+ writer.set(column, row, value);
632
+ }
633
+ /**
634
+ * Declare an edge id column with role "id" from a scan of the file's edge ids: f64 when every
635
+ * id is a number, string otherwise (numbers are then stored as their text and reported once).
636
+ * @param edges - the edge records
637
+ * @param read - how to read an edge's raw id
638
+ * @param name - the column name
639
+ * @param unique - whether uniqueness is enforced at freeze
640
+ * @returns the column, or null when no edge has an id
641
+ */
642
+ declareEdgeIds(edges, read, name, unique) {
643
+ let numbers = 0;
644
+ let strings = 0;
645
+ for (const edge of edges) {
646
+ if (!isJsonObject(edge)) {
647
+ continue;
648
+ }
649
+ const raw = read(edge);
650
+ if (typeof raw === "number") {
651
+ numbers++;
652
+ }
653
+ else if (typeof raw === "string") {
654
+ strings++;
655
+ }
656
+ }
657
+ if (numbers + strings === 0) {
658
+ return null;
659
+ }
660
+ const dtype = strings === 0 ? "f64" : "string";
661
+ const columnName = uniqueColumnName(name, "id", (candidate) => this.edges.taken(candidate));
662
+ const handle = this.edges.declare({ name: columnName, dtype, role: "id", nullable: true, unique });
663
+ const stringify = strings > 0 && numbers > 0;
664
+ const seen = unique ? new Set() : null;
665
+ if (stringify) {
666
+ this.report.warning("coercion", JSON_ISSUE.EDGE_ID_STRINGIFIED, `edge ids mix numbers and strings; ${numbers} numeric id(s) stored as text`, { element: columnName });
667
+ }
668
+ return { handle, stringify, seen };
669
+ }
670
+ /**
671
+ * The value an edge id column stores for a raw id, checked BEFORE the edge is pushed so a bad
672
+ * id skips the edge without touching the sink (design section 11.1).
673
+ * @param column - the column, or null when the file has no edge ids
674
+ * @param raw - the raw id value
675
+ * @returns the value to store, or null when there is nothing to store
676
+ */
677
+ edgeIdValue(column, raw) {
678
+ if (column === null || raw === undefined || raw === null) {
679
+ return null;
680
+ }
681
+ let value;
682
+ if (typeof raw === "number") {
683
+ value = column.stringify ? String(raw) : raw;
684
+ }
685
+ else if (typeof raw === "string") {
686
+ value = raw;
687
+ }
688
+ else {
689
+ throw new GraphFormatError("E_COLUMN_TYPE", `an edge id must be a string or a number, found ${describe(raw)}`, { found: typeof raw });
690
+ }
691
+ if (column.seen !== null) {
692
+ const text = String(value);
693
+ if (column.seen.has(text)) {
694
+ throw new GraphFormatError(JSON_ISSUE.DUPLICATE_EDGE_ID, `edge id ${JSON.stringify(value)} is declared more than once; the edge is skipped`, { id: value });
695
+ }
696
+ column.seen.add(text);
697
+ }
698
+ return value;
699
+ }
700
+ /**
701
+ * Write an edge id value from edgeIdValue().
702
+ * @param column - the column, or null
703
+ * @param edge - the edge index
704
+ * @param value - the value, or null for none
705
+ */
706
+ setEdgeId(column, edge, value) {
707
+ if (column !== null && value !== null) {
708
+ this.edges.set(column.handle, edge, value);
709
+ }
710
+ }
711
+ }
712
+ // ============================================================ document level
713
+ /**
714
+ * Parse the whole text; syntax errors and empty input abort the import.
715
+ * @param text - the decoded text
716
+ * @param report - the report
717
+ * @returns the parsed value
718
+ */
719
+ function parseDocument(text, report) {
720
+ if (text.trim().length === 0) {
721
+ report.fail(JSON_ISSUE.EMPTY_INPUT, "the input is empty");
722
+ }
723
+ try {
724
+ return JSON.parse(text);
725
+ }
726
+ catch (err) {
727
+ const message = err instanceof Error ? err.message : String(err);
728
+ return report.fail(JSON_ISSUE.SYNTAX, `invalid JSON: ${message}`);
729
+ }
730
+ }
731
+ /**
732
+ * The dialect to read: the forced one, else the shape rule of sniffJsonDialect(); a document
733
+ * that matches no dialect is a fatal E_JSON_DIALECT.
734
+ * @param root - the parsed document
735
+ * @param forced - the caller's dialect option
736
+ * @param report - the report the failure is recorded in
737
+ * @returns the dialect
738
+ */
739
+ function detectDialect(root, forced, report) {
740
+ if (forced !== "auto") {
741
+ return forced;
742
+ }
743
+ const dialect = sniffJsonDialect(root);
744
+ if (dialect !== null) {
745
+ return dialect;
746
+ }
747
+ if (Array.isArray(root)) {
748
+ return report.fail(JSON_ISSUE.DIALECT, "a top-level array is only read as Cytoscape elements (objects with a data record)");
749
+ }
750
+ if (!isJsonObject(root)) {
751
+ return report.fail(JSON_ISSUE.DIALECT, `the document is a JSON ${describe(root)}, not a graph object`);
752
+ }
753
+ return report.fail(JSON_ISSUE.DIALECT, "no known dialect: expected nodes / links / edges (node-link), elements (Cytoscape) or graph (JGF)");
754
+ }
755
+ /**
756
+ * A section that must be an array: fail when it is something else.
757
+ * @param value - the section
758
+ * @param what - its name
759
+ * @param report - the report
760
+ * @returns the array, or null when absent
761
+ */
762
+ function arraySection(value, what, report) {
763
+ if (value === undefined || value === null) {
764
+ return null;
765
+ }
766
+ if (!Array.isArray(value)) {
767
+ return report.fail(JSON_ISSUE.SHAPE, `${what} must be an array, found ${describe(value)}`, { element: what });
768
+ }
769
+ return value;
770
+ }
771
+ /**
772
+ * A boolean flag with a default and a warning when it has another type.
773
+ * @param value - the flag value
774
+ * @param what - its name
775
+ * @param fallback - the default
776
+ * @param report - the report
777
+ * @returns the flag
778
+ */
779
+ function flagOf(value, what, fallback, report) {
780
+ if (value === undefined || value === null) {
781
+ return fallback;
782
+ }
783
+ if (typeof value === "boolean") {
784
+ return value;
785
+ }
786
+ report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `${what} is ${describe(value)}, not a boolean; ${fallback} assumed`, { element: what });
787
+ return fallback;
788
+ }
789
+ /**
790
+ * The endpoint key and value of an edge record: the explicit key when given, else the first of the
791
+ * default keys the record has.
792
+ * @param record - the edge record
793
+ * @param explicit - the caller's key, or null
794
+ * @param defaults - the default keys
795
+ * @returns the key used (null when none) and its value
796
+ */
797
+ function endpointOf(record, explicit, defaults) {
798
+ if (explicit !== null) {
799
+ return { key: hasKey(record, explicit) ? explicit : null, value: record[explicit] };
800
+ }
801
+ for (const key of defaults) {
802
+ if (hasKey(record, key)) {
803
+ return { key, value: record[key] };
804
+ }
805
+ }
806
+ return { key: null, value: undefined };
807
+ }
808
+ /**
809
+ * Whether any object of an array has a key with a non-null value.
810
+ * @param items - the array
811
+ * @param key - the key
812
+ * @returns true when some element carries the field
813
+ */
814
+ function anyHas(items, key) {
815
+ return items.some((item) => isJsonObject(item) && item[key] !== undefined && item[key] !== null);
816
+ }
817
+ /**
818
+ * Whether a value is a non-negative integer below a bound.
819
+ * @param value - the value
820
+ * @param bound - the exclusive bound
821
+ * @returns true for an index
822
+ */
823
+ function isIndexBelow(value, bound) {
824
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < bound;
825
+ }
826
+ // ============================================================ node-link / d3
827
+ /**
828
+ * Read a node-link or d3 document.
829
+ * @param ctx - the context
830
+ * @param root - the document
831
+ * @param dialect - "node-link" or "d3"
832
+ */
833
+ function importNodeLink(ctx, root, dialect) {
834
+ const { report, json } = ctx;
835
+ let { edgesKey } = json;
836
+ if (edgesKey === null) {
837
+ edgesKey = hasKey(root, "edges") || !hasKey(root, "links") ? "edges" : "links";
838
+ }
839
+ const nodes = arraySection(root.nodes, "nodes", report);
840
+ const edges = arraySection(root[edgesKey], edgesKey, report);
841
+ if (nodes === null) {
842
+ report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no nodes array; nodes come from the edges", { element: "nodes" });
843
+ }
844
+ if (edges === null) {
845
+ report.error("missing-value", JSON_ISSUE.MISSING_SECTION, `the document has no ${edgesKey} array; the graph has no edges`, { element: edgesKey });
846
+ }
847
+ const directed = flagOf(root.directed, "directed", ctx.defaultDirected(dialect), report);
848
+ const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
849
+ ctx.setHeader(directed);
850
+ ctx.writeGraphDict(root.graph, "graph");
851
+ const nodeList = nodes ?? [];
852
+ const edgeList = edges ?? [];
853
+ ctx.sink.reserve(nodeList.length, edgeList.length);
854
+ // the id key: the caller's, else "id" when any node has it, else "name" (d3); nodes without any
855
+ // id key are positional (nodeIdFrom "index", or a d3 v3 file whose nodes carry no id at all)
856
+ let { nodeIdKey } = json;
857
+ let positional = ctx.options.nodeIdFrom === "index";
858
+ if (!positional && nodeIdKey === null) {
859
+ const candidates = ctx.options.nodeIdFrom === "label" ? ["label", "name", "id"] : ["id", "name"];
860
+ nodeIdKey = candidates.find((key) => anyHas(nodeList, key)) ?? null;
861
+ if (ctx.options.nodeIdFrom === "label" && nodeIdKey === "id") {
862
+ report.warning("unsupported", JSON_ISSUE.OPTION_IGNORED, 'nodeIdFrom "label": no node has a label or name key; ids are read from "id"', { element: "nodeIdFrom" });
863
+ }
864
+ if (nodeIdKey === null) {
865
+ if (nodeList.length > 0) {
866
+ positional = true;
867
+ report.warning("missing-value", JSON_ISSUE.POSITIONAL_NODES, "no node has an id or name key; array positions are the node ids", { element: "nodes" });
868
+ }
869
+ else {
870
+ nodeIdKey = "id";
871
+ }
872
+ }
873
+ }
874
+ if (positional) {
875
+ nodeIdKey = null;
876
+ }
877
+ let indexLinks;
878
+ if (positional) {
879
+ indexLinks = true;
880
+ }
881
+ else if (json.indexLinks === "auto") {
882
+ indexLinks = nodeIdKey !== null && looksIndexLinked(nodeList, edgeList, nodeIdKey, json);
883
+ }
884
+ else {
885
+ ({ indexLinks } = json);
886
+ }
887
+ const positionIds = indexLinks ? [] : null;
888
+ for (let i = 0; i < nodeList.length; i++) {
889
+ const element = `nodes[${i}]`;
890
+ const record = nodeList[i];
891
+ let pushed = null;
892
+ if (!isJsonObject(record)) {
893
+ ctx.badElement("node", element);
894
+ }
895
+ else {
896
+ let id;
897
+ if (nodeIdKey === null) {
898
+ id = i;
899
+ }
900
+ else {
901
+ id = ctx.coerceId(hasKey(record, nodeIdKey) ? record[nodeIdKey] : undefined, element);
902
+ }
903
+ if (id === null) {
904
+ ctx.countSkipped("node");
905
+ }
906
+ else {
907
+ const index = ctx.pushNode(id, element);
908
+ if (index >= 0) {
909
+ pushed = id;
910
+ writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
911
+ }
912
+ }
913
+ }
914
+ positionIds?.push(pushed);
915
+ }
916
+ throwIfAborted(ctx.options.signal);
917
+ const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds);
918
+ ctx.setMeta({
919
+ dialect,
920
+ edgesKey,
921
+ nodeIdKey,
922
+ indexLinks,
923
+ sourceKey: endpointKeys.source ?? undefined,
924
+ targetKey: endpointKeys.target ?? undefined,
925
+ }, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
926
+ }
927
+ /**
928
+ * Read the node-link / d3 edge records: endpoints by id or array position, the weight, the other
929
+ * keys as attributes.
930
+ * @param ctx - the context
931
+ * @param edgeList - the edge records
932
+ * @param edgesKey - the top-level key they came from, for issues
933
+ * @param positionIds - the ids by array position under index links, or null
934
+ * @returns the source and target keys the first well-formed edge used (null when none did)
935
+ */
936
+ function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
937
+ const { json } = ctx;
938
+ const kind = ctx.uniformKind();
939
+ let sourceKey = null;
940
+ let targetKey = null;
941
+ for (let i = 0; i < edgeList.length; i++) {
942
+ const element = `${edgesKey}[${i}]`;
943
+ const record = edgeList[i];
944
+ if (!isJsonObject(record)) {
945
+ ctx.badElement("edge", element);
946
+ continue;
947
+ }
948
+ const source = endpointOf(record, json.sourceKey, NODE_LINK_SOURCE_KEYS);
949
+ const target = endpointOf(record, json.targetKey, NODE_LINK_TARGET_KEYS);
950
+ if (source.key === null || target.key === null) {
951
+ ctx.missingEndpoint(element, source.key === null ? "source" : "target");
952
+ continue;
953
+ }
954
+ sourceKey ?? (sourceKey = source.key);
955
+ targetKey ?? (targetKey = target.key);
956
+ try {
957
+ const u = resolveEndpoint(ctx, source.value, positionIds, element, "source");
958
+ const v = resolveEndpoint(ctx, target.value, positionIds, element, "target");
959
+ if (u === null || v === null) {
960
+ ctx.countSkipped("edge");
961
+ continue;
962
+ }
963
+ const weight = ctx.weightOf(record);
964
+ const edge = ctx.pushEdge(u, v, kind, weight, element);
965
+ const { weightFrom } = ctx.options;
966
+ for (const key of Object.keys(record)) {
967
+ if (key !== source.key && key !== target.key && key !== weightFrom) {
968
+ ctx.edges.write(edge, key, record[key], SUFFIX.data);
969
+ }
970
+ }
971
+ }
972
+ catch (err) {
973
+ ctx.skip(err, "edge", element);
974
+ }
975
+ }
976
+ return { source: sourceKey, target: targetKey };
977
+ }
978
+ /**
979
+ * Write the flat attributes of a node record (every own key the filter keeps).
980
+ * @param ctx - the context
981
+ * @param writer - the node writer
982
+ * @param index - the node index
983
+ * @param record - the record
984
+ * @param id - the node id, for issues
985
+ * @param keep - which keys are attributes
986
+ */
987
+ function writeFlat(ctx, writer, index, record, id, keep) {
988
+ try {
989
+ for (const key of Object.keys(record)) {
990
+ if (keep(key)) {
991
+ writer.write(index, key, record[key], SUFFIX.data);
992
+ }
993
+ }
994
+ }
995
+ catch (err) {
996
+ ctx.report.recordError(err, { element: String(id) });
997
+ }
998
+ }
999
+ /**
1000
+ * The d3 index-link heuristic: endpoints are array positions when every endpoint is a
1001
+ * non-negative integer and no node id is a number (a numeric id would make the endpoints ids;
1002
+ * research note 07: d3 links reference nodes by array index and are never coerced to ids). An
1003
+ * index at or beyond the node count is then E_BAD_INDEX, never a new numeric node.
1004
+ * @param nodes - the node records
1005
+ * @param edges - the edge records
1006
+ * @param nodeIdKey - the node id key
1007
+ * @param json - the format options (endpoint keys)
1008
+ * @returns true when endpoints are indices
1009
+ */
1010
+ function looksIndexLinked(nodes, edges, nodeIdKey, json) {
1011
+ if (edges.length === 0 || nodes.length === 0) {
1012
+ return false;
1013
+ }
1014
+ for (const node of nodes) {
1015
+ if (isJsonObject(node) && typeof node[nodeIdKey] === "number") {
1016
+ return false;
1017
+ }
1018
+ }
1019
+ let seen = 0;
1020
+ for (const edge of edges) {
1021
+ if (!isJsonObject(edge)) {
1022
+ continue;
1023
+ }
1024
+ const s = endpointOf(edge, json.sourceKey, NODE_LINK_SOURCE_KEYS).value;
1025
+ const t = endpointOf(edge, json.targetKey, NODE_LINK_TARGET_KEYS).value;
1026
+ if (!isIndexBelow(s, Infinity) || !isIndexBelow(t, Infinity)) {
1027
+ return false;
1028
+ }
1029
+ seen++;
1030
+ }
1031
+ return seen > 0;
1032
+ }
1033
+ /**
1034
+ * Resolve one endpoint: a node id through the coercion rule, or an array position through the
1035
+ * ids pushed so far under index links.
1036
+ * @param ctx - the context
1037
+ * @param raw - the endpoint value
1038
+ * @param positionIds - the ids by array position under index links, or null
1039
+ * @param element - the edge element name
1040
+ * @param field - "source" or "target"
1041
+ * @returns the id, or null when reported
1042
+ */
1043
+ function resolveEndpoint(ctx, raw, positionIds, element, field) {
1044
+ if (positionIds === null) {
1045
+ return ctx.coerceId(raw, `${element}.${field}`);
1046
+ }
1047
+ if (!isIndexBelow(raw, positionIds.length)) {
1048
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_INDEX, `${element}: ${field} ${describe(raw)} is not a node index below ${positionIds.length}`, { element });
1049
+ return null;
1050
+ }
1051
+ const id = positionIds[raw];
1052
+ if (id === null) {
1053
+ ctx.report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${field} names a node that was skipped`, {
1054
+ element,
1055
+ });
1056
+ }
1057
+ return id;
1058
+ }
1059
+ // ============================================================ vis.js
1060
+ /**
1061
+ * Read a vis.js document: `nodes` with `id`, `edges` with `from` / `to` and an optional `id`.
1062
+ * @param ctx - the context
1063
+ * @param root - the document
1064
+ */
1065
+ function importVis(ctx, root) {
1066
+ const { report, json } = ctx;
1067
+ const nodes = arraySection(root.nodes, "nodes", report) ?? [];
1068
+ const edges = arraySection(root.edges, "edges", report) ?? [];
1069
+ ctx.setHeader(ctx.defaultDirected("vis"));
1070
+ ctx.sink.reserve(nodes.length, edges.length);
1071
+ const nodeIdKey = json.nodeIdKey ?? "id";
1072
+ ctx.reportNodeIdFrom("vis", `the ${JSON.stringify(nodeIdKey)} key`);
1073
+ for (let i = 0; i < nodes.length; i++) {
1074
+ const element = `nodes[${i}]`;
1075
+ const record = nodes[i];
1076
+ if (!isJsonObject(record)) {
1077
+ ctx.badElement("node", element);
1078
+ continue;
1079
+ }
1080
+ const id = ctx.coerceId(hasKey(record, nodeIdKey) ? record[nodeIdKey] : undefined, element);
1081
+ if (id === null) {
1082
+ ctx.countSkipped("node");
1083
+ continue;
1084
+ }
1085
+ const index = ctx.pushNode(id, element);
1086
+ if (index >= 0) {
1087
+ writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
1088
+ }
1089
+ }
1090
+ throwIfAborted(ctx.options.signal);
1091
+ const ids = ctx.declareEdgeIds(edges, (edge) => edge.id, "id", true);
1092
+ const kind = ctx.uniformKind();
1093
+ let sourceKey = null;
1094
+ let targetKey = null;
1095
+ for (let i = 0; i < edges.length; i++) {
1096
+ const element = `edges[${i}]`;
1097
+ const record = edges[i];
1098
+ if (!isJsonObject(record)) {
1099
+ ctx.badElement("edge", element);
1100
+ continue;
1101
+ }
1102
+ const source = endpointOf(record, json.sourceKey, VIS_SOURCE_KEYS);
1103
+ const target = endpointOf(record, json.targetKey, VIS_TARGET_KEYS);
1104
+ if (source.key === null || target.key === null) {
1105
+ ctx.missingEndpoint(element, source.key === null ? "from" : "to");
1106
+ continue;
1107
+ }
1108
+ sourceKey ?? (sourceKey = source.key);
1109
+ targetKey ?? (targetKey = target.key);
1110
+ try {
1111
+ const u = ctx.coerceId(source.value, `${element}.from`);
1112
+ const v = ctx.coerceId(target.value, `${element}.to`);
1113
+ if (u === null || v === null) {
1114
+ ctx.countSkipped("edge");
1115
+ continue;
1116
+ }
1117
+ const idValue = ctx.edgeIdValue(ids, record.id);
1118
+ const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(record), element);
1119
+ ctx.setEdgeId(ids, edge, idValue);
1120
+ const { weightFrom } = ctx.options;
1121
+ for (const key of Object.keys(record)) {
1122
+ if (key !== source.key && key !== target.key && key !== "id" && key !== weightFrom) {
1123
+ ctx.edges.write(edge, key, record[key], SUFFIX.data);
1124
+ }
1125
+ }
1126
+ }
1127
+ catch (err) {
1128
+ ctx.skip(err, "edge", element);
1129
+ }
1130
+ }
1131
+ ctx.setMeta({ dialect: "vis", nodeIdKey, sourceKey: sourceKey ?? undefined, targetKey: targetKey ?? undefined }, ctx.weightOriginPatch());
1132
+ }
1133
+ // ============================================================ graphology
1134
+ /**
1135
+ * Read a graphology serialisation: `options.type` decides the header direction ("mixed" or absent:
1136
+ * from the edges' `undirected` flags), `options.multi` the declared multigraph flag, node `key`
1137
+ * the id, `attributes` the columns, edge `key` the edge id.
1138
+ * @param ctx - the context
1139
+ * @param root - the document
1140
+ */
1141
+ function importGraphology(ctx, root) {
1142
+ const { report } = ctx;
1143
+ const nodes = arraySection(root.nodes, "nodes", report) ?? [];
1144
+ const edges = arraySection(root.edges, "edges", report) ?? [];
1145
+ const options = isJsonObject(root.options) ? root.options : {};
1146
+ if (hasKey(root, "options") && !isJsonObject(root.options)) {
1147
+ report.warning("validation-error", JSON_ISSUE.BAD_FLAG, "options is not an object; ignored", {
1148
+ element: "options",
1149
+ });
1150
+ }
1151
+ let type;
1152
+ if (options.type === "directed" || options.type === "undirected" || options.type === "mixed") {
1153
+ ({ type } = options);
1154
+ }
1155
+ else {
1156
+ if (options.type !== undefined && options.type !== null) {
1157
+ report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`, { element: "options.type" });
1158
+ }
1159
+ type = "mixed";
1160
+ }
1161
+ let directed;
1162
+ if (type === "mixed") {
1163
+ const objects = edges.filter((edge) => isJsonObject(edge));
1164
+ const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
1165
+ directed = objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
1166
+ }
1167
+ else {
1168
+ directed = type === "directed";
1169
+ }
1170
+ ctx.setHeader(directed);
1171
+ const multi = hasKey(options, "multi") ? flagOf(options.multi, "options.multi", false, report) : null;
1172
+ const allowSelfLoops = hasKey(options, "allowSelfLoops")
1173
+ ? flagOf(options.allowSelfLoops, "options.allowSelfLoops", true, report)
1174
+ : undefined;
1175
+ ctx.writeGraphDict(root.attributes, "attributes");
1176
+ ctx.reportNodeIdFrom("graphology", "the node key");
1177
+ ctx.sink.reserve(nodes.length, edges.length);
1178
+ for (let i = 0; i < nodes.length; i++) {
1179
+ const element = `nodes[${i}]`;
1180
+ const record = nodes[i];
1181
+ if (!isJsonObject(record)) {
1182
+ ctx.badElement("node", element);
1183
+ continue;
1184
+ }
1185
+ const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
1186
+ if (id === null) {
1187
+ ctx.countSkipped("node");
1188
+ continue;
1189
+ }
1190
+ pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
1191
+ }
1192
+ throwIfAborted(ctx.options.signal);
1193
+ const ids = ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true);
1194
+ for (let i = 0; i < edges.length; i++) {
1195
+ const element = `edges[${i}]`;
1196
+ const record = edges[i];
1197
+ if (!isJsonObject(record)) {
1198
+ ctx.badElement("edge", element);
1199
+ continue;
1200
+ }
1201
+ if (!hasKey(record, "source") || !hasKey(record, "target")) {
1202
+ ctx.missingEndpoint(element, hasKey(record, "source") ? "target" : "source");
1203
+ continue;
1204
+ }
1205
+ try {
1206
+ const u = ctx.coerceId(record.source, `${element}.source`);
1207
+ const v = ctx.coerceId(record.target, `${element}.target`);
1208
+ if (u === null || v === null) {
1209
+ ctx.countSkipped("edge");
1210
+ continue;
1211
+ }
1212
+ let kind;
1213
+ if (type === "mixed") {
1214
+ kind = flagOf(record.undirected, `${element}.undirected`, false, report) ? "undirected" : "directed";
1215
+ }
1216
+ else {
1217
+ kind = type;
1218
+ }
1219
+ const attributes = isJsonObject(record.attributes) ? record.attributes : {};
1220
+ const idValue = ctx.edgeIdValue(ids, record.key);
1221
+ const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
1222
+ ctx.setEdgeId(ids, edge, idValue);
1223
+ ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom);
1224
+ }
1225
+ catch (err) {
1226
+ ctx.skip(err, "edge", element);
1227
+ }
1228
+ }
1229
+ ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
1230
+ }
1231
+ /**
1232
+ * Push a node whose attributes live in a nested dict (graphology `attributes`, JGF `metadata`);
1233
+ * element-level keys the dialect does not define are kept with the `#element` suffix.
1234
+ * @param ctx - the context
1235
+ * @param id - the node id
1236
+ * @param record - the element record
1237
+ * @param dictKey - the key of the nested dict
1238
+ * @param structural - the element keys that are not attributes
1239
+ * @param element - the element name
1240
+ * @returns the node index, or -1 when skipped
1241
+ */
1242
+ function pushNestedNode(ctx, id, record, dictKey, structural, element) {
1243
+ const index = ctx.pushNode(id, element);
1244
+ if (index < 0) {
1245
+ return index;
1246
+ }
1247
+ try {
1248
+ const dict = record[dictKey];
1249
+ if (dict !== undefined && dict !== null && !isJsonObject(dict)) {
1250
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: ${dictKey} must be an object, found ${describe(dict)}`, { element });
1251
+ }
1252
+ ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null);
1253
+ }
1254
+ catch (err) {
1255
+ ctx.report.recordError(err, { element: String(id) });
1256
+ }
1257
+ return index;
1258
+ }
1259
+ // ============================================================ JSON Graph Format
1260
+ /**
1261
+ * Read a JGF v2 document (`graph` or `graphs[graphIndex]`): nodes keyed by id (or a v1 array with
1262
+ * `id`), `label` with role "label", `metadata` as columns, edges with `id` / `relation` /
1263
+ * `directed` / `label` / `metadata`, hyperedges per the `hyperedges` option.
1264
+ * @param ctx - the context
1265
+ * @param root - the document
1266
+ */
1267
+ function importJgf(ctx, root) {
1268
+ const { report } = ctx;
1269
+ const graph = jgfGraphOf(ctx, root);
1270
+ const edges = arraySection(graph.edges, "graph.edges", report) ?? [];
1271
+ const hyperedges = arraySection(graph.hyperedges, "graph.hyperedges", report) ?? [];
1272
+ const nodesRaw = graph.nodes;
1273
+ if (nodesRaw !== undefined && nodesRaw !== null && !isJsonObject(nodesRaw) && !Array.isArray(nodesRaw)) {
1274
+ report.fail(JSON_ISSUE.SHAPE, `graph.nodes must be an object keyed by id or an array, found ${describe(nodesRaw)}`);
1275
+ }
1276
+ const nodeRecords = Array.isArray(nodesRaw) ? nodesRaw : Object.values(nodesRaw ?? {});
1277
+ let directed;
1278
+ if (typeof graph.directed === "boolean") {
1279
+ ({ directed } = graph);
1280
+ }
1281
+ else if (ctx.explicitDefaultDirected) {
1282
+ directed = ctx.options.defaultDirected;
1283
+ }
1284
+ else {
1285
+ if (graph.directed !== undefined && graph.directed !== null) {
1286
+ report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `graph.directed is ${describe(graph.directed)}, not a boolean`, { element: "graph.directed" });
1287
+ }
1288
+ // the spec default is true; a file whose every edge says directed: false is read as undirected
1289
+ const objects = edges.filter((edge) => isJsonObject(edge));
1290
+ directed = objects.length === 0 || !objects.every((edge) => edge.directed === false);
1291
+ }
1292
+ ctx.setHeader(directed);
1293
+ ctx.writeGraphDict(graph.metadata, "graph.metadata");
1294
+ const shape = {
1295
+ dialect: "jgf",
1296
+ id: typeof graph.id === "string" ? graph.id : undefined,
1297
+ type: typeof graph.type === "string" ? graph.type : undefined,
1298
+ };
1299
+ const label = typeof graph.label === "string" ? graph.label : null;
1300
+ ctx.reportNodeIdFrom("jgf", "the node keys");
1301
+ ctx.sink.reserve(nodeRecords.length, edges.length);
1302
+ const labelColumn = ctx.nodes.declareIf({ name: "label", dtype: "string", role: "label", nullable: true }, anyHas(nodeRecords, "label"));
1303
+ if (isJsonObject(nodesRaw)) {
1304
+ for (const key of Object.keys(nodesRaw)) {
1305
+ const element = `nodes[${JSON.stringify(key)}]`;
1306
+ const record = nodesRaw[key];
1307
+ if (record !== null && !isJsonObject(record)) {
1308
+ ctx.badElement("node", element);
1309
+ continue;
1310
+ }
1311
+ const id = ctx.coerceId(key, element);
1312
+ if (id === null) {
1313
+ ctx.countSkipped("node");
1314
+ continue;
1315
+ }
1316
+ pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
1317
+ }
1318
+ }
1319
+ else {
1320
+ for (let i = 0; i < nodeRecords.length; i++) {
1321
+ const element = `nodes[${i}]`;
1322
+ const record = nodeRecords[i];
1323
+ if (!isJsonObject(record)) {
1324
+ ctx.badElement("node", element);
1325
+ continue;
1326
+ }
1327
+ const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
1328
+ if (id === null) {
1329
+ ctx.countSkipped("node");
1330
+ continue;
1331
+ }
1332
+ pushJgfNode(ctx, id, record, labelColumn, element);
1333
+ }
1334
+ }
1335
+ throwIfAborted(ctx.options.signal);
1336
+ const ids = ctx.declareEdgeIds([...edges, ...hyperedges], (edge) => edge.id, "id", false);
1337
+ const relationColumn = ctx.edges.declareIf({ name: "relation", dtype: "string", role: "kind", nullable: true }, anyHas(edges, "relation") || anyHas(hyperedges, "relation"));
1338
+ const edgeLabelColumn = ctx.edges.declareIf({ name: "label", dtype: "string", role: "label", nullable: true }, anyHas(edges, "label") || anyHas(hyperedges, "label"));
1339
+ const writeJgfEdge = (record, u, v, kind, element, structural = JGF_EDGE_KEYS) => {
1340
+ const { metadata } = record;
1341
+ const dict = isJsonObject(metadata) ? metadata : {};
1342
+ const idValue = ctx.edgeIdValue(ids, record.id);
1343
+ const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(dict), element);
1344
+ ctx.setEdgeId(ids, edge, idValue);
1345
+ ctx.writeStringField(ctx.edges, relationColumn, edge, record.relation, "relation", element);
1346
+ ctx.writeStringField(ctx.edges, edgeLabelColumn, edge, record.label, "label", element);
1347
+ if (metadata !== undefined && metadata !== null && !isJsonObject(metadata)) {
1348
+ report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
1349
+ element,
1350
+ });
1351
+ }
1352
+ ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom);
1353
+ };
1354
+ for (let i = 0; i < edges.length; i++) {
1355
+ const element = `edges[${i}]`;
1356
+ const record = edges[i];
1357
+ if (!isJsonObject(record)) {
1358
+ ctx.badElement("edge", element);
1359
+ continue;
1360
+ }
1361
+ if (!hasKey(record, "source") || !hasKey(record, "target")) {
1362
+ ctx.missingEndpoint(element, hasKey(record, "source") ? "target" : "source");
1363
+ continue;
1364
+ }
1365
+ try {
1366
+ const u = ctx.coerceId(record.source, `${element}.source`);
1367
+ const v = ctx.coerceId(record.target, `${element}.target`);
1368
+ if (u === null || v === null) {
1369
+ ctx.countSkipped("edge");
1370
+ continue;
1371
+ }
1372
+ const edgeDirected = flagOf(record.directed, `${element}.directed`, directed, report);
1373
+ writeJgfEdge(record, u, v, edgeDirected ? "directed" : "undirected", element);
1374
+ }
1375
+ catch (err) {
1376
+ ctx.skip(err, "edge", element);
1377
+ }
1378
+ }
1379
+ importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
1380
+ ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
1381
+ }
1382
+ /**
1383
+ * The graph object of a JGF document: `graph`, or `graphs[graphIndex]`.
1384
+ * @param ctx - the context
1385
+ * @param root - the document
1386
+ * @returns the graph object; the import fails when there is none
1387
+ */
1388
+ function jgfGraphOf(ctx, root) {
1389
+ const { report } = ctx;
1390
+ if (isJsonObject(root.graph)) {
1391
+ return root.graph;
1392
+ }
1393
+ const graphs = arraySection(root.graphs, "graphs", report) ?? [];
1394
+ if (graphs.length === 0) {
1395
+ report.fail(JSON_ISSUE.SHAPE, "a JGF document needs a graph object or a non-empty graphs array");
1396
+ }
1397
+ if (graphs.length > 1) {
1398
+ report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${ctx.json.graphIndex}] is read`, { element: "graphs" });
1399
+ }
1400
+ if (ctx.json.graphIndex >= graphs.length) {
1401
+ report.fail(JSON_ISSUE.SHAPE, `graphIndex ${ctx.json.graphIndex} is beyond the ${graphs.length} graph(s)`);
1402
+ }
1403
+ const graph = graphs[ctx.json.graphIndex];
1404
+ if (!isJsonObject(graph)) {
1405
+ return report.fail(JSON_ISSUE.SHAPE, `graphs[${ctx.json.graphIndex}] is not an object`);
1406
+ }
1407
+ return graph;
1408
+ }
1409
+ /**
1410
+ * Push a JGF node: `label` into the declared column, `metadata` as attributes.
1411
+ * @param ctx - the context
1412
+ * @param id - the node id
1413
+ * @param record - the node record
1414
+ * @param labelColumn - the label column handle, or INVALID_INDEX
1415
+ * @param element - the element name
1416
+ */
1417
+ function pushJgfNode(ctx, id, record, labelColumn, element) {
1418
+ const index = ctx.pushNode(id, element);
1419
+ if (index < 0) {
1420
+ return;
1421
+ }
1422
+ try {
1423
+ ctx.writeStringField(ctx.nodes, labelColumn, index, record.label, "label", element);
1424
+ const { metadata } = record;
1425
+ if (metadata !== undefined && metadata !== null && !isJsonObject(metadata)) {
1426
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
1427
+ element,
1428
+ });
1429
+ }
1430
+ ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null);
1431
+ }
1432
+ catch (err) {
1433
+ ctx.report.recordError(err, { element: String(id) });
1434
+ }
1435
+ }
1436
+ /**
1437
+ * JGF hyperedges per the `hyperedges` option: "error" aborts, "skip" (default) records a warning
1438
+ * and a loss note, "star" and "clique" expand an undirected `{ nodes }` hyperedge into edges from
1439
+ * its first node (star) or between every pair (clique); a directed `{ source, target }` hyperedge
1440
+ * becomes every source -> target edge under both policies. Every expanded edge carries the
1441
+ * hyperedge's id, relation, label and metadata.
1442
+ * @param ctx - the context
1443
+ * @param hyperedges - the hyperedge records
1444
+ * @param directed - the graph's direction
1445
+ * @param push - the edge writer of the JGF reader
1446
+ */
1447
+ function importHyperedges(ctx, hyperedges, directed, push) {
1448
+ if (hyperedges.length === 0) {
1449
+ return;
1450
+ }
1451
+ const { report } = ctx;
1452
+ const policy = ctx.options.hyperedges;
1453
+ if (policy === "error") {
1454
+ report.error("unsupported", JSON_ISSUE.HYPEREDGE, `${hyperedges.length} hyperedge(s) (hyperedges: "error")`, {
1455
+ element: "hyperedges",
1456
+ });
1457
+ throw report.abort("hyperedges refused", { code: JSON_ISSUE.HYPEREDGE, count: hyperedges.length });
1458
+ }
1459
+ if (policy === "skip") {
1460
+ report.warning("unsupported", JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(s) skipped`, {
1461
+ element: "hyperedges",
1462
+ });
1463
+ report.loss(JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(s) were not imported`, null, hyperedges.length);
1464
+ return;
1465
+ }
1466
+ for (let i = 0; i < hyperedges.length; i++) {
1467
+ const element = `hyperedges[${i}]`;
1468
+ const record = hyperedges[i];
1469
+ if (!isJsonObject(record)) {
1470
+ ctx.badElement("edge", element);
1471
+ continue;
1472
+ }
1473
+ try {
1474
+ if (Array.isArray(record.nodes)) {
1475
+ const members = record.nodes.map((raw) => ctx.requireId(raw, element));
1476
+ if (members.length < 2) {
1477
+ throw new GraphFormatError("E_INVALID_ID", `${element}: an undirected hyperedge needs two or more nodes`, { reason: "hyperedge shape" });
1478
+ }
1479
+ const kind = directed ? "directed" : "undirected";
1480
+ if (policy === "star") {
1481
+ for (let k = 1; k < members.length; k++) {
1482
+ push(record, members[0], members[k], kind, element, JGF_HYPEREDGE_KEYS);
1483
+ }
1484
+ }
1485
+ else {
1486
+ for (let a = 0; a < members.length; a++) {
1487
+ for (let b = a + 1; b < members.length; b++) {
1488
+ push(record, members[a], members[b], kind, element, JGF_HYPEREDGE_KEYS);
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ else if (Array.isArray(record.source) && Array.isArray(record.target)) {
1494
+ const sources = record.source.map((raw) => ctx.requireId(raw, element));
1495
+ const targets = record.target.map((raw) => ctx.requireId(raw, element));
1496
+ if (sources.length === 0 || targets.length === 0) {
1497
+ throw new GraphFormatError("E_INVALID_ID", `${element}: a directed hyperedge needs sources and targets`, { reason: "hyperedge shape" });
1498
+ }
1499
+ for (const s of sources) {
1500
+ for (const t of targets) {
1501
+ push(record, s, t, "directed", element, JGF_HYPEREDGE_KEYS);
1502
+ }
1503
+ }
1504
+ }
1505
+ else {
1506
+ report.error("validation-error", JSON_ISSUE.HYPEREDGE_SHAPE, `${element} has neither a nodes array nor source / target arrays`, { element });
1507
+ ctx.countSkipped("edge");
1508
+ }
1509
+ }
1510
+ catch (err) {
1511
+ ctx.skip(err, "edge", element);
1512
+ }
1513
+ }
1514
+ }
1515
+ // ============================================================ Cytoscape
1516
+ /**
1517
+ * Read Cytoscape.js elements: `elements.nodes` / `elements.edges`, a flat `elements` array (group
1518
+ * from `group` or from the presence of source / target), or a top-level array. `data.id` is the id,
1519
+ * `data.parent` the parent (resolved after every node is known), `position` the position column,
1520
+ * `classes` the classes list; the other `data` keys are attributes and the element-level keys
1521
+ * (selected, locked, ...) are columns of the same name.
1522
+ * @param ctx - the context
1523
+ * @param root - the document
1524
+ */
1525
+ function importCytoscape(ctx, root) {
1526
+ const { report } = ctx;
1527
+ let elements;
1528
+ let extra;
1529
+ if (Array.isArray(root)) {
1530
+ elements = root;
1531
+ }
1532
+ else if (isJsonObject(root)) {
1533
+ ({ elements } = root);
1534
+ ctx.writeGraphDict(root.data, "data");
1535
+ const rest = {};
1536
+ for (const key of Object.keys(root)) {
1537
+ if (key !== "elements" && key !== "data") {
1538
+ rest[key] = root[key];
1539
+ }
1540
+ }
1541
+ if (Object.keys(rest).length > 0) {
1542
+ extra = rest;
1543
+ }
1544
+ }
1545
+ else {
1546
+ report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
1547
+ }
1548
+ const { nodes, edges } = cytoscapeSections(ctx, elements);
1549
+ ctx.setHeader(ctx.defaultDirected("cytoscape"));
1550
+ ctx.reportNodeIdFrom("cytoscape", "data.id");
1551
+ ctx.sink.reserve(nodes.length, edges.length);
1552
+ const dataHas = (items, key) => items.some((item) => isJsonObject(item) && isJsonObject(item.data) && item.data[key] !== undefined);
1553
+ const positionColumn = ctx.nodes.declareIf({
1554
+ name: POSITION_COLUMN,
1555
+ dtype: "f32",
1556
+ components: 3,
1557
+ role: "position",
1558
+ mutable: true,
1559
+ nullable: true,
1560
+ extra: { sourceDims: 2, units: "file" },
1561
+ origin: { format: "json", namespace: "cytoscape" },
1562
+ }, anyHas(nodes, "position"));
1563
+ const classesColumn = ctx.nodes.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(nodes, "classes"));
1564
+ const parentColumn = ctx.nodes.declareIf({ name: PARENT_COLUMN, dtype: "u32", role: "parent", refersTo: "node", nullable: true }, dataHas(nodes, "parent"));
1565
+ const edgeClassesColumn = ctx.edges.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(edges, "classes"));
1566
+ const parents = [];
1567
+ const point = [0, 0, 0];
1568
+ for (let i = 0; i < nodes.length; i++) {
1569
+ const element = `nodes[${i}]`;
1570
+ const record = nodes[i];
1571
+ if (!isJsonObject(record) || !isJsonObject(record.data)) {
1572
+ ctx.badElement("node", element, "an element with a data object");
1573
+ continue;
1574
+ }
1575
+ const { data } = record;
1576
+ const id = ctx.coerceId(hasKey(data, "id") ? data.id : undefined, element);
1577
+ if (id === null) {
1578
+ ctx.countSkipped("node");
1579
+ continue;
1580
+ }
1581
+ const index = ctx.pushNode(id, element);
1582
+ if (index < 0) {
1583
+ continue;
1584
+ }
1585
+ try {
1586
+ for (const key of Object.keys(data)) {
1587
+ if (key === "id") {
1588
+ continue;
1589
+ }
1590
+ if (key === "parent") {
1591
+ const raw = data.parent;
1592
+ if (raw !== undefined && raw !== null) {
1593
+ const parent = ctx.coerceId(raw, `${element}.data.parent`);
1594
+ if (parent !== null) {
1595
+ parents.push({ index, parent, element });
1596
+ }
1597
+ }
1598
+ continue;
1599
+ }
1600
+ ctx.nodes.write(index, key, data[key], SUFFIX.data);
1601
+ }
1602
+ const { position } = record;
1603
+ if (position !== undefined && position !== null) {
1604
+ if (isJsonObject(position) && typeof position.x === "number" && typeof position.y === "number") {
1605
+ point[0] = position.x;
1606
+ point[1] = position.y;
1607
+ ctx.nodes.set(positionColumn, index, point);
1608
+ }
1609
+ else {
1610
+ report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y`, { element });
1611
+ }
1612
+ }
1613
+ writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
1614
+ writeElementKeys(ctx.nodes, index, record);
1615
+ }
1616
+ catch (err) {
1617
+ report.recordError(err, { element: String(id) });
1618
+ }
1619
+ }
1620
+ for (const { index, parent, element } of parents) {
1621
+ const parentIndex = ctx.sink.indexOf(parent);
1622
+ if (parentIndex === INVALID_INDEX) {
1623
+ report.error("missing-value", JSON_ISSUE.UNKNOWN_PARENT, `${element}: parent ${JSON.stringify(parent)} is not a node`, { element });
1624
+ continue;
1625
+ }
1626
+ ctx.nodes.set(parentColumn, index, parentIndex);
1627
+ }
1628
+ throwIfAborted(ctx.options.signal);
1629
+ importCytoscapeEdges(ctx, edges, edgeClassesColumn);
1630
+ ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
1631
+ }
1632
+ /**
1633
+ * Read the Cytoscape edge elements: `data.id`, `data.source` / `data.target`, the weight, the
1634
+ * other data keys as attributes, `classes` and the element-level keys.
1635
+ * @param ctx - the context
1636
+ * @param edges - the edge elements
1637
+ * @param edgeClassesColumn - the edge classes column, or INVALID_INDEX
1638
+ */
1639
+ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
1640
+ const ids = ctx.declareEdgeIds(edges, (edge) => (isJsonObject(edge.data) ? edge.data.id : undefined), "id", true);
1641
+ const kind = ctx.uniformKind();
1642
+ for (let i = 0; i < edges.length; i++) {
1643
+ const element = `edges[${i}]`;
1644
+ const record = edges[i];
1645
+ if (!isJsonObject(record) || !isJsonObject(record.data)) {
1646
+ ctx.badElement("edge", element, "an element with a data object");
1647
+ continue;
1648
+ }
1649
+ const { data } = record;
1650
+ if (!hasKey(data, "source") || !hasKey(data, "target")) {
1651
+ ctx.missingEndpoint(element, `data.${hasKey(data, "source") ? "target" : "source"}`);
1652
+ continue;
1653
+ }
1654
+ try {
1655
+ const u = ctx.coerceId(data.source, `${element}.data.source`);
1656
+ const v = ctx.coerceId(data.target, `${element}.data.target`);
1657
+ if (u === null || v === null) {
1658
+ ctx.countSkipped("edge");
1659
+ continue;
1660
+ }
1661
+ const idValue = ctx.edgeIdValue(ids, data.id);
1662
+ const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(data), element);
1663
+ ctx.setEdgeId(ids, edge, idValue);
1664
+ const { weightFrom } = ctx.options;
1665
+ for (const key of Object.keys(data)) {
1666
+ if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
1667
+ ctx.edges.write(edge, key, data[key], SUFFIX.data);
1668
+ }
1669
+ }
1670
+ writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
1671
+ writeElementKeys(ctx.edges, edge, record);
1672
+ }
1673
+ catch (err) {
1674
+ ctx.skip(err, "edge", element);
1675
+ }
1676
+ }
1677
+ }
1678
+ /**
1679
+ * The node and edge element arrays of a Cytoscape `elements` value: an object with `nodes` /
1680
+ * `edges`, a flat array split by isNodeElement(), or nothing (reported).
1681
+ * @param ctx - the context
1682
+ * @param elements - the `elements` value
1683
+ * @returns the two arrays; the import fails when elements has another type
1684
+ */
1685
+ function cytoscapeSections(ctx, elements) {
1686
+ const { report } = ctx;
1687
+ if (Array.isArray(elements)) {
1688
+ return {
1689
+ nodes: elements.filter((item) => isJsonObject(item) && isNodeElement(item)),
1690
+ edges: elements.filter((item) => isJsonObject(item) && !isNodeElement(item)),
1691
+ };
1692
+ }
1693
+ if (isJsonObject(elements)) {
1694
+ return {
1695
+ nodes: arraySection(elements.nodes, "elements.nodes", report) ?? [],
1696
+ edges: arraySection(elements.edges, "elements.edges", report) ?? [],
1697
+ };
1698
+ }
1699
+ if (elements === undefined || elements === null) {
1700
+ report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no elements", {
1701
+ element: "elements",
1702
+ });
1703
+ return { nodes: [], edges: [] };
1704
+ }
1705
+ return report.fail(JSON_ISSUE.SHAPE, `elements must be an object or an array, found ${describe(elements)}`);
1706
+ }
1707
+ /**
1708
+ * Whether a flat Cytoscape element is a node: `group: "nodes"`, or no source / target in its data.
1709
+ * @param element - the element
1710
+ * @returns true for a node
1711
+ */
1712
+ function isNodeElement(element) {
1713
+ if (element.group === "nodes") {
1714
+ return true;
1715
+ }
1716
+ if (element.group === "edges") {
1717
+ return false;
1718
+ }
1719
+ const { data } = element;
1720
+ return !(isJsonObject(data) && hasKey(data, "source") && hasKey(data, "target"));
1721
+ }
1722
+ /**
1723
+ * Write the classes of an element: a space-separated string or an array of strings.
1724
+ * @param ctx - the context
1725
+ * @param writer - the table writer
1726
+ * @param column - the classes column, or INVALID_INDEX when no element has classes
1727
+ * @param row - the row
1728
+ * @param raw - the `classes` value
1729
+ * @param element - the element name
1730
+ */
1731
+ function writeClasses(ctx, writer, column, row, raw, element) {
1732
+ if (column === INVALID_INDEX || raw === undefined || raw === null) {
1733
+ return;
1734
+ }
1735
+ let classes;
1736
+ if (typeof raw === "string") {
1737
+ classes = raw.split(/\s+/).filter((c) => c.length > 0);
1738
+ }
1739
+ else if (Array.isArray(raw) && raw.every((c) => typeof c === "string")) {
1740
+ classes = raw;
1741
+ }
1742
+ else {
1743
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: classes must be a string or an array of strings`, { element });
1744
+ return;
1745
+ }
1746
+ writer.set(column, row, classes);
1747
+ }
1748
+ /**
1749
+ * Write the element-level keys of a Cytoscape element other than the structural ones: the known
1750
+ * keys under their own name, unknown ones with the `#element` suffix.
1751
+ * @param writer - the table writer
1752
+ * @param row - the row
1753
+ * @param record - the element
1754
+ */
1755
+ function writeElementKeys(writer, row, record) {
1756
+ for (const key of Object.keys(record)) {
1757
+ if (CYTOSCAPE_STRUCTURAL_KEYS.has(key)) {
1758
+ continue;
1759
+ }
1760
+ const name = CYTOSCAPE_ELEMENT_KEYS.has(key) ? key : `${key}${SUFFIX.element}`;
1761
+ writer.write(row, name, record[key], SUFFIX.data);
1762
+ }
1763
+ }
1764
+ // ============================================================ the plugin
1765
+ /**
1766
+ * The JSON importer plugin (design section 8.4).
1767
+ */
1768
+ export const jsonImporter = Object.freeze({
1769
+ format: "json",
1770
+ extensions: Object.freeze([".json"]),
1771
+ mimeTypes: Object.freeze(["application/json"]),
1772
+ /**
1773
+ * Confidence that the head is a JSON graph document: 0 unless it starts with `{` or `[`, 0.5
1774
+ * for any JSON, 0.9 when a graph key (nodes, links, edges, elements, graph, graphs) appears in
1775
+ * the head.
1776
+ * @param head - the first bytes
1777
+ * @returns the confidence
1778
+ */
1779
+ sniff(head) {
1780
+ const text = new TextDecoder("utf-8").decode(head.subarray(0, SNIFF_BYTES));
1781
+ const trimmed = (text.startsWith(BOM) ? text.slice(1) : text).trimStart();
1782
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
1783
+ return 0;
1784
+ }
1785
+ return SNIFF_KEYS.some((key) => trimmed.includes(key)) ? 0.9 : 0.5;
1786
+ },
1787
+ /**
1788
+ * Read a JSON graph document into the sink.
1789
+ * @param input - the text, bytes or stream
1790
+ * @param sink - the sink
1791
+ * @param options - format-specific and common options
1792
+ * @returns the import report; ImportError on a fatal error or beyond the error limit
1793
+ */
1794
+ async import(input, sink, options) {
1795
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
1796
+ const json = resolveJsonOptions(options);
1797
+ const report = new ImportReportBuilder("json", resolved.errorLimit);
1798
+ const text = await readText(input, report, resolved);
1799
+ const root = parseDocument(text, report);
1800
+ const dialect = detectDialect(root, json.dialect, report);
1801
+ const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== undefined);
1802
+ reportSinkOptions(sink, options, report);
1803
+ reportUnusedOptions(options, report, USED_OPTIONS);
1804
+ if (dialect === "cytoscape") {
1805
+ importCytoscape(ctx, root);
1806
+ throwIfAborted(resolved.signal);
1807
+ return report.finish();
1808
+ }
1809
+ const doc = isJsonObject(root)
1810
+ ? root
1811
+ : report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
1812
+ switch (dialect) {
1813
+ case "node-link":
1814
+ case "d3":
1815
+ importNodeLink(ctx, doc, dialect);
1816
+ break;
1817
+ case "jgf":
1818
+ importJgf(ctx, doc);
1819
+ break;
1820
+ case "graphology":
1821
+ importGraphology(ctx, doc);
1822
+ break;
1823
+ case "vis":
1824
+ importVis(ctx, doc);
1825
+ break;
1826
+ default: {
1827
+ const name = dialect;
1828
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {
1829
+ option: "dialect",
1830
+ found: name,
1831
+ });
1832
+ }
1833
+ }
1834
+ throwIfAborted(resolved.signal);
1835
+ return report.finish();
1836
+ },
1837
+ });
1838
+ //# sourceMappingURL=importer.js.map