@graphty/graph-io 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -414
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -1
- package/dist/chunks/choose-BbJI55dx.js +56 -0
- package/dist/chunks/choose-BbJI55dx.js.map +1 -0
- package/dist/chunks/constants-BCsFsMj4.js +231 -0
- package/dist/chunks/constants-BCsFsMj4.js.map +1 -0
- package/dist/chunks/{escape-B-tkk_Cl.js → escape-D0g6NNUy.js} +382 -92
- package/dist/chunks/escape-D0g6NNUy.js.map +1 -0
- package/dist/chunks/export-BTI4yQgy.js +4333 -0
- package/dist/chunks/export-BTI4yQgy.js.map +1 -0
- package/dist/chunks/exporter-BMqk5oxW.js +3465 -0
- package/dist/chunks/exporter-BMqk5oxW.js.map +1 -0
- package/dist/chunks/{importer-eUREhBGj.js → exporter-Cra2GYcN.js} +4969 -3201
- package/dist/chunks/exporter-Cra2GYcN.js.map +1 -0
- package/dist/chunks/{exporter-BGrsWamJ.js → exporter-Dxh231ZF.js} +38 -229
- package/dist/chunks/exporter-Dxh231ZF.js.map +1 -0
- package/dist/chunks/{exporter-DIeGJXAZ.js → exporter-tne475KK.js} +22 -20
- package/dist/chunks/exporter-tne475KK.js.map +1 -0
- package/dist/chunks/importer-90YWhJoF.js +2811 -0
- package/dist/chunks/importer-90YWhJoF.js.map +1 -0
- package/dist/chunks/{importer-D_e7e7LX.js → importer-Bcl1d4xH.js} +398 -145
- package/dist/chunks/importer-Bcl1d4xH.js.map +1 -0
- package/dist/chunks/{importer-BWY2FFc5.js → importer-BfIsp1f9.js} +255 -77
- package/dist/chunks/importer-BfIsp1f9.js.map +1 -0
- package/dist/chunks/{importer-zGLo8Dg_.js → importer-C_TEA1zJ.js} +484 -157
- package/dist/chunks/importer-C_TEA1zJ.js.map +1 -0
- package/dist/chunks/{importer-DET1tg6s.js → importer-DCjOvO7U.js} +1687 -310
- package/dist/chunks/importer-DCjOvO7U.js.map +1 -0
- package/dist/chunks/{importer-vi3bSdtw.js → importer-DfVu6j59.js} +509 -76
- package/dist/chunks/importer-DfVu6j59.js.map +1 -0
- package/dist/chunks/{importer-DIrbbnAf.js → importer-Dku_VAIY.js} +585 -226
- package/dist/chunks/importer-Dku_VAIY.js.map +1 -0
- package/dist/chunks/json-elements-3tnyt5uZ.js +1225 -0
- package/dist/chunks/json-elements-3tnyt5uZ.js.map +1 -0
- package/dist/chunks/ontology-export-B9Tcf37D.js +357 -0
- package/dist/chunks/ontology-export-B9Tcf37D.js.map +1 -0
- package/dist/chunks/{records-dbRkxwaq.js → records-CQXT__mY.js} +298 -114
- package/dist/chunks/records-CQXT__mY.js.map +1 -0
- package/dist/chunks/{text-Dr0Ifpag.js → text-DmcUs5_Q.js} +29 -11
- package/dist/chunks/text-DmcUs5_Q.js.map +1 -0
- package/dist/chunks/{writer-C7flM-Ih.js → writer-DxjgHwSe.js} +19 -1
- package/dist/chunks/writer-DxjgHwSe.js.map +1 -0
- package/dist/csv.js +951 -261
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +6 -3
- package/dist/cx2.js +2 -2
- package/dist/cys.js +6 -3
- package/dist/dot.js +1 -1
- package/dist/gexf.js +554 -122
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +342 -85
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +1105 -213
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +3 -2
- package/dist/graphml.js.map +1 -1
- package/dist/json.js +3 -2
- package/dist/neo4j.js +564 -157
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +6 -3
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts +18 -6
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +6 -3
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts +29 -11
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +25 -7
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/cell-budget.d.ts +9 -2
- package/dist/src/common/cell-budget.d.ts.map +1 -1
- package/dist/src/common/cell-budget.js +11 -3
- package/dist/src/common/cell-budget.js.map +1 -1
- package/dist/src/common/choose.d.ts +19 -0
- package/dist/src/common/choose.d.ts.map +1 -0
- package/dist/src/common/choose.js +72 -0
- package/dist/src/common/choose.js.map +1 -0
- package/dist/src/common/codes.d.ts +456 -66
- package/dist/src/common/codes.d.ts.map +1 -1
- package/dist/src/common/codes.js +458 -66
- package/dist/src/common/codes.js.map +1 -1
- package/dist/src/common/compare.d.ts +81 -0
- package/dist/src/common/compare.d.ts.map +1 -0
- package/dist/src/common/compare.js +394 -0
- package/dist/src/common/compare.js.map +1 -0
- package/dist/src/common/cx-export.d.ts +123 -0
- package/dist/src/common/cx-export.d.ts.map +1 -0
- package/dist/src/common/cx-export.js +334 -0
- package/dist/src/common/cx-export.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +22 -2
- package/dist/src/common/declared-types.d.ts.map +1 -1
- package/dist/src/common/declared-types.js +19 -1
- package/dist/src/common/declared-types.js.map +1 -1
- package/dist/src/common/direction.d.ts +74 -50
- package/dist/src/common/direction.d.ts.map +1 -1
- package/dist/src/common/direction.js +47 -27
- package/dist/src/common/direction.js.map +1 -1
- package/dist/src/common/escape.d.ts +21 -9
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +33 -17
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/common/export.d.ts +144 -32
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +216 -42
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/common/format.d.ts +23 -1
- package/dist/src/common/format.d.ts.map +1 -1
- package/dist/src/common/format.js +26 -1
- package/dist/src/common/format.js.map +1 -1
- package/dist/src/common/ids.d.ts +9 -2
- package/dist/src/common/ids.d.ts.map +1 -1
- package/dist/src/common/ids.js +6 -2
- package/dist/src/common/ids.js.map +1 -1
- package/dist/src/common/input.d.ts +122 -28
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +890 -124
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.d.ts +144 -20
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +868 -188
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/line-format.d.ts +90 -0
- package/dist/src/common/line-format.d.ts.map +1 -0
- package/dist/src/common/line-format.js +185 -0
- package/dist/src/common/line-format.js.map +1 -0
- package/dist/src/common/lists.d.ts +6 -1
- package/dist/src/common/lists.d.ts.map +1 -1
- package/dist/src/common/lists.js +2 -0
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/ontology-export.d.ts +167 -0
- package/dist/src/common/ontology-export.d.ts.map +1 -0
- package/dist/src/common/ontology-export.js +359 -0
- package/dist/src/common/ontology-export.js.map +1 -0
- package/dist/src/common/ontology.d.ts +25 -3
- package/dist/src/common/ontology.d.ts.map +1 -1
- package/dist/src/common/ontology.js +30 -9
- package/dist/src/common/ontology.js.map +1 -1
- package/dist/src/common/options.d.ts +62 -12
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +89 -11
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/plural.d.ts +31 -0
- package/dist/src/common/plural.d.ts.map +1 -0
- package/dist/src/common/plural.js +46 -0
- package/dist/src/common/plural.js.map +1 -0
- package/dist/src/common/report.d.ts +72 -14
- package/dist/src/common/report.d.ts.map +1 -1
- package/dist/src/common/report.js +146 -22
- package/dist/src/common/report.js.map +1 -1
- package/dist/src/common/temporal.d.ts +22 -5
- package/dist/src/common/temporal.d.ts.map +1 -1
- package/dist/src/common/temporal.js +13 -2
- package/dist/src/common/temporal.js.map +1 -1
- package/dist/src/common/text.d.ts +39 -23
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +51 -21
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/weights.d.ts +37 -4
- package/dist/src/common/weights.d.ts.map +1 -1
- package/dist/src/common/weights.js +61 -2
- package/dist/src/common/weights.js.map +1 -1
- package/dist/src/common/writer.d.ts +19 -3
- package/dist/src/common/writer.d.ts.map +1 -1
- package/dist/src/common/writer.js +30 -2
- package/dist/src/common/writer.js.map +1 -1
- package/dist/src/common/xml.d.ts +125 -9
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +400 -83
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts +44 -4
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +134 -5
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts +80 -24
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +145 -32
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/header.d.ts +38 -9
- package/dist/src/formats/csv/header.d.ts.map +1 -1
- package/dist/src/formats/csv/header.js +46 -10
- package/dist/src/formats/csv/header.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts +187 -37
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +835 -204
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/index.d.ts +100 -19
- package/dist/src/formats/csv/index.d.ts.map +1 -1
- package/dist/src/formats/csv/index.js +94 -21
- package/dist/src/formats/csv/index.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts +95 -10
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +333 -122
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/csv/values.d.ts +1 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -1
- package/dist/src/formats/csv/values.js +1 -0
- package/dist/src/formats/csv/values.js.map +1 -1
- package/dist/src/formats/cx/exporter.d.ts +102 -0
- package/dist/src/formats/cx/exporter.d.ts.map +1 -0
- package/dist/src/formats/cx/exporter.js +1217 -0
- package/dist/src/formats/cx/exporter.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts +79 -32
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +680 -285
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx/index.d.ts +4 -3
- package/dist/src/formats/cx/index.d.ts.map +1 -1
- package/dist/src/formats/cx/index.js +4 -3
- package/dist/src/formats/cx/index.js.map +1 -1
- package/dist/src/formats/cx2/exporter.d.ts +29 -11
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +39 -268
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts +78 -28
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +226 -79
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cx2/index.d.ts +2 -1
- package/dist/src/formats/cx2/index.d.ts.map +1 -1
- package/dist/src/formats/cx2/index.js +2 -1
- package/dist/src/formats/cx2/index.js.map +1 -1
- package/dist/src/formats/cys/constants.d.ts +66 -17
- package/dist/src/formats/cys/constants.d.ts.map +1 -1
- package/dist/src/formats/cys/constants.js +59 -18
- package/dist/src/formats/cys/constants.js.map +1 -1
- package/dist/src/formats/cys/exporter.d.ts +118 -0
- package/dist/src/formats/cys/exporter.d.ts.map +1 -0
- package/dist/src/formats/cys/exporter.js +1072 -0
- package/dist/src/formats/cys/exporter.js.map +1 -0
- package/dist/src/formats/cys/importer.d.ts +22 -8
- package/dist/src/formats/cys/importer.d.ts.map +1 -1
- package/dist/src/formats/cys/importer.js +510 -149
- package/dist/src/formats/cys/importer.js.map +1 -1
- package/dist/src/formats/cys/index.d.ts +4 -2
- package/dist/src/formats/cys/index.d.ts.map +1 -1
- package/dist/src/formats/cys/index.js +4 -2
- package/dist/src/formats/cys/index.js.map +1 -1
- package/dist/src/formats/cys/session.d.ts +46 -7
- package/dist/src/formats/cys/session.d.ts.map +1 -1
- package/dist/src/formats/cys/session.js +98 -24
- package/dist/src/formats/cys/session.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts +9 -2
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +29 -7
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts +42 -14
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +56 -29
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts +91 -29
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +190 -39
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/index.d.ts +2 -1
- package/dist/src/formats/dot/index.d.ts.map +1 -1
- package/dist/src/formats/dot/index.js +2 -1
- package/dist/src/formats/dot/index.js.map +1 -1
- package/dist/src/formats/dot/names.d.ts +44 -11
- package/dist/src/formats/dot/names.d.ts.map +1 -1
- package/dist/src/formats/dot/names.js +44 -11
- package/dist/src/formats/dot/names.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +9 -2
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +20 -5
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts +46 -15
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +45 -15
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +87 -23
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +471 -90
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +84 -21
- package/dist/src/formats/gexf/index.d.ts.map +1 -1
- package/dist/src/formats/gexf/index.js +80 -23
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gexf/schema.d.ts +67 -15
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +59 -13
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts +69 -21
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +74 -29
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +116 -25
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +290 -61
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +87 -24
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +79 -26
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts +67 -18
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +108 -26
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/constants.d.ts +174 -44
- package/dist/src/formats/graphml/constants.d.ts.map +1 -1
- package/dist/src/formats/graphml/constants.js +165 -44
- package/dist/src/formats/graphml/constants.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts +17 -9
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +33 -12
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts +19 -7
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +466 -39
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/index.d.ts +2 -1
- package/dist/src/formats/graphml/index.d.ts.map +1 -1
- package/dist/src/formats/graphml/index.js +2 -1
- package/dist/src/formats/graphml/index.js.map +1 -1
- package/dist/src/formats/graphml/tree.d.ts +6 -2
- package/dist/src/formats/graphml/tree.d.ts.map +1 -1
- package/dist/src/formats/graphml/tree.js +6 -2
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +8 -3
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +36 -15
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts +88 -24
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +109 -19
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts +134 -22
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +135 -54
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +173 -52
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +899 -220
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +3 -2
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +3 -2
- package/dist/src/formats/json/index.js.map +1 -1
- package/dist/src/formats/json/obographs-export.d.ts +68 -0
- package/dist/src/formats/json/obographs-export.d.ts.map +1 -0
- package/dist/src/formats/json/obographs-export.js +941 -0
- package/dist/src/formats/json/obographs-export.js.map +1 -0
- package/dist/src/formats/json/obographs.d.ts +26 -0
- package/dist/src/formats/json/obographs.d.ts.map +1 -1
- package/dist/src/formats/json/obographs.js +297 -80
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts +109 -21
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +238 -85
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts +17 -4
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +36 -4
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts +107 -27
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +415 -63
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/neo4j/index.d.ts +73 -23
- package/dist/src/formats/neo4j/index.d.ts.map +1 -1
- package/dist/src/formats/neo4j/index.js +65 -25
- package/dist/src/formats/neo4j/index.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts +150 -0
- package/dist/src/formats/obo/exporter.d.ts.map +1 -0
- package/dist/src/formats/obo/exporter.js +1736 -0
- package/dist/src/formats/obo/exporter.js.map +1 -0
- package/dist/src/formats/obo/importer.d.ts +95 -19
- package/dist/src/formats/obo/importer.d.ts.map +1 -1
- package/dist/src/formats/obo/importer.js +475 -104
- package/dist/src/formats/obo/importer.js.map +1 -1
- package/dist/src/formats/obo/index.d.ts +6 -3
- package/dist/src/formats/obo/index.d.ts.map +1 -1
- package/dist/src/formats/obo/index.js +6 -3
- package/dist/src/formats/obo/index.js.map +1 -1
- package/dist/src/formats/obo/syntax.d.ts +108 -4
- package/dist/src/formats/obo/syntax.d.ts.map +1 -1
- package/dist/src/formats/obo/syntax.js +211 -34
- package/dist/src/formats/obo/syntax.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts +54 -16
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +66 -34
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +84 -22
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +303 -83
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/index.d.ts +2 -1
- package/dist/src/formats/pajek/index.d.ts.map +1 -1
- package/dist/src/formats/pajek/index.js +2 -1
- package/dist/src/formats/pajek/index.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +77 -14
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +183 -51
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts +1 -0
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
- package/dist/src/formats/xgmml/columns.js +4 -0
- package/dist/src/formats/xgmml/columns.js.map +1 -1
- package/dist/src/formats/xgmml/constants.d.ts +186 -56
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
- package/dist/src/formats/xgmml/constants.js +181 -57
- package/dist/src/formats/xgmml/constants.js.map +1 -1
- package/dist/src/formats/xgmml/document.d.ts +65 -11
- package/dist/src/formats/xgmml/document.d.ts.map +1 -1
- package/dist/src/formats/xgmml/document.js +157 -28
- package/dist/src/formats/xgmml/document.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +61 -3
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +129 -26
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts +13 -6
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +20 -21
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts +33 -12
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +35 -72
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/formats/xgmml/index.d.ts +1 -0
- package/dist/src/formats/xgmml/index.d.ts.map +1 -1
- package/dist/src/formats/xgmml/index.js +1 -0
- package/dist/src/formats/xgmml/index.js.map +1 -1
- package/dist/src/formats/xgmml/values.d.ts +8 -1
- package/dist/src/formats/xgmml/values.d.ts.map +1 -1
- package/dist/src/formats/xgmml/values.js +7 -2
- package/dist/src/formats/xgmml/values.js.map +1 -1
- package/dist/src/index.d.ts +23 -18
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +28 -16
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +478 -92
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +807 -88
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +61 -19
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +121 -29
- package/dist/src/sniff.js.map +1 -1
- package/dist/src/types.d.ts +362 -112
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +28 -7
- package/dist/src/types.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/docs/samples/README.md +25 -0
- package/docs/samples/got-edges.csv +353 -0
- package/docs/samples/got-network.graphml +1384 -0
- package/docs/samples/got-nodes.csv +108 -0
- package/docs/samples/got.cx +930 -0
- package/docs/samples/got.cx2 +465 -0
- package/docs/samples/got.gexf +471 -0
- package/docs/samples/got.gml +2650 -0
- package/docs/samples/got.json +2546 -0
- package/docs/samples/got.net +461 -0
- package/docs/samples/movies-nodes.csv +9 -0
- package/docs/samples/movies-rels.csv +8 -0
- package/docs/samples/networks.cys +0 -0
- package/docs/samples/proteins.xgmml +9 -0
- package/docs/samples/teams.gv +24 -0
- package/docs/samples/vehicles.obo +25 -0
- package/package.json +9 -4
- package/src/children.ts +18 -6
- package/src/common/attributes.ts +29 -11
- package/src/common/cell-budget.ts +11 -3
- package/src/common/choose.ts +89 -0
- package/src/common/codes.ts +490 -66
- package/src/common/compare.ts +472 -0
- package/src/common/cx-export.ts +432 -0
- package/src/common/declared-types.ts +38 -4
- package/src/common/direction.ts +78 -51
- package/src/common/escape.ts +33 -17
- package/src/common/export.ts +284 -47
- package/src/common/format.ts +30 -1
- package/src/common/ids.ts +14 -3
- package/src/common/input.ts +1057 -134
- package/src/common/json-elements.ts +1034 -198
- package/src/common/line-format.ts +249 -0
- package/src/common/lists.ts +6 -1
- package/src/common/ontology-export.ts +461 -0
- package/src/common/ontology.ts +31 -9
- package/src/common/options.ts +110 -15
- package/src/common/plural.ts +49 -0
- package/src/common/report.ts +168 -25
- package/src/common/temporal.ts +22 -5
- package/src/common/text.ts +62 -23
- package/src/common/weights.ts +74 -4
- package/src/common/writer.ts +37 -3
- package/src/common/xml.ts +477 -88
- package/src/common/zip.ts +169 -7
- package/src/formats/csv/exporter.ts +201 -46
- package/src/formats/csv/header.ts +50 -11
- package/src/formats/csv/importer.ts +1133 -245
- package/src/formats/csv/index.ts +106 -19
- package/src/formats/csv/records.ts +396 -122
- package/src/formats/csv/values.ts +1 -0
- package/src/formats/cx/exporter.ts +1483 -0
- package/src/formats/cx/importer.ts +888 -300
- package/src/formats/cx/index.ts +4 -3
- package/src/formats/cx2/exporter.ts +61 -338
- package/src/formats/cx2/importer.ts +317 -86
- package/src/formats/cx2/index.ts +2 -1
- package/src/formats/cys/constants.ts +62 -17
- package/src/formats/cys/exporter.ts +1368 -0
- package/src/formats/cys/importer.ts +659 -157
- package/src/formats/cys/index.ts +4 -2
- package/src/formats/cys/session.ts +139 -31
- package/src/formats/cys/tables.ts +47 -9
- package/src/formats/dot/exporter.ts +83 -37
- package/src/formats/dot/importer.ts +248 -54
- package/src/formats/dot/index.ts +2 -1
- package/src/formats/dot/names.ts +44 -11
- package/src/formats/dot/tokenizer.ts +25 -6
- package/src/formats/gexf/exporter.ts +57 -18
- package/src/formats/gexf/importer.ts +636 -105
- package/src/formats/gexf/index.ts +90 -21
- package/src/formats/gexf/schema.ts +67 -15
- package/src/formats/gml/exporter.ts +90 -36
- package/src/formats/gml/importer.ts +388 -67
- package/src/formats/gml/index.ts +85 -24
- package/src/formats/gml/syntax.ts +111 -26
- package/src/formats/graphml/constants.ts +178 -44
- package/src/formats/graphml/exporter.ts +52 -18
- package/src/formats/graphml/importer.ts +640 -60
- package/src/formats/graphml/index.ts +2 -1
- package/src/formats/graphml/tree.ts +6 -2
- package/src/formats/graphml/yfiles.ts +37 -15
- package/src/formats/json/dialect.ts +128 -29
- package/src/formats/json/exporter.ts +205 -68
- package/src/formats/json/importer.ts +1163 -253
- package/src/formats/json/index.ts +3 -1
- package/src/formats/json/obographs-export.ts +1197 -0
- package/src/formats/json/obographs.ts +353 -92
- package/src/formats/neo4j/exporter.ts +320 -101
- package/src/formats/neo4j/header.ts +44 -6
- package/src/formats/neo4j/importer.ts +512 -73
- package/src/formats/neo4j/index.ts +75 -23
- package/src/formats/obo/exporter.ts +2210 -0
- package/src/formats/obo/importer.ts +622 -109
- package/src/formats/obo/index.ts +6 -3
- package/src/formats/obo/syntax.ts +232 -37
- package/src/formats/pajek/exporter.ts +89 -41
- package/src/formats/pajek/importer.ts +407 -93
- package/src/formats/pajek/index.ts +2 -1
- package/src/formats/pajek/syntax.ts +213 -52
- package/src/formats/xgmml/columns.ts +10 -0
- package/src/formats/xgmml/constants.ts +183 -56
- package/src/formats/xgmml/document.ts +212 -34
- package/src/formats/xgmml/emit.ts +175 -34
- package/src/formats/xgmml/exporter.ts +35 -27
- package/src/formats/xgmml/importer.ts +64 -86
- package/src/formats/xgmml/index.ts +1 -0
- package/src/formats/xgmml/values.ts +11 -3
- package/src/index.ts +81 -11
- package/src/registry.ts +1121 -135
- package/src/sniff.ts +163 -32
- package/src/types.ts +385 -114
- package/dist/chunks/escape-B-tkk_Cl.js.map +0 -1
- package/dist/chunks/export-Bh60Dv-n.js +0 -582
- package/dist/chunks/export-Bh60Dv-n.js.map +0 -1
- package/dist/chunks/exporter-BGrsWamJ.js.map +0 -1
- package/dist/chunks/exporter-DIeGJXAZ.js.map +0 -1
- package/dist/chunks/importer-BWY2FFc5.js.map +0 -1
- package/dist/chunks/importer-DD-xv9_Y.js +0 -1471
- package/dist/chunks/importer-DD-xv9_Y.js.map +0 -1
- package/dist/chunks/importer-DET1tg6s.js.map +0 -1
- package/dist/chunks/importer-DEcKhsmQ.js +0 -1614
- package/dist/chunks/importer-DEcKhsmQ.js.map +0 -1
- package/dist/chunks/importer-DIrbbnAf.js.map +0 -1
- package/dist/chunks/importer-D_e7e7LX.js.map +0 -1
- package/dist/chunks/importer-eUREhBGj.js.map +0 -1
- package/dist/chunks/importer-vi3bSdtw.js.map +0 -1
- package/dist/chunks/importer-zGLo8Dg_.js.map +0 -1
- package/dist/chunks/json-elements-DDS8N2Dd.js +0 -779
- package/dist/chunks/json-elements-DDS8N2Dd.js.map +0 -1
- package/dist/chunks/ontology-BnrJ4I98.js +0 -113
- package/dist/chunks/ontology-BnrJ4I98.js.map +0 -1
- package/dist/chunks/records-dbRkxwaq.js.map +0 -1
- package/dist/chunks/report-B1z4WT9e.js +0 -2372
- package/dist/chunks/report-B1z4WT9e.js.map +0 -1
- package/dist/chunks/text-Dr0Ifpag.js.map +0 -1
- package/dist/chunks/weights-Dzba96G3.js +0 -176
- package/dist/chunks/weights-Dzba96G3.js.map +0 -1
- package/dist/chunks/writer-C7flM-Ih.js.map +0 -1
|
@@ -0,0 +1,2210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OBO exporter: writes a snapshot as an OBO 1.4 flat file, the text format of the Gene
|
|
3
|
+
* Ontology and the OBO Foundry ontologies.
|
|
4
|
+
*
|
|
5
|
+
* Every node is a frame (`[Term]`, or `[Instance]` / `[Typedef]` from the `type` column) in node
|
|
6
|
+
* order; every edge is a clause on its source's frame: `is_a: T` for the relation `is_a`,
|
|
7
|
+
* `instance_of: T` on an Instance, `relationship: R T` for anything else, an edge without a
|
|
8
|
+
* relation as `is_a` (the `relation` option). The columns of the OBO vocabulary
|
|
9
|
+
* (src/common/ontology.ts: `name`, `namespace`, `def`, `synonym`, `xref`, ...) are written back
|
|
10
|
+
* as their tags; any other node column becomes `property_value: <column> "<value>" xsd:<type>`
|
|
11
|
+
* lines (with a `[Typedef]` declaring the column as a metadata relation), any other edge column a
|
|
12
|
+
* qualifier of the edge's clause. A placeholder node (`graphty.placeholder`, a target no frame
|
|
13
|
+
* declared) gets no frame, so the re-import makes it again. The header, the Typedefs and the
|
|
14
|
+
* unknown frames an OBO import kept in `meta.extra.obo` are written back.
|
|
15
|
+
*
|
|
16
|
+
* check() predicts every difference the OBO importer produces on re-import: a vocabulary column
|
|
17
|
+
* whose values the file cannot carry exactly is written as `property_value` lines instead and
|
|
18
|
+
* reported, never written half right.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type Column, GraphFormatError, type GraphSnapshot, type NodeId } from "@graphty/graph-format";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
COLUMN_AS_PROPERTY_VALUE_CODE,
|
|
25
|
+
GRAPH_COLUMN_AS_METADATA_CODE,
|
|
26
|
+
NODE_ORDER_CODE,
|
|
27
|
+
RELATION_ASSUMED_CODE,
|
|
28
|
+
ROLE_ASSUMED_CODE,
|
|
29
|
+
TYPEDEF_NODES_CODE,
|
|
30
|
+
} from "../../common/codes.js";
|
|
31
|
+
import { type PairFolding, pairFolding } from "../../common/direction.js";
|
|
32
|
+
import { capabilities, LOSS } from "../../common/export.js";
|
|
33
|
+
import { formatNumber } from "../../common/format.js";
|
|
34
|
+
import { OBO_NODE_COLUMNS, PLACEHOLDER_COLUMN, SYNONYM_SCOPES } from "../../common/ontology.js";
|
|
35
|
+
import {
|
|
36
|
+
capabilityNotes,
|
|
37
|
+
cellOf,
|
|
38
|
+
emptyColumnNotes,
|
|
39
|
+
fitsVocabulary,
|
|
40
|
+
FRAME_KINDS,
|
|
41
|
+
type FrameKind,
|
|
42
|
+
framelessNodes,
|
|
43
|
+
isQualifierRecord,
|
|
44
|
+
movedNodes,
|
|
45
|
+
type NoteFn,
|
|
46
|
+
notePropertyColumn,
|
|
47
|
+
slotColumn,
|
|
48
|
+
textOf,
|
|
49
|
+
UNWRITTEN_ROLES,
|
|
50
|
+
} from "../../common/ontology-export.js";
|
|
51
|
+
import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
|
|
52
|
+
import { agree, plural } from "../../common/plural.js";
|
|
53
|
+
import { type ExplicitWeights, explicitWeights } from "../../common/weights.js";
|
|
54
|
+
import { encodeChunks, joinText } from "../../common/writer.js";
|
|
55
|
+
import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
|
|
56
|
+
import { BOOL_TAGS, DEPRECATED_TAGS, ID_LIST_TAGS, TEXT_TAGS, TYPEDEF_TAGS } from "./importer.js";
|
|
57
|
+
import {
|
|
58
|
+
escapeOboQuoted,
|
|
59
|
+
escapeOboValue,
|
|
60
|
+
escapeOboWord,
|
|
61
|
+
hasLineEnd,
|
|
62
|
+
isOboWord,
|
|
63
|
+
isQualifierName,
|
|
64
|
+
NOT_IN_WORD,
|
|
65
|
+
qualifierBlock,
|
|
66
|
+
tokenize,
|
|
67
|
+
} from "./syntax.js";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The format-specific options of the OBO exporter.
|
|
71
|
+
* @category Built-in formats
|
|
72
|
+
*/
|
|
73
|
+
export interface OboExportOptions extends CommonExportOptions {
|
|
74
|
+
/**
|
|
75
|
+
* The relation written for an edge that has none of its own (no `relation` value): an OBO id
|
|
76
|
+
* such as `is_a` or `part_of`. Reasoners and ROBOT read `is_a` as subclassing.
|
|
77
|
+
* @defaultValue "is_a"
|
|
78
|
+
*/
|
|
79
|
+
relation?: string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The `ontology` id written in the header, such as `go` or `uberon`. It may hold letters,
|
|
82
|
+
* digits and `_ . - /`; any other character makes checkExport() and the save throw
|
|
83
|
+
* E_UNSUPPORTED. The default is the id an OBO import read, else the graph name with every other
|
|
84
|
+
* character replaced by `_`, else no `ontology` line.
|
|
85
|
+
* @defaultValue as read, else the graph name
|
|
86
|
+
*/
|
|
87
|
+
ontology?: string | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The loss notes the OBO exporter's check() returns, by name. A key is the code without its
|
|
92
|
+
* severity and format prefixes.
|
|
93
|
+
* @category Built-in formats
|
|
94
|
+
*/
|
|
95
|
+
export const OBO_LOSS = Object.freeze({
|
|
96
|
+
/** A node column outside the OBO vocabulary (or one whose values a tag cannot carry exactly) reads back inside the `property_value` column. */
|
|
97
|
+
COLUMN_AS_PROPERTY_VALUE: COLUMN_AS_PROPERTY_VALUE_CODE,
|
|
98
|
+
/** An edge column (or the explicit weights) reads back inside the `qualifiers` column. */
|
|
99
|
+
EDGE_COLUMN_AS_QUALIFIER: "W_OBO_EDGE_COLUMN_AS_QUALIFIER",
|
|
100
|
+
/** Every edge is written from its source to its target, so an undirected graph reads back as directed. */
|
|
101
|
+
UNDIRECTED_AS_DIRECTED: "W_OBO_UNDIRECTED_AS_DIRECTED",
|
|
102
|
+
/** Two identical clauses on one frame (parallel edges with one relation and the same qualifiers, a repeated list item) read back as one. */
|
|
103
|
+
DUPLICATE_CLAUSE: "W_OBO_DUPLICATE_CLAUSE",
|
|
104
|
+
/** An edge without a relation is written with the `relation` option (default `is_a`). */
|
|
105
|
+
RELATION_ASSUMED: RELATION_ASSUMED_CODE,
|
|
106
|
+
/** A relation that is not an OBO id (empty, or holding a space, `!`, `{` or `}`) is written with `_` in place of those characters. */
|
|
107
|
+
RELATION_RENAMED: "W_OBO_RELATION_RENAMED",
|
|
108
|
+
/** The graph name is not an ontology id; the header `ontology` is written with `_` in place of the other characters. */
|
|
109
|
+
ONTOLOGY_NAME: "W_OBO_ONTOLOGY_NAME",
|
|
110
|
+
/** `[Typedef]` frames read back as nodes only under the importer's `typedefs: "nodes"`; by default they are metadata. */
|
|
111
|
+
TYPEDEF_NODES: TYPEDEF_NODES_CODE,
|
|
112
|
+
/** Edges are written in the frame of their source node, so they read back grouped by source, in node order. */
|
|
113
|
+
EDGE_ORDER: "W_OBO_EDGE_ORDER",
|
|
114
|
+
/**
|
|
115
|
+
* A graph attribute is written as a header `property_value` and reads back in `snapshot.meta.extra.obo.header`,
|
|
116
|
+
* not as a graph attribute.
|
|
117
|
+
*/
|
|
118
|
+
GRAPH_COLUMN_AS_METADATA: GRAPH_COLUMN_AS_METADATA_CODE,
|
|
119
|
+
/** A carriage return or form feed in a text cannot be written; it is written as a line feed. */
|
|
120
|
+
LINE_END: "W_OBO_LINE_END",
|
|
121
|
+
/** The nodes read back in a different order. */
|
|
122
|
+
NODE_ORDER: NODE_ORDER_CODE,
|
|
123
|
+
/** A mutual pair is written as two clauses without its mark. */
|
|
124
|
+
MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
|
|
125
|
+
/**
|
|
126
|
+
* The undirected edges of a mixed graph are written the way `onMixedDirection` chose ("directed" or "undirected").
|
|
127
|
+
*/
|
|
128
|
+
MIXED_DIRECTION: LOSS.MIXED_DIRECTION,
|
|
129
|
+
/**
|
|
130
|
+
* The graph has both directed and undirected edges and `onMixedDirection` is "error". An import stops; a save to a
|
|
131
|
+
* format that holds one direction per file fails with E_DIRECTED. Pass "directed" or "undirected" to read or write
|
|
132
|
+
* it anyway.
|
|
133
|
+
*/
|
|
134
|
+
MIXED_DIRECTION_ERROR: LOSS.MIXED_DIRECTION_ERROR,
|
|
135
|
+
/** OBO has no edge ids. */
|
|
136
|
+
EDGE_IDS_DROPPED: LOSS.EDGE_IDS_DROPPED,
|
|
137
|
+
/** OBO has no positions. */
|
|
138
|
+
POSITIONS_DROPPED: LOSS.POSITIONS,
|
|
139
|
+
/** OBO has no containment. */
|
|
140
|
+
HIERARCHY_DROPPED: LOSS.HIERARCHY,
|
|
141
|
+
/** OBO has no time. */
|
|
142
|
+
TEMPORAL_DROPPED: LOSS.TEMPORAL,
|
|
143
|
+
/** OBO has no visual columns. */
|
|
144
|
+
VIZ_DROPPED: LOSS.VIZ,
|
|
145
|
+
/** An attribute with a role the format has no place for is written as a plain attribute; the role is lost. */
|
|
146
|
+
ROLE_DROPPED: LOSS.ROLE,
|
|
147
|
+
/**
|
|
148
|
+
* An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
|
|
149
|
+
* reads back with that role.
|
|
150
|
+
*/
|
|
151
|
+
ROLE_ASSUMED: ROLE_ASSUMED_CODE,
|
|
152
|
+
/**
|
|
153
|
+
* An attribute with a role (for example, the label) is written where the format keeps that role, and reads back under the
|
|
154
|
+
* name the format's importer gives it.
|
|
155
|
+
*/
|
|
156
|
+
COLUMN_NAME_CHANGED: LOSS.COLUMN_NAME_CHANGED,
|
|
157
|
+
/**
|
|
158
|
+
* An OBO attribute stored as text where graph-io uses a dictionary (or the other way round); it reads back with
|
|
159
|
+
* graph-io's usual type. The values are the same.
|
|
160
|
+
*/
|
|
161
|
+
DTYPE_UNSUPPORTED: LOSS.DTYPE,
|
|
162
|
+
/** A column without a value on any written element reads back absent. */
|
|
163
|
+
EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
|
|
164
|
+
/** A declared default: OBO has none. */
|
|
165
|
+
DEFAULT_DROPPED: LOSS.DEFAULT,
|
|
166
|
+
/** Declared options: OBO has no enumerations. */
|
|
167
|
+
OPTIONS_DROPPED: LOSS.OPTIONS,
|
|
168
|
+
/** An extension table OBO cannot carry. */
|
|
169
|
+
EXTENSION_TABLE_DROPPED: LOSS.EXTENSION_TABLE,
|
|
170
|
+
/**
|
|
171
|
+
* Node ids the format cannot write, under `sanitizeIds: "error"`; the save fails with E_INVALID_ID. Pass
|
|
172
|
+
* `sanitizeIds: "mangle"` to rewrite them.
|
|
173
|
+
*/
|
|
174
|
+
ID_CHARSET: LOSS.ID_CHARSET,
|
|
175
|
+
/** Node ids OBO cannot write as they are, under sanitizeIds "mangle": rewritten, the original kept. */
|
|
176
|
+
ID_MANGLED: LOSS.ID_MANGLED,
|
|
177
|
+
/** Numeric ids are written as text and read back as strings. */
|
|
178
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
179
|
+
/**
|
|
180
|
+
* Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
|
|
181
|
+
* E_INVALID_ID.
|
|
182
|
+
*/
|
|
183
|
+
ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What the OBO flat file keeps: a directed multigraph with self-loops and any id (the exporter's
|
|
188
|
+
* own rule refuses whitespace, control characters, `!`, `{` and `}`). No column of the snapshot's own reads back as
|
|
189
|
+
* a column: the file keeps the OBO vocabulary (`name`, `def`, `synonym`, ... with their own
|
|
190
|
+
* types) and writes every other node column as property values and every edge column as
|
|
191
|
+
* qualifiers, which check() reports column by column.
|
|
192
|
+
* @category Built-in formats
|
|
193
|
+
*/
|
|
194
|
+
export const OBO_CAPABILITIES: ExportCapabilities = capabilities({
|
|
195
|
+
mixedDirection: false,
|
|
196
|
+
multiEdges: true,
|
|
197
|
+
selfLoops: true,
|
|
198
|
+
edgeIds: "none",
|
|
199
|
+
idCharset: "any",
|
|
200
|
+
dtypes: [],
|
|
201
|
+
components: false,
|
|
202
|
+
lists: false,
|
|
203
|
+
json: false,
|
|
204
|
+
defaults: false,
|
|
205
|
+
options: false,
|
|
206
|
+
hierarchy: false,
|
|
207
|
+
temporal: "none",
|
|
208
|
+
graphAttributes: false,
|
|
209
|
+
positions: false,
|
|
210
|
+
viz: false,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/** The roles OBO has a slot for, and the column names its importer gives them. */
|
|
214
|
+
|
|
215
|
+
/** The node column that keeps a rewritten id's original (restored by restoreMangledIds). */
|
|
216
|
+
const ORIGINAL_ID = "graphty:originalId";
|
|
217
|
+
|
|
218
|
+
/** The edge columns of the vocabulary. */
|
|
219
|
+
const RELATION_COLUMN = "relation";
|
|
220
|
+
const QUALIFIERS_COLUMN = "qualifiers";
|
|
221
|
+
|
|
222
|
+
/** The qualifier name the explicit weights are written under. */
|
|
223
|
+
const WEIGHT_QUALIFIER = "weight";
|
|
224
|
+
|
|
225
|
+
/** The relations a file may use without declaring them (the importer's built-ins). */
|
|
226
|
+
const BUILTIN_RELATIONS: ReadonlySet<string> = new Set([
|
|
227
|
+
"is_a",
|
|
228
|
+
"instance_of",
|
|
229
|
+
"disjoint_from",
|
|
230
|
+
"inverse_of",
|
|
231
|
+
"union_of",
|
|
232
|
+
"intersection_of",
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
/** Marks where the edge clauses go in TAG_ORDER. */
|
|
236
|
+
const EDGES = "\u0000edges";
|
|
237
|
+
|
|
238
|
+
/** Marks where the unknown tags go in TAG_ORDER. */
|
|
239
|
+
const UNRECOGNIZED = "obo.unrecognized";
|
|
240
|
+
|
|
241
|
+
/** The order of the tags in a frame (the OBO 1.4 guide's, Term, Instance and Typedef tags merged). */
|
|
242
|
+
const TAG_ORDER: readonly string[] = [
|
|
243
|
+
"is_anonymous",
|
|
244
|
+
"name",
|
|
245
|
+
"namespace",
|
|
246
|
+
"alt_id",
|
|
247
|
+
"def",
|
|
248
|
+
"comment",
|
|
249
|
+
"subset",
|
|
250
|
+
"synonym",
|
|
251
|
+
"xref",
|
|
252
|
+
"builtin",
|
|
253
|
+
"property_value",
|
|
254
|
+
"domain",
|
|
255
|
+
"range",
|
|
256
|
+
"holds_over_chain",
|
|
257
|
+
"is_anti_symmetric",
|
|
258
|
+
"is_cyclic",
|
|
259
|
+
"is_reflexive",
|
|
260
|
+
"is_symmetric",
|
|
261
|
+
"is_asymmetric",
|
|
262
|
+
"is_transitive",
|
|
263
|
+
"is_functional",
|
|
264
|
+
"is_inverse_functional",
|
|
265
|
+
EDGES,
|
|
266
|
+
"intersection_of",
|
|
267
|
+
"union_of",
|
|
268
|
+
"equivalent_to",
|
|
269
|
+
"disjoint_from",
|
|
270
|
+
"inverse_of",
|
|
271
|
+
"transitive_over",
|
|
272
|
+
"equivalent_to_chain",
|
|
273
|
+
"disjoint_over",
|
|
274
|
+
"created_by",
|
|
275
|
+
"creation_date",
|
|
276
|
+
"is_obsolete",
|
|
277
|
+
"replaced_by",
|
|
278
|
+
"consider",
|
|
279
|
+
"expand_assertion_to",
|
|
280
|
+
"expand_expression_to",
|
|
281
|
+
"is_metadata_tag",
|
|
282
|
+
"is_class_level",
|
|
283
|
+
UNRECOGNIZED,
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
/** The header tags in the 1.4 guide's order; every other kept header tag follows them. */
|
|
287
|
+
const HEADER_ORDER: readonly string[] = [
|
|
288
|
+
"format-version",
|
|
289
|
+
"data-version",
|
|
290
|
+
"version",
|
|
291
|
+
"date",
|
|
292
|
+
"saved-by",
|
|
293
|
+
"auto-generated-by",
|
|
294
|
+
"import",
|
|
295
|
+
"subsetdef",
|
|
296
|
+
"synonymtypedef",
|
|
297
|
+
"idspace",
|
|
298
|
+
"default-relationship-id-prefix",
|
|
299
|
+
"id-mapping",
|
|
300
|
+
"default-namespace",
|
|
301
|
+
"namespace-id-rule",
|
|
302
|
+
"remark",
|
|
303
|
+
"ontology",
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
/** Tags the importer applies (so an unknown tag of that name would not read back as unknown). */
|
|
307
|
+
const APPLIED_TAGS: ReadonlySet<string> = new Set([
|
|
308
|
+
"id",
|
|
309
|
+
"is_a",
|
|
310
|
+
"instance_of",
|
|
311
|
+
"relationship",
|
|
312
|
+
"intersection_of",
|
|
313
|
+
"holds_over_chain",
|
|
314
|
+
"equivalent_to_chain",
|
|
315
|
+
"def",
|
|
316
|
+
"expand_assertion_to",
|
|
317
|
+
"expand_expression_to",
|
|
318
|
+
"synonym",
|
|
319
|
+
"xref",
|
|
320
|
+
"property_value",
|
|
321
|
+
...TEXT_TAGS,
|
|
322
|
+
...BOOL_TAGS,
|
|
323
|
+
...ID_LIST_TAGS,
|
|
324
|
+
...DEPRECATED_TAGS.keys(),
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
/** The vocabulary node columns the flat file writes as tags (`type`, `name` and `propertyType` aside). */
|
|
328
|
+
const TAG_COLUMNS: ReadonlySet<string> = new Set(
|
|
329
|
+
Object.keys(OBO_NODE_COLUMNS).filter((name) => name !== "type" && name !== "name" && name !== "propertyType"),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
/** The xsd datatype of a dtype in a `property_value` line. */
|
|
333
|
+
const XSD: Readonly<Record<string, string>> = Object.freeze({
|
|
334
|
+
string: "xsd:string",
|
|
335
|
+
dict: "xsd:string",
|
|
336
|
+
bool: "xsd:boolean",
|
|
337
|
+
i32: "xsd:integer",
|
|
338
|
+
u32: "xsd:integer",
|
|
339
|
+
u8: "xsd:integer",
|
|
340
|
+
f32: "xsd:double",
|
|
341
|
+
f64: "xsd:double",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/** A node column written as `property_value` lines. */
|
|
345
|
+
interface PropertyColumn {
|
|
346
|
+
readonly column: Column;
|
|
347
|
+
/** The relation the lines name (the column name, or its id form). */
|
|
348
|
+
readonly relation: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** An edge column written as qualifiers. */
|
|
352
|
+
interface QualifierColumn {
|
|
353
|
+
readonly column: Column;
|
|
354
|
+
/** The qualifier name (the column name, or its written form). */
|
|
355
|
+
readonly name: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Everything write() needs, computed once by plan(). */
|
|
359
|
+
interface Plan {
|
|
360
|
+
readonly notes: LossNote[];
|
|
361
|
+
readonly fatal: GraphFormatError | null;
|
|
362
|
+
readonly options: { readonly relation: string; readonly ontology: string | null };
|
|
363
|
+
/** The id written for each node. */
|
|
364
|
+
readonly ids: readonly string[];
|
|
365
|
+
/** The rewritten nodes' original ids, by node index. */
|
|
366
|
+
readonly originals: ReadonlyMap<number, string>;
|
|
367
|
+
/** 1 for a node written without a frame (a placeholder the re-import makes again). */
|
|
368
|
+
readonly frameless: Uint8Array;
|
|
369
|
+
/** The frame type of each node. */
|
|
370
|
+
readonly kinds: readonly FrameKind[];
|
|
371
|
+
/** The label column (written as `name`), or null. */
|
|
372
|
+
readonly label: Column | null;
|
|
373
|
+
/** The vocabulary columns written as their tags, by tag. */
|
|
374
|
+
readonly tags: ReadonlyMap<string, Column>;
|
|
375
|
+
/** The node columns written as `property_value` lines. */
|
|
376
|
+
readonly properties: readonly PropertyColumn[];
|
|
377
|
+
/** The written edges by source: edgeStart[i]..edgeStart[i + 1] in edgeOrder. */
|
|
378
|
+
readonly edgeStart: Uint32Array;
|
|
379
|
+
readonly edgeOrder: Uint32Array;
|
|
380
|
+
/** The target of each edge. */
|
|
381
|
+
readonly dst: ArrayLike<number>;
|
|
382
|
+
/** The relation written for each edge. */
|
|
383
|
+
readonly relations: readonly string[];
|
|
384
|
+
/** The vocabulary `qualifiers` edge column, or null. */
|
|
385
|
+
readonly qualifiers: Column | null;
|
|
386
|
+
/** The other edge columns written as qualifiers. */
|
|
387
|
+
readonly edgeColumns: readonly QualifierColumn[];
|
|
388
|
+
readonly weights: ExplicitWeights;
|
|
389
|
+
/** The header lines. */
|
|
390
|
+
readonly header: readonly string[];
|
|
391
|
+
/** The frames after the nodes (kept Typedefs, declared relations, unknown frames), as lines. */
|
|
392
|
+
readonly tail: readonly string[];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Counts gathered while frames are written. */
|
|
396
|
+
interface FrameStats {
|
|
397
|
+
/** Texts holding a carriage return or form feed. */
|
|
398
|
+
lineEnds: number;
|
|
399
|
+
/** Whether every `obo.qualifiers` entry found its clause. */
|
|
400
|
+
qualifiersPlaced: boolean;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ============================================================ values
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Whether a text survives as an unquoted tag value: the reader trims it, and the OBO grammar
|
|
407
|
+
* (fastobo, owlapi) refuses an empty one.
|
|
408
|
+
* @param text - the text
|
|
409
|
+
* @returns true when the text is not empty and has no surrounding whitespace
|
|
410
|
+
*/
|
|
411
|
+
function isTrimmed(text: string): boolean {
|
|
412
|
+
return text.length > 0 && text === text.trim();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Whether a value is an array whose items all pass a test.
|
|
417
|
+
* @param value - the value
|
|
418
|
+
* @param item - the item test
|
|
419
|
+
* @param nonEmpty - whether the array must have an item
|
|
420
|
+
* @returns true when it is such an array
|
|
421
|
+
*/
|
|
422
|
+
function arrayOf(value: unknown, item: (v: unknown) => boolean, nonEmpty = true): value is unknown[] {
|
|
423
|
+
return Array.isArray(value) && (!nonEmpty || value.length > 0) && value.every(item);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Whether a value is a string the unquoted xref slot keeps (non-empty, no surrounding whitespace).
|
|
428
|
+
* @param value - the value
|
|
429
|
+
* @returns true for such a string
|
|
430
|
+
*/
|
|
431
|
+
function isXrefId(value: unknown): value is string {
|
|
432
|
+
return typeof value === "string" && value.length > 0 && isTrimmed(value);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Whether a value is an OBO word.
|
|
437
|
+
* @param value - the value
|
|
438
|
+
* @returns true for a string isOboWord() accepts
|
|
439
|
+
*/
|
|
440
|
+
function isWord(value: unknown): value is string {
|
|
441
|
+
return typeof value === "string" && isOboWord(value);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Whether an object has exactly the given keys, `qualifiers` optional (and then a qualifier record).
|
|
446
|
+
* @param value - the value
|
|
447
|
+
* @param keys - the required keys
|
|
448
|
+
* @returns true when it has those keys and nothing else
|
|
449
|
+
*/
|
|
450
|
+
function hasKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
451
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const own = Object.keys(value);
|
|
455
|
+
const record = value as Record<string, unknown>;
|
|
456
|
+
if (Object.hasOwn(record, "qualifiers")) {
|
|
457
|
+
if (!isQualifierRecord(record.qualifiers, isQualifierName)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
return own.length === keys.length + 1 && keys.every((k) => Object.hasOwn(record, k));
|
|
461
|
+
}
|
|
462
|
+
return own.length === keys.length && keys.every((k) => Object.hasOwn(record, k));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Replace every character a word cannot hold with `_`.
|
|
467
|
+
* @param text - the text
|
|
468
|
+
* @returns a word
|
|
469
|
+
*/
|
|
470
|
+
function wordOf(text: string): string {
|
|
471
|
+
const out = text.replaceAll(new RegExp(NOT_IN_WORD.source, "gu"), "_");
|
|
472
|
+
return out.length === 0 ? "_" : out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* A qualifier record as name / value pairs (a list value as one pair per item).
|
|
477
|
+
* @param record - a record isQualifierRecord() accepts
|
|
478
|
+
* @returns the pairs
|
|
479
|
+
*/
|
|
480
|
+
function qualifierPairs(record: unknown): [string, string][] {
|
|
481
|
+
const out: [string, string][] = [];
|
|
482
|
+
for (const [name, value] of Object.entries(record as Record<string, string | string[]>)) {
|
|
483
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
484
|
+
out.push([name, item]);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ============================================================ the plan
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Resolve the OBO-specific options.
|
|
494
|
+
* @param options - the caller's options
|
|
495
|
+
* @returns the relation and the ontology; E_UNSUPPORTED for a value that is not an OBO id
|
|
496
|
+
*/
|
|
497
|
+
function resolveOboOptions(options: OboExportOptions | undefined): {
|
|
498
|
+
readonly relation: string;
|
|
499
|
+
readonly ontology: string | null;
|
|
500
|
+
} {
|
|
501
|
+
const relation = options?.relation ?? "is_a";
|
|
502
|
+
if (typeof relation !== "string" || !isOboWord(relation)) {
|
|
503
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option relation: ${JSON.stringify(relation)} is not an OBO id`, {
|
|
504
|
+
option: "relation",
|
|
505
|
+
found: relation,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const ontology = options?.ontology ?? null;
|
|
509
|
+
if (ontology !== null && (typeof ontology !== "string" || !/^[A-Za-z0-9_.\-/]+$/.test(ontology))) {
|
|
510
|
+
throw new GraphFormatError(
|
|
511
|
+
"E_UNSUPPORTED",
|
|
512
|
+
`option ontology: ${JSON.stringify(ontology)} is not an ontology id (letters, digits, _ . - /)`,
|
|
513
|
+
{ option: "ontology", found: ontology },
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return { relation, ontology };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The ids to write: each node's id text, or under "mangle" a word with `_` for the characters a
|
|
521
|
+
* word cannot hold (and `_2`, `_3`... on a collision), the original kept.
|
|
522
|
+
* @param snapshot - the snapshot
|
|
523
|
+
* @param mode - the sanitizeIds option
|
|
524
|
+
* @param note - records a note
|
|
525
|
+
* @returns the ids, the originals and the fatal error, if any
|
|
526
|
+
*/
|
|
527
|
+
function planIds(
|
|
528
|
+
snapshot: GraphSnapshot,
|
|
529
|
+
mode: "error" | "mangle",
|
|
530
|
+
note: NoteFn,
|
|
531
|
+
): { ids: string[]; originals: Map<number, string>; fatal: GraphFormatError | null } {
|
|
532
|
+
const n = snapshot.nodeCount;
|
|
533
|
+
const ids: string[] = new Array<string>(n);
|
|
534
|
+
const bad: number[] = [];
|
|
535
|
+
let numeric = 0;
|
|
536
|
+
for (let i = 0; i < n; i++) {
|
|
537
|
+
const id: NodeId = snapshot.ids.idOf(i);
|
|
538
|
+
if (typeof id === "number") {
|
|
539
|
+
numeric++;
|
|
540
|
+
}
|
|
541
|
+
ids[i] = String(id);
|
|
542
|
+
if (!isOboWord(ids[i])) {
|
|
543
|
+
bad.push(i);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (numeric > 0) {
|
|
547
|
+
note(
|
|
548
|
+
LOSS.ID_TEXT_TYPE,
|
|
549
|
+
`${numeric} numeric node id${plural(numeric)} ${agree(numeric, "is", "are")} written as text and read back as strings`,
|
|
550
|
+
null,
|
|
551
|
+
numeric,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const originals = new Map<number, string>();
|
|
555
|
+
let fatal: GraphFormatError | null = null;
|
|
556
|
+
if (bad.length > 0 && mode === "error") {
|
|
557
|
+
fatal = refuseIds(snapshot, ids, bad, note);
|
|
558
|
+
} else if (bad.length > 0) {
|
|
559
|
+
mangleIds(ids, bad, originals, note);
|
|
560
|
+
}
|
|
561
|
+
return { ids, originals, fatal: fatal ?? collidingIds(snapshot, ids, note) };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Note the ids that are not OBO words, for a save that refuses them.
|
|
566
|
+
* @param snapshot - the snapshot
|
|
567
|
+
* @param ids - the id texts
|
|
568
|
+
* @param bad - the indices of the ids that are not OBO words
|
|
569
|
+
* @param note - records a note
|
|
570
|
+
* @returns the error the save throws
|
|
571
|
+
*/
|
|
572
|
+
function refuseIds(
|
|
573
|
+
snapshot: GraphSnapshot,
|
|
574
|
+
ids: readonly string[],
|
|
575
|
+
bad: readonly number[],
|
|
576
|
+
note: NoteFn,
|
|
577
|
+
): GraphFormatError {
|
|
578
|
+
note(
|
|
579
|
+
LOSS.ID_CHARSET,
|
|
580
|
+
`${bad.length} node id${plural(bad.length)} ${agree(bad.length, "is", "are")} not OBO ids (empty, or holding whitespace, a control character, "!", "{" or "}"); the save fails unless sanitizeIds is "mangle"`,
|
|
581
|
+
null,
|
|
582
|
+
bad.length,
|
|
583
|
+
);
|
|
584
|
+
return new GraphFormatError(
|
|
585
|
+
"E_INVALID_ID",
|
|
586
|
+
`${bad.length} node id${plural(bad.length)} cannot be written as OBO ids (first: ${JSON.stringify(ids[bad[0]])}); pass sanitizeIds: "mangle" to rewrite them`,
|
|
587
|
+
{ reason: "charset", count: bad.length, id: snapshot.ids.idOf(bad[0]), index: bad[0] },
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Rewrite the ids that are not OBO words with "_" (and `_2`, `_3`... on a collision), keeping the
|
|
593
|
+
* originals.
|
|
594
|
+
* @param ids - the id texts, rewritten in place
|
|
595
|
+
* @param bad - the indices of the ids that are not OBO words
|
|
596
|
+
* @param originals - receives each rewritten id's original text
|
|
597
|
+
* @param note - records a note
|
|
598
|
+
*/
|
|
599
|
+
function mangleIds(ids: string[], bad: readonly number[], originals: Map<number, string>, note: NoteFn): void {
|
|
600
|
+
const badSet = new Set(bad);
|
|
601
|
+
const used = new Set(ids.filter((_, i) => !badSet.has(i)));
|
|
602
|
+
for (const i of bad) {
|
|
603
|
+
const base = wordOf(ids[i]);
|
|
604
|
+
let candidate = base;
|
|
605
|
+
let k = 2;
|
|
606
|
+
while (used.has(candidate)) {
|
|
607
|
+
candidate = `${base}_${k}`;
|
|
608
|
+
k++;
|
|
609
|
+
}
|
|
610
|
+
used.add(candidate);
|
|
611
|
+
originals.set(i, ids[i]);
|
|
612
|
+
ids[i] = candidate;
|
|
613
|
+
}
|
|
614
|
+
note(
|
|
615
|
+
LOSS.ID_MANGLED,
|
|
616
|
+
`${bad.length} node id${plural(bad.length)} that ${agree(bad.length, "is", "are")} not OBO ids ${agree(bad.length, "is", "are")} rewritten with "_"; the originals are kept as ${ORIGINAL_ID} property values (JSON text; an import with restoreMangledIds: true reads them back as the ids)`,
|
|
617
|
+
null,
|
|
618
|
+
bad.length,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Note the first two node ids that have the same text, which OBO cannot tell apart.
|
|
624
|
+
* @param snapshot - the snapshot
|
|
625
|
+
* @param ids - the id texts
|
|
626
|
+
* @param note - records a note
|
|
627
|
+
* @returns the error the save throws, or null when every text is unique
|
|
628
|
+
*/
|
|
629
|
+
function collidingIds(snapshot: GraphSnapshot, ids: readonly string[], note: NoteFn): GraphFormatError | null {
|
|
630
|
+
const seen = new Map<string, number>();
|
|
631
|
+
for (let i = 0; i < ids.length; i++) {
|
|
632
|
+
const before = seen.get(ids[i]);
|
|
633
|
+
if (before !== undefined) {
|
|
634
|
+
note(
|
|
635
|
+
LOSS.ID_TEXT_COLLISION,
|
|
636
|
+
`node ids ${JSON.stringify(snapshot.ids.idOf(before))} and ${JSON.stringify(snapshot.ids.idOf(i))} have the same text; the save fails`,
|
|
637
|
+
null,
|
|
638
|
+
1,
|
|
639
|
+
);
|
|
640
|
+
return new GraphFormatError("E_INVALID_ID", "two node ids have the same text; OBO keeps ids as text", {
|
|
641
|
+
reason: "collision",
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
seen.set(ids[i], i);
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The written edges (the mirror halves of folded pairs left out) grouped by source, in node order
|
|
651
|
+
* then edge order, and the direction notes.
|
|
652
|
+
* @param snapshot - the snapshot
|
|
653
|
+
* @param folding - the pair folding
|
|
654
|
+
* @param common - the common options
|
|
655
|
+
* @param note - records a note
|
|
656
|
+
* @returns the edge list, the CSR by source and the targets
|
|
657
|
+
*/
|
|
658
|
+
function planEdges(
|
|
659
|
+
snapshot: GraphSnapshot,
|
|
660
|
+
folding: PairFolding,
|
|
661
|
+
common: ResolvedExportOptions,
|
|
662
|
+
note: NoteFn,
|
|
663
|
+
): { written: number[]; edgeStart: Uint32Array; edgeOrder: Uint32Array; dst: ArrayLike<number> } {
|
|
664
|
+
const { src, dst } = snapshot.edgeList();
|
|
665
|
+
const written: number[] = [];
|
|
666
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
667
|
+
if (!folding.folded(e)) {
|
|
668
|
+
written.push(e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const edgeStart = new Uint32Array(snapshot.nodeCount + 1);
|
|
672
|
+
for (const e of written) {
|
|
673
|
+
edgeStart[src[e] + 1]++;
|
|
674
|
+
}
|
|
675
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
676
|
+
edgeStart[i + 1] += edgeStart[i];
|
|
677
|
+
}
|
|
678
|
+
const fill = edgeStart.slice(0, snapshot.nodeCount);
|
|
679
|
+
const edgeOrder = new Uint32Array(written.length);
|
|
680
|
+
for (const e of written) {
|
|
681
|
+
edgeOrder[fill[src[e]]++] = e;
|
|
682
|
+
}
|
|
683
|
+
let moved = 0;
|
|
684
|
+
for (let k = 0; k < edgeOrder.length; k++) {
|
|
685
|
+
if (edgeOrder[k] !== written[k]) {
|
|
686
|
+
moved++;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (moved > 0) {
|
|
690
|
+
note(
|
|
691
|
+
OBO_LOSS.EDGE_ORDER,
|
|
692
|
+
`${moved} edge${plural(moved)} ${agree(moved, "reads", "read")} back at another position: edges are written on their source's frame, grouped by source in node order`,
|
|
693
|
+
null,
|
|
694
|
+
moved,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (!snapshot.directed) {
|
|
698
|
+
note(
|
|
699
|
+
OBO_LOSS.UNDIRECTED_AS_DIRECTED,
|
|
700
|
+
`the snapshot is undirected; every edge is written from its source to its target and reads back directed (${written.length} edge${plural(written.length)})`,
|
|
701
|
+
null,
|
|
702
|
+
written.length,
|
|
703
|
+
);
|
|
704
|
+
} else if (common.onMixedDirection !== "error") {
|
|
705
|
+
const undirected = written.filter((e) => !folding.sourceDirected(e)).length;
|
|
706
|
+
if (undirected > 0) {
|
|
707
|
+
note(
|
|
708
|
+
OBO_LOSS.UNDIRECTED_AS_DIRECTED,
|
|
709
|
+
`${undirected} undirected edge${plural(undirected)} ${agree(undirected, "is", "are")} written as one directed clause each; OBO has no undirected edge`,
|
|
710
|
+
null,
|
|
711
|
+
undirected,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (folding.mutualCount > 0) {
|
|
716
|
+
note(
|
|
717
|
+
LOSS.MUTUAL_EXPANDED,
|
|
718
|
+
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two clauses; the mutual mark is lost`,
|
|
719
|
+
null,
|
|
720
|
+
folding.mutualCount,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return { written, edgeStart, edgeOrder, dst };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The frame types: the `type` column when every written node has one of Term, Instance or
|
|
728
|
+
* Typedef, else Term for every node (and the column is a property value).
|
|
729
|
+
* @param snapshot - the snapshot
|
|
730
|
+
* @param frameless - the frameless flags
|
|
731
|
+
* @returns the kinds and the type column used, or null
|
|
732
|
+
*/
|
|
733
|
+
function planKinds(snapshot: GraphSnapshot, frameless: Uint8Array): { kinds: FrameKind[]; type: Column | null } {
|
|
734
|
+
const kinds = new Array<FrameKind>(snapshot.nodeCount).fill("Term");
|
|
735
|
+
const type = snapshot.nodes.get("type");
|
|
736
|
+
if (type === null || (type.dtype !== "dict" && type.dtype !== "string")) {
|
|
737
|
+
return { kinds, type: null };
|
|
738
|
+
}
|
|
739
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
740
|
+
if (frameless[i] === 1) {
|
|
741
|
+
if (type.isSet(i)) {
|
|
742
|
+
return { kinds: kinds.fill("Term"), type: null };
|
|
743
|
+
}
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const value = cellOf(type, i);
|
|
747
|
+
if (typeof value !== "string" || !FRAME_KINDS.has(value)) {
|
|
748
|
+
return { kinds: kinds.fill("Term"), type: null };
|
|
749
|
+
}
|
|
750
|
+
kinds[i] = value as FrameKind;
|
|
751
|
+
}
|
|
752
|
+
return { kinds, type };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Whether a vocabulary column's values are written by its tag and read back unchanged, on every
|
|
757
|
+
* written node (the vocabulary's dtype, the shape the importer builds, OBO words where the tag
|
|
758
|
+
* takes an id, no surrounding space in an unquoted value, a Typedef tag on Typedef frames only).
|
|
759
|
+
* @param name - the column (a TAG_COLUMNS name)
|
|
760
|
+
* @param column - the column
|
|
761
|
+
* @param rows - the written nodes
|
|
762
|
+
* @param kinds - the frame types
|
|
763
|
+
* @param scopes - the default scope of each declared synonym type
|
|
764
|
+
* @returns true when the tag carries the column
|
|
765
|
+
*/
|
|
766
|
+
function tagCarries(
|
|
767
|
+
name: string,
|
|
768
|
+
column: Column,
|
|
769
|
+
rows: readonly number[],
|
|
770
|
+
kinds: readonly FrameKind[],
|
|
771
|
+
scopes: ReadonlyMap<string, string | null>,
|
|
772
|
+
): boolean {
|
|
773
|
+
const spec = OBO_NODE_COLUMNS[name];
|
|
774
|
+
if (!fitsVocabulary(spec.dtype, column) || column.meta.components > 1) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
const typedefOnly = TYPEDEF_TAGS.has(name);
|
|
778
|
+
const valid = (v: unknown): boolean => {
|
|
779
|
+
if (TEXT_TAGS.has(name)) {
|
|
780
|
+
return typeof v === "string" && isTrimmed(v);
|
|
781
|
+
}
|
|
782
|
+
if (BOOL_TAGS.has(name)) {
|
|
783
|
+
return typeof v === "boolean";
|
|
784
|
+
}
|
|
785
|
+
if (ID_LIST_TAGS.has(name)) {
|
|
786
|
+
return arrayOf(v, isWord);
|
|
787
|
+
}
|
|
788
|
+
switch (name) {
|
|
789
|
+
case "def":
|
|
790
|
+
return typeof v === "string";
|
|
791
|
+
case "def.xrefs":
|
|
792
|
+
return arrayOf(v, isXrefId, false);
|
|
793
|
+
case "xref":
|
|
794
|
+
return arrayOf(v, isXrefId);
|
|
795
|
+
case "xref.descriptions":
|
|
796
|
+
return (
|
|
797
|
+
typeof v === "object" &&
|
|
798
|
+
v !== null &&
|
|
799
|
+
!Array.isArray(v) &&
|
|
800
|
+
Object.values(v).every((d) => typeof d === "string")
|
|
801
|
+
);
|
|
802
|
+
case "synonym":
|
|
803
|
+
return arrayOf(
|
|
804
|
+
v,
|
|
805
|
+
(s) =>
|
|
806
|
+
hasKeys(s, ["text", "scope", "type", "xrefs"]) &&
|
|
807
|
+
typeof s.text === "string" &&
|
|
808
|
+
typeof s.scope === "string" &&
|
|
809
|
+
SYNONYM_SCOPES.has(s.scope) &&
|
|
810
|
+
(s.type === null || isWord(s.type)) &&
|
|
811
|
+
(s.type === null || (scopes.get(s.type) ?? s.scope) === s.scope) &&
|
|
812
|
+
arrayOf(s.xrefs, isXrefId, false),
|
|
813
|
+
);
|
|
814
|
+
case "intersection_of":
|
|
815
|
+
return arrayOf(
|
|
816
|
+
v,
|
|
817
|
+
(x) =>
|
|
818
|
+
hasKeys(x, ["relation", "target"]) &&
|
|
819
|
+
(x.relation === null || isWord(x.relation)) &&
|
|
820
|
+
isWord(x.target),
|
|
821
|
+
);
|
|
822
|
+
case "property_value":
|
|
823
|
+
return arrayOf(
|
|
824
|
+
v,
|
|
825
|
+
(x) =>
|
|
826
|
+
hasKeys(x, ["relation", "value", "datatype"]) &&
|
|
827
|
+
isWord(x.relation) &&
|
|
828
|
+
x.relation !== ORIGINAL_ID &&
|
|
829
|
+
typeof x.value === "string" &&
|
|
830
|
+
(x.datatype === null || isWord(x.datatype)),
|
|
831
|
+
);
|
|
832
|
+
case "holds_over_chain":
|
|
833
|
+
case "equivalent_to_chain":
|
|
834
|
+
return arrayOf(v, (pair) => arrayOf(pair, isWord) && pair.length === 2);
|
|
835
|
+
case "expand_assertion_to":
|
|
836
|
+
case "expand_expression_to":
|
|
837
|
+
return arrayOf(
|
|
838
|
+
v,
|
|
839
|
+
(x) =>
|
|
840
|
+
hasKeys(x, ["template", "xrefs"]) &&
|
|
841
|
+
typeof x.template === "string" &&
|
|
842
|
+
arrayOf(x.xrefs, isXrefId, false),
|
|
843
|
+
);
|
|
844
|
+
case "obo.qualifiers":
|
|
845
|
+
return (
|
|
846
|
+
typeof v === "object" &&
|
|
847
|
+
v !== null &&
|
|
848
|
+
!Array.isArray(v) &&
|
|
849
|
+
Object.values(v).every((entries) =>
|
|
850
|
+
arrayOf(
|
|
851
|
+
entries,
|
|
852
|
+
(entry) =>
|
|
853
|
+
hasKeys(entry, ["value"]) &&
|
|
854
|
+
Object.hasOwn(entry, "qualifiers") &&
|
|
855
|
+
typeof entry.value === "string" &&
|
|
856
|
+
isQualifierRecord(entry.qualifiers, isQualifierName),
|
|
857
|
+
),
|
|
858
|
+
)
|
|
859
|
+
);
|
|
860
|
+
default:
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
for (const i of rows) {
|
|
865
|
+
if (!column.isSet(i)) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (typedefOnly && kinds[i] !== "Typedef") {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
if (!valid(cellOf(column, i))) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Whether the `obo.unrecognized` column reads back unchanged: a record of tag to texts, each tag
|
|
880
|
+
* one the importer does not apply on that frame type.
|
|
881
|
+
* @param column - the column
|
|
882
|
+
* @param rows - the written nodes
|
|
883
|
+
* @param kinds - the frame types
|
|
884
|
+
* @returns true when every cell is written as unknown tags and read back as the same
|
|
885
|
+
*/
|
|
886
|
+
function unrecognizedCarried(column: Column, rows: readonly number[], kinds: readonly FrameKind[]): boolean {
|
|
887
|
+
if (column.dtype !== "json") {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
for (const i of rows) {
|
|
891
|
+
const v = cellOf(column, i);
|
|
892
|
+
if (v === undefined) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
for (const [tag, values] of Object.entries(v)) {
|
|
899
|
+
const applied =
|
|
900
|
+
(APPLIED_TAGS.has(tag) || TYPEDEF_TAGS.has(tag)) &&
|
|
901
|
+
!(TYPEDEF_TAGS.has(tag) && kinds[i] !== "Typedef") &&
|
|
902
|
+
!(tag === "instance_of" && kinds[i] !== "Instance");
|
|
903
|
+
if (
|
|
904
|
+
applied ||
|
|
905
|
+
tag.length === 0 ||
|
|
906
|
+
!isTrimmed(tag) ||
|
|
907
|
+
!arrayOf(values, (t) => typeof t === "string" && isTrimmed(t))
|
|
908
|
+
) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* The default scope of each synonym type the kept header declares (null: none declared).
|
|
918
|
+
* @param header - the kept header (tag to raw values)
|
|
919
|
+
* @returns the scopes by type
|
|
920
|
+
*/
|
|
921
|
+
function synonymTypeScopes(header: Readonly<Record<string, readonly string[]>>): Map<string, string | null> {
|
|
922
|
+
const out = new Map<string, string | null>();
|
|
923
|
+
for (const raw of header.synonymtypedef ?? []) {
|
|
924
|
+
const tokens = tokenize(raw);
|
|
925
|
+
if (tokens.length > 0 && tokens[0].kind === "word") {
|
|
926
|
+
const scope = tokens.slice(1).find((t) => t.kind === "word" && SYNONYM_SCOPES.has(t.text));
|
|
927
|
+
out.set(tokens[0].text, scope?.text ?? null);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* The record of an OBO import kept under `meta.extra.obo`, or empty records.
|
|
935
|
+
* @param snapshot - the snapshot
|
|
936
|
+
* @returns the header, the Typedefs and the unknown frames
|
|
937
|
+
*/
|
|
938
|
+
function keptObo(snapshot: GraphSnapshot): {
|
|
939
|
+
header: Record<string, string[]>;
|
|
940
|
+
typedefs: Record<string, Record<string, string[]>>;
|
|
941
|
+
unknownFrames: { type: string; clauses: Record<string, string[]> }[];
|
|
942
|
+
} {
|
|
943
|
+
const raw = snapshot.meta.extra.obo;
|
|
944
|
+
const record =
|
|
945
|
+
typeof raw === "object" && raw !== null && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
|
|
946
|
+
const clauses = (value: unknown): Record<string, string[]> => {
|
|
947
|
+
const out: Record<string, string[]> = {};
|
|
948
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
949
|
+
for (const [tag, values] of Object.entries(value)) {
|
|
950
|
+
if (Array.isArray(values)) {
|
|
951
|
+
out[tag] = values.filter((v): v is string => typeof v === "string");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return out;
|
|
956
|
+
};
|
|
957
|
+
const typedefs: Record<string, Record<string, string[]>> = {};
|
|
958
|
+
if (typeof record.typedefs === "object" && record.typedefs !== null && !Array.isArray(record.typedefs)) {
|
|
959
|
+
for (const [id, frame] of Object.entries(record.typedefs)) {
|
|
960
|
+
typedefs[id] = clauses(frame);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const unknownFrames = Array.isArray(record.unknownFrames)
|
|
964
|
+
? record.unknownFrames.flatMap((f: unknown) =>
|
|
965
|
+
typeof f === "object" && f !== null && typeof (f as { type?: unknown }).type === "string"
|
|
966
|
+
? [{ type: (f as { type: string }).type, clauses: clauses((f as { clauses?: unknown }).clauses) }]
|
|
967
|
+
: [],
|
|
968
|
+
)
|
|
969
|
+
: [];
|
|
970
|
+
return { header: clauses(record.header), typedefs, unknownFrames };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Plan an export: the notes, the fatal condition and what is written.
|
|
975
|
+
* @param snapshot - the snapshot
|
|
976
|
+
* @param options - the caller's options
|
|
977
|
+
* @returns the plan
|
|
978
|
+
*/
|
|
979
|
+
function plan(snapshot: GraphSnapshot, options: (OboExportOptions & CommonExportOptions) | undefined): Plan {
|
|
980
|
+
const common = resolveExportOptions(options);
|
|
981
|
+
const obo = resolveOboOptions(options);
|
|
982
|
+
const notes: LossNote[] = [];
|
|
983
|
+
const note: NoteFn = (code, message, column = null, count = null) => {
|
|
984
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
985
|
+
};
|
|
986
|
+
let fatal: GraphFormatError | null = null;
|
|
987
|
+
|
|
988
|
+
const { ids, originals, fatal: idFatal } = planIds(snapshot, common.sanitizeIds, note);
|
|
989
|
+
fatal ??= idFatal;
|
|
990
|
+
const folding = pairFolding(snapshot);
|
|
991
|
+
const { written, edgeStart, edgeOrder, dst } = planEdges(snapshot, folding, common, note);
|
|
992
|
+
const frameless = framelessNodes(snapshot, written, {
|
|
993
|
+
outgoingAllowed: false,
|
|
994
|
+
changed: (i) => originals.has(i),
|
|
995
|
+
});
|
|
996
|
+
const moved = movedNodes(
|
|
997
|
+
frameless,
|
|
998
|
+
Array.from(edgeOrder, (e) => dst[e]),
|
|
999
|
+
);
|
|
1000
|
+
if (moved > 0) {
|
|
1001
|
+
note(
|
|
1002
|
+
NODE_ORDER_CODE,
|
|
1003
|
+
`${moved} node${plural(moved)} ${agree(moved, "reads", "read")} back at another position: placeholder nodes have no frame and read back after the written nodes`,
|
|
1004
|
+
null,
|
|
1005
|
+
moved,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const rows: number[] = [];
|
|
1009
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1010
|
+
if (frameless[i] === 0) {
|
|
1011
|
+
rows.push(i);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const { kinds, type } = planKinds(snapshot, frameless);
|
|
1015
|
+
const typedefNodes = rows.filter((i) => kinds[i] === "Typedef").length;
|
|
1016
|
+
if (typedefNodes > 0) {
|
|
1017
|
+
note(
|
|
1018
|
+
OBO_LOSS.TYPEDEF_NODES,
|
|
1019
|
+
`${typedefNodes} node${plural(typedefNodes)} ${agree(typedefNodes, "is", "are")} written as [Typedef] frames; they read back as nodes only when imported with typedefs: "nodes", and then after the other nodes, with the [Typedef] frames that declare the relations`,
|
|
1020
|
+
"type",
|
|
1021
|
+
typedefNodes,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const kept = keptObo(snapshot);
|
|
1026
|
+
const scopes = synonymTypeScopes(kept.header);
|
|
1027
|
+
// the label: the label column, or a role-less name column
|
|
1028
|
+
const labelSlot = slotColumn(snapshot.nodes, "label", "name");
|
|
1029
|
+
const labelColumn = labelSlot.column;
|
|
1030
|
+
const label =
|
|
1031
|
+
labelColumn !== null && rows.every((i) => !labelColumn.isSet(i) || isTrimmed(String(labelColumn.value(i))))
|
|
1032
|
+
? labelColumn
|
|
1033
|
+
: null;
|
|
1034
|
+
const tags = planTags(snapshot, rows, kinds, scopes, label);
|
|
1035
|
+
fatal ??= capabilityNotes(snapshot, common, {
|
|
1036
|
+
caps: OBO_CAPABILITIES,
|
|
1037
|
+
label,
|
|
1038
|
+
labelAssumed: labelSlot.assumed,
|
|
1039
|
+
edgeSlot: "a qualifier",
|
|
1040
|
+
note,
|
|
1041
|
+
});
|
|
1042
|
+
oboColumnNotes(snapshot, label, tags, note);
|
|
1043
|
+
const properties = planProperties(snapshot, rows, frameless, label, tags, type, note);
|
|
1044
|
+
|
|
1045
|
+
// edges
|
|
1046
|
+
const relationSlot = slotColumn(snapshot.edges, "kind", RELATION_COLUMN);
|
|
1047
|
+
const relations = planRelations(snapshot, written, relationSlot.column, obo.relation, note);
|
|
1048
|
+
if (relationSlot.assumed && relationSlot.column !== null) {
|
|
1049
|
+
note(
|
|
1050
|
+
ROLE_ASSUMED_CODE,
|
|
1051
|
+
`edge column "${RELATION_COLUMN}" has no role, so it is taken as the relation of each edge: it is written as each line's relation, and reads back with the relation role ("kind")`,
|
|
1052
|
+
RELATION_COLUMN,
|
|
1053
|
+
relationSlot.column.length - relationSlot.column.nullCount,
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const weights = explicitWeights(snapshot);
|
|
1057
|
+
const { qualifiers, edgeColumns } = planEdgeColumns(snapshot, written, relationSlot.column, weights, note);
|
|
1058
|
+
emptyColumnNotes(snapshot, rows, frameless, written, note);
|
|
1059
|
+
|
|
1060
|
+
const p: Plan = {
|
|
1061
|
+
notes,
|
|
1062
|
+
fatal,
|
|
1063
|
+
options: obo,
|
|
1064
|
+
ids,
|
|
1065
|
+
originals,
|
|
1066
|
+
frameless,
|
|
1067
|
+
kinds,
|
|
1068
|
+
label,
|
|
1069
|
+
tags,
|
|
1070
|
+
properties,
|
|
1071
|
+
edgeStart,
|
|
1072
|
+
edgeOrder,
|
|
1073
|
+
dst,
|
|
1074
|
+
relations,
|
|
1075
|
+
qualifiers,
|
|
1076
|
+
edgeColumns,
|
|
1077
|
+
weights,
|
|
1078
|
+
header: [],
|
|
1079
|
+
tail: [],
|
|
1080
|
+
};
|
|
1081
|
+
// write every frame once: the duplicate clauses, the line ends and the qualifier placement
|
|
1082
|
+
let stats = simulate(p, rows);
|
|
1083
|
+
if (!stats.qualifiersPlaced) {
|
|
1084
|
+
tags.delete("obo.qualifiers");
|
|
1085
|
+
const column = snapshot.nodes.get("obo.qualifiers");
|
|
1086
|
+
if (column !== null) {
|
|
1087
|
+
properties.push({ column, relation: "obo.qualifiers" });
|
|
1088
|
+
notePropertyColumn(note, column, rows, "property_value lines (relation obo.qualifiers)");
|
|
1089
|
+
}
|
|
1090
|
+
stats = simulate(p, rows);
|
|
1091
|
+
}
|
|
1092
|
+
if (stats.duplicates > 0) {
|
|
1093
|
+
note(
|
|
1094
|
+
OBO_LOSS.DUPLICATE_CLAUSE,
|
|
1095
|
+
`${stats.duplicates} clause${plural(stats.duplicates)} ${agree(stats.duplicates, "repeats", "repeat")} another clause of their frame word for word (parallel edges with one relation and the same qualifiers, a repeated list item) and read back as one`,
|
|
1096
|
+
null,
|
|
1097
|
+
stats.duplicates,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
const header = planHeader(snapshot, kept.header, obo, rows, tags, note, stats);
|
|
1101
|
+
const tail = planTail({
|
|
1102
|
+
kept,
|
|
1103
|
+
rows,
|
|
1104
|
+
kinds,
|
|
1105
|
+
ids,
|
|
1106
|
+
properties,
|
|
1107
|
+
relations,
|
|
1108
|
+
edges: edgeOrder,
|
|
1109
|
+
xref: tags.get("xref") ?? null,
|
|
1110
|
+
});
|
|
1111
|
+
if (stats.lineEnds > 0) {
|
|
1112
|
+
note(
|
|
1113
|
+
OBO_LOSS.LINE_END,
|
|
1114
|
+
`${stats.lineEnds} text${plural(stats.lineEnds)} ${agree(stats.lineEnds, "holds", "hold")} a carriage return or form feed, which OBO cannot carry; written as a line feed`,
|
|
1115
|
+
null,
|
|
1116
|
+
stats.lineEnds,
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
return { ...p, header, tail };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* The vocabulary columns written as their tags: each one tagCarries() accepts, then the pairs
|
|
1124
|
+
* resolved (def.xrefs needs def, a def needs its def.xrefs written when that column exists, the
|
|
1125
|
+
* xref descriptions need their xrefs written).
|
|
1126
|
+
* @param snapshot - the snapshot
|
|
1127
|
+
* @param rows - the written nodes
|
|
1128
|
+
* @param kinds - the frame types
|
|
1129
|
+
* @param scopes - the default scope of each declared synonym type
|
|
1130
|
+
* @param label - the label column (never a tag)
|
|
1131
|
+
* @returns the columns by tag
|
|
1132
|
+
*/
|
|
1133
|
+
function planTags(
|
|
1134
|
+
snapshot: GraphSnapshot,
|
|
1135
|
+
rows: readonly number[],
|
|
1136
|
+
kinds: readonly FrameKind[],
|
|
1137
|
+
scopes: ReadonlyMap<string, string | null>,
|
|
1138
|
+
label: Column | null,
|
|
1139
|
+
): Map<string, Column> {
|
|
1140
|
+
const tags = new Map<string, Column>();
|
|
1141
|
+
for (const name of TAG_COLUMNS) {
|
|
1142
|
+
const column = snapshot.nodes.get(name);
|
|
1143
|
+
if (column === null || column === label || (column.meta.role !== null && column.meta.role !== "label")) {
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (
|
|
1147
|
+
name === UNRECOGNIZED
|
|
1148
|
+
? unrecognizedCarried(column, rows, kinds)
|
|
1149
|
+
: tagCarries(name, column, rows, kinds, scopes)
|
|
1150
|
+
) {
|
|
1151
|
+
tags.set(name, column);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
// pairs: def.xrefs needs def; xref.descriptions needs its xrefs written
|
|
1155
|
+
const def = tags.get("def");
|
|
1156
|
+
const defXrefs = tags.get("def.xrefs");
|
|
1157
|
+
if (defXrefs !== undefined && (def === undefined || rows.some((i) => def.isSet(i) !== defXrefs.isSet(i)))) {
|
|
1158
|
+
tags.delete("def.xrefs");
|
|
1159
|
+
}
|
|
1160
|
+
if (def !== undefined && !tags.has("def.xrefs") && snapshot.nodes.get("def.xrefs") !== null) {
|
|
1161
|
+
// a def.xrefs column that is not written would read back as the empty list on every def
|
|
1162
|
+
tags.delete("def");
|
|
1163
|
+
}
|
|
1164
|
+
const descriptions = tags.get("xref.descriptions");
|
|
1165
|
+
if (descriptions !== undefined && !rows.every((i) => describedXrefsWritten(tags, descriptions, i))) {
|
|
1166
|
+
tags.delete("xref.descriptions");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return tags;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Whether every described xref of a node is written in one of its xref slots (so the description
|
|
1174
|
+
* has a place).
|
|
1175
|
+
* @param tags - the columns written as tags
|
|
1176
|
+
* @param descriptions - the `xref.descriptions` column
|
|
1177
|
+
* @param row - the node
|
|
1178
|
+
* @returns true when every description has its xref
|
|
1179
|
+
*/
|
|
1180
|
+
function describedXrefsWritten(tags: ReadonlyMap<string, Column>, descriptions: Column, row: number): boolean {
|
|
1181
|
+
const map = cellOf(descriptions, row) as Record<string, string> | undefined;
|
|
1182
|
+
if (map === undefined) {
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
const written = new Set<string>();
|
|
1186
|
+
const add = (values: unknown): void => {
|
|
1187
|
+
if (Array.isArray(values)) {
|
|
1188
|
+
for (const v of values) {
|
|
1189
|
+
if (typeof v === "string") {
|
|
1190
|
+
written.add(v);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
for (const name of ["def.xrefs", "xref"]) {
|
|
1196
|
+
const column = tags.get(name);
|
|
1197
|
+
if (column !== undefined) {
|
|
1198
|
+
add(cellOf(column, row));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
for (const name of ["synonym", "expand_assertion_to", "expand_expression_to"]) {
|
|
1202
|
+
const column = tags.get(name);
|
|
1203
|
+
const items = column === undefined ? undefined : cellOf(column, row);
|
|
1204
|
+
if (Array.isArray(items)) {
|
|
1205
|
+
for (const item of items as { xrefs: unknown }[]) {
|
|
1206
|
+
add(item.xrefs);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return Object.keys(map).every((id) => written.has(id));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* The OBO-only column notes: the vocabulary column of the other text dtype, and the graph columns
|
|
1215
|
+
* written as header property values.
|
|
1216
|
+
* @param snapshot - the snapshot
|
|
1217
|
+
* @param label - the label column written as `name`, or null
|
|
1218
|
+
* @param tags - the vocabulary columns written as tags
|
|
1219
|
+
* @param note - records a note
|
|
1220
|
+
*/
|
|
1221
|
+
function oboColumnNotes(
|
|
1222
|
+
snapshot: GraphSnapshot,
|
|
1223
|
+
label: Column | null,
|
|
1224
|
+
tags: ReadonlyMap<string, Column>,
|
|
1225
|
+
note: NoteFn,
|
|
1226
|
+
): void {
|
|
1227
|
+
for (const [name, column] of [...tags, ...(label === null ? [] : ([["name", label]] as const))]) {
|
|
1228
|
+
const want = name === "name" ? "string" : OBO_NODE_COLUMNS[name].dtype;
|
|
1229
|
+
if (
|
|
1230
|
+
((want === "string" || want === "dict") && column.dtype !== want) ||
|
|
1231
|
+
(column.dtype === "list" && column.child.dtype !== "string")
|
|
1232
|
+
) {
|
|
1233
|
+
const held = column.dtype === "list" ? `${column.child.dtype} items` : column.dtype;
|
|
1234
|
+
note(
|
|
1235
|
+
LOSS.DTYPE,
|
|
1236
|
+
`node column "${column.meta.name}" holds ${held}; it reads back as ${column.dtype === "list" ? "string items" : want}`,
|
|
1237
|
+
column.meta.name,
|
|
1238
|
+
column.length - column.nullCount,
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const column of snapshot.graph) {
|
|
1243
|
+
note(
|
|
1244
|
+
GRAPH_COLUMN_AS_METADATA_CODE,
|
|
1245
|
+
`graph column "${column.meta.name}" is written as a header property_value and reads back in meta.extra.obo.header, not as a column`,
|
|
1246
|
+
column.meta.name,
|
|
1247
|
+
column.length - column.nullCount,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* The node columns written as `property_value` lines: every column that is not written by a tag,
|
|
1254
|
+
* the label or the type, and not one OBO leaves out (positions, visual and temporal columns), each
|
|
1255
|
+
* with its note.
|
|
1256
|
+
* @param snapshot - the snapshot
|
|
1257
|
+
* @param rows - the written nodes
|
|
1258
|
+
* @param frameless - the frameless flags
|
|
1259
|
+
* @param label - the label column
|
|
1260
|
+
* @param tags - the columns written as tags
|
|
1261
|
+
* @param type - the type column
|
|
1262
|
+
* @param note - records a note
|
|
1263
|
+
* @returns the columns
|
|
1264
|
+
*/
|
|
1265
|
+
function planProperties(
|
|
1266
|
+
snapshot: GraphSnapshot,
|
|
1267
|
+
rows: readonly number[],
|
|
1268
|
+
frameless: Uint8Array,
|
|
1269
|
+
label: Column | null,
|
|
1270
|
+
tags: ReadonlyMap<string, Column>,
|
|
1271
|
+
type: Column | null,
|
|
1272
|
+
note: NoteFn,
|
|
1273
|
+
): PropertyColumn[] {
|
|
1274
|
+
const out: PropertyColumn[] = [];
|
|
1275
|
+
const slotted = new Set<Column>([
|
|
1276
|
+
...tags.values(),
|
|
1277
|
+
...(label === null ? [] : [label]),
|
|
1278
|
+
...(type === null ? [] : [type]),
|
|
1279
|
+
]);
|
|
1280
|
+
for (const column of snapshot.nodes) {
|
|
1281
|
+
const { role, name } = column.meta;
|
|
1282
|
+
if (slotted.has(column) || (role !== null && UNWRITTEN_ROLES.has(role))) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (name === PLACEHOLDER_COLUMN && rows.every((i) => !column.isSet(i)) && frameless.includes(1)) {
|
|
1286
|
+
// read back from the frameless nodes alone
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const relation = isOboWord(name) && name !== ORIGINAL_ID ? name : wordOf(name.replaceAll(":", "_"));
|
|
1290
|
+
out.push({ column, relation });
|
|
1291
|
+
notePropertyColumn(note, column, rows, `property_value lines (relation ${relation})`);
|
|
1292
|
+
}
|
|
1293
|
+
return out;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* The relation written for each edge, with the notes of the assumed and renamed ones.
|
|
1298
|
+
* @param snapshot - the snapshot
|
|
1299
|
+
* @param written - the written edges
|
|
1300
|
+
* @param relation - the relation column, or null
|
|
1301
|
+
* @param fallback - the relation of an edge without one
|
|
1302
|
+
* @param note - records a note
|
|
1303
|
+
* @returns the relation per edge index
|
|
1304
|
+
*/
|
|
1305
|
+
function planRelations(
|
|
1306
|
+
snapshot: GraphSnapshot,
|
|
1307
|
+
written: readonly number[],
|
|
1308
|
+
relation: Column | null,
|
|
1309
|
+
fallback: string,
|
|
1310
|
+
note: NoteFn,
|
|
1311
|
+
): string[] {
|
|
1312
|
+
const out = new Array<string>(snapshot.edgeCount).fill(fallback);
|
|
1313
|
+
let assumed = 0;
|
|
1314
|
+
let renamed = 0;
|
|
1315
|
+
for (const e of written) {
|
|
1316
|
+
const value = relation === null ? undefined : cellOf(relation, e);
|
|
1317
|
+
if (typeof value !== "string") {
|
|
1318
|
+
assumed++;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (isOboWord(value)) {
|
|
1322
|
+
out[e] = value;
|
|
1323
|
+
} else {
|
|
1324
|
+
out[e] = wordOf(value);
|
|
1325
|
+
renamed++;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (assumed > 0) {
|
|
1329
|
+
note(
|
|
1330
|
+
OBO_LOSS.RELATION_ASSUMED,
|
|
1331
|
+
`${assumed} edge${plural(assumed)} ${agree(assumed, "has", "have")} no relation; written as ${fallback} (an ontology reads is_a as subclassing)`,
|
|
1332
|
+
relation?.meta.name ?? null,
|
|
1333
|
+
assumed,
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (renamed > 0) {
|
|
1337
|
+
note(
|
|
1338
|
+
OBO_LOSS.RELATION_RENAMED,
|
|
1339
|
+
`${renamed} relation${plural(renamed)} ${agree(renamed, "is", "are")} not OBO ids; written with "_" in place of the space, "!", "{" or "}"`,
|
|
1340
|
+
relation?.meta.name ?? null,
|
|
1341
|
+
renamed,
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return out;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* The edge columns written as qualifiers: the vocabulary `qualifiers` column when its cells are
|
|
1349
|
+
* qualifier records, every other edge column (and the explicit weights) as `name="value"` pairs,
|
|
1350
|
+
* each with its note.
|
|
1351
|
+
* @param snapshot - the snapshot
|
|
1352
|
+
* @param written - the written edges
|
|
1353
|
+
* @param relation - the relation column
|
|
1354
|
+
* @param weights - the explicit weights
|
|
1355
|
+
* @param note - records a note
|
|
1356
|
+
* @returns the qualifiers column and the other columns
|
|
1357
|
+
*/
|
|
1358
|
+
function planEdgeColumns(
|
|
1359
|
+
snapshot: GraphSnapshot,
|
|
1360
|
+
written: readonly number[],
|
|
1361
|
+
relation: Column | null,
|
|
1362
|
+
weights: ExplicitWeights,
|
|
1363
|
+
note: NoteFn,
|
|
1364
|
+
): { qualifiers: Column | null; edgeColumns: QualifierColumn[] } {
|
|
1365
|
+
const qualifiers = qualifierRecords(snapshot, written);
|
|
1366
|
+
const edgeColumns: QualifierColumn[] = [];
|
|
1367
|
+
const count = (column: Column): number => written.filter((e) => column.isSet(e)).length;
|
|
1368
|
+
const merged = qualifiers === null ? "" : " (merged with its values)";
|
|
1369
|
+
for (const column of snapshot.edges) {
|
|
1370
|
+
const { role, name } = column.meta;
|
|
1371
|
+
if (
|
|
1372
|
+
column === relation ||
|
|
1373
|
+
column === qualifiers ||
|
|
1374
|
+
role === "id" ||
|
|
1375
|
+
(role !== null && UNWRITTEN_ROLES.has(role))
|
|
1376
|
+
) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const written2 = isQualifierName(name) ? name : wordOf(name.replaceAll(/[=",[\]\\]/g, "_"));
|
|
1380
|
+
edgeColumns.push({ column, name: written2 });
|
|
1381
|
+
const n = count(column);
|
|
1382
|
+
if (n > 0) {
|
|
1383
|
+
note(
|
|
1384
|
+
OBO_LOSS.EDGE_COLUMN_AS_QUALIFIER,
|
|
1385
|
+
`edge column "${name}" is written as the qualifier ${written2} and reads back inside the qualifiers column${merged}`,
|
|
1386
|
+
name,
|
|
1387
|
+
n,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const explicit = weights.weighted ? written.filter((e) => weights.isExplicit(e)).length : 0;
|
|
1392
|
+
if (explicit > 0) {
|
|
1393
|
+
note(
|
|
1394
|
+
OBO_LOSS.EDGE_COLUMN_AS_QUALIFIER,
|
|
1395
|
+
`${explicit} explicit edge weight${plural(explicit)} ${agree(explicit, "is", "are")} written as the qualifier ${WEIGHT_QUALIFIER} and read back inside the qualifiers column, not as weights`,
|
|
1396
|
+
snapshot.edges.byRole("weight")?.meta.name ?? null,
|
|
1397
|
+
explicit,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return { qualifiers, edgeColumns };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* The vocabulary `qualifiers` edge column, when every written cell of it is a qualifier record.
|
|
1405
|
+
* @param snapshot - the snapshot
|
|
1406
|
+
* @param written - the written edges
|
|
1407
|
+
* @returns the column, or null when there is none or it holds something else
|
|
1408
|
+
*/
|
|
1409
|
+
function qualifierRecords(snapshot: GraphSnapshot, written: readonly number[]): Column | null {
|
|
1410
|
+
const qualifiers = snapshot.edges.get(QUALIFIERS_COLUMN);
|
|
1411
|
+
if (qualifiers?.dtype !== "json" || qualifiers.meta.role !== null) {
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
const records = written.every(
|
|
1415
|
+
(e) => !qualifiers.isSet(e) || isQualifierRecord(cellOf(qualifiers, e), isQualifierName),
|
|
1416
|
+
);
|
|
1417
|
+
return records ? qualifiers : null;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* The header lines: the kept header in the guide's order with the missing fields filled from the
|
|
1422
|
+
* metadata, and the graph columns as `property_value` lines.
|
|
1423
|
+
* @param snapshot - the snapshot
|
|
1424
|
+
* @param kept - the kept header
|
|
1425
|
+
* @param obo - the OBO options
|
|
1426
|
+
* @param obo.ontology - the ontology option, or null
|
|
1427
|
+
* @param rows - the written nodes
|
|
1428
|
+
* @param tags - the columns written as tags
|
|
1429
|
+
* @param note - records a note
|
|
1430
|
+
* @param stats - the line-end counter
|
|
1431
|
+
* @returns the lines
|
|
1432
|
+
*/
|
|
1433
|
+
function planHeader(
|
|
1434
|
+
snapshot: GraphSnapshot,
|
|
1435
|
+
kept: Readonly<Record<string, readonly string[]>>,
|
|
1436
|
+
obo: { readonly ontology: string | null },
|
|
1437
|
+
rows: readonly number[],
|
|
1438
|
+
tags: ReadonlyMap<string, Column>,
|
|
1439
|
+
note: NoteFn,
|
|
1440
|
+
stats: FrameStats,
|
|
1441
|
+
): string[] {
|
|
1442
|
+
const { values, filled } = headerFields(snapshot, kept, obo.ontology, note);
|
|
1443
|
+
// a default namespace applies to every frame without one: keep it only when none lacks one
|
|
1444
|
+
const namespace = tags.get("namespace");
|
|
1445
|
+
if (values.has("default-namespace") && (namespace === undefined || rows.some((i) => !namespace.isSet(i)))) {
|
|
1446
|
+
values.delete("default-namespace");
|
|
1447
|
+
}
|
|
1448
|
+
const lines: string[] = [];
|
|
1449
|
+
// the filled fields in the guide's order, then the kept ones as the file had them
|
|
1450
|
+
const tagsInOrder = [
|
|
1451
|
+
...HEADER_ORDER.filter((t) => filled.has(t)),
|
|
1452
|
+
...[...values.keys()].filter((t) => !filled.has(t)),
|
|
1453
|
+
];
|
|
1454
|
+
for (const tag of tagsInOrder) {
|
|
1455
|
+
for (const value of values.get(tag) ?? []) {
|
|
1456
|
+
lines.push(`${escapeTag(tag)}: ${value}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (lines.length === 0 && rows.length === 0) {
|
|
1460
|
+
// a file of nothing reads back as an empty input: the version line makes it an empty ontology
|
|
1461
|
+
lines.push("format-version: 1.4");
|
|
1462
|
+
}
|
|
1463
|
+
for (const column of snapshot.graph) {
|
|
1464
|
+
const relation = isOboWord(column.meta.name) ? column.meta.name : wordOf(column.meta.name);
|
|
1465
|
+
for (const line of propertyLines({ column, relation }, 0, stats)) {
|
|
1466
|
+
lines.push(`property_value: ${line}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return lines;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* The header fields: the kept ones, and for a graph not read from OBO the fields its metadata has.
|
|
1474
|
+
* @param snapshot - the snapshot
|
|
1475
|
+
* @param kept - the kept header
|
|
1476
|
+
* @param ontologyOption - the ontology option, or null
|
|
1477
|
+
* @param note - records a note
|
|
1478
|
+
* @returns the values by tag, and the tags filled in (not kept)
|
|
1479
|
+
*/
|
|
1480
|
+
function headerFields(
|
|
1481
|
+
snapshot: GraphSnapshot,
|
|
1482
|
+
kept: Readonly<Record<string, readonly string[]>>,
|
|
1483
|
+
ontologyOption: string | null,
|
|
1484
|
+
note: NoteFn,
|
|
1485
|
+
): { values: Map<string, string[]>; filled: Set<string> } {
|
|
1486
|
+
const { meta } = snapshot;
|
|
1487
|
+
const values = new Map<string, string[]>(Object.entries(kept).map(([tag, v]) => [tag, v.map(rawValue)]));
|
|
1488
|
+
const filled = new Set<string>();
|
|
1489
|
+
const fill = (tag: string, value: string | null): void => {
|
|
1490
|
+
if (value !== null && !values.has(tag)) {
|
|
1491
|
+
values.set(tag, [value]);
|
|
1492
|
+
filled.add(tag);
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
// a file read from OBO keeps its own header; any other graph gets the fields its metadata has
|
|
1496
|
+
if (meta.sourceFormat !== "obo") {
|
|
1497
|
+
fill("format-version", "1.4");
|
|
1498
|
+
fill("date", oboDate(meta.created));
|
|
1499
|
+
fill("saved-by", meta.creator === null ? null : escapeOboValue(meta.creator));
|
|
1500
|
+
}
|
|
1501
|
+
const ontology = ontologyField(ontologyOption, meta.name, values.has("ontology"), note);
|
|
1502
|
+
if (ontology !== null) {
|
|
1503
|
+
if (!values.has("ontology")) {
|
|
1504
|
+
filled.add("ontology");
|
|
1505
|
+
}
|
|
1506
|
+
values.set("ontology", [ontology]);
|
|
1507
|
+
}
|
|
1508
|
+
return { values, filled };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* The ontology header field to write: the ontology option, else the graph name when it is an
|
|
1513
|
+
* ontology id, else the name rewritten with "_" when the kept header has none.
|
|
1514
|
+
* @param option - the ontology option, or null
|
|
1515
|
+
* @param name - the graph name, or null
|
|
1516
|
+
* @param kept - whether the kept header has an ontology field
|
|
1517
|
+
* @param note - records a note
|
|
1518
|
+
* @returns the value to set, or null to leave the field as it is
|
|
1519
|
+
*/
|
|
1520
|
+
function ontologyField(option: string | null, name: string | null, kept: boolean, note: NoteFn): string | null {
|
|
1521
|
+
if (option !== null) {
|
|
1522
|
+
return option;
|
|
1523
|
+
}
|
|
1524
|
+
if (name === null || name.length === 0 || kept) {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
if (/^[A-Za-z0-9_.\-/]+$/.test(name)) {
|
|
1528
|
+
return name;
|
|
1529
|
+
}
|
|
1530
|
+
const slug = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
|
|
1531
|
+
note(
|
|
1532
|
+
OBO_LOSS.ONTOLOGY_NAME,
|
|
1533
|
+
`the graph name ${JSON.stringify(name)} is not an ontology id; written as ${slug}`,
|
|
1534
|
+
null,
|
|
1535
|
+
null,
|
|
1536
|
+
);
|
|
1537
|
+
return slug;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* A kept raw value made safe for one line (a line break in metadata a caller built becomes `\n`).
|
|
1542
|
+
* @param raw - the raw value
|
|
1543
|
+
* @returns the value to write
|
|
1544
|
+
*/
|
|
1545
|
+
function rawValue(raw: string): string {
|
|
1546
|
+
return raw.replaceAll(/\r\n|\r|\n|\f/g, String.raw`\n`);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* A tag name escaped for the left of its colon.
|
|
1551
|
+
* @param tag - the tag
|
|
1552
|
+
* @returns the escaped tag
|
|
1553
|
+
*/
|
|
1554
|
+
function escapeTag(tag: string): string {
|
|
1555
|
+
return escapeOboValue(tag).replaceAll(":", String.raw`\:`);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* An ISO date (`yyyy-MM-ddTHH:mm...`) as the OBO header date `dd:MM:yyyy HH:mm`.
|
|
1560
|
+
* @param iso - the date, or null
|
|
1561
|
+
* @returns the header text, or null when the date is not of that form
|
|
1562
|
+
*/
|
|
1563
|
+
function oboDate(iso: string | null): string | null {
|
|
1564
|
+
const m = iso === null ? null : /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?/.exec(iso);
|
|
1565
|
+
if (m === null) {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
return `${m[3]}:${m[2]}:${m[1]} ${m[4] ?? "00"}:${m[5] ?? "00"}`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** What the frames after the nodes are planned from. */
|
|
1572
|
+
interface TailInput {
|
|
1573
|
+
/** The kept Typedef frames and unknown frames. */
|
|
1574
|
+
readonly kept: {
|
|
1575
|
+
typedefs: Record<string, Record<string, string[]>>;
|
|
1576
|
+
unknownFrames: { type: string; clauses: Record<string, string[]> }[];
|
|
1577
|
+
};
|
|
1578
|
+
/** The written nodes. */
|
|
1579
|
+
readonly rows: readonly number[];
|
|
1580
|
+
/** The frame type of each node. */
|
|
1581
|
+
readonly kinds: readonly FrameKind[];
|
|
1582
|
+
/** The written id of each node. */
|
|
1583
|
+
readonly ids: readonly string[];
|
|
1584
|
+
/** The property-value columns. */
|
|
1585
|
+
readonly properties: readonly PropertyColumn[];
|
|
1586
|
+
/** The relation of each edge. */
|
|
1587
|
+
readonly relations: readonly string[];
|
|
1588
|
+
/** The written edges. */
|
|
1589
|
+
readonly edges: Iterable<number>;
|
|
1590
|
+
/** The xref column written as tags (a Typedef node's xrefs declare relations), or null. */
|
|
1591
|
+
readonly xref: Column | null;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* The frames after the nodes: the kept Typedefs (unless a node is that Typedef), a declaration of
|
|
1596
|
+
* every relation and property-value relation no Typedef declares, and the kept unknown frames.
|
|
1597
|
+
* @param input - what the frames are planned from
|
|
1598
|
+
* @returns the lines
|
|
1599
|
+
*/
|
|
1600
|
+
function planTail(input: TailInput): string[] {
|
|
1601
|
+
const { kept, rows, properties, relations } = input;
|
|
1602
|
+
const lines: string[] = [];
|
|
1603
|
+
const declared = nodeTypedefs(input);
|
|
1604
|
+
keptTypedefLines(kept.typedefs, declared, lines);
|
|
1605
|
+
const declare = (relation: string, metadata: boolean): void => {
|
|
1606
|
+
if (declared.has(relation)) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
declared.add(relation);
|
|
1610
|
+
lines.push("", "[Typedef]", `id: ${escapeOboWord(relation)}`, `name: ${escapeOboValue(relation)}`);
|
|
1611
|
+
if (metadata) {
|
|
1612
|
+
lines.push("is_metadata_tag: true");
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
const used = new Set<string>();
|
|
1616
|
+
for (const e of input.edges) {
|
|
1617
|
+
used.add(relations[e]);
|
|
1618
|
+
}
|
|
1619
|
+
for (const { column, relation } of properties) {
|
|
1620
|
+
if (rows.some((i) => column.isSet(i))) {
|
|
1621
|
+
declare(relation, true);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
for (const relation of used) {
|
|
1625
|
+
declare(relation, false);
|
|
1626
|
+
}
|
|
1627
|
+
for (const frame of kept.unknownFrames) {
|
|
1628
|
+
lines.push("", `[${frame.type.replaceAll(/[\]\r\n]/g, "_")}]`, ...clauseLines(frame.clauses));
|
|
1629
|
+
}
|
|
1630
|
+
return lines;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* The relations the built-in ones, the Typedef nodes and their xrefs declare.
|
|
1635
|
+
* @param input - what the frames are planned from
|
|
1636
|
+
* @returns the declared relation ids
|
|
1637
|
+
*/
|
|
1638
|
+
function nodeTypedefs(input: TailInput): Set<string> {
|
|
1639
|
+
const { rows, kinds, ids, xref } = input;
|
|
1640
|
+
const declared = new Set<string>(BUILTIN_RELATIONS);
|
|
1641
|
+
for (const i of rows) {
|
|
1642
|
+
if (kinds[i] !== "Typedef") {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
declared.add(ids[i]);
|
|
1646
|
+
const xrefs = xref === null ? undefined : cellOf(xref, i);
|
|
1647
|
+
for (const x of Array.isArray(xrefs) ? xrefs : []) {
|
|
1648
|
+
declared.add(String(x));
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return declared;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Write the kept Typedef frames no node already declares, and declare them and their xrefs.
|
|
1656
|
+
* @param typedefs - the kept Typedef frames by id
|
|
1657
|
+
* @param declared - the declared relations, added to
|
|
1658
|
+
* @param lines - the lines, appended to
|
|
1659
|
+
*/
|
|
1660
|
+
function keptTypedefLines(
|
|
1661
|
+
typedefs: Record<string, Record<string, string[]>>,
|
|
1662
|
+
declared: Set<string>,
|
|
1663
|
+
lines: string[],
|
|
1664
|
+
): void {
|
|
1665
|
+
for (const [id, clauses] of Object.entries(typedefs)) {
|
|
1666
|
+
if (declared.has(id) && !BUILTIN_RELATIONS.has(id)) {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
declared.add(id);
|
|
1670
|
+
lines.push("", "[Typedef]", `id: ${escapeOboWord(id)}`, ...clauseLines(clauses, "id"));
|
|
1671
|
+
for (const xref of clauses.xref ?? []) {
|
|
1672
|
+
declared.add(xref.trim());
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* The clause lines of a kept frame, each value made safe for one line.
|
|
1679
|
+
* @param clauses - the values by tag
|
|
1680
|
+
* @param skip - a tag to leave out, or null
|
|
1681
|
+
* @returns the lines
|
|
1682
|
+
*/
|
|
1683
|
+
function clauseLines(clauses: Record<string, string[]>, skip: string | null = null): string[] {
|
|
1684
|
+
return Object.entries(clauses)
|
|
1685
|
+
.filter(([tag]) => tag !== skip)
|
|
1686
|
+
.flatMap(([tag, values]) => values.map((value) => `${escapeTag(tag)}: ${rawValue(value)}`));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ============================================================ the frames
|
|
1690
|
+
|
|
1691
|
+
/** Hands out the `obo.qualifiers` entries of one frame to the clauses they belong to, in order. */
|
|
1692
|
+
class QualifierCursor {
|
|
1693
|
+
private readonly entries: Record<string, { value: string; qualifiers: unknown }[]>;
|
|
1694
|
+
|
|
1695
|
+
private readonly next = new Map<string, number>();
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Create a cursor.
|
|
1699
|
+
* @param cell - the frame's `obo.qualifiers` cell, or undefined
|
|
1700
|
+
*/
|
|
1701
|
+
constructor(cell: unknown) {
|
|
1702
|
+
this.entries = (cell ?? {}) as Record<string, { value: string; qualifiers: unknown }[]>;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* The qualifier block of a clause: the next entry of its tag when that entry is for this value.
|
|
1707
|
+
* @param tag - the tag (or `<tag>.xrefs` for an xref inside a list)
|
|
1708
|
+
* @param value - the clause's value as the column holds it
|
|
1709
|
+
* @returns the block with a leading space, or ""
|
|
1710
|
+
*/
|
|
1711
|
+
block(tag: string, value: string): string {
|
|
1712
|
+
const list = Object.hasOwn(this.entries, tag) ? this.entries[tag] : undefined;
|
|
1713
|
+
const k = this.next.get(tag) ?? 0;
|
|
1714
|
+
if (list === undefined || k >= list.length || list[k].value !== value) {
|
|
1715
|
+
return "";
|
|
1716
|
+
}
|
|
1717
|
+
this.next.set(tag, k + 1);
|
|
1718
|
+
return qualifierBlock(qualifierPairs(list[k].qualifiers));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Whether every entry found its clause.
|
|
1723
|
+
* @returns true when all were placed
|
|
1724
|
+
*/
|
|
1725
|
+
placed(): boolean {
|
|
1726
|
+
return Object.entries(this.entries).every(([tag, list]) => (this.next.get(tag) ?? 0) === list.length);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** The per-frame writer state. */
|
|
1731
|
+
interface FrameState {
|
|
1732
|
+
readonly p: Plan;
|
|
1733
|
+
readonly row: number;
|
|
1734
|
+
readonly cursor: QualifierCursor;
|
|
1735
|
+
/** The xrefs whose description was written already. */
|
|
1736
|
+
readonly described: Set<string>;
|
|
1737
|
+
readonly stats: FrameStats;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* Count a text that loses a line end, and return it.
|
|
1742
|
+
* @param s - the frame state's stats
|
|
1743
|
+
* @param text - the text
|
|
1744
|
+
* @returns the text
|
|
1745
|
+
*/
|
|
1746
|
+
function counted(s: FrameStats, text: string): string {
|
|
1747
|
+
if (hasLineEnd(text)) {
|
|
1748
|
+
s.lineEnds++;
|
|
1749
|
+
}
|
|
1750
|
+
return text;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* One xref as written inside a list or after `xref:`: its id, its description the first time the
|
|
1755
|
+
* frame names it, and its own qualifiers.
|
|
1756
|
+
* @param f - the frame state
|
|
1757
|
+
* @param id - the xref id
|
|
1758
|
+
* @param listTag - the `<tag>.xrefs` key of its qualifiers, or null after `xref:`
|
|
1759
|
+
* @returns the text
|
|
1760
|
+
*/
|
|
1761
|
+
function xrefText(f: FrameState, id: string, listTag: string | null): string {
|
|
1762
|
+
let out = escapeOboValue(counted(f.stats, id));
|
|
1763
|
+
const descriptions = f.p.tags.get("xref.descriptions");
|
|
1764
|
+
const map =
|
|
1765
|
+
descriptions === undefined ? undefined : (cellOf(descriptions, f.row) as Record<string, string> | undefined);
|
|
1766
|
+
if (map !== undefined && Object.hasOwn(map, id) && !f.described.has(id)) {
|
|
1767
|
+
f.described.add(id);
|
|
1768
|
+
out += ` "${escapeOboQuoted(counted(f.stats, map[id]))}"`;
|
|
1769
|
+
}
|
|
1770
|
+
if (listTag !== null) {
|
|
1771
|
+
out += f.cursor.block(listTag, id);
|
|
1772
|
+
}
|
|
1773
|
+
return out;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* An xref list: `[A:1, B:2 "description"]`.
|
|
1778
|
+
* @param f - the frame state
|
|
1779
|
+
* @param ids - the xref ids
|
|
1780
|
+
* @param tag - the clause's tag
|
|
1781
|
+
* @returns the list
|
|
1782
|
+
*/
|
|
1783
|
+
function xrefList(f: FrameState, ids: readonly string[], tag: string): string {
|
|
1784
|
+
const field = `${tag}.xrefs`;
|
|
1785
|
+
return `[${ids.map((id) => xrefText(f, id, field)).join(", ")}]`;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* The `property_value` values of one cell of a property column: `relation "value" xsd:type` per
|
|
1790
|
+
* value (a list item each, JSON text for nested values).
|
|
1791
|
+
* @param prop - the column and its relation
|
|
1792
|
+
* @param row - the row
|
|
1793
|
+
* @param stats - the line-end counter
|
|
1794
|
+
* @returns the values after `property_value: `
|
|
1795
|
+
*/
|
|
1796
|
+
function propertyLines(prop: PropertyColumn, row: number, stats: FrameStats): string[] {
|
|
1797
|
+
const { column, relation } = prop;
|
|
1798
|
+
const value = cellOf(column, row);
|
|
1799
|
+
if (value === undefined) {
|
|
1800
|
+
return [];
|
|
1801
|
+
}
|
|
1802
|
+
const head = escapeOboWord(relation);
|
|
1803
|
+
const one = (v: unknown, dtype: string): string => {
|
|
1804
|
+
let text: string;
|
|
1805
|
+
let xsd = XSD[dtype] ?? "xsd:string";
|
|
1806
|
+
if (typeof v === "number" && xsd === "xsd:double") {
|
|
1807
|
+
text = xsdDouble(v, dtype);
|
|
1808
|
+
} else if (typeof v === "string" || typeof v === "boolean" || typeof v === "number") {
|
|
1809
|
+
text = textOf(v, dtype);
|
|
1810
|
+
} else {
|
|
1811
|
+
text = JSON.stringify(v) ?? "null";
|
|
1812
|
+
xsd = "xsd:string";
|
|
1813
|
+
}
|
|
1814
|
+
return `${head} "${escapeOboQuoted(counted(stats, text))}" ${xsd}`;
|
|
1815
|
+
};
|
|
1816
|
+
if (column.dtype === "list") {
|
|
1817
|
+
return (value as readonly unknown[]).map((item) => one(item, column.child.dtype));
|
|
1818
|
+
}
|
|
1819
|
+
if (column.dtype === "json" || column.meta.components > 1) {
|
|
1820
|
+
return [one(value, "json")];
|
|
1821
|
+
}
|
|
1822
|
+
return [one(value, column.dtype)];
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* A double as xsd:double writes it: INF, -INF and NaN for the non-finite values.
|
|
1827
|
+
* @param v - the value
|
|
1828
|
+
* @param dtype - the column dtype (f32 values use their shortest text)
|
|
1829
|
+
* @returns the text
|
|
1830
|
+
*/
|
|
1831
|
+
function xsdDouble(v: number, dtype: string): string {
|
|
1832
|
+
if (Number.isNaN(v)) {
|
|
1833
|
+
return "NaN";
|
|
1834
|
+
}
|
|
1835
|
+
if (!Number.isFinite(v)) {
|
|
1836
|
+
return v > 0 ? "INF" : "-INF";
|
|
1837
|
+
}
|
|
1838
|
+
return formatNumber(v, dtype === "f32" ? "f32" : "f64");
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* The qualifier block of one edge: the `qualifiers` record, then the other columns and the weight.
|
|
1843
|
+
* @param f - the frame state
|
|
1844
|
+
* @param e - the edge
|
|
1845
|
+
* @returns the block with a leading space, or ""
|
|
1846
|
+
*/
|
|
1847
|
+
function edgeQualifiers(f: FrameState, e: number): string {
|
|
1848
|
+
const { p } = f;
|
|
1849
|
+
const pairs: [string, string][] = [];
|
|
1850
|
+
if (p.qualifiers !== null) {
|
|
1851
|
+
const record = cellOf(p.qualifiers, e);
|
|
1852
|
+
if (record !== undefined) {
|
|
1853
|
+
pairs.push(...qualifierPairs(record));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
for (const { column, name } of p.edgeColumns) {
|
|
1857
|
+
pairs.push(...qualifierTexts(column, e).map((text): [string, string] => [name, text]));
|
|
1858
|
+
}
|
|
1859
|
+
const weight = p.weights.text(e);
|
|
1860
|
+
if (weight !== null) {
|
|
1861
|
+
pairs.push([WEIGHT_QUALIFIER, weight]);
|
|
1862
|
+
}
|
|
1863
|
+
for (const [, value] of pairs) {
|
|
1864
|
+
counted(f.stats, value);
|
|
1865
|
+
}
|
|
1866
|
+
return qualifierBlock(pairs);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* The qualifier values an edge column gives one edge: one per item of a list, none when unset.
|
|
1871
|
+
* @param column - the edge column
|
|
1872
|
+
* @param e - the edge
|
|
1873
|
+
* @returns the value texts
|
|
1874
|
+
*/
|
|
1875
|
+
function qualifierTexts(column: Column, e: number): string[] {
|
|
1876
|
+
const value = cellOf(column, e);
|
|
1877
|
+
if (value === undefined) {
|
|
1878
|
+
return [];
|
|
1879
|
+
}
|
|
1880
|
+
if (column.dtype === "list") {
|
|
1881
|
+
return (value as readonly unknown[]).map((item) => textOf(item, column.child.dtype));
|
|
1882
|
+
}
|
|
1883
|
+
return [column.dtype === "json" ? (JSON.stringify(value) ?? "null") : textOf(value, column.dtype)];
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* The clauses of one tag column on a frame.
|
|
1888
|
+
* @param f - the frame state
|
|
1889
|
+
* @param tag - the tag
|
|
1890
|
+
* @param column - the column
|
|
1891
|
+
* @param out - the lines
|
|
1892
|
+
*/
|
|
1893
|
+
function tagLines(f: FrameState, tag: string, column: Column, out: string[]): void {
|
|
1894
|
+
const value = cellOf(column, f.row);
|
|
1895
|
+
if (value === undefined) {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const { cursor, stats } = f;
|
|
1899
|
+
const push = (text: string, stored: string, raw = false): void => {
|
|
1900
|
+
out.push(`${tag}: ${raw ? text : escapeOboValue(counted(stats, text))}${cursor.block(tag, stored)}`);
|
|
1901
|
+
};
|
|
1902
|
+
if (TEXT_TAGS.has(tag)) {
|
|
1903
|
+
push(value as string, value as string);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (BOOL_TAGS.has(tag)) {
|
|
1907
|
+
push(value ? "true" : "false", value ? "true" : "false", true);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (ID_LIST_TAGS.has(tag)) {
|
|
1911
|
+
for (const id of value as string[]) {
|
|
1912
|
+
push(escapeOboWord(id), id, true);
|
|
1913
|
+
}
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
// def.xrefs, xref.descriptions and obo.qualifiers are written with the clauses they belong to
|
|
1917
|
+
if (Object.hasOwn(STRUCTURED_TAGS, tag)) {
|
|
1918
|
+
STRUCTURED_TAGS[tag](f, tag, value, out);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/** Writes the clauses of a structured tag column's cell. */
|
|
1923
|
+
type StructuredLines = (f: FrameState, tag: string, value: unknown, out: string[]) => void;
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* The qualifier block of a structured value.
|
|
1927
|
+
* @param qualifiers - the value's qualifiers, or undefined
|
|
1928
|
+
* @returns the block with a leading space, or ""
|
|
1929
|
+
*/
|
|
1930
|
+
function blockOf(qualifiers: unknown): string {
|
|
1931
|
+
return qualifiers === undefined ? "" : qualifierBlock(qualifierPairs(qualifiers));
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/** The writers of the tags whose cells hold more than a text, an id list or a boolean. */
|
|
1935
|
+
const STRUCTURED_TAGS: Readonly<Record<string, StructuredLines>> = {
|
|
1936
|
+
def(f, _tag, value, out) {
|
|
1937
|
+
const xrefs = f.p.tags.get("def.xrefs");
|
|
1938
|
+
const ids = (xrefs === undefined ? [] : (cellOf(xrefs, f.row) ?? [])) as string[];
|
|
1939
|
+
const text = value as string;
|
|
1940
|
+
out.push(
|
|
1941
|
+
`def: "${escapeOboQuoted(counted(f.stats, text))}" ${xrefList(f, ids, "def")}${f.cursor.block("def", text)}`,
|
|
1942
|
+
);
|
|
1943
|
+
},
|
|
1944
|
+
xref(f, _tag, value, out) {
|
|
1945
|
+
for (const id of value as string[]) {
|
|
1946
|
+
out.push(`xref: ${xrefText(f, id, null)}${f.cursor.block("xref", id)}`);
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
synonym(f, _tag, value, out) {
|
|
1950
|
+
for (const s of value as {
|
|
1951
|
+
text: string;
|
|
1952
|
+
scope: string;
|
|
1953
|
+
type: string | null;
|
|
1954
|
+
xrefs: string[];
|
|
1955
|
+
qualifiers?: unknown;
|
|
1956
|
+
}[]) {
|
|
1957
|
+
const type = s.type === null ? "" : ` ${escapeOboWord(s.type)}`;
|
|
1958
|
+
out.push(
|
|
1959
|
+
`synonym: "${escapeOboQuoted(counted(f.stats, s.text))}" ${s.scope}${type} ${xrefList(f, s.xrefs, "synonym")}${blockOf(s.qualifiers)}`,
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
intersection_of(_f, _tag, value, out) {
|
|
1964
|
+
for (const x of value as { relation: string | null; target: string; qualifiers?: unknown }[]) {
|
|
1965
|
+
const relation = x.relation === null ? "" : `${escapeOboWord(x.relation)} `;
|
|
1966
|
+
out.push(`intersection_of: ${relation}${escapeOboWord(x.target)}${blockOf(x.qualifiers)}`);
|
|
1967
|
+
}
|
|
1968
|
+
},
|
|
1969
|
+
property_value(f, _tag, value, out) {
|
|
1970
|
+
for (const x of value as { relation: string; value: string; datatype: string | null; qualifiers?: unknown }[]) {
|
|
1971
|
+
const datatype = x.datatype === null ? "" : ` ${escapeOboWord(x.datatype)}`;
|
|
1972
|
+
const literal =
|
|
1973
|
+
x.datatype === null && isOboWord(x.value)
|
|
1974
|
+
? escapeOboWord(x.value)
|
|
1975
|
+
: `"${escapeOboQuoted(counted(f.stats, x.value))}"${datatype}`;
|
|
1976
|
+
out.push(`property_value: ${escapeOboWord(x.relation)} ${literal}${blockOf(x.qualifiers)}`);
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
holds_over_chain: chainLines,
|
|
1980
|
+
equivalent_to_chain: chainLines,
|
|
1981
|
+
expand_assertion_to: expansionLines,
|
|
1982
|
+
expand_expression_to: expansionLines,
|
|
1983
|
+
[UNRECOGNIZED](f, _tag, value, out) {
|
|
1984
|
+
for (const [unknown, values] of Object.entries(value as Record<string, string[]>)) {
|
|
1985
|
+
for (const v of values) {
|
|
1986
|
+
out.push(`${escapeTag(unknown)}: ${escapeOboValue(counted(f.stats, v))}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* The clauses of a relation chain tag (`holds_over_chain`, `equivalent_to_chain`).
|
|
1994
|
+
* @param f - the frame state
|
|
1995
|
+
* @param tag - the tag
|
|
1996
|
+
* @param value - the cell: pairs of relations
|
|
1997
|
+
* @param out - the lines
|
|
1998
|
+
*/
|
|
1999
|
+
function chainLines(f: FrameState, tag: string, value: unknown, out: string[]): void {
|
|
2000
|
+
for (const [a, b] of value as [string, string][]) {
|
|
2001
|
+
const pair = `${a} ${b}`;
|
|
2002
|
+
out.push(`${tag}: ${escapeOboWord(a)} ${escapeOboWord(b)}${f.cursor.block(tag, pair)}`);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* The clauses of a macro expansion tag (`expand_assertion_to`, `expand_expression_to`).
|
|
2008
|
+
* @param f - the frame state
|
|
2009
|
+
* @param tag - the tag
|
|
2010
|
+
* @param value - the cell: templates with their xrefs
|
|
2011
|
+
* @param out - the lines
|
|
2012
|
+
*/
|
|
2013
|
+
function expansionLines(f: FrameState, tag: string, value: unknown, out: string[]): void {
|
|
2014
|
+
for (const x of value as { template: string; xrefs: string[]; qualifiers?: unknown }[]) {
|
|
2015
|
+
out.push(
|
|
2016
|
+
`${tag}: "${escapeOboQuoted(counted(f.stats, x.template))}" ${xrefList(f, x.xrefs, tag)}${blockOf(x.qualifiers)}`,
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* The lines of one node's frame.
|
|
2023
|
+
* @param p - the plan
|
|
2024
|
+
* @param row - the node
|
|
2025
|
+
* @param stats - the counters
|
|
2026
|
+
* @returns the lines, the frame header first
|
|
2027
|
+
*/
|
|
2028
|
+
function frameLines(p: Plan, row: number, stats: FrameStats): string[] {
|
|
2029
|
+
const qualifiersColumn = p.tags.get("obo.qualifiers");
|
|
2030
|
+
const f: FrameState = {
|
|
2031
|
+
p,
|
|
2032
|
+
row,
|
|
2033
|
+
cursor: new QualifierCursor(qualifiersColumn === undefined ? undefined : cellOf(qualifiersColumn, row)),
|
|
2034
|
+
described: new Set(),
|
|
2035
|
+
stats,
|
|
2036
|
+
};
|
|
2037
|
+
const out = [`[${p.kinds[row]}]`, `id: ${escapeOboWord(p.ids[row])}`];
|
|
2038
|
+
for (const tag of TAG_ORDER) {
|
|
2039
|
+
if (tag === "name") {
|
|
2040
|
+
nameLine(f, out);
|
|
2041
|
+
} else if (tag === "property_value") {
|
|
2042
|
+
propertyValueLines(f, out);
|
|
2043
|
+
} else if (tag === EDGES) {
|
|
2044
|
+
edgeLines(f, out);
|
|
2045
|
+
} else {
|
|
2046
|
+
const column = p.tags.get(tag);
|
|
2047
|
+
if (column !== undefined) {
|
|
2048
|
+
tagLines(f, tag, column, out);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
stats.qualifiersPlaced &&= f.cursor.placed();
|
|
2053
|
+
return out;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* The name clause of a frame, from the label column.
|
|
2058
|
+
* @param f - the frame state
|
|
2059
|
+
* @param out - the lines
|
|
2060
|
+
*/
|
|
2061
|
+
function nameLine(f: FrameState, out: string[]): void {
|
|
2062
|
+
const name = f.p.label === null ? undefined : cellOf(f.p.label, f.row);
|
|
2063
|
+
if (typeof name === "string") {
|
|
2064
|
+
out.push(`name: ${escapeOboValue(counted(f.stats, name))}${f.cursor.block("name", name)}`);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* The property_value clauses of a frame: the original id of a rewritten one, the property_value
|
|
2070
|
+
* column, then the property-value columns.
|
|
2071
|
+
* @param f - the frame state
|
|
2072
|
+
* @param out - the lines
|
|
2073
|
+
*/
|
|
2074
|
+
function propertyValueLines(f: FrameState, out: string[]): void {
|
|
2075
|
+
const { p, row } = f;
|
|
2076
|
+
const original = p.originals.get(row);
|
|
2077
|
+
if (original !== undefined) {
|
|
2078
|
+
// as JSON text, so a carriage return in the original survives
|
|
2079
|
+
out.push(`property_value: ${ORIGINAL_ID} "${escapeOboQuoted(JSON.stringify(original))}" xsd:string`);
|
|
2080
|
+
}
|
|
2081
|
+
const column = p.tags.get("property_value");
|
|
2082
|
+
if (column !== undefined) {
|
|
2083
|
+
tagLines(f, "property_value", column, out);
|
|
2084
|
+
}
|
|
2085
|
+
for (const prop of p.properties) {
|
|
2086
|
+
for (const line of propertyLines(prop, row, f.stats)) {
|
|
2087
|
+
out.push(`property_value: ${line}`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* The edge clauses of a frame: `is_a` (and `instance_of` on an Instance) as their own tags, any
|
|
2094
|
+
* other relation as a `relationship` clause.
|
|
2095
|
+
* @param f - the frame state
|
|
2096
|
+
* @param out - the lines
|
|
2097
|
+
*/
|
|
2098
|
+
function edgeLines(f: FrameState, out: string[]): void {
|
|
2099
|
+
const { p, row } = f;
|
|
2100
|
+
for (let k = p.edgeStart[row]; k < p.edgeStart[row + 1]; k++) {
|
|
2101
|
+
const e = p.edgeOrder[k];
|
|
2102
|
+
const target = escapeOboWord(p.ids[p.dst[e]]);
|
|
2103
|
+
const relation = p.relations[e];
|
|
2104
|
+
const block = edgeQualifiers(f, e);
|
|
2105
|
+
if (relation === "is_a" || (relation === "instance_of" && p.kinds[row] === "Instance")) {
|
|
2106
|
+
out.push(`${relation}: ${target}${block}`);
|
|
2107
|
+
} else {
|
|
2108
|
+
out.push(`relationship: ${escapeOboWord(relation)} ${target}${block}`);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Write every frame once without keeping the text: count the clauses that repeat another of their
|
|
2115
|
+
* frame (the importer reads them as one), the line ends and whether the qualifiers were placed.
|
|
2116
|
+
* @param p - the plan
|
|
2117
|
+
* @param rows - the written nodes
|
|
2118
|
+
* @returns the counts
|
|
2119
|
+
*/
|
|
2120
|
+
function simulate(p: Plan, rows: readonly number[]): FrameStats & { duplicates: number } {
|
|
2121
|
+
const stats = { lineEnds: 0, qualifiersPlaced: true, duplicates: 0 };
|
|
2122
|
+
for (const i of rows) {
|
|
2123
|
+
const lines = frameLines(p, i, stats);
|
|
2124
|
+
const seen = new Set<string>();
|
|
2125
|
+
for (let k = 2; k < lines.length; k++) {
|
|
2126
|
+
if (seen.has(lines[k])) {
|
|
2127
|
+
stats.duplicates++;
|
|
2128
|
+
}
|
|
2129
|
+
seen.add(lines[k]);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return stats;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
/**
|
|
2136
|
+
* The text parts of the file: the header, one frame per written node, the tail frames.
|
|
2137
|
+
* @param p - the plan
|
|
2138
|
+
* @yields the header and one part per frame
|
|
2139
|
+
* @returns nothing
|
|
2140
|
+
*/
|
|
2141
|
+
function* write(p: Plan): Generator<string, void, undefined> {
|
|
2142
|
+
if (p.fatal !== null) {
|
|
2143
|
+
throw p.fatal;
|
|
2144
|
+
}
|
|
2145
|
+
yield p.header.map((line) => `${line}\n`).join("");
|
|
2146
|
+
const stats: FrameStats = { lineEnds: 0, qualifiersPlaced: true };
|
|
2147
|
+
for (let i = 0; i < p.ids.length; i++) {
|
|
2148
|
+
if (p.frameless[i] === 0) {
|
|
2149
|
+
yield `\n${frameLines(p, i, stats).join("\n")}\n`;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
if (p.tail.length > 0) {
|
|
2153
|
+
yield `${p.tail.join("\n")}\n`;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* The text parts of an export, planned when the first part is asked for, so a refused option
|
|
2159
|
+
* rejects the export instead of throwing from the call.
|
|
2160
|
+
* @param snapshot - the snapshot
|
|
2161
|
+
* @param options - format-specific and common options
|
|
2162
|
+
* @yields the parts
|
|
2163
|
+
* @returns nothing
|
|
2164
|
+
*/
|
|
2165
|
+
function* written(
|
|
2166
|
+
snapshot: GraphSnapshot,
|
|
2167
|
+
options: (OboExportOptions & CommonExportOptions) | undefined,
|
|
2168
|
+
): Generator<string, void, undefined> {
|
|
2169
|
+
yield* write(plan(snapshot, options));
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* The OBO exporter plugin.
|
|
2174
|
+
* @category Built-in formats
|
|
2175
|
+
*/
|
|
2176
|
+
export const oboExporter: GraphExporter<OboExportOptions> = Object.freeze({
|
|
2177
|
+
format: "obo",
|
|
2178
|
+
options: Object.freeze(["ontology", "relation"]),
|
|
2179
|
+
capabilities: OBO_CAPABILITIES,
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Pre-flight: what export() would lose, without writing anything.
|
|
2183
|
+
* @param snapshot - the snapshot
|
|
2184
|
+
* @param options - format-specific and common options
|
|
2185
|
+
* @returns the notes, empty when the export is exact
|
|
2186
|
+
*/
|
|
2187
|
+
check(snapshot: GraphSnapshot, options?: OboExportOptions & CommonExportOptions): readonly LossNote[] {
|
|
2188
|
+
return Object.freeze([...plan(snapshot, options).notes]);
|
|
2189
|
+
},
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* Write the file as UTF-8 chunks.
|
|
2193
|
+
* @param snapshot - the snapshot
|
|
2194
|
+
* @param options - format-specific and common options
|
|
2195
|
+
* @returns the chunks
|
|
2196
|
+
*/
|
|
2197
|
+
export(snapshot: GraphSnapshot, options?: OboExportOptions & CommonExportOptions): AsyncIterable<Uint8Array> {
|
|
2198
|
+
return encodeChunks(written(snapshot, options));
|
|
2199
|
+
},
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Write the file as one string.
|
|
2203
|
+
* @param snapshot - the snapshot
|
|
2204
|
+
* @param options - format-specific and common options
|
|
2205
|
+
* @returns the file
|
|
2206
|
+
*/
|
|
2207
|
+
exportToString(snapshot: GraphSnapshot, options?: OboExportOptions & CommonExportOptions): Promise<string> {
|
|
2208
|
+
return joinText(written(snapshot, options));
|
|
2209
|
+
},
|
|
2210
|
+
});
|