@graphty/graph-io 0.3.24 → 0.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/README.md +49 -414
  2. package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
  3. package/dist/chunks/choose-BbJI55dx.js +56 -0
  4. package/dist/chunks/choose-BbJI55dx.js.map +1 -0
  5. package/dist/chunks/constants-BCsFsMj4.js +231 -0
  6. package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
  7. package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
  8. package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
  9. package/dist/chunks/export-BTI4yQgy.js +4333 -0
  10. package/dist/chunks/export-BTI4yQgy.js.map +1 -0
  11. package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
  12. package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
  13. package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
  14. package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
  15. package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
  16. package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
  17. package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
  18. package/dist/chunks/exporter-tne475KK.js.map +1 -0
  19. package/dist/chunks/importer-90YWhJoF.js +2811 -0
  20. package/dist/chunks/importer-90YWhJoF.js.map +1 -0
  21. package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
  22. package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
  23. package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
  24. package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
  25. package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
  26. package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
  27. package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
  28. package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
  29. package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
  30. package/dist/chunks/importer-DfVu6j59.js.map +1 -0
  31. package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
  32. package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
  33. package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
  34. package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
  35. package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
  36. package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
  37. package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
  38. package/dist/chunks/records-CQXT__mY.js.map +1 -0
  39. package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
  40. package/dist/chunks/text-DmcUs5_Q.js.map +1 -0
  41. package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
  42. package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
  43. package/dist/csv.js +951 -261
  44. package/dist/csv.js.map +1 -1
  45. package/dist/cx.js +6 -3
  46. package/dist/cx2.js +2 -2
  47. package/dist/cys.js +6 -3
  48. package/dist/dot.js +1 -1
  49. package/dist/gexf.js +554 -122
  50. package/dist/gexf.js.map +1 -1
  51. package/dist/gml.js +342 -85
  52. package/dist/gml.js.map +1 -1
  53. package/dist/graph-io.js +1105 -213
  54. package/dist/graph-io.js.map +1 -1
  55. package/dist/graphml.js +3 -2
  56. package/dist/graphml.js.map +1 -1
  57. package/dist/json.js +3 -2
  58. package/dist/neo4j.js +564 -157
  59. package/dist/neo4j.js.map +1 -1
  60. package/dist/obo.js +6 -3
  61. package/dist/pajek.js +1 -1
  62. package/dist/src/children.d.ts +18 -6
  63. package/dist/src/children.d.ts.map +1 -1
  64. package/dist/src/children.js +6 -3
  65. package/dist/src/children.js.map +1 -1
  66. package/dist/src/common/attributes.d.ts +29 -11
  67. package/dist/src/common/attributes.d.ts.map +1 -1
  68. package/dist/src/common/attributes.js +25 -7
  69. package/dist/src/common/attributes.js.map +1 -1
  70. package/dist/src/common/cell-budget.d.ts +9 -2
  71. package/dist/src/common/cell-budget.d.ts.map +1 -1
  72. package/dist/src/common/cell-budget.js +11 -3
  73. package/dist/src/common/cell-budget.js.map +1 -1
  74. package/dist/src/common/choose.d.ts +19 -0
  75. package/dist/src/common/choose.d.ts.map +1 -0
  76. package/dist/src/common/choose.js +72 -0
  77. package/dist/src/common/choose.js.map +1 -0
  78. package/dist/src/common/codes.d.ts +456 -66
  79. package/dist/src/common/codes.d.ts.map +1 -1
  80. package/dist/src/common/codes.js +458 -66
  81. package/dist/src/common/codes.js.map +1 -1
  82. package/dist/src/common/compare.d.ts +81 -0
  83. package/dist/src/common/compare.d.ts.map +1 -0
  84. package/dist/src/common/compare.js +394 -0
  85. package/dist/src/common/compare.js.map +1 -0
  86. package/dist/src/common/cx-export.d.ts +123 -0
  87. package/dist/src/common/cx-export.d.ts.map +1 -0
  88. package/dist/src/common/cx-export.js +334 -0
  89. package/dist/src/common/cx-export.js.map +1 -0
  90. package/dist/src/common/declared-types.d.ts +22 -2
  91. package/dist/src/common/declared-types.d.ts.map +1 -1
  92. package/dist/src/common/declared-types.js +19 -1
  93. package/dist/src/common/declared-types.js.map +1 -1
  94. package/dist/src/common/direction.d.ts +74 -50
  95. package/dist/src/common/direction.d.ts.map +1 -1
  96. package/dist/src/common/direction.js +47 -27
  97. package/dist/src/common/direction.js.map +1 -1
  98. package/dist/src/common/escape.d.ts +21 -9
  99. package/dist/src/common/escape.d.ts.map +1 -1
  100. package/dist/src/common/escape.js +33 -17
  101. package/dist/src/common/escape.js.map +1 -1
  102. package/dist/src/common/export.d.ts +144 -32
  103. package/dist/src/common/export.d.ts.map +1 -1
  104. package/dist/src/common/export.js +216 -42
  105. package/dist/src/common/export.js.map +1 -1
  106. package/dist/src/common/format.d.ts +23 -1
  107. package/dist/src/common/format.d.ts.map +1 -1
  108. package/dist/src/common/format.js +26 -1
  109. package/dist/src/common/format.js.map +1 -1
  110. package/dist/src/common/ids.d.ts +9 -2
  111. package/dist/src/common/ids.d.ts.map +1 -1
  112. package/dist/src/common/ids.js +6 -2
  113. package/dist/src/common/ids.js.map +1 -1
  114. package/dist/src/common/input.d.ts +122 -28
  115. package/dist/src/common/input.d.ts.map +1 -1
  116. package/dist/src/common/input.js +890 -124
  117. package/dist/src/common/input.js.map +1 -1
  118. package/dist/src/common/json-elements.d.ts +144 -20
  119. package/dist/src/common/json-elements.d.ts.map +1 -1
  120. package/dist/src/common/json-elements.js +868 -188
  121. package/dist/src/common/json-elements.js.map +1 -1
  122. package/dist/src/common/line-format.d.ts +90 -0
  123. package/dist/src/common/line-format.d.ts.map +1 -0
  124. package/dist/src/common/line-format.js +185 -0
  125. package/dist/src/common/line-format.js.map +1 -0
  126. package/dist/src/common/lists.d.ts +6 -1
  127. package/dist/src/common/lists.d.ts.map +1 -1
  128. package/dist/src/common/lists.js +2 -0
  129. package/dist/src/common/lists.js.map +1 -1
  130. package/dist/src/common/ontology-export.d.ts +167 -0
  131. package/dist/src/common/ontology-export.d.ts.map +1 -0
  132. package/dist/src/common/ontology-export.js +359 -0
  133. package/dist/src/common/ontology-export.js.map +1 -0
  134. package/dist/src/common/ontology.d.ts +25 -3
  135. package/dist/src/common/ontology.d.ts.map +1 -1
  136. package/dist/src/common/ontology.js +30 -9
  137. package/dist/src/common/ontology.js.map +1 -1
  138. package/dist/src/common/options.d.ts +62 -12
  139. package/dist/src/common/options.d.ts.map +1 -1
  140. package/dist/src/common/options.js +89 -11
  141. package/dist/src/common/options.js.map +1 -1
  142. package/dist/src/common/plural.d.ts +31 -0
  143. package/dist/src/common/plural.d.ts.map +1 -0
  144. package/dist/src/common/plural.js +46 -0
  145. package/dist/src/common/plural.js.map +1 -0
  146. package/dist/src/common/report.d.ts +72 -14
  147. package/dist/src/common/report.d.ts.map +1 -1
  148. package/dist/src/common/report.js +146 -22
  149. package/dist/src/common/report.js.map +1 -1
  150. package/dist/src/common/temporal.d.ts +22 -5
  151. package/dist/src/common/temporal.d.ts.map +1 -1
  152. package/dist/src/common/temporal.js +13 -2
  153. package/dist/src/common/temporal.js.map +1 -1
  154. package/dist/src/common/text.d.ts +39 -23
  155. package/dist/src/common/text.d.ts.map +1 -1
  156. package/dist/src/common/text.js +51 -21
  157. package/dist/src/common/text.js.map +1 -1
  158. package/dist/src/common/weights.d.ts +37 -4
  159. package/dist/src/common/weights.d.ts.map +1 -1
  160. package/dist/src/common/weights.js +61 -2
  161. package/dist/src/common/weights.js.map +1 -1
  162. package/dist/src/common/writer.d.ts +19 -3
  163. package/dist/src/common/writer.d.ts.map +1 -1
  164. package/dist/src/common/writer.js +30 -2
  165. package/dist/src/common/writer.js.map +1 -1
  166. package/dist/src/common/xml.d.ts +125 -9
  167. package/dist/src/common/xml.d.ts.map +1 -1
  168. package/dist/src/common/xml.js +400 -83
  169. package/dist/src/common/xml.js.map +1 -1
  170. package/dist/src/common/zip.d.ts +44 -4
  171. package/dist/src/common/zip.d.ts.map +1 -1
  172. package/dist/src/common/zip.js +134 -5
  173. package/dist/src/common/zip.js.map +1 -1
  174. package/dist/src/formats/csv/exporter.d.ts +80 -24
  175. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  176. package/dist/src/formats/csv/exporter.js +145 -32
  177. package/dist/src/formats/csv/exporter.js.map +1 -1
  178. package/dist/src/formats/csv/header.d.ts +38 -9
  179. package/dist/src/formats/csv/header.d.ts.map +1 -1
  180. package/dist/src/formats/csv/header.js +46 -10
  181. package/dist/src/formats/csv/header.js.map +1 -1
  182. package/dist/src/formats/csv/importer.d.ts +187 -37
  183. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  184. package/dist/src/formats/csv/importer.js +835 -204
  185. package/dist/src/formats/csv/importer.js.map +1 -1
  186. package/dist/src/formats/csv/index.d.ts +100 -19
  187. package/dist/src/formats/csv/index.d.ts.map +1 -1
  188. package/dist/src/formats/csv/index.js +94 -21
  189. package/dist/src/formats/csv/index.js.map +1 -1
  190. package/dist/src/formats/csv/records.d.ts +95 -10
  191. package/dist/src/formats/csv/records.d.ts.map +1 -1
  192. package/dist/src/formats/csv/records.js +333 -122
  193. package/dist/src/formats/csv/records.js.map +1 -1
  194. package/dist/src/formats/csv/values.d.ts +1 -0
  195. package/dist/src/formats/csv/values.d.ts.map +1 -1
  196. package/dist/src/formats/csv/values.js +1 -0
  197. package/dist/src/formats/csv/values.js.map +1 -1
  198. package/dist/src/formats/cx/exporter.d.ts +102 -0
  199. package/dist/src/formats/cx/exporter.d.ts.map +1 -0
  200. package/dist/src/formats/cx/exporter.js +1217 -0
  201. package/dist/src/formats/cx/exporter.js.map +1 -0
  202. package/dist/src/formats/cx/importer.d.ts +79 -32
  203. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  204. package/dist/src/formats/cx/importer.js +680 -285
  205. package/dist/src/formats/cx/importer.js.map +1 -1
  206. package/dist/src/formats/cx/index.d.ts +4 -3
  207. package/dist/src/formats/cx/index.d.ts.map +1 -1
  208. package/dist/src/formats/cx/index.js +4 -3
  209. package/dist/src/formats/cx/index.js.map +1 -1
  210. package/dist/src/formats/cx2/exporter.d.ts +29 -11
  211. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  212. package/dist/src/formats/cx2/exporter.js +39 -268
  213. package/dist/src/formats/cx2/exporter.js.map +1 -1
  214. package/dist/src/formats/cx2/importer.d.ts +78 -28
  215. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  216. package/dist/src/formats/cx2/importer.js +226 -79
  217. package/dist/src/formats/cx2/importer.js.map +1 -1
  218. package/dist/src/formats/cx2/index.d.ts +2 -1
  219. package/dist/src/formats/cx2/index.d.ts.map +1 -1
  220. package/dist/src/formats/cx2/index.js +2 -1
  221. package/dist/src/formats/cx2/index.js.map +1 -1
  222. package/dist/src/formats/cys/constants.d.ts +66 -17
  223. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  224. package/dist/src/formats/cys/constants.js +59 -18
  225. package/dist/src/formats/cys/constants.js.map +1 -1
  226. package/dist/src/formats/cys/exporter.d.ts +118 -0
  227. package/dist/src/formats/cys/exporter.d.ts.map +1 -0
  228. package/dist/src/formats/cys/exporter.js +1072 -0
  229. package/dist/src/formats/cys/exporter.js.map +1 -0
  230. package/dist/src/formats/cys/importer.d.ts +22 -8
  231. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  232. package/dist/src/formats/cys/importer.js +510 -149
  233. package/dist/src/formats/cys/importer.js.map +1 -1
  234. package/dist/src/formats/cys/index.d.ts +4 -2
  235. package/dist/src/formats/cys/index.d.ts.map +1 -1
  236. package/dist/src/formats/cys/index.js +4 -2
  237. package/dist/src/formats/cys/index.js.map +1 -1
  238. package/dist/src/formats/cys/session.d.ts +46 -7
  239. package/dist/src/formats/cys/session.d.ts.map +1 -1
  240. package/dist/src/formats/cys/session.js +98 -24
  241. package/dist/src/formats/cys/session.js.map +1 -1
  242. package/dist/src/formats/cys/tables.d.ts +9 -2
  243. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  244. package/dist/src/formats/cys/tables.js +29 -7
  245. package/dist/src/formats/cys/tables.js.map +1 -1
  246. package/dist/src/formats/dot/exporter.d.ts +42 -14
  247. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  248. package/dist/src/formats/dot/exporter.js +56 -29
  249. package/dist/src/formats/dot/exporter.js.map +1 -1
  250. package/dist/src/formats/dot/importer.d.ts +91 -29
  251. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  252. package/dist/src/formats/dot/importer.js +190 -39
  253. package/dist/src/formats/dot/importer.js.map +1 -1
  254. package/dist/src/formats/dot/index.d.ts +2 -1
  255. package/dist/src/formats/dot/index.d.ts.map +1 -1
  256. package/dist/src/formats/dot/index.js +2 -1
  257. package/dist/src/formats/dot/index.js.map +1 -1
  258. package/dist/src/formats/dot/names.d.ts +44 -11
  259. package/dist/src/formats/dot/names.d.ts.map +1 -1
  260. package/dist/src/formats/dot/names.js +44 -11
  261. package/dist/src/formats/dot/names.js.map +1 -1
  262. package/dist/src/formats/dot/tokenizer.d.ts +9 -2
  263. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  264. package/dist/src/formats/dot/tokenizer.js +20 -5
  265. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  266. package/dist/src/formats/gexf/exporter.d.ts +46 -15
  267. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  268. package/dist/src/formats/gexf/exporter.js +45 -15
  269. package/dist/src/formats/gexf/exporter.js.map +1 -1
  270. package/dist/src/formats/gexf/importer.d.ts +87 -23
  271. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  272. package/dist/src/formats/gexf/importer.js +471 -90
  273. package/dist/src/formats/gexf/importer.js.map +1 -1
  274. package/dist/src/formats/gexf/index.d.ts +84 -21
  275. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  276. package/dist/src/formats/gexf/index.js +80 -23
  277. package/dist/src/formats/gexf/index.js.map +1 -1
  278. package/dist/src/formats/gexf/schema.d.ts +67 -15
  279. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  280. package/dist/src/formats/gexf/schema.js +59 -13
  281. package/dist/src/formats/gexf/schema.js.map +1 -1
  282. package/dist/src/formats/gml/exporter.d.ts +69 -21
  283. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/gml/exporter.js +74 -29
  285. package/dist/src/formats/gml/exporter.js.map +1 -1
  286. package/dist/src/formats/gml/importer.d.ts +116 -25
  287. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  288. package/dist/src/formats/gml/importer.js +290 -61
  289. package/dist/src/formats/gml/importer.js.map +1 -1
  290. package/dist/src/formats/gml/index.d.ts +87 -24
  291. package/dist/src/formats/gml/index.d.ts.map +1 -1
  292. package/dist/src/formats/gml/index.js +79 -26
  293. package/dist/src/formats/gml/index.js.map +1 -1
  294. package/dist/src/formats/gml/syntax.d.ts +67 -18
  295. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  296. package/dist/src/formats/gml/syntax.js +108 -26
  297. package/dist/src/formats/gml/syntax.js.map +1 -1
  298. package/dist/src/formats/graphml/constants.d.ts +174 -44
  299. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  300. package/dist/src/formats/graphml/constants.js +165 -44
  301. package/dist/src/formats/graphml/constants.js.map +1 -1
  302. package/dist/src/formats/graphml/exporter.d.ts +17 -9
  303. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  304. package/dist/src/formats/graphml/exporter.js +33 -12
  305. package/dist/src/formats/graphml/exporter.js.map +1 -1
  306. package/dist/src/formats/graphml/importer.d.ts +19 -7
  307. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  308. package/dist/src/formats/graphml/importer.js +466 -39
  309. package/dist/src/formats/graphml/importer.js.map +1 -1
  310. package/dist/src/formats/graphml/index.d.ts +2 -1
  311. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  312. package/dist/src/formats/graphml/index.js +2 -1
  313. package/dist/src/formats/graphml/index.js.map +1 -1
  314. package/dist/src/formats/graphml/tree.d.ts +6 -2
  315. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  316. package/dist/src/formats/graphml/tree.js +6 -2
  317. package/dist/src/formats/graphml/tree.js.map +1 -1
  318. package/dist/src/formats/graphml/yfiles.d.ts +8 -3
  319. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  320. package/dist/src/formats/graphml/yfiles.js +36 -15
  321. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  322. package/dist/src/formats/json/dialect.d.ts +88 -24
  323. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  324. package/dist/src/formats/json/dialect.js +109 -19
  325. package/dist/src/formats/json/dialect.js.map +1 -1
  326. package/dist/src/formats/json/exporter.d.ts +134 -22
  327. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  328. package/dist/src/formats/json/exporter.js +135 -54
  329. package/dist/src/formats/json/exporter.js.map +1 -1
  330. package/dist/src/formats/json/importer.d.ts +173 -52
  331. package/dist/src/formats/json/importer.d.ts.map +1 -1
  332. package/dist/src/formats/json/importer.js +899 -220
  333. package/dist/src/formats/json/importer.js.map +1 -1
  334. package/dist/src/formats/json/index.d.ts +3 -2
  335. package/dist/src/formats/json/index.d.ts.map +1 -1
  336. package/dist/src/formats/json/index.js +3 -2
  337. package/dist/src/formats/json/index.js.map +1 -1
  338. package/dist/src/formats/json/obographs-export.d.ts +68 -0
  339. package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
  340. package/dist/src/formats/json/obographs-export.js +941 -0
  341. package/dist/src/formats/json/obographs-export.js.map +1 -0
  342. package/dist/src/formats/json/obographs.d.ts +26 -0
  343. package/dist/src/formats/json/obographs.d.ts.map +1 -1
  344. package/dist/src/formats/json/obographs.js +297 -80
  345. package/dist/src/formats/json/obographs.js.map +1 -1
  346. package/dist/src/formats/neo4j/exporter.d.ts +109 -21
  347. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  348. package/dist/src/formats/neo4j/exporter.js +238 -85
  349. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  350. package/dist/src/formats/neo4j/header.d.ts +17 -4
  351. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  352. package/dist/src/formats/neo4j/header.js +36 -4
  353. package/dist/src/formats/neo4j/header.js.map +1 -1
  354. package/dist/src/formats/neo4j/importer.d.ts +107 -27
  355. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  356. package/dist/src/formats/neo4j/importer.js +415 -63
  357. package/dist/src/formats/neo4j/importer.js.map +1 -1
  358. package/dist/src/formats/neo4j/index.d.ts +73 -23
  359. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  360. package/dist/src/formats/neo4j/index.js +65 -25
  361. package/dist/src/formats/neo4j/index.js.map +1 -1
  362. package/dist/src/formats/obo/exporter.d.ts +150 -0
  363. package/dist/src/formats/obo/exporter.d.ts.map +1 -0
  364. package/dist/src/formats/obo/exporter.js +1736 -0
  365. package/dist/src/formats/obo/exporter.js.map +1 -0
  366. package/dist/src/formats/obo/importer.d.ts +95 -19
  367. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  368. package/dist/src/formats/obo/importer.js +475 -104
  369. package/dist/src/formats/obo/importer.js.map +1 -1
  370. package/dist/src/formats/obo/index.d.ts +6 -3
  371. package/dist/src/formats/obo/index.d.ts.map +1 -1
  372. package/dist/src/formats/obo/index.js +6 -3
  373. package/dist/src/formats/obo/index.js.map +1 -1
  374. package/dist/src/formats/obo/syntax.d.ts +108 -4
  375. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  376. package/dist/src/formats/obo/syntax.js +211 -34
  377. package/dist/src/formats/obo/syntax.js.map +1 -1
  378. package/dist/src/formats/pajek/exporter.d.ts +54 -16
  379. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  380. package/dist/src/formats/pajek/exporter.js +66 -34
  381. package/dist/src/formats/pajek/exporter.js.map +1 -1
  382. package/dist/src/formats/pajek/importer.d.ts +84 -22
  383. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  384. package/dist/src/formats/pajek/importer.js +303 -83
  385. package/dist/src/formats/pajek/importer.js.map +1 -1
  386. package/dist/src/formats/pajek/index.d.ts +2 -1
  387. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  388. package/dist/src/formats/pajek/index.js +2 -1
  389. package/dist/src/formats/pajek/index.js.map +1 -1
  390. package/dist/src/formats/pajek/syntax.d.ts +77 -14
  391. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  392. package/dist/src/formats/pajek/syntax.js +183 -51
  393. package/dist/src/formats/pajek/syntax.js.map +1 -1
  394. package/dist/src/formats/xgmml/columns.d.ts +1 -0
  395. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  396. package/dist/src/formats/xgmml/columns.js +4 -0
  397. package/dist/src/formats/xgmml/columns.js.map +1 -1
  398. package/dist/src/formats/xgmml/constants.d.ts +186 -56
  399. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  400. package/dist/src/formats/xgmml/constants.js +181 -57
  401. package/dist/src/formats/xgmml/constants.js.map +1 -1
  402. package/dist/src/formats/xgmml/document.d.ts +65 -11
  403. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  404. package/dist/src/formats/xgmml/document.js +157 -28
  405. package/dist/src/formats/xgmml/document.js.map +1 -1
  406. package/dist/src/formats/xgmml/emit.d.ts +61 -3
  407. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  408. package/dist/src/formats/xgmml/emit.js +129 -26
  409. package/dist/src/formats/xgmml/emit.js.map +1 -1
  410. package/dist/src/formats/xgmml/exporter.d.ts +13 -6
  411. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  412. package/dist/src/formats/xgmml/exporter.js +20 -21
  413. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  414. package/dist/src/formats/xgmml/importer.d.ts +33 -12
  415. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  416. package/dist/src/formats/xgmml/importer.js +35 -72
  417. package/dist/src/formats/xgmml/importer.js.map +1 -1
  418. package/dist/src/formats/xgmml/index.d.ts +1 -0
  419. package/dist/src/formats/xgmml/index.d.ts.map +1 -1
  420. package/dist/src/formats/xgmml/index.js +1 -0
  421. package/dist/src/formats/xgmml/index.js.map +1 -1
  422. package/dist/src/formats/xgmml/values.d.ts +8 -1
  423. package/dist/src/formats/xgmml/values.d.ts.map +1 -1
  424. package/dist/src/formats/xgmml/values.js +7 -2
  425. package/dist/src/formats/xgmml/values.js.map +1 -1
  426. package/dist/src/index.d.ts +23 -18
  427. package/dist/src/index.d.ts.map +1 -1
  428. package/dist/src/index.js +28 -16
  429. package/dist/src/index.js.map +1 -1
  430. package/dist/src/registry.d.ts +478 -92
  431. package/dist/src/registry.d.ts.map +1 -1
  432. package/dist/src/registry.js +807 -88
  433. package/dist/src/registry.js.map +1 -1
  434. package/dist/src/sniff.d.ts +61 -19
  435. package/dist/src/sniff.d.ts.map +1 -1
  436. package/dist/src/sniff.js +121 -29
  437. package/dist/src/sniff.js.map +1 -1
  438. package/dist/src/types.d.ts +362 -112
  439. package/dist/src/types.d.ts.map +1 -1
  440. package/dist/src/types.js +28 -7
  441. package/dist/src/types.js.map +1 -1
  442. package/dist/xgmml.js +2 -2
  443. package/docs/samples/README.md +25 -0
  444. package/docs/samples/got-edges.csv +353 -0
  445. package/docs/samples/got-network.graphml +1384 -0
  446. package/docs/samples/got-nodes.csv +108 -0
  447. package/docs/samples/got.cx +930 -0
  448. package/docs/samples/got.cx2 +465 -0
  449. package/docs/samples/got.gexf +471 -0
  450. package/docs/samples/got.gml +2650 -0
  451. package/docs/samples/got.json +2546 -0
  452. package/docs/samples/got.net +461 -0
  453. package/docs/samples/movies-nodes.csv +9 -0
  454. package/docs/samples/movies-rels.csv +8 -0
  455. package/docs/samples/networks.cys +0 -0
  456. package/docs/samples/proteins.xgmml +9 -0
  457. package/docs/samples/teams.gv +24 -0
  458. package/docs/samples/vehicles.obo +25 -0
  459. package/package.json +9 -4
  460. package/src/children.ts +18 -6
  461. package/src/common/attributes.ts +29 -11
  462. package/src/common/cell-budget.ts +11 -3
  463. package/src/common/choose.ts +89 -0
  464. package/src/common/codes.ts +490 -66
  465. package/src/common/compare.ts +472 -0
  466. package/src/common/cx-export.ts +432 -0
  467. package/src/common/declared-types.ts +38 -4
  468. package/src/common/direction.ts +78 -51
  469. package/src/common/escape.ts +33 -17
  470. package/src/common/export.ts +284 -47
  471. package/src/common/format.ts +30 -1
  472. package/src/common/ids.ts +14 -3
  473. package/src/common/input.ts +1057 -134
  474. package/src/common/json-elements.ts +1034 -198
  475. package/src/common/line-format.ts +249 -0
  476. package/src/common/lists.ts +6 -1
  477. package/src/common/ontology-export.ts +461 -0
  478. package/src/common/ontology.ts +31 -9
  479. package/src/common/options.ts +110 -15
  480. package/src/common/plural.ts +49 -0
  481. package/src/common/report.ts +168 -25
  482. package/src/common/temporal.ts +22 -5
  483. package/src/common/text.ts +62 -23
  484. package/src/common/weights.ts +74 -4
  485. package/src/common/writer.ts +37 -3
  486. package/src/common/xml.ts +477 -88
  487. package/src/common/zip.ts +169 -7
  488. package/src/formats/csv/exporter.ts +201 -46
  489. package/src/formats/csv/header.ts +50 -11
  490. package/src/formats/csv/importer.ts +1133 -245
  491. package/src/formats/csv/index.ts +106 -19
  492. package/src/formats/csv/records.ts +396 -122
  493. package/src/formats/csv/values.ts +1 -0
  494. package/src/formats/cx/exporter.ts +1483 -0
  495. package/src/formats/cx/importer.ts +888 -300
  496. package/src/formats/cx/index.ts +4 -3
  497. package/src/formats/cx2/exporter.ts +61 -338
  498. package/src/formats/cx2/importer.ts +317 -86
  499. package/src/formats/cx2/index.ts +2 -1
  500. package/src/formats/cys/constants.ts +62 -17
  501. package/src/formats/cys/exporter.ts +1368 -0
  502. package/src/formats/cys/importer.ts +659 -157
  503. package/src/formats/cys/index.ts +4 -2
  504. package/src/formats/cys/session.ts +139 -31
  505. package/src/formats/cys/tables.ts +47 -9
  506. package/src/formats/dot/exporter.ts +83 -37
  507. package/src/formats/dot/importer.ts +248 -54
  508. package/src/formats/dot/index.ts +2 -1
  509. package/src/formats/dot/names.ts +44 -11
  510. package/src/formats/dot/tokenizer.ts +25 -6
  511. package/src/formats/gexf/exporter.ts +57 -18
  512. package/src/formats/gexf/importer.ts +636 -105
  513. package/src/formats/gexf/index.ts +90 -21
  514. package/src/formats/gexf/schema.ts +67 -15
  515. package/src/formats/gml/exporter.ts +90 -36
  516. package/src/formats/gml/importer.ts +388 -67
  517. package/src/formats/gml/index.ts +85 -24
  518. package/src/formats/gml/syntax.ts +111 -26
  519. package/src/formats/graphml/constants.ts +178 -44
  520. package/src/formats/graphml/exporter.ts +52 -18
  521. package/src/formats/graphml/importer.ts +640 -60
  522. package/src/formats/graphml/index.ts +2 -1
  523. package/src/formats/graphml/tree.ts +6 -2
  524. package/src/formats/graphml/yfiles.ts +37 -15
  525. package/src/formats/json/dialect.ts +128 -29
  526. package/src/formats/json/exporter.ts +205 -68
  527. package/src/formats/json/importer.ts +1163 -253
  528. package/src/formats/json/index.ts +3 -1
  529. package/src/formats/json/obographs-export.ts +1197 -0
  530. package/src/formats/json/obographs.ts +353 -92
  531. package/src/formats/neo4j/exporter.ts +320 -101
  532. package/src/formats/neo4j/header.ts +44 -6
  533. package/src/formats/neo4j/importer.ts +512 -73
  534. package/src/formats/neo4j/index.ts +75 -23
  535. package/src/formats/obo/exporter.ts +2210 -0
  536. package/src/formats/obo/importer.ts +622 -109
  537. package/src/formats/obo/index.ts +6 -3
  538. package/src/formats/obo/syntax.ts +232 -37
  539. package/src/formats/pajek/exporter.ts +89 -41
  540. package/src/formats/pajek/importer.ts +407 -93
  541. package/src/formats/pajek/index.ts +2 -1
  542. package/src/formats/pajek/syntax.ts +213 -52
  543. package/src/formats/xgmml/columns.ts +10 -0
  544. package/src/formats/xgmml/constants.ts +183 -56
  545. package/src/formats/xgmml/document.ts +212 -34
  546. package/src/formats/xgmml/emit.ts +175 -34
  547. package/src/formats/xgmml/exporter.ts +35 -27
  548. package/src/formats/xgmml/importer.ts +64 -86
  549. package/src/formats/xgmml/index.ts +1 -0
  550. package/src/formats/xgmml/values.ts +11 -3
  551. package/src/index.ts +81 -11
  552. package/src/registry.ts +1121 -135
  553. package/src/sniff.ts +163 -32
  554. package/src/types.ts +385 -114
  555. package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
  556. package/dist/chunks/export-Bh60Dv-n.js +0 -582
  557. package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
  558. package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
  559. package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
  560. package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
  561. package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
  562. package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
  563. package/dist/chunks/importer-DET1tg6s.js.map +0 -1
  564. package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
  565. package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
  566. package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
  567. package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
  568. package/dist/chunks/importer-eUREhBGj.js.map +0 -1
  569. package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
  570. package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
  571. package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
  572. package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
  573. package/dist/chunks/ontology-BnrJ4I98.js +0 -113
  574. package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
  575. package/dist/chunks/records-dbRkxwaq.js.map +0 -1
  576. package/dist/chunks/report-B1z4WT9e.js +0 -2372
  577. package/dist/chunks/report-B1z4WT9e.js.map +0 -1
  578. package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
  579. package/dist/chunks/weights-Dzba96G3.js +0 -176
  580. package/dist/chunks/weights-Dzba96G3.js.map +0 -1
  581. package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
@@ -12,17 +12,37 @@
12
12
  */
13
13
  import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
14
14
  import { declareResolved, uniqueColumnName } from "./attributes.js";
15
- import { ASPECT_ORDER_CODE, BAD_ASPECT_BLOCK_CODE, COLUMN_RENAMED_CODE, COUNT_MISMATCH_CODE, STATUS_FAILED_CODE, STATUS_WARNING_CODE, } from "./codes.js";
15
+ import { ASPECT_ORDER_CODE, BAD_ASPECT_BLOCK_CODE, BAD_VALUE_CODE, COLUMN_RENAMED_CODE, COUNT_MISMATCH_CODE, DUPLICATE_ATTRIBUTE_CODE, JSON_NONSTANDARD_NUMBER_CODE, MULTI_ASPECT_FRAGMENT_CODE, PRECISION_CODE, STATUS_FAILED_CODE, STATUS_WARNING_CODE, } from "./codes.js";
16
+ import { agree, plural } from "./plural.js";
17
+ import { trimTrailingZeros } from "./text.js";
16
18
  // ============================================================ exact integers
17
- /** A run of 16 digits not inside a fraction: the shortest integer literal that can exceed 2^53 (9007199254740992). */
19
+ /**
20
+ * A run of 16 digits not inside a fraction: the shortest integer literal that can exceed 2^53 (9007199254740992).
21
+ * @category Plugin helpers
22
+ */
18
23
  export const MAYBE_UNSAFE_INTEGER = /(?<![0-9.])[0-9]{16}/;
24
+ /**
25
+ * A literal the double read for may not equal: an exponent of 15 or more (`9.007199254740993e15`,
26
+ * `1e400`: an integer beyond 2^53 or an overflow), an exponent of -300 or less (`1e-400`: an
27
+ * underflow to 0), or 16 or more digits split by a decimal point before an exponent
28
+ * (`90071992547409.93e2`). MAYBE_UNSAFE_INTEGER sees none of them.
29
+ * ponytail: a literal that underflows through 300+ leading fraction zeros and no exponent is not
30
+ * gated; add a digit-count check if such files turn up.
31
+ * @category Plugin helpers
32
+ */
33
+ export const MAYBE_INEXACT_EXPONENT = new RegExp([
34
+ /\d[eE]\+?0*(?:1[5-9]|[2-9]\d|[1-9]\d{2,})/, // an exponent of 15 or more
35
+ /\d[eE]-0*(?:[3-9]\d{2}|[1-9]\d{3,})/, // an exponent of -300 or less
36
+ /(?<![\d.])(?=(?:\d\.?){16})\d+\.\d+[eE]/, // 16 or more digits split by a decimal point
37
+ ]
38
+ .map((r) => r.source)
39
+ .join("|"));
19
40
  /**
20
41
  * The prefix of the string a non-standard token or an exact integer is rewritten to (a NUL
21
- * character first, which no sensible attribute value starts with).
42
+ * character first, which no sensible attribute value starts with). A document that already holds
43
+ * the text gets a numbered variant, so a genuine string is never revived as a number.
22
44
  */
23
- const SENTINEL = `${String.fromCharCode(0)}graph-io:`;
24
- /** The sentinel prefix of an integer literal kept as its digits. */
25
- const EXACT_SENTINEL = `${SENTINEL}exact:`;
45
+ const SENTINEL = `${String.fromCodePoint(0)}graph-io`;
26
46
  /** The non-standard tokens Python's json module writes, longest first so -Infinity wins over a bare minus. */
27
47
  const NONSTANDARD_TOKENS = [
28
48
  ["-Infinity", -Infinity],
@@ -31,24 +51,103 @@ const NONSTANDARD_TOKENS = [
31
51
  ];
32
52
  /** A JSON integer literal (no fraction, no exponent, no leading zero), as CANONICAL_INTEGER in common/ids.ts. */
33
53
  const INTEGER_LITERAL = /^-?(0|[1-9][0-9]*)$/;
54
+ /** A JSON number literal, split into sign, integer digits, fraction digits and exponent. */
55
+ const NUMBER_PARTS = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
56
+ /**
57
+ * The sentinel prefix of one rewrite: SENTINEL with a colon, or a numbered variant when the document
58
+ * already holds that one. A string can only decode to a NUL by escaping it (`\u0000`), so when the
59
+ * text holds that escape the escaped strings are decoded and searched too: any other escape in them
60
+ * (`graph\u002dio:`) would hide the sentinel from a search of the raw text.
61
+ * @param text - the document text
62
+ * @returns the prefix
63
+ */
64
+ function sentinelFor(text) {
65
+ let haystack = text;
66
+ if (/\\u0000/i.test(text)) {
67
+ const decoded = [];
68
+ for (const [literal] of text.matchAll(/"(?:[^"\\]|\\.)*"/g)) {
69
+ if (literal.includes("\\")) {
70
+ decoded.push(decodeString(literal));
71
+ }
72
+ }
73
+ haystack = `${decoded.join("\n")}\n${text}`;
74
+ }
75
+ let k = 0;
76
+ while (haystack.includes(`graph-io${k === 0 ? "" : String(k)}:`)) {
77
+ k++;
78
+ }
79
+ return `${SENTINEL}${k === 0 ? "" : String(k)}:`;
80
+ }
81
+ /**
82
+ * Decode one JSON string literal, or give it back as written when it is not valid (the parse then
83
+ * reports the syntax error).
84
+ * @param literal - the literal with its quotes
85
+ * @returns the decoded value
86
+ */
87
+ function decodeString(literal) {
88
+ try {
89
+ return String(JSON.parse(literal));
90
+ }
91
+ catch {
92
+ return literal;
93
+ }
94
+ }
95
+ /**
96
+ * Whether a number literal with a fraction or an exponent reads back as another value: an integer
97
+ * beyond 2^53 written with more significant digits than a double keeps, which the nearest double
98
+ * misses, or a finite literal beyond the double range or a non-zero one below it.
99
+ * @param literal - the literal as written
100
+ * @returns true when the double read for it is not the value it denotes
101
+ */
102
+ function isInexactLiteral(literal) {
103
+ const value = Number(literal);
104
+ if (!Number.isFinite(value)) {
105
+ return true;
106
+ }
107
+ if (value === 0) {
108
+ // a non-zero literal below the smallest double underflows to 0
109
+ return /[1-9]/.test(literal.split(/[eE]/)[0]);
110
+ }
111
+ if (Number.isSafeInteger(value) || !Number.isInteger(value)) {
112
+ return false;
113
+ }
114
+ const m = NUMBER_PARTS.exec(literal);
115
+ if (m === null) {
116
+ return false;
117
+ }
118
+ const digits = trimTrailingZeros(m[2] + (m[3] ?? ""));
119
+ const exponent = Number(m[4] ?? "0") - (m[3] ?? "").length + ((m[2] + (m[3] ?? "")).length - digits.length);
120
+ if (digits.replace(/^0+/, "").length <= 15 || exponent < 0) {
121
+ // up to 15 significant digits the nearest double reads back as the same text (1e39 is the
122
+ // float it says); a fraction that rounds to an integer is ordinary floating-point rounding
123
+ return false;
124
+ }
125
+ const exact = BigInt(digits) * 10n ** BigInt(exponent);
126
+ return exact !== BigInt(Math.abs(value));
127
+ }
34
128
  /**
35
129
  * Rewrite the numbers JSON.parse cannot read exactly, outside strings and in value positions only:
36
130
  * NaN / Infinity / -Infinity become sentinel strings (when `nonstandard` is true), an integer
37
131
  * literal that is not a safe integer becomes a string of its digits (or, with `exactSentinel`, a
38
- * sentinel string reviveExact() turns into an ExactInteger). A container stack tells a value
39
- * position (after `:`, `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays invalid.
132
+ * sentinel string the result's reviveExact turns into an ExactInteger). A container stack tells a
133
+ * value position (after `:`, `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays
134
+ * invalid. Literals with a fraction or an exponent that no double holds are listed, not rewritten.
40
135
  * @param text - the document text
41
136
  * @param options - which rewrites apply
42
137
  * @param options.nonstandard - rewrite NaN / Infinity / -Infinity (default true)
43
138
  * @param options.exactSentinel - quote big integers as sentinels instead of bare digits (default false)
44
- * @returns the rewritten text, the non-standard tokens seen and the integer literals quoted
139
+ * @returns the rewritten text, what was found and the revivers of this rewrite
140
+ * @category Plugin helpers
45
141
  */
46
142
  export function rewriteNumbers(text, options = {}) {
47
143
  const nonstandard = options.nonstandard ?? true;
48
144
  const exactSentinel = options.exactSentinel ?? false;
145
+ const sentinel = sentinelFor(text);
146
+ const exactPrefix = `${sentinel}exact:`;
49
147
  const parts = [];
50
148
  const tokens = new Set();
51
149
  const bigIntegers = [];
150
+ const inexact = [];
52
151
  const arrays = [];
53
152
  let expectValue = true;
54
153
  let copied = 0;
@@ -87,7 +186,7 @@ export function rewriteNumbers(text, options = {}) {
87
186
  if (token !== undefined) {
88
187
  end = i + token[0].length;
89
188
  tokens.add(token[0]);
90
- replacement = JSON.stringify(`${SENTINEL}${token[0]}`);
189
+ replacement = JSON.stringify(`${sentinel}${token[0]}`);
91
190
  }
92
191
  else if (ch === "-" || (ch >= "0" && ch <= "9")) {
93
192
  end = i + 1;
@@ -95,9 +194,14 @@ export function rewriteNumbers(text, options = {}) {
95
194
  end++;
96
195
  }
97
196
  const literal = text.slice(i, end);
98
- if (INTEGER_LITERAL.test(literal) && !Number.isSafeInteger(Number(literal))) {
99
- bigIntegers.push(literal);
100
- replacement = exactSentinel ? JSON.stringify(`${EXACT_SENTINEL}${literal}`) : `"${literal}"`;
197
+ if (INTEGER_LITERAL.test(literal)) {
198
+ if (!Number.isSafeInteger(Number(literal))) {
199
+ bigIntegers.push(literal);
200
+ replacement = exactSentinel ? JSON.stringify(`${exactPrefix}${literal}`) : `"${literal}"`;
201
+ }
202
+ }
203
+ else if (isInexactLiteral(literal)) {
204
+ inexact.push(literal);
101
205
  }
102
206
  }
103
207
  if (replacement !== null) {
@@ -110,23 +214,163 @@ export function rewriteNumbers(text, options = {}) {
110
214
  i++;
111
215
  }
112
216
  parts.push(text.slice(copied));
113
- return { text: parts.join(""), tokens, bigIntegers };
217
+ const revive = (_key, value) => {
218
+ if (typeof value === "string" && value.startsWith(sentinel)) {
219
+ const found = NONSTANDARD_TOKENS.find(([word]) => word === value.slice(sentinel.length));
220
+ return found === undefined ? value : found[1];
221
+ }
222
+ return value;
223
+ };
224
+ const reviveExact = (_key, value) => typeof value === "string" && value.startsWith(exactPrefix)
225
+ ? new ExactInteger(value.slice(exactPrefix.length))
226
+ : value;
227
+ return { text: parts.join(""), tokens, bigIntegers, inexact, revive, reviveExact };
114
228
  }
229
+ /** Keys of one object compared pairwise up to this count; a wider object switches to a Set. */
230
+ const LINEAR_KEYS = 32;
115
231
  /**
116
- * The JSON.parse reviver that turns the non-standard sentinel strings of rewriteNumbers() back into
117
- * numbers.
118
- * @param _key - the member key (unused)
119
- * @param value - the parsed value
120
- * @returns the number for a sentinel string, the value otherwise
232
+ * The keys that occur twice in one object of a valid JSON text, which JSON.parse silently reduces
233
+ * to the last value (RFC 8259 section 4: names SHOULD be unique). One pass over the characters
234
+ * that jumps over strings; the keys of a small object are compared in place, without
235
+ * slicing (the parse itself costs about as much as this scan, so it allocates nothing per key).
236
+ * Keys are compared as written: `"\u0069d"` and `"id"` are not recognized as one key.
237
+ * @param text - a text JSON.parse accepted (or its rewrite)
238
+ * @returns every repetition in document order
239
+ * @category Plugin helpers
121
240
  */
122
- export function reviveNonstandard(_key, value) {
123
- if (typeof value === "string" && value.startsWith(SENTINEL)) {
124
- const found = NONSTANDARD_TOKENS.find(([word]) => word === value.slice(SENTINEL.length));
125
- return found === undefined ? value : found[1];
241
+ export function findDuplicateKeys(text) {
242
+ const found = [];
243
+ // per depth: the [start, end) of each key of the open object (reused), or -1 for an array
244
+ const spans = [];
245
+ const wide = [];
246
+ let depth = 0;
247
+ const n = text.length;
248
+ let at = 0;
249
+ while (at < n) {
250
+ const c = text.codePointAt(at);
251
+ if (c === 123 || c === 91) {
252
+ // { or [
253
+ openContainer(spans, wide, depth, c === 91);
254
+ depth++;
255
+ }
256
+ else if (c === 125 || c === 93) {
257
+ depth--;
258
+ }
259
+ else if (c === 34) {
260
+ const end = closingQuote(text, at + 1);
261
+ const keys = depth > 0 ? spans[depth - 1] : null;
262
+ // an array's span list starts with -1: its strings are values, never keys
263
+ const isObjectKey = keys !== null && keys[0] !== -1 && isKey(text, end + 1);
264
+ if (isObjectKey && isRepeated(text, at + 1, end, keys, wide, depth - 1)) {
265
+ found.push({ key: text.slice(at + 1, end), offset: at });
266
+ }
267
+ at = end;
268
+ }
269
+ at++;
126
270
  }
127
- return value;
271
+ return found;
128
272
  }
129
- /** An integer literal beyond 2^53, kept as its exact digits (a CX id must never lose a digit). */
273
+ /**
274
+ * Start the key list of a container just opened at a depth (reusing the list of an earlier one).
275
+ * @param spans - the key spans per depth
276
+ * @param wide - the key sets per depth, for wide objects
277
+ * @param depth - the container's depth
278
+ * @param isArray - whether it is an array, whose strings are never keys
279
+ */
280
+ function openContainer(spans, wide, depth, isArray) {
281
+ if (spans.length === depth) {
282
+ spans.push([]);
283
+ wide.push(null);
284
+ }
285
+ spans[depth].length = 0;
286
+ if (isArray) {
287
+ spans[depth].push(-1);
288
+ }
289
+ wide[depth] = null;
290
+ }
291
+ /**
292
+ * Whether a key repeats one already seen in its object, remembering it otherwise.
293
+ * @param text - the text
294
+ * @param start - the key's first character
295
+ * @param end - the key's closing quote
296
+ * @param keys - the spans of the object's keys so far
297
+ * @param wide - the Set of each depth's keys once the object is wide
298
+ * @param level - the object's depth
299
+ * @returns true for a repetition
300
+ */
301
+ function isRepeated(text, start, end, keys, wide, level) {
302
+ const set = wide[level];
303
+ if (set !== null) {
304
+ const key = text.slice(start, end);
305
+ return set.has(key) || (set.add(key), false);
306
+ }
307
+ const length = end - start;
308
+ for (let k = 0; k < keys.length; k += 2) {
309
+ if (keys[k + 1] - keys[k] === length && sameText(text, keys[k], start, length)) {
310
+ return true;
311
+ }
312
+ }
313
+ keys.push(start, end);
314
+ if (keys.length > 2 * LINEAR_KEYS) {
315
+ const all = new Set();
316
+ for (let k = 0; k < keys.length; k += 2) {
317
+ all.add(text.slice(keys[k], keys[k + 1]));
318
+ }
319
+ wide[level] = all;
320
+ }
321
+ return false;
322
+ }
323
+ /**
324
+ * Whether two ranges of one text hold the same characters.
325
+ * @param text - the text
326
+ * @param a - the first range's start
327
+ * @param b - the second range's start
328
+ * @param length - the ranges' length
329
+ * @returns true when equal
330
+ */
331
+ function sameText(text, a, b, length) {
332
+ for (let i = 0; i < length; i++) {
333
+ if (text.codePointAt(a + i) !== text.codePointAt(b + i)) {
334
+ return false;
335
+ }
336
+ }
337
+ return true;
338
+ }
339
+ /**
340
+ * The offset of the quote that closes a JSON string.
341
+ * @param text - the text
342
+ * @param from - the offset after the opening quote
343
+ * @returns the closing quote's offset (the text length when there is none)
344
+ */
345
+ function closingQuote(text, from) {
346
+ for (let q = text.indexOf('"', from); q >= 0; q = text.indexOf('"', q + 1)) {
347
+ let backslashes = 0;
348
+ while (text.codePointAt(q - 1 - backslashes) === 92) {
349
+ backslashes++;
350
+ }
351
+ if (backslashes % 2 === 0) {
352
+ return q;
353
+ }
354
+ }
355
+ return text.length;
356
+ }
357
+ /**
358
+ * Whether the string that ends before an offset is an object key: a colon follows after whitespace.
359
+ * @param text - the text
360
+ * @param from - the offset after the closing quote
361
+ * @returns true for a key
362
+ */
363
+ function isKey(text, from) {
364
+ let i = from;
365
+ while (i < text.length && isSpace(text.codePointAt(i))) {
366
+ i++;
367
+ }
368
+ return text.codePointAt(i) === 58;
369
+ }
370
+ /**
371
+ * An integer literal beyond 2^53, kept as its exact digits (a CX id must never lose a digit).
372
+ * @category Plugin helpers
373
+ */
130
374
  export class ExactInteger {
131
375
  /**
132
376
  * Wrap the digits of an integer literal.
@@ -143,22 +387,11 @@ export class ExactInteger {
143
387
  return Number(this.digits);
144
388
  }
145
389
  }
146
- /**
147
- * The JSON.parse reviver of an element whose big integers were rewritten as exact sentinels.
148
- * @param _key - the member key (unused)
149
- * @param value - the parsed value
150
- * @returns an ExactInteger for an exact sentinel, the value otherwise
151
- */
152
- function reviveExact(_key, value) {
153
- if (typeof value === "string" && value.startsWith(EXACT_SENTINEL)) {
154
- return new ExactInteger(value.slice(EXACT_SENTINEL.length));
155
- }
156
- return value;
157
- }
158
390
  /**
159
391
  * Parse one JSON value, keeping integer literals beyond 2^53 as ExactInteger.
160
392
  * @param text - the JSON text
161
393
  * @returns the value and whether it may hold an ExactInteger; SyntaxError when it is not JSON
394
+ * @category Plugin helpers
162
395
  */
163
396
  export function parseExact(text) {
164
397
  if (!MAYBE_UNSAFE_INTEGER.test(text)) {
@@ -168,7 +401,7 @@ export function parseExact(text) {
168
401
  if (scan.bigIntegers.length === 0) {
169
402
  return { value: JSON.parse(text), exact: false };
170
403
  }
171
- return { value: JSON.parse(scan.text, reviveExact), exact: true };
404
+ return { value: JSON.parse(scan.text, scan.reviveExact), exact: true };
172
405
  }
173
406
  /**
174
407
  * A parsed value with every ExactInteger replaced by its nearest number, for metadata and for
@@ -176,6 +409,7 @@ export function parseExact(text) {
176
409
  * @param value - the value
177
410
  * @param onPrecision - called with the digits of each integer that lost precision
178
411
  * @returns the plain JSON value
412
+ * @category Plugin helpers
179
413
  */
180
414
  export function plainJson(value, onPrecision) {
181
415
  if (value instanceof ExactInteger) {
@@ -188,14 +422,208 @@ export function plainJson(value, onPrecision) {
188
422
  if (typeof value === "object" && value !== null) {
189
423
  const out = {};
190
424
  for (const [key, item] of Object.entries(value)) {
191
- out[key] = plainJson(item, onPrecision);
425
+ setOwn(out, key, plainJson(item, onPrecision));
192
426
  }
193
427
  return out;
194
428
  }
195
429
  return value;
196
430
  }
431
+ /**
432
+ * Set an own property, also for the key `__proto__` (which an assignment would turn into the
433
+ * object's prototype, losing the entry).
434
+ * @param target - the object
435
+ * @param key - the key
436
+ * @param value - the value
437
+ * @category Plugin helpers
438
+ */
439
+ export function setOwn(target, key, value) {
440
+ if (key === "__proto__") {
441
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
442
+ }
443
+ else {
444
+ target[key] = value;
445
+ }
446
+ }
447
+ /**
448
+ * Parse a text with the bare tokens NaN / Infinity / -Infinity that Python's json writes.
449
+ * @param text - the JSON text that JSON.parse refused
450
+ * @returns the value, whether it may hold an ExactInteger and the tokens seen; null when the
451
+ * tokens are not what made the text invalid
452
+ */
453
+ function parseNonstandard(text) {
454
+ if (!/NaN|Infinity/.test(text)) {
455
+ return null;
456
+ }
457
+ const scan = rewriteNumbers(text, { nonstandard: true, exactSentinel: true });
458
+ if (scan.tokens.size === 0) {
459
+ return null;
460
+ }
461
+ try {
462
+ const value = JSON.parse(scan.text, (key, raw) => scan.reviveExact(key, scan.revive(key, raw)));
463
+ return { value, exact: scan.bigIntegers.length > 0, tokens: scan.tokens };
464
+ }
465
+ catch {
466
+ return null;
467
+ }
468
+ }
469
+ /** A lone surrogate code unit: a high one not followed by a low one, or a low one not after a high one. */
470
+ /** The escapes of the surrogate range, as a message writes them. */
471
+ const SURROGATE_ESCAPES = String.raw `\uD800-\uDFFF`;
472
+ const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
473
+ /** Text that may hold a surrogate: an escaped one, or a raw one (string input). */
474
+ const MAYBE_SURROGATE = /\\u[dD][89a-fA-F]|[\ud800-\udfff]/;
475
+ /**
476
+ * Replace every lone surrogate of a parsed value's strings and keys with U+FFFD, in place.
477
+ * @param value - the parsed value
478
+ * @param fixed - counts the strings repaired and names the keys two repaired keys collapse into
479
+ * @param fixed.count - the count
480
+ * @param fixed.collisions - a key one object holds twice once repaired (the later value is kept)
481
+ * @returns the value (a string, or an object with a repaired key, is returned as a new value)
482
+ */
483
+ function repairSurrogates(value, fixed) {
484
+ if (typeof value === "string") {
485
+ const repaired = value.replaceAll(LONE_SURROGATE, "\ufffd");
486
+ if (repaired !== value) {
487
+ fixed.count++;
488
+ }
489
+ return repaired;
490
+ }
491
+ if (Array.isArray(value)) {
492
+ for (let i = 0; i < value.length; i++) {
493
+ value[i] = repairSurrogates(value[i], fixed);
494
+ }
495
+ return value;
496
+ }
497
+ if (isRecord(value)) {
498
+ const keys = Object.keys(value);
499
+ const repairedKeys = keys.map((key) => key.replaceAll(LONE_SURROGATE, "\ufffd"));
500
+ const renamed = repairedKeys.some((key, i) => key !== keys[i]);
501
+ const out = renamed ? {} : value;
502
+ keys.forEach((key, i) => {
503
+ if (repairedKeys[i] !== key) {
504
+ fixed.count++;
505
+ }
506
+ if (renamed && Object.hasOwn(out, repairedKeys[i])) {
507
+ fixed.collisions.push(repairedKeys[i]);
508
+ }
509
+ setOwn(out, repairedKeys[i], repairSurrogates(value[key], fixed));
510
+ });
511
+ return out;
512
+ }
513
+ return value;
514
+ }
515
+ /**
516
+ * The number of keys of a parsed value, at every depth.
517
+ * @param value - the value
518
+ * @returns the count
519
+ */
520
+ function keyCount(value) {
521
+ if (Array.isArray(value)) {
522
+ return value.reduce((n, item) => n + keyCount(item), 0);
523
+ }
524
+ if (isRecord(value)) {
525
+ return Object.keys(value).reduce((n, key) => n + 1 + keyCount(value[key]), 0);
526
+ }
527
+ return 0;
528
+ }
529
+ /**
530
+ * The first key an object of a JSON text holds twice (JSON.parse keeps the later value silently).
531
+ * @param text - valid JSON text
532
+ * @returns the key, or null
533
+ */
534
+ function duplicateKey(text) {
535
+ const stack = [];
536
+ let i = 0;
537
+ while (i < text.length) {
538
+ const c = text[i];
539
+ if (c === "{") {
540
+ stack.push(new Set());
541
+ }
542
+ else if (c === "[") {
543
+ stack.push(null);
544
+ }
545
+ else if (c === "}" || c === "]") {
546
+ stack.pop();
547
+ }
548
+ else if (c === '"') {
549
+ const start = i;
550
+ i = stringEnd(text, i + 1);
551
+ const keys = stack.at(-1) ?? null;
552
+ if (keys !== null && text[skipSpace(text, i + 1)] === ":") {
553
+ const key = JSON.parse(text.slice(start, i + 1));
554
+ if (keys.has(key)) {
555
+ return key;
556
+ }
557
+ keys.add(key);
558
+ }
559
+ }
560
+ i++;
561
+ }
562
+ return null;
563
+ }
564
+ /**
565
+ * The offset of the quote that ends a JSON string, skipping escaped characters.
566
+ * @param text - the text
567
+ * @param from - the offset after the opening quote
568
+ * @returns the closing quote's offset, or the text length (or one past it) when there is none
569
+ */
570
+ function stringEnd(text, from) {
571
+ let i = from;
572
+ while (i < text.length && text[i] !== '"') {
573
+ i += text[i] === "\\" ? 2 : 1;
574
+ }
575
+ return i;
576
+ }
577
+ /**
578
+ * The index of the first character at or after `from` that is not JSON whitespace.
579
+ * @param text - the text
580
+ * @param from - the first index
581
+ * @returns the index (text.length at the end)
582
+ */
583
+ function skipSpace(text, from) {
584
+ let i = from;
585
+ while (i < text.length && isSpace(text.codePointAt(i))) {
586
+ i++;
587
+ }
588
+ return i;
589
+ }
590
+ /** A key followed by its colon, for the cheap duplicate-key check. */
591
+ const KEY_COLON = /"\s*:/g;
592
+ /**
593
+ * The checks of a parsed element that JSON.parse cannot make, recorded in the report: lone
594
+ * surrogates (replaced with U+FFFD, E_BAD_VALUE) and a key held twice by one object
595
+ * (W_DUPLICATE_ATTRIBUTE; the later value is the one read, also for two keys that a repair makes one).
596
+ * @param text - the element text
597
+ * @param parsed - the parsed element
598
+ * @param line - its line
599
+ * @param report - the report
600
+ * @returns the value, repaired
601
+ */
602
+ function checkParsed(text, parsed, line, report) {
603
+ let value = parsed;
604
+ if (MAYBE_SURROGATE.test(text)) {
605
+ const fixed = { count: 0, collisions: [] };
606
+ value = repairSurrogates(value, fixed);
607
+ for (const key of fixed.collisions) {
608
+ report.warnOnce("validation-error", DUPLICATE_ATTRIBUTE_CODE, `an object holds two keys that are the same key ${JSON.stringify(key)} once their lone surrogates are replaced with U+FFFD; the later value is read`, { line, element: key }, `${DUPLICATE_ATTRIBUTE_CODE}:json:${key}`);
609
+ }
610
+ if (fixed.count > 0) {
611
+ report.error("validation-error", BAD_VALUE_CODE, `${fixed.count} string${plural(fixed.count)} ${agree(fixed.count, "holds", "hold")} a lone surrogate (an unpaired ${SURROGATE_ESCAPES} escape); each is read with U+FFFD in its place`, { line });
612
+ }
613
+ }
614
+ if (typeof value === "object" && value !== null && (text.match(KEY_COLON)?.length ?? 0) > keyCount(value)) {
615
+ const key = duplicateKey(text);
616
+ if (key !== null) {
617
+ report.warnOnce("validation-error", DUPLICATE_ATTRIBUTE_CODE, `an object holds the key ${JSON.stringify(key)} twice; the later value is read`, { line, element: key }, `${DUPLICATE_ATTRIBUTE_CODE}:json:${key}`);
618
+ }
619
+ }
620
+ return value;
621
+ }
197
622
  // ============================================================ the streaming aspect scanner
198
- /** A problem with the document's JSON; the importer turns it into a fatal E_SYNTAX or E_EMPTY_INPUT. */
623
+ /**
624
+ * A problem with the document's JSON; the importer turns it into a fatal E_SYNTAX or E_EMPTY_INPUT.
625
+ * @category Plugin helpers
626
+ */
199
627
  export class JsonScanError extends Error {
200
628
  /**
201
629
  * Create the error.
@@ -260,6 +688,7 @@ class ChunkCursor {
260
688
  this.captureStart = 0;
261
689
  }
262
690
  this.linesBefore += countNewlines(this.buf, 0, this.buf.length);
691
+ const endsWithCr = this.buf.endsWith("\r");
263
692
  for (;;) {
264
693
  const next = await this.iterator.next();
265
694
  if (next.done === true) {
@@ -268,6 +697,10 @@ class ChunkCursor {
268
697
  return false;
269
698
  }
270
699
  if (next.value.length > 0) {
700
+ if (endsWithCr && next.value.startsWith("\n")) {
701
+ // a CR LF split across two chunks is one line break, counted with the CR
702
+ this.linesBefore--;
703
+ }
271
704
  this.buf = next.value;
272
705
  this.pos = 0;
273
706
  return true;
@@ -281,6 +714,14 @@ class ChunkCursor {
281
714
  line() {
282
715
  return this.linesBefore + countNewlines(this.buf, 0, Math.min(this.pos, this.buf.length));
283
716
  }
717
+ /**
718
+ * The text from the read position to the end of the current chunk, at most `length` characters.
719
+ * @param length - the most characters
720
+ * @returns the text
721
+ */
722
+ ahead(length) {
723
+ return this.buf.slice(this.pos, this.pos + length);
724
+ }
284
725
  /**
285
726
  * Skip whitespace and return the next character without consuming it.
286
727
  * @returns the character, or "" at the end of the input
@@ -412,7 +853,7 @@ class ChunkCursor {
412
853
  }
413
854
  }
414
855
  /**
415
- * The number of line feeds in a slice of a string.
856
+ * The number of line breaks in a slice of a string: LF, CR LF and a CR alone (old Mac files).
416
857
  * @param text - the string
417
858
  * @param from - the first index
418
859
  * @param to - one past the last index
@@ -425,6 +866,13 @@ function countNewlines(text, from, to) {
425
866
  count++;
426
867
  i = text.indexOf("\n", i + 1);
427
868
  }
869
+ i = text.indexOf("\r", from);
870
+ while (i >= 0 && i < to) {
871
+ if (text[i + 1] !== "\n") {
872
+ count++;
873
+ }
874
+ i = text.indexOf("\r", i + 1);
875
+ }
428
876
  return count;
429
877
  }
430
878
  /**
@@ -436,65 +884,125 @@ function messageOf(err) {
436
884
  return err instanceof Error ? err.message : String(err);
437
885
  }
438
886
  /**
439
- * Parse a captured value, turning a parse failure into a JsonScanError at its line.
887
+ * Parse a captured value, turning a parse failure into a JsonScanError at its line. With a report,
888
+ * the bare tokens NaN / Infinity / -Infinity are read as numbers (W_JSON_NONSTANDARD_NUMBER), and
889
+ * lone surrogates and repeated keys are recorded (checkParsed()).
440
890
  * @param text - the value text
441
891
  * @param line - the line the value starts on
892
+ * @param report - the report, or null to only parse
442
893
  * @returns the value and whether it may hold an ExactInteger
443
894
  */
444
- function parseAt(text, line) {
895
+ function parseAt(text, line, report) {
896
+ let parsed;
445
897
  try {
446
- return parseExact(text);
898
+ parsed = parseExact(text);
447
899
  }
448
900
  catch (err) {
449
- throw new JsonScanError(`invalid JSON: ${messageOf(err)}`, line);
901
+ const recovered = report === null ? null : parseNonstandard(text);
902
+ if (recovered === null) {
903
+ throw new JsonScanError(`invalid JSON: ${messageOf(err)}`, line);
904
+ }
905
+ report?.warnOnce("coercion", JSON_NONSTANDARD_NUMBER_CODE, `the document uses the non-standard token${plural(recovered.tokens.size)} ${[...recovered.tokens].join(", ")}, which strict JSON does not allow; read as numbers`, { line });
906
+ parsed = recovered;
907
+ }
908
+ if (report !== null) {
909
+ parsed = { ...parsed, value: checkParsed(text, parsed.value, line, report) };
910
+ }
911
+ return parsed;
912
+ }
913
+ /**
914
+ * What a document that does not start with "[" looks like, for the syntax error: HTML or XML (an
915
+ * error page saved under the file's name), a ZIP archive, or nothing in particular.
916
+ * @param head - the first characters
917
+ * @returns a sentence to append, or ""
918
+ */
919
+ function notJsonHint(head) {
920
+ if (head.startsWith("<")) {
921
+ return "; the input looks like HTML or XML (an error page saved as the file?), not JSON";
922
+ }
923
+ if (head.startsWith("PK\u0003\u0004")) {
924
+ return "; the input is a ZIP archive (a Cytoscape session .cys is read by the cys importer), not JSON";
450
925
  }
926
+ return "";
451
927
  }
452
928
  /**
453
929
  * Walk a CX document (CX1 or CX2): a top-level array of members, each normally a one-key object
454
930
  * `{"<aspect>": [elements]}`. The elements of such a block are parsed and yielded one at a time;
455
931
  * any other member is parsed whole and yielded as a "member". A document that is not an array is
456
- * yielded as one "root" event. Syntax errors throw JsonScanError.
932
+ * yielded as one "root" event (an object without reading it). Syntax errors throw JsonScanError.
457
933
  * @param chunks - the decoded text
934
+ * @param report - where NaN tokens, lone surrogates and repeated keys are recorded (parseAt()); null to only parse
458
935
  * @yields the events in document order
459
936
  * @returns nothing
460
937
  */
461
- export async function* scanAspects(chunks) {
938
+ export async function* scanAspects(chunks, report = null) {
462
939
  const cur = new ChunkCursor(chunks);
463
940
  const first = await cur.peek();
464
941
  if (first === "") {
465
942
  throw new JsonScanError("the input is empty", 1, true);
466
943
  }
467
- if (first !== "[") {
468
- const line = cur.line();
469
- cur.beginCapture();
470
- await cur.skipValue();
471
- const { value } = parseAt(cur.endCapture(), line);
472
- if ((await cur.peek()) !== "") {
473
- throw new JsonScanError("unexpected text after the document", cur.line());
474
- }
475
- yield { kind: "root", value, line };
944
+ const hint = notJsonHint(cur.ahead(16));
945
+ if (hint === "" && cur.ahead(8).includes(String.fromCodePoint(0))) {
946
+ throw new JsonScanError('the input holds NUL characters: UTF-16 without a byte order mark (pass the encoding option, such as "utf-16le"), or binary data; not JSON', cur.line());
947
+ }
948
+ if (first === "[") {
949
+ cur.pos++;
950
+ yield* members(cur, report);
476
951
  return;
477
952
  }
478
- cur.pos++;
953
+ const line = cur.line();
954
+ if (first === "{") {
955
+ yield { kind: "root", value: undefined, object: true, line };
956
+ return;
957
+ }
958
+ yield { kind: "root", value: await scalarRoot(cur, line, hint), object: false, line };
959
+ }
960
+ /**
961
+ * Read a document that is neither an array nor an object: one value, and nothing after it.
962
+ * @param cur - the cursor at the value
963
+ * @param line - the value's line
964
+ * @param hint - what the input looks like when it is not JSON, for the error
965
+ * @returns the value
966
+ */
967
+ async function scalarRoot(cur, line, hint) {
968
+ cur.beginCapture();
969
+ await cur.skipValue();
970
+ let value;
971
+ try {
972
+ ({ value } = parseAt(cur.endCapture(), line, null));
973
+ }
974
+ catch (err) {
975
+ if (err instanceof JsonScanError && hint !== "") {
976
+ throw new JsonScanError(`${err.message}${hint}`, err.line);
977
+ }
978
+ throw err;
979
+ }
980
+ if ((await cur.peek()) !== "") {
981
+ throw new JsonScanError("unexpected text after the document", cur.line());
982
+ }
983
+ return value;
984
+ }
985
+ /**
986
+ * Read the members of the top-level array, and check that nothing follows it.
987
+ * @param cur - the cursor after the opening bracket
988
+ * @param report - the report of parseAt(), or null
989
+ * @yields the members' events
990
+ * @returns nothing
991
+ */
992
+ async function* members(cur, report) {
479
993
  let block = 0;
480
- if ((await cur.peek()) === "]") {
994
+ let next = await cur.peek();
995
+ if (next === "]") {
481
996
  cur.pos++;
482
997
  }
483
- else {
484
- for (;;) {
485
- yield* member(cur, block);
486
- block++;
487
- const next = await cur.peek();
488
- if (next === ",") {
489
- cur.pos++;
490
- continue;
491
- }
492
- if (next === "]") {
493
- cur.pos++;
494
- break;
495
- }
998
+ while (next !== "]") {
999
+ yield* member(cur, block, report);
1000
+ block++;
1001
+ next = await cur.peek();
1002
+ if (next !== "," && next !== "]") {
496
1003
  throw new JsonScanError(next === "" ? "the document ends before its closing bracket" : `expected "," or "]", found "${next}"`, cur.line());
497
1004
  }
1005
+ cur.pos++;
498
1006
  }
499
1007
  if ((await cur.peek()) !== "") {
500
1008
  throw new JsonScanError("unexpected text after the closing bracket", cur.line());
@@ -504,151 +1012,247 @@ export async function* scanAspects(chunks) {
504
1012
  * Read one member of the top-level array.
505
1013
  * @param cur - the cursor at the member's first character
506
1014
  * @param block - the member's position among the members
1015
+ * @param report - the report of parseAt(), or null
507
1016
  * @yields the member's events
508
1017
  * @returns nothing
509
1018
  */
510
- async function* member(cur, block) {
1019
+ async function* member(cur, block, report) {
511
1020
  const c = await cur.peek();
512
1021
  const line = cur.line();
513
1022
  if (c === "" || c === "]" || c === ",") {
514
1023
  throw new JsonScanError(c === "" ? "the document ends inside its array" : `unexpected "${c}"`, line);
515
1024
  }
1025
+ cur.beginCapture();
516
1026
  if (c !== "{") {
517
- cur.beginCapture();
518
1027
  await cur.skipValue();
519
- if (cur.deepest > MAX_ELEMENT_DEPTH) {
520
- cur.dropCapture();
521
- yield { kind: "deep", aspect: null, block, depth: cur.deepest, line };
522
- return;
523
- }
524
- const parsed = parseAt(cur.endCapture(), line);
525
- yield { kind: "member", block, value: parsed.value, exact: parsed.exact, line };
1028
+ yield wholeMember(cur, block, line, report);
526
1029
  return;
527
1030
  }
528
- cur.beginCapture();
529
1031
  cur.pos++;
530
- const k = await cur.peek();
531
- if (k !== '"') {
1032
+ if ((await cur.peek()) !== '"') {
532
1033
  // `{}` or not JSON: parse the whole member so a syntax error is reported as such
533
1034
  await cur.skipValue(1);
534
- if (cur.deepest > MAX_ELEMENT_DEPTH) {
535
- cur.dropCapture();
536
- yield { kind: "deep", aspect: null, block, depth: cur.deepest, line };
537
- return;
538
- }
539
- const parsed = parseAt(cur.endCapture(), line);
540
- yield { kind: "member", block, value: parsed.value, exact: parsed.exact, line };
1035
+ yield wholeMember(cur, block, line, report);
541
1036
  return;
542
1037
  }
543
- // the outer capture keeps the member's text in case it is not a block; readString nests its own
1038
+ // every array-valued key is a block whose elements stream, whatever the key order; any other
1039
+ // value is kept as text, so a member without an array is parsed whole (a CX2 descriptor, a
1040
+ // one-object aspect) and one with an array names its other keys (extraKeys), skipped
544
1041
  const outer = cur.endCapture();
545
- const keyLine = cur.line();
546
- const aspect = await cur.readString();
1042
+ const firstLine = cur.line();
1043
+ const keys = { others: [], firstBlock: null, deep: 0 };
1044
+ let next = ",";
1045
+ let index = 0;
1046
+ while (next === ",") {
1047
+ const key = await readKey(cur);
1048
+ yield* keyValue(cur, key, index, block, keys, report);
1049
+ index++;
1050
+ next = await cur.peek();
1051
+ if (next !== "," && next !== "}") {
1052
+ throw new JsonScanError(next === "" ? "the document ends inside a member" : `expected "," or "}" after the key "${key.name}"`, cur.line());
1053
+ }
1054
+ cur.pos++;
1055
+ }
1056
+ yield* memberEnd(keys, block, line, { outer, firstLine }, report);
1057
+ }
1058
+ /**
1059
+ * The event of a member read whole (its capture already taken): the member, or "deep" when it
1060
+ * nests too deep to parse.
1061
+ * @param cur - the cursor after the member
1062
+ * @param block - the member's position among the members
1063
+ * @param line - the member's line
1064
+ * @param report - the report of parseAt(), or null
1065
+ * @returns the event
1066
+ */
1067
+ function wholeMember(cur, block, line, report) {
1068
+ if (cur.deepest > MAX_ELEMENT_DEPTH) {
1069
+ cur.dropCapture();
1070
+ return { kind: "deep", aspect: null, block, depth: cur.deepest, line };
1071
+ }
1072
+ const text = cur.endCapture();
1073
+ const parsed = parseAt(text, line, report);
1074
+ return { kind: "member", block, value: parsed.value, exact: parsed.exact, text, line };
1075
+ }
1076
+ /**
1077
+ * Read a member's key and the colon after it.
1078
+ * @param cur - the cursor at the key
1079
+ * @returns the key and its line
1080
+ */
1081
+ async function readKey(cur) {
1082
+ if ((await cur.peek()) !== '"') {
1083
+ throw new JsonScanError("expected a key", cur.line());
1084
+ }
1085
+ const line = cur.line();
1086
+ const name = await cur.readString();
547
1087
  if ((await cur.peek()) !== ":") {
548
- throw new JsonScanError(`expected ":" after the key "${aspect}"`, cur.line());
1088
+ throw new JsonScanError(`expected ":" after the key "${name}"`, cur.line());
549
1089
  }
550
1090
  cur.pos++;
551
- const v = await cur.peek();
552
- if (v !== "[") {
553
- // not a block: read the rest of the object and parse the member whole
554
- cur.beginCapture();
555
- await cur.skipValue(1);
556
- if (cur.deepest > MAX_ELEMENT_DEPTH) {
557
- cur.dropCapture();
558
- yield { kind: "deep", aspect, block, depth: cur.deepest, line };
559
- return;
560
- }
561
- const rest = cur.endCapture();
562
- const text = `${outer}${JSON.stringify(aspect)}:${rest}`;
563
- const parsed = parseAt(text, keyLine);
564
- yield { kind: "member", block, value: parsed.value, exact: parsed.exact, line };
1091
+ return { name, line };
1092
+ }
1093
+ /**
1094
+ * Read one key's value: an array is a block whose elements stream; any other value is kept as text
1095
+ * (or, when it nests too deep to parse, only its depth).
1096
+ * @param cur - the cursor at the value
1097
+ * @param key - the key
1098
+ * @param key.name - its name
1099
+ * @param key.line - its line
1100
+ * @param index - the key's position in the member
1101
+ * @param block - the member's position among the members
1102
+ * @param keys - what the member's keys held so far
1103
+ * @param report - the report of parseAt(), or null
1104
+ * @yields the block's events
1105
+ * @returns nothing
1106
+ */
1107
+ async function* keyValue(cur, key, index, block, keys, report) {
1108
+ if ((await cur.peek()) === "[") {
1109
+ cur.pos++;
1110
+ yield { kind: "block", aspect: key.name, block, shared: index > 0, line: key.line };
1111
+ keys.firstBlock ?? (keys.firstBlock = key.name);
1112
+ yield* blockElements(cur, key.name, block, report);
565
1113
  return;
566
1114
  }
567
- cur.pos++;
568
- yield { kind: "block", aspect, block, line };
569
- if ((await cur.peek()) === "]") {
570
- cur.pos++;
1115
+ cur.beginCapture();
1116
+ await cur.skipValue();
1117
+ // the member's own brace is one level more
1118
+ if (cur.deepest + 1 > MAX_ELEMENT_DEPTH) {
1119
+ // never parse (recursively) what no one reads
1120
+ cur.dropCapture();
1121
+ keys.deep = Math.max(keys.deep, cur.deepest + 1);
1122
+ keys.others.push({ key: key.name, text: null, line: key.line });
571
1123
  }
572
1124
  else {
573
- for (;;) {
574
- const e = await cur.peek();
575
- if (e === "") {
576
- throw new JsonScanError(`the document ends inside the "${aspect}" block`, cur.line());
577
- }
578
- const elementLine = cur.line();
579
- cur.beginCapture();
580
- await cur.skipValue();
581
- if (cur.deepest > MAX_ELEMENT_DEPTH) {
582
- cur.dropCapture();
583
- yield { kind: "deep", aspect, block, depth: cur.deepest, line: elementLine };
584
- }
585
- else {
586
- const text = cur.endCapture();
587
- const parsed = parseAt(text, elementLine);
588
- yield {
589
- kind: "element",
590
- aspect,
591
- block,
592
- value: parsed.value,
593
- exact: parsed.exact,
594
- text,
595
- line: elementLine,
596
- };
597
- }
598
- const next = await cur.peek();
599
- if (next === ",") {
600
- cur.pos++;
601
- continue;
602
- }
603
- if (next === "]") {
604
- cur.pos++;
605
- break;
1125
+ keys.others.push({ key: key.name, text: cur.endCapture(), line: key.line });
1126
+ }
1127
+ }
1128
+ /**
1129
+ * The events after a one-object member's keys: with a block, its other keys (checked, then named
1130
+ * as extraKeys); without one, the member parsed whole from its kept text.
1131
+ * @param keys - what the member's keys held
1132
+ * @param block - the member's position among the members
1133
+ * @param line - the member's line
1134
+ * @param start - the member's opening text and the line after it
1135
+ * @param start.outer - the text up to the first key
1136
+ * @param start.firstLine - the line of the first key
1137
+ * @param report - the report of parseAt(), or null
1138
+ * @yields the events
1139
+ * @returns nothing
1140
+ */
1141
+ function* memberEnd(keys, block, line, start, report) {
1142
+ const { others, firstBlock, deep } = keys;
1143
+ if (firstBlock !== null) {
1144
+ for (const other of others) {
1145
+ if (other.text !== null) {
1146
+ parseAt(other.text, other.line, null);
606
1147
  }
607
- throw new JsonScanError(next === ""
608
- ? `the document ends inside the "${aspect}" block`
609
- : `expected "," or "]" in the "${aspect}" block, found "${next}"`, cur.line());
610
1148
  }
611
- }
612
- const end = await cur.peek();
613
- if (end === "}") {
614
- cur.pos++;
1149
+ if (others.length > 0) {
1150
+ yield {
1151
+ kind: "extraKeys",
1152
+ aspect: firstBlock,
1153
+ block,
1154
+ keys: others.map((o) => o.key),
1155
+ line: others[0].line,
1156
+ };
1157
+ }
615
1158
  return;
616
1159
  }
617
- if (end !== ",") {
618
- throw new JsonScanError(end === "" ? "the document ends inside a member" : `expected "}" after the "${aspect}" block`, cur.line());
1160
+ if (deep > 0) {
1161
+ yield { kind: "deep", aspect: others[0].key, block, depth: deep, line };
1162
+ return;
619
1163
  }
620
- // more keys: a malformed block; skip their values and name them
621
- const keys = [];
622
- const extraLine = cur.line();
623
- while ((await cur.peek()) === ",") {
1164
+ const members = others.map((o) => [JSON.stringify(o.key), o.text ?? ""].join(":"));
1165
+ const text = `${start.outer}${members.join(",")}}`;
1166
+ const parsed = parseAt(text, start.firstLine, report);
1167
+ yield { kind: "member", block, value: parsed.value, exact: parsed.exact, text, line };
1168
+ }
1169
+ /**
1170
+ * Read the elements of a block whose opening bracket was consumed, through its closing bracket.
1171
+ * @param cur - the cursor after the opening bracket
1172
+ * @param aspect - the block's aspect
1173
+ * @param block - the member's position among the members
1174
+ * @param report - the report of parseAt(), or null
1175
+ * @yields an element (or deep) event per element
1176
+ * @returns nothing
1177
+ */
1178
+ async function* blockElements(cur, aspect, block, report) {
1179
+ if ((await cur.peek()) === "]") {
624
1180
  cur.pos++;
625
- if ((await cur.peek()) !== '"') {
626
- throw new JsonScanError("expected a key", cur.line());
627
- }
628
- keys.push(await cur.readString());
629
- if ((await cur.peek()) !== ":") {
630
- throw new JsonScanError('expected ":"', cur.line());
1181
+ return;
1182
+ }
1183
+ for (;;) {
1184
+ const e = await cur.peek();
1185
+ if (e === "") {
1186
+ throw new JsonScanError(`the document ends inside the "${aspect}" block`, cur.line());
631
1187
  }
632
- cur.pos++;
633
- await cur.peek();
1188
+ const elementLine = cur.line();
634
1189
  cur.beginCapture();
635
1190
  await cur.skipValue();
636
- parseAt(cur.endCapture(), cur.line());
637
- }
638
- if ((await cur.peek()) !== "}") {
639
- throw new JsonScanError('expected "}"', cur.line());
1191
+ if (cur.deepest > MAX_ELEMENT_DEPTH) {
1192
+ cur.dropCapture();
1193
+ yield { kind: "deep", aspect, block, depth: cur.deepest, line: elementLine };
1194
+ }
1195
+ else {
1196
+ const text = cur.endCapture();
1197
+ const parsed = parseAt(text, elementLine, report);
1198
+ yield {
1199
+ kind: "element",
1200
+ aspect,
1201
+ block,
1202
+ value: parsed.value,
1203
+ exact: parsed.exact,
1204
+ text,
1205
+ line: elementLine,
1206
+ };
1207
+ }
1208
+ const next = await cur.peek();
1209
+ if (next === ",") {
1210
+ cur.pos++;
1211
+ continue;
1212
+ }
1213
+ if (next === "]") {
1214
+ cur.pos++;
1215
+ return;
1216
+ }
1217
+ throw new JsonScanError(next === ""
1218
+ ? `the document ends inside the "${aspect}" block`
1219
+ : `expected "," or "]" in the "${aspect}" block, found "${next}"`, cur.line());
640
1220
  }
641
- cur.pos++;
642
- yield { kind: "extraKeys", aspect, block, keys, line: extraLine };
643
1221
  }
644
1222
  /**
645
1223
  * Report an element or member the scanner refused for its depth (E_BAD_ASPECT_BLOCK; skipped).
646
1224
  * @param report - the report
647
1225
  * @param event - the "deep" event
1226
+ * @category Plugin helpers
648
1227
  */
649
1228
  export function reportTooDeep(report, event) {
650
1229
  report.error("parse-error", BAD_ASPECT_BLOCK_CODE, `${event.aspect === null ? "a member of the document" : `an element of "${event.aspect}"`} is nested ${event.depth} levels deep, more than the ${MAX_ELEMENT_DEPTH} a graph keeps; skipped`, { line: event.line, element: event.aspect });
651
1230
  }
1231
+ /**
1232
+ * The precision callback of plainJson() for an aspect kept verbatim in the metadata: W_PRECISION
1233
+ * once, since the digits stored there are not the file's.
1234
+ * @param report - the report
1235
+ * @param where - where the aspect is kept (meta.extra.cx, ...)
1236
+ * @returns the callback
1237
+ * @category Plugin helpers
1238
+ */
1239
+ export function keptPrecision(report, where) {
1240
+ return (digits) => {
1241
+ report.warnOnce("precision", PRECISION_CODE, `an aspect kept in ${where} holds ${digits}, beyond 2^53; it is kept as the nearest double`, { element: where }, `${PRECISION_CODE}:kept`);
1242
+ };
1243
+ }
1244
+ /**
1245
+ * Warn once (W_MULTI_ASPECT_FRAGMENT) about a block that shares its array member with another
1246
+ * aspect (`{"nodes": [...], "edges": [...]}`); a block of its own needs nothing.
1247
+ * @param report - the report
1248
+ * @param event - the "block" event
1249
+ * @category Plugin helpers
1250
+ */
1251
+ export function reportSharedBlock(report, event) {
1252
+ if (event.shared) {
1253
+ report.warnOnce("coercion", MULTI_ASPECT_FRAGMENT_CODE, `the "${event.aspect}" aspect shares an array member with another aspect; each is read as its own fragment`, { line: event.line, element: event.aspect });
1254
+ }
1255
+ }
652
1256
  const DECIMAL_INTEGER_TEXT = /^-?(0|[1-9][0-9]*)$/;
653
1257
  /**
654
1258
  * Apply the CX id rule (design section 1.0.2) to a parsed value: a safe integer is the id; an
@@ -657,6 +1261,7 @@ const DECIMAL_INTEGER_TEXT = /^-?(0|[1-9][0-9]*)$/;
657
1261
  * @param raw - the parsed value
658
1262
  * @param inexactLiteral - whether the number was written as a non-integer literal (`5.0`, `1e3`)
659
1263
  * @returns the id and how it was spelled
1264
+ * @category Plugin helpers
660
1265
  */
661
1266
  export function cxId(raw, inexactLiteral = false) {
662
1267
  if (raw instanceof ExactInteger) {
@@ -686,18 +1291,24 @@ function invalidCxId(raw, reason) {
686
1291
  reason,
687
1292
  });
688
1293
  }
1294
+ /** A number literal at lastIndex (sticky). */
1295
+ const NUMBER_LITERAL = /-?\d[\d.eE+-]*/y;
689
1296
  /**
690
1297
  * Whether the element text writes a top-level key's number as a non-integer literal (`"id": 5.0`,
691
1298
  * `"@id": 1e3`). A lexical check, so the parsed value (5) cannot tell. Only a key of the element
692
1299
  * itself counts, not one nested in its `v` (where `s` and `id` are ordinary attribute names).
693
1300
  * @param text - the element's JSON text
694
1301
  * @param key - the key
1302
+ * @param keyDepth - the nesting depth of the element's own keys: 1, or 2 for an element written as
1303
+ * a member's single object (`{"nodes": {"@id": 1.0}}`)
695
1304
  * @returns true when the literal holds a fraction or an exponent
1305
+ * @category Plugin helpers
696
1306
  */
697
- export function inexactLiteral(text, key) {
1307
+ export function inexactLiteral(text, key, keyDepth = 1) {
698
1308
  const quoted = JSON.stringify(key);
699
1309
  let depth = 0;
700
- for (let i = 0; i < text.length; i++) {
1310
+ let i = 0;
1311
+ while (i < text.length) {
701
1312
  const c = text[i];
702
1313
  if (c === "{" || c === "[") {
703
1314
  depth++;
@@ -707,22 +1318,33 @@ export function inexactLiteral(text, key) {
707
1318
  }
708
1319
  else if (c === '"') {
709
1320
  const start = i;
710
- for (i++; i < text.length && text[i] !== '"'; i++) {
711
- if (text[i] === "\\") {
712
- i++;
713
- }
714
- }
1321
+ i = stringEnd(text, i + 1);
715
1322
  // the key itself (a string at depth 1 followed by a colon), not a value spelled like it
716
- if (depth === 1 && text.startsWith(quoted, start) && /^\s*:/.test(text.slice(i + 1, i + 16))) {
717
- const match = /^\s*:\s*(-?[0-9][0-9.eE+-]*)/.exec(text.slice(i + 1, i + 64));
718
- return match !== null && /[.eE]/.test(match[1]);
1323
+ const colon = skipSpace(text, i + 1);
1324
+ if (depth === keyDepth && text.startsWith(quoted, start) && text[colon] === ":") {
1325
+ return inexactNumberAt(text, skipSpace(text, colon + 1));
719
1326
  }
720
1327
  }
1328
+ i++;
721
1329
  }
722
1330
  return false;
723
1331
  }
1332
+ /**
1333
+ * Whether a number literal at an offset holds a fraction or an exponent.
1334
+ * @param text - the text
1335
+ * @param at - the offset
1336
+ * @returns true when it does; false for an integer or no number
1337
+ */
1338
+ function inexactNumberAt(text, at) {
1339
+ NUMBER_LITERAL.lastIndex = at;
1340
+ const match = NUMBER_LITERAL.exec(text);
1341
+ return match !== null && /[.eE]/.test(match[0]);
1342
+ }
724
1343
  // ============================================================ Cytoscape positions (design section 1.0.2)
725
- /** The name of the position column of every Cytoscape-family importer. */
1344
+ /**
1345
+ * The name of the position column of every Cytoscape-family importer.
1346
+ * @category Plugin helpers
1347
+ */
726
1348
  export const POSITION_COLUMN = "position";
727
1349
  /** The name of the stacking-order column (Cytoscape's NODE_Z_LOCATION). */
728
1350
  const Z_COLUMN = "z";
@@ -731,6 +1353,7 @@ const Z_COLUMN = "z";
731
1353
  * @param format - the importer's format name
732
1354
  * @param sourceDims - 3 when z goes into the position (zAs "position")
733
1355
  * @returns the declaration
1356
+ * @category Plugin helpers
734
1357
  */
735
1358
  export function positionDecl(format, sourceDims = 2) {
736
1359
  return {
@@ -748,6 +1371,7 @@ export function positionDecl(format, sourceDims = 2) {
748
1371
  * The stacking-order column `z` (f64, origin namespace "cytoscape").
749
1372
  * @param format - the importer's format name
750
1373
  * @returns the declaration
1374
+ * @category Plugin helpers
751
1375
  */
752
1376
  export function zDecl(format) {
753
1377
  return {
@@ -758,11 +1382,39 @@ export function zDecl(format) {
758
1382
  extra: { cytoscape: "z" },
759
1383
  };
760
1384
  }
1385
+ /**
1386
+ * Whether a coordinate fits the f32 position column: finite once rounded to f32 (1e39 and the
1387
+ * Infinity of a literal like 1e400 do not, nor does NaN).
1388
+ * @param value - the coordinate
1389
+ * @returns true when the column can hold it
1390
+ * @category Plugin helpers
1391
+ */
1392
+ export function fitsF32(value) {
1393
+ return Number.isFinite(Math.fround(value));
1394
+ }
1395
+ /**
1396
+ * The head of an input as text for a sniff: UTF-16 by its byte order mark, else UTF-8, never
1397
+ * failing, without the BOM.
1398
+ * @param head - the first bytes
1399
+ * @returns the text
1400
+ * @category Plugin helpers
1401
+ */
1402
+ export function headText(head) {
1403
+ let encoding = "utf-8";
1404
+ if (head.byteLength >= 2 && head[0] === 0xff && head[1] === 0xfe) {
1405
+ encoding = "utf-16le";
1406
+ }
1407
+ else if (head.byteLength >= 2 && head[0] === 0xfe && head[1] === 0xff) {
1408
+ encoding = "utf-16be";
1409
+ }
1410
+ return new TextDecoder(encoding).decode(head).replace(/^\uFEFF/, "");
1411
+ }
761
1412
  /**
762
1413
  * Cytoscape's screen y (growing downward) as graph-io's y (growing upward), and back: a negation
763
1414
  * that never yields -0.
764
1415
  * @param y - the coordinate
765
1416
  * @returns the flipped coordinate
1417
+ * @category Plugin helpers
766
1418
  */
767
1419
  export function flipY(y) {
768
1420
  return y === 0 ? 0 : -y;
@@ -775,6 +1427,7 @@ const STRUCTURE_ASPECTS = new Set(["metaData", "status", "numberVerification"]);
775
1427
  * `metaData` or an aspect after the post-metadata or after `status` is W_ASPECT_ORDER; declared
776
1428
  * element counts are compared with what was read (W_COUNT_MISMATCH); `status.success: false` is
777
1429
  * fatal (E_STATUS_FAILED) and `success: true` with an error text a warning (W_STATUS_WARNING).
1430
+ * @category Plugin helpers
778
1431
  */
779
1432
  export class CxStructure {
780
1433
  /**
@@ -787,8 +1440,9 @@ export class CxStructure {
787
1440
  /**
788
1441
  * Create the checker.
789
1442
  * @param report - the report
1443
+ * @param canonical - the name an aspect named in metaData is counted under (default: itself)
790
1444
  */
791
- constructor(report) {
1445
+ constructor(report, canonical = (name) => name) {
792
1446
  /** Elements read per aspect name. */
793
1447
  this.counts = new Map();
794
1448
  /** Blocks read per aspect name. */
@@ -802,6 +1456,7 @@ export class CxStructure {
802
1456
  /** The status element, when one was read. */
803
1457
  this.status = undefined;
804
1458
  this.report = report;
1459
+ this.canonical = canonical;
805
1460
  }
806
1461
  /**
807
1462
  * Record the start of a block (or a member holding one aspect).
@@ -844,15 +1499,38 @@ export class CxStructure {
844
1499
  element(aspect, value, line) {
845
1500
  this.counts.set(aspect, (this.counts.get(aspect) ?? 0) + 1);
846
1501
  if (aspect === "metaData") {
847
- if (isRecord(value) && typeof value.name === "string" && typeof value.elementCount === "number") {
848
- this.declared.set(value.name, value.elementCount);
849
- }
1502
+ this.readCount(value, line);
850
1503
  return;
851
1504
  }
852
1505
  if (aspect === "status") {
853
1506
  this.readStatus(value, line);
854
1507
  }
855
1508
  }
1509
+ /**
1510
+ * Record the element count a metaData element declares; a count that is not an integer (text,
1511
+ * a fraction) or that names no aspect is W_COUNT_MISMATCH and not checked.
1512
+ * @param value - the metaData element
1513
+ * @param line - its line
1514
+ */
1515
+ readCount(value, line) {
1516
+ if (!isRecord(value) || value.elementCount === undefined || value.elementCount === null) {
1517
+ return;
1518
+ }
1519
+ const raw = value.elementCount;
1520
+ let count = null;
1521
+ if (raw instanceof ExactInteger) {
1522
+ count = Number(raw.digits);
1523
+ }
1524
+ else if (typeof raw === "number" && Number.isInteger(raw)) {
1525
+ count = raw;
1526
+ }
1527
+ if (typeof value.name !== "string" || count === null) {
1528
+ const shownCount = raw instanceof ExactInteger ? raw.digits : JSON.stringify(raw);
1529
+ this.report.warning("validation-error", COUNT_MISMATCH_CODE, `a metaData element declares the elementCount ${shownCount} for the name ${JSON.stringify(value.name) ?? "undefined"}; not a count of a named aspect, not checked`, { line, element: "metaData" });
1530
+ return;
1531
+ }
1532
+ this.declared.set(this.canonical(value.name), count);
1533
+ }
856
1534
  /**
857
1535
  * Check a status element: success false is fatal, an error text with success true a warning.
858
1536
  * @param value - the element
@@ -891,7 +1569,7 @@ export class CxStructure {
891
1569
  for (const [name, declared] of this.declared) {
892
1570
  const read = this.counts.get(name) ?? 0;
893
1571
  if (read !== declared) {
894
- this.report.warning("validation-error", COUNT_MISMATCH_CODE, `metaData declares ${declared} "${name}" element(s); ${read} were read`, { element: name });
1572
+ this.report.warning("validation-error", COUNT_MISMATCH_CODE, `metaData declares ${declared} "${name}" element${plural(declared)}; ${read} ${agree(read, "was", "were")} read`, { element: name });
895
1573
  }
896
1574
  }
897
1575
  }
@@ -900,6 +1578,7 @@ export class CxStructure {
900
1578
  * Whether a value is a plain JSON object.
901
1579
  * @param value - any value
902
1580
  * @returns true for a non-null, non-array object
1581
+ * @category Plugin helpers
903
1582
  */
904
1583
  export function isRecord(value) {
905
1584
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof ExactInteger);
@@ -913,6 +1592,7 @@ export function isRecord(value) {
913
1592
  * @param decl - the declaration
914
1593
  * @param report - the report the rename is recorded in
915
1594
  * @returns the handle
1595
+ * @category Plugin helpers
916
1596
  */
917
1597
  export function declareFresh(sink, domain, decl, report) {
918
1598
  const taken = (name) => (domain === "node" ? sink.nodeColumn(name) : sink.edgeColumn(name)) !== INVALID_INDEX;