@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
package/dist/csv.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-
|
|
1
|
+
import { b as plural, bV as DICT_SAMPLE_ROWS, ac as DictHeuristic, R as ROLE_TAKEN_CODE$1, I as ID_MERGED_CODE$1, m as DUPLICATE_EDGE_ID_CODE$1, n as DUPLICATE_NODE_CODE$1, t as MISSING_ID_CODE$1, s as MISSING_ENDPOINT_CODE$1, E as EMPTY_INPUT_CODE$1, al as resolveImportOptions, a8 as ImportReportBuilder, ao as reportSinkOptions, an as reportUnusedOptions, aq as DirectionResolver, ar as IdCoercer, am as throwIfAborted, O as OPTION_IGNORED_CODE, aL as parseWeightText, a0 as uniqueColumnName, ay as RENAMED_CODE, a1 as declareResolved, c as capabilities, L as LOSS, a as checkCapabilities, r as resolveExportOptions, p as pairFolding, bE as countMixedEdges, $ as agree, e as explicitWeights, b$ as otherFormatDialect, bT as joinListText, aG as formatInteger, aF as formatDecimal, aH as canonicalId, M as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, d as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, C as COLUMN_RENAMED_CODE, w as UNKNOWN_ENCODING_CODE, x as ENCODING_FALLBACK_CODE, y as INVALID_ENCODING_CODE, z as INVALID_UTF8_CODE, P as PRECISION_CODE, F as ELEMENT_ISSUE, G as INPUT_ISSUE } from "./chunks/export-CHd4tQE2.js";
|
|
2
|
+
import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-DzXjAiTl.js";
|
|
3
3
|
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { c as capabilities, L as LOSS, a as checkCapabilities, d as countMixedEdges } from "./chunks/export-Bh60Dv-n.js";
|
|
8
|
-
import { j as joinText, e as encodeChunks } from "./chunks/writer-C7flM-Ih.js";
|
|
4
|
+
import { s as stripLeadingComments, a as sniffNewline, b as sniffDelimiter, D as DELIMITER_CANDIDATES, c as checkRecordSyntax, C as CsvRecordReader, d as splitRecords, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-CdlvL4RO.js";
|
|
5
|
+
import { o as quoteCsvCell } from "./chunks/escape-D1HPTUrP.js";
|
|
6
|
+
import { j as joinText, e as encodeChunks } from "./chunks/writer-DxjgHwSe.js";
|
|
9
7
|
const SOURCE_NAMES = Object.freeze([
|
|
10
8
|
"source",
|
|
11
9
|
"src",
|
|
@@ -61,7 +59,7 @@ function resolveColumnRef(names, ref, option) {
|
|
|
61
59
|
if (!Number.isInteger(ref) || ref < 0 || ref >= names.length) {
|
|
62
60
|
throw new GraphFormatError(
|
|
63
61
|
"E_UNSUPPORTED",
|
|
64
|
-
`option ${option}: column ${ref} does not exist (the file has ${names.length} column(
|
|
62
|
+
`option ${option}: column ${ref} does not exist (the file has ${names.length} column${plural(names.length)})`,
|
|
65
63
|
{ option, found: ref, columns: names.length }
|
|
66
64
|
);
|
|
67
65
|
}
|
|
@@ -79,15 +77,24 @@ function resolveColumnRef(names, ref, option) {
|
|
|
79
77
|
}
|
|
80
78
|
function looksLikeHeader(first, second) {
|
|
81
79
|
let allText = true;
|
|
80
|
+
let marker = false;
|
|
82
81
|
for (const cell of first) {
|
|
83
82
|
const text = cell.trim();
|
|
84
83
|
if (HEADER_MARKERS.has(text.toLowerCase())) {
|
|
85
|
-
|
|
84
|
+
marker = true;
|
|
86
85
|
}
|
|
87
86
|
if (text.length === 0 || inferTextDtype(text) !== "string") {
|
|
88
87
|
allText = false;
|
|
89
88
|
}
|
|
90
89
|
}
|
|
90
|
+
if (marker) {
|
|
91
|
+
const names = headerNames(first);
|
|
92
|
+
if (second === null || findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const chained = first.length === 2 && second.length >= 1 && first[1].trim() === second[0].trim();
|
|
96
|
+
return !chained || first[1].trim().length === 0;
|
|
97
|
+
}
|
|
91
98
|
if (!allText || second === null) {
|
|
92
99
|
return false;
|
|
93
100
|
}
|
|
@@ -280,6 +287,17 @@ const ID_MERGED_CODE = ID_MERGED_CODE$1;
|
|
|
280
287
|
const COLUMN_MISSING_CODE = "W_CSV_COLUMN_MISSING";
|
|
281
288
|
const ROLE_TAKEN_CODE = ROLE_TAKEN_CODE$1;
|
|
282
289
|
const DUPLICATE_EDGE_ID_CODE = DUPLICATE_EDGE_ID_CODE$1;
|
|
290
|
+
const OTHER_FORMAT_CODE = "E_CSV_OTHER_FORMAT";
|
|
291
|
+
const STRAY_QUOTE_CODE = "W_CSV_STRAY_QUOTE";
|
|
292
|
+
const COMMENT_DIRECTION_CODE = "W_CSV_COMMENT_DIRECTION";
|
|
293
|
+
const TYPE_COLUMN_IGNORED_CODE = "W_CSV_TYPE_COLUMN_IGNORED";
|
|
294
|
+
const TRAILING_HEADER_DELIMITER_CODE = "W_CSV_TRAILING_HEADER_DELIMITER";
|
|
295
|
+
const SINGLE_COLUMN_CODE = "W_CSV_SINGLE_COLUMN";
|
|
296
|
+
const AMBIGUOUS_COLUMN_CODE = "W_CSV_AMBIGUOUS_COLUMN";
|
|
297
|
+
const PADDED_ID_CODE = "W_CSV_PADDED_ID";
|
|
298
|
+
const COMMENT_LIKE_RECORD_CODE = "W_CSV_COMMENT_LIKE_RECORD";
|
|
299
|
+
const TRAILING_DELIMITER_CODE = "W_CSV_TRAILING_DELIMITER";
|
|
300
|
+
const WEIGHT_AS_ATTRIBUTE_CODE = "W_CSV_WEIGHT_AS_ATTRIBUTE";
|
|
283
301
|
const TABLE_MODES = /* @__PURE__ */ new Set(["edges", "nodes", "adjacency", "auto"]);
|
|
284
302
|
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
285
303
|
"ids",
|
|
@@ -298,20 +316,24 @@ const USED_OPTIONS_WITH_NODES = /* @__PURE__ */ new Set([
|
|
|
298
316
|
...USED_OPTIONS,
|
|
299
317
|
"nodeIdFrom"
|
|
300
318
|
]);
|
|
301
|
-
const BAD_DELIMITERS = /* @__PURE__ */ new Set(['"', "\n", "\r"]);
|
|
302
319
|
const COMMENT_CHARS = Object.freeze(["#", "%"]);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
320
|
+
const DECIMAL_COMMA = /^\s*[+-]?\d+,\d+(?:[eE][+-]?\d+)?\s*$/;
|
|
321
|
+
function commentDirection(comment) {
|
|
322
|
+
const text = comment.slice(1).trim().toLowerCase();
|
|
323
|
+
if (/^(?:directed graph|asym)\b/.test(text)) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
if (/^(?:undirected graph|sym|bip)\b/.test(text)) {
|
|
327
|
+
return false;
|
|
312
328
|
}
|
|
313
329
|
return null;
|
|
314
330
|
}
|
|
331
|
+
function directionName(directed) {
|
|
332
|
+
return directed ? "directed" : "undirected";
|
|
333
|
+
}
|
|
334
|
+
function isPadded(text, quoted) {
|
|
335
|
+
return quoted !== true && text.trim().length !== text.length;
|
|
336
|
+
}
|
|
315
337
|
function resolveCsvOptions(options) {
|
|
316
338
|
const o = options ?? {};
|
|
317
339
|
if (o.delimiter !== void 0 && (typeof o.delimiter !== "string" || o.delimiter.length === 0)) {
|
|
@@ -320,12 +342,21 @@ function resolveCsvOptions(options) {
|
|
|
320
342
|
found: o.delimiter
|
|
321
343
|
});
|
|
322
344
|
}
|
|
323
|
-
if (o.
|
|
324
|
-
throw new GraphFormatError("E_UNSUPPORTED",
|
|
325
|
-
option: "
|
|
326
|
-
found: o.
|
|
345
|
+
if (o.decimal !== void 0 && o.decimal !== "." && o.decimal !== ",") {
|
|
346
|
+
throw new GraphFormatError("E_UNSUPPORTED", 'option decimal: expected "." or ","', {
|
|
347
|
+
option: "decimal",
|
|
348
|
+
found: o.decimal
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (o.decimal === "," && o.delimiter === ",") {
|
|
352
|
+
throw new GraphFormatError("E_UNSUPPORTED", 'options delimiter and decimal are both ","; pass delimiter: ";"', {
|
|
353
|
+
option: "decimal",
|
|
354
|
+
found: o.decimal
|
|
327
355
|
});
|
|
328
356
|
}
|
|
357
|
+
if (o.delimiter !== void 0) {
|
|
358
|
+
checkRecordSyntax({ delimiter: o.delimiter, quote: '"' });
|
|
359
|
+
}
|
|
329
360
|
if (o.header !== void 0 && o.header !== "auto" && typeof o.header !== "boolean") {
|
|
330
361
|
throw new GraphFormatError("E_UNSUPPORTED", 'option header: expected true, false or "auto"', {
|
|
331
362
|
option: "header",
|
|
@@ -338,14 +369,14 @@ function resolveCsvOptions(options) {
|
|
|
338
369
|
found: o.table
|
|
339
370
|
});
|
|
340
371
|
}
|
|
341
|
-
for (const name of ["sourceColumn", "targetColumn", "idColumn"]) {
|
|
372
|
+
for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"]) {
|
|
342
373
|
checkColumnRef(name, o[name]);
|
|
343
374
|
}
|
|
344
375
|
if (o.typeColumn !== null) {
|
|
345
376
|
checkColumnRef("typeColumn", o.typeColumn);
|
|
346
377
|
}
|
|
347
378
|
if (o.table === "adjacency") {
|
|
348
|
-
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn"]) {
|
|
379
|
+
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"]) {
|
|
349
380
|
if (o[name] !== void 0) {
|
|
350
381
|
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
|
|
351
382
|
option: name,
|
|
@@ -368,14 +399,17 @@ function resolveCsvOptions(options) {
|
|
|
368
399
|
}
|
|
369
400
|
return {
|
|
370
401
|
delimiter: o.delimiter ?? null,
|
|
402
|
+
decimalComma: o.decimal === ",",
|
|
371
403
|
header: o.header ?? "auto",
|
|
372
404
|
table: o.table ?? "auto",
|
|
373
405
|
sourceColumn: o.sourceColumn ?? null,
|
|
374
406
|
targetColumn: o.targetColumn ?? null,
|
|
375
407
|
typeColumn: o.typeColumn,
|
|
376
408
|
idColumn: o.idColumn ?? null,
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
labelColumn: o.labelColumn ?? null,
|
|
410
|
+
nodes: nodesInput(o.nodes),
|
|
411
|
+
rowNumberIds: o.rowNumberIds ?? false,
|
|
412
|
+
explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null
|
|
379
413
|
};
|
|
380
414
|
}
|
|
381
415
|
function checkColumnRef(name, value) {
|
|
@@ -477,6 +511,7 @@ class TableReader {
|
|
|
477
511
|
this.labelHandle = INVALID_INDEX;
|
|
478
512
|
this.dataRows = 0;
|
|
479
513
|
this.nodeOrdinal = 0;
|
|
514
|
+
this.adjacencyHinted = false;
|
|
480
515
|
this.edgeIds = /* @__PURE__ */ new Set();
|
|
481
516
|
this.where = { line: null, element: null };
|
|
482
517
|
this.state = state;
|
|
@@ -484,17 +519,25 @@ class TableReader {
|
|
|
484
519
|
this.rowNumbers = state.csv.rowNumberIds && (kind === "nodes" || kind === "auto" && state.csv.nodes === null);
|
|
485
520
|
const readerOptions = {
|
|
486
521
|
delimiter: state.csv.delimiter,
|
|
522
|
+
// a decimal comma is never the delimiter
|
|
523
|
+
...state.csv.decimalComma ? { candidates: DELIMITER_CANDIDATES.filter((d) => d !== ",") } : {},
|
|
487
524
|
comments: COMMENT_CHARS,
|
|
488
525
|
// field counts say nothing about an adjacency table, whose rows vary in width
|
|
489
526
|
skipQuoteErrors: kind === "adjacency",
|
|
490
527
|
signal: state.common.signal,
|
|
491
528
|
onProgress: progress ? state.common.onProgress : null,
|
|
492
|
-
encoding: state.common.encoding
|
|
529
|
+
encoding: state.common.encoding,
|
|
530
|
+
// an adjacency table has no header: an empty one is the empty graph
|
|
531
|
+
allowEmpty: kind === "adjacency",
|
|
532
|
+
// explicit dialect options say the caller knows the input is a table
|
|
533
|
+
inspect: hasExplicitDialect(state.csv) ? void 0 : (preview, line) => {
|
|
534
|
+
this.refuseOtherFormat(preview, line);
|
|
535
|
+
}
|
|
493
536
|
};
|
|
494
537
|
this.reader = new CsvRecordReader(input, state.report, readerOptions);
|
|
495
538
|
}
|
|
496
539
|
/**
|
|
497
|
-
* Read every row; the reader is closed (and a stream
|
|
540
|
+
* Read every row; the reader is closed (and a stream canceled) when the import aborts midway.
|
|
498
541
|
*/
|
|
499
542
|
async read() {
|
|
500
543
|
const iterator = this.reader[Symbol.asyncIterator]();
|
|
@@ -510,7 +553,10 @@ class TableReader {
|
|
|
510
553
|
*/
|
|
511
554
|
async readRows(iterator) {
|
|
512
555
|
const { report } = this.state;
|
|
513
|
-
|
|
556
|
+
let first = await iterator.next();
|
|
557
|
+
if (!first.done && (this.reader.sepDirective || this.isSepDirective(first.value))) {
|
|
558
|
+
first = await iterator.next();
|
|
559
|
+
}
|
|
514
560
|
if (first.done && this.kind === "adjacency") {
|
|
515
561
|
return;
|
|
516
562
|
}
|
|
@@ -530,7 +576,7 @@ class TableReader {
|
|
|
530
576
|
const secondRow = second.done ? null : second.value;
|
|
531
577
|
const secondLine = this.reader.line;
|
|
532
578
|
const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
|
|
533
|
-
header = looksLikeHeader(firstRow, secondRow);
|
|
579
|
+
header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
|
|
534
580
|
if (!header) {
|
|
535
581
|
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
536
582
|
}
|
|
@@ -543,11 +589,39 @@ class TableReader {
|
|
|
543
589
|
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
544
590
|
}
|
|
545
591
|
}
|
|
592
|
+
if (this.rowNumbers && !header) {
|
|
593
|
+
report.warning(
|
|
594
|
+
"unsupported",
|
|
595
|
+
OPTION_IGNORED_CODE,
|
|
596
|
+
"option rowNumberIds ignored: a headerless node table takes its ids from column 0",
|
|
597
|
+
{ line: firstLine, element: "rowNumberIds" }
|
|
598
|
+
);
|
|
599
|
+
}
|
|
546
600
|
const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
|
|
547
|
-
|
|
548
|
-
|
|
601
|
+
const last = firstRow.length - 1;
|
|
602
|
+
const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
|
|
603
|
+
this.applyCommentDirection();
|
|
604
|
+
this.plan = this.resolvePlan(
|
|
605
|
+
names,
|
|
606
|
+
header,
|
|
607
|
+
firstLine,
|
|
608
|
+
pending.map((p) => p.row),
|
|
609
|
+
optionalLast
|
|
610
|
+
);
|
|
611
|
+
if (optionalLast) {
|
|
612
|
+
report.warning(
|
|
613
|
+
"validation-error",
|
|
614
|
+
TRAILING_HEADER_DELIMITER_CODE,
|
|
615
|
+
`the header ends in a delimiter; rows without the empty last cell are read as complete`,
|
|
616
|
+
{ line: firstLine, element: names[last] }
|
|
617
|
+
);
|
|
549
618
|
}
|
|
550
|
-
this.
|
|
619
|
+
this.reportCommentLikeRecords(names.length);
|
|
620
|
+
this.reportSingleColumn(
|
|
621
|
+
names.length,
|
|
622
|
+
pending.map((p) => p.row),
|
|
623
|
+
firstLine
|
|
624
|
+
);
|
|
551
625
|
this.prepareColumns(firstLine);
|
|
552
626
|
for (const { row, quoted, line } of pending) {
|
|
553
627
|
this.processRow(row, quoted, line);
|
|
@@ -573,37 +647,161 @@ class TableReader {
|
|
|
573
647
|
line: firstLine
|
|
574
648
|
});
|
|
575
649
|
}
|
|
650
|
+
this.reportReaderFindings();
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Whether a row is Excel's delimiter directive (`sep=;`) under a delimiter the caller gave (a
|
|
654
|
+
* sniffed one comes from the directive itself: `reader.sepDirective`).
|
|
655
|
+
* @param row - the first row
|
|
656
|
+
* @returns true for the directive
|
|
657
|
+
*/
|
|
658
|
+
isSepDirective(row) {
|
|
659
|
+
return this.state.csv.delimiter !== null && !this.reader.quoted[0] && /^sep=.$/i.test(row.join(this.state.csv.delimiter));
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Fail when the input opens like another format (an HTML error page, a JSON, GML, DOT or Pajek
|
|
663
|
+
* file handed to the CSV importer), which any delimiter would otherwise read as rows of text.
|
|
664
|
+
* Runs on the raw preview before any record is parsed, so a quoting error in the other
|
|
665
|
+
* format's syntax cannot abort first.
|
|
666
|
+
* @param preview - the start of the input, leading comments removed
|
|
667
|
+
* @param line - the line it starts on
|
|
668
|
+
*/
|
|
669
|
+
refuseOtherFormat(preview, line) {
|
|
670
|
+
if (OTHER_FORMATS.some((r) => r.test(preview))) {
|
|
671
|
+
this.state.report.fail(
|
|
672
|
+
OTHER_FORMAT_CODE,
|
|
673
|
+
`the input opens like another format (XML / HTML, JSON, GML, DOT or Pajek), not CSV: ${JSON.stringify(preview.trimStart().slice(0, 60))}`,
|
|
674
|
+
{ line }
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Whether a column option names a cell of the first row by text.
|
|
680
|
+
* @param row - the first row
|
|
681
|
+
* @returns true when sourceColumn, targetColumn, idColumn or typeColumn names one of its cells
|
|
682
|
+
*/
|
|
683
|
+
namesExplicitColumn(row) {
|
|
684
|
+
const { csv } = this.state;
|
|
685
|
+
const names = headerNames(row);
|
|
686
|
+
return [csv.sourceColumn, csv.targetColumn, csv.idColumn, csv.typeColumn, csv.labelColumn].some(
|
|
687
|
+
(ref) => typeof ref === "string" && findColumn(names, [ref]) >= 0
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Take the file-level direction from the leading SNAP / KONECT comments of this table: the first
|
|
692
|
+
* declaring line wins; a later line declaring the other direction, and a declaration that
|
|
693
|
+
* overrides the caller's explicit defaultDirected, are reported.
|
|
694
|
+
*/
|
|
695
|
+
applyCommentDirection() {
|
|
696
|
+
const { state } = this;
|
|
697
|
+
const { leadingComments, leadingCommentLines } = this.reader;
|
|
698
|
+
for (let i = 0; i < leadingComments.length; i++) {
|
|
699
|
+
const comment = leadingComments[i];
|
|
700
|
+
const directed = commentDirection(comment);
|
|
701
|
+
if (directed === null) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const where = { line: leadingCommentLines[i], element: comment };
|
|
705
|
+
if (state.commentDirected === null) {
|
|
706
|
+
state.commentDirected = directed;
|
|
707
|
+
state.commentText = comment;
|
|
708
|
+
const explicit = state.csv.explicitDirected;
|
|
709
|
+
if (explicit !== null && explicit !== directed) {
|
|
710
|
+
state.report.warning(
|
|
711
|
+
"validation-error",
|
|
712
|
+
COMMENT_DIRECTION_CODE,
|
|
713
|
+
`${JSON.stringify(comment)} declares a ${directionName(directed)} graph, which overrides defaultDirected: ${explicit}`,
|
|
714
|
+
where
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
} else if (state.commentDirected !== directed) {
|
|
718
|
+
state.report.warning(
|
|
719
|
+
"validation-error",
|
|
720
|
+
COMMENT_DIRECTION_CODE,
|
|
721
|
+
`${JSON.stringify(comment)} declares a ${directionName(directed)} graph but ${JSON.stringify(state.commentText)} declared a ${directionName(state.commentDirected)} one; the first applies`,
|
|
722
|
+
where
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Report a leading `#` / `%` line that has the fields of a record (a first id starting with `#`,
|
|
729
|
+
* swallowed as a comment): its text directly follows the comment character and splits into as
|
|
730
|
+
* many cells as the table has columns. SNAP and KONECT comment lines put a space after it.
|
|
731
|
+
* @param width - the number of columns
|
|
732
|
+
*/
|
|
733
|
+
reportCommentLikeRecords(width) {
|
|
734
|
+
const { delimiter, leadingComments, leadingCommentLines } = this.reader;
|
|
735
|
+
if (width < 2 || delimiter === null) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
for (let i = 0; i < leadingComments.length; i++) {
|
|
739
|
+
const comment = leadingComments[i];
|
|
740
|
+
if (comment.length > 1 && comment[1].trim().length > 0 && comment.split(delimiter).length === width) {
|
|
741
|
+
this.state.report.warnOnce(
|
|
742
|
+
"parse-error",
|
|
743
|
+
COMMENT_LIKE_RECORD_CODE,
|
|
744
|
+
`${JSON.stringify(comment)} was skipped as a leading comment but has the fields of a record; quote an id that starts with # or %`,
|
|
745
|
+
{ line: leadingCommentLines[i], element: comment }
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Report a one-column table whose cells another delimiter would split consistently (a delimiter
|
|
752
|
+
* option that does not match the file).
|
|
753
|
+
* @param width - the number of columns
|
|
754
|
+
* @param rows - the data rows read so far
|
|
755
|
+
* @param line - the first line
|
|
756
|
+
*/
|
|
757
|
+
reportSingleColumn(width, rows, line) {
|
|
758
|
+
if (width !== 1 || rows.length === 0 || this.plan?.kind === "adjacency") {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const candidates = DELIMITER_CANDIDATES.filter((d) => d !== this.reader.delimiter);
|
|
762
|
+
const other = sniffDelimiter(`${rows.map((r) => r[0]).join("\n")}
|
|
763
|
+
`, "\n", candidates);
|
|
764
|
+
if (other !== null) {
|
|
765
|
+
this.state.report.warning(
|
|
766
|
+
"parse-error",
|
|
767
|
+
SINGLE_COLUMN_CODE,
|
|
768
|
+
`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`,
|
|
769
|
+
{ line }
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Report what the record reader noticed while reading: a stray quote. */
|
|
774
|
+
reportReaderFindings() {
|
|
775
|
+
const { report } = this.state;
|
|
776
|
+
const { strayQuoteLine } = this.reader;
|
|
777
|
+
if (strayQuoteLine > 0) {
|
|
778
|
+
report.warnOnce(
|
|
779
|
+
"parse-error",
|
|
780
|
+
STRAY_QUOTE_CODE,
|
|
781
|
+
`a quote inside an unquoted field is kept as text (RFC 4180 quotes a whole field)`,
|
|
782
|
+
{ line: strayQuoteLine }
|
|
783
|
+
);
|
|
784
|
+
}
|
|
576
785
|
}
|
|
577
786
|
/**
|
|
578
787
|
* Decide the table's columns from its header.
|
|
579
788
|
* @param names - the column names
|
|
580
789
|
* @param header - whether the file has a header row
|
|
581
790
|
* @param line - the header line
|
|
791
|
+
* @param sample - the data rows read so far
|
|
792
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
582
793
|
* @returns the plan; the import aborts when no endpoints (or id) resolve
|
|
583
794
|
*/
|
|
584
|
-
resolvePlan(names, header, line) {
|
|
585
|
-
const { csv
|
|
795
|
+
resolvePlan(names, header, line, sample, optionalLast) {
|
|
796
|
+
const { csv } = this.state;
|
|
586
797
|
if (this.kind === "adjacency") {
|
|
587
798
|
return { kind: "adjacency", names: [] };
|
|
588
799
|
}
|
|
589
|
-
const width = names.length;
|
|
590
800
|
let source = -1;
|
|
591
801
|
let target = -1;
|
|
592
802
|
if (this.kind !== "nodes") {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
} else if (header) {
|
|
596
|
-
source = findColumn(names, SOURCE_NAMES);
|
|
597
|
-
} else {
|
|
598
|
-
source = width >= 2 ? 0 : -1;
|
|
599
|
-
}
|
|
600
|
-
if (csv.targetColumn !== null) {
|
|
601
|
-
target = resolveColumnRef(names, csv.targetColumn, "targetColumn");
|
|
602
|
-
} else if (header) {
|
|
603
|
-
target = findColumn(names, TARGET_NAMES);
|
|
604
|
-
} else {
|
|
605
|
-
target = width >= 2 ? 1 : -1;
|
|
606
|
-
}
|
|
803
|
+
source = endpointColumn(names, header, csv.sourceColumn, "sourceColumn", SOURCE_NAMES, 0);
|
|
804
|
+
target = endpointColumn(names, header, csv.targetColumn, "targetColumn", TARGET_NAMES, 1);
|
|
607
805
|
if (source >= 0 && target >= 0 && source === target) {
|
|
608
806
|
throw new GraphFormatError("E_UNSUPPORTED", "sourceColumn and targetColumn name the same column", {
|
|
609
807
|
option: "targetColumn",
|
|
@@ -612,10 +810,33 @@ class TableReader {
|
|
|
612
810
|
}
|
|
613
811
|
}
|
|
614
812
|
if (source >= 0 && target >= 0) {
|
|
615
|
-
return this.edgePlan(names, header, source, target);
|
|
813
|
+
return this.edgePlan(names, header, source, target, line, sample, optionalLast);
|
|
616
814
|
}
|
|
815
|
+
this.refuseWithoutEndpoints(names, header, source, target, line);
|
|
816
|
+
return this.nodePlan(names, header, line, optionalLast);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Refuse a table without both endpoint columns, unless it can be a node table: an edge table
|
|
820
|
+
* was asked for, a single endpoint column was found, or nothing names an id.
|
|
821
|
+
* @param names - the column names
|
|
822
|
+
* @param header - whether the file has a header row
|
|
823
|
+
* @param source - the source column, or -1
|
|
824
|
+
* @param target - the target column, or -1
|
|
825
|
+
* @param line - the header line
|
|
826
|
+
*/
|
|
827
|
+
refuseWithoutEndpoints(names, header, source, target, line) {
|
|
828
|
+
const { csv, report } = this.state;
|
|
617
829
|
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
618
|
-
if (
|
|
830
|
+
if (source >= 0 || target >= 0) {
|
|
831
|
+
const [has, missing] = source >= 0 ? ["source", "target"] : ["target", "source"];
|
|
832
|
+
report.fail(
|
|
833
|
+
NO_ENDPOINT_COLUMNS_CODE,
|
|
834
|
+
`the header has a ${has} column (${JSON.stringify(names[Math.max(source, target)])}) but no ${missing} column (${shown})`,
|
|
835
|
+
{ line },
|
|
836
|
+
{ columns: [...names] }
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
if (this.kind === "edges" || this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null || csv.nodes !== null)) {
|
|
619
840
|
report.fail(
|
|
620
841
|
NO_ENDPOINT_COLUMNS_CODE,
|
|
621
842
|
`no source / target columns in the header (${shown}); a node table goes in the nodes option`,
|
|
@@ -623,7 +844,7 @@ class TableReader {
|
|
|
623
844
|
{ columns: [...names] }
|
|
624
845
|
);
|
|
625
846
|
}
|
|
626
|
-
const idResolves = header ? findColumn(names, ID_NAMES) >= 0 :
|
|
847
|
+
const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : names.length >= 1;
|
|
627
848
|
if (this.kind === "auto" && csv.idColumn === null && !idResolves && !this.rowNumbers) {
|
|
628
849
|
report.fail(
|
|
629
850
|
NO_ENDPOINT_COLUMNS_CODE,
|
|
@@ -632,7 +853,6 @@ class TableReader {
|
|
|
632
853
|
{ columns: [...names] }
|
|
633
854
|
);
|
|
634
855
|
}
|
|
635
|
-
return this.nodePlan(names, header, line);
|
|
636
856
|
}
|
|
637
857
|
/**
|
|
638
858
|
* The columns of an edge table.
|
|
@@ -640,50 +860,26 @@ class TableReader {
|
|
|
640
860
|
* @param header - whether the file has a header row
|
|
641
861
|
* @param source - the source column
|
|
642
862
|
* @param target - the target column
|
|
863
|
+
* @param line - the header (or first) line
|
|
864
|
+
* @param sample - the data rows read so far
|
|
865
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
643
866
|
* @returns the plan
|
|
644
867
|
*/
|
|
645
|
-
edgePlan(names, header, source, target) {
|
|
646
|
-
const { csv
|
|
868
|
+
edgePlan(names, header, source, target, line, sample, optionalLast) {
|
|
869
|
+
const { csv } = this.state;
|
|
647
870
|
const claimed = /* @__PURE__ */ new Set([source, target]);
|
|
648
|
-
|
|
649
|
-
if (common.weightFrom !== null) {
|
|
650
|
-
if (header) {
|
|
651
|
-
weight = findFree(names, [common.weightFrom], claimed);
|
|
652
|
-
if (weight < 0 && this.state.weightFromExplicit) {
|
|
653
|
-
report.warning(
|
|
654
|
-
"missing-value",
|
|
655
|
-
COLUMN_MISSING_CODE,
|
|
656
|
-
`weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`,
|
|
657
|
-
{ line: this.reader.line, element: common.weightFrom }
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
} else if (names.length >= 3 && !claimed.has(2)) {
|
|
661
|
-
weight = 2;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
871
|
+
const weight = this.weightColumn(names, header, claimed, line, sample);
|
|
664
872
|
if (weight >= 0) {
|
|
665
873
|
claimed.add(weight);
|
|
666
874
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (header && names[source] === "Source" && names[target] === "Target") {
|
|
670
|
-
type = findFree(names, [TYPE_NAME], claimed);
|
|
671
|
-
if (type >= 0 && names[type] !== TYPE_NAME) {
|
|
672
|
-
type = -1;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
} else if (csv.typeColumn !== null) {
|
|
676
|
-
type = resolveColumnRef(names, csv.typeColumn, "typeColumn");
|
|
677
|
-
if (claimed.has(type)) {
|
|
678
|
-
throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
|
|
679
|
-
option: "typeColumn",
|
|
680
|
-
found: names[type]
|
|
681
|
-
});
|
|
682
|
-
}
|
|
875
|
+
if (header) {
|
|
876
|
+
this.reportEndpointRivals(names, source, target, weight, line);
|
|
683
877
|
}
|
|
878
|
+
const type = this.typeColumn(names, header, source, target, claimed);
|
|
684
879
|
if (type >= 0) {
|
|
685
880
|
claimed.add(type);
|
|
686
881
|
}
|
|
882
|
+
const typeHint = header && type < 0 && csv.typeColumn === void 0 ? findFree(names, [TYPE_NAME], claimed) : -1;
|
|
687
883
|
const id = header ? findFree(names, EDGE_ID_NAMES, claimed) : -1;
|
|
688
884
|
if (id >= 0) {
|
|
689
885
|
claimed.add(id);
|
|
@@ -698,56 +894,162 @@ class TableReader {
|
|
|
698
894
|
attributes.push(i);
|
|
699
895
|
}
|
|
700
896
|
}
|
|
701
|
-
return {
|
|
897
|
+
return {
|
|
898
|
+
kind: "edges",
|
|
899
|
+
names,
|
|
900
|
+
width: names.length,
|
|
901
|
+
source,
|
|
902
|
+
target,
|
|
903
|
+
weight,
|
|
904
|
+
type,
|
|
905
|
+
typeHint,
|
|
906
|
+
id,
|
|
907
|
+
label,
|
|
908
|
+
attributes,
|
|
909
|
+
optionalLast
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* The weight column of an edge table: the weightFrom column of a header (a warning when it was
|
|
914
|
+
* asked for and is missing), else the third column of a headerless edge list unless it holds text.
|
|
915
|
+
* @param names - the column names
|
|
916
|
+
* @param header - whether the file has a header row
|
|
917
|
+
* @param claimed - the endpoint columns
|
|
918
|
+
* @param line - the header (or first) line
|
|
919
|
+
* @param sample - the data rows read so far
|
|
920
|
+
* @returns the column index, or -1
|
|
921
|
+
*/
|
|
922
|
+
weightColumn(names, header, claimed, line, sample) {
|
|
923
|
+
const { common, report } = this.state;
|
|
924
|
+
if (common.weightFrom === null) {
|
|
925
|
+
return -1;
|
|
926
|
+
}
|
|
927
|
+
if (header) {
|
|
928
|
+
const weight = findFree(names, [common.weightFrom], claimed);
|
|
929
|
+
if (weight < 0 && this.state.weightFromExplicit) {
|
|
930
|
+
report.warning(
|
|
931
|
+
"missing-value",
|
|
932
|
+
COLUMN_MISSING_CODE,
|
|
933
|
+
`weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`,
|
|
934
|
+
{ line, element: common.weightFrom }
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
return weight;
|
|
938
|
+
}
|
|
939
|
+
if (names.length < 3 || claimed.has(2)) {
|
|
940
|
+
return -1;
|
|
941
|
+
}
|
|
942
|
+
if (isTextTriple(names.length, sample)) {
|
|
943
|
+
report.warnOnce(
|
|
944
|
+
"validation-error",
|
|
945
|
+
WEIGHT_AS_ATTRIBUTE_CODE,
|
|
946
|
+
`the third column holds text in the first rows, so it is read as the attribute ${names[2]}, not the weight`,
|
|
947
|
+
{ line, element: names[2] }
|
|
948
|
+
);
|
|
949
|
+
return -1;
|
|
950
|
+
}
|
|
951
|
+
return 2;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Warn about header columns that name the source, target or weight beside the chosen one.
|
|
955
|
+
* @param names - the header names
|
|
956
|
+
* @param source - the source column
|
|
957
|
+
* @param target - the target column
|
|
958
|
+
* @param weight - the weight column, or -1
|
|
959
|
+
* @param line - the header line
|
|
960
|
+
*/
|
|
961
|
+
reportEndpointRivals(names, source, target, weight, line) {
|
|
962
|
+
const { csv, common } = this.state;
|
|
963
|
+
if (csv.sourceColumn === null) {
|
|
964
|
+
this.reportRivals(names, SOURCE_NAMES, source, "source", line);
|
|
965
|
+
}
|
|
966
|
+
if (csv.targetColumn === null) {
|
|
967
|
+
this.reportRivals(names, TARGET_NAMES, target, "target", line);
|
|
968
|
+
}
|
|
969
|
+
if (weight >= 0 && common.weightFrom !== null) {
|
|
970
|
+
this.reportRivals(names, [common.weightFrom], weight, "weight", line);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* The edge type column: the typeColumn option, else Gephi's `Type` beside `Source` and `Target`.
|
|
975
|
+
* @param names - the column names
|
|
976
|
+
* @param header - whether the file has a header row
|
|
977
|
+
* @param source - the source column
|
|
978
|
+
* @param target - the target column
|
|
979
|
+
* @param claimed - the columns other roles took
|
|
980
|
+
* @returns the column index, or -1
|
|
981
|
+
*/
|
|
982
|
+
typeColumn(names, header, source, target, claimed) {
|
|
983
|
+
const { typeColumn } = this.state.csv;
|
|
984
|
+
if (typeColumn === null) {
|
|
985
|
+
return -1;
|
|
986
|
+
}
|
|
987
|
+
if (typeColumn === void 0) {
|
|
988
|
+
const gephi = header && names[source] === "Source" && names[target] === "Target";
|
|
989
|
+
const type2 = gephi ? findFree(names, [TYPE_NAME], claimed) : -1;
|
|
990
|
+
return type2 >= 0 && names[type2] === TYPE_NAME ? type2 : -1;
|
|
991
|
+
}
|
|
992
|
+
const type = resolveColumnRef(names, typeColumn, "typeColumn");
|
|
993
|
+
if (claimed.has(type)) {
|
|
994
|
+
throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
|
|
995
|
+
option: "typeColumn",
|
|
996
|
+
found: names[type]
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
return type;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Warn about header columns that name a role another column was chosen for (`Source,source`,
|
|
1003
|
+
* `from,to,source`, `Weight,weight`): the one not chosen is read as a plain attribute.
|
|
1004
|
+
* @param names - the header names
|
|
1005
|
+
* @param candidates - the names of the role
|
|
1006
|
+
* @param chosen - the column chosen
|
|
1007
|
+
* @param role - the role, for the message
|
|
1008
|
+
* @param line - the header line
|
|
1009
|
+
*/
|
|
1010
|
+
reportRivals(names, candidates, chosen, role, line) {
|
|
1011
|
+
const lower2 = new Set(candidates.map((c) => c.toLowerCase()));
|
|
1012
|
+
names.forEach((name, i) => {
|
|
1013
|
+
if (i !== chosen && lower2.has(name.toLowerCase())) {
|
|
1014
|
+
this.state.report.warning(
|
|
1015
|
+
"validation-error",
|
|
1016
|
+
AMBIGUOUS_COLUMN_CODE,
|
|
1017
|
+
`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`,
|
|
1018
|
+
{ line, element: name }
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* The label column of a node table: the labelColumn option, else a header column named `label`.
|
|
1025
|
+
* @param names - the column names
|
|
1026
|
+
* @param header - whether the file has a header row
|
|
1027
|
+
* @param claimed - columns another role took
|
|
1028
|
+
* @returns the column index, or -1
|
|
1029
|
+
*/
|
|
1030
|
+
labelOf(names, header, claimed) {
|
|
1031
|
+
const { labelColumn } = this.state.csv;
|
|
1032
|
+
if (labelColumn !== null) {
|
|
1033
|
+
return resolveColumnRef(names, labelColumn, "labelColumn");
|
|
1034
|
+
}
|
|
1035
|
+
return header ? findFree(names, LABEL_NAMES, claimed) : -1;
|
|
702
1036
|
}
|
|
703
1037
|
/**
|
|
704
1038
|
* The columns of a node table.
|
|
705
1039
|
* @param names - the column names
|
|
706
1040
|
* @param header - whether the file has a header row
|
|
707
1041
|
* @param line - the header line
|
|
1042
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
708
1043
|
* @returns the plan; the import aborts when no id column resolves
|
|
709
1044
|
*/
|
|
710
|
-
nodePlan(names, header, line) {
|
|
711
|
-
const {
|
|
1045
|
+
nodePlan(names, header, line, optionalLast) {
|
|
1046
|
+
const { common } = this.state;
|
|
712
1047
|
const claimed = /* @__PURE__ */ new Set();
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
} else if (names.length >= 1) {
|
|
719
|
-
idColumn = 0;
|
|
720
|
-
}
|
|
721
|
-
const label = header ? findFree(names, LABEL_NAMES, new Set(idColumn >= 0 ? [idColumn] : [])) : -1;
|
|
722
|
-
let id;
|
|
723
|
-
switch (common.nodeIdFrom) {
|
|
724
|
-
case "label":
|
|
725
|
-
if (label < 0) {
|
|
726
|
-
report.fail(NO_ID_COLUMN_CODE, 'nodeIdFrom is "label" but the node table has no label column', {
|
|
727
|
-
line
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
id = label;
|
|
731
|
-
break;
|
|
732
|
-
case "index":
|
|
733
|
-
id = -1;
|
|
734
|
-
break;
|
|
735
|
-
default:
|
|
736
|
-
if (idColumn < 0 && this.rowNumbers) {
|
|
737
|
-
id = -1;
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
if (idColumn < 0) {
|
|
741
|
-
report.fail(
|
|
742
|
-
NO_ID_COLUMN_CODE,
|
|
743
|
-
`no id column in the node table header (${names.map((n) => JSON.stringify(n)).join(", ")})`,
|
|
744
|
-
{ line },
|
|
745
|
-
{ columns: [...names] }
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
id = idColumn;
|
|
749
|
-
claimed.add(idColumn);
|
|
750
|
-
break;
|
|
1048
|
+
const idColumn = this.idColumnOf(names, header);
|
|
1049
|
+
const label = this.labelOf(names, header, new Set(idColumn >= 0 ? [idColumn] : []));
|
|
1050
|
+
const id = this.nodeIdColumn(names, idColumn, label, line);
|
|
1051
|
+
if (id >= 0) {
|
|
1052
|
+
claimed.add(id);
|
|
751
1053
|
}
|
|
752
1054
|
if (label >= 0) {
|
|
753
1055
|
claimed.add(label);
|
|
@@ -759,7 +1061,60 @@ class TableReader {
|
|
|
759
1061
|
}
|
|
760
1062
|
}
|
|
761
1063
|
const rowNumber = id < 0 && common.nodeIdFrom !== "index";
|
|
762
|
-
return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes };
|
|
1064
|
+
return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes, optionalLast };
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* The id column of a node table: the idColumn option, else a header column with an id name,
|
|
1068
|
+
* else the first column of a headerless table.
|
|
1069
|
+
* @param names - the column names
|
|
1070
|
+
* @param header - whether the file has a header row
|
|
1071
|
+
* @returns the column index, or -1
|
|
1072
|
+
*/
|
|
1073
|
+
idColumnOf(names, header) {
|
|
1074
|
+
const { idColumn } = this.state.csv;
|
|
1075
|
+
if (idColumn !== null) {
|
|
1076
|
+
return resolveColumnRef(names, idColumn, "idColumn");
|
|
1077
|
+
}
|
|
1078
|
+
if (header) {
|
|
1079
|
+
return findColumn(names, ID_NAMES);
|
|
1080
|
+
}
|
|
1081
|
+
return names.length >= 1 ? 0 : -1;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* The column node ids come from, as nodeIdFrom says: the label, none (row numbers), or the id
|
|
1085
|
+
* column. The import aborts when that column is missing.
|
|
1086
|
+
* @param names - the column names
|
|
1087
|
+
* @param idColumn - the id column, or -1
|
|
1088
|
+
* @param label - the label column, or -1
|
|
1089
|
+
* @param line - the header line
|
|
1090
|
+
* @returns the column index, or -1 for row numbers
|
|
1091
|
+
*/
|
|
1092
|
+
nodeIdColumn(names, idColumn, label, line) {
|
|
1093
|
+
const { common, report } = this.state;
|
|
1094
|
+
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
1095
|
+
if (common.nodeIdFrom === "index") {
|
|
1096
|
+
return -1;
|
|
1097
|
+
}
|
|
1098
|
+
if (common.nodeIdFrom === "label") {
|
|
1099
|
+
if (label < 0) {
|
|
1100
|
+
report.fail(
|
|
1101
|
+
NO_ID_COLUMN_CODE,
|
|
1102
|
+
`nodeIdFrom is "label", but the node table has no column named "label" (${shown}); pass labelColumn to name the column that holds the labels`,
|
|
1103
|
+
{ line },
|
|
1104
|
+
{ columns: [...names] }
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
return label;
|
|
1108
|
+
}
|
|
1109
|
+
if (idColumn < 0 && !this.rowNumbers) {
|
|
1110
|
+
report.fail(
|
|
1111
|
+
NO_ID_COLUMN_CODE,
|
|
1112
|
+
`no id column in the node table header (${shown})`,
|
|
1113
|
+
{ line },
|
|
1114
|
+
{ columns: [...names] }
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
return idColumn;
|
|
763
1118
|
}
|
|
764
1119
|
/**
|
|
765
1120
|
* Declare the role columns and create a writer per attribute column.
|
|
@@ -817,6 +1172,13 @@ class TableReader {
|
|
|
817
1172
|
processRow(row, quoted, line) {
|
|
818
1173
|
const plan = this.requirePlan();
|
|
819
1174
|
this.dataRows++;
|
|
1175
|
+
if (this.state.csv.decimalComma) {
|
|
1176
|
+
for (let i = 0; i < row.length; i++) {
|
|
1177
|
+
if (DECIMAL_COMMA.test(row[i])) {
|
|
1178
|
+
row[i] = row[i].replace(",", ".");
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
820
1182
|
if (plan.kind === "edges") {
|
|
821
1183
|
this.processEdgeRow(plan, row, quoted, line);
|
|
822
1184
|
} else if (plan.kind === "nodes") {
|
|
@@ -826,7 +1188,7 @@ class TableReader {
|
|
|
826
1188
|
}
|
|
827
1189
|
}
|
|
828
1190
|
/**
|
|
829
|
-
* Push one adjacency row: the node, then one edge per set
|
|
1191
|
+
* Push one adjacency row: the node, then one edge per set neighbor cell, in row order.
|
|
830
1192
|
* @param row - the cells
|
|
831
1193
|
* @param quoted - whether each cell was quoted
|
|
832
1194
|
* @param line - the row's line
|
|
@@ -837,16 +1199,16 @@ class TableReader {
|
|
|
837
1199
|
const { where } = this;
|
|
838
1200
|
where.line = line;
|
|
839
1201
|
where.element = null;
|
|
840
|
-
let
|
|
1202
|
+
let neighbors = 0;
|
|
841
1203
|
for (let k = 1; k < row.length; k++) {
|
|
842
1204
|
if (!isUnset(row[k], quoted[k])) {
|
|
843
|
-
|
|
1205
|
+
neighbors++;
|
|
844
1206
|
}
|
|
845
1207
|
}
|
|
846
1208
|
if (isUnset(row[0], quoted[0])) {
|
|
847
|
-
report.error("missing-value", MISSING_ID_CODE, `
|
|
1209
|
+
report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
|
|
848
1210
|
counts.skippedNodes++;
|
|
849
|
-
counts.skippedEdges +=
|
|
1211
|
+
counts.skippedEdges += neighbors;
|
|
850
1212
|
return;
|
|
851
1213
|
}
|
|
852
1214
|
const kind = this.state.commentDirected ?? common.defaultDirected ? "directed" : "undirected";
|
|
@@ -865,7 +1227,7 @@ class TableReader {
|
|
|
865
1227
|
} catch (err) {
|
|
866
1228
|
report.recordError(err, where);
|
|
867
1229
|
counts.skippedNodes++;
|
|
868
|
-
counts.skippedEdges +=
|
|
1230
|
+
counts.skippedEdges += neighbors;
|
|
869
1231
|
return;
|
|
870
1232
|
}
|
|
871
1233
|
for (let k = 1; k < row.length; k++) {
|
|
@@ -891,19 +1253,15 @@ class TableReader {
|
|
|
891
1253
|
* Push one edge row: endpoints, weight, direction, then the attribute cells.
|
|
892
1254
|
* @param plan - the edge plan
|
|
893
1255
|
* @param row - the cells
|
|
894
|
-
* @param
|
|
1256
|
+
* @param cellQuoted - whether each cell was quoted
|
|
895
1257
|
* @param line - the row's line
|
|
896
1258
|
*/
|
|
897
|
-
processEdgeRow(plan, row,
|
|
898
|
-
const { report
|
|
1259
|
+
processEdgeRow(plan, row, cellQuoted, line) {
|
|
1260
|
+
const { report } = this.state;
|
|
899
1261
|
const { counts } = report;
|
|
1262
|
+
const quoted = completeRow(plan, row, cellQuoted, report, line);
|
|
900
1263
|
if (row.length !== plan.width) {
|
|
901
|
-
|
|
902
|
-
"validation-error",
|
|
903
|
-
FIELD_COUNT_CODE,
|
|
904
|
-
`line ${line}: ${row.length} field(s), the header has ${plan.width}`,
|
|
905
|
-
{ line }
|
|
906
|
-
);
|
|
1264
|
+
this.reportRowWidth(plan, row, line);
|
|
907
1265
|
counts.skippedEdges++;
|
|
908
1266
|
return;
|
|
909
1267
|
}
|
|
@@ -911,64 +1269,36 @@ class TableReader {
|
|
|
911
1269
|
const targetText = row[plan.target];
|
|
912
1270
|
const sourceMissing = isUnset(sourceText, quoted[plan.source]);
|
|
913
1271
|
if (sourceMissing || isUnset(targetText, quoted[plan.target])) {
|
|
914
|
-
report.error(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
`line ${line}: blank ${sourceMissing ? "source" : "target"} cell`,
|
|
918
|
-
{ line }
|
|
919
|
-
);
|
|
1272
|
+
report.error("missing-value", MISSING_ENDPOINT_CODE, `blank ${sourceMissing ? "source" : "target"} cell`, {
|
|
1273
|
+
line
|
|
1274
|
+
});
|
|
920
1275
|
counts.skippedEdges++;
|
|
921
1276
|
return;
|
|
922
1277
|
}
|
|
923
|
-
|
|
924
|
-
if (
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
report.error(
|
|
928
|
-
"validation-error",
|
|
929
|
-
BAD_TYPE_CODE,
|
|
930
|
-
`line ${line}: Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`,
|
|
931
|
-
{ line }
|
|
932
|
-
);
|
|
933
|
-
counts.skippedEdges++;
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
if (parsed !== void 0) {
|
|
937
|
-
kind = parsed;
|
|
938
|
-
}
|
|
1278
|
+
const kind = this.rowKind(plan, row, line);
|
|
1279
|
+
if (kind === null) {
|
|
1280
|
+
counts.skippedEdges++;
|
|
1281
|
+
return;
|
|
939
1282
|
}
|
|
940
1283
|
const { where } = this;
|
|
941
1284
|
where.line = line;
|
|
942
1285
|
where.element = null;
|
|
943
1286
|
const idText = plan.id >= 0 && !isUnset(row[plan.id], quoted[plan.id]) ? row[plan.id] : null;
|
|
944
|
-
if (idText !== null) {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
this.edgeIds.add(idText);
|
|
1287
|
+
if (idText !== null && this.edgeIds.has(idText)) {
|
|
1288
|
+
report.error(
|
|
1289
|
+
"validation-error",
|
|
1290
|
+
DUPLICATE_EDGE_ID_CODE,
|
|
1291
|
+
`edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`,
|
|
1292
|
+
{ line, element: idText }
|
|
1293
|
+
);
|
|
1294
|
+
counts.skippedEdges++;
|
|
1295
|
+
return;
|
|
956
1296
|
}
|
|
1297
|
+
this.checkPadded(sourceText, quoted[plan.source], line);
|
|
1298
|
+
this.checkPadded(targetText, quoted[plan.target], line);
|
|
957
1299
|
let edge;
|
|
958
1300
|
try {
|
|
959
|
-
|
|
960
|
-
const target = this.coerce(targetText);
|
|
961
|
-
const weight = plan.weight >= 0 ? parseWeightText(row[plan.weight]) : void 0;
|
|
962
|
-
if (!this.state.headerSet) {
|
|
963
|
-
this.state.headerSet = true;
|
|
964
|
-
resolver.setHeader(kind !== "undirected", where);
|
|
965
|
-
}
|
|
966
|
-
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
967
|
-
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
968
|
-
const before = sink.edgeCount;
|
|
969
|
-
edge = resolver.addEdge(source, target, kind, weight, where);
|
|
970
|
-
counts.edges += sink.edgeCount - before;
|
|
971
|
-
counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
1301
|
+
edge = this.addEdge(sourceText, targetText, kind, plan.weight >= 0 ? row[plan.weight] : null);
|
|
972
1302
|
} catch (err) {
|
|
973
1303
|
where.element = idText ?? `${sourceText}->${targetText}`;
|
|
974
1304
|
report.recordError(err, where);
|
|
@@ -976,40 +1306,133 @@ class TableReader {
|
|
|
976
1306
|
return;
|
|
977
1307
|
}
|
|
978
1308
|
if (idText !== null) {
|
|
1309
|
+
this.edgeIds.add(idText);
|
|
979
1310
|
this.writeRole(this.idHandle, "edge", edge, idText, plan.names[plan.id], line);
|
|
980
1311
|
}
|
|
1312
|
+
this.warnTypeHint(plan, row, line);
|
|
981
1313
|
if (plan.label >= 0 && !isUnset(row[plan.label], quoted[plan.label])) {
|
|
982
1314
|
this.writeRole(this.labelHandle, "edge", edge, row[plan.label], plan.names[plan.label], line);
|
|
983
1315
|
}
|
|
984
1316
|
this.writeAttributes(plan, row, quoted, edge, line);
|
|
985
1317
|
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Warn, once, when a column that is not the type column holds a direction word.
|
|
1320
|
+
* @param plan - the edge plan
|
|
1321
|
+
* @param row - the cells
|
|
1322
|
+
* @param line - the row's line
|
|
1323
|
+
*/
|
|
1324
|
+
warnTypeHint(plan, row, line) {
|
|
1325
|
+
if (plan.typeHint < 0 || typeof parseKind(row[plan.typeHint]) !== "string") {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
this.state.report.warnOnce(
|
|
1329
|
+
"unsupported",
|
|
1330
|
+
TYPE_COLUMN_IGNORED_CODE,
|
|
1331
|
+
`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`,
|
|
1332
|
+
{ line, element: plan.names[plan.typeHint] }
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Report a row with the wrong number of cells. The first row too long for an auto table says
|
|
1337
|
+
* what an adjacency list needs, since one reads this way.
|
|
1338
|
+
* @param plan - the edge plan
|
|
1339
|
+
* @param row - the cells
|
|
1340
|
+
* @param line - the row's line
|
|
1341
|
+
*/
|
|
1342
|
+
reportRowWidth(plan, row, line) {
|
|
1343
|
+
const hint = this.state.csv.table === "auto" && row.length > plan.width && !this.adjacencyHinted ? '; if each line is a node followed by its neighbors, pass table: "adjacency"' : "";
|
|
1344
|
+
this.adjacencyHinted ||= hint !== "";
|
|
1345
|
+
this.state.report.error(
|
|
1346
|
+
"validation-error",
|
|
1347
|
+
FIELD_COUNT_CODE,
|
|
1348
|
+
`${row.length} field${plural(row.length)}, expected ${plan.width}${hint}`,
|
|
1349
|
+
{ line }
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* The direction of an edge row: its Type cell, else the file's or the default direction.
|
|
1354
|
+
* @param plan - the edge plan
|
|
1355
|
+
* @param row - the cells
|
|
1356
|
+
* @param line - the row's line
|
|
1357
|
+
* @returns the kind, or null when the Type cell is not a direction (reported)
|
|
1358
|
+
*/
|
|
1359
|
+
rowKind(plan, row, line) {
|
|
1360
|
+
const directed = this.state.commentDirected ?? this.state.common.defaultDirected;
|
|
1361
|
+
const kind = directed ? "directed" : "undirected";
|
|
1362
|
+
if (plan.type < 0) {
|
|
1363
|
+
return kind;
|
|
1364
|
+
}
|
|
1365
|
+
const parsed = parseKind(row[plan.type]);
|
|
1366
|
+
if (parsed === null) {
|
|
1367
|
+
this.state.report.error(
|
|
1368
|
+
"validation-error",
|
|
1369
|
+
BAD_TYPE_CODE,
|
|
1370
|
+
`Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`,
|
|
1371
|
+
{ line }
|
|
1372
|
+
);
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
return parsed ?? kind;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Add an edge row's edge, counting the edge and the nodes it brings in.
|
|
1379
|
+
* @param sourceText - the source cell
|
|
1380
|
+
* @param targetText - the target cell
|
|
1381
|
+
* @param kind - the direction
|
|
1382
|
+
* @param weightText - the weight cell, or null
|
|
1383
|
+
* @returns the edge's index
|
|
1384
|
+
*/
|
|
1385
|
+
addEdge(sourceText, targetText, kind, weightText) {
|
|
1386
|
+
const { report, sink, resolver } = this.state;
|
|
1387
|
+
const { where } = this;
|
|
1388
|
+
const source = this.coerce(sourceText);
|
|
1389
|
+
const target = this.coerce(targetText);
|
|
1390
|
+
const weight = weightText === null ? void 0 : parseWeightText(weightText, report);
|
|
1391
|
+
if (!this.state.headerSet) {
|
|
1392
|
+
this.state.headerSet = true;
|
|
1393
|
+
resolver.setHeader(kind !== "undirected", where);
|
|
1394
|
+
}
|
|
1395
|
+
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
1396
|
+
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
1397
|
+
const before = sink.edgeCount;
|
|
1398
|
+
const edge = resolver.addEdge(source, target, kind, weight, where);
|
|
1399
|
+
report.counts.edges += sink.edgeCount - before;
|
|
1400
|
+
report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
1401
|
+
return edge;
|
|
1402
|
+
}
|
|
986
1403
|
/**
|
|
987
1404
|
* Push one node row: the id, then the label and attribute cells.
|
|
988
1405
|
* @param plan - the node plan
|
|
989
1406
|
* @param row - the cells
|
|
990
|
-
* @param
|
|
1407
|
+
* @param cellQuoted - whether each cell was quoted
|
|
991
1408
|
* @param line - the row's line
|
|
992
1409
|
*/
|
|
993
|
-
processNodeRow(plan, row,
|
|
1410
|
+
processNodeRow(plan, row, cellQuoted, line) {
|
|
994
1411
|
const { report, sink } = this.state;
|
|
995
1412
|
const { counts } = report;
|
|
996
1413
|
const ordinal = this.nodeOrdinal++;
|
|
1414
|
+
const quoted = completeRow(plan, row, cellQuoted, report, line);
|
|
997
1415
|
if (row.length !== plan.width) {
|
|
998
1416
|
report.error(
|
|
999
1417
|
"validation-error",
|
|
1000
1418
|
FIELD_COUNT_CODE,
|
|
1001
|
-
|
|
1002
|
-
{
|
|
1419
|
+
`${row.length} field${plural(row.length)}, expected ${plan.width}`,
|
|
1420
|
+
{
|
|
1421
|
+
line
|
|
1422
|
+
}
|
|
1003
1423
|
);
|
|
1004
1424
|
counts.skippedNodes++;
|
|
1005
1425
|
return;
|
|
1006
1426
|
}
|
|
1007
1427
|
const idText = plan.id >= 0 ? row[plan.id] : String(ordinal);
|
|
1008
1428
|
if (plan.id >= 0 && isUnset(idText, quoted[plan.id])) {
|
|
1009
|
-
report.error("missing-value", MISSING_ID_CODE, `
|
|
1429
|
+
report.error("missing-value", MISSING_ID_CODE, `blank id cell`, { line });
|
|
1010
1430
|
counts.skippedNodes++;
|
|
1011
1431
|
return;
|
|
1012
1432
|
}
|
|
1433
|
+
if (plan.id >= 0) {
|
|
1434
|
+
this.checkPadded(idText, quoted[plan.id], line);
|
|
1435
|
+
}
|
|
1013
1436
|
const { where } = this;
|
|
1014
1437
|
where.line = line;
|
|
1015
1438
|
where.element = idText;
|
|
@@ -1020,7 +1443,7 @@ class TableReader {
|
|
|
1020
1443
|
report.warning(
|
|
1021
1444
|
"merged",
|
|
1022
1445
|
DUPLICATE_NODE_CODE,
|
|
1023
|
-
`
|
|
1446
|
+
`node ${JSON.stringify(id)} already exists; its attributes are overwritten`,
|
|
1024
1447
|
where
|
|
1025
1448
|
);
|
|
1026
1449
|
} else {
|
|
@@ -1037,6 +1460,23 @@ class TableReader {
|
|
|
1037
1460
|
}
|
|
1038
1461
|
this.writeAttributes(plan, row, quoted, index, line);
|
|
1039
1462
|
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Warn once per import about an unquoted id cell with leading or trailing whitespace, which is
|
|
1465
|
+
* a different id from the bare text (RFC 4180 keeps spaces).
|
|
1466
|
+
* @param text - the cell text
|
|
1467
|
+
* @param quoted - whether the cell was quoted
|
|
1468
|
+
* @param line - the row's line
|
|
1469
|
+
*/
|
|
1470
|
+
checkPadded(text, quoted, line) {
|
|
1471
|
+
if (isPadded(text, quoted)) {
|
|
1472
|
+
this.state.report.warnOnce(
|
|
1473
|
+
"validation-error",
|
|
1474
|
+
PADDED_ID_CODE,
|
|
1475
|
+
`id ${JSON.stringify(text)} has leading or trailing whitespace and is kept as written, a different id from the trimmed text`,
|
|
1476
|
+
{ line, element: text }
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1040
1480
|
/**
|
|
1041
1481
|
* Coerce an id cell, reporting a merge under `ids: "number"`.
|
|
1042
1482
|
* @param text - the cell text
|
|
@@ -1102,29 +1542,104 @@ class TableReader {
|
|
|
1102
1542
|
}
|
|
1103
1543
|
}
|
|
1104
1544
|
}
|
|
1545
|
+
function isTextTriple(width, sample) {
|
|
1546
|
+
return width === 3 && sample.length > 0 && sample.every((row) => row.length === 3 && !isBlank(row[2]) && numberOrNull(row[2]) === null);
|
|
1547
|
+
}
|
|
1548
|
+
function hasExplicitDialect(csv) {
|
|
1549
|
+
return csv.delimiter !== null || csv.header !== "auto" || csv.table !== "auto" || csv.sourceColumn !== null || csv.targetColumn !== null || csv.idColumn !== null || csv.typeColumn !== void 0 && csv.typeColumn !== null;
|
|
1550
|
+
}
|
|
1551
|
+
function completeRow(plan, row, quoted, report, line) {
|
|
1552
|
+
if (row.length === plan.width + 1 && isUnset(row[plan.width], quoted[plan.width])) {
|
|
1553
|
+
row.pop();
|
|
1554
|
+
report.warnOnce(
|
|
1555
|
+
"validation-error",
|
|
1556
|
+
TRAILING_DELIMITER_CODE,
|
|
1557
|
+
`the row ends in a delimiter the header does not have; the empty last cell is dropped`,
|
|
1558
|
+
{ line }
|
|
1559
|
+
);
|
|
1560
|
+
return quoted;
|
|
1561
|
+
}
|
|
1562
|
+
if (!plan.optionalLast || row.length !== plan.width - 1) {
|
|
1563
|
+
return quoted;
|
|
1564
|
+
}
|
|
1565
|
+
const flags = quoted.slice(0, row.length);
|
|
1566
|
+
row.push("");
|
|
1567
|
+
flags.push(false);
|
|
1568
|
+
return flags;
|
|
1569
|
+
}
|
|
1105
1570
|
const HEAD_BYTES = 4096;
|
|
1106
|
-
|
|
1571
|
+
function endpointColumn(names, header, option, optionName, known, position) {
|
|
1572
|
+
if (option !== null) {
|
|
1573
|
+
return resolveColumnRef(names, option, optionName);
|
|
1574
|
+
}
|
|
1575
|
+
if (header) {
|
|
1576
|
+
return findColumn(names, known);
|
|
1577
|
+
}
|
|
1578
|
+
return names.length >= 2 ? position : -1;
|
|
1579
|
+
}
|
|
1580
|
+
const TAG_NAME = String.raw`<[a-z_][\w.-]*(?::[a-z_][\w.-]*)?`;
|
|
1581
|
+
const OTHER_FORMATS = [
|
|
1582
|
+
/^\s*<(?:\?xml|!doctype|!--)/i,
|
|
1583
|
+
// an XML declaration, a doctype or a comment
|
|
1584
|
+
new RegExp(String.raw`^\s*${TAG_NAME}\s*\/?>\s*(?:[^,;|\s]*<|$)`, "i"),
|
|
1585
|
+
// a tag, then another tag
|
|
1586
|
+
new RegExp(String.raw`^\s*${TAG_NAME}\s+[a-z_][\w.:-]*\s*=`, "i"),
|
|
1587
|
+
// a tag with an attribute
|
|
1588
|
+
/^\s*\/[*/]/,
|
|
1589
|
+
// a C comment (DOT)
|
|
1590
|
+
/^\s*\{\s*(?:["}]|$)/,
|
|
1591
|
+
// a JSON object
|
|
1592
|
+
/^\s*\[\s*(?:[[{"\]\d-]|$)/,
|
|
1593
|
+
// a JSON array
|
|
1594
|
+
/^\s*(?:strict\s+)?(?:di)?graph(?:\s+\S+)?\s*\{/i,
|
|
1595
|
+
// a DOT graph
|
|
1596
|
+
/^\s*\*(?:vertices|network|arcs|edges)\b/i,
|
|
1597
|
+
// a Pajek section
|
|
1598
|
+
/^\s*(?:creator\s+"|graph\s*\[)/i
|
|
1599
|
+
// GML
|
|
1600
|
+
];
|
|
1107
1601
|
function sniff(head) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1602
|
+
let label = "utf-8";
|
|
1603
|
+
if (head[0] === 255 && head[1] === 254) {
|
|
1604
|
+
label = "utf-16le";
|
|
1605
|
+
} else if (head[0] === 254 && head[1] === 255) {
|
|
1606
|
+
label = "utf-16be";
|
|
1607
|
+
}
|
|
1608
|
+
const body = stripLeadingComments(new TextDecoder(label).decode(head.subarray(0, HEAD_BYTES)), COMMENT_CHARS);
|
|
1609
|
+
if (body.trim().length === 0 || OTHER_FORMATS.some((r) => r.test(body))) {
|
|
1111
1610
|
return 0;
|
|
1112
1611
|
}
|
|
1113
1612
|
const newline = sniffNewline(body);
|
|
1114
|
-
const delimiter = sniffDelimiter(body, newline);
|
|
1613
|
+
const delimiter = sniffDelimiter(body, newline, DELIMITER_CANDIDATES, '"', false, true);
|
|
1115
1614
|
if (delimiter === null) {
|
|
1116
|
-
|
|
1615
|
+
const first = body.split(newline, 1)[0];
|
|
1616
|
+
return DELIMITER_CANDIDATES.some((d) => hasEndpointHeader(first, d)) ? 0.9 : 0;
|
|
1117
1617
|
}
|
|
1118
|
-
const
|
|
1119
|
-
const
|
|
1120
|
-
const names = headerNames(firstLine.replace(/\r$/, "").split(delimiter));
|
|
1618
|
+
const records = splitRecords(body, delimiter, '"', 2, false, true) ?? [];
|
|
1619
|
+
const names = headerNames(records[0] ?? []);
|
|
1121
1620
|
if (findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
|
|
1122
1621
|
return 0.9;
|
|
1123
1622
|
}
|
|
1124
|
-
if (findColumn(names, ID_NAMES) >= 0) {
|
|
1623
|
+
if (delimiter !== " " && findColumn(names, ID_NAMES) >= 0) {
|
|
1125
1624
|
return 0.6;
|
|
1126
1625
|
}
|
|
1127
|
-
|
|
1626
|
+
if (delimiter === " " && names.length > 3) {
|
|
1627
|
+
return 0;
|
|
1628
|
+
}
|
|
1629
|
+
return looksLikeProse(body, newline, delimiter) ? 0 : 0.3;
|
|
1630
|
+
}
|
|
1631
|
+
const SENTENCE = /[A-Za-z]{2}[.!?](?:\s|$)/;
|
|
1632
|
+
function looksLikeProse(body, newline, delimiter) {
|
|
1633
|
+
const lines2 = body.split(newline).filter((l) => l.trim() !== "");
|
|
1634
|
+
if (lines2.length < 2) {
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
const rows = splitRecords(body, delimiter, '"', 8, false, true) ?? [];
|
|
1638
|
+
return rows.some((row) => row.some((cell) => /\S\s+\S/.test(cell.trim()) && SENTENCE.test(cell.trim())));
|
|
1639
|
+
}
|
|
1640
|
+
function hasEndpointHeader(line, delimiter) {
|
|
1641
|
+
const names = headerNames(splitRecords(line, delimiter, '"', 1, false, true)?.[0] ?? []);
|
|
1642
|
+
return names.length > 1 && findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0;
|
|
1128
1643
|
}
|
|
1129
1644
|
async function importCsv(input, sink, options) {
|
|
1130
1645
|
const common = resolveImportOptions(options, { ids: "canonical", defaultDirected: true, weightFrom: "weight" });
|
|
@@ -1145,7 +1660,8 @@ async function importCsv(input, sink, options) {
|
|
|
1145
1660
|
coercer: new IdCoercer(common.ids),
|
|
1146
1661
|
resolver: new DirectionResolver(sink, report, common.onMixedDirection),
|
|
1147
1662
|
headerSet: false,
|
|
1148
|
-
commentDirected: null
|
|
1663
|
+
commentDirected: null,
|
|
1664
|
+
commentText: ""
|
|
1149
1665
|
};
|
|
1150
1666
|
if (csv.nodes !== null) {
|
|
1151
1667
|
await new TableReader(state, csv.nodes, "nodes", false).read();
|
|
@@ -1155,7 +1671,7 @@ async function importCsv(input, sink, options) {
|
|
|
1155
1671
|
report.warning(
|
|
1156
1672
|
"coercion",
|
|
1157
1673
|
ID_MERGED_CODE,
|
|
1158
|
-
`${state.coercer.mergeCount} id cell(
|
|
1674
|
+
`${state.coercer.mergeCount} id cell${plural(state.coercer.mergeCount)} merged into ids other cells already produced under ids: "number"`
|
|
1159
1675
|
);
|
|
1160
1676
|
}
|
|
1161
1677
|
throwIfAborted(common.signal);
|
|
@@ -1163,15 +1679,39 @@ async function importCsv(input, sink, options) {
|
|
|
1163
1679
|
}
|
|
1164
1680
|
const csvImporter = Object.freeze({
|
|
1165
1681
|
format: "csv",
|
|
1682
|
+
options: Object.freeze([
|
|
1683
|
+
"decimal",
|
|
1684
|
+
"delimiter",
|
|
1685
|
+
"header",
|
|
1686
|
+
"idColumn",
|
|
1687
|
+
"labelColumn",
|
|
1688
|
+
"nodes",
|
|
1689
|
+
"rowNumberIds",
|
|
1690
|
+
"sourceColumn",
|
|
1691
|
+
"table",
|
|
1692
|
+
"targetColumn",
|
|
1693
|
+
"typeColumn"
|
|
1694
|
+
]),
|
|
1166
1695
|
extensions: Object.freeze([".csv", ".tsv", ".edges", ".edgelist"]),
|
|
1167
1696
|
mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values", "text/plain"]),
|
|
1168
1697
|
sniff,
|
|
1169
1698
|
import: importCsv
|
|
1170
1699
|
});
|
|
1700
|
+
function nodesInput(value) {
|
|
1701
|
+
if (value === void 0) {
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
const object = value;
|
|
1705
|
+
const blob = typeof value === "object" && typeof object.stream === "function" && typeof object.size === "number";
|
|
1706
|
+
return blob ? value.stream() : value;
|
|
1707
|
+
}
|
|
1171
1708
|
const CSV_LOSS = Object.freeze({
|
|
1172
|
-
/**
|
|
1709
|
+
/**
|
|
1710
|
+
* Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
|
|
1711
|
+
* E_INVALID_ID.
|
|
1712
|
+
*/
|
|
1173
1713
|
ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
|
|
1174
|
-
/** Ids
|
|
1714
|
+
/** Ids that read back as a different type, such as the text "7" as the number 7. */
|
|
1175
1715
|
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
1176
1716
|
/**
|
|
1177
1717
|
* The generic dialect has no direction column; an undirected or mixed graph reads back as
|
|
@@ -1183,26 +1723,48 @@ const CSV_LOSS = Object.freeze({
|
|
|
1183
1723
|
MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
|
|
1184
1724
|
/** An attribute column named like a reserved header is not written. */
|
|
1185
1725
|
RESERVED_NAME: "W_CSV_RESERVED_NAME",
|
|
1186
|
-
/**
|
|
1726
|
+
/**
|
|
1727
|
+
* An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
|
|
1728
|
+
* reads back with that role.
|
|
1729
|
+
*/
|
|
1187
1730
|
ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
|
|
1188
|
-
/** A role column (id, label) whose name the importer does not
|
|
1731
|
+
/** A role column (id, label) whose name the importer does not recognize; the role is lost. */
|
|
1189
1732
|
ROLE_NAME: "W_CSV_ROLE_NAME",
|
|
1190
|
-
/**
|
|
1733
|
+
/** An id or label attribute that is not text reads back as text. */
|
|
1191
1734
|
TEXT_ROLE: "W_CSV_TEXT_ROLE",
|
|
1192
1735
|
/** NaN / Infinity in a numeric column read back as text. */
|
|
1193
1736
|
NONFINITE: "W_CSV_NONFINITE",
|
|
1194
|
-
/**
|
|
1737
|
+
/**
|
|
1738
|
+
* A text value that reads back as a number or a boolean, because the format does not record that it was text (the
|
|
1739
|
+
* text "42" reads back as the number 42).
|
|
1740
|
+
*/
|
|
1195
1741
|
TEXT_INFERRED: LOSS.TEXT_INFERRED,
|
|
1196
|
-
/**
|
|
1742
|
+
/**
|
|
1743
|
+
* A text attribute reads back as a dictionary attribute, or the reverse, because the importer chooses by how often
|
|
1744
|
+
* its values repeat. The values are the same.
|
|
1745
|
+
*/
|
|
1197
1746
|
STORAGE_CLASS_CHANGED: LOSS.STORAGE_CLASS,
|
|
1198
1747
|
/** Node attributes are written by a `table: "nodes"` export only. */
|
|
1199
1748
|
NODE_TABLE: "W_CSV_NODE_TABLE",
|
|
1200
|
-
/**
|
|
1749
|
+
/**
|
|
1750
|
+
* The edge table has no row for a node without edges, so isolated nodes are missing when the file is read back.
|
|
1751
|
+
* Write the node table too (`table: "nodes"`).
|
|
1752
|
+
*/
|
|
1201
1753
|
ISOLATED_NODES: "W_CSV_ISOLATED_NODES",
|
|
1202
|
-
/**
|
|
1754
|
+
/**
|
|
1755
|
+
* The edge table lists nodes in the order they first appear in an edge, so the node order changes when the file is
|
|
1756
|
+
* read back.
|
|
1757
|
+
*/
|
|
1203
1758
|
NODE_ORDER: "W_CSV_NODE_ORDER",
|
|
1204
1759
|
/** An adjacency table holds no edge column but the weight: edge ids, labels and attributes are not written. */
|
|
1205
|
-
EDGE_COLUMNS: "W_CSV_EDGE_COLUMNS"
|
|
1760
|
+
EDGE_COLUMNS: "W_CSV_EDGE_COLUMNS",
|
|
1761
|
+
/**
|
|
1762
|
+
* `header: false` with columns a file without a header cannot name: graph-io reads such a file by position
|
|
1763
|
+
* (source, target, weight, then columns it names column4, column5, ...), so these columns read back under other
|
|
1764
|
+
* names or in the wrong place (for example, an edge id in the weight's place). Write the header, or write only source,
|
|
1765
|
+
* target and weight with `dialect: "generic"`.
|
|
1766
|
+
*/
|
|
1767
|
+
HEADERLESS: "W_CSV_HEADERLESS"
|
|
1206
1768
|
});
|
|
1207
1769
|
const CSV_CAPABILITIES = capabilities({
|
|
1208
1770
|
mixedDirection: true,
|
|
@@ -1240,7 +1802,7 @@ const SKIPPED_ROLES = /* @__PURE__ */ new Set([
|
|
|
1240
1802
|
]);
|
|
1241
1803
|
function resolveCsvExportOptions(options) {
|
|
1242
1804
|
const o = options ?? {};
|
|
1243
|
-
if (o.dialect !== void 0 && o.dialect !== "gephi" && o.dialect !== "generic") {
|
|
1805
|
+
if (o.dialect !== void 0 && o.dialect !== "gephi" && o.dialect !== "generic" && !otherFormatDialect("csv", o.dialect)) {
|
|
1244
1806
|
throw new GraphFormatError("E_UNSUPPORTED", 'option dialect: expected "gephi" or "generic"', {
|
|
1245
1807
|
option: "dialect",
|
|
1246
1808
|
found: o.dialect
|
|
@@ -1278,7 +1840,8 @@ function resolveCsvExportOptions(options) {
|
|
|
1278
1840
|
});
|
|
1279
1841
|
}
|
|
1280
1842
|
return {
|
|
1281
|
-
|
|
1843
|
+
// another format's dialect (a JSON "d3" in a shared options object) leaves CSV's default
|
|
1844
|
+
dialect: DIALECTS[o.dialect === "generic" ? "generic" : "gephi"],
|
|
1282
1845
|
table: o.table ?? "edges",
|
|
1283
1846
|
delimiter: o.delimiter ?? ",",
|
|
1284
1847
|
newline: o.newline ?? "\n",
|
|
@@ -1338,6 +1901,18 @@ function cellText(column, row) {
|
|
|
1338
1901
|
}
|
|
1339
1902
|
}
|
|
1340
1903
|
}
|
|
1904
|
+
function nodeTableNote(names, table, note) {
|
|
1905
|
+
const written = names.length;
|
|
1906
|
+
if (written > 0) {
|
|
1907
|
+
const shown = names.slice(0, 3).map((n) => JSON.stringify(n)).join(", ") + (written > 3 ? ", ..." : "");
|
|
1908
|
+
note(
|
|
1909
|
+
CSV_LOSS.NODE_TABLE,
|
|
1910
|
+
`the ${table === "adjacency" ? "adjacency" : "edge"} table has no room for node attributes: ${written} node column${plural(written)} (${shown}) ${agree(written, "is", "are")} written only by a second export with table: "nodes"`,
|
|
1911
|
+
null,
|
|
1912
|
+
written
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1341
1916
|
function idNotes(snapshot, values, note) {
|
|
1342
1917
|
const { ids } = snapshot;
|
|
1343
1918
|
if (ids.kind === "mixed") {
|
|
@@ -1354,7 +1929,7 @@ function idNotes(snapshot, values, note) {
|
|
|
1354
1929
|
if (collisions > 0) {
|
|
1355
1930
|
note(
|
|
1356
1931
|
CSV_LOSS.ID_TEXT_COLLISION,
|
|
1357
|
-
`${collisions} node id(
|
|
1932
|
+
`${collisions} node id${plural(collisions)} ${agree(collisions, "shares", "share")} their text with another id (a number and a string); the save fails with E_INVALID_ID`,
|
|
1358
1933
|
null,
|
|
1359
1934
|
collisions
|
|
1360
1935
|
);
|
|
@@ -1371,7 +1946,7 @@ function idNotes(snapshot, values, note) {
|
|
|
1371
1946
|
if (changed > 0) {
|
|
1372
1947
|
note(
|
|
1373
1948
|
CSV_LOSS.ID_TEXT_TYPE,
|
|
1374
|
-
`${changed} node id(
|
|
1949
|
+
`${changed} node id${plural(changed)} ${agree(changed, "reads", "read")} back as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`,
|
|
1375
1950
|
null,
|
|
1376
1951
|
changed
|
|
1377
1952
|
);
|
|
@@ -1401,14 +1976,14 @@ function planExport(snapshot, options, values) {
|
|
|
1401
1976
|
if (!snapshot.directed) {
|
|
1402
1977
|
note(
|
|
1403
1978
|
CSV_LOSS.DIRECTION_DROPPED,
|
|
1404
|
-
`${where} has no direction column; ${snapshot.edgeCount} undirected edge(
|
|
1979
|
+
`${where} has no direction column; ${snapshot.edgeCount} undirected edge${plural(snapshot.edgeCount)} ${agree(snapshot.edgeCount, "reads", "read")} back as directed unless the importer is told otherwise`,
|
|
1405
1980
|
null,
|
|
1406
1981
|
snapshot.edgeCount
|
|
1407
1982
|
);
|
|
1408
1983
|
} else if (mixed > 0) {
|
|
1409
1984
|
note(
|
|
1410
1985
|
CSV_LOSS.DIRECTION_DROPPED,
|
|
1411
|
-
`${where} has no direction column; ${mixed} undirected edge(
|
|
1986
|
+
`${where} has no direction column; ${mixed} undirected edge${plural(mixed)} of a mixed graph read back as one directed edge each`,
|
|
1412
1987
|
null,
|
|
1413
1988
|
mixed
|
|
1414
1989
|
);
|
|
@@ -1424,7 +1999,7 @@ function planExport(snapshot, options, values) {
|
|
|
1424
1999
|
if (folding.mutualCount > 0) {
|
|
1425
2000
|
note(
|
|
1426
2001
|
CSV_LOSS.MUTUAL_EXPANDED,
|
|
1427
|
-
`${folding.mutualCount} mutual pair(
|
|
2002
|
+
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
|
|
1428
2003
|
null,
|
|
1429
2004
|
folding.mutualCount
|
|
1430
2005
|
);
|
|
@@ -1452,7 +2027,7 @@ function planExport(snapshot, options, values) {
|
|
|
1452
2027
|
if (dropped > 0) {
|
|
1453
2028
|
note(
|
|
1454
2029
|
CSV_LOSS.EDGE_COLUMNS,
|
|
1455
|
-
`${dropped} edge column(
|
|
2030
|
+
`${dropped} edge column${plural(dropped)} ${agree(dropped, "is", "are")} not written: an adjacency table holds the weight only`,
|
|
1456
2031
|
null,
|
|
1457
2032
|
dropped
|
|
1458
2033
|
);
|
|
@@ -1489,15 +2064,11 @@ function planExport(snapshot, options, values) {
|
|
|
1489
2064
|
}
|
|
1490
2065
|
}
|
|
1491
2066
|
} else {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
null,
|
|
1498
|
-
written
|
|
1499
|
-
);
|
|
1500
|
-
}
|
|
2067
|
+
nodeTableNote(
|
|
2068
|
+
[...nodeLabel === null ? [] : [nodeLabel.meta.name], ...nodeColumns.map((c) => c.column.meta.name)],
|
|
2069
|
+
csv.table,
|
|
2070
|
+
note
|
|
2071
|
+
);
|
|
1501
2072
|
}
|
|
1502
2073
|
return {
|
|
1503
2074
|
csv,
|
|
@@ -1541,7 +2112,7 @@ function noteNodeCoverage(snapshot, edgeRows, note) {
|
|
|
1541
2112
|
if (isolated > 0) {
|
|
1542
2113
|
note(
|
|
1543
2114
|
CSV_LOSS.ISOLATED_NODES,
|
|
1544
|
-
`${isolated} node(
|
|
2115
|
+
`${isolated} node${plural(isolated)} ${agree(isolated, "has", "have")} no edge and cannot be written by the edge table; write the node table (table: "nodes") to keep them`,
|
|
1545
2116
|
null,
|
|
1546
2117
|
isolated
|
|
1547
2118
|
);
|
|
@@ -1549,7 +2120,7 @@ function noteNodeCoverage(snapshot, edgeRows, note) {
|
|
|
1549
2120
|
if (reordered > 0) {
|
|
1550
2121
|
note(
|
|
1551
2122
|
CSV_LOSS.NODE_ORDER,
|
|
1552
|
-
`${reordered} node(
|
|
2123
|
+
`${reordered} node${plural(reordered)} ${agree(reordered, "is", "are")} first mentioned by an edge row out of index order; a re-import numbers nodes by first appearance`,
|
|
1553
2124
|
null,
|
|
1554
2125
|
reordered
|
|
1555
2126
|
);
|
|
@@ -1654,7 +2225,12 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
|
|
|
1654
2225
|
}
|
|
1655
2226
|
}
|
|
1656
2227
|
if (nonFinite > 0) {
|
|
1657
|
-
note(
|
|
2228
|
+
note(
|
|
2229
|
+
CSV_LOSS.NONFINITE,
|
|
2230
|
+
`${label}: ${nonFinite} non-finite value${plural(nonFinite)} ${agree(nonFinite, "reads", "read")} back as text`,
|
|
2231
|
+
name,
|
|
2232
|
+
nonFinite
|
|
2233
|
+
);
|
|
1658
2234
|
}
|
|
1659
2235
|
return;
|
|
1660
2236
|
}
|
|
@@ -1716,7 +2292,7 @@ function neighbourCell(id, weight) {
|
|
|
1716
2292
|
}
|
|
1717
2293
|
function* adjacencyLines(snapshot, plan) {
|
|
1718
2294
|
const { delimiter, newline } = plan.csv;
|
|
1719
|
-
const forced = (text) => `"${text.
|
|
2295
|
+
const forced = (text) => `"${text.replaceAll('"', '""')}"`;
|
|
1720
2296
|
const quote = (text) => DELIMITER_CANDIDATES.some((c) => text.includes(c)) ? forced(text) : quoteCsvCell(text, delimiter);
|
|
1721
2297
|
const first = (text) => /^[#%]/.test(text) ? forced(text) : quote(text);
|
|
1722
2298
|
const list = snapshot.edgeList();
|
|
@@ -1862,15 +2438,58 @@ function prepare(snapshot, options) {
|
|
|
1862
2438
|
}
|
|
1863
2439
|
return plan;
|
|
1864
2440
|
}
|
|
2441
|
+
function headerlessNotes(plan) {
|
|
2442
|
+
const { csv } = plan;
|
|
2443
|
+
if (csv.header || csv.table === "adjacency") {
|
|
2444
|
+
return [];
|
|
2445
|
+
}
|
|
2446
|
+
const names = csv.table === "nodes" ? headerlessNodeColumns(plan) : headerlessEdgeColumns(plan);
|
|
2447
|
+
if (names.length === 0) {
|
|
2448
|
+
return [];
|
|
2449
|
+
}
|
|
2450
|
+
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
2451
|
+
return [
|
|
2452
|
+
Object.freeze({
|
|
2453
|
+
code: CSV_LOSS.HEADERLESS,
|
|
2454
|
+
message: `without a header the file is read back by position (${csv.table === "nodes" ? "id" : "source, target, weight"}, then unnamed columns), so ${names.length === 1 ? "the column" : "the columns"} ${shown} ${agree(names.length, "reads", "read")} back unnamed or in another column's place; write the header, or for an edge list pass dialect: "generic" and write only source, target and weight`,
|
|
2455
|
+
column: null,
|
|
2456
|
+
count: names.length
|
|
2457
|
+
})
|
|
2458
|
+
];
|
|
2459
|
+
}
|
|
2460
|
+
function headerlessNodeColumns(plan) {
|
|
2461
|
+
const label = plan.nodeLabel === null ? [] : [plan.nodeLabel.meta.name];
|
|
2462
|
+
return [...label, ...plan.nodeColumns.map((c) => c.header)];
|
|
2463
|
+
}
|
|
2464
|
+
function headerlessEdgeColumns(plan) {
|
|
2465
|
+
const { dialect } = plan.csv;
|
|
2466
|
+
const names = [];
|
|
2467
|
+
if (dialect.type !== null) {
|
|
2468
|
+
names.push(dialect.type);
|
|
2469
|
+
}
|
|
2470
|
+
if (plan.edgeId !== null) {
|
|
2471
|
+
names.push(plan.edgeId.meta.name);
|
|
2472
|
+
}
|
|
2473
|
+
if (plan.edgeLabel !== null) {
|
|
2474
|
+
names.push(plan.edgeLabel.meta.name);
|
|
2475
|
+
}
|
|
2476
|
+
if (plan.weights.weighted && names.length > 0) {
|
|
2477
|
+
names.push(dialect.weight);
|
|
2478
|
+
}
|
|
2479
|
+
names.push(...plan.edgeColumns.map((c) => c.header));
|
|
2480
|
+
return names;
|
|
2481
|
+
}
|
|
1865
2482
|
function check(snapshot, options) {
|
|
1866
2483
|
const plan = planExport(snapshot, options, true);
|
|
1867
2484
|
return Object.freeze([
|
|
1868
2485
|
...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, { roles: KEPT_ROLES }),
|
|
1869
|
-
...plan.notes
|
|
2486
|
+
...plan.notes,
|
|
2487
|
+
...headerlessNotes(plan)
|
|
1870
2488
|
]);
|
|
1871
2489
|
}
|
|
1872
2490
|
const csvExporter = Object.freeze({
|
|
1873
2491
|
format: "csv",
|
|
2492
|
+
options: Object.freeze(["delimiter", "dialect", "header", "newline", "table"]),
|
|
1874
2493
|
capabilities: CSV_CAPABILITIES,
|
|
1875
2494
|
check,
|
|
1876
2495
|
export(snapshot, options) {
|
|
@@ -1881,21 +2500,37 @@ const csvExporter = Object.freeze({
|
|
|
1881
2500
|
}
|
|
1882
2501
|
});
|
|
1883
2502
|
const CSV_ISSUE = Object.freeze({
|
|
1884
|
-
|
|
2503
|
+
...INPUT_ISSUE,
|
|
2504
|
+
...ELEMENT_ISSUE,
|
|
2505
|
+
/** An integer beyond 2^53 was stored as the nearest 64-bit float; reported once per column. */
|
|
2506
|
+
PRECISION: PRECISION_CODE,
|
|
2507
|
+
/** The input is empty. The import stops. */
|
|
1885
2508
|
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
1886
|
-
/** The input
|
|
2509
|
+
/** The input is not valid UTF-8. The import stops. */
|
|
1887
2510
|
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
1888
|
-
/**
|
|
2511
|
+
/**
|
|
2512
|
+
* Some bytes are not valid in the encoding that was chosen (by a byte order mark, the file's declaration or the
|
|
2513
|
+
* `encoding` option). The import stops.
|
|
2514
|
+
*/
|
|
1889
2515
|
INVALID_ENCODING: INVALID_ENCODING_CODE,
|
|
1890
2516
|
/** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
|
|
1891
2517
|
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
1892
2518
|
/** A declared encoding the platform cannot decode was ignored. */
|
|
1893
2519
|
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
1894
|
-
/**
|
|
2520
|
+
/**
|
|
2521
|
+
* The header names no source and target columns and no id column. Pass `sourceColumn` and `targetColumn` to name
|
|
2522
|
+
* the columns. The import stops.
|
|
2523
|
+
*/
|
|
1895
2524
|
NO_ENDPOINT_COLUMNS: NO_ENDPOINT_COLUMNS_CODE,
|
|
1896
|
-
/**
|
|
2525
|
+
/**
|
|
2526
|
+
* The node table has no id column. Pass `idColumn` to name it, or `rowNumberIds: true` to number the rows. The
|
|
2527
|
+
* import stops.
|
|
2528
|
+
*/
|
|
1897
2529
|
NO_ID_COLUMN: NO_ID_COLUMN_CODE,
|
|
1898
|
-
/**
|
|
2530
|
+
/**
|
|
2531
|
+
* A row with a different number of fields than the header; the row is skipped. An adjacency list read without
|
|
2532
|
+
* `table: "adjacency"` gives one per long row, and the first one's message suggests the option.
|
|
2533
|
+
*/
|
|
1899
2534
|
FIELD_COUNT: FIELD_COUNT_CODE,
|
|
1900
2535
|
/** An edge row with a blank source or target. */
|
|
1901
2536
|
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
@@ -1903,9 +2538,12 @@ const CSV_ISSUE = Object.freeze({
|
|
|
1903
2538
|
MISSING_ID: MISSING_ID_CODE,
|
|
1904
2539
|
/** A Type cell outside Directed / Undirected / Mutual. */
|
|
1905
2540
|
BAD_TYPE: BAD_TYPE_CODE,
|
|
1906
|
-
/**
|
|
2541
|
+
/** A quoted field is never closed. The import stops. */
|
|
1907
2542
|
UNCLOSED_QUOTE: UNCLOSED_QUOTE_CODE,
|
|
1908
|
-
/**
|
|
2543
|
+
/**
|
|
2544
|
+
* There is text after a closing quote, such as `"a"b`. Inside a quoted field, write a quote as two quotes. The
|
|
2545
|
+
* import stops.
|
|
2546
|
+
*/
|
|
1909
2547
|
QUOTE: BAD_QUOTE_CODE,
|
|
1910
2548
|
/** A header and no data rows. */
|
|
1911
2549
|
NO_DATA_ROWS: NO_DATA_ROWS_CODE,
|
|
@@ -1913,26 +2551,78 @@ const CSV_ISSUE = Object.freeze({
|
|
|
1913
2551
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
1914
2552
|
/** An edge row repeating an edge id (skipped). */
|
|
1915
2553
|
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
1916
|
-
/** Two id cells
|
|
2554
|
+
/** Two different id cells became the same number because `ids` is "number", so their nodes were merged. */
|
|
1917
2555
|
ID_MERGED: ID_MERGED_CODE,
|
|
1918
2556
|
/** An explicitly named weight column the file does not have. */
|
|
1919
2557
|
COLUMN_MISSING: COLUMN_MISSING_CODE,
|
|
1920
|
-
/**
|
|
2558
|
+
/**
|
|
2559
|
+
* You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
|
|
2560
|
+
* a plain attribute.
|
|
2561
|
+
*/
|
|
1921
2562
|
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
1922
|
-
/**
|
|
2563
|
+
/**
|
|
2564
|
+
* An attribute was renamed `<name>#<suffix>` because another attribute already has its name, for example a
|
|
2565
|
+
* repeated column header.
|
|
2566
|
+
*/
|
|
1923
2567
|
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
1924
|
-
/**
|
|
2568
|
+
/** You set an option this format does not use; it had no effect. The message names the option. */
|
|
1925
2569
|
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
1926
|
-
/**
|
|
2570
|
+
/**
|
|
2571
|
+
* You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
|
|
2572
|
+
* `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
|
|
2573
|
+
*/
|
|
1927
2574
|
SINK_OPTION: SINK_OPTION_CODE,
|
|
1928
|
-
/**
|
|
2575
|
+
/**
|
|
2576
|
+
* You read into a graph builder whose direction is already set, or which already holds edges, so the file is read
|
|
2577
|
+
* with the builder's direction instead of its own.
|
|
2578
|
+
*/
|
|
1929
2579
|
DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
|
|
1930
|
-
/** Edges
|
|
2580
|
+
/** Edges of the other direction were read with the direction `onMixedDirection` chose. */
|
|
1931
2581
|
DIRECTION_FORCED: DIRECTION_FORCED_CODE,
|
|
1932
|
-
/**
|
|
2582
|
+
/**
|
|
2583
|
+
* The graph has both directed and undirected edges and `onMixedDirection` is "error". An import stops; a save to a
|
|
2584
|
+
* format that holds one direction per file fails with E_DIRECTED. Pass "directed" or "undirected" to read or write
|
|
2585
|
+
* it anyway.
|
|
2586
|
+
*/
|
|
1933
2587
|
MIXED_DIRECTION: MIXED_DIRECTION_CODE,
|
|
1934
|
-
/**
|
|
1935
|
-
|
|
2588
|
+
/**
|
|
2589
|
+
* Your graph builder cannot change an attribute's type after its first value, so a text column keeps the type of
|
|
2590
|
+
* its first values.
|
|
2591
|
+
*/
|
|
2592
|
+
WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE,
|
|
2593
|
+
/** The input starts like another format (XML or HTML, JSON, GML, DOT or Pajek), not CSV. The import stops. */
|
|
2594
|
+
OTHER_FORMAT: OTHER_FORMAT_CODE,
|
|
2595
|
+
/**
|
|
2596
|
+
* A quote inside a field that does not start with a quote is kept as part of the text. Reported once per import.
|
|
2597
|
+
*/
|
|
2598
|
+
STRAY_QUOTE: STRAY_QUOTE_CODE,
|
|
2599
|
+
/** A leading comment's direction disagrees with defaultDirected or an earlier comment. */
|
|
2600
|
+
COMMENT_DIRECTION: COMMENT_DIRECTION_CODE,
|
|
2601
|
+
/**
|
|
2602
|
+
* A column named like `Type` holds direction words (Directed, Undirected), but graph-io reads direction only from
|
|
2603
|
+
* a `Type` column beside `Source` and `Target`, or from the `typeColumn` option. Reported once per import.
|
|
2604
|
+
*/
|
|
2605
|
+
TYPE_COLUMN_IGNORED: TYPE_COLUMN_IGNORED_CODE,
|
|
2606
|
+
/** The header ends in a delimiter; rows without the empty last cell are complete. */
|
|
2607
|
+
TRAILING_HEADER_DELIMITER: TRAILING_HEADER_DELIMITER_CODE,
|
|
2608
|
+
/** Every row is one cell another delimiter would split (a likely wrong delimiter option). */
|
|
2609
|
+
SINGLE_COLUMN: SINGLE_COLUMN_CODE,
|
|
2610
|
+
/** Several header columns name one role; the one not chosen is a plain attribute. */
|
|
2611
|
+
AMBIGUOUS_COLUMN: AMBIGUOUS_COLUMN_CODE,
|
|
2612
|
+
/**
|
|
2613
|
+
* An id that is not quoted has spaces at its start or end; they are kept, so it is a different id from the trimmed
|
|
2614
|
+
* text. Reported once per import.
|
|
2615
|
+
*/
|
|
2616
|
+
PADDED_ID: PADDED_ID_CODE,
|
|
2617
|
+
/** A leading # / % line skipped as a comment has the fields of a record. */
|
|
2618
|
+
COMMENT_LIKE_RECORD: COMMENT_LIKE_RECORD_CODE,
|
|
2619
|
+
/** Every data row ends in one extra delimiter; the empty last cell is dropped. Reported once per import. */
|
|
2620
|
+
TRAILING_DELIMITER: TRAILING_DELIMITER_CODE,
|
|
2621
|
+
/**
|
|
2622
|
+
* In a file without a header, the third column holds text, so it is read as an attribute, not as the edge weight.
|
|
2623
|
+
* Reported once per import.
|
|
2624
|
+
*/
|
|
2625
|
+
WEIGHT_AS_ATTRIBUTE: WEIGHT_AS_ATTRIBUTE_CODE
|
|
1936
2626
|
});
|
|
1937
2627
|
export {
|
|
1938
2628
|
CSV_CAPABILITIES,
|