@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.
- package/README.md +49 -414
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
- package/dist/chunks/choose-DrXtu_96.js +56 -0
- package/dist/chunks/choose-DrXtu_96.js.map +1 -0
- package/dist/chunks/constants-ikG85dT8.js +231 -0
- package/dist/chunks/constants-ikG85dT8.js.map +1 -0
- package/dist/chunks/{escape-B-tkk_Cl.js → escape-D1HPTUrP.js} +390 -99
- package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
- package/dist/chunks/export-CHd4tQE2.js +4333 -0
- package/dist/chunks/export-CHd4tQE2.js.map +1 -0
- package/dist/chunks/{importer-eUREhBGj.js → exporter-D23CKNqF.js} +4969 -3201
- package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
- package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-DfWTgrti.js} +23 -21
- package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
- package/dist/chunks/exporter-DhtW6FxK.js +3465 -0
- package/dist/chunks/exporter-DhtW6FxK.js.map +1 -0
- package/dist/chunks/{exporter-BGrsWamJ.js → exporter-IHv4nkCm.js} +38 -229
- package/dist/chunks/exporter-IHv4nkCm.js.map +1 -0
- package/dist/chunks/{importer-BWY2FFc5.js → importer-B1wmPQjr.js} +255 -77
- package/dist/chunks/importer-B1wmPQjr.js.map +1 -0
- package/dist/chunks/{importer-D_e7e7LX.js → importer-B2W_58pz.js} +399 -146
- package/dist/chunks/importer-B2W_58pz.js.map +1 -0
- package/dist/chunks/importer-BSALvkui.js +2811 -0
- package/dist/chunks/importer-BSALvkui.js.map +1 -0
- package/dist/chunks/{importer-vi3bSdtw.js → importer-BVRE--Un.js} +510 -77
- package/dist/chunks/importer-BVRE--Un.js.map +1 -0
- package/dist/chunks/{importer-DET1tg6s.js → importer-CIAzt-wo.js} +1689 -312
- package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
- package/dist/chunks/{importer-DIrbbnAf.js → importer-DpEW5zeG.js} +585 -226
- package/dist/chunks/importer-DpEW5zeG.js.map +1 -0
- package/dist/chunks/{importer-zGLo8Dg_.js → importer-z_FL4VMb.js} +488 -161
- package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
- package/dist/chunks/json-elements-BAmNBZok.js +1225 -0
- package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
- package/dist/chunks/ontology-export-Bab4eUA8.js +357 -0
- package/dist/chunks/ontology-export-Bab4eUA8.js.map +1 -0
- package/dist/chunks/{records-dbRkxwaq.js → records-CdlvL4RO.js} +298 -114
- package/dist/chunks/records-CdlvL4RO.js.map +1 -0
- package/dist/chunks/{text-Dr0Ifpag.js → text-DzXjAiTl.js} +29 -11
- package/dist/chunks/text-DzXjAiTl.js.map +1 -0
- package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
- package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
- package/dist/csv.js +952 -262
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +6 -3
- package/dist/cx2.js +2 -2
- package/dist/cys.js +6 -3
- package/dist/dot.js +1 -1
- package/dist/gexf.js +554 -122
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +344 -87
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +1105 -213
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +3 -2
- package/dist/graphml.js.map +1 -1
- package/dist/json.js +3 -2
- package/dist/neo4j.js +565 -158
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +6 -3
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts +18 -6
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +6 -3
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts +29 -11
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +25 -7
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/cell-budget.d.ts +9 -2
- package/dist/src/common/cell-budget.d.ts.map +1 -1
- package/dist/src/common/cell-budget.js +11 -3
- package/dist/src/common/cell-budget.js.map +1 -1
- package/dist/src/common/choose.d.ts +19 -0
- package/dist/src/common/choose.d.ts.map +1 -0
- package/dist/src/common/choose.js +72 -0
- package/dist/src/common/choose.js.map +1 -0
- package/dist/src/common/codes.d.ts +456 -66
- package/dist/src/common/codes.d.ts.map +1 -1
- package/dist/src/common/codes.js +458 -66
- package/dist/src/common/codes.js.map +1 -1
- package/dist/src/common/compare.d.ts +81 -0
- package/dist/src/common/compare.d.ts.map +1 -0
- package/dist/src/common/compare.js +394 -0
- package/dist/src/common/compare.js.map +1 -0
- package/dist/src/common/cx-export.d.ts +123 -0
- package/dist/src/common/cx-export.d.ts.map +1 -0
- package/dist/src/common/cx-export.js +334 -0
- package/dist/src/common/cx-export.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +22 -2
- package/dist/src/common/declared-types.d.ts.map +1 -1
- package/dist/src/common/declared-types.js +21 -3
- package/dist/src/common/declared-types.js.map +1 -1
- package/dist/src/common/direction.d.ts +74 -50
- package/dist/src/common/direction.d.ts.map +1 -1
- package/dist/src/common/direction.js +47 -27
- package/dist/src/common/direction.js.map +1 -1
- package/dist/src/common/escape.d.ts +21 -9
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +39 -22
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/common/export.d.ts +144 -32
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +216 -42
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/common/format.d.ts +23 -1
- package/dist/src/common/format.d.ts.map +1 -1
- package/dist/src/common/format.js +26 -1
- package/dist/src/common/format.js.map +1 -1
- package/dist/src/common/ids.d.ts +9 -2
- package/dist/src/common/ids.d.ts.map +1 -1
- package/dist/src/common/ids.js +6 -2
- package/dist/src/common/ids.js.map +1 -1
- package/dist/src/common/input.d.ts +122 -28
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +890 -124
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.d.ts +144 -20
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +869 -189
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/line-format.d.ts +90 -0
- package/dist/src/common/line-format.d.ts.map +1 -0
- package/dist/src/common/line-format.js +185 -0
- package/dist/src/common/line-format.js.map +1 -0
- package/dist/src/common/lists.d.ts +6 -1
- package/dist/src/common/lists.d.ts.map +1 -1
- package/dist/src/common/lists.js +3 -1
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/ontology-export.d.ts +167 -0
- package/dist/src/common/ontology-export.d.ts.map +1 -0
- package/dist/src/common/ontology-export.js +359 -0
- package/dist/src/common/ontology-export.js.map +1 -0
- package/dist/src/common/ontology.d.ts +25 -3
- package/dist/src/common/ontology.d.ts.map +1 -1
- package/dist/src/common/ontology.js +30 -9
- package/dist/src/common/ontology.js.map +1 -1
- package/dist/src/common/options.d.ts +62 -12
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +89 -11
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/plural.d.ts +31 -0
- package/dist/src/common/plural.d.ts.map +1 -0
- package/dist/src/common/plural.js +46 -0
- package/dist/src/common/plural.js.map +1 -0
- package/dist/src/common/report.d.ts +72 -14
- package/dist/src/common/report.d.ts.map +1 -1
- package/dist/src/common/report.js +146 -22
- package/dist/src/common/report.js.map +1 -1
- package/dist/src/common/temporal.d.ts +22 -5
- package/dist/src/common/temporal.d.ts.map +1 -1
- package/dist/src/common/temporal.js +13 -2
- package/dist/src/common/temporal.js.map +1 -1
- package/dist/src/common/text.d.ts +39 -23
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +51 -21
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/weights.d.ts +37 -4
- package/dist/src/common/weights.d.ts.map +1 -1
- package/dist/src/common/weights.js +61 -2
- package/dist/src/common/weights.js.map +1 -1
- package/dist/src/common/writer.d.ts +19 -3
- package/dist/src/common/writer.d.ts.map +1 -1
- package/dist/src/common/writer.js +30 -2
- package/dist/src/common/writer.js.map +1 -1
- package/dist/src/common/xml.d.ts +125 -9
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +402 -85
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts +44 -4
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +134 -5
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts +80 -24
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +146 -33
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/header.d.ts +38 -9
- package/dist/src/formats/csv/header.d.ts.map +1 -1
- package/dist/src/formats/csv/header.js +46 -10
- package/dist/src/formats/csv/header.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts +187 -37
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +835 -204
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/index.d.ts +100 -19
- package/dist/src/formats/csv/index.d.ts.map +1 -1
- package/dist/src/formats/csv/index.js +94 -21
- package/dist/src/formats/csv/index.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts +95 -10
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +333 -122
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/csv/values.d.ts +1 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -1
- package/dist/src/formats/csv/values.js +1 -0
- package/dist/src/formats/csv/values.js.map +1 -1
- package/dist/src/formats/cx/exporter.d.ts +102 -0
- package/dist/src/formats/cx/exporter.d.ts.map +1 -0
- package/dist/src/formats/cx/exporter.js +1217 -0
- package/dist/src/formats/cx/exporter.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts +79 -32
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +682 -287
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx/index.d.ts +4 -3
- package/dist/src/formats/cx/index.d.ts.map +1 -1
- package/dist/src/formats/cx/index.js +4 -3
- package/dist/src/formats/cx/index.js.map +1 -1
- package/dist/src/formats/cx2/exporter.d.ts +29 -11
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +39 -268
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts +78 -28
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +227 -80
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cx2/index.d.ts +2 -1
- package/dist/src/formats/cx2/index.d.ts.map +1 -1
- package/dist/src/formats/cx2/index.js +2 -1
- package/dist/src/formats/cx2/index.js.map +1 -1
- package/dist/src/formats/cys/constants.d.ts +66 -17
- package/dist/src/formats/cys/constants.d.ts.map +1 -1
- package/dist/src/formats/cys/constants.js +59 -18
- package/dist/src/formats/cys/constants.js.map +1 -1
- package/dist/src/formats/cys/exporter.d.ts +118 -0
- package/dist/src/formats/cys/exporter.d.ts.map +1 -0
- package/dist/src/formats/cys/exporter.js +1072 -0
- package/dist/src/formats/cys/exporter.js.map +1 -0
- package/dist/src/formats/cys/importer.d.ts +22 -8
- package/dist/src/formats/cys/importer.d.ts.map +1 -1
- package/dist/src/formats/cys/importer.js +510 -149
- package/dist/src/formats/cys/importer.js.map +1 -1
- package/dist/src/formats/cys/index.d.ts +4 -2
- package/dist/src/formats/cys/index.d.ts.map +1 -1
- package/dist/src/formats/cys/index.js +4 -2
- package/dist/src/formats/cys/index.js.map +1 -1
- package/dist/src/formats/cys/session.d.ts +46 -7
- package/dist/src/formats/cys/session.d.ts.map +1 -1
- package/dist/src/formats/cys/session.js +101 -27
- package/dist/src/formats/cys/session.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts +9 -2
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +29 -7
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts +42 -14
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +56 -29
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts +91 -29
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +190 -39
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/index.d.ts +2 -1
- package/dist/src/formats/dot/index.d.ts.map +1 -1
- package/dist/src/formats/dot/index.js +2 -1
- package/dist/src/formats/dot/index.js.map +1 -1
- package/dist/src/formats/dot/names.d.ts +44 -11
- package/dist/src/formats/dot/names.d.ts.map +1 -1
- package/dist/src/formats/dot/names.js +44 -11
- package/dist/src/formats/dot/names.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +9 -2
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +20 -5
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts +46 -15
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +45 -15
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +87 -23
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +471 -90
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +84 -21
- package/dist/src/formats/gexf/index.d.ts.map +1 -1
- package/dist/src/formats/gexf/index.js +80 -23
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gexf/schema.d.ts +67 -15
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +59 -13
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts +69 -21
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +74 -29
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +116 -25
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +290 -61
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +87 -24
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +79 -26
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts +67 -18
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +110 -28
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/constants.d.ts +174 -44
- package/dist/src/formats/graphml/constants.d.ts.map +1 -1
- package/dist/src/formats/graphml/constants.js +165 -44
- package/dist/src/formats/graphml/constants.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts +17 -9
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +33 -12
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts +19 -7
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +466 -39
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/index.d.ts +2 -1
- package/dist/src/formats/graphml/index.d.ts.map +1 -1
- package/dist/src/formats/graphml/index.js +2 -1
- package/dist/src/formats/graphml/index.js.map +1 -1
- package/dist/src/formats/graphml/tree.d.ts +6 -2
- package/dist/src/formats/graphml/tree.d.ts.map +1 -1
- package/dist/src/formats/graphml/tree.js +6 -2
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +8 -3
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +36 -15
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts +88 -24
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +109 -19
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts +134 -22
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +135 -54
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +173 -52
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +899 -220
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +3 -2
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +3 -2
- package/dist/src/formats/json/index.js.map +1 -1
- package/dist/src/formats/json/obographs-export.d.ts +68 -0
- package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
- package/dist/src/formats/json/obographs-export.js +941 -0
- package/dist/src/formats/json/obographs-export.js.map +1 -0
- package/dist/src/formats/json/obographs.d.ts +26 -0
- package/dist/src/formats/json/obographs.d.ts.map +1 -1
- package/dist/src/formats/json/obographs.js +297 -80
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts +109 -21
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +239 -86
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts +17 -4
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +36 -4
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts +107 -27
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +415 -63
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/neo4j/index.d.ts +73 -23
- package/dist/src/formats/neo4j/index.d.ts.map +1 -1
- package/dist/src/formats/neo4j/index.js +65 -25
- package/dist/src/formats/neo4j/index.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts +150 -0
- package/dist/src/formats/obo/exporter.d.ts.map +1 -0
- package/dist/src/formats/obo/exporter.js +1736 -0
- package/dist/src/formats/obo/exporter.js.map +1 -0
- package/dist/src/formats/obo/importer.d.ts +95 -19
- package/dist/src/formats/obo/importer.d.ts.map +1 -1
- package/dist/src/formats/obo/importer.js +475 -104
- package/dist/src/formats/obo/importer.js.map +1 -1
- package/dist/src/formats/obo/index.d.ts +6 -3
- package/dist/src/formats/obo/index.d.ts.map +1 -1
- package/dist/src/formats/obo/index.js +6 -3
- package/dist/src/formats/obo/index.js.map +1 -1
- package/dist/src/formats/obo/syntax.d.ts +108 -4
- package/dist/src/formats/obo/syntax.d.ts.map +1 -1
- package/dist/src/formats/obo/syntax.js +211 -34
- package/dist/src/formats/obo/syntax.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts +54 -16
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +67 -35
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +84 -22
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +303 -83
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/index.d.ts +2 -1
- package/dist/src/formats/pajek/index.d.ts.map +1 -1
- package/dist/src/formats/pajek/index.js +2 -1
- package/dist/src/formats/pajek/index.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +77 -14
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +186 -54
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts +1 -0
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
- package/dist/src/formats/xgmml/columns.js +4 -0
- package/dist/src/formats/xgmml/columns.js.map +1 -1
- package/dist/src/formats/xgmml/constants.d.ts +186 -56
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
- package/dist/src/formats/xgmml/constants.js +181 -57
- package/dist/src/formats/xgmml/constants.js.map +1 -1
- package/dist/src/formats/xgmml/document.d.ts +65 -11
- package/dist/src/formats/xgmml/document.d.ts.map +1 -1
- package/dist/src/formats/xgmml/document.js +157 -28
- package/dist/src/formats/xgmml/document.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +61 -3
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +129 -26
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts +13 -6
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +21 -22
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts +33 -12
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +35 -72
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/formats/xgmml/index.d.ts +1 -0
- package/dist/src/formats/xgmml/index.d.ts.map +1 -1
- package/dist/src/formats/xgmml/index.js +1 -0
- package/dist/src/formats/xgmml/index.js.map +1 -1
- package/dist/src/formats/xgmml/values.d.ts +8 -1
- package/dist/src/formats/xgmml/values.d.ts.map +1 -1
- package/dist/src/formats/xgmml/values.js +8 -3
- package/dist/src/formats/xgmml/values.js.map +1 -1
- package/dist/src/index.d.ts +23 -18
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +28 -16
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +478 -92
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +807 -88
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +61 -19
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +121 -29
- package/dist/src/sniff.js.map +1 -1
- package/dist/src/types.d.ts +362 -112
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +28 -7
- package/dist/src/types.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/docs/samples/README.md +25 -0
- package/docs/samples/got-edges.csv +353 -0
- package/docs/samples/got-network.graphml +1384 -0
- package/docs/samples/got-nodes.csv +108 -0
- package/docs/samples/got.cx +930 -0
- package/docs/samples/got.cx2 +465 -0
- package/docs/samples/got.gexf +471 -0
- package/docs/samples/got.gml +2650 -0
- package/docs/samples/got.json +2546 -0
- package/docs/samples/got.net +461 -0
- package/docs/samples/movies-nodes.csv +9 -0
- package/docs/samples/movies-rels.csv +8 -0
- package/docs/samples/networks.cys +0 -0
- package/docs/samples/proteins.xgmml +9 -0
- package/docs/samples/teams.gv +24 -0
- package/docs/samples/vehicles.obo +25 -0
- package/package.json +9 -4
- package/src/children.ts +18 -6
- package/src/common/attributes.ts +29 -11
- package/src/common/cell-budget.ts +11 -3
- package/src/common/choose.ts +89 -0
- package/src/common/codes.ts +490 -66
- package/src/common/compare.ts +472 -0
- package/src/common/cx-export.ts +432 -0
- package/src/common/declared-types.ts +40 -6
- package/src/common/direction.ts +78 -51
- package/src/common/escape.ts +39 -22
- package/src/common/export.ts +284 -47
- package/src/common/format.ts +30 -1
- package/src/common/ids.ts +14 -3
- package/src/common/input.ts +1057 -134
- package/src/common/json-elements.ts +1035 -199
- package/src/common/line-format.ts +249 -0
- package/src/common/lists.ts +7 -2
- package/src/common/ontology-export.ts +461 -0
- package/src/common/ontology.ts +31 -9
- package/src/common/options.ts +110 -15
- package/src/common/plural.ts +49 -0
- package/src/common/report.ts +168 -25
- package/src/common/temporal.ts +22 -5
- package/src/common/text.ts +62 -23
- package/src/common/weights.ts +74 -4
- package/src/common/writer.ts +37 -3
- package/src/common/xml.ts +479 -90
- package/src/common/zip.ts +169 -7
- package/src/formats/csv/exporter.ts +202 -47
- package/src/formats/csv/header.ts +50 -11
- package/src/formats/csv/importer.ts +1133 -245
- package/src/formats/csv/index.ts +106 -19
- package/src/formats/csv/records.ts +396 -122
- package/src/formats/csv/values.ts +1 -0
- package/src/formats/cx/exporter.ts +1483 -0
- package/src/formats/cx/importer.ts +890 -302
- package/src/formats/cx/index.ts +4 -3
- package/src/formats/cx2/exporter.ts +62 -339
- package/src/formats/cx2/importer.ts +318 -87
- package/src/formats/cx2/index.ts +2 -1
- package/src/formats/cys/constants.ts +62 -17
- package/src/formats/cys/exporter.ts +1368 -0
- package/src/formats/cys/importer.ts +659 -157
- package/src/formats/cys/index.ts +4 -2
- package/src/formats/cys/session.ts +142 -34
- package/src/formats/cys/tables.ts +47 -9
- package/src/formats/dot/exporter.ts +83 -37
- package/src/formats/dot/importer.ts +248 -54
- package/src/formats/dot/index.ts +2 -1
- package/src/formats/dot/names.ts +44 -11
- package/src/formats/dot/tokenizer.ts +25 -6
- package/src/formats/gexf/exporter.ts +57 -18
- package/src/formats/gexf/importer.ts +636 -105
- package/src/formats/gexf/index.ts +90 -21
- package/src/formats/gexf/schema.ts +67 -15
- package/src/formats/gml/exporter.ts +90 -36
- package/src/formats/gml/importer.ts +388 -67
- package/src/formats/gml/index.ts +85 -24
- package/src/formats/gml/syntax.ts +113 -28
- package/src/formats/graphml/constants.ts +178 -44
- package/src/formats/graphml/exporter.ts +52 -18
- package/src/formats/graphml/importer.ts +640 -60
- package/src/formats/graphml/index.ts +2 -1
- package/src/formats/graphml/tree.ts +6 -2
- package/src/formats/graphml/yfiles.ts +37 -15
- package/src/formats/json/dialect.ts +128 -29
- package/src/formats/json/exporter.ts +205 -68
- package/src/formats/json/importer.ts +1163 -253
- package/src/formats/json/index.ts +3 -1
- package/src/formats/json/obographs-export.ts +1197 -0
- package/src/formats/json/obographs.ts +353 -92
- package/src/formats/neo4j/exporter.ts +321 -102
- package/src/formats/neo4j/header.ts +44 -6
- package/src/formats/neo4j/importer.ts +512 -73
- package/src/formats/neo4j/index.ts +75 -23
- package/src/formats/obo/exporter.ts +2210 -0
- package/src/formats/obo/importer.ts +622 -109
- package/src/formats/obo/index.ts +6 -3
- package/src/formats/obo/syntax.ts +232 -37
- package/src/formats/pajek/exporter.ts +90 -42
- package/src/formats/pajek/importer.ts +407 -93
- package/src/formats/pajek/index.ts +2 -1
- package/src/formats/pajek/syntax.ts +216 -55
- package/src/formats/xgmml/columns.ts +10 -0
- package/src/formats/xgmml/constants.ts +183 -56
- package/src/formats/xgmml/document.ts +212 -34
- package/src/formats/xgmml/emit.ts +175 -34
- package/src/formats/xgmml/exporter.ts +36 -28
- package/src/formats/xgmml/importer.ts +64 -86
- package/src/formats/xgmml/index.ts +1 -0
- package/src/formats/xgmml/values.ts +12 -4
- package/src/index.ts +81 -11
- package/src/registry.ts +1121 -135
- package/src/sniff.ts +163 -32
- package/src/types.ts +385 -114
- package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
- package/dist/chunks/export-Bh60Dv-n.js +0 -582
- package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
- package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
- package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
- package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
- package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
- package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
- package/dist/chunks/importer-DET1tg6s.js.map +0 -1
- package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
- package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
- package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
- package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
- package/dist/chunks/importer-eUREhBGj.js.map +0 -1
- package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
- package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
- package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
- package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
- package/dist/chunks/ontology-BnrJ4I98.js +0 -113
- package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
- package/dist/chunks/records-dbRkxwaq.js.map +0 -1
- package/dist/chunks/report-B1z4WT9e.js +0 -2372
- package/dist/chunks/report-B1z4WT9e.js.map +0 -1
- package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
- package/dist/chunks/weights-Dzba96G3.js +0 -176
- package/dist/chunks/weights-Dzba96G3.js.map +0 -1
- package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
|
@@ -24,37 +24,67 @@ import { declareResolved, uniqueColumnName } from "./attributes.js";
|
|
|
24
24
|
import {
|
|
25
25
|
ASPECT_ORDER_CODE,
|
|
26
26
|
BAD_ASPECT_BLOCK_CODE,
|
|
27
|
+
BAD_VALUE_CODE,
|
|
27
28
|
COLUMN_RENAMED_CODE,
|
|
28
29
|
COUNT_MISMATCH_CODE,
|
|
30
|
+
DUPLICATE_ATTRIBUTE_CODE,
|
|
31
|
+
JSON_NONSTANDARD_NUMBER_CODE,
|
|
32
|
+
MULTI_ASPECT_FRAGMENT_CODE,
|
|
33
|
+
PRECISION_CODE,
|
|
29
34
|
STATUS_FAILED_CODE,
|
|
30
35
|
STATUS_WARNING_CODE,
|
|
31
36
|
} from "./codes.js";
|
|
37
|
+
import { agree, plural } from "./plural.js";
|
|
32
38
|
import { type ImportReportBuilder } from "./report.js";
|
|
39
|
+
import { trimTrailingZeros } from "./text.js";
|
|
33
40
|
|
|
34
41
|
// ============================================================ exact integers
|
|
35
42
|
|
|
36
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* A run of 16 digits not inside a fraction: the shortest integer literal that can exceed 2^53 (9007199254740992).
|
|
45
|
+
* @category Plugin helpers
|
|
46
|
+
*/
|
|
37
47
|
export const MAYBE_UNSAFE_INTEGER = /(?<![0-9.])[0-9]{16}/;
|
|
38
48
|
|
|
39
49
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
50
|
+
* A literal the double read for may not equal: an exponent of 15 or more (`9.007199254740993e15`,
|
|
51
|
+
* `1e400`: an integer beyond 2^53 or an overflow), an exponent of -300 or less (`1e-400`: an
|
|
52
|
+
* underflow to 0), or 16 or more digits split by a decimal point before an exponent
|
|
53
|
+
* (`90071992547409.93e2`). MAYBE_UNSAFE_INTEGER sees none of them.
|
|
54
|
+
* ponytail: a literal that underflows through 300+ leading fraction zeros and no exponent is not
|
|
55
|
+
* gated; add a digit-count check if such files turn up.
|
|
56
|
+
* @category Plugin helpers
|
|
42
57
|
*/
|
|
43
|
-
const
|
|
58
|
+
export const MAYBE_INEXACT_EXPONENT = new RegExp(
|
|
59
|
+
[
|
|
60
|
+
/\d[eE]\+?0*(?:1[5-9]|[2-9]\d|[1-9]\d{2,})/, // an exponent of 15 or more
|
|
61
|
+
/\d[eE]-0*(?:[3-9]\d{2}|[1-9]\d{3,})/, // an exponent of -300 or less
|
|
62
|
+
/(?<![\d.])(?=(?:\d\.?){16})\d+\.\d+[eE]/, // 16 or more digits split by a decimal point
|
|
63
|
+
]
|
|
64
|
+
.map((r) => r.source)
|
|
65
|
+
.join("|"),
|
|
66
|
+
);
|
|
44
67
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The prefix of the string a non-standard token or an exact integer is rewritten to (a NUL
|
|
70
|
+
* character first, which no sensible attribute value starts with). A document that already holds
|
|
71
|
+
* the text gets a numbered variant, so a genuine string is never revived as a number.
|
|
72
|
+
*/
|
|
73
|
+
const SENTINEL = `${String.fromCodePoint(0)}graph-io`;
|
|
47
74
|
|
|
48
75
|
/** The non-standard tokens Python's json module writes, longest first so -Infinity wins over a bare minus. */
|
|
49
76
|
const NONSTANDARD_TOKENS: readonly (readonly [string, number])[] = [
|
|
50
77
|
["-Infinity", -Infinity],
|
|
51
78
|
["Infinity", Infinity],
|
|
52
|
-
["NaN", NaN],
|
|
79
|
+
["NaN", Number.NaN],
|
|
53
80
|
];
|
|
54
81
|
|
|
55
82
|
/** A JSON integer literal (no fraction, no exponent, no leading zero), as CANONICAL_INTEGER in common/ids.ts. */
|
|
56
83
|
const INTEGER_LITERAL = /^-?(0|[1-9][0-9]*)$/;
|
|
57
84
|
|
|
85
|
+
/** A JSON number literal, split into sign, integer digits, fraction digits and exponent. */
|
|
86
|
+
const NUMBER_PARTS = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
|
|
87
|
+
|
|
58
88
|
/** What rewriteNumbers() found. */
|
|
59
89
|
interface RewrittenNumbers {
|
|
60
90
|
/** The rewritten text. */
|
|
@@ -63,19 +93,106 @@ interface RewrittenNumbers {
|
|
|
63
93
|
readonly tokens: Set<string>;
|
|
64
94
|
/** The integer literals beyond 2^53 that were quoted. */
|
|
65
95
|
readonly bigIntegers: string[];
|
|
96
|
+
/**
|
|
97
|
+
* The literals with a fraction or an exponent that denote an integer beyond 2^53 a double cannot
|
|
98
|
+
* hold (`9007199254740993.0`), and the finite literals that overflow to an infinity (`1e400`) or
|
|
99
|
+
* underflow to 0 (`1e-400`):
|
|
100
|
+
* read as the nearest double, which changes the value.
|
|
101
|
+
*/
|
|
102
|
+
readonly inexact: string[];
|
|
103
|
+
/** The JSON.parse reviver that turns the sentinel strings of this rewrite back into numbers. */
|
|
104
|
+
readonly revive: (key: string, value: unknown) => unknown;
|
|
105
|
+
/** The JSON.parse reviver that turns this rewrite's exact sentinels into ExactInteger. */
|
|
106
|
+
readonly reviveExact: (key: string, value: unknown) => unknown;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The sentinel prefix of one rewrite: SENTINEL with a colon, or a numbered variant when the document
|
|
111
|
+
* already holds that one. A string can only decode to a NUL by escaping it (`\u0000`), so when the
|
|
112
|
+
* text holds that escape the escaped strings are decoded and searched too: any other escape in them
|
|
113
|
+
* (`graph\u002dio:`) would hide the sentinel from a search of the raw text.
|
|
114
|
+
* @param text - the document text
|
|
115
|
+
* @returns the prefix
|
|
116
|
+
*/
|
|
117
|
+
function sentinelFor(text: string): string {
|
|
118
|
+
let haystack = text;
|
|
119
|
+
if (/\\u0000/i.test(text)) {
|
|
120
|
+
const decoded: string[] = [];
|
|
121
|
+
for (const [literal] of text.matchAll(/"(?:[^"\\]|\\.)*"/g)) {
|
|
122
|
+
if (literal.includes("\\")) {
|
|
123
|
+
decoded.push(decodeString(literal));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
haystack = `${decoded.join("\n")}\n${text}`;
|
|
127
|
+
}
|
|
128
|
+
let k = 0;
|
|
129
|
+
while (haystack.includes(`graph-io${k === 0 ? "" : String(k)}:`)) {
|
|
130
|
+
k++;
|
|
131
|
+
}
|
|
132
|
+
return `${SENTINEL}${k === 0 ? "" : String(k)}:`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Decode one JSON string literal, or give it back as written when it is not valid (the parse then
|
|
137
|
+
* reports the syntax error).
|
|
138
|
+
* @param literal - the literal with its quotes
|
|
139
|
+
* @returns the decoded value
|
|
140
|
+
*/
|
|
141
|
+
function decodeString(literal: string): string {
|
|
142
|
+
try {
|
|
143
|
+
return String(JSON.parse(literal));
|
|
144
|
+
} catch {
|
|
145
|
+
return literal;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether a number literal with a fraction or an exponent reads back as another value: an integer
|
|
151
|
+
* beyond 2^53 written with more significant digits than a double keeps, which the nearest double
|
|
152
|
+
* misses, or a finite literal beyond the double range or a non-zero one below it.
|
|
153
|
+
* @param literal - the literal as written
|
|
154
|
+
* @returns true when the double read for it is not the value it denotes
|
|
155
|
+
*/
|
|
156
|
+
function isInexactLiteral(literal: string): boolean {
|
|
157
|
+
const value = Number(literal);
|
|
158
|
+
if (!Number.isFinite(value)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (value === 0) {
|
|
162
|
+
// a non-zero literal below the smallest double underflows to 0
|
|
163
|
+
return /[1-9]/.test(literal.split(/[eE]/)[0]);
|
|
164
|
+
}
|
|
165
|
+
if (Number.isSafeInteger(value) || !Number.isInteger(value)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const m = NUMBER_PARTS.exec(literal);
|
|
169
|
+
if (m === null) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const digits = trimTrailingZeros(m[2] + (m[3] ?? ""));
|
|
173
|
+
const exponent = Number(m[4] ?? "0") - (m[3] ?? "").length + ((m[2] + (m[3] ?? "")).length - digits.length);
|
|
174
|
+
if (digits.replace(/^0+/, "").length <= 15 || exponent < 0) {
|
|
175
|
+
// up to 15 significant digits the nearest double reads back as the same text (1e39 is the
|
|
176
|
+
// float it says); a fraction that rounds to an integer is ordinary floating-point rounding
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const exact = BigInt(digits) * 10n ** BigInt(exponent);
|
|
180
|
+
return exact !== BigInt(Math.abs(value));
|
|
66
181
|
}
|
|
67
182
|
|
|
68
183
|
/**
|
|
69
184
|
* Rewrite the numbers JSON.parse cannot read exactly, outside strings and in value positions only:
|
|
70
185
|
* NaN / Infinity / -Infinity become sentinel strings (when `nonstandard` is true), an integer
|
|
71
186
|
* literal that is not a safe integer becomes a string of its digits (or, with `exactSentinel`, a
|
|
72
|
-
* sentinel string reviveExact
|
|
73
|
-
* position (after `:`, `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays
|
|
187
|
+
* sentinel string the result's reviveExact turns into an ExactInteger). A container stack tells a
|
|
188
|
+
* value position (after `:`, `[`, or `,` inside an array) from a key position, so `{NaN: 1}` stays
|
|
189
|
+
* invalid. Literals with a fraction or an exponent that no double holds are listed, not rewritten.
|
|
74
190
|
* @param text - the document text
|
|
75
191
|
* @param options - which rewrites apply
|
|
76
192
|
* @param options.nonstandard - rewrite NaN / Infinity / -Infinity (default true)
|
|
77
193
|
* @param options.exactSentinel - quote big integers as sentinels instead of bare digits (default false)
|
|
78
|
-
* @returns the rewritten text,
|
|
194
|
+
* @returns the rewritten text, what was found and the revivers of this rewrite
|
|
195
|
+
* @category Plugin helpers
|
|
79
196
|
*/
|
|
80
197
|
export function rewriteNumbers(
|
|
81
198
|
text: string,
|
|
@@ -83,9 +200,12 @@ export function rewriteNumbers(
|
|
|
83
200
|
): RewrittenNumbers {
|
|
84
201
|
const nonstandard = options.nonstandard ?? true;
|
|
85
202
|
const exactSentinel = options.exactSentinel ?? false;
|
|
203
|
+
const sentinel = sentinelFor(text);
|
|
204
|
+
const exactPrefix = `${sentinel}exact:`;
|
|
86
205
|
const parts: string[] = [];
|
|
87
206
|
const tokens = new Set<string>();
|
|
88
207
|
const bigIntegers: string[] = [];
|
|
208
|
+
const inexact: string[] = [];
|
|
89
209
|
const arrays: boolean[] = [];
|
|
90
210
|
let expectValue = true;
|
|
91
211
|
let copied = 0;
|
|
@@ -120,16 +240,20 @@ export function rewriteNumbers(
|
|
|
120
240
|
if (token !== undefined) {
|
|
121
241
|
end = i + token[0].length;
|
|
122
242
|
tokens.add(token[0]);
|
|
123
|
-
replacement = JSON.stringify(`${
|
|
243
|
+
replacement = JSON.stringify(`${sentinel}${token[0]}`);
|
|
124
244
|
} else if (ch === "-" || (ch >= "0" && ch <= "9")) {
|
|
125
245
|
end = i + 1;
|
|
126
246
|
while (end < n && "0123456789+-.eE".includes(text[end])) {
|
|
127
247
|
end++;
|
|
128
248
|
}
|
|
129
249
|
const literal = text.slice(i, end);
|
|
130
|
-
if (INTEGER_LITERAL.test(literal)
|
|
131
|
-
|
|
132
|
-
|
|
250
|
+
if (INTEGER_LITERAL.test(literal)) {
|
|
251
|
+
if (!Number.isSafeInteger(Number(literal))) {
|
|
252
|
+
bigIntegers.push(literal);
|
|
253
|
+
replacement = exactSentinel ? JSON.stringify(`${exactPrefix}${literal}`) : `"${literal}"`;
|
|
254
|
+
}
|
|
255
|
+
} else if (isInexactLiteral(literal)) {
|
|
256
|
+
inexact.push(literal);
|
|
133
257
|
}
|
|
134
258
|
}
|
|
135
259
|
if (replacement !== null) {
|
|
@@ -142,25 +266,185 @@ export function rewriteNumbers(
|
|
|
142
266
|
i++;
|
|
143
267
|
}
|
|
144
268
|
parts.push(text.slice(copied));
|
|
145
|
-
|
|
269
|
+
const revive = (_key: string, value: unknown): unknown => {
|
|
270
|
+
if (typeof value === "string" && value.startsWith(sentinel)) {
|
|
271
|
+
const found = NONSTANDARD_TOKENS.find(([word]) => word === value.slice(sentinel.length));
|
|
272
|
+
return found === undefined ? value : found[1];
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
};
|
|
276
|
+
const reviveExact = (_key: string, value: unknown): unknown =>
|
|
277
|
+
typeof value === "string" && value.startsWith(exactPrefix)
|
|
278
|
+
? new ExactInteger(value.slice(exactPrefix.length))
|
|
279
|
+
: value;
|
|
280
|
+
return { text: parts.join(""), tokens, bigIntegers, inexact, revive, reviveExact };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** One key repeated in a JSON object. */
|
|
284
|
+
interface DuplicateKey {
|
|
285
|
+
/** The key. */
|
|
286
|
+
readonly key: string;
|
|
287
|
+
/** The UTF-16 offset of the repeated occurrence. */
|
|
288
|
+
readonly offset: number;
|
|
146
289
|
}
|
|
147
290
|
|
|
291
|
+
/** Keys of one object compared pairwise up to this count; a wider object switches to a Set. */
|
|
292
|
+
const LINEAR_KEYS = 32;
|
|
293
|
+
|
|
148
294
|
/**
|
|
149
|
-
* The
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
295
|
+
* The keys that occur twice in one object of a valid JSON text, which JSON.parse silently reduces
|
|
296
|
+
* to the last value (RFC 8259 section 4: names SHOULD be unique). One pass over the characters
|
|
297
|
+
* that jumps over strings; the keys of a small object are compared in place, without
|
|
298
|
+
* slicing (the parse itself costs about as much as this scan, so it allocates nothing per key).
|
|
299
|
+
* Keys are compared as written: `"\u0069d"` and `"id"` are not recognized as one key.
|
|
300
|
+
* @param text - a text JSON.parse accepted (or its rewrite)
|
|
301
|
+
* @returns every repetition in document order
|
|
302
|
+
* @category Plugin helpers
|
|
154
303
|
*/
|
|
155
|
-
export function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
304
|
+
export function findDuplicateKeys(text: string): DuplicateKey[] {
|
|
305
|
+
const found: DuplicateKey[] = [];
|
|
306
|
+
// per depth: the [start, end) of each key of the open object (reused), or -1 for an array
|
|
307
|
+
const spans: number[][] = [];
|
|
308
|
+
const wide: (Set<string> | null)[] = [];
|
|
309
|
+
let depth = 0;
|
|
310
|
+
const n = text.length;
|
|
311
|
+
let at = 0;
|
|
312
|
+
while (at < n) {
|
|
313
|
+
const c = text.codePointAt(at);
|
|
314
|
+
if (c === 123 || c === 91) {
|
|
315
|
+
// { or [
|
|
316
|
+
openContainer(spans, wide, depth, c === 91);
|
|
317
|
+
depth++;
|
|
318
|
+
} else if (c === 125 || c === 93) {
|
|
319
|
+
depth--;
|
|
320
|
+
} else if (c === 34) {
|
|
321
|
+
const end = closingQuote(text, at + 1);
|
|
322
|
+
const keys = depth > 0 ? spans[depth - 1] : null;
|
|
323
|
+
// an array's span list starts with -1: its strings are values, never keys
|
|
324
|
+
const isObjectKey = keys !== null && keys[0] !== -1 && isKey(text, end + 1);
|
|
325
|
+
if (isObjectKey && isRepeated(text, at + 1, end, keys, wide, depth - 1)) {
|
|
326
|
+
found.push({ key: text.slice(at + 1, end), offset: at });
|
|
327
|
+
}
|
|
328
|
+
at = end;
|
|
329
|
+
}
|
|
330
|
+
at++;
|
|
159
331
|
}
|
|
160
|
-
return
|
|
332
|
+
return found;
|
|
161
333
|
}
|
|
162
334
|
|
|
163
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* Start the key list of a container just opened at a depth (reusing the list of an earlier one).
|
|
337
|
+
* @param spans - the key spans per depth
|
|
338
|
+
* @param wide - the key sets per depth, for wide objects
|
|
339
|
+
* @param depth - the container's depth
|
|
340
|
+
* @param isArray - whether it is an array, whose strings are never keys
|
|
341
|
+
*/
|
|
342
|
+
function openContainer(spans: number[][], wide: (Set<string> | null)[], depth: number, isArray: boolean): void {
|
|
343
|
+
if (spans.length === depth) {
|
|
344
|
+
spans.push([]);
|
|
345
|
+
wide.push(null);
|
|
346
|
+
}
|
|
347
|
+
spans[depth].length = 0;
|
|
348
|
+
if (isArray) {
|
|
349
|
+
spans[depth].push(-1);
|
|
350
|
+
}
|
|
351
|
+
wide[depth] = null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Whether a key repeats one already seen in its object, remembering it otherwise.
|
|
356
|
+
* @param text - the text
|
|
357
|
+
* @param start - the key's first character
|
|
358
|
+
* @param end - the key's closing quote
|
|
359
|
+
* @param keys - the spans of the object's keys so far
|
|
360
|
+
* @param wide - the Set of each depth's keys once the object is wide
|
|
361
|
+
* @param level - the object's depth
|
|
362
|
+
* @returns true for a repetition
|
|
363
|
+
*/
|
|
364
|
+
function isRepeated(
|
|
365
|
+
text: string,
|
|
366
|
+
start: number,
|
|
367
|
+
end: number,
|
|
368
|
+
keys: number[],
|
|
369
|
+
wide: (Set<string> | null)[],
|
|
370
|
+
level: number,
|
|
371
|
+
): boolean {
|
|
372
|
+
const set = wide[level];
|
|
373
|
+
if (set !== null) {
|
|
374
|
+
const key = text.slice(start, end);
|
|
375
|
+
return set.has(key) || (set.add(key), false);
|
|
376
|
+
}
|
|
377
|
+
const length = end - start;
|
|
378
|
+
for (let k = 0; k < keys.length; k += 2) {
|
|
379
|
+
if (keys[k + 1] - keys[k] === length && sameText(text, keys[k], start, length)) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
keys.push(start, end);
|
|
384
|
+
if (keys.length > 2 * LINEAR_KEYS) {
|
|
385
|
+
const all = new Set<string>();
|
|
386
|
+
for (let k = 0; k < keys.length; k += 2) {
|
|
387
|
+
all.add(text.slice(keys[k], keys[k + 1]));
|
|
388
|
+
}
|
|
389
|
+
wide[level] = all;
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Whether two ranges of one text hold the same characters.
|
|
396
|
+
* @param text - the text
|
|
397
|
+
* @param a - the first range's start
|
|
398
|
+
* @param b - the second range's start
|
|
399
|
+
* @param length - the ranges' length
|
|
400
|
+
* @returns true when equal
|
|
401
|
+
*/
|
|
402
|
+
function sameText(text: string, a: number, b: number, length: number): boolean {
|
|
403
|
+
for (let i = 0; i < length; i++) {
|
|
404
|
+
if (text.codePointAt(a + i) !== text.codePointAt(b + i)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The offset of the quote that closes a JSON string.
|
|
413
|
+
* @param text - the text
|
|
414
|
+
* @param from - the offset after the opening quote
|
|
415
|
+
* @returns the closing quote's offset (the text length when there is none)
|
|
416
|
+
*/
|
|
417
|
+
function closingQuote(text: string, from: number): number {
|
|
418
|
+
for (let q = text.indexOf('"', from); q >= 0; q = text.indexOf('"', q + 1)) {
|
|
419
|
+
let backslashes = 0;
|
|
420
|
+
while (text.codePointAt(q - 1 - backslashes) === 92) {
|
|
421
|
+
backslashes++;
|
|
422
|
+
}
|
|
423
|
+
if (backslashes % 2 === 0) {
|
|
424
|
+
return q;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return text.length;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Whether the string that ends before an offset is an object key: a colon follows after whitespace.
|
|
432
|
+
* @param text - the text
|
|
433
|
+
* @param from - the offset after the closing quote
|
|
434
|
+
* @returns true for a key
|
|
435
|
+
*/
|
|
436
|
+
function isKey(text: string, from: number): boolean {
|
|
437
|
+
let i = from;
|
|
438
|
+
while (i < text.length && isSpace(text.codePointAt(i))) {
|
|
439
|
+
i++;
|
|
440
|
+
}
|
|
441
|
+
return text.codePointAt(i) === 58;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* An integer literal beyond 2^53, kept as its exact digits (a CX id must never lose a digit).
|
|
446
|
+
* @category Plugin helpers
|
|
447
|
+
*/
|
|
164
448
|
export class ExactInteger {
|
|
165
449
|
/** The digits, with a leading minus for a negative value. */
|
|
166
450
|
readonly digits: string;
|
|
@@ -182,23 +466,11 @@ export class ExactInteger {
|
|
|
182
466
|
}
|
|
183
467
|
}
|
|
184
468
|
|
|
185
|
-
/**
|
|
186
|
-
* The JSON.parse reviver of an element whose big integers were rewritten as exact sentinels.
|
|
187
|
-
* @param _key - the member key (unused)
|
|
188
|
-
* @param value - the parsed value
|
|
189
|
-
* @returns an ExactInteger for an exact sentinel, the value otherwise
|
|
190
|
-
*/
|
|
191
|
-
function reviveExact(_key: string, value: unknown): unknown {
|
|
192
|
-
if (typeof value === "string" && value.startsWith(EXACT_SENTINEL)) {
|
|
193
|
-
return new ExactInteger(value.slice(EXACT_SENTINEL.length));
|
|
194
|
-
}
|
|
195
|
-
return value;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
469
|
/**
|
|
199
470
|
* Parse one JSON value, keeping integer literals beyond 2^53 as ExactInteger.
|
|
200
471
|
* @param text - the JSON text
|
|
201
472
|
* @returns the value and whether it may hold an ExactInteger; SyntaxError when it is not JSON
|
|
473
|
+
* @category Plugin helpers
|
|
202
474
|
*/
|
|
203
475
|
export function parseExact(text: string): { readonly value: unknown; readonly exact: boolean } {
|
|
204
476
|
if (!MAYBE_UNSAFE_INTEGER.test(text)) {
|
|
@@ -208,7 +480,7 @@ export function parseExact(text: string): { readonly value: unknown; readonly ex
|
|
|
208
480
|
if (scan.bigIntegers.length === 0) {
|
|
209
481
|
return { value: JSON.parse(text) as unknown, exact: false };
|
|
210
482
|
}
|
|
211
|
-
return { value: JSON.parse(scan.text, reviveExact) as unknown, exact: true };
|
|
483
|
+
return { value: JSON.parse(scan.text, scan.reviveExact) as unknown, exact: true };
|
|
212
484
|
}
|
|
213
485
|
|
|
214
486
|
/**
|
|
@@ -217,6 +489,7 @@ export function parseExact(text: string): { readonly value: unknown; readonly ex
|
|
|
217
489
|
* @param value - the value
|
|
218
490
|
* @param onPrecision - called with the digits of each integer that lost precision
|
|
219
491
|
* @returns the plain JSON value
|
|
492
|
+
* @category Plugin helpers
|
|
220
493
|
*/
|
|
221
494
|
export function plainJson(value: unknown, onPrecision?: (digits: string) => void): unknown {
|
|
222
495
|
if (value instanceof ExactInteger) {
|
|
@@ -229,16 +502,236 @@ export function plainJson(value: unknown, onPrecision?: (digits: string) => void
|
|
|
229
502
|
if (typeof value === "object" && value !== null) {
|
|
230
503
|
const out: Record<string, unknown> = {};
|
|
231
504
|
for (const [key, item] of Object.entries(value)) {
|
|
232
|
-
out
|
|
505
|
+
setOwn(out, key, plainJson(item, onPrecision));
|
|
233
506
|
}
|
|
234
507
|
return out;
|
|
235
508
|
}
|
|
236
509
|
return value;
|
|
237
510
|
}
|
|
238
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Set an own property, also for the key `__proto__` (which an assignment would turn into the
|
|
514
|
+
* object's prototype, losing the entry).
|
|
515
|
+
* @param target - the object
|
|
516
|
+
* @param key - the key
|
|
517
|
+
* @param value - the value
|
|
518
|
+
* @category Plugin helpers
|
|
519
|
+
*/
|
|
520
|
+
export function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
521
|
+
if (key === "__proto__") {
|
|
522
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
523
|
+
} else {
|
|
524
|
+
target[key] = value;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Parse a text with the bare tokens NaN / Infinity / -Infinity that Python's json writes.
|
|
530
|
+
* @param text - the JSON text that JSON.parse refused
|
|
531
|
+
* @returns the value, whether it may hold an ExactInteger and the tokens seen; null when the
|
|
532
|
+
* tokens are not what made the text invalid
|
|
533
|
+
*/
|
|
534
|
+
function parseNonstandard(
|
|
535
|
+
text: string,
|
|
536
|
+
): { readonly value: unknown; readonly exact: boolean; readonly tokens: ReadonlySet<string> } | null {
|
|
537
|
+
if (!/NaN|Infinity/.test(text)) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
const scan = rewriteNumbers(text, { nonstandard: true, exactSentinel: true });
|
|
541
|
+
if (scan.tokens.size === 0) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const value = JSON.parse(scan.text, (key, raw) => scan.reviveExact(key, scan.revive(key, raw))) as unknown;
|
|
546
|
+
return { value, exact: scan.bigIntegers.length > 0, tokens: scan.tokens };
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** A lone surrogate code unit: a high one not followed by a low one, or a low one not after a high one. */
|
|
553
|
+
/** The escapes of the surrogate range, as a message writes them. */
|
|
554
|
+
const SURROGATE_ESCAPES = String.raw`\uD800-\uDFFF`;
|
|
555
|
+
|
|
556
|
+
const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
|
|
557
|
+
|
|
558
|
+
/** Text that may hold a surrogate: an escaped one, or a raw one (string input). */
|
|
559
|
+
const MAYBE_SURROGATE = /\\u[dD][89a-fA-F]|[\ud800-\udfff]/;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Replace every lone surrogate of a parsed value's strings and keys with U+FFFD, in place.
|
|
563
|
+
* @param value - the parsed value
|
|
564
|
+
* @param fixed - counts the strings repaired and names the keys two repaired keys collapse into
|
|
565
|
+
* @param fixed.count - the count
|
|
566
|
+
* @param fixed.collisions - a key one object holds twice once repaired (the later value is kept)
|
|
567
|
+
* @returns the value (a string, or an object with a repaired key, is returned as a new value)
|
|
568
|
+
*/
|
|
569
|
+
function repairSurrogates(value: unknown, fixed: { count: number; collisions: string[] }): unknown {
|
|
570
|
+
if (typeof value === "string") {
|
|
571
|
+
const repaired = value.replaceAll(LONE_SURROGATE, "\ufffd");
|
|
572
|
+
if (repaired !== value) {
|
|
573
|
+
fixed.count++;
|
|
574
|
+
}
|
|
575
|
+
return repaired;
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(value)) {
|
|
578
|
+
for (let i = 0; i < value.length; i++) {
|
|
579
|
+
value[i] = repairSurrogates(value[i], fixed);
|
|
580
|
+
}
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
if (isRecord(value)) {
|
|
584
|
+
const keys = Object.keys(value);
|
|
585
|
+
const repairedKeys = keys.map((key) => key.replaceAll(LONE_SURROGATE, "\ufffd"));
|
|
586
|
+
const renamed = repairedKeys.some((key, i) => key !== keys[i]);
|
|
587
|
+
const out: Record<string, unknown> = renamed ? {} : value;
|
|
588
|
+
keys.forEach((key, i) => {
|
|
589
|
+
if (repairedKeys[i] !== key) {
|
|
590
|
+
fixed.count++;
|
|
591
|
+
}
|
|
592
|
+
if (renamed && Object.hasOwn(out, repairedKeys[i])) {
|
|
593
|
+
fixed.collisions.push(repairedKeys[i]);
|
|
594
|
+
}
|
|
595
|
+
setOwn(out, repairedKeys[i], repairSurrogates(value[key], fixed));
|
|
596
|
+
});
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* The number of keys of a parsed value, at every depth.
|
|
604
|
+
* @param value - the value
|
|
605
|
+
* @returns the count
|
|
606
|
+
*/
|
|
607
|
+
function keyCount(value: unknown): number {
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
return value.reduce<number>((n, item) => n + keyCount(item), 0);
|
|
610
|
+
}
|
|
611
|
+
if (isRecord(value)) {
|
|
612
|
+
return Object.keys(value).reduce((n, key) => n + 1 + keyCount(value[key]), 0);
|
|
613
|
+
}
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* The first key an object of a JSON text holds twice (JSON.parse keeps the later value silently).
|
|
619
|
+
* @param text - valid JSON text
|
|
620
|
+
* @returns the key, or null
|
|
621
|
+
*/
|
|
622
|
+
function duplicateKey(text: string): string | null {
|
|
623
|
+
const stack: (Set<string> | null)[] = [];
|
|
624
|
+
let i = 0;
|
|
625
|
+
while (i < text.length) {
|
|
626
|
+
const c = text[i];
|
|
627
|
+
if (c === "{") {
|
|
628
|
+
stack.push(new Set());
|
|
629
|
+
} else if (c === "[") {
|
|
630
|
+
stack.push(null);
|
|
631
|
+
} else if (c === "}" || c === "]") {
|
|
632
|
+
stack.pop();
|
|
633
|
+
} else if (c === '"') {
|
|
634
|
+
const start = i;
|
|
635
|
+
i = stringEnd(text, i + 1);
|
|
636
|
+
const keys = stack.at(-1) ?? null;
|
|
637
|
+
if (keys !== null && text[skipSpace(text, i + 1)] === ":") {
|
|
638
|
+
const key = JSON.parse(text.slice(start, i + 1)) as string;
|
|
639
|
+
if (keys.has(key)) {
|
|
640
|
+
return key;
|
|
641
|
+
}
|
|
642
|
+
keys.add(key);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
i++;
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* The offset of the quote that ends a JSON string, skipping escaped characters.
|
|
652
|
+
* @param text - the text
|
|
653
|
+
* @param from - the offset after the opening quote
|
|
654
|
+
* @returns the closing quote's offset, or the text length (or one past it) when there is none
|
|
655
|
+
*/
|
|
656
|
+
function stringEnd(text: string, from: number): number {
|
|
657
|
+
let i = from;
|
|
658
|
+
while (i < text.length && text[i] !== '"') {
|
|
659
|
+
i += text[i] === "\\" ? 2 : 1;
|
|
660
|
+
}
|
|
661
|
+
return i;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The index of the first character at or after `from` that is not JSON whitespace.
|
|
666
|
+
* @param text - the text
|
|
667
|
+
* @param from - the first index
|
|
668
|
+
* @returns the index (text.length at the end)
|
|
669
|
+
*/
|
|
670
|
+
function skipSpace(text: string, from: number): number {
|
|
671
|
+
let i = from;
|
|
672
|
+
while (i < text.length && isSpace(text.codePointAt(i))) {
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
return i;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** A key followed by its colon, for the cheap duplicate-key check. */
|
|
679
|
+
const KEY_COLON = /"\s*:/g;
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* The checks of a parsed element that JSON.parse cannot make, recorded in the report: lone
|
|
683
|
+
* surrogates (replaced with U+FFFD, E_BAD_VALUE) and a key held twice by one object
|
|
684
|
+
* (W_DUPLICATE_ATTRIBUTE; the later value is the one read, also for two keys that a repair makes one).
|
|
685
|
+
* @param text - the element text
|
|
686
|
+
* @param parsed - the parsed element
|
|
687
|
+
* @param line - its line
|
|
688
|
+
* @param report - the report
|
|
689
|
+
* @returns the value, repaired
|
|
690
|
+
*/
|
|
691
|
+
function checkParsed(text: string, parsed: unknown, line: number, report: ImportReportBuilder): unknown {
|
|
692
|
+
let value = parsed;
|
|
693
|
+
if (MAYBE_SURROGATE.test(text)) {
|
|
694
|
+
const fixed = { count: 0, collisions: [] as string[] };
|
|
695
|
+
value = repairSurrogates(value, fixed);
|
|
696
|
+
for (const key of fixed.collisions) {
|
|
697
|
+
report.warnOnce(
|
|
698
|
+
"validation-error",
|
|
699
|
+
DUPLICATE_ATTRIBUTE_CODE,
|
|
700
|
+
`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`,
|
|
701
|
+
{ line, element: key },
|
|
702
|
+
`${DUPLICATE_ATTRIBUTE_CODE}:json:${key}`,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (fixed.count > 0) {
|
|
706
|
+
report.error(
|
|
707
|
+
"validation-error",
|
|
708
|
+
BAD_VALUE_CODE,
|
|
709
|
+
`${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`,
|
|
710
|
+
{ line },
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (typeof value === "object" && value !== null && (text.match(KEY_COLON)?.length ?? 0) > keyCount(value)) {
|
|
715
|
+
const key = duplicateKey(text);
|
|
716
|
+
if (key !== null) {
|
|
717
|
+
report.warnOnce(
|
|
718
|
+
"validation-error",
|
|
719
|
+
DUPLICATE_ATTRIBUTE_CODE,
|
|
720
|
+
`an object holds the key ${JSON.stringify(key)} twice; the later value is read`,
|
|
721
|
+
{ line, element: key },
|
|
722
|
+
`${DUPLICATE_ATTRIBUTE_CODE}:json:${key}`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return value;
|
|
727
|
+
}
|
|
728
|
+
|
|
239
729
|
// ============================================================ the streaming aspect scanner
|
|
240
730
|
|
|
241
|
-
/**
|
|
731
|
+
/**
|
|
732
|
+
* A problem with the document's JSON; the importer turns it into a fatal E_SYNTAX or E_EMPTY_INPUT.
|
|
733
|
+
* @category Plugin helpers
|
|
734
|
+
*/
|
|
242
735
|
export class JsonScanError extends Error {
|
|
243
736
|
/** The 1-based line of the problem. */
|
|
244
737
|
readonly line: number;
|
|
@@ -260,12 +753,19 @@ export class JsonScanError extends Error {
|
|
|
260
753
|
}
|
|
261
754
|
}
|
|
262
755
|
|
|
263
|
-
/**
|
|
756
|
+
/**
|
|
757
|
+
* One thing the scanner found in a CX document.
|
|
758
|
+
* @category Plugin helpers
|
|
759
|
+
*/
|
|
264
760
|
export type AspectEvent =
|
|
265
761
|
| {
|
|
266
|
-
/**
|
|
762
|
+
/**
|
|
763
|
+
* A top-level value that is not an array: a scalar document, parsed; an object is not
|
|
764
|
+
* read at all (object true, value undefined), however large it is.
|
|
765
|
+
*/
|
|
267
766
|
readonly kind: "root";
|
|
268
767
|
readonly value: unknown;
|
|
768
|
+
readonly object: boolean;
|
|
269
769
|
readonly line: number;
|
|
270
770
|
}
|
|
271
771
|
| {
|
|
@@ -273,6 +773,8 @@ export type AspectEvent =
|
|
|
273
773
|
readonly kind: "block";
|
|
274
774
|
readonly aspect: string;
|
|
275
775
|
readonly block: number;
|
|
776
|
+
/** Whether the aspect is not the member's first key (`{"nodes": [...], "edges": [...]}`). */
|
|
777
|
+
readonly shared: boolean;
|
|
276
778
|
readonly line: number;
|
|
277
779
|
}
|
|
278
780
|
| {
|
|
@@ -288,7 +790,7 @@ export type AspectEvent =
|
|
|
288
790
|
readonly line: number;
|
|
289
791
|
}
|
|
290
792
|
| {
|
|
291
|
-
/**
|
|
793
|
+
/** The keys of a block member whose values are not arrays (`{"nodes": [...], "x": 1}`, in any order); skipped. */
|
|
292
794
|
readonly kind: "extraKeys";
|
|
293
795
|
readonly aspect: string;
|
|
294
796
|
readonly block: number;
|
|
@@ -312,6 +814,8 @@ export type AspectEvent =
|
|
|
312
814
|
readonly block: number;
|
|
313
815
|
readonly value: unknown;
|
|
314
816
|
readonly exact: boolean;
|
|
817
|
+
/** The member's JSON text (for lexical checks of its ids). */
|
|
818
|
+
readonly text: string;
|
|
315
819
|
readonly line: number;
|
|
316
820
|
};
|
|
317
821
|
|
|
@@ -320,7 +824,7 @@ export type AspectEvent =
|
|
|
320
824
|
* @param c - the character code
|
|
321
825
|
* @returns true for space, line feed, carriage return and tab
|
|
322
826
|
*/
|
|
323
|
-
const isSpace = (c: number): boolean => c === 32 || c === 10 || c === 13 || c === 9;
|
|
827
|
+
const isSpace = (c: number | undefined): boolean => c === 32 || c === 10 || c === 13 || c === 9;
|
|
324
828
|
|
|
325
829
|
/**
|
|
326
830
|
* Whether a character code ends a bare scalar (number, true, false, null).
|
|
@@ -378,6 +882,7 @@ class ChunkCursor {
|
|
|
378
882
|
this.captureStart = 0;
|
|
379
883
|
}
|
|
380
884
|
this.linesBefore += countNewlines(this.buf, 0, this.buf.length);
|
|
885
|
+
const endsWithCr = this.buf.endsWith("\r");
|
|
381
886
|
for (;;) {
|
|
382
887
|
const next = await this.iterator.next();
|
|
383
888
|
if (next.done === true) {
|
|
@@ -386,6 +891,10 @@ class ChunkCursor {
|
|
|
386
891
|
return false;
|
|
387
892
|
}
|
|
388
893
|
if (next.value.length > 0) {
|
|
894
|
+
if (endsWithCr && next.value.startsWith("\n")) {
|
|
895
|
+
// a CR LF split across two chunks is one line break, counted with the CR
|
|
896
|
+
this.linesBefore--;
|
|
897
|
+
}
|
|
389
898
|
this.buf = next.value;
|
|
390
899
|
this.pos = 0;
|
|
391
900
|
return true;
|
|
@@ -401,6 +910,15 @@ class ChunkCursor {
|
|
|
401
910
|
return this.linesBefore + countNewlines(this.buf, 0, Math.min(this.pos, this.buf.length));
|
|
402
911
|
}
|
|
403
912
|
|
|
913
|
+
/**
|
|
914
|
+
* The text from the read position to the end of the current chunk, at most `length` characters.
|
|
915
|
+
* @param length - the most characters
|
|
916
|
+
* @returns the text
|
|
917
|
+
*/
|
|
918
|
+
ahead(length: number): string {
|
|
919
|
+
return this.buf.slice(this.pos, this.pos + length);
|
|
920
|
+
}
|
|
921
|
+
|
|
404
922
|
/**
|
|
405
923
|
* Skip whitespace and return the next character without consuming it.
|
|
406
924
|
* @returns the character, or "" at the end of the input
|
|
@@ -537,7 +1055,7 @@ class ChunkCursor {
|
|
|
537
1055
|
}
|
|
538
1056
|
|
|
539
1057
|
/**
|
|
540
|
-
* The number of line
|
|
1058
|
+
* The number of line breaks in a slice of a string: LF, CR LF and a CR alone (old Mac files).
|
|
541
1059
|
* @param text - the string
|
|
542
1060
|
* @param from - the first index
|
|
543
1061
|
* @param to - one past the last index
|
|
@@ -550,6 +1068,13 @@ function countNewlines(text: string, from: number, to: number): number {
|
|
|
550
1068
|
count++;
|
|
551
1069
|
i = text.indexOf("\n", i + 1);
|
|
552
1070
|
}
|
|
1071
|
+
i = text.indexOf("\r", from);
|
|
1072
|
+
while (i >= 0 && i < to) {
|
|
1073
|
+
if (text[i + 1] !== "\n") {
|
|
1074
|
+
count++;
|
|
1075
|
+
}
|
|
1076
|
+
i = text.indexOf("\r", i + 1);
|
|
1077
|
+
}
|
|
553
1078
|
return count;
|
|
554
1079
|
}
|
|
555
1080
|
|
|
@@ -563,67 +1088,148 @@ function messageOf(err: unknown): string {
|
|
|
563
1088
|
}
|
|
564
1089
|
|
|
565
1090
|
/**
|
|
566
|
-
* Parse a captured value, turning a parse failure into a JsonScanError at its line.
|
|
1091
|
+
* Parse a captured value, turning a parse failure into a JsonScanError at its line. With a report,
|
|
1092
|
+
* the bare tokens NaN / Infinity / -Infinity are read as numbers (W_JSON_NONSTANDARD_NUMBER), and
|
|
1093
|
+
* lone surrogates and repeated keys are recorded (checkParsed()).
|
|
567
1094
|
* @param text - the value text
|
|
568
1095
|
* @param line - the line the value starts on
|
|
1096
|
+
* @param report - the report, or null to only parse
|
|
569
1097
|
* @returns the value and whether it may hold an ExactInteger
|
|
570
1098
|
*/
|
|
571
|
-
function parseAt(
|
|
1099
|
+
function parseAt(
|
|
1100
|
+
text: string,
|
|
1101
|
+
line: number,
|
|
1102
|
+
report: ImportReportBuilder | null,
|
|
1103
|
+
): { readonly value: unknown; readonly exact: boolean } {
|
|
1104
|
+
let parsed: { readonly value: unknown; readonly exact: boolean };
|
|
572
1105
|
try {
|
|
573
|
-
|
|
1106
|
+
parsed = parseExact(text);
|
|
574
1107
|
} catch (err) {
|
|
575
|
-
|
|
1108
|
+
const recovered = report === null ? null : parseNonstandard(text);
|
|
1109
|
+
if (recovered === null) {
|
|
1110
|
+
throw new JsonScanError(`invalid JSON: ${messageOf(err)}`, line);
|
|
1111
|
+
}
|
|
1112
|
+
report?.warnOnce(
|
|
1113
|
+
"coercion",
|
|
1114
|
+
JSON_NONSTANDARD_NUMBER_CODE,
|
|
1115
|
+
`the document uses the non-standard token${plural(recovered.tokens.size)} ${[...recovered.tokens].join(", ")}, which strict JSON does not allow; read as numbers`,
|
|
1116
|
+
{ line },
|
|
1117
|
+
);
|
|
1118
|
+
parsed = recovered;
|
|
1119
|
+
}
|
|
1120
|
+
if (report !== null) {
|
|
1121
|
+
parsed = { ...parsed, value: checkParsed(text, parsed.value, line, report) };
|
|
1122
|
+
}
|
|
1123
|
+
return parsed;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* What a document that does not start with "[" looks like, for the syntax error: HTML or XML (an
|
|
1128
|
+
* error page saved under the file's name), a ZIP archive, or nothing in particular.
|
|
1129
|
+
* @param head - the first characters
|
|
1130
|
+
* @returns a sentence to append, or ""
|
|
1131
|
+
*/
|
|
1132
|
+
function notJsonHint(head: string): string {
|
|
1133
|
+
if (head.startsWith("<")) {
|
|
1134
|
+
return "; the input looks like HTML or XML (an error page saved as the file?), not JSON";
|
|
1135
|
+
}
|
|
1136
|
+
if (head.startsWith("PK\u0003\u0004")) {
|
|
1137
|
+
return "; the input is a ZIP archive (a Cytoscape session .cys is read by the cys importer), not JSON";
|
|
576
1138
|
}
|
|
1139
|
+
return "";
|
|
577
1140
|
}
|
|
578
1141
|
|
|
579
1142
|
/**
|
|
580
1143
|
* Walk a CX document (CX1 or CX2): a top-level array of members, each normally a one-key object
|
|
581
1144
|
* `{"<aspect>": [elements]}`. The elements of such a block are parsed and yielded one at a time;
|
|
582
1145
|
* any other member is parsed whole and yielded as a "member". A document that is not an array is
|
|
583
|
-
* yielded as one "root" event. Syntax errors throw JsonScanError.
|
|
1146
|
+
* yielded as one "root" event (an object without reading it). Syntax errors throw JsonScanError.
|
|
584
1147
|
* @param chunks - the decoded text
|
|
1148
|
+
* @param report - where NaN tokens, lone surrogates and repeated keys are recorded (parseAt()); null to only parse
|
|
585
1149
|
* @yields the events in document order
|
|
586
1150
|
* @returns nothing
|
|
587
1151
|
*/
|
|
588
|
-
export async function* scanAspects(
|
|
1152
|
+
export async function* scanAspects(
|
|
1153
|
+
chunks: AsyncIterable<string>,
|
|
1154
|
+
report: ImportReportBuilder | null = null,
|
|
1155
|
+
): AsyncGenerator<AspectEvent, void, undefined> {
|
|
589
1156
|
const cur = new ChunkCursor(chunks);
|
|
590
1157
|
const first = await cur.peek();
|
|
591
1158
|
if (first === "") {
|
|
592
1159
|
throw new JsonScanError("the input is empty", 1, true);
|
|
593
1160
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1161
|
+
const hint = notJsonHint(cur.ahead(16));
|
|
1162
|
+
if (hint === "" && cur.ahead(8).includes(String.fromCodePoint(0))) {
|
|
1163
|
+
throw new JsonScanError(
|
|
1164
|
+
'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',
|
|
1165
|
+
cur.line(),
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (first === "[") {
|
|
1169
|
+
cur.pos++;
|
|
1170
|
+
yield* members(cur, report);
|
|
603
1171
|
return;
|
|
604
1172
|
}
|
|
605
|
-
cur.
|
|
1173
|
+
const line = cur.line();
|
|
1174
|
+
if (first === "{") {
|
|
1175
|
+
yield { kind: "root", value: undefined, object: true, line };
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
yield { kind: "root", value: await scalarRoot(cur, line, hint), object: false, line };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Read a document that is neither an array nor an object: one value, and nothing after it.
|
|
1183
|
+
* @param cur - the cursor at the value
|
|
1184
|
+
* @param line - the value's line
|
|
1185
|
+
* @param hint - what the input looks like when it is not JSON, for the error
|
|
1186
|
+
* @returns the value
|
|
1187
|
+
*/
|
|
1188
|
+
async function scalarRoot(cur: ChunkCursor, line: number, hint: string): Promise<unknown> {
|
|
1189
|
+
cur.beginCapture();
|
|
1190
|
+
await cur.skipValue();
|
|
1191
|
+
let value: unknown;
|
|
1192
|
+
try {
|
|
1193
|
+
({ value } = parseAt(cur.endCapture(), line, null));
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
if (err instanceof JsonScanError && hint !== "") {
|
|
1196
|
+
throw new JsonScanError(`${err.message}${hint}`, err.line);
|
|
1197
|
+
}
|
|
1198
|
+
throw err;
|
|
1199
|
+
}
|
|
1200
|
+
if ((await cur.peek()) !== "") {
|
|
1201
|
+
throw new JsonScanError("unexpected text after the document", cur.line());
|
|
1202
|
+
}
|
|
1203
|
+
return value;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Read the members of the top-level array, and check that nothing follows it.
|
|
1208
|
+
* @param cur - the cursor after the opening bracket
|
|
1209
|
+
* @param report - the report of parseAt(), or null
|
|
1210
|
+
* @yields the members' events
|
|
1211
|
+
* @returns nothing
|
|
1212
|
+
*/
|
|
1213
|
+
async function* members(
|
|
1214
|
+
cur: ChunkCursor,
|
|
1215
|
+
report: ImportReportBuilder | null,
|
|
1216
|
+
): AsyncGenerator<AspectEvent, void, undefined> {
|
|
606
1217
|
let block = 0;
|
|
607
|
-
|
|
1218
|
+
let next = await cur.peek();
|
|
1219
|
+
if (next === "]") {
|
|
608
1220
|
cur.pos++;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
cur.pos++;
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
if (next === "]") {
|
|
619
|
-
cur.pos++;
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
1221
|
+
}
|
|
1222
|
+
while (next !== "]") {
|
|
1223
|
+
yield* member(cur, block, report);
|
|
1224
|
+
block++;
|
|
1225
|
+
next = await cur.peek();
|
|
1226
|
+
if (next !== "," && next !== "]") {
|
|
622
1227
|
throw new JsonScanError(
|
|
623
1228
|
next === "" ? "the document ends before its closing bracket" : `expected "," or "]", found "${next}"`,
|
|
624
1229
|
cur.line(),
|
|
625
1230
|
);
|
|
626
1231
|
}
|
|
1232
|
+
cur.pos++;
|
|
627
1233
|
}
|
|
628
1234
|
if ((await cur.peek()) !== "") {
|
|
629
1235
|
throw new JsonScanError("unexpected text after the closing bracket", cur.line());
|
|
@@ -634,152 +1240,257 @@ export async function* scanAspects(chunks: AsyncIterable<string>): AsyncGenerato
|
|
|
634
1240
|
* Read one member of the top-level array.
|
|
635
1241
|
* @param cur - the cursor at the member's first character
|
|
636
1242
|
* @param block - the member's position among the members
|
|
1243
|
+
* @param report - the report of parseAt(), or null
|
|
637
1244
|
* @yields the member's events
|
|
638
1245
|
* @returns nothing
|
|
639
1246
|
*/
|
|
640
|
-
async function* member(
|
|
1247
|
+
async function* member(
|
|
1248
|
+
cur: ChunkCursor,
|
|
1249
|
+
block: number,
|
|
1250
|
+
report: ImportReportBuilder | null,
|
|
1251
|
+
): AsyncGenerator<AspectEvent, void, undefined> {
|
|
641
1252
|
const c = await cur.peek();
|
|
642
1253
|
const line = cur.line();
|
|
643
1254
|
if (c === "" || c === "]" || c === ",") {
|
|
644
1255
|
throw new JsonScanError(c === "" ? "the document ends inside its array" : `unexpected "${c}"`, line);
|
|
645
1256
|
}
|
|
1257
|
+
cur.beginCapture();
|
|
646
1258
|
if (c !== "{") {
|
|
647
|
-
cur.beginCapture();
|
|
648
1259
|
await cur.skipValue();
|
|
649
|
-
|
|
650
|
-
cur.dropCapture();
|
|
651
|
-
yield { kind: "deep", aspect: null, block, depth: cur.deepest, line };
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
const parsed = parseAt(cur.endCapture(), line);
|
|
655
|
-
yield { kind: "member", block, value: parsed.value, exact: parsed.exact, line };
|
|
1260
|
+
yield wholeMember(cur, block, line, report);
|
|
656
1261
|
return;
|
|
657
1262
|
}
|
|
658
|
-
cur.beginCapture();
|
|
659
1263
|
cur.pos++;
|
|
660
|
-
|
|
661
|
-
if (k !== '"') {
|
|
1264
|
+
if ((await cur.peek()) !== '"') {
|
|
662
1265
|
// `{}` or not JSON: parse the whole member so a syntax error is reported as such
|
|
663
1266
|
await cur.skipValue(1);
|
|
664
|
-
|
|
665
|
-
cur.dropCapture();
|
|
666
|
-
yield { kind: "deep", aspect: null, block, depth: cur.deepest, line };
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
const parsed = parseAt(cur.endCapture(), line);
|
|
670
|
-
yield { kind: "member", block, value: parsed.value, exact: parsed.exact, line };
|
|
1267
|
+
yield wholeMember(cur, block, line, report);
|
|
671
1268
|
return;
|
|
672
1269
|
}
|
|
673
|
-
//
|
|
1270
|
+
// every array-valued key is a block whose elements stream, whatever the key order; any other
|
|
1271
|
+
// value is kept as text, so a member without an array is parsed whole (a CX2 descriptor, a
|
|
1272
|
+
// one-object aspect) and one with an array names its other keys (extraKeys), skipped
|
|
674
1273
|
const outer = cur.endCapture();
|
|
675
|
-
const
|
|
676
|
-
const
|
|
1274
|
+
const firstLine = cur.line();
|
|
1275
|
+
const keys: MemberKeys = { others: [], firstBlock: null, deep: 0 };
|
|
1276
|
+
let next = ",";
|
|
1277
|
+
let index = 0;
|
|
1278
|
+
while (next === ",") {
|
|
1279
|
+
const key = await readKey(cur);
|
|
1280
|
+
yield* keyValue(cur, key, index, block, keys, report);
|
|
1281
|
+
index++;
|
|
1282
|
+
next = await cur.peek();
|
|
1283
|
+
if (next !== "," && next !== "}") {
|
|
1284
|
+
throw new JsonScanError(
|
|
1285
|
+
next === "" ? "the document ends inside a member" : `expected "," or "}" after the key "${key.name}"`,
|
|
1286
|
+
cur.line(),
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
cur.pos++;
|
|
1290
|
+
}
|
|
1291
|
+
yield* memberEnd(keys, block, line, { outer, firstLine }, report);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** What one-object member's keys held: the non-array values as text, the first block, the deepest value. */
|
|
1295
|
+
interface MemberKeys {
|
|
1296
|
+
readonly others: { key: string; text: string | null; line: number }[];
|
|
1297
|
+
firstBlock: string | null;
|
|
1298
|
+
deep: number;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* The event of a member read whole (its capture already taken): the member, or "deep" when it
|
|
1303
|
+
* nests too deep to parse.
|
|
1304
|
+
* @param cur - the cursor after the member
|
|
1305
|
+
* @param block - the member's position among the members
|
|
1306
|
+
* @param line - the member's line
|
|
1307
|
+
* @param report - the report of parseAt(), or null
|
|
1308
|
+
* @returns the event
|
|
1309
|
+
*/
|
|
1310
|
+
function wholeMember(cur: ChunkCursor, block: number, line: number, report: ImportReportBuilder | null): AspectEvent {
|
|
1311
|
+
if (cur.deepest > MAX_ELEMENT_DEPTH) {
|
|
1312
|
+
cur.dropCapture();
|
|
1313
|
+
return { kind: "deep", aspect: null, block, depth: cur.deepest, line };
|
|
1314
|
+
}
|
|
1315
|
+
const text = cur.endCapture();
|
|
1316
|
+
const parsed = parseAt(text, line, report);
|
|
1317
|
+
return { kind: "member", block, value: parsed.value, exact: parsed.exact, text, line };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Read a member's key and the colon after it.
|
|
1322
|
+
* @param cur - the cursor at the key
|
|
1323
|
+
* @returns the key and its line
|
|
1324
|
+
*/
|
|
1325
|
+
async function readKey(cur: ChunkCursor): Promise<{ name: string; line: number }> {
|
|
1326
|
+
if ((await cur.peek()) !== '"') {
|
|
1327
|
+
throw new JsonScanError("expected a key", cur.line());
|
|
1328
|
+
}
|
|
1329
|
+
const line = cur.line();
|
|
1330
|
+
const name = await cur.readString();
|
|
677
1331
|
if ((await cur.peek()) !== ":") {
|
|
678
|
-
throw new JsonScanError(`expected ":" after the key "${
|
|
1332
|
+
throw new JsonScanError(`expected ":" after the key "${name}"`, cur.line());
|
|
679
1333
|
}
|
|
680
1334
|
cur.pos++;
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1335
|
+
return { name, line };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Read one key's value: an array is a block whose elements stream; any other value is kept as text
|
|
1340
|
+
* (or, when it nests too deep to parse, only its depth).
|
|
1341
|
+
* @param cur - the cursor at the value
|
|
1342
|
+
* @param key - the key
|
|
1343
|
+
* @param key.name - its name
|
|
1344
|
+
* @param key.line - its line
|
|
1345
|
+
* @param index - the key's position in the member
|
|
1346
|
+
* @param block - the member's position among the members
|
|
1347
|
+
* @param keys - what the member's keys held so far
|
|
1348
|
+
* @param report - the report of parseAt(), or null
|
|
1349
|
+
* @yields the block's events
|
|
1350
|
+
* @returns nothing
|
|
1351
|
+
*/
|
|
1352
|
+
async function* keyValue(
|
|
1353
|
+
cur: ChunkCursor,
|
|
1354
|
+
key: { name: string; line: number },
|
|
1355
|
+
index: number,
|
|
1356
|
+
block: number,
|
|
1357
|
+
keys: MemberKeys,
|
|
1358
|
+
report: ImportReportBuilder | null,
|
|
1359
|
+
): AsyncGenerator<AspectEvent, void, undefined> {
|
|
1360
|
+
if ((await cur.peek()) === "[") {
|
|
1361
|
+
cur.pos++;
|
|
1362
|
+
yield { kind: "block", aspect: key.name, block, shared: index > 0, line: key.line };
|
|
1363
|
+
keys.firstBlock ??= key.name;
|
|
1364
|
+
yield* blockElements(cur, key.name, block, report);
|
|
695
1365
|
return;
|
|
696
1366
|
}
|
|
697
|
-
cur.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1367
|
+
cur.beginCapture();
|
|
1368
|
+
await cur.skipValue();
|
|
1369
|
+
// the member's own brace is one level more
|
|
1370
|
+
if (cur.deepest + 1 > MAX_ELEMENT_DEPTH) {
|
|
1371
|
+
// never parse (recursively) what no one reads
|
|
1372
|
+
cur.dropCapture();
|
|
1373
|
+
keys.deep = Math.max(keys.deep, cur.deepest + 1);
|
|
1374
|
+
keys.others.push({ key: key.name, text: null, line: key.line });
|
|
701
1375
|
} else {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (next === "]") {
|
|
732
|
-
cur.pos++;
|
|
733
|
-
break;
|
|
1376
|
+
keys.others.push({ key: key.name, text: cur.endCapture(), line: key.line });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* The events after a one-object member's keys: with a block, its other keys (checked, then named
|
|
1382
|
+
* as extraKeys); without one, the member parsed whole from its kept text.
|
|
1383
|
+
* @param keys - what the member's keys held
|
|
1384
|
+
* @param block - the member's position among the members
|
|
1385
|
+
* @param line - the member's line
|
|
1386
|
+
* @param start - the member's opening text and the line after it
|
|
1387
|
+
* @param start.outer - the text up to the first key
|
|
1388
|
+
* @param start.firstLine - the line of the first key
|
|
1389
|
+
* @param report - the report of parseAt(), or null
|
|
1390
|
+
* @yields the events
|
|
1391
|
+
* @returns nothing
|
|
1392
|
+
*/
|
|
1393
|
+
function* memberEnd(
|
|
1394
|
+
keys: MemberKeys,
|
|
1395
|
+
block: number,
|
|
1396
|
+
line: number,
|
|
1397
|
+
start: { outer: string; firstLine: number },
|
|
1398
|
+
report: ImportReportBuilder | null,
|
|
1399
|
+
): Generator<AspectEvent, void, undefined> {
|
|
1400
|
+
const { others, firstBlock, deep } = keys;
|
|
1401
|
+
if (firstBlock !== null) {
|
|
1402
|
+
for (const other of others) {
|
|
1403
|
+
if (other.text !== null) {
|
|
1404
|
+
parseAt(other.text, other.line, null);
|
|
734
1405
|
}
|
|
735
|
-
throw new JsonScanError(
|
|
736
|
-
next === ""
|
|
737
|
-
? `the document ends inside the "${aspect}" block`
|
|
738
|
-
: `expected "," or "]" in the "${aspect}" block, found "${next}"`,
|
|
739
|
-
cur.line(),
|
|
740
|
-
);
|
|
741
1406
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1407
|
+
if (others.length > 0) {
|
|
1408
|
+
yield {
|
|
1409
|
+
kind: "extraKeys",
|
|
1410
|
+
aspect: firstBlock,
|
|
1411
|
+
block,
|
|
1412
|
+
keys: others.map((o) => o.key),
|
|
1413
|
+
line: others[0].line,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
746
1416
|
return;
|
|
747
1417
|
}
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
cur.line(),
|
|
752
|
-
);
|
|
1418
|
+
if (deep > 0) {
|
|
1419
|
+
yield { kind: "deep", aspect: others[0].key, block, depth: deep, line };
|
|
1420
|
+
return;
|
|
753
1421
|
}
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
|
|
1422
|
+
const members = others.map((o) => [JSON.stringify(o.key), o.text ?? ""].join(":"));
|
|
1423
|
+
const text = `${start.outer}${members.join(",")}}`;
|
|
1424
|
+
const parsed = parseAt(text, start.firstLine, report);
|
|
1425
|
+
yield { kind: "member", block, value: parsed.value, exact: parsed.exact, text, line };
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Read the elements of a block whose opening bracket was consumed, through its closing bracket.
|
|
1430
|
+
* @param cur - the cursor after the opening bracket
|
|
1431
|
+
* @param aspect - the block's aspect
|
|
1432
|
+
* @param block - the member's position among the members
|
|
1433
|
+
* @param report - the report of parseAt(), or null
|
|
1434
|
+
* @yields an element (or deep) event per element
|
|
1435
|
+
* @returns nothing
|
|
1436
|
+
*/
|
|
1437
|
+
async function* blockElements(
|
|
1438
|
+
cur: ChunkCursor,
|
|
1439
|
+
aspect: string,
|
|
1440
|
+
block: number,
|
|
1441
|
+
report: ImportReportBuilder | null,
|
|
1442
|
+
): AsyncGenerator<AspectEvent, void, undefined> {
|
|
1443
|
+
if ((await cur.peek()) === "]") {
|
|
758
1444
|
cur.pos++;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (
|
|
764
|
-
throw new JsonScanError(
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
for (;;) {
|
|
1448
|
+
const e = await cur.peek();
|
|
1449
|
+
if (e === "") {
|
|
1450
|
+
throw new JsonScanError(`the document ends inside the "${aspect}" block`, cur.line());
|
|
765
1451
|
}
|
|
766
|
-
cur.
|
|
767
|
-
await cur.peek();
|
|
1452
|
+
const elementLine = cur.line();
|
|
768
1453
|
cur.beginCapture();
|
|
769
1454
|
await cur.skipValue();
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1455
|
+
if (cur.deepest > MAX_ELEMENT_DEPTH) {
|
|
1456
|
+
cur.dropCapture();
|
|
1457
|
+
yield { kind: "deep", aspect, block, depth: cur.deepest, line: elementLine };
|
|
1458
|
+
} else {
|
|
1459
|
+
const text = cur.endCapture();
|
|
1460
|
+
const parsed = parseAt(text, elementLine, report);
|
|
1461
|
+
yield {
|
|
1462
|
+
kind: "element",
|
|
1463
|
+
aspect,
|
|
1464
|
+
block,
|
|
1465
|
+
value: parsed.value,
|
|
1466
|
+
exact: parsed.exact,
|
|
1467
|
+
text,
|
|
1468
|
+
line: elementLine,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const next = await cur.peek();
|
|
1472
|
+
if (next === ",") {
|
|
1473
|
+
cur.pos++;
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (next === "]") {
|
|
1477
|
+
cur.pos++;
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
throw new JsonScanError(
|
|
1481
|
+
next === ""
|
|
1482
|
+
? `the document ends inside the "${aspect}" block`
|
|
1483
|
+
: `expected "," or "]" in the "${aspect}" block, found "${next}"`,
|
|
1484
|
+
cur.line(),
|
|
1485
|
+
);
|
|
774
1486
|
}
|
|
775
|
-
cur.pos++;
|
|
776
|
-
yield { kind: "extraKeys", aspect, block, keys, line: extraLine };
|
|
777
1487
|
}
|
|
778
1488
|
|
|
779
1489
|
/**
|
|
780
1490
|
* Report an element or member the scanner refused for its depth (E_BAD_ASPECT_BLOCK; skipped).
|
|
781
1491
|
* @param report - the report
|
|
782
1492
|
* @param event - the "deep" event
|
|
1493
|
+
* @category Plugin helpers
|
|
783
1494
|
*/
|
|
784
1495
|
export function reportTooDeep(report: ImportReportBuilder, event: Extract<AspectEvent, { kind: "deep" }>): void {
|
|
785
1496
|
report.error(
|
|
@@ -790,6 +1501,44 @@ export function reportTooDeep(report: ImportReportBuilder, event: Extract<Aspect
|
|
|
790
1501
|
);
|
|
791
1502
|
}
|
|
792
1503
|
|
|
1504
|
+
/**
|
|
1505
|
+
* The precision callback of plainJson() for an aspect kept verbatim in the metadata: W_PRECISION
|
|
1506
|
+
* once, since the digits stored there are not the file's.
|
|
1507
|
+
* @param report - the report
|
|
1508
|
+
* @param where - where the aspect is kept (meta.extra.cx, ...)
|
|
1509
|
+
* @returns the callback
|
|
1510
|
+
* @category Plugin helpers
|
|
1511
|
+
*/
|
|
1512
|
+
export function keptPrecision(report: ImportReportBuilder, where: string): (digits: string) => void {
|
|
1513
|
+
return (digits) => {
|
|
1514
|
+
report.warnOnce(
|
|
1515
|
+
"precision",
|
|
1516
|
+
PRECISION_CODE,
|
|
1517
|
+
`an aspect kept in ${where} holds ${digits}, beyond 2^53; it is kept as the nearest double`,
|
|
1518
|
+
{ element: where },
|
|
1519
|
+
`${PRECISION_CODE}:kept`,
|
|
1520
|
+
);
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Warn once (W_MULTI_ASPECT_FRAGMENT) about a block that shares its array member with another
|
|
1526
|
+
* aspect (`{"nodes": [...], "edges": [...]}`); a block of its own needs nothing.
|
|
1527
|
+
* @param report - the report
|
|
1528
|
+
* @param event - the "block" event
|
|
1529
|
+
* @category Plugin helpers
|
|
1530
|
+
*/
|
|
1531
|
+
export function reportSharedBlock(report: ImportReportBuilder, event: Extract<AspectEvent, { kind: "block" }>): void {
|
|
1532
|
+
if (event.shared) {
|
|
1533
|
+
report.warnOnce(
|
|
1534
|
+
"coercion",
|
|
1535
|
+
MULTI_ASPECT_FRAGMENT_CODE,
|
|
1536
|
+
`the "${event.aspect}" aspect shares an array member with another aspect; each is read as its own fragment`,
|
|
1537
|
+
{ line: event.line, element: event.aspect },
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
793
1542
|
// ============================================================ the CX id rule (design section 1.0.2)
|
|
794
1543
|
|
|
795
1544
|
/** How an id was spelled, when it was not a plain safe integer. */
|
|
@@ -812,6 +1561,7 @@ const DECIMAL_INTEGER_TEXT = /^-?(0|[1-9][0-9]*)$/;
|
|
|
812
1561
|
* @param raw - the parsed value
|
|
813
1562
|
* @param inexactLiteral - whether the number was written as a non-integer literal (`5.0`, `1e3`)
|
|
814
1563
|
* @returns the id and how it was spelled
|
|
1564
|
+
* @category Plugin helpers
|
|
815
1565
|
*/
|
|
816
1566
|
export function cxId(raw: unknown, inexactLiteral = false): CxId {
|
|
817
1567
|
if (raw instanceof ExactInteger) {
|
|
@@ -843,18 +1593,25 @@ function invalidCxId(raw: unknown, reason: string): GraphFormatError {
|
|
|
843
1593
|
});
|
|
844
1594
|
}
|
|
845
1595
|
|
|
1596
|
+
/** A number literal at lastIndex (sticky). */
|
|
1597
|
+
const NUMBER_LITERAL = /-?\d[\d.eE+-]*/y;
|
|
1598
|
+
|
|
846
1599
|
/**
|
|
847
1600
|
* Whether the element text writes a top-level key's number as a non-integer literal (`"id": 5.0`,
|
|
848
1601
|
* `"@id": 1e3`). A lexical check, so the parsed value (5) cannot tell. Only a key of the element
|
|
849
1602
|
* itself counts, not one nested in its `v` (where `s` and `id` are ordinary attribute names).
|
|
850
1603
|
* @param text - the element's JSON text
|
|
851
1604
|
* @param key - the key
|
|
1605
|
+
* @param keyDepth - the nesting depth of the element's own keys: 1, or 2 for an element written as
|
|
1606
|
+
* a member's single object (`{"nodes": {"@id": 1.0}}`)
|
|
852
1607
|
* @returns true when the literal holds a fraction or an exponent
|
|
1608
|
+
* @category Plugin helpers
|
|
853
1609
|
*/
|
|
854
|
-
export function inexactLiteral(text: string, key: string): boolean {
|
|
1610
|
+
export function inexactLiteral(text: string, key: string, keyDepth = 1): boolean {
|
|
855
1611
|
const quoted = JSON.stringify(key);
|
|
856
1612
|
let depth = 0;
|
|
857
|
-
|
|
1613
|
+
let i = 0;
|
|
1614
|
+
while (i < text.length) {
|
|
858
1615
|
const c = text[i];
|
|
859
1616
|
if (c === "{" || c === "[") {
|
|
860
1617
|
depth++;
|
|
@@ -862,24 +1619,36 @@ export function inexactLiteral(text: string, key: string): boolean {
|
|
|
862
1619
|
depth--;
|
|
863
1620
|
} else if (c === '"') {
|
|
864
1621
|
const start = i;
|
|
865
|
-
|
|
866
|
-
if (text[i] === "\\") {
|
|
867
|
-
i++;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
1622
|
+
i = stringEnd(text, i + 1);
|
|
870
1623
|
// the key itself (a string at depth 1 followed by a colon), not a value spelled like it
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
return
|
|
1624
|
+
const colon = skipSpace(text, i + 1);
|
|
1625
|
+
if (depth === keyDepth && text.startsWith(quoted, start) && text[colon] === ":") {
|
|
1626
|
+
return inexactNumberAt(text, skipSpace(text, colon + 1));
|
|
874
1627
|
}
|
|
875
1628
|
}
|
|
1629
|
+
i++;
|
|
876
1630
|
}
|
|
877
1631
|
return false;
|
|
878
1632
|
}
|
|
879
1633
|
|
|
1634
|
+
/**
|
|
1635
|
+
* Whether a number literal at an offset holds a fraction or an exponent.
|
|
1636
|
+
* @param text - the text
|
|
1637
|
+
* @param at - the offset
|
|
1638
|
+
* @returns true when it does; false for an integer or no number
|
|
1639
|
+
*/
|
|
1640
|
+
function inexactNumberAt(text: string, at: number): boolean {
|
|
1641
|
+
NUMBER_LITERAL.lastIndex = at;
|
|
1642
|
+
const match = NUMBER_LITERAL.exec(text);
|
|
1643
|
+
return match !== null && /[.eE]/.test(match[0]);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
880
1646
|
// ============================================================ Cytoscape positions (design section 1.0.2)
|
|
881
1647
|
|
|
882
|
-
/**
|
|
1648
|
+
/**
|
|
1649
|
+
* The name of the position column of every Cytoscape-family importer.
|
|
1650
|
+
* @category Plugin helpers
|
|
1651
|
+
*/
|
|
883
1652
|
export const POSITION_COLUMN = "position";
|
|
884
1653
|
|
|
885
1654
|
/** The name of the stacking-order column (Cytoscape's NODE_Z_LOCATION). */
|
|
@@ -890,6 +1659,7 @@ const Z_COLUMN = "z";
|
|
|
890
1659
|
* @param format - the importer's format name
|
|
891
1660
|
* @param sourceDims - 3 when z goes into the position (zAs "position")
|
|
892
1661
|
* @returns the declaration
|
|
1662
|
+
* @category Plugin helpers
|
|
893
1663
|
*/
|
|
894
1664
|
export function positionDecl(format: string, sourceDims = 2): ColumnDecl {
|
|
895
1665
|
return {
|
|
@@ -908,6 +1678,7 @@ export function positionDecl(format: string, sourceDims = 2): ColumnDecl {
|
|
|
908
1678
|
* The stacking-order column `z` (f64, origin namespace "cytoscape").
|
|
909
1679
|
* @param format - the importer's format name
|
|
910
1680
|
* @returns the declaration
|
|
1681
|
+
* @category Plugin helpers
|
|
911
1682
|
*/
|
|
912
1683
|
export function zDecl(format: string): ColumnDecl {
|
|
913
1684
|
return {
|
|
@@ -919,11 +1690,40 @@ export function zDecl(format: string): ColumnDecl {
|
|
|
919
1690
|
};
|
|
920
1691
|
}
|
|
921
1692
|
|
|
1693
|
+
/**
|
|
1694
|
+
* Whether a coordinate fits the f32 position column: finite once rounded to f32 (1e39 and the
|
|
1695
|
+
* Infinity of a literal like 1e400 do not, nor does NaN).
|
|
1696
|
+
* @param value - the coordinate
|
|
1697
|
+
* @returns true when the column can hold it
|
|
1698
|
+
* @category Plugin helpers
|
|
1699
|
+
*/
|
|
1700
|
+
export function fitsF32(value: number): boolean {
|
|
1701
|
+
return Number.isFinite(Math.fround(value));
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* The head of an input as text for a sniff: UTF-16 by its byte order mark, else UTF-8, never
|
|
1706
|
+
* failing, without the BOM.
|
|
1707
|
+
* @param head - the first bytes
|
|
1708
|
+
* @returns the text
|
|
1709
|
+
* @category Plugin helpers
|
|
1710
|
+
*/
|
|
1711
|
+
export function headText(head: Uint8Array): string {
|
|
1712
|
+
let encoding = "utf-8";
|
|
1713
|
+
if (head.byteLength >= 2 && head[0] === 0xff && head[1] === 0xfe) {
|
|
1714
|
+
encoding = "utf-16le";
|
|
1715
|
+
} else if (head.byteLength >= 2 && head[0] === 0xfe && head[1] === 0xff) {
|
|
1716
|
+
encoding = "utf-16be";
|
|
1717
|
+
}
|
|
1718
|
+
return new TextDecoder(encoding).decode(head).replace(/^\uFEFF/, "");
|
|
1719
|
+
}
|
|
1720
|
+
|
|
922
1721
|
/**
|
|
923
1722
|
* Cytoscape's screen y (growing downward) as graph-io's y (growing upward), and back: a negation
|
|
924
1723
|
* that never yields -0.
|
|
925
1724
|
* @param y - the coordinate
|
|
926
1725
|
* @returns the flipped coordinate
|
|
1726
|
+
* @category Plugin helpers
|
|
927
1727
|
*/
|
|
928
1728
|
export function flipY(y: number): number {
|
|
929
1729
|
return y === 0 ? 0 : -y;
|
|
@@ -939,6 +1739,7 @@ const STRUCTURE_ASPECTS: ReadonlySet<string> = new Set(["metaData", "status", "n
|
|
|
939
1739
|
* `metaData` or an aspect after the post-metadata or after `status` is W_ASPECT_ORDER; declared
|
|
940
1740
|
* element counts are compared with what was read (W_COUNT_MISMATCH); `status.success: false` is
|
|
941
1741
|
* fatal (E_STATUS_FAILED) and `success: true` with an error text a warning (W_STATUS_WARNING).
|
|
1742
|
+
* @category Plugin helpers
|
|
942
1743
|
*/
|
|
943
1744
|
export class CxStructure {
|
|
944
1745
|
private readonly report: ImportReportBuilder;
|
|
@@ -971,12 +1772,17 @@ export class CxStructure {
|
|
|
971
1772
|
return this.statusSeen;
|
|
972
1773
|
}
|
|
973
1774
|
|
|
1775
|
+
/** The name an aspect is counted under (CX1 reads old Cytoscape names under their cy names). */
|
|
1776
|
+
private readonly canonical: (name: string) => string;
|
|
1777
|
+
|
|
974
1778
|
/**
|
|
975
1779
|
* Create the checker.
|
|
976
1780
|
* @param report - the report
|
|
1781
|
+
* @param canonical - the name an aspect named in metaData is counted under (default: itself)
|
|
977
1782
|
*/
|
|
978
|
-
constructor(report: ImportReportBuilder) {
|
|
1783
|
+
constructor(report: ImportReportBuilder, canonical: (name: string) => string = (name) => name) {
|
|
979
1784
|
this.report = report;
|
|
1785
|
+
this.canonical = canonical;
|
|
980
1786
|
}
|
|
981
1787
|
|
|
982
1788
|
/**
|
|
@@ -1039,9 +1845,7 @@ export class CxStructure {
|
|
|
1039
1845
|
element(aspect: string, value: unknown, line: number): void {
|
|
1040
1846
|
this.counts.set(aspect, (this.counts.get(aspect) ?? 0) + 1);
|
|
1041
1847
|
if (aspect === "metaData") {
|
|
1042
|
-
|
|
1043
|
-
this.declared.set(value.name, value.elementCount);
|
|
1044
|
-
}
|
|
1848
|
+
this.readCount(value, line);
|
|
1045
1849
|
return;
|
|
1046
1850
|
}
|
|
1047
1851
|
if (aspect === "status") {
|
|
@@ -1049,6 +1853,36 @@ export class CxStructure {
|
|
|
1049
1853
|
}
|
|
1050
1854
|
}
|
|
1051
1855
|
|
|
1856
|
+
/**
|
|
1857
|
+
* Record the element count a metaData element declares; a count that is not an integer (text,
|
|
1858
|
+
* a fraction) or that names no aspect is W_COUNT_MISMATCH and not checked.
|
|
1859
|
+
* @param value - the metaData element
|
|
1860
|
+
* @param line - its line
|
|
1861
|
+
*/
|
|
1862
|
+
private readCount(value: unknown, line: number): void {
|
|
1863
|
+
if (!isRecord(value) || value.elementCount === undefined || value.elementCount === null) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const raw = value.elementCount;
|
|
1867
|
+
let count: number | null = null;
|
|
1868
|
+
if (raw instanceof ExactInteger) {
|
|
1869
|
+
count = Number(raw.digits);
|
|
1870
|
+
} else if (typeof raw === "number" && Number.isInteger(raw)) {
|
|
1871
|
+
count = raw;
|
|
1872
|
+
}
|
|
1873
|
+
if (typeof value.name !== "string" || count === null) {
|
|
1874
|
+
const shownCount = raw instanceof ExactInteger ? raw.digits : JSON.stringify(raw);
|
|
1875
|
+
this.report.warning(
|
|
1876
|
+
"validation-error",
|
|
1877
|
+
COUNT_MISMATCH_CODE,
|
|
1878
|
+
`a metaData element declares the elementCount ${shownCount} for the name ${JSON.stringify(value.name) ?? "undefined"}; not a count of a named aspect, not checked`,
|
|
1879
|
+
{ line, element: "metaData" },
|
|
1880
|
+
);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
this.declared.set(this.canonical(value.name), count);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1052
1886
|
/**
|
|
1053
1887
|
* Check a status element: success false is fatal, an error text with success true a warning.
|
|
1054
1888
|
* @param value - the element
|
|
@@ -1098,7 +1932,7 @@ export class CxStructure {
|
|
|
1098
1932
|
this.report.warning(
|
|
1099
1933
|
"validation-error",
|
|
1100
1934
|
COUNT_MISMATCH_CODE,
|
|
1101
|
-
`metaData declares ${declared} "${name}" element(
|
|
1935
|
+
`metaData declares ${declared} "${name}" element${plural(declared)}; ${read} ${agree(read, "was", "were")} read`,
|
|
1102
1936
|
{ element: name },
|
|
1103
1937
|
);
|
|
1104
1938
|
}
|
|
@@ -1110,6 +1944,7 @@ export class CxStructure {
|
|
|
1110
1944
|
* Whether a value is a plain JSON object.
|
|
1111
1945
|
* @param value - any value
|
|
1112
1946
|
* @returns true for a non-null, non-array object
|
|
1947
|
+
* @category Plugin helpers
|
|
1113
1948
|
*/
|
|
1114
1949
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1115
1950
|
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof ExactInteger);
|
|
@@ -1124,6 +1959,7 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
1124
1959
|
* @param decl - the declaration
|
|
1125
1960
|
* @param report - the report the rename is recorded in
|
|
1126
1961
|
* @returns the handle
|
|
1962
|
+
* @category Plugin helpers
|
|
1127
1963
|
*/
|
|
1128
1964
|
export function declareFresh(
|
|
1129
1965
|
sink: GraphSink,
|