@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The CSV / TSV importer (design sections 8.4 and 8.6; research note 07 section 2.6): a streaming
|
|
3
3
|
* edge-list reader for the generic (`source,target[,weight,...]`), Gephi (`Source,Target,Type,Id,
|
|
4
4
|
* Label,Weight,...`) and headerless (`u v [w]`) dialects, with an optional node table merged by id,
|
|
5
|
-
* and, with `table: "adjacency"`, an adjacency table (`node,
|
|
5
|
+
* and, with `table: "adjacency"`, an adjacency table (`node,neighbor[:weight],...`).
|
|
6
6
|
*
|
|
7
7
|
* - The delimiter is sniffed from a preview unless given; LF, CRLF and lone-CR files all read.
|
|
8
8
|
* - The first row is a header when it holds a known column name or when it is all text over a
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* all-text column of low cardinality becomes a dict (design section 5.4); an `id` column of the
|
|
22
22
|
* edge table is the edge id (role id, unique); a `label` column is the label (role label).
|
|
23
23
|
* - An adjacency table has no header (unless `header: true`, which skips the first row): each row
|
|
24
|
-
* is a node followed by its
|
|
24
|
+
* is a node followed by its neighbors, one edge per neighbor in row order. A neighbor cell
|
|
25
25
|
* `id:weight` carries the edge's weight when the text after its LAST colon is a number; a cell
|
|
26
26
|
* ending in a bare colon (`a:1:`) is the id before it with no weight; any other cell is the id
|
|
27
27
|
* as written (`http://x`). A row holding only its node adds an isolated node. An empty adjacency
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
ID_MERGED_CODE as SHARED_ID_MERGED_CODE,
|
|
55
55
|
MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE,
|
|
56
56
|
MISSING_ID_CODE as SHARED_MISSING_ID_CODE,
|
|
57
|
+
OPTION_IGNORED_CODE,
|
|
57
58
|
ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE,
|
|
58
59
|
} from "../../common/codes.js";
|
|
59
60
|
import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
type ResolvedImportOptions,
|
|
66
67
|
resolveImportOptions,
|
|
67
68
|
} from "../../common/options.js";
|
|
69
|
+
import { plural } from "../../common/plural.js";
|
|
68
70
|
import { ImportReportBuilder } from "../../common/report.js";
|
|
69
71
|
import { parseWeightText } from "../../common/weights.js";
|
|
70
72
|
import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
|
|
@@ -82,70 +84,225 @@ import {
|
|
|
82
84
|
TARGET_NAMES,
|
|
83
85
|
TYPE_NAME,
|
|
84
86
|
} from "./header.js";
|
|
85
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
checkRecordSyntax,
|
|
89
|
+
type CsvReaderOptions,
|
|
90
|
+
CsvRecordReader,
|
|
91
|
+
DELIMITER_CANDIDATES,
|
|
92
|
+
sniffDelimiter,
|
|
93
|
+
sniffNewline,
|
|
94
|
+
splitRecords,
|
|
95
|
+
stripLeadingComments,
|
|
96
|
+
} from "./records.js";
|
|
86
97
|
import { InferredColumn } from "./values.js";
|
|
87
98
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The format-specific options of the CSV importer.
|
|
101
|
+
* @category Built-in formats
|
|
102
|
+
*/
|
|
103
|
+
export interface CsvImportOptions extends CommonImportOptions {
|
|
104
|
+
/**
|
|
105
|
+
* The field delimiter. The default is to detect it from the first rows: `,`, tab, `;`, `|` or
|
|
106
|
+
* space. It applies to the `nodes` table too.
|
|
107
|
+
* @defaultValue detected
|
|
108
|
+
*/
|
|
91
109
|
delimiter?: string | undefined;
|
|
92
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* The decimal separator of numbers. With ",", a cell such as `2,5` (digits, a comma, digits) is
|
|
112
|
+
* read as the number 2.5, for weights and attributes alike, and the delimiter is detected among
|
|
113
|
+
* the others (`;`, tab, `|`, space), so a spreadsheet saved in a locale that writes decimal
|
|
114
|
+
* commas reads as it is. A thousands separator is not read.
|
|
115
|
+
* @defaultValue "."
|
|
116
|
+
*/
|
|
117
|
+
decimal?: "." | "," | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Whether the first row is a header; "auto" decides from its content (a header names columns, a
|
|
120
|
+
* data row holds ids and numbers). It applies to the `nodes` table too.
|
|
121
|
+
* @defaultValue "auto"
|
|
122
|
+
*/
|
|
93
123
|
header?: boolean | "auto" | undefined;
|
|
94
124
|
/**
|
|
95
|
-
* What the input is:
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
125
|
+
* What the input is: "edges" (one edge per row), "nodes" (one node per row), "adjacency" (a
|
|
126
|
+
* node and its neighbors per row, `node,neighbor[:weight],...`, with no header unless you pass
|
|
127
|
+
* `header: true`), or "auto": an edge table when source and target columns can be found, a node
|
|
128
|
+
* table when only an id column can. An adjacency table is never detected, because its rows look
|
|
129
|
+
* like an edge list.
|
|
130
|
+
* @defaultValue "auto"
|
|
99
131
|
*/
|
|
100
132
|
table?: "edges" | "nodes" | "adjacency" | "auto" | undefined;
|
|
101
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* The source column, by name or 0-based position. The default is the column whose header is a
|
|
135
|
+
* recognized source name (`source`, `from`, `src` and others; the CSV format page lists them
|
|
136
|
+
* all); in a file without a header, the first column. A header with no recognized source and
|
|
137
|
+
* target names needs this option and `targetColumn`.
|
|
138
|
+
* @defaultValue from the header
|
|
139
|
+
*/
|
|
102
140
|
sourceColumn?: CsvColumnRef | undefined;
|
|
103
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* The target column, by name or 0-based position. The default is the column whose header is a
|
|
143
|
+
* recognized target name (`target`, `to`, `dst` and others; the CSV format page lists them
|
|
144
|
+
* all); in a file without a header, the second column.
|
|
145
|
+
* @defaultValue from the header
|
|
146
|
+
*/
|
|
104
147
|
targetColumn?: CsvColumnRef | undefined;
|
|
105
148
|
/**
|
|
106
|
-
* The
|
|
107
|
-
* column of a Gephi table (
|
|
149
|
+
* The column that gives each edge's direction (Directed / Undirected / Mutual), by name or
|
|
150
|
+
* position. The default is the `Type` column of a Gephi table (one whose header has exactly
|
|
151
|
+
* `Source` and `Target`); null reads no such column.
|
|
152
|
+
* @defaultValue Gephi's Type
|
|
108
153
|
*/
|
|
109
154
|
typeColumn?: CsvColumnRef | null | undefined;
|
|
110
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* The id column of a node table, by name or position. The default is the column the header
|
|
157
|
+
* names `id` (or `node`, `name`, `key`).
|
|
158
|
+
* @defaultValue from the header
|
|
159
|
+
*/
|
|
111
160
|
idColumn?: CsvColumnRef | undefined;
|
|
112
|
-
/** A node table read before the edges: its ids become nodes and its other columns node attributes. */
|
|
113
|
-
nodes?: ImportInput | undefined;
|
|
114
161
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
162
|
+
* The label column of a node table, by name or position. The default is the column the header
|
|
163
|
+
* names `label` (in any case). With `nodeIdFrom: "label"`, the node ids come from this column,
|
|
164
|
+
* and the edge table must name its nodes by these labels too.
|
|
165
|
+
* @defaultValue from the header
|
|
166
|
+
*/
|
|
167
|
+
labelColumn?: CsvColumnRef | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* A node table to read before the edges: its ids become nodes and its other columns node
|
|
170
|
+
* attributes. Pass it as a string, bytes, a stream, or a `File` or `Blob` (for example, the second file a
|
|
171
|
+
* user picked).
|
|
172
|
+
*/
|
|
173
|
+
nodes?: ImportInput | Blob | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* Give the nodes of a node table without an id column the row number as id (0 for the first
|
|
176
|
+
* data row, turned into an id by `ids`), instead of failing with E_CSV_NO_ID_COLUMN. It applies
|
|
177
|
+
* to the `nodes` table when you pass one, else to the input. Under `header: "auto"` that
|
|
178
|
+
* table's first row is then read as a header; under `header: false` the option is ignored with
|
|
179
|
+
* a warning, because a table without a header takes its ids from its first column.
|
|
180
|
+
* @defaultValue false
|
|
119
181
|
*/
|
|
120
182
|
rowNumberIds?: boolean | undefined;
|
|
121
183
|
}
|
|
122
184
|
|
|
123
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* The input holds no header row at all.
|
|
187
|
+
* @category Issue and loss codes
|
|
188
|
+
*/
|
|
124
189
|
export const EMPTY_INPUT_CODE = SHARED_EMPTY_INPUT_CODE;
|
|
125
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* The header names no source / target (or, for a node table, no id) column.
|
|
192
|
+
* @category Issue and loss codes
|
|
193
|
+
*/
|
|
126
194
|
export const NO_ENDPOINT_COLUMNS_CODE = "E_CSV_NO_ENDPOINT_COLUMNS";
|
|
127
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* A node table without an id column.
|
|
197
|
+
* @category Issue and loss codes
|
|
198
|
+
*/
|
|
128
199
|
export const NO_ID_COLUMN_CODE = "E_CSV_NO_ID_COLUMN";
|
|
129
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* A row with a different number of fields than the header.
|
|
202
|
+
* @category Issue and loss codes
|
|
203
|
+
*/
|
|
130
204
|
export const FIELD_COUNT_CODE = "E_CSV_FIELD_COUNT";
|
|
131
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* An edge row with a blank source or target cell.
|
|
207
|
+
* @category Issue and loss codes
|
|
208
|
+
*/
|
|
132
209
|
export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
|
|
133
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* A node row with a blank id cell.
|
|
212
|
+
* @category Issue and loss codes
|
|
213
|
+
*/
|
|
134
214
|
export const MISSING_ID_CODE = SHARED_MISSING_ID_CODE;
|
|
135
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* A Type cell that is not Directed, Undirected or Mutual.
|
|
217
|
+
* @category Issue and loss codes
|
|
218
|
+
*/
|
|
136
219
|
export const BAD_TYPE_CODE = "E_CSV_BAD_TYPE";
|
|
137
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* The table has a header and no data rows.
|
|
222
|
+
* @category Issue and loss codes
|
|
223
|
+
*/
|
|
138
224
|
export const NO_DATA_ROWS_CODE = "W_CSV_NO_DATA_ROWS";
|
|
139
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* A node table row repeats an id; its attributes overwrite the earlier row's.
|
|
227
|
+
* @category Issue and loss codes
|
|
228
|
+
*/
|
|
140
229
|
export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
|
|
141
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Two different id cells became the same number because `ids` is "number", so their nodes were merged.
|
|
232
|
+
* @category Issue and loss codes
|
|
233
|
+
*/
|
|
142
234
|
export const ID_MERGED_CODE = SHARED_ID_MERGED_CODE;
|
|
143
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* An explicitly named weight column the file does not have.
|
|
237
|
+
* @category Issue and loss codes
|
|
238
|
+
*/
|
|
144
239
|
export const COLUMN_MISSING_CODE = "W_CSV_COLUMN_MISSING";
|
|
145
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* You read into a graph builder that already has an id or label attribute, so this file's one is kept without its
|
|
242
|
+
* role.
|
|
243
|
+
* @category Issue and loss codes
|
|
244
|
+
*/
|
|
146
245
|
export const ROLE_TAKEN_CODE = SHARED_ROLE_TAKEN_CODE;
|
|
147
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* A repeated edge id (the column is unique); the edge is skipped.
|
|
248
|
+
* @category Issue and loss codes
|
|
249
|
+
*/
|
|
148
250
|
export const DUPLICATE_EDGE_ID_CODE = SHARED_DUPLICATE_EDGE_ID_CODE;
|
|
251
|
+
/**
|
|
252
|
+
* The input starts like another format (XML or HTML, JSON, GML, DOT or Pajek), not CSV. The import stops.
|
|
253
|
+
* @category Issue and loss codes
|
|
254
|
+
*/
|
|
255
|
+
export const OTHER_FORMAT_CODE = "E_CSV_OTHER_FORMAT";
|
|
256
|
+
/**
|
|
257
|
+
* A quote inside an unquoted field (RFC 4180 forbids it); the quote is kept as text.
|
|
258
|
+
* @category Issue and loss codes
|
|
259
|
+
*/
|
|
260
|
+
export const STRAY_QUOTE_CODE = "W_CSV_STRAY_QUOTE";
|
|
261
|
+
/**
|
|
262
|
+
* A leading comment's direction disagrees with defaultDirected or with an earlier comment.
|
|
263
|
+
* @category Issue and loss codes
|
|
264
|
+
*/
|
|
265
|
+
export const COMMENT_DIRECTION_CODE = "W_CSV_COMMENT_DIRECTION";
|
|
266
|
+
/**
|
|
267
|
+
* A column named like Gephi's Type holds direction words but is read as a plain attribute.
|
|
268
|
+
* @category Issue and loss codes
|
|
269
|
+
*/
|
|
270
|
+
export const TYPE_COLUMN_IGNORED_CODE = "W_CSV_TYPE_COLUMN_IGNORED";
|
|
271
|
+
/**
|
|
272
|
+
* The header ends in a delimiter; rows without the empty last cell are read as complete.
|
|
273
|
+
* @category Issue and loss codes
|
|
274
|
+
*/
|
|
275
|
+
export const TRAILING_HEADER_DELIMITER_CODE = "W_CSV_TRAILING_HEADER_DELIMITER";
|
|
276
|
+
/**
|
|
277
|
+
* Every row is one cell that another delimiter would split (a likely wrong delimiter).
|
|
278
|
+
* @category Issue and loss codes
|
|
279
|
+
*/
|
|
280
|
+
export const SINGLE_COLUMN_CODE = "W_CSV_SINGLE_COLUMN";
|
|
281
|
+
/**
|
|
282
|
+
* Several header columns name the same role; the one not chosen is a plain attribute.
|
|
283
|
+
* @category Issue and loss codes
|
|
284
|
+
*/
|
|
285
|
+
export const AMBIGUOUS_COLUMN_CODE = "W_CSV_AMBIGUOUS_COLUMN";
|
|
286
|
+
/**
|
|
287
|
+
* An unquoted id with leading or trailing whitespace (kept, RFC 4180), distinct from the bare id.
|
|
288
|
+
* @category Issue and loss codes
|
|
289
|
+
*/
|
|
290
|
+
export const PADDED_ID_CODE = "W_CSV_PADDED_ID";
|
|
291
|
+
/**
|
|
292
|
+
* A leading `#` / `%` line skipped as a comment has the fields of a record.
|
|
293
|
+
* @category Issue and loss codes
|
|
294
|
+
*/
|
|
295
|
+
export const COMMENT_LIKE_RECORD_CODE = "W_CSV_COMMENT_LIKE_RECORD";
|
|
296
|
+
/**
|
|
297
|
+
* Data rows end in one extra empty cell (a trailing delimiter); the cell is dropped.
|
|
298
|
+
* @category Issue and loss codes
|
|
299
|
+
*/
|
|
300
|
+
export const TRAILING_DELIMITER_CODE = "W_CSV_TRAILING_DELIMITER";
|
|
301
|
+
/**
|
|
302
|
+
* A headerless three-column table's third column holds text, so it is an attribute, not the weight.
|
|
303
|
+
* @category Issue and loss codes
|
|
304
|
+
*/
|
|
305
|
+
export const WEIGHT_AS_ATTRIBUTE_CODE = "W_CSV_WEIGHT_AS_ATTRIBUTE";
|
|
149
306
|
|
|
150
307
|
const TABLE_MODES: ReadonlySet<string> = new Set(["edges", "nodes", "adjacency", "auto"]);
|
|
151
308
|
|
|
@@ -169,11 +326,11 @@ const USED_OPTIONS_WITH_NODES: ReadonlySet<keyof CommonImportOptions> = new Set<
|
|
|
169
326
|
...USED_OPTIONS,
|
|
170
327
|
"nodeIdFrom",
|
|
171
328
|
]);
|
|
172
|
-
const BAD_DELIMITERS: ReadonlySet<string> = new Set(['"', "\n", "\r"]);
|
|
173
329
|
|
|
174
330
|
/** The CSV options with defaults applied. */
|
|
175
331
|
interface ResolvedCsvOptions {
|
|
176
332
|
readonly delimiter: string | null;
|
|
333
|
+
readonly decimalComma: boolean;
|
|
177
334
|
readonly header: boolean | "auto";
|
|
178
335
|
readonly table: "edges" | "nodes" | "adjacency" | "auto";
|
|
179
336
|
readonly sourceColumn: CsvColumnRef | null;
|
|
@@ -181,8 +338,11 @@ interface ResolvedCsvOptions {
|
|
|
181
338
|
/** The direction column reference; null for none; undefined for the Gephi rule. */
|
|
182
339
|
readonly typeColumn: CsvColumnRef | null | undefined;
|
|
183
340
|
readonly idColumn: CsvColumnRef | null;
|
|
341
|
+
readonly labelColumn: CsvColumnRef | null;
|
|
184
342
|
readonly nodes: ImportInput | null;
|
|
185
343
|
readonly rowNumberIds: boolean;
|
|
344
|
+
/** The caller's explicit defaultDirected; null when it was left to its default. */
|
|
345
|
+
readonly explicitDirected: boolean | null;
|
|
186
346
|
}
|
|
187
347
|
|
|
188
348
|
/** The columns of an edge table, by index. */
|
|
@@ -194,9 +354,13 @@ interface EdgePlan {
|
|
|
194
354
|
readonly target: number;
|
|
195
355
|
readonly weight: number;
|
|
196
356
|
readonly type: number;
|
|
357
|
+
/** A column named like Type (any case) left as an attribute, or -1; its direction words are reported. */
|
|
358
|
+
readonly typeHint: number;
|
|
197
359
|
readonly id: number;
|
|
198
360
|
readonly label: number;
|
|
199
361
|
readonly attributes: readonly number[];
|
|
362
|
+
/** Whether a row may omit the last cell (the header ends in a delimiter). */
|
|
363
|
+
readonly optionalLast: boolean;
|
|
200
364
|
}
|
|
201
365
|
|
|
202
366
|
/** The columns of a node table, by index. */
|
|
@@ -210,9 +374,11 @@ interface NodePlan {
|
|
|
210
374
|
readonly rowNumber: boolean;
|
|
211
375
|
readonly label: number;
|
|
212
376
|
readonly attributes: readonly number[];
|
|
377
|
+
/** Whether a row may omit the last cell (the header ends in a delimiter). */
|
|
378
|
+
readonly optionalLast: boolean;
|
|
213
379
|
}
|
|
214
380
|
|
|
215
|
-
/** An adjacency table: no columns, a node and its
|
|
381
|
+
/** An adjacency table: no columns, a node and its neighbors per row. */
|
|
216
382
|
interface AdjacencyPlan {
|
|
217
383
|
readonly kind: "adjacency";
|
|
218
384
|
readonly names: readonly string[];
|
|
@@ -234,31 +400,54 @@ interface ImportState {
|
|
|
234
400
|
* (the `defaultDirected` option applies).
|
|
235
401
|
*/
|
|
236
402
|
commentDirected: boolean | null;
|
|
403
|
+
/** The comment line that set commentDirected. */
|
|
404
|
+
commentText: string;
|
|
237
405
|
}
|
|
238
406
|
|
|
239
407
|
/** The comment characters of the SNAP (`#`) and KONECT (`%`) headers (research note 07 section 2.6). */
|
|
240
408
|
const COMMENT_CHARS: readonly string[] = Object.freeze(["#", "%"]);
|
|
241
409
|
|
|
410
|
+
/** A number written with a decimal comma: digits, one comma, digits, an optional exponent. */
|
|
411
|
+
const DECIMAL_COMMA = /^\s*[+-]?\d+,\d+(?:[eE][+-]?\d+)?\s*$/;
|
|
412
|
+
|
|
242
413
|
/**
|
|
243
|
-
* The direction a SNAP or KONECT comment
|
|
414
|
+
* The direction a SNAP or KONECT comment line declares (research note 07 section 2.6): SNAP
|
|
244
415
|
* pages write `# Directed graph` / `# Undirected graph`, KONECT's first line is `% sym` (undirected),
|
|
245
|
-
* `% asym` (directed) or `% bip` (bipartite, undirected).
|
|
246
|
-
*
|
|
247
|
-
* @
|
|
416
|
+
* `% asym` (directed) or `% bip` (bipartite, undirected). Only whole words count, so
|
|
417
|
+
* `% symbols` or `# bipartite-ish` declare nothing.
|
|
418
|
+
* @param comment - a leading comment line
|
|
419
|
+
* @returns true / false when the line declares the direction, null otherwise
|
|
248
420
|
*/
|
|
249
|
-
function commentDirection(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
421
|
+
function commentDirection(comment: string): boolean | null {
|
|
422
|
+
const text = comment.slice(1).trim().toLowerCase();
|
|
423
|
+
if (/^(?:directed graph|asym)\b/.test(text)) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
if (/^(?:undirected graph|sym|bip)\b/.test(text)) {
|
|
427
|
+
return false;
|
|
258
428
|
}
|
|
259
429
|
return null;
|
|
260
430
|
}
|
|
261
431
|
|
|
432
|
+
/**
|
|
433
|
+
* The name of a direction, for messages.
|
|
434
|
+
* @param directed - the direction
|
|
435
|
+
* @returns "directed" or "undirected"
|
|
436
|
+
*/
|
|
437
|
+
function directionName(directed: boolean): string {
|
|
438
|
+
return directed ? "directed" : "undirected";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Whether an unquoted id cell has leading or trailing whitespace (kept as written, RFC 4180).
|
|
443
|
+
* @param text - the cell text
|
|
444
|
+
* @param quoted - whether the cell was quoted
|
|
445
|
+
* @returns true when padded
|
|
446
|
+
*/
|
|
447
|
+
function isPadded(text: string, quoted: boolean | undefined): boolean {
|
|
448
|
+
return quoted !== true && text.trim().length !== text.length;
|
|
449
|
+
}
|
|
450
|
+
|
|
262
451
|
/**
|
|
263
452
|
* Apply the defaults of the CSV options and check every value.
|
|
264
453
|
* @param options - the caller's options
|
|
@@ -272,12 +461,22 @@ function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | u
|
|
|
272
461
|
found: o.delimiter,
|
|
273
462
|
});
|
|
274
463
|
}
|
|
275
|
-
if (o.
|
|
276
|
-
throw new GraphFormatError("E_UNSUPPORTED",
|
|
277
|
-
option: "
|
|
278
|
-
found: o.
|
|
464
|
+
if (o.decimal !== undefined && o.decimal !== "." && o.decimal !== ",") {
|
|
465
|
+
throw new GraphFormatError("E_UNSUPPORTED", 'option decimal: expected "." or ","', {
|
|
466
|
+
option: "decimal",
|
|
467
|
+
found: o.decimal,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (o.decimal === "," && o.delimiter === ",") {
|
|
471
|
+
throw new GraphFormatError("E_UNSUPPORTED", 'options delimiter and decimal are both ","; pass delimiter: ";"', {
|
|
472
|
+
option: "decimal",
|
|
473
|
+
found: o.decimal,
|
|
279
474
|
});
|
|
280
475
|
}
|
|
476
|
+
if (o.delimiter !== undefined) {
|
|
477
|
+
// one character, not a line break, not the quote
|
|
478
|
+
checkRecordSyntax({ delimiter: o.delimiter, quote: '"' });
|
|
479
|
+
}
|
|
281
480
|
if (o.header !== undefined && o.header !== "auto" && typeof o.header !== "boolean") {
|
|
282
481
|
throw new GraphFormatError("E_UNSUPPORTED", 'option header: expected true, false or "auto"', {
|
|
283
482
|
option: "header",
|
|
@@ -290,14 +489,14 @@ function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | u
|
|
|
290
489
|
found: o.table,
|
|
291
490
|
});
|
|
292
491
|
}
|
|
293
|
-
for (const name of ["sourceColumn", "targetColumn", "idColumn"] as const) {
|
|
492
|
+
for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"] as const) {
|
|
294
493
|
checkColumnRef(name, o[name]);
|
|
295
494
|
}
|
|
296
495
|
if (o.typeColumn !== null) {
|
|
297
496
|
checkColumnRef("typeColumn", o.typeColumn);
|
|
298
497
|
}
|
|
299
498
|
if (o.table === "adjacency") {
|
|
300
|
-
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn"] as const) {
|
|
499
|
+
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"] as const) {
|
|
301
500
|
if (o[name] !== undefined) {
|
|
302
501
|
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
|
|
303
502
|
option: name,
|
|
@@ -320,14 +519,17 @@ function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | u
|
|
|
320
519
|
}
|
|
321
520
|
return {
|
|
322
521
|
delimiter: o.delimiter ?? null,
|
|
522
|
+
decimalComma: o.decimal === ",",
|
|
323
523
|
header: o.header ?? "auto",
|
|
324
524
|
table: o.table ?? "auto",
|
|
325
525
|
sourceColumn: o.sourceColumn ?? null,
|
|
326
526
|
targetColumn: o.targetColumn ?? null,
|
|
327
527
|
typeColumn: o.typeColumn,
|
|
328
528
|
idColumn: o.idColumn ?? null,
|
|
329
|
-
|
|
529
|
+
labelColumn: o.labelColumn ?? null,
|
|
530
|
+
nodes: nodesInput(o.nodes),
|
|
330
531
|
rowNumberIds: o.rowNumberIds ?? false,
|
|
532
|
+
explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
|
|
331
533
|
};
|
|
332
534
|
}
|
|
333
535
|
|
|
@@ -465,11 +667,12 @@ function parseKind(text: string): EdgeKind | null | undefined {
|
|
|
465
667
|
}
|
|
466
668
|
|
|
467
669
|
/**
|
|
468
|
-
* Split an adjacency
|
|
670
|
+
* Split an adjacency neighbor cell into its id and weight: the text after the LAST colon is the
|
|
469
671
|
* weight when it is a number, nothing when it is empty (`a:1:` is the id `a:1`, unweighted, the
|
|
470
672
|
* form the exporter writes for an id containing a colon), and part of the id otherwise.
|
|
471
673
|
* @param text - the cell text
|
|
472
674
|
* @returns the id text and the weight (undefined when the cell has none)
|
|
675
|
+
* @category Plugin helpers
|
|
473
676
|
*/
|
|
474
677
|
export function splitNeighbour(text: string): { readonly id: string; readonly weight: number | undefined } {
|
|
475
678
|
const colon = text.lastIndexOf(":");
|
|
@@ -526,6 +729,8 @@ class TableReader {
|
|
|
526
729
|
private dataRows = 0;
|
|
527
730
|
|
|
528
731
|
private nodeOrdinal = 0;
|
|
732
|
+
/** Whether a field-count error has already suggested `table: "adjacency"`. */
|
|
733
|
+
private adjacencyHinted = false;
|
|
529
734
|
|
|
530
735
|
/** The edge ids seen so far (the id column is unique; a repeat is skipped with an issue). */
|
|
531
736
|
private readonly edgeIds = new Set<string>();
|
|
@@ -550,18 +755,28 @@ class TableReader {
|
|
|
550
755
|
this.rowNumbers = state.csv.rowNumberIds && (kind === "nodes" || (kind === "auto" && state.csv.nodes === null));
|
|
551
756
|
const readerOptions: CsvReaderOptions = {
|
|
552
757
|
delimiter: state.csv.delimiter,
|
|
758
|
+
// a decimal comma is never the delimiter
|
|
759
|
+
...(state.csv.decimalComma ? { candidates: DELIMITER_CANDIDATES.filter((d) => d !== ",") } : {}),
|
|
553
760
|
comments: COMMENT_CHARS,
|
|
554
761
|
// field counts say nothing about an adjacency table, whose rows vary in width
|
|
555
762
|
skipQuoteErrors: kind === "adjacency",
|
|
556
763
|
signal: state.common.signal,
|
|
557
764
|
onProgress: progress ? state.common.onProgress : null,
|
|
558
765
|
encoding: state.common.encoding,
|
|
766
|
+
// an adjacency table has no header: an empty one is the empty graph
|
|
767
|
+
allowEmpty: kind === "adjacency",
|
|
768
|
+
// explicit dialect options say the caller knows the input is a table
|
|
769
|
+
inspect: hasExplicitDialect(state.csv)
|
|
770
|
+
? undefined
|
|
771
|
+
: (preview, line) => {
|
|
772
|
+
this.refuseOtherFormat(preview, line);
|
|
773
|
+
},
|
|
559
774
|
};
|
|
560
775
|
this.reader = new CsvRecordReader(input, state.report, readerOptions);
|
|
561
776
|
}
|
|
562
777
|
|
|
563
778
|
/**
|
|
564
|
-
* Read every row; the reader is closed (and a stream
|
|
779
|
+
* Read every row; the reader is closed (and a stream canceled) when the import aborts midway.
|
|
565
780
|
*/
|
|
566
781
|
async read(): Promise<void> {
|
|
567
782
|
const iterator = this.reader[Symbol.asyncIterator]();
|
|
@@ -578,7 +793,11 @@ class TableReader {
|
|
|
578
793
|
*/
|
|
579
794
|
private async readRows(iterator: AsyncGenerator<string[], void, undefined>): Promise<void> {
|
|
580
795
|
const { report } = this.state;
|
|
581
|
-
|
|
796
|
+
let first = await iterator.next();
|
|
797
|
+
if (!first.done && (this.reader.sepDirective || this.isSepDirective(first.value))) {
|
|
798
|
+
// Excel's `sep=;` line: the reader took the delimiter from it; it is not a row
|
|
799
|
+
first = await iterator.next();
|
|
800
|
+
}
|
|
582
801
|
if (first.done && this.kind === "adjacency") {
|
|
583
802
|
// an adjacency table has no header: an empty one is the empty graph
|
|
584
803
|
return;
|
|
@@ -603,7 +822,8 @@ class TableReader {
|
|
|
603
822
|
const secondRow: string[] | null = second.done ? null : second.value;
|
|
604
823
|
const secondLine = this.reader.line;
|
|
605
824
|
const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
|
|
606
|
-
|
|
825
|
+
// a column option naming a cell of the first row makes that row the header
|
|
826
|
+
header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
|
|
607
827
|
if (!header) {
|
|
608
828
|
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
609
829
|
}
|
|
@@ -616,11 +836,39 @@ class TableReader {
|
|
|
616
836
|
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
617
837
|
}
|
|
618
838
|
}
|
|
839
|
+
if (this.rowNumbers && !header) {
|
|
840
|
+
report.warning(
|
|
841
|
+
"unsupported",
|
|
842
|
+
OPTION_IGNORED_CODE,
|
|
843
|
+
"option rowNumberIds ignored: a headerless node table takes its ids from column 0",
|
|
844
|
+
{ line: firstLine, element: "rowNumberIds" },
|
|
845
|
+
);
|
|
846
|
+
}
|
|
619
847
|
const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
|
|
620
|
-
|
|
621
|
-
|
|
848
|
+
const last = firstRow.length - 1;
|
|
849
|
+
const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
|
|
850
|
+
this.applyCommentDirection();
|
|
851
|
+
this.plan = this.resolvePlan(
|
|
852
|
+
names,
|
|
853
|
+
header,
|
|
854
|
+
firstLine,
|
|
855
|
+
pending.map((p) => p.row),
|
|
856
|
+
optionalLast,
|
|
857
|
+
);
|
|
858
|
+
if (optionalLast) {
|
|
859
|
+
report.warning(
|
|
860
|
+
"validation-error",
|
|
861
|
+
TRAILING_HEADER_DELIMITER_CODE,
|
|
862
|
+
`the header ends in a delimiter; rows without the empty last cell are read as complete`,
|
|
863
|
+
{ line: firstLine, element: names[last] },
|
|
864
|
+
);
|
|
622
865
|
}
|
|
623
|
-
this.
|
|
866
|
+
this.reportCommentLikeRecords(names.length);
|
|
867
|
+
this.reportSingleColumn(
|
|
868
|
+
names.length,
|
|
869
|
+
pending.map((p) => p.row),
|
|
870
|
+
firstLine,
|
|
871
|
+
);
|
|
624
872
|
this.prepareColumns(firstLine);
|
|
625
873
|
for (const { row, quoted, line } of pending) {
|
|
626
874
|
this.processRow(row, quoted, line);
|
|
@@ -646,6 +894,151 @@ class TableReader {
|
|
|
646
894
|
line: firstLine,
|
|
647
895
|
});
|
|
648
896
|
}
|
|
897
|
+
this.reportReaderFindings();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Whether a row is Excel's delimiter directive (`sep=;`) under a delimiter the caller gave (a
|
|
902
|
+
* sniffed one comes from the directive itself: `reader.sepDirective`).
|
|
903
|
+
* @param row - the first row
|
|
904
|
+
* @returns true for the directive
|
|
905
|
+
*/
|
|
906
|
+
private isSepDirective(row: readonly string[]): boolean {
|
|
907
|
+
return (
|
|
908
|
+
this.state.csv.delimiter !== null &&
|
|
909
|
+
!this.reader.quoted[0] &&
|
|
910
|
+
/^sep=.$/i.test(row.join(this.state.csv.delimiter))
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Fail when the input opens like another format (an HTML error page, a JSON, GML, DOT or Pajek
|
|
916
|
+
* file handed to the CSV importer), which any delimiter would otherwise read as rows of text.
|
|
917
|
+
* Runs on the raw preview before any record is parsed, so a quoting error in the other
|
|
918
|
+
* format's syntax cannot abort first.
|
|
919
|
+
* @param preview - the start of the input, leading comments removed
|
|
920
|
+
* @param line - the line it starts on
|
|
921
|
+
*/
|
|
922
|
+
private refuseOtherFormat(preview: string, line: number): void {
|
|
923
|
+
if (OTHER_FORMATS.some((r) => r.test(preview))) {
|
|
924
|
+
this.state.report.fail(
|
|
925
|
+
OTHER_FORMAT_CODE,
|
|
926
|
+
`the input opens like another format (XML / HTML, JSON, GML, DOT or Pajek), not CSV: ${JSON.stringify(preview.trimStart().slice(0, 60))}`,
|
|
927
|
+
{ line },
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Whether a column option names a cell of the first row by text.
|
|
934
|
+
* @param row - the first row
|
|
935
|
+
* @returns true when sourceColumn, targetColumn, idColumn or typeColumn names one of its cells
|
|
936
|
+
*/
|
|
937
|
+
private namesExplicitColumn(row: readonly string[]): boolean {
|
|
938
|
+
const { csv } = this.state;
|
|
939
|
+
const names = headerNames(row);
|
|
940
|
+
return [csv.sourceColumn, csv.targetColumn, csv.idColumn, csv.typeColumn, csv.labelColumn].some(
|
|
941
|
+
(ref) => typeof ref === "string" && findColumn(names, [ref]) >= 0,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Take the file-level direction from the leading SNAP / KONECT comments of this table: the first
|
|
947
|
+
* declaring line wins; a later line declaring the other direction, and a declaration that
|
|
948
|
+
* overrides the caller's explicit defaultDirected, are reported.
|
|
949
|
+
*/
|
|
950
|
+
private applyCommentDirection(): void {
|
|
951
|
+
const { state } = this;
|
|
952
|
+
const { leadingComments, leadingCommentLines } = this.reader;
|
|
953
|
+
for (let i = 0; i < leadingComments.length; i++) {
|
|
954
|
+
const comment = leadingComments[i];
|
|
955
|
+
const directed = commentDirection(comment);
|
|
956
|
+
if (directed === null) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const where = { line: leadingCommentLines[i], element: comment };
|
|
960
|
+
if (state.commentDirected === null) {
|
|
961
|
+
state.commentDirected = directed;
|
|
962
|
+
state.commentText = comment;
|
|
963
|
+
const explicit = state.csv.explicitDirected;
|
|
964
|
+
if (explicit !== null && explicit !== directed) {
|
|
965
|
+
state.report.warning(
|
|
966
|
+
"validation-error",
|
|
967
|
+
COMMENT_DIRECTION_CODE,
|
|
968
|
+
`${JSON.stringify(comment)} declares a ${directionName(directed)} graph, which overrides defaultDirected: ${explicit}`,
|
|
969
|
+
where,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
} else if (state.commentDirected !== directed) {
|
|
973
|
+
state.report.warning(
|
|
974
|
+
"validation-error",
|
|
975
|
+
COMMENT_DIRECTION_CODE,
|
|
976
|
+
`${JSON.stringify(comment)} declares a ${directionName(directed)} graph but ${JSON.stringify(state.commentText)} declared a ${directionName(state.commentDirected)} one; the first applies`,
|
|
977
|
+
where,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Report a leading `#` / `%` line that has the fields of a record (a first id starting with `#`,
|
|
985
|
+
* swallowed as a comment): its text directly follows the comment character and splits into as
|
|
986
|
+
* many cells as the table has columns. SNAP and KONECT comment lines put a space after it.
|
|
987
|
+
* @param width - the number of columns
|
|
988
|
+
*/
|
|
989
|
+
private reportCommentLikeRecords(width: number): void {
|
|
990
|
+
const { delimiter, leadingComments, leadingCommentLines } = this.reader;
|
|
991
|
+
if (width < 2 || delimiter === null) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
for (let i = 0; i < leadingComments.length; i++) {
|
|
995
|
+
const comment = leadingComments[i];
|
|
996
|
+
if (comment.length > 1 && comment[1].trim().length > 0 && comment.split(delimiter).length === width) {
|
|
997
|
+
this.state.report.warnOnce(
|
|
998
|
+
"parse-error",
|
|
999
|
+
COMMENT_LIKE_RECORD_CODE,
|
|
1000
|
+
`${JSON.stringify(comment)} was skipped as a leading comment but has the fields of a record; quote an id that starts with # or %`,
|
|
1001
|
+
{ line: leadingCommentLines[i], element: comment },
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Report a one-column table whose cells another delimiter would split consistently (a delimiter
|
|
1009
|
+
* option that does not match the file).
|
|
1010
|
+
* @param width - the number of columns
|
|
1011
|
+
* @param rows - the data rows read so far
|
|
1012
|
+
* @param line - the first line
|
|
1013
|
+
*/
|
|
1014
|
+
private reportSingleColumn(width: number, rows: readonly (readonly string[])[], line: number): void {
|
|
1015
|
+
if (width !== 1 || rows.length === 0 || this.plan?.kind === "adjacency") {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const candidates = DELIMITER_CANDIDATES.filter((d) => d !== this.reader.delimiter);
|
|
1019
|
+
const other = sniffDelimiter(`${rows.map((r) => r[0]).join("\n")}\n`, "\n", candidates);
|
|
1020
|
+
if (other !== null) {
|
|
1021
|
+
this.state.report.warning(
|
|
1022
|
+
"parse-error",
|
|
1023
|
+
SINGLE_COLUMN_CODE,
|
|
1024
|
+
`every row read so far is one cell that ${JSON.stringify(other)} splits consistently; pass delimiter: ${JSON.stringify(other)} if that is the file's delimiter`,
|
|
1025
|
+
{ line },
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/** Report what the record reader noticed while reading: a stray quote. */
|
|
1031
|
+
private reportReaderFindings(): void {
|
|
1032
|
+
const { report } = this.state;
|
|
1033
|
+
const { strayQuoteLine } = this.reader;
|
|
1034
|
+
if (strayQuoteLine > 0) {
|
|
1035
|
+
report.warnOnce(
|
|
1036
|
+
"parse-error",
|
|
1037
|
+
STRAY_QUOTE_CODE,
|
|
1038
|
+
`a quote inside an unquoted field is kept as text (RFC 4180 quotes a whole field)`,
|
|
1039
|
+
{ line: strayQuoteLine },
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
649
1042
|
}
|
|
650
1043
|
|
|
651
1044
|
/**
|
|
@@ -653,31 +1046,26 @@ class TableReader {
|
|
|
653
1046
|
* @param names - the column names
|
|
654
1047
|
* @param header - whether the file has a header row
|
|
655
1048
|
* @param line - the header line
|
|
1049
|
+
* @param sample - the data rows read so far
|
|
1050
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
656
1051
|
* @returns the plan; the import aborts when no endpoints (or id) resolve
|
|
657
1052
|
*/
|
|
658
|
-
private resolvePlan(
|
|
659
|
-
|
|
1053
|
+
private resolvePlan(
|
|
1054
|
+
names: readonly string[],
|
|
1055
|
+
header: boolean,
|
|
1056
|
+
line: number,
|
|
1057
|
+
sample: readonly (readonly string[])[],
|
|
1058
|
+
optionalLast: boolean,
|
|
1059
|
+
): EdgePlan | NodePlan | AdjacencyPlan {
|
|
1060
|
+
const { csv } = this.state;
|
|
660
1061
|
if (this.kind === "adjacency") {
|
|
661
1062
|
return { kind: "adjacency", names: [] };
|
|
662
1063
|
}
|
|
663
|
-
const width = names.length;
|
|
664
1064
|
let source = -1;
|
|
665
1065
|
let target = -1;
|
|
666
1066
|
if (this.kind !== "nodes") {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
} else if (header) {
|
|
670
|
-
source = findColumn(names, SOURCE_NAMES);
|
|
671
|
-
} else {
|
|
672
|
-
source = width >= 2 ? 0 : -1;
|
|
673
|
-
}
|
|
674
|
-
if (csv.targetColumn !== null) {
|
|
675
|
-
target = resolveColumnRef(names, csv.targetColumn, "targetColumn");
|
|
676
|
-
} else if (header) {
|
|
677
|
-
target = findColumn(names, TARGET_NAMES);
|
|
678
|
-
} else {
|
|
679
|
-
target = width >= 2 ? 1 : -1;
|
|
680
|
-
}
|
|
1067
|
+
source = endpointColumn(names, header, csv.sourceColumn, "sourceColumn", SOURCE_NAMES, 0);
|
|
1068
|
+
target = endpointColumn(names, header, csv.targetColumn, "targetColumn", TARGET_NAMES, 1);
|
|
681
1069
|
if (source >= 0 && target >= 0 && source === target) {
|
|
682
1070
|
throw new GraphFormatError("E_UNSUPPORTED", "sourceColumn and targetColumn name the same column", {
|
|
683
1071
|
option: "targetColumn",
|
|
@@ -686,12 +1074,43 @@ class TableReader {
|
|
|
686
1074
|
}
|
|
687
1075
|
}
|
|
688
1076
|
if (source >= 0 && target >= 0) {
|
|
689
|
-
return this.edgePlan(names, header, source, target);
|
|
1077
|
+
return this.edgePlan(names, header, source, target, line, sample, optionalLast);
|
|
690
1078
|
}
|
|
1079
|
+
this.refuseWithoutEndpoints(names, header, source, target, line);
|
|
1080
|
+
return this.nodePlan(names, header, line, optionalLast);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Refuse a table without both endpoint columns, unless it can be a node table: an edge table
|
|
1085
|
+
* was asked for, a single endpoint column was found, or nothing names an id.
|
|
1086
|
+
* @param names - the column names
|
|
1087
|
+
* @param header - whether the file has a header row
|
|
1088
|
+
* @param source - the source column, or -1
|
|
1089
|
+
* @param target - the target column, or -1
|
|
1090
|
+
* @param line - the header line
|
|
1091
|
+
*/
|
|
1092
|
+
private refuseWithoutEndpoints(
|
|
1093
|
+
names: readonly string[],
|
|
1094
|
+
header: boolean,
|
|
1095
|
+
source: number,
|
|
1096
|
+
target: number,
|
|
1097
|
+
line: number,
|
|
1098
|
+
): void {
|
|
1099
|
+
const { csv, report } = this.state;
|
|
691
1100
|
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
1101
|
+
if (source >= 0 || target >= 0) {
|
|
1102
|
+
// one endpoint column is a broken edge table, never a node table
|
|
1103
|
+
const [has, missing] = source >= 0 ? ["source", "target"] : ["target", "source"];
|
|
1104
|
+
report.fail(
|
|
1105
|
+
NO_ENDPOINT_COLUMNS_CODE,
|
|
1106
|
+
`the header has a ${has} column (${JSON.stringify(names[Math.max(source, target)])}) but no ${missing} column (${shown})`,
|
|
1107
|
+
{ line },
|
|
1108
|
+
{ columns: [...names] },
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
692
1111
|
if (
|
|
693
1112
|
this.kind === "edges" ||
|
|
694
|
-
(this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null))
|
|
1113
|
+
(this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null || csv.nodes !== null))
|
|
695
1114
|
) {
|
|
696
1115
|
report.fail(
|
|
697
1116
|
NO_ENDPOINT_COLUMNS_CODE,
|
|
@@ -700,7 +1119,7 @@ class TableReader {
|
|
|
700
1119
|
{ columns: [...names] },
|
|
701
1120
|
);
|
|
702
1121
|
}
|
|
703
|
-
const idResolves = header ? findColumn(names, ID_NAMES) >= 0 :
|
|
1122
|
+
const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : names.length >= 1;
|
|
704
1123
|
if (this.kind === "auto" && csv.idColumn === null && !idResolves && !this.rowNumbers) {
|
|
705
1124
|
report.fail(
|
|
706
1125
|
NO_ENDPOINT_COLUMNS_CODE,
|
|
@@ -709,7 +1128,6 @@ class TableReader {
|
|
|
709
1128
|
{ columns: [...names] },
|
|
710
1129
|
);
|
|
711
1130
|
}
|
|
712
|
-
return this.nodePlan(names, header, line);
|
|
713
1131
|
}
|
|
714
1132
|
|
|
715
1133
|
/**
|
|
@@ -718,50 +1136,35 @@ class TableReader {
|
|
|
718
1136
|
* @param header - whether the file has a header row
|
|
719
1137
|
* @param source - the source column
|
|
720
1138
|
* @param target - the target column
|
|
1139
|
+
* @param line - the header (or first) line
|
|
1140
|
+
* @param sample - the data rows read so far
|
|
1141
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
721
1142
|
* @returns the plan
|
|
722
1143
|
*/
|
|
723
|
-
private edgePlan(
|
|
724
|
-
|
|
1144
|
+
private edgePlan(
|
|
1145
|
+
names: readonly string[],
|
|
1146
|
+
header: boolean,
|
|
1147
|
+
source: number,
|
|
1148
|
+
target: number,
|
|
1149
|
+
line: number,
|
|
1150
|
+
sample: readonly (readonly string[])[],
|
|
1151
|
+
optionalLast: boolean,
|
|
1152
|
+
): EdgePlan {
|
|
1153
|
+
const { csv } = this.state;
|
|
725
1154
|
const claimed = new Set<number>([source, target]);
|
|
726
|
-
|
|
727
|
-
if (common.weightFrom !== null) {
|
|
728
|
-
if (header) {
|
|
729
|
-
weight = findFree(names, [common.weightFrom], claimed);
|
|
730
|
-
if (weight < 0 && this.state.weightFromExplicit) {
|
|
731
|
-
report.warning(
|
|
732
|
-
"missing-value",
|
|
733
|
-
COLUMN_MISSING_CODE,
|
|
734
|
-
`weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`,
|
|
735
|
-
{ line: this.reader.line, element: common.weightFrom },
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
} else if (names.length >= 3 && !claimed.has(2)) {
|
|
739
|
-
weight = 2;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
1155
|
+
const weight = this.weightColumn(names, header, claimed, line, sample);
|
|
742
1156
|
if (weight >= 0) {
|
|
743
1157
|
claimed.add(weight);
|
|
744
1158
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (header && names[source] === "Source" && names[target] === "Target") {
|
|
748
|
-
type = findFree(names, [TYPE_NAME], claimed);
|
|
749
|
-
if (type >= 0 && names[type] !== TYPE_NAME) {
|
|
750
|
-
type = -1;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
} else if (csv.typeColumn !== null) {
|
|
754
|
-
type = resolveColumnRef(names, csv.typeColumn, "typeColumn");
|
|
755
|
-
if (claimed.has(type)) {
|
|
756
|
-
throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
|
|
757
|
-
option: "typeColumn",
|
|
758
|
-
found: names[type],
|
|
759
|
-
});
|
|
760
|
-
}
|
|
1159
|
+
if (header) {
|
|
1160
|
+
this.reportEndpointRivals(names, source, target, weight, line);
|
|
761
1161
|
}
|
|
1162
|
+
const type = this.typeColumn(names, header, source, target, claimed);
|
|
762
1163
|
if (type >= 0) {
|
|
763
1164
|
claimed.add(type);
|
|
764
1165
|
}
|
|
1166
|
+
const typeHint =
|
|
1167
|
+
header && type < 0 && csv.typeColumn === undefined ? findFree(names, [TYPE_NAME], claimed) : -1;
|
|
765
1168
|
const id = header ? findFree(names, EDGE_ID_NAMES, claimed) : -1;
|
|
766
1169
|
if (id >= 0) {
|
|
767
1170
|
claimed.add(id);
|
|
@@ -776,7 +1179,174 @@ class TableReader {
|
|
|
776
1179
|
attributes.push(i);
|
|
777
1180
|
}
|
|
778
1181
|
}
|
|
779
|
-
return {
|
|
1182
|
+
return {
|
|
1183
|
+
kind: "edges",
|
|
1184
|
+
names,
|
|
1185
|
+
width: names.length,
|
|
1186
|
+
source,
|
|
1187
|
+
target,
|
|
1188
|
+
weight,
|
|
1189
|
+
type,
|
|
1190
|
+
typeHint,
|
|
1191
|
+
id,
|
|
1192
|
+
label,
|
|
1193
|
+
attributes,
|
|
1194
|
+
optionalLast,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* The weight column of an edge table: the weightFrom column of a header (a warning when it was
|
|
1200
|
+
* asked for and is missing), else the third column of a headerless edge list unless it holds text.
|
|
1201
|
+
* @param names - the column names
|
|
1202
|
+
* @param header - whether the file has a header row
|
|
1203
|
+
* @param claimed - the endpoint columns
|
|
1204
|
+
* @param line - the header (or first) line
|
|
1205
|
+
* @param sample - the data rows read so far
|
|
1206
|
+
* @returns the column index, or -1
|
|
1207
|
+
*/
|
|
1208
|
+
private weightColumn(
|
|
1209
|
+
names: readonly string[],
|
|
1210
|
+
header: boolean,
|
|
1211
|
+
claimed: ReadonlySet<number>,
|
|
1212
|
+
line: number,
|
|
1213
|
+
sample: readonly (readonly string[])[],
|
|
1214
|
+
): number {
|
|
1215
|
+
const { common, report } = this.state;
|
|
1216
|
+
if (common.weightFrom === null) {
|
|
1217
|
+
return -1;
|
|
1218
|
+
}
|
|
1219
|
+
if (header) {
|
|
1220
|
+
const weight = findFree(names, [common.weightFrom], claimed);
|
|
1221
|
+
if (weight < 0 && this.state.weightFromExplicit) {
|
|
1222
|
+
report.warning(
|
|
1223
|
+
"missing-value",
|
|
1224
|
+
COLUMN_MISSING_CODE,
|
|
1225
|
+
`weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`,
|
|
1226
|
+
{ line, element: common.weightFrom },
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
return weight;
|
|
1230
|
+
}
|
|
1231
|
+
if (names.length < 3 || claimed.has(2)) {
|
|
1232
|
+
return -1;
|
|
1233
|
+
}
|
|
1234
|
+
if (isTextTriple(names.length, sample)) {
|
|
1235
|
+
report.warnOnce(
|
|
1236
|
+
"validation-error",
|
|
1237
|
+
WEIGHT_AS_ATTRIBUTE_CODE,
|
|
1238
|
+
`the third column holds text in the first rows, so it is read as the attribute ${names[2]}, not the weight`,
|
|
1239
|
+
{ line, element: names[2] },
|
|
1240
|
+
);
|
|
1241
|
+
return -1;
|
|
1242
|
+
}
|
|
1243
|
+
return 2;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Warn about header columns that name the source, target or weight beside the chosen one.
|
|
1248
|
+
* @param names - the header names
|
|
1249
|
+
* @param source - the source column
|
|
1250
|
+
* @param target - the target column
|
|
1251
|
+
* @param weight - the weight column, or -1
|
|
1252
|
+
* @param line - the header line
|
|
1253
|
+
*/
|
|
1254
|
+
private reportEndpointRivals(
|
|
1255
|
+
names: readonly string[],
|
|
1256
|
+
source: number,
|
|
1257
|
+
target: number,
|
|
1258
|
+
weight: number,
|
|
1259
|
+
line: number,
|
|
1260
|
+
): void {
|
|
1261
|
+
const { csv, common } = this.state;
|
|
1262
|
+
if (csv.sourceColumn === null) {
|
|
1263
|
+
this.reportRivals(names, SOURCE_NAMES, source, "source", line);
|
|
1264
|
+
}
|
|
1265
|
+
if (csv.targetColumn === null) {
|
|
1266
|
+
this.reportRivals(names, TARGET_NAMES, target, "target", line);
|
|
1267
|
+
}
|
|
1268
|
+
if (weight >= 0 && common.weightFrom !== null) {
|
|
1269
|
+
this.reportRivals(names, [common.weightFrom], weight, "weight", line);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* The edge type column: the typeColumn option, else Gephi's `Type` beside `Source` and `Target`.
|
|
1275
|
+
* @param names - the column names
|
|
1276
|
+
* @param header - whether the file has a header row
|
|
1277
|
+
* @param source - the source column
|
|
1278
|
+
* @param target - the target column
|
|
1279
|
+
* @param claimed - the columns other roles took
|
|
1280
|
+
* @returns the column index, or -1
|
|
1281
|
+
*/
|
|
1282
|
+
private typeColumn(
|
|
1283
|
+
names: readonly string[],
|
|
1284
|
+
header: boolean,
|
|
1285
|
+
source: number,
|
|
1286
|
+
target: number,
|
|
1287
|
+
claimed: ReadonlySet<number>,
|
|
1288
|
+
): number {
|
|
1289
|
+
const { typeColumn } = this.state.csv;
|
|
1290
|
+
if (typeColumn === null) {
|
|
1291
|
+
return -1;
|
|
1292
|
+
}
|
|
1293
|
+
if (typeColumn === undefined) {
|
|
1294
|
+
const gephi = header && names[source] === "Source" && names[target] === "Target";
|
|
1295
|
+
const type = gephi ? findFree(names, [TYPE_NAME], claimed) : -1;
|
|
1296
|
+
return type >= 0 && names[type] === TYPE_NAME ? type : -1;
|
|
1297
|
+
}
|
|
1298
|
+
const type = resolveColumnRef(names, typeColumn, "typeColumn");
|
|
1299
|
+
if (claimed.has(type)) {
|
|
1300
|
+
throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
|
|
1301
|
+
option: "typeColumn",
|
|
1302
|
+
found: names[type],
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
return type;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Warn about header columns that name a role another column was chosen for (`Source,source`,
|
|
1310
|
+
* `from,to,source`, `Weight,weight`): the one not chosen is read as a plain attribute.
|
|
1311
|
+
* @param names - the header names
|
|
1312
|
+
* @param candidates - the names of the role
|
|
1313
|
+
* @param chosen - the column chosen
|
|
1314
|
+
* @param role - the role, for the message
|
|
1315
|
+
* @param line - the header line
|
|
1316
|
+
*/
|
|
1317
|
+
private reportRivals(
|
|
1318
|
+
names: readonly string[],
|
|
1319
|
+
candidates: readonly string[],
|
|
1320
|
+
chosen: number,
|
|
1321
|
+
role: string,
|
|
1322
|
+
line: number,
|
|
1323
|
+
): void {
|
|
1324
|
+
const lower = new Set(candidates.map((c) => c.toLowerCase()));
|
|
1325
|
+
names.forEach((name, i) => {
|
|
1326
|
+
if (i !== chosen && lower.has(name.toLowerCase())) {
|
|
1327
|
+
this.state.report.warning(
|
|
1328
|
+
"validation-error",
|
|
1329
|
+
AMBIGUOUS_COLUMN_CODE,
|
|
1330
|
+
`columns ${JSON.stringify(names[chosen])} and ${JSON.stringify(name)} both name the ${role}; ${JSON.stringify(names[chosen])} is used and ${JSON.stringify(name)} is a plain attribute`,
|
|
1331
|
+
{ line, element: name },
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* The label column of a node table: the labelColumn option, else a header column named `label`.
|
|
1339
|
+
* @param names - the column names
|
|
1340
|
+
* @param header - whether the file has a header row
|
|
1341
|
+
* @param claimed - columns another role took
|
|
1342
|
+
* @returns the column index, or -1
|
|
1343
|
+
*/
|
|
1344
|
+
private labelOf(names: readonly string[], header: boolean, claimed: ReadonlySet<number>): number {
|
|
1345
|
+
const { labelColumn } = this.state.csv;
|
|
1346
|
+
if (labelColumn !== null) {
|
|
1347
|
+
return resolveColumnRef(names, labelColumn, "labelColumn");
|
|
1348
|
+
}
|
|
1349
|
+
return header ? findFree(names, LABEL_NAMES, claimed) : -1;
|
|
780
1350
|
}
|
|
781
1351
|
|
|
782
1352
|
/**
|
|
@@ -784,49 +1354,17 @@ class TableReader {
|
|
|
784
1354
|
* @param names - the column names
|
|
785
1355
|
* @param header - whether the file has a header row
|
|
786
1356
|
* @param line - the header line
|
|
1357
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
787
1358
|
* @returns the plan; the import aborts when no id column resolves
|
|
788
1359
|
*/
|
|
789
|
-
private nodePlan(names: readonly string[], header: boolean, line: number): NodePlan {
|
|
790
|
-
const {
|
|
1360
|
+
private nodePlan(names: readonly string[], header: boolean, line: number, optionalLast: boolean): NodePlan {
|
|
1361
|
+
const { common } = this.state;
|
|
791
1362
|
const claimed = new Set<number>();
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
} else if (names.length >= 1) {
|
|
798
|
-
idColumn = 0;
|
|
799
|
-
}
|
|
800
|
-
const label = header ? findFree(names, LABEL_NAMES, new Set(idColumn >= 0 ? [idColumn] : [])) : -1;
|
|
801
|
-
let id: number;
|
|
802
|
-
switch (common.nodeIdFrom) {
|
|
803
|
-
case "label":
|
|
804
|
-
if (label < 0) {
|
|
805
|
-
report.fail(NO_ID_COLUMN_CODE, 'nodeIdFrom is "label" but the node table has no label column', {
|
|
806
|
-
line,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
id = label;
|
|
810
|
-
break;
|
|
811
|
-
case "index":
|
|
812
|
-
id = -1;
|
|
813
|
-
break;
|
|
814
|
-
default:
|
|
815
|
-
if (idColumn < 0 && this.rowNumbers) {
|
|
816
|
-
id = -1;
|
|
817
|
-
break;
|
|
818
|
-
}
|
|
819
|
-
if (idColumn < 0) {
|
|
820
|
-
report.fail(
|
|
821
|
-
NO_ID_COLUMN_CODE,
|
|
822
|
-
`no id column in the node table header (${names.map((n) => JSON.stringify(n)).join(", ")})`,
|
|
823
|
-
{ line },
|
|
824
|
-
{ columns: [...names] },
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
id = idColumn;
|
|
828
|
-
claimed.add(idColumn);
|
|
829
|
-
break;
|
|
1363
|
+
const idColumn = this.idColumnOf(names, header);
|
|
1364
|
+
const label = this.labelOf(names, header, new Set(idColumn >= 0 ? [idColumn] : []));
|
|
1365
|
+
const id = this.nodeIdColumn(names, idColumn, label, line);
|
|
1366
|
+
if (id >= 0) {
|
|
1367
|
+
claimed.add(id);
|
|
830
1368
|
}
|
|
831
1369
|
if (label >= 0) {
|
|
832
1370
|
claimed.add(label);
|
|
@@ -838,7 +1376,62 @@ class TableReader {
|
|
|
838
1376
|
}
|
|
839
1377
|
}
|
|
840
1378
|
const rowNumber = id < 0 && common.nodeIdFrom !== "index";
|
|
841
|
-
return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes };
|
|
1379
|
+
return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes, optionalLast };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* The id column of a node table: the idColumn option, else a header column with an id name,
|
|
1384
|
+
* else the first column of a headerless table.
|
|
1385
|
+
* @param names - the column names
|
|
1386
|
+
* @param header - whether the file has a header row
|
|
1387
|
+
* @returns the column index, or -1
|
|
1388
|
+
*/
|
|
1389
|
+
private idColumnOf(names: readonly string[], header: boolean): number {
|
|
1390
|
+
const { idColumn } = this.state.csv;
|
|
1391
|
+
if (idColumn !== null) {
|
|
1392
|
+
return resolveColumnRef(names, idColumn, "idColumn");
|
|
1393
|
+
}
|
|
1394
|
+
if (header) {
|
|
1395
|
+
return findColumn(names, ID_NAMES);
|
|
1396
|
+
}
|
|
1397
|
+
return names.length >= 1 ? 0 : -1;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* The column node ids come from, as nodeIdFrom says: the label, none (row numbers), or the id
|
|
1402
|
+
* column. The import aborts when that column is missing.
|
|
1403
|
+
* @param names - the column names
|
|
1404
|
+
* @param idColumn - the id column, or -1
|
|
1405
|
+
* @param label - the label column, or -1
|
|
1406
|
+
* @param line - the header line
|
|
1407
|
+
* @returns the column index, or -1 for row numbers
|
|
1408
|
+
*/
|
|
1409
|
+
private nodeIdColumn(names: readonly string[], idColumn: number, label: number, line: number): number {
|
|
1410
|
+
const { common, report } = this.state;
|
|
1411
|
+
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
1412
|
+
if (common.nodeIdFrom === "index") {
|
|
1413
|
+
return -1;
|
|
1414
|
+
}
|
|
1415
|
+
if (common.nodeIdFrom === "label") {
|
|
1416
|
+
if (label < 0) {
|
|
1417
|
+
report.fail(
|
|
1418
|
+
NO_ID_COLUMN_CODE,
|
|
1419
|
+
`nodeIdFrom is "label", but the node table has no column named "label" (${shown}); pass labelColumn to name the column that holds the labels`,
|
|
1420
|
+
{ line },
|
|
1421
|
+
{ columns: [...names] },
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return label;
|
|
1425
|
+
}
|
|
1426
|
+
if (idColumn < 0 && !this.rowNumbers) {
|
|
1427
|
+
report.fail(
|
|
1428
|
+
NO_ID_COLUMN_CODE,
|
|
1429
|
+
`no id column in the node table header (${shown})`,
|
|
1430
|
+
{ line },
|
|
1431
|
+
{ columns: [...names] },
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
return idColumn;
|
|
842
1435
|
}
|
|
843
1436
|
|
|
844
1437
|
/**
|
|
@@ -899,6 +1492,13 @@ class TableReader {
|
|
|
899
1492
|
private processRow(row: string[], quoted: readonly boolean[], line: number): void {
|
|
900
1493
|
const plan = this.requirePlan();
|
|
901
1494
|
this.dataRows++;
|
|
1495
|
+
if (this.state.csv.decimalComma) {
|
|
1496
|
+
for (let i = 0; i < row.length; i++) {
|
|
1497
|
+
if (DECIMAL_COMMA.test(row[i])) {
|
|
1498
|
+
row[i] = row[i].replace(",", ".");
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
902
1502
|
if (plan.kind === "edges") {
|
|
903
1503
|
this.processEdgeRow(plan, row, quoted, line);
|
|
904
1504
|
} else if (plan.kind === "nodes") {
|
|
@@ -909,7 +1509,7 @@ class TableReader {
|
|
|
909
1509
|
}
|
|
910
1510
|
|
|
911
1511
|
/**
|
|
912
|
-
* Push one adjacency row: the node, then one edge per set
|
|
1512
|
+
* Push one adjacency row: the node, then one edge per set neighbor cell, in row order.
|
|
913
1513
|
* @param row - the cells
|
|
914
1514
|
* @param quoted - whether each cell was quoted
|
|
915
1515
|
* @param line - the row's line
|
|
@@ -920,16 +1520,16 @@ class TableReader {
|
|
|
920
1520
|
const { where } = this;
|
|
921
1521
|
where.line = line;
|
|
922
1522
|
where.element = null;
|
|
923
|
-
let
|
|
1523
|
+
let neighbors = 0;
|
|
924
1524
|
for (let k = 1; k < row.length; k++) {
|
|
925
1525
|
if (!isUnset(row[k], quoted[k])) {
|
|
926
|
-
|
|
1526
|
+
neighbors++;
|
|
927
1527
|
}
|
|
928
1528
|
}
|
|
929
1529
|
if (isUnset(row[0], quoted[0])) {
|
|
930
|
-
report.error("missing-value", MISSING_ID_CODE, `
|
|
1530
|
+
report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
|
|
931
1531
|
counts.skippedNodes++;
|
|
932
|
-
counts.skippedEdges +=
|
|
1532
|
+
counts.skippedEdges += neighbors;
|
|
933
1533
|
return;
|
|
934
1534
|
}
|
|
935
1535
|
const kind: EdgeKind = (this.state.commentDirected ?? common.defaultDirected) ? "directed" : "undirected";
|
|
@@ -948,7 +1548,7 @@ class TableReader {
|
|
|
948
1548
|
} catch (err) {
|
|
949
1549
|
report.recordError(err, where);
|
|
950
1550
|
counts.skippedNodes++;
|
|
951
|
-
counts.skippedEdges +=
|
|
1551
|
+
counts.skippedEdges += neighbors;
|
|
952
1552
|
return;
|
|
953
1553
|
}
|
|
954
1554
|
for (let k = 1; k < row.length; k++) {
|
|
@@ -975,19 +1575,15 @@ class TableReader {
|
|
|
975
1575
|
* Push one edge row: endpoints, weight, direction, then the attribute cells.
|
|
976
1576
|
* @param plan - the edge plan
|
|
977
1577
|
* @param row - the cells
|
|
978
|
-
* @param
|
|
1578
|
+
* @param cellQuoted - whether each cell was quoted
|
|
979
1579
|
* @param line - the row's line
|
|
980
1580
|
*/
|
|
981
|
-
private processEdgeRow(plan: EdgePlan, row: string[],
|
|
982
|
-
const { report
|
|
1581
|
+
private processEdgeRow(plan: EdgePlan, row: string[], cellQuoted: readonly boolean[], line: number): void {
|
|
1582
|
+
const { report } = this.state;
|
|
983
1583
|
const { counts } = report;
|
|
1584
|
+
const quoted = completeRow(plan, row, cellQuoted, report, line);
|
|
984
1585
|
if (row.length !== plan.width) {
|
|
985
|
-
|
|
986
|
-
"validation-error",
|
|
987
|
-
FIELD_COUNT_CODE,
|
|
988
|
-
`line ${line}: ${row.length} field(s), the header has ${plan.width}`,
|
|
989
|
-
{ line },
|
|
990
|
-
);
|
|
1586
|
+
this.reportRowWidth(plan, row, line);
|
|
991
1587
|
counts.skippedEdges++;
|
|
992
1588
|
return;
|
|
993
1589
|
}
|
|
@@ -995,64 +1591,36 @@ class TableReader {
|
|
|
995
1591
|
const targetText = row[plan.target];
|
|
996
1592
|
const sourceMissing = isUnset(sourceText, quoted[plan.source]);
|
|
997
1593
|
if (sourceMissing || isUnset(targetText, quoted[plan.target])) {
|
|
998
|
-
report.error(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
`line ${line}: blank ${sourceMissing ? "source" : "target"} cell`,
|
|
1002
|
-
{ line },
|
|
1003
|
-
);
|
|
1594
|
+
report.error("missing-value", MISSING_ENDPOINT_CODE, `blank ${sourceMissing ? "source" : "target"} cell`, {
|
|
1595
|
+
line,
|
|
1596
|
+
});
|
|
1004
1597
|
counts.skippedEdges++;
|
|
1005
1598
|
return;
|
|
1006
1599
|
}
|
|
1007
|
-
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
report.error(
|
|
1012
|
-
"validation-error",
|
|
1013
|
-
BAD_TYPE_CODE,
|
|
1014
|
-
`line ${line}: Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`,
|
|
1015
|
-
{ line },
|
|
1016
|
-
);
|
|
1017
|
-
counts.skippedEdges++;
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (parsed !== undefined) {
|
|
1021
|
-
kind = parsed;
|
|
1022
|
-
}
|
|
1600
|
+
const kind = this.rowKind(plan, row, line);
|
|
1601
|
+
if (kind === null) {
|
|
1602
|
+
counts.skippedEdges++;
|
|
1603
|
+
return;
|
|
1023
1604
|
}
|
|
1024
1605
|
const { where } = this;
|
|
1025
1606
|
where.line = line;
|
|
1026
1607
|
where.element = null;
|
|
1027
1608
|
const idText = plan.id >= 0 && !isUnset(row[plan.id], quoted[plan.id]) ? row[plan.id] : null;
|
|
1028
|
-
if (idText !== null) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
this.edgeIds.add(idText);
|
|
1609
|
+
if (idText !== null && this.edgeIds.has(idText)) {
|
|
1610
|
+
report.error(
|
|
1611
|
+
"validation-error",
|
|
1612
|
+
DUPLICATE_EDGE_ID_CODE,
|
|
1613
|
+
`edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`,
|
|
1614
|
+
{ line, element: idText },
|
|
1615
|
+
);
|
|
1616
|
+
counts.skippedEdges++;
|
|
1617
|
+
return;
|
|
1040
1618
|
}
|
|
1619
|
+
this.checkPadded(sourceText, quoted[plan.source], line);
|
|
1620
|
+
this.checkPadded(targetText, quoted[plan.target], line);
|
|
1041
1621
|
let edge: number;
|
|
1042
1622
|
try {
|
|
1043
|
-
|
|
1044
|
-
const target = this.coerce(targetText);
|
|
1045
|
-
const weight = plan.weight >= 0 ? parseWeightText(row[plan.weight]) : undefined;
|
|
1046
|
-
if (!this.state.headerSet) {
|
|
1047
|
-
this.state.headerSet = true;
|
|
1048
|
-
resolver.setHeader(kind !== "undirected", where);
|
|
1049
|
-
}
|
|
1050
|
-
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
1051
|
-
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
1052
|
-
const before = sink.edgeCount;
|
|
1053
|
-
edge = resolver.addEdge(source, target, kind, weight, where);
|
|
1054
|
-
counts.edges += sink.edgeCount - before;
|
|
1055
|
-
counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
1623
|
+
edge = this.addEdge(sourceText, targetText, kind, plan.weight >= 0 ? row[plan.weight] : null);
|
|
1056
1624
|
} catch (err) {
|
|
1057
1625
|
where.element = idText ?? `${sourceText}->${targetText}`;
|
|
1058
1626
|
report.recordError(err, where);
|
|
@@ -1060,41 +1628,142 @@ class TableReader {
|
|
|
1060
1628
|
return;
|
|
1061
1629
|
}
|
|
1062
1630
|
if (idText !== null) {
|
|
1631
|
+
// only an imported edge holds its id: a skipped row leaves it free
|
|
1632
|
+
this.edgeIds.add(idText);
|
|
1063
1633
|
this.writeRole(this.idHandle, "edge", edge, idText, plan.names[plan.id], line);
|
|
1064
1634
|
}
|
|
1635
|
+
this.warnTypeHint(plan, row, line);
|
|
1065
1636
|
if (plan.label >= 0 && !isUnset(row[plan.label], quoted[plan.label])) {
|
|
1066
1637
|
this.writeRole(this.labelHandle, "edge", edge, row[plan.label], plan.names[plan.label], line);
|
|
1067
1638
|
}
|
|
1068
1639
|
this.writeAttributes(plan, row, quoted, edge, line);
|
|
1069
1640
|
}
|
|
1070
1641
|
|
|
1642
|
+
/**
|
|
1643
|
+
* Warn, once, when a column that is not the type column holds a direction word.
|
|
1644
|
+
* @param plan - the edge plan
|
|
1645
|
+
* @param row - the cells
|
|
1646
|
+
* @param line - the row's line
|
|
1647
|
+
*/
|
|
1648
|
+
private warnTypeHint(plan: EdgePlan, row: readonly string[], line: number): void {
|
|
1649
|
+
if (plan.typeHint < 0 || typeof parseKind(row[plan.typeHint]) !== "string") {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
this.state.report.warnOnce(
|
|
1653
|
+
"unsupported",
|
|
1654
|
+
TYPE_COLUMN_IGNORED_CODE,
|
|
1655
|
+
`column ${JSON.stringify(plan.names[plan.typeHint])} holds the direction word ${JSON.stringify(row[plan.typeHint])} but is a plain attribute; the per-row direction is read from an exact "Type" column beside "Source" and "Target", or from the typeColumn option`,
|
|
1656
|
+
{ line, element: plan.names[plan.typeHint] },
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Report a row with the wrong number of cells. The first row too long for an auto table says
|
|
1662
|
+
* what an adjacency list needs, since one reads this way.
|
|
1663
|
+
* @param plan - the edge plan
|
|
1664
|
+
* @param row - the cells
|
|
1665
|
+
* @param line - the row's line
|
|
1666
|
+
*/
|
|
1667
|
+
private reportRowWidth(plan: EdgePlan, row: readonly string[], line: number): void {
|
|
1668
|
+
const hint =
|
|
1669
|
+
this.state.csv.table === "auto" && row.length > plan.width && !this.adjacencyHinted
|
|
1670
|
+
? '; if each line is a node followed by its neighbors, pass table: "adjacency"'
|
|
1671
|
+
: "";
|
|
1672
|
+
this.adjacencyHinted ||= hint !== "";
|
|
1673
|
+
this.state.report.error(
|
|
1674
|
+
"validation-error",
|
|
1675
|
+
FIELD_COUNT_CODE,
|
|
1676
|
+
`${row.length} field${plural(row.length)}, expected ${plan.width}${hint}`,
|
|
1677
|
+
{ line },
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* The direction of an edge row: its Type cell, else the file's or the default direction.
|
|
1683
|
+
* @param plan - the edge plan
|
|
1684
|
+
* @param row - the cells
|
|
1685
|
+
* @param line - the row's line
|
|
1686
|
+
* @returns the kind, or null when the Type cell is not a direction (reported)
|
|
1687
|
+
*/
|
|
1688
|
+
private rowKind(plan: EdgePlan, row: readonly string[], line: number): EdgeKind | null {
|
|
1689
|
+
const directed = this.state.commentDirected ?? this.state.common.defaultDirected;
|
|
1690
|
+
const kind: EdgeKind = directed ? "directed" : "undirected";
|
|
1691
|
+
if (plan.type < 0) {
|
|
1692
|
+
return kind;
|
|
1693
|
+
}
|
|
1694
|
+
const parsed = parseKind(row[plan.type]);
|
|
1695
|
+
if (parsed === null) {
|
|
1696
|
+
this.state.report.error(
|
|
1697
|
+
"validation-error",
|
|
1698
|
+
BAD_TYPE_CODE,
|
|
1699
|
+
`Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`,
|
|
1700
|
+
{ line },
|
|
1701
|
+
);
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
return parsed ?? kind;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Add an edge row's edge, counting the edge and the nodes it brings in.
|
|
1709
|
+
* @param sourceText - the source cell
|
|
1710
|
+
* @param targetText - the target cell
|
|
1711
|
+
* @param kind - the direction
|
|
1712
|
+
* @param weightText - the weight cell, or null
|
|
1713
|
+
* @returns the edge's index
|
|
1714
|
+
*/
|
|
1715
|
+
private addEdge(sourceText: string, targetText: string, kind: EdgeKind, weightText: string | null): number {
|
|
1716
|
+
const { report, sink, resolver } = this.state;
|
|
1717
|
+
const { where } = this;
|
|
1718
|
+
const source = this.coerce(sourceText);
|
|
1719
|
+
const target = this.coerce(targetText);
|
|
1720
|
+
const weight = weightText === null ? undefined : parseWeightText(weightText, report);
|
|
1721
|
+
if (!this.state.headerSet) {
|
|
1722
|
+
this.state.headerSet = true;
|
|
1723
|
+
resolver.setHeader(kind !== "undirected", where);
|
|
1724
|
+
}
|
|
1725
|
+
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
1726
|
+
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
1727
|
+
const before = sink.edgeCount;
|
|
1728
|
+
const edge = resolver.addEdge(source, target, kind, weight, where);
|
|
1729
|
+
report.counts.edges += sink.edgeCount - before;
|
|
1730
|
+
report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
1731
|
+
return edge;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1071
1734
|
/**
|
|
1072
1735
|
* Push one node row: the id, then the label and attribute cells.
|
|
1073
1736
|
* @param plan - the node plan
|
|
1074
1737
|
* @param row - the cells
|
|
1075
|
-
* @param
|
|
1738
|
+
* @param cellQuoted - whether each cell was quoted
|
|
1076
1739
|
* @param line - the row's line
|
|
1077
1740
|
*/
|
|
1078
|
-
private processNodeRow(plan: NodePlan, row: string[],
|
|
1741
|
+
private processNodeRow(plan: NodePlan, row: string[], cellQuoted: readonly boolean[], line: number): void {
|
|
1079
1742
|
const { report, sink } = this.state;
|
|
1080
1743
|
const { counts } = report;
|
|
1081
1744
|
const ordinal = this.nodeOrdinal++;
|
|
1745
|
+
const quoted = completeRow(plan, row, cellQuoted, report, line);
|
|
1082
1746
|
if (row.length !== plan.width) {
|
|
1083
1747
|
report.error(
|
|
1084
1748
|
"validation-error",
|
|
1085
1749
|
FIELD_COUNT_CODE,
|
|
1086
|
-
|
|
1087
|
-
{
|
|
1750
|
+
`${row.length} field${plural(row.length)}, expected ${plan.width}`,
|
|
1751
|
+
{
|
|
1752
|
+
line,
|
|
1753
|
+
},
|
|
1088
1754
|
);
|
|
1089
1755
|
counts.skippedNodes++;
|
|
1090
1756
|
return;
|
|
1091
1757
|
}
|
|
1092
1758
|
const idText = plan.id >= 0 ? row[plan.id] : String(ordinal);
|
|
1093
1759
|
if (plan.id >= 0 && isUnset(idText, quoted[plan.id])) {
|
|
1094
|
-
report.error("missing-value", MISSING_ID_CODE, `
|
|
1760
|
+
report.error("missing-value", MISSING_ID_CODE, `blank id cell`, { line });
|
|
1095
1761
|
counts.skippedNodes++;
|
|
1096
1762
|
return;
|
|
1097
1763
|
}
|
|
1764
|
+
if (plan.id >= 0) {
|
|
1765
|
+
this.checkPadded(idText, quoted[plan.id], line);
|
|
1766
|
+
}
|
|
1098
1767
|
const { where } = this;
|
|
1099
1768
|
where.line = line;
|
|
1100
1769
|
where.element = idText;
|
|
@@ -1105,7 +1774,7 @@ class TableReader {
|
|
|
1105
1774
|
report.warning(
|
|
1106
1775
|
"merged",
|
|
1107
1776
|
DUPLICATE_NODE_CODE,
|
|
1108
|
-
`
|
|
1777
|
+
`node ${JSON.stringify(id)} already exists; its attributes are overwritten`,
|
|
1109
1778
|
where,
|
|
1110
1779
|
);
|
|
1111
1780
|
} else {
|
|
@@ -1123,6 +1792,24 @@ class TableReader {
|
|
|
1123
1792
|
this.writeAttributes(plan, row, quoted, index, line);
|
|
1124
1793
|
}
|
|
1125
1794
|
|
|
1795
|
+
/**
|
|
1796
|
+
* Warn once per import about an unquoted id cell with leading or trailing whitespace, which is
|
|
1797
|
+
* a different id from the bare text (RFC 4180 keeps spaces).
|
|
1798
|
+
* @param text - the cell text
|
|
1799
|
+
* @param quoted - whether the cell was quoted
|
|
1800
|
+
* @param line - the row's line
|
|
1801
|
+
*/
|
|
1802
|
+
private checkPadded(text: string, quoted: boolean | undefined, line: number): void {
|
|
1803
|
+
if (isPadded(text, quoted)) {
|
|
1804
|
+
this.state.report.warnOnce(
|
|
1805
|
+
"validation-error",
|
|
1806
|
+
PADDED_ID_CODE,
|
|
1807
|
+
`id ${JSON.stringify(text)} has leading or trailing whitespace and is kept as written, a different id from the trimmed text`,
|
|
1808
|
+
{ line, element: text },
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1126
1813
|
/**
|
|
1127
1814
|
* Coerce an id cell, reporting a merge under `ids: "number"`.
|
|
1128
1815
|
* @param text - the cell text
|
|
@@ -1204,37 +1891,206 @@ class TableReader {
|
|
|
1204
1891
|
}
|
|
1205
1892
|
}
|
|
1206
1893
|
|
|
1894
|
+
/**
|
|
1895
|
+
* Whether a headerless table is a triple list whose third column is text (`a b knows`): every row
|
|
1896
|
+
* read so far has exactly three cells and a third cell that is not a number. That column is then an
|
|
1897
|
+
* attribute rather than the positional weight. A wider or ragged table keeps the positional weight,
|
|
1898
|
+
* so prose or an adjacency list read as edges still fails per row instead of importing.
|
|
1899
|
+
* @param width - the number of columns
|
|
1900
|
+
* @param sample - the data rows read so far
|
|
1901
|
+
* @returns true for a text triple list
|
|
1902
|
+
*/
|
|
1903
|
+
function isTextTriple(width: number, sample: readonly (readonly string[])[]): boolean {
|
|
1904
|
+
return (
|
|
1905
|
+
width === 3 &&
|
|
1906
|
+
sample.length > 0 &&
|
|
1907
|
+
sample.every((row) => row.length === 3 && !isBlank(row[2]) && numberOrNull(row[2]) === null)
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* Whether the caller fixed how the input is read (a delimiter, the header, the table kind or a
|
|
1913
|
+
* column): it is then a table, and its first line is never refused as another format.
|
|
1914
|
+
* @param csv - the resolved options
|
|
1915
|
+
* @returns true when any of them is explicit
|
|
1916
|
+
*/
|
|
1917
|
+
function hasExplicitDialect(csv: ResolvedCsvOptions): boolean {
|
|
1918
|
+
return (
|
|
1919
|
+
csv.delimiter !== null ||
|
|
1920
|
+
csv.header !== "auto" ||
|
|
1921
|
+
csv.table !== "auto" ||
|
|
1922
|
+
csv.sourceColumn !== null ||
|
|
1923
|
+
csv.targetColumn !== null ||
|
|
1924
|
+
csv.idColumn !== null ||
|
|
1925
|
+
(csv.typeColumn !== undefined && csv.typeColumn !== null)
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* Fit a row whose width differs from the header's by one empty last cell: a row that omits the
|
|
1931
|
+
* empty last cell of a header ending in a delimiter gets an unset cell appended, and a row ending
|
|
1932
|
+
* in a delimiter the header does not have (Excel, pandas) loses that empty cell, reported once.
|
|
1933
|
+
* @param plan - the plan
|
|
1934
|
+
* @param row - the cells (changed in place)
|
|
1935
|
+
* @param quoted - whether each cell was quoted
|
|
1936
|
+
* @param report - the report
|
|
1937
|
+
* @param line - the row's line
|
|
1938
|
+
* @returns the quoted flags of the fitted row
|
|
1939
|
+
*/
|
|
1940
|
+
function completeRow(
|
|
1941
|
+
plan: EdgePlan | NodePlan,
|
|
1942
|
+
row: string[],
|
|
1943
|
+
quoted: readonly boolean[],
|
|
1944
|
+
report: ImportReportBuilder,
|
|
1945
|
+
line: number,
|
|
1946
|
+
): readonly boolean[] {
|
|
1947
|
+
if (row.length === plan.width + 1 && isUnset(row[plan.width], quoted[plan.width])) {
|
|
1948
|
+
row.pop();
|
|
1949
|
+
report.warnOnce(
|
|
1950
|
+
"validation-error",
|
|
1951
|
+
TRAILING_DELIMITER_CODE,
|
|
1952
|
+
`the row ends in a delimiter the header does not have; the empty last cell is dropped`,
|
|
1953
|
+
{ line },
|
|
1954
|
+
);
|
|
1955
|
+
return quoted;
|
|
1956
|
+
}
|
|
1957
|
+
if (!plan.optionalLast || row.length !== plan.width - 1) {
|
|
1958
|
+
return quoted;
|
|
1959
|
+
}
|
|
1960
|
+
const flags = quoted.slice(0, row.length);
|
|
1961
|
+
row.push("");
|
|
1962
|
+
flags.push(false);
|
|
1963
|
+
return flags;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1207
1966
|
const HEAD_BYTES = 4096;
|
|
1208
|
-
const OTHER_FORMAT = /^\s*(<|[[{]|(strict\s+)?(di)?graph(\s+\S+)?\s*\{|\*vertices|creator\b|graph\s*\[)/i;
|
|
1209
1967
|
|
|
1210
1968
|
/**
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1969
|
+
* The openings of the formats a CSV reader would otherwise split into rows of text: an XML / HTML
|
|
1970
|
+
* tag or declaration (not an IRI such as `<http://a>`, nor a bracketed id such as `<alice smith>`
|
|
1971
|
+
* or `<a>,b`: a tag needs an attribute, or another tag after it), a JSON object (`{"` or `{}`) or
|
|
1972
|
+
* array, a DOT graph, a Pajek section, GML's `Creator "..."` or `graph [`. Matched against the
|
|
1973
|
+
* start of the input.
|
|
1974
|
+
*/
|
|
1975
|
+
/**
|
|
1976
|
+
* An endpoint column: the one the option names, else (with a header) the first with a known
|
|
1977
|
+
* endpoint name, else (without one) the column at its position in a headerless edge list.
|
|
1978
|
+
* @param names - the column names
|
|
1979
|
+
* @param header - whether the file has a header row
|
|
1980
|
+
* @param option - the sourceColumn / targetColumn option, or null
|
|
1981
|
+
* @param optionName - the option's name, for its error
|
|
1982
|
+
* @param known - the endpoint names to look for
|
|
1983
|
+
* @param position - the column of this endpoint in a headerless edge list
|
|
1984
|
+
* @returns the column index, or -1
|
|
1985
|
+
*/
|
|
1986
|
+
function endpointColumn(
|
|
1987
|
+
names: readonly string[],
|
|
1988
|
+
header: boolean,
|
|
1989
|
+
option: CsvColumnRef | null,
|
|
1990
|
+
optionName: string,
|
|
1991
|
+
known: readonly string[],
|
|
1992
|
+
position: number,
|
|
1993
|
+
): number {
|
|
1994
|
+
if (option !== null) {
|
|
1995
|
+
return resolveColumnRef(names, option, optionName);
|
|
1996
|
+
}
|
|
1997
|
+
if (header) {
|
|
1998
|
+
return findColumn(names, known);
|
|
1999
|
+
}
|
|
2000
|
+
return names.length >= 2 ? position : -1;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
const TAG_NAME = String.raw`<[a-z_][\w.-]*(?::[a-z_][\w.-]*)?`;
|
|
2004
|
+
const OTHER_FORMATS: readonly RegExp[] = [
|
|
2005
|
+
/^\s*<(?:\?xml|!doctype|!--)/i, // an XML declaration, a doctype or a comment
|
|
2006
|
+
new RegExp(String.raw`^\s*${TAG_NAME}\s*\/?>\s*(?:[^,;|\s]*<|$)`, "i"), // a tag, then another tag
|
|
2007
|
+
new RegExp(String.raw`^\s*${TAG_NAME}\s+[a-z_][\w.:-]*\s*=`, "i"), // a tag with an attribute
|
|
2008
|
+
/^\s*\/[*/]/, // a C comment (DOT)
|
|
2009
|
+
/^\s*\{\s*(?:["}]|$)/, // a JSON object
|
|
2010
|
+
/^\s*\[\s*(?:[[{"\]\d-]|$)/, // a JSON array
|
|
2011
|
+
/^\s*(?:strict\s+)?(?:di)?graph(?:\s+\S+)?\s*\{/i, // a DOT graph
|
|
2012
|
+
/^\s*\*(?:vertices|network|arcs|edges)\b/i, // a Pajek section
|
|
2013
|
+
/^\s*(?:creator\s+"|graph\s*\[)/i, // GML
|
|
2014
|
+
];
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Sniff confidence for the registry: 0 for XML, JSON, GML, DOT (also a leading C comment) and Pajek
|
|
2018
|
+
* openings after the leading `#` / `%` comment lines the reader skips, and 0 for a head that is
|
|
2019
|
+
* only such comments (the comments of another format, too long to see past); otherwise a
|
|
2020
|
+
* delimited first row with endpoint headers is 0.9 (even when a later row has the wrong width, which
|
|
2021
|
+
* the importer reports), with an id header 0.6, and other consistently delimited rows 0.3, except
|
|
2022
|
+
* rows of more than three space-separated words, which is plain text such as a sentence, not a
|
|
2023
|
+
* headerless edge list (source, target, weight), so it is 0.
|
|
1214
2024
|
* @param head - the first bytes of the input
|
|
1215
2025
|
* @returns a confidence in 0..1
|
|
1216
2026
|
*/
|
|
1217
2027
|
function sniff(head: Uint8Array): number {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
2028
|
+
let label = "utf-8";
|
|
2029
|
+
if (head[0] === 0xff && head[1] === 0xfe) {
|
|
2030
|
+
label = "utf-16le";
|
|
2031
|
+
} else if (head[0] === 0xfe && head[1] === 0xff) {
|
|
2032
|
+
label = "utf-16be";
|
|
2033
|
+
}
|
|
2034
|
+
// the decoder drops the BOM it was chosen by; the reader skips leading comment lines
|
|
2035
|
+
const body = stripLeadingComments(new TextDecoder(label).decode(head.subarray(0, HEAD_BYTES)), COMMENT_CHARS);
|
|
2036
|
+
if (body.trim().length === 0 || OTHER_FORMATS.some((r) => r.test(body))) {
|
|
1221
2037
|
return 0;
|
|
1222
2038
|
}
|
|
1223
2039
|
const newline = sniffNewline(body);
|
|
1224
|
-
const delimiter = sniffDelimiter(body, newline);
|
|
2040
|
+
const delimiter = sniffDelimiter(body, newline, DELIMITER_CANDIDATES, '"', false, true);
|
|
1225
2041
|
if (delimiter === null) {
|
|
1226
|
-
|
|
2042
|
+
// a header naming both endpoints is evidence enough; a short or long row later is the
|
|
2043
|
+
// importer's to report, not a reason to refuse the file
|
|
2044
|
+
const first = body.split(newline, 1)[0];
|
|
2045
|
+
return DELIMITER_CANDIDATES.some((d) => hasEndpointHeader(first, d)) ? 0.9 : 0;
|
|
1227
2046
|
}
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
const names = headerNames(
|
|
2047
|
+
// the first record, honoring quotes (R and pandas quote every header name)
|
|
2048
|
+
const records = splitRecords(body, delimiter, '"', 2, false, true) ?? [];
|
|
2049
|
+
const names = headerNames(records[0] ?? []);
|
|
1231
2050
|
if (findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
|
|
1232
2051
|
return 0.9;
|
|
1233
2052
|
}
|
|
1234
|
-
|
|
2053
|
+
// a space-split row naming an id is as likely a line of text ("1 First node") as a header
|
|
2054
|
+
if (delimiter !== " " && findColumn(names, ID_NAMES) >= 0) {
|
|
1235
2055
|
return 0.6;
|
|
1236
2056
|
}
|
|
1237
|
-
|
|
2057
|
+
if (delimiter === " " && names.length > 3) {
|
|
2058
|
+
return 0;
|
|
2059
|
+
}
|
|
2060
|
+
return looksLikeProse(body, newline, delimiter) ? 0 : 0.3;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/** A cell that reads as part of a sentence: words with a sentence end after a letter (`Monday.`, `world. Thanks`). */
|
|
2064
|
+
const SENTENCE = /[A-Za-z]{2}[.!?](?:\s|$)/;
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Whether headerless delimited text is more likely a line of prose than an edge list: a single line
|
|
2068
|
+
* ("hello world", "Dear team, the meeting is on Monday."), or a cell of more than one word that
|
|
2069
|
+
* ends a sentence. An edge list a reader means to load has more than one row and ids, not sentences;
|
|
2070
|
+
* such input is still read as CSV when the caller names the format.
|
|
2071
|
+
* @param body - the head, decoded, without its leading comments
|
|
2072
|
+
* @param newline - the head's newline
|
|
2073
|
+
* @param delimiter - the sniffed delimiter
|
|
2074
|
+
* @returns true for prose
|
|
2075
|
+
*/
|
|
2076
|
+
function looksLikeProse(body: string, newline: string, delimiter: string): boolean {
|
|
2077
|
+
const lines = body.split(newline).filter((l) => l.trim() !== "");
|
|
2078
|
+
if (lines.length < 2) {
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
const rows = splitRecords(body, delimiter, '"', 8, false, true) ?? [];
|
|
2082
|
+
return rows.some((row) => row.some((cell) => /\S\s+\S/.test(cell.trim()) && SENTENCE.test(cell.trim())));
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Whether one line, split on a delimiter, names a source and a target column.
|
|
2087
|
+
* @param line - the first line of the input
|
|
2088
|
+
* @param delimiter - the delimiter to split on
|
|
2089
|
+
* @returns true for an edge table header
|
|
2090
|
+
*/
|
|
2091
|
+
function hasEndpointHeader(line: string, delimiter: string): boolean {
|
|
2092
|
+
const names = headerNames(splitRecords(line, delimiter, '"', 1, false, true)?.[0] ?? []);
|
|
2093
|
+
return names.length > 1 && findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0;
|
|
1238
2094
|
}
|
|
1239
2095
|
|
|
1240
2096
|
/**
|
|
@@ -1268,6 +2124,7 @@ async function importCsv(
|
|
|
1268
2124
|
resolver: new DirectionResolver(sink, report, common.onMixedDirection),
|
|
1269
2125
|
headerSet: false,
|
|
1270
2126
|
commentDirected: null,
|
|
2127
|
+
commentText: "",
|
|
1271
2128
|
};
|
|
1272
2129
|
if (csv.nodes !== null) {
|
|
1273
2130
|
await new TableReader(state, csv.nodes, "nodes", false).read();
|
|
@@ -1277,18 +2134,49 @@ async function importCsv(
|
|
|
1277
2134
|
report.warning(
|
|
1278
2135
|
"coercion",
|
|
1279
2136
|
ID_MERGED_CODE,
|
|
1280
|
-
`${state.coercer.mergeCount} id cell(
|
|
2137
|
+
`${state.coercer.mergeCount} id cell${plural(state.coercer.mergeCount)} merged into ids other cells already produced under ids: "number"`,
|
|
1281
2138
|
);
|
|
1282
2139
|
}
|
|
1283
2140
|
throwIfAborted(common.signal);
|
|
1284
2141
|
return report.finish();
|
|
1285
2142
|
}
|
|
1286
2143
|
|
|
1287
|
-
/**
|
|
2144
|
+
/**
|
|
2145
|
+
* The CSV / TSV importer plugin (subpath `@graphty/graph-io/csv`).
|
|
2146
|
+
* @category Built-in formats
|
|
2147
|
+
*/
|
|
1288
2148
|
export const csvImporter: GraphImporter<CsvImportOptions> = Object.freeze({
|
|
1289
2149
|
format: "csv",
|
|
2150
|
+
options: Object.freeze([
|
|
2151
|
+
"decimal",
|
|
2152
|
+
"delimiter",
|
|
2153
|
+
"header",
|
|
2154
|
+
"idColumn",
|
|
2155
|
+
"labelColumn",
|
|
2156
|
+
"nodes",
|
|
2157
|
+
"rowNumberIds",
|
|
2158
|
+
"sourceColumn",
|
|
2159
|
+
"table",
|
|
2160
|
+
"targetColumn",
|
|
2161
|
+
"typeColumn",
|
|
2162
|
+
]),
|
|
1290
2163
|
extensions: Object.freeze([".csv", ".tsv", ".edges", ".edgelist"]),
|
|
1291
2164
|
mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values", "text/plain"]),
|
|
1292
2165
|
sniff,
|
|
1293
2166
|
import: importCsv,
|
|
1294
2167
|
});
|
|
2168
|
+
|
|
2169
|
+
/**
|
|
2170
|
+
* The `nodes` option as an input: a Blob or File is read through its stream. Checked by shape, so a
|
|
2171
|
+
* Blob from another realm or from Node's `fs.openAsBlob()` counts too.
|
|
2172
|
+
* @param value - the option value
|
|
2173
|
+
* @returns the input, or null when there is no node table
|
|
2174
|
+
*/
|
|
2175
|
+
function nodesInput(value: ImportInput | Blob | undefined): ImportInput | null {
|
|
2176
|
+
if (value === undefined) {
|
|
2177
|
+
return null;
|
|
2178
|
+
}
|
|
2179
|
+
const object = value as { stream?: unknown; size?: unknown };
|
|
2180
|
+
const blob = typeof value === "object" && typeof object.stream === "function" && typeof object.size === "number";
|
|
2181
|
+
return blob ? (value as Blob).stream() : (value as ImportInput);
|
|
2182
|
+
}
|