@graphty/graph-io 0.3.25 → 0.3.27

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 (581) hide show
  1. package/README.md +49 -414
  2. package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
  3. package/dist/chunks/choose-DrXtu_96.js +56 -0
  4. package/dist/chunks/choose-DrXtu_96.js.map +1 -0
  5. package/dist/chunks/constants-ikG85dT8.js +231 -0
  6. package/dist/chunks/constants-ikG85dT8.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D1HPTUrP.js} +390 -99
  8. package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
  9. package/dist/chunks/export-CHd4tQE2.js +4333 -0
  10. package/dist/chunks/export-CHd4tQE2.js.map +1 -0
  11. package/dist/chunks/{importer-eUREhBGj.js → exporter-D23CKNqF.js} +4969 -3201
  12. package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
  13. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-DfWTgrti.js} +23 -21
  14. package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
  15. package/dist/chunks/exporter-DhtW6FxK.js +3465 -0
  16. package/dist/chunks/exporter-DhtW6FxK.js.map +1 -0
  17. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-IHv4nkCm.js} +38 -229
  18. package/dist/chunks/exporter-IHv4nkCm.js.map +1 -0
  19. package/dist/chunks/{importer-BWY2FFc5.js → importer-B1wmPQjr.js} +255 -77
  20. package/dist/chunks/importer-B1wmPQjr.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-B2W_58pz.js} +399 -146
  22. package/dist/chunks/importer-B2W_58pz.js.map +1 -0
  23. package/dist/chunks/importer-BSALvkui.js +2811 -0
  24. package/dist/chunks/importer-BSALvkui.js.map +1 -0
  25. package/dist/chunks/{importer-vi3bSdtw.js → importer-BVRE--Un.js} +510 -77
  26. package/dist/chunks/importer-BVRE--Un.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-CIAzt-wo.js} +1689 -312
  28. package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
  29. package/dist/chunks/{importer-DIrbbnAf.js → importer-DpEW5zeG.js} +585 -226
  30. package/dist/chunks/importer-DpEW5zeG.js.map +1 -0
  31. package/dist/chunks/{importer-zGLo8Dg_.js → importer-z_FL4VMb.js} +488 -161
  32. package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
  33. package/dist/chunks/json-elements-BAmNBZok.js +1225 -0
  34. package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
  35. package/dist/chunks/ontology-export-Bab4eUA8.js +357 -0
  36. package/dist/chunks/ontology-export-Bab4eUA8.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CdlvL4RO.js} +298 -114
  38. package/dist/chunks/records-CdlvL4RO.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DzXjAiTl.js} +29 -11
  40. package/dist/chunks/text-DzXjAiTl.js.map +1 -0
  41. package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
  42. package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
  43. package/dist/csv.js +952 -262
  44. package/dist/csv.js.map +1 -1
  45. package/dist/cx.js +6 -3
  46. package/dist/cx2.js +2 -2
  47. package/dist/cys.js +6 -3
  48. package/dist/dot.js +1 -1
  49. package/dist/gexf.js +554 -122
  50. package/dist/gexf.js.map +1 -1
  51. package/dist/gml.js +344 -87
  52. package/dist/gml.js.map +1 -1
  53. package/dist/graph-io.js +1105 -213
  54. package/dist/graph-io.js.map +1 -1
  55. package/dist/graphml.js +3 -2
  56. package/dist/graphml.js.map +1 -1
  57. package/dist/json.js +3 -2
  58. package/dist/neo4j.js +565 -158
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +6 -3
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/children.d.ts +18 -6
  63. package/dist/src/children.d.ts.map +1 -1
  64. package/dist/src/children.js +6 -3
  65. package/dist/src/children.js.map +1 -1
  66. package/dist/src/common/attributes.d.ts +29 -11
  67. package/dist/src/common/attributes.d.ts.map +1 -1
  68. package/dist/src/common/attributes.js +25 -7
  69. package/dist/src/common/attributes.js.map +1 -1
  70. package/dist/src/common/cell-budget.d.ts +9 -2
  71. package/dist/src/common/cell-budget.d.ts.map +1 -1
  72. package/dist/src/common/cell-budget.js +11 -3
  73. package/dist/src/common/cell-budget.js.map +1 -1
  74. package/dist/src/common/choose.d.ts +19 -0
  75. package/dist/src/common/choose.d.ts.map +1 -0
  76. package/dist/src/common/choose.js +72 -0
  77. package/dist/src/common/choose.js.map +1 -0
  78. package/dist/src/common/codes.d.ts +456 -66
  79. package/dist/src/common/codes.d.ts.map +1 -1
  80. package/dist/src/common/codes.js +458 -66
  81. package/dist/src/common/codes.js.map +1 -1
  82. package/dist/src/common/compare.d.ts +81 -0
  83. package/dist/src/common/compare.d.ts.map +1 -0
  84. package/dist/src/common/compare.js +394 -0
  85. package/dist/src/common/compare.js.map +1 -0
  86. package/dist/src/common/cx-export.d.ts +123 -0
  87. package/dist/src/common/cx-export.d.ts.map +1 -0
  88. package/dist/src/common/cx-export.js +334 -0
  89. package/dist/src/common/cx-export.js.map +1 -0
  90. package/dist/src/common/declared-types.d.ts +22 -2
  91. package/dist/src/common/declared-types.d.ts.map +1 -1
  92. package/dist/src/common/declared-types.js +21 -3
  93. package/dist/src/common/declared-types.js.map +1 -1
  94. package/dist/src/common/direction.d.ts +74 -50
  95. package/dist/src/common/direction.d.ts.map +1 -1
  96. package/dist/src/common/direction.js +47 -27
  97. package/dist/src/common/direction.js.map +1 -1
  98. package/dist/src/common/escape.d.ts +21 -9
  99. package/dist/src/common/escape.d.ts.map +1 -1
  100. package/dist/src/common/escape.js +39 -22
  101. package/dist/src/common/escape.js.map +1 -1
  102. package/dist/src/common/export.d.ts +144 -32
  103. package/dist/src/common/export.d.ts.map +1 -1
  104. package/dist/src/common/export.js +216 -42
  105. package/dist/src/common/export.js.map +1 -1
  106. package/dist/src/common/format.d.ts +23 -1
  107. package/dist/src/common/format.d.ts.map +1 -1
  108. package/dist/src/common/format.js +26 -1
  109. package/dist/src/common/format.js.map +1 -1
  110. package/dist/src/common/ids.d.ts +9 -2
  111. package/dist/src/common/ids.d.ts.map +1 -1
  112. package/dist/src/common/ids.js +6 -2
  113. package/dist/src/common/ids.js.map +1 -1
  114. package/dist/src/common/input.d.ts +122 -28
  115. package/dist/src/common/input.d.ts.map +1 -1
  116. package/dist/src/common/input.js +890 -124
  117. package/dist/src/common/input.js.map +1 -1
  118. package/dist/src/common/json-elements.d.ts +144 -20
  119. package/dist/src/common/json-elements.d.ts.map +1 -1
  120. package/dist/src/common/json-elements.js +869 -189
  121. package/dist/src/common/json-elements.js.map +1 -1
  122. package/dist/src/common/line-format.d.ts +90 -0
  123. package/dist/src/common/line-format.d.ts.map +1 -0
  124. package/dist/src/common/line-format.js +185 -0
  125. package/dist/src/common/line-format.js.map +1 -0
  126. package/dist/src/common/lists.d.ts +6 -1
  127. package/dist/src/common/lists.d.ts.map +1 -1
  128. package/dist/src/common/lists.js +3 -1
  129. package/dist/src/common/lists.js.map +1 -1
  130. package/dist/src/common/ontology-export.d.ts +167 -0
  131. package/dist/src/common/ontology-export.d.ts.map +1 -0
  132. package/dist/src/common/ontology-export.js +359 -0
  133. package/dist/src/common/ontology-export.js.map +1 -0
  134. package/dist/src/common/ontology.d.ts +25 -3
  135. package/dist/src/common/ontology.d.ts.map +1 -1
  136. package/dist/src/common/ontology.js +30 -9
  137. package/dist/src/common/ontology.js.map +1 -1
  138. package/dist/src/common/options.d.ts +62 -12
  139. package/dist/src/common/options.d.ts.map +1 -1
  140. package/dist/src/common/options.js +89 -11
  141. package/dist/src/common/options.js.map +1 -1
  142. package/dist/src/common/plural.d.ts +31 -0
  143. package/dist/src/common/plural.d.ts.map +1 -0
  144. package/dist/src/common/plural.js +46 -0
  145. package/dist/src/common/plural.js.map +1 -0
  146. package/dist/src/common/report.d.ts +72 -14
  147. package/dist/src/common/report.d.ts.map +1 -1
  148. package/dist/src/common/report.js +146 -22
  149. package/dist/src/common/report.js.map +1 -1
  150. package/dist/src/common/temporal.d.ts +22 -5
  151. package/dist/src/common/temporal.d.ts.map +1 -1
  152. package/dist/src/common/temporal.js +13 -2
  153. package/dist/src/common/temporal.js.map +1 -1
  154. package/dist/src/common/text.d.ts +39 -23
  155. package/dist/src/common/text.d.ts.map +1 -1
  156. package/dist/src/common/text.js +51 -21
  157. package/dist/src/common/text.js.map +1 -1
  158. package/dist/src/common/weights.d.ts +37 -4
  159. package/dist/src/common/weights.d.ts.map +1 -1
  160. package/dist/src/common/weights.js +61 -2
  161. package/dist/src/common/weights.js.map +1 -1
  162. package/dist/src/common/writer.d.ts +19 -3
  163. package/dist/src/common/writer.d.ts.map +1 -1
  164. package/dist/src/common/writer.js +30 -2
  165. package/dist/src/common/writer.js.map +1 -1
  166. package/dist/src/common/xml.d.ts +125 -9
  167. package/dist/src/common/xml.d.ts.map +1 -1
  168. package/dist/src/common/xml.js +402 -85
  169. package/dist/src/common/xml.js.map +1 -1
  170. package/dist/src/common/zip.d.ts +44 -4
  171. package/dist/src/common/zip.d.ts.map +1 -1
  172. package/dist/src/common/zip.js +134 -5
  173. package/dist/src/common/zip.js.map +1 -1
  174. package/dist/src/formats/csv/exporter.d.ts +80 -24
  175. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  176. package/dist/src/formats/csv/exporter.js +146 -33
  177. package/dist/src/formats/csv/exporter.js.map +1 -1
  178. package/dist/src/formats/csv/header.d.ts +38 -9
  179. package/dist/src/formats/csv/header.d.ts.map +1 -1
  180. package/dist/src/formats/csv/header.js +46 -10
  181. package/dist/src/formats/csv/header.js.map +1 -1
  182. package/dist/src/formats/csv/importer.d.ts +187 -37
  183. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  184. package/dist/src/formats/csv/importer.js +835 -204
  185. package/dist/src/formats/csv/importer.js.map +1 -1
  186. package/dist/src/formats/csv/index.d.ts +100 -19
  187. package/dist/src/formats/csv/index.d.ts.map +1 -1
  188. package/dist/src/formats/csv/index.js +94 -21
  189. package/dist/src/formats/csv/index.js.map +1 -1
  190. package/dist/src/formats/csv/records.d.ts +95 -10
  191. package/dist/src/formats/csv/records.d.ts.map +1 -1
  192. package/dist/src/formats/csv/records.js +333 -122
  193. package/dist/src/formats/csv/records.js.map +1 -1
  194. package/dist/src/formats/csv/values.d.ts +1 -0
  195. package/dist/src/formats/csv/values.d.ts.map +1 -1
  196. package/dist/src/formats/csv/values.js +1 -0
  197. package/dist/src/formats/csv/values.js.map +1 -1
  198. package/dist/src/formats/cx/exporter.d.ts +102 -0
  199. package/dist/src/formats/cx/exporter.d.ts.map +1 -0
  200. package/dist/src/formats/cx/exporter.js +1217 -0
  201. package/dist/src/formats/cx/exporter.js.map +1 -0
  202. package/dist/src/formats/cx/importer.d.ts +79 -32
  203. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  204. package/dist/src/formats/cx/importer.js +682 -287
  205. package/dist/src/formats/cx/importer.js.map +1 -1
  206. package/dist/src/formats/cx/index.d.ts +4 -3
  207. package/dist/src/formats/cx/index.d.ts.map +1 -1
  208. package/dist/src/formats/cx/index.js +4 -3
  209. package/dist/src/formats/cx/index.js.map +1 -1
  210. package/dist/src/formats/cx2/exporter.d.ts +29 -11
  211. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  212. package/dist/src/formats/cx2/exporter.js +39 -268
  213. package/dist/src/formats/cx2/exporter.js.map +1 -1
  214. package/dist/src/formats/cx2/importer.d.ts +78 -28
  215. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  216. package/dist/src/formats/cx2/importer.js +227 -80
  217. package/dist/src/formats/cx2/importer.js.map +1 -1
  218. package/dist/src/formats/cx2/index.d.ts +2 -1
  219. package/dist/src/formats/cx2/index.d.ts.map +1 -1
  220. package/dist/src/formats/cx2/index.js +2 -1
  221. package/dist/src/formats/cx2/index.js.map +1 -1
  222. package/dist/src/formats/cys/constants.d.ts +66 -17
  223. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  224. package/dist/src/formats/cys/constants.js +59 -18
  225. package/dist/src/formats/cys/constants.js.map +1 -1
  226. package/dist/src/formats/cys/exporter.d.ts +118 -0
  227. package/dist/src/formats/cys/exporter.d.ts.map +1 -0
  228. package/dist/src/formats/cys/exporter.js +1072 -0
  229. package/dist/src/formats/cys/exporter.js.map +1 -0
  230. package/dist/src/formats/cys/importer.d.ts +22 -8
  231. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  232. package/dist/src/formats/cys/importer.js +510 -149
  233. package/dist/src/formats/cys/importer.js.map +1 -1
  234. package/dist/src/formats/cys/index.d.ts +4 -2
  235. package/dist/src/formats/cys/index.d.ts.map +1 -1
  236. package/dist/src/formats/cys/index.js +4 -2
  237. package/dist/src/formats/cys/index.js.map +1 -1
  238. package/dist/src/formats/cys/session.d.ts +46 -7
  239. package/dist/src/formats/cys/session.d.ts.map +1 -1
  240. package/dist/src/formats/cys/session.js +101 -27
  241. package/dist/src/formats/cys/session.js.map +1 -1
  242. package/dist/src/formats/cys/tables.d.ts +9 -2
  243. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  244. package/dist/src/formats/cys/tables.js +29 -7
  245. package/dist/src/formats/cys/tables.js.map +1 -1
  246. package/dist/src/formats/dot/exporter.d.ts +42 -14
  247. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  248. package/dist/src/formats/dot/exporter.js +56 -29
  249. package/dist/src/formats/dot/exporter.js.map +1 -1
  250. package/dist/src/formats/dot/importer.d.ts +91 -29
  251. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  252. package/dist/src/formats/dot/importer.js +190 -39
  253. package/dist/src/formats/dot/importer.js.map +1 -1
  254. package/dist/src/formats/dot/index.d.ts +2 -1
  255. package/dist/src/formats/dot/index.d.ts.map +1 -1
  256. package/dist/src/formats/dot/index.js +2 -1
  257. package/dist/src/formats/dot/index.js.map +1 -1
  258. package/dist/src/formats/dot/names.d.ts +44 -11
  259. package/dist/src/formats/dot/names.d.ts.map +1 -1
  260. package/dist/src/formats/dot/names.js +44 -11
  261. package/dist/src/formats/dot/names.js.map +1 -1
  262. package/dist/src/formats/dot/tokenizer.d.ts +9 -2
  263. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  264. package/dist/src/formats/dot/tokenizer.js +20 -5
  265. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  266. package/dist/src/formats/gexf/exporter.d.ts +46 -15
  267. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  268. package/dist/src/formats/gexf/exporter.js +45 -15
  269. package/dist/src/formats/gexf/exporter.js.map +1 -1
  270. package/dist/src/formats/gexf/importer.d.ts +87 -23
  271. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  272. package/dist/src/formats/gexf/importer.js +471 -90
  273. package/dist/src/formats/gexf/importer.js.map +1 -1
  274. package/dist/src/formats/gexf/index.d.ts +84 -21
  275. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  276. package/dist/src/formats/gexf/index.js +80 -23
  277. package/dist/src/formats/gexf/index.js.map +1 -1
  278. package/dist/src/formats/gexf/schema.d.ts +67 -15
  279. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  280. package/dist/src/formats/gexf/schema.js +59 -13
  281. package/dist/src/formats/gexf/schema.js.map +1 -1
  282. package/dist/src/formats/gml/exporter.d.ts +69 -21
  283. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/gml/exporter.js +74 -29
  285. package/dist/src/formats/gml/exporter.js.map +1 -1
  286. package/dist/src/formats/gml/importer.d.ts +116 -25
  287. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  288. package/dist/src/formats/gml/importer.js +290 -61
  289. package/dist/src/formats/gml/importer.js.map +1 -1
  290. package/dist/src/formats/gml/index.d.ts +87 -24
  291. package/dist/src/formats/gml/index.d.ts.map +1 -1
  292. package/dist/src/formats/gml/index.js +79 -26
  293. package/dist/src/formats/gml/index.js.map +1 -1
  294. package/dist/src/formats/gml/syntax.d.ts +67 -18
  295. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  296. package/dist/src/formats/gml/syntax.js +110 -28
  297. package/dist/src/formats/gml/syntax.js.map +1 -1
  298. package/dist/src/formats/graphml/constants.d.ts +174 -44
  299. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  300. package/dist/src/formats/graphml/constants.js +165 -44
  301. package/dist/src/formats/graphml/constants.js.map +1 -1
  302. package/dist/src/formats/graphml/exporter.d.ts +17 -9
  303. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  304. package/dist/src/formats/graphml/exporter.js +33 -12
  305. package/dist/src/formats/graphml/exporter.js.map +1 -1
  306. package/dist/src/formats/graphml/importer.d.ts +19 -7
  307. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  308. package/dist/src/formats/graphml/importer.js +466 -39
  309. package/dist/src/formats/graphml/importer.js.map +1 -1
  310. package/dist/src/formats/graphml/index.d.ts +2 -1
  311. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  312. package/dist/src/formats/graphml/index.js +2 -1
  313. package/dist/src/formats/graphml/index.js.map +1 -1
  314. package/dist/src/formats/graphml/tree.d.ts +6 -2
  315. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  316. package/dist/src/formats/graphml/tree.js +6 -2
  317. package/dist/src/formats/graphml/tree.js.map +1 -1
  318. package/dist/src/formats/graphml/yfiles.d.ts +8 -3
  319. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  320. package/dist/src/formats/graphml/yfiles.js +36 -15
  321. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  322. package/dist/src/formats/json/dialect.d.ts +88 -24
  323. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  324. package/dist/src/formats/json/dialect.js +109 -19
  325. package/dist/src/formats/json/dialect.js.map +1 -1
  326. package/dist/src/formats/json/exporter.d.ts +134 -22
  327. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  328. package/dist/src/formats/json/exporter.js +135 -54
  329. package/dist/src/formats/json/exporter.js.map +1 -1
  330. package/dist/src/formats/json/importer.d.ts +173 -52
  331. package/dist/src/formats/json/importer.d.ts.map +1 -1
  332. package/dist/src/formats/json/importer.js +899 -220
  333. package/dist/src/formats/json/importer.js.map +1 -1
  334. package/dist/src/formats/json/index.d.ts +3 -2
  335. package/dist/src/formats/json/index.d.ts.map +1 -1
  336. package/dist/src/formats/json/index.js +3 -2
  337. package/dist/src/formats/json/index.js.map +1 -1
  338. package/dist/src/formats/json/obographs-export.d.ts +68 -0
  339. package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
  340. package/dist/src/formats/json/obographs-export.js +941 -0
  341. package/dist/src/formats/json/obographs-export.js.map +1 -0
  342. package/dist/src/formats/json/obographs.d.ts +26 -0
  343. package/dist/src/formats/json/obographs.d.ts.map +1 -1
  344. package/dist/src/formats/json/obographs.js +297 -80
  345. package/dist/src/formats/json/obographs.js.map +1 -1
  346. package/dist/src/formats/neo4j/exporter.d.ts +109 -21
  347. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  348. package/dist/src/formats/neo4j/exporter.js +239 -86
  349. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  350. package/dist/src/formats/neo4j/header.d.ts +17 -4
  351. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  352. package/dist/src/formats/neo4j/header.js +36 -4
  353. package/dist/src/formats/neo4j/header.js.map +1 -1
  354. package/dist/src/formats/neo4j/importer.d.ts +107 -27
  355. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  356. package/dist/src/formats/neo4j/importer.js +415 -63
  357. package/dist/src/formats/neo4j/importer.js.map +1 -1
  358. package/dist/src/formats/neo4j/index.d.ts +73 -23
  359. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  360. package/dist/src/formats/neo4j/index.js +65 -25
  361. package/dist/src/formats/neo4j/index.js.map +1 -1
  362. package/dist/src/formats/obo/exporter.d.ts +150 -0
  363. package/dist/src/formats/obo/exporter.d.ts.map +1 -0
  364. package/dist/src/formats/obo/exporter.js +1736 -0
  365. package/dist/src/formats/obo/exporter.js.map +1 -0
  366. package/dist/src/formats/obo/importer.d.ts +95 -19
  367. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  368. package/dist/src/formats/obo/importer.js +475 -104
  369. package/dist/src/formats/obo/importer.js.map +1 -1
  370. package/dist/src/formats/obo/index.d.ts +6 -3
  371. package/dist/src/formats/obo/index.d.ts.map +1 -1
  372. package/dist/src/formats/obo/index.js +6 -3
  373. package/dist/src/formats/obo/index.js.map +1 -1
  374. package/dist/src/formats/obo/syntax.d.ts +108 -4
  375. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  376. package/dist/src/formats/obo/syntax.js +211 -34
  377. package/dist/src/formats/obo/syntax.js.map +1 -1
  378. package/dist/src/formats/pajek/exporter.d.ts +54 -16
  379. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  380. package/dist/src/formats/pajek/exporter.js +67 -35
  381. package/dist/src/formats/pajek/exporter.js.map +1 -1
  382. package/dist/src/formats/pajek/importer.d.ts +84 -22
  383. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  384. package/dist/src/formats/pajek/importer.js +303 -83
  385. package/dist/src/formats/pajek/importer.js.map +1 -1
  386. package/dist/src/formats/pajek/index.d.ts +2 -1
  387. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  388. package/dist/src/formats/pajek/index.js +2 -1
  389. package/dist/src/formats/pajek/index.js.map +1 -1
  390. package/dist/src/formats/pajek/syntax.d.ts +77 -14
  391. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  392. package/dist/src/formats/pajek/syntax.js +186 -54
  393. package/dist/src/formats/pajek/syntax.js.map +1 -1
  394. package/dist/src/formats/xgmml/columns.d.ts +1 -0
  395. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  396. package/dist/src/formats/xgmml/columns.js +4 -0
  397. package/dist/src/formats/xgmml/columns.js.map +1 -1
  398. package/dist/src/formats/xgmml/constants.d.ts +186 -56
  399. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  400. package/dist/src/formats/xgmml/constants.js +181 -57
  401. package/dist/src/formats/xgmml/constants.js.map +1 -1
  402. package/dist/src/formats/xgmml/document.d.ts +65 -11
  403. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  404. package/dist/src/formats/xgmml/document.js +157 -28
  405. package/dist/src/formats/xgmml/document.js.map +1 -1
  406. package/dist/src/formats/xgmml/emit.d.ts +61 -3
  407. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  408. package/dist/src/formats/xgmml/emit.js +129 -26
  409. package/dist/src/formats/xgmml/emit.js.map +1 -1
  410. package/dist/src/formats/xgmml/exporter.d.ts +13 -6
  411. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  412. package/dist/src/formats/xgmml/exporter.js +21 -22
  413. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  414. package/dist/src/formats/xgmml/importer.d.ts +33 -12
  415. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  416. package/dist/src/formats/xgmml/importer.js +35 -72
  417. package/dist/src/formats/xgmml/importer.js.map +1 -1
  418. package/dist/src/formats/xgmml/index.d.ts +1 -0
  419. package/dist/src/formats/xgmml/index.d.ts.map +1 -1
  420. package/dist/src/formats/xgmml/index.js +1 -0
  421. package/dist/src/formats/xgmml/index.js.map +1 -1
  422. package/dist/src/formats/xgmml/values.d.ts +8 -1
  423. package/dist/src/formats/xgmml/values.d.ts.map +1 -1
  424. package/dist/src/formats/xgmml/values.js +8 -3
  425. package/dist/src/formats/xgmml/values.js.map +1 -1
  426. package/dist/src/index.d.ts +23 -18
  427. package/dist/src/index.d.ts.map +1 -1
  428. package/dist/src/index.js +28 -16
  429. package/dist/src/index.js.map +1 -1
  430. package/dist/src/registry.d.ts +478 -92
  431. package/dist/src/registry.d.ts.map +1 -1
  432. package/dist/src/registry.js +807 -88
  433. package/dist/src/registry.js.map +1 -1
  434. package/dist/src/sniff.d.ts +61 -19
  435. package/dist/src/sniff.d.ts.map +1 -1
  436. package/dist/src/sniff.js +121 -29
  437. package/dist/src/sniff.js.map +1 -1
  438. package/dist/src/types.d.ts +362 -112
  439. package/dist/src/types.d.ts.map +1 -1
  440. package/dist/src/types.js +28 -7
  441. package/dist/src/types.js.map +1 -1
  442. package/dist/xgmml.js +2 -2
  443. package/docs/samples/README.md +25 -0
  444. package/docs/samples/got-edges.csv +353 -0
  445. package/docs/samples/got-network.graphml +1384 -0
  446. package/docs/samples/got-nodes.csv +108 -0
  447. package/docs/samples/got.cx +930 -0
  448. package/docs/samples/got.cx2 +465 -0
  449. package/docs/samples/got.gexf +471 -0
  450. package/docs/samples/got.gml +2650 -0
  451. package/docs/samples/got.json +2546 -0
  452. package/docs/samples/got.net +461 -0
  453. package/docs/samples/movies-nodes.csv +9 -0
  454. package/docs/samples/movies-rels.csv +8 -0
  455. package/docs/samples/networks.cys +0 -0
  456. package/docs/samples/proteins.xgmml +9 -0
  457. package/docs/samples/teams.gv +24 -0
  458. package/docs/samples/vehicles.obo +25 -0
  459. package/package.json +9 -4
  460. package/src/children.ts +18 -6
  461. package/src/common/attributes.ts +29 -11
  462. package/src/common/cell-budget.ts +11 -3
  463. package/src/common/choose.ts +89 -0
  464. package/src/common/codes.ts +490 -66
  465. package/src/common/compare.ts +472 -0
  466. package/src/common/cx-export.ts +432 -0
  467. package/src/common/declared-types.ts +40 -6
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +39 -22
  470. package/src/common/export.ts +284 -47
  471. package/src/common/format.ts +30 -1
  472. package/src/common/ids.ts +14 -3
  473. package/src/common/input.ts +1057 -134
  474. package/src/common/json-elements.ts +1035 -199
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +7 -2
  477. package/src/common/ontology-export.ts +461 -0
  478. package/src/common/ontology.ts +31 -9
  479. package/src/common/options.ts +110 -15
  480. package/src/common/plural.ts +49 -0
  481. package/src/common/report.ts +168 -25
  482. package/src/common/temporal.ts +22 -5
  483. package/src/common/text.ts +62 -23
  484. package/src/common/weights.ts +74 -4
  485. package/src/common/writer.ts +37 -3
  486. package/src/common/xml.ts +479 -90
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +202 -47
  489. package/src/formats/csv/header.ts +50 -11
  490. package/src/formats/csv/importer.ts +1133 -245
  491. package/src/formats/csv/index.ts +106 -19
  492. package/src/formats/csv/records.ts +396 -122
  493. package/src/formats/csv/values.ts +1 -0
  494. package/src/formats/cx/exporter.ts +1483 -0
  495. package/src/formats/cx/importer.ts +890 -302
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +62 -339
  498. package/src/formats/cx2/importer.ts +318 -87
  499. package/src/formats/cx2/index.ts +2 -1
  500. package/src/formats/cys/constants.ts +62 -17
  501. package/src/formats/cys/exporter.ts +1368 -0
  502. package/src/formats/cys/importer.ts +659 -157
  503. package/src/formats/cys/index.ts +4 -2
  504. package/src/formats/cys/session.ts +142 -34
  505. package/src/formats/cys/tables.ts +47 -9
  506. package/src/formats/dot/exporter.ts +83 -37
  507. package/src/formats/dot/importer.ts +248 -54
  508. package/src/formats/dot/index.ts +2 -1
  509. package/src/formats/dot/names.ts +44 -11
  510. package/src/formats/dot/tokenizer.ts +25 -6
  511. package/src/formats/gexf/exporter.ts +57 -18
  512. package/src/formats/gexf/importer.ts +636 -105
  513. package/src/formats/gexf/index.ts +90 -21
  514. package/src/formats/gexf/schema.ts +67 -15
  515. package/src/formats/gml/exporter.ts +90 -36
  516. package/src/formats/gml/importer.ts +388 -67
  517. package/src/formats/gml/index.ts +85 -24
  518. package/src/formats/gml/syntax.ts +113 -28
  519. package/src/formats/graphml/constants.ts +178 -44
  520. package/src/formats/graphml/exporter.ts +52 -18
  521. package/src/formats/graphml/importer.ts +640 -60
  522. package/src/formats/graphml/index.ts +2 -1
  523. package/src/formats/graphml/tree.ts +6 -2
  524. package/src/formats/graphml/yfiles.ts +37 -15
  525. package/src/formats/json/dialect.ts +128 -29
  526. package/src/formats/json/exporter.ts +205 -68
  527. package/src/formats/json/importer.ts +1163 -253
  528. package/src/formats/json/index.ts +3 -1
  529. package/src/formats/json/obographs-export.ts +1197 -0
  530. package/src/formats/json/obographs.ts +353 -92
  531. package/src/formats/neo4j/exporter.ts +321 -102
  532. package/src/formats/neo4j/header.ts +44 -6
  533. package/src/formats/neo4j/importer.ts +512 -73
  534. package/src/formats/neo4j/index.ts +75 -23
  535. package/src/formats/obo/exporter.ts +2210 -0
  536. package/src/formats/obo/importer.ts +622 -109
  537. package/src/formats/obo/index.ts +6 -3
  538. package/src/formats/obo/syntax.ts +232 -37
  539. package/src/formats/pajek/exporter.ts +90 -42
  540. package/src/formats/pajek/importer.ts +407 -93
  541. package/src/formats/pajek/index.ts +2 -1
  542. package/src/formats/pajek/syntax.ts +216 -55
  543. package/src/formats/xgmml/columns.ts +10 -0
  544. package/src/formats/xgmml/constants.ts +183 -56
  545. package/src/formats/xgmml/document.ts +212 -34
  546. package/src/formats/xgmml/emit.ts +175 -34
  547. package/src/formats/xgmml/exporter.ts +36 -28
  548. package/src/formats/xgmml/importer.ts +64 -86
  549. package/src/formats/xgmml/index.ts +1 -0
  550. package/src/formats/xgmml/values.ts +12 -4
  551. package/src/index.ts +81 -11
  552. package/src/registry.ts +1121 -135
  553. package/src/sniff.ts +163 -32
  554. package/src/types.ts +385 -114
  555. package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
  556. package/dist/chunks/export-Bh60Dv-n.js +0 -582
  557. package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
  558. package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
  559. package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
  560. package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
  561. package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
  562. package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
  563. package/dist/chunks/importer-DET1tg6s.js.map +0 -1
  564. package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
  565. package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
  566. package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
  567. package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
  568. package/dist/chunks/importer-eUREhBGj.js.map +0 -1
  569. package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
  570. package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
  571. package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
  572. package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
  573. package/dist/chunks/ontology-BnrJ4I98.js +0 -113
  574. package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
  575. package/dist/chunks/records-dbRkxwaq.js.map +0 -1
  576. package/dist/chunks/report-B1z4WT9e.js +0 -2372
  577. package/dist/chunks/report-B1z4WT9e.js.map +0 -1
  578. package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
  579. package/dist/chunks/weights-Dzba96G3.js +0 -176
  580. package/dist/chunks/weights-Dzba96G3.js.map +0 -1
  581. package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
@@ -4,7 +4,7 @@
4
4
  * `directed` / `multigraph` / `graph`), the d3 lineage of the same shape (`name` ids, integer
5
5
  * index endpoints), JSON Graph Format v2 (nodes keyed by id, per-edge `directed`, hyperedges),
6
6
  * Cytoscape.js elements (`data.id` / `data.source` / `data.target`, `position`, `classes`,
7
- * `data.parent`), graphology serialisation (`key` / `attributes`, `undirected` edges, `options`)
7
+ * `data.parent`), graphology serialization (`key` / `attributes`, `undirected` edges, `options`)
8
8
  * vis.js (`from` / `to`), and NetworkX adjacency_data (`nodes` + `adjacency`) and tree_data (nested
9
9
  * `id` / `children`) -- and pushes it scalar by scalar into the sink.
10
10
  *
@@ -32,38 +32,49 @@
32
32
  */
33
33
  import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
34
34
  import { uniqueColumnName } from "../../common/attributes.js";
35
- import { AMBIGUOUS_GRAPH_NAME_CODE, BAD_VALUE_CODE, DANGLING_REFERENCE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, GRAPH_NOT_FOUND_CODE, HYPEREDGE_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, TOO_LARGE_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
35
+ import { AMBIGUOUS_GRAPH_NAME_CODE, BAD_VALUE_CODE, COLUMN_RENAMED_CODE, DANGLING_REFERENCE_CODE, DUPLICATE_ATTRIBUTE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, ELEMENT_ISSUE, EMPTY_COLUMN_DROPPED_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, GRAPH_NOT_FOUND_CODE, HYPEREDGE_CODE, INPUT_ISSUE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, JSON_NONSTANDARD_NUMBER_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, PARENT_CYCLE_CODE, PRECISION_CODE, ROLE_TAKEN_CODE, SYNTAX_CODE, TOO_LARGE_CODE, UNKNOWN_ELEMENT_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
36
36
  import { DirectionResolver } from "../../common/direction.js";
37
37
  import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
38
- import { readText, textChunks, throwIfAborted } from "../../common/input.js";
39
- import { flipY, MAYBE_UNSAFE_INTEGER, reviveNonstandard, rewriteNumbers } from "../../common/json-elements.js";
40
- import { chooseGraph, reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
38
+ import { readText, throwIfAborted } from "../../common/input.js";
39
+ import { findDuplicateKeys, flipY, MAYBE_INEXACT_EXPONENT, MAYBE_UNSAFE_INTEGER, rewriteNumbers, } from "../../common/json-elements.js";
40
+ import { chooseGraph, graphChosen, reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
41
+ import { agree, plural } from "../../common/plural.js";
41
42
  import { ImportReportBuilder } from "../../common/report.js";
42
43
  import { weightFromValue } from "../../common/weights.js";
43
- import { sniffJsonDialectHead } from "../../sniff.js";
44
+ import { headBytes, sniffJsonDialectHead } from "../../sniff.js";
44
45
  import { CLASSES_COLUMN, CYTOSCAPE_ELEMENT_KEYS, CYTOSCAPE_STRUCTURAL_KEYS, DIALECT_DEFAULT_DIRECTED, hasKey, isJsonImportDialect, isJsonObject, JSON_IMPORT_DIALECTS, META_KEY, NODE_LINK_SOURCE_KEYS, NODE_LINK_TARGET_KEYS, PARENT_COLUMN, POSITION_COLUMN, sniffJsonDialect, SUFFIX, } from "./dialect.js";
45
46
  import { importObographs } from "./obographs.js";
46
47
  /**
47
- * The issue codes the JSON importer records (design section 8.6), by name: the codes shared with
48
- * the other importers (src/common/codes.ts) and the JSON-specific ones. A key is the code without
48
+ * The issue codes the JSON importer records, by name: the codes shared with
49
+ * the other importers and the JSON-specific ones. A key is the code without
49
50
  * its severity and format prefixes.
51
+ * @category Built-in formats
50
52
  */
51
53
  export const JSON_ISSUE = Object.freeze({
52
- /** The text is empty or whitespace (fatal). */
54
+ ...INPUT_ISSUE,
55
+ ...ELEMENT_ISSUE,
56
+ /** The text is empty or whitespace. The import stops. */
53
57
  EMPTY_INPUT: EMPTY_INPUT_CODE,
54
- /** JSON.parse refused the text (fatal). */
58
+ /** The text is not valid JSON. The import stops. */
55
59
  SYNTAX: SYNTAX_CODE,
56
- /** No dialect matches the document's top-level shape. */
60
+ /** No dialect matches the document's top-level shape. The import stops. */
57
61
  DIALECT: "E_JSON_DIALECT",
58
62
  /** A section (nodes, edges, elements, graph) has the wrong JSON type. */
59
63
  SHAPE: "E_JSON_SHAPE",
60
- /** A node-link document lacks its nodes or its edges array, or a Cytoscape document its elements. */
61
- MISSING_SECTION: "E_MISSING_SECTION",
64
+ /**
65
+ * A section the dialect expects is not there: a node-link document without its nodes or its edges array, an
66
+ * adjacency document without lists for some nodes, or a `nodesPath` / `edgesPath` that names nothing. Nothing is
67
+ * skipped: the graph is read without that section (nodes come from the edges, or there are no edges).
68
+ */
69
+ MISSING_SECTION: "W_MISSING_SECTION",
62
70
  /** A node record or an element is not an object. */
63
71
  BAD_ELEMENT: "E_BAD_ELEMENT",
64
72
  /** A node record has no id. */
65
73
  MISSING_ID: MISSING_ID_CODE,
66
- /** A node id is a JSON boolean or null (legal in NetworkX, not a NodeId); coerced only under ids "string". */
74
+ /**
75
+ * A node id is a JSON boolean or null, which graph-io cannot use as an id; the node is skipped. With `ids:
76
+ * "string"` it is read as the text "true", "false" or "null".
77
+ */
67
78
  UNSUPPORTED_ID: "E_UNSUPPORTED_ID",
68
79
  /** An edge record has no source or no target. */
69
80
  MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
@@ -79,25 +90,35 @@ export const JSON_ISSUE = Object.freeze({
79
90
  DUPLICATE_NODE: DUPLICATE_NODE_CODE,
80
91
  /** An edge id (Cytoscape data.id, graphology key, vis id) repeated by a later edge; the edge is skipped. */
81
92
  DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
82
- /** Two distinct id texts merged into one number under ids "number". */
93
+ /** Two different id texts became the same number because `ids` is "number", so their nodes were merged. */
83
94
  ID_MERGED: ID_MERGED_CODE,
84
95
  /** Edge ids of mixed JSON types were stored as text. */
85
96
  EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
86
- /** A JGF or OBO Graphs `graphs` array holds more than one graph; only the chosen one is read. */
97
+ /**
98
+ * The file holds several graphs and only the first was read. It is not added when `graphIndex` or `graphName` chose the graph.
99
+ * `importAllGraphs()` reads every one.
100
+ */
87
101
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
88
- /** `graphIndex` is beyond the `graphs` array, or `graphName` names none of its graphs (fatal). */
102
+ /**
103
+ * `graphIndex` is past the end of the `graphs` array, or `graphName` matches none of its graphs. The import stops.
104
+ */
89
105
  GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
90
- /** `graphName` names more than one graph of the `graphs` array (fatal). */
106
+ /** `graphName` matches more than one graph of the `graphs` array. The import stops. */
91
107
  AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
92
- /** obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
108
+ /** Obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
93
109
  OBOGRAPHS_SUBJ: "W_JSON_OBOGRAPHS_SUBJ",
94
- /** obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under addMissingNodes false). */
110
+ /**
111
+ * Obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under
112
+ * addMissingNodes false).
113
+ */
95
114
  DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
96
- /** The document is longer than one JavaScript string can hold (fatal; category unsupported). */
115
+ /** The document is longer than a JavaScript string can hold. The import stops. */
97
116
  TOO_LARGE: TOO_LARGE_CODE,
98
- /** JGF hyperedges under the "error" policy. */
117
+ /** A JGF hyperedge (an edge with more than two ends) while `hyperedges` is "error". */
99
118
  HYPEREDGE: HYPEREDGE_CODE,
100
- /** JGF hyperedges skipped under the default "skip" policy. */
119
+ /**
120
+ * JGF hyperedges were skipped, because `hyperedges` is "skip" (the default). Pass "star" or "clique" to keep them.
121
+ */
101
122
  HYPEREDGES_SKIPPED: "W_HYPEREDGES_SKIPPED",
102
123
  /** A JGF hyperedge with neither a nodes array nor source / target arrays. */
103
124
  HYPEREDGE_SHAPE: "E_HYPEREDGE_SHAPE",
@@ -105,22 +126,59 @@ export const JSON_ISSUE = Object.freeze({
105
126
  POSITIONAL_NODES: "W_POSITIONAL_NODES",
106
127
  /** A node-link / d3 top-level key the importer does not read (the other of edges / links, an unknown key); it is dropped. */
107
128
  UNREAD_KEY: "W_JSON_UNREAD_KEY",
108
- /** A builder-policy option (addMissingNodes, duplicateEdges, selfLoops, weightDtype) differs from the sink's (the shared W_SINK_OPTION). */
129
+ /**
130
+ * You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
131
+ * `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
132
+ */
109
133
  SINK_OPTION: SINK_OPTION_CODE,
110
- /** A common option the dialect has no use for (nodeIdFrom outside node-link, long, restoreMangledIds). */
134
+ /** You set an option this format does not use; it had no effect. The message names the option. */
111
135
  OPTION_IGNORED: OPTION_IGNORED_CODE,
112
- /** The input holds invalid UTF-8 (fatal). */
136
+ /** The input is not valid UTF-8. The import stops. */
113
137
  INVALID_UTF8: INVALID_UTF8_CODE,
114
- /** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
138
+ /**
139
+ * Some bytes are not valid in the encoding that was chosen (by a byte order mark, the file's declaration or the
140
+ * `encoding` option). The import stops.
141
+ */
115
142
  INVALID_ENCODING: INVALID_ENCODING_CODE,
116
143
  /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
117
144
  ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
118
145
  /** The document uses the non-standard tokens NaN / Infinity / -Infinity (Python's json writes them); read as numbers. */
119
- NONSTANDARD_NUMBER: "W_JSON_NONSTANDARD_NUMBER",
146
+ NONSTANDARD_NUMBER: JSON_NONSTANDARD_NUMBER_CODE,
120
147
  /** Integer literals beyond 2^53 were read as their exact digits (strings), not as rounded numbers. */
121
148
  BIG_INTEGER: "W_JSON_BIG_INTEGER",
122
149
  /** A declared encoding the platform cannot decode was ignored. */
123
150
  UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
151
+ /** An integer beyond 2^53 was stored as the nearest 64-bit float; pass `long: "string"` to keep every digit. */
152
+ PRECISION: PRECISION_CODE,
153
+ /**
154
+ * The document contradicts itself: a declared option its edges break (multigraph false with
155
+ * parallel links, graphology's options), a record whose section disagrees with its shape, a
156
+ * JGF inner id other than its key, the two listings of one adjacency edge.
157
+ */
158
+ INCONSISTENT: "W_JSON_INCONSISTENT",
159
+ /** IndexLinks "auto" read integer endpoints as array positions although they also name node ids. */
160
+ INDEX_LINKS: "W_JSON_INDEX_LINKS",
161
+ /** A Cytoscape parent link that would close a cycle; that link is dropped. */
162
+ PARENT_CYCLE: PARENT_CYCLE_CODE,
163
+ /** An attribute key that is null on every element makes no column (NetworkX writes None as null). */
164
+ EMPTY_COLUMN_DROPPED: EMPTY_COLUMN_DROPPED_CODE,
165
+ /** OBO Graphs: a node type or synonym predicate outside the schema's set; kept as written, once per name. */
166
+ UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
167
+ /**
168
+ * A key repeated in one JSON object (JSON.parse keeps the last value, the earlier is dropped),
169
+ * or, in OBO Graphs, a single-valued OBO tag given twice in basicPropertyValues (the first is kept).
170
+ */
171
+ DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
172
+ /**
173
+ * An attribute was renamed `<name>#<suffix>` because another attribute already has its name, for example
174
+ * two attributes declared with the same name.
175
+ */
176
+ COLUMN_RENAMED: COLUMN_RENAMED_CODE,
177
+ /**
178
+ * You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
179
+ * a plain attribute.
180
+ */
181
+ ROLE_TAKEN: ROLE_TAKEN_CODE,
124
182
  });
125
183
  /** The common options the JSON importer reads (the rest is reported by reportUnusedOptions). */
126
184
  const USED_OPTIONS = new Set([
@@ -141,6 +199,8 @@ const USED_OPTIONS = new Set([
141
199
  const FORMAT_DEFAULTS = { ids: "keep", defaultDirected: false, weightFrom: "weight" };
142
200
  /** Bytes of the head sniff() inspects. */
143
201
  const SNIFF_BYTES = 4096;
202
+ /** How many dangling endpoints the W_DANGLING_REFERENCE message names. */
203
+ const DANGLING_SHOWN = 5;
144
204
  /** Elements pushed between two checks of the cancellation signal (the whole document is one chunk). */
145
205
  const ABORT_CHECK_INTERVAL = 64;
146
206
  /** The top-level keys whose presence in the head marks a graph document rather than arbitrary JSON. */
@@ -238,18 +298,24 @@ function pathOption(name, value) {
238
298
  return segments;
239
299
  }
240
300
  /**
241
- * The value at a path of object keys, or undefined when a step is missing or not an object.
301
+ * The value at a path of object keys and array positions (`graphs.0.nodes`), or undefined when a
302
+ * step is missing.
242
303
  * @param root - the document
243
- * @param segments - the keys
304
+ * @param segments - the keys; a decimal integer indexes an array
244
305
  * @returns the value
245
306
  */
246
307
  function valueAt(root, segments) {
247
308
  let value = root;
248
309
  for (const segment of segments) {
249
- if (!isJsonObject(value) || !hasKey(value, segment)) {
310
+ if (Array.isArray(value) && /^(0|[1-9]\d*)$/.test(segment) && Number(segment) < value.length) {
311
+ value = value[Number(segment)];
312
+ }
313
+ else if (isJsonObject(value) && hasKey(value, segment)) {
314
+ value = value[segment];
315
+ }
316
+ else {
250
317
  return undefined;
251
318
  }
252
- value = value[segment];
253
319
  }
254
320
  return value;
255
321
  }
@@ -272,7 +338,7 @@ function applyPaths(root, json, report) {
272
338
  const value = valueAt(root, segments);
273
339
  if (value === undefined) {
274
340
  const path = segments.join(".");
275
- report.error("missing-value", JSON_ISSUE.MISSING_SECTION, `${option} ${JSON.stringify(path)} names nothing in the document`, { element: path });
341
+ report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, `${option} ${JSON.stringify(path)} names nothing in the document`, { element: path });
276
342
  return [];
277
343
  }
278
344
  return value;
@@ -339,12 +405,23 @@ function pathEdgesKey(json, edgesPath) {
339
405
  * rule over the record, else node-link.
340
406
  * @param record - the graph record
341
407
  * @param forced - the caller's dialect option
408
+ * @param placedEdges - whether edgesPath placed the record's edge array (under links or edges)
342
409
  * @returns the dialect
343
410
  */
344
- function pathDialect(record, forced) {
411
+ function pathDialect(record, forced, placedEdges) {
345
412
  if (forced !== "auto") {
346
413
  return forced;
347
414
  }
415
+ // edgesPath puts the edge array under links only so a bare d3 document stays d3; under edges,
416
+ // vis and graphology edges are told apart from NetworkX links again. A links key the document
417
+ // itself holds is NetworkX's and settles node-link.
418
+ if (placedEdges && isJsonObject(record) && hasKey(record, "links") && !hasKey(record, "edges")) {
419
+ const { links, ...rest } = record;
420
+ const asEdges = sniffJsonDialect({ ...rest, edges: links });
421
+ if (asEdges === "vis" || asEdges === "graphology") {
422
+ return asEdges;
423
+ }
424
+ }
348
425
  const sniffed = sniffJsonDialect(record);
349
426
  return sniffed !== null && PATH_DIALECTS.has(sniffed) ? sniffed : "node-link";
350
427
  }
@@ -361,7 +438,7 @@ function documentOf(parsed, json, report) {
361
438
  return { root: parsed, dialect: detectDialect(parsed, json.dialect, report) };
362
439
  }
363
440
  const root = applyPaths(parsed, json, report);
364
- const dialect = pathDialect(root, json.dialect);
441
+ const dialect = pathDialect(root, json.dialect, json.edgesPath !== null);
365
442
  // vis and graphology read their edges from the edges key only
366
443
  if (json.edgesPath !== null && (dialect === "vis" || dialect === "graphology") && isJsonObject(root)) {
367
444
  const key = pathEdgesKey(json, json.edgesPath);
@@ -432,12 +509,16 @@ class AttributeWriter {
432
509
  * Create a writer.
433
510
  * @param sink - the sink
434
511
  * @param domain - node or edge
512
+ * @param report - where a refused cell is recorded
435
513
  */
436
- constructor(sink, domain) {
514
+ constructor(sink, domain, report) {
437
515
  this.handles = new Map();
438
516
  this.reservedNames = new Set();
517
+ /** Column names written only with null so far (NetworkX writes None as null). */
518
+ this.nullOnly = new Set();
439
519
  this.sink = sink;
440
520
  this.domain = domain;
521
+ this.report = report;
441
522
  }
442
523
  /**
443
524
  * Declare a structural column up front; attribute keys with its name are suffixed from now on.
@@ -468,28 +549,53 @@ class AttributeWriter {
468
549
  return this.lookup(name) !== INVALID_INDEX;
469
550
  }
470
551
  /**
471
- * Write one attribute cell by its source key; null and undefined leave the row unset.
552
+ * Write one attribute cell by its source key; null and undefined leave the row unset. A cell the
553
+ * sink refuses is recorded and skipped, so the element's other keys are still written.
472
554
  * @param row - the node or edge index
473
555
  * @param key - the source key
474
556
  * @param value - the JSON value
475
557
  * @param suffix - the suffix applied when the key collides with a structural column
558
+ * @param element - the element name for an issue
476
559
  */
477
- write(row, key, value, suffix) {
478
- if (value === undefined || value === null) {
560
+ write(row, key, value, suffix, element) {
561
+ if (value === undefined) {
479
562
  return;
480
563
  }
481
564
  const name = this.reservedNames.has(key) ? `${key}${suffix}` : key;
482
- const cached = this.handles.get(name);
483
- if (cached !== undefined) {
484
- this.set(cached, row, value);
565
+ if (value === null) {
566
+ this.nullOnly.add(name);
567
+ return;
568
+ }
569
+ try {
570
+ const cached = this.handles.get(name);
571
+ if (cached !== undefined) {
572
+ this.set(cached, row, value);
573
+ return;
574
+ }
575
+ this.set(name, row, value);
576
+ }
577
+ catch (err) {
578
+ if (!(err instanceof GraphFormatError)) {
579
+ throw err;
580
+ }
581
+ this.report.recordError(err, { element: `${element}.${key}` });
485
582
  return;
486
583
  }
487
- this.set(name, row, value);
488
584
  const handle = this.lookup(name);
489
585
  if (handle !== INVALID_INDEX) {
490
586
  this.handles.set(name, handle);
491
587
  }
492
588
  }
589
+ /**
590
+ * Report the keys that were null on every element that had them: they made no column, so a
591
+ * round trip loses them (W_EMPTY_COLUMN_DROPPED, one warning per table).
592
+ */
593
+ reportNullOnly() {
594
+ const names = [...this.nullOnly].filter((name) => this.lookup(name) === INVALID_INDEX);
595
+ if (names.length > 0) {
596
+ this.report.warning("missing-value", EMPTY_COLUMN_DROPPED_CODE, `${this.domain} key${plural(names.length)} ${names.map((n) => JSON.stringify(n)).join(", ")} ${agree(names.length, "is", "are")} null wherever they appear; no column is made for them`, { element: names[0] });
597
+ }
598
+ }
493
599
  /**
494
600
  * Write through a handle or a name.
495
601
  * @param column - the handle or name
@@ -516,6 +622,7 @@ class AttributeWriter {
516
622
  // ============================================================ the import context
517
623
  /**
518
624
  * Everything one import call shares between the dialect readers.
625
+ * @category Plugin helpers
519
626
  */
520
627
  export class ImportContext {
521
628
  /**
@@ -531,14 +638,21 @@ export class ImportContext {
531
638
  this.elementsSinceCheck = 0;
532
639
  /** The direction the file declares (or the default), set by setHeader(). */
533
640
  this.fileDirected = false;
641
+ /** Endpoints that named no node and became placeholder nodes, the first few kept for the message. */
642
+ this.danglingIds = [];
643
+ this.danglingCount = 0;
644
+ /** Set when the document has no node section: every node comes from an edge (E_MISSING_SECTION says so). */
645
+ this.nodesFromEdges = false;
646
+ /** Dangling endpoints whose id, as the other JSON type, is a node (`"1"` next to the node 1). */
647
+ this.nearMatches = [];
534
648
  this.sink = sink;
535
649
  this.report = report;
536
650
  this.options = options;
537
651
  this.json = json;
538
652
  this.ids = new IdCoercer(options.ids);
539
653
  this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
540
- this.nodes = new AttributeWriter(sink, "node");
541
- this.edges = new AttributeWriter(sink, "edge");
654
+ this.nodes = new AttributeWriter(sink, "node", report);
655
+ this.edges = new AttributeWriter(sink, "edge", report);
542
656
  this.explicitDefaultDirected = explicitDefaultDirected;
543
657
  }
544
658
  /**
@@ -629,13 +743,14 @@ export class ImportContext {
629
743
  * Add a node, counting it or recording the failure.
630
744
  * @param id - the node id
631
745
  * @param element - the element name
746
+ * @param why - more about a repeated id, for the duplicate warning
632
747
  * @returns the node index, or -1 when the sink refused the node
633
748
  */
634
- pushNode(id, element) {
749
+ pushNode(id, element, why = "") {
635
750
  this.checkAbort();
636
751
  const existing = this.sink.indexOf(id);
637
752
  if (existing !== INVALID_INDEX) {
638
- this.report.warning("merged", JSON_ISSUE.DUPLICATE_NODE, `${element}: node ${JSON.stringify(id)} already exists; its attributes are merged (the later values win)`, { element });
753
+ this.report.warning("merged", JSON_ISSUE.DUPLICATE_NODE, `${element}: node ${JSON.stringify(id)} already exists${why}; its attributes are merged (the later values win)`, { element });
639
754
  return existing;
640
755
  }
641
756
  let index;
@@ -662,16 +777,50 @@ export class ImportContext {
662
777
  pushEdge(source, target, kind, weight, element) {
663
778
  this.checkAbort();
664
779
  const before = this.sink.edgeCount;
665
- // endpoints the sink creates (addMissingNodes) count as nodes too
666
- let created = this.sink.indexOf(source) === INVALID_INDEX ? 1 : 0;
667
- if (source !== target && this.sink.indexOf(target) === INVALID_INDEX) {
668
- created++;
669
- }
780
+ // endpoints the sink creates (addMissingNodes) count as nodes too, and are reported
781
+ const missingSource = this.sink.indexOf(source) === INVALID_INDEX;
782
+ const missingTarget = source !== target && this.sink.indexOf(target) === INVALID_INDEX;
670
783
  const edge = this.direction.addEdge(source, target, kind, weight, { element });
671
784
  this.report.counts.edges += this.sink.edgeCount - before;
672
- this.report.counts.nodes += created;
785
+ if (missingSource) {
786
+ this.dangling(source);
787
+ }
788
+ if (missingTarget) {
789
+ this.dangling(target);
790
+ }
673
791
  return edge;
674
792
  }
793
+ /**
794
+ * Count a placeholder node an edge endpoint created.
795
+ * @param id - the endpoint
796
+ */
797
+ dangling(id) {
798
+ this.report.counts.nodes++;
799
+ this.danglingCount++;
800
+ if (this.danglingIds.length < DANGLING_SHOWN) {
801
+ this.danglingIds.push(id);
802
+ }
803
+ const other = typeof id === "number" ? String(id) : Number(id);
804
+ if (this.nearMatches.length < DANGLING_SHOWN &&
805
+ (typeof other === "string" || String(other) === id) &&
806
+ this.sink.indexOf(other) !== INVALID_INDEX) {
807
+ this.nearMatches.push(id);
808
+ }
809
+ }
810
+ /**
811
+ * Report the edge endpoints that named no node (the shared W_DANGLING_REFERENCE, once with the
812
+ * count): each became a placeholder node, which is rarely what the file meant.
813
+ */
814
+ reportDangling() {
815
+ if (this.danglingCount === 0 || this.nodesFromEdges) {
816
+ return;
817
+ }
818
+ const shown = this.danglingIds.map((id) => JSON.stringify(id)).join(", ");
819
+ const near = this.nearMatches.length > 0
820
+ ? `; ${this.nearMatches.map((id) => JSON.stringify(id)).join(", ")} ${agree(this.nearMatches.length, "names", "name")} a node id of another JSON type (a string next to a number), which is a different id`
821
+ : "";
822
+ this.report.warning("validation-error", JSON_ISSUE.DANGLING_REFERENCE, `${this.danglingCount} edge endpoint${plural(this.danglingCount)} ${agree(this.danglingCount, "names", "name")} no node and became placeholder nodes: ${shown}${this.danglingCount > DANGLING_SHOWN ? ", ..." : ""}${near}`, { element: String(this.danglingIds[0]) });
823
+ }
675
824
  /**
676
825
  * Check the cancellation signal every ABORT_CHECK_INTERVAL pushed elements, so an abort raised
677
826
  * while the whole in-memory document is being walked rejects promptly.
@@ -805,16 +954,17 @@ export class ImportContext {
805
954
  * @param dict - the nested attribute dict
806
955
  * @param structural - the element keys that are not attributes
807
956
  * @param weightFrom - the weight key to skip in the dict, or null
957
+ * @param element - the element name for issues
808
958
  */
809
- writeNested(writer, row, record, dict, structural, weightFrom) {
959
+ writeNested(writer, row, record, dict, structural, weightFrom, element) {
810
960
  for (const key of Object.keys(dict)) {
811
961
  if (key !== weightFrom) {
812
- writer.write(row, key, dict[key], SUFFIX.data);
962
+ writer.write(row, key, dict[key], SUFFIX.data, element);
813
963
  }
814
964
  }
815
965
  for (const key of Object.keys(record)) {
816
966
  if (!structural.has(key)) {
817
- writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data);
967
+ writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data, element);
818
968
  }
819
969
  }
820
970
  }
@@ -849,6 +999,7 @@ export class ImportContext {
849
999
  declareEdgeIds(edges, read, name, unique) {
850
1000
  let numbers = 0;
851
1001
  let strings = 0;
1002
+ let others = 0;
852
1003
  for (const edge of edges) {
853
1004
  if (!isJsonObject(edge)) {
854
1005
  continue;
@@ -860,17 +1011,21 @@ export class ImportContext {
860
1011
  else if (typeof raw === "string") {
861
1012
  strings++;
862
1013
  }
1014
+ else if (raw !== undefined && raw !== null) {
1015
+ others++;
1016
+ }
863
1017
  }
864
- if (numbers + strings === 0) {
1018
+ if (numbers + strings + others === 0) {
865
1019
  return null;
866
1020
  }
867
- const dtype = strings === 0 ? "f64" : "string";
1021
+ // ids of no usable type still get a column, so edgeIdValue() reports each instead of dropping it
1022
+ const dtype = strings === 0 && numbers > 0 ? "f64" : "string";
868
1023
  const columnName = uniqueColumnName(name, "id", (candidate) => this.edges.taken(candidate));
869
1024
  const handle = this.edges.declare({ name: columnName, dtype, role: "id", nullable: true, unique });
870
1025
  const stringify = strings > 0 && numbers > 0;
871
1026
  const seen = unique ? new Set() : null;
872
1027
  if (stringify) {
873
- this.report.warning("coercion", JSON_ISSUE.EDGE_ID_STRINGIFIED, `edge ids mix numbers and strings; ${numbers} numeric id(s) stored as text`, { element: columnName });
1028
+ this.report.warning("coercion", JSON_ISSUE.EDGE_ID_STRINGIFIED, `edge ids mix numbers and strings; ${numbers} numeric id${plural(numbers)} stored as text`, { element: columnName });
874
1029
  }
875
1030
  return { handle, stringify, seen };
876
1031
  }
@@ -918,7 +1073,8 @@ export class ImportContext {
918
1073
  }
919
1074
  // ============================================================ document level
920
1075
  /**
921
- * Parse the whole text; syntax errors and empty input abort the import.
1076
+ * Parse the whole text; syntax errors and empty input abort the import. A key repeated in one
1077
+ * object (which JSON.parse silently reduces to its last value) is reported per repetition.
922
1078
  * @param text - the decoded text
923
1079
  * @param report - the report
924
1080
  * @returns the parsed value
@@ -929,6 +1085,8 @@ function parseDocument(text, report) {
929
1085
  }
930
1086
  let root;
931
1087
  let syntaxError = null;
1088
+ // the text the syntax error was found in: the rewrite's when it parsed further than the original
1089
+ let errorText = text;
932
1090
  try {
933
1091
  root = JSON.parse(text);
934
1092
  }
@@ -936,32 +1094,156 @@ function parseDocument(text, report) {
936
1094
  syntaxError = err instanceof Error ? err.message : String(err);
937
1095
  }
938
1096
  // the fast path: strict JSON without a digit run long enough to be an unsafe integer
939
- if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text)) {
1097
+ if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text) && !MAYBE_INEXACT_EXPONENT.test(text)) {
1098
+ reportDuplicateKeys(text, report);
940
1099
  return root;
941
1100
  }
942
- const scan = rewriteNumbers(text);
943
- if (scan.tokens.size > 0 || scan.bigIntegers.length > 0) {
944
- try {
945
- root = JSON.parse(scan.text, scan.tokens.size > 0 ? reviveNonstandard : undefined);
946
- syntaxError = null;
1101
+ let scan;
1102
+ try {
1103
+ scan = rewriteNumbers(text);
1104
+ if (scan.tokens.size > 0 || scan.bigIntegers.length > 0) {
1105
+ try {
1106
+ root = JSON.parse(scan.text, scan.tokens.size > 0 ? scan.revive : undefined);
1107
+ syntaxError = null;
1108
+ }
1109
+ catch (err) {
1110
+ if (err instanceof RangeError) {
1111
+ throw err;
1112
+ }
1113
+ // the rewrite read past the non-standard tokens: its error is the real one
1114
+ syntaxError = err instanceof Error ? err.message : String(err);
1115
+ errorText = scan.text;
1116
+ }
947
1117
  }
948
- catch {
949
- // the rewrite did not make it valid JSON: report the parser's message on the original text
1118
+ }
1119
+ catch (err) {
1120
+ if (!(err instanceof RangeError)) {
1121
+ throw err;
950
1122
  }
1123
+ // the rewrite (quoted big integers, sentinel strings) is longer than one string can hold
1124
+ const message = `the document with its numbers rewritten is longer than the most one JavaScript string holds`;
1125
+ report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
1126
+ throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
951
1127
  }
952
1128
  if (syntaxError !== null) {
953
- return report.fail(JSON_ISSUE.SYNTAX, `invalid JSON: ${syntaxError}`);
1129
+ return report.fail(JSON_ISSUE.SYNTAX, syntaxMessage(text, syntaxError), {
1130
+ line: syntaxLine(errorText, syntaxError),
1131
+ });
954
1132
  }
1133
+ reportDuplicateKeys(text, report);
955
1134
  if (scan.tokens.size > 0) {
956
- report.warning("coercion", JSON_ISSUE.NONSTANDARD_NUMBER, `the document uses the non-standard token(s) ${[...scan.tokens].join(", ")}, which strict JSON does not allow; read as numbers`);
1135
+ report.warning("coercion", JSON_ISSUE.NONSTANDARD_NUMBER, `the document uses the non-standard token${plural(scan.tokens.size)} ${[...scan.tokens].join(", ")}, which strict JSON does not allow; read as numbers`);
957
1136
  }
958
1137
  if (scan.bigIntegers.length > 0) {
959
- const shown = scan.bigIntegers.slice(0, BIG_INTEGERS_SHOWN).join(", ");
960
- const more = scan.bigIntegers.length - BIG_INTEGERS_SHOWN;
961
- report.warning("precision", JSON_ISSUE.BIG_INTEGER, `${scan.bigIntegers.length} integer(s) beyond 2^53 kept as text so no digit is lost: ${shown}${more > 0 ? ` and ${more} more` : ""}`);
1138
+ report.warning("precision", JSON_ISSUE.BIG_INTEGER, `${scan.bigIntegers.length} integer${plural(scan.bigIntegers.length)} beyond 2^53 kept as text so no digit is lost: ${listed(scan.bigIntegers)}`);
1139
+ }
1140
+ if (scan.inexact.length > 0) {
1141
+ report.warning("precision", JSON_ISSUE.PRECISION, `${scan.inexact.length} number literal${plural(scan.inexact.length)} no double holds exactly, read as the nearest double (an infinity beyond the range, 0 below it): ${listed(scan.inexact)}`);
962
1142
  }
963
1143
  return root;
964
1144
  }
1145
+ /**
1146
+ * The first literals of a list for a message, with the count of the rest.
1147
+ * @param literals - the literals
1148
+ * @returns the text
1149
+ */
1150
+ function listed(literals) {
1151
+ const more = literals.length - BIG_INTEGERS_SHOWN;
1152
+ const shown = literals.slice(0, BIG_INTEGERS_SHOWN).join(", ");
1153
+ return more > 0 ? `${shown} and ${more} more` : shown;
1154
+ }
1155
+ /** How many repeated keys are reported one by one before the rest is summed up. */
1156
+ const DUPLICATE_KEYS_SHOWN = 10;
1157
+ /**
1158
+ * Report every key repeated in one object (JSON.parse keeps the last value, so the earlier one is
1159
+ * gone): one warning per repetition up to DUPLICATE_KEYS_SHOWN, then one with the remaining count.
1160
+ * @param text - the document text
1161
+ * @param report - the report
1162
+ */
1163
+ function reportDuplicateKeys(text, report) {
1164
+ const found = findDuplicateKeys(text);
1165
+ let line = 1;
1166
+ let counted = 0;
1167
+ for (const { key, offset } of found.slice(0, DUPLICATE_KEYS_SHOWN)) {
1168
+ line += countLines(text, counted, offset);
1169
+ counted = offset;
1170
+ report.warning("merged", JSON_ISSUE.DUPLICATE_ATTRIBUTE, `key ${JSON.stringify(key)} appears twice in one object; the earlier value is dropped (JSON.parse keeps the last)`, { line, element: key });
1171
+ }
1172
+ if (found.length > DUPLICATE_KEYS_SHOWN) {
1173
+ report.warning("merged", JSON_ISSUE.DUPLICATE_ATTRIBUTE, `${found.length - DUPLICATE_KEYS_SHOWN} more repeated key${plural(found.length - DUPLICATE_KEYS_SHOWN)}; each earlier value is dropped`);
1174
+ }
1175
+ }
1176
+ /**
1177
+ * The line breaks in a range of the text.
1178
+ * @param text - the text
1179
+ * @param from - the start offset
1180
+ * @param to - the end offset
1181
+ * @returns how many \n the range holds
1182
+ */
1183
+ function countLines(text, from, to) {
1184
+ let lines = 0;
1185
+ for (let i = text.indexOf("\n", from); i >= 0 && i < to; i = text.indexOf("\n", i + 1)) {
1186
+ lines++;
1187
+ }
1188
+ return lines;
1189
+ }
1190
+ /** The longest text the line of an unpositioned syntax error is searched in (a bisection of JSON.parse calls). */
1191
+ const SYNTAX_SEARCH_LIMIT = 64 * 1024 * 1024;
1192
+ /**
1193
+ * The 1-based line of a JSON.parse error: from the message's "(line L" or "position N" when the
1194
+ * engine gives one, the last line for an unexpected end, else the shortest prefix that fails the
1195
+ * same way (V8 names no position for "Unexpected token").
1196
+ * @param text - the document
1197
+ * @param message - the parser's message
1198
+ * @returns the line
1199
+ */
1200
+ function syntaxLine(text, message) {
1201
+ const line = /\(line (\d+)/.exec(message);
1202
+ if (line !== null) {
1203
+ return Number(line[1]);
1204
+ }
1205
+ const position = /position (\d+)/.exec(message);
1206
+ if (position !== null) {
1207
+ return countLines(text, 0, Number(position[1])) + 1;
1208
+ }
1209
+ if (!message.startsWith("Unexpected token") || text.length > SYNTAX_SEARCH_LIMIT) {
1210
+ // ponytail: an unexpected end (or a huge text) is placed on the last line
1211
+ return countLines(text, 0, text.length) + 1;
1212
+ }
1213
+ // the shortest prefix that holds the offending token: shorter prefixes end early instead
1214
+ let lo = 0;
1215
+ let hi = text.length;
1216
+ while (lo < hi) {
1217
+ const mid = (lo + hi) >>> 1;
1218
+ let failsOnToken = false;
1219
+ try {
1220
+ JSON.parse(text.slice(0, mid + 1));
1221
+ }
1222
+ catch (err) {
1223
+ failsOnToken = err instanceof Error && err.message.startsWith("Unexpected token");
1224
+ }
1225
+ if (failsOnToken) {
1226
+ hi = mid;
1227
+ }
1228
+ else {
1229
+ lo = mid + 1;
1230
+ }
1231
+ }
1232
+ return countLines(text, 0, lo) + 1;
1233
+ }
1234
+ /**
1235
+ * The E_SYNTAX message: the parser's, with a hint when the text is JSON Lines (one document per
1236
+ * line, as apoc.export.json and many loggers write), which this importer does not read.
1237
+ * @param text - the document
1238
+ * @param message - the parser's message
1239
+ * @returns the message
1240
+ */
1241
+ function syntaxMessage(text, message) {
1242
+ if (looksLikeJsonLines(text)) {
1243
+ return `invalid JSON: the input is JSON Lines (one JSON value per line), which is not a graph format graph-io reads; convert it to one JSON document (${message})`;
1244
+ }
1245
+ return `invalid JSON: ${message}`;
1246
+ }
965
1247
  /** How many of the integers kept as text the warning lists. */
966
1248
  const BIG_INTEGERS_SHOWN = 10;
967
1249
  /**
@@ -1024,7 +1306,7 @@ function flagOf(value, what, fallback, report) {
1024
1306
  }
1025
1307
  /**
1026
1308
  * The endpoint key and value of an edge record: the explicit key when given, else the first of the
1027
- * default keys the record has.
1309
+ * default keys the record has (a null value counts as absent).
1028
1310
  * @param record - the edge record
1029
1311
  * @param explicit - the caller's key, or null
1030
1312
  * @param defaults - the default keys
@@ -1032,15 +1314,57 @@ function flagOf(value, what, fallback, report) {
1032
1314
  */
1033
1315
  function endpointOf(record, explicit, defaults) {
1034
1316
  if (explicit !== null) {
1035
- return { key: hasKey(record, explicit) ? explicit : null, value: record[explicit] };
1317
+ return { key: present(record, explicit) ? explicit : null, value: record[explicit] };
1036
1318
  }
1037
1319
  for (const key of defaults) {
1038
- if (hasKey(record, key)) {
1320
+ if (present(record, key)) {
1039
1321
  return { key, value: record[key] };
1040
1322
  }
1041
1323
  }
1042
1324
  return { key: null, value: undefined };
1043
1325
  }
1326
+ /**
1327
+ * Whether a record has a key with a value other than null: a null endpoint is an absent one.
1328
+ * @param record - the record
1329
+ * @param key - the key
1330
+ * @returns true when the key holds a non-null value
1331
+ */
1332
+ function present(record, key) {
1333
+ return hasKey(record, key) && record[key] !== null;
1334
+ }
1335
+ /**
1336
+ * Report every key of a record the dialect does not read (W_JSON_UNREAD_KEY): it is dropped.
1337
+ * @param ctx - the context
1338
+ * @param record - the document or graph record
1339
+ * @param known - the keys the dialect reads
1340
+ * @param prefix - the record's path for the messages (`""` for the document, `"graph."`)
1341
+ */
1342
+ function reportUnreadKeys(ctx, record, known, prefix) {
1343
+ for (const key of Object.keys(record)) {
1344
+ if (known.has(key)) {
1345
+ continue;
1346
+ }
1347
+ const value = record[key];
1348
+ const entries = Array.isArray(value) && value.length === 1 ? "entry" : "entries";
1349
+ const what = Array.isArray(value) ? `${String(value.length)} ${entries}` : describe(value);
1350
+ ctx.report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `${prefix.length === 0 ? "top-level key" : "key"} ${prefix}${key} (${what}) is not read; dropped`, { element: `${prefix}${key}` });
1351
+ }
1352
+ }
1353
+ /** The keys of a vis.js document the reader reads. */
1354
+ const VIS_DOCUMENT_KEYS = new Set(["nodes", "edges"]);
1355
+ /** The keys of a graphology serialization the reader reads. */
1356
+ const GRAPHOLOGY_DOCUMENT_KEYS = new Set(["nodes", "edges", "options", "attributes"]);
1357
+ /** The keys of a JGF graph object the reader reads. */
1358
+ const JGF_GRAPH_KEYS = new Set([
1359
+ "id",
1360
+ "type",
1361
+ "label",
1362
+ "directed",
1363
+ "metadata",
1364
+ "nodes",
1365
+ "edges",
1366
+ "hyperedges",
1367
+ ]);
1044
1368
  /**
1045
1369
  * Whether any object of an array has a key with a non-null value.
1046
1370
  * @param items - the array
@@ -1075,26 +1399,17 @@ function importNodeLink(ctx, root, dialect) {
1075
1399
  const nodes = arraySection(root.nodes, "nodes", report);
1076
1400
  const edges = arraySection(root[edgesKey], edgesKey, report);
1077
1401
  if (nodes === null) {
1078
- report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no nodes array; nodes come from the edges", { element: "nodes" });
1402
+ report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no nodes array; nodes come from the edges", { element: "nodes" });
1403
+ ctx.nodesFromEdges = true;
1079
1404
  }
1080
1405
  if (edges === null) {
1081
- report.error("missing-value", JSON_ISSUE.MISSING_SECTION, `the document has no ${edgesKey} array; the graph has no edges`, { element: edgesKey });
1406
+ report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, `the document has no ${edgesKey} array; the graph has no edges`, { element: edgesKey });
1082
1407
  }
1083
1408
  const directed = flagOf(root.directed, "directed", ctx.defaultDirected(dialect), report);
1084
1409
  const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
1085
1410
  ctx.setHeader(directed);
1086
1411
  ctx.writeGraphDict(root.graph, "graph");
1087
- for (const key of Object.keys(root)) {
1088
- if (key !== "nodes" && key !== edgesKey && key !== "directed" && key !== "multigraph" && key !== "graph") {
1089
- const value = root[key];
1090
- const what = Array.isArray(value)
1091
- ? `${String(value.length)} ${value.length === 1 ? "entry" : "entries"}`
1092
- : describe(value);
1093
- report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${what}) is not read; dropped`, {
1094
- element: key,
1095
- });
1096
- }
1097
- }
1412
+ reportUnreadKeys(ctx, root, new Set(["nodes", edgesKey, "directed", "multigraph", "graph"]), "");
1098
1413
  const nodeList = nodes ?? [];
1099
1414
  const edgeList = edges ?? [];
1100
1415
  ctx.sink.reserve(nodeList.length, edgeList.length);
@@ -1105,8 +1420,10 @@ function importNodeLink(ctx, root, dialect) {
1105
1420
  if (!positional && nodeIdKey === null) {
1106
1421
  const candidates = ctx.options.nodeIdFrom === "label" ? ["label", "name", "id"] : ["id", "name"];
1107
1422
  nodeIdKey = candidates.find((key) => anyHas(nodeList, key)) ?? null;
1108
- if (ctx.options.nodeIdFrom === "label" && nodeIdKey === "id") {
1109
- report.warning("unsupported", JSON_ISSUE.OPTION_IGNORED, 'nodeIdFrom "label": no node has a label or name key; ids are read from "id"', { element: "nodeIdFrom" });
1423
+ if (ctx.options.nodeIdFrom === "label" && (nodeIdKey === "id" || nodeIdKey === "name")) {
1424
+ report.warning("unsupported", JSON_ISSUE.OPTION_IGNORED, nodeIdKey === "id"
1425
+ ? 'nodeIdFrom "label": no node has a label or name key; ids are read from "id"'
1426
+ : 'nodeIdFrom "label": no node has a "label" key; ids are read from "name"', { element: "nodeIdFrom" });
1110
1427
  }
1111
1428
  if (nodeIdKey === null) {
1112
1429
  if (nodeList.length > 0) {
@@ -1132,6 +1449,9 @@ function importNodeLink(ctx, root, dialect) {
1132
1449
  ({ indexLinks } = json);
1133
1450
  }
1134
1451
  const positionIds = indexLinks ? [] : null;
1452
+ if (indexLinks && !positional && json.indexLinks === "auto" && nodeIdKey !== null) {
1453
+ reportAmbiguousIndexLinks(ctx, nodeList, edgeList, nodeIdKey);
1454
+ }
1135
1455
  for (let i = 0; i < nodeList.length; i++) {
1136
1456
  const element = `nodes[${i}]`;
1137
1457
  const record = nodeList[i];
@@ -1154,14 +1474,14 @@ function importNodeLink(ctx, root, dialect) {
1154
1474
  const index = ctx.pushNode(id, element);
1155
1475
  if (index >= 0) {
1156
1476
  pushed = id;
1157
- writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
1477
+ writeFlat(ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
1158
1478
  }
1159
1479
  }
1160
1480
  }
1161
1481
  positionIds?.push(pushed);
1162
1482
  }
1163
1483
  throwIfAborted(ctx.options.signal);
1164
- const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds);
1484
+ const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds, multigraph);
1165
1485
  ctx.setMeta({
1166
1486
  dialect,
1167
1487
  edgesKey,
@@ -1178,13 +1498,19 @@ function importNodeLink(ctx, root, dialect) {
1178
1498
  * @param edgeList - the edge records
1179
1499
  * @param edgesKey - the top-level key they came from, for issues
1180
1500
  * @param positionIds - the ids by array position under index links, or null
1501
+ * @param multigraph - the declared multigraph flag, or null when the document has none
1181
1502
  * @returns the source and target keys the first well-formed edge used (null when none did)
1182
1503
  */
1183
- function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
1504
+ function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds, multigraph) {
1184
1505
  const { json } = ctx;
1185
1506
  const kind = ctx.uniformKind();
1186
1507
  let sourceKey = null;
1187
1508
  let targetKey = null;
1509
+ // multigraph false: the pairs seen, to report parallel links; true: the (pair, key) triples, since
1510
+ // NetworkX reads a repeated (u, v, key) as one edge
1511
+ // ponytail: one string per edge whenever the flag is declared; a hash of the index pair if it shows in profiles
1512
+ const pairs = multigraph === null ? null : new Set();
1513
+ let parallels = 0;
1188
1514
  for (let i = 0; i < edgeList.length; i++) {
1189
1515
  const element = `${edgesKey}[${i}]`;
1190
1516
  const record = edgeList[i];
@@ -1208,11 +1534,25 @@ function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
1208
1534
  continue;
1209
1535
  }
1210
1536
  const weight = ctx.weightOf(record);
1537
+ if (pairs !== null) {
1538
+ const pair = pairKey(u, v, kind === "directed");
1539
+ if (multigraph === false) {
1540
+ parallels += pairs.has(pair) ? 1 : 0;
1541
+ pairs.add(pair);
1542
+ }
1543
+ else if (hasKey(record, "key")) {
1544
+ const triple = `${pair} ${JSON.stringify(record.key)}`;
1545
+ if (pairs.has(triple)) {
1546
+ throw new GraphFormatError(JSON_ISSUE.DUPLICATE_EDGE_ID, `${element}: key ${JSON.stringify(record.key)} is repeated for the same pair; NetworkX reads it as the same edge, so it is skipped`, { id: String(record.key) });
1547
+ }
1548
+ pairs.add(triple);
1549
+ }
1550
+ }
1211
1551
  const edge = ctx.pushEdge(u, v, kind, weight, element);
1212
1552
  const { weightFrom } = ctx.options;
1213
1553
  for (const key of Object.keys(record)) {
1214
1554
  if (key !== source.key && key !== target.key && key !== weightFrom) {
1215
- ctx.edges.write(edge, key, record[key], SUFFIX.data);
1555
+ ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
1216
1556
  }
1217
1557
  }
1218
1558
  }
@@ -1220,32 +1560,71 @@ function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
1220
1560
  ctx.skip(err, "edge", element);
1221
1561
  }
1222
1562
  }
1563
+ if (parallels > 0) {
1564
+ ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `the document declares multigraph false, but ${parallels} link${plural(parallels)} ${agree(parallels, "repeats", "repeat")} the endpoints of an earlier one; all are kept`, { element: edgesKey });
1565
+ }
1223
1566
  return { source: sourceKey, target: targetKey };
1224
1567
  }
1225
1568
  /**
1226
- * Write the flat attributes of a node record (every own key the filter keeps).
1569
+ * The key of an endpoint pair: ordered for a directed edge, unordered for an undirected one.
1570
+ * @param u - the source
1571
+ * @param v - the target
1572
+ * @param directed - whether the order matters
1573
+ * @returns the key
1574
+ */
1575
+ function pairKey(u, v, directed) {
1576
+ const a = JSON.stringify(u);
1577
+ const b = JSON.stringify(v);
1578
+ return directed || a <= b ? `${a} ${b}` : `${b} ${a}`;
1579
+ }
1580
+ /**
1581
+ * Warn when indexLinks "auto" read integer endpoints as array positions although some also equal
1582
+ * a node id's text (`"2"`): the file may mean the ids, and the edges are then off by the positions.
1227
1583
  * @param ctx - the context
1584
+ * @param nodes - the node records
1585
+ * @param edges - the edge records
1586
+ * @param nodeIdKey - the node id key
1587
+ */
1588
+ function reportAmbiguousIndexLinks(ctx, nodes, edges, nodeIdKey) {
1589
+ const ids = new Set();
1590
+ for (const node of nodes) {
1591
+ if (isJsonObject(node) && typeof node[nodeIdKey] === "string") {
1592
+ ids.add(node[nodeIdKey]);
1593
+ }
1594
+ }
1595
+ for (const edge of edges) {
1596
+ if (!isJsonObject(edge)) {
1597
+ continue;
1598
+ }
1599
+ for (const value of [
1600
+ endpointOf(edge, ctx.json.sourceKey, NODE_LINK_SOURCE_KEYS).value,
1601
+ endpointOf(edge, ctx.json.targetKey, NODE_LINK_TARGET_KEYS).value,
1602
+ ]) {
1603
+ if (typeof value === "number" && ids.has(String(value))) {
1604
+ ctx.report.warning("coercion", JSON_ISSUE.INDEX_LINKS, `integer endpoints are read as array positions, but ${value} is also a node id; pass indexLinks: false to read the endpoints as ids`, { element: "indexLinks" });
1605
+ return;
1606
+ }
1607
+ }
1608
+ }
1609
+ }
1610
+ /**
1611
+ * Write the flat attributes of a node record (every own key the filter keeps).
1228
1612
  * @param writer - the node writer
1229
1613
  * @param index - the node index
1230
1614
  * @param record - the record
1231
1615
  * @param id - the node id, for issues
1232
1616
  * @param keep - which keys are attributes
1233
1617
  */
1234
- function writeFlat(ctx, writer, index, record, id, keep) {
1235
- try {
1236
- for (const key of Object.keys(record)) {
1237
- if (keep(key)) {
1238
- writer.write(index, key, record[key], SUFFIX.data);
1239
- }
1618
+ function writeFlat(writer, index, record, id, keep) {
1619
+ for (const key of Object.keys(record)) {
1620
+ if (keep(key)) {
1621
+ writer.write(index, key, record[key], SUFFIX.data, String(id));
1240
1622
  }
1241
1623
  }
1242
- catch (err) {
1243
- ctx.report.recordError(err, { element: String(id) });
1244
- }
1245
1624
  }
1246
1625
  /**
1247
- * The d3 index-link heuristic: endpoints are array positions when every endpoint is a
1248
- * non-negative integer and no node id is a number (a numeric id would make the endpoints ids;
1626
+ * The d3 index-link heuristic: endpoints are array positions when every endpoint is a number, at
1627
+ * least one is a non-negative integer, and no node id is a number (a numeric id would make the endpoints ids;
1249
1628
  * research note 07: d3 links reference nodes by array index and are never coerced to ids). An
1250
1629
  * index at or beyond the node count is then E_BAD_INDEX, never a new numeric node.
1251
1630
  * @param nodes - the node records
@@ -1270,10 +1649,14 @@ function looksIndexLinked(nodes, edges, nodeIdKey, json) {
1270
1649
  }
1271
1650
  const s = endpointOf(edge, json.sourceKey, NODE_LINK_SOURCE_KEYS).value;
1272
1651
  const t = endpointOf(edge, json.targetKey, NODE_LINK_TARGET_KEYS).value;
1273
- if (!isIndexBelow(s, Infinity) || !isIndexBelow(t, Infinity)) {
1652
+ // a fractional or negative number is a bad index (E_BAD_INDEX), never a reason to read every
1653
+ // other endpoint as an id; a missing endpoint is reported on its own
1654
+ if ((s !== undefined && typeof s !== "number") || (t !== undefined && typeof t !== "number")) {
1274
1655
  return false;
1275
1656
  }
1276
- seen++;
1657
+ if (isIndexBelow(s, Infinity) || isIndexBelow(t, Infinity)) {
1658
+ seen++;
1659
+ }
1277
1660
  }
1278
1661
  return seen > 0;
1279
1662
  }
@@ -1313,6 +1696,8 @@ function importVis(ctx, root) {
1313
1696
  const { report, json } = ctx;
1314
1697
  const nodes = arraySection(root.nodes, "nodes", report) ?? [];
1315
1698
  const edges = arraySection(root.edges, "edges", report) ?? [];
1699
+ // vis.js options and groups are rendering settings, not graph data
1700
+ reportUnreadKeys(ctx, root, VIS_DOCUMENT_KEYS, "");
1316
1701
  ctx.setHeader(ctx.defaultDirected("vis"));
1317
1702
  ctx.sink.reserve(nodes.length, edges.length);
1318
1703
  const nodeIdKey = json.nodeIdKey ?? "id";
@@ -1331,7 +1716,7 @@ function importVis(ctx, root) {
1331
1716
  }
1332
1717
  const index = ctx.pushNode(id, element);
1333
1718
  if (index >= 0) {
1334
- writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
1719
+ writeFlat(ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
1335
1720
  }
1336
1721
  }
1337
1722
  throwIfAborted(ctx.options.signal);
@@ -1367,7 +1752,7 @@ function importVis(ctx, root) {
1367
1752
  const { weightFrom } = ctx.options;
1368
1753
  for (const key of Object.keys(record)) {
1369
1754
  if (key !== source.key && key !== target.key && key !== "id" && key !== weightFrom) {
1370
- ctx.edges.write(edge, key, record[key], SUFFIX.data);
1755
+ ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
1371
1756
  }
1372
1757
  }
1373
1758
  }
@@ -1375,12 +1760,21 @@ function importVis(ctx, root) {
1375
1760
  ctx.skip(err, "edge", element);
1376
1761
  }
1377
1762
  }
1763
+ // vis.js draws arrows from the `arrows` edge option; the file declares no graph direction, so the
1764
+ // arrows stay an attribute and the graph is read as the dialect's default
1765
+ const arrows = kind === "undirected"
1766
+ ? edges.filter((e) => isJsonObject(e) && e.arrows !== undefined && e.arrows !== null && e.arrows !== "")
1767
+ .length
1768
+ : 0;
1769
+ if (arrows > 0) {
1770
+ report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${arrows} edge${plural(arrows)} ${agree(arrows, "carries", "carry")} vis.js arrows but the graph is read undirected; the arrows are kept as an attribute (pass defaultDirected: true to read the edges as directed)`, { element: "arrows" });
1771
+ }
1378
1772
  ctx.setMeta({ dialect: "vis", nodeIdKey, sourceKey: sourceKey ?? undefined, targetKey: targetKey ?? undefined }, ctx.weightOriginPatch());
1379
1773
  }
1380
1774
  // ============================================================ NetworkX adjacency_data / tree_data
1381
1775
  /**
1382
1776
  * Read a NetworkX adjacency_data document: `nodes` as in node-link, and `adjacency[i]` the
1383
- * neighbour list of `nodes[i]`, one `{ id, key?, ...attributes }` entry per edge. An undirected
1777
+ * neighbor list of `nodes[i]`, one `{ id, key?, ...attributes }` entry per edge. An undirected
1384
1778
  * file lists every edge from both ends, so an entry whose mirror (the same pair and `key`) was
1385
1779
  * already read is that edge again and is not pushed twice; a self-loop is listed once.
1386
1780
  * @param ctx - the context
@@ -1391,7 +1785,7 @@ function importAdjacency(ctx, root) {
1391
1785
  const nodes = arraySection(root.nodes, "nodes", report) ?? [];
1392
1786
  const adjacency = arraySection(root.adjacency, "adjacency", report);
1393
1787
  if (adjacency === null) {
1394
- report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no adjacency array; the graph has no edges", {
1788
+ report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no adjacency array; the graph has no edges", {
1395
1789
  element: "adjacency",
1396
1790
  });
1397
1791
  }
@@ -1428,7 +1822,7 @@ function importAdjacency(ctx, root) {
1428
1822
  const index = ctx.pushNode(id, element);
1429
1823
  if (index >= 0) {
1430
1824
  pushed = id;
1431
- writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== idKey);
1825
+ writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
1432
1826
  }
1433
1827
  }
1434
1828
  }
@@ -1437,8 +1831,9 @@ function importAdjacency(ctx, root) {
1437
1831
  throwIfAborted(ctx.options.signal);
1438
1832
  const kind = ctx.uniformKind();
1439
1833
  const { weightFrom } = ctx.options;
1440
- // undirected: entries read once whose mirror is still to come, by pair and key
1834
+ // undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
1441
1835
  const pending = new Map();
1836
+ let disagreeing = 0;
1442
1837
  for (let i = 0; i < lists.length; i++) {
1443
1838
  const element = `adjacency[${i}]`;
1444
1839
  const list = lists[i];
@@ -1449,7 +1844,7 @@ function importAdjacency(ctx, root) {
1449
1844
  const source = i < owners.length ? owners[i] : null;
1450
1845
  if (source === null) {
1451
1846
  const why = i < nodes.length ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
1452
- report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge(s) are skipped`, {
1847
+ report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`, {
1453
1848
  element,
1454
1849
  });
1455
1850
  report.counts.skippedEdges += list.length;
@@ -1472,13 +1867,17 @@ function importAdjacency(ctx, root) {
1472
1867
  ctx.countSkipped("edge");
1473
1868
  continue;
1474
1869
  }
1475
- if (!directed && isMirroredEntry(pending, source, target, record.key)) {
1476
- continue;
1870
+ if (!directed) {
1871
+ const mirror = mirroredEntry(pending, source, target, record, idKey);
1872
+ disagreeing += mirror === "disagrees" ? 1 : 0;
1873
+ if (mirror !== "new") {
1874
+ continue;
1875
+ }
1477
1876
  }
1478
1877
  const edge = ctx.pushEdge(source, target, kind, ctx.weightOf(record), entry);
1479
1878
  for (const key of Object.keys(record)) {
1480
1879
  if (key !== idKey && key !== weightFrom) {
1481
- ctx.edges.write(edge, key, record[key], SUFFIX.data);
1880
+ ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
1482
1881
  }
1483
1882
  }
1484
1883
  }
@@ -1487,35 +1886,80 @@ function importAdjacency(ctx, root) {
1487
1886
  }
1488
1887
  }
1489
1888
  }
1889
+ reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, disagreeing, pending);
1490
1890
  ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
1491
1891
  }
1492
1892
  /**
1493
- * Whether an undirected adjacency entry is the mirror of one already read (the same unordered
1494
- * pair and key), consuming it; otherwise the entry is remembered as awaiting its mirror. A
1495
- * self-loop is listed once and never awaits one.
1496
- * @param pending - the entries awaiting their mirror, by owner, other end and key
1893
+ * Report what an adjacency_data document leaves inconsistent: nodes without an adjacency list
1894
+ * (NetworkX writes one per node, so the file was cut), undirected edges whose two listings carry
1895
+ * different attributes (the first is kept), and undirected entries whose mirror never came.
1896
+ * @param ctx - the context
1897
+ * @param nodes - the number of node records
1898
+ * @param lists - the number of adjacency lists, or null when the section is missing (reported already)
1899
+ * @param disagreeing - the mirrors whose attributes differ
1900
+ * @param pending - the entries still awaiting their mirror
1901
+ */
1902
+ function reportAdjacencyShape(ctx, nodes, lists, disagreeing, pending) {
1903
+ const { report } = ctx;
1904
+ if (lists !== null && lists < nodes) {
1905
+ report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, `${nodes - lists} node${plural(nodes - lists)} from nodes[${lists}] on ${agree(nodes - lists, "has", "have")} no adjacency list (NetworkX writes one per node); their edges listed elsewhere are kept`, { element: "adjacency" });
1906
+ }
1907
+ if (disagreeing > 0) {
1908
+ report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `the two listings of ${disagreeing} undirected edge${plural(disagreeing)} ${agree(disagreeing, "disagrees", "disagree")} on their attributes; the first listing is kept`, { element: "adjacency" });
1909
+ }
1910
+ let unmatched = 0;
1911
+ for (const waiting of pending.values()) {
1912
+ unmatched += waiting.length;
1913
+ }
1914
+ if (unmatched > 0) {
1915
+ report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${unmatched} undirected adjacency entr(ies) have no mirror in the other node's list; each is read as one edge`, { element: "adjacency" });
1916
+ }
1917
+ }
1918
+ /**
1919
+ * A JSON.stringify replacer that writes every object's keys in sorted order.
1920
+ * @param _key - the key
1921
+ * @param value - the value
1922
+ * @returns the value, an object with its keys sorted
1923
+ */
1924
+ function sortedKeys(_key, value) {
1925
+ return isJsonObject(value) ? Object.fromEntries(Object.entries(value).sort(([x], [y]) => (x < y ? -1 : 1))) : value;
1926
+ }
1927
+ /**
1928
+ * Whether an undirected adjacency entry is the mirror of one already read (the same unordered pair
1929
+ * and key), consuming it; otherwise the entry is remembered as awaiting its mirror. A self-loop is
1930
+ * listed once and never awaits one.
1931
+ * @param pending - the entries awaiting their mirror, by owner, other end and key: their attributes
1497
1932
  * @param source - the owner of the list
1498
1933
  * @param target - the entry's node
1499
- * @param key - the entry's multigraph key, or undefined
1500
- * @returns true when the entry is a mirror and must not be pushed
1934
+ * @param record - the entry
1935
+ * @param idKey - the entry's id key (not an attribute)
1936
+ * @returns "new" for an entry to push, "mirror" for the second listing of one read, "disagrees"
1937
+ * for a second listing with other attributes
1501
1938
  */
1502
- function isMirroredEntry(pending, source, target, key) {
1939
+ function mirroredEntry(pending, source, target, record, idKey) {
1503
1940
  const a = JSON.stringify(source);
1504
1941
  const b = JSON.stringify(target);
1505
1942
  if (a === b) {
1506
- return false;
1943
+ return "new";
1507
1944
  }
1508
- const k = JSON.stringify(key ?? null);
1945
+ const k = JSON.stringify(record.key ?? null);
1946
+ // key order is not part of a JSON object, so two listings in another order still agree
1947
+ const attributes = JSON.stringify(Object.fromEntries(Object.entries(record).filter(([key]) => key !== idKey)), sortedKeys);
1509
1948
  // an entry is the mirror of one listed by the other end, never of a parallel entry of its own list
1510
- const mirror = `${b} ${a} ${k}`;
1511
- const waiting = pending.get(mirror) ?? 0;
1512
- if (waiting > 0) {
1513
- pending.set(mirror, waiting - 1);
1514
- return true;
1949
+ const waiting = pending.get(`${b} ${a} ${k}`);
1950
+ if (waiting !== undefined && waiting.length > 0) {
1951
+ const first = waiting.shift();
1952
+ return first === attributes ? "mirror" : "disagrees";
1515
1953
  }
1516
1954
  const own = `${a} ${b} ${k}`;
1517
- pending.set(own, (pending.get(own) ?? 0) + 1);
1518
- return false;
1955
+ const list = pending.get(own);
1956
+ if (list === undefined) {
1957
+ pending.set(own, [attributes]);
1958
+ }
1959
+ else {
1960
+ list.push(attributes);
1961
+ }
1962
+ return "new";
1519
1963
  }
1520
1964
  /**
1521
1965
  * Whether a value is a list of [string, value] pairs, the shape adjacency_data gives the graph dict.
@@ -1555,7 +1999,7 @@ function importTree(ctx, root) {
1555
1999
  id = null;
1556
2000
  }
1557
2001
  else {
1558
- writeFlat(ctx, ctx.nodes, ctx.sink.indexOf(id), record, id, (key) => key !== idKey && key !== "children");
2002
+ writeFlat(ctx.nodes, ctx.sink.indexOf(id), record, id, (key) => key !== idKey && key !== "children");
1559
2003
  if (parent !== null) {
1560
2004
  try {
1561
2005
  ctx.pushEdge(parent, id, kind, undefined, element);
@@ -1579,7 +2023,7 @@ function importTree(ctx, root) {
1579
2023
  }
1580
2024
  // ============================================================ graphology
1581
2025
  /**
1582
- * Read a graphology serialisation: `options.type` decides the header direction ("mixed" or absent:
2026
+ * Read a graphology serialization: `options.type` decides the header direction ("mixed" or absent:
1583
2027
  * from the edges' `undirected` flags), `options.multi` the declared multigraph flag, node `key`
1584
2028
  * the id, `attributes` the columns, edge `key` the edge id.
1585
2029
  * @param ctx - the context
@@ -1589,6 +2033,7 @@ function importGraphology(ctx, root) {
1589
2033
  const { report } = ctx;
1590
2034
  const nodes = arraySection(root.nodes, "nodes", report) ?? [];
1591
2035
  const edges = arraySection(root.edges, "edges", report) ?? [];
2036
+ reportUnreadKeys(ctx, root, GRAPHOLOGY_DOCUMENT_KEYS, "");
1592
2037
  const options = isJsonObject(root.options) ? root.options : {};
1593
2038
  if (hasKey(root, "options") && !isJsonObject(root.options)) {
1594
2039
  report.warning("validation-error", JSON_ISSUE.BAD_FLAG, "options is not an object; ignored", {
@@ -1638,6 +2083,9 @@ function importGraphology(ctx, root) {
1638
2083
  }
1639
2084
  throwIfAborted(ctx.options.signal);
1640
2085
  const ids = ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true);
2086
+ // what the edges do against the declared options: graphology itself refuses such a document
2087
+ const violations = { selfLoops: 0, parallels: 0, flags: 0 };
2088
+ const pairs = multi === false ? new Set() : null;
1641
2089
  for (let i = 0; i < edges.length; i++) {
1642
2090
  const element = `edges[${i}]`;
1643
2091
  const record = edges[i];
@@ -1645,8 +2093,8 @@ function importGraphology(ctx, root) {
1645
2093
  ctx.badElement("edge", element);
1646
2094
  continue;
1647
2095
  }
1648
- if (!hasKey(record, "source") || !hasKey(record, "target")) {
1649
- ctx.missingEndpoint(element, hasKey(record, "source") ? "target" : "source");
2096
+ if (!present(record, "source") || !present(record, "target")) {
2097
+ ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
1650
2098
  continue;
1651
2099
  }
1652
2100
  try {
@@ -1662,19 +2110,62 @@ function importGraphology(ctx, root) {
1662
2110
  }
1663
2111
  else {
1664
2112
  kind = type;
2113
+ if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
2114
+ violations.flags++;
2115
+ }
2116
+ }
2117
+ const { attributes: rawAttributes } = record;
2118
+ if (rawAttributes !== undefined && rawAttributes !== null && !isJsonObject(rawAttributes)) {
2119
+ report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: attributes must be an object, found ${describe(rawAttributes)}`, { element });
1665
2120
  }
1666
- const attributes = isJsonObject(record.attributes) ? record.attributes : {};
2121
+ const attributes = isJsonObject(rawAttributes) ? rawAttributes : {};
1667
2122
  const idValue = ctx.edgeIdValue(ids, record.key);
1668
2123
  const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
1669
2124
  ctx.setEdgeId(ids, edge, idValue);
1670
- ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom);
2125
+ ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
2126
+ if (allowSelfLoops === false && u === v) {
2127
+ violations.selfLoops++;
2128
+ }
2129
+ if (pairs !== null) {
2130
+ const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
2131
+ violations.parallels += pairs.has(pair) ? 1 : 0;
2132
+ pairs.add(pair);
2133
+ }
1671
2134
  }
1672
2135
  catch (err) {
1673
2136
  ctx.skip(err, "edge", element);
1674
2137
  }
1675
2138
  }
2139
+ reportGraphologyViolations(ctx, violations, type);
1676
2140
  ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
1677
2141
  }
2142
+ /**
2143
+ * Report the edges that contradict a graphology document's declared options (W_JSON_INCONSISTENT,
2144
+ * one warning per kind); they are kept.
2145
+ * @param ctx - the context
2146
+ * @param violations - the counts
2147
+ * @param violations.selfLoops - self-loops under allowSelfLoops false
2148
+ * @param violations.parallels - parallel edges under multi false
2149
+ * @param violations.flags - per-edge undirected flags against options.type
2150
+ * @param type - options.type
2151
+ */
2152
+ function reportGraphologyViolations(ctx, violations, type) {
2153
+ const notes = [];
2154
+ if (violations.selfLoops > 0) {
2155
+ notes.push(`options.allowSelfLoops is false, but ${violations.selfLoops} self-loop${plural(violations.selfLoops)} ${agree(violations.selfLoops, "is", "are")} listed`);
2156
+ }
2157
+ if (violations.parallels > 0) {
2158
+ notes.push(`options.multi is false, but ${violations.parallels} parallel edge${plural(violations.parallels)} ${agree(violations.parallels, "is", "are")} listed`);
2159
+ }
2160
+ if (violations.flags > 0) {
2161
+ notes.push(`options.type is ${type}, but ${violations.flags} edge${plural(violations.flags)} ${agree(violations.flags, "carries", "carry")} the other undirected flag; read as ${type}`);
2162
+ }
2163
+ for (const note of notes) {
2164
+ ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${note}; every edge is kept`, {
2165
+ element: "options",
2166
+ });
2167
+ }
2168
+ }
1678
2169
  /**
1679
2170
  * Push a node whose attributes live in a nested dict (graphology `attributes`, JGF `metadata`);
1680
2171
  * element-level keys the dialect does not define are kept with the `#element` suffix.
@@ -1696,7 +2187,7 @@ function pushNestedNode(ctx, id, record, dictKey, structural, element) {
1696
2187
  if (dict !== undefined && dict !== null && !isJsonObject(dict)) {
1697
2188
  ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: ${dictKey} must be an object, found ${describe(dict)}`, { element });
1698
2189
  }
1699
- ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null);
2190
+ ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null, element);
1700
2191
  }
1701
2192
  catch (err) {
1702
2193
  ctx.report.recordError(err, { element: String(id) });
@@ -1760,6 +2251,9 @@ function importJgf(ctx, root) {
1760
2251
  ctx.countSkipped("node");
1761
2252
  continue;
1762
2253
  }
2254
+ if (record !== null && hasKey(record, "id") && record.id !== key) {
2255
+ report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element}: the inner id ${describe(record.id)} differs from the key; the key is the id, the inner one is kept as id${SUFFIX.element}`, { element });
2256
+ }
1763
2257
  pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
1764
2258
  }
1765
2259
  }
@@ -1796,7 +2290,7 @@ function importJgf(ctx, root) {
1796
2290
  element,
1797
2291
  });
1798
2292
  }
1799
- ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom);
2293
+ ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom, element);
1800
2294
  };
1801
2295
  for (let i = 0; i < edges.length; i++) {
1802
2296
  const element = `edges[${i}]`;
@@ -1805,8 +2299,8 @@ function importJgf(ctx, root) {
1805
2299
  ctx.badElement("edge", element);
1806
2300
  continue;
1807
2301
  }
1808
- if (!hasKey(record, "source") || !hasKey(record, "target")) {
1809
- ctx.missingEndpoint(element, hasKey(record, "source") ? "target" : "source");
2302
+ if (!present(record, "source") || !present(record, "target")) {
2303
+ ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
1810
2304
  continue;
1811
2305
  }
1812
2306
  try {
@@ -1834,7 +2328,18 @@ function importJgf(ctx, root) {
1834
2328
  * @returns the graph object; the import fails when there is none
1835
2329
  */
1836
2330
  function jgfGraphOf(ctx, root) {
1837
- return isJsonObject(root.graph) ? root.graph : chosenGraph(ctx, root, "JGF");
2331
+ const single = isJsonObject(root.graph);
2332
+ // a document with both graph and graphs is read by its graph; the graphs are not
2333
+ reportUnreadKeys(ctx, root, new Set([single ? "graph" : "graphs"]), "");
2334
+ const graph = single ? root.graph : chosenGraph(ctx, root, "JGF");
2335
+ const prefix = single ? "graph." : `graphs[${root.graphs.indexOf(graph)}].`;
2336
+ reportUnreadKeys(ctx, graph, JGF_GRAPH_KEYS, prefix);
2337
+ for (const key of ["id", "type", "label"]) {
2338
+ if (graph[key] !== undefined && graph[key] !== null && typeof graph[key] !== "string") {
2339
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${prefix}${key} must be a string, found ${describe(graph[key])}; ignored`, { element: `${prefix}${key}` });
2340
+ }
2341
+ }
2342
+ return graph;
1838
2343
  }
1839
2344
  /**
1840
2345
  * The graph of a `graphs` array (JGF, OBO Graphs) that graphIndex / graphName choose, with
@@ -1843,6 +2348,7 @@ function jgfGraphOf(ctx, root) {
1843
2348
  * @param root - the document
1844
2349
  * @param what - the dialect's name, for the messages
1845
2350
  * @returns the graph object; the import fails when there is none
2351
+ * @category Plugin helpers
1846
2352
  */
1847
2353
  export function chosenGraph(ctx, root, what) {
1848
2354
  const { report } = ctx;
@@ -1851,12 +2357,19 @@ export function chosenGraph(ctx, root, what) {
1851
2357
  report.fail(JSON_ISSUE.SHAPE, `a ${what} document needs a graph object or a non-empty graphs array`);
1852
2358
  }
1853
2359
  const index = ctx.json.all === true ? ctx.json.graphIndex : chooseGraph(graphs.map(graphNameOf), ctx.json.choice, report);
1854
- if (graphs.length > 1 && ctx.json.all !== true) {
1855
- report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped), importAll() reads every one`, { element: "graphs" });
2360
+ if (graphs.length > 1 && ctx.json.all !== true && !graphChosen(ctx.json.choice)) {
2361
+ report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped); importAllGraphs() reads every one`, { element: "graphs" });
1856
2362
  }
1857
2363
  const graph = graphs[index];
1858
2364
  if (!isJsonObject(graph)) {
1859
- return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
2365
+ if (ctx.json.all !== true) {
2366
+ return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
2367
+ }
2368
+ // importAll(): one bad graph is that graph's error, read as empty; the others are still read
2369
+ report.error("validation-error", JSON_ISSUE.SHAPE, `graphs[${index}] is not an object; it is read as empty`, {
2370
+ element: `graphs[${index}]`,
2371
+ });
2372
+ return {};
1860
2373
  }
1861
2374
  return graph;
1862
2375
  }
@@ -1916,33 +2429,28 @@ function listingsOf(root, dialect) {
1916
2429
  }
1917
2430
  return [{ index: 0, name: null, nodes: null, edges: null }];
1918
2431
  }
1919
- /** The longest string V8 makes (2^29 - 24 UTF-16 code units); a longer document cannot be one JSON.parse input. */
1920
- const MAX_TEXT_LENGTH = 2 ** 29 - 24;
1921
2432
  /**
1922
- * Read the whole input as one string, failing with E_TOO_LARGE (category unsupported) before the
1923
- * join when it is longer than one JavaScript string can hold (OBO Graphs files such as
1924
- * ncbitaxon.json are; design 7.1 defers streaming the JSON reader).
1925
- * @param input - the input
1926
- * @param report - the report
1927
- * @param options - cancellation, progress and encoding
1928
- * @returns the text
2433
+ * Whether a text that is not one JSON document is JSON Lines / NDJSON: its first two non-blank
2434
+ * lines are each a JSON object or array.
2435
+ * @param text - the document
2436
+ * @returns true for JSON Lines
1929
2437
  */
1930
- async function readJsonText(input, report, options) {
1931
- if (typeof input === "string") {
1932
- return readText(input, report, options);
1933
- }
1934
- const parts = [];
1935
- let length = 0;
1936
- for await (const chunk of textChunks(input, report, options)) {
1937
- length += chunk.length;
1938
- if (length > MAX_TEXT_LENGTH) {
1939
- const message = `the document is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`;
1940
- report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
1941
- throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
1942
- }
1943
- parts.push(chunk);
1944
- }
1945
- return parts.length === 1 ? parts[0] : parts.join("");
2438
+ function looksLikeJsonLines(text) {
2439
+ const lines = text
2440
+ .slice(0, 64 * 1024)
2441
+ .split(/\r?\n/)
2442
+ .filter((line) => line.trim().length > 0)
2443
+ .slice(0, 2);
2444
+ return (lines.length === 2 &&
2445
+ lines.every((line) => {
2446
+ try {
2447
+ const value = JSON.parse(line);
2448
+ return typeof value === "object" && value !== null;
2449
+ }
2450
+ catch {
2451
+ return false;
2452
+ }
2453
+ }));
1946
2454
  }
1947
2455
  /**
1948
2456
  * Push a JGF node: `label` into the declared column, `metadata` as attributes.
@@ -1965,7 +2473,7 @@ function pushJgfNode(ctx, id, record, labelColumn, element) {
1965
2473
  element,
1966
2474
  });
1967
2475
  }
1968
- ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null);
2476
+ ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null, element);
1969
2477
  }
1970
2478
  catch (err) {
1971
2479
  ctx.report.recordError(err, { element: String(id) });
@@ -1989,18 +2497,33 @@ function importHyperedges(ctx, hyperedges, directed, push) {
1989
2497
  const { report } = ctx;
1990
2498
  const policy = ctx.options.hyperedges;
1991
2499
  if (policy === "error") {
1992
- report.error("unsupported", JSON_ISSUE.HYPEREDGE, `${hyperedges.length} hyperedge(s) (hyperedges: "error")`, {
2500
+ report.error("unsupported", JSON_ISSUE.HYPEREDGE, `${hyperedges.length} hyperedge${plural(hyperedges.length)} (hyperedges: "error")`, {
1993
2501
  element: "hyperedges",
1994
2502
  });
1995
2503
  throw report.abort("hyperedges refused", { code: JSON_ISSUE.HYPEREDGE, count: hyperedges.length });
1996
2504
  }
1997
2505
  if (policy === "skip") {
1998
- report.warning("unsupported", JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(s) skipped`, {
2506
+ report.warning("unsupported", JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge${plural(hyperedges.length)} skipped`, {
1999
2507
  element: "hyperedges",
2000
2508
  });
2001
- report.loss(JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(s) were not imported`, null, hyperedges.length);
2509
+ report.loss(JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge${plural(hyperedges.length)} ${agree(hyperedges.length, "was", "were")} not imported`, null, hyperedges.length);
2002
2510
  return;
2003
2511
  }
2512
+ // each expanded edge is pushed on its own, so one the sink refuses is reported and counted
2513
+ // while the others stay
2514
+ const expand = (record, pairs, kind, element) => {
2515
+ for (const [u, v] of pairs) {
2516
+ try {
2517
+ push(record, u, v, kind, element, JGF_HYPEREDGE_KEYS);
2518
+ }
2519
+ catch (err) {
2520
+ ctx.skip(err, "edge", element);
2521
+ }
2522
+ }
2523
+ };
2524
+ // the edges expanded so far: the cap is on the whole import, so many hyperedges just under it
2525
+ // cannot amplify without bound either
2526
+ const budget = { used: 0 };
2004
2527
  for (let i = 0; i < hyperedges.length; i++) {
2005
2528
  const element = `hyperedges[${i}]`;
2006
2529
  const record = hyperedges[i];
@@ -2010,23 +2533,26 @@ function importHyperedges(ctx, hyperedges, directed, push) {
2010
2533
  }
2011
2534
  try {
2012
2535
  if (Array.isArray(record.nodes)) {
2013
- const members = record.nodes.map((raw) => ctx.requireId(raw, element));
2536
+ const members = distinctMembers(ctx, record.nodes.map((raw) => ctx.requireId(raw, element)), element);
2014
2537
  if (members.length < 2) {
2015
2538
  throw new GraphFormatError("E_INVALID_ID", `${element}: an undirected hyperedge needs two or more nodes`, { reason: "hyperedge shape" });
2016
2539
  }
2017
- const kind = directed ? "directed" : "undirected";
2540
+ const n = members.length;
2541
+ checkExpansion(policy === "star" ? n - 1 : (n * (n - 1)) / 2, budget, element);
2542
+ const pairs = [];
2018
2543
  if (policy === "star") {
2019
- for (let k = 1; k < members.length; k++) {
2020
- push(record, members[0], members[k], kind, element, JGF_HYPEREDGE_KEYS);
2544
+ for (let k = 1; k < n; k++) {
2545
+ pairs.push([members[0], members[k]]);
2021
2546
  }
2022
2547
  }
2023
2548
  else {
2024
- for (let a = 0; a < members.length; a++) {
2025
- for (let b = a + 1; b < members.length; b++) {
2026
- push(record, members[a], members[b], kind, element, JGF_HYPEREDGE_KEYS);
2549
+ for (let a = 0; a < n; a++) {
2550
+ for (let b = a + 1; b < n; b++) {
2551
+ pairs.push([members[a], members[b]]);
2027
2552
  }
2028
2553
  }
2029
2554
  }
2555
+ expand(record, pairs, directed ? "directed" : "undirected", element);
2030
2556
  }
2031
2557
  else if (Array.isArray(record.source) && Array.isArray(record.target)) {
2032
2558
  const sources = record.source.map((raw) => ctx.requireId(raw, element));
@@ -2034,11 +2560,8 @@ function importHyperedges(ctx, hyperedges, directed, push) {
2034
2560
  if (sources.length === 0 || targets.length === 0) {
2035
2561
  throw new GraphFormatError("E_INVALID_ID", `${element}: a directed hyperedge needs sources and targets`, { reason: "hyperedge shape" });
2036
2562
  }
2037
- for (const s of sources) {
2038
- for (const t of targets) {
2039
- push(record, s, t, "directed", element, JGF_HYPEREDGE_KEYS);
2040
- }
2041
- }
2563
+ checkExpansion(sources.length * targets.length, budget, element);
2564
+ expand(record, sources.flatMap((s) => targets.map((t) => [s, t])), "directed", element);
2042
2565
  }
2043
2566
  else {
2044
2567
  report.error("validation-error", JSON_ISSUE.HYPEREDGE_SHAPE, `${element} has neither a nodes array nor source / target arrays`, { element });
@@ -2046,10 +2569,52 @@ function importHyperedges(ctx, hyperedges, directed, push) {
2046
2569
  }
2047
2570
  }
2048
2571
  catch (err) {
2049
- ctx.skip(err, "edge", element);
2572
+ if (err instanceof ExpansionRefused) {
2573
+ // one hyperedge over the cap is skipped; the shared E_TOO_LARGE rule would stop the import
2574
+ report.error("unsupported", err.code, err.message, { element });
2575
+ ctx.countSkipped("edge");
2576
+ }
2577
+ else {
2578
+ ctx.skip(err, "edge", element);
2579
+ }
2050
2580
  }
2051
2581
  }
2052
2582
  }
2583
+ /** A hyperedge whose expansion would pass MAX_HYPEREDGE_EXPANSION: that hyperedge alone is skipped. */
2584
+ class ExpansionRefused extends GraphFormatError {
2585
+ }
2586
+ /** The most edges the hyperedges of one import expand into; a 20k-member clique would be 200M edges. */
2587
+ // ponytail: a fixed cap; make it an option if a real file needs more
2588
+ const MAX_HYPEREDGE_EXPANSION = 1000000;
2589
+ /**
2590
+ * Refuse an expansion that would take the import beyond MAX_HYPEREDGE_EXPANSION expanded edges,
2591
+ * before any of it is pushed; otherwise count it against the budget.
2592
+ * @param count - the number of edges the expansion makes
2593
+ * @param budget - the edges the import's hyperedges expanded into so far
2594
+ * @param budget.used - that count, updated
2595
+ * @param element - the hyperedge's name
2596
+ */
2597
+ function checkExpansion(count, budget, element) {
2598
+ if (budget.used + count > MAX_HYPEREDGE_EXPANSION) {
2599
+ throw new ExpansionRefused("E_TOO_LARGE", `${element}: the expansion makes ${count} edges, which with the ${budget.used} already expanded is more than ${MAX_HYPEREDGE_EXPANSION}; the hyperedge is skipped`, { count, used: budget.used, limit: MAX_HYPEREDGE_EXPANSION });
2600
+ }
2601
+ budget.used += count;
2602
+ }
2603
+ /**
2604
+ * The members of an undirected hyperedge with repeats removed (a repeat would expand into a
2605
+ * self-loop and parallel edges), each repeat reported as E_HYPEREDGE_SHAPE.
2606
+ * @param ctx - the context
2607
+ * @param members - the members as listed
2608
+ * @param element - the hyperedge's name
2609
+ * @returns the distinct members in first-listed order
2610
+ */
2611
+ function distinctMembers(ctx, members, element) {
2612
+ const distinct = [...new Set(members)];
2613
+ if (distinct.length < members.length) {
2614
+ ctx.report.error("validation-error", JSON_ISSUE.HYPEREDGE_SHAPE, `${element} lists ${members.length - distinct.length} member${plural(members.length - distinct.length)} more than once; each is read once`, { element });
2615
+ }
2616
+ return distinct;
2617
+ }
2053
2618
  // ============================================================ Cytoscape
2054
2619
  /**
2055
2620
  * Read Cytoscape.js elements: `elements.nodes` / `elements.edges`, a flat `elements` array (group
@@ -2095,7 +2660,8 @@ function importCytoscape(ctx, root) {
2095
2660
  role: "position",
2096
2661
  mutable: true,
2097
2662
  nullable: true,
2098
- extra: { sourceDims: 2, units: "file" },
2663
+ // 3D layouts written by Cytoscape-compatible tools carry a z
2664
+ extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
2099
2665
  origin: { format: "json", namespace: "cytoscape" },
2100
2666
  }, anyHas(nodes, "position"));
2101
2667
  const classesColumn = ctx.nodes.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(nodes, "classes"));
@@ -2120,6 +2686,9 @@ function importCytoscape(ctx, root) {
2120
2686
  if (index < 0) {
2121
2687
  continue;
2122
2688
  }
2689
+ if (present(data, "source") && present(data, "target")) {
2690
+ report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`, { element });
2691
+ }
2123
2692
  try {
2124
2693
  for (const key of Object.keys(data)) {
2125
2694
  if (key === "id") {
@@ -2135,38 +2704,98 @@ function importCytoscape(ctx, root) {
2135
2704
  }
2136
2705
  continue;
2137
2706
  }
2138
- ctx.nodes.write(index, key, data[key], SUFFIX.data);
2707
+ ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
2139
2708
  }
2140
2709
  const { position } = record;
2141
2710
  if (position !== undefined && position !== null) {
2142
- if (isJsonObject(position) && typeof position.x === "number" && typeof position.y === "number") {
2711
+ const z = isJsonObject(position) ? (position.z ?? 0) : 0;
2712
+ if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
2143
2713
  point[0] = position.x;
2144
2714
  // Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
2145
2715
  point[1] = flipY(position.y);
2716
+ point[2] = z;
2146
2717
  ctx.nodes.set(positionColumn, index, point);
2147
2718
  }
2148
2719
  else {
2149
- report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y`, { element });
2720
+ report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y (and z) that are finite and within the f32 range`, { element });
2150
2721
  }
2151
2722
  }
2152
2723
  writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
2153
- writeElementKeys(ctx.nodes, index, record);
2724
+ writeElementKeys(ctx.nodes, index, record, element);
2154
2725
  }
2155
2726
  catch (err) {
2156
2727
  report.recordError(err, { element: String(id) });
2157
2728
  }
2158
2729
  }
2159
- for (const { index, parent, element } of parents) {
2730
+ resolveCytoscapeParents(ctx, parents, parentColumn);
2731
+ throwIfAborted(ctx.options.signal);
2732
+ importCytoscapeEdges(ctx, edges, edgeClassesColumn);
2733
+ ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
2734
+ }
2735
+ /**
2736
+ * Set the parent of each Cytoscape node once every node is known (a parent may come later): an
2737
+ * unknown parent is E_UNKNOWN_PARENT, a link that would make a node its own ancestor E_PARENT_CYCLE
2738
+ * (Cytoscape.js refuses those too); either link is dropped and the node kept.
2739
+ * @param ctx - the context
2740
+ * @param parents - the parent links in document order
2741
+ * @param parentColumn - the parent column
2742
+ */
2743
+ function resolveCytoscapeParents(ctx, parents, parentColumn) {
2744
+ const { report } = ctx;
2745
+ // the last link per child (a duplicate node's later parent wins), child -> its link
2746
+ const linkOf = new Map();
2747
+ for (let order = 0; order < parents.length; order++) {
2748
+ const { index, parent, element } = parents[order];
2160
2749
  const parentIndex = ctx.sink.indexOf(parent);
2161
2750
  if (parentIndex === INVALID_INDEX) {
2162
2751
  report.error("missing-value", JSON_ISSUE.UNKNOWN_PARENT, `${element}: parent ${JSON.stringify(parent)} is not a node`, { element });
2163
2752
  continue;
2164
2753
  }
2165
- ctx.nodes.set(parentColumn, index, parentIndex);
2754
+ linkOf.set(index, { parent: parentIndex, id: parent, order, element });
2755
+ }
2756
+ // one walk per chain, each node visited once: on a cycle the link that came last in the document
2757
+ // (the one that closed it) is dropped
2758
+ const done = new Set();
2759
+ for (const start of linkOf.keys()) {
2760
+ const path = [];
2761
+ const onPath = new Set();
2762
+ let node = start;
2763
+ while (node !== undefined && !done.has(node) && !onPath.has(node)) {
2764
+ path.push(node);
2765
+ onPath.add(node);
2766
+ node = linkOf.get(node)?.parent;
2767
+ }
2768
+ if (node !== undefined && onPath.has(node)) {
2769
+ dropClosingLink(report, linkOf, path.slice(path.indexOf(node)));
2770
+ }
2771
+ for (const member of path) {
2772
+ done.add(member);
2773
+ }
2774
+ }
2775
+ for (const [index, { parent }] of linkOf) {
2776
+ ctx.nodes.set(parentColumn, index, parent);
2166
2777
  }
2167
- throwIfAborted(ctx.options.signal);
2168
- importCytoscapeEdges(ctx, edges, edgeClassesColumn);
2169
- ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
2778
+ }
2779
+ /**
2780
+ * Drop the link of a parent cycle that came last in the document (the one that closed it).
2781
+ * @param report - the report
2782
+ * @param linkOf - the links by child, the dropped one removed
2783
+ * @param cycle - the nodes of the cycle
2784
+ */
2785
+ function dropClosingLink(report, linkOf, cycle) {
2786
+ let closing = null;
2787
+ for (const member of cycle) {
2788
+ const link = linkOf.get(member);
2789
+ if (link !== undefined && (closing === null || link.order > closing[1].order)) {
2790
+ closing = [member, link];
2791
+ }
2792
+ }
2793
+ if (closing === null) {
2794
+ return;
2795
+ }
2796
+ const { id, element } = closing[1];
2797
+ report.error("validation-error", JSON_ISSUE.PARENT_CYCLE, `${element}: parent ${JSON.stringify(id)} would make the node its own ancestor; the link is dropped`, { element });
2798
+ linkOf.delete(closing[0]);
2170
2799
  }
2171
2800
  /**
2172
2801
  * Read the Cytoscape edge elements: `data.id`, `data.source` / `data.target`, the weight, the
@@ -2186,8 +2815,8 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
2186
2815
  continue;
2187
2816
  }
2188
2817
  const { data } = record;
2189
- if (!hasKey(data, "source") || !hasKey(data, "target")) {
2190
- ctx.missingEndpoint(element, `data.${hasKey(data, "source") ? "target" : "source"}`);
2818
+ if (!present(data, "source") || !present(data, "target")) {
2819
+ ctx.missingEndpoint(element, `data.${present(data, "source") ? "target" : "source"}`);
2191
2820
  continue;
2192
2821
  }
2193
2822
  try {
@@ -2203,11 +2832,11 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
2203
2832
  const { weightFrom } = ctx.options;
2204
2833
  for (const key of Object.keys(data)) {
2205
2834
  if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
2206
- ctx.edges.write(edge, key, data[key], SUFFIX.data);
2835
+ ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
2207
2836
  }
2208
2837
  }
2209
2838
  writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
2210
- writeElementKeys(ctx.edges, edge, record);
2839
+ writeElementKeys(ctx.edges, edge, record, element);
2211
2840
  }
2212
2841
  catch (err) {
2213
2842
  ctx.skip(err, "edge", element);
@@ -2224,10 +2853,16 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
2224
2853
  function cytoscapeSections(ctx, elements) {
2225
2854
  const { report } = ctx;
2226
2855
  if (Array.isArray(elements)) {
2227
- return {
2228
- nodes: elements.filter((item) => isJsonObject(item) && isNodeElement(item)),
2229
- edges: elements.filter((item) => isJsonObject(item) && !isNodeElement(item)),
2230
- };
2856
+ const nodes = [];
2857
+ const edges = [];
2858
+ elements.forEach((item, i) => {
2859
+ if (isJsonObject(item) && item.group !== undefined && item.group !== "nodes" && item.group !== "edges") {
2860
+ report.error("validation-error", JSON_ISSUE.BAD_VALUE, `elements[${i}]: group ${describe(item.group)} is neither "nodes" nor "edges"; classed by its endpoints`, { element: `elements[${i}]` });
2861
+ }
2862
+ // a non-object item stays with the nodes, whose reader reports it as E_BAD_ELEMENT
2863
+ (isJsonObject(item) && !isNodeElement(item) ? edges : nodes).push(item);
2864
+ });
2865
+ return { nodes, edges };
2231
2866
  }
2232
2867
  if (isJsonObject(elements)) {
2233
2868
  return {
@@ -2236,13 +2871,31 @@ function cytoscapeSections(ctx, elements) {
2236
2871
  };
2237
2872
  }
2238
2873
  if (elements === undefined || elements === null) {
2239
- report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no elements", {
2874
+ report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no elements", {
2240
2875
  element: "elements",
2241
2876
  });
2242
2877
  return { nodes: [], edges: [] };
2243
2878
  }
2244
2879
  return report.fail(JSON_ISSUE.SHAPE, `elements must be an object or an array, found ${describe(elements)}`);
2245
2880
  }
2881
+ /**
2882
+ * Whether a Cytoscape element's position has a numeric z.
2883
+ * @param element - the element
2884
+ * @returns true when position.z is a number
2885
+ */
2886
+ function hasPositionZ(element) {
2887
+ return isJsonObject(element) && isJsonObject(element.position) && typeof element.position.z === "number";
2888
+ }
2889
+ /** The largest finite f32, the bound of a position coordinate. */
2890
+ const F32_MAX = 3.4028234663852886e38;
2891
+ /**
2892
+ * Whether a value is a coordinate the f32 position column holds: a finite number within the f32 range.
2893
+ * @param value - the value
2894
+ * @returns true for a storable coordinate
2895
+ */
2896
+ function isCoordinate(value) {
2897
+ return typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= F32_MAX;
2898
+ }
2246
2899
  /**
2247
2900
  * Whether a flat Cytoscape element is a node: `group: "nodes"`, or no source / target in its data.
2248
2901
  * @param element - the element
@@ -2290,22 +2943,36 @@ function writeClasses(ctx, writer, column, row, raw, element) {
2290
2943
  * @param writer - the table writer
2291
2944
  * @param row - the row
2292
2945
  * @param record - the element
2946
+ * @param element - the element name for issues
2293
2947
  */
2294
- function writeElementKeys(writer, row, record) {
2948
+ function writeElementKeys(writer, row, record, element) {
2295
2949
  for (const key of Object.keys(record)) {
2296
2950
  if (CYTOSCAPE_STRUCTURAL_KEYS.has(key)) {
2297
2951
  continue;
2298
2952
  }
2299
2953
  const name = CYTOSCAPE_ELEMENT_KEYS.has(key) ? key : `${key}${SUFFIX.element}`;
2300
- writer.write(row, name, record[key], SUFFIX.data);
2954
+ writer.write(row, name, record[key], SUFFIX.data, element);
2301
2955
  }
2302
2956
  }
2303
2957
  // ============================================================ the plugin
2304
2958
  /**
2305
- * The JSON importer plugin (design section 8.4).
2959
+ * The JSON importer plugin.
2960
+ * @category Built-in formats
2306
2961
  */
2307
2962
  export const jsonImporter = Object.freeze({
2308
2963
  format: "json",
2964
+ options: Object.freeze([
2965
+ "dialect",
2966
+ "edgesKey",
2967
+ "edgesPath",
2968
+ "indexLinks",
2969
+ "nodeIdKey",
2970
+ "nodesPath",
2971
+ "oboIds",
2972
+ "sourceKey",
2973
+ "targetKey",
2974
+ "typedefs",
2975
+ ]),
2309
2976
  extensions: Object.freeze([".json"]),
2310
2977
  mimeTypes: Object.freeze(["application/json"]),
2311
2978
  /**
@@ -2316,7 +2983,8 @@ export const jsonImporter = Object.freeze({
2316
2983
  * @returns the confidence
2317
2984
  */
2318
2985
  sniff(head) {
2319
- const text = new TextDecoder("utf-8").decode(head.subarray(0, SNIFF_BYTES));
2986
+ // headBytes transcodes a UTF-16 head with a BOM, which import() decodes too
2987
+ const text = new TextDecoder("utf-8").decode(headBytes(head).subarray(0, SNIFF_BYTES));
2320
2988
  const trimmed = (text.startsWith(BOM) ? text.slice(1) : text).trimStart();
2321
2989
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
2322
2990
  return 0;
@@ -2337,7 +3005,7 @@ export const jsonImporter = Object.freeze({
2337
3005
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2338
3006
  const json = resolveJsonOptions(options);
2339
3007
  const report = new ImportReportBuilder("json", resolved.errorLimit);
2340
- const text = await readJsonText(input, report, resolved);
3008
+ const text = await readText(input, report, { ...resolved, bomlessUtf16: true });
2341
3009
  const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2342
3010
  readGraph(root, dialect, sink, report, resolved, json, options);
2343
3011
  return report.finish();
@@ -2354,7 +3022,7 @@ export const jsonImporter = Object.freeze({
2354
3022
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2355
3023
  const json = resolveJsonOptions(options);
2356
3024
  const report = new ImportReportBuilder("json", resolved.errorLimit);
2357
- const text = await readJsonText(input, report, resolved);
3025
+ const text = await readText(input, report, { ...resolved, bomlessUtf16: true });
2358
3026
  const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2359
3027
  return listingsOf(root, dialect);
2360
3028
  },
@@ -2370,7 +3038,7 @@ export const jsonImporter = Object.freeze({
2370
3038
  const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2371
3039
  const json = resolveJsonOptions(options);
2372
3040
  const first = new ImportReportBuilder("json", resolved.errorLimit);
2373
- const text = await readJsonText(input, first, resolved);
3041
+ const text = await readText(input, first, { ...resolved, bomlessUtf16: true });
2374
3042
  const { root, dialect } = documentOf(parseDocument(text, first), json, first);
2375
3043
  const graphs = listingsOf(root, dialect).length;
2376
3044
  const reports = [];
@@ -2397,14 +3065,26 @@ function readGraph(root, dialect, sink, report, resolved, json, options) {
2397
3065
  // the obographs reader refuses missing endpoints itself, so addMissingNodes false holds on any sink
2398
3066
  reportSinkOptions(sink, options, report, dialect === "obographs");
2399
3067
  reportUnusedOptions(options, report, USED_OPTIONS);
3068
+ readDialect(ctx, root, dialect);
3069
+ ctx.reportDangling();
3070
+ ctx.nodes.reportNullOnly();
3071
+ ctx.edges.reportNullOnly();
3072
+ throwIfAborted(resolved.signal);
3073
+ }
3074
+ /**
3075
+ * Read one graph with the reader of its dialect.
3076
+ * @param ctx - the import context
3077
+ * @param root - the parsed document
3078
+ * @param dialect - its dialect
3079
+ */
3080
+ function readDialect(ctx, root, dialect) {
2400
3081
  if (dialect === "cytoscape") {
2401
3082
  importCytoscape(ctx, root);
2402
- throwIfAborted(resolved.signal);
2403
3083
  return;
2404
3084
  }
2405
3085
  const doc = isJsonObject(root)
2406
3086
  ? root
2407
- : report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
3087
+ : ctx.report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
2408
3088
  switch (dialect) {
2409
3089
  case "node-link":
2410
3090
  case "d3":
@@ -2436,6 +3116,5 @@ function readGraph(root, dialect, sink, report, resolved, json, options) {
2436
3116
  });
2437
3117
  }
2438
3118
  }
2439
- throwIfAborted(resolved.signal);
2440
3119
  }
2441
3120
  //# sourceMappingURL=importer.js.map