@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `directed` / `multigraph` / `graph`), the d3 lineage of the same shape (`name` ids, integer
|
|
5
5
|
* index endpoints), JSON Graph Format v2 (nodes keyed by id, per-edge `directed`, hyperedges),
|
|
6
6
|
* Cytoscape.js elements (`data.id` / `data.source` / `data.target`, `position`, `classes`,
|
|
7
|
-
* `data.parent`), graphology
|
|
7
|
+
* `data.parent`), graphology serialization (`key` / `attributes`, `undirected` edges, `options`)
|
|
8
8
|
* vis.js (`from` / `to`), and NetworkX adjacency_data (`nodes` + `adjacency`) and tree_data (nested
|
|
9
9
|
* `id` / `children`) -- and pushes it scalar by scalar into the sink.
|
|
10
10
|
*
|
|
@@ -45,30 +45,47 @@ import { uniqueColumnName } from "../../common/attributes.js";
|
|
|
45
45
|
import {
|
|
46
46
|
AMBIGUOUS_GRAPH_NAME_CODE,
|
|
47
47
|
BAD_VALUE_CODE,
|
|
48
|
+
COLUMN_RENAMED_CODE,
|
|
48
49
|
DANGLING_REFERENCE_CODE,
|
|
50
|
+
DUPLICATE_ATTRIBUTE_CODE,
|
|
49
51
|
DUPLICATE_EDGE_ID_CODE,
|
|
50
52
|
DUPLICATE_NODE_CODE,
|
|
53
|
+
ELEMENT_ISSUE,
|
|
54
|
+
EMPTY_COLUMN_DROPPED_CODE,
|
|
51
55
|
EMPTY_INPUT_CODE,
|
|
52
56
|
ENCODING_FALLBACK_CODE,
|
|
53
57
|
GRAPH_NOT_FOUND_CODE,
|
|
54
58
|
HYPEREDGE_CODE,
|
|
59
|
+
INPUT_ISSUE,
|
|
55
60
|
INVALID_ENCODING_CODE,
|
|
56
61
|
INVALID_UTF8_CODE,
|
|
62
|
+
JSON_NONSTANDARD_NUMBER_CODE,
|
|
57
63
|
MISSING_ENDPOINT_CODE,
|
|
58
64
|
MISSING_ID_CODE,
|
|
59
65
|
MULTIPLE_GRAPHS_CODE,
|
|
60
66
|
OPTION_IGNORED_CODE,
|
|
67
|
+
PARENT_CYCLE_CODE,
|
|
68
|
+
PRECISION_CODE,
|
|
69
|
+
ROLE_TAKEN_CODE,
|
|
61
70
|
SYNTAX_CODE,
|
|
62
71
|
TOO_LARGE_CODE,
|
|
72
|
+
UNKNOWN_ELEMENT_CODE,
|
|
63
73
|
UNKNOWN_ENCODING_CODE,
|
|
64
74
|
UNKNOWN_PARENT_CODE,
|
|
65
75
|
} from "../../common/codes.js";
|
|
66
76
|
import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
|
|
67
77
|
import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
68
|
-
import { readText,
|
|
69
|
-
import {
|
|
78
|
+
import { readText, throwIfAborted } from "../../common/input.js";
|
|
79
|
+
import {
|
|
80
|
+
findDuplicateKeys,
|
|
81
|
+
flipY,
|
|
82
|
+
MAYBE_INEXACT_EXPONENT,
|
|
83
|
+
MAYBE_UNSAFE_INTEGER,
|
|
84
|
+
rewriteNumbers,
|
|
85
|
+
} from "../../common/json-elements.js";
|
|
70
86
|
import {
|
|
71
87
|
chooseGraph,
|
|
88
|
+
graphChosen,
|
|
72
89
|
type ImportFormatDefaults,
|
|
73
90
|
reportSinkOptions,
|
|
74
91
|
reportUnusedOptions,
|
|
@@ -76,9 +93,10 @@ import {
|
|
|
76
93
|
resolveImportOptions,
|
|
77
94
|
SINK_OPTION_CODE,
|
|
78
95
|
} from "../../common/options.js";
|
|
96
|
+
import { agree, plural } from "../../common/plural.js";
|
|
79
97
|
import { ImportReportBuilder } from "../../common/report.js";
|
|
80
98
|
import { weightFromValue } from "../../common/weights.js";
|
|
81
|
-
import { sniffJsonDialectHead } from "../../sniff.js";
|
|
99
|
+
import { headBytes, sniffJsonDialectHead } from "../../sniff.js";
|
|
82
100
|
import {
|
|
83
101
|
type CommonImportOptions,
|
|
84
102
|
type GraphChoiceOptions,
|
|
@@ -112,75 +130,109 @@ import { importObographs } from "./obographs.js";
|
|
|
112
130
|
* The format-specific options of the JSON importer. `graphIndex` / `graphName` choose one graph of
|
|
113
131
|
* a JGF or OBO Graphs `graphs` array (the first by default); a graph's name is its `id`, else its
|
|
114
132
|
* label.
|
|
133
|
+
* @category Built-in formats
|
|
115
134
|
*/
|
|
116
|
-
export interface JsonImportOptions extends GraphChoiceOptions {
|
|
117
|
-
/**
|
|
135
|
+
export interface JsonImportOptions extends GraphChoiceOptions, CommonImportOptions {
|
|
136
|
+
/**
|
|
137
|
+
* The dialect to read; "auto" detects it from the document. `jsonShapeOf(snapshot).dialect` is
|
|
138
|
+
* the dialect that was read.
|
|
139
|
+
* @defaultValue "auto"
|
|
140
|
+
*/
|
|
118
141
|
dialect?: JsonImportDialect | "auto" | undefined;
|
|
119
142
|
/**
|
|
120
|
-
* node-link
|
|
121
|
-
*
|
|
143
|
+
* node-link, d3, vis, adjacency and tree documents: the node key that holds the id. The default
|
|
144
|
+
* is "id" (for node-link and d3, "name" when no node has an "id").
|
|
145
|
+
* @defaultValue "id"
|
|
122
146
|
*/
|
|
123
147
|
nodeIdKey?: string | undefined;
|
|
124
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* node-link and d3 documents: the top-level key that holds the edges. The default is "edges"
|
|
150
|
+
* when the document has it, else "links".
|
|
151
|
+
* @defaultValue "edges" or "links"
|
|
152
|
+
*/
|
|
125
153
|
edgesKey?: string | undefined;
|
|
126
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* node-link, d3 and vis documents: the edge key that holds the source. The default is the first
|
|
156
|
+
* of "source", "src" and "from" the edges use (vis: "from").
|
|
157
|
+
* @defaultValue "source"
|
|
158
|
+
*/
|
|
127
159
|
sourceKey?: string | undefined;
|
|
128
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* node-link, d3 and vis documents: the edge key that holds the target. The default is the first
|
|
162
|
+
* of "target", "dst" and "to" the edges use (vis: "to").
|
|
163
|
+
* @defaultValue "target"
|
|
164
|
+
*/
|
|
129
165
|
targetKey?: string | undefined;
|
|
130
166
|
/**
|
|
131
|
-
* node-link
|
|
132
|
-
* every
|
|
167
|
+
* node-link and d3 documents: whether edge ends are positions in the node array rather than
|
|
168
|
+
* ids; "auto" says yes when every end is an integer below the number of nodes and no node id is
|
|
169
|
+
* a number.
|
|
170
|
+
* @defaultValue "auto"
|
|
133
171
|
*/
|
|
134
172
|
indexLinks?: boolean | "auto" | undefined;
|
|
135
173
|
/**
|
|
136
|
-
*
|
|
137
|
-
* and `.../obo/go#regulates` as `regulates`, the
|
|
138
|
-
*
|
|
174
|
+
* OBO Graphs documents: "curie" reads `http://purl.obolibrary.org/obo/GO_0008150` as
|
|
175
|
+
* `GO:0008150` and `.../obo/go#regulates` as `regulates`, the ids the `.obo` file of the same
|
|
176
|
+
* ontology uses; "iri" keeps every IRI as written.
|
|
177
|
+
* @defaultValue "curie"
|
|
139
178
|
*/
|
|
140
179
|
oboIds?: "curie" | "iri" | undefined;
|
|
141
180
|
/**
|
|
142
|
-
*
|
|
143
|
-
* in `meta.extra.obographs`,
|
|
144
|
-
* nodes and edges.
|
|
181
|
+
* OBO Graphs documents: "metadata" keeps the relation definitions (PROPERTY nodes, with their
|
|
182
|
+
* subPropertyOf and inverseOf edges) in `snapshot.meta.extra.obographs`, the way the OBO
|
|
183
|
+
* importer keeps `[Typedef]` frames; "nodes" makes them nodes and edges of the graph.
|
|
184
|
+
* @defaultValue "metadata"
|
|
145
185
|
*/
|
|
146
186
|
typedefs?: "metadata" | "nodes" | undefined;
|
|
147
187
|
/**
|
|
148
|
-
* node-link
|
|
149
|
-
* from the document
|
|
150
|
-
* (its `directed`, `multigraph`, `graph`
|
|
151
|
-
*
|
|
188
|
+
* node-link, d3, vis and graphology documents: where the node array is, as a dotted path of
|
|
189
|
+
* keys and array positions from the top of the document (`"data.nodes"`, `"graphs.0.nodes"`).
|
|
190
|
+
* The object that holds the array is read as the graph (its `directed`, `multigraph`, `graph`
|
|
191
|
+
* and edge keys). A path that leads nowhere is reported as E_MISSING_SECTION and the graph has
|
|
192
|
+
* no nodes.
|
|
193
|
+
* @defaultValue "nodes"
|
|
152
194
|
*/
|
|
153
195
|
nodesPath?: string | undefined;
|
|
154
196
|
/**
|
|
155
|
-
* node-link
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
197
|
+
* node-link, d3, vis and graphology documents: where the edge array is, as a dotted path
|
|
198
|
+
* (`"data.links"`). The default is the edges or links key next to the node array. A path that
|
|
199
|
+
* leads nowhere is reported as E_MISSING_SECTION and the graph has no edges.
|
|
200
|
+
* @defaultValue next to the nodes
|
|
159
201
|
*/
|
|
160
202
|
edgesPath?: string | undefined;
|
|
161
203
|
}
|
|
162
204
|
|
|
163
205
|
/**
|
|
164
|
-
* The issue codes the JSON importer records
|
|
165
|
-
* the other importers
|
|
206
|
+
* The issue codes the JSON importer records, by name: the codes shared with
|
|
207
|
+
* the other importers and the JSON-specific ones. A key is the code without
|
|
166
208
|
* its severity and format prefixes.
|
|
209
|
+
* @category Built-in formats
|
|
167
210
|
*/
|
|
168
211
|
export const JSON_ISSUE = Object.freeze({
|
|
169
|
-
|
|
212
|
+
...INPUT_ISSUE,
|
|
213
|
+
...ELEMENT_ISSUE,
|
|
214
|
+
/** The text is empty or whitespace. The import stops. */
|
|
170
215
|
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
171
|
-
/** JSON.
|
|
216
|
+
/** The text is not valid JSON. The import stops. */
|
|
172
217
|
SYNTAX: SYNTAX_CODE,
|
|
173
|
-
/** No dialect matches the document's top-level shape. */
|
|
218
|
+
/** No dialect matches the document's top-level shape. The import stops. */
|
|
174
219
|
DIALECT: "E_JSON_DIALECT",
|
|
175
220
|
/** A section (nodes, edges, elements, graph) has the wrong JSON type. */
|
|
176
221
|
SHAPE: "E_JSON_SHAPE",
|
|
177
|
-
/**
|
|
178
|
-
|
|
222
|
+
/**
|
|
223
|
+
* A section the dialect expects is not there: a node-link document without its nodes or its edges array, an
|
|
224
|
+
* adjacency document without lists for some nodes, or a `nodesPath` / `edgesPath` that names nothing. Nothing is
|
|
225
|
+
* skipped: the graph is read without that section (nodes come from the edges, or there are no edges).
|
|
226
|
+
*/
|
|
227
|
+
MISSING_SECTION: "W_MISSING_SECTION",
|
|
179
228
|
/** A node record or an element is not an object. */
|
|
180
229
|
BAD_ELEMENT: "E_BAD_ELEMENT",
|
|
181
230
|
/** A node record has no id. */
|
|
182
231
|
MISSING_ID: MISSING_ID_CODE,
|
|
183
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* A node id is a JSON boolean or null, which graph-io cannot use as an id; the node is skipped. With `ids:
|
|
234
|
+
* "string"` it is read as the text "true", "false" or "null".
|
|
235
|
+
*/
|
|
184
236
|
UNSUPPORTED_ID: "E_UNSUPPORTED_ID",
|
|
185
237
|
/** An edge record has no source or no target. */
|
|
186
238
|
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
@@ -196,25 +248,35 @@ export const JSON_ISSUE = Object.freeze({
|
|
|
196
248
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
197
249
|
/** An edge id (Cytoscape data.id, graphology key, vis id) repeated by a later edge; the edge is skipped. */
|
|
198
250
|
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
199
|
-
/** Two
|
|
251
|
+
/** Two different id texts became the same number because `ids` is "number", so their nodes were merged. */
|
|
200
252
|
ID_MERGED: ID_MERGED_CODE,
|
|
201
253
|
/** Edge ids of mixed JSON types were stored as text. */
|
|
202
254
|
EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
|
|
203
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* The file holds several graphs and only the first was read. It is not added when `graphIndex` or `graphName` chose the graph.
|
|
257
|
+
* `importAllGraphs()` reads every one.
|
|
258
|
+
*/
|
|
204
259
|
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
205
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* `graphIndex` is past the end of the `graphs` array, or `graphName` matches none of its graphs. The import stops.
|
|
262
|
+
*/
|
|
206
263
|
GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
|
|
207
|
-
/** `graphName`
|
|
264
|
+
/** `graphName` matches more than one graph of the `graphs` array. The import stops. */
|
|
208
265
|
AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
|
|
209
|
-
/**
|
|
266
|
+
/** Obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
|
|
210
267
|
OBOGRAPHS_SUBJ: "W_JSON_OBOGRAPHS_SUBJ",
|
|
211
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under
|
|
270
|
+
* addMissingNodes false).
|
|
271
|
+
*/
|
|
212
272
|
DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
|
|
213
|
-
/** The document is longer than
|
|
273
|
+
/** The document is longer than a JavaScript string can hold. The import stops. */
|
|
214
274
|
TOO_LARGE: TOO_LARGE_CODE,
|
|
215
|
-
/** JGF hyperedges
|
|
275
|
+
/** A JGF hyperedge (an edge with more than two ends) while `hyperedges` is "error". */
|
|
216
276
|
HYPEREDGE: HYPEREDGE_CODE,
|
|
217
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* JGF hyperedges were skipped, because `hyperedges` is "skip" (the default). Pass "star" or "clique" to keep them.
|
|
279
|
+
*/
|
|
218
280
|
HYPEREDGES_SKIPPED: "W_HYPEREDGES_SKIPPED",
|
|
219
281
|
/** A JGF hyperedge with neither a nodes array nor source / target arrays. */
|
|
220
282
|
HYPEREDGE_SHAPE: "E_HYPEREDGE_SHAPE",
|
|
@@ -222,22 +284,59 @@ export const JSON_ISSUE = Object.freeze({
|
|
|
222
284
|
POSITIONAL_NODES: "W_POSITIONAL_NODES",
|
|
223
285
|
/** A node-link / d3 top-level key the importer does not read (the other of edges / links, an unknown key); it is dropped. */
|
|
224
286
|
UNREAD_KEY: "W_JSON_UNREAD_KEY",
|
|
225
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
|
|
289
|
+
* `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
|
|
290
|
+
*/
|
|
226
291
|
SINK_OPTION: SINK_OPTION_CODE,
|
|
227
|
-
/**
|
|
292
|
+
/** You set an option this format does not use; it had no effect. The message names the option. */
|
|
228
293
|
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
229
|
-
/** The input
|
|
294
|
+
/** The input is not valid UTF-8. The import stops. */
|
|
230
295
|
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
231
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* Some bytes are not valid in the encoding that was chosen (by a byte order mark, the file's declaration or the
|
|
298
|
+
* `encoding` option). The import stops.
|
|
299
|
+
*/
|
|
232
300
|
INVALID_ENCODING: INVALID_ENCODING_CODE,
|
|
233
301
|
/** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
|
|
234
302
|
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
235
303
|
/** The document uses the non-standard tokens NaN / Infinity / -Infinity (Python's json writes them); read as numbers. */
|
|
236
|
-
NONSTANDARD_NUMBER:
|
|
304
|
+
NONSTANDARD_NUMBER: JSON_NONSTANDARD_NUMBER_CODE,
|
|
237
305
|
/** Integer literals beyond 2^53 were read as their exact digits (strings), not as rounded numbers. */
|
|
238
306
|
BIG_INTEGER: "W_JSON_BIG_INTEGER",
|
|
239
307
|
/** A declared encoding the platform cannot decode was ignored. */
|
|
240
308
|
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
309
|
+
/** An integer beyond 2^53 was stored as the nearest 64-bit float; pass `long: "string"` to keep every digit. */
|
|
310
|
+
PRECISION: PRECISION_CODE,
|
|
311
|
+
/**
|
|
312
|
+
* The document contradicts itself: a declared option its edges break (multigraph false with
|
|
313
|
+
* parallel links, graphology's options), a record whose section disagrees with its shape, a
|
|
314
|
+
* JGF inner id other than its key, the two listings of one adjacency edge.
|
|
315
|
+
*/
|
|
316
|
+
INCONSISTENT: "W_JSON_INCONSISTENT",
|
|
317
|
+
/** IndexLinks "auto" read integer endpoints as array positions although they also name node ids. */
|
|
318
|
+
INDEX_LINKS: "W_JSON_INDEX_LINKS",
|
|
319
|
+
/** A Cytoscape parent link that would close a cycle; that link is dropped. */
|
|
320
|
+
PARENT_CYCLE: PARENT_CYCLE_CODE,
|
|
321
|
+
/** An attribute key that is null on every element makes no column (NetworkX writes None as null). */
|
|
322
|
+
EMPTY_COLUMN_DROPPED: EMPTY_COLUMN_DROPPED_CODE,
|
|
323
|
+
/** OBO Graphs: a node type or synonym predicate outside the schema's set; kept as written, once per name. */
|
|
324
|
+
UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
|
|
325
|
+
/**
|
|
326
|
+
* A key repeated in one JSON object (JSON.parse keeps the last value, the earlier is dropped),
|
|
327
|
+
* or, in OBO Graphs, a single-valued OBO tag given twice in basicPropertyValues (the first is kept).
|
|
328
|
+
*/
|
|
329
|
+
DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
|
|
330
|
+
/**
|
|
331
|
+
* An attribute was renamed `<name>#<suffix>` because another attribute already has its name, for example
|
|
332
|
+
* two attributes declared with the same name.
|
|
333
|
+
*/
|
|
334
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
335
|
+
/**
|
|
336
|
+
* You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
|
|
337
|
+
* a plain attribute.
|
|
338
|
+
*/
|
|
339
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
241
340
|
});
|
|
242
341
|
|
|
243
342
|
/** The common options the JSON importer reads (the rest is reported by reportUnusedOptions). */
|
|
@@ -262,6 +361,9 @@ const FORMAT_DEFAULTS: ImportFormatDefaults = { ids: "keep", defaultDirected: fa
|
|
|
262
361
|
/** Bytes of the head sniff() inspects. */
|
|
263
362
|
const SNIFF_BYTES = 4096;
|
|
264
363
|
|
|
364
|
+
/** How many dangling endpoints the W_DANGLING_REFERENCE message names. */
|
|
365
|
+
const DANGLING_SHOWN = 5;
|
|
366
|
+
|
|
265
367
|
/** Elements pushed between two checks of the cancellation signal (the whole document is one chunk). */
|
|
266
368
|
const ABORT_CHECK_INTERVAL = 64;
|
|
267
369
|
|
|
@@ -405,18 +507,22 @@ function pathOption(name: string, value: unknown): readonly string[] | null {
|
|
|
405
507
|
}
|
|
406
508
|
|
|
407
509
|
/**
|
|
408
|
-
* The value at a path of object keys, or undefined when a
|
|
510
|
+
* The value at a path of object keys and array positions (`graphs.0.nodes`), or undefined when a
|
|
511
|
+
* step is missing.
|
|
409
512
|
* @param root - the document
|
|
410
|
-
* @param segments - the keys
|
|
513
|
+
* @param segments - the keys; a decimal integer indexes an array
|
|
411
514
|
* @returns the value
|
|
412
515
|
*/
|
|
413
516
|
function valueAt(root: unknown, segments: readonly string[]): unknown {
|
|
414
517
|
let value = root;
|
|
415
518
|
for (const segment of segments) {
|
|
416
|
-
if (
|
|
519
|
+
if (Array.isArray(value) && /^(0|[1-9]\d*)$/.test(segment) && Number(segment) < value.length) {
|
|
520
|
+
value = value[Number(segment)];
|
|
521
|
+
} else if (isJsonObject(value) && hasKey(value, segment)) {
|
|
522
|
+
value = value[segment];
|
|
523
|
+
} else {
|
|
417
524
|
return undefined;
|
|
418
525
|
}
|
|
419
|
-
value = value[segment];
|
|
420
526
|
}
|
|
421
527
|
return value;
|
|
422
528
|
}
|
|
@@ -440,7 +546,7 @@ function applyPaths(root: unknown, json: ResolvedJsonOptions, report: ImportRepo
|
|
|
440
546
|
const value = valueAt(root, segments);
|
|
441
547
|
if (value === undefined) {
|
|
442
548
|
const path = segments.join(".");
|
|
443
|
-
report.
|
|
549
|
+
report.warning(
|
|
444
550
|
"missing-value",
|
|
445
551
|
JSON_ISSUE.MISSING_SECTION,
|
|
446
552
|
`${option} ${JSON.stringify(path)} names nothing in the document`,
|
|
@@ -517,12 +623,23 @@ function pathEdgesKey(json: ResolvedJsonOptions, edgesPath: readonly string[]):
|
|
|
517
623
|
* rule over the record, else node-link.
|
|
518
624
|
* @param record - the graph record
|
|
519
625
|
* @param forced - the caller's dialect option
|
|
626
|
+
* @param placedEdges - whether edgesPath placed the record's edge array (under links or edges)
|
|
520
627
|
* @returns the dialect
|
|
521
628
|
*/
|
|
522
|
-
function pathDialect(record: unknown, forced: JsonImportDialect | "auto"): JsonImportDialect {
|
|
629
|
+
function pathDialect(record: unknown, forced: JsonImportDialect | "auto", placedEdges: boolean): JsonImportDialect {
|
|
523
630
|
if (forced !== "auto") {
|
|
524
631
|
return forced;
|
|
525
632
|
}
|
|
633
|
+
// edgesPath puts the edge array under links only so a bare d3 document stays d3; under edges,
|
|
634
|
+
// vis and graphology edges are told apart from NetworkX links again. A links key the document
|
|
635
|
+
// itself holds is NetworkX's and settles node-link.
|
|
636
|
+
if (placedEdges && isJsonObject(record) && hasKey(record, "links") && !hasKey(record, "edges")) {
|
|
637
|
+
const { links, ...rest } = record;
|
|
638
|
+
const asEdges = sniffJsonDialect({ ...rest, edges: links });
|
|
639
|
+
if (asEdges === "vis" || asEdges === "graphology") {
|
|
640
|
+
return asEdges;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
526
643
|
const sniffed = sniffJsonDialect(record);
|
|
527
644
|
return sniffed !== null && PATH_DIALECTS.has(sniffed) ? sniffed : "node-link";
|
|
528
645
|
}
|
|
@@ -544,7 +661,7 @@ function documentOf(
|
|
|
544
661
|
return { root: parsed, dialect: detectDialect(parsed, json.dialect, report) };
|
|
545
662
|
}
|
|
546
663
|
const root = applyPaths(parsed, json, report);
|
|
547
|
-
const dialect = pathDialect(root, json.dialect);
|
|
664
|
+
const dialect = pathDialect(root, json.dialect, json.edgesPath !== null);
|
|
548
665
|
// vis and graphology read their edges from the edges key only
|
|
549
666
|
if (json.edgesPath !== null && (dialect === "vis" || dialect === "graphology") && isJsonObject(root)) {
|
|
550
667
|
const key = pathEdgesKey(json, json.edgesPath);
|
|
@@ -628,14 +745,21 @@ class AttributeWriter {
|
|
|
628
745
|
|
|
629
746
|
private readonly reservedNames = new Set<string>();
|
|
630
747
|
|
|
748
|
+
private readonly report: ImportReportBuilder;
|
|
749
|
+
|
|
750
|
+
/** Column names written only with null so far (NetworkX writes None as null). */
|
|
751
|
+
private readonly nullOnly = new Set<string>();
|
|
752
|
+
|
|
631
753
|
/**
|
|
632
754
|
* Create a writer.
|
|
633
755
|
* @param sink - the sink
|
|
634
756
|
* @param domain - node or edge
|
|
757
|
+
* @param report - where a refused cell is recorded
|
|
635
758
|
*/
|
|
636
|
-
constructor(sink: GraphSink, domain: "node" | "edge") {
|
|
759
|
+
constructor(sink: GraphSink, domain: "node" | "edge", report: ImportReportBuilder) {
|
|
637
760
|
this.sink = sink;
|
|
638
761
|
this.domain = domain;
|
|
762
|
+
this.report = report;
|
|
639
763
|
}
|
|
640
764
|
|
|
641
765
|
/**
|
|
@@ -670,29 +794,59 @@ class AttributeWriter {
|
|
|
670
794
|
}
|
|
671
795
|
|
|
672
796
|
/**
|
|
673
|
-
* Write one attribute cell by its source key; null and undefined leave the row unset.
|
|
797
|
+
* Write one attribute cell by its source key; null and undefined leave the row unset. A cell the
|
|
798
|
+
* sink refuses is recorded and skipped, so the element's other keys are still written.
|
|
674
799
|
* @param row - the node or edge index
|
|
675
800
|
* @param key - the source key
|
|
676
801
|
* @param value - the JSON value
|
|
677
802
|
* @param suffix - the suffix applied when the key collides with a structural column
|
|
803
|
+
* @param element - the element name for an issue
|
|
678
804
|
*/
|
|
679
|
-
write(row: number, key: string, value: unknown, suffix: string): void {
|
|
680
|
-
if (value === undefined
|
|
805
|
+
write(row: number, key: string, value: unknown, suffix: string, element: string): void {
|
|
806
|
+
if (value === undefined) {
|
|
681
807
|
return;
|
|
682
808
|
}
|
|
683
809
|
const name = this.reservedNames.has(key) ? `${key}${suffix}` : key;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
810
|
+
if (value === null) {
|
|
811
|
+
this.nullOnly.add(name);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const cached = this.handles.get(name);
|
|
816
|
+
if (cached !== undefined) {
|
|
817
|
+
this.set(cached, row, value);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this.set(name, row, value);
|
|
821
|
+
} catch (err) {
|
|
822
|
+
if (!(err instanceof GraphFormatError)) {
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
825
|
+
this.report.recordError(err, { element: `${element}.${key}` });
|
|
687
826
|
return;
|
|
688
827
|
}
|
|
689
|
-
this.set(name, row, value);
|
|
690
828
|
const handle = this.lookup(name);
|
|
691
829
|
if (handle !== INVALID_INDEX) {
|
|
692
830
|
this.handles.set(name, handle);
|
|
693
831
|
}
|
|
694
832
|
}
|
|
695
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Report the keys that were null on every element that had them: they made no column, so a
|
|
836
|
+
* round trip loses them (W_EMPTY_COLUMN_DROPPED, one warning per table).
|
|
837
|
+
*/
|
|
838
|
+
reportNullOnly(): void {
|
|
839
|
+
const names = [...this.nullOnly].filter((name) => this.lookup(name) === INVALID_INDEX);
|
|
840
|
+
if (names.length > 0) {
|
|
841
|
+
this.report.warning(
|
|
842
|
+
"missing-value",
|
|
843
|
+
EMPTY_COLUMN_DROPPED_CODE,
|
|
844
|
+
`${this.domain} key${plural(names.length)} ${names.map((n) => JSON.stringify(n)).join(", ")} ${agree(names.length, "is", "are")} null wherever they appear; no column is made for them`,
|
|
845
|
+
{ element: names[0] },
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
696
850
|
/**
|
|
697
851
|
* Write through a handle or a name.
|
|
698
852
|
* @param column - the handle or name
|
|
@@ -721,6 +875,7 @@ class AttributeWriter {
|
|
|
721
875
|
|
|
722
876
|
/**
|
|
723
877
|
* Everything one import call shares between the dialect readers.
|
|
878
|
+
* @category Plugin helpers
|
|
724
879
|
*/
|
|
725
880
|
export class ImportContext {
|
|
726
881
|
readonly sink: GraphSink;
|
|
@@ -766,8 +921,8 @@ export class ImportContext {
|
|
|
766
921
|
this.json = json;
|
|
767
922
|
this.ids = new IdCoercer(options.ids);
|
|
768
923
|
this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
769
|
-
this.nodes = new AttributeWriter(sink, "node");
|
|
770
|
-
this.edges = new AttributeWriter(sink, "edge");
|
|
924
|
+
this.nodes = new AttributeWriter(sink, "node", report);
|
|
925
|
+
this.edges = new AttributeWriter(sink, "edge", report);
|
|
771
926
|
this.explicitDefaultDirected = explicitDefaultDirected;
|
|
772
927
|
}
|
|
773
928
|
|
|
@@ -882,16 +1037,17 @@ export class ImportContext {
|
|
|
882
1037
|
* Add a node, counting it or recording the failure.
|
|
883
1038
|
* @param id - the node id
|
|
884
1039
|
* @param element - the element name
|
|
1040
|
+
* @param why - more about a repeated id, for the duplicate warning
|
|
885
1041
|
* @returns the node index, or -1 when the sink refused the node
|
|
886
1042
|
*/
|
|
887
|
-
pushNode(id: NodeId, element: string): number {
|
|
1043
|
+
pushNode(id: NodeId, element: string, why = ""): number {
|
|
888
1044
|
this.checkAbort();
|
|
889
1045
|
const existing = this.sink.indexOf(id);
|
|
890
1046
|
if (existing !== INVALID_INDEX) {
|
|
891
1047
|
this.report.warning(
|
|
892
1048
|
"merged",
|
|
893
1049
|
JSON_ISSUE.DUPLICATE_NODE,
|
|
894
|
-
`${element}: node ${JSON.stringify(id)} already exists; its attributes are merged (the later values win)`,
|
|
1050
|
+
`${element}: node ${JSON.stringify(id)} already exists${why}; its attributes are merged (the later values win)`,
|
|
895
1051
|
{ element },
|
|
896
1052
|
);
|
|
897
1053
|
return existing;
|
|
@@ -920,17 +1076,72 @@ export class ImportContext {
|
|
|
920
1076
|
pushEdge(source: NodeId, target: NodeId, kind: EdgeKind, weight: number | undefined, element: string): number {
|
|
921
1077
|
this.checkAbort();
|
|
922
1078
|
const before = this.sink.edgeCount;
|
|
923
|
-
// endpoints the sink creates (addMissingNodes) count as nodes too
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
created++;
|
|
927
|
-
}
|
|
1079
|
+
// endpoints the sink creates (addMissingNodes) count as nodes too, and are reported
|
|
1080
|
+
const missingSource = this.sink.indexOf(source) === INVALID_INDEX;
|
|
1081
|
+
const missingTarget = source !== target && this.sink.indexOf(target) === INVALID_INDEX;
|
|
928
1082
|
const edge = this.direction.addEdge(source, target, kind, weight, { element });
|
|
929
1083
|
this.report.counts.edges += this.sink.edgeCount - before;
|
|
930
|
-
|
|
1084
|
+
if (missingSource) {
|
|
1085
|
+
this.dangling(source);
|
|
1086
|
+
}
|
|
1087
|
+
if (missingTarget) {
|
|
1088
|
+
this.dangling(target);
|
|
1089
|
+
}
|
|
931
1090
|
return edge;
|
|
932
1091
|
}
|
|
933
1092
|
|
|
1093
|
+
/** Endpoints that named no node and became placeholder nodes, the first few kept for the message. */
|
|
1094
|
+
private readonly danglingIds: NodeId[] = [];
|
|
1095
|
+
|
|
1096
|
+
private danglingCount = 0;
|
|
1097
|
+
|
|
1098
|
+
/** Set when the document has no node section: every node comes from an edge (E_MISSING_SECTION says so). */
|
|
1099
|
+
nodesFromEdges = false;
|
|
1100
|
+
|
|
1101
|
+
/** Dangling endpoints whose id, as the other JSON type, is a node (`"1"` next to the node 1). */
|
|
1102
|
+
private readonly nearMatches: NodeId[] = [];
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Count a placeholder node an edge endpoint created.
|
|
1106
|
+
* @param id - the endpoint
|
|
1107
|
+
*/
|
|
1108
|
+
private dangling(id: NodeId): void {
|
|
1109
|
+
this.report.counts.nodes++;
|
|
1110
|
+
this.danglingCount++;
|
|
1111
|
+
if (this.danglingIds.length < DANGLING_SHOWN) {
|
|
1112
|
+
this.danglingIds.push(id);
|
|
1113
|
+
}
|
|
1114
|
+
const other = typeof id === "number" ? String(id) : Number(id);
|
|
1115
|
+
if (
|
|
1116
|
+
this.nearMatches.length < DANGLING_SHOWN &&
|
|
1117
|
+
(typeof other === "string" || String(other) === id) &&
|
|
1118
|
+
this.sink.indexOf(other) !== INVALID_INDEX
|
|
1119
|
+
) {
|
|
1120
|
+
this.nearMatches.push(id);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Report the edge endpoints that named no node (the shared W_DANGLING_REFERENCE, once with the
|
|
1126
|
+
* count): each became a placeholder node, which is rarely what the file meant.
|
|
1127
|
+
*/
|
|
1128
|
+
reportDangling(): void {
|
|
1129
|
+
if (this.danglingCount === 0 || this.nodesFromEdges) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const shown = this.danglingIds.map((id) => JSON.stringify(id)).join(", ");
|
|
1133
|
+
const near =
|
|
1134
|
+
this.nearMatches.length > 0
|
|
1135
|
+
? `; ${this.nearMatches.map((id) => JSON.stringify(id)).join(", ")} ${agree(this.nearMatches.length, "names", "name")} a node id of another JSON type (a string next to a number), which is a different id`
|
|
1136
|
+
: "";
|
|
1137
|
+
this.report.warning(
|
|
1138
|
+
"validation-error",
|
|
1139
|
+
JSON_ISSUE.DANGLING_REFERENCE,
|
|
1140
|
+
`${this.danglingCount} edge endpoint${plural(this.danglingCount)} ${agree(this.danglingCount, "names", "name")} no node and became placeholder nodes: ${shown}${this.danglingCount > DANGLING_SHOWN ? ", ..." : ""}${near}`,
|
|
1141
|
+
{ element: String(this.danglingIds[0]) },
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
934
1145
|
/**
|
|
935
1146
|
* Check the cancellation signal every ABORT_CHECK_INTERVAL pushed elements, so an abort raised
|
|
936
1147
|
* while the whole in-memory document is being walked rejects promptly.
|
|
@@ -1070,6 +1281,7 @@ export class ImportContext {
|
|
|
1070
1281
|
* @param dict - the nested attribute dict
|
|
1071
1282
|
* @param structural - the element keys that are not attributes
|
|
1072
1283
|
* @param weightFrom - the weight key to skip in the dict, or null
|
|
1284
|
+
* @param element - the element name for issues
|
|
1073
1285
|
*/
|
|
1074
1286
|
writeNested(
|
|
1075
1287
|
writer: AttributeWriter,
|
|
@@ -1078,15 +1290,16 @@ export class ImportContext {
|
|
|
1078
1290
|
dict: JsonRecord,
|
|
1079
1291
|
structural: ReadonlySet<string>,
|
|
1080
1292
|
weightFrom: string | null,
|
|
1293
|
+
element: string,
|
|
1081
1294
|
): void {
|
|
1082
1295
|
for (const key of Object.keys(dict)) {
|
|
1083
1296
|
if (key !== weightFrom) {
|
|
1084
|
-
writer.write(row, key, dict[key], SUFFIX.data);
|
|
1297
|
+
writer.write(row, key, dict[key], SUFFIX.data, element);
|
|
1085
1298
|
}
|
|
1086
1299
|
}
|
|
1087
1300
|
for (const key of Object.keys(record)) {
|
|
1088
1301
|
if (!structural.has(key)) {
|
|
1089
|
-
writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data);
|
|
1302
|
+
writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data, element);
|
|
1090
1303
|
}
|
|
1091
1304
|
}
|
|
1092
1305
|
}
|
|
@@ -1140,6 +1353,7 @@ export class ImportContext {
|
|
|
1140
1353
|
): EdgeIdColumn | null {
|
|
1141
1354
|
let numbers = 0;
|
|
1142
1355
|
let strings = 0;
|
|
1356
|
+
let others = 0;
|
|
1143
1357
|
for (const edge of edges) {
|
|
1144
1358
|
if (!isJsonObject(edge)) {
|
|
1145
1359
|
continue;
|
|
@@ -1149,12 +1363,15 @@ export class ImportContext {
|
|
|
1149
1363
|
numbers++;
|
|
1150
1364
|
} else if (typeof raw === "string") {
|
|
1151
1365
|
strings++;
|
|
1366
|
+
} else if (raw !== undefined && raw !== null) {
|
|
1367
|
+
others++;
|
|
1152
1368
|
}
|
|
1153
1369
|
}
|
|
1154
|
-
if (numbers + strings === 0) {
|
|
1370
|
+
if (numbers + strings + others === 0) {
|
|
1155
1371
|
return null;
|
|
1156
1372
|
}
|
|
1157
|
-
|
|
1373
|
+
// ids of no usable type still get a column, so edgeIdValue() reports each instead of dropping it
|
|
1374
|
+
const dtype = strings === 0 && numbers > 0 ? "f64" : "string";
|
|
1158
1375
|
const columnName = uniqueColumnName(name, "id", (candidate) => this.edges.taken(candidate));
|
|
1159
1376
|
const handle = this.edges.declare({ name: columnName, dtype, role: "id", nullable: true, unique });
|
|
1160
1377
|
const stringify = strings > 0 && numbers > 0;
|
|
@@ -1163,7 +1380,7 @@ export class ImportContext {
|
|
|
1163
1380
|
this.report.warning(
|
|
1164
1381
|
"coercion",
|
|
1165
1382
|
JSON_ISSUE.EDGE_ID_STRINGIFIED,
|
|
1166
|
-
`edge ids mix numbers and strings; ${numbers} numeric id(
|
|
1383
|
+
`edge ids mix numbers and strings; ${numbers} numeric id${plural(numbers)} stored as text`,
|
|
1167
1384
|
{ element: columnName },
|
|
1168
1385
|
);
|
|
1169
1386
|
}
|
|
@@ -1223,7 +1440,8 @@ export class ImportContext {
|
|
|
1223
1440
|
// ============================================================ document level
|
|
1224
1441
|
|
|
1225
1442
|
/**
|
|
1226
|
-
* Parse the whole text; syntax errors and empty input abort the import.
|
|
1443
|
+
* Parse the whole text; syntax errors and empty input abort the import. A key repeated in one
|
|
1444
|
+
* object (which JSON.parse silently reduces to its last value) is reported per repetition.
|
|
1227
1445
|
* @param text - the decoded text
|
|
1228
1446
|
* @param report - the report
|
|
1229
1447
|
* @returns the parsed value
|
|
@@ -1234,46 +1452,189 @@ function parseDocument(text: string, report: ImportReportBuilder): unknown {
|
|
|
1234
1452
|
}
|
|
1235
1453
|
let root: unknown;
|
|
1236
1454
|
let syntaxError: string | null = null;
|
|
1455
|
+
// the text the syntax error was found in: the rewrite's when it parsed further than the original
|
|
1456
|
+
let errorText = text;
|
|
1237
1457
|
try {
|
|
1238
1458
|
root = JSON.parse(text) as unknown;
|
|
1239
1459
|
} catch (err) {
|
|
1240
1460
|
syntaxError = err instanceof Error ? err.message : String(err);
|
|
1241
1461
|
}
|
|
1242
1462
|
// the fast path: strict JSON without a digit run long enough to be an unsafe integer
|
|
1243
|
-
if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text)) {
|
|
1463
|
+
if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text) && !MAYBE_INEXACT_EXPONENT.test(text)) {
|
|
1464
|
+
reportDuplicateKeys(text, report);
|
|
1244
1465
|
return root;
|
|
1245
1466
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1467
|
+
let scan: ReturnType<typeof rewriteNumbers>;
|
|
1468
|
+
try {
|
|
1469
|
+
scan = rewriteNumbers(text);
|
|
1470
|
+
if (scan.tokens.size > 0 || scan.bigIntegers.length > 0) {
|
|
1471
|
+
try {
|
|
1472
|
+
root = JSON.parse(scan.text, scan.tokens.size > 0 ? scan.revive : undefined) as unknown;
|
|
1473
|
+
syntaxError = null;
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
if (err instanceof RangeError) {
|
|
1476
|
+
throw err;
|
|
1477
|
+
}
|
|
1478
|
+
// the rewrite read past the non-standard tokens: its error is the real one
|
|
1479
|
+
syntaxError = err instanceof Error ? err.message : String(err);
|
|
1480
|
+
errorText = scan.text;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
if (!(err instanceof RangeError)) {
|
|
1485
|
+
throw err;
|
|
1253
1486
|
}
|
|
1487
|
+
// the rewrite (quoted big integers, sentinel strings) is longer than one string can hold
|
|
1488
|
+
const message = `the document with its numbers rewritten is longer than the most one JavaScript string holds`;
|
|
1489
|
+
report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
|
|
1490
|
+
throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
|
|
1254
1491
|
}
|
|
1255
1492
|
if (syntaxError !== null) {
|
|
1256
|
-
return report.fail(JSON_ISSUE.SYNTAX,
|
|
1493
|
+
return report.fail(JSON_ISSUE.SYNTAX, syntaxMessage(text, syntaxError), {
|
|
1494
|
+
line: syntaxLine(errorText, syntaxError),
|
|
1495
|
+
});
|
|
1257
1496
|
}
|
|
1497
|
+
reportDuplicateKeys(text, report);
|
|
1258
1498
|
if (scan.tokens.size > 0) {
|
|
1259
1499
|
report.warning(
|
|
1260
1500
|
"coercion",
|
|
1261
1501
|
JSON_ISSUE.NONSTANDARD_NUMBER,
|
|
1262
|
-
`the document uses the non-standard token(
|
|
1502
|
+
`the document uses the non-standard token${plural(scan.tokens.size)} ${[...scan.tokens].join(", ")}, which strict JSON does not allow; read as numbers`,
|
|
1263
1503
|
);
|
|
1264
1504
|
}
|
|
1265
1505
|
if (scan.bigIntegers.length > 0) {
|
|
1266
|
-
const shown = scan.bigIntegers.slice(0, BIG_INTEGERS_SHOWN).join(", ");
|
|
1267
|
-
const more = scan.bigIntegers.length - BIG_INTEGERS_SHOWN;
|
|
1268
1506
|
report.warning(
|
|
1269
1507
|
"precision",
|
|
1270
1508
|
JSON_ISSUE.BIG_INTEGER,
|
|
1271
|
-
`${scan.bigIntegers.length} integer(
|
|
1509
|
+
`${scan.bigIntegers.length} integer${plural(scan.bigIntegers.length)} beyond 2^53 kept as text so no digit is lost: ${listed(scan.bigIntegers)}`,
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (scan.inexact.length > 0) {
|
|
1513
|
+
report.warning(
|
|
1514
|
+
"precision",
|
|
1515
|
+
JSON_ISSUE.PRECISION,
|
|
1516
|
+
`${scan.inexact.length} number literal${plural(scan.inexact.length)} no double holds exactly, read as the nearest double (an infinity beyond the range, 0 below it): ${listed(scan.inexact)}`,
|
|
1272
1517
|
);
|
|
1273
1518
|
}
|
|
1274
1519
|
return root;
|
|
1275
1520
|
}
|
|
1276
1521
|
|
|
1522
|
+
/**
|
|
1523
|
+
* The first literals of a list for a message, with the count of the rest.
|
|
1524
|
+
* @param literals - the literals
|
|
1525
|
+
* @returns the text
|
|
1526
|
+
*/
|
|
1527
|
+
function listed(literals: readonly string[]): string {
|
|
1528
|
+
const more = literals.length - BIG_INTEGERS_SHOWN;
|
|
1529
|
+
const shown = literals.slice(0, BIG_INTEGERS_SHOWN).join(", ");
|
|
1530
|
+
return more > 0 ? `${shown} and ${more} more` : shown;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/** How many repeated keys are reported one by one before the rest is summed up. */
|
|
1534
|
+
const DUPLICATE_KEYS_SHOWN = 10;
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Report every key repeated in one object (JSON.parse keeps the last value, so the earlier one is
|
|
1538
|
+
* gone): one warning per repetition up to DUPLICATE_KEYS_SHOWN, then one with the remaining count.
|
|
1539
|
+
* @param text - the document text
|
|
1540
|
+
* @param report - the report
|
|
1541
|
+
*/
|
|
1542
|
+
function reportDuplicateKeys(text: string, report: ImportReportBuilder): void {
|
|
1543
|
+
const found = findDuplicateKeys(text);
|
|
1544
|
+
let line = 1;
|
|
1545
|
+
let counted = 0;
|
|
1546
|
+
for (const { key, offset } of found.slice(0, DUPLICATE_KEYS_SHOWN)) {
|
|
1547
|
+
line += countLines(text, counted, offset);
|
|
1548
|
+
counted = offset;
|
|
1549
|
+
report.warning(
|
|
1550
|
+
"merged",
|
|
1551
|
+
JSON_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
1552
|
+
`key ${JSON.stringify(key)} appears twice in one object; the earlier value is dropped (JSON.parse keeps the last)`,
|
|
1553
|
+
{ line, element: key },
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
if (found.length > DUPLICATE_KEYS_SHOWN) {
|
|
1557
|
+
report.warning(
|
|
1558
|
+
"merged",
|
|
1559
|
+
JSON_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
1560
|
+
`${found.length - DUPLICATE_KEYS_SHOWN} more repeated key${plural(found.length - DUPLICATE_KEYS_SHOWN)}; each earlier value is dropped`,
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* The line breaks in a range of the text.
|
|
1567
|
+
* @param text - the text
|
|
1568
|
+
* @param from - the start offset
|
|
1569
|
+
* @param to - the end offset
|
|
1570
|
+
* @returns how many \n the range holds
|
|
1571
|
+
*/
|
|
1572
|
+
function countLines(text: string, from: number, to: number): number {
|
|
1573
|
+
let lines = 0;
|
|
1574
|
+
for (let i = text.indexOf("\n", from); i >= 0 && i < to; i = text.indexOf("\n", i + 1)) {
|
|
1575
|
+
lines++;
|
|
1576
|
+
}
|
|
1577
|
+
return lines;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/** The longest text the line of an unpositioned syntax error is searched in (a bisection of JSON.parse calls). */
|
|
1581
|
+
const SYNTAX_SEARCH_LIMIT = 64 * 1024 * 1024;
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* The 1-based line of a JSON.parse error: from the message's "(line L" or "position N" when the
|
|
1585
|
+
* engine gives one, the last line for an unexpected end, else the shortest prefix that fails the
|
|
1586
|
+
* same way (V8 names no position for "Unexpected token").
|
|
1587
|
+
* @param text - the document
|
|
1588
|
+
* @param message - the parser's message
|
|
1589
|
+
* @returns the line
|
|
1590
|
+
*/
|
|
1591
|
+
function syntaxLine(text: string, message: string): number {
|
|
1592
|
+
const line = /\(line (\d+)/.exec(message);
|
|
1593
|
+
if (line !== null) {
|
|
1594
|
+
return Number(line[1]);
|
|
1595
|
+
}
|
|
1596
|
+
const position = /position (\d+)/.exec(message);
|
|
1597
|
+
if (position !== null) {
|
|
1598
|
+
return countLines(text, 0, Number(position[1])) + 1;
|
|
1599
|
+
}
|
|
1600
|
+
if (!message.startsWith("Unexpected token") || text.length > SYNTAX_SEARCH_LIMIT) {
|
|
1601
|
+
// ponytail: an unexpected end (or a huge text) is placed on the last line
|
|
1602
|
+
return countLines(text, 0, text.length) + 1;
|
|
1603
|
+
}
|
|
1604
|
+
// the shortest prefix that holds the offending token: shorter prefixes end early instead
|
|
1605
|
+
let lo = 0;
|
|
1606
|
+
let hi = text.length;
|
|
1607
|
+
while (lo < hi) {
|
|
1608
|
+
const mid = (lo + hi) >>> 1;
|
|
1609
|
+
let failsOnToken = false;
|
|
1610
|
+
try {
|
|
1611
|
+
JSON.parse(text.slice(0, mid + 1));
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
failsOnToken = err instanceof Error && err.message.startsWith("Unexpected token");
|
|
1614
|
+
}
|
|
1615
|
+
if (failsOnToken) {
|
|
1616
|
+
hi = mid;
|
|
1617
|
+
} else {
|
|
1618
|
+
lo = mid + 1;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return countLines(text, 0, lo) + 1;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* The E_SYNTAX message: the parser's, with a hint when the text is JSON Lines (one document per
|
|
1626
|
+
* line, as apoc.export.json and many loggers write), which this importer does not read.
|
|
1627
|
+
* @param text - the document
|
|
1628
|
+
* @param message - the parser's message
|
|
1629
|
+
* @returns the message
|
|
1630
|
+
*/
|
|
1631
|
+
function syntaxMessage(text: string, message: string): string {
|
|
1632
|
+
if (looksLikeJsonLines(text)) {
|
|
1633
|
+
return `invalid JSON: the input is JSON Lines (one JSON value per line), which is not a graph format graph-io reads; convert it to one JSON document (${message})`;
|
|
1634
|
+
}
|
|
1635
|
+
return `invalid JSON: ${message}`;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1277
1638
|
/** How many of the integers kept as text the warning lists. */
|
|
1278
1639
|
const BIG_INTEGERS_SHOWN = 10;
|
|
1279
1640
|
|
|
@@ -1355,7 +1716,7 @@ function flagOf(value: unknown, what: string, fallback: boolean, report: ImportR
|
|
|
1355
1716
|
|
|
1356
1717
|
/**
|
|
1357
1718
|
* The endpoint key and value of an edge record: the explicit key when given, else the first of the
|
|
1358
|
-
* default keys the record has.
|
|
1719
|
+
* default keys the record has (a null value counts as absent).
|
|
1359
1720
|
* @param record - the edge record
|
|
1360
1721
|
* @param explicit - the caller's key, or null
|
|
1361
1722
|
* @param defaults - the default keys
|
|
@@ -1367,16 +1728,68 @@ function endpointOf(
|
|
|
1367
1728
|
defaults: readonly string[],
|
|
1368
1729
|
): { readonly key: string | null; readonly value: unknown } {
|
|
1369
1730
|
if (explicit !== null) {
|
|
1370
|
-
return { key:
|
|
1731
|
+
return { key: present(record, explicit) ? explicit : null, value: record[explicit] };
|
|
1371
1732
|
}
|
|
1372
1733
|
for (const key of defaults) {
|
|
1373
|
-
if (
|
|
1734
|
+
if (present(record, key)) {
|
|
1374
1735
|
return { key, value: record[key] };
|
|
1375
1736
|
}
|
|
1376
1737
|
}
|
|
1377
1738
|
return { key: null, value: undefined };
|
|
1378
1739
|
}
|
|
1379
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* Whether a record has a key with a value other than null: a null endpoint is an absent one.
|
|
1743
|
+
* @param record - the record
|
|
1744
|
+
* @param key - the key
|
|
1745
|
+
* @returns true when the key holds a non-null value
|
|
1746
|
+
*/
|
|
1747
|
+
function present(record: JsonRecord, key: string): boolean {
|
|
1748
|
+
return hasKey(record, key) && record[key] !== null;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Report every key of a record the dialect does not read (W_JSON_UNREAD_KEY): it is dropped.
|
|
1753
|
+
* @param ctx - the context
|
|
1754
|
+
* @param record - the document or graph record
|
|
1755
|
+
* @param known - the keys the dialect reads
|
|
1756
|
+
* @param prefix - the record's path for the messages (`""` for the document, `"graph."`)
|
|
1757
|
+
*/
|
|
1758
|
+
function reportUnreadKeys(ctx: ImportContext, record: JsonRecord, known: ReadonlySet<string>, prefix: string): void {
|
|
1759
|
+
for (const key of Object.keys(record)) {
|
|
1760
|
+
if (known.has(key)) {
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const value = record[key];
|
|
1764
|
+
const entries = Array.isArray(value) && value.length === 1 ? "entry" : "entries";
|
|
1765
|
+
const what = Array.isArray(value) ? `${String(value.length)} ${entries}` : describe(value);
|
|
1766
|
+
ctx.report.warning(
|
|
1767
|
+
"unsupported",
|
|
1768
|
+
JSON_ISSUE.UNREAD_KEY,
|
|
1769
|
+
`${prefix.length === 0 ? "top-level key" : "key"} ${prefix}${key} (${what}) is not read; dropped`,
|
|
1770
|
+
{ element: `${prefix}${key}` },
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/** The keys of a vis.js document the reader reads. */
|
|
1776
|
+
const VIS_DOCUMENT_KEYS: ReadonlySet<string> = new Set(["nodes", "edges"]);
|
|
1777
|
+
|
|
1778
|
+
/** The keys of a graphology serialization the reader reads. */
|
|
1779
|
+
const GRAPHOLOGY_DOCUMENT_KEYS: ReadonlySet<string> = new Set(["nodes", "edges", "options", "attributes"]);
|
|
1780
|
+
|
|
1781
|
+
/** The keys of a JGF graph object the reader reads. */
|
|
1782
|
+
const JGF_GRAPH_KEYS: ReadonlySet<string> = new Set([
|
|
1783
|
+
"id",
|
|
1784
|
+
"type",
|
|
1785
|
+
"label",
|
|
1786
|
+
"directed",
|
|
1787
|
+
"metadata",
|
|
1788
|
+
"nodes",
|
|
1789
|
+
"edges",
|
|
1790
|
+
"hyperedges",
|
|
1791
|
+
]);
|
|
1792
|
+
|
|
1380
1793
|
/**
|
|
1381
1794
|
* Whether any object of an array has a key with a non-null value.
|
|
1382
1795
|
* @param items - the array
|
|
@@ -1414,15 +1827,16 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: "node-lin
|
|
|
1414
1827
|
const nodes = arraySection(root.nodes, "nodes", report);
|
|
1415
1828
|
const edges = arraySection(root[edgesKey], edgesKey, report);
|
|
1416
1829
|
if (nodes === null) {
|
|
1417
|
-
report.
|
|
1830
|
+
report.warning(
|
|
1418
1831
|
"missing-value",
|
|
1419
1832
|
JSON_ISSUE.MISSING_SECTION,
|
|
1420
1833
|
"the document has no nodes array; nodes come from the edges",
|
|
1421
1834
|
{ element: "nodes" },
|
|
1422
1835
|
);
|
|
1836
|
+
ctx.nodesFromEdges = true;
|
|
1423
1837
|
}
|
|
1424
1838
|
if (edges === null) {
|
|
1425
|
-
report.
|
|
1839
|
+
report.warning(
|
|
1426
1840
|
"missing-value",
|
|
1427
1841
|
JSON_ISSUE.MISSING_SECTION,
|
|
1428
1842
|
`the document has no ${edgesKey} array; the graph has no edges`,
|
|
@@ -1433,22 +1847,7 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: "node-lin
|
|
|
1433
1847
|
const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
|
|
1434
1848
|
ctx.setHeader(directed);
|
|
1435
1849
|
ctx.writeGraphDict(root.graph, "graph");
|
|
1436
|
-
|
|
1437
|
-
if (key !== "nodes" && key !== edgesKey && key !== "directed" && key !== "multigraph" && key !== "graph") {
|
|
1438
|
-
const value = root[key];
|
|
1439
|
-
const what = Array.isArray(value)
|
|
1440
|
-
? `${String(value.length)} ${value.length === 1 ? "entry" : "entries"}`
|
|
1441
|
-
: describe(value);
|
|
1442
|
-
report.warning(
|
|
1443
|
-
"unsupported",
|
|
1444
|
-
JSON_ISSUE.UNREAD_KEY,
|
|
1445
|
-
`top-level key ${key} (${what}) is not read; dropped`,
|
|
1446
|
-
{
|
|
1447
|
-
element: key,
|
|
1448
|
-
},
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1850
|
+
reportUnreadKeys(ctx, root, new Set(["nodes", edgesKey, "directed", "multigraph", "graph"]), "");
|
|
1452
1851
|
|
|
1453
1852
|
const nodeList = nodes ?? [];
|
|
1454
1853
|
const edgeList = edges ?? [];
|
|
@@ -1461,11 +1860,13 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: "node-lin
|
|
|
1461
1860
|
if (!positional && nodeIdKey === null) {
|
|
1462
1861
|
const candidates = ctx.options.nodeIdFrom === "label" ? ["label", "name", "id"] : ["id", "name"];
|
|
1463
1862
|
nodeIdKey = candidates.find((key) => anyHas(nodeList, key)) ?? null;
|
|
1464
|
-
if (ctx.options.nodeIdFrom === "label" && nodeIdKey === "id") {
|
|
1863
|
+
if (ctx.options.nodeIdFrom === "label" && (nodeIdKey === "id" || nodeIdKey === "name")) {
|
|
1465
1864
|
report.warning(
|
|
1466
1865
|
"unsupported",
|
|
1467
1866
|
JSON_ISSUE.OPTION_IGNORED,
|
|
1468
|
-
|
|
1867
|
+
nodeIdKey === "id"
|
|
1868
|
+
? 'nodeIdFrom "label": no node has a label or name key; ids are read from "id"'
|
|
1869
|
+
: 'nodeIdFrom "label": no node has a "label" key; ids are read from "name"',
|
|
1469
1870
|
{ element: "nodeIdFrom" },
|
|
1470
1871
|
);
|
|
1471
1872
|
}
|
|
@@ -1495,6 +1896,9 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: "node-lin
|
|
|
1495
1896
|
({ indexLinks } = json);
|
|
1496
1897
|
}
|
|
1497
1898
|
const positionIds: (NodeId | null)[] | null = indexLinks ? [] : null;
|
|
1899
|
+
if (indexLinks && !positional && json.indexLinks === "auto" && nodeIdKey !== null) {
|
|
1900
|
+
reportAmbiguousIndexLinks(ctx, nodeList, edgeList, nodeIdKey);
|
|
1901
|
+
}
|
|
1498
1902
|
|
|
1499
1903
|
for (let i = 0; i < nodeList.length; i++) {
|
|
1500
1904
|
const element = `nodes[${i}]`;
|
|
@@ -1515,14 +1919,14 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: "node-lin
|
|
|
1515
1919
|
const index = ctx.pushNode(id, element);
|
|
1516
1920
|
if (index >= 0) {
|
|
1517
1921
|
pushed = id;
|
|
1518
|
-
writeFlat(ctx
|
|
1922
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
|
|
1519
1923
|
}
|
|
1520
1924
|
}
|
|
1521
1925
|
}
|
|
1522
1926
|
positionIds?.push(pushed);
|
|
1523
1927
|
}
|
|
1524
1928
|
throwIfAborted(ctx.options.signal);
|
|
1525
|
-
const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds);
|
|
1929
|
+
const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds, multigraph);
|
|
1526
1930
|
ctx.setMeta(
|
|
1527
1931
|
{
|
|
1528
1932
|
dialect,
|
|
@@ -1543,6 +1947,7 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: "node-lin
|
|
|
1543
1947
|
* @param edgeList - the edge records
|
|
1544
1948
|
* @param edgesKey - the top-level key they came from, for issues
|
|
1545
1949
|
* @param positionIds - the ids by array position under index links, or null
|
|
1950
|
+
* @param multigraph - the declared multigraph flag, or null when the document has none
|
|
1546
1951
|
* @returns the source and target keys the first well-formed edge used (null when none did)
|
|
1547
1952
|
*/
|
|
1548
1953
|
function importNodeLinkEdges(
|
|
@@ -1550,11 +1955,17 @@ function importNodeLinkEdges(
|
|
|
1550
1955
|
edgeList: readonly unknown[],
|
|
1551
1956
|
edgesKey: string,
|
|
1552
1957
|
positionIds: readonly (NodeId | null)[] | null,
|
|
1958
|
+
multigraph: boolean | null,
|
|
1553
1959
|
): { source: string | null; target: string | null } {
|
|
1554
1960
|
const { json } = ctx;
|
|
1555
1961
|
const kind = ctx.uniformKind();
|
|
1556
1962
|
let sourceKey: string | null = null;
|
|
1557
1963
|
let targetKey: string | null = null;
|
|
1964
|
+
// multigraph false: the pairs seen, to report parallel links; true: the (pair, key) triples, since
|
|
1965
|
+
// NetworkX reads a repeated (u, v, key) as one edge
|
|
1966
|
+
// ponytail: one string per edge whenever the flag is declared; a hash of the index pair if it shows in profiles
|
|
1967
|
+
const pairs = multigraph === null ? null : new Set<string>();
|
|
1968
|
+
let parallels = 0;
|
|
1558
1969
|
for (let i = 0; i < edgeList.length; i++) {
|
|
1559
1970
|
const element = `${edgesKey}[${i}]`;
|
|
1560
1971
|
const record = edgeList[i];
|
|
@@ -1578,23 +1989,101 @@ function importNodeLinkEdges(
|
|
|
1578
1989
|
continue;
|
|
1579
1990
|
}
|
|
1580
1991
|
const weight = ctx.weightOf(record);
|
|
1992
|
+
if (pairs !== null) {
|
|
1993
|
+
const pair = pairKey(u, v, kind === "directed");
|
|
1994
|
+
if (multigraph === false) {
|
|
1995
|
+
parallels += pairs.has(pair) ? 1 : 0;
|
|
1996
|
+
pairs.add(pair);
|
|
1997
|
+
} else if (hasKey(record, "key")) {
|
|
1998
|
+
const triple = `${pair} ${JSON.stringify(record.key)}`;
|
|
1999
|
+
if (pairs.has(triple)) {
|
|
2000
|
+
throw new GraphFormatError(
|
|
2001
|
+
JSON_ISSUE.DUPLICATE_EDGE_ID,
|
|
2002
|
+
`${element}: key ${JSON.stringify(record.key)} is repeated for the same pair; NetworkX reads it as the same edge, so it is skipped`,
|
|
2003
|
+
{ id: String(record.key) },
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
pairs.add(triple);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
1581
2009
|
const edge = ctx.pushEdge(u, v, kind, weight, element);
|
|
1582
2010
|
const { weightFrom } = ctx.options;
|
|
1583
2011
|
for (const key of Object.keys(record)) {
|
|
1584
2012
|
if (key !== source.key && key !== target.key && key !== weightFrom) {
|
|
1585
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
2013
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1586
2014
|
}
|
|
1587
2015
|
}
|
|
1588
2016
|
} catch (err) {
|
|
1589
2017
|
ctx.skip(err, "edge", element);
|
|
1590
2018
|
}
|
|
1591
2019
|
}
|
|
2020
|
+
if (parallels > 0) {
|
|
2021
|
+
ctx.report.warning(
|
|
2022
|
+
"validation-error",
|
|
2023
|
+
JSON_ISSUE.INCONSISTENT,
|
|
2024
|
+
`the document declares multigraph false, but ${parallels} link${plural(parallels)} ${agree(parallels, "repeats", "repeat")} the endpoints of an earlier one; all are kept`,
|
|
2025
|
+
{ element: edgesKey },
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
1592
2028
|
return { source: sourceKey, target: targetKey };
|
|
1593
2029
|
}
|
|
1594
2030
|
|
|
1595
2031
|
/**
|
|
1596
|
-
*
|
|
2032
|
+
* The key of an endpoint pair: ordered for a directed edge, unordered for an undirected one.
|
|
2033
|
+
* @param u - the source
|
|
2034
|
+
* @param v - the target
|
|
2035
|
+
* @param directed - whether the order matters
|
|
2036
|
+
* @returns the key
|
|
2037
|
+
*/
|
|
2038
|
+
function pairKey(u: NodeId, v: NodeId, directed: boolean): string {
|
|
2039
|
+
const a = JSON.stringify(u);
|
|
2040
|
+
const b = JSON.stringify(v);
|
|
2041
|
+
return directed || a <= b ? `${a} ${b}` : `${b} ${a}`;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* Warn when indexLinks "auto" read integer endpoints as array positions although some also equal
|
|
2046
|
+
* a node id's text (`"2"`): the file may mean the ids, and the edges are then off by the positions.
|
|
1597
2047
|
* @param ctx - the context
|
|
2048
|
+
* @param nodes - the node records
|
|
2049
|
+
* @param edges - the edge records
|
|
2050
|
+
* @param nodeIdKey - the node id key
|
|
2051
|
+
*/
|
|
2052
|
+
function reportAmbiguousIndexLinks(
|
|
2053
|
+
ctx: ImportContext,
|
|
2054
|
+
nodes: readonly unknown[],
|
|
2055
|
+
edges: readonly unknown[],
|
|
2056
|
+
nodeIdKey: string,
|
|
2057
|
+
): void {
|
|
2058
|
+
const ids = new Set<string>();
|
|
2059
|
+
for (const node of nodes) {
|
|
2060
|
+
if (isJsonObject(node) && typeof node[nodeIdKey] === "string") {
|
|
2061
|
+
ids.add(node[nodeIdKey]);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
for (const edge of edges) {
|
|
2065
|
+
if (!isJsonObject(edge)) {
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
for (const value of [
|
|
2069
|
+
endpointOf(edge, ctx.json.sourceKey, NODE_LINK_SOURCE_KEYS).value,
|
|
2070
|
+
endpointOf(edge, ctx.json.targetKey, NODE_LINK_TARGET_KEYS).value,
|
|
2071
|
+
]) {
|
|
2072
|
+
if (typeof value === "number" && ids.has(String(value))) {
|
|
2073
|
+
ctx.report.warning(
|
|
2074
|
+
"coercion",
|
|
2075
|
+
JSON_ISSUE.INDEX_LINKS,
|
|
2076
|
+
`integer endpoints are read as array positions, but ${value} is also a node id; pass indexLinks: false to read the endpoints as ids`,
|
|
2077
|
+
{ element: "indexLinks" },
|
|
2078
|
+
);
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Write the flat attributes of a node record (every own key the filter keeps).
|
|
1598
2087
|
* @param writer - the node writer
|
|
1599
2088
|
* @param index - the node index
|
|
1600
2089
|
* @param record - the record
|
|
@@ -1602,27 +2091,22 @@ function importNodeLinkEdges(
|
|
|
1602
2091
|
* @param keep - which keys are attributes
|
|
1603
2092
|
*/
|
|
1604
2093
|
function writeFlat(
|
|
1605
|
-
ctx: ImportContext,
|
|
1606
2094
|
writer: AttributeWriter,
|
|
1607
2095
|
index: number,
|
|
1608
2096
|
record: JsonRecord,
|
|
1609
2097
|
id: NodeId,
|
|
1610
2098
|
keep: (key: string) => boolean,
|
|
1611
2099
|
): void {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
writer.write(index, key, record[key], SUFFIX.data);
|
|
1616
|
-
}
|
|
2100
|
+
for (const key of Object.keys(record)) {
|
|
2101
|
+
if (keep(key)) {
|
|
2102
|
+
writer.write(index, key, record[key], SUFFIX.data, String(id));
|
|
1617
2103
|
}
|
|
1618
|
-
} catch (err) {
|
|
1619
|
-
ctx.report.recordError(err, { element: String(id) });
|
|
1620
2104
|
}
|
|
1621
2105
|
}
|
|
1622
2106
|
|
|
1623
2107
|
/**
|
|
1624
|
-
* The d3 index-link heuristic: endpoints are array positions when every endpoint is a
|
|
1625
|
-
* non-negative integer and no node id is a number (a numeric id would make the endpoints ids;
|
|
2108
|
+
* The d3 index-link heuristic: endpoints are array positions when every endpoint is a number, at
|
|
2109
|
+
* least one is a non-negative integer, and no node id is a number (a numeric id would make the endpoints ids;
|
|
1626
2110
|
* research note 07: d3 links reference nodes by array index and are never coerced to ids). An
|
|
1627
2111
|
* index at or beyond the node count is then E_BAD_INDEX, never a new numeric node.
|
|
1628
2112
|
* @param nodes - the node records
|
|
@@ -1652,10 +2136,14 @@ function looksIndexLinked(
|
|
|
1652
2136
|
}
|
|
1653
2137
|
const s = endpointOf(edge, json.sourceKey, NODE_LINK_SOURCE_KEYS).value;
|
|
1654
2138
|
const t = endpointOf(edge, json.targetKey, NODE_LINK_TARGET_KEYS).value;
|
|
1655
|
-
|
|
2139
|
+
// a fractional or negative number is a bad index (E_BAD_INDEX), never a reason to read every
|
|
2140
|
+
// other endpoint as an id; a missing endpoint is reported on its own
|
|
2141
|
+
if ((s !== undefined && typeof s !== "number") || (t !== undefined && typeof t !== "number")) {
|
|
1656
2142
|
return false;
|
|
1657
2143
|
}
|
|
1658
|
-
|
|
2144
|
+
if (isIndexBelow(s, Infinity) || isIndexBelow(t, Infinity)) {
|
|
2145
|
+
seen++;
|
|
2146
|
+
}
|
|
1659
2147
|
}
|
|
1660
2148
|
return seen > 0;
|
|
1661
2149
|
}
|
|
@@ -1709,6 +2197,8 @@ function importVis(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1709
2197
|
const { report, json } = ctx;
|
|
1710
2198
|
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
1711
2199
|
const edges = arraySection(root.edges, "edges", report) ?? [];
|
|
2200
|
+
// vis.js options and groups are rendering settings, not graph data
|
|
2201
|
+
reportUnreadKeys(ctx, root, VIS_DOCUMENT_KEYS, "");
|
|
1712
2202
|
ctx.setHeader(ctx.defaultDirected("vis"));
|
|
1713
2203
|
ctx.sink.reserve(nodes.length, edges.length);
|
|
1714
2204
|
const nodeIdKey = json.nodeIdKey ?? "id";
|
|
@@ -1727,7 +2217,7 @@ function importVis(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1727
2217
|
}
|
|
1728
2218
|
const index = ctx.pushNode(id, element);
|
|
1729
2219
|
if (index >= 0) {
|
|
1730
|
-
writeFlat(ctx
|
|
2220
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
|
|
1731
2221
|
}
|
|
1732
2222
|
}
|
|
1733
2223
|
throwIfAborted(ctx.options.signal);
|
|
@@ -1763,13 +2253,28 @@ function importVis(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1763
2253
|
const { weightFrom } = ctx.options;
|
|
1764
2254
|
for (const key of Object.keys(record)) {
|
|
1765
2255
|
if (key !== source.key && key !== target.key && key !== "id" && key !== weightFrom) {
|
|
1766
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
2256
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1767
2257
|
}
|
|
1768
2258
|
}
|
|
1769
2259
|
} catch (err) {
|
|
1770
2260
|
ctx.skip(err, "edge", element);
|
|
1771
2261
|
}
|
|
1772
2262
|
}
|
|
2263
|
+
// vis.js draws arrows from the `arrows` edge option; the file declares no graph direction, so the
|
|
2264
|
+
// arrows stay an attribute and the graph is read as the dialect's default
|
|
2265
|
+
const arrows =
|
|
2266
|
+
kind === "undirected"
|
|
2267
|
+
? edges.filter((e) => isJsonObject(e) && e.arrows !== undefined && e.arrows !== null && e.arrows !== "")
|
|
2268
|
+
.length
|
|
2269
|
+
: 0;
|
|
2270
|
+
if (arrows > 0) {
|
|
2271
|
+
report.warning(
|
|
2272
|
+
"validation-error",
|
|
2273
|
+
JSON_ISSUE.INCONSISTENT,
|
|
2274
|
+
`${arrows} edge${plural(arrows)} ${agree(arrows, "carries", "carry")} vis.js arrows but the graph is read undirected; the arrows are kept as an attribute (pass defaultDirected: true to read the edges as directed)`,
|
|
2275
|
+
{ element: "arrows" },
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
1773
2278
|
ctx.setMeta(
|
|
1774
2279
|
{ dialect: "vis", nodeIdKey, sourceKey: sourceKey ?? undefined, targetKey: targetKey ?? undefined },
|
|
1775
2280
|
ctx.weightOriginPatch(),
|
|
@@ -1780,7 +2285,7 @@ function importVis(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1780
2285
|
|
|
1781
2286
|
/**
|
|
1782
2287
|
* Read a NetworkX adjacency_data document: `nodes` as in node-link, and `adjacency[i]` the
|
|
1783
|
-
*
|
|
2288
|
+
* neighbor list of `nodes[i]`, one `{ id, key?, ...attributes }` entry per edge. An undirected
|
|
1784
2289
|
* file lists every edge from both ends, so an entry whose mirror (the same pair and `key`) was
|
|
1785
2290
|
* already read is that edge again and is not pushed twice; a self-loop is listed once.
|
|
1786
2291
|
* @param ctx - the context
|
|
@@ -1791,7 +2296,7 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1791
2296
|
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
1792
2297
|
const adjacency = arraySection(root.adjacency, "adjacency", report);
|
|
1793
2298
|
if (adjacency === null) {
|
|
1794
|
-
report.
|
|
2299
|
+
report.warning(
|
|
1795
2300
|
"missing-value",
|
|
1796
2301
|
JSON_ISSUE.MISSING_SECTION,
|
|
1797
2302
|
"the document has no adjacency array; the graph has no edges",
|
|
@@ -1839,7 +2344,7 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1839
2344
|
const index = ctx.pushNode(id, element);
|
|
1840
2345
|
if (index >= 0) {
|
|
1841
2346
|
pushed = id;
|
|
1842
|
-
writeFlat(ctx
|
|
2347
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
|
|
1843
2348
|
}
|
|
1844
2349
|
}
|
|
1845
2350
|
}
|
|
@@ -1848,8 +2353,9 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1848
2353
|
throwIfAborted(ctx.options.signal);
|
|
1849
2354
|
const kind = ctx.uniformKind();
|
|
1850
2355
|
const { weightFrom } = ctx.options;
|
|
1851
|
-
// undirected: entries read once whose mirror is still to come, by pair and key
|
|
1852
|
-
const pending = new Map<string,
|
|
2356
|
+
// undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
|
|
2357
|
+
const pending = new Map<string, string[]>();
|
|
2358
|
+
let disagreeing = 0;
|
|
1853
2359
|
for (let i = 0; i < lists.length; i++) {
|
|
1854
2360
|
const element = `adjacency[${i}]`;
|
|
1855
2361
|
const list = lists[i];
|
|
@@ -1863,7 +2369,7 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1863
2369
|
report.error(
|
|
1864
2370
|
"missing-value",
|
|
1865
2371
|
JSON_ISSUE.BAD_INDEX,
|
|
1866
|
-
`${element}: ${why}; its ${list.length} edge(
|
|
2372
|
+
`${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`,
|
|
1867
2373
|
{
|
|
1868
2374
|
element,
|
|
1869
2375
|
},
|
|
@@ -1888,13 +2394,17 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1888
2394
|
ctx.countSkipped("edge");
|
|
1889
2395
|
continue;
|
|
1890
2396
|
}
|
|
1891
|
-
if (!directed
|
|
1892
|
-
|
|
2397
|
+
if (!directed) {
|
|
2398
|
+
const mirror = mirroredEntry(pending, source, target, record, idKey);
|
|
2399
|
+
disagreeing += mirror === "disagrees" ? 1 : 0;
|
|
2400
|
+
if (mirror !== "new") {
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
1893
2403
|
}
|
|
1894
2404
|
const edge = ctx.pushEdge(source, target, kind, ctx.weightOf(record), entry);
|
|
1895
2405
|
for (const key of Object.keys(record)) {
|
|
1896
2406
|
if (key !== idKey && key !== weightFrom) {
|
|
1897
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
2407
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1898
2408
|
}
|
|
1899
2409
|
}
|
|
1900
2410
|
} catch (err) {
|
|
@@ -1902,36 +2412,112 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1902
2412
|
}
|
|
1903
2413
|
}
|
|
1904
2414
|
}
|
|
2415
|
+
reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, disagreeing, pending);
|
|
1905
2416
|
ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
|
|
1906
2417
|
}
|
|
1907
2418
|
|
|
1908
2419
|
/**
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
* @param
|
|
2420
|
+
* Report what an adjacency_data document leaves inconsistent: nodes without an adjacency list
|
|
2421
|
+
* (NetworkX writes one per node, so the file was cut), undirected edges whose two listings carry
|
|
2422
|
+
* different attributes (the first is kept), and undirected entries whose mirror never came.
|
|
2423
|
+
* @param ctx - the context
|
|
2424
|
+
* @param nodes - the number of node records
|
|
2425
|
+
* @param lists - the number of adjacency lists, or null when the section is missing (reported already)
|
|
2426
|
+
* @param disagreeing - the mirrors whose attributes differ
|
|
2427
|
+
* @param pending - the entries still awaiting their mirror
|
|
2428
|
+
*/
|
|
2429
|
+
function reportAdjacencyShape(
|
|
2430
|
+
ctx: ImportContext,
|
|
2431
|
+
nodes: number,
|
|
2432
|
+
lists: number | null,
|
|
2433
|
+
disagreeing: number,
|
|
2434
|
+
pending: ReadonlyMap<string, readonly string[]>,
|
|
2435
|
+
): void {
|
|
2436
|
+
const { report } = ctx;
|
|
2437
|
+
if (lists !== null && lists < nodes) {
|
|
2438
|
+
report.warning(
|
|
2439
|
+
"missing-value",
|
|
2440
|
+
JSON_ISSUE.MISSING_SECTION,
|
|
2441
|
+
`${nodes - lists} node${plural(nodes - lists)} from nodes[${lists}] on ${agree(nodes - lists, "has", "have")} no adjacency list (NetworkX writes one per node); their edges listed elsewhere are kept`,
|
|
2442
|
+
{ element: "adjacency" },
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
if (disagreeing > 0) {
|
|
2446
|
+
report.warning(
|
|
2447
|
+
"validation-error",
|
|
2448
|
+
JSON_ISSUE.INCONSISTENT,
|
|
2449
|
+
`the two listings of ${disagreeing} undirected edge${plural(disagreeing)} ${agree(disagreeing, "disagrees", "disagree")} on their attributes; the first listing is kept`,
|
|
2450
|
+
{ element: "adjacency" },
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
let unmatched = 0;
|
|
2454
|
+
for (const waiting of pending.values()) {
|
|
2455
|
+
unmatched += waiting.length;
|
|
2456
|
+
}
|
|
2457
|
+
if (unmatched > 0) {
|
|
2458
|
+
report.warning(
|
|
2459
|
+
"validation-error",
|
|
2460
|
+
JSON_ISSUE.INCONSISTENT,
|
|
2461
|
+
`${unmatched} undirected adjacency entr(ies) have no mirror in the other node's list; each is read as one edge`,
|
|
2462
|
+
{ element: "adjacency" },
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* A JSON.stringify replacer that writes every object's keys in sorted order.
|
|
2469
|
+
* @param _key - the key
|
|
2470
|
+
* @param value - the value
|
|
2471
|
+
* @returns the value, an object with its keys sorted
|
|
2472
|
+
*/
|
|
2473
|
+
function sortedKeys(_key: string, value: unknown): unknown {
|
|
2474
|
+
return isJsonObject(value) ? Object.fromEntries(Object.entries(value).sort(([x], [y]) => (x < y ? -1 : 1))) : value;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Whether an undirected adjacency entry is the mirror of one already read (the same unordered pair
|
|
2479
|
+
* and key), consuming it; otherwise the entry is remembered as awaiting its mirror. A self-loop is
|
|
2480
|
+
* listed once and never awaits one.
|
|
2481
|
+
* @param pending - the entries awaiting their mirror, by owner, other end and key: their attributes
|
|
1913
2482
|
* @param source - the owner of the list
|
|
1914
2483
|
* @param target - the entry's node
|
|
1915
|
-
* @param
|
|
1916
|
-
* @
|
|
2484
|
+
* @param record - the entry
|
|
2485
|
+
* @param idKey - the entry's id key (not an attribute)
|
|
2486
|
+
* @returns "new" for an entry to push, "mirror" for the second listing of one read, "disagrees"
|
|
2487
|
+
* for a second listing with other attributes
|
|
1917
2488
|
*/
|
|
1918
|
-
function
|
|
2489
|
+
function mirroredEntry(
|
|
2490
|
+
pending: Map<string, string[]>,
|
|
2491
|
+
source: NodeId,
|
|
2492
|
+
target: NodeId,
|
|
2493
|
+
record: JsonRecord,
|
|
2494
|
+
idKey: string,
|
|
2495
|
+
): "new" | "mirror" | "disagrees" {
|
|
1919
2496
|
const a = JSON.stringify(source);
|
|
1920
2497
|
const b = JSON.stringify(target);
|
|
1921
2498
|
if (a === b) {
|
|
1922
|
-
return
|
|
2499
|
+
return "new";
|
|
1923
2500
|
}
|
|
1924
|
-
const k = JSON.stringify(key ?? null);
|
|
2501
|
+
const k = JSON.stringify(record.key ?? null);
|
|
2502
|
+
// key order is not part of a JSON object, so two listings in another order still agree
|
|
2503
|
+
const attributes = JSON.stringify(
|
|
2504
|
+
Object.fromEntries(Object.entries(record).filter(([key]) => key !== idKey)),
|
|
2505
|
+
sortedKeys,
|
|
2506
|
+
);
|
|
1925
2507
|
// an entry is the mirror of one listed by the other end, never of a parallel entry of its own list
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
return true;
|
|
2508
|
+
const waiting = pending.get(`${b} ${a} ${k}`);
|
|
2509
|
+
if (waiting !== undefined && waiting.length > 0) {
|
|
2510
|
+
const first = waiting.shift();
|
|
2511
|
+
return first === attributes ? "mirror" : "disagrees";
|
|
1931
2512
|
}
|
|
1932
2513
|
const own = `${a} ${b} ${k}`;
|
|
1933
|
-
|
|
1934
|
-
|
|
2514
|
+
const list = pending.get(own);
|
|
2515
|
+
if (list === undefined) {
|
|
2516
|
+
pending.set(own, [attributes]);
|
|
2517
|
+
} else {
|
|
2518
|
+
list.push(attributes);
|
|
2519
|
+
}
|
|
2520
|
+
return "new";
|
|
1935
2521
|
}
|
|
1936
2522
|
|
|
1937
2523
|
/**
|
|
@@ -1971,7 +2557,7 @@ function importTree(ctx: ImportContext, root: JsonRecord): void {
|
|
|
1971
2557
|
} else if (ctx.pushNode(id, element) < 0) {
|
|
1972
2558
|
id = null;
|
|
1973
2559
|
} else {
|
|
1974
|
-
writeFlat(ctx
|
|
2560
|
+
writeFlat(ctx.nodes, ctx.sink.indexOf(id), record, id, (key) => key !== idKey && key !== "children");
|
|
1975
2561
|
if (parent !== null) {
|
|
1976
2562
|
try {
|
|
1977
2563
|
ctx.pushEdge(parent, id, kind, undefined, element);
|
|
@@ -2000,7 +2586,7 @@ function importTree(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2000
2586
|
// ============================================================ graphology
|
|
2001
2587
|
|
|
2002
2588
|
/**
|
|
2003
|
-
* Read a graphology
|
|
2589
|
+
* Read a graphology serialization: `options.type` decides the header direction ("mixed" or absent:
|
|
2004
2590
|
* from the edges' `undirected` flags), `options.multi` the declared multigraph flag, node `key`
|
|
2005
2591
|
* the id, `attributes` the columns, edge `key` the edge id.
|
|
2006
2592
|
* @param ctx - the context
|
|
@@ -2010,6 +2596,7 @@ function importGraphology(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2010
2596
|
const { report } = ctx;
|
|
2011
2597
|
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
2012
2598
|
const edges = arraySection(root.edges, "edges", report) ?? [];
|
|
2599
|
+
reportUnreadKeys(ctx, root, GRAPHOLOGY_DOCUMENT_KEYS, "");
|
|
2013
2600
|
const options = isJsonObject(root.options) ? root.options : {};
|
|
2014
2601
|
if (hasKey(root, "options") && !isJsonObject(root.options)) {
|
|
2015
2602
|
report.warning("validation-error", JSON_ISSUE.BAD_FLAG, "options is not an object; ignored", {
|
|
@@ -2064,6 +2651,9 @@ function importGraphology(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2064
2651
|
throwIfAborted(ctx.options.signal);
|
|
2065
2652
|
|
|
2066
2653
|
const ids = ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true);
|
|
2654
|
+
// what the edges do against the declared options: graphology itself refuses such a document
|
|
2655
|
+
const violations = { selfLoops: 0, parallels: 0, flags: 0 };
|
|
2656
|
+
const pairs = multi === false ? new Set<string>() : null;
|
|
2067
2657
|
for (let i = 0; i < edges.length; i++) {
|
|
2068
2658
|
const element = `edges[${i}]`;
|
|
2069
2659
|
const record = edges[i];
|
|
@@ -2071,8 +2661,8 @@ function importGraphology(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2071
2661
|
ctx.badElement("edge", element);
|
|
2072
2662
|
continue;
|
|
2073
2663
|
}
|
|
2074
|
-
if (!
|
|
2075
|
-
ctx.missingEndpoint(element,
|
|
2664
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2665
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
2076
2666
|
continue;
|
|
2077
2667
|
}
|
|
2078
2668
|
try {
|
|
@@ -2087,19 +2677,78 @@ function importGraphology(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2087
2677
|
kind = flagOf(record.undirected, `${element}.undirected`, false, report) ? "undirected" : "directed";
|
|
2088
2678
|
} else {
|
|
2089
2679
|
kind = type;
|
|
2680
|
+
if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
|
|
2681
|
+
violations.flags++;
|
|
2682
|
+
}
|
|
2090
2683
|
}
|
|
2091
|
-
const attributes
|
|
2684
|
+
const { attributes: rawAttributes } = record;
|
|
2685
|
+
if (rawAttributes !== undefined && rawAttributes !== null && !isJsonObject(rawAttributes)) {
|
|
2686
|
+
report.error(
|
|
2687
|
+
"validation-error",
|
|
2688
|
+
JSON_ISSUE.BAD_VALUE,
|
|
2689
|
+
`${element}: attributes must be an object, found ${describe(rawAttributes)}`,
|
|
2690
|
+
{ element },
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
const attributes = isJsonObject(rawAttributes) ? rawAttributes : {};
|
|
2092
2694
|
const idValue = ctx.edgeIdValue(ids, record.key);
|
|
2093
2695
|
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
2094
2696
|
ctx.setEdgeId(ids, edge, idValue);
|
|
2095
|
-
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom);
|
|
2697
|
+
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
|
|
2698
|
+
if (allowSelfLoops === false && u === v) {
|
|
2699
|
+
violations.selfLoops++;
|
|
2700
|
+
}
|
|
2701
|
+
if (pairs !== null) {
|
|
2702
|
+
const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
|
|
2703
|
+
violations.parallels += pairs.has(pair) ? 1 : 0;
|
|
2704
|
+
pairs.add(pair);
|
|
2705
|
+
}
|
|
2096
2706
|
} catch (err) {
|
|
2097
2707
|
ctx.skip(err, "edge", element);
|
|
2098
2708
|
}
|
|
2099
2709
|
}
|
|
2710
|
+
reportGraphologyViolations(ctx, violations, type);
|
|
2100
2711
|
ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
|
|
2101
2712
|
}
|
|
2102
2713
|
|
|
2714
|
+
/**
|
|
2715
|
+
* Report the edges that contradict a graphology document's declared options (W_JSON_INCONSISTENT,
|
|
2716
|
+
* one warning per kind); they are kept.
|
|
2717
|
+
* @param ctx - the context
|
|
2718
|
+
* @param violations - the counts
|
|
2719
|
+
* @param violations.selfLoops - self-loops under allowSelfLoops false
|
|
2720
|
+
* @param violations.parallels - parallel edges under multi false
|
|
2721
|
+
* @param violations.flags - per-edge undirected flags against options.type
|
|
2722
|
+
* @param type - options.type
|
|
2723
|
+
*/
|
|
2724
|
+
function reportGraphologyViolations(
|
|
2725
|
+
ctx: ImportContext,
|
|
2726
|
+
violations: { readonly selfLoops: number; readonly parallels: number; readonly flags: number },
|
|
2727
|
+
type: string,
|
|
2728
|
+
): void {
|
|
2729
|
+
const notes: string[] = [];
|
|
2730
|
+
if (violations.selfLoops > 0) {
|
|
2731
|
+
notes.push(
|
|
2732
|
+
`options.allowSelfLoops is false, but ${violations.selfLoops} self-loop${plural(violations.selfLoops)} ${agree(violations.selfLoops, "is", "are")} listed`,
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
if (violations.parallels > 0) {
|
|
2736
|
+
notes.push(
|
|
2737
|
+
`options.multi is false, but ${violations.parallels} parallel edge${plural(violations.parallels)} ${agree(violations.parallels, "is", "are")} listed`,
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
if (violations.flags > 0) {
|
|
2741
|
+
notes.push(
|
|
2742
|
+
`options.type is ${type}, but ${violations.flags} edge${plural(violations.flags)} ${agree(violations.flags, "carries", "carry")} the other undirected flag; read as ${type}`,
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
for (const note of notes) {
|
|
2746
|
+
ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${note}; every edge is kept`, {
|
|
2747
|
+
element: "options",
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2103
2752
|
/**
|
|
2104
2753
|
* Push a node whose attributes live in a nested dict (graphology `attributes`, JGF `metadata`);
|
|
2105
2754
|
* element-level keys the dialect does not define are kept with the `#element` suffix.
|
|
@@ -2133,7 +2782,7 @@ function pushNestedNode(
|
|
|
2133
2782
|
{ element },
|
|
2134
2783
|
);
|
|
2135
2784
|
}
|
|
2136
|
-
ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null);
|
|
2785
|
+
ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null, element);
|
|
2137
2786
|
} catch (err) {
|
|
2138
2787
|
ctx.report.recordError(err, { element: String(id) });
|
|
2139
2788
|
}
|
|
@@ -2209,6 +2858,14 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2209
2858
|
ctx.countSkipped("node");
|
|
2210
2859
|
continue;
|
|
2211
2860
|
}
|
|
2861
|
+
if (record !== null && hasKey(record, "id") && record.id !== key) {
|
|
2862
|
+
report.warning(
|
|
2863
|
+
"validation-error",
|
|
2864
|
+
JSON_ISSUE.INCONSISTENT,
|
|
2865
|
+
`${element}: the inner id ${describe(record.id)} differs from the key; the key is the id, the inner one is kept as id${SUFFIX.element}`,
|
|
2866
|
+
{ element },
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2212
2869
|
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
2213
2870
|
}
|
|
2214
2871
|
} else {
|
|
@@ -2258,7 +2915,7 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2258
2915
|
element,
|
|
2259
2916
|
});
|
|
2260
2917
|
}
|
|
2261
|
-
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom);
|
|
2918
|
+
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom, element);
|
|
2262
2919
|
};
|
|
2263
2920
|
|
|
2264
2921
|
for (let i = 0; i < edges.length; i++) {
|
|
@@ -2268,8 +2925,8 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2268
2925
|
ctx.badElement("edge", element);
|
|
2269
2926
|
continue;
|
|
2270
2927
|
}
|
|
2271
|
-
if (!
|
|
2272
|
-
ctx.missingEndpoint(element,
|
|
2928
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2929
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
2273
2930
|
continue;
|
|
2274
2931
|
}
|
|
2275
2932
|
try {
|
|
@@ -2298,7 +2955,23 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2298
2955
|
* @returns the graph object; the import fails when there is none
|
|
2299
2956
|
*/
|
|
2300
2957
|
function jgfGraphOf(ctx: ImportContext, root: JsonRecord): JsonRecord {
|
|
2301
|
-
|
|
2958
|
+
const single = isJsonObject(root.graph);
|
|
2959
|
+
// a document with both graph and graphs is read by its graph; the graphs are not
|
|
2960
|
+
reportUnreadKeys(ctx, root, new Set([single ? "graph" : "graphs"]), "");
|
|
2961
|
+
const graph = single ? (root.graph as JsonRecord) : chosenGraph(ctx, root, "JGF");
|
|
2962
|
+
const prefix = single ? "graph." : `graphs[${(root.graphs as unknown[]).indexOf(graph)}].`;
|
|
2963
|
+
reportUnreadKeys(ctx, graph, JGF_GRAPH_KEYS, prefix);
|
|
2964
|
+
for (const key of ["id", "type", "label"]) {
|
|
2965
|
+
if (graph[key] !== undefined && graph[key] !== null && typeof graph[key] !== "string") {
|
|
2966
|
+
ctx.report.error(
|
|
2967
|
+
"validation-error",
|
|
2968
|
+
JSON_ISSUE.BAD_VALUE,
|
|
2969
|
+
`${prefix}${key} must be a string, found ${describe(graph[key])}; ignored`,
|
|
2970
|
+
{ element: `${prefix}${key}` },
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return graph;
|
|
2302
2975
|
}
|
|
2303
2976
|
|
|
2304
2977
|
/**
|
|
@@ -2308,6 +2981,7 @@ function jgfGraphOf(ctx: ImportContext, root: JsonRecord): JsonRecord {
|
|
|
2308
2981
|
* @param root - the document
|
|
2309
2982
|
* @param what - the dialect's name, for the messages
|
|
2310
2983
|
* @returns the graph object; the import fails when there is none
|
|
2984
|
+
* @category Plugin helpers
|
|
2311
2985
|
*/
|
|
2312
2986
|
export function chosenGraph(ctx: ImportContext, root: JsonRecord, what: string): JsonRecord {
|
|
2313
2987
|
const { report } = ctx;
|
|
@@ -2317,17 +2991,24 @@ export function chosenGraph(ctx: ImportContext, root: JsonRecord, what: string):
|
|
|
2317
2991
|
}
|
|
2318
2992
|
const index =
|
|
2319
2993
|
ctx.json.all === true ? ctx.json.graphIndex : chooseGraph(graphs.map(graphNameOf), ctx.json.choice, report);
|
|
2320
|
-
if (graphs.length > 1 && ctx.json.all !== true) {
|
|
2994
|
+
if (graphs.length > 1 && ctx.json.all !== true && !graphChosen(ctx.json.choice)) {
|
|
2321
2995
|
report.warning(
|
|
2322
2996
|
"unsupported",
|
|
2323
2997
|
JSON_ISSUE.MULTIPLE_GRAPHS,
|
|
2324
|
-
`the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped)
|
|
2998
|
+
`the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped); importAllGraphs() reads every one`,
|
|
2325
2999
|
{ element: "graphs" },
|
|
2326
3000
|
);
|
|
2327
3001
|
}
|
|
2328
3002
|
const graph = graphs[index];
|
|
2329
3003
|
if (!isJsonObject(graph)) {
|
|
2330
|
-
|
|
3004
|
+
if (ctx.json.all !== true) {
|
|
3005
|
+
return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
|
|
3006
|
+
}
|
|
3007
|
+
// importAll(): one bad graph is that graph's error, read as empty; the others are still read
|
|
3008
|
+
report.error("validation-error", JSON_ISSUE.SHAPE, `graphs[${index}] is not an object; it is read as empty`, {
|
|
3009
|
+
element: `graphs[${index}]`,
|
|
3010
|
+
});
|
|
3011
|
+
return {};
|
|
2331
3012
|
}
|
|
2332
3013
|
return graph;
|
|
2333
3014
|
}
|
|
@@ -2391,38 +3072,29 @@ function listingsOf(root: unknown, dialect: JsonImportDialect): GraphListing[] {
|
|
|
2391
3072
|
return [{ index: 0, name: null, nodes: null, edges: null }];
|
|
2392
3073
|
}
|
|
2393
3074
|
|
|
2394
|
-
/** The longest string V8 makes (2^29 - 24 UTF-16 code units); a longer document cannot be one JSON.parse input. */
|
|
2395
|
-
const MAX_TEXT_LENGTH = 2 ** 29 - 24;
|
|
2396
|
-
|
|
2397
3075
|
/**
|
|
2398
|
-
*
|
|
2399
|
-
*
|
|
2400
|
-
*
|
|
2401
|
-
* @
|
|
2402
|
-
* @param report - the report
|
|
2403
|
-
* @param options - cancellation, progress and encoding
|
|
2404
|
-
* @returns the text
|
|
3076
|
+
* Whether a text that is not one JSON document is JSON Lines / NDJSON: its first two non-blank
|
|
3077
|
+
* lines are each a JSON object or array.
|
|
3078
|
+
* @param text - the document
|
|
3079
|
+
* @returns true for JSON Lines
|
|
2405
3080
|
*/
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
)
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
parts.push(chunk);
|
|
2424
|
-
}
|
|
2425
|
-
return parts.length === 1 ? parts[0] : parts.join("");
|
|
3081
|
+
function looksLikeJsonLines(text: string): boolean {
|
|
3082
|
+
const lines = text
|
|
3083
|
+
.slice(0, 64 * 1024)
|
|
3084
|
+
.split(/\r?\n/)
|
|
3085
|
+
.filter((line) => line.trim().length > 0)
|
|
3086
|
+
.slice(0, 2);
|
|
3087
|
+
return (
|
|
3088
|
+
lines.length === 2 &&
|
|
3089
|
+
lines.every((line) => {
|
|
3090
|
+
try {
|
|
3091
|
+
const value: unknown = JSON.parse(line);
|
|
3092
|
+
return typeof value === "object" && value !== null;
|
|
3093
|
+
} catch {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
})
|
|
3097
|
+
);
|
|
2426
3098
|
}
|
|
2427
3099
|
|
|
2428
3100
|
/**
|
|
@@ -2452,7 +3124,7 @@ function pushJgfNode(
|
|
|
2452
3124
|
element,
|
|
2453
3125
|
});
|
|
2454
3126
|
}
|
|
2455
|
-
ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null);
|
|
3127
|
+
ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null, element);
|
|
2456
3128
|
} catch (err) {
|
|
2457
3129
|
ctx.report.recordError(err, { element: String(id) });
|
|
2458
3130
|
}
|
|
@@ -2488,23 +3160,52 @@ function importHyperedges(
|
|
|
2488
3160
|
const { report } = ctx;
|
|
2489
3161
|
const policy = ctx.options.hyperedges;
|
|
2490
3162
|
if (policy === "error") {
|
|
2491
|
-
report.error(
|
|
2492
|
-
|
|
2493
|
-
|
|
3163
|
+
report.error(
|
|
3164
|
+
"unsupported",
|
|
3165
|
+
JSON_ISSUE.HYPEREDGE,
|
|
3166
|
+
`${hyperedges.length} hyperedge${plural(hyperedges.length)} (hyperedges: "error")`,
|
|
3167
|
+
{
|
|
3168
|
+
element: "hyperedges",
|
|
3169
|
+
},
|
|
3170
|
+
);
|
|
2494
3171
|
throw report.abort("hyperedges refused", { code: JSON_ISSUE.HYPEREDGE, count: hyperedges.length });
|
|
2495
3172
|
}
|
|
2496
3173
|
if (policy === "skip") {
|
|
2497
|
-
report.warning(
|
|
2498
|
-
|
|
2499
|
-
|
|
3174
|
+
report.warning(
|
|
3175
|
+
"unsupported",
|
|
3176
|
+
JSON_ISSUE.HYPEREDGES_SKIPPED,
|
|
3177
|
+
`${hyperedges.length} hyperedge${plural(hyperedges.length)} skipped`,
|
|
3178
|
+
{
|
|
3179
|
+
element: "hyperedges",
|
|
3180
|
+
},
|
|
3181
|
+
);
|
|
2500
3182
|
report.loss(
|
|
2501
3183
|
JSON_ISSUE.HYPEREDGES_SKIPPED,
|
|
2502
|
-
`${hyperedges.length} hyperedge(
|
|
3184
|
+
`${hyperedges.length} hyperedge${plural(hyperedges.length)} ${agree(hyperedges.length, "was", "were")} not imported`,
|
|
2503
3185
|
null,
|
|
2504
3186
|
hyperedges.length,
|
|
2505
3187
|
);
|
|
2506
3188
|
return;
|
|
2507
3189
|
}
|
|
3190
|
+
// each expanded edge is pushed on its own, so one the sink refuses is reported and counted
|
|
3191
|
+
// while the others stay
|
|
3192
|
+
const expand = (
|
|
3193
|
+
record: JsonRecord,
|
|
3194
|
+
pairs: readonly (readonly [NodeId, NodeId])[],
|
|
3195
|
+
kind: EdgeKind,
|
|
3196
|
+
element: string,
|
|
3197
|
+
): void => {
|
|
3198
|
+
for (const [u, v] of pairs) {
|
|
3199
|
+
try {
|
|
3200
|
+
push(record, u, v, kind, element, JGF_HYPEREDGE_KEYS);
|
|
3201
|
+
} catch (err) {
|
|
3202
|
+
ctx.skip(err, "edge", element);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
// the edges expanded so far: the cap is on the whole import, so many hyperedges just under it
|
|
3207
|
+
// cannot amplify without bound either
|
|
3208
|
+
const budget = { used: 0 };
|
|
2508
3209
|
for (let i = 0; i < hyperedges.length; i++) {
|
|
2509
3210
|
const element = `hyperedges[${i}]`;
|
|
2510
3211
|
const record = hyperedges[i];
|
|
@@ -2514,7 +3215,11 @@ function importHyperedges(
|
|
|
2514
3215
|
}
|
|
2515
3216
|
try {
|
|
2516
3217
|
if (Array.isArray(record.nodes)) {
|
|
2517
|
-
const members =
|
|
3218
|
+
const members = distinctMembers(
|
|
3219
|
+
ctx,
|
|
3220
|
+
record.nodes.map((raw) => ctx.requireId(raw, element)),
|
|
3221
|
+
element,
|
|
3222
|
+
);
|
|
2518
3223
|
if (members.length < 2) {
|
|
2519
3224
|
throw new GraphFormatError(
|
|
2520
3225
|
"E_INVALID_ID",
|
|
@@ -2522,18 +3227,21 @@ function importHyperedges(
|
|
|
2522
3227
|
{ reason: "hyperedge shape" },
|
|
2523
3228
|
);
|
|
2524
3229
|
}
|
|
2525
|
-
const
|
|
3230
|
+
const n = members.length;
|
|
3231
|
+
checkExpansion(policy === "star" ? n - 1 : (n * (n - 1)) / 2, budget, element);
|
|
3232
|
+
const pairs: [NodeId, NodeId][] = [];
|
|
2526
3233
|
if (policy === "star") {
|
|
2527
|
-
for (let k = 1; k <
|
|
2528
|
-
push(
|
|
3234
|
+
for (let k = 1; k < n; k++) {
|
|
3235
|
+
pairs.push([members[0], members[k]]);
|
|
2529
3236
|
}
|
|
2530
3237
|
} else {
|
|
2531
|
-
for (let a = 0; a <
|
|
2532
|
-
for (let b = a + 1; b <
|
|
2533
|
-
push(
|
|
3238
|
+
for (let a = 0; a < n; a++) {
|
|
3239
|
+
for (let b = a + 1; b < n; b++) {
|
|
3240
|
+
pairs.push([members[a], members[b]]);
|
|
2534
3241
|
}
|
|
2535
3242
|
}
|
|
2536
3243
|
}
|
|
3244
|
+
expand(record, pairs, directed ? "directed" : "undirected", element);
|
|
2537
3245
|
} else if (Array.isArray(record.source) && Array.isArray(record.target)) {
|
|
2538
3246
|
const sources = record.source.map((raw) => ctx.requireId(raw, element));
|
|
2539
3247
|
const targets = record.target.map((raw) => ctx.requireId(raw, element));
|
|
@@ -2544,11 +3252,13 @@ function importHyperedges(
|
|
|
2544
3252
|
{ reason: "hyperedge shape" },
|
|
2545
3253
|
);
|
|
2546
3254
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
3255
|
+
checkExpansion(sources.length * targets.length, budget, element);
|
|
3256
|
+
expand(
|
|
3257
|
+
record,
|
|
3258
|
+
sources.flatMap((s) => targets.map((t) => [s, t] as const)),
|
|
3259
|
+
"directed",
|
|
3260
|
+
element,
|
|
3261
|
+
);
|
|
2552
3262
|
} else {
|
|
2553
3263
|
report.error(
|
|
2554
3264
|
"validation-error",
|
|
@@ -2559,11 +3269,64 @@ function importHyperedges(
|
|
|
2559
3269
|
ctx.countSkipped("edge");
|
|
2560
3270
|
}
|
|
2561
3271
|
} catch (err) {
|
|
2562
|
-
|
|
3272
|
+
if (err instanceof ExpansionRefused) {
|
|
3273
|
+
// one hyperedge over the cap is skipped; the shared E_TOO_LARGE rule would stop the import
|
|
3274
|
+
report.error("unsupported", err.code, err.message, { element });
|
|
3275
|
+
ctx.countSkipped("edge");
|
|
3276
|
+
} else {
|
|
3277
|
+
ctx.skip(err, "edge", element);
|
|
3278
|
+
}
|
|
2563
3279
|
}
|
|
2564
3280
|
}
|
|
2565
3281
|
}
|
|
2566
3282
|
|
|
3283
|
+
/** A hyperedge whose expansion would pass MAX_HYPEREDGE_EXPANSION: that hyperedge alone is skipped. */
|
|
3284
|
+
class ExpansionRefused extends GraphFormatError {}
|
|
3285
|
+
|
|
3286
|
+
/** The most edges the hyperedges of one import expand into; a 20k-member clique would be 200M edges. */
|
|
3287
|
+
// ponytail: a fixed cap; make it an option if a real file needs more
|
|
3288
|
+
const MAX_HYPEREDGE_EXPANSION = 1_000_000;
|
|
3289
|
+
|
|
3290
|
+
/**
|
|
3291
|
+
* Refuse an expansion that would take the import beyond MAX_HYPEREDGE_EXPANSION expanded edges,
|
|
3292
|
+
* before any of it is pushed; otherwise count it against the budget.
|
|
3293
|
+
* @param count - the number of edges the expansion makes
|
|
3294
|
+
* @param budget - the edges the import's hyperedges expanded into so far
|
|
3295
|
+
* @param budget.used - that count, updated
|
|
3296
|
+
* @param element - the hyperedge's name
|
|
3297
|
+
*/
|
|
3298
|
+
function checkExpansion(count: number, budget: { used: number }, element: string): void {
|
|
3299
|
+
if (budget.used + count > MAX_HYPEREDGE_EXPANSION) {
|
|
3300
|
+
throw new ExpansionRefused(
|
|
3301
|
+
"E_TOO_LARGE",
|
|
3302
|
+
`${element}: the expansion makes ${count} edges, which with the ${budget.used} already expanded is more than ${MAX_HYPEREDGE_EXPANSION}; the hyperedge is skipped`,
|
|
3303
|
+
{ count, used: budget.used, limit: MAX_HYPEREDGE_EXPANSION },
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
budget.used += count;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
/**
|
|
3310
|
+
* The members of an undirected hyperedge with repeats removed (a repeat would expand into a
|
|
3311
|
+
* self-loop and parallel edges), each repeat reported as E_HYPEREDGE_SHAPE.
|
|
3312
|
+
* @param ctx - the context
|
|
3313
|
+
* @param members - the members as listed
|
|
3314
|
+
* @param element - the hyperedge's name
|
|
3315
|
+
* @returns the distinct members in first-listed order
|
|
3316
|
+
*/
|
|
3317
|
+
function distinctMembers(ctx: ImportContext, members: readonly NodeId[], element: string): NodeId[] {
|
|
3318
|
+
const distinct = [...new Set(members)];
|
|
3319
|
+
if (distinct.length < members.length) {
|
|
3320
|
+
ctx.report.error(
|
|
3321
|
+
"validation-error",
|
|
3322
|
+
JSON_ISSUE.HYPEREDGE_SHAPE,
|
|
3323
|
+
`${element} lists ${members.length - distinct.length} member${plural(members.length - distinct.length)} more than once; each is read once`,
|
|
3324
|
+
{ element },
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
return distinct;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
2567
3330
|
// ============================================================ Cytoscape
|
|
2568
3331
|
|
|
2569
3332
|
/**
|
|
@@ -2611,7 +3374,8 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
2611
3374
|
role: "position",
|
|
2612
3375
|
mutable: true,
|
|
2613
3376
|
nullable: true,
|
|
2614
|
-
|
|
3377
|
+
// 3D layouts written by Cytoscape-compatible tools carry a z
|
|
3378
|
+
extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
|
|
2615
3379
|
origin: { format: "json", namespace: "cytoscape" },
|
|
2616
3380
|
},
|
|
2617
3381
|
anyHas(nodes, "position"),
|
|
@@ -2648,6 +3412,14 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
2648
3412
|
if (index < 0) {
|
|
2649
3413
|
continue;
|
|
2650
3414
|
}
|
|
3415
|
+
if (present(data, "source") && present(data, "target")) {
|
|
3416
|
+
report.warning(
|
|
3417
|
+
"validation-error",
|
|
3418
|
+
JSON_ISSUE.INCONSISTENT,
|
|
3419
|
+
`${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`,
|
|
3420
|
+
{ element },
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
2651
3423
|
try {
|
|
2652
3424
|
for (const key of Object.keys(data)) {
|
|
2653
3425
|
if (key === "id") {
|
|
@@ -2663,31 +3435,65 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
2663
3435
|
}
|
|
2664
3436
|
continue;
|
|
2665
3437
|
}
|
|
2666
|
-
ctx.nodes.write(index, key, data[key], SUFFIX.data);
|
|
3438
|
+
ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
|
|
2667
3439
|
}
|
|
2668
3440
|
const { position } = record;
|
|
2669
3441
|
if (position !== undefined && position !== null) {
|
|
2670
|
-
|
|
3442
|
+
const z = isJsonObject(position) ? (position.z ?? 0) : 0;
|
|
3443
|
+
if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
|
|
2671
3444
|
point[0] = position.x;
|
|
2672
3445
|
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
2673
3446
|
point[1] = flipY(position.y);
|
|
3447
|
+
point[2] = z;
|
|
2674
3448
|
ctx.nodes.set(positionColumn, index, point);
|
|
2675
3449
|
} else {
|
|
2676
3450
|
report.error(
|
|
2677
3451
|
"validation-error",
|
|
2678
3452
|
JSON_ISSUE.BAD_VALUE,
|
|
2679
|
-
`${element}: position must be an object with numeric x and y`,
|
|
3453
|
+
`${element}: position must be an object with numeric x and y (and z) that are finite and within the f32 range`,
|
|
2680
3454
|
{ element },
|
|
2681
3455
|
);
|
|
2682
3456
|
}
|
|
2683
3457
|
}
|
|
2684
3458
|
writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
|
|
2685
|
-
writeElementKeys(ctx.nodes, index, record);
|
|
3459
|
+
writeElementKeys(ctx.nodes, index, record, element);
|
|
2686
3460
|
} catch (err) {
|
|
2687
3461
|
report.recordError(err, { element: String(id) });
|
|
2688
3462
|
}
|
|
2689
3463
|
}
|
|
2690
|
-
|
|
3464
|
+
resolveCytoscapeParents(ctx, parents, parentColumn);
|
|
3465
|
+
throwIfAborted(ctx.options.signal);
|
|
3466
|
+
|
|
3467
|
+
importCytoscapeEdges(ctx, edges, edgeClassesColumn);
|
|
3468
|
+
ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
/** One Cytoscape parent link: the parent's index and id, its document order and element name. */
|
|
3472
|
+
interface ParentLink {
|
|
3473
|
+
readonly parent: number;
|
|
3474
|
+
readonly id: NodeId;
|
|
3475
|
+
readonly order: number;
|
|
3476
|
+
readonly element: string;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
/**
|
|
3480
|
+
* Set the parent of each Cytoscape node once every node is known (a parent may come later): an
|
|
3481
|
+
* unknown parent is E_UNKNOWN_PARENT, a link that would make a node its own ancestor E_PARENT_CYCLE
|
|
3482
|
+
* (Cytoscape.js refuses those too); either link is dropped and the node kept.
|
|
3483
|
+
* @param ctx - the context
|
|
3484
|
+
* @param parents - the parent links in document order
|
|
3485
|
+
* @param parentColumn - the parent column
|
|
3486
|
+
*/
|
|
3487
|
+
function resolveCytoscapeParents(
|
|
3488
|
+
ctx: ImportContext,
|
|
3489
|
+
parents: readonly { readonly index: number; readonly parent: NodeId; readonly element: string }[],
|
|
3490
|
+
parentColumn: ColumnHandle,
|
|
3491
|
+
): void {
|
|
3492
|
+
const { report } = ctx;
|
|
3493
|
+
// the last link per child (a duplicate node's later parent wins), child -> its link
|
|
3494
|
+
const linkOf = new Map<number, ParentLink>();
|
|
3495
|
+
for (let order = 0; order < parents.length; order++) {
|
|
3496
|
+
const { index, parent, element } = parents[order];
|
|
2691
3497
|
const parentIndex = ctx.sink.indexOf(parent);
|
|
2692
3498
|
if (parentIndex === INVALID_INDEX) {
|
|
2693
3499
|
report.error(
|
|
@@ -2698,12 +3504,57 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
2698
3504
|
);
|
|
2699
3505
|
continue;
|
|
2700
3506
|
}
|
|
2701
|
-
|
|
3507
|
+
linkOf.set(index, { parent: parentIndex, id: parent, order, element });
|
|
2702
3508
|
}
|
|
2703
|
-
|
|
3509
|
+
// one walk per chain, each node visited once: on a cycle the link that came last in the document
|
|
3510
|
+
// (the one that closed it) is dropped
|
|
3511
|
+
const done = new Set<number>();
|
|
3512
|
+
for (const start of linkOf.keys()) {
|
|
3513
|
+
const path: number[] = [];
|
|
3514
|
+
const onPath = new Set<number>();
|
|
3515
|
+
let node: number | undefined = start;
|
|
3516
|
+
while (node !== undefined && !done.has(node) && !onPath.has(node)) {
|
|
3517
|
+
path.push(node);
|
|
3518
|
+
onPath.add(node);
|
|
3519
|
+
node = linkOf.get(node)?.parent;
|
|
3520
|
+
}
|
|
3521
|
+
if (node !== undefined && onPath.has(node)) {
|
|
3522
|
+
dropClosingLink(report, linkOf, path.slice(path.indexOf(node)));
|
|
3523
|
+
}
|
|
3524
|
+
for (const member of path) {
|
|
3525
|
+
done.add(member);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
for (const [index, { parent }] of linkOf) {
|
|
3529
|
+
ctx.nodes.set(parentColumn, index, parent);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
2704
3532
|
|
|
2705
|
-
|
|
2706
|
-
|
|
3533
|
+
/**
|
|
3534
|
+
* Drop the link of a parent cycle that came last in the document (the one that closed it).
|
|
3535
|
+
* @param report - the report
|
|
3536
|
+
* @param linkOf - the links by child, the dropped one removed
|
|
3537
|
+
* @param cycle - the nodes of the cycle
|
|
3538
|
+
*/
|
|
3539
|
+
function dropClosingLink(report: ImportReportBuilder, linkOf: Map<number, ParentLink>, cycle: readonly number[]): void {
|
|
3540
|
+
let closing: [number, ParentLink] | null = null;
|
|
3541
|
+
for (const member of cycle) {
|
|
3542
|
+
const link = linkOf.get(member);
|
|
3543
|
+
if (link !== undefined && (closing === null || link.order > closing[1].order)) {
|
|
3544
|
+
closing = [member, link];
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
if (closing === null) {
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
const { id, element } = closing[1];
|
|
3551
|
+
report.error(
|
|
3552
|
+
"validation-error",
|
|
3553
|
+
JSON_ISSUE.PARENT_CYCLE,
|
|
3554
|
+
`${element}: parent ${JSON.stringify(id)} would make the node its own ancestor; the link is dropped`,
|
|
3555
|
+
{ element },
|
|
3556
|
+
);
|
|
3557
|
+
linkOf.delete(closing[0]);
|
|
2707
3558
|
}
|
|
2708
3559
|
|
|
2709
3560
|
/**
|
|
@@ -2724,8 +3575,8 @@ function importCytoscapeEdges(ctx: ImportContext, edges: readonly unknown[], edg
|
|
|
2724
3575
|
continue;
|
|
2725
3576
|
}
|
|
2726
3577
|
const { data } = record;
|
|
2727
|
-
if (!
|
|
2728
|
-
ctx.missingEndpoint(element, `data.${
|
|
3578
|
+
if (!present(data, "source") || !present(data, "target")) {
|
|
3579
|
+
ctx.missingEndpoint(element, `data.${present(data, "source") ? "target" : "source"}`);
|
|
2729
3580
|
continue;
|
|
2730
3581
|
}
|
|
2731
3582
|
try {
|
|
@@ -2741,11 +3592,11 @@ function importCytoscapeEdges(ctx: ImportContext, edges: readonly unknown[], edg
|
|
|
2741
3592
|
const { weightFrom } = ctx.options;
|
|
2742
3593
|
for (const key of Object.keys(data)) {
|
|
2743
3594
|
if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
|
|
2744
|
-
ctx.edges.write(edge, key, data[key], SUFFIX.data);
|
|
3595
|
+
ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
|
|
2745
3596
|
}
|
|
2746
3597
|
}
|
|
2747
3598
|
writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
|
|
2748
|
-
writeElementKeys(ctx.edges, edge, record);
|
|
3599
|
+
writeElementKeys(ctx.edges, edge, record, element);
|
|
2749
3600
|
} catch (err) {
|
|
2750
3601
|
ctx.skip(err, "edge", element);
|
|
2751
3602
|
}
|
|
@@ -2765,10 +3616,21 @@ function cytoscapeSections(
|
|
|
2765
3616
|
): { readonly nodes: readonly unknown[]; readonly edges: readonly unknown[] } {
|
|
2766
3617
|
const { report } = ctx;
|
|
2767
3618
|
if (Array.isArray(elements)) {
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3619
|
+
const nodes: unknown[] = [];
|
|
3620
|
+
const edges: unknown[] = [];
|
|
3621
|
+
elements.forEach((item: unknown, i) => {
|
|
3622
|
+
if (isJsonObject(item) && item.group !== undefined && item.group !== "nodes" && item.group !== "edges") {
|
|
3623
|
+
report.error(
|
|
3624
|
+
"validation-error",
|
|
3625
|
+
JSON_ISSUE.BAD_VALUE,
|
|
3626
|
+
`elements[${i}]: group ${describe(item.group)} is neither "nodes" nor "edges"; classed by its endpoints`,
|
|
3627
|
+
{ element: `elements[${i}]` },
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
// a non-object item stays with the nodes, whose reader reports it as E_BAD_ELEMENT
|
|
3631
|
+
(isJsonObject(item) && !isNodeElement(item) ? edges : nodes).push(item);
|
|
3632
|
+
});
|
|
3633
|
+
return { nodes, edges };
|
|
2772
3634
|
}
|
|
2773
3635
|
if (isJsonObject(elements)) {
|
|
2774
3636
|
return {
|
|
@@ -2777,7 +3639,7 @@ function cytoscapeSections(
|
|
|
2777
3639
|
};
|
|
2778
3640
|
}
|
|
2779
3641
|
if (elements === undefined || elements === null) {
|
|
2780
|
-
report.
|
|
3642
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no elements", {
|
|
2781
3643
|
element: "elements",
|
|
2782
3644
|
});
|
|
2783
3645
|
return { nodes: [], edges: [] };
|
|
@@ -2785,6 +3647,27 @@ function cytoscapeSections(
|
|
|
2785
3647
|
return report.fail(JSON_ISSUE.SHAPE, `elements must be an object or an array, found ${describe(elements)}`);
|
|
2786
3648
|
}
|
|
2787
3649
|
|
|
3650
|
+
/**
|
|
3651
|
+
* Whether a Cytoscape element's position has a numeric z.
|
|
3652
|
+
* @param element - the element
|
|
3653
|
+
* @returns true when position.z is a number
|
|
3654
|
+
*/
|
|
3655
|
+
function hasPositionZ(element: unknown): boolean {
|
|
3656
|
+
return isJsonObject(element) && isJsonObject(element.position) && typeof element.position.z === "number";
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
/** The largest finite f32, the bound of a position coordinate. */
|
|
3660
|
+
const F32_MAX = 3.4028234663852886e38;
|
|
3661
|
+
|
|
3662
|
+
/**
|
|
3663
|
+
* Whether a value is a coordinate the f32 position column holds: a finite number within the f32 range.
|
|
3664
|
+
* @param value - the value
|
|
3665
|
+
* @returns true for a storable coordinate
|
|
3666
|
+
*/
|
|
3667
|
+
function isCoordinate(value: unknown): value is number {
|
|
3668
|
+
return typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= F32_MAX;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
2788
3671
|
/**
|
|
2789
3672
|
* Whether a flat Cytoscape element is a node: `group: "nodes"`, or no source / target in its data.
|
|
2790
3673
|
* @param element - the element
|
|
@@ -2844,24 +3727,38 @@ function writeClasses(
|
|
|
2844
3727
|
* @param writer - the table writer
|
|
2845
3728
|
* @param row - the row
|
|
2846
3729
|
* @param record - the element
|
|
3730
|
+
* @param element - the element name for issues
|
|
2847
3731
|
*/
|
|
2848
|
-
function writeElementKeys(writer: AttributeWriter, row: number, record: JsonRecord): void {
|
|
3732
|
+
function writeElementKeys(writer: AttributeWriter, row: number, record: JsonRecord, element: string): void {
|
|
2849
3733
|
for (const key of Object.keys(record)) {
|
|
2850
3734
|
if (CYTOSCAPE_STRUCTURAL_KEYS.has(key)) {
|
|
2851
3735
|
continue;
|
|
2852
3736
|
}
|
|
2853
3737
|
const name = CYTOSCAPE_ELEMENT_KEYS.has(key) ? key : `${key}${SUFFIX.element}`;
|
|
2854
|
-
writer.write(row, name, record[key], SUFFIX.data);
|
|
3738
|
+
writer.write(row, name, record[key], SUFFIX.data, element);
|
|
2855
3739
|
}
|
|
2856
3740
|
}
|
|
2857
3741
|
|
|
2858
3742
|
// ============================================================ the plugin
|
|
2859
3743
|
|
|
2860
3744
|
/**
|
|
2861
|
-
* The JSON importer plugin
|
|
3745
|
+
* The JSON importer plugin.
|
|
3746
|
+
* @category Built-in formats
|
|
2862
3747
|
*/
|
|
2863
3748
|
export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
|
|
2864
3749
|
format: "json",
|
|
3750
|
+
options: Object.freeze([
|
|
3751
|
+
"dialect",
|
|
3752
|
+
"edgesKey",
|
|
3753
|
+
"edgesPath",
|
|
3754
|
+
"indexLinks",
|
|
3755
|
+
"nodeIdKey",
|
|
3756
|
+
"nodesPath",
|
|
3757
|
+
"oboIds",
|
|
3758
|
+
"sourceKey",
|
|
3759
|
+
"targetKey",
|
|
3760
|
+
"typedefs",
|
|
3761
|
+
]),
|
|
2865
3762
|
extensions: Object.freeze([".json"]),
|
|
2866
3763
|
mimeTypes: Object.freeze(["application/json"]),
|
|
2867
3764
|
|
|
@@ -2873,7 +3770,8 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
|
|
|
2873
3770
|
* @returns the confidence
|
|
2874
3771
|
*/
|
|
2875
3772
|
sniff(head: Uint8Array): number {
|
|
2876
|
-
|
|
3773
|
+
// headBytes transcodes a UTF-16 head with a BOM, which import() decodes too
|
|
3774
|
+
const text = new TextDecoder("utf-8").decode(headBytes(head).subarray(0, SNIFF_BYTES));
|
|
2877
3775
|
const trimmed = (text.startsWith(BOM) ? text.slice(1) : text).trimStart();
|
|
2878
3776
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
2879
3777
|
return 0;
|
|
@@ -2899,7 +3797,7 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
|
|
|
2899
3797
|
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2900
3798
|
const json = resolveJsonOptions(options);
|
|
2901
3799
|
const report = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2902
|
-
const text = await
|
|
3800
|
+
const text = await readText(input, report, { ...resolved, bomlessUtf16: true });
|
|
2903
3801
|
const { root, dialect } = documentOf(parseDocument(text, report), json, report);
|
|
2904
3802
|
readGraph(root, dialect, sink, report, resolved, json, options);
|
|
2905
3803
|
return report.finish();
|
|
@@ -2920,7 +3818,7 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
|
|
|
2920
3818
|
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2921
3819
|
const json = resolveJsonOptions(options);
|
|
2922
3820
|
const report = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2923
|
-
const text = await
|
|
3821
|
+
const text = await readText(input, report, { ...resolved, bomlessUtf16: true });
|
|
2924
3822
|
const { root, dialect } = documentOf(parseDocument(text, report), json, report);
|
|
2925
3823
|
return listingsOf(root, dialect);
|
|
2926
3824
|
},
|
|
@@ -2941,7 +3839,7 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
|
|
|
2941
3839
|
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2942
3840
|
const json = resolveJsonOptions(options);
|
|
2943
3841
|
const first = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2944
|
-
const text = await
|
|
3842
|
+
const text = await readText(input, first, { ...resolved, bomlessUtf16: true });
|
|
2945
3843
|
const { root, dialect } = documentOf(parseDocument(text, first), json, first);
|
|
2946
3844
|
const graphs = listingsOf(root, dialect).length;
|
|
2947
3845
|
const reports: ImportReport[] = [];
|
|
@@ -2977,14 +3875,27 @@ function readGraph(
|
|
|
2977
3875
|
// the obographs reader refuses missing endpoints itself, so addMissingNodes false holds on any sink
|
|
2978
3876
|
reportSinkOptions(sink, options, report, dialect === "obographs");
|
|
2979
3877
|
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
3878
|
+
readDialect(ctx, root, dialect);
|
|
3879
|
+
ctx.reportDangling();
|
|
3880
|
+
ctx.nodes.reportNullOnly();
|
|
3881
|
+
ctx.edges.reportNullOnly();
|
|
3882
|
+
throwIfAborted(resolved.signal);
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
/**
|
|
3886
|
+
* Read one graph with the reader of its dialect.
|
|
3887
|
+
* @param ctx - the import context
|
|
3888
|
+
* @param root - the parsed document
|
|
3889
|
+
* @param dialect - its dialect
|
|
3890
|
+
*/
|
|
3891
|
+
function readDialect(ctx: ImportContext, root: unknown, dialect: JsonImportDialect): void {
|
|
2980
3892
|
if (dialect === "cytoscape") {
|
|
2981
3893
|
importCytoscape(ctx, root);
|
|
2982
|
-
throwIfAborted(resolved.signal);
|
|
2983
3894
|
return;
|
|
2984
3895
|
}
|
|
2985
3896
|
const doc = isJsonObject(root)
|
|
2986
3897
|
? root
|
|
2987
|
-
: report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
|
|
3898
|
+
: ctx.report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
|
|
2988
3899
|
switch (dialect) {
|
|
2989
3900
|
case "node-link":
|
|
2990
3901
|
case "d3":
|
|
@@ -3016,5 +3927,4 @@ function readGraph(
|
|
|
3016
3927
|
});
|
|
3017
3928
|
}
|
|
3018
3929
|
}
|
|
3019
|
-
throwIfAborted(resolved.signal);
|
|
3020
3930
|
}
|