@graphty/graph-io 0.3.24 → 0.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/README.md +49 -414
  2. package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
  3. package/dist/chunks/choose-BbJI55dx.js +56 -0
  4. package/dist/chunks/choose-BbJI55dx.js.map +1 -0
  5. package/dist/chunks/constants-BCsFsMj4.js +231 -0
  6. package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
  8. package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
  9. package/dist/chunks/export-BTI4yQgy.js +4333 -0
  10. package/dist/chunks/export-BTI4yQgy.js.map +1 -0
  11. package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
  12. package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
  13. package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
  14. package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
  15. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
  16. package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
  17. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
  18. package/dist/chunks/exporter-tne475KK.js.map +1 -0
  19. package/dist/chunks/importer-90YWhJoF.js +2811 -0
  20. package/dist/chunks/importer-90YWhJoF.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
  22. package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
  23. package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
  24. package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
  25. package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
  26. package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
  28. package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
  29. package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
  30. package/dist/chunks/importer-DfVu6j59.js.map +1 -0
  31. package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
  32. package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
  33. package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
  34. package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
  35. package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
  36. package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
  38. package/dist/chunks/records-CQXT__mY.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
  40. package/dist/chunks/text-DmcUs5_Q.js.map +1 -0
  41. package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
  42. package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
  43. package/dist/csv.js +951 -261
  44. package/dist/csv.js.map +1 -1
  45. package/dist/cx.js +6 -3
  46. package/dist/cx2.js +2 -2
  47. package/dist/cys.js +6 -3
  48. package/dist/dot.js +1 -1
  49. package/dist/gexf.js +554 -122
  50. package/dist/gexf.js.map +1 -1
  51. package/dist/gml.js +342 -85
  52. package/dist/gml.js.map +1 -1
  53. package/dist/graph-io.js +1105 -213
  54. package/dist/graph-io.js.map +1 -1
  55. package/dist/graphml.js +3 -2
  56. package/dist/graphml.js.map +1 -1
  57. package/dist/json.js +3 -2
  58. package/dist/neo4j.js +564 -157
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +6 -3
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/children.d.ts +18 -6
  63. package/dist/src/children.d.ts.map +1 -1
  64. package/dist/src/children.js +6 -3
  65. package/dist/src/children.js.map +1 -1
  66. package/dist/src/common/attributes.d.ts +29 -11
  67. package/dist/src/common/attributes.d.ts.map +1 -1
  68. package/dist/src/common/attributes.js +25 -7
  69. package/dist/src/common/attributes.js.map +1 -1
  70. package/dist/src/common/cell-budget.d.ts +9 -2
  71. package/dist/src/common/cell-budget.d.ts.map +1 -1
  72. package/dist/src/common/cell-budget.js +11 -3
  73. package/dist/src/common/cell-budget.js.map +1 -1
  74. package/dist/src/common/choose.d.ts +19 -0
  75. package/dist/src/common/choose.d.ts.map +1 -0
  76. package/dist/src/common/choose.js +72 -0
  77. package/dist/src/common/choose.js.map +1 -0
  78. package/dist/src/common/codes.d.ts +456 -66
  79. package/dist/src/common/codes.d.ts.map +1 -1
  80. package/dist/src/common/codes.js +458 -66
  81. package/dist/src/common/codes.js.map +1 -1
  82. package/dist/src/common/compare.d.ts +81 -0
  83. package/dist/src/common/compare.d.ts.map +1 -0
  84. package/dist/src/common/compare.js +394 -0
  85. package/dist/src/common/compare.js.map +1 -0
  86. package/dist/src/common/cx-export.d.ts +123 -0
  87. package/dist/src/common/cx-export.d.ts.map +1 -0
  88. package/dist/src/common/cx-export.js +334 -0
  89. package/dist/src/common/cx-export.js.map +1 -0
  90. package/dist/src/common/declared-types.d.ts +22 -2
  91. package/dist/src/common/declared-types.d.ts.map +1 -1
  92. package/dist/src/common/declared-types.js +19 -1
  93. package/dist/src/common/declared-types.js.map +1 -1
  94. package/dist/src/common/direction.d.ts +74 -50
  95. package/dist/src/common/direction.d.ts.map +1 -1
  96. package/dist/src/common/direction.js +47 -27
  97. package/dist/src/common/direction.js.map +1 -1
  98. package/dist/src/common/escape.d.ts +21 -9
  99. package/dist/src/common/escape.d.ts.map +1 -1
  100. package/dist/src/common/escape.js +33 -17
  101. package/dist/src/common/escape.js.map +1 -1
  102. package/dist/src/common/export.d.ts +144 -32
  103. package/dist/src/common/export.d.ts.map +1 -1
  104. package/dist/src/common/export.js +216 -42
  105. package/dist/src/common/export.js.map +1 -1
  106. package/dist/src/common/format.d.ts +23 -1
  107. package/dist/src/common/format.d.ts.map +1 -1
  108. package/dist/src/common/format.js +26 -1
  109. package/dist/src/common/format.js.map +1 -1
  110. package/dist/src/common/ids.d.ts +9 -2
  111. package/dist/src/common/ids.d.ts.map +1 -1
  112. package/dist/src/common/ids.js +6 -2
  113. package/dist/src/common/ids.js.map +1 -1
  114. package/dist/src/common/input.d.ts +122 -28
  115. package/dist/src/common/input.d.ts.map +1 -1
  116. package/dist/src/common/input.js +890 -124
  117. package/dist/src/common/input.js.map +1 -1
  118. package/dist/src/common/json-elements.d.ts +144 -20
  119. package/dist/src/common/json-elements.d.ts.map +1 -1
  120. package/dist/src/common/json-elements.js +868 -188
  121. package/dist/src/common/json-elements.js.map +1 -1
  122. package/dist/src/common/line-format.d.ts +90 -0
  123. package/dist/src/common/line-format.d.ts.map +1 -0
  124. package/dist/src/common/line-format.js +185 -0
  125. package/dist/src/common/line-format.js.map +1 -0
  126. package/dist/src/common/lists.d.ts +6 -1
  127. package/dist/src/common/lists.d.ts.map +1 -1
  128. package/dist/src/common/lists.js +2 -0
  129. package/dist/src/common/lists.js.map +1 -1
  130. package/dist/src/common/ontology-export.d.ts +167 -0
  131. package/dist/src/common/ontology-export.d.ts.map +1 -0
  132. package/dist/src/common/ontology-export.js +359 -0
  133. package/dist/src/common/ontology-export.js.map +1 -0
  134. package/dist/src/common/ontology.d.ts +25 -3
  135. package/dist/src/common/ontology.d.ts.map +1 -1
  136. package/dist/src/common/ontology.js +30 -9
  137. package/dist/src/common/ontology.js.map +1 -1
  138. package/dist/src/common/options.d.ts +62 -12
  139. package/dist/src/common/options.d.ts.map +1 -1
  140. package/dist/src/common/options.js +89 -11
  141. package/dist/src/common/options.js.map +1 -1
  142. package/dist/src/common/plural.d.ts +31 -0
  143. package/dist/src/common/plural.d.ts.map +1 -0
  144. package/dist/src/common/plural.js +46 -0
  145. package/dist/src/common/plural.js.map +1 -0
  146. package/dist/src/common/report.d.ts +72 -14
  147. package/dist/src/common/report.d.ts.map +1 -1
  148. package/dist/src/common/report.js +146 -22
  149. package/dist/src/common/report.js.map +1 -1
  150. package/dist/src/common/temporal.d.ts +22 -5
  151. package/dist/src/common/temporal.d.ts.map +1 -1
  152. package/dist/src/common/temporal.js +13 -2
  153. package/dist/src/common/temporal.js.map +1 -1
  154. package/dist/src/common/text.d.ts +39 -23
  155. package/dist/src/common/text.d.ts.map +1 -1
  156. package/dist/src/common/text.js +51 -21
  157. package/dist/src/common/text.js.map +1 -1
  158. package/dist/src/common/weights.d.ts +37 -4
  159. package/dist/src/common/weights.d.ts.map +1 -1
  160. package/dist/src/common/weights.js +61 -2
  161. package/dist/src/common/weights.js.map +1 -1
  162. package/dist/src/common/writer.d.ts +19 -3
  163. package/dist/src/common/writer.d.ts.map +1 -1
  164. package/dist/src/common/writer.js +30 -2
  165. package/dist/src/common/writer.js.map +1 -1
  166. package/dist/src/common/xml.d.ts +125 -9
  167. package/dist/src/common/xml.d.ts.map +1 -1
  168. package/dist/src/common/xml.js +400 -83
  169. package/dist/src/common/xml.js.map +1 -1
  170. package/dist/src/common/zip.d.ts +44 -4
  171. package/dist/src/common/zip.d.ts.map +1 -1
  172. package/dist/src/common/zip.js +134 -5
  173. package/dist/src/common/zip.js.map +1 -1
  174. package/dist/src/formats/csv/exporter.d.ts +80 -24
  175. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  176. package/dist/src/formats/csv/exporter.js +145 -32
  177. package/dist/src/formats/csv/exporter.js.map +1 -1
  178. package/dist/src/formats/csv/header.d.ts +38 -9
  179. package/dist/src/formats/csv/header.d.ts.map +1 -1
  180. package/dist/src/formats/csv/header.js +46 -10
  181. package/dist/src/formats/csv/header.js.map +1 -1
  182. package/dist/src/formats/csv/importer.d.ts +187 -37
  183. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  184. package/dist/src/formats/csv/importer.js +835 -204
  185. package/dist/src/formats/csv/importer.js.map +1 -1
  186. package/dist/src/formats/csv/index.d.ts +100 -19
  187. package/dist/src/formats/csv/index.d.ts.map +1 -1
  188. package/dist/src/formats/csv/index.js +94 -21
  189. package/dist/src/formats/csv/index.js.map +1 -1
  190. package/dist/src/formats/csv/records.d.ts +95 -10
  191. package/dist/src/formats/csv/records.d.ts.map +1 -1
  192. package/dist/src/formats/csv/records.js +333 -122
  193. package/dist/src/formats/csv/records.js.map +1 -1
  194. package/dist/src/formats/csv/values.d.ts +1 -0
  195. package/dist/src/formats/csv/values.d.ts.map +1 -1
  196. package/dist/src/formats/csv/values.js +1 -0
  197. package/dist/src/formats/csv/values.js.map +1 -1
  198. package/dist/src/formats/cx/exporter.d.ts +102 -0
  199. package/dist/src/formats/cx/exporter.d.ts.map +1 -0
  200. package/dist/src/formats/cx/exporter.js +1217 -0
  201. package/dist/src/formats/cx/exporter.js.map +1 -0
  202. package/dist/src/formats/cx/importer.d.ts +79 -32
  203. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  204. package/dist/src/formats/cx/importer.js +680 -285
  205. package/dist/src/formats/cx/importer.js.map +1 -1
  206. package/dist/src/formats/cx/index.d.ts +4 -3
  207. package/dist/src/formats/cx/index.d.ts.map +1 -1
  208. package/dist/src/formats/cx/index.js +4 -3
  209. package/dist/src/formats/cx/index.js.map +1 -1
  210. package/dist/src/formats/cx2/exporter.d.ts +29 -11
  211. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  212. package/dist/src/formats/cx2/exporter.js +39 -268
  213. package/dist/src/formats/cx2/exporter.js.map +1 -1
  214. package/dist/src/formats/cx2/importer.d.ts +78 -28
  215. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  216. package/dist/src/formats/cx2/importer.js +226 -79
  217. package/dist/src/formats/cx2/importer.js.map +1 -1
  218. package/dist/src/formats/cx2/index.d.ts +2 -1
  219. package/dist/src/formats/cx2/index.d.ts.map +1 -1
  220. package/dist/src/formats/cx2/index.js +2 -1
  221. package/dist/src/formats/cx2/index.js.map +1 -1
  222. package/dist/src/formats/cys/constants.d.ts +66 -17
  223. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  224. package/dist/src/formats/cys/constants.js +59 -18
  225. package/dist/src/formats/cys/constants.js.map +1 -1
  226. package/dist/src/formats/cys/exporter.d.ts +118 -0
  227. package/dist/src/formats/cys/exporter.d.ts.map +1 -0
  228. package/dist/src/formats/cys/exporter.js +1072 -0
  229. package/dist/src/formats/cys/exporter.js.map +1 -0
  230. package/dist/src/formats/cys/importer.d.ts +22 -8
  231. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  232. package/dist/src/formats/cys/importer.js +510 -149
  233. package/dist/src/formats/cys/importer.js.map +1 -1
  234. package/dist/src/formats/cys/index.d.ts +4 -2
  235. package/dist/src/formats/cys/index.d.ts.map +1 -1
  236. package/dist/src/formats/cys/index.js +4 -2
  237. package/dist/src/formats/cys/index.js.map +1 -1
  238. package/dist/src/formats/cys/session.d.ts +46 -7
  239. package/dist/src/formats/cys/session.d.ts.map +1 -1
  240. package/dist/src/formats/cys/session.js +98 -24
  241. package/dist/src/formats/cys/session.js.map +1 -1
  242. package/dist/src/formats/cys/tables.d.ts +9 -2
  243. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  244. package/dist/src/formats/cys/tables.js +29 -7
  245. package/dist/src/formats/cys/tables.js.map +1 -1
  246. package/dist/src/formats/dot/exporter.d.ts +42 -14
  247. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  248. package/dist/src/formats/dot/exporter.js +56 -29
  249. package/dist/src/formats/dot/exporter.js.map +1 -1
  250. package/dist/src/formats/dot/importer.d.ts +91 -29
  251. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  252. package/dist/src/formats/dot/importer.js +190 -39
  253. package/dist/src/formats/dot/importer.js.map +1 -1
  254. package/dist/src/formats/dot/index.d.ts +2 -1
  255. package/dist/src/formats/dot/index.d.ts.map +1 -1
  256. package/dist/src/formats/dot/index.js +2 -1
  257. package/dist/src/formats/dot/index.js.map +1 -1
  258. package/dist/src/formats/dot/names.d.ts +44 -11
  259. package/dist/src/formats/dot/names.d.ts.map +1 -1
  260. package/dist/src/formats/dot/names.js +44 -11
  261. package/dist/src/formats/dot/names.js.map +1 -1
  262. package/dist/src/formats/dot/tokenizer.d.ts +9 -2
  263. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  264. package/dist/src/formats/dot/tokenizer.js +20 -5
  265. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  266. package/dist/src/formats/gexf/exporter.d.ts +46 -15
  267. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  268. package/dist/src/formats/gexf/exporter.js +45 -15
  269. package/dist/src/formats/gexf/exporter.js.map +1 -1
  270. package/dist/src/formats/gexf/importer.d.ts +87 -23
  271. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  272. package/dist/src/formats/gexf/importer.js +471 -90
  273. package/dist/src/formats/gexf/importer.js.map +1 -1
  274. package/dist/src/formats/gexf/index.d.ts +84 -21
  275. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  276. package/dist/src/formats/gexf/index.js +80 -23
  277. package/dist/src/formats/gexf/index.js.map +1 -1
  278. package/dist/src/formats/gexf/schema.d.ts +67 -15
  279. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  280. package/dist/src/formats/gexf/schema.js +59 -13
  281. package/dist/src/formats/gexf/schema.js.map +1 -1
  282. package/dist/src/formats/gml/exporter.d.ts +69 -21
  283. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/gml/exporter.js +74 -29
  285. package/dist/src/formats/gml/exporter.js.map +1 -1
  286. package/dist/src/formats/gml/importer.d.ts +116 -25
  287. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  288. package/dist/src/formats/gml/importer.js +290 -61
  289. package/dist/src/formats/gml/importer.js.map +1 -1
  290. package/dist/src/formats/gml/index.d.ts +87 -24
  291. package/dist/src/formats/gml/index.d.ts.map +1 -1
  292. package/dist/src/formats/gml/index.js +79 -26
  293. package/dist/src/formats/gml/index.js.map +1 -1
  294. package/dist/src/formats/gml/syntax.d.ts +67 -18
  295. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  296. package/dist/src/formats/gml/syntax.js +108 -26
  297. package/dist/src/formats/gml/syntax.js.map +1 -1
  298. package/dist/src/formats/graphml/constants.d.ts +174 -44
  299. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  300. package/dist/src/formats/graphml/constants.js +165 -44
  301. package/dist/src/formats/graphml/constants.js.map +1 -1
  302. package/dist/src/formats/graphml/exporter.d.ts +17 -9
  303. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  304. package/dist/src/formats/graphml/exporter.js +33 -12
  305. package/dist/src/formats/graphml/exporter.js.map +1 -1
  306. package/dist/src/formats/graphml/importer.d.ts +19 -7
  307. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  308. package/dist/src/formats/graphml/importer.js +466 -39
  309. package/dist/src/formats/graphml/importer.js.map +1 -1
  310. package/dist/src/formats/graphml/index.d.ts +2 -1
  311. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  312. package/dist/src/formats/graphml/index.js +2 -1
  313. package/dist/src/formats/graphml/index.js.map +1 -1
  314. package/dist/src/formats/graphml/tree.d.ts +6 -2
  315. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  316. package/dist/src/formats/graphml/tree.js +6 -2
  317. package/dist/src/formats/graphml/tree.js.map +1 -1
  318. package/dist/src/formats/graphml/yfiles.d.ts +8 -3
  319. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  320. package/dist/src/formats/graphml/yfiles.js +36 -15
  321. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  322. package/dist/src/formats/json/dialect.d.ts +88 -24
  323. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  324. package/dist/src/formats/json/dialect.js +109 -19
  325. package/dist/src/formats/json/dialect.js.map +1 -1
  326. package/dist/src/formats/json/exporter.d.ts +134 -22
  327. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  328. package/dist/src/formats/json/exporter.js +135 -54
  329. package/dist/src/formats/json/exporter.js.map +1 -1
  330. package/dist/src/formats/json/importer.d.ts +173 -52
  331. package/dist/src/formats/json/importer.d.ts.map +1 -1
  332. package/dist/src/formats/json/importer.js +899 -220
  333. package/dist/src/formats/json/importer.js.map +1 -1
  334. package/dist/src/formats/json/index.d.ts +3 -2
  335. package/dist/src/formats/json/index.d.ts.map +1 -1
  336. package/dist/src/formats/json/index.js +3 -2
  337. package/dist/src/formats/json/index.js.map +1 -1
  338. package/dist/src/formats/json/obographs-export.d.ts +68 -0
  339. package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
  340. package/dist/src/formats/json/obographs-export.js +941 -0
  341. package/dist/src/formats/json/obographs-export.js.map +1 -0
  342. package/dist/src/formats/json/obographs.d.ts +26 -0
  343. package/dist/src/formats/json/obographs.d.ts.map +1 -1
  344. package/dist/src/formats/json/obographs.js +297 -80
  345. package/dist/src/formats/json/obographs.js.map +1 -1
  346. package/dist/src/formats/neo4j/exporter.d.ts +109 -21
  347. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  348. package/dist/src/formats/neo4j/exporter.js +238 -85
  349. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  350. package/dist/src/formats/neo4j/header.d.ts +17 -4
  351. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  352. package/dist/src/formats/neo4j/header.js +36 -4
  353. package/dist/src/formats/neo4j/header.js.map +1 -1
  354. package/dist/src/formats/neo4j/importer.d.ts +107 -27
  355. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  356. package/dist/src/formats/neo4j/importer.js +415 -63
  357. package/dist/src/formats/neo4j/importer.js.map +1 -1
  358. package/dist/src/formats/neo4j/index.d.ts +73 -23
  359. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  360. package/dist/src/formats/neo4j/index.js +65 -25
  361. package/dist/src/formats/neo4j/index.js.map +1 -1
  362. package/dist/src/formats/obo/exporter.d.ts +150 -0
  363. package/dist/src/formats/obo/exporter.d.ts.map +1 -0
  364. package/dist/src/formats/obo/exporter.js +1736 -0
  365. package/dist/src/formats/obo/exporter.js.map +1 -0
  366. package/dist/src/formats/obo/importer.d.ts +95 -19
  367. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  368. package/dist/src/formats/obo/importer.js +475 -104
  369. package/dist/src/formats/obo/importer.js.map +1 -1
  370. package/dist/src/formats/obo/index.d.ts +6 -3
  371. package/dist/src/formats/obo/index.d.ts.map +1 -1
  372. package/dist/src/formats/obo/index.js +6 -3
  373. package/dist/src/formats/obo/index.js.map +1 -1
  374. package/dist/src/formats/obo/syntax.d.ts +108 -4
  375. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  376. package/dist/src/formats/obo/syntax.js +211 -34
  377. package/dist/src/formats/obo/syntax.js.map +1 -1
  378. package/dist/src/formats/pajek/exporter.d.ts +54 -16
  379. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  380. package/dist/src/formats/pajek/exporter.js +66 -34
  381. package/dist/src/formats/pajek/exporter.js.map +1 -1
  382. package/dist/src/formats/pajek/importer.d.ts +84 -22
  383. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  384. package/dist/src/formats/pajek/importer.js +303 -83
  385. package/dist/src/formats/pajek/importer.js.map +1 -1
  386. package/dist/src/formats/pajek/index.d.ts +2 -1
  387. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  388. package/dist/src/formats/pajek/index.js +2 -1
  389. package/dist/src/formats/pajek/index.js.map +1 -1
  390. package/dist/src/formats/pajek/syntax.d.ts +77 -14
  391. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  392. package/dist/src/formats/pajek/syntax.js +183 -51
  393. package/dist/src/formats/pajek/syntax.js.map +1 -1
  394. package/dist/src/formats/xgmml/columns.d.ts +1 -0
  395. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  396. package/dist/src/formats/xgmml/columns.js +4 -0
  397. package/dist/src/formats/xgmml/columns.js.map +1 -1
  398. package/dist/src/formats/xgmml/constants.d.ts +186 -56
  399. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  400. package/dist/src/formats/xgmml/constants.js +181 -57
  401. package/dist/src/formats/xgmml/constants.js.map +1 -1
  402. package/dist/src/formats/xgmml/document.d.ts +65 -11
  403. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  404. package/dist/src/formats/xgmml/document.js +157 -28
  405. package/dist/src/formats/xgmml/document.js.map +1 -1
  406. package/dist/src/formats/xgmml/emit.d.ts +61 -3
  407. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  408. package/dist/src/formats/xgmml/emit.js +129 -26
  409. package/dist/src/formats/xgmml/emit.js.map +1 -1
  410. package/dist/src/formats/xgmml/exporter.d.ts +13 -6
  411. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  412. package/dist/src/formats/xgmml/exporter.js +20 -21
  413. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  414. package/dist/src/formats/xgmml/importer.d.ts +33 -12
  415. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  416. package/dist/src/formats/xgmml/importer.js +35 -72
  417. package/dist/src/formats/xgmml/importer.js.map +1 -1
  418. package/dist/src/formats/xgmml/index.d.ts +1 -0
  419. package/dist/src/formats/xgmml/index.d.ts.map +1 -1
  420. package/dist/src/formats/xgmml/index.js +1 -0
  421. package/dist/src/formats/xgmml/index.js.map +1 -1
  422. package/dist/src/formats/xgmml/values.d.ts +8 -1
  423. package/dist/src/formats/xgmml/values.d.ts.map +1 -1
  424. package/dist/src/formats/xgmml/values.js +7 -2
  425. package/dist/src/formats/xgmml/values.js.map +1 -1
  426. package/dist/src/index.d.ts +23 -18
  427. package/dist/src/index.d.ts.map +1 -1
  428. package/dist/src/index.js +28 -16
  429. package/dist/src/index.js.map +1 -1
  430. package/dist/src/registry.d.ts +478 -92
  431. package/dist/src/registry.d.ts.map +1 -1
  432. package/dist/src/registry.js +807 -88
  433. package/dist/src/registry.js.map +1 -1
  434. package/dist/src/sniff.d.ts +61 -19
  435. package/dist/src/sniff.d.ts.map +1 -1
  436. package/dist/src/sniff.js +121 -29
  437. package/dist/src/sniff.js.map +1 -1
  438. package/dist/src/types.d.ts +362 -112
  439. package/dist/src/types.d.ts.map +1 -1
  440. package/dist/src/types.js +28 -7
  441. package/dist/src/types.js.map +1 -1
  442. package/dist/xgmml.js +2 -2
  443. package/docs/samples/README.md +25 -0
  444. package/docs/samples/got-edges.csv +353 -0
  445. package/docs/samples/got-network.graphml +1384 -0
  446. package/docs/samples/got-nodes.csv +108 -0
  447. package/docs/samples/got.cx +930 -0
  448. package/docs/samples/got.cx2 +465 -0
  449. package/docs/samples/got.gexf +471 -0
  450. package/docs/samples/got.gml +2650 -0
  451. package/docs/samples/got.json +2546 -0
  452. package/docs/samples/got.net +461 -0
  453. package/docs/samples/movies-nodes.csv +9 -0
  454. package/docs/samples/movies-rels.csv +8 -0
  455. package/docs/samples/networks.cys +0 -0
  456. package/docs/samples/proteins.xgmml +9 -0
  457. package/docs/samples/teams.gv +24 -0
  458. package/docs/samples/vehicles.obo +25 -0
  459. package/package.json +9 -4
  460. package/src/children.ts +18 -6
  461. package/src/common/attributes.ts +29 -11
  462. package/src/common/cell-budget.ts +11 -3
  463. package/src/common/choose.ts +89 -0
  464. package/src/common/codes.ts +490 -66
  465. package/src/common/compare.ts +472 -0
  466. package/src/common/cx-export.ts +432 -0
  467. package/src/common/declared-types.ts +38 -4
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +33 -17
  470. package/src/common/export.ts +284 -47
  471. package/src/common/format.ts +30 -1
  472. package/src/common/ids.ts +14 -3
  473. package/src/common/input.ts +1057 -134
  474. package/src/common/json-elements.ts +1034 -198
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +6 -1
  477. package/src/common/ontology-export.ts +461 -0
  478. package/src/common/ontology.ts +31 -9
  479. package/src/common/options.ts +110 -15
  480. package/src/common/plural.ts +49 -0
  481. package/src/common/report.ts +168 -25
  482. package/src/common/temporal.ts +22 -5
  483. package/src/common/text.ts +62 -23
  484. package/src/common/weights.ts +74 -4
  485. package/src/common/writer.ts +37 -3
  486. package/src/common/xml.ts +477 -88
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +201 -46
  489. package/src/formats/csv/header.ts +50 -11
  490. package/src/formats/csv/importer.ts +1133 -245
  491. package/src/formats/csv/index.ts +106 -19
  492. package/src/formats/csv/records.ts +396 -122
  493. package/src/formats/csv/values.ts +1 -0
  494. package/src/formats/cx/exporter.ts +1483 -0
  495. package/src/formats/cx/importer.ts +888 -300
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +61 -338
  498. package/src/formats/cx2/importer.ts +317 -86
  499. package/src/formats/cx2/index.ts +2 -1
  500. package/src/formats/cys/constants.ts +62 -17
  501. package/src/formats/cys/exporter.ts +1368 -0
  502. package/src/formats/cys/importer.ts +659 -157
  503. package/src/formats/cys/index.ts +4 -2
  504. package/src/formats/cys/session.ts +139 -31
  505. package/src/formats/cys/tables.ts +47 -9
  506. package/src/formats/dot/exporter.ts +83 -37
  507. package/src/formats/dot/importer.ts +248 -54
  508. package/src/formats/dot/index.ts +2 -1
  509. package/src/formats/dot/names.ts +44 -11
  510. package/src/formats/dot/tokenizer.ts +25 -6
  511. package/src/formats/gexf/exporter.ts +57 -18
  512. package/src/formats/gexf/importer.ts +636 -105
  513. package/src/formats/gexf/index.ts +90 -21
  514. package/src/formats/gexf/schema.ts +67 -15
  515. package/src/formats/gml/exporter.ts +90 -36
  516. package/src/formats/gml/importer.ts +388 -67
  517. package/src/formats/gml/index.ts +85 -24
  518. package/src/formats/gml/syntax.ts +111 -26
  519. package/src/formats/graphml/constants.ts +178 -44
  520. package/src/formats/graphml/exporter.ts +52 -18
  521. package/src/formats/graphml/importer.ts +640 -60
  522. package/src/formats/graphml/index.ts +2 -1
  523. package/src/formats/graphml/tree.ts +6 -2
  524. package/src/formats/graphml/yfiles.ts +37 -15
  525. package/src/formats/json/dialect.ts +128 -29
  526. package/src/formats/json/exporter.ts +205 -68
  527. package/src/formats/json/importer.ts +1163 -253
  528. package/src/formats/json/index.ts +3 -1
  529. package/src/formats/json/obographs-export.ts +1197 -0
  530. package/src/formats/json/obographs.ts +353 -92
  531. package/src/formats/neo4j/exporter.ts +320 -101
  532. package/src/formats/neo4j/header.ts +44 -6
  533. package/src/formats/neo4j/importer.ts +512 -73
  534. package/src/formats/neo4j/index.ts +75 -23
  535. package/src/formats/obo/exporter.ts +2210 -0
  536. package/src/formats/obo/importer.ts +622 -109
  537. package/src/formats/obo/index.ts +6 -3
  538. package/src/formats/obo/syntax.ts +232 -37
  539. package/src/formats/pajek/exporter.ts +89 -41
  540. package/src/formats/pajek/importer.ts +407 -93
  541. package/src/formats/pajek/index.ts +2 -1
  542. package/src/formats/pajek/syntax.ts +213 -52
  543. package/src/formats/xgmml/columns.ts +10 -0
  544. package/src/formats/xgmml/constants.ts +183 -56
  545. package/src/formats/xgmml/document.ts +212 -34
  546. package/src/formats/xgmml/emit.ts +175 -34
  547. package/src/formats/xgmml/exporter.ts +35 -27
  548. package/src/formats/xgmml/importer.ts +64 -86
  549. package/src/formats/xgmml/index.ts +1 -0
  550. package/src/formats/xgmml/values.ts +11 -3
  551. package/src/index.ts +81 -11
  552. package/src/registry.ts +1121 -135
  553. package/src/sniff.ts +163 -32
  554. package/src/types.ts +385 -114
  555. package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
  556. package/dist/chunks/export-Bh60Dv-n.js +0 -582
  557. package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
  558. package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
  559. package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
  560. package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
  561. package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
  562. package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
  563. package/dist/chunks/importer-DET1tg6s.js.map +0 -1
  564. package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
  565. package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
  566. package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
  567. package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
  568. package/dist/chunks/importer-eUREhBGj.js.map +0 -1
  569. package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
  570. package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
  571. package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
  572. package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
  573. package/dist/chunks/ontology-BnrJ4I98.js +0 -113
  574. package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
  575. package/dist/chunks/records-dbRkxwaq.js.map +0 -1
  576. package/dist/chunks/report-B1z4WT9e.js +0 -2372
  577. package/dist/chunks/report-B1z4WT9e.js.map +0 -1
  578. package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
  579. package/dist/chunks/weights-Dzba96G3.js +0 -176
  580. package/dist/chunks/weights-Dzba96G3.js.map +0 -1
  581. package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
@@ -2,7 +2,7 @@
2
2
  * The CSV / TSV importer (design sections 8.4 and 8.6; research note 07 section 2.6): a streaming
3
3
  * edge-list reader for the generic (`source,target[,weight,...]`), Gephi (`Source,Target,Type,Id,
4
4
  * Label,Weight,...`) and headerless (`u v [w]`) dialects, with an optional node table merged by id,
5
- * and, with `table: "adjacency"`, an adjacency table (`node,neighbour[:weight],...`).
5
+ * and, with `table: "adjacency"`, an adjacency table (`node,neighbor[:weight],...`).
6
6
  *
7
7
  * - The delimiter is sniffed from a preview unless given; LF, CRLF and lone-CR files all read.
8
8
  * - The first row is a header when it holds a known column name or when it is all text over a
@@ -21,7 +21,7 @@
21
21
  * all-text column of low cardinality becomes a dict (design section 5.4); an `id` column of the
22
22
  * edge table is the edge id (role id, unique); a `label` column is the label (role label).
23
23
  * - An adjacency table has no header (unless `header: true`, which skips the first row): each row
24
- * is a node followed by its neighbours, one edge per neighbour in row order. A neighbour cell
24
+ * is a node followed by its neighbors, one edge per neighbor in row order. A neighbor cell
25
25
  * `id:weight` carries the edge's weight when the text after its LAST colon is a number; a cell
26
26
  * ending in a bare colon (`a:1:`) is the id before it with no weight; any other cell is the id
27
27
  * as written (`http://x`). A row holding only its node adds an isolated node. An empty adjacency
@@ -38,42 +38,138 @@
38
38
  */
39
39
  import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
40
40
  import { declareResolved, RENAMED_CODE, uniqueColumnName } from "../../common/attributes.js";
41
- import { DUPLICATE_EDGE_ID_CODE as SHARED_DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE as SHARED_DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE as SHARED_EMPTY_INPUT_CODE, ID_MERGED_CODE as SHARED_ID_MERGED_CODE, MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE, MISSING_ID_CODE as SHARED_MISSING_ID_CODE, ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE, } from "../../common/codes.js";
41
+ import { DUPLICATE_EDGE_ID_CODE as SHARED_DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE as SHARED_DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE as SHARED_EMPTY_INPUT_CODE, ID_MERGED_CODE as SHARED_ID_MERGED_CODE, MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE, MISSING_ID_CODE as SHARED_MISSING_ID_CODE, OPTION_IGNORED_CODE, ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE, } from "../../common/codes.js";
42
42
  import { DirectionResolver } from "../../common/direction.js";
43
43
  import { IdCoercer } from "../../common/ids.js";
44
44
  import { throwIfAborted } from "../../common/input.js";
45
45
  import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
46
+ import { plural } from "../../common/plural.js";
46
47
  import { ImportReportBuilder } from "../../common/report.js";
47
48
  import { parseWeightText } from "../../common/weights.js";
48
49
  import { EDGE_ID_NAMES, findColumn, headerNames, ID_NAMES, LABEL_NAMES, looksLikeHeader, positionalNames, resolveColumnRef, SOURCE_NAMES, TARGET_NAMES, TYPE_NAME, } from "./header.js";
49
- import { CsvRecordReader, sniffDelimiter, sniffNewline } from "./records.js";
50
+ import { checkRecordSyntax, CsvRecordReader, DELIMITER_CANDIDATES, sniffDelimiter, sniffNewline, splitRecords, stripLeadingComments, } from "./records.js";
50
51
  import { InferredColumn } from "./values.js";
51
- /** Issue code: the input holds no header row at all. */
52
+ /**
53
+ * The input holds no header row at all.
54
+ * @category Issue and loss codes
55
+ */
52
56
  export const EMPTY_INPUT_CODE = SHARED_EMPTY_INPUT_CODE;
53
- /** Issue code: the header names no source / target (or, for a node table, no id) column. */
57
+ /**
58
+ * The header names no source / target (or, for a node table, no id) column.
59
+ * @category Issue and loss codes
60
+ */
54
61
  export const NO_ENDPOINT_COLUMNS_CODE = "E_CSV_NO_ENDPOINT_COLUMNS";
55
- /** Issue code: a node table without an id column. */
62
+ /**
63
+ * A node table without an id column.
64
+ * @category Issue and loss codes
65
+ */
56
66
  export const NO_ID_COLUMN_CODE = "E_CSV_NO_ID_COLUMN";
57
- /** Issue code: a row with a different number of fields than the header. */
67
+ /**
68
+ * A row with a different number of fields than the header.
69
+ * @category Issue and loss codes
70
+ */
58
71
  export const FIELD_COUNT_CODE = "E_CSV_FIELD_COUNT";
59
- /** Issue code: an edge row with a blank source or target cell. */
72
+ /**
73
+ * An edge row with a blank source or target cell.
74
+ * @category Issue and loss codes
75
+ */
60
76
  export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
61
- /** Issue code: a node row with a blank id cell. */
77
+ /**
78
+ * A node row with a blank id cell.
79
+ * @category Issue and loss codes
80
+ */
62
81
  export const MISSING_ID_CODE = SHARED_MISSING_ID_CODE;
63
- /** Issue code: a Type cell that is not Directed, Undirected or Mutual. */
82
+ /**
83
+ * A Type cell that is not Directed, Undirected or Mutual.
84
+ * @category Issue and loss codes
85
+ */
64
86
  export const BAD_TYPE_CODE = "E_CSV_BAD_TYPE";
65
- /** Issue code: the table has a header and no data rows. */
87
+ /**
88
+ * The table has a header and no data rows.
89
+ * @category Issue and loss codes
90
+ */
66
91
  export const NO_DATA_ROWS_CODE = "W_CSV_NO_DATA_ROWS";
67
- /** Issue code: a node table row repeats an id; its attributes overwrite the earlier row's. */
92
+ /**
93
+ * A node table row repeats an id; its attributes overwrite the earlier row's.
94
+ * @category Issue and loss codes
95
+ */
68
96
  export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
69
- /** Issue code: two distinct id cells became one id under `ids: "number"` (design section 4.1). */
97
+ /**
98
+ * Two different id cells became the same number because `ids` is "number", so their nodes were merged.
99
+ * @category Issue and loss codes
100
+ */
70
101
  export const ID_MERGED_CODE = SHARED_ID_MERGED_CODE;
71
- /** Issue code: an explicitly named weight column the file does not have. */
102
+ /**
103
+ * An explicitly named weight column the file does not have.
104
+ * @category Issue and loss codes
105
+ */
72
106
  export const COLUMN_MISSING_CODE = "W_CSV_COLUMN_MISSING";
73
- /** Issue code: a column whose role (id, label) is already held by another column of the sink. */
107
+ /**
108
+ * You read into a graph builder that already has an id or label attribute, so this file's one is kept without its
109
+ * role.
110
+ * @category Issue and loss codes
111
+ */
74
112
  export const ROLE_TAKEN_CODE = SHARED_ROLE_TAKEN_CODE;
75
- /** Issue code: a repeated edge id (the column is unique); the edge is skipped. */
113
+ /**
114
+ * A repeated edge id (the column is unique); the edge is skipped.
115
+ * @category Issue and loss codes
116
+ */
76
117
  export const DUPLICATE_EDGE_ID_CODE = SHARED_DUPLICATE_EDGE_ID_CODE;
118
+ /**
119
+ * The input starts like another format (XML or HTML, JSON, GML, DOT or Pajek), not CSV. The import stops.
120
+ * @category Issue and loss codes
121
+ */
122
+ export const OTHER_FORMAT_CODE = "E_CSV_OTHER_FORMAT";
123
+ /**
124
+ * A quote inside an unquoted field (RFC 4180 forbids it); the quote is kept as text.
125
+ * @category Issue and loss codes
126
+ */
127
+ export const STRAY_QUOTE_CODE = "W_CSV_STRAY_QUOTE";
128
+ /**
129
+ * A leading comment's direction disagrees with defaultDirected or with an earlier comment.
130
+ * @category Issue and loss codes
131
+ */
132
+ export const COMMENT_DIRECTION_CODE = "W_CSV_COMMENT_DIRECTION";
133
+ /**
134
+ * A column named like Gephi's Type holds direction words but is read as a plain attribute.
135
+ * @category Issue and loss codes
136
+ */
137
+ export const TYPE_COLUMN_IGNORED_CODE = "W_CSV_TYPE_COLUMN_IGNORED";
138
+ /**
139
+ * The header ends in a delimiter; rows without the empty last cell are read as complete.
140
+ * @category Issue and loss codes
141
+ */
142
+ export const TRAILING_HEADER_DELIMITER_CODE = "W_CSV_TRAILING_HEADER_DELIMITER";
143
+ /**
144
+ * Every row is one cell that another delimiter would split (a likely wrong delimiter).
145
+ * @category Issue and loss codes
146
+ */
147
+ export const SINGLE_COLUMN_CODE = "W_CSV_SINGLE_COLUMN";
148
+ /**
149
+ * Several header columns name the same role; the one not chosen is a plain attribute.
150
+ * @category Issue and loss codes
151
+ */
152
+ export const AMBIGUOUS_COLUMN_CODE = "W_CSV_AMBIGUOUS_COLUMN";
153
+ /**
154
+ * An unquoted id with leading or trailing whitespace (kept, RFC 4180), distinct from the bare id.
155
+ * @category Issue and loss codes
156
+ */
157
+ export const PADDED_ID_CODE = "W_CSV_PADDED_ID";
158
+ /**
159
+ * A leading `#` / `%` line skipped as a comment has the fields of a record.
160
+ * @category Issue and loss codes
161
+ */
162
+ export const COMMENT_LIKE_RECORD_CODE = "W_CSV_COMMENT_LIKE_RECORD";
163
+ /**
164
+ * Data rows end in one extra empty cell (a trailing delimiter); the cell is dropped.
165
+ * @category Issue and loss codes
166
+ */
167
+ export const TRAILING_DELIMITER_CODE = "W_CSV_TRAILING_DELIMITER";
168
+ /**
169
+ * A headerless three-column table's third column holds text, so it is an attribute, not the weight.
170
+ * @category Issue and loss codes
171
+ */
172
+ export const WEIGHT_AS_ATTRIBUTE_CODE = "W_CSV_WEIGHT_AS_ATTRIBUTE";
77
173
  const TABLE_MODES = new Set(["edges", "nodes", "adjacency", "auto"]);
78
174
  /** The common options an edge-table import reads (the rest is reported by reportUnusedOptions). */
79
175
  const USED_OPTIONS = new Set([
@@ -94,28 +190,45 @@ const USED_OPTIONS_WITH_NODES = new Set([
94
190
  ...USED_OPTIONS,
95
191
  "nodeIdFrom",
96
192
  ]);
97
- const BAD_DELIMITERS = new Set(['"', "\n", "\r"]);
98
193
  /** The comment characters of the SNAP (`#`) and KONECT (`%`) headers (research note 07 section 2.6). */
99
194
  const COMMENT_CHARS = Object.freeze(["#", "%"]);
195
+ /** A number written with a decimal comma: digits, one comma, digits, an optional exponent. */
196
+ const DECIMAL_COMMA = /^\s*[+-]?\d+,\d+(?:[eE][+-]?\d+)?\s*$/;
100
197
  /**
101
- * The direction a SNAP or KONECT comment header declares (research note 07 section 2.6): SNAP
198
+ * The direction a SNAP or KONECT comment line declares (research note 07 section 2.6): SNAP
102
199
  * pages write `# Directed graph` / `# Undirected graph`, KONECT's first line is `% sym` (undirected),
103
- * `% asym` (directed) or `% bip` (bipartite, undirected).
104
- * @param comments - the leading comment lines
105
- * @returns true / false when a line declares the direction, null otherwise
200
+ * `% asym` (directed) or `% bip` (bipartite, undirected). Only whole words count, so
201
+ * `% symbols` or `# bipartite-ish` declare nothing.
202
+ * @param comment - a leading comment line
203
+ * @returns true / false when the line declares the direction, null otherwise
106
204
  */
107
- function commentDirection(comments) {
108
- for (const comment of comments) {
109
- const text = comment.slice(1).trim().toLowerCase();
110
- if (text.startsWith("directed graph") || text.startsWith("asym")) {
111
- return true;
112
- }
113
- if (text.startsWith("undirected graph") || text.startsWith("sym") || text.startsWith("bip")) {
114
- return false;
115
- }
205
+ function commentDirection(comment) {
206
+ const text = comment.slice(1).trim().toLowerCase();
207
+ if (/^(?:directed graph|asym)\b/.test(text)) {
208
+ return true;
209
+ }
210
+ if (/^(?:undirected graph|sym|bip)\b/.test(text)) {
211
+ return false;
116
212
  }
117
213
  return null;
118
214
  }
215
+ /**
216
+ * The name of a direction, for messages.
217
+ * @param directed - the direction
218
+ * @returns "directed" or "undirected"
219
+ */
220
+ function directionName(directed) {
221
+ return directed ? "directed" : "undirected";
222
+ }
223
+ /**
224
+ * Whether an unquoted id cell has leading or trailing whitespace (kept as written, RFC 4180).
225
+ * @param text - the cell text
226
+ * @param quoted - whether the cell was quoted
227
+ * @returns true when padded
228
+ */
229
+ function isPadded(text, quoted) {
230
+ return quoted !== true && text.trim().length !== text.length;
231
+ }
119
232
  /**
120
233
  * Apply the defaults of the CSV options and check every value.
121
234
  * @param options - the caller's options
@@ -129,12 +242,22 @@ function resolveCsvOptions(options) {
129
242
  found: o.delimiter,
130
243
  });
131
244
  }
132
- if (o.delimiter !== undefined && BAD_DELIMITERS.has(o.delimiter)) {
133
- throw new GraphFormatError("E_UNSUPPORTED", "option delimiter: a quote or a line break cannot delimit", {
134
- option: "delimiter",
135
- found: o.delimiter,
245
+ if (o.decimal !== undefined && o.decimal !== "." && o.decimal !== ",") {
246
+ throw new GraphFormatError("E_UNSUPPORTED", 'option decimal: expected "." or ","', {
247
+ option: "decimal",
248
+ found: o.decimal,
249
+ });
250
+ }
251
+ if (o.decimal === "," && o.delimiter === ",") {
252
+ throw new GraphFormatError("E_UNSUPPORTED", 'options delimiter and decimal are both ","; pass delimiter: ";"', {
253
+ option: "decimal",
254
+ found: o.decimal,
136
255
  });
137
256
  }
257
+ if (o.delimiter !== undefined) {
258
+ // one character, not a line break, not the quote
259
+ checkRecordSyntax({ delimiter: o.delimiter, quote: '"' });
260
+ }
138
261
  if (o.header !== undefined && o.header !== "auto" && typeof o.header !== "boolean") {
139
262
  throw new GraphFormatError("E_UNSUPPORTED", 'option header: expected true, false or "auto"', {
140
263
  option: "header",
@@ -147,14 +270,14 @@ function resolveCsvOptions(options) {
147
270
  found: o.table,
148
271
  });
149
272
  }
150
- for (const name of ["sourceColumn", "targetColumn", "idColumn"]) {
273
+ for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"]) {
151
274
  checkColumnRef(name, o[name]);
152
275
  }
153
276
  if (o.typeColumn !== null) {
154
277
  checkColumnRef("typeColumn", o.typeColumn);
155
278
  }
156
279
  if (o.table === "adjacency") {
157
- for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn"]) {
280
+ for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"]) {
158
281
  if (o[name] !== undefined) {
159
282
  throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
160
283
  option: name,
@@ -177,14 +300,17 @@ function resolveCsvOptions(options) {
177
300
  }
178
301
  return {
179
302
  delimiter: o.delimiter ?? null,
303
+ decimalComma: o.decimal === ",",
180
304
  header: o.header ?? "auto",
181
305
  table: o.table ?? "auto",
182
306
  sourceColumn: o.sourceColumn ?? null,
183
307
  targetColumn: o.targetColumn ?? null,
184
308
  typeColumn: o.typeColumn,
185
309
  idColumn: o.idColumn ?? null,
186
- nodes: o.nodes ?? null,
310
+ labelColumn: o.labelColumn ?? null,
311
+ nodes: nodesInput(o.nodes),
187
312
  rowNumberIds: o.rowNumberIds ?? false,
313
+ explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
188
314
  };
189
315
  }
190
316
  /**
@@ -301,11 +427,12 @@ function parseKind(text) {
301
427
  }
302
428
  }
303
429
  /**
304
- * Split an adjacency neighbour cell into its id and weight: the text after the LAST colon is the
430
+ * Split an adjacency neighbor cell into its id and weight: the text after the LAST colon is the
305
431
  * weight when it is a number, nothing when it is empty (`a:1:` is the id `a:1`, unweighted, the
306
432
  * form the exporter writes for an id containing a colon), and part of the id otherwise.
307
433
  * @param text - the cell text
308
434
  * @returns the id text and the weight (undefined when the cell has none)
435
+ * @category Plugin helpers
309
436
  */
310
437
  export function splitNeighbour(text) {
311
438
  const colon = text.lastIndexOf(":");
@@ -351,6 +478,8 @@ class TableReader {
351
478
  this.labelHandle = INVALID_INDEX;
352
479
  this.dataRows = 0;
353
480
  this.nodeOrdinal = 0;
481
+ /** Whether a field-count error has already suggested `table: "adjacency"`. */
482
+ this.adjacencyHinted = false;
354
483
  /** The edge ids seen so far (the id column is unique; a repeat is skipped with an issue). */
355
484
  this.edgeIds = new Set();
356
485
  this.where = { line: null, element: null };
@@ -359,17 +488,27 @@ class TableReader {
359
488
  this.rowNumbers = state.csv.rowNumberIds && (kind === "nodes" || (kind === "auto" && state.csv.nodes === null));
360
489
  const readerOptions = {
361
490
  delimiter: state.csv.delimiter,
491
+ // a decimal comma is never the delimiter
492
+ ...(state.csv.decimalComma ? { candidates: DELIMITER_CANDIDATES.filter((d) => d !== ",") } : {}),
362
493
  comments: COMMENT_CHARS,
363
494
  // field counts say nothing about an adjacency table, whose rows vary in width
364
495
  skipQuoteErrors: kind === "adjacency",
365
496
  signal: state.common.signal,
366
497
  onProgress: progress ? state.common.onProgress : null,
367
498
  encoding: state.common.encoding,
499
+ // an adjacency table has no header: an empty one is the empty graph
500
+ allowEmpty: kind === "adjacency",
501
+ // explicit dialect options say the caller knows the input is a table
502
+ inspect: hasExplicitDialect(state.csv)
503
+ ? undefined
504
+ : (preview, line) => {
505
+ this.refuseOtherFormat(preview, line);
506
+ },
368
507
  };
369
508
  this.reader = new CsvRecordReader(input, state.report, readerOptions);
370
509
  }
371
510
  /**
372
- * Read every row; the reader is closed (and a stream cancelled) when the import aborts midway.
511
+ * Read every row; the reader is closed (and a stream canceled) when the import aborts midway.
373
512
  */
374
513
  async read() {
375
514
  const iterator = this.reader[Symbol.asyncIterator]();
@@ -386,7 +525,11 @@ class TableReader {
386
525
  */
387
526
  async readRows(iterator) {
388
527
  const { report } = this.state;
389
- const first = await iterator.next();
528
+ let first = await iterator.next();
529
+ if (!first.done && (this.reader.sepDirective || this.isSepDirective(first.value))) {
530
+ // Excel's `sep=;` line: the reader took the delimiter from it; it is not a row
531
+ first = await iterator.next();
532
+ }
390
533
  if (first.done && this.kind === "adjacency") {
391
534
  // an adjacency table has no header: an empty one is the empty graph
392
535
  return;
@@ -412,7 +555,8 @@ class TableReader {
412
555
  const secondRow = second.done ? null : second.value;
413
556
  const secondLine = this.reader.line;
414
557
  const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
415
- header = looksLikeHeader(firstRow, secondRow);
558
+ // a column option naming a cell of the first row makes that row the header
559
+ header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
416
560
  if (!header) {
417
561
  pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
418
562
  }
@@ -426,11 +570,19 @@ class TableReader {
426
570
  pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
427
571
  }
428
572
  }
429
- const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
430
- if (this.kind !== "nodes" && this.state.commentDirected === null) {
431
- this.state.commentDirected = commentDirection(this.reader.leadingComments);
573
+ if (this.rowNumbers && !header) {
574
+ report.warning("unsupported", OPTION_IGNORED_CODE, "option rowNumberIds ignored: a headerless node table takes its ids from column 0", { line: firstLine, element: "rowNumberIds" });
432
575
  }
433
- this.plan = this.resolvePlan(names, header, firstLine);
576
+ const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
577
+ const last = firstRow.length - 1;
578
+ const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
579
+ this.applyCommentDirection();
580
+ this.plan = this.resolvePlan(names, header, firstLine, pending.map((p) => p.row), optionalLast);
581
+ if (optionalLast) {
582
+ report.warning("validation-error", TRAILING_HEADER_DELIMITER_CODE, `the header ends in a delimiter; rows without the empty last cell are read as complete`, { line: firstLine, element: names[last] });
583
+ }
584
+ this.reportCommentLikeRecords(names.length);
585
+ this.reportSingleColumn(names.length, pending.map((p) => p.row), firstLine);
434
586
  this.prepareColumns(firstLine);
435
587
  for (const { row, quoted, line } of pending) {
436
588
  this.processRow(row, quoted, line);
@@ -456,41 +608,132 @@ class TableReader {
456
608
  line: firstLine,
457
609
  });
458
610
  }
611
+ this.reportReaderFindings();
612
+ }
613
+ /**
614
+ * Whether a row is Excel's delimiter directive (`sep=;`) under a delimiter the caller gave (a
615
+ * sniffed one comes from the directive itself: `reader.sepDirective`).
616
+ * @param row - the first row
617
+ * @returns true for the directive
618
+ */
619
+ isSepDirective(row) {
620
+ return (this.state.csv.delimiter !== null &&
621
+ !this.reader.quoted[0] &&
622
+ /^sep=.$/i.test(row.join(this.state.csv.delimiter)));
623
+ }
624
+ /**
625
+ * Fail when the input opens like another format (an HTML error page, a JSON, GML, DOT or Pajek
626
+ * file handed to the CSV importer), which any delimiter would otherwise read as rows of text.
627
+ * Runs on the raw preview before any record is parsed, so a quoting error in the other
628
+ * format's syntax cannot abort first.
629
+ * @param preview - the start of the input, leading comments removed
630
+ * @param line - the line it starts on
631
+ */
632
+ refuseOtherFormat(preview, line) {
633
+ if (OTHER_FORMATS.some((r) => r.test(preview))) {
634
+ this.state.report.fail(OTHER_FORMAT_CODE, `the input opens like another format (XML / HTML, JSON, GML, DOT or Pajek), not CSV: ${JSON.stringify(preview.trimStart().slice(0, 60))}`, { line });
635
+ }
636
+ }
637
+ /**
638
+ * Whether a column option names a cell of the first row by text.
639
+ * @param row - the first row
640
+ * @returns true when sourceColumn, targetColumn, idColumn or typeColumn names one of its cells
641
+ */
642
+ namesExplicitColumn(row) {
643
+ const { csv } = this.state;
644
+ const names = headerNames(row);
645
+ return [csv.sourceColumn, csv.targetColumn, csv.idColumn, csv.typeColumn, csv.labelColumn].some((ref) => typeof ref === "string" && findColumn(names, [ref]) >= 0);
646
+ }
647
+ /**
648
+ * Take the file-level direction from the leading SNAP / KONECT comments of this table: the first
649
+ * declaring line wins; a later line declaring the other direction, and a declaration that
650
+ * overrides the caller's explicit defaultDirected, are reported.
651
+ */
652
+ applyCommentDirection() {
653
+ const { state } = this;
654
+ const { leadingComments, leadingCommentLines } = this.reader;
655
+ for (let i = 0; i < leadingComments.length; i++) {
656
+ const comment = leadingComments[i];
657
+ const directed = commentDirection(comment);
658
+ if (directed === null) {
659
+ continue;
660
+ }
661
+ const where = { line: leadingCommentLines[i], element: comment };
662
+ if (state.commentDirected === null) {
663
+ state.commentDirected = directed;
664
+ state.commentText = comment;
665
+ const explicit = state.csv.explicitDirected;
666
+ if (explicit !== null && explicit !== directed) {
667
+ state.report.warning("validation-error", COMMENT_DIRECTION_CODE, `${JSON.stringify(comment)} declares a ${directionName(directed)} graph, which overrides defaultDirected: ${explicit}`, where);
668
+ }
669
+ }
670
+ else if (state.commentDirected !== directed) {
671
+ state.report.warning("validation-error", COMMENT_DIRECTION_CODE, `${JSON.stringify(comment)} declares a ${directionName(directed)} graph but ${JSON.stringify(state.commentText)} declared a ${directionName(state.commentDirected)} one; the first applies`, where);
672
+ }
673
+ }
674
+ }
675
+ /**
676
+ * Report a leading `#` / `%` line that has the fields of a record (a first id starting with `#`,
677
+ * swallowed as a comment): its text directly follows the comment character and splits into as
678
+ * many cells as the table has columns. SNAP and KONECT comment lines put a space after it.
679
+ * @param width - the number of columns
680
+ */
681
+ reportCommentLikeRecords(width) {
682
+ const { delimiter, leadingComments, leadingCommentLines } = this.reader;
683
+ if (width < 2 || delimiter === null) {
684
+ return;
685
+ }
686
+ for (let i = 0; i < leadingComments.length; i++) {
687
+ const comment = leadingComments[i];
688
+ if (comment.length > 1 && comment[1].trim().length > 0 && comment.split(delimiter).length === width) {
689
+ this.state.report.warnOnce("parse-error", COMMENT_LIKE_RECORD_CODE, `${JSON.stringify(comment)} was skipped as a leading comment but has the fields of a record; quote an id that starts with # or %`, { line: leadingCommentLines[i], element: comment });
690
+ }
691
+ }
692
+ }
693
+ /**
694
+ * Report a one-column table whose cells another delimiter would split consistently (a delimiter
695
+ * option that does not match the file).
696
+ * @param width - the number of columns
697
+ * @param rows - the data rows read so far
698
+ * @param line - the first line
699
+ */
700
+ reportSingleColumn(width, rows, line) {
701
+ if (width !== 1 || rows.length === 0 || this.plan?.kind === "adjacency") {
702
+ return;
703
+ }
704
+ const candidates = DELIMITER_CANDIDATES.filter((d) => d !== this.reader.delimiter);
705
+ const other = sniffDelimiter(`${rows.map((r) => r[0]).join("\n")}\n`, "\n", candidates);
706
+ if (other !== null) {
707
+ this.state.report.warning("parse-error", SINGLE_COLUMN_CODE, `every row read so far is one cell that ${JSON.stringify(other)} splits consistently; pass delimiter: ${JSON.stringify(other)} if that is the file's delimiter`, { line });
708
+ }
709
+ }
710
+ /** Report what the record reader noticed while reading: a stray quote. */
711
+ reportReaderFindings() {
712
+ const { report } = this.state;
713
+ const { strayQuoteLine } = this.reader;
714
+ if (strayQuoteLine > 0) {
715
+ report.warnOnce("parse-error", STRAY_QUOTE_CODE, `a quote inside an unquoted field is kept as text (RFC 4180 quotes a whole field)`, { line: strayQuoteLine });
716
+ }
459
717
  }
460
718
  /**
461
719
  * Decide the table's columns from its header.
462
720
  * @param names - the column names
463
721
  * @param header - whether the file has a header row
464
722
  * @param line - the header line
723
+ * @param sample - the data rows read so far
724
+ * @param optionalLast - whether a row may omit the last cell
465
725
  * @returns the plan; the import aborts when no endpoints (or id) resolve
466
726
  */
467
- resolvePlan(names, header, line) {
468
- const { csv, report } = this.state;
727
+ resolvePlan(names, header, line, sample, optionalLast) {
728
+ const { csv } = this.state;
469
729
  if (this.kind === "adjacency") {
470
730
  return { kind: "adjacency", names: [] };
471
731
  }
472
- const width = names.length;
473
732
  let source = -1;
474
733
  let target = -1;
475
734
  if (this.kind !== "nodes") {
476
- if (csv.sourceColumn !== null) {
477
- source = resolveColumnRef(names, csv.sourceColumn, "sourceColumn");
478
- }
479
- else if (header) {
480
- source = findColumn(names, SOURCE_NAMES);
481
- }
482
- else {
483
- source = width >= 2 ? 0 : -1;
484
- }
485
- if (csv.targetColumn !== null) {
486
- target = resolveColumnRef(names, csv.targetColumn, "targetColumn");
487
- }
488
- else if (header) {
489
- target = findColumn(names, TARGET_NAMES);
490
- }
491
- else {
492
- target = width >= 2 ? 1 : -1;
493
- }
735
+ source = endpointColumn(names, header, csv.sourceColumn, "sourceColumn", SOURCE_NAMES, 0);
736
+ target = endpointColumn(names, header, csv.targetColumn, "targetColumn", TARGET_NAMES, 1);
494
737
  if (source >= 0 && target >= 0 && source === target) {
495
738
  throw new GraphFormatError("E_UNSUPPORTED", "sourceColumn and targetColumn name the same column", {
496
739
  option: "targetColumn",
@@ -499,18 +742,36 @@ class TableReader {
499
742
  }
500
743
  }
501
744
  if (source >= 0 && target >= 0) {
502
- return this.edgePlan(names, header, source, target);
745
+ return this.edgePlan(names, header, source, target, line, sample, optionalLast);
503
746
  }
747
+ this.refuseWithoutEndpoints(names, header, source, target, line);
748
+ return this.nodePlan(names, header, line, optionalLast);
749
+ }
750
+ /**
751
+ * Refuse a table without both endpoint columns, unless it can be a node table: an edge table
752
+ * was asked for, a single endpoint column was found, or nothing names an id.
753
+ * @param names - the column names
754
+ * @param header - whether the file has a header row
755
+ * @param source - the source column, or -1
756
+ * @param target - the target column, or -1
757
+ * @param line - the header line
758
+ */
759
+ refuseWithoutEndpoints(names, header, source, target, line) {
760
+ const { csv, report } = this.state;
504
761
  const shown = names.map((n) => JSON.stringify(n)).join(", ");
762
+ if (source >= 0 || target >= 0) {
763
+ // one endpoint column is a broken edge table, never a node table
764
+ const [has, missing] = source >= 0 ? ["source", "target"] : ["target", "source"];
765
+ report.fail(NO_ENDPOINT_COLUMNS_CODE, `the header has a ${has} column (${JSON.stringify(names[Math.max(source, target)])}) but no ${missing} column (${shown})`, { line }, { columns: [...names] });
766
+ }
505
767
  if (this.kind === "edges" ||
506
- (this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null))) {
768
+ (this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null || csv.nodes !== null))) {
507
769
  report.fail(NO_ENDPOINT_COLUMNS_CODE, `no source / target columns in the header (${shown}); a node table goes in the nodes option`, { line }, { columns: [...names] });
508
770
  }
509
- const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : width >= 1;
771
+ const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : names.length >= 1;
510
772
  if (this.kind === "auto" && csv.idColumn === null && !idResolves && !this.rowNumbers) {
511
773
  report.fail(NO_ENDPOINT_COLUMNS_CODE, `no source / target columns and no id column in the header (${shown}); the input is neither an edge table nor a node table`, { line }, { columns: [...names] });
512
774
  }
513
- return this.nodePlan(names, header, line);
514
775
  }
515
776
  /**
516
777
  * The columns of an edge table.
@@ -518,47 +779,26 @@ class TableReader {
518
779
  * @param header - whether the file has a header row
519
780
  * @param source - the source column
520
781
  * @param target - the target column
782
+ * @param line - the header (or first) line
783
+ * @param sample - the data rows read so far
784
+ * @param optionalLast - whether a row may omit the last cell
521
785
  * @returns the plan
522
786
  */
523
- edgePlan(names, header, source, target) {
524
- const { csv, common, report } = this.state;
787
+ edgePlan(names, header, source, target, line, sample, optionalLast) {
788
+ const { csv } = this.state;
525
789
  const claimed = new Set([source, target]);
526
- let weight = -1;
527
- if (common.weightFrom !== null) {
528
- if (header) {
529
- weight = findFree(names, [common.weightFrom], claimed);
530
- if (weight < 0 && this.state.weightFromExplicit) {
531
- report.warning("missing-value", COLUMN_MISSING_CODE, `weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`, { line: this.reader.line, element: common.weightFrom });
532
- }
533
- }
534
- else if (names.length >= 3 && !claimed.has(2)) {
535
- weight = 2;
536
- }
537
- }
790
+ const weight = this.weightColumn(names, header, claimed, line, sample);
538
791
  if (weight >= 0) {
539
792
  claimed.add(weight);
540
793
  }
541
- let type = -1;
542
- if (csv.typeColumn === undefined) {
543
- if (header && names[source] === "Source" && names[target] === "Target") {
544
- type = findFree(names, [TYPE_NAME], claimed);
545
- if (type >= 0 && names[type] !== TYPE_NAME) {
546
- type = -1;
547
- }
548
- }
549
- }
550
- else if (csv.typeColumn !== null) {
551
- type = resolveColumnRef(names, csv.typeColumn, "typeColumn");
552
- if (claimed.has(type)) {
553
- throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
554
- option: "typeColumn",
555
- found: names[type],
556
- });
557
- }
794
+ if (header) {
795
+ this.reportEndpointRivals(names, source, target, weight, line);
558
796
  }
797
+ const type = this.typeColumn(names, header, source, target, claimed);
559
798
  if (type >= 0) {
560
799
  claimed.add(type);
561
800
  }
801
+ const typeHint = header && type < 0 && csv.typeColumn === undefined ? findFree(names, [TYPE_NAME], claimed) : -1;
562
802
  const id = header ? findFree(names, EDGE_ID_NAMES, claimed) : -1;
563
803
  if (id >= 0) {
564
804
  claimed.add(id);
@@ -573,53 +813,147 @@ class TableReader {
573
813
  attributes.push(i);
574
814
  }
575
815
  }
576
- return { kind: "edges", names, width: names.length, source, target, weight, type, id, label, attributes };
816
+ return {
817
+ kind: "edges",
818
+ names,
819
+ width: names.length,
820
+ source,
821
+ target,
822
+ weight,
823
+ type,
824
+ typeHint,
825
+ id,
826
+ label,
827
+ attributes,
828
+ optionalLast,
829
+ };
830
+ }
831
+ /**
832
+ * The weight column of an edge table: the weightFrom column of a header (a warning when it was
833
+ * asked for and is missing), else the third column of a headerless edge list unless it holds text.
834
+ * @param names - the column names
835
+ * @param header - whether the file has a header row
836
+ * @param claimed - the endpoint columns
837
+ * @param line - the header (or first) line
838
+ * @param sample - the data rows read so far
839
+ * @returns the column index, or -1
840
+ */
841
+ weightColumn(names, header, claimed, line, sample) {
842
+ const { common, report } = this.state;
843
+ if (common.weightFrom === null) {
844
+ return -1;
845
+ }
846
+ if (header) {
847
+ const weight = findFree(names, [common.weightFrom], claimed);
848
+ if (weight < 0 && this.state.weightFromExplicit) {
849
+ report.warning("missing-value", COLUMN_MISSING_CODE, `weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`, { line, element: common.weightFrom });
850
+ }
851
+ return weight;
852
+ }
853
+ if (names.length < 3 || claimed.has(2)) {
854
+ return -1;
855
+ }
856
+ if (isTextTriple(names.length, sample)) {
857
+ report.warnOnce("validation-error", WEIGHT_AS_ATTRIBUTE_CODE, `the third column holds text in the first rows, so it is read as the attribute ${names[2]}, not the weight`, { line, element: names[2] });
858
+ return -1;
859
+ }
860
+ return 2;
861
+ }
862
+ /**
863
+ * Warn about header columns that name the source, target or weight beside the chosen one.
864
+ * @param names - the header names
865
+ * @param source - the source column
866
+ * @param target - the target column
867
+ * @param weight - the weight column, or -1
868
+ * @param line - the header line
869
+ */
870
+ reportEndpointRivals(names, source, target, weight, line) {
871
+ const { csv, common } = this.state;
872
+ if (csv.sourceColumn === null) {
873
+ this.reportRivals(names, SOURCE_NAMES, source, "source", line);
874
+ }
875
+ if (csv.targetColumn === null) {
876
+ this.reportRivals(names, TARGET_NAMES, target, "target", line);
877
+ }
878
+ if (weight >= 0 && common.weightFrom !== null) {
879
+ this.reportRivals(names, [common.weightFrom], weight, "weight", line);
880
+ }
881
+ }
882
+ /**
883
+ * The edge type column: the typeColumn option, else Gephi's `Type` beside `Source` and `Target`.
884
+ * @param names - the column names
885
+ * @param header - whether the file has a header row
886
+ * @param source - the source column
887
+ * @param target - the target column
888
+ * @param claimed - the columns other roles took
889
+ * @returns the column index, or -1
890
+ */
891
+ typeColumn(names, header, source, target, claimed) {
892
+ const { typeColumn } = this.state.csv;
893
+ if (typeColumn === null) {
894
+ return -1;
895
+ }
896
+ if (typeColumn === undefined) {
897
+ const gephi = header && names[source] === "Source" && names[target] === "Target";
898
+ const type = gephi ? findFree(names, [TYPE_NAME], claimed) : -1;
899
+ return type >= 0 && names[type] === TYPE_NAME ? type : -1;
900
+ }
901
+ const type = resolveColumnRef(names, typeColumn, "typeColumn");
902
+ if (claimed.has(type)) {
903
+ throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
904
+ option: "typeColumn",
905
+ found: names[type],
906
+ });
907
+ }
908
+ return type;
909
+ }
910
+ /**
911
+ * Warn about header columns that name a role another column was chosen for (`Source,source`,
912
+ * `from,to,source`, `Weight,weight`): the one not chosen is read as a plain attribute.
913
+ * @param names - the header names
914
+ * @param candidates - the names of the role
915
+ * @param chosen - the column chosen
916
+ * @param role - the role, for the message
917
+ * @param line - the header line
918
+ */
919
+ reportRivals(names, candidates, chosen, role, line) {
920
+ const lower = new Set(candidates.map((c) => c.toLowerCase()));
921
+ names.forEach((name, i) => {
922
+ if (i !== chosen && lower.has(name.toLowerCase())) {
923
+ this.state.report.warning("validation-error", AMBIGUOUS_COLUMN_CODE, `columns ${JSON.stringify(names[chosen])} and ${JSON.stringify(name)} both name the ${role}; ${JSON.stringify(names[chosen])} is used and ${JSON.stringify(name)} is a plain attribute`, { line, element: name });
924
+ }
925
+ });
926
+ }
927
+ /**
928
+ * The label column of a node table: the labelColumn option, else a header column named `label`.
929
+ * @param names - the column names
930
+ * @param header - whether the file has a header row
931
+ * @param claimed - columns another role took
932
+ * @returns the column index, or -1
933
+ */
934
+ labelOf(names, header, claimed) {
935
+ const { labelColumn } = this.state.csv;
936
+ if (labelColumn !== null) {
937
+ return resolveColumnRef(names, labelColumn, "labelColumn");
938
+ }
939
+ return header ? findFree(names, LABEL_NAMES, claimed) : -1;
577
940
  }
578
941
  /**
579
942
  * The columns of a node table.
580
943
  * @param names - the column names
581
944
  * @param header - whether the file has a header row
582
945
  * @param line - the header line
946
+ * @param optionalLast - whether a row may omit the last cell
583
947
  * @returns the plan; the import aborts when no id column resolves
584
948
  */
585
- nodePlan(names, header, line) {
586
- const { csv, common, report } = this.state;
949
+ nodePlan(names, header, line, optionalLast) {
950
+ const { common } = this.state;
587
951
  const claimed = new Set();
588
- let idColumn = -1;
589
- if (csv.idColumn !== null) {
590
- idColumn = resolveColumnRef(names, csv.idColumn, "idColumn");
591
- }
592
- else if (header) {
593
- idColumn = findColumn(names, ID_NAMES);
594
- }
595
- else if (names.length >= 1) {
596
- idColumn = 0;
597
- }
598
- const label = header ? findFree(names, LABEL_NAMES, new Set(idColumn >= 0 ? [idColumn] : [])) : -1;
599
- let id;
600
- switch (common.nodeIdFrom) {
601
- case "label":
602
- if (label < 0) {
603
- report.fail(NO_ID_COLUMN_CODE, 'nodeIdFrom is "label" but the node table has no label column', {
604
- line,
605
- });
606
- }
607
- id = label;
608
- break;
609
- case "index":
610
- id = -1;
611
- break;
612
- default:
613
- if (idColumn < 0 && this.rowNumbers) {
614
- id = -1;
615
- break;
616
- }
617
- if (idColumn < 0) {
618
- report.fail(NO_ID_COLUMN_CODE, `no id column in the node table header (${names.map((n) => JSON.stringify(n)).join(", ")})`, { line }, { columns: [...names] });
619
- }
620
- id = idColumn;
621
- claimed.add(idColumn);
622
- break;
952
+ const idColumn = this.idColumnOf(names, header);
953
+ const label = this.labelOf(names, header, new Set(idColumn >= 0 ? [idColumn] : []));
954
+ const id = this.nodeIdColumn(names, idColumn, label, line);
955
+ if (id >= 0) {
956
+ claimed.add(id);
623
957
  }
624
958
  if (label >= 0) {
625
959
  claimed.add(label);
@@ -631,7 +965,50 @@ class TableReader {
631
965
  }
632
966
  }
633
967
  const rowNumber = id < 0 && common.nodeIdFrom !== "index";
634
- return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes };
968
+ return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes, optionalLast };
969
+ }
970
+ /**
971
+ * The id column of a node table: the idColumn option, else a header column with an id name,
972
+ * else the first column of a headerless table.
973
+ * @param names - the column names
974
+ * @param header - whether the file has a header row
975
+ * @returns the column index, or -1
976
+ */
977
+ idColumnOf(names, header) {
978
+ const { idColumn } = this.state.csv;
979
+ if (idColumn !== null) {
980
+ return resolveColumnRef(names, idColumn, "idColumn");
981
+ }
982
+ if (header) {
983
+ return findColumn(names, ID_NAMES);
984
+ }
985
+ return names.length >= 1 ? 0 : -1;
986
+ }
987
+ /**
988
+ * The column node ids come from, as nodeIdFrom says: the label, none (row numbers), or the id
989
+ * column. The import aborts when that column is missing.
990
+ * @param names - the column names
991
+ * @param idColumn - the id column, or -1
992
+ * @param label - the label column, or -1
993
+ * @param line - the header line
994
+ * @returns the column index, or -1 for row numbers
995
+ */
996
+ nodeIdColumn(names, idColumn, label, line) {
997
+ const { common, report } = this.state;
998
+ const shown = names.map((n) => JSON.stringify(n)).join(", ");
999
+ if (common.nodeIdFrom === "index") {
1000
+ return -1;
1001
+ }
1002
+ if (common.nodeIdFrom === "label") {
1003
+ if (label < 0) {
1004
+ report.fail(NO_ID_COLUMN_CODE, `nodeIdFrom is "label", but the node table has no column named "label" (${shown}); pass labelColumn to name the column that holds the labels`, { line }, { columns: [...names] });
1005
+ }
1006
+ return label;
1007
+ }
1008
+ if (idColumn < 0 && !this.rowNumbers) {
1009
+ report.fail(NO_ID_COLUMN_CODE, `no id column in the node table header (${shown})`, { line }, { columns: [...names] });
1010
+ }
1011
+ return idColumn;
635
1012
  }
636
1013
  /**
637
1014
  * Declare the role columns and create a writer per attribute column.
@@ -675,6 +1052,13 @@ class TableReader {
675
1052
  processRow(row, quoted, line) {
676
1053
  const plan = this.requirePlan();
677
1054
  this.dataRows++;
1055
+ if (this.state.csv.decimalComma) {
1056
+ for (let i = 0; i < row.length; i++) {
1057
+ if (DECIMAL_COMMA.test(row[i])) {
1058
+ row[i] = row[i].replace(",", ".");
1059
+ }
1060
+ }
1061
+ }
678
1062
  if (plan.kind === "edges") {
679
1063
  this.processEdgeRow(plan, row, quoted, line);
680
1064
  }
@@ -686,7 +1070,7 @@ class TableReader {
686
1070
  }
687
1071
  }
688
1072
  /**
689
- * Push one adjacency row: the node, then one edge per set neighbour cell, in row order.
1073
+ * Push one adjacency row: the node, then one edge per set neighbor cell, in row order.
690
1074
  * @param row - the cells
691
1075
  * @param quoted - whether each cell was quoted
692
1076
  * @param line - the row's line
@@ -697,16 +1081,16 @@ class TableReader {
697
1081
  const { where } = this;
698
1082
  where.line = line;
699
1083
  where.element = null;
700
- let neighbours = 0;
1084
+ let neighbors = 0;
701
1085
  for (let k = 1; k < row.length; k++) {
702
1086
  if (!isUnset(row[k], quoted[k])) {
703
- neighbours++;
1087
+ neighbors++;
704
1088
  }
705
1089
  }
706
1090
  if (isUnset(row[0], quoted[0])) {
707
- report.error("missing-value", MISSING_ID_CODE, `line ${line}: blank node cell`, { line });
1091
+ report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
708
1092
  counts.skippedNodes++;
709
- counts.skippedEdges += neighbours;
1093
+ counts.skippedEdges += neighbors;
710
1094
  return;
711
1095
  }
712
1096
  const kind = (this.state.commentDirected ?? common.defaultDirected) ? "directed" : "undirected";
@@ -726,7 +1110,7 @@ class TableReader {
726
1110
  catch (err) {
727
1111
  report.recordError(err, where);
728
1112
  counts.skippedNodes++;
729
- counts.skippedEdges += neighbours;
1113
+ counts.skippedEdges += neighbors;
730
1114
  return;
731
1115
  }
732
1116
  for (let k = 1; k < row.length; k++) {
@@ -753,14 +1137,15 @@ class TableReader {
753
1137
  * Push one edge row: endpoints, weight, direction, then the attribute cells.
754
1138
  * @param plan - the edge plan
755
1139
  * @param row - the cells
756
- * @param quoted - whether each cell was quoted
1140
+ * @param cellQuoted - whether each cell was quoted
757
1141
  * @param line - the row's line
758
1142
  */
759
- processEdgeRow(plan, row, quoted, line) {
760
- const { report, sink, resolver, common } = this.state;
1143
+ processEdgeRow(plan, row, cellQuoted, line) {
1144
+ const { report } = this.state;
761
1145
  const { counts } = report;
1146
+ const quoted = completeRow(plan, row, cellQuoted, report, line);
762
1147
  if (row.length !== plan.width) {
763
- report.error("validation-error", FIELD_COUNT_CODE, `line ${line}: ${row.length} field(s), the header has ${plan.width}`, { line });
1148
+ this.reportRowWidth(plan, row, line);
764
1149
  counts.skippedEdges++;
765
1150
  return;
766
1151
  }
@@ -768,49 +1153,31 @@ class TableReader {
768
1153
  const targetText = row[plan.target];
769
1154
  const sourceMissing = isUnset(sourceText, quoted[plan.source]);
770
1155
  if (sourceMissing || isUnset(targetText, quoted[plan.target])) {
771
- report.error("missing-value", MISSING_ENDPOINT_CODE, `line ${line}: blank ${sourceMissing ? "source" : "target"} cell`, { line });
1156
+ report.error("missing-value", MISSING_ENDPOINT_CODE, `blank ${sourceMissing ? "source" : "target"} cell`, {
1157
+ line,
1158
+ });
772
1159
  counts.skippedEdges++;
773
1160
  return;
774
1161
  }
775
- let kind = (this.state.commentDirected ?? common.defaultDirected) ? "directed" : "undirected";
776
- if (plan.type >= 0) {
777
- const parsed = parseKind(row[plan.type]);
778
- if (parsed === null) {
779
- report.error("validation-error", BAD_TYPE_CODE, `line ${line}: Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`, { line });
780
- counts.skippedEdges++;
781
- return;
782
- }
783
- if (parsed !== undefined) {
784
- kind = parsed;
785
- }
1162
+ const kind = this.rowKind(plan, row, line);
1163
+ if (kind === null) {
1164
+ counts.skippedEdges++;
1165
+ return;
786
1166
  }
787
1167
  const { where } = this;
788
1168
  where.line = line;
789
1169
  where.element = null;
790
1170
  const idText = plan.id >= 0 && !isUnset(row[plan.id], quoted[plan.id]) ? row[plan.id] : null;
791
- if (idText !== null) {
792
- if (this.edgeIds.has(idText)) {
793
- report.error("validation-error", DUPLICATE_EDGE_ID_CODE, `line ${line}: edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`, { line, element: idText });
794
- counts.skippedEdges++;
795
- return;
796
- }
797
- this.edgeIds.add(idText);
1171
+ if (idText !== null && this.edgeIds.has(idText)) {
1172
+ report.error("validation-error", DUPLICATE_EDGE_ID_CODE, `edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`, { line, element: idText });
1173
+ counts.skippedEdges++;
1174
+ return;
798
1175
  }
1176
+ this.checkPadded(sourceText, quoted[plan.source], line);
1177
+ this.checkPadded(targetText, quoted[plan.target], line);
799
1178
  let edge;
800
1179
  try {
801
- const source = this.coerce(sourceText);
802
- const target = this.coerce(targetText);
803
- const weight = plan.weight >= 0 ? parseWeightText(row[plan.weight]) : undefined;
804
- if (!this.state.headerSet) {
805
- this.state.headerSet = true;
806
- resolver.setHeader(kind !== "undirected", where);
807
- }
808
- const sourceNew = sink.indexOf(source) === INVALID_INDEX;
809
- const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
810
- const before = sink.edgeCount;
811
- edge = resolver.addEdge(source, target, kind, weight, where);
812
- counts.edges += sink.edgeCount - before;
813
- counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
1180
+ edge = this.addEdge(sourceText, targetText, kind, plan.weight >= 0 ? row[plan.weight] : null);
814
1181
  }
815
1182
  catch (err) {
816
1183
  where.element = idText ?? `${sourceText}->${targetText}`;
@@ -819,35 +1186,116 @@ class TableReader {
819
1186
  return;
820
1187
  }
821
1188
  if (idText !== null) {
1189
+ // only an imported edge holds its id: a skipped row leaves it free
1190
+ this.edgeIds.add(idText);
822
1191
  this.writeRole(this.idHandle, "edge", edge, idText, plan.names[plan.id], line);
823
1192
  }
1193
+ this.warnTypeHint(plan, row, line);
824
1194
  if (plan.label >= 0 && !isUnset(row[plan.label], quoted[plan.label])) {
825
1195
  this.writeRole(this.labelHandle, "edge", edge, row[plan.label], plan.names[plan.label], line);
826
1196
  }
827
1197
  this.writeAttributes(plan, row, quoted, edge, line);
828
1198
  }
1199
+ /**
1200
+ * Warn, once, when a column that is not the type column holds a direction word.
1201
+ * @param plan - the edge plan
1202
+ * @param row - the cells
1203
+ * @param line - the row's line
1204
+ */
1205
+ warnTypeHint(plan, row, line) {
1206
+ if (plan.typeHint < 0 || typeof parseKind(row[plan.typeHint]) !== "string") {
1207
+ return;
1208
+ }
1209
+ this.state.report.warnOnce("unsupported", TYPE_COLUMN_IGNORED_CODE, `column ${JSON.stringify(plan.names[plan.typeHint])} holds the direction word ${JSON.stringify(row[plan.typeHint])} but is a plain attribute; the per-row direction is read from an exact "Type" column beside "Source" and "Target", or from the typeColumn option`, { line, element: plan.names[plan.typeHint] });
1210
+ }
1211
+ /**
1212
+ * Report a row with the wrong number of cells. The first row too long for an auto table says
1213
+ * what an adjacency list needs, since one reads this way.
1214
+ * @param plan - the edge plan
1215
+ * @param row - the cells
1216
+ * @param line - the row's line
1217
+ */
1218
+ reportRowWidth(plan, row, line) {
1219
+ const hint = this.state.csv.table === "auto" && row.length > plan.width && !this.adjacencyHinted
1220
+ ? '; if each line is a node followed by its neighbors, pass table: "adjacency"'
1221
+ : "";
1222
+ this.adjacencyHinted || (this.adjacencyHinted = hint !== "");
1223
+ this.state.report.error("validation-error", FIELD_COUNT_CODE, `${row.length} field${plural(row.length)}, expected ${plan.width}${hint}`, { line });
1224
+ }
1225
+ /**
1226
+ * The direction of an edge row: its Type cell, else the file's or the default direction.
1227
+ * @param plan - the edge plan
1228
+ * @param row - the cells
1229
+ * @param line - the row's line
1230
+ * @returns the kind, or null when the Type cell is not a direction (reported)
1231
+ */
1232
+ rowKind(plan, row, line) {
1233
+ const directed = this.state.commentDirected ?? this.state.common.defaultDirected;
1234
+ const kind = directed ? "directed" : "undirected";
1235
+ if (plan.type < 0) {
1236
+ return kind;
1237
+ }
1238
+ const parsed = parseKind(row[plan.type]);
1239
+ if (parsed === null) {
1240
+ this.state.report.error("validation-error", BAD_TYPE_CODE, `Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`, { line });
1241
+ return null;
1242
+ }
1243
+ return parsed ?? kind;
1244
+ }
1245
+ /**
1246
+ * Add an edge row's edge, counting the edge and the nodes it brings in.
1247
+ * @param sourceText - the source cell
1248
+ * @param targetText - the target cell
1249
+ * @param kind - the direction
1250
+ * @param weightText - the weight cell, or null
1251
+ * @returns the edge's index
1252
+ */
1253
+ addEdge(sourceText, targetText, kind, weightText) {
1254
+ const { report, sink, resolver } = this.state;
1255
+ const { where } = this;
1256
+ const source = this.coerce(sourceText);
1257
+ const target = this.coerce(targetText);
1258
+ const weight = weightText === null ? undefined : parseWeightText(weightText, report);
1259
+ if (!this.state.headerSet) {
1260
+ this.state.headerSet = true;
1261
+ resolver.setHeader(kind !== "undirected", where);
1262
+ }
1263
+ const sourceNew = sink.indexOf(source) === INVALID_INDEX;
1264
+ const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
1265
+ const before = sink.edgeCount;
1266
+ const edge = resolver.addEdge(source, target, kind, weight, where);
1267
+ report.counts.edges += sink.edgeCount - before;
1268
+ report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
1269
+ return edge;
1270
+ }
829
1271
  /**
830
1272
  * Push one node row: the id, then the label and attribute cells.
831
1273
  * @param plan - the node plan
832
1274
  * @param row - the cells
833
- * @param quoted - whether each cell was quoted
1275
+ * @param cellQuoted - whether each cell was quoted
834
1276
  * @param line - the row's line
835
1277
  */
836
- processNodeRow(plan, row, quoted, line) {
1278
+ processNodeRow(plan, row, cellQuoted, line) {
837
1279
  const { report, sink } = this.state;
838
1280
  const { counts } = report;
839
1281
  const ordinal = this.nodeOrdinal++;
1282
+ const quoted = completeRow(plan, row, cellQuoted, report, line);
840
1283
  if (row.length !== plan.width) {
841
- report.error("validation-error", FIELD_COUNT_CODE, `line ${line}: ${row.length} field(s), the header has ${plan.width}`, { line });
1284
+ report.error("validation-error", FIELD_COUNT_CODE, `${row.length} field${plural(row.length)}, expected ${plan.width}`, {
1285
+ line,
1286
+ });
842
1287
  counts.skippedNodes++;
843
1288
  return;
844
1289
  }
845
1290
  const idText = plan.id >= 0 ? row[plan.id] : String(ordinal);
846
1291
  if (plan.id >= 0 && isUnset(idText, quoted[plan.id])) {
847
- report.error("missing-value", MISSING_ID_CODE, `line ${line}: blank id cell`, { line });
1292
+ report.error("missing-value", MISSING_ID_CODE, `blank id cell`, { line });
848
1293
  counts.skippedNodes++;
849
1294
  return;
850
1295
  }
1296
+ if (plan.id >= 0) {
1297
+ this.checkPadded(idText, quoted[plan.id], line);
1298
+ }
851
1299
  const { where } = this;
852
1300
  where.line = line;
853
1301
  where.element = idText;
@@ -855,7 +1303,7 @@ class TableReader {
855
1303
  try {
856
1304
  const id = plan.id >= 0 || plan.rowNumber ? this.coerce(idText) : ordinal;
857
1305
  if (sink.indexOf(id) !== INVALID_INDEX) {
858
- report.warning("merged", DUPLICATE_NODE_CODE, `line ${line}: node ${JSON.stringify(id)} already exists; its attributes are overwritten`, where);
1306
+ report.warning("merged", DUPLICATE_NODE_CODE, `node ${JSON.stringify(id)} already exists; its attributes are overwritten`, where);
859
1307
  }
860
1308
  else {
861
1309
  counts.nodes++;
@@ -872,6 +1320,18 @@ class TableReader {
872
1320
  }
873
1321
  this.writeAttributes(plan, row, quoted, index, line);
874
1322
  }
1323
+ /**
1324
+ * Warn once per import about an unquoted id cell with leading or trailing whitespace, which is
1325
+ * a different id from the bare text (RFC 4180 keeps spaces).
1326
+ * @param text - the cell text
1327
+ * @param quoted - whether the cell was quoted
1328
+ * @param line - the row's line
1329
+ */
1330
+ checkPadded(text, quoted, line) {
1331
+ if (isPadded(text, quoted)) {
1332
+ this.state.report.warnOnce("validation-error", PADDED_ID_CODE, `id ${JSON.stringify(text)} has leading or trailing whitespace and is kept as written, a different id from the trimmed text`, { line, element: text });
1333
+ }
1334
+ }
875
1335
  /**
876
1336
  * Coerce an id cell, reporting a merge under `ids: "number"`.
877
1337
  * @param text - the cell text
@@ -935,36 +1395,176 @@ class TableReader {
935
1395
  }
936
1396
  }
937
1397
  }
1398
+ /**
1399
+ * Whether a headerless table is a triple list whose third column is text (`a b knows`): every row
1400
+ * read so far has exactly three cells and a third cell that is not a number. That column is then an
1401
+ * attribute rather than the positional weight. A wider or ragged table keeps the positional weight,
1402
+ * so prose or an adjacency list read as edges still fails per row instead of importing.
1403
+ * @param width - the number of columns
1404
+ * @param sample - the data rows read so far
1405
+ * @returns true for a text triple list
1406
+ */
1407
+ function isTextTriple(width, sample) {
1408
+ return (width === 3 &&
1409
+ sample.length > 0 &&
1410
+ sample.every((row) => row.length === 3 && !isBlank(row[2]) && numberOrNull(row[2]) === null));
1411
+ }
1412
+ /**
1413
+ * Whether the caller fixed how the input is read (a delimiter, the header, the table kind or a
1414
+ * column): it is then a table, and its first line is never refused as another format.
1415
+ * @param csv - the resolved options
1416
+ * @returns true when any of them is explicit
1417
+ */
1418
+ function hasExplicitDialect(csv) {
1419
+ return (csv.delimiter !== null ||
1420
+ csv.header !== "auto" ||
1421
+ csv.table !== "auto" ||
1422
+ csv.sourceColumn !== null ||
1423
+ csv.targetColumn !== null ||
1424
+ csv.idColumn !== null ||
1425
+ (csv.typeColumn !== undefined && csv.typeColumn !== null));
1426
+ }
1427
+ /**
1428
+ * Fit a row whose width differs from the header's by one empty last cell: a row that omits the
1429
+ * empty last cell of a header ending in a delimiter gets an unset cell appended, and a row ending
1430
+ * in a delimiter the header does not have (Excel, pandas) loses that empty cell, reported once.
1431
+ * @param plan - the plan
1432
+ * @param row - the cells (changed in place)
1433
+ * @param quoted - whether each cell was quoted
1434
+ * @param report - the report
1435
+ * @param line - the row's line
1436
+ * @returns the quoted flags of the fitted row
1437
+ */
1438
+ function completeRow(plan, row, quoted, report, line) {
1439
+ if (row.length === plan.width + 1 && isUnset(row[plan.width], quoted[plan.width])) {
1440
+ row.pop();
1441
+ report.warnOnce("validation-error", TRAILING_DELIMITER_CODE, `the row ends in a delimiter the header does not have; the empty last cell is dropped`, { line });
1442
+ return quoted;
1443
+ }
1444
+ if (!plan.optionalLast || row.length !== plan.width - 1) {
1445
+ return quoted;
1446
+ }
1447
+ const flags = quoted.slice(0, row.length);
1448
+ row.push("");
1449
+ flags.push(false);
1450
+ return flags;
1451
+ }
938
1452
  const HEAD_BYTES = 4096;
939
- const OTHER_FORMAT = /^\s*(<|[[{]|(strict\s+)?(di)?graph(\s+\S+)?\s*\{|\*vertices|creator\b|graph\s*\[)/i;
940
1453
  /**
941
- * Sniff confidence for the registry: 0 for XML, JSON, GML, DOT and Pajek openings; otherwise a
942
- * delimited first row with endpoint headers is 0.9, with an id header 0.6, any consistently
943
- * delimited rows 0.3, a single column 0.
1454
+ * The openings of the formats a CSV reader would otherwise split into rows of text: an XML / HTML
1455
+ * tag or declaration (not an IRI such as `<http://a>`, nor a bracketed id such as `<alice smith>`
1456
+ * or `<a>,b`: a tag needs an attribute, or another tag after it), a JSON object (`{"` or `{}`) or
1457
+ * array, a DOT graph, a Pajek section, GML's `Creator "..."` or `graph [`. Matched against the
1458
+ * start of the input.
1459
+ */
1460
+ /**
1461
+ * An endpoint column: the one the option names, else (with a header) the first with a known
1462
+ * endpoint name, else (without one) the column at its position in a headerless edge list.
1463
+ * @param names - the column names
1464
+ * @param header - whether the file has a header row
1465
+ * @param option - the sourceColumn / targetColumn option, or null
1466
+ * @param optionName - the option's name, for its error
1467
+ * @param known - the endpoint names to look for
1468
+ * @param position - the column of this endpoint in a headerless edge list
1469
+ * @returns the column index, or -1
1470
+ */
1471
+ function endpointColumn(names, header, option, optionName, known, position) {
1472
+ if (option !== null) {
1473
+ return resolveColumnRef(names, option, optionName);
1474
+ }
1475
+ if (header) {
1476
+ return findColumn(names, known);
1477
+ }
1478
+ return names.length >= 2 ? position : -1;
1479
+ }
1480
+ const TAG_NAME = String.raw `<[a-z_][\w.-]*(?::[a-z_][\w.-]*)?`;
1481
+ const OTHER_FORMATS = [
1482
+ /^\s*<(?:\?xml|!doctype|!--)/i, // an XML declaration, a doctype or a comment
1483
+ new RegExp(String.raw `^\s*${TAG_NAME}\s*\/?>\s*(?:[^,;|\s]*<|$)`, "i"), // a tag, then another tag
1484
+ new RegExp(String.raw `^\s*${TAG_NAME}\s+[a-z_][\w.:-]*\s*=`, "i"), // a tag with an attribute
1485
+ /^\s*\/[*/]/, // a C comment (DOT)
1486
+ /^\s*\{\s*(?:["}]|$)/, // a JSON object
1487
+ /^\s*\[\s*(?:[[{"\]\d-]|$)/, // a JSON array
1488
+ /^\s*(?:strict\s+)?(?:di)?graph(?:\s+\S+)?\s*\{/i, // a DOT graph
1489
+ /^\s*\*(?:vertices|network|arcs|edges)\b/i, // a Pajek section
1490
+ /^\s*(?:creator\s+"|graph\s*\[)/i, // GML
1491
+ ];
1492
+ /**
1493
+ * Sniff confidence for the registry: 0 for XML, JSON, GML, DOT (also a leading C comment) and Pajek
1494
+ * openings after the leading `#` / `%` comment lines the reader skips, and 0 for a head that is
1495
+ * only such comments (the comments of another format, too long to see past); otherwise a
1496
+ * delimited first row with endpoint headers is 0.9 (even when a later row has the wrong width, which
1497
+ * the importer reports), with an id header 0.6, and other consistently delimited rows 0.3, except
1498
+ * rows of more than three space-separated words, which is plain text such as a sentence, not a
1499
+ * headerless edge list (source, target, weight), so it is 0.
944
1500
  * @param head - the first bytes of the input
945
1501
  * @returns a confidence in 0..1
946
1502
  */
947
1503
  function sniff(head) {
948
- const text = new TextDecoder("utf-8").decode(head.subarray(0, HEAD_BYTES));
949
- const body = text.startsWith(String.fromCharCode(0xfeff)) ? text.slice(1) : text;
950
- if (body.trim().length === 0 || OTHER_FORMAT.test(body)) {
1504
+ let label = "utf-8";
1505
+ if (head[0] === 0xff && head[1] === 0xfe) {
1506
+ label = "utf-16le";
1507
+ }
1508
+ else if (head[0] === 0xfe && head[1] === 0xff) {
1509
+ label = "utf-16be";
1510
+ }
1511
+ // the decoder drops the BOM it was chosen by; the reader skips leading comment lines
1512
+ const body = stripLeadingComments(new TextDecoder(label).decode(head.subarray(0, HEAD_BYTES)), COMMENT_CHARS);
1513
+ if (body.trim().length === 0 || OTHER_FORMATS.some((r) => r.test(body))) {
951
1514
  return 0;
952
1515
  }
953
1516
  const newline = sniffNewline(body);
954
- const delimiter = sniffDelimiter(body, newline);
1517
+ const delimiter = sniffDelimiter(body, newline, DELIMITER_CANDIDATES, '"', false, true);
955
1518
  if (delimiter === null) {
956
- return 0;
1519
+ // a header naming both endpoints is evidence enough; a short or long row later is the
1520
+ // importer's to report, not a reason to refuse the file
1521
+ const first = body.split(newline, 1)[0];
1522
+ return DELIMITER_CANDIDATES.some((d) => hasEndpointHeader(first, d)) ? 0.9 : 0;
957
1523
  }
958
- const end = body.indexOf(newline);
959
- const firstLine = end < 0 ? body : body.slice(0, end);
960
- const names = headerNames(firstLine.replace(/\r$/, "").split(delimiter));
1524
+ // the first record, honoring quotes (R and pandas quote every header name)
1525
+ const records = splitRecords(body, delimiter, '"', 2, false, true) ?? [];
1526
+ const names = headerNames(records[0] ?? []);
961
1527
  if (findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
962
1528
  return 0.9;
963
1529
  }
964
- if (findColumn(names, ID_NAMES) >= 0) {
1530
+ // a space-split row naming an id is as likely a line of text ("1 First node") as a header
1531
+ if (delimiter !== " " && findColumn(names, ID_NAMES) >= 0) {
965
1532
  return 0.6;
966
1533
  }
967
- return 0.3;
1534
+ if (delimiter === " " && names.length > 3) {
1535
+ return 0;
1536
+ }
1537
+ return looksLikeProse(body, newline, delimiter) ? 0 : 0.3;
1538
+ }
1539
+ /** A cell that reads as part of a sentence: words with a sentence end after a letter (`Monday.`, `world. Thanks`). */
1540
+ const SENTENCE = /[A-Za-z]{2}[.!?](?:\s|$)/;
1541
+ /**
1542
+ * Whether headerless delimited text is more likely a line of prose than an edge list: a single line
1543
+ * ("hello world", "Dear team, the meeting is on Monday."), or a cell of more than one word that
1544
+ * ends a sentence. An edge list a reader means to load has more than one row and ids, not sentences;
1545
+ * such input is still read as CSV when the caller names the format.
1546
+ * @param body - the head, decoded, without its leading comments
1547
+ * @param newline - the head's newline
1548
+ * @param delimiter - the sniffed delimiter
1549
+ * @returns true for prose
1550
+ */
1551
+ function looksLikeProse(body, newline, delimiter) {
1552
+ const lines = body.split(newline).filter((l) => l.trim() !== "");
1553
+ if (lines.length < 2) {
1554
+ return true;
1555
+ }
1556
+ const rows = splitRecords(body, delimiter, '"', 8, false, true) ?? [];
1557
+ return rows.some((row) => row.some((cell) => /\S\s+\S/.test(cell.trim()) && SENTENCE.test(cell.trim())));
1558
+ }
1559
+ /**
1560
+ * Whether one line, split on a delimiter, names a source and a target column.
1561
+ * @param line - the first line of the input
1562
+ * @param delimiter - the delimiter to split on
1563
+ * @returns true for an edge table header
1564
+ */
1565
+ function hasEndpointHeader(line, delimiter) {
1566
+ const names = headerNames(splitRecords(line, delimiter, '"', 1, false, true)?.[0] ?? []);
1567
+ return names.length > 1 && findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0;
968
1568
  }
969
1569
  /**
970
1570
  * Import a CSV edge table (and an optional node table) into a sink.
@@ -989,23 +1589,54 @@ async function importCsv(input, sink, options) {
989
1589
  resolver: new DirectionResolver(sink, report, common.onMixedDirection),
990
1590
  headerSet: false,
991
1591
  commentDirected: null,
1592
+ commentText: "",
992
1593
  };
993
1594
  if (csv.nodes !== null) {
994
1595
  await new TableReader(state, csv.nodes, "nodes", false).read();
995
1596
  }
996
1597
  await new TableReader(state, input, csv.table, true).read();
997
1598
  if (state.coercer.mergeCount > 1) {
998
- report.warning("coercion", ID_MERGED_CODE, `${state.coercer.mergeCount} id cell(s) merged into ids other cells already produced under ids: "number"`);
1599
+ report.warning("coercion", ID_MERGED_CODE, `${state.coercer.mergeCount} id cell${plural(state.coercer.mergeCount)} merged into ids other cells already produced under ids: "number"`);
999
1600
  }
1000
1601
  throwIfAborted(common.signal);
1001
1602
  return report.finish();
1002
1603
  }
1003
- /** The CSV / TSV importer plugin (subpath `@graphty/graph-io/csv`). */
1604
+ /**
1605
+ * The CSV / TSV importer plugin (subpath `@graphty/graph-io/csv`).
1606
+ * @category Built-in formats
1607
+ */
1004
1608
  export const csvImporter = Object.freeze({
1005
1609
  format: "csv",
1610
+ options: Object.freeze([
1611
+ "decimal",
1612
+ "delimiter",
1613
+ "header",
1614
+ "idColumn",
1615
+ "labelColumn",
1616
+ "nodes",
1617
+ "rowNumberIds",
1618
+ "sourceColumn",
1619
+ "table",
1620
+ "targetColumn",
1621
+ "typeColumn",
1622
+ ]),
1006
1623
  extensions: Object.freeze([".csv", ".tsv", ".edges", ".edgelist"]),
1007
1624
  mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values", "text/plain"]),
1008
1625
  sniff,
1009
1626
  import: importCsv,
1010
1627
  });
1628
+ /**
1629
+ * The `nodes` option as an input: a Blob or File is read through its stream. Checked by shape, so a
1630
+ * Blob from another realm or from Node's `fs.openAsBlob()` counts too.
1631
+ * @param value - the option value
1632
+ * @returns the input, or null when there is no node table
1633
+ */
1634
+ function nodesInput(value) {
1635
+ if (value === undefined) {
1636
+ return null;
1637
+ }
1638
+ const object = value;
1639
+ const blob = typeof value === "object" && typeof object.stream === "function" && typeof object.size === "number";
1640
+ return blob ? value.stream() : value;
1641
+ }
1011
1642
  //# sourceMappingURL=importer.js.map