@graphty/graph-io 0.3.25 → 0.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/README.md +49 -414
  2. package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
  3. package/dist/chunks/choose-BbJI55dx.js +56 -0
  4. package/dist/chunks/choose-BbJI55dx.js.map +1 -0
  5. package/dist/chunks/constants-BCsFsMj4.js +231 -0
  6. package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
  8. package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
  9. package/dist/chunks/export-BTI4yQgy.js +4333 -0
  10. package/dist/chunks/export-BTI4yQgy.js.map +1 -0
  11. package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
  12. package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
  13. package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
  14. package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
  15. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
  16. package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
  17. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
  18. package/dist/chunks/exporter-tne475KK.js.map +1 -0
  19. package/dist/chunks/importer-90YWhJoF.js +2811 -0
  20. package/dist/chunks/importer-90YWhJoF.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
  22. package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
  23. package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
  24. package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
  25. package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
  26. package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
  28. package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
  29. package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
  30. package/dist/chunks/importer-DfVu6j59.js.map +1 -0
  31. package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
  32. package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
  33. package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
  34. package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
  35. package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
  36. package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
  38. package/dist/chunks/records-CQXT__mY.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
  40. package/dist/chunks/text-DmcUs5_Q.js.map +1 -0
  41. package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
  42. package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
  43. package/dist/csv.js +951 -261
  44. package/dist/csv.js.map +1 -1
  45. package/dist/cx.js +6 -3
  46. package/dist/cx2.js +2 -2
  47. package/dist/cys.js +6 -3
  48. package/dist/dot.js +1 -1
  49. package/dist/gexf.js +554 -122
  50. package/dist/gexf.js.map +1 -1
  51. package/dist/gml.js +342 -85
  52. package/dist/gml.js.map +1 -1
  53. package/dist/graph-io.js +1105 -213
  54. package/dist/graph-io.js.map +1 -1
  55. package/dist/graphml.js +3 -2
  56. package/dist/graphml.js.map +1 -1
  57. package/dist/json.js +3 -2
  58. package/dist/neo4j.js +564 -157
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +6 -3
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/children.d.ts +18 -6
  63. package/dist/src/children.d.ts.map +1 -1
  64. package/dist/src/children.js +6 -3
  65. package/dist/src/children.js.map +1 -1
  66. package/dist/src/common/attributes.d.ts +29 -11
  67. package/dist/src/common/attributes.d.ts.map +1 -1
  68. package/dist/src/common/attributes.js +25 -7
  69. package/dist/src/common/attributes.js.map +1 -1
  70. package/dist/src/common/cell-budget.d.ts +9 -2
  71. package/dist/src/common/cell-budget.d.ts.map +1 -1
  72. package/dist/src/common/cell-budget.js +11 -3
  73. package/dist/src/common/cell-budget.js.map +1 -1
  74. package/dist/src/common/choose.d.ts +19 -0
  75. package/dist/src/common/choose.d.ts.map +1 -0
  76. package/dist/src/common/choose.js +72 -0
  77. package/dist/src/common/choose.js.map +1 -0
  78. package/dist/src/common/codes.d.ts +456 -66
  79. package/dist/src/common/codes.d.ts.map +1 -1
  80. package/dist/src/common/codes.js +458 -66
  81. package/dist/src/common/codes.js.map +1 -1
  82. package/dist/src/common/compare.d.ts +81 -0
  83. package/dist/src/common/compare.d.ts.map +1 -0
  84. package/dist/src/common/compare.js +394 -0
  85. package/dist/src/common/compare.js.map +1 -0
  86. package/dist/src/common/cx-export.d.ts +123 -0
  87. package/dist/src/common/cx-export.d.ts.map +1 -0
  88. package/dist/src/common/cx-export.js +334 -0
  89. package/dist/src/common/cx-export.js.map +1 -0
  90. package/dist/src/common/declared-types.d.ts +22 -2
  91. package/dist/src/common/declared-types.d.ts.map +1 -1
  92. package/dist/src/common/declared-types.js +19 -1
  93. package/dist/src/common/declared-types.js.map +1 -1
  94. package/dist/src/common/direction.d.ts +74 -50
  95. package/dist/src/common/direction.d.ts.map +1 -1
  96. package/dist/src/common/direction.js +47 -27
  97. package/dist/src/common/direction.js.map +1 -1
  98. package/dist/src/common/escape.d.ts +21 -9
  99. package/dist/src/common/escape.d.ts.map +1 -1
  100. package/dist/src/common/escape.js +33 -17
  101. package/dist/src/common/escape.js.map +1 -1
  102. package/dist/src/common/export.d.ts +144 -32
  103. package/dist/src/common/export.d.ts.map +1 -1
  104. package/dist/src/common/export.js +216 -42
  105. package/dist/src/common/export.js.map +1 -1
  106. package/dist/src/common/format.d.ts +23 -1
  107. package/dist/src/common/format.d.ts.map +1 -1
  108. package/dist/src/common/format.js +26 -1
  109. package/dist/src/common/format.js.map +1 -1
  110. package/dist/src/common/ids.d.ts +9 -2
  111. package/dist/src/common/ids.d.ts.map +1 -1
  112. package/dist/src/common/ids.js +6 -2
  113. package/dist/src/common/ids.js.map +1 -1
  114. package/dist/src/common/input.d.ts +122 -28
  115. package/dist/src/common/input.d.ts.map +1 -1
  116. package/dist/src/common/input.js +890 -124
  117. package/dist/src/common/input.js.map +1 -1
  118. package/dist/src/common/json-elements.d.ts +144 -20
  119. package/dist/src/common/json-elements.d.ts.map +1 -1
  120. package/dist/src/common/json-elements.js +868 -188
  121. package/dist/src/common/json-elements.js.map +1 -1
  122. package/dist/src/common/line-format.d.ts +90 -0
  123. package/dist/src/common/line-format.d.ts.map +1 -0
  124. package/dist/src/common/line-format.js +185 -0
  125. package/dist/src/common/line-format.js.map +1 -0
  126. package/dist/src/common/lists.d.ts +6 -1
  127. package/dist/src/common/lists.d.ts.map +1 -1
  128. package/dist/src/common/lists.js +2 -0
  129. package/dist/src/common/lists.js.map +1 -1
  130. package/dist/src/common/ontology-export.d.ts +167 -0
  131. package/dist/src/common/ontology-export.d.ts.map +1 -0
  132. package/dist/src/common/ontology-export.js +359 -0
  133. package/dist/src/common/ontology-export.js.map +1 -0
  134. package/dist/src/common/ontology.d.ts +25 -3
  135. package/dist/src/common/ontology.d.ts.map +1 -1
  136. package/dist/src/common/ontology.js +30 -9
  137. package/dist/src/common/ontology.js.map +1 -1
  138. package/dist/src/common/options.d.ts +62 -12
  139. package/dist/src/common/options.d.ts.map +1 -1
  140. package/dist/src/common/options.js +89 -11
  141. package/dist/src/common/options.js.map +1 -1
  142. package/dist/src/common/plural.d.ts +31 -0
  143. package/dist/src/common/plural.d.ts.map +1 -0
  144. package/dist/src/common/plural.js +46 -0
  145. package/dist/src/common/plural.js.map +1 -0
  146. package/dist/src/common/report.d.ts +72 -14
  147. package/dist/src/common/report.d.ts.map +1 -1
  148. package/dist/src/common/report.js +146 -22
  149. package/dist/src/common/report.js.map +1 -1
  150. package/dist/src/common/temporal.d.ts +22 -5
  151. package/dist/src/common/temporal.d.ts.map +1 -1
  152. package/dist/src/common/temporal.js +13 -2
  153. package/dist/src/common/temporal.js.map +1 -1
  154. package/dist/src/common/text.d.ts +39 -23
  155. package/dist/src/common/text.d.ts.map +1 -1
  156. package/dist/src/common/text.js +51 -21
  157. package/dist/src/common/text.js.map +1 -1
  158. package/dist/src/common/weights.d.ts +37 -4
  159. package/dist/src/common/weights.d.ts.map +1 -1
  160. package/dist/src/common/weights.js +61 -2
  161. package/dist/src/common/weights.js.map +1 -1
  162. package/dist/src/common/writer.d.ts +19 -3
  163. package/dist/src/common/writer.d.ts.map +1 -1
  164. package/dist/src/common/writer.js +30 -2
  165. package/dist/src/common/writer.js.map +1 -1
  166. package/dist/src/common/xml.d.ts +125 -9
  167. package/dist/src/common/xml.d.ts.map +1 -1
  168. package/dist/src/common/xml.js +400 -83
  169. package/dist/src/common/xml.js.map +1 -1
  170. package/dist/src/common/zip.d.ts +44 -4
  171. package/dist/src/common/zip.d.ts.map +1 -1
  172. package/dist/src/common/zip.js +134 -5
  173. package/dist/src/common/zip.js.map +1 -1
  174. package/dist/src/formats/csv/exporter.d.ts +80 -24
  175. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  176. package/dist/src/formats/csv/exporter.js +145 -32
  177. package/dist/src/formats/csv/exporter.js.map +1 -1
  178. package/dist/src/formats/csv/header.d.ts +38 -9
  179. package/dist/src/formats/csv/header.d.ts.map +1 -1
  180. package/dist/src/formats/csv/header.js +46 -10
  181. package/dist/src/formats/csv/header.js.map +1 -1
  182. package/dist/src/formats/csv/importer.d.ts +187 -37
  183. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  184. package/dist/src/formats/csv/importer.js +835 -204
  185. package/dist/src/formats/csv/importer.js.map +1 -1
  186. package/dist/src/formats/csv/index.d.ts +100 -19
  187. package/dist/src/formats/csv/index.d.ts.map +1 -1
  188. package/dist/src/formats/csv/index.js +94 -21
  189. package/dist/src/formats/csv/index.js.map +1 -1
  190. package/dist/src/formats/csv/records.d.ts +95 -10
  191. package/dist/src/formats/csv/records.d.ts.map +1 -1
  192. package/dist/src/formats/csv/records.js +333 -122
  193. package/dist/src/formats/csv/records.js.map +1 -1
  194. package/dist/src/formats/csv/values.d.ts +1 -0
  195. package/dist/src/formats/csv/values.d.ts.map +1 -1
  196. package/dist/src/formats/csv/values.js +1 -0
  197. package/dist/src/formats/csv/values.js.map +1 -1
  198. package/dist/src/formats/cx/exporter.d.ts +102 -0
  199. package/dist/src/formats/cx/exporter.d.ts.map +1 -0
  200. package/dist/src/formats/cx/exporter.js +1217 -0
  201. package/dist/src/formats/cx/exporter.js.map +1 -0
  202. package/dist/src/formats/cx/importer.d.ts +79 -32
  203. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  204. package/dist/src/formats/cx/importer.js +680 -285
  205. package/dist/src/formats/cx/importer.js.map +1 -1
  206. package/dist/src/formats/cx/index.d.ts +4 -3
  207. package/dist/src/formats/cx/index.d.ts.map +1 -1
  208. package/dist/src/formats/cx/index.js +4 -3
  209. package/dist/src/formats/cx/index.js.map +1 -1
  210. package/dist/src/formats/cx2/exporter.d.ts +29 -11
  211. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  212. package/dist/src/formats/cx2/exporter.js +39 -268
  213. package/dist/src/formats/cx2/exporter.js.map +1 -1
  214. package/dist/src/formats/cx2/importer.d.ts +78 -28
  215. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  216. package/dist/src/formats/cx2/importer.js +226 -79
  217. package/dist/src/formats/cx2/importer.js.map +1 -1
  218. package/dist/src/formats/cx2/index.d.ts +2 -1
  219. package/dist/src/formats/cx2/index.d.ts.map +1 -1
  220. package/dist/src/formats/cx2/index.js +2 -1
  221. package/dist/src/formats/cx2/index.js.map +1 -1
  222. package/dist/src/formats/cys/constants.d.ts +66 -17
  223. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  224. package/dist/src/formats/cys/constants.js +59 -18
  225. package/dist/src/formats/cys/constants.js.map +1 -1
  226. package/dist/src/formats/cys/exporter.d.ts +118 -0
  227. package/dist/src/formats/cys/exporter.d.ts.map +1 -0
  228. package/dist/src/formats/cys/exporter.js +1072 -0
  229. package/dist/src/formats/cys/exporter.js.map +1 -0
  230. package/dist/src/formats/cys/importer.d.ts +22 -8
  231. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  232. package/dist/src/formats/cys/importer.js +510 -149
  233. package/dist/src/formats/cys/importer.js.map +1 -1
  234. package/dist/src/formats/cys/index.d.ts +4 -2
  235. package/dist/src/formats/cys/index.d.ts.map +1 -1
  236. package/dist/src/formats/cys/index.js +4 -2
  237. package/dist/src/formats/cys/index.js.map +1 -1
  238. package/dist/src/formats/cys/session.d.ts +46 -7
  239. package/dist/src/formats/cys/session.d.ts.map +1 -1
  240. package/dist/src/formats/cys/session.js +98 -24
  241. package/dist/src/formats/cys/session.js.map +1 -1
  242. package/dist/src/formats/cys/tables.d.ts +9 -2
  243. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  244. package/dist/src/formats/cys/tables.js +29 -7
  245. package/dist/src/formats/cys/tables.js.map +1 -1
  246. package/dist/src/formats/dot/exporter.d.ts +42 -14
  247. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  248. package/dist/src/formats/dot/exporter.js +56 -29
  249. package/dist/src/formats/dot/exporter.js.map +1 -1
  250. package/dist/src/formats/dot/importer.d.ts +91 -29
  251. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  252. package/dist/src/formats/dot/importer.js +190 -39
  253. package/dist/src/formats/dot/importer.js.map +1 -1
  254. package/dist/src/formats/dot/index.d.ts +2 -1
  255. package/dist/src/formats/dot/index.d.ts.map +1 -1
  256. package/dist/src/formats/dot/index.js +2 -1
  257. package/dist/src/formats/dot/index.js.map +1 -1
  258. package/dist/src/formats/dot/names.d.ts +44 -11
  259. package/dist/src/formats/dot/names.d.ts.map +1 -1
  260. package/dist/src/formats/dot/names.js +44 -11
  261. package/dist/src/formats/dot/names.js.map +1 -1
  262. package/dist/src/formats/dot/tokenizer.d.ts +9 -2
  263. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  264. package/dist/src/formats/dot/tokenizer.js +20 -5
  265. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  266. package/dist/src/formats/gexf/exporter.d.ts +46 -15
  267. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  268. package/dist/src/formats/gexf/exporter.js +45 -15
  269. package/dist/src/formats/gexf/exporter.js.map +1 -1
  270. package/dist/src/formats/gexf/importer.d.ts +87 -23
  271. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  272. package/dist/src/formats/gexf/importer.js +471 -90
  273. package/dist/src/formats/gexf/importer.js.map +1 -1
  274. package/dist/src/formats/gexf/index.d.ts +84 -21
  275. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  276. package/dist/src/formats/gexf/index.js +80 -23
  277. package/dist/src/formats/gexf/index.js.map +1 -1
  278. package/dist/src/formats/gexf/schema.d.ts +67 -15
  279. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  280. package/dist/src/formats/gexf/schema.js +59 -13
  281. package/dist/src/formats/gexf/schema.js.map +1 -1
  282. package/dist/src/formats/gml/exporter.d.ts +69 -21
  283. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/gml/exporter.js +74 -29
  285. package/dist/src/formats/gml/exporter.js.map +1 -1
  286. package/dist/src/formats/gml/importer.d.ts +116 -25
  287. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  288. package/dist/src/formats/gml/importer.js +290 -61
  289. package/dist/src/formats/gml/importer.js.map +1 -1
  290. package/dist/src/formats/gml/index.d.ts +87 -24
  291. package/dist/src/formats/gml/index.d.ts.map +1 -1
  292. package/dist/src/formats/gml/index.js +79 -26
  293. package/dist/src/formats/gml/index.js.map +1 -1
  294. package/dist/src/formats/gml/syntax.d.ts +67 -18
  295. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  296. package/dist/src/formats/gml/syntax.js +108 -26
  297. package/dist/src/formats/gml/syntax.js.map +1 -1
  298. package/dist/src/formats/graphml/constants.d.ts +174 -44
  299. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  300. package/dist/src/formats/graphml/constants.js +165 -44
  301. package/dist/src/formats/graphml/constants.js.map +1 -1
  302. package/dist/src/formats/graphml/exporter.d.ts +17 -9
  303. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  304. package/dist/src/formats/graphml/exporter.js +33 -12
  305. package/dist/src/formats/graphml/exporter.js.map +1 -1
  306. package/dist/src/formats/graphml/importer.d.ts +19 -7
  307. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  308. package/dist/src/formats/graphml/importer.js +466 -39
  309. package/dist/src/formats/graphml/importer.js.map +1 -1
  310. package/dist/src/formats/graphml/index.d.ts +2 -1
  311. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  312. package/dist/src/formats/graphml/index.js +2 -1
  313. package/dist/src/formats/graphml/index.js.map +1 -1
  314. package/dist/src/formats/graphml/tree.d.ts +6 -2
  315. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  316. package/dist/src/formats/graphml/tree.js +6 -2
  317. package/dist/src/formats/graphml/tree.js.map +1 -1
  318. package/dist/src/formats/graphml/yfiles.d.ts +8 -3
  319. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  320. package/dist/src/formats/graphml/yfiles.js +36 -15
  321. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  322. package/dist/src/formats/json/dialect.d.ts +88 -24
  323. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  324. package/dist/src/formats/json/dialect.js +109 -19
  325. package/dist/src/formats/json/dialect.js.map +1 -1
  326. package/dist/src/formats/json/exporter.d.ts +134 -22
  327. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  328. package/dist/src/formats/json/exporter.js +135 -54
  329. package/dist/src/formats/json/exporter.js.map +1 -1
  330. package/dist/src/formats/json/importer.d.ts +173 -52
  331. package/dist/src/formats/json/importer.d.ts.map +1 -1
  332. package/dist/src/formats/json/importer.js +899 -220
  333. package/dist/src/formats/json/importer.js.map +1 -1
  334. package/dist/src/formats/json/index.d.ts +3 -2
  335. package/dist/src/formats/json/index.d.ts.map +1 -1
  336. package/dist/src/formats/json/index.js +3 -2
  337. package/dist/src/formats/json/index.js.map +1 -1
  338. package/dist/src/formats/json/obographs-export.d.ts +68 -0
  339. package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
  340. package/dist/src/formats/json/obographs-export.js +941 -0
  341. package/dist/src/formats/json/obographs-export.js.map +1 -0
  342. package/dist/src/formats/json/obographs.d.ts +26 -0
  343. package/dist/src/formats/json/obographs.d.ts.map +1 -1
  344. package/dist/src/formats/json/obographs.js +297 -80
  345. package/dist/src/formats/json/obographs.js.map +1 -1
  346. package/dist/src/formats/neo4j/exporter.d.ts +109 -21
  347. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  348. package/dist/src/formats/neo4j/exporter.js +238 -85
  349. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  350. package/dist/src/formats/neo4j/header.d.ts +17 -4
  351. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  352. package/dist/src/formats/neo4j/header.js +36 -4
  353. package/dist/src/formats/neo4j/header.js.map +1 -1
  354. package/dist/src/formats/neo4j/importer.d.ts +107 -27
  355. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  356. package/dist/src/formats/neo4j/importer.js +415 -63
  357. package/dist/src/formats/neo4j/importer.js.map +1 -1
  358. package/dist/src/formats/neo4j/index.d.ts +73 -23
  359. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  360. package/dist/src/formats/neo4j/index.js +65 -25
  361. package/dist/src/formats/neo4j/index.js.map +1 -1
  362. package/dist/src/formats/obo/exporter.d.ts +150 -0
  363. package/dist/src/formats/obo/exporter.d.ts.map +1 -0
  364. package/dist/src/formats/obo/exporter.js +1736 -0
  365. package/dist/src/formats/obo/exporter.js.map +1 -0
  366. package/dist/src/formats/obo/importer.d.ts +95 -19
  367. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  368. package/dist/src/formats/obo/importer.js +475 -104
  369. package/dist/src/formats/obo/importer.js.map +1 -1
  370. package/dist/src/formats/obo/index.d.ts +6 -3
  371. package/dist/src/formats/obo/index.d.ts.map +1 -1
  372. package/dist/src/formats/obo/index.js +6 -3
  373. package/dist/src/formats/obo/index.js.map +1 -1
  374. package/dist/src/formats/obo/syntax.d.ts +108 -4
  375. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  376. package/dist/src/formats/obo/syntax.js +211 -34
  377. package/dist/src/formats/obo/syntax.js.map +1 -1
  378. package/dist/src/formats/pajek/exporter.d.ts +54 -16
  379. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  380. package/dist/src/formats/pajek/exporter.js +66 -34
  381. package/dist/src/formats/pajek/exporter.js.map +1 -1
  382. package/dist/src/formats/pajek/importer.d.ts +84 -22
  383. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  384. package/dist/src/formats/pajek/importer.js +303 -83
  385. package/dist/src/formats/pajek/importer.js.map +1 -1
  386. package/dist/src/formats/pajek/index.d.ts +2 -1
  387. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  388. package/dist/src/formats/pajek/index.js +2 -1
  389. package/dist/src/formats/pajek/index.js.map +1 -1
  390. package/dist/src/formats/pajek/syntax.d.ts +77 -14
  391. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  392. package/dist/src/formats/pajek/syntax.js +183 -51
  393. package/dist/src/formats/pajek/syntax.js.map +1 -1
  394. package/dist/src/formats/xgmml/columns.d.ts +1 -0
  395. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  396. package/dist/src/formats/xgmml/columns.js +4 -0
  397. package/dist/src/formats/xgmml/columns.js.map +1 -1
  398. package/dist/src/formats/xgmml/constants.d.ts +186 -56
  399. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  400. package/dist/src/formats/xgmml/constants.js +181 -57
  401. package/dist/src/formats/xgmml/constants.js.map +1 -1
  402. package/dist/src/formats/xgmml/document.d.ts +65 -11
  403. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  404. package/dist/src/formats/xgmml/document.js +157 -28
  405. package/dist/src/formats/xgmml/document.js.map +1 -1
  406. package/dist/src/formats/xgmml/emit.d.ts +61 -3
  407. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  408. package/dist/src/formats/xgmml/emit.js +129 -26
  409. package/dist/src/formats/xgmml/emit.js.map +1 -1
  410. package/dist/src/formats/xgmml/exporter.d.ts +13 -6
  411. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  412. package/dist/src/formats/xgmml/exporter.js +20 -21
  413. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  414. package/dist/src/formats/xgmml/importer.d.ts +33 -12
  415. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  416. package/dist/src/formats/xgmml/importer.js +35 -72
  417. package/dist/src/formats/xgmml/importer.js.map +1 -1
  418. package/dist/src/formats/xgmml/index.d.ts +1 -0
  419. package/dist/src/formats/xgmml/index.d.ts.map +1 -1
  420. package/dist/src/formats/xgmml/index.js +1 -0
  421. package/dist/src/formats/xgmml/index.js.map +1 -1
  422. package/dist/src/formats/xgmml/values.d.ts +8 -1
  423. package/dist/src/formats/xgmml/values.d.ts.map +1 -1
  424. package/dist/src/formats/xgmml/values.js +7 -2
  425. package/dist/src/formats/xgmml/values.js.map +1 -1
  426. package/dist/src/index.d.ts +23 -18
  427. package/dist/src/index.d.ts.map +1 -1
  428. package/dist/src/index.js +28 -16
  429. package/dist/src/index.js.map +1 -1
  430. package/dist/src/registry.d.ts +478 -92
  431. package/dist/src/registry.d.ts.map +1 -1
  432. package/dist/src/registry.js +807 -88
  433. package/dist/src/registry.js.map +1 -1
  434. package/dist/src/sniff.d.ts +61 -19
  435. package/dist/src/sniff.d.ts.map +1 -1
  436. package/dist/src/sniff.js +121 -29
  437. package/dist/src/sniff.js.map +1 -1
  438. package/dist/src/types.d.ts +362 -112
  439. package/dist/src/types.d.ts.map +1 -1
  440. package/dist/src/types.js +28 -7
  441. package/dist/src/types.js.map +1 -1
  442. package/dist/xgmml.js +2 -2
  443. package/docs/samples/README.md +25 -0
  444. package/docs/samples/got-edges.csv +353 -0
  445. package/docs/samples/got-network.graphml +1384 -0
  446. package/docs/samples/got-nodes.csv +108 -0
  447. package/docs/samples/got.cx +930 -0
  448. package/docs/samples/got.cx2 +465 -0
  449. package/docs/samples/got.gexf +471 -0
  450. package/docs/samples/got.gml +2650 -0
  451. package/docs/samples/got.json +2546 -0
  452. package/docs/samples/got.net +461 -0
  453. package/docs/samples/movies-nodes.csv +9 -0
  454. package/docs/samples/movies-rels.csv +8 -0
  455. package/docs/samples/networks.cys +0 -0
  456. package/docs/samples/proteins.xgmml +9 -0
  457. package/docs/samples/teams.gv +24 -0
  458. package/docs/samples/vehicles.obo +25 -0
  459. package/package.json +9 -4
  460. package/src/children.ts +18 -6
  461. package/src/common/attributes.ts +29 -11
  462. package/src/common/cell-budget.ts +11 -3
  463. package/src/common/choose.ts +89 -0
  464. package/src/common/codes.ts +490 -66
  465. package/src/common/compare.ts +472 -0
  466. package/src/common/cx-export.ts +432 -0
  467. package/src/common/declared-types.ts +38 -4
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +33 -17
  470. package/src/common/export.ts +284 -47
  471. package/src/common/format.ts +30 -1
  472. package/src/common/ids.ts +14 -3
  473. package/src/common/input.ts +1057 -134
  474. package/src/common/json-elements.ts +1034 -198
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +6 -1
  477. package/src/common/ontology-export.ts +461 -0
  478. package/src/common/ontology.ts +31 -9
  479. package/src/common/options.ts +110 -15
  480. package/src/common/plural.ts +49 -0
  481. package/src/common/report.ts +168 -25
  482. package/src/common/temporal.ts +22 -5
  483. package/src/common/text.ts +62 -23
  484. package/src/common/weights.ts +74 -4
  485. package/src/common/writer.ts +37 -3
  486. package/src/common/xml.ts +477 -88
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +201 -46
  489. package/src/formats/csv/header.ts +50 -11
  490. package/src/formats/csv/importer.ts +1133 -245
  491. package/src/formats/csv/index.ts +106 -19
  492. package/src/formats/csv/records.ts +396 -122
  493. package/src/formats/csv/values.ts +1 -0
  494. package/src/formats/cx/exporter.ts +1483 -0
  495. package/src/formats/cx/importer.ts +888 -300
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +61 -338
  498. package/src/formats/cx2/importer.ts +317 -86
  499. package/src/formats/cx2/index.ts +2 -1
  500. package/src/formats/cys/constants.ts +62 -17
  501. package/src/formats/cys/exporter.ts +1368 -0
  502. package/src/formats/cys/importer.ts +659 -157
  503. package/src/formats/cys/index.ts +4 -2
  504. package/src/formats/cys/session.ts +139 -31
  505. package/src/formats/cys/tables.ts +47 -9
  506. package/src/formats/dot/exporter.ts +83 -37
  507. package/src/formats/dot/importer.ts +248 -54
  508. package/src/formats/dot/index.ts +2 -1
  509. package/src/formats/dot/names.ts +44 -11
  510. package/src/formats/dot/tokenizer.ts +25 -6
  511. package/src/formats/gexf/exporter.ts +57 -18
  512. package/src/formats/gexf/importer.ts +636 -105
  513. package/src/formats/gexf/index.ts +90 -21
  514. package/src/formats/gexf/schema.ts +67 -15
  515. package/src/formats/gml/exporter.ts +90 -36
  516. package/src/formats/gml/importer.ts +388 -67
  517. package/src/formats/gml/index.ts +85 -24
  518. package/src/formats/gml/syntax.ts +111 -26
  519. package/src/formats/graphml/constants.ts +178 -44
  520. package/src/formats/graphml/exporter.ts +52 -18
  521. package/src/formats/graphml/importer.ts +640 -60
  522. package/src/formats/graphml/index.ts +2 -1
  523. package/src/formats/graphml/tree.ts +6 -2
  524. package/src/formats/graphml/yfiles.ts +37 -15
  525. package/src/formats/json/dialect.ts +128 -29
  526. package/src/formats/json/exporter.ts +205 -68
  527. package/src/formats/json/importer.ts +1163 -253
  528. package/src/formats/json/index.ts +3 -1
  529. package/src/formats/json/obographs-export.ts +1197 -0
  530. package/src/formats/json/obographs.ts +353 -92
  531. package/src/formats/neo4j/exporter.ts +320 -101
  532. package/src/formats/neo4j/header.ts +44 -6
  533. package/src/formats/neo4j/importer.ts +512 -73
  534. package/src/formats/neo4j/index.ts +75 -23
  535. package/src/formats/obo/exporter.ts +2210 -0
  536. package/src/formats/obo/importer.ts +622 -109
  537. package/src/formats/obo/index.ts +6 -3
  538. package/src/formats/obo/syntax.ts +232 -37
  539. package/src/formats/pajek/exporter.ts +89 -41
  540. package/src/formats/pajek/importer.ts +407 -93
  541. package/src/formats/pajek/index.ts +2 -1
  542. package/src/formats/pajek/syntax.ts +213 -52
  543. package/src/formats/xgmml/columns.ts +10 -0
  544. package/src/formats/xgmml/constants.ts +183 -56
  545. package/src/formats/xgmml/document.ts +212 -34
  546. package/src/formats/xgmml/emit.ts +175 -34
  547. package/src/formats/xgmml/exporter.ts +35 -27
  548. package/src/formats/xgmml/importer.ts +64 -86
  549. package/src/formats/xgmml/index.ts +1 -0
  550. package/src/formats/xgmml/values.ts +11 -3
  551. package/src/index.ts +81 -11
  552. package/src/registry.ts +1121 -135
  553. package/src/sniff.ts +163 -32
  554. package/src/types.ts +385 -114
  555. package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
  556. package/dist/chunks/export-Bh60Dv-n.js +0 -582
  557. package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
  558. package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
  559. package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
  560. package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
  561. package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
  562. package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
  563. package/dist/chunks/importer-DET1tg6s.js.map +0 -1
  564. package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
  565. package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
  566. package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
  567. package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
  568. package/dist/chunks/importer-eUREhBGj.js.map +0 -1
  569. package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
  570. package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
  571. package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
  572. package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
  573. package/dist/chunks/ontology-BnrJ4I98.js +0 -113
  574. package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
  575. package/dist/chunks/records-dbRkxwaq.js.map +0 -1
  576. package/dist/chunks/report-B1z4WT9e.js +0 -2372
  577. package/dist/chunks/report-B1z4WT9e.js.map +0 -1
  578. package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
  579. package/dist/chunks/weights-Dzba96G3.js +0 -176
  580. package/dist/chunks/weights-Dzba96G3.js.map +0 -1
  581. package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
@@ -34,8 +34,10 @@ import {
34
34
  DANGLING_REFERENCE_CODE,
35
35
  DUPLICATE_ATTRIBUTE_CODE,
36
36
  DUPLICATE_NODE_CODE,
37
+ ELEMENT_ISSUE,
37
38
  EMPTY_INPUT_CODE,
38
39
  ENCODING_FALLBACK_CODE,
40
+ INPUT_ISSUE,
39
41
  INVALID_ENCODING_CODE,
40
42
  INVALID_UTF8_CODE,
41
43
  MISSING_ID_CODE,
@@ -54,6 +56,7 @@ import {
54
56
  resolveImportOptions,
55
57
  SINK_OPTION_CODE,
56
58
  } from "../../common/options.js";
59
+ import { agree, plural } from "../../common/plural.js";
57
60
  import { ImportReportBuilder } from "../../common/report.js";
58
61
  import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
59
62
  import {
@@ -62,30 +65,51 @@ import {
62
65
  parseXref,
63
66
  parseXrefList,
64
67
  type Qualifiers,
68
+ splitFormFeeds,
65
69
  splitQualifiers,
66
70
  splitTagValue,
67
71
  stripComment,
68
72
  type Token,
69
73
  tokenize,
74
+ unclosedBlock,
70
75
  unescapeObo,
71
76
  type Xref,
72
77
  } from "./syntax.js";
73
78
 
74
- /** The format-specific options of the OBO importer. */
75
- export interface OboImportOptions {
76
- /** "keep" (default): obsolete terms are nodes with `is_obsolete` true; "drop": they and their edges are left out. */
79
+ /**
80
+ * The format-specific options of the OBO importer.
81
+ * @category Built-in formats
82
+ */
83
+ export interface OboImportOptions extends CommonImportOptions {
84
+ /**
85
+ * "keep" reads obsolete terms as nodes with `is_obsolete` set to true; "drop" leaves them and
86
+ * their edges out.
87
+ * @defaultValue "keep"
88
+ */
77
89
  obsolete?: "keep" | "drop" | undefined;
78
- /** "metadata" (default): `[Typedef]` frames go to `meta.extra.obo.typedefs`; "nodes": they are nodes too, with their `is_a` edges. */
90
+ /**
91
+ * "metadata" keeps the `[Typedef]` frames (relation definitions) in
92
+ * `snapshot.meta.extra.obo.typedefs`; "nodes" makes them nodes too, with their `is_a` edges.
93
+ * @defaultValue "metadata"
94
+ */
79
95
  typedefs?: "metadata" | "nodes" | undefined;
80
96
  }
81
97
 
82
- /** Issue codes of the OBO importer (design section 4.3). */
98
+ /**
99
+ * Issue codes of the OBO importer.
100
+ * @category Built-in formats
101
+ */
83
102
  export const OBO_ISSUE = Object.freeze({
84
- /** The input is empty or whitespace (fatal). */
103
+ ...INPUT_ISSUE,
104
+ ...ELEMENT_ISSUE,
105
+ /** The input is empty or whitespace. The import stops. */
85
106
  EMPTY_INPUT: EMPTY_INPUT_CODE,
86
- /** The input holds invalid UTF-8 (fatal). */
107
+ /** The input is not valid UTF-8. The import stops. */
87
108
  INVALID_UTF8: INVALID_UTF8_CODE,
88
- /** Invalid bytes in the encoding a BOM or the encoding option chose (fatal). */
109
+ /**
110
+ * Some bytes are not valid in the encoding that was chosen (by a byte order mark or the `encoding` option). The
111
+ * import stops.
112
+ */
89
113
  INVALID_ENCODING: INVALID_ENCODING_CODE,
90
114
  /** Bytes that are not UTF-8 were read as windows-1252. */
91
115
  ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
@@ -95,9 +119,9 @@ export const OBO_ISSUE = Object.freeze({
95
119
  MISSING_ID: MISSING_ID_CODE,
96
120
  /** A clause whose value cannot be read (a boolean other than true / false, a relationship with one or three values); the clause is skipped. */
97
121
  BAD_VALUE: BAD_VALUE_CODE,
98
- /** Two frames with one id were merged (spec 4.1.1). */
122
+ /** Two frames have the same id, so they were merged into one node. */
99
123
  DUPLICATE_NODE: DUPLICATE_NODE_CODE,
100
- /** A single-valued tag given twice for one id (a cardinality violation); the first is kept. */
124
+ /** A tag that may appear once per frame appears twice; the first value is kept. */
101
125
  DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
102
126
  /** An unknown tag (kept in `obo.unrecognized`) or frame type (skipped), once per name. */
103
127
  UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
@@ -105,6 +129,16 @@ export const OBO_ISSUE = Object.freeze({
105
129
  DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
106
130
  /** A line without a colon, an unterminated quote, a def without its xref list, a qualifier block that does not parse, an unescaped brace. */
107
131
  SYNTAX: "W_OBO_SYNTAX",
132
+ /**
133
+ * Nothing in the input looks like OBO: there is no frame header and no header tag. It may be an HTML page, a JSON
134
+ * or GML file, or UTF-16 text without a byte order mark. The import stops.
135
+ */
136
+ NOT_OBO: "E_OBO_NOT_OBO",
137
+ /**
138
+ * The header has no `format-version` (OBO 1.2 and 1.4 require one), or names a version other than 1.0, 1.2 or 1.4.
139
+ * The file is read accepting the tags of every version.
140
+ */
141
+ FORMAT_VERSION: "W_OBO_FORMAT_VERSION",
108
142
  /** The frame's `id` is not its first clause; it is used anyway. */
109
143
  ID_NOT_FIRST: "W_OBO_ID_NOT_FIRST",
110
144
  /** A synonym without a scope in a file that does not say 1.2, or with a scope that is not one of the four. */
@@ -113,7 +147,7 @@ export const OBO_ISSUE = Object.freeze({
113
147
  UNDECLARED: "W_OBO_UNDECLARED",
114
148
  /** One id for a Term and a Typedef (or an Instance); the Term (the first node frame) is the node. */
115
149
  ID_KIND_CLASH: "W_OBO_ID_KIND_CLASH",
116
- /** Fewer than two `intersection_of` or `union_of` clauses on a frame. */
150
+ /** A frame has only one `intersection_of` or `union_of` clause, where OBO needs at least two. */
117
151
  CARDINALITY: "W_OBO_CARDINALITY",
118
152
  /** An obsolete term with is_a / relationship, or replaced_by / consider on a term that is not obsolete. */
119
153
  OBSOLETION: "W_OBO_OBSOLETION",
@@ -121,19 +155,31 @@ export const OBO_ISSUE = Object.freeze({
121
155
  DEPRECATED_TAG: "W_OBO_DEPRECATED_TAG",
122
156
  /** A backslash line continuation (deprecated in 1.4). */
123
157
  DEPRECATED_SYNTAX: "W_OBO_DEPRECATED_SYNTAX",
124
- /** A header clause kept in `meta.extra.obo.header` whose meaning is not applied (import, id-mapping, the treat-xrefs macros, owl-axioms). */
158
+ /**
159
+ * A header clause whose meaning graph-io does not apply (`import`, `id-mapping`, the `treat-xrefs` macros,
160
+ * `owl-axioms`); it is kept in `snapshot.meta.extra.obo.header`.
161
+ */
125
162
  HEADER_NOT_APPLIED: "W_OBO_HEADER_NOT_APPLIED",
126
163
  /** Obsolete terms and their edges left out under obsolete: "drop". */
127
164
  OBSOLETE_DROPPED: "W_OBO_OBSOLETE_DROPPED",
128
- /** Two distinct id texts became one number under ids "number". */
165
+ /**
166
+ * Two different id texts became the same number because `ids` is "number" (for example, "042" and "42"), so their nodes
167
+ * were merged.
168
+ */
129
169
  ID_MERGED: ID_MERGED_CODE,
130
- /** A vocabulary column renamed `<name>#obo` because the sink already holds the name. */
170
+ /** An OBO attribute was renamed `<name>#obo` because another attribute already has its name. */
131
171
  COLUMN_RENAMED: RENAMED_CODE,
132
- /** A vocabulary column declared without its role because the sink already holds it. */
172
+ /**
173
+ * You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
174
+ * a plain attribute.
175
+ */
133
176
  ROLE_TAKEN: ROLE_TAKEN_CODE,
134
- /** A common option the format has no use for (defaultDirected, weightFrom, nodeIdFrom, ...). */
177
+ /** You set an option this format does not use; it had no effect. The message names the option. */
135
178
  OPTION_IGNORED: OPTION_IGNORED_CODE,
136
- /** A builder-policy option the caller passed that the caller's sink does not use. */
179
+ /**
180
+ * You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
181
+ * `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
182
+ */
137
183
  SINK_OPTION: SINK_OPTION_CODE,
138
184
  });
139
185
 
@@ -149,8 +195,12 @@ const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof Commo
149
195
  "signal",
150
196
  "onProgress",
151
197
  "encoding",
198
+ "restoreMangledIds",
152
199
  ]);
153
200
 
201
+ /** The property_value relation under which the exporter keeps an id it rewrote (`sanitizeIds: "mangle"`). */
202
+ const ORIGINAL_ID = "graphty:originalId";
203
+
154
204
  const DEFAULTS: ImportFormatDefaults = { ids: "keep", defaultDirected: true, weightFrom: null };
155
205
 
156
206
  /** Frames between two checks of the cancellation signal. */
@@ -170,8 +220,11 @@ const DEPRECATED_HEADER: ReadonlyMap<string, string> = new Map([
170
220
  ["version", "data-version"],
171
221
  ]);
172
222
 
173
- /** The 1.0 / 1.2 frame tags read as their 1.4 meaning: tag to [1.4 tag, synonym scope]. */
174
- const DEPRECATED_TAGS: ReadonlyMap<string, readonly [string, string | null]> = new Map([
223
+ /**
224
+ * The 1.0 / 1.2 frame tags read as their 1.4 meaning: tag to [1.4 tag, synonym scope].
225
+ * @category Plugin helpers
226
+ */
227
+ export const DEPRECATED_TAGS: ReadonlyMap<string, readonly [string, string | null]> = new Map([
175
228
  ["exact_synonym", ["synonym", "EXACT"]],
176
229
  ["narrow_synonym", ["synonym", "NARROW"]],
177
230
  ["broad_synonym", ["synonym", "BROAD"]],
@@ -192,8 +245,11 @@ const BUILTIN_RELATIONS: ReadonlySet<string> = new Set([
192
245
  "intersection_of",
193
246
  ]);
194
247
 
195
- /** Single-valued text tags (first value kept). */
196
- const TEXT_TAGS: ReadonlySet<string> = new Set([
248
+ /**
249
+ * Single-valued text tags (first value kept).
250
+ * @category Plugin helpers
251
+ */
252
+ export const TEXT_TAGS: ReadonlySet<string> = new Set([
197
253
  "name",
198
254
  "namespace",
199
255
  "comment",
@@ -204,13 +260,19 @@ const TEXT_TAGS: ReadonlySet<string> = new Set([
204
260
  "inverse_of",
205
261
  ]);
206
262
 
207
- /** Single-valued boolean tags. */
208
- const BOOL_TAGS: ReadonlySet<string> = new Set(
263
+ /**
264
+ * Single-valued boolean tags.
265
+ * @category Plugin helpers
266
+ */
267
+ export const BOOL_TAGS: ReadonlySet<string> = new Set(
209
268
  Object.keys(OBO_NODE_COLUMNS).filter((name) => OBO_NODE_COLUMNS[name].dtype === "bool"),
210
269
  );
211
270
 
212
- /** Tags whose value is one id, collected in a list column. */
213
- const ID_LIST_TAGS: ReadonlySet<string> = new Set([
271
+ /**
272
+ * Tags whose value is one id, collected in a list column.
273
+ * @category Plugin helpers
274
+ */
275
+ export const ID_LIST_TAGS: ReadonlySet<string> = new Set([
214
276
  "alt_id",
215
277
  "subset",
216
278
  "replaced_by",
@@ -222,8 +284,87 @@ const ID_LIST_TAGS: ReadonlySet<string> = new Set([
222
284
  "disjoint_over",
223
285
  ]);
224
286
 
225
- /** Tags that only a Typedef frame may carry (elsewhere they are unrecognized). */
226
- const TYPEDEF_TAGS: ReadonlySet<string> = new Set([
287
+ /** Header tags the specification allows once (the first is kept). */
288
+ const SINGLE_HEADER_TAGS: ReadonlySet<string> = new Set([
289
+ "format-version",
290
+ "data-version",
291
+ "version",
292
+ "date",
293
+ "saved-by",
294
+ "auto-generated-by",
295
+ "default-namespace",
296
+ "ontology",
297
+ "default-relationship-id-prefix",
298
+ ]);
299
+
300
+ /** The format versions the importer knows (it reads their union). */
301
+ const KNOWN_VERSION = /^(GO_)?1\.[024]$/;
302
+
303
+ /** Tags whose value is an id, where a qualifier block cut short by a truncated file is dropped. */
304
+ const ID_VALUE_TAGS: ReadonlySet<string> = new Set([
305
+ ...ID_LIST_TAGS,
306
+ "is_a",
307
+ "instance_of",
308
+ "relationship",
309
+ "intersection_of",
310
+ ]);
311
+
312
+ /** Warnings listed per code before the rest are summed up in one (a file of 50k unknown tags). */
313
+ const MAX_TALLIES_PER_CODE = 100;
314
+
315
+ /**
316
+ * Whether a text holds a control character other than tab, line feed, form feed (a line end in
317
+ * the 1.4 grammar, text inside quotes) and carriage return.
318
+ * @param text - the text
319
+ * @returns true when it does
320
+ */
321
+ function hasControl(text: string): boolean {
322
+ for (const ch of text) {
323
+ if ((ch < " " && ch !== "\t" && ch !== "\n" && ch !== "\f" && ch !== "\r") || ch === "\x7f") {
324
+ return true;
325
+ }
326
+ }
327
+ return false;
328
+ }
329
+
330
+ /**
331
+ * Whether a tag could name an OBO tag: no whitespace, control characters or the punctuation of
332
+ * other formats (`{`, `"`, `<`, brackets, parentheses).
333
+ * @param tag - the tag
334
+ * @returns true for a plausible tag
335
+ */
336
+ function isTagName(tag: string): boolean {
337
+ return tag.length > 0 && !hasControl(tag) && !/[\s{}"<>[\]()]/.test(tag);
338
+ }
339
+
340
+ /**
341
+ * A record with no prototype, so a key named after an Object.prototype member (`__proto__`,
342
+ * `constructor`, `toString`) is an own key like any other.
343
+ * @returns the empty record
344
+ */
345
+ function ownRecord<T>(): Record<string, T> {
346
+ return Object.create(null) as Record<string, T>;
347
+ }
348
+
349
+ /**
350
+ * Append a value to the list of a key in a null-prototype record.
351
+ * @param map - the record
352
+ * @param key - the key
353
+ * @param value - the value
354
+ */
355
+ function append<T>(map: Record<string, T[]>, key: string, value: T): void {
356
+ if (Object.hasOwn(map, key)) {
357
+ map[key].push(value);
358
+ } else {
359
+ map[key] = [value];
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Tags that only a Typedef frame may carry (elsewhere they are unrecognized).
365
+ * @category Plugin helpers
366
+ */
367
+ export const TYPEDEF_TAGS: ReadonlySet<string> = new Set([
227
368
  "domain",
228
369
  "range",
229
370
  "inverse_of",
@@ -286,7 +427,7 @@ interface Where {
286
427
 
287
428
  /** A warning counted while reading and recorded once at the end. */
288
429
  interface Tally {
289
- readonly category: "parse-error" | "validation-error" | "coercion" | "unsupported";
430
+ readonly category: "parse-error" | "validation-error" | "coercion" | "unsupported" | "merged";
290
431
  readonly code: string;
291
432
  readonly element: string;
292
433
  readonly what: string;
@@ -343,7 +484,19 @@ class OboReader {
343
484
  private readonly signal: AbortSignal | null;
344
485
 
345
486
  /** Header clauses by tag, raw, in order. */
346
- private readonly header: Record<string, string[]> = {};
487
+ private readonly header: Record<string, string[]> = ownRecord<string[]>();
488
+
489
+ /** Whether `[Typedef]` frames are nodes (typedefs "nodes"). */
490
+ private readonly typedefsAsNodes: boolean;
491
+
492
+ /** Whether any frame header was read. */
493
+ private frameSeen = false;
494
+
495
+ /** Header clauses whose tag could be an OBO tag. */
496
+ private tagClauses = 0;
497
+
498
+ /** The first line that is not a frame header, a comment or a tag-value line with a plausible tag. */
499
+ private firstBadLine: number | null = null;
347
500
 
348
501
  private version: string | null = null;
349
502
 
@@ -377,10 +530,12 @@ class OboReader {
377
530
  * Create a reader.
378
531
  * @param report - the report
379
532
  * @param signal - the cancellation signal
533
+ * @param typedefsAsNodes - whether `[Typedef]` frames are nodes
380
534
  */
381
- constructor(report: ImportReportBuilder, signal: AbortSignal | null) {
535
+ constructor(report: ImportReportBuilder, signal: AbortSignal | null, typedefsAsNodes: boolean) {
382
536
  this.report = report;
383
537
  this.signal = signal;
538
+ this.typedefsAsNodes = typedefsAsNodes;
384
539
  }
385
540
 
386
541
  /**
@@ -397,32 +552,72 @@ class OboReader {
397
552
  if (t.startsWith("!")) {
398
553
  return;
399
554
  }
400
- const header = /^\[([^\]]*)\]\s*(?:!.*)?$/.exec(t);
401
- if (header !== null) {
402
- this.startFrame(header[1].trim(), line);
555
+ if (t.startsWith("[") && this.frameHeader(t, line)) {
403
556
  return;
404
557
  }
405
558
  // the comment goes first, so a colon inside it never makes `foo ! a:b` a tag-value line
406
559
  const tv = splitTagValue(stripComment(t));
407
- if (tv === null) {
408
- this.tally("parse-error", OBO_ISSUE.SYNTAX, "a line without a colon was skipped", "line", line);
560
+ if (tv === null || tv.tag.length === 0) {
561
+ this.firstBadLine ??= line;
562
+ const what = tv === null ? "a line without a colon was skipped" : "a line without a tag was skipped";
563
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, what, "line", line);
409
564
  return;
410
565
  }
411
566
  const clause: Clause = { tag: tv.tag, value: stripComment(tv.rest), line };
412
567
  if (this.frame === null) {
568
+ if (isTagName(tv.tag)) {
569
+ this.tagClauses++;
570
+ } else {
571
+ this.firstBadLine ??= line;
572
+ }
413
573
  this.headerClause(clause);
414
574
  } else {
415
575
  this.frame.clauses.push(clause);
416
576
  }
417
577
  }
418
578
 
579
+ /**
580
+ * Start a frame at a line opening with `[`: a well-formed header, or one damaged so that only a
581
+ * frame header can be meant (a known frame name, or no colon: `[Term`, `[Term] extra`), with a
582
+ * warning, so the clauses after it never merge into the frame before.
583
+ * @param t - the trimmed line
584
+ * @param line - its number
585
+ * @returns false when the line is not a frame header
586
+ */
587
+ private frameHeader(t: string, line: number): boolean {
588
+ const close = t.indexOf("]");
589
+ const name = (close < 0 ? t.slice(1) : t.slice(1, close)).trim();
590
+ const after = close < 0 ? "" : t.slice(close + 1).trim();
591
+ const clean = close >= 0 && (after.length === 0 || after.startsWith("!"));
592
+ if (!clean && !FRAME_KINDS.has(name) && t.includes(":")) {
593
+ return false;
594
+ }
595
+ if (!clean) {
596
+ const what =
597
+ close < 0
598
+ ? "a frame header without its closing ] starts the frame"
599
+ : "text after a frame header was ignored";
600
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, what, `[${name}]`, line);
601
+ }
602
+ // a known frame name, or a clean header with a name: anything else (`[`, `[{"id": 1}]`, a JSON
603
+ // array) is read as a damaged header but is no proof the input is OBO
604
+ const proof = FRAME_KINDS.has(name) || (clean && /^[A-Za-z]/.test(name) && isTagName(name));
605
+ if (!proof) {
606
+ this.firstBadLine ??= line;
607
+ }
608
+ this.startFrame(name, line, proof);
609
+ return true;
610
+ }
611
+
419
612
  /**
420
613
  * Start a frame, finishing the one before.
421
614
  * @param name - the frame type
422
615
  * @param line - the header's line
616
+ * @param proof - whether the header shows the input is OBO
423
617
  */
424
- private startFrame(name: string, line: number): void {
618
+ private startFrame(name: string, line: number, proof: boolean): void {
425
619
  this.finishFrame();
620
+ this.frameSeen ||= proof;
426
621
  const kind = FRAME_KINDS.has(name) ? (name as FrameKind) : null;
427
622
  if (kind === null) {
428
623
  this.tally(
@@ -441,14 +636,31 @@ class OboReader {
441
636
  * @param clause - the clause
442
637
  */
443
638
  private headerClause(clause: Clause): void {
444
- (this.header[clause.tag] ??= []).push(clause.value);
639
+ append(this.header, clause.tag, clause.value);
445
640
  const { tag } = clause;
641
+ if (SINGLE_HEADER_TAGS.has(tag) && this.header[tag].length > 1) {
642
+ this.tally(
643
+ "validation-error",
644
+ OBO_ISSUE.DUPLICATE_ATTRIBUTE,
645
+ "a single-valued header tag given twice; the first is kept",
646
+ tag,
647
+ clause.line,
648
+ );
649
+ }
446
650
  const tokens = tokenize(splitQualifiers(clause.value).value);
447
651
  const first = tokens.length > 0 && tokens[0].kind === "word" ? tokens[0].text : null;
448
652
  if (tag === "format-version") {
449
653
  this.version ??= unescapeObo(clause.value).trim();
450
654
  } else if (tag === "default-namespace") {
451
655
  this.defaultNamespace ??= first;
656
+ } else if ((tag === "subsetdef" || tag === "synonymtypedef") && first === null) {
657
+ this.tally(
658
+ "parse-error",
659
+ OBO_ISSUE.SYNTAX,
660
+ "a declaration whose name is not a word declares nothing",
661
+ tag,
662
+ clause.line,
663
+ );
452
664
  } else if (tag === "subsetdef" && first !== null) {
453
665
  this.subsets.add(first);
454
666
  } else if (tag === "synonymtypedef" && first !== null) {
@@ -479,9 +691,9 @@ class OboReader {
479
691
  }
480
692
  if (frame.kind === null) {
481
693
  // the guides: an unrecognized frame must survive, so its clauses are kept, raw
482
- const clauses: Record<string, string[]> = {};
694
+ const clauses = ownRecord<string[]>();
483
695
  for (const clause of frame.clauses) {
484
- (clauses[clause.tag] ??= []).push(clause.value);
696
+ append(clauses, clause.tag, clause.value);
485
697
  }
486
698
  this.unknownFrames.push({ type: frame.name, clauses });
487
699
  return;
@@ -491,7 +703,11 @@ class OboReader {
491
703
  throwIfAborted(this.signal);
492
704
  }
493
705
  const idAt = frame.clauses.findIndex((c) => c.tag === "id");
494
- const id = idAt < 0 ? "" : unescapeObo(splitQualifiers(frame.clauses[idAt].value).value).trim();
706
+ const idParts = idAt < 0 ? null : splitQualifiers(frame.clauses[idAt].value);
707
+ const rawId = idParts === null ? "" : idParts.value.trim();
708
+ const id = unescapeObo(rawId).trim();
709
+ // a Typedef is metadata unless typedefs "nodes": it would never have been a node
710
+ const wouldBeNode = frame.kind !== "Typedef" || this.typedefsAsNodes;
495
711
  if (id.length === 0) {
496
712
  this.report.error(
497
713
  "missing-value",
@@ -501,7 +717,23 @@ class OboReader {
501
717
  line: frame.line,
502
718
  },
503
719
  );
504
- this.report.counts.skippedNodes++;
720
+ if (wouldBeNode) {
721
+ this.report.counts.skippedNodes++;
722
+ }
723
+ return;
724
+ }
725
+ if (tokenize(rawId).length > 1 || hasControl(id)) {
726
+ // a reference cannot name such an id (is_a needs one word), so the frame is refused
727
+ // rather than made a node no edge can reach
728
+ this.report.error(
729
+ "validation-error",
730
+ OBO_ISSUE.BAD_VALUE,
731
+ `a [${frame.kind}] frame's id ${JSON.stringify(id)} holds unescaped whitespace or a control character; the frame is skipped`,
732
+ { line: frame.clauses[idAt].line, element: id },
733
+ );
734
+ if (wouldBeNode) {
735
+ this.report.counts.skippedNodes++;
736
+ }
505
737
  return;
506
738
  }
507
739
  if (idAt > 0) {
@@ -513,7 +745,8 @@ class OboReader {
513
745
  frame.clauses[idAt].line,
514
746
  );
515
747
  }
516
- const record = this.recordFor(frame.kind, id, frame.line);
748
+ const target = this.recordFor(frame.kind, id, frame.line);
749
+ this.extraQualifiers(target, "id", id, idParts?.qualifiers ?? null);
517
750
  for (let i = 0; i < frame.clauses.length; i++) {
518
751
  const clause = frame.clauses[i];
519
752
  if (clause.tag === "id") {
@@ -528,10 +761,10 @@ class OboReader {
528
761
  }
529
762
  continue;
530
763
  }
531
- this.applyClause(record, clause);
764
+ this.applyClause(target, clause);
532
765
  }
533
- if (record.kind === "Typedef") {
534
- record.raw.id ??= [id];
766
+ if (target.kind === "Typedef" && !Object.hasOwn(target.raw, "id")) {
767
+ target.raw.id = [id];
535
768
  }
536
769
  }
537
770
 
@@ -555,19 +788,17 @@ class OboReader {
555
788
  line,
556
789
  );
557
790
  } else {
558
- this.report.warning(
791
+ this.tally(
559
792
  "merged",
560
793
  OBO_ISSUE.DUPLICATE_NODE,
561
- `two [${kind}] frames have the id ${id}; they are merged (spec 4.1.1)`,
562
- {
563
- line,
564
- element: id,
565
- },
794
+ `[${kind}] frames with this id are merged (spec 4.1.1)`,
795
+ id,
796
+ line,
566
797
  );
567
798
  }
568
799
  return existing;
569
800
  }
570
- const record: NodeRecord = {
801
+ const created: NodeRecord = {
571
802
  id,
572
803
  kind,
573
804
  line,
@@ -575,10 +806,10 @@ class OboReader {
575
806
  lists: new Map(),
576
807
  edges: [],
577
808
  seen: new Set(),
578
- raw: {},
809
+ raw: ownRecord<string[]>(),
579
810
  };
580
- map.set(id, record);
581
- return record;
811
+ map.set(id, created);
812
+ return created;
582
813
  }
583
814
 
584
815
  /**
@@ -606,7 +837,18 @@ class OboReader {
606
837
  );
607
838
  [tag, scope] = deprecated;
608
839
  }
609
- const split = splitQualifiers(clause.value);
840
+ let split = splitQualifiers(clause.value);
841
+ const open = ID_VALUE_TAGS.has(tag) ? unclosedBlock(split.value) : -1;
842
+ if (open >= 0) {
843
+ this.tally(
844
+ "parse-error",
845
+ OBO_ISSUE.SYNTAX,
846
+ "a qualifier block without its closing brace was dropped",
847
+ tag,
848
+ clause.line,
849
+ );
850
+ split = splitQualifiers(split.value.slice(0, open).trimEnd());
851
+ }
610
852
  const { qualifiers } = split;
611
853
  if (split.badBlock !== null) {
612
854
  this.tally(
@@ -673,17 +915,17 @@ class OboReader {
673
915
  }
674
916
  record.seen.add(key);
675
917
  if (record.kind === "Typedef") {
676
- (record.raw[clause.tag] ??= []).push(clause.value);
918
+ append(record.raw, clause.tag, clause.value);
677
919
  }
678
920
  const { tag, scope, qualifiers, value } = this.clauseParts(clause);
921
+ // a control character kept in the value is the shared W_CONTROL_CHARACTER of the text check
679
922
  const where = { line: clause.line, element: record.id };
680
923
  if (TYPEDEF_TAGS.has(tag) && record.kind !== "Typedef") {
681
924
  this.unrecognized(record, clause);
682
925
  return;
683
926
  }
684
927
  if (TEXT_TAGS.has(tag)) {
685
- this.single(record, tag, unescapeObo(value).trim(), clause.line);
686
- this.extraQualifiers(record, tag, unescapeObo(value).trim(), qualifiers);
928
+ this.text(record, tag, value, qualifiers, clause.line);
687
929
  return;
688
930
  }
689
931
  if (BOOL_TAGS.has(tag)) {
@@ -797,7 +1039,7 @@ class OboReader {
797
1039
  return;
798
1040
  }
799
1041
  this.push(record, "xref", xref.id);
800
- this.describe(record, [xref]);
1042
+ this.describe(record, [xref], tag, clause.line);
801
1043
  this.extraQualifiers(record, tag, xref.id, qualifiers ?? xref.qualifiers);
802
1044
  return;
803
1045
  }
@@ -809,6 +1051,30 @@ class OboReader {
809
1051
  }
810
1052
  }
811
1053
 
1054
+ /**
1055
+ * A single-valued text clause (name, comment, ...), warning about a quote that never closes
1056
+ * (stripComment() then keeps a `!` comment after it as text).
1057
+ * @param record - the record
1058
+ * @param tag - the tag
1059
+ * @param value - the raw value
1060
+ * @param qualifiers - its qualifier block
1061
+ * @param line - its line
1062
+ */
1063
+ private text(record: NodeRecord, tag: string, value: string, qualifiers: Qualifiers | null, line: number): void {
1064
+ if (tokenize(value).some((t) => t.kind === "quoted" && t.unterminated)) {
1065
+ this.tally(
1066
+ "parse-error",
1067
+ OBO_ISSUE.SYNTAX,
1068
+ "an unterminated quote; the text after it, a comment included, is kept",
1069
+ tag,
1070
+ line,
1071
+ );
1072
+ }
1073
+ const text = unescapeObo(value).trim();
1074
+ this.single(record, tag, text, line);
1075
+ this.extraQualifiers(record, tag, text, qualifiers);
1076
+ }
1077
+
812
1078
  /**
813
1079
  * Read a value that must be one word.
814
1080
  * @param tokens - the value's tokens
@@ -865,8 +1131,14 @@ class OboReader {
865
1131
  this.tally("parse-error", OBO_ISSUE.SYNTAX, "the xref list is missing", tag, line);
866
1132
  return { text: tokens[0].text, xrefs: [] };
867
1133
  }
1134
+ if (list.unterminated) {
1135
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, "an xref list without its closing ]; kept as read", tag, line);
1136
+ }
1137
+ if (tokens.length > 2) {
1138
+ this.tally("parse-error", OBO_ISSUE.SYNTAX, "text after the xref list was ignored", tag, line);
1139
+ }
868
1140
  const xrefs = parseXrefList(list.text);
869
- this.describe(record, xrefs);
1141
+ this.describe(record, xrefs, tag, line);
870
1142
  this.xrefQualifiers(record, tag, xrefs);
871
1143
  return { text: tokens[0].text, xrefs: xrefs.map((x) => x.id) };
872
1144
  }
@@ -969,8 +1241,31 @@ class OboReader {
969
1241
  line,
970
1242
  );
971
1243
  }
1244
+ const listAt = list === undefined ? -1 : tokens.indexOf(list);
1245
+ const stray = tokens.some(
1246
+ (t, k) =>
1247
+ k > 0 && (t.kind === "quoted" || (t.kind === "list" && k !== listAt) || (listAt >= 0 && k > listAt)),
1248
+ );
1249
+ if (stray) {
1250
+ this.tally(
1251
+ "parse-error",
1252
+ OBO_ISSUE.SYNTAX,
1253
+ "a second quoted string, a second xref list or text after the list was ignored",
1254
+ "synonym",
1255
+ line,
1256
+ );
1257
+ }
1258
+ if (list?.unterminated === true) {
1259
+ this.tally(
1260
+ "parse-error",
1261
+ OBO_ISSUE.SYNTAX,
1262
+ "an xref list without its closing ]; kept as read",
1263
+ "synonym",
1264
+ line,
1265
+ );
1266
+ }
972
1267
  const xrefs = list === undefined ? [] : parseXrefList(list.text);
973
- this.describe(record, xrefs);
1268
+ this.describe(record, xrefs, "synonym", line);
974
1269
  this.xrefQualifiers(record, "synonym", xrefs);
975
1270
  this.push(
976
1271
  record,
@@ -1009,7 +1304,26 @@ class OboReader {
1009
1304
  );
1010
1305
  return;
1011
1306
  }
1012
- const datatype = tokens.length === 3 ? tokens[2].text : null;
1307
+ if (tokens.some((t) => t.unterminated)) {
1308
+ this.tally(
1309
+ "parse-error",
1310
+ OBO_ISSUE.SYNTAX,
1311
+ "an unterminated quote; the text runs to the end of the line",
1312
+ "property_value",
1313
+ where.line,
1314
+ );
1315
+ }
1316
+ let datatype = tokens.length === 3 ? tokens[2].text : null;
1317
+ if (tokens.length === 3 && tokens[2].kind !== "word") {
1318
+ this.tally(
1319
+ "parse-error",
1320
+ OBO_ISSUE.SYNTAX,
1321
+ "a datatype that is not a word was ignored",
1322
+ "property_value",
1323
+ where.line,
1324
+ );
1325
+ datatype = null;
1326
+ }
1013
1327
  this.push(
1014
1328
  record,
1015
1329
  "property_value",
@@ -1059,16 +1373,33 @@ class OboReader {
1059
1373
  }
1060
1374
 
1061
1375
  /**
1062
- * Record xref descriptions in `xref.descriptions`.
1063
- * @param record - the record
1376
+ * Record xref descriptions in `xref.descriptions` (the first description of an id is kept),
1377
+ * warning about a description whose quote never closes.
1378
+ * @param target - the record
1064
1379
  * @param xrefs - the xrefs
1380
+ * @param tag - the clause's tag
1381
+ * @param line - the clause's line
1065
1382
  */
1066
- private describe(record: NodeRecord, xrefs: readonly Xref[]): void {
1383
+ private describe(target: NodeRecord, xrefs: readonly Xref[], tag: string, line: number): void {
1067
1384
  for (const xref of xrefs) {
1385
+ if (xref.unterminated) {
1386
+ this.tally(
1387
+ "parse-error",
1388
+ OBO_ISSUE.SYNTAX,
1389
+ "an xref description whose quote never closes runs to the end",
1390
+ tag,
1391
+ line,
1392
+ );
1393
+ }
1068
1394
  if (xref.description !== null) {
1069
- const map = (record.values.get("xref.descriptions") ?? {}) as Record<string, string>;
1070
- map[xref.id] ??= xref.description;
1071
- record.values.set("xref.descriptions", map);
1395
+ let map = target.values.get("xref.descriptions") as Record<string, string> | undefined;
1396
+ if (map === undefined) {
1397
+ map = ownRecord<string>();
1398
+ target.values.set("xref.descriptions", map);
1399
+ }
1400
+ if (!Object.hasOwn(map, xref.id)) {
1401
+ map[xref.id] = xref.description;
1402
+ }
1072
1403
  }
1073
1404
  }
1074
1405
  }
@@ -1097,7 +1428,7 @@ class OboReader {
1097
1428
  if (qualifiers === null) {
1098
1429
  return;
1099
1430
  }
1100
- const map = (record.values.get("obo.qualifiers") ?? {}) as Record<string, unknown[]>;
1431
+ const map = (record.values.get("obo.qualifiers") as Record<string, unknown[]> | undefined) ?? {};
1101
1432
  (map[tag] ??= []).push({ value, qualifiers });
1102
1433
  record.values.set("obo.qualifiers", map);
1103
1434
  }
@@ -1108,9 +1439,12 @@ class OboReader {
1108
1439
  * @param clause - the clause
1109
1440
  */
1110
1441
  private unrecognized(record: NodeRecord, clause: Clause): void {
1111
- const map = (record.values.get("obo.unrecognized") ?? {}) as Record<string, string[]>;
1112
- (map[clause.tag] ??= []).push(unescapeObo(clause.value));
1113
- record.values.set("obo.unrecognized", map);
1442
+ let map = record.values.get("obo.unrecognized") as Record<string, string[]> | undefined;
1443
+ if (map === undefined) {
1444
+ map = ownRecord<string[]>();
1445
+ record.values.set("obo.unrecognized", map);
1446
+ }
1447
+ append(map, clause.tag, unescapeObo(clause.value));
1114
1448
  this.tally(
1115
1449
  "validation-error",
1116
1450
  OBO_ISSUE.UNKNOWN_ELEMENT,
@@ -1138,22 +1472,92 @@ class OboReader {
1138
1472
  }
1139
1473
  }
1140
1474
 
1141
- /** Record the counted warnings, in the order they were first seen. */
1475
+ /**
1476
+ * Record the counted warnings, in the order they were first seen; past MAX_TALLIES_PER_CODE
1477
+ * of one code, the rest are summed up in one warning.
1478
+ */
1142
1479
  flushTallies(): void {
1480
+ const listed = new Map<string, number>();
1481
+ const over = new Map<string, Tally[]>();
1143
1482
  for (const t of this.tallies.values()) {
1483
+ const n = (listed.get(t.code) ?? 0) + 1;
1484
+ listed.set(t.code, n);
1485
+ if (n > MAX_TALLIES_PER_CODE) {
1486
+ const rest = over.get(t.code) ?? [];
1487
+ rest.push(t);
1488
+ over.set(t.code, rest);
1489
+ continue;
1490
+ }
1144
1491
  this.report.warning(
1145
1492
  t.category,
1146
1493
  t.code,
1147
- `${t.element}: ${t.what} (${t.count} time(s), first at line ${t.line})`,
1494
+ `${t.element}: ${t.what} (${t.count} time${plural(t.count)}, first at line ${t.line})`,
1148
1495
  {
1149
1496
  line: t.line,
1150
1497
  element: t.element,
1151
1498
  },
1152
1499
  );
1153
1500
  }
1501
+ for (const [code, rest] of over) {
1502
+ this.report.warning(
1503
+ rest[0].category,
1504
+ code,
1505
+ `${rest.length} more element${plural(rest.length)} with this warning, not listed (first: ${rest[0].element}: ${rest[0].what}, line ${rest[0].line})`,
1506
+ { line: rest[0].line },
1507
+ );
1508
+ }
1154
1509
  this.tallies.clear();
1155
1510
  }
1156
1511
 
1512
+ /**
1513
+ * The first line that makes the input not OBO: null unless no frame header and no plausible
1514
+ * header tag was read while some line was neither (an HTML page, a JSON, GML, DOT or CSV file,
1515
+ * UTF-16 read without its BOM). A file of only comments is OBO, if an empty one.
1516
+ * @returns the line, or null
1517
+ */
1518
+ notObo(): number | null {
1519
+ return this.frameSeen || this.tagClauses > 0 ? null : this.firstBadLine;
1520
+ }
1521
+
1522
+ /**
1523
+ * Warn about the header once the file is read: a missing or unknown format-version (the
1524
+ * synonym-scope rule depends on it), a date that is not a real calendar date.
1525
+ */
1526
+ checkHeader(): void {
1527
+ if (this.version === null) {
1528
+ const empty = this.frameSeen ? "" : " and the file has no frames: nothing was read";
1529
+ this.tally(
1530
+ "validation-error",
1531
+ OBO_ISSUE.FORMAT_VERSION,
1532
+ `the header has no format-version (required by 1.2 and 1.4)${empty}; read as the 1.0 / 1.2 / 1.4 union`,
1533
+ "format-version",
1534
+ 1,
1535
+ );
1536
+ } else if (!KNOWN_VERSION.test(this.version)) {
1537
+ this.tally(
1538
+ "validation-error",
1539
+ OBO_ISSUE.FORMAT_VERSION,
1540
+ `format-version ${JSON.stringify(this.version)} is not 1.0, 1.2 or 1.4; read as 1.4`,
1541
+ "format-version",
1542
+ 1,
1543
+ );
1544
+ }
1545
+ const date = this.header.date?.[0];
1546
+ if (
1547
+ date !== undefined &&
1548
+ /^\d{2}:\d{2}:\d{4}\s+\d{2}:\d{2}$/.test(date.trim()) &&
1549
+ oboDate(date.trim()) === null
1550
+ ) {
1551
+ this.tally(
1552
+ "validation-error",
1553
+ OBO_ISSUE.SYNTAX,
1554
+ "the header date is not a real calendar date; created is null",
1555
+ "date",
1556
+ 1,
1557
+ );
1558
+ }
1559
+ }
1560
+
1157
1561
  /**
1158
1562
  * The header as GraphMeta fields (design 4.2): name from `ontology`, the version, `created` from
1159
1563
  * the `dd:MM:yyyy HH:mm` date, `creator` from `saved-by`.
@@ -1211,7 +1615,9 @@ function oboDate(text: string | null): string | null {
1211
1615
  return null;
1212
1616
  }
1213
1617
  const [, dd, mm, yyyy, hh, min] = m;
1214
- if (Number(mm) < 1 || Number(mm) > 12 || Number(dd) < 1 || Number(dd) > 31 || Number(hh) > 23 || Number(min) > 59) {
1618
+ // a real calendar date: 31:02 and 31:04 do not survive the round trip through Date
1619
+ const day = new Date(Date.UTC(Number(yyyy), Number(mm) - 1, Number(dd)));
1620
+ if (Number(mm) < 1 || Number(mm) > 12 || day.getUTCDate() !== Number(dd) || Number(hh) > 23 || Number(min) > 59) {
1215
1621
  return null;
1216
1622
  }
1217
1623
  return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
@@ -1339,12 +1745,12 @@ function planGraph(
1339
1745
  }
1340
1746
  }
1341
1747
  reader.flushTallies();
1342
- reportDangling(records, dangling, addMissingNodes, danglingEdges, report);
1748
+ reportDangling(records, dangling, addMissingNodes, danglingEdges, report, new Set(reader.typedefs.keys()));
1343
1749
  if (dropped.size > 0) {
1344
1750
  report.warning(
1345
1751
  "merged",
1346
1752
  OBO_ISSUE.OBSOLETE_DROPPED,
1347
- `${dropped.size} obsolete term(s) and ${droppedEdges} edge(s) to or from them were left out (obsolete: "drop")`,
1753
+ `${dropped.size} obsolete term${plural(dropped.size)} and ${droppedEdges} edge${plural(droppedEdges)} to or from them were left out (obsolete: "drop")`,
1348
1754
  { element: [...dropped][0] },
1349
1755
  );
1350
1756
  }
@@ -1363,6 +1769,7 @@ function planGraph(
1363
1769
  * @param addMissingNodes - whether they became placeholder nodes
1364
1770
  * @param droppedEdges - the edges dropped under addMissingNodes false
1365
1771
  * @param report - the report
1772
+ * @param typedefs - the ids of the Typedef frames (kept as metadata, so not nodes)
1366
1773
  */
1367
1774
  function reportDangling(
1368
1775
  records: readonly NodeRecord[],
@@ -1370,6 +1777,7 @@ function reportDangling(
1370
1777
  addMissingNodes: boolean,
1371
1778
  droppedEdges: number,
1372
1779
  report: ImportReportBuilder,
1780
+ typedefs: ReadonlySet<string>,
1373
1781
  ): void {
1374
1782
  if (dangling.length === 0) {
1375
1783
  return;
@@ -1382,16 +1790,19 @@ function reportDangling(
1382
1790
  }
1383
1791
  const shown = dangling.slice(0, 5).map((id) => {
1384
1792
  const primary = altOf.get(id);
1793
+ if (typedefs.has(id)) {
1794
+ return `${id} (a Typedef, kept as metadata, not a node)`;
1795
+ }
1385
1796
  return primary === undefined ? id : `${id} (an alt_id of ${primary})`;
1386
1797
  });
1387
1798
  const more = dangling.length > 5 ? `, ... (${dangling.length - 5} more)` : "";
1388
1799
  const action = addMissingNodes
1389
1800
  ? "each became a placeholder node (graphty.placeholder)"
1390
- : `${droppedEdges} edge(s) to them were dropped (addMissingNodes false)`;
1801
+ : `${droppedEdges} edge${plural(droppedEdges)} to them ${agree(droppedEdges, "was", "were")} dropped (addMissingNodes false)`;
1391
1802
  report.warning(
1392
1803
  "validation-error",
1393
1804
  OBO_ISSUE.DANGLING_REFERENCE,
1394
- `${dangling.length} target(s) no frame declares: ${shown.join(", ")}${more}; ${action}`,
1805
+ `${dangling.length} target${plural(dangling.length)} no frame declares: ${shown.join(", ")}${more}; ${action}`,
1395
1806
  { element: dangling[0] },
1396
1807
  );
1397
1808
  }
@@ -1406,6 +1817,9 @@ class Pusher {
1406
1817
 
1407
1818
  private readonly signal: AbortSignal | null;
1408
1819
 
1820
+ /** The original ids restored under restoreMangledIds, by the id the file wrote. */
1821
+ readonly restored = new Map<string, string>();
1822
+
1409
1823
  private since = 0;
1410
1824
 
1411
1825
  /**
@@ -1427,7 +1841,7 @@ class Pusher {
1427
1841
  * @returns the node id
1428
1842
  */
1429
1843
  id(text: string): NodeId {
1430
- const id = this.coercer.text(text);
1844
+ const id = this.coercer.text(this.restored.get(text) ?? text);
1431
1845
  const merge = this.coercer.lastMerge;
1432
1846
  if (merge !== null) {
1433
1847
  this.report.warning(
@@ -1490,6 +1904,9 @@ function writeGraph(
1490
1904
  ): void {
1491
1905
  const plan = planGraph(reader, report, options.addMissingNodes, obo);
1492
1906
  const push = new Pusher(sink, report, options);
1907
+ if (options.restoreMangledIds) {
1908
+ restoreIds(plan.nodes, push.restored);
1909
+ }
1493
1910
  const namespace = reader.namespaceDefault();
1494
1911
  // columns, declared only when used, in the vocabulary's order
1495
1912
  const used = new Set<string>(["type"]);
@@ -1536,6 +1953,50 @@ function writeGraph(
1536
1953
  writeEdges(plan, push, options);
1537
1954
  }
1538
1955
 
1956
+ /**
1957
+ * Take back the ids the exporter rewrote under `sanitizeIds: "mangle"`: a frame's
1958
+ * `property_value: graphty:originalId "..."` names its original id, which replaces the written one
1959
+ * wherever the file names it (the property value itself is dropped). An original that another
1960
+ * frame already uses as its id is not restored.
1961
+ * @param records - the node records
1962
+ * @param restored - receives written id to original id
1963
+ */
1964
+ function restoreIds(records: readonly NodeRecord[], restored: Map<string, string>): void {
1965
+ const taken = new Set(records.map((r) => r.id));
1966
+ for (const record of records) {
1967
+ const values = record.lists.get("property_value") as { relation: string; value: string }[] | undefined;
1968
+ const at = values?.findIndex((v) => v.relation === ORIGINAL_ID) ?? -1;
1969
+ if (values === undefined || at < 0) {
1970
+ continue;
1971
+ }
1972
+ const original = originalText(values[at].value);
1973
+ if (taken.has(original)) {
1974
+ continue;
1975
+ }
1976
+ taken.add(original);
1977
+ restored.set(record.id, original);
1978
+ values.splice(at, 1);
1979
+ if (values.length === 0) {
1980
+ record.lists.delete("property_value");
1981
+ }
1982
+ }
1983
+ }
1984
+
1985
+ /**
1986
+ * The original id of a `graphty:originalId` property value: the exporter writes it as JSON text (so
1987
+ * every character survives); a value that is not a JSON string is the id as it stands.
1988
+ * @param value - the property value
1989
+ * @returns the original id
1990
+ */
1991
+ function originalText(value: string): string {
1992
+ try {
1993
+ const parsed: unknown = JSON.parse(value);
1994
+ return typeof parsed === "string" ? parsed : value;
1995
+ } catch {
1996
+ return value;
1997
+ }
1998
+ }
1999
+
1539
2000
  /**
1540
2001
  * Push the planned edges through the direction resolver, with their relation and qualifiers.
1541
2002
  * @param plan - the plan
@@ -1576,6 +2037,71 @@ function writeEdges(plan: GraphPlan, push: Pusher, options: ReturnType<typeof re
1576
2037
  }
1577
2038
  }
1578
2039
 
2040
+ /**
2041
+ * Feed the physical lines to the reader as logical lines: backslash continuations joined (the parts
2042
+ * collected and joined once, so a value continued over many lines costs its length), form feeds
2043
+ * outside quotes split. A backslash inside a `!` comment never continues a line, and a continuation
2044
+ * never swallows a frame header or a blank line.
2045
+ * @param lines - the line reader
2046
+ * @param reader - the OBO reader
2047
+ */
2048
+ async function readLines(lines: LineReader, reader: OboReader): Promise<void> {
2049
+ const parts: string[] = [];
2050
+ let partLine = 0;
2051
+ const feed = (text: string, line: number): void => {
2052
+ for (const piece of splitFormFeeds(text)) {
2053
+ reader.line(piece, line);
2054
+ }
2055
+ };
2056
+ for await (const physical of lines) {
2057
+ const { line } = lines;
2058
+ if (parts.length > 0) {
2059
+ const head = physical.trimStart();
2060
+ if (head.length === 0 || head.startsWith("[")) {
2061
+ reader.tally(
2062
+ "parse-error",
2063
+ OBO_ISSUE.SYNTAX,
2064
+ "a line continuation before a frame header or a blank line was ignored",
2065
+ "\\",
2066
+ partLine,
2067
+ );
2068
+ feed(parts.join(""), partLine);
2069
+ parts.length = 0;
2070
+ } else {
2071
+ reader.tally(
2072
+ "coercion",
2073
+ OBO_ISSUE.DEPRECATED_SYNTAX,
2074
+ "a backslash line continuation was joined (deprecated in 1.4)",
2075
+ "\\",
2076
+ partLine,
2077
+ );
2078
+ }
2079
+ }
2080
+ if (endsWithContinuation(physical) && stripComment(physical) === physical) {
2081
+ parts.push(physical.slice(0, -1));
2082
+ partLine = line;
2083
+ continue;
2084
+ }
2085
+ if (parts.length > 0) {
2086
+ parts.push(physical);
2087
+ feed(parts.join(""), line);
2088
+ parts.length = 0;
2089
+ } else {
2090
+ feed(physical, line);
2091
+ }
2092
+ }
2093
+ if (parts.length > 0) {
2094
+ reader.tally(
2095
+ "coercion",
2096
+ OBO_ISSUE.DEPRECATED_SYNTAX,
2097
+ "a backslash line continuation was joined (deprecated in 1.4)",
2098
+ "\\",
2099
+ partLine,
2100
+ );
2101
+ feed(parts.join(""), partLine);
2102
+ }
2103
+ }
2104
+
1579
2105
  /**
1580
2106
  * Confidence that a head is OBO (design 1.5): after a BOM, blank lines and `!` comment lines, 0.9
1581
2107
  * when the first significant line is `format-version:` or a `[Term]` / `[Typedef]` / `[Instance]`
@@ -1615,10 +2141,12 @@ function sniffObo(head: Uint8Array): number {
1615
2141
  }
1616
2142
 
1617
2143
  /**
1618
- * The OBO importer plugin (design section 4).
2144
+ * The OBO importer plugin.
2145
+ * @category Built-in formats
1619
2146
  */
1620
2147
  export const oboImporter: GraphImporter<OboImportOptions> = Object.freeze({
1621
2148
  format: "obo",
2149
+ options: Object.freeze(["obsolete", "typedefs"]),
1622
2150
  extensions: Object.freeze([".obo"]),
1623
2151
  mimeTypes: Object.freeze(["text/obo", "application/obo"]),
1624
2152
 
@@ -1648,39 +2176,24 @@ export const oboImporter: GraphImporter<OboImportOptions> = Object.freeze({
1648
2176
  const report = new ImportReportBuilder("obo", resolved.errorLimit);
1649
2177
  reportSinkOptions(sink, options, report, true);
1650
2178
  reportUnusedOptions(options, report, USED_OPTIONS);
1651
- const reader = new OboReader(report, resolved.signal);
1652
- const lines = new LineReader(input, report, resolved);
1653
- let pending: string | null = null;
1654
- for await (const physical of lines) {
1655
- const { line } = lines;
1656
- const text: string = pending === null ? physical : pending + physical;
1657
- pending = null;
1658
- if (endsWithContinuation(text)) {
1659
- reader.tally(
1660
- "coercion",
1661
- OBO_ISSUE.DEPRECATED_SYNTAX,
1662
- "a backslash line continuation was joined (deprecated in 1.4)",
1663
- "\\",
1664
- line,
1665
- );
1666
- pending = text.slice(0, -1);
1667
- continue;
1668
- }
1669
- // the 1.4 grammar counts form feed as a line end; LineReader does not
1670
- for (const piece of text.includes("\f") ? text.split("\f") : [text]) {
1671
- reader.line(piece, line);
1672
- }
1673
- }
1674
- if (pending !== null) {
1675
- reader.line(pending, lines.line);
1676
- }
2179
+ const reader = new OboReader(report, resolved.signal, obo.typedefs === "nodes");
2180
+ await readLines(new LineReader(input, report, resolved), reader);
1677
2181
  reader.finishFrame();
1678
2182
  if (!reader.significant) {
1679
2183
  report.fail(OBO_ISSUE.EMPTY_INPUT, "the input is empty");
1680
2184
  }
2185
+ const notObo = reader.notObo();
2186
+ if (notObo !== null) {
2187
+ report.fail(
2188
+ OBO_ISSUE.NOT_OBO,
2189
+ `the input is not OBO: it has no frame header and no header tag, and line ${notObo} is not a tag-value line`,
2190
+ { line: notObo },
2191
+ );
2192
+ }
1681
2193
  throwIfAborted(resolved.signal);
2194
+ reader.checkHeader();
1682
2195
  writeGraph(reader, sink, report, resolved, obo);
1683
- const typedefs: Record<string, Record<string, string[]>> = {};
2196
+ const typedefs = ownRecord<Record<string, string[]>>();
1684
2197
  for (const [id, record] of reader.typedefs) {
1685
2198
  typedefs[id] = record.raw;
1686
2199
  }