@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
package/dist/csv.js CHANGED
@@ -1,11 +1,9 @@
1
- import { aV as DICT_SAMPLE_ROWS, _ as DictHeuristic, R as ROLE_TAKEN_CODE$1, I as ID_MERGED_CODE$1, c as DUPLICATE_EDGE_ID_CODE$1, d as DUPLICATE_NODE_CODE$1, f as MISSING_ID_CODE$1, e as MISSING_ENDPOINT_CODE$1, u as EMPTY_INPUT_CODE$1, v as resolveImportOptions, w as ImportReportBuilder, F as reportSinkOptions, z as reportUnusedOptions, J as DirectionResolver, K as IdCoercer, y as throwIfAborted, Q as uniqueColumnName, at as RENAMED_CODE, L as declareResolved, V as resolveExportOptions, X as pairFolding, aT as joinListText, ax as canonicalId, af as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, a as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, O as OPTION_IGNORED_CODE, C as COLUMN_RENAMED_CODE, q as UNKNOWN_ENCODING_CODE, E as ENCODING_FALLBACK_CODE, r as INVALID_ENCODING_CODE, s as INVALID_UTF8_CODE } from "./chunks/report-B1z4WT9e.js";
2
- import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-Dr0Ifpag.js";
1
+ import { b as plural, bV as DICT_SAMPLE_ROWS, ac as DictHeuristic, R as ROLE_TAKEN_CODE$1, I as ID_MERGED_CODE$1, m as DUPLICATE_EDGE_ID_CODE$1, n as DUPLICATE_NODE_CODE$1, t as MISSING_ID_CODE$1, s as MISSING_ENDPOINT_CODE$1, E as EMPTY_INPUT_CODE$1, al as resolveImportOptions, a8 as ImportReportBuilder, ao as reportSinkOptions, an as reportUnusedOptions, aq as DirectionResolver, ar as IdCoercer, am as throwIfAborted, O as OPTION_IGNORED_CODE, aL as parseWeightText, a0 as uniqueColumnName, ay as RENAMED_CODE, a1 as declareResolved, c as capabilities, L as LOSS, a as checkCapabilities, r as resolveExportOptions, p as pairFolding, bE as countMixedEdges, $ as agree, e as explicitWeights, b$ as otherFormatDialect, bT as joinListText, aG as formatInteger, aF as formatDecimal, aH as canonicalId, M as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, d as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, C as COLUMN_RENAMED_CODE, w as UNKNOWN_ENCODING_CODE, x as ENCODING_FALLBACK_CODE, y as INVALID_ENCODING_CODE, z as INVALID_UTF8_CODE, P as PRECISION_CODE, F as ELEMENT_ISSUE, G as INPUT_ISSUE } from "./chunks/export-BTI4yQgy.js";
2
+ import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-DmcUs5_Q.js";
3
3
  import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
4
- import { p as parseWeightText, e as explicitWeights, d as formatInteger, b as formatDecimal } from "./chunks/weights-Dzba96G3.js";
5
- import { s as sniffNewline, a as sniffDelimiter, C as CsvRecordReader, D as DELIMITER_CANDIDATES, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-dbRkxwaq.js";
6
- import { m as quoteCsvCell } from "./chunks/escape-B-tkk_Cl.js";
7
- import { c as capabilities, L as LOSS, a as checkCapabilities, d as countMixedEdges } from "./chunks/export-Bh60Dv-n.js";
8
- import { j as joinText, e as encodeChunks } from "./chunks/writer-C7flM-Ih.js";
4
+ import { s as stripLeadingComments, a as sniffNewline, b as sniffDelimiter, D as DELIMITER_CANDIDATES, c as checkRecordSyntax, C as CsvRecordReader, d as splitRecords, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-CQXT__mY.js";
5
+ import { o as quoteCsvCell } from "./chunks/escape-D0g6NNUy.js";
6
+ import { j as joinText, e as encodeChunks } from "./chunks/writer-DxjgHwSe.js";
9
7
  const SOURCE_NAMES = Object.freeze([
10
8
  "source",
11
9
  "src",
@@ -61,7 +59,7 @@ function resolveColumnRef(names, ref, option) {
61
59
  if (!Number.isInteger(ref) || ref < 0 || ref >= names.length) {
62
60
  throw new GraphFormatError(
63
61
  "E_UNSUPPORTED",
64
- `option ${option}: column ${ref} does not exist (the file has ${names.length} column(s))`,
62
+ `option ${option}: column ${ref} does not exist (the file has ${names.length} column${plural(names.length)})`,
65
63
  { option, found: ref, columns: names.length }
66
64
  );
67
65
  }
@@ -79,15 +77,24 @@ function resolveColumnRef(names, ref, option) {
79
77
  }
80
78
  function looksLikeHeader(first, second) {
81
79
  let allText = true;
80
+ let marker = false;
82
81
  for (const cell of first) {
83
82
  const text = cell.trim();
84
83
  if (HEADER_MARKERS.has(text.toLowerCase())) {
85
- return true;
84
+ marker = true;
86
85
  }
87
86
  if (text.length === 0 || inferTextDtype(text) !== "string") {
88
87
  allText = false;
89
88
  }
90
89
  }
90
+ if (marker) {
91
+ const names = headerNames(first);
92
+ if (second === null || findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
93
+ return true;
94
+ }
95
+ const chained = first.length === 2 && second.length >= 1 && first[1].trim() === second[0].trim();
96
+ return !chained || first[1].trim().length === 0;
97
+ }
91
98
  if (!allText || second === null) {
92
99
  return false;
93
100
  }
@@ -280,6 +287,17 @@ const ID_MERGED_CODE = ID_MERGED_CODE$1;
280
287
  const COLUMN_MISSING_CODE = "W_CSV_COLUMN_MISSING";
281
288
  const ROLE_TAKEN_CODE = ROLE_TAKEN_CODE$1;
282
289
  const DUPLICATE_EDGE_ID_CODE = DUPLICATE_EDGE_ID_CODE$1;
290
+ const OTHER_FORMAT_CODE = "E_CSV_OTHER_FORMAT";
291
+ const STRAY_QUOTE_CODE = "W_CSV_STRAY_QUOTE";
292
+ const COMMENT_DIRECTION_CODE = "W_CSV_COMMENT_DIRECTION";
293
+ const TYPE_COLUMN_IGNORED_CODE = "W_CSV_TYPE_COLUMN_IGNORED";
294
+ const TRAILING_HEADER_DELIMITER_CODE = "W_CSV_TRAILING_HEADER_DELIMITER";
295
+ const SINGLE_COLUMN_CODE = "W_CSV_SINGLE_COLUMN";
296
+ const AMBIGUOUS_COLUMN_CODE = "W_CSV_AMBIGUOUS_COLUMN";
297
+ const PADDED_ID_CODE = "W_CSV_PADDED_ID";
298
+ const COMMENT_LIKE_RECORD_CODE = "W_CSV_COMMENT_LIKE_RECORD";
299
+ const TRAILING_DELIMITER_CODE = "W_CSV_TRAILING_DELIMITER";
300
+ const WEIGHT_AS_ATTRIBUTE_CODE = "W_CSV_WEIGHT_AS_ATTRIBUTE";
283
301
  const TABLE_MODES = /* @__PURE__ */ new Set(["edges", "nodes", "adjacency", "auto"]);
284
302
  const USED_OPTIONS = /* @__PURE__ */ new Set([
285
303
  "ids",
@@ -298,20 +316,24 @@ const USED_OPTIONS_WITH_NODES = /* @__PURE__ */ new Set([
298
316
  ...USED_OPTIONS,
299
317
  "nodeIdFrom"
300
318
  ]);
301
- const BAD_DELIMITERS = /* @__PURE__ */ new Set(['"', "\n", "\r"]);
302
319
  const COMMENT_CHARS = Object.freeze(["#", "%"]);
303
- function commentDirection(comments) {
304
- for (const comment of comments) {
305
- const text = comment.slice(1).trim().toLowerCase();
306
- if (text.startsWith("directed graph") || text.startsWith("asym")) {
307
- return true;
308
- }
309
- if (text.startsWith("undirected graph") || text.startsWith("sym") || text.startsWith("bip")) {
310
- return false;
311
- }
320
+ const DECIMAL_COMMA = /^\s*[+-]?\d+,\d+(?:[eE][+-]?\d+)?\s*$/;
321
+ function commentDirection(comment) {
322
+ const text = comment.slice(1).trim().toLowerCase();
323
+ if (/^(?:directed graph|asym)\b/.test(text)) {
324
+ return true;
325
+ }
326
+ if (/^(?:undirected graph|sym|bip)\b/.test(text)) {
327
+ return false;
312
328
  }
313
329
  return null;
314
330
  }
331
+ function directionName(directed) {
332
+ return directed ? "directed" : "undirected";
333
+ }
334
+ function isPadded(text, quoted) {
335
+ return quoted !== true && text.trim().length !== text.length;
336
+ }
315
337
  function resolveCsvOptions(options) {
316
338
  const o = options ?? {};
317
339
  if (o.delimiter !== void 0 && (typeof o.delimiter !== "string" || o.delimiter.length === 0)) {
@@ -320,12 +342,21 @@ function resolveCsvOptions(options) {
320
342
  found: o.delimiter
321
343
  });
322
344
  }
323
- if (o.delimiter !== void 0 && BAD_DELIMITERS.has(o.delimiter)) {
324
- throw new GraphFormatError("E_UNSUPPORTED", "option delimiter: a quote or a line break cannot delimit", {
325
- option: "delimiter",
326
- found: o.delimiter
345
+ if (o.decimal !== void 0 && o.decimal !== "." && o.decimal !== ",") {
346
+ throw new GraphFormatError("E_UNSUPPORTED", 'option decimal: expected "." or ","', {
347
+ option: "decimal",
348
+ found: o.decimal
349
+ });
350
+ }
351
+ if (o.decimal === "," && o.delimiter === ",") {
352
+ throw new GraphFormatError("E_UNSUPPORTED", 'options delimiter and decimal are both ","; pass delimiter: ";"', {
353
+ option: "decimal",
354
+ found: o.decimal
327
355
  });
328
356
  }
357
+ if (o.delimiter !== void 0) {
358
+ checkRecordSyntax({ delimiter: o.delimiter, quote: '"' });
359
+ }
329
360
  if (o.header !== void 0 && o.header !== "auto" && typeof o.header !== "boolean") {
330
361
  throw new GraphFormatError("E_UNSUPPORTED", 'option header: expected true, false or "auto"', {
331
362
  option: "header",
@@ -338,14 +369,14 @@ function resolveCsvOptions(options) {
338
369
  found: o.table
339
370
  });
340
371
  }
341
- for (const name of ["sourceColumn", "targetColumn", "idColumn"]) {
372
+ for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"]) {
342
373
  checkColumnRef(name, o[name]);
343
374
  }
344
375
  if (o.typeColumn !== null) {
345
376
  checkColumnRef("typeColumn", o.typeColumn);
346
377
  }
347
378
  if (o.table === "adjacency") {
348
- for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn"]) {
379
+ for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"]) {
349
380
  if (o[name] !== void 0) {
350
381
  throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
351
382
  option: name,
@@ -368,14 +399,17 @@ function resolveCsvOptions(options) {
368
399
  }
369
400
  return {
370
401
  delimiter: o.delimiter ?? null,
402
+ decimalComma: o.decimal === ",",
371
403
  header: o.header ?? "auto",
372
404
  table: o.table ?? "auto",
373
405
  sourceColumn: o.sourceColumn ?? null,
374
406
  targetColumn: o.targetColumn ?? null,
375
407
  typeColumn: o.typeColumn,
376
408
  idColumn: o.idColumn ?? null,
377
- nodes: o.nodes ?? null,
378
- rowNumberIds: o.rowNumberIds ?? false
409
+ labelColumn: o.labelColumn ?? null,
410
+ nodes: nodesInput(o.nodes),
411
+ rowNumberIds: o.rowNumberIds ?? false,
412
+ explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null
379
413
  };
380
414
  }
381
415
  function checkColumnRef(name, value) {
@@ -477,6 +511,7 @@ class TableReader {
477
511
  this.labelHandle = INVALID_INDEX;
478
512
  this.dataRows = 0;
479
513
  this.nodeOrdinal = 0;
514
+ this.adjacencyHinted = false;
480
515
  this.edgeIds = /* @__PURE__ */ new Set();
481
516
  this.where = { line: null, element: null };
482
517
  this.state = state;
@@ -484,17 +519,25 @@ class TableReader {
484
519
  this.rowNumbers = state.csv.rowNumberIds && (kind === "nodes" || kind === "auto" && state.csv.nodes === null);
485
520
  const readerOptions = {
486
521
  delimiter: state.csv.delimiter,
522
+ // a decimal comma is never the delimiter
523
+ ...state.csv.decimalComma ? { candidates: DELIMITER_CANDIDATES.filter((d) => d !== ",") } : {},
487
524
  comments: COMMENT_CHARS,
488
525
  // field counts say nothing about an adjacency table, whose rows vary in width
489
526
  skipQuoteErrors: kind === "adjacency",
490
527
  signal: state.common.signal,
491
528
  onProgress: progress ? state.common.onProgress : null,
492
- encoding: state.common.encoding
529
+ encoding: state.common.encoding,
530
+ // an adjacency table has no header: an empty one is the empty graph
531
+ allowEmpty: kind === "adjacency",
532
+ // explicit dialect options say the caller knows the input is a table
533
+ inspect: hasExplicitDialect(state.csv) ? void 0 : (preview, line) => {
534
+ this.refuseOtherFormat(preview, line);
535
+ }
493
536
  };
494
537
  this.reader = new CsvRecordReader(input, state.report, readerOptions);
495
538
  }
496
539
  /**
497
- * Read every row; the reader is closed (and a stream cancelled) when the import aborts midway.
540
+ * Read every row; the reader is closed (and a stream canceled) when the import aborts midway.
498
541
  */
499
542
  async read() {
500
543
  const iterator = this.reader[Symbol.asyncIterator]();
@@ -510,7 +553,10 @@ class TableReader {
510
553
  */
511
554
  async readRows(iterator) {
512
555
  const { report } = this.state;
513
- const first = await iterator.next();
556
+ let first = await iterator.next();
557
+ if (!first.done && (this.reader.sepDirective || this.isSepDirective(first.value))) {
558
+ first = await iterator.next();
559
+ }
514
560
  if (first.done && this.kind === "adjacency") {
515
561
  return;
516
562
  }
@@ -530,7 +576,7 @@ class TableReader {
530
576
  const secondRow = second.done ? null : second.value;
531
577
  const secondLine = this.reader.line;
532
578
  const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
533
- header = looksLikeHeader(firstRow, secondRow);
579
+ header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
534
580
  if (!header) {
535
581
  pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
536
582
  }
@@ -543,11 +589,39 @@ class TableReader {
543
589
  pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
544
590
  }
545
591
  }
592
+ if (this.rowNumbers && !header) {
593
+ report.warning(
594
+ "unsupported",
595
+ OPTION_IGNORED_CODE,
596
+ "option rowNumberIds ignored: a headerless node table takes its ids from column 0",
597
+ { line: firstLine, element: "rowNumberIds" }
598
+ );
599
+ }
546
600
  const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
547
- if (this.kind !== "nodes" && this.state.commentDirected === null) {
548
- this.state.commentDirected = commentDirection(this.reader.leadingComments);
601
+ const last = firstRow.length - 1;
602
+ const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
603
+ this.applyCommentDirection();
604
+ this.plan = this.resolvePlan(
605
+ names,
606
+ header,
607
+ firstLine,
608
+ pending.map((p) => p.row),
609
+ optionalLast
610
+ );
611
+ if (optionalLast) {
612
+ report.warning(
613
+ "validation-error",
614
+ TRAILING_HEADER_DELIMITER_CODE,
615
+ `the header ends in a delimiter; rows without the empty last cell are read as complete`,
616
+ { line: firstLine, element: names[last] }
617
+ );
549
618
  }
550
- this.plan = this.resolvePlan(names, header, firstLine);
619
+ this.reportCommentLikeRecords(names.length);
620
+ this.reportSingleColumn(
621
+ names.length,
622
+ pending.map((p) => p.row),
623
+ firstLine
624
+ );
551
625
  this.prepareColumns(firstLine);
552
626
  for (const { row, quoted, line } of pending) {
553
627
  this.processRow(row, quoted, line);
@@ -573,37 +647,161 @@ class TableReader {
573
647
  line: firstLine
574
648
  });
575
649
  }
650
+ this.reportReaderFindings();
651
+ }
652
+ /**
653
+ * Whether a row is Excel's delimiter directive (`sep=;`) under a delimiter the caller gave (a
654
+ * sniffed one comes from the directive itself: `reader.sepDirective`).
655
+ * @param row - the first row
656
+ * @returns true for the directive
657
+ */
658
+ isSepDirective(row) {
659
+ return this.state.csv.delimiter !== null && !this.reader.quoted[0] && /^sep=.$/i.test(row.join(this.state.csv.delimiter));
660
+ }
661
+ /**
662
+ * Fail when the input opens like another format (an HTML error page, a JSON, GML, DOT or Pajek
663
+ * file handed to the CSV importer), which any delimiter would otherwise read as rows of text.
664
+ * Runs on the raw preview before any record is parsed, so a quoting error in the other
665
+ * format's syntax cannot abort first.
666
+ * @param preview - the start of the input, leading comments removed
667
+ * @param line - the line it starts on
668
+ */
669
+ refuseOtherFormat(preview, line) {
670
+ if (OTHER_FORMATS.some((r) => r.test(preview))) {
671
+ this.state.report.fail(
672
+ OTHER_FORMAT_CODE,
673
+ `the input opens like another format (XML / HTML, JSON, GML, DOT or Pajek), not CSV: ${JSON.stringify(preview.trimStart().slice(0, 60))}`,
674
+ { line }
675
+ );
676
+ }
677
+ }
678
+ /**
679
+ * Whether a column option names a cell of the first row by text.
680
+ * @param row - the first row
681
+ * @returns true when sourceColumn, targetColumn, idColumn or typeColumn names one of its cells
682
+ */
683
+ namesExplicitColumn(row) {
684
+ const { csv } = this.state;
685
+ const names = headerNames(row);
686
+ return [csv.sourceColumn, csv.targetColumn, csv.idColumn, csv.typeColumn, csv.labelColumn].some(
687
+ (ref) => typeof ref === "string" && findColumn(names, [ref]) >= 0
688
+ );
689
+ }
690
+ /**
691
+ * Take the file-level direction from the leading SNAP / KONECT comments of this table: the first
692
+ * declaring line wins; a later line declaring the other direction, and a declaration that
693
+ * overrides the caller's explicit defaultDirected, are reported.
694
+ */
695
+ applyCommentDirection() {
696
+ const { state } = this;
697
+ const { leadingComments, leadingCommentLines } = this.reader;
698
+ for (let i = 0; i < leadingComments.length; i++) {
699
+ const comment = leadingComments[i];
700
+ const directed = commentDirection(comment);
701
+ if (directed === null) {
702
+ continue;
703
+ }
704
+ const where = { line: leadingCommentLines[i], element: comment };
705
+ if (state.commentDirected === null) {
706
+ state.commentDirected = directed;
707
+ state.commentText = comment;
708
+ const explicit = state.csv.explicitDirected;
709
+ if (explicit !== null && explicit !== directed) {
710
+ state.report.warning(
711
+ "validation-error",
712
+ COMMENT_DIRECTION_CODE,
713
+ `${JSON.stringify(comment)} declares a ${directionName(directed)} graph, which overrides defaultDirected: ${explicit}`,
714
+ where
715
+ );
716
+ }
717
+ } else if (state.commentDirected !== directed) {
718
+ state.report.warning(
719
+ "validation-error",
720
+ COMMENT_DIRECTION_CODE,
721
+ `${JSON.stringify(comment)} declares a ${directionName(directed)} graph but ${JSON.stringify(state.commentText)} declared a ${directionName(state.commentDirected)} one; the first applies`,
722
+ where
723
+ );
724
+ }
725
+ }
726
+ }
727
+ /**
728
+ * Report a leading `#` / `%` line that has the fields of a record (a first id starting with `#`,
729
+ * swallowed as a comment): its text directly follows the comment character and splits into as
730
+ * many cells as the table has columns. SNAP and KONECT comment lines put a space after it.
731
+ * @param width - the number of columns
732
+ */
733
+ reportCommentLikeRecords(width) {
734
+ const { delimiter, leadingComments, leadingCommentLines } = this.reader;
735
+ if (width < 2 || delimiter === null) {
736
+ return;
737
+ }
738
+ for (let i = 0; i < leadingComments.length; i++) {
739
+ const comment = leadingComments[i];
740
+ if (comment.length > 1 && comment[1].trim().length > 0 && comment.split(delimiter).length === width) {
741
+ this.state.report.warnOnce(
742
+ "parse-error",
743
+ COMMENT_LIKE_RECORD_CODE,
744
+ `${JSON.stringify(comment)} was skipped as a leading comment but has the fields of a record; quote an id that starts with # or %`,
745
+ { line: leadingCommentLines[i], element: comment }
746
+ );
747
+ }
748
+ }
749
+ }
750
+ /**
751
+ * Report a one-column table whose cells another delimiter would split consistently (a delimiter
752
+ * option that does not match the file).
753
+ * @param width - the number of columns
754
+ * @param rows - the data rows read so far
755
+ * @param line - the first line
756
+ */
757
+ reportSingleColumn(width, rows, line) {
758
+ if (width !== 1 || rows.length === 0 || this.plan?.kind === "adjacency") {
759
+ return;
760
+ }
761
+ const candidates = DELIMITER_CANDIDATES.filter((d) => d !== this.reader.delimiter);
762
+ const other = sniffDelimiter(`${rows.map((r) => r[0]).join("\n")}
763
+ `, "\n", candidates);
764
+ if (other !== null) {
765
+ this.state.report.warning(
766
+ "parse-error",
767
+ SINGLE_COLUMN_CODE,
768
+ `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`,
769
+ { line }
770
+ );
771
+ }
772
+ }
773
+ /** Report what the record reader noticed while reading: a stray quote. */
774
+ reportReaderFindings() {
775
+ const { report } = this.state;
776
+ const { strayQuoteLine } = this.reader;
777
+ if (strayQuoteLine > 0) {
778
+ report.warnOnce(
779
+ "parse-error",
780
+ STRAY_QUOTE_CODE,
781
+ `a quote inside an unquoted field is kept as text (RFC 4180 quotes a whole field)`,
782
+ { line: strayQuoteLine }
783
+ );
784
+ }
576
785
  }
577
786
  /**
578
787
  * Decide the table's columns from its header.
579
788
  * @param names - the column names
580
789
  * @param header - whether the file has a header row
581
790
  * @param line - the header line
791
+ * @param sample - the data rows read so far
792
+ * @param optionalLast - whether a row may omit the last cell
582
793
  * @returns the plan; the import aborts when no endpoints (or id) resolve
583
794
  */
584
- resolvePlan(names, header, line) {
585
- const { csv, report } = this.state;
795
+ resolvePlan(names, header, line, sample, optionalLast) {
796
+ const { csv } = this.state;
586
797
  if (this.kind === "adjacency") {
587
798
  return { kind: "adjacency", names: [] };
588
799
  }
589
- const width = names.length;
590
800
  let source = -1;
591
801
  let target = -1;
592
802
  if (this.kind !== "nodes") {
593
- if (csv.sourceColumn !== null) {
594
- source = resolveColumnRef(names, csv.sourceColumn, "sourceColumn");
595
- } else if (header) {
596
- source = findColumn(names, SOURCE_NAMES);
597
- } else {
598
- source = width >= 2 ? 0 : -1;
599
- }
600
- if (csv.targetColumn !== null) {
601
- target = resolveColumnRef(names, csv.targetColumn, "targetColumn");
602
- } else if (header) {
603
- target = findColumn(names, TARGET_NAMES);
604
- } else {
605
- target = width >= 2 ? 1 : -1;
606
- }
803
+ source = endpointColumn(names, header, csv.sourceColumn, "sourceColumn", SOURCE_NAMES, 0);
804
+ target = endpointColumn(names, header, csv.targetColumn, "targetColumn", TARGET_NAMES, 1);
607
805
  if (source >= 0 && target >= 0 && source === target) {
608
806
  throw new GraphFormatError("E_UNSUPPORTED", "sourceColumn and targetColumn name the same column", {
609
807
  option: "targetColumn",
@@ -612,10 +810,33 @@ class TableReader {
612
810
  }
613
811
  }
614
812
  if (source >= 0 && target >= 0) {
615
- return this.edgePlan(names, header, source, target);
813
+ return this.edgePlan(names, header, source, target, line, sample, optionalLast);
616
814
  }
815
+ this.refuseWithoutEndpoints(names, header, source, target, line);
816
+ return this.nodePlan(names, header, line, optionalLast);
817
+ }
818
+ /**
819
+ * Refuse a table without both endpoint columns, unless it can be a node table: an edge table
820
+ * was asked for, a single endpoint column was found, or nothing names an id.
821
+ * @param names - the column names
822
+ * @param header - whether the file has a header row
823
+ * @param source - the source column, or -1
824
+ * @param target - the target column, or -1
825
+ * @param line - the header line
826
+ */
827
+ refuseWithoutEndpoints(names, header, source, target, line) {
828
+ const { csv, report } = this.state;
617
829
  const shown = names.map((n) => JSON.stringify(n)).join(", ");
618
- if (this.kind === "edges" || this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null)) {
830
+ if (source >= 0 || target >= 0) {
831
+ const [has, missing] = source >= 0 ? ["source", "target"] : ["target", "source"];
832
+ report.fail(
833
+ NO_ENDPOINT_COLUMNS_CODE,
834
+ `the header has a ${has} column (${JSON.stringify(names[Math.max(source, target)])}) but no ${missing} column (${shown})`,
835
+ { line },
836
+ { columns: [...names] }
837
+ );
838
+ }
839
+ if (this.kind === "edges" || this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null || csv.nodes !== null)) {
619
840
  report.fail(
620
841
  NO_ENDPOINT_COLUMNS_CODE,
621
842
  `no source / target columns in the header (${shown}); a node table goes in the nodes option`,
@@ -623,7 +844,7 @@ class TableReader {
623
844
  { columns: [...names] }
624
845
  );
625
846
  }
626
- const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : width >= 1;
847
+ const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : names.length >= 1;
627
848
  if (this.kind === "auto" && csv.idColumn === null && !idResolves && !this.rowNumbers) {
628
849
  report.fail(
629
850
  NO_ENDPOINT_COLUMNS_CODE,
@@ -632,7 +853,6 @@ class TableReader {
632
853
  { columns: [...names] }
633
854
  );
634
855
  }
635
- return this.nodePlan(names, header, line);
636
856
  }
637
857
  /**
638
858
  * The columns of an edge table.
@@ -640,50 +860,26 @@ class TableReader {
640
860
  * @param header - whether the file has a header row
641
861
  * @param source - the source column
642
862
  * @param target - the target column
863
+ * @param line - the header (or first) line
864
+ * @param sample - the data rows read so far
865
+ * @param optionalLast - whether a row may omit the last cell
643
866
  * @returns the plan
644
867
  */
645
- edgePlan(names, header, source, target) {
646
- const { csv, common, report } = this.state;
868
+ edgePlan(names, header, source, target, line, sample, optionalLast) {
869
+ const { csv } = this.state;
647
870
  const claimed = /* @__PURE__ */ new Set([source, target]);
648
- let weight = -1;
649
- if (common.weightFrom !== null) {
650
- if (header) {
651
- weight = findFree(names, [common.weightFrom], claimed);
652
- if (weight < 0 && this.state.weightFromExplicit) {
653
- report.warning(
654
- "missing-value",
655
- COLUMN_MISSING_CODE,
656
- `weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`,
657
- { line: this.reader.line, element: common.weightFrom }
658
- );
659
- }
660
- } else if (names.length >= 3 && !claimed.has(2)) {
661
- weight = 2;
662
- }
663
- }
871
+ const weight = this.weightColumn(names, header, claimed, line, sample);
664
872
  if (weight >= 0) {
665
873
  claimed.add(weight);
666
874
  }
667
- let type = -1;
668
- if (csv.typeColumn === void 0) {
669
- if (header && names[source] === "Source" && names[target] === "Target") {
670
- type = findFree(names, [TYPE_NAME], claimed);
671
- if (type >= 0 && names[type] !== TYPE_NAME) {
672
- type = -1;
673
- }
674
- }
675
- } else if (csv.typeColumn !== null) {
676
- type = resolveColumnRef(names, csv.typeColumn, "typeColumn");
677
- if (claimed.has(type)) {
678
- throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
679
- option: "typeColumn",
680
- found: names[type]
681
- });
682
- }
875
+ if (header) {
876
+ this.reportEndpointRivals(names, source, target, weight, line);
683
877
  }
878
+ const type = this.typeColumn(names, header, source, target, claimed);
684
879
  if (type >= 0) {
685
880
  claimed.add(type);
686
881
  }
882
+ const typeHint = header && type < 0 && csv.typeColumn === void 0 ? findFree(names, [TYPE_NAME], claimed) : -1;
687
883
  const id = header ? findFree(names, EDGE_ID_NAMES, claimed) : -1;
688
884
  if (id >= 0) {
689
885
  claimed.add(id);
@@ -698,56 +894,162 @@ class TableReader {
698
894
  attributes.push(i);
699
895
  }
700
896
  }
701
- return { kind: "edges", names, width: names.length, source, target, weight, type, id, label, attributes };
897
+ return {
898
+ kind: "edges",
899
+ names,
900
+ width: names.length,
901
+ source,
902
+ target,
903
+ weight,
904
+ type,
905
+ typeHint,
906
+ id,
907
+ label,
908
+ attributes,
909
+ optionalLast
910
+ };
911
+ }
912
+ /**
913
+ * The weight column of an edge table: the weightFrom column of a header (a warning when it was
914
+ * asked for and is missing), else the third column of a headerless edge list unless it holds text.
915
+ * @param names - the column names
916
+ * @param header - whether the file has a header row
917
+ * @param claimed - the endpoint columns
918
+ * @param line - the header (or first) line
919
+ * @param sample - the data rows read so far
920
+ * @returns the column index, or -1
921
+ */
922
+ weightColumn(names, header, claimed, line, sample) {
923
+ const { common, report } = this.state;
924
+ if (common.weightFrom === null) {
925
+ return -1;
926
+ }
927
+ if (header) {
928
+ const weight = findFree(names, [common.weightFrom], claimed);
929
+ if (weight < 0 && this.state.weightFromExplicit) {
930
+ report.warning(
931
+ "missing-value",
932
+ COLUMN_MISSING_CODE,
933
+ `weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`,
934
+ { line, element: common.weightFrom }
935
+ );
936
+ }
937
+ return weight;
938
+ }
939
+ if (names.length < 3 || claimed.has(2)) {
940
+ return -1;
941
+ }
942
+ if (isTextTriple(names.length, sample)) {
943
+ report.warnOnce(
944
+ "validation-error",
945
+ WEIGHT_AS_ATTRIBUTE_CODE,
946
+ `the third column holds text in the first rows, so it is read as the attribute ${names[2]}, not the weight`,
947
+ { line, element: names[2] }
948
+ );
949
+ return -1;
950
+ }
951
+ return 2;
952
+ }
953
+ /**
954
+ * Warn about header columns that name the source, target or weight beside the chosen one.
955
+ * @param names - the header names
956
+ * @param source - the source column
957
+ * @param target - the target column
958
+ * @param weight - the weight column, or -1
959
+ * @param line - the header line
960
+ */
961
+ reportEndpointRivals(names, source, target, weight, line) {
962
+ const { csv, common } = this.state;
963
+ if (csv.sourceColumn === null) {
964
+ this.reportRivals(names, SOURCE_NAMES, source, "source", line);
965
+ }
966
+ if (csv.targetColumn === null) {
967
+ this.reportRivals(names, TARGET_NAMES, target, "target", line);
968
+ }
969
+ if (weight >= 0 && common.weightFrom !== null) {
970
+ this.reportRivals(names, [common.weightFrom], weight, "weight", line);
971
+ }
972
+ }
973
+ /**
974
+ * The edge type column: the typeColumn option, else Gephi's `Type` beside `Source` and `Target`.
975
+ * @param names - the column names
976
+ * @param header - whether the file has a header row
977
+ * @param source - the source column
978
+ * @param target - the target column
979
+ * @param claimed - the columns other roles took
980
+ * @returns the column index, or -1
981
+ */
982
+ typeColumn(names, header, source, target, claimed) {
983
+ const { typeColumn } = this.state.csv;
984
+ if (typeColumn === null) {
985
+ return -1;
986
+ }
987
+ if (typeColumn === void 0) {
988
+ const gephi = header && names[source] === "Source" && names[target] === "Target";
989
+ const type2 = gephi ? findFree(names, [TYPE_NAME], claimed) : -1;
990
+ return type2 >= 0 && names[type2] === TYPE_NAME ? type2 : -1;
991
+ }
992
+ const type = resolveColumnRef(names, typeColumn, "typeColumn");
993
+ if (claimed.has(type)) {
994
+ throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
995
+ option: "typeColumn",
996
+ found: names[type]
997
+ });
998
+ }
999
+ return type;
1000
+ }
1001
+ /**
1002
+ * Warn about header columns that name a role another column was chosen for (`Source,source`,
1003
+ * `from,to,source`, `Weight,weight`): the one not chosen is read as a plain attribute.
1004
+ * @param names - the header names
1005
+ * @param candidates - the names of the role
1006
+ * @param chosen - the column chosen
1007
+ * @param role - the role, for the message
1008
+ * @param line - the header line
1009
+ */
1010
+ reportRivals(names, candidates, chosen, role, line) {
1011
+ const lower2 = new Set(candidates.map((c) => c.toLowerCase()));
1012
+ names.forEach((name, i) => {
1013
+ if (i !== chosen && lower2.has(name.toLowerCase())) {
1014
+ this.state.report.warning(
1015
+ "validation-error",
1016
+ AMBIGUOUS_COLUMN_CODE,
1017
+ `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`,
1018
+ { line, element: name }
1019
+ );
1020
+ }
1021
+ });
1022
+ }
1023
+ /**
1024
+ * The label column of a node table: the labelColumn option, else a header column named `label`.
1025
+ * @param names - the column names
1026
+ * @param header - whether the file has a header row
1027
+ * @param claimed - columns another role took
1028
+ * @returns the column index, or -1
1029
+ */
1030
+ labelOf(names, header, claimed) {
1031
+ const { labelColumn } = this.state.csv;
1032
+ if (labelColumn !== null) {
1033
+ return resolveColumnRef(names, labelColumn, "labelColumn");
1034
+ }
1035
+ return header ? findFree(names, LABEL_NAMES, claimed) : -1;
702
1036
  }
703
1037
  /**
704
1038
  * The columns of a node table.
705
1039
  * @param names - the column names
706
1040
  * @param header - whether the file has a header row
707
1041
  * @param line - the header line
1042
+ * @param optionalLast - whether a row may omit the last cell
708
1043
  * @returns the plan; the import aborts when no id column resolves
709
1044
  */
710
- nodePlan(names, header, line) {
711
- const { csv, common, report } = this.state;
1045
+ nodePlan(names, header, line, optionalLast) {
1046
+ const { common } = this.state;
712
1047
  const claimed = /* @__PURE__ */ new Set();
713
- let idColumn = -1;
714
- if (csv.idColumn !== null) {
715
- idColumn = resolveColumnRef(names, csv.idColumn, "idColumn");
716
- } else if (header) {
717
- idColumn = findColumn(names, ID_NAMES);
718
- } else if (names.length >= 1) {
719
- idColumn = 0;
720
- }
721
- const label = header ? findFree(names, LABEL_NAMES, new Set(idColumn >= 0 ? [idColumn] : [])) : -1;
722
- let id;
723
- switch (common.nodeIdFrom) {
724
- case "label":
725
- if (label < 0) {
726
- report.fail(NO_ID_COLUMN_CODE, 'nodeIdFrom is "label" but the node table has no label column', {
727
- line
728
- });
729
- }
730
- id = label;
731
- break;
732
- case "index":
733
- id = -1;
734
- break;
735
- default:
736
- if (idColumn < 0 && this.rowNumbers) {
737
- id = -1;
738
- break;
739
- }
740
- if (idColumn < 0) {
741
- report.fail(
742
- NO_ID_COLUMN_CODE,
743
- `no id column in the node table header (${names.map((n) => JSON.stringify(n)).join(", ")})`,
744
- { line },
745
- { columns: [...names] }
746
- );
747
- }
748
- id = idColumn;
749
- claimed.add(idColumn);
750
- break;
1048
+ const idColumn = this.idColumnOf(names, header);
1049
+ const label = this.labelOf(names, header, new Set(idColumn >= 0 ? [idColumn] : []));
1050
+ const id = this.nodeIdColumn(names, idColumn, label, line);
1051
+ if (id >= 0) {
1052
+ claimed.add(id);
751
1053
  }
752
1054
  if (label >= 0) {
753
1055
  claimed.add(label);
@@ -759,7 +1061,60 @@ class TableReader {
759
1061
  }
760
1062
  }
761
1063
  const rowNumber = id < 0 && common.nodeIdFrom !== "index";
762
- return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes };
1064
+ return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes, optionalLast };
1065
+ }
1066
+ /**
1067
+ * The id column of a node table: the idColumn option, else a header column with an id name,
1068
+ * else the first column of a headerless table.
1069
+ * @param names - the column names
1070
+ * @param header - whether the file has a header row
1071
+ * @returns the column index, or -1
1072
+ */
1073
+ idColumnOf(names, header) {
1074
+ const { idColumn } = this.state.csv;
1075
+ if (idColumn !== null) {
1076
+ return resolveColumnRef(names, idColumn, "idColumn");
1077
+ }
1078
+ if (header) {
1079
+ return findColumn(names, ID_NAMES);
1080
+ }
1081
+ return names.length >= 1 ? 0 : -1;
1082
+ }
1083
+ /**
1084
+ * The column node ids come from, as nodeIdFrom says: the label, none (row numbers), or the id
1085
+ * column. The import aborts when that column is missing.
1086
+ * @param names - the column names
1087
+ * @param idColumn - the id column, or -1
1088
+ * @param label - the label column, or -1
1089
+ * @param line - the header line
1090
+ * @returns the column index, or -1 for row numbers
1091
+ */
1092
+ nodeIdColumn(names, idColumn, label, line) {
1093
+ const { common, report } = this.state;
1094
+ const shown = names.map((n) => JSON.stringify(n)).join(", ");
1095
+ if (common.nodeIdFrom === "index") {
1096
+ return -1;
1097
+ }
1098
+ if (common.nodeIdFrom === "label") {
1099
+ if (label < 0) {
1100
+ report.fail(
1101
+ NO_ID_COLUMN_CODE,
1102
+ `nodeIdFrom is "label", but the node table has no column named "label" (${shown}); pass labelColumn to name the column that holds the labels`,
1103
+ { line },
1104
+ { columns: [...names] }
1105
+ );
1106
+ }
1107
+ return label;
1108
+ }
1109
+ if (idColumn < 0 && !this.rowNumbers) {
1110
+ report.fail(
1111
+ NO_ID_COLUMN_CODE,
1112
+ `no id column in the node table header (${shown})`,
1113
+ { line },
1114
+ { columns: [...names] }
1115
+ );
1116
+ }
1117
+ return idColumn;
763
1118
  }
764
1119
  /**
765
1120
  * Declare the role columns and create a writer per attribute column.
@@ -817,6 +1172,13 @@ class TableReader {
817
1172
  processRow(row, quoted, line) {
818
1173
  const plan = this.requirePlan();
819
1174
  this.dataRows++;
1175
+ if (this.state.csv.decimalComma) {
1176
+ for (let i = 0; i < row.length; i++) {
1177
+ if (DECIMAL_COMMA.test(row[i])) {
1178
+ row[i] = row[i].replace(",", ".");
1179
+ }
1180
+ }
1181
+ }
820
1182
  if (plan.kind === "edges") {
821
1183
  this.processEdgeRow(plan, row, quoted, line);
822
1184
  } else if (plan.kind === "nodes") {
@@ -826,7 +1188,7 @@ class TableReader {
826
1188
  }
827
1189
  }
828
1190
  /**
829
- * Push one adjacency row: the node, then one edge per set neighbour cell, in row order.
1191
+ * Push one adjacency row: the node, then one edge per set neighbor cell, in row order.
830
1192
  * @param row - the cells
831
1193
  * @param quoted - whether each cell was quoted
832
1194
  * @param line - the row's line
@@ -837,16 +1199,16 @@ class TableReader {
837
1199
  const { where } = this;
838
1200
  where.line = line;
839
1201
  where.element = null;
840
- let neighbours = 0;
1202
+ let neighbors = 0;
841
1203
  for (let k = 1; k < row.length; k++) {
842
1204
  if (!isUnset(row[k], quoted[k])) {
843
- neighbours++;
1205
+ neighbors++;
844
1206
  }
845
1207
  }
846
1208
  if (isUnset(row[0], quoted[0])) {
847
- report.error("missing-value", MISSING_ID_CODE, `line ${line}: blank node cell`, { line });
1209
+ report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
848
1210
  counts.skippedNodes++;
849
- counts.skippedEdges += neighbours;
1211
+ counts.skippedEdges += neighbors;
850
1212
  return;
851
1213
  }
852
1214
  const kind = this.state.commentDirected ?? common.defaultDirected ? "directed" : "undirected";
@@ -865,7 +1227,7 @@ class TableReader {
865
1227
  } catch (err) {
866
1228
  report.recordError(err, where);
867
1229
  counts.skippedNodes++;
868
- counts.skippedEdges += neighbours;
1230
+ counts.skippedEdges += neighbors;
869
1231
  return;
870
1232
  }
871
1233
  for (let k = 1; k < row.length; k++) {
@@ -891,19 +1253,15 @@ class TableReader {
891
1253
  * Push one edge row: endpoints, weight, direction, then the attribute cells.
892
1254
  * @param plan - the edge plan
893
1255
  * @param row - the cells
894
- * @param quoted - whether each cell was quoted
1256
+ * @param cellQuoted - whether each cell was quoted
895
1257
  * @param line - the row's line
896
1258
  */
897
- processEdgeRow(plan, row, quoted, line) {
898
- const { report, sink, resolver, common } = this.state;
1259
+ processEdgeRow(plan, row, cellQuoted, line) {
1260
+ const { report } = this.state;
899
1261
  const { counts } = report;
1262
+ const quoted = completeRow(plan, row, cellQuoted, report, line);
900
1263
  if (row.length !== plan.width) {
901
- report.error(
902
- "validation-error",
903
- FIELD_COUNT_CODE,
904
- `line ${line}: ${row.length} field(s), the header has ${plan.width}`,
905
- { line }
906
- );
1264
+ this.reportRowWidth(plan, row, line);
907
1265
  counts.skippedEdges++;
908
1266
  return;
909
1267
  }
@@ -911,64 +1269,36 @@ class TableReader {
911
1269
  const targetText = row[plan.target];
912
1270
  const sourceMissing = isUnset(sourceText, quoted[plan.source]);
913
1271
  if (sourceMissing || isUnset(targetText, quoted[plan.target])) {
914
- report.error(
915
- "missing-value",
916
- MISSING_ENDPOINT_CODE,
917
- `line ${line}: blank ${sourceMissing ? "source" : "target"} cell`,
918
- { line }
919
- );
1272
+ report.error("missing-value", MISSING_ENDPOINT_CODE, `blank ${sourceMissing ? "source" : "target"} cell`, {
1273
+ line
1274
+ });
920
1275
  counts.skippedEdges++;
921
1276
  return;
922
1277
  }
923
- let kind = this.state.commentDirected ?? common.defaultDirected ? "directed" : "undirected";
924
- if (plan.type >= 0) {
925
- const parsed = parseKind(row[plan.type]);
926
- if (parsed === null) {
927
- report.error(
928
- "validation-error",
929
- BAD_TYPE_CODE,
930
- `line ${line}: Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`,
931
- { line }
932
- );
933
- counts.skippedEdges++;
934
- return;
935
- }
936
- if (parsed !== void 0) {
937
- kind = parsed;
938
- }
1278
+ const kind = this.rowKind(plan, row, line);
1279
+ if (kind === null) {
1280
+ counts.skippedEdges++;
1281
+ return;
939
1282
  }
940
1283
  const { where } = this;
941
1284
  where.line = line;
942
1285
  where.element = null;
943
1286
  const idText = plan.id >= 0 && !isUnset(row[plan.id], quoted[plan.id]) ? row[plan.id] : null;
944
- if (idText !== null) {
945
- if (this.edgeIds.has(idText)) {
946
- report.error(
947
- "validation-error",
948
- DUPLICATE_EDGE_ID_CODE,
949
- `line ${line}: edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`,
950
- { line, element: idText }
951
- );
952
- counts.skippedEdges++;
953
- return;
954
- }
955
- this.edgeIds.add(idText);
1287
+ if (idText !== null && this.edgeIds.has(idText)) {
1288
+ report.error(
1289
+ "validation-error",
1290
+ DUPLICATE_EDGE_ID_CODE,
1291
+ `edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`,
1292
+ { line, element: idText }
1293
+ );
1294
+ counts.skippedEdges++;
1295
+ return;
956
1296
  }
1297
+ this.checkPadded(sourceText, quoted[plan.source], line);
1298
+ this.checkPadded(targetText, quoted[plan.target], line);
957
1299
  let edge;
958
1300
  try {
959
- const source = this.coerce(sourceText);
960
- const target = this.coerce(targetText);
961
- const weight = plan.weight >= 0 ? parseWeightText(row[plan.weight]) : void 0;
962
- if (!this.state.headerSet) {
963
- this.state.headerSet = true;
964
- resolver.setHeader(kind !== "undirected", where);
965
- }
966
- const sourceNew = sink.indexOf(source) === INVALID_INDEX;
967
- const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
968
- const before = sink.edgeCount;
969
- edge = resolver.addEdge(source, target, kind, weight, where);
970
- counts.edges += sink.edgeCount - before;
971
- counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
1301
+ edge = this.addEdge(sourceText, targetText, kind, plan.weight >= 0 ? row[plan.weight] : null);
972
1302
  } catch (err) {
973
1303
  where.element = idText ?? `${sourceText}->${targetText}`;
974
1304
  report.recordError(err, where);
@@ -976,40 +1306,133 @@ class TableReader {
976
1306
  return;
977
1307
  }
978
1308
  if (idText !== null) {
1309
+ this.edgeIds.add(idText);
979
1310
  this.writeRole(this.idHandle, "edge", edge, idText, plan.names[plan.id], line);
980
1311
  }
1312
+ this.warnTypeHint(plan, row, line);
981
1313
  if (plan.label >= 0 && !isUnset(row[plan.label], quoted[plan.label])) {
982
1314
  this.writeRole(this.labelHandle, "edge", edge, row[plan.label], plan.names[plan.label], line);
983
1315
  }
984
1316
  this.writeAttributes(plan, row, quoted, edge, line);
985
1317
  }
1318
+ /**
1319
+ * Warn, once, when a column that is not the type column holds a direction word.
1320
+ * @param plan - the edge plan
1321
+ * @param row - the cells
1322
+ * @param line - the row's line
1323
+ */
1324
+ warnTypeHint(plan, row, line) {
1325
+ if (plan.typeHint < 0 || typeof parseKind(row[plan.typeHint]) !== "string") {
1326
+ return;
1327
+ }
1328
+ this.state.report.warnOnce(
1329
+ "unsupported",
1330
+ TYPE_COLUMN_IGNORED_CODE,
1331
+ `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`,
1332
+ { line, element: plan.names[plan.typeHint] }
1333
+ );
1334
+ }
1335
+ /**
1336
+ * Report a row with the wrong number of cells. The first row too long for an auto table says
1337
+ * what an adjacency list needs, since one reads this way.
1338
+ * @param plan - the edge plan
1339
+ * @param row - the cells
1340
+ * @param line - the row's line
1341
+ */
1342
+ reportRowWidth(plan, row, line) {
1343
+ const hint = this.state.csv.table === "auto" && row.length > plan.width && !this.adjacencyHinted ? '; if each line is a node followed by its neighbors, pass table: "adjacency"' : "";
1344
+ this.adjacencyHinted ||= hint !== "";
1345
+ this.state.report.error(
1346
+ "validation-error",
1347
+ FIELD_COUNT_CODE,
1348
+ `${row.length} field${plural(row.length)}, expected ${plan.width}${hint}`,
1349
+ { line }
1350
+ );
1351
+ }
1352
+ /**
1353
+ * The direction of an edge row: its Type cell, else the file's or the default direction.
1354
+ * @param plan - the edge plan
1355
+ * @param row - the cells
1356
+ * @param line - the row's line
1357
+ * @returns the kind, or null when the Type cell is not a direction (reported)
1358
+ */
1359
+ rowKind(plan, row, line) {
1360
+ const directed = this.state.commentDirected ?? this.state.common.defaultDirected;
1361
+ const kind = directed ? "directed" : "undirected";
1362
+ if (plan.type < 0) {
1363
+ return kind;
1364
+ }
1365
+ const parsed = parseKind(row[plan.type]);
1366
+ if (parsed === null) {
1367
+ this.state.report.error(
1368
+ "validation-error",
1369
+ BAD_TYPE_CODE,
1370
+ `Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`,
1371
+ { line }
1372
+ );
1373
+ return null;
1374
+ }
1375
+ return parsed ?? kind;
1376
+ }
1377
+ /**
1378
+ * Add an edge row's edge, counting the edge and the nodes it brings in.
1379
+ * @param sourceText - the source cell
1380
+ * @param targetText - the target cell
1381
+ * @param kind - the direction
1382
+ * @param weightText - the weight cell, or null
1383
+ * @returns the edge's index
1384
+ */
1385
+ addEdge(sourceText, targetText, kind, weightText) {
1386
+ const { report, sink, resolver } = this.state;
1387
+ const { where } = this;
1388
+ const source = this.coerce(sourceText);
1389
+ const target = this.coerce(targetText);
1390
+ const weight = weightText === null ? void 0 : parseWeightText(weightText, report);
1391
+ if (!this.state.headerSet) {
1392
+ this.state.headerSet = true;
1393
+ resolver.setHeader(kind !== "undirected", where);
1394
+ }
1395
+ const sourceNew = sink.indexOf(source) === INVALID_INDEX;
1396
+ const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
1397
+ const before = sink.edgeCount;
1398
+ const edge = resolver.addEdge(source, target, kind, weight, where);
1399
+ report.counts.edges += sink.edgeCount - before;
1400
+ report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
1401
+ return edge;
1402
+ }
986
1403
  /**
987
1404
  * Push one node row: the id, then the label and attribute cells.
988
1405
  * @param plan - the node plan
989
1406
  * @param row - the cells
990
- * @param quoted - whether each cell was quoted
1407
+ * @param cellQuoted - whether each cell was quoted
991
1408
  * @param line - the row's line
992
1409
  */
993
- processNodeRow(plan, row, quoted, line) {
1410
+ processNodeRow(plan, row, cellQuoted, line) {
994
1411
  const { report, sink } = this.state;
995
1412
  const { counts } = report;
996
1413
  const ordinal = this.nodeOrdinal++;
1414
+ const quoted = completeRow(plan, row, cellQuoted, report, line);
997
1415
  if (row.length !== plan.width) {
998
1416
  report.error(
999
1417
  "validation-error",
1000
1418
  FIELD_COUNT_CODE,
1001
- `line ${line}: ${row.length} field(s), the header has ${plan.width}`,
1002
- { line }
1419
+ `${row.length} field${plural(row.length)}, expected ${plan.width}`,
1420
+ {
1421
+ line
1422
+ }
1003
1423
  );
1004
1424
  counts.skippedNodes++;
1005
1425
  return;
1006
1426
  }
1007
1427
  const idText = plan.id >= 0 ? row[plan.id] : String(ordinal);
1008
1428
  if (plan.id >= 0 && isUnset(idText, quoted[plan.id])) {
1009
- report.error("missing-value", MISSING_ID_CODE, `line ${line}: blank id cell`, { line });
1429
+ report.error("missing-value", MISSING_ID_CODE, `blank id cell`, { line });
1010
1430
  counts.skippedNodes++;
1011
1431
  return;
1012
1432
  }
1433
+ if (plan.id >= 0) {
1434
+ this.checkPadded(idText, quoted[plan.id], line);
1435
+ }
1013
1436
  const { where } = this;
1014
1437
  where.line = line;
1015
1438
  where.element = idText;
@@ -1020,7 +1443,7 @@ class TableReader {
1020
1443
  report.warning(
1021
1444
  "merged",
1022
1445
  DUPLICATE_NODE_CODE,
1023
- `line ${line}: node ${JSON.stringify(id)} already exists; its attributes are overwritten`,
1446
+ `node ${JSON.stringify(id)} already exists; its attributes are overwritten`,
1024
1447
  where
1025
1448
  );
1026
1449
  } else {
@@ -1037,6 +1460,23 @@ class TableReader {
1037
1460
  }
1038
1461
  this.writeAttributes(plan, row, quoted, index, line);
1039
1462
  }
1463
+ /**
1464
+ * Warn once per import about an unquoted id cell with leading or trailing whitespace, which is
1465
+ * a different id from the bare text (RFC 4180 keeps spaces).
1466
+ * @param text - the cell text
1467
+ * @param quoted - whether the cell was quoted
1468
+ * @param line - the row's line
1469
+ */
1470
+ checkPadded(text, quoted, line) {
1471
+ if (isPadded(text, quoted)) {
1472
+ this.state.report.warnOnce(
1473
+ "validation-error",
1474
+ PADDED_ID_CODE,
1475
+ `id ${JSON.stringify(text)} has leading or trailing whitespace and is kept as written, a different id from the trimmed text`,
1476
+ { line, element: text }
1477
+ );
1478
+ }
1479
+ }
1040
1480
  /**
1041
1481
  * Coerce an id cell, reporting a merge under `ids: "number"`.
1042
1482
  * @param text - the cell text
@@ -1102,29 +1542,104 @@ class TableReader {
1102
1542
  }
1103
1543
  }
1104
1544
  }
1545
+ function isTextTriple(width, sample) {
1546
+ return width === 3 && sample.length > 0 && sample.every((row) => row.length === 3 && !isBlank(row[2]) && numberOrNull(row[2]) === null);
1547
+ }
1548
+ function hasExplicitDialect(csv) {
1549
+ return csv.delimiter !== null || csv.header !== "auto" || csv.table !== "auto" || csv.sourceColumn !== null || csv.targetColumn !== null || csv.idColumn !== null || csv.typeColumn !== void 0 && csv.typeColumn !== null;
1550
+ }
1551
+ function completeRow(plan, row, quoted, report, line) {
1552
+ if (row.length === plan.width + 1 && isUnset(row[plan.width], quoted[plan.width])) {
1553
+ row.pop();
1554
+ report.warnOnce(
1555
+ "validation-error",
1556
+ TRAILING_DELIMITER_CODE,
1557
+ `the row ends in a delimiter the header does not have; the empty last cell is dropped`,
1558
+ { line }
1559
+ );
1560
+ return quoted;
1561
+ }
1562
+ if (!plan.optionalLast || row.length !== plan.width - 1) {
1563
+ return quoted;
1564
+ }
1565
+ const flags = quoted.slice(0, row.length);
1566
+ row.push("");
1567
+ flags.push(false);
1568
+ return flags;
1569
+ }
1105
1570
  const HEAD_BYTES = 4096;
1106
- const OTHER_FORMAT = /^\s*(<|[[{]|(strict\s+)?(di)?graph(\s+\S+)?\s*\{|\*vertices|creator\b|graph\s*\[)/i;
1571
+ function endpointColumn(names, header, option, optionName, known, position) {
1572
+ if (option !== null) {
1573
+ return resolveColumnRef(names, option, optionName);
1574
+ }
1575
+ if (header) {
1576
+ return findColumn(names, known);
1577
+ }
1578
+ return names.length >= 2 ? position : -1;
1579
+ }
1580
+ const TAG_NAME = String.raw`<[a-z_][\w.-]*(?::[a-z_][\w.-]*)?`;
1581
+ const OTHER_FORMATS = [
1582
+ /^\s*<(?:\?xml|!doctype|!--)/i,
1583
+ // an XML declaration, a doctype or a comment
1584
+ new RegExp(String.raw`^\s*${TAG_NAME}\s*\/?>\s*(?:[^,;|\s]*<|$)`, "i"),
1585
+ // a tag, then another tag
1586
+ new RegExp(String.raw`^\s*${TAG_NAME}\s+[a-z_][\w.:-]*\s*=`, "i"),
1587
+ // a tag with an attribute
1588
+ /^\s*\/[*/]/,
1589
+ // a C comment (DOT)
1590
+ /^\s*\{\s*(?:["}]|$)/,
1591
+ // a JSON object
1592
+ /^\s*\[\s*(?:[[{"\]\d-]|$)/,
1593
+ // a JSON array
1594
+ /^\s*(?:strict\s+)?(?:di)?graph(?:\s+\S+)?\s*\{/i,
1595
+ // a DOT graph
1596
+ /^\s*\*(?:vertices|network|arcs|edges)\b/i,
1597
+ // a Pajek section
1598
+ /^\s*(?:creator\s+"|graph\s*\[)/i
1599
+ // GML
1600
+ ];
1107
1601
  function sniff(head) {
1108
- const text = new TextDecoder("utf-8").decode(head.subarray(0, HEAD_BYTES));
1109
- const body = text.startsWith(String.fromCharCode(65279)) ? text.slice(1) : text;
1110
- if (body.trim().length === 0 || OTHER_FORMAT.test(body)) {
1602
+ let label = "utf-8";
1603
+ if (head[0] === 255 && head[1] === 254) {
1604
+ label = "utf-16le";
1605
+ } else if (head[0] === 254 && head[1] === 255) {
1606
+ label = "utf-16be";
1607
+ }
1608
+ const body = stripLeadingComments(new TextDecoder(label).decode(head.subarray(0, HEAD_BYTES)), COMMENT_CHARS);
1609
+ if (body.trim().length === 0 || OTHER_FORMATS.some((r) => r.test(body))) {
1111
1610
  return 0;
1112
1611
  }
1113
1612
  const newline = sniffNewline(body);
1114
- const delimiter = sniffDelimiter(body, newline);
1613
+ const delimiter = sniffDelimiter(body, newline, DELIMITER_CANDIDATES, '"', false, true);
1115
1614
  if (delimiter === null) {
1116
- return 0;
1615
+ const first = body.split(newline, 1)[0];
1616
+ return DELIMITER_CANDIDATES.some((d) => hasEndpointHeader(first, d)) ? 0.9 : 0;
1117
1617
  }
1118
- const end = body.indexOf(newline);
1119
- const firstLine = end < 0 ? body : body.slice(0, end);
1120
- const names = headerNames(firstLine.replace(/\r$/, "").split(delimiter));
1618
+ const records = splitRecords(body, delimiter, '"', 2, false, true) ?? [];
1619
+ const names = headerNames(records[0] ?? []);
1121
1620
  if (findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
1122
1621
  return 0.9;
1123
1622
  }
1124
- if (findColumn(names, ID_NAMES) >= 0) {
1623
+ if (delimiter !== " " && findColumn(names, ID_NAMES) >= 0) {
1125
1624
  return 0.6;
1126
1625
  }
1127
- return 0.3;
1626
+ if (delimiter === " " && names.length > 3) {
1627
+ return 0;
1628
+ }
1629
+ return looksLikeProse(body, newline, delimiter) ? 0 : 0.3;
1630
+ }
1631
+ const SENTENCE = /[A-Za-z]{2}[.!?](?:\s|$)/;
1632
+ function looksLikeProse(body, newline, delimiter) {
1633
+ const lines2 = body.split(newline).filter((l) => l.trim() !== "");
1634
+ if (lines2.length < 2) {
1635
+ return true;
1636
+ }
1637
+ const rows = splitRecords(body, delimiter, '"', 8, false, true) ?? [];
1638
+ return rows.some((row) => row.some((cell) => /\S\s+\S/.test(cell.trim()) && SENTENCE.test(cell.trim())));
1639
+ }
1640
+ function hasEndpointHeader(line, delimiter) {
1641
+ const names = headerNames(splitRecords(line, delimiter, '"', 1, false, true)?.[0] ?? []);
1642
+ return names.length > 1 && findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0;
1128
1643
  }
1129
1644
  async function importCsv(input, sink, options) {
1130
1645
  const common = resolveImportOptions(options, { ids: "canonical", defaultDirected: true, weightFrom: "weight" });
@@ -1145,7 +1660,8 @@ async function importCsv(input, sink, options) {
1145
1660
  coercer: new IdCoercer(common.ids),
1146
1661
  resolver: new DirectionResolver(sink, report, common.onMixedDirection),
1147
1662
  headerSet: false,
1148
- commentDirected: null
1663
+ commentDirected: null,
1664
+ commentText: ""
1149
1665
  };
1150
1666
  if (csv.nodes !== null) {
1151
1667
  await new TableReader(state, csv.nodes, "nodes", false).read();
@@ -1155,7 +1671,7 @@ async function importCsv(input, sink, options) {
1155
1671
  report.warning(
1156
1672
  "coercion",
1157
1673
  ID_MERGED_CODE,
1158
- `${state.coercer.mergeCount} id cell(s) merged into ids other cells already produced under ids: "number"`
1674
+ `${state.coercer.mergeCount} id cell${plural(state.coercer.mergeCount)} merged into ids other cells already produced under ids: "number"`
1159
1675
  );
1160
1676
  }
1161
1677
  throwIfAborted(common.signal);
@@ -1163,15 +1679,39 @@ async function importCsv(input, sink, options) {
1163
1679
  }
1164
1680
  const csvImporter = Object.freeze({
1165
1681
  format: "csv",
1682
+ options: Object.freeze([
1683
+ "decimal",
1684
+ "delimiter",
1685
+ "header",
1686
+ "idColumn",
1687
+ "labelColumn",
1688
+ "nodes",
1689
+ "rowNumberIds",
1690
+ "sourceColumn",
1691
+ "table",
1692
+ "targetColumn",
1693
+ "typeColumn"
1694
+ ]),
1166
1695
  extensions: Object.freeze([".csv", ".tsv", ".edges", ".edgelist"]),
1167
1696
  mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values", "text/plain"]),
1168
1697
  sniff,
1169
1698
  import: importCsv
1170
1699
  });
1700
+ function nodesInput(value) {
1701
+ if (value === void 0) {
1702
+ return null;
1703
+ }
1704
+ const object = value;
1705
+ const blob = typeof value === "object" && typeof object.stream === "function" && typeof object.size === "number";
1706
+ return blob ? value.stream() : value;
1707
+ }
1171
1708
  const CSV_LOSS = Object.freeze({
1172
- /** Two node ids share one text (a number and a string); export() throws E_INVALID_ID. */
1709
+ /**
1710
+ * Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
1711
+ * E_INVALID_ID.
1712
+ */
1173
1713
  ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
1174
- /** Ids whose text reads back as the other type under the canonical rule. */
1714
+ /** Ids that read back as a different type, such as the text "7" as the number 7. */
1175
1715
  ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
1176
1716
  /**
1177
1717
  * The generic dialect has no direction column; an undirected or mixed graph reads back as
@@ -1183,26 +1723,48 @@ const CSV_LOSS = Object.freeze({
1183
1723
  MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
1184
1724
  /** An attribute column named like a reserved header is not written. */
1185
1725
  RESERVED_NAME: "W_CSV_RESERVED_NAME",
1186
- /** A column without a role that the importer gives one back by its name. */
1726
+ /**
1727
+ * An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
1728
+ * reads back with that role.
1729
+ */
1187
1730
  ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
1188
- /** A role column (id, label) whose name the importer does not recognise; the role is lost. */
1731
+ /** A role column (id, label) whose name the importer does not recognize; the role is lost. */
1189
1732
  ROLE_NAME: "W_CSV_ROLE_NAME",
1190
- /** A role column (id, label) that is not string / dict reads back as string. */
1733
+ /** An id or label attribute that is not text reads back as text. */
1191
1734
  TEXT_ROLE: "W_CSV_TEXT_ROLE",
1192
1735
  /** NaN / Infinity in a numeric column read back as text. */
1193
1736
  NONFINITE: "W_CSV_NONFINITE",
1194
- /** A text column whose every value reads back as a number or boolean. */
1737
+ /**
1738
+ * A text value that reads back as a number or a boolean, because the format does not record that it was text (the
1739
+ * text "42" reads back as the number 42).
1740
+ */
1195
1741
  TEXT_INFERRED: LOSS.TEXT_INFERRED,
1196
- /** A dict column whose cardinality makes the importer read it back as string, or the reverse. */
1742
+ /**
1743
+ * A text attribute reads back as a dictionary attribute, or the reverse, because the importer chooses by how often
1744
+ * its values repeat. The values are the same.
1745
+ */
1197
1746
  STORAGE_CLASS_CHANGED: LOSS.STORAGE_CLASS,
1198
1747
  /** Node attributes are written by a `table: "nodes"` export only. */
1199
1748
  NODE_TABLE: "W_CSV_NODE_TABLE",
1200
- /** The edge table carries no node without an edge: isolated nodes vanish on re-import. */
1749
+ /**
1750
+ * The edge table has no row for a node without edges, so isolated nodes are missing when the file is read back.
1751
+ * Write the node table too (`table: "nodes"`).
1752
+ */
1201
1753
  ISOLATED_NODES: "W_CSV_ISOLATED_NODES",
1202
- /** The edge table lists nodes by first appearance; the node order (and indices) change on re-import. */
1754
+ /**
1755
+ * The edge table lists nodes in the order they first appear in an edge, so the node order changes when the file is
1756
+ * read back.
1757
+ */
1203
1758
  NODE_ORDER: "W_CSV_NODE_ORDER",
1204
1759
  /** An adjacency table holds no edge column but the weight: edge ids, labels and attributes are not written. */
1205
- EDGE_COLUMNS: "W_CSV_EDGE_COLUMNS"
1760
+ EDGE_COLUMNS: "W_CSV_EDGE_COLUMNS",
1761
+ /**
1762
+ * `header: false` with columns a file without a header cannot name: graph-io reads such a file by position
1763
+ * (source, target, weight, then columns it names column4, column5, ...), so these columns read back under other
1764
+ * names or in the wrong place (for example, an edge id in the weight's place). Write the header, or write only source,
1765
+ * target and weight with `dialect: "generic"`.
1766
+ */
1767
+ HEADERLESS: "W_CSV_HEADERLESS"
1206
1768
  });
1207
1769
  const CSV_CAPABILITIES = capabilities({
1208
1770
  mixedDirection: true,
@@ -1240,7 +1802,7 @@ const SKIPPED_ROLES = /* @__PURE__ */ new Set([
1240
1802
  ]);
1241
1803
  function resolveCsvExportOptions(options) {
1242
1804
  const o = options ?? {};
1243
- if (o.dialect !== void 0 && o.dialect !== "gephi" && o.dialect !== "generic") {
1805
+ if (o.dialect !== void 0 && o.dialect !== "gephi" && o.dialect !== "generic" && !otherFormatDialect("csv", o.dialect)) {
1244
1806
  throw new GraphFormatError("E_UNSUPPORTED", 'option dialect: expected "gephi" or "generic"', {
1245
1807
  option: "dialect",
1246
1808
  found: o.dialect
@@ -1278,7 +1840,8 @@ function resolveCsvExportOptions(options) {
1278
1840
  });
1279
1841
  }
1280
1842
  return {
1281
- dialect: DIALECTS[o.dialect ?? "gephi"],
1843
+ // another format's dialect (a JSON "d3" in a shared options object) leaves CSV's default
1844
+ dialect: DIALECTS[o.dialect === "generic" ? "generic" : "gephi"],
1282
1845
  table: o.table ?? "edges",
1283
1846
  delimiter: o.delimiter ?? ",",
1284
1847
  newline: o.newline ?? "\n",
@@ -1338,6 +1901,18 @@ function cellText(column, row) {
1338
1901
  }
1339
1902
  }
1340
1903
  }
1904
+ function nodeTableNote(names, table, note) {
1905
+ const written = names.length;
1906
+ if (written > 0) {
1907
+ const shown = names.slice(0, 3).map((n) => JSON.stringify(n)).join(", ") + (written > 3 ? ", ..." : "");
1908
+ note(
1909
+ CSV_LOSS.NODE_TABLE,
1910
+ `the ${table === "adjacency" ? "adjacency" : "edge"} table has no room for node attributes: ${written} node column${plural(written)} (${shown}) ${agree(written, "is", "are")} written only by a second export with table: "nodes"`,
1911
+ null,
1912
+ written
1913
+ );
1914
+ }
1915
+ }
1341
1916
  function idNotes(snapshot, values, note) {
1342
1917
  const { ids } = snapshot;
1343
1918
  if (ids.kind === "mixed") {
@@ -1354,7 +1929,7 @@ function idNotes(snapshot, values, note) {
1354
1929
  if (collisions > 0) {
1355
1930
  note(
1356
1931
  CSV_LOSS.ID_TEXT_COLLISION,
1357
- `${collisions} node id(s) share their text with another id (a number and a string); export() will throw E_INVALID_ID`,
1932
+ `${collisions} node id${plural(collisions)} ${agree(collisions, "shares", "share")} their text with another id (a number and a string); the save fails with E_INVALID_ID`,
1358
1933
  null,
1359
1934
  collisions
1360
1935
  );
@@ -1371,7 +1946,7 @@ function idNotes(snapshot, values, note) {
1371
1946
  if (changed > 0) {
1372
1947
  note(
1373
1948
  CSV_LOSS.ID_TEXT_TYPE,
1374
- `${changed} node id(s) read back as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`,
1949
+ `${changed} node id${plural(changed)} ${agree(changed, "reads", "read")} back as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`,
1375
1950
  null,
1376
1951
  changed
1377
1952
  );
@@ -1401,14 +1976,14 @@ function planExport(snapshot, options, values) {
1401
1976
  if (!snapshot.directed) {
1402
1977
  note(
1403
1978
  CSV_LOSS.DIRECTION_DROPPED,
1404
- `${where} has no direction column; ${snapshot.edgeCount} undirected edge(s) read back as directed unless the importer is told otherwise`,
1979
+ `${where} has no direction column; ${snapshot.edgeCount} undirected edge${plural(snapshot.edgeCount)} ${agree(snapshot.edgeCount, "reads", "read")} back as directed unless the importer is told otherwise`,
1405
1980
  null,
1406
1981
  snapshot.edgeCount
1407
1982
  );
1408
1983
  } else if (mixed > 0) {
1409
1984
  note(
1410
1985
  CSV_LOSS.DIRECTION_DROPPED,
1411
- `${where} has no direction column; ${mixed} undirected edge(s) of a mixed graph read back as one directed edge each`,
1986
+ `${where} has no direction column; ${mixed} undirected edge${plural(mixed)} of a mixed graph read back as one directed edge each`,
1412
1987
  null,
1413
1988
  mixed
1414
1989
  );
@@ -1424,7 +1999,7 @@ function planExport(snapshot, options, values) {
1424
1999
  if (folding.mutualCount > 0) {
1425
2000
  note(
1426
2001
  CSV_LOSS.MUTUAL_EXPANDED,
1427
- `${folding.mutualCount} mutual pair(s) are written as two directed rows; the mutual mark is lost`,
2002
+ `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
1428
2003
  null,
1429
2004
  folding.mutualCount
1430
2005
  );
@@ -1452,7 +2027,7 @@ function planExport(snapshot, options, values) {
1452
2027
  if (dropped > 0) {
1453
2028
  note(
1454
2029
  CSV_LOSS.EDGE_COLUMNS,
1455
- `${dropped} edge column(s) are not written: an adjacency table holds the weight only`,
2030
+ `${dropped} edge column${plural(dropped)} ${agree(dropped, "is", "are")} not written: an adjacency table holds the weight only`,
1456
2031
  null,
1457
2032
  dropped
1458
2033
  );
@@ -1489,15 +2064,11 @@ function planExport(snapshot, options, values) {
1489
2064
  }
1490
2065
  }
1491
2066
  } else {
1492
- const written = nodeColumns.length + (nodeLabel === null ? 0 : 1);
1493
- if (written > 0) {
1494
- note(
1495
- CSV_LOSS.NODE_TABLE,
1496
- `${written} node column(s) are written by a table: "nodes" export only`,
1497
- null,
1498
- written
1499
- );
1500
- }
2067
+ nodeTableNote(
2068
+ [...nodeLabel === null ? [] : [nodeLabel.meta.name], ...nodeColumns.map((c) => c.column.meta.name)],
2069
+ csv.table,
2070
+ note
2071
+ );
1501
2072
  }
1502
2073
  return {
1503
2074
  csv,
@@ -1541,7 +2112,7 @@ function noteNodeCoverage(snapshot, edgeRows, note) {
1541
2112
  if (isolated > 0) {
1542
2113
  note(
1543
2114
  CSV_LOSS.ISOLATED_NODES,
1544
- `${isolated} node(s) have no edge and cannot be written by the edge table; write the node table (table: "nodes") to keep them`,
2115
+ `${isolated} node${plural(isolated)} ${agree(isolated, "has", "have")} no edge and cannot be written by the edge table; write the node table (table: "nodes") to keep them`,
1545
2116
  null,
1546
2117
  isolated
1547
2118
  );
@@ -1549,7 +2120,7 @@ function noteNodeCoverage(snapshot, edgeRows, note) {
1549
2120
  if (reordered > 0) {
1550
2121
  note(
1551
2122
  CSV_LOSS.NODE_ORDER,
1552
- `${reordered} node(s) are first mentioned by an edge row out of index order; a re-import numbers nodes by first appearance`,
2123
+ `${reordered} node${plural(reordered)} ${agree(reordered, "is", "are")} first mentioned by an edge row out of index order; a re-import numbers nodes by first appearance`,
1553
2124
  null,
1554
2125
  reordered
1555
2126
  );
@@ -1654,7 +2225,12 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
1654
2225
  }
1655
2226
  }
1656
2227
  if (nonFinite > 0) {
1657
- note(CSV_LOSS.NONFINITE, `${label}: ${nonFinite} non-finite value(s) read back as text`, name, nonFinite);
2228
+ note(
2229
+ CSV_LOSS.NONFINITE,
2230
+ `${label}: ${nonFinite} non-finite value${plural(nonFinite)} ${agree(nonFinite, "reads", "read")} back as text`,
2231
+ name,
2232
+ nonFinite
2233
+ );
1658
2234
  }
1659
2235
  return;
1660
2236
  }
@@ -1862,15 +2438,58 @@ function prepare(snapshot, options) {
1862
2438
  }
1863
2439
  return plan;
1864
2440
  }
2441
+ function headerlessNotes(plan) {
2442
+ const { csv } = plan;
2443
+ if (csv.header || csv.table === "adjacency") {
2444
+ return [];
2445
+ }
2446
+ const names = csv.table === "nodes" ? headerlessNodeColumns(plan) : headerlessEdgeColumns(plan);
2447
+ if (names.length === 0) {
2448
+ return [];
2449
+ }
2450
+ const shown = names.map((n) => JSON.stringify(n)).join(", ");
2451
+ return [
2452
+ Object.freeze({
2453
+ code: CSV_LOSS.HEADERLESS,
2454
+ message: `without a header the file is read back by position (${csv.table === "nodes" ? "id" : "source, target, weight"}, then unnamed columns), so ${names.length === 1 ? "the column" : "the columns"} ${shown} ${agree(names.length, "reads", "read")} back unnamed or in another column's place; write the header, or for an edge list pass dialect: "generic" and write only source, target and weight`,
2455
+ column: null,
2456
+ count: names.length
2457
+ })
2458
+ ];
2459
+ }
2460
+ function headerlessNodeColumns(plan) {
2461
+ const label = plan.nodeLabel === null ? [] : [plan.nodeLabel.meta.name];
2462
+ return [...label, ...plan.nodeColumns.map((c) => c.header)];
2463
+ }
2464
+ function headerlessEdgeColumns(plan) {
2465
+ const { dialect } = plan.csv;
2466
+ const names = [];
2467
+ if (dialect.type !== null) {
2468
+ names.push(dialect.type);
2469
+ }
2470
+ if (plan.edgeId !== null) {
2471
+ names.push(plan.edgeId.meta.name);
2472
+ }
2473
+ if (plan.edgeLabel !== null) {
2474
+ names.push(plan.edgeLabel.meta.name);
2475
+ }
2476
+ if (plan.weights.weighted && names.length > 0) {
2477
+ names.push(dialect.weight);
2478
+ }
2479
+ names.push(...plan.edgeColumns.map((c) => c.header));
2480
+ return names;
2481
+ }
1865
2482
  function check(snapshot, options) {
1866
2483
  const plan = planExport(snapshot, options, true);
1867
2484
  return Object.freeze([
1868
2485
  ...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, { roles: KEPT_ROLES }),
1869
- ...plan.notes
2486
+ ...plan.notes,
2487
+ ...headerlessNotes(plan)
1870
2488
  ]);
1871
2489
  }
1872
2490
  const csvExporter = Object.freeze({
1873
2491
  format: "csv",
2492
+ options: Object.freeze(["delimiter", "dialect", "header", "newline", "table"]),
1874
2493
  capabilities: CSV_CAPABILITIES,
1875
2494
  check,
1876
2495
  export(snapshot, options) {
@@ -1881,21 +2500,37 @@ const csvExporter = Object.freeze({
1881
2500
  }
1882
2501
  });
1883
2502
  const CSV_ISSUE = Object.freeze({
1884
- /** The input is empty (fatal). */
2503
+ ...INPUT_ISSUE,
2504
+ ...ELEMENT_ISSUE,
2505
+ /** An integer beyond 2^53 was stored as the nearest 64-bit float; reported once per column. */
2506
+ PRECISION: PRECISION_CODE,
2507
+ /** The input is empty. The import stops. */
1885
2508
  EMPTY_INPUT: EMPTY_INPUT_CODE,
1886
- /** The input holds invalid UTF-8 (fatal). */
2509
+ /** The input is not valid UTF-8. The import stops. */
1887
2510
  INVALID_UTF8: INVALID_UTF8_CODE,
1888
- /** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
2511
+ /**
2512
+ * Some bytes are not valid in the encoding that was chosen (by a byte order mark, the file's declaration or the
2513
+ * `encoding` option). The import stops.
2514
+ */
1889
2515
  INVALID_ENCODING: INVALID_ENCODING_CODE,
1890
2516
  /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
1891
2517
  ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
1892
2518
  /** A declared encoding the platform cannot decode was ignored. */
1893
2519
  UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
1894
- /** The header names neither endpoint columns nor an id column (fatal). */
2520
+ /**
2521
+ * The header names no source and target columns and no id column. Pass `sourceColumn` and `targetColumn` to name
2522
+ * the columns. The import stops.
2523
+ */
1895
2524
  NO_ENDPOINT_COLUMNS: NO_ENDPOINT_COLUMNS_CODE,
1896
- /** A node table without an id column (fatal). */
2525
+ /**
2526
+ * The node table has no id column. Pass `idColumn` to name it, or `rowNumberIds: true` to number the rows. The
2527
+ * import stops.
2528
+ */
1897
2529
  NO_ID_COLUMN: NO_ID_COLUMN_CODE,
1898
- /** A row with a different field count than the header. */
2530
+ /**
2531
+ * A row with a different number of fields than the header; the row is skipped. An adjacency list read without
2532
+ * `table: "adjacency"` gives one per long row, and the first one's message suggests the option.
2533
+ */
1899
2534
  FIELD_COUNT: FIELD_COUNT_CODE,
1900
2535
  /** An edge row with a blank source or target. */
1901
2536
  MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
@@ -1903,9 +2538,12 @@ const CSV_ISSUE = Object.freeze({
1903
2538
  MISSING_ID: MISSING_ID_CODE,
1904
2539
  /** A Type cell outside Directed / Undirected / Mutual. */
1905
2540
  BAD_TYPE: BAD_TYPE_CODE,
1906
- /** An unterminated quoted field (fatal). */
2541
+ /** A quoted field is never closed. The import stops. */
1907
2542
  UNCLOSED_QUOTE: UNCLOSED_QUOTE_CODE,
1908
- /** Text after a closing quote (fatal). */
2543
+ /**
2544
+ * There is text after a closing quote, such as `"a"b`. Inside a quoted field, write a quote as two quotes. The
2545
+ * import stops.
2546
+ */
1909
2547
  QUOTE: BAD_QUOTE_CODE,
1910
2548
  /** A header and no data rows. */
1911
2549
  NO_DATA_ROWS: NO_DATA_ROWS_CODE,
@@ -1913,26 +2551,78 @@ const CSV_ISSUE = Object.freeze({
1913
2551
  DUPLICATE_NODE: DUPLICATE_NODE_CODE,
1914
2552
  /** An edge row repeating an edge id (skipped). */
1915
2553
  DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
1916
- /** Two id cells merged into one number under ids "number". */
2554
+ /** Two different id cells became the same number because `ids` is "number", so their nodes were merged. */
1917
2555
  ID_MERGED: ID_MERGED_CODE,
1918
2556
  /** An explicitly named weight column the file does not have. */
1919
2557
  COLUMN_MISSING: COLUMN_MISSING_CODE,
1920
- /** A column whose role another column of the sink already holds. */
2558
+ /**
2559
+ * You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
2560
+ * a plain attribute.
2561
+ */
1921
2562
  ROLE_TAKEN: ROLE_TAKEN_CODE,
1922
- /** A column renamed `<name>#<position>` (a repeated header, or a name the sink holds with another shape). */
2563
+ /**
2564
+ * An attribute was renamed `<name>#<suffix>` because another attribute already has its name, for example a
2565
+ * repeated column header.
2566
+ */
1923
2567
  COLUMN_RENAMED: COLUMN_RENAMED_CODE,
1924
- /** A common option the importer has no use for was given. */
2568
+ /** You set an option this format does not use; it had no effect. The message names the option. */
1925
2569
  OPTION_IGNORED: OPTION_IGNORED_CODE,
1926
- /** A builder-policy option the sink does not honour. */
2570
+ /**
2571
+ * You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
2572
+ * `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
2573
+ */
1927
2574
  SINK_OPTION: SINK_OPTION_CODE,
1928
- /** The sink refused the file's direction. */
2575
+ /**
2576
+ * You read into a graph builder whose direction is already set, or which already holds edges, so the file is read
2577
+ * with the builder's direction instead of its own.
2578
+ */
1929
2579
  DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
1930
- /** Edges forced to the policy's direction. */
2580
+ /** Edges of the other direction were read with the direction `onMixedDirection` chose. */
1931
2581
  DIRECTION_FORCED: DIRECTION_FORCED_CODE,
1932
- /** A mixed file under onMixedDirection "error" (fatal). */
2582
+ /**
2583
+ * The graph has both directed and undirected edges and `onMixedDirection` is "error". An import stops; a save to a
2584
+ * format that holds one direction per file fails with E_DIRECTED. Pass "directed" or "undirected" to read or write
2585
+ * it anyway.
2586
+ */
1933
2587
  MIXED_DIRECTION: MIXED_DIRECTION_CODE,
1934
- /** A text column the sink could not widen to the dtype its cells imply. */
1935
- WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE
2588
+ /**
2589
+ * Your graph builder cannot change an attribute's type after its first value, so a text column keeps the type of
2590
+ * its first values.
2591
+ */
2592
+ WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE,
2593
+ /** The input starts like another format (XML or HTML, JSON, GML, DOT or Pajek), not CSV. The import stops. */
2594
+ OTHER_FORMAT: OTHER_FORMAT_CODE,
2595
+ /**
2596
+ * A quote inside a field that does not start with a quote is kept as part of the text. Reported once per import.
2597
+ */
2598
+ STRAY_QUOTE: STRAY_QUOTE_CODE,
2599
+ /** A leading comment's direction disagrees with defaultDirected or an earlier comment. */
2600
+ COMMENT_DIRECTION: COMMENT_DIRECTION_CODE,
2601
+ /**
2602
+ * A column named like `Type` holds direction words (Directed, Undirected), but graph-io reads direction only from
2603
+ * a `Type` column beside `Source` and `Target`, or from the `typeColumn` option. Reported once per import.
2604
+ */
2605
+ TYPE_COLUMN_IGNORED: TYPE_COLUMN_IGNORED_CODE,
2606
+ /** The header ends in a delimiter; rows without the empty last cell are complete. */
2607
+ TRAILING_HEADER_DELIMITER: TRAILING_HEADER_DELIMITER_CODE,
2608
+ /** Every row is one cell another delimiter would split (a likely wrong delimiter option). */
2609
+ SINGLE_COLUMN: SINGLE_COLUMN_CODE,
2610
+ /** Several header columns name one role; the one not chosen is a plain attribute. */
2611
+ AMBIGUOUS_COLUMN: AMBIGUOUS_COLUMN_CODE,
2612
+ /**
2613
+ * An id that is not quoted has spaces at its start or end; they are kept, so it is a different id from the trimmed
2614
+ * text. Reported once per import.
2615
+ */
2616
+ PADDED_ID: PADDED_ID_CODE,
2617
+ /** A leading # / % line skipped as a comment has the fields of a record. */
2618
+ COMMENT_LIKE_RECORD: COMMENT_LIKE_RECORD_CODE,
2619
+ /** Every data row ends in one extra delimiter; the empty last cell is dropped. Reported once per import. */
2620
+ TRAILING_DELIMITER: TRAILING_DELIMITER_CODE,
2621
+ /**
2622
+ * In a file without a header, the third column holds text, so it is read as an attribute, not as the edge weight.
2623
+ * Reported once per import.
2624
+ */
2625
+ WEIGHT_AS_ATTRIBUTE: WEIGHT_AS_ATTRIBUTE_CODE
1936
2626
  });
1937
2627
  export {
1938
2628
  CSV_CAPABILITIES,