@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,3465 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
import { S as SINK_OPTION_CODE, O as OPTION_IGNORED_CODE, R as ROLE_TAKEN_CODE, ay as RENAMED_CODE, I as ID_MERGED_CODE, i as DANGLING_REFERENCE_CODE, g as UNKNOWN_ELEMENT_CODE, o as DUPLICATE_ATTRIBUTE_CODE, n as DUPLICATE_NODE_CODE, a3 as BAD_VALUE_CODE, t as MISSING_ID_CODE, w as UNKNOWN_ENCODING_CODE, x as ENCODING_FALLBACK_CODE, y as INVALID_ENCODING_CODE, z as INVALID_UTF8_CODE, E as EMPTY_INPUT_CODE, F as ELEMENT_ISSUE, G as INPUT_ISSUE, al as resolveImportOptions, a8 as ImportReportBuilder, ao as reportSinkOptions, an as reportUnusedOptions, am as throwIfAborted, b as plural, a1 as declareResolved, ar as IdCoercer, aq as DirectionResolver, $ as agree, az as LineReader, c as capabilities, L as LOSS, au as ROLE_ASSUMED_CODE, aA as NODE_ORDER_CODE, aB as GRAPH_COLUMN_AS_METADATA_CODE, aC as TYPEDEF_NODES_CODE, aD as RELATION_ASSUMED_CODE, at as COLUMN_AS_PROPERTY_VALUE_CODE, r as resolveExportOptions, p as pairFolding, e as explicitWeights, av as formatNumber } from "./export-BTI4yQgy.js";
|
|
3
|
+
import { S as SYNONYM_SCOPES, O as OBO_NODE_COLUMNS, o as oboColumnDecl, P as PLACEHOLDER_COLUMN, f as framelessNodes, m as movedNodes, s as slotColumn, c as capabilityNotes, e as emptyColumnNotes, a as cellOf, F as FRAME_KINDS$1, U as UNWRITTEN_ROLES, n as notePropertyColumn, b as fitsVocabulary, i as isQualifierRecord, t as textOf } from "./ontology-export-B9Tcf37D.js";
|
|
4
|
+
import { j as joinText, e as encodeChunks } from "./writer-DxjgHwSe.js";
|
|
5
|
+
const ESCAPES = Object.freeze({
|
|
6
|
+
n: "\n",
|
|
7
|
+
t: " ",
|
|
8
|
+
// the guides read `\W` as a space; the 1.4 BNF (and fastobo) as the letter W (design 4.1)
|
|
9
|
+
W: " "
|
|
10
|
+
});
|
|
11
|
+
function unescapeObo(text) {
|
|
12
|
+
if (!text.includes("\\")) {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
let out = "";
|
|
16
|
+
for (let i = 0; i < text.length; i++) {
|
|
17
|
+
const ch = text[i];
|
|
18
|
+
if (ch !== "\\") {
|
|
19
|
+
out += ch;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
i++;
|
|
23
|
+
if (i < text.length) {
|
|
24
|
+
const next = text[i];
|
|
25
|
+
out += ESCAPES[next] ?? next;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function isEscaped(text, index) {
|
|
31
|
+
let run = 0;
|
|
32
|
+
for (let i = index - 1; i >= 0 && text[i] === "\\"; i--) {
|
|
33
|
+
run++;
|
|
34
|
+
}
|
|
35
|
+
return run % 2 === 1;
|
|
36
|
+
}
|
|
37
|
+
function splitFormFeeds(line) {
|
|
38
|
+
if (!line.includes("\f")) {
|
|
39
|
+
return [line];
|
|
40
|
+
}
|
|
41
|
+
const pieces = [];
|
|
42
|
+
let quoted = false;
|
|
43
|
+
let start = 0;
|
|
44
|
+
for (let i = 0; i < line.length; i += line[i] === "\\" ? 2 : 1) {
|
|
45
|
+
const ch = line[i];
|
|
46
|
+
if (ch === '"') {
|
|
47
|
+
quoted = !quoted;
|
|
48
|
+
} else if (ch === "\f" && !quoted) {
|
|
49
|
+
pieces.push(line.slice(start, i));
|
|
50
|
+
start = i + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
pieces.push(line.slice(start));
|
|
54
|
+
return pieces;
|
|
55
|
+
}
|
|
56
|
+
function endsWithContinuation(line) {
|
|
57
|
+
return line.endsWith("\\") && !isEscaped(line, line.length - 1);
|
|
58
|
+
}
|
|
59
|
+
function splitTagValue(line) {
|
|
60
|
+
for (let i = 0; i < line.length; i++) {
|
|
61
|
+
if (line[i] === "\\") {
|
|
62
|
+
i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (line[i] === ":") {
|
|
66
|
+
return { tag: unescapeObo(line.slice(0, i).trim()), rest: line.slice(i + 1).trimStart() };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function stripComment(rest) {
|
|
72
|
+
let quoted = false;
|
|
73
|
+
for (let i = 0; i < rest.length; i++) {
|
|
74
|
+
const ch = rest[i];
|
|
75
|
+
if (ch === "\\") {
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const atStart = i === 0 || rest[i - 1] === " " || rest[i - 1] === " ";
|
|
80
|
+
if (ch === '"' && (quoted || atStart)) {
|
|
81
|
+
quoted = !quoted;
|
|
82
|
+
} else if (ch === "!" && !quoted && atStart) {
|
|
83
|
+
return rest.slice(0, i).trimEnd();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return rest.trimEnd();
|
|
87
|
+
}
|
|
88
|
+
function splitQualifiers(value) {
|
|
89
|
+
if (!value.endsWith("}")) {
|
|
90
|
+
return { value, qualifiers: null, badBlock: null };
|
|
91
|
+
}
|
|
92
|
+
const blocks = braceBlocks(value);
|
|
93
|
+
const parsed = [];
|
|
94
|
+
let end = value.length;
|
|
95
|
+
let badBlock = null;
|
|
96
|
+
while (end > 0 && value[end - 1] === "}" && !isEscaped(value, end - 1)) {
|
|
97
|
+
const open = blocks.get(end - 1) ?? -1;
|
|
98
|
+
const block = open < 0 ? null : parseQualifiers(value.slice(open + 1, end - 1));
|
|
99
|
+
if (block === null) {
|
|
100
|
+
badBlock = value.slice(Math.max(open, 0), end);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
parsed.push(block);
|
|
104
|
+
end = open;
|
|
105
|
+
while (end > 0 && /\s/.test(value[end - 1])) {
|
|
106
|
+
end--;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let qualifiers = null;
|
|
110
|
+
for (let i = parsed.length - 1; i >= 0; i--) {
|
|
111
|
+
qualifiers ??= /* @__PURE__ */ Object.create(null);
|
|
112
|
+
for (const [name, v] of Object.entries(parsed[i])) {
|
|
113
|
+
addQualifier(qualifiers, name, v);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { value: value.slice(0, end), qualifiers, badBlock };
|
|
117
|
+
}
|
|
118
|
+
function braceBlocks(value) {
|
|
119
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
120
|
+
let quoted = false;
|
|
121
|
+
let open = -1;
|
|
122
|
+
for (let i = 0; i < value.length; i++) {
|
|
123
|
+
const ch = value[i];
|
|
124
|
+
if (ch === "\\") {
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '"') {
|
|
129
|
+
quoted = !quoted;
|
|
130
|
+
} else if (!quoted && ch === "{") {
|
|
131
|
+
open = i;
|
|
132
|
+
} else if (!quoted && ch === "}") {
|
|
133
|
+
blocks.set(i, open);
|
|
134
|
+
open = -1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return blocks;
|
|
138
|
+
}
|
|
139
|
+
function unclosedBlock(value) {
|
|
140
|
+
let quoted = false;
|
|
141
|
+
let open = -1;
|
|
142
|
+
for (let i = 0; i < value.length; i += value[i] === "\\" ? 2 : 1) {
|
|
143
|
+
const ch = value[i];
|
|
144
|
+
if (ch === "\\") {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === '"' && open < 0) {
|
|
148
|
+
quoted = !quoted;
|
|
149
|
+
} else if (!quoted && ch === "{") {
|
|
150
|
+
open = i;
|
|
151
|
+
} else if (!quoted && ch === "}") {
|
|
152
|
+
open = -1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return open;
|
|
156
|
+
}
|
|
157
|
+
function addQualifier(record, name, value) {
|
|
158
|
+
const values = Array.isArray(value) ? value : [value];
|
|
159
|
+
if (!Object.prototype.hasOwnProperty.call(record, name)) {
|
|
160
|
+
record[name] = Array.isArray(value) ? [...value] : value;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const before = record[name];
|
|
164
|
+
if (Array.isArray(before)) {
|
|
165
|
+
for (const v of values) {
|
|
166
|
+
before.push(v);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
record[name] = [before, ...values];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function parseQualifiers(inner) {
|
|
173
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
174
|
+
let i = 0;
|
|
175
|
+
const n = inner.length;
|
|
176
|
+
let any = false;
|
|
177
|
+
while (i < n) {
|
|
178
|
+
while (i < n && (inner[i] === " " || inner[i] === " " || inner[i] === ",")) {
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
if (i >= n) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const eq = inner.indexOf("=", i);
|
|
185
|
+
if (eq < 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const name = unescapeObo(inner.slice(i, eq).trim());
|
|
189
|
+
if (name.length === 0 || name.includes('"')) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
i = eq + 1;
|
|
193
|
+
while (i < n && (inner[i] === " " || inner[i] === " ")) {
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
let value;
|
|
197
|
+
if (inner[i] === '"') {
|
|
198
|
+
const end = closingQuote(inner, i + 1);
|
|
199
|
+
if (end < 0) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
value = unescapeObo(inner.slice(i + 1, end));
|
|
203
|
+
i = end + 1;
|
|
204
|
+
} else {
|
|
205
|
+
let end = i;
|
|
206
|
+
while (end < n && inner[end] !== ",") {
|
|
207
|
+
end += inner[end] === "\\" ? 2 : 1;
|
|
208
|
+
}
|
|
209
|
+
value = unescapeObo(inner.slice(i, end).trim());
|
|
210
|
+
i = end;
|
|
211
|
+
}
|
|
212
|
+
addQualifier(out, name, value);
|
|
213
|
+
any = true;
|
|
214
|
+
}
|
|
215
|
+
return any ? out : null;
|
|
216
|
+
}
|
|
217
|
+
function closingQuote(text, from) {
|
|
218
|
+
for (let i = from; i < text.length; i++) {
|
|
219
|
+
if (text[i] === "\\") {
|
|
220
|
+
i++;
|
|
221
|
+
} else if (text[i] === '"') {
|
|
222
|
+
return i;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
function tokenize(value) {
|
|
228
|
+
const tokens = [];
|
|
229
|
+
const n = value.length;
|
|
230
|
+
let i = 0;
|
|
231
|
+
while (i < n) {
|
|
232
|
+
const ch = value[i];
|
|
233
|
+
if (ch === " " || ch === " ") {
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (ch === '"') {
|
|
238
|
+
const end2 = closingQuote(value, i + 1);
|
|
239
|
+
const stop = end2 < 0 ? n : end2;
|
|
240
|
+
let after = stop + 1;
|
|
241
|
+
if (end2 >= 0 && value[after] === "@") {
|
|
242
|
+
while (after < n && value[after] !== " " && value[after] !== " ") {
|
|
243
|
+
after++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const suffix = end2 < 0 ? "" : value.slice(stop + 1, after);
|
|
247
|
+
tokens.push({
|
|
248
|
+
kind: "quoted",
|
|
249
|
+
text: unescapeObo(value.slice(i + 1, stop)) + suffix,
|
|
250
|
+
unterminated: end2 < 0
|
|
251
|
+
});
|
|
252
|
+
i = after;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (ch === "[") {
|
|
256
|
+
const end2 = closingBracket(value, i + 1);
|
|
257
|
+
const stop = end2 < 0 ? n : end2;
|
|
258
|
+
tokens.push({ kind: "list", text: value.slice(i + 1, stop), unterminated: end2 < 0 });
|
|
259
|
+
i = stop + 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
let end = i;
|
|
263
|
+
while (end < n && value[end] !== " " && value[end] !== " ") {
|
|
264
|
+
end += value[end] === "\\" ? 2 : 1;
|
|
265
|
+
}
|
|
266
|
+
tokens.push({ kind: "word", text: unescapeObo(value.slice(i, Math.min(end, n))), unterminated: false });
|
|
267
|
+
i = end;
|
|
268
|
+
}
|
|
269
|
+
return tokens;
|
|
270
|
+
}
|
|
271
|
+
function closingBracket(text, from) {
|
|
272
|
+
let quoted = false;
|
|
273
|
+
for (let i = from; i < text.length; i++) {
|
|
274
|
+
const ch = text[i];
|
|
275
|
+
if (ch === "\\") {
|
|
276
|
+
i++;
|
|
277
|
+
} else if (ch === '"') {
|
|
278
|
+
quoted = !quoted;
|
|
279
|
+
} else if (ch === "]" && !quoted) {
|
|
280
|
+
return i;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return -1;
|
|
284
|
+
}
|
|
285
|
+
function parseXref(raw) {
|
|
286
|
+
const { value, qualifiers } = splitQualifiers(raw.trim());
|
|
287
|
+
let quote = -1;
|
|
288
|
+
for (let i = 0; i < value.length; i++) {
|
|
289
|
+
if (value[i] === "\\") {
|
|
290
|
+
i++;
|
|
291
|
+
} else if (value[i] === '"') {
|
|
292
|
+
quote = i;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const id = unescapeObo((quote < 0 ? value : value.slice(0, quote)).trim());
|
|
297
|
+
if (id.length === 0) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
let description = null;
|
|
301
|
+
let unterminated = false;
|
|
302
|
+
if (quote >= 0) {
|
|
303
|
+
const end = closingQuote(value, quote + 1);
|
|
304
|
+
unterminated = end < 0;
|
|
305
|
+
description = unescapeObo(value.slice(quote + 1, end < 0 ? value.length : end));
|
|
306
|
+
}
|
|
307
|
+
return { id, description, qualifiers, unterminated };
|
|
308
|
+
}
|
|
309
|
+
function parseXrefList(inner) {
|
|
310
|
+
const out = [];
|
|
311
|
+
let quoted = false;
|
|
312
|
+
let depth = 0;
|
|
313
|
+
let start = 0;
|
|
314
|
+
const flush = (end) => {
|
|
315
|
+
const xref = parseXref(inner.slice(start, end));
|
|
316
|
+
if (xref !== null) {
|
|
317
|
+
out.push(xref);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
for (let i = 0; i < inner.length; i++) {
|
|
321
|
+
const ch = inner[i];
|
|
322
|
+
if (ch === "\\") {
|
|
323
|
+
i++;
|
|
324
|
+
} else if (ch === '"') {
|
|
325
|
+
quoted = !quoted;
|
|
326
|
+
} else if (!quoted && ch === "{") {
|
|
327
|
+
depth++;
|
|
328
|
+
} else if (!quoted && ch === "}") {
|
|
329
|
+
depth = Math.max(0, depth - 1);
|
|
330
|
+
} else if (!quoted && depth === 0 && ch === ",") {
|
|
331
|
+
flush(i);
|
|
332
|
+
start = i + 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
flush(inner.length);
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
function hasStrayBrace(value) {
|
|
339
|
+
let quoted = false;
|
|
340
|
+
let listed = false;
|
|
341
|
+
for (let i = 0; i < value.length; i++) {
|
|
342
|
+
const ch = value[i];
|
|
343
|
+
if (ch === "\\") {
|
|
344
|
+
i++;
|
|
345
|
+
} else if (ch === '"') {
|
|
346
|
+
quoted = !quoted;
|
|
347
|
+
} else if (!quoted && (ch === "[" || ch === "]")) {
|
|
348
|
+
listed = ch === "[";
|
|
349
|
+
} else if (!quoted && !listed && (ch === "{" || ch === "}")) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const LINE_ENDS = /\r\n|\r|\f/g;
|
|
356
|
+
function hasLineEnd(text) {
|
|
357
|
+
return text.includes("\r") || text.includes("\f");
|
|
358
|
+
}
|
|
359
|
+
function escapeOboQuoted(text) {
|
|
360
|
+
return text.replaceAll(LINE_ENDS, "\n").replaceAll(/[\\"\n\t]/g, (ch) => {
|
|
361
|
+
switch (ch) {
|
|
362
|
+
case "\n":
|
|
363
|
+
return String.raw`\n`;
|
|
364
|
+
case " ":
|
|
365
|
+
return String.raw`\t`;
|
|
366
|
+
default:
|
|
367
|
+
return `\\${ch}`;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function escapeOboValue(text) {
|
|
372
|
+
return escapeOboQuoted(text).replaceAll(/[!{}[\],]/g, (ch) => `\\${ch}`);
|
|
373
|
+
}
|
|
374
|
+
function isOboWord(text) {
|
|
375
|
+
return text.length > 0 && !NOT_IN_WORD.test(text);
|
|
376
|
+
}
|
|
377
|
+
const NOT_IN_WORD = /[\s!{}\p{Cc}]/u;
|
|
378
|
+
function escapeOboWord(word) {
|
|
379
|
+
const out = word.replaceAll(/[\\"]/g, (ch) => `\\${ch}`);
|
|
380
|
+
return out.startsWith("[") ? `\\${out}` : out;
|
|
381
|
+
}
|
|
382
|
+
function isQualifierName(name) {
|
|
383
|
+
return name.length > 0 && name === name.trim() && !/[=",{}[\]!\\\n\r\t\f]/.test(name);
|
|
384
|
+
}
|
|
385
|
+
function qualifierBlock(pairs) {
|
|
386
|
+
if (pairs.length === 0) {
|
|
387
|
+
return "";
|
|
388
|
+
}
|
|
389
|
+
const items = pairs.map(([name, value]) => `${name}="${escapeOboQuoted(value)}"`);
|
|
390
|
+
return ` {${items.join(", ")}}`;
|
|
391
|
+
}
|
|
392
|
+
const OBO_ISSUE = Object.freeze({
|
|
393
|
+
...INPUT_ISSUE,
|
|
394
|
+
...ELEMENT_ISSUE,
|
|
395
|
+
/** The input is empty or whitespace. The import stops. */
|
|
396
|
+
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
397
|
+
/** The input is not valid UTF-8. The import stops. */
|
|
398
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
399
|
+
/**
|
|
400
|
+
* Some bytes are not valid in the encoding that was chosen (by a byte order mark or the `encoding` option). The
|
|
401
|
+
* import stops.
|
|
402
|
+
*/
|
|
403
|
+
INVALID_ENCODING: INVALID_ENCODING_CODE,
|
|
404
|
+
/** Bytes that are not UTF-8 were read as windows-1252. */
|
|
405
|
+
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
406
|
+
/** A declared encoding the platform cannot decode was ignored. */
|
|
407
|
+
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
408
|
+
/** A frame without an `id` clause; the frame is skipped. */
|
|
409
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
410
|
+
/** A clause whose value cannot be read (a boolean other than true / false, a relationship with one or three values); the clause is skipped. */
|
|
411
|
+
BAD_VALUE: BAD_VALUE_CODE,
|
|
412
|
+
/** Two frames have the same id, so they were merged into one node. */
|
|
413
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
414
|
+
/** A tag that may appear once per frame appears twice; the first value is kept. */
|
|
415
|
+
DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
|
|
416
|
+
/** An unknown tag (kept in `obo.unrecognized`) or frame type (skipped), once per name. */
|
|
417
|
+
UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
|
|
418
|
+
/** A target no frame declares: a placeholder node was made, or the edge dropped under addMissingNodes false. */
|
|
419
|
+
DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
|
|
420
|
+
/** A line without a colon, an unterminated quote, a def without its xref list, a qualifier block that does not parse, an unescaped brace. */
|
|
421
|
+
SYNTAX: "W_OBO_SYNTAX",
|
|
422
|
+
/**
|
|
423
|
+
* Nothing in the input looks like OBO: there is no frame header and no header tag. It may be an HTML page, a JSON
|
|
424
|
+
* or GML file, or UTF-16 text without a byte order mark. The import stops.
|
|
425
|
+
*/
|
|
426
|
+
NOT_OBO: "E_OBO_NOT_OBO",
|
|
427
|
+
/**
|
|
428
|
+
* The header has no `format-version` (OBO 1.2 and 1.4 require one), or names a version other than 1.0, 1.2 or 1.4.
|
|
429
|
+
* The file is read accepting the tags of every version.
|
|
430
|
+
*/
|
|
431
|
+
FORMAT_VERSION: "W_OBO_FORMAT_VERSION",
|
|
432
|
+
/** The frame's `id` is not its first clause; it is used anyway. */
|
|
433
|
+
ID_NOT_FIRST: "W_OBO_ID_NOT_FIRST",
|
|
434
|
+
/** A synonym without a scope in a file that does not say 1.2, or with a scope that is not one of the four. */
|
|
435
|
+
SYNONYM_SCOPE: "W_OBO_SYNONYM_SCOPE",
|
|
436
|
+
/** A relation, subset or synonym type that nothing declares. */
|
|
437
|
+
UNDECLARED: "W_OBO_UNDECLARED",
|
|
438
|
+
/** One id for a Term and a Typedef (or an Instance); the Term (the first node frame) is the node. */
|
|
439
|
+
ID_KIND_CLASH: "W_OBO_ID_KIND_CLASH",
|
|
440
|
+
/** A frame has only one `intersection_of` or `union_of` clause, where OBO needs at least two. */
|
|
441
|
+
CARDINALITY: "W_OBO_CARDINALITY",
|
|
442
|
+
/** An obsolete term with is_a / relationship, or replaced_by / consider on a term that is not obsolete. */
|
|
443
|
+
OBSOLETION: "W_OBO_OBSOLETION",
|
|
444
|
+
/** An OBO 1.0 / 1.2 tag read as its 1.4 meaning (exact_synonym, xref_analog, use_term, typeref, version). */
|
|
445
|
+
DEPRECATED_TAG: "W_OBO_DEPRECATED_TAG",
|
|
446
|
+
/** A backslash line continuation (deprecated in 1.4). */
|
|
447
|
+
DEPRECATED_SYNTAX: "W_OBO_DEPRECATED_SYNTAX",
|
|
448
|
+
/**
|
|
449
|
+
* A header clause whose meaning graph-io does not apply (`import`, `id-mapping`, the `treat-xrefs` macros,
|
|
450
|
+
* `owl-axioms`); it is kept in `snapshot.meta.extra.obo.header`.
|
|
451
|
+
*/
|
|
452
|
+
HEADER_NOT_APPLIED: "W_OBO_HEADER_NOT_APPLIED",
|
|
453
|
+
/** Obsolete terms and their edges left out under obsolete: "drop". */
|
|
454
|
+
OBSOLETE_DROPPED: "W_OBO_OBSOLETE_DROPPED",
|
|
455
|
+
/**
|
|
456
|
+
* Two different id texts became the same number because `ids` is "number" (for example, "042" and "42"), so their nodes
|
|
457
|
+
* were merged.
|
|
458
|
+
*/
|
|
459
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
460
|
+
/** An OBO attribute was renamed `<name>#obo` because another attribute already has its name. */
|
|
461
|
+
COLUMN_RENAMED: RENAMED_CODE,
|
|
462
|
+
/**
|
|
463
|
+
* You read into a graph builder that already has an id, label or position attribute, so this file's one is kept as
|
|
464
|
+
* a plain attribute.
|
|
465
|
+
*/
|
|
466
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
467
|
+
/** You set an option this format does not use; it had no effect. The message names the option. */
|
|
468
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
469
|
+
/**
|
|
470
|
+
* You read into your own graph builder, which was created with a different `addMissingNodes`, `duplicateEdges`,
|
|
471
|
+
* `selfLoops` or `weightDtype` than the option you passed; the builder's setting applies.
|
|
472
|
+
*/
|
|
473
|
+
SINK_OPTION: SINK_OPTION_CODE
|
|
474
|
+
});
|
|
475
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
476
|
+
"ids",
|
|
477
|
+
"addMissingNodes",
|
|
478
|
+
"duplicateEdges",
|
|
479
|
+
"selfLoops",
|
|
480
|
+
"onMixedDirection",
|
|
481
|
+
"weightDtype",
|
|
482
|
+
"errorLimit",
|
|
483
|
+
"signal",
|
|
484
|
+
"onProgress",
|
|
485
|
+
"encoding",
|
|
486
|
+
"restoreMangledIds"
|
|
487
|
+
]);
|
|
488
|
+
const ORIGINAL_ID$1 = "graphty:originalId";
|
|
489
|
+
const DEFAULTS = { ids: "keep", defaultDirected: true, weightFrom: null };
|
|
490
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
491
|
+
const SNIFF_BYTES = 4096;
|
|
492
|
+
const FRAME_KINDS = /* @__PURE__ */ new Set(["Term", "Typedef", "Instance"]);
|
|
493
|
+
const NOT_APPLIED_HEADER = /^(import|typeref|id-mapping|default-relationship-id-prefix|owl-axioms|treat-xrefs-as-.*)$/;
|
|
494
|
+
const DEPRECATED_HEADER = /* @__PURE__ */ new Map([
|
|
495
|
+
["typeref", "import"],
|
|
496
|
+
["version", "data-version"]
|
|
497
|
+
]);
|
|
498
|
+
const DEPRECATED_TAGS = /* @__PURE__ */ new Map([
|
|
499
|
+
["exact_synonym", ["synonym", "EXACT"]],
|
|
500
|
+
["narrow_synonym", ["synonym", "NARROW"]],
|
|
501
|
+
["broad_synonym", ["synonym", "BROAD"]],
|
|
502
|
+
["related_synonym", ["synonym", "RELATED"]],
|
|
503
|
+
["xref_analog", ["xref", null]],
|
|
504
|
+
["xref_unk", ["xref", null]],
|
|
505
|
+
["xref_unknown", ["xref", null]],
|
|
506
|
+
["use_term", ["consider", null]]
|
|
507
|
+
]);
|
|
508
|
+
const BUILTIN_RELATIONS$1 = /* @__PURE__ */ new Set([
|
|
509
|
+
"is_a",
|
|
510
|
+
"instance_of",
|
|
511
|
+
"disjoint_from",
|
|
512
|
+
"inverse_of",
|
|
513
|
+
"union_of",
|
|
514
|
+
"intersection_of"
|
|
515
|
+
]);
|
|
516
|
+
const TEXT_TAGS = /* @__PURE__ */ new Set([
|
|
517
|
+
"name",
|
|
518
|
+
"namespace",
|
|
519
|
+
"comment",
|
|
520
|
+
"created_by",
|
|
521
|
+
"creation_date",
|
|
522
|
+
"domain",
|
|
523
|
+
"range",
|
|
524
|
+
"inverse_of"
|
|
525
|
+
]);
|
|
526
|
+
const BOOL_TAGS = new Set(
|
|
527
|
+
Object.keys(OBO_NODE_COLUMNS).filter((name) => OBO_NODE_COLUMNS[name].dtype === "bool")
|
|
528
|
+
);
|
|
529
|
+
const ID_LIST_TAGS = /* @__PURE__ */ new Set([
|
|
530
|
+
"alt_id",
|
|
531
|
+
"subset",
|
|
532
|
+
"replaced_by",
|
|
533
|
+
"consider",
|
|
534
|
+
"union_of",
|
|
535
|
+
"equivalent_to",
|
|
536
|
+
"disjoint_from",
|
|
537
|
+
"transitive_over",
|
|
538
|
+
"disjoint_over"
|
|
539
|
+
]);
|
|
540
|
+
const SINGLE_HEADER_TAGS = /* @__PURE__ */ new Set([
|
|
541
|
+
"format-version",
|
|
542
|
+
"data-version",
|
|
543
|
+
"version",
|
|
544
|
+
"date",
|
|
545
|
+
"saved-by",
|
|
546
|
+
"auto-generated-by",
|
|
547
|
+
"default-namespace",
|
|
548
|
+
"ontology",
|
|
549
|
+
"default-relationship-id-prefix"
|
|
550
|
+
]);
|
|
551
|
+
const KNOWN_VERSION = /^(GO_)?1\.[024]$/;
|
|
552
|
+
const ID_VALUE_TAGS = /* @__PURE__ */ new Set([
|
|
553
|
+
...ID_LIST_TAGS,
|
|
554
|
+
"is_a",
|
|
555
|
+
"instance_of",
|
|
556
|
+
"relationship",
|
|
557
|
+
"intersection_of"
|
|
558
|
+
]);
|
|
559
|
+
const MAX_TALLIES_PER_CODE = 100;
|
|
560
|
+
function hasControl(text) {
|
|
561
|
+
for (const ch of text) {
|
|
562
|
+
if (ch < " " && ch !== " " && ch !== "\n" && ch !== "\f" && ch !== "\r" || ch === "") {
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
function isTagName(tag) {
|
|
569
|
+
return tag.length > 0 && !hasControl(tag) && !/[\s{}"<>[\]()]/.test(tag);
|
|
570
|
+
}
|
|
571
|
+
function ownRecord() {
|
|
572
|
+
return /* @__PURE__ */ Object.create(null);
|
|
573
|
+
}
|
|
574
|
+
function append(map, key, value) {
|
|
575
|
+
if (Object.hasOwn(map, key)) {
|
|
576
|
+
map[key].push(value);
|
|
577
|
+
} else {
|
|
578
|
+
map[key] = [value];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const TYPEDEF_TAGS = /* @__PURE__ */ new Set([
|
|
582
|
+
"domain",
|
|
583
|
+
"range",
|
|
584
|
+
"inverse_of",
|
|
585
|
+
"transitive_over",
|
|
586
|
+
"disjoint_over",
|
|
587
|
+
"holds_over_chain",
|
|
588
|
+
"equivalent_to_chain",
|
|
589
|
+
"expand_assertion_to",
|
|
590
|
+
"expand_expression_to",
|
|
591
|
+
"is_cyclic",
|
|
592
|
+
"is_reflexive",
|
|
593
|
+
"is_symmetric",
|
|
594
|
+
"is_anti_symmetric",
|
|
595
|
+
"is_asymmetric",
|
|
596
|
+
"is_transitive",
|
|
597
|
+
"is_functional",
|
|
598
|
+
"is_inverse_functional",
|
|
599
|
+
"is_metadata_tag",
|
|
600
|
+
"is_class_level"
|
|
601
|
+
]);
|
|
602
|
+
function resolveOboOptions$1(options) {
|
|
603
|
+
const obsolete = options?.obsolete ?? "keep";
|
|
604
|
+
const typedefs = options?.typedefs ?? "metadata";
|
|
605
|
+
if (obsolete !== "keep" && obsolete !== "drop") {
|
|
606
|
+
throw unsupported("obsolete", obsolete, ["keep", "drop"]);
|
|
607
|
+
}
|
|
608
|
+
if (typedefs !== "metadata" && typedefs !== "nodes") {
|
|
609
|
+
throw unsupported("typedefs", typedefs, ["metadata", "nodes"]);
|
|
610
|
+
}
|
|
611
|
+
return { obsolete, typedefs };
|
|
612
|
+
}
|
|
613
|
+
function unsupported(option, found, supported) {
|
|
614
|
+
return new GraphFormatError(
|
|
615
|
+
"E_UNSUPPORTED",
|
|
616
|
+
`option ${option}: ${JSON.stringify(found)} is not one of ${supported.join(", ")}`,
|
|
617
|
+
{
|
|
618
|
+
option,
|
|
619
|
+
found,
|
|
620
|
+
supported
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
class OboReader {
|
|
625
|
+
/**
|
|
626
|
+
* Create a reader.
|
|
627
|
+
* @param report - the report
|
|
628
|
+
* @param signal - the cancellation signal
|
|
629
|
+
* @param typedefsAsNodes - whether `[Typedef]` frames are nodes
|
|
630
|
+
*/
|
|
631
|
+
constructor(report, signal, typedefsAsNodes) {
|
|
632
|
+
this.header = ownRecord();
|
|
633
|
+
this.frameSeen = false;
|
|
634
|
+
this.tagClauses = 0;
|
|
635
|
+
this.firstBadLine = null;
|
|
636
|
+
this.version = null;
|
|
637
|
+
this.defaultNamespace = null;
|
|
638
|
+
this.subsets = /* @__PURE__ */ new Set();
|
|
639
|
+
this.synonymTypes = /* @__PURE__ */ new Map();
|
|
640
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
641
|
+
this.typedefs = /* @__PURE__ */ new Map();
|
|
642
|
+
this.tallies = /* @__PURE__ */ new Map();
|
|
643
|
+
this.frame = null;
|
|
644
|
+
this.unknownFrames = [];
|
|
645
|
+
this.framesSinceCheck = 0;
|
|
646
|
+
this.significant = false;
|
|
647
|
+
this.report = report;
|
|
648
|
+
this.signal = signal;
|
|
649
|
+
this.typedefsAsNodes = typedefsAsNodes;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Read one logical line.
|
|
653
|
+
* @param text - the line, continuation joined
|
|
654
|
+
* @param line - its 1-based number
|
|
655
|
+
*/
|
|
656
|
+
line(text, line) {
|
|
657
|
+
const t = text.trim();
|
|
658
|
+
if (t.length === 0) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this.significant = true;
|
|
662
|
+
if (t.startsWith("!")) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (t.startsWith("[") && this.frameHeader(t, line)) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const tv = splitTagValue(stripComment(t));
|
|
669
|
+
if (tv === null || tv.tag.length === 0) {
|
|
670
|
+
this.firstBadLine ??= line;
|
|
671
|
+
const what = tv === null ? "a line without a colon was skipped" : "a line without a tag was skipped";
|
|
672
|
+
this.tally("parse-error", OBO_ISSUE.SYNTAX, what, "line", line);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const clause = { tag: tv.tag, value: stripComment(tv.rest), line };
|
|
676
|
+
if (this.frame === null) {
|
|
677
|
+
if (isTagName(tv.tag)) {
|
|
678
|
+
this.tagClauses++;
|
|
679
|
+
} else {
|
|
680
|
+
this.firstBadLine ??= line;
|
|
681
|
+
}
|
|
682
|
+
this.headerClause(clause);
|
|
683
|
+
} else {
|
|
684
|
+
this.frame.clauses.push(clause);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Start a frame at a line opening with `[`: a well-formed header, or one damaged so that only a
|
|
689
|
+
* frame header can be meant (a known frame name, or no colon: `[Term`, `[Term] extra`), with a
|
|
690
|
+
* warning, so the clauses after it never merge into the frame before.
|
|
691
|
+
* @param t - the trimmed line
|
|
692
|
+
* @param line - its number
|
|
693
|
+
* @returns false when the line is not a frame header
|
|
694
|
+
*/
|
|
695
|
+
frameHeader(t, line) {
|
|
696
|
+
const close = t.indexOf("]");
|
|
697
|
+
const name = (close < 0 ? t.slice(1) : t.slice(1, close)).trim();
|
|
698
|
+
const after = close < 0 ? "" : t.slice(close + 1).trim();
|
|
699
|
+
const clean = close >= 0 && (after.length === 0 || after.startsWith("!"));
|
|
700
|
+
if (!clean && !FRAME_KINDS.has(name) && t.includes(":")) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
if (!clean) {
|
|
704
|
+
const what = close < 0 ? "a frame header without its closing ] starts the frame" : "text after a frame header was ignored";
|
|
705
|
+
this.tally("parse-error", OBO_ISSUE.SYNTAX, what, `[${name}]`, line);
|
|
706
|
+
}
|
|
707
|
+
const proof = FRAME_KINDS.has(name) || clean && /^[A-Za-z]/.test(name) && isTagName(name);
|
|
708
|
+
if (!proof) {
|
|
709
|
+
this.firstBadLine ??= line;
|
|
710
|
+
}
|
|
711
|
+
this.startFrame(name, line, proof);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Start a frame, finishing the one before.
|
|
716
|
+
* @param name - the frame type
|
|
717
|
+
* @param line - the header's line
|
|
718
|
+
* @param proof - whether the header shows the input is OBO
|
|
719
|
+
*/
|
|
720
|
+
startFrame(name, line, proof) {
|
|
721
|
+
this.finishFrame();
|
|
722
|
+
this.frameSeen ||= proof;
|
|
723
|
+
const kind = FRAME_KINDS.has(name) ? name : null;
|
|
724
|
+
if (kind === null) {
|
|
725
|
+
this.tally(
|
|
726
|
+
"validation-error",
|
|
727
|
+
OBO_ISSUE.UNKNOWN_ELEMENT,
|
|
728
|
+
"frames of an unknown type are not nodes; kept in meta.extra.obo.unknownFrames",
|
|
729
|
+
`[${name}]`,
|
|
730
|
+
line
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
this.frame = { kind, name, line, clauses: [] };
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Record one header clause.
|
|
737
|
+
* @param clause - the clause
|
|
738
|
+
*/
|
|
739
|
+
headerClause(clause) {
|
|
740
|
+
append(this.header, clause.tag, clause.value);
|
|
741
|
+
const { tag } = clause;
|
|
742
|
+
if (SINGLE_HEADER_TAGS.has(tag) && this.header[tag].length > 1) {
|
|
743
|
+
this.tally(
|
|
744
|
+
"validation-error",
|
|
745
|
+
OBO_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
746
|
+
"a single-valued header tag given twice; the first is kept",
|
|
747
|
+
tag,
|
|
748
|
+
clause.line
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const tokens = tokenize(splitQualifiers(clause.value).value);
|
|
752
|
+
const first = tokens.length > 0 && tokens[0].kind === "word" ? tokens[0].text : null;
|
|
753
|
+
if (tag === "format-version") {
|
|
754
|
+
this.version ??= unescapeObo(clause.value).trim();
|
|
755
|
+
} else if (tag === "default-namespace") {
|
|
756
|
+
this.defaultNamespace ??= first;
|
|
757
|
+
} else if ((tag === "subsetdef" || tag === "synonymtypedef") && first === null) {
|
|
758
|
+
this.tally(
|
|
759
|
+
"parse-error",
|
|
760
|
+
OBO_ISSUE.SYNTAX,
|
|
761
|
+
"a declaration whose name is not a word declares nothing",
|
|
762
|
+
tag,
|
|
763
|
+
clause.line
|
|
764
|
+
);
|
|
765
|
+
} else if (tag === "subsetdef" && first !== null) {
|
|
766
|
+
this.subsets.add(first);
|
|
767
|
+
} else if (tag === "synonymtypedef" && first !== null) {
|
|
768
|
+
const scope = tokens.slice(1).find((t) => t.kind === "word" && SYNONYM_SCOPES.has(t.text));
|
|
769
|
+
this.synonymTypes.set(first, scope?.text ?? null);
|
|
770
|
+
}
|
|
771
|
+
const now = DEPRECATED_HEADER.get(tag);
|
|
772
|
+
if (now !== void 0) {
|
|
773
|
+
this.tally("coercion", OBO_ISSUE.DEPRECATED_TAG, `the 1.0 header tag is read as ${now}`, tag, clause.line);
|
|
774
|
+
}
|
|
775
|
+
if (NOT_APPLIED_HEADER.test(tag)) {
|
|
776
|
+
this.tally(
|
|
777
|
+
"unsupported",
|
|
778
|
+
OBO_ISSUE.HEADER_NOT_APPLIED,
|
|
779
|
+
"kept in meta.extra.obo.header, not applied",
|
|
780
|
+
tag,
|
|
781
|
+
clause.line
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/** Finish the frame being read: find its id and merge its clauses into the record of that id. */
|
|
786
|
+
finishFrame() {
|
|
787
|
+
const { frame } = this;
|
|
788
|
+
this.frame = null;
|
|
789
|
+
if (frame === null) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (frame.kind === null) {
|
|
793
|
+
const clauses = ownRecord();
|
|
794
|
+
for (const clause of frame.clauses) {
|
|
795
|
+
append(clauses, clause.tag, clause.value);
|
|
796
|
+
}
|
|
797
|
+
this.unknownFrames.push({ type: frame.name, clauses });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (++this.framesSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
801
|
+
this.framesSinceCheck = 0;
|
|
802
|
+
throwIfAborted(this.signal);
|
|
803
|
+
}
|
|
804
|
+
const idAt = frame.clauses.findIndex((c) => c.tag === "id");
|
|
805
|
+
const idParts = idAt < 0 ? null : splitQualifiers(frame.clauses[idAt].value);
|
|
806
|
+
const rawId = idParts === null ? "" : idParts.value.trim();
|
|
807
|
+
const id = unescapeObo(rawId).trim();
|
|
808
|
+
const wouldBeNode = frame.kind !== "Typedef" || this.typedefsAsNodes;
|
|
809
|
+
if (id.length === 0) {
|
|
810
|
+
this.report.error(
|
|
811
|
+
"missing-value",
|
|
812
|
+
OBO_ISSUE.MISSING_ID,
|
|
813
|
+
`a [${frame.kind}] frame has no id; it is skipped`,
|
|
814
|
+
{
|
|
815
|
+
line: frame.line
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
if (wouldBeNode) {
|
|
819
|
+
this.report.counts.skippedNodes++;
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (tokenize(rawId).length > 1 || hasControl(id)) {
|
|
824
|
+
this.report.error(
|
|
825
|
+
"validation-error",
|
|
826
|
+
OBO_ISSUE.BAD_VALUE,
|
|
827
|
+
`a [${frame.kind}] frame's id ${JSON.stringify(id)} holds unescaped whitespace or a control character; the frame is skipped`,
|
|
828
|
+
{ line: frame.clauses[idAt].line, element: id }
|
|
829
|
+
);
|
|
830
|
+
if (wouldBeNode) {
|
|
831
|
+
this.report.counts.skippedNodes++;
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (idAt > 0) {
|
|
836
|
+
this.tally(
|
|
837
|
+
"validation-error",
|
|
838
|
+
OBO_ISSUE.ID_NOT_FIRST,
|
|
839
|
+
"the id clause is not the frame's first; it is used",
|
|
840
|
+
"id",
|
|
841
|
+
frame.clauses[idAt].line
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const target = this.recordFor(frame.kind, id, frame.line);
|
|
845
|
+
this.extraQualifiers(target, "id", id, idParts?.qualifiers ?? null);
|
|
846
|
+
for (let i = 0; i < frame.clauses.length; i++) {
|
|
847
|
+
const clause = frame.clauses[i];
|
|
848
|
+
if (clause.tag === "id") {
|
|
849
|
+
if (i !== idAt) {
|
|
850
|
+
this.tally(
|
|
851
|
+
"validation-error",
|
|
852
|
+
OBO_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
853
|
+
"a frame names a second id; the first is kept",
|
|
854
|
+
"id",
|
|
855
|
+
clause.line
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
this.applyClause(target, clause);
|
|
861
|
+
}
|
|
862
|
+
if (target.kind === "Typedef" && !Object.hasOwn(target.raw, "id")) {
|
|
863
|
+
target.raw.id = [id];
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* The record a frame merges into: the existing one of its id, or a new one.
|
|
868
|
+
* @param kind - the frame type
|
|
869
|
+
* @param id - the id
|
|
870
|
+
* @param line - the frame's line
|
|
871
|
+
* @returns the record
|
|
872
|
+
*/
|
|
873
|
+
recordFor(kind, id, line) {
|
|
874
|
+
const map = kind === "Typedef" ? this.typedefs : this.nodes;
|
|
875
|
+
const existing = map.get(id);
|
|
876
|
+
if (existing !== void 0) {
|
|
877
|
+
if (existing.kind !== kind) {
|
|
878
|
+
this.tally(
|
|
879
|
+
"validation-error",
|
|
880
|
+
OBO_ISSUE.ID_KIND_CLASH,
|
|
881
|
+
`an id names both a ${existing.kind} and an ${kind}; the ${existing.kind} is the node`,
|
|
882
|
+
id,
|
|
883
|
+
line
|
|
884
|
+
);
|
|
885
|
+
} else {
|
|
886
|
+
this.tally(
|
|
887
|
+
"merged",
|
|
888
|
+
OBO_ISSUE.DUPLICATE_NODE,
|
|
889
|
+
`[${kind}] frames with this id are merged (spec 4.1.1)`,
|
|
890
|
+
id,
|
|
891
|
+
line
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
return existing;
|
|
895
|
+
}
|
|
896
|
+
const created = {
|
|
897
|
+
id,
|
|
898
|
+
kind,
|
|
899
|
+
line,
|
|
900
|
+
values: /* @__PURE__ */ new Map(),
|
|
901
|
+
lists: /* @__PURE__ */ new Map(),
|
|
902
|
+
edges: [],
|
|
903
|
+
seen: /* @__PURE__ */ new Set(),
|
|
904
|
+
raw: ownRecord()
|
|
905
|
+
};
|
|
906
|
+
map.set(id, created);
|
|
907
|
+
return created;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* A clause's tag (a 1.0 tag read as its current one), the synonym scope that tag implies, its
|
|
911
|
+
* qualifier block and its value, with the syntax warnings about stray braces recorded.
|
|
912
|
+
* @param clause - the clause
|
|
913
|
+
* @returns the parts
|
|
914
|
+
*/
|
|
915
|
+
clauseParts(clause) {
|
|
916
|
+
let { tag } = clause;
|
|
917
|
+
let scope = null;
|
|
918
|
+
const deprecated = DEPRECATED_TAGS.get(tag);
|
|
919
|
+
if (deprecated !== void 0) {
|
|
920
|
+
this.tally(
|
|
921
|
+
"coercion",
|
|
922
|
+
OBO_ISSUE.DEPRECATED_TAG,
|
|
923
|
+
`the 1.0 tag is read as ${deprecated[0]}`,
|
|
924
|
+
tag,
|
|
925
|
+
clause.line
|
|
926
|
+
);
|
|
927
|
+
[tag, scope] = deprecated;
|
|
928
|
+
}
|
|
929
|
+
let split = splitQualifiers(clause.value);
|
|
930
|
+
const open = ID_VALUE_TAGS.has(tag) ? unclosedBlock(split.value) : -1;
|
|
931
|
+
if (open >= 0) {
|
|
932
|
+
this.tally(
|
|
933
|
+
"parse-error",
|
|
934
|
+
OBO_ISSUE.SYNTAX,
|
|
935
|
+
"a qualifier block without its closing brace was dropped",
|
|
936
|
+
tag,
|
|
937
|
+
clause.line
|
|
938
|
+
);
|
|
939
|
+
split = splitQualifiers(split.value.slice(0, open).trimEnd());
|
|
940
|
+
}
|
|
941
|
+
const { qualifiers } = split;
|
|
942
|
+
if (split.badBlock !== null) {
|
|
943
|
+
this.tally(
|
|
944
|
+
"parse-error",
|
|
945
|
+
OBO_ISSUE.SYNTAX,
|
|
946
|
+
"a closing brace that is not a qualifier block was kept as text",
|
|
947
|
+
tag,
|
|
948
|
+
clause.line
|
|
949
|
+
);
|
|
950
|
+
} else if (hasStrayBrace(split.value)) {
|
|
951
|
+
this.tally(
|
|
952
|
+
"parse-error",
|
|
953
|
+
OBO_ISSUE.SYNTAX,
|
|
954
|
+
"an unescaped brace inside a value was kept as text",
|
|
955
|
+
tag,
|
|
956
|
+
clause.line
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return { tag, scope, qualifiers, value: split.value };
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* A def, expand_assertion_to or expand_expression_to clause: quoted text and its xref list.
|
|
963
|
+
* @param record - the record
|
|
964
|
+
* @param tag - the tag
|
|
965
|
+
* @param value - the clause value
|
|
966
|
+
* @param tokens - its tokens
|
|
967
|
+
* @param qualifiers - its qualifier block
|
|
968
|
+
* @param line - its line
|
|
969
|
+
*/
|
|
970
|
+
definition(record, tag, value, tokens, qualifiers, line) {
|
|
971
|
+
if (tag === "def" && record.values.has("def")) {
|
|
972
|
+
this.single(record, "def", null, line);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const text = this.quotedWithXrefs(record, tag, value, tokens, line);
|
|
976
|
+
if (tag === "def") {
|
|
977
|
+
this.single(record, "def", text.text, line, () => {
|
|
978
|
+
record.values.set("def.xrefs", text.xrefs);
|
|
979
|
+
});
|
|
980
|
+
this.extraQualifiers(record, tag, text.text, qualifiers);
|
|
981
|
+
} else {
|
|
982
|
+
this.push(record, tag, withQualifiers({ template: text.text, xrefs: text.xrefs }, qualifiers));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Apply one clause to a record.
|
|
987
|
+
* @param record - the record
|
|
988
|
+
* @param clause - the clause
|
|
989
|
+
*/
|
|
990
|
+
applyClause(record, clause) {
|
|
991
|
+
const key = `${clause.tag}\0${clause.value}`;
|
|
992
|
+
if (record.seen.has(key)) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
record.seen.add(key);
|
|
996
|
+
if (record.kind === "Typedef") {
|
|
997
|
+
append(record.raw, clause.tag, clause.value);
|
|
998
|
+
}
|
|
999
|
+
const { tag, scope, qualifiers, value } = this.clauseParts(clause);
|
|
1000
|
+
const where = { line: clause.line, element: record.id };
|
|
1001
|
+
if (TYPEDEF_TAGS.has(tag) && record.kind !== "Typedef") {
|
|
1002
|
+
this.unrecognized(record, clause);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (TEXT_TAGS.has(tag)) {
|
|
1006
|
+
this.text(record, tag, value, qualifiers, clause.line);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (BOOL_TAGS.has(tag)) {
|
|
1010
|
+
const text = unescapeObo(value).trim();
|
|
1011
|
+
if (text !== "true" && text !== "false") {
|
|
1012
|
+
this.report.error(
|
|
1013
|
+
"validation-error",
|
|
1014
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1015
|
+
`${tag}: ${JSON.stringify(text)} is not true or false; the clause is skipped`,
|
|
1016
|
+
where
|
|
1017
|
+
);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
this.single(record, tag, text === "true", clause.line);
|
|
1021
|
+
this.extraQualifiers(record, tag, text, qualifiers);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const tokens = tokenize(value);
|
|
1025
|
+
if (ID_LIST_TAGS.has(tag)) {
|
|
1026
|
+
const id = this.oneWord(tokens, tag, where);
|
|
1027
|
+
if (id !== null) {
|
|
1028
|
+
this.push(record, tag, id);
|
|
1029
|
+
this.extraQualifiers(record, tag, id, qualifiers);
|
|
1030
|
+
if (tag === "subset" && !this.subsets.has(id)) {
|
|
1031
|
+
this.tally(
|
|
1032
|
+
"validation-error",
|
|
1033
|
+
OBO_ISSUE.UNDECLARED,
|
|
1034
|
+
"a subset no subsetdef declares",
|
|
1035
|
+
id,
|
|
1036
|
+
clause.line
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
switch (tag) {
|
|
1043
|
+
case "is_a":
|
|
1044
|
+
case "instance_of": {
|
|
1045
|
+
if (tag === "instance_of" && record.kind !== "Instance") {
|
|
1046
|
+
this.unrecognized(record, clause);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const target = this.oneWord(tokens, tag, where);
|
|
1050
|
+
if (target !== null) {
|
|
1051
|
+
record.edges.push({ relation: tag, target, qualifiers, line: clause.line });
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
case "relationship": {
|
|
1056
|
+
if (tokens.length !== 2 || tokens.some((t) => t.kind !== "word")) {
|
|
1057
|
+
this.report.error(
|
|
1058
|
+
"validation-error",
|
|
1059
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1060
|
+
`relationship needs a relation and a target, found ${JSON.stringify(unescapeObo(value))}; the clause is skipped`,
|
|
1061
|
+
where
|
|
1062
|
+
);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
record.edges.push({ relation: tokens[0].text, target: tokens[1].text, qualifiers, line: clause.line });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
case "intersection_of": {
|
|
1069
|
+
if (tokens.length < 1 || tokens.length > 2 || tokens.some((t) => t.kind !== "word")) {
|
|
1070
|
+
this.report.error(
|
|
1071
|
+
"validation-error",
|
|
1072
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1073
|
+
`intersection_of needs a class, or a relation and a class, found ${JSON.stringify(unescapeObo(value))}; the clause is skipped`,
|
|
1074
|
+
where
|
|
1075
|
+
);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const item = tokens.length === 1 ? { relation: null, target: tokens[0].text } : { relation: tokens[0].text, target: tokens[1].text };
|
|
1079
|
+
this.push(record, tag, withQualifiers(item, qualifiers));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
case "holds_over_chain":
|
|
1083
|
+
case "equivalent_to_chain": {
|
|
1084
|
+
if (tokens.length !== 2 || tokens.some((t) => t.kind !== "word")) {
|
|
1085
|
+
this.report.error(
|
|
1086
|
+
"validation-error",
|
|
1087
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1088
|
+
`${tag} needs two relations; the clause is skipped`,
|
|
1089
|
+
where
|
|
1090
|
+
);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
this.push(record, tag, [tokens[0].text, tokens[1].text]);
|
|
1094
|
+
this.extraQualifiers(record, tag, `${tokens[0].text} ${tokens[1].text}`, qualifiers);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
case "def":
|
|
1098
|
+
case "expand_assertion_to":
|
|
1099
|
+
case "expand_expression_to":
|
|
1100
|
+
this.definition(record, tag, value, tokens, qualifiers, clause.line);
|
|
1101
|
+
return;
|
|
1102
|
+
case "synonym":
|
|
1103
|
+
this.synonym(record, tokens, scope, qualifiers, clause.line);
|
|
1104
|
+
return;
|
|
1105
|
+
case "xref": {
|
|
1106
|
+
const xref = parseXref(value);
|
|
1107
|
+
if (xref === null) {
|
|
1108
|
+
this.report.error(
|
|
1109
|
+
"validation-error",
|
|
1110
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1111
|
+
"xref has no id; the clause is skipped",
|
|
1112
|
+
where
|
|
1113
|
+
);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
this.push(record, "xref", xref.id);
|
|
1117
|
+
this.describe(record, [xref], tag, clause.line);
|
|
1118
|
+
this.extraQualifiers(record, tag, xref.id, qualifiers ?? xref.qualifiers);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
case "property_value":
|
|
1122
|
+
this.propertyValue(record, tokens, qualifiers, where);
|
|
1123
|
+
return;
|
|
1124
|
+
default:
|
|
1125
|
+
this.unrecognized(record, clause);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* A single-valued text clause (name, comment, ...), warning about a quote that never closes
|
|
1130
|
+
* (stripComment() then keeps a `!` comment after it as text).
|
|
1131
|
+
* @param record - the record
|
|
1132
|
+
* @param tag - the tag
|
|
1133
|
+
* @param value - the raw value
|
|
1134
|
+
* @param qualifiers - its qualifier block
|
|
1135
|
+
* @param line - its line
|
|
1136
|
+
*/
|
|
1137
|
+
text(record, tag, value, qualifiers, line) {
|
|
1138
|
+
if (tokenize(value).some((t) => t.kind === "quoted" && t.unterminated)) {
|
|
1139
|
+
this.tally(
|
|
1140
|
+
"parse-error",
|
|
1141
|
+
OBO_ISSUE.SYNTAX,
|
|
1142
|
+
"an unterminated quote; the text after it, a comment included, is kept",
|
|
1143
|
+
tag,
|
|
1144
|
+
line
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const text = unescapeObo(value).trim();
|
|
1148
|
+
this.single(record, tag, text, line);
|
|
1149
|
+
this.extraQualifiers(record, tag, text, qualifiers);
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Read a value that must be one word.
|
|
1153
|
+
* @param tokens - the value's tokens
|
|
1154
|
+
* @param tag - the tag, for the message
|
|
1155
|
+
* @param where - the location
|
|
1156
|
+
* @returns the word, or null after reporting E_BAD_VALUE
|
|
1157
|
+
*/
|
|
1158
|
+
oneWord(tokens, tag, where) {
|
|
1159
|
+
if (tokens.length === 1 && tokens[0].kind === "word") {
|
|
1160
|
+
return tokens[0].text;
|
|
1161
|
+
}
|
|
1162
|
+
const found = tokens.map((t) => t.text).join(" ");
|
|
1163
|
+
this.report.error(
|
|
1164
|
+
"validation-error",
|
|
1165
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1166
|
+
`${tag} needs one id, found ${JSON.stringify(found)}; the clause is skipped`,
|
|
1167
|
+
where
|
|
1168
|
+
);
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Read `"text" [xrefs]` (def, expand_*), reporting the forms the grammar does not allow.
|
|
1173
|
+
* @param record - the record (xref descriptions go to its `xref.descriptions`)
|
|
1174
|
+
* @param tag - the tag
|
|
1175
|
+
* @param value - the raw value
|
|
1176
|
+
* @param tokens - its tokens
|
|
1177
|
+
* @param line - the line
|
|
1178
|
+
* @returns the text and the xref ids
|
|
1179
|
+
*/
|
|
1180
|
+
quotedWithXrefs(record, tag, value, tokens, line) {
|
|
1181
|
+
if (tokens.length === 0 || tokens[0].kind !== "quoted") {
|
|
1182
|
+
this.tally("parse-error", OBO_ISSUE.SYNTAX, "the text is not quoted; the whole value is kept", tag, line);
|
|
1183
|
+
return { text: unescapeObo(value).trim(), xrefs: [] };
|
|
1184
|
+
}
|
|
1185
|
+
if (tokens[0].unterminated) {
|
|
1186
|
+
this.tally(
|
|
1187
|
+
"parse-error",
|
|
1188
|
+
OBO_ISSUE.SYNTAX,
|
|
1189
|
+
"an unterminated quote; the text runs to the end of the line",
|
|
1190
|
+
tag,
|
|
1191
|
+
line
|
|
1192
|
+
);
|
|
1193
|
+
return { text: tokens[0].text, xrefs: [] };
|
|
1194
|
+
}
|
|
1195
|
+
const list = tokens[1];
|
|
1196
|
+
if (list?.kind !== "list") {
|
|
1197
|
+
this.tally("parse-error", OBO_ISSUE.SYNTAX, "the xref list is missing", tag, line);
|
|
1198
|
+
return { text: tokens[0].text, xrefs: [] };
|
|
1199
|
+
}
|
|
1200
|
+
if (list.unterminated) {
|
|
1201
|
+
this.tally("parse-error", OBO_ISSUE.SYNTAX, "an xref list without its closing ]; kept as read", tag, line);
|
|
1202
|
+
}
|
|
1203
|
+
if (tokens.length > 2) {
|
|
1204
|
+
this.tally("parse-error", OBO_ISSUE.SYNTAX, "text after the xref list was ignored", tag, line);
|
|
1205
|
+
}
|
|
1206
|
+
const xrefs = parseXrefList(list.text);
|
|
1207
|
+
this.describe(record, xrefs, tag, line);
|
|
1208
|
+
this.xrefQualifiers(record, tag, xrefs);
|
|
1209
|
+
return { text: tokens[0].text, xrefs: xrefs.map((x) => x.id) };
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Read a synonym: `"text" SCOPE? TYPE? [xrefs]` (the scope optional in 1.2).
|
|
1213
|
+
* @param record - the record
|
|
1214
|
+
* @param tokens - the value's tokens
|
|
1215
|
+
* @param fixedScope - the scope of a 1.0 tag (exact_synonym), or null
|
|
1216
|
+
* @param qualifiers - the clause's qualifiers
|
|
1217
|
+
* @param line - the line
|
|
1218
|
+
*/
|
|
1219
|
+
synonym(record, tokens, fixedScope, qualifiers, line) {
|
|
1220
|
+
if (tokens.length === 0 || tokens[0].kind !== "quoted") {
|
|
1221
|
+
this.report.error(
|
|
1222
|
+
"validation-error",
|
|
1223
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1224
|
+
"a synonym needs its quoted text; the clause is skipped",
|
|
1225
|
+
{
|
|
1226
|
+
line,
|
|
1227
|
+
element: record.id
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (tokens[0].unterminated) {
|
|
1233
|
+
this.tally(
|
|
1234
|
+
"parse-error",
|
|
1235
|
+
OBO_ISSUE.SYNTAX,
|
|
1236
|
+
"an unterminated quote; the text runs to the end of the line",
|
|
1237
|
+
"synonym",
|
|
1238
|
+
line
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const words = tokens.slice(1).filter((t) => t.kind === "word").map((t) => t.text);
|
|
1242
|
+
const list = tokens.slice(1).find((t) => t.kind === "list");
|
|
1243
|
+
let scope = fixedScope;
|
|
1244
|
+
let type = null;
|
|
1245
|
+
if (fixedScope === null) {
|
|
1246
|
+
if (words.length > 0 && SYNONYM_SCOPES.has(words[0])) {
|
|
1247
|
+
[scope] = words;
|
|
1248
|
+
type = words[1] ?? null;
|
|
1249
|
+
} else if (words.length > 0 && this.synonymTypes.has(words[0])) {
|
|
1250
|
+
[type] = words;
|
|
1251
|
+
scope = "RELATED";
|
|
1252
|
+
} else if (words.length > 0) {
|
|
1253
|
+
[type] = words;
|
|
1254
|
+
this.tally(
|
|
1255
|
+
"validation-error",
|
|
1256
|
+
OBO_ISSUE.SYNONYM_SCOPE,
|
|
1257
|
+
`a scope that is not EXACT, BROAD, NARROW or RELATED was kept as the type, scope null`,
|
|
1258
|
+
words[0],
|
|
1259
|
+
line
|
|
1260
|
+
);
|
|
1261
|
+
} else {
|
|
1262
|
+
scope = "RELATED";
|
|
1263
|
+
}
|
|
1264
|
+
if (scope === "RELATED" && (words.length === 0 || words[0] === type) && !this.allowsMissingScope()) {
|
|
1265
|
+
this.tally(
|
|
1266
|
+
"validation-error",
|
|
1267
|
+
OBO_ISSUE.SYNONYM_SCOPE,
|
|
1268
|
+
"a synonym without a scope (1.2 only) is read as RELATED",
|
|
1269
|
+
"synonym",
|
|
1270
|
+
line
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (type !== null) {
|
|
1275
|
+
const declared = this.synonymTypes.get(type);
|
|
1276
|
+
if (declared === void 0) {
|
|
1277
|
+
if (scope !== null) {
|
|
1278
|
+
this.tally(
|
|
1279
|
+
"validation-error",
|
|
1280
|
+
OBO_ISSUE.UNDECLARED,
|
|
1281
|
+
"a synonym type no synonymtypedef declares",
|
|
1282
|
+
type,
|
|
1283
|
+
line
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
} else if (declared !== null) {
|
|
1287
|
+
scope = declared;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (words.length > 2 || list === void 0) {
|
|
1291
|
+
this.tally(
|
|
1292
|
+
"parse-error",
|
|
1293
|
+
OBO_ISSUE.SYNTAX,
|
|
1294
|
+
list === void 0 ? "a synonym without its xref list" : "extra words after the synonym type",
|
|
1295
|
+
"synonym",
|
|
1296
|
+
line
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
const listAt = list === void 0 ? -1 : tokens.indexOf(list);
|
|
1300
|
+
const stray = tokens.some(
|
|
1301
|
+
(t, k) => k > 0 && (t.kind === "quoted" || t.kind === "list" && k !== listAt || listAt >= 0 && k > listAt)
|
|
1302
|
+
);
|
|
1303
|
+
if (stray) {
|
|
1304
|
+
this.tally(
|
|
1305
|
+
"parse-error",
|
|
1306
|
+
OBO_ISSUE.SYNTAX,
|
|
1307
|
+
"a second quoted string, a second xref list or text after the list was ignored",
|
|
1308
|
+
"synonym",
|
|
1309
|
+
line
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
if (list?.unterminated === true) {
|
|
1313
|
+
this.tally(
|
|
1314
|
+
"parse-error",
|
|
1315
|
+
OBO_ISSUE.SYNTAX,
|
|
1316
|
+
"an xref list without its closing ]; kept as read",
|
|
1317
|
+
"synonym",
|
|
1318
|
+
line
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const xrefs = list === void 0 ? [] : parseXrefList(list.text);
|
|
1322
|
+
this.describe(record, xrefs, "synonym", line);
|
|
1323
|
+
this.xrefQualifiers(record, "synonym", xrefs);
|
|
1324
|
+
this.push(
|
|
1325
|
+
record,
|
|
1326
|
+
"synonym",
|
|
1327
|
+
withQualifiers({ text: tokens[0].text, scope, type, xrefs: xrefs.map((x) => x.id) }, qualifiers)
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Whether the file's version lets a synonym omit its scope (1.0 and 1.2).
|
|
1332
|
+
* @returns true for a 1.2 or 1.0 file
|
|
1333
|
+
*/
|
|
1334
|
+
allowsMissingScope() {
|
|
1335
|
+
return this.version !== null && /^(1\.[0-2]|GO_1\.[0-2])\b/.test(this.version);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Read a property_value: `relation "literal" datatype` or `relation id`.
|
|
1339
|
+
* @param record - the record
|
|
1340
|
+
* @param tokens - the value's tokens
|
|
1341
|
+
* @param qualifiers - the clause's qualifiers
|
|
1342
|
+
* @param where - the location
|
|
1343
|
+
*/
|
|
1344
|
+
propertyValue(record, tokens, qualifiers, where) {
|
|
1345
|
+
if (tokens.length < 2 || tokens.length > 3 || tokens[0].kind !== "word" || tokens[1].kind === "list") {
|
|
1346
|
+
this.report.error(
|
|
1347
|
+
"validation-error",
|
|
1348
|
+
OBO_ISSUE.BAD_VALUE,
|
|
1349
|
+
"property_value needs a relation and a value; the clause is skipped",
|
|
1350
|
+
where
|
|
1351
|
+
);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (tokens.some((t) => t.unterminated)) {
|
|
1355
|
+
this.tally(
|
|
1356
|
+
"parse-error",
|
|
1357
|
+
OBO_ISSUE.SYNTAX,
|
|
1358
|
+
"an unterminated quote; the text runs to the end of the line",
|
|
1359
|
+
"property_value",
|
|
1360
|
+
where.line
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
let datatype = tokens.length === 3 ? tokens[2].text : null;
|
|
1364
|
+
if (tokens.length === 3 && tokens[2].kind !== "word") {
|
|
1365
|
+
this.tally(
|
|
1366
|
+
"parse-error",
|
|
1367
|
+
OBO_ISSUE.SYNTAX,
|
|
1368
|
+
"a datatype that is not a word was ignored",
|
|
1369
|
+
"property_value",
|
|
1370
|
+
where.line
|
|
1371
|
+
);
|
|
1372
|
+
datatype = null;
|
|
1373
|
+
}
|
|
1374
|
+
this.push(
|
|
1375
|
+
record,
|
|
1376
|
+
"property_value",
|
|
1377
|
+
withQualifiers({ relation: tokens[0].text, value: tokens[1].text, datatype }, qualifiers)
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Set a single-valued column, keeping the first value of an id.
|
|
1382
|
+
* @param record - the record
|
|
1383
|
+
* @param column - the column
|
|
1384
|
+
* @param value - the value
|
|
1385
|
+
* @param line - the line
|
|
1386
|
+
* @param also - more to set together with the value
|
|
1387
|
+
*/
|
|
1388
|
+
single(record, column, value, line, also) {
|
|
1389
|
+
if (record.values.has(column)) {
|
|
1390
|
+
this.tally(
|
|
1391
|
+
"validation-error",
|
|
1392
|
+
OBO_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
1393
|
+
"a single-valued tag given twice for one id; the first is kept",
|
|
1394
|
+
column,
|
|
1395
|
+
line
|
|
1396
|
+
);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
record.values.set(column, value);
|
|
1400
|
+
also?.();
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Append to a list column, without repeats (merged frames take the union).
|
|
1404
|
+
* @param record - the record
|
|
1405
|
+
* @param column - the column
|
|
1406
|
+
* @param value - the item
|
|
1407
|
+
*/
|
|
1408
|
+
push(record, column, value) {
|
|
1409
|
+
let list = record.lists.get(column);
|
|
1410
|
+
if (list === void 0) {
|
|
1411
|
+
list = [];
|
|
1412
|
+
record.lists.set(column, list);
|
|
1413
|
+
}
|
|
1414
|
+
if (typeof value === "object" || !list.includes(value)) {
|
|
1415
|
+
list.push(value);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Record xref descriptions in `xref.descriptions` (the first description of an id is kept),
|
|
1420
|
+
* warning about a description whose quote never closes.
|
|
1421
|
+
* @param target - the record
|
|
1422
|
+
* @param xrefs - the xrefs
|
|
1423
|
+
* @param tag - the clause's tag
|
|
1424
|
+
* @param line - the clause's line
|
|
1425
|
+
*/
|
|
1426
|
+
describe(target, xrefs, tag, line) {
|
|
1427
|
+
for (const xref of xrefs) {
|
|
1428
|
+
if (xref.unterminated) {
|
|
1429
|
+
this.tally(
|
|
1430
|
+
"parse-error",
|
|
1431
|
+
OBO_ISSUE.SYNTAX,
|
|
1432
|
+
"an xref description whose quote never closes runs to the end",
|
|
1433
|
+
tag,
|
|
1434
|
+
line
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
if (xref.description !== null) {
|
|
1438
|
+
let map = target.values.get("xref.descriptions");
|
|
1439
|
+
if (map === void 0) {
|
|
1440
|
+
map = ownRecord();
|
|
1441
|
+
target.values.set("xref.descriptions", map);
|
|
1442
|
+
}
|
|
1443
|
+
if (!Object.hasOwn(map, xref.id)) {
|
|
1444
|
+
map[xref.id] = xref.description;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Keep the per-xref qualifiers of an xref list (OBO 1.2: `[A:1 {source="x"}]`) in
|
|
1451
|
+
* `obo.qualifiers` under `<tag>.xrefs`.
|
|
1452
|
+
* @param record - the record
|
|
1453
|
+
* @param tag - the clause's tag
|
|
1454
|
+
* @param xrefs - the list's xrefs
|
|
1455
|
+
*/
|
|
1456
|
+
xrefQualifiers(record, tag, xrefs) {
|
|
1457
|
+
for (const xref of xrefs) {
|
|
1458
|
+
this.extraQualifiers(record, `${tag}.xrefs`, xref.id, xref.qualifiers);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Keep the qualifiers of a clause whose column has no place for them in `obo.qualifiers`.
|
|
1463
|
+
* @param record - the record
|
|
1464
|
+
* @param tag - the tag
|
|
1465
|
+
* @param value - the clause's value as stored
|
|
1466
|
+
* @param qualifiers - the qualifiers, or null
|
|
1467
|
+
*/
|
|
1468
|
+
extraQualifiers(record, tag, value, qualifiers) {
|
|
1469
|
+
if (qualifiers === null) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const map = record.values.get("obo.qualifiers") ?? {};
|
|
1473
|
+
(map[tag] ??= []).push({ value, qualifiers });
|
|
1474
|
+
record.values.set("obo.qualifiers", map);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Keep an unknown clause in `obo.unrecognized`.
|
|
1478
|
+
* @param record - the record
|
|
1479
|
+
* @param clause - the clause
|
|
1480
|
+
*/
|
|
1481
|
+
unrecognized(record, clause) {
|
|
1482
|
+
let map = record.values.get("obo.unrecognized");
|
|
1483
|
+
if (map === void 0) {
|
|
1484
|
+
map = ownRecord();
|
|
1485
|
+
record.values.set("obo.unrecognized", map);
|
|
1486
|
+
}
|
|
1487
|
+
append(map, clause.tag, unescapeObo(clause.value));
|
|
1488
|
+
this.tally(
|
|
1489
|
+
"validation-error",
|
|
1490
|
+
OBO_ISSUE.UNKNOWN_ELEMENT,
|
|
1491
|
+
"an unknown tag was kept in obo.unrecognized",
|
|
1492
|
+
clause.tag,
|
|
1493
|
+
clause.line
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Count a warning, recorded once per code and element at the end.
|
|
1498
|
+
* @param category - the category
|
|
1499
|
+
* @param code - the code
|
|
1500
|
+
* @param what - what happened
|
|
1501
|
+
* @param element - the tag, id or name it is about
|
|
1502
|
+
* @param line - the first line
|
|
1503
|
+
*/
|
|
1504
|
+
tally(category, code, what, element, line) {
|
|
1505
|
+
const key = `${code}\0${element}\0${what}`;
|
|
1506
|
+
const found = this.tallies.get(key);
|
|
1507
|
+
if (found === void 0) {
|
|
1508
|
+
this.tallies.set(key, { category, code, element, what, line, count: 1 });
|
|
1509
|
+
} else {
|
|
1510
|
+
found.count++;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Record the counted warnings, in the order they were first seen; past MAX_TALLIES_PER_CODE
|
|
1515
|
+
* of one code, the rest are summed up in one warning.
|
|
1516
|
+
*/
|
|
1517
|
+
flushTallies() {
|
|
1518
|
+
const listed = /* @__PURE__ */ new Map();
|
|
1519
|
+
const over = /* @__PURE__ */ new Map();
|
|
1520
|
+
for (const t of this.tallies.values()) {
|
|
1521
|
+
const n = (listed.get(t.code) ?? 0) + 1;
|
|
1522
|
+
listed.set(t.code, n);
|
|
1523
|
+
if (n > MAX_TALLIES_PER_CODE) {
|
|
1524
|
+
const rest = over.get(t.code) ?? [];
|
|
1525
|
+
rest.push(t);
|
|
1526
|
+
over.set(t.code, rest);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
this.report.warning(
|
|
1530
|
+
t.category,
|
|
1531
|
+
t.code,
|
|
1532
|
+
`${t.element}: ${t.what} (${t.count} time${plural(t.count)}, first at line ${t.line})`,
|
|
1533
|
+
{
|
|
1534
|
+
line: t.line,
|
|
1535
|
+
element: t.element
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
for (const [code, rest] of over) {
|
|
1540
|
+
this.report.warning(
|
|
1541
|
+
rest[0].category,
|
|
1542
|
+
code,
|
|
1543
|
+
`${rest.length} more element${plural(rest.length)} with this warning, not listed (first: ${rest[0].element}: ${rest[0].what}, line ${rest[0].line})`,
|
|
1544
|
+
{ line: rest[0].line }
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
this.tallies.clear();
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* The first line that makes the input not OBO: null unless no frame header and no plausible
|
|
1551
|
+
* header tag was read while some line was neither (an HTML page, a JSON, GML, DOT or CSV file,
|
|
1552
|
+
* UTF-16 read without its BOM). A file of only comments is OBO, if an empty one.
|
|
1553
|
+
* @returns the line, or null
|
|
1554
|
+
*/
|
|
1555
|
+
notObo() {
|
|
1556
|
+
return this.frameSeen || this.tagClauses > 0 ? null : this.firstBadLine;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Warn about the header once the file is read: a missing or unknown format-version (the
|
|
1560
|
+
* synonym-scope rule depends on it), a date that is not a real calendar date.
|
|
1561
|
+
*/
|
|
1562
|
+
checkHeader() {
|
|
1563
|
+
if (this.version === null) {
|
|
1564
|
+
const empty = this.frameSeen ? "" : " and the file has no frames: nothing was read";
|
|
1565
|
+
this.tally(
|
|
1566
|
+
"validation-error",
|
|
1567
|
+
OBO_ISSUE.FORMAT_VERSION,
|
|
1568
|
+
`the header has no format-version (required by 1.2 and 1.4)${empty}; read as the 1.0 / 1.2 / 1.4 union`,
|
|
1569
|
+
"format-version",
|
|
1570
|
+
1
|
|
1571
|
+
);
|
|
1572
|
+
} else if (!KNOWN_VERSION.test(this.version)) {
|
|
1573
|
+
this.tally(
|
|
1574
|
+
"validation-error",
|
|
1575
|
+
OBO_ISSUE.FORMAT_VERSION,
|
|
1576
|
+
`format-version ${JSON.stringify(this.version)} is not 1.0, 1.2 or 1.4; read as 1.4`,
|
|
1577
|
+
"format-version",
|
|
1578
|
+
1
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
const date = this.header.date?.[0];
|
|
1582
|
+
if (date !== void 0 && /^\d{2}:\d{2}:\d{4}\s+\d{2}:\d{2}$/.test(date.trim()) && oboDate$1(date.trim()) === null) {
|
|
1583
|
+
this.tally(
|
|
1584
|
+
"validation-error",
|
|
1585
|
+
OBO_ISSUE.SYNTAX,
|
|
1586
|
+
"the header date is not a real calendar date; created is null",
|
|
1587
|
+
"date",
|
|
1588
|
+
1
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The header as GraphMeta fields (design 4.2): name from `ontology`, the version, `created` from
|
|
1594
|
+
* the `dd:MM:yyyy HH:mm` date, `creator` from `saved-by`.
|
|
1595
|
+
* @returns the metadata patch, without `extra`
|
|
1596
|
+
*/
|
|
1597
|
+
meta() {
|
|
1598
|
+
const first = (tag) => {
|
|
1599
|
+
const values = this.header[tag];
|
|
1600
|
+
return values === void 0 ? null : unescapeObo(values[0]).trim();
|
|
1601
|
+
};
|
|
1602
|
+
return {
|
|
1603
|
+
name: first("ontology"),
|
|
1604
|
+
sourceFormat: "obo",
|
|
1605
|
+
sourceVersion: this.version,
|
|
1606
|
+
created: oboDate$1(first("date")),
|
|
1607
|
+
creator: first("saved-by")
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* The raw header clauses.
|
|
1612
|
+
* @returns the header by tag
|
|
1613
|
+
*/
|
|
1614
|
+
headerRecord() {
|
|
1615
|
+
return this.header;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* The namespace a frame without one gets.
|
|
1619
|
+
* @returns the default namespace, or null
|
|
1620
|
+
*/
|
|
1621
|
+
namespaceDefault() {
|
|
1622
|
+
return this.defaultNamespace;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function withQualifiers(item, qualifiers) {
|
|
1626
|
+
return qualifiers === null ? item : { ...item, qualifiers };
|
|
1627
|
+
}
|
|
1628
|
+
function oboDate$1(text) {
|
|
1629
|
+
const m = text === null ? null : /^(\d{2}):(\d{2}):(\d{4})\s+(\d{2}):(\d{2})$/.exec(text);
|
|
1630
|
+
if (m === null) {
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
const [, dd, mm, yyyy, hh, min] = m;
|
|
1634
|
+
const day = new Date(Date.UTC(Number(yyyy), Number(mm) - 1, Number(dd)));
|
|
1635
|
+
if (Number(mm) < 1 || Number(mm) > 12 || day.getUTCDate() !== Number(dd) || Number(hh) > 23 || Number(min) > 59) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
|
|
1639
|
+
}
|
|
1640
|
+
function planGraph(reader, report, addMissingNodes, obo) {
|
|
1641
|
+
const records = [...reader.nodes.values()];
|
|
1642
|
+
for (const [id, typedef] of reader.typedefs) {
|
|
1643
|
+
const node = reader.nodes.get(id);
|
|
1644
|
+
if (node !== void 0) {
|
|
1645
|
+
reader.tally(
|
|
1646
|
+
"validation-error",
|
|
1647
|
+
OBO_ISSUE.ID_KIND_CLASH,
|
|
1648
|
+
`an id names both a ${node.kind} and a Typedef; the ${node.kind} is the node`,
|
|
1649
|
+
id,
|
|
1650
|
+
typedef.line
|
|
1651
|
+
);
|
|
1652
|
+
} else if (obo.typedefs === "nodes") {
|
|
1653
|
+
records.push(typedef);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
for (const record of [...reader.nodes.values(), ...reader.typedefs.values()]) {
|
|
1657
|
+
for (const tag of ["intersection_of", "union_of"]) {
|
|
1658
|
+
if (record.lists.get(tag)?.length === 1) {
|
|
1659
|
+
reader.tally(
|
|
1660
|
+
"validation-error",
|
|
1661
|
+
OBO_ISSUE.CARDINALITY,
|
|
1662
|
+
"a single clause (a class expression needs two or more)",
|
|
1663
|
+
tag,
|
|
1664
|
+
record.line
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
1670
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1671
|
+
for (const record of records) {
|
|
1672
|
+
const obsolete = record.values.get("is_obsolete") === true;
|
|
1673
|
+
(obo.obsolete === "drop" && obsolete ? dropped : kept).add(record.id);
|
|
1674
|
+
if (obsolete && record.edges.length > 0) {
|
|
1675
|
+
reader.tally(
|
|
1676
|
+
"validation-error",
|
|
1677
|
+
OBO_ISSUE.OBSOLETION,
|
|
1678
|
+
"an obsolete term has is_a or relationship clauses; they are kept",
|
|
1679
|
+
"is_obsolete",
|
|
1680
|
+
record.edges[0].line
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
if (!obsolete && (record.lists.has("replaced_by") || record.lists.has("consider"))) {
|
|
1684
|
+
reader.tally(
|
|
1685
|
+
"validation-error",
|
|
1686
|
+
OBO_ISSUE.OBSOLETION,
|
|
1687
|
+
"replaced_by or consider on a term that is not obsolete; kept",
|
|
1688
|
+
"replaced_by",
|
|
1689
|
+
record.line
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const declared = new Set(reader.typedefs.keys());
|
|
1694
|
+
for (const typedef of reader.typedefs.values()) {
|
|
1695
|
+
for (const xref of typedef.lists.get("xref") ?? []) {
|
|
1696
|
+
declared.add(xref);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const dangling = [];
|
|
1700
|
+
const danglingSet = /* @__PURE__ */ new Set();
|
|
1701
|
+
let danglingEdges = 0;
|
|
1702
|
+
let droppedEdges = 0;
|
|
1703
|
+
const edges = [];
|
|
1704
|
+
for (const record of records) {
|
|
1705
|
+
for (const edge of record.edges) {
|
|
1706
|
+
if (dropped.has(record.id) || dropped.has(edge.target)) {
|
|
1707
|
+
droppedEdges++;
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
if (!BUILTIN_RELATIONS$1.has(edge.relation) && !declared.has(edge.relation)) {
|
|
1711
|
+
reader.tally(
|
|
1712
|
+
"validation-error",
|
|
1713
|
+
OBO_ISSUE.UNDECLARED,
|
|
1714
|
+
"a relation no Typedef declares",
|
|
1715
|
+
edge.relation,
|
|
1716
|
+
edge.line
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
if (!kept.has(edge.target)) {
|
|
1720
|
+
if (!danglingSet.has(edge.target)) {
|
|
1721
|
+
danglingSet.add(edge.target);
|
|
1722
|
+
dangling.push(edge.target);
|
|
1723
|
+
}
|
|
1724
|
+
if (!addMissingNodes) {
|
|
1725
|
+
danglingEdges++;
|
|
1726
|
+
report.counts.skippedEdges++;
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
edges.push({ source: record.id, edge });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
reader.flushTallies();
|
|
1734
|
+
reportDangling(records, dangling, addMissingNodes, danglingEdges, report, new Set(reader.typedefs.keys()));
|
|
1735
|
+
if (dropped.size > 0) {
|
|
1736
|
+
report.warning(
|
|
1737
|
+
"merged",
|
|
1738
|
+
OBO_ISSUE.OBSOLETE_DROPPED,
|
|
1739
|
+
`${dropped.size} obsolete term${plural(dropped.size)} and ${droppedEdges} edge${plural(droppedEdges)} to or from them were left out (obsolete: "drop")`,
|
|
1740
|
+
{ element: [...dropped][0] }
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
nodes: records.filter((record) => kept.has(record.id)),
|
|
1745
|
+
placeholders: addMissingNodes ? dangling : [],
|
|
1746
|
+
edges
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function reportDangling(records, dangling, addMissingNodes, droppedEdges, report, typedefs) {
|
|
1750
|
+
if (dangling.length === 0) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const altOf = /* @__PURE__ */ new Map();
|
|
1754
|
+
for (const record of records) {
|
|
1755
|
+
for (const alt of record.lists.get("alt_id") ?? []) {
|
|
1756
|
+
altOf.set(alt, record.id);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const shown = dangling.slice(0, 5).map((id) => {
|
|
1760
|
+
const primary = altOf.get(id);
|
|
1761
|
+
if (typedefs.has(id)) {
|
|
1762
|
+
return `${id} (a Typedef, kept as metadata, not a node)`;
|
|
1763
|
+
}
|
|
1764
|
+
return primary === void 0 ? id : `${id} (an alt_id of ${primary})`;
|
|
1765
|
+
});
|
|
1766
|
+
const more = dangling.length > 5 ? `, ... (${dangling.length - 5} more)` : "";
|
|
1767
|
+
const action = addMissingNodes ? "each became a placeholder node (graphty.placeholder)" : `${droppedEdges} edge${plural(droppedEdges)} to them ${agree(droppedEdges, "was", "were")} dropped (addMissingNodes false)`;
|
|
1768
|
+
report.warning(
|
|
1769
|
+
"validation-error",
|
|
1770
|
+
OBO_ISSUE.DANGLING_REFERENCE,
|
|
1771
|
+
`${dangling.length} target${plural(dangling.length)} no frame declares: ${shown.join(", ")}${more}; ${action}`,
|
|
1772
|
+
{ element: dangling[0] }
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
class Pusher {
|
|
1776
|
+
/**
|
|
1777
|
+
* Create a pusher.
|
|
1778
|
+
* @param sink - the sink
|
|
1779
|
+
* @param report - the report
|
|
1780
|
+
* @param options - the resolved common options
|
|
1781
|
+
*/
|
|
1782
|
+
constructor(sink, report, options) {
|
|
1783
|
+
this.restored = /* @__PURE__ */ new Map();
|
|
1784
|
+
this.since = 0;
|
|
1785
|
+
this.sink = sink;
|
|
1786
|
+
this.report = report;
|
|
1787
|
+
this.coercer = new IdCoercer(options.ids);
|
|
1788
|
+
this.signal = options.signal;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* An id text under the `ids` rule, reporting a merge.
|
|
1792
|
+
* @param text - the id as written
|
|
1793
|
+
* @returns the node id
|
|
1794
|
+
*/
|
|
1795
|
+
id(text) {
|
|
1796
|
+
const id = this.coercer.text(this.restored.get(text) ?? text);
|
|
1797
|
+
const merge = this.coercer.lastMerge;
|
|
1798
|
+
if (merge !== null) {
|
|
1799
|
+
this.report.warning(
|
|
1800
|
+
"coercion",
|
|
1801
|
+
OBO_ISSUE.ID_MERGED,
|
|
1802
|
+
`id text ${JSON.stringify(merge.text)} merged with ${JSON.stringify(merge.previousText)} as ${merge.id}`,
|
|
1803
|
+
{ element: text }
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
return id;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Add a node, counting it, or record why it was refused.
|
|
1810
|
+
* @param text - the id as written
|
|
1811
|
+
* @param line - the line, when known
|
|
1812
|
+
* @returns the node index, or -1 when refused
|
|
1813
|
+
*/
|
|
1814
|
+
node(text, line) {
|
|
1815
|
+
this.tick();
|
|
1816
|
+
try {
|
|
1817
|
+
const id = this.id(text);
|
|
1818
|
+
const known = this.sink.indexOf(id) !== INVALID_INDEX;
|
|
1819
|
+
const index = this.sink.addNode(id);
|
|
1820
|
+
if (!known) {
|
|
1821
|
+
this.report.counts.nodes++;
|
|
1822
|
+
}
|
|
1823
|
+
return index;
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
this.report.recordError(err, { line, element: text });
|
|
1826
|
+
this.report.counts.skippedNodes++;
|
|
1827
|
+
return -1;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
/** Check the cancellation signal every ABORT_CHECK_INTERVAL pushes. */
|
|
1831
|
+
tick() {
|
|
1832
|
+
if (++this.since >= ABORT_CHECK_INTERVAL) {
|
|
1833
|
+
this.since = 0;
|
|
1834
|
+
throwIfAborted(this.signal);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function writeGraph(reader, sink, report, options, obo) {
|
|
1839
|
+
const plan2 = planGraph(reader, report, options.addMissingNodes, obo);
|
|
1840
|
+
const push = new Pusher(sink, report, options);
|
|
1841
|
+
if (options.restoreMangledIds) {
|
|
1842
|
+
restoreIds(plan2.nodes, push.restored);
|
|
1843
|
+
}
|
|
1844
|
+
const namespace = reader.namespaceDefault();
|
|
1845
|
+
const used = /* @__PURE__ */ new Set(["type"]);
|
|
1846
|
+
for (const record of plan2.nodes) {
|
|
1847
|
+
for (const name of [...record.values.keys(), ...record.lists.keys()]) {
|
|
1848
|
+
used.add(name);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (namespace !== null) {
|
|
1852
|
+
used.add("namespace");
|
|
1853
|
+
}
|
|
1854
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1855
|
+
for (const name of plan2.nodes.length > 0 ? Object.keys(OBO_NODE_COLUMNS) : []) {
|
|
1856
|
+
if (used.has(name)) {
|
|
1857
|
+
columns.set(name, declareResolved(sink, "node", oboColumnDecl("node", name), report).handle);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
sink.reserve(plan2.nodes.length + plan2.placeholders.length, plan2.edges.length);
|
|
1861
|
+
for (const record of plan2.nodes) {
|
|
1862
|
+
const index = push.node(record.id, record.line);
|
|
1863
|
+
if (index < 0) {
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
const cells = [["type", record.kind], ...record.values, ...record.lists];
|
|
1867
|
+
if (namespace !== null && !record.values.has("namespace")) {
|
|
1868
|
+
cells.push(["namespace", namespace]);
|
|
1869
|
+
}
|
|
1870
|
+
for (const [name, value] of cells) {
|
|
1871
|
+
const handle = columns.get(name);
|
|
1872
|
+
if (handle !== void 0) {
|
|
1873
|
+
sink.setNodeValue(handle, index, value);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (plan2.placeholders.length > 0) {
|
|
1878
|
+
const placeholder = declareResolved(sink, "node", oboColumnDecl("node", PLACEHOLDER_COLUMN), report).handle;
|
|
1879
|
+
for (const text of plan2.placeholders) {
|
|
1880
|
+
const index = push.node(text);
|
|
1881
|
+
if (index >= 0) {
|
|
1882
|
+
sink.setNodeValue(placeholder, index, true);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
writeEdges(plan2, push, options);
|
|
1887
|
+
}
|
|
1888
|
+
function restoreIds(records, restored) {
|
|
1889
|
+
const taken = new Set(records.map((r) => r.id));
|
|
1890
|
+
for (const record of records) {
|
|
1891
|
+
const values = record.lists.get("property_value");
|
|
1892
|
+
const at = values?.findIndex((v) => v.relation === ORIGINAL_ID$1) ?? -1;
|
|
1893
|
+
if (values === void 0 || at < 0) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const original = originalText(values[at].value);
|
|
1897
|
+
if (taken.has(original)) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
taken.add(original);
|
|
1901
|
+
restored.set(record.id, original);
|
|
1902
|
+
values.splice(at, 1);
|
|
1903
|
+
if (values.length === 0) {
|
|
1904
|
+
record.lists.delete("property_value");
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function originalText(value) {
|
|
1909
|
+
try {
|
|
1910
|
+
const parsed = JSON.parse(value);
|
|
1911
|
+
return typeof parsed === "string" ? parsed : value;
|
|
1912
|
+
} catch {
|
|
1913
|
+
return value;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function writeEdges(plan2, push, options) {
|
|
1917
|
+
const { sink, report } = push;
|
|
1918
|
+
const direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
1919
|
+
direction.setHeader(true);
|
|
1920
|
+
if (plan2.edges.length === 0) {
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
const relation = declareResolved(sink, "edge", oboColumnDecl("edge", "relation"), report).handle;
|
|
1924
|
+
const qualifiers = plan2.edges.some((e) => e.edge.qualifiers !== null) ? declareResolved(sink, "edge", oboColumnDecl("edge", "qualifiers"), report).handle : null;
|
|
1925
|
+
for (const { source, edge } of plan2.edges) {
|
|
1926
|
+
push.tick();
|
|
1927
|
+
const before = sink.edgeCount;
|
|
1928
|
+
try {
|
|
1929
|
+
const e = direction.addEdge(push.id(source), push.id(edge.target), "directed", void 0, {
|
|
1930
|
+
line: edge.line,
|
|
1931
|
+
element: source
|
|
1932
|
+
});
|
|
1933
|
+
report.counts.edges += sink.edgeCount - before;
|
|
1934
|
+
if (e < 0) {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
sink.setEdgeValue(relation, e, edge.relation);
|
|
1938
|
+
if (qualifiers !== null && edge.qualifiers !== null) {
|
|
1939
|
+
sink.setEdgeValue(qualifiers, e, edge.qualifiers);
|
|
1940
|
+
}
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
report.recordError(err, { line: edge.line, element: source });
|
|
1943
|
+
report.counts.skippedEdges++;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function readLines(lines, reader) {
|
|
1948
|
+
const parts = [];
|
|
1949
|
+
let partLine = 0;
|
|
1950
|
+
const feed = (text, line) => {
|
|
1951
|
+
for (const piece of splitFormFeeds(text)) {
|
|
1952
|
+
reader.line(piece, line);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
for await (const physical of lines) {
|
|
1956
|
+
const { line } = lines;
|
|
1957
|
+
if (parts.length > 0) {
|
|
1958
|
+
const head = physical.trimStart();
|
|
1959
|
+
if (head.length === 0 || head.startsWith("[")) {
|
|
1960
|
+
reader.tally(
|
|
1961
|
+
"parse-error",
|
|
1962
|
+
OBO_ISSUE.SYNTAX,
|
|
1963
|
+
"a line continuation before a frame header or a blank line was ignored",
|
|
1964
|
+
"\\",
|
|
1965
|
+
partLine
|
|
1966
|
+
);
|
|
1967
|
+
feed(parts.join(""), partLine);
|
|
1968
|
+
parts.length = 0;
|
|
1969
|
+
} else {
|
|
1970
|
+
reader.tally(
|
|
1971
|
+
"coercion",
|
|
1972
|
+
OBO_ISSUE.DEPRECATED_SYNTAX,
|
|
1973
|
+
"a backslash line continuation was joined (deprecated in 1.4)",
|
|
1974
|
+
"\\",
|
|
1975
|
+
partLine
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (endsWithContinuation(physical) && stripComment(physical) === physical) {
|
|
1980
|
+
parts.push(physical.slice(0, -1));
|
|
1981
|
+
partLine = line;
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (parts.length > 0) {
|
|
1985
|
+
parts.push(physical);
|
|
1986
|
+
feed(parts.join(""), line);
|
|
1987
|
+
parts.length = 0;
|
|
1988
|
+
} else {
|
|
1989
|
+
feed(physical, line);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (parts.length > 0) {
|
|
1993
|
+
reader.tally(
|
|
1994
|
+
"coercion",
|
|
1995
|
+
OBO_ISSUE.DEPRECATED_SYNTAX,
|
|
1996
|
+
"a backslash line continuation was joined (deprecated in 1.4)",
|
|
1997
|
+
"\\",
|
|
1998
|
+
partLine
|
|
1999
|
+
);
|
|
2000
|
+
feed(parts.join(""), partLine);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
function sniffObo(head) {
|
|
2004
|
+
let text = new TextDecoder("utf-8").decode(head.subarray(0, SNIFF_BYTES));
|
|
2005
|
+
if (text.charCodeAt(0) === 65279) {
|
|
2006
|
+
text = text.slice(1);
|
|
2007
|
+
}
|
|
2008
|
+
const lines = text.split(/\r\n|\r|\n|\f/);
|
|
2009
|
+
if (lines.length > 1 && head.byteLength >= SNIFF_BYTES) {
|
|
2010
|
+
lines.pop();
|
|
2011
|
+
}
|
|
2012
|
+
let tagLines2 = 0;
|
|
2013
|
+
for (const raw of lines) {
|
|
2014
|
+
const line = raw.trim();
|
|
2015
|
+
if (line.length === 0 || line.startsWith("!")) {
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (/^\[(Term|Typedef|Instance)\]\s*(!.*)?$/.test(line)) {
|
|
2019
|
+
return 0.9;
|
|
2020
|
+
}
|
|
2021
|
+
if (tagLines2 === 0 && /^format-version\s*:/.test(line)) {
|
|
2022
|
+
return 0.9;
|
|
2023
|
+
}
|
|
2024
|
+
if (/^[A-Za-z][A-Za-z0-9_-]*\s*:(\s|$)/.test(line)) {
|
|
2025
|
+
tagLines2++;
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
return 0;
|
|
2029
|
+
}
|
|
2030
|
+
return tagLines2 > 0 ? 0.4 : 0;
|
|
2031
|
+
}
|
|
2032
|
+
const oboImporter = Object.freeze({
|
|
2033
|
+
format: "obo",
|
|
2034
|
+
options: Object.freeze(["obsolete", "typedefs"]),
|
|
2035
|
+
extensions: Object.freeze([".obo"]),
|
|
2036
|
+
mimeTypes: Object.freeze(["text/obo", "application/obo"]),
|
|
2037
|
+
/**
|
|
2038
|
+
* Confidence that the head is an OBO file.
|
|
2039
|
+
* @param head - the first bytes
|
|
2040
|
+
* @returns the confidence
|
|
2041
|
+
*/
|
|
2042
|
+
sniff(head) {
|
|
2043
|
+
return sniffObo(head);
|
|
2044
|
+
},
|
|
2045
|
+
/**
|
|
2046
|
+
* Read an OBO file into the sink.
|
|
2047
|
+
* @param input - the text, bytes or stream
|
|
2048
|
+
* @param sink - the sink
|
|
2049
|
+
* @param options - format-specific and common options
|
|
2050
|
+
* @returns the import report; ImportError on a fatal error or beyond the error limit
|
|
2051
|
+
*/
|
|
2052
|
+
async import(input, sink, options) {
|
|
2053
|
+
const resolved = resolveImportOptions(options, DEFAULTS);
|
|
2054
|
+
const obo = resolveOboOptions$1(options);
|
|
2055
|
+
const report = new ImportReportBuilder("obo", resolved.errorLimit);
|
|
2056
|
+
reportSinkOptions(sink, options, report, true);
|
|
2057
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
2058
|
+
const reader = new OboReader(report, resolved.signal, obo.typedefs === "nodes");
|
|
2059
|
+
await readLines(new LineReader(input, report, resolved), reader);
|
|
2060
|
+
reader.finishFrame();
|
|
2061
|
+
if (!reader.significant) {
|
|
2062
|
+
report.fail(OBO_ISSUE.EMPTY_INPUT, "the input is empty");
|
|
2063
|
+
}
|
|
2064
|
+
const notObo = reader.notObo();
|
|
2065
|
+
if (notObo !== null) {
|
|
2066
|
+
report.fail(
|
|
2067
|
+
OBO_ISSUE.NOT_OBO,
|
|
2068
|
+
`the input is not OBO: it has no frame header and no header tag, and line ${notObo} is not a tag-value line`,
|
|
2069
|
+
{ line: notObo }
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
throwIfAborted(resolved.signal);
|
|
2073
|
+
reader.checkHeader();
|
|
2074
|
+
writeGraph(reader, sink, report, resolved, obo);
|
|
2075
|
+
const typedefs = ownRecord();
|
|
2076
|
+
for (const [id, record] of reader.typedefs) {
|
|
2077
|
+
typedefs[id] = record.raw;
|
|
2078
|
+
}
|
|
2079
|
+
const extra = { header: reader.headerRecord(), typedefs };
|
|
2080
|
+
if (reader.unknownFrames.length > 0) {
|
|
2081
|
+
extra.unknownFrames = reader.unknownFrames;
|
|
2082
|
+
}
|
|
2083
|
+
sink.setMeta({ ...reader.meta(), extra: { obo: extra } });
|
|
2084
|
+
throwIfAborted(resolved.signal);
|
|
2085
|
+
return report.finish();
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
const OBO_LOSS = Object.freeze({
|
|
2089
|
+
/** A node column outside the OBO vocabulary (or one whose values a tag cannot carry exactly) reads back inside the `property_value` column. */
|
|
2090
|
+
COLUMN_AS_PROPERTY_VALUE: COLUMN_AS_PROPERTY_VALUE_CODE,
|
|
2091
|
+
/** An edge column (or the explicit weights) reads back inside the `qualifiers` column. */
|
|
2092
|
+
EDGE_COLUMN_AS_QUALIFIER: "W_OBO_EDGE_COLUMN_AS_QUALIFIER",
|
|
2093
|
+
/** Every edge is written from its source to its target, so an undirected graph reads back as directed. */
|
|
2094
|
+
UNDIRECTED_AS_DIRECTED: "W_OBO_UNDIRECTED_AS_DIRECTED",
|
|
2095
|
+
/** Two identical clauses on one frame (parallel edges with one relation and the same qualifiers, a repeated list item) read back as one. */
|
|
2096
|
+
DUPLICATE_CLAUSE: "W_OBO_DUPLICATE_CLAUSE",
|
|
2097
|
+
/** An edge without a relation is written with the `relation` option (default `is_a`). */
|
|
2098
|
+
RELATION_ASSUMED: RELATION_ASSUMED_CODE,
|
|
2099
|
+
/** A relation that is not an OBO id (empty, or holding a space, `!`, `{` or `}`) is written with `_` in place of those characters. */
|
|
2100
|
+
RELATION_RENAMED: "W_OBO_RELATION_RENAMED",
|
|
2101
|
+
/** The graph name is not an ontology id; the header `ontology` is written with `_` in place of the other characters. */
|
|
2102
|
+
ONTOLOGY_NAME: "W_OBO_ONTOLOGY_NAME",
|
|
2103
|
+
/** `[Typedef]` frames read back as nodes only under the importer's `typedefs: "nodes"`; by default they are metadata. */
|
|
2104
|
+
TYPEDEF_NODES: TYPEDEF_NODES_CODE,
|
|
2105
|
+
/** Edges are written in the frame of their source node, so they read back grouped by source, in node order. */
|
|
2106
|
+
EDGE_ORDER: "W_OBO_EDGE_ORDER",
|
|
2107
|
+
/**
|
|
2108
|
+
* A graph attribute is written as a header `property_value` and reads back in `snapshot.meta.extra.obo.header`,
|
|
2109
|
+
* not as a graph attribute.
|
|
2110
|
+
*/
|
|
2111
|
+
GRAPH_COLUMN_AS_METADATA: GRAPH_COLUMN_AS_METADATA_CODE,
|
|
2112
|
+
/** A carriage return or form feed in a text cannot be written; it is written as a line feed. */
|
|
2113
|
+
LINE_END: "W_OBO_LINE_END",
|
|
2114
|
+
/** The nodes read back in a different order. */
|
|
2115
|
+
NODE_ORDER: NODE_ORDER_CODE,
|
|
2116
|
+
/** A mutual pair is written as two clauses without its mark. */
|
|
2117
|
+
MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
|
|
2118
|
+
/**
|
|
2119
|
+
* The undirected edges of a mixed graph are written the way `onMixedDirection` chose ("directed" or "undirected").
|
|
2120
|
+
*/
|
|
2121
|
+
MIXED_DIRECTION: LOSS.MIXED_DIRECTION,
|
|
2122
|
+
/**
|
|
2123
|
+
* The graph has both directed and undirected edges and `onMixedDirection` is "error". An import stops; a save to a
|
|
2124
|
+
* format that holds one direction per file fails with E_DIRECTED. Pass "directed" or "undirected" to read or write
|
|
2125
|
+
* it anyway.
|
|
2126
|
+
*/
|
|
2127
|
+
MIXED_DIRECTION_ERROR: LOSS.MIXED_DIRECTION_ERROR,
|
|
2128
|
+
/** OBO has no edge ids. */
|
|
2129
|
+
EDGE_IDS_DROPPED: LOSS.EDGE_IDS_DROPPED,
|
|
2130
|
+
/** OBO has no positions. */
|
|
2131
|
+
POSITIONS_DROPPED: LOSS.POSITIONS,
|
|
2132
|
+
/** OBO has no containment. */
|
|
2133
|
+
HIERARCHY_DROPPED: LOSS.HIERARCHY,
|
|
2134
|
+
/** OBO has no time. */
|
|
2135
|
+
TEMPORAL_DROPPED: LOSS.TEMPORAL,
|
|
2136
|
+
/** OBO has no visual columns. */
|
|
2137
|
+
VIZ_DROPPED: LOSS.VIZ,
|
|
2138
|
+
/** An attribute with a role the format has no place for is written as a plain attribute; the role is lost. */
|
|
2139
|
+
ROLE_DROPPED: LOSS.ROLE,
|
|
2140
|
+
/**
|
|
2141
|
+
* An attribute without a role is written where the format keeps a role (for example, a `name` column as the label), and
|
|
2142
|
+
* reads back with that role.
|
|
2143
|
+
*/
|
|
2144
|
+
ROLE_ASSUMED: ROLE_ASSUMED_CODE,
|
|
2145
|
+
/**
|
|
2146
|
+
* An attribute with a role (for example, the label) is written where the format keeps that role, and reads back under the
|
|
2147
|
+
* name the format's importer gives it.
|
|
2148
|
+
*/
|
|
2149
|
+
COLUMN_NAME_CHANGED: LOSS.COLUMN_NAME_CHANGED,
|
|
2150
|
+
/**
|
|
2151
|
+
* An OBO attribute stored as text where graph-io uses a dictionary (or the other way round); it reads back with
|
|
2152
|
+
* graph-io's usual type. The values are the same.
|
|
2153
|
+
*/
|
|
2154
|
+
DTYPE_UNSUPPORTED: LOSS.DTYPE,
|
|
2155
|
+
/** A column without a value on any written element reads back absent. */
|
|
2156
|
+
EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
|
|
2157
|
+
/** A declared default: OBO has none. */
|
|
2158
|
+
DEFAULT_DROPPED: LOSS.DEFAULT,
|
|
2159
|
+
/** Declared options: OBO has no enumerations. */
|
|
2160
|
+
OPTIONS_DROPPED: LOSS.OPTIONS,
|
|
2161
|
+
/** An extension table OBO cannot carry. */
|
|
2162
|
+
EXTENSION_TABLE_DROPPED: LOSS.EXTENSION_TABLE,
|
|
2163
|
+
/**
|
|
2164
|
+
* Node ids the format cannot write, under `sanitizeIds: "error"`; the save fails with E_INVALID_ID. Pass
|
|
2165
|
+
* `sanitizeIds: "mangle"` to rewrite them.
|
|
2166
|
+
*/
|
|
2167
|
+
ID_CHARSET: LOSS.ID_CHARSET,
|
|
2168
|
+
/** Node ids OBO cannot write as they are, under sanitizeIds "mangle": rewritten, the original kept. */
|
|
2169
|
+
ID_MANGLED: LOSS.ID_MANGLED,
|
|
2170
|
+
/** Numeric ids are written as text and read back as strings. */
|
|
2171
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
2172
|
+
/**
|
|
2173
|
+
* Two node ids would be written as the same text (the number 5 and the text "5"); the save fails with
|
|
2174
|
+
* E_INVALID_ID.
|
|
2175
|
+
*/
|
|
2176
|
+
ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION
|
|
2177
|
+
});
|
|
2178
|
+
const OBO_CAPABILITIES = capabilities({
|
|
2179
|
+
mixedDirection: false,
|
|
2180
|
+
multiEdges: true,
|
|
2181
|
+
selfLoops: true,
|
|
2182
|
+
edgeIds: "none",
|
|
2183
|
+
idCharset: "any",
|
|
2184
|
+
dtypes: [],
|
|
2185
|
+
components: false,
|
|
2186
|
+
lists: false,
|
|
2187
|
+
json: false,
|
|
2188
|
+
defaults: false,
|
|
2189
|
+
options: false,
|
|
2190
|
+
hierarchy: false,
|
|
2191
|
+
temporal: "none",
|
|
2192
|
+
graphAttributes: false,
|
|
2193
|
+
positions: false,
|
|
2194
|
+
viz: false
|
|
2195
|
+
});
|
|
2196
|
+
const ORIGINAL_ID = "graphty:originalId";
|
|
2197
|
+
const RELATION_COLUMN = "relation";
|
|
2198
|
+
const QUALIFIERS_COLUMN = "qualifiers";
|
|
2199
|
+
const WEIGHT_QUALIFIER = "weight";
|
|
2200
|
+
const BUILTIN_RELATIONS = /* @__PURE__ */ new Set([
|
|
2201
|
+
"is_a",
|
|
2202
|
+
"instance_of",
|
|
2203
|
+
"disjoint_from",
|
|
2204
|
+
"inverse_of",
|
|
2205
|
+
"union_of",
|
|
2206
|
+
"intersection_of"
|
|
2207
|
+
]);
|
|
2208
|
+
const EDGES = "\0edges";
|
|
2209
|
+
const UNRECOGNIZED = "obo.unrecognized";
|
|
2210
|
+
const TAG_ORDER = [
|
|
2211
|
+
"is_anonymous",
|
|
2212
|
+
"name",
|
|
2213
|
+
"namespace",
|
|
2214
|
+
"alt_id",
|
|
2215
|
+
"def",
|
|
2216
|
+
"comment",
|
|
2217
|
+
"subset",
|
|
2218
|
+
"synonym",
|
|
2219
|
+
"xref",
|
|
2220
|
+
"builtin",
|
|
2221
|
+
"property_value",
|
|
2222
|
+
"domain",
|
|
2223
|
+
"range",
|
|
2224
|
+
"holds_over_chain",
|
|
2225
|
+
"is_anti_symmetric",
|
|
2226
|
+
"is_cyclic",
|
|
2227
|
+
"is_reflexive",
|
|
2228
|
+
"is_symmetric",
|
|
2229
|
+
"is_asymmetric",
|
|
2230
|
+
"is_transitive",
|
|
2231
|
+
"is_functional",
|
|
2232
|
+
"is_inverse_functional",
|
|
2233
|
+
EDGES,
|
|
2234
|
+
"intersection_of",
|
|
2235
|
+
"union_of",
|
|
2236
|
+
"equivalent_to",
|
|
2237
|
+
"disjoint_from",
|
|
2238
|
+
"inverse_of",
|
|
2239
|
+
"transitive_over",
|
|
2240
|
+
"equivalent_to_chain",
|
|
2241
|
+
"disjoint_over",
|
|
2242
|
+
"created_by",
|
|
2243
|
+
"creation_date",
|
|
2244
|
+
"is_obsolete",
|
|
2245
|
+
"replaced_by",
|
|
2246
|
+
"consider",
|
|
2247
|
+
"expand_assertion_to",
|
|
2248
|
+
"expand_expression_to",
|
|
2249
|
+
"is_metadata_tag",
|
|
2250
|
+
"is_class_level",
|
|
2251
|
+
UNRECOGNIZED
|
|
2252
|
+
];
|
|
2253
|
+
const HEADER_ORDER = [
|
|
2254
|
+
"format-version",
|
|
2255
|
+
"data-version",
|
|
2256
|
+
"version",
|
|
2257
|
+
"date",
|
|
2258
|
+
"saved-by",
|
|
2259
|
+
"auto-generated-by",
|
|
2260
|
+
"import",
|
|
2261
|
+
"subsetdef",
|
|
2262
|
+
"synonymtypedef",
|
|
2263
|
+
"idspace",
|
|
2264
|
+
"default-relationship-id-prefix",
|
|
2265
|
+
"id-mapping",
|
|
2266
|
+
"default-namespace",
|
|
2267
|
+
"namespace-id-rule",
|
|
2268
|
+
"remark",
|
|
2269
|
+
"ontology"
|
|
2270
|
+
];
|
|
2271
|
+
const APPLIED_TAGS = /* @__PURE__ */ new Set([
|
|
2272
|
+
"id",
|
|
2273
|
+
"is_a",
|
|
2274
|
+
"instance_of",
|
|
2275
|
+
"relationship",
|
|
2276
|
+
"intersection_of",
|
|
2277
|
+
"holds_over_chain",
|
|
2278
|
+
"equivalent_to_chain",
|
|
2279
|
+
"def",
|
|
2280
|
+
"expand_assertion_to",
|
|
2281
|
+
"expand_expression_to",
|
|
2282
|
+
"synonym",
|
|
2283
|
+
"xref",
|
|
2284
|
+
"property_value",
|
|
2285
|
+
...TEXT_TAGS,
|
|
2286
|
+
...BOOL_TAGS,
|
|
2287
|
+
...ID_LIST_TAGS,
|
|
2288
|
+
...DEPRECATED_TAGS.keys()
|
|
2289
|
+
]);
|
|
2290
|
+
const TAG_COLUMNS = new Set(
|
|
2291
|
+
Object.keys(OBO_NODE_COLUMNS).filter((name) => name !== "type" && name !== "name" && name !== "propertyType")
|
|
2292
|
+
);
|
|
2293
|
+
const XSD = Object.freeze({
|
|
2294
|
+
string: "xsd:string",
|
|
2295
|
+
dict: "xsd:string",
|
|
2296
|
+
bool: "xsd:boolean",
|
|
2297
|
+
i32: "xsd:integer",
|
|
2298
|
+
u32: "xsd:integer",
|
|
2299
|
+
u8: "xsd:integer",
|
|
2300
|
+
f32: "xsd:double",
|
|
2301
|
+
f64: "xsd:double"
|
|
2302
|
+
});
|
|
2303
|
+
function isTrimmed(text) {
|
|
2304
|
+
return text.length > 0 && text === text.trim();
|
|
2305
|
+
}
|
|
2306
|
+
function arrayOf(value, item, nonEmpty = true) {
|
|
2307
|
+
return Array.isArray(value) && (!nonEmpty || value.length > 0) && value.every(item);
|
|
2308
|
+
}
|
|
2309
|
+
function isXrefId(value) {
|
|
2310
|
+
return typeof value === "string" && value.length > 0 && isTrimmed(value);
|
|
2311
|
+
}
|
|
2312
|
+
function isWord(value) {
|
|
2313
|
+
return typeof value === "string" && isOboWord(value);
|
|
2314
|
+
}
|
|
2315
|
+
function hasKeys(value, keys) {
|
|
2316
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2317
|
+
return false;
|
|
2318
|
+
}
|
|
2319
|
+
const own = Object.keys(value);
|
|
2320
|
+
const record = value;
|
|
2321
|
+
if (Object.hasOwn(record, "qualifiers")) {
|
|
2322
|
+
if (!isQualifierRecord(record.qualifiers, isQualifierName)) {
|
|
2323
|
+
return false;
|
|
2324
|
+
}
|
|
2325
|
+
return own.length === keys.length + 1 && keys.every((k) => Object.hasOwn(record, k));
|
|
2326
|
+
}
|
|
2327
|
+
return own.length === keys.length && keys.every((k) => Object.hasOwn(record, k));
|
|
2328
|
+
}
|
|
2329
|
+
function wordOf(text) {
|
|
2330
|
+
const out = text.replaceAll(new RegExp(NOT_IN_WORD.source, "gu"), "_");
|
|
2331
|
+
return out.length === 0 ? "_" : out;
|
|
2332
|
+
}
|
|
2333
|
+
function qualifierPairs(record) {
|
|
2334
|
+
const out = [];
|
|
2335
|
+
for (const [name, value] of Object.entries(record)) {
|
|
2336
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
2337
|
+
out.push([name, item]);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return out;
|
|
2341
|
+
}
|
|
2342
|
+
function resolveOboOptions(options) {
|
|
2343
|
+
const relation = options?.relation ?? "is_a";
|
|
2344
|
+
if (typeof relation !== "string" || !isOboWord(relation)) {
|
|
2345
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option relation: ${JSON.stringify(relation)} is not an OBO id`, {
|
|
2346
|
+
option: "relation",
|
|
2347
|
+
found: relation
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
const ontology = options?.ontology ?? null;
|
|
2351
|
+
if (ontology !== null && (typeof ontology !== "string" || !/^[A-Za-z0-9_.\-/]+$/.test(ontology))) {
|
|
2352
|
+
throw new GraphFormatError(
|
|
2353
|
+
"E_UNSUPPORTED",
|
|
2354
|
+
`option ontology: ${JSON.stringify(ontology)} is not an ontology id (letters, digits, _ . - /)`,
|
|
2355
|
+
{ option: "ontology", found: ontology }
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
return { relation, ontology };
|
|
2359
|
+
}
|
|
2360
|
+
function planIds(snapshot, mode, note) {
|
|
2361
|
+
const n = snapshot.nodeCount;
|
|
2362
|
+
const ids = new Array(n);
|
|
2363
|
+
const bad = [];
|
|
2364
|
+
let numeric = 0;
|
|
2365
|
+
for (let i = 0; i < n; i++) {
|
|
2366
|
+
const id = snapshot.ids.idOf(i);
|
|
2367
|
+
if (typeof id === "number") {
|
|
2368
|
+
numeric++;
|
|
2369
|
+
}
|
|
2370
|
+
ids[i] = String(id);
|
|
2371
|
+
if (!isOboWord(ids[i])) {
|
|
2372
|
+
bad.push(i);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (numeric > 0) {
|
|
2376
|
+
note(
|
|
2377
|
+
LOSS.ID_TEXT_TYPE,
|
|
2378
|
+
`${numeric} numeric node id${plural(numeric)} ${agree(numeric, "is", "are")} written as text and read back as strings`,
|
|
2379
|
+
null,
|
|
2380
|
+
numeric
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
const originals = /* @__PURE__ */ new Map();
|
|
2384
|
+
let fatal = null;
|
|
2385
|
+
if (bad.length > 0 && mode === "error") {
|
|
2386
|
+
fatal = refuseIds(snapshot, ids, bad, note);
|
|
2387
|
+
} else if (bad.length > 0) {
|
|
2388
|
+
mangleIds(ids, bad, originals, note);
|
|
2389
|
+
}
|
|
2390
|
+
return { ids, originals, fatal: fatal ?? collidingIds(snapshot, ids, note) };
|
|
2391
|
+
}
|
|
2392
|
+
function refuseIds(snapshot, ids, bad, note) {
|
|
2393
|
+
note(
|
|
2394
|
+
LOSS.ID_CHARSET,
|
|
2395
|
+
`${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"`,
|
|
2396
|
+
null,
|
|
2397
|
+
bad.length
|
|
2398
|
+
);
|
|
2399
|
+
return new GraphFormatError(
|
|
2400
|
+
"E_INVALID_ID",
|
|
2401
|
+
`${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`,
|
|
2402
|
+
{ reason: "charset", count: bad.length, id: snapshot.ids.idOf(bad[0]), index: bad[0] }
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
function mangleIds(ids, bad, originals, note) {
|
|
2406
|
+
const badSet = new Set(bad);
|
|
2407
|
+
const used = new Set(ids.filter((_, i) => !badSet.has(i)));
|
|
2408
|
+
for (const i of bad) {
|
|
2409
|
+
const base = wordOf(ids[i]);
|
|
2410
|
+
let candidate = base;
|
|
2411
|
+
let k = 2;
|
|
2412
|
+
while (used.has(candidate)) {
|
|
2413
|
+
candidate = `${base}_${k}`;
|
|
2414
|
+
k++;
|
|
2415
|
+
}
|
|
2416
|
+
used.add(candidate);
|
|
2417
|
+
originals.set(i, ids[i]);
|
|
2418
|
+
ids[i] = candidate;
|
|
2419
|
+
}
|
|
2420
|
+
note(
|
|
2421
|
+
LOSS.ID_MANGLED,
|
|
2422
|
+
`${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)`,
|
|
2423
|
+
null,
|
|
2424
|
+
bad.length
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
function collidingIds(snapshot, ids, note) {
|
|
2428
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2429
|
+
for (let i = 0; i < ids.length; i++) {
|
|
2430
|
+
const before = seen.get(ids[i]);
|
|
2431
|
+
if (before !== void 0) {
|
|
2432
|
+
note(
|
|
2433
|
+
LOSS.ID_TEXT_COLLISION,
|
|
2434
|
+
`node ids ${JSON.stringify(snapshot.ids.idOf(before))} and ${JSON.stringify(snapshot.ids.idOf(i))} have the same text; the save fails`,
|
|
2435
|
+
null,
|
|
2436
|
+
1
|
|
2437
|
+
);
|
|
2438
|
+
return new GraphFormatError("E_INVALID_ID", "two node ids have the same text; OBO keeps ids as text", {
|
|
2439
|
+
reason: "collision"
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
seen.set(ids[i], i);
|
|
2443
|
+
}
|
|
2444
|
+
return null;
|
|
2445
|
+
}
|
|
2446
|
+
function planEdges(snapshot, folding, common, note) {
|
|
2447
|
+
const { src, dst } = snapshot.edgeList();
|
|
2448
|
+
const written2 = [];
|
|
2449
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
2450
|
+
if (!folding.folded(e)) {
|
|
2451
|
+
written2.push(e);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
const edgeStart = new Uint32Array(snapshot.nodeCount + 1);
|
|
2455
|
+
for (const e of written2) {
|
|
2456
|
+
edgeStart[src[e] + 1]++;
|
|
2457
|
+
}
|
|
2458
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
2459
|
+
edgeStart[i + 1] += edgeStart[i];
|
|
2460
|
+
}
|
|
2461
|
+
const fill = edgeStart.slice(0, snapshot.nodeCount);
|
|
2462
|
+
const edgeOrder = new Uint32Array(written2.length);
|
|
2463
|
+
for (const e of written2) {
|
|
2464
|
+
edgeOrder[fill[src[e]]++] = e;
|
|
2465
|
+
}
|
|
2466
|
+
let moved = 0;
|
|
2467
|
+
for (let k = 0; k < edgeOrder.length; k++) {
|
|
2468
|
+
if (edgeOrder[k] !== written2[k]) {
|
|
2469
|
+
moved++;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (moved > 0) {
|
|
2473
|
+
note(
|
|
2474
|
+
OBO_LOSS.EDGE_ORDER,
|
|
2475
|
+
`${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`,
|
|
2476
|
+
null,
|
|
2477
|
+
moved
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
if (!snapshot.directed) {
|
|
2481
|
+
note(
|
|
2482
|
+
OBO_LOSS.UNDIRECTED_AS_DIRECTED,
|
|
2483
|
+
`the snapshot is undirected; every edge is written from its source to its target and reads back directed (${written2.length} edge${plural(written2.length)})`,
|
|
2484
|
+
null,
|
|
2485
|
+
written2.length
|
|
2486
|
+
);
|
|
2487
|
+
} else if (common.onMixedDirection !== "error") {
|
|
2488
|
+
const undirected = written2.filter((e) => !folding.sourceDirected(e)).length;
|
|
2489
|
+
if (undirected > 0) {
|
|
2490
|
+
note(
|
|
2491
|
+
OBO_LOSS.UNDIRECTED_AS_DIRECTED,
|
|
2492
|
+
`${undirected} undirected edge${plural(undirected)} ${agree(undirected, "is", "are")} written as one directed clause each; OBO has no undirected edge`,
|
|
2493
|
+
null,
|
|
2494
|
+
undirected
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (folding.mutualCount > 0) {
|
|
2499
|
+
note(
|
|
2500
|
+
LOSS.MUTUAL_EXPANDED,
|
|
2501
|
+
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two clauses; the mutual mark is lost`,
|
|
2502
|
+
null,
|
|
2503
|
+
folding.mutualCount
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
return { written: written2, edgeStart, edgeOrder, dst };
|
|
2507
|
+
}
|
|
2508
|
+
function planKinds(snapshot, frameless) {
|
|
2509
|
+
const kinds = new Array(snapshot.nodeCount).fill("Term");
|
|
2510
|
+
const type = snapshot.nodes.get("type");
|
|
2511
|
+
if (type === null || type.dtype !== "dict" && type.dtype !== "string") {
|
|
2512
|
+
return { kinds, type: null };
|
|
2513
|
+
}
|
|
2514
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
2515
|
+
if (frameless[i] === 1) {
|
|
2516
|
+
if (type.isSet(i)) {
|
|
2517
|
+
return { kinds: kinds.fill("Term"), type: null };
|
|
2518
|
+
}
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
const value = cellOf(type, i);
|
|
2522
|
+
if (typeof value !== "string" || !FRAME_KINDS$1.has(value)) {
|
|
2523
|
+
return { kinds: kinds.fill("Term"), type: null };
|
|
2524
|
+
}
|
|
2525
|
+
kinds[i] = value;
|
|
2526
|
+
}
|
|
2527
|
+
return { kinds, type };
|
|
2528
|
+
}
|
|
2529
|
+
function tagCarries(name, column, rows, kinds, scopes) {
|
|
2530
|
+
const spec = OBO_NODE_COLUMNS[name];
|
|
2531
|
+
if (!fitsVocabulary(spec.dtype, column) || column.meta.components > 1) {
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
const typedefOnly = TYPEDEF_TAGS.has(name);
|
|
2535
|
+
const valid = (v) => {
|
|
2536
|
+
if (TEXT_TAGS.has(name)) {
|
|
2537
|
+
return typeof v === "string" && isTrimmed(v);
|
|
2538
|
+
}
|
|
2539
|
+
if (BOOL_TAGS.has(name)) {
|
|
2540
|
+
return typeof v === "boolean";
|
|
2541
|
+
}
|
|
2542
|
+
if (ID_LIST_TAGS.has(name)) {
|
|
2543
|
+
return arrayOf(v, isWord);
|
|
2544
|
+
}
|
|
2545
|
+
switch (name) {
|
|
2546
|
+
case "def":
|
|
2547
|
+
return typeof v === "string";
|
|
2548
|
+
case "def.xrefs":
|
|
2549
|
+
return arrayOf(v, isXrefId, false);
|
|
2550
|
+
case "xref":
|
|
2551
|
+
return arrayOf(v, isXrefId);
|
|
2552
|
+
case "xref.descriptions":
|
|
2553
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((d) => typeof d === "string");
|
|
2554
|
+
case "synonym":
|
|
2555
|
+
return arrayOf(
|
|
2556
|
+
v,
|
|
2557
|
+
(s) => hasKeys(s, ["text", "scope", "type", "xrefs"]) && typeof s.text === "string" && typeof s.scope === "string" && SYNONYM_SCOPES.has(s.scope) && (s.type === null || isWord(s.type)) && (s.type === null || (scopes.get(s.type) ?? s.scope) === s.scope) && arrayOf(s.xrefs, isXrefId, false)
|
|
2558
|
+
);
|
|
2559
|
+
case "intersection_of":
|
|
2560
|
+
return arrayOf(
|
|
2561
|
+
v,
|
|
2562
|
+
(x) => hasKeys(x, ["relation", "target"]) && (x.relation === null || isWord(x.relation)) && isWord(x.target)
|
|
2563
|
+
);
|
|
2564
|
+
case "property_value":
|
|
2565
|
+
return arrayOf(
|
|
2566
|
+
v,
|
|
2567
|
+
(x) => hasKeys(x, ["relation", "value", "datatype"]) && isWord(x.relation) && x.relation !== ORIGINAL_ID && typeof x.value === "string" && (x.datatype === null || isWord(x.datatype))
|
|
2568
|
+
);
|
|
2569
|
+
case "holds_over_chain":
|
|
2570
|
+
case "equivalent_to_chain":
|
|
2571
|
+
return arrayOf(v, (pair) => arrayOf(pair, isWord) && pair.length === 2);
|
|
2572
|
+
case "expand_assertion_to":
|
|
2573
|
+
case "expand_expression_to":
|
|
2574
|
+
return arrayOf(
|
|
2575
|
+
v,
|
|
2576
|
+
(x) => hasKeys(x, ["template", "xrefs"]) && typeof x.template === "string" && arrayOf(x.xrefs, isXrefId, false)
|
|
2577
|
+
);
|
|
2578
|
+
case "obo.qualifiers":
|
|
2579
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every(
|
|
2580
|
+
(entries) => arrayOf(
|
|
2581
|
+
entries,
|
|
2582
|
+
(entry) => hasKeys(entry, ["value"]) && Object.hasOwn(entry, "qualifiers") && typeof entry.value === "string" && isQualifierRecord(entry.qualifiers, isQualifierName)
|
|
2583
|
+
)
|
|
2584
|
+
);
|
|
2585
|
+
default:
|
|
2586
|
+
return false;
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
for (const i of rows) {
|
|
2590
|
+
if (!column.isSet(i)) {
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
if (typedefOnly && kinds[i] !== "Typedef") {
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
if (!valid(cellOf(column, i))) {
|
|
2597
|
+
return false;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return true;
|
|
2601
|
+
}
|
|
2602
|
+
function unrecognizedCarried(column, rows, kinds) {
|
|
2603
|
+
if (column.dtype !== "json") {
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
for (const i of rows) {
|
|
2607
|
+
const v = cellOf(column, i);
|
|
2608
|
+
if (v === void 0) {
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
for (const [tag, values] of Object.entries(v)) {
|
|
2615
|
+
const applied = (APPLIED_TAGS.has(tag) || TYPEDEF_TAGS.has(tag)) && !(TYPEDEF_TAGS.has(tag) && kinds[i] !== "Typedef") && !(tag === "instance_of" && kinds[i] !== "Instance");
|
|
2616
|
+
if (applied || tag.length === 0 || !isTrimmed(tag) || !arrayOf(values, (t) => typeof t === "string" && isTrimmed(t))) {
|
|
2617
|
+
return false;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return true;
|
|
2622
|
+
}
|
|
2623
|
+
function synonymTypeScopes(header) {
|
|
2624
|
+
const out = /* @__PURE__ */ new Map();
|
|
2625
|
+
for (const raw of header.synonymtypedef ?? []) {
|
|
2626
|
+
const tokens = tokenize(raw);
|
|
2627
|
+
if (tokens.length > 0 && tokens[0].kind === "word") {
|
|
2628
|
+
const scope = tokens.slice(1).find((t) => t.kind === "word" && SYNONYM_SCOPES.has(t.text));
|
|
2629
|
+
out.set(tokens[0].text, scope?.text ?? null);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return out;
|
|
2633
|
+
}
|
|
2634
|
+
function keptObo(snapshot) {
|
|
2635
|
+
const raw = snapshot.meta.extra.obo;
|
|
2636
|
+
const record = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
2637
|
+
const clauses = (value) => {
|
|
2638
|
+
const out = {};
|
|
2639
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2640
|
+
for (const [tag, values] of Object.entries(value)) {
|
|
2641
|
+
if (Array.isArray(values)) {
|
|
2642
|
+
out[tag] = values.filter((v) => typeof v === "string");
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return out;
|
|
2647
|
+
};
|
|
2648
|
+
const typedefs = {};
|
|
2649
|
+
if (typeof record.typedefs === "object" && record.typedefs !== null && !Array.isArray(record.typedefs)) {
|
|
2650
|
+
for (const [id, frame] of Object.entries(record.typedefs)) {
|
|
2651
|
+
typedefs[id] = clauses(frame);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
const unknownFrames = Array.isArray(record.unknownFrames) ? record.unknownFrames.flatMap(
|
|
2655
|
+
(f) => typeof f === "object" && f !== null && typeof f.type === "string" ? [{ type: f.type, clauses: clauses(f.clauses) }] : []
|
|
2656
|
+
) : [];
|
|
2657
|
+
return { header: clauses(record.header), typedefs, unknownFrames };
|
|
2658
|
+
}
|
|
2659
|
+
function plan(snapshot, options) {
|
|
2660
|
+
const common = resolveExportOptions(options);
|
|
2661
|
+
const obo = resolveOboOptions(options);
|
|
2662
|
+
const notes = [];
|
|
2663
|
+
const note = (code, message, column = null, count = null) => {
|
|
2664
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
2665
|
+
};
|
|
2666
|
+
let fatal = null;
|
|
2667
|
+
const { ids, originals, fatal: idFatal } = planIds(snapshot, common.sanitizeIds, note);
|
|
2668
|
+
fatal ??= idFatal;
|
|
2669
|
+
const folding = pairFolding(snapshot);
|
|
2670
|
+
const { written: written2, edgeStart, edgeOrder, dst } = planEdges(snapshot, folding, common, note);
|
|
2671
|
+
const frameless = framelessNodes(snapshot, written2, {
|
|
2672
|
+
outgoingAllowed: false,
|
|
2673
|
+
changed: (i) => originals.has(i)
|
|
2674
|
+
});
|
|
2675
|
+
const moved = movedNodes(
|
|
2676
|
+
frameless,
|
|
2677
|
+
Array.from(edgeOrder, (e) => dst[e])
|
|
2678
|
+
);
|
|
2679
|
+
if (moved > 0) {
|
|
2680
|
+
note(
|
|
2681
|
+
NODE_ORDER_CODE,
|
|
2682
|
+
`${moved} node${plural(moved)} ${agree(moved, "reads", "read")} back at another position: placeholder nodes have no frame and read back after the written nodes`,
|
|
2683
|
+
null,
|
|
2684
|
+
moved
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
const rows = [];
|
|
2688
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
2689
|
+
if (frameless[i] === 0) {
|
|
2690
|
+
rows.push(i);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
const { kinds, type } = planKinds(snapshot, frameless);
|
|
2694
|
+
const typedefNodes = rows.filter((i) => kinds[i] === "Typedef").length;
|
|
2695
|
+
if (typedefNodes > 0) {
|
|
2696
|
+
note(
|
|
2697
|
+
OBO_LOSS.TYPEDEF_NODES,
|
|
2698
|
+
`${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`,
|
|
2699
|
+
"type",
|
|
2700
|
+
typedefNodes
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
const kept = keptObo(snapshot);
|
|
2704
|
+
const scopes = synonymTypeScopes(kept.header);
|
|
2705
|
+
const labelSlot = slotColumn(snapshot.nodes, "label", "name");
|
|
2706
|
+
const labelColumn = labelSlot.column;
|
|
2707
|
+
const label = labelColumn !== null && rows.every((i) => !labelColumn.isSet(i) || isTrimmed(String(labelColumn.value(i)))) ? labelColumn : null;
|
|
2708
|
+
const tags = planTags(snapshot, rows, kinds, scopes, label);
|
|
2709
|
+
fatal ??= capabilityNotes(snapshot, common, {
|
|
2710
|
+
caps: OBO_CAPABILITIES,
|
|
2711
|
+
label,
|
|
2712
|
+
labelAssumed: labelSlot.assumed,
|
|
2713
|
+
edgeSlot: "a qualifier",
|
|
2714
|
+
note
|
|
2715
|
+
});
|
|
2716
|
+
oboColumnNotes(snapshot, label, tags, note);
|
|
2717
|
+
const properties = planProperties(snapshot, rows, frameless, label, tags, type, note);
|
|
2718
|
+
const relationSlot = slotColumn(snapshot.edges, "kind", RELATION_COLUMN);
|
|
2719
|
+
const relations = planRelations(snapshot, written2, relationSlot.column, obo.relation, note);
|
|
2720
|
+
if (relationSlot.assumed && relationSlot.column !== null) {
|
|
2721
|
+
note(
|
|
2722
|
+
ROLE_ASSUMED_CODE,
|
|
2723
|
+
`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")`,
|
|
2724
|
+
RELATION_COLUMN,
|
|
2725
|
+
relationSlot.column.length - relationSlot.column.nullCount
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
const weights = explicitWeights(snapshot);
|
|
2729
|
+
const { qualifiers, edgeColumns } = planEdgeColumns(snapshot, written2, relationSlot.column, weights, note);
|
|
2730
|
+
emptyColumnNotes(snapshot, rows, frameless, written2, note);
|
|
2731
|
+
const p = {
|
|
2732
|
+
notes,
|
|
2733
|
+
fatal,
|
|
2734
|
+
options: obo,
|
|
2735
|
+
ids,
|
|
2736
|
+
originals,
|
|
2737
|
+
frameless,
|
|
2738
|
+
kinds,
|
|
2739
|
+
label,
|
|
2740
|
+
tags,
|
|
2741
|
+
properties,
|
|
2742
|
+
edgeStart,
|
|
2743
|
+
edgeOrder,
|
|
2744
|
+
dst,
|
|
2745
|
+
relations,
|
|
2746
|
+
qualifiers,
|
|
2747
|
+
edgeColumns,
|
|
2748
|
+
weights,
|
|
2749
|
+
header: [],
|
|
2750
|
+
tail: []
|
|
2751
|
+
};
|
|
2752
|
+
let stats = simulate(p, rows);
|
|
2753
|
+
if (!stats.qualifiersPlaced) {
|
|
2754
|
+
tags.delete("obo.qualifiers");
|
|
2755
|
+
const column = snapshot.nodes.get("obo.qualifiers");
|
|
2756
|
+
if (column !== null) {
|
|
2757
|
+
properties.push({ column, relation: "obo.qualifiers" });
|
|
2758
|
+
notePropertyColumn(note, column, rows, "property_value lines (relation obo.qualifiers)");
|
|
2759
|
+
}
|
|
2760
|
+
stats = simulate(p, rows);
|
|
2761
|
+
}
|
|
2762
|
+
if (stats.duplicates > 0) {
|
|
2763
|
+
note(
|
|
2764
|
+
OBO_LOSS.DUPLICATE_CLAUSE,
|
|
2765
|
+
`${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`,
|
|
2766
|
+
null,
|
|
2767
|
+
stats.duplicates
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
const header = planHeader(snapshot, kept.header, obo, rows, tags, note, stats);
|
|
2771
|
+
const tail = planTail({
|
|
2772
|
+
kept,
|
|
2773
|
+
rows,
|
|
2774
|
+
kinds,
|
|
2775
|
+
ids,
|
|
2776
|
+
properties,
|
|
2777
|
+
relations,
|
|
2778
|
+
edges: edgeOrder,
|
|
2779
|
+
xref: tags.get("xref") ?? null
|
|
2780
|
+
});
|
|
2781
|
+
if (stats.lineEnds > 0) {
|
|
2782
|
+
note(
|
|
2783
|
+
OBO_LOSS.LINE_END,
|
|
2784
|
+
`${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`,
|
|
2785
|
+
null,
|
|
2786
|
+
stats.lineEnds
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
return { ...p, header, tail };
|
|
2790
|
+
}
|
|
2791
|
+
function planTags(snapshot, rows, kinds, scopes, label) {
|
|
2792
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2793
|
+
for (const name of TAG_COLUMNS) {
|
|
2794
|
+
const column = snapshot.nodes.get(name);
|
|
2795
|
+
if (column === null || column === label || column.meta.role !== null && column.meta.role !== "label") {
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
if (name === UNRECOGNIZED ? unrecognizedCarried(column, rows, kinds) : tagCarries(name, column, rows, kinds, scopes)) {
|
|
2799
|
+
tags.set(name, column);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
const def = tags.get("def");
|
|
2803
|
+
const defXrefs = tags.get("def.xrefs");
|
|
2804
|
+
if (defXrefs !== void 0 && (def === void 0 || rows.some((i) => def.isSet(i) !== defXrefs.isSet(i)))) {
|
|
2805
|
+
tags.delete("def.xrefs");
|
|
2806
|
+
}
|
|
2807
|
+
if (def !== void 0 && !tags.has("def.xrefs") && snapshot.nodes.get("def.xrefs") !== null) {
|
|
2808
|
+
tags.delete("def");
|
|
2809
|
+
}
|
|
2810
|
+
const descriptions = tags.get("xref.descriptions");
|
|
2811
|
+
if (descriptions !== void 0 && !rows.every((i) => describedXrefsWritten(tags, descriptions, i))) {
|
|
2812
|
+
tags.delete("xref.descriptions");
|
|
2813
|
+
}
|
|
2814
|
+
return tags;
|
|
2815
|
+
}
|
|
2816
|
+
function describedXrefsWritten(tags, descriptions, row) {
|
|
2817
|
+
const map = cellOf(descriptions, row);
|
|
2818
|
+
if (map === void 0) {
|
|
2819
|
+
return true;
|
|
2820
|
+
}
|
|
2821
|
+
const written2 = /* @__PURE__ */ new Set();
|
|
2822
|
+
const add = (values) => {
|
|
2823
|
+
if (Array.isArray(values)) {
|
|
2824
|
+
for (const v of values) {
|
|
2825
|
+
if (typeof v === "string") {
|
|
2826
|
+
written2.add(v);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
for (const name of ["def.xrefs", "xref"]) {
|
|
2832
|
+
const column = tags.get(name);
|
|
2833
|
+
if (column !== void 0) {
|
|
2834
|
+
add(cellOf(column, row));
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
for (const name of ["synonym", "expand_assertion_to", "expand_expression_to"]) {
|
|
2838
|
+
const column = tags.get(name);
|
|
2839
|
+
const items = column === void 0 ? void 0 : cellOf(column, row);
|
|
2840
|
+
if (Array.isArray(items)) {
|
|
2841
|
+
for (const item of items) {
|
|
2842
|
+
add(item.xrefs);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
return Object.keys(map).every((id) => written2.has(id));
|
|
2847
|
+
}
|
|
2848
|
+
function oboColumnNotes(snapshot, label, tags, note) {
|
|
2849
|
+
for (const [name, column] of [...tags, ...label === null ? [] : [["name", label]]]) {
|
|
2850
|
+
const want = name === "name" ? "string" : OBO_NODE_COLUMNS[name].dtype;
|
|
2851
|
+
if ((want === "string" || want === "dict") && column.dtype !== want || column.dtype === "list" && column.child.dtype !== "string") {
|
|
2852
|
+
const held = column.dtype === "list" ? `${column.child.dtype} items` : column.dtype;
|
|
2853
|
+
note(
|
|
2854
|
+
LOSS.DTYPE,
|
|
2855
|
+
`node column "${column.meta.name}" holds ${held}; it reads back as ${column.dtype === "list" ? "string items" : want}`,
|
|
2856
|
+
column.meta.name,
|
|
2857
|
+
column.length - column.nullCount
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
for (const column of snapshot.graph) {
|
|
2862
|
+
note(
|
|
2863
|
+
GRAPH_COLUMN_AS_METADATA_CODE,
|
|
2864
|
+
`graph column "${column.meta.name}" is written as a header property_value and reads back in meta.extra.obo.header, not as a column`,
|
|
2865
|
+
column.meta.name,
|
|
2866
|
+
column.length - column.nullCount
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function planProperties(snapshot, rows, frameless, label, tags, type, note) {
|
|
2871
|
+
const out = [];
|
|
2872
|
+
const slotted = /* @__PURE__ */ new Set([
|
|
2873
|
+
...tags.values(),
|
|
2874
|
+
...label === null ? [] : [label],
|
|
2875
|
+
...type === null ? [] : [type]
|
|
2876
|
+
]);
|
|
2877
|
+
for (const column of snapshot.nodes) {
|
|
2878
|
+
const { role, name } = column.meta;
|
|
2879
|
+
if (slotted.has(column) || role !== null && UNWRITTEN_ROLES.has(role)) {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
if (name === PLACEHOLDER_COLUMN && rows.every((i) => !column.isSet(i)) && frameless.includes(1)) {
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
const relation = isOboWord(name) && name !== ORIGINAL_ID ? name : wordOf(name.replaceAll(":", "_"));
|
|
2886
|
+
out.push({ column, relation });
|
|
2887
|
+
notePropertyColumn(note, column, rows, `property_value lines (relation ${relation})`);
|
|
2888
|
+
}
|
|
2889
|
+
return out;
|
|
2890
|
+
}
|
|
2891
|
+
function planRelations(snapshot, written2, relation, fallback, note) {
|
|
2892
|
+
const out = new Array(snapshot.edgeCount).fill(fallback);
|
|
2893
|
+
let assumed = 0;
|
|
2894
|
+
let renamed = 0;
|
|
2895
|
+
for (const e of written2) {
|
|
2896
|
+
const value = relation === null ? void 0 : cellOf(relation, e);
|
|
2897
|
+
if (typeof value !== "string") {
|
|
2898
|
+
assumed++;
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
if (isOboWord(value)) {
|
|
2902
|
+
out[e] = value;
|
|
2903
|
+
} else {
|
|
2904
|
+
out[e] = wordOf(value);
|
|
2905
|
+
renamed++;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
if (assumed > 0) {
|
|
2909
|
+
note(
|
|
2910
|
+
OBO_LOSS.RELATION_ASSUMED,
|
|
2911
|
+
`${assumed} edge${plural(assumed)} ${agree(assumed, "has", "have")} no relation; written as ${fallback} (an ontology reads is_a as subclassing)`,
|
|
2912
|
+
relation?.meta.name ?? null,
|
|
2913
|
+
assumed
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
if (renamed > 0) {
|
|
2917
|
+
note(
|
|
2918
|
+
OBO_LOSS.RELATION_RENAMED,
|
|
2919
|
+
`${renamed} relation${plural(renamed)} ${agree(renamed, "is", "are")} not OBO ids; written with "_" in place of the space, "!", "{" or "}"`,
|
|
2920
|
+
relation?.meta.name ?? null,
|
|
2921
|
+
renamed
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
return out;
|
|
2925
|
+
}
|
|
2926
|
+
function planEdgeColumns(snapshot, written2, relation, weights, note) {
|
|
2927
|
+
const qualifiers = qualifierRecords(snapshot, written2);
|
|
2928
|
+
const edgeColumns = [];
|
|
2929
|
+
const count = (column) => written2.filter((e) => column.isSet(e)).length;
|
|
2930
|
+
const merged = qualifiers === null ? "" : " (merged with its values)";
|
|
2931
|
+
for (const column of snapshot.edges) {
|
|
2932
|
+
const { role, name } = column.meta;
|
|
2933
|
+
if (column === relation || column === qualifiers || role === "id" || role !== null && UNWRITTEN_ROLES.has(role)) {
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
const written22 = isQualifierName(name) ? name : wordOf(name.replaceAll(/[=",[\]\\]/g, "_"));
|
|
2937
|
+
edgeColumns.push({ column, name: written22 });
|
|
2938
|
+
const n = count(column);
|
|
2939
|
+
if (n > 0) {
|
|
2940
|
+
note(
|
|
2941
|
+
OBO_LOSS.EDGE_COLUMN_AS_QUALIFIER,
|
|
2942
|
+
`edge column "${name}" is written as the qualifier ${written22} and reads back inside the qualifiers column${merged}`,
|
|
2943
|
+
name,
|
|
2944
|
+
n
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
const explicit = weights.weighted ? written2.filter((e) => weights.isExplicit(e)).length : 0;
|
|
2949
|
+
if (explicit > 0) {
|
|
2950
|
+
note(
|
|
2951
|
+
OBO_LOSS.EDGE_COLUMN_AS_QUALIFIER,
|
|
2952
|
+
`${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`,
|
|
2953
|
+
snapshot.edges.byRole("weight")?.meta.name ?? null,
|
|
2954
|
+
explicit
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
return { qualifiers, edgeColumns };
|
|
2958
|
+
}
|
|
2959
|
+
function qualifierRecords(snapshot, written2) {
|
|
2960
|
+
const qualifiers = snapshot.edges.get(QUALIFIERS_COLUMN);
|
|
2961
|
+
if (qualifiers?.dtype !== "json" || qualifiers.meta.role !== null) {
|
|
2962
|
+
return null;
|
|
2963
|
+
}
|
|
2964
|
+
const records = written2.every(
|
|
2965
|
+
(e) => !qualifiers.isSet(e) || isQualifierRecord(cellOf(qualifiers, e), isQualifierName)
|
|
2966
|
+
);
|
|
2967
|
+
return records ? qualifiers : null;
|
|
2968
|
+
}
|
|
2969
|
+
function planHeader(snapshot, kept, obo, rows, tags, note, stats) {
|
|
2970
|
+
const { values, filled } = headerFields(snapshot, kept, obo.ontology, note);
|
|
2971
|
+
const namespace = tags.get("namespace");
|
|
2972
|
+
if (values.has("default-namespace") && (namespace === void 0 || rows.some((i) => !namespace.isSet(i)))) {
|
|
2973
|
+
values.delete("default-namespace");
|
|
2974
|
+
}
|
|
2975
|
+
const lines = [];
|
|
2976
|
+
const tagsInOrder = [
|
|
2977
|
+
...HEADER_ORDER.filter((t) => filled.has(t)),
|
|
2978
|
+
...[...values.keys()].filter((t) => !filled.has(t))
|
|
2979
|
+
];
|
|
2980
|
+
for (const tag of tagsInOrder) {
|
|
2981
|
+
for (const value of values.get(tag) ?? []) {
|
|
2982
|
+
lines.push(`${escapeTag(tag)}: ${value}`);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
if (lines.length === 0 && rows.length === 0) {
|
|
2986
|
+
lines.push("format-version: 1.4");
|
|
2987
|
+
}
|
|
2988
|
+
for (const column of snapshot.graph) {
|
|
2989
|
+
const relation = isOboWord(column.meta.name) ? column.meta.name : wordOf(column.meta.name);
|
|
2990
|
+
for (const line of propertyLines({ column, relation }, 0, stats)) {
|
|
2991
|
+
lines.push(`property_value: ${line}`);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return lines;
|
|
2995
|
+
}
|
|
2996
|
+
function headerFields(snapshot, kept, ontologyOption, note) {
|
|
2997
|
+
const { meta } = snapshot;
|
|
2998
|
+
const values = new Map(Object.entries(kept).map(([tag, v]) => [tag, v.map(rawValue)]));
|
|
2999
|
+
const filled = /* @__PURE__ */ new Set();
|
|
3000
|
+
const fill = (tag, value) => {
|
|
3001
|
+
if (value !== null && !values.has(tag)) {
|
|
3002
|
+
values.set(tag, [value]);
|
|
3003
|
+
filled.add(tag);
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
if (meta.sourceFormat !== "obo") {
|
|
3007
|
+
fill("format-version", "1.4");
|
|
3008
|
+
fill("date", oboDate(meta.created));
|
|
3009
|
+
fill("saved-by", meta.creator === null ? null : escapeOboValue(meta.creator));
|
|
3010
|
+
}
|
|
3011
|
+
const ontology = ontologyField(ontologyOption, meta.name, values.has("ontology"), note);
|
|
3012
|
+
if (ontology !== null) {
|
|
3013
|
+
if (!values.has("ontology")) {
|
|
3014
|
+
filled.add("ontology");
|
|
3015
|
+
}
|
|
3016
|
+
values.set("ontology", [ontology]);
|
|
3017
|
+
}
|
|
3018
|
+
return { values, filled };
|
|
3019
|
+
}
|
|
3020
|
+
function ontologyField(option, name, kept, note) {
|
|
3021
|
+
if (option !== null) {
|
|
3022
|
+
return option;
|
|
3023
|
+
}
|
|
3024
|
+
if (name === null || name.length === 0 || kept) {
|
|
3025
|
+
return null;
|
|
3026
|
+
}
|
|
3027
|
+
if (/^[A-Za-z0-9_.\-/]+$/.test(name)) {
|
|
3028
|
+
return name;
|
|
3029
|
+
}
|
|
3030
|
+
const slug = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
|
|
3031
|
+
note(
|
|
3032
|
+
OBO_LOSS.ONTOLOGY_NAME,
|
|
3033
|
+
`the graph name ${JSON.stringify(name)} is not an ontology id; written as ${slug}`,
|
|
3034
|
+
null,
|
|
3035
|
+
null
|
|
3036
|
+
);
|
|
3037
|
+
return slug;
|
|
3038
|
+
}
|
|
3039
|
+
function rawValue(raw) {
|
|
3040
|
+
return raw.replaceAll(/\r\n|\r|\n|\f/g, String.raw`\n`);
|
|
3041
|
+
}
|
|
3042
|
+
function escapeTag(tag) {
|
|
3043
|
+
return escapeOboValue(tag).replaceAll(":", String.raw`\:`);
|
|
3044
|
+
}
|
|
3045
|
+
function oboDate(iso) {
|
|
3046
|
+
const m = iso === null ? null : /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?/.exec(iso);
|
|
3047
|
+
if (m === null) {
|
|
3048
|
+
return null;
|
|
3049
|
+
}
|
|
3050
|
+
return `${m[3]}:${m[2]}:${m[1]} ${m[4] ?? "00"}:${m[5] ?? "00"}`;
|
|
3051
|
+
}
|
|
3052
|
+
function planTail(input) {
|
|
3053
|
+
const { kept, rows, properties, relations } = input;
|
|
3054
|
+
const lines = [];
|
|
3055
|
+
const declared = nodeTypedefs(input);
|
|
3056
|
+
keptTypedefLines(kept.typedefs, declared, lines);
|
|
3057
|
+
const declare = (relation, metadata) => {
|
|
3058
|
+
if (declared.has(relation)) {
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
declared.add(relation);
|
|
3062
|
+
lines.push("", "[Typedef]", `id: ${escapeOboWord(relation)}`, `name: ${escapeOboValue(relation)}`);
|
|
3063
|
+
if (metadata) {
|
|
3064
|
+
lines.push("is_metadata_tag: true");
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
const used = /* @__PURE__ */ new Set();
|
|
3068
|
+
for (const e of input.edges) {
|
|
3069
|
+
used.add(relations[e]);
|
|
3070
|
+
}
|
|
3071
|
+
for (const { column, relation } of properties) {
|
|
3072
|
+
if (rows.some((i) => column.isSet(i))) {
|
|
3073
|
+
declare(relation, true);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
for (const relation of used) {
|
|
3077
|
+
declare(relation, false);
|
|
3078
|
+
}
|
|
3079
|
+
for (const frame of kept.unknownFrames) {
|
|
3080
|
+
lines.push("", `[${frame.type.replaceAll(/[\]\r\n]/g, "_")}]`, ...clauseLines(frame.clauses));
|
|
3081
|
+
}
|
|
3082
|
+
return lines;
|
|
3083
|
+
}
|
|
3084
|
+
function nodeTypedefs(input) {
|
|
3085
|
+
const { rows, kinds, ids, xref } = input;
|
|
3086
|
+
const declared = new Set(BUILTIN_RELATIONS);
|
|
3087
|
+
for (const i of rows) {
|
|
3088
|
+
if (kinds[i] !== "Typedef") {
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
declared.add(ids[i]);
|
|
3092
|
+
const xrefs = xref === null ? void 0 : cellOf(xref, i);
|
|
3093
|
+
for (const x of Array.isArray(xrefs) ? xrefs : []) {
|
|
3094
|
+
declared.add(String(x));
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return declared;
|
|
3098
|
+
}
|
|
3099
|
+
function keptTypedefLines(typedefs, declared, lines) {
|
|
3100
|
+
for (const [id, clauses] of Object.entries(typedefs)) {
|
|
3101
|
+
if (declared.has(id) && !BUILTIN_RELATIONS.has(id)) {
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
declared.add(id);
|
|
3105
|
+
lines.push("", "[Typedef]", `id: ${escapeOboWord(id)}`, ...clauseLines(clauses, "id"));
|
|
3106
|
+
for (const xref of clauses.xref ?? []) {
|
|
3107
|
+
declared.add(xref.trim());
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function clauseLines(clauses, skip = null) {
|
|
3112
|
+
return Object.entries(clauses).filter(([tag]) => tag !== skip).flatMap(([tag, values]) => values.map((value) => `${escapeTag(tag)}: ${rawValue(value)}`));
|
|
3113
|
+
}
|
|
3114
|
+
class QualifierCursor {
|
|
3115
|
+
/**
|
|
3116
|
+
* Create a cursor.
|
|
3117
|
+
* @param cell - the frame's `obo.qualifiers` cell, or undefined
|
|
3118
|
+
*/
|
|
3119
|
+
constructor(cell) {
|
|
3120
|
+
this.next = /* @__PURE__ */ new Map();
|
|
3121
|
+
this.entries = cell ?? {};
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* The qualifier block of a clause: the next entry of its tag when that entry is for this value.
|
|
3125
|
+
* @param tag - the tag (or `<tag>.xrefs` for an xref inside a list)
|
|
3126
|
+
* @param value - the clause's value as the column holds it
|
|
3127
|
+
* @returns the block with a leading space, or ""
|
|
3128
|
+
*/
|
|
3129
|
+
block(tag, value) {
|
|
3130
|
+
const list = Object.hasOwn(this.entries, tag) ? this.entries[tag] : void 0;
|
|
3131
|
+
const k = this.next.get(tag) ?? 0;
|
|
3132
|
+
if (list === void 0 || k >= list.length || list[k].value !== value) {
|
|
3133
|
+
return "";
|
|
3134
|
+
}
|
|
3135
|
+
this.next.set(tag, k + 1);
|
|
3136
|
+
return qualifierBlock(qualifierPairs(list[k].qualifiers));
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Whether every entry found its clause.
|
|
3140
|
+
* @returns true when all were placed
|
|
3141
|
+
*/
|
|
3142
|
+
placed() {
|
|
3143
|
+
return Object.entries(this.entries).every(([tag, list]) => (this.next.get(tag) ?? 0) === list.length);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
function counted(s, text) {
|
|
3147
|
+
if (hasLineEnd(text)) {
|
|
3148
|
+
s.lineEnds++;
|
|
3149
|
+
}
|
|
3150
|
+
return text;
|
|
3151
|
+
}
|
|
3152
|
+
function xrefText(f, id, listTag) {
|
|
3153
|
+
let out = escapeOboValue(counted(f.stats, id));
|
|
3154
|
+
const descriptions = f.p.tags.get("xref.descriptions");
|
|
3155
|
+
const map = descriptions === void 0 ? void 0 : cellOf(descriptions, f.row);
|
|
3156
|
+
if (map !== void 0 && Object.hasOwn(map, id) && !f.described.has(id)) {
|
|
3157
|
+
f.described.add(id);
|
|
3158
|
+
out += ` "${escapeOboQuoted(counted(f.stats, map[id]))}"`;
|
|
3159
|
+
}
|
|
3160
|
+
if (listTag !== null) {
|
|
3161
|
+
out += f.cursor.block(listTag, id);
|
|
3162
|
+
}
|
|
3163
|
+
return out;
|
|
3164
|
+
}
|
|
3165
|
+
function xrefList(f, ids, tag) {
|
|
3166
|
+
const field = `${tag}.xrefs`;
|
|
3167
|
+
return `[${ids.map((id) => xrefText(f, id, field)).join(", ")}]`;
|
|
3168
|
+
}
|
|
3169
|
+
function propertyLines(prop, row, stats) {
|
|
3170
|
+
const { column, relation } = prop;
|
|
3171
|
+
const value = cellOf(column, row);
|
|
3172
|
+
if (value === void 0) {
|
|
3173
|
+
return [];
|
|
3174
|
+
}
|
|
3175
|
+
const head = escapeOboWord(relation);
|
|
3176
|
+
const one = (v, dtype) => {
|
|
3177
|
+
let text;
|
|
3178
|
+
let xsd = XSD[dtype] ?? "xsd:string";
|
|
3179
|
+
if (typeof v === "number" && xsd === "xsd:double") {
|
|
3180
|
+
text = xsdDouble(v, dtype);
|
|
3181
|
+
} else if (typeof v === "string" || typeof v === "boolean" || typeof v === "number") {
|
|
3182
|
+
text = textOf(v, dtype);
|
|
3183
|
+
} else {
|
|
3184
|
+
text = JSON.stringify(v) ?? "null";
|
|
3185
|
+
xsd = "xsd:string";
|
|
3186
|
+
}
|
|
3187
|
+
return `${head} "${escapeOboQuoted(counted(stats, text))}" ${xsd}`;
|
|
3188
|
+
};
|
|
3189
|
+
if (column.dtype === "list") {
|
|
3190
|
+
return value.map((item) => one(item, column.child.dtype));
|
|
3191
|
+
}
|
|
3192
|
+
if (column.dtype === "json" || column.meta.components > 1) {
|
|
3193
|
+
return [one(value, "json")];
|
|
3194
|
+
}
|
|
3195
|
+
return [one(value, column.dtype)];
|
|
3196
|
+
}
|
|
3197
|
+
function xsdDouble(v, dtype) {
|
|
3198
|
+
if (Number.isNaN(v)) {
|
|
3199
|
+
return "NaN";
|
|
3200
|
+
}
|
|
3201
|
+
if (!Number.isFinite(v)) {
|
|
3202
|
+
return v > 0 ? "INF" : "-INF";
|
|
3203
|
+
}
|
|
3204
|
+
return formatNumber(v, dtype === "f32" ? "f32" : "f64");
|
|
3205
|
+
}
|
|
3206
|
+
function edgeQualifiers(f, e) {
|
|
3207
|
+
const { p } = f;
|
|
3208
|
+
const pairs = [];
|
|
3209
|
+
if (p.qualifiers !== null) {
|
|
3210
|
+
const record = cellOf(p.qualifiers, e);
|
|
3211
|
+
if (record !== void 0) {
|
|
3212
|
+
pairs.push(...qualifierPairs(record));
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
for (const { column, name } of p.edgeColumns) {
|
|
3216
|
+
pairs.push(...qualifierTexts(column, e).map((text) => [name, text]));
|
|
3217
|
+
}
|
|
3218
|
+
const weight = p.weights.text(e);
|
|
3219
|
+
if (weight !== null) {
|
|
3220
|
+
pairs.push([WEIGHT_QUALIFIER, weight]);
|
|
3221
|
+
}
|
|
3222
|
+
for (const [, value] of pairs) {
|
|
3223
|
+
counted(f.stats, value);
|
|
3224
|
+
}
|
|
3225
|
+
return qualifierBlock(pairs);
|
|
3226
|
+
}
|
|
3227
|
+
function qualifierTexts(column, e) {
|
|
3228
|
+
const value = cellOf(column, e);
|
|
3229
|
+
if (value === void 0) {
|
|
3230
|
+
return [];
|
|
3231
|
+
}
|
|
3232
|
+
if (column.dtype === "list") {
|
|
3233
|
+
return value.map((item) => textOf(item, column.child.dtype));
|
|
3234
|
+
}
|
|
3235
|
+
return [column.dtype === "json" ? JSON.stringify(value) ?? "null" : textOf(value, column.dtype)];
|
|
3236
|
+
}
|
|
3237
|
+
function tagLines(f, tag, column, out) {
|
|
3238
|
+
const value = cellOf(column, f.row);
|
|
3239
|
+
if (value === void 0) {
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
const { cursor, stats } = f;
|
|
3243
|
+
const push = (text, stored, raw = false) => {
|
|
3244
|
+
out.push(`${tag}: ${raw ? text : escapeOboValue(counted(stats, text))}${cursor.block(tag, stored)}`);
|
|
3245
|
+
};
|
|
3246
|
+
if (TEXT_TAGS.has(tag)) {
|
|
3247
|
+
push(value, value);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
if (BOOL_TAGS.has(tag)) {
|
|
3251
|
+
push(value ? "true" : "false", value ? "true" : "false", true);
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
if (ID_LIST_TAGS.has(tag)) {
|
|
3255
|
+
for (const id of value) {
|
|
3256
|
+
push(escapeOboWord(id), id, true);
|
|
3257
|
+
}
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
if (Object.hasOwn(STRUCTURED_TAGS, tag)) {
|
|
3261
|
+
STRUCTURED_TAGS[tag](f, tag, value, out);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
function blockOf(qualifiers) {
|
|
3265
|
+
return qualifiers === void 0 ? "" : qualifierBlock(qualifierPairs(qualifiers));
|
|
3266
|
+
}
|
|
3267
|
+
const STRUCTURED_TAGS = {
|
|
3268
|
+
def(f, _tag, value, out) {
|
|
3269
|
+
const xrefs = f.p.tags.get("def.xrefs");
|
|
3270
|
+
const ids = xrefs === void 0 ? [] : cellOf(xrefs, f.row) ?? [];
|
|
3271
|
+
const text = value;
|
|
3272
|
+
out.push(
|
|
3273
|
+
`def: "${escapeOboQuoted(counted(f.stats, text))}" ${xrefList(f, ids, "def")}${f.cursor.block("def", text)}`
|
|
3274
|
+
);
|
|
3275
|
+
},
|
|
3276
|
+
xref(f, _tag, value, out) {
|
|
3277
|
+
for (const id of value) {
|
|
3278
|
+
out.push(`xref: ${xrefText(f, id, null)}${f.cursor.block("xref", id)}`);
|
|
3279
|
+
}
|
|
3280
|
+
},
|
|
3281
|
+
synonym(f, _tag, value, out) {
|
|
3282
|
+
for (const s of value) {
|
|
3283
|
+
const type = s.type === null ? "" : ` ${escapeOboWord(s.type)}`;
|
|
3284
|
+
out.push(
|
|
3285
|
+
`synonym: "${escapeOboQuoted(counted(f.stats, s.text))}" ${s.scope}${type} ${xrefList(f, s.xrefs, "synonym")}${blockOf(s.qualifiers)}`
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
},
|
|
3289
|
+
intersection_of(_f, _tag, value, out) {
|
|
3290
|
+
for (const x of value) {
|
|
3291
|
+
const relation = x.relation === null ? "" : `${escapeOboWord(x.relation)} `;
|
|
3292
|
+
out.push(`intersection_of: ${relation}${escapeOboWord(x.target)}${blockOf(x.qualifiers)}`);
|
|
3293
|
+
}
|
|
3294
|
+
},
|
|
3295
|
+
property_value(f, _tag, value, out) {
|
|
3296
|
+
for (const x of value) {
|
|
3297
|
+
const datatype = x.datatype === null ? "" : ` ${escapeOboWord(x.datatype)}`;
|
|
3298
|
+
const literal = x.datatype === null && isOboWord(x.value) ? escapeOboWord(x.value) : `"${escapeOboQuoted(counted(f.stats, x.value))}"${datatype}`;
|
|
3299
|
+
out.push(`property_value: ${escapeOboWord(x.relation)} ${literal}${blockOf(x.qualifiers)}`);
|
|
3300
|
+
}
|
|
3301
|
+
},
|
|
3302
|
+
holds_over_chain: chainLines,
|
|
3303
|
+
equivalent_to_chain: chainLines,
|
|
3304
|
+
expand_assertion_to: expansionLines,
|
|
3305
|
+
expand_expression_to: expansionLines,
|
|
3306
|
+
[UNRECOGNIZED](f, _tag, value, out) {
|
|
3307
|
+
for (const [unknown, values] of Object.entries(value)) {
|
|
3308
|
+
for (const v of values) {
|
|
3309
|
+
out.push(`${escapeTag(unknown)}: ${escapeOboValue(counted(f.stats, v))}`);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
function chainLines(f, tag, value, out) {
|
|
3315
|
+
for (const [a, b] of value) {
|
|
3316
|
+
const pair = `${a} ${b}`;
|
|
3317
|
+
out.push(`${tag}: ${escapeOboWord(a)} ${escapeOboWord(b)}${f.cursor.block(tag, pair)}`);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function expansionLines(f, tag, value, out) {
|
|
3321
|
+
for (const x of value) {
|
|
3322
|
+
out.push(
|
|
3323
|
+
`${tag}: "${escapeOboQuoted(counted(f.stats, x.template))}" ${xrefList(f, x.xrefs, tag)}${blockOf(x.qualifiers)}`
|
|
3324
|
+
);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
function frameLines(p, row, stats) {
|
|
3328
|
+
const qualifiersColumn = p.tags.get("obo.qualifiers");
|
|
3329
|
+
const f = {
|
|
3330
|
+
p,
|
|
3331
|
+
row,
|
|
3332
|
+
cursor: new QualifierCursor(qualifiersColumn === void 0 ? void 0 : cellOf(qualifiersColumn, row)),
|
|
3333
|
+
described: /* @__PURE__ */ new Set(),
|
|
3334
|
+
stats
|
|
3335
|
+
};
|
|
3336
|
+
const out = [`[${p.kinds[row]}]`, `id: ${escapeOboWord(p.ids[row])}`];
|
|
3337
|
+
for (const tag of TAG_ORDER) {
|
|
3338
|
+
if (tag === "name") {
|
|
3339
|
+
nameLine(f, out);
|
|
3340
|
+
} else if (tag === "property_value") {
|
|
3341
|
+
propertyValueLines(f, out);
|
|
3342
|
+
} else if (tag === EDGES) {
|
|
3343
|
+
edgeLines(f, out);
|
|
3344
|
+
} else {
|
|
3345
|
+
const column = p.tags.get(tag);
|
|
3346
|
+
if (column !== void 0) {
|
|
3347
|
+
tagLines(f, tag, column, out);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
stats.qualifiersPlaced &&= f.cursor.placed();
|
|
3352
|
+
return out;
|
|
3353
|
+
}
|
|
3354
|
+
function nameLine(f, out) {
|
|
3355
|
+
const name = f.p.label === null ? void 0 : cellOf(f.p.label, f.row);
|
|
3356
|
+
if (typeof name === "string") {
|
|
3357
|
+
out.push(`name: ${escapeOboValue(counted(f.stats, name))}${f.cursor.block("name", name)}`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
function propertyValueLines(f, out) {
|
|
3361
|
+
const { p, row } = f;
|
|
3362
|
+
const original = p.originals.get(row);
|
|
3363
|
+
if (original !== void 0) {
|
|
3364
|
+
out.push(`property_value: ${ORIGINAL_ID} "${escapeOboQuoted(JSON.stringify(original))}" xsd:string`);
|
|
3365
|
+
}
|
|
3366
|
+
const column = p.tags.get("property_value");
|
|
3367
|
+
if (column !== void 0) {
|
|
3368
|
+
tagLines(f, "property_value", column, out);
|
|
3369
|
+
}
|
|
3370
|
+
for (const prop of p.properties) {
|
|
3371
|
+
for (const line of propertyLines(prop, row, f.stats)) {
|
|
3372
|
+
out.push(`property_value: ${line}`);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function edgeLines(f, out) {
|
|
3377
|
+
const { p, row } = f;
|
|
3378
|
+
for (let k = p.edgeStart[row]; k < p.edgeStart[row + 1]; k++) {
|
|
3379
|
+
const e = p.edgeOrder[k];
|
|
3380
|
+
const target = escapeOboWord(p.ids[p.dst[e]]);
|
|
3381
|
+
const relation = p.relations[e];
|
|
3382
|
+
const block = edgeQualifiers(f, e);
|
|
3383
|
+
if (relation === "is_a" || relation === "instance_of" && p.kinds[row] === "Instance") {
|
|
3384
|
+
out.push(`${relation}: ${target}${block}`);
|
|
3385
|
+
} else {
|
|
3386
|
+
out.push(`relationship: ${escapeOboWord(relation)} ${target}${block}`);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
function simulate(p, rows) {
|
|
3391
|
+
const stats = { lineEnds: 0, qualifiersPlaced: true, duplicates: 0 };
|
|
3392
|
+
for (const i of rows) {
|
|
3393
|
+
const lines = frameLines(p, i, stats);
|
|
3394
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3395
|
+
for (let k = 2; k < lines.length; k++) {
|
|
3396
|
+
if (seen.has(lines[k])) {
|
|
3397
|
+
stats.duplicates++;
|
|
3398
|
+
}
|
|
3399
|
+
seen.add(lines[k]);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
return stats;
|
|
3403
|
+
}
|
|
3404
|
+
function* write(p) {
|
|
3405
|
+
if (p.fatal !== null) {
|
|
3406
|
+
throw p.fatal;
|
|
3407
|
+
}
|
|
3408
|
+
yield p.header.map((line) => `${line}
|
|
3409
|
+
`).join("");
|
|
3410
|
+
const stats = { lineEnds: 0, qualifiersPlaced: true };
|
|
3411
|
+
for (let i = 0; i < p.ids.length; i++) {
|
|
3412
|
+
if (p.frameless[i] === 0) {
|
|
3413
|
+
yield `
|
|
3414
|
+
${frameLines(p, i, stats).join("\n")}
|
|
3415
|
+
`;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
if (p.tail.length > 0) {
|
|
3419
|
+
yield `${p.tail.join("\n")}
|
|
3420
|
+
`;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
function* written(snapshot, options) {
|
|
3424
|
+
yield* write(plan(snapshot, options));
|
|
3425
|
+
}
|
|
3426
|
+
const oboExporter = Object.freeze({
|
|
3427
|
+
format: "obo",
|
|
3428
|
+
options: Object.freeze(["ontology", "relation"]),
|
|
3429
|
+
capabilities: OBO_CAPABILITIES,
|
|
3430
|
+
/**
|
|
3431
|
+
* Pre-flight: what export() would lose, without writing anything.
|
|
3432
|
+
* @param snapshot - the snapshot
|
|
3433
|
+
* @param options - format-specific and common options
|
|
3434
|
+
* @returns the notes, empty when the export is exact
|
|
3435
|
+
*/
|
|
3436
|
+
check(snapshot, options) {
|
|
3437
|
+
return Object.freeze([...plan(snapshot, options).notes]);
|
|
3438
|
+
},
|
|
3439
|
+
/**
|
|
3440
|
+
* Write the file as UTF-8 chunks.
|
|
3441
|
+
* @param snapshot - the snapshot
|
|
3442
|
+
* @param options - format-specific and common options
|
|
3443
|
+
* @returns the chunks
|
|
3444
|
+
*/
|
|
3445
|
+
export(snapshot, options) {
|
|
3446
|
+
return encodeChunks(written(snapshot, options));
|
|
3447
|
+
},
|
|
3448
|
+
/**
|
|
3449
|
+
* Write the file as one string.
|
|
3450
|
+
* @param snapshot - the snapshot
|
|
3451
|
+
* @param options - format-specific and common options
|
|
3452
|
+
* @returns the file
|
|
3453
|
+
*/
|
|
3454
|
+
exportToString(snapshot, options) {
|
|
3455
|
+
return joinText(written(snapshot, options));
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
export {
|
|
3459
|
+
OBO_CAPABILITIES as O,
|
|
3460
|
+
OBO_ISSUE as a,
|
|
3461
|
+
OBO_LOSS as b,
|
|
3462
|
+
oboImporter as c,
|
|
3463
|
+
oboExporter as o
|
|
3464
|
+
};
|
|
3465
|
+
//# sourceMappingURL=exporter-BMqk5oxW.js.map
|