@graphty/graph-io 0.3.25 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -414
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
- package/dist/chunks/choose-BbJI55dx.js +56 -0
- package/dist/chunks/choose-BbJI55dx.js.map +1 -0
- package/dist/chunks/constants-BCsFsMj4.js +231 -0
- package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
- package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
- package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
- package/dist/chunks/export-BTI4yQgy.js +4333 -0
- package/dist/chunks/export-BTI4yQgy.js.map +1 -0
- package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
- package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
- package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
- package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
- package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
- package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
- package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
- package/dist/chunks/exporter-tne475KK.js.map +1 -0
- package/dist/chunks/importer-90YWhJoF.js +2811 -0
- package/dist/chunks/importer-90YWhJoF.js.map +1 -0
- package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
- package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
- package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
- package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
- package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
- package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
- package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
- package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
- package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
- package/dist/chunks/importer-DfVu6j59.js.map +1 -0
- package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
- package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
- package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
- package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
- package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
- package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
- package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
- package/dist/chunks/records-CQXT__mY.js.map +1 -0
- package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
- package/dist/chunks/text-DmcUs5_Q.js.map +1 -0
- package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
- package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
- package/dist/csv.js +951 -261
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +6 -3
- package/dist/cx2.js +2 -2
- package/dist/cys.js +6 -3
- package/dist/dot.js +1 -1
- package/dist/gexf.js +554 -122
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +342 -85
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +1105 -213
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +3 -2
- package/dist/graphml.js.map +1 -1
- package/dist/json.js +3 -2
- package/dist/neo4j.js +564 -157
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +6 -3
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts +18 -6
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +6 -3
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts +29 -11
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +25 -7
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/cell-budget.d.ts +9 -2
- package/dist/src/common/cell-budget.d.ts.map +1 -1
- package/dist/src/common/cell-budget.js +11 -3
- package/dist/src/common/cell-budget.js.map +1 -1
- package/dist/src/common/choose.d.ts +19 -0
- package/dist/src/common/choose.d.ts.map +1 -0
- package/dist/src/common/choose.js +72 -0
- package/dist/src/common/choose.js.map +1 -0
- package/dist/src/common/codes.d.ts +456 -66
- package/dist/src/common/codes.d.ts.map +1 -1
- package/dist/src/common/codes.js +458 -66
- package/dist/src/common/codes.js.map +1 -1
- package/dist/src/common/compare.d.ts +81 -0
- package/dist/src/common/compare.d.ts.map +1 -0
- package/dist/src/common/compare.js +394 -0
- package/dist/src/common/compare.js.map +1 -0
- package/dist/src/common/cx-export.d.ts +123 -0
- package/dist/src/common/cx-export.d.ts.map +1 -0
- package/dist/src/common/cx-export.js +334 -0
- package/dist/src/common/cx-export.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +22 -2
- package/dist/src/common/declared-types.d.ts.map +1 -1
- package/dist/src/common/declared-types.js +19 -1
- package/dist/src/common/declared-types.js.map +1 -1
- package/dist/src/common/direction.d.ts +74 -50
- package/dist/src/common/direction.d.ts.map +1 -1
- package/dist/src/common/direction.js +47 -27
- package/dist/src/common/direction.js.map +1 -1
- package/dist/src/common/escape.d.ts +21 -9
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +33 -17
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/common/export.d.ts +144 -32
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +216 -42
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/common/format.d.ts +23 -1
- package/dist/src/common/format.d.ts.map +1 -1
- package/dist/src/common/format.js +26 -1
- package/dist/src/common/format.js.map +1 -1
- package/dist/src/common/ids.d.ts +9 -2
- package/dist/src/common/ids.d.ts.map +1 -1
- package/dist/src/common/ids.js +6 -2
- package/dist/src/common/ids.js.map +1 -1
- package/dist/src/common/input.d.ts +122 -28
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +890 -124
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.d.ts +144 -20
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +868 -188
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/line-format.d.ts +90 -0
- package/dist/src/common/line-format.d.ts.map +1 -0
- package/dist/src/common/line-format.js +185 -0
- package/dist/src/common/line-format.js.map +1 -0
- package/dist/src/common/lists.d.ts +6 -1
- package/dist/src/common/lists.d.ts.map +1 -1
- package/dist/src/common/lists.js +2 -0
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/ontology-export.d.ts +167 -0
- package/dist/src/common/ontology-export.d.ts.map +1 -0
- package/dist/src/common/ontology-export.js +359 -0
- package/dist/src/common/ontology-export.js.map +1 -0
- package/dist/src/common/ontology.d.ts +25 -3
- package/dist/src/common/ontology.d.ts.map +1 -1
- package/dist/src/common/ontology.js +30 -9
- package/dist/src/common/ontology.js.map +1 -1
- package/dist/src/common/options.d.ts +62 -12
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +89 -11
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/plural.d.ts +31 -0
- package/dist/src/common/plural.d.ts.map +1 -0
- package/dist/src/common/plural.js +46 -0
- package/dist/src/common/plural.js.map +1 -0
- package/dist/src/common/report.d.ts +72 -14
- package/dist/src/common/report.d.ts.map +1 -1
- package/dist/src/common/report.js +146 -22
- package/dist/src/common/report.js.map +1 -1
- package/dist/src/common/temporal.d.ts +22 -5
- package/dist/src/common/temporal.d.ts.map +1 -1
- package/dist/src/common/temporal.js +13 -2
- package/dist/src/common/temporal.js.map +1 -1
- package/dist/src/common/text.d.ts +39 -23
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +51 -21
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/weights.d.ts +37 -4
- package/dist/src/common/weights.d.ts.map +1 -1
- package/dist/src/common/weights.js +61 -2
- package/dist/src/common/weights.js.map +1 -1
- package/dist/src/common/writer.d.ts +19 -3
- package/dist/src/common/writer.d.ts.map +1 -1
- package/dist/src/common/writer.js +30 -2
- package/dist/src/common/writer.js.map +1 -1
- package/dist/src/common/xml.d.ts +125 -9
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +400 -83
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts +44 -4
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +134 -5
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts +80 -24
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +145 -32
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/header.d.ts +38 -9
- package/dist/src/formats/csv/header.d.ts.map +1 -1
- package/dist/src/formats/csv/header.js +46 -10
- package/dist/src/formats/csv/header.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts +187 -37
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +835 -204
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/index.d.ts +100 -19
- package/dist/src/formats/csv/index.d.ts.map +1 -1
- package/dist/src/formats/csv/index.js +94 -21
- package/dist/src/formats/csv/index.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts +95 -10
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +333 -122
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/csv/values.d.ts +1 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -1
- package/dist/src/formats/csv/values.js +1 -0
- package/dist/src/formats/csv/values.js.map +1 -1
- package/dist/src/formats/cx/exporter.d.ts +102 -0
- package/dist/src/formats/cx/exporter.d.ts.map +1 -0
- package/dist/src/formats/cx/exporter.js +1217 -0
- package/dist/src/formats/cx/exporter.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts +79 -32
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +680 -285
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx/index.d.ts +4 -3
- package/dist/src/formats/cx/index.d.ts.map +1 -1
- package/dist/src/formats/cx/index.js +4 -3
- package/dist/src/formats/cx/index.js.map +1 -1
- package/dist/src/formats/cx2/exporter.d.ts +29 -11
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +39 -268
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts +78 -28
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +226 -79
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cx2/index.d.ts +2 -1
- package/dist/src/formats/cx2/index.d.ts.map +1 -1
- package/dist/src/formats/cx2/index.js +2 -1
- package/dist/src/formats/cx2/index.js.map +1 -1
- package/dist/src/formats/cys/constants.d.ts +66 -17
- package/dist/src/formats/cys/constants.d.ts.map +1 -1
- package/dist/src/formats/cys/constants.js +59 -18
- package/dist/src/formats/cys/constants.js.map +1 -1
- package/dist/src/formats/cys/exporter.d.ts +118 -0
- package/dist/src/formats/cys/exporter.d.ts.map +1 -0
- package/dist/src/formats/cys/exporter.js +1072 -0
- package/dist/src/formats/cys/exporter.js.map +1 -0
- package/dist/src/formats/cys/importer.d.ts +22 -8
- package/dist/src/formats/cys/importer.d.ts.map +1 -1
- package/dist/src/formats/cys/importer.js +510 -149
- package/dist/src/formats/cys/importer.js.map +1 -1
- package/dist/src/formats/cys/index.d.ts +4 -2
- package/dist/src/formats/cys/index.d.ts.map +1 -1
- package/dist/src/formats/cys/index.js +4 -2
- package/dist/src/formats/cys/index.js.map +1 -1
- package/dist/src/formats/cys/session.d.ts +46 -7
- package/dist/src/formats/cys/session.d.ts.map +1 -1
- package/dist/src/formats/cys/session.js +98 -24
- package/dist/src/formats/cys/session.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts +9 -2
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +29 -7
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts +42 -14
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +56 -29
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts +91 -29
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +190 -39
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/index.d.ts +2 -1
- package/dist/src/formats/dot/index.d.ts.map +1 -1
- package/dist/src/formats/dot/index.js +2 -1
- package/dist/src/formats/dot/index.js.map +1 -1
- package/dist/src/formats/dot/names.d.ts +44 -11
- package/dist/src/formats/dot/names.d.ts.map +1 -1
- package/dist/src/formats/dot/names.js +44 -11
- package/dist/src/formats/dot/names.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +9 -2
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +20 -5
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts +46 -15
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +45 -15
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +87 -23
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +471 -90
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +84 -21
- package/dist/src/formats/gexf/index.d.ts.map +1 -1
- package/dist/src/formats/gexf/index.js +80 -23
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gexf/schema.d.ts +67 -15
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +59 -13
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts +69 -21
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +74 -29
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +116 -25
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +290 -61
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +87 -24
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +79 -26
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts +67 -18
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +108 -26
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/constants.d.ts +174 -44
- package/dist/src/formats/graphml/constants.d.ts.map +1 -1
- package/dist/src/formats/graphml/constants.js +165 -44
- package/dist/src/formats/graphml/constants.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts +17 -9
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +33 -12
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts +19 -7
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +466 -39
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/index.d.ts +2 -1
- package/dist/src/formats/graphml/index.d.ts.map +1 -1
- package/dist/src/formats/graphml/index.js +2 -1
- package/dist/src/formats/graphml/index.js.map +1 -1
- package/dist/src/formats/graphml/tree.d.ts +6 -2
- package/dist/src/formats/graphml/tree.d.ts.map +1 -1
- package/dist/src/formats/graphml/tree.js +6 -2
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +8 -3
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +36 -15
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts +88 -24
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +109 -19
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts +134 -22
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +135 -54
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +173 -52
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +899 -220
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +3 -2
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +3 -2
- package/dist/src/formats/json/index.js.map +1 -1
- package/dist/src/formats/json/obographs-export.d.ts +68 -0
- package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
- package/dist/src/formats/json/obographs-export.js +941 -0
- package/dist/src/formats/json/obographs-export.js.map +1 -0
- package/dist/src/formats/json/obographs.d.ts +26 -0
- package/dist/src/formats/json/obographs.d.ts.map +1 -1
- package/dist/src/formats/json/obographs.js +297 -80
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts +109 -21
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +238 -85
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts +17 -4
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +36 -4
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts +107 -27
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +415 -63
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/neo4j/index.d.ts +73 -23
- package/dist/src/formats/neo4j/index.d.ts.map +1 -1
- package/dist/src/formats/neo4j/index.js +65 -25
- package/dist/src/formats/neo4j/index.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts +150 -0
- package/dist/src/formats/obo/exporter.d.ts.map +1 -0
- package/dist/src/formats/obo/exporter.js +1736 -0
- package/dist/src/formats/obo/exporter.js.map +1 -0
- package/dist/src/formats/obo/importer.d.ts +95 -19
- package/dist/src/formats/obo/importer.d.ts.map +1 -1
- package/dist/src/formats/obo/importer.js +475 -104
- package/dist/src/formats/obo/importer.js.map +1 -1
- package/dist/src/formats/obo/index.d.ts +6 -3
- package/dist/src/formats/obo/index.d.ts.map +1 -1
- package/dist/src/formats/obo/index.js +6 -3
- package/dist/src/formats/obo/index.js.map +1 -1
- package/dist/src/formats/obo/syntax.d.ts +108 -4
- package/dist/src/formats/obo/syntax.d.ts.map +1 -1
- package/dist/src/formats/obo/syntax.js +211 -34
- package/dist/src/formats/obo/syntax.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts +54 -16
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +66 -34
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +84 -22
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +303 -83
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/index.d.ts +2 -1
- package/dist/src/formats/pajek/index.d.ts.map +1 -1
- package/dist/src/formats/pajek/index.js +2 -1
- package/dist/src/formats/pajek/index.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +77 -14
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +183 -51
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts +1 -0
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
- package/dist/src/formats/xgmml/columns.js +4 -0
- package/dist/src/formats/xgmml/columns.js.map +1 -1
- package/dist/src/formats/xgmml/constants.d.ts +186 -56
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
- package/dist/src/formats/xgmml/constants.js +181 -57
- package/dist/src/formats/xgmml/constants.js.map +1 -1
- package/dist/src/formats/xgmml/document.d.ts +65 -11
- package/dist/src/formats/xgmml/document.d.ts.map +1 -1
- package/dist/src/formats/xgmml/document.js +157 -28
- package/dist/src/formats/xgmml/document.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +61 -3
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +129 -26
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts +13 -6
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +20 -21
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts +33 -12
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +35 -72
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/formats/xgmml/index.d.ts +1 -0
- package/dist/src/formats/xgmml/index.d.ts.map +1 -1
- package/dist/src/formats/xgmml/index.js +1 -0
- package/dist/src/formats/xgmml/index.js.map +1 -1
- package/dist/src/formats/xgmml/values.d.ts +8 -1
- package/dist/src/formats/xgmml/values.d.ts.map +1 -1
- package/dist/src/formats/xgmml/values.js +7 -2
- package/dist/src/formats/xgmml/values.js.map +1 -1
- package/dist/src/index.d.ts +23 -18
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +28 -16
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +478 -92
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +807 -88
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +61 -19
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +121 -29
- package/dist/src/sniff.js.map +1 -1
- package/dist/src/types.d.ts +362 -112
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +28 -7
- package/dist/src/types.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/docs/samples/README.md +25 -0
- package/docs/samples/got-edges.csv +353 -0
- package/docs/samples/got-network.graphml +1384 -0
- package/docs/samples/got-nodes.csv +108 -0
- package/docs/samples/got.cx +930 -0
- package/docs/samples/got.cx2 +465 -0
- package/docs/samples/got.gexf +471 -0
- package/docs/samples/got.gml +2650 -0
- package/docs/samples/got.json +2546 -0
- package/docs/samples/got.net +461 -0
- package/docs/samples/movies-nodes.csv +9 -0
- package/docs/samples/movies-rels.csv +8 -0
- package/docs/samples/networks.cys +0 -0
- package/docs/samples/proteins.xgmml +9 -0
- package/docs/samples/teams.gv +24 -0
- package/docs/samples/vehicles.obo +25 -0
- package/package.json +9 -4
- package/src/children.ts +18 -6
- package/src/common/attributes.ts +29 -11
- package/src/common/cell-budget.ts +11 -3
- package/src/common/choose.ts +89 -0
- package/src/common/codes.ts +490 -66
- package/src/common/compare.ts +472 -0
- package/src/common/cx-export.ts +432 -0
- package/src/common/declared-types.ts +38 -4
- package/src/common/direction.ts +78 -51
- package/src/common/escape.ts +33 -17
- package/src/common/export.ts +284 -47
- package/src/common/format.ts +30 -1
- package/src/common/ids.ts +14 -3
- package/src/common/input.ts +1057 -134
- package/src/common/json-elements.ts +1034 -198
- package/src/common/line-format.ts +249 -0
- package/src/common/lists.ts +6 -1
- package/src/common/ontology-export.ts +461 -0
- package/src/common/ontology.ts +31 -9
- package/src/common/options.ts +110 -15
- package/src/common/plural.ts +49 -0
- package/src/common/report.ts +168 -25
- package/src/common/temporal.ts +22 -5
- package/src/common/text.ts +62 -23
- package/src/common/weights.ts +74 -4
- package/src/common/writer.ts +37 -3
- package/src/common/xml.ts +477 -88
- package/src/common/zip.ts +169 -7
- package/src/formats/csv/exporter.ts +201 -46
- package/src/formats/csv/header.ts +50 -11
- package/src/formats/csv/importer.ts +1133 -245
- package/src/formats/csv/index.ts +106 -19
- package/src/formats/csv/records.ts +396 -122
- package/src/formats/csv/values.ts +1 -0
- package/src/formats/cx/exporter.ts +1483 -0
- package/src/formats/cx/importer.ts +888 -300
- package/src/formats/cx/index.ts +4 -3
- package/src/formats/cx2/exporter.ts +61 -338
- package/src/formats/cx2/importer.ts +317 -86
- package/src/formats/cx2/index.ts +2 -1
- package/src/formats/cys/constants.ts +62 -17
- package/src/formats/cys/exporter.ts +1368 -0
- package/src/formats/cys/importer.ts +659 -157
- package/src/formats/cys/index.ts +4 -2
- package/src/formats/cys/session.ts +139 -31
- package/src/formats/cys/tables.ts +47 -9
- package/src/formats/dot/exporter.ts +83 -37
- package/src/formats/dot/importer.ts +248 -54
- package/src/formats/dot/index.ts +2 -1
- package/src/formats/dot/names.ts +44 -11
- package/src/formats/dot/tokenizer.ts +25 -6
- package/src/formats/gexf/exporter.ts +57 -18
- package/src/formats/gexf/importer.ts +636 -105
- package/src/formats/gexf/index.ts +90 -21
- package/src/formats/gexf/schema.ts +67 -15
- package/src/formats/gml/exporter.ts +90 -36
- package/src/formats/gml/importer.ts +388 -67
- package/src/formats/gml/index.ts +85 -24
- package/src/formats/gml/syntax.ts +111 -26
- package/src/formats/graphml/constants.ts +178 -44
- package/src/formats/graphml/exporter.ts +52 -18
- package/src/formats/graphml/importer.ts +640 -60
- package/src/formats/graphml/index.ts +2 -1
- package/src/formats/graphml/tree.ts +6 -2
- package/src/formats/graphml/yfiles.ts +37 -15
- package/src/formats/json/dialect.ts +128 -29
- package/src/formats/json/exporter.ts +205 -68
- package/src/formats/json/importer.ts +1163 -253
- package/src/formats/json/index.ts +3 -1
- package/src/formats/json/obographs-export.ts +1197 -0
- package/src/formats/json/obographs.ts +353 -92
- package/src/formats/neo4j/exporter.ts +320 -101
- package/src/formats/neo4j/header.ts +44 -6
- package/src/formats/neo4j/importer.ts +512 -73
- package/src/formats/neo4j/index.ts +75 -23
- package/src/formats/obo/exporter.ts +2210 -0
- package/src/formats/obo/importer.ts +622 -109
- package/src/formats/obo/index.ts +6 -3
- package/src/formats/obo/syntax.ts +232 -37
- package/src/formats/pajek/exporter.ts +89 -41
- package/src/formats/pajek/importer.ts +407 -93
- package/src/formats/pajek/index.ts +2 -1
- package/src/formats/pajek/syntax.ts +213 -52
- package/src/formats/xgmml/columns.ts +10 -0
- package/src/formats/xgmml/constants.ts +183 -56
- package/src/formats/xgmml/document.ts +212 -34
- package/src/formats/xgmml/emit.ts +175 -34
- package/src/formats/xgmml/exporter.ts +35 -27
- package/src/formats/xgmml/importer.ts +64 -86
- package/src/formats/xgmml/index.ts +1 -0
- package/src/formats/xgmml/values.ts +11 -3
- package/src/index.ts +81 -11
- package/src/registry.ts +1121 -135
- package/src/sniff.ts +163 -32
- package/src/types.ts +385 -114
- package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
- package/dist/chunks/export-Bh60Dv-n.js +0 -582
- package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
- package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
- package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
- package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
- package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
- package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
- package/dist/chunks/importer-DET1tg6s.js.map +0 -1
- package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
- package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
- package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
- package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
- package/dist/chunks/importer-eUREhBGj.js.map +0 -1
- package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
- package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
- package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
- package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
- package/dist/chunks/ontology-BnrJ4I98.js +0 -113
- package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
- package/dist/chunks/records-dbRkxwaq.js.map +0 -1
- package/dist/chunks/report-B1z4WT9e.js +0 -2372
- package/dist/chunks/report-B1z4WT9e.js.map +0 -1
- package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
- package/dist/chunks/weights-Dzba96G3.js +0 -176
- package/dist/chunks/weights-Dzba96G3.js.map +0 -1
- package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
|
@@ -0,0 +1,2811 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
import { ae as AMBIGUOUS_GRAPH_NAME_CODE, af as GRAPH_NOT_FOUND_CODE, l as MULTIPLE_GRAPHS_CODE, N as NO_GRAPH_CODE, E as EMPTY_INPUT_CODE, i as DANGLING_REFERENCE_CODE, ah as STYLES_NOT_IMPORTED_CODE, ad as TOO_LARGE_CODE, w as UNKNOWN_ENCODING_CODE, x as ENCODING_FALLBACK_CODE, F as ELEMENT_ISSUE, G as INPUT_ISSUE, bt as decodeEntryName, am as throwIfAborted, c as capabilities, L as LOSS, r as resolveExportOptions, a as checkCapabilities, p as pairFolding, b as plural, $ as agree, e as explicitWeights, aF as formatDecimal, ab as formatF64, aa as formatF32, ac as DictHeuristic, c0 as readBytes, a8 as ImportReportBuilder, a4 as textChunks, aT as ImportError, ao as reportSinkOptions, an as reportUnusedOptions, a7 as chooseGraph, ap as graphChosen, al as resolveImportOptions, c1 as elementCount, ar as IdCoercer, a1 as declareResolved } from "./export-BTI4yQgy.js";
|
|
3
|
+
import { h as hasIllegalXmlChar, e as escapeXmlAttribute, o as quoteCsvCell, t as tokenizeXml, f as xmlDeclaredEncoding, X as XmlSyntaxError } from "./escape-D0g6NNUy.js";
|
|
4
|
+
import { O as ORIGINAL_ID_ATTRIBUTE } from "./constants-BCsFsMj4.js";
|
|
5
|
+
import { X as XGMML_ISSUE, Z as Z_COLUMN, b as XLINK_NAMESPACE, C as CY_NAMESPACE, c as XGMML_NAMESPACE, G as GRAPHICS_COLUMN, E as EDGE_ID_COLUMN, P as POSITION_COLUMN, g as PARENT_COLUMN, h as PARENTS_COLUMN, S as SUBGRAPH_COLUMN, N as NESTED_NETWORK_COLUMN, e as NETWORK_POINTER_COLUMN, f as NETWORKS_COLUMN, r as resolveSettings, m as membersOf, k as XgmmlEmitter, l as isCyTrue, p as parseXgmml, n as dialectOf, i as XGMML_ORIGIN_NAMESPACE, o as parseCoordinate } from "./importer-Bcl1d4xH.js";
|
|
6
|
+
import { C as CsvRecordReader } from "./records-CQXT__mY.js";
|
|
7
|
+
const FORMAT = "cys";
|
|
8
|
+
const EXTENSIONS = Object.freeze([".cys"]);
|
|
9
|
+
const MIME_TYPES = Object.freeze(["application/zip"]);
|
|
10
|
+
const META_KEY = "cytoscape";
|
|
11
|
+
const CYTOSCAPE_NAMESPACE = "cytoscape";
|
|
12
|
+
const SELECTED_COLUMN = "cytoscape.selected";
|
|
13
|
+
const HIDDEN_COLUMN = "cytoscape.hidden";
|
|
14
|
+
const VIEW_POSITION_PREFIX = "position@";
|
|
15
|
+
const DEFAULT_MAX_UNCOMPRESSED = 2 * 1024 * 1024 * 1024;
|
|
16
|
+
const MAX_RATIO = 1e3;
|
|
17
|
+
const RELAYED = Object.fromEntries(
|
|
18
|
+
Object.values(XGMML_ISSUE).map((code) => [code.replace(/^[EW]_/, ""), code])
|
|
19
|
+
);
|
|
20
|
+
const CYS_ISSUE = Object.freeze({
|
|
21
|
+
...INPUT_ISSUE,
|
|
22
|
+
...ELEMENT_ISSUE,
|
|
23
|
+
/** A session XML entry that is not UTF-8 and declares no encoding was read as windows-1252. */
|
|
24
|
+
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
25
|
+
/** A session XML entry declares an encoding the platform cannot decode; read as UTF-8. */
|
|
26
|
+
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
27
|
+
...RELAYED,
|
|
28
|
+
/** The input is not a zip archive (or is text). */
|
|
29
|
+
NOT_ZIP: "E_CYS_NOT_ZIP",
|
|
30
|
+
/** The archive is damaged: no end record, offsets outside the file, a bad CRC, truncated data. */
|
|
31
|
+
CORRUPT: "E_CYS_CORRUPT",
|
|
32
|
+
/** A zip without a session marker (`<x.y.z>.version` or `cysession.xml`). */
|
|
33
|
+
NOT_SESSION: "E_CYS_NOT_SESSION",
|
|
34
|
+
/** A zip feature the reader does not support: encryption, a compression method, split archives. */
|
|
35
|
+
UNSUPPORTED: "E_CYS_UNSUPPORTED",
|
|
36
|
+
/** A session version graph-io cannot read: a major above 3, or the 2011 3.0 pre-release layout. */
|
|
37
|
+
VERSION: "E_CYS_VERSION",
|
|
38
|
+
/** A table or a virtual column that cannot be read at all; it is skipped. */
|
|
39
|
+
TABLE: "E_CYS_TABLE",
|
|
40
|
+
/** Table rows with too few or too many cells, a repeated key, or a key matching no element. */
|
|
41
|
+
TABLE_ROW: "W_CYS_TABLE_ROW",
|
|
42
|
+
/** The members of a collapsed group are not in the network itself; they are listed in `snapshot.meta.extra`. */
|
|
43
|
+
COLLAPSED_GROUP: "W_CYS_COLLAPSED_GROUP",
|
|
44
|
+
/** Entries the importer does not read (apps, global tables, properties, images, thumbnails). */
|
|
45
|
+
ENTRY_SKIPPED: "W_CYS_ENTRY_SKIPPED",
|
|
46
|
+
/** Two entries with one name; the first is read. */
|
|
47
|
+
DUPLICATE_ENTRY: "W_CYS_DUPLICATE_ENTRY",
|
|
48
|
+
/** A cysession.xml network record without an id, or naming a file an earlier record names. */
|
|
49
|
+
SESSION_RECORD: "W_CYS_SESSION_RECORD",
|
|
50
|
+
/** The archive inflates beyond maxUncompressedBytes, or an entry beyond the ratio limit. */
|
|
51
|
+
TOO_LARGE: TOO_LARGE_CODE,
|
|
52
|
+
/** The session's styles are not applied. */
|
|
53
|
+
STYLES_NOT_IMPORTED: STYLES_NOT_IMPORTED_CODE,
|
|
54
|
+
/** A view, table or network the session names but does not hold. */
|
|
55
|
+
DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
|
|
56
|
+
/** The input is empty. */
|
|
57
|
+
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
58
|
+
/** The session holds no network. */
|
|
59
|
+
NO_GRAPH: NO_GRAPH_CODE,
|
|
60
|
+
/**
|
|
61
|
+
* The file holds several graphs and only the first was read. It is not added when `graphIndex` or `graphName` chose the graph.
|
|
62
|
+
* `importAllGraphs()` reads every one.
|
|
63
|
+
*/
|
|
64
|
+
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
65
|
+
/** `graphIndex` or `graphName` matches no network in the session. The import stops. */
|
|
66
|
+
GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
|
|
67
|
+
/** `graphName` matches several networks in the session. The import stops. */
|
|
68
|
+
AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE
|
|
69
|
+
});
|
|
70
|
+
class ZipError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* Create the error.
|
|
73
|
+
* @param kind - what went wrong
|
|
74
|
+
* @param message - a plain-ASCII message
|
|
75
|
+
*/
|
|
76
|
+
constructor(kind, message) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = "ZipError";
|
|
79
|
+
this.kind = kind;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const RATIO_FLOOR = 1024 * 1024;
|
|
83
|
+
const EOCD_SIGNATURE = 101010256;
|
|
84
|
+
const ZIP64_LOCATOR_SIGNATURE = 117853008;
|
|
85
|
+
const ZIP64_EOCD_SIGNATURE = 101075792;
|
|
86
|
+
const CENTRAL_SIGNATURE = 33639248;
|
|
87
|
+
const LOCAL_SIGNATURE = 67324752;
|
|
88
|
+
const EOCD_SIZE = 22;
|
|
89
|
+
const MAX_COMMENT = 65535;
|
|
90
|
+
const CENTRAL_SIZE = 46;
|
|
91
|
+
const LOCAL_SIZE = 30;
|
|
92
|
+
const ZIP64_EOCD_SIZE = 56;
|
|
93
|
+
const ZIP64_LOCATOR_SIZE = 20;
|
|
94
|
+
const U16_MAX = 65535;
|
|
95
|
+
const U32_MAX = 4294967295;
|
|
96
|
+
const UTF8_FLAG = 2048;
|
|
97
|
+
const CRC_SLICE = 1024 * 1024;
|
|
98
|
+
const METHOD_NAMES = {
|
|
99
|
+
1: "shrink",
|
|
100
|
+
6: "implode",
|
|
101
|
+
9: "deflate64",
|
|
102
|
+
12: "bzip2",
|
|
103
|
+
14: "LZMA",
|
|
104
|
+
93: "zstd",
|
|
105
|
+
95: "xz",
|
|
106
|
+
96: "JPEG",
|
|
107
|
+
97: "WavPack",
|
|
108
|
+
98: "PPMd",
|
|
109
|
+
99: "AES encryption"
|
|
110
|
+
};
|
|
111
|
+
function startsLikeZip(bytes) {
|
|
112
|
+
return bytes.byteLength >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
|
|
113
|
+
}
|
|
114
|
+
function readZipDirectory(bytes) {
|
|
115
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
116
|
+
const eocd = findEndRecord(bytes, view);
|
|
117
|
+
if (eocd < 0) {
|
|
118
|
+
throw startsLikeZip(bytes) ? new ZipError("corrupt", "the archive has no end of central directory record (truncated?)") : new ZipError("not-zip", "the input is not a zip archive");
|
|
119
|
+
}
|
|
120
|
+
let disk = view.getUint16(eocd + 4, true);
|
|
121
|
+
let directoryDisk = view.getUint16(eocd + 6, true);
|
|
122
|
+
let count = view.getUint16(eocd + 10, true);
|
|
123
|
+
let directorySize = view.getUint32(eocd + 12, true);
|
|
124
|
+
let directoryOffset = view.getUint32(eocd + 16, true);
|
|
125
|
+
let directoryEnd = eocd;
|
|
126
|
+
const locator = eocd - ZIP64_LOCATOR_SIZE;
|
|
127
|
+
if (locator >= 0 && view.getUint32(locator, true) === ZIP64_LOCATOR_SIGNATURE) {
|
|
128
|
+
const record = zip64EndRecord(view, locator);
|
|
129
|
+
disk = view.getUint32(record + 16, true);
|
|
130
|
+
directoryDisk = view.getUint32(record + 20, true);
|
|
131
|
+
count = readU64(view, record + 32);
|
|
132
|
+
directorySize = readU64(view, record + 40);
|
|
133
|
+
directoryOffset = readU64(view, record + 48);
|
|
134
|
+
directoryEnd = record;
|
|
135
|
+
} else if (count === U16_MAX || directorySize === U32_MAX || directoryOffset === U32_MAX) {
|
|
136
|
+
throw new ZipError("corrupt", "the end record needs a zip64 record the archive does not have");
|
|
137
|
+
}
|
|
138
|
+
if (disk !== 0 || directoryDisk !== 0) {
|
|
139
|
+
throw new ZipError("unsupported", "a split (multi-disk) archive cannot be read");
|
|
140
|
+
}
|
|
141
|
+
const start = directoryEnd - directorySize;
|
|
142
|
+
if (start < 0 || directoryOffset > bytes.byteLength) {
|
|
143
|
+
throw new ZipError("corrupt", "the central directory lies outside the archive");
|
|
144
|
+
}
|
|
145
|
+
const shift = start - directoryOffset;
|
|
146
|
+
if (shift < 0) {
|
|
147
|
+
throw new ZipError("corrupt", "the central directory offset does not match its position");
|
|
148
|
+
}
|
|
149
|
+
const entries2 = [];
|
|
150
|
+
let at = start;
|
|
151
|
+
for (let i = 0; i < count; i++) {
|
|
152
|
+
if (at + CENTRAL_SIZE > directoryEnd || view.getUint32(at, true) !== CENTRAL_SIGNATURE) {
|
|
153
|
+
throw new ZipError("corrupt", `central directory entry ${i + 1} of ${count} is damaged`);
|
|
154
|
+
}
|
|
155
|
+
const nameLength = view.getUint16(at + 28, true);
|
|
156
|
+
const extraLength = view.getUint16(at + 30, true);
|
|
157
|
+
const commentLength = view.getUint16(at + 32, true);
|
|
158
|
+
const next = at + CENTRAL_SIZE + nameLength + extraLength + commentLength;
|
|
159
|
+
if (next > directoryEnd) {
|
|
160
|
+
throw new ZipError("corrupt", `central directory entry ${i + 1} runs past the directory`);
|
|
161
|
+
}
|
|
162
|
+
const flags = view.getUint16(at + 8, true);
|
|
163
|
+
const name = decodeEntryName(
|
|
164
|
+
bytes.subarray(at + CENTRAL_SIZE, at + CENTRAL_SIZE + nameLength),
|
|
165
|
+
(flags & UTF8_FLAG) === 0
|
|
166
|
+
);
|
|
167
|
+
let compressedSize = view.getUint32(at + 20, true);
|
|
168
|
+
let size = view.getUint32(at + 24, true);
|
|
169
|
+
let localOffset = view.getUint32(at + 42, true);
|
|
170
|
+
if (compressedSize === U32_MAX || size === U32_MAX || localOffset === U32_MAX) {
|
|
171
|
+
({ compressedSize, size, localOffset } = zip64Extra(view, at + CENTRAL_SIZE + nameLength, extraLength, {
|
|
172
|
+
compressedSize,
|
|
173
|
+
size,
|
|
174
|
+
localOffset
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
entries2.push(
|
|
178
|
+
Object.freeze({
|
|
179
|
+
name,
|
|
180
|
+
method: view.getUint16(at + 10, true),
|
|
181
|
+
flags,
|
|
182
|
+
crc32: view.getUint32(at + 16, true),
|
|
183
|
+
compressedSize,
|
|
184
|
+
size,
|
|
185
|
+
localOffset: localOffset + shift,
|
|
186
|
+
directory: name.endsWith("/")
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
at = next;
|
|
190
|
+
}
|
|
191
|
+
if (at + 4 <= directoryEnd && view.getUint32(at, true) === CENTRAL_SIGNATURE) {
|
|
192
|
+
throw new ZipError(
|
|
193
|
+
"corrupt",
|
|
194
|
+
`the end record counts ${count} entries but the central directory holds more; entries would go unread`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
checkOverlap(entries2, view);
|
|
198
|
+
return entries2;
|
|
199
|
+
}
|
|
200
|
+
function checkOverlap(entries2, view) {
|
|
201
|
+
const sorted = [...entries2].sort((a, b) => a.localOffset - b.localOffset);
|
|
202
|
+
const end = (entry) => {
|
|
203
|
+
const at = entry.localOffset;
|
|
204
|
+
const intact = at + LOCAL_SIZE <= view.byteLength && view.getUint32(at, true) === LOCAL_SIGNATURE;
|
|
205
|
+
const lengths = intact ? view.getUint16(at + 26, true) + view.getUint16(at + 28, true) : 0;
|
|
206
|
+
return at + LOCAL_SIZE + lengths + entry.compressedSize;
|
|
207
|
+
};
|
|
208
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
209
|
+
const before = sorted[i - 1];
|
|
210
|
+
if (sorted[i].localOffset < end(before)) {
|
|
211
|
+
throw new ZipError("corrupt", `the entries ${before.name} and ${sorted[i].name} overlap`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function findEndRecord(bytes, view) {
|
|
216
|
+
const lowest = Math.max(0, bytes.byteLength - EOCD_SIZE - MAX_COMMENT);
|
|
217
|
+
let fallback = -1;
|
|
218
|
+
for (let at = bytes.byteLength - EOCD_SIZE; at >= lowest; at--) {
|
|
219
|
+
if (view.getUint32(at, true) !== EOCD_SIGNATURE) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (at + EOCD_SIZE + view.getUint16(at + 20, true) === bytes.byteLength) {
|
|
223
|
+
return at;
|
|
224
|
+
}
|
|
225
|
+
if (fallback < 0) {
|
|
226
|
+
fallback = at;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return fallback;
|
|
230
|
+
}
|
|
231
|
+
function zip64EndRecord(view, locator) {
|
|
232
|
+
const stated = readU64(view, locator + 8);
|
|
233
|
+
for (const at of [stated, locator - ZIP64_EOCD_SIZE]) {
|
|
234
|
+
if (at >= 0 && at + ZIP64_EOCD_SIZE <= locator && view.getUint32(at, true) === ZIP64_EOCD_SIGNATURE) {
|
|
235
|
+
return at;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw new ZipError("corrupt", "the zip64 end record the locator names is missing");
|
|
239
|
+
}
|
|
240
|
+
function zip64Extra(view, at, length, fields) {
|
|
241
|
+
const out = { ...fields };
|
|
242
|
+
for (let p = at; p + 4 <= at + length; ) {
|
|
243
|
+
const id = view.getUint16(p, true);
|
|
244
|
+
const dataLength = view.getUint16(p + 2, true);
|
|
245
|
+
if (id === 1) {
|
|
246
|
+
let q = p + 4;
|
|
247
|
+
const end = q + dataLength;
|
|
248
|
+
for (const key of ["size", "compressedSize", "localOffset"]) {
|
|
249
|
+
if (fields[key] === U32_MAX) {
|
|
250
|
+
if (q + 8 > end) {
|
|
251
|
+
throw new ZipError("corrupt", "a zip64 extra field is too short");
|
|
252
|
+
}
|
|
253
|
+
out[key] = readU64(view, q);
|
|
254
|
+
q += 8;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
p += 4 + dataLength;
|
|
260
|
+
}
|
|
261
|
+
throw new ZipError("corrupt", "an entry needs a zip64 extra field it does not have");
|
|
262
|
+
}
|
|
263
|
+
function readU64(view, at) {
|
|
264
|
+
const high = view.getUint32(at + 4, true);
|
|
265
|
+
if (high >= 2097152) {
|
|
266
|
+
throw new ZipError("unsupported", "a zip64 size or offset beyond 2^53 cannot be read");
|
|
267
|
+
}
|
|
268
|
+
return high * 4294967296 + view.getUint32(at, true);
|
|
269
|
+
}
|
|
270
|
+
async function readZipEntry(bytes, entry, options) {
|
|
271
|
+
if ((entry.flags & 1) !== 0 || entry.method === 99) {
|
|
272
|
+
throw new ZipError("unsupported", `${entry.name}: the entry is encrypted`);
|
|
273
|
+
}
|
|
274
|
+
if (entry.method !== 0 && entry.method !== 8) {
|
|
275
|
+
const name = METHOD_NAMES[entry.method] ?? `method ${entry.method}`;
|
|
276
|
+
throw new ZipError(
|
|
277
|
+
"unsupported",
|
|
278
|
+
`${entry.name}: compression ${name} is not supported (only stored and deflate)`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (entry.size > options.maxBytes) {
|
|
282
|
+
throw new ZipError(
|
|
283
|
+
"too-large",
|
|
284
|
+
`${entry.name}: ${entry.size} uncompressed bytes exceed the limit of ${options.maxBytes}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (entry.size > RATIO_FLOOR && entry.size > options.maxRatio * Math.max(entry.compressedSize, 1)) {
|
|
288
|
+
throw new ZipError(
|
|
289
|
+
"too-large",
|
|
290
|
+
`${entry.name}: ${entry.size} bytes from ${entry.compressedSize} is a compression ratio above ${options.maxRatio}:1`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const data = entryData(bytes, entry);
|
|
294
|
+
if (entry.method === 0) {
|
|
295
|
+
if (data.byteLength !== entry.size) {
|
|
296
|
+
throw new ZipError("corrupt", `${entry.name}: a stored entry's sizes disagree`);
|
|
297
|
+
}
|
|
298
|
+
let crc = 4294967295;
|
|
299
|
+
for (let at = 0; at < data.byteLength; at += CRC_SLICE) {
|
|
300
|
+
throwIfAborted(options.signal);
|
|
301
|
+
crc = crcUpdate(crc, data.subarray(at, Math.min(at + CRC_SLICE, data.byteLength)));
|
|
302
|
+
options.onBytes?.(Math.min(at + CRC_SLICE, data.byteLength));
|
|
303
|
+
}
|
|
304
|
+
if ((crc ^ 4294967295) >>> 0 !== entry.crc32) {
|
|
305
|
+
throw new ZipError("corrupt", `${entry.name}: the data does not match its CRC-32`);
|
|
306
|
+
}
|
|
307
|
+
return data;
|
|
308
|
+
}
|
|
309
|
+
return inflate(data, entry, options);
|
|
310
|
+
}
|
|
311
|
+
function entryData(bytes, entry) {
|
|
312
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
313
|
+
const at = entry.localOffset;
|
|
314
|
+
if (at + LOCAL_SIZE > bytes.byteLength || view.getUint32(at, true) !== LOCAL_SIGNATURE) {
|
|
315
|
+
throw new ZipError("corrupt", `${entry.name}: the local header is missing`);
|
|
316
|
+
}
|
|
317
|
+
const nameLength = view.getUint16(at + 26, true);
|
|
318
|
+
const localName2 = decodeEntryName(
|
|
319
|
+
bytes.subarray(at + LOCAL_SIZE, Math.min(bytes.byteLength, at + LOCAL_SIZE + nameLength)),
|
|
320
|
+
(entry.flags & UTF8_FLAG) === 0
|
|
321
|
+
);
|
|
322
|
+
if (localName2 !== entry.name) {
|
|
323
|
+
throw new ZipError(
|
|
324
|
+
"corrupt",
|
|
325
|
+
`${entry.name}: the local header names the entry ${JSON.stringify(localName2)}, the central directory ${JSON.stringify(entry.name)}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const start = at + LOCAL_SIZE + nameLength + view.getUint16(at + 28, true);
|
|
329
|
+
const end = start + entry.compressedSize;
|
|
330
|
+
if (end > bytes.byteLength) {
|
|
331
|
+
throw new ZipError("corrupt", `${entry.name}: the entry's data runs past the end of the archive (truncated?)`);
|
|
332
|
+
}
|
|
333
|
+
return bytes.subarray(start, end);
|
|
334
|
+
}
|
|
335
|
+
async function inflate(data, entry, options) {
|
|
336
|
+
if (typeof DecompressionStream !== "function") {
|
|
337
|
+
throw new ZipError(
|
|
338
|
+
"unsupported",
|
|
339
|
+
"this runtime has no DecompressionStream, so deflated entries cannot be read"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const member = new Uint8Array(10 + data.byteLength + 8);
|
|
343
|
+
member.set([31, 139, 8, 0, 0, 0, 0, 0, 0, 255]);
|
|
344
|
+
member.set(data, 10);
|
|
345
|
+
const trailer = new DataView(member.buffer, 10 + data.byteLength, 8);
|
|
346
|
+
trailer.setUint32(0, entry.crc32, true);
|
|
347
|
+
trailer.setUint32(4, entry.size % 4294967296, true);
|
|
348
|
+
const source = new ReadableStream({
|
|
349
|
+
start(controller) {
|
|
350
|
+
controller.enqueue(member);
|
|
351
|
+
controller.close();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
const reader = source.pipeThrough(new DecompressionStream("gzip")).getReader();
|
|
355
|
+
const parts = [];
|
|
356
|
+
let total = 0;
|
|
357
|
+
try {
|
|
358
|
+
for (; ; ) {
|
|
359
|
+
throwIfAborted(options.signal);
|
|
360
|
+
let chunk;
|
|
361
|
+
try {
|
|
362
|
+
chunk = await reader.read();
|
|
363
|
+
} catch {
|
|
364
|
+
throw new ZipError(
|
|
365
|
+
"corrupt",
|
|
366
|
+
`${entry.name}: the deflate data is damaged or does not match its CRC-32 and size`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (chunk.done) {
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
total += chunk.value.byteLength;
|
|
373
|
+
if (total > entry.size) {
|
|
374
|
+
throw new ZipError("corrupt", `${entry.name}: the data is longer than its directory size`);
|
|
375
|
+
}
|
|
376
|
+
parts.push(chunk.value);
|
|
377
|
+
options.onBytes?.(total);
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
await reader.cancel().catch(() => void 0);
|
|
381
|
+
}
|
|
382
|
+
if (total !== entry.size) {
|
|
383
|
+
throw new ZipError("corrupt", `${entry.name}: the data is shorter than its directory size`);
|
|
384
|
+
}
|
|
385
|
+
if (parts.length === 1) {
|
|
386
|
+
return parts[0];
|
|
387
|
+
}
|
|
388
|
+
const out = new Uint8Array(total);
|
|
389
|
+
let at = 0;
|
|
390
|
+
for (const part of parts) {
|
|
391
|
+
out.set(part, at);
|
|
392
|
+
at += part.byteLength;
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
const CRC_TABLE = (() => {
|
|
397
|
+
const table = new Uint32Array(256);
|
|
398
|
+
for (let n = 0; n < 256; n++) {
|
|
399
|
+
let c = n;
|
|
400
|
+
for (let k = 0; k < 8; k++) {
|
|
401
|
+
c = (c & 1) === 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
402
|
+
}
|
|
403
|
+
table[n] = c >>> 0;
|
|
404
|
+
}
|
|
405
|
+
return table;
|
|
406
|
+
})();
|
|
407
|
+
function crcUpdate(crc, bytes) {
|
|
408
|
+
let c = crc;
|
|
409
|
+
for (const byte of bytes) {
|
|
410
|
+
c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
|
|
411
|
+
}
|
|
412
|
+
return c >>> 0;
|
|
413
|
+
}
|
|
414
|
+
function crc32(bytes) {
|
|
415
|
+
return (crcUpdate(4294967295, bytes) ^ 4294967295) >>> 0;
|
|
416
|
+
}
|
|
417
|
+
const DOS_DATE = 0 << 9 | 1 << 5 | 1;
|
|
418
|
+
const STORED_VERSION = 10;
|
|
419
|
+
function* writeZip(entries2) {
|
|
420
|
+
const encoder = new TextEncoder();
|
|
421
|
+
const central = [];
|
|
422
|
+
let offset = 0;
|
|
423
|
+
let count = 0;
|
|
424
|
+
for (const entry of entries2) {
|
|
425
|
+
const name = encoder.encode(entry.name);
|
|
426
|
+
const { data } = entry;
|
|
427
|
+
if (offset + LOCAL_SIZE + name.byteLength + data.byteLength >= U32_MAX || count === U16_MAX) {
|
|
428
|
+
throw new GraphFormatError("E_TOO_LARGE", "the archive needs zip64, which graph-io does not write", {
|
|
429
|
+
entry: entry.name
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const crc = crc32(data);
|
|
433
|
+
const local = new Uint8Array(LOCAL_SIZE + name.byteLength);
|
|
434
|
+
const lv = new DataView(local.buffer);
|
|
435
|
+
lv.setUint32(0, LOCAL_SIGNATURE, true);
|
|
436
|
+
lv.setUint16(4, STORED_VERSION, true);
|
|
437
|
+
lv.setUint16(6, UTF8_FLAG, true);
|
|
438
|
+
lv.setUint16(8, 0, true);
|
|
439
|
+
lv.setUint16(10, 0, true);
|
|
440
|
+
lv.setUint16(12, DOS_DATE, true);
|
|
441
|
+
lv.setUint32(14, crc, true);
|
|
442
|
+
lv.setUint32(18, data.byteLength, true);
|
|
443
|
+
lv.setUint32(22, data.byteLength, true);
|
|
444
|
+
lv.setUint16(26, name.byteLength, true);
|
|
445
|
+
lv.setUint16(28, 0, true);
|
|
446
|
+
local.set(name, LOCAL_SIZE);
|
|
447
|
+
const record = new Uint8Array(CENTRAL_SIZE + name.byteLength);
|
|
448
|
+
const cv = new DataView(record.buffer);
|
|
449
|
+
cv.setUint32(0, CENTRAL_SIGNATURE, true);
|
|
450
|
+
cv.setUint16(4, STORED_VERSION, true);
|
|
451
|
+
cv.setUint16(6, STORED_VERSION, true);
|
|
452
|
+
cv.setUint16(8, UTF8_FLAG, true);
|
|
453
|
+
cv.setUint16(10, 0, true);
|
|
454
|
+
cv.setUint16(12, 0, true);
|
|
455
|
+
cv.setUint16(14, DOS_DATE, true);
|
|
456
|
+
cv.setUint32(16, crc, true);
|
|
457
|
+
cv.setUint32(20, data.byteLength, true);
|
|
458
|
+
cv.setUint32(24, data.byteLength, true);
|
|
459
|
+
cv.setUint16(28, name.byteLength, true);
|
|
460
|
+
cv.setUint32(42, offset, true);
|
|
461
|
+
record.set(name, CENTRAL_SIZE);
|
|
462
|
+
central.push(record);
|
|
463
|
+
yield local;
|
|
464
|
+
if (data.byteLength > 0) {
|
|
465
|
+
yield data;
|
|
466
|
+
}
|
|
467
|
+
offset += local.byteLength + data.byteLength;
|
|
468
|
+
count++;
|
|
469
|
+
}
|
|
470
|
+
let size = 0;
|
|
471
|
+
for (const record of central) {
|
|
472
|
+
size += record.byteLength;
|
|
473
|
+
yield record;
|
|
474
|
+
}
|
|
475
|
+
const end = new Uint8Array(EOCD_SIZE);
|
|
476
|
+
const ev = new DataView(end.buffer);
|
|
477
|
+
ev.setUint32(0, EOCD_SIGNATURE, true);
|
|
478
|
+
ev.setUint16(8, count, true);
|
|
479
|
+
ev.setUint16(10, count, true);
|
|
480
|
+
ev.setUint32(12, size, true);
|
|
481
|
+
ev.setUint32(16, offset, true);
|
|
482
|
+
yield end;
|
|
483
|
+
}
|
|
484
|
+
const CYS_LOSS = Object.freeze({
|
|
485
|
+
/** CyCSV has no unset text cell: an unset cell of a text column reads back as "". */
|
|
486
|
+
UNSET_AS_EMPTY_STRING: "W_CYS_UNSET_AS_EMPTY_STRING",
|
|
487
|
+
/**
|
|
488
|
+
* List cells CyCSV cannot hold exactly: an unset or empty list of text reads back as [""], an
|
|
489
|
+
* empty list of numbers or booleans reads back unset, trailing empty text items vanish, and an
|
|
490
|
+
* item holding a newline splits in two.
|
|
491
|
+
*/
|
|
492
|
+
LIST_ITEMS: "W_CYS_LIST_ITEMS",
|
|
493
|
+
/** A text cell starting with "=" is a formula to Cytoscape (an error cell there); graph-io reads it back as text. */
|
|
494
|
+
TEXT_AS_EQUATION: "W_CYS_TEXT_AS_EQUATION",
|
|
495
|
+
/** A nested (json) column is written as a text column holding its JSON text. */
|
|
496
|
+
JSON_AS_STRING: "W_CYS_JSON_AS_STRING",
|
|
497
|
+
/** A position that cannot be written as it is: another shape, a non-finite coordinate, a z read back in the z column. */
|
|
498
|
+
POSITION: "W_CYS_POSITION",
|
|
499
|
+
/**
|
|
500
|
+
* An attribute with a role (for example, the label) is written where the format keeps that role, and reads back under the
|
|
501
|
+
* name the format's importer gives it.
|
|
502
|
+
*/
|
|
503
|
+
COLUMN_NAME_CHANGED: LOSS.COLUMN_NAME_CHANGED,
|
|
504
|
+
/**
|
|
505
|
+
* An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
|
|
506
|
+
* reads back with that role.
|
|
507
|
+
*/
|
|
508
|
+
ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
|
|
509
|
+
/** A mutual pair is written as two directed edges without its mark. */
|
|
510
|
+
MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
|
|
511
|
+
/** Node ids that are numbers read back as their text. */
|
|
512
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
513
|
+
/**
|
|
514
|
+
* Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
|
|
515
|
+
* E_INVALID_ID.
|
|
516
|
+
*/
|
|
517
|
+
ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
|
|
518
|
+
/**
|
|
519
|
+
* Node ids the format cannot write, under `sanitizeIds: "error"`; the save fails with E_INVALID_ID. Pass
|
|
520
|
+
* `sanitizeIds: "mangle"` to rewrite them.
|
|
521
|
+
*/
|
|
522
|
+
ID_CHARSET: LOSS.ID_CHARSET,
|
|
523
|
+
/** Node ids that are not positive integers under sanitizeIds "mangle": renumbered, originals kept. */
|
|
524
|
+
ID_MANGLED: LOSS.ID_MANGLED,
|
|
525
|
+
/** Edges without a usable id get generated SUIDs. */
|
|
526
|
+
EDGE_IDS_GENERATED: LOSS.EDGE_IDS_GENERATED,
|
|
527
|
+
/**
|
|
528
|
+
* An attribute type Cytoscape stores as a wider one (a 32-bit float as Double, a byte as Integer), or a label that
|
|
529
|
+
* is not text and is written as text; it reads back with the Cytoscape type.
|
|
530
|
+
*/
|
|
531
|
+
DTYPE_UNSUPPORTED: LOSS.DTYPE,
|
|
532
|
+
/** A column whose every cell is unset (and which is not text) vanishes. */
|
|
533
|
+
EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
|
|
534
|
+
/**
|
|
535
|
+
* A text attribute reads back as a dictionary attribute, or the reverse, because the importer chooses by how often
|
|
536
|
+
* its values repeat. The values are the same.
|
|
537
|
+
*/
|
|
538
|
+
STORAGE_CLASS_CHANGED: LOSS.STORAGE_CLASS,
|
|
539
|
+
/** A plain `weight` edge column reads back as the edge weight. */
|
|
540
|
+
WEIGHT_KEY_CLASH: LOSS.WEIGHT_KEY_CLASH,
|
|
541
|
+
/** A parent / parents column: groups are not written. */
|
|
542
|
+
HIERARCHY_DROPPED: LOSS.HIERARCHY,
|
|
543
|
+
/** A start / end / timestamp column: sessions have no time. */
|
|
544
|
+
TEMPORAL_DROPPED: LOSS.TEMPORAL,
|
|
545
|
+
/** An attribute with a role the format has no place for is written as a plain attribute; the role is lost. */
|
|
546
|
+
ROLE_DROPPED: LOSS.ROLE,
|
|
547
|
+
/** An extension table the session cannot carry. */
|
|
548
|
+
EXTENSION_TABLE_DROPPED: LOSS.EXTENSION_TABLE
|
|
549
|
+
});
|
|
550
|
+
const CYS_CAPABILITIES = capabilities({
|
|
551
|
+
mixedDirection: true,
|
|
552
|
+
multiEdges: true,
|
|
553
|
+
selfLoops: true,
|
|
554
|
+
edgeIds: "required",
|
|
555
|
+
idCharset: "integer",
|
|
556
|
+
dtypes: ["string", "dict", "f64", "i32", "bool", "list"],
|
|
557
|
+
components: false,
|
|
558
|
+
lists: true,
|
|
559
|
+
json: false,
|
|
560
|
+
defaults: false,
|
|
561
|
+
options: false,
|
|
562
|
+
hierarchy: false,
|
|
563
|
+
temporal: "none",
|
|
564
|
+
graphAttributes: true,
|
|
565
|
+
positions: true,
|
|
566
|
+
viz: false
|
|
567
|
+
});
|
|
568
|
+
const SLOT_ROLES = /* @__PURE__ */ new Set(["label", "id", "position"]);
|
|
569
|
+
const ROLE_NAMES = Object.freeze({
|
|
570
|
+
label: "name",
|
|
571
|
+
id: EDGE_ID_COLUMN,
|
|
572
|
+
position: POSITION_COLUMN
|
|
573
|
+
});
|
|
574
|
+
const SKIPPED_ROLES = /* @__PURE__ */ new Set([
|
|
575
|
+
"directed",
|
|
576
|
+
"pair",
|
|
577
|
+
"mutual",
|
|
578
|
+
"weight",
|
|
579
|
+
"timeText",
|
|
580
|
+
"parent",
|
|
581
|
+
"parents",
|
|
582
|
+
"start",
|
|
583
|
+
"end",
|
|
584
|
+
"timestamp",
|
|
585
|
+
"timestamps",
|
|
586
|
+
"spells",
|
|
587
|
+
"open",
|
|
588
|
+
"spellsOpen",
|
|
589
|
+
"position",
|
|
590
|
+
"color",
|
|
591
|
+
"size",
|
|
592
|
+
"shape",
|
|
593
|
+
"thickness"
|
|
594
|
+
]);
|
|
595
|
+
const BUILT_IN = {
|
|
596
|
+
node: /* @__PURE__ */ new Map([
|
|
597
|
+
["name", "java.lang.String"],
|
|
598
|
+
["shared name", "java.lang.String"],
|
|
599
|
+
["selected", "java.lang.Boolean"]
|
|
600
|
+
]),
|
|
601
|
+
edge: /* @__PURE__ */ new Map([
|
|
602
|
+
["name", "java.lang.String"],
|
|
603
|
+
["shared name", "java.lang.String"],
|
|
604
|
+
["interaction", "java.lang.String"],
|
|
605
|
+
["shared interaction", "java.lang.String"],
|
|
606
|
+
["selected", "java.lang.Boolean"]
|
|
607
|
+
]),
|
|
608
|
+
network: /* @__PURE__ */ new Map([
|
|
609
|
+
["name", "java.lang.String"],
|
|
610
|
+
["shared name", "java.lang.String"],
|
|
611
|
+
["selected", "java.lang.Boolean"],
|
|
612
|
+
["__annotations", "java.util.List<java.lang.String>"]
|
|
613
|
+
])
|
|
614
|
+
};
|
|
615
|
+
const IMPORTER_NAMES = {
|
|
616
|
+
node: /* @__PURE__ */ new Set([
|
|
617
|
+
POSITION_COLUMN,
|
|
618
|
+
Z_COLUMN,
|
|
619
|
+
GRAPHICS_COLUMN,
|
|
620
|
+
PARENT_COLUMN,
|
|
621
|
+
PARENTS_COLUMN,
|
|
622
|
+
SUBGRAPH_COLUMN,
|
|
623
|
+
NESTED_NETWORK_COLUMN,
|
|
624
|
+
NETWORK_POINTER_COLUMN,
|
|
625
|
+
NETWORKS_COLUMN
|
|
626
|
+
]),
|
|
627
|
+
edge: /* @__PURE__ */ new Set([EDGE_ID_COLUMN, GRAPHICS_COLUMN]),
|
|
628
|
+
network: /* @__PURE__ */ new Set([GRAPHICS_COLUMN])
|
|
629
|
+
};
|
|
630
|
+
const STRING = "java.lang.String";
|
|
631
|
+
const DOUBLE = "java.lang.Double";
|
|
632
|
+
const INTEGER = "java.lang.Integer";
|
|
633
|
+
const LONG = "java.lang.Long";
|
|
634
|
+
const BOOLEAN = "java.lang.Boolean";
|
|
635
|
+
const I32_MAX = 2147483647;
|
|
636
|
+
const ROOT_FOLDER = "CytoscapeSession/";
|
|
637
|
+
const DEFAULT_NAME = "Network";
|
|
638
|
+
const SUID_TEXT = /^[1-9]\d*$/;
|
|
639
|
+
function suidOf(id) {
|
|
640
|
+
if (typeof id === "number") {
|
|
641
|
+
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
|
642
|
+
}
|
|
643
|
+
if (SUID_TEXT.test(id)) {
|
|
644
|
+
const n = Number(id);
|
|
645
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
function keptNodeSuids(snapshot) {
|
|
650
|
+
const kept = [];
|
|
651
|
+
const bad = [];
|
|
652
|
+
const used = /* @__PURE__ */ new Set();
|
|
653
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
654
|
+
const suid = suidOf(snapshot.ids.idOf(i));
|
|
655
|
+
if (suid === null || used.has(suid)) {
|
|
656
|
+
kept.push(0);
|
|
657
|
+
bad.push(i);
|
|
658
|
+
} else {
|
|
659
|
+
used.add(suid);
|
|
660
|
+
kept.push(suid);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return { kept, bad };
|
|
664
|
+
}
|
|
665
|
+
function keptEdgeSuids(snapshot, nodeSuids) {
|
|
666
|
+
const column = snapshot.edges.byRole("id");
|
|
667
|
+
const out = new Array(snapshot.edgeCount).fill(0);
|
|
668
|
+
if (column === null) {
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
const used = /* @__PURE__ */ new Set();
|
|
672
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
673
|
+
const value = column.isSet(e) ? column.value(e) : null;
|
|
674
|
+
const suid = typeof value === "number" || typeof value === "string" ? suidOf(value) : null;
|
|
675
|
+
if (suid !== null && !used.has(suid) && !nodeSuids.has(suid)) {
|
|
676
|
+
used.add(suid);
|
|
677
|
+
out[e] = suid;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
function scalarClass(column, dtype) {
|
|
683
|
+
switch (dtype) {
|
|
684
|
+
case "bool":
|
|
685
|
+
return BOOLEAN;
|
|
686
|
+
case "i32":
|
|
687
|
+
case "u8":
|
|
688
|
+
return INTEGER;
|
|
689
|
+
case "u32":
|
|
690
|
+
return maxValue(column) > I32_MAX ? LONG : INTEGER;
|
|
691
|
+
case "f32":
|
|
692
|
+
case "f64":
|
|
693
|
+
return /long$/i.test(column.meta.origin?.type ?? "") && allSafeIntegers(column) ? LONG : DOUBLE;
|
|
694
|
+
default:
|
|
695
|
+
return STRING;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function itemsOf(column, row) {
|
|
699
|
+
return column.dtype === "list" ? [...column.sliceOf(row)] : [column.value(row)];
|
|
700
|
+
}
|
|
701
|
+
function allSafeIntegers(column) {
|
|
702
|
+
for (let r = 0; r < column.length; r++) {
|
|
703
|
+
if (column.isSet(r) && !itemsOf(column, r).every((v) => typeof v === "number" && Number.isSafeInteger(v))) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
function maxValue(column) {
|
|
710
|
+
let max = 0;
|
|
711
|
+
for (let r = 0; r < column.length; r++) {
|
|
712
|
+
if (column.isSet(r)) {
|
|
713
|
+
for (const v of itemsOf(column, r)) {
|
|
714
|
+
if (typeof v === "number" && v > max) {
|
|
715
|
+
max = v;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return max;
|
|
721
|
+
}
|
|
722
|
+
function valueText(value, javaClass, dtype) {
|
|
723
|
+
if (javaClass === BOOLEAN) {
|
|
724
|
+
return value === true ? "true" : "false";
|
|
725
|
+
}
|
|
726
|
+
if (javaClass === DOUBLE) {
|
|
727
|
+
return formatDecimal(value, dtype === "f32" ? "f32" : "f64");
|
|
728
|
+
}
|
|
729
|
+
if (javaClass === INTEGER || javaClass === LONG) {
|
|
730
|
+
return String(value);
|
|
731
|
+
}
|
|
732
|
+
return textOf(value);
|
|
733
|
+
}
|
|
734
|
+
function textOf(value) {
|
|
735
|
+
if (typeof value === "string") {
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
if (typeof value === "number") {
|
|
739
|
+
return formatF64(value);
|
|
740
|
+
}
|
|
741
|
+
if (typeof value === "boolean") {
|
|
742
|
+
return value ? "true" : "false";
|
|
743
|
+
}
|
|
744
|
+
if (ArrayBuffer.isView(value)) {
|
|
745
|
+
return JSON.stringify(Array.from(value).map(finiteOrText));
|
|
746
|
+
}
|
|
747
|
+
return JSON.stringify(value) ?? "";
|
|
748
|
+
}
|
|
749
|
+
function finiteOrText(v) {
|
|
750
|
+
return Number.isFinite(v) ? v : String(v);
|
|
751
|
+
}
|
|
752
|
+
function candidateOf(column, name) {
|
|
753
|
+
const { meta } = column;
|
|
754
|
+
if (column.dtype === "list") {
|
|
755
|
+
const item = meta.itemDtype;
|
|
756
|
+
const itemClass = scalarClass(column, item === "json" ? null : item);
|
|
757
|
+
return {
|
|
758
|
+
name,
|
|
759
|
+
source: column,
|
|
760
|
+
javaClass: `java.util.List<${itemClass}>`,
|
|
761
|
+
cell: (row) => column.isSet(row) ? [...column.sliceOf(row)].map((v) => valueText(v, itemClass, item)).join("\n") : null
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const javaClass = meta.dtype === "json" || meta.components > 1 ? STRING : scalarClass(column, meta.dtype);
|
|
765
|
+
return {
|
|
766
|
+
name,
|
|
767
|
+
source: column,
|
|
768
|
+
javaClass,
|
|
769
|
+
cell: (row) => column.isSet(row) ? valueText(column.value(row), javaClass, meta.dtype) : null
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function settleNames(domain, candidates, note) {
|
|
773
|
+
const taken = /* @__PURE__ */ new Set(["suid"]);
|
|
774
|
+
const out = [];
|
|
775
|
+
for (const c of candidates) {
|
|
776
|
+
const lower = c.name.toLowerCase();
|
|
777
|
+
const builtIn = BUILT_IN[domain].get(lower);
|
|
778
|
+
let { name } = c;
|
|
779
|
+
const clash = builtIn !== void 0 && builtIn !== c.javaClass ? builtIn : null;
|
|
780
|
+
if (taken.has(lower) || clash !== null) {
|
|
781
|
+
let k = 2;
|
|
782
|
+
while (taken.has(`${lower}#${k}`)) {
|
|
783
|
+
k++;
|
|
784
|
+
}
|
|
785
|
+
name = `${c.name}#${k}`;
|
|
786
|
+
const why = renameReason(lower, clash);
|
|
787
|
+
const set = c.source === null ? null : c.source.length - c.source.nullCount;
|
|
788
|
+
note(LOSS.COLUMN_NAME_CHANGED, `${domain} column "${c.name}" is written as "${name}": ${why}`, c.name, set);
|
|
789
|
+
} else if (c.source !== null && IMPORTER_NAMES[domain].has(c.name)) {
|
|
790
|
+
note(
|
|
791
|
+
LOSS.COLUMN_NAME_CHANGED,
|
|
792
|
+
`${domain} column "${c.name}" is written to the session's table and reads back as "${c.name}#2": graph-io's session reader makes a "${c.name}" column of its own`,
|
|
793
|
+
c.name,
|
|
794
|
+
c.source.length - c.source.nullCount
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
taken.add(name.toLowerCase());
|
|
798
|
+
out.push({ name, javaClass: c.javaClass, cell: c.cell });
|
|
799
|
+
}
|
|
800
|
+
return out;
|
|
801
|
+
}
|
|
802
|
+
function renameReason(lower, clash) {
|
|
803
|
+
if (lower === "suid") {
|
|
804
|
+
return "SUID is Cytoscape's key column";
|
|
805
|
+
}
|
|
806
|
+
if (clash !== null) {
|
|
807
|
+
return `Cytoscape's own "${lower}" column holds ${clash}`;
|
|
808
|
+
}
|
|
809
|
+
return "its name is taken (Cytoscape column names ignore case)";
|
|
810
|
+
}
|
|
811
|
+
function javaSplit(text) {
|
|
812
|
+
if (!text.includes("\n")) {
|
|
813
|
+
return [text];
|
|
814
|
+
}
|
|
815
|
+
const items = text.split("\n");
|
|
816
|
+
while (items.at(-1) === "") {
|
|
817
|
+
items.pop();
|
|
818
|
+
}
|
|
819
|
+
return items;
|
|
820
|
+
}
|
|
821
|
+
function cellNotes(domain, c, rows, note) {
|
|
822
|
+
const { source } = c;
|
|
823
|
+
if (source === null) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const label = `${domain} column "${source.meta.name}"`;
|
|
827
|
+
const { name } = source.meta;
|
|
828
|
+
const text = c.javaClass === STRING;
|
|
829
|
+
const { unset, equations, lists, set, heuristic } = cellCounts(c, source, rows);
|
|
830
|
+
if (source.dtype === "json") {
|
|
831
|
+
note(CYS_LOSS.JSON_AS_STRING, `${label} holds nested values; written as their JSON text`, name, set);
|
|
832
|
+
}
|
|
833
|
+
if (set === 0 && (!text || rows === 0)) {
|
|
834
|
+
note(LOSS.EMPTY_COLUMN, `${label} has no value; it is not in the re-import`, name, null);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (text && unset > 0) {
|
|
838
|
+
note(
|
|
839
|
+
CYS_LOSS.UNSET_AS_EMPTY_STRING,
|
|
840
|
+
`${label}: ${unset} unset cell${plural(unset)} ${agree(unset, "is", "are")} written empty and read back as ""`,
|
|
841
|
+
name,
|
|
842
|
+
unset
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (lists > 0) {
|
|
846
|
+
note(
|
|
847
|
+
CYS_LOSS.LIST_ITEMS,
|
|
848
|
+
`${label}: ${lists} list cell${plural(lists)} ${agree(lists, "reads", "read")} back changed (an unset or empty list of text as [""], an empty list of numbers unset, trailing empty items dropped, items holding a newline split)`,
|
|
849
|
+
name,
|
|
850
|
+
lists
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (equations > 0) {
|
|
854
|
+
note(
|
|
855
|
+
CYS_LOSS.TEXT_AS_EQUATION,
|
|
856
|
+
`${label}: ${equations} cell${plural(equations)} start with "=", which Cytoscape reads as a formula (an error cell); graph-io reads them back as text`,
|
|
857
|
+
name,
|
|
858
|
+
equations
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
if (text && (source.dtype === "string" || source.dtype === "dict")) {
|
|
862
|
+
const readsAs = heuristic.decide();
|
|
863
|
+
if (readsAs !== source.dtype) {
|
|
864
|
+
note(LOSS.STORAGE_CLASS, `${label} reads back as ${readsAs} (the cardinality heuristic)`, name, null);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function cellCounts(c, source, rows) {
|
|
869
|
+
const text = c.javaClass === STRING;
|
|
870
|
+
const stringList = c.javaClass === `java.util.List<${STRING}>`;
|
|
871
|
+
const counts = { unset: 0, equations: 0, lists: 0, set: 0, heuristic: new DictHeuristic() };
|
|
872
|
+
for (let r = 0; r < rows; r++) {
|
|
873
|
+
const cell = c.cell(r);
|
|
874
|
+
if (cell === null) {
|
|
875
|
+
counts.unset++;
|
|
876
|
+
} else {
|
|
877
|
+
counts.set++;
|
|
878
|
+
}
|
|
879
|
+
const written = cell ?? "";
|
|
880
|
+
if (text && !counts.heuristic.decided) {
|
|
881
|
+
counts.heuristic.observe(written);
|
|
882
|
+
}
|
|
883
|
+
if (written.startsWith("=")) {
|
|
884
|
+
counts.equations++;
|
|
885
|
+
} else if (source.dtype === "list" && listChanges(source, r, cell, stringList)) {
|
|
886
|
+
counts.lists++;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return counts;
|
|
890
|
+
}
|
|
891
|
+
function listChanges(source, r, cell, stringList) {
|
|
892
|
+
const written = cell ?? "";
|
|
893
|
+
let back = null;
|
|
894
|
+
if (stringList) {
|
|
895
|
+
back = written.length === 0 ? [""] : javaSplit(written);
|
|
896
|
+
} else if (written.length > 0) {
|
|
897
|
+
back = javaSplit(written).filter((item) => item.length > 0);
|
|
898
|
+
}
|
|
899
|
+
const items = cell === null ? null : [...source.sliceOf(r)].map((v) => valueText(v, "", null));
|
|
900
|
+
if (back === null || items === null) {
|
|
901
|
+
return back !== items;
|
|
902
|
+
}
|
|
903
|
+
return back.length !== items.length || stringList && back.some((b, i) => b !== items[i]);
|
|
904
|
+
}
|
|
905
|
+
function isZ(column) {
|
|
906
|
+
return column.meta.name === Z_COLUMN && column.meta.dtype === "f64" && column.meta.role === null;
|
|
907
|
+
}
|
|
908
|
+
function elementCandidates(snapshot, domain, own, skip, note) {
|
|
909
|
+
const table = domain === "node" ? snapshot.nodes : snapshot.edges;
|
|
910
|
+
const label = table.byRole("label");
|
|
911
|
+
const out = [];
|
|
912
|
+
if (label !== null) {
|
|
913
|
+
const c = candidateOf(label, "name");
|
|
914
|
+
out.push({ ...c, javaClass: STRING, cell: (row) => label.isSet(row) ? textOf(label.value(row)) : null });
|
|
915
|
+
if (label.dtype !== "string" && label.dtype !== "dict") {
|
|
916
|
+
note(
|
|
917
|
+
LOSS.DTYPE,
|
|
918
|
+
`${domain} label column "${label.meta.name}" (${label.dtype}) is written as text and reads back as string`,
|
|
919
|
+
label.meta.name,
|
|
920
|
+
label.length - label.nullCount
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
out.push(...own);
|
|
925
|
+
for (const column of table) {
|
|
926
|
+
const { role } = column.meta;
|
|
927
|
+
if (column === label || role !== null && (SKIPPED_ROLES.has(role) || role === "id") || skip(column)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
out.push(candidateOf(column, column.meta.name));
|
|
931
|
+
if (label === null && role === null && column.meta.name === "name" && scalarClass(column, column.dtype) === STRING) {
|
|
932
|
+
note(
|
|
933
|
+
LOSS.ROLE_ASSUMED,
|
|
934
|
+
`${domain} column "name" is Cytoscape's name and reads back as the label`,
|
|
935
|
+
"name",
|
|
936
|
+
column.length - column.nullCount
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
function planTable(domain, candidates, rows, note) {
|
|
943
|
+
for (const c of candidates) {
|
|
944
|
+
cellNotes(domain, c, rows, note);
|
|
945
|
+
}
|
|
946
|
+
return settleNames(domain, candidates, note);
|
|
947
|
+
}
|
|
948
|
+
function idTextNotes(snapshot, note) {
|
|
949
|
+
let collisions = 0;
|
|
950
|
+
let numeric = 0;
|
|
951
|
+
const texts = /* @__PURE__ */ new Set();
|
|
952
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
953
|
+
const id = snapshot.ids.idOf(i);
|
|
954
|
+
numeric += typeof id === "number" ? 1 : 0;
|
|
955
|
+
const text = String(id);
|
|
956
|
+
if (texts.has(text)) {
|
|
957
|
+
collisions++;
|
|
958
|
+
}
|
|
959
|
+
texts.add(text);
|
|
960
|
+
}
|
|
961
|
+
let fatal = null;
|
|
962
|
+
if (collisions > 0) {
|
|
963
|
+
note(
|
|
964
|
+
LOSS.ID_TEXT_COLLISION,
|
|
965
|
+
`${collisions} node id${plural(collisions)} ${agree(collisions, "shares", "share")} their text with another id (a number and a string); the save fails with E_INVALID_ID`,
|
|
966
|
+
null,
|
|
967
|
+
collisions
|
|
968
|
+
);
|
|
969
|
+
fatal = new GraphFormatError(
|
|
970
|
+
"E_INVALID_ID",
|
|
971
|
+
`${collisions} node id${plural(collisions)} ${agree(collisions, "shares", "share")} their text with another id`,
|
|
972
|
+
{ reason: "collision", count: collisions }
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (numeric > 0) {
|
|
976
|
+
note(
|
|
977
|
+
LOSS.ID_TEXT_TYPE,
|
|
978
|
+
`${numeric} numeric node id${plural(numeric)} ${agree(numeric, "reads", "read")} back as their text`,
|
|
979
|
+
null,
|
|
980
|
+
numeric
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return fatal;
|
|
984
|
+
}
|
|
985
|
+
function badIdNotes(snapshot, bad, mangle, note) {
|
|
986
|
+
if (mangle) {
|
|
987
|
+
note(
|
|
988
|
+
LOSS.ID_MANGLED,
|
|
989
|
+
`${bad.length} node id${plural(bad.length)} that ${agree(bad.length, "is", "are")} not positive integers ${agree(bad.length, "is", "are")} renumbered, since Cytoscape ids are positive integers; the originals are kept in the "${ORIGINAL_ID_ATTRIBUTE}" column (restored by restoreMangledIds)`,
|
|
990
|
+
null,
|
|
991
|
+
bad.length
|
|
992
|
+
);
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
note(
|
|
996
|
+
LOSS.ID_CHARSET,
|
|
997
|
+
`${bad.length} node id${plural(bad.length)} ${agree(bad.length, "is", "are")} not positive integers, which Cytoscape requires; the save fails unless sanitizeIds is "mangle"`,
|
|
998
|
+
null,
|
|
999
|
+
bad.length
|
|
1000
|
+
);
|
|
1001
|
+
const first = snapshot.ids.idOf(bad[0]);
|
|
1002
|
+
return new GraphFormatError(
|
|
1003
|
+
"E_INVALID_ID",
|
|
1004
|
+
`${bad.length} node id${plural(bad.length)} cannot be written as Cytoscape ids, which are positive integers (first: ${JSON.stringify(first)} at index ${bad[0]}); pass sanitizeIds: "mangle" to rewrite them`,
|
|
1005
|
+
{ reason: "charset", charset: "integer", count: bad.length, index: bad[0] }
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
function planIds(snapshot, common, note) {
|
|
1009
|
+
const { kept, bad } = keptNodeSuids(snapshot);
|
|
1010
|
+
const collision = idTextNotes(snapshot, note);
|
|
1011
|
+
const charset = bad.length > 0 ? badIdNotes(snapshot, bad, common.sanitizeIds === "mangle", note) : null;
|
|
1012
|
+
const fatal = collision ?? charset;
|
|
1013
|
+
const nodeSet = new Set(kept.filter((s) => s > 0));
|
|
1014
|
+
const edgeKept = keptEdgeSuids(snapshot, nodeSet);
|
|
1015
|
+
let next = 1;
|
|
1016
|
+
for (const s of [...nodeSet, ...edgeKept]) {
|
|
1017
|
+
next = Math.max(next, s + 1);
|
|
1018
|
+
}
|
|
1019
|
+
const nodeSuids = kept.map((s) => s > 0 ? s : next++);
|
|
1020
|
+
const idColumn = snapshot.edges.byRole("id");
|
|
1021
|
+
let generated = 0;
|
|
1022
|
+
const edgeSuids = edgeKept.map((s) => {
|
|
1023
|
+
if (s > 0) {
|
|
1024
|
+
return s;
|
|
1025
|
+
}
|
|
1026
|
+
generated++;
|
|
1027
|
+
return next++;
|
|
1028
|
+
});
|
|
1029
|
+
if (idColumn !== null && generated > 0) {
|
|
1030
|
+
note(
|
|
1031
|
+
LOSS.EDGE_IDS_GENERATED,
|
|
1032
|
+
`${generated} edge${plural(generated)} ${agree(generated, "has", "have")} no distinct positive integer id in "${idColumn.meta.name}"; they are written with new positive integer ids`,
|
|
1033
|
+
idColumn.meta.name,
|
|
1034
|
+
generated
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (idColumn !== null && idColumn.dtype !== "string") {
|
|
1038
|
+
note(
|
|
1039
|
+
LOSS.DTYPE,
|
|
1040
|
+
`edge id column "${idColumn.meta.name}" is ${idColumn.dtype}; edge ids read back as text`,
|
|
1041
|
+
idColumn.meta.name,
|
|
1042
|
+
snapshot.edgeCount - idColumn.nullCount
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
return { nodeSuids, edgeSuids, next, fatal, mangled: common.sanitizeIds === "mangle" && bad.length > 0 };
|
|
1046
|
+
}
|
|
1047
|
+
function networkCandidates(snapshot, name) {
|
|
1048
|
+
const out = [];
|
|
1049
|
+
const graphName = snapshot.graph.get("name");
|
|
1050
|
+
if (graphName === null || graphName.dtype !== "string" && graphName.dtype !== "dict") {
|
|
1051
|
+
out.push({ name: "name", source: null, javaClass: STRING, cell: () => name });
|
|
1052
|
+
}
|
|
1053
|
+
const { description } = snapshot.meta;
|
|
1054
|
+
if (description !== null && snapshot.graph.get("description") === null) {
|
|
1055
|
+
out.push({ name: "description", source: null, javaClass: STRING, cell: () => description });
|
|
1056
|
+
}
|
|
1057
|
+
for (const column of snapshot.graph) {
|
|
1058
|
+
out.push(candidateOf(column, column.meta.name));
|
|
1059
|
+
}
|
|
1060
|
+
return out;
|
|
1061
|
+
}
|
|
1062
|
+
function plan(snapshot, common) {
|
|
1063
|
+
const notes = [];
|
|
1064
|
+
const note = (code, message, column = null, count = null) => {
|
|
1065
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
1066
|
+
};
|
|
1067
|
+
for (const gen of checkCapabilities(snapshot, CYS_CAPABILITIES, common, {
|
|
1068
|
+
roles: SLOT_ROLES,
|
|
1069
|
+
roleNames: ROLE_NAMES,
|
|
1070
|
+
positionDtype: "f32"
|
|
1071
|
+
})) {
|
|
1072
|
+
if (gen.code !== LOSS.JSON && gen.code !== LOSS.ID_CHARSET && gen.code !== LOSS.ID_MANGLED) {
|
|
1073
|
+
notes.push(gen);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const ids = planIds(snapshot, common, note);
|
|
1077
|
+
const { nodeSuids, edgeSuids, mangled, fatal } = ids;
|
|
1078
|
+
let { next } = ids;
|
|
1079
|
+
const rootSuid = next++;
|
|
1080
|
+
const subSuid = next++;
|
|
1081
|
+
const folding = pairFolding(snapshot);
|
|
1082
|
+
if (folding.mutualCount > 0) {
|
|
1083
|
+
note(
|
|
1084
|
+
LOSS.MUTUAL_EXPANDED,
|
|
1085
|
+
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed edges; the mutual mark is lost`,
|
|
1086
|
+
null,
|
|
1087
|
+
folding.mutualCount
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
const position = snapshot.nodes.byRole("position");
|
|
1091
|
+
const z = [...snapshot.nodes].find(isZ) ?? null;
|
|
1092
|
+
const positionZ = position !== null && z === null;
|
|
1093
|
+
if (position !== null) {
|
|
1094
|
+
positionNotes(position, z, note);
|
|
1095
|
+
}
|
|
1096
|
+
const placed = position !== null && position.nullCount < position.length || z !== null && z.nullCount < z.length;
|
|
1097
|
+
const viewSuid = placed ? next++ : null;
|
|
1098
|
+
const viewNodeBase = next;
|
|
1099
|
+
const nodeOwn = mangled ? [
|
|
1100
|
+
{
|
|
1101
|
+
name: ORIGINAL_ID_ATTRIBUTE,
|
|
1102
|
+
source: null,
|
|
1103
|
+
javaClass: STRING,
|
|
1104
|
+
// every node carries its original, so an empty id ("") is told apart from an unset cell
|
|
1105
|
+
cell: (row) => String(snapshot.ids.idOf(row))
|
|
1106
|
+
}
|
|
1107
|
+
] : [];
|
|
1108
|
+
const nodeTable = planTable(
|
|
1109
|
+
"node",
|
|
1110
|
+
elementCandidates(snapshot, "node", nodeOwn, (c) => placed && c === z, note),
|
|
1111
|
+
snapshot.nodeCount,
|
|
1112
|
+
note
|
|
1113
|
+
);
|
|
1114
|
+
const weights = explicitWeights(snapshot);
|
|
1115
|
+
const plainWeight = snapshot.edges.get("weight");
|
|
1116
|
+
if (plainWeight !== null && plainWeight.meta.role === null && !weights.weighted) {
|
|
1117
|
+
note(LOSS.WEIGHT_KEY_CLASH, `edge column "weight" reads back as the edge weight`, "weight", null);
|
|
1118
|
+
}
|
|
1119
|
+
const edgeOwn = weights.weighted ? [
|
|
1120
|
+
{
|
|
1121
|
+
name: "weight",
|
|
1122
|
+
source: null,
|
|
1123
|
+
javaClass: DOUBLE,
|
|
1124
|
+
cell: (e) => weights.isExplicit(e) ? formatDecimal(weights.value(e)) : null
|
|
1125
|
+
}
|
|
1126
|
+
] : [];
|
|
1127
|
+
const edgeRows = snapshot.edgeCount;
|
|
1128
|
+
const edgeTable = planTable(
|
|
1129
|
+
"edge",
|
|
1130
|
+
elementCandidates(snapshot, "edge", edgeOwn, () => false, note),
|
|
1131
|
+
edgeRows,
|
|
1132
|
+
note
|
|
1133
|
+
);
|
|
1134
|
+
const name = snapshot.meta.name ?? DEFAULT_NAME;
|
|
1135
|
+
const networkTable = planTable("network", networkCandidates(snapshot, name), 1, note);
|
|
1136
|
+
const rootTable = [{ name: "name", javaClass: STRING, cell: () => name }];
|
|
1137
|
+
return {
|
|
1138
|
+
notes,
|
|
1139
|
+
fatal,
|
|
1140
|
+
nodeSuids,
|
|
1141
|
+
edgeSuids,
|
|
1142
|
+
rootSuid,
|
|
1143
|
+
subSuid,
|
|
1144
|
+
viewSuid,
|
|
1145
|
+
viewNodeBase,
|
|
1146
|
+
name,
|
|
1147
|
+
folding,
|
|
1148
|
+
nodeTable,
|
|
1149
|
+
edgeTable,
|
|
1150
|
+
networkTable,
|
|
1151
|
+
rootTable,
|
|
1152
|
+
position,
|
|
1153
|
+
z: placed ? z : null,
|
|
1154
|
+
positionZ
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function positionNotes(position, z, note) {
|
|
1158
|
+
const { name, components } = position.meta;
|
|
1159
|
+
if (components !== 3) {
|
|
1160
|
+
note(
|
|
1161
|
+
CYS_LOSS.POSITION,
|
|
1162
|
+
`position column "${name}" has ${components} component${plural(components)} and reads back with 3`,
|
|
1163
|
+
name,
|
|
1164
|
+
null
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
let nonFinite = 0;
|
|
1168
|
+
let depth = 0;
|
|
1169
|
+
for (let r = 0; r < position.length; r++) {
|
|
1170
|
+
if (!position.isSet(r)) {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const xyz = Array.from(position.value(r));
|
|
1174
|
+
if (xyz.slice(0, 2).some((v) => !Number.isFinite(v))) {
|
|
1175
|
+
nonFinite++;
|
|
1176
|
+
} else if ((xyz[2] ?? 0) !== 0) {
|
|
1177
|
+
depth++;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (nonFinite > 0) {
|
|
1181
|
+
note(
|
|
1182
|
+
CYS_LOSS.POSITION,
|
|
1183
|
+
`${nonFinite} position${plural(nonFinite)} with a non-finite coordinate ${agree(nonFinite, "is", "are")} not written`,
|
|
1184
|
+
name,
|
|
1185
|
+
nonFinite
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
if (depth > 0) {
|
|
1189
|
+
note(
|
|
1190
|
+
CYS_LOSS.POSITION,
|
|
1191
|
+
z === null ? `${depth} position${plural(depth)} ${agree(depth, "has", "have")} a z; it is written as the view's z and reads back in the z column` : `${depth} position${plural(depth)} ${agree(depth, "has", "have")} a z, but the view's z holds the z column; the position z is lost`,
|
|
1192
|
+
name,
|
|
1193
|
+
depth
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function sessionEscape(text) {
|
|
1198
|
+
const wellFormed = text.replaceAll(new RegExp("[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]", "g"), "�");
|
|
1199
|
+
return encodeURIComponent(wellFormed).replaceAll(/[!~'()]/g, (c) => `%${(c.codePointAt(0) ?? 0).toString(16).toUpperCase()}`).replaceAll("%20", "+").replaceAll("-", "%2D");
|
|
1200
|
+
}
|
|
1201
|
+
function* cyTable(title, columns, rows) {
|
|
1202
|
+
const line = (cells) => `${cells.map((c) => quoteCsvCell(c)).join(",")}
|
|
1203
|
+
`;
|
|
1204
|
+
yield line(["CyCSV-Version", "1"]);
|
|
1205
|
+
yield line(["SUID", ...columns.map((c) => c.name)]);
|
|
1206
|
+
yield line([LONG, ...columns.map((c) => c.javaClass)]);
|
|
1207
|
+
yield line(["", ...columns.map(() => "mutable")]);
|
|
1208
|
+
yield line([title, ""]);
|
|
1209
|
+
for (const [suid, row] of rows) {
|
|
1210
|
+
yield line([String(suid), ...columns.map((c) => c.cell(row) ?? "")]);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
1214
|
+
const NAMESPACES = `xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xlink="${XLINK_NAMESPACE}" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cy="${CY_NAMESPACE}" xmlns="${XGMML_NAMESPACE}"`;
|
|
1215
|
+
function* networkFile(snapshot, p) {
|
|
1216
|
+
const label = !hasIllegalXmlChar(p.name) ? ` label="${escapeXmlAttribute(p.name)}"` : "";
|
|
1217
|
+
yield XML_DECLARATION;
|
|
1218
|
+
yield `<graph id="${p.rootSuid}"${label} directed="${snapshot.directed ? "1" : "0"}" cy:view="0" cy:registered="0" cy:documentVersion="3.0" ${NAMESPACES}>
|
|
1219
|
+
`;
|
|
1220
|
+
yield ` <att>
|
|
1221
|
+
<graph id="${p.subSuid}"${label} cy:registered="1">
|
|
1222
|
+
`;
|
|
1223
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1224
|
+
yield ` <node id="${p.nodeSuids[i]}"/>
|
|
1225
|
+
`;
|
|
1226
|
+
}
|
|
1227
|
+
const { src, dst } = snapshot.edgeList();
|
|
1228
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1229
|
+
if (p.folding.folded(e)) {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const directed = snapshot.directed && p.folding.sourceDirected(e);
|
|
1233
|
+
yield ` <edge id="${p.edgeSuids[e]}" source="${p.nodeSuids[src[e]]}" target="${p.nodeSuids[dst[e]]}" cy:directed="${directed ? "1" : "0"}"/>
|
|
1234
|
+
`;
|
|
1235
|
+
}
|
|
1236
|
+
yield " </graph>\n </att>\n</graph>\n";
|
|
1237
|
+
}
|
|
1238
|
+
function* viewFile(snapshot, p, viewSuid) {
|
|
1239
|
+
yield XML_DECLARATION;
|
|
1240
|
+
yield `<graph id="${viewSuid}" label="${viewSuid}" cy:view="1" cy:networkId="${p.subSuid}" cy:visualStyle="default" cy:rendererId="org.cytoscape.ding" cy:documentVersion="3.0" ${NAMESPACES}>
|
|
1241
|
+
`;
|
|
1242
|
+
let view = p.viewNodeBase;
|
|
1243
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1244
|
+
const coords = viewCoords(p, i);
|
|
1245
|
+
if (coords.length > 0) {
|
|
1246
|
+
yield ` <node id="${view++}" cy:nodeId="${p.nodeSuids[i]}">
|
|
1247
|
+
<graphics ${coords.join(" ")}/>
|
|
1248
|
+
</node>
|
|
1249
|
+
`;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
yield "</graph>\n";
|
|
1253
|
+
}
|
|
1254
|
+
function viewCoords(p, i) {
|
|
1255
|
+
const coords = [];
|
|
1256
|
+
if (p.position?.isSet(i) === true) {
|
|
1257
|
+
const [x, y, z] = Array.from(p.position.value(i));
|
|
1258
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
1259
|
+
coords.push(`x="${formatF32(x)}"`, `y="${formatF32(y === 0 ? 0 : -y)}"`);
|
|
1260
|
+
}
|
|
1261
|
+
if (Number.isFinite(x) && Number.isFinite(y) && p.positionZ && z !== void 0 && z !== 0 && Number.isFinite(z)) {
|
|
1262
|
+
coords.push(`z="${formatF32(z)}"`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (p.z?.isSet(i) === true) {
|
|
1266
|
+
coords.push(`z="${formatDecimal(p.z.value(i))}"`);
|
|
1267
|
+
}
|
|
1268
|
+
return coords;
|
|
1269
|
+
}
|
|
1270
|
+
function encode(parts) {
|
|
1271
|
+
let text = "";
|
|
1272
|
+
for (const part of parts) {
|
|
1273
|
+
text += part;
|
|
1274
|
+
}
|
|
1275
|
+
return new TextEncoder().encode(text);
|
|
1276
|
+
}
|
|
1277
|
+
function* entries(snapshot, p) {
|
|
1278
|
+
if (p.fatal !== null) {
|
|
1279
|
+
throw p.fatal;
|
|
1280
|
+
}
|
|
1281
|
+
const name = sessionEscape(p.name);
|
|
1282
|
+
const tables = `${ROOT_FOLDER}tables/${p.subSuid}-${name}/LOCAL_ATTRS-org.cytoscape.model.`;
|
|
1283
|
+
const rowsOf = (suids, skip) => suids.flatMap((suid, row) => skip?.(row) === true ? [] : [[suid, row]]);
|
|
1284
|
+
const nodeTable = `${p.name} default node`;
|
|
1285
|
+
const edgeTable = `${p.name} default edge`;
|
|
1286
|
+
const networkTable = `${p.name} default network`;
|
|
1287
|
+
yield { name: `${ROOT_FOLDER}3.0.0.version`, data: new Uint8Array(0) };
|
|
1288
|
+
yield { name: `${ROOT_FOLDER}networks/${p.rootSuid}-${name}.xgmml`, data: encode(networkFile(snapshot, p)) };
|
|
1289
|
+
yield {
|
|
1290
|
+
name: `${tables}CyNode-${sessionEscape(nodeTable)}.cytable`,
|
|
1291
|
+
data: encode(cyTable(nodeTable, p.nodeTable, rowsOf(p.nodeSuids)))
|
|
1292
|
+
};
|
|
1293
|
+
yield {
|
|
1294
|
+
name: `${tables}CyEdge-${sessionEscape(edgeTable)}.cytable`,
|
|
1295
|
+
data: encode(
|
|
1296
|
+
cyTable(
|
|
1297
|
+
edgeTable,
|
|
1298
|
+
p.edgeTable,
|
|
1299
|
+
rowsOf(p.edgeSuids, (e) => p.folding.folded(e))
|
|
1300
|
+
)
|
|
1301
|
+
)
|
|
1302
|
+
};
|
|
1303
|
+
yield {
|
|
1304
|
+
name: `${tables}CyNetwork-${sessionEscape(networkTable)}.cytable`,
|
|
1305
|
+
data: encode(cyTable(networkTable, p.networkTable, [[p.subSuid, 0]]))
|
|
1306
|
+
};
|
|
1307
|
+
yield {
|
|
1308
|
+
name: `${ROOT_FOLDER}tables/${p.rootSuid}-${name}/LOCAL_ATTRS-org.cytoscape.model.CyNetwork-${p.rootSuid}+default+network.cytable`,
|
|
1309
|
+
data: encode(cyTable(`${p.rootSuid} default network`, p.rootTable, [[p.rootSuid, 0]]))
|
|
1310
|
+
};
|
|
1311
|
+
if (p.viewSuid !== null) {
|
|
1312
|
+
yield {
|
|
1313
|
+
name: `${ROOT_FOLDER}views/${p.subSuid}-${p.viewSuid}-${name}.xgmml`,
|
|
1314
|
+
data: encode(viewFile(snapshot, p, p.viewSuid))
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
async function* write(snapshot, options) {
|
|
1319
|
+
const p = plan(snapshot, resolveExportOptions(options));
|
|
1320
|
+
await Promise.resolve();
|
|
1321
|
+
yield* writeZip(entries(snapshot, p));
|
|
1322
|
+
}
|
|
1323
|
+
const cysExporter = Object.freeze({
|
|
1324
|
+
format: FORMAT,
|
|
1325
|
+
options: Object.freeze([]),
|
|
1326
|
+
capabilities: CYS_CAPABILITIES,
|
|
1327
|
+
/**
|
|
1328
|
+
* Pre-flight: what export() would lose, without writing anything.
|
|
1329
|
+
* @param snapshot - the snapshot
|
|
1330
|
+
* @param options - the common options
|
|
1331
|
+
* @returns the notes, empty when the export is exact
|
|
1332
|
+
*/
|
|
1333
|
+
check(snapshot, options) {
|
|
1334
|
+
return Object.freeze([...plan(snapshot, resolveExportOptions(options)).notes]);
|
|
1335
|
+
},
|
|
1336
|
+
/**
|
|
1337
|
+
* Write the session as zip bytes (stored entries, one at a time).
|
|
1338
|
+
* @param snapshot - the snapshot
|
|
1339
|
+
* @param options - the common options
|
|
1340
|
+
* @returns the bytes
|
|
1341
|
+
*/
|
|
1342
|
+
export(snapshot, options) {
|
|
1343
|
+
return write(snapshot, options);
|
|
1344
|
+
},
|
|
1345
|
+
/**
|
|
1346
|
+
* A session is a zip archive, not text: always rejects with E_UNSUPPORTED (use exportGraphToBytes() or exportGraphToBlob()).
|
|
1347
|
+
* @returns never
|
|
1348
|
+
*/
|
|
1349
|
+
exportToString() {
|
|
1350
|
+
return Promise.reject(
|
|
1351
|
+
new GraphFormatError(
|
|
1352
|
+
"E_UNSUPPORTED",
|
|
1353
|
+
"a Cytoscape session is a zip file, not text; use exportGraphToBytes() or exportGraphToBlob()",
|
|
1354
|
+
{ reason: "binary" }
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
function urlDecode(text) {
|
|
1360
|
+
const plus = text.replace(/\+/g, " ");
|
|
1361
|
+
try {
|
|
1362
|
+
return decodeURIComponent(plus);
|
|
1363
|
+
} catch {
|
|
1364
|
+
return plus;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async function openSession(input, report, common, maxBytes) {
|
|
1368
|
+
const bytes = await readBytes(input, common);
|
|
1369
|
+
if (bytes === null) {
|
|
1370
|
+
report.fail(
|
|
1371
|
+
CYS_ISSUE.NOT_ZIP,
|
|
1372
|
+
"a Cytoscape session is a zip archive; text input cannot hold one (pass the bytes)"
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (bytes.byteLength === 0) {
|
|
1376
|
+
report.fail(CYS_ISSUE.EMPTY_INPUT, "the input is empty");
|
|
1377
|
+
}
|
|
1378
|
+
let zipEntries;
|
|
1379
|
+
try {
|
|
1380
|
+
zipEntries = readZipDirectory(bytes);
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
return zipFailure(err, report);
|
|
1383
|
+
}
|
|
1384
|
+
const layout = layoutOf(zipEntries, report);
|
|
1385
|
+
let budget = maxBytes;
|
|
1386
|
+
let inflated = 0;
|
|
1387
|
+
const total = [
|
|
1388
|
+
...layout.networks,
|
|
1389
|
+
...layout.views,
|
|
1390
|
+
...layout.tables,
|
|
1391
|
+
...layout.files.values(),
|
|
1392
|
+
...[layout.cytables, layout.networkList, layout.cysession].filter((e) => e !== null)
|
|
1393
|
+
].reduce((sum, e) => sum + e.zip.size, 0);
|
|
1394
|
+
const charged = /* @__PURE__ */ new Set();
|
|
1395
|
+
return {
|
|
1396
|
+
layout,
|
|
1397
|
+
read: async (entry, into) => {
|
|
1398
|
+
const first = !charged.has(entry.name);
|
|
1399
|
+
const before = inflated;
|
|
1400
|
+
try {
|
|
1401
|
+
const data = await readZipEntry(bytes, entry.zip, {
|
|
1402
|
+
signal: common.signal,
|
|
1403
|
+
maxBytes: first ? budget : entry.zip.size,
|
|
1404
|
+
maxRatio: MAX_RATIO,
|
|
1405
|
+
onBytes: first ? (n) => common.onProgress?.(Math.min(before + n, total), total) : void 0
|
|
1406
|
+
});
|
|
1407
|
+
if (first) {
|
|
1408
|
+
charged.add(entry.name);
|
|
1409
|
+
budget -= data.byteLength;
|
|
1410
|
+
inflated += data.byteLength;
|
|
1411
|
+
}
|
|
1412
|
+
return data;
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
return zipFailure(err, into);
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
done: () => {
|
|
1418
|
+
common.onProgress?.(total, total);
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function zipFailure(err, report) {
|
|
1423
|
+
if (!(err instanceof ZipError)) {
|
|
1424
|
+
throw err;
|
|
1425
|
+
}
|
|
1426
|
+
const code = {
|
|
1427
|
+
"not-zip": CYS_ISSUE.NOT_ZIP,
|
|
1428
|
+
corrupt: CYS_ISSUE.CORRUPT,
|
|
1429
|
+
unsupported: CYS_ISSUE.UNSUPPORTED,
|
|
1430
|
+
"too-large": CYS_ISSUE.TOO_LARGE
|
|
1431
|
+
}[err.kind];
|
|
1432
|
+
if (err.kind === "too-large") {
|
|
1433
|
+
report.error("unsupported", code, err.message);
|
|
1434
|
+
throw report.abort(err.message, { code });
|
|
1435
|
+
}
|
|
1436
|
+
report.fail(code, err.message);
|
|
1437
|
+
}
|
|
1438
|
+
function shallowest(names, suffix) {
|
|
1439
|
+
const depth = (name) => name.split("/").length;
|
|
1440
|
+
const found = names.filter((n) => suffix.test(n));
|
|
1441
|
+
const least = Math.min(...found.map(depth));
|
|
1442
|
+
return found.filter((n) => depth(n) === least);
|
|
1443
|
+
}
|
|
1444
|
+
const NETWORK_FILE = /^networks\/(\d+)(?:-([^/]*))?\.xgmml$/;
|
|
1445
|
+
const VIEW_FILE = /^views\/(\d+)-(\d+)(?:-[^/]*)?\.xgmml$/;
|
|
1446
|
+
const TABLE_FILE = /^tables\/((\d+)(?:-[^/]*)?\/([^/-]+)-([^/-]+)-[^/]*\.cytable)$/;
|
|
1447
|
+
const NOISE = /(^|\/)(__MACOSX\/|\.DS_Store$)/;
|
|
1448
|
+
function layoutOf(zipEntries, report) {
|
|
1449
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1450
|
+
const repeated = [];
|
|
1451
|
+
for (const entry of zipEntries) {
|
|
1452
|
+
const name = entry.name.replaceAll("\\", "/");
|
|
1453
|
+
if (name.endsWith("/") || NOISE.test(name)) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (byName.has(name)) {
|
|
1457
|
+
repeated.push(name);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
byName.set(name, entry);
|
|
1461
|
+
}
|
|
1462
|
+
if (repeated.length > 0) {
|
|
1463
|
+
report.warning(
|
|
1464
|
+
"unsupported",
|
|
1465
|
+
CYS_ISSUE.DUPLICATE_ENTRY,
|
|
1466
|
+
`${repeated.length} entry name${plural(repeated.length)} appear more than once; the first of each is read: ${listed(repeated)}`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const names = [...byName.keys()];
|
|
1470
|
+
const markers = shallowest(names, /(^|\/)[^/]+\.version$/);
|
|
1471
|
+
const marker = markers.at(0);
|
|
1472
|
+
const cysession = shallowest(names, /(^|\/)cysession\.xml$/).at(0);
|
|
1473
|
+
if (markers.length > 1) {
|
|
1474
|
+
report.warning(
|
|
1475
|
+
"validation-error",
|
|
1476
|
+
CYS_ISSUE.ENTRY_SKIPPED,
|
|
1477
|
+
`the session has ${markers.length} version markers (${listed(markers)}); ${markers[0]} is used and the others are not read`
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
if (marker === void 0 && cysession === void 0) {
|
|
1481
|
+
report.fail(
|
|
1482
|
+
CYS_ISSUE.NOT_SESSION,
|
|
1483
|
+
"the zip holds neither a session version marker (<x.y.z>.version) nor cysession.xml: it is not a Cytoscape session"
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
const anchor = marker ?? cysession;
|
|
1487
|
+
const root = anchor.slice(0, anchor.lastIndexOf("/") + 1);
|
|
1488
|
+
let era = "2";
|
|
1489
|
+
let version = "2.0.0";
|
|
1490
|
+
if (marker !== void 0) {
|
|
1491
|
+
version = marker.slice(root.length, -".version".length);
|
|
1492
|
+
if (!/^\d+(\.\d+)*$/.test(version)) {
|
|
1493
|
+
report.fail(
|
|
1494
|
+
CYS_ISSUE.VERSION,
|
|
1495
|
+
`the session version marker ${marker} does not parse as a version (<major>.<minor>.<patch>.version)`
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const major = Number(version.split(".")[0]);
|
|
1499
|
+
if (major > 3) {
|
|
1500
|
+
report.fail(
|
|
1501
|
+
CYS_ISSUE.VERSION,
|
|
1502
|
+
`the session version ${version} is newer than the Cytoscape 3 sessions graph-io reads`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
era = major === 3 ? "3" : "2";
|
|
1506
|
+
}
|
|
1507
|
+
const layout = {
|
|
1508
|
+
era,
|
|
1509
|
+
version,
|
|
1510
|
+
root,
|
|
1511
|
+
networks: [],
|
|
1512
|
+
views: [],
|
|
1513
|
+
tables: [],
|
|
1514
|
+
cytables: null,
|
|
1515
|
+
networkList: null,
|
|
1516
|
+
cysession: null,
|
|
1517
|
+
files: /* @__PURE__ */ new Map(),
|
|
1518
|
+
styles: [],
|
|
1519
|
+
skipped: []
|
|
1520
|
+
};
|
|
1521
|
+
for (const [name, zip] of byName) {
|
|
1522
|
+
if (!name.startsWith(root) || name === marker) {
|
|
1523
|
+
if (name !== marker && !markers.includes(name)) {
|
|
1524
|
+
layout.skipped.push(name);
|
|
1525
|
+
}
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
const path = name.slice(root.length);
|
|
1529
|
+
const entry = { path, name, zip };
|
|
1530
|
+
if (era === "3") {
|
|
1531
|
+
classify3(entry, layout);
|
|
1532
|
+
} else {
|
|
1533
|
+
classify2(entry, layout);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return layout;
|
|
1537
|
+
}
|
|
1538
|
+
function classify3(entry, layout) {
|
|
1539
|
+
const { path } = entry;
|
|
1540
|
+
const network = NETWORK_FILE.exec(path);
|
|
1541
|
+
const view = VIEW_FILE.exec(path);
|
|
1542
|
+
const table = TABLE_FILE.exec(path);
|
|
1543
|
+
if (network !== null) {
|
|
1544
|
+
layout.networks.push({ ...entry, suid: network[1], title: urlDecode(network[2] ?? "") });
|
|
1545
|
+
} else if (view !== null) {
|
|
1546
|
+
layout.views.push({ ...entry, network: view[1], view: view[2] });
|
|
1547
|
+
} else if (path === "tables/cytables.xml") {
|
|
1548
|
+
layout.cytables = entry;
|
|
1549
|
+
} else if (table !== null) {
|
|
1550
|
+
const element = {
|
|
1551
|
+
"org.cytoscape.model.CyNode": "node",
|
|
1552
|
+
"org.cytoscape.model.CyEdge": "edge",
|
|
1553
|
+
"org.cytoscape.model.CyNetwork": "network"
|
|
1554
|
+
}[urlDecode(table[4])];
|
|
1555
|
+
if (element === void 0) {
|
|
1556
|
+
layout.skipped.push(entry.name);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
layout.tables.push({
|
|
1560
|
+
...entry,
|
|
1561
|
+
tablePath: table[1],
|
|
1562
|
+
network: table[2],
|
|
1563
|
+
namespace: urlDecode(table[3]),
|
|
1564
|
+
element
|
|
1565
|
+
});
|
|
1566
|
+
} else if (path === "apps/org.cytoscape.swing-application/network_list.xml") {
|
|
1567
|
+
layout.networkList = entry;
|
|
1568
|
+
} else if (/(^|\/)session_vizmap\.(xml|props)$/.test(path)) {
|
|
1569
|
+
layout.styles.push(entry);
|
|
1570
|
+
} else {
|
|
1571
|
+
layout.skipped.push(entry.name);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function classify2(entry, layout) {
|
|
1575
|
+
const { path } = entry;
|
|
1576
|
+
if (path === "cysession.xml") {
|
|
1577
|
+
layout.cysession = entry;
|
|
1578
|
+
} else if (/^[^/]+\.xgmml$/.test(path)) {
|
|
1579
|
+
layout.files.set(path, entry);
|
|
1580
|
+
} else if (/^session_vizmap\.(props|xml)$/.test(path)) {
|
|
1581
|
+
layout.styles.push(entry);
|
|
1582
|
+
} else {
|
|
1583
|
+
layout.skipped.push(entry.name);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function listed(names) {
|
|
1587
|
+
const head = names.slice(0, 10).map(entryLabel).join(", ");
|
|
1588
|
+
return names.length > 10 ? `${head} and ${names.length - 10} more` : head;
|
|
1589
|
+
}
|
|
1590
|
+
function entryLabel(name) {
|
|
1591
|
+
return name.replace(/^[^/]*CytoscapeSession[^/]*\//, "");
|
|
1592
|
+
}
|
|
1593
|
+
async function readXmlTree(bytes, entry, report, common) {
|
|
1594
|
+
const tree = await parseXmlTree(bytes, entry, report, common);
|
|
1595
|
+
return typeof tree === "string" ? report.fail(CYS_ISSUE.CORRUPT, `${entry}: ${tree}`) : tree;
|
|
1596
|
+
}
|
|
1597
|
+
async function parseXmlTree(bytes, entry, report, common) {
|
|
1598
|
+
const scratch = new ImportReportBuilder(FORMAT, 0);
|
|
1599
|
+
const stack = [{ name: "", attrs: /* @__PURE__ */ new Map(), children: [] }];
|
|
1600
|
+
try {
|
|
1601
|
+
await tokenizeXml(
|
|
1602
|
+
textChunks(bytes, scratch, { signal: common.signal, declaredEncoding: xmlDeclaredEncoding, xml: true }),
|
|
1603
|
+
{
|
|
1604
|
+
start(name, attrs) {
|
|
1605
|
+
const node = { name: name.slice(name.lastIndexOf(":") + 1), attrs, children: [] };
|
|
1606
|
+
stack[stack.length - 1].children.push(node);
|
|
1607
|
+
stack.push(node);
|
|
1608
|
+
},
|
|
1609
|
+
end() {
|
|
1610
|
+
stack.pop();
|
|
1611
|
+
},
|
|
1612
|
+
text() {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
if (err instanceof XmlSyntaxError || err instanceof ImportError) {
|
|
1618
|
+
return err.message;
|
|
1619
|
+
}
|
|
1620
|
+
throw err;
|
|
1621
|
+
}
|
|
1622
|
+
for (const issue of scratch.issues) {
|
|
1623
|
+
if (issue.severity === "warning") {
|
|
1624
|
+
report.warning(issue.category, issue.code, `${entry}: ${issue.message}`, {
|
|
1625
|
+
line: issue.line,
|
|
1626
|
+
element: entry
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const [root] = stack[0].children;
|
|
1631
|
+
return root ?? "the document is empty";
|
|
1632
|
+
}
|
|
1633
|
+
function elementsNamed(node, name) {
|
|
1634
|
+
const out = [];
|
|
1635
|
+
const stack = [node];
|
|
1636
|
+
while (stack.length > 0) {
|
|
1637
|
+
const n = stack.pop();
|
|
1638
|
+
if (n.name === name) {
|
|
1639
|
+
out.push(n);
|
|
1640
|
+
}
|
|
1641
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
1642
|
+
stack.push(n.children[i]);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return out;
|
|
1646
|
+
}
|
|
1647
|
+
const JAVA_TYPES = {
|
|
1648
|
+
"java.lang.String": "String",
|
|
1649
|
+
"java.lang.Long": "Long",
|
|
1650
|
+
"java.lang.Integer": "Integer",
|
|
1651
|
+
"java.lang.Double": "Double",
|
|
1652
|
+
"java.lang.Boolean": "Boolean"
|
|
1653
|
+
};
|
|
1654
|
+
const LIST_CLASS = /^java\.util\.List<\s*([\w.]+)\s*>$/;
|
|
1655
|
+
async function readCyTable(bytes, path, entry, report, common) {
|
|
1656
|
+
const scratch = new ImportReportBuilder(FORMAT, 0);
|
|
1657
|
+
const reader = new CsvRecordReader(bytes, scratch, { delimiter: ",", encoding: "utf-8", signal: common.signal });
|
|
1658
|
+
const records = [];
|
|
1659
|
+
try {
|
|
1660
|
+
for await (const cells of reader) {
|
|
1661
|
+
records.push({ cells, line: reader.line });
|
|
1662
|
+
}
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
if (err instanceof ImportError) {
|
|
1665
|
+
return tableError(report, entry, scratch.issues.at(-1)?.message ?? err.message);
|
|
1666
|
+
}
|
|
1667
|
+
throw err;
|
|
1668
|
+
}
|
|
1669
|
+
let at = 0;
|
|
1670
|
+
let version = 0;
|
|
1671
|
+
if (records[0]?.cells[0] === "CyCSV-Version") {
|
|
1672
|
+
const { cells } = records[0];
|
|
1673
|
+
if (cells.length !== 2 || !/^\d+$/.test(cells[1].trim())) {
|
|
1674
|
+
return tableError(report, entry, `the version row ${JSON.stringify(cells)} does not parse`);
|
|
1675
|
+
}
|
|
1676
|
+
version = Number(cells[1]);
|
|
1677
|
+
at = 1;
|
|
1678
|
+
}
|
|
1679
|
+
if (version !== 0 && version !== 1) {
|
|
1680
|
+
return tableError(report, entry, `CyCSV version "${records[0].cells[1]}" is not 0 or 1`);
|
|
1681
|
+
}
|
|
1682
|
+
let headerLines = version === 1 ? 4 : 3;
|
|
1683
|
+
if (records.length < at + headerLines - 1) {
|
|
1684
|
+
return tableError(report, entry, "the table header is incomplete");
|
|
1685
|
+
}
|
|
1686
|
+
const beforeTitle = records[at + headerLines - 2];
|
|
1687
|
+
const title = records.at(at + headerLines - 1);
|
|
1688
|
+
if (title === void 0 || title.line > beforeTitle.line + 1 && !beforeTitle.cells.some((c) => /[\r\n]/.test(c))) {
|
|
1689
|
+
headerLines--;
|
|
1690
|
+
}
|
|
1691
|
+
const names = records[at].cells;
|
|
1692
|
+
const classes = records[at + 1].cells;
|
|
1693
|
+
if (names.length === 0 || classes.length !== names.length) {
|
|
1694
|
+
return tableError(
|
|
1695
|
+
report,
|
|
1696
|
+
entry,
|
|
1697
|
+
`${names.length} column name${plural(names.length)} but ${classes.length} column class(es)`
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
const columns = [];
|
|
1701
|
+
for (let i = 0; i < names.length; i++) {
|
|
1702
|
+
const column = columnOf(names[i], classes[i]);
|
|
1703
|
+
if (column === null) {
|
|
1704
|
+
return tableError(
|
|
1705
|
+
report,
|
|
1706
|
+
entry,
|
|
1707
|
+
`column "${names[i]}" has the class "${classes[i]}", which graph-io cannot read`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
columns.push(column);
|
|
1711
|
+
}
|
|
1712
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1713
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1714
|
+
const problems = { short: 0, long: 0, repeated: 0 };
|
|
1715
|
+
for (const record of records.slice(at + headerLines)) {
|
|
1716
|
+
const { cells } = record;
|
|
1717
|
+
if (cells.length < columns.length) {
|
|
1718
|
+
problems.short++;
|
|
1719
|
+
} else if (cells.length > columns.length) {
|
|
1720
|
+
problems.long++;
|
|
1721
|
+
}
|
|
1722
|
+
const key = cells[0];
|
|
1723
|
+
if (rows.has(key)) {
|
|
1724
|
+
problems.repeated++;
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
rows.set(key, cells.slice(0, columns.length));
|
|
1728
|
+
lines.set(key, record.line);
|
|
1729
|
+
}
|
|
1730
|
+
if (problems.short + problems.long + problems.repeated > 0) {
|
|
1731
|
+
report.warning(
|
|
1732
|
+
"parse-error",
|
|
1733
|
+
CYS_ISSUE.TABLE_ROW,
|
|
1734
|
+
`${entryLabel(entry)}: ${problems.short} row${plural(problems.short)} with too few cells (the rest are unset), ${problems.long} with too many (the extra cells are ignored), ${problems.repeated} repeating a key (the first row is read)`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return { path, entry, columns, rows, lines };
|
|
1738
|
+
}
|
|
1739
|
+
function columnOf(name, javaClass) {
|
|
1740
|
+
const scalar = JAVA_TYPES[javaClass.trim()];
|
|
1741
|
+
if (scalar !== void 0) {
|
|
1742
|
+
return { name, type: scalar, list: false };
|
|
1743
|
+
}
|
|
1744
|
+
const list = LIST_CLASS.exec(javaClass.trim());
|
|
1745
|
+
const item = list === null ? void 0 : JAVA_TYPES[list[1]];
|
|
1746
|
+
return item === void 0 ? null : { name, type: item, list: true };
|
|
1747
|
+
}
|
|
1748
|
+
function tableError(report, entry, reason) {
|
|
1749
|
+
report.error("parse-error", CYS_ISSUE.TABLE, `${entryLabel(entry)}: ${reason}; the table is not read`);
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
function cellAtt(column, text, line, namespace, hidden) {
|
|
1753
|
+
if (text.startsWith("=")) {
|
|
1754
|
+
return att(column.name, column.list ? "List" : column.type, null, text, line, namespace, hidden, true);
|
|
1755
|
+
}
|
|
1756
|
+
if (!column.list) {
|
|
1757
|
+
if (text.length === 0 && column.type !== "String") {
|
|
1758
|
+
return null;
|
|
1759
|
+
}
|
|
1760
|
+
return att(column.name, column.type, null, text, line, namespace, hidden, false);
|
|
1761
|
+
}
|
|
1762
|
+
if (text.length === 0 && column.type !== "String") {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
const list = att(column.name, "List", column.type, null, line, namespace, hidden, false);
|
|
1766
|
+
for (const item of javaSplitLines(text)) {
|
|
1767
|
+
if (item.length === 0 && column.type !== "String") {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
list.children.push(att(column.name, column.type, null, item, line, namespace, hidden, false));
|
|
1771
|
+
}
|
|
1772
|
+
return list;
|
|
1773
|
+
}
|
|
1774
|
+
function javaSplitLines(text) {
|
|
1775
|
+
if (!text.includes("\n")) {
|
|
1776
|
+
return [text];
|
|
1777
|
+
}
|
|
1778
|
+
const items = text.split("\n");
|
|
1779
|
+
while (items.length > 0 && items[items.length - 1].length === 0) {
|
|
1780
|
+
items.pop();
|
|
1781
|
+
}
|
|
1782
|
+
return items;
|
|
1783
|
+
}
|
|
1784
|
+
function att(name, cyType, elementType, value, line, namespace, hidden, equation) {
|
|
1785
|
+
return {
|
|
1786
|
+
name,
|
|
1787
|
+
type: null,
|
|
1788
|
+
cyType,
|
|
1789
|
+
elementType,
|
|
1790
|
+
value,
|
|
1791
|
+
hidden,
|
|
1792
|
+
equation,
|
|
1793
|
+
children: [],
|
|
1794
|
+
extra: {},
|
|
1795
|
+
xml: null,
|
|
1796
|
+
hasGraph: false,
|
|
1797
|
+
text: "",
|
|
1798
|
+
line,
|
|
1799
|
+
namespace
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
async function virtualColumnsOf(target, virtuals, tableOf, report) {
|
|
1803
|
+
const out = [];
|
|
1804
|
+
for (const virtual of virtuals) {
|
|
1805
|
+
if (virtual.targetTable !== target.path) {
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
const resolved = await resolve(virtual, virtuals, tableOf, /* @__PURE__ */ new Set());
|
|
1809
|
+
if (typeof resolved === "string") {
|
|
1810
|
+
report.error(
|
|
1811
|
+
"parse-error",
|
|
1812
|
+
CYS_ISSUE.TABLE,
|
|
1813
|
+
`${target.entry}: the virtual column "${virtual.name}" ${resolved}; the column is not read`
|
|
1814
|
+
);
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const targetKey = target.columns.findIndex((c) => c.name === virtual.targetJoinKey);
|
|
1818
|
+
if (targetKey < 0) {
|
|
1819
|
+
report.error(
|
|
1820
|
+
"parse-error",
|
|
1821
|
+
CYS_ISSUE.TABLE,
|
|
1822
|
+
`${target.entry}: the virtual column "${virtual.name}" joins on the column "${virtual.targetJoinKey}", which the table does not have; the column is not read`
|
|
1823
|
+
);
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
const values = /* @__PURE__ */ new Map();
|
|
1827
|
+
for (const [key, cells] of target.rows) {
|
|
1828
|
+
const join = targetKey <= 0 ? key : cells[targetKey];
|
|
1829
|
+
const value = join === void 0 ? void 0 : resolved.values.get(join);
|
|
1830
|
+
if (value !== void 0) {
|
|
1831
|
+
values.set(key, value);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
out.push({ column: { ...resolved.column, name: virtual.name }, values });
|
|
1835
|
+
}
|
|
1836
|
+
return out;
|
|
1837
|
+
}
|
|
1838
|
+
async function resolve(virtual, virtuals, tableOf, seen) {
|
|
1839
|
+
if (seen.has(virtual)) {
|
|
1840
|
+
return "is part of a cycle of virtual columns";
|
|
1841
|
+
}
|
|
1842
|
+
seen.add(virtual);
|
|
1843
|
+
const source = await tableOf(virtual.sourceTable);
|
|
1844
|
+
if (source === null) {
|
|
1845
|
+
return `names the source table "${virtual.sourceTable}", which the session does not hold or cannot read`;
|
|
1846
|
+
}
|
|
1847
|
+
const joinIndex = source.columns.findIndex((c) => c.name === virtual.sourceJoinKey);
|
|
1848
|
+
if (joinIndex < 0) {
|
|
1849
|
+
return `joins on the column "${virtual.sourceJoinKey}", which its source table does not have`;
|
|
1850
|
+
}
|
|
1851
|
+
const joinOf = (key, cells) => joinIndex === 0 ? key : cells[joinIndex];
|
|
1852
|
+
const own = source.columns.findIndex((c) => c.name === virtual.sourceColumn);
|
|
1853
|
+
if (own >= 0) {
|
|
1854
|
+
const values2 = /* @__PURE__ */ new Map();
|
|
1855
|
+
for (const [key, cells] of source.rows) {
|
|
1856
|
+
const join = joinOf(key, cells);
|
|
1857
|
+
if (join !== void 0 && cells[own] !== void 0) {
|
|
1858
|
+
values2.set(join, cells[own]);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return { column: source.columns[own], values: values2 };
|
|
1862
|
+
}
|
|
1863
|
+
const chained = virtuals.find((v) => v.targetTable === source.path && v.name === virtual.sourceColumn);
|
|
1864
|
+
if (chained === void 0) {
|
|
1865
|
+
return `names the source column "${virtual.sourceColumn}", which its source table does not have`;
|
|
1866
|
+
}
|
|
1867
|
+
const inner = await resolve(chained, virtuals, tableOf, seen);
|
|
1868
|
+
if (typeof inner === "string") {
|
|
1869
|
+
return inner;
|
|
1870
|
+
}
|
|
1871
|
+
const values = /* @__PURE__ */ new Map();
|
|
1872
|
+
const chainedJoin = source.columns.findIndex((c) => c.name === chained.targetJoinKey);
|
|
1873
|
+
if (chainedJoin < 0) {
|
|
1874
|
+
return `reads the virtual column "${chained.name}", which joins on the column "${chained.targetJoinKey}" its table does not have`;
|
|
1875
|
+
}
|
|
1876
|
+
for (const [key, cells] of source.rows) {
|
|
1877
|
+
const lookup = chainedJoin <= 0 ? key : cells[chainedJoin];
|
|
1878
|
+
const value = lookup === void 0 ? void 0 : inner.values.get(lookup);
|
|
1879
|
+
const join = joinOf(key, cells);
|
|
1880
|
+
if (value !== void 0 && join !== void 0) {
|
|
1881
|
+
values.set(join, value);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return { column: inner.column, values };
|
|
1885
|
+
}
|
|
1886
|
+
const FORMAT_DEFAULTS = {
|
|
1887
|
+
ids: "keep",
|
|
1888
|
+
defaultDirected: true,
|
|
1889
|
+
weightFrom: "weight",
|
|
1890
|
+
addMissingNodes: false
|
|
1891
|
+
};
|
|
1892
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
1893
|
+
"ids",
|
|
1894
|
+
"addMissingNodes",
|
|
1895
|
+
"duplicateEdges",
|
|
1896
|
+
"selfLoops",
|
|
1897
|
+
"onMixedDirection",
|
|
1898
|
+
"defaultDirected",
|
|
1899
|
+
"weightFrom",
|
|
1900
|
+
"weightDtype",
|
|
1901
|
+
"long",
|
|
1902
|
+
"restoreMangledIds",
|
|
1903
|
+
"errorLimit",
|
|
1904
|
+
"signal",
|
|
1905
|
+
"onProgress"
|
|
1906
|
+
]);
|
|
1907
|
+
function budgetOption(value) {
|
|
1908
|
+
if (value === void 0) {
|
|
1909
|
+
return DEFAULT_MAX_UNCOMPRESSED;
|
|
1910
|
+
}
|
|
1911
|
+
if (typeof value !== "number" || !(value > 0)) {
|
|
1912
|
+
throw new GraphFormatError(
|
|
1913
|
+
"E_UNSUPPORTED",
|
|
1914
|
+
`option maxUncompressedBytes: ${JSON.stringify(value)} is not a positive number`,
|
|
1915
|
+
{
|
|
1916
|
+
option: "maxUncompressedBytes",
|
|
1917
|
+
found: value
|
|
1918
|
+
}
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
return value;
|
|
1922
|
+
}
|
|
1923
|
+
async function prepare(input, sink, options) {
|
|
1924
|
+
const common = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
1925
|
+
const report = new ImportReportBuilder(FORMAT, common.errorLimit);
|
|
1926
|
+
if (sink !== null) {
|
|
1927
|
+
reportSinkOptions(sink, options, report, true);
|
|
1928
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1929
|
+
}
|
|
1930
|
+
const { zAs } = resolveSettings({ zAs: options?.zAs }, false);
|
|
1931
|
+
const maxBytes = budgetOption(options?.maxUncompressedBytes);
|
|
1932
|
+
const session = await openSession(input, report, common, maxBytes);
|
|
1933
|
+
const inner = Object.freeze({ ...common, onProgress: null, encoding: null });
|
|
1934
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1935
|
+
const choices = session.layout.era === "3" ? await networks3(session, report, inner, docs) : await networks2(session, report, inner);
|
|
1936
|
+
return { report, common, inner, zAs, session, choices, docs };
|
|
1937
|
+
}
|
|
1938
|
+
async function parseEntry(session, entry, report, inner) {
|
|
1939
|
+
return await parseOptionalEntry(session, entry, report, inner, false);
|
|
1940
|
+
}
|
|
1941
|
+
async function parseOptionalEntry(session, entry, report, inner, optional) {
|
|
1942
|
+
const scratch = new ImportReportBuilder(FORMAT, Math.max(0, report.errorLimit - report.errorCount));
|
|
1943
|
+
try {
|
|
1944
|
+
const bytes = await session.read(entry, scratch);
|
|
1945
|
+
const doc = await parseXgmml(bytes, scratch, inner, void 0);
|
|
1946
|
+
const dialect = dialectOf(doc, scratch);
|
|
1947
|
+
relay(scratch.issues, report, entry.name);
|
|
1948
|
+
addCounts(scratch, report);
|
|
1949
|
+
return { doc, dialect };
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
if (!(err instanceof ImportError)) {
|
|
1952
|
+
throw err;
|
|
1953
|
+
}
|
|
1954
|
+
addCounts(scratch, report);
|
|
1955
|
+
const { issues } = scratch;
|
|
1956
|
+
if (scratch.truncated) {
|
|
1957
|
+
relay(issues, report, entry.name);
|
|
1958
|
+
}
|
|
1959
|
+
const fatal = issues.at(-1);
|
|
1960
|
+
relay(issues.slice(0, -1), report, entry.name);
|
|
1961
|
+
const message = `${entryLabel(entry.name)}: ${fatal?.message ?? err.message}`;
|
|
1962
|
+
const where = { line: fatal?.line ?? null };
|
|
1963
|
+
if (fatal?.code === CYS_ISSUE.TOO_LARGE) {
|
|
1964
|
+
report.error("unsupported", fatal.code, message);
|
|
1965
|
+
throw report.abort(message, { code: fatal.code });
|
|
1966
|
+
}
|
|
1967
|
+
if (optional) {
|
|
1968
|
+
report.error("parse-error", fatal?.code ?? CYS_ISSUE.CORRUPT, `${message}; the entry is not read`, where);
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
return report.fail(fatal?.code ?? CYS_ISSUE.CORRUPT, message, where);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
function addCounts(from, to) {
|
|
1975
|
+
for (const key of Object.keys(from.counts)) {
|
|
1976
|
+
to.counts[key] += from.counts[key];
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function relay(issues, report, entry) {
|
|
1980
|
+
for (const issue of issues) {
|
|
1981
|
+
const where = { line: issue.line, element: issue.element };
|
|
1982
|
+
if (issue.severity === "error") {
|
|
1983
|
+
report.error(issue.category, issue.code, `${entryLabel(entry)}: ${issue.message}`, where);
|
|
1984
|
+
} else {
|
|
1985
|
+
report.warning(issue.category, issue.code, `${entryLabel(entry)}: ${issue.message}`, where);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
async function networks3(session, report, inner, docs) {
|
|
1990
|
+
const { layout } = session;
|
|
1991
|
+
const choices = [];
|
|
1992
|
+
for (const network of layout.networks) {
|
|
1993
|
+
const parsed = await parseEntry(session, network, report, inner);
|
|
1994
|
+
if (parsed.dialect.view) {
|
|
1995
|
+
report.fail(
|
|
1996
|
+
XGMML_ISSUE.VIEW_DOCUMENT,
|
|
1997
|
+
`${network.name}: this is a session view file (cy:view), not a network file; it holds no topology`,
|
|
1998
|
+
{ line: parsed.doc.root.line }
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
docs.set(network.name, parsed);
|
|
2002
|
+
for (const graph of registeredGraphs(parsed)) {
|
|
2003
|
+
if (graph.id === null) {
|
|
2004
|
+
const label = graph.label === null ? "" : ` ("${graph.label}")`;
|
|
2005
|
+
report.error(
|
|
2006
|
+
"missing-value",
|
|
2007
|
+
XGMML_ISSUE.MISSING_ID,
|
|
2008
|
+
`${network.name}: a registered subnetwork${label} has no id; its tables and views cannot be found and it is skipped`,
|
|
2009
|
+
{ line: graph.line }
|
|
2010
|
+
);
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
const members = membersOf(parsed.doc, graph);
|
|
2014
|
+
choices.push({
|
|
2015
|
+
era: "3",
|
|
2016
|
+
network,
|
|
2017
|
+
graph,
|
|
2018
|
+
graphId: graph.id,
|
|
2019
|
+
name: graph.label ?? graph.id,
|
|
2020
|
+
nodes: new Set(members.nodes).size,
|
|
2021
|
+
edges: members.edges.length
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
const known = new Set(choices.map((c) => c.graphId));
|
|
2026
|
+
const strayViews = layout.views.filter((v) => !known.has(v.network)).map((v) => v.name);
|
|
2027
|
+
const strayTables = layout.tables.filter(
|
|
2028
|
+
(t) => !known.has(t.network) && !layout.networks.some((n) => n.suid === t.network) && !isGroupTable(t, session, docs)
|
|
2029
|
+
);
|
|
2030
|
+
if (strayViews.length > 0 || strayTables.length > 0) {
|
|
2031
|
+
report.warning(
|
|
2032
|
+
"validation-error",
|
|
2033
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
2034
|
+
`${strayViews.length + strayTables.length} view${plural(strayViews.length + strayTables.length)} or table${plural(strayViews.length + strayTables.length)} ${agree(strayViews.length + strayTables.length, "belongs", "belong")} to a network the session does not hold and were not read: ${listed([...strayViews, ...strayTables.map((t) => t.name)])}`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
if (layout.networkList === null) {
|
|
2038
|
+
return choices;
|
|
2039
|
+
}
|
|
2040
|
+
const tree = await parseXmlTree(
|
|
2041
|
+
await session.read(layout.networkList, report),
|
|
2042
|
+
layout.networkList.name,
|
|
2043
|
+
report,
|
|
2044
|
+
inner
|
|
2045
|
+
);
|
|
2046
|
+
if (typeof tree === "string") {
|
|
2047
|
+
report.warning(
|
|
2048
|
+
"unsupported",
|
|
2049
|
+
CYS_ISSUE.ENTRY_SKIPPED,
|
|
2050
|
+
`${layout.networkList.name}: ${tree}; it is not read and the networks are listed in entry order`
|
|
2051
|
+
);
|
|
2052
|
+
return choices;
|
|
2053
|
+
}
|
|
2054
|
+
const order = /* @__PURE__ */ new Map();
|
|
2055
|
+
for (const node of elementsNamed(tree, "network")) {
|
|
2056
|
+
const id = node.attrs.get("id");
|
|
2057
|
+
const position = Number(node.attrs.get("order"));
|
|
2058
|
+
if (id !== void 0 && Number.isFinite(position)) {
|
|
2059
|
+
order.set(id, position);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return choices.map((choice, index) => ({ choice, index })).sort(
|
|
2063
|
+
(a, b) => (order.get(a.choice.graphId) ?? Infinity) - (order.get(b.choice.graphId) ?? Infinity) || a.index - b.index
|
|
2064
|
+
).map((c) => c.choice);
|
|
2065
|
+
}
|
|
2066
|
+
function isGroupTable(table, session, docs) {
|
|
2067
|
+
for (const network of session.layout.networks) {
|
|
2068
|
+
if (docs.get(network.name)?.doc.graphs.some((g) => g.id === table.network) === true) {
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
function registeredGraphs(parsed) {
|
|
2075
|
+
const subnetworks = parsed.doc.root.subgraphs.filter((g) => isCyTrue(g.registered));
|
|
2076
|
+
return subnetworks.length === 0 && isCyTrue(parsed.doc.root.registered) ? [parsed.doc.root] : subnetworks;
|
|
2077
|
+
}
|
|
2078
|
+
async function import3(prepared, choice, sink, parsed) {
|
|
2079
|
+
const { graph } = choice;
|
|
2080
|
+
const members = membersOf(parsed.doc, graph);
|
|
2081
|
+
const nodes = byId(members.nodes);
|
|
2082
|
+
const edges = byId(members.edges);
|
|
2083
|
+
const restore = saveRecords(graph, nodes, edges);
|
|
2084
|
+
try {
|
|
2085
|
+
await fill3(prepared, choice, sink, parsed, nodes, edges);
|
|
2086
|
+
} finally {
|
|
2087
|
+
restore();
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function saveRecords(graph, nodes, edges) {
|
|
2091
|
+
const saved = [...nodes.values()].map((n) => ({
|
|
2092
|
+
n,
|
|
2093
|
+
atts: n.atts.length,
|
|
2094
|
+
x: n.x,
|
|
2095
|
+
y: n.y,
|
|
2096
|
+
z: n.z,
|
|
2097
|
+
graphics: n.graphics
|
|
2098
|
+
}));
|
|
2099
|
+
const savedEdges = [...edges.values()].map((e) => ({ e, atts: e.atts.length, graphics: e.graphics }));
|
|
2100
|
+
const graphAtts = graph.atts.length;
|
|
2101
|
+
const graphGraphics = graph.graphics;
|
|
2102
|
+
return () => {
|
|
2103
|
+
for (const { n, atts, x, y, z, graphics } of saved) {
|
|
2104
|
+
n.atts.length = atts;
|
|
2105
|
+
Object.assign(n, { x, y, z, graphics });
|
|
2106
|
+
}
|
|
2107
|
+
for (const { e, atts, graphics } of savedEdges) {
|
|
2108
|
+
e.atts.length = atts;
|
|
2109
|
+
e.graphics = graphics;
|
|
2110
|
+
}
|
|
2111
|
+
graph.atts.length = graphAtts;
|
|
2112
|
+
graph.graphics = graphGraphics;
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
async function fill3(prepared, choice, sink, parsed, nodes, edges) {
|
|
2116
|
+
const { report, inner } = prepared;
|
|
2117
|
+
const { doc, dialect } = parsed;
|
|
2118
|
+
const { graph } = choice;
|
|
2119
|
+
const members = membersOf(doc, graph);
|
|
2120
|
+
const unmatched = await applyTables(prepared, choice, doc, nodes, edges);
|
|
2121
|
+
if (unmatched > 0) {
|
|
2122
|
+
report.warning(
|
|
2123
|
+
"parse-error",
|
|
2124
|
+
CYS_ISSUE.TABLE_ROW,
|
|
2125
|
+
`${unmatched} table row${plural(unmatched)} of network ${choice.graphId} ${agree(unmatched, "names", "name")} no node, edge or network in it (left over from deleted elements) and ${agree(unmatched, "is", "are")} not read`
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
const restoredIds = inner.restoreMangledIds ? restoreIds(members.nodes) : /* @__PURE__ */ new Map();
|
|
2129
|
+
const { visualStyle, extraViews, extraIds } = await readViews(prepared, choice, nodes, edges);
|
|
2130
|
+
const cyMeta = sessionMeta(prepared, {
|
|
2131
|
+
collection: doc.root.label ?? choice.network.title,
|
|
2132
|
+
network: choice.graphId,
|
|
2133
|
+
parentNetwork: parentNetwork(graph, prepared.choices),
|
|
2134
|
+
visualStyle
|
|
2135
|
+
});
|
|
2136
|
+
const groups = [];
|
|
2137
|
+
cyMeta.groups = groups;
|
|
2138
|
+
const extras = {
|
|
2139
|
+
sourceFormat: FORMAT,
|
|
2140
|
+
entry: choice.network.name,
|
|
2141
|
+
graphName: localName(graph) ?? choice.name,
|
|
2142
|
+
metaExtra: { [META_KEY]: cyMeta },
|
|
2143
|
+
labelFromName: true,
|
|
2144
|
+
restoredIds,
|
|
2145
|
+
groupNodes: groupNodeIds(members.nodes),
|
|
2146
|
+
resolvePointer: pointerResolver(prepared, doc),
|
|
2147
|
+
onMissingMember: (group, member) => {
|
|
2148
|
+
addMissingMember(groups, group, member);
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
const settings = { labelAliases: false, cytoscapeEscapes: false, zAs: prepared.zAs };
|
|
2152
|
+
new XgmmlEmitter(doc, dialect, sink, report, prepared.inner, settings, extras).emit(graph);
|
|
2153
|
+
if (groups.length > 0) {
|
|
2154
|
+
const missing = groups.reduce((n, g) => n + g.members.length, 0);
|
|
2155
|
+
report.warning(
|
|
2156
|
+
"unsupported",
|
|
2157
|
+
CYS_ISSUE.COLLAPSED_GROUP,
|
|
2158
|
+
`${groups.length} collapsed group${plural(groups.length)} ${agree(groups.length, "holds", "hold")} ${missing} member${plural(missing)} that are not in the network; they are listed in meta.extra.cytoscape.groups`
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
reportRootOnly(parsed, report, choice.network.name);
|
|
2162
|
+
writeViewPositions(sink, extraViews, extraIds, report, prepared.inner, restoredIds);
|
|
2163
|
+
}
|
|
2164
|
+
function groupNodeIds(nodes) {
|
|
2165
|
+
return new Set(
|
|
2166
|
+
nodes.filter((n) => n.atts.some((a) => a.name === "__isGroup" && isCyTrue(a.value))).flatMap((n) => n.id === null ? [] : [n.id])
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
function addMissingMember(groups, group, member) {
|
|
2170
|
+
const record = groups.find((g) => g.group === group);
|
|
2171
|
+
if (record === void 0) {
|
|
2172
|
+
groups.push({ group, members: [member] });
|
|
2173
|
+
} else if (!record.members.includes(member)) {
|
|
2174
|
+
record.members.push(member);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
async function applyTables(prepared, choice, doc, nodes, edges) {
|
|
2178
|
+
const { report, inner, session } = prepared;
|
|
2179
|
+
const { layout } = session;
|
|
2180
|
+
const tables = /* @__PURE__ */ new Map();
|
|
2181
|
+
const tableOf = (path) => {
|
|
2182
|
+
let pending = tables.get(path);
|
|
2183
|
+
if (pending === void 0) {
|
|
2184
|
+
const entry = layout.tables.find((t) => t.tablePath === path);
|
|
2185
|
+
pending = entry === void 0 ? Promise.resolve(null) : session.read(entry, report).then((bytes) => readCyTable(bytes, path, entry.name, report, inner));
|
|
2186
|
+
tables.set(path, pending);
|
|
2187
|
+
}
|
|
2188
|
+
return pending;
|
|
2189
|
+
};
|
|
2190
|
+
const ctx = {
|
|
2191
|
+
choice,
|
|
2192
|
+
graph: choice.graph,
|
|
2193
|
+
nodes,
|
|
2194
|
+
edges,
|
|
2195
|
+
// a row of an element the file declares outside this network (a collapsed group's member, a
|
|
2196
|
+
// meta-edge) is Cytoscape's bookkeeping, not a stale row
|
|
2197
|
+
declared: /* @__PURE__ */ new Set([
|
|
2198
|
+
...doc.nodes.flatMap((n) => n.id === null ? [] : [n.id]),
|
|
2199
|
+
...doc.edges.flatMap((e) => e.id === null ? [] : [e.id])
|
|
2200
|
+
]),
|
|
2201
|
+
virtuals: layout.cytables === null ? [] : await readVirtuals(session, layout.cytables, report, inner),
|
|
2202
|
+
tableOf,
|
|
2203
|
+
report
|
|
2204
|
+
};
|
|
2205
|
+
let unmatched = 0;
|
|
2206
|
+
for (const entry of layout.tables.filter((t) => t.network === choice.graphId && t.element !== null)) {
|
|
2207
|
+
throwIfAborted(inner.signal);
|
|
2208
|
+
const table = await tableOf(entry.tablePath);
|
|
2209
|
+
if (table !== null) {
|
|
2210
|
+
unmatched += await applyTable(ctx, entry, table);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return unmatched;
|
|
2214
|
+
}
|
|
2215
|
+
async function applyTable(ctx, entry, table) {
|
|
2216
|
+
const local = entry.namespace === "LOCAL_ATTRS";
|
|
2217
|
+
const namespace = local ? null : entry.namespace;
|
|
2218
|
+
const hidden = !local && entry.namespace !== "SHARED_ATTRS";
|
|
2219
|
+
const target = (key) => {
|
|
2220
|
+
if (entry.element === "network") {
|
|
2221
|
+
return key === ctx.choice.graphId ? ctx.graph.atts : void 0;
|
|
2222
|
+
}
|
|
2223
|
+
return (entry.element === "node" ? ctx.nodes : ctx.edges).get(key)?.atts;
|
|
2224
|
+
};
|
|
2225
|
+
let unmatched = 0;
|
|
2226
|
+
for (const [key, cells] of table.rows) {
|
|
2227
|
+
const atts = target(key);
|
|
2228
|
+
if (atts !== void 0) {
|
|
2229
|
+
atts.push(...rowAtts(table, key, cells, namespace, hidden));
|
|
2230
|
+
} else if (!ctx.declared.has(key)) {
|
|
2231
|
+
unmatched++;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
for (const virtual of await virtualColumnsOf(table, ctx.virtuals, ctx.tableOf, ctx.report)) {
|
|
2235
|
+
const owned = table.columns.some((c) => c.name === virtual.column.name);
|
|
2236
|
+
const ns = local ? sharedNamespace(owned) : namespace;
|
|
2237
|
+
for (const [key, text] of virtual.values) {
|
|
2238
|
+
const atts = target(key);
|
|
2239
|
+
if (atts !== void 0) {
|
|
2240
|
+
pushAtt(atts, cellAtt(virtual.column, text, table.lines.get(key) ?? 0, ns, hidden));
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return unmatched;
|
|
2245
|
+
}
|
|
2246
|
+
function rowAtts(table, key, cells, namespace, hidden) {
|
|
2247
|
+
const line = table.lines.get(key) ?? 0;
|
|
2248
|
+
const out = [];
|
|
2249
|
+
for (let i = 1; i < table.columns.length && i < cells.length; i++) {
|
|
2250
|
+
pushAtt(out, cellAtt(table.columns[i], cells[i], line, namespace, hidden));
|
|
2251
|
+
}
|
|
2252
|
+
return out;
|
|
2253
|
+
}
|
|
2254
|
+
function sharedNamespace(owned) {
|
|
2255
|
+
return owned ? "SHARED_ATTRS" : null;
|
|
2256
|
+
}
|
|
2257
|
+
function pushAtt(atts, att2) {
|
|
2258
|
+
if (att2 !== null) {
|
|
2259
|
+
atts.push(att2);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
async function readViews(prepared, choice, nodes, edges) {
|
|
2263
|
+
const { report, inner, session } = prepared;
|
|
2264
|
+
const views = session.layout.views.filter((v) => v.network === choice.graphId).sort((a, b) => a.view.length - b.view.length || (a.view < b.view ? -1 : Number(a.view > b.view)));
|
|
2265
|
+
let visualStyle = null;
|
|
2266
|
+
const extraViews = [];
|
|
2267
|
+
const extraIds = [];
|
|
2268
|
+
let first = true;
|
|
2269
|
+
for (const view of views) {
|
|
2270
|
+
const viewDoc = (await parseOptionalEntry(session, view, report, inner, true))?.doc;
|
|
2271
|
+
if (viewDoc === void 0) {
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
if (first) {
|
|
2275
|
+
first = false;
|
|
2276
|
+
visualStyle = viewDoc.root.attrs.get("cy:visualStyle") ?? null;
|
|
2277
|
+
applyView(viewDoc, choice.graph, nodes, edges, report, view.name);
|
|
2278
|
+
} else {
|
|
2279
|
+
extraViews.push(viewPositions(viewDoc, prepared.zAs, nodes, report, view.name));
|
|
2280
|
+
extraIds.push(view.view);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
return { visualStyle, extraViews, extraIds };
|
|
2284
|
+
}
|
|
2285
|
+
async function readVirtuals(session, entry, report, inner) {
|
|
2286
|
+
const tree = await parseXmlTree(await session.read(entry, report), entry.name, report, inner);
|
|
2287
|
+
if (typeof tree === "string") {
|
|
2288
|
+
report.error(
|
|
2289
|
+
"parse-error",
|
|
2290
|
+
CYS_ISSUE.TABLE,
|
|
2291
|
+
`${entryLabel(entry.name)}: ${tree}; its virtual columns are not read`
|
|
2292
|
+
);
|
|
2293
|
+
return [];
|
|
2294
|
+
}
|
|
2295
|
+
const out = [];
|
|
2296
|
+
for (const node of elementsNamed(tree, "virtualColumn")) {
|
|
2297
|
+
const missing = ["name", "targetTable", "sourceTable", "sourceColumn"].filter((k) => !node.attrs.has(k));
|
|
2298
|
+
if (missing.length > 0) {
|
|
2299
|
+
const named = node.attrs.has("name") ? ` "${node.attrs.get("name") ?? ""}"` : "";
|
|
2300
|
+
report.error(
|
|
2301
|
+
"parse-error",
|
|
2302
|
+
CYS_ISSUE.TABLE,
|
|
2303
|
+
`${entryLabel(entry.name)}: a virtual column${named} has no ${missing.join(", ")}; the column is not read`
|
|
2304
|
+
);
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
const a = (key) => node.attrs.get(key) ?? "";
|
|
2308
|
+
out.push({
|
|
2309
|
+
name: a("name"),
|
|
2310
|
+
targetTable: a("targetTable"),
|
|
2311
|
+
sourceTable: a("sourceTable"),
|
|
2312
|
+
sourceColumn: a("sourceColumn"),
|
|
2313
|
+
sourceJoinKey: node.attrs.get("sourceJoinKey") ?? "SUID",
|
|
2314
|
+
targetJoinKey: node.attrs.get("targetJoinKey") ?? "SUID"
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
return out;
|
|
2318
|
+
}
|
|
2319
|
+
function byId(records) {
|
|
2320
|
+
const out = /* @__PURE__ */ new Map();
|
|
2321
|
+
for (const record of records) {
|
|
2322
|
+
if (record.id !== null && !out.has(record.id)) {
|
|
2323
|
+
out.set(record.id, record);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return out;
|
|
2327
|
+
}
|
|
2328
|
+
function applyView(view, graph, nodes, edges, report, entry) {
|
|
2329
|
+
let dangling = 0;
|
|
2330
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2331
|
+
for (const node of view.nodes) {
|
|
2332
|
+
const target = node.viewId === null ? void 0 : nodes.get(node.viewId);
|
|
2333
|
+
if (target === void 0) {
|
|
2334
|
+
dangling++;
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
duplicateViewNode(seen, node, report, entry);
|
|
2338
|
+
target.x = node.x;
|
|
2339
|
+
target.y = node.y;
|
|
2340
|
+
target.z = node.z;
|
|
2341
|
+
if (node.graphics !== null) {
|
|
2342
|
+
target.graphics = { ...target.graphics ?? {}, ...node.graphics };
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
for (const edge of view.edges) {
|
|
2346
|
+
const target = edge.viewId === null ? void 0 : edges.get(edge.viewId);
|
|
2347
|
+
if (target === void 0) {
|
|
2348
|
+
dangling++;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (edge.graphics !== null) {
|
|
2352
|
+
target.graphics = { ...target.graphics ?? {}, ...edge.graphics };
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (view.root.graphics !== null) {
|
|
2356
|
+
graph.graphics = { ...graph.graphics ?? {}, ...view.root.graphics };
|
|
2357
|
+
}
|
|
2358
|
+
if (dangling > 0) {
|
|
2359
|
+
report.warning(
|
|
2360
|
+
"validation-error",
|
|
2361
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
2362
|
+
`${entryLabel(entry)}: ${dangling} view element${plural(dangling)} ${agree(dangling, "names", "name")} no node or edge of the network; they are not read`
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function duplicateViewNode(seen, node, report, entry) {
|
|
2367
|
+
const id = node.viewId;
|
|
2368
|
+
if (seen.has(id)) {
|
|
2369
|
+
report.warning(
|
|
2370
|
+
"validation-error",
|
|
2371
|
+
XGMML_ISSUE.DUPLICATE_NODE,
|
|
2372
|
+
`${entryLabel(entry)}: the view gives node "${id}" twice; the later element is used`,
|
|
2373
|
+
{ line: node.line, element: id }
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
seen.add(id);
|
|
2377
|
+
}
|
|
2378
|
+
function viewPositions(view, zAs, nodes, report, entry) {
|
|
2379
|
+
const out = /* @__PURE__ */ new Map();
|
|
2380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2381
|
+
let dangling = 0;
|
|
2382
|
+
const coordinate = (text, node) => {
|
|
2383
|
+
if (text === null) {
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
const n = parseCoordinate(text);
|
|
2387
|
+
if (n === null) {
|
|
2388
|
+
report.error(
|
|
2389
|
+
"validation-error",
|
|
2390
|
+
XGMML_ISSUE.BAD_VALUE,
|
|
2391
|
+
`${entryLabel(entry)}: graphics coordinate "${text}" is not a number`,
|
|
2392
|
+
{
|
|
2393
|
+
line: node.line,
|
|
2394
|
+
element: node.viewId
|
|
2395
|
+
}
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
return n;
|
|
2399
|
+
};
|
|
2400
|
+
for (const node of view.nodes) {
|
|
2401
|
+
if (node.viewId === null || !nodes.has(node.viewId)) {
|
|
2402
|
+
dangling++;
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
duplicateViewNode(seen, node, report, entry);
|
|
2406
|
+
const x = coordinate(node.x, node);
|
|
2407
|
+
const y = coordinate(node.y, node);
|
|
2408
|
+
const z = zAs === "position" ? coordinate(node.z, node) : null;
|
|
2409
|
+
if (x !== null && y !== null) {
|
|
2410
|
+
out.set(node.viewId, [x, y === 0 ? 0 : -y, z ?? 0]);
|
|
2411
|
+
} else {
|
|
2412
|
+
out.delete(node.viewId);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
if (dangling > 0) {
|
|
2416
|
+
report.warning(
|
|
2417
|
+
"validation-error",
|
|
2418
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
2419
|
+
`${entryLabel(entry)}: ${dangling} view element${plural(dangling)} ${agree(dangling, "names", "name")} no node of the network; they are not read`
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
return out;
|
|
2423
|
+
}
|
|
2424
|
+
function writeViewPositions(sink, views, viewIds, report, common, restoredIds) {
|
|
2425
|
+
const coercer = new IdCoercer(common.ids);
|
|
2426
|
+
views.forEach((positions, i) => {
|
|
2427
|
+
const name = `${VIEW_POSITION_PREFIX}${i + 2}`;
|
|
2428
|
+
const { handle } = declareResolved(
|
|
2429
|
+
sink,
|
|
2430
|
+
"node",
|
|
2431
|
+
{
|
|
2432
|
+
name,
|
|
2433
|
+
dtype: "f32",
|
|
2434
|
+
components: 3,
|
|
2435
|
+
nullable: true,
|
|
2436
|
+
origin: {
|
|
2437
|
+
format: FORMAT,
|
|
2438
|
+
id: viewIds[i],
|
|
2439
|
+
title: null,
|
|
2440
|
+
type: "graphics",
|
|
2441
|
+
namespace: XGMML_ORIGIN_NAMESPACE
|
|
2442
|
+
},
|
|
2443
|
+
extra: { sourceDims: 2, units: "file" }
|
|
2444
|
+
},
|
|
2445
|
+
report,
|
|
2446
|
+
{ element: name }
|
|
2447
|
+
);
|
|
2448
|
+
for (const [id, xyz] of positions) {
|
|
2449
|
+
let index;
|
|
2450
|
+
try {
|
|
2451
|
+
index = sink.indexOf(coercer.text(restoredIds.get(id) ?? id));
|
|
2452
|
+
} catch {
|
|
2453
|
+
continue;
|
|
2454
|
+
}
|
|
2455
|
+
if (index !== INVALID_INDEX) {
|
|
2456
|
+
sink.setNodeValue(handle, index, xyz);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
function restoreIds(nodes) {
|
|
2462
|
+
const out = /* @__PURE__ */ new Map();
|
|
2463
|
+
for (const node of nodes) {
|
|
2464
|
+
const at = node.atts.findIndex(
|
|
2465
|
+
(a) => a.name === ORIGINAL_ID_ATTRIBUTE && (a.namespace ?? null) === null && a.value !== null && a.children.length === 0
|
|
2466
|
+
);
|
|
2467
|
+
if (node.id !== null && at >= 0) {
|
|
2468
|
+
out.set(node.id, node.atts[at].value);
|
|
2469
|
+
node.atts.splice(at, 1);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
return out;
|
|
2473
|
+
}
|
|
2474
|
+
function localName(graph) {
|
|
2475
|
+
const att2 = graph.atts.find((a) => a.name === "name" && (a.namespace ?? null) === null);
|
|
2476
|
+
return att2?.value ?? null;
|
|
2477
|
+
}
|
|
2478
|
+
function parentNetwork(graph, choices) {
|
|
2479
|
+
const att2 = graph.atts.find((a) => a.name === "__parentNetwork.SUID" || a.name === "Cy2 Parent Network.SUID");
|
|
2480
|
+
if (att2?.value === void 0 || att2.value === null || att2.value.length === 0) {
|
|
2481
|
+
return null;
|
|
2482
|
+
}
|
|
2483
|
+
const suid = att2.value;
|
|
2484
|
+
const named = choices.find((c) => c.era === "3" && c.graphId === suid);
|
|
2485
|
+
return named?.name ?? suid;
|
|
2486
|
+
}
|
|
2487
|
+
function pointerResolver(prepared, doc) {
|
|
2488
|
+
const names = /* @__PURE__ */ new Map();
|
|
2489
|
+
for (const graph of doc.graphs) {
|
|
2490
|
+
const { href } = graph;
|
|
2491
|
+
const hash = href?.indexOf("#") ?? -1;
|
|
2492
|
+
if (href === null || hash <= 0 || names.has(href)) {
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
const file = urlDecode(href.slice(0, hash));
|
|
2496
|
+
const id = href.slice(hash + 1);
|
|
2497
|
+
const network = prepared.session.layout.networks.find(
|
|
2498
|
+
(n) => urlDecode(n.path.slice("networks/".length)) === file
|
|
2499
|
+
);
|
|
2500
|
+
const parsed = network === void 0 ? void 0 : prepared.docs.get(network.name);
|
|
2501
|
+
const target = parsed?.doc.graphs.find((g) => g.id === id);
|
|
2502
|
+
if (target !== void 0) {
|
|
2503
|
+
names.set(href, target.label ?? id);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return (href) => names.get(href) ?? null;
|
|
2507
|
+
}
|
|
2508
|
+
function reportRootOnly(parsed, report, entry) {
|
|
2509
|
+
const held = /* @__PURE__ */ new Set();
|
|
2510
|
+
for (const graph of registeredGraphs(parsed)) {
|
|
2511
|
+
const members = membersOf(parsed.doc, graph);
|
|
2512
|
+
members.nodes.forEach((n) => held.add(n));
|
|
2513
|
+
members.edges.forEach((e) => held.add(e));
|
|
2514
|
+
}
|
|
2515
|
+
const nodes = parsed.doc.nodes.filter((n) => !held.has(n)).length;
|
|
2516
|
+
const edges = parsed.doc.edges.filter((e) => !held.has(e)).length;
|
|
2517
|
+
if (nodes + edges > 0) {
|
|
2518
|
+
report.warning(
|
|
2519
|
+
"unsupported",
|
|
2520
|
+
XGMML_ISSUE.ROOT_ONLY_ELEMENTS,
|
|
2521
|
+
`${entryLabel(entry)}: ${elementCount(nodes, edges)} ${agree(nodes + edges, "belongs", "belong")} to none of the session's networks (group meta-edges or collapsed group members) and ${agree(nodes + edges, "was", "were")} not read`
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
async function networks2(session, report, inner) {
|
|
2526
|
+
const { layout } = session;
|
|
2527
|
+
if (layout.cysession === null) {
|
|
2528
|
+
return report.fail(CYS_ISSUE.NOT_SESSION, "the session has no cysession.xml");
|
|
2529
|
+
}
|
|
2530
|
+
const tree = await readXmlTree(await session.read(layout.cysession, report), layout.cysession.name, report, inner);
|
|
2531
|
+
const documentVersion = tree.attrs.get("documentVersion") ?? "";
|
|
2532
|
+
if (documentVersion.length > 0 && !/^\d+(\.\d+)*$/.test(documentVersion.trim())) {
|
|
2533
|
+
report.warning(
|
|
2534
|
+
"validation-error",
|
|
2535
|
+
XGMML_ISSUE.DOCUMENT_VERSION,
|
|
2536
|
+
`${layout.cysession.name}: documentVersion "${documentVersion}" does not parse; the session is read as Cytoscape 2.x`
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
if (documentVersion.startsWith("3")) {
|
|
2540
|
+
report.fail(
|
|
2541
|
+
CYS_ISSUE.VERSION,
|
|
2542
|
+
`cysession.xml has documentVersion ${documentVersion} and no version marker: the 2011 Cytoscape 3.0 pre-release layout, which no released Cytoscape reads`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
const byDecoded = /* @__PURE__ */ new Map();
|
|
2546
|
+
for (const [path, entry] of layout.files) {
|
|
2547
|
+
byDecoded.set(path, entry);
|
|
2548
|
+
byDecoded.set(urlDecode(path), entry);
|
|
2549
|
+
}
|
|
2550
|
+
const order = [...layout.files.values()];
|
|
2551
|
+
const choices = [];
|
|
2552
|
+
const missing = [];
|
|
2553
|
+
const named = /* @__PURE__ */ new Set();
|
|
2554
|
+
for (const record of elementsNamed(tree, "network")) {
|
|
2555
|
+
const given = record.attrs.get("id");
|
|
2556
|
+
const filename = record.attrs.get("filename") ?? (given === void 0 ? void 0 : `${given}.xgmml`);
|
|
2557
|
+
if (given === "Network Root") {
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
if (filename === void 0) {
|
|
2561
|
+
sessionRecord(
|
|
2562
|
+
report,
|
|
2563
|
+
layout.cysession.name,
|
|
2564
|
+
"a <network> has neither an id nor a filename; it is not read"
|
|
2565
|
+
);
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
const id = given ?? urlDecode(filename.replace(/\.xgmml$/i, ""));
|
|
2569
|
+
if (given === void 0) {
|
|
2570
|
+
sessionRecord(
|
|
2571
|
+
report,
|
|
2572
|
+
layout.cysession.name,
|
|
2573
|
+
`a <network> for ${filename} has no id; it is named "${id}" after its file`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
const file = byDecoded.get(filename);
|
|
2577
|
+
if (file === void 0) {
|
|
2578
|
+
missing.push(filename);
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (named.has(file)) {
|
|
2582
|
+
sessionRecord(
|
|
2583
|
+
report,
|
|
2584
|
+
layout.cysession.name,
|
|
2585
|
+
`the <network> "${id}" names ${filename}, which an earlier record already names; it is not read a second time`
|
|
2586
|
+
);
|
|
2587
|
+
continue;
|
|
2588
|
+
}
|
|
2589
|
+
named.add(file);
|
|
2590
|
+
const parent = record.children.find((c) => c.name === "parent")?.attrs.get("id") ?? null;
|
|
2591
|
+
choices.push({
|
|
2592
|
+
era: "2",
|
|
2593
|
+
file,
|
|
2594
|
+
name: id,
|
|
2595
|
+
record,
|
|
2596
|
+
parent: parent === "Network Root" || parent === "NULL" ? null : parent
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
if (missing.length > 0) {
|
|
2600
|
+
report.warning(
|
|
2601
|
+
"validation-error",
|
|
2602
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
2603
|
+
`cysession.xml names ${missing.length} network file${plural(missing.length)} the session does not hold: ${listed(missing)}`
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
const unnamed = order.filter((e) => !named.has(e)).map((e) => e.name);
|
|
2607
|
+
if (unnamed.length > 0) {
|
|
2608
|
+
report.warning(
|
|
2609
|
+
"unsupported",
|
|
2610
|
+
CYS_ISSUE.ENTRY_SKIPPED,
|
|
2611
|
+
`${unnamed.length} network file${plural(unnamed.length)} ${agree(unnamed.length, "is", "are")} not in cysession.xml's network tree and were not read: ${listed(unnamed)}`
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
return choices.sort((a, b) => order.indexOf(a.file) - order.indexOf(b.file));
|
|
2615
|
+
}
|
|
2616
|
+
function sessionRecord(report, entry, message) {
|
|
2617
|
+
report.warning("validation-error", CYS_ISSUE.SESSION_RECORD, `${entryLabel(entry)}: ${message}`);
|
|
2618
|
+
}
|
|
2619
|
+
async function import2(prepared, choice, sink) {
|
|
2620
|
+
const { report, inner, session } = prepared;
|
|
2621
|
+
const { doc, dialect } = await parseEntry(session, choice.file, report, inner);
|
|
2622
|
+
let unmatched = 0;
|
|
2623
|
+
for (const [list, column] of [
|
|
2624
|
+
["selectedNodes", SELECTED_COLUMN],
|
|
2625
|
+
["hiddenNodes", HIDDEN_COLUMN]
|
|
2626
|
+
]) {
|
|
2627
|
+
unmatched += mark(doc.nodes, idsIn(choice.record, list), column);
|
|
2628
|
+
}
|
|
2629
|
+
for (const [list, column] of [
|
|
2630
|
+
["selectedEdges", SELECTED_COLUMN],
|
|
2631
|
+
["hiddenEdges", HIDDEN_COLUMN]
|
|
2632
|
+
]) {
|
|
2633
|
+
unmatched += mark(doc.edges, idsIn(choice.record, list), column);
|
|
2634
|
+
}
|
|
2635
|
+
if (unmatched > 0) {
|
|
2636
|
+
report.warning(
|
|
2637
|
+
"validation-error",
|
|
2638
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
2639
|
+
`cysession.xml lists ${unmatched} selected or hidden name${plural(unmatched)} that ${choice.file.name} does not hold; they are not read`
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
const cyMeta = sessionMeta(prepared, {
|
|
2643
|
+
collection: null,
|
|
2644
|
+
network: choice.name,
|
|
2645
|
+
parentNetwork: choice.parent,
|
|
2646
|
+
visualStyle: choice.record.attrs.get("visualStyle") ?? null
|
|
2647
|
+
});
|
|
2648
|
+
const extras = {
|
|
2649
|
+
sourceFormat: FORMAT,
|
|
2650
|
+
entry: choice.file.name,
|
|
2651
|
+
graphName: choice.name,
|
|
2652
|
+
metaExtra: { [META_KEY]: cyMeta }
|
|
2653
|
+
};
|
|
2654
|
+
const settings = { labelAliases: true, cytoscapeEscapes: true, zAs: prepared.zAs };
|
|
2655
|
+
new XgmmlEmitter(doc, dialect, sink, report, inner, settings, extras).emit(null);
|
|
2656
|
+
}
|
|
2657
|
+
function idsIn(record, list) {
|
|
2658
|
+
const out = /* @__PURE__ */ new Set();
|
|
2659
|
+
for (const holder of record.children.filter((c) => c.name === list)) {
|
|
2660
|
+
for (const item of holder.children) {
|
|
2661
|
+
const id = item.attrs.get("id");
|
|
2662
|
+
if (id !== void 0) {
|
|
2663
|
+
out.add(id);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return out;
|
|
2668
|
+
}
|
|
2669
|
+
function mark(records, ids, column) {
|
|
2670
|
+
if (ids.size === 0) {
|
|
2671
|
+
return 0;
|
|
2672
|
+
}
|
|
2673
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2674
|
+
for (const record of records) {
|
|
2675
|
+
if (record.id !== null && ids.has(record.id)) {
|
|
2676
|
+
matched.add(record.id);
|
|
2677
|
+
}
|
|
2678
|
+
if (record.label !== null && ids.has(record.label)) {
|
|
2679
|
+
matched.add(record.label);
|
|
2680
|
+
}
|
|
2681
|
+
if (record.id !== null && ids.has(record.id) || record.label !== null && ids.has(record.label)) {
|
|
2682
|
+
record.atts.push({
|
|
2683
|
+
name: column,
|
|
2684
|
+
type: null,
|
|
2685
|
+
cyType: "Boolean",
|
|
2686
|
+
elementType: null,
|
|
2687
|
+
value: "true",
|
|
2688
|
+
hidden: false,
|
|
2689
|
+
equation: false,
|
|
2690
|
+
children: [],
|
|
2691
|
+
extra: {},
|
|
2692
|
+
xml: null,
|
|
2693
|
+
hasGraph: false,
|
|
2694
|
+
text: "",
|
|
2695
|
+
line: record.line,
|
|
2696
|
+
namespace: CYTOSCAPE_NAMESPACE
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return ids.size - matched.size;
|
|
2701
|
+
}
|
|
2702
|
+
function sessionMeta(prepared, facts) {
|
|
2703
|
+
const { layout } = prepared.session;
|
|
2704
|
+
const { report } = prepared;
|
|
2705
|
+
if (layout.styles.length > 0) {
|
|
2706
|
+
report.warning(
|
|
2707
|
+
"unsupported",
|
|
2708
|
+
CYS_ISSUE.STYLES_NOT_IMPORTED,
|
|
2709
|
+
`the session's styles (${layout.styles.map((s) => s.path).join(", ")}) are not applied`
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
if (layout.skipped.length > 0) {
|
|
2713
|
+
report.warning(
|
|
2714
|
+
"unsupported",
|
|
2715
|
+
CYS_ISSUE.ENTRY_SKIPPED,
|
|
2716
|
+
`${layout.skipped.length} ${layout.skipped.length === 1 ? "entry holds" : "entries hold"} no graph data and ${agree(layout.skipped.length, "was", "were")} not read (apps, global tables, properties, images): ${listed(layout.skipped)}`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
return {
|
|
2720
|
+
sessionVersion: layout.version,
|
|
2721
|
+
...facts,
|
|
2722
|
+
skippedEntries: [...layout.skipped]
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
async function importChoice(prepared, index, sink) {
|
|
2726
|
+
const choice = prepared.choices[index];
|
|
2727
|
+
if (choice.era === "3") {
|
|
2728
|
+
const parsed = prepared.docs.get(choice.network.name);
|
|
2729
|
+
await import3(prepared, choice, sink, parsed);
|
|
2730
|
+
} else {
|
|
2731
|
+
await import2(prepared, choice, sink);
|
|
2732
|
+
}
|
|
2733
|
+
throwIfAborted(prepared.common.signal);
|
|
2734
|
+
return prepared.report.finish();
|
|
2735
|
+
}
|
|
2736
|
+
async function importCys(input, sink, options) {
|
|
2737
|
+
const prepared = await prepare(input, sink, options);
|
|
2738
|
+
const index = chooseGraph(
|
|
2739
|
+
prepared.choices.map((c) => c.name),
|
|
2740
|
+
options,
|
|
2741
|
+
prepared.report
|
|
2742
|
+
);
|
|
2743
|
+
if (prepared.choices.length > 1 && !graphChosen(options)) {
|
|
2744
|
+
prepared.report.warning(
|
|
2745
|
+
"unsupported",
|
|
2746
|
+
CYS_ISSUE.MULTIPLE_GRAPHS,
|
|
2747
|
+
`the session holds ${prepared.choices.length} networks; only the first is read (importAllGraphs() reads every one; graphIndex or graphName chooses one)`
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
const report = await importChoice(prepared, index, sink);
|
|
2751
|
+
prepared.session.done();
|
|
2752
|
+
return report;
|
|
2753
|
+
}
|
|
2754
|
+
async function importAllCys(input, sinkFor, options) {
|
|
2755
|
+
const first = await prepare(input, null, options);
|
|
2756
|
+
const opened = first.report.fork();
|
|
2757
|
+
const reports = [];
|
|
2758
|
+
for (let i = 0; i < first.choices.length; i++) {
|
|
2759
|
+
const sink = sinkFor(i);
|
|
2760
|
+
const prepared = i === 0 ? first : { ...first, report: opened.fork() };
|
|
2761
|
+
reportSinkOptions(sink, options, prepared.report, true);
|
|
2762
|
+
reportUnusedOptions(options, prepared.report, USED_OPTIONS);
|
|
2763
|
+
reports.push(await importChoice(prepared, i, sink));
|
|
2764
|
+
}
|
|
2765
|
+
first.session.done();
|
|
2766
|
+
return reports;
|
|
2767
|
+
}
|
|
2768
|
+
async function listCysGraphs(input, options) {
|
|
2769
|
+
const prepared = await prepare(input, null, options);
|
|
2770
|
+
return prepared.choices.map(
|
|
2771
|
+
(choice, index) => Object.freeze({
|
|
2772
|
+
index,
|
|
2773
|
+
name: choice.name,
|
|
2774
|
+
nodes: choice.era === "3" ? choice.nodes : null,
|
|
2775
|
+
edges: choice.era === "3" ? choice.edges : null
|
|
2776
|
+
})
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
const SESSION_NAMES = [
|
|
2780
|
+
/CytoscapeSession|cysession\.xml/,
|
|
2781
|
+
new RegExp("(?<!\\d)\\d+\\.\\d+\\.\\d+\\.version"),
|
|
2782
|
+
/(?:^|\/)apps\/org\.cytoscape\./,
|
|
2783
|
+
/(?:^|\/)(?:networks|views)\/\d+-[^/]*\.xgmml/,
|
|
2784
|
+
/(?:^|\/)tables\/\d+-[^/]*\/[^/]+\.cytable/
|
|
2785
|
+
];
|
|
2786
|
+
function sniffCys(head) {
|
|
2787
|
+
for (let at = 0; at + 4 <= head.byteLength; at++) {
|
|
2788
|
+
if (startsLikeZip(head.subarray(at))) {
|
|
2789
|
+
return SESSION_NAMES.some((r) => r.test(decodeEntryName(head.subarray(at)))) ? 0.95 : 0;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return 0;
|
|
2793
|
+
}
|
|
2794
|
+
const cysImporter = Object.freeze({
|
|
2795
|
+
format: FORMAT,
|
|
2796
|
+
options: Object.freeze(["maxUncompressedBytes", "zAs"]),
|
|
2797
|
+
extensions: EXTENSIONS,
|
|
2798
|
+
mimeTypes: MIME_TYPES,
|
|
2799
|
+
sniff: sniffCys,
|
|
2800
|
+
import: importCys,
|
|
2801
|
+
importAll: importAllCys,
|
|
2802
|
+
listGraphs: listCysGraphs
|
|
2803
|
+
});
|
|
2804
|
+
export {
|
|
2805
|
+
CYS_CAPABILITIES as C,
|
|
2806
|
+
CYS_ISSUE as a,
|
|
2807
|
+
CYS_LOSS as b,
|
|
2808
|
+
cysExporter as c,
|
|
2809
|
+
cysImporter as d
|
|
2810
|
+
};
|
|
2811
|
+
//# sourceMappingURL=importer-90YWhJoF.js.map
|