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