@graphty/graph-io 0.3.24 → 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
@@ -4,29 +4,48 @@
4
4
  * text chunks, as lines, or as one string, with BOM handling, cancellation through an AbortSignal
5
5
  * and byte progress.
6
6
  *
7
- * Bytes are decoded ONCE, here, for every importer. The encoding is, in order of precedence: the
8
- * caller's `encoding` option; a byte order mark (UTF-8, UTF-16LE, UTF-16BE); the encoding the file
9
- * declares (the importer's `declaredEncoding` reader: the XML prolog for GEXF and GraphML, the
10
- * `charset` attribute for DOT); else UTF-8. Decoding is strict (`fatal: true`): an invalid sequence
7
+ * Bytes are decoded ONCE, here, for every importer. The encoding is, in order of precedence: a
8
+ * byte order mark (UTF-8, UTF-16LE, UTF-16BE; a UTF-32 one is E_INVALID_ENCODING); the caller's
9
+ * `encoding` option; the encoding the file declares (the importer's `declaredEncoding` reader: the
10
+ * XML prolog for GEXF and GraphML, the `charset` attribute for DOT); else UTF-8. Whenever two of
11
+ * them disagree the one that applies is named in a W_ENCODING_CONFLICT warning. BOM-less UTF-16
12
+ * (every other byte 0) is E_INVALID_ENCODING asking for the option, and a known non-graph file (an
13
+ * HTML page, a PDF, gzip or zip data, a PNG) is E_FOREIGN_FORMAT. Decoding is strict (`fatal: true`): an invalid sequence
11
14
  * is never a silent U+FFFD that could alias two ids. Only in the last case, when bytes that are not
12
15
  * valid UTF-8 appear while everything before them was ASCII, is the rest of the input read as
13
16
  * windows-1252 (the superset of ISO 8859-1 that Excel, Pajek and older tools write), with the
14
- * warning W_ENCODING_FALLBACK; invalid UTF-8 after valid non-ASCII UTF-8 is E_INVALID_UTF8. A
15
- * leading U+FEFF is stripped from text and from decoded bytes alike.
17
+ * warning W_ENCODING_FALLBACK; invalid UTF-8 after valid non-ASCII UTF-8, in data holding control
18
+ * bytes (binary data), or cut at the end of the input is E_INVALID_UTF8.
19
+ *
20
+ * The decoded text, whatever the input shape, is then checked once: leading U+FEFFs are stripped, a
21
+ * trailing Ctrl-Z (the DOS end-of-file marker) is dropped, the first control character or stray
22
+ * U+FEFF is reported W_CONTROL_CHARACTER, and a text that is empty or only whitespace is the fatal
23
+ * E_EMPTY_INPUT.
16
24
  */
17
25
 
18
26
  import { GraphFormatError } from "@graphty/graph-format";
19
27
 
20
28
  import { type ImportInput } from "../types.js";
21
- import { ENCODING_FALLBACK_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, UNKNOWN_ENCODING_CODE } from "./codes.js";
29
+ import {
30
+ CONTROL_CHARACTER_CODE,
31
+ EMPTY_INPUT_CODE,
32
+ ENCODING_CONFLICT_CODE,
33
+ ENCODING_FALLBACK_CODE,
34
+ FOREIGN_FORMAT_CODE,
35
+ INVALID_ENCODING_CODE,
36
+ INVALID_UTF8_CODE,
37
+ OPTION_IGNORED_CODE,
38
+ TOO_LARGE_CODE,
39
+ UNKNOWN_ENCODING_CODE,
40
+ } from "./codes.js";
22
41
  import { type ImportReportBuilder } from "./report.js";
23
42
 
24
43
  export { INVALID_UTF8_CODE };
25
44
 
26
45
  /**
27
46
  * Cancellation and progress hooks of the reader; the resolved importer options satisfy this shape.
28
- * Consumed by the per-format importers and exporters under src/formats.
29
47
  * @public
48
+ * @category Plugin helpers
30
49
  */
31
50
  export interface ReadOptions {
32
51
  /** The cancellation signal, or null / undefined for none. */
@@ -40,18 +59,39 @@ export interface ReadOptions {
40
59
  * windows-1252 (ASCII-compatible), the encoding label it declares, or null.
41
60
  */
42
61
  readonly declaredEncoding?: ((head: string) => string | null) | undefined;
62
+ /** How many leading bytes the declaration reader sees (default 1024). */
63
+ readonly declarationBytes?: number | undefined;
64
+ /**
65
+ * Whether BOM-less UTF-16 that the NUL pattern of its head shows is read in that encoding (JSON:
66
+ * RFC 8259 section 8.1, RFC 4627 section 3) instead of failing with E_INVALID_ENCODING.
67
+ */
68
+ readonly bomlessUtf16?: boolean | undefined;
69
+ /** Whether an input with no text but whitespace is valid (else E_EMPTY_INPUT). */
70
+ readonly allowEmpty?: boolean | undefined;
71
+ /**
72
+ * Whether the format is XML: an (X)HTML document is then left to the format's own root-element
73
+ * check (an XHTML page may embed the format's elements) instead of being E_FOREIGN_FORMAT.
74
+ */
75
+ readonly xml?: boolean | undefined;
76
+ /**
77
+ * Whether a NUL byte in undeclared input fails the import as binary data (or BOM-less UTF-16)
78
+ * even where the bytes are valid UTF-8: for formats whose grammar never reports a NUL itself
79
+ * (CSV). False by default, so a format's own syntax error names it.
80
+ */
81
+ readonly nulIsBinary?: boolean | undefined;
43
82
  }
44
83
 
45
84
  /** How many leading bytes the declaration check sees (an XML prolog, a DOT `charset` near the top). */
46
85
  const HEAD_BYTES = 1024;
47
86
 
48
- /** How many leading bytes the BOM check needs. */
49
- const BOM_BYTES = 3;
87
+ /** How many leading bytes the BOM check needs (a UTF-32 mark is four). */
88
+ const BOM_BYTES = 4;
50
89
 
51
90
  /**
52
91
  * The canonical name of an encoding label, or null when the platform's TextDecoder does not know it.
53
92
  * @param label - a WHATWG encoding label ("latin1", "UTF-16LE", "cp1252", ...)
54
93
  * @returns the canonical name ("windows-1252", "utf-16le", ...) or null
94
+ * @category Plugin helpers
55
95
  */
56
96
  export function canonicalEncoding(label: string): string | null {
57
97
  try {
@@ -62,23 +102,139 @@ export function canonicalEncoding(label: string): string | null {
62
102
  }
63
103
 
64
104
  /**
65
- * The encoding a byte order mark announces.
105
+ * The encoding a byte order mark announces. The UTF-32 marks are checked first: FF FE 00 00 is the
106
+ * UTF-32LE mark, not a UTF-16LE one.
66
107
  * @param head - the first bytes
67
- * @returns "utf-8", "utf-16le", "utf-16be" or null
108
+ * @returns "utf-8", "utf-16le", "utf-16be", "utf-32le", "utf-32be" or null
68
109
  */
69
110
  function bomEncoding(head: Uint8Array): string | null {
70
- if (head.byteLength >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
111
+ if (startsWithBytes(head, [0xff, 0xfe, 0x00, 0x00])) {
112
+ return "utf-32le";
113
+ }
114
+ if (startsWithBytes(head, [0x00, 0x00, 0xfe, 0xff])) {
115
+ return "utf-32be";
116
+ }
117
+ if (startsWithBytes(head, [0xef, 0xbb, 0xbf])) {
71
118
  return "utf-8";
72
119
  }
73
- if (head.byteLength >= 2 && head[0] === 0xff && head[1] === 0xfe) {
120
+ if (startsWithBytes(head, [0xff, 0xfe])) {
74
121
  return "utf-16le";
75
122
  }
76
- if (head.byteLength >= 2 && head[0] === 0xfe && head[1] === 0xff) {
123
+ if (startsWithBytes(head, [0xfe, 0xff])) {
77
124
  return "utf-16be";
78
125
  }
79
126
  return null;
80
127
  }
81
128
 
129
+ /**
130
+ * Whether bytes start with a signature.
131
+ * @param bytes - the bytes
132
+ * @param signature - the leading bytes to look for
133
+ * @returns true when every signature byte matches
134
+ */
135
+ function startsWithBytes(bytes: Uint8Array, signature: readonly number[]): boolean {
136
+ return bytes.byteLength >= signature.length && signature.every((b, i) => bytes[i] === b);
137
+ }
138
+
139
+ /**
140
+ * The UTF-16 byte order of a head without a BOM whose every other byte is 0 (ASCII text in UTF-16),
141
+ * or null. At least 90% of the first 512 code units must follow the pattern.
142
+ * @param head - the first bytes
143
+ * @returns "utf-16le", "utf-16be" or null
144
+ */
145
+ function bomlessUtf16(head: Uint8Array): string | null {
146
+ const pairs = Math.min(head.byteLength >> 1, 512);
147
+ if (pairs < 2) {
148
+ return null;
149
+ }
150
+ let le = 0;
151
+ let be = 0;
152
+ for (let i = 0; i < pairs; i++) {
153
+ const even = head[2 * i];
154
+ const odd = head[2 * i + 1];
155
+ if (even !== 0 && odd === 0) {
156
+ le++;
157
+ } else if (even === 0 && odd !== 0) {
158
+ be++;
159
+ }
160
+ }
161
+ if (le >= pairs * 0.9) {
162
+ return "utf-16le";
163
+ }
164
+ return be >= pairs * 0.9 ? "utf-16be" : null;
165
+ }
166
+
167
+ /**
168
+ * The UTF-16 byte order of BOM-less UTF-16 input that starts with an XML declaration (XML 1.0
169
+ * Appendix F: `<?` as 3C 00 3F 00 or 00 3C 00 3F).
170
+ * @param head - the first bytes
171
+ * @returns "utf-16le", "utf-16be" or null
172
+ */
173
+ function xmlDeclarationUtf16(head: Uint8Array): string | null {
174
+ if (startsWithBytes(head, [0x3c, 0x00, 0x3f, 0x00])) {
175
+ return "utf-16le";
176
+ }
177
+ return startsWithBytes(head, [0x00, 0x3c, 0x00, 0x3f]) ? "utf-16be" : null;
178
+ }
179
+
180
+ /**
181
+ * What a head is when it is a known file type that is no graph format: compressed or archived data,
182
+ * an image, a PDF, an HTML page. Graph formats are text (or, for a Cytoscape session, a zip the
183
+ * registry hands to its own importer), so such an input is never read as text.
184
+ * @param head - the first bytes or characters of the input
185
+ * @returns a description such as "gzip-compressed data (decompress it first)", or null
186
+ * @category Plugin helpers
187
+ */
188
+ export function foreignKind(head: Uint8Array | string): string | null {
189
+ if (typeof head !== "string") {
190
+ if (startsWithBytes(head, [0x1f, 0x8b])) {
191
+ return "gzip-compressed data (decompress it first)";
192
+ }
193
+ if (startsWithBytes(head, [0x50, 0x4b, 0x03, 0x04]) || startsWithBytes(head, [0x50, 0x4b, 0x05, 0x06])) {
194
+ return "a zip archive (a Cytoscape session (.cys) is read by the cys importer; extract any other graph file from it first)";
195
+ }
196
+ if (startsWithBytes(head, [0x89, 0x50, 0x4e, 0x47])) {
197
+ return "a PNG image";
198
+ }
199
+ }
200
+ const text = typeof head === "string" ? head.slice(0, 64) : String.fromCodePoint(...head.subarray(0, 64));
201
+ const utf8Bom = String.fromCodePoint(0xef, 0xbb, 0xbf);
202
+ let body = text.startsWith(utf8Bom) ? text.slice(3) : text;
203
+ body = body.startsWith(BOM) ? body.slice(1) : body;
204
+ if (body.startsWith("%PDF-")) {
205
+ return "a PDF document";
206
+ }
207
+ if (/^\s*(<!doctype\s+html[\s>]|<html[\s>])/i.test(body)) {
208
+ return "an HTML document (likely an error page saved in place of the file)";
209
+ }
210
+ return null;
211
+ }
212
+
213
+ /**
214
+ * Fail the import with E_FOREIGN_FORMAT when its head is a known non-graph file; an (X)HTML page
215
+ * is left to an XML format's own root-element check.
216
+ * @param head - the first bytes or the first text
217
+ * @param report - the report
218
+ * @param options - whether the format is XML
219
+ */
220
+ function refuseForeign(head: Uint8Array | string, report: ImportReportBuilder, options: ReadOptions): void {
221
+ const foreign = foreignKind(head);
222
+ if (foreign !== null && !(options.xml === true && foreign.startsWith("an HTML"))) {
223
+ report.fail(FOREIGN_FORMAT_CODE, `the input is ${foreign}, not ${report.format} text`);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Whether two encodings name the same family for a declaration check: equal, or both UTF-16 (a
229
+ * prolog's "UTF-16" says nothing about the byte order the BOM gives).
230
+ * @param a - a canonical encoding name
231
+ * @param b - another
232
+ * @returns true when they agree
233
+ */
234
+ function sameFamily(a: string, b: string): boolean {
235
+ return a === b || (a.startsWith("utf-16") && b.startsWith("utf-16"));
236
+ }
237
+
82
238
  /**
83
239
  * The one byte decoder of an import: picks the encoding from the head (option, BOM, declaration,
84
240
  * UTF-8) and decodes strictly, switching to windows-1252 with a warning when undeclared input
@@ -101,6 +257,28 @@ class ByteDecoder {
101
257
  /** Bytes decoded so far, for error positions. */
102
258
  private offset = 0;
103
259
 
260
+ /** Whether the decoder switched to windows-1252 because undeclared input was not UTF-8. */
261
+ private fellBack = false;
262
+
263
+ /**
264
+ * The first control byte (other than TAB, LF, FF, CR and a final Ctrl-Z) of undeclared input
265
+ * that may still turn out not to be UTF-8: its offset and value, or null. Tracked from the
266
+ * start of the input, so the binary-data verdict never depends on how the input was chunked.
267
+ */
268
+ private control: { readonly at: number; readonly byte: number } | null = null;
269
+
270
+ /** The offset of a Ctrl-Z that ended the previous chunk: control data if more bytes follow. */
271
+ private subAt = -1;
272
+
273
+ /** Set while flushing because a text chunk follows: a sequence cut there is not windows-1252. */
274
+ private textFollows = false;
275
+
276
+ /** Whether the file declares UTF-8 (read like an undeclared file, but the fallback warning says so). */
277
+ private declaredUtf8 = false;
278
+
279
+ /** After the windows-1252 fallback: whether later bytes holding valid UTF-8 are still to be reported (once). */
280
+ private mixedPending = false;
281
+
104
282
  /**
105
283
  * Create the decoder of one import.
106
284
  * @param report - where warnings and the fatal decode error go
@@ -120,21 +298,56 @@ class ByteDecoder {
120
298
  }
121
299
 
122
300
  /**
123
- * Choose the encoding from the first bytes of the input.
124
- * @param head - the first bytes (up to HEAD_BYTES; fewer when the input is shorter)
301
+ * Choose the encoding from the first bytes of the input: a BOM, else the option, else the
302
+ * declaration, else UTF-8 (see the module comment), warning where two of them disagree.
303
+ * @param head - the first bytes (up to the declaration size; fewer when the input is shorter)
125
304
  */
126
305
  start(head: Uint8Array): void {
306
+ refuseForeign(head, this.report, this.options);
127
307
  const explicit = this.options.encoding ?? null;
308
+ const bom = bomEncoding(head);
309
+ if (bom === "utf-32le" || bom === "utf-32be") {
310
+ this.report.fail(
311
+ INVALID_ENCODING_CODE,
312
+ `the input starts with a ${bom.toUpperCase()} byte order mark; UTF-32 is not supported (convert the file to UTF-8)`,
313
+ undefined,
314
+ { byteOffset: 0 },
315
+ );
316
+ }
317
+ if (bom !== null) {
318
+ const option = explicit === null ? null : canonicalEncoding(explicit);
319
+ if (option !== null && option !== bom) {
320
+ this.conflict(`the encoding option says ${option} but the byte order mark says ${bom}; read as ${bom}`);
321
+ }
322
+ this.checkDeclaration(head, bom, "its byte order mark says");
323
+ this.use(bom, false);
324
+ return;
325
+ }
128
326
  if (explicit !== null) {
129
327
  this.use(explicit, false);
328
+ this.checkDeclaration(head, this.encoding, "the encoding option says");
130
329
  return;
131
330
  }
132
- const bom = bomEncoding(head);
133
- if (bom !== null) {
134
- this.use(bom, false);
331
+ const xmlUtf16 = xmlDeclarationUtf16(head);
332
+ if (xmlUtf16 !== null) {
333
+ this.checkDeclaration(head, xmlUtf16, "its XML declaration's byte pattern says");
334
+ this.use(xmlUtf16, false);
135
335
  return;
136
336
  }
137
- const declared = this.options.declaredEncoding?.(new TextDecoder("windows-1252").decode(head)) ?? null;
337
+ const utf16 = bomlessUtf16(head);
338
+ if (utf16 !== null && this.options.bomlessUtf16 === true) {
339
+ this.use(utf16, false);
340
+ return;
341
+ }
342
+ if (utf16 !== null) {
343
+ this.report.fail(
344
+ INVALID_ENCODING_CODE,
345
+ `the input looks like ${utf16} text without a byte order mark (every other byte is 0); pass the encoding option ${JSON.stringify(utf16)} to read it`,
346
+ undefined,
347
+ { byteOffset: 0 },
348
+ );
349
+ }
350
+ const declared = this.declared(head, "windows-1252");
138
351
  if (declared !== null) {
139
352
  const canonical = canonicalEncoding(declared);
140
353
  if (canonical === null) {
@@ -144,9 +357,16 @@ class ByteDecoder {
144
357
  `the input declares the encoding ${JSON.stringify(declared)}, which this platform cannot decode; reading it as UTF-8`,
145
358
  { element: declared },
146
359
  );
147
- } else if (canonical !== "utf-8" && !canonical.startsWith("utf-16")) {
148
- // a declared UTF-16 without a BOM is not UTF-16 (its declaration read as ASCII), and
149
- // a declared UTF-8 gets the same undeclared treatment: tools often write the default
360
+ } else if (canonical.startsWith("utf-16")) {
361
+ // a declared UTF-16 over bytes that are not UTF-16 (no BOM, no NUL pattern): the
362
+ // declaration itself was readable as ASCII
363
+ this.conflict(
364
+ `the input declares ${JSON.stringify(declared)} but has no byte order mark and its bytes are not UTF-16; read as UTF-8`,
365
+ );
366
+ } else if (canonical === "utf-8") {
367
+ this.declaredUtf8 = true;
368
+ } else {
369
+ // a declared UTF-8 gets the undeclared treatment: tools often write the default
150
370
  // prolog over Latin-1 bytes
151
371
  this.use(canonical, false);
152
372
  return;
@@ -155,70 +375,251 @@ class ByteDecoder {
155
375
  this.use("utf-8", true);
156
376
  }
157
377
 
378
+ /**
379
+ * The encoding the head declares, read through the format's declaration reader.
380
+ * @param head - the first bytes
381
+ * @param encoding - the encoding to read the head in for the reader
382
+ * @returns the declared label, or null (also for a format without declarations)
383
+ */
384
+ private declared(head: Uint8Array, encoding: string): string | null {
385
+ const reader = this.options.declaredEncoding;
386
+ return reader === undefined ? null : reader(new TextDecoder(encoding).decode(head));
387
+ }
388
+
389
+ /**
390
+ * Warn when the file declares an encoding other than the one a BOM or the option chose.
391
+ * @param head - the first bytes
392
+ * @param chosen - the encoding the input is read in
393
+ * @param why - what chose it, for the message
394
+ */
395
+ private checkDeclaration(head: Uint8Array, chosen: string, why: string): void {
396
+ const declared = this.declared(head, chosen);
397
+ const canonical = declared === null ? null : canonicalEncoding(declared);
398
+ if (canonical !== null && !sameFamily(canonical, chosen)) {
399
+ this.conflict(
400
+ `the input declares the encoding ${JSON.stringify(declared)} but ${why} ${chosen}; read as ${chosen}`,
401
+ );
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Record a W_ENCODING_CONFLICT warning.
407
+ * @param message - which sources disagree and which applies
408
+ */
409
+ private conflict(message: string): void {
410
+ this.report.warning("coercion", ENCODING_CONFLICT_CODE, message);
411
+ }
412
+
158
413
  /**
159
414
  * Decode the next bytes.
160
- * @param bytes - the bytes
415
+ * @param input - the bytes
161
416
  * @param stream - whether more bytes follow
162
417
  * @returns the text
163
418
  */
164
- decode(bytes: Uint8Array, stream: boolean): string {
419
+ decode(input: Uint8Array, stream: boolean): string {
420
+ // a browser TextDecoder refuses a view of shared memory: decode a private copy
421
+ const bytes =
422
+ typeof SharedArrayBuffer === "function" && input.buffer instanceof SharedArrayBuffer
423
+ ? input.slice()
424
+ : input;
165
425
  const decoder = this.decoder as TextDecoder;
166
- let text: string;
426
+ if (this.fellBack || (this.mayFallBack && this.asciiSoFar)) {
427
+ this.watch(bytes, !stream);
428
+ if (this.fellBack && this.control !== null) {
429
+ this.binary(this.control);
430
+ }
431
+ }
432
+ const text = this.decodeOrRecover(decoder, bytes, stream);
433
+ if (this.mixedPending) {
434
+ this.reportUtf8After(bytes, this.offset);
435
+ }
436
+ if (this.asciiSoFar && this.mayFallBack) {
437
+ this.trackAscii(bytes, text);
438
+ }
439
+ if (this.mayFallBack && this.options.nulIsBinary === true && text.includes("\0")) {
440
+ // a NUL never occurs in a text file: undeclared bytes holding one are binary or BOM-less UTF-16
441
+ this.binary({ at: this.offset + bytes.indexOf(0), byte: 0 });
442
+ }
443
+ this.offset += bytes.byteLength;
444
+ return text;
445
+ }
446
+
447
+ /**
448
+ * Decode bytes, recovering from bytes that are not valid in the encoding.
449
+ * @param decoder - the decoder
450
+ * @param bytes - the bytes
451
+ * @param stream - whether more bytes follow
452
+ * @returns the text
453
+ */
454
+ private decodeOrRecover(decoder: TextDecoder, bytes: Uint8Array, stream: boolean): string {
167
455
  try {
168
- text = decoder.decode(bytes, { stream });
456
+ return decoder.decode(bytes, { stream });
169
457
  } catch (err) {
170
458
  if (!(err instanceof TypeError)) {
171
459
  throw err;
172
460
  }
173
- text = this.recover(bytes, stream);
461
+ return this.recover(bytes, stream);
174
462
  }
175
- if (this.asciiSoFar && this.mayFallBack) {
176
- if (/[\u0080-\uffff]/.test(text)) {
177
- this.asciiSoFar = false;
178
- this.carry = new Uint8Array(0);
179
- } else {
180
- // every character so far is one ASCII byte, so what was not emitted is held back
181
- const all = this.carry.byteLength === 0 ? bytes : concatBytes([this.carry, bytes]);
182
- this.carry = all.slice(text.length);
183
- }
463
+ }
464
+
465
+ /**
466
+ * Track whether the input is ASCII so far. While it is, every character is one byte, so the
467
+ * bytes not yet emitted are held back for a fallback.
468
+ * @param bytes - the bytes just decoded
469
+ * @param text - their text
470
+ */
471
+ private trackAscii(bytes: Uint8Array, text: string): void {
472
+ if (/[\u0080-\uffff]/.test(text)) {
473
+ this.asciiSoFar = false;
474
+ this.carry = new Uint8Array(0);
475
+ return;
476
+ }
477
+ const all = this.carry.byteLength === 0 ? bytes : concatBytes([this.carry, bytes]);
478
+ this.carry = all.slice(text.length);
479
+ }
480
+
481
+ /**
482
+ * Flush the bytes held back because a text chunk follows (the input switches from bytes to
483
+ * strings): a multi-byte sequence cut there is invalid UTF-8, never windows-1252 text.
484
+ * @param flush - the flush to run (the decoder's own, or the head's)
485
+ * @returns the text of the held-back bytes
486
+ */
487
+ flushBeforeText(flush: () => string): string {
488
+ this.textFollows = true;
489
+ try {
490
+ return flush();
491
+ } finally {
492
+ this.textFollows = false;
184
493
  }
185
- this.offset += bytes.byteLength;
186
- return text;
187
494
  }
188
495
 
189
496
  /**
190
- * A decode failed: switch to windows-1252 when allowed, else fail the import.
497
+ * A decode failed: switch to windows-1252 when allowed, else fail the import with the position
498
+ * of the first byte that is not valid.
191
499
  * @param bytes - the bytes that failed
192
500
  * @param stream - whether more bytes follow
193
501
  * @returns the bytes decoded as windows-1252
194
502
  */
195
503
  private recover(bytes: Uint8Array, stream: boolean): string {
196
- const where = { byteOffset: this.offset };
197
504
  const all = this.carry.byteLength === 0 ? bytes : concatBytes([this.carry, bytes]);
198
- // a NUL byte never occurs in windows-1252 text: it marks binary data (or BOM-less UTF-16)
199
- if (this.mayFallBack && this.asciiSoFar && !all.includes(0) && !startsWithUtf8(all)) {
200
- this.report.warning(
201
- "coercion",
202
- ENCODING_FALLBACK_CODE,
203
- `the input is not valid UTF-8 (near byte ${this.offset}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`,
204
- );
205
- this.use("windows-1252", false);
206
- return (this.decoder as TextDecoder).decode(all, { stream });
505
+ const base = this.offset - this.carry.byteLength;
506
+ const bad = this.encoding === "utf-8" ? invalidUtf8(all) : null;
507
+ const at = base + (bad === null ? 0 : bad.index);
508
+ if (this.mayFallBack && this.asciiSoFar) {
509
+ // windows-1252 text holds no control bytes but TAB, LF, FF and CR: one anywhere in
510
+ // undeclared input that is not UTF-8 marks binary data (watch() saw every byte so far)
511
+ if (this.control !== null) {
512
+ return this.binary(this.control);
513
+ }
514
+ if (this.textFollows && bad?.cut === true) {
515
+ return this.report.fail(
516
+ INVALID_UTF8_CODE,
517
+ `invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`,
518
+ undefined,
519
+ { byteOffset: at },
520
+ );
521
+ }
522
+ if (!stream && bad?.truncated === true) {
523
+ return this.report.fail(
524
+ INVALID_UTF8_CODE,
525
+ `the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`,
526
+ undefined,
527
+ { byteOffset: at },
528
+ );
529
+ }
530
+ if (!startsWithUtf8(all)) {
531
+ const cutNote =
532
+ !stream && bad?.cut === true
533
+ ? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated"
534
+ : "";
535
+ this.report.warning(
536
+ "coercion",
537
+ ENCODING_FALLBACK_CODE,
538
+ this.declaredUtf8
539
+ ? `the input declares UTF-8 but is not valid UTF-8 (at byte ${at}); everything before was ASCII, so it is read as windows-1252 (pass the encoding option to choose another)`
540
+ : `the input is not valid UTF-8 (at byte ${at}${cutNote}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`,
541
+ );
542
+ this.use("windows-1252", false);
543
+ this.fellBack = true;
544
+ this.mixedPending = true;
545
+ this.reportUtf8After(all, base);
546
+ return (this.decoder as TextDecoder).decode(all, { stream });
547
+ }
207
548
  }
208
549
  if (this.encoding === "utf-8") {
209
550
  const after = this.mayFallBack ? " after valid non-ASCII UTF-8 text; pass the encoding option" : "";
210
- return this.report.fail(
211
- INVALID_UTF8_CODE,
212
- `invalid UTF-8 near byte ${this.offset}${after}`,
213
- undefined,
214
- where,
215
- );
551
+ return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}${after}`, undefined, {
552
+ byteOffset: at,
553
+ });
216
554
  }
555
+ const end = this.offset + bytes.byteLength;
556
+ const odd = !stream && this.encoding.startsWith("utf-16") && end % 2 === 1;
217
557
  return this.report.fail(
218
558
  INVALID_ENCODING_CODE,
219
- `the bytes near byte ${this.offset} are not valid ${this.encoding}`,
559
+ odd
560
+ ? `the input ends with half a ${this.encoding} code unit at byte ${end - 1} (it may be truncated)`
561
+ : `the bytes near byte ${base} are not valid ${this.encoding}`,
220
562
  undefined,
221
- where,
563
+ { byteOffset: odd ? end - 1 : base },
564
+ );
565
+ }
566
+
567
+ /**
568
+ * After the windows-1252 fallback, report once that later bytes hold a valid multi-byte UTF-8
569
+ * sequence: the input mixes encodings, and those characters are read as two or three
570
+ * windows-1252 characters each.
571
+ * @param bytes - bytes being decoded as windows-1252
572
+ * @param start - the byte offset of the first of them
573
+ */
574
+ private reportUtf8After(bytes: Uint8Array, start: number): void {
575
+ const at = utf8SequenceIndex(bytes);
576
+ if (at < 0) {
577
+ return;
578
+ }
579
+ this.mixedPending = false;
580
+ this.report.warning(
581
+ "coercion",
582
+ ENCODING_FALLBACK_CODE,
583
+ `bytes read as windows-1252 hold valid UTF-8 (at byte ${start + at}): the input mixes encodings, and its UTF-8 characters are read as windows-1252 (pass the encoding option to choose one)`,
584
+ );
585
+ }
586
+
587
+ /**
588
+ * Note the first control byte of the next bytes (see `control`).
589
+ * @param bytes - the bytes, starting at `offset`
590
+ * @param final - whether they end the input
591
+ */
592
+ private watch(bytes: Uint8Array, final: boolean): void {
593
+ if (this.control !== null || bytes.byteLength === 0) {
594
+ return;
595
+ }
596
+ if (this.subAt >= 0) {
597
+ this.control = { at: this.subAt, byte: 0x1a };
598
+ return;
599
+ }
600
+ // a Ctrl-Z at the end of a chunk is held until it is known whether more bytes follow
601
+ const index = controlByte(bytes, true);
602
+ if (index >= 0) {
603
+ this.control = { at: this.offset + index, byte: bytes[index] };
604
+ } else if (!final && bytes[bytes.byteLength - 1] === 0x1a) {
605
+ this.subAt = this.offset + bytes.byteLength - 1;
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Fail the import as binary data.
611
+ * @param control - the control byte that shows it
612
+ * @param control.at - its offset
613
+ * @param control.byte - its value
614
+ * @returns never
615
+ */
616
+ private binary(control: { readonly at: number; readonly byte: number }): never {
617
+ const hex = control.byte.toString(16).padStart(2, "0");
618
+ return this.report.fail(
619
+ INVALID_UTF8_CODE,
620
+ `the input is binary data, not text: control byte 0x${hex} at byte ${control.at}`,
621
+ undefined,
622
+ { byteOffset: control.at },
222
623
  );
223
624
  }
224
625
 
@@ -234,6 +635,20 @@ class ByteDecoder {
234
635
  }
235
636
  }
236
637
 
638
+ /**
639
+ * Where the first complete, valid multi-byte UTF-8 sequence of some bytes starts.
640
+ * @param bytes - the bytes
641
+ * @returns its index, or -1
642
+ */
643
+ function utf8SequenceIndex(bytes: Uint8Array): number {
644
+ for (let i = 0; i < bytes.byteLength; i++) {
645
+ if (bytes[i] >= 0xc2 && isUtf8SequenceAt(bytes, i)) {
646
+ return i;
647
+ }
648
+ }
649
+ return -1;
650
+ }
651
+
237
652
  /**
238
653
  * Whether the first non-ASCII byte of a chunk starts a valid UTF-8 sequence: then the chunk holds
239
654
  * valid non-ASCII UTF-8 before whatever made it fail, and the input is not windows-1252.
@@ -242,6 +657,19 @@ class ByteDecoder {
242
657
  */
243
658
  function startsWithUtf8(bytes: Uint8Array): boolean {
244
659
  const i = bytes.findIndex((b) => b >= 0x80);
660
+ return i >= 0 && isUtf8SequenceAt(bytes, i);
661
+ }
662
+
663
+ /** A strict UTF-8 decoder for checking single sequences (decode() without stream keeps no state). */
664
+ const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true });
665
+
666
+ /**
667
+ * Whether a complete, valid multi-byte UTF-8 sequence starts at an index.
668
+ * @param bytes - the bytes
669
+ * @param i - the index of the lead byte
670
+ * @returns true when the sequence is complete and valid
671
+ */
672
+ function isUtf8SequenceAt(bytes: Uint8Array, i: number): boolean {
245
673
  const lead = bytes[i];
246
674
  let length = 0;
247
675
  if (lead >= 0xc2 && lead <= 0xdf) {
@@ -251,30 +679,150 @@ function startsWithUtf8(bytes: Uint8Array): boolean {
251
679
  } else if (lead >= 0xf0 && lead <= 0xf4) {
252
680
  length = 4;
253
681
  }
254
- if (i < 0 || length === 0 || i + length > bytes.byteLength) {
682
+ if (length === 0 || i + length > bytes.byteLength) {
255
683
  return false;
256
684
  }
257
685
  try {
258
- new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(i, i + length));
686
+ STRICT_UTF8.decode(bytes.subarray(i, i + length));
259
687
  return true;
260
688
  } catch {
261
689
  return false;
262
690
  }
263
691
  }
264
692
 
693
+ /** The upper half (0x80-0xFF) of code page 437, the zip format's legacy name encoding (APPNOTE appendix D). */
694
+ const CP437_HIGH: readonly number[] = [
695
+ 0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0, 0x00e5, 0x00e7, 0x00ea, 0x00eb, 0x00e8, 0x00ef, 0x00ee, 0x00ec,
696
+ 0x00c4, 0x00c5, 0x00c9, 0x00e6, 0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9, 0x00ff, 0x00d6, 0x00dc, 0x00a2,
697
+ 0x00a3, 0x00a5, 0x20a7, 0x0192, 0x00e1, 0x00ed, 0x00f3, 0x00fa, 0x00f1, 0x00d1, 0x00aa, 0x00ba, 0x00bf, 0x2310,
698
+ 0x00ac, 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
699
+ 0x2555, 0x2563, 0x2551, 0x2557, 0x255d, 0x255c, 0x255b, 0x2510, 0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c,
700
+ 0x255e, 0x255f, 0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559,
701
+ 0x2558, 0x2552, 0x2553, 0x256b, 0x256a, 0x2518, 0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580, 0x03b1, 0x00df,
702
+ 0x0393, 0x03c0, 0x03a3, 0x03c3, 0x00b5, 0x03c4, 0x03a6, 0x0398, 0x03a9, 0x03b4, 0x221e, 0x03c6, 0x03b5, 0x2229,
703
+ 0x2261, 0x00b1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00f7, 0x2248, 0x00b0, 0x2219, 0x00b7, 0x221a, 0x207f, 0x00b2,
704
+ 0x25a0, 0x00a0,
705
+ ];
706
+
265
707
  /**
266
- * A zip entry name as text (APPNOTE 4.4.17): UTF-8 when the bytes are valid UTF-8, whether or not
267
- * the entry sets the language-encoding flag (bit 11), else windows-1252 (WHATWG has no CP437, and
268
- * session entry names are ASCII in practice). Never throws: a name is a matching key, never a path,
269
- * so an undecodable one still has to name its entry.
708
+ * The first invalid UTF-8 sequence of a chunk (the WHATWG decoder's rules: no overlong forms, no
709
+ * surrogates, nothing above U+10FFFF).
710
+ * @param bytes - the bytes
711
+ * @returns its index, whether it is a valid sequence cut by the end of the bytes (`cut`), and whether
712
+ * that cut sequence holds at least one continuation byte (truncation rather than a stray windows-1252
713
+ * byte); index 0 when none is found
714
+ */
715
+ function invalidUtf8(bytes: Uint8Array): Utf8Problem {
716
+ let i = 0;
717
+ while (i < bytes.byteLength) {
718
+ const lead = bytes[i];
719
+ const sequence = lead < 0x80 ? ASCII_SEQUENCE : utf8Sequence(lead);
720
+ if (sequence === null) {
721
+ return { index: i, truncated: false, cut: false };
722
+ }
723
+ const problem = checkContinuation(bytes, i, sequence);
724
+ if (problem !== null) {
725
+ return problem;
726
+ }
727
+ i += sequence.need + 1;
728
+ }
729
+ return { index: 0, truncated: false, cut: false };
730
+ }
731
+
732
+ /** Where a UTF-8 check stopped, and why. */
733
+ interface Utf8Problem {
734
+ readonly index: number;
735
+ readonly truncated: boolean;
736
+ readonly cut: boolean;
737
+ }
738
+
739
+ /** The continuation bytes a UTF-8 lead byte needs, and the range of the first one. */
740
+ interface Utf8Sequence {
741
+ readonly need: number;
742
+ readonly low: number;
743
+ readonly high: number;
744
+ }
745
+
746
+ const ASCII_SEQUENCE: Utf8Sequence = { need: 0, low: 0x80, high: 0xbf };
747
+
748
+ /**
749
+ * What a UTF-8 lead byte needs after it (no overlong forms, no surrogates, nothing above U+10FFFF).
750
+ * @param lead - the lead byte, 0x80 or above
751
+ * @returns the sequence, or null when the byte cannot lead one
752
+ */
753
+ function utf8Sequence(lead: number): Utf8Sequence | null {
754
+ if (lead >= 0xc2 && lead <= 0xdf) {
755
+ return { need: 1, low: 0x80, high: 0xbf };
756
+ }
757
+ if (lead >= 0xe0 && lead <= 0xef) {
758
+ return { need: 2, low: lead === 0xe0 ? 0xa0 : 0x80, high: lead === 0xed ? 0x9f : 0xbf };
759
+ }
760
+ if (lead >= 0xf0 && lead <= 0xf4) {
761
+ return { need: 3, low: lead === 0xf0 ? 0x90 : 0x80, high: lead === 0xf4 ? 0x8f : 0xbf };
762
+ }
763
+ return null;
764
+ }
765
+
766
+ /**
767
+ * Check the continuation bytes of the sequence led at `i`.
768
+ * @param bytes - the bytes
769
+ * @param i - the lead byte's index
770
+ * @param sequence - what the lead byte needs
771
+ * @returns the problem, or null when the sequence is valid
772
+ */
773
+ function checkContinuation(bytes: Uint8Array, i: number, sequence: Utf8Sequence): Utf8Problem | null {
774
+ for (let k = 1; k <= sequence.need; k++) {
775
+ if (i + k >= bytes.byteLength) {
776
+ return { index: i, truncated: k > 1, cut: true };
777
+ }
778
+ const next = bytes[i + k];
779
+ const low = k === 1 ? sequence.low : 0x80;
780
+ const high = k === 1 ? sequence.high : 0xbf;
781
+ if (next < low || next > high) {
782
+ return { index: i, truncated: false, cut: false };
783
+ }
784
+ }
785
+ return null;
786
+ }
787
+
788
+ /**
789
+ * The first control byte of a chunk that text never holds: below 0x20 but TAB, LF, FF and CR, and
790
+ * not a Ctrl-Z that ends the input (the DOS end-of-file marker).
791
+ * @param bytes - the bytes
792
+ * @param final - whether the bytes end the input
793
+ * @returns its index, or -1
794
+ */
795
+ function controlByte(bytes: Uint8Array, final: boolean): number {
796
+ const last = bytes.byteLength - 1;
797
+ return bytes.findIndex(
798
+ (b, i) =>
799
+ b < 0x20 && b !== 0x09 && b !== 0x0a && b !== 0x0c && b !== 0x0d && !(b === 0x1a && final && i === last),
800
+ );
801
+ }
802
+
803
+ /**
804
+ * A zip entry name as text (APPNOTE 4.4.17 and appendix D): UTF-8 when the bytes are valid UTF-8,
805
+ * whether or not the entry sets the language-encoding flag (bit 11); otherwise code page 437 when
806
+ * the flag is clear (what Windows and older tools write), and windows-1252 when the entry claims
807
+ * UTF-8 (or no flags are known). Never throws: a name is a matching key, never a path, so an
808
+ * undecodable one still has to name its entry.
270
809
  * @param bytes - the name bytes from the central directory or a local header
810
+ * @param cp437 - the entry's bit 11 is clear: decode a non-UTF-8 name as code page 437
271
811
  * @returns the name
812
+ * @category Plugin helpers
272
813
  */
273
- export function decodeEntryName(bytes: Uint8Array): string {
814
+ export function decodeEntryName(bytes: Uint8Array, cp437 = false): string {
274
815
  try {
275
816
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
276
817
  } catch {
277
- return new TextDecoder("windows-1252").decode(bytes);
818
+ if (!cp437) {
819
+ return new TextDecoder("windows-1252").decode(bytes);
820
+ }
821
+ let out = "";
822
+ for (const byte of bytes) {
823
+ out += String.fromCodePoint(byte < 0x80 ? byte : CP437_HIGH[byte - 0x80]);
824
+ }
825
+ return out;
278
826
  }
279
827
  }
280
828
 
@@ -297,22 +845,40 @@ function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
297
845
  }
298
846
 
299
847
  /**
300
- * Bytes decoded per call when the input is one in-memory Uint8Array: every consumer of textChunks()
301
- * then sees bounded chunks whatever the input shape (a line reader or a record reader that holds
302
- * one parse result per chunk never holds more than this much text at once), and progress stays
303
- * granular.
848
+ * Bytes decoded per call when the input is one in-memory Uint8Array, and characters per chunk of a
849
+ * string input: every consumer of textChunks() then sees bounded chunks whatever the input shape (a
850
+ * line reader or a record reader that holds one parse result per chunk never holds more than this
851
+ * much text at once), and progress stays granular.
304
852
  */
305
853
  const DECODE_SLICE = 256 * 1024;
306
854
 
307
855
  const BOM = String.fromCharCode(0xfeff);
308
856
 
857
+ /** The DOS end-of-file marker, Ctrl-Z. */
858
+ const SUB = String.fromCodePoint(0x1a);
859
+
860
+ /**
861
+ * The longest string V8 makes (2^29 - 24 UTF-16 code units); longer text cannot be one string.
862
+ * @category Plugin helpers
863
+ */
864
+ export const MAX_TEXT_LENGTH = 2 ** 29 - 24;
865
+
866
+ /**
867
+ * A character text keeps only by accident: a C0 control but TAB, LF, FF and CR, DEL, a C1 control
868
+ * (also what windows-1252 bytes it leaves undefined decode to), and U+FEFF inside the text.
869
+ */
870
+ const CONTROL_CHARACTER = new RegExp(
871
+ `[${String.fromCodePoint(0x00)}-${String.fromCodePoint(0x08, 0x0b, 0x0e)}-${String.fromCodePoint(0x1f, 0x7f)}-${String.fromCodePoint(0x9f)}${BOM}]`,
872
+ );
873
+
309
874
  /**
310
875
  * Whether a value is an ImportInput this module can read.
311
876
  * @param input - any value
312
877
  * @returns true for a string, a Uint8Array, a ReadableStream or an async iterable
878
+ * @category Plugin helpers
313
879
  */
314
880
  export function isImportInput(input: unknown): input is ImportInput {
315
- if (typeof input === "string" || input instanceof Uint8Array) {
881
+ if (typeof input === "string" || asBytes(input) !== null) {
316
882
  return true;
317
883
  }
318
884
  if (typeof input !== "object" || input === null) {
@@ -321,11 +887,73 @@ export function isImportInput(input: unknown): input is ImportInput {
321
887
  return typeof (input as { getReader?: unknown }).getReader === "function" || Symbol.asyncIterator in input;
322
888
  }
323
889
 
890
+ /**
891
+ * A Uint8Array, also one from another realm (a vm context, an iframe), as a Uint8Array of this one.
892
+ * @param value - any value
893
+ * @returns the bytes, or null when the value is not a Uint8Array
894
+ */
895
+ function asBytes(value: unknown): Uint8Array | null {
896
+ if (value instanceof Uint8Array) {
897
+ return value;
898
+ }
899
+ if (ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object Uint8Array]") {
900
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
901
+ }
902
+ return null;
903
+ }
904
+
905
+ /**
906
+ * The input as one of the ImportInput shapes, or E_UNSUPPORTED naming what was passed and how to
907
+ * pass it instead (an ArrayBuffer, a Blob, a fetch Response, null from a failed read).
908
+ * @param input - what the caller passed
909
+ * @returns the input (a Uint8Array from another realm re-viewed in this one)
910
+ * @category Plugin helpers
911
+ */
912
+ export function normalizeInput(input: unknown): ImportInput {
913
+ if (typeof input === "string") {
914
+ return input;
915
+ }
916
+ const bytes = asBytes(input);
917
+ if (bytes !== null) {
918
+ return bytes;
919
+ }
920
+ if (isImportInput(input)) {
921
+ return input;
922
+ }
923
+ const tag = Object.prototype.toString.call(input).slice(8, -1);
924
+ let found: string = input === null ? "null" : typeof input;
925
+ let fix = "";
926
+ if (tag === "ArrayBuffer" || tag === "SharedArrayBuffer") {
927
+ found = `an ${tag}`;
928
+ fix = "; wrap it as new Uint8Array(buffer)";
929
+ } else if (ArrayBuffer.isView(input)) {
930
+ found = `a ${tag}`;
931
+ fix = "; pass new Uint8Array(view.buffer, view.byteOffset, view.byteLength)";
932
+ } else if (typeof input === "object" && input !== null) {
933
+ const object = input as { stream?: unknown; size?: unknown; body?: unknown; arrayBuffer?: unknown };
934
+ if (typeof object.stream === "function" && typeof object.size === "number") {
935
+ found = "a Blob or File";
936
+ fix = "; pass blob.stream()";
937
+ } else if ("body" in object && typeof object.arrayBuffer === "function") {
938
+ found = "a fetch Response";
939
+ fix = "; pass response.body";
940
+ } else {
941
+ found = `an object (${tag})`;
942
+ }
943
+ }
944
+ throw new GraphFormatError(
945
+ "E_UNSUPPORTED",
946
+ `the input is ${found}; an importer reads a string, a Uint8Array, a ReadableStream or an async iterable of strings and Uint8Arrays${fix}`,
947
+ { reason: "input type", found },
948
+ );
949
+ }
950
+
324
951
  /**
325
952
  * The total size of an in-memory input (bytes for a Uint8Array, UTF-16 code units for a string),
326
- * for the `bytesTotal` argument of onProgress; null for a stream or an iterable.
953
+ * null for a stream or an iterable.
327
954
  * @param input - the input
328
955
  * @returns the size, or null when unknown up front
956
+ * @category Plugin helpers
329
957
  */
330
958
  export function inputLength(input: ImportInput): number | null {
331
959
  if (typeof input === "string") {
@@ -337,13 +965,38 @@ export function inputLength(input: ImportInput): number | null {
337
965
  return null;
338
966
  }
339
967
 
968
+ /**
969
+ * The UTF-8 length of a text, the unit onProgress counts text in (a lone surrogate counts as the
970
+ * three bytes of U+FFFD).
971
+ * @param text - the text
972
+ * @returns its length in UTF-8 bytes
973
+ */
974
+ function utf8Length(text: string): number {
975
+ let bytes = 0;
976
+ let i = 0;
977
+ while (i < text.length) {
978
+ const c = text.charCodeAt(i); // NOSONAR(S7758): reads UTF-16 code units on purpose
979
+ const pair = c >= 0xd800 && c <= 0xdbff && (text.charCodeAt(i + 1) & 0xfc00) === 0xdc00; // NOSONAR(S7758): reads UTF-16 code units on purpose
980
+ if (c < 0x80) {
981
+ bytes++;
982
+ } else if (c < 0x800) {
983
+ bytes += 2;
984
+ } else {
985
+ bytes += pair ? 4 : 3;
986
+ }
987
+ i += pair ? 2 : 1;
988
+ }
989
+ return bytes;
990
+ }
991
+
340
992
  /**
341
993
  * Throw the signal's reason when it is aborted, exactly as the platform's
342
994
  * `AbortSignal.throwIfAborted()` does: the reason as it is (the DOMException named "AbortError"
343
995
  * of a reason-less abort, or whatever the caller passed to `abort(reason)`, an Error or not), so a
344
996
  * caller can compare the rejection with `signal.reason`. Only a runtime that stores no reason at
345
- * all gets a synthesised AbortError.
997
+ * all gets a synthesized AbortError.
346
998
  * @param signal - the signal, or null
999
+ * @category Writing a format
347
1000
  */
348
1001
  export function throwIfAborted(signal: AbortSignal | null | undefined): void {
349
1002
  if (signal === null || signal === undefined || !signal.aborted) {
@@ -371,97 +1024,314 @@ function abortError(): Error {
371
1024
  return err;
372
1025
  }
373
1026
 
1027
+ /**
1028
+ * A promise that also settles when the signal fires, rejecting with its reason: a read that stalls
1029
+ * (a network body that never delivers) cannot keep an aborted import pending. The caller checks
1030
+ * the signal before calling.
1031
+ * @param promise - the pending operation
1032
+ * @param signal - the cancellation signal, or null
1033
+ * @returns the operation's result, or the abort reason as a rejection
1034
+ * @category Plugin helpers
1035
+ */
1036
+ export function abortable<T>(promise: Promise<T>, signal: AbortSignal | null): Promise<T> {
1037
+ if (signal === null || typeof signal.addEventListener !== "function") {
1038
+ return promise;
1039
+ }
1040
+ return new Promise<T>((resolve, reject) => {
1041
+ const onAbort = (): void => {
1042
+ try {
1043
+ throwIfAborted(signal);
1044
+ } catch (err) {
1045
+ reject(err as Error);
1046
+ }
1047
+ };
1048
+ signal.addEventListener("abort", onAbort, { once: true });
1049
+ promise.then(
1050
+ (value) => {
1051
+ signal.removeEventListener("abort", onAbort);
1052
+ resolve(value);
1053
+ },
1054
+ (err: unknown) => {
1055
+ signal.removeEventListener("abort", onAbort);
1056
+ reject(err as Error);
1057
+ },
1058
+ );
1059
+ });
1060
+ }
1061
+
1062
+ /**
1063
+ * A reader of a stream, or E_UNSUPPORTED when another reader holds the stream already.
1064
+ * @param stream - the stream
1065
+ * @returns the reader
1066
+ * @category Plugin helpers
1067
+ */
1068
+ export function lockReader(stream: ReadableStream<Uint8Array>): ReadableStreamDefaultReader<Uint8Array> {
1069
+ if (stream.locked) {
1070
+ throw new GraphFormatError(
1071
+ "E_UNSUPPORTED",
1072
+ "the input ReadableStream is locked: another reader holds it (release it, or pass a fresh stream)",
1073
+ { reason: "stream locked" },
1074
+ );
1075
+ }
1076
+ return stream.getReader();
1077
+ }
1078
+
1079
+ /**
1080
+ * The checks every decoded text passes once, whatever the input shape: leading U+FEFFs stripped,
1081
+ * a known non-graph document (HTML, PDF) refused, a trailing Ctrl-Z held back and dropped at the
1082
+ * end, the first control character reported, and an input with no text but whitespace refused.
1083
+ */
1084
+ class TextFilter {
1085
+ private first = true;
1086
+
1087
+ private content = false;
1088
+
1089
+ private heldSub = false;
1090
+
1091
+ private warned = false;
1092
+
1093
+ private strayBom = false;
1094
+
1095
+ /**
1096
+ * Create the filter of one import.
1097
+ * @param report - where the warnings and the fatal errors go
1098
+ * @param options - whether an empty input is valid, whether the format is XML
1099
+ */
1100
+ constructor(
1101
+ private readonly report: ImportReportBuilder,
1102
+ private readonly options: ReadOptions,
1103
+ ) {}
1104
+
1105
+ /**
1106
+ * Filter the next decoded text.
1107
+ * @param chunk - the text
1108
+ * @returns the text to hand on
1109
+ */
1110
+ push(chunk: string): string {
1111
+ let text = this.heldSub ? SUB + chunk : chunk;
1112
+ this.heldSub = false;
1113
+ if (text.length === 0) {
1114
+ return text;
1115
+ }
1116
+ if (this.first) {
1117
+ text = this.skipByteOrderMarks(text);
1118
+ if (text.length === 0) {
1119
+ return text;
1120
+ }
1121
+ this.first = false;
1122
+ // ponytail: only the first non-empty chunk is checked, so a document arriving one
1123
+ // character per chunk is not recognized; the parser then fails on it instead
1124
+ refuseForeign(text, this.report, this.options);
1125
+ }
1126
+ if (text.endsWith(SUB)) {
1127
+ this.heldSub = true;
1128
+ text = text.slice(0, -1);
1129
+ }
1130
+ if (!this.content && /\S/.test(text)) {
1131
+ this.content = true;
1132
+ }
1133
+ if (!this.warned) {
1134
+ this.warnControl(text);
1135
+ }
1136
+ return text;
1137
+ }
1138
+
1139
+ /**
1140
+ * Drop the byte order marks the input starts with, warning when there is more than one.
1141
+ * @param text - the first text
1142
+ * @returns the text without them
1143
+ */
1144
+ private skipByteOrderMarks(text: string): string {
1145
+ let start = 0;
1146
+ while (text.codePointAt(start) === 0xfeff) {
1147
+ start++;
1148
+ }
1149
+ if (start > 1 && !this.strayBom) {
1150
+ // the first U+FEFF is the byte order mark; another one is a stray character
1151
+ this.strayBom = true;
1152
+ this.report.warning(
1153
+ "validation-error",
1154
+ CONTROL_CHARACTER_CODE,
1155
+ `the input starts with ${start} byte order marks (U+FEFF); the extra ones were ignored`,
1156
+ { element: "U+FEFF" },
1157
+ );
1158
+ }
1159
+ return text.slice(start);
1160
+ }
1161
+
1162
+ /**
1163
+ * Warn, once, about the first control character the text holds.
1164
+ * @param text - the text
1165
+ */
1166
+ private warnControl(text: string): void {
1167
+ const match = CONTROL_CHARACTER.exec(text);
1168
+ if (match === null) {
1169
+ return;
1170
+ }
1171
+ this.warned = true;
1172
+ const code = (match[0].codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0");
1173
+ const around = JSON.stringify(text.slice(Math.max(0, match.index - 20), match.index + 20));
1174
+ this.report.warning(
1175
+ "validation-error",
1176
+ CONTROL_CHARACTER_CODE,
1177
+ `the input holds the control character U+${code} (in ${around}); it is kept in the id or value it is part of`,
1178
+ { element: `U+${code}` },
1179
+ );
1180
+ }
1181
+
1182
+ /** The input ended: drop a held Ctrl-Z with a warning, and refuse an input without content. */
1183
+ end(): void {
1184
+ if (this.heldSub) {
1185
+ this.report.warning(
1186
+ "validation-error",
1187
+ CONTROL_CHARACTER_CODE,
1188
+ "the input ends with a Ctrl-Z (0x1A), the DOS end-of-file marker; it was ignored",
1189
+ { element: "U+001A" },
1190
+ );
1191
+ }
1192
+ if (!this.content && this.options.allowEmpty !== true) {
1193
+ this.report.fail(EMPTY_INPUT_CODE, this.first ? "the input is empty" : "the input holds only whitespace");
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * Record W_OPTION_IGNORED for an encoding option given with text input, which is already decoded.
1200
+ * @param report - the report
1201
+ * @param encoding - the option, or null / undefined
1202
+ */
1203
+ function reportTextEncoding(report: ImportReportBuilder, encoding: string | null | undefined): void {
1204
+ if (encoding !== null && encoding !== undefined) {
1205
+ report.warning(
1206
+ "unsupported",
1207
+ OPTION_IGNORED_CODE,
1208
+ "option encoding has no effect on text input, which is already decoded",
1209
+ { element: "encoding" },
1210
+ );
1211
+ }
1212
+ }
1213
+
374
1214
  /**
375
1215
  * Read an ImportInput as a sequence of decoded text chunks. Chunk boundaries carry no meaning:
376
1216
  * a caller that needs lines uses LineReader, one that needs the whole document uses readText().
377
- * The signal is checked before every chunk; a stream is cancelled when the consumer stops early
378
- * or the signal fires. Progress is reported after every chunk.
379
- * @param input - the input
380
- * @param report - the report the decode error is recorded in (E_INVALID_UTF8, then ImportError)
381
- * @param options - cancellation and progress
382
- * @yields decoded text; the first chunk has any leading BOM removed
1217
+ * The signal is checked before every chunk and while a stream read is pending; a stream is
1218
+ * canceled when the consumer stops early or the signal fires. Progress is reported after every
1219
+ * chunk, in bytes (text counted as UTF-8).
1220
+ * @param rawInput - the input
1221
+ * @param report - the report decode errors and the text checks are recorded in (E_INVALID_UTF8,
1222
+ * E_EMPTY_INPUT, ... then ImportError)
1223
+ * @param options - cancellation, progress and the encoding
1224
+ * @yields decoded text; leading BOMs and a trailing Ctrl-Z removed
383
1225
  * @returns nothing
1226
+ * @category Writing a format
384
1227
  */
385
1228
  export async function* textChunks(
386
- input: ImportInput,
1229
+ rawInput: ImportInput,
387
1230
  report: ImportReportBuilder,
388
1231
  options: ReadOptions = {},
389
1232
  ): AsyncGenerator<string, void, undefined> {
1233
+ const input = normalizeInput(rawInput);
390
1234
  const signal = options.signal ?? null;
391
1235
  const onProgress = options.onProgress ?? null;
392
- const total = inputLength(input);
393
- let done = 0;
394
- let first = true;
395
- const emit = (text: string): string => {
396
- if (first && text.length > 0) {
397
- first = false;
398
- return text.startsWith(BOM) ? text.slice(1) : text;
399
- }
400
- return text;
401
- };
1236
+ const filter = new TextFilter(report, options);
402
1237
  throwIfAborted(signal);
403
1238
  if (typeof input === "string") {
404
- const text = emit(input);
405
- done = input.length;
406
- if (text.length > 0) {
407
- yield text;
1239
+ reportTextEncoding(report, options.encoding);
1240
+ const total = onProgress === null ? 0 : utf8Length(input);
1241
+ let done = 0;
1242
+ let start = 0;
1243
+ while (start < input.length) {
1244
+ throwIfAborted(signal);
1245
+ let end = Math.min(start + DECODE_SLICE, input.length);
1246
+ const highSurrogate = (input.charCodeAt(end - 1) & 0xfc00) === 0xd800; // NOSONAR(S7758): reads UTF-16 code units on purpose
1247
+ if (end < input.length && highSurrogate) {
1248
+ end++; // never split a surrogate pair
1249
+ }
1250
+ const piece = start === 0 && end === input.length ? input : input.slice(start, end);
1251
+ start = end;
1252
+ const text = filter.push(piece);
1253
+ if (text.length > 0) {
1254
+ yield text;
1255
+ }
1256
+ if (onProgress !== null) {
1257
+ done += utf8Length(piece);
1258
+ onProgress(done, total);
1259
+ }
408
1260
  }
409
- onProgress?.(done, total ?? undefined);
1261
+ if (input.length === 0) {
1262
+ onProgress?.(0, 0);
1263
+ }
1264
+ filter.end();
410
1265
  return;
411
1266
  }
412
1267
  const decoder = new ByteDecoder(report, options);
1268
+ const headBytes = Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
413
1269
  if (input instanceof Uint8Array) {
414
- decoder.start(input.subarray(0, HEAD_BYTES));
415
- for (let offset = 0; offset < input.byteLength; offset += DECODE_SLICE) {
1270
+ const length = input.byteLength;
1271
+ decoder.start(input.subarray(0, headBytes));
1272
+ for (let offset = 0; offset < length; offset += DECODE_SLICE) {
416
1273
  throwIfAborted(signal);
417
- const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, input.byteLength));
418
- const text = emit(decoder.decode(slice, offset + DECODE_SLICE < input.byteLength));
419
- done = Math.min(offset + DECODE_SLICE, input.byteLength);
1274
+ if (input.byteLength !== length) {
1275
+ throw new GraphFormatError(
1276
+ "E_UNSUPPORTED",
1277
+ "the input's buffer was detached (transferred) or resized during the import",
1278
+ { reason: "detached" },
1279
+ );
1280
+ }
1281
+ const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
1282
+ const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
420
1283
  if (text.length > 0) {
421
1284
  yield text;
422
1285
  }
423
- onProgress?.(done, total ?? undefined);
1286
+ onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
424
1287
  }
425
- if (input.byteLength === 0) {
1288
+ if (length === 0) {
426
1289
  onProgress?.(0, 0);
427
1290
  }
1291
+ filter.end();
428
1292
  return;
429
1293
  }
430
1294
  // the first bytes of a stream are held back until the BOM check (and the declaration check,
431
1295
  // when the format has one) can see enough of them, or the input ended, or text arrived, so a
432
1296
  // BOM or a declaration split across small chunks is still seen
433
- const needed = options.declaredEncoding === undefined ? BOM_BYTES : HEAD_BYTES;
1297
+ const needed = options.declaredEncoding === undefined ? BOM_BYTES : headBytes;
1298
+ let done = 0;
434
1299
  let head: Uint8Array[] = [];
435
1300
  let headLength = 0;
436
1301
  const flushHead = (stream: boolean): string => {
437
1302
  const bytes = concatBytes(head);
438
1303
  head = [];
439
1304
  headLength = 0;
440
- decoder.start(bytes.subarray(0, HEAD_BYTES));
1305
+ decoder.start(bytes.subarray(0, headBytes));
441
1306
  return decoder.decode(bytes, stream);
442
1307
  };
443
1308
  const chunks = isReadableStream(input) ? streamChunks(input, signal) : input;
444
1309
  for await (const chunk of chunks) {
445
1310
  throwIfAborted(signal);
446
1311
  let text: string;
1312
+ const bytes = typeof chunk === "string" ? null : asBytes(chunk);
447
1313
  if (typeof chunk === "string") {
448
- // finish any byte sequence still pending in the decoder before switching to text
1314
+ // finish any byte sequence still pending in the decoder before switching to text; the
1315
+ // BOM and declaration checks apply to the start of the input only, so bytes after text
1316
+ // are never taken for a BOM
449
1317
  let pending = "";
450
- if (!decoder.started && headLength > 0) {
451
- pending = flushHead(false);
1318
+ if (headLength > 0) {
1319
+ pending = decoder.flushBeforeText(() => flushHead(false));
452
1320
  } else if (decoder.started) {
453
- pending = decoder.decode(new Uint8Array(0), false);
1321
+ pending = decoder.flushBeforeText(() => decoder.decode(new Uint8Array(0), false));
1322
+ } else {
1323
+ decoder.start(new Uint8Array(0));
454
1324
  }
455
- text = emit(pending + chunk);
456
- done += chunk.length;
457
- } else if (chunk instanceof Uint8Array) {
458
- done += chunk.byteLength;
1325
+ text = filter.push(pending + chunk);
1326
+ done += onProgress === null ? 0 : utf8Length(chunk);
1327
+ } else if (bytes !== null) {
1328
+ done += bytes.byteLength;
459
1329
  if (decoder.started) {
460
- text = emit(decoder.decode(chunk, true));
1330
+ text = filter.push(decoder.decode(bytes, true));
461
1331
  } else {
462
- head.push(chunk);
463
- headLength += chunk.byteLength;
464
- text = headLength >= needed ? emit(flushHead(true)) : "";
1332
+ head.push(bytes);
1333
+ headLength += bytes.byteLength;
1334
+ text = headLength >= needed ? filter.push(flushHead(true)) : "";
465
1335
  }
466
1336
  } else {
467
1337
  throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
@@ -475,15 +1345,16 @@ export async function* textChunks(
475
1345
  onProgress?.(done);
476
1346
  }
477
1347
  let tail = "";
478
- if (!decoder.started && headLength > 0) {
1348
+ if (headLength > 0) {
479
1349
  tail = flushHead(false);
480
1350
  } else if (decoder.started) {
481
1351
  tail = decoder.decode(new Uint8Array(0), false);
482
1352
  }
483
- tail = emit(tail);
1353
+ tail = filter.push(tail);
484
1354
  if (tail.length > 0) {
485
1355
  yield tail;
486
1356
  }
1357
+ filter.end();
487
1358
  onProgress?.(done, done);
488
1359
  }
489
1360
 
@@ -499,7 +1370,8 @@ function isReadableStream(
499
1370
  }
500
1371
 
501
1372
  /**
502
- * Iterate a ReadableStream through a reader, cancelling the stream when iteration stops early.
1373
+ * Iterate a ReadableStream through a reader, cancelling the stream (with the signal's reason) when
1374
+ * iteration stops early or the signal fires, also while a read is pending.
503
1375
  * @param stream - the stream
504
1376
  * @param signal - the cancellation signal, or null
505
1377
  * @yields the stream's chunks
@@ -509,12 +1381,12 @@ async function* streamChunks(
509
1381
  stream: ReadableStream<Uint8Array>,
510
1382
  signal: AbortSignal | null,
511
1383
  ): AsyncGenerator<Uint8Array, void, undefined> {
512
- const reader = stream.getReader();
1384
+ const reader = lockReader(stream);
513
1385
  let finished = false;
514
1386
  try {
515
1387
  for (;;) {
516
1388
  throwIfAborted(signal);
517
- const { done, value } = await reader.read();
1389
+ const { done, value } = await abortable(reader.read(), signal);
518
1390
  if (done) {
519
1391
  finished = true;
520
1392
  return;
@@ -531,14 +1403,15 @@ async function* streamChunks(
531
1403
 
532
1404
  /**
533
1405
  * Read the whole input as bytes, for a binary format (a zip): nothing is decoded. The signal is
534
- * checked before every chunk and progress reported after it; a stream is cancelled when the read
1406
+ * checked before every chunk and progress reported after it; a stream is canceled when the read
535
1407
  * stops early.
536
- * @param input - the input
1408
+ * @param rawInput - the input
537
1409
  * @param options - cancellation and progress (the encoding is ignored)
538
1410
  * @returns the bytes, or null when the input is text (a string, or a chunk that is a string),
539
1411
  * which a binary format cannot read
540
1412
  */
541
- export async function readBytes(input: ImportInput, options: ReadOptions = {}): Promise<Uint8Array | null> {
1413
+ export async function readBytes(rawInput: ImportInput, options: ReadOptions = {}): Promise<Uint8Array | null> {
1414
+ const input = normalizeInput(rawInput);
542
1415
  const signal = options.signal ?? null;
543
1416
  throwIfAborted(signal);
544
1417
  if (typeof input === "string") {
@@ -555,14 +1428,15 @@ export async function readBytes(input: ImportInput, options: ReadOptions = {}):
555
1428
  if (typeof chunk === "string") {
556
1429
  return null;
557
1430
  }
558
- if (!(chunk instanceof Uint8Array)) {
1431
+ const bytes = asBytes(chunk);
1432
+ if (bytes === null) {
559
1433
  throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
560
1434
  reason: "chunk type",
561
1435
  found: typeof chunk,
562
1436
  });
563
1437
  }
564
- parts.push(chunk);
565
- done += chunk.byteLength;
1438
+ parts.push(bytes);
1439
+ done += bytes.byteLength;
566
1440
  options.onProgress?.(done);
567
1441
  }
568
1442
  options.onProgress?.(done, done);
@@ -570,24 +1444,54 @@ export async function readBytes(input: ImportInput, options: ReadOptions = {}):
570
1444
  }
571
1445
 
572
1446
  /**
573
- * Read the whole input as one string (the GML / DOT / JSON path, design section 8.4).
574
- * @param input - the input
1447
+ * Fail an import whose text is longer than one JavaScript string can hold.
1448
+ * @param report - the report
1449
+ * @param message - what is too long
1450
+ * @param line - the line, when known
1451
+ * @category Plugin helpers
1452
+ */
1453
+ export function tooLarge(report: ImportReportBuilder, message: string, line?: number): never {
1454
+ report.failWith("unsupported", TOO_LARGE_CODE, message, line === undefined ? undefined : { line });
1455
+ }
1456
+
1457
+ /**
1458
+ * Read the whole input as one string (what the GML, DOT and JSON importers do), failing with
1459
+ * E_TOO_LARGE (category unsupported) before the join when it is longer than one JavaScript string
1460
+ * can hold. A string input is used as it is (one progress call), after the same text checks.
1461
+ * @param rawInput - the input
575
1462
  * @param report - the report the decode error is recorded in
576
1463
  * @param options - cancellation and progress
577
- * @returns the decoded text without a leading BOM
1464
+ * @returns the decoded text without a leading BOM or a trailing Ctrl-Z
1465
+ * @category Writing a format
578
1466
  */
579
1467
  export async function readText(
580
- input: ImportInput,
1468
+ rawInput: ImportInput,
581
1469
  report: ImportReportBuilder,
582
1470
  options: ReadOptions = {},
583
1471
  ): Promise<string> {
1472
+ const input = normalizeInput(rawInput);
584
1473
  if (typeof input === "string") {
585
1474
  throwIfAborted(options.signal);
586
- options.onProgress?.(input.length, input.length);
587
- return input.startsWith(BOM) ? input.slice(1) : input;
1475
+ reportTextEncoding(report, options.encoding);
1476
+ const filter = new TextFilter(report, options);
1477
+ const text = filter.push(input);
1478
+ filter.end();
1479
+ if (options.onProgress !== null && options.onProgress !== undefined) {
1480
+ const total = utf8Length(input);
1481
+ options.onProgress(total, total);
1482
+ }
1483
+ return text;
588
1484
  }
589
1485
  const parts: string[] = [];
1486
+ let length = 0;
590
1487
  for await (const chunk of textChunks(input, report, options)) {
1488
+ length += chunk.length;
1489
+ if (length > MAX_TEXT_LENGTH) {
1490
+ tooLarge(
1491
+ report,
1492
+ `the document is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`,
1493
+ );
1494
+ }
591
1495
  parts.push(chunk);
592
1496
  }
593
1497
  return parts.length === 1 ? parts[0] : parts.join("");
@@ -599,6 +1503,7 @@ export async function readText(
599
1503
  * `\n`, `\r\n` and lone `\r` all end a line; the terminator is not part of the text; a final
600
1504
  * line without a terminator is yielded when non-empty, and every line in between is yielded even
601
1505
  * when empty (the importer decides what a blank line means).
1506
+ * @category Writing a format
602
1507
  */
603
1508
  export class LineReader implements AsyncIterable<string> {
604
1509
  private readonly input: ImportInput;
@@ -629,6 +1534,14 @@ export class LineReader implements AsyncIterable<string> {
629
1534
  return this.lineNumber;
630
1535
  }
631
1536
 
1537
+ /**
1538
+ * The E_TOO_LARGE message of a line longer than one string.
1539
+ * @returns the message
1540
+ */
1541
+ private lineTooLong(): string {
1542
+ return `a line is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`;
1543
+ }
1544
+
632
1545
  /**
633
1546
  * Iterate the lines.
634
1547
  * @yields one line at a time, terminator removed
@@ -639,6 +1552,7 @@ export class LineReader implements AsyncIterable<string> {
639
1552
  // may end with a `\r` whose meaning the next chunk decides); joined once when the line ends,
640
1553
  // so a line spanning many chunks costs its length, not its length times the chunk count.
641
1554
  const pending: string[] = [];
1555
+ let pendingLength = 0;
642
1556
  let trailingCr = false;
643
1557
  for await (const chunk of textChunks(this.input, this.report, this.options)) {
644
1558
  let start = 0;
@@ -650,6 +1564,7 @@ export class LineReader implements AsyncIterable<string> {
650
1564
  this.lineNumber++;
651
1565
  const joined = pending.join("");
652
1566
  pending.length = 0;
1567
+ pendingLength = 0;
653
1568
  yield joined.slice(0, -1);
654
1569
  if (chunk.charCodeAt(0) === 10) {
655
1570
  start = 1;
@@ -678,9 +1593,13 @@ export class LineReader implements AsyncIterable<string> {
678
1593
  if (pending.length === 0) {
679
1594
  yield piece;
680
1595
  } else {
1596
+ if (pendingLength + piece.length > MAX_TEXT_LENGTH) {
1597
+ tooLarge(this.report, this.lineTooLong(), this.lineNumber);
1598
+ }
681
1599
  pending.push(piece);
682
1600
  const joined = pending.join("");
683
1601
  pending.length = 0;
1602
+ pendingLength = 0;
684
1603
  yield joined;
685
1604
  }
686
1605
  start = next;
@@ -692,6 +1611,10 @@ export class LineReader implements AsyncIterable<string> {
692
1611
  }
693
1612
  }
694
1613
  if (start < end) {
1614
+ pendingLength += end - start;
1615
+ if (pendingLength > MAX_TEXT_LENGTH) {
1616
+ tooLarge(this.report, this.lineTooLong(), this.lineNumber + 1);
1617
+ }
695
1618
  pending.push(start === 0 ? chunk : chunk.slice(start));
696
1619
  trailingCr = chunk.charCodeAt(end - 1) === 13;
697
1620
  }