@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
@@ -46,21 +46,26 @@ import {
46
46
  DUPLICATE_ATTRIBUTE_CODE,
47
47
  DUPLICATE_EDGE_ID_CODE,
48
48
  DUPLICATE_NODE_CODE,
49
+ ELEMENT_ISSUE,
49
50
  EMPTY_INPUT_CODE,
50
51
  ENCODING_FALLBACK_CODE,
51
52
  GRAPH_NOT_FOUND_CODE,
52
53
  ID_MERGED_CODE,
53
54
  ID_TEXT_TYPE_CODE,
55
+ INPUT_ISSUE,
54
56
  INVALID_ENCODING_CODE,
55
57
  INVALID_UTF8_CODE,
58
+ JSON_NONSTANDARD_NUMBER_CODE,
56
59
  MISSING_ENDPOINT_CODE,
57
60
  MISSING_ID_CODE,
61
+ MULTI_ASPECT_FRAGMENT_CODE,
58
62
  MULTIPLE_GRAPHS_CODE,
59
63
  NO_GRAPH_CODE,
60
64
  OPTION_IGNORED_CODE,
61
65
  PARENT_CYCLE_CODE,
62
66
  PRECISION_CODE,
63
67
  ROLE_TAKEN_CODE,
68
+ SINGLE_OBJECT_ASPECT_CODE,
64
69
  SINK_OPTION_CODE,
65
70
  STATUS_FAILED_CODE,
66
71
  STATUS_WARNING_CODE,
@@ -68,6 +73,7 @@ import {
68
73
  SYNTAX_CODE,
69
74
  TOO_LARGE_CODE,
70
75
  UNKNOWN_ATTR_TYPE_CODE,
76
+ UNKNOWN_ELEMENT_CODE,
71
77
  UNKNOWN_ENCODING_CODE,
72
78
  UNKNOWN_PARENT_CODE,
73
79
  WIDENED_CODE,
@@ -80,24 +86,30 @@ import {
80
86
  CxStructure,
81
87
  declareFresh,
82
88
  ExactInteger,
89
+ fitsF32,
83
90
  flipY,
91
+ headText,
84
92
  inexactLiteral,
85
93
  isRecord,
86
94
  JsonScanError,
95
+ keptPrecision,
87
96
  plainJson,
88
97
  positionDecl,
98
+ reportSharedBlock,
89
99
  reportTooDeep,
90
100
  scanAspects,
91
101
  zDecl,
92
102
  } from "../../common/json-elements.js";
93
103
  import {
94
104
  chooseGraph,
105
+ graphChosen,
95
106
  type ImportFormatDefaults,
96
107
  reportSinkOptions,
97
108
  reportUnusedOptions,
98
109
  type ResolvedImportOptions,
99
110
  resolveImportOptions,
100
111
  } from "../../common/options.js";
112
+ import { agree, plural } from "../../common/plural.js";
101
113
  import { ImportReportBuilder } from "../../common/report.js";
102
114
  import { weightFromValue } from "../../common/weights.js";
103
115
  import {
@@ -108,7 +120,7 @@ import {
108
120
  type ImportInput,
109
121
  type ImportReport,
110
122
  } from "../../types.js";
111
- import { zAsOption } from "../cx2/importer.js";
123
+ import { ORIGINAL_ID_ATTRIBUTE, zAsOption } from "../cx2/importer.js";
112
124
 
113
125
  /** The format name. */
114
126
  const CX_FORMAT = "cx";
@@ -116,36 +128,46 @@ const CX_FORMAT = "cx";
116
128
  /** The origin namespace of the per-element visual property columns. */
117
129
  const CX_BYPASS_NAMESPACE = "cx.bypass";
118
130
 
119
- /** The format-specific options of the CX importer. */
120
- export interface CxImportOptions extends GraphChoiceOptions {
131
+ /**
132
+ * The format-specific options of the CX importer.
133
+ * @category Built-in formats
134
+ */
135
+ export interface CxImportOptions extends GraphChoiceOptions, CommonImportOptions {
121
136
  /**
122
- * Where a node's `z` goes: "column" (default) keeps it in the f64 node column `z` (Cytoscape
123
- * writes a stacking order there); "position" makes it the third component of the position.
137
+ * Where Cytoscape's `z` value (a drawing order, not a depth) goes: "column" keeps it as a node
138
+ * attribute named `z`; "position" makes it the third coordinate of the position.
139
+ * @defaultValue "column"
124
140
  */
125
141
  zAs?: "column" | "position" | undefined;
126
142
  }
127
143
 
128
144
  /**
129
- * The issue codes the CX importer records (design section 1.2), by name: the codes shared with
130
- * the other importers (src/common/codes.ts) and the CX-specific ones. A key is the code without
145
+ * The issue codes the CX importer records, by name: the codes shared with
146
+ * the other importers and the CX-specific ones. A key is the code without
131
147
  * its severity and format prefixes.
148
+ * @category Built-in formats
132
149
  */
133
150
  export const CX_ISSUE = Object.freeze({
134
- /** The input is empty (fatal). */
151
+ ...INPUT_ISSUE,
152
+ ...ELEMENT_ISSUE,
153
+ /** The input is empty. The import stops. */
135
154
  EMPTY_INPUT: EMPTY_INPUT_CODE,
136
- /** The text is not JSON (fatal). */
155
+ /** The text is not valid JSON. The import stops. */
137
156
  SYNTAX: SYNTAX_CODE,
138
- /** Invalid UTF-8 (fatal). */
157
+ /** The input is not valid UTF-8. The import stops. */
139
158
  INVALID_UTF8: INVALID_UTF8_CODE,
140
- /** Invalid bytes in the encoding a BOM or the encoding option chose (fatal). */
159
+ /**
160
+ * Some bytes are not valid in the encoding that was chosen (by a byte order mark or the `encoding` option). The
161
+ * import stops.
162
+ */
141
163
  INVALID_ENCODING: INVALID_ENCODING_CODE,
142
164
  /** Bytes that are not UTF-8 were read as windows-1252. */
143
165
  ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
144
166
  /** An encoding the platform cannot decode was ignored. */
145
167
  UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
146
- /** The document is not a CX array, or it is CX2 (fatal; the message names CX2). */
168
+ /** The document is not a CX array, or it is CX2 (the message says so; read it as `cx2`). The import stops. */
147
169
  NOT_CX: "E_CX_NOT_CX",
148
- /** numberVerification holds another value than 2^48 - 1, or comes twice. */
170
+ /** NumberVerification holds another value than 2^48 - 1, or comes twice. */
149
171
  NUMBER_VERIFICATION: "W_CX_NUMBER_VERIFICATION",
150
172
  /** An old Cytoscape aspect name (visualProperties, subNetworks, ...) read under its cy name. */
151
173
  OLD_ASPECT_NAME: "W_CX_OLD_ASPECT_NAME",
@@ -153,11 +175,14 @@ export const CX_ISSUE = Object.freeze({
153
175
  GROUP_NODE_ADDED: "W_CX_GROUP_NODE_ADDED",
154
176
  /** Nodes or edges of the root network that no subnetwork holds: Cytoscape shows them in no network; not read. */
155
177
  ROOT_ONLY: "W_CX_ROOT_ONLY",
156
- /** The producer marked the document as failed (fatal). */
178
+ /** The program that wrote the file marked it as failed, so it is incomplete. The import stops. */
157
179
  STATUS_FAILED: STATUS_FAILED_CODE,
158
- /** The producer marked the document as successful with an error text. */
180
+ /** The program that wrote the file marked it as successful but added an error message; the message is shown. */
159
181
  STATUS_WARNING: STATUS_WARNING_CODE,
160
- /** A member of the array that is not a one-key aspect block, or an element that is not an object. */
182
+ /**
183
+ * A member of the file's top-level array that is not a block with one key (`{"nodes": [...]}`), or an element of a
184
+ * block that is not an object. It is skipped; the rest of the file is read.
185
+ */
161
186
  BAD_ASPECT_BLOCK: BAD_ASPECT_BLOCK_CODE,
162
187
  /** An aspect after the post-metadata or after the status, a third metaData. */
163
188
  ASPECT_ORDER: ASPECT_ORDER_CODE,
@@ -165,7 +190,10 @@ export const CX_ISSUE = Object.freeze({
165
190
  COUNT_MISMATCH: COUNT_MISMATCH_CODE,
166
191
  /** A value that does not parse as its data type; the cell is unset. */
167
192
  BAD_VALUE: BAD_VALUE_CODE,
168
- /** One attribute name with several data types: the column takes the wider one. */
193
+ /**
194
+ * An attribute's type was widened because a later value did not fit: an integer above 2^31 in an integer column,
195
+ * or two declared types for one attribute.
196
+ */
169
197
  WIDENED: WIDENED_CODE,
170
198
  /** A data type CX does not define; the value is kept as text. */
171
199
  UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
@@ -177,9 +205,9 @@ export const CX_ISSUE = Object.freeze({
177
205
  UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
178
206
  /** A group membership that would close a parent cycle; dropped. */
179
207
  PARENT_CYCLE: PARENT_CYCLE_CODE,
180
- /** The style rules of cyVisualProperties are not applied (issue #706). */
208
+ /** The style rules of cyVisualProperties are not applied; they are kept so a CX export writes them back. */
181
209
  STYLES_NOT_IMPORTED: STYLES_NOT_IMPORTED_CODE,
182
- /** A node without an @id. */
210
+ /** A node without an id (its `@id` key). The node is skipped. */
183
211
  MISSING_ID: MISSING_ID_CODE,
184
212
  /** An edge without s or t. */
185
213
  MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
@@ -189,37 +217,63 @@ export const CX_ISSUE = Object.freeze({
189
217
  DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
190
218
  /** An id that is not an integer. */
191
219
  INVALID_ID: "E_INVALID_ID",
220
+ /**
221
+ * A node or edge key CX does not define, an aspect graph-io keeps for writing back, or a table CX does not define;
222
+ * it is skipped.
223
+ */
224
+ UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
225
+ /** A member holding several aspects; each array-valued key is read as its own fragment. */
226
+ MULTI_ASPECT_FRAGMENT: MULTI_ASPECT_FRAGMENT_CODE,
227
+ /** An aspect written as one object, not an array of elements; read as one element. */
228
+ SINGLE_OBJECT_ASPECT: SINGLE_OBJECT_ASPECT_CODE,
229
+ /** The bare tokens NaN / Infinity / -Infinity (Python's json writes them), read as numbers. */
230
+ JSON_NONSTANDARD_NUMBER: JSON_NONSTANDARD_NUMBER_CODE,
192
231
  /** An edge endpoint naming no node of the graph (addMissingNodes false, the default). */
193
232
  UNKNOWN_NODE: "E_UNKNOWN_NODE",
194
233
  /** A weight that is not a number. */
195
234
  INVALID_WEIGHT: "E_INVALID_WEIGHT",
196
235
  /** An id spelled as a string or a non-integer literal; read as the integer. */
197
236
  ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
198
- /** An integer beyond 2^53: an id kept as its digits, a value stored as the nearest f64. */
237
+ /** An integer beyond 2^53 was stored as the nearest 64-bit float; pass `long: "string"` to keep every digit. */
199
238
  PRECISION: PRECISION_CODE,
200
239
  /** Two id texts merged under `ids: "number"`. */
201
240
  ID_MERGED: ID_MERGED_CODE,
202
- /** A collection read by import(): the other subnetworks are skipped. */
241
+ /**
242
+ * The file holds several graphs and only the first was read. It is not added when `graphIndex` or `graphName` chose the graph.
243
+ * `importAllGraphs()` reads every one.
244
+ */
203
245
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
204
- /** graphIndex or graphName names no subnetwork (fatal). */
246
+ /** `graphIndex` or `graphName` matches no subnetwork. The import stops. */
205
247
  GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
206
- /** graphName names several subnetworks (fatal). */
248
+ /** `graphName` matches several subnetworks. The import stops. */
207
249
  AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
208
- /** The input holds no graph (fatal). */
250
+ /** The input holds no graph. The import stops. */
209
251
  NO_GRAPH: NO_GRAPH_CODE,
210
- /** A column renamed because its name was taken. */
252
+ /**
253
+ * An attribute was renamed `<name>#<suffix>` because another attribute already has its name, for example
254
+ * two attributes declared with the same name.
255
+ */
211
256
  COLUMN_RENAMED: COLUMN_RENAMED_CODE,
212
- /** A column that lost its role because another column holds it. */
257
+ /**
258
+ * You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
259
+ * a plain attribute.
260
+ */
213
261
  ROLE_TAKEN: ROLE_TAKEN_CODE,
214
- /** The sink refused the direction. */
262
+ /**
263
+ * You read into a graph builder whose direction is already set, or which already holds edges, so the file is read
264
+ * with the builder's direction instead of its own.
265
+ */
215
266
  DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
216
- /** Edges forced to the policy's direction. */
267
+ /** Edges of the other direction were read with the direction `onMixedDirection` chose. */
217
268
  DIRECTION_FORCED: DIRECTION_FORCED_CODE,
218
- /** A common option CX has no use for. */
269
+ /** You set an option this format does not use; it had no effect. The message names the option. */
219
270
  OPTION_IGNORED: OPTION_IGNORED_CODE,
220
- /** A builder option the caller's sink does not honour. */
271
+ /**
272
+ * You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
273
+ * `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
274
+ */
221
275
  SINK_OPTION: SINK_OPTION_CODE,
222
- /** The input is beyond a size limit (fatal). */
276
+ /** The input is larger than graph-io's size limit. The import stops. */
223
277
  TOO_LARGE: TOO_LARGE_CODE,
224
278
  });
225
279
 
@@ -232,6 +286,7 @@ const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof Commo
232
286
  "weightFrom",
233
287
  "weightDtype",
234
288
  "long",
289
+ "restoreMangledIds",
235
290
  "errorLimit",
236
291
  "signal",
237
292
  "onProgress",
@@ -251,12 +306,21 @@ const ABORT_CHECK_INTERVAL = 64;
251
306
  const NUMBER_VERIFICATION_VALUES: ReadonlySet<string> = new Set(["281474976710655", "9223372036854775807"]);
252
307
 
253
308
  /** Old Cytoscape aspect names and the names they are read under. */
254
- const OLD_ASPECT_NAMES: Readonly<Record<string, string>> = {
255
- visualProperties: "cyVisualProperties",
256
- subNetworks: "cySubNetworks",
257
- networkRelations: "cyNetworkRelations",
258
- hiddenAttributes: "cyHiddenAttributes",
259
- };
309
+ const OLD_ASPECT_NAMES: ReadonlyMap<string, string> = new Map([
310
+ ["visualProperties", "cyVisualProperties"],
311
+ ["subNetworks", "cySubNetworks"],
312
+ ["networkRelations", "cyNetworkRelations"],
313
+ ["hiddenAttributes", "cyHiddenAttributes"],
314
+ ]);
315
+
316
+ /** The keys a nodes element defines. */
317
+ const NODE_KEYS: ReadonlySet<string> = new Set(["@id", "n", "r"]);
318
+
319
+ /** The keys an edges element defines. */
320
+ const EDGE_KEYS: ReadonlySet<string> = new Set(["@id", "s", "t", "i"]);
321
+
322
+ /** The tables cyTableColumn applies to. */
323
+ const TABLES: ReadonlySet<string> = new Set(["node_table", "edge_table", "network_table"]);
260
324
 
261
325
  /** The aspects the importer reads into the graph (everything else is kept verbatim). */
262
326
  const READ_ASPECTS: ReadonlySet<string> = new Set([
@@ -299,6 +363,9 @@ const ALSO_KEPT: ReadonlySet<string> = new Set([
299
363
  /** Cytoscape's table-cell style aspects (both spellings): style rules, kept and reported, never read. */
300
364
  const TABLE_STYLE_ASPECTS: readonly string[] = ["tableVisualProperties", "cyTableVisualProperties"];
301
365
 
366
+ /** The importer's own entries of meta.extra.cx, which an aspect of the same name would collide with. */
367
+ const OWN_EXTRA_KEYS: readonly string[] = ["groups", "subnetwork"];
368
+
302
369
  /** The first keys that make a head CX for sure, and the other aspect names a CX document may start with. */
303
370
  const CX_FIRST_KEYS: readonly string[] = ["numberVerification", "metaData"];
304
371
  const CX_ASPECT_KEYS: readonly string[] = [
@@ -339,14 +406,14 @@ interface CxType {
339
406
  }
340
407
 
341
408
  /** The CX data types, with the Java reader's float aliases. */
342
- const SCALARS: Readonly<Record<string, CxScalar>> = {
343
- string: "string",
344
- boolean: "boolean",
345
- integer: "integer",
346
- long: "long",
347
- double: "double",
348
- float: "double",
349
- };
409
+ const SCALARS: ReadonlyMap<string, CxScalar> = new Map([
410
+ ["string", "string"],
411
+ ["boolean", "boolean"],
412
+ ["integer", "integer"],
413
+ ["long", "long"],
414
+ ["double", "double"],
415
+ ["float", "double"],
416
+ ]);
350
417
 
351
418
  /**
352
419
  * Resolve a `d` value.
@@ -361,7 +428,7 @@ function cxType(d: unknown): CxType | null {
361
428
  return null;
362
429
  }
363
430
  const list = d.startsWith("list_of_");
364
- const scalar = SCALARS[list ? d.slice("list_of_".length) : d] as CxScalar | undefined;
431
+ const scalar = SCALARS.get(list ? d.slice("list_of_".length) : d);
365
432
  return scalar === undefined ? null : { scalar, list };
366
433
  }
367
434
 
@@ -492,7 +559,15 @@ function parseScalar(
492
559
  if (value === "Infinity" || value === "-Infinity") {
493
560
  return Number(value);
494
561
  }
495
- return DECIMAL_TEXT.test(value) ? Number(value) : BAD;
562
+ if (!DECIMAL_TEXT.test(value)) {
563
+ return BAD;
564
+ }
565
+ const n = Number(value);
566
+ if (!Number.isFinite(n)) {
567
+ // beyond the range of a double: Java's parseDouble gives Infinity too, but say so
568
+ onPrecision(value);
569
+ }
570
+ return n;
496
571
  }
497
572
  }
498
573
  }
@@ -653,15 +728,16 @@ function aspect(doc: CxDocument, name: string): readonly Held[] {
653
728
  * The inexact-literal bits of an element.
654
729
  * @param text - the element's JSON text
655
730
  * @param keys - the id keys to check, bit 1, 2, 4 in order
731
+ * @param depth - the depth of the element's own keys in the text (2: a member's single object)
656
732
  * @returns the bits
657
733
  */
658
- function inexactBits(text: string, keys: readonly string[]): number {
734
+ function inexactBits(text: string, keys: readonly string[], depth = 1): number {
659
735
  if (!/[0-9][.eE]/.test(text)) {
660
736
  return 0;
661
737
  }
662
738
  let bits = 0;
663
739
  keys.forEach((key, i) => {
664
- if (inexactLiteral(text, key)) {
740
+ if (inexactLiteral(text, key, depth)) {
665
741
  bits |= 1 << i;
666
742
  }
667
743
  });
@@ -709,6 +785,117 @@ function checkNumberVerification(value: unknown, count: number, report: ImportRe
709
785
  }
710
786
  }
711
787
 
788
+ /**
789
+ * Whether an aspect name is one CX (or NDEx, or Cytoscape) defines.
790
+ * @param name - the aspect name as written
791
+ * @returns true for a known aspect
792
+ */
793
+ function isCxAspect(name: string): boolean {
794
+ return (
795
+ READ_ASPECTS.has(name) ||
796
+ OLD_ASPECT_NAMES.has(name) ||
797
+ CX_ASPECT_KEYS.includes(name) ||
798
+ TABLE_STYLE_ASPECTS.includes(name)
799
+ );
800
+ }
801
+
802
+ /** What collectElement() needs across elements. */
803
+ interface CollectState {
804
+ readonly doc: CxDocument;
805
+ readonly report: ImportReportBuilder;
806
+ /** numberVerification elements read so far. */
807
+ verifications: number;
808
+ /** The precision callback of the kept aspects. */
809
+ readonly onKeptPrecision: (digits: string) => void;
810
+ }
811
+
812
+ /** One parsed aspect element and where it came from. */
813
+ interface CollectedElement {
814
+ readonly name: string;
815
+ readonly value: unknown;
816
+ readonly text: string;
817
+ readonly line: number;
818
+ readonly exact: boolean;
819
+ /** The depth of the element's own keys in text (2 for a member's single object). */
820
+ readonly depth: number;
821
+ }
822
+
823
+ /** The id keys of the aspects whose ids are checked for precision. */
824
+ const ID_KEYS: ReadonlyMap<string, readonly string[]> = new Map([
825
+ ["nodes", ["@id"]],
826
+ ["edges", ["@id", "s", "t"]],
827
+ ]);
828
+
829
+ /**
830
+ * File an attribute element by its attribute name n; one without a usable name is reported and skipped.
831
+ * @param table - the aspect's attribute table
832
+ * @param name - the aspect
833
+ * @param held - the element
834
+ * @param report - the report
835
+ */
836
+ function fileAttribute(table: Map<string, Held[]>, name: string, held: Held, report: ImportReportBuilder): void {
837
+ const { value, line } = held;
838
+ const n = isRecord(value) ? value.n : undefined;
839
+ if (n === undefined || n === null) {
840
+ report.error("missing-value", MISSING_ID_CODE, `a ${name} element has no attribute name n; skipped`, {
841
+ line,
842
+ element: name,
843
+ });
844
+ return;
845
+ }
846
+ if (typeof n !== "string" || n === "") {
847
+ report.error(
848
+ "validation-error",
849
+ BAD_VALUE_CODE,
850
+ `a ${name} element has the attribute name n ${shown(n)}, not a non-empty string; skipped`,
851
+ { line, element: name },
852
+ );
853
+ return;
854
+ }
855
+ if (!table.has(n)) {
856
+ table.set(n, []);
857
+ }
858
+ table.get(n)?.push(held);
859
+ }
860
+
861
+ /**
862
+ * File one parsed aspect element: kept verbatim, checked (numberVerification), filed by attribute
863
+ * name (an attribute element without a usable name n is reported and skipped) or by aspect.
864
+ * @param state - the document being collected
865
+ * @param element - the element
866
+ */
867
+ function collectElement(state: CollectState, element: CollectedElement): void {
868
+ const { doc, report } = state;
869
+ const { name, value, text, line, exact, depth } = element;
870
+ doc.structure.element(name, value, line);
871
+ if (ALSO_KEPT.has(name) || !READ_ASPECTS.has(name)) {
872
+ if (!doc.kept.has(name)) {
873
+ doc.kept.set(name, []);
874
+ }
875
+ // numberVerification holds Java's Long.MAX_VALUE in every Cytoscape file: checked, never warned
876
+ const onPrecision = name === "numberVerification" ? undefined : state.onKeptPrecision;
877
+ doc.kept.get(name)?.push(exact ? plainJson(value, onPrecision) : value);
878
+ }
879
+ if (!READ_ASPECTS.has(name) || name === "metaData" || name === "status") {
880
+ return;
881
+ }
882
+ if (name === "numberVerification") {
883
+ checkNumberVerification(value, ++state.verifications, report, line);
884
+ return;
885
+ }
886
+ const ids = ID_KEYS.get(name);
887
+ const held: Held = { value, line, inexact: ids === undefined ? 0 : inexactBits(text, ids, depth) };
888
+ const table = attributeTable(doc, name);
889
+ if (table !== null) {
890
+ fileAttribute(table, name, held, report);
891
+ return;
892
+ }
893
+ if (!doc.aspects.has(name)) {
894
+ doc.aspects.set(name, []);
895
+ }
896
+ doc.aspects.get(name)?.push(held);
897
+ }
898
+
712
899
  /**
713
900
  * Read the whole document through the streaming scanner, checking its structure.
714
901
  * @param input - the input
@@ -721,17 +908,37 @@ async function readDocument(
721
908
  report: ImportReportBuilder,
722
909
  options: ResolvedImportOptions,
723
910
  ): Promise<CxDocument> {
724
- const structure = new CxStructure(report);
911
+ const structure = new CxStructure(report, (name) => OLD_ASPECT_NAMES.get(name) ?? name);
725
912
  const doc: CxDocument = {
726
913
  aspects: new Map(),
727
914
  attributes: { node: new Map(), edge: new Map(), network: new Map() },
728
915
  kept: new Map(),
729
916
  structure,
730
917
  };
918
+ const state: CollectState = {
919
+ doc,
920
+ report,
921
+ verifications: 0,
922
+ onKeptPrecision: keptPrecision(report, "meta.extra.cx"),
923
+ };
731
924
  let sinceCheck = 0;
732
- let verifications = 0;
925
+ let members = 0;
926
+ let known = 0;
927
+ const unknownNames = new Set<string>();
928
+ const lastMember = { block: -1 };
929
+ const countMember = (block: number, name: string | null): void => {
930
+ if (block !== lastMember.block) {
931
+ lastMember.block = block;
932
+ members++;
933
+ }
934
+ if (name !== null && isCxAspect(name)) {
935
+ known++;
936
+ } else if (name !== null) {
937
+ unknownNames.add(name);
938
+ }
939
+ };
733
940
  const canonical = (name: string, line: number): string => {
734
- const renamed = OLD_ASPECT_NAMES[name] as string | undefined;
941
+ const renamed = OLD_ASPECT_NAMES.get(name);
735
942
  if (renamed === undefined) {
736
943
  return name;
737
944
  }
@@ -744,51 +951,11 @@ async function readDocument(
744
951
  );
745
952
  return renamed;
746
953
  };
747
- const collect = (name: string, value: unknown, text: string, line: number, exact: boolean): void => {
748
- structure.element(name, value, line);
749
- if (ALSO_KEPT.has(name) || !READ_ASPECTS.has(name)) {
750
- if (!doc.kept.has(name)) {
751
- doc.kept.set(name, []);
752
- }
753
- doc.kept.get(name)?.push(exact ? plainJson(value) : value);
754
- }
755
- if (!READ_ASPECTS.has(name) || name === "metaData" || name === "status") {
756
- return;
757
- }
758
- if (name === "numberVerification") {
759
- checkNumberVerification(value, ++verifications, report, line);
760
- return;
761
- }
762
- let inexact = 0;
763
- if (name === "nodes") {
764
- inexact = inexactBits(text, ["@id"]);
765
- } else if (name === "edges") {
766
- inexact = inexactBits(text, ["@id", "s", "t"]);
767
- }
768
- const held: Held = { value, line, inexact };
769
- const table = attributeTable(doc, name);
770
- if (table !== null) {
771
- if (!isRecord(value) || typeof value.n !== "string") {
772
- report.error("missing-value", MISSING_ID_CODE, `a ${name} element has no attribute name n; skipped`, {
773
- line,
774
- element: name,
775
- });
776
- return;
777
- }
778
- const key = value.n;
779
- if (!table.has(key)) {
780
- table.set(key, []);
781
- }
782
- table.get(key)?.push(held);
783
- return;
784
- }
785
- if (!doc.aspects.has(name)) {
786
- doc.aspects.set(name, []);
787
- }
788
- doc.aspects.get(name)?.push(held);
954
+ const collect = (name: string, value: unknown, text: string, line: number, exact: boolean, depth = 1): void => {
955
+ collectElement(state, { name, value, text, line, exact, depth });
789
956
  };
790
957
  try {
791
- for await (const event of scanAspects(textChunks(input, report, options))) {
958
+ for await (const event of scanAspects(textChunks(input, report, options), report)) {
792
959
  if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
793
960
  sinceCheck = 0;
794
961
  throwIfAborted(options.signal);
@@ -797,7 +964,7 @@ async function readDocument(
797
964
  case "root":
798
965
  report.fail(
799
966
  CX_ISSUE.NOT_CX,
800
- `a CX document is a JSON array of aspects; found ${isRecord(event.value) ? "an object" : shown(event.value)}`,
967
+ `a CX document is a JSON array of aspects; found ${event.object ? "an object" : shown(event.value)}`,
801
968
  { line: event.line },
802
969
  );
803
970
  break;
@@ -812,11 +979,21 @@ async function readDocument(
812
979
  }
813
980
  const keys = isRecord(value) ? Object.keys(value) : [];
814
981
  if (isRecord(value) && keys.length === 1 && isRecord(value[keys[0]])) {
982
+ countMember(event.block, keys[0]);
815
983
  const name = canonical(keys[0], event.line);
984
+ if (isCxAspect(name)) {
985
+ report.warning(
986
+ "validation-error",
987
+ CX_ISSUE.SINGLE_OBJECT_ASPECT,
988
+ `the "${keys[0]}" aspect is one object, not an array of elements; read as one element`,
989
+ { line: event.line, element: keys[0] },
990
+ );
991
+ }
816
992
  structure.block(name, event.line);
817
- collect(name, value[keys[0]], "", event.line, event.exact);
993
+ collect(name, value[keys[0]], event.text, event.line, event.exact, 2);
818
994
  break;
819
995
  }
996
+ countMember(event.block, null);
820
997
  report.error(
821
998
  "parse-error",
822
999
  BAD_ASPECT_BLOCK_CODE,
@@ -826,21 +1003,24 @@ async function readDocument(
826
1003
  break;
827
1004
  }
828
1005
  case "block":
1006
+ countMember(event.block, event.aspect);
1007
+ reportSharedBlock(report, event);
829
1008
  structure.block(canonical(event.aspect, event.line), event.line);
830
1009
  break;
831
1010
  case "element": {
832
- const name = OLD_ASPECT_NAMES[event.aspect] ?? event.aspect;
1011
+ const name = OLD_ASPECT_NAMES.get(event.aspect) ?? event.aspect;
833
1012
  collect(name, event.value, event.text, event.line, event.exact);
834
1013
  break;
835
1014
  }
836
1015
  case "deep":
1016
+ countMember(event.block, event.aspect);
837
1017
  reportTooDeep(report, event);
838
1018
  break;
839
1019
  case "extraKeys":
840
1020
  report.error(
841
1021
  "parse-error",
842
1022
  BAD_ASPECT_BLOCK_CODE,
843
- `the "${event.aspect}" fragment holds more keys (${event.keys.join(", ")}); a fragment has one key, the others are skipped`,
1023
+ `the "${event.aspect}" fragment holds more keys (${event.keys.join(", ")}) whose values are not arrays; a fragment has one key, the others are skipped`,
844
1024
  { line: event.line, element: event.aspect },
845
1025
  );
846
1026
  break;
@@ -854,6 +1034,20 @@ async function readDocument(
854
1034
  }
855
1035
  throw err;
856
1036
  }
1037
+ if (members > 0 && known === 0) {
1038
+ const found = [...unknownNames].slice(0, 5).map((n) => JSON.stringify(n));
1039
+ const hint = unknownNames.has("data") ? ": Cytoscape.js elements?" : "";
1040
+ const seen = found.length > 0 ? ` (found ${found.join(", ")}${hint})` : "";
1041
+ report.fail(CX_ISSUE.NOT_CX, `no member of the array is a CX aspect${seen}; the input is not CX`);
1042
+ }
1043
+ if (structure.hasStatus && !structure.statusWellFormed()) {
1044
+ report.error(
1045
+ "validation-error",
1046
+ BAD_VALUE_CODE,
1047
+ `the status element is ${shown(structure.status ?? null)}, not an object with a boolean success; ignored`,
1048
+ { element: "status" },
1049
+ );
1050
+ }
857
1051
  structure.checkCounts();
858
1052
  return doc;
859
1053
  }
@@ -888,11 +1082,13 @@ function refId(raw: unknown): NodeId | null {
888
1082
  }
889
1083
 
890
1084
  /**
891
- * A member list: the ids of an array, or null for "all".
1085
+ * A member list: the ids of an array, or null for "all" (an absent list is empty).
892
1086
  * @param raw - the list
1087
+ * @param onBad - told about a list that is neither an array nor "all" (BAD_VALUE) and about a
1088
+ * member that is not an id (INVALID_ID)
893
1089
  * @returns the set, or null for every element
894
1090
  */
895
- function memberSet(raw: unknown): Set<NodeId> | null {
1091
+ function memberSet(raw: unknown, onBad?: (code: string, message: string) => void): Set<NodeId> | null {
896
1092
  if (raw === "all") {
897
1093
  return null;
898
1094
  }
@@ -900,87 +1096,79 @@ function memberSet(raw: unknown): Set<NodeId> | null {
900
1096
  if (Array.isArray(raw)) {
901
1097
  for (const item of raw) {
902
1098
  const id = refId(item);
903
- if (id !== null) {
1099
+ if (id === null) {
1100
+ onBad?.(CX_ISSUE.INVALID_ID, `the member ${shown(item)} is not an id; ignored`);
1101
+ } else {
904
1102
  out.add(id);
905
1103
  }
906
1104
  }
1105
+ } else if (raw !== undefined) {
1106
+ onBad?.(BAD_VALUE_CODE, `the member list ${shown(raw)} is neither a list of ids nor "all"; read as empty`);
907
1107
  }
908
1108
  return out;
909
1109
  }
910
1110
 
911
1111
  /**
912
- * The graphs of a document, in the order Cytoscape opens them.
913
- * @param doc - the document
914
- * @returns one plan per graph (at least one)
1112
+ * The id of an element of a structural aspect (cySubNetworks, cyViews, cyNetworkRelations), or
1113
+ * null with an issue: E_BAD_ASPECT_BLOCK for an element that is not an object, E_MISSING_ID or
1114
+ * E_INVALID_ID for its id.
1115
+ * @param held - the element
1116
+ * @param name - the aspect
1117
+ * @param key - the id key
1118
+ * @param report - the report
1119
+ * @returns the element and its id, or null
915
1120
  */
916
- function graphPlans(doc: CxDocument): GraphPlan[] {
917
- const subs = new Map<NodeId, { nodes: Set<NodeId> | null; edges: Set<NodeId> | null }>();
918
- for (const { value } of aspect(doc, "cySubNetworks")) {
919
- const id = isRecord(value) ? refId(value["@id"]) : null;
920
- if (id !== null && isRecord(value) && !subs.has(id)) {
921
- subs.set(id, { nodes: memberSet(value.nodes), edges: memberSet(value.edges) });
922
- }
923
- }
924
- const relationNames = new Map<NodeId, string>();
925
- const order: NodeId[] = [];
926
- const views = new Map<NodeId, NodeId[]>();
927
- const addView = (sub: NodeId, view: NodeId): void => {
928
- const list = views.get(sub) ?? [];
929
- if (!list.includes(view)) {
930
- list.push(view);
931
- }
932
- views.set(sub, list);
933
- };
934
- for (const { value } of aspect(doc, "cyNetworkRelations")) {
935
- if (!isRecord(value)) {
936
- continue;
937
- }
938
- const child = refId(value.c);
939
- if (child === null) {
940
- continue;
941
- }
942
- if (value.r === "view") {
943
- const parent = refId(value.p);
944
- if (parent !== null) {
945
- addView(parent, child);
946
- }
947
- continue;
948
- }
949
- if (typeof value.name === "string") {
950
- relationNames.set(child, value.name);
951
- }
952
- if (subs.has(child) && !order.includes(child)) {
953
- order.push(child);
954
- }
1121
+ function structuralId(
1122
+ held: Held,
1123
+ name: string,
1124
+ key: string,
1125
+ report: ImportReportBuilder,
1126
+ ): { readonly value: Record<string, unknown>; readonly id: NodeId } | null {
1127
+ const { value, line } = held;
1128
+ if (!isRecord(value)) {
1129
+ report.error(
1130
+ "parse-error",
1131
+ BAD_ASPECT_BLOCK_CODE,
1132
+ `a ${name} element is ${shown(value)}, not an object; skipped`,
1133
+ {
1134
+ line,
1135
+ element: name,
1136
+ },
1137
+ );
1138
+ return null;
955
1139
  }
956
- for (const { value } of aspect(doc, "cyViews")) {
957
- const view = isRecord(value) ? refId(value["@id"]) : null;
958
- const sub = isRecord(value) ? refId(value.s) : null;
959
- if (view !== null && sub !== null) {
960
- addView(sub, view);
961
- }
1140
+ if (value[key] === undefined || value[key] === null) {
1141
+ report.error("missing-value", MISSING_ID_CODE, `a ${name} element has no ${key}; skipped`, {
1142
+ line,
1143
+ element: name,
1144
+ });
1145
+ return null;
962
1146
  }
963
- for (const id of subs.keys()) {
964
- if (!order.includes(id)) {
965
- order.push(id);
966
- }
1147
+ const id = refId(value[key]);
1148
+ if (id === null) {
1149
+ report.error(
1150
+ "validation-error",
1151
+ CX_ISSUE.INVALID_ID,
1152
+ `a ${name} element has the ${key} ${shown(value[key])}, not an integer id; skipped`,
1153
+ {
1154
+ line,
1155
+ element: name,
1156
+ },
1157
+ );
1158
+ return null;
967
1159
  }
968
- const networkName = (sub: NodeId | null): string | null => {
969
- let found: string | null = null;
970
- for (const { value } of doc.attributes.network.get("name") ?? []) {
971
- if (!isRecord(value) || typeof value.v !== "string") {
972
- continue;
973
- }
974
- const scope = value.s === undefined ? null : refId(value.s);
975
- if (scope === sub) {
976
- return value.v;
977
- }
978
- if (scope === null) {
979
- found ??= value.v;
980
- }
981
- }
982
- return found;
983
- };
1160
+ return { value, id };
1161
+ }
1162
+
1163
+ /**
1164
+ * The graphs of a document, in the order Cytoscape opens them.
1165
+ * @param doc - the document
1166
+ * @param report - where malformed subnetworks, views and relations are recorded
1167
+ * @returns one plan per graph (at least one)
1168
+ */
1169
+ function graphPlans(doc: CxDocument, report: ImportReportBuilder): GraphPlan[] {
1170
+ const subs = readSubnetworks(doc, report);
1171
+ const { relationNames, order, views } = readRelations(doc, subs, report);
984
1172
  if (order.length <= 1) {
985
1173
  const sub = order.length === 1 ? order[0] : null;
986
1174
  const members = sub === null ? undefined : subs.get(sub);
@@ -991,10 +1179,11 @@ function graphPlans(doc: CxDocument): GraphPlan[] {
991
1179
  if (graphViews.length === 0) {
992
1180
  graphViews = layoutViews(doc);
993
1181
  }
1182
+ const relationName = sub === null ? null : (relationNames.get(sub) ?? null);
994
1183
  return [
995
1184
  {
996
1185
  index: 0,
997
- name: (sub === null ? null : (relationNames.get(sub) ?? null)) ?? networkName(sub),
1186
+ name: relationName ?? networkName(doc, sub),
998
1187
  subnetwork: sub,
999
1188
  nodes: members?.nodes ?? null,
1000
1189
  edges: members?.edges ?? null,
@@ -1004,7 +1193,7 @@ function graphPlans(doc: CxDocument): GraphPlan[] {
1004
1193
  }
1005
1194
  return order.map((sub, index) => ({
1006
1195
  index,
1007
- name: relationNames.get(sub) ?? networkName(sub),
1196
+ name: relationNames.get(sub) ?? networkName(doc, sub),
1008
1197
  subnetwork: sub,
1009
1198
  nodes: subs.get(sub)?.nodes ?? null,
1010
1199
  edges: subs.get(sub)?.edges ?? null,
@@ -1012,6 +1201,139 @@ function graphPlans(doc: CxDocument): GraphPlan[] {
1012
1201
  }));
1013
1202
  }
1014
1203
 
1204
+ /** A subnetwork's members: null when the element does not list them. */
1205
+ type Subnetwork = { nodes: Set<NodeId> | null; edges: Set<NodeId> | null };
1206
+
1207
+ /**
1208
+ * The subnetworks a document declares (cySubNetworks), by id; a repeated id is reported and skipped.
1209
+ * @param doc - the document
1210
+ * @param report - the report
1211
+ * @returns the subnetworks
1212
+ */
1213
+ function readSubnetworks(doc: CxDocument, report: ImportReportBuilder): Map<NodeId, Subnetwork> {
1214
+ const subs = new Map<NodeId, Subnetwork>();
1215
+ for (const held of aspect(doc, "cySubNetworks")) {
1216
+ const sub = structuralId(held, "cySubNetworks", "@id", report);
1217
+ if (sub === null) {
1218
+ continue;
1219
+ }
1220
+ const { value, id } = sub;
1221
+ if (subs.has(id)) {
1222
+ report.error(
1223
+ "validation-error",
1224
+ BAD_ASPECT_BLOCK_CODE,
1225
+ `a second cySubNetworks element has the @id ${String(id)}; skipped, the first is used`,
1226
+ { line: held.line, element: "cySubNetworks" },
1227
+ );
1228
+ continue;
1229
+ }
1230
+ const onBad = (code: string, message: string): void => {
1231
+ report.error("validation-error", code, `subnetwork ${String(id)}: ${message}`, {
1232
+ line: held.line,
1233
+ element: "cySubNetworks",
1234
+ });
1235
+ };
1236
+ subs.set(id, { nodes: memberSet(value.nodes, onBad), edges: memberSet(value.edges, onBad) });
1237
+ }
1238
+ return subs;
1239
+ }
1240
+
1241
+ /**
1242
+ * The network relations of a document: each subnetwork's name, the order Cytoscape opens them in
1243
+ * (the relations' order, then the other subnetworks), and each one's views.
1244
+ * @param doc - the document
1245
+ * @param subs - the subnetworks
1246
+ * @param report - the report
1247
+ * @returns the names, the order and the views
1248
+ */
1249
+ function readRelations(
1250
+ doc: CxDocument,
1251
+ subs: ReadonlyMap<NodeId, Subnetwork>,
1252
+ report: ImportReportBuilder,
1253
+ ): { relationNames: Map<NodeId, string>; order: NodeId[]; views: Map<NodeId, NodeId[]> } {
1254
+ const relationNames = new Map<NodeId, string>();
1255
+ const order: NodeId[] = [];
1256
+ const views = new Map<NodeId, NodeId[]>();
1257
+ const addView = (sub: NodeId, view: NodeId): void => {
1258
+ const list = views.get(sub) ?? [];
1259
+ if (!list.includes(view)) {
1260
+ list.push(view);
1261
+ }
1262
+ views.set(sub, list);
1263
+ };
1264
+
1265
+ for (const held of aspect(doc, "cyNetworkRelations")) {
1266
+ const relation = structuralId(held, "cyNetworkRelations", "c", report);
1267
+ if (relation !== null) {
1268
+ fileRelation(relation, subs, { relationNames, order, addView });
1269
+ }
1270
+ }
1271
+ for (const held of aspect(doc, "cyViews")) {
1272
+ const view = structuralId(held, "cyViews", "@id", report);
1273
+ const sub = view === null ? null : refId(view.value.s);
1274
+ if (view !== null && sub !== null) {
1275
+ addView(sub, view.id);
1276
+ }
1277
+ }
1278
+ order.push(...[...subs.keys()].filter((id) => !order.includes(id)));
1279
+ return { relationNames, order, views };
1280
+ }
1281
+
1282
+ /**
1283
+ * File one network relation: a view of a subnetwork, or a subnetwork's name and its place in the order.
1284
+ * @param relation - the relation element and its child id
1285
+ * @param relation.value - the element
1286
+ * @param relation.id - the child: a view or a subnetwork
1287
+ * @param subs - the subnetworks
1288
+ * @param out - where the relation is filed
1289
+ * @param out.relationNames - each subnetwork's name
1290
+ * @param out.order - the subnetworks in relation order
1291
+ * @param out.addView - records a view of a subnetwork
1292
+ */
1293
+ function fileRelation(
1294
+ relation: { readonly value: Record<string, unknown>; readonly id: NodeId },
1295
+ subs: ReadonlyMap<NodeId, Subnetwork>,
1296
+ out: { relationNames: Map<NodeId, string>; order: NodeId[]; addView: (sub: NodeId, view: NodeId) => void },
1297
+ ): void {
1298
+ const { value, id: child } = relation;
1299
+ if (value.r === "view") {
1300
+ const parent = refId(value.p);
1301
+ if (parent !== null) {
1302
+ out.addView(parent, child);
1303
+ }
1304
+ return;
1305
+ }
1306
+ if (typeof value.name === "string") {
1307
+ out.relationNames.set(child, value.name);
1308
+ }
1309
+ if (subs.has(child) && !out.order.includes(child)) {
1310
+ out.order.push(child);
1311
+ }
1312
+ }
1313
+
1314
+ /**
1315
+ * A subnetwork's name from the network attributes: the one scoped to it, else the unscoped one.
1316
+ * @param doc - the document
1317
+ * @param sub - the subnetwork, or null
1318
+ * @returns the name, or null
1319
+ */
1320
+ function networkName(doc: CxDocument, sub: NodeId | null): string | null {
1321
+ let found: string | null = null;
1322
+ for (const { value } of doc.attributes.network.get("name") ?? []) {
1323
+ if (!isRecord(value) || typeof value.v !== "string") {
1324
+ continue;
1325
+ }
1326
+ const scope = value.s === undefined ? null : refId(value.s);
1327
+ if (scope === sub) {
1328
+ return value.v;
1329
+ }
1330
+ if (scope === null) {
1331
+ found ??= value.v;
1332
+ }
1333
+ }
1334
+ return found;
1335
+ }
1336
+
1015
1337
  /**
1016
1338
  * The distinct views the layout entries name, in order of appearance.
1017
1339
  * @param doc - the document
@@ -1072,12 +1394,18 @@ class CxReader {
1072
1394
  /** Every edge id of the root network. */
1073
1395
  private readonly rootEdges = new Set<NodeId>();
1074
1396
 
1075
- /** CX node id -> sink node index, for this graph. */
1397
+ /** CX node id (as written, before the `ids` option) -> sink node index, for this graph. */
1076
1398
  private readonly nodeRows = new Map<NodeId, number>();
1077
1399
 
1078
- /** CX edge id -> sink edge index, for this graph. */
1400
+ /** Sink node id -> sink node index: two CX ids the `ids` option makes one are one node. */
1401
+ private readonly sinkRows = new Map<NodeId, number>();
1402
+
1403
+ /** CX edge id (as written) -> sink edge index, for this graph. */
1079
1404
  private readonly edgeRows = new Map<NodeId, number>();
1080
1405
 
1406
+ /** CX node id -> the id given to the sink, for the nodes whose original id was restored. */
1407
+ private readonly sinkIds = new Map<NodeId, NodeId>();
1408
+
1081
1409
  /** The subnetwork ids of the document. */
1082
1410
  private readonly subnetworks = new Set<NodeId>();
1083
1411
 
@@ -1148,6 +1476,7 @@ class CxReader {
1148
1476
  this.direction.setHeader(true);
1149
1477
  this.checkMembers();
1150
1478
  this.checkRootOnly();
1479
+ this.checkTableColumns();
1151
1480
  const nodeColumns = this.declareAttributes("node");
1152
1481
  this.readNodes();
1153
1482
  this.readGroups();
@@ -1160,9 +1489,14 @@ class CxReader {
1160
1489
  this.readVisualProperties();
1161
1490
  this.readProvenance();
1162
1491
  for (const [kind, count] of this.dangling) {
1163
- report.warning("validation-error", DANGLING_REFERENCE_CODE, `${count} ${kind}(s) name nothing; ignored`, {
1164
- element: kind,
1165
- });
1492
+ report.warning(
1493
+ "validation-error",
1494
+ DANGLING_REFERENCE_CODE,
1495
+ `${count} ${kind}${plural(count)} ${agree(count, "names", "name")} nothing; ignored`,
1496
+ {
1497
+ element: kind,
1498
+ },
1499
+ );
1166
1500
  }
1167
1501
  this.setMeta();
1168
1502
  }
@@ -1194,16 +1528,6 @@ class CxReader {
1194
1528
  return this.plan.nodes === null || this.plan.nodes.has(id);
1195
1529
  }
1196
1530
 
1197
- /**
1198
- * The key of the node and edge row maps for a CX id as an aspect writes it: the id the `ids`
1199
- * option makes of it, which is what idOf() stored the element under.
1200
- * @param raw - the CX id
1201
- * @returns the row key
1202
- */
1203
- private key(raw: NodeId): NodeId {
1204
- return this.options.ids === "keep" ? raw : this.coercer.value(raw);
1205
- }
1206
-
1207
1531
  /**
1208
1532
  * Whether a scoped value applies to this graph: unscoped, or scoped to its subnetwork. A scope
1209
1533
  * naming no subnetwork is counted as dangling.
@@ -1225,14 +1549,16 @@ class CxReader {
1225
1549
  }
1226
1550
 
1227
1551
  /**
1228
- * The id of an element, by the CX id rule and the `ids` option.
1552
+ * The id of an element by the CX id rule, as the document's references name it (the `ids`
1553
+ * option applies only to the id the sink holds, sinkId()).
1229
1554
  * @param raw - the parsed id
1230
1555
  * @param inexact - whether the literal was not a plain integer
1231
1556
  * @param element - the element name for issues
1557
+ * @param line - the element's line
1232
1558
  * @param edgeId - whether this is an edge's own id (stored in the f64 id column, not kept as digits)
1233
1559
  * @returns the id, or null when it was reported
1234
1560
  */
1235
- private idOf(raw: unknown, inexact: boolean, element: string, edgeId = false): NodeId | null {
1561
+ private idOf(raw: unknown, inexact: boolean, element: string, line: number, edgeId = false): NodeId | null {
1236
1562
  try {
1237
1563
  const parsed = cxId(raw, inexact);
1238
1564
  if (parsed.note === "text") {
@@ -1240,7 +1566,7 @@ class CxReader {
1240
1566
  "coercion",
1241
1567
  ID_TEXT_TYPE_CODE,
1242
1568
  `${element}: the id ${shown(raw)} is not written as an integer; read as ${String(parsed.id)}`,
1243
- { element },
1569
+ { line, element },
1244
1570
  );
1245
1571
  } else if (parsed.note === "precision" && edgeId) {
1246
1572
  // the edge is told apart by its digits, but the id column is f64
@@ -1248,7 +1574,7 @@ class CxReader {
1248
1574
  "precision",
1249
1575
  PRECISION_CODE,
1250
1576
  `${element}: the edge id ${String(parsed.id)} is beyond 2^53; the id column holds the nearest double`,
1251
- { element },
1577
+ { line, element },
1252
1578
  `${PRECISION_CODE}:edge`,
1253
1579
  );
1254
1580
  } else if (parsed.note === "precision") {
@@ -1257,31 +1583,101 @@ class CxReader {
1257
1583
  "precision",
1258
1584
  PRECISION_CODE,
1259
1585
  `${element}: the id ${String(parsed.id)} is beyond 2^53; kept as its digits (a string id)`,
1260
- { element },
1586
+ { line, element },
1261
1587
  );
1262
1588
  }
1263
- return this.options.ids === "keep" ? parsed.id : this.coercer.value(parsed.id);
1589
+ return parsed.id;
1264
1590
  } catch (err) {
1265
- this.report.recordError(err, { element });
1591
+ this.report.recordError(err, { line, element });
1266
1592
  return null;
1267
1593
  }
1268
1594
  }
1269
1595
 
1596
+ /**
1597
+ * The id the sink holds a CX node under: its restored original id, else the `ids` option applied.
1598
+ * @param id - the CX id
1599
+ * @returns the sink id
1600
+ */
1601
+ private sinkId(id: NodeId): NodeId {
1602
+ const original = this.sinkIds.get(id);
1603
+ if (original !== undefined) {
1604
+ return original;
1605
+ }
1606
+ return this.options.ids === "keep" ? id : this.coercer.value(id);
1607
+ }
1608
+
1609
+ /**
1610
+ * Add a node to the sink (once per sink id) and map its CX id to the row.
1611
+ * @param id - the CX id
1612
+ * @returns the row, and whether the sink id was new
1613
+ */
1614
+ private addNode(id: NodeId): { readonly row: number; readonly added: boolean } {
1615
+ const sinkId = this.sinkId(id);
1616
+ const existing = this.sinkRows.get(sinkId);
1617
+ if (existing !== undefined) {
1618
+ this.nodeRows.set(id, existing);
1619
+ return { row: existing, added: false };
1620
+ }
1621
+ const row = this.sink.addNode(sinkId);
1622
+ this.sinkRows.set(sinkId, row);
1623
+ this.nodeRows.set(id, row);
1624
+ return { row, added: true };
1625
+ }
1626
+
1270
1627
  /**
1271
1628
  * Record a long value beyond 2^53 (once).
1272
1629
  * @param name - the attribute
1273
1630
  * @param digits - the value
1274
1631
  */
1275
1632
  private precision(name: string, digits: string): void {
1633
+ const overflow = !Number.isFinite(Number(digits));
1276
1634
  this.report.warnOnce(
1277
1635
  "precision",
1278
1636
  PRECISION_CODE,
1279
- `"${name}": ${digits} is beyond 2^53; stored as the nearest double`,
1637
+ overflow
1638
+ ? `"${name}": ${digits} is beyond the range of a double; stored as ${String(Number(digits))}`
1639
+ : `"${name}": ${digits} is beyond 2^53; stored as the nearest double`,
1280
1640
  { element: name },
1281
1641
  `${PRECISION_CODE}:value`,
1282
1642
  );
1283
1643
  }
1284
1644
 
1645
+ /**
1646
+ * Report the keys of a node or edge element CX does not define, once per key.
1647
+ * @param record - the element
1648
+ * @param known - the keys it defines
1649
+ * @param what - nodes or edges
1650
+ * @param line - its line
1651
+ */
1652
+ private unknownKeys(record: Record<string, unknown>, known: ReadonlySet<string>, what: string, line: number): void {
1653
+ for (const key of Object.keys(record)) {
1654
+ if (!known.has(key)) {
1655
+ this.report.warnOnce(
1656
+ "unsupported",
1657
+ UNKNOWN_ELEMENT_CODE,
1658
+ `the ${what} key "${key}" is not defined by CX; its values are not read`,
1659
+ { line, element: key },
1660
+ `${UNKNOWN_ELEMENT_CODE}:${what}:${key}`,
1661
+ );
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ /** Report cyTableColumn entries for a table CX does not define (once per table name). */
1667
+ private checkTableColumns(): void {
1668
+ for (const { value, line } of aspect(this.doc, "cyTableColumn")) {
1669
+ if (isRecord(value) && typeof value.applies_to === "string" && !TABLES.has(value.applies_to)) {
1670
+ this.report.warnOnce(
1671
+ "unsupported",
1672
+ UNKNOWN_ELEMENT_CODE,
1673
+ `a cyTableColumn entry applies to "${value.applies_to}", which is not node_table, edge_table or network_table; ignored`,
1674
+ { line, element: value.applies_to },
1675
+ `${UNKNOWN_ELEMENT_CODE}:table:${value.applies_to}`,
1676
+ );
1677
+ }
1678
+ }
1679
+ }
1680
+
1285
1681
  /** Count the subnetwork members that name no node or edge. */
1286
1682
  private checkMembers(): void {
1287
1683
  for (const id of this.plan.nodes ?? []) {
@@ -1328,7 +1724,7 @@ class CxReader {
1328
1724
  this.report.warning(
1329
1725
  "unsupported",
1330
1726
  CX_ISSUE.ROOT_ONLY,
1331
- `${nodes} node(s) and ${edges} edge(s) of the root network belong to no subnetwork (cySubNetworks); they are not read`,
1727
+ `${nodes} node${plural(nodes)} and ${edges} edge${plural(edges)} of the root network belong to no subnetwork (cySubNetworks); they are not read`,
1332
1728
  { element: "cySubNetworks" },
1333
1729
  );
1334
1730
  }
@@ -1359,7 +1755,10 @@ class CxReader {
1359
1755
  const declared = this.tableColumns(domain === "node" ? "node_table" : "edge_table");
1360
1756
  const names = new Set<string>([...declared.keys(), ...this.doc.attributes[domain].keys()]);
1361
1757
  for (const name of names) {
1362
- if (name === "" || (domain === "edge" && name === this.options.weightFrom)) {
1758
+ if (
1759
+ (domain === "edge" && name === this.options.weightFrom) ||
1760
+ (domain === "node" && name === ORIGINAL_ID_ATTRIBUTE && this.options.restoreMangledIds)
1761
+ ) {
1363
1762
  continue;
1364
1763
  }
1365
1764
  const types: CxType[] = [];
@@ -1455,7 +1854,15 @@ class CxReader {
1455
1854
  continue;
1456
1855
  }
1457
1856
  const type = cxType(value.d);
1458
- if (type !== null) {
1857
+ if (type === null) {
1858
+ this.report.warnOnce(
1859
+ "unsupported",
1860
+ UNKNOWN_ATTR_TYPE_CODE,
1861
+ `cyTableColumn declares "${value.n}" with the data type ${shown(value.d)}, which CX does not define; its values are typed by their own d`,
1862
+ { element: value.n },
1863
+ `${UNKNOWN_ATTR_TYPE_CODE}:table:${table}:${value.n}`,
1864
+ );
1865
+ } else {
1459
1866
  out.set(value.n, type);
1460
1867
  }
1461
1868
  }
@@ -1515,14 +1922,6 @@ class CxReader {
1515
1922
  if (scope === 0) {
1516
1923
  continue;
1517
1924
  }
1518
- const target = refId(value.po);
1519
- const row = target === null ? undefined : rows.get(this.key(target));
1520
- if (row === undefined) {
1521
- if (target === null || !root.has(target)) {
1522
- this.dangle(`${domain} attribute target`);
1523
- }
1524
- continue;
1525
- }
1526
1925
  if (value.v === undefined) {
1527
1926
  this.report.error(
1528
1927
  "missing-value",
@@ -1535,26 +1934,37 @@ class CxReader {
1535
1934
  );
1536
1935
  continue;
1537
1936
  }
1538
- if (value.v === null) {
1539
- continue;
1540
- }
1541
- const previous = chosen.get(row);
1542
- if (previous !== undefined && previous.scope > scope) {
1543
- continue;
1544
- }
1545
- if (
1546
- previous?.scope === scope &&
1547
- JSON.stringify(previous.value) !== JSON.stringify(plainJson(value.v))
1548
- ) {
1549
- this.report.warnOnce(
1550
- "validation-error",
1551
- DUPLICATE_ATTRIBUTE_CODE,
1552
- `${domain} ${shown(value.po)} has the attribute "${column.name}" twice; the later value wins`,
1553
- { line, element: column.name },
1554
- `${DUPLICATE_ATTRIBUTE_CODE}:${domain}:${column.name}`,
1555
- );
1937
+ // po names one element, or (cxio's shared values) a list of them
1938
+ for (const raw of Array.isArray(value.po) ? (value.po as unknown[]) : [value.po]) {
1939
+ const target = refId(raw);
1940
+ const row = target === null ? undefined : rows.get(target);
1941
+ if (row === undefined) {
1942
+ if (target === null || !root.has(target)) {
1943
+ this.dangle(`${domain} attribute target`);
1944
+ }
1945
+ continue;
1946
+ }
1947
+ if (value.v === null) {
1948
+ continue;
1949
+ }
1950
+ const previous = chosen.get(row);
1951
+ if (previous !== undefined && previous.scope > scope) {
1952
+ continue;
1953
+ }
1954
+ if (
1955
+ previous?.scope === scope &&
1956
+ JSON.stringify(previous.value) !== JSON.stringify(plainJson(value.v))
1957
+ ) {
1958
+ this.report.warnOnce(
1959
+ "validation-error",
1960
+ DUPLICATE_ATTRIBUTE_CODE,
1961
+ `${domain} ${shown(raw)} has the attribute "${column.name}" twice; the later value wins`,
1962
+ { line, element: column.name },
1963
+ `${DUPLICATE_ATTRIBUTE_CODE}:${domain}:${column.name}`,
1964
+ );
1965
+ }
1966
+ chosen.set(row, { scope, value: value.v, d: value.d, line });
1556
1967
  }
1557
- chosen.set(row, { scope, value: value.v, d: value.d, line });
1558
1968
  }
1559
1969
  for (const [row, { value, d, line }] of chosen) {
1560
1970
  this.writeCell(domain, column, row, value, d, line);
@@ -1608,6 +2018,15 @@ class CxReader {
1608
2018
  }
1609
2019
  let { handle } = column;
1610
2020
  if (this.isStructural(domain, column.name)) {
2021
+ if (Array.isArray(raw)) {
2022
+ this.report.error(
2023
+ "validation-error",
2024
+ BAD_VALUE_CODE,
2025
+ `${element}: ${shown(raw)} is a list; "${column.name}" holds one string, the cell is unset`,
2026
+ { line, element: column.name },
2027
+ );
2028
+ return;
2029
+ }
1611
2030
  handle = this.coreColumn(domain, column.name);
1612
2031
  const prior = this.coreValue(domain, row, column.name);
1613
2032
  if (prior !== undefined && prior !== value) {
@@ -1684,7 +2103,8 @@ class CxReader {
1684
2103
 
1685
2104
  /** Read the nodes of this graph. */
1686
2105
  private readNodes(): void {
1687
- const { report, sink } = this;
2106
+ const { report } = this;
2107
+ const originals = this.originalIds();
1688
2108
  for (const held of aspect(this.doc, "nodes")) {
1689
2109
  this.checkAbort();
1690
2110
  const { value, line } = held;
@@ -1706,40 +2126,66 @@ class CxReader {
1706
2126
  report.counts.skippedNodes++;
1707
2127
  continue;
1708
2128
  }
2129
+ this.unknownKeys(value, NODE_KEYS, "node", line);
1709
2130
  const element = `node ${shown(value["@id"])}`;
1710
- const id = this.idOf(value["@id"], (held.inexact & 1) !== 0, element);
2131
+ const id = this.idOf(value["@id"], (held.inexact & 1) !== 0, element, line);
1711
2132
  if (id === null) {
1712
2133
  report.counts.skippedNodes++;
1713
2134
  continue;
1714
2135
  }
1715
- // membership lists hold CX ids as written, before the ids option coerces them
1716
- if (!this.inGraph(refId(value["@id"]) ?? id)) {
2136
+ if (!this.inGraph(id)) {
1717
2137
  continue;
1718
2138
  }
1719
2139
  let row = this.nodeRows.get(id);
1720
- if (row !== undefined) {
1721
- report.warning(
1722
- "merged",
1723
- DUPLICATE_NODE_CODE,
1724
- `${element} is declared more than once; its fields are merged (the later values win)`,
1725
- { line, element },
1726
- );
1727
- } else {
2140
+ let added = false;
2141
+ if (row === undefined) {
2142
+ const original = originals.get(refId(value["@id"]) ?? id);
2143
+ if (original !== undefined) {
2144
+ this.idType = "mixed";
2145
+ this.sinkIds.set(id, original);
2146
+ }
1728
2147
  try {
1729
- row = sink.addNode(id);
2148
+ ({ row, added } = this.addNode(id));
1730
2149
  } catch (err) {
1731
2150
  report.recordError(err, { line, element });
1732
2151
  report.counts.skippedNodes++;
1733
2152
  continue;
1734
2153
  }
1735
- this.nodeRows.set(id, row);
2154
+ }
2155
+ if (added) {
1736
2156
  report.counts.nodes++;
2157
+ } else {
2158
+ report.warning(
2159
+ "merged",
2160
+ DUPLICATE_NODE_CODE,
2161
+ `${element} is declared more than once; its fields are merged (the later values win)`,
2162
+ { line, element },
2163
+ );
1737
2164
  }
1738
2165
  this.writeCore("node", row, "name", value.n, element, line);
1739
2166
  this.writeCore("node", row, "represents", value.r, element, line);
1740
2167
  }
1741
2168
  }
1742
2169
 
2170
+ /**
2171
+ * The original ids the exporter's `sanitizeIds: "mangle"` kept in the `graphty:originalId`
2172
+ * attribute, when they are to be restored.
2173
+ * @returns CX node id -> original id
2174
+ */
2175
+ private originalIds(): Map<NodeId, string> {
2176
+ const out = new Map<NodeId, string>();
2177
+ if (!this.options.restoreMangledIds) {
2178
+ return out;
2179
+ }
2180
+ for (const { value } of this.doc.attributes.node.get(ORIGINAL_ID_ATTRIBUTE) ?? []) {
2181
+ const po = isRecord(value) && this.scopePeek(value.s) !== 0 ? refId(value.po) : null;
2182
+ if (po !== null && isRecord(value) && typeof value.v === "string") {
2183
+ out.set(po, value.v);
2184
+ }
2185
+ }
2186
+ return out;
2187
+ }
2188
+
1743
2189
  /**
1744
2190
  * Write a core string field (n, r, i).
1745
2191
  * @param domain - node or edge
@@ -1799,30 +2245,40 @@ class CxReader {
1799
2245
  }
1800
2246
  const parents = new Map<number, number[]>();
1801
2247
  const collapsed: [number, boolean][] = [];
1802
- for (const { value, line } of groups) {
1803
- if (!isRecord(value)) {
2248
+ for (const held of groups) {
2249
+ const group = structuralId(held, "cyGroups", "@id", this.report);
2250
+ if (group === null) {
1804
2251
  continue;
1805
2252
  }
1806
- const id = refId(value["@id"]);
1807
- if (id === null) {
1808
- this.report.error("missing-value", MISSING_ID_CODE, "a cyGroups element has no @id", {
1809
- line,
1810
- element: "cyGroups",
1811
- });
1812
- continue;
2253
+ const { value, id } = group;
2254
+ const { line } = held;
2255
+ if (value.nodes !== undefined && value.nodes !== null && !Array.isArray(value.nodes)) {
2256
+ this.report.error(
2257
+ "validation-error",
2258
+ BAD_VALUE_CODE,
2259
+ `group ${String(id)}: nodes is ${shown(value.nodes)}, not a list of node ids; no member is read`,
2260
+ { line, element: String(id) },
2261
+ );
2262
+ }
2263
+ if (value.collapsed !== undefined && value.collapsed !== null && typeof value.collapsed !== "boolean") {
2264
+ this.report.error(
2265
+ "validation-error",
2266
+ BAD_VALUE_CODE,
2267
+ `group ${String(id)}: collapsed is ${shown(value.collapsed)}, not a boolean; unset`,
2268
+ { line, element: String(id) },
2269
+ );
1813
2270
  }
1814
- let groupRow = this.nodeRows.get(this.key(id));
2271
+ let groupRow = this.nodeRows.get(id);
1815
2272
  if (groupRow === undefined) {
1816
2273
  if (this.rootNodes.has(id) || !this.hasMemberIn(value.nodes)) {
1817
2274
  continue;
1818
2275
  }
1819
2276
  try {
1820
- groupRow = this.sink.addNode(this.key(id));
2277
+ groupRow = this.addNode(id).row;
1821
2278
  } catch (err) {
1822
2279
  this.report.recordError(err, { line, element: String(id) });
1823
2280
  continue;
1824
2281
  }
1825
- this.nodeRows.set(this.key(id), groupRow);
1826
2282
  this.report.counts.nodes++;
1827
2283
  this.report.warning(
1828
2284
  "coercion",
@@ -1839,7 +2295,7 @@ class CxReader {
1839
2295
  }
1840
2296
  for (const raw of Array.isArray(value.nodes) ? (value.nodes as unknown[]) : []) {
1841
2297
  const member = refId(raw);
1842
- const row = member === null ? undefined : this.nodeRows.get(this.key(member));
2298
+ const row = member === null ? undefined : this.nodeRows.get(member);
1843
2299
  if (row === undefined) {
1844
2300
  if (member === null || !this.rootNodes.has(member)) {
1845
2301
  this.report.error(
@@ -1896,7 +2352,7 @@ class CxReader {
1896
2352
  Array.isArray(raw) &&
1897
2353
  raw.some((item) => {
1898
2354
  const id = refId(item);
1899
- return id !== null && this.nodeRows.has(this.key(id));
2355
+ return id !== null && this.nodeRows.has(id);
1900
2356
  })
1901
2357
  );
1902
2358
  }
@@ -1963,6 +2419,12 @@ class CxReader {
1963
2419
  for (const { value, line } of entries) {
1964
2420
  this.checkAbort();
1965
2421
  if (!isRecord(value)) {
2422
+ this.report.error(
2423
+ "parse-error",
2424
+ BAD_ASPECT_BLOCK_CODE,
2425
+ `a cartesianLayout element is ${shown(value)}, not an object; skipped`,
2426
+ { line, element: "cartesianLayout" },
2427
+ );
1966
2428
  continue;
1967
2429
  }
1968
2430
  const view = value.view === undefined || value.view === null ? null : refId(value.view);
@@ -1983,14 +2445,31 @@ class CxReader {
1983
2445
  );
1984
2446
  continue;
1985
2447
  }
2448
+ if (!fitsF32(value.x) || !fitsF32(value.y)) {
2449
+ this.report.error(
2450
+ "validation-error",
2451
+ BAD_VALUE_CODE,
2452
+ `a cartesianLayout element for node ${shown(value.node)} has the coordinates ${String(value.x)}, ${String(value.y)}, beyond what the f32 position column holds; skipped`,
2453
+ { line, element: "cartesianLayout" },
2454
+ );
2455
+ continue;
2456
+ }
1986
2457
  const node = refId(value.node);
1987
- const row = node === null ? undefined : this.nodeRows.get(this.key(node));
2458
+ const row = node === null ? undefined : this.nodeRows.get(node);
1988
2459
  if (row === undefined) {
1989
2460
  if (node === null || !this.rootNodes.has(node)) {
1990
2461
  this.dangle("layout entry");
1991
2462
  }
1992
2463
  continue;
1993
2464
  }
2465
+ if (value.z !== undefined && value.z !== null && typeof value.z !== "number") {
2466
+ this.report.error(
2467
+ "validation-error",
2468
+ BAD_VALUE_CODE,
2469
+ `a cartesianLayout element for node ${shown(value.node)} has the z ${shown(value.z)}, not a number; z is unset`,
2470
+ { line, element: "cartesianLayout" },
2471
+ );
2472
+ }
1994
2473
  const key = `${slot}:${row}`;
1995
2474
  if (seen.has(key)) {
1996
2475
  this.report.warnOnce(
@@ -2069,6 +2548,7 @@ class CxReader {
2069
2548
  private readEdges(): void {
2070
2549
  const { report, sink } = this;
2071
2550
  const members = this.plan.edges;
2551
+ this.weights = this.edgeWeights();
2072
2552
  for (const held of aspect(this.doc, "edges")) {
2073
2553
  this.checkAbort();
2074
2554
  const { value, line } = held;
@@ -2090,13 +2570,14 @@ class CxReader {
2090
2570
  report.counts.skippedEdges++;
2091
2571
  continue;
2092
2572
  }
2573
+ this.unknownKeys(value, EDGE_KEYS, "edge", line);
2093
2574
  const element = `edge ${shown(value["@id"])}`;
2094
- const id = this.idOf(value["@id"], (held.inexact & 1) !== 0, element, true);
2575
+ const id = this.idOf(value["@id"], (held.inexact & 1) !== 0, element, line, true);
2095
2576
  if (id === null) {
2096
2577
  report.counts.skippedEdges++;
2097
2578
  continue;
2098
2579
  }
2099
- if (members !== null && !members.has(refId(value["@id"]) ?? id)) {
2580
+ if (members !== null && !members.has(id)) {
2100
2581
  continue;
2101
2582
  }
2102
2583
  if (value.s === undefined || value.s === null || value.t === undefined || value.t === null) {
@@ -2112,8 +2593,8 @@ class CxReader {
2112
2593
  report.counts.skippedEdges++;
2113
2594
  continue;
2114
2595
  }
2115
- const s = this.idOf(value.s, (held.inexact & 2) !== 0, element);
2116
- const t = s === null ? null : this.idOf(value.t, (held.inexact & 4) !== 0, element);
2596
+ const s = this.idOf(value.s, (held.inexact & 2) !== 0, element, line);
2597
+ const t = s === null ? null : this.idOf(value.t, (held.inexact & 4) !== 0, element, line);
2117
2598
  if (s === null || t === null) {
2118
2599
  report.counts.skippedEdges++;
2119
2600
  continue;
@@ -2143,7 +2624,7 @@ class CxReader {
2143
2624
  const before = sink.edgeCount;
2144
2625
  let edge: number;
2145
2626
  try {
2146
- edge = this.direction.addEdge(s, t, "directed", weight, { line, element });
2627
+ edge = this.direction.addEdge(this.sinkId(s), this.sinkId(t), "directed", weight, { line, element });
2147
2628
  } catch (err) {
2148
2629
  report.recordError(err, { line, element });
2149
2630
  report.counts.skippedEdges++;
@@ -2186,39 +2667,89 @@ class CxReader {
2186
2667
  return false;
2187
2668
  }
2188
2669
  try {
2189
- this.nodeRows.set(id, this.sink.addNode(id));
2670
+ if (this.addNode(id).added) {
2671
+ this.report.counts.nodes++;
2672
+ }
2190
2673
  } catch (err) {
2191
2674
  this.report.recordError(err, { line, element });
2192
2675
  return false;
2193
2676
  }
2194
- this.report.counts.nodes++;
2195
2677
  return true;
2196
2678
  }
2197
2679
 
2198
- /** The weight attribute values of the edges, by edge id (built once). */
2199
- private weights: Map<NodeId, unknown> | null = null;
2680
+ /** The weight attribute values of the edges, by edge id. */
2681
+ private weights = new Map<NodeId, unknown>();
2200
2682
 
2201
2683
  /**
2202
- * The weight of an edge: its weightFrom attribute, parsed as a number.
2203
- * @param id - the edge id
2204
- * @returns the weight, or undefined; E_INVALID_WEIGHT for a non-number
2684
+ * The weightFrom attribute values by edge id: this subnetwork's own value beats the unscoped
2685
+ * one, and of two in one scope the later wins (W_DUPLICATE_ATTRIBUTE when they differ), as
2686
+ * writeAttributes() rules every other attribute.
2687
+ * @returns the values (parsed JSON) by edge id
2205
2688
  */
2206
- private weightOf(id: NodeId): number | undefined {
2689
+ private edgeWeights(): Map<NodeId, unknown> {
2207
2690
  const { weightFrom } = this.options;
2691
+ const chosen = new Map<NodeId, { scope: number; value: unknown }>();
2208
2692
  if (weightFrom === null) {
2209
- return undefined;
2693
+ return new Map();
2210
2694
  }
2211
- if (this.weights === null) {
2212
- this.weights = new Map();
2213
- for (const { value } of this.doc.attributes.edge.get(weightFrom) ?? []) {
2214
- if (isRecord(value) && this.scopePeek(value.s) !== 0) {
2215
- const po = refId(value.po);
2216
- if (po !== null && value.v !== undefined && value.v !== null) {
2217
- this.weights.set(this.key(po), plainJson(value.v));
2218
- }
2219
- }
2695
+ for (const { value, line } of this.doc.attributes.edge.get(weightFrom) ?? []) {
2696
+ const scope = isRecord(value) ? this.scopePeek(value.s) : 0;
2697
+ if (!isRecord(value) || scope === 0 || value.v === undefined || value.v === null) {
2698
+ continue;
2699
+ }
2700
+ const v = plainJson(value.v);
2701
+ for (const raw of Array.isArray(value.po) ? (value.po as unknown[]) : [value.po]) {
2702
+ this.chooseWeight(chosen, raw, { scope, value: v }, line, weightFrom);
2220
2703
  }
2221
2704
  }
2705
+ return new Map([...chosen].map(([id, { value }]) => [id, value]));
2706
+ }
2707
+
2708
+ /**
2709
+ * Keep one weight attribute element's value for one edge, unless a more specific scope already
2710
+ * gave it one; a second value in the same scope wins, with a warning.
2711
+ * @param chosen - the value chosen so far per edge
2712
+ * @param raw - the edge reference as written
2713
+ * @param weight - the value and the scope it applies in
2714
+ * @param weight.scope - the scope: higher is more specific
2715
+ * @param weight.value - the value
2716
+ * @param line - the element's line
2717
+ * @param weightFrom - the attribute name
2718
+ */
2719
+ private chooseWeight(
2720
+ chosen: Map<NodeId, { scope: number; value: unknown }>,
2721
+ raw: unknown,
2722
+ weight: { scope: number; value: unknown },
2723
+ line: number,
2724
+ weightFrom: string,
2725
+ ): void {
2726
+ const po = refId(raw);
2727
+ const previous = po === null ? undefined : chosen.get(po);
2728
+ if (po === null || (previous !== undefined && previous.scope > weight.scope)) {
2729
+ return;
2730
+ }
2731
+ if (previous?.scope === weight.scope && JSON.stringify(previous.value) !== JSON.stringify(weight.value)) {
2732
+ this.report.warnOnce(
2733
+ "validation-error",
2734
+ DUPLICATE_ATTRIBUTE_CODE,
2735
+ `edge ${shown(raw)} has the attribute "${weightFrom}" twice; the later value wins`,
2736
+ { line, element: weightFrom },
2737
+ `${DUPLICATE_ATTRIBUTE_CODE}:edge:${weightFrom}`,
2738
+ );
2739
+ }
2740
+ chosen.set(po, weight);
2741
+ }
2742
+
2743
+ /**
2744
+ * The weight of an edge: its weightFrom attribute, parsed as a number (the declared d is not
2745
+ * consulted: a value that is not a number is E_INVALID_WEIGHT whatever its type says).
2746
+ * @param id - the edge id
2747
+ * @returns the weight, or undefined; E_INVALID_WEIGHT for a non-number
2748
+ */
2749
+ private weightOf(id: NodeId): number | undefined {
2750
+ if (this.options.weightFrom === null) {
2751
+ return undefined;
2752
+ }
2222
2753
  const raw = this.weights.get(id);
2223
2754
  if (typeof raw === "string" && isNullText(raw)) {
2224
2755
  return undefined;
@@ -2233,17 +2764,42 @@ class CxReader {
2233
2764
  for (const [name, elements] of this.doc.attributes.network) {
2234
2765
  let chosen: Record<string, unknown> | null = null;
2235
2766
  let chosenScope = 0;
2236
- for (const { value } of elements) {
2767
+ for (const { value, line } of elements) {
2237
2768
  if (!isRecord(value)) {
2238
2769
  continue;
2239
2770
  }
2240
2771
  const scope = this.scopeOf(value.s);
2241
- if (scope !== 0 && scope >= chosenScope) {
2772
+ if (scope === 0) {
2773
+ continue;
2774
+ }
2775
+ if (value.v === undefined) {
2776
+ this.report.error(
2777
+ "missing-value",
2778
+ BAD_VALUE_CODE,
2779
+ `a networkAttributes element for "${name}" has no v; skipped`,
2780
+ { line, element: name },
2781
+ );
2782
+ continue;
2783
+ }
2784
+ if (
2785
+ chosen !== null &&
2786
+ scope === chosenScope &&
2787
+ JSON.stringify(plainJson(chosen.v)) !== JSON.stringify(plainJson(value.v))
2788
+ ) {
2789
+ this.report.warnOnce(
2790
+ "validation-error",
2791
+ DUPLICATE_ATTRIBUTE_CODE,
2792
+ `the network attribute "${name}" is given twice; the later value wins`,
2793
+ { line, element: name },
2794
+ `${DUPLICATE_ATTRIBUTE_CODE}:network:${name}`,
2795
+ );
2796
+ }
2797
+ if (scope >= chosenScope) {
2242
2798
  chosen = value;
2243
2799
  chosenScope = scope;
2244
2800
  }
2245
2801
  }
2246
- if (chosen === null || chosen.v === undefined || chosen.v === null || name === "") {
2802
+ if (chosen?.v === undefined || chosen.v === null) {
2247
2803
  continue;
2248
2804
  }
2249
2805
  const type = cxType(chosen.d);
@@ -2321,7 +2877,7 @@ class CxReader {
2321
2877
  const domain = value.properties_of === "nodes" ? "node" : "edge";
2322
2878
  const target = refId(value.applies_to);
2323
2879
  const rows = domain === "node" ? this.nodeRows : this.edgeRows;
2324
- const row = target === null ? undefined : rows.get(this.key(target));
2880
+ const row = target === null ? undefined : rows.get(target);
2325
2881
  if (row === undefined) {
2326
2882
  if (target === null || !(domain === "node" ? this.rootNodes : this.rootEdges).has(target)) {
2327
2883
  this.dangle(`${domain} visual property entry`);
@@ -2354,7 +2910,7 @@ class CxReader {
2354
2910
  name: property,
2355
2911
  dtype: "string",
2356
2912
  nullable: true,
2357
- origin: { format: CX_FORMAT, id: null, namespace: CX_BYPASS_NAMESPACE },
2913
+ origin: { format: CX_FORMAT, id: property, namespace: CX_BYPASS_NAMESPACE },
2358
2914
  },
2359
2915
  this.report,
2360
2916
  );
@@ -2379,15 +2935,15 @@ class CxReader {
2379
2935
  }
2380
2936
  if (defaults + mappings + dependencies + tableStyles > 0) {
2381
2937
  const parts = [
2382
- `cyVisualProperties: ${defaults} default(s), ${mappings} mapping(s), ${dependencies} dependenc(ies)`,
2938
+ `cyVisualProperties: ${defaults} default${plural(defaults)}, ${mappings} mapping${plural(mappings)}, ${dependencies} dependenc(ies)`,
2383
2939
  ];
2384
2940
  if (tableStyles > 0) {
2385
- parts.push(`${tableStyles} table style element(s)`);
2941
+ parts.push(`${tableStyles} table style element${plural(tableStyles)}`);
2386
2942
  }
2387
2943
  this.report.warning(
2388
2944
  "unsupported",
2389
2945
  STYLES_NOT_IMPORTED_CODE,
2390
- `the file's style rules are not applied (${parts.join("; ")}); they are kept in meta.extra.cx (style import is issue #706)`,
2946
+ `the file's style rules are not applied (${parts.join("; ")}); they are kept in meta.extra.cx`,
2391
2947
  { element: "cyVisualProperties" },
2392
2948
  );
2393
2949
  }
@@ -2416,7 +2972,7 @@ class CxReader {
2416
2972
  continue;
2417
2973
  }
2418
2974
  const po = refId(value.po);
2419
- const row = po === null ? undefined : this.nodeRows.get(this.key(po));
2975
+ const row = po === null ? undefined : this.nodeRows.get(po);
2420
2976
  if (row === undefined) {
2421
2977
  if (po === null || !this.rootNodes.has(po)) {
2422
2978
  this.dangle("functionTerms entry");
@@ -2434,8 +2990,8 @@ class CxReader {
2434
2990
  }
2435
2991
  const node = refId(value.node);
2436
2992
  const edge = refId(value.edge);
2437
- const row = node === null ? undefined : this.nodeRows.get(this.key(node));
2438
- const edgeRow = edge === null ? undefined : this.edgeRows.get(this.key(edge));
2993
+ const row = node === null ? undefined : this.nodeRows.get(node);
2994
+ const edgeRow = edge === null ? undefined : this.edgeRows.get(edge);
2439
2995
  if (row === undefined || edgeRow === undefined) {
2440
2996
  if (node === null || !this.rootNodes.has(node) || edge === null || !this.rootEdges.has(edge)) {
2441
2997
  this.dangle("reifiedEdges entry");
@@ -2474,16 +3030,35 @@ class CxReader {
2474
3030
  return;
2475
3031
  }
2476
3032
  const fields = [...new Set(records.flatMap((r) => Object.keys(r)).filter((k) => k !== "@id"))];
3033
+ const names = new Set(["id"]);
2477
3034
  const decls: ColumnDecl[] = [
2478
3035
  { name: "id", dtype: "f64", nullable: true },
2479
- ...fields.map((field): ColumnDecl => ({ name: field, dtype: "json", nullable: true })),
3036
+ ...fields.map((field): ColumnDecl => {
3037
+ const column = uniqueColumnName(field, null, (n) => names.has(n));
3038
+ names.add(column);
3039
+ if (column !== field) {
3040
+ this.report.warning(
3041
+ "coercion",
3042
+ COLUMN_RENAMED_CODE,
3043
+ `${name} column "${field}" renamed to "${column}": the table's own id column holds that name`,
3044
+ { element: field },
3045
+ );
3046
+ }
3047
+ return { name: column, dtype: "json", nullable: true };
3048
+ }),
2480
3049
  ];
2481
3050
  const table = this.sink.addExtensionTable(name, decls);
3051
+ const precision = (digits: string): void => {
3052
+ this.precision(name, digits);
3053
+ };
2482
3054
  for (const record of records) {
2483
3055
  const id = refId(record["@id"]);
3056
+ if (typeof id === "string") {
3057
+ precision(id);
3058
+ }
2484
3059
  this.sink.addExtensionRow(table, [
2485
3060
  id === null ? null : Number(id),
2486
- ...fields.map((field) => (record[field] === undefined ? null : plainJson(record[field]))),
3061
+ ...fields.map((field) => (record[field] === undefined ? null : plainJson(record[field], precision))),
2487
3062
  ]);
2488
3063
  }
2489
3064
  }
@@ -2509,7 +3084,7 @@ class CxReader {
2509
3084
  .map(Number);
2510
3085
  for (const raw of targets) {
2511
3086
  const target = refId(raw);
2512
- const row = target === null ? undefined : rows.get(this.key(target));
3087
+ const row = target === null ? undefined : rows.get(target);
2513
3088
  if (row === undefined) {
2514
3089
  if (target === null || !root.has(target)) {
2515
3090
  this.dangle(`${aspectName} entry`);
@@ -2576,12 +3151,24 @@ class CxReader {
2576
3151
  };
2577
3152
  const extra: Record<string, unknown> = Object.fromEntries(this.doc.kept);
2578
3153
  const groups = aspect(this.doc, "cyGroups").map((h) => plainJson(h.value));
3154
+ const own: Record<string, unknown> = {};
2579
3155
  if (groups.length > 0) {
2580
- extra.groups = groups;
3156
+ own.groups = groups;
2581
3157
  }
2582
3158
  if (this.plan.subnetwork !== null) {
2583
- extra.subnetwork = this.plan.subnetwork;
3159
+ own.subnetwork = this.plan.subnetwork;
3160
+ }
3161
+ for (const key of OWN_EXTRA_KEYS) {
3162
+ if (key in own && this.doc.kept.has(key)) {
3163
+ this.report.warning(
3164
+ "unsupported",
3165
+ UNKNOWN_ELEMENT_CODE,
3166
+ `the aspect "${key}" is not kept: meta.extra.cx.${key} holds the importer's own ${key === "groups" ? "cyGroups" : "subnetwork"} data`,
3167
+ { element: key },
3168
+ );
3169
+ }
2584
3170
  }
3171
+ Object.assign(extra, own);
2585
3172
  const { weightFrom } = this.options;
2586
3173
  const name = this.plan.name ?? describe("name");
2587
3174
  const description = describe("description");
@@ -2621,7 +3208,7 @@ async function run(
2621
3208
  const first = new ImportReportBuilder(CX_FORMAT, resolved.errorLimit);
2622
3209
  reportUnusedOptions(options, first, USED_OPTIONS);
2623
3210
  const doc = await readDocument(input, first, resolved);
2624
- const plans = graphPlans(doc);
3211
+ const plans = graphPlans(doc, first);
2625
3212
  const chosen = pick(plans, first);
2626
3213
  const reports: ImportReport[] = [];
2627
3214
  chosen.forEach((index, k) => {
@@ -2636,10 +3223,12 @@ async function run(
2636
3223
  }
2637
3224
 
2638
3225
  /**
2639
- * The CX version 1 importer plugin (design section 1.2).
3226
+ * The CX version 1 importer plugin.
3227
+ * @category Built-in formats
2640
3228
  */
2641
3229
  export const cxImporter: GraphImporter<CxImportOptions> = Object.freeze({
2642
3230
  format: CX_FORMAT,
3231
+ options: Object.freeze(["zAs"]),
2643
3232
  extensions: Object.freeze([".cx"]),
2644
3233
  mimeTypes: Object.freeze(["application/json"]),
2645
3234
 
@@ -2651,8 +3240,7 @@ export const cxImporter: GraphImporter<CxImportOptions> = Object.freeze({
2651
3240
  * @returns the confidence
2652
3241
  */
2653
3242
  sniff(head: Uint8Array): number {
2654
- const text = new TextDecoder("utf-8").decode(head.subarray(0, 1024)).replace(/^\uFEFF/, "");
2655
- const match = /^\s*\[\s*\{\s*"((?:[^"\\]|\\.)*)"\s*:/.exec(text);
3243
+ const match = /^\s*\[\s*\{\s*"((?:[^"\\]|\\.)*)"\s*:/.exec(headText(head));
2656
3244
  if (match === null) {
2657
3245
  return 0;
2658
3246
  }
@@ -2685,11 +3273,11 @@ export const cxImporter: GraphImporter<CxImportOptions> = Object.freeze({
2685
3273
  options,
2686
3274
  first,
2687
3275
  );
2688
- if (plans.length > 1) {
3276
+ if (plans.length > 1 && !graphChosen(options)) {
2689
3277
  first.warning(
2690
3278
  "unsupported",
2691
3279
  MULTIPLE_GRAPHS_CODE,
2692
- `the collection holds ${plans.length} subnetworks; graph ${index} is read and ${plans.length - 1} skipped (importAll() reads every one)`,
3280
+ `the collection holds ${plans.length} subnetworks; graph ${index} is read and ${plans.length - 1} skipped (importAllGraphs() reads every one)`,
2693
3281
  );
2694
3282
  }
2695
3283
  return [index];
@@ -2729,7 +3317,7 @@ export const cxImporter: GraphImporter<CxImportOptions> = Object.freeze({
2729
3317
  const doc = await readDocument(input, report, resolved);
2730
3318
  const nodes = aspect(doc, "nodes").length;
2731
3319
  const edges = aspect(doc, "edges").length;
2732
- return graphPlans(doc).map((plan) =>
3320
+ return graphPlans(doc, report).map((plan) =>
2733
3321
  Object.freeze({
2734
3322
  index: plan.index,
2735
3323
  name: plan.name,