@graphty/graph-io 0.3.24 → 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
|
@@ -0,0 +1,4333 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
const FETCH_CODE = "E_FETCH";
|
|
3
|
+
const MISSING_ID_CODE = "E_MISSING_ID";
|
|
4
|
+
const MISSING_ENDPOINT_CODE = "E_MISSING_ENDPOINT";
|
|
5
|
+
const DUPLICATE_NODE_CODE = "W_DUPLICATE_NODE";
|
|
6
|
+
const NO_GRAPH_CODE = "E_NO_GRAPH";
|
|
7
|
+
const MULTIPLE_GRAPHS_CODE = "W_MULTIPLE_GRAPHS";
|
|
8
|
+
const EMPTY_INPUT_CODE = "E_EMPTY_INPUT";
|
|
9
|
+
const SYNTAX_CODE = "E_SYNTAX";
|
|
10
|
+
const XML_SYNTAX_CODE = "E_XML_SYNTAX";
|
|
11
|
+
const UNKNOWN_ELEMENT_CODE = "W_UNKNOWN_ELEMENT";
|
|
12
|
+
const STRAY_TEXT_CODE = "W_STRAY_TEXT";
|
|
13
|
+
const UNKNOWN_PARENT_CODE = "E_UNKNOWN_PARENT";
|
|
14
|
+
const HYPEREDGE_CODE = "E_HYPEREDGE";
|
|
15
|
+
const DUPLICATE_KEY_CODE = "E_DUPLICATE_KEY";
|
|
16
|
+
const COLUMN_RENAMED_CODE = "W_COLUMN_RENAMED";
|
|
17
|
+
const COLUMN_DROPPED_CODE = "W_COLUMN_DROPPED";
|
|
18
|
+
const ROLE_TAKEN_CODE = "W_ROLE_TAKEN";
|
|
19
|
+
const PRECISION_CODE = "W_PRECISION";
|
|
20
|
+
const ID_MERGED_CODE = "W_ID_MERGED";
|
|
21
|
+
const SINK_OPTION_CODE = "W_SINK_OPTION";
|
|
22
|
+
const OPTION_IGNORED_CODE = "W_OPTION_IGNORED";
|
|
23
|
+
const INVALID_UTF8_CODE = "E_INVALID_UTF8";
|
|
24
|
+
const INVALID_ENCODING_CODE = "E_INVALID_ENCODING";
|
|
25
|
+
const ENCODING_FALLBACK_CODE = "W_ENCODING_FALLBACK";
|
|
26
|
+
const UNKNOWN_ENCODING_CODE = "W_UNKNOWN_ENCODING";
|
|
27
|
+
const ENCODING_CONFLICT_CODE = "W_ENCODING_CONFLICT";
|
|
28
|
+
const CONTROL_CHARACTER_CODE = "W_CONTROL_CHARACTER";
|
|
29
|
+
const FOREIGN_FORMAT_CODE = "E_FOREIGN_FORMAT";
|
|
30
|
+
const ISSUES_SUPPRESSED_CODE = "W_ISSUES_SUPPRESSED";
|
|
31
|
+
const UNKNOWN_XML_ATTRIBUTE_CODE = "W_UNKNOWN_XML_ATTRIBUTE";
|
|
32
|
+
const DIRECTION_REFUSED_CODE = "W_DIRECTION_REFUSED";
|
|
33
|
+
const DIRECTION_FORCED_CODE = "W_DIRECTION_FORCED";
|
|
34
|
+
const MIXED_DIRECTION_CODE = "E_MIXED_DIRECTION";
|
|
35
|
+
const COUNT_HINT_CODE = "W_COUNT_HINT";
|
|
36
|
+
const UNKNOWN_ATTR_TYPE_CODE = "W_UNKNOWN_ATTR_TYPE";
|
|
37
|
+
const BAD_DEFAULT_CODE = "W_BAD_DEFAULT";
|
|
38
|
+
const BAD_OPTIONS_CODE = "W_BAD_OPTIONS";
|
|
39
|
+
const DUPLICATE_EDGE_ID_CODE = "E_DUPLICATE_EDGE_ID";
|
|
40
|
+
const BAD_VALUE_CODE = "E_BAD_VALUE";
|
|
41
|
+
const WIDENED_CODE = "W_WIDENED";
|
|
42
|
+
const DANGLING_REFERENCE_CODE = "W_DANGLING_REFERENCE";
|
|
43
|
+
const DUPLICATE_ATTRIBUTE_CODE = "W_DUPLICATE_ATTRIBUTE";
|
|
44
|
+
const PARENT_CYCLE_CODE = "E_PARENT_CYCLE";
|
|
45
|
+
const EQUATION_AS_TEXT_CODE = "W_EQUATION_AS_TEXT";
|
|
46
|
+
const STYLES_NOT_IMPORTED_CODE = "W_STYLES_NOT_IMPORTED";
|
|
47
|
+
const BAD_ASPECT_BLOCK_CODE = "E_BAD_ASPECT_BLOCK";
|
|
48
|
+
const MULTI_ASPECT_FRAGMENT_CODE = "W_MULTI_ASPECT_FRAGMENT";
|
|
49
|
+
const SINGLE_OBJECT_ASPECT_CODE = "W_SINGLE_OBJECT_ASPECT";
|
|
50
|
+
const JSON_NONSTANDARD_NUMBER_CODE = "W_JSON_NONSTANDARD_NUMBER";
|
|
51
|
+
const ASPECT_ORDER_CODE = "W_ASPECT_ORDER";
|
|
52
|
+
const COUNT_MISMATCH_CODE = "W_COUNT_MISMATCH";
|
|
53
|
+
const STATUS_FAILED_CODE = "E_STATUS_FAILED";
|
|
54
|
+
const STATUS_WARNING_CODE = "W_STATUS_WARNING";
|
|
55
|
+
const TOO_LARGE_CODE = "E_TOO_LARGE";
|
|
56
|
+
const COLUMN_TYPE_CODE = "E_COLUMN_TYPE";
|
|
57
|
+
const GRAPH_NOT_FOUND_CODE = "E_GRAPH_NOT_FOUND";
|
|
58
|
+
const AMBIGUOUS_GRAPH_NAME_CODE = "E_AMBIGUOUS_GRAPH_NAME";
|
|
59
|
+
const DUPLICATE_EDGE_CODE = "E_DUPLICATE_EDGE";
|
|
60
|
+
const SELF_LOOP_CODE = "E_SELF_LOOP";
|
|
61
|
+
const EDGES_MERGED_CODE = "W_EDGES_MERGED";
|
|
62
|
+
const SELF_LOOPS_DROPPED_CODE = "W_SELF_LOOPS_DROPPED";
|
|
63
|
+
const UNKNOWN_OPTION_CODE = "W_UNKNOWN_OPTION";
|
|
64
|
+
const BAD_LINE_CODE = "E_BAD_LINE";
|
|
65
|
+
const WEIGHT_NOT_FOUND_CODE = "W_WEIGHT_NOT_FOUND";
|
|
66
|
+
const ROLE_DROPPED_CODE = "W_ROLE_DROPPED";
|
|
67
|
+
const PARENTS_DROPPED_CODE = "W_PARENTS_DROPPED";
|
|
68
|
+
const TEMPORAL_DROPPED_CODE = "W_TEMPORAL_DROPPED";
|
|
69
|
+
const MUTUAL_EXPANDED_CODE = "W_MUTUAL_EXPANDED";
|
|
70
|
+
const MUTUAL_AS_UNDIRECTED_CODE = "W_MUTUAL_AS_UNDIRECTED";
|
|
71
|
+
const ID_TEXT_TYPE_CODE = "W_ID_TEXT_TYPE";
|
|
72
|
+
const ID_TEXT_COLLISION_CODE = "E_ID_TEXT_COLLISION";
|
|
73
|
+
const TEMPORAL_TEXT_DROPPED_CODE = "W_TEMPORAL_TEXT_DROPPED";
|
|
74
|
+
const WEIGHTS_DROPPED_CODE = "W_WEIGHTS_DROPPED";
|
|
75
|
+
const WEIGHT_KEY_CLASH_CODE = "W_WEIGHT_KEY_CLASH";
|
|
76
|
+
const ROLE_ASSUMED_CODE = "W_ROLE_ASSUMED";
|
|
77
|
+
const COLUMN_RENAMED_LOSS_CODE = "W_COLUMN_NAME_CHANGED";
|
|
78
|
+
const EMPTY_COLUMN_DROPPED_CODE = "W_EMPTY_COLUMN_DROPPED";
|
|
79
|
+
const STORAGE_CLASS_CODE = "W_STORAGE_CLASS_CHANGED";
|
|
80
|
+
const INTEGRAL_F64_CODE = "W_INTEGRAL_F64_AS_I32";
|
|
81
|
+
const TEXT_INFERRED_CODE = "W_TEXT_INFERRED";
|
|
82
|
+
const XML_ILLEGAL_CHAR_CODE = "E_XML_ILLEGAL_CHAR";
|
|
83
|
+
const COLUMN_AS_PROPERTY_VALUE_CODE = "W_COLUMN_AS_PROPERTY_VALUE";
|
|
84
|
+
const RELATION_ASSUMED_CODE = "W_RELATION_ASSUMED";
|
|
85
|
+
const TYPEDEF_NODES_CODE = "W_TYPEDEF_NODES";
|
|
86
|
+
const GRAPH_COLUMN_AS_METADATA_CODE = "W_GRAPH_COLUMN_AS_METADATA";
|
|
87
|
+
const DIRECTION_DROPPED_CODE = "W_DIRECTION_DROPPED";
|
|
88
|
+
const NONFINITE_AS_NULL_CODE = "W_NONFINITE_AS_NULL";
|
|
89
|
+
const NODE_ORDER_CODE = "W_NODE_ORDER";
|
|
90
|
+
const OPTIONS_GAINED_CODE = "W_OPTIONS_GAINED";
|
|
91
|
+
const ELEMENT_ISSUE = Object.freeze({
|
|
92
|
+
/** A node id of a type the graph cannot hold (an empty id, a non-finite number, an object). The node is skipped. */
|
|
93
|
+
INVALID_ID: "E_INVALID_ID",
|
|
94
|
+
/** An edge names a node the file never declares, while `addMissingNodes` is false. The edge is skipped. */
|
|
95
|
+
UNKNOWN_NODE: "E_UNKNOWN_NODE",
|
|
96
|
+
/**
|
|
97
|
+
* An edge weight that is not a number (text such as "2,5" with a decimal comma, NaN, a boolean). The whole edge is
|
|
98
|
+
* skipped, not only its weight.
|
|
99
|
+
*/
|
|
100
|
+
INVALID_WEIGHT: "E_INVALID_WEIGHT",
|
|
101
|
+
/** A second edge between the same two nodes, while `duplicateEdges` is "error". The edge is skipped. */
|
|
102
|
+
DUPLICATE_EDGE: DUPLICATE_EDGE_CODE,
|
|
103
|
+
/** An edge from a node to itself, while `selfLoops` is "error". The edge is skipped. */
|
|
104
|
+
SELF_LOOP: SELF_LOOP_CODE,
|
|
105
|
+
/** An edge id declared twice. The second edge is skipped. */
|
|
106
|
+
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE
|
|
107
|
+
});
|
|
108
|
+
const INPUT_ISSUE = Object.freeze({
|
|
109
|
+
/** The input is empty or holds only whitespace. The import stops. */
|
|
110
|
+
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
111
|
+
/** The input, or one of its lines, is longer than a JavaScript string can hold. The import stops. */
|
|
112
|
+
TOO_LARGE: TOO_LARGE_CODE,
|
|
113
|
+
/** A byte order mark, the encoding option and the declared encoding disagree. */
|
|
114
|
+
ENCODING_CONFLICT: ENCODING_CONFLICT_CODE,
|
|
115
|
+
/** A control character or a stray U+FEFF in the text, or an ignored trailing Ctrl-Z. */
|
|
116
|
+
CONTROL_CHARACTER: CONTROL_CHARACTER_CODE,
|
|
117
|
+
/** The input is an HTML page, a PDF, compressed or zip data, or an image. The import stops. */
|
|
118
|
+
FOREIGN_FORMAT: FOREIGN_FORMAT_CODE,
|
|
119
|
+
/** Warnings of one code beyond the number a report keeps, counted in one warning. */
|
|
120
|
+
ISSUES_SUPPRESSED: ISSUES_SUPPRESSED_CODE
|
|
121
|
+
});
|
|
122
|
+
function plural(n) {
|
|
123
|
+
return n === 1 ? "" : "s";
|
|
124
|
+
}
|
|
125
|
+
function agree(n, one, many) {
|
|
126
|
+
return n === 1 ? one : many;
|
|
127
|
+
}
|
|
128
|
+
function elementCount(nodes, edges) {
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (nodes > 0 || edges === 0) {
|
|
131
|
+
parts.push(`${nodes} node${plural(nodes)}`);
|
|
132
|
+
}
|
|
133
|
+
if (edges > 0) {
|
|
134
|
+
parts.push(`${edges} edge${plural(edges)}`);
|
|
135
|
+
}
|
|
136
|
+
return parts.join(" and ");
|
|
137
|
+
}
|
|
138
|
+
function withArticle(word) {
|
|
139
|
+
return `${/^[aeiou]/i.test(word) ? "an" : "a"} ${word}`;
|
|
140
|
+
}
|
|
141
|
+
const TIME_TEXT_ROLE = "timeText";
|
|
142
|
+
const TIME_TEXT_SUFFIX = ".text";
|
|
143
|
+
const DATE_TIME$1 = /^(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})(?:[T ]([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\.([0-9]{1,9}))?)?)?(Z|z|[+-][0-9]{2}(?::?[0-9]{2})?)?$/;
|
|
144
|
+
const TIME$1 = /^([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\.([0-9]{1,9}))?)?(Z|z|[+-][0-9]{2}(?::?[0-9]{2})?)?$/;
|
|
145
|
+
const NUMERIC = /^[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?$/;
|
|
146
|
+
const MS_PER_MINUTE = 6e4;
|
|
147
|
+
const MS_PER_DAY = 864e5;
|
|
148
|
+
function parseTemporal(text, kind) {
|
|
149
|
+
let value;
|
|
150
|
+
switch (kind) {
|
|
151
|
+
case "date":
|
|
152
|
+
case "dateTime":
|
|
153
|
+
case "localDateTime":
|
|
154
|
+
value = parseDateTime(text, kind);
|
|
155
|
+
break;
|
|
156
|
+
case "time":
|
|
157
|
+
case "localTime":
|
|
158
|
+
value = parseTime(text, kind);
|
|
159
|
+
break;
|
|
160
|
+
default: {
|
|
161
|
+
const name = kind;
|
|
162
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown temporal kind ${name}`, { kind: name });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const canonical = formatTemporal(value, kind);
|
|
166
|
+
return { value, text: canonical === text ? null : text };
|
|
167
|
+
}
|
|
168
|
+
function parseTimeText(text, timeFormat) {
|
|
169
|
+
const trimmed = text.trim();
|
|
170
|
+
switch (timeFormat) {
|
|
171
|
+
case "integer":
|
|
172
|
+
case "double":
|
|
173
|
+
return parseNumericTime(trimmed);
|
|
174
|
+
case "date":
|
|
175
|
+
return parseTemporal(trimmed, "date");
|
|
176
|
+
case "dateTime":
|
|
177
|
+
return parseTemporal(trimmed, "dateTime");
|
|
178
|
+
case null:
|
|
179
|
+
return NUMERIC.test(trimmed) ? parseNumericTime(trimmed) : parseTemporal(trimmed, "dateTime");
|
|
180
|
+
default: {
|
|
181
|
+
const name = timeFormat;
|
|
182
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown timeformat ${name}`, { timeFormat: name });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function formatTemporal(value, kind) {
|
|
187
|
+
switch (kind) {
|
|
188
|
+
case "date": {
|
|
189
|
+
const d = new Date(value);
|
|
190
|
+
return `${year(d)}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
|
|
191
|
+
}
|
|
192
|
+
case "dateTime":
|
|
193
|
+
case "localDateTime": {
|
|
194
|
+
const d = new Date(value);
|
|
195
|
+
const date = `${year(d)}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
|
|
196
|
+
const time = clock(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
197
|
+
return kind === "dateTime" ? `${date}T${time}Z` : `${date}T${time}`;
|
|
198
|
+
}
|
|
199
|
+
case "time":
|
|
200
|
+
case "localTime": {
|
|
201
|
+
const ms = (value % MS_PER_DAY + MS_PER_DAY) % MS_PER_DAY;
|
|
202
|
+
const h = Math.floor(ms / 36e5);
|
|
203
|
+
const m = Math.floor(ms % 36e5 / MS_PER_MINUTE);
|
|
204
|
+
const s = Math.floor(ms % MS_PER_MINUTE / 1e3);
|
|
205
|
+
const text = clock(h, m, s, ms % 1e3);
|
|
206
|
+
return kind === "time" ? `${text}Z` : text;
|
|
207
|
+
}
|
|
208
|
+
default: {
|
|
209
|
+
const name = kind;
|
|
210
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown temporal kind ${name}`, { kind: name });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function formatTimeValue(value, timeFormat) {
|
|
215
|
+
switch (timeFormat) {
|
|
216
|
+
case "date":
|
|
217
|
+
return formatTemporal(value, "date");
|
|
218
|
+
case "dateTime":
|
|
219
|
+
return formatTemporal(value, "dateTime");
|
|
220
|
+
case "integer":
|
|
221
|
+
case "double":
|
|
222
|
+
case null:
|
|
223
|
+
return String(value);
|
|
224
|
+
default: {
|
|
225
|
+
const name = timeFormat;
|
|
226
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown timeformat ${name}`, { timeFormat: name });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function timeTextCompanion(column) {
|
|
231
|
+
return {
|
|
232
|
+
name: column + TIME_TEXT_SUFFIX,
|
|
233
|
+
dtype: "string",
|
|
234
|
+
role: TIME_TEXT_ROLE,
|
|
235
|
+
nullable: true,
|
|
236
|
+
extra: { for: column }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function parseNumericTime(text) {
|
|
240
|
+
if (!NUMERIC.test(text)) {
|
|
241
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${text}" is not a numeric time`, { value: text });
|
|
242
|
+
}
|
|
243
|
+
const value = Number(text);
|
|
244
|
+
return { value, text: String(value) === text ? null : text };
|
|
245
|
+
}
|
|
246
|
+
function parseDateTime(text, kind) {
|
|
247
|
+
const m = DATE_TIME$1.exec(text);
|
|
248
|
+
if (m === null) {
|
|
249
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${text}" is not an ISO-8601 ${kind}`, { value: text, kind });
|
|
250
|
+
}
|
|
251
|
+
const y = Number(m[1]);
|
|
252
|
+
const month = Number(m[2]);
|
|
253
|
+
const day = Number(m[3]);
|
|
254
|
+
const hour = m[4] === void 0 ? 0 : Number(m[4]);
|
|
255
|
+
const minute = m[5] === void 0 ? 0 : Number(m[5]);
|
|
256
|
+
const second = m[6] === void 0 ? 0 : Number(m[6]);
|
|
257
|
+
const ms = m[7] === void 0 ? 0 : fractionMs(m[7]);
|
|
258
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 24 || minute > 59 || second > 60) {
|
|
259
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${text}" is out of range for a ${kind}`, { value: text, kind });
|
|
260
|
+
}
|
|
261
|
+
const d = /* @__PURE__ */ new Date(0);
|
|
262
|
+
d.setUTCFullYear(y, month - 1, day);
|
|
263
|
+
d.setUTCHours(hour, minute, second, ms);
|
|
264
|
+
if (d.getUTCDate() !== day && hour !== 24) {
|
|
265
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${text}" is not a calendar date`, { value: text, kind });
|
|
266
|
+
}
|
|
267
|
+
const offset = m[8] === void 0 ? 0 : offsetMinutes(m[8]);
|
|
268
|
+
return d.getTime() - offset * MS_PER_MINUTE;
|
|
269
|
+
}
|
|
270
|
+
function parseTime(text, kind) {
|
|
271
|
+
const m = TIME$1.exec(text);
|
|
272
|
+
if (m === null) {
|
|
273
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${text}" is not an ISO-8601 ${kind}`, { value: text, kind });
|
|
274
|
+
}
|
|
275
|
+
const hour = Number(m[1]);
|
|
276
|
+
const minute = Number(m[2]);
|
|
277
|
+
const second = m[3] === void 0 ? 0 : Number(m[3]);
|
|
278
|
+
const ms = m[4] === void 0 ? 0 : fractionMs(m[4]);
|
|
279
|
+
if (hour > 24 || minute > 59 || second > 60) {
|
|
280
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${text}" is out of range for a ${kind}`, { value: text, kind });
|
|
281
|
+
}
|
|
282
|
+
const local = ((hour * 60 + minute) * 60 + second) * 1e3 + ms;
|
|
283
|
+
const offset = m[5] === void 0 ? 0 : offsetMinutes(m[5]);
|
|
284
|
+
const utc = local - offset * MS_PER_MINUTE;
|
|
285
|
+
return (utc % MS_PER_DAY + MS_PER_DAY) % MS_PER_DAY;
|
|
286
|
+
}
|
|
287
|
+
function fractionMs(digits) {
|
|
288
|
+
return Math.floor(Number(`0.${digits}`) * 1e3);
|
|
289
|
+
}
|
|
290
|
+
function offsetMinutes(zone) {
|
|
291
|
+
if (zone === "Z" || zone === "z") {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
const sign = zone.startsWith("-") ? -1 : 1;
|
|
295
|
+
const digits = zone.slice(1).replace(":", "");
|
|
296
|
+
const hours = Number(digits.slice(0, 2));
|
|
297
|
+
const minutes = digits.length > 2 ? Number(digits.slice(2, 4)) : 0;
|
|
298
|
+
return sign * (hours * 60 + minutes);
|
|
299
|
+
}
|
|
300
|
+
function pad2(n) {
|
|
301
|
+
return n < 10 ? `0${n}` : String(n);
|
|
302
|
+
}
|
|
303
|
+
function year(d) {
|
|
304
|
+
const y = d.getUTCFullYear();
|
|
305
|
+
const abs = String(Math.abs(y)).padStart(4, "0");
|
|
306
|
+
return y < 0 ? `-${abs}` : abs;
|
|
307
|
+
}
|
|
308
|
+
function clock(h, m, s, ms) {
|
|
309
|
+
const base = `${pad2(h)}:${pad2(m)}:${pad2(s)}`;
|
|
310
|
+
return ms === 0 ? base : `${base}.${String(ms).padStart(3, "0")}`;
|
|
311
|
+
}
|
|
312
|
+
const BOOLEAN = { dtype: "bool", kind: "boolean" };
|
|
313
|
+
const INTEGER = { dtype: "i32", kind: "integer" };
|
|
314
|
+
const LONG = { dtype: "f64", kind: "long", precision: true };
|
|
315
|
+
const FLOAT = { dtype: "f32", kind: "float" };
|
|
316
|
+
const DOUBLE = { dtype: "f64", kind: "double" };
|
|
317
|
+
const STRING = { dtype: "string", kind: "string" };
|
|
318
|
+
const DATE = { dtype: "f64", kind: "temporal", temporal: "date" };
|
|
319
|
+
const DATE_TIME = { dtype: "f64", kind: "temporal", temporal: "dateTime" };
|
|
320
|
+
const LOCAL_DATE_TIME = { dtype: "f64", kind: "temporal", temporal: "localDateTime" };
|
|
321
|
+
const TIME = { dtype: "f64", kind: "temporal", temporal: "time" };
|
|
322
|
+
const LOCAL_TIME = { dtype: "f64", kind: "temporal", temporal: "localTime" };
|
|
323
|
+
const DURATION = { dtype: "string", kind: "duration" };
|
|
324
|
+
const POINT = { dtype: "json", kind: "point" };
|
|
325
|
+
const RECORD = { dtype: "json", kind: "json" };
|
|
326
|
+
const GEXF_SCALARS = {
|
|
327
|
+
integer: INTEGER,
|
|
328
|
+
long: LONG,
|
|
329
|
+
double: DOUBLE,
|
|
330
|
+
float: FLOAT,
|
|
331
|
+
boolean: BOOLEAN,
|
|
332
|
+
string: STRING,
|
|
333
|
+
anyuri: STRING,
|
|
334
|
+
char: STRING,
|
|
335
|
+
byte: INTEGER,
|
|
336
|
+
short: INTEGER,
|
|
337
|
+
bigdecimal: STRING,
|
|
338
|
+
biginteger: STRING,
|
|
339
|
+
date: DATE,
|
|
340
|
+
datetime: DATE_TIME
|
|
341
|
+
};
|
|
342
|
+
const GRAPHML_SCALARS = {
|
|
343
|
+
boolean: BOOLEAN,
|
|
344
|
+
int: INTEGER,
|
|
345
|
+
long: LONG,
|
|
346
|
+
float: FLOAT,
|
|
347
|
+
double: DOUBLE,
|
|
348
|
+
string: STRING
|
|
349
|
+
};
|
|
350
|
+
const GML_SCALARS = {
|
|
351
|
+
int: INTEGER,
|
|
352
|
+
real: DOUBLE,
|
|
353
|
+
string: STRING,
|
|
354
|
+
record: RECORD
|
|
355
|
+
};
|
|
356
|
+
const NEO4J_SCALARS = {
|
|
357
|
+
int: INTEGER,
|
|
358
|
+
integer: LONG,
|
|
359
|
+
long: LONG,
|
|
360
|
+
float: FLOAT,
|
|
361
|
+
double: DOUBLE,
|
|
362
|
+
boolean: BOOLEAN,
|
|
363
|
+
byte: INTEGER,
|
|
364
|
+
short: INTEGER,
|
|
365
|
+
char: STRING,
|
|
366
|
+
string: STRING,
|
|
367
|
+
point: POINT,
|
|
368
|
+
date: DATE,
|
|
369
|
+
localtime: LOCAL_TIME,
|
|
370
|
+
time: TIME,
|
|
371
|
+
localdatetime: LOCAL_DATE_TIME,
|
|
372
|
+
datetime: DATE_TIME,
|
|
373
|
+
duration: DURATION
|
|
374
|
+
};
|
|
375
|
+
const TABLES = {
|
|
376
|
+
gexf: GEXF_SCALARS,
|
|
377
|
+
graphml: GRAPHML_SCALARS,
|
|
378
|
+
gml: GML_SCALARS,
|
|
379
|
+
neo4j: NEO4J_SCALARS
|
|
380
|
+
};
|
|
381
|
+
const INTEGER_TEXT$1 = /^[+-]?[0-9]+$/;
|
|
382
|
+
const DECIMAL_TEXT = /^[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?$/;
|
|
383
|
+
const I32_MIN = -2147483648;
|
|
384
|
+
const I32_MAX = 2147483647;
|
|
385
|
+
function mapDeclaredType(format, type, long) {
|
|
386
|
+
const table = TABLES[format];
|
|
387
|
+
let key = type.trim().toLowerCase();
|
|
388
|
+
let list2 = false;
|
|
389
|
+
if (format === "gexf" && key.startsWith("list") && key.length > 4) {
|
|
390
|
+
list2 = true;
|
|
391
|
+
key = key.slice(4);
|
|
392
|
+
} else if (format === "neo4j" && key.endsWith("[]")) {
|
|
393
|
+
list2 = true;
|
|
394
|
+
key = key.slice(0, -2);
|
|
395
|
+
} else if (format === "gml" && key === "list") {
|
|
396
|
+
return {
|
|
397
|
+
declared: type,
|
|
398
|
+
dtype: "list",
|
|
399
|
+
itemDtype: "json",
|
|
400
|
+
kind: "json",
|
|
401
|
+
list: true,
|
|
402
|
+
temporal: null,
|
|
403
|
+
precision: false
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
let entry = table[key];
|
|
407
|
+
if (entry === void 0) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
if (entry.kind === "long" && long === "string") {
|
|
411
|
+
entry = STRING;
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
declared: type,
|
|
415
|
+
dtype: list2 ? "list" : entry.dtype,
|
|
416
|
+
itemDtype: list2 ? entry.dtype : null,
|
|
417
|
+
kind: entry.kind,
|
|
418
|
+
list: list2,
|
|
419
|
+
temporal: entry.temporal ?? null,
|
|
420
|
+
precision: entry.precision ?? false
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function stringSpec(declared) {
|
|
424
|
+
return {
|
|
425
|
+
declared: declared ?? "",
|
|
426
|
+
dtype: "string",
|
|
427
|
+
itemDtype: null,
|
|
428
|
+
kind: "string",
|
|
429
|
+
list: false,
|
|
430
|
+
temporal: null,
|
|
431
|
+
precision: false
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function parseScalarText(text, kind, temporal = null) {
|
|
435
|
+
switch (kind) {
|
|
436
|
+
case "boolean": {
|
|
437
|
+
const value = parseBooleanText(text);
|
|
438
|
+
if (value === null) {
|
|
439
|
+
throw typeError(text, kind);
|
|
440
|
+
}
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
case "integer": {
|
|
444
|
+
const trimmed = text.trim();
|
|
445
|
+
if (!INTEGER_TEXT$1.test(trimmed)) {
|
|
446
|
+
throw typeError(text, kind);
|
|
447
|
+
}
|
|
448
|
+
const value = Number(trimmed);
|
|
449
|
+
if (value < I32_MIN || value > I32_MAX) {
|
|
450
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${trimmed}" is outside the 32-bit integer range`, {
|
|
451
|
+
value: trimmed,
|
|
452
|
+
kind
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
case "long": {
|
|
458
|
+
const trimmed = text.trim();
|
|
459
|
+
if (!INTEGER_TEXT$1.test(trimmed)) {
|
|
460
|
+
throw typeError(text, kind);
|
|
461
|
+
}
|
|
462
|
+
return Number(trimmed);
|
|
463
|
+
}
|
|
464
|
+
case "float":
|
|
465
|
+
case "double":
|
|
466
|
+
return parseDecimalText(text, kind);
|
|
467
|
+
case "string":
|
|
468
|
+
case "duration":
|
|
469
|
+
return text;
|
|
470
|
+
case "temporal": {
|
|
471
|
+
if (temporal === null) {
|
|
472
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "a temporal value needs its temporal kind", { kind });
|
|
473
|
+
}
|
|
474
|
+
return parseTemporal(text.trim(), temporal).value;
|
|
475
|
+
}
|
|
476
|
+
case "point":
|
|
477
|
+
return parsePointText(text);
|
|
478
|
+
case "json":
|
|
479
|
+
return parseJsonText(text);
|
|
480
|
+
default: {
|
|
481
|
+
const name = kind;
|
|
482
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown value kind ${name}`, { kind: name });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function parseBooleanText(text) {
|
|
487
|
+
switch (text.trim().toLowerCase()) {
|
|
488
|
+
case "true":
|
|
489
|
+
case "1":
|
|
490
|
+
return true;
|
|
491
|
+
case "false":
|
|
492
|
+
case "0":
|
|
493
|
+
return false;
|
|
494
|
+
default:
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function parseDecimalText(text, kind = "double") {
|
|
499
|
+
const trimmed = text.trim();
|
|
500
|
+
if (DECIMAL_TEXT.test(trimmed)) {
|
|
501
|
+
return Number(trimmed);
|
|
502
|
+
}
|
|
503
|
+
switch (trimmed) {
|
|
504
|
+
case "INF":
|
|
505
|
+
case "+INF":
|
|
506
|
+
case "Infinity":
|
|
507
|
+
case "+Infinity":
|
|
508
|
+
return Infinity;
|
|
509
|
+
case "-INF":
|
|
510
|
+
case "-Infinity":
|
|
511
|
+
return -Infinity;
|
|
512
|
+
case "NaN":
|
|
513
|
+
return NaN;
|
|
514
|
+
default:
|
|
515
|
+
throw typeError(text, kind);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function overflowsToInfinity(text) {
|
|
519
|
+
const trimmed = text.trim();
|
|
520
|
+
return /^[+-]?[0-9.]/.test(trimmed) && Number(trimmed) === (trimmed.startsWith("-") ? -Infinity : Infinity);
|
|
521
|
+
}
|
|
522
|
+
function parsePointText(text) {
|
|
523
|
+
const trimmed = text.trim();
|
|
524
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
|
525
|
+
throw typeError(text, "point");
|
|
526
|
+
}
|
|
527
|
+
const out = {};
|
|
528
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
529
|
+
if (inner.length === 0) {
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
for (const part of inner.split(",")) {
|
|
533
|
+
const colon = part.indexOf(":");
|
|
534
|
+
if (colon < 0) {
|
|
535
|
+
throw typeError(text, "point");
|
|
536
|
+
}
|
|
537
|
+
const key = part.slice(0, colon).trim().replace(/^["']|["']$/g, "");
|
|
538
|
+
const raw = part.slice(colon + 1).trim();
|
|
539
|
+
if (key.length === 0) {
|
|
540
|
+
throw typeError(text, "point");
|
|
541
|
+
}
|
|
542
|
+
if (DECIMAL_TEXT.test(raw)) {
|
|
543
|
+
out[key] = Number(raw);
|
|
544
|
+
} else if (raw.startsWith("'") && raw.endsWith("'") || raw.startsWith('"') && raw.endsWith('"')) {
|
|
545
|
+
out[key] = raw.slice(1, -1);
|
|
546
|
+
} else {
|
|
547
|
+
throw typeError(text, "point");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
function parseJsonText(text) {
|
|
553
|
+
try {
|
|
554
|
+
return JSON.parse(text);
|
|
555
|
+
} catch {
|
|
556
|
+
throw typeError(text, "json");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function typeError(text, kind) {
|
|
560
|
+
const shown = text.length > 40 ? `${text.slice(0, 40)}...` : text;
|
|
561
|
+
return new GraphFormatError("E_COLUMN_TYPE", `"${shown}" is not ${withArticle(kind)}`, { value: text, kind });
|
|
562
|
+
}
|
|
563
|
+
function splitListText(text, syntax) {
|
|
564
|
+
const trimmed = text.trim();
|
|
565
|
+
if (trimmed.length === 0) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
switch (syntax) {
|
|
569
|
+
case "gexf": {
|
|
570
|
+
if (isBracketed(trimmed)) {
|
|
571
|
+
return splitBracketed(trimmed);
|
|
572
|
+
}
|
|
573
|
+
const separator = firstSeparator(trimmed, ["|", ",", ";"]);
|
|
574
|
+
return separator === null ? [trimmed] : splitOn(trimmed, separator);
|
|
575
|
+
}
|
|
576
|
+
case "brackets":
|
|
577
|
+
return isBracketed(trimmed) ? splitBracketed(trimmed) : [trimmed];
|
|
578
|
+
case "pipe":
|
|
579
|
+
return splitOn(trimmed, "|");
|
|
580
|
+
case "comma":
|
|
581
|
+
return splitOn(trimmed, ",");
|
|
582
|
+
case "semicolon":
|
|
583
|
+
return splitOn(trimmed, ";");
|
|
584
|
+
default: {
|
|
585
|
+
const name = syntax;
|
|
586
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown list syntax ${name}`, {
|
|
587
|
+
option: "listSyntax",
|
|
588
|
+
found: name
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function joinListText(items, syntax) {
|
|
594
|
+
switch (syntax) {
|
|
595
|
+
case "gexf":
|
|
596
|
+
case "brackets":
|
|
597
|
+
return `[${items.map(quoteBracketItem).join(", ")}]`;
|
|
598
|
+
case "pipe":
|
|
599
|
+
return items.join("|");
|
|
600
|
+
case "comma":
|
|
601
|
+
return items.join(",");
|
|
602
|
+
case "semicolon":
|
|
603
|
+
return items.join(";");
|
|
604
|
+
default: {
|
|
605
|
+
const name = syntax;
|
|
606
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown list syntax ${name}`, {
|
|
607
|
+
option: "listSyntax",
|
|
608
|
+
found: name
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function isBracketed(text) {
|
|
614
|
+
return text.length >= 2 && text.startsWith("[") && text.endsWith("]");
|
|
615
|
+
}
|
|
616
|
+
function firstSeparator(text, candidates) {
|
|
617
|
+
for (const candidate of candidates) {
|
|
618
|
+
if (text.includes(candidate)) {
|
|
619
|
+
return candidate;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
function splitOn(text, separator) {
|
|
625
|
+
return text.split(separator).map((item) => item.trim());
|
|
626
|
+
}
|
|
627
|
+
function splitBracketed(text) {
|
|
628
|
+
const inner = text.slice(1, -1);
|
|
629
|
+
const items = [];
|
|
630
|
+
const n = inner.length;
|
|
631
|
+
if (inner.trim().length === 0) {
|
|
632
|
+
return items;
|
|
633
|
+
}
|
|
634
|
+
let i = 0;
|
|
635
|
+
for (; ; ) {
|
|
636
|
+
while (i < n && isSpace(inner.charCodeAt(i))) {
|
|
637
|
+
i++;
|
|
638
|
+
}
|
|
639
|
+
let item = "";
|
|
640
|
+
if (i < n && (inner[i] === '"' || inner[i] === "'")) {
|
|
641
|
+
const quote = inner[i];
|
|
642
|
+
i++;
|
|
643
|
+
while (i < n) {
|
|
644
|
+
if (inner[i] === quote) {
|
|
645
|
+
if (inner[i + 1] === quote) {
|
|
646
|
+
item += quote;
|
|
647
|
+
i += 2;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
i++;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
item += inner[i];
|
|
654
|
+
i++;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
let end = inner.indexOf(",", i);
|
|
658
|
+
if (end < 0) {
|
|
659
|
+
end = n;
|
|
660
|
+
}
|
|
661
|
+
item += inner.slice(i, end).trim();
|
|
662
|
+
items.push(item);
|
|
663
|
+
i = end;
|
|
664
|
+
if (i >= n) {
|
|
665
|
+
return items;
|
|
666
|
+
}
|
|
667
|
+
i++;
|
|
668
|
+
if (inner.slice(i).trim().length === 0) {
|
|
669
|
+
items.push("");
|
|
670
|
+
return items;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function isSpace(c) {
|
|
675
|
+
return c === 32 || c === 9 || c === 13 || c === 10;
|
|
676
|
+
}
|
|
677
|
+
function quoteBracketItem(item) {
|
|
678
|
+
if (item.length === 0 || /[,[\]"']/.test(item) || item !== item.trim()) {
|
|
679
|
+
return `"${item.replace(/"/g, '""')}"`;
|
|
680
|
+
}
|
|
681
|
+
return item;
|
|
682
|
+
}
|
|
683
|
+
const UNKNOWN_TYPE_CODE = UNKNOWN_ATTR_TYPE_CODE;
|
|
684
|
+
const RENAMED_CODE = COLUMN_RENAMED_CODE;
|
|
685
|
+
const DICT_SAMPLE_ROWS = 1024;
|
|
686
|
+
function declareAttribute(input) {
|
|
687
|
+
const issues = [];
|
|
688
|
+
const listSyntax = input.listSyntax ?? "gexf";
|
|
689
|
+
let spec;
|
|
690
|
+
if (input.type === null) {
|
|
691
|
+
spec = stringSpec(null);
|
|
692
|
+
} else {
|
|
693
|
+
const mapped = mapDeclaredType(input.format, input.type, input.long);
|
|
694
|
+
if (mapped === null) {
|
|
695
|
+
spec = stringSpec(input.type);
|
|
696
|
+
issues.push({
|
|
697
|
+
category: "unsupported",
|
|
698
|
+
code: UNKNOWN_TYPE_CODE,
|
|
699
|
+
message: `${input.format} type "${input.type}" is not supported; values kept as text`
|
|
700
|
+
});
|
|
701
|
+
} else {
|
|
702
|
+
spec = mapped;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const { name, renamed } = resolveName(input);
|
|
706
|
+
if (renamed) {
|
|
707
|
+
issues.push({
|
|
708
|
+
category: "coercion",
|
|
709
|
+
code: RENAMED_CODE,
|
|
710
|
+
message: `attribute "${input.title ?? input.id ?? ""}" renamed to "${name}": the name was taken`
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
const decl = {
|
|
714
|
+
name,
|
|
715
|
+
dtype: spec.dtype,
|
|
716
|
+
nullable: true,
|
|
717
|
+
origin: {
|
|
718
|
+
format: input.format,
|
|
719
|
+
id: input.id,
|
|
720
|
+
title: input.title !== null && input.title !== name ? input.title : null,
|
|
721
|
+
type: input.type,
|
|
722
|
+
namespace: input.namespace ?? null
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
if (spec.list) {
|
|
726
|
+
decl.itemDtype = spec.itemDtype ?? "string";
|
|
727
|
+
}
|
|
728
|
+
if (input.role !== void 0 && input.role !== null) {
|
|
729
|
+
decl.role = input.role;
|
|
730
|
+
}
|
|
731
|
+
if (input.dynamic === true) {
|
|
732
|
+
decl.dynamic = true;
|
|
733
|
+
}
|
|
734
|
+
const optionsText = input.optionsText ?? null;
|
|
735
|
+
if (optionsText !== null) {
|
|
736
|
+
try {
|
|
737
|
+
const options = splitListText(optionsText, listSyntax).map((item) => parseItem(item, spec));
|
|
738
|
+
decl.options = options;
|
|
739
|
+
if (spec.dtype === "string" && !spec.list && options.every((o) => typeof o === "string")) {
|
|
740
|
+
decl.dtype = "dict";
|
|
741
|
+
spec = { ...spec, dtype: "dict" };
|
|
742
|
+
}
|
|
743
|
+
} catch (err) {
|
|
744
|
+
issues.push({
|
|
745
|
+
category: "validation-error",
|
|
746
|
+
code: BAD_OPTIONS_CODE,
|
|
747
|
+
message: `options "${optionsText}" of attribute "${name}" do not parse as ${spec.declared}: ${messageOf$1(err)}`
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const defaultText = input.defaultText ?? null;
|
|
752
|
+
if (defaultText !== null) {
|
|
753
|
+
try {
|
|
754
|
+
decl.default = parseDeclaredValue(defaultText, spec, listSyntax);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
issues.push({
|
|
757
|
+
category: "validation-error",
|
|
758
|
+
code: BAD_DEFAULT_CODE,
|
|
759
|
+
message: `default "${defaultText}" of attribute "${name}" does not parse as ${spec.declared}: ${messageOf$1(err)}`
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const companion = spec.temporal !== null && !spec.list ? timeTextCompanion(name) : null;
|
|
764
|
+
return { decl, spec, listSyntax, companion, renamed, issues };
|
|
765
|
+
}
|
|
766
|
+
function parseDeclaredValue(text, spec, listSyntax) {
|
|
767
|
+
if (spec.list) {
|
|
768
|
+
return splitListText(text, listSyntax).map((item) => parseItem(item, spec));
|
|
769
|
+
}
|
|
770
|
+
return parseItem(text, spec);
|
|
771
|
+
}
|
|
772
|
+
function parseDeclaredTemporal(text, spec) {
|
|
773
|
+
if (spec.temporal === null || spec.list) {
|
|
774
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `"${spec.declared}" is not a scalar temporal type`, {
|
|
775
|
+
declared: spec.declared
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return parseTemporal(text.trim(), spec.temporal);
|
|
779
|
+
}
|
|
780
|
+
const INTEGER_TEXT = /^[+-]?[0-9]+$/;
|
|
781
|
+
function losesPrecision(spec, text) {
|
|
782
|
+
if (!spec.precision) {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
const trimmed = text.trim();
|
|
786
|
+
if (!INTEGER_TEXT.test(trimmed)) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
const n = Number(trimmed);
|
|
790
|
+
return !Number.isFinite(n) || BigInt(trimmed) !== BigInt(n);
|
|
791
|
+
}
|
|
792
|
+
function declareOn(sink, domain, decl) {
|
|
793
|
+
return domain === "node" ? sink.declareNodeColumn(decl) : sink.declareEdgeColumn(decl);
|
|
794
|
+
}
|
|
795
|
+
function declareResolved(sink, domain, decl, report, where) {
|
|
796
|
+
let current = decl;
|
|
797
|
+
let renamed = false;
|
|
798
|
+
let roleDropped = false;
|
|
799
|
+
for (; ; ) {
|
|
800
|
+
try {
|
|
801
|
+
return { handle: declareOn(sink, domain, current), decl: current, renamed, roleDropped };
|
|
802
|
+
} catch (err) {
|
|
803
|
+
if (!(err instanceof GraphFormatError)) {
|
|
804
|
+
throw err;
|
|
805
|
+
}
|
|
806
|
+
if (err.code === "E_COLUMN_EXISTS" && !renamed) {
|
|
807
|
+
const taken = takenIn(sink, domain);
|
|
808
|
+
const id = current.origin?.id ?? null;
|
|
809
|
+
const name = uniqueColumnName(current.name, id, taken);
|
|
810
|
+
report.warning(
|
|
811
|
+
"coercion",
|
|
812
|
+
COLUMN_RENAMED_CODE,
|
|
813
|
+
`${domain} column "${current.name}" renamed to "${name}": the sink already holds that name with another shape`,
|
|
814
|
+
{ ...where, element: where?.element ?? current.name }
|
|
815
|
+
);
|
|
816
|
+
current = { ...current, name };
|
|
817
|
+
renamed = true;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (err.code === "E_DUPLICATE_ROLE" && current.role !== void 0 && !roleDropped) {
|
|
821
|
+
const holder = typeof err.details.holder === "string" ? err.details.holder : "another column";
|
|
822
|
+
report.warning(
|
|
823
|
+
"coercion",
|
|
824
|
+
ROLE_TAKEN_CODE,
|
|
825
|
+
`${domain} column "${current.name}" loses the ${current.role} role: "${holder}" already holds it`,
|
|
826
|
+
{ ...where, element: where?.element ?? current.name }
|
|
827
|
+
);
|
|
828
|
+
const { role: _role, ...withoutRole } = current;
|
|
829
|
+
current = withoutRole;
|
|
830
|
+
roleDropped = true;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
throw err;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function declareCompanion(sink, domain, companion, report) {
|
|
838
|
+
try {
|
|
839
|
+
return declareOn(sink, domain, companion);
|
|
840
|
+
} catch (err) {
|
|
841
|
+
if (err instanceof GraphFormatError && err.code === "E_DUPLICATE_ROLE") {
|
|
842
|
+
report?.warnOnce(
|
|
843
|
+
"coercion",
|
|
844
|
+
ROLE_TAKEN_CODE,
|
|
845
|
+
`${domain} column "${companion.name}" is declared without the timeText role: another companion holds it (exporters use extra.for)`,
|
|
846
|
+
{ element: companion.name }
|
|
847
|
+
);
|
|
848
|
+
const { role: _role, ...withoutRole } = companion;
|
|
849
|
+
return declareOn(sink, domain, withoutRole);
|
|
850
|
+
}
|
|
851
|
+
throw err;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function takenIn(sink, domain) {
|
|
855
|
+
return (name) => (domain === "node" ? sink.nodeColumn(name) : sink.edgeColumn(name)) !== INVALID_INDEX;
|
|
856
|
+
}
|
|
857
|
+
function uniqueColumnName(name, id, taken) {
|
|
858
|
+
if (!taken(name)) {
|
|
859
|
+
return name;
|
|
860
|
+
}
|
|
861
|
+
const base = id === null ? name : `${name}#${id}`;
|
|
862
|
+
if (id !== null && !taken(base)) {
|
|
863
|
+
return base;
|
|
864
|
+
}
|
|
865
|
+
for (let n = 2; ; n++) {
|
|
866
|
+
const candidate = `${base}#${n}`;
|
|
867
|
+
if (!taken(candidate)) {
|
|
868
|
+
return candidate;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
class DictHeuristic {
|
|
873
|
+
/**
|
|
874
|
+
* Create a heuristic.
|
|
875
|
+
* @param sampleRows - rows observed before deciding; DICT_SAMPLE_ROWS by default
|
|
876
|
+
*/
|
|
877
|
+
constructor(sampleRows = DICT_SAMPLE_ROWS) {
|
|
878
|
+
this.distinct = /* @__PURE__ */ new Set();
|
|
879
|
+
this.rowsSeen = 0;
|
|
880
|
+
this.sampleRows = sampleRows;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Rows observed so far.
|
|
884
|
+
* @returns the count
|
|
885
|
+
*/
|
|
886
|
+
get rows() {
|
|
887
|
+
return this.rowsSeen;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Distinct values observed so far.
|
|
891
|
+
* @returns the count
|
|
892
|
+
*/
|
|
893
|
+
get distinctCount() {
|
|
894
|
+
return this.distinct.size;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Whether the sample is full.
|
|
898
|
+
* @returns true once sampleRows values were observed
|
|
899
|
+
*/
|
|
900
|
+
get decided() {
|
|
901
|
+
return this.rowsSeen >= this.sampleRows;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Observe one value.
|
|
905
|
+
* @param value - the value text
|
|
906
|
+
* @returns true when this observation completed the sample
|
|
907
|
+
*/
|
|
908
|
+
observe(value) {
|
|
909
|
+
this.rowsSeen++;
|
|
910
|
+
if (this.distinct.size < this.rowsSeen) {
|
|
911
|
+
this.distinct.add(value);
|
|
912
|
+
}
|
|
913
|
+
return this.rowsSeen === this.sampleRows;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* The dtype the sample suggests: dict when the distinct count is below rows / 2 (and at least
|
|
917
|
+
* one row was seen), string otherwise. Valid before the sample is full (end of input).
|
|
918
|
+
* @returns "dict" or "string"
|
|
919
|
+
*/
|
|
920
|
+
decide() {
|
|
921
|
+
return this.rowsSeen > 0 && this.distinct.size * 2 < this.rowsSeen ? "dict" : "string";
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function resolveName(input) {
|
|
925
|
+
const preferred = input.title !== null && input.title.length > 0 ? input.title : input.id;
|
|
926
|
+
if (preferred === null || preferred.length === 0) {
|
|
927
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "an attribute declaration needs a title or an id", {
|
|
928
|
+
field: "name",
|
|
929
|
+
format: input.format
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (input.taken === void 0) {
|
|
933
|
+
return { name: preferred, renamed: false };
|
|
934
|
+
}
|
|
935
|
+
const name = uniqueColumnName(preferred, input.id, input.taken);
|
|
936
|
+
return { name, renamed: name !== preferred };
|
|
937
|
+
}
|
|
938
|
+
function parseItem(text, spec) {
|
|
939
|
+
return parseScalarText(text, spec.kind, spec.temporal);
|
|
940
|
+
}
|
|
941
|
+
function messageOf$1(err) {
|
|
942
|
+
return err instanceof Error ? err.message : String(err);
|
|
943
|
+
}
|
|
944
|
+
const DIRECTED_COLUMN = "graphty.directed";
|
|
945
|
+
const PAIR_COLUMN = "graphty.pair";
|
|
946
|
+
const MUTUAL_COLUMN = "graphty.mutual";
|
|
947
|
+
const DIRECTED_DECL = { name: DIRECTED_COLUMN, dtype: "bool", role: "directed" };
|
|
948
|
+
const PAIR_DECL = { name: PAIR_COLUMN, dtype: "u32", role: "pair", refersTo: "edge" };
|
|
949
|
+
const MUTUAL_DECL = { name: MUTUAL_COLUMN, dtype: "bool", role: "mutual" };
|
|
950
|
+
class DirectionResolver {
|
|
951
|
+
/**
|
|
952
|
+
* Create a resolver.
|
|
953
|
+
* @param sink - the sink edges are pushed into
|
|
954
|
+
* @param report - the report issues are recorded in
|
|
955
|
+
* @param policy - the resolved onMixedDirection option
|
|
956
|
+
*/
|
|
957
|
+
constructor(sink, report, policy) {
|
|
958
|
+
this.directedHandle = INVALID_INDEX;
|
|
959
|
+
this.pairHandle = INVALID_INDEX;
|
|
960
|
+
this.mutualHandle = INVALID_INDEX;
|
|
961
|
+
this.firstEdge = -1;
|
|
962
|
+
this.nextEdge = -1;
|
|
963
|
+
this.forcedCount = 0;
|
|
964
|
+
this.headerSet = false;
|
|
965
|
+
this.mirrorOfLast = INVALID_INDEX;
|
|
966
|
+
this.sink = sink;
|
|
967
|
+
this.report = report;
|
|
968
|
+
this.policy = policy;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* The sink's current direction.
|
|
972
|
+
* @returns true when directed
|
|
973
|
+
*/
|
|
974
|
+
get directed() {
|
|
975
|
+
return this.sink.directed;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Whether the graph holds undirected edges stored as two edges.
|
|
979
|
+
* @returns true once the first such edge was added
|
|
980
|
+
*/
|
|
981
|
+
get expanded() {
|
|
982
|
+
return this.pairHandle !== INVALID_INDEX;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Set the graph's direction before the first edge: the direction the file declares, or the
|
|
986
|
+
* `defaultDirected` option for a file that declares none. Under `onMixedDirection` "directed" or
|
|
987
|
+
* "undirected" that direction is used instead, with a warning when it differs. When the sink
|
|
988
|
+
* cannot change direction (a caller's builder that already holds edges or was locked), the
|
|
989
|
+
* sink's direction stands and a warning is recorded.
|
|
990
|
+
* @param headerDirected - the direction the file declares (or the default)
|
|
991
|
+
* @param where - the line of the header, when known
|
|
992
|
+
*/
|
|
993
|
+
setHeader(headerDirected, where) {
|
|
994
|
+
this.headerSet = true;
|
|
995
|
+
let wanted = headerDirected;
|
|
996
|
+
if (this.policy === "directed" || this.policy === "undirected") {
|
|
997
|
+
wanted = this.policy === "directed";
|
|
998
|
+
if (wanted !== headerDirected) {
|
|
999
|
+
this.report.warnOnce(
|
|
1000
|
+
"coercion",
|
|
1001
|
+
DIRECTION_FORCED_CODE,
|
|
1002
|
+
`the file declares ${direction(headerDirected)} edges; read as ${direction(wanted)} per onMixedDirection`,
|
|
1003
|
+
{ line: where?.line ?? null },
|
|
1004
|
+
`${DIRECTION_FORCED_CODE}:${direction(headerDirected)}`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const { sink } = this;
|
|
1009
|
+
if (sink.directed === wanted) {
|
|
1010
|
+
this.adoptExisting();
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
if (wanted && sink.edgeCount > 0 && !sink.directedLocked) {
|
|
1015
|
+
const before = sink.edgeCount;
|
|
1016
|
+
sink.setDirected(true, { expand: true });
|
|
1017
|
+
this.adoptExpansionColumns();
|
|
1018
|
+
this.report.counts.expandedMixed += before;
|
|
1019
|
+
} else {
|
|
1020
|
+
sink.setDirected(wanted);
|
|
1021
|
+
}
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
if (!(err instanceof GraphFormatError) || err.code !== "E_DIRECTED") {
|
|
1024
|
+
throw err;
|
|
1025
|
+
}
|
|
1026
|
+
this.report.warning(
|
|
1027
|
+
"coercion",
|
|
1028
|
+
DIRECTION_REFUSED_CODE,
|
|
1029
|
+
`the file is ${direction(wanted)} but the sink is ${direction(sink.directed)} (${reasonOf(err)}); read as ${direction(sink.directed)}`,
|
|
1030
|
+
{ line: where?.line ?? null }
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
this.adoptExisting();
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Pick up reserved columns a caller's sink already holds (an earlier expansion), so every edge
|
|
1037
|
+
* pushed from now on gets its graphty.directed value.
|
|
1038
|
+
*/
|
|
1039
|
+
adoptExisting() {
|
|
1040
|
+
if (this.pairHandle === INVALID_INDEX && this.sink.edgeColumn(PAIR_COLUMN) !== INVALID_INDEX) {
|
|
1041
|
+
this.adoptExpansionColumns();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The second edge of the edge the last addEdge() call added, when that edge was stored as two;
|
|
1046
|
+
* otherwise INVALID_INDEX (4294967295). An edge attribute or weight you set after addEdge()
|
|
1047
|
+
* belongs on both: set it on the index addEdge() returned and, when this is not INVALID_INDEX,
|
|
1048
|
+
* on this index too.
|
|
1049
|
+
* @returns the second edge's index, or INVALID_INDEX
|
|
1050
|
+
*/
|
|
1051
|
+
get lastMirror() {
|
|
1052
|
+
return this.mirrorOfLast;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Add one edge. An edge with the graph's direction is added as it is; one with the other
|
|
1056
|
+
* direction is stored as two edges, given the graph's direction, or refused, as
|
|
1057
|
+
* `onMixedDirection` says. When it is stored as two, the return value is the first and
|
|
1058
|
+
* `lastMirror` the second.
|
|
1059
|
+
* @param source - the source id
|
|
1060
|
+
* @param target - the target id
|
|
1061
|
+
* @param kind - the edge's direction in the file
|
|
1062
|
+
* @param weight - the weight, or undefined when the file gives none
|
|
1063
|
+
* @param where - the line and element, for issues
|
|
1064
|
+
* @returns the index of the edge added (the first one, when it was stored as two)
|
|
1065
|
+
*/
|
|
1066
|
+
addEdge(source, target, kind, weight, where) {
|
|
1067
|
+
if (!this.headerSet) {
|
|
1068
|
+
throw new GraphFormatError("E_DIRECTED", "DirectionResolver.setHeader() must run before the first edge", {
|
|
1069
|
+
reason: "header not set"
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
const { sink } = this;
|
|
1073
|
+
this.mirrorOfLast = INVALID_INDEX;
|
|
1074
|
+
const edgeDirected = kind !== "undirected";
|
|
1075
|
+
if (edgeDirected === sink.directed && kind !== "mutual") {
|
|
1076
|
+
const e = sink.addEdge(source, target, weight);
|
|
1077
|
+
this.track(e);
|
|
1078
|
+
if (this.directedHandle !== INVALID_INDEX) {
|
|
1079
|
+
sink.setEdgeValue(this.directedHandle, e, edgeDirected);
|
|
1080
|
+
}
|
|
1081
|
+
return e;
|
|
1082
|
+
}
|
|
1083
|
+
switch (this.policy) {
|
|
1084
|
+
case "directed":
|
|
1085
|
+
case "undirected": {
|
|
1086
|
+
this.forcedCount++;
|
|
1087
|
+
this.report.warnOnce(
|
|
1088
|
+
"coercion",
|
|
1089
|
+
DIRECTION_FORCED_CODE,
|
|
1090
|
+
`${kind} edges read as ${direction(sink.directed)} per onMixedDirection`,
|
|
1091
|
+
where,
|
|
1092
|
+
`${DIRECTION_FORCED_CODE}:${kind}`
|
|
1093
|
+
);
|
|
1094
|
+
const e = sink.addEdge(source, target, weight);
|
|
1095
|
+
this.track(e);
|
|
1096
|
+
if (this.directedHandle !== INVALID_INDEX) {
|
|
1097
|
+
sink.setEdgeValue(this.directedHandle, e, sink.directed);
|
|
1098
|
+
}
|
|
1099
|
+
return e;
|
|
1100
|
+
}
|
|
1101
|
+
case "error":
|
|
1102
|
+
this.report.error(
|
|
1103
|
+
"validation-error",
|
|
1104
|
+
MIXED_DIRECTION_CODE,
|
|
1105
|
+
`${kind} edge in a ${direction(sink.directed)} graph (onMixedDirection: "error")`,
|
|
1106
|
+
where
|
|
1107
|
+
);
|
|
1108
|
+
throw this.report.abort("mixed direction refused", { code: MIXED_DIRECTION_CODE });
|
|
1109
|
+
case "expand":
|
|
1110
|
+
return this.expand(source, target, kind, weight, where);
|
|
1111
|
+
default: {
|
|
1112
|
+
const name = this.policy;
|
|
1113
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown onMixedDirection ${name}`, {
|
|
1114
|
+
option: "onMixedDirection",
|
|
1115
|
+
found: name
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* How many edges were given a direction other than their own, under `onMixedDirection`
|
|
1122
|
+
* "directed" or "undirected".
|
|
1123
|
+
* @returns the count
|
|
1124
|
+
*/
|
|
1125
|
+
get forced() {
|
|
1126
|
+
return this.forcedCount;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Expand one edge that differs from the sink's direction.
|
|
1130
|
+
* @param source - the source id
|
|
1131
|
+
* @param target - the target id
|
|
1132
|
+
* @param kind - the edge's direction in the file
|
|
1133
|
+
* @param weight - the weight, or undefined
|
|
1134
|
+
* @param where - the line and element, for issues
|
|
1135
|
+
* @returns the primary edge index
|
|
1136
|
+
*/
|
|
1137
|
+
expand(source, target, kind, weight, where) {
|
|
1138
|
+
const { sink } = this;
|
|
1139
|
+
if (!sink.directed) {
|
|
1140
|
+
try {
|
|
1141
|
+
const before = sink.edgeCount;
|
|
1142
|
+
sink.setDirected(true, { expand: true });
|
|
1143
|
+
this.adoptExpansionColumns();
|
|
1144
|
+
this.report.counts.expandedMixed += before;
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
if (!(err instanceof GraphFormatError) || err.code !== "E_DIRECTED") {
|
|
1147
|
+
throw err;
|
|
1148
|
+
}
|
|
1149
|
+
this.report.error(
|
|
1150
|
+
"coercion",
|
|
1151
|
+
DIRECTION_REFUSED_CODE,
|
|
1152
|
+
`${direction(true)} edge in a locked ${direction(false)} sink cannot be expanded`,
|
|
1153
|
+
where
|
|
1154
|
+
);
|
|
1155
|
+
throw this.report.abort("the sink refused the expansion to directed", {
|
|
1156
|
+
code: DIRECTION_REFUSED_CODE,
|
|
1157
|
+
reason: err.details.reason
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (kind === "directed") {
|
|
1161
|
+
const e = sink.addEdge(source, target, weight);
|
|
1162
|
+
this.track(e);
|
|
1163
|
+
sink.setEdgeValue(this.directedHandle, e, true);
|
|
1164
|
+
return e;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
this.ensureExpansionColumns();
|
|
1168
|
+
const first = sink.addEdge(source, target, weight);
|
|
1169
|
+
this.track(first);
|
|
1170
|
+
const mutual = kind === "mutual";
|
|
1171
|
+
sink.setEdgeValue(this.directedHandle, first, mutual);
|
|
1172
|
+
this.report.counts.expandedMixed++;
|
|
1173
|
+
if (mutual) {
|
|
1174
|
+
this.ensureMutualColumn();
|
|
1175
|
+
sink.setEdgeValue(this.mutualHandle, first, true);
|
|
1176
|
+
}
|
|
1177
|
+
const u = sink.indexOf(source);
|
|
1178
|
+
const v = sink.indexOf(target);
|
|
1179
|
+
if (u === v) {
|
|
1180
|
+
return first;
|
|
1181
|
+
}
|
|
1182
|
+
const mirror = sink.addEdge(target, source, weight);
|
|
1183
|
+
this.track(mirror);
|
|
1184
|
+
this.mirrorOfLast = mirror;
|
|
1185
|
+
sink.setEdgeValue(this.directedHandle, mirror, mutual);
|
|
1186
|
+
sink.setEdgeValue(this.pairHandle, first, mirror);
|
|
1187
|
+
sink.setEdgeValue(this.pairHandle, mirror, first);
|
|
1188
|
+
return first;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Note an edge index this resolver pushed, for the backfill of graphty.directed.
|
|
1192
|
+
* @param e - the edge index
|
|
1193
|
+
*/
|
|
1194
|
+
track(e) {
|
|
1195
|
+
if (this.firstEdge < 0) {
|
|
1196
|
+
this.firstEdge = e;
|
|
1197
|
+
}
|
|
1198
|
+
this.nextEdge = e + 1;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Pick up the reserved columns the builder wrote during its own expansion.
|
|
1202
|
+
*/
|
|
1203
|
+
adoptExpansionColumns() {
|
|
1204
|
+
this.directedHandle = this.sink.declareEdgeColumn(DIRECTED_DECL);
|
|
1205
|
+
this.pairHandle = this.sink.declareEdgeColumn(PAIR_DECL);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Declare the reserved columns on first use in a directed sink and backfill graphty.directed
|
|
1209
|
+
* = true for every edge this resolver pushed so far (they were directed in the source).
|
|
1210
|
+
*/
|
|
1211
|
+
ensureExpansionColumns() {
|
|
1212
|
+
if (this.pairHandle !== INVALID_INDEX) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
this.directedHandle = this.sink.declareEdgeColumn(DIRECTED_DECL);
|
|
1216
|
+
this.pairHandle = this.sink.declareEdgeColumn(PAIR_DECL);
|
|
1217
|
+
if (this.firstEdge >= 0) {
|
|
1218
|
+
for (let e = this.firstEdge; e < this.nextEdge; e++) {
|
|
1219
|
+
this.sink.setEdgeValue(this.directedHandle, e, true);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
/** Declare graphty.mutual on first use. */
|
|
1224
|
+
ensureMutualColumn() {
|
|
1225
|
+
if (this.mutualHandle === INVALID_INDEX) {
|
|
1226
|
+
this.mutualHandle = this.sink.declareEdgeColumn(MUTUAL_DECL);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function reasonOf(err) {
|
|
1231
|
+
const { reason } = err.details;
|
|
1232
|
+
return typeof reason === "string" ? reason : "refused";
|
|
1233
|
+
}
|
|
1234
|
+
function direction(directed) {
|
|
1235
|
+
return directed ? "directed" : "undirected";
|
|
1236
|
+
}
|
|
1237
|
+
function pairFolding(snapshot, options = {}) {
|
|
1238
|
+
const foldMutual = options.foldMutual ?? false;
|
|
1239
|
+
const directedColumn = roleColumn(snapshot.edges.byRole("directed"), "bool");
|
|
1240
|
+
const pairColumn = roleColumn(snapshot.edges.byRole("pair"), "u32");
|
|
1241
|
+
const mutualColumn = roleColumn(snapshot.edges.byRole("mutual"), "bool");
|
|
1242
|
+
const { edgeCount } = snapshot;
|
|
1243
|
+
const mateOf = (e) => {
|
|
1244
|
+
if (pairColumn === null || !pairColumn.isSet(e)) {
|
|
1245
|
+
return INVALID_INDEX;
|
|
1246
|
+
}
|
|
1247
|
+
const mate = pairColumn.value(e);
|
|
1248
|
+
return typeof mate === "number" && mate !== INVALID_INDEX ? mate : INVALID_INDEX;
|
|
1249
|
+
};
|
|
1250
|
+
const sourceDirected = (e) => directedColumn === null || !directedColumn.isSet(e) || directedColumn.value(e) === true;
|
|
1251
|
+
const isMutual = (e) => {
|
|
1252
|
+
if (mutualColumn === null) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
if (mutualColumn.isSet(e) && mutualColumn.value(e) === true) {
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
const mate = mateOf(e);
|
|
1259
|
+
return mate !== INVALID_INDEX && mutualColumn.isSet(mate) && mutualColumn.value(mate) === true;
|
|
1260
|
+
};
|
|
1261
|
+
const folded = (e) => {
|
|
1262
|
+
const mate = mateOf(e);
|
|
1263
|
+
if (mate === INVALID_INDEX || mate >= e) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
return !sourceDirected(e) || foldMutual && isMutual(e);
|
|
1267
|
+
};
|
|
1268
|
+
let mutualCount = 0;
|
|
1269
|
+
if (mutualColumn !== null) {
|
|
1270
|
+
for (let e = 0; e < edgeCount; e++) {
|
|
1271
|
+
if (mutualColumn.isSet(e) && mutualColumn.value(e) === true && mateOf(e) > e) {
|
|
1272
|
+
mutualCount++;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return { expanded: pairColumn !== null, mutualCount, folded, mateOf, sourceDirected, isMutual };
|
|
1277
|
+
}
|
|
1278
|
+
function roleColumn(column, dtype) {
|
|
1279
|
+
return column !== null && column.dtype === dtype ? column : null;
|
|
1280
|
+
}
|
|
1281
|
+
const CANONICAL_INTEGER = /^-?(0|[1-9][0-9]*)$/;
|
|
1282
|
+
function canonicalId(text) {
|
|
1283
|
+
if (text !== "-0" && CANONICAL_INTEGER.test(text)) {
|
|
1284
|
+
const n = Number(text);
|
|
1285
|
+
if (Number.isSafeInteger(n)) {
|
|
1286
|
+
return n;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return text;
|
|
1290
|
+
}
|
|
1291
|
+
function isCanonicalIntegerText(text) {
|
|
1292
|
+
return typeof canonicalId(text) === "number";
|
|
1293
|
+
}
|
|
1294
|
+
function coerceIdText(text, mode) {
|
|
1295
|
+
switch (mode) {
|
|
1296
|
+
case "keep":
|
|
1297
|
+
case "string":
|
|
1298
|
+
return text;
|
|
1299
|
+
case "canonical":
|
|
1300
|
+
return canonicalId(text);
|
|
1301
|
+
case "number": {
|
|
1302
|
+
if (text.trim().length === 0) {
|
|
1303
|
+
throw invalidId(text, "empty text is not a number");
|
|
1304
|
+
}
|
|
1305
|
+
const n = Number(text);
|
|
1306
|
+
if (!Number.isFinite(n)) {
|
|
1307
|
+
throw invalidId(text, "not a finite number");
|
|
1308
|
+
}
|
|
1309
|
+
return n;
|
|
1310
|
+
}
|
|
1311
|
+
default: {
|
|
1312
|
+
const name = mode;
|
|
1313
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id coercion ${name}`, { option: "ids", found: name });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function coerceId(value, mode) {
|
|
1318
|
+
if (typeof value === "string") {
|
|
1319
|
+
return coerceIdText(value, mode);
|
|
1320
|
+
}
|
|
1321
|
+
if (typeof value === "number") {
|
|
1322
|
+
if (!Number.isFinite(value)) {
|
|
1323
|
+
throw invalidId(value, "not a finite number");
|
|
1324
|
+
}
|
|
1325
|
+
return mode === "string" ? String(value) : value;
|
|
1326
|
+
}
|
|
1327
|
+
if (mode === "string" && (typeof value === "boolean" || typeof value === "bigint" || value === null)) {
|
|
1328
|
+
return String(value);
|
|
1329
|
+
}
|
|
1330
|
+
throw invalidId(value, `a ${value === null ? "null" : typeof value} is not an id under ids: "${mode}"`);
|
|
1331
|
+
}
|
|
1332
|
+
function invalidId(value, reason) {
|
|
1333
|
+
const shown = typeof value === "string" || typeof value === "number" ? value : typeof value;
|
|
1334
|
+
return new GraphFormatError("E_INVALID_ID", `invalid node id ${JSON.stringify(shown)}: ${reason}`, {
|
|
1335
|
+
reason,
|
|
1336
|
+
value: typeof value === "bigint" ? value.toString() : value
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
class IdCoercer {
|
|
1340
|
+
/**
|
|
1341
|
+
* Create a coercer.
|
|
1342
|
+
* @param mode - the rule
|
|
1343
|
+
*/
|
|
1344
|
+
constructor(mode) {
|
|
1345
|
+
this.mergeCount = 0;
|
|
1346
|
+
this.lastMerge = null;
|
|
1347
|
+
this.mode = mode;
|
|
1348
|
+
this.firstText = mode === "number" ? /* @__PURE__ */ new Map() : null;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Coerce a text cell, recording a merge under "number" when a different text already produced
|
|
1352
|
+
* the same number.
|
|
1353
|
+
* @param text - the cell text
|
|
1354
|
+
* @returns the id
|
|
1355
|
+
*/
|
|
1356
|
+
text(text) {
|
|
1357
|
+
const id = coerceIdText(text, this.mode);
|
|
1358
|
+
this.lastMerge = null;
|
|
1359
|
+
if (this.firstText !== null && typeof id === "number") {
|
|
1360
|
+
const previous = this.firstText.get(id);
|
|
1361
|
+
if (previous === void 0) {
|
|
1362
|
+
this.firstText.set(id, text);
|
|
1363
|
+
} else if (previous !== text) {
|
|
1364
|
+
this.mergeCount++;
|
|
1365
|
+
this.lastMerge = { id, text, previousText: previous };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return id;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Coerce a typed value.
|
|
1372
|
+
* @param value - the value
|
|
1373
|
+
* @returns the id
|
|
1374
|
+
*/
|
|
1375
|
+
value(value) {
|
|
1376
|
+
if (typeof value === "string") {
|
|
1377
|
+
return this.text(value);
|
|
1378
|
+
}
|
|
1379
|
+
this.lastMerge = null;
|
|
1380
|
+
return coerceId(value, this.mode);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const HEAD_BYTES = 1024;
|
|
1384
|
+
const BOM_BYTES = 4;
|
|
1385
|
+
function canonicalEncoding(label) {
|
|
1386
|
+
try {
|
|
1387
|
+
return new TextDecoder(label.trim()).encoding;
|
|
1388
|
+
} catch {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function bomEncoding(head) {
|
|
1393
|
+
if (startsWithBytes(head, [255, 254, 0, 0])) {
|
|
1394
|
+
return "utf-32le";
|
|
1395
|
+
}
|
|
1396
|
+
if (startsWithBytes(head, [0, 0, 254, 255])) {
|
|
1397
|
+
return "utf-32be";
|
|
1398
|
+
}
|
|
1399
|
+
if (startsWithBytes(head, [239, 187, 191])) {
|
|
1400
|
+
return "utf-8";
|
|
1401
|
+
}
|
|
1402
|
+
if (startsWithBytes(head, [255, 254])) {
|
|
1403
|
+
return "utf-16le";
|
|
1404
|
+
}
|
|
1405
|
+
if (startsWithBytes(head, [254, 255])) {
|
|
1406
|
+
return "utf-16be";
|
|
1407
|
+
}
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
function startsWithBytes(bytes, signature) {
|
|
1411
|
+
return bytes.byteLength >= signature.length && signature.every((b, i) => bytes[i] === b);
|
|
1412
|
+
}
|
|
1413
|
+
function bomlessUtf16(head) {
|
|
1414
|
+
const pairs = Math.min(head.byteLength >> 1, 512);
|
|
1415
|
+
if (pairs < 2) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
let le = 0;
|
|
1419
|
+
let be = 0;
|
|
1420
|
+
for (let i = 0; i < pairs; i++) {
|
|
1421
|
+
const even = head[2 * i];
|
|
1422
|
+
const odd = head[2 * i + 1];
|
|
1423
|
+
if (even !== 0 && odd === 0) {
|
|
1424
|
+
le++;
|
|
1425
|
+
} else if (even === 0 && odd !== 0) {
|
|
1426
|
+
be++;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (le >= pairs * 0.9) {
|
|
1430
|
+
return "utf-16le";
|
|
1431
|
+
}
|
|
1432
|
+
return be >= pairs * 0.9 ? "utf-16be" : null;
|
|
1433
|
+
}
|
|
1434
|
+
function xmlDeclarationUtf16(head) {
|
|
1435
|
+
if (startsWithBytes(head, [60, 0, 63, 0])) {
|
|
1436
|
+
return "utf-16le";
|
|
1437
|
+
}
|
|
1438
|
+
return startsWithBytes(head, [0, 60, 0, 63]) ? "utf-16be" : null;
|
|
1439
|
+
}
|
|
1440
|
+
function foreignKind(head) {
|
|
1441
|
+
if (typeof head !== "string") {
|
|
1442
|
+
if (startsWithBytes(head, [31, 139])) {
|
|
1443
|
+
return "gzip-compressed data (decompress it first)";
|
|
1444
|
+
}
|
|
1445
|
+
if (startsWithBytes(head, [80, 75, 3, 4]) || startsWithBytes(head, [80, 75, 5, 6])) {
|
|
1446
|
+
return "a zip archive (a Cytoscape session (.cys) is read by the cys importer; extract any other graph file from it first)";
|
|
1447
|
+
}
|
|
1448
|
+
if (startsWithBytes(head, [137, 80, 78, 71])) {
|
|
1449
|
+
return "a PNG image";
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const text = typeof head === "string" ? head.slice(0, 64) : String.fromCodePoint(...head.subarray(0, 64));
|
|
1453
|
+
const utf8Bom = String.fromCodePoint(239, 187, 191);
|
|
1454
|
+
let body = text.startsWith(utf8Bom) ? text.slice(3) : text;
|
|
1455
|
+
body = body.startsWith(BOM) ? body.slice(1) : body;
|
|
1456
|
+
if (body.startsWith("%PDF-")) {
|
|
1457
|
+
return "a PDF document";
|
|
1458
|
+
}
|
|
1459
|
+
if (/^\s*(<!doctype\s+html[\s>]|<html[\s>])/i.test(body)) {
|
|
1460
|
+
return "an HTML document (likely an error page saved in place of the file)";
|
|
1461
|
+
}
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
function refuseForeign(head, report, options) {
|
|
1465
|
+
const foreign = foreignKind(head);
|
|
1466
|
+
if (foreign !== null && !(options.xml === true && foreign.startsWith("an HTML"))) {
|
|
1467
|
+
report.fail(FOREIGN_FORMAT_CODE, `the input is ${foreign}, not ${report.format} text`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function sameFamily(a, b) {
|
|
1471
|
+
return a === b || a.startsWith("utf-16") && b.startsWith("utf-16");
|
|
1472
|
+
}
|
|
1473
|
+
class ByteDecoder {
|
|
1474
|
+
/**
|
|
1475
|
+
* Create the decoder of one import.
|
|
1476
|
+
* @param report - where warnings and the fatal decode error go
|
|
1477
|
+
* @param options - the encoding option and the format's declaration reader
|
|
1478
|
+
*/
|
|
1479
|
+
constructor(report, options) {
|
|
1480
|
+
this.report = report;
|
|
1481
|
+
this.options = options;
|
|
1482
|
+
this.decoder = null;
|
|
1483
|
+
this.encoding = "utf-8";
|
|
1484
|
+
this.mayFallBack = false;
|
|
1485
|
+
this.asciiSoFar = true;
|
|
1486
|
+
this.carry = new Uint8Array(0);
|
|
1487
|
+
this.offset = 0;
|
|
1488
|
+
this.fellBack = false;
|
|
1489
|
+
this.control = null;
|
|
1490
|
+
this.subAt = -1;
|
|
1491
|
+
this.textFollows = false;
|
|
1492
|
+
this.declaredUtf8 = false;
|
|
1493
|
+
this.mixedPending = false;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Whether start() ran.
|
|
1497
|
+
* @returns true once the encoding is chosen
|
|
1498
|
+
*/
|
|
1499
|
+
get started() {
|
|
1500
|
+
return this.decoder !== null;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Choose the encoding from the first bytes of the input: a BOM, else the option, else the
|
|
1504
|
+
* declaration, else UTF-8 (see the module comment), warning where two of them disagree.
|
|
1505
|
+
* @param head - the first bytes (up to the declaration size; fewer when the input is shorter)
|
|
1506
|
+
*/
|
|
1507
|
+
start(head) {
|
|
1508
|
+
refuseForeign(head, this.report, this.options);
|
|
1509
|
+
const explicit = this.options.encoding ?? null;
|
|
1510
|
+
const bom = bomEncoding(head);
|
|
1511
|
+
if (bom === "utf-32le" || bom === "utf-32be") {
|
|
1512
|
+
this.report.fail(
|
|
1513
|
+
INVALID_ENCODING_CODE,
|
|
1514
|
+
`the input starts with a ${bom.toUpperCase()} byte order mark; UTF-32 is not supported (convert the file to UTF-8)`,
|
|
1515
|
+
void 0,
|
|
1516
|
+
{ byteOffset: 0 }
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
if (bom !== null) {
|
|
1520
|
+
const option = explicit === null ? null : canonicalEncoding(explicit);
|
|
1521
|
+
if (option !== null && option !== bom) {
|
|
1522
|
+
this.conflict(`the encoding option says ${option} but the byte order mark says ${bom}; read as ${bom}`);
|
|
1523
|
+
}
|
|
1524
|
+
this.checkDeclaration(head, bom, "its byte order mark says");
|
|
1525
|
+
this.use(bom, false);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (explicit !== null) {
|
|
1529
|
+
this.use(explicit, false);
|
|
1530
|
+
this.checkDeclaration(head, this.encoding, "the encoding option says");
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const xmlUtf16 = xmlDeclarationUtf16(head);
|
|
1534
|
+
if (xmlUtf16 !== null) {
|
|
1535
|
+
this.checkDeclaration(head, xmlUtf16, "its XML declaration's byte pattern says");
|
|
1536
|
+
this.use(xmlUtf16, false);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const utf16 = bomlessUtf16(head);
|
|
1540
|
+
if (utf16 !== null && this.options.bomlessUtf16 === true) {
|
|
1541
|
+
this.use(utf16, false);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (utf16 !== null) {
|
|
1545
|
+
this.report.fail(
|
|
1546
|
+
INVALID_ENCODING_CODE,
|
|
1547
|
+
`the input looks like ${utf16} text without a byte order mark (every other byte is 0); pass the encoding option ${JSON.stringify(utf16)} to read it`,
|
|
1548
|
+
void 0,
|
|
1549
|
+
{ byteOffset: 0 }
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
const declared = this.declared(head, "windows-1252");
|
|
1553
|
+
if (declared !== null) {
|
|
1554
|
+
const canonical = canonicalEncoding(declared);
|
|
1555
|
+
if (canonical === null) {
|
|
1556
|
+
this.report.warning(
|
|
1557
|
+
"unsupported",
|
|
1558
|
+
UNKNOWN_ENCODING_CODE,
|
|
1559
|
+
`the input declares the encoding ${JSON.stringify(declared)}, which this platform cannot decode; reading it as UTF-8`,
|
|
1560
|
+
{ element: declared }
|
|
1561
|
+
);
|
|
1562
|
+
} else if (canonical.startsWith("utf-16")) {
|
|
1563
|
+
this.conflict(
|
|
1564
|
+
`the input declares ${JSON.stringify(declared)} but has no byte order mark and its bytes are not UTF-16; read as UTF-8`
|
|
1565
|
+
);
|
|
1566
|
+
} else if (canonical === "utf-8") {
|
|
1567
|
+
this.declaredUtf8 = true;
|
|
1568
|
+
} else {
|
|
1569
|
+
this.use(canonical, false);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
this.use("utf-8", true);
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* The encoding the head declares, read through the format's declaration reader.
|
|
1577
|
+
* @param head - the first bytes
|
|
1578
|
+
* @param encoding - the encoding to read the head in for the reader
|
|
1579
|
+
* @returns the declared label, or null (also for a format without declarations)
|
|
1580
|
+
*/
|
|
1581
|
+
declared(head, encoding) {
|
|
1582
|
+
const reader = this.options.declaredEncoding;
|
|
1583
|
+
return reader === void 0 ? null : reader(new TextDecoder(encoding).decode(head));
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Warn when the file declares an encoding other than the one a BOM or the option chose.
|
|
1587
|
+
* @param head - the first bytes
|
|
1588
|
+
* @param chosen - the encoding the input is read in
|
|
1589
|
+
* @param why - what chose it, for the message
|
|
1590
|
+
*/
|
|
1591
|
+
checkDeclaration(head, chosen, why) {
|
|
1592
|
+
const declared = this.declared(head, chosen);
|
|
1593
|
+
const canonical = declared === null ? null : canonicalEncoding(declared);
|
|
1594
|
+
if (canonical !== null && !sameFamily(canonical, chosen)) {
|
|
1595
|
+
this.conflict(
|
|
1596
|
+
`the input declares the encoding ${JSON.stringify(declared)} but ${why} ${chosen}; read as ${chosen}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Record a W_ENCODING_CONFLICT warning.
|
|
1602
|
+
* @param message - which sources disagree and which applies
|
|
1603
|
+
*/
|
|
1604
|
+
conflict(message) {
|
|
1605
|
+
this.report.warning("coercion", ENCODING_CONFLICT_CODE, message);
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Decode the next bytes.
|
|
1609
|
+
* @param input - the bytes
|
|
1610
|
+
* @param stream - whether more bytes follow
|
|
1611
|
+
* @returns the text
|
|
1612
|
+
*/
|
|
1613
|
+
decode(input, stream) {
|
|
1614
|
+
const bytes = typeof SharedArrayBuffer === "function" && input.buffer instanceof SharedArrayBuffer ? input.slice() : input;
|
|
1615
|
+
const decoder = this.decoder;
|
|
1616
|
+
if (this.fellBack || this.mayFallBack && this.asciiSoFar) {
|
|
1617
|
+
this.watch(bytes, !stream);
|
|
1618
|
+
if (this.fellBack && this.control !== null) {
|
|
1619
|
+
this.binary(this.control);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const text = this.decodeOrRecover(decoder, bytes, stream);
|
|
1623
|
+
if (this.mixedPending) {
|
|
1624
|
+
this.reportUtf8After(bytes, this.offset);
|
|
1625
|
+
}
|
|
1626
|
+
if (this.asciiSoFar && this.mayFallBack) {
|
|
1627
|
+
this.trackAscii(bytes, text);
|
|
1628
|
+
}
|
|
1629
|
+
if (this.mayFallBack && this.options.nulIsBinary === true && text.includes("\0")) {
|
|
1630
|
+
this.binary({ at: this.offset + bytes.indexOf(0), byte: 0 });
|
|
1631
|
+
}
|
|
1632
|
+
this.offset += bytes.byteLength;
|
|
1633
|
+
return text;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Decode bytes, recovering from bytes that are not valid in the encoding.
|
|
1637
|
+
* @param decoder - the decoder
|
|
1638
|
+
* @param bytes - the bytes
|
|
1639
|
+
* @param stream - whether more bytes follow
|
|
1640
|
+
* @returns the text
|
|
1641
|
+
*/
|
|
1642
|
+
decodeOrRecover(decoder, bytes, stream) {
|
|
1643
|
+
try {
|
|
1644
|
+
return decoder.decode(bytes, { stream });
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
if (!(err instanceof TypeError)) {
|
|
1647
|
+
throw err;
|
|
1648
|
+
}
|
|
1649
|
+
return this.recover(bytes, stream);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Track whether the input is ASCII so far. While it is, every character is one byte, so the
|
|
1654
|
+
* bytes not yet emitted are held back for a fallback.
|
|
1655
|
+
* @param bytes - the bytes just decoded
|
|
1656
|
+
* @param text - their text
|
|
1657
|
+
*/
|
|
1658
|
+
trackAscii(bytes, text) {
|
|
1659
|
+
if (/[\u0080-\uffff]/.test(text)) {
|
|
1660
|
+
this.asciiSoFar = false;
|
|
1661
|
+
this.carry = new Uint8Array(0);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const all = this.carry.byteLength === 0 ? bytes : concatBytes([this.carry, bytes]);
|
|
1665
|
+
this.carry = all.slice(text.length);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Flush the bytes held back because a text chunk follows (the input switches from bytes to
|
|
1669
|
+
* strings): a multi-byte sequence cut there is invalid UTF-8, never windows-1252 text.
|
|
1670
|
+
* @param flush - the flush to run (the decoder's own, or the head's)
|
|
1671
|
+
* @returns the text of the held-back bytes
|
|
1672
|
+
*/
|
|
1673
|
+
flushBeforeText(flush) {
|
|
1674
|
+
this.textFollows = true;
|
|
1675
|
+
try {
|
|
1676
|
+
return flush();
|
|
1677
|
+
} finally {
|
|
1678
|
+
this.textFollows = false;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* A decode failed: switch to windows-1252 when allowed, else fail the import with the position
|
|
1683
|
+
* of the first byte that is not valid.
|
|
1684
|
+
* @param bytes - the bytes that failed
|
|
1685
|
+
* @param stream - whether more bytes follow
|
|
1686
|
+
* @returns the bytes decoded as windows-1252
|
|
1687
|
+
*/
|
|
1688
|
+
recover(bytes, stream) {
|
|
1689
|
+
const all = this.carry.byteLength === 0 ? bytes : concatBytes([this.carry, bytes]);
|
|
1690
|
+
const base = this.offset - this.carry.byteLength;
|
|
1691
|
+
const bad = this.encoding === "utf-8" ? invalidUtf8(all) : null;
|
|
1692
|
+
const at = base + (bad === null ? 0 : bad.index);
|
|
1693
|
+
if (this.mayFallBack && this.asciiSoFar) {
|
|
1694
|
+
if (this.control !== null) {
|
|
1695
|
+
return this.binary(this.control);
|
|
1696
|
+
}
|
|
1697
|
+
if (this.textFollows && bad?.cut === true) {
|
|
1698
|
+
return this.report.fail(
|
|
1699
|
+
INVALID_UTF8_CODE,
|
|
1700
|
+
`invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`,
|
|
1701
|
+
void 0,
|
|
1702
|
+
{ byteOffset: at }
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (!stream && bad?.truncated === true) {
|
|
1706
|
+
return this.report.fail(
|
|
1707
|
+
INVALID_UTF8_CODE,
|
|
1708
|
+
`the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`,
|
|
1709
|
+
void 0,
|
|
1710
|
+
{ byteOffset: at }
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
if (!startsWithUtf8(all)) {
|
|
1714
|
+
const cutNote = !stream && bad?.cut === true ? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated" : "";
|
|
1715
|
+
this.report.warning(
|
|
1716
|
+
"coercion",
|
|
1717
|
+
ENCODING_FALLBACK_CODE,
|
|
1718
|
+
this.declaredUtf8 ? `the input declares UTF-8 but is not valid UTF-8 (at byte ${at}); everything before was ASCII, so it is read as windows-1252 (pass the encoding option to choose another)` : `the input is not valid UTF-8 (at byte ${at}${cutNote}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`
|
|
1719
|
+
);
|
|
1720
|
+
this.use("windows-1252", false);
|
|
1721
|
+
this.fellBack = true;
|
|
1722
|
+
this.mixedPending = true;
|
|
1723
|
+
this.reportUtf8After(all, base);
|
|
1724
|
+
return this.decoder.decode(all, { stream });
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (this.encoding === "utf-8") {
|
|
1728
|
+
const after = this.mayFallBack ? " after valid non-ASCII UTF-8 text; pass the encoding option" : "";
|
|
1729
|
+
return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}${after}`, void 0, {
|
|
1730
|
+
byteOffset: at
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
const end = this.offset + bytes.byteLength;
|
|
1734
|
+
const odd = !stream && this.encoding.startsWith("utf-16") && end % 2 === 1;
|
|
1735
|
+
return this.report.fail(
|
|
1736
|
+
INVALID_ENCODING_CODE,
|
|
1737
|
+
odd ? `the input ends with half a ${this.encoding} code unit at byte ${end - 1} (it may be truncated)` : `the bytes near byte ${base} are not valid ${this.encoding}`,
|
|
1738
|
+
void 0,
|
|
1739
|
+
{ byteOffset: odd ? end - 1 : base }
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* After the windows-1252 fallback, report once that later bytes hold a valid multi-byte UTF-8
|
|
1744
|
+
* sequence: the input mixes encodings, and those characters are read as two or three
|
|
1745
|
+
* windows-1252 characters each.
|
|
1746
|
+
* @param bytes - bytes being decoded as windows-1252
|
|
1747
|
+
* @param start - the byte offset of the first of them
|
|
1748
|
+
*/
|
|
1749
|
+
reportUtf8After(bytes, start) {
|
|
1750
|
+
const at = utf8SequenceIndex(bytes);
|
|
1751
|
+
if (at < 0) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
this.mixedPending = false;
|
|
1755
|
+
this.report.warning(
|
|
1756
|
+
"coercion",
|
|
1757
|
+
ENCODING_FALLBACK_CODE,
|
|
1758
|
+
`bytes read as windows-1252 hold valid UTF-8 (at byte ${start + at}): the input mixes encodings, and its UTF-8 characters are read as windows-1252 (pass the encoding option to choose one)`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Note the first control byte of the next bytes (see `control`).
|
|
1763
|
+
* @param bytes - the bytes, starting at `offset`
|
|
1764
|
+
* @param final - whether they end the input
|
|
1765
|
+
*/
|
|
1766
|
+
watch(bytes, final) {
|
|
1767
|
+
if (this.control !== null || bytes.byteLength === 0) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (this.subAt >= 0) {
|
|
1771
|
+
this.control = { at: this.subAt, byte: 26 };
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const index = controlByte(bytes, true);
|
|
1775
|
+
if (index >= 0) {
|
|
1776
|
+
this.control = { at: this.offset + index, byte: bytes[index] };
|
|
1777
|
+
} else if (!final && bytes[bytes.byteLength - 1] === 26) {
|
|
1778
|
+
this.subAt = this.offset + bytes.byteLength - 1;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Fail the import as binary data.
|
|
1783
|
+
* @param control - the control byte that shows it
|
|
1784
|
+
* @param control.at - its offset
|
|
1785
|
+
* @param control.byte - its value
|
|
1786
|
+
* @returns never
|
|
1787
|
+
*/
|
|
1788
|
+
binary(control) {
|
|
1789
|
+
const hex = control.byte.toString(16).padStart(2, "0");
|
|
1790
|
+
return this.report.fail(
|
|
1791
|
+
INVALID_UTF8_CODE,
|
|
1792
|
+
`the input is binary data, not text: control byte 0x${hex} at byte ${control.at}`,
|
|
1793
|
+
void 0,
|
|
1794
|
+
{ byteOffset: control.at }
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Switch to an encoding.
|
|
1799
|
+
* @param label - the encoding label
|
|
1800
|
+
* @param mayFallBack - whether a UTF-8 failure may switch to windows-1252
|
|
1801
|
+
*/
|
|
1802
|
+
use(label, mayFallBack) {
|
|
1803
|
+
this.decoder = new TextDecoder(label, { fatal: true, ignoreBOM: true });
|
|
1804
|
+
this.encoding = this.decoder.encoding;
|
|
1805
|
+
this.mayFallBack = mayFallBack;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function utf8SequenceIndex(bytes) {
|
|
1809
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1810
|
+
if (bytes[i] >= 194 && isUtf8SequenceAt(bytes, i)) {
|
|
1811
|
+
return i;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return -1;
|
|
1815
|
+
}
|
|
1816
|
+
function startsWithUtf8(bytes) {
|
|
1817
|
+
const i = bytes.findIndex((b) => b >= 128);
|
|
1818
|
+
return i >= 0 && isUtf8SequenceAt(bytes, i);
|
|
1819
|
+
}
|
|
1820
|
+
const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
1821
|
+
function isUtf8SequenceAt(bytes, i) {
|
|
1822
|
+
const lead = bytes[i];
|
|
1823
|
+
let length = 0;
|
|
1824
|
+
if (lead >= 194 && lead <= 223) {
|
|
1825
|
+
length = 2;
|
|
1826
|
+
} else if (lead >= 224 && lead <= 239) {
|
|
1827
|
+
length = 3;
|
|
1828
|
+
} else if (lead >= 240 && lead <= 244) {
|
|
1829
|
+
length = 4;
|
|
1830
|
+
}
|
|
1831
|
+
if (length === 0 || i + length > bytes.byteLength) {
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
try {
|
|
1835
|
+
STRICT_UTF8.decode(bytes.subarray(i, i + length));
|
|
1836
|
+
return true;
|
|
1837
|
+
} catch {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
const CP437_HIGH = [
|
|
1842
|
+
199,
|
|
1843
|
+
252,
|
|
1844
|
+
233,
|
|
1845
|
+
226,
|
|
1846
|
+
228,
|
|
1847
|
+
224,
|
|
1848
|
+
229,
|
|
1849
|
+
231,
|
|
1850
|
+
234,
|
|
1851
|
+
235,
|
|
1852
|
+
232,
|
|
1853
|
+
239,
|
|
1854
|
+
238,
|
|
1855
|
+
236,
|
|
1856
|
+
196,
|
|
1857
|
+
197,
|
|
1858
|
+
201,
|
|
1859
|
+
230,
|
|
1860
|
+
198,
|
|
1861
|
+
244,
|
|
1862
|
+
246,
|
|
1863
|
+
242,
|
|
1864
|
+
251,
|
|
1865
|
+
249,
|
|
1866
|
+
255,
|
|
1867
|
+
214,
|
|
1868
|
+
220,
|
|
1869
|
+
162,
|
|
1870
|
+
163,
|
|
1871
|
+
165,
|
|
1872
|
+
8359,
|
|
1873
|
+
402,
|
|
1874
|
+
225,
|
|
1875
|
+
237,
|
|
1876
|
+
243,
|
|
1877
|
+
250,
|
|
1878
|
+
241,
|
|
1879
|
+
209,
|
|
1880
|
+
170,
|
|
1881
|
+
186,
|
|
1882
|
+
191,
|
|
1883
|
+
8976,
|
|
1884
|
+
172,
|
|
1885
|
+
189,
|
|
1886
|
+
188,
|
|
1887
|
+
161,
|
|
1888
|
+
171,
|
|
1889
|
+
187,
|
|
1890
|
+
9617,
|
|
1891
|
+
9618,
|
|
1892
|
+
9619,
|
|
1893
|
+
9474,
|
|
1894
|
+
9508,
|
|
1895
|
+
9569,
|
|
1896
|
+
9570,
|
|
1897
|
+
9558,
|
|
1898
|
+
9557,
|
|
1899
|
+
9571,
|
|
1900
|
+
9553,
|
|
1901
|
+
9559,
|
|
1902
|
+
9565,
|
|
1903
|
+
9564,
|
|
1904
|
+
9563,
|
|
1905
|
+
9488,
|
|
1906
|
+
9492,
|
|
1907
|
+
9524,
|
|
1908
|
+
9516,
|
|
1909
|
+
9500,
|
|
1910
|
+
9472,
|
|
1911
|
+
9532,
|
|
1912
|
+
9566,
|
|
1913
|
+
9567,
|
|
1914
|
+
9562,
|
|
1915
|
+
9556,
|
|
1916
|
+
9577,
|
|
1917
|
+
9574,
|
|
1918
|
+
9568,
|
|
1919
|
+
9552,
|
|
1920
|
+
9580,
|
|
1921
|
+
9575,
|
|
1922
|
+
9576,
|
|
1923
|
+
9572,
|
|
1924
|
+
9573,
|
|
1925
|
+
9561,
|
|
1926
|
+
9560,
|
|
1927
|
+
9554,
|
|
1928
|
+
9555,
|
|
1929
|
+
9579,
|
|
1930
|
+
9578,
|
|
1931
|
+
9496,
|
|
1932
|
+
9484,
|
|
1933
|
+
9608,
|
|
1934
|
+
9604,
|
|
1935
|
+
9612,
|
|
1936
|
+
9616,
|
|
1937
|
+
9600,
|
|
1938
|
+
945,
|
|
1939
|
+
223,
|
|
1940
|
+
915,
|
|
1941
|
+
960,
|
|
1942
|
+
931,
|
|
1943
|
+
963,
|
|
1944
|
+
181,
|
|
1945
|
+
964,
|
|
1946
|
+
934,
|
|
1947
|
+
920,
|
|
1948
|
+
937,
|
|
1949
|
+
948,
|
|
1950
|
+
8734,
|
|
1951
|
+
966,
|
|
1952
|
+
949,
|
|
1953
|
+
8745,
|
|
1954
|
+
8801,
|
|
1955
|
+
177,
|
|
1956
|
+
8805,
|
|
1957
|
+
8804,
|
|
1958
|
+
8992,
|
|
1959
|
+
8993,
|
|
1960
|
+
247,
|
|
1961
|
+
8776,
|
|
1962
|
+
176,
|
|
1963
|
+
8729,
|
|
1964
|
+
183,
|
|
1965
|
+
8730,
|
|
1966
|
+
8319,
|
|
1967
|
+
178,
|
|
1968
|
+
9632,
|
|
1969
|
+
160
|
|
1970
|
+
];
|
|
1971
|
+
function invalidUtf8(bytes) {
|
|
1972
|
+
let i = 0;
|
|
1973
|
+
while (i < bytes.byteLength) {
|
|
1974
|
+
const lead = bytes[i];
|
|
1975
|
+
const sequence = lead < 128 ? ASCII_SEQUENCE : utf8Sequence(lead);
|
|
1976
|
+
if (sequence === null) {
|
|
1977
|
+
return { index: i, truncated: false, cut: false };
|
|
1978
|
+
}
|
|
1979
|
+
const problem = checkContinuation(bytes, i, sequence);
|
|
1980
|
+
if (problem !== null) {
|
|
1981
|
+
return problem;
|
|
1982
|
+
}
|
|
1983
|
+
i += sequence.need + 1;
|
|
1984
|
+
}
|
|
1985
|
+
return { index: 0, truncated: false, cut: false };
|
|
1986
|
+
}
|
|
1987
|
+
const ASCII_SEQUENCE = { need: 0, low: 128, high: 191 };
|
|
1988
|
+
function utf8Sequence(lead) {
|
|
1989
|
+
if (lead >= 194 && lead <= 223) {
|
|
1990
|
+
return { need: 1, low: 128, high: 191 };
|
|
1991
|
+
}
|
|
1992
|
+
if (lead >= 224 && lead <= 239) {
|
|
1993
|
+
return { need: 2, low: lead === 224 ? 160 : 128, high: lead === 237 ? 159 : 191 };
|
|
1994
|
+
}
|
|
1995
|
+
if (lead >= 240 && lead <= 244) {
|
|
1996
|
+
return { need: 3, low: lead === 240 ? 144 : 128, high: lead === 244 ? 143 : 191 };
|
|
1997
|
+
}
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
function checkContinuation(bytes, i, sequence) {
|
|
2001
|
+
for (let k = 1; k <= sequence.need; k++) {
|
|
2002
|
+
if (i + k >= bytes.byteLength) {
|
|
2003
|
+
return { index: i, truncated: k > 1, cut: true };
|
|
2004
|
+
}
|
|
2005
|
+
const next = bytes[i + k];
|
|
2006
|
+
const low = k === 1 ? sequence.low : 128;
|
|
2007
|
+
const high = k === 1 ? sequence.high : 191;
|
|
2008
|
+
if (next < low || next > high) {
|
|
2009
|
+
return { index: i, truncated: false, cut: false };
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
function controlByte(bytes, final) {
|
|
2015
|
+
const last = bytes.byteLength - 1;
|
|
2016
|
+
return bytes.findIndex(
|
|
2017
|
+
(b, i) => b < 32 && b !== 9 && b !== 10 && b !== 12 && b !== 13 && !(b === 26 && final && i === last)
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
function decodeEntryName(bytes, cp437 = false) {
|
|
2021
|
+
try {
|
|
2022
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
2023
|
+
} catch {
|
|
2024
|
+
if (!cp437) {
|
|
2025
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
2026
|
+
}
|
|
2027
|
+
let out = "";
|
|
2028
|
+
for (const byte of bytes) {
|
|
2029
|
+
out += String.fromCodePoint(byte < 128 ? byte : CP437_HIGH[byte - 128]);
|
|
2030
|
+
}
|
|
2031
|
+
return out;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function concatBytes(chunks) {
|
|
2035
|
+
if (chunks.length === 1) {
|
|
2036
|
+
return chunks[0];
|
|
2037
|
+
}
|
|
2038
|
+
const out = new Uint8Array(chunks.reduce((sum, c) => sum + c.byteLength, 0));
|
|
2039
|
+
let offset = 0;
|
|
2040
|
+
for (const chunk of chunks) {
|
|
2041
|
+
out.set(chunk, offset);
|
|
2042
|
+
offset += chunk.byteLength;
|
|
2043
|
+
}
|
|
2044
|
+
return out;
|
|
2045
|
+
}
|
|
2046
|
+
const DECODE_SLICE = 256 * 1024;
|
|
2047
|
+
const BOM = String.fromCharCode(65279);
|
|
2048
|
+
const SUB = String.fromCodePoint(26);
|
|
2049
|
+
const MAX_TEXT_LENGTH = 2 ** 29 - 24;
|
|
2050
|
+
const CONTROL_CHARACTER = new RegExp(
|
|
2051
|
+
`[${String.fromCodePoint(0)}-${String.fromCodePoint(8, 11, 14)}-${String.fromCodePoint(31, 127)}-${String.fromCodePoint(159)}${BOM}]`
|
|
2052
|
+
);
|
|
2053
|
+
function isImportInput(input) {
|
|
2054
|
+
if (typeof input === "string" || asBytes(input) !== null) {
|
|
2055
|
+
return true;
|
|
2056
|
+
}
|
|
2057
|
+
if (typeof input !== "object" || input === null) {
|
|
2058
|
+
return false;
|
|
2059
|
+
}
|
|
2060
|
+
return typeof input.getReader === "function" || Symbol.asyncIterator in input;
|
|
2061
|
+
}
|
|
2062
|
+
function asBytes(value) {
|
|
2063
|
+
if (value instanceof Uint8Array) {
|
|
2064
|
+
return value;
|
|
2065
|
+
}
|
|
2066
|
+
if (ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object Uint8Array]") {
|
|
2067
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
2068
|
+
}
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
function normalizeInput(input) {
|
|
2072
|
+
if (typeof input === "string") {
|
|
2073
|
+
return input;
|
|
2074
|
+
}
|
|
2075
|
+
const bytes = asBytes(input);
|
|
2076
|
+
if (bytes !== null) {
|
|
2077
|
+
return bytes;
|
|
2078
|
+
}
|
|
2079
|
+
if (isImportInput(input)) {
|
|
2080
|
+
return input;
|
|
2081
|
+
}
|
|
2082
|
+
const tag = Object.prototype.toString.call(input).slice(8, -1);
|
|
2083
|
+
let found = input === null ? "null" : typeof input;
|
|
2084
|
+
let fix = "";
|
|
2085
|
+
if (tag === "ArrayBuffer" || tag === "SharedArrayBuffer") {
|
|
2086
|
+
found = `an ${tag}`;
|
|
2087
|
+
fix = "; wrap it as new Uint8Array(buffer)";
|
|
2088
|
+
} else if (ArrayBuffer.isView(input)) {
|
|
2089
|
+
found = `a ${tag}`;
|
|
2090
|
+
fix = "; pass new Uint8Array(view.buffer, view.byteOffset, view.byteLength)";
|
|
2091
|
+
} else if (typeof input === "object" && input !== null) {
|
|
2092
|
+
const object = input;
|
|
2093
|
+
if (typeof object.stream === "function" && typeof object.size === "number") {
|
|
2094
|
+
found = "a Blob or File";
|
|
2095
|
+
fix = "; pass blob.stream()";
|
|
2096
|
+
} else if ("body" in object && typeof object.arrayBuffer === "function") {
|
|
2097
|
+
found = "a fetch Response";
|
|
2098
|
+
fix = "; pass response.body";
|
|
2099
|
+
} else {
|
|
2100
|
+
found = `an object (${tag})`;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
throw new GraphFormatError(
|
|
2104
|
+
"E_UNSUPPORTED",
|
|
2105
|
+
`the input is ${found}; an importer reads a string, a Uint8Array, a ReadableStream or an async iterable of strings and Uint8Arrays${fix}`,
|
|
2106
|
+
{ reason: "input type", found }
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
function inputLength(input) {
|
|
2110
|
+
if (typeof input === "string") {
|
|
2111
|
+
return input.length;
|
|
2112
|
+
}
|
|
2113
|
+
if (input instanceof Uint8Array) {
|
|
2114
|
+
return input.byteLength;
|
|
2115
|
+
}
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
function utf8Length(text) {
|
|
2119
|
+
let bytes = 0;
|
|
2120
|
+
let i = 0;
|
|
2121
|
+
while (i < text.length) {
|
|
2122
|
+
const c = text.charCodeAt(i);
|
|
2123
|
+
const pair = c >= 55296 && c <= 56319 && (text.charCodeAt(i + 1) & 64512) === 56320;
|
|
2124
|
+
if (c < 128) {
|
|
2125
|
+
bytes++;
|
|
2126
|
+
} else if (c < 2048) {
|
|
2127
|
+
bytes += 2;
|
|
2128
|
+
} else {
|
|
2129
|
+
bytes += pair ? 4 : 3;
|
|
2130
|
+
}
|
|
2131
|
+
i += pair ? 2 : 1;
|
|
2132
|
+
}
|
|
2133
|
+
return bytes;
|
|
2134
|
+
}
|
|
2135
|
+
function throwIfAborted(signal) {
|
|
2136
|
+
if (signal === null || signal === void 0 || !signal.aborted) {
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const { reason } = signal;
|
|
2140
|
+
if (reason === void 0) {
|
|
2141
|
+
throw abortError();
|
|
2142
|
+
}
|
|
2143
|
+
throw reason;
|
|
2144
|
+
}
|
|
2145
|
+
function abortError() {
|
|
2146
|
+
const message = "The import was aborted";
|
|
2147
|
+
if (typeof DOMException === "function") {
|
|
2148
|
+
return new DOMException(message, "AbortError");
|
|
2149
|
+
}
|
|
2150
|
+
const err = new Error(message);
|
|
2151
|
+
err.name = "AbortError";
|
|
2152
|
+
return err;
|
|
2153
|
+
}
|
|
2154
|
+
function abortable(promise, signal) {
|
|
2155
|
+
if (signal === null || typeof signal.addEventListener !== "function") {
|
|
2156
|
+
return promise;
|
|
2157
|
+
}
|
|
2158
|
+
return new Promise((resolve, reject) => {
|
|
2159
|
+
const onAbort = () => {
|
|
2160
|
+
try {
|
|
2161
|
+
throwIfAborted(signal);
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
reject(err);
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2167
|
+
promise.then(
|
|
2168
|
+
(value) => {
|
|
2169
|
+
signal.removeEventListener("abort", onAbort);
|
|
2170
|
+
resolve(value);
|
|
2171
|
+
},
|
|
2172
|
+
(err) => {
|
|
2173
|
+
signal.removeEventListener("abort", onAbort);
|
|
2174
|
+
reject(err);
|
|
2175
|
+
}
|
|
2176
|
+
);
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
function lockReader(stream) {
|
|
2180
|
+
if (stream.locked) {
|
|
2181
|
+
throw new GraphFormatError(
|
|
2182
|
+
"E_UNSUPPORTED",
|
|
2183
|
+
"the input ReadableStream is locked: another reader holds it (release it, or pass a fresh stream)",
|
|
2184
|
+
{ reason: "stream locked" }
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
return stream.getReader();
|
|
2188
|
+
}
|
|
2189
|
+
class TextFilter {
|
|
2190
|
+
/**
|
|
2191
|
+
* Create the filter of one import.
|
|
2192
|
+
* @param report - where the warnings and the fatal errors go
|
|
2193
|
+
* @param options - whether an empty input is valid, whether the format is XML
|
|
2194
|
+
*/
|
|
2195
|
+
constructor(report, options) {
|
|
2196
|
+
this.report = report;
|
|
2197
|
+
this.options = options;
|
|
2198
|
+
this.first = true;
|
|
2199
|
+
this.content = false;
|
|
2200
|
+
this.heldSub = false;
|
|
2201
|
+
this.warned = false;
|
|
2202
|
+
this.strayBom = false;
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Filter the next decoded text.
|
|
2206
|
+
* @param chunk - the text
|
|
2207
|
+
* @returns the text to hand on
|
|
2208
|
+
*/
|
|
2209
|
+
push(chunk) {
|
|
2210
|
+
let text = this.heldSub ? SUB + chunk : chunk;
|
|
2211
|
+
this.heldSub = false;
|
|
2212
|
+
if (text.length === 0) {
|
|
2213
|
+
return text;
|
|
2214
|
+
}
|
|
2215
|
+
if (this.first) {
|
|
2216
|
+
text = this.skipByteOrderMarks(text);
|
|
2217
|
+
if (text.length === 0) {
|
|
2218
|
+
return text;
|
|
2219
|
+
}
|
|
2220
|
+
this.first = false;
|
|
2221
|
+
refuseForeign(text, this.report, this.options);
|
|
2222
|
+
}
|
|
2223
|
+
if (text.endsWith(SUB)) {
|
|
2224
|
+
this.heldSub = true;
|
|
2225
|
+
text = text.slice(0, -1);
|
|
2226
|
+
}
|
|
2227
|
+
if (!this.content && /\S/.test(text)) {
|
|
2228
|
+
this.content = true;
|
|
2229
|
+
}
|
|
2230
|
+
if (!this.warned) {
|
|
2231
|
+
this.warnControl(text);
|
|
2232
|
+
}
|
|
2233
|
+
return text;
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Drop the byte order marks the input starts with, warning when there is more than one.
|
|
2237
|
+
* @param text - the first text
|
|
2238
|
+
* @returns the text without them
|
|
2239
|
+
*/
|
|
2240
|
+
skipByteOrderMarks(text) {
|
|
2241
|
+
let start = 0;
|
|
2242
|
+
while (text.codePointAt(start) === 65279) {
|
|
2243
|
+
start++;
|
|
2244
|
+
}
|
|
2245
|
+
if (start > 1 && !this.strayBom) {
|
|
2246
|
+
this.strayBom = true;
|
|
2247
|
+
this.report.warning(
|
|
2248
|
+
"validation-error",
|
|
2249
|
+
CONTROL_CHARACTER_CODE,
|
|
2250
|
+
`the input starts with ${start} byte order marks (U+FEFF); the extra ones were ignored`,
|
|
2251
|
+
{ element: "U+FEFF" }
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
return text.slice(start);
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Warn, once, about the first control character the text holds.
|
|
2258
|
+
* @param text - the text
|
|
2259
|
+
*/
|
|
2260
|
+
warnControl(text) {
|
|
2261
|
+
const match = CONTROL_CHARACTER.exec(text);
|
|
2262
|
+
if (match === null) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
this.warned = true;
|
|
2266
|
+
const code = (match[0].codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0");
|
|
2267
|
+
const around = JSON.stringify(text.slice(Math.max(0, match.index - 20), match.index + 20));
|
|
2268
|
+
this.report.warning(
|
|
2269
|
+
"validation-error",
|
|
2270
|
+
CONTROL_CHARACTER_CODE,
|
|
2271
|
+
`the input holds the control character U+${code} (in ${around}); it is kept in the id or value it is part of`,
|
|
2272
|
+
{ element: `U+${code}` }
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
/** The input ended: drop a held Ctrl-Z with a warning, and refuse an input without content. */
|
|
2276
|
+
end() {
|
|
2277
|
+
if (this.heldSub) {
|
|
2278
|
+
this.report.warning(
|
|
2279
|
+
"validation-error",
|
|
2280
|
+
CONTROL_CHARACTER_CODE,
|
|
2281
|
+
"the input ends with a Ctrl-Z (0x1A), the DOS end-of-file marker; it was ignored",
|
|
2282
|
+
{ element: "U+001A" }
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
if (!this.content && this.options.allowEmpty !== true) {
|
|
2286
|
+
this.report.fail(EMPTY_INPUT_CODE, this.first ? "the input is empty" : "the input holds only whitespace");
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
function reportTextEncoding(report, encoding) {
|
|
2291
|
+
if (encoding !== null && encoding !== void 0) {
|
|
2292
|
+
report.warning(
|
|
2293
|
+
"unsupported",
|
|
2294
|
+
OPTION_IGNORED_CODE,
|
|
2295
|
+
"option encoding has no effect on text input, which is already decoded",
|
|
2296
|
+
{ element: "encoding" }
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
async function* textChunks(rawInput, report, options = {}) {
|
|
2301
|
+
const input = normalizeInput(rawInput);
|
|
2302
|
+
const signal = options.signal ?? null;
|
|
2303
|
+
const onProgress = options.onProgress ?? null;
|
|
2304
|
+
const filter = new TextFilter(report, options);
|
|
2305
|
+
throwIfAborted(signal);
|
|
2306
|
+
if (typeof input === "string") {
|
|
2307
|
+
reportTextEncoding(report, options.encoding);
|
|
2308
|
+
const total = onProgress === null ? 0 : utf8Length(input);
|
|
2309
|
+
let done2 = 0;
|
|
2310
|
+
let start = 0;
|
|
2311
|
+
while (start < input.length) {
|
|
2312
|
+
throwIfAborted(signal);
|
|
2313
|
+
let end = Math.min(start + DECODE_SLICE, input.length);
|
|
2314
|
+
const highSurrogate = (input.charCodeAt(end - 1) & 64512) === 55296;
|
|
2315
|
+
if (end < input.length && highSurrogate) {
|
|
2316
|
+
end++;
|
|
2317
|
+
}
|
|
2318
|
+
const piece = start === 0 && end === input.length ? input : input.slice(start, end);
|
|
2319
|
+
start = end;
|
|
2320
|
+
const text = filter.push(piece);
|
|
2321
|
+
if (text.length > 0) {
|
|
2322
|
+
yield text;
|
|
2323
|
+
}
|
|
2324
|
+
if (onProgress !== null) {
|
|
2325
|
+
done2 += utf8Length(piece);
|
|
2326
|
+
onProgress(done2, total);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (input.length === 0) {
|
|
2330
|
+
onProgress?.(0, 0);
|
|
2331
|
+
}
|
|
2332
|
+
filter.end();
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
const decoder = new ByteDecoder(report, options);
|
|
2336
|
+
const headBytes = Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
|
|
2337
|
+
if (input instanceof Uint8Array) {
|
|
2338
|
+
const length = input.byteLength;
|
|
2339
|
+
decoder.start(input.subarray(0, headBytes));
|
|
2340
|
+
for (let offset = 0; offset < length; offset += DECODE_SLICE) {
|
|
2341
|
+
throwIfAborted(signal);
|
|
2342
|
+
if (input.byteLength !== length) {
|
|
2343
|
+
throw new GraphFormatError(
|
|
2344
|
+
"E_UNSUPPORTED",
|
|
2345
|
+
"the input's buffer was detached (transferred) or resized during the import",
|
|
2346
|
+
{ reason: "detached" }
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
|
|
2350
|
+
const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
|
|
2351
|
+
if (text.length > 0) {
|
|
2352
|
+
yield text;
|
|
2353
|
+
}
|
|
2354
|
+
onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
|
|
2355
|
+
}
|
|
2356
|
+
if (length === 0) {
|
|
2357
|
+
onProgress?.(0, 0);
|
|
2358
|
+
}
|
|
2359
|
+
filter.end();
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const needed = options.declaredEncoding === void 0 ? BOM_BYTES : headBytes;
|
|
2363
|
+
let done = 0;
|
|
2364
|
+
let head = [];
|
|
2365
|
+
let headLength = 0;
|
|
2366
|
+
const flushHead = (stream) => {
|
|
2367
|
+
const bytes = concatBytes(head);
|
|
2368
|
+
head = [];
|
|
2369
|
+
headLength = 0;
|
|
2370
|
+
decoder.start(bytes.subarray(0, headBytes));
|
|
2371
|
+
return decoder.decode(bytes, stream);
|
|
2372
|
+
};
|
|
2373
|
+
const chunks = isReadableStream(input) ? streamChunks(input, signal) : input;
|
|
2374
|
+
for await (const chunk of chunks) {
|
|
2375
|
+
throwIfAborted(signal);
|
|
2376
|
+
let text;
|
|
2377
|
+
const bytes = typeof chunk === "string" ? null : asBytes(chunk);
|
|
2378
|
+
if (typeof chunk === "string") {
|
|
2379
|
+
let pending = "";
|
|
2380
|
+
if (headLength > 0) {
|
|
2381
|
+
pending = decoder.flushBeforeText(() => flushHead(false));
|
|
2382
|
+
} else if (decoder.started) {
|
|
2383
|
+
pending = decoder.flushBeforeText(() => decoder.decode(new Uint8Array(0), false));
|
|
2384
|
+
} else {
|
|
2385
|
+
decoder.start(new Uint8Array(0));
|
|
2386
|
+
}
|
|
2387
|
+
text = filter.push(pending + chunk);
|
|
2388
|
+
done += onProgress === null ? 0 : utf8Length(chunk);
|
|
2389
|
+
} else if (bytes !== null) {
|
|
2390
|
+
done += bytes.byteLength;
|
|
2391
|
+
if (decoder.started) {
|
|
2392
|
+
text = filter.push(decoder.decode(bytes, true));
|
|
2393
|
+
} else {
|
|
2394
|
+
head.push(bytes);
|
|
2395
|
+
headLength += bytes.byteLength;
|
|
2396
|
+
text = headLength >= needed ? filter.push(flushHead(true)) : "";
|
|
2397
|
+
}
|
|
2398
|
+
} else {
|
|
2399
|
+
throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
|
|
2400
|
+
reason: "chunk type",
|
|
2401
|
+
found: typeof chunk
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
if (text.length > 0) {
|
|
2405
|
+
yield text;
|
|
2406
|
+
}
|
|
2407
|
+
onProgress?.(done);
|
|
2408
|
+
}
|
|
2409
|
+
let tail = "";
|
|
2410
|
+
if (headLength > 0) {
|
|
2411
|
+
tail = flushHead(false);
|
|
2412
|
+
} else if (decoder.started) {
|
|
2413
|
+
tail = decoder.decode(new Uint8Array(0), false);
|
|
2414
|
+
}
|
|
2415
|
+
tail = filter.push(tail);
|
|
2416
|
+
if (tail.length > 0) {
|
|
2417
|
+
yield tail;
|
|
2418
|
+
}
|
|
2419
|
+
filter.end();
|
|
2420
|
+
onProgress?.(done, done);
|
|
2421
|
+
}
|
|
2422
|
+
function isReadableStream(input) {
|
|
2423
|
+
return typeof input.getReader === "function";
|
|
2424
|
+
}
|
|
2425
|
+
async function* streamChunks(stream, signal) {
|
|
2426
|
+
const reader = lockReader(stream);
|
|
2427
|
+
let finished = false;
|
|
2428
|
+
try {
|
|
2429
|
+
for (; ; ) {
|
|
2430
|
+
throwIfAborted(signal);
|
|
2431
|
+
const { done, value } = await abortable(reader.read(), signal);
|
|
2432
|
+
if (done) {
|
|
2433
|
+
finished = true;
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
yield value;
|
|
2437
|
+
}
|
|
2438
|
+
} finally {
|
|
2439
|
+
if (!finished) {
|
|
2440
|
+
await reader.cancel(signal?.reason).catch(() => void 0);
|
|
2441
|
+
}
|
|
2442
|
+
reader.releaseLock();
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
async function readBytes(rawInput, options = {}) {
|
|
2446
|
+
const input = normalizeInput(rawInput);
|
|
2447
|
+
const signal = options.signal ?? null;
|
|
2448
|
+
throwIfAborted(signal);
|
|
2449
|
+
if (typeof input === "string") {
|
|
2450
|
+
return null;
|
|
2451
|
+
}
|
|
2452
|
+
if (input instanceof Uint8Array) {
|
|
2453
|
+
options.onProgress?.(input.byteLength, input.byteLength);
|
|
2454
|
+
return input;
|
|
2455
|
+
}
|
|
2456
|
+
const parts = [];
|
|
2457
|
+
let done = 0;
|
|
2458
|
+
for await (const chunk of isReadableStream(input) ? streamChunks(input, signal) : input) {
|
|
2459
|
+
throwIfAborted(signal);
|
|
2460
|
+
if (typeof chunk === "string") {
|
|
2461
|
+
return null;
|
|
2462
|
+
}
|
|
2463
|
+
const bytes = asBytes(chunk);
|
|
2464
|
+
if (bytes === null) {
|
|
2465
|
+
throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
|
|
2466
|
+
reason: "chunk type",
|
|
2467
|
+
found: typeof chunk
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
parts.push(bytes);
|
|
2471
|
+
done += bytes.byteLength;
|
|
2472
|
+
options.onProgress?.(done);
|
|
2473
|
+
}
|
|
2474
|
+
options.onProgress?.(done, done);
|
|
2475
|
+
return parts.length === 0 ? new Uint8Array(0) : concatBytes(parts);
|
|
2476
|
+
}
|
|
2477
|
+
function tooLarge(report, message, line) {
|
|
2478
|
+
report.failWith("unsupported", TOO_LARGE_CODE, message, line === void 0 ? void 0 : { line });
|
|
2479
|
+
}
|
|
2480
|
+
async function readText(rawInput, report, options = {}) {
|
|
2481
|
+
const input = normalizeInput(rawInput);
|
|
2482
|
+
if (typeof input === "string") {
|
|
2483
|
+
throwIfAborted(options.signal);
|
|
2484
|
+
reportTextEncoding(report, options.encoding);
|
|
2485
|
+
const filter = new TextFilter(report, options);
|
|
2486
|
+
const text = filter.push(input);
|
|
2487
|
+
filter.end();
|
|
2488
|
+
if (options.onProgress !== null && options.onProgress !== void 0) {
|
|
2489
|
+
const total = utf8Length(input);
|
|
2490
|
+
options.onProgress(total, total);
|
|
2491
|
+
}
|
|
2492
|
+
return text;
|
|
2493
|
+
}
|
|
2494
|
+
const parts = [];
|
|
2495
|
+
let length = 0;
|
|
2496
|
+
for await (const chunk of textChunks(input, report, options)) {
|
|
2497
|
+
length += chunk.length;
|
|
2498
|
+
if (length > MAX_TEXT_LENGTH) {
|
|
2499
|
+
tooLarge(
|
|
2500
|
+
report,
|
|
2501
|
+
`the document is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
parts.push(chunk);
|
|
2505
|
+
}
|
|
2506
|
+
return parts.length === 1 ? parts[0] : parts.join("");
|
|
2507
|
+
}
|
|
2508
|
+
class LineReader {
|
|
2509
|
+
/**
|
|
2510
|
+
* Create a reader over an input; nothing is read until iteration starts.
|
|
2511
|
+
* @param input - the input
|
|
2512
|
+
* @param report - the report decode errors are recorded in
|
|
2513
|
+
* @param options - cancellation and progress
|
|
2514
|
+
*/
|
|
2515
|
+
constructor(input, report, options = {}) {
|
|
2516
|
+
this.lineNumber = 0;
|
|
2517
|
+
this.input = input;
|
|
2518
|
+
this.report = report;
|
|
2519
|
+
this.options = options;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* The 1-based number of the line most recently yielded.
|
|
2523
|
+
* @returns the line number; 0 before the first line
|
|
2524
|
+
*/
|
|
2525
|
+
get line() {
|
|
2526
|
+
return this.lineNumber;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* The E_TOO_LARGE message of a line longer than one string.
|
|
2530
|
+
* @returns the message
|
|
2531
|
+
*/
|
|
2532
|
+
lineTooLong() {
|
|
2533
|
+
return `a line is longer than ${MAX_TEXT_LENGTH} characters, the most one JavaScript string holds`;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Iterate the lines.
|
|
2537
|
+
* @yields one line at a time, terminator removed
|
|
2538
|
+
* @returns nothing
|
|
2539
|
+
*/
|
|
2540
|
+
async *[Symbol.asyncIterator]() {
|
|
2541
|
+
const pending = [];
|
|
2542
|
+
let pendingLength = 0;
|
|
2543
|
+
let trailingCr = false;
|
|
2544
|
+
for await (const chunk of textChunks(this.input, this.report, this.options)) {
|
|
2545
|
+
let start = 0;
|
|
2546
|
+
const end = chunk.length;
|
|
2547
|
+
if (trailingCr) {
|
|
2548
|
+
trailingCr = false;
|
|
2549
|
+
this.lineNumber++;
|
|
2550
|
+
const joined = pending.join("");
|
|
2551
|
+
pending.length = 0;
|
|
2552
|
+
pendingLength = 0;
|
|
2553
|
+
yield joined.slice(0, -1);
|
|
2554
|
+
if (chunk.charCodeAt(0) === 10) {
|
|
2555
|
+
start = 1;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
let nl = chunk.indexOf("\n", start);
|
|
2559
|
+
let cr = chunk.indexOf("\r", start);
|
|
2560
|
+
while (nl >= 0 || cr >= 0) {
|
|
2561
|
+
let cut;
|
|
2562
|
+
let next;
|
|
2563
|
+
if (cr >= 0 && (nl < 0 || cr < nl)) {
|
|
2564
|
+
if (cr === end - 1) {
|
|
2565
|
+
break;
|
|
2566
|
+
}
|
|
2567
|
+
cut = cr;
|
|
2568
|
+
next = chunk.charCodeAt(cr + 1) === 10 ? cr + 2 : cr + 1;
|
|
2569
|
+
} else {
|
|
2570
|
+
cut = nl;
|
|
2571
|
+
next = nl + 1;
|
|
2572
|
+
}
|
|
2573
|
+
this.lineNumber++;
|
|
2574
|
+
const piece = chunk.slice(start, cut);
|
|
2575
|
+
if (pending.length === 0) {
|
|
2576
|
+
yield piece;
|
|
2577
|
+
} else {
|
|
2578
|
+
if (pendingLength + piece.length > MAX_TEXT_LENGTH) {
|
|
2579
|
+
tooLarge(this.report, this.lineTooLong(), this.lineNumber);
|
|
2580
|
+
}
|
|
2581
|
+
pending.push(piece);
|
|
2582
|
+
const joined = pending.join("");
|
|
2583
|
+
pending.length = 0;
|
|
2584
|
+
pendingLength = 0;
|
|
2585
|
+
yield joined;
|
|
2586
|
+
}
|
|
2587
|
+
start = next;
|
|
2588
|
+
if (nl >= 0 && nl < next) {
|
|
2589
|
+
nl = chunk.indexOf("\n", next);
|
|
2590
|
+
}
|
|
2591
|
+
if (cr >= 0 && cr < next) {
|
|
2592
|
+
cr = chunk.indexOf("\r", next);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (start < end) {
|
|
2596
|
+
pendingLength += end - start;
|
|
2597
|
+
if (pendingLength > MAX_TEXT_LENGTH) {
|
|
2598
|
+
tooLarge(this.report, this.lineTooLong(), this.lineNumber + 1);
|
|
2599
|
+
}
|
|
2600
|
+
pending.push(start === 0 ? chunk : chunk.slice(start));
|
|
2601
|
+
trailingCr = chunk.charCodeAt(end - 1) === 13;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
if (pending.length > 0) {
|
|
2605
|
+
this.lineNumber++;
|
|
2606
|
+
const joined = pending.join("");
|
|
2607
|
+
yield joined.endsWith("\r") ? joined.slice(0, -1) : joined;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
const ID_COERCIONS = /* @__PURE__ */ new Set(["keep", "canonical", "string", "number"]);
|
|
2612
|
+
const NODE_ID_SOURCES = /* @__PURE__ */ new Set(["id", "label", "index"]);
|
|
2613
|
+
const DUPLICATE_POLICIES = /* @__PURE__ */ new Set(["keep", "error", "first", "last", "sum", "min", "max"]);
|
|
2614
|
+
const SELF_LOOP_POLICIES = /* @__PURE__ */ new Set(["keep", "drop", "error"]);
|
|
2615
|
+
const IMPORT_MIXED_POLICIES = /* @__PURE__ */ new Set(["expand", "directed", "undirected", "error"]);
|
|
2616
|
+
const EXPORT_MIXED_POLICIES = /* @__PURE__ */ new Set(["error", "directed", "undirected"]);
|
|
2617
|
+
const WEIGHT_DTYPES = /* @__PURE__ */ new Set(["f32", "f64"]);
|
|
2618
|
+
const LONG_MODES = /* @__PURE__ */ new Set(["f64", "string"]);
|
|
2619
|
+
const HYPEREDGE_POLICIES = /* @__PURE__ */ new Set(["error", "skip", "star", "clique"]);
|
|
2620
|
+
const SANITIZE_MODES = /* @__PURE__ */ new Set(["error", "mangle"]);
|
|
2621
|
+
const DEFAULT_ERROR_LIMIT = 100;
|
|
2622
|
+
const SINK_OPTION_NAMES = ["addMissingNodes", "duplicateEdges", "selfLoops", "weightDtype"];
|
|
2623
|
+
function reportSinkOptions(sink, options, report, enforcesMissingNodes = false) {
|
|
2624
|
+
if (options === void 0) {
|
|
2625
|
+
return 0;
|
|
2626
|
+
}
|
|
2627
|
+
let recorded = 0;
|
|
2628
|
+
for (const name of SINK_OPTION_NAMES) {
|
|
2629
|
+
const wanted = options[name];
|
|
2630
|
+
const actual = sink.options[name];
|
|
2631
|
+
if (wanted === void 0 || wanted === actual) {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
if (name === "addMissingNodes" && enforcesMissingNodes && wanted === false) {
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
report.warning(
|
|
2638
|
+
"coercion",
|
|
2639
|
+
SINK_OPTION_CODE,
|
|
2640
|
+
`option ${name}: ${JSON.stringify(wanted)} requested but the sink uses ${JSON.stringify(actual)}; the sink's setting applies`,
|
|
2641
|
+
{ element: name }
|
|
2642
|
+
);
|
|
2643
|
+
recorded++;
|
|
2644
|
+
}
|
|
2645
|
+
return recorded;
|
|
2646
|
+
}
|
|
2647
|
+
const IGNORABLE_OPTION_NAMES = [
|
|
2648
|
+
"ids",
|
|
2649
|
+
"nodeIdFrom",
|
|
2650
|
+
"addMissingNodes",
|
|
2651
|
+
"duplicateEdges",
|
|
2652
|
+
"selfLoops",
|
|
2653
|
+
"onMixedDirection",
|
|
2654
|
+
"defaultDirected",
|
|
2655
|
+
"weightFrom",
|
|
2656
|
+
"weightDtype",
|
|
2657
|
+
"long",
|
|
2658
|
+
"restoreMangledIds",
|
|
2659
|
+
"hyperedges"
|
|
2660
|
+
];
|
|
2661
|
+
const FIXED_DEFAULTS = {
|
|
2662
|
+
nodeIdFrom: "id",
|
|
2663
|
+
duplicateEdges: "keep",
|
|
2664
|
+
selfLoops: "keep",
|
|
2665
|
+
onMixedDirection: "expand",
|
|
2666
|
+
weightDtype: "f64",
|
|
2667
|
+
long: "f64",
|
|
2668
|
+
restoreMangledIds: true,
|
|
2669
|
+
hyperedges: "skip"
|
|
2670
|
+
};
|
|
2671
|
+
function reportUnusedOptions(options, report, used) {
|
|
2672
|
+
if (options === void 0) {
|
|
2673
|
+
return 0;
|
|
2674
|
+
}
|
|
2675
|
+
let recorded = 0;
|
|
2676
|
+
for (const name of IGNORABLE_OPTION_NAMES) {
|
|
2677
|
+
if (used.has(name) || SINK_OPTION_NAMES.includes(name)) {
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const value = options[name];
|
|
2681
|
+
if (value === void 0 || value === FIXED_DEFAULTS[name]) {
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
report.warning(
|
|
2685
|
+
"unsupported",
|
|
2686
|
+
OPTION_IGNORED_CODE,
|
|
2687
|
+
`option ${name}: ${JSON.stringify(value)} has no effect on the ${report.format} importer`,
|
|
2688
|
+
{ element: name }
|
|
2689
|
+
);
|
|
2690
|
+
recorded++;
|
|
2691
|
+
}
|
|
2692
|
+
return recorded;
|
|
2693
|
+
}
|
|
2694
|
+
function graphChosen(options) {
|
|
2695
|
+
return options?.graphIndex !== void 0 || options?.graphName !== void 0;
|
|
2696
|
+
}
|
|
2697
|
+
function graphNamesText(names) {
|
|
2698
|
+
const shown = names.slice(0, 10).map((n, i) => `${i} ${n === null ? "(unnamed)" : JSON.stringify(n)}`);
|
|
2699
|
+
const more = names.length > shown.length ? `, and ${names.length - shown.length} more` : "";
|
|
2700
|
+
return `; the file holds ${shown.join(", ")}${more}`;
|
|
2701
|
+
}
|
|
2702
|
+
function chooseGraph(names, options, report) {
|
|
2703
|
+
const { graphIndex, graphName } = options ?? {};
|
|
2704
|
+
if (graphIndex !== void 0 && (typeof graphIndex !== "number" || !Number.isInteger(graphIndex) || graphIndex < 0)) {
|
|
2705
|
+
throw new GraphFormatError(
|
|
2706
|
+
"E_UNSUPPORTED",
|
|
2707
|
+
`option graphIndex: ${describe(graphIndex)} is not a non-negative integer`,
|
|
2708
|
+
{
|
|
2709
|
+
option: "graphIndex",
|
|
2710
|
+
found: graphIndex
|
|
2711
|
+
}
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
if (graphName !== void 0 && typeof graphName !== "string") {
|
|
2715
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option graphName: ${describe(graphName)} is not a string`, {
|
|
2716
|
+
option: "graphName",
|
|
2717
|
+
found: graphName
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
if (graphIndex !== void 0 && graphName !== void 0) {
|
|
2721
|
+
throw new GraphFormatError("E_UNSUPPORTED", "options graphIndex and graphName both choose a graph; pass one", {
|
|
2722
|
+
option: "graphName",
|
|
2723
|
+
found: graphName
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
if (names.length === 0) {
|
|
2727
|
+
return report.fail(NO_GRAPH_CODE, "the input holds no graph");
|
|
2728
|
+
}
|
|
2729
|
+
if (graphName !== void 0) {
|
|
2730
|
+
const matches = names.flatMap((name, i) => name === graphName ? [i] : []);
|
|
2731
|
+
if (matches.length > 1) {
|
|
2732
|
+
return report.fail(
|
|
2733
|
+
AMBIGUOUS_GRAPH_NAME_CODE,
|
|
2734
|
+
`graphName ${JSON.stringify(graphName)} names the graphs at indexes ${matches.join(", ")}; pass graphIndex`,
|
|
2735
|
+
{ element: graphName }
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
if (matches.length === 0) {
|
|
2739
|
+
return report.fail(
|
|
2740
|
+
GRAPH_NOT_FOUND_CODE,
|
|
2741
|
+
`graphName ${JSON.stringify(graphName)} names none of the ${names.length} graph${plural(names.length)}${graphNamesText(names)}`,
|
|
2742
|
+
{ element: graphName },
|
|
2743
|
+
{ names: [...names] }
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
return matches[0];
|
|
2747
|
+
}
|
|
2748
|
+
const index = graphIndex ?? 0;
|
|
2749
|
+
if (index >= names.length) {
|
|
2750
|
+
return report.fail(
|
|
2751
|
+
GRAPH_NOT_FOUND_CODE,
|
|
2752
|
+
`graphIndex ${index} is beyond the ${names.length} graph${plural(names.length)}${graphNamesText(names)}`
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
return index;
|
|
2756
|
+
}
|
|
2757
|
+
function resolveImportOptions(options, defaults) {
|
|
2758
|
+
const o = options ?? {};
|
|
2759
|
+
return Object.freeze({
|
|
2760
|
+
ids: enumOption("ids", o.ids, defaults.ids, ID_COERCIONS),
|
|
2761
|
+
nodeIdFrom: enumOption("nodeIdFrom", o.nodeIdFrom, "id", NODE_ID_SOURCES),
|
|
2762
|
+
addMissingNodes: booleanOption("addMissingNodes", o.addMissingNodes, defaults.addMissingNodes ?? true),
|
|
2763
|
+
duplicateEdges: enumOption("duplicateEdges", o.duplicateEdges, "keep", DUPLICATE_POLICIES),
|
|
2764
|
+
selfLoops: enumOption("selfLoops", o.selfLoops, "keep", SELF_LOOP_POLICIES),
|
|
2765
|
+
onMixedDirection: enumOption("onMixedDirection", o.onMixedDirection, "expand", IMPORT_MIXED_POLICIES),
|
|
2766
|
+
defaultDirected: booleanOption("defaultDirected", o.defaultDirected, defaults.defaultDirected),
|
|
2767
|
+
weightFrom: weightFromOption(o.weightFrom, defaults.weightFrom),
|
|
2768
|
+
weightDtype: enumOption("weightDtype", o.weightDtype, "f64", WEIGHT_DTYPES),
|
|
2769
|
+
long: enumOption("long", o.long, "f64", LONG_MODES),
|
|
2770
|
+
restoreMangledIds: booleanOption("restoreMangledIds", o.restoreMangledIds, true),
|
|
2771
|
+
hyperedges: enumOption("hyperedges", o.hyperedges, "skip", HYPEREDGE_POLICIES),
|
|
2772
|
+
errorLimit: errorLimitOption(o.errorLimit),
|
|
2773
|
+
signal: signalOption(o.signal),
|
|
2774
|
+
onProgress: progressOption(o.onProgress),
|
|
2775
|
+
encoding: encodingOption(o.encoding)
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function resolveExportOptions(options) {
|
|
2779
|
+
const o = options ?? {};
|
|
2780
|
+
return Object.freeze({
|
|
2781
|
+
sanitizeIds: enumOption("sanitizeIds", o.sanitizeIds, "error", SANITIZE_MODES),
|
|
2782
|
+
onMixedDirection: enumOption("onMixedDirection", o.onMixedDirection, "error", EXPORT_MIXED_POLICIES)
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
function enumOption(name, value, fallback, allowed) {
|
|
2786
|
+
if (value === void 0) {
|
|
2787
|
+
return fallback;
|
|
2788
|
+
}
|
|
2789
|
+
if (typeof value !== "string" || !allowed.has(value)) {
|
|
2790
|
+
throw new GraphFormatError(
|
|
2791
|
+
"E_UNSUPPORTED",
|
|
2792
|
+
`option ${name}: ${describe(value)} is not one of ${list(allowed)}`,
|
|
2793
|
+
{
|
|
2794
|
+
option: name,
|
|
2795
|
+
found: value,
|
|
2796
|
+
supported: [...allowed]
|
|
2797
|
+
}
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
return value;
|
|
2801
|
+
}
|
|
2802
|
+
function booleanOption(name, value, fallback) {
|
|
2803
|
+
if (value === void 0) {
|
|
2804
|
+
return fallback;
|
|
2805
|
+
}
|
|
2806
|
+
if (typeof value !== "boolean") {
|
|
2807
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${describe(value)} is not a boolean`, {
|
|
2808
|
+
option: name,
|
|
2809
|
+
found: value
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
return value;
|
|
2813
|
+
}
|
|
2814
|
+
function weightFromOption(value, fallback) {
|
|
2815
|
+
if (value === void 0) {
|
|
2816
|
+
return fallback;
|
|
2817
|
+
}
|
|
2818
|
+
if (value === null || typeof value === "string" && value.length > 0) {
|
|
2819
|
+
return value;
|
|
2820
|
+
}
|
|
2821
|
+
throw new GraphFormatError(
|
|
2822
|
+
"E_UNSUPPORTED",
|
|
2823
|
+
`option weightFrom: ${describe(value)} is not an attribute name or null`,
|
|
2824
|
+
{ option: "weightFrom", found: value }
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
function errorLimitOption(value) {
|
|
2828
|
+
if (value === void 0) {
|
|
2829
|
+
return DEFAULT_ERROR_LIMIT;
|
|
2830
|
+
}
|
|
2831
|
+
if (typeof value === "number" && value >= 0 && (Number.isInteger(value) || value === Infinity)) {
|
|
2832
|
+
return value;
|
|
2833
|
+
}
|
|
2834
|
+
throw new GraphFormatError(
|
|
2835
|
+
"E_UNSUPPORTED",
|
|
2836
|
+
`option errorLimit: ${describe(value)} is not a non-negative integer or Infinity`,
|
|
2837
|
+
{ option: "errorLimit", found: value }
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
function signalOption(value) {
|
|
2841
|
+
if (value === void 0 || value === null) {
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2844
|
+
if (typeof value === "object" && typeof value.aborted === "boolean") {
|
|
2845
|
+
return value;
|
|
2846
|
+
}
|
|
2847
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option signal: ${describe(value)} is not an AbortSignal`, {
|
|
2848
|
+
option: "signal",
|
|
2849
|
+
found: typeof value
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
function progressOption(value) {
|
|
2853
|
+
if (value === void 0 || value === null) {
|
|
2854
|
+
return null;
|
|
2855
|
+
}
|
|
2856
|
+
if (typeof value === "function") {
|
|
2857
|
+
return value;
|
|
2858
|
+
}
|
|
2859
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option onProgress: ${describe(value)} is not a function`, {
|
|
2860
|
+
option: "onProgress",
|
|
2861
|
+
found: typeof value
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
function encodingOption(value) {
|
|
2865
|
+
if (value === void 0 || value === null) {
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
const canonical = typeof value === "string" ? canonicalEncoding(value) : null;
|
|
2869
|
+
if (canonical === null) {
|
|
2870
|
+
throw new GraphFormatError(
|
|
2871
|
+
"E_UNSUPPORTED",
|
|
2872
|
+
`option encoding: ${describe(value)} is not an encoding this platform's TextDecoder knows`,
|
|
2873
|
+
{ option: "encoding", found: value }
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
return canonical;
|
|
2877
|
+
}
|
|
2878
|
+
function forDecodedText(options) {
|
|
2879
|
+
return { ...options, encoding: void 0, onProgress: void 0 };
|
|
2880
|
+
}
|
|
2881
|
+
const FORMAT_DIALECTS = Object.freeze({
|
|
2882
|
+
csv: Object.freeze(["gephi", "generic"]),
|
|
2883
|
+
json: Object.freeze(["node-link", "d3", "jgf", "cytoscape", "graphology", "vis", "obographs"])
|
|
2884
|
+
});
|
|
2885
|
+
function otherFormatDialect(format, value) {
|
|
2886
|
+
return typeof value === "string" && Object.entries(FORMAT_DIALECTS).some(([f, names]) => f !== format && names.includes(value));
|
|
2887
|
+
}
|
|
2888
|
+
function describe(value) {
|
|
2889
|
+
switch (typeof value) {
|
|
2890
|
+
case "string":
|
|
2891
|
+
return JSON.stringify(value);
|
|
2892
|
+
case "number":
|
|
2893
|
+
case "boolean":
|
|
2894
|
+
return String(value);
|
|
2895
|
+
default:
|
|
2896
|
+
return value === null ? "null" : typeof value;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function list(allowed) {
|
|
2900
|
+
return [...allowed].map((v) => JSON.stringify(v)).join(", ");
|
|
2901
|
+
}
|
|
2902
|
+
class ImportError extends GraphFormatError {
|
|
2903
|
+
/**
|
|
2904
|
+
* Create an ImportError.
|
|
2905
|
+
* @param message - a plain-ASCII human-readable message
|
|
2906
|
+
* @param report - the partial report
|
|
2907
|
+
* @param details - optional machine-readable context; its `code` names the issue that stopped the import
|
|
2908
|
+
*/
|
|
2909
|
+
constructor(message, report, details) {
|
|
2910
|
+
super("E_IMPORT", message, details);
|
|
2911
|
+
this.name = "ImportError";
|
|
2912
|
+
Object.defineProperty(this, "report", { value: report, enumerable: false });
|
|
2913
|
+
this.issue = stoppingIssue(report.issues, details?.code);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function stoppingIssue(issues, code) {
|
|
2917
|
+
let lastError = null;
|
|
2918
|
+
for (let i = issues.length - 1; i >= 0; i--) {
|
|
2919
|
+
if (issues[i].code === code) {
|
|
2920
|
+
return issues[i];
|
|
2921
|
+
}
|
|
2922
|
+
if (lastError === null && issues[i].severity === "error") {
|
|
2923
|
+
lastError = issues[i];
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
return lastError;
|
|
2927
|
+
}
|
|
2928
|
+
const MAX_WARNINGS_PER_CODE = 1e3;
|
|
2929
|
+
const CATEGORY_BY_CODE = {
|
|
2930
|
+
E_UNKNOWN_NODE: "missing-value",
|
|
2931
|
+
E_UNKNOWN_COLUMN: "missing-value",
|
|
2932
|
+
E_DIRECTED: "coercion",
|
|
2933
|
+
E_TOO_LARGE: "unsupported",
|
|
2934
|
+
E_UNSUPPORTED: "unsupported",
|
|
2935
|
+
E_GPU_INELIGIBLE: "unsupported"
|
|
2936
|
+
};
|
|
2937
|
+
const PARSE_ERROR_CODE = "E_PARSE";
|
|
2938
|
+
function withLine(message, where) {
|
|
2939
|
+
const line = where?.line;
|
|
2940
|
+
return line === void 0 || line === null || message.includes(`line ${line}`) ? message : `line ${line}: ${message}`;
|
|
2941
|
+
}
|
|
2942
|
+
class ImportReportBuilder {
|
|
2943
|
+
/**
|
|
2944
|
+
* Create a builder for one import call.
|
|
2945
|
+
* @param format - the importer's format name
|
|
2946
|
+
* @param errorLimit - errors tolerated before the import aborts; Infinity for no limit
|
|
2947
|
+
*/
|
|
2948
|
+
constructor(format, errorLimit) {
|
|
2949
|
+
this.counts = { nodes: 0, edges: 0, skippedNodes: 0, skippedEdges: 0, expandedMixed: 0 };
|
|
2950
|
+
this.issueList = [];
|
|
2951
|
+
this.lossList = [];
|
|
2952
|
+
this.onceCodes = /* @__PURE__ */ new Set();
|
|
2953
|
+
this.warningsByCode = /* @__PURE__ */ new Map();
|
|
2954
|
+
this.errors = 0;
|
|
2955
|
+
this.warnings = 0;
|
|
2956
|
+
this.truncatedFlag = false;
|
|
2957
|
+
this.format = format;
|
|
2958
|
+
this.errorLimit = errorLimit;
|
|
2959
|
+
this.startedAt = now();
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Issues recorded so far, in order.
|
|
2963
|
+
* @returns the live list (not copied; finish() copies)
|
|
2964
|
+
*/
|
|
2965
|
+
get issues() {
|
|
2966
|
+
return this.issueList;
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Issues with severity "error".
|
|
2970
|
+
* @returns the count
|
|
2971
|
+
*/
|
|
2972
|
+
get errorCount() {
|
|
2973
|
+
return this.errors;
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Issues with severity "warning".
|
|
2977
|
+
* @returns the count
|
|
2978
|
+
*/
|
|
2979
|
+
get warningCount() {
|
|
2980
|
+
return this.warnings;
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Whether the error limit was exceeded.
|
|
2984
|
+
* @returns true once an error beyond the limit was recorded
|
|
2985
|
+
*/
|
|
2986
|
+
get truncated() {
|
|
2987
|
+
return this.truncatedFlag;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Loss notes recorded so far.
|
|
2991
|
+
* @returns the live list
|
|
2992
|
+
*/
|
|
2993
|
+
get lossy() {
|
|
2994
|
+
return this.lossList;
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Record an error. When the count goes beyond the error limit the report is marked truncated
|
|
2998
|
+
* and an ImportError carrying it is thrown; the importer does not catch that.
|
|
2999
|
+
* @param category - the issue category
|
|
3000
|
+
* @param code - a stable code such as "E_UNKNOWN_NODE"
|
|
3001
|
+
* @param message - a plain-ASCII message
|
|
3002
|
+
* @param where - the line and element, when known
|
|
3003
|
+
* @returns the recorded issue
|
|
3004
|
+
*/
|
|
3005
|
+
error(category, code, message, where) {
|
|
3006
|
+
const issue = makeIssue(category, "error", code, message, where);
|
|
3007
|
+
this.issueList.push(issue);
|
|
3008
|
+
this.errors++;
|
|
3009
|
+
if (this.errors > this.errorLimit) {
|
|
3010
|
+
this.truncatedFlag = true;
|
|
3011
|
+
throw this.abort(`error limit of ${this.errorLimit} exceeded: ${withLine(message, where)}`, {
|
|
3012
|
+
code,
|
|
3013
|
+
limit: this.errorLimit
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
return issue;
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Record a warning; warnings never count toward the limit. Beyond MAX_WARNINGS_PER_CODE
|
|
3020
|
+
* warnings of one code the warning is counted, not kept: finish() adds one W_ISSUES_SUPPRESSED
|
|
3021
|
+
* warning per such code with the number dropped.
|
|
3022
|
+
* @param category - the issue category
|
|
3023
|
+
* @param code - a stable code such as "W_WIDENED"
|
|
3024
|
+
* @param message - a plain-ASCII message
|
|
3025
|
+
* @param where - the line and element, when known
|
|
3026
|
+
* @returns the recorded issue
|
|
3027
|
+
*/
|
|
3028
|
+
warning(category, code, message, where) {
|
|
3029
|
+
const issue = makeIssue(category, "warning", code, message, where);
|
|
3030
|
+
const seen = (this.warningsByCode.get(code) ?? 0) + 1;
|
|
3031
|
+
this.warningsByCode.set(code, seen);
|
|
3032
|
+
if (seen <= MAX_WARNINGS_PER_CODE) {
|
|
3033
|
+
this.issueList.push(issue);
|
|
3034
|
+
this.warnings++;
|
|
3035
|
+
}
|
|
3036
|
+
return issue;
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Record a warning the first time its key is seen and ignore later repeats, for conditions
|
|
3040
|
+
* that would otherwise produce one warning per row.
|
|
3041
|
+
* @param category - the issue category
|
|
3042
|
+
* @param code - the stable code
|
|
3043
|
+
* @param message - a plain-ASCII message
|
|
3044
|
+
* @param where - the line and element, when known
|
|
3045
|
+
* @param key - what repeats are deduplicated on; the code itself by default (pass `code:column`
|
|
3046
|
+
* to warn once per column)
|
|
3047
|
+
* @returns the recorded issue, or null when the key was already recorded
|
|
3048
|
+
*/
|
|
3049
|
+
warnOnce(category, code, message, where, key = code) {
|
|
3050
|
+
if (this.onceCodes.has(key)) {
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
this.onceCodes.add(key);
|
|
3054
|
+
return this.warning(category, code, message, where);
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Record something the importer could not represent.
|
|
3058
|
+
* @param code - a stable code
|
|
3059
|
+
* @param message - a plain-ASCII message
|
|
3060
|
+
* @param column - the affected column, or null
|
|
3061
|
+
* @param count - the affected count, or null
|
|
3062
|
+
* @returns the recorded note
|
|
3063
|
+
*/
|
|
3064
|
+
loss(code, message, column = null, count = null) {
|
|
3065
|
+
const note = Object.freeze({ code, message, column, count });
|
|
3066
|
+
this.lossList.push(note);
|
|
3067
|
+
return note;
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* The per-element catch: record a thrown GraphFormatError as an error
|
|
3071
|
+
* issue with its code and the category the code implies. Anything else is re-thrown unchanged:
|
|
3072
|
+
* an ImportError (the import already aborted), an abort reason, and any other thrown value (a
|
|
3073
|
+
* TypeError or RangeError is a programming error in the importer or the sink, never a defect of
|
|
3074
|
+
* the input, so it must surface as an exception rather than as an issue blamed on the file).
|
|
3075
|
+
* @param err - the thrown value
|
|
3076
|
+
* @param where - the line and element, when known
|
|
3077
|
+
* @returns the recorded issue
|
|
3078
|
+
*/
|
|
3079
|
+
recordError(err, where) {
|
|
3080
|
+
if (err instanceof GraphFormatError && !(err instanceof ImportError)) {
|
|
3081
|
+
const category = CATEGORY_BY_CODE[err.code] ?? "validation-error";
|
|
3082
|
+
if (err.code === "E_TOO_LARGE") {
|
|
3083
|
+
return this.failWith(category, err.code, err.message, where);
|
|
3084
|
+
}
|
|
3085
|
+
return this.error(category, err.code, err.message, where);
|
|
3086
|
+
}
|
|
3087
|
+
throw err;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* A builder that starts from everything this one recorded so far: issues, counts, loss notes,
|
|
3091
|
+
* the warnOnce keys, the error and warning counts and the truncation flag. For an importer that
|
|
3092
|
+
* reads its input once and emits several graphs, each with its own report.
|
|
3093
|
+
* @returns the copy; later records to either builder do not reach the other
|
|
3094
|
+
*/
|
|
3095
|
+
fork() {
|
|
3096
|
+
const copy = new ImportReportBuilder(this.format, this.errorLimit);
|
|
3097
|
+
Object.assign(copy.counts, this.counts);
|
|
3098
|
+
copy.issueList.push(...this.issueList);
|
|
3099
|
+
copy.lossList.push(...this.lossList);
|
|
3100
|
+
for (const key of this.onceCodes) {
|
|
3101
|
+
copy.onceCodes.add(key);
|
|
3102
|
+
}
|
|
3103
|
+
for (const [code, count] of this.warningsByCode) {
|
|
3104
|
+
copy.warningsByCode.set(code, count);
|
|
3105
|
+
}
|
|
3106
|
+
copy.errors = this.errors;
|
|
3107
|
+
copy.warnings = this.warnings;
|
|
3108
|
+
copy.truncatedFlag = this.truncatedFlag;
|
|
3109
|
+
return copy;
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Build the ImportError that aborts the import, carrying the report so far. The caller throws
|
|
3113
|
+
* it; this method only constructs it so it can be used in expression position.
|
|
3114
|
+
* @param message - a plain-ASCII message
|
|
3115
|
+
* @param details - optional machine-readable context
|
|
3116
|
+
* @returns the error to throw
|
|
3117
|
+
*/
|
|
3118
|
+
abort(message, details) {
|
|
3119
|
+
return new ImportError(message, this.finish(), details);
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Record a fatal parse error and abort: the issue is recorded (even beyond the error limit)
|
|
3123
|
+
* and an ImportError carrying the report is thrown.
|
|
3124
|
+
* @param code - a stable code such as "E_INVALID_UTF8"
|
|
3125
|
+
* @param message - a plain-ASCII message
|
|
3126
|
+
* @param where - the line and element, when known
|
|
3127
|
+
* @param details - optional machine-readable context for the ImportError
|
|
3128
|
+
*/
|
|
3129
|
+
fail(code, message, where, details) {
|
|
3130
|
+
this.failWith("parse-error", code, message, where, details);
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Record a fatal error of any category and abort, as fail() does for a parse error: a size
|
|
3134
|
+
* limit (E_TOO_LARGE, category "unsupported") stops an import without being a defect of the
|
|
3135
|
+
* syntax. A fatal error is not the error limit: `truncated` stays as it was.
|
|
3136
|
+
* @param category - the issue category
|
|
3137
|
+
* @param code - a stable code
|
|
3138
|
+
* @param message - a plain-ASCII message
|
|
3139
|
+
* @param where - the line and element, when known
|
|
3140
|
+
* @param details - optional machine-readable context for the ImportError
|
|
3141
|
+
*/
|
|
3142
|
+
failWith(category, code, message, where, details) {
|
|
3143
|
+
this.issueList.push(makeIssue(category, "error", code, message, where));
|
|
3144
|
+
this.errors++;
|
|
3145
|
+
throw this.abort(withLine(message, where), { code, ...details });
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* Add a finished report to this one: its issues after the ones recorded so far, its counts, its
|
|
3149
|
+
* loss notes and its truncated flag. Use it when an importer hands part of the work to another
|
|
3150
|
+
* importer (for example, a wrapper around the CSV importer) and returns one report for both. The issues
|
|
3151
|
+
* keep their codes; errors added this way do not count toward this report's error limit, since
|
|
3152
|
+
* the other importer applied its own.
|
|
3153
|
+
* @param report - the report the other importer returned
|
|
3154
|
+
* @returns this builder
|
|
3155
|
+
* @example
|
|
3156
|
+
* ```ts
|
|
3157
|
+
* const report = new ImportReportBuilder("netscope", opts.errorLimit);
|
|
3158
|
+
* const text = await readText(input, report, opts);
|
|
3159
|
+
* report.include(await csvImporter.import(text, sink, options));
|
|
3160
|
+
* return report.finish();
|
|
3161
|
+
* ```
|
|
3162
|
+
*/
|
|
3163
|
+
include(report) {
|
|
3164
|
+
for (const issue of report.issues) {
|
|
3165
|
+
this.issueList.push(issue);
|
|
3166
|
+
if (issue.severity === "error") {
|
|
3167
|
+
this.errors++;
|
|
3168
|
+
} else {
|
|
3169
|
+
this.warnings++;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
const { counts } = report;
|
|
3173
|
+
this.counts.nodes += counts.nodes;
|
|
3174
|
+
this.counts.edges += counts.edges;
|
|
3175
|
+
this.counts.skippedNodes += counts.skippedNodes;
|
|
3176
|
+
this.counts.skippedEdges += counts.skippedEdges;
|
|
3177
|
+
this.counts.expandedMixed += counts.expandedMixed;
|
|
3178
|
+
this.lossList.push(...report.lossy);
|
|
3179
|
+
this.truncatedFlag ||= report.truncated;
|
|
3180
|
+
return this;
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* The report as it stands: a frozen snapshot with the parse duration so far. Can be called more
|
|
3184
|
+
* than once; each call reflects everything recorded up to that point.
|
|
3185
|
+
* @returns the report
|
|
3186
|
+
*/
|
|
3187
|
+
finish() {
|
|
3188
|
+
const { nodes, edges, skippedNodes, skippedEdges, expandedMixed } = this.counts;
|
|
3189
|
+
const suppressed = [];
|
|
3190
|
+
for (const [code, seen] of this.warningsByCode) {
|
|
3191
|
+
if (seen > MAX_WARNINGS_PER_CODE) {
|
|
3192
|
+
const more = seen - MAX_WARNINGS_PER_CODE;
|
|
3193
|
+
suppressed.push(
|
|
3194
|
+
makeIssue(
|
|
3195
|
+
"unsupported",
|
|
3196
|
+
"warning",
|
|
3197
|
+
ISSUES_SUPPRESSED_CODE,
|
|
3198
|
+
`${more} more ${code} warning${plural(more)} ${agree(more, "was", "were")} not kept (the report keeps the first ${MAX_WARNINGS_PER_CODE})`,
|
|
3199
|
+
{ element: code }
|
|
3200
|
+
)
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return Object.freeze({
|
|
3205
|
+
format: this.format,
|
|
3206
|
+
counts: Object.freeze({ nodes, edges, skippedNodes, skippedEdges, expandedMixed }),
|
|
3207
|
+
issues: Object.freeze([...this.issueList, ...suppressed]),
|
|
3208
|
+
errorCount: this.errors,
|
|
3209
|
+
warningCount: this.warnings + suppressed.length,
|
|
3210
|
+
truncated: this.truncatedFlag,
|
|
3211
|
+
lossy: Object.freeze([...this.lossList]),
|
|
3212
|
+
durationMs: now() - this.startedAt
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
function makeIssue(category, severity, code, message, where) {
|
|
3217
|
+
return Object.freeze({
|
|
3218
|
+
category,
|
|
3219
|
+
severity,
|
|
3220
|
+
code,
|
|
3221
|
+
message,
|
|
3222
|
+
line: where?.line ?? null,
|
|
3223
|
+
element: where?.element ?? null
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
function isAbortError(err) {
|
|
3227
|
+
if (typeof err !== "object" || err === null) {
|
|
3228
|
+
return false;
|
|
3229
|
+
}
|
|
3230
|
+
const { name } = err;
|
|
3231
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
3232
|
+
}
|
|
3233
|
+
function messageOf(err) {
|
|
3234
|
+
if (err instanceof Error) {
|
|
3235
|
+
return err.message;
|
|
3236
|
+
}
|
|
3237
|
+
if (typeof err === "string") {
|
|
3238
|
+
return err;
|
|
3239
|
+
}
|
|
3240
|
+
return `non-error thrown (${typeof err})`;
|
|
3241
|
+
}
|
|
3242
|
+
function now() {
|
|
3243
|
+
return typeof performance === "object" ? performance.now() : Date.now();
|
|
3244
|
+
}
|
|
3245
|
+
function formatF32(value) {
|
|
3246
|
+
if (!Number.isFinite(value)) {
|
|
3247
|
+
return String(value);
|
|
3248
|
+
}
|
|
3249
|
+
if (value === 0) {
|
|
3250
|
+
return Object.is(value, -0) ? "-0" : "0";
|
|
3251
|
+
}
|
|
3252
|
+
for (let digits = 1; digits <= 9; digits++) {
|
|
3253
|
+
const text = value.toPrecision(digits);
|
|
3254
|
+
if (Math.fround(Number(text)) === value) {
|
|
3255
|
+
return String(Number(text));
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
return String(value);
|
|
3259
|
+
}
|
|
3260
|
+
function survivesF32(value) {
|
|
3261
|
+
return Number(formatF32(Math.fround(value))) === value;
|
|
3262
|
+
}
|
|
3263
|
+
function formatF64(value) {
|
|
3264
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
3265
|
+
}
|
|
3266
|
+
function idText(id) {
|
|
3267
|
+
return typeof id === "number" ? formatF64(id) : id;
|
|
3268
|
+
}
|
|
3269
|
+
function formatDecimal(value, dtype = "f64") {
|
|
3270
|
+
if (!Number.isFinite(value)) {
|
|
3271
|
+
return String(value);
|
|
3272
|
+
}
|
|
3273
|
+
if (Object.is(value, -0)) {
|
|
3274
|
+
return "-0.0";
|
|
3275
|
+
}
|
|
3276
|
+
const text = formatNumber(value, dtype);
|
|
3277
|
+
return text.includes(".") || text.includes("e") ? text : `${text}.0`;
|
|
3278
|
+
}
|
|
3279
|
+
function formatGmlReal(value, dtype = "f64") {
|
|
3280
|
+
if (Number.isNaN(value)) {
|
|
3281
|
+
return "NAN";
|
|
3282
|
+
}
|
|
3283
|
+
if (!Number.isFinite(value)) {
|
|
3284
|
+
return value > 0 ? "+INF" : "-INF";
|
|
3285
|
+
}
|
|
3286
|
+
const text = formatDecimal(value, dtype);
|
|
3287
|
+
if (text.includes(".")) {
|
|
3288
|
+
return text;
|
|
3289
|
+
}
|
|
3290
|
+
const e = text.indexOf("e");
|
|
3291
|
+
return `${text.slice(0, e)}.0${text.slice(e)}`;
|
|
3292
|
+
}
|
|
3293
|
+
function formatInteger(value) {
|
|
3294
|
+
if (Number.isSafeInteger(value) || !Number.isFinite(value)) {
|
|
3295
|
+
return String(value);
|
|
3296
|
+
}
|
|
3297
|
+
return BigInt(value).toString();
|
|
3298
|
+
}
|
|
3299
|
+
function formatNumber(value, dtype) {
|
|
3300
|
+
return dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
3301
|
+
}
|
|
3302
|
+
const DEFAULT_WEIGHT = 1;
|
|
3303
|
+
function isWeightField(name, weightFrom) {
|
|
3304
|
+
return weightFrom !== null && name === weightFrom;
|
|
3305
|
+
}
|
|
3306
|
+
function parseWeightText(text, report) {
|
|
3307
|
+
const trimmed = text.trim();
|
|
3308
|
+
if (trimmed.length === 0) {
|
|
3309
|
+
return void 0;
|
|
3310
|
+
}
|
|
3311
|
+
let value;
|
|
3312
|
+
try {
|
|
3313
|
+
value = parseDecimalText(trimmed);
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
throw invalidWeight(text, err);
|
|
3316
|
+
}
|
|
3317
|
+
if (Number.isNaN(value)) {
|
|
3318
|
+
throw invalidWeight(text, null);
|
|
3319
|
+
}
|
|
3320
|
+
if (report !== void 0) {
|
|
3321
|
+
reportWeightPrecision(trimmed, value, report);
|
|
3322
|
+
}
|
|
3323
|
+
return value;
|
|
3324
|
+
}
|
|
3325
|
+
function reportWeightPrecision(text, value, report) {
|
|
3326
|
+
if (!Number.isSafeInteger(value) && Number.isFinite(value) && /^[+-]?\d+$/.test(text) && BigInt(text) !== BigInt(value)) {
|
|
3327
|
+
report.warnOnce(
|
|
3328
|
+
"precision",
|
|
3329
|
+
PRECISION_CODE,
|
|
3330
|
+
`edge weights: integers beyond 2^53 are stored as the nearest double (first: ${text})`,
|
|
3331
|
+
void 0,
|
|
3332
|
+
`${PRECISION_CODE}:edge weight`
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
function weightFromValue(value) {
|
|
3337
|
+
if (value === void 0 || value === null) {
|
|
3338
|
+
return void 0;
|
|
3339
|
+
}
|
|
3340
|
+
if (typeof value === "number") {
|
|
3341
|
+
if (Number.isNaN(value)) {
|
|
3342
|
+
throw invalidWeight(value, null);
|
|
3343
|
+
}
|
|
3344
|
+
return value;
|
|
3345
|
+
}
|
|
3346
|
+
if (typeof value === "string") {
|
|
3347
|
+
return parseWeightText(value);
|
|
3348
|
+
}
|
|
3349
|
+
throw invalidWeight(value, null);
|
|
3350
|
+
}
|
|
3351
|
+
function invalidWeight(value, cause) {
|
|
3352
|
+
let shown;
|
|
3353
|
+
switch (typeof value) {
|
|
3354
|
+
case "string":
|
|
3355
|
+
shown = JSON.stringify(value);
|
|
3356
|
+
break;
|
|
3357
|
+
case "number":
|
|
3358
|
+
shown = String(value);
|
|
3359
|
+
break;
|
|
3360
|
+
default:
|
|
3361
|
+
shown = typeof value;
|
|
3362
|
+
}
|
|
3363
|
+
return new GraphFormatError("E_INVALID_WEIGHT", `invalid edge weight ${shown}`, {
|
|
3364
|
+
value: typeof value === "object" ? typeof value : value,
|
|
3365
|
+
cause: cause instanceof Error ? cause.message : null
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
const NEGATIVE_ZERO_TEXT = "-0";
|
|
3369
|
+
const NUMERIC_DTYPES = /* @__PURE__ */ new Set(["f32", "f64", "i32", "u32", "u8"]);
|
|
3370
|
+
function weightFormatter(dtype) {
|
|
3371
|
+
switch (dtype) {
|
|
3372
|
+
case "f32":
|
|
3373
|
+
return formatF32;
|
|
3374
|
+
case "f64":
|
|
3375
|
+
return formatF64;
|
|
3376
|
+
default:
|
|
3377
|
+
return formatInteger;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function explicitWeights(snapshot) {
|
|
3381
|
+
const shadowColumn = snapshot.edges.byRole("weight");
|
|
3382
|
+
const shadow = shadowColumn !== null && NUMERIC_DTYPES.has(shadowColumn.dtype) ? shadowColumn : null;
|
|
3383
|
+
if (shadow !== null) {
|
|
3384
|
+
const dtype = shadow.dtype === "f32" ? "f32" : "f64";
|
|
3385
|
+
const format = weightFormatter(shadow.dtype);
|
|
3386
|
+
return {
|
|
3387
|
+
weighted: true,
|
|
3388
|
+
dtype,
|
|
3389
|
+
isExplicit: (e) => shadow.isSet(e),
|
|
3390
|
+
value: (e) => shadow.value(e),
|
|
3391
|
+
text: (e, integral = false) => {
|
|
3392
|
+
if (!shadow.isSet(e)) {
|
|
3393
|
+
return null;
|
|
3394
|
+
}
|
|
3395
|
+
const value = shadow.value(e);
|
|
3396
|
+
if (Object.is(value, -0)) {
|
|
3397
|
+
return NEGATIVE_ZERO_TEXT;
|
|
3398
|
+
}
|
|
3399
|
+
return integral && Number.isInteger(value) ? formatInteger(value) : format(value);
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
const { weights } = snapshot.edgeList();
|
|
3404
|
+
if (weights === null || !snapshot.flags.weighted) {
|
|
3405
|
+
return {
|
|
3406
|
+
weighted: false,
|
|
3407
|
+
dtype: "f32",
|
|
3408
|
+
isExplicit: () => false,
|
|
3409
|
+
value: () => DEFAULT_WEIGHT,
|
|
3410
|
+
text: () => null
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
weighted: true,
|
|
3415
|
+
dtype: "f32",
|
|
3416
|
+
isExplicit: () => true,
|
|
3417
|
+
value: (e) => weights[e],
|
|
3418
|
+
text: (e, integral = false) => {
|
|
3419
|
+
const value = weights[e];
|
|
3420
|
+
if (Object.is(value, -0)) {
|
|
3421
|
+
return NEGATIVE_ZERO_TEXT;
|
|
3422
|
+
}
|
|
3423
|
+
return integral && Number.isInteger(value) ? formatInteger(value) : formatF32(value);
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
function edgeWeights(snapshot) {
|
|
3428
|
+
const { weights } = snapshot.edgeList();
|
|
3429
|
+
if (weights === null) {
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
const out = Float64Array.from(weights);
|
|
3433
|
+
const exact = snapshot.edges.byRole("weight");
|
|
3434
|
+
if (exact !== null) {
|
|
3435
|
+
for (let e = 0; e < out.length; e++) {
|
|
3436
|
+
const v = exact.value(e);
|
|
3437
|
+
if (typeof v === "number") {
|
|
3438
|
+
out[e] = v;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return out;
|
|
3443
|
+
}
|
|
3444
|
+
const ID_CHARSET_REFUSAL = "E_ID_CHARSET";
|
|
3445
|
+
const LOSS = Object.freeze({
|
|
3446
|
+
/**
|
|
3447
|
+
* A node or edge attribute is not written: the format has no attributes, or none of this kind
|
|
3448
|
+
* (`CheckExtras.attributes` false).
|
|
3449
|
+
*/
|
|
3450
|
+
COLUMN_DROPPED: COLUMN_DROPPED_CODE,
|
|
3451
|
+
/**
|
|
3452
|
+
* The graph has both directed and undirected edges, the format holds one direction per file, and
|
|
3453
|
+
* `onMixedDirection` is "directed" or "undirected": every edge is written with that direction.
|
|
3454
|
+
*/
|
|
3455
|
+
MIXED_DIRECTION: "W_MIXED_DIRECTION",
|
|
3456
|
+
/**
|
|
3457
|
+
* The graph has both directed and undirected edges and `onMixedDirection` is "error". An import stops; a save to a
|
|
3458
|
+
* format that holds one direction per file fails with E_DIRECTED. Pass "directed" or "undirected" to read or write
|
|
3459
|
+
* it anyway.
|
|
3460
|
+
*/
|
|
3461
|
+
MIXED_DIRECTION_ERROR: MIXED_DIRECTION_CODE,
|
|
3462
|
+
/** Parallel edges in a format without them. */
|
|
3463
|
+
MULTI_EDGES: "W_MULTI_EDGES",
|
|
3464
|
+
/** Self-loops in a format without them. */
|
|
3465
|
+
SELF_LOOPS: "W_SELF_LOOPS",
|
|
3466
|
+
/**
|
|
3467
|
+
* The format needs an id on every edge and the graph has none, so the file gets ids e0, e1, e2 and so on, which
|
|
3468
|
+
* read back as edge ids.
|
|
3469
|
+
*/
|
|
3470
|
+
EDGE_IDS_GENERATED: "W_EDGE_IDS_GENERATED",
|
|
3471
|
+
/** The graph has edge ids and the format has no place for them, so they are not written. */
|
|
3472
|
+
EDGE_IDS_DROPPED: "W_EDGE_IDS_DROPPED",
|
|
3473
|
+
/** Ids outside the format's charset are rewritten (sanitizeIds "mangle"). */
|
|
3474
|
+
ID_MANGLED: "W_ID_MANGLED",
|
|
3475
|
+
/**
|
|
3476
|
+
* Node ids the format cannot write, under `sanitizeIds: "error"`; the save fails with E_INVALID_ID. Pass
|
|
3477
|
+
* `sanitizeIds: "mangle"` to rewrite them.
|
|
3478
|
+
*/
|
|
3479
|
+
ID_CHARSET: ID_CHARSET_REFUSAL,
|
|
3480
|
+
/** The format numbers nodes 1..N; ids that are not their 1-based index are kept as labels only. */
|
|
3481
|
+
ID_RENUMBERED: "W_ID_RENUMBERED",
|
|
3482
|
+
/**
|
|
3483
|
+
* An attribute's type is one the format cannot store as it is (for example, a 32-bit float in a format that has only 64-bit
|
|
3484
|
+
* numbers), so it reads back with the nearest type the format has.
|
|
3485
|
+
*/
|
|
3486
|
+
DTYPE: "W_DTYPE_UNSUPPORTED",
|
|
3487
|
+
/**
|
|
3488
|
+
* An attribute with several numbers per element (other than a position) is written as separate values or a list,
|
|
3489
|
+
* and does not read back as one attribute.
|
|
3490
|
+
*/
|
|
3491
|
+
COMPONENTS: "W_COMPONENTS_FLATTENED",
|
|
3492
|
+
/** A list column in a format without lists. */
|
|
3493
|
+
LIST: "W_LIST_UNSUPPORTED",
|
|
3494
|
+
/** A json column in a format without nested values. */
|
|
3495
|
+
JSON: "W_JSON_UNSUPPORTED",
|
|
3496
|
+
/** A declared default in a format without defaults. */
|
|
3497
|
+
DEFAULT: "W_DEFAULT_DROPPED",
|
|
3498
|
+
/** Declared options in a format without enumerations. */
|
|
3499
|
+
OPTIONS: "W_OPTIONS_DROPPED",
|
|
3500
|
+
/** A parent / parents column in a format without containment. */
|
|
3501
|
+
HIERARCHY: "W_HIERARCHY_DROPPED",
|
|
3502
|
+
/** A start / end / timestamp column in a format without temporal support. */
|
|
3503
|
+
TEMPORAL: TEMPORAL_DROPPED_CODE,
|
|
3504
|
+
/** A `<column>.text` companion the format cannot carry. */
|
|
3505
|
+
TEMPORAL_TEXT: TEMPORAL_TEXT_DROPPED_CODE,
|
|
3506
|
+
/** An attribute with a role the format has no place for is written as a plain attribute; the role is lost. */
|
|
3507
|
+
ROLE: ROLE_DROPPED_CODE,
|
|
3508
|
+
/** A parents (multi-parent) column in a format with single containment only. */
|
|
3509
|
+
PARENTS: PARENTS_DROPPED_CODE,
|
|
3510
|
+
/** A mutual pair written as two directed edges without the mutual mark. */
|
|
3511
|
+
MUTUAL_EXPANDED: MUTUAL_EXPANDED_CODE,
|
|
3512
|
+
/** A mutual pair written as one undirected edge; the mark is lost and the pair reads back undirected. */
|
|
3513
|
+
MUTUAL_AS_UNDIRECTED: MUTUAL_AS_UNDIRECTED_CODE,
|
|
3514
|
+
/** Node ids whose written text reads back as the other type under the importer's id rule. */
|
|
3515
|
+
ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
|
|
3516
|
+
/**
|
|
3517
|
+
* Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
|
|
3518
|
+
* E_INVALID_ID.
|
|
3519
|
+
*/
|
|
3520
|
+
ID_TEXT_COLLISION: ID_TEXT_COLLISION_CODE,
|
|
3521
|
+
/**
|
|
3522
|
+
* An attribute without the weight role is named like the attribute graph-io reads weights from (for example, `weight`), so
|
|
3523
|
+
* it reads back as the edge weight.
|
|
3524
|
+
*/
|
|
3525
|
+
WEIGHT_KEY_CLASH: WEIGHT_KEY_CLASH_CODE,
|
|
3526
|
+
/**
|
|
3527
|
+
* An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
|
|
3528
|
+
* reads back with that role.
|
|
3529
|
+
*/
|
|
3530
|
+
ROLE_ASSUMED: ROLE_ASSUMED_CODE,
|
|
3531
|
+
/**
|
|
3532
|
+
* An attribute with a role (for example, the label) is written where the format keeps that role, and reads back under the
|
|
3533
|
+
* name the format's importer gives it.
|
|
3534
|
+
*/
|
|
3535
|
+
COLUMN_NAME_CHANGED: COLUMN_RENAMED_LOSS_CODE,
|
|
3536
|
+
/** A declared column whose every row is unset vanishes through a format without declarations. */
|
|
3537
|
+
EMPTY_COLUMN: EMPTY_COLUMN_DROPPED_CODE,
|
|
3538
|
+
/**
|
|
3539
|
+
* A text attribute reads back as a dictionary attribute, or the reverse, because the importer chooses by how often
|
|
3540
|
+
* its values repeat. The values are the same.
|
|
3541
|
+
*/
|
|
3542
|
+
STORAGE_CLASS: STORAGE_CLASS_CODE,
|
|
3543
|
+
/**
|
|
3544
|
+
* A number attribute whose values are all whole numbers reads back as integers, because the format does not record
|
|
3545
|
+
* the type.
|
|
3546
|
+
*/
|
|
3547
|
+
INTEGRAL_F64: INTEGRAL_F64_CODE,
|
|
3548
|
+
/**
|
|
3549
|
+
* A text value that reads back as a number or a boolean, because the format does not record that it was text (the
|
|
3550
|
+
* text "42" reads back as the number 42).
|
|
3551
|
+
*/
|
|
3552
|
+
TEXT_INFERRED: TEXT_INFERRED_CODE,
|
|
3553
|
+
/** A text value holds a character XML 1.0 forbids (most control characters); the save fails with E_COLUMN_TYPE. */
|
|
3554
|
+
XML_ILLEGAL_CHAR: XML_ILLEGAL_CHAR_CODE,
|
|
3555
|
+
/**
|
|
3556
|
+
* A text attribute stored as a dictionary, without a declared list of allowed values, reads back with its distinct
|
|
3557
|
+
* values as that list.
|
|
3558
|
+
*/
|
|
3559
|
+
OPTIONS_GAINED: OPTIONS_GAINED_CODE,
|
|
3560
|
+
/** A spells column in a format without spells. */
|
|
3561
|
+
SPELLS: "W_SPELLS_DROPPED",
|
|
3562
|
+
/** Dynamic attribute values (extension tables) in a format without them. */
|
|
3563
|
+
DYNAMIC_VALUES: "W_DYNAMIC_VALUES_DROPPED",
|
|
3564
|
+
/** An open-interval column in a format without open intervals. */
|
|
3565
|
+
OPEN_INTERVAL: "W_OPEN_INTERVAL",
|
|
3566
|
+
/** Graph-level attributes in a format without them. */
|
|
3567
|
+
GRAPH_ATTRIBUTES: "W_GRAPH_ATTRIBUTES_DROPPED",
|
|
3568
|
+
/** A position column in a format without positions. */
|
|
3569
|
+
POSITIONS: "W_POSITIONS_DROPPED",
|
|
3570
|
+
/** Visual columns in a format without them. */
|
|
3571
|
+
VIZ: "W_VIZ_DROPPED",
|
|
3572
|
+
/** An extension table the format cannot carry. */
|
|
3573
|
+
EXTENSION_TABLE: "W_EXTENSION_TABLE_DROPPED",
|
|
3574
|
+
/** A NaN or infinite number the format cannot spell; not written (or written as null), it reads back unset. */
|
|
3575
|
+
NONFINITE_AS_NULL: NONFINITE_AS_NULL_CODE,
|
|
3576
|
+
/**
|
|
3577
|
+
* The graph has edge weights and the file does not keep them (the format has no weights, or an option turned them
|
|
3578
|
+
* off): every edge reads back with the default weight 1.
|
|
3579
|
+
*/
|
|
3580
|
+
WEIGHTS_DROPPED: WEIGHTS_DROPPED_CODE
|
|
3581
|
+
});
|
|
3582
|
+
function refusedSave(notes, codes = {}) {
|
|
3583
|
+
const note = notes.find((n) => n.code.startsWith("E_"));
|
|
3584
|
+
if (note === void 0) {
|
|
3585
|
+
return null;
|
|
3586
|
+
}
|
|
3587
|
+
const code = codes[note.code] ?? REFUSAL_CODES[note.code] ?? "E_UNSUPPORTED";
|
|
3588
|
+
return new GraphFormatError(code, note.message, { code: note.code, column: note.column, count: note.count });
|
|
3589
|
+
}
|
|
3590
|
+
const REFUSAL_CODES = {
|
|
3591
|
+
[ID_CHARSET_REFUSAL]: "E_INVALID_ID",
|
|
3592
|
+
[ID_TEXT_COLLISION_CODE]: "E_INVALID_ID",
|
|
3593
|
+
[MIXED_DIRECTION_CODE]: "E_DIRECTED",
|
|
3594
|
+
[XML_ILLEGAL_CHAR_CODE]: "E_COLUMN_TYPE"
|
|
3595
|
+
};
|
|
3596
|
+
const NO_CAPABILITIES = Object.freeze({
|
|
3597
|
+
mixedDirection: false,
|
|
3598
|
+
multiEdges: false,
|
|
3599
|
+
selfLoops: false,
|
|
3600
|
+
edgeIds: "none",
|
|
3601
|
+
idCharset: "any",
|
|
3602
|
+
dtypes: Object.freeze([]),
|
|
3603
|
+
components: false,
|
|
3604
|
+
lists: false,
|
|
3605
|
+
json: false,
|
|
3606
|
+
defaults: false,
|
|
3607
|
+
options: false,
|
|
3608
|
+
hierarchy: false,
|
|
3609
|
+
temporal: "none",
|
|
3610
|
+
graphAttributes: false,
|
|
3611
|
+
positions: false,
|
|
3612
|
+
viz: false
|
|
3613
|
+
});
|
|
3614
|
+
function capabilities(supported) {
|
|
3615
|
+
return Object.freeze({ ...NO_CAPABILITIES, ...supported, dtypes: Object.freeze([...supported.dtypes ?? []]) });
|
|
3616
|
+
}
|
|
3617
|
+
const STRUCTURAL_ROLES = /* @__PURE__ */ new Set(["directed", "pair", "mutual", "weight", "originalId"]);
|
|
3618
|
+
const VIZ_ROLES = /* @__PURE__ */ new Set(["color", "size", "shape", "thickness"]);
|
|
3619
|
+
const TEMPORAL_ROLES = /* @__PURE__ */ new Set(["start", "end", "timestamp", "timestamps"]);
|
|
3620
|
+
const HIERARCHY_ROLES = /* @__PURE__ */ new Set(["parent", "parents"]);
|
|
3621
|
+
function checkCapabilities(snapshot, caps, options, extras = {}) {
|
|
3622
|
+
const notes = [];
|
|
3623
|
+
const note = (code, message, column = null, count = null) => {
|
|
3624
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
3625
|
+
};
|
|
3626
|
+
const mixed = countMixedEdges(snapshot);
|
|
3627
|
+
if (mixed > 0 && !caps.mixedDirection) {
|
|
3628
|
+
if (options.onMixedDirection === "error") {
|
|
3629
|
+
note(
|
|
3630
|
+
LOSS.MIXED_DIRECTION_ERROR,
|
|
3631
|
+
`${mixed} undirected edge${plural(mixed)} in a directed graph, and the format holds one direction per file, so the save fails unless onMixedDirection is "directed" or "undirected"`,
|
|
3632
|
+
null,
|
|
3633
|
+
mixed
|
|
3634
|
+
);
|
|
3635
|
+
} else {
|
|
3636
|
+
note(
|
|
3637
|
+
LOSS.MIXED_DIRECTION,
|
|
3638
|
+
`${mixed} undirected edge${plural(mixed)} in a directed graph ${agree(mixed, "is", "are")} written as ${options.onMixedDirection} (onMixedDirection), and the format holds one direction per file`,
|
|
3639
|
+
null,
|
|
3640
|
+
mixed
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
if (snapshot.flags.multigraph && !caps.multiEdges) {
|
|
3645
|
+
const parallel = countParallelArcs(snapshot);
|
|
3646
|
+
note(
|
|
3647
|
+
LOSS.MULTI_EDGES,
|
|
3648
|
+
`${parallel} parallel edge${plural(parallel)}; the format has no parallel edges`,
|
|
3649
|
+
null,
|
|
3650
|
+
parallel
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
if (snapshot.selfLoopCount > 0 && !caps.selfLoops) {
|
|
3654
|
+
note(
|
|
3655
|
+
LOSS.SELF_LOOPS,
|
|
3656
|
+
`${snapshot.selfLoopCount} self-loop${plural(snapshot.selfLoopCount)}; the format has no self-loops`,
|
|
3657
|
+
null,
|
|
3658
|
+
snapshot.selfLoopCount
|
|
3659
|
+
);
|
|
3660
|
+
}
|
|
3661
|
+
const edgeIds = snapshot.edges.byRole("id");
|
|
3662
|
+
if (caps.edgeIds === "required" && edgeIds === null && snapshot.edgeCount > 0) {
|
|
3663
|
+
note(
|
|
3664
|
+
LOSS.EDGE_IDS_GENERATED,
|
|
3665
|
+
"the format requires edge ids, and the graph has none; the edges are written with generated ids, which read back as a new edge id column",
|
|
3666
|
+
null,
|
|
3667
|
+
snapshot.edgeCount
|
|
3668
|
+
);
|
|
3669
|
+
} else if (caps.edgeIds === "none" && edgeIds !== null) {
|
|
3670
|
+
note(
|
|
3671
|
+
LOSS.EDGE_IDS_DROPPED,
|
|
3672
|
+
`edge id column "${edgeIds.meta.name}" cannot be written`,
|
|
3673
|
+
edgeIds.meta.name,
|
|
3674
|
+
snapshot.edgeCount
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
const unrepresentable = countUnrepresentableIds(snapshot, caps.idCharset);
|
|
3678
|
+
if (unrepresentable > 0) {
|
|
3679
|
+
if (caps.idCharset === "dense-1-based") {
|
|
3680
|
+
note(
|
|
3681
|
+
LOSS.ID_RENUMBERED,
|
|
3682
|
+
options.sanitizeIds === "mangle" ? `${unrepresentable} node id${plural(unrepresentable)} ${agree(unrepresentable, "is", "are")} not their 1-based index; nodes are numbered 1..N, the original ids are written too, and an import with restoreMangledIds: true reads them back as the ids; they are also the labels of nodes without a label value` : `${unrepresentable} node id${plural(unrepresentable)} ${agree(unrepresentable, "is", "are")} not their 1-based index; nodes are numbered 1..N and ids kept as labels of the nodes without a label value (a node with one loses its id)`,
|
|
3683
|
+
null,
|
|
3684
|
+
unrepresentable
|
|
3685
|
+
);
|
|
3686
|
+
} else if (options.sanitizeIds === "mangle") {
|
|
3687
|
+
note(
|
|
3688
|
+
LOSS.ID_MANGLED,
|
|
3689
|
+
`${unrepresentable} node id${plural(unrepresentable)} ${charsetText(caps.idCharset, unrepresentable)}, so they are rewritten; the original ids are written too, and an import with restoreMangledIds: true reads them back`,
|
|
3690
|
+
null,
|
|
3691
|
+
unrepresentable
|
|
3692
|
+
);
|
|
3693
|
+
} else {
|
|
3694
|
+
note(
|
|
3695
|
+
LOSS.ID_CHARSET,
|
|
3696
|
+
`${unrepresentable} node id${plural(unrepresentable)} ${charsetText(caps.idCharset, unrepresentable)}, so the save fails unless sanitizeIds is "mangle"`,
|
|
3697
|
+
null,
|
|
3698
|
+
unrepresentable
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
if (extras.idsReadBack !== void 0) {
|
|
3703
|
+
let retyped = 0;
|
|
3704
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
3705
|
+
const id = snapshot.ids.idOf(i);
|
|
3706
|
+
if (coerceIdText(String(id), extras.idsReadBack) !== id) {
|
|
3707
|
+
retyped++;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
if (retyped > 0) {
|
|
3711
|
+
note(
|
|
3712
|
+
LOSS.ID_TEXT_TYPE,
|
|
3713
|
+
`${retyped} node id${plural(retyped)} ${agree(retyped, "reads", "read")} back with another type (a number as text, or text as a number)`,
|
|
3714
|
+
null,
|
|
3715
|
+
retyped
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
checkWeights(snapshot, extras, note);
|
|
3720
|
+
checkColumns(snapshot.nodes, "node", caps, extras, note);
|
|
3721
|
+
checkColumns(snapshot.edges, "edge", caps, extras, note);
|
|
3722
|
+
const graphColumns = snapshot.graph.names();
|
|
3723
|
+
if (graphColumns.length > 0) {
|
|
3724
|
+
if (caps.graphAttributes) {
|
|
3725
|
+
checkColumns(snapshot.graph, "graph", caps, extras, note);
|
|
3726
|
+
} else {
|
|
3727
|
+
note(
|
|
3728
|
+
LOSS.GRAPH_ATTRIBUTES,
|
|
3729
|
+
`${graphColumns.length} graph attribute${plural(graphColumns.length)} cannot be written`,
|
|
3730
|
+
null,
|
|
3731
|
+
graphColumns.length
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
for (const [name, table] of snapshot.extensions) {
|
|
3736
|
+
if (name.startsWith("temporal:")) {
|
|
3737
|
+
if (caps.temporal !== "dynamic-values") {
|
|
3738
|
+
note(
|
|
3739
|
+
LOSS.DYNAMIC_VALUES,
|
|
3740
|
+
`dynamic values of "${name}" (${table.rowCount} row${plural(table.rowCount)}) cannot be written`,
|
|
3741
|
+
name,
|
|
3742
|
+
table.rowCount
|
|
3743
|
+
);
|
|
3744
|
+
}
|
|
3745
|
+
} else {
|
|
3746
|
+
note(
|
|
3747
|
+
LOSS.EXTENSION_TABLE,
|
|
3748
|
+
`extension table "${name}" (${table.rowCount} row${plural(table.rowCount)}) cannot be written`,
|
|
3749
|
+
name,
|
|
3750
|
+
table.rowCount
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
return notes;
|
|
3755
|
+
}
|
|
3756
|
+
function charsetText(charset, n) {
|
|
3757
|
+
const are = agree(n, "is", "are");
|
|
3758
|
+
switch (charset) {
|
|
3759
|
+
case "nmtoken":
|
|
3760
|
+
return `${are} not XML name tokens (letters, digits and . - _ : only; no spaces)`;
|
|
3761
|
+
case "integer":
|
|
3762
|
+
return `${are} not integers`;
|
|
3763
|
+
default:
|
|
3764
|
+
return `cannot be written as ${charset} ids`;
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
function writesAttributes(domain, extras) {
|
|
3768
|
+
if (domain === "node") {
|
|
3769
|
+
return extras.nodeAttributes ?? extras.attributes;
|
|
3770
|
+
}
|
|
3771
|
+
if (domain === "edge") {
|
|
3772
|
+
return extras.edgeAttributes ?? extras.attributes;
|
|
3773
|
+
}
|
|
3774
|
+
return extras.attributes;
|
|
3775
|
+
}
|
|
3776
|
+
function checkWeights(snapshot, extras, note) {
|
|
3777
|
+
if (extras.weights === false && explicitWeights(snapshot).weighted) {
|
|
3778
|
+
note(
|
|
3779
|
+
LOSS.WEIGHTS_DROPPED,
|
|
3780
|
+
`the edge weights are not written; ${snapshot.edgeCount} edge${plural(snapshot.edgeCount)} ${agree(snapshot.edgeCount, "reads", "read")} back with weight 1`,
|
|
3781
|
+
null,
|
|
3782
|
+
snapshot.edgeCount
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
const ROLE_WORDS = {
|
|
3787
|
+
label: "labels",
|
|
3788
|
+
weight: "weights",
|
|
3789
|
+
id: "ids",
|
|
3790
|
+
position: "positions",
|
|
3791
|
+
originalId: "original ids"
|
|
3792
|
+
};
|
|
3793
|
+
function checkColumns(table, domain, caps, extras, note) {
|
|
3794
|
+
for (const column of table) {
|
|
3795
|
+
const { meta } = column;
|
|
3796
|
+
const { name, role } = meta;
|
|
3797
|
+
if (role !== null && STRUCTURAL_ROLES.has(role)) {
|
|
3798
|
+
continue;
|
|
3799
|
+
}
|
|
3800
|
+
const label = `${domain} column "${name}"`;
|
|
3801
|
+
if (role !== null && extras.roleNames !== void 0) {
|
|
3802
|
+
const fixed = extras.roleNames[role];
|
|
3803
|
+
if (fixed !== void 0 && fixed !== name) {
|
|
3804
|
+
note(
|
|
3805
|
+
LOSS.COLUMN_NAME_CHANGED,
|
|
3806
|
+
`${label} (the ${ROLE_WORDS[role] ?? role}) is written as the format's own ${role} and reads back as "${fixed}"`,
|
|
3807
|
+
name,
|
|
3808
|
+
column.length - column.nullCount
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
if (domain === "edge" && role === "id") {
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
const named = extras.writtenColumns?.[domain]?.includes(name) ?? false;
|
|
3816
|
+
if (writesAttributes(domain, extras) === false && !named && !(role !== null && (extras.roles?.has(role) ?? false))) {
|
|
3817
|
+
note(LOSS.COLUMN_DROPPED, `${label} is not written`, name, column.length - column.nullCount);
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
if (role === "timeText") {
|
|
3821
|
+
if (extras.temporalText !== true) {
|
|
3822
|
+
note(
|
|
3823
|
+
LOSS.TEMPORAL_TEXT,
|
|
3824
|
+
`${label} (the original text of a date or time column) cannot be written`,
|
|
3825
|
+
name,
|
|
3826
|
+
column.length - column.nullCount
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
if (role === "position") {
|
|
3832
|
+
if (!caps.positions) {
|
|
3833
|
+
note(LOSS.POSITIONS, `${label} (positions) cannot be written`, name, column.length - column.nullCount);
|
|
3834
|
+
} else if (extras.positionDtype !== void 0 && meta.dtype !== extras.positionDtype) {
|
|
3835
|
+
note(
|
|
3836
|
+
LOSS.DTYPE,
|
|
3837
|
+
`${label} (positions) is ${meta.dtype}; the format reads positions back as ${extras.positionDtype}`,
|
|
3838
|
+
name,
|
|
3839
|
+
column.length - column.nullCount
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
if (role !== null && VIZ_ROLES.has(role)) {
|
|
3845
|
+
if (!caps.viz) {
|
|
3846
|
+
note(LOSS.VIZ, `${label} (${role}) cannot be written`, name, column.length - column.nullCount);
|
|
3847
|
+
}
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
if (role !== null && HIERARCHY_ROLES.has(role)) {
|
|
3851
|
+
if (!caps.hierarchy) {
|
|
3852
|
+
note(LOSS.HIERARCHY, `${label} (${role}) cannot be written`, name, column.length - column.nullCount);
|
|
3853
|
+
}
|
|
3854
|
+
continue;
|
|
3855
|
+
}
|
|
3856
|
+
if (role !== null && TEMPORAL_ROLES.has(role)) {
|
|
3857
|
+
if (caps.temporal === "none") {
|
|
3858
|
+
note(LOSS.TEMPORAL, `${label} (${role}) cannot be written`, name, column.length - column.nullCount);
|
|
3859
|
+
}
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3862
|
+
if (role === "spells") {
|
|
3863
|
+
if (caps.temporal !== "spells" && caps.temporal !== "dynamic-values") {
|
|
3864
|
+
note(LOSS.SPELLS, `${label} (spells) cannot be written`, name, column.length - column.nullCount);
|
|
3865
|
+
}
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
if (role === "open" || role === "spellsOpen") {
|
|
3869
|
+
if (extras.openIntervals !== true) {
|
|
3870
|
+
note(
|
|
3871
|
+
LOSS.OPEN_INTERVAL,
|
|
3872
|
+
`${label} (open intervals) cannot be written`,
|
|
3873
|
+
name,
|
|
3874
|
+
column.length - column.nullCount
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
if (role !== null && !(extras.roles?.has(role) ?? false)) {
|
|
3880
|
+
note(
|
|
3881
|
+
LOSS.ROLE,
|
|
3882
|
+
`${label} (${role}) is written as a plain attribute; the format has no ${role} slot and the role is lost`,
|
|
3883
|
+
name,
|
|
3884
|
+
column.length - column.nullCount
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
if (meta.dynamic && caps.temporal !== "dynamic-values") {
|
|
3888
|
+
note(LOSS.DYNAMIC_VALUES, `${label} is dynamic; only its static value can be written`, name, null);
|
|
3889
|
+
}
|
|
3890
|
+
checkDtype(column, label, caps, note);
|
|
3891
|
+
if (meta.default !== void 0 && !caps.defaults) {
|
|
3892
|
+
note(LOSS.DEFAULT, `${label} declares a default; the format has no defaults`, name, null);
|
|
3893
|
+
}
|
|
3894
|
+
if (meta.options !== null && !caps.options) {
|
|
3895
|
+
note(LOSS.OPTIONS, `${label} declares options; the format has no enumerations`, name, null);
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
function checkDtype(column, label, caps, note) {
|
|
3900
|
+
const { meta } = column;
|
|
3901
|
+
const { name } = meta;
|
|
3902
|
+
const set = column.length - column.nullCount;
|
|
3903
|
+
switch (meta.dtype) {
|
|
3904
|
+
case "list":
|
|
3905
|
+
if (!caps.lists) {
|
|
3906
|
+
note(LOSS.LIST, `${label} is a list; the format has no lists`, name, set);
|
|
3907
|
+
} else if (meta.itemDtype !== null && !caps.dtypes.includes(meta.itemDtype)) {
|
|
3908
|
+
note(
|
|
3909
|
+
LOSS.DTYPE,
|
|
3910
|
+
`${label} holds ${meta.itemDtype} items; the format cannot keep that dtype`,
|
|
3911
|
+
name,
|
|
3912
|
+
set
|
|
3913
|
+
);
|
|
3914
|
+
} else if (meta.itemDtype === "dict") {
|
|
3915
|
+
note(LOSS.DTYPE, `${label} holds dict items; list items read back as string`, name, set);
|
|
3916
|
+
}
|
|
3917
|
+
break;
|
|
3918
|
+
case "json":
|
|
3919
|
+
if (!caps.json) {
|
|
3920
|
+
note(LOSS.JSON, `${label} holds nested values; the format has no nested values`, name, set);
|
|
3921
|
+
}
|
|
3922
|
+
break;
|
|
3923
|
+
default:
|
|
3924
|
+
if (!caps.dtypes.includes(meta.dtype)) {
|
|
3925
|
+
note(LOSS.DTYPE, `${label} is ${meta.dtype}; the format cannot keep that dtype`, name, set);
|
|
3926
|
+
}
|
|
3927
|
+
if (meta.components > 1 && !caps.components) {
|
|
3928
|
+
note(
|
|
3929
|
+
LOSS.COMPONENTS,
|
|
3930
|
+
`${label} has ${meta.components} components; the format has no strides`,
|
|
3931
|
+
name,
|
|
3932
|
+
set
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
break;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
function countMixedEdges(snapshot) {
|
|
3939
|
+
if (!snapshot.directed) {
|
|
3940
|
+
return 0;
|
|
3941
|
+
}
|
|
3942
|
+
const directed = snapshot.edges.byRole("directed");
|
|
3943
|
+
if (directed === null || directed.dtype !== "bool") {
|
|
3944
|
+
return 0;
|
|
3945
|
+
}
|
|
3946
|
+
const pair = snapshot.edges.byRole("pair");
|
|
3947
|
+
let undirectedRows = 0;
|
|
3948
|
+
let paired = 0;
|
|
3949
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
3950
|
+
if (directed.isSet(e) && directed.value(e) === false) {
|
|
3951
|
+
undirectedRows++;
|
|
3952
|
+
if (pair !== null && pair.dtype === "u32" && pair.isSet(e) && pair.data[e] !== INVALID_INDEX) {
|
|
3953
|
+
paired++;
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return paired / 2 + (undirectedRows - paired);
|
|
3958
|
+
}
|
|
3959
|
+
function countParallelArcs(snapshot) {
|
|
3960
|
+
const { rowPtr, colIdx, nodeCount } = snapshot;
|
|
3961
|
+
let count = 0;
|
|
3962
|
+
for (let u = 0; u < nodeCount; u++) {
|
|
3963
|
+
for (let a = rowPtr[u] + 1; a < rowPtr[u + 1]; a++) {
|
|
3964
|
+
if (colIdx[a] === colIdx[a - 1]) {
|
|
3965
|
+
count++;
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
return snapshot.directed ? count : Math.ceil(count / 2);
|
|
3970
|
+
}
|
|
3971
|
+
const NAME_CHAR_RANGES = [
|
|
3972
|
+
[45, 46],
|
|
3973
|
+
[48, 58],
|
|
3974
|
+
[65, 90],
|
|
3975
|
+
[95, 95],
|
|
3976
|
+
[97, 122],
|
|
3977
|
+
[183, 183],
|
|
3978
|
+
[192, 214],
|
|
3979
|
+
[216, 246],
|
|
3980
|
+
[248, 893],
|
|
3981
|
+
[895, 8191],
|
|
3982
|
+
[8204, 8205],
|
|
3983
|
+
[8255, 8256],
|
|
3984
|
+
[8304, 8591],
|
|
3985
|
+
[11264, 12271],
|
|
3986
|
+
[12289, 55295],
|
|
3987
|
+
[63744, 64975],
|
|
3988
|
+
[65008, 65533],
|
|
3989
|
+
[65536, 983039]
|
|
3990
|
+
];
|
|
3991
|
+
function isNameChar(cp) {
|
|
3992
|
+
for (const [lo, hi] of NAME_CHAR_RANGES) {
|
|
3993
|
+
if (cp < lo) {
|
|
3994
|
+
return false;
|
|
3995
|
+
}
|
|
3996
|
+
if (cp <= hi) {
|
|
3997
|
+
return true;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
return false;
|
|
4001
|
+
}
|
|
4002
|
+
function isNmtoken(text) {
|
|
4003
|
+
if (text.length === 0) {
|
|
4004
|
+
return false;
|
|
4005
|
+
}
|
|
4006
|
+
for (const ch of text) {
|
|
4007
|
+
const cp = ch.codePointAt(0);
|
|
4008
|
+
if (cp === void 0 || !isNameChar(cp)) {
|
|
4009
|
+
return false;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
return true;
|
|
4013
|
+
}
|
|
4014
|
+
function isRepresentableId(id, charset) {
|
|
4015
|
+
switch (charset) {
|
|
4016
|
+
case "any":
|
|
4017
|
+
return true;
|
|
4018
|
+
case "nmtoken":
|
|
4019
|
+
return isNmtoken(String(id));
|
|
4020
|
+
case "integer":
|
|
4021
|
+
return typeof id === "number" && Number.isSafeInteger(id);
|
|
4022
|
+
case "dense-1-based":
|
|
4023
|
+
return false;
|
|
4024
|
+
default: {
|
|
4025
|
+
const name = charset;
|
|
4026
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id charset ${name}`, { charset: name });
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
function countUnrepresentableIds(snapshot, charset) {
|
|
4031
|
+
const { ids } = snapshot;
|
|
4032
|
+
if (charset === "any") {
|
|
4033
|
+
return 0;
|
|
4034
|
+
}
|
|
4035
|
+
if (charset === "dense-1-based") {
|
|
4036
|
+
if (ids.kind === "identity" && ids.offset === 1) {
|
|
4037
|
+
return 0;
|
|
4038
|
+
}
|
|
4039
|
+
let changed = 0;
|
|
4040
|
+
for (let i = 0; i < ids.size; i++) {
|
|
4041
|
+
if (ids.idOf(i) !== i + 1) {
|
|
4042
|
+
changed++;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
return changed;
|
|
4046
|
+
}
|
|
4047
|
+
let count = 0;
|
|
4048
|
+
for (let i = 0; i < ids.size; i++) {
|
|
4049
|
+
if (!isRepresentableId(ids.idOf(i), charset)) {
|
|
4050
|
+
count++;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
return count;
|
|
4054
|
+
}
|
|
4055
|
+
function sanitizeIds(snapshot, charset, mode) {
|
|
4056
|
+
const { ids } = snapshot;
|
|
4057
|
+
const n = ids.size;
|
|
4058
|
+
if (charset === "dense-1-based") {
|
|
4059
|
+
const changed = countUnrepresentableIds(snapshot, charset);
|
|
4060
|
+
return {
|
|
4061
|
+
charset,
|
|
4062
|
+
changed,
|
|
4063
|
+
idAt: (i) => i + 1,
|
|
4064
|
+
isChanged: (i) => ids.idOf(i) !== i + 1,
|
|
4065
|
+
originalAt: (i) => ids.idOf(i)
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
const unchanged = {
|
|
4069
|
+
charset,
|
|
4070
|
+
changed: 0,
|
|
4071
|
+
idAt: (i) => ids.idOf(i),
|
|
4072
|
+
isChanged: () => false,
|
|
4073
|
+
originalAt: (i) => ids.idOf(i)
|
|
4074
|
+
};
|
|
4075
|
+
if (charset === "any") {
|
|
4076
|
+
return unchanged;
|
|
4077
|
+
}
|
|
4078
|
+
const bad = [];
|
|
4079
|
+
for (let i = 0; i < n; i++) {
|
|
4080
|
+
if (!isRepresentableId(ids.idOf(i), charset)) {
|
|
4081
|
+
bad.push(i);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
if (bad.length === 0) {
|
|
4085
|
+
return unchanged;
|
|
4086
|
+
}
|
|
4087
|
+
if (mode === "error") {
|
|
4088
|
+
const first = ids.idOf(bad[0]);
|
|
4089
|
+
throw new GraphFormatError(
|
|
4090
|
+
"E_INVALID_ID",
|
|
4091
|
+
`${bad.length} node id${plural(bad.length)} ${charsetText(charset, bad.length)} (first: ${JSON.stringify(first)} at index ${bad[0]}); pass sanitizeIds: "mangle" to rewrite them`,
|
|
4092
|
+
{ reason: "charset", charset, count: bad.length, id: first, index: bad[0] }
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
const written = /* @__PURE__ */ new Map();
|
|
4096
|
+
if (charset === "nmtoken") {
|
|
4097
|
+
const used = /* @__PURE__ */ new Set();
|
|
4098
|
+
for (let i = 0; i < n; i++) {
|
|
4099
|
+
const id = ids.idOf(i);
|
|
4100
|
+
if (isRepresentableId(id, charset)) {
|
|
4101
|
+
used.add(String(id));
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
for (const i of bad) {
|
|
4105
|
+
const base = mangleNmtoken(String(ids.idOf(i)));
|
|
4106
|
+
let candidate = base;
|
|
4107
|
+
for (let k = 2; used.has(candidate); k++) {
|
|
4108
|
+
candidate = `${base}_${k}`;
|
|
4109
|
+
}
|
|
4110
|
+
used.add(candidate);
|
|
4111
|
+
written.set(i, candidate);
|
|
4112
|
+
}
|
|
4113
|
+
} else {
|
|
4114
|
+
const used = /* @__PURE__ */ new Set();
|
|
4115
|
+
for (let i = 0; i < n; i++) {
|
|
4116
|
+
const id = ids.idOf(i);
|
|
4117
|
+
if (typeof id === "number" && Number.isSafeInteger(id)) {
|
|
4118
|
+
used.add(id);
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
let next = 0;
|
|
4122
|
+
for (const i of bad) {
|
|
4123
|
+
while (used.has(next)) {
|
|
4124
|
+
next++;
|
|
4125
|
+
}
|
|
4126
|
+
used.add(next);
|
|
4127
|
+
written.set(i, next);
|
|
4128
|
+
next++;
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
return {
|
|
4132
|
+
charset,
|
|
4133
|
+
changed: bad.length,
|
|
4134
|
+
idAt: (i) => written.get(i) ?? ids.idOf(i),
|
|
4135
|
+
isChanged: (i) => written.has(i),
|
|
4136
|
+
originalAt: (i) => ids.idOf(i)
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
function mangleNmtoken(text) {
|
|
4140
|
+
let out = "";
|
|
4141
|
+
for (const ch of text) {
|
|
4142
|
+
const cp = ch.codePointAt(0);
|
|
4143
|
+
out += cp !== void 0 && isNameChar(cp) ? ch : "_";
|
|
4144
|
+
}
|
|
4145
|
+
return out.length === 0 ? "_" : out;
|
|
4146
|
+
}
|
|
4147
|
+
export {
|
|
4148
|
+
agree as $,
|
|
4149
|
+
COLUMN_TYPE_CODE as A,
|
|
4150
|
+
BAD_DEFAULT_CODE as B,
|
|
4151
|
+
COLUMN_RENAMED_CODE as C,
|
|
4152
|
+
DIRECTION_FORCED_CODE as D,
|
|
4153
|
+
EMPTY_INPUT_CODE as E,
|
|
4154
|
+
ELEMENT_ISSUE as F,
|
|
4155
|
+
INPUT_ISSUE as G,
|
|
4156
|
+
HYPEREDGE_CODE as H,
|
|
4157
|
+
ID_MERGED_CODE as I,
|
|
4158
|
+
ID_TEXT_TYPE_CODE as J,
|
|
4159
|
+
ROLE_DROPPED_CODE as K,
|
|
4160
|
+
LOSS as L,
|
|
4161
|
+
MIXED_DIRECTION_CODE as M,
|
|
4162
|
+
NO_GRAPH_CODE as N,
|
|
4163
|
+
OPTION_IGNORED_CODE as O,
|
|
4164
|
+
PRECISION_CODE as P,
|
|
4165
|
+
PARENTS_DROPPED_CODE as Q,
|
|
4166
|
+
ROLE_TAKEN_CODE as R,
|
|
4167
|
+
SINK_OPTION_CODE as S,
|
|
4168
|
+
MUTUAL_AS_UNDIRECTED_CODE as T,
|
|
4169
|
+
UNKNOWN_ATTR_TYPE_CODE as U,
|
|
4170
|
+
BAD_ASPECT_BLOCK_CODE as V,
|
|
4171
|
+
MULTI_ASPECT_FRAGMENT_CODE as W,
|
|
4172
|
+
XML_SYNTAX_CODE as X,
|
|
4173
|
+
ASPECT_ORDER_CODE as Y,
|
|
4174
|
+
STATUS_FAILED_CODE as Z,
|
|
4175
|
+
STATUS_WARNING_CODE as _,
|
|
4176
|
+
checkCapabilities as a,
|
|
4177
|
+
COLUMN_DROPPED_CODE as a$,
|
|
4178
|
+
uniqueColumnName as a0,
|
|
4179
|
+
declareResolved as a1,
|
|
4180
|
+
JSON_NONSTANDARD_NUMBER_CODE as a2,
|
|
4181
|
+
BAD_VALUE_CODE as a3,
|
|
4182
|
+
textChunks as a4,
|
|
4183
|
+
MAX_TEXT_LENGTH as a5,
|
|
4184
|
+
tooLarge as a6,
|
|
4185
|
+
chooseGraph as a7,
|
|
4186
|
+
ImportReportBuilder as a8,
|
|
4187
|
+
idText as a9,
|
|
4188
|
+
NODE_ORDER_CODE as aA,
|
|
4189
|
+
GRAPH_COLUMN_AS_METADATA_CODE as aB,
|
|
4190
|
+
TYPEDEF_NODES_CODE as aC,
|
|
4191
|
+
RELATION_ASSUMED_CODE as aD,
|
|
4192
|
+
sanitizeIds as aE,
|
|
4193
|
+
formatDecimal as aF,
|
|
4194
|
+
formatInteger as aG,
|
|
4195
|
+
canonicalId as aH,
|
|
4196
|
+
coerceIdText as aI,
|
|
4197
|
+
survivesF32 as aJ,
|
|
4198
|
+
isWeightField as aK,
|
|
4199
|
+
parseWeightText as aL,
|
|
4200
|
+
isAbortError as aM,
|
|
4201
|
+
BAD_LINE_CODE as aN,
|
|
4202
|
+
messageOf as aO,
|
|
4203
|
+
describe as aP,
|
|
4204
|
+
UNKNOWN_OPTION_CODE as aQ,
|
|
4205
|
+
normalizeInput as aR,
|
|
4206
|
+
foreignKind as aS,
|
|
4207
|
+
ImportError as aT,
|
|
4208
|
+
FETCH_CODE as aU,
|
|
4209
|
+
EDGES_MERGED_CODE as aV,
|
|
4210
|
+
SELF_LOOPS_DROPPED_CODE as aW,
|
|
4211
|
+
WEIGHT_NOT_FOUND_CODE as aX,
|
|
4212
|
+
lockReader as aY,
|
|
4213
|
+
abortable as aZ,
|
|
4214
|
+
BAD_OPTIONS_CODE as a_,
|
|
4215
|
+
formatF32 as aa,
|
|
4216
|
+
formatF64 as ab,
|
|
4217
|
+
DictHeuristic as ac,
|
|
4218
|
+
TOO_LARGE_CODE as ad,
|
|
4219
|
+
AMBIGUOUS_GRAPH_NAME_CODE as ae,
|
|
4220
|
+
GRAPH_NOT_FOUND_CODE as af,
|
|
4221
|
+
SINGLE_OBJECT_ASPECT_CODE as ag,
|
|
4222
|
+
STYLES_NOT_IMPORTED_CODE as ah,
|
|
4223
|
+
PARENT_CYCLE_CODE as ai,
|
|
4224
|
+
WIDENED_CODE as aj,
|
|
4225
|
+
SYNTAX_CODE as ak,
|
|
4226
|
+
resolveImportOptions as al,
|
|
4227
|
+
throwIfAborted as am,
|
|
4228
|
+
reportUnusedOptions as an,
|
|
4229
|
+
reportSinkOptions as ao,
|
|
4230
|
+
graphChosen as ap,
|
|
4231
|
+
DirectionResolver as aq,
|
|
4232
|
+
IdCoercer as ar,
|
|
4233
|
+
weightFromValue as as,
|
|
4234
|
+
COLUMN_AS_PROPERTY_VALUE_CODE as at,
|
|
4235
|
+
ROLE_ASSUMED_CODE as au,
|
|
4236
|
+
formatNumber as av,
|
|
4237
|
+
isNameChar as aw,
|
|
4238
|
+
XML_ILLEGAL_CHAR_CODE as ax,
|
|
4239
|
+
RENAMED_CODE as ay,
|
|
4240
|
+
LineReader as az,
|
|
4241
|
+
plural as b,
|
|
4242
|
+
otherFormatDialect as b$,
|
|
4243
|
+
COLUMN_RENAMED_LOSS_CODE as b0,
|
|
4244
|
+
CONTROL_CHARACTER_CODE as b1,
|
|
4245
|
+
DEFAULT_ERROR_LIMIT as b2,
|
|
4246
|
+
DIRECTED_COLUMN as b3,
|
|
4247
|
+
DIRECTION_DROPPED_CODE as b4,
|
|
4248
|
+
DUPLICATE_EDGE_CODE as b5,
|
|
4249
|
+
EMPTY_COLUMN_DROPPED_CODE as b6,
|
|
4250
|
+
EQUATION_AS_TEXT_CODE as b7,
|
|
4251
|
+
FOREIGN_FORMAT_CODE as b8,
|
|
4252
|
+
ID_TEXT_COLLISION_CODE as b9,
|
|
4253
|
+
isNmtoken as bA,
|
|
4254
|
+
mangleNmtoken as bB,
|
|
4255
|
+
readText as bC,
|
|
4256
|
+
refusedSave as bD,
|
|
4257
|
+
countMixedEdges as bE,
|
|
4258
|
+
canonicalEncoding as bF,
|
|
4259
|
+
TIME_TEXT_ROLE as bG,
|
|
4260
|
+
TIME_TEXT_SUFFIX as bH,
|
|
4261
|
+
declareAttribute as bI,
|
|
4262
|
+
overflowsToInfinity as bJ,
|
|
4263
|
+
parseDeclaredTemporal as bK,
|
|
4264
|
+
parseDeclaredValue as bL,
|
|
4265
|
+
losesPrecision as bM,
|
|
4266
|
+
parseTimeText as bN,
|
|
4267
|
+
splitListText as bO,
|
|
4268
|
+
timeTextCompanion as bP,
|
|
4269
|
+
parseDecimalText as bQ,
|
|
4270
|
+
formatTimeValue as bR,
|
|
4271
|
+
mapDeclaredType as bS,
|
|
4272
|
+
joinListText as bT,
|
|
4273
|
+
formatTemporal as bU,
|
|
4274
|
+
DICT_SAMPLE_ROWS as bV,
|
|
4275
|
+
reportWeightPrecision as bW,
|
|
4276
|
+
takenIn as bX,
|
|
4277
|
+
declareOn as bY,
|
|
4278
|
+
parseTemporal as bZ,
|
|
4279
|
+
withArticle as b_,
|
|
4280
|
+
INTEGRAL_F64_CODE as ba,
|
|
4281
|
+
ISSUES_SUPPRESSED_CODE as bb,
|
|
4282
|
+
MUTUAL_COLUMN as bc,
|
|
4283
|
+
MUTUAL_EXPANDED_CODE as bd,
|
|
4284
|
+
NONFINITE_AS_NULL_CODE as be,
|
|
4285
|
+
NO_CAPABILITIES as bf,
|
|
4286
|
+
OPTIONS_GAINED_CODE as bg,
|
|
4287
|
+
PAIR_COLUMN as bh,
|
|
4288
|
+
PARSE_ERROR_CODE as bi,
|
|
4289
|
+
SELF_LOOP_CODE as bj,
|
|
4290
|
+
STORAGE_CLASS_CODE as bk,
|
|
4291
|
+
TEMPORAL_DROPPED_CODE as bl,
|
|
4292
|
+
TEMPORAL_TEXT_DROPPED_CODE as bm,
|
|
4293
|
+
TEXT_INFERRED_CODE as bn,
|
|
4294
|
+
UNKNOWN_TYPE_CODE as bo,
|
|
4295
|
+
WEIGHTS_DROPPED_CODE as bp,
|
|
4296
|
+
WEIGHT_KEY_CLASH_CODE as bq,
|
|
4297
|
+
coerceId as br,
|
|
4298
|
+
declareCompanion as bs,
|
|
4299
|
+
decodeEntryName as bt,
|
|
4300
|
+
edgeWeights as bu,
|
|
4301
|
+
forDecodedText as bv,
|
|
4302
|
+
formatGmlReal as bw,
|
|
4303
|
+
inputLength as bx,
|
|
4304
|
+
isCanonicalIntegerText as by,
|
|
4305
|
+
isImportInput as bz,
|
|
4306
|
+
capabilities as c,
|
|
4307
|
+
readBytes as c0,
|
|
4308
|
+
elementCount as c1,
|
|
4309
|
+
DIRECTION_REFUSED_CODE as d,
|
|
4310
|
+
explicitWeights as e,
|
|
4311
|
+
STRAY_TEXT_CODE as f,
|
|
4312
|
+
UNKNOWN_ELEMENT_CODE as g,
|
|
4313
|
+
UNKNOWN_XML_ATTRIBUTE_CODE as h,
|
|
4314
|
+
DANGLING_REFERENCE_CODE as i,
|
|
4315
|
+
COUNT_MISMATCH_CODE as j,
|
|
4316
|
+
COUNT_HINT_CODE as k,
|
|
4317
|
+
MULTIPLE_GRAPHS_CODE as l,
|
|
4318
|
+
DUPLICATE_EDGE_ID_CODE as m,
|
|
4319
|
+
DUPLICATE_NODE_CODE as n,
|
|
4320
|
+
DUPLICATE_ATTRIBUTE_CODE as o,
|
|
4321
|
+
pairFolding as p,
|
|
4322
|
+
UNKNOWN_PARENT_CODE as q,
|
|
4323
|
+
resolveExportOptions as r,
|
|
4324
|
+
MISSING_ENDPOINT_CODE as s,
|
|
4325
|
+
MISSING_ID_CODE as t,
|
|
4326
|
+
DUPLICATE_KEY_CODE as u,
|
|
4327
|
+
ENCODING_CONFLICT_CODE as v,
|
|
4328
|
+
UNKNOWN_ENCODING_CODE as w,
|
|
4329
|
+
ENCODING_FALLBACK_CODE as x,
|
|
4330
|
+
INVALID_ENCODING_CODE as y,
|
|
4331
|
+
INVALID_UTF8_CODE as z
|
|
4332
|
+
};
|
|
4333
|
+
//# sourceMappingURL=export-BTI4yQgy.js.map
|