@graphty/graph-io 0.3.25 → 0.3.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/README.md +49 -414
  2. package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
  3. package/dist/chunks/choose-DrXtu_96.js +56 -0
  4. package/dist/chunks/choose-DrXtu_96.js.map +1 -0
  5. package/dist/chunks/constants-ikG85dT8.js +231 -0
  6. package/dist/chunks/constants-ikG85dT8.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D1HPTUrP.js} +390 -99
  8. package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
  9. package/dist/chunks/export-CHd4tQE2.js +4333 -0
  10. package/dist/chunks/export-CHd4tQE2.js.map +1 -0
  11. package/dist/chunks/{importer-eUREhBGj.js → exporter-D23CKNqF.js} +4969 -3201
  12. package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
  13. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-DfWTgrti.js} +23 -21
  14. package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
  15. package/dist/chunks/exporter-DhtW6FxK.js +3465 -0
  16. package/dist/chunks/exporter-DhtW6FxK.js.map +1 -0
  17. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-IHv4nkCm.js} +38 -229
  18. package/dist/chunks/exporter-IHv4nkCm.js.map +1 -0
  19. package/dist/chunks/{importer-BWY2FFc5.js → importer-B1wmPQjr.js} +255 -77
  20. package/dist/chunks/importer-B1wmPQjr.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-B2W_58pz.js} +399 -146
  22. package/dist/chunks/importer-B2W_58pz.js.map +1 -0
  23. package/dist/chunks/importer-BSALvkui.js +2811 -0
  24. package/dist/chunks/importer-BSALvkui.js.map +1 -0
  25. package/dist/chunks/{importer-vi3bSdtw.js → importer-BVRE--Un.js} +510 -77
  26. package/dist/chunks/importer-BVRE--Un.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-CIAzt-wo.js} +1689 -312
  28. package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
  29. package/dist/chunks/{importer-DIrbbnAf.js → importer-DpEW5zeG.js} +585 -226
  30. package/dist/chunks/importer-DpEW5zeG.js.map +1 -0
  31. package/dist/chunks/{importer-zGLo8Dg_.js → importer-z_FL4VMb.js} +488 -161
  32. package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
  33. package/dist/chunks/json-elements-BAmNBZok.js +1225 -0
  34. package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
  35. package/dist/chunks/ontology-export-Bab4eUA8.js +357 -0
  36. package/dist/chunks/ontology-export-Bab4eUA8.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CdlvL4RO.js} +298 -114
  38. package/dist/chunks/records-CdlvL4RO.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DzXjAiTl.js} +29 -11
  40. package/dist/chunks/text-DzXjAiTl.js.map +1 -0
  41. package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
  42. package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
  43. package/dist/csv.js +952 -262
  44. package/dist/csv.js.map +1 -1
  45. package/dist/cx.js +6 -3
  46. package/dist/cx2.js +2 -2
  47. package/dist/cys.js +6 -3
  48. package/dist/dot.js +1 -1
  49. package/dist/gexf.js +554 -122
  50. package/dist/gexf.js.map +1 -1
  51. package/dist/gml.js +344 -87
  52. package/dist/gml.js.map +1 -1
  53. package/dist/graph-io.js +1105 -213
  54. package/dist/graph-io.js.map +1 -1
  55. package/dist/graphml.js +3 -2
  56. package/dist/graphml.js.map +1 -1
  57. package/dist/json.js +3 -2
  58. package/dist/neo4j.js +565 -158
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +6 -3
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/children.d.ts +18 -6
  63. package/dist/src/children.d.ts.map +1 -1
  64. package/dist/src/children.js +6 -3
  65. package/dist/src/children.js.map +1 -1
  66. package/dist/src/common/attributes.d.ts +29 -11
  67. package/dist/src/common/attributes.d.ts.map +1 -1
  68. package/dist/src/common/attributes.js +25 -7
  69. package/dist/src/common/attributes.js.map +1 -1
  70. package/dist/src/common/cell-budget.d.ts +9 -2
  71. package/dist/src/common/cell-budget.d.ts.map +1 -1
  72. package/dist/src/common/cell-budget.js +11 -3
  73. package/dist/src/common/cell-budget.js.map +1 -1
  74. package/dist/src/common/choose.d.ts +19 -0
  75. package/dist/src/common/choose.d.ts.map +1 -0
  76. package/dist/src/common/choose.js +72 -0
  77. package/dist/src/common/choose.js.map +1 -0
  78. package/dist/src/common/codes.d.ts +456 -66
  79. package/dist/src/common/codes.d.ts.map +1 -1
  80. package/dist/src/common/codes.js +458 -66
  81. package/dist/src/common/codes.js.map +1 -1
  82. package/dist/src/common/compare.d.ts +81 -0
  83. package/dist/src/common/compare.d.ts.map +1 -0
  84. package/dist/src/common/compare.js +394 -0
  85. package/dist/src/common/compare.js.map +1 -0
  86. package/dist/src/common/cx-export.d.ts +123 -0
  87. package/dist/src/common/cx-export.d.ts.map +1 -0
  88. package/dist/src/common/cx-export.js +334 -0
  89. package/dist/src/common/cx-export.js.map +1 -0
  90. package/dist/src/common/declared-types.d.ts +22 -2
  91. package/dist/src/common/declared-types.d.ts.map +1 -1
  92. package/dist/src/common/declared-types.js +21 -3
  93. package/dist/src/common/declared-types.js.map +1 -1
  94. package/dist/src/common/direction.d.ts +74 -50
  95. package/dist/src/common/direction.d.ts.map +1 -1
  96. package/dist/src/common/direction.js +47 -27
  97. package/dist/src/common/direction.js.map +1 -1
  98. package/dist/src/common/escape.d.ts +21 -9
  99. package/dist/src/common/escape.d.ts.map +1 -1
  100. package/dist/src/common/escape.js +39 -22
  101. package/dist/src/common/escape.js.map +1 -1
  102. package/dist/src/common/export.d.ts +144 -32
  103. package/dist/src/common/export.d.ts.map +1 -1
  104. package/dist/src/common/export.js +216 -42
  105. package/dist/src/common/export.js.map +1 -1
  106. package/dist/src/common/format.d.ts +23 -1
  107. package/dist/src/common/format.d.ts.map +1 -1
  108. package/dist/src/common/format.js +26 -1
  109. package/dist/src/common/format.js.map +1 -1
  110. package/dist/src/common/ids.d.ts +9 -2
  111. package/dist/src/common/ids.d.ts.map +1 -1
  112. package/dist/src/common/ids.js +6 -2
  113. package/dist/src/common/ids.js.map +1 -1
  114. package/dist/src/common/input.d.ts +122 -28
  115. package/dist/src/common/input.d.ts.map +1 -1
  116. package/dist/src/common/input.js +890 -124
  117. package/dist/src/common/input.js.map +1 -1
  118. package/dist/src/common/json-elements.d.ts +144 -20
  119. package/dist/src/common/json-elements.d.ts.map +1 -1
  120. package/dist/src/common/json-elements.js +869 -189
  121. package/dist/src/common/json-elements.js.map +1 -1
  122. package/dist/src/common/line-format.d.ts +90 -0
  123. package/dist/src/common/line-format.d.ts.map +1 -0
  124. package/dist/src/common/line-format.js +185 -0
  125. package/dist/src/common/line-format.js.map +1 -0
  126. package/dist/src/common/lists.d.ts +6 -1
  127. package/dist/src/common/lists.d.ts.map +1 -1
  128. package/dist/src/common/lists.js +3 -1
  129. package/dist/src/common/lists.js.map +1 -1
  130. package/dist/src/common/ontology-export.d.ts +167 -0
  131. package/dist/src/common/ontology-export.d.ts.map +1 -0
  132. package/dist/src/common/ontology-export.js +359 -0
  133. package/dist/src/common/ontology-export.js.map +1 -0
  134. package/dist/src/common/ontology.d.ts +25 -3
  135. package/dist/src/common/ontology.d.ts.map +1 -1
  136. package/dist/src/common/ontology.js +30 -9
  137. package/dist/src/common/ontology.js.map +1 -1
  138. package/dist/src/common/options.d.ts +62 -12
  139. package/dist/src/common/options.d.ts.map +1 -1
  140. package/dist/src/common/options.js +89 -11
  141. package/dist/src/common/options.js.map +1 -1
  142. package/dist/src/common/plural.d.ts +31 -0
  143. package/dist/src/common/plural.d.ts.map +1 -0
  144. package/dist/src/common/plural.js +46 -0
  145. package/dist/src/common/plural.js.map +1 -0
  146. package/dist/src/common/report.d.ts +72 -14
  147. package/dist/src/common/report.d.ts.map +1 -1
  148. package/dist/src/common/report.js +146 -22
  149. package/dist/src/common/report.js.map +1 -1
  150. package/dist/src/common/temporal.d.ts +22 -5
  151. package/dist/src/common/temporal.d.ts.map +1 -1
  152. package/dist/src/common/temporal.js +13 -2
  153. package/dist/src/common/temporal.js.map +1 -1
  154. package/dist/src/common/text.d.ts +39 -23
  155. package/dist/src/common/text.d.ts.map +1 -1
  156. package/dist/src/common/text.js +51 -21
  157. package/dist/src/common/text.js.map +1 -1
  158. package/dist/src/common/weights.d.ts +37 -4
  159. package/dist/src/common/weights.d.ts.map +1 -1
  160. package/dist/src/common/weights.js +61 -2
  161. package/dist/src/common/weights.js.map +1 -1
  162. package/dist/src/common/writer.d.ts +19 -3
  163. package/dist/src/common/writer.d.ts.map +1 -1
  164. package/dist/src/common/writer.js +30 -2
  165. package/dist/src/common/writer.js.map +1 -1
  166. package/dist/src/common/xml.d.ts +125 -9
  167. package/dist/src/common/xml.d.ts.map +1 -1
  168. package/dist/src/common/xml.js +402 -85
  169. package/dist/src/common/xml.js.map +1 -1
  170. package/dist/src/common/zip.d.ts +44 -4
  171. package/dist/src/common/zip.d.ts.map +1 -1
  172. package/dist/src/common/zip.js +134 -5
  173. package/dist/src/common/zip.js.map +1 -1
  174. package/dist/src/formats/csv/exporter.d.ts +80 -24
  175. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  176. package/dist/src/formats/csv/exporter.js +146 -33
  177. package/dist/src/formats/csv/exporter.js.map +1 -1
  178. package/dist/src/formats/csv/header.d.ts +38 -9
  179. package/dist/src/formats/csv/header.d.ts.map +1 -1
  180. package/dist/src/formats/csv/header.js +46 -10
  181. package/dist/src/formats/csv/header.js.map +1 -1
  182. package/dist/src/formats/csv/importer.d.ts +187 -37
  183. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  184. package/dist/src/formats/csv/importer.js +835 -204
  185. package/dist/src/formats/csv/importer.js.map +1 -1
  186. package/dist/src/formats/csv/index.d.ts +100 -19
  187. package/dist/src/formats/csv/index.d.ts.map +1 -1
  188. package/dist/src/formats/csv/index.js +94 -21
  189. package/dist/src/formats/csv/index.js.map +1 -1
  190. package/dist/src/formats/csv/records.d.ts +95 -10
  191. package/dist/src/formats/csv/records.d.ts.map +1 -1
  192. package/dist/src/formats/csv/records.js +333 -122
  193. package/dist/src/formats/csv/records.js.map +1 -1
  194. package/dist/src/formats/csv/values.d.ts +1 -0
  195. package/dist/src/formats/csv/values.d.ts.map +1 -1
  196. package/dist/src/formats/csv/values.js +1 -0
  197. package/dist/src/formats/csv/values.js.map +1 -1
  198. package/dist/src/formats/cx/exporter.d.ts +102 -0
  199. package/dist/src/formats/cx/exporter.d.ts.map +1 -0
  200. package/dist/src/formats/cx/exporter.js +1217 -0
  201. package/dist/src/formats/cx/exporter.js.map +1 -0
  202. package/dist/src/formats/cx/importer.d.ts +79 -32
  203. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  204. package/dist/src/formats/cx/importer.js +682 -287
  205. package/dist/src/formats/cx/importer.js.map +1 -1
  206. package/dist/src/formats/cx/index.d.ts +4 -3
  207. package/dist/src/formats/cx/index.d.ts.map +1 -1
  208. package/dist/src/formats/cx/index.js +4 -3
  209. package/dist/src/formats/cx/index.js.map +1 -1
  210. package/dist/src/formats/cx2/exporter.d.ts +29 -11
  211. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  212. package/dist/src/formats/cx2/exporter.js +39 -268
  213. package/dist/src/formats/cx2/exporter.js.map +1 -1
  214. package/dist/src/formats/cx2/importer.d.ts +78 -28
  215. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  216. package/dist/src/formats/cx2/importer.js +227 -80
  217. package/dist/src/formats/cx2/importer.js.map +1 -1
  218. package/dist/src/formats/cx2/index.d.ts +2 -1
  219. package/dist/src/formats/cx2/index.d.ts.map +1 -1
  220. package/dist/src/formats/cx2/index.js +2 -1
  221. package/dist/src/formats/cx2/index.js.map +1 -1
  222. package/dist/src/formats/cys/constants.d.ts +66 -17
  223. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  224. package/dist/src/formats/cys/constants.js +59 -18
  225. package/dist/src/formats/cys/constants.js.map +1 -1
  226. package/dist/src/formats/cys/exporter.d.ts +118 -0
  227. package/dist/src/formats/cys/exporter.d.ts.map +1 -0
  228. package/dist/src/formats/cys/exporter.js +1072 -0
  229. package/dist/src/formats/cys/exporter.js.map +1 -0
  230. package/dist/src/formats/cys/importer.d.ts +22 -8
  231. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  232. package/dist/src/formats/cys/importer.js +510 -149
  233. package/dist/src/formats/cys/importer.js.map +1 -1
  234. package/dist/src/formats/cys/index.d.ts +4 -2
  235. package/dist/src/formats/cys/index.d.ts.map +1 -1
  236. package/dist/src/formats/cys/index.js +4 -2
  237. package/dist/src/formats/cys/index.js.map +1 -1
  238. package/dist/src/formats/cys/session.d.ts +46 -7
  239. package/dist/src/formats/cys/session.d.ts.map +1 -1
  240. package/dist/src/formats/cys/session.js +101 -27
  241. package/dist/src/formats/cys/session.js.map +1 -1
  242. package/dist/src/formats/cys/tables.d.ts +9 -2
  243. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  244. package/dist/src/formats/cys/tables.js +29 -7
  245. package/dist/src/formats/cys/tables.js.map +1 -1
  246. package/dist/src/formats/dot/exporter.d.ts +42 -14
  247. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  248. package/dist/src/formats/dot/exporter.js +56 -29
  249. package/dist/src/formats/dot/exporter.js.map +1 -1
  250. package/dist/src/formats/dot/importer.d.ts +91 -29
  251. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  252. package/dist/src/formats/dot/importer.js +190 -39
  253. package/dist/src/formats/dot/importer.js.map +1 -1
  254. package/dist/src/formats/dot/index.d.ts +2 -1
  255. package/dist/src/formats/dot/index.d.ts.map +1 -1
  256. package/dist/src/formats/dot/index.js +2 -1
  257. package/dist/src/formats/dot/index.js.map +1 -1
  258. package/dist/src/formats/dot/names.d.ts +44 -11
  259. package/dist/src/formats/dot/names.d.ts.map +1 -1
  260. package/dist/src/formats/dot/names.js +44 -11
  261. package/dist/src/formats/dot/names.js.map +1 -1
  262. package/dist/src/formats/dot/tokenizer.d.ts +9 -2
  263. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  264. package/dist/src/formats/dot/tokenizer.js +20 -5
  265. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  266. package/dist/src/formats/gexf/exporter.d.ts +46 -15
  267. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  268. package/dist/src/formats/gexf/exporter.js +45 -15
  269. package/dist/src/formats/gexf/exporter.js.map +1 -1
  270. package/dist/src/formats/gexf/importer.d.ts +87 -23
  271. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  272. package/dist/src/formats/gexf/importer.js +471 -90
  273. package/dist/src/formats/gexf/importer.js.map +1 -1
  274. package/dist/src/formats/gexf/index.d.ts +84 -21
  275. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  276. package/dist/src/formats/gexf/index.js +80 -23
  277. package/dist/src/formats/gexf/index.js.map +1 -1
  278. package/dist/src/formats/gexf/schema.d.ts +67 -15
  279. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  280. package/dist/src/formats/gexf/schema.js +59 -13
  281. package/dist/src/formats/gexf/schema.js.map +1 -1
  282. package/dist/src/formats/gml/exporter.d.ts +69 -21
  283. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/gml/exporter.js +74 -29
  285. package/dist/src/formats/gml/exporter.js.map +1 -1
  286. package/dist/src/formats/gml/importer.d.ts +116 -25
  287. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  288. package/dist/src/formats/gml/importer.js +290 -61
  289. package/dist/src/formats/gml/importer.js.map +1 -1
  290. package/dist/src/formats/gml/index.d.ts +87 -24
  291. package/dist/src/formats/gml/index.d.ts.map +1 -1
  292. package/dist/src/formats/gml/index.js +79 -26
  293. package/dist/src/formats/gml/index.js.map +1 -1
  294. package/dist/src/formats/gml/syntax.d.ts +67 -18
  295. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  296. package/dist/src/formats/gml/syntax.js +110 -28
  297. package/dist/src/formats/gml/syntax.js.map +1 -1
  298. package/dist/src/formats/graphml/constants.d.ts +174 -44
  299. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  300. package/dist/src/formats/graphml/constants.js +165 -44
  301. package/dist/src/formats/graphml/constants.js.map +1 -1
  302. package/dist/src/formats/graphml/exporter.d.ts +17 -9
  303. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  304. package/dist/src/formats/graphml/exporter.js +33 -12
  305. package/dist/src/formats/graphml/exporter.js.map +1 -1
  306. package/dist/src/formats/graphml/importer.d.ts +19 -7
  307. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  308. package/dist/src/formats/graphml/importer.js +466 -39
  309. package/dist/src/formats/graphml/importer.js.map +1 -1
  310. package/dist/src/formats/graphml/index.d.ts +2 -1
  311. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  312. package/dist/src/formats/graphml/index.js +2 -1
  313. package/dist/src/formats/graphml/index.js.map +1 -1
  314. package/dist/src/formats/graphml/tree.d.ts +6 -2
  315. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  316. package/dist/src/formats/graphml/tree.js +6 -2
  317. package/dist/src/formats/graphml/tree.js.map +1 -1
  318. package/dist/src/formats/graphml/yfiles.d.ts +8 -3
  319. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  320. package/dist/src/formats/graphml/yfiles.js +36 -15
  321. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  322. package/dist/src/formats/json/dialect.d.ts +88 -24
  323. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  324. package/dist/src/formats/json/dialect.js +109 -19
  325. package/dist/src/formats/json/dialect.js.map +1 -1
  326. package/dist/src/formats/json/exporter.d.ts +134 -22
  327. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  328. package/dist/src/formats/json/exporter.js +135 -54
  329. package/dist/src/formats/json/exporter.js.map +1 -1
  330. package/dist/src/formats/json/importer.d.ts +173 -52
  331. package/dist/src/formats/json/importer.d.ts.map +1 -1
  332. package/dist/src/formats/json/importer.js +899 -220
  333. package/dist/src/formats/json/importer.js.map +1 -1
  334. package/dist/src/formats/json/index.d.ts +3 -2
  335. package/dist/src/formats/json/index.d.ts.map +1 -1
  336. package/dist/src/formats/json/index.js +3 -2
  337. package/dist/src/formats/json/index.js.map +1 -1
  338. package/dist/src/formats/json/obographs-export.d.ts +68 -0
  339. package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
  340. package/dist/src/formats/json/obographs-export.js +941 -0
  341. package/dist/src/formats/json/obographs-export.js.map +1 -0
  342. package/dist/src/formats/json/obographs.d.ts +26 -0
  343. package/dist/src/formats/json/obographs.d.ts.map +1 -1
  344. package/dist/src/formats/json/obographs.js +297 -80
  345. package/dist/src/formats/json/obographs.js.map +1 -1
  346. package/dist/src/formats/neo4j/exporter.d.ts +109 -21
  347. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  348. package/dist/src/formats/neo4j/exporter.js +239 -86
  349. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  350. package/dist/src/formats/neo4j/header.d.ts +17 -4
  351. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  352. package/dist/src/formats/neo4j/header.js +36 -4
  353. package/dist/src/formats/neo4j/header.js.map +1 -1
  354. package/dist/src/formats/neo4j/importer.d.ts +107 -27
  355. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  356. package/dist/src/formats/neo4j/importer.js +415 -63
  357. package/dist/src/formats/neo4j/importer.js.map +1 -1
  358. package/dist/src/formats/neo4j/index.d.ts +73 -23
  359. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  360. package/dist/src/formats/neo4j/index.js +65 -25
  361. package/dist/src/formats/neo4j/index.js.map +1 -1
  362. package/dist/src/formats/obo/exporter.d.ts +150 -0
  363. package/dist/src/formats/obo/exporter.d.ts.map +1 -0
  364. package/dist/src/formats/obo/exporter.js +1736 -0
  365. package/dist/src/formats/obo/exporter.js.map +1 -0
  366. package/dist/src/formats/obo/importer.d.ts +95 -19
  367. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  368. package/dist/src/formats/obo/importer.js +475 -104
  369. package/dist/src/formats/obo/importer.js.map +1 -1
  370. package/dist/src/formats/obo/index.d.ts +6 -3
  371. package/dist/src/formats/obo/index.d.ts.map +1 -1
  372. package/dist/src/formats/obo/index.js +6 -3
  373. package/dist/src/formats/obo/index.js.map +1 -1
  374. package/dist/src/formats/obo/syntax.d.ts +108 -4
  375. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  376. package/dist/src/formats/obo/syntax.js +211 -34
  377. package/dist/src/formats/obo/syntax.js.map +1 -1
  378. package/dist/src/formats/pajek/exporter.d.ts +54 -16
  379. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  380. package/dist/src/formats/pajek/exporter.js +67 -35
  381. package/dist/src/formats/pajek/exporter.js.map +1 -1
  382. package/dist/src/formats/pajek/importer.d.ts +84 -22
  383. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  384. package/dist/src/formats/pajek/importer.js +303 -83
  385. package/dist/src/formats/pajek/importer.js.map +1 -1
  386. package/dist/src/formats/pajek/index.d.ts +2 -1
  387. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  388. package/dist/src/formats/pajek/index.js +2 -1
  389. package/dist/src/formats/pajek/index.js.map +1 -1
  390. package/dist/src/formats/pajek/syntax.d.ts +77 -14
  391. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  392. package/dist/src/formats/pajek/syntax.js +186 -54
  393. package/dist/src/formats/pajek/syntax.js.map +1 -1
  394. package/dist/src/formats/xgmml/columns.d.ts +1 -0
  395. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  396. package/dist/src/formats/xgmml/columns.js +4 -0
  397. package/dist/src/formats/xgmml/columns.js.map +1 -1
  398. package/dist/src/formats/xgmml/constants.d.ts +186 -56
  399. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  400. package/dist/src/formats/xgmml/constants.js +181 -57
  401. package/dist/src/formats/xgmml/constants.js.map +1 -1
  402. package/dist/src/formats/xgmml/document.d.ts +65 -11
  403. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  404. package/dist/src/formats/xgmml/document.js +157 -28
  405. package/dist/src/formats/xgmml/document.js.map +1 -1
  406. package/dist/src/formats/xgmml/emit.d.ts +61 -3
  407. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  408. package/dist/src/formats/xgmml/emit.js +129 -26
  409. package/dist/src/formats/xgmml/emit.js.map +1 -1
  410. package/dist/src/formats/xgmml/exporter.d.ts +13 -6
  411. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  412. package/dist/src/formats/xgmml/exporter.js +21 -22
  413. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  414. package/dist/src/formats/xgmml/importer.d.ts +33 -12
  415. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  416. package/dist/src/formats/xgmml/importer.js +35 -72
  417. package/dist/src/formats/xgmml/importer.js.map +1 -1
  418. package/dist/src/formats/xgmml/index.d.ts +1 -0
  419. package/dist/src/formats/xgmml/index.d.ts.map +1 -1
  420. package/dist/src/formats/xgmml/index.js +1 -0
  421. package/dist/src/formats/xgmml/index.js.map +1 -1
  422. package/dist/src/formats/xgmml/values.d.ts +8 -1
  423. package/dist/src/formats/xgmml/values.d.ts.map +1 -1
  424. package/dist/src/formats/xgmml/values.js +8 -3
  425. package/dist/src/formats/xgmml/values.js.map +1 -1
  426. package/dist/src/index.d.ts +23 -18
  427. package/dist/src/index.d.ts.map +1 -1
  428. package/dist/src/index.js +28 -16
  429. package/dist/src/index.js.map +1 -1
  430. package/dist/src/registry.d.ts +478 -92
  431. package/dist/src/registry.d.ts.map +1 -1
  432. package/dist/src/registry.js +807 -88
  433. package/dist/src/registry.js.map +1 -1
  434. package/dist/src/sniff.d.ts +61 -19
  435. package/dist/src/sniff.d.ts.map +1 -1
  436. package/dist/src/sniff.js +121 -29
  437. package/dist/src/sniff.js.map +1 -1
  438. package/dist/src/types.d.ts +362 -112
  439. package/dist/src/types.d.ts.map +1 -1
  440. package/dist/src/types.js +28 -7
  441. package/dist/src/types.js.map +1 -1
  442. package/dist/xgmml.js +2 -2
  443. package/docs/samples/README.md +25 -0
  444. package/docs/samples/got-edges.csv +353 -0
  445. package/docs/samples/got-network.graphml +1384 -0
  446. package/docs/samples/got-nodes.csv +108 -0
  447. package/docs/samples/got.cx +930 -0
  448. package/docs/samples/got.cx2 +465 -0
  449. package/docs/samples/got.gexf +471 -0
  450. package/docs/samples/got.gml +2650 -0
  451. package/docs/samples/got.json +2546 -0
  452. package/docs/samples/got.net +461 -0
  453. package/docs/samples/movies-nodes.csv +9 -0
  454. package/docs/samples/movies-rels.csv +8 -0
  455. package/docs/samples/networks.cys +0 -0
  456. package/docs/samples/proteins.xgmml +9 -0
  457. package/docs/samples/teams.gv +24 -0
  458. package/docs/samples/vehicles.obo +25 -0
  459. package/package.json +9 -4
  460. package/src/children.ts +18 -6
  461. package/src/common/attributes.ts +29 -11
  462. package/src/common/cell-budget.ts +11 -3
  463. package/src/common/choose.ts +89 -0
  464. package/src/common/codes.ts +490 -66
  465. package/src/common/compare.ts +472 -0
  466. package/src/common/cx-export.ts +432 -0
  467. package/src/common/declared-types.ts +40 -6
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +39 -22
  470. package/src/common/export.ts +284 -47
  471. package/src/common/format.ts +30 -1
  472. package/src/common/ids.ts +14 -3
  473. package/src/common/input.ts +1057 -134
  474. package/src/common/json-elements.ts +1035 -199
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +7 -2
  477. package/src/common/ontology-export.ts +461 -0
  478. package/src/common/ontology.ts +31 -9
  479. package/src/common/options.ts +110 -15
  480. package/src/common/plural.ts +49 -0
  481. package/src/common/report.ts +168 -25
  482. package/src/common/temporal.ts +22 -5
  483. package/src/common/text.ts +62 -23
  484. package/src/common/weights.ts +74 -4
  485. package/src/common/writer.ts +37 -3
  486. package/src/common/xml.ts +479 -90
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +202 -47
  489. package/src/formats/csv/header.ts +50 -11
  490. package/src/formats/csv/importer.ts +1133 -245
  491. package/src/formats/csv/index.ts +106 -19
  492. package/src/formats/csv/records.ts +396 -122
  493. package/src/formats/csv/values.ts +1 -0
  494. package/src/formats/cx/exporter.ts +1483 -0
  495. package/src/formats/cx/importer.ts +890 -302
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +62 -339
  498. package/src/formats/cx2/importer.ts +318 -87
  499. package/src/formats/cx2/index.ts +2 -1
  500. package/src/formats/cys/constants.ts +62 -17
  501. package/src/formats/cys/exporter.ts +1368 -0
  502. package/src/formats/cys/importer.ts +659 -157
  503. package/src/formats/cys/index.ts +4 -2
  504. package/src/formats/cys/session.ts +142 -34
  505. package/src/formats/cys/tables.ts +47 -9
  506. package/src/formats/dot/exporter.ts +83 -37
  507. package/src/formats/dot/importer.ts +248 -54
  508. package/src/formats/dot/index.ts +2 -1
  509. package/src/formats/dot/names.ts +44 -11
  510. package/src/formats/dot/tokenizer.ts +25 -6
  511. package/src/formats/gexf/exporter.ts +57 -18
  512. package/src/formats/gexf/importer.ts +636 -105
  513. package/src/formats/gexf/index.ts +90 -21
  514. package/src/formats/gexf/schema.ts +67 -15
  515. package/src/formats/gml/exporter.ts +90 -36
  516. package/src/formats/gml/importer.ts +388 -67
  517. package/src/formats/gml/index.ts +85 -24
  518. package/src/formats/gml/syntax.ts +113 -28
  519. package/src/formats/graphml/constants.ts +178 -44
  520. package/src/formats/graphml/exporter.ts +52 -18
  521. package/src/formats/graphml/importer.ts +640 -60
  522. package/src/formats/graphml/index.ts +2 -1
  523. package/src/formats/graphml/tree.ts +6 -2
  524. package/src/formats/graphml/yfiles.ts +37 -15
  525. package/src/formats/json/dialect.ts +128 -29
  526. package/src/formats/json/exporter.ts +205 -68
  527. package/src/formats/json/importer.ts +1163 -253
  528. package/src/formats/json/index.ts +3 -1
  529. package/src/formats/json/obographs-export.ts +1197 -0
  530. package/src/formats/json/obographs.ts +353 -92
  531. package/src/formats/neo4j/exporter.ts +321 -102
  532. package/src/formats/neo4j/header.ts +44 -6
  533. package/src/formats/neo4j/importer.ts +512 -73
  534. package/src/formats/neo4j/index.ts +75 -23
  535. package/src/formats/obo/exporter.ts +2210 -0
  536. package/src/formats/obo/importer.ts +622 -109
  537. package/src/formats/obo/index.ts +6 -3
  538. package/src/formats/obo/syntax.ts +232 -37
  539. package/src/formats/pajek/exporter.ts +90 -42
  540. package/src/formats/pajek/importer.ts +407 -93
  541. package/src/formats/pajek/index.ts +2 -1
  542. package/src/formats/pajek/syntax.ts +216 -55
  543. package/src/formats/xgmml/columns.ts +10 -0
  544. package/src/formats/xgmml/constants.ts +183 -56
  545. package/src/formats/xgmml/document.ts +212 -34
  546. package/src/formats/xgmml/emit.ts +175 -34
  547. package/src/formats/xgmml/exporter.ts +36 -28
  548. package/src/formats/xgmml/importer.ts +64 -86
  549. package/src/formats/xgmml/index.ts +1 -0
  550. package/src/formats/xgmml/values.ts +12 -4
  551. package/src/index.ts +81 -11
  552. package/src/registry.ts +1121 -135
  553. package/src/sniff.ts +163 -32
  554. package/src/types.ts +385 -114
  555. package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
  556. package/dist/chunks/export-Bh60Dv-n.js +0 -582
  557. package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
  558. package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
  559. package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
  560. package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
  561. package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
  562. package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
  563. package/dist/chunks/importer-DET1tg6s.js.map +0 -1
  564. package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
  565. package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
  566. package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
  567. package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
  568. package/dist/chunks/importer-eUREhBGj.js.map +0 -1
  569. package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
  570. package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
  571. package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
  572. package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
  573. package/dist/chunks/ontology-BnrJ4I98.js +0 -113
  574. package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
  575. package/dist/chunks/records-dbRkxwaq.js.map +0 -1
  576. package/dist/chunks/report-B1z4WT9e.js +0 -2372
  577. package/dist/chunks/report-B1z4WT9e.js.map +0 -1
  578. package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
  579. package/dist/chunks/weights-Dzba96G3.js +0 -176
  580. package/dist/chunks/weights-Dzba96G3.js.map +0 -1
  581. package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
@@ -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;
@@ -8,24 +8,71 @@
8
8
  */
9
9
  import { GraphFormatError, } from "@graphty/graph-format";
10
10
  import { CellBudgetBuilder, maxEmptyCellsOption } from "./common/cell-budget.js";
11
- import { throwIfAborted } from "./common/input.js";
12
- import { ImportReportBuilder } from "./common/report.js";
11
+ import { EDGES_MERGED_CODE, EMPTY_INPUT_CODE, FETCH_CODE, OPTION_IGNORED_CODE, SELF_LOOPS_DROPPED_CODE, UNKNOWN_OPTION_CODE, WEIGHT_NOT_FOUND_CODE, } from "./common/codes.js";
12
+ import { abortable, foreignKind, lockReader, normalizeInput, throwIfAborted } from "./common/input.js";
13
+ import { chooseGraph, resolveImportOptions } from "./common/options.js";
14
+ import { agree, plural } from "./common/plural.js";
15
+ import { ImportReportBuilder, isAbortError, messageOf } from "./common/report.js";
16
+ import { collectBytes } from "./common/writer.js";
13
17
  import { csvExporter, csvImporter } from "./formats/csv/index.js";
14
- import { cxImporter } from "./formats/cx/index.js";
18
+ import { cxExporter, cxImporter } from "./formats/cx/index.js";
15
19
  import { cx2Exporter, cx2Importer } from "./formats/cx2/index.js";
16
- import { cysImporter } from "./formats/cys/index.js";
20
+ import { cysExporter, cysImporter } from "./formats/cys/index.js";
17
21
  import { dotExporter, dotImporter } from "./formats/dot/index.js";
18
22
  import { gexfExporter, gexfImporter } from "./formats/gexf/index.js";
19
23
  import { gmlExporter, gmlImporter } from "./formats/gml/index.js";
20
24
  import { graphmlExporter, graphmlImporter } from "./formats/graphml/index.js";
21
25
  import { jsonExporter, jsonImporter } from "./formats/json/index.js";
22
26
  import { neo4jExporter, neo4jImporter } from "./formats/neo4j/index.js";
23
- import { oboImporter } from "./formats/obo/index.js";
27
+ import { oboExporter, oboImporter } from "./formats/obo/index.js";
24
28
  import { pajekExporter, pajekImporter } from "./formats/pajek/index.js";
25
29
  import { xgmmlExporter, xgmmlImporter } from "./formats/xgmml/index.js";
26
30
  import { rankFormats, SNIFF_HEAD_BYTES } from "./sniff.js";
27
- /** The issue code of an input whose format no registered importer recognises. */
31
+ import { ImportError, } from "./types.js";
32
+ /**
33
+ * The input is in no format graph-io can read (or none of the formats you registered, for your own FormatRegistry).
34
+ * Pass `format` when you know what the file is.
35
+ * @category Issue and loss codes
36
+ */
28
37
  export const UNKNOWN_FORMAT_CODE = "E_UNKNOWN_FORMAT";
38
+ /**
39
+ * Registered importer whose sniff() threw while the format was being chosen; that importer was treated as not
40
+ * recognizing the input (a defect in that importer).
41
+ * @category Issue and loss codes
42
+ */
43
+ export const SNIFF_FAILED_CODE = "W_SNIFF_FAILED";
44
+ /**
45
+ * The options the load and save functions take themselves: the common import and export options and the functions'
46
+ * own. With the formats' own `options` lists, these are the names a load or a save knows.
47
+ */
48
+ const FUNCTION_OPTIONS = new Set([
49
+ "addMissingNodes",
50
+ "builder",
51
+ "defaultDirected",
52
+ "duplicateEdges",
53
+ "encoding",
54
+ "errorLimit",
55
+ "filename",
56
+ "format",
57
+ "freeze",
58
+ "graphIndex",
59
+ "graphName",
60
+ "hyperedges",
61
+ "ids",
62
+ "long",
63
+ "maxEmptyCells",
64
+ "mimeType",
65
+ "nodeIdFrom",
66
+ "onMixedDirection",
67
+ "onProgress",
68
+ "request",
69
+ "restoreMangledIds",
70
+ "sanitizeIds",
71
+ "selfLoops",
72
+ "signal",
73
+ "weightDtype",
74
+ "weightFrom",
75
+ ]);
29
76
  /** The keys of ImportGraphOptions that belong to the registry, never to an importer. */
30
77
  const REGISTRY_KEYS = new Set([
31
78
  "format",
@@ -37,8 +84,9 @@ const REGISTRY_KEYS = new Set([
37
84
  ]);
38
85
  /**
39
86
  * A registry of importers and exporters by format name. Registration order is the tie-break
40
- * order of sniffing (design section 8.2); the default registry lists the built-in formats in the
87
+ * order of sniffing; the default registry lists the built-in formats in the
41
88
  * order of GRAPH_FORMATS.
89
+ * @category Formats and detection
42
90
  */
43
91
  export class FormatRegistry {
44
92
  constructor() {
@@ -126,10 +174,10 @@ export class FormatRegistry {
126
174
  return [...new Set([...this.importerMap.keys(), ...this.exporterMap.keys()])];
127
175
  }
128
176
  /**
129
- * Rank the registered importers for an input (design section 8.2; the successor of
130
- * graphty-element's detectFormat()).
131
- * @param hints - the filename, MIME type and / or head of the input
132
- * @returns the candidates, best first; empty when nothing matches
177
+ * Every format that could be the input, best first, from the same hints as sniff(): use it to
178
+ * offer the user a choice when the best guess is not certain.
179
+ * @param hints - `{ filename, mimeType, head }`, any of them
180
+ * @returns the candidates, best first; empty when no format claims the input
133
181
  */
134
182
  sniffAll(hints) {
135
183
  return rankFormats(hints, this.importerMap.values());
@@ -144,40 +192,116 @@ export class FormatRegistry {
144
192
  return ranked.length > 0 ? ranked[0] : null;
145
193
  }
146
194
  /**
147
- * Read an input into a fresh builder and freeze it (design section 8.4: for callers who do not
148
- * own a sink). The format is the one named in the options, else sniffed from the filename,
149
- * the MIME type and the first bytes of the content; the builder is seeded from the common
150
- * options with `directed: true` as a placeholder that the importer overrides from the file.
151
- * An input holding several graphs yields the first, with a warning naming how many were
152
- * skipped; importAllGraphs() returns every one.
195
+ * Read a graph from a string, bytes or a stream. The format is the one you name in `format`,
196
+ * else it is detected from `filename`, `mimeType` and the first bytes of the content. From an
197
+ * input that holds several graphs, it reads the one `graphIndex` or `graphName` chooses, else
198
+ * the first, with a W_MULTIPLE_GRAPHS warning; importAllGraphs() returns every one.
153
199
  * @param input - the text, bytes, stream or chunks to read
154
200
  * @param options - the format, hints, common and format-specific import options
155
- * @returns the snapshot, the import report and the freeze report
201
+ * @returns the graph (`snapshot`), the format it was read as, and the import report
156
202
  */
157
203
  async importGraph(input, options = {}) {
158
- const chosen = await this.choose(input, options);
159
- const builder = seededBuilder(options, chosen.importer.format);
204
+ const opts = options;
205
+ const picked = await this.choose(input, opts);
206
+ const unknown = this.unknownOptions(opts).map((name) => Object.freeze({
207
+ category: "unsupported",
208
+ severity: "warning",
209
+ code: UNKNOWN_OPTION_CODE,
210
+ message: unknownOptionText(name),
211
+ line: null,
212
+ element: name,
213
+ }));
214
+ const chosen = unknown.length === 0 ? picked : { ...picked, warnings: [...picked.warnings, ...unknown] };
215
+ const { importer } = chosen;
216
+ if ((opts.graphIndex !== undefined || opts.graphName !== undefined) &&
217
+ importer.importAll !== undefined &&
218
+ importer.listGraphs === undefined) {
219
+ // an importer that reads several graphs but does not choose among them itself (DOT, GML,
220
+ // Pajek): read them all and return the chosen one, so the choice is never ignored
221
+ const all = await this.readAll(chosen, opts);
222
+ const names = all.map((r) => r.snapshot.meta.name);
223
+ return all[chooseGraph(names, opts, new ImportReportBuilder(importer.format, 0))];
224
+ }
225
+ let builder;
160
226
  let report;
161
227
  try {
162
- report = await chosen.importer.import(chosen.source, builder, importerOptions(options));
228
+ builder = seededBuilder(opts, chosen.importer.format);
229
+ report = await chosen.importer.import(chosen.source, builder, importerOptions(opts));
163
230
  }
164
231
  catch (err) {
165
232
  await chosen.peeked?.close();
166
- throw err;
233
+ throw chosen.sniff === null ? notReadableAs(err, importer.format, opts.filename) : err;
234
+ }
235
+ return result(chosen, builder, report, opts);
236
+ }
237
+ /**
238
+ * Fetch a graph file from a URL and load it; see the top-level loadFromUrl() for the full
239
+ * description.
240
+ * @param url - an absolute URL, or in a browser one relative to the page
241
+ * @param options - import options, plus `request` for fetch
242
+ * @returns the graph, the format it was read as, and the import report
243
+ */
244
+ async loadFromUrl(url, options = {}) {
245
+ const opts = options;
246
+ const { request, ...rest } = opts;
247
+ const signal = request?.signal ?? rest.signal ?? null;
248
+ const method = request?.method ?? "GET";
249
+ const where = `${method} ${String(url)}`;
250
+ let response;
251
+ try {
252
+ response = await fetch(url, { signal: rest.signal, ...request });
253
+ }
254
+ catch (err) {
255
+ if (signal?.aborted === true || isAbortError(err)) {
256
+ throw err;
257
+ }
258
+ return fetchFailed(rest, url, `${where} failed: ${fetchErrorText(url, err)}`, null, err);
259
+ }
260
+ if (!response.ok) {
261
+ await response.body?.cancel().catch(() => undefined);
262
+ const status = `${String(response.status)} ${response.statusText}`.trim();
263
+ return fetchFailed(rest, url, `${where} failed: ${status}`, response.status, null);
167
264
  }
168
- return result(chosen, builder, report, options);
265
+ return this.importGraph(response.body ?? new Uint8Array(0), {
266
+ ...rest,
267
+ filename: rest.filename ?? urlFilename(url),
268
+ mimeType: rest.mimeType ?? response.headers.get("content-type"),
269
+ });
270
+ }
271
+ /**
272
+ * Load a graph from a File or Blob; see the top-level loadFromFile() for the full description.
273
+ * @param file - the File or Blob to read
274
+ * @param options - the same options as importGraph()
275
+ * @returns the graph, the format it was read as, and the import report
276
+ */
277
+ loadFromFile(file, options = {}) {
278
+ const opts = options;
279
+ const { name } = file;
280
+ return this.importGraph(file.stream(), {
281
+ ...opts,
282
+ filename: opts.filename ?? (typeof name === "string" ? name : null),
283
+ mimeType: opts.mimeType ?? (file.type === "" ? null : file.type),
284
+ });
169
285
  }
170
286
  /**
171
287
  * Read every graph of an input (a DOT file with several graphs, a Pajek project with several
172
- * networks, a JGF document with a `graphs` array), each into its own fresh builder, frozen.
173
- * A format whose importer has no importAll() holds one graph per input, so the result has one
174
- * entry. Options, sniffing and the builder seed are those of importGraph().
288
+ * networks, a JGF document with a `graphs` array), each as its own snapshot. A format that holds
289
+ * one graph per file gives one result. Options and detection are those of importGraph().
175
290
  * @param input - the text, bytes, stream or chunks to read
176
291
  * @param options - the format, hints, common and format-specific import options
177
292
  * @returns one result per graph, in document order
178
293
  */
179
294
  async importAllGraphs(input, options = {}) {
180
- const chosen = await this.choose(input, options);
295
+ const opts = options;
296
+ return this.readAll(await this.choose(input, opts), opts);
297
+ }
298
+ /**
299
+ * Read every graph of an input with the chosen importer.
300
+ * @param chosen - the importer and the input to hand it
301
+ * @param options - the importGraph options
302
+ * @returns one result per graph, in document order
303
+ */
304
+ async readAll(chosen, options) {
181
305
  const { importer } = chosen;
182
306
  const builders = [];
183
307
  let reports;
@@ -196,9 +320,16 @@ export class FormatRegistry {
196
320
  }
197
321
  catch (err) {
198
322
  await chosen.peeked?.close();
199
- throw err;
323
+ throw inGraph(chosen.sniff === null ? notReadableAs(err, importer.format, options.filename) : err, builders.length - 1);
200
324
  }
201
- return reports.map((report, i) => result(chosen, builders[i], report, options));
325
+ return reports.map((report, i) => {
326
+ try {
327
+ return result(chosen, builders[i], report, options);
328
+ }
329
+ catch (err) {
330
+ throw inGraph(err, i);
331
+ }
332
+ });
202
333
  }
203
334
  /**
204
335
  * The graphs of an input that can hold several, without importing them (a Cytoscape session's
@@ -210,11 +341,12 @@ export class FormatRegistry {
210
341
  * list its graphs (importGraph() then reads the first)
211
342
  */
212
343
  async listGraphs(input, options = {}) {
213
- const chosen = await this.choose(input, options);
344
+ const opts = options;
345
+ const chosen = await this.choose(input, opts);
214
346
  try {
215
347
  return chosen.importer.listGraphs === undefined
216
348
  ? null
217
- : await chosen.importer.listGraphs(chosen.source, importerOptions(options));
349
+ : await chosen.importer.listGraphs(chosen.source, importerOptions(opts));
218
350
  }
219
351
  finally {
220
352
  await chosen.peeked?.close();
@@ -222,25 +354,69 @@ export class FormatRegistry {
222
354
  }
223
355
  /**
224
356
  * The importer for an input: the named format, or the sniffed one.
225
- * @param input - the input
357
+ * @param rawInput - the input as the caller passed it
226
358
  * @param options - the importGraph options
227
359
  * @returns the importer, the sniff, and the input to read (replayed for a stream)
228
360
  */
229
- async choose(input, options) {
361
+ async choose(rawInput, options) {
362
+ const input = normalizeInput(rawInput);
230
363
  const requested = options.format ?? "auto";
231
364
  if (requested !== "auto") {
232
- return { importer: this.importer(requested), sniff: null, source: input, peeked: null };
365
+ return { importer: this.importer(requested), sniff: null, source: input, peeked: null, warnings: [] };
366
+ }
367
+ // the options are checked before the input is touched, so a bad option never leaves a
368
+ // peeked stream locked; the importer resolves them again with its own defaults
369
+ const common = resolveImportOptions(options, { ids: "keep", defaultDirected: true, weightFrom: null });
370
+ maxEmptyCellsOption(options.maxEmptyCells);
371
+ const peeked = await peekHead(input, SNIFF_HEAD_BYTES, common.signal);
372
+ // the head is sniffed as the importer will read it: in the caller's encoding when given
373
+ const head = common.encoding !== null && peeked.head instanceof Uint8Array
374
+ ? new TextDecoder(common.encoding).decode(peeked.head, { stream: true })
375
+ : peeked.head;
376
+ let sniff;
377
+ const failures = new ImportReportBuilder("unknown", 0);
378
+ const warnings = [];
379
+ try {
380
+ const hints = { filename: options.filename, mimeType: options.mimeType, head };
381
+ const ranked = rankFormats(hints, this.importerMap.values(), (format, err) => {
382
+ warnings.push(failures.warning("unsupported", SNIFF_FAILED_CODE, `the ${format} importer's sniff() threw (${messageOf(err)}); it was treated as not recognizing the input`, { element: format }));
383
+ });
384
+ sniff = ranked.length > 0 ? ranked[0] : null;
385
+ }
386
+ catch (err) {
387
+ await peeked.close();
388
+ throw err;
389
+ }
390
+ // a known non-graph file is refused whatever its name says, unless an importer claims its
391
+ // content (a Cytoscape session is a zip); an HTML page always (the XML sniffers would
392
+ // otherwise claim it by its markup)
393
+ const foreign = foreignKind(peeked.head);
394
+ const claimed = sniff !== null && sniff.content > 0 && !(foreign?.startsWith("an HTML") ?? false);
395
+ if (sniff === null || (foreign !== null && !claimed)) {
396
+ await peeked.close();
397
+ return this.refuseUndetected(failures, head, foreign, options);
233
398
  }
234
- const peeked = await peekHead(input, SNIFF_HEAD_BYTES, options.signal ?? null);
235
- const sniff = this.sniff({ filename: options.filename, mimeType: options.mimeType, head: peeked.head });
236
- if (sniff === null) {
237
- const report = new ImportReportBuilder("unknown", 0);
238
- return report.fail(UNKNOWN_FORMAT_CODE, `no registered importer recognises the input${describeHints(options)}; pass the format explicitly`, undefined, { formats: this.formats() });
399
+ return { importer: this.importer(sniff.format), sniff, source: peeked.input, peeked, warnings };
400
+ }
401
+ /**
402
+ * Refuse an input no importer recognizes: empty, or not in a known graph format.
403
+ * @param failures - the report the error is recorded in
404
+ * @param head - the head, as sniffed
405
+ * @param foreign - what known non-graph file the head is, or null
406
+ * @param options - the importGraph options
407
+ * @returns never: always throws
408
+ */
409
+ refuseUndetected(failures, head, foreign, options) {
410
+ const text = typeof head === "string" ? head : new TextDecoder().decode(head ?? new Uint8Array(0));
411
+ if (text.trim() === "" && text.length < SNIFF_HEAD_BYTES) {
412
+ // nothing to detect: say the input is empty, as a named format would
413
+ return failures.fail(EMPTY_INPUT_CODE, text === "" ? "the input is empty" : "the input holds only whitespace");
239
414
  }
240
- return { importer: this.importer(sniff.format), sniff, source: peeked.input, peeked };
415
+ const what = foreign === null ? "" : `: it is ${foreign}`;
416
+ return failures.fail(UNKNOWN_FORMAT_CODE, `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`, undefined, { formats: this.formats() });
241
417
  }
242
418
  /**
243
- * Write a snapshot in a format, as UTF-8 chunks (design section 8.5).
419
+ * Write a snapshot in a format, as UTF-8 chunks.
244
420
  * @param snapshot - the snapshot
245
421
  * @param format - the format name
246
422
  * @param options - the exporter's common and format-specific options
@@ -259,6 +435,33 @@ export class FormatRegistry {
259
435
  async exportGraphToString(snapshot, format, options) {
260
436
  return this.exporter(format).exportToString(snapshot, options);
261
437
  }
438
+ /**
439
+ * Write a snapshot in a format as one buffer of UTF-8 bytes; see the top-level
440
+ * exportGraphToBytes().
441
+ * @param snapshot - the graph to write
442
+ * @param format - a format name where `canExport` is true
443
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
444
+ * @returns the encoded file
445
+ */
446
+ exportGraphToBytes(snapshot, format, options) {
447
+ return collectBytes(this.exportGraph(snapshot, format, options));
448
+ }
449
+ /**
450
+ * Write a snapshot in a format as a Blob typed with the format's first MIME type; see the
451
+ * top-level exportGraphToBlob().
452
+ * @param snapshot - the graph to write
453
+ * @param format - a format name where `canExport` is true
454
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
455
+ * @returns the file as a Blob
456
+ */
457
+ async exportGraphToBlob(snapshot, format, options) {
458
+ const parts = [];
459
+ for await (const chunk of this.exportGraph(snapshot, format, options)) {
460
+ parts.push(chunk);
461
+ }
462
+ const info = this.listFormats().find((f) => f.format === format);
463
+ return new Blob(parts, { type: info?.mimeTypes[0] ?? "application/octet-stream" });
464
+ }
262
465
  /**
263
466
  * What exporting a snapshot in a format would lose, without writing anything.
264
467
  * @param snapshot - the snapshot
@@ -267,12 +470,94 @@ export class FormatRegistry {
267
470
  * @returns the loss notes, empty when the export is exact
268
471
  */
269
472
  checkExport(snapshot, format, options) {
270
- return this.exporter(format).check(snapshot, options);
473
+ const notes = this.exporter(format).check(snapshot, options);
474
+ const unknown = this.unknownOptions(options);
475
+ if (unknown.length === 0) {
476
+ return notes;
477
+ }
478
+ return Object.freeze([
479
+ ...notes,
480
+ ...unknown.map((name) => Object.freeze({
481
+ code: UNKNOWN_OPTION_CODE,
482
+ message: unknownOptionText(name),
483
+ column: null,
484
+ count: null,
485
+ })),
486
+ ]);
487
+ }
488
+ /**
489
+ * The option names no registered format and no load or save function takes (misspellings), or none while a
490
+ * registered format does not list its options.
491
+ * @param options - the caller's options
492
+ * @returns the unknown names
493
+ */
494
+ unknownOptions(options) {
495
+ if (options === null || options === undefined) {
496
+ return [];
497
+ }
498
+ const known = new Set(FUNCTION_OPTIONS);
499
+ for (const plugin of [...this.importerMap.values(), ...this.exporterMap.values()]) {
500
+ if (plugin.options === undefined) {
501
+ return [];
502
+ }
503
+ for (const name of plugin.options) {
504
+ known.add(name);
505
+ }
506
+ }
507
+ return Object.entries(options)
508
+ .filter(([name, value]) => value !== undefined && !known.has(name))
509
+ .map(([name]) => name);
510
+ }
511
+ /**
512
+ * Browser only: write a graph in a format and have the browser save it as a file; see the
513
+ * top-level downloadGraph(). Use this method on a registry from createRegistry() to download
514
+ * without bundling every built-in format.
515
+ * @param snapshot - the graph to write
516
+ * @param format - a format name from listFormats() where `canExport` is true
517
+ * @param options - the export options, plus `filename`
518
+ * @returns resolves once the download has been handed to the browser
519
+ */
520
+ async downloadGraph(snapshot, format, options = {}) {
521
+ if (typeof document === "undefined") {
522
+ throw new GraphFormatError("E_UNSUPPORTED", "downloadGraph() needs a browser document; use exportGraphToBytes() and write the bytes yourself");
523
+ }
524
+ const { filename, ...rest } = options;
525
+ const blob = await this.exportGraphToBlob(snapshot, format, rest);
526
+ const extension = this.listFormats().find((f) => f.format === format)?.extensions[0] ?? `.${format}`;
527
+ const href = URL.createObjectURL(blob);
528
+ const anchor = document.createElement("a");
529
+ anchor.href = href;
530
+ anchor.download = filename ?? `graph${extension}`;
531
+ anchor.click();
532
+ // Safari cancels the download when the URL is revoked in the same task
533
+ setTimeout(() => {
534
+ URL.revokeObjectURL(href);
535
+ }, 0);
536
+ }
537
+ /**
538
+ * Every format this registry can read or write, in registration order; see the top-level
539
+ * listFormats().
540
+ * @returns one entry per format
541
+ */
542
+ listFormats() {
543
+ return this.formats().map((format) => {
544
+ const importer = this.importerMap.get(format);
545
+ const exporter = this.exporterMap.get(format);
546
+ return Object.freeze({
547
+ format,
548
+ extensions: importer?.extensions ?? exporter?.extensions ?? [],
549
+ mimeTypes: importer?.mimeTypes ?? exporter?.mimeTypes ?? [],
550
+ canImport: importer !== undefined,
551
+ canExport: exporter !== undefined,
552
+ capabilities: exporter?.capabilities ?? null,
553
+ });
554
+ });
271
555
  }
272
556
  }
273
557
  /**
274
- * A registry holding the eight built-in importers and exporters in the order of GRAPH_FORMATS.
558
+ * A new registry holding every built-in importer and exporter, in the order of GRAPH_FORMATS.
275
559
  * @returns a new registry
560
+ * @category Formats and detection
276
561
  */
277
562
  export function createRegistry() {
278
563
  return new FormatRegistry()
@@ -297,77 +582,355 @@ export function createRegistry() {
297
582
  .registerImporter(cx2Importer)
298
583
  .registerExporter(cx2Exporter)
299
584
  .registerImporter(cxImporter)
585
+ .registerExporter(cxExporter)
300
586
  .registerImporter(oboImporter)
301
- .registerImporter(cysImporter);
587
+ .registerExporter(oboExporter)
588
+ .registerImporter(cysImporter)
589
+ .registerExporter(cysExporter);
302
590
  }
303
- /** The default registry: every built-in format. */
304
- export const registry = createRegistry();
305
591
  /**
306
- * Read an input into a fresh builder and freeze it, through the default registry (design section
307
- * 8.4: `importGraph(input, { format?, ...options })`).
308
- * @param input - the text, bytes, stream or chunks to read
309
- * @param options - the format, hints, common and format-specific import options
310
- * @returns the snapshot, the import report and the freeze report
592
+ * The default registry: every built-in format.
593
+ * @category Formats and detection
594
+ */
595
+ export const registry = /* @__PURE__ */ createRegistry();
596
+ /**
597
+ * Read a graph from a string, bytes or a stream. The format is the one you name in `format`, else
598
+ * it is detected from `filename`, `mimeType` and the first bytes; an input that matches no format
599
+ * fails with E_UNKNOWN_FORMAT. From a file that holds several graphs it reads the one `graphIndex`
600
+ * or `graphName` chooses, else the first, with a W_MULTIPLE_GRAPHS warning. For a File or Blob use
601
+ * loadFromFile(), for a URL loadFromUrl().
602
+ * @param input - the whole text, the bytes (a Uint8Array or a Node Buffer), a ReadableStream of
603
+ * bytes, or an async iterable of text or byte chunks (a Node read stream)
604
+ * @param options - the format, format hints, the options every importer takes and the format's own
605
+ * options
606
+ * @returns the graph (`snapshot`), the format it was read as, and the import report
607
+ * @throws ImportError when the input could not be read; `err.issue?.code` says why
608
+ * @throws GraphFormatError with code E_UNSUPPORTED for a `format` no importer is registered for, or
609
+ * an option value that is not allowed (`err.details.option` names it)
610
+ * @throws the signal's reason when `signal` aborts
611
+ * @category Loading
311
612
  */
312
613
  export function importGraph(input, options) {
313
614
  return registry.importGraph(input, options);
314
615
  }
315
616
  /**
316
- * Read every graph of an input, each into its own frozen snapshot, through the default registry.
317
- * @param input - the text, bytes, stream or chunks to read
318
- * @param options - the format, hints, common and format-specific import options
319
- * @returns one result per graph, in document order
617
+ * Read every graph of a file that can hold several (DOT, GML, a Pajek project, a JSON Graph Format
618
+ * or OBO Graphs document, CX, XGMML, a Cytoscape session). A format that holds one graph per file
619
+ * gives one result. Options and errors are the same as importGraph()'s.
620
+ * @param input - the text, bytes or stream to read
621
+ * @param options - the same options as importGraph()
622
+ * @returns one result per graph, in file order
623
+ * @category Loading
320
624
  */
321
625
  export function importAllGraphs(input, options) {
322
626
  return registry.importAllGraphs(input, options);
323
627
  }
324
628
  /**
325
- * The graphs of an input that can hold several, through the default registry.
326
- * @param input - the text, bytes, stream or chunks to read
327
- * @param options - the format, hints, common and format-specific import options
328
- * @returns one listing per graph, in document order; null when the format does not list its graphs
629
+ * List the graphs of a file without reading them: each one's `index`, `name`, and node and edge
630
+ * counts when the file states them. Pass the index or name to importGraph() as `graphIndex` or
631
+ * `graphName`. The counts come from the file and can differ from what an import reads, for example
632
+ * when the import adds edges the listing does not count. JSON, CX, XGMML and Cytoscape sessions can
633
+ * list their graphs; for DOT, GML and Pajek it returns null, and importAllGraphs() is the way to see
634
+ * them.
635
+ * @param input - the text, bytes or stream to read
636
+ * @param options - the same options as importGraph()
637
+ * @returns one listing per graph, in file order; null when the format cannot list its graphs
638
+ * @category Loading
329
639
  */
330
640
  export function listGraphs(input, options) {
331
641
  return registry.listGraphs(input, options);
332
642
  }
333
643
  /**
334
- * Write a snapshot in a format through the default registry, as UTF-8 chunks.
335
- * @param snapshot - the snapshot
336
- * @param format - the format name
337
- * @param options - the exporter's common and format-specific options
338
- * @returns the encoded chunks
644
+ * Write a graph in a format as a stream of UTF-8 byte chunks, for a graph too large to hold as one
645
+ * file in memory. In Node, pipe it to a file with `pipeline(exportGraph(...), createWriteStream(path))`.
646
+ * Options and errors are the same as exportGraphToBytes()'s.
647
+ * @param snapshot - the graph to write
648
+ * @param format - a format name from listFormats() where `canExport` is true
649
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
650
+ * @returns the file's bytes, chunk by chunk
651
+ * @category Saving
339
652
  */
340
653
  export function exportGraph(snapshot, format, options) {
341
654
  return registry.exportGraph(snapshot, format, options);
342
655
  }
343
656
  /**
344
- * Write a snapshot in a format through the default registry, as one string.
345
- * @param snapshot - the snapshot
346
- * @param format - the format name
347
- * @param options - the exporter's common and format-specific options
348
- * @returns the whole document
657
+ * Write a graph in a format and return the file as one string. A binary format (a Cytoscape
658
+ * session) cannot be a string and fails with E_UNSUPPORTED; use exportGraphToBytes() for it. Options
659
+ * and errors are the same as exportGraphToBytes()'s.
660
+ * @param snapshot - the graph to write
661
+ * @param format - a format name from listFormats() where `canExport` is true
662
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
663
+ * @returns the whole file
664
+ * @category Saving
349
665
  */
350
666
  export async function exportGraphToString(snapshot, format, options) {
351
667
  return registry.exportGraphToString(snapshot, format, options);
352
668
  }
353
669
  /**
354
- * What exporting a snapshot in a format would lose, through the default registry.
355
- * @param snapshot - the snapshot
356
- * @param format - the format name
357
- * @param options - the exporter's common and format-specific options
358
- * @returns the loss notes, empty when the export is exact
670
+ * What saving a graph in a format would lose, without writing anything. Pass the same options you
671
+ * will pass to the save. Each note has a stable `code`: "E_" means the save would throw, "W_" means
672
+ * the file is written but that part does not read back the same. An empty array means the file
673
+ * reads back as the same graph. Runs synchronously.
674
+ * @param snapshot - the graph to check
675
+ * @param format - a format name from listFormats() where `canExport` is true
676
+ * @param options - the options you will save with
677
+ * @returns the loss notes, empty when the save is exact
678
+ * @throws GraphFormatError with code E_UNSUPPORTED when no exporter is registered for `format`, or
679
+ * when an option value is not allowed (`err.details.option` names it)
680
+ * @category Saving
359
681
  */
360
682
  export function checkExport(snapshot, format, options) {
361
683
  return registry.checkExport(snapshot, format, options);
362
684
  }
363
685
  /**
364
- * Sniff an input's format through the default registry.
365
- * @param hints - the filename, MIME type and / or head of the input
366
- * @returns the best candidate, or null when no built-in importer claims it
686
+ * Fetch a graph file from a URL and load it. The file is streamed into the importer as it
687
+ * downloads, so a large file is not held in memory twice. When you do not name a `format`, it is
688
+ * worked out from the URL's file name, the response's Content-Type and the first bytes of the
689
+ * file. Your own `filename` or `mimeType` option takes the place of the hint from the URL or the
690
+ * response. Every other option goes to importGraph() unchanged, including format-specific ones
691
+ * such as `delimiter` or `dialect`, and `graphIndex` / `graphName` for files that hold several
692
+ * graphs. The result is the same as importGraph()'s. A successful load can still have skipped
693
+ * elements; check `report.errorCount` and `report.warningCount`, or pass `errorLimit: 0` to make
694
+ * the first error fatal. Works wherever fetch exists: browsers, Node 18 and later, Deno, Bun and
695
+ * workers.
696
+ * @param url - an absolute URL, or in a browser one relative to the page
697
+ * @param options - import options, plus `request` for fetch
698
+ * @returns the graph, the format it was read as, and the import report
699
+ * @throws ImportError when the file could not be loaded; `err.issue?.code` is "E_FETCH" for a
700
+ * network failure, a CORS refusal or a status outside 200-299 (`err.details.url`,
701
+ * `err.details.status` -- null for a network failure -- and `err.details.cause`, fetch's own
702
+ * error), "E_UNKNOWN_FORMAT" when no format recognizes the file (pass `format`), or the code of
703
+ * the parse error
704
+ * @throws GraphFormatError with code E_UNSUPPORTED when `format` names no registered importer
705
+ * @throws the signal's reason when `signal` aborts
706
+ * @example
707
+ * ```ts
708
+ * import { GraphFormatError, loadFromUrl } from "@graphty/graph-io";
709
+ *
710
+ * try {
711
+ * const { snapshot, format } = await loadFromUrl("https://example.com/data/karate.gml");
712
+ * console.log(`${format}: ${snapshot.nodeCount} nodes, ${snapshot.edgeCount} edges`);
713
+ * } catch (err) {
714
+ * if (err instanceof GraphFormatError) {
715
+ * console.error(err.message);
716
+ * } else {
717
+ * throw err;
718
+ * }
719
+ * }
720
+ * ```
721
+ * @category Loading
722
+ */
723
+ export function loadFromUrl(url, options) {
724
+ return registry.loadFromUrl(url, options);
725
+ }
726
+ /**
727
+ * Load a graph from a File or Blob: a file the user picked in an `<input type="file">`, a file
728
+ * dropped on the page, a Blob you built, or in Node a Blob from `fs.openAsBlob(path)`. The content
729
+ * is streamed into the importer. A File's `name` and the Blob's `type` are used as format hints
730
+ * unless you pass `filename` or `mimeType` yourself. Options, result and errors are the same as
731
+ * loadFromUrl()'s, without E_FETCH.
732
+ * @param file - the File or Blob to read
733
+ * @param options - the same options as importGraph()
734
+ * @returns the graph, the format it was read as, and the import report
735
+ * @throws ImportError when the file could not be loaded (`err.issue?.code` E_UNKNOWN_FORMAT or a parse error)
736
+ * @throws GraphFormatError with code E_UNSUPPORTED when `format` names no registered importer
737
+ * @throws the signal's reason when `signal` aborts
738
+ * @example
739
+ * ```ts
740
+ * input.addEventListener("change", async () => {
741
+ * const file = input.files?.[0];
742
+ * if (file) {
743
+ * const { snapshot, report } = await loadFromFile(file);
744
+ * console.log(`${snapshot.nodeCount} nodes, ${report.errorCount} skipped`);
745
+ * }
746
+ * });
747
+ * ```
748
+ * @category Loading
749
+ */
750
+ export function loadFromFile(file, options) {
751
+ return registry.loadFromFile(file, options);
752
+ }
753
+ /**
754
+ * Write a graph in a format and return the whole file as UTF-8 bytes, ready for fs.writeFile, a
755
+ * fetch body or a zip entry. Prefer this over exportGraphToString() when the result goes to a file
756
+ * or the network. To stream a very large graph, use exportGraph(). To find out beforehand what the
757
+ * file will not keep, call checkExport() with the same format and the same options object: every
758
+ * "E_" note it returns makes this call throw, and every "W_" note describes a loss this call
759
+ * accepts. Most "E_" notes go away with `sanitizeIds: "mangle"` (rewrite ids the format cannot
760
+ * hold) or `onMixedDirection: "directed"` / `"undirected"`. The format's own options (the CSV
761
+ * `table` and `dialect`, the GEXF `version`, ...) go in the same object.
762
+ * @param snapshot - the graph to write
763
+ * @param format - a format name from listFormats() where `canExport` is true
764
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
765
+ * @returns the encoded file
766
+ * @throws GraphFormatError with code E_UNSUPPORTED when no exporter is registered for `format`
767
+ * @throws GraphFormatError when the graph cannot be written in this format under these options
768
+ * (checkExport() returns an "E_" note for the same call). The thrown code names the kind of
769
+ * failure rather than the note: E_INVALID_ID for ids (the E_ID_CHARSET and E_ID_TEXT_COLLISION
770
+ * notes), E_DIRECTED for direction (E_MIXED_DIRECTION), and E_COLUMN_TYPE or E_UNSUPPORTED for a
771
+ * value the format cannot write; each note's entry on the codes page says which
772
+ * @throws GraphFormatError with code E_UNSUPPORTED for an option value that is not allowed
773
+ * (`err.details.option` names it)
774
+ * @example
775
+ * ```ts
776
+ * import { writeFile } from "node:fs/promises";
777
+ *
778
+ * await writeFile("out.gexf", await exportGraphToBytes(snapshot, "gexf"));
779
+ * ```
780
+ * @category Saving
781
+ */
782
+ export function exportGraphToBytes(snapshot, format, options) {
783
+ return registry.exportGraphToBytes(snapshot, format, options);
784
+ }
785
+ /**
786
+ * Write a graph in a format and return it as a Blob, for uploading with fetch or FormData or for
787
+ * offering as a download. The Blob's `type` is the first entry of the format's `mimeTypes` in
788
+ * listFormats(), or application/octet-stream when the format lists none. The exporter's chunks
789
+ * are kept as they are and never joined into one string. Options and errors are the same as
790
+ * exportGraphToBytes().
791
+ * @param snapshot - the graph to write
792
+ * @param format - a format name from listFormats() where `canExport` is true
793
+ * @param options - sanitizeIds, onMixedDirection and the format's own options
794
+ * @returns the file as a Blob
795
+ * @throws the same errors as exportGraphToBytes()
796
+ * @example
797
+ * ```ts
798
+ * const body = new FormData();
799
+ * body.append("file", await exportGraphToBlob(snapshot, "graphml"), "graph.graphml");
800
+ * await fetch("/api/graphs", { method: "POST", body });
801
+ * ```
802
+ * @category Saving
803
+ */
804
+ export function exportGraphToBlob(snapshot, format, options) {
805
+ return registry.exportGraphToBlob(snapshot, format, options);
806
+ }
807
+ /**
808
+ * Browser only: write a graph in a format and have the browser save it as a file, the same as
809
+ * clicking a download link. Call it from a click handler; awaiting other work first is fine. The
810
+ * promise resolves once the file has been handed to the browser. There is no way to learn
811
+ * whether the user then canceled the save. Calling it where there is no `document` (Node, a
812
+ * worker) throws instead of silently doing nothing; importing it is safe everywhere. Formats you
813
+ * registered with `registry.registerExporter()` can be downloaded too.
814
+ * @param snapshot - the graph to write
815
+ * @param format - a format name from listFormats() where `canExport` is true
816
+ * @param options - the export options, plus `filename`
817
+ * @returns resolves once the download has been handed to the browser
818
+ * @throws GraphFormatError with code E_UNSUPPORTED when called without a DOM `document`
819
+ * @throws the same errors as exportGraphToBytes()
820
+ * @example
821
+ * ```ts
822
+ * saveButton.addEventListener("click", () => {
823
+ * void downloadGraph(snapshot, "graphml", { filename: "network.graphml" });
824
+ * });
825
+ * ```
826
+ * @category Saving
827
+ */
828
+ export async function downloadGraph(snapshot, format, options = {}) {
829
+ return registry.downloadGraph(snapshot, format, options);
830
+ }
831
+ /**
832
+ * Every format the default registry can read or write, including any you registered yourself:
833
+ * the built-in formats in the order of GRAPH_FORMATS (the order format detection prefers them),
834
+ * then yours in registration order. Use it to fill a "Save as" menu (the entries with
835
+ * `canExport`), a file input's `accept` attribute (the `extensions` of the entries with
836
+ * `canImport`), or to pick the output format from a file name.
837
+ * @returns one entry per format
838
+ * @example
839
+ * ```ts
840
+ * input.accept = listFormats().filter((f) => f.canImport).flatMap((f) => f.extensions).join(",");
841
+ * const target = listFormats().find((f) => f.canExport && f.extensions.includes(extensionOf(path) ?? ""));
842
+ * ```
843
+ * @category Formats and detection
844
+ */
845
+ export function listFormats() {
846
+ return registry.listFormats();
847
+ }
848
+ /**
849
+ * Detect a file's format without reading it, from what you know about it: a `filename` (only the
850
+ * extension counts), a `mimeType`, and `head`, the file's first bytes or characters (up to
851
+ * SNIFF_HEAD_BYTES; pass `bytes.subarray(0, SNIFF_HEAD_BYTES)`). It uses every format of the default
852
+ * registry, including the ones you registered. For every candidate, best first, call
853
+ * `registry.sniffAll(hints)`.
854
+ * @param hints - `{ filename, mimeType, head }`, any of them
855
+ * @returns the best candidate (`format`, `confidence` and what matched), or null when no format
856
+ * claims the input
857
+ * @example
858
+ * ```ts
859
+ * sniff({ filename: "graph.xml", head: "<?xml version=\"1.0\"?><gexf" })?.format; // "gexf"
860
+ * ```
861
+ * @category Formats and detection
367
862
  */
368
863
  export function sniff(hints) {
369
864
  return registry.sniff(hints);
370
865
  }
866
+ /**
867
+ * Abort a loadFromUrl() whose fetch failed with an E_FETCH ImportError.
868
+ * @param options - the import options (for the format name on the report)
869
+ * @param url - the URL
870
+ * @param message - the message
871
+ * @param status - the HTTP status, or null for a network failure
872
+ * @param cause - fetch's own error, or null
873
+ * @returns never; always throws
874
+ */
875
+ function fetchFailed(options, url, message, status, cause) {
876
+ const format = options.format === undefined || options.format === "auto" ? "unknown" : options.format;
877
+ return new ImportReportBuilder(format, 0).fail(FETCH_CODE, message, undefined, { url: String(url), status, cause });
878
+ }
879
+ /**
880
+ * The error for an input refused by the format the caller named before any element was read: the
881
+ * parser's message ("line 1: text outside the root element") prefixed with the file and the format,
882
+ * since the likely cause is a file of another kind, not a damaged one.
883
+ * @param err - what the importer threw
884
+ * @param format - the format named in `format`
885
+ * @param filename - the file name hint, if any
886
+ * @returns the error to throw
887
+ */
888
+ function notReadableAs(err, format, filename) {
889
+ if (!(err instanceof ImportError) ||
890
+ err.issue?.category !== "parse-error" ||
891
+ err.report.errorCount > 1 ||
892
+ err.report.counts.nodes + err.report.counts.edges > 0) {
893
+ return err;
894
+ }
895
+ const what = filename === undefined || filename === null ? "the input" : JSON.stringify(filename);
896
+ return new ImportError(`${what} could not be read as ${format}: ${err.message}`, err.report, err.details);
897
+ }
898
+ /**
899
+ * Why fetch() rejected, in words that point at the fix: a relative URL with no page to resolve it
900
+ * against (Node), a file: URL fetch() does not read, or a network error, which in a browser
901
+ * includes a CORS refusal.
902
+ * @param url - the URL as given
903
+ * @param err - what fetch() threw
904
+ * @returns the reason
905
+ */
906
+ function fetchErrorText(url, err) {
907
+ let absolute = null;
908
+ try {
909
+ absolute = new URL(url);
910
+ }
911
+ catch {
912
+ // relative: resolvable only against a page's location
913
+ }
914
+ if (absolute === null && globalThis.location === undefined) {
915
+ 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()";
916
+ }
917
+ if (absolute?.protocol === "file:") {
918
+ return "fetch() cannot read file: URLs here; read a local file with loadFromFile() or importGraph()";
919
+ }
920
+ const cause = err?.cause?.message;
921
+ const detail = typeof cause === "string" && cause !== "" ? ` (${cause})` : "";
922
+ return `the server could not be reached, or (in a browser) it does not allow this page to read the file (CORS)${detail}`;
923
+ }
924
+ /**
925
+ * The last path segment of a URL, as a format hint.
926
+ * @param url - the URL, absolute or relative
927
+ * @returns the file name, or null when the path ends in "/"
928
+ */
929
+ function urlFilename(url) {
930
+ const path = new URL(url, "http://localhost/").pathname;
931
+ const name = path.slice(path.lastIndexOf("/") + 1);
932
+ return name === "" ? null : name;
933
+ }
371
934
  /**
372
935
  * The options handed to the importer: everything but the registry's own keys.
373
936
  * @param options - the importGraph options
@@ -391,6 +954,26 @@ function importerOptions(options) {
391
954
  */
392
955
  function seededBuilder(options, format) {
393
956
  const maxEmptyCells = maxEmptyCellsOption(options.maxEmptyCells);
957
+ try {
958
+ return newBuilder(options, format, maxEmptyCells);
959
+ }
960
+ catch (err) {
961
+ // the builder names a bad value by its field; report it as the option the caller passed
962
+ if (err instanceof GraphFormatError && err.code === "E_UNSUPPORTED" && typeof err.details.field === "string") {
963
+ const option = err.details.field;
964
+ throw new GraphFormatError("E_UNSUPPORTED", `option ${option}: ${JSON.stringify(err.details.found)} is not one of the values it takes`, { option, found: err.details.found });
965
+ }
966
+ throw err;
967
+ }
968
+ }
969
+ /**
970
+ * The builder of seededBuilder(), before its option errors are reworded.
971
+ * @param options - the importGraph options
972
+ * @param format - the format being read
973
+ * @param maxEmptyCells - the empty-cell budget
974
+ * @returns the builder
975
+ */
976
+ function newBuilder(options, format, maxEmptyCells) {
394
977
  return new CellBudgetBuilder({
395
978
  weightDtype: options.weightDtype ?? "f64",
396
979
  ...options.builder,
@@ -400,6 +983,20 @@ function seededBuilder(options, format) {
400
983
  selfLoops: options.selfLoops ?? "keep",
401
984
  }, format, maxEmptyCells);
402
985
  }
986
+ /**
987
+ * Name the graph an importAllGraphs() failure belongs to: the report is that graph's own, so the
988
+ * message is prefixed with its index and `details.graphIndex` set, for every graph including the
989
+ * first. Anything other than an ImportError raised while a graph was being read passes unchanged.
990
+ * @param err - the error thrown while reading or freezing a graph
991
+ * @param index - the index of that graph, -1 before the first graph began
992
+ * @returns the error to throw
993
+ */
994
+ function inGraph(err, index) {
995
+ if (!(err instanceof ImportError) || index < 0) {
996
+ return err;
997
+ }
998
+ return new ImportError(`graph ${index}: ${err.message}`, err.report, { ...err.details, graphIndex: index });
999
+ }
403
1000
  /**
404
1001
  * Freeze an imported builder into an importGraph result.
405
1002
  * @param chosen - the importer and its sniff
@@ -409,15 +1006,125 @@ function seededBuilder(options, format) {
409
1006
  * @returns the frozen result
410
1007
  */
411
1008
  function result(chosen, builder, report, options) {
412
- const frozen = builder.freezeWithReport(options.freeze);
413
- return Object.freeze({
1009
+ let frozen;
1010
+ try {
1011
+ frozen = builder.freezeWithReport(options.freeze);
1012
+ }
1013
+ catch (err) {
1014
+ throw freezeFailure(err, report);
1015
+ }
1016
+ const { warnings } = chosen;
1017
+ const result = {
414
1018
  format: chosen.importer.format,
415
1019
  sniff: chosen.sniff,
416
1020
  snapshot: frozen.snapshot,
417
- report,
418
- freeze: frozen.report,
1021
+ report: withFreezeWarnings(weightNotFound(warnings.length === 0
1022
+ ? report
1023
+ : Object.freeze({
1024
+ ...report,
1025
+ issues: Object.freeze([...warnings, ...report.issues]),
1026
+ warningCount: report.warningCount + warnings.length,
1027
+ }), frozen.snapshot, options.weightFrom), frozen.report),
1028
+ };
1029
+ // not enumerable: it holds one entry per edge, and console.log(result) should show the graph, not that
1030
+ Object.defineProperty(result, "freeze", { value: frozen.report, enumerable: false });
1031
+ return Object.freeze(result);
1032
+ }
1033
+ /**
1034
+ * The message of a W_UNKNOWN_OPTION warning or note.
1035
+ * @param name - the option name
1036
+ * @returns the message
1037
+ */
1038
+ function unknownOptionText(name) {
1039
+ 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`;
1040
+ }
1041
+ /**
1042
+ * The import report with one W_WEIGHT_NOT_FOUND warning when the caller's `weightFrom` named an attribute that gave
1043
+ * no edge a weight, so a name that misses (a shared options object read against GML's `value`) is not silent. A
1044
+ * format that does not read `weightFrom` already reported it as W_OPTION_IGNORED.
1045
+ * @param report - the import's report
1046
+ * @param snapshot - the graph read
1047
+ * @param weightFrom - the caller's option
1048
+ * @returns the report, with the warning added when it applies
1049
+ */
1050
+ function weightNotFound(report, snapshot, weightFrom) {
1051
+ if (typeof weightFrom !== "string" ||
1052
+ snapshot.edgeCount === 0 ||
1053
+ snapshot.edgeList().weights !== null ||
1054
+ report.issues.some((i) => i.code === OPTION_IGNORED_CODE && i.element === "weightFrom")) {
1055
+ return report;
1056
+ }
1057
+ const issue = Object.freeze({
1058
+ category: "missing-value",
1059
+ severity: "warning",
1060
+ code: WEIGHT_NOT_FOUND_CODE,
1061
+ message: `weightFrom: ${JSON.stringify(weightFrom)} names no attribute an edge has a value for, so the graph has no weights`,
1062
+ line: null,
1063
+ element: "weightFrom",
1064
+ });
1065
+ return Object.freeze({
1066
+ ...report,
1067
+ issues: Object.freeze([...report.issues, issue]),
1068
+ warningCount: report.warningCount + 1,
1069
+ });
1070
+ }
1071
+ /**
1072
+ * The import report with what the builder's policies changed while the graph was built: one
1073
+ * W_EDGES_MERGED warning when `duplicateEdges` merged parallel edges (whatever told them apart, a
1074
+ * relation or a label, is lost) and one W_SELF_LOOPS_DROPPED warning when `selfLoops: "drop"`
1075
+ * removed self-loops, so nothing the caller's options removed goes unreported.
1076
+ * @param report - the import's report
1077
+ * @param freeze - the freeze report
1078
+ * @returns the report, with the warnings added
1079
+ */
1080
+ function withFreezeWarnings(report, freeze) {
1081
+ const added = [];
1082
+ const warn = (category, code, message) => {
1083
+ added.push(Object.freeze({ category, severity: "warning", code, message, line: null, element: null }));
1084
+ };
1085
+ if (freeze.mergedEdges > 0) {
1086
+ warn("merged", EDGES_MERGED_CODE, `${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`);
1087
+ }
1088
+ if (freeze.droppedSelfLoops > 0) {
1089
+ warn("validation-error", SELF_LOOPS_DROPPED_CODE, `${freeze.droppedSelfLoops} self-loop${plural(freeze.droppedSelfLoops)} ${agree(freeze.droppedSelfLoops, "was", "were")} removed by selfLoops: "drop"`);
1090
+ }
1091
+ if (added.length === 0) {
1092
+ return report;
1093
+ }
1094
+ return Object.freeze({
1095
+ ...report,
1096
+ issues: Object.freeze([...report.issues, ...added]),
1097
+ warningCount: report.warningCount + added.length,
419
1098
  });
420
1099
  }
1100
+ /**
1101
+ * The ImportError of a freeze the builder refused (a `duplicateEdges: "error"` or `selfLoops:
1102
+ * "error"` policy meeting a duplicate edge or a self-loop the file holds): the builder's code
1103
+ * recorded as an error issue on the import's report, so the caller gets the report and the file's
1104
+ * defect rather than a bare GraphFormatError.
1105
+ * @param err - what freezeWithReport() threw
1106
+ * @param report - the import's report
1107
+ * @returns the error to throw (anything that is not a GraphFormatError, unchanged)
1108
+ */
1109
+ function freezeFailure(err, report) {
1110
+ if (!(err instanceof GraphFormatError) || err instanceof ImportError) {
1111
+ return err;
1112
+ }
1113
+ const issue = Object.freeze({
1114
+ category: "validation-error",
1115
+ severity: "error",
1116
+ code: err.code,
1117
+ message: err.message,
1118
+ line: null,
1119
+ element: null,
1120
+ });
1121
+ const failed = Object.freeze({
1122
+ ...report,
1123
+ issues: Object.freeze([...report.issues, issue]),
1124
+ errorCount: report.errorCount + 1,
1125
+ });
1126
+ return new ImportError(`the graph cannot be frozen: ${err.message}`, failed, { code: err.code, ...err.details });
1127
+ }
421
1128
  /**
422
1129
  * The E_UNSUPPORTED error of a format name nothing is registered under.
423
1130
  * @param kind - importer or exporter
@@ -463,7 +1170,7 @@ async function peekHead(input, bytes, signal) {
463
1170
  }
464
1171
  throwIfAborted(signal);
465
1172
  const source = typeof input.getReader === "function"
466
- ? readerIterator(input)
1173
+ ? readerIterator(input, signal)
467
1174
  : input[Symbol.asyncIterator]();
468
1175
  const buffered = [];
469
1176
  let size = 0;
@@ -565,19 +1272,31 @@ function replay(buffered, source, finished) {
565
1272
  };
566
1273
  }
567
1274
  /**
568
- * An async iterator over a ReadableStream's chunks that cancels the stream when closed early.
1275
+ * An async iterator over a ReadableStream's chunks that cancels the stream (with the signal's
1276
+ * reason) when closed early or when the signal fires during a read, and releases the reader when
1277
+ * the stream ends or errors.
569
1278
  * @param stream - the stream
1279
+ * @param signal - the cancellation signal, or null
570
1280
  * @returns the iterator
571
1281
  */
572
- function readerIterator(stream) {
573
- const reader = stream.getReader();
1282
+ function readerIterator(stream, signal) {
1283
+ const reader = lockReader(stream);
574
1284
  let done = false;
575
1285
  return {
576
1286
  async next() {
577
1287
  if (done) {
578
1288
  return { done: true, value: undefined };
579
1289
  }
580
- const result = await reader.read();
1290
+ let result;
1291
+ try {
1292
+ result = await abortable(reader.read(), signal);
1293
+ }
1294
+ catch (err) {
1295
+ done = true;
1296
+ await reader.cancel(signal?.reason).catch(() => undefined);
1297
+ reader.releaseLock();
1298
+ throw err;
1299
+ }
581
1300
  if (result.done) {
582
1301
  done = true;
583
1302
  reader.releaseLock();
@@ -588,7 +1307,7 @@ function readerIterator(stream) {
588
1307
  async return() {
589
1308
  if (!done) {
590
1309
  done = true;
591
- await reader.cancel().catch(() => undefined);
1310
+ await reader.cancel(signal?.reason).catch(() => undefined);
592
1311
  reader.releaseLock();
593
1312
  }
594
1313
  return { done: true, value: undefined };