@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
package/src/common/xml.ts CHANGED
@@ -8,11 +8,13 @@
8
8
  * instead of re-reading the token from its `<`, and line numbers are counted from a cached next
9
9
  * line-break position, so a document without line breaks costs the same as one with them.
10
10
  *
11
- * What it handles: the XML declaration and processing instructions (skipped), comments (skipped),
11
+ * What it handles: the XML declaration and processing instructions (skipped), comments (skipped;
12
+ * a `--` inside one, which XML 1.0 forbids, is accepted: a comment carries no data and banner
13
+ * comments such as `<!-- ----- -->` are common in hand-written files),
12
14
  * CDATA sections (text), a DOCTYPE with an internal subset (skipped; entity declarations are not
13
15
  * expanded, so an unknown entity reference is a syntax error), the five predefined entities and
14
16
  * numeric character references (decimal and hexadecimal) in text and attribute values, attribute
15
- * value whitespace normalisation, end-of-line normalisation (CR LF and lone CR become LF), and
17
+ * value whitespace normalization, end-of-line normalization (CR LF and lone CR become LF), and
16
18
  * well-formedness: matching end tags, one root element, no text outside it, no duplicate
17
19
  * attributes, no unterminated markup at the end of the input. Namespaces are not resolved; element
18
20
  * and attribute names are reported as written (with their prefix).
@@ -30,16 +32,23 @@ import { type Column, type GraphSnapshot } from "@graphty/graph-format";
30
32
  import { type LossNote } from "../types.js";
31
33
  import { XML_ILLEGAL_CHAR_CODE } from "./codes.js";
32
34
  import { isNameChar } from "./export.js";
35
+ import { agree, plural } from "./plural.js";
33
36
 
34
- /** The events of the tokenizer; every callback is synchronous. */
37
+ /**
38
+ * The events of the tokenizer; every callback is synchronous.
39
+ * @category Plugin helpers
40
+ */
35
41
  export interface XmlHandler {
36
42
  /**
37
43
  * An element starts (a self-closing element produces start then end).
38
44
  * @param name - the element name as written, prefix included
39
45
  * @param attrs - the attributes, entities decoded, in document order
40
46
  * @param line - the 1-based line of the `<`
47
+ * @param ns - the namespace URI the element's prefix (or the default namespace) is bound to:
48
+ * "" when the element is in no namespace, null when its prefix is undeclared or the name is not
49
+ * namespace-well-formed (a leading or trailing colon, two colons)
41
50
  */
42
- start(name: string, attrs: ReadonlyMap<string, string>, line: number): void;
51
+ start(name: string, attrs: ReadonlyMap<string, string>, line: number, ns: string | null): void;
43
52
  /**
44
53
  * An element ends.
45
54
  * @param name - the element name
@@ -53,9 +62,18 @@ export interface XmlHandler {
53
62
  * @param line - the 1-based line where the run starts
54
63
  */
55
64
  text(text: string, line: number): void;
65
+ /**
66
+ * A DOCTYPE declaration (optional; its internal subset is never expanded).
67
+ * @param text - the declaration as written, from `<!DOCTYPE` to its `>`
68
+ * @param line - the 1-based line of the `<`
69
+ */
70
+ doctype?(text: string, line: number): void;
56
71
  }
57
72
 
58
- /** A well-formedness or syntax error, with the line it was found on. */
73
+ /**
74
+ * A well-formedness or syntax error, with the line it was found on.
75
+ * @category Plugin helpers
76
+ */
59
77
  export class XmlSyntaxError extends Error {
60
78
  /** The 1-based line. */
61
79
  readonly line: number;
@@ -73,10 +91,26 @@ export class XmlSyntaxError extends Error {
73
91
  }
74
92
 
75
93
  /**
76
- * Opt-in repairs of two defects the Cytoscape XGMML writer is known to produce (research note
77
- * `research-xgmml.md` 3.8 and 5). Each is off unless its callback is given; GEXF and GraphML never
94
+ * The input holds no markup at all (empty, whitespace only, a byte order mark only): an importer
95
+ * reports it as E_EMPTY_INPUT rather than as a syntax error.
96
+ * @category Plugin helpers
97
+ */
98
+ export class XmlEmptyInputError extends XmlSyntaxError {
99
+ /**
100
+ * Create the error.
101
+ * @param line - the 1-based line where the input ended
102
+ */
103
+ constructor(line: number) {
104
+ super("the input is empty: no root element", line);
105
+ this.name = "XmlEmptyInputError";
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Opt-in repairs of two defects the Cytoscape XGMML writer is known to produce. Each is off unless its callback is given; GEXF and GraphML never
78
111
  * pass them, so their documents stay strictly well-formed. The callback is told the line of each
79
112
  * repair, so the importer can warn per occurrence.
113
+ * @category Plugin helpers
80
114
  */
81
115
  export interface XmlRepairs {
82
116
  /**
@@ -122,6 +156,7 @@ const OPEN_BRACKET = 91;
122
156
  const CLOSE_BRACKET = 93;
123
157
  const BANG = 33;
124
158
  const DASH = 45;
159
+ const QUESTION = 63;
125
160
 
126
161
  /** The longest entity reference held back at a chunk boundary (`&#x10FFFF;` is 10 characters). */
127
162
  const MAX_ENTITY_LENGTH = 16;
@@ -178,11 +213,22 @@ const ILLEGAL_CHAR = new RegExp(
178
213
  * written even as a character reference, so a conforming parser rejects the whole document.
179
214
  * @param text - the text
180
215
  * @returns true when the text cannot appear in an XML 1.0 document
216
+ * @category Plugin helpers
181
217
  */
182
218
  export function hasIllegalXmlChar(text: string): boolean {
183
219
  return ILLEGAL_CHAR.test(text);
184
220
  }
185
221
 
222
+ /**
223
+ * Where the first character XML 1.0 forbids is in a text.
224
+ * @param text - the text
225
+ * @returns its index, or -1
226
+ */
227
+ function illegalCharIndex(text: string): number {
228
+ const match = ILLEGAL_CHAR.exec(text);
229
+ return match === null ? -1 : match.index;
230
+ }
231
+
186
232
  /**
187
233
  * The loss notes of the texts a snapshot holds that no XML 1.0 document can carry (see
188
234
  * hasIllegalXmlChar): one E_XML_ILLEGAL_CHAR note per column (node, edge and graph string / dict /
@@ -190,6 +236,7 @@ export function hasIllegalXmlChar(text: string): boolean {
190
236
  * the first such text. Shared by the GEXF and GraphML exporters' check().
191
237
  * @param snapshot - the snapshot
192
238
  * @returns the notes, empty when every text is writable
239
+ * @category Plugin helpers
193
240
  */
194
241
  export function xmlIllegalTextNotes(snapshot: GraphSnapshot): LossNote[] {
195
242
  const notes: LossNote[] = [];
@@ -204,7 +251,7 @@ export function xmlIllegalTextNotes(snapshot: GraphSnapshot): LossNote[] {
204
251
  notes.push(
205
252
  Object.freeze({
206
253
  code: XML_ILLEGAL_CHAR_CODE,
207
- message: `${ids} node id(s) hold a character XML 1.0 cannot carry; export() will throw`,
254
+ message: `${ids} node id${plural(ids)} ${agree(ids, "holds", "hold")} a character XML 1.0 cannot carry; the save fails`,
208
255
  column: null,
209
256
  count: ids,
210
257
  }),
@@ -221,7 +268,7 @@ export function xmlIllegalTextNotes(snapshot: GraphSnapshot): LossNote[] {
221
268
  notes.push(
222
269
  Object.freeze({
223
270
  code: XML_ILLEGAL_CHAR_CODE,
224
- message: `${domain} column "${column.meta.name}": ${bad} value(s) hold a character XML 1.0 cannot carry; export() will throw`,
271
+ message: `${domain} column "${column.meta.name}": ${bad} value${plural(bad)} ${agree(bad, "holds", "hold")} a character XML 1.0 cannot carry; the save fails`,
225
272
  column: column.meta.name,
226
273
  count: bad,
227
274
  }),
@@ -258,7 +305,10 @@ function countIllegalRows(column: Column): number {
258
305
  return bad;
259
306
  }
260
307
 
261
- /** The encoding pseudo-attribute of an XML declaration at the very start of a document. */
308
+ /**
309
+ * The encoding pseudo-attribute of an XML declaration at the very start of a document (one
310
+ * anywhere else is a syntax error of the tokenizer).
311
+ */
262
312
  const XML_DECLARED_ENCODING = /^<\?xml\s[^>]*?\bencoding\s*=\s*["']([A-Za-z][A-Za-z0-9._-]*)["']/;
263
313
 
264
314
  /**
@@ -266,16 +316,35 @@ const XML_DECLARED_ENCODING = /^<\?xml\s[^>]*?\bencoding\s*=\s*["']([A-Za-z][A-Z
266
316
  * for the shared byte decoder (common/input.ts).
267
317
  * @param head - the start of the document, decoded as windows-1252
268
318
  * @returns the declared label, or null when there is no declaration or it names no encoding
319
+ * @category Plugin helpers
269
320
  */
270
321
  export function xmlDeclaredEncoding(head: string): string | null {
271
322
  const match = XML_DECLARED_ENCODING.exec(head);
272
323
  return match === null ? null : match[1];
273
324
  }
274
325
 
326
+ /**
327
+ * The head of a document as text for an XML format's sniff(): UTF-16 when a byte order mark (or
328
+ * BOM-less UTF-16 `<?`, XML 1.0 Appendix F) says so, else UTF-8, never failing.
329
+ * @param head - the first bytes
330
+ * @returns the text, a leading byte order mark removed
331
+ * @category Plugin helpers
332
+ */
333
+ export function sniffXmlText(head: Uint8Array): string {
334
+ let label = "utf-8";
335
+ if ((head[0] === 0xff && head[1] === 0xfe) || (head[0] === 0x3c && head[1] === 0x00 && head[2] === 0x3f)) {
336
+ label = "utf-16le";
337
+ } else if ((head[0] === 0xfe && head[1] === 0xff) || (head[0] === 0x00 && head[1] === 0x3c && head[3] === 0x3f)) {
338
+ label = "utf-16be";
339
+ }
340
+ return new TextDecoder(label, { fatal: false }).decode(head);
341
+ }
342
+
275
343
  /**
276
344
  * Whether a text is an XML Name.
277
345
  * @param text - the text
278
346
  * @returns true for a well-formed name
347
+ * @category Plugin helpers
279
348
  */
280
349
  export function isXmlName(text: string): boolean {
281
350
  if (text.length === 0) {
@@ -314,6 +383,7 @@ function isXmlChar(cp: number): boolean {
314
383
  * @param line - the line, for errors
315
384
  * @param repairs - the opt-in repairs (XGMML only); none by default
316
385
  * @returns the decoded text
386
+ * @category Plugin helpers
317
387
  */
318
388
  export function decodeEntities(raw: string, line: number, repairs?: XmlRepairs): string {
319
389
  let amp = raw.indexOf("&");
@@ -338,13 +408,14 @@ export function decodeEntities(raw: string, line: number, repairs?: XmlRepairs):
338
408
  continue;
339
409
  }
340
410
  if (semi < 0) {
341
- throw new XmlSyntaxError("unterminated entity reference", line);
411
+ throw new XmlSyntaxError("unterminated entity reference", lineAt(raw, amp, line));
342
412
  }
343
413
  const name = raw.slice(amp + 1, semi);
344
414
  if (name.startsWith("#")) {
345
415
  const hex = name.startsWith("#x") || name.startsWith("#X");
346
416
  const digits = name.slice(hex ? 2 : 1);
347
- const ok = hex ? /^[0-9a-fA-F]{1,6}$/.test(digits) : /^[0-9]{1,7}$/.test(digits);
417
+ // XML 1.0 puts no limit on leading zeros (&#x0000000041; is "A")
418
+ const ok = hex ? /^0*[\da-fA-F]{1,6}$/.test(digits) : /^0*\d{1,7}$/.test(digits);
348
419
  const cp = ok ? Number.parseInt(digits, hex ? 16 : 10) : -1;
349
420
  const low = cp >= 0xd800 && cp <= 0xdbff ? lowSurrogateAfter(raw, semi + 1, repairs) : null;
350
421
  if (low !== null) {
@@ -355,13 +426,13 @@ export function decodeEntities(raw: string, line: number, repairs?: XmlRepairs):
355
426
  continue;
356
427
  }
357
428
  if (cp < 0 || !isXmlChar(cp)) {
358
- throw new XmlSyntaxError(`invalid character reference &${name};`, line);
429
+ throw new XmlSyntaxError(`invalid character reference &${name};`, lineAt(raw, amp, line));
359
430
  }
360
431
  out += String.fromCodePoint(cp);
361
432
  } else {
362
433
  const value = NAMED_ENTITIES[name];
363
434
  if (value === undefined) {
364
- throw new XmlSyntaxError(`unknown entity &${name};`, line);
435
+ throw new XmlSyntaxError(`unknown entity &${name};`, lineAt(raw, amp, line));
365
436
  }
366
437
  out += value;
367
438
  }
@@ -414,6 +485,7 @@ function lowSurrogateAfter(
414
485
  * Whether a text is whitespace only.
415
486
  * @param text - the text
416
487
  * @returns true when every character is XML whitespace (also for an empty text)
488
+ * @category Plugin helpers
417
489
  */
418
490
  export function isWhitespace(text: string): boolean {
419
491
  for (let i = 0; i < text.length; i++) {
@@ -428,6 +500,7 @@ export function isWhitespace(text: string): boolean {
428
500
  * The local part of a possibly prefixed XML name.
429
501
  * @param name - the name as written
430
502
  * @returns the text after the last colon, or the name itself
503
+ * @category Plugin helpers
431
504
  */
432
505
  export function localName(name: string): string {
433
506
  const colon = name.lastIndexOf(":");
@@ -440,6 +513,7 @@ export function localName(name: string): string {
440
513
  * @param chunks - the text (already UTF-8 decoded, BOM removed)
441
514
  * @param handler - the event sink
442
515
  * @param repairs - the opt-in repairs (XGMML only); none by default
516
+ * @category Plugin helpers
443
517
  */
444
518
  export async function tokenizeXml(
445
519
  chunks: AsyncIterable<string>,
@@ -447,10 +521,40 @@ export async function tokenizeXml(
447
521
  repairs?: XmlRepairs,
448
522
  ): Promise<void> {
449
523
  const tokenizer = new XmlTokenizer(handler, repairs);
450
- for await (const chunk of chunks) {
451
- tokenizer.push(chunk);
524
+ // XML 1.0 section 2.8: nothing may precede the XML declaration, not even whitespace (template
525
+ // output often adds a line break, and the declared encoding would then go unread)
526
+ let skipped = 0;
527
+ let lines = 1;
528
+ let leading = true;
529
+ try {
530
+ for await (const chunk of chunks) {
531
+ if (leading) {
532
+ const start = chunk.search(/\S/);
533
+ const blank = start < 0 ? chunk : chunk.slice(0, start);
534
+ skipped += blank.length;
535
+ lines += blank.split("\n").length - 1;
536
+ if (start >= 0) {
537
+ leading = false;
538
+ if (skipped > 0 && /^<\?xml\s/.test(chunk.slice(start, start + 6))) {
539
+ throw new XmlSyntaxError(
540
+ "the XML declaration must be at the very start of the document; whitespace precedes it",
541
+ lines,
542
+ );
543
+ }
544
+ }
545
+ }
546
+ tokenizer.push(chunk);
547
+ }
548
+ tokenizer.finish();
549
+ } catch (err) {
550
+ if (err instanceof XmlSyntaxError && tokenizer.declaresXml11 && !err.message.includes("XML 1.1")) {
551
+ throw new XmlSyntaxError(
552
+ `${err.message}; the document declares XML 1.1, which graph-io does not read (it reads XML 1.0)`,
553
+ err.line,
554
+ );
555
+ }
556
+ throw err;
452
557
  }
453
- tokenizer.finish();
454
558
  }
455
559
 
456
560
  /** The result of parsing one start tag out of the buffer. */
@@ -461,6 +565,8 @@ interface StartTag {
461
565
  readonly attrs: Map<string, string>;
462
566
  /** Whether the tag ends with `/>`. */
463
567
  readonly selfClosing: boolean;
568
+ /** Whether an attribute name starts with `xmlns` (a namespace declaration may be among them). */
569
+ readonly declaresNamespace: boolean;
464
570
  /** The buffer index one past the `>`. */
465
571
  readonly end: number;
466
572
  }
@@ -481,7 +587,7 @@ const TERMINATORS: Readonly<Partial<Record<PendingKind, string>>> = {
481
587
  * until the end is found, so a token spanning many chunks costs its length once) and the state
482
588
  * the terminator scan is in. `tail` holds the last characters of the pieces a fixed terminator
483
589
  * could straddle; `quote` is the open quote character (0 outside a value) of a start tag or a
484
- * DOCTYPE; `depth`, `comment` and `run` track a DOCTYPE's internal subset.
590
+ * DOCTYPE; `depth`, `comment`, `pi` and `run` track a DOCTYPE's internal subset.
485
591
  */
486
592
  interface Pending {
487
593
  kind: PendingKind;
@@ -490,12 +596,30 @@ interface Pending {
490
596
  quote: number;
491
597
  depth: number;
492
598
  comment: boolean;
599
+ pi: boolean;
493
600
  run: number;
494
601
  }
495
602
 
603
+ /** One `xmlns` / `xmlns:prefix` declaration in scope. */
604
+ interface NamespaceBinding {
605
+ /** The prefix; "" for the default namespace. */
606
+ readonly prefix: string;
607
+ /** The namespace URI; "" undeclares the default namespace. */
608
+ readonly uri: string;
609
+ /** The element depth that declared it (the stack length before the element was pushed). */
610
+ readonly depth: number;
611
+ }
612
+
613
+ /** The namespace the `xml` prefix is bound to by definition. */
614
+ const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
615
+
616
+ /** The markup declarations a truncated `<!...` at the end of the input may have been cut from. */
617
+ const DECLARATION_OPENERS: readonly string[] = ["<!--", "<![CDATA[", "<!DOCTYPE"];
618
+
496
619
  /**
497
620
  * The tokenizer state: the unconsumed tail of the input, the current line, the open element
498
621
  * stack and the text run being accumulated. `push()` chunks, then `finish()`.
622
+ * @category Plugin helpers
499
623
  */
500
624
  export class XmlTokenizer {
501
625
  private readonly handler: XmlHandler;
@@ -532,6 +656,27 @@ export class XmlTokenizer {
532
656
 
533
657
  private rootClosed = false;
534
658
 
659
+ /** Whether any markup was consumed (an input with none is empty, not malformed). */
660
+ private markupSeen = false;
661
+
662
+ /** Whether nothing has been consumed yet: the XML declaration may only appear here. */
663
+ private atStart = true;
664
+
665
+ /** Whether a DOCTYPE declaration was read (one is allowed, in the prolog). */
666
+ private doctypeSeen = false;
667
+
668
+ /** The version of the XML declaration, or null. */
669
+ private version: string | null = null;
670
+
671
+ /** The last two characters of the character data since the last markup, for the `]]>` check. */
672
+ private textTail = "";
673
+
674
+ /** A high surrogate held back from the end of the previous chunk (its low half may start the next). */
675
+ private pendingHigh = "";
676
+
677
+ /** The namespace declarations in scope, innermost last. */
678
+ private readonly bindings: NamespaceBinding[] = [];
679
+
535
680
  private readonly repairs: XmlRepairs | undefined;
536
681
 
537
682
  /**
@@ -544,12 +689,31 @@ export class XmlTokenizer {
544
689
  this.repairs = repairs;
545
690
  }
546
691
 
692
+ /**
693
+ * Whether the document's XML declaration says version 1.1 (read by the XML 1.0 rules all the
694
+ * same; the syntax error of a 1.1-only construct then says why).
695
+ * @returns true after a `<?xml version="1.1"?>` declaration
696
+ */
697
+ get declaresXml11(): boolean {
698
+ return this.version === "1.1";
699
+ }
700
+
547
701
  /**
548
702
  * Feed one chunk and emit every complete token in it.
549
703
  * @param chunk - the text
550
704
  */
551
705
  push(chunk: string): void {
552
706
  let text = chunk;
707
+ if (this.pendingHigh.length > 0) {
708
+ text = this.pendingHigh + text;
709
+ this.pendingHigh = "";
710
+ }
711
+ const last = text.charCodeAt(text.length - 1); // NOSONAR(S7758): reads UTF-16 code units on purpose
712
+ if (last >= 0xd800 && last <= 0xdbff) {
713
+ // a text chunk may end between the two halves of an astral character
714
+ this.pendingHigh = text.slice(-1);
715
+ text = text.slice(0, -1);
716
+ }
553
717
  if (this.pendingCr) {
554
718
  text = `\r${text}`;
555
719
  this.pendingCr = false;
@@ -559,7 +723,7 @@ export class XmlTokenizer {
559
723
  text = text.slice(0, -1);
560
724
  }
561
725
  if (text.includes("\r")) {
562
- text = text.replace(/\r\n?/g, "\n");
726
+ text = text.replaceAll(/\r\n?/g, "\n");
563
727
  }
564
728
  this.feed(text);
565
729
  }
@@ -567,11 +731,13 @@ export class XmlTokenizer {
567
731
  /** Signal the end of the input: flush the last text run and check well-formedness. */
568
732
  finish(): void {
569
733
  this.final = true;
734
+ const held = this.pendingHigh;
735
+ this.pendingHigh = "";
570
736
  if (this.pendingCr) {
571
737
  this.pendingCr = false;
572
- this.feed("\n");
738
+ this.feed(`\n${held}`);
573
739
  } else {
574
- this.feed("");
740
+ this.feed(held);
575
741
  }
576
742
  if (this.pending !== null || this.buffer.length > 0) {
577
743
  throw new XmlSyntaxError("unexpected end of input inside markup", this.line);
@@ -581,14 +747,17 @@ export class XmlTokenizer {
581
747
  throw new XmlSyntaxError(`unclosed element <${this.stack[this.stack.length - 1]}>`, this.line);
582
748
  }
583
749
  if (!this.rootSeen) {
750
+ if (!this.markupSeen) {
751
+ throw new XmlEmptyInputError(this.line);
752
+ }
584
753
  throw new XmlSyntaxError("no root element", this.line);
585
754
  }
586
755
  }
587
756
 
588
757
  /**
589
- * Append normalised text: continue a pending token's terminator scan over the new text alone,
758
+ * Append normalized text: continue a pending token's terminator scan over the new text alone,
590
759
  * and once the token is complete (or when none is pending) scan the buffer for tokens.
591
- * @param text - the text, line breaks normalised
760
+ * @param text - the text, line breaks normalized
592
761
  */
593
762
  private feed(text: string): void {
594
763
  const { pending } = this;
@@ -697,9 +866,19 @@ export class XmlTokenizer {
697
866
  * @returns the index one past the `>`, or -1
698
867
  */
699
868
  private scanDoctype(pending: Pending, text: string, from: number): number {
700
- let { quote, depth, comment, run } = pending;
869
+ let { quote, depth, comment, pi, run } = pending;
701
870
  for (let i = from; i < text.length; i++) {
702
871
  const c = text.charCodeAt(i);
872
+ if (pi) {
873
+ // a processing instruction in the subset ends at `?>`; quotes inside it are text
874
+ if (c === GT && run === 1) {
875
+ pi = false;
876
+ run = 0;
877
+ } else {
878
+ run = c === QUESTION ? 1 : 0;
879
+ }
880
+ continue;
881
+ }
703
882
  if (comment) {
704
883
  if (c === DASH) {
705
884
  run++;
@@ -731,6 +910,11 @@ export class XmlTokenizer {
731
910
  run = 0;
732
911
  continue;
733
912
  }
913
+ if (run === 1 && c === QUESTION) {
914
+ pi = true;
915
+ run = 0;
916
+ continue;
917
+ }
734
918
  run = 0;
735
919
  if (c === LT) {
736
920
  run = 1;
@@ -747,6 +931,7 @@ export class XmlTokenizer {
747
931
  pending.quote = 0;
748
932
  pending.depth = 0;
749
933
  pending.comment = false;
934
+ pending.pi = false;
750
935
  pending.run = 0;
751
936
  return i + 1;
752
937
  }
@@ -754,6 +939,7 @@ export class XmlTokenizer {
754
939
  pending.quote = quote;
755
940
  pending.depth = depth;
756
941
  pending.comment = comment;
942
+ pending.pi = pi;
757
943
  pending.run = run;
758
944
  return -1;
759
945
  }
@@ -775,7 +961,13 @@ export class XmlTokenizer {
775
961
  if (!this.final) {
776
962
  // an entity reference may be split across chunks: hold back from its "&"
777
963
  const amp = buffer.lastIndexOf("&");
778
- if (amp >= pos && amp >= length - MAX_ENTITY_LENGTH && buffer.indexOf(";", amp) < 0) {
964
+ if (
965
+ amp >= pos &&
966
+ !buffer.includes(";", amp) &&
967
+ // ponytail: a zero-padded reference longer than MAX_ENTITY_LENGTH is held back
968
+ // whole, so an absurdly long run of zeros is re-scanned once per chunk
969
+ (amp >= length - MAX_ENTITY_LENGTH || /^&#[xX]?0[\da-fA-F]*$/.test(buffer.slice(amp)))
970
+ ) {
779
971
  end = amp;
780
972
  }
781
973
  if (this.repairs?.surrogatePair !== undefined) {
@@ -822,6 +1014,7 @@ export class XmlTokenizer {
822
1014
  quote: 0,
823
1015
  depth: 0,
824
1016
  comment: false,
1017
+ pi: false,
825
1018
  run: 0,
826
1019
  };
827
1020
  if (piece.length < MIN_DECIDABLE_MARKUP) {
@@ -866,60 +1059,32 @@ export class XmlTokenizer {
866
1059
  * @returns the index after the markup, or -1 when the buffer ends before the markup does
867
1060
  */
868
1061
  private consumeMarkup(pos: number): number {
1062
+ const end = this.markupEnd(pos);
1063
+ if (end >= 0) {
1064
+ this.markupSeen = true;
1065
+ this.atStart = false;
1066
+ // markup ends a run of character data: a `]]>` cannot straddle it
1067
+ this.textTail = "";
1068
+ }
1069
+ return end;
1070
+ }
1071
+
1072
+ /**
1073
+ * Read the markup starting at `pos` (a `<`) and deliver what it means.
1074
+ * @param pos - the index of the `<`
1075
+ * @returns the index after the markup, or -1 when the buffer ends before the markup does
1076
+ */
1077
+ private markupEnd(pos: number): number {
869
1078
  const { buffer } = this;
870
1079
  if (buffer.startsWith("<!", pos)) {
871
- if (buffer.length - pos < MIN_DECIDABLE_MARKUP && !this.final) {
872
- // "<![CDATA[" is 9 characters; wait until the kind of declaration is decidable
873
- return -1;
874
- }
875
- if (buffer.startsWith("<!--", pos)) {
876
- const end = buffer.indexOf("-->", pos + 4);
877
- if (end < 0) {
878
- return -1;
879
- }
880
- this.advanceLine(pos, end + 3);
881
- return end + 3;
882
- }
883
- if (buffer.startsWith("<![CDATA[", pos)) {
884
- const end = buffer.indexOf("]]>", pos + 9);
885
- if (end < 0) {
886
- return -1;
887
- }
888
- if (this.text.length === 0) {
889
- this.textLine = this.line;
890
- }
891
- const cdata = buffer.slice(pos + 9, end);
892
- if (hasIllegalXmlChar(cdata)) {
893
- throw new XmlSyntaxError("a character XML 1.0 forbids appears in a CDATA section", this.line);
894
- }
895
- this.text += cdata;
896
- this.advanceLine(pos, end + 3);
897
- return end + 3;
898
- }
899
- if (!buffer.startsWith("<!DOCTYPE", pos)) {
900
- throw new XmlSyntaxError("unexpected markup declaration", this.line);
901
- }
902
- const state: Pending = {
903
- kind: "doctype",
904
- pieces: [],
905
- tail: "",
906
- quote: 0,
907
- depth: 0,
908
- comment: false,
909
- run: 0,
910
- };
911
- const end = this.scanDoctype(state, buffer, pos + 9);
912
- if (end < 0) {
913
- return -1;
914
- }
915
- this.advanceLine(pos, end);
916
- return end;
1080
+ return this.declarationEnd(pos);
917
1081
  }
918
1082
  if (buffer.startsWith("<?", pos)) {
919
1083
  const end = buffer.indexOf("?>", pos + 2);
920
1084
  if (end < 0) {
921
1085
  return -1;
922
1086
  }
1087
+ this.processingInstruction(pos, end);
923
1088
  this.advanceLine(pos, end + 2);
924
1089
  return end + 2;
925
1090
  }
@@ -928,7 +1093,8 @@ export class XmlTokenizer {
928
1093
  if (end < 0) {
929
1094
  return -1;
930
1095
  }
931
- const name = buffer.slice(pos + 2, end).trim();
1096
+ // whitespace may follow the name, never precede it
1097
+ const name = buffer.slice(pos + 2, end).trimEnd();
932
1098
  if (!isXmlName(name)) {
933
1099
  throw new XmlSyntaxError(`malformed end tag </${name}>`, this.line);
934
1100
  }
@@ -942,7 +1108,7 @@ export class XmlTokenizer {
942
1108
  return -1;
943
1109
  }
944
1110
  this.flushText();
945
- this.startElement(tag.name, tag.attrs);
1111
+ this.startElement(tag.name, tag.attrs, tag.declaresNamespace);
946
1112
  if (tag.selfClosing) {
947
1113
  this.endElement(tag.name);
948
1114
  }
@@ -950,6 +1116,161 @@ export class XmlTokenizer {
950
1116
  return tag.end;
951
1117
  }
952
1118
 
1119
+ /**
1120
+ * Read the CDATA section at `pos` into the text.
1121
+ * @param pos - the index of the `<`
1122
+ * @returns the index after the section, or -1 when the buffer ends before the section does
1123
+ */
1124
+ private cdataEnd(pos: number): number {
1125
+ const end = this.buffer.indexOf("]]>", pos + 9);
1126
+ if (end < 0) {
1127
+ return -1;
1128
+ }
1129
+ if (this.text.length === 0) {
1130
+ this.textLine = this.line;
1131
+ }
1132
+ const cdata = this.buffer.slice(pos + 9, end);
1133
+ const bad = illegalCharIndex(cdata);
1134
+ if (bad >= 0) {
1135
+ throw new XmlSyntaxError(
1136
+ "a character XML 1.0 forbids appears in a CDATA section",
1137
+ this.lineIn(pos, pos + 9 + bad),
1138
+ );
1139
+ }
1140
+ this.text += cdata;
1141
+ this.advanceLine(pos, end + 3);
1142
+ return end + 3;
1143
+ }
1144
+
1145
+ /**
1146
+ * Read the `<!...>` markup at `pos`: a comment, a CDATA section or the DOCTYPE.
1147
+ * @param pos - the index of the `<`
1148
+ * @returns the index after the markup, or -1 when the buffer ends before the markup does
1149
+ */
1150
+ private declarationEnd(pos: number): number {
1151
+ const { buffer } = this;
1152
+ if (buffer.length - pos < MIN_DECIDABLE_MARKUP) {
1153
+ // "<![CDATA[" is 9 characters; wait until the kind of declaration is decidable, and at
1154
+ // the end of the input read the start of one cut short as a truncation
1155
+ const head = buffer.slice(pos);
1156
+ if (!this.final || DECLARATION_OPENERS.some((opener) => opener.startsWith(head))) {
1157
+ return -1;
1158
+ }
1159
+ }
1160
+ if (buffer.startsWith("<!--", pos)) {
1161
+ const end = buffer.indexOf("-->", pos + 4);
1162
+ if (end < 0) {
1163
+ return -1;
1164
+ }
1165
+ // a "--" inside, which XML 1.0 forbids, is accepted: the comment carries no data
1166
+ this.advanceLine(pos, end + 3);
1167
+ return end + 3;
1168
+ }
1169
+ if (buffer.startsWith("<![CDATA[", pos)) {
1170
+ return this.cdataEnd(pos);
1171
+ }
1172
+ if (!buffer.startsWith("<!DOCTYPE", pos)) {
1173
+ if (buffer.slice(pos + 2, pos + 9).toUpperCase() === "DOCTYPE") {
1174
+ throw new XmlSyntaxError(
1175
+ `<!${buffer.slice(pos + 2, pos + 9)}> is not XML (it must be written <!DOCTYPE>); the input looks like an HTML document`,
1176
+ this.line,
1177
+ );
1178
+ }
1179
+ throw new XmlSyntaxError("unexpected markup declaration", this.line);
1180
+ }
1181
+ if (this.rootSeen || this.doctypeSeen) {
1182
+ throw new XmlSyntaxError("a DOCTYPE declaration is allowed once, before the root element", this.line);
1183
+ }
1184
+ const state: Pending = {
1185
+ kind: "doctype",
1186
+ pieces: [],
1187
+ tail: "",
1188
+ quote: 0,
1189
+ depth: 0,
1190
+ comment: false,
1191
+ pi: false,
1192
+ run: 0,
1193
+ };
1194
+ const end = this.scanDoctype(state, buffer, pos + 9);
1195
+ if (end < 0) {
1196
+ return -1;
1197
+ }
1198
+ this.doctypeSeen = true;
1199
+ this.handler.doctype?.(buffer.slice(pos, end), this.line);
1200
+ this.advanceLine(pos, end);
1201
+ return end;
1202
+ }
1203
+
1204
+ /**
1205
+ * Check a processing instruction `<?target ...?>` (skipped otherwise): its target must be a
1206
+ * name, and the reserved target `xml` is the XML declaration, allowed only at the very start
1207
+ * of the document with a 1.x version.
1208
+ * @param pos - the index of the `<`
1209
+ * @param end - the index of the `?>`
1210
+ */
1211
+ private processingInstruction(pos: number, end: number): void {
1212
+ const body = this.buffer.slice(pos + 2, end);
1213
+ const target = /^[^\s]*/.exec(body)?.[0] ?? "";
1214
+ if (!isXmlName(target)) {
1215
+ throw new XmlSyntaxError(`a processing instruction needs a target name, not "${target}"`, this.line);
1216
+ }
1217
+ if (target.toLowerCase() !== "xml") {
1218
+ return;
1219
+ }
1220
+ if (target !== "xml") {
1221
+ throw new XmlSyntaxError(
1222
+ `<?${target} is not an XML declaration (which is written <?xml in lower case); the target is reserved`,
1223
+ this.line,
1224
+ );
1225
+ }
1226
+ if (!this.atStart || pos !== 0) {
1227
+ throw new XmlSyntaxError(
1228
+ "an XML declaration (<?xml ...?>) may appear only at the very start of the document; the target xml is reserved",
1229
+ this.line,
1230
+ );
1231
+ }
1232
+ const version = /\bversion\s*=\s*(["'])([^"']*)\1/.exec(body)?.[2];
1233
+ if (version === undefined) {
1234
+ throw new XmlSyntaxError("the XML declaration has no version", this.line);
1235
+ }
1236
+ if (!/^1\.\d+$/.test(version)) {
1237
+ throw new XmlSyntaxError(`XML version ${version} is not supported (only 1.x)`, this.line);
1238
+ }
1239
+ this.version = version;
1240
+ }
1241
+
1242
+ /**
1243
+ * The line of a buffer position after `start`, the position the current line count refers to.
1244
+ * Used on the error path only.
1245
+ * @param start - a position on line `this.line`
1246
+ * @param at - the position
1247
+ * @returns its line
1248
+ */
1249
+ private lineIn(start: number, at: number): number {
1250
+ return lineAt(this.buffer.slice(start, at), at - start, this.line);
1251
+ }
1252
+
1253
+ /**
1254
+ * Decode the entities of a text, naming XML 1.1 when a character reference XML 1.0 forbids
1255
+ * appears in a document that declares version 1.1.
1256
+ * @param raw - the text as written
1257
+ * @param line - the line the text starts on
1258
+ * @returns the decoded text
1259
+ */
1260
+ private decode(raw: string, line: number): string {
1261
+ try {
1262
+ return decodeEntities(raw, line, this.repairs);
1263
+ } catch (err) {
1264
+ if (err instanceof XmlSyntaxError && this.version !== null && this.version !== "1.0") {
1265
+ throw new XmlSyntaxError(
1266
+ `${err.message} (the document declares XML ${this.version}, whose character rules are not supported; it is read as XML 1.0)`,
1267
+ err.line,
1268
+ );
1269
+ }
1270
+ throw err;
1271
+ }
1272
+ }
1273
+
953
1274
  /**
954
1275
  * Parse a start tag at `pos`.
955
1276
  * @param pos - the index of the `<`
@@ -969,7 +1290,9 @@ export class XmlTokenizer {
969
1290
  const name = buffer.slice(i, nameEnd);
970
1291
  i = nameEnd;
971
1292
  const attrs = new Map<string, string>();
1293
+ let declaresNamespace = false;
972
1294
  for (;;) {
1295
+ const before = i;
973
1296
  while (i < length && isSpace(buffer.charCodeAt(i))) {
974
1297
  i++;
975
1298
  }
@@ -978,23 +1301,30 @@ export class XmlTokenizer {
978
1301
  }
979
1302
  const c = buffer.charCodeAt(i);
980
1303
  if (c === GT) {
981
- return { name, attrs, selfClosing: false, end: i + 1 };
1304
+ return { name, attrs, selfClosing: false, declaresNamespace, end: i + 1 };
982
1305
  }
983
1306
  if (c === SLASH) {
984
1307
  if (i + 1 >= length) {
985
1308
  return null;
986
1309
  }
987
1310
  if (buffer.charCodeAt(i + 1) !== GT) {
988
- throw new XmlSyntaxError(`unexpected "/" in <${name}>`, this.line);
1311
+ throw new XmlSyntaxError(`unexpected "/" in <${name}>`, this.lineIn(pos, i));
989
1312
  }
990
- return { name, attrs, selfClosing: true, end: i + 2 };
1313
+ return { name, attrs, selfClosing: true, declaresNamespace, end: i + 2 };
1314
+ }
1315
+ if (i === before) {
1316
+ // the name, or the previous value's closing quote, runs into the next attribute
1317
+ throw new XmlSyntaxError(
1318
+ `the attributes of <${name}> must be separated by whitespace`,
1319
+ this.lineIn(pos, i),
1320
+ );
991
1321
  }
992
1322
  const attrEnd = this.readName(i);
993
1323
  if (attrEnd < 0) {
994
1324
  return null;
995
1325
  }
996
1326
  if (attrEnd === i) {
997
- throw new XmlSyntaxError(`malformed attribute in <${name}>`, this.line);
1327
+ throw new XmlSyntaxError(`malformed attribute in <${name}>`, this.lineIn(pos, i));
998
1328
  }
999
1329
  const attrName = buffer.slice(i, attrEnd);
1000
1330
  i = attrEnd;
@@ -1005,7 +1335,7 @@ export class XmlTokenizer {
1005
1335
  return null;
1006
1336
  }
1007
1337
  if (buffer.charCodeAt(i) !== EQUALS) {
1008
- throw new XmlSyntaxError(`attribute ${attrName} of <${name}> has no value`, this.line);
1338
+ throw new XmlSyntaxError(`attribute ${attrName} of <${name}> has no value`, this.lineIn(pos, i));
1009
1339
  }
1010
1340
  i++;
1011
1341
  while (i < length && isSpace(buffer.charCodeAt(i))) {
@@ -1016,23 +1346,30 @@ export class XmlTokenizer {
1016
1346
  }
1017
1347
  const quote = buffer.charCodeAt(i);
1018
1348
  if (quote !== QUOTE && quote !== APOS) {
1019
- throw new XmlSyntaxError(`attribute ${attrName} of <${name}> is not quoted`, this.line);
1349
+ throw new XmlSyntaxError(`attribute ${attrName} of <${name}> is not quoted`, this.lineIn(pos, i));
1020
1350
  }
1021
1351
  const close = buffer.indexOf(quote === QUOTE ? '"' : "'", i + 1);
1022
1352
  if (close < 0) {
1023
1353
  return null;
1024
1354
  }
1355
+ const line = this.lineIn(pos, i);
1025
1356
  if (attrs.has(attrName)) {
1026
- throw new XmlSyntaxError(`duplicate attribute ${attrName} in <${name}>`, this.line);
1357
+ throw new XmlSyntaxError(`duplicate attribute ${attrName} in <${name}>`, line);
1027
1358
  }
1028
1359
  const raw = buffer.slice(i + 1, close);
1029
1360
  if (hasIllegalXmlChar(raw)) {
1030
1361
  throw new XmlSyntaxError(
1031
1362
  `a character XML 1.0 forbids appears in attribute ${attrName} of <${name}>`,
1032
- this.line,
1363
+ line,
1033
1364
  );
1034
1365
  }
1035
- attrs.set(attrName, decodeEntities(normalizeAttributeValue(raw), this.line, this.repairs));
1366
+ if (raw.includes("<")) {
1367
+ throw new XmlSyntaxError(`a "<" in the value of attribute ${attrName} of <${name}> (write &lt;)`, line);
1368
+ }
1369
+ if (attrName.startsWith("xmlns")) {
1370
+ declaresNamespace = true;
1371
+ }
1372
+ attrs.set(attrName, this.decode(normalizeAttributeValue(raw), line));
1036
1373
  i = close + 1;
1037
1374
  }
1038
1375
  }
@@ -1096,10 +1433,26 @@ export class XmlTokenizer {
1096
1433
  this.textLine = this.line;
1097
1434
  }
1098
1435
  const raw = this.buffer.slice(start, end);
1099
- if (hasIllegalXmlChar(raw)) {
1100
- throw new XmlSyntaxError("a character XML 1.0 forbids appears in character data", this.line);
1436
+ const bad = illegalCharIndex(raw);
1437
+ if (bad >= 0) {
1438
+ throw new XmlSyntaxError(
1439
+ "a character XML 1.0 forbids appears in character data",
1440
+ lineAt(raw, bad, this.line),
1441
+ );
1101
1442
  }
1102
- this.text += decodeEntities(raw, this.line, this.repairs);
1443
+ if (raw.includes(">")) {
1444
+ const probe = this.textTail + raw;
1445
+ const close = probe.indexOf("]]>");
1446
+ if (close >= 0) {
1447
+ throw new XmlSyntaxError(
1448
+ '"]]>" is not allowed in character data (write ]]&gt;)',
1449
+ lineAt(raw, Math.max(0, close - this.textTail.length), this.line),
1450
+ );
1451
+ }
1452
+ }
1453
+ this.textTail = raw.length >= 2 ? raw.slice(-2) : (this.textTail + raw).slice(-2);
1454
+ this.atStart = false;
1455
+ this.text += this.decode(raw, this.line);
1103
1456
  this.advanceLine(start, end);
1104
1457
  }
1105
1458
 
@@ -1145,16 +1498,49 @@ export class XmlTokenizer {
1145
1498
  * Open an element.
1146
1499
  * @param name - the element name
1147
1500
  * @param attrs - its attributes
1501
+ * @param declaresNamespace - whether an attribute name starts with `xmlns`
1148
1502
  */
1149
- private startElement(name: string, attrs: Map<string, string>): void {
1503
+ private startElement(name: string, attrs: Map<string, string>, declaresNamespace: boolean): void {
1150
1504
  if (this.stack.length === 0) {
1151
1505
  if (this.rootClosed) {
1152
1506
  throw new XmlSyntaxError(`a second root element <${name}> follows the document element`, this.line);
1153
1507
  }
1154
1508
  this.rootSeen = true;
1155
1509
  }
1510
+ if (declaresNamespace) {
1511
+ for (const [key, uri] of attrs) {
1512
+ if (key === "xmlns") {
1513
+ this.bindings.push({ prefix: "", uri, depth: this.stack.length });
1514
+ } else if (key.startsWith("xmlns:")) {
1515
+ this.bindings.push({ prefix: key.slice(6), uri, depth: this.stack.length });
1516
+ }
1517
+ }
1518
+ }
1156
1519
  this.stack.push(name);
1157
- this.handler.start(name, attrs, this.line);
1520
+ this.handler.start(name, attrs, this.line, this.namespaceOf(name));
1521
+ }
1522
+
1523
+ /**
1524
+ * The namespace an element name is in (Namespaces in XML 1.0 section 6).
1525
+ * @param name - the element name as written
1526
+ * @returns the URI; "" for no namespace; null for an undeclared prefix or a name that is not
1527
+ * namespace-well-formed
1528
+ */
1529
+ private namespaceOf(name: string): string | null {
1530
+ const colon = name.indexOf(":");
1531
+ if (colon >= 0 && (colon === 0 || colon === name.length - 1 || name.includes(":", colon + 1))) {
1532
+ return null;
1533
+ }
1534
+ const prefix = colon < 0 ? "" : name.slice(0, colon);
1535
+ for (let i = this.bindings.length - 1; i >= 0; i--) {
1536
+ if (this.bindings[i].prefix === prefix) {
1537
+ return this.bindings[i].uri;
1538
+ }
1539
+ }
1540
+ if (prefix === "xml") {
1541
+ return XML_NAMESPACE;
1542
+ }
1543
+ return prefix === "" ? "" : null;
1158
1544
  }
1159
1545
 
1160
1546
  /**
@@ -1170,6 +1556,9 @@ export class XmlTokenizer {
1170
1556
  throw new XmlSyntaxError(`end tag </${name}> does not match <${open}>`, this.line);
1171
1557
  }
1172
1558
  this.stack.pop();
1559
+ while ((this.bindings.at(-1)?.depth ?? -1) >= this.stack.length) {
1560
+ this.bindings.pop();
1561
+ }
1173
1562
  if (this.stack.length === 0) {
1174
1563
  this.rootClosed = true;
1175
1564
  }
@@ -1178,11 +1567,11 @@ export class XmlTokenizer {
1178
1567
  }
1179
1568
 
1180
1569
  /**
1181
- * Attribute value normalisation (XML 1.0 section 3.3.3): a literal tab or line break becomes a
1570
+ * Attribute value normalization (XML 1.0 section 3.3.3): a literal tab or line break becomes a
1182
1571
  * space; a character reference to one is kept, which is why this runs before entity decoding.
1183
1572
  * @param raw - the value between the quotes
1184
- * @returns the normalised value
1573
+ * @returns the normalized value
1185
1574
  */
1186
1575
  function normalizeAttributeValue(raw: string): string {
1187
- return raw.includes("\n") || raw.includes("\t") ? raw.replace(/[\n\t]/g, " ") : raw;
1576
+ return raw.includes("\n") || raw.includes("\t") ? raw.replaceAll(/[\n\t]/g, " ") : raw;
1188
1577
  }