@graphty/graph-io 0.3.25 → 0.3.27

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-DrXtu_96.js +56 -0
  4. package/dist/chunks/choose-DrXtu_96.js.map +1 -0
  5. package/dist/chunks/constants-ikG85dT8.js +231 -0
  6. package/dist/chunks/constants-ikG85dT8.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D1HPTUrP.js} +390 -99
  8. package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
  9. package/dist/chunks/export-CHd4tQE2.js +4333 -0
  10. package/dist/chunks/export-CHd4tQE2.js.map +1 -0
  11. package/dist/chunks/{importer-eUREhBGj.js → exporter-D23CKNqF.js} +4969 -3201
  12. package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
  13. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-DfWTgrti.js} +23 -21
  14. package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
  15. package/dist/chunks/exporter-DhtW6FxK.js +3465 -0
  16. package/dist/chunks/exporter-DhtW6FxK.js.map +1 -0
  17. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-IHv4nkCm.js} +38 -229
  18. package/dist/chunks/exporter-IHv4nkCm.js.map +1 -0
  19. package/dist/chunks/{importer-BWY2FFc5.js → importer-B1wmPQjr.js} +255 -77
  20. package/dist/chunks/importer-B1wmPQjr.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-B2W_58pz.js} +399 -146
  22. package/dist/chunks/importer-B2W_58pz.js.map +1 -0
  23. package/dist/chunks/importer-BSALvkui.js +2811 -0
  24. package/dist/chunks/importer-BSALvkui.js.map +1 -0
  25. package/dist/chunks/{importer-vi3bSdtw.js → importer-BVRE--Un.js} +510 -77
  26. package/dist/chunks/importer-BVRE--Un.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-CIAzt-wo.js} +1689 -312
  28. package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
  29. package/dist/chunks/{importer-DIrbbnAf.js → importer-DpEW5zeG.js} +585 -226
  30. package/dist/chunks/importer-DpEW5zeG.js.map +1 -0
  31. package/dist/chunks/{importer-zGLo8Dg_.js → importer-z_FL4VMb.js} +488 -161
  32. package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
  33. package/dist/chunks/json-elements-BAmNBZok.js +1225 -0
  34. package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
  35. package/dist/chunks/ontology-export-Bab4eUA8.js +357 -0
  36. package/dist/chunks/ontology-export-Bab4eUA8.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CdlvL4RO.js} +298 -114
  38. package/dist/chunks/records-CdlvL4RO.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DzXjAiTl.js} +29 -11
  40. package/dist/chunks/text-DzXjAiTl.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 +952 -262
  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 +344 -87
  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 +565 -158
  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 +21 -3
  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 +39 -22
  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 +869 -189
  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 +3 -1
  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 +402 -85
  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 +146 -33
  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 +682 -287
  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 +227 -80
  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 +101 -27
  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 +110 -28
  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 +239 -86
  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 +67 -35
  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 +186 -54
  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 +21 -22
  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 +8 -3
  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 +40 -6
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +39 -22
  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 +1035 -199
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +7 -2
  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 +479 -90
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +202 -47
  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 +890 -302
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +62 -339
  498. package/src/formats/cx2/importer.ts +318 -87
  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 +142 -34
  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 +113 -28
  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 +321 -102
  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 +90 -42
  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 +216 -55
  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 +36 -28
  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 +12 -4
  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
@@ -9,7 +9,9 @@
9
9
  * groups, nested-network pointers); the subnetwork's CyCSV tables give its columns (LOCAL_ATTRS as
10
10
  * they are, the SHARED_ATTRS columns `cytables.xml` joins in, HIDDEN and app tables as hidden
11
11
  * columns under their namespace); its first view gives positions (y flipped) and the `graphics`
12
- * column, further views `position@<n>` columns. 2.x: one full XGMML file per network, with
12
+ * column, further views `position@<n>` columns. A table's `name` is the label (what Cytoscape
13
+ * shows), in a label column named `name`, and ids an exporter mangled are restored from the node
14
+ * table's `graphty:originalId` (restoreMangledIds). 2.x: one full XGMML file per network, with
13
15
  * selection and hidden state from `cysession.xml`. Everything is resolved by the XGMML emitter, so
14
16
  * both readers share every column rule. Styles are not read (issue #706): W_STYLES_NOT_IMPORTED.
15
17
  */
@@ -18,15 +20,17 @@ import { GraphFormatError, type GraphSink, INVALID_INDEX } from "@graphty/graph-
18
20
 
19
21
  import { declareResolved } from "../../common/attributes.js";
20
22
  import { IdCoercer } from "../../common/ids.js";
21
- import { decodeEntryName, readBytes, throwIfAborted } from "../../common/input.js";
23
+ import { decodeEntryName, throwIfAborted } from "../../common/input.js";
22
24
  import {
23
25
  chooseGraph,
26
+ graphChosen,
24
27
  type ImportFormatDefaults,
25
28
  reportSinkOptions,
26
29
  reportUnusedOptions,
27
30
  type ResolvedImportOptions,
28
31
  resolveImportOptions,
29
32
  } from "../../common/options.js";
33
+ import { agree, elementCount, plural } from "../../common/plural.js";
30
34
  import { ImportReportBuilder } from "../../common/report.js";
31
35
  import { startsLikeZip } from "../../common/zip.js";
32
36
  import {
@@ -38,6 +42,7 @@ import {
38
42
  type ImportInput,
39
43
  type ImportReport,
40
44
  } from "../../types.js";
45
+ import { ORIGINAL_ID_ATTRIBUTE } from "../graphml/constants.js";
41
46
  import { XGMML_ISSUE, XGMML_ORIGIN_NAMESPACE } from "../xgmml/constants.js";
42
47
  import {
43
48
  type AttRec,
@@ -52,6 +57,7 @@ import {
52
57
  dialectOf,
53
58
  type EmitExtras,
54
59
  membersOf,
60
+ parseCoordinate,
55
61
  XgmmlEmitter,
56
62
  type XgmmlSettings,
57
63
  } from "../xgmml/emit.js";
@@ -70,9 +76,11 @@ import {
70
76
  } from "./constants.js";
71
77
  import {
72
78
  elementsNamed,
79
+ entryLabel,
73
80
  listed,
74
81
  type NetworkEntry,
75
82
  openSession,
83
+ parseXmlTree,
76
84
  readXmlTree,
77
85
  type Session,
78
86
  type SessionEntry,
@@ -82,22 +90,33 @@ import {
82
90
  } from "./session.js";
83
91
  import { cellAtt, type CyTable, readCyTable, type VirtualColumn, virtualColumnsOf } from "./tables.js";
84
92
 
85
- /** The format-specific options of the session importer. */
86
- export interface CysImportOptions extends GraphChoiceOptions {
87
- /** Where Cytoscape's z (a stacking order) goes: the `z` column (default) or the position. */
93
+ /**
94
+ * The format-specific options of the session importer.
95
+ * @category Built-in formats
96
+ */
97
+ export interface CysImportOptions extends GraphChoiceOptions, CommonImportOptions {
98
+ /**
99
+ * Where Cytoscape's `z` value (a drawing order, not a depth) goes: "column" keeps it as a node
100
+ * attribute named `z`; "position" makes it the third coordinate of the position.
101
+ * @defaultValue "column"
102
+ */
88
103
  zAs?: "column" | "position" | undefined;
89
104
  /**
90
- * The most bytes one import may inflate, in total (default 2 GiB); an entry beyond it, or one
91
- * whose compression ratio is above 1000:1, is E_TOO_LARGE.
105
+ * The most bytes one import may unpack from the session archive, in total (2 GiB). A file that
106
+ * would unpack to more, or one compressed more than 1000 to 1, fails with E_TOO_LARGE.
107
+ * @defaultValue 2147483648
92
108
  */
93
109
  maxUncompressedBytes?: number | undefined;
94
110
  }
95
111
 
96
- /** The per-format defaults: SUIDs as written, edges directed unless they say otherwise, no weight. */
112
+ /**
113
+ * The per-format defaults: SUIDs as written, edges directed unless they say otherwise, the edge
114
+ * `weight` column as the weight (as for XGMML, the format of the session's networks).
115
+ */
97
116
  const FORMAT_DEFAULTS: ImportFormatDefaults = {
98
117
  ids: "keep",
99
118
  defaultDirected: true,
100
- weightFrom: null,
119
+ weightFrom: "weight",
101
120
  addMissingNodes: false,
102
121
  };
103
122
 
@@ -112,6 +131,7 @@ const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof Commo
112
131
  "weightFrom",
113
132
  "weightDtype",
114
133
  "long",
134
+ "restoreMangledIds",
115
135
  "errorLimit",
116
136
  "signal",
117
137
  "onProgress",
@@ -124,6 +144,8 @@ type Choice = Choice3 | Choice2;
124
144
  interface Choice3 {
125
145
  readonly era: "3";
126
146
  readonly network: NetworkEntry;
147
+ /** The subnetwork itself (two subnetworks can share an id). */
148
+ readonly graph: GraphRec;
127
149
  readonly graphId: string;
128
150
  readonly name: string | null;
129
151
  readonly nodes: number;
@@ -225,23 +247,72 @@ async function parseEntry(
225
247
  report: ImportReportBuilder,
226
248
  inner: ResolvedImportOptions,
227
249
  ): Promise<Parsed> {
228
- const bytes = await session.read(entry);
229
- const scratch = new ImportReportBuilder(FORMAT, Number.MAX_SAFE_INTEGER);
250
+ return (await parseOptionalEntry(session, entry, report, inner, false)) as Parsed;
251
+ }
252
+
253
+ /**
254
+ * Read an XGMML entry, its issues and counts relayed into the import's report (the entry name
255
+ * in each message), parsing within what is left of the import's error limit. A fatal issue fails
256
+ * the import, unless the entry is optional (a view): then it is recorded as an error and the
257
+ * entry is skipped (null).
258
+ * @param session - the session
259
+ * @param entry - the entry
260
+ * @param report - the import's report
261
+ * @param inner - the options entries are read with
262
+ * @param optional - whether the import can go on without the entry
263
+ * @returns the document and its dialect, or null for an optional entry that cannot be read
264
+ */
265
+ async function parseOptionalEntry(
266
+ session: Session,
267
+ entry: SessionEntry,
268
+ report: ImportReportBuilder,
269
+ inner: ResolvedImportOptions,
270
+ optional: boolean,
271
+ ): Promise<Parsed | null> {
272
+ const scratch = new ImportReportBuilder(FORMAT, Math.max(0, report.errorLimit - report.errorCount));
230
273
  try {
274
+ // a damaged entry (bad CRC, truncated deflate) fails the scratch report like a parse error
275
+ const bytes = await session.read(entry, scratch);
231
276
  const doc = await parseXgmml(bytes, scratch, inner, undefined);
232
277
  const dialect = dialectOf(doc, scratch);
233
278
  relay(scratch.issues, report, entry.name);
279
+ addCounts(scratch, report);
234
280
  return { doc, dialect };
235
281
  } catch (err) {
236
282
  if (!(err instanceof ImportError)) {
237
283
  throw err;
238
284
  }
285
+ addCounts(scratch, report);
239
286
  const { issues } = scratch;
287
+ if (scratch.truncated) {
288
+ // the entry reached the import's error limit: relaying its errors aborts the import
289
+ relay(issues, report, entry.name);
290
+ }
240
291
  const fatal = issues.at(-1);
241
292
  relay(issues.slice(0, -1), report, entry.name);
242
- return report.fail(fatal?.code ?? CYS_ISSUE.CORRUPT, `${entry.name}: ${fatal?.message ?? err.message}`, {
243
- line: fatal?.line ?? null,
244
- });
293
+ const message = `${entryLabel(entry.name)}: ${fatal?.message ?? err.message}`;
294
+ const where = { line: fatal?.line ?? null };
295
+ if (fatal?.code === CYS_ISSUE.TOO_LARGE) {
296
+ // the byte budget is a limit of the whole import, not damage to one entry
297
+ report.error("unsupported", fatal.code, message);
298
+ throw report.abort(message, { code: fatal.code });
299
+ }
300
+ if (optional) {
301
+ report.error("parse-error", fatal?.code ?? CYS_ISSUE.CORRUPT, `${message}; the entry is not read`, where);
302
+ return null;
303
+ }
304
+ return report.fail(fatal?.code ?? CYS_ISSUE.CORRUPT, message, where);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Add an entry's element counts (the nodes skipped while parsing it) to the import's.
310
+ * @param from - the entry's report
311
+ * @param to - the import's report
312
+ */
313
+ function addCounts(from: ImportReportBuilder, to: ImportReportBuilder): void {
314
+ for (const key of Object.keys(from.counts) as (keyof typeof from.counts)[]) {
315
+ to.counts[key] += from.counts[key];
245
316
  }
246
317
  }
247
318
 
@@ -255,9 +326,9 @@ function relay(issues: ImportReport["issues"], report: ImportReportBuilder, entr
255
326
  for (const issue of issues) {
256
327
  const where = { line: issue.line, element: issue.element };
257
328
  if (issue.severity === "error") {
258
- report.error(issue.category, issue.code, `${entry}: ${issue.message}`, where);
329
+ report.error(issue.category, issue.code, `${entryLabel(entry)}: ${issue.message}`, where);
259
330
  } else {
260
- report.warning(issue.category, issue.code, `${entry}: ${issue.message}`, where);
331
+ report.warning(issue.category, issue.code, `${entryLabel(entry)}: ${issue.message}`, where);
261
332
  }
262
333
  }
263
334
  }
@@ -282,13 +353,31 @@ async function networks3(
282
353
  const choices: Choice3[] = [];
283
354
  for (const network of layout.networks) {
284
355
  const parsed = await parseEntry(session, network, report, inner);
356
+ if (parsed.dialect.view) {
357
+ report.fail(
358
+ XGMML_ISSUE.VIEW_DOCUMENT,
359
+ `${network.name}: this is a session view file (cy:view), not a network file; it holds no topology`,
360
+ { line: parsed.doc.root.line },
361
+ );
362
+ }
285
363
  docs.set(network.name, parsed);
286
364
  for (const graph of registeredGraphs(parsed)) {
365
+ if (graph.id === null) {
366
+ const label = graph.label === null ? "" : ` ("${graph.label}")`;
367
+ report.error(
368
+ "missing-value",
369
+ XGMML_ISSUE.MISSING_ID,
370
+ `${network.name}: a registered subnetwork${label} has no id; its tables and views cannot be found and it is skipped`,
371
+ { line: graph.line },
372
+ );
373
+ continue;
374
+ }
287
375
  const members = membersOf(parsed.doc, graph);
288
376
  choices.push({
289
377
  era: "3",
290
378
  network,
291
- graphId: graph.id ?? "",
379
+ graph,
380
+ graphId: graph.id,
292
381
  name: graph.label ?? graph.id,
293
382
  nodes: new Set(members.nodes).size,
294
383
  edges: members.edges.length,
@@ -307,13 +396,27 @@ async function networks3(
307
396
  report.warning(
308
397
  "validation-error",
309
398
  CYS_ISSUE.DANGLING_REFERENCE,
310
- `${strayViews.length + strayTables.length} view(s) or table(s) belong to a network the session does not hold and were not read: ${listed([...strayViews, ...strayTables.map((t) => t.name)])}`,
399
+ `${strayViews.length + strayTables.length} view${plural(strayViews.length + strayTables.length)} or table${plural(strayViews.length + strayTables.length)} ${agree(strayViews.length + strayTables.length, "belongs", "belong")} to a network the session does not hold and were not read: ${listed([...strayViews, ...strayTables.map((t) => t.name)])}`,
311
400
  );
312
401
  }
313
402
  if (layout.networkList === null) {
314
403
  return choices;
315
404
  }
316
- const tree = await readXmlTree(await session.read(layout.networkList), layout.networkList.name, report, inner);
405
+ const tree = await parseXmlTree(
406
+ await session.read(layout.networkList, report),
407
+ layout.networkList.name,
408
+ report,
409
+ inner,
410
+ );
411
+ if (typeof tree === "string") {
412
+ // the list only orders the networks: without it they keep the order of their entries
413
+ report.warning(
414
+ "unsupported",
415
+ CYS_ISSUE.ENTRY_SKIPPED,
416
+ `${layout.networkList.name}: ${tree}; it is not read and the networks are listed in entry order`,
417
+ );
418
+ return choices;
419
+ }
317
420
  const order = new Map<string, number>();
318
421
  for (const node of elementsNamed(tree, "network")) {
319
422
  const id = node.attrs.get("id");
@@ -355,7 +458,9 @@ function isGroupTable(table: TableEntry, session: Session, docs: ReadonlyMap<str
355
458
  * @returns the graphs
356
459
  */
357
460
  function registeredGraphs(parsed: Parsed): GraphRec[] {
358
- return parsed.doc.root.subgraphs.filter((g) => isCyTrue(g.registered));
461
+ const subnetworks = parsed.doc.root.subgraphs.filter((g) => isCyTrue(g.registered));
462
+ // a registered root with no subnetworks (another tool's network file) is the network itself
463
+ return subnetworks.length === 0 && isCyTrue(parsed.doc.root.registered) ? [parsed.doc.root] : subnetworks;
359
464
  }
360
465
 
361
466
  /**
@@ -366,100 +471,88 @@ function registeredGraphs(parsed: Parsed): GraphRec[] {
366
471
  * @param parsed - its network file, parsed for this import (its records are filled in)
367
472
  */
368
473
  async function import3(prepared: Prepared, choice: Choice3, sink: GraphSink, parsed: Parsed): Promise<void> {
369
- const { report, inner, session } = prepared;
370
- const { layout } = session;
371
- const { doc, dialect } = parsed;
372
- const graph = doc.root.subgraphs.find((g) => g.id === choice.graphId) as GraphRec;
373
- const members = membersOf(doc, graph);
474
+ const { graph } = choice;
475
+ const members = membersOf(parsed.doc, graph);
374
476
  const nodes = byId(members.nodes);
375
477
  const edges = byId(members.edges);
376
- const cache = new Map<string, Promise<CyTable | null>>();
377
- const tableOf = (path: string): Promise<CyTable | null> => {
378
- let pending = cache.get(path);
379
- if (pending === undefined) {
380
- const entry = layout.tables.find((t) => t.tablePath === path);
381
- pending =
382
- entry === undefined
383
- ? Promise.resolve(null)
384
- : session.read(entry).then((bytes) => readCyTable(bytes, path, entry.name, report, inner));
385
- cache.set(path, pending);
386
- }
387
- return pending;
388
- };
389
- const virtuals = layout.cytables === null ? [] : await readVirtuals(session, layout.cytables, report, inner);
390
- // a row of an element the file declares outside this network (a collapsed group's member, a
391
- // meta-edge) is Cytoscape's bookkeeping, not a stale row
392
- const declared = new Set<string>([
393
- ...doc.nodes.flatMap((n) => (n.id === null ? [] : [n.id])),
394
- ...doc.edges.flatMap((e) => (e.id === null ? [] : [e.id])),
395
- ]);
396
- let unmatched = 0;
397
- for (const entry of layout.tables.filter((t) => t.network === choice.graphId && t.element !== null)) {
398
- throwIfAborted(inner.signal);
399
- const table = await tableOf(entry.tablePath);
400
- if (table === null) {
401
- continue;
402
- }
403
- const local = entry.namespace === "LOCAL_ATTRS";
404
- const shared = entry.namespace === "SHARED_ATTRS";
405
- const namespace = local ? null : entry.namespace;
406
- const hidden = !local && !shared;
407
- const target = (key: string): AttRec[] | undefined => {
408
- if (entry.element === "network") {
409
- return key === choice.graphId ? graph.atts : undefined;
410
- }
411
- return (entry.element === "node" ? nodes : edges).get(key)?.atts;
412
- };
413
- for (const [key, cells] of table.rows) {
414
- const atts = target(key);
415
- if (atts === undefined) {
416
- if (!declared.has(key)) {
417
- unmatched++;
418
- }
419
- continue;
420
- }
421
- const line = table.lines.get(key) ?? 0;
422
- for (let i = 1; i < table.columns.length && i < cells.length; i++) {
423
- const att = cellAtt(table.columns[i], cells[i], line, namespace, hidden);
424
- if (att !== null) {
425
- atts.push(att);
426
- }
427
- }
478
+ // the records are shared by every network of the file: what this import adds is undone after
479
+ const restore = saveRecords(graph, nodes, edges);
480
+ try {
481
+ await fill3(prepared, choice, sink, parsed, nodes, edges);
482
+ } finally {
483
+ restore();
484
+ }
485
+ }
486
+
487
+ /**
488
+ * The state of the records an import fills in (atts, coordinates, graphics), and a function that
489
+ * puts it back.
490
+ * @param graph - the subnetwork
491
+ * @param nodes - its node records
492
+ * @param edges - its edge records
493
+ * @returns the restore function
494
+ */
495
+ function saveRecords(
496
+ graph: GraphRec,
497
+ nodes: ReadonlyMap<string, NodeRec>,
498
+ edges: ReadonlyMap<string, EdgeRec>,
499
+ ): () => void {
500
+ const saved = [...nodes.values()].map((n) => ({
501
+ n,
502
+ atts: n.atts.length,
503
+ x: n.x,
504
+ y: n.y,
505
+ z: n.z,
506
+ graphics: n.graphics,
507
+ }));
508
+ const savedEdges = [...edges.values()].map((e) => ({ e, atts: e.atts.length, graphics: e.graphics }));
509
+ const graphAtts = graph.atts.length;
510
+ const graphGraphics = graph.graphics;
511
+ return (): void => {
512
+ for (const { n, atts, x, y, z, graphics } of saved) {
513
+ n.atts.length = atts;
514
+ Object.assign(n, { x, y, z, graphics });
428
515
  }
429
- for (const virtual of await virtualColumnsOf(table, virtuals, tableOf, report)) {
430
- // a shared column named like a local one is renamed; the local one keeps the name
431
- const owned = table.columns.some((c) => c.name === virtual.column.name);
432
- let ns = namespace;
433
- if (local) {
434
- ns = owned ? "SHARED_ATTRS" : null;
435
- }
436
- for (const [key, text] of virtual.values) {
437
- const att = cellAtt(virtual.column, text, table.lines.get(key) ?? 0, ns, hidden);
438
- if (att !== null) {
439
- target(key)?.push(att);
440
- }
441
- }
516
+ for (const { e, atts, graphics } of savedEdges) {
517
+ e.atts.length = atts;
518
+ e.graphics = graphics;
442
519
  }
443
- }
520
+ graph.atts.length = graphAtts;
521
+ graph.graphics = graphGraphics;
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Fill one subnetwork's records from its tables and views and emit it.
527
+ * @param prepared - the prepared import
528
+ * @param choice - the subnetwork
529
+ * @param sink - the sink
530
+ * @param parsed - its network file
531
+ * @param nodes - its node records by id
532
+ * @param edges - its edge records by id
533
+ */
534
+ async function fill3(
535
+ prepared: Prepared,
536
+ choice: Choice3,
537
+ sink: GraphSink,
538
+ parsed: Parsed,
539
+ nodes: ReadonlyMap<string, NodeRec>,
540
+ edges: ReadonlyMap<string, EdgeRec>,
541
+ ): Promise<void> {
542
+ const { report, inner } = prepared;
543
+ const { doc, dialect } = parsed;
544
+ const { graph } = choice;
545
+ const members = membersOf(doc, graph);
546
+ const unmatched = await applyTables(prepared, choice, doc, nodes, edges);
444
547
  if (unmatched > 0) {
445
548
  report.warning(
446
549
  "parse-error",
447
550
  CYS_ISSUE.TABLE_ROW,
448
- `${unmatched} table row(s) of network ${choice.graphId} name no node, edge or network of it (stale rows); they are not read`,
551
+ `${unmatched} table row${plural(unmatched)} of network ${choice.graphId} ${agree(unmatched, "names", "name")} no node, edge or network in it (left over from deleted elements) and ${agree(unmatched, "is", "are")} not read`,
449
552
  );
450
553
  }
451
- const views = layout.views.filter((v) => v.network === choice.graphId);
452
- let visualStyle: string | null = null;
453
- const extraViews: Map<string, [number, number, number]>[] = [];
454
- for (const [i, view] of views.entries()) {
455
- const viewDoc = (await parseEntry(session, view, report, inner)).doc;
456
- if (i === 0) {
457
- visualStyle = viewDoc.root.attrs.get("cy:visualStyle") ?? null;
458
- applyView(viewDoc, graph, nodes, edges, report, view.name);
459
- } else {
460
- extraViews.push(viewPositions(viewDoc, prepared.zAs));
461
- }
462
- }
554
+ const restoredIds = inner.restoreMangledIds ? restoreIds(members.nodes) : new Map<string, string>();
555
+ const { visualStyle, extraViews, extraIds } = await readViews(prepared, choice, nodes, edges);
463
556
  const cyMeta = sessionMeta(prepared, {
464
557
  collection: doc.root.label ?? choice.network.title,
465
558
  network: choice.graphId,
@@ -473,40 +566,259 @@ async function import3(prepared: Prepared, choice: Choice3, sink: GraphSink, par
473
566
  entry: choice.network.name,
474
567
  graphName: localName(graph) ?? choice.name,
475
568
  metaExtra: { [META_KEY]: cyMeta },
476
- groupNodes: new Set(
477
- members.nodes
478
- .filter((n) => n.atts.some((a) => a.name === "__isGroup" && isCyTrue(a.value)))
479
- .flatMap((n) => (n.id === null ? [] : [n.id])),
480
- ),
569
+ labelFromName: true,
570
+ restoredIds,
571
+ groupNodes: groupNodeIds(members.nodes),
481
572
  resolvePointer: pointerResolver(prepared, doc),
482
573
  onMissingMember: (group, member): void => {
483
- const record = groups.find((g) => g.group === group);
484
- if (record === undefined) {
485
- groups.push({ group, members: [member] });
486
- } else if (!record.members.includes(member)) {
487
- record.members.push(member);
488
- }
574
+ addMissingMember(groups, group, member);
489
575
  },
490
576
  };
491
577
  const settings: XgmmlSettings = { labelAliases: false, cytoscapeEscapes: false, zAs: prepared.zAs };
492
578
  new XgmmlEmitter(doc, dialect, sink, report, prepared.inner, settings, extras).emit(graph);
493
579
  if (groups.length > 0) {
580
+ const missing = groups.reduce((n, g) => n + g.members.length, 0);
494
581
  report.warning(
495
582
  "unsupported",
496
583
  CYS_ISSUE.COLLAPSED_GROUP,
497
- `${groups.length} collapsed group(s) hold ${groups.reduce((n, g) => n + g.members.length, 0)} member(s) that are not in the network; they are listed in meta.extra.cytoscape.groups`,
584
+ `${groups.length} collapsed group${plural(groups.length)} ${agree(groups.length, "holds", "hold")} ${missing} member${plural(missing)} that are not in the network; they are listed in meta.extra.cytoscape.groups`,
498
585
  );
499
586
  }
500
587
  reportRootOnly(parsed, report, choice.network.name);
501
- writeViewPositions(
502
- sink,
503
- extraViews,
504
- views.slice(1).map((v) => v.view),
505
- report,
506
- prepared.inner,
588
+ writeViewPositions(sink, extraViews, extraIds, report, prepared.inner, restoredIds);
589
+ }
590
+
591
+ /**
592
+ * The ids of the nodes Cytoscape marks as groups.
593
+ * @param nodes - the network's node records
594
+ * @returns their ids
595
+ */
596
+ function groupNodeIds(nodes: readonly NodeRec[]): Set<string> {
597
+ return new Set(
598
+ nodes
599
+ .filter((n) => n.atts.some((a) => a.name === "__isGroup" && isCyTrue(a.value)))
600
+ .flatMap((n) => (n.id === null ? [] : [n.id])),
507
601
  );
508
602
  }
509
603
 
604
+ /**
605
+ * Record a collapsed group's member that is not in the network.
606
+ * @param groups - the groups with missing members, added to
607
+ * @param group - the group's id
608
+ * @param member - the member's id
609
+ */
610
+ function addMissingMember(groups: { group: string; members: string[] }[], group: string, member: string): void {
611
+ const record = groups.find((g) => g.group === group);
612
+ if (record === undefined) {
613
+ groups.push({ group, members: [member] });
614
+ } else if (!record.members.includes(member)) {
615
+ record.members.push(member);
616
+ }
617
+ }
618
+
619
+ /** What reading one subnetwork's tables shares. */
620
+ interface TableContext {
621
+ readonly choice: Choice3;
622
+ readonly graph: Choice3["graph"];
623
+ readonly nodes: ReadonlyMap<string, NodeRec>;
624
+ readonly edges: ReadonlyMap<string, EdgeRec>;
625
+ /** The ids of the elements the network file declares, inside this network or outside it. */
626
+ readonly declared: ReadonlySet<string>;
627
+ readonly virtuals: readonly VirtualColumn[];
628
+ readonly tableOf: (path: string) => Promise<CyTable | null>;
629
+ readonly report: ImportReportBuilder;
630
+ }
631
+
632
+ /**
633
+ * Add the cells of a subnetwork's tables to its node, edge and network records.
634
+ * @param prepared - the prepared import
635
+ * @param choice - the subnetwork
636
+ * @param doc - its network file
637
+ * @param nodes - its node records by id
638
+ * @param edges - its edge records by id
639
+ * @returns the number of rows that name no element of the network
640
+ */
641
+ async function applyTables(
642
+ prepared: Prepared,
643
+ choice: Choice3,
644
+ doc: Parsed["doc"],
645
+ nodes: ReadonlyMap<string, NodeRec>,
646
+ edges: ReadonlyMap<string, EdgeRec>,
647
+ ): Promise<number> {
648
+ const { report, inner, session } = prepared;
649
+ const { layout } = session;
650
+ // each network reads its tables into its own report, so a shared table's issues, and a
651
+ // table that fails the import, land in every network that reads it.
652
+ // ponytail: a table shared by several networks is inflated and parsed once per network (the
653
+ // byte budget is charged once); cache the parse and relay its issues if importAll gets slow
654
+ const tables = new Map<string, Promise<CyTable | null>>();
655
+ const tableOf = (path: string): Promise<CyTable | null> => {
656
+ let pending = tables.get(path);
657
+ if (pending === undefined) {
658
+ const entry = layout.tables.find((t) => t.tablePath === path);
659
+ pending =
660
+ entry === undefined
661
+ ? Promise.resolve(null)
662
+ : session.read(entry, report).then((bytes) => readCyTable(bytes, path, entry.name, report, inner));
663
+ tables.set(path, pending);
664
+ }
665
+ return pending;
666
+ };
667
+ const ctx: TableContext = {
668
+ choice,
669
+ graph: choice.graph,
670
+ nodes,
671
+ edges,
672
+ // a row of an element the file declares outside this network (a collapsed group's member, a
673
+ // meta-edge) is Cytoscape's bookkeeping, not a stale row
674
+ declared: new Set<string>([
675
+ ...doc.nodes.flatMap((n) => (n.id === null ? [] : [n.id])),
676
+ ...doc.edges.flatMap((e) => (e.id === null ? [] : [e.id])),
677
+ ]),
678
+ virtuals: layout.cytables === null ? [] : await readVirtuals(session, layout.cytables, report, inner),
679
+ tableOf,
680
+ report,
681
+ };
682
+ let unmatched = 0;
683
+ for (const entry of layout.tables.filter((t) => t.network === choice.graphId && t.element !== null)) {
684
+ throwIfAborted(inner.signal);
685
+ const table = await tableOf(entry.tablePath);
686
+ if (table !== null) {
687
+ unmatched += await applyTable(ctx, entry, table);
688
+ }
689
+ }
690
+ return unmatched;
691
+ }
692
+
693
+ /**
694
+ * Add the cells of one table, and of the shared columns that extend it, to the records.
695
+ * @param ctx - the shared state
696
+ * @param entry - the table's entry
697
+ * @param table - the table
698
+ * @returns the number of rows that name no element of the network
699
+ */
700
+ async function applyTable(ctx: TableContext, entry: TableEntry, table: CyTable): Promise<number> {
701
+ const local = entry.namespace === "LOCAL_ATTRS";
702
+ const namespace = local ? null : entry.namespace;
703
+ const hidden = !local && entry.namespace !== "SHARED_ATTRS";
704
+ const target = (key: string): AttRec[] | undefined => {
705
+ if (entry.element === "network") {
706
+ return key === ctx.choice.graphId ? ctx.graph.atts : undefined;
707
+ }
708
+ return (entry.element === "node" ? ctx.nodes : ctx.edges).get(key)?.atts;
709
+ };
710
+ let unmatched = 0;
711
+ for (const [key, cells] of table.rows) {
712
+ const atts = target(key);
713
+ if (atts !== undefined) {
714
+ atts.push(...rowAtts(table, key, cells, namespace, hidden));
715
+ } else if (!ctx.declared.has(key)) {
716
+ unmatched++;
717
+ }
718
+ }
719
+ for (const virtual of await virtualColumnsOf(table, ctx.virtuals, ctx.tableOf, ctx.report)) {
720
+ // a shared column named like a local one is renamed; the local one keeps the name
721
+ const owned = table.columns.some((c) => c.name === virtual.column.name);
722
+ const ns = local ? sharedNamespace(owned) : namespace;
723
+ for (const [key, text] of virtual.values) {
724
+ const atts = target(key);
725
+ if (atts !== undefined) {
726
+ pushAtt(atts, cellAtt(virtual.column, text, table.lines.get(key) ?? 0, ns, hidden));
727
+ }
728
+ }
729
+ }
730
+ return unmatched;
731
+ }
732
+
733
+ /**
734
+ * The attributes of one table row's cells (the first cell is the SUID key).
735
+ * @param table - the table
736
+ * @param key - the row's key
737
+ * @param cells - the row's cells
738
+ * @param namespace - the namespace the attributes are written in, or null
739
+ * @param hidden - whether the table is one of Cytoscape's hidden ones
740
+ * @returns the attributes of the cells that hold a value
741
+ */
742
+ function rowAtts(
743
+ table: CyTable,
744
+ key: string,
745
+ cells: readonly string[],
746
+ namespace: string | null,
747
+ hidden: boolean,
748
+ ): AttRec[] {
749
+ const line = table.lines.get(key) ?? 0;
750
+ const out: AttRec[] = [];
751
+ for (let i = 1; i < table.columns.length && i < cells.length; i++) {
752
+ pushAtt(out, cellAtt(table.columns[i], cells[i], line, namespace, hidden));
753
+ }
754
+ return out;
755
+ }
756
+
757
+ /**
758
+ * The namespace of a shared column in a local table: renamed when a local column has its name.
759
+ * @param owned - whether a local column has the shared column's name
760
+ * @returns "SHARED_ATTRS", or null for none
761
+ */
762
+ function sharedNamespace(owned: boolean): string | null {
763
+ return owned ? "SHARED_ATTRS" : null;
764
+ }
765
+
766
+ /**
767
+ * Add an attribute to a record's list, unless there is none.
768
+ * @param atts - the record's attributes
769
+ * @param att - the attribute, or null
770
+ */
771
+ function pushAtt(atts: AttRec[], att: AttRec | null): void {
772
+ if (att !== null) {
773
+ atts.push(att);
774
+ }
775
+ }
776
+
777
+ /**
778
+ * Read a subnetwork's views: the first (the lowest view SUID, whatever the order of the entries)
779
+ * fills the records' coordinates and graphics, each further one gives positions only. A view that
780
+ * cannot be read is recorded and skipped.
781
+ * @param prepared - the prepared import
782
+ * @param choice - the subnetwork
783
+ * @param nodes - its node records by id
784
+ * @param edges - its edge records by id
785
+ * @returns the first view's style, and the positions and SUIDs of the further views
786
+ */
787
+ async function readViews(
788
+ prepared: Prepared,
789
+ choice: Choice3,
790
+ nodes: ReadonlyMap<string, NodeRec>,
791
+ edges: ReadonlyMap<string, EdgeRec>,
792
+ ): Promise<{
793
+ visualStyle: string | null;
794
+ extraViews: Map<string, [number, number, number]>[];
795
+ extraIds: string[];
796
+ }> {
797
+ const { report, inner, session } = prepared;
798
+ const views = session.layout.views
799
+ .filter((v) => v.network === choice.graphId)
800
+ .sort((a, b) => a.view.length - b.view.length || (a.view < b.view ? -1 : Number(a.view > b.view)));
801
+ let visualStyle: string | null = null;
802
+ const extraViews: Map<string, [number, number, number]>[] = [];
803
+ const extraIds: string[] = [];
804
+ let first = true;
805
+ for (const view of views) {
806
+ const viewDoc = (await parseOptionalEntry(session, view, report, inner, true))?.doc;
807
+ if (viewDoc === undefined) {
808
+ continue;
809
+ }
810
+ if (first) {
811
+ first = false;
812
+ visualStyle = viewDoc.root.attrs.get("cy:visualStyle") ?? null;
813
+ applyView(viewDoc, choice.graph, nodes, edges, report, view.name);
814
+ } else {
815
+ extraViews.push(viewPositions(viewDoc, prepared.zAs, nodes, report, view.name));
816
+ extraIds.push(view.view);
817
+ }
818
+ }
819
+ return { visualStyle, extraViews, extraIds };
820
+ }
821
+
510
822
  /**
511
823
  * The virtual columns of `tables/cytables.xml`.
512
824
  * @param session - the session
@@ -521,9 +833,28 @@ async function readVirtuals(
521
833
  report: ImportReportBuilder,
522
834
  inner: ResolvedImportOptions,
523
835
  ): Promise<VirtualColumn[]> {
524
- const tree = await readXmlTree(await session.read(entry), entry.name, report, inner);
836
+ const tree = await parseXmlTree(await session.read(entry, report), entry.name, report, inner);
837
+ if (typeof tree === "string") {
838
+ // only the shared columns are lost: every table's own columns are still read
839
+ report.error(
840
+ "parse-error",
841
+ CYS_ISSUE.TABLE,
842
+ `${entryLabel(entry.name)}: ${tree}; its virtual columns are not read`,
843
+ );
844
+ return [];
845
+ }
525
846
  const out: VirtualColumn[] = [];
526
847
  for (const node of elementsNamed(tree, "virtualColumn")) {
848
+ const missing = ["name", "targetTable", "sourceTable", "sourceColumn"].filter((k) => !node.attrs.has(k));
849
+ if (missing.length > 0) {
850
+ const named = node.attrs.has("name") ? ` "${node.attrs.get("name") ?? ""}"` : "";
851
+ report.error(
852
+ "parse-error",
853
+ CYS_ISSUE.TABLE,
854
+ `${entryLabel(entry.name)}: a virtual column${named} has no ${missing.join(", ")}; the column is not read`,
855
+ );
856
+ continue;
857
+ }
527
858
  const a = (key: string): string => node.attrs.get(key) ?? "";
528
859
  out.push({
529
860
  name: a("name"),
@@ -572,12 +903,14 @@ function applyView(
572
903
  entry: string,
573
904
  ): void {
574
905
  let dangling = 0;
906
+ const seen = new Set<string>();
575
907
  for (const node of view.nodes) {
576
908
  const target = node.viewId === null ? undefined : nodes.get(node.viewId);
577
909
  if (target === undefined) {
578
910
  dangling++;
579
911
  continue;
580
912
  }
913
+ duplicateViewNode(seen, node, report, entry);
581
914
  target.x = node.x;
582
915
  target.y = node.y;
583
916
  target.z = node.z;
@@ -602,28 +935,93 @@ function applyView(
602
935
  report.warning(
603
936
  "validation-error",
604
937
  CYS_ISSUE.DANGLING_REFERENCE,
605
- `${entry}: ${dangling} view element(s) name no node or edge of the network; they are not read`,
938
+ `${entryLabel(entry)}: ${dangling} view element${plural(dangling)} ${agree(dangling, "names", "name")} no node or edge of the network; they are not read`,
606
939
  );
607
940
  }
608
941
  }
609
942
 
610
943
  /**
611
- * The positions of a further view, by model node id: y flipped to y-up, z in the position only
612
- * under zAs "position".
944
+ * W_DUPLICATE_NODE for a view element that repeats a node an earlier one of the view gave.
945
+ * @param seen - the model node ids seen so far in the view
946
+ * @param node - the view element
947
+ * @param report - the report
948
+ * @param entry - the view entry name
949
+ */
950
+ function duplicateViewNode(seen: Set<string>, node: NodeRec, report: ImportReportBuilder, entry: string): void {
951
+ const id = node.viewId as string;
952
+ if (seen.has(id)) {
953
+ report.warning(
954
+ "validation-error",
955
+ XGMML_ISSUE.DUPLICATE_NODE,
956
+ `${entryLabel(entry)}: the view gives node "${id}" twice; the later element is used`,
957
+ { line: node.line, element: id },
958
+ );
959
+ }
960
+ seen.add(id);
961
+ }
962
+
963
+ /**
964
+ * The positions of a further view, by model node id, read by the first view's rules: y flipped
965
+ * to y-up, z in the position only under zAs "position", a coordinate that does not parse
966
+ * E_BAD_VALUE (the position is unset; a bad z is 0), an element naming no node of the network
967
+ * counted in one W_DANGLING_REFERENCE.
613
968
  * @param view - the view document
614
969
  * @param zAs - where z goes
970
+ * @param nodes - the network's node records by id
971
+ * @param report - the report
972
+ * @param entry - the view entry name
615
973
  * @returns the positions
616
974
  */
617
- function viewPositions(view: XgmmlDocument, zAs: "column" | "position"): Map<string, [number, number, number]> {
975
+ function viewPositions(
976
+ view: XgmmlDocument,
977
+ zAs: "column" | "position",
978
+ nodes: ReadonlyMap<string, NodeRec>,
979
+ report: ImportReportBuilder,
980
+ entry: string,
981
+ ): Map<string, [number, number, number]> {
618
982
  const out = new Map<string, [number, number, number]>();
983
+ const seen = new Set<string>();
984
+ let dangling = 0;
985
+ const coordinate = (text: string | null, node: NodeRec): number | null => {
986
+ if (text === null) {
987
+ return null;
988
+ }
989
+ const n = parseCoordinate(text);
990
+ if (n === null) {
991
+ report.error(
992
+ "validation-error",
993
+ XGMML_ISSUE.BAD_VALUE,
994
+ `${entryLabel(entry)}: graphics coordinate "${text}" is not a number`,
995
+ {
996
+ line: node.line,
997
+ element: node.viewId,
998
+ },
999
+ );
1000
+ }
1001
+ return n;
1002
+ };
619
1003
  for (const node of view.nodes) {
620
- const x = Number(node.x);
621
- const y = Number(node.y);
622
- const z = zAs === "position" ? Number(node.z ?? 0) : 0;
623
- if (node.viewId !== null && node.x !== null && node.y !== null && Number.isFinite(x) && Number.isFinite(y)) {
624
- out.set(node.viewId, [x, y === 0 ? 0 : -y, Number.isFinite(z) ? z : 0]);
1004
+ if (node.viewId === null || !nodes.has(node.viewId)) {
1005
+ dangling++;
1006
+ continue;
1007
+ }
1008
+ duplicateViewNode(seen, node, report, entry);
1009
+ const x = coordinate(node.x, node);
1010
+ const y = coordinate(node.y, node);
1011
+ const z = zAs === "position" ? coordinate(node.z, node) : null;
1012
+ if (x !== null && y !== null) {
1013
+ out.set(node.viewId, [x, y === 0 ? 0 : -y, z ?? 0]);
1014
+ } else {
1015
+ out.delete(node.viewId);
625
1016
  }
626
1017
  }
1018
+ if (dangling > 0) {
1019
+ report.warning(
1020
+ "validation-error",
1021
+ CYS_ISSUE.DANGLING_REFERENCE,
1022
+ `${entryLabel(entry)}: ${dangling} view element${plural(dangling)} ${agree(dangling, "names", "name")} no node of the network; they are not read`,
1023
+ );
1024
+ }
627
1025
  return out;
628
1026
  }
629
1027
 
@@ -634,6 +1032,7 @@ function viewPositions(view: XgmmlDocument, zAs: "column" | "position"): Map<str
634
1032
  * @param viewIds - each view's SUID
635
1033
  * @param report - the report
636
1034
  * @param common - the id rule
1035
+ * @param restoredIds - the original ids of mangled nodes, by written id
637
1036
  */
638
1037
  function writeViewPositions(
639
1038
  sink: GraphSink,
@@ -641,6 +1040,7 @@ function writeViewPositions(
641
1040
  viewIds: readonly string[],
642
1041
  report: ImportReportBuilder,
643
1042
  common: ResolvedImportOptions,
1043
+ restoredIds: ReadonlyMap<string, string>,
644
1044
  ): void {
645
1045
  const coercer = new IdCoercer(common.ids);
646
1046
  views.forEach((positions, i) => {
@@ -668,7 +1068,7 @@ function writeViewPositions(
668
1068
  for (const [id, xyz] of positions) {
669
1069
  let index: number;
670
1070
  try {
671
- index = sink.indexOf(coercer.text(id));
1071
+ index = sink.indexOf(coercer.text(restoredIds.get(id) ?? id));
672
1072
  } catch {
673
1073
  continue; // an id the id rule refuses was refused, and recorded, as a node already
674
1074
  }
@@ -679,6 +1079,30 @@ function writeViewPositions(
679
1079
  });
680
1080
  }
681
1081
 
1082
+ /**
1083
+ * The original ids an exporter's `sanitizeIds: "mangle"` kept in the node table's
1084
+ * `graphty:originalId` column; each such cell is taken out of the node's columns.
1085
+ * @param nodes - the node records, table cells merged in
1086
+ * @returns the original id by written id
1087
+ */
1088
+ function restoreIds(nodes: readonly NodeRec[]): Map<string, string> {
1089
+ const out = new Map<string, string>();
1090
+ for (const node of nodes) {
1091
+ const at = node.atts.findIndex(
1092
+ (a) =>
1093
+ a.name === ORIGINAL_ID_ATTRIBUTE &&
1094
+ (a.namespace ?? null) === null &&
1095
+ a.value !== null &&
1096
+ a.children.length === 0,
1097
+ );
1098
+ if (node.id !== null && at >= 0) {
1099
+ out.set(node.id, node.atts[at].value as string);
1100
+ node.atts.splice(at, 1);
1101
+ }
1102
+ }
1103
+ return out;
1104
+ }
1105
+
682
1106
  /**
683
1107
  * The network's name from its own table (the LOCAL `name` att), else null.
684
1108
  * @param graph - the subnetwork with its table atts
@@ -754,7 +1178,7 @@ function reportRootOnly(parsed: Parsed, report: ImportReportBuilder, entry: stri
754
1178
  report.warning(
755
1179
  "unsupported",
756
1180
  XGMML_ISSUE.ROOT_ONLY_ELEMENTS,
757
- `${entry}: ${nodes} node(s) and ${edges} edge(s) belong to no registered network (group meta-edges, collapsed group members) and were not read`,
1181
+ `${entryLabel(entry)}: ${elementCount(nodes, edges)} ${agree(nodes + edges, "belongs", "belong")} to none of the session's networks (group meta-edges or collapsed group members) and ${agree(nodes + edges, "was", "were")} not read`,
758
1182
  );
759
1183
  }
760
1184
  }
@@ -778,8 +1202,15 @@ async function networks2(
778
1202
  if (layout.cysession === null) {
779
1203
  return report.fail(CYS_ISSUE.NOT_SESSION, "the session has no cysession.xml");
780
1204
  }
781
- const tree = await readXmlTree(await session.read(layout.cysession), layout.cysession.name, report, inner);
1205
+ const tree = await readXmlTree(await session.read(layout.cysession, report), layout.cysession.name, report, inner);
782
1206
  const documentVersion = tree.attrs.get("documentVersion") ?? "";
1207
+ if (documentVersion.length > 0 && !/^\d+(\.\d+)*$/.test(documentVersion.trim())) {
1208
+ report.warning(
1209
+ "validation-error",
1210
+ XGMML_ISSUE.DOCUMENT_VERSION,
1211
+ `${layout.cysession.name}: documentVersion "${documentVersion}" does not parse; the session is read as Cytoscape 2.x`,
1212
+ );
1213
+ }
783
1214
  if (documentVersion.startsWith("3")) {
784
1215
  report.fail(
785
1216
  CYS_ISSUE.VERSION,
@@ -796,16 +1227,40 @@ async function networks2(
796
1227
  const missing: string[] = [];
797
1228
  const named = new Set<SessionEntry>();
798
1229
  for (const record of elementsNamed(tree, "network")) {
799
- const id = record.attrs.get("id") ?? "";
800
- const filename = record.attrs.get("filename") ?? `${id}.xgmml`;
801
- if (id === "Network Root") {
1230
+ const given = record.attrs.get("id");
1231
+ const filename = record.attrs.get("filename") ?? (given === undefined ? undefined : `${given}.xgmml`);
1232
+ if (given === "Network Root") {
1233
+ continue;
1234
+ }
1235
+ if (filename === undefined) {
1236
+ sessionRecord(
1237
+ report,
1238
+ layout.cysession.name,
1239
+ "a <network> has neither an id nor a filename; it is not read",
1240
+ );
802
1241
  continue;
803
1242
  }
1243
+ const id = given ?? urlDecode(filename.replace(/\.xgmml$/i, ""));
1244
+ if (given === undefined) {
1245
+ sessionRecord(
1246
+ report,
1247
+ layout.cysession.name,
1248
+ `a <network> for ${filename} has no id; it is named "${id}" after its file`,
1249
+ );
1250
+ }
804
1251
  const file = byDecoded.get(filename);
805
1252
  if (file === undefined) {
806
1253
  missing.push(filename);
807
1254
  continue;
808
1255
  }
1256
+ if (named.has(file)) {
1257
+ sessionRecord(
1258
+ report,
1259
+ layout.cysession.name,
1260
+ `the <network> "${id}" names ${filename}, which an earlier record already names; it is not read a second time`,
1261
+ );
1262
+ continue;
1263
+ }
809
1264
  named.add(file);
810
1265
  const parent = record.children.find((c) => c.name === "parent")?.attrs.get("id") ?? null;
811
1266
  choices.push({
@@ -820,7 +1275,7 @@ async function networks2(
820
1275
  report.warning(
821
1276
  "validation-error",
822
1277
  CYS_ISSUE.DANGLING_REFERENCE,
823
- `cysession.xml names ${missing.length} network file(s) the session does not hold: ${listed(missing)}`,
1278
+ `cysession.xml names ${missing.length} network file${plural(missing.length)} the session does not hold: ${listed(missing)}`,
824
1279
  );
825
1280
  }
826
1281
  const unnamed = order.filter((e) => !named.has(e)).map((e) => e.name);
@@ -828,12 +1283,23 @@ async function networks2(
828
1283
  report.warning(
829
1284
  "unsupported",
830
1285
  CYS_ISSUE.ENTRY_SKIPPED,
831
- `${unnamed.length} network file(s) are not in cysession.xml's network tree and were not read: ${listed(unnamed)}`,
1286
+ `${unnamed.length} network file${plural(unnamed.length)} ${agree(unnamed.length, "is", "are")} not in cysession.xml's network tree and were not read: ${listed(unnamed)}`,
832
1287
  );
833
1288
  }
834
1289
  return choices.sort((a, b) => order.indexOf(a.file) - order.indexOf(b.file));
835
1290
  }
836
1291
 
1292
+ /**
1293
+ * W_CYS_SESSION_RECORD: a cysession.xml network record without an id, or one naming a file an
1294
+ * earlier record names.
1295
+ * @param report - the report
1296
+ * @param entry - the cysession.xml entry name
1297
+ * @param message - what is wrong and what is done
1298
+ */
1299
+ function sessionRecord(report: ImportReportBuilder, entry: string, message: string): void {
1300
+ report.warning("validation-error", CYS_ISSUE.SESSION_RECORD, `${entryLabel(entry)}: ${message}`);
1301
+ }
1302
+
837
1303
  /**
838
1304
  * Import one network of a 2.x session.
839
1305
  * @param prepared - the prepared import
@@ -843,17 +1309,25 @@ async function networks2(
843
1309
  async function import2(prepared: Prepared, choice: Choice2, sink: GraphSink): Promise<void> {
844
1310
  const { report, inner, session } = prepared;
845
1311
  const { doc, dialect } = await parseEntry(session, choice.file, report, inner);
1312
+ let unmatched = 0;
846
1313
  for (const [list, column] of [
847
1314
  ["selectedNodes", SELECTED_COLUMN],
848
1315
  ["hiddenNodes", HIDDEN_COLUMN],
849
1316
  ] as const) {
850
- mark(doc.nodes, idsIn(choice.record, list), column);
1317
+ unmatched += mark(doc.nodes, idsIn(choice.record, list), column);
851
1318
  }
852
1319
  for (const [list, column] of [
853
1320
  ["selectedEdges", SELECTED_COLUMN],
854
1321
  ["hiddenEdges", HIDDEN_COLUMN],
855
1322
  ] as const) {
856
- mark(doc.edges, idsIn(choice.record, list), column);
1323
+ unmatched += mark(doc.edges, idsIn(choice.record, list), column);
1324
+ }
1325
+ if (unmatched > 0) {
1326
+ report.warning(
1327
+ "validation-error",
1328
+ CYS_ISSUE.DANGLING_REFERENCE,
1329
+ `cysession.xml lists ${unmatched} selected or hidden name${plural(unmatched)} that ${choice.file.name} does not hold; they are not read`,
1330
+ );
857
1331
  }
858
1332
  const cyMeta = sessionMeta(prepared, {
859
1333
  collection: null,
@@ -896,12 +1370,20 @@ function idsIn(record: XmlNode, list: string): Set<string> {
896
1370
  * @param records - the node or edge records
897
1371
  * @param ids - the listed names
898
1372
  * @param column - the column
1373
+ * @returns how many listed names match no record
899
1374
  */
900
- function mark(records: readonly (NodeRec | EdgeRec)[], ids: ReadonlySet<string>, column: string): void {
1375
+ function mark(records: readonly (NodeRec | EdgeRec)[], ids: ReadonlySet<string>, column: string): number {
901
1376
  if (ids.size === 0) {
902
- return;
1377
+ return 0;
903
1378
  }
1379
+ const matched = new Set<string>();
904
1380
  for (const record of records) {
1381
+ if (record.id !== null && ids.has(record.id)) {
1382
+ matched.add(record.id);
1383
+ }
1384
+ if (record.label !== null && ids.has(record.label)) {
1385
+ matched.add(record.label);
1386
+ }
905
1387
  if ((record.id !== null && ids.has(record.id)) || (record.label !== null && ids.has(record.label))) {
906
1388
  record.atts.push({
907
1389
  name: column,
@@ -921,6 +1403,7 @@ function mark(records: readonly (NodeRec | EdgeRec)[], ids: ReadonlySet<string>,
921
1403
  });
922
1404
  }
923
1405
  }
1406
+ return ids.size - matched.size;
924
1407
  }
925
1408
 
926
1409
  // ---------------------------------------------------------------------------------------- shared
@@ -957,14 +1440,14 @@ function sessionMeta(
957
1440
  report.warning(
958
1441
  "unsupported",
959
1442
  CYS_ISSUE.STYLES_NOT_IMPORTED,
960
- `the session's styles (${layout.styles.map((s) => s.path).join(", ")}) are not applied: style import is issue #706`,
1443
+ `the session's styles (${layout.styles.map((s) => s.path).join(", ")}) are not applied`,
961
1444
  );
962
1445
  }
963
1446
  if (layout.skipped.length > 0) {
964
1447
  report.warning(
965
1448
  "unsupported",
966
1449
  CYS_ISSUE.ENTRY_SKIPPED,
967
- `${layout.skipped.length} entries hold no graph data and were not read (apps, global tables, properties, images): ${listed(layout.skipped)}`,
1450
+ `${layout.skipped.length} ${layout.skipped.length === 1 ? "entry holds" : "entries hold"} no graph data and ${agree(layout.skipped.length, "was", "were")} not read (apps, global tables, properties, images): ${listed(layout.skipped)}`,
968
1451
  );
969
1452
  }
970
1453
  return {
@@ -1011,14 +1494,16 @@ async function importCys(
1011
1494
  options,
1012
1495
  prepared.report,
1013
1496
  );
1014
- if (prepared.choices.length > 1) {
1497
+ if (prepared.choices.length > 1 && !graphChosen(options)) {
1015
1498
  prepared.report.warning(
1016
1499
  "unsupported",
1017
1500
  CYS_ISSUE.MULTIPLE_GRAPHS,
1018
- `the session holds ${prepared.choices.length} networks; ${prepared.choices.length - 1} were not read (use importAll, graphIndex or graphName)`,
1501
+ `the session holds ${prepared.choices.length} networks; only the first is read (importAllGraphs() reads every one; graphIndex or graphName chooses one)`,
1019
1502
  );
1020
1503
  }
1021
- return importChoice(prepared, index, sink);
1504
+ const report = await importChoice(prepared, index, sink);
1505
+ prepared.session.done();
1506
+ return report;
1022
1507
  }
1023
1508
 
1024
1509
  /**
@@ -1033,17 +1518,19 @@ async function importAllCys(
1033
1518
  sinkFor: (index: number) => GraphSink,
1034
1519
  options?: CysImportOptions & CommonImportOptions,
1035
1520
  ): Promise<ImportReport[]> {
1036
- const bytes = (await readBytes(input, { signal: options?.signal ?? null })) ?? input;
1037
- const first = await prepare(bytes, null, options);
1521
+ // the archive is opened and its network files parsed once: one byte budget, one progress,
1522
+ // and every network's report starts from what reading the session recorded
1523
+ const first = await prepare(input, null, options);
1524
+ const opened = first.report.fork();
1038
1525
  const reports: ImportReport[] = [];
1039
1526
  for (let i = 0; i < first.choices.length; i++) {
1040
1527
  const sink = sinkFor(i);
1041
- // every network gets fresh records and a fresh report
1042
- const prepared = i === 0 ? first : await prepare(bytes, null, options);
1528
+ const prepared = i === 0 ? first : { ...first, report: opened.fork() };
1043
1529
  reportSinkOptions(sink, options, prepared.report, true);
1044
1530
  reportUnusedOptions(options, prepared.report, USED_OPTIONS);
1045
1531
  reports.push(await importChoice(prepared, i, sink));
1046
1532
  }
1533
+ first.session.done();
1047
1534
  return reports;
1048
1535
  }
1049
1536
 
@@ -1068,8 +1555,18 @@ async function listCysGraphs(
1068
1555
  );
1069
1556
  }
1070
1557
 
1071
- /** A session's marker or folder name in the head of the archive. */
1072
- const SESSION_NAME = /CytoscapeSession|cysession\.xml|\d+\.\d+\.\d+\.version/;
1558
+ /**
1559
+ * A session's marker or folder name in the head of the archive, or an entry only a session has
1560
+ * (Cytoscape's app state, a network, view or table file), for a session whose folder was renamed.
1561
+ * The head is decoded whole, so a name can sit anywhere in it.
1562
+ */
1563
+ const SESSION_NAMES: readonly RegExp[] = [
1564
+ /CytoscapeSession|cysession\.xml/,
1565
+ /(?<!\d)\d+\.\d+\.\d+\.version/,
1566
+ /(?:^|\/)apps\/org\.cytoscape\./,
1567
+ /(?:^|\/)(?:networks|views)\/\d+-[^/]*\.xgmml/,
1568
+ /(?:^|\/)tables\/\d+-[^/]*\/[^/]+\.cytable/,
1569
+ ];
1073
1570
 
1074
1571
  /**
1075
1572
  * Confidence that a head of bytes is a Cytoscape session: 0.95 for a zip whose head names the
@@ -1078,19 +1575,24 @@ const SESSION_NAME = /CytoscapeSession|cysession\.xml|\d+\.\d+\.\d+\.version/;
1078
1575
  * start after a short stub (a self-extracting archive) within the head.
1079
1576
  * @param head - the first bytes
1080
1577
  * @returns the confidence
1578
+ * @category Plugin helpers
1081
1579
  */
1082
1580
  export function sniffCys(head: Uint8Array): number {
1083
1581
  for (let at = 0; at + 4 <= head.byteLength; at++) {
1084
1582
  if (startsLikeZip(head.subarray(at))) {
1085
- return SESSION_NAME.test(decodeEntryName(head.subarray(at))) ? 0.95 : 0;
1583
+ return SESSION_NAMES.some((r) => r.test(decodeEntryName(head.subarray(at)))) ? 0.95 : 0;
1086
1584
  }
1087
1585
  }
1088
1586
  return 0;
1089
1587
  }
1090
1588
 
1091
- /** The Cytoscape session importer. */
1589
+ /**
1590
+ * The Cytoscape session importer.
1591
+ * @category Built-in formats
1592
+ */
1092
1593
  export const cysImporter: GraphImporter<CysImportOptions> = Object.freeze({
1093
1594
  format: FORMAT,
1595
+ options: Object.freeze(["maxUncompressedBytes", "zAs"]),
1094
1596
  extensions: EXTENSIONS,
1095
1597
  mimeTypes: MIME_TYPES,
1096
1598
  sniff: sniffCys,