@graphty/graph-io 0.3.25 → 0.3.26

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-BbJI55dx.js +56 -0
  4. package/dist/chunks/choose-BbJI55dx.js.map +1 -0
  5. package/dist/chunks/constants-BCsFsMj4.js +231 -0
  6. package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
  8. package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
  9. package/dist/chunks/export-BTI4yQgy.js +4333 -0
  10. package/dist/chunks/export-BTI4yQgy.js.map +1 -0
  11. package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
  12. package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
  13. package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
  14. package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
  15. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
  16. package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
  17. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
  18. package/dist/chunks/exporter-tne475KK.js.map +1 -0
  19. package/dist/chunks/importer-90YWhJoF.js +2811 -0
  20. package/dist/chunks/importer-90YWhJoF.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
  22. package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
  23. package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
  24. package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
  25. package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
  26. package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
  28. package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
  29. package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
  30. package/dist/chunks/importer-DfVu6j59.js.map +1 -0
  31. package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
  32. package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
  33. package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
  34. package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
  35. package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
  36. package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
  38. package/dist/chunks/records-CQXT__mY.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
  40. package/dist/chunks/text-DmcUs5_Q.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 +951 -261
  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 +342 -85
  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 +564 -157
  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 +19 -1
  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 +33 -17
  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 +868 -188
  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 +2 -0
  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 +400 -83
  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 +145 -32
  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 +680 -285
  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 +226 -79
  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 +98 -24
  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 +108 -26
  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 +238 -85
  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 +66 -34
  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 +183 -51
  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 +20 -21
  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 +7 -2
  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 +38 -4
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +33 -17
  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 +1034 -198
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +6 -1
  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 +477 -88
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +201 -46
  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 +888 -300
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +61 -338
  498. package/src/formats/cx2/importer.ts +317 -86
  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 +139 -31
  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 +111 -26
  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 +320 -101
  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 +89 -41
  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 +213 -52
  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 +35 -27
  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 +11 -3
  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
package/src/registry.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The importer / exporter registry (design sections 8.2 and 8.4): the eight built-in formats
2
+ * The importer / exporter registry: the built-in formats
3
3
  * registered by name, `sniff()` over them, and the two conveniences for callers who do not own a
4
4
  * sink: `importGraph()` sniffs the format, creates a builder seeded from the common options
5
5
  * (`directed: true` as a placeholder; the importer sets the real value), imports and freezes;
@@ -17,92 +17,265 @@ import {
17
17
  } from "@graphty/graph-format";
18
18
 
19
19
  import { CellBudgetBuilder, maxEmptyCellsOption } from "./common/cell-budget.js";
20
- import { throwIfAborted } from "./common/input.js";
21
- import { ImportReportBuilder } from "./common/report.js";
20
+ import {
21
+ EDGES_MERGED_CODE,
22
+ EMPTY_INPUT_CODE,
23
+ FETCH_CODE,
24
+ OPTION_IGNORED_CODE,
25
+ SELF_LOOPS_DROPPED_CODE,
26
+ UNKNOWN_OPTION_CODE,
27
+ WEIGHT_NOT_FOUND_CODE,
28
+ } from "./common/codes.js";
29
+ import { abortable, foreignKind, lockReader, normalizeInput, throwIfAborted } from "./common/input.js";
30
+ import { chooseGraph, resolveImportOptions } from "./common/options.js";
31
+ import { agree, plural } from "./common/plural.js";
32
+ import { ImportReportBuilder, isAbortError, messageOf } from "./common/report.js";
33
+ import { collectBytes } from "./common/writer.js";
22
34
  import { csvExporter, csvImporter } from "./formats/csv/index.js";
23
- import { cxImporter } from "./formats/cx/index.js";
35
+ import { cxExporter, cxImporter } from "./formats/cx/index.js";
24
36
  import { cx2Exporter, cx2Importer } from "./formats/cx2/index.js";
25
- import { cysImporter } from "./formats/cys/index.js";
37
+ import { cysExporter, cysImporter } from "./formats/cys/index.js";
26
38
  import { dotExporter, dotImporter } from "./formats/dot/index.js";
27
39
  import { gexfExporter, gexfImporter } from "./formats/gexf/index.js";
28
40
  import { gmlExporter, gmlImporter } from "./formats/gml/index.js";
29
41
  import { graphmlExporter, graphmlImporter } from "./formats/graphml/index.js";
30
42
  import { jsonExporter, jsonImporter } from "./formats/json/index.js";
31
43
  import { neo4jExporter, neo4jImporter } from "./formats/neo4j/index.js";
32
- import { oboImporter } from "./formats/obo/index.js";
44
+ import { oboExporter, oboImporter } from "./formats/obo/index.js";
33
45
  import { pajekExporter, pajekImporter } from "./formats/pajek/index.js";
34
46
  import { xgmmlExporter, xgmmlImporter } from "./formats/xgmml/index.js";
35
- import { rankFormats, SNIFF_HEAD_BYTES, type SniffHints, type SniffResult } from "./sniff.js";
47
+ import { type FormatName, rankFormats, SNIFF_HEAD_BYTES, type SniffHints, type SniffResult } from "./sniff.js";
36
48
  import {
37
49
  type CommonExportOptions,
38
50
  type CommonImportOptions,
51
+ type ExportCapabilities,
39
52
  type GraphChoiceOptions,
40
53
  type GraphExporter,
41
54
  type GraphImporter,
42
55
  type GraphListing,
56
+ ImportError,
43
57
  type ImportInput,
58
+ type ImportIssue,
44
59
  type ImportReport,
60
+ type IssueCategory,
45
61
  type LossNote,
46
62
  } from "./types.js";
47
63
 
48
- /** The issue code of an input whose format no registered importer recognises. */
64
+ /**
65
+ * The input is in no format graph-io can read (or none of the formats you registered, for your own FormatRegistry).
66
+ * Pass `format` when you know what the file is.
67
+ * @category Issue and loss codes
68
+ */
49
69
  export const UNKNOWN_FORMAT_CODE = "E_UNKNOWN_FORMAT";
50
70
 
51
- /** The builder options importGraph() accepts beyond the ones the common import options seed. */
71
+ /**
72
+ * Registered importer whose sniff() threw while the format was being chosen; that importer was treated as not
73
+ * recognizing the input (a defect in that importer).
74
+ * @category Issue and loss codes
75
+ */
76
+ export const SNIFF_FAILED_CODE = "W_SNIFF_FAILED";
77
+
78
+ /**
79
+ * The `builder` option of importGraph(): settings for building the graph that the common import
80
+ * options do not cover. `weighted`: "auto" (the default) gives the graph weights when any edge has
81
+ * one, true always does (every weight 1 when the file has none), false never does. `expectedNodes`
82
+ * and `expectedEdges`: how many nodes and edges to make room for up front, which saves time on a
83
+ * large file whose size you know.
84
+ * @category Loading
85
+ */
52
86
  export type BuilderSeed = Omit<
53
87
  GraphBuilderOptions,
54
88
  "directed" | "addMissingNodes" | "duplicateEdges" | "selfLoops" | "weightDtype"
55
89
  >;
56
90
 
57
91
  /**
58
- * The options of importGraph(): the common import options (which also seed the registry's
59
- * builder, design section 8.4), the format choice and the hints sniffing uses, the builder and
60
- * freeze options, and any format-specific option (`delimiter`, `dialect`, ...) passed through to
61
- * the importer unchanged. `graphIndex` / `graphName` choose one graph of an input that holds
62
- * several, for the formats that list their graphs.
92
+ * The options of importGraph(), importAllGraphs(), listGraphs() and loadFromFile(): the common
93
+ * import options, the format and the hints used to detect it, the graph to read from a file that
94
+ * holds several, and any format-specific option (`delimiter`, `dialect`, ...), which reaches the
95
+ * importer unchanged.
96
+ * @category Loading
63
97
  */
64
98
  export interface ImportGraphOptions extends CommonImportOptions, GraphChoiceOptions {
65
- /** The format name, or "auto" (default) to sniff it from the filename, MIME type and content. */
66
- readonly format?: string | undefined;
67
- /** The file name or path the input came from, a hint for sniffing. */
99
+ /**
100
+ * The format to read the input as ("graphml", "csv", ...; `listFormats()` names them all), or
101
+ * "auto" to work it out from `filename`, `mimeType` and the first bytes of the input. Name it to
102
+ * be strict: detection reads a file graph-io cannot place as the closest format it recognizes.
103
+ * @defaultValue "auto"
104
+ */
105
+ readonly format?: FormatName | "auto" | undefined;
106
+ /**
107
+ * The file name or full path the input came from; only its extension is used, as a format hint.
108
+ * loadFromUrl() takes it from the URL and loadFromFile() from a File's name when you do not pass
109
+ * it.
110
+ */
68
111
  readonly filename?: string | null | undefined;
69
- /** The MIME type the input was served as, a hint for sniffing. */
112
+ /**
113
+ * The MIME type the input was served as, a format hint. loadFromUrl() takes it from the
114
+ * response's Content-Type and loadFromFile() from the Blob's `type` when you do not pass it.
115
+ */
70
116
  readonly mimeType?: string | null | undefined;
71
- /** Builder options the common options do not cover (`weighted`, `expectedNodes`, ...). */
117
+ /**
118
+ * Settings for building the graph: `weighted` ("auto", true or false: whether the graph gets
119
+ * weights), `expectedNodes` and `expectedEdges` (room to make up front for a large file). Most
120
+ * programs never set it.
121
+ */
72
122
  readonly builder?: BuilderSeed | undefined;
73
- /** Options of the freeze that follows the import. */
123
+ /**
124
+ * Settings for the last step of an import, which turns what was read into the snapshot:
125
+ * `label` (a name for debugging, read back as `snapshot.label`), `prepare` (views of the graph
126
+ * to compute up front, such as `["reverse"]`), `checksum` (record checksums, which
127
+ * `snapshot.validate({ checksum: true })` compares later to catch changed memory) and `profile` (record how long each step took, in `result.freeze.timings`). Most
128
+ * programs never set it.
129
+ */
74
130
  readonly freeze?: FreezeOptions | undefined;
75
131
  /**
76
- * The most attribute slots that hold no value the import may allocate before it stops with
77
- * E_TOO_MANY_EMPTY_CELLS. Every attribute is a column with one slot per node (or edge), so a file
132
+ * The most attribute slots that hold no value the import may allocate before it stops with an
133
+ * ImportError whose `issue.code` is E_TOO_MANY_EMPTY_CELLS. Every attribute is a column with one slot per node (or edge), so a file
78
134
  * whose nodes each have a differently named attribute would otherwise need nodes x attributes
79
- * memory. Default 2^24 (16,777,216); Infinity turns the check off. Dense files are never stopped.
135
+ * memory. Infinity turns the check off. Files whose elements mostly share their attributes are
136
+ * never stopped.
137
+ * @defaultValue 16777216
80
138
  */
81
139
  readonly maxEmptyCells?: number | undefined;
82
140
  /** Format-specific options, passed to the importer as they are. */
83
141
  readonly [formatOption: string]: unknown;
84
142
  }
85
143
 
86
- /** What importGraph() returns (design section 8.4). */
144
+ /**
145
+ * What importGraph(), loadFromUrl() and loadFromFile() return.
146
+ * @category Loading
147
+ */
87
148
  export interface ImportGraphResult {
88
- /** The format the input was read as. */
149
+ /** The format the input was read as ("graphml", ...). */
89
150
  readonly format: string;
90
- /** The sniff that chose the importer, or null when the caller named the format. */
151
+ /**
152
+ * How the format was detected (the candidate's `confidence` and what matched), or null when you
153
+ * named the format. Most callers can ignore it. For the JSON dialect that was read, call
154
+ * `jsonShapeOf(snapshot)` from `@graphty/graph-io/json`.
155
+ */
91
156
  readonly sniff: SniffResult | null;
92
- /** The frozen snapshot. */
157
+ /**
158
+ * The graph. `snapshot.nodeCount`, `snapshot.edgeCount`, attribute tables and so on come from
159
+ * the `@graphty/graph-format` package.
160
+ */
93
161
  readonly snapshot: GraphSnapshot;
94
- /** The importer's report. */
162
+ /**
163
+ * What happened during the import. `errorCount` counts skipped elements (see `errorLimit`,
164
+ * default 100); `warningCount` counts elements that were kept but changed; `issues` lists both;
165
+ * `counts.skippedNodes` / `counts.skippedEdges` say how much is missing from `snapshot`.
166
+ */
95
167
  readonly report: ImportReport;
96
- /** The freeze report (design section 6.6). */
168
+ /**
169
+ * What the last step of the import did to the graph: `mergedEdges` (parallel edges merged by
170
+ * `duplicateEdges`), `droppedSelfLoops` (removed by `selfLoops: "drop"`), and `timings` when you
171
+ * passed `freeze: { profile: true }`. Both counts are also reported as warnings, so most callers
172
+ * can ignore it. It is not enumerable, so `console.log(result)` and `{ ...result }` leave it out.
173
+ */
97
174
  readonly freeze: FreezeReport;
98
175
  }
99
176
 
100
- /** The options of exportGraph(): the common export options plus any format-specific option, passed through. */
177
+ /**
178
+ * Options for loadFromUrl(): everything importGraph() accepts, plus the fetch request settings.
179
+ * @example
180
+ * ```ts
181
+ * await loadFromUrl("/api/graph.csv", { request: { headers: { Authorization: token } }, delimiter: ";" });
182
+ * ```
183
+ * @category Loading
184
+ */
185
+ export interface LoadFromUrlOptions extends ImportGraphOptions {
186
+ /**
187
+ * Passed to fetch() as its second argument: headers, credentials, mode and so on. If you set
188
+ * `signal` in the import options and not here, the same signal also cancels the download.
189
+ */
190
+ readonly request?: RequestInit | undefined;
191
+ }
192
+
193
+ /**
194
+ * Options for downloadGraph(): the export options plus the name of the saved file.
195
+ * @example
196
+ * ```ts
197
+ * await downloadGraph(snapshot, "graphml", { filename: "network.graphml" });
198
+ * ```
199
+ * @category Saving
200
+ */
201
+ export interface DownloadGraphOptions extends ExportGraphOptions {
202
+ /**
203
+ * The file name the browser saves as. The default is "graph" plus the first entry of the
204
+ * format's `extensions` ("graph.graphml"), or "graph.<format>" when it lists none.
205
+ * @defaultValue "graph" plus the format's extension
206
+ */
207
+ readonly filename?: string | undefined;
208
+ }
209
+
210
+ /**
211
+ * One format the registry can read, write or both.
212
+ * @category Formats and detection
213
+ */
214
+ export interface FormatInfo {
215
+ /** The name you pass as `format`: "graphml", "gexf", "csv", "pajek", ... */
216
+ readonly format: string;
217
+ /**
218
+ * File extensions with the leading dot, most common first (".graphml", ".net"). Empty only for a
219
+ * custom format that declares none.
220
+ */
221
+ readonly extensions: readonly string[];
222
+ /** MIME types, most specific first. Empty when the format declares none. */
223
+ readonly mimeTypes: readonly string[];
224
+ /** Whether loadFromUrl(), loadFromFile() and importGraph() can read it. */
225
+ readonly canImport: boolean;
226
+ /** Whether the export functions can write it. */
227
+ readonly canExport: boolean;
228
+ /**
229
+ * What the format can store exactly, or null when it cannot be written. The fields are
230
+ * mixedDirection, multiEdges, selfLoops, edgeIds, idCharset, dtypes, components, lists, json,
231
+ * defaults, options, hierarchy, temporal, graphAttributes, positions and viz (each documented on
232
+ * ExportCapabilities). checkExport() tells you where a particular graph goes beyond this.
233
+ */
234
+ readonly capabilities: ExportCapabilities | null;
235
+ }
236
+
237
+ /**
238
+ * The options of exportGraph(): the common export options plus any format-specific option, passed through.
239
+ * @category Saving
240
+ */
101
241
  export interface ExportGraphOptions extends CommonExportOptions {
102
242
  /** Format-specific options, passed to the exporter as they are. */
103
243
  readonly [formatOption: string]: unknown;
104
244
  }
105
245
 
246
+ /**
247
+ * The options the load and save functions take themselves: the common import and export options and the functions'
248
+ * own. With the formats' own `options` lists, these are the names a load or a save knows.
249
+ */
250
+ const FUNCTION_OPTIONS: ReadonlySet<string> = new Set([
251
+ "addMissingNodes",
252
+ "builder",
253
+ "defaultDirected",
254
+ "duplicateEdges",
255
+ "encoding",
256
+ "errorLimit",
257
+ "filename",
258
+ "format",
259
+ "freeze",
260
+ "graphIndex",
261
+ "graphName",
262
+ "hyperedges",
263
+ "ids",
264
+ "long",
265
+ "maxEmptyCells",
266
+ "mimeType",
267
+ "nodeIdFrom",
268
+ "onMixedDirection",
269
+ "onProgress",
270
+ "request",
271
+ "restoreMangledIds",
272
+ "sanitizeIds",
273
+ "selfLoops",
274
+ "signal",
275
+ "weightDtype",
276
+ "weightFrom",
277
+ ]);
278
+
106
279
  /** The keys of ImportGraphOptions that belong to the registry, never to an importer. */
107
280
  const REGISTRY_KEYS: ReadonlySet<string> = new Set([
108
281
  "format",
@@ -115,8 +288,9 @@ const REGISTRY_KEYS: ReadonlySet<string> = new Set([
115
288
 
116
289
  /**
117
290
  * A registry of importers and exporters by format name. Registration order is the tie-break
118
- * order of sniffing (design section 8.2); the default registry lists the built-in formats in the
291
+ * order of sniffing; the default registry lists the built-in formats in the
119
292
  * order of GRAPH_FORMATS.
293
+ * @category Formats and detection
120
294
  */
121
295
  export class FormatRegistry {
122
296
  private readonly importerMap = new Map<string, GraphImporter>();
@@ -149,7 +323,7 @@ export class FormatRegistry {
149
323
  * @param format - the format name
150
324
  * @returns the importer; E_UNSUPPORTED when none is registered
151
325
  */
152
- importer(format: string): GraphImporter {
326
+ importer(format: FormatName): GraphImporter {
153
327
  const importer = this.importerMap.get(format);
154
328
  if (importer === undefined) {
155
329
  throw unknownFormat("importer", format, this.importerMap.keys());
@@ -162,7 +336,7 @@ export class FormatRegistry {
162
336
  * @param format - the format name
163
337
  * @returns the exporter; E_UNSUPPORTED when none is registered
164
338
  */
165
- exporter(format: string): GraphExporter {
339
+ exporter(format: FormatName): GraphExporter {
166
340
  const exporter = this.exporterMap.get(format);
167
341
  if (exporter === undefined) {
168
342
  throw unknownFormat("exporter", format, this.exporterMap.keys());
@@ -175,7 +349,7 @@ export class FormatRegistry {
175
349
  * @param format - the format name
176
350
  * @returns true when importer(format) would succeed
177
351
  */
178
- hasImporter(format: string): boolean {
352
+ hasImporter(format: FormatName): boolean {
179
353
  return this.importerMap.has(format);
180
354
  }
181
355
 
@@ -184,7 +358,7 @@ export class FormatRegistry {
184
358
  * @param format - the format name
185
359
  * @returns true when exporter(format) would succeed
186
360
  */
187
- hasExporter(format: string): boolean {
361
+ hasExporter(format: FormatName): boolean {
188
362
  return this.exporterMap.has(format);
189
363
  }
190
364
 
@@ -213,10 +387,10 @@ export class FormatRegistry {
213
387
  }
214
388
 
215
389
  /**
216
- * Rank the registered importers for an input (design section 8.2; the successor of
217
- * graphty-element's detectFormat()).
218
- * @param hints - the filename, MIME type and / or head of the input
219
- * @returns the candidates, best first; empty when nothing matches
390
+ * Every format that could be the input, best first, from the same hints as sniff(): use it to
391
+ * offer the user a choice when the best guess is not certain.
392
+ * @param hints - `{ filename, mimeType, head }`, any of them
393
+ * @returns the candidates, best first; empty when no format claims the input
220
394
  */
221
395
  sniffAll(hints: SniffHints): readonly SniffResult[] {
222
396
  return rankFormats(hints, this.importerMap.values());
@@ -233,40 +407,132 @@ export class FormatRegistry {
233
407
  }
234
408
 
235
409
  /**
236
- * Read an input into a fresh builder and freeze it (design section 8.4: for callers who do not
237
- * own a sink). The format is the one named in the options, else sniffed from the filename,
238
- * the MIME type and the first bytes of the content; the builder is seeded from the common
239
- * options with `directed: true` as a placeholder that the importer overrides from the file.
240
- * An input holding several graphs yields the first, with a warning naming how many were
241
- * skipped; importAllGraphs() returns every one.
410
+ * Read a graph from a string, bytes or a stream. The format is the one you name in `format`,
411
+ * else it is detected from `filename`, `mimeType` and the first bytes of the content. From an
412
+ * input that holds several graphs, it reads the one `graphIndex` or `graphName` chooses, else
413
+ * the first, with a W_MULTIPLE_GRAPHS warning; importAllGraphs() returns every one.
242
414
  * @param input - the text, bytes, stream or chunks to read
243
415
  * @param options - the format, hints, common and format-specific import options
244
- * @returns the snapshot, the import report and the freeze report
416
+ * @returns the graph (`snapshot`), the format it was read as, and the import report
245
417
  */
246
- async importGraph(input: ImportInput, options: ImportGraphOptions = {}): Promise<ImportGraphResult> {
247
- const chosen = await this.choose(input, options);
248
- const builder = seededBuilder(options, chosen.importer.format);
418
+ async importGraph(
419
+ input: ImportInput,
420
+ options: ImportGraphOptions | CommonImportOptions = {},
421
+ ): Promise<ImportGraphResult> {
422
+ const opts = options as ImportGraphOptions;
423
+ const picked = await this.choose(input, opts);
424
+ const unknown = this.unknownOptions(opts).map(
425
+ (name): ImportIssue =>
426
+ Object.freeze({
427
+ category: "unsupported",
428
+ severity: "warning",
429
+ code: UNKNOWN_OPTION_CODE,
430
+ message: unknownOptionText(name),
431
+ line: null,
432
+ element: name,
433
+ }),
434
+ );
435
+ const chosen = unknown.length === 0 ? picked : { ...picked, warnings: [...picked.warnings, ...unknown] };
436
+ const { importer } = chosen;
437
+ if (
438
+ (opts.graphIndex !== undefined || opts.graphName !== undefined) &&
439
+ importer.importAll !== undefined &&
440
+ importer.listGraphs === undefined
441
+ ) {
442
+ // an importer that reads several graphs but does not choose among them itself (DOT, GML,
443
+ // Pajek): read them all and return the chosen one, so the choice is never ignored
444
+ const all = await this.readAll(chosen, opts);
445
+ const names = all.map((r) => r.snapshot.meta.name);
446
+ return all[chooseGraph(names, opts, new ImportReportBuilder(importer.format, 0))];
447
+ }
448
+ let builder: GraphBuilder;
249
449
  let report: ImportReport;
250
450
  try {
251
- report = await chosen.importer.import(chosen.source, builder, importerOptions(options));
451
+ builder = seededBuilder(opts, chosen.importer.format);
452
+ report = await chosen.importer.import(chosen.source, builder, importerOptions(opts));
252
453
  } catch (err) {
253
454
  await chosen.peeked?.close();
254
- throw err;
455
+ throw chosen.sniff === null ? notReadableAs(err, importer.format, opts.filename) : err;
255
456
  }
256
- return result(chosen, builder, report, options);
457
+ return result(chosen, builder, report, opts);
458
+ }
459
+
460
+ /**
461
+ * Fetch a graph file from a URL and load it; see the top-level loadFromUrl() for the full
462
+ * description.
463
+ * @param url - an absolute URL, or in a browser one relative to the page
464
+ * @param options - import options, plus `request` for fetch
465
+ * @returns the graph, the format it was read as, and the import report
466
+ */
467
+ async loadFromUrl(
468
+ url: string | URL,
469
+ options: LoadFromUrlOptions | CommonImportOptions = {},
470
+ ): Promise<ImportGraphResult> {
471
+ const opts = options as LoadFromUrlOptions;
472
+ const { request, ...rest } = opts;
473
+ const signal = request?.signal ?? rest.signal ?? null;
474
+ const method = request?.method ?? "GET";
475
+ const where = `${method} ${String(url)}`;
476
+ let response: Response;
477
+ try {
478
+ response = await fetch(url, { signal: rest.signal, ...request });
479
+ } catch (err) {
480
+ if (signal?.aborted === true || isAbortError(err)) {
481
+ throw err;
482
+ }
483
+ return fetchFailed(rest, url, `${where} failed: ${fetchErrorText(url, err)}`, null, err);
484
+ }
485
+ if (!response.ok) {
486
+ await response.body?.cancel().catch(() => undefined);
487
+ const status = `${String(response.status)} ${response.statusText}`.trim();
488
+ return fetchFailed(rest, url, `${where} failed: ${status}`, response.status, null);
489
+ }
490
+ return this.importGraph(response.body ?? new Uint8Array(0), {
491
+ ...rest,
492
+ filename: rest.filename ?? urlFilename(url),
493
+ mimeType: rest.mimeType ?? response.headers.get("content-type"),
494
+ });
495
+ }
496
+
497
+ /**
498
+ * Load a graph from a File or Blob; see the top-level loadFromFile() for the full description.
499
+ * @param file - the File or Blob to read
500
+ * @param options - the same options as importGraph()
501
+ * @returns the graph, the format it was read as, and the import report
502
+ */
503
+ loadFromFile(file: Blob, options: ImportGraphOptions | CommonImportOptions = {}): Promise<ImportGraphResult> {
504
+ const opts = options as ImportGraphOptions;
505
+ const { name } = file as { name?: unknown };
506
+ return this.importGraph(file.stream(), {
507
+ ...opts,
508
+ filename: opts.filename ?? (typeof name === "string" ? name : null),
509
+ mimeType: opts.mimeType ?? (file.type === "" ? null : file.type),
510
+ });
257
511
  }
258
512
 
259
513
  /**
260
514
  * Read every graph of an input (a DOT file with several graphs, a Pajek project with several
261
- * networks, a JGF document with a `graphs` array), each into its own fresh builder, frozen.
262
- * A format whose importer has no importAll() holds one graph per input, so the result has one
263
- * entry. Options, sniffing and the builder seed are those of importGraph().
515
+ * networks, a JGF document with a `graphs` array), each as its own snapshot. A format that holds
516
+ * one graph per file gives one result. Options and detection are those of importGraph().
264
517
  * @param input - the text, bytes, stream or chunks to read
265
518
  * @param options - the format, hints, common and format-specific import options
266
519
  * @returns one result per graph, in document order
267
520
  */
268
- async importAllGraphs(input: ImportInput, options: ImportGraphOptions = {}): Promise<ImportGraphResult[]> {
269
- const chosen = await this.choose(input, options);
521
+ async importAllGraphs(
522
+ input: ImportInput,
523
+ options: ImportGraphOptions | CommonImportOptions = {},
524
+ ): Promise<ImportGraphResult[]> {
525
+ const opts = options as ImportGraphOptions;
526
+ return this.readAll(await this.choose(input, opts), opts);
527
+ }
528
+
529
+ /**
530
+ * Read every graph of an input with the chosen importer.
531
+ * @param chosen - the importer and the input to hand it
532
+ * @param options - the importGraph options
533
+ * @returns one result per graph, in document order
534
+ */
535
+ private async readAll(chosen: ChosenImporter, options: ImportGraphOptions): Promise<ImportGraphResult[]> {
270
536
  const { importer } = chosen;
271
537
  const builders: GraphBuilder[] = [];
272
538
  let reports: ImportReport[];
@@ -287,9 +553,18 @@ export class FormatRegistry {
287
553
  }
288
554
  } catch (err) {
289
555
  await chosen.peeked?.close();
290
- throw err;
556
+ throw inGraph(
557
+ chosen.sniff === null ? notReadableAs(err, importer.format, options.filename) : err,
558
+ builders.length - 1,
559
+ );
291
560
  }
292
- return reports.map((report, i) => result(chosen, builders[i], report, options));
561
+ return reports.map((report, i) => {
562
+ try {
563
+ return result(chosen, builders[i], report, options);
564
+ } catch (err) {
565
+ throw inGraph(err, i);
566
+ }
567
+ });
293
568
  }
294
569
 
295
570
  /**
@@ -301,12 +576,16 @@ export class FormatRegistry {
301
576
  * @returns one listing per graph, in document order; null when the format's importer does not
302
577
  * list its graphs (importGraph() then reads the first)
303
578
  */
304
- async listGraphs(input: ImportInput, options: ImportGraphOptions = {}): Promise<readonly GraphListing[] | null> {
305
- const chosen = await this.choose(input, options);
579
+ async listGraphs(
580
+ input: ImportInput,
581
+ options: ImportGraphOptions | CommonImportOptions = {},
582
+ ): Promise<readonly GraphListing[] | null> {
583
+ const opts = options as ImportGraphOptions;
584
+ const chosen = await this.choose(input, opts);
306
585
  try {
307
586
  return chosen.importer.listGraphs === undefined
308
587
  ? null
309
- : await chosen.importer.listGraphs(chosen.source, importerOptions(options));
588
+ : await chosen.importer.listGraphs(chosen.source, importerOptions(opts));
310
589
  } finally {
311
590
  await chosen.peeked?.close();
312
591
  }
@@ -314,37 +593,101 @@ export class FormatRegistry {
314
593
 
315
594
  /**
316
595
  * The importer for an input: the named format, or the sniffed one.
317
- * @param input - the input
596
+ * @param rawInput - the input as the caller passed it
318
597
  * @param options - the importGraph options
319
598
  * @returns the importer, the sniff, and the input to read (replayed for a stream)
320
599
  */
321
- private async choose(input: ImportInput, options: ImportGraphOptions): Promise<ChosenImporter> {
600
+ private async choose(rawInput: ImportInput, options: ImportGraphOptions): Promise<ChosenImporter> {
601
+ const input = normalizeInput(rawInput);
322
602
  const requested = options.format ?? "auto";
323
603
  if (requested !== "auto") {
324
- return { importer: this.importer(requested), sniff: null, source: input, peeked: null };
604
+ return { importer: this.importer(requested), sniff: null, source: input, peeked: null, warnings: [] };
605
+ }
606
+ // the options are checked before the input is touched, so a bad option never leaves a
607
+ // peeked stream locked; the importer resolves them again with its own defaults
608
+ const common = resolveImportOptions(options, { ids: "keep", defaultDirected: true, weightFrom: null });
609
+ maxEmptyCellsOption(options.maxEmptyCells);
610
+ const peeked = await peekHead(input, SNIFF_HEAD_BYTES, common.signal);
611
+ // the head is sniffed as the importer will read it: in the caller's encoding when given
612
+ const head =
613
+ common.encoding !== null && peeked.head instanceof Uint8Array
614
+ ? new TextDecoder(common.encoding).decode(peeked.head, { stream: true })
615
+ : peeked.head;
616
+ let sniff: SniffResult | null;
617
+ const failures = new ImportReportBuilder("unknown", 0);
618
+ const warnings: ImportIssue[] = [];
619
+ try {
620
+ const hints = { filename: options.filename, mimeType: options.mimeType, head };
621
+ const ranked = rankFormats(hints, this.importerMap.values(), (format, err) => {
622
+ warnings.push(
623
+ failures.warning(
624
+ "unsupported",
625
+ SNIFF_FAILED_CODE,
626
+ `the ${format} importer's sniff() threw (${messageOf(err)}); it was treated as not recognizing the input`,
627
+ { element: format },
628
+ ),
629
+ );
630
+ });
631
+ sniff = ranked.length > 0 ? ranked[0] : null;
632
+ } catch (err) {
633
+ await peeked.close();
634
+ throw err;
635
+ }
636
+ // a known non-graph file is refused whatever its name says, unless an importer claims its
637
+ // content (a Cytoscape session is a zip); an HTML page always (the XML sniffers would
638
+ // otherwise claim it by its markup)
639
+ const foreign = foreignKind(peeked.head);
640
+ const claimed = sniff !== null && sniff.content > 0 && !(foreign?.startsWith("an HTML") ?? false);
641
+ if (sniff === null || (foreign !== null && !claimed)) {
642
+ await peeked.close();
643
+ return this.refuseUndetected(failures, head, foreign, options);
325
644
  }
326
- const peeked = await peekHead(input, SNIFF_HEAD_BYTES, options.signal ?? null);
327
- const sniff = this.sniff({ filename: options.filename, mimeType: options.mimeType, head: peeked.head });
328
- if (sniff === null) {
329
- const report = new ImportReportBuilder("unknown", 0);
330
- return report.fail(
331
- UNKNOWN_FORMAT_CODE,
332
- `no registered importer recognises the input${describeHints(options)}; pass the format explicitly`,
333
- undefined,
334
- { formats: this.formats() },
645
+ return { importer: this.importer(sniff.format), sniff, source: peeked.input, peeked, warnings };
646
+ }
647
+
648
+ /**
649
+ * Refuse an input no importer recognizes: empty, or not in a known graph format.
650
+ * @param failures - the report the error is recorded in
651
+ * @param head - the head, as sniffed
652
+ * @param foreign - what known non-graph file the head is, or null
653
+ * @param options - the importGraph options
654
+ * @returns never: always throws
655
+ */
656
+ private refuseUndetected(
657
+ failures: ImportReportBuilder,
658
+ head: Uint8Array | string | null,
659
+ foreign: string | null,
660
+ options: ImportGraphOptions,
661
+ ): never {
662
+ const text = typeof head === "string" ? head : new TextDecoder().decode(head ?? new Uint8Array(0));
663
+ if (text.trim() === "" && text.length < SNIFF_HEAD_BYTES) {
664
+ // nothing to detect: say the input is empty, as a named format would
665
+ return failures.fail(
666
+ EMPTY_INPUT_CODE,
667
+ text === "" ? "the input is empty" : "the input holds only whitespace",
335
668
  );
336
669
  }
337
- return { importer: this.importer(sniff.format), sniff, source: peeked.input, peeked };
670
+ const what = foreign === null ? "" : `: it is ${foreign}`;
671
+ return failures.fail(
672
+ UNKNOWN_FORMAT_CODE,
673
+ `the input is not in a graph format graph-io recognizes${describeHints(options)}${what}; if you know its format, pass it as the format option`,
674
+ undefined,
675
+ { formats: this.formats() },
676
+ );
338
677
  }
339
678
 
340
679
  /**
341
- * Write a snapshot in a format, as UTF-8 chunks (design section 8.5).
680
+ * Write a snapshot in a format, as UTF-8 chunks.
342
681
  * @param snapshot - the snapshot
343
682
  * @param format - the format name
344
683
  * @param options - the exporter's common and format-specific options
345
684
  * @returns the encoded chunks
346
685
  */
347
- exportGraph(snapshot: GraphSnapshot, format: string, options?: ExportGraphOptions): AsyncIterable<Uint8Array> {
686
+ exportGraph(
687
+ snapshot: GraphSnapshot,
688
+ format: FormatName,
689
+ options?: ExportGraphOptions | CommonExportOptions,
690
+ ): AsyncIterable<Uint8Array> {
348
691
  return this.exporter(format).export(snapshot, options);
349
692
  }
350
693
 
@@ -355,10 +698,51 @@ export class FormatRegistry {
355
698
  * @param options - the exporter's common and format-specific options
356
699
  * @returns the whole document
357
700
  */
358
- async exportGraphToString(snapshot: GraphSnapshot, format: string, options?: ExportGraphOptions): Promise<string> {
701
+ async exportGraphToString(
702
+ snapshot: GraphSnapshot,
703
+ format: FormatName,
704
+ options?: ExportGraphOptions | CommonExportOptions,
705
+ ): Promise<string> {
359
706
  return this.exporter(format).exportToString(snapshot, options);
360
707
  }
361
708
 
709
+ /**
710
+ * Write a snapshot in a format as one buffer of UTF-8 bytes; see the top-level
711
+ * exportGraphToBytes().
712
+ * @param snapshot - the graph to write
713
+ * @param format - a format name where `canExport` is true
714
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
715
+ * @returns the encoded file
716
+ */
717
+ exportGraphToBytes(
718
+ snapshot: GraphSnapshot,
719
+ format: FormatName,
720
+ options?: ExportGraphOptions | CommonExportOptions,
721
+ ): Promise<Uint8Array> {
722
+ return collectBytes(this.exportGraph(snapshot, format, options));
723
+ }
724
+
725
+ /**
726
+ * Write a snapshot in a format as a Blob typed with the format's first MIME type; see the
727
+ * top-level exportGraphToBlob().
728
+ * @param snapshot - the graph to write
729
+ * @param format - a format name where `canExport` is true
730
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
731
+ * @returns the file as a Blob
732
+ */
733
+ async exportGraphToBlob(
734
+ snapshot: GraphSnapshot,
735
+ format: FormatName,
736
+ options?: ExportGraphOptions | CommonExportOptions,
737
+ ): Promise<Blob> {
738
+ const parts: Uint8Array[] = [];
739
+ for await (const chunk of this.exportGraph(snapshot, format, options)) {
740
+ parts.push(chunk);
741
+ }
742
+ const info = this.listFormats().find((f) => f.format === format);
743
+ return new Blob(parts as BlobPart[], { type: info?.mimeTypes[0] ?? "application/octet-stream" });
744
+ }
745
+
362
746
  /**
363
747
  * What exporting a snapshot in a format would lose, without writing anything.
364
748
  * @param snapshot - the snapshot
@@ -366,14 +750,112 @@ export class FormatRegistry {
366
750
  * @param options - the exporter's common and format-specific options
367
751
  * @returns the loss notes, empty when the export is exact
368
752
  */
369
- checkExport(snapshot: GraphSnapshot, format: string, options?: ExportGraphOptions): readonly LossNote[] {
370
- return this.exporter(format).check(snapshot, options);
753
+ checkExport(
754
+ snapshot: GraphSnapshot,
755
+ format: FormatName,
756
+ options?: ExportGraphOptions | CommonExportOptions,
757
+ ): readonly LossNote[] {
758
+ const notes = this.exporter(format).check(snapshot, options);
759
+ const unknown = this.unknownOptions(options);
760
+ if (unknown.length === 0) {
761
+ return notes;
762
+ }
763
+ return Object.freeze([
764
+ ...notes,
765
+ ...unknown.map((name) =>
766
+ Object.freeze({
767
+ code: UNKNOWN_OPTION_CODE,
768
+ message: unknownOptionText(name),
769
+ column: null,
770
+ count: null,
771
+ }),
772
+ ),
773
+ ]);
774
+ }
775
+
776
+ /**
777
+ * The option names no registered format and no load or save function takes (misspellings), or none while a
778
+ * registered format does not list its options.
779
+ * @param options - the caller's options
780
+ * @returns the unknown names
781
+ */
782
+ private unknownOptions(options: object | null | undefined): string[] {
783
+ if (options === null || options === undefined) {
784
+ return [];
785
+ }
786
+ const known = new Set(FUNCTION_OPTIONS);
787
+ for (const plugin of [...this.importerMap.values(), ...this.exporterMap.values()]) {
788
+ if (plugin.options === undefined) {
789
+ return [];
790
+ }
791
+ for (const name of plugin.options) {
792
+ known.add(name);
793
+ }
794
+ }
795
+ return Object.entries(options)
796
+ .filter(([name, value]) => value !== undefined && !known.has(name))
797
+ .map(([name]) => name);
798
+ }
799
+
800
+ /**
801
+ * Browser only: write a graph in a format and have the browser save it as a file; see the
802
+ * top-level downloadGraph(). Use this method on a registry from createRegistry() to download
803
+ * without bundling every built-in format.
804
+ * @param snapshot - the graph to write
805
+ * @param format - a format name from listFormats() where `canExport` is true
806
+ * @param options - the export options, plus `filename`
807
+ * @returns resolves once the download has been handed to the browser
808
+ */
809
+ async downloadGraph(
810
+ snapshot: GraphSnapshot,
811
+ format: FormatName,
812
+ options: DownloadGraphOptions | CommonExportOptions = {},
813
+ ): Promise<void> {
814
+ if (typeof document === "undefined") {
815
+ throw new GraphFormatError(
816
+ "E_UNSUPPORTED",
817
+ "downloadGraph() needs a browser document; use exportGraphToBytes() and write the bytes yourself",
818
+ );
819
+ }
820
+ const { filename, ...rest } = options as DownloadGraphOptions;
821
+ const blob = await this.exportGraphToBlob(snapshot, format, rest);
822
+ const extension = this.listFormats().find((f) => f.format === format)?.extensions[0] ?? `.${format}`;
823
+ const href = URL.createObjectURL(blob);
824
+ const anchor = document.createElement("a");
825
+ anchor.href = href;
826
+ anchor.download = filename ?? `graph${extension}`;
827
+ anchor.click();
828
+ // Safari cancels the download when the URL is revoked in the same task
829
+ setTimeout(() => {
830
+ URL.revokeObjectURL(href);
831
+ }, 0);
832
+ }
833
+
834
+ /**
835
+ * Every format this registry can read or write, in registration order; see the top-level
836
+ * listFormats().
837
+ * @returns one entry per format
838
+ */
839
+ listFormats(): readonly FormatInfo[] {
840
+ return this.formats().map((format) => {
841
+ const importer = this.importerMap.get(format);
842
+ const exporter = this.exporterMap.get(format);
843
+ return Object.freeze({
844
+ format,
845
+ extensions: importer?.extensions ?? exporter?.extensions ?? [],
846
+ mimeTypes: importer?.mimeTypes ?? exporter?.mimeTypes ?? [],
847
+ canImport: importer !== undefined,
848
+ canExport: exporter !== undefined,
849
+ capabilities: exporter?.capabilities ?? null,
850
+ });
851
+ });
371
852
  }
372
853
  }
373
854
 
374
855
  /**
375
- * A registry holding the eight built-in importers and exporters in the order of GRAPH_FORMATS.
856
+ * A new registry holding every built-in importer and exporter, in the order of GRAPH_FORMATS.
376
857
  * @returns a new registry
858
+ * @category Formats and detection
377
859
  */
378
860
  export function createRegistry(): FormatRegistry {
379
861
  return new FormatRegistry()
@@ -398,98 +880,420 @@ export function createRegistry(): FormatRegistry {
398
880
  .registerImporter(cx2Importer)
399
881
  .registerExporter(cx2Exporter)
400
882
  .registerImporter(cxImporter)
883
+ .registerExporter(cxExporter)
401
884
  .registerImporter(oboImporter)
402
- .registerImporter(cysImporter);
885
+ .registerExporter(oboExporter)
886
+ .registerImporter(cysImporter)
887
+ .registerExporter(cysExporter);
403
888
  }
404
889
 
405
- /** The default registry: every built-in format. */
406
- export const registry: FormatRegistry = createRegistry();
890
+ /**
891
+ * The default registry: every built-in format.
892
+ * @category Formats and detection
893
+ */
894
+ export const registry: FormatRegistry = /* @__PURE__ */ createRegistry();
407
895
 
408
896
  /**
409
- * Read an input into a fresh builder and freeze it, through the default registry (design section
410
- * 8.4: `importGraph(input, { format?, ...options })`).
411
- * @param input - the text, bytes, stream or chunks to read
412
- * @param options - the format, hints, common and format-specific import options
413
- * @returns the snapshot, the import report and the freeze report
897
+ * Read a graph from a string, bytes or a stream. The format is the one you name in `format`, else
898
+ * it is detected from `filename`, `mimeType` and the first bytes; an input that matches no format
899
+ * fails with E_UNKNOWN_FORMAT. From a file that holds several graphs it reads the one `graphIndex`
900
+ * or `graphName` chooses, else the first, with a W_MULTIPLE_GRAPHS warning. For a File or Blob use
901
+ * loadFromFile(), for a URL loadFromUrl().
902
+ * @param input - the whole text, the bytes (a Uint8Array or a Node Buffer), a ReadableStream of
903
+ * bytes, or an async iterable of text or byte chunks (a Node read stream)
904
+ * @param options - the format, format hints, the options every importer takes and the format's own
905
+ * options
906
+ * @returns the graph (`snapshot`), the format it was read as, and the import report
907
+ * @throws ImportError when the input could not be read; `err.issue?.code` says why
908
+ * @throws GraphFormatError with code E_UNSUPPORTED for a `format` no importer is registered for, or
909
+ * an option value that is not allowed (`err.details.option` names it)
910
+ * @throws the signal's reason when `signal` aborts
911
+ * @category Loading
414
912
  */
415
- export function importGraph(input: ImportInput, options?: ImportGraphOptions): Promise<ImportGraphResult> {
913
+ export function importGraph(
914
+ input: ImportInput,
915
+ options?: ImportGraphOptions | CommonImportOptions,
916
+ ): Promise<ImportGraphResult> {
416
917
  return registry.importGraph(input, options);
417
918
  }
418
919
 
419
920
  /**
420
- * Read every graph of an input, each into its own frozen snapshot, through the default registry.
421
- * @param input - the text, bytes, stream or chunks to read
422
- * @param options - the format, hints, common and format-specific import options
423
- * @returns one result per graph, in document order
921
+ * Read every graph of a file that can hold several (DOT, GML, a Pajek project, a JSON Graph Format
922
+ * or OBO Graphs document, CX, XGMML, a Cytoscape session). A format that holds one graph per file
923
+ * gives one result. Options and errors are the same as importGraph()'s.
924
+ * @param input - the text, bytes or stream to read
925
+ * @param options - the same options as importGraph()
926
+ * @returns one result per graph, in file order
927
+ * @category Loading
424
928
  */
425
- export function importAllGraphs(input: ImportInput, options?: ImportGraphOptions): Promise<ImportGraphResult[]> {
929
+ export function importAllGraphs(
930
+ input: ImportInput,
931
+ options?: ImportGraphOptions | CommonImportOptions,
932
+ ): Promise<ImportGraphResult[]> {
426
933
  return registry.importAllGraphs(input, options);
427
934
  }
428
935
 
429
936
  /**
430
- * The graphs of an input that can hold several, through the default registry.
431
- * @param input - the text, bytes, stream or chunks to read
432
- * @param options - the format, hints, common and format-specific import options
433
- * @returns one listing per graph, in document order; null when the format does not list its graphs
937
+ * List the graphs of a file without reading them: each one's `index`, `name`, and node and edge
938
+ * counts when the file states them. Pass the index or name to importGraph() as `graphIndex` or
939
+ * `graphName`. The counts come from the file and can differ from what an import reads, for example
940
+ * when the import adds edges the listing does not count. JSON, CX, XGMML and Cytoscape sessions can
941
+ * list their graphs; for DOT, GML and Pajek it returns null, and importAllGraphs() is the way to see
942
+ * them.
943
+ * @param input - the text, bytes or stream to read
944
+ * @param options - the same options as importGraph()
945
+ * @returns one listing per graph, in file order; null when the format cannot list its graphs
946
+ * @category Loading
434
947
  */
435
- export function listGraphs(input: ImportInput, options?: ImportGraphOptions): Promise<readonly GraphListing[] | null> {
948
+ export function listGraphs(
949
+ input: ImportInput,
950
+ options?: ImportGraphOptions | CommonImportOptions,
951
+ ): Promise<readonly GraphListing[] | null> {
436
952
  return registry.listGraphs(input, options);
437
953
  }
438
954
 
439
955
  /**
440
- * Write a snapshot in a format through the default registry, as UTF-8 chunks.
441
- * @param snapshot - the snapshot
442
- * @param format - the format name
443
- * @param options - the exporter's common and format-specific options
444
- * @returns the encoded chunks
956
+ * Write a graph in a format as a stream of UTF-8 byte chunks, for a graph too large to hold as one
957
+ * file in memory. In Node, pipe it to a file with `pipeline(exportGraph(...), createWriteStream(path))`.
958
+ * Options and errors are the same as exportGraphToBytes()'s.
959
+ * @param snapshot - the graph to write
960
+ * @param format - a format name from listFormats() where `canExport` is true
961
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
962
+ * @returns the file's bytes, chunk by chunk
963
+ * @category Saving
445
964
  */
446
965
  export function exportGraph(
447
966
  snapshot: GraphSnapshot,
448
- format: string,
449
- options?: ExportGraphOptions,
967
+ format: FormatName,
968
+ options?: ExportGraphOptions | CommonExportOptions,
450
969
  ): AsyncIterable<Uint8Array> {
451
970
  return registry.exportGraph(snapshot, format, options);
452
971
  }
453
972
 
454
973
  /**
455
- * Write a snapshot in a format through the default registry, as one string.
456
- * @param snapshot - the snapshot
457
- * @param format - the format name
458
- * @param options - the exporter's common and format-specific options
459
- * @returns the whole document
974
+ * Write a graph in a format and return the file as one string. A binary format (a Cytoscape
975
+ * session) cannot be a string and fails with E_UNSUPPORTED; use exportGraphToBytes() for it. Options
976
+ * and errors are the same as exportGraphToBytes()'s.
977
+ * @param snapshot - the graph to write
978
+ * @param format - a format name from listFormats() where `canExport` is true
979
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
980
+ * @returns the whole file
981
+ * @category Saving
460
982
  */
461
983
  export async function exportGraphToString(
462
984
  snapshot: GraphSnapshot,
463
- format: string,
464
- options?: ExportGraphOptions,
985
+ format: FormatName,
986
+ options?: ExportGraphOptions | CommonExportOptions,
465
987
  ): Promise<string> {
466
988
  return registry.exportGraphToString(snapshot, format, options);
467
989
  }
468
990
 
469
991
  /**
470
- * What exporting a snapshot in a format would lose, through the default registry.
471
- * @param snapshot - the snapshot
472
- * @param format - the format name
473
- * @param options - the exporter's common and format-specific options
474
- * @returns the loss notes, empty when the export is exact
992
+ * What saving a graph in a format would lose, without writing anything. Pass the same options you
993
+ * will pass to the save. Each note has a stable `code`: "E_" means the save would throw, "W_" means
994
+ * the file is written but that part does not read back the same. An empty array means the file
995
+ * reads back as the same graph. Runs synchronously.
996
+ * @param snapshot - the graph to check
997
+ * @param format - a format name from listFormats() where `canExport` is true
998
+ * @param options - the options you will save with
999
+ * @returns the loss notes, empty when the save is exact
1000
+ * @throws GraphFormatError with code E_UNSUPPORTED when no exporter is registered for `format`, or
1001
+ * when an option value is not allowed (`err.details.option` names it)
1002
+ * @category Saving
475
1003
  */
476
1004
  export function checkExport(
477
1005
  snapshot: GraphSnapshot,
478
- format: string,
479
- options?: ExportGraphOptions,
1006
+ format: FormatName,
1007
+ options?: ExportGraphOptions | CommonExportOptions,
480
1008
  ): readonly LossNote[] {
481
1009
  return registry.checkExport(snapshot, format, options);
482
1010
  }
483
1011
 
484
1012
  /**
485
- * Sniff an input's format through the default registry.
486
- * @param hints - the filename, MIME type and / or head of the input
487
- * @returns the best candidate, or null when no built-in importer claims it
1013
+ * Fetch a graph file from a URL and load it. The file is streamed into the importer as it
1014
+ * downloads, so a large file is not held in memory twice. When you do not name a `format`, it is
1015
+ * worked out from the URL's file name, the response's Content-Type and the first bytes of the
1016
+ * file. Your own `filename` or `mimeType` option takes the place of the hint from the URL or the
1017
+ * response. Every other option goes to importGraph() unchanged, including format-specific ones
1018
+ * such as `delimiter` or `dialect`, and `graphIndex` / `graphName` for files that hold several
1019
+ * graphs. The result is the same as importGraph()'s. A successful load can still have skipped
1020
+ * elements; check `report.errorCount` and `report.warningCount`, or pass `errorLimit: 0` to make
1021
+ * the first error fatal. Works wherever fetch exists: browsers, Node 18 and later, Deno, Bun and
1022
+ * workers.
1023
+ * @param url - an absolute URL, or in a browser one relative to the page
1024
+ * @param options - import options, plus `request` for fetch
1025
+ * @returns the graph, the format it was read as, and the import report
1026
+ * @throws ImportError when the file could not be loaded; `err.issue?.code` is "E_FETCH" for a
1027
+ * network failure, a CORS refusal or a status outside 200-299 (`err.details.url`,
1028
+ * `err.details.status` -- null for a network failure -- and `err.details.cause`, fetch's own
1029
+ * error), "E_UNKNOWN_FORMAT" when no format recognizes the file (pass `format`), or the code of
1030
+ * the parse error
1031
+ * @throws GraphFormatError with code E_UNSUPPORTED when `format` names no registered importer
1032
+ * @throws the signal's reason when `signal` aborts
1033
+ * @example
1034
+ * ```ts
1035
+ * import { GraphFormatError, loadFromUrl } from "@graphty/graph-io";
1036
+ *
1037
+ * try {
1038
+ * const { snapshot, format } = await loadFromUrl("https://example.com/data/karate.gml");
1039
+ * console.log(`${format}: ${snapshot.nodeCount} nodes, ${snapshot.edgeCount} edges`);
1040
+ * } catch (err) {
1041
+ * if (err instanceof GraphFormatError) {
1042
+ * console.error(err.message);
1043
+ * } else {
1044
+ * throw err;
1045
+ * }
1046
+ * }
1047
+ * ```
1048
+ * @category Loading
1049
+ */
1050
+ export function loadFromUrl(
1051
+ url: string | URL,
1052
+ options?: LoadFromUrlOptions | CommonImportOptions,
1053
+ ): Promise<ImportGraphResult> {
1054
+ return registry.loadFromUrl(url, options);
1055
+ }
1056
+
1057
+ /**
1058
+ * Load a graph from a File or Blob: a file the user picked in an `<input type="file">`, a file
1059
+ * dropped on the page, a Blob you built, or in Node a Blob from `fs.openAsBlob(path)`. The content
1060
+ * is streamed into the importer. A File's `name` and the Blob's `type` are used as format hints
1061
+ * unless you pass `filename` or `mimeType` yourself. Options, result and errors are the same as
1062
+ * loadFromUrl()'s, without E_FETCH.
1063
+ * @param file - the File or Blob to read
1064
+ * @param options - the same options as importGraph()
1065
+ * @returns the graph, the format it was read as, and the import report
1066
+ * @throws ImportError when the file could not be loaded (`err.issue?.code` E_UNKNOWN_FORMAT or a parse error)
1067
+ * @throws GraphFormatError with code E_UNSUPPORTED when `format` names no registered importer
1068
+ * @throws the signal's reason when `signal` aborts
1069
+ * @example
1070
+ * ```ts
1071
+ * input.addEventListener("change", async () => {
1072
+ * const file = input.files?.[0];
1073
+ * if (file) {
1074
+ * const { snapshot, report } = await loadFromFile(file);
1075
+ * console.log(`${snapshot.nodeCount} nodes, ${report.errorCount} skipped`);
1076
+ * }
1077
+ * });
1078
+ * ```
1079
+ * @category Loading
1080
+ */
1081
+ export function loadFromFile(
1082
+ file: Blob,
1083
+ options?: ImportGraphOptions | CommonImportOptions,
1084
+ ): Promise<ImportGraphResult> {
1085
+ return registry.loadFromFile(file, options);
1086
+ }
1087
+
1088
+ /**
1089
+ * Write a graph in a format and return the whole file as UTF-8 bytes, ready for fs.writeFile, a
1090
+ * fetch body or a zip entry. Prefer this over exportGraphToString() when the result goes to a file
1091
+ * or the network. To stream a very large graph, use exportGraph(). To find out beforehand what the
1092
+ * file will not keep, call checkExport() with the same format and the same options object: every
1093
+ * "E_" note it returns makes this call throw, and every "W_" note describes a loss this call
1094
+ * accepts. Most "E_" notes go away with `sanitizeIds: "mangle"` (rewrite ids the format cannot
1095
+ * hold) or `onMixedDirection: "directed"` / `"undirected"`. The format's own options (the CSV
1096
+ * `table` and `dialect`, the GEXF `version`, ...) go in the same object.
1097
+ * @param snapshot - the graph to write
1098
+ * @param format - a format name from listFormats() where `canExport` is true
1099
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
1100
+ * @returns the encoded file
1101
+ * @throws GraphFormatError with code E_UNSUPPORTED when no exporter is registered for `format`
1102
+ * @throws GraphFormatError when the graph cannot be written in this format under these options
1103
+ * (checkExport() returns an "E_" note for the same call). The thrown code names the kind of
1104
+ * failure rather than the note: E_INVALID_ID for ids (the E_ID_CHARSET and E_ID_TEXT_COLLISION
1105
+ * notes), E_DIRECTED for direction (E_MIXED_DIRECTION), and E_COLUMN_TYPE or E_UNSUPPORTED for a
1106
+ * value the format cannot write; each note's entry on the codes page says which
1107
+ * @throws GraphFormatError with code E_UNSUPPORTED for an option value that is not allowed
1108
+ * (`err.details.option` names it)
1109
+ * @example
1110
+ * ```ts
1111
+ * import { writeFile } from "node:fs/promises";
1112
+ *
1113
+ * await writeFile("out.gexf", await exportGraphToBytes(snapshot, "gexf"));
1114
+ * ```
1115
+ * @category Saving
1116
+ */
1117
+ export function exportGraphToBytes(
1118
+ snapshot: GraphSnapshot,
1119
+ format: FormatName,
1120
+ options?: ExportGraphOptions | CommonExportOptions,
1121
+ ): Promise<Uint8Array> {
1122
+ return registry.exportGraphToBytes(snapshot, format, options);
1123
+ }
1124
+
1125
+ /**
1126
+ * Write a graph in a format and return it as a Blob, for uploading with fetch or FormData or for
1127
+ * offering as a download. The Blob's `type` is the first entry of the format's `mimeTypes` in
1128
+ * listFormats(), or application/octet-stream when the format lists none. The exporter's chunks
1129
+ * are kept as they are and never joined into one string. Options and errors are the same as
1130
+ * exportGraphToBytes().
1131
+ * @param snapshot - the graph to write
1132
+ * @param format - a format name from listFormats() where `canExport` is true
1133
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
1134
+ * @returns the file as a Blob
1135
+ * @throws the same errors as exportGraphToBytes()
1136
+ * @example
1137
+ * ```ts
1138
+ * const body = new FormData();
1139
+ * body.append("file", await exportGraphToBlob(snapshot, "graphml"), "graph.graphml");
1140
+ * await fetch("/api/graphs", { method: "POST", body });
1141
+ * ```
1142
+ * @category Saving
1143
+ */
1144
+ export function exportGraphToBlob(
1145
+ snapshot: GraphSnapshot,
1146
+ format: FormatName,
1147
+ options?: ExportGraphOptions | CommonExportOptions,
1148
+ ): Promise<Blob> {
1149
+ return registry.exportGraphToBlob(snapshot, format, options);
1150
+ }
1151
+
1152
+ /**
1153
+ * Browser only: write a graph in a format and have the browser save it as a file, the same as
1154
+ * clicking a download link. Call it from a click handler; awaiting other work first is fine. The
1155
+ * promise resolves once the file has been handed to the browser. There is no way to learn
1156
+ * whether the user then canceled the save. Calling it where there is no `document` (Node, a
1157
+ * worker) throws instead of silently doing nothing; importing it is safe everywhere. Formats you
1158
+ * registered with `registry.registerExporter()` can be downloaded too.
1159
+ * @param snapshot - the graph to write
1160
+ * @param format - a format name from listFormats() where `canExport` is true
1161
+ * @param options - the export options, plus `filename`
1162
+ * @returns resolves once the download has been handed to the browser
1163
+ * @throws GraphFormatError with code E_UNSUPPORTED when called without a DOM `document`
1164
+ * @throws the same errors as exportGraphToBytes()
1165
+ * @example
1166
+ * ```ts
1167
+ * saveButton.addEventListener("click", () => {
1168
+ * void downloadGraph(snapshot, "graphml", { filename: "network.graphml" });
1169
+ * });
1170
+ * ```
1171
+ * @category Saving
1172
+ */
1173
+ export async function downloadGraph(
1174
+ snapshot: GraphSnapshot,
1175
+ format: FormatName,
1176
+ options: DownloadGraphOptions | CommonExportOptions = {},
1177
+ ): Promise<void> {
1178
+ return registry.downloadGraph(snapshot, format, options);
1179
+ }
1180
+
1181
+ /**
1182
+ * Every format the default registry can read or write, including any you registered yourself:
1183
+ * the built-in formats in the order of GRAPH_FORMATS (the order format detection prefers them),
1184
+ * then yours in registration order. Use it to fill a "Save as" menu (the entries with
1185
+ * `canExport`), a file input's `accept` attribute (the `extensions` of the entries with
1186
+ * `canImport`), or to pick the output format from a file name.
1187
+ * @returns one entry per format
1188
+ * @example
1189
+ * ```ts
1190
+ * input.accept = listFormats().filter((f) => f.canImport).flatMap((f) => f.extensions).join(",");
1191
+ * const target = listFormats().find((f) => f.canExport && f.extensions.includes(extensionOf(path) ?? ""));
1192
+ * ```
1193
+ * @category Formats and detection
1194
+ */
1195
+ export function listFormats(): readonly FormatInfo[] {
1196
+ return registry.listFormats();
1197
+ }
1198
+
1199
+ /**
1200
+ * Detect a file's format without reading it, from what you know about it: a `filename` (only the
1201
+ * extension counts), a `mimeType`, and `head`, the file's first bytes or characters (up to
1202
+ * SNIFF_HEAD_BYTES; pass `bytes.subarray(0, SNIFF_HEAD_BYTES)`). It uses every format of the default
1203
+ * registry, including the ones you registered. For every candidate, best first, call
1204
+ * `registry.sniffAll(hints)`.
1205
+ * @param hints - `{ filename, mimeType, head }`, any of them
1206
+ * @returns the best candidate (`format`, `confidence` and what matched), or null when no format
1207
+ * claims the input
1208
+ * @example
1209
+ * ```ts
1210
+ * sniff({ filename: "graph.xml", head: "<?xml version=\"1.0\"?><gexf" })?.format; // "gexf"
1211
+ * ```
1212
+ * @category Formats and detection
488
1213
  */
489
1214
  export function sniff(hints: SniffHints): SniffResult | null {
490
1215
  return registry.sniff(hints);
491
1216
  }
492
1217
 
1218
+ /**
1219
+ * Abort a loadFromUrl() whose fetch failed with an E_FETCH ImportError.
1220
+ * @param options - the import options (for the format name on the report)
1221
+ * @param url - the URL
1222
+ * @param message - the message
1223
+ * @param status - the HTTP status, or null for a network failure
1224
+ * @param cause - fetch's own error, or null
1225
+ * @returns never; always throws
1226
+ */
1227
+ function fetchFailed(
1228
+ options: ImportGraphOptions,
1229
+ url: string | URL,
1230
+ message: string,
1231
+ status: number | null,
1232
+ cause: unknown,
1233
+ ): never {
1234
+ const format = options.format === undefined || options.format === "auto" ? "unknown" : options.format;
1235
+ return new ImportReportBuilder(format, 0).fail(FETCH_CODE, message, undefined, { url: String(url), status, cause });
1236
+ }
1237
+
1238
+ /**
1239
+ * The error for an input refused by the format the caller named before any element was read: the
1240
+ * parser's message ("line 1: text outside the root element") prefixed with the file and the format,
1241
+ * since the likely cause is a file of another kind, not a damaged one.
1242
+ * @param err - what the importer threw
1243
+ * @param format - the format named in `format`
1244
+ * @param filename - the file name hint, if any
1245
+ * @returns the error to throw
1246
+ */
1247
+ function notReadableAs(err: unknown, format: string, filename: string | null | undefined): unknown {
1248
+ if (
1249
+ !(err instanceof ImportError) ||
1250
+ err.issue?.category !== "parse-error" ||
1251
+ err.report.errorCount > 1 ||
1252
+ err.report.counts.nodes + err.report.counts.edges > 0
1253
+ ) {
1254
+ return err;
1255
+ }
1256
+ const what = filename === undefined || filename === null ? "the input" : JSON.stringify(filename);
1257
+ return new ImportError(`${what} could not be read as ${format}: ${err.message}`, err.report, err.details);
1258
+ }
1259
+
1260
+ /**
1261
+ * Why fetch() rejected, in words that point at the fix: a relative URL with no page to resolve it
1262
+ * against (Node), a file: URL fetch() does not read, or a network error, which in a browser
1263
+ * includes a CORS refusal.
1264
+ * @param url - the URL as given
1265
+ * @param err - what fetch() threw
1266
+ * @returns the reason
1267
+ */
1268
+ function fetchErrorText(url: string | URL, err: unknown): string {
1269
+ let absolute: URL | null = null;
1270
+ try {
1271
+ absolute = new URL(url);
1272
+ } catch {
1273
+ // relative: resolvable only against a page's location
1274
+ }
1275
+ if (absolute === null && (globalThis as { location?: unknown }).location === undefined) {
1276
+ return "a relative URL needs a web page to resolve against; outside a browser pass an absolute http(s) URL, or read a local file with loadFromFile() or importGraph()";
1277
+ }
1278
+ if (absolute?.protocol === "file:") {
1279
+ return "fetch() cannot read file: URLs here; read a local file with loadFromFile() or importGraph()";
1280
+ }
1281
+ const cause = (err as { cause?: { message?: unknown } } | null)?.cause?.message;
1282
+ const detail = typeof cause === "string" && cause !== "" ? ` (${cause})` : "";
1283
+ return `the server could not be reached, or (in a browser) it does not allow this page to read the file (CORS)${detail}`;
1284
+ }
1285
+
1286
+ /**
1287
+ * The last path segment of a URL, as a format hint.
1288
+ * @param url - the URL, absolute or relative
1289
+ * @returns the file name, or null when the path ends in "/"
1290
+ */
1291
+ function urlFilename(url: string | URL): string | null {
1292
+ const path = new URL(url, "http://localhost/").pathname;
1293
+ const name = path.slice(path.lastIndexOf("/") + 1);
1294
+ return name === "" ? null : name;
1295
+ }
1296
+
493
1297
  /**
494
1298
  * The options handed to the importer: everything but the registry's own keys.
495
1299
  * @param options - the importGraph options
@@ -515,6 +1319,8 @@ interface ChosenImporter {
515
1319
  readonly source: ImportInput;
516
1320
  /** The peeked head of a sniffed input, or null. */
517
1321
  readonly peeked: PeekedInput | null;
1322
+ /** W_SNIFF_FAILED warnings for the import report, one per importer whose sniff() threw. */
1323
+ readonly warnings: readonly ImportIssue[];
518
1324
  }
519
1325
 
520
1326
  /**
@@ -526,6 +1332,30 @@ interface ChosenImporter {
526
1332
  */
527
1333
  function seededBuilder(options: ImportGraphOptions, format: string): GraphBuilder {
528
1334
  const maxEmptyCells = maxEmptyCellsOption(options.maxEmptyCells);
1335
+ try {
1336
+ return newBuilder(options, format, maxEmptyCells);
1337
+ } catch (err) {
1338
+ // the builder names a bad value by its field; report it as the option the caller passed
1339
+ if (err instanceof GraphFormatError && err.code === "E_UNSUPPORTED" && typeof err.details.field === "string") {
1340
+ const option = err.details.field;
1341
+ throw new GraphFormatError(
1342
+ "E_UNSUPPORTED",
1343
+ `option ${option}: ${JSON.stringify(err.details.found)} is not one of the values it takes`,
1344
+ { option, found: err.details.found },
1345
+ );
1346
+ }
1347
+ throw err;
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * The builder of seededBuilder(), before its option errors are reworded.
1353
+ * @param options - the importGraph options
1354
+ * @param format - the format being read
1355
+ * @param maxEmptyCells - the empty-cell budget
1356
+ * @returns the builder
1357
+ */
1358
+ function newBuilder(options: ImportGraphOptions, format: string, maxEmptyCells: number): GraphBuilder {
529
1359
  return new CellBudgetBuilder(
530
1360
  {
531
1361
  weightDtype: options.weightDtype ?? "f64",
@@ -540,6 +1370,21 @@ function seededBuilder(options: ImportGraphOptions, format: string): GraphBuilde
540
1370
  );
541
1371
  }
542
1372
 
1373
+ /**
1374
+ * Name the graph an importAllGraphs() failure belongs to: the report is that graph's own, so the
1375
+ * message is prefixed with its index and `details.graphIndex` set, for every graph including the
1376
+ * first. Anything other than an ImportError raised while a graph was being read passes unchanged.
1377
+ * @param err - the error thrown while reading or freezing a graph
1378
+ * @param index - the index of that graph, -1 before the first graph began
1379
+ * @returns the error to throw
1380
+ */
1381
+ function inGraph(err: unknown, index: number): unknown {
1382
+ if (!(err instanceof ImportError) || index < 0) {
1383
+ return err;
1384
+ }
1385
+ return new ImportError(`graph ${index}: ${err.message}`, err.report, { ...err.details, graphIndex: index });
1386
+ }
1387
+
543
1388
  /**
544
1389
  * Freeze an imported builder into an importGraph result.
545
1390
  * @param chosen - the importer and its sniff
@@ -554,14 +1399,144 @@ function result(
554
1399
  report: ImportReport,
555
1400
  options: ImportGraphOptions,
556
1401
  ): ImportGraphResult {
557
- const frozen = builder.freezeWithReport(options.freeze);
558
- return Object.freeze({
1402
+ let frozen: ReturnType<GraphBuilder["freezeWithReport"]>;
1403
+ try {
1404
+ frozen = builder.freezeWithReport(options.freeze);
1405
+ } catch (err) {
1406
+ throw freezeFailure(err, report);
1407
+ }
1408
+ const { warnings } = chosen;
1409
+ const result = {
559
1410
  format: chosen.importer.format,
560
1411
  sniff: chosen.sniff,
561
1412
  snapshot: frozen.snapshot,
562
- report,
563
- freeze: frozen.report,
1413
+ report: withFreezeWarnings(
1414
+ weightNotFound(
1415
+ warnings.length === 0
1416
+ ? report
1417
+ : Object.freeze({
1418
+ ...report,
1419
+ issues: Object.freeze([...warnings, ...report.issues]),
1420
+ warningCount: report.warningCount + warnings.length,
1421
+ }),
1422
+ frozen.snapshot,
1423
+ options.weightFrom,
1424
+ ),
1425
+ frozen.report,
1426
+ ),
1427
+ };
1428
+ // not enumerable: it holds one entry per edge, and console.log(result) should show the graph, not that
1429
+ Object.defineProperty(result, "freeze", { value: frozen.report, enumerable: false });
1430
+ return Object.freeze(result as ImportGraphResult);
1431
+ }
1432
+
1433
+ /**
1434
+ * The message of a W_UNKNOWN_OPTION warning or note.
1435
+ * @param name - the option name
1436
+ * @returns the message
1437
+ */
1438
+ function unknownOptionText(name: string): string {
1439
+ return `option ${JSON.stringify(name)} is not an option of any format or of the load and save functions, so it has no effect; check its spelling`;
1440
+ }
1441
+
1442
+ /**
1443
+ * The import report with one W_WEIGHT_NOT_FOUND warning when the caller's `weightFrom` named an attribute that gave
1444
+ * no edge a weight, so a name that misses (a shared options object read against GML's `value`) is not silent. A
1445
+ * format that does not read `weightFrom` already reported it as W_OPTION_IGNORED.
1446
+ * @param report - the import's report
1447
+ * @param snapshot - the graph read
1448
+ * @param weightFrom - the caller's option
1449
+ * @returns the report, with the warning added when it applies
1450
+ */
1451
+ function weightNotFound(report: ImportReport, snapshot: GraphSnapshot, weightFrom: unknown): ImportReport {
1452
+ if (
1453
+ typeof weightFrom !== "string" ||
1454
+ snapshot.edgeCount === 0 ||
1455
+ snapshot.edgeList().weights !== null ||
1456
+ report.issues.some((i) => i.code === OPTION_IGNORED_CODE && i.element === "weightFrom")
1457
+ ) {
1458
+ return report;
1459
+ }
1460
+ const issue: ImportIssue = Object.freeze({
1461
+ category: "missing-value",
1462
+ severity: "warning",
1463
+ code: WEIGHT_NOT_FOUND_CODE,
1464
+ message: `weightFrom: ${JSON.stringify(weightFrom)} names no attribute an edge has a value for, so the graph has no weights`,
1465
+ line: null,
1466
+ element: "weightFrom",
1467
+ });
1468
+ return Object.freeze({
1469
+ ...report,
1470
+ issues: Object.freeze([...report.issues, issue]),
1471
+ warningCount: report.warningCount + 1,
1472
+ });
1473
+ }
1474
+
1475
+ /**
1476
+ * The import report with what the builder's policies changed while the graph was built: one
1477
+ * W_EDGES_MERGED warning when `duplicateEdges` merged parallel edges (whatever told them apart, a
1478
+ * relation or a label, is lost) and one W_SELF_LOOPS_DROPPED warning when `selfLoops: "drop"`
1479
+ * removed self-loops, so nothing the caller's options removed goes unreported.
1480
+ * @param report - the import's report
1481
+ * @param freeze - the freeze report
1482
+ * @returns the report, with the warnings added
1483
+ */
1484
+ function withFreezeWarnings(report: ImportReport, freeze: FreezeReport): ImportReport {
1485
+ const added: ImportIssue[] = [];
1486
+ const warn = (category: IssueCategory, code: string, message: string): void => {
1487
+ added.push(Object.freeze({ category, severity: "warning" as const, code, message, line: null, element: null }));
1488
+ };
1489
+ if (freeze.mergedEdges > 0) {
1490
+ warn(
1491
+ "merged",
1492
+ EDGES_MERGED_CODE,
1493
+ `${freeze.mergedEdges} parallel edge${plural(freeze.mergedEdges)} ${agree(freeze.mergedEdges, "was", "were")} merged into one edge per pair by the duplicateEdges policy; whatever told them apart (a relation, a label) is lost`,
1494
+ );
1495
+ }
1496
+ if (freeze.droppedSelfLoops > 0) {
1497
+ warn(
1498
+ "validation-error",
1499
+ SELF_LOOPS_DROPPED_CODE,
1500
+ `${freeze.droppedSelfLoops} self-loop${plural(freeze.droppedSelfLoops)} ${agree(freeze.droppedSelfLoops, "was", "were")} removed by selfLoops: "drop"`,
1501
+ );
1502
+ }
1503
+ if (added.length === 0) {
1504
+ return report;
1505
+ }
1506
+ return Object.freeze({
1507
+ ...report,
1508
+ issues: Object.freeze([...report.issues, ...added]),
1509
+ warningCount: report.warningCount + added.length,
1510
+ });
1511
+ }
1512
+
1513
+ /**
1514
+ * The ImportError of a freeze the builder refused (a `duplicateEdges: "error"` or `selfLoops:
1515
+ * "error"` policy meeting a duplicate edge or a self-loop the file holds): the builder's code
1516
+ * recorded as an error issue on the import's report, so the caller gets the report and the file's
1517
+ * defect rather than a bare GraphFormatError.
1518
+ * @param err - what freezeWithReport() threw
1519
+ * @param report - the import's report
1520
+ * @returns the error to throw (anything that is not a GraphFormatError, unchanged)
1521
+ */
1522
+ function freezeFailure(err: unknown, report: ImportReport): unknown {
1523
+ if (!(err instanceof GraphFormatError) || err instanceof ImportError) {
1524
+ return err;
1525
+ }
1526
+ const issue = Object.freeze({
1527
+ category: "validation-error" as const,
1528
+ severity: "error" as const,
1529
+ code: err.code,
1530
+ message: err.message,
1531
+ line: null,
1532
+ element: null,
564
1533
  });
1534
+ const failed: ImportReport = Object.freeze({
1535
+ ...report,
1536
+ issues: Object.freeze([...report.issues, issue]),
1537
+ errorCount: report.errorCount + 1,
1538
+ });
1539
+ return new ImportError(`the graph cannot be frozen: ${err.message}`, failed, { code: err.code, ...err.details });
565
1540
  }
566
1541
 
567
1542
  /**
@@ -604,7 +1579,7 @@ interface PeekedInput {
604
1579
  readonly input: ImportInput;
605
1580
  /**
606
1581
  * Close the source when the importer never iterated the replaying input (it threw first, an
607
- * abort for instance): a stream's reader is cancelled and released, an async generator finalised.
1582
+ * abort for instance): a stream's reader is canceled and released, an async generator finalised.
608
1583
  * @returns when the source is closed
609
1584
  */
610
1585
  close(): Promise<void>;
@@ -630,7 +1605,7 @@ async function peekHead(input: ImportInput, bytes: number, signal: AbortSignal |
630
1605
  throwIfAborted(signal);
631
1606
  const source: AsyncIterator<string | Uint8Array> =
632
1607
  typeof (input as { getReader?: unknown }).getReader === "function"
633
- ? readerIterator(input as ReadableStream<Uint8Array>)
1608
+ ? readerIterator(input as ReadableStream<Uint8Array>, signal)
634
1609
  : (input as AsyncIterable<string | Uint8Array>)[Symbol.asyncIterator]();
635
1610
  const buffered: (string | Uint8Array)[] = [];
636
1611
  let size = 0;
@@ -742,19 +1717,30 @@ function replay(
742
1717
  }
743
1718
 
744
1719
  /**
745
- * An async iterator over a ReadableStream's chunks that cancels the stream when closed early.
1720
+ * An async iterator over a ReadableStream's chunks that cancels the stream (with the signal's
1721
+ * reason) when closed early or when the signal fires during a read, and releases the reader when
1722
+ * the stream ends or errors.
746
1723
  * @param stream - the stream
1724
+ * @param signal - the cancellation signal, or null
747
1725
  * @returns the iterator
748
1726
  */
749
- function readerIterator(stream: ReadableStream<Uint8Array>): AsyncIterator<Uint8Array> {
750
- const reader = stream.getReader();
1727
+ function readerIterator(stream: ReadableStream<Uint8Array>, signal: AbortSignal | null): AsyncIterator<Uint8Array> {
1728
+ const reader = lockReader(stream);
751
1729
  let done = false;
752
1730
  return {
753
1731
  async next(): Promise<IteratorResult<Uint8Array>> {
754
1732
  if (done) {
755
1733
  return { done: true, value: undefined };
756
1734
  }
757
- const result = await reader.read();
1735
+ let result: ReadableStreamReadResult<Uint8Array>;
1736
+ try {
1737
+ result = await abortable(reader.read(), signal);
1738
+ } catch (err) {
1739
+ done = true;
1740
+ await reader.cancel(signal?.reason).catch(() => undefined);
1741
+ reader.releaseLock();
1742
+ throw err;
1743
+ }
758
1744
  if (result.done) {
759
1745
  done = true;
760
1746
  reader.releaseLock();
@@ -765,7 +1751,7 @@ function readerIterator(stream: ReadableStream<Uint8Array>): AsyncIterator<Uint8
765
1751
  async return(): Promise<IteratorResult<Uint8Array>> {
766
1752
  if (!done) {
767
1753
  done = true;
768
- await reader.cancel().catch(() => undefined);
1754
+ await reader.cancel(signal?.reason).catch(() => undefined);
769
1755
  reader.releaseLock();
770
1756
  }
771
1757
  return { done: true, value: undefined };