@graphty/graph-io 0.3.25 → 0.3.26
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-BbJI55dx.js +56 -0
- package/dist/chunks/choose-BbJI55dx.js.map +1 -0
- package/dist/chunks/constants-BCsFsMj4.js +231 -0
- package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
- package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
- package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
- package/dist/chunks/export-BTI4yQgy.js +4333 -0
- package/dist/chunks/export-BTI4yQgy.js.map +1 -0
- package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
- package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
- package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
- package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
- package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
- package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
- package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
- package/dist/chunks/exporter-tne475KK.js.map +1 -0
- package/dist/chunks/importer-90YWhJoF.js +2811 -0
- package/dist/chunks/importer-90YWhJoF.js.map +1 -0
- package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
- package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
- package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
- package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
- package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
- package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
- package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
- package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
- package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
- package/dist/chunks/importer-DfVu6j59.js.map +1 -0
- package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
- package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
- package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
- package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
- package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
- package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
- package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
- package/dist/chunks/records-CQXT__mY.js.map +1 -0
- package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
- package/dist/chunks/text-DmcUs5_Q.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 +951 -261
- 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 +342 -85
- 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 +564 -157
- 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 +19 -1
- 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 +33 -17
- 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 +868 -188
- 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 +2 -0
- 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 +400 -83
- 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 +145 -32
- 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 +680 -285
- 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 +226 -79
- 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 +98 -24
- 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 +108 -26
- 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 +238 -85
- 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 +66 -34
- 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 +183 -51
- 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 +20 -21
- 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 +7 -2
- 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 +38 -4
- package/src/common/direction.ts +78 -51
- package/src/common/escape.ts +33 -17
- 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 +1034 -198
- package/src/common/line-format.ts +249 -0
- package/src/common/lists.ts +6 -1
- 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 +477 -88
- package/src/common/zip.ts +169 -7
- package/src/formats/csv/exporter.ts +201 -46
- 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 +888 -300
- package/src/formats/cx/index.ts +4 -3
- package/src/formats/cx2/exporter.ts +61 -338
- package/src/formats/cx2/importer.ts +317 -86
- 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 +139 -31
- 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 +111 -26
- 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 +320 -101
- 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 +89 -41
- package/src/formats/pajek/importer.ts +407 -93
- package/src/formats/pajek/index.ts +2 -1
- package/src/formats/pajek/syntax.ts +213 -52
- 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 +35 -27
- package/src/formats/xgmml/importer.ts +64 -86
- package/src/formats/xgmml/index.ts +1 -0
- package/src/formats/xgmml/values.ts +11 -3
- package/src/index.ts +81 -11
- package/src/registry.ts +1121 -135
- package/src/sniff.ts +163 -32
- package/src/types.ts +385 -114
- package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
- package/dist/chunks/export-Bh60Dv-n.js +0 -582
- package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
- package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
- package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
- package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
- package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
- package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
- package/dist/chunks/importer-DET1tg6s.js.map +0 -1
- package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
- package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
- package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
- package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
- package/dist/chunks/importer-eUREhBGj.js.map +0 -1
- package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
- package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
- package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
- package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
- package/dist/chunks/ontology-BnrJ4I98.js +0 -113
- package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
- package/dist/chunks/records-dbRkxwaq.js.map +0 -1
- package/dist/chunks/report-B1z4WT9e.js +0 -2372
- package/dist/chunks/report-B1z4WT9e.js.map +0 -1
- package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
- package/dist/chunks/weights-Dzba96G3.js +0 -176
- package/dist/chunks/weights-Dzba96G3.js.map +0 -1
- package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The CSV / TSV importer (design sections 8.4 and 8.6; research note 07 section 2.6): a streaming
|
|
3
3
|
* edge-list reader for the generic (`source,target[,weight,...]`), Gephi (`Source,Target,Type,Id,
|
|
4
4
|
* Label,Weight,...`) and headerless (`u v [w]`) dialects, with an optional node table merged by id,
|
|
5
|
-
* and, with `table: "adjacency"`, an adjacency table (`node,
|
|
5
|
+
* and, with `table: "adjacency"`, an adjacency table (`node,neighbor[:weight],...`).
|
|
6
6
|
*
|
|
7
7
|
* - The delimiter is sniffed from a preview unless given; LF, CRLF and lone-CR files all read.
|
|
8
8
|
* - The first row is a header when it holds a known column name or when it is all text over a
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* all-text column of low cardinality becomes a dict (design section 5.4); an `id` column of the
|
|
22
22
|
* edge table is the edge id (role id, unique); a `label` column is the label (role label).
|
|
23
23
|
* - An adjacency table has no header (unless `header: true`, which skips the first row): each row
|
|
24
|
-
* is a node followed by its
|
|
24
|
+
* is a node followed by its neighbors, one edge per neighbor in row order. A neighbor cell
|
|
25
25
|
* `id:weight` carries the edge's weight when the text after its LAST colon is a number; a cell
|
|
26
26
|
* ending in a bare colon (`a:1:`) is the id before it with no weight; any other cell is the id
|
|
27
27
|
* as written (`http://x`). A row holding only its node adds an isolated node. An empty adjacency
|
|
@@ -38,42 +38,138 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
|
|
40
40
|
import { declareResolved, RENAMED_CODE, uniqueColumnName } from "../../common/attributes.js";
|
|
41
|
-
import { DUPLICATE_EDGE_ID_CODE as SHARED_DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE as SHARED_DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE as SHARED_EMPTY_INPUT_CODE, ID_MERGED_CODE as SHARED_ID_MERGED_CODE, MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE, MISSING_ID_CODE as SHARED_MISSING_ID_CODE, ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE, } from "../../common/codes.js";
|
|
41
|
+
import { DUPLICATE_EDGE_ID_CODE as SHARED_DUPLICATE_EDGE_ID_CODE, DUPLICATE_NODE_CODE as SHARED_DUPLICATE_NODE_CODE, EMPTY_INPUT_CODE as SHARED_EMPTY_INPUT_CODE, ID_MERGED_CODE as SHARED_ID_MERGED_CODE, MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE, MISSING_ID_CODE as SHARED_MISSING_ID_CODE, OPTION_IGNORED_CODE, ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE, } from "../../common/codes.js";
|
|
42
42
|
import { DirectionResolver } from "../../common/direction.js";
|
|
43
43
|
import { IdCoercer } from "../../common/ids.js";
|
|
44
44
|
import { throwIfAborted } from "../../common/input.js";
|
|
45
45
|
import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
|
|
46
|
+
import { plural } from "../../common/plural.js";
|
|
46
47
|
import { ImportReportBuilder } from "../../common/report.js";
|
|
47
48
|
import { parseWeightText } from "../../common/weights.js";
|
|
48
49
|
import { EDGE_ID_NAMES, findColumn, headerNames, ID_NAMES, LABEL_NAMES, looksLikeHeader, positionalNames, resolveColumnRef, SOURCE_NAMES, TARGET_NAMES, TYPE_NAME, } from "./header.js";
|
|
49
|
-
import { CsvRecordReader, sniffDelimiter, sniffNewline } from "./records.js";
|
|
50
|
+
import { checkRecordSyntax, CsvRecordReader, DELIMITER_CANDIDATES, sniffDelimiter, sniffNewline, splitRecords, stripLeadingComments, } from "./records.js";
|
|
50
51
|
import { InferredColumn } from "./values.js";
|
|
51
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The input holds no header row at all.
|
|
54
|
+
* @category Issue and loss codes
|
|
55
|
+
*/
|
|
52
56
|
export const EMPTY_INPUT_CODE = SHARED_EMPTY_INPUT_CODE;
|
|
53
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The header names no source / target (or, for a node table, no id) column.
|
|
59
|
+
* @category Issue and loss codes
|
|
60
|
+
*/
|
|
54
61
|
export const NO_ENDPOINT_COLUMNS_CODE = "E_CSV_NO_ENDPOINT_COLUMNS";
|
|
55
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* A node table without an id column.
|
|
64
|
+
* @category Issue and loss codes
|
|
65
|
+
*/
|
|
56
66
|
export const NO_ID_COLUMN_CODE = "E_CSV_NO_ID_COLUMN";
|
|
57
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* A row with a different number of fields than the header.
|
|
69
|
+
* @category Issue and loss codes
|
|
70
|
+
*/
|
|
58
71
|
export const FIELD_COUNT_CODE = "E_CSV_FIELD_COUNT";
|
|
59
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* An edge row with a blank source or target cell.
|
|
74
|
+
* @category Issue and loss codes
|
|
75
|
+
*/
|
|
60
76
|
export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
|
|
61
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* A node row with a blank id cell.
|
|
79
|
+
* @category Issue and loss codes
|
|
80
|
+
*/
|
|
62
81
|
export const MISSING_ID_CODE = SHARED_MISSING_ID_CODE;
|
|
63
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* A Type cell that is not Directed, Undirected or Mutual.
|
|
84
|
+
* @category Issue and loss codes
|
|
85
|
+
*/
|
|
64
86
|
export const BAD_TYPE_CODE = "E_CSV_BAD_TYPE";
|
|
65
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* The table has a header and no data rows.
|
|
89
|
+
* @category Issue and loss codes
|
|
90
|
+
*/
|
|
66
91
|
export const NO_DATA_ROWS_CODE = "W_CSV_NO_DATA_ROWS";
|
|
67
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* A node table row repeats an id; its attributes overwrite the earlier row's.
|
|
94
|
+
* @category Issue and loss codes
|
|
95
|
+
*/
|
|
68
96
|
export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
|
|
69
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Two different id cells became the same number because `ids` is "number", so their nodes were merged.
|
|
99
|
+
* @category Issue and loss codes
|
|
100
|
+
*/
|
|
70
101
|
export const ID_MERGED_CODE = SHARED_ID_MERGED_CODE;
|
|
71
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* An explicitly named weight column the file does not have.
|
|
104
|
+
* @category Issue and loss codes
|
|
105
|
+
*/
|
|
72
106
|
export const COLUMN_MISSING_CODE = "W_CSV_COLUMN_MISSING";
|
|
73
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* You read into a graph builder that already has an id or label attribute, so this file's one is kept without its
|
|
109
|
+
* role.
|
|
110
|
+
* @category Issue and loss codes
|
|
111
|
+
*/
|
|
74
112
|
export const ROLE_TAKEN_CODE = SHARED_ROLE_TAKEN_CODE;
|
|
75
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* A repeated edge id (the column is unique); the edge is skipped.
|
|
115
|
+
* @category Issue and loss codes
|
|
116
|
+
*/
|
|
76
117
|
export const DUPLICATE_EDGE_ID_CODE = SHARED_DUPLICATE_EDGE_ID_CODE;
|
|
118
|
+
/**
|
|
119
|
+
* The input starts like another format (XML or HTML, JSON, GML, DOT or Pajek), not CSV. The import stops.
|
|
120
|
+
* @category Issue and loss codes
|
|
121
|
+
*/
|
|
122
|
+
export const OTHER_FORMAT_CODE = "E_CSV_OTHER_FORMAT";
|
|
123
|
+
/**
|
|
124
|
+
* A quote inside an unquoted field (RFC 4180 forbids it); the quote is kept as text.
|
|
125
|
+
* @category Issue and loss codes
|
|
126
|
+
*/
|
|
127
|
+
export const STRAY_QUOTE_CODE = "W_CSV_STRAY_QUOTE";
|
|
128
|
+
/**
|
|
129
|
+
* A leading comment's direction disagrees with defaultDirected or with an earlier comment.
|
|
130
|
+
* @category Issue and loss codes
|
|
131
|
+
*/
|
|
132
|
+
export const COMMENT_DIRECTION_CODE = "W_CSV_COMMENT_DIRECTION";
|
|
133
|
+
/**
|
|
134
|
+
* A column named like Gephi's Type holds direction words but is read as a plain attribute.
|
|
135
|
+
* @category Issue and loss codes
|
|
136
|
+
*/
|
|
137
|
+
export const TYPE_COLUMN_IGNORED_CODE = "W_CSV_TYPE_COLUMN_IGNORED";
|
|
138
|
+
/**
|
|
139
|
+
* The header ends in a delimiter; rows without the empty last cell are read as complete.
|
|
140
|
+
* @category Issue and loss codes
|
|
141
|
+
*/
|
|
142
|
+
export const TRAILING_HEADER_DELIMITER_CODE = "W_CSV_TRAILING_HEADER_DELIMITER";
|
|
143
|
+
/**
|
|
144
|
+
* Every row is one cell that another delimiter would split (a likely wrong delimiter).
|
|
145
|
+
* @category Issue and loss codes
|
|
146
|
+
*/
|
|
147
|
+
export const SINGLE_COLUMN_CODE = "W_CSV_SINGLE_COLUMN";
|
|
148
|
+
/**
|
|
149
|
+
* Several header columns name the same role; the one not chosen is a plain attribute.
|
|
150
|
+
* @category Issue and loss codes
|
|
151
|
+
*/
|
|
152
|
+
export const AMBIGUOUS_COLUMN_CODE = "W_CSV_AMBIGUOUS_COLUMN";
|
|
153
|
+
/**
|
|
154
|
+
* An unquoted id with leading or trailing whitespace (kept, RFC 4180), distinct from the bare id.
|
|
155
|
+
* @category Issue and loss codes
|
|
156
|
+
*/
|
|
157
|
+
export const PADDED_ID_CODE = "W_CSV_PADDED_ID";
|
|
158
|
+
/**
|
|
159
|
+
* A leading `#` / `%` line skipped as a comment has the fields of a record.
|
|
160
|
+
* @category Issue and loss codes
|
|
161
|
+
*/
|
|
162
|
+
export const COMMENT_LIKE_RECORD_CODE = "W_CSV_COMMENT_LIKE_RECORD";
|
|
163
|
+
/**
|
|
164
|
+
* Data rows end in one extra empty cell (a trailing delimiter); the cell is dropped.
|
|
165
|
+
* @category Issue and loss codes
|
|
166
|
+
*/
|
|
167
|
+
export const TRAILING_DELIMITER_CODE = "W_CSV_TRAILING_DELIMITER";
|
|
168
|
+
/**
|
|
169
|
+
* A headerless three-column table's third column holds text, so it is an attribute, not the weight.
|
|
170
|
+
* @category Issue and loss codes
|
|
171
|
+
*/
|
|
172
|
+
export const WEIGHT_AS_ATTRIBUTE_CODE = "W_CSV_WEIGHT_AS_ATTRIBUTE";
|
|
77
173
|
const TABLE_MODES = new Set(["edges", "nodes", "adjacency", "auto"]);
|
|
78
174
|
/** The common options an edge-table import reads (the rest is reported by reportUnusedOptions). */
|
|
79
175
|
const USED_OPTIONS = new Set([
|
|
@@ -94,28 +190,45 @@ const USED_OPTIONS_WITH_NODES = new Set([
|
|
|
94
190
|
...USED_OPTIONS,
|
|
95
191
|
"nodeIdFrom",
|
|
96
192
|
]);
|
|
97
|
-
const BAD_DELIMITERS = new Set(['"', "\n", "\r"]);
|
|
98
193
|
/** The comment characters of the SNAP (`#`) and KONECT (`%`) headers (research note 07 section 2.6). */
|
|
99
194
|
const COMMENT_CHARS = Object.freeze(["#", "%"]);
|
|
195
|
+
/** A number written with a decimal comma: digits, one comma, digits, an optional exponent. */
|
|
196
|
+
const DECIMAL_COMMA = /^\s*[+-]?\d+,\d+(?:[eE][+-]?\d+)?\s*$/;
|
|
100
197
|
/**
|
|
101
|
-
* The direction a SNAP or KONECT comment
|
|
198
|
+
* The direction a SNAP or KONECT comment line declares (research note 07 section 2.6): SNAP
|
|
102
199
|
* pages write `# Directed graph` / `# Undirected graph`, KONECT's first line is `% sym` (undirected),
|
|
103
|
-
* `% asym` (directed) or `% bip` (bipartite, undirected).
|
|
104
|
-
*
|
|
105
|
-
* @
|
|
200
|
+
* `% asym` (directed) or `% bip` (bipartite, undirected). Only whole words count, so
|
|
201
|
+
* `% symbols` or `# bipartite-ish` declare nothing.
|
|
202
|
+
* @param comment - a leading comment line
|
|
203
|
+
* @returns true / false when the line declares the direction, null otherwise
|
|
106
204
|
*/
|
|
107
|
-
function commentDirection(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
205
|
+
function commentDirection(comment) {
|
|
206
|
+
const text = comment.slice(1).trim().toLowerCase();
|
|
207
|
+
if (/^(?:directed graph|asym)\b/.test(text)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (/^(?:undirected graph|sym|bip)\b/.test(text)) {
|
|
211
|
+
return false;
|
|
116
212
|
}
|
|
117
213
|
return null;
|
|
118
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* The name of a direction, for messages.
|
|
217
|
+
* @param directed - the direction
|
|
218
|
+
* @returns "directed" or "undirected"
|
|
219
|
+
*/
|
|
220
|
+
function directionName(directed) {
|
|
221
|
+
return directed ? "directed" : "undirected";
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Whether an unquoted id cell has leading or trailing whitespace (kept as written, RFC 4180).
|
|
225
|
+
* @param text - the cell text
|
|
226
|
+
* @param quoted - whether the cell was quoted
|
|
227
|
+
* @returns true when padded
|
|
228
|
+
*/
|
|
229
|
+
function isPadded(text, quoted) {
|
|
230
|
+
return quoted !== true && text.trim().length !== text.length;
|
|
231
|
+
}
|
|
119
232
|
/**
|
|
120
233
|
* Apply the defaults of the CSV options and check every value.
|
|
121
234
|
* @param options - the caller's options
|
|
@@ -129,12 +242,22 @@ function resolveCsvOptions(options) {
|
|
|
129
242
|
found: o.delimiter,
|
|
130
243
|
});
|
|
131
244
|
}
|
|
132
|
-
if (o.
|
|
133
|
-
throw new GraphFormatError("E_UNSUPPORTED",
|
|
134
|
-
option: "
|
|
135
|
-
found: o.
|
|
245
|
+
if (o.decimal !== undefined && o.decimal !== "." && o.decimal !== ",") {
|
|
246
|
+
throw new GraphFormatError("E_UNSUPPORTED", 'option decimal: expected "." or ","', {
|
|
247
|
+
option: "decimal",
|
|
248
|
+
found: o.decimal,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (o.decimal === "," && o.delimiter === ",") {
|
|
252
|
+
throw new GraphFormatError("E_UNSUPPORTED", 'options delimiter and decimal are both ","; pass delimiter: ";"', {
|
|
253
|
+
option: "decimal",
|
|
254
|
+
found: o.decimal,
|
|
136
255
|
});
|
|
137
256
|
}
|
|
257
|
+
if (o.delimiter !== undefined) {
|
|
258
|
+
// one character, not a line break, not the quote
|
|
259
|
+
checkRecordSyntax({ delimiter: o.delimiter, quote: '"' });
|
|
260
|
+
}
|
|
138
261
|
if (o.header !== undefined && o.header !== "auto" && typeof o.header !== "boolean") {
|
|
139
262
|
throw new GraphFormatError("E_UNSUPPORTED", 'option header: expected true, false or "auto"', {
|
|
140
263
|
option: "header",
|
|
@@ -147,14 +270,14 @@ function resolveCsvOptions(options) {
|
|
|
147
270
|
found: o.table,
|
|
148
271
|
});
|
|
149
272
|
}
|
|
150
|
-
for (const name of ["sourceColumn", "targetColumn", "idColumn"]) {
|
|
273
|
+
for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"]) {
|
|
151
274
|
checkColumnRef(name, o[name]);
|
|
152
275
|
}
|
|
153
276
|
if (o.typeColumn !== null) {
|
|
154
277
|
checkColumnRef("typeColumn", o.typeColumn);
|
|
155
278
|
}
|
|
156
279
|
if (o.table === "adjacency") {
|
|
157
|
-
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn"]) {
|
|
280
|
+
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"]) {
|
|
158
281
|
if (o[name] !== undefined) {
|
|
159
282
|
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
|
|
160
283
|
option: name,
|
|
@@ -177,14 +300,17 @@ function resolveCsvOptions(options) {
|
|
|
177
300
|
}
|
|
178
301
|
return {
|
|
179
302
|
delimiter: o.delimiter ?? null,
|
|
303
|
+
decimalComma: o.decimal === ",",
|
|
180
304
|
header: o.header ?? "auto",
|
|
181
305
|
table: o.table ?? "auto",
|
|
182
306
|
sourceColumn: o.sourceColumn ?? null,
|
|
183
307
|
targetColumn: o.targetColumn ?? null,
|
|
184
308
|
typeColumn: o.typeColumn,
|
|
185
309
|
idColumn: o.idColumn ?? null,
|
|
186
|
-
|
|
310
|
+
labelColumn: o.labelColumn ?? null,
|
|
311
|
+
nodes: nodesInput(o.nodes),
|
|
187
312
|
rowNumberIds: o.rowNumberIds ?? false,
|
|
313
|
+
explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
|
|
188
314
|
};
|
|
189
315
|
}
|
|
190
316
|
/**
|
|
@@ -301,11 +427,12 @@ function parseKind(text) {
|
|
|
301
427
|
}
|
|
302
428
|
}
|
|
303
429
|
/**
|
|
304
|
-
* Split an adjacency
|
|
430
|
+
* Split an adjacency neighbor cell into its id and weight: the text after the LAST colon is the
|
|
305
431
|
* weight when it is a number, nothing when it is empty (`a:1:` is the id `a:1`, unweighted, the
|
|
306
432
|
* form the exporter writes for an id containing a colon), and part of the id otherwise.
|
|
307
433
|
* @param text - the cell text
|
|
308
434
|
* @returns the id text and the weight (undefined when the cell has none)
|
|
435
|
+
* @category Plugin helpers
|
|
309
436
|
*/
|
|
310
437
|
export function splitNeighbour(text) {
|
|
311
438
|
const colon = text.lastIndexOf(":");
|
|
@@ -351,6 +478,8 @@ class TableReader {
|
|
|
351
478
|
this.labelHandle = INVALID_INDEX;
|
|
352
479
|
this.dataRows = 0;
|
|
353
480
|
this.nodeOrdinal = 0;
|
|
481
|
+
/** Whether a field-count error has already suggested `table: "adjacency"`. */
|
|
482
|
+
this.adjacencyHinted = false;
|
|
354
483
|
/** The edge ids seen so far (the id column is unique; a repeat is skipped with an issue). */
|
|
355
484
|
this.edgeIds = new Set();
|
|
356
485
|
this.where = { line: null, element: null };
|
|
@@ -359,17 +488,27 @@ class TableReader {
|
|
|
359
488
|
this.rowNumbers = state.csv.rowNumberIds && (kind === "nodes" || (kind === "auto" && state.csv.nodes === null));
|
|
360
489
|
const readerOptions = {
|
|
361
490
|
delimiter: state.csv.delimiter,
|
|
491
|
+
// a decimal comma is never the delimiter
|
|
492
|
+
...(state.csv.decimalComma ? { candidates: DELIMITER_CANDIDATES.filter((d) => d !== ",") } : {}),
|
|
362
493
|
comments: COMMENT_CHARS,
|
|
363
494
|
// field counts say nothing about an adjacency table, whose rows vary in width
|
|
364
495
|
skipQuoteErrors: kind === "adjacency",
|
|
365
496
|
signal: state.common.signal,
|
|
366
497
|
onProgress: progress ? state.common.onProgress : null,
|
|
367
498
|
encoding: state.common.encoding,
|
|
499
|
+
// an adjacency table has no header: an empty one is the empty graph
|
|
500
|
+
allowEmpty: kind === "adjacency",
|
|
501
|
+
// explicit dialect options say the caller knows the input is a table
|
|
502
|
+
inspect: hasExplicitDialect(state.csv)
|
|
503
|
+
? undefined
|
|
504
|
+
: (preview, line) => {
|
|
505
|
+
this.refuseOtherFormat(preview, line);
|
|
506
|
+
},
|
|
368
507
|
};
|
|
369
508
|
this.reader = new CsvRecordReader(input, state.report, readerOptions);
|
|
370
509
|
}
|
|
371
510
|
/**
|
|
372
|
-
* Read every row; the reader is closed (and a stream
|
|
511
|
+
* Read every row; the reader is closed (and a stream canceled) when the import aborts midway.
|
|
373
512
|
*/
|
|
374
513
|
async read() {
|
|
375
514
|
const iterator = this.reader[Symbol.asyncIterator]();
|
|
@@ -386,7 +525,11 @@ class TableReader {
|
|
|
386
525
|
*/
|
|
387
526
|
async readRows(iterator) {
|
|
388
527
|
const { report } = this.state;
|
|
389
|
-
|
|
528
|
+
let first = await iterator.next();
|
|
529
|
+
if (!first.done && (this.reader.sepDirective || this.isSepDirective(first.value))) {
|
|
530
|
+
// Excel's `sep=;` line: the reader took the delimiter from it; it is not a row
|
|
531
|
+
first = await iterator.next();
|
|
532
|
+
}
|
|
390
533
|
if (first.done && this.kind === "adjacency") {
|
|
391
534
|
// an adjacency table has no header: an empty one is the empty graph
|
|
392
535
|
return;
|
|
@@ -412,7 +555,8 @@ class TableReader {
|
|
|
412
555
|
const secondRow = second.done ? null : second.value;
|
|
413
556
|
const secondLine = this.reader.line;
|
|
414
557
|
const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
|
|
415
|
-
|
|
558
|
+
// a column option naming a cell of the first row makes that row the header
|
|
559
|
+
header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
|
|
416
560
|
if (!header) {
|
|
417
561
|
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
418
562
|
}
|
|
@@ -426,11 +570,19 @@ class TableReader {
|
|
|
426
570
|
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
427
571
|
}
|
|
428
572
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.state.commentDirected = commentDirection(this.reader.leadingComments);
|
|
573
|
+
if (this.rowNumbers && !header) {
|
|
574
|
+
report.warning("unsupported", OPTION_IGNORED_CODE, "option rowNumberIds ignored: a headerless node table takes its ids from column 0", { line: firstLine, element: "rowNumberIds" });
|
|
432
575
|
}
|
|
433
|
-
|
|
576
|
+
const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
|
|
577
|
+
const last = firstRow.length - 1;
|
|
578
|
+
const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
|
|
579
|
+
this.applyCommentDirection();
|
|
580
|
+
this.plan = this.resolvePlan(names, header, firstLine, pending.map((p) => p.row), optionalLast);
|
|
581
|
+
if (optionalLast) {
|
|
582
|
+
report.warning("validation-error", TRAILING_HEADER_DELIMITER_CODE, `the header ends in a delimiter; rows without the empty last cell are read as complete`, { line: firstLine, element: names[last] });
|
|
583
|
+
}
|
|
584
|
+
this.reportCommentLikeRecords(names.length);
|
|
585
|
+
this.reportSingleColumn(names.length, pending.map((p) => p.row), firstLine);
|
|
434
586
|
this.prepareColumns(firstLine);
|
|
435
587
|
for (const { row, quoted, line } of pending) {
|
|
436
588
|
this.processRow(row, quoted, line);
|
|
@@ -456,41 +608,132 @@ class TableReader {
|
|
|
456
608
|
line: firstLine,
|
|
457
609
|
});
|
|
458
610
|
}
|
|
611
|
+
this.reportReaderFindings();
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Whether a row is Excel's delimiter directive (`sep=;`) under a delimiter the caller gave (a
|
|
615
|
+
* sniffed one comes from the directive itself: `reader.sepDirective`).
|
|
616
|
+
* @param row - the first row
|
|
617
|
+
* @returns true for the directive
|
|
618
|
+
*/
|
|
619
|
+
isSepDirective(row) {
|
|
620
|
+
return (this.state.csv.delimiter !== null &&
|
|
621
|
+
!this.reader.quoted[0] &&
|
|
622
|
+
/^sep=.$/i.test(row.join(this.state.csv.delimiter)));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Fail when the input opens like another format (an HTML error page, a JSON, GML, DOT or Pajek
|
|
626
|
+
* file handed to the CSV importer), which any delimiter would otherwise read as rows of text.
|
|
627
|
+
* Runs on the raw preview before any record is parsed, so a quoting error in the other
|
|
628
|
+
* format's syntax cannot abort first.
|
|
629
|
+
* @param preview - the start of the input, leading comments removed
|
|
630
|
+
* @param line - the line it starts on
|
|
631
|
+
*/
|
|
632
|
+
refuseOtherFormat(preview, line) {
|
|
633
|
+
if (OTHER_FORMATS.some((r) => r.test(preview))) {
|
|
634
|
+
this.state.report.fail(OTHER_FORMAT_CODE, `the input opens like another format (XML / HTML, JSON, GML, DOT or Pajek), not CSV: ${JSON.stringify(preview.trimStart().slice(0, 60))}`, { line });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Whether a column option names a cell of the first row by text.
|
|
639
|
+
* @param row - the first row
|
|
640
|
+
* @returns true when sourceColumn, targetColumn, idColumn or typeColumn names one of its cells
|
|
641
|
+
*/
|
|
642
|
+
namesExplicitColumn(row) {
|
|
643
|
+
const { csv } = this.state;
|
|
644
|
+
const names = headerNames(row);
|
|
645
|
+
return [csv.sourceColumn, csv.targetColumn, csv.idColumn, csv.typeColumn, csv.labelColumn].some((ref) => typeof ref === "string" && findColumn(names, [ref]) >= 0);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Take the file-level direction from the leading SNAP / KONECT comments of this table: the first
|
|
649
|
+
* declaring line wins; a later line declaring the other direction, and a declaration that
|
|
650
|
+
* overrides the caller's explicit defaultDirected, are reported.
|
|
651
|
+
*/
|
|
652
|
+
applyCommentDirection() {
|
|
653
|
+
const { state } = this;
|
|
654
|
+
const { leadingComments, leadingCommentLines } = this.reader;
|
|
655
|
+
for (let i = 0; i < leadingComments.length; i++) {
|
|
656
|
+
const comment = leadingComments[i];
|
|
657
|
+
const directed = commentDirection(comment);
|
|
658
|
+
if (directed === null) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const where = { line: leadingCommentLines[i], element: comment };
|
|
662
|
+
if (state.commentDirected === null) {
|
|
663
|
+
state.commentDirected = directed;
|
|
664
|
+
state.commentText = comment;
|
|
665
|
+
const explicit = state.csv.explicitDirected;
|
|
666
|
+
if (explicit !== null && explicit !== directed) {
|
|
667
|
+
state.report.warning("validation-error", COMMENT_DIRECTION_CODE, `${JSON.stringify(comment)} declares a ${directionName(directed)} graph, which overrides defaultDirected: ${explicit}`, where);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else if (state.commentDirected !== directed) {
|
|
671
|
+
state.report.warning("validation-error", COMMENT_DIRECTION_CODE, `${JSON.stringify(comment)} declares a ${directionName(directed)} graph but ${JSON.stringify(state.commentText)} declared a ${directionName(state.commentDirected)} one; the first applies`, where);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Report a leading `#` / `%` line that has the fields of a record (a first id starting with `#`,
|
|
677
|
+
* swallowed as a comment): its text directly follows the comment character and splits into as
|
|
678
|
+
* many cells as the table has columns. SNAP and KONECT comment lines put a space after it.
|
|
679
|
+
* @param width - the number of columns
|
|
680
|
+
*/
|
|
681
|
+
reportCommentLikeRecords(width) {
|
|
682
|
+
const { delimiter, leadingComments, leadingCommentLines } = this.reader;
|
|
683
|
+
if (width < 2 || delimiter === null) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
for (let i = 0; i < leadingComments.length; i++) {
|
|
687
|
+
const comment = leadingComments[i];
|
|
688
|
+
if (comment.length > 1 && comment[1].trim().length > 0 && comment.split(delimiter).length === width) {
|
|
689
|
+
this.state.report.warnOnce("parse-error", COMMENT_LIKE_RECORD_CODE, `${JSON.stringify(comment)} was skipped as a leading comment but has the fields of a record; quote an id that starts with # or %`, { line: leadingCommentLines[i], element: comment });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Report a one-column table whose cells another delimiter would split consistently (a delimiter
|
|
695
|
+
* option that does not match the file).
|
|
696
|
+
* @param width - the number of columns
|
|
697
|
+
* @param rows - the data rows read so far
|
|
698
|
+
* @param line - the first line
|
|
699
|
+
*/
|
|
700
|
+
reportSingleColumn(width, rows, line) {
|
|
701
|
+
if (width !== 1 || rows.length === 0 || this.plan?.kind === "adjacency") {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const candidates = DELIMITER_CANDIDATES.filter((d) => d !== this.reader.delimiter);
|
|
705
|
+
const other = sniffDelimiter(`${rows.map((r) => r[0]).join("\n")}\n`, "\n", candidates);
|
|
706
|
+
if (other !== null) {
|
|
707
|
+
this.state.report.warning("parse-error", SINGLE_COLUMN_CODE, `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`, { line });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/** Report what the record reader noticed while reading: a stray quote. */
|
|
711
|
+
reportReaderFindings() {
|
|
712
|
+
const { report } = this.state;
|
|
713
|
+
const { strayQuoteLine } = this.reader;
|
|
714
|
+
if (strayQuoteLine > 0) {
|
|
715
|
+
report.warnOnce("parse-error", STRAY_QUOTE_CODE, `a quote inside an unquoted field is kept as text (RFC 4180 quotes a whole field)`, { line: strayQuoteLine });
|
|
716
|
+
}
|
|
459
717
|
}
|
|
460
718
|
/**
|
|
461
719
|
* Decide the table's columns from its header.
|
|
462
720
|
* @param names - the column names
|
|
463
721
|
* @param header - whether the file has a header row
|
|
464
722
|
* @param line - the header line
|
|
723
|
+
* @param sample - the data rows read so far
|
|
724
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
465
725
|
* @returns the plan; the import aborts when no endpoints (or id) resolve
|
|
466
726
|
*/
|
|
467
|
-
resolvePlan(names, header, line) {
|
|
468
|
-
const { csv
|
|
727
|
+
resolvePlan(names, header, line, sample, optionalLast) {
|
|
728
|
+
const { csv } = this.state;
|
|
469
729
|
if (this.kind === "adjacency") {
|
|
470
730
|
return { kind: "adjacency", names: [] };
|
|
471
731
|
}
|
|
472
|
-
const width = names.length;
|
|
473
732
|
let source = -1;
|
|
474
733
|
let target = -1;
|
|
475
734
|
if (this.kind !== "nodes") {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
else if (header) {
|
|
480
|
-
source = findColumn(names, SOURCE_NAMES);
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
source = width >= 2 ? 0 : -1;
|
|
484
|
-
}
|
|
485
|
-
if (csv.targetColumn !== null) {
|
|
486
|
-
target = resolveColumnRef(names, csv.targetColumn, "targetColumn");
|
|
487
|
-
}
|
|
488
|
-
else if (header) {
|
|
489
|
-
target = findColumn(names, TARGET_NAMES);
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
target = width >= 2 ? 1 : -1;
|
|
493
|
-
}
|
|
735
|
+
source = endpointColumn(names, header, csv.sourceColumn, "sourceColumn", SOURCE_NAMES, 0);
|
|
736
|
+
target = endpointColumn(names, header, csv.targetColumn, "targetColumn", TARGET_NAMES, 1);
|
|
494
737
|
if (source >= 0 && target >= 0 && source === target) {
|
|
495
738
|
throw new GraphFormatError("E_UNSUPPORTED", "sourceColumn and targetColumn name the same column", {
|
|
496
739
|
option: "targetColumn",
|
|
@@ -499,18 +742,36 @@ class TableReader {
|
|
|
499
742
|
}
|
|
500
743
|
}
|
|
501
744
|
if (source >= 0 && target >= 0) {
|
|
502
|
-
return this.edgePlan(names, header, source, target);
|
|
745
|
+
return this.edgePlan(names, header, source, target, line, sample, optionalLast);
|
|
503
746
|
}
|
|
747
|
+
this.refuseWithoutEndpoints(names, header, source, target, line);
|
|
748
|
+
return this.nodePlan(names, header, line, optionalLast);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Refuse a table without both endpoint columns, unless it can be a node table: an edge table
|
|
752
|
+
* was asked for, a single endpoint column was found, or nothing names an id.
|
|
753
|
+
* @param names - the column names
|
|
754
|
+
* @param header - whether the file has a header row
|
|
755
|
+
* @param source - the source column, or -1
|
|
756
|
+
* @param target - the target column, or -1
|
|
757
|
+
* @param line - the header line
|
|
758
|
+
*/
|
|
759
|
+
refuseWithoutEndpoints(names, header, source, target, line) {
|
|
760
|
+
const { csv, report } = this.state;
|
|
504
761
|
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
762
|
+
if (source >= 0 || target >= 0) {
|
|
763
|
+
// one endpoint column is a broken edge table, never a node table
|
|
764
|
+
const [has, missing] = source >= 0 ? ["source", "target"] : ["target", "source"];
|
|
765
|
+
report.fail(NO_ENDPOINT_COLUMNS_CODE, `the header has a ${has} column (${JSON.stringify(names[Math.max(source, target)])}) but no ${missing} column (${shown})`, { line }, { columns: [...names] });
|
|
766
|
+
}
|
|
505
767
|
if (this.kind === "edges" ||
|
|
506
|
-
(this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null))) {
|
|
768
|
+
(this.kind === "auto" && (csv.sourceColumn !== null || csv.targetColumn !== null || csv.nodes !== null))) {
|
|
507
769
|
report.fail(NO_ENDPOINT_COLUMNS_CODE, `no source / target columns in the header (${shown}); a node table goes in the nodes option`, { line }, { columns: [...names] });
|
|
508
770
|
}
|
|
509
|
-
const idResolves = header ? findColumn(names, ID_NAMES) >= 0 :
|
|
771
|
+
const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : names.length >= 1;
|
|
510
772
|
if (this.kind === "auto" && csv.idColumn === null && !idResolves && !this.rowNumbers) {
|
|
511
773
|
report.fail(NO_ENDPOINT_COLUMNS_CODE, `no source / target columns and no id column in the header (${shown}); the input is neither an edge table nor a node table`, { line }, { columns: [...names] });
|
|
512
774
|
}
|
|
513
|
-
return this.nodePlan(names, header, line);
|
|
514
775
|
}
|
|
515
776
|
/**
|
|
516
777
|
* The columns of an edge table.
|
|
@@ -518,47 +779,26 @@ class TableReader {
|
|
|
518
779
|
* @param header - whether the file has a header row
|
|
519
780
|
* @param source - the source column
|
|
520
781
|
* @param target - the target column
|
|
782
|
+
* @param line - the header (or first) line
|
|
783
|
+
* @param sample - the data rows read so far
|
|
784
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
521
785
|
* @returns the plan
|
|
522
786
|
*/
|
|
523
|
-
edgePlan(names, header, source, target) {
|
|
524
|
-
const { csv
|
|
787
|
+
edgePlan(names, header, source, target, line, sample, optionalLast) {
|
|
788
|
+
const { csv } = this.state;
|
|
525
789
|
const claimed = new Set([source, target]);
|
|
526
|
-
|
|
527
|
-
if (common.weightFrom !== null) {
|
|
528
|
-
if (header) {
|
|
529
|
-
weight = findFree(names, [common.weightFrom], claimed);
|
|
530
|
-
if (weight < 0 && this.state.weightFromExplicit) {
|
|
531
|
-
report.warning("missing-value", COLUMN_MISSING_CODE, `weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`, { line: this.reader.line, element: common.weightFrom });
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
else if (names.length >= 3 && !claimed.has(2)) {
|
|
535
|
-
weight = 2;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
790
|
+
const weight = this.weightColumn(names, header, claimed, line, sample);
|
|
538
791
|
if (weight >= 0) {
|
|
539
792
|
claimed.add(weight);
|
|
540
793
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (header && names[source] === "Source" && names[target] === "Target") {
|
|
544
|
-
type = findFree(names, [TYPE_NAME], claimed);
|
|
545
|
-
if (type >= 0 && names[type] !== TYPE_NAME) {
|
|
546
|
-
type = -1;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
else if (csv.typeColumn !== null) {
|
|
551
|
-
type = resolveColumnRef(names, csv.typeColumn, "typeColumn");
|
|
552
|
-
if (claimed.has(type)) {
|
|
553
|
-
throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
|
|
554
|
-
option: "typeColumn",
|
|
555
|
-
found: names[type],
|
|
556
|
-
});
|
|
557
|
-
}
|
|
794
|
+
if (header) {
|
|
795
|
+
this.reportEndpointRivals(names, source, target, weight, line);
|
|
558
796
|
}
|
|
797
|
+
const type = this.typeColumn(names, header, source, target, claimed);
|
|
559
798
|
if (type >= 0) {
|
|
560
799
|
claimed.add(type);
|
|
561
800
|
}
|
|
801
|
+
const typeHint = header && type < 0 && csv.typeColumn === undefined ? findFree(names, [TYPE_NAME], claimed) : -1;
|
|
562
802
|
const id = header ? findFree(names, EDGE_ID_NAMES, claimed) : -1;
|
|
563
803
|
if (id >= 0) {
|
|
564
804
|
claimed.add(id);
|
|
@@ -573,53 +813,147 @@ class TableReader {
|
|
|
573
813
|
attributes.push(i);
|
|
574
814
|
}
|
|
575
815
|
}
|
|
576
|
-
return {
|
|
816
|
+
return {
|
|
817
|
+
kind: "edges",
|
|
818
|
+
names,
|
|
819
|
+
width: names.length,
|
|
820
|
+
source,
|
|
821
|
+
target,
|
|
822
|
+
weight,
|
|
823
|
+
type,
|
|
824
|
+
typeHint,
|
|
825
|
+
id,
|
|
826
|
+
label,
|
|
827
|
+
attributes,
|
|
828
|
+
optionalLast,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* The weight column of an edge table: the weightFrom column of a header (a warning when it was
|
|
833
|
+
* asked for and is missing), else the third column of a headerless edge list unless it holds text.
|
|
834
|
+
* @param names - the column names
|
|
835
|
+
* @param header - whether the file has a header row
|
|
836
|
+
* @param claimed - the endpoint columns
|
|
837
|
+
* @param line - the header (or first) line
|
|
838
|
+
* @param sample - the data rows read so far
|
|
839
|
+
* @returns the column index, or -1
|
|
840
|
+
*/
|
|
841
|
+
weightColumn(names, header, claimed, line, sample) {
|
|
842
|
+
const { common, report } = this.state;
|
|
843
|
+
if (common.weightFrom === null) {
|
|
844
|
+
return -1;
|
|
845
|
+
}
|
|
846
|
+
if (header) {
|
|
847
|
+
const weight = findFree(names, [common.weightFrom], claimed);
|
|
848
|
+
if (weight < 0 && this.state.weightFromExplicit) {
|
|
849
|
+
report.warning("missing-value", COLUMN_MISSING_CODE, `weight column ${JSON.stringify(common.weightFrom)} not found; edges are unweighted`, { line, element: common.weightFrom });
|
|
850
|
+
}
|
|
851
|
+
return weight;
|
|
852
|
+
}
|
|
853
|
+
if (names.length < 3 || claimed.has(2)) {
|
|
854
|
+
return -1;
|
|
855
|
+
}
|
|
856
|
+
if (isTextTriple(names.length, sample)) {
|
|
857
|
+
report.warnOnce("validation-error", WEIGHT_AS_ATTRIBUTE_CODE, `the third column holds text in the first rows, so it is read as the attribute ${names[2]}, not the weight`, { line, element: names[2] });
|
|
858
|
+
return -1;
|
|
859
|
+
}
|
|
860
|
+
return 2;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Warn about header columns that name the source, target or weight beside the chosen one.
|
|
864
|
+
* @param names - the header names
|
|
865
|
+
* @param source - the source column
|
|
866
|
+
* @param target - the target column
|
|
867
|
+
* @param weight - the weight column, or -1
|
|
868
|
+
* @param line - the header line
|
|
869
|
+
*/
|
|
870
|
+
reportEndpointRivals(names, source, target, weight, line) {
|
|
871
|
+
const { csv, common } = this.state;
|
|
872
|
+
if (csv.sourceColumn === null) {
|
|
873
|
+
this.reportRivals(names, SOURCE_NAMES, source, "source", line);
|
|
874
|
+
}
|
|
875
|
+
if (csv.targetColumn === null) {
|
|
876
|
+
this.reportRivals(names, TARGET_NAMES, target, "target", line);
|
|
877
|
+
}
|
|
878
|
+
if (weight >= 0 && common.weightFrom !== null) {
|
|
879
|
+
this.reportRivals(names, [common.weightFrom], weight, "weight", line);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* The edge type column: the typeColumn option, else Gephi's `Type` beside `Source` and `Target`.
|
|
884
|
+
* @param names - the column names
|
|
885
|
+
* @param header - whether the file has a header row
|
|
886
|
+
* @param source - the source column
|
|
887
|
+
* @param target - the target column
|
|
888
|
+
* @param claimed - the columns other roles took
|
|
889
|
+
* @returns the column index, or -1
|
|
890
|
+
*/
|
|
891
|
+
typeColumn(names, header, source, target, claimed) {
|
|
892
|
+
const { typeColumn } = this.state.csv;
|
|
893
|
+
if (typeColumn === null) {
|
|
894
|
+
return -1;
|
|
895
|
+
}
|
|
896
|
+
if (typeColumn === undefined) {
|
|
897
|
+
const gephi = header && names[source] === "Source" && names[target] === "Target";
|
|
898
|
+
const type = gephi ? findFree(names, [TYPE_NAME], claimed) : -1;
|
|
899
|
+
return type >= 0 && names[type] === TYPE_NAME ? type : -1;
|
|
900
|
+
}
|
|
901
|
+
const type = resolveColumnRef(names, typeColumn, "typeColumn");
|
|
902
|
+
if (claimed.has(type)) {
|
|
903
|
+
throw new GraphFormatError("E_UNSUPPORTED", "typeColumn names an endpoint or weight column", {
|
|
904
|
+
option: "typeColumn",
|
|
905
|
+
found: names[type],
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return type;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Warn about header columns that name a role another column was chosen for (`Source,source`,
|
|
912
|
+
* `from,to,source`, `Weight,weight`): the one not chosen is read as a plain attribute.
|
|
913
|
+
* @param names - the header names
|
|
914
|
+
* @param candidates - the names of the role
|
|
915
|
+
* @param chosen - the column chosen
|
|
916
|
+
* @param role - the role, for the message
|
|
917
|
+
* @param line - the header line
|
|
918
|
+
*/
|
|
919
|
+
reportRivals(names, candidates, chosen, role, line) {
|
|
920
|
+
const lower = new Set(candidates.map((c) => c.toLowerCase()));
|
|
921
|
+
names.forEach((name, i) => {
|
|
922
|
+
if (i !== chosen && lower.has(name.toLowerCase())) {
|
|
923
|
+
this.state.report.warning("validation-error", AMBIGUOUS_COLUMN_CODE, `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`, { line, element: name });
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* The label column of a node table: the labelColumn option, else a header column named `label`.
|
|
929
|
+
* @param names - the column names
|
|
930
|
+
* @param header - whether the file has a header row
|
|
931
|
+
* @param claimed - columns another role took
|
|
932
|
+
* @returns the column index, or -1
|
|
933
|
+
*/
|
|
934
|
+
labelOf(names, header, claimed) {
|
|
935
|
+
const { labelColumn } = this.state.csv;
|
|
936
|
+
if (labelColumn !== null) {
|
|
937
|
+
return resolveColumnRef(names, labelColumn, "labelColumn");
|
|
938
|
+
}
|
|
939
|
+
return header ? findFree(names, LABEL_NAMES, claimed) : -1;
|
|
577
940
|
}
|
|
578
941
|
/**
|
|
579
942
|
* The columns of a node table.
|
|
580
943
|
* @param names - the column names
|
|
581
944
|
* @param header - whether the file has a header row
|
|
582
945
|
* @param line - the header line
|
|
946
|
+
* @param optionalLast - whether a row may omit the last cell
|
|
583
947
|
* @returns the plan; the import aborts when no id column resolves
|
|
584
948
|
*/
|
|
585
|
-
nodePlan(names, header, line) {
|
|
586
|
-
const {
|
|
949
|
+
nodePlan(names, header, line, optionalLast) {
|
|
950
|
+
const { common } = this.state;
|
|
587
951
|
const claimed = new Set();
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
idColumn = findColumn(names, ID_NAMES);
|
|
594
|
-
}
|
|
595
|
-
else if (names.length >= 1) {
|
|
596
|
-
idColumn = 0;
|
|
597
|
-
}
|
|
598
|
-
const label = header ? findFree(names, LABEL_NAMES, new Set(idColumn >= 0 ? [idColumn] : [])) : -1;
|
|
599
|
-
let id;
|
|
600
|
-
switch (common.nodeIdFrom) {
|
|
601
|
-
case "label":
|
|
602
|
-
if (label < 0) {
|
|
603
|
-
report.fail(NO_ID_COLUMN_CODE, 'nodeIdFrom is "label" but the node table has no label column', {
|
|
604
|
-
line,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
id = label;
|
|
608
|
-
break;
|
|
609
|
-
case "index":
|
|
610
|
-
id = -1;
|
|
611
|
-
break;
|
|
612
|
-
default:
|
|
613
|
-
if (idColumn < 0 && this.rowNumbers) {
|
|
614
|
-
id = -1;
|
|
615
|
-
break;
|
|
616
|
-
}
|
|
617
|
-
if (idColumn < 0) {
|
|
618
|
-
report.fail(NO_ID_COLUMN_CODE, `no id column in the node table header (${names.map((n) => JSON.stringify(n)).join(", ")})`, { line }, { columns: [...names] });
|
|
619
|
-
}
|
|
620
|
-
id = idColumn;
|
|
621
|
-
claimed.add(idColumn);
|
|
622
|
-
break;
|
|
952
|
+
const idColumn = this.idColumnOf(names, header);
|
|
953
|
+
const label = this.labelOf(names, header, new Set(idColumn >= 0 ? [idColumn] : []));
|
|
954
|
+
const id = this.nodeIdColumn(names, idColumn, label, line);
|
|
955
|
+
if (id >= 0) {
|
|
956
|
+
claimed.add(id);
|
|
623
957
|
}
|
|
624
958
|
if (label >= 0) {
|
|
625
959
|
claimed.add(label);
|
|
@@ -631,7 +965,50 @@ class TableReader {
|
|
|
631
965
|
}
|
|
632
966
|
}
|
|
633
967
|
const rowNumber = id < 0 && common.nodeIdFrom !== "index";
|
|
634
|
-
return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes };
|
|
968
|
+
return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes, optionalLast };
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* The id column of a node table: the idColumn option, else a header column with an id name,
|
|
972
|
+
* else the first column of a headerless table.
|
|
973
|
+
* @param names - the column names
|
|
974
|
+
* @param header - whether the file has a header row
|
|
975
|
+
* @returns the column index, or -1
|
|
976
|
+
*/
|
|
977
|
+
idColumnOf(names, header) {
|
|
978
|
+
const { idColumn } = this.state.csv;
|
|
979
|
+
if (idColumn !== null) {
|
|
980
|
+
return resolveColumnRef(names, idColumn, "idColumn");
|
|
981
|
+
}
|
|
982
|
+
if (header) {
|
|
983
|
+
return findColumn(names, ID_NAMES);
|
|
984
|
+
}
|
|
985
|
+
return names.length >= 1 ? 0 : -1;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* The column node ids come from, as nodeIdFrom says: the label, none (row numbers), or the id
|
|
989
|
+
* column. The import aborts when that column is missing.
|
|
990
|
+
* @param names - the column names
|
|
991
|
+
* @param idColumn - the id column, or -1
|
|
992
|
+
* @param label - the label column, or -1
|
|
993
|
+
* @param line - the header line
|
|
994
|
+
* @returns the column index, or -1 for row numbers
|
|
995
|
+
*/
|
|
996
|
+
nodeIdColumn(names, idColumn, label, line) {
|
|
997
|
+
const { common, report } = this.state;
|
|
998
|
+
const shown = names.map((n) => JSON.stringify(n)).join(", ");
|
|
999
|
+
if (common.nodeIdFrom === "index") {
|
|
1000
|
+
return -1;
|
|
1001
|
+
}
|
|
1002
|
+
if (common.nodeIdFrom === "label") {
|
|
1003
|
+
if (label < 0) {
|
|
1004
|
+
report.fail(NO_ID_COLUMN_CODE, `nodeIdFrom is "label", but the node table has no column named "label" (${shown}); pass labelColumn to name the column that holds the labels`, { line }, { columns: [...names] });
|
|
1005
|
+
}
|
|
1006
|
+
return label;
|
|
1007
|
+
}
|
|
1008
|
+
if (idColumn < 0 && !this.rowNumbers) {
|
|
1009
|
+
report.fail(NO_ID_COLUMN_CODE, `no id column in the node table header (${shown})`, { line }, { columns: [...names] });
|
|
1010
|
+
}
|
|
1011
|
+
return idColumn;
|
|
635
1012
|
}
|
|
636
1013
|
/**
|
|
637
1014
|
* Declare the role columns and create a writer per attribute column.
|
|
@@ -675,6 +1052,13 @@ class TableReader {
|
|
|
675
1052
|
processRow(row, quoted, line) {
|
|
676
1053
|
const plan = this.requirePlan();
|
|
677
1054
|
this.dataRows++;
|
|
1055
|
+
if (this.state.csv.decimalComma) {
|
|
1056
|
+
for (let i = 0; i < row.length; i++) {
|
|
1057
|
+
if (DECIMAL_COMMA.test(row[i])) {
|
|
1058
|
+
row[i] = row[i].replace(",", ".");
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
678
1062
|
if (plan.kind === "edges") {
|
|
679
1063
|
this.processEdgeRow(plan, row, quoted, line);
|
|
680
1064
|
}
|
|
@@ -686,7 +1070,7 @@ class TableReader {
|
|
|
686
1070
|
}
|
|
687
1071
|
}
|
|
688
1072
|
/**
|
|
689
|
-
* Push one adjacency row: the node, then one edge per set
|
|
1073
|
+
* Push one adjacency row: the node, then one edge per set neighbor cell, in row order.
|
|
690
1074
|
* @param row - the cells
|
|
691
1075
|
* @param quoted - whether each cell was quoted
|
|
692
1076
|
* @param line - the row's line
|
|
@@ -697,16 +1081,16 @@ class TableReader {
|
|
|
697
1081
|
const { where } = this;
|
|
698
1082
|
where.line = line;
|
|
699
1083
|
where.element = null;
|
|
700
|
-
let
|
|
1084
|
+
let neighbors = 0;
|
|
701
1085
|
for (let k = 1; k < row.length; k++) {
|
|
702
1086
|
if (!isUnset(row[k], quoted[k])) {
|
|
703
|
-
|
|
1087
|
+
neighbors++;
|
|
704
1088
|
}
|
|
705
1089
|
}
|
|
706
1090
|
if (isUnset(row[0], quoted[0])) {
|
|
707
|
-
report.error("missing-value", MISSING_ID_CODE, `
|
|
1091
|
+
report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
|
|
708
1092
|
counts.skippedNodes++;
|
|
709
|
-
counts.skippedEdges +=
|
|
1093
|
+
counts.skippedEdges += neighbors;
|
|
710
1094
|
return;
|
|
711
1095
|
}
|
|
712
1096
|
const kind = (this.state.commentDirected ?? common.defaultDirected) ? "directed" : "undirected";
|
|
@@ -726,7 +1110,7 @@ class TableReader {
|
|
|
726
1110
|
catch (err) {
|
|
727
1111
|
report.recordError(err, where);
|
|
728
1112
|
counts.skippedNodes++;
|
|
729
|
-
counts.skippedEdges +=
|
|
1113
|
+
counts.skippedEdges += neighbors;
|
|
730
1114
|
return;
|
|
731
1115
|
}
|
|
732
1116
|
for (let k = 1; k < row.length; k++) {
|
|
@@ -753,14 +1137,15 @@ class TableReader {
|
|
|
753
1137
|
* Push one edge row: endpoints, weight, direction, then the attribute cells.
|
|
754
1138
|
* @param plan - the edge plan
|
|
755
1139
|
* @param row - the cells
|
|
756
|
-
* @param
|
|
1140
|
+
* @param cellQuoted - whether each cell was quoted
|
|
757
1141
|
* @param line - the row's line
|
|
758
1142
|
*/
|
|
759
|
-
processEdgeRow(plan, row,
|
|
760
|
-
const { report
|
|
1143
|
+
processEdgeRow(plan, row, cellQuoted, line) {
|
|
1144
|
+
const { report } = this.state;
|
|
761
1145
|
const { counts } = report;
|
|
1146
|
+
const quoted = completeRow(plan, row, cellQuoted, report, line);
|
|
762
1147
|
if (row.length !== plan.width) {
|
|
763
|
-
|
|
1148
|
+
this.reportRowWidth(plan, row, line);
|
|
764
1149
|
counts.skippedEdges++;
|
|
765
1150
|
return;
|
|
766
1151
|
}
|
|
@@ -768,49 +1153,31 @@ class TableReader {
|
|
|
768
1153
|
const targetText = row[plan.target];
|
|
769
1154
|
const sourceMissing = isUnset(sourceText, quoted[plan.source]);
|
|
770
1155
|
if (sourceMissing || isUnset(targetText, quoted[plan.target])) {
|
|
771
|
-
report.error("missing-value", MISSING_ENDPOINT_CODE, `
|
|
1156
|
+
report.error("missing-value", MISSING_ENDPOINT_CODE, `blank ${sourceMissing ? "source" : "target"} cell`, {
|
|
1157
|
+
line,
|
|
1158
|
+
});
|
|
772
1159
|
counts.skippedEdges++;
|
|
773
1160
|
return;
|
|
774
1161
|
}
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
report.error("validation-error", BAD_TYPE_CODE, `line ${line}: Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`, { line });
|
|
780
|
-
counts.skippedEdges++;
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
if (parsed !== undefined) {
|
|
784
|
-
kind = parsed;
|
|
785
|
-
}
|
|
1162
|
+
const kind = this.rowKind(plan, row, line);
|
|
1163
|
+
if (kind === null) {
|
|
1164
|
+
counts.skippedEdges++;
|
|
1165
|
+
return;
|
|
786
1166
|
}
|
|
787
1167
|
const { where } = this;
|
|
788
1168
|
where.line = line;
|
|
789
1169
|
where.element = null;
|
|
790
1170
|
const idText = plan.id >= 0 && !isUnset(row[plan.id], quoted[plan.id]) ? row[plan.id] : null;
|
|
791
|
-
if (idText !== null) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
this.edgeIds.add(idText);
|
|
1171
|
+
if (idText !== null && this.edgeIds.has(idText)) {
|
|
1172
|
+
report.error("validation-error", DUPLICATE_EDGE_ID_CODE, `edge id ${JSON.stringify(idText)} repeats an earlier row's; the row is skipped`, { line, element: idText });
|
|
1173
|
+
counts.skippedEdges++;
|
|
1174
|
+
return;
|
|
798
1175
|
}
|
|
1176
|
+
this.checkPadded(sourceText, quoted[plan.source], line);
|
|
1177
|
+
this.checkPadded(targetText, quoted[plan.target], line);
|
|
799
1178
|
let edge;
|
|
800
1179
|
try {
|
|
801
|
-
|
|
802
|
-
const target = this.coerce(targetText);
|
|
803
|
-
const weight = plan.weight >= 0 ? parseWeightText(row[plan.weight]) : undefined;
|
|
804
|
-
if (!this.state.headerSet) {
|
|
805
|
-
this.state.headerSet = true;
|
|
806
|
-
resolver.setHeader(kind !== "undirected", where);
|
|
807
|
-
}
|
|
808
|
-
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
809
|
-
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
810
|
-
const before = sink.edgeCount;
|
|
811
|
-
edge = resolver.addEdge(source, target, kind, weight, where);
|
|
812
|
-
counts.edges += sink.edgeCount - before;
|
|
813
|
-
counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
1180
|
+
edge = this.addEdge(sourceText, targetText, kind, plan.weight >= 0 ? row[plan.weight] : null);
|
|
814
1181
|
}
|
|
815
1182
|
catch (err) {
|
|
816
1183
|
where.element = idText ?? `${sourceText}->${targetText}`;
|
|
@@ -819,35 +1186,116 @@ class TableReader {
|
|
|
819
1186
|
return;
|
|
820
1187
|
}
|
|
821
1188
|
if (idText !== null) {
|
|
1189
|
+
// only an imported edge holds its id: a skipped row leaves it free
|
|
1190
|
+
this.edgeIds.add(idText);
|
|
822
1191
|
this.writeRole(this.idHandle, "edge", edge, idText, plan.names[plan.id], line);
|
|
823
1192
|
}
|
|
1193
|
+
this.warnTypeHint(plan, row, line);
|
|
824
1194
|
if (plan.label >= 0 && !isUnset(row[plan.label], quoted[plan.label])) {
|
|
825
1195
|
this.writeRole(this.labelHandle, "edge", edge, row[plan.label], plan.names[plan.label], line);
|
|
826
1196
|
}
|
|
827
1197
|
this.writeAttributes(plan, row, quoted, edge, line);
|
|
828
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Warn, once, when a column that is not the type column holds a direction word.
|
|
1201
|
+
* @param plan - the edge plan
|
|
1202
|
+
* @param row - the cells
|
|
1203
|
+
* @param line - the row's line
|
|
1204
|
+
*/
|
|
1205
|
+
warnTypeHint(plan, row, line) {
|
|
1206
|
+
if (plan.typeHint < 0 || typeof parseKind(row[plan.typeHint]) !== "string") {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
this.state.report.warnOnce("unsupported", TYPE_COLUMN_IGNORED_CODE, `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`, { line, element: plan.names[plan.typeHint] });
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Report a row with the wrong number of cells. The first row too long for an auto table says
|
|
1213
|
+
* what an adjacency list needs, since one reads this way.
|
|
1214
|
+
* @param plan - the edge plan
|
|
1215
|
+
* @param row - the cells
|
|
1216
|
+
* @param line - the row's line
|
|
1217
|
+
*/
|
|
1218
|
+
reportRowWidth(plan, row, line) {
|
|
1219
|
+
const hint = this.state.csv.table === "auto" && row.length > plan.width && !this.adjacencyHinted
|
|
1220
|
+
? '; if each line is a node followed by its neighbors, pass table: "adjacency"'
|
|
1221
|
+
: "";
|
|
1222
|
+
this.adjacencyHinted || (this.adjacencyHinted = hint !== "");
|
|
1223
|
+
this.state.report.error("validation-error", FIELD_COUNT_CODE, `${row.length} field${plural(row.length)}, expected ${plan.width}${hint}`, { line });
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The direction of an edge row: its Type cell, else the file's or the default direction.
|
|
1227
|
+
* @param plan - the edge plan
|
|
1228
|
+
* @param row - the cells
|
|
1229
|
+
* @param line - the row's line
|
|
1230
|
+
* @returns the kind, or null when the Type cell is not a direction (reported)
|
|
1231
|
+
*/
|
|
1232
|
+
rowKind(plan, row, line) {
|
|
1233
|
+
const directed = this.state.commentDirected ?? this.state.common.defaultDirected;
|
|
1234
|
+
const kind = directed ? "directed" : "undirected";
|
|
1235
|
+
if (plan.type < 0) {
|
|
1236
|
+
return kind;
|
|
1237
|
+
}
|
|
1238
|
+
const parsed = parseKind(row[plan.type]);
|
|
1239
|
+
if (parsed === null) {
|
|
1240
|
+
this.state.report.error("validation-error", BAD_TYPE_CODE, `Type ${JSON.stringify(row[plan.type])} is not Directed, Undirected or Mutual`, { line });
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
return parsed ?? kind;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Add an edge row's edge, counting the edge and the nodes it brings in.
|
|
1247
|
+
* @param sourceText - the source cell
|
|
1248
|
+
* @param targetText - the target cell
|
|
1249
|
+
* @param kind - the direction
|
|
1250
|
+
* @param weightText - the weight cell, or null
|
|
1251
|
+
* @returns the edge's index
|
|
1252
|
+
*/
|
|
1253
|
+
addEdge(sourceText, targetText, kind, weightText) {
|
|
1254
|
+
const { report, sink, resolver } = this.state;
|
|
1255
|
+
const { where } = this;
|
|
1256
|
+
const source = this.coerce(sourceText);
|
|
1257
|
+
const target = this.coerce(targetText);
|
|
1258
|
+
const weight = weightText === null ? undefined : parseWeightText(weightText, report);
|
|
1259
|
+
if (!this.state.headerSet) {
|
|
1260
|
+
this.state.headerSet = true;
|
|
1261
|
+
resolver.setHeader(kind !== "undirected", where);
|
|
1262
|
+
}
|
|
1263
|
+
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
1264
|
+
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
1265
|
+
const before = sink.edgeCount;
|
|
1266
|
+
const edge = resolver.addEdge(source, target, kind, weight, where);
|
|
1267
|
+
report.counts.edges += sink.edgeCount - before;
|
|
1268
|
+
report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
1269
|
+
return edge;
|
|
1270
|
+
}
|
|
829
1271
|
/**
|
|
830
1272
|
* Push one node row: the id, then the label and attribute cells.
|
|
831
1273
|
* @param plan - the node plan
|
|
832
1274
|
* @param row - the cells
|
|
833
|
-
* @param
|
|
1275
|
+
* @param cellQuoted - whether each cell was quoted
|
|
834
1276
|
* @param line - the row's line
|
|
835
1277
|
*/
|
|
836
|
-
processNodeRow(plan, row,
|
|
1278
|
+
processNodeRow(plan, row, cellQuoted, line) {
|
|
837
1279
|
const { report, sink } = this.state;
|
|
838
1280
|
const { counts } = report;
|
|
839
1281
|
const ordinal = this.nodeOrdinal++;
|
|
1282
|
+
const quoted = completeRow(plan, row, cellQuoted, report, line);
|
|
840
1283
|
if (row.length !== plan.width) {
|
|
841
|
-
report.error("validation-error", FIELD_COUNT_CODE,
|
|
1284
|
+
report.error("validation-error", FIELD_COUNT_CODE, `${row.length} field${plural(row.length)}, expected ${plan.width}`, {
|
|
1285
|
+
line,
|
|
1286
|
+
});
|
|
842
1287
|
counts.skippedNodes++;
|
|
843
1288
|
return;
|
|
844
1289
|
}
|
|
845
1290
|
const idText = plan.id >= 0 ? row[plan.id] : String(ordinal);
|
|
846
1291
|
if (plan.id >= 0 && isUnset(idText, quoted[plan.id])) {
|
|
847
|
-
report.error("missing-value", MISSING_ID_CODE, `
|
|
1292
|
+
report.error("missing-value", MISSING_ID_CODE, `blank id cell`, { line });
|
|
848
1293
|
counts.skippedNodes++;
|
|
849
1294
|
return;
|
|
850
1295
|
}
|
|
1296
|
+
if (plan.id >= 0) {
|
|
1297
|
+
this.checkPadded(idText, quoted[plan.id], line);
|
|
1298
|
+
}
|
|
851
1299
|
const { where } = this;
|
|
852
1300
|
where.line = line;
|
|
853
1301
|
where.element = idText;
|
|
@@ -855,7 +1303,7 @@ class TableReader {
|
|
|
855
1303
|
try {
|
|
856
1304
|
const id = plan.id >= 0 || plan.rowNumber ? this.coerce(idText) : ordinal;
|
|
857
1305
|
if (sink.indexOf(id) !== INVALID_INDEX) {
|
|
858
|
-
report.warning("merged", DUPLICATE_NODE_CODE, `
|
|
1306
|
+
report.warning("merged", DUPLICATE_NODE_CODE, `node ${JSON.stringify(id)} already exists; its attributes are overwritten`, where);
|
|
859
1307
|
}
|
|
860
1308
|
else {
|
|
861
1309
|
counts.nodes++;
|
|
@@ -872,6 +1320,18 @@ class TableReader {
|
|
|
872
1320
|
}
|
|
873
1321
|
this.writeAttributes(plan, row, quoted, index, line);
|
|
874
1322
|
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Warn once per import about an unquoted id cell with leading or trailing whitespace, which is
|
|
1325
|
+
* a different id from the bare text (RFC 4180 keeps spaces).
|
|
1326
|
+
* @param text - the cell text
|
|
1327
|
+
* @param quoted - whether the cell was quoted
|
|
1328
|
+
* @param line - the row's line
|
|
1329
|
+
*/
|
|
1330
|
+
checkPadded(text, quoted, line) {
|
|
1331
|
+
if (isPadded(text, quoted)) {
|
|
1332
|
+
this.state.report.warnOnce("validation-error", PADDED_ID_CODE, `id ${JSON.stringify(text)} has leading or trailing whitespace and is kept as written, a different id from the trimmed text`, { line, element: text });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
875
1335
|
/**
|
|
876
1336
|
* Coerce an id cell, reporting a merge under `ids: "number"`.
|
|
877
1337
|
* @param text - the cell text
|
|
@@ -935,36 +1395,176 @@ class TableReader {
|
|
|
935
1395
|
}
|
|
936
1396
|
}
|
|
937
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Whether a headerless table is a triple list whose third column is text (`a b knows`): every row
|
|
1400
|
+
* read so far has exactly three cells and a third cell that is not a number. That column is then an
|
|
1401
|
+
* attribute rather than the positional weight. A wider or ragged table keeps the positional weight,
|
|
1402
|
+
* so prose or an adjacency list read as edges still fails per row instead of importing.
|
|
1403
|
+
* @param width - the number of columns
|
|
1404
|
+
* @param sample - the data rows read so far
|
|
1405
|
+
* @returns true for a text triple list
|
|
1406
|
+
*/
|
|
1407
|
+
function isTextTriple(width, sample) {
|
|
1408
|
+
return (width === 3 &&
|
|
1409
|
+
sample.length > 0 &&
|
|
1410
|
+
sample.every((row) => row.length === 3 && !isBlank(row[2]) && numberOrNull(row[2]) === null));
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Whether the caller fixed how the input is read (a delimiter, the header, the table kind or a
|
|
1414
|
+
* column): it is then a table, and its first line is never refused as another format.
|
|
1415
|
+
* @param csv - the resolved options
|
|
1416
|
+
* @returns true when any of them is explicit
|
|
1417
|
+
*/
|
|
1418
|
+
function hasExplicitDialect(csv) {
|
|
1419
|
+
return (csv.delimiter !== null ||
|
|
1420
|
+
csv.header !== "auto" ||
|
|
1421
|
+
csv.table !== "auto" ||
|
|
1422
|
+
csv.sourceColumn !== null ||
|
|
1423
|
+
csv.targetColumn !== null ||
|
|
1424
|
+
csv.idColumn !== null ||
|
|
1425
|
+
(csv.typeColumn !== undefined && csv.typeColumn !== null));
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Fit a row whose width differs from the header's by one empty last cell: a row that omits the
|
|
1429
|
+
* empty last cell of a header ending in a delimiter gets an unset cell appended, and a row ending
|
|
1430
|
+
* in a delimiter the header does not have (Excel, pandas) loses that empty cell, reported once.
|
|
1431
|
+
* @param plan - the plan
|
|
1432
|
+
* @param row - the cells (changed in place)
|
|
1433
|
+
* @param quoted - whether each cell was quoted
|
|
1434
|
+
* @param report - the report
|
|
1435
|
+
* @param line - the row's line
|
|
1436
|
+
* @returns the quoted flags of the fitted row
|
|
1437
|
+
*/
|
|
1438
|
+
function completeRow(plan, row, quoted, report, line) {
|
|
1439
|
+
if (row.length === plan.width + 1 && isUnset(row[plan.width], quoted[plan.width])) {
|
|
1440
|
+
row.pop();
|
|
1441
|
+
report.warnOnce("validation-error", TRAILING_DELIMITER_CODE, `the row ends in a delimiter the header does not have; the empty last cell is dropped`, { line });
|
|
1442
|
+
return quoted;
|
|
1443
|
+
}
|
|
1444
|
+
if (!plan.optionalLast || row.length !== plan.width - 1) {
|
|
1445
|
+
return quoted;
|
|
1446
|
+
}
|
|
1447
|
+
const flags = quoted.slice(0, row.length);
|
|
1448
|
+
row.push("");
|
|
1449
|
+
flags.push(false);
|
|
1450
|
+
return flags;
|
|
1451
|
+
}
|
|
938
1452
|
const HEAD_BYTES = 4096;
|
|
939
|
-
const OTHER_FORMAT = /^\s*(<|[[{]|(strict\s+)?(di)?graph(\s+\S+)?\s*\{|\*vertices|creator\b|graph\s*\[)/i;
|
|
940
1453
|
/**
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
1454
|
+
* The openings of the formats a CSV reader would otherwise split into rows of text: an XML / HTML
|
|
1455
|
+
* tag or declaration (not an IRI such as `<http://a>`, nor a bracketed id such as `<alice smith>`
|
|
1456
|
+
* or `<a>,b`: a tag needs an attribute, or another tag after it), a JSON object (`{"` or `{}`) or
|
|
1457
|
+
* array, a DOT graph, a Pajek section, GML's `Creator "..."` or `graph [`. Matched against the
|
|
1458
|
+
* start of the input.
|
|
1459
|
+
*/
|
|
1460
|
+
/**
|
|
1461
|
+
* An endpoint column: the one the option names, else (with a header) the first with a known
|
|
1462
|
+
* endpoint name, else (without one) the column at its position in a headerless edge list.
|
|
1463
|
+
* @param names - the column names
|
|
1464
|
+
* @param header - whether the file has a header row
|
|
1465
|
+
* @param option - the sourceColumn / targetColumn option, or null
|
|
1466
|
+
* @param optionName - the option's name, for its error
|
|
1467
|
+
* @param known - the endpoint names to look for
|
|
1468
|
+
* @param position - the column of this endpoint in a headerless edge list
|
|
1469
|
+
* @returns the column index, or -1
|
|
1470
|
+
*/
|
|
1471
|
+
function endpointColumn(names, header, option, optionName, known, position) {
|
|
1472
|
+
if (option !== null) {
|
|
1473
|
+
return resolveColumnRef(names, option, optionName);
|
|
1474
|
+
}
|
|
1475
|
+
if (header) {
|
|
1476
|
+
return findColumn(names, known);
|
|
1477
|
+
}
|
|
1478
|
+
return names.length >= 2 ? position : -1;
|
|
1479
|
+
}
|
|
1480
|
+
const TAG_NAME = String.raw `<[a-z_][\w.-]*(?::[a-z_][\w.-]*)?`;
|
|
1481
|
+
const OTHER_FORMATS = [
|
|
1482
|
+
/^\s*<(?:\?xml|!doctype|!--)/i, // an XML declaration, a doctype or a comment
|
|
1483
|
+
new RegExp(String.raw `^\s*${TAG_NAME}\s*\/?>\s*(?:[^,;|\s]*<|$)`, "i"), // a tag, then another tag
|
|
1484
|
+
new RegExp(String.raw `^\s*${TAG_NAME}\s+[a-z_][\w.:-]*\s*=`, "i"), // a tag with an attribute
|
|
1485
|
+
/^\s*\/[*/]/, // a C comment (DOT)
|
|
1486
|
+
/^\s*\{\s*(?:["}]|$)/, // a JSON object
|
|
1487
|
+
/^\s*\[\s*(?:[[{"\]\d-]|$)/, // a JSON array
|
|
1488
|
+
/^\s*(?:strict\s+)?(?:di)?graph(?:\s+\S+)?\s*\{/i, // a DOT graph
|
|
1489
|
+
/^\s*\*(?:vertices|network|arcs|edges)\b/i, // a Pajek section
|
|
1490
|
+
/^\s*(?:creator\s+"|graph\s*\[)/i, // GML
|
|
1491
|
+
];
|
|
1492
|
+
/**
|
|
1493
|
+
* Sniff confidence for the registry: 0 for XML, JSON, GML, DOT (also a leading C comment) and Pajek
|
|
1494
|
+
* openings after the leading `#` / `%` comment lines the reader skips, and 0 for a head that is
|
|
1495
|
+
* only such comments (the comments of another format, too long to see past); otherwise a
|
|
1496
|
+
* delimited first row with endpoint headers is 0.9 (even when a later row has the wrong width, which
|
|
1497
|
+
* the importer reports), with an id header 0.6, and other consistently delimited rows 0.3, except
|
|
1498
|
+
* rows of more than three space-separated words, which is plain text such as a sentence, not a
|
|
1499
|
+
* headerless edge list (source, target, weight), so it is 0.
|
|
944
1500
|
* @param head - the first bytes of the input
|
|
945
1501
|
* @returns a confidence in 0..1
|
|
946
1502
|
*/
|
|
947
1503
|
function sniff(head) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1504
|
+
let label = "utf-8";
|
|
1505
|
+
if (head[0] === 0xff && head[1] === 0xfe) {
|
|
1506
|
+
label = "utf-16le";
|
|
1507
|
+
}
|
|
1508
|
+
else if (head[0] === 0xfe && head[1] === 0xff) {
|
|
1509
|
+
label = "utf-16be";
|
|
1510
|
+
}
|
|
1511
|
+
// the decoder drops the BOM it was chosen by; the reader skips leading comment lines
|
|
1512
|
+
const body = stripLeadingComments(new TextDecoder(label).decode(head.subarray(0, HEAD_BYTES)), COMMENT_CHARS);
|
|
1513
|
+
if (body.trim().length === 0 || OTHER_FORMATS.some((r) => r.test(body))) {
|
|
951
1514
|
return 0;
|
|
952
1515
|
}
|
|
953
1516
|
const newline = sniffNewline(body);
|
|
954
|
-
const delimiter = sniffDelimiter(body, newline);
|
|
1517
|
+
const delimiter = sniffDelimiter(body, newline, DELIMITER_CANDIDATES, '"', false, true);
|
|
955
1518
|
if (delimiter === null) {
|
|
956
|
-
|
|
1519
|
+
// a header naming both endpoints is evidence enough; a short or long row later is the
|
|
1520
|
+
// importer's to report, not a reason to refuse the file
|
|
1521
|
+
const first = body.split(newline, 1)[0];
|
|
1522
|
+
return DELIMITER_CANDIDATES.some((d) => hasEndpointHeader(first, d)) ? 0.9 : 0;
|
|
957
1523
|
}
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
const names = headerNames(
|
|
1524
|
+
// the first record, honoring quotes (R and pandas quote every header name)
|
|
1525
|
+
const records = splitRecords(body, delimiter, '"', 2, false, true) ?? [];
|
|
1526
|
+
const names = headerNames(records[0] ?? []);
|
|
961
1527
|
if (findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0) {
|
|
962
1528
|
return 0.9;
|
|
963
1529
|
}
|
|
964
|
-
|
|
1530
|
+
// a space-split row naming an id is as likely a line of text ("1 First node") as a header
|
|
1531
|
+
if (delimiter !== " " && findColumn(names, ID_NAMES) >= 0) {
|
|
965
1532
|
return 0.6;
|
|
966
1533
|
}
|
|
967
|
-
|
|
1534
|
+
if (delimiter === " " && names.length > 3) {
|
|
1535
|
+
return 0;
|
|
1536
|
+
}
|
|
1537
|
+
return looksLikeProse(body, newline, delimiter) ? 0 : 0.3;
|
|
1538
|
+
}
|
|
1539
|
+
/** A cell that reads as part of a sentence: words with a sentence end after a letter (`Monday.`, `world. Thanks`). */
|
|
1540
|
+
const SENTENCE = /[A-Za-z]{2}[.!?](?:\s|$)/;
|
|
1541
|
+
/**
|
|
1542
|
+
* Whether headerless delimited text is more likely a line of prose than an edge list: a single line
|
|
1543
|
+
* ("hello world", "Dear team, the meeting is on Monday."), or a cell of more than one word that
|
|
1544
|
+
* ends a sentence. An edge list a reader means to load has more than one row and ids, not sentences;
|
|
1545
|
+
* such input is still read as CSV when the caller names the format.
|
|
1546
|
+
* @param body - the head, decoded, without its leading comments
|
|
1547
|
+
* @param newline - the head's newline
|
|
1548
|
+
* @param delimiter - the sniffed delimiter
|
|
1549
|
+
* @returns true for prose
|
|
1550
|
+
*/
|
|
1551
|
+
function looksLikeProse(body, newline, delimiter) {
|
|
1552
|
+
const lines = body.split(newline).filter((l) => l.trim() !== "");
|
|
1553
|
+
if (lines.length < 2) {
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
const rows = splitRecords(body, delimiter, '"', 8, false, true) ?? [];
|
|
1557
|
+
return rows.some((row) => row.some((cell) => /\S\s+\S/.test(cell.trim()) && SENTENCE.test(cell.trim())));
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Whether one line, split on a delimiter, names a source and a target column.
|
|
1561
|
+
* @param line - the first line of the input
|
|
1562
|
+
* @param delimiter - the delimiter to split on
|
|
1563
|
+
* @returns true for an edge table header
|
|
1564
|
+
*/
|
|
1565
|
+
function hasEndpointHeader(line, delimiter) {
|
|
1566
|
+
const names = headerNames(splitRecords(line, delimiter, '"', 1, false, true)?.[0] ?? []);
|
|
1567
|
+
return names.length > 1 && findColumn(names, SOURCE_NAMES) >= 0 && findColumn(names, TARGET_NAMES) >= 0;
|
|
968
1568
|
}
|
|
969
1569
|
/**
|
|
970
1570
|
* Import a CSV edge table (and an optional node table) into a sink.
|
|
@@ -989,23 +1589,54 @@ async function importCsv(input, sink, options) {
|
|
|
989
1589
|
resolver: new DirectionResolver(sink, report, common.onMixedDirection),
|
|
990
1590
|
headerSet: false,
|
|
991
1591
|
commentDirected: null,
|
|
1592
|
+
commentText: "",
|
|
992
1593
|
};
|
|
993
1594
|
if (csv.nodes !== null) {
|
|
994
1595
|
await new TableReader(state, csv.nodes, "nodes", false).read();
|
|
995
1596
|
}
|
|
996
1597
|
await new TableReader(state, input, csv.table, true).read();
|
|
997
1598
|
if (state.coercer.mergeCount > 1) {
|
|
998
|
-
report.warning("coercion", ID_MERGED_CODE, `${state.coercer.mergeCount} id cell(
|
|
1599
|
+
report.warning("coercion", ID_MERGED_CODE, `${state.coercer.mergeCount} id cell${plural(state.coercer.mergeCount)} merged into ids other cells already produced under ids: "number"`);
|
|
999
1600
|
}
|
|
1000
1601
|
throwIfAborted(common.signal);
|
|
1001
1602
|
return report.finish();
|
|
1002
1603
|
}
|
|
1003
|
-
/**
|
|
1604
|
+
/**
|
|
1605
|
+
* The CSV / TSV importer plugin (subpath `@graphty/graph-io/csv`).
|
|
1606
|
+
* @category Built-in formats
|
|
1607
|
+
*/
|
|
1004
1608
|
export const csvImporter = Object.freeze({
|
|
1005
1609
|
format: "csv",
|
|
1610
|
+
options: Object.freeze([
|
|
1611
|
+
"decimal",
|
|
1612
|
+
"delimiter",
|
|
1613
|
+
"header",
|
|
1614
|
+
"idColumn",
|
|
1615
|
+
"labelColumn",
|
|
1616
|
+
"nodes",
|
|
1617
|
+
"rowNumberIds",
|
|
1618
|
+
"sourceColumn",
|
|
1619
|
+
"table",
|
|
1620
|
+
"targetColumn",
|
|
1621
|
+
"typeColumn",
|
|
1622
|
+
]),
|
|
1006
1623
|
extensions: Object.freeze([".csv", ".tsv", ".edges", ".edgelist"]),
|
|
1007
1624
|
mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values", "text/plain"]),
|
|
1008
1625
|
sniff,
|
|
1009
1626
|
import: importCsv,
|
|
1010
1627
|
});
|
|
1628
|
+
/**
|
|
1629
|
+
* The `nodes` option as an input: a Blob or File is read through its stream. Checked by shape, so a
|
|
1630
|
+
* Blob from another realm or from Node's `fs.openAsBlob()` counts too.
|
|
1631
|
+
* @param value - the option value
|
|
1632
|
+
* @returns the input, or null when there is no node table
|
|
1633
|
+
*/
|
|
1634
|
+
function nodesInput(value) {
|
|
1635
|
+
if (value === undefined) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
const object = value;
|
|
1639
|
+
const blob = typeof value === "object" && typeof object.stream === "function" && typeof object.size === "number";
|
|
1640
|
+
return blob ? value.stream() : value;
|
|
1641
|
+
}
|
|
1011
1642
|
//# sourceMappingURL=importer.js.map
|