@graphty/graph-io 0.3.25 → 0.3.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/README.md +49 -414
  2. package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
  3. package/dist/chunks/choose-DrXtu_96.js +56 -0
  4. package/dist/chunks/choose-DrXtu_96.js.map +1 -0
  5. package/dist/chunks/constants-ikG85dT8.js +231 -0
  6. package/dist/chunks/constants-ikG85dT8.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D1HPTUrP.js} +390 -99
  8. package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
  9. package/dist/chunks/export-CHd4tQE2.js +4333 -0
  10. package/dist/chunks/export-CHd4tQE2.js.map +1 -0
  11. package/dist/chunks/{importer-eUREhBGj.js → exporter-D23CKNqF.js} +4969 -3201
  12. package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
  13. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-DfWTgrti.js} +23 -21
  14. package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
  15. package/dist/chunks/exporter-DhtW6FxK.js +3465 -0
  16. package/dist/chunks/exporter-DhtW6FxK.js.map +1 -0
  17. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-IHv4nkCm.js} +38 -229
  18. package/dist/chunks/exporter-IHv4nkCm.js.map +1 -0
  19. package/dist/chunks/{importer-BWY2FFc5.js → importer-B1wmPQjr.js} +255 -77
  20. package/dist/chunks/importer-B1wmPQjr.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-B2W_58pz.js} +399 -146
  22. package/dist/chunks/importer-B2W_58pz.js.map +1 -0
  23. package/dist/chunks/importer-BSALvkui.js +2811 -0
  24. package/dist/chunks/importer-BSALvkui.js.map +1 -0
  25. package/dist/chunks/{importer-vi3bSdtw.js → importer-BVRE--Un.js} +510 -77
  26. package/dist/chunks/importer-BVRE--Un.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-CIAzt-wo.js} +1689 -312
  28. package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
  29. package/dist/chunks/{importer-DIrbbnAf.js → importer-DpEW5zeG.js} +585 -226
  30. package/dist/chunks/importer-DpEW5zeG.js.map +1 -0
  31. package/dist/chunks/{importer-zGLo8Dg_.js → importer-z_FL4VMb.js} +488 -161
  32. package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
  33. package/dist/chunks/json-elements-BAmNBZok.js +1225 -0
  34. package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
  35. package/dist/chunks/ontology-export-Bab4eUA8.js +357 -0
  36. package/dist/chunks/ontology-export-Bab4eUA8.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CdlvL4RO.js} +298 -114
  38. package/dist/chunks/records-CdlvL4RO.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DzXjAiTl.js} +29 -11
  40. package/dist/chunks/text-DzXjAiTl.js.map +1 -0
  41. package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
  42. package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
  43. package/dist/csv.js +952 -262
  44. package/dist/csv.js.map +1 -1
  45. package/dist/cx.js +6 -3
  46. package/dist/cx2.js +2 -2
  47. package/dist/cys.js +6 -3
  48. package/dist/dot.js +1 -1
  49. package/dist/gexf.js +554 -122
  50. package/dist/gexf.js.map +1 -1
  51. package/dist/gml.js +344 -87
  52. package/dist/gml.js.map +1 -1
  53. package/dist/graph-io.js +1105 -213
  54. package/dist/graph-io.js.map +1 -1
  55. package/dist/graphml.js +3 -2
  56. package/dist/graphml.js.map +1 -1
  57. package/dist/json.js +3 -2
  58. package/dist/neo4j.js +565 -158
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +6 -3
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/children.d.ts +18 -6
  63. package/dist/src/children.d.ts.map +1 -1
  64. package/dist/src/children.js +6 -3
  65. package/dist/src/children.js.map +1 -1
  66. package/dist/src/common/attributes.d.ts +29 -11
  67. package/dist/src/common/attributes.d.ts.map +1 -1
  68. package/dist/src/common/attributes.js +25 -7
  69. package/dist/src/common/attributes.js.map +1 -1
  70. package/dist/src/common/cell-budget.d.ts +9 -2
  71. package/dist/src/common/cell-budget.d.ts.map +1 -1
  72. package/dist/src/common/cell-budget.js +11 -3
  73. package/dist/src/common/cell-budget.js.map +1 -1
  74. package/dist/src/common/choose.d.ts +19 -0
  75. package/dist/src/common/choose.d.ts.map +1 -0
  76. package/dist/src/common/choose.js +72 -0
  77. package/dist/src/common/choose.js.map +1 -0
  78. package/dist/src/common/codes.d.ts +456 -66
  79. package/dist/src/common/codes.d.ts.map +1 -1
  80. package/dist/src/common/codes.js +458 -66
  81. package/dist/src/common/codes.js.map +1 -1
  82. package/dist/src/common/compare.d.ts +81 -0
  83. package/dist/src/common/compare.d.ts.map +1 -0
  84. package/dist/src/common/compare.js +394 -0
  85. package/dist/src/common/compare.js.map +1 -0
  86. package/dist/src/common/cx-export.d.ts +123 -0
  87. package/dist/src/common/cx-export.d.ts.map +1 -0
  88. package/dist/src/common/cx-export.js +334 -0
  89. package/dist/src/common/cx-export.js.map +1 -0
  90. package/dist/src/common/declared-types.d.ts +22 -2
  91. package/dist/src/common/declared-types.d.ts.map +1 -1
  92. package/dist/src/common/declared-types.js +21 -3
  93. package/dist/src/common/declared-types.js.map +1 -1
  94. package/dist/src/common/direction.d.ts +74 -50
  95. package/dist/src/common/direction.d.ts.map +1 -1
  96. package/dist/src/common/direction.js +47 -27
  97. package/dist/src/common/direction.js.map +1 -1
  98. package/dist/src/common/escape.d.ts +21 -9
  99. package/dist/src/common/escape.d.ts.map +1 -1
  100. package/dist/src/common/escape.js +39 -22
  101. package/dist/src/common/escape.js.map +1 -1
  102. package/dist/src/common/export.d.ts +144 -32
  103. package/dist/src/common/export.d.ts.map +1 -1
  104. package/dist/src/common/export.js +216 -42
  105. package/dist/src/common/export.js.map +1 -1
  106. package/dist/src/common/format.d.ts +23 -1
  107. package/dist/src/common/format.d.ts.map +1 -1
  108. package/dist/src/common/format.js +26 -1
  109. package/dist/src/common/format.js.map +1 -1
  110. package/dist/src/common/ids.d.ts +9 -2
  111. package/dist/src/common/ids.d.ts.map +1 -1
  112. package/dist/src/common/ids.js +6 -2
  113. package/dist/src/common/ids.js.map +1 -1
  114. package/dist/src/common/input.d.ts +122 -28
  115. package/dist/src/common/input.d.ts.map +1 -1
  116. package/dist/src/common/input.js +890 -124
  117. package/dist/src/common/input.js.map +1 -1
  118. package/dist/src/common/json-elements.d.ts +144 -20
  119. package/dist/src/common/json-elements.d.ts.map +1 -1
  120. package/dist/src/common/json-elements.js +869 -189
  121. package/dist/src/common/json-elements.js.map +1 -1
  122. package/dist/src/common/line-format.d.ts +90 -0
  123. package/dist/src/common/line-format.d.ts.map +1 -0
  124. package/dist/src/common/line-format.js +185 -0
  125. package/dist/src/common/line-format.js.map +1 -0
  126. package/dist/src/common/lists.d.ts +6 -1
  127. package/dist/src/common/lists.d.ts.map +1 -1
  128. package/dist/src/common/lists.js +3 -1
  129. package/dist/src/common/lists.js.map +1 -1
  130. package/dist/src/common/ontology-export.d.ts +167 -0
  131. package/dist/src/common/ontology-export.d.ts.map +1 -0
  132. package/dist/src/common/ontology-export.js +359 -0
  133. package/dist/src/common/ontology-export.js.map +1 -0
  134. package/dist/src/common/ontology.d.ts +25 -3
  135. package/dist/src/common/ontology.d.ts.map +1 -1
  136. package/dist/src/common/ontology.js +30 -9
  137. package/dist/src/common/ontology.js.map +1 -1
  138. package/dist/src/common/options.d.ts +62 -12
  139. package/dist/src/common/options.d.ts.map +1 -1
  140. package/dist/src/common/options.js +89 -11
  141. package/dist/src/common/options.js.map +1 -1
  142. package/dist/src/common/plural.d.ts +31 -0
  143. package/dist/src/common/plural.d.ts.map +1 -0
  144. package/dist/src/common/plural.js +46 -0
  145. package/dist/src/common/plural.js.map +1 -0
  146. package/dist/src/common/report.d.ts +72 -14
  147. package/dist/src/common/report.d.ts.map +1 -1
  148. package/dist/src/common/report.js +146 -22
  149. package/dist/src/common/report.js.map +1 -1
  150. package/dist/src/common/temporal.d.ts +22 -5
  151. package/dist/src/common/temporal.d.ts.map +1 -1
  152. package/dist/src/common/temporal.js +13 -2
  153. package/dist/src/common/temporal.js.map +1 -1
  154. package/dist/src/common/text.d.ts +39 -23
  155. package/dist/src/common/text.d.ts.map +1 -1
  156. package/dist/src/common/text.js +51 -21
  157. package/dist/src/common/text.js.map +1 -1
  158. package/dist/src/common/weights.d.ts +37 -4
  159. package/dist/src/common/weights.d.ts.map +1 -1
  160. package/dist/src/common/weights.js +61 -2
  161. package/dist/src/common/weights.js.map +1 -1
  162. package/dist/src/common/writer.d.ts +19 -3
  163. package/dist/src/common/writer.d.ts.map +1 -1
  164. package/dist/src/common/writer.js +30 -2
  165. package/dist/src/common/writer.js.map +1 -1
  166. package/dist/src/common/xml.d.ts +125 -9
  167. package/dist/src/common/xml.d.ts.map +1 -1
  168. package/dist/src/common/xml.js +402 -85
  169. package/dist/src/common/xml.js.map +1 -1
  170. package/dist/src/common/zip.d.ts +44 -4
  171. package/dist/src/common/zip.d.ts.map +1 -1
  172. package/dist/src/common/zip.js +134 -5
  173. package/dist/src/common/zip.js.map +1 -1
  174. package/dist/src/formats/csv/exporter.d.ts +80 -24
  175. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  176. package/dist/src/formats/csv/exporter.js +146 -33
  177. package/dist/src/formats/csv/exporter.js.map +1 -1
  178. package/dist/src/formats/csv/header.d.ts +38 -9
  179. package/dist/src/formats/csv/header.d.ts.map +1 -1
  180. package/dist/src/formats/csv/header.js +46 -10
  181. package/dist/src/formats/csv/header.js.map +1 -1
  182. package/dist/src/formats/csv/importer.d.ts +187 -37
  183. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  184. package/dist/src/formats/csv/importer.js +835 -204
  185. package/dist/src/formats/csv/importer.js.map +1 -1
  186. package/dist/src/formats/csv/index.d.ts +100 -19
  187. package/dist/src/formats/csv/index.d.ts.map +1 -1
  188. package/dist/src/formats/csv/index.js +94 -21
  189. package/dist/src/formats/csv/index.js.map +1 -1
  190. package/dist/src/formats/csv/records.d.ts +95 -10
  191. package/dist/src/formats/csv/records.d.ts.map +1 -1
  192. package/dist/src/formats/csv/records.js +333 -122
  193. package/dist/src/formats/csv/records.js.map +1 -1
  194. package/dist/src/formats/csv/values.d.ts +1 -0
  195. package/dist/src/formats/csv/values.d.ts.map +1 -1
  196. package/dist/src/formats/csv/values.js +1 -0
  197. package/dist/src/formats/csv/values.js.map +1 -1
  198. package/dist/src/formats/cx/exporter.d.ts +102 -0
  199. package/dist/src/formats/cx/exporter.d.ts.map +1 -0
  200. package/dist/src/formats/cx/exporter.js +1217 -0
  201. package/dist/src/formats/cx/exporter.js.map +1 -0
  202. package/dist/src/formats/cx/importer.d.ts +79 -32
  203. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  204. package/dist/src/formats/cx/importer.js +682 -287
  205. package/dist/src/formats/cx/importer.js.map +1 -1
  206. package/dist/src/formats/cx/index.d.ts +4 -3
  207. package/dist/src/formats/cx/index.d.ts.map +1 -1
  208. package/dist/src/formats/cx/index.js +4 -3
  209. package/dist/src/formats/cx/index.js.map +1 -1
  210. package/dist/src/formats/cx2/exporter.d.ts +29 -11
  211. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  212. package/dist/src/formats/cx2/exporter.js +39 -268
  213. package/dist/src/formats/cx2/exporter.js.map +1 -1
  214. package/dist/src/formats/cx2/importer.d.ts +78 -28
  215. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  216. package/dist/src/formats/cx2/importer.js +227 -80
  217. package/dist/src/formats/cx2/importer.js.map +1 -1
  218. package/dist/src/formats/cx2/index.d.ts +2 -1
  219. package/dist/src/formats/cx2/index.d.ts.map +1 -1
  220. package/dist/src/formats/cx2/index.js +2 -1
  221. package/dist/src/formats/cx2/index.js.map +1 -1
  222. package/dist/src/formats/cys/constants.d.ts +66 -17
  223. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  224. package/dist/src/formats/cys/constants.js +59 -18
  225. package/dist/src/formats/cys/constants.js.map +1 -1
  226. package/dist/src/formats/cys/exporter.d.ts +118 -0
  227. package/dist/src/formats/cys/exporter.d.ts.map +1 -0
  228. package/dist/src/formats/cys/exporter.js +1072 -0
  229. package/dist/src/formats/cys/exporter.js.map +1 -0
  230. package/dist/src/formats/cys/importer.d.ts +22 -8
  231. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  232. package/dist/src/formats/cys/importer.js +510 -149
  233. package/dist/src/formats/cys/importer.js.map +1 -1
  234. package/dist/src/formats/cys/index.d.ts +4 -2
  235. package/dist/src/formats/cys/index.d.ts.map +1 -1
  236. package/dist/src/formats/cys/index.js +4 -2
  237. package/dist/src/formats/cys/index.js.map +1 -1
  238. package/dist/src/formats/cys/session.d.ts +46 -7
  239. package/dist/src/formats/cys/session.d.ts.map +1 -1
  240. package/dist/src/formats/cys/session.js +101 -27
  241. package/dist/src/formats/cys/session.js.map +1 -1
  242. package/dist/src/formats/cys/tables.d.ts +9 -2
  243. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  244. package/dist/src/formats/cys/tables.js +29 -7
  245. package/dist/src/formats/cys/tables.js.map +1 -1
  246. package/dist/src/formats/dot/exporter.d.ts +42 -14
  247. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  248. package/dist/src/formats/dot/exporter.js +56 -29
  249. package/dist/src/formats/dot/exporter.js.map +1 -1
  250. package/dist/src/formats/dot/importer.d.ts +91 -29
  251. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  252. package/dist/src/formats/dot/importer.js +190 -39
  253. package/dist/src/formats/dot/importer.js.map +1 -1
  254. package/dist/src/formats/dot/index.d.ts +2 -1
  255. package/dist/src/formats/dot/index.d.ts.map +1 -1
  256. package/dist/src/formats/dot/index.js +2 -1
  257. package/dist/src/formats/dot/index.js.map +1 -1
  258. package/dist/src/formats/dot/names.d.ts +44 -11
  259. package/dist/src/formats/dot/names.d.ts.map +1 -1
  260. package/dist/src/formats/dot/names.js +44 -11
  261. package/dist/src/formats/dot/names.js.map +1 -1
  262. package/dist/src/formats/dot/tokenizer.d.ts +9 -2
  263. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  264. package/dist/src/formats/dot/tokenizer.js +20 -5
  265. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  266. package/dist/src/formats/gexf/exporter.d.ts +46 -15
  267. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  268. package/dist/src/formats/gexf/exporter.js +45 -15
  269. package/dist/src/formats/gexf/exporter.js.map +1 -1
  270. package/dist/src/formats/gexf/importer.d.ts +87 -23
  271. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  272. package/dist/src/formats/gexf/importer.js +471 -90
  273. package/dist/src/formats/gexf/importer.js.map +1 -1
  274. package/dist/src/formats/gexf/index.d.ts +84 -21
  275. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  276. package/dist/src/formats/gexf/index.js +80 -23
  277. package/dist/src/formats/gexf/index.js.map +1 -1
  278. package/dist/src/formats/gexf/schema.d.ts +67 -15
  279. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  280. package/dist/src/formats/gexf/schema.js +59 -13
  281. package/dist/src/formats/gexf/schema.js.map +1 -1
  282. package/dist/src/formats/gml/exporter.d.ts +69 -21
  283. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/gml/exporter.js +74 -29
  285. package/dist/src/formats/gml/exporter.js.map +1 -1
  286. package/dist/src/formats/gml/importer.d.ts +116 -25
  287. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  288. package/dist/src/formats/gml/importer.js +290 -61
  289. package/dist/src/formats/gml/importer.js.map +1 -1
  290. package/dist/src/formats/gml/index.d.ts +87 -24
  291. package/dist/src/formats/gml/index.d.ts.map +1 -1
  292. package/dist/src/formats/gml/index.js +79 -26
  293. package/dist/src/formats/gml/index.js.map +1 -1
  294. package/dist/src/formats/gml/syntax.d.ts +67 -18
  295. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  296. package/dist/src/formats/gml/syntax.js +110 -28
  297. package/dist/src/formats/gml/syntax.js.map +1 -1
  298. package/dist/src/formats/graphml/constants.d.ts +174 -44
  299. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  300. package/dist/src/formats/graphml/constants.js +165 -44
  301. package/dist/src/formats/graphml/constants.js.map +1 -1
  302. package/dist/src/formats/graphml/exporter.d.ts +17 -9
  303. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  304. package/dist/src/formats/graphml/exporter.js +33 -12
  305. package/dist/src/formats/graphml/exporter.js.map +1 -1
  306. package/dist/src/formats/graphml/importer.d.ts +19 -7
  307. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  308. package/dist/src/formats/graphml/importer.js +466 -39
  309. package/dist/src/formats/graphml/importer.js.map +1 -1
  310. package/dist/src/formats/graphml/index.d.ts +2 -1
  311. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  312. package/dist/src/formats/graphml/index.js +2 -1
  313. package/dist/src/formats/graphml/index.js.map +1 -1
  314. package/dist/src/formats/graphml/tree.d.ts +6 -2
  315. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  316. package/dist/src/formats/graphml/tree.js +6 -2
  317. package/dist/src/formats/graphml/tree.js.map +1 -1
  318. package/dist/src/formats/graphml/yfiles.d.ts +8 -3
  319. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  320. package/dist/src/formats/graphml/yfiles.js +36 -15
  321. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  322. package/dist/src/formats/json/dialect.d.ts +88 -24
  323. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  324. package/dist/src/formats/json/dialect.js +109 -19
  325. package/dist/src/formats/json/dialect.js.map +1 -1
  326. package/dist/src/formats/json/exporter.d.ts +134 -22
  327. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  328. package/dist/src/formats/json/exporter.js +135 -54
  329. package/dist/src/formats/json/exporter.js.map +1 -1
  330. package/dist/src/formats/json/importer.d.ts +173 -52
  331. package/dist/src/formats/json/importer.d.ts.map +1 -1
  332. package/dist/src/formats/json/importer.js +899 -220
  333. package/dist/src/formats/json/importer.js.map +1 -1
  334. package/dist/src/formats/json/index.d.ts +3 -2
  335. package/dist/src/formats/json/index.d.ts.map +1 -1
  336. package/dist/src/formats/json/index.js +3 -2
  337. package/dist/src/formats/json/index.js.map +1 -1
  338. package/dist/src/formats/json/obographs-export.d.ts +68 -0
  339. package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
  340. package/dist/src/formats/json/obographs-export.js +941 -0
  341. package/dist/src/formats/json/obographs-export.js.map +1 -0
  342. package/dist/src/formats/json/obographs.d.ts +26 -0
  343. package/dist/src/formats/json/obographs.d.ts.map +1 -1
  344. package/dist/src/formats/json/obographs.js +297 -80
  345. package/dist/src/formats/json/obographs.js.map +1 -1
  346. package/dist/src/formats/neo4j/exporter.d.ts +109 -21
  347. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  348. package/dist/src/formats/neo4j/exporter.js +239 -86
  349. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  350. package/dist/src/formats/neo4j/header.d.ts +17 -4
  351. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  352. package/dist/src/formats/neo4j/header.js +36 -4
  353. package/dist/src/formats/neo4j/header.js.map +1 -1
  354. package/dist/src/formats/neo4j/importer.d.ts +107 -27
  355. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  356. package/dist/src/formats/neo4j/importer.js +415 -63
  357. package/dist/src/formats/neo4j/importer.js.map +1 -1
  358. package/dist/src/formats/neo4j/index.d.ts +73 -23
  359. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  360. package/dist/src/formats/neo4j/index.js +65 -25
  361. package/dist/src/formats/neo4j/index.js.map +1 -1
  362. package/dist/src/formats/obo/exporter.d.ts +150 -0
  363. package/dist/src/formats/obo/exporter.d.ts.map +1 -0
  364. package/dist/src/formats/obo/exporter.js +1736 -0
  365. package/dist/src/formats/obo/exporter.js.map +1 -0
  366. package/dist/src/formats/obo/importer.d.ts +95 -19
  367. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  368. package/dist/src/formats/obo/importer.js +475 -104
  369. package/dist/src/formats/obo/importer.js.map +1 -1
  370. package/dist/src/formats/obo/index.d.ts +6 -3
  371. package/dist/src/formats/obo/index.d.ts.map +1 -1
  372. package/dist/src/formats/obo/index.js +6 -3
  373. package/dist/src/formats/obo/index.js.map +1 -1
  374. package/dist/src/formats/obo/syntax.d.ts +108 -4
  375. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  376. package/dist/src/formats/obo/syntax.js +211 -34
  377. package/dist/src/formats/obo/syntax.js.map +1 -1
  378. package/dist/src/formats/pajek/exporter.d.ts +54 -16
  379. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  380. package/dist/src/formats/pajek/exporter.js +67 -35
  381. package/dist/src/formats/pajek/exporter.js.map +1 -1
  382. package/dist/src/formats/pajek/importer.d.ts +84 -22
  383. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  384. package/dist/src/formats/pajek/importer.js +303 -83
  385. package/dist/src/formats/pajek/importer.js.map +1 -1
  386. package/dist/src/formats/pajek/index.d.ts +2 -1
  387. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  388. package/dist/src/formats/pajek/index.js +2 -1
  389. package/dist/src/formats/pajek/index.js.map +1 -1
  390. package/dist/src/formats/pajek/syntax.d.ts +77 -14
  391. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  392. package/dist/src/formats/pajek/syntax.js +186 -54
  393. package/dist/src/formats/pajek/syntax.js.map +1 -1
  394. package/dist/src/formats/xgmml/columns.d.ts +1 -0
  395. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  396. package/dist/src/formats/xgmml/columns.js +4 -0
  397. package/dist/src/formats/xgmml/columns.js.map +1 -1
  398. package/dist/src/formats/xgmml/constants.d.ts +186 -56
  399. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  400. package/dist/src/formats/xgmml/constants.js +181 -57
  401. package/dist/src/formats/xgmml/constants.js.map +1 -1
  402. package/dist/src/formats/xgmml/document.d.ts +65 -11
  403. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  404. package/dist/src/formats/xgmml/document.js +157 -28
  405. package/dist/src/formats/xgmml/document.js.map +1 -1
  406. package/dist/src/formats/xgmml/emit.d.ts +61 -3
  407. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  408. package/dist/src/formats/xgmml/emit.js +129 -26
  409. package/dist/src/formats/xgmml/emit.js.map +1 -1
  410. package/dist/src/formats/xgmml/exporter.d.ts +13 -6
  411. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  412. package/dist/src/formats/xgmml/exporter.js +21 -22
  413. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  414. package/dist/src/formats/xgmml/importer.d.ts +33 -12
  415. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  416. package/dist/src/formats/xgmml/importer.js +35 -72
  417. package/dist/src/formats/xgmml/importer.js.map +1 -1
  418. package/dist/src/formats/xgmml/index.d.ts +1 -0
  419. package/dist/src/formats/xgmml/index.d.ts.map +1 -1
  420. package/dist/src/formats/xgmml/index.js +1 -0
  421. package/dist/src/formats/xgmml/index.js.map +1 -1
  422. package/dist/src/formats/xgmml/values.d.ts +8 -1
  423. package/dist/src/formats/xgmml/values.d.ts.map +1 -1
  424. package/dist/src/formats/xgmml/values.js +8 -3
  425. package/dist/src/formats/xgmml/values.js.map +1 -1
  426. package/dist/src/index.d.ts +23 -18
  427. package/dist/src/index.d.ts.map +1 -1
  428. package/dist/src/index.js +28 -16
  429. package/dist/src/index.js.map +1 -1
  430. package/dist/src/registry.d.ts +478 -92
  431. package/dist/src/registry.d.ts.map +1 -1
  432. package/dist/src/registry.js +807 -88
  433. package/dist/src/registry.js.map +1 -1
  434. package/dist/src/sniff.d.ts +61 -19
  435. package/dist/src/sniff.d.ts.map +1 -1
  436. package/dist/src/sniff.js +121 -29
  437. package/dist/src/sniff.js.map +1 -1
  438. package/dist/src/types.d.ts +362 -112
  439. package/dist/src/types.d.ts.map +1 -1
  440. package/dist/src/types.js +28 -7
  441. package/dist/src/types.js.map +1 -1
  442. package/dist/xgmml.js +2 -2
  443. package/docs/samples/README.md +25 -0
  444. package/docs/samples/got-edges.csv +353 -0
  445. package/docs/samples/got-network.graphml +1384 -0
  446. package/docs/samples/got-nodes.csv +108 -0
  447. package/docs/samples/got.cx +930 -0
  448. package/docs/samples/got.cx2 +465 -0
  449. package/docs/samples/got.gexf +471 -0
  450. package/docs/samples/got.gml +2650 -0
  451. package/docs/samples/got.json +2546 -0
  452. package/docs/samples/got.net +461 -0
  453. package/docs/samples/movies-nodes.csv +9 -0
  454. package/docs/samples/movies-rels.csv +8 -0
  455. package/docs/samples/networks.cys +0 -0
  456. package/docs/samples/proteins.xgmml +9 -0
  457. package/docs/samples/teams.gv +24 -0
  458. package/docs/samples/vehicles.obo +25 -0
  459. package/package.json +9 -4
  460. package/src/children.ts +18 -6
  461. package/src/common/attributes.ts +29 -11
  462. package/src/common/cell-budget.ts +11 -3
  463. package/src/common/choose.ts +89 -0
  464. package/src/common/codes.ts +490 -66
  465. package/src/common/compare.ts +472 -0
  466. package/src/common/cx-export.ts +432 -0
  467. package/src/common/declared-types.ts +40 -6
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +39 -22
  470. package/src/common/export.ts +284 -47
  471. package/src/common/format.ts +30 -1
  472. package/src/common/ids.ts +14 -3
  473. package/src/common/input.ts +1057 -134
  474. package/src/common/json-elements.ts +1035 -199
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +7 -2
  477. package/src/common/ontology-export.ts +461 -0
  478. package/src/common/ontology.ts +31 -9
  479. package/src/common/options.ts +110 -15
  480. package/src/common/plural.ts +49 -0
  481. package/src/common/report.ts +168 -25
  482. package/src/common/temporal.ts +22 -5
  483. package/src/common/text.ts +62 -23
  484. package/src/common/weights.ts +74 -4
  485. package/src/common/writer.ts +37 -3
  486. package/src/common/xml.ts +479 -90
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +202 -47
  489. package/src/formats/csv/header.ts +50 -11
  490. package/src/formats/csv/importer.ts +1133 -245
  491. package/src/formats/csv/index.ts +106 -19
  492. package/src/formats/csv/records.ts +396 -122
  493. package/src/formats/csv/values.ts +1 -0
  494. package/src/formats/cx/exporter.ts +1483 -0
  495. package/src/formats/cx/importer.ts +890 -302
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +62 -339
  498. package/src/formats/cx2/importer.ts +318 -87
  499. package/src/formats/cx2/index.ts +2 -1
  500. package/src/formats/cys/constants.ts +62 -17
  501. package/src/formats/cys/exporter.ts +1368 -0
  502. package/src/formats/cys/importer.ts +659 -157
  503. package/src/formats/cys/index.ts +4 -2
  504. package/src/formats/cys/session.ts +142 -34
  505. package/src/formats/cys/tables.ts +47 -9
  506. package/src/formats/dot/exporter.ts +83 -37
  507. package/src/formats/dot/importer.ts +248 -54
  508. package/src/formats/dot/index.ts +2 -1
  509. package/src/formats/dot/names.ts +44 -11
  510. package/src/formats/dot/tokenizer.ts +25 -6
  511. package/src/formats/gexf/exporter.ts +57 -18
  512. package/src/formats/gexf/importer.ts +636 -105
  513. package/src/formats/gexf/index.ts +90 -21
  514. package/src/formats/gexf/schema.ts +67 -15
  515. package/src/formats/gml/exporter.ts +90 -36
  516. package/src/formats/gml/importer.ts +388 -67
  517. package/src/formats/gml/index.ts +85 -24
  518. package/src/formats/gml/syntax.ts +113 -28
  519. package/src/formats/graphml/constants.ts +178 -44
  520. package/src/formats/graphml/exporter.ts +52 -18
  521. package/src/formats/graphml/importer.ts +640 -60
  522. package/src/formats/graphml/index.ts +2 -1
  523. package/src/formats/graphml/tree.ts +6 -2
  524. package/src/formats/graphml/yfiles.ts +37 -15
  525. package/src/formats/json/dialect.ts +128 -29
  526. package/src/formats/json/exporter.ts +205 -68
  527. package/src/formats/json/importer.ts +1163 -253
  528. package/src/formats/json/index.ts +3 -1
  529. package/src/formats/json/obographs-export.ts +1197 -0
  530. package/src/formats/json/obographs.ts +353 -92
  531. package/src/formats/neo4j/exporter.ts +321 -102
  532. package/src/formats/neo4j/header.ts +44 -6
  533. package/src/formats/neo4j/importer.ts +512 -73
  534. package/src/formats/neo4j/index.ts +75 -23
  535. package/src/formats/obo/exporter.ts +2210 -0
  536. package/src/formats/obo/importer.ts +622 -109
  537. package/src/formats/obo/index.ts +6 -3
  538. package/src/formats/obo/syntax.ts +232 -37
  539. package/src/formats/pajek/exporter.ts +90 -42
  540. package/src/formats/pajek/importer.ts +407 -93
  541. package/src/formats/pajek/index.ts +2 -1
  542. package/src/formats/pajek/syntax.ts +216 -55
  543. package/src/formats/xgmml/columns.ts +10 -0
  544. package/src/formats/xgmml/constants.ts +183 -56
  545. package/src/formats/xgmml/document.ts +212 -34
  546. package/src/formats/xgmml/emit.ts +175 -34
  547. package/src/formats/xgmml/exporter.ts +36 -28
  548. package/src/formats/xgmml/importer.ts +64 -86
  549. package/src/formats/xgmml/index.ts +1 -0
  550. package/src/formats/xgmml/values.ts +12 -4
  551. package/src/index.ts +81 -11
  552. package/src/registry.ts +1121 -135
  553. package/src/sniff.ts +163 -32
  554. package/src/types.ts +385 -114
  555. package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
  556. package/dist/chunks/export-Bh60Dv-n.js +0 -582
  557. package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
  558. package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
  559. package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
  560. package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
  561. package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
  562. package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
  563. package/dist/chunks/importer-DET1tg6s.js.map +0 -1
  564. package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
  565. package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
  566. package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
  567. package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
  568. package/dist/chunks/importer-eUREhBGj.js.map +0 -1
  569. package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
  570. package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
  571. package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
  572. package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
  573. package/dist/chunks/ontology-BnrJ4I98.js +0 -113
  574. package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
  575. package/dist/chunks/records-dbRkxwaq.js.map +0 -1
  576. package/dist/chunks/report-B1z4WT9e.js +0 -2372
  577. package/dist/chunks/report-B1z4WT9e.js.map +0 -1
  578. package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
  579. package/dist/chunks/weights-Dzba96G3.js +0 -176
  580. package/dist/chunks/weights-Dzba96G3.js.map +0 -1
  581. package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
@@ -16,17 +16,26 @@
16
16
 
17
17
  import { GraphFormatError } from "@graphty/graph-format";
18
18
 
19
- import { type ReadOptions, textChunks } from "../../common/input.js";
19
+ import { MAX_TEXT_LENGTH, type ReadOptions, textChunks, tooLarge } from "../../common/input.js";
20
20
  import { type ImportReportBuilder } from "../../common/report.js";
21
21
  import { type ImportInput } from "../../types.js";
22
22
 
23
- /** Issue code: a quoted field is never closed; the import aborts (everything after it would be one cell). */
23
+ /**
24
+ * A quoted field is never closed; the import aborts (everything after it would be one cell).
25
+ * @category Issue and loss codes
26
+ */
24
27
  export const UNCLOSED_QUOTE_CODE = "E_CSV_UNCLOSED_QUOTE";
25
28
 
26
- /** Issue code: a closing quote is followed by text other than a delimiter or a line break; the import aborts. */
29
+ /**
30
+ * A closing quote is followed by text other than a delimiter or a line break; the import aborts.
31
+ * @category Issue and loss codes
32
+ */
27
33
  export const BAD_QUOTE_CODE = "E_CSV_QUOTE";
28
34
 
29
- /** The delimiters tried, in priority order, when none is given. */
35
+ /**
36
+ * The delimiters tried, in priority order, when none is given.
37
+ * @category Plugin helpers
38
+ */
30
39
  export const DELIMITER_CANDIDATES: readonly string[] = Object.freeze([",", "\t", ";", "|", " "]);
31
40
 
32
41
  /** Rows the delimiter sniff looks at. */
@@ -37,6 +46,11 @@ const PREVIEW_CHARS = 64 * 1024;
37
46
 
38
47
  const LF = 10;
39
48
  const CR = 13;
49
+ const TAB = 9;
50
+ const SPACE = 32;
51
+
52
+ /** An Excel delimiter directive: a first line `sep=;` names the delimiter and is not a record. */
53
+ const SEP_DIRECTIVE = /^sep=([^\r\n])(?:\r\n|\n|\r|$)/i;
40
54
 
41
55
  /** Tokeniser state: at the start of a field. */
42
56
  const START = 0;
@@ -53,7 +67,10 @@ const COMMENT = 5;
53
67
 
54
68
  type State = typeof START | typeof UNQUOTED | typeof QUOTED | typeof CLOSING | typeof AFTER_QUOTED | typeof COMMENT;
55
69
 
56
- /** The delimiter and quote of a reader. */
70
+ /**
71
+ * The delimiter and quote of a reader.
72
+ * @category Plugin helpers
73
+ */
57
74
  export interface RecordSyntax {
58
75
  /** The field delimiter, one character; null sniffs it from the first rows. */
59
76
  readonly delimiter: string | null;
@@ -72,6 +89,20 @@ export interface RecordSyntax {
72
89
  * other text (see sniffDelimiter). False by default: that text then aborts the read.
73
90
  */
74
91
  readonly skipQuoteErrors?: boolean | undefined;
92
+ /**
93
+ * Whether a space delimiter means the whitespace dialect of SNAP / KONECT edge lists: any run of
94
+ * spaces and tabs separates two fields, and whitespace at the start and end of a line is
95
+ * ignored. False by default (every space is a delimiter).
96
+ */
97
+ readonly collapseSpaces?: boolean | undefined;
98
+ /** Whether an Excel `sep=X` first line names the delimiter for the sniff. False by default. */
99
+ readonly sepDirective?: boolean | undefined;
100
+ /**
101
+ * Called with the sniff preview (leading comment lines removed) and the line it starts on,
102
+ * before any record is scanned, so a caller can refuse input that is not delimited text at all.
103
+ * Only called when the delimiter is sniffed.
104
+ */
105
+ readonly inspect?: ((preview: string, line: number) => void) | undefined;
75
106
  }
76
107
 
77
108
  /**
@@ -79,6 +110,7 @@ export interface RecordSyntax {
79
110
  * must differ.
80
111
  * @param syntax - the delimiter (or null for sniffing) and quote
81
112
  * @returns the syntax unchanged; E_UNSUPPORTED when invalid
113
+ * @category Plugin helpers
82
114
  */
83
115
  export function checkRecordSyntax(syntax: RecordSyntax): RecordSyntax {
84
116
  for (const [option, value] of [
@@ -110,13 +142,14 @@ export function checkRecordSyntax(syntax: RecordSyntax): RecordSyntax {
110
142
  * (classic Mac files); `\n` otherwise.
111
143
  * @param text - the preview text
112
144
  * @returns "\n" or "\r"
145
+ * @category Plugin helpers
113
146
  */
114
147
  export function sniffNewline(text: string): "\n" | "\r" {
115
148
  return !text.includes("\n") && text.includes("\r") ? "\r" : "\n";
116
149
  }
117
150
 
118
151
  /**
119
- * Split a text into records synchronously (the first `maxRows` of them), honouring quotes; for
152
+ * Split a text into records synchronously (the first `maxRows` of them), honoring quotes; for
120
153
  * the delimiter sniff and the registry's head sniff, where the input is a bounded preview.
121
154
  * @param text - the text
122
155
  * @param delimiter - the delimiter
@@ -124,86 +157,177 @@ export function sniffNewline(text: string): "\n" | "\r" {
124
157
  * @param maxRows - the most rows to return
125
158
  * @param strictQuotes - return null when a closing quote is followed by text other than the
126
159
  * delimiter or a line break (the import would abort under this delimiter)
160
+ * @param collapse - the whitespace dialect (RecordSyntax.collapseSpaces) for a space delimiter
127
161
  * @returns the rows as cell arrays (blank lines skipped), or null (see strictQuotes)
162
+ * @category Plugin helpers
128
163
  */
129
- function splitRecords(
164
+ export function splitRecords(
130
165
  text: string,
131
166
  delimiter: string,
132
167
  quote: string,
133
168
  maxRows: number,
134
169
  strictQuotes = false,
170
+ collapse = false,
135
171
  ): string[][] | null {
136
- const rows: string[][] = [];
137
- const delimiterCode = delimiter.charCodeAt(0);
138
- const quoteCode = quote.charCodeAt(0);
139
- let cells: string[] = [];
140
- let state: State = START;
141
- let segment = 0;
142
- let field = "";
143
- const n = text.length;
144
- for (let i = 0; i < n && rows.length < maxRows; i++) {
145
- const c = text.charCodeAt(i);
146
- if (state === QUOTED) {
147
- if (c === quoteCode) {
148
- field += text.slice(segment, i);
149
- state = CLOSING;
150
- }
151
- continue;
172
+ const delimiterCode = delimiter.codePointAt(0);
173
+ const splitter = new RecordSplitter(text, quote, delimiterCode, collapse && delimiterCode === SPACE, strictQuotes);
174
+ let i = 0;
175
+ while (i < text.length && splitter.rows.length < maxRows) {
176
+ i = splitter.step(i);
177
+ if (i < 0) {
178
+ return null;
152
179
  }
153
- if (state === CLOSING) {
154
- if (c === quoteCode) {
155
- field += quote;
156
- segment = i + 1;
157
- state = QUOTED;
158
- continue;
159
- }
160
- if (strictQuotes && c !== delimiterCode && c !== LF && c !== CR) {
161
- return null;
180
+ }
181
+ if (splitter.rows.length < maxRows) {
182
+ splitter.finish();
183
+ }
184
+ return splitter.rows;
185
+ }
186
+
187
+ /** The state machine of splitRecords(): one character at a time, quotes honored. */
188
+ class RecordSplitter {
189
+ /** The rows read so far. */
190
+ readonly rows: string[][] = [];
191
+
192
+ private cells: string[] = [];
193
+
194
+ private state: State = START;
195
+
196
+ /** Where the unread part of the current field starts. */
197
+ private segment = 0;
198
+
199
+ private field = "";
200
+
201
+ private readonly quoteCode: number | undefined;
202
+
203
+ /**
204
+ * Create a splitter.
205
+ * @param text - the text
206
+ * @param quote - the quote character
207
+ * @param delimiterCode - the delimiter's code
208
+ * @param whitespace - whether runs of spaces and tabs are one separator
209
+ * @param strictQuotes - whether text after a closing quote gives up
210
+ */
211
+ constructor(
212
+ private readonly text: string,
213
+ private readonly quote: string,
214
+ private readonly delimiterCode: number | undefined,
215
+ private readonly whitespace: boolean,
216
+ private readonly strictQuotes: boolean,
217
+ ) {
218
+ this.quoteCode = quote.codePointAt(0);
219
+ }
220
+
221
+ /**
222
+ * Read one character.
223
+ * @param i - its index
224
+ * @returns the index of the next character to read, or -1 to give up (strictQuotes)
225
+ */
226
+ step(i: number): number {
227
+ const c = this.text.codePointAt(i);
228
+ if (this.state === QUOTED) {
229
+ if (c === this.quoteCode) {
230
+ this.field += this.text.slice(this.segment, i);
231
+ this.state = CLOSING;
162
232
  }
163
- state = AFTER_QUOTED;
164
- segment = i;
233
+ return i + 1;
165
234
  }
166
- if (c === delimiterCode || c === LF || c === CR) {
167
- if (state === UNQUOTED || state === AFTER_QUOTED) {
168
- field += text.slice(segment, i);
169
- }
170
- if (c === delimiterCode) {
171
- cells.push(field);
172
- field = "";
173
- state = START;
174
- } else {
175
- if (state !== START || cells.length > 0) {
176
- cells.push(field);
177
- rows.push(cells);
178
- cells = [];
179
- field = "";
180
- }
181
- state = START;
182
- if (c === CR && text.charCodeAt(i + 1) === LF) {
183
- i++;
184
- }
235
+ const isDelimiter = c === this.delimiterCode || (this.whitespace && c === TAB);
236
+ if (this.state === CLOSING) {
237
+ const next = this.afterQuote(i, c, isDelimiter || c === LF || c === CR);
238
+ if (next !== null) {
239
+ return next;
185
240
  }
186
- segment = i + 1;
187
- continue;
188
241
  }
189
- if (state === START) {
190
- if (c === quoteCode) {
191
- state = QUOTED;
192
- segment = i + 1;
193
- } else {
194
- state = UNQUOTED;
195
- segment = i;
242
+ if (isDelimiter || c === LF || c === CR) {
243
+ return this.separator(i, isDelimiter);
244
+ }
245
+ if (this.state === START) {
246
+ const quoted = c === this.quoteCode;
247
+ this.state = quoted ? QUOTED : UNQUOTED;
248
+ this.segment = quoted ? i + 1 : i;
249
+ }
250
+ return i + 1;
251
+ }
252
+
253
+ /**
254
+ * Read the character after a quote that may close a quoted field: a second quote is a quote
255
+ * character in the field; anything else closes it.
256
+ * @param i - its index
257
+ * @param c - its code
258
+ * @param separates - whether it is a delimiter or a line break
259
+ * @returns the index of the next character to read, -1 to give up (strictQuotes), or null to
260
+ * read this character as the one after the field
261
+ */
262
+ private afterQuote(i: number, c: number | undefined, separates: boolean): number | null {
263
+ if (c === this.quoteCode) {
264
+ this.field += this.quote;
265
+ this.segment = i + 1;
266
+ this.state = QUOTED;
267
+ return i + 1;
268
+ }
269
+ if (this.strictQuotes && !separates) {
270
+ return -1;
271
+ }
272
+ this.state = AFTER_QUOTED;
273
+ this.segment = i;
274
+ return null;
275
+ }
276
+
277
+ /**
278
+ * Read a delimiter or a line break: end the field, and at a line break the row.
279
+ * @param i - its index
280
+ * @param isDelimiter - whether it is a delimiter
281
+ * @returns the index of the next character to read
282
+ */
283
+ private separator(i: number, isDelimiter: boolean): number {
284
+ if (this.state === UNQUOTED || this.state === AFTER_QUOTED) {
285
+ this.field += this.text.slice(this.segment, i);
286
+ }
287
+ let next = i + 1;
288
+ if (isDelimiter && this.whitespace && this.state === START) {
289
+ // a run of whitespace, or indentation: one separator
290
+ } else if (isDelimiter) {
291
+ this.cells.push(this.field);
292
+ this.field = "";
293
+ this.state = START;
294
+ } else {
295
+ this.endRow();
296
+ this.state = START;
297
+ if (this.text.codePointAt(i) === CR && this.text.codePointAt(i + 1) === LF) {
298
+ next = i + 2;
196
299
  }
197
300
  }
301
+ this.segment = next;
302
+ return next;
303
+ }
304
+
305
+ /** End the row at a line break; a blank line is no row. */
306
+ private endRow(): void {
307
+ if (this.state === START && this.cells.length === 0) {
308
+ return;
309
+ }
310
+ if (!(this.whitespace && this.state === START)) {
311
+ this.cells.push(this.field);
312
+ }
313
+ this.rows.push(this.cells);
314
+ this.cells = [];
315
+ this.field = "";
198
316
  }
199
- if (rows.length < maxRows && (state !== START || cells.length > 0)) {
200
- if (state === UNQUOTED || state === AFTER_QUOTED || state === QUOTED) {
201
- field += text.slice(segment, n);
317
+
318
+ /** End the text: the last row, when it has no line break after it. */
319
+ finish(): void {
320
+ if (this.state === START && this.cells.length === 0) {
321
+ return;
322
+ }
323
+ if (this.state === UNQUOTED || this.state === AFTER_QUOTED || this.state === QUOTED) {
324
+ this.field += this.text.slice(this.segment);
325
+ }
326
+ if (!(this.whitespace && this.state === START)) {
327
+ this.cells.push(this.field);
202
328
  }
203
- cells.push(field);
204
- rows.push(cells);
329
+ this.rows.push(this.cells);
205
330
  }
206
- return rows;
207
331
  }
208
332
 
209
333
  /**
@@ -212,30 +336,37 @@ function splitRecords(
212
336
  * @param text - the preview text
213
337
  * @param comments - the comment characters
214
338
  * @returns the text from the first non-comment line on
339
+ * @category Plugin helpers
215
340
  */
216
- function stripLeadingComments(text: string, comments: readonly string[]): string {
341
+ export function stripLeadingComments(text: string, comments: readonly string[]): string {
217
342
  if (comments.length === 0) {
218
343
  return text;
219
344
  }
220
345
  let at = 0;
221
- while (at < text.length && comments.includes(text[at])) {
346
+ while (at < text.length) {
347
+ if (text[at] === "\n" || text[at] === "\r") {
348
+ // a blank line among the leading comments (the scanner skips it too)
349
+ at++;
350
+ continue;
351
+ }
352
+ if (!comments.includes(text[at])) {
353
+ break;
354
+ }
222
355
  const lf = text.indexOf("\n", at);
223
356
  const cr = text.indexOf("\r", at);
224
357
  const end = Math.min(lf < 0 ? Infinity : lf, cr < 0 ? Infinity : cr);
225
358
  if (end === Infinity) {
226
359
  return "";
227
360
  }
228
- at = end + 1;
229
- if (text[at - 1] === "\r" && text[at] === "\n") {
230
- at++;
231
- }
361
+ at = end;
232
362
  }
233
363
  return at === 0 ? text : text.slice(at);
234
364
  }
235
365
 
236
366
  /**
237
367
  * Sniff the delimiter of a text: every candidate is tried over the first rows and the one whose
238
- * field count is most consistent across rows wins (ties broken by the higher field count); a
368
+ * field count is most consistent across rows wins (a tie goes to the earlier candidate, so a
369
+ * space inside comma-separated cells never beats the comma); a
239
370
  * candidate that yields fewer than two fields per row on average is never chosen. `null` when no
240
371
  * candidate qualifies (a single-column file).
241
372
  * @param text - the preview text
@@ -247,7 +378,9 @@ function stripLeadingComments(text: string, comments: readonly string[]): string
247
378
  * by other text. Only for inputs whose field counts say nothing (an adjacency table's rows vary in
248
379
  * width): elsewhere a stray character after a quote would make a worse candidate win silently
249
380
  * instead of aborting the read
381
+ * @param collapse - read a space candidate in the whitespace dialect (RecordSyntax.collapseSpaces)
250
382
  * @returns the delimiter, or null
383
+ * @category Plugin helpers
251
384
  */
252
385
  export function sniffDelimiter(
253
386
  text: string,
@@ -255,16 +388,16 @@ export function sniffDelimiter(
255
388
  candidates: readonly string[] = DELIMITER_CANDIDATES,
256
389
  quote = '"',
257
390
  skipQuoteErrors = false,
391
+ collapse = false,
258
392
  ): string | null {
259
393
  let best: string | null = null;
260
394
  let bestDelta = Infinity;
261
- let bestAverage = 0;
262
395
  const terminated = text.endsWith("\n") || text.endsWith("\r");
263
396
  for (const delimiter of candidates) {
264
397
  if (delimiter === quote || delimiter === newline) {
265
398
  continue;
266
399
  }
267
- const split = splitRecords(text, delimiter, quote, PREVIEW_ROWS, skipQuoteErrors);
400
+ const split = splitRecords(text, delimiter, quote, PREVIEW_ROWS, skipQuoteErrors, collapse);
268
401
  if (split === null) {
269
402
  // a quoted cell this delimiter does not close: it is not the file's delimiter
270
403
  continue;
@@ -287,10 +420,9 @@ export function sniffDelimiter(
287
420
  }
288
421
  }
289
422
  const average = total / rows.length;
290
- if (average > 1.99 && (delta < bestDelta || (delta === bestDelta && average > bestAverage))) {
423
+ if (average > 1.99 && delta < bestDelta) {
291
424
  best = delimiter;
292
425
  bestDelta = delta;
293
- bestAverage = average;
294
426
  }
295
427
  }
296
428
  return best;
@@ -312,6 +444,7 @@ function isBlankRow(row: readonly string[]): boolean {
312
444
  * are skipped. The arrays are reused between records. When the syntax gives no delimiter, the
313
445
  * first rows (PREVIEW_ROWS, or PREVIEW_CHARS characters) are buffered and the delimiter sniffed
314
446
  * from them before the first record is yielded.
447
+ * @category Plugin helpers
315
448
  */
316
449
  export class RecordReader implements AsyncIterable<number> {
317
450
  /** The cells of the record most recently yielded; only the first `count` entries are valid. */
@@ -323,9 +456,18 @@ export class RecordReader implements AsyncIterable<number> {
323
456
  /** The leading comment lines (without their line breaks), in order; empty without `syntax.comments`. */
324
457
  readonly leadingComments: string[] = [];
325
458
 
459
+ /** The 1-based line of each leading comment, parallel to `leadingComments`. */
460
+ readonly leadingCommentLines: number[] = [];
461
+
462
+ /** The line of the first quote inside an unquoted field (kept as text, RFC 4180 forbids it); 0 for none. */
463
+ strayQuoteLine = 0;
464
+
465
+ /** Whether the sniff took the delimiter from an Excel `sep=X` first line (yielded as the first record). */
466
+ sepDirective = false;
467
+
326
468
  private readonly input: ImportInput;
327
469
 
328
- private readonly report: ImportReportBuilder;
470
+ readonly report: ImportReportBuilder;
329
471
 
330
472
  private readonly readOptions: ReadOptions;
331
473
 
@@ -398,12 +540,15 @@ export class RecordReader implements AsyncIterable<number> {
398
540
  /**
399
541
  * Record a text-after-quote error (fatal).
400
542
  * @param line - the line the field started on
543
+ * @param field - the 1-based position of the field in its record
401
544
  * @returns never
402
545
  */
403
- badQuote(line: number): never {
404
- return this.report.fail(BAD_QUOTE_CODE, `line ${line}: text after the closing quote of a quoted field`, {
405
- line,
406
- });
546
+ badQuote(line: number, field: number): never {
547
+ return this.report.fail(
548
+ BAD_QUOTE_CODE,
549
+ `text after the closing quote of quoted field ${field} (a quote inside a quoted field is written as two quotes)`,
550
+ { line },
551
+ );
407
552
  }
408
553
 
409
554
  /**
@@ -435,6 +580,11 @@ export class RecordReader implements AsyncIterable<number> {
435
580
  let breaks = 0;
436
581
  let lastWasCr = false;
437
582
  let sniffed = false;
583
+ // the leading comment lines are left out of the sniff, so they do not count toward its rows
584
+ const commentCodes = (this.syntax.comments ?? []).map((c) => c.codePointAt(0));
585
+ let leading = commentCodes.length > 0;
586
+ let lineStart = true;
587
+ let inComment = false;
438
588
  for await (const chunk of source) {
439
589
  if (sniffed) {
440
590
  yield chunk;
@@ -444,12 +594,18 @@ export class RecordReader implements AsyncIterable<number> {
444
594
  length += chunk.length;
445
595
  for (let i = 0; i < chunk.length && breaks < PREVIEW_ROWS; i++) {
446
596
  const c = chunk.charCodeAt(i);
447
- if (c === LF) {
448
- if (!lastWasCr) {
597
+ if (c === LF || c === CR) {
598
+ if (!(c === LF && lastWasCr) && !inComment) {
449
599
  breaks++;
450
600
  }
451
- } else if (c === CR) {
452
- breaks++;
601
+ inComment = false;
602
+ lineStart = true;
603
+ } else {
604
+ if (lineStart && leading) {
605
+ inComment = commentCodes.includes(c);
606
+ leading = inComment;
607
+ }
608
+ lineStart = false;
453
609
  }
454
610
  lastWasCr = c === CR;
455
611
  }
@@ -476,13 +632,30 @@ export class RecordReader implements AsyncIterable<number> {
476
632
  * @param text - the preview
477
633
  */
478
634
  private sniff(text: string): void {
479
- const candidates = this.syntax.candidates ?? DELIMITER_CANDIDATES;
480
635
  // the sniff is a heuristic over the first rows: a single chunk holding a huge quoted cell
481
636
  // is capped so the candidate scans stay bounded
482
- const body = stripLeadingComments(text.slice(0, PREVIEW_CHARS), this.syntax.comments ?? []);
637
+ const preview = text.slice(0, PREVIEW_CHARS);
638
+ const body = stripLeadingComments(preview, this.syntax.comments ?? []);
639
+ if (this.syntax.inspect !== undefined) {
640
+ const skipped = preview.slice(0, preview.length - body.length).match(/\r\n|\r|\n/g)?.length ?? 0;
641
+ this.syntax.inspect(body, skipped + 1);
642
+ }
643
+ const directive = this.syntax.sepDirective === true ? SEP_DIRECTIVE.exec(text) : null;
644
+ if (directive !== null && directive[1] !== this.syntax.quote) {
645
+ this.delimiterText = directive[1];
646
+ this.sepDirective = true;
647
+ return;
648
+ }
649
+ const candidates = this.syntax.candidates ?? DELIMITER_CANDIDATES;
483
650
  this.delimiterText =
484
- sniffDelimiter(body, sniffNewline(body), candidates, this.syntax.quote, this.syntax.skipQuoteErrors) ??
485
- candidates[0];
651
+ sniffDelimiter(
652
+ body,
653
+ sniffNewline(body),
654
+ candidates,
655
+ this.syntax.quote,
656
+ this.syntax.skipQuoteErrors,
657
+ this.syntax.collapseSpaces,
658
+ ) ?? candidates[0];
486
659
  }
487
660
  }
488
661
 
@@ -515,6 +688,9 @@ class RecordScanner {
515
688
 
516
689
  private delimiterCode = -1;
517
690
 
691
+ /** The whitespace dialect: the delimiter is a space and RecordSyntax.collapseSpaces is set. */
692
+ private collapse = false;
693
+
518
694
  private state: State = START;
519
695
 
520
696
  private field = "";
@@ -525,7 +701,7 @@ class RecordScanner {
525
701
 
526
702
  private lastWasCr = false;
527
703
 
528
- /** Whether no record has started yet (comment lines are recognised until one does). */
704
+ /** Whether no record has started yet (comment lines are recognized until one does). */
529
705
  private leading: boolean;
530
706
 
531
707
  private comment = "";
@@ -552,6 +728,7 @@ class RecordScanner {
552
728
  setDelimiter(code: number): void {
553
729
  if (this.delimiterCode < 0) {
554
730
  this.delimiterCode = code;
731
+ this.collapse = code === SPACE && this.reader.syntax.collapseSpaces === true;
555
732
  }
556
733
  }
557
734
 
@@ -570,8 +747,9 @@ class RecordScanner {
570
747
  const c = chunk.charCodeAt(i);
571
748
  if (this.state === COMMENT) {
572
749
  if (c === LF || c === CR) {
573
- this.comment += chunk.slice(this.segment, i);
750
+ this.comment = this.grow(this.comment, chunk.slice(this.segment, i));
574
751
  this.reader.leadingComments.push(this.comment);
752
+ this.reader.leadingCommentLines.push(this.line);
575
753
  this.comment = "";
576
754
  this.state = START;
577
755
  this.endLine(c);
@@ -591,29 +769,30 @@ class RecordScanner {
591
769
  i = n;
592
770
  break;
593
771
  }
594
- this.field += chunk.slice(this.segment, q);
772
+ this.field = this.grow(this.field, chunk.slice(this.segment, q));
595
773
  this.state = CLOSING;
596
774
  this.lastWasCr = false;
597
775
  i = q + 1;
598
776
  continue;
599
777
  }
778
+ const isDelimiter = c === delimiterCode || (this.collapse && c === TAB);
600
779
  if (this.state === CLOSING) {
601
780
  if (c === quoteCode) {
602
- this.field += this.quoteText;
781
+ this.field = this.grow(this.field, this.quoteText);
603
782
  this.segment = i + 1;
604
783
  this.state = QUOTED;
605
784
  this.lastWasCr = false;
606
785
  i++;
607
786
  continue;
608
787
  }
609
- if (c !== delimiterCode && c !== LF && c !== CR) {
610
- this.reader.badQuote(this.startLine);
788
+ if (!isDelimiter && c !== LF && c !== CR) {
789
+ this.reader.badQuote(this.startLine, this.count + 1);
611
790
  }
612
791
  this.state = AFTER_QUOTED;
613
792
  this.segment = i;
614
793
  // fall through to the delimiter / line-end handling below without consuming c
615
794
  }
616
- if (c === delimiterCode || c === LF || c === CR) {
795
+ if (isDelimiter || c === LF || c === CR) {
617
796
  if (c === LF && this.lastWasCr) {
618
797
  // the second half of a CRLF already ended the record
619
798
  this.lastWasCr = false;
@@ -622,9 +801,16 @@ class RecordScanner {
622
801
  continue;
623
802
  }
624
803
  if (this.state === UNQUOTED) {
625
- this.field += chunk.slice(this.segment, i);
804
+ this.field = this.grow(this.field, chunk.slice(this.segment, i));
805
+ }
806
+ if (isDelimiter && this.collapse && this.state === START) {
807
+ // a run of whitespace, or indentation: one separator
808
+ this.lastWasCr = false;
809
+ this.segment = i + 1;
810
+ i++;
811
+ continue;
626
812
  }
627
- if (c === delimiterCode) {
813
+ if (isDelimiter) {
628
814
  this.push();
629
815
  this.state = START;
630
816
  this.lastWasCr = false;
@@ -632,9 +818,11 @@ class RecordScanner {
632
818
  i++;
633
819
  continue;
634
820
  }
635
- const blank = this.state === START && this.count === 0;
636
- if (!blank) {
821
+ // a line ending right after a whitespace run in the whitespace dialect ends no field
822
+ if (!(this.state === START && (this.count === 0 || this.collapse))) {
637
823
  this.push();
824
+ }
825
+ if (this.count > 0) {
638
826
  this.ready = true;
639
827
  this.recordLine = this.startLine;
640
828
  }
@@ -664,6 +852,8 @@ class RecordScanner {
664
852
  this.state = UNQUOTED;
665
853
  this.segment = i;
666
854
  }
855
+ } else if (c === quoteCode && this.state === UNQUOTED && this.reader.strayQuoteLine === 0) {
856
+ this.reader.strayQuoteLine = this.startLine;
667
857
  }
668
858
  i++;
669
859
  }
@@ -680,9 +870,9 @@ class RecordScanner {
680
870
  return;
681
871
  }
682
872
  if (this.state === COMMENT) {
683
- this.comment += chunk.slice(this.segment, n);
873
+ this.comment = this.grow(this.comment, chunk.slice(this.segment, n));
684
874
  } else if (this.state === QUOTED || this.state === UNQUOTED) {
685
- this.field += chunk.slice(this.segment, n);
875
+ this.field = this.grow(this.field, chunk.slice(this.segment, n));
686
876
  }
687
877
  this.segment = n;
688
878
  }
@@ -696,13 +886,16 @@ class RecordScanner {
696
886
  const { state } = this;
697
887
  if (state === COMMENT) {
698
888
  this.reader.leadingComments.push(this.comment);
889
+ this.reader.leadingCommentLines.push(this.line);
699
890
  return false;
700
891
  }
701
892
  if (state === QUOTED) {
702
893
  this.reader.unclosedQuote(this.startLine);
703
894
  }
704
895
  if (state !== START || this.count > 0) {
705
- this.push();
896
+ if (!(state === START && this.collapse)) {
897
+ this.push();
898
+ }
706
899
  this.recordLine = this.startLine;
707
900
  return true;
708
901
  }
@@ -716,23 +909,46 @@ class RecordScanner {
716
909
  * @param to - the index after the run
717
910
  */
718
911
  private countLineBreaks(chunk: string, from: number, to: number): void {
719
- let { line, lastWasCr } = this;
720
- for (let i = from; i < to; i++) {
721
- const c = chunk.charCodeAt(i);
722
- if (c === LF) {
723
- if (!lastWasCr) {
724
- line++;
725
- }
726
- lastWasCr = false;
727
- } else if (c === CR) {
912
+ // indexOf, not a charCodeAt loop: a quoted cell may be hundreds of MiB, and a per-character
913
+ // loop over it is the whole cost of reading it (and many times that under coverage)
914
+ const run = chunk.slice(from, to);
915
+ let { line } = this;
916
+ // the index of the last CR seen; -1 is a CR just before the run, -2 none
917
+ let lastCr = this.lastWasCr ? -1 : -2;
918
+ let lf = run.indexOf("\n");
919
+ let cr = run.indexOf("\r");
920
+ while (lf >= 0 || cr >= 0) {
921
+ if (lf < 0 || (cr >= 0 && cr < lf)) {
728
922
  line++;
729
- lastWasCr = true;
923
+ lastCr = cr;
924
+ cr = run.indexOf("\r", cr + 1);
730
925
  } else {
731
- lastWasCr = false;
926
+ if (lf !== lastCr + 1) {
927
+ line++;
928
+ }
929
+ lf = run.indexOf("\n", lf + 1);
732
930
  }
733
931
  }
734
932
  this.line = line;
735
- this.lastWasCr = lastWasCr;
933
+ this.lastWasCr = lastCr === run.length - 1;
934
+ }
935
+
936
+ /**
937
+ * Append to the field or comment in progress, failing with E_TOO_LARGE instead of a RangeError
938
+ * when the result would be longer than one JavaScript string can hold.
939
+ * @param text - the text so far
940
+ * @param piece - the text to append
941
+ * @returns the joined text
942
+ */
943
+ private grow(text: string, piece: string): string {
944
+ if (text.length + piece.length > MAX_TEXT_LENGTH) {
945
+ tooLarge(
946
+ this.reader.report,
947
+ `a cell is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`,
948
+ this.startLine,
949
+ );
950
+ }
951
+ return text + piece;
736
952
  }
737
953
 
738
954
  /** Store the field in progress as the next cell. */
@@ -758,7 +974,10 @@ class RecordScanner {
758
974
  }
759
975
  }
760
976
 
761
- /** What the CSV importer's reader needs besides the input. */
977
+ /**
978
+ * What the CSV importer's reader needs besides the input.
979
+ * @category Plugin helpers
980
+ */
762
981
  export interface CsvReaderOptions extends ReadOptions {
763
982
  /** The delimiter; null or undefined sniffs it from the preview. */
764
983
  readonly delimiter?: string | null | undefined;
@@ -766,6 +985,8 @@ export interface CsvReaderOptions extends ReadOptions {
766
985
  readonly comments?: readonly string[] | undefined;
767
986
  /** RecordSyntax.skipQuoteErrors; false by default. */
768
987
  readonly skipQuoteErrors?: boolean | undefined;
988
+ /** RecordSyntax.inspect; none by default. */
989
+ readonly inspect?: ((preview: string, line: number) => void) | undefined;
769
990
  }
770
991
 
771
992
  /**
@@ -774,7 +995,10 @@ export interface CsvReaderOptions extends ReadOptions {
774
995
  * on. The delimiter is known after the first row (`reader.delimiter`). Rows come out as fresh
775
996
  * arrays of cell texts exactly as written (no trimming, no typing); `reader.quoted` tells, for
776
997
  * the row just yielded, which cells were quoted (a quoted empty cell is the empty string, an
777
- * unquoted one is "not set"). Blank lines and whitespace-only single-cell lines are skipped.
998
+ * unquoted one is "not set"). Blank lines, whitespace-only lines and rows of empty unquoted cells
999
+ * are skipped. A space delimiter reads the whitespace dialect (RecordSyntax.collapseSpaces), and an
1000
+ * Excel `sep=X` first line names the delimiter when none is given.
1001
+ * @category Plugin helpers
778
1002
  */
779
1003
  export class CsvRecordReader implements AsyncIterable<string[]> {
780
1004
  private readonly inner: RecordReader;
@@ -794,8 +1018,17 @@ export class CsvRecordReader implements AsyncIterable<string[]> {
794
1018
  quote: '"',
795
1019
  comments: options.comments,
796
1020
  skipQuoteErrors: options.skipQuoteErrors,
1021
+ collapseSpaces: true,
1022
+ sepDirective: true,
1023
+ inspect: options.inspect,
1024
+ },
1025
+ {
1026
+ signal: options.signal,
1027
+ onProgress: options.onProgress,
1028
+ encoding: options.encoding,
1029
+ allowEmpty: options.allowEmpty,
1030
+ nulIsBinary: true,
797
1031
  },
798
- { signal: options.signal, onProgress: options.onProgress, encoding: options.encoding },
799
1032
  );
800
1033
  }
801
1034
 
@@ -807,6 +1040,14 @@ export class CsvRecordReader implements AsyncIterable<string[]> {
807
1040
  return this.inner.leadingComments;
808
1041
  }
809
1042
 
1043
+ /**
1044
+ * The 1-based line of each leading comment, parallel to `leadingComments`.
1045
+ * @returns the lines
1046
+ */
1047
+ get leadingCommentLines(): readonly number[] {
1048
+ return this.inner.leadingCommentLines;
1049
+ }
1050
+
810
1051
  /**
811
1052
  * The 1-based line the most recently yielded row starts on.
812
1053
  * @returns the line; 0 before the first row
@@ -831,6 +1072,22 @@ export class CsvRecordReader implements AsyncIterable<string[]> {
831
1072
  return this.inner.quoted;
832
1073
  }
833
1074
 
1075
+ /**
1076
+ * The line of the first quote inside an unquoted field.
1077
+ * @returns the line, or 0 when there was none
1078
+ */
1079
+ get strayQuoteLine(): number {
1080
+ return this.inner.strayQuoteLine;
1081
+ }
1082
+
1083
+ /**
1084
+ * Whether the first record is an Excel `sep=X` line the delimiter was taken from.
1085
+ * @returns true when it is
1086
+ */
1087
+ get sepDirective(): boolean {
1088
+ return this.inner.sepDirective;
1089
+ }
1090
+
834
1091
  /**
835
1092
  * Iterate the rows.
836
1093
  * @yields one row at a time as its cell texts
@@ -839,10 +1096,27 @@ export class CsvRecordReader implements AsyncIterable<string[]> {
839
1096
  async *[Symbol.asyncIterator](): AsyncGenerator<string[], void, undefined> {
840
1097
  const { inner } = this;
841
1098
  for await (const count of inner) {
842
- if (count === 1 && !inner.quoted[0] && inner.cells[0].trim().length === 0) {
1099
+ if (isBlankRecord(inner.cells, inner.quoted, count)) {
843
1100
  continue;
844
1101
  }
845
1102
  yield inner.cells.slice(0, count);
846
1103
  }
847
1104
  }
848
1105
  }
1106
+
1107
+ /**
1108
+ * Whether a record holds nothing: every cell unquoted and blank (a blank line, a whitespace-only
1109
+ * line, or an Excel empty row `,,,`). A quoted empty cell is a value.
1110
+ * @param cells - the cells
1111
+ * @param quoted - whether each was quoted
1112
+ * @param count - the cell count
1113
+ * @returns true when the record carries no value
1114
+ */
1115
+ function isBlankRecord(cells: readonly string[], quoted: readonly boolean[], count: number): boolean {
1116
+ for (let k = 0; k < count; k++) {
1117
+ if (quoted[k] || cells[k].trim().length > 0) {
1118
+ return false;
1119
+ }
1120
+ }
1121
+ return true;
1122
+ }