@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
|
*
|
|
@@ -32,38 +32,49 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
|
|
34
34
|
import { uniqueColumnName } from "../../common/attributes.js";
|
|
35
|
-
import { AMBIGUOUS_GRAPH_NAME_CODE, BAD_VALUE_CODE, DANGLING_REFERENCE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, GRAPH_NOT_FOUND_CODE, HYPEREDGE_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, TOO_LARGE_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
|
|
35
|
+
import { AMBIGUOUS_GRAPH_NAME_CODE, BAD_VALUE_CODE, COLUMN_RENAMED_CODE, DANGLING_REFERENCE_CODE, DUPLICATE_ATTRIBUTE_CODE, DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE, ELEMENT_ISSUE, EMPTY_COLUMN_DROPPED_CODE, EMPTY_INPUT_CODE, ENCODING_FALLBACK_CODE, GRAPH_NOT_FOUND_CODE, HYPEREDGE_CODE, INPUT_ISSUE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, JSON_NONSTANDARD_NUMBER_CODE, MISSING_ENDPOINT_CODE, MISSING_ID_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, PARENT_CYCLE_CODE, PRECISION_CODE, ROLE_TAKEN_CODE, SYNTAX_CODE, TOO_LARGE_CODE, UNKNOWN_ELEMENT_CODE, UNKNOWN_ENCODING_CODE, UNKNOWN_PARENT_CODE, } from "../../common/codes.js";
|
|
36
36
|
import { DirectionResolver } from "../../common/direction.js";
|
|
37
37
|
import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
38
|
-
import { readText,
|
|
39
|
-
import { flipY,
|
|
40
|
-
import { chooseGraph, reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
|
|
38
|
+
import { readText, throwIfAborted } from "../../common/input.js";
|
|
39
|
+
import { findDuplicateKeys, flipY, MAYBE_INEXACT_EXPONENT, MAYBE_UNSAFE_INTEGER, rewriteNumbers, } from "../../common/json-elements.js";
|
|
40
|
+
import { chooseGraph, graphChosen, reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
|
|
41
|
+
import { agree, plural } from "../../common/plural.js";
|
|
41
42
|
import { ImportReportBuilder } from "../../common/report.js";
|
|
42
43
|
import { weightFromValue } from "../../common/weights.js";
|
|
43
|
-
import { sniffJsonDialectHead } from "../../sniff.js";
|
|
44
|
+
import { headBytes, sniffJsonDialectHead } from "../../sniff.js";
|
|
44
45
|
import { CLASSES_COLUMN, CYTOSCAPE_ELEMENT_KEYS, CYTOSCAPE_STRUCTURAL_KEYS, DIALECT_DEFAULT_DIRECTED, hasKey, isJsonImportDialect, isJsonObject, JSON_IMPORT_DIALECTS, META_KEY, NODE_LINK_SOURCE_KEYS, NODE_LINK_TARGET_KEYS, PARENT_COLUMN, POSITION_COLUMN, sniffJsonDialect, SUFFIX, } from "./dialect.js";
|
|
45
46
|
import { importObographs } from "./obographs.js";
|
|
46
47
|
/**
|
|
47
|
-
* The issue codes the JSON importer records
|
|
48
|
-
* the other importers
|
|
48
|
+
* The issue codes the JSON importer records, by name: the codes shared with
|
|
49
|
+
* the other importers and the JSON-specific ones. A key is the code without
|
|
49
50
|
* its severity and format prefixes.
|
|
51
|
+
* @category Built-in formats
|
|
50
52
|
*/
|
|
51
53
|
export const JSON_ISSUE = Object.freeze({
|
|
52
|
-
|
|
54
|
+
...INPUT_ISSUE,
|
|
55
|
+
...ELEMENT_ISSUE,
|
|
56
|
+
/** The text is empty or whitespace. The import stops. */
|
|
53
57
|
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
54
|
-
/** JSON.
|
|
58
|
+
/** The text is not valid JSON. The import stops. */
|
|
55
59
|
SYNTAX: SYNTAX_CODE,
|
|
56
|
-
/** No dialect matches the document's top-level shape. */
|
|
60
|
+
/** No dialect matches the document's top-level shape. The import stops. */
|
|
57
61
|
DIALECT: "E_JSON_DIALECT",
|
|
58
62
|
/** A section (nodes, edges, elements, graph) has the wrong JSON type. */
|
|
59
63
|
SHAPE: "E_JSON_SHAPE",
|
|
60
|
-
/**
|
|
61
|
-
|
|
64
|
+
/**
|
|
65
|
+
* A section the dialect expects is not there: a node-link document without its nodes or its edges array, an
|
|
66
|
+
* adjacency document without lists for some nodes, or a `nodesPath` / `edgesPath` that names nothing. Nothing is
|
|
67
|
+
* skipped: the graph is read without that section (nodes come from the edges, or there are no edges).
|
|
68
|
+
*/
|
|
69
|
+
MISSING_SECTION: "W_MISSING_SECTION",
|
|
62
70
|
/** A node record or an element is not an object. */
|
|
63
71
|
BAD_ELEMENT: "E_BAD_ELEMENT",
|
|
64
72
|
/** A node record has no id. */
|
|
65
73
|
MISSING_ID: MISSING_ID_CODE,
|
|
66
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* A node id is a JSON boolean or null, which graph-io cannot use as an id; the node is skipped. With `ids:
|
|
76
|
+
* "string"` it is read as the text "true", "false" or "null".
|
|
77
|
+
*/
|
|
67
78
|
UNSUPPORTED_ID: "E_UNSUPPORTED_ID",
|
|
68
79
|
/** An edge record has no source or no target. */
|
|
69
80
|
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
@@ -79,25 +90,35 @@ export const JSON_ISSUE = Object.freeze({
|
|
|
79
90
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
80
91
|
/** An edge id (Cytoscape data.id, graphology key, vis id) repeated by a later edge; the edge is skipped. */
|
|
81
92
|
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
82
|
-
/** Two
|
|
93
|
+
/** Two different id texts became the same number because `ids` is "number", so their nodes were merged. */
|
|
83
94
|
ID_MERGED: ID_MERGED_CODE,
|
|
84
95
|
/** Edge ids of mixed JSON types were stored as text. */
|
|
85
96
|
EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
|
|
86
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* The file holds several graphs and only the first was read. It is not added when `graphIndex` or `graphName` chose the graph.
|
|
99
|
+
* `importAllGraphs()` reads every one.
|
|
100
|
+
*/
|
|
87
101
|
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
88
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* `graphIndex` is past the end of the `graphs` array, or `graphName` matches none of its graphs. The import stops.
|
|
104
|
+
*/
|
|
89
105
|
GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
|
|
90
|
-
/** `graphName`
|
|
106
|
+
/** `graphName` matches more than one graph of the `graphs` array. The import stops. */
|
|
91
107
|
AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
|
|
92
|
-
/**
|
|
108
|
+
/** Obographs: an edge uses the outdated `subj` key of the OBO Graphs README; it is read as `sub`. */
|
|
93
109
|
OBOGRAPHS_SUBJ: "W_JSON_OBOGRAPHS_SUBJ",
|
|
94
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Obographs: an edge endpoint missing from `nodes` (a placeholder node is made, or the edge dropped under
|
|
112
|
+
* addMissingNodes false).
|
|
113
|
+
*/
|
|
95
114
|
DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
|
|
96
|
-
/** The document is longer than
|
|
115
|
+
/** The document is longer than a JavaScript string can hold. The import stops. */
|
|
97
116
|
TOO_LARGE: TOO_LARGE_CODE,
|
|
98
|
-
/** JGF hyperedges
|
|
117
|
+
/** A JGF hyperedge (an edge with more than two ends) while `hyperedges` is "error". */
|
|
99
118
|
HYPEREDGE: HYPEREDGE_CODE,
|
|
100
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* JGF hyperedges were skipped, because `hyperedges` is "skip" (the default). Pass "star" or "clique" to keep them.
|
|
121
|
+
*/
|
|
101
122
|
HYPEREDGES_SKIPPED: "W_HYPEREDGES_SKIPPED",
|
|
102
123
|
/** A JGF hyperedge with neither a nodes array nor source / target arrays. */
|
|
103
124
|
HYPEREDGE_SHAPE: "E_HYPEREDGE_SHAPE",
|
|
@@ -105,22 +126,59 @@ export const JSON_ISSUE = Object.freeze({
|
|
|
105
126
|
POSITIONAL_NODES: "W_POSITIONAL_NODES",
|
|
106
127
|
/** A node-link / d3 top-level key the importer does not read (the other of edges / links, an unknown key); it is dropped. */
|
|
107
128
|
UNREAD_KEY: "W_JSON_UNREAD_KEY",
|
|
108
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
|
|
131
|
+
* `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
|
|
132
|
+
*/
|
|
109
133
|
SINK_OPTION: SINK_OPTION_CODE,
|
|
110
|
-
/**
|
|
134
|
+
/** You set an option this format does not use; it had no effect. The message names the option. */
|
|
111
135
|
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
112
|
-
/** The input
|
|
136
|
+
/** The input is not valid UTF-8. The import stops. */
|
|
113
137
|
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
114
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Some bytes are not valid in the encoding that was chosen (by a byte order mark, the file's declaration or the
|
|
140
|
+
* `encoding` option). The import stops.
|
|
141
|
+
*/
|
|
115
142
|
INVALID_ENCODING: INVALID_ENCODING_CODE,
|
|
116
143
|
/** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
|
|
117
144
|
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
118
145
|
/** The document uses the non-standard tokens NaN / Infinity / -Infinity (Python's json writes them); read as numbers. */
|
|
119
|
-
NONSTANDARD_NUMBER:
|
|
146
|
+
NONSTANDARD_NUMBER: JSON_NONSTANDARD_NUMBER_CODE,
|
|
120
147
|
/** Integer literals beyond 2^53 were read as their exact digits (strings), not as rounded numbers. */
|
|
121
148
|
BIG_INTEGER: "W_JSON_BIG_INTEGER",
|
|
122
149
|
/** A declared encoding the platform cannot decode was ignored. */
|
|
123
150
|
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
151
|
+
/** An integer beyond 2^53 was stored as the nearest 64-bit float; pass `long: "string"` to keep every digit. */
|
|
152
|
+
PRECISION: PRECISION_CODE,
|
|
153
|
+
/**
|
|
154
|
+
* The document contradicts itself: a declared option its edges break (multigraph false with
|
|
155
|
+
* parallel links, graphology's options), a record whose section disagrees with its shape, a
|
|
156
|
+
* JGF inner id other than its key, the two listings of one adjacency edge.
|
|
157
|
+
*/
|
|
158
|
+
INCONSISTENT: "W_JSON_INCONSISTENT",
|
|
159
|
+
/** IndexLinks "auto" read integer endpoints as array positions although they also name node ids. */
|
|
160
|
+
INDEX_LINKS: "W_JSON_INDEX_LINKS",
|
|
161
|
+
/** A Cytoscape parent link that would close a cycle; that link is dropped. */
|
|
162
|
+
PARENT_CYCLE: PARENT_CYCLE_CODE,
|
|
163
|
+
/** An attribute key that is null on every element makes no column (NetworkX writes None as null). */
|
|
164
|
+
EMPTY_COLUMN_DROPPED: EMPTY_COLUMN_DROPPED_CODE,
|
|
165
|
+
/** OBO Graphs: a node type or synonym predicate outside the schema's set; kept as written, once per name. */
|
|
166
|
+
UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
|
|
167
|
+
/**
|
|
168
|
+
* A key repeated in one JSON object (JSON.parse keeps the last value, the earlier is dropped),
|
|
169
|
+
* or, in OBO Graphs, a single-valued OBO tag given twice in basicPropertyValues (the first is kept).
|
|
170
|
+
*/
|
|
171
|
+
DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
|
|
172
|
+
/**
|
|
173
|
+
* An attribute was renamed `<name>#<suffix>` because another attribute already has its name, for example
|
|
174
|
+
* two attributes declared with the same name.
|
|
175
|
+
*/
|
|
176
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
177
|
+
/**
|
|
178
|
+
* You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
|
|
179
|
+
* a plain attribute.
|
|
180
|
+
*/
|
|
181
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
124
182
|
});
|
|
125
183
|
/** The common options the JSON importer reads (the rest is reported by reportUnusedOptions). */
|
|
126
184
|
const USED_OPTIONS = new Set([
|
|
@@ -141,6 +199,8 @@ const USED_OPTIONS = new Set([
|
|
|
141
199
|
const FORMAT_DEFAULTS = { ids: "keep", defaultDirected: false, weightFrom: "weight" };
|
|
142
200
|
/** Bytes of the head sniff() inspects. */
|
|
143
201
|
const SNIFF_BYTES = 4096;
|
|
202
|
+
/** How many dangling endpoints the W_DANGLING_REFERENCE message names. */
|
|
203
|
+
const DANGLING_SHOWN = 5;
|
|
144
204
|
/** Elements pushed between two checks of the cancellation signal (the whole document is one chunk). */
|
|
145
205
|
const ABORT_CHECK_INTERVAL = 64;
|
|
146
206
|
/** The top-level keys whose presence in the head marks a graph document rather than arbitrary JSON. */
|
|
@@ -238,18 +298,24 @@ function pathOption(name, value) {
|
|
|
238
298
|
return segments;
|
|
239
299
|
}
|
|
240
300
|
/**
|
|
241
|
-
* The value at a path of object keys, or undefined when a
|
|
301
|
+
* The value at a path of object keys and array positions (`graphs.0.nodes`), or undefined when a
|
|
302
|
+
* step is missing.
|
|
242
303
|
* @param root - the document
|
|
243
|
-
* @param segments - the keys
|
|
304
|
+
* @param segments - the keys; a decimal integer indexes an array
|
|
244
305
|
* @returns the value
|
|
245
306
|
*/
|
|
246
307
|
function valueAt(root, segments) {
|
|
247
308
|
let value = root;
|
|
248
309
|
for (const segment of segments) {
|
|
249
|
-
if (
|
|
310
|
+
if (Array.isArray(value) && /^(0|[1-9]\d*)$/.test(segment) && Number(segment) < value.length) {
|
|
311
|
+
value = value[Number(segment)];
|
|
312
|
+
}
|
|
313
|
+
else if (isJsonObject(value) && hasKey(value, segment)) {
|
|
314
|
+
value = value[segment];
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
250
317
|
return undefined;
|
|
251
318
|
}
|
|
252
|
-
value = value[segment];
|
|
253
319
|
}
|
|
254
320
|
return value;
|
|
255
321
|
}
|
|
@@ -272,7 +338,7 @@ function applyPaths(root, json, report) {
|
|
|
272
338
|
const value = valueAt(root, segments);
|
|
273
339
|
if (value === undefined) {
|
|
274
340
|
const path = segments.join(".");
|
|
275
|
-
report.
|
|
341
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, `${option} ${JSON.stringify(path)} names nothing in the document`, { element: path });
|
|
276
342
|
return [];
|
|
277
343
|
}
|
|
278
344
|
return value;
|
|
@@ -339,12 +405,23 @@ function pathEdgesKey(json, edgesPath) {
|
|
|
339
405
|
* rule over the record, else node-link.
|
|
340
406
|
* @param record - the graph record
|
|
341
407
|
* @param forced - the caller's dialect option
|
|
408
|
+
* @param placedEdges - whether edgesPath placed the record's edge array (under links or edges)
|
|
342
409
|
* @returns the dialect
|
|
343
410
|
*/
|
|
344
|
-
function pathDialect(record, forced) {
|
|
411
|
+
function pathDialect(record, forced, placedEdges) {
|
|
345
412
|
if (forced !== "auto") {
|
|
346
413
|
return forced;
|
|
347
414
|
}
|
|
415
|
+
// edgesPath puts the edge array under links only so a bare d3 document stays d3; under edges,
|
|
416
|
+
// vis and graphology edges are told apart from NetworkX links again. A links key the document
|
|
417
|
+
// itself holds is NetworkX's and settles node-link.
|
|
418
|
+
if (placedEdges && isJsonObject(record) && hasKey(record, "links") && !hasKey(record, "edges")) {
|
|
419
|
+
const { links, ...rest } = record;
|
|
420
|
+
const asEdges = sniffJsonDialect({ ...rest, edges: links });
|
|
421
|
+
if (asEdges === "vis" || asEdges === "graphology") {
|
|
422
|
+
return asEdges;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
348
425
|
const sniffed = sniffJsonDialect(record);
|
|
349
426
|
return sniffed !== null && PATH_DIALECTS.has(sniffed) ? sniffed : "node-link";
|
|
350
427
|
}
|
|
@@ -361,7 +438,7 @@ function documentOf(parsed, json, report) {
|
|
|
361
438
|
return { root: parsed, dialect: detectDialect(parsed, json.dialect, report) };
|
|
362
439
|
}
|
|
363
440
|
const root = applyPaths(parsed, json, report);
|
|
364
|
-
const dialect = pathDialect(root, json.dialect);
|
|
441
|
+
const dialect = pathDialect(root, json.dialect, json.edgesPath !== null);
|
|
365
442
|
// vis and graphology read their edges from the edges key only
|
|
366
443
|
if (json.edgesPath !== null && (dialect === "vis" || dialect === "graphology") && isJsonObject(root)) {
|
|
367
444
|
const key = pathEdgesKey(json, json.edgesPath);
|
|
@@ -432,12 +509,16 @@ class AttributeWriter {
|
|
|
432
509
|
* Create a writer.
|
|
433
510
|
* @param sink - the sink
|
|
434
511
|
* @param domain - node or edge
|
|
512
|
+
* @param report - where a refused cell is recorded
|
|
435
513
|
*/
|
|
436
|
-
constructor(sink, domain) {
|
|
514
|
+
constructor(sink, domain, report) {
|
|
437
515
|
this.handles = new Map();
|
|
438
516
|
this.reservedNames = new Set();
|
|
517
|
+
/** Column names written only with null so far (NetworkX writes None as null). */
|
|
518
|
+
this.nullOnly = new Set();
|
|
439
519
|
this.sink = sink;
|
|
440
520
|
this.domain = domain;
|
|
521
|
+
this.report = report;
|
|
441
522
|
}
|
|
442
523
|
/**
|
|
443
524
|
* Declare a structural column up front; attribute keys with its name are suffixed from now on.
|
|
@@ -468,28 +549,53 @@ class AttributeWriter {
|
|
|
468
549
|
return this.lookup(name) !== INVALID_INDEX;
|
|
469
550
|
}
|
|
470
551
|
/**
|
|
471
|
-
* Write one attribute cell by its source key; null and undefined leave the row unset.
|
|
552
|
+
* Write one attribute cell by its source key; null and undefined leave the row unset. A cell the
|
|
553
|
+
* sink refuses is recorded and skipped, so the element's other keys are still written.
|
|
472
554
|
* @param row - the node or edge index
|
|
473
555
|
* @param key - the source key
|
|
474
556
|
* @param value - the JSON value
|
|
475
557
|
* @param suffix - the suffix applied when the key collides with a structural column
|
|
558
|
+
* @param element - the element name for an issue
|
|
476
559
|
*/
|
|
477
|
-
write(row, key, value, suffix) {
|
|
478
|
-
if (value === undefined
|
|
560
|
+
write(row, key, value, suffix, element) {
|
|
561
|
+
if (value === undefined) {
|
|
479
562
|
return;
|
|
480
563
|
}
|
|
481
564
|
const name = this.reservedNames.has(key) ? `${key}${suffix}` : key;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
565
|
+
if (value === null) {
|
|
566
|
+
this.nullOnly.add(name);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const cached = this.handles.get(name);
|
|
571
|
+
if (cached !== undefined) {
|
|
572
|
+
this.set(cached, row, value);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.set(name, row, value);
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
if (!(err instanceof GraphFormatError)) {
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
this.report.recordError(err, { element: `${element}.${key}` });
|
|
485
582
|
return;
|
|
486
583
|
}
|
|
487
|
-
this.set(name, row, value);
|
|
488
584
|
const handle = this.lookup(name);
|
|
489
585
|
if (handle !== INVALID_INDEX) {
|
|
490
586
|
this.handles.set(name, handle);
|
|
491
587
|
}
|
|
492
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Report the keys that were null on every element that had them: they made no column, so a
|
|
591
|
+
* round trip loses them (W_EMPTY_COLUMN_DROPPED, one warning per table).
|
|
592
|
+
*/
|
|
593
|
+
reportNullOnly() {
|
|
594
|
+
const names = [...this.nullOnly].filter((name) => this.lookup(name) === INVALID_INDEX);
|
|
595
|
+
if (names.length > 0) {
|
|
596
|
+
this.report.warning("missing-value", EMPTY_COLUMN_DROPPED_CODE, `${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`, { element: names[0] });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
493
599
|
/**
|
|
494
600
|
* Write through a handle or a name.
|
|
495
601
|
* @param column - the handle or name
|
|
@@ -516,6 +622,7 @@ class AttributeWriter {
|
|
|
516
622
|
// ============================================================ the import context
|
|
517
623
|
/**
|
|
518
624
|
* Everything one import call shares between the dialect readers.
|
|
625
|
+
* @category Plugin helpers
|
|
519
626
|
*/
|
|
520
627
|
export class ImportContext {
|
|
521
628
|
/**
|
|
@@ -531,14 +638,21 @@ export class ImportContext {
|
|
|
531
638
|
this.elementsSinceCheck = 0;
|
|
532
639
|
/** The direction the file declares (or the default), set by setHeader(). */
|
|
533
640
|
this.fileDirected = false;
|
|
641
|
+
/** Endpoints that named no node and became placeholder nodes, the first few kept for the message. */
|
|
642
|
+
this.danglingIds = [];
|
|
643
|
+
this.danglingCount = 0;
|
|
644
|
+
/** Set when the document has no node section: every node comes from an edge (E_MISSING_SECTION says so). */
|
|
645
|
+
this.nodesFromEdges = false;
|
|
646
|
+
/** Dangling endpoints whose id, as the other JSON type, is a node (`"1"` next to the node 1). */
|
|
647
|
+
this.nearMatches = [];
|
|
534
648
|
this.sink = sink;
|
|
535
649
|
this.report = report;
|
|
536
650
|
this.options = options;
|
|
537
651
|
this.json = json;
|
|
538
652
|
this.ids = new IdCoercer(options.ids);
|
|
539
653
|
this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
540
|
-
this.nodes = new AttributeWriter(sink, "node");
|
|
541
|
-
this.edges = new AttributeWriter(sink, "edge");
|
|
654
|
+
this.nodes = new AttributeWriter(sink, "node", report);
|
|
655
|
+
this.edges = new AttributeWriter(sink, "edge", report);
|
|
542
656
|
this.explicitDefaultDirected = explicitDefaultDirected;
|
|
543
657
|
}
|
|
544
658
|
/**
|
|
@@ -629,13 +743,14 @@ export class ImportContext {
|
|
|
629
743
|
* Add a node, counting it or recording the failure.
|
|
630
744
|
* @param id - the node id
|
|
631
745
|
* @param element - the element name
|
|
746
|
+
* @param why - more about a repeated id, for the duplicate warning
|
|
632
747
|
* @returns the node index, or -1 when the sink refused the node
|
|
633
748
|
*/
|
|
634
|
-
pushNode(id, element) {
|
|
749
|
+
pushNode(id, element, why = "") {
|
|
635
750
|
this.checkAbort();
|
|
636
751
|
const existing = this.sink.indexOf(id);
|
|
637
752
|
if (existing !== INVALID_INDEX) {
|
|
638
|
-
this.report.warning("merged", JSON_ISSUE.DUPLICATE_NODE, `${element}: node ${JSON.stringify(id)} already exists; its attributes are merged (the later values win)`, { element });
|
|
753
|
+
this.report.warning("merged", JSON_ISSUE.DUPLICATE_NODE, `${element}: node ${JSON.stringify(id)} already exists${why}; its attributes are merged (the later values win)`, { element });
|
|
639
754
|
return existing;
|
|
640
755
|
}
|
|
641
756
|
let index;
|
|
@@ -662,16 +777,50 @@ export class ImportContext {
|
|
|
662
777
|
pushEdge(source, target, kind, weight, element) {
|
|
663
778
|
this.checkAbort();
|
|
664
779
|
const before = this.sink.edgeCount;
|
|
665
|
-
// endpoints the sink creates (addMissingNodes) count as nodes too
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
created++;
|
|
669
|
-
}
|
|
780
|
+
// endpoints the sink creates (addMissingNodes) count as nodes too, and are reported
|
|
781
|
+
const missingSource = this.sink.indexOf(source) === INVALID_INDEX;
|
|
782
|
+
const missingTarget = source !== target && this.sink.indexOf(target) === INVALID_INDEX;
|
|
670
783
|
const edge = this.direction.addEdge(source, target, kind, weight, { element });
|
|
671
784
|
this.report.counts.edges += this.sink.edgeCount - before;
|
|
672
|
-
|
|
785
|
+
if (missingSource) {
|
|
786
|
+
this.dangling(source);
|
|
787
|
+
}
|
|
788
|
+
if (missingTarget) {
|
|
789
|
+
this.dangling(target);
|
|
790
|
+
}
|
|
673
791
|
return edge;
|
|
674
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Count a placeholder node an edge endpoint created.
|
|
795
|
+
* @param id - the endpoint
|
|
796
|
+
*/
|
|
797
|
+
dangling(id) {
|
|
798
|
+
this.report.counts.nodes++;
|
|
799
|
+
this.danglingCount++;
|
|
800
|
+
if (this.danglingIds.length < DANGLING_SHOWN) {
|
|
801
|
+
this.danglingIds.push(id);
|
|
802
|
+
}
|
|
803
|
+
const other = typeof id === "number" ? String(id) : Number(id);
|
|
804
|
+
if (this.nearMatches.length < DANGLING_SHOWN &&
|
|
805
|
+
(typeof other === "string" || String(other) === id) &&
|
|
806
|
+
this.sink.indexOf(other) !== INVALID_INDEX) {
|
|
807
|
+
this.nearMatches.push(id);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Report the edge endpoints that named no node (the shared W_DANGLING_REFERENCE, once with the
|
|
812
|
+
* count): each became a placeholder node, which is rarely what the file meant.
|
|
813
|
+
*/
|
|
814
|
+
reportDangling() {
|
|
815
|
+
if (this.danglingCount === 0 || this.nodesFromEdges) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const shown = this.danglingIds.map((id) => JSON.stringify(id)).join(", ");
|
|
819
|
+
const near = this.nearMatches.length > 0
|
|
820
|
+
? `; ${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`
|
|
821
|
+
: "";
|
|
822
|
+
this.report.warning("validation-error", JSON_ISSUE.DANGLING_REFERENCE, `${this.danglingCount} edge endpoint${plural(this.danglingCount)} ${agree(this.danglingCount, "names", "name")} no node and became placeholder nodes: ${shown}${this.danglingCount > DANGLING_SHOWN ? ", ..." : ""}${near}`, { element: String(this.danglingIds[0]) });
|
|
823
|
+
}
|
|
675
824
|
/**
|
|
676
825
|
* Check the cancellation signal every ABORT_CHECK_INTERVAL pushed elements, so an abort raised
|
|
677
826
|
* while the whole in-memory document is being walked rejects promptly.
|
|
@@ -805,16 +954,17 @@ export class ImportContext {
|
|
|
805
954
|
* @param dict - the nested attribute dict
|
|
806
955
|
* @param structural - the element keys that are not attributes
|
|
807
956
|
* @param weightFrom - the weight key to skip in the dict, or null
|
|
957
|
+
* @param element - the element name for issues
|
|
808
958
|
*/
|
|
809
|
-
writeNested(writer, row, record, dict, structural, weightFrom) {
|
|
959
|
+
writeNested(writer, row, record, dict, structural, weightFrom, element) {
|
|
810
960
|
for (const key of Object.keys(dict)) {
|
|
811
961
|
if (key !== weightFrom) {
|
|
812
|
-
writer.write(row, key, dict[key], SUFFIX.data);
|
|
962
|
+
writer.write(row, key, dict[key], SUFFIX.data, element);
|
|
813
963
|
}
|
|
814
964
|
}
|
|
815
965
|
for (const key of Object.keys(record)) {
|
|
816
966
|
if (!structural.has(key)) {
|
|
817
|
-
writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data);
|
|
967
|
+
writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data, element);
|
|
818
968
|
}
|
|
819
969
|
}
|
|
820
970
|
}
|
|
@@ -849,6 +999,7 @@ export class ImportContext {
|
|
|
849
999
|
declareEdgeIds(edges, read, name, unique) {
|
|
850
1000
|
let numbers = 0;
|
|
851
1001
|
let strings = 0;
|
|
1002
|
+
let others = 0;
|
|
852
1003
|
for (const edge of edges) {
|
|
853
1004
|
if (!isJsonObject(edge)) {
|
|
854
1005
|
continue;
|
|
@@ -860,17 +1011,21 @@ export class ImportContext {
|
|
|
860
1011
|
else if (typeof raw === "string") {
|
|
861
1012
|
strings++;
|
|
862
1013
|
}
|
|
1014
|
+
else if (raw !== undefined && raw !== null) {
|
|
1015
|
+
others++;
|
|
1016
|
+
}
|
|
863
1017
|
}
|
|
864
|
-
if (numbers + strings === 0) {
|
|
1018
|
+
if (numbers + strings + others === 0) {
|
|
865
1019
|
return null;
|
|
866
1020
|
}
|
|
867
|
-
|
|
1021
|
+
// ids of no usable type still get a column, so edgeIdValue() reports each instead of dropping it
|
|
1022
|
+
const dtype = strings === 0 && numbers > 0 ? "f64" : "string";
|
|
868
1023
|
const columnName = uniqueColumnName(name, "id", (candidate) => this.edges.taken(candidate));
|
|
869
1024
|
const handle = this.edges.declare({ name: columnName, dtype, role: "id", nullable: true, unique });
|
|
870
1025
|
const stringify = strings > 0 && numbers > 0;
|
|
871
1026
|
const seen = unique ? new Set() : null;
|
|
872
1027
|
if (stringify) {
|
|
873
|
-
this.report.warning("coercion", JSON_ISSUE.EDGE_ID_STRINGIFIED, `edge ids mix numbers and strings; ${numbers} numeric id(
|
|
1028
|
+
this.report.warning("coercion", JSON_ISSUE.EDGE_ID_STRINGIFIED, `edge ids mix numbers and strings; ${numbers} numeric id${plural(numbers)} stored as text`, { element: columnName });
|
|
874
1029
|
}
|
|
875
1030
|
return { handle, stringify, seen };
|
|
876
1031
|
}
|
|
@@ -918,7 +1073,8 @@ export class ImportContext {
|
|
|
918
1073
|
}
|
|
919
1074
|
// ============================================================ document level
|
|
920
1075
|
/**
|
|
921
|
-
* Parse the whole text; syntax errors and empty input abort the import.
|
|
1076
|
+
* Parse the whole text; syntax errors and empty input abort the import. A key repeated in one
|
|
1077
|
+
* object (which JSON.parse silently reduces to its last value) is reported per repetition.
|
|
922
1078
|
* @param text - the decoded text
|
|
923
1079
|
* @param report - the report
|
|
924
1080
|
* @returns the parsed value
|
|
@@ -929,6 +1085,8 @@ function parseDocument(text, report) {
|
|
|
929
1085
|
}
|
|
930
1086
|
let root;
|
|
931
1087
|
let syntaxError = null;
|
|
1088
|
+
// the text the syntax error was found in: the rewrite's when it parsed further than the original
|
|
1089
|
+
let errorText = text;
|
|
932
1090
|
try {
|
|
933
1091
|
root = JSON.parse(text);
|
|
934
1092
|
}
|
|
@@ -936,32 +1094,156 @@ function parseDocument(text, report) {
|
|
|
936
1094
|
syntaxError = err instanceof Error ? err.message : String(err);
|
|
937
1095
|
}
|
|
938
1096
|
// the fast path: strict JSON without a digit run long enough to be an unsafe integer
|
|
939
|
-
if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text)) {
|
|
1097
|
+
if (syntaxError === null && !MAYBE_UNSAFE_INTEGER.test(text) && !MAYBE_INEXACT_EXPONENT.test(text)) {
|
|
1098
|
+
reportDuplicateKeys(text, report);
|
|
940
1099
|
return root;
|
|
941
1100
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1101
|
+
let scan;
|
|
1102
|
+
try {
|
|
1103
|
+
scan = rewriteNumbers(text);
|
|
1104
|
+
if (scan.tokens.size > 0 || scan.bigIntegers.length > 0) {
|
|
1105
|
+
try {
|
|
1106
|
+
root = JSON.parse(scan.text, scan.tokens.size > 0 ? scan.revive : undefined);
|
|
1107
|
+
syntaxError = null;
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
if (err instanceof RangeError) {
|
|
1111
|
+
throw err;
|
|
1112
|
+
}
|
|
1113
|
+
// the rewrite read past the non-standard tokens: its error is the real one
|
|
1114
|
+
syntaxError = err instanceof Error ? err.message : String(err);
|
|
1115
|
+
errorText = scan.text;
|
|
1116
|
+
}
|
|
947
1117
|
}
|
|
948
|
-
|
|
949
|
-
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
if (!(err instanceof RangeError)) {
|
|
1121
|
+
throw err;
|
|
950
1122
|
}
|
|
1123
|
+
// the rewrite (quoted big integers, sentinel strings) is longer than one string can hold
|
|
1124
|
+
const message = `the document with its numbers rewritten is longer than the most one JavaScript string holds`;
|
|
1125
|
+
report.error("unsupported", JSON_ISSUE.TOO_LARGE, message);
|
|
1126
|
+
throw report.abort(message, { code: JSON_ISSUE.TOO_LARGE });
|
|
951
1127
|
}
|
|
952
1128
|
if (syntaxError !== null) {
|
|
953
|
-
return report.fail(JSON_ISSUE.SYNTAX,
|
|
1129
|
+
return report.fail(JSON_ISSUE.SYNTAX, syntaxMessage(text, syntaxError), {
|
|
1130
|
+
line: syntaxLine(errorText, syntaxError),
|
|
1131
|
+
});
|
|
954
1132
|
}
|
|
1133
|
+
reportDuplicateKeys(text, report);
|
|
955
1134
|
if (scan.tokens.size > 0) {
|
|
956
|
-
report.warning("coercion", JSON_ISSUE.NONSTANDARD_NUMBER, `the document uses the non-standard token(
|
|
1135
|
+
report.warning("coercion", JSON_ISSUE.NONSTANDARD_NUMBER, `the document uses the non-standard token${plural(scan.tokens.size)} ${[...scan.tokens].join(", ")}, which strict JSON does not allow; read as numbers`);
|
|
957
1136
|
}
|
|
958
1137
|
if (scan.bigIntegers.length > 0) {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1138
|
+
report.warning("precision", JSON_ISSUE.BIG_INTEGER, `${scan.bigIntegers.length} integer${plural(scan.bigIntegers.length)} beyond 2^53 kept as text so no digit is lost: ${listed(scan.bigIntegers)}`);
|
|
1139
|
+
}
|
|
1140
|
+
if (scan.inexact.length > 0) {
|
|
1141
|
+
report.warning("precision", JSON_ISSUE.PRECISION, `${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)}`);
|
|
962
1142
|
}
|
|
963
1143
|
return root;
|
|
964
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* The first literals of a list for a message, with the count of the rest.
|
|
1147
|
+
* @param literals - the literals
|
|
1148
|
+
* @returns the text
|
|
1149
|
+
*/
|
|
1150
|
+
function listed(literals) {
|
|
1151
|
+
const more = literals.length - BIG_INTEGERS_SHOWN;
|
|
1152
|
+
const shown = literals.slice(0, BIG_INTEGERS_SHOWN).join(", ");
|
|
1153
|
+
return more > 0 ? `${shown} and ${more} more` : shown;
|
|
1154
|
+
}
|
|
1155
|
+
/** How many repeated keys are reported one by one before the rest is summed up. */
|
|
1156
|
+
const DUPLICATE_KEYS_SHOWN = 10;
|
|
1157
|
+
/**
|
|
1158
|
+
* Report every key repeated in one object (JSON.parse keeps the last value, so the earlier one is
|
|
1159
|
+
* gone): one warning per repetition up to DUPLICATE_KEYS_SHOWN, then one with the remaining count.
|
|
1160
|
+
* @param text - the document text
|
|
1161
|
+
* @param report - the report
|
|
1162
|
+
*/
|
|
1163
|
+
function reportDuplicateKeys(text, report) {
|
|
1164
|
+
const found = findDuplicateKeys(text);
|
|
1165
|
+
let line = 1;
|
|
1166
|
+
let counted = 0;
|
|
1167
|
+
for (const { key, offset } of found.slice(0, DUPLICATE_KEYS_SHOWN)) {
|
|
1168
|
+
line += countLines(text, counted, offset);
|
|
1169
|
+
counted = offset;
|
|
1170
|
+
report.warning("merged", JSON_ISSUE.DUPLICATE_ATTRIBUTE, `key ${JSON.stringify(key)} appears twice in one object; the earlier value is dropped (JSON.parse keeps the last)`, { line, element: key });
|
|
1171
|
+
}
|
|
1172
|
+
if (found.length > DUPLICATE_KEYS_SHOWN) {
|
|
1173
|
+
report.warning("merged", JSON_ISSUE.DUPLICATE_ATTRIBUTE, `${found.length - DUPLICATE_KEYS_SHOWN} more repeated key${plural(found.length - DUPLICATE_KEYS_SHOWN)}; each earlier value is dropped`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* The line breaks in a range of the text.
|
|
1178
|
+
* @param text - the text
|
|
1179
|
+
* @param from - the start offset
|
|
1180
|
+
* @param to - the end offset
|
|
1181
|
+
* @returns how many \n the range holds
|
|
1182
|
+
*/
|
|
1183
|
+
function countLines(text, from, to) {
|
|
1184
|
+
let lines = 0;
|
|
1185
|
+
for (let i = text.indexOf("\n", from); i >= 0 && i < to; i = text.indexOf("\n", i + 1)) {
|
|
1186
|
+
lines++;
|
|
1187
|
+
}
|
|
1188
|
+
return lines;
|
|
1189
|
+
}
|
|
1190
|
+
/** The longest text the line of an unpositioned syntax error is searched in (a bisection of JSON.parse calls). */
|
|
1191
|
+
const SYNTAX_SEARCH_LIMIT = 64 * 1024 * 1024;
|
|
1192
|
+
/**
|
|
1193
|
+
* The 1-based line of a JSON.parse error: from the message's "(line L" or "position N" when the
|
|
1194
|
+
* engine gives one, the last line for an unexpected end, else the shortest prefix that fails the
|
|
1195
|
+
* same way (V8 names no position for "Unexpected token").
|
|
1196
|
+
* @param text - the document
|
|
1197
|
+
* @param message - the parser's message
|
|
1198
|
+
* @returns the line
|
|
1199
|
+
*/
|
|
1200
|
+
function syntaxLine(text, message) {
|
|
1201
|
+
const line = /\(line (\d+)/.exec(message);
|
|
1202
|
+
if (line !== null) {
|
|
1203
|
+
return Number(line[1]);
|
|
1204
|
+
}
|
|
1205
|
+
const position = /position (\d+)/.exec(message);
|
|
1206
|
+
if (position !== null) {
|
|
1207
|
+
return countLines(text, 0, Number(position[1])) + 1;
|
|
1208
|
+
}
|
|
1209
|
+
if (!message.startsWith("Unexpected token") || text.length > SYNTAX_SEARCH_LIMIT) {
|
|
1210
|
+
// ponytail: an unexpected end (or a huge text) is placed on the last line
|
|
1211
|
+
return countLines(text, 0, text.length) + 1;
|
|
1212
|
+
}
|
|
1213
|
+
// the shortest prefix that holds the offending token: shorter prefixes end early instead
|
|
1214
|
+
let lo = 0;
|
|
1215
|
+
let hi = text.length;
|
|
1216
|
+
while (lo < hi) {
|
|
1217
|
+
const mid = (lo + hi) >>> 1;
|
|
1218
|
+
let failsOnToken = false;
|
|
1219
|
+
try {
|
|
1220
|
+
JSON.parse(text.slice(0, mid + 1));
|
|
1221
|
+
}
|
|
1222
|
+
catch (err) {
|
|
1223
|
+
failsOnToken = err instanceof Error && err.message.startsWith("Unexpected token");
|
|
1224
|
+
}
|
|
1225
|
+
if (failsOnToken) {
|
|
1226
|
+
hi = mid;
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
lo = mid + 1;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return countLines(text, 0, lo) + 1;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* The E_SYNTAX message: the parser's, with a hint when the text is JSON Lines (one document per
|
|
1236
|
+
* line, as apoc.export.json and many loggers write), which this importer does not read.
|
|
1237
|
+
* @param text - the document
|
|
1238
|
+
* @param message - the parser's message
|
|
1239
|
+
* @returns the message
|
|
1240
|
+
*/
|
|
1241
|
+
function syntaxMessage(text, message) {
|
|
1242
|
+
if (looksLikeJsonLines(text)) {
|
|
1243
|
+
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})`;
|
|
1244
|
+
}
|
|
1245
|
+
return `invalid JSON: ${message}`;
|
|
1246
|
+
}
|
|
965
1247
|
/** How many of the integers kept as text the warning lists. */
|
|
966
1248
|
const BIG_INTEGERS_SHOWN = 10;
|
|
967
1249
|
/**
|
|
@@ -1024,7 +1306,7 @@ function flagOf(value, what, fallback, report) {
|
|
|
1024
1306
|
}
|
|
1025
1307
|
/**
|
|
1026
1308
|
* The endpoint key and value of an edge record: the explicit key when given, else the first of the
|
|
1027
|
-
* default keys the record has.
|
|
1309
|
+
* default keys the record has (a null value counts as absent).
|
|
1028
1310
|
* @param record - the edge record
|
|
1029
1311
|
* @param explicit - the caller's key, or null
|
|
1030
1312
|
* @param defaults - the default keys
|
|
@@ -1032,15 +1314,57 @@ function flagOf(value, what, fallback, report) {
|
|
|
1032
1314
|
*/
|
|
1033
1315
|
function endpointOf(record, explicit, defaults) {
|
|
1034
1316
|
if (explicit !== null) {
|
|
1035
|
-
return { key:
|
|
1317
|
+
return { key: present(record, explicit) ? explicit : null, value: record[explicit] };
|
|
1036
1318
|
}
|
|
1037
1319
|
for (const key of defaults) {
|
|
1038
|
-
if (
|
|
1320
|
+
if (present(record, key)) {
|
|
1039
1321
|
return { key, value: record[key] };
|
|
1040
1322
|
}
|
|
1041
1323
|
}
|
|
1042
1324
|
return { key: null, value: undefined };
|
|
1043
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Whether a record has a key with a value other than null: a null endpoint is an absent one.
|
|
1328
|
+
* @param record - the record
|
|
1329
|
+
* @param key - the key
|
|
1330
|
+
* @returns true when the key holds a non-null value
|
|
1331
|
+
*/
|
|
1332
|
+
function present(record, key) {
|
|
1333
|
+
return hasKey(record, key) && record[key] !== null;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Report every key of a record the dialect does not read (W_JSON_UNREAD_KEY): it is dropped.
|
|
1337
|
+
* @param ctx - the context
|
|
1338
|
+
* @param record - the document or graph record
|
|
1339
|
+
* @param known - the keys the dialect reads
|
|
1340
|
+
* @param prefix - the record's path for the messages (`""` for the document, `"graph."`)
|
|
1341
|
+
*/
|
|
1342
|
+
function reportUnreadKeys(ctx, record, known, prefix) {
|
|
1343
|
+
for (const key of Object.keys(record)) {
|
|
1344
|
+
if (known.has(key)) {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const value = record[key];
|
|
1348
|
+
const entries = Array.isArray(value) && value.length === 1 ? "entry" : "entries";
|
|
1349
|
+
const what = Array.isArray(value) ? `${String(value.length)} ${entries}` : describe(value);
|
|
1350
|
+
ctx.report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `${prefix.length === 0 ? "top-level key" : "key"} ${prefix}${key} (${what}) is not read; dropped`, { element: `${prefix}${key}` });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/** The keys of a vis.js document the reader reads. */
|
|
1354
|
+
const VIS_DOCUMENT_KEYS = new Set(["nodes", "edges"]);
|
|
1355
|
+
/** The keys of a graphology serialization the reader reads. */
|
|
1356
|
+
const GRAPHOLOGY_DOCUMENT_KEYS = new Set(["nodes", "edges", "options", "attributes"]);
|
|
1357
|
+
/** The keys of a JGF graph object the reader reads. */
|
|
1358
|
+
const JGF_GRAPH_KEYS = new Set([
|
|
1359
|
+
"id",
|
|
1360
|
+
"type",
|
|
1361
|
+
"label",
|
|
1362
|
+
"directed",
|
|
1363
|
+
"metadata",
|
|
1364
|
+
"nodes",
|
|
1365
|
+
"edges",
|
|
1366
|
+
"hyperedges",
|
|
1367
|
+
]);
|
|
1044
1368
|
/**
|
|
1045
1369
|
* Whether any object of an array has a key with a non-null value.
|
|
1046
1370
|
* @param items - the array
|
|
@@ -1075,26 +1399,17 @@ function importNodeLink(ctx, root, dialect) {
|
|
|
1075
1399
|
const nodes = arraySection(root.nodes, "nodes", report);
|
|
1076
1400
|
const edges = arraySection(root[edgesKey], edgesKey, report);
|
|
1077
1401
|
if (nodes === null) {
|
|
1078
|
-
report.
|
|
1402
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no nodes array; nodes come from the edges", { element: "nodes" });
|
|
1403
|
+
ctx.nodesFromEdges = true;
|
|
1079
1404
|
}
|
|
1080
1405
|
if (edges === null) {
|
|
1081
|
-
report.
|
|
1406
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, `the document has no ${edgesKey} array; the graph has no edges`, { element: edgesKey });
|
|
1082
1407
|
}
|
|
1083
1408
|
const directed = flagOf(root.directed, "directed", ctx.defaultDirected(dialect), report);
|
|
1084
1409
|
const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
|
|
1085
1410
|
ctx.setHeader(directed);
|
|
1086
1411
|
ctx.writeGraphDict(root.graph, "graph");
|
|
1087
|
-
|
|
1088
|
-
if (key !== "nodes" && key !== edgesKey && key !== "directed" && key !== "multigraph" && key !== "graph") {
|
|
1089
|
-
const value = root[key];
|
|
1090
|
-
const what = Array.isArray(value)
|
|
1091
|
-
? `${String(value.length)} ${value.length === 1 ? "entry" : "entries"}`
|
|
1092
|
-
: describe(value);
|
|
1093
|
-
report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${what}) is not read; dropped`, {
|
|
1094
|
-
element: key,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1412
|
+
reportUnreadKeys(ctx, root, new Set(["nodes", edgesKey, "directed", "multigraph", "graph"]), "");
|
|
1098
1413
|
const nodeList = nodes ?? [];
|
|
1099
1414
|
const edgeList = edges ?? [];
|
|
1100
1415
|
ctx.sink.reserve(nodeList.length, edgeList.length);
|
|
@@ -1105,8 +1420,10 @@ function importNodeLink(ctx, root, dialect) {
|
|
|
1105
1420
|
if (!positional && nodeIdKey === null) {
|
|
1106
1421
|
const candidates = ctx.options.nodeIdFrom === "label" ? ["label", "name", "id"] : ["id", "name"];
|
|
1107
1422
|
nodeIdKey = candidates.find((key) => anyHas(nodeList, key)) ?? null;
|
|
1108
|
-
if (ctx.options.nodeIdFrom === "label" && nodeIdKey === "id") {
|
|
1109
|
-
report.warning("unsupported", JSON_ISSUE.OPTION_IGNORED,
|
|
1423
|
+
if (ctx.options.nodeIdFrom === "label" && (nodeIdKey === "id" || nodeIdKey === "name")) {
|
|
1424
|
+
report.warning("unsupported", JSON_ISSUE.OPTION_IGNORED, nodeIdKey === "id"
|
|
1425
|
+
? 'nodeIdFrom "label": no node has a label or name key; ids are read from "id"'
|
|
1426
|
+
: 'nodeIdFrom "label": no node has a "label" key; ids are read from "name"', { element: "nodeIdFrom" });
|
|
1110
1427
|
}
|
|
1111
1428
|
if (nodeIdKey === null) {
|
|
1112
1429
|
if (nodeList.length > 0) {
|
|
@@ -1132,6 +1449,9 @@ function importNodeLink(ctx, root, dialect) {
|
|
|
1132
1449
|
({ indexLinks } = json);
|
|
1133
1450
|
}
|
|
1134
1451
|
const positionIds = indexLinks ? [] : null;
|
|
1452
|
+
if (indexLinks && !positional && json.indexLinks === "auto" && nodeIdKey !== null) {
|
|
1453
|
+
reportAmbiguousIndexLinks(ctx, nodeList, edgeList, nodeIdKey);
|
|
1454
|
+
}
|
|
1135
1455
|
for (let i = 0; i < nodeList.length; i++) {
|
|
1136
1456
|
const element = `nodes[${i}]`;
|
|
1137
1457
|
const record = nodeList[i];
|
|
@@ -1154,14 +1474,14 @@ function importNodeLink(ctx, root, dialect) {
|
|
|
1154
1474
|
const index = ctx.pushNode(id, element);
|
|
1155
1475
|
if (index >= 0) {
|
|
1156
1476
|
pushed = id;
|
|
1157
|
-
writeFlat(ctx
|
|
1477
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
|
|
1158
1478
|
}
|
|
1159
1479
|
}
|
|
1160
1480
|
}
|
|
1161
1481
|
positionIds?.push(pushed);
|
|
1162
1482
|
}
|
|
1163
1483
|
throwIfAborted(ctx.options.signal);
|
|
1164
|
-
const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds);
|
|
1484
|
+
const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds, multigraph);
|
|
1165
1485
|
ctx.setMeta({
|
|
1166
1486
|
dialect,
|
|
1167
1487
|
edgesKey,
|
|
@@ -1178,13 +1498,19 @@ function importNodeLink(ctx, root, dialect) {
|
|
|
1178
1498
|
* @param edgeList - the edge records
|
|
1179
1499
|
* @param edgesKey - the top-level key they came from, for issues
|
|
1180
1500
|
* @param positionIds - the ids by array position under index links, or null
|
|
1501
|
+
* @param multigraph - the declared multigraph flag, or null when the document has none
|
|
1181
1502
|
* @returns the source and target keys the first well-formed edge used (null when none did)
|
|
1182
1503
|
*/
|
|
1183
|
-
function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
|
|
1504
|
+
function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds, multigraph) {
|
|
1184
1505
|
const { json } = ctx;
|
|
1185
1506
|
const kind = ctx.uniformKind();
|
|
1186
1507
|
let sourceKey = null;
|
|
1187
1508
|
let targetKey = null;
|
|
1509
|
+
// multigraph false: the pairs seen, to report parallel links; true: the (pair, key) triples, since
|
|
1510
|
+
// NetworkX reads a repeated (u, v, key) as one edge
|
|
1511
|
+
// ponytail: one string per edge whenever the flag is declared; a hash of the index pair if it shows in profiles
|
|
1512
|
+
const pairs = multigraph === null ? null : new Set();
|
|
1513
|
+
let parallels = 0;
|
|
1188
1514
|
for (let i = 0; i < edgeList.length; i++) {
|
|
1189
1515
|
const element = `${edgesKey}[${i}]`;
|
|
1190
1516
|
const record = edgeList[i];
|
|
@@ -1208,11 +1534,25 @@ function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
|
|
|
1208
1534
|
continue;
|
|
1209
1535
|
}
|
|
1210
1536
|
const weight = ctx.weightOf(record);
|
|
1537
|
+
if (pairs !== null) {
|
|
1538
|
+
const pair = pairKey(u, v, kind === "directed");
|
|
1539
|
+
if (multigraph === false) {
|
|
1540
|
+
parallels += pairs.has(pair) ? 1 : 0;
|
|
1541
|
+
pairs.add(pair);
|
|
1542
|
+
}
|
|
1543
|
+
else if (hasKey(record, "key")) {
|
|
1544
|
+
const triple = `${pair} ${JSON.stringify(record.key)}`;
|
|
1545
|
+
if (pairs.has(triple)) {
|
|
1546
|
+
throw new GraphFormatError(JSON_ISSUE.DUPLICATE_EDGE_ID, `${element}: key ${JSON.stringify(record.key)} is repeated for the same pair; NetworkX reads it as the same edge, so it is skipped`, { id: String(record.key) });
|
|
1547
|
+
}
|
|
1548
|
+
pairs.add(triple);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1211
1551
|
const edge = ctx.pushEdge(u, v, kind, weight, element);
|
|
1212
1552
|
const { weightFrom } = ctx.options;
|
|
1213
1553
|
for (const key of Object.keys(record)) {
|
|
1214
1554
|
if (key !== source.key && key !== target.key && key !== weightFrom) {
|
|
1215
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
1555
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1216
1556
|
}
|
|
1217
1557
|
}
|
|
1218
1558
|
}
|
|
@@ -1220,32 +1560,71 @@ function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
|
|
|
1220
1560
|
ctx.skip(err, "edge", element);
|
|
1221
1561
|
}
|
|
1222
1562
|
}
|
|
1563
|
+
if (parallels > 0) {
|
|
1564
|
+
ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `the document declares multigraph false, but ${parallels} link${plural(parallels)} ${agree(parallels, "repeats", "repeat")} the endpoints of an earlier one; all are kept`, { element: edgesKey });
|
|
1565
|
+
}
|
|
1223
1566
|
return { source: sourceKey, target: targetKey };
|
|
1224
1567
|
}
|
|
1225
1568
|
/**
|
|
1226
|
-
*
|
|
1569
|
+
* The key of an endpoint pair: ordered for a directed edge, unordered for an undirected one.
|
|
1570
|
+
* @param u - the source
|
|
1571
|
+
* @param v - the target
|
|
1572
|
+
* @param directed - whether the order matters
|
|
1573
|
+
* @returns the key
|
|
1574
|
+
*/
|
|
1575
|
+
function pairKey(u, v, directed) {
|
|
1576
|
+
const a = JSON.stringify(u);
|
|
1577
|
+
const b = JSON.stringify(v);
|
|
1578
|
+
return directed || a <= b ? `${a} ${b}` : `${b} ${a}`;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Warn when indexLinks "auto" read integer endpoints as array positions although some also equal
|
|
1582
|
+
* a node id's text (`"2"`): the file may mean the ids, and the edges are then off by the positions.
|
|
1227
1583
|
* @param ctx - the context
|
|
1584
|
+
* @param nodes - the node records
|
|
1585
|
+
* @param edges - the edge records
|
|
1586
|
+
* @param nodeIdKey - the node id key
|
|
1587
|
+
*/
|
|
1588
|
+
function reportAmbiguousIndexLinks(ctx, nodes, edges, nodeIdKey) {
|
|
1589
|
+
const ids = new Set();
|
|
1590
|
+
for (const node of nodes) {
|
|
1591
|
+
if (isJsonObject(node) && typeof node[nodeIdKey] === "string") {
|
|
1592
|
+
ids.add(node[nodeIdKey]);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
for (const edge of edges) {
|
|
1596
|
+
if (!isJsonObject(edge)) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
for (const value of [
|
|
1600
|
+
endpointOf(edge, ctx.json.sourceKey, NODE_LINK_SOURCE_KEYS).value,
|
|
1601
|
+
endpointOf(edge, ctx.json.targetKey, NODE_LINK_TARGET_KEYS).value,
|
|
1602
|
+
]) {
|
|
1603
|
+
if (typeof value === "number" && ids.has(String(value))) {
|
|
1604
|
+
ctx.report.warning("coercion", JSON_ISSUE.INDEX_LINKS, `integer endpoints are read as array positions, but ${value} is also a node id; pass indexLinks: false to read the endpoints as ids`, { element: "indexLinks" });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Write the flat attributes of a node record (every own key the filter keeps).
|
|
1228
1612
|
* @param writer - the node writer
|
|
1229
1613
|
* @param index - the node index
|
|
1230
1614
|
* @param record - the record
|
|
1231
1615
|
* @param id - the node id, for issues
|
|
1232
1616
|
* @param keep - which keys are attributes
|
|
1233
1617
|
*/
|
|
1234
|
-
function writeFlat(
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
writer.write(index, key, record[key], SUFFIX.data);
|
|
1239
|
-
}
|
|
1618
|
+
function writeFlat(writer, index, record, id, keep) {
|
|
1619
|
+
for (const key of Object.keys(record)) {
|
|
1620
|
+
if (keep(key)) {
|
|
1621
|
+
writer.write(index, key, record[key], SUFFIX.data, String(id));
|
|
1240
1622
|
}
|
|
1241
1623
|
}
|
|
1242
|
-
catch (err) {
|
|
1243
|
-
ctx.report.recordError(err, { element: String(id) });
|
|
1244
|
-
}
|
|
1245
1624
|
}
|
|
1246
1625
|
/**
|
|
1247
|
-
* The d3 index-link heuristic: endpoints are array positions when every endpoint is a
|
|
1248
|
-
* non-negative integer and no node id is a number (a numeric id would make the endpoints ids;
|
|
1626
|
+
* The d3 index-link heuristic: endpoints are array positions when every endpoint is a number, at
|
|
1627
|
+
* least one is a non-negative integer, and no node id is a number (a numeric id would make the endpoints ids;
|
|
1249
1628
|
* research note 07: d3 links reference nodes by array index and are never coerced to ids). An
|
|
1250
1629
|
* index at or beyond the node count is then E_BAD_INDEX, never a new numeric node.
|
|
1251
1630
|
* @param nodes - the node records
|
|
@@ -1270,10 +1649,14 @@ function looksIndexLinked(nodes, edges, nodeIdKey, json) {
|
|
|
1270
1649
|
}
|
|
1271
1650
|
const s = endpointOf(edge, json.sourceKey, NODE_LINK_SOURCE_KEYS).value;
|
|
1272
1651
|
const t = endpointOf(edge, json.targetKey, NODE_LINK_TARGET_KEYS).value;
|
|
1273
|
-
|
|
1652
|
+
// a fractional or negative number is a bad index (E_BAD_INDEX), never a reason to read every
|
|
1653
|
+
// other endpoint as an id; a missing endpoint is reported on its own
|
|
1654
|
+
if ((s !== undefined && typeof s !== "number") || (t !== undefined && typeof t !== "number")) {
|
|
1274
1655
|
return false;
|
|
1275
1656
|
}
|
|
1276
|
-
|
|
1657
|
+
if (isIndexBelow(s, Infinity) || isIndexBelow(t, Infinity)) {
|
|
1658
|
+
seen++;
|
|
1659
|
+
}
|
|
1277
1660
|
}
|
|
1278
1661
|
return seen > 0;
|
|
1279
1662
|
}
|
|
@@ -1313,6 +1696,8 @@ function importVis(ctx, root) {
|
|
|
1313
1696
|
const { report, json } = ctx;
|
|
1314
1697
|
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
1315
1698
|
const edges = arraySection(root.edges, "edges", report) ?? [];
|
|
1699
|
+
// vis.js options and groups are rendering settings, not graph data
|
|
1700
|
+
reportUnreadKeys(ctx, root, VIS_DOCUMENT_KEYS, "");
|
|
1316
1701
|
ctx.setHeader(ctx.defaultDirected("vis"));
|
|
1317
1702
|
ctx.sink.reserve(nodes.length, edges.length);
|
|
1318
1703
|
const nodeIdKey = json.nodeIdKey ?? "id";
|
|
@@ -1331,7 +1716,7 @@ function importVis(ctx, root) {
|
|
|
1331
1716
|
}
|
|
1332
1717
|
const index = ctx.pushNode(id, element);
|
|
1333
1718
|
if (index >= 0) {
|
|
1334
|
-
writeFlat(ctx
|
|
1719
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
|
|
1335
1720
|
}
|
|
1336
1721
|
}
|
|
1337
1722
|
throwIfAborted(ctx.options.signal);
|
|
@@ -1367,7 +1752,7 @@ function importVis(ctx, root) {
|
|
|
1367
1752
|
const { weightFrom } = ctx.options;
|
|
1368
1753
|
for (const key of Object.keys(record)) {
|
|
1369
1754
|
if (key !== source.key && key !== target.key && key !== "id" && key !== weightFrom) {
|
|
1370
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
1755
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1371
1756
|
}
|
|
1372
1757
|
}
|
|
1373
1758
|
}
|
|
@@ -1375,12 +1760,21 @@ function importVis(ctx, root) {
|
|
|
1375
1760
|
ctx.skip(err, "edge", element);
|
|
1376
1761
|
}
|
|
1377
1762
|
}
|
|
1763
|
+
// vis.js draws arrows from the `arrows` edge option; the file declares no graph direction, so the
|
|
1764
|
+
// arrows stay an attribute and the graph is read as the dialect's default
|
|
1765
|
+
const arrows = kind === "undirected"
|
|
1766
|
+
? edges.filter((e) => isJsonObject(e) && e.arrows !== undefined && e.arrows !== null && e.arrows !== "")
|
|
1767
|
+
.length
|
|
1768
|
+
: 0;
|
|
1769
|
+
if (arrows > 0) {
|
|
1770
|
+
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${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)`, { element: "arrows" });
|
|
1771
|
+
}
|
|
1378
1772
|
ctx.setMeta({ dialect: "vis", nodeIdKey, sourceKey: sourceKey ?? undefined, targetKey: targetKey ?? undefined }, ctx.weightOriginPatch());
|
|
1379
1773
|
}
|
|
1380
1774
|
// ============================================================ NetworkX adjacency_data / tree_data
|
|
1381
1775
|
/**
|
|
1382
1776
|
* Read a NetworkX adjacency_data document: `nodes` as in node-link, and `adjacency[i]` the
|
|
1383
|
-
*
|
|
1777
|
+
* neighbor list of `nodes[i]`, one `{ id, key?, ...attributes }` entry per edge. An undirected
|
|
1384
1778
|
* file lists every edge from both ends, so an entry whose mirror (the same pair and `key`) was
|
|
1385
1779
|
* already read is that edge again and is not pushed twice; a self-loop is listed once.
|
|
1386
1780
|
* @param ctx - the context
|
|
@@ -1391,7 +1785,7 @@ function importAdjacency(ctx, root) {
|
|
|
1391
1785
|
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
1392
1786
|
const adjacency = arraySection(root.adjacency, "adjacency", report);
|
|
1393
1787
|
if (adjacency === null) {
|
|
1394
|
-
report.
|
|
1788
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no adjacency array; the graph has no edges", {
|
|
1395
1789
|
element: "adjacency",
|
|
1396
1790
|
});
|
|
1397
1791
|
}
|
|
@@ -1428,7 +1822,7 @@ function importAdjacency(ctx, root) {
|
|
|
1428
1822
|
const index = ctx.pushNode(id, element);
|
|
1429
1823
|
if (index >= 0) {
|
|
1430
1824
|
pushed = id;
|
|
1431
|
-
writeFlat(ctx
|
|
1825
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
|
|
1432
1826
|
}
|
|
1433
1827
|
}
|
|
1434
1828
|
}
|
|
@@ -1437,8 +1831,9 @@ function importAdjacency(ctx, root) {
|
|
|
1437
1831
|
throwIfAborted(ctx.options.signal);
|
|
1438
1832
|
const kind = ctx.uniformKind();
|
|
1439
1833
|
const { weightFrom } = ctx.options;
|
|
1440
|
-
// undirected: entries read once whose mirror is still to come, by pair and key
|
|
1834
|
+
// undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
|
|
1441
1835
|
const pending = new Map();
|
|
1836
|
+
let disagreeing = 0;
|
|
1442
1837
|
for (let i = 0; i < lists.length; i++) {
|
|
1443
1838
|
const element = `adjacency[${i}]`;
|
|
1444
1839
|
const list = lists[i];
|
|
@@ -1449,7 +1844,7 @@ function importAdjacency(ctx, root) {
|
|
|
1449
1844
|
const source = i < owners.length ? owners[i] : null;
|
|
1450
1845
|
if (source === null) {
|
|
1451
1846
|
const why = i < nodes.length ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
|
|
1452
|
-
report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge(
|
|
1847
|
+
report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`, {
|
|
1453
1848
|
element,
|
|
1454
1849
|
});
|
|
1455
1850
|
report.counts.skippedEdges += list.length;
|
|
@@ -1472,13 +1867,17 @@ function importAdjacency(ctx, root) {
|
|
|
1472
1867
|
ctx.countSkipped("edge");
|
|
1473
1868
|
continue;
|
|
1474
1869
|
}
|
|
1475
|
-
if (!directed
|
|
1476
|
-
|
|
1870
|
+
if (!directed) {
|
|
1871
|
+
const mirror = mirroredEntry(pending, source, target, record, idKey);
|
|
1872
|
+
disagreeing += mirror === "disagrees" ? 1 : 0;
|
|
1873
|
+
if (mirror !== "new") {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1477
1876
|
}
|
|
1478
1877
|
const edge = ctx.pushEdge(source, target, kind, ctx.weightOf(record), entry);
|
|
1479
1878
|
for (const key of Object.keys(record)) {
|
|
1480
1879
|
if (key !== idKey && key !== weightFrom) {
|
|
1481
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
1880
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1482
1881
|
}
|
|
1483
1882
|
}
|
|
1484
1883
|
}
|
|
@@ -1487,35 +1886,80 @@ function importAdjacency(ctx, root) {
|
|
|
1487
1886
|
}
|
|
1488
1887
|
}
|
|
1489
1888
|
}
|
|
1889
|
+
reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, disagreeing, pending);
|
|
1490
1890
|
ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
|
|
1491
1891
|
}
|
|
1492
1892
|
/**
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
* @param
|
|
1893
|
+
* Report what an adjacency_data document leaves inconsistent: nodes without an adjacency list
|
|
1894
|
+
* (NetworkX writes one per node, so the file was cut), undirected edges whose two listings carry
|
|
1895
|
+
* different attributes (the first is kept), and undirected entries whose mirror never came.
|
|
1896
|
+
* @param ctx - the context
|
|
1897
|
+
* @param nodes - the number of node records
|
|
1898
|
+
* @param lists - the number of adjacency lists, or null when the section is missing (reported already)
|
|
1899
|
+
* @param disagreeing - the mirrors whose attributes differ
|
|
1900
|
+
* @param pending - the entries still awaiting their mirror
|
|
1901
|
+
*/
|
|
1902
|
+
function reportAdjacencyShape(ctx, nodes, lists, disagreeing, pending) {
|
|
1903
|
+
const { report } = ctx;
|
|
1904
|
+
if (lists !== null && lists < nodes) {
|
|
1905
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, `${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`, { element: "adjacency" });
|
|
1906
|
+
}
|
|
1907
|
+
if (disagreeing > 0) {
|
|
1908
|
+
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `the two listings of ${disagreeing} undirected edge${plural(disagreeing)} ${agree(disagreeing, "disagrees", "disagree")} on their attributes; the first listing is kept`, { element: "adjacency" });
|
|
1909
|
+
}
|
|
1910
|
+
let unmatched = 0;
|
|
1911
|
+
for (const waiting of pending.values()) {
|
|
1912
|
+
unmatched += waiting.length;
|
|
1913
|
+
}
|
|
1914
|
+
if (unmatched > 0) {
|
|
1915
|
+
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${unmatched} undirected adjacency entr(ies) have no mirror in the other node's list; each is read as one edge`, { element: "adjacency" });
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* A JSON.stringify replacer that writes every object's keys in sorted order.
|
|
1920
|
+
* @param _key - the key
|
|
1921
|
+
* @param value - the value
|
|
1922
|
+
* @returns the value, an object with its keys sorted
|
|
1923
|
+
*/
|
|
1924
|
+
function sortedKeys(_key, value) {
|
|
1925
|
+
return isJsonObject(value) ? Object.fromEntries(Object.entries(value).sort(([x], [y]) => (x < y ? -1 : 1))) : value;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Whether an undirected adjacency entry is the mirror of one already read (the same unordered pair
|
|
1929
|
+
* and key), consuming it; otherwise the entry is remembered as awaiting its mirror. A self-loop is
|
|
1930
|
+
* listed once and never awaits one.
|
|
1931
|
+
* @param pending - the entries awaiting their mirror, by owner, other end and key: their attributes
|
|
1497
1932
|
* @param source - the owner of the list
|
|
1498
1933
|
* @param target - the entry's node
|
|
1499
|
-
* @param
|
|
1500
|
-
* @
|
|
1934
|
+
* @param record - the entry
|
|
1935
|
+
* @param idKey - the entry's id key (not an attribute)
|
|
1936
|
+
* @returns "new" for an entry to push, "mirror" for the second listing of one read, "disagrees"
|
|
1937
|
+
* for a second listing with other attributes
|
|
1501
1938
|
*/
|
|
1502
|
-
function
|
|
1939
|
+
function mirroredEntry(pending, source, target, record, idKey) {
|
|
1503
1940
|
const a = JSON.stringify(source);
|
|
1504
1941
|
const b = JSON.stringify(target);
|
|
1505
1942
|
if (a === b) {
|
|
1506
|
-
return
|
|
1943
|
+
return "new";
|
|
1507
1944
|
}
|
|
1508
|
-
const k = JSON.stringify(key ?? null);
|
|
1945
|
+
const k = JSON.stringify(record.key ?? null);
|
|
1946
|
+
// key order is not part of a JSON object, so two listings in another order still agree
|
|
1947
|
+
const attributes = JSON.stringify(Object.fromEntries(Object.entries(record).filter(([key]) => key !== idKey)), sortedKeys);
|
|
1509
1948
|
// an entry is the mirror of one listed by the other end, never of a parallel entry of its own list
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return true;
|
|
1949
|
+
const waiting = pending.get(`${b} ${a} ${k}`);
|
|
1950
|
+
if (waiting !== undefined && waiting.length > 0) {
|
|
1951
|
+
const first = waiting.shift();
|
|
1952
|
+
return first === attributes ? "mirror" : "disagrees";
|
|
1515
1953
|
}
|
|
1516
1954
|
const own = `${a} ${b} ${k}`;
|
|
1517
|
-
|
|
1518
|
-
|
|
1955
|
+
const list = pending.get(own);
|
|
1956
|
+
if (list === undefined) {
|
|
1957
|
+
pending.set(own, [attributes]);
|
|
1958
|
+
}
|
|
1959
|
+
else {
|
|
1960
|
+
list.push(attributes);
|
|
1961
|
+
}
|
|
1962
|
+
return "new";
|
|
1519
1963
|
}
|
|
1520
1964
|
/**
|
|
1521
1965
|
* Whether a value is a list of [string, value] pairs, the shape adjacency_data gives the graph dict.
|
|
@@ -1555,7 +1999,7 @@ function importTree(ctx, root) {
|
|
|
1555
1999
|
id = null;
|
|
1556
2000
|
}
|
|
1557
2001
|
else {
|
|
1558
|
-
writeFlat(ctx
|
|
2002
|
+
writeFlat(ctx.nodes, ctx.sink.indexOf(id), record, id, (key) => key !== idKey && key !== "children");
|
|
1559
2003
|
if (parent !== null) {
|
|
1560
2004
|
try {
|
|
1561
2005
|
ctx.pushEdge(parent, id, kind, undefined, element);
|
|
@@ -1579,7 +2023,7 @@ function importTree(ctx, root) {
|
|
|
1579
2023
|
}
|
|
1580
2024
|
// ============================================================ graphology
|
|
1581
2025
|
/**
|
|
1582
|
-
* Read a graphology
|
|
2026
|
+
* Read a graphology serialization: `options.type` decides the header direction ("mixed" or absent:
|
|
1583
2027
|
* from the edges' `undirected` flags), `options.multi` the declared multigraph flag, node `key`
|
|
1584
2028
|
* the id, `attributes` the columns, edge `key` the edge id.
|
|
1585
2029
|
* @param ctx - the context
|
|
@@ -1589,6 +2033,7 @@ function importGraphology(ctx, root) {
|
|
|
1589
2033
|
const { report } = ctx;
|
|
1590
2034
|
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
1591
2035
|
const edges = arraySection(root.edges, "edges", report) ?? [];
|
|
2036
|
+
reportUnreadKeys(ctx, root, GRAPHOLOGY_DOCUMENT_KEYS, "");
|
|
1592
2037
|
const options = isJsonObject(root.options) ? root.options : {};
|
|
1593
2038
|
if (hasKey(root, "options") && !isJsonObject(root.options)) {
|
|
1594
2039
|
report.warning("validation-error", JSON_ISSUE.BAD_FLAG, "options is not an object; ignored", {
|
|
@@ -1638,6 +2083,9 @@ function importGraphology(ctx, root) {
|
|
|
1638
2083
|
}
|
|
1639
2084
|
throwIfAborted(ctx.options.signal);
|
|
1640
2085
|
const ids = ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true);
|
|
2086
|
+
// what the edges do against the declared options: graphology itself refuses such a document
|
|
2087
|
+
const violations = { selfLoops: 0, parallels: 0, flags: 0 };
|
|
2088
|
+
const pairs = multi === false ? new Set() : null;
|
|
1641
2089
|
for (let i = 0; i < edges.length; i++) {
|
|
1642
2090
|
const element = `edges[${i}]`;
|
|
1643
2091
|
const record = edges[i];
|
|
@@ -1645,8 +2093,8 @@ function importGraphology(ctx, root) {
|
|
|
1645
2093
|
ctx.badElement("edge", element);
|
|
1646
2094
|
continue;
|
|
1647
2095
|
}
|
|
1648
|
-
if (!
|
|
1649
|
-
ctx.missingEndpoint(element,
|
|
2096
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2097
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
1650
2098
|
continue;
|
|
1651
2099
|
}
|
|
1652
2100
|
try {
|
|
@@ -1662,19 +2110,62 @@ function importGraphology(ctx, root) {
|
|
|
1662
2110
|
}
|
|
1663
2111
|
else {
|
|
1664
2112
|
kind = type;
|
|
2113
|
+
if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
|
|
2114
|
+
violations.flags++;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
const { attributes: rawAttributes } = record;
|
|
2118
|
+
if (rawAttributes !== undefined && rawAttributes !== null && !isJsonObject(rawAttributes)) {
|
|
2119
|
+
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: attributes must be an object, found ${describe(rawAttributes)}`, { element });
|
|
1665
2120
|
}
|
|
1666
|
-
const attributes = isJsonObject(
|
|
2121
|
+
const attributes = isJsonObject(rawAttributes) ? rawAttributes : {};
|
|
1667
2122
|
const idValue = ctx.edgeIdValue(ids, record.key);
|
|
1668
2123
|
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
1669
2124
|
ctx.setEdgeId(ids, edge, idValue);
|
|
1670
|
-
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom);
|
|
2125
|
+
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
|
|
2126
|
+
if (allowSelfLoops === false && u === v) {
|
|
2127
|
+
violations.selfLoops++;
|
|
2128
|
+
}
|
|
2129
|
+
if (pairs !== null) {
|
|
2130
|
+
const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
|
|
2131
|
+
violations.parallels += pairs.has(pair) ? 1 : 0;
|
|
2132
|
+
pairs.add(pair);
|
|
2133
|
+
}
|
|
1671
2134
|
}
|
|
1672
2135
|
catch (err) {
|
|
1673
2136
|
ctx.skip(err, "edge", element);
|
|
1674
2137
|
}
|
|
1675
2138
|
}
|
|
2139
|
+
reportGraphologyViolations(ctx, violations, type);
|
|
1676
2140
|
ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
|
|
1677
2141
|
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Report the edges that contradict a graphology document's declared options (W_JSON_INCONSISTENT,
|
|
2144
|
+
* one warning per kind); they are kept.
|
|
2145
|
+
* @param ctx - the context
|
|
2146
|
+
* @param violations - the counts
|
|
2147
|
+
* @param violations.selfLoops - self-loops under allowSelfLoops false
|
|
2148
|
+
* @param violations.parallels - parallel edges under multi false
|
|
2149
|
+
* @param violations.flags - per-edge undirected flags against options.type
|
|
2150
|
+
* @param type - options.type
|
|
2151
|
+
*/
|
|
2152
|
+
function reportGraphologyViolations(ctx, violations, type) {
|
|
2153
|
+
const notes = [];
|
|
2154
|
+
if (violations.selfLoops > 0) {
|
|
2155
|
+
notes.push(`options.allowSelfLoops is false, but ${violations.selfLoops} self-loop${plural(violations.selfLoops)} ${agree(violations.selfLoops, "is", "are")} listed`);
|
|
2156
|
+
}
|
|
2157
|
+
if (violations.parallels > 0) {
|
|
2158
|
+
notes.push(`options.multi is false, but ${violations.parallels} parallel edge${plural(violations.parallels)} ${agree(violations.parallels, "is", "are")} listed`);
|
|
2159
|
+
}
|
|
2160
|
+
if (violations.flags > 0) {
|
|
2161
|
+
notes.push(`options.type is ${type}, but ${violations.flags} edge${plural(violations.flags)} ${agree(violations.flags, "carries", "carry")} the other undirected flag; read as ${type}`);
|
|
2162
|
+
}
|
|
2163
|
+
for (const note of notes) {
|
|
2164
|
+
ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${note}; every edge is kept`, {
|
|
2165
|
+
element: "options",
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
1678
2169
|
/**
|
|
1679
2170
|
* Push a node whose attributes live in a nested dict (graphology `attributes`, JGF `metadata`);
|
|
1680
2171
|
* element-level keys the dialect does not define are kept with the `#element` suffix.
|
|
@@ -1696,7 +2187,7 @@ function pushNestedNode(ctx, id, record, dictKey, structural, element) {
|
|
|
1696
2187
|
if (dict !== undefined && dict !== null && !isJsonObject(dict)) {
|
|
1697
2188
|
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: ${dictKey} must be an object, found ${describe(dict)}`, { element });
|
|
1698
2189
|
}
|
|
1699
|
-
ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null);
|
|
2190
|
+
ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null, element);
|
|
1700
2191
|
}
|
|
1701
2192
|
catch (err) {
|
|
1702
2193
|
ctx.report.recordError(err, { element: String(id) });
|
|
@@ -1760,6 +2251,9 @@ function importJgf(ctx, root) {
|
|
|
1760
2251
|
ctx.countSkipped("node");
|
|
1761
2252
|
continue;
|
|
1762
2253
|
}
|
|
2254
|
+
if (record !== null && hasKey(record, "id") && record.id !== key) {
|
|
2255
|
+
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${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}`, { element });
|
|
2256
|
+
}
|
|
1763
2257
|
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
1764
2258
|
}
|
|
1765
2259
|
}
|
|
@@ -1796,7 +2290,7 @@ function importJgf(ctx, root) {
|
|
|
1796
2290
|
element,
|
|
1797
2291
|
});
|
|
1798
2292
|
}
|
|
1799
|
-
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom);
|
|
2293
|
+
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom, element);
|
|
1800
2294
|
};
|
|
1801
2295
|
for (let i = 0; i < edges.length; i++) {
|
|
1802
2296
|
const element = `edges[${i}]`;
|
|
@@ -1805,8 +2299,8 @@ function importJgf(ctx, root) {
|
|
|
1805
2299
|
ctx.badElement("edge", element);
|
|
1806
2300
|
continue;
|
|
1807
2301
|
}
|
|
1808
|
-
if (!
|
|
1809
|
-
ctx.missingEndpoint(element,
|
|
2302
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2303
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
1810
2304
|
continue;
|
|
1811
2305
|
}
|
|
1812
2306
|
try {
|
|
@@ -1834,7 +2328,18 @@ function importJgf(ctx, root) {
|
|
|
1834
2328
|
* @returns the graph object; the import fails when there is none
|
|
1835
2329
|
*/
|
|
1836
2330
|
function jgfGraphOf(ctx, root) {
|
|
1837
|
-
|
|
2331
|
+
const single = isJsonObject(root.graph);
|
|
2332
|
+
// a document with both graph and graphs is read by its graph; the graphs are not
|
|
2333
|
+
reportUnreadKeys(ctx, root, new Set([single ? "graph" : "graphs"]), "");
|
|
2334
|
+
const graph = single ? root.graph : chosenGraph(ctx, root, "JGF");
|
|
2335
|
+
const prefix = single ? "graph." : `graphs[${root.graphs.indexOf(graph)}].`;
|
|
2336
|
+
reportUnreadKeys(ctx, graph, JGF_GRAPH_KEYS, prefix);
|
|
2337
|
+
for (const key of ["id", "type", "label"]) {
|
|
2338
|
+
if (graph[key] !== undefined && graph[key] !== null && typeof graph[key] !== "string") {
|
|
2339
|
+
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${prefix}${key} must be a string, found ${describe(graph[key])}; ignored`, { element: `${prefix}${key}` });
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return graph;
|
|
1838
2343
|
}
|
|
1839
2344
|
/**
|
|
1840
2345
|
* The graph of a `graphs` array (JGF, OBO Graphs) that graphIndex / graphName choose, with
|
|
@@ -1843,6 +2348,7 @@ function jgfGraphOf(ctx, root) {
|
|
|
1843
2348
|
* @param root - the document
|
|
1844
2349
|
* @param what - the dialect's name, for the messages
|
|
1845
2350
|
* @returns the graph object; the import fails when there is none
|
|
2351
|
+
* @category Plugin helpers
|
|
1846
2352
|
*/
|
|
1847
2353
|
export function chosenGraph(ctx, root, what) {
|
|
1848
2354
|
const { report } = ctx;
|
|
@@ -1851,12 +2357,19 @@ export function chosenGraph(ctx, root, what) {
|
|
|
1851
2357
|
report.fail(JSON_ISSUE.SHAPE, `a ${what} document needs a graph object or a non-empty graphs array`);
|
|
1852
2358
|
}
|
|
1853
2359
|
const index = ctx.json.all === true ? ctx.json.graphIndex : chooseGraph(graphs.map(graphNameOf), ctx.json.choice, report);
|
|
1854
|
-
if (graphs.length > 1 && ctx.json.all !== true) {
|
|
1855
|
-
report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped)
|
|
2360
|
+
if (graphs.length > 1 && ctx.json.all !== true && !graphChosen(ctx.json.choice)) {
|
|
2361
|
+
report.warning("unsupported", JSON_ISSUE.MULTIPLE_GRAPHS, `the document holds ${graphs.length} graphs; only graphs[${index}] is read (${graphs.length - 1} skipped); importAllGraphs() reads every one`, { element: "graphs" });
|
|
1856
2362
|
}
|
|
1857
2363
|
const graph = graphs[index];
|
|
1858
2364
|
if (!isJsonObject(graph)) {
|
|
1859
|
-
|
|
2365
|
+
if (ctx.json.all !== true) {
|
|
2366
|
+
return report.fail(JSON_ISSUE.SHAPE, `graphs[${index}] is not an object`);
|
|
2367
|
+
}
|
|
2368
|
+
// importAll(): one bad graph is that graph's error, read as empty; the others are still read
|
|
2369
|
+
report.error("validation-error", JSON_ISSUE.SHAPE, `graphs[${index}] is not an object; it is read as empty`, {
|
|
2370
|
+
element: `graphs[${index}]`,
|
|
2371
|
+
});
|
|
2372
|
+
return {};
|
|
1860
2373
|
}
|
|
1861
2374
|
return graph;
|
|
1862
2375
|
}
|
|
@@ -1916,33 +2429,28 @@ function listingsOf(root, dialect) {
|
|
|
1916
2429
|
}
|
|
1917
2430
|
return [{ index: 0, name: null, nodes: null, edges: null }];
|
|
1918
2431
|
}
|
|
1919
|
-
/** The longest string V8 makes (2^29 - 24 UTF-16 code units); a longer document cannot be one JSON.parse input. */
|
|
1920
|
-
const MAX_TEXT_LENGTH = 2 ** 29 - 24;
|
|
1921
2432
|
/**
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
* @
|
|
1926
|
-
* @param report - the report
|
|
1927
|
-
* @param options - cancellation, progress and encoding
|
|
1928
|
-
* @returns the text
|
|
2433
|
+
* Whether a text that is not one JSON document is JSON Lines / NDJSON: its first two non-blank
|
|
2434
|
+
* lines are each a JSON object or array.
|
|
2435
|
+
* @param text - the document
|
|
2436
|
+
* @returns true for JSON Lines
|
|
1929
2437
|
*/
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2438
|
+
function looksLikeJsonLines(text) {
|
|
2439
|
+
const lines = text
|
|
2440
|
+
.slice(0, 64 * 1024)
|
|
2441
|
+
.split(/\r?\n/)
|
|
2442
|
+
.filter((line) => line.trim().length > 0)
|
|
2443
|
+
.slice(0, 2);
|
|
2444
|
+
return (lines.length === 2 &&
|
|
2445
|
+
lines.every((line) => {
|
|
2446
|
+
try {
|
|
2447
|
+
const value = JSON.parse(line);
|
|
2448
|
+
return typeof value === "object" && value !== null;
|
|
2449
|
+
}
|
|
2450
|
+
catch {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
}));
|
|
1946
2454
|
}
|
|
1947
2455
|
/**
|
|
1948
2456
|
* Push a JGF node: `label` into the declared column, `metadata` as attributes.
|
|
@@ -1965,7 +2473,7 @@ function pushJgfNode(ctx, id, record, labelColumn, element) {
|
|
|
1965
2473
|
element,
|
|
1966
2474
|
});
|
|
1967
2475
|
}
|
|
1968
|
-
ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null);
|
|
2476
|
+
ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null, element);
|
|
1969
2477
|
}
|
|
1970
2478
|
catch (err) {
|
|
1971
2479
|
ctx.report.recordError(err, { element: String(id) });
|
|
@@ -1989,18 +2497,33 @@ function importHyperedges(ctx, hyperedges, directed, push) {
|
|
|
1989
2497
|
const { report } = ctx;
|
|
1990
2498
|
const policy = ctx.options.hyperedges;
|
|
1991
2499
|
if (policy === "error") {
|
|
1992
|
-
report.error("unsupported", JSON_ISSUE.HYPEREDGE, `${hyperedges.length} hyperedge(
|
|
2500
|
+
report.error("unsupported", JSON_ISSUE.HYPEREDGE, `${hyperedges.length} hyperedge${plural(hyperedges.length)} (hyperedges: "error")`, {
|
|
1993
2501
|
element: "hyperedges",
|
|
1994
2502
|
});
|
|
1995
2503
|
throw report.abort("hyperedges refused", { code: JSON_ISSUE.HYPEREDGE, count: hyperedges.length });
|
|
1996
2504
|
}
|
|
1997
2505
|
if (policy === "skip") {
|
|
1998
|
-
report.warning("unsupported", JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(
|
|
2506
|
+
report.warning("unsupported", JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge${plural(hyperedges.length)} skipped`, {
|
|
1999
2507
|
element: "hyperedges",
|
|
2000
2508
|
});
|
|
2001
|
-
report.loss(JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(
|
|
2509
|
+
report.loss(JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge${plural(hyperedges.length)} ${agree(hyperedges.length, "was", "were")} not imported`, null, hyperedges.length);
|
|
2002
2510
|
return;
|
|
2003
2511
|
}
|
|
2512
|
+
// each expanded edge is pushed on its own, so one the sink refuses is reported and counted
|
|
2513
|
+
// while the others stay
|
|
2514
|
+
const expand = (record, pairs, kind, element) => {
|
|
2515
|
+
for (const [u, v] of pairs) {
|
|
2516
|
+
try {
|
|
2517
|
+
push(record, u, v, kind, element, JGF_HYPEREDGE_KEYS);
|
|
2518
|
+
}
|
|
2519
|
+
catch (err) {
|
|
2520
|
+
ctx.skip(err, "edge", element);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
// the edges expanded so far: the cap is on the whole import, so many hyperedges just under it
|
|
2525
|
+
// cannot amplify without bound either
|
|
2526
|
+
const budget = { used: 0 };
|
|
2004
2527
|
for (let i = 0; i < hyperedges.length; i++) {
|
|
2005
2528
|
const element = `hyperedges[${i}]`;
|
|
2006
2529
|
const record = hyperedges[i];
|
|
@@ -2010,23 +2533,26 @@ function importHyperedges(ctx, hyperedges, directed, push) {
|
|
|
2010
2533
|
}
|
|
2011
2534
|
try {
|
|
2012
2535
|
if (Array.isArray(record.nodes)) {
|
|
2013
|
-
const members = record.nodes.map((raw) => ctx.requireId(raw, element));
|
|
2536
|
+
const members = distinctMembers(ctx, record.nodes.map((raw) => ctx.requireId(raw, element)), element);
|
|
2014
2537
|
if (members.length < 2) {
|
|
2015
2538
|
throw new GraphFormatError("E_INVALID_ID", `${element}: an undirected hyperedge needs two or more nodes`, { reason: "hyperedge shape" });
|
|
2016
2539
|
}
|
|
2017
|
-
const
|
|
2540
|
+
const n = members.length;
|
|
2541
|
+
checkExpansion(policy === "star" ? n - 1 : (n * (n - 1)) / 2, budget, element);
|
|
2542
|
+
const pairs = [];
|
|
2018
2543
|
if (policy === "star") {
|
|
2019
|
-
for (let k = 1; k <
|
|
2020
|
-
push(
|
|
2544
|
+
for (let k = 1; k < n; k++) {
|
|
2545
|
+
pairs.push([members[0], members[k]]);
|
|
2021
2546
|
}
|
|
2022
2547
|
}
|
|
2023
2548
|
else {
|
|
2024
|
-
for (let a = 0; a <
|
|
2025
|
-
for (let b = a + 1; b <
|
|
2026
|
-
push(
|
|
2549
|
+
for (let a = 0; a < n; a++) {
|
|
2550
|
+
for (let b = a + 1; b < n; b++) {
|
|
2551
|
+
pairs.push([members[a], members[b]]);
|
|
2027
2552
|
}
|
|
2028
2553
|
}
|
|
2029
2554
|
}
|
|
2555
|
+
expand(record, pairs, directed ? "directed" : "undirected", element);
|
|
2030
2556
|
}
|
|
2031
2557
|
else if (Array.isArray(record.source) && Array.isArray(record.target)) {
|
|
2032
2558
|
const sources = record.source.map((raw) => ctx.requireId(raw, element));
|
|
@@ -2034,11 +2560,8 @@ function importHyperedges(ctx, hyperedges, directed, push) {
|
|
|
2034
2560
|
if (sources.length === 0 || targets.length === 0) {
|
|
2035
2561
|
throw new GraphFormatError("E_INVALID_ID", `${element}: a directed hyperedge needs sources and targets`, { reason: "hyperedge shape" });
|
|
2036
2562
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
push(record, s, t, "directed", element, JGF_HYPEREDGE_KEYS);
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2563
|
+
checkExpansion(sources.length * targets.length, budget, element);
|
|
2564
|
+
expand(record, sources.flatMap((s) => targets.map((t) => [s, t])), "directed", element);
|
|
2042
2565
|
}
|
|
2043
2566
|
else {
|
|
2044
2567
|
report.error("validation-error", JSON_ISSUE.HYPEREDGE_SHAPE, `${element} has neither a nodes array nor source / target arrays`, { element });
|
|
@@ -2046,10 +2569,52 @@ function importHyperedges(ctx, hyperedges, directed, push) {
|
|
|
2046
2569
|
}
|
|
2047
2570
|
}
|
|
2048
2571
|
catch (err) {
|
|
2049
|
-
|
|
2572
|
+
if (err instanceof ExpansionRefused) {
|
|
2573
|
+
// one hyperedge over the cap is skipped; the shared E_TOO_LARGE rule would stop the import
|
|
2574
|
+
report.error("unsupported", err.code, err.message, { element });
|
|
2575
|
+
ctx.countSkipped("edge");
|
|
2576
|
+
}
|
|
2577
|
+
else {
|
|
2578
|
+
ctx.skip(err, "edge", element);
|
|
2579
|
+
}
|
|
2050
2580
|
}
|
|
2051
2581
|
}
|
|
2052
2582
|
}
|
|
2583
|
+
/** A hyperedge whose expansion would pass MAX_HYPEREDGE_EXPANSION: that hyperedge alone is skipped. */
|
|
2584
|
+
class ExpansionRefused extends GraphFormatError {
|
|
2585
|
+
}
|
|
2586
|
+
/** The most edges the hyperedges of one import expand into; a 20k-member clique would be 200M edges. */
|
|
2587
|
+
// ponytail: a fixed cap; make it an option if a real file needs more
|
|
2588
|
+
const MAX_HYPEREDGE_EXPANSION = 1000000;
|
|
2589
|
+
/**
|
|
2590
|
+
* Refuse an expansion that would take the import beyond MAX_HYPEREDGE_EXPANSION expanded edges,
|
|
2591
|
+
* before any of it is pushed; otherwise count it against the budget.
|
|
2592
|
+
* @param count - the number of edges the expansion makes
|
|
2593
|
+
* @param budget - the edges the import's hyperedges expanded into so far
|
|
2594
|
+
* @param budget.used - that count, updated
|
|
2595
|
+
* @param element - the hyperedge's name
|
|
2596
|
+
*/
|
|
2597
|
+
function checkExpansion(count, budget, element) {
|
|
2598
|
+
if (budget.used + count > MAX_HYPEREDGE_EXPANSION) {
|
|
2599
|
+
throw new ExpansionRefused("E_TOO_LARGE", `${element}: the expansion makes ${count} edges, which with the ${budget.used} already expanded is more than ${MAX_HYPEREDGE_EXPANSION}; the hyperedge is skipped`, { count, used: budget.used, limit: MAX_HYPEREDGE_EXPANSION });
|
|
2600
|
+
}
|
|
2601
|
+
budget.used += count;
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* The members of an undirected hyperedge with repeats removed (a repeat would expand into a
|
|
2605
|
+
* self-loop and parallel edges), each repeat reported as E_HYPEREDGE_SHAPE.
|
|
2606
|
+
* @param ctx - the context
|
|
2607
|
+
* @param members - the members as listed
|
|
2608
|
+
* @param element - the hyperedge's name
|
|
2609
|
+
* @returns the distinct members in first-listed order
|
|
2610
|
+
*/
|
|
2611
|
+
function distinctMembers(ctx, members, element) {
|
|
2612
|
+
const distinct = [...new Set(members)];
|
|
2613
|
+
if (distinct.length < members.length) {
|
|
2614
|
+
ctx.report.error("validation-error", JSON_ISSUE.HYPEREDGE_SHAPE, `${element} lists ${members.length - distinct.length} member${plural(members.length - distinct.length)} more than once; each is read once`, { element });
|
|
2615
|
+
}
|
|
2616
|
+
return distinct;
|
|
2617
|
+
}
|
|
2053
2618
|
// ============================================================ Cytoscape
|
|
2054
2619
|
/**
|
|
2055
2620
|
* Read Cytoscape.js elements: `elements.nodes` / `elements.edges`, a flat `elements` array (group
|
|
@@ -2095,7 +2660,8 @@ function importCytoscape(ctx, root) {
|
|
|
2095
2660
|
role: "position",
|
|
2096
2661
|
mutable: true,
|
|
2097
2662
|
nullable: true,
|
|
2098
|
-
|
|
2663
|
+
// 3D layouts written by Cytoscape-compatible tools carry a z
|
|
2664
|
+
extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
|
|
2099
2665
|
origin: { format: "json", namespace: "cytoscape" },
|
|
2100
2666
|
}, anyHas(nodes, "position"));
|
|
2101
2667
|
const classesColumn = ctx.nodes.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(nodes, "classes"));
|
|
@@ -2120,6 +2686,9 @@ function importCytoscape(ctx, root) {
|
|
|
2120
2686
|
if (index < 0) {
|
|
2121
2687
|
continue;
|
|
2122
2688
|
}
|
|
2689
|
+
if (present(data, "source") && present(data, "target")) {
|
|
2690
|
+
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`, { element });
|
|
2691
|
+
}
|
|
2123
2692
|
try {
|
|
2124
2693
|
for (const key of Object.keys(data)) {
|
|
2125
2694
|
if (key === "id") {
|
|
@@ -2135,38 +2704,98 @@ function importCytoscape(ctx, root) {
|
|
|
2135
2704
|
}
|
|
2136
2705
|
continue;
|
|
2137
2706
|
}
|
|
2138
|
-
ctx.nodes.write(index, key, data[key], SUFFIX.data);
|
|
2707
|
+
ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
|
|
2139
2708
|
}
|
|
2140
2709
|
const { position } = record;
|
|
2141
2710
|
if (position !== undefined && position !== null) {
|
|
2142
|
-
|
|
2711
|
+
const z = isJsonObject(position) ? (position.z ?? 0) : 0;
|
|
2712
|
+
if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
|
|
2143
2713
|
point[0] = position.x;
|
|
2144
2714
|
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
2145
2715
|
point[1] = flipY(position.y);
|
|
2716
|
+
point[2] = z;
|
|
2146
2717
|
ctx.nodes.set(positionColumn, index, point);
|
|
2147
2718
|
}
|
|
2148
2719
|
else {
|
|
2149
|
-
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y`, { element });
|
|
2720
|
+
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y (and z) that are finite and within the f32 range`, { element });
|
|
2150
2721
|
}
|
|
2151
2722
|
}
|
|
2152
2723
|
writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
|
|
2153
|
-
writeElementKeys(ctx.nodes, index, record);
|
|
2724
|
+
writeElementKeys(ctx.nodes, index, record, element);
|
|
2154
2725
|
}
|
|
2155
2726
|
catch (err) {
|
|
2156
2727
|
report.recordError(err, { element: String(id) });
|
|
2157
2728
|
}
|
|
2158
2729
|
}
|
|
2159
|
-
|
|
2730
|
+
resolveCytoscapeParents(ctx, parents, parentColumn);
|
|
2731
|
+
throwIfAborted(ctx.options.signal);
|
|
2732
|
+
importCytoscapeEdges(ctx, edges, edgeClassesColumn);
|
|
2733
|
+
ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Set the parent of each Cytoscape node once every node is known (a parent may come later): an
|
|
2737
|
+
* unknown parent is E_UNKNOWN_PARENT, a link that would make a node its own ancestor E_PARENT_CYCLE
|
|
2738
|
+
* (Cytoscape.js refuses those too); either link is dropped and the node kept.
|
|
2739
|
+
* @param ctx - the context
|
|
2740
|
+
* @param parents - the parent links in document order
|
|
2741
|
+
* @param parentColumn - the parent column
|
|
2742
|
+
*/
|
|
2743
|
+
function resolveCytoscapeParents(ctx, parents, parentColumn) {
|
|
2744
|
+
const { report } = ctx;
|
|
2745
|
+
// the last link per child (a duplicate node's later parent wins), child -> its link
|
|
2746
|
+
const linkOf = new Map();
|
|
2747
|
+
for (let order = 0; order < parents.length; order++) {
|
|
2748
|
+
const { index, parent, element } = parents[order];
|
|
2160
2749
|
const parentIndex = ctx.sink.indexOf(parent);
|
|
2161
2750
|
if (parentIndex === INVALID_INDEX) {
|
|
2162
2751
|
report.error("missing-value", JSON_ISSUE.UNKNOWN_PARENT, `${element}: parent ${JSON.stringify(parent)} is not a node`, { element });
|
|
2163
2752
|
continue;
|
|
2164
2753
|
}
|
|
2165
|
-
|
|
2754
|
+
linkOf.set(index, { parent: parentIndex, id: parent, order, element });
|
|
2755
|
+
}
|
|
2756
|
+
// one walk per chain, each node visited once: on a cycle the link that came last in the document
|
|
2757
|
+
// (the one that closed it) is dropped
|
|
2758
|
+
const done = new Set();
|
|
2759
|
+
for (const start of linkOf.keys()) {
|
|
2760
|
+
const path = [];
|
|
2761
|
+
const onPath = new Set();
|
|
2762
|
+
let node = start;
|
|
2763
|
+
while (node !== undefined && !done.has(node) && !onPath.has(node)) {
|
|
2764
|
+
path.push(node);
|
|
2765
|
+
onPath.add(node);
|
|
2766
|
+
node = linkOf.get(node)?.parent;
|
|
2767
|
+
}
|
|
2768
|
+
if (node !== undefined && onPath.has(node)) {
|
|
2769
|
+
dropClosingLink(report, linkOf, path.slice(path.indexOf(node)));
|
|
2770
|
+
}
|
|
2771
|
+
for (const member of path) {
|
|
2772
|
+
done.add(member);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
for (const [index, { parent }] of linkOf) {
|
|
2776
|
+
ctx.nodes.set(parentColumn, index, parent);
|
|
2166
2777
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Drop the link of a parent cycle that came last in the document (the one that closed it).
|
|
2781
|
+
* @param report - the report
|
|
2782
|
+
* @param linkOf - the links by child, the dropped one removed
|
|
2783
|
+
* @param cycle - the nodes of the cycle
|
|
2784
|
+
*/
|
|
2785
|
+
function dropClosingLink(report, linkOf, cycle) {
|
|
2786
|
+
let closing = null;
|
|
2787
|
+
for (const member of cycle) {
|
|
2788
|
+
const link = linkOf.get(member);
|
|
2789
|
+
if (link !== undefined && (closing === null || link.order > closing[1].order)) {
|
|
2790
|
+
closing = [member, link];
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (closing === null) {
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const { id, element } = closing[1];
|
|
2797
|
+
report.error("validation-error", JSON_ISSUE.PARENT_CYCLE, `${element}: parent ${JSON.stringify(id)} would make the node its own ancestor; the link is dropped`, { element });
|
|
2798
|
+
linkOf.delete(closing[0]);
|
|
2170
2799
|
}
|
|
2171
2800
|
/**
|
|
2172
2801
|
* Read the Cytoscape edge elements: `data.id`, `data.source` / `data.target`, the weight, the
|
|
@@ -2186,8 +2815,8 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
|
|
|
2186
2815
|
continue;
|
|
2187
2816
|
}
|
|
2188
2817
|
const { data } = record;
|
|
2189
|
-
if (!
|
|
2190
|
-
ctx.missingEndpoint(element, `data.${
|
|
2818
|
+
if (!present(data, "source") || !present(data, "target")) {
|
|
2819
|
+
ctx.missingEndpoint(element, `data.${present(data, "source") ? "target" : "source"}`);
|
|
2191
2820
|
continue;
|
|
2192
2821
|
}
|
|
2193
2822
|
try {
|
|
@@ -2203,11 +2832,11 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
|
|
|
2203
2832
|
const { weightFrom } = ctx.options;
|
|
2204
2833
|
for (const key of Object.keys(data)) {
|
|
2205
2834
|
if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
|
|
2206
|
-
ctx.edges.write(edge, key, data[key], SUFFIX.data);
|
|
2835
|
+
ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
|
|
2207
2836
|
}
|
|
2208
2837
|
}
|
|
2209
2838
|
writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
|
|
2210
|
-
writeElementKeys(ctx.edges, edge, record);
|
|
2839
|
+
writeElementKeys(ctx.edges, edge, record, element);
|
|
2211
2840
|
}
|
|
2212
2841
|
catch (err) {
|
|
2213
2842
|
ctx.skip(err, "edge", element);
|
|
@@ -2224,10 +2853,16 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
|
|
|
2224
2853
|
function cytoscapeSections(ctx, elements) {
|
|
2225
2854
|
const { report } = ctx;
|
|
2226
2855
|
if (Array.isArray(elements)) {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2856
|
+
const nodes = [];
|
|
2857
|
+
const edges = [];
|
|
2858
|
+
elements.forEach((item, i) => {
|
|
2859
|
+
if (isJsonObject(item) && item.group !== undefined && item.group !== "nodes" && item.group !== "edges") {
|
|
2860
|
+
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `elements[${i}]: group ${describe(item.group)} is neither "nodes" nor "edges"; classed by its endpoints`, { element: `elements[${i}]` });
|
|
2861
|
+
}
|
|
2862
|
+
// a non-object item stays with the nodes, whose reader reports it as E_BAD_ELEMENT
|
|
2863
|
+
(isJsonObject(item) && !isNodeElement(item) ? edges : nodes).push(item);
|
|
2864
|
+
});
|
|
2865
|
+
return { nodes, edges };
|
|
2231
2866
|
}
|
|
2232
2867
|
if (isJsonObject(elements)) {
|
|
2233
2868
|
return {
|
|
@@ -2236,13 +2871,31 @@ function cytoscapeSections(ctx, elements) {
|
|
|
2236
2871
|
};
|
|
2237
2872
|
}
|
|
2238
2873
|
if (elements === undefined || elements === null) {
|
|
2239
|
-
report.
|
|
2874
|
+
report.warning("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no elements", {
|
|
2240
2875
|
element: "elements",
|
|
2241
2876
|
});
|
|
2242
2877
|
return { nodes: [], edges: [] };
|
|
2243
2878
|
}
|
|
2244
2879
|
return report.fail(JSON_ISSUE.SHAPE, `elements must be an object or an array, found ${describe(elements)}`);
|
|
2245
2880
|
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Whether a Cytoscape element's position has a numeric z.
|
|
2883
|
+
* @param element - the element
|
|
2884
|
+
* @returns true when position.z is a number
|
|
2885
|
+
*/
|
|
2886
|
+
function hasPositionZ(element) {
|
|
2887
|
+
return isJsonObject(element) && isJsonObject(element.position) && typeof element.position.z === "number";
|
|
2888
|
+
}
|
|
2889
|
+
/** The largest finite f32, the bound of a position coordinate. */
|
|
2890
|
+
const F32_MAX = 3.4028234663852886e38;
|
|
2891
|
+
/**
|
|
2892
|
+
* Whether a value is a coordinate the f32 position column holds: a finite number within the f32 range.
|
|
2893
|
+
* @param value - the value
|
|
2894
|
+
* @returns true for a storable coordinate
|
|
2895
|
+
*/
|
|
2896
|
+
function isCoordinate(value) {
|
|
2897
|
+
return typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= F32_MAX;
|
|
2898
|
+
}
|
|
2246
2899
|
/**
|
|
2247
2900
|
* Whether a flat Cytoscape element is a node: `group: "nodes"`, or no source / target in its data.
|
|
2248
2901
|
* @param element - the element
|
|
@@ -2290,22 +2943,36 @@ function writeClasses(ctx, writer, column, row, raw, element) {
|
|
|
2290
2943
|
* @param writer - the table writer
|
|
2291
2944
|
* @param row - the row
|
|
2292
2945
|
* @param record - the element
|
|
2946
|
+
* @param element - the element name for issues
|
|
2293
2947
|
*/
|
|
2294
|
-
function writeElementKeys(writer, row, record) {
|
|
2948
|
+
function writeElementKeys(writer, row, record, element) {
|
|
2295
2949
|
for (const key of Object.keys(record)) {
|
|
2296
2950
|
if (CYTOSCAPE_STRUCTURAL_KEYS.has(key)) {
|
|
2297
2951
|
continue;
|
|
2298
2952
|
}
|
|
2299
2953
|
const name = CYTOSCAPE_ELEMENT_KEYS.has(key) ? key : `${key}${SUFFIX.element}`;
|
|
2300
|
-
writer.write(row, name, record[key], SUFFIX.data);
|
|
2954
|
+
writer.write(row, name, record[key], SUFFIX.data, element);
|
|
2301
2955
|
}
|
|
2302
2956
|
}
|
|
2303
2957
|
// ============================================================ the plugin
|
|
2304
2958
|
/**
|
|
2305
|
-
* The JSON importer plugin
|
|
2959
|
+
* The JSON importer plugin.
|
|
2960
|
+
* @category Built-in formats
|
|
2306
2961
|
*/
|
|
2307
2962
|
export const jsonImporter = Object.freeze({
|
|
2308
2963
|
format: "json",
|
|
2964
|
+
options: Object.freeze([
|
|
2965
|
+
"dialect",
|
|
2966
|
+
"edgesKey",
|
|
2967
|
+
"edgesPath",
|
|
2968
|
+
"indexLinks",
|
|
2969
|
+
"nodeIdKey",
|
|
2970
|
+
"nodesPath",
|
|
2971
|
+
"oboIds",
|
|
2972
|
+
"sourceKey",
|
|
2973
|
+
"targetKey",
|
|
2974
|
+
"typedefs",
|
|
2975
|
+
]),
|
|
2309
2976
|
extensions: Object.freeze([".json"]),
|
|
2310
2977
|
mimeTypes: Object.freeze(["application/json"]),
|
|
2311
2978
|
/**
|
|
@@ -2316,7 +2983,8 @@ export const jsonImporter = Object.freeze({
|
|
|
2316
2983
|
* @returns the confidence
|
|
2317
2984
|
*/
|
|
2318
2985
|
sniff(head) {
|
|
2319
|
-
|
|
2986
|
+
// headBytes transcodes a UTF-16 head with a BOM, which import() decodes too
|
|
2987
|
+
const text = new TextDecoder("utf-8").decode(headBytes(head).subarray(0, SNIFF_BYTES));
|
|
2320
2988
|
const trimmed = (text.startsWith(BOM) ? text.slice(1) : text).trimStart();
|
|
2321
2989
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
2322
2990
|
return 0;
|
|
@@ -2337,7 +3005,7 @@ export const jsonImporter = Object.freeze({
|
|
|
2337
3005
|
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2338
3006
|
const json = resolveJsonOptions(options);
|
|
2339
3007
|
const report = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2340
|
-
const text = await
|
|
3008
|
+
const text = await readText(input, report, { ...resolved, bomlessUtf16: true });
|
|
2341
3009
|
const { root, dialect } = documentOf(parseDocument(text, report), json, report);
|
|
2342
3010
|
readGraph(root, dialect, sink, report, resolved, json, options);
|
|
2343
3011
|
return report.finish();
|
|
@@ -2354,7 +3022,7 @@ export const jsonImporter = Object.freeze({
|
|
|
2354
3022
|
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2355
3023
|
const json = resolveJsonOptions(options);
|
|
2356
3024
|
const report = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2357
|
-
const text = await
|
|
3025
|
+
const text = await readText(input, report, { ...resolved, bomlessUtf16: true });
|
|
2358
3026
|
const { root, dialect } = documentOf(parseDocument(text, report), json, report);
|
|
2359
3027
|
return listingsOf(root, dialect);
|
|
2360
3028
|
},
|
|
@@ -2370,7 +3038,7 @@ export const jsonImporter = Object.freeze({
|
|
|
2370
3038
|
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2371
3039
|
const json = resolveJsonOptions(options);
|
|
2372
3040
|
const first = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2373
|
-
const text = await
|
|
3041
|
+
const text = await readText(input, first, { ...resolved, bomlessUtf16: true });
|
|
2374
3042
|
const { root, dialect } = documentOf(parseDocument(text, first), json, first);
|
|
2375
3043
|
const graphs = listingsOf(root, dialect).length;
|
|
2376
3044
|
const reports = [];
|
|
@@ -2397,14 +3065,26 @@ function readGraph(root, dialect, sink, report, resolved, json, options) {
|
|
|
2397
3065
|
// the obographs reader refuses missing endpoints itself, so addMissingNodes false holds on any sink
|
|
2398
3066
|
reportSinkOptions(sink, options, report, dialect === "obographs");
|
|
2399
3067
|
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
3068
|
+
readDialect(ctx, root, dialect);
|
|
3069
|
+
ctx.reportDangling();
|
|
3070
|
+
ctx.nodes.reportNullOnly();
|
|
3071
|
+
ctx.edges.reportNullOnly();
|
|
3072
|
+
throwIfAborted(resolved.signal);
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Read one graph with the reader of its dialect.
|
|
3076
|
+
* @param ctx - the import context
|
|
3077
|
+
* @param root - the parsed document
|
|
3078
|
+
* @param dialect - its dialect
|
|
3079
|
+
*/
|
|
3080
|
+
function readDialect(ctx, root, dialect) {
|
|
2400
3081
|
if (dialect === "cytoscape") {
|
|
2401
3082
|
importCytoscape(ctx, root);
|
|
2402
|
-
throwIfAborted(resolved.signal);
|
|
2403
3083
|
return;
|
|
2404
3084
|
}
|
|
2405
3085
|
const doc = isJsonObject(root)
|
|
2406
3086
|
? root
|
|
2407
|
-
: report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
|
|
3087
|
+
: ctx.report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
|
|
2408
3088
|
switch (dialect) {
|
|
2409
3089
|
case "node-link":
|
|
2410
3090
|
case "d3":
|
|
@@ -2436,6 +3116,5 @@ function readGraph(root, dialect, sink, report, resolved, json, options) {
|
|
|
2436
3116
|
});
|
|
2437
3117
|
}
|
|
2438
3118
|
}
|
|
2439
|
-
throwIfAborted(resolved.signal);
|
|
2440
3119
|
}
|
|
2441
3120
|
//# sourceMappingURL=importer.js.map
|