@graphty/graph-io 0.3.25 → 0.3.26

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