@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
@@ -0,0 +1,1736 @@
1
+ /**
2
+ * The OBO exporter: writes a snapshot as an OBO 1.4 flat file, the text format of the Gene
3
+ * Ontology and the OBO Foundry ontologies.
4
+ *
5
+ * Every node is a frame (`[Term]`, or `[Instance]` / `[Typedef]` from the `type` column) in node
6
+ * order; every edge is a clause on its source's frame: `is_a: T` for the relation `is_a`,
7
+ * `instance_of: T` on an Instance, `relationship: R T` for anything else, an edge without a
8
+ * relation as `is_a` (the `relation` option). The columns of the OBO vocabulary
9
+ * (src/common/ontology.ts: `name`, `namespace`, `def`, `synonym`, `xref`, ...) are written back
10
+ * as their tags; any other node column becomes `property_value: <column> "<value>" xsd:<type>`
11
+ * lines (with a `[Typedef]` declaring the column as a metadata relation), any other edge column a
12
+ * qualifier of the edge's clause. A placeholder node (`graphty.placeholder`, a target no frame
13
+ * declared) gets no frame, so the re-import makes it again. The header, the Typedefs and the
14
+ * unknown frames an OBO import kept in `meta.extra.obo` are written back.
15
+ *
16
+ * check() predicts every difference the OBO importer produces on re-import: a vocabulary column
17
+ * whose values the file cannot carry exactly is written as `property_value` lines instead and
18
+ * reported, never written half right.
19
+ */
20
+ import { GraphFormatError } from "@graphty/graph-format";
21
+ import { COLUMN_AS_PROPERTY_VALUE_CODE, GRAPH_COLUMN_AS_METADATA_CODE, NODE_ORDER_CODE, RELATION_ASSUMED_CODE, ROLE_ASSUMED_CODE, TYPEDEF_NODES_CODE, } from "../../common/codes.js";
22
+ import { pairFolding } from "../../common/direction.js";
23
+ import { capabilities, LOSS } from "../../common/export.js";
24
+ import { formatNumber } from "../../common/format.js";
25
+ import { OBO_NODE_COLUMNS, PLACEHOLDER_COLUMN, SYNONYM_SCOPES } from "../../common/ontology.js";
26
+ import { capabilityNotes, cellOf, emptyColumnNotes, fitsVocabulary, FRAME_KINDS, framelessNodes, isQualifierRecord, movedNodes, notePropertyColumn, slotColumn, textOf, UNWRITTEN_ROLES, } from "../../common/ontology-export.js";
27
+ import { resolveExportOptions } from "../../common/options.js";
28
+ import { agree, plural } from "../../common/plural.js";
29
+ import { explicitWeights } from "../../common/weights.js";
30
+ import { encodeChunks, joinText } from "../../common/writer.js";
31
+ import { BOOL_TAGS, DEPRECATED_TAGS, ID_LIST_TAGS, TEXT_TAGS, TYPEDEF_TAGS } from "./importer.js";
32
+ import { escapeOboQuoted, escapeOboValue, escapeOboWord, hasLineEnd, isOboWord, isQualifierName, NOT_IN_WORD, qualifierBlock, tokenize, } from "./syntax.js";
33
+ /**
34
+ * The loss notes the OBO exporter's check() returns, by name. A key is the code without its
35
+ * severity and format prefixes.
36
+ * @category Built-in formats
37
+ */
38
+ export const OBO_LOSS = Object.freeze({
39
+ /** A node column outside the OBO vocabulary (or one whose values a tag cannot carry exactly) reads back inside the `property_value` column. */
40
+ COLUMN_AS_PROPERTY_VALUE: COLUMN_AS_PROPERTY_VALUE_CODE,
41
+ /** An edge column (or the explicit weights) reads back inside the `qualifiers` column. */
42
+ EDGE_COLUMN_AS_QUALIFIER: "W_OBO_EDGE_COLUMN_AS_QUALIFIER",
43
+ /** Every edge is written from its source to its target, so an undirected graph reads back as directed. */
44
+ UNDIRECTED_AS_DIRECTED: "W_OBO_UNDIRECTED_AS_DIRECTED",
45
+ /** Two identical clauses on one frame (parallel edges with one relation and the same qualifiers, a repeated list item) read back as one. */
46
+ DUPLICATE_CLAUSE: "W_OBO_DUPLICATE_CLAUSE",
47
+ /** An edge without a relation is written with the `relation` option (default `is_a`). */
48
+ RELATION_ASSUMED: RELATION_ASSUMED_CODE,
49
+ /** A relation that is not an OBO id (empty, or holding a space, `!`, `{` or `}`) is written with `_` in place of those characters. */
50
+ RELATION_RENAMED: "W_OBO_RELATION_RENAMED",
51
+ /** The graph name is not an ontology id; the header `ontology` is written with `_` in place of the other characters. */
52
+ ONTOLOGY_NAME: "W_OBO_ONTOLOGY_NAME",
53
+ /** `[Typedef]` frames read back as nodes only under the importer's `typedefs: "nodes"`; by default they are metadata. */
54
+ TYPEDEF_NODES: TYPEDEF_NODES_CODE,
55
+ /** Edges are written in the frame of their source node, so they read back grouped by source, in node order. */
56
+ EDGE_ORDER: "W_OBO_EDGE_ORDER",
57
+ /**
58
+ * A graph attribute is written as a header `property_value` and reads back in `snapshot.meta.extra.obo.header`,
59
+ * not as a graph attribute.
60
+ */
61
+ GRAPH_COLUMN_AS_METADATA: GRAPH_COLUMN_AS_METADATA_CODE,
62
+ /** A carriage return or form feed in a text cannot be written; it is written as a line feed. */
63
+ LINE_END: "W_OBO_LINE_END",
64
+ /** The nodes read back in a different order. */
65
+ NODE_ORDER: NODE_ORDER_CODE,
66
+ /** A mutual pair is written as two clauses without its mark. */
67
+ MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
68
+ /**
69
+ * The undirected edges of a mixed graph are written the way `onMixedDirection` chose ("directed" or "undirected").
70
+ */
71
+ MIXED_DIRECTION: LOSS.MIXED_DIRECTION,
72
+ /**
73
+ * The graph has both directed and undirected edges and `onMixedDirection` is "error". An import stops; a save to a
74
+ * format that holds one direction per file fails with E_DIRECTED. Pass "directed" or "undirected" to read or write
75
+ * it anyway.
76
+ */
77
+ MIXED_DIRECTION_ERROR: LOSS.MIXED_DIRECTION_ERROR,
78
+ /** OBO has no edge ids. */
79
+ EDGE_IDS_DROPPED: LOSS.EDGE_IDS_DROPPED,
80
+ /** OBO has no positions. */
81
+ POSITIONS_DROPPED: LOSS.POSITIONS,
82
+ /** OBO has no containment. */
83
+ HIERARCHY_DROPPED: LOSS.HIERARCHY,
84
+ /** OBO has no time. */
85
+ TEMPORAL_DROPPED: LOSS.TEMPORAL,
86
+ /** OBO has no visual columns. */
87
+ VIZ_DROPPED: LOSS.VIZ,
88
+ /** An attribute with a role the format has no place for is written as a plain attribute; the role is lost. */
89
+ ROLE_DROPPED: LOSS.ROLE,
90
+ /**
91
+ * An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
92
+ * reads back with that role.
93
+ */
94
+ ROLE_ASSUMED: ROLE_ASSUMED_CODE,
95
+ /**
96
+ * An attribute with a role (for example, the label) is written where the format keeps that role, and reads back under the
97
+ * name the format's importer gives it.
98
+ */
99
+ COLUMN_NAME_CHANGED: LOSS.COLUMN_NAME_CHANGED,
100
+ /**
101
+ * An OBO attribute stored as text where graph-io uses a dictionary (or the other way round); it reads back with
102
+ * graph-io's usual type. The values are the same.
103
+ */
104
+ DTYPE_UNSUPPORTED: LOSS.DTYPE,
105
+ /** A column without a value on any written element reads back absent. */
106
+ EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
107
+ /** A declared default: OBO has none. */
108
+ DEFAULT_DROPPED: LOSS.DEFAULT,
109
+ /** Declared options: OBO has no enumerations. */
110
+ OPTIONS_DROPPED: LOSS.OPTIONS,
111
+ /** An extension table OBO cannot carry. */
112
+ EXTENSION_TABLE_DROPPED: LOSS.EXTENSION_TABLE,
113
+ /**
114
+ * Node ids the format cannot write, under `sanitizeIds: "error"`; the save fails with E_INVALID_ID. Pass
115
+ * `sanitizeIds: "mangle"` to rewrite them.
116
+ */
117
+ ID_CHARSET: LOSS.ID_CHARSET,
118
+ /** Node ids OBO cannot write as they are, under sanitizeIds "mangle": rewritten, the original kept. */
119
+ ID_MANGLED: LOSS.ID_MANGLED,
120
+ /** Numeric ids are written as text and read back as strings. */
121
+ ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
122
+ /**
123
+ * Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
124
+ * E_INVALID_ID.
125
+ */
126
+ ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
127
+ });
128
+ /**
129
+ * What the OBO flat file keeps: a directed multigraph with self-loops and any id (the exporter's
130
+ * own rule refuses whitespace, control characters, `!`, `{` and `}`). No column of the snapshot's own reads back as
131
+ * a column: the file keeps the OBO vocabulary (`name`, `def`, `synonym`, ... with their own
132
+ * types) and writes every other node column as property values and every edge column as
133
+ * qualifiers, which check() reports column by column.
134
+ * @category Built-in formats
135
+ */
136
+ export const OBO_CAPABILITIES = capabilities({
137
+ mixedDirection: false,
138
+ multiEdges: true,
139
+ selfLoops: true,
140
+ edgeIds: "none",
141
+ idCharset: "any",
142
+ dtypes: [],
143
+ components: false,
144
+ lists: false,
145
+ json: false,
146
+ defaults: false,
147
+ options: false,
148
+ hierarchy: false,
149
+ temporal: "none",
150
+ graphAttributes: false,
151
+ positions: false,
152
+ viz: false,
153
+ });
154
+ /** The roles OBO has a slot for, and the column names its importer gives them. */
155
+ /** The node column that keeps a rewritten id's original (restored by restoreMangledIds). */
156
+ const ORIGINAL_ID = "graphty:originalId";
157
+ /** The edge columns of the vocabulary. */
158
+ const RELATION_COLUMN = "relation";
159
+ const QUALIFIERS_COLUMN = "qualifiers";
160
+ /** The qualifier name the explicit weights are written under. */
161
+ const WEIGHT_QUALIFIER = "weight";
162
+ /** The relations a file may use without declaring them (the importer's built-ins). */
163
+ const BUILTIN_RELATIONS = new Set([
164
+ "is_a",
165
+ "instance_of",
166
+ "disjoint_from",
167
+ "inverse_of",
168
+ "union_of",
169
+ "intersection_of",
170
+ ]);
171
+ /** Marks where the edge clauses go in TAG_ORDER. */
172
+ const EDGES = "\u0000edges";
173
+ /** Marks where the unknown tags go in TAG_ORDER. */
174
+ const UNRECOGNIZED = "obo.unrecognized";
175
+ /** The order of the tags in a frame (the OBO 1.4 guide's, Term, Instance and Typedef tags merged). */
176
+ const TAG_ORDER = [
177
+ "is_anonymous",
178
+ "name",
179
+ "namespace",
180
+ "alt_id",
181
+ "def",
182
+ "comment",
183
+ "subset",
184
+ "synonym",
185
+ "xref",
186
+ "builtin",
187
+ "property_value",
188
+ "domain",
189
+ "range",
190
+ "holds_over_chain",
191
+ "is_anti_symmetric",
192
+ "is_cyclic",
193
+ "is_reflexive",
194
+ "is_symmetric",
195
+ "is_asymmetric",
196
+ "is_transitive",
197
+ "is_functional",
198
+ "is_inverse_functional",
199
+ EDGES,
200
+ "intersection_of",
201
+ "union_of",
202
+ "equivalent_to",
203
+ "disjoint_from",
204
+ "inverse_of",
205
+ "transitive_over",
206
+ "equivalent_to_chain",
207
+ "disjoint_over",
208
+ "created_by",
209
+ "creation_date",
210
+ "is_obsolete",
211
+ "replaced_by",
212
+ "consider",
213
+ "expand_assertion_to",
214
+ "expand_expression_to",
215
+ "is_metadata_tag",
216
+ "is_class_level",
217
+ UNRECOGNIZED,
218
+ ];
219
+ /** The header tags in the 1.4 guide's order; every other kept header tag follows them. */
220
+ const HEADER_ORDER = [
221
+ "format-version",
222
+ "data-version",
223
+ "version",
224
+ "date",
225
+ "saved-by",
226
+ "auto-generated-by",
227
+ "import",
228
+ "subsetdef",
229
+ "synonymtypedef",
230
+ "idspace",
231
+ "default-relationship-id-prefix",
232
+ "id-mapping",
233
+ "default-namespace",
234
+ "namespace-id-rule",
235
+ "remark",
236
+ "ontology",
237
+ ];
238
+ /** Tags the importer applies (so an unknown tag of that name would not read back as unknown). */
239
+ const APPLIED_TAGS = new Set([
240
+ "id",
241
+ "is_a",
242
+ "instance_of",
243
+ "relationship",
244
+ "intersection_of",
245
+ "holds_over_chain",
246
+ "equivalent_to_chain",
247
+ "def",
248
+ "expand_assertion_to",
249
+ "expand_expression_to",
250
+ "synonym",
251
+ "xref",
252
+ "property_value",
253
+ ...TEXT_TAGS,
254
+ ...BOOL_TAGS,
255
+ ...ID_LIST_TAGS,
256
+ ...DEPRECATED_TAGS.keys(),
257
+ ]);
258
+ /** The vocabulary node columns the flat file writes as tags (`type`, `name` and `propertyType` aside). */
259
+ const TAG_COLUMNS = new Set(Object.keys(OBO_NODE_COLUMNS).filter((name) => name !== "type" && name !== "name" && name !== "propertyType"));
260
+ /** The xsd datatype of a dtype in a `property_value` line. */
261
+ const XSD = Object.freeze({
262
+ string: "xsd:string",
263
+ dict: "xsd:string",
264
+ bool: "xsd:boolean",
265
+ i32: "xsd:integer",
266
+ u32: "xsd:integer",
267
+ u8: "xsd:integer",
268
+ f32: "xsd:double",
269
+ f64: "xsd:double",
270
+ });
271
+ // ============================================================ values
272
+ /**
273
+ * Whether a text survives as an unquoted tag value: the reader trims it, and the OBO grammar
274
+ * (fastobo, owlapi) refuses an empty one.
275
+ * @param text - the text
276
+ * @returns true when the text is not empty and has no surrounding whitespace
277
+ */
278
+ function isTrimmed(text) {
279
+ return text.length > 0 && text === text.trim();
280
+ }
281
+ /**
282
+ * Whether a value is an array whose items all pass a test.
283
+ * @param value - the value
284
+ * @param item - the item test
285
+ * @param nonEmpty - whether the array must have an item
286
+ * @returns true when it is such an array
287
+ */
288
+ function arrayOf(value, item, nonEmpty = true) {
289
+ return Array.isArray(value) && (!nonEmpty || value.length > 0) && value.every(item);
290
+ }
291
+ /**
292
+ * Whether a value is a string the unquoted xref slot keeps (non-empty, no surrounding whitespace).
293
+ * @param value - the value
294
+ * @returns true for such a string
295
+ */
296
+ function isXrefId(value) {
297
+ return typeof value === "string" && value.length > 0 && isTrimmed(value);
298
+ }
299
+ /**
300
+ * Whether a value is an OBO word.
301
+ * @param value - the value
302
+ * @returns true for a string isOboWord() accepts
303
+ */
304
+ function isWord(value) {
305
+ return typeof value === "string" && isOboWord(value);
306
+ }
307
+ /**
308
+ * Whether an object has exactly the given keys, `qualifiers` optional (and then a qualifier record).
309
+ * @param value - the value
310
+ * @param keys - the required keys
311
+ * @returns true when it has those keys and nothing else
312
+ */
313
+ function hasKeys(value, keys) {
314
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
315
+ return false;
316
+ }
317
+ const own = Object.keys(value);
318
+ const record = value;
319
+ if (Object.hasOwn(record, "qualifiers")) {
320
+ if (!isQualifierRecord(record.qualifiers, isQualifierName)) {
321
+ return false;
322
+ }
323
+ return own.length === keys.length + 1 && keys.every((k) => Object.hasOwn(record, k));
324
+ }
325
+ return own.length === keys.length && keys.every((k) => Object.hasOwn(record, k));
326
+ }
327
+ /**
328
+ * Replace every character a word cannot hold with `_`.
329
+ * @param text - the text
330
+ * @returns a word
331
+ */
332
+ function wordOf(text) {
333
+ const out = text.replaceAll(new RegExp(NOT_IN_WORD.source, "gu"), "_");
334
+ return out.length === 0 ? "_" : out;
335
+ }
336
+ /**
337
+ * A qualifier record as name / value pairs (a list value as one pair per item).
338
+ * @param record - a record isQualifierRecord() accepts
339
+ * @returns the pairs
340
+ */
341
+ function qualifierPairs(record) {
342
+ const out = [];
343
+ for (const [name, value] of Object.entries(record)) {
344
+ for (const item of Array.isArray(value) ? value : [value]) {
345
+ out.push([name, item]);
346
+ }
347
+ }
348
+ return out;
349
+ }
350
+ // ============================================================ the plan
351
+ /**
352
+ * Resolve the OBO-specific options.
353
+ * @param options - the caller's options
354
+ * @returns the relation and the ontology; E_UNSUPPORTED for a value that is not an OBO id
355
+ */
356
+ function resolveOboOptions(options) {
357
+ const relation = options?.relation ?? "is_a";
358
+ if (typeof relation !== "string" || !isOboWord(relation)) {
359
+ throw new GraphFormatError("E_UNSUPPORTED", `option relation: ${JSON.stringify(relation)} is not an OBO id`, {
360
+ option: "relation",
361
+ found: relation,
362
+ });
363
+ }
364
+ const ontology = options?.ontology ?? null;
365
+ if (ontology !== null && (typeof ontology !== "string" || !/^[A-Za-z0-9_.\-/]+$/.test(ontology))) {
366
+ throw new GraphFormatError("E_UNSUPPORTED", `option ontology: ${JSON.stringify(ontology)} is not an ontology id (letters, digits, _ . - /)`, { option: "ontology", found: ontology });
367
+ }
368
+ return { relation, ontology };
369
+ }
370
+ /**
371
+ * The ids to write: each node's id text, or under "mangle" a word with `_` for the characters a
372
+ * word cannot hold (and `_2`, `_3`... on a collision), the original kept.
373
+ * @param snapshot - the snapshot
374
+ * @param mode - the sanitizeIds option
375
+ * @param note - records a note
376
+ * @returns the ids, the originals and the fatal error, if any
377
+ */
378
+ function planIds(snapshot, mode, note) {
379
+ const n = snapshot.nodeCount;
380
+ const ids = new Array(n);
381
+ const bad = [];
382
+ let numeric = 0;
383
+ for (let i = 0; i < n; i++) {
384
+ const id = snapshot.ids.idOf(i);
385
+ if (typeof id === "number") {
386
+ numeric++;
387
+ }
388
+ ids[i] = String(id);
389
+ if (!isOboWord(ids[i])) {
390
+ bad.push(i);
391
+ }
392
+ }
393
+ if (numeric > 0) {
394
+ note(LOSS.ID_TEXT_TYPE, `${numeric} numeric node id${plural(numeric)} ${agree(numeric, "is", "are")} written as text and read back as strings`, null, numeric);
395
+ }
396
+ const originals = new Map();
397
+ let fatal = null;
398
+ if (bad.length > 0 && mode === "error") {
399
+ fatal = refuseIds(snapshot, ids, bad, note);
400
+ }
401
+ else if (bad.length > 0) {
402
+ mangleIds(ids, bad, originals, note);
403
+ }
404
+ return { ids, originals, fatal: fatal ?? collidingIds(snapshot, ids, note) };
405
+ }
406
+ /**
407
+ * Note the ids that are not OBO words, for a save that refuses them.
408
+ * @param snapshot - the snapshot
409
+ * @param ids - the id texts
410
+ * @param bad - the indices of the ids that are not OBO words
411
+ * @param note - records a note
412
+ * @returns the error the save throws
413
+ */
414
+ function refuseIds(snapshot, ids, bad, note) {
415
+ note(LOSS.ID_CHARSET, `${bad.length} node id${plural(bad.length)} ${agree(bad.length, "is", "are")} not OBO ids (empty, or holding whitespace, a control character, "!", "{" or "}"); the save fails unless sanitizeIds is "mangle"`, null, bad.length);
416
+ return new GraphFormatError("E_INVALID_ID", `${bad.length} node id${plural(bad.length)} cannot be written as OBO ids (first: ${JSON.stringify(ids[bad[0]])}); pass sanitizeIds: "mangle" to rewrite them`, { reason: "charset", count: bad.length, id: snapshot.ids.idOf(bad[0]), index: bad[0] });
417
+ }
418
+ /**
419
+ * Rewrite the ids that are not OBO words with "_" (and `_2`, `_3`... on a collision), keeping the
420
+ * originals.
421
+ * @param ids - the id texts, rewritten in place
422
+ * @param bad - the indices of the ids that are not OBO words
423
+ * @param originals - receives each rewritten id's original text
424
+ * @param note - records a note
425
+ */
426
+ function mangleIds(ids, bad, originals, note) {
427
+ const badSet = new Set(bad);
428
+ const used = new Set(ids.filter((_, i) => !badSet.has(i)));
429
+ for (const i of bad) {
430
+ const base = wordOf(ids[i]);
431
+ let candidate = base;
432
+ let k = 2;
433
+ while (used.has(candidate)) {
434
+ candidate = `${base}_${k}`;
435
+ k++;
436
+ }
437
+ used.add(candidate);
438
+ originals.set(i, ids[i]);
439
+ ids[i] = candidate;
440
+ }
441
+ note(LOSS.ID_MANGLED, `${bad.length} node id${plural(bad.length)} that ${agree(bad.length, "is", "are")} not OBO ids ${agree(bad.length, "is", "are")} rewritten with "_"; the originals are kept as ${ORIGINAL_ID} property values (JSON text; an import with restoreMangledIds: true reads them back as the ids)`, null, bad.length);
442
+ }
443
+ /**
444
+ * Note the first two node ids that have the same text, which OBO cannot tell apart.
445
+ * @param snapshot - the snapshot
446
+ * @param ids - the id texts
447
+ * @param note - records a note
448
+ * @returns the error the save throws, or null when every text is unique
449
+ */
450
+ function collidingIds(snapshot, ids, note) {
451
+ const seen = new Map();
452
+ for (let i = 0; i < ids.length; i++) {
453
+ const before = seen.get(ids[i]);
454
+ if (before !== undefined) {
455
+ note(LOSS.ID_TEXT_COLLISION, `node ids ${JSON.stringify(snapshot.ids.idOf(before))} and ${JSON.stringify(snapshot.ids.idOf(i))} have the same text; the save fails`, null, 1);
456
+ return new GraphFormatError("E_INVALID_ID", "two node ids have the same text; OBO keeps ids as text", {
457
+ reason: "collision",
458
+ });
459
+ }
460
+ seen.set(ids[i], i);
461
+ }
462
+ return null;
463
+ }
464
+ /**
465
+ * The written edges (the mirror halves of folded pairs left out) grouped by source, in node order
466
+ * then edge order, and the direction notes.
467
+ * @param snapshot - the snapshot
468
+ * @param folding - the pair folding
469
+ * @param common - the common options
470
+ * @param note - records a note
471
+ * @returns the edge list, the CSR by source and the targets
472
+ */
473
+ function planEdges(snapshot, folding, common, note) {
474
+ const { src, dst } = snapshot.edgeList();
475
+ const written = [];
476
+ for (let e = 0; e < snapshot.edgeCount; e++) {
477
+ if (!folding.folded(e)) {
478
+ written.push(e);
479
+ }
480
+ }
481
+ const edgeStart = new Uint32Array(snapshot.nodeCount + 1);
482
+ for (const e of written) {
483
+ edgeStart[src[e] + 1]++;
484
+ }
485
+ for (let i = 0; i < snapshot.nodeCount; i++) {
486
+ edgeStart[i + 1] += edgeStart[i];
487
+ }
488
+ const fill = edgeStart.slice(0, snapshot.nodeCount);
489
+ const edgeOrder = new Uint32Array(written.length);
490
+ for (const e of written) {
491
+ edgeOrder[fill[src[e]]++] = e;
492
+ }
493
+ let moved = 0;
494
+ for (let k = 0; k < edgeOrder.length; k++) {
495
+ if (edgeOrder[k] !== written[k]) {
496
+ moved++;
497
+ }
498
+ }
499
+ if (moved > 0) {
500
+ note(OBO_LOSS.EDGE_ORDER, `${moved} edge${plural(moved)} ${agree(moved, "reads", "read")} back at another position: edges are written on their source's frame, grouped by source in node order`, null, moved);
501
+ }
502
+ if (!snapshot.directed) {
503
+ note(OBO_LOSS.UNDIRECTED_AS_DIRECTED, `the snapshot is undirected; every edge is written from its source to its target and reads back directed (${written.length} edge${plural(written.length)})`, null, written.length);
504
+ }
505
+ else if (common.onMixedDirection !== "error") {
506
+ const undirected = written.filter((e) => !folding.sourceDirected(e)).length;
507
+ if (undirected > 0) {
508
+ note(OBO_LOSS.UNDIRECTED_AS_DIRECTED, `${undirected} undirected edge${plural(undirected)} ${agree(undirected, "is", "are")} written as one directed clause each; OBO has no undirected edge`, null, undirected);
509
+ }
510
+ }
511
+ if (folding.mutualCount > 0) {
512
+ note(LOSS.MUTUAL_EXPANDED, `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two clauses; the mutual mark is lost`, null, folding.mutualCount);
513
+ }
514
+ return { written, edgeStart, edgeOrder, dst };
515
+ }
516
+ /**
517
+ * The frame types: the `type` column when every written node has one of Term, Instance or
518
+ * Typedef, else Term for every node (and the column is a property value).
519
+ * @param snapshot - the snapshot
520
+ * @param frameless - the frameless flags
521
+ * @returns the kinds and the type column used, or null
522
+ */
523
+ function planKinds(snapshot, frameless) {
524
+ const kinds = new Array(snapshot.nodeCount).fill("Term");
525
+ const type = snapshot.nodes.get("type");
526
+ if (type === null || (type.dtype !== "dict" && type.dtype !== "string")) {
527
+ return { kinds, type: null };
528
+ }
529
+ for (let i = 0; i < snapshot.nodeCount; i++) {
530
+ if (frameless[i] === 1) {
531
+ if (type.isSet(i)) {
532
+ return { kinds: kinds.fill("Term"), type: null };
533
+ }
534
+ continue;
535
+ }
536
+ const value = cellOf(type, i);
537
+ if (typeof value !== "string" || !FRAME_KINDS.has(value)) {
538
+ return { kinds: kinds.fill("Term"), type: null };
539
+ }
540
+ kinds[i] = value;
541
+ }
542
+ return { kinds, type };
543
+ }
544
+ /**
545
+ * Whether a vocabulary column's values are written by its tag and read back unchanged, on every
546
+ * written node (the vocabulary's dtype, the shape the importer builds, OBO words where the tag
547
+ * takes an id, no surrounding space in an unquoted value, a Typedef tag on Typedef frames only).
548
+ * @param name - the column (a TAG_COLUMNS name)
549
+ * @param column - the column
550
+ * @param rows - the written nodes
551
+ * @param kinds - the frame types
552
+ * @param scopes - the default scope of each declared synonym type
553
+ * @returns true when the tag carries the column
554
+ */
555
+ function tagCarries(name, column, rows, kinds, scopes) {
556
+ const spec = OBO_NODE_COLUMNS[name];
557
+ if (!fitsVocabulary(spec.dtype, column) || column.meta.components > 1) {
558
+ return false;
559
+ }
560
+ const typedefOnly = TYPEDEF_TAGS.has(name);
561
+ const valid = (v) => {
562
+ if (TEXT_TAGS.has(name)) {
563
+ return typeof v === "string" && isTrimmed(v);
564
+ }
565
+ if (BOOL_TAGS.has(name)) {
566
+ return typeof v === "boolean";
567
+ }
568
+ if (ID_LIST_TAGS.has(name)) {
569
+ return arrayOf(v, isWord);
570
+ }
571
+ switch (name) {
572
+ case "def":
573
+ return typeof v === "string";
574
+ case "def.xrefs":
575
+ return arrayOf(v, isXrefId, false);
576
+ case "xref":
577
+ return arrayOf(v, isXrefId);
578
+ case "xref.descriptions":
579
+ return (typeof v === "object" &&
580
+ v !== null &&
581
+ !Array.isArray(v) &&
582
+ Object.values(v).every((d) => typeof d === "string"));
583
+ case "synonym":
584
+ return arrayOf(v, (s) => hasKeys(s, ["text", "scope", "type", "xrefs"]) &&
585
+ typeof s.text === "string" &&
586
+ typeof s.scope === "string" &&
587
+ SYNONYM_SCOPES.has(s.scope) &&
588
+ (s.type === null || isWord(s.type)) &&
589
+ (s.type === null || (scopes.get(s.type) ?? s.scope) === s.scope) &&
590
+ arrayOf(s.xrefs, isXrefId, false));
591
+ case "intersection_of":
592
+ return arrayOf(v, (x) => hasKeys(x, ["relation", "target"]) &&
593
+ (x.relation === null || isWord(x.relation)) &&
594
+ isWord(x.target));
595
+ case "property_value":
596
+ return arrayOf(v, (x) => hasKeys(x, ["relation", "value", "datatype"]) &&
597
+ isWord(x.relation) &&
598
+ x.relation !== ORIGINAL_ID &&
599
+ typeof x.value === "string" &&
600
+ (x.datatype === null || isWord(x.datatype)));
601
+ case "holds_over_chain":
602
+ case "equivalent_to_chain":
603
+ return arrayOf(v, (pair) => arrayOf(pair, isWord) && pair.length === 2);
604
+ case "expand_assertion_to":
605
+ case "expand_expression_to":
606
+ return arrayOf(v, (x) => hasKeys(x, ["template", "xrefs"]) &&
607
+ typeof x.template === "string" &&
608
+ arrayOf(x.xrefs, isXrefId, false));
609
+ case "obo.qualifiers":
610
+ return (typeof v === "object" &&
611
+ v !== null &&
612
+ !Array.isArray(v) &&
613
+ Object.values(v).every((entries) => arrayOf(entries, (entry) => hasKeys(entry, ["value"]) &&
614
+ Object.hasOwn(entry, "qualifiers") &&
615
+ typeof entry.value === "string" &&
616
+ isQualifierRecord(entry.qualifiers, isQualifierName))));
617
+ default:
618
+ return false;
619
+ }
620
+ };
621
+ for (const i of rows) {
622
+ if (!column.isSet(i)) {
623
+ continue;
624
+ }
625
+ if (typedefOnly && kinds[i] !== "Typedef") {
626
+ return false;
627
+ }
628
+ if (!valid(cellOf(column, i))) {
629
+ return false;
630
+ }
631
+ }
632
+ return true;
633
+ }
634
+ /**
635
+ * Whether the `obo.unrecognized` column reads back unchanged: a record of tag to texts, each tag
636
+ * one the importer does not apply on that frame type.
637
+ * @param column - the column
638
+ * @param rows - the written nodes
639
+ * @param kinds - the frame types
640
+ * @returns true when every cell is written as unknown tags and read back as the same
641
+ */
642
+ function unrecognizedCarried(column, rows, kinds) {
643
+ if (column.dtype !== "json") {
644
+ return false;
645
+ }
646
+ for (const i of rows) {
647
+ const v = cellOf(column, i);
648
+ if (v === undefined) {
649
+ continue;
650
+ }
651
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
652
+ return false;
653
+ }
654
+ for (const [tag, values] of Object.entries(v)) {
655
+ const applied = (APPLIED_TAGS.has(tag) || TYPEDEF_TAGS.has(tag)) &&
656
+ !(TYPEDEF_TAGS.has(tag) && kinds[i] !== "Typedef") &&
657
+ !(tag === "instance_of" && kinds[i] !== "Instance");
658
+ if (applied ||
659
+ tag.length === 0 ||
660
+ !isTrimmed(tag) ||
661
+ !arrayOf(values, (t) => typeof t === "string" && isTrimmed(t))) {
662
+ return false;
663
+ }
664
+ }
665
+ }
666
+ return true;
667
+ }
668
+ /**
669
+ * The default scope of each synonym type the kept header declares (null: none declared).
670
+ * @param header - the kept header (tag to raw values)
671
+ * @returns the scopes by type
672
+ */
673
+ function synonymTypeScopes(header) {
674
+ const out = new Map();
675
+ for (const raw of header.synonymtypedef ?? []) {
676
+ const tokens = tokenize(raw);
677
+ if (tokens.length > 0 && tokens[0].kind === "word") {
678
+ const scope = tokens.slice(1).find((t) => t.kind === "word" && SYNONYM_SCOPES.has(t.text));
679
+ out.set(tokens[0].text, scope?.text ?? null);
680
+ }
681
+ }
682
+ return out;
683
+ }
684
+ /**
685
+ * The record of an OBO import kept under `meta.extra.obo`, or empty records.
686
+ * @param snapshot - the snapshot
687
+ * @returns the header, the Typedefs and the unknown frames
688
+ */
689
+ function keptObo(snapshot) {
690
+ const raw = snapshot.meta.extra.obo;
691
+ const record = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
692
+ const clauses = (value) => {
693
+ const out = {};
694
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
695
+ for (const [tag, values] of Object.entries(value)) {
696
+ if (Array.isArray(values)) {
697
+ out[tag] = values.filter((v) => typeof v === "string");
698
+ }
699
+ }
700
+ }
701
+ return out;
702
+ };
703
+ const typedefs = {};
704
+ if (typeof record.typedefs === "object" && record.typedefs !== null && !Array.isArray(record.typedefs)) {
705
+ for (const [id, frame] of Object.entries(record.typedefs)) {
706
+ typedefs[id] = clauses(frame);
707
+ }
708
+ }
709
+ const unknownFrames = Array.isArray(record.unknownFrames)
710
+ ? record.unknownFrames.flatMap((f) => typeof f === "object" && f !== null && typeof f.type === "string"
711
+ ? [{ type: f.type, clauses: clauses(f.clauses) }]
712
+ : [])
713
+ : [];
714
+ return { header: clauses(record.header), typedefs, unknownFrames };
715
+ }
716
+ /**
717
+ * Plan an export: the notes, the fatal condition and what is written.
718
+ * @param snapshot - the snapshot
719
+ * @param options - the caller's options
720
+ * @returns the plan
721
+ */
722
+ function plan(snapshot, options) {
723
+ const common = resolveExportOptions(options);
724
+ const obo = resolveOboOptions(options);
725
+ const notes = [];
726
+ const note = (code, message, column = null, count = null) => {
727
+ notes.push(Object.freeze({ code, message, column, count }));
728
+ };
729
+ let fatal = null;
730
+ const { ids, originals, fatal: idFatal } = planIds(snapshot, common.sanitizeIds, note);
731
+ fatal ?? (fatal = idFatal);
732
+ const folding = pairFolding(snapshot);
733
+ const { written, edgeStart, edgeOrder, dst } = planEdges(snapshot, folding, common, note);
734
+ const frameless = framelessNodes(snapshot, written, {
735
+ outgoingAllowed: false,
736
+ changed: (i) => originals.has(i),
737
+ });
738
+ const moved = movedNodes(frameless, Array.from(edgeOrder, (e) => dst[e]));
739
+ if (moved > 0) {
740
+ note(NODE_ORDER_CODE, `${moved} node${plural(moved)} ${agree(moved, "reads", "read")} back at another position: placeholder nodes have no frame and read back after the written nodes`, null, moved);
741
+ }
742
+ const rows = [];
743
+ for (let i = 0; i < snapshot.nodeCount; i++) {
744
+ if (frameless[i] === 0) {
745
+ rows.push(i);
746
+ }
747
+ }
748
+ const { kinds, type } = planKinds(snapshot, frameless);
749
+ const typedefNodes = rows.filter((i) => kinds[i] === "Typedef").length;
750
+ if (typedefNodes > 0) {
751
+ note(OBO_LOSS.TYPEDEF_NODES, `${typedefNodes} node${plural(typedefNodes)} ${agree(typedefNodes, "is", "are")} written as [Typedef] frames; they read back as nodes only when imported with typedefs: "nodes", and then after the other nodes, with the [Typedef] frames that declare the relations`, "type", typedefNodes);
752
+ }
753
+ const kept = keptObo(snapshot);
754
+ const scopes = synonymTypeScopes(kept.header);
755
+ // the label: the label column, or a role-less name column
756
+ const labelSlot = slotColumn(snapshot.nodes, "label", "name");
757
+ const labelColumn = labelSlot.column;
758
+ const label = labelColumn !== null && rows.every((i) => !labelColumn.isSet(i) || isTrimmed(String(labelColumn.value(i))))
759
+ ? labelColumn
760
+ : null;
761
+ const tags = planTags(snapshot, rows, kinds, scopes, label);
762
+ fatal ?? (fatal = capabilityNotes(snapshot, common, {
763
+ caps: OBO_CAPABILITIES,
764
+ label,
765
+ labelAssumed: labelSlot.assumed,
766
+ edgeSlot: "a qualifier",
767
+ note,
768
+ }));
769
+ oboColumnNotes(snapshot, label, tags, note);
770
+ const properties = planProperties(snapshot, rows, frameless, label, tags, type, note);
771
+ // edges
772
+ const relationSlot = slotColumn(snapshot.edges, "kind", RELATION_COLUMN);
773
+ const relations = planRelations(snapshot, written, relationSlot.column, obo.relation, note);
774
+ if (relationSlot.assumed && relationSlot.column !== null) {
775
+ note(ROLE_ASSUMED_CODE, `edge column "${RELATION_COLUMN}" has no role, so it is taken as the relation of each edge: it is written as each line's relation, and reads back with the relation role ("kind")`, RELATION_COLUMN, relationSlot.column.length - relationSlot.column.nullCount);
776
+ }
777
+ const weights = explicitWeights(snapshot);
778
+ const { qualifiers, edgeColumns } = planEdgeColumns(snapshot, written, relationSlot.column, weights, note);
779
+ emptyColumnNotes(snapshot, rows, frameless, written, note);
780
+ const p = {
781
+ notes,
782
+ fatal,
783
+ options: obo,
784
+ ids,
785
+ originals,
786
+ frameless,
787
+ kinds,
788
+ label,
789
+ tags,
790
+ properties,
791
+ edgeStart,
792
+ edgeOrder,
793
+ dst,
794
+ relations,
795
+ qualifiers,
796
+ edgeColumns,
797
+ weights,
798
+ header: [],
799
+ tail: [],
800
+ };
801
+ // write every frame once: the duplicate clauses, the line ends and the qualifier placement
802
+ let stats = simulate(p, rows);
803
+ if (!stats.qualifiersPlaced) {
804
+ tags.delete("obo.qualifiers");
805
+ const column = snapshot.nodes.get("obo.qualifiers");
806
+ if (column !== null) {
807
+ properties.push({ column, relation: "obo.qualifiers" });
808
+ notePropertyColumn(note, column, rows, "property_value lines (relation obo.qualifiers)");
809
+ }
810
+ stats = simulate(p, rows);
811
+ }
812
+ if (stats.duplicates > 0) {
813
+ note(OBO_LOSS.DUPLICATE_CLAUSE, `${stats.duplicates} clause${plural(stats.duplicates)} ${agree(stats.duplicates, "repeats", "repeat")} another clause of their frame word for word (parallel edges with one relation and the same qualifiers, a repeated list item) and read back as one`, null, stats.duplicates);
814
+ }
815
+ const header = planHeader(snapshot, kept.header, obo, rows, tags, note, stats);
816
+ const tail = planTail({
817
+ kept,
818
+ rows,
819
+ kinds,
820
+ ids,
821
+ properties,
822
+ relations,
823
+ edges: edgeOrder,
824
+ xref: tags.get("xref") ?? null,
825
+ });
826
+ if (stats.lineEnds > 0) {
827
+ note(OBO_LOSS.LINE_END, `${stats.lineEnds} text${plural(stats.lineEnds)} ${agree(stats.lineEnds, "holds", "hold")} a carriage return or form feed, which OBO cannot carry; written as a line feed`, null, stats.lineEnds);
828
+ }
829
+ return { ...p, header, tail };
830
+ }
831
+ /**
832
+ * The vocabulary columns written as their tags: each one tagCarries() accepts, then the pairs
833
+ * resolved (def.xrefs needs def, a def needs its def.xrefs written when that column exists, the
834
+ * xref descriptions need their xrefs written).
835
+ * @param snapshot - the snapshot
836
+ * @param rows - the written nodes
837
+ * @param kinds - the frame types
838
+ * @param scopes - the default scope of each declared synonym type
839
+ * @param label - the label column (never a tag)
840
+ * @returns the columns by tag
841
+ */
842
+ function planTags(snapshot, rows, kinds, scopes, label) {
843
+ const tags = new Map();
844
+ for (const name of TAG_COLUMNS) {
845
+ const column = snapshot.nodes.get(name);
846
+ if (column === null || column === label || (column.meta.role !== null && column.meta.role !== "label")) {
847
+ continue;
848
+ }
849
+ if (name === UNRECOGNIZED
850
+ ? unrecognizedCarried(column, rows, kinds)
851
+ : tagCarries(name, column, rows, kinds, scopes)) {
852
+ tags.set(name, column);
853
+ }
854
+ }
855
+ // pairs: def.xrefs needs def; xref.descriptions needs its xrefs written
856
+ const def = tags.get("def");
857
+ const defXrefs = tags.get("def.xrefs");
858
+ if (defXrefs !== undefined && (def === undefined || rows.some((i) => def.isSet(i) !== defXrefs.isSet(i)))) {
859
+ tags.delete("def.xrefs");
860
+ }
861
+ if (def !== undefined && !tags.has("def.xrefs") && snapshot.nodes.get("def.xrefs") !== null) {
862
+ // a def.xrefs column that is not written would read back as the empty list on every def
863
+ tags.delete("def");
864
+ }
865
+ const descriptions = tags.get("xref.descriptions");
866
+ if (descriptions !== undefined && !rows.every((i) => describedXrefsWritten(tags, descriptions, i))) {
867
+ tags.delete("xref.descriptions");
868
+ }
869
+ return tags;
870
+ }
871
+ /**
872
+ * Whether every described xref of a node is written in one of its xref slots (so the description
873
+ * has a place).
874
+ * @param tags - the columns written as tags
875
+ * @param descriptions - the `xref.descriptions` column
876
+ * @param row - the node
877
+ * @returns true when every description has its xref
878
+ */
879
+ function describedXrefsWritten(tags, descriptions, row) {
880
+ const map = cellOf(descriptions, row);
881
+ if (map === undefined) {
882
+ return true;
883
+ }
884
+ const written = new Set();
885
+ const add = (values) => {
886
+ if (Array.isArray(values)) {
887
+ for (const v of values) {
888
+ if (typeof v === "string") {
889
+ written.add(v);
890
+ }
891
+ }
892
+ }
893
+ };
894
+ for (const name of ["def.xrefs", "xref"]) {
895
+ const column = tags.get(name);
896
+ if (column !== undefined) {
897
+ add(cellOf(column, row));
898
+ }
899
+ }
900
+ for (const name of ["synonym", "expand_assertion_to", "expand_expression_to"]) {
901
+ const column = tags.get(name);
902
+ const items = column === undefined ? undefined : cellOf(column, row);
903
+ if (Array.isArray(items)) {
904
+ for (const item of items) {
905
+ add(item.xrefs);
906
+ }
907
+ }
908
+ }
909
+ return Object.keys(map).every((id) => written.has(id));
910
+ }
911
+ /**
912
+ * The OBO-only column notes: the vocabulary column of the other text dtype, and the graph columns
913
+ * written as header property values.
914
+ * @param snapshot - the snapshot
915
+ * @param label - the label column written as `name`, or null
916
+ * @param tags - the vocabulary columns written as tags
917
+ * @param note - records a note
918
+ */
919
+ function oboColumnNotes(snapshot, label, tags, note) {
920
+ for (const [name, column] of [...tags, ...(label === null ? [] : [["name", label]])]) {
921
+ const want = name === "name" ? "string" : OBO_NODE_COLUMNS[name].dtype;
922
+ if (((want === "string" || want === "dict") && column.dtype !== want) ||
923
+ (column.dtype === "list" && column.child.dtype !== "string")) {
924
+ const held = column.dtype === "list" ? `${column.child.dtype} items` : column.dtype;
925
+ note(LOSS.DTYPE, `node column "${column.meta.name}" holds ${held}; it reads back as ${column.dtype === "list" ? "string items" : want}`, column.meta.name, column.length - column.nullCount);
926
+ }
927
+ }
928
+ for (const column of snapshot.graph) {
929
+ note(GRAPH_COLUMN_AS_METADATA_CODE, `graph column "${column.meta.name}" is written as a header property_value and reads back in meta.extra.obo.header, not as a column`, column.meta.name, column.length - column.nullCount);
930
+ }
931
+ }
932
+ /**
933
+ * The node columns written as `property_value` lines: every column that is not written by a tag,
934
+ * the label or the type, and not one OBO leaves out (positions, visual and temporal columns), each
935
+ * with its note.
936
+ * @param snapshot - the snapshot
937
+ * @param rows - the written nodes
938
+ * @param frameless - the frameless flags
939
+ * @param label - the label column
940
+ * @param tags - the columns written as tags
941
+ * @param type - the type column
942
+ * @param note - records a note
943
+ * @returns the columns
944
+ */
945
+ function planProperties(snapshot, rows, frameless, label, tags, type, note) {
946
+ const out = [];
947
+ const slotted = new Set([
948
+ ...tags.values(),
949
+ ...(label === null ? [] : [label]),
950
+ ...(type === null ? [] : [type]),
951
+ ]);
952
+ for (const column of snapshot.nodes) {
953
+ const { role, name } = column.meta;
954
+ if (slotted.has(column) || (role !== null && UNWRITTEN_ROLES.has(role))) {
955
+ continue;
956
+ }
957
+ if (name === PLACEHOLDER_COLUMN && rows.every((i) => !column.isSet(i)) && frameless.includes(1)) {
958
+ // read back from the frameless nodes alone
959
+ continue;
960
+ }
961
+ const relation = isOboWord(name) && name !== ORIGINAL_ID ? name : wordOf(name.replaceAll(":", "_"));
962
+ out.push({ column, relation });
963
+ notePropertyColumn(note, column, rows, `property_value lines (relation ${relation})`);
964
+ }
965
+ return out;
966
+ }
967
+ /**
968
+ * The relation written for each edge, with the notes of the assumed and renamed ones.
969
+ * @param snapshot - the snapshot
970
+ * @param written - the written edges
971
+ * @param relation - the relation column, or null
972
+ * @param fallback - the relation of an edge without one
973
+ * @param note - records a note
974
+ * @returns the relation per edge index
975
+ */
976
+ function planRelations(snapshot, written, relation, fallback, note) {
977
+ const out = new Array(snapshot.edgeCount).fill(fallback);
978
+ let assumed = 0;
979
+ let renamed = 0;
980
+ for (const e of written) {
981
+ const value = relation === null ? undefined : cellOf(relation, e);
982
+ if (typeof value !== "string") {
983
+ assumed++;
984
+ continue;
985
+ }
986
+ if (isOboWord(value)) {
987
+ out[e] = value;
988
+ }
989
+ else {
990
+ out[e] = wordOf(value);
991
+ renamed++;
992
+ }
993
+ }
994
+ if (assumed > 0) {
995
+ note(OBO_LOSS.RELATION_ASSUMED, `${assumed} edge${plural(assumed)} ${agree(assumed, "has", "have")} no relation; written as ${fallback} (an ontology reads is_a as subclassing)`, relation?.meta.name ?? null, assumed);
996
+ }
997
+ if (renamed > 0) {
998
+ note(OBO_LOSS.RELATION_RENAMED, `${renamed} relation${plural(renamed)} ${agree(renamed, "is", "are")} not OBO ids; written with "_" in place of the space, "!", "{" or "}"`, relation?.meta.name ?? null, renamed);
999
+ }
1000
+ return out;
1001
+ }
1002
+ /**
1003
+ * The edge columns written as qualifiers: the vocabulary `qualifiers` column when its cells are
1004
+ * qualifier records, every other edge column (and the explicit weights) as `name="value"` pairs,
1005
+ * each with its note.
1006
+ * @param snapshot - the snapshot
1007
+ * @param written - the written edges
1008
+ * @param relation - the relation column
1009
+ * @param weights - the explicit weights
1010
+ * @param note - records a note
1011
+ * @returns the qualifiers column and the other columns
1012
+ */
1013
+ function planEdgeColumns(snapshot, written, relation, weights, note) {
1014
+ const qualifiers = qualifierRecords(snapshot, written);
1015
+ const edgeColumns = [];
1016
+ const count = (column) => written.filter((e) => column.isSet(e)).length;
1017
+ const merged = qualifiers === null ? "" : " (merged with its values)";
1018
+ for (const column of snapshot.edges) {
1019
+ const { role, name } = column.meta;
1020
+ if (column === relation ||
1021
+ column === qualifiers ||
1022
+ role === "id" ||
1023
+ (role !== null && UNWRITTEN_ROLES.has(role))) {
1024
+ continue;
1025
+ }
1026
+ const written2 = isQualifierName(name) ? name : wordOf(name.replaceAll(/[=",[\]\\]/g, "_"));
1027
+ edgeColumns.push({ column, name: written2 });
1028
+ const n = count(column);
1029
+ if (n > 0) {
1030
+ note(OBO_LOSS.EDGE_COLUMN_AS_QUALIFIER, `edge column "${name}" is written as the qualifier ${written2} and reads back inside the qualifiers column${merged}`, name, n);
1031
+ }
1032
+ }
1033
+ const explicit = weights.weighted ? written.filter((e) => weights.isExplicit(e)).length : 0;
1034
+ if (explicit > 0) {
1035
+ note(OBO_LOSS.EDGE_COLUMN_AS_QUALIFIER, `${explicit} explicit edge weight${plural(explicit)} ${agree(explicit, "is", "are")} written as the qualifier ${WEIGHT_QUALIFIER} and read back inside the qualifiers column, not as weights`, snapshot.edges.byRole("weight")?.meta.name ?? null, explicit);
1036
+ }
1037
+ return { qualifiers, edgeColumns };
1038
+ }
1039
+ /**
1040
+ * The vocabulary `qualifiers` edge column, when every written cell of it is a qualifier record.
1041
+ * @param snapshot - the snapshot
1042
+ * @param written - the written edges
1043
+ * @returns the column, or null when there is none or it holds something else
1044
+ */
1045
+ function qualifierRecords(snapshot, written) {
1046
+ const qualifiers = snapshot.edges.get(QUALIFIERS_COLUMN);
1047
+ if (qualifiers?.dtype !== "json" || qualifiers.meta.role !== null) {
1048
+ return null;
1049
+ }
1050
+ const records = written.every((e) => !qualifiers.isSet(e) || isQualifierRecord(cellOf(qualifiers, e), isQualifierName));
1051
+ return records ? qualifiers : null;
1052
+ }
1053
+ /**
1054
+ * The header lines: the kept header in the guide's order with the missing fields filled from the
1055
+ * metadata, and the graph columns as `property_value` lines.
1056
+ * @param snapshot - the snapshot
1057
+ * @param kept - the kept header
1058
+ * @param obo - the OBO options
1059
+ * @param obo.ontology - the ontology option, or null
1060
+ * @param rows - the written nodes
1061
+ * @param tags - the columns written as tags
1062
+ * @param note - records a note
1063
+ * @param stats - the line-end counter
1064
+ * @returns the lines
1065
+ */
1066
+ function planHeader(snapshot, kept, obo, rows, tags, note, stats) {
1067
+ const { values, filled } = headerFields(snapshot, kept, obo.ontology, note);
1068
+ // a default namespace applies to every frame without one: keep it only when none lacks one
1069
+ const namespace = tags.get("namespace");
1070
+ if (values.has("default-namespace") && (namespace === undefined || rows.some((i) => !namespace.isSet(i)))) {
1071
+ values.delete("default-namespace");
1072
+ }
1073
+ const lines = [];
1074
+ // the filled fields in the guide's order, then the kept ones as the file had them
1075
+ const tagsInOrder = [
1076
+ ...HEADER_ORDER.filter((t) => filled.has(t)),
1077
+ ...[...values.keys()].filter((t) => !filled.has(t)),
1078
+ ];
1079
+ for (const tag of tagsInOrder) {
1080
+ for (const value of values.get(tag) ?? []) {
1081
+ lines.push(`${escapeTag(tag)}: ${value}`);
1082
+ }
1083
+ }
1084
+ if (lines.length === 0 && rows.length === 0) {
1085
+ // a file of nothing reads back as an empty input: the version line makes it an empty ontology
1086
+ lines.push("format-version: 1.4");
1087
+ }
1088
+ for (const column of snapshot.graph) {
1089
+ const relation = isOboWord(column.meta.name) ? column.meta.name : wordOf(column.meta.name);
1090
+ for (const line of propertyLines({ column, relation }, 0, stats)) {
1091
+ lines.push(`property_value: ${line}`);
1092
+ }
1093
+ }
1094
+ return lines;
1095
+ }
1096
+ /**
1097
+ * The header fields: the kept ones, and for a graph not read from OBO the fields its metadata has.
1098
+ * @param snapshot - the snapshot
1099
+ * @param kept - the kept header
1100
+ * @param ontologyOption - the ontology option, or null
1101
+ * @param note - records a note
1102
+ * @returns the values by tag, and the tags filled in (not kept)
1103
+ */
1104
+ function headerFields(snapshot, kept, ontologyOption, note) {
1105
+ const { meta } = snapshot;
1106
+ const values = new Map(Object.entries(kept).map(([tag, v]) => [tag, v.map(rawValue)]));
1107
+ const filled = new Set();
1108
+ const fill = (tag, value) => {
1109
+ if (value !== null && !values.has(tag)) {
1110
+ values.set(tag, [value]);
1111
+ filled.add(tag);
1112
+ }
1113
+ };
1114
+ // a file read from OBO keeps its own header; any other graph gets the fields its metadata has
1115
+ if (meta.sourceFormat !== "obo") {
1116
+ fill("format-version", "1.4");
1117
+ fill("date", oboDate(meta.created));
1118
+ fill("saved-by", meta.creator === null ? null : escapeOboValue(meta.creator));
1119
+ }
1120
+ const ontology = ontologyField(ontologyOption, meta.name, values.has("ontology"), note);
1121
+ if (ontology !== null) {
1122
+ if (!values.has("ontology")) {
1123
+ filled.add("ontology");
1124
+ }
1125
+ values.set("ontology", [ontology]);
1126
+ }
1127
+ return { values, filled };
1128
+ }
1129
+ /**
1130
+ * The ontology header field to write: the ontology option, else the graph name when it is an
1131
+ * ontology id, else the name rewritten with "_" when the kept header has none.
1132
+ * @param option - the ontology option, or null
1133
+ * @param name - the graph name, or null
1134
+ * @param kept - whether the kept header has an ontology field
1135
+ * @param note - records a note
1136
+ * @returns the value to set, or null to leave the field as it is
1137
+ */
1138
+ function ontologyField(option, name, kept, note) {
1139
+ if (option !== null) {
1140
+ return option;
1141
+ }
1142
+ if (name === null || name.length === 0 || kept) {
1143
+ return null;
1144
+ }
1145
+ if (/^[A-Za-z0-9_.\-/]+$/.test(name)) {
1146
+ return name;
1147
+ }
1148
+ const slug = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
1149
+ note(OBO_LOSS.ONTOLOGY_NAME, `the graph name ${JSON.stringify(name)} is not an ontology id; written as ${slug}`, null, null);
1150
+ return slug;
1151
+ }
1152
+ /**
1153
+ * A kept raw value made safe for one line (a line break in metadata a caller built becomes `\n`).
1154
+ * @param raw - the raw value
1155
+ * @returns the value to write
1156
+ */
1157
+ function rawValue(raw) {
1158
+ return raw.replaceAll(/\r\n|\r|\n|\f/g, String.raw `\n`);
1159
+ }
1160
+ /**
1161
+ * A tag name escaped for the left of its colon.
1162
+ * @param tag - the tag
1163
+ * @returns the escaped tag
1164
+ */
1165
+ function escapeTag(tag) {
1166
+ return escapeOboValue(tag).replaceAll(":", String.raw `\:`);
1167
+ }
1168
+ /**
1169
+ * An ISO date (`yyyy-MM-ddTHH:mm...`) as the OBO header date `dd:MM:yyyy HH:mm`.
1170
+ * @param iso - the date, or null
1171
+ * @returns the header text, or null when the date is not of that form
1172
+ */
1173
+ function oboDate(iso) {
1174
+ const m = iso === null ? null : /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?/.exec(iso);
1175
+ if (m === null) {
1176
+ return null;
1177
+ }
1178
+ return `${m[3]}:${m[2]}:${m[1]} ${m[4] ?? "00"}:${m[5] ?? "00"}`;
1179
+ }
1180
+ /**
1181
+ * The frames after the nodes: the kept Typedefs (unless a node is that Typedef), a declaration of
1182
+ * every relation and property-value relation no Typedef declares, and the kept unknown frames.
1183
+ * @param input - what the frames are planned from
1184
+ * @returns the lines
1185
+ */
1186
+ function planTail(input) {
1187
+ const { kept, rows, properties, relations } = input;
1188
+ const lines = [];
1189
+ const declared = nodeTypedefs(input);
1190
+ keptTypedefLines(kept.typedefs, declared, lines);
1191
+ const declare = (relation, metadata) => {
1192
+ if (declared.has(relation)) {
1193
+ return;
1194
+ }
1195
+ declared.add(relation);
1196
+ lines.push("", "[Typedef]", `id: ${escapeOboWord(relation)}`, `name: ${escapeOboValue(relation)}`);
1197
+ if (metadata) {
1198
+ lines.push("is_metadata_tag: true");
1199
+ }
1200
+ };
1201
+ const used = new Set();
1202
+ for (const e of input.edges) {
1203
+ used.add(relations[e]);
1204
+ }
1205
+ for (const { column, relation } of properties) {
1206
+ if (rows.some((i) => column.isSet(i))) {
1207
+ declare(relation, true);
1208
+ }
1209
+ }
1210
+ for (const relation of used) {
1211
+ declare(relation, false);
1212
+ }
1213
+ for (const frame of kept.unknownFrames) {
1214
+ lines.push("", `[${frame.type.replaceAll(/[\]\r\n]/g, "_")}]`, ...clauseLines(frame.clauses));
1215
+ }
1216
+ return lines;
1217
+ }
1218
+ /**
1219
+ * The relations the built-in ones, the Typedef nodes and their xrefs declare.
1220
+ * @param input - what the frames are planned from
1221
+ * @returns the declared relation ids
1222
+ */
1223
+ function nodeTypedefs(input) {
1224
+ const { rows, kinds, ids, xref } = input;
1225
+ const declared = new Set(BUILTIN_RELATIONS);
1226
+ for (const i of rows) {
1227
+ if (kinds[i] !== "Typedef") {
1228
+ continue;
1229
+ }
1230
+ declared.add(ids[i]);
1231
+ const xrefs = xref === null ? undefined : cellOf(xref, i);
1232
+ for (const x of Array.isArray(xrefs) ? xrefs : []) {
1233
+ declared.add(String(x));
1234
+ }
1235
+ }
1236
+ return declared;
1237
+ }
1238
+ /**
1239
+ * Write the kept Typedef frames no node already declares, and declare them and their xrefs.
1240
+ * @param typedefs - the kept Typedef frames by id
1241
+ * @param declared - the declared relations, added to
1242
+ * @param lines - the lines, appended to
1243
+ */
1244
+ function keptTypedefLines(typedefs, declared, lines) {
1245
+ for (const [id, clauses] of Object.entries(typedefs)) {
1246
+ if (declared.has(id) && !BUILTIN_RELATIONS.has(id)) {
1247
+ continue;
1248
+ }
1249
+ declared.add(id);
1250
+ lines.push("", "[Typedef]", `id: ${escapeOboWord(id)}`, ...clauseLines(clauses, "id"));
1251
+ for (const xref of clauses.xref ?? []) {
1252
+ declared.add(xref.trim());
1253
+ }
1254
+ }
1255
+ }
1256
+ /**
1257
+ * The clause lines of a kept frame, each value made safe for one line.
1258
+ * @param clauses - the values by tag
1259
+ * @param skip - a tag to leave out, or null
1260
+ * @returns the lines
1261
+ */
1262
+ function clauseLines(clauses, skip = null) {
1263
+ return Object.entries(clauses)
1264
+ .filter(([tag]) => tag !== skip)
1265
+ .flatMap(([tag, values]) => values.map((value) => `${escapeTag(tag)}: ${rawValue(value)}`));
1266
+ }
1267
+ // ============================================================ the frames
1268
+ /** Hands out the `obo.qualifiers` entries of one frame to the clauses they belong to, in order. */
1269
+ class QualifierCursor {
1270
+ /**
1271
+ * Create a cursor.
1272
+ * @param cell - the frame's `obo.qualifiers` cell, or undefined
1273
+ */
1274
+ constructor(cell) {
1275
+ this.next = new Map();
1276
+ this.entries = (cell ?? {});
1277
+ }
1278
+ /**
1279
+ * The qualifier block of a clause: the next entry of its tag when that entry is for this value.
1280
+ * @param tag - the tag (or `<tag>.xrefs` for an xref inside a list)
1281
+ * @param value - the clause's value as the column holds it
1282
+ * @returns the block with a leading space, or ""
1283
+ */
1284
+ block(tag, value) {
1285
+ const list = Object.hasOwn(this.entries, tag) ? this.entries[tag] : undefined;
1286
+ const k = this.next.get(tag) ?? 0;
1287
+ if (list === undefined || k >= list.length || list[k].value !== value) {
1288
+ return "";
1289
+ }
1290
+ this.next.set(tag, k + 1);
1291
+ return qualifierBlock(qualifierPairs(list[k].qualifiers));
1292
+ }
1293
+ /**
1294
+ * Whether every entry found its clause.
1295
+ * @returns true when all were placed
1296
+ */
1297
+ placed() {
1298
+ return Object.entries(this.entries).every(([tag, list]) => (this.next.get(tag) ?? 0) === list.length);
1299
+ }
1300
+ }
1301
+ /**
1302
+ * Count a text that loses a line end, and return it.
1303
+ * @param s - the frame state's stats
1304
+ * @param text - the text
1305
+ * @returns the text
1306
+ */
1307
+ function counted(s, text) {
1308
+ if (hasLineEnd(text)) {
1309
+ s.lineEnds++;
1310
+ }
1311
+ return text;
1312
+ }
1313
+ /**
1314
+ * One xref as written inside a list or after `xref:`: its id, its description the first time the
1315
+ * frame names it, and its own qualifiers.
1316
+ * @param f - the frame state
1317
+ * @param id - the xref id
1318
+ * @param listTag - the `<tag>.xrefs` key of its qualifiers, or null after `xref:`
1319
+ * @returns the text
1320
+ */
1321
+ function xrefText(f, id, listTag) {
1322
+ let out = escapeOboValue(counted(f.stats, id));
1323
+ const descriptions = f.p.tags.get("xref.descriptions");
1324
+ const map = descriptions === undefined ? undefined : cellOf(descriptions, f.row);
1325
+ if (map !== undefined && Object.hasOwn(map, id) && !f.described.has(id)) {
1326
+ f.described.add(id);
1327
+ out += ` "${escapeOboQuoted(counted(f.stats, map[id]))}"`;
1328
+ }
1329
+ if (listTag !== null) {
1330
+ out += f.cursor.block(listTag, id);
1331
+ }
1332
+ return out;
1333
+ }
1334
+ /**
1335
+ * An xref list: `[A:1, B:2 "description"]`.
1336
+ * @param f - the frame state
1337
+ * @param ids - the xref ids
1338
+ * @param tag - the clause's tag
1339
+ * @returns the list
1340
+ */
1341
+ function xrefList(f, ids, tag) {
1342
+ const field = `${tag}.xrefs`;
1343
+ return `[${ids.map((id) => xrefText(f, id, field)).join(", ")}]`;
1344
+ }
1345
+ /**
1346
+ * The `property_value` values of one cell of a property column: `relation "value" xsd:type` per
1347
+ * value (a list item each, JSON text for nested values).
1348
+ * @param prop - the column and its relation
1349
+ * @param row - the row
1350
+ * @param stats - the line-end counter
1351
+ * @returns the values after `property_value: `
1352
+ */
1353
+ function propertyLines(prop, row, stats) {
1354
+ const { column, relation } = prop;
1355
+ const value = cellOf(column, row);
1356
+ if (value === undefined) {
1357
+ return [];
1358
+ }
1359
+ const head = escapeOboWord(relation);
1360
+ const one = (v, dtype) => {
1361
+ let text;
1362
+ let xsd = XSD[dtype] ?? "xsd:string";
1363
+ if (typeof v === "number" && xsd === "xsd:double") {
1364
+ text = xsdDouble(v, dtype);
1365
+ }
1366
+ else if (typeof v === "string" || typeof v === "boolean" || typeof v === "number") {
1367
+ text = textOf(v, dtype);
1368
+ }
1369
+ else {
1370
+ text = JSON.stringify(v) ?? "null";
1371
+ xsd = "xsd:string";
1372
+ }
1373
+ return `${head} "${escapeOboQuoted(counted(stats, text))}" ${xsd}`;
1374
+ };
1375
+ if (column.dtype === "list") {
1376
+ return value.map((item) => one(item, column.child.dtype));
1377
+ }
1378
+ if (column.dtype === "json" || column.meta.components > 1) {
1379
+ return [one(value, "json")];
1380
+ }
1381
+ return [one(value, column.dtype)];
1382
+ }
1383
+ /**
1384
+ * A double as xsd:double writes it: INF, -INF and NaN for the non-finite values.
1385
+ * @param v - the value
1386
+ * @param dtype - the column dtype (f32 values use their shortest text)
1387
+ * @returns the text
1388
+ */
1389
+ function xsdDouble(v, dtype) {
1390
+ if (Number.isNaN(v)) {
1391
+ return "NaN";
1392
+ }
1393
+ if (!Number.isFinite(v)) {
1394
+ return v > 0 ? "INF" : "-INF";
1395
+ }
1396
+ return formatNumber(v, dtype === "f32" ? "f32" : "f64");
1397
+ }
1398
+ /**
1399
+ * The qualifier block of one edge: the `qualifiers` record, then the other columns and the weight.
1400
+ * @param f - the frame state
1401
+ * @param e - the edge
1402
+ * @returns the block with a leading space, or ""
1403
+ */
1404
+ function edgeQualifiers(f, e) {
1405
+ const { p } = f;
1406
+ const pairs = [];
1407
+ if (p.qualifiers !== null) {
1408
+ const record = cellOf(p.qualifiers, e);
1409
+ if (record !== undefined) {
1410
+ pairs.push(...qualifierPairs(record));
1411
+ }
1412
+ }
1413
+ for (const { column, name } of p.edgeColumns) {
1414
+ pairs.push(...qualifierTexts(column, e).map((text) => [name, text]));
1415
+ }
1416
+ const weight = p.weights.text(e);
1417
+ if (weight !== null) {
1418
+ pairs.push([WEIGHT_QUALIFIER, weight]);
1419
+ }
1420
+ for (const [, value] of pairs) {
1421
+ counted(f.stats, value);
1422
+ }
1423
+ return qualifierBlock(pairs);
1424
+ }
1425
+ /**
1426
+ * The qualifier values an edge column gives one edge: one per item of a list, none when unset.
1427
+ * @param column - the edge column
1428
+ * @param e - the edge
1429
+ * @returns the value texts
1430
+ */
1431
+ function qualifierTexts(column, e) {
1432
+ const value = cellOf(column, e);
1433
+ if (value === undefined) {
1434
+ return [];
1435
+ }
1436
+ if (column.dtype === "list") {
1437
+ return value.map((item) => textOf(item, column.child.dtype));
1438
+ }
1439
+ return [column.dtype === "json" ? (JSON.stringify(value) ?? "null") : textOf(value, column.dtype)];
1440
+ }
1441
+ /**
1442
+ * The clauses of one tag column on a frame.
1443
+ * @param f - the frame state
1444
+ * @param tag - the tag
1445
+ * @param column - the column
1446
+ * @param out - the lines
1447
+ */
1448
+ function tagLines(f, tag, column, out) {
1449
+ const value = cellOf(column, f.row);
1450
+ if (value === undefined) {
1451
+ return;
1452
+ }
1453
+ const { cursor, stats } = f;
1454
+ const push = (text, stored, raw = false) => {
1455
+ out.push(`${tag}: ${raw ? text : escapeOboValue(counted(stats, text))}${cursor.block(tag, stored)}`);
1456
+ };
1457
+ if (TEXT_TAGS.has(tag)) {
1458
+ push(value, value);
1459
+ return;
1460
+ }
1461
+ if (BOOL_TAGS.has(tag)) {
1462
+ push(value ? "true" : "false", value ? "true" : "false", true);
1463
+ return;
1464
+ }
1465
+ if (ID_LIST_TAGS.has(tag)) {
1466
+ for (const id of value) {
1467
+ push(escapeOboWord(id), id, true);
1468
+ }
1469
+ return;
1470
+ }
1471
+ // def.xrefs, xref.descriptions and obo.qualifiers are written with the clauses they belong to
1472
+ if (Object.hasOwn(STRUCTURED_TAGS, tag)) {
1473
+ STRUCTURED_TAGS[tag](f, tag, value, out);
1474
+ }
1475
+ }
1476
+ /**
1477
+ * The qualifier block of a structured value.
1478
+ * @param qualifiers - the value's qualifiers, or undefined
1479
+ * @returns the block with a leading space, or ""
1480
+ */
1481
+ function blockOf(qualifiers) {
1482
+ return qualifiers === undefined ? "" : qualifierBlock(qualifierPairs(qualifiers));
1483
+ }
1484
+ /** The writers of the tags whose cells hold more than a text, an id list or a boolean. */
1485
+ const STRUCTURED_TAGS = {
1486
+ def(f, _tag, value, out) {
1487
+ const xrefs = f.p.tags.get("def.xrefs");
1488
+ const ids = (xrefs === undefined ? [] : (cellOf(xrefs, f.row) ?? []));
1489
+ const text = value;
1490
+ out.push(`def: "${escapeOboQuoted(counted(f.stats, text))}" ${xrefList(f, ids, "def")}${f.cursor.block("def", text)}`);
1491
+ },
1492
+ xref(f, _tag, value, out) {
1493
+ for (const id of value) {
1494
+ out.push(`xref: ${xrefText(f, id, null)}${f.cursor.block("xref", id)}`);
1495
+ }
1496
+ },
1497
+ synonym(f, _tag, value, out) {
1498
+ for (const s of value) {
1499
+ const type = s.type === null ? "" : ` ${escapeOboWord(s.type)}`;
1500
+ out.push(`synonym: "${escapeOboQuoted(counted(f.stats, s.text))}" ${s.scope}${type} ${xrefList(f, s.xrefs, "synonym")}${blockOf(s.qualifiers)}`);
1501
+ }
1502
+ },
1503
+ intersection_of(_f, _tag, value, out) {
1504
+ for (const x of value) {
1505
+ const relation = x.relation === null ? "" : `${escapeOboWord(x.relation)} `;
1506
+ out.push(`intersection_of: ${relation}${escapeOboWord(x.target)}${blockOf(x.qualifiers)}`);
1507
+ }
1508
+ },
1509
+ property_value(f, _tag, value, out) {
1510
+ for (const x of value) {
1511
+ const datatype = x.datatype === null ? "" : ` ${escapeOboWord(x.datatype)}`;
1512
+ const literal = x.datatype === null && isOboWord(x.value)
1513
+ ? escapeOboWord(x.value)
1514
+ : `"${escapeOboQuoted(counted(f.stats, x.value))}"${datatype}`;
1515
+ out.push(`property_value: ${escapeOboWord(x.relation)} ${literal}${blockOf(x.qualifiers)}`);
1516
+ }
1517
+ },
1518
+ holds_over_chain: chainLines,
1519
+ equivalent_to_chain: chainLines,
1520
+ expand_assertion_to: expansionLines,
1521
+ expand_expression_to: expansionLines,
1522
+ [UNRECOGNIZED](f, _tag, value, out) {
1523
+ for (const [unknown, values] of Object.entries(value)) {
1524
+ for (const v of values) {
1525
+ out.push(`${escapeTag(unknown)}: ${escapeOboValue(counted(f.stats, v))}`);
1526
+ }
1527
+ }
1528
+ },
1529
+ };
1530
+ /**
1531
+ * The clauses of a relation chain tag (`holds_over_chain`, `equivalent_to_chain`).
1532
+ * @param f - the frame state
1533
+ * @param tag - the tag
1534
+ * @param value - the cell: pairs of relations
1535
+ * @param out - the lines
1536
+ */
1537
+ function chainLines(f, tag, value, out) {
1538
+ for (const [a, b] of value) {
1539
+ const pair = `${a} ${b}`;
1540
+ out.push(`${tag}: ${escapeOboWord(a)} ${escapeOboWord(b)}${f.cursor.block(tag, pair)}`);
1541
+ }
1542
+ }
1543
+ /**
1544
+ * The clauses of a macro expansion tag (`expand_assertion_to`, `expand_expression_to`).
1545
+ * @param f - the frame state
1546
+ * @param tag - the tag
1547
+ * @param value - the cell: templates with their xrefs
1548
+ * @param out - the lines
1549
+ */
1550
+ function expansionLines(f, tag, value, out) {
1551
+ for (const x of value) {
1552
+ out.push(`${tag}: "${escapeOboQuoted(counted(f.stats, x.template))}" ${xrefList(f, x.xrefs, tag)}${blockOf(x.qualifiers)}`);
1553
+ }
1554
+ }
1555
+ /**
1556
+ * The lines of one node's frame.
1557
+ * @param p - the plan
1558
+ * @param row - the node
1559
+ * @param stats - the counters
1560
+ * @returns the lines, the frame header first
1561
+ */
1562
+ function frameLines(p, row, stats) {
1563
+ const qualifiersColumn = p.tags.get("obo.qualifiers");
1564
+ const f = {
1565
+ p,
1566
+ row,
1567
+ cursor: new QualifierCursor(qualifiersColumn === undefined ? undefined : cellOf(qualifiersColumn, row)),
1568
+ described: new Set(),
1569
+ stats,
1570
+ };
1571
+ const out = [`[${p.kinds[row]}]`, `id: ${escapeOboWord(p.ids[row])}`];
1572
+ for (const tag of TAG_ORDER) {
1573
+ if (tag === "name") {
1574
+ nameLine(f, out);
1575
+ }
1576
+ else if (tag === "property_value") {
1577
+ propertyValueLines(f, out);
1578
+ }
1579
+ else if (tag === EDGES) {
1580
+ edgeLines(f, out);
1581
+ }
1582
+ else {
1583
+ const column = p.tags.get(tag);
1584
+ if (column !== undefined) {
1585
+ tagLines(f, tag, column, out);
1586
+ }
1587
+ }
1588
+ }
1589
+ stats.qualifiersPlaced && (stats.qualifiersPlaced = f.cursor.placed());
1590
+ return out;
1591
+ }
1592
+ /**
1593
+ * The name clause of a frame, from the label column.
1594
+ * @param f - the frame state
1595
+ * @param out - the lines
1596
+ */
1597
+ function nameLine(f, out) {
1598
+ const name = f.p.label === null ? undefined : cellOf(f.p.label, f.row);
1599
+ if (typeof name === "string") {
1600
+ out.push(`name: ${escapeOboValue(counted(f.stats, name))}${f.cursor.block("name", name)}`);
1601
+ }
1602
+ }
1603
+ /**
1604
+ * The property_value clauses of a frame: the original id of a rewritten one, the property_value
1605
+ * column, then the property-value columns.
1606
+ * @param f - the frame state
1607
+ * @param out - the lines
1608
+ */
1609
+ function propertyValueLines(f, out) {
1610
+ const { p, row } = f;
1611
+ const original = p.originals.get(row);
1612
+ if (original !== undefined) {
1613
+ // as JSON text, so a carriage return in the original survives
1614
+ out.push(`property_value: ${ORIGINAL_ID} "${escapeOboQuoted(JSON.stringify(original))}" xsd:string`);
1615
+ }
1616
+ const column = p.tags.get("property_value");
1617
+ if (column !== undefined) {
1618
+ tagLines(f, "property_value", column, out);
1619
+ }
1620
+ for (const prop of p.properties) {
1621
+ for (const line of propertyLines(prop, row, f.stats)) {
1622
+ out.push(`property_value: ${line}`);
1623
+ }
1624
+ }
1625
+ }
1626
+ /**
1627
+ * The edge clauses of a frame: `is_a` (and `instance_of` on an Instance) as their own tags, any
1628
+ * other relation as a `relationship` clause.
1629
+ * @param f - the frame state
1630
+ * @param out - the lines
1631
+ */
1632
+ function edgeLines(f, out) {
1633
+ const { p, row } = f;
1634
+ for (let k = p.edgeStart[row]; k < p.edgeStart[row + 1]; k++) {
1635
+ const e = p.edgeOrder[k];
1636
+ const target = escapeOboWord(p.ids[p.dst[e]]);
1637
+ const relation = p.relations[e];
1638
+ const block = edgeQualifiers(f, e);
1639
+ if (relation === "is_a" || (relation === "instance_of" && p.kinds[row] === "Instance")) {
1640
+ out.push(`${relation}: ${target}${block}`);
1641
+ }
1642
+ else {
1643
+ out.push(`relationship: ${escapeOboWord(relation)} ${target}${block}`);
1644
+ }
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Write every frame once without keeping the text: count the clauses that repeat another of their
1649
+ * frame (the importer reads them as one), the line ends and whether the qualifiers were placed.
1650
+ * @param p - the plan
1651
+ * @param rows - the written nodes
1652
+ * @returns the counts
1653
+ */
1654
+ function simulate(p, rows) {
1655
+ const stats = { lineEnds: 0, qualifiersPlaced: true, duplicates: 0 };
1656
+ for (const i of rows) {
1657
+ const lines = frameLines(p, i, stats);
1658
+ const seen = new Set();
1659
+ for (let k = 2; k < lines.length; k++) {
1660
+ if (seen.has(lines[k])) {
1661
+ stats.duplicates++;
1662
+ }
1663
+ seen.add(lines[k]);
1664
+ }
1665
+ }
1666
+ return stats;
1667
+ }
1668
+ /**
1669
+ * The text parts of the file: the header, one frame per written node, the tail frames.
1670
+ * @param p - the plan
1671
+ * @yields the header and one part per frame
1672
+ * @returns nothing
1673
+ */
1674
+ function* write(p) {
1675
+ if (p.fatal !== null) {
1676
+ throw p.fatal;
1677
+ }
1678
+ yield p.header.map((line) => `${line}\n`).join("");
1679
+ const stats = { lineEnds: 0, qualifiersPlaced: true };
1680
+ for (let i = 0; i < p.ids.length; i++) {
1681
+ if (p.frameless[i] === 0) {
1682
+ yield `\n${frameLines(p, i, stats).join("\n")}\n`;
1683
+ }
1684
+ }
1685
+ if (p.tail.length > 0) {
1686
+ yield `${p.tail.join("\n")}\n`;
1687
+ }
1688
+ }
1689
+ /**
1690
+ * The text parts of an export, planned when the first part is asked for, so a refused option
1691
+ * rejects the export instead of throwing from the call.
1692
+ * @param snapshot - the snapshot
1693
+ * @param options - format-specific and common options
1694
+ * @yields the parts
1695
+ * @returns nothing
1696
+ */
1697
+ function* written(snapshot, options) {
1698
+ yield* write(plan(snapshot, options));
1699
+ }
1700
+ /**
1701
+ * The OBO exporter plugin.
1702
+ * @category Built-in formats
1703
+ */
1704
+ export const oboExporter = Object.freeze({
1705
+ format: "obo",
1706
+ options: Object.freeze(["ontology", "relation"]),
1707
+ capabilities: OBO_CAPABILITIES,
1708
+ /**
1709
+ * Pre-flight: what export() would lose, without writing anything.
1710
+ * @param snapshot - the snapshot
1711
+ * @param options - format-specific and common options
1712
+ * @returns the notes, empty when the export is exact
1713
+ */
1714
+ check(snapshot, options) {
1715
+ return Object.freeze([...plan(snapshot, options).notes]);
1716
+ },
1717
+ /**
1718
+ * Write the file as UTF-8 chunks.
1719
+ * @param snapshot - the snapshot
1720
+ * @param options - format-specific and common options
1721
+ * @returns the chunks
1722
+ */
1723
+ export(snapshot, options) {
1724
+ return encodeChunks(written(snapshot, options));
1725
+ },
1726
+ /**
1727
+ * Write the file as one string.
1728
+ * @param snapshot - the snapshot
1729
+ * @param options - format-specific and common options
1730
+ * @returns the file
1731
+ */
1732
+ exportToString(snapshot, options) {
1733
+ return joinText(written(snapshot, options));
1734
+ },
1735
+ });
1736
+ //# sourceMappingURL=exporter.js.map