@graphty/graph-io 0.3.43 → 0.3.44
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/dist/chunks/approximations-3Q4TsdTh.js +40 -0
- package/dist/chunks/approximations-3Q4TsdTh.js.map +1 -0
- package/dist/chunks/block-JQiwViEt.js +2777 -0
- package/dist/chunks/block-JQiwViEt.js.map +1 -0
- package/dist/chunks/{choose-DvpbdRuz.js → choose-BemygfH8.js} +3 -2
- package/dist/chunks/{choose-DvpbdRuz.js.map → choose-BemygfH8.js.map} +1 -1
- package/dist/chunks/{constants-fT3E8JYY.js → constants-BFUR_CtU.js} +6 -2
- package/dist/chunks/constants-BFUR_CtU.js.map +1 -0
- package/dist/chunks/convert-BcQ8_jMo.js +59 -0
- package/dist/chunks/convert-BcQ8_jMo.js.map +1 -0
- package/dist/chunks/emit-DCwFLts5.js +3456 -0
- package/dist/chunks/emit-DCwFLts5.js.map +1 -0
- package/dist/chunks/{escape-B62ZGM_a.js → escape-B1rMftd9.js} +43 -20
- package/dist/chunks/escape-B1rMftd9.js.map +1 -0
- package/dist/chunks/{export-rKLeGQMo.js → export-D7IGzTG4.js} +196 -792
- package/dist/chunks/export-D7IGzTG4.js.map +1 -0
- package/dist/chunks/{exporter-BWwEuOPT.js → exporter-BtnqYuuj.js} +22 -12
- package/dist/chunks/exporter-BtnqYuuj.js.map +1 -0
- package/dist/chunks/{exporter-yLxKBvLE.js → exporter-ChPyhBOv.js} +4 -3
- package/dist/chunks/{exporter-yLxKBvLE.js.map → exporter-ChPyhBOv.js.map} +1 -1
- package/dist/chunks/{exporter-8hLNemFk.js → exporter-KEPxYUXD.js} +133 -74
- package/dist/chunks/exporter-KEPxYUXD.js.map +1 -0
- package/dist/chunks/{exporter-RVtXVetw.js → exporter-quPUCBiC.js} +1805 -287
- package/dist/chunks/exporter-quPUCBiC.js.map +1 -0
- package/dist/chunks/format-GDmO6m6Q.js +676 -0
- package/dist/chunks/format-GDmO6m6Q.js.map +1 -0
- package/dist/chunks/generic-BL06jrJf.js +1347 -0
- package/dist/chunks/generic-BL06jrJf.js.map +1 -0
- package/dist/chunks/{importer-WrGRZb0N.js → importer-B7r2owVf.js} +449 -102
- package/dist/chunks/importer-B7r2owVf.js.map +1 -0
- package/dist/chunks/{importer-Ht7LNg8L.js → importer-BFmOP96W.js} +116 -296
- package/dist/chunks/importer-BFmOP96W.js.map +1 -0
- package/dist/chunks/{importer-BbNb7eNg.js → importer-C2nzGhdV.js} +1390 -129
- package/dist/chunks/importer-C2nzGhdV.js.map +1 -0
- package/dist/chunks/{importer-Dz7tI8Sa.js → importer-CbX2_rNF.js} +617 -115
- package/dist/chunks/importer-CbX2_rNF.js.map +1 -0
- package/dist/chunks/{importer-D794XUTe.js → importer-CdzeN6X4.js} +31 -8
- package/dist/chunks/importer-CdzeN6X4.js.map +1 -0
- package/dist/chunks/{importer-xZ-3suY1.js → importer-ChKNC_GN.js} +791 -135
- package/dist/chunks/importer-ChKNC_GN.js.map +1 -0
- package/dist/chunks/{importer-C98dpSH4.js → importer-dwoYpSqb.js} +141 -82
- package/dist/chunks/importer-dwoYpSqb.js.map +1 -0
- package/dist/chunks/index-B9fdNpTA.js +4310 -0
- package/dist/chunks/index-B9fdNpTA.js.map +1 -0
- package/dist/chunks/{json-elements-DKSqpNW1.js → json-elements-C4FwgnH5.js} +123 -32
- package/dist/chunks/json-elements-C4FwgnH5.js.map +1 -0
- package/dist/chunks/lexicon-B-knUi-l.js +508 -0
- package/dist/chunks/lexicon-B-knUi-l.js.map +1 -0
- package/dist/chunks/{ontology-export-CtRXjg_i.js → ontology-export-BSpbY5FL.js} +3 -2
- package/dist/chunks/{ontology-export-CtRXjg_i.js.map → ontology-export-BSpbY5FL.js.map} +1 -1
- package/dist/chunks/{records-Cu4bxYOm.js → records-Nwz_rbNt.js} +2 -2
- package/dist/chunks/{records-Cu4bxYOm.js.map → records-Nwz_rbNt.js.map} +1 -1
- package/dist/chunks/styles-Brt9JMfs.js +690 -0
- package/dist/chunks/styles-Brt9JMfs.js.map +1 -0
- package/dist/chunks/styles-_N-C8S-U.js +2102 -0
- package/dist/chunks/styles-_N-C8S-U.js.map +1 -0
- package/dist/chunks/styles-yed-DAfcIkf7.js +842 -0
- package/dist/chunks/styles-yed-DAfcIkf7.js.map +1 -0
- package/dist/chunks/{text-DHOJXnED.js → text-CgzMqHQb.js} +2 -2
- package/dist/chunks/{text-DHOJXnED.js.map → text-CgzMqHQb.js.map} +1 -1
- package/dist/chunks/units-XMZFIFe1.js +19 -0
- package/dist/chunks/units-XMZFIFe1.js.map +1 -0
- package/dist/csv.js +5 -4
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +5 -2
- package/dist/dot.js.map +1 -1
- package/dist/gexf.js +292 -412
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +8 -3031
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +354 -240
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +4 -2
- package/dist/graphml.js.map +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +4 -3
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +4 -2
- package/dist/positions.d.ts +1 -0
- package/dist/positions.js +427 -0
- package/dist/positions.js.map +1 -0
- package/dist/src/common/codes.d.ts +6 -0
- package/dist/src/common/codes.d.ts.map +1 -1
- package/dist/src/common/codes.js +6 -0
- package/dist/src/common/codes.js.map +1 -1
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +4 -2
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/common/export.d.ts +10 -0
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +123 -100
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +8 -1
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.d.ts +12 -9
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +160 -37
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/notes.d.ts +42 -0
- package/dist/src/common/notes.d.ts.map +1 -0
- package/dist/src/common/notes.js +72 -0
- package/dist/src/common/notes.js.map +1 -0
- package/dist/src/common/report.d.ts +18 -0
- package/dist/src/common/report.d.ts.map +1 -1
- package/dist/src/common/report.js +36 -1
- package/dist/src/common/report.js.map +1 -1
- package/dist/src/common/units.d.ts +24 -0
- package/dist/src/common/units.d.ts.map +1 -0
- package/dist/src/common/units.js +26 -0
- package/dist/src/common/units.js.map +1 -0
- package/dist/src/common/xml.d.ts +18 -0
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +42 -5
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/formats/cx/exporter.d.ts +6 -0
- package/dist/src/formats/cx/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx/exporter.js +71 -8
- package/dist/src/formats/cx/exporter.js.map +1 -1
- package/dist/src/formats/cx/importer.d.ts +5 -2
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +102 -72
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx/styles.d.ts +102 -0
- package/dist/src/formats/cx/styles.d.ts.map +1 -0
- package/dist/src/formats/cx/styles.js +225 -0
- package/dist/src/formats/cx/styles.js.map +1 -0
- package/dist/src/formats/cx2/exporter.d.ts +5 -0
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +181 -71
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts +4 -1
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +19 -1
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cx2/styles.d.ts +18 -0
- package/dist/src/formats/cx2/styles.d.ts.map +1 -0
- package/dist/src/formats/cx2/styles.js +24 -0
- package/dist/src/formats/cx2/styles.js.map +1 -0
- package/dist/src/formats/cys/constants.d.ts +2 -0
- package/dist/src/formats/cys/constants.d.ts.map +1 -1
- package/dist/src/formats/cys/constants.js +2 -0
- package/dist/src/formats/cys/constants.js.map +1 -1
- package/dist/src/formats/cys/exporter.d.ts +6 -2
- package/dist/src/formats/cys/exporter.d.ts.map +1 -1
- package/dist/src/formats/cys/exporter.js +215 -53
- package/dist/src/formats/cys/exporter.js.map +1 -1
- package/dist/src/formats/cys/importer.d.ts +2 -1
- package/dist/src/formats/cys/importer.d.ts.map +1 -1
- package/dist/src/formats/cys/importer.js +119 -6
- package/dist/src/formats/cys/importer.js.map +1 -1
- package/dist/src/formats/cys/session.d.ts +2 -0
- package/dist/src/formats/cys/session.d.ts.map +1 -1
- package/dist/src/formats/cys/session.js +5 -0
- package/dist/src/formats/cys/session.js.map +1 -1
- package/dist/src/formats/cys/styles.d.ts +38 -0
- package/dist/src/formats/cys/styles.d.ts.map +1 -0
- package/dist/src/formats/cys/styles.js +213 -0
- package/dist/src/formats/cys/styles.js.map +1 -0
- package/dist/src/formats/cys/tables.d.ts +8 -3
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +88 -34
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts +2 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +195 -81
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +100 -20
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/index.d.ts +1 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -1
- package/dist/src/formats/dot/index.js +1 -0
- package/dist/src/formats/dot/index.js.map +1 -1
- package/dist/src/formats/dot/positions.d.ts +14 -0
- package/dist/src/formats/dot/positions.d.ts.map +1 -0
- package/dist/src/formats/dot/positions.js +70 -0
- package/dist/src/formats/dot/positions.js.map +1 -0
- package/dist/src/formats/dot/styles.d.ts +82 -0
- package/dist/src/formats/dot/styles.d.ts.map +1 -0
- package/dist/src/formats/dot/styles.js +1484 -0
- package/dist/src/formats/dot/styles.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +20 -5
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +8 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +251 -131
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +8 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -1
- package/dist/src/formats/gexf/index.js +10 -2
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gexf/schema.d.ts +5 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +5 -0
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gexf/styles.d.ts +74 -0
- package/dist/src/formats/gexf/styles.d.ts.map +1 -0
- package/dist/src/formats/gexf/styles.js +604 -0
- package/dist/src/formats/gexf/styles.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +1 -5
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +19 -8
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +4 -0
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +2 -1
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +2 -1
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/gml/positions.d.ts +14 -0
- package/dist/src/formats/gml/positions.d.ts.map +1 -0
- package/dist/src/formats/gml/positions.js +46 -0
- package/dist/src/formats/gml/positions.js.map +1 -0
- package/dist/src/formats/gml/styles.d.ts +59 -0
- package/dist/src/formats/gml/styles.d.ts.map +1 -0
- package/dist/src/formats/gml/styles.js +1056 -0
- package/dist/src/formats/gml/styles.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +11 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +151 -62
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/constants.d.ts +4 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -1
- package/dist/src/formats/graphml/constants.js +5 -1
- package/dist/src/formats/graphml/constants.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +133 -65
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts +4 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +100 -44
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/index.d.ts +1 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -1
- package/dist/src/formats/graphml/index.js +1 -0
- package/dist/src/formats/graphml/index.js.map +1 -1
- package/dist/src/formats/graphml/styles-plain.d.ts +19 -0
- package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -0
- package/dist/src/formats/graphml/styles-plain.js +187 -0
- package/dist/src/formats/graphml/styles-plain.js.map +1 -0
- package/dist/src/formats/graphml/styles-yed.d.ts +179 -0
- package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -0
- package/dist/src/formats/graphml/styles-yed.js +1086 -0
- package/dist/src/formats/graphml/styles-yed.js.map +1 -0
- package/dist/src/formats/graphml/styles-yfiles3.d.ts +19 -0
- package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -0
- package/dist/src/formats/graphml/styles-yfiles3.js +508 -0
- package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -1
- package/dist/src/formats/graphml/tree.js +34 -19
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +16 -2
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +83 -12
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts +5 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +68 -25
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +22 -3
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +439 -264
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/positions.d.ts +16 -0
- package/dist/src/formats/json/positions.d.ts.map +1 -0
- package/dist/src/formats/json/positions.js +44 -0
- package/dist/src/formats/json/positions.js.map +1 -0
- package/dist/src/formats/json/styles.d.ts +102 -0
- package/dist/src/formats/json/styles.d.ts.map +1 -0
- package/dist/src/formats/json/styles.js +1753 -0
- package/dist/src/formats/json/styles.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +2 -1
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts +2 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -76
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +4 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +60 -6
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/index.d.ts +2 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -1
- package/dist/src/formats/pajek/index.js +2 -0
- package/dist/src/formats/pajek/index.js.map +1 -1
- package/dist/src/formats/pajek/styles.d.ts +50 -0
- package/dist/src/formats/pajek/styles.d.ts.map +1 -0
- package/dist/src/formats/pajek/styles.js +569 -0
- package/dist/src/formats/pajek/styles.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +6 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +6 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
- package/dist/src/formats/xgmml/columns.js +34 -9
- package/dist/src/formats/xgmml/columns.js.map +1 -1
- package/dist/src/formats/xgmml/constants.d.ts +2 -0
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
- package/dist/src/formats/xgmml/constants.js +3 -1
- package/dist/src/formats/xgmml/constants.js.map +1 -1
- package/dist/src/formats/xgmml/document.d.ts +12 -8
- package/dist/src/formats/xgmml/document.d.ts.map +1 -1
- package/dist/src/formats/xgmml/document.js +56 -13
- package/dist/src/formats/xgmml/document.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +12 -6
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts +3 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +22 -9
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts +2 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +31 -24
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/formats/xgmml/styles.d.ts +104 -0
- package/dist/src/formats/xgmml/styles.d.ts.map +1 -0
- package/dist/src/formats/xgmml/styles.js +310 -0
- package/dist/src/formats/xgmml/styles.js.map +1 -0
- package/dist/src/index.d.ts +9 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/positions/codes.d.ts +31 -0
- package/dist/src/positions/codes.d.ts.map +1 -0
- package/dist/src/positions/codes.js +26 -0
- package/dist/src/positions/codes.js.map +1 -0
- package/dist/src/positions/convert.d.ts +58 -0
- package/dist/src/positions/convert.d.ts.map +1 -0
- package/dist/src/positions/convert.js +107 -0
- package/dist/src/positions/convert.js.map +1 -0
- package/dist/src/positions/extract.d.ts +27 -0
- package/dist/src/positions/extract.d.ts.map +1 -0
- package/dist/src/positions/extract.js +526 -0
- package/dist/src/positions/extract.js.map +1 -0
- package/dist/src/positions/index.d.ts +10 -0
- package/dist/src/positions/index.d.ts.map +1 -0
- package/dist/src/positions/index.js +9 -0
- package/dist/src/positions/index.js.map +1 -0
- package/dist/src/positions/types.d.ts +208 -0
- package/dist/src/positions/types.d.ts.map +1 -0
- package/dist/src/positions/types.js +10 -0
- package/dist/src/positions/types.js.map +1 -0
- package/dist/src/registry.d.ts +30 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +130 -9
- package/dist/src/registry.js.map +1 -1
- package/dist/src/styles/annotations.d.ts +132 -0
- package/dist/src/styles/annotations.d.ts.map +1 -0
- package/dist/src/styles/annotations.js +320 -0
- package/dist/src/styles/annotations.js.map +1 -0
- package/dist/src/styles/budget.d.ts +45 -0
- package/dist/src/styles/budget.d.ts.map +1 -0
- package/dist/src/styles/budget.js +58 -0
- package/dist/src/styles/budget.js.map +1 -0
- package/dist/src/styles/canon/bends.d.ts +32 -0
- package/dist/src/styles/canon/bends.d.ts.map +1 -0
- package/dist/src/styles/canon/bends.js +74 -0
- package/dist/src/styles/canon/bends.js.map +1 -0
- package/dist/src/styles/canon/color-tables.d.ts +22 -0
- package/dist/src/styles/canon/color-tables.d.ts.map +1 -0
- package/dist/src/styles/canon/color-tables.js +369 -0
- package/dist/src/styles/canon/color-tables.js.map +1 -0
- package/dist/src/styles/canon/color.d.ts +66 -0
- package/dist/src/styles/canon/color.d.ts.map +1 -0
- package/dist/src/styles/canon/color.js +548 -0
- package/dist/src/styles/canon/color.js.map +1 -0
- package/dist/src/styles/canon/enums.d.ts +25 -0
- package/dist/src/styles/canon/enums.d.ts.map +1 -0
- package/dist/src/styles/canon/enums.js +187 -0
- package/dist/src/styles/canon/enums.js.map +1 -0
- package/dist/src/styles/canon/graphviz.d.ts +48 -0
- package/dist/src/styles/canon/graphviz.d.ts.map +1 -0
- package/dist/src/styles/canon/graphviz.js +160 -0
- package/dist/src/styles/canon/graphviz.js.map +1 -0
- package/dist/src/styles/canon/index.d.ts +13 -0
- package/dist/src/styles/canon/index.d.ts.map +1 -0
- package/dist/src/styles/canon/index.js +13 -0
- package/dist/src/styles/canon/index.js.map +1 -0
- package/dist/src/styles/canon/number.d.ts +59 -0
- package/dist/src/styles/canon/number.d.ts.map +1 -0
- package/dist/src/styles/canon/number.js +142 -0
- package/dist/src/styles/canon/number.js.map +1 -0
- package/dist/src/styles/canon/site.d.ts +51 -0
- package/dist/src/styles/canon/site.d.ts.map +1 -0
- package/dist/src/styles/canon/site.js +63 -0
- package/dist/src/styles/canon/site.js.map +1 -0
- package/dist/src/styles/canon/text.d.ts +61 -0
- package/dist/src/styles/canon/text.d.ts.map +1 -0
- package/dist/src/styles/canon/text.js +199 -0
- package/dist/src/styles/canon/text.js.map +1 -0
- package/dist/src/styles/codes.d.ts +95 -0
- package/dist/src/styles/codes.d.ts.map +1 -0
- package/dist/src/styles/codes.js +97 -0
- package/dist/src/styles/codes.js.map +1 -0
- package/dist/src/styles/cyjs/cascade.d.ts +134 -0
- package/dist/src/styles/cyjs/cascade.d.ts.map +1 -0
- package/dist/src/styles/cyjs/cascade.js +2252 -0
- package/dist/src/styles/cyjs/cascade.js.map +1 -0
- package/dist/src/styles/cyjs/defaults-table.d.ts +25 -0
- package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -0
- package/dist/src/styles/cyjs/defaults-table.js +76 -0
- package/dist/src/styles/cyjs/defaults-table.js.map +1 -0
- package/dist/src/styles/cyjs/emit.d.ts +82 -0
- package/dist/src/styles/cyjs/emit.d.ts.map +1 -0
- package/dist/src/styles/cyjs/emit.js +790 -0
- package/dist/src/styles/cyjs/emit.js.map +1 -0
- package/dist/src/styles/cyjs/index.d.ts +13 -0
- package/dist/src/styles/cyjs/index.d.ts.map +1 -0
- package/dist/src/styles/cyjs/index.js +13 -0
- package/dist/src/styles/cyjs/index.js.map +1 -0
- package/dist/src/styles/cyjs/live.d.ts +46 -0
- package/dist/src/styles/cyjs/live.d.ts.map +1 -0
- package/dist/src/styles/cyjs/live.js +108 -0
- package/dist/src/styles/cyjs/live.js.map +1 -0
- package/dist/src/styles/cyjs/mappers.d.ts +147 -0
- package/dist/src/styles/cyjs/mappers.d.ts.map +1 -0
- package/dist/src/styles/cyjs/mappers.js +744 -0
- package/dist/src/styles/cyjs/mappers.js.map +1 -0
- package/dist/src/styles/cyjs/selector.d.ts +113 -0
- package/dist/src/styles/cyjs/selector.d.ts.map +1 -0
- package/dist/src/styles/cyjs/selector.js +611 -0
- package/dist/src/styles/cyjs/selector.js.map +1 -0
- package/dist/src/styles/cyjs/stylesheet-text.d.ts +37 -0
- package/dist/src/styles/cyjs/stylesheet-text.d.ts.map +1 -0
- package/dist/src/styles/cyjs/stylesheet-text.js +181 -0
- package/dist/src/styles/cyjs/stylesheet-text.js.map +1 -0
- package/dist/src/styles/cytoscape/emit.d.ts +125 -0
- package/dist/src/styles/cytoscape/emit.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/emit.js +1025 -0
- package/dist/src/styles/cytoscape/emit.js.map +1 -0
- package/dist/src/styles/cytoscape/index.d.ts +10 -0
- package/dist/src/styles/cytoscape/index.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/index.js +10 -0
- package/dist/src/styles/cytoscape/index.js.map +1 -0
- package/dist/src/styles/cytoscape/lexicon.d.ts +49 -0
- package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/lexicon.js +603 -0
- package/dist/src/styles/cytoscape/lexicon.js.map +1 -0
- package/dist/src/styles/cytoscape/mappings.d.ts +171 -0
- package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/mappings.js +460 -0
- package/dist/src/styles/cytoscape/mappings.js.map +1 -0
- package/dist/src/styles/cytoscape/resolve.d.ts +137 -0
- package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/resolve.js +381 -0
- package/dist/src/styles/cytoscape/resolve.js.map +1 -0
- package/dist/src/styles/cytoscape/translate.d.ts +50 -0
- package/dist/src/styles/cytoscape/translate.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/translate.js +156 -0
- package/dist/src/styles/cytoscape/translate.js.map +1 -0
- package/dist/src/styles/defaults.d.ts +90 -0
- package/dist/src/styles/defaults.d.ts.map +1 -0
- package/dist/src/styles/defaults.js +414 -0
- package/dist/src/styles/defaults.js.map +1 -0
- package/dist/src/styles/index.d.ts +15 -0
- package/dist/src/styles/index.d.ts.map +1 -0
- package/dist/src/styles/index.js +14 -0
- package/dist/src/styles/index.js.map +1 -0
- package/dist/src/styles/internal.d.ts +17 -0
- package/dist/src/styles/internal.d.ts.map +1 -0
- package/dist/src/styles/internal.js +16 -0
- package/dist/src/styles/internal.js.map +1 -0
- package/dist/src/styles/io/approximations.d.ts +24 -0
- package/dist/src/styles/io/approximations.d.ts.map +1 -0
- package/dist/src/styles/io/approximations.js +51 -0
- package/dist/src/styles/io/approximations.js.map +1 -0
- package/dist/src/styles/io/block.d.ts +166 -0
- package/dist/src/styles/io/block.d.ts.map +1 -0
- package/dist/src/styles/io/block.js +930 -0
- package/dist/src/styles/io/block.js.map +1 -0
- package/dist/src/styles/io/generic.d.ts +88 -0
- package/dist/src/styles/io/generic.d.ts.map +1 -0
- package/dist/src/styles/io/generic.js +516 -0
- package/dist/src/styles/io/generic.js.map +1 -0
- package/dist/src/styles/io/index.d.ts +8 -0
- package/dist/src/styles/io/index.d.ts.map +1 -0
- package/dist/src/styles/io/index.js +7 -0
- package/dist/src/styles/io/index.js.map +1 -0
- package/dist/src/styles/io/writer.d.ts +80 -0
- package/dist/src/styles/io/writer.d.ts.map +1 -0
- package/dist/src/styles/io/writer.js +596 -0
- package/dist/src/styles/io/writer.js.map +1 -0
- package/dist/src/styles/notes.d.ts +47 -0
- package/dist/src/styles/notes.d.ts.map +1 -0
- package/dist/src/styles/notes.js +140 -0
- package/dist/src/styles/notes.js.map +1 -0
- package/dist/src/styles/plan/classes.d.ts +31 -0
- package/dist/src/styles/plan/classes.d.ts.map +1 -0
- package/dist/src/styles/plan/classes.js +139 -0
- package/dist/src/styles/plan/classes.js.map +1 -0
- package/dist/src/styles/plan/continuous.d.ts +32 -0
- package/dist/src/styles/plan/continuous.d.ts.map +1 -0
- package/dist/src/styles/plan/continuous.js +165 -0
- package/dist/src/styles/plan/continuous.js.map +1 -0
- package/dist/src/styles/plan/discrete.d.ts +39 -0
- package/dist/src/styles/plan/discrete.d.ts.map +1 -0
- package/dist/src/styles/plan/discrete.js +127 -0
- package/dist/src/styles/plan/discrete.js.map +1 -0
- package/dist/src/styles/plan/families.d.ts +31 -0
- package/dist/src/styles/plan/families.d.ts.map +1 -0
- package/dist/src/styles/plan/families.js +95 -0
- package/dist/src/styles/plan/families.js.map +1 -0
- package/dist/src/styles/plan/features.d.ts +58 -0
- package/dist/src/styles/plan/features.d.ts.map +1 -0
- package/dist/src/styles/plan/features.js +185 -0
- package/dist/src/styles/plan/features.js.map +1 -0
- package/dist/src/styles/plan/index.d.ts +7 -0
- package/dist/src/styles/plan/index.d.ts.map +1 -0
- package/dist/src/styles/plan/index.js +7 -0
- package/dist/src/styles/plan/index.js.map +1 -0
- package/dist/src/styles/plan/plan.d.ts +71 -0
- package/dist/src/styles/plan/plan.d.ts.map +1 -0
- package/dist/src/styles/plan/plan.js +893 -0
- package/dist/src/styles/plan/plan.js.map +1 -0
- package/dist/src/styles/plan/resolve.d.ts +137 -0
- package/dist/src/styles/plan/resolve.d.ts.map +1 -0
- package/dist/src/styles/plan/resolve.js +587 -0
- package/dist/src/styles/plan/resolve.js.map +1 -0
- package/dist/src/styles/types.d.ts +702 -0
- package/dist/src/styles/types.d.ts.map +1 -0
- package/dist/src/styles/types.js +10 -0
- package/dist/src/styles/types.js.map +1 -0
- package/dist/src/styles/vocabulary.d.ts +41 -0
- package/dist/src/styles/vocabulary.d.ts.map +1 -0
- package/dist/src/styles/vocabulary.js +140 -0
- package/dist/src/styles/vocabulary.js.map +1 -0
- package/dist/src/types.d.ts +64 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +322 -0
- package/dist/styles.js.map +1 -0
- package/dist/xgmml.js +5 -4
- package/dist/xgmml.js.map +1 -1
- package/package.json +13 -2
- package/src/common/codes.ts +7 -0
- package/src/common/escape.ts +5 -2
- package/src/common/export.ts +179 -140
- package/src/common/input.ts +8 -4
- package/src/common/json-elements.ts +169 -38
- package/src/common/notes.ts +94 -0
- package/src/common/report.ts +41 -1
- package/src/common/units.ts +42 -0
- package/src/common/xml.ts +45 -5
- package/src/formats/cx/exporter.ts +95 -11
- package/src/formats/cx/importer.ts +126 -90
- package/src/formats/cx/styles.ts +304 -0
- package/src/formats/cx2/exporter.ts +234 -77
- package/src/formats/cx2/importer.ts +19 -1
- package/src/formats/cx2/styles.ts +29 -0
- package/src/formats/cys/constants.ts +3 -0
- package/src/formats/cys/exporter.ts +246 -60
- package/src/formats/cys/importer.ts +125 -9
- package/src/formats/cys/session.ts +6 -0
- package/src/formats/cys/styles.ts +254 -0
- package/src/formats/cys/tables.ts +117 -48
- package/src/formats/dot/exporter.ts +262 -126
- package/src/formats/dot/importer.ts +111 -20
- package/src/formats/dot/index.ts +1 -0
- package/src/formats/dot/positions.ts +77 -0
- package/src/formats/dot/styles.ts +1741 -0
- package/src/formats/gexf/exporter.ts +27 -11
- package/src/formats/gexf/importer.ts +308 -158
- package/src/formats/gexf/index.ts +10 -0
- package/src/formats/gexf/schema.ts +5 -0
- package/src/formats/gexf/styles.ts +719 -0
- package/src/formats/gml/exporter.ts +33 -14
- package/src/formats/gml/importer.ts +4 -0
- package/src/formats/gml/index.ts +2 -1
- package/src/formats/gml/positions.ts +52 -0
- package/src/formats/gml/styles.ts +1248 -0
- package/src/formats/gml/syntax.ts +173 -62
- package/src/formats/graphml/constants.ts +5 -0
- package/src/formats/graphml/exporter.ts +163 -71
- package/src/formats/graphml/importer.ts +119 -45
- package/src/formats/graphml/index.ts +1 -0
- package/src/formats/graphml/styles-plain.ts +236 -0
- package/src/formats/graphml/styles-yed.ts +1221 -0
- package/src/formats/graphml/styles-yfiles3.ts +573 -0
- package/src/formats/graphml/tree.ts +36 -20
- package/src/formats/graphml/yfiles.ts +89 -13
- package/src/formats/json/exporter.ts +76 -26
- package/src/formats/json/importer.ts +541 -322
- package/src/formats/json/positions.ts +50 -0
- package/src/formats/json/styles.ts +2082 -0
- package/src/formats/neo4j/exporter.ts +2 -1
- package/src/formats/pajek/exporter.ts +195 -106
- package/src/formats/pajek/importer.ts +78 -9
- package/src/formats/pajek/index.ts +2 -0
- package/src/formats/pajek/styles.ts +685 -0
- package/src/formats/pajek/syntax.ts +7 -0
- package/src/formats/xgmml/columns.ts +39 -9
- package/src/formats/xgmml/constants.ts +3 -0
- package/src/formats/xgmml/document.ts +54 -12
- package/src/formats/xgmml/emit.ts +11 -6
- package/src/formats/xgmml/exporter.ts +32 -13
- package/src/formats/xgmml/importer.ts +43 -29
- package/src/formats/xgmml/styles.ts +400 -0
- package/src/index.ts +16 -0
- package/src/positions/codes.ts +32 -0
- package/src/positions/convert.ts +123 -0
- package/src/positions/extract.ts +619 -0
- package/src/positions/index.ts +23 -0
- package/src/positions/types.ts +221 -0
- package/src/registry.ts +179 -8
- package/src/styles/annotations.ts +478 -0
- package/src/styles/budget.ts +60 -0
- package/src/styles/canon/bends.ts +84 -0
- package/src/styles/canon/color-tables.ts +386 -0
- package/src/styles/canon/color.ts +624 -0
- package/src/styles/canon/enums.ts +211 -0
- package/src/styles/canon/graphviz.ts +189 -0
- package/src/styles/canon/index.ts +13 -0
- package/src/styles/canon/number.ts +152 -0
- package/src/styles/canon/site.ts +87 -0
- package/src/styles/canon/text.ts +221 -0
- package/src/styles/codes.ts +104 -0
- package/src/styles/cyjs/cascade.ts +2670 -0
- package/src/styles/cyjs/defaults-table.ts +82 -0
- package/src/styles/cyjs/emit.ts +951 -0
- package/src/styles/cyjs/index.ts +37 -0
- package/src/styles/cyjs/live.ts +147 -0
- package/src/styles/cyjs/mappers.ts +879 -0
- package/src/styles/cyjs/selector.ts +778 -0
- package/src/styles/cyjs/stylesheet-text.ts +214 -0
- package/src/styles/cytoscape/emit.ts +1264 -0
- package/src/styles/cytoscape/index.ts +40 -0
- package/src/styles/cytoscape/lexicon.ts +656 -0
- package/src/styles/cytoscape/mappings.ts +603 -0
- package/src/styles/cytoscape/resolve.ts +521 -0
- package/src/styles/cytoscape/translate.ts +237 -0
- package/src/styles/defaults.ts +504 -0
- package/src/styles/index.ts +57 -0
- package/src/styles/internal.ts +17 -0
- package/src/styles/io/approximations.ts +47 -0
- package/src/styles/io/block.ts +1138 -0
- package/src/styles/io/generic.ts +687 -0
- package/src/styles/io/index.ts +18 -0
- package/src/styles/io/writer.ts +745 -0
- package/src/styles/notes.ts +165 -0
- package/src/styles/plan/classes.ts +166 -0
- package/src/styles/plan/continuous.ts +218 -0
- package/src/styles/plan/discrete.ts +165 -0
- package/src/styles/plan/families.ts +115 -0
- package/src/styles/plan/features.ts +234 -0
- package/src/styles/plan/index.ts +7 -0
- package/src/styles/plan/plan.ts +1129 -0
- package/src/styles/plan/resolve.ts +685 -0
- package/src/styles/types.ts +814 -0
- package/src/styles/vocabulary.ts +172 -0
- package/src/types.ts +76 -1
- package/dist/chunks/constants-fT3E8JYY.js.map +0 -1
- package/dist/chunks/escape-B62ZGM_a.js.map +0 -1
- package/dist/chunks/export-rKLeGQMo.js.map +0 -1
- package/dist/chunks/exporter-8hLNemFk.js.map +0 -1
- package/dist/chunks/exporter-BWwEuOPT.js.map +0 -1
- package/dist/chunks/exporter-RVtXVetw.js.map +0 -1
- package/dist/chunks/importer-BbNb7eNg.js.map +0 -1
- package/dist/chunks/importer-C98dpSH4.js.map +0 -1
- package/dist/chunks/importer-D794XUTe.js.map +0 -1
- package/dist/chunks/importer-Dz7tI8Sa.js.map +0 -1
- package/dist/chunks/importer-Ht7LNg8L.js.map +0 -1
- package/dist/chunks/importer-WrGRZb0N.js.map +0 -1
- package/dist/chunks/importer-xZ-3suY1.js.map +0 -1
- package/dist/chunks/json-elements-DKSqpNW1.js.map +0 -1
|
@@ -0,0 +1,2670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cytoscape.js style engine: a stylesheet applied to the raw element JSON the way Cytoscape.js 3.34 applies it,
|
|
3
|
+
* giving what Cytoscape.js draws for every node and edge, and the stylesheet's rules as neutral rules.
|
|
4
|
+
*
|
|
5
|
+
* - Elements are taken as `cy.add()` takes them: nodes first, ids made text, duplicates and edges to missing nodes
|
|
6
|
+
* dropped, compound parents linked without cycles.
|
|
7
|
+
* - Comparisons read the RAW element data (a field of mixed types is compared with `===`, not as graph-io's column
|
|
8
|
+
* holds it), and a dotted field reads a nested path.
|
|
9
|
+
* - Beneath the file's rules lie Cytoscape.js's own default rules (edges 3px wide, compound parents gray rectangles,
|
|
10
|
+
* selected elements blue); per property the last matching rule wins; a mapper that fails resets the property to
|
|
11
|
+
* its Cytoscape.js default; an element's own `style` is a bypass over every rule.
|
|
12
|
+
* - Every selector is evaluated through indexes (class, id, value) where one test decides, so a sheet of 20,000
|
|
13
|
+
* per-value rules or a selector of 100,000 class tests costs one pass over the elements it names. Every step spends
|
|
14
|
+
* from the extraction's one counted budget.
|
|
15
|
+
*
|
|
16
|
+
* The rules: a selector a short predicate expresses exactly over graph-io's columns becomes that predicate; any other
|
|
17
|
+
* becomes a membership style column (`member.<n>`) with a short `has` test. A run of three or more rules that each
|
|
18
|
+
* test one class, one id or one value of the same field and only set values becomes ONE mapping rule over a winner
|
|
19
|
+
* column. A `data()` or `mapData()` mapper becomes a passthrough or continuous mapping when graph-io's column gives
|
|
20
|
+
* exactly Cytoscape.js's values, else the mapper's per-element values as a style column.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { GraphSnapshot } from "@graphty/graph-format";
|
|
24
|
+
|
|
25
|
+
import { StyleBudget } from "../budget.js";
|
|
26
|
+
import type { CanonSite } from "../canon/site.js";
|
|
27
|
+
import { STYLE_ISSUE } from "../codes.js";
|
|
28
|
+
import { StyleNotes } from "../notes.js";
|
|
29
|
+
import { canonicalFor, degreeValues, evalContinuous, numberOf, plainCell } from "../plan/resolve.js";
|
|
30
|
+
import type {
|
|
31
|
+
ColumnRef,
|
|
32
|
+
ContinuousPoint,
|
|
33
|
+
ImporterStyles,
|
|
34
|
+
Mapping,
|
|
35
|
+
Predicate,
|
|
36
|
+
ReaderOutput,
|
|
37
|
+
Scalar,
|
|
38
|
+
StyleColumn,
|
|
39
|
+
StyleRule,
|
|
40
|
+
StyleTarget,
|
|
41
|
+
StyleValue,
|
|
42
|
+
StyleWorkBudget,
|
|
43
|
+
} from "../types.js";
|
|
44
|
+
import { CYJS_DEFAULT_STYLESHEET, CYJS_PROPERTY_DEFAULTS } from "./defaults-table.js";
|
|
45
|
+
import {
|
|
46
|
+
applyCyjsMapper,
|
|
47
|
+
CYJS_THROWS,
|
|
48
|
+
type CyjsFlat,
|
|
49
|
+
cyjsNeutral,
|
|
50
|
+
type CyjsParsed,
|
|
51
|
+
cyjsProperties,
|
|
52
|
+
cyjsPropertyName,
|
|
53
|
+
type CyjsPropertySpec,
|
|
54
|
+
cyjsRendered,
|
|
55
|
+
parseCyjsValue,
|
|
56
|
+
} from "./mappers.js";
|
|
57
|
+
import { type CyjsCheck, type CyjsQuery, type CyjsSelector, parseCyjsSelector } from "./selector.js";
|
|
58
|
+
import { type CyjsSheetEntry, parseCyjsStylesheetText } from "./stylesheet-text.js";
|
|
59
|
+
|
|
60
|
+
// ============================================================ elements
|
|
61
|
+
|
|
62
|
+
/** One element as a Cytoscape.js core holds it. */
|
|
63
|
+
export interface CyjsElement {
|
|
64
|
+
/** The element's JSON as given. */
|
|
65
|
+
readonly json: Readonly<Record<string, unknown>>;
|
|
66
|
+
/** Its `data` object as given. */
|
|
67
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
68
|
+
/** Fields Cytoscape.js rewrites on add: an id made text or generated, endpoints and parent made text. */
|
|
69
|
+
readonly overrides: ReadonlyMap<string, unknown>;
|
|
70
|
+
/** The id. */
|
|
71
|
+
readonly id: string;
|
|
72
|
+
/** The classes. */
|
|
73
|
+
readonly classes: ReadonlySet<unknown>;
|
|
74
|
+
/** `:selected`. */
|
|
75
|
+
readonly selected: boolean;
|
|
76
|
+
/** `:selectable`. */
|
|
77
|
+
readonly selectable: boolean;
|
|
78
|
+
/** `:locked`. */
|
|
79
|
+
readonly locked: boolean;
|
|
80
|
+
/** `:grabbable`. */
|
|
81
|
+
readonly grabbable: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The elements of a Cytoscape.js core, with their links. */
|
|
85
|
+
export interface CyjsGraph {
|
|
86
|
+
/** Nodes in the order the core holds them. */
|
|
87
|
+
readonly nodes: readonly CyjsElement[];
|
|
88
|
+
/** Edges in the order the core holds them. */
|
|
89
|
+
readonly edges: readonly CyjsElement[];
|
|
90
|
+
/** Each edge's source node. */
|
|
91
|
+
readonly source: Int32Array;
|
|
92
|
+
/** Each edge's target node. */
|
|
93
|
+
readonly target: Int32Array;
|
|
94
|
+
/** Each node's compound parent, or -1. */
|
|
95
|
+
readonly parent: Int32Array;
|
|
96
|
+
/** Each node's children. */
|
|
97
|
+
readonly children: readonly (readonly number[])[];
|
|
98
|
+
/** Each node's incident edges (a loop once). */
|
|
99
|
+
readonly incident: readonly (readonly number[])[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether a value is a plain object (not an array).
|
|
104
|
+
* @param v - the value
|
|
105
|
+
* @returns true for an object
|
|
106
|
+
*/
|
|
107
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
108
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* An id as Cytoscape.js keeps it: a finite number made text, a non-blank string as it is; null when Cytoscape.js
|
|
113
|
+
* refuses it.
|
|
114
|
+
* @param v - the value
|
|
115
|
+
* @returns the id, or null
|
|
116
|
+
*/
|
|
117
|
+
function idText(v: unknown): string | null {
|
|
118
|
+
if (typeof v === "number" && !Number.isNaN(v)) {
|
|
119
|
+
return String(v);
|
|
120
|
+
}
|
|
121
|
+
if (typeof v === "string" && v !== "" && !/^\s+$/.test(v)) {
|
|
122
|
+
return v;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The flags of an element from its JSON.
|
|
129
|
+
* @param json - the element
|
|
130
|
+
* @param edge - whether it is an edge
|
|
131
|
+
* @returns the selection, lock and grab flags
|
|
132
|
+
*/
|
|
133
|
+
function flags(
|
|
134
|
+
json: Readonly<Record<string, unknown>>,
|
|
135
|
+
edge: boolean,
|
|
136
|
+
): Pick<CyjsElement, "selected" | "selectable" | "locked" | "grabbable"> {
|
|
137
|
+
const pannable = json.pannable === undefined ? edge : Boolean(json.pannable);
|
|
138
|
+
const grabbable = json.grabbable === undefined ? true : Boolean(json.grabbable);
|
|
139
|
+
return {
|
|
140
|
+
selected: Boolean(json.selected),
|
|
141
|
+
selectable: json.selectable === undefined ? true : Boolean(json.selectable),
|
|
142
|
+
locked: Boolean(json.locked),
|
|
143
|
+
grabbable: !pannable && grabbable,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* An element's classes as Cytoscape.js reads them: an array's truthy items, or a string split at whitespace.
|
|
149
|
+
* @param v - the `classes` field
|
|
150
|
+
* @returns the classes
|
|
151
|
+
*/
|
|
152
|
+
function classSet(v: unknown): Set<unknown> {
|
|
153
|
+
const out = new Set<unknown>();
|
|
154
|
+
let items: unknown[] = [];
|
|
155
|
+
if (Array.isArray(v)) {
|
|
156
|
+
items = v;
|
|
157
|
+
} else if (typeof v === "string") {
|
|
158
|
+
items = v.split(/\s+/);
|
|
159
|
+
}
|
|
160
|
+
for (const c of items) {
|
|
161
|
+
if (c) {
|
|
162
|
+
out.add(c);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Each node's compound parent, as Cytoscape.js keeps it: a parent naming no node, an edge, or a descendant of the
|
|
170
|
+
* node itself (which would make a cycle) is dropped. Near-linear in the nodes, however deep the hierarchy.
|
|
171
|
+
* @param nodes - the core's nodes, in order; a dropped parent is recorded in their overrides
|
|
172
|
+
* @param ids - the index of each node id (edge ids map to -1)
|
|
173
|
+
* @returns each node's parent index (-1 for none) and each node's children
|
|
174
|
+
*/
|
|
175
|
+
function parentsOf(
|
|
176
|
+
nodes: readonly CyjsElement[],
|
|
177
|
+
ids: ReadonlyMap<string, number>,
|
|
178
|
+
): { parent: Int32Array; children: number[][] } {
|
|
179
|
+
const parent = new Int32Array(nodes.length).fill(-1);
|
|
180
|
+
const children: number[][] = nodes.map(() => []);
|
|
181
|
+
// root[x] leads to the root of x's parent chain; paths are halved as they are walked
|
|
182
|
+
const root = Int32Array.from(nodes, (_n, i) => i);
|
|
183
|
+
const rootOf = (x: number): number => {
|
|
184
|
+
while (root[x] !== x) {
|
|
185
|
+
root[x] = root[root[x]];
|
|
186
|
+
x = root[x];
|
|
187
|
+
}
|
|
188
|
+
return x;
|
|
189
|
+
};
|
|
190
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
191
|
+
const raw = nodes[i].data.parent;
|
|
192
|
+
if (raw === undefined || raw === null) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const text = typeof raw === "number" && !Number.isNaN(raw) ? String(raw) : raw;
|
|
196
|
+
const p = typeof text === "string" ? ids.get(text) : undefined;
|
|
197
|
+
if (p === undefined || p < 0) {
|
|
198
|
+
(nodes[i].overrides as Map<string, unknown>).set("parent", undefined);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (typeof raw === "number") {
|
|
202
|
+
(nodes[i].overrides as Map<string, unknown>).set("parent", text);
|
|
203
|
+
}
|
|
204
|
+
// a node never becomes its own ancestor: node i has no parent yet, so it is an ancestor of p exactly when it
|
|
205
|
+
// is the root of p's chain, found in near-constant time through the union-find roots
|
|
206
|
+
if (rootOf(p) === i) {
|
|
207
|
+
(nodes[i].overrides as Map<string, unknown>).set("parent", undefined);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
parent[i] = p;
|
|
211
|
+
root[i] = p;
|
|
212
|
+
children[p].push(i);
|
|
213
|
+
}
|
|
214
|
+
return { parent, children };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The elements a Cytoscape.js core holds after `cy.add(elements)`.
|
|
219
|
+
* @param elements - an array of element JSON, or `{ nodes, edges }`
|
|
220
|
+
* @returns the elements and their links
|
|
221
|
+
*/
|
|
222
|
+
export function cyjsGraph(elements: unknown): CyjsGraph {
|
|
223
|
+
const { nodeJson, edgeJson } = elementGroups(elements);
|
|
224
|
+
const ids = new Map<string, number>();
|
|
225
|
+
const make = elementMaker(ids);
|
|
226
|
+
const nodes: CyjsElement[] = [];
|
|
227
|
+
for (const json of nodeJson) {
|
|
228
|
+
const e = make(json, false, new Map());
|
|
229
|
+
if (e !== null) {
|
|
230
|
+
ids.set(e.id, nodes.length);
|
|
231
|
+
nodes.push(e);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const edges: CyjsElement[] = [];
|
|
235
|
+
const src: number[] = [];
|
|
236
|
+
const tgt: number[] = [];
|
|
237
|
+
const edgeIds = new Set<string>();
|
|
238
|
+
for (const json of edgeJson) {
|
|
239
|
+
const overrides = new Map<string, unknown>();
|
|
240
|
+
const e = make(json, true, overrides);
|
|
241
|
+
const ends = e === null || edgeIds.has(e.id) ? null : edgeEnds(e, ids, overrides);
|
|
242
|
+
if (e !== null && ends !== null) {
|
|
243
|
+
edgeIds.add(e.id);
|
|
244
|
+
edges.push(e);
|
|
245
|
+
src.push(ends[0]);
|
|
246
|
+
tgt.push(ends[1]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const id of edgeIds) {
|
|
250
|
+
ids.set(id, -1);
|
|
251
|
+
}
|
|
252
|
+
const { parent, children } = parentsOf(nodes, ids);
|
|
253
|
+
const incident: number[][] = nodes.map(() => []);
|
|
254
|
+
for (let e = 0; e < edges.length; e++) {
|
|
255
|
+
incident[src[e]].push(e);
|
|
256
|
+
if (src[e] !== tgt[e]) {
|
|
257
|
+
incident[tgt[e]].push(e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
nodes,
|
|
262
|
+
edges,
|
|
263
|
+
source: Int32Array.from(src),
|
|
264
|
+
target: Int32Array.from(tgt),
|
|
265
|
+
parent,
|
|
266
|
+
children,
|
|
267
|
+
incident,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The node and edge JSON of `cy.add(elements)`: an array's items by their `group`, else an edge when they name a
|
|
273
|
+
* source and a target; a `{ nodes, edges }` object's by where they are.
|
|
274
|
+
* @param elements - an array of element JSON, or `{ nodes, edges }`
|
|
275
|
+
* @returns the node and edge JSON, in order
|
|
276
|
+
*/
|
|
277
|
+
function elementGroups(elements: unknown): {
|
|
278
|
+
nodeJson: Record<string, unknown>[];
|
|
279
|
+
edgeJson: Record<string, unknown>[];
|
|
280
|
+
} {
|
|
281
|
+
const nodeJson: Record<string, unknown>[] = [];
|
|
282
|
+
const edgeJson: Record<string, unknown>[] = [];
|
|
283
|
+
const take = (item: unknown, group: unknown): void => {
|
|
284
|
+
const g = groupOf(item, group);
|
|
285
|
+
if (g === "nodes") {
|
|
286
|
+
nodeJson.push(item as Record<string, unknown>);
|
|
287
|
+
} else if (g === "edges") {
|
|
288
|
+
edgeJson.push(item as Record<string, unknown>);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
if (Array.isArray(elements)) {
|
|
292
|
+
for (const item of elements) {
|
|
293
|
+
take(item, undefined);
|
|
294
|
+
}
|
|
295
|
+
} else if (isObject(elements)) {
|
|
296
|
+
for (const item of Array.isArray(elements.nodes) ? elements.nodes : []) {
|
|
297
|
+
take(item, "nodes");
|
|
298
|
+
}
|
|
299
|
+
for (const item of Array.isArray(elements.edges) ? elements.edges : []) {
|
|
300
|
+
take(item, "edges");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { nodeJson, edgeJson };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The group an element JSON is added to: the one its place gives, else its own `group`, else an edge when its data
|
|
308
|
+
* names a source and a target.
|
|
309
|
+
* @param item - the element JSON
|
|
310
|
+
* @param group - the group its place gives, or undefined
|
|
311
|
+
* @returns the group, or undefined for no element
|
|
312
|
+
*/
|
|
313
|
+
function groupOf(item: unknown, group: unknown): unknown {
|
|
314
|
+
if (!isObject(item)) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
const data = item.data ?? {};
|
|
318
|
+
if (!isObject(data)) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
const given = group ?? item.group;
|
|
322
|
+
if (given !== undefined && given !== null) {
|
|
323
|
+
return given;
|
|
324
|
+
}
|
|
325
|
+
const ends = data.source !== undefined && data.source !== null && data.target !== undefined && data.target !== null;
|
|
326
|
+
return ends ? "edges" : "nodes";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Makes the elements of one core: an element without an id gets a generated one; one whose id is taken or not an id
|
|
331
|
+
* is dropped.
|
|
332
|
+
* @param ids - the ids taken so far, by node index
|
|
333
|
+
* @returns the maker: the element of a JSON, or null when it is dropped
|
|
334
|
+
*/
|
|
335
|
+
function elementMaker(
|
|
336
|
+
ids: ReadonlyMap<string, number>,
|
|
337
|
+
): (json: Record<string, unknown>, edge: boolean, overrides: Map<string, unknown>) => CyjsElement | null {
|
|
338
|
+
let generated = 0;
|
|
339
|
+
return (json, edge, overrides) => {
|
|
340
|
+
const data = isObject(json.data) ? json.data : {};
|
|
341
|
+
let id: string | null;
|
|
342
|
+
if (data.id === undefined) {
|
|
343
|
+
id = `${String.fromCodePoint(0)}generated-${String(generated++)}`;
|
|
344
|
+
overrides.set("id", id);
|
|
345
|
+
} else {
|
|
346
|
+
id = idText(data.id);
|
|
347
|
+
if (id === null || ids.has(id)) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
if (typeof data.id === "number") {
|
|
351
|
+
overrides.set("id", id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { json, data, overrides, id, classes: classSet(json.classes), ...flags(json, edge) };
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* An edge's source and target nodes; a number endpoint is read as its text.
|
|
360
|
+
* @param e - the edge
|
|
361
|
+
* @param ids - the node ids
|
|
362
|
+
* @param overrides - the edge's data as Cytoscape.js rewrites it
|
|
363
|
+
* @returns the two node indexes, or null when an endpoint names no node
|
|
364
|
+
*/
|
|
365
|
+
function edgeEnds(
|
|
366
|
+
e: CyjsElement,
|
|
367
|
+
ids: ReadonlyMap<string, number>,
|
|
368
|
+
overrides: Map<string, unknown>,
|
|
369
|
+
): [number, number] | null {
|
|
370
|
+
const ends: number[] = [];
|
|
371
|
+
for (const field of ["source", "target"] as const) {
|
|
372
|
+
const raw = e.data[field];
|
|
373
|
+
const text = typeof raw === "number" && !Number.isNaN(raw) ? String(raw) : raw;
|
|
374
|
+
if (typeof raw === "number") {
|
|
375
|
+
overrides.set(field, text);
|
|
376
|
+
}
|
|
377
|
+
const at = typeof text === "string" ? ids.get(text) : undefined;
|
|
378
|
+
if (at !== undefined) {
|
|
379
|
+
ends.push(at);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return ends.length === 2 ? [ends[0], ends[1]] : null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* A data field as a selector reads it (`ele.data(field)`): the field itself, else a dotted path walked step by step.
|
|
387
|
+
* @param e - the element
|
|
388
|
+
* @param field - the field
|
|
389
|
+
* @returns the value, or undefined
|
|
390
|
+
*/
|
|
391
|
+
function selectorField(e: CyjsElement, field: string): unknown {
|
|
392
|
+
if (e.overrides.has(field)) {
|
|
393
|
+
return e.overrides.get(field);
|
|
394
|
+
}
|
|
395
|
+
const direct = e.data[field];
|
|
396
|
+
if (direct !== undefined || !field.includes(".")) {
|
|
397
|
+
return direct;
|
|
398
|
+
}
|
|
399
|
+
let v: unknown = e.data;
|
|
400
|
+
const path = field.split(".");
|
|
401
|
+
let k = 0;
|
|
402
|
+
while (v !== null && v !== undefined && k < path.length) {
|
|
403
|
+
v = (v as Record<string, unknown>)[path[k++]];
|
|
404
|
+
}
|
|
405
|
+
return k === path.length ? v : undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* An element's data as a mapper reads it, with Cytoscape.js's rewrites of id, endpoints and parent.
|
|
410
|
+
* @param e - the element
|
|
411
|
+
* @returns an object to read fields from
|
|
412
|
+
*/
|
|
413
|
+
function mapperData(e: CyjsElement): unknown {
|
|
414
|
+
if (e.overrides.size === 0) {
|
|
415
|
+
return e.data;
|
|
416
|
+
}
|
|
417
|
+
return new Proxy(e.data, {
|
|
418
|
+
get: (target, key) =>
|
|
419
|
+
typeof key === "string" && e.overrides.has(key)
|
|
420
|
+
? e.overrides.get(key)
|
|
421
|
+
: (target as Record<string | symbol, unknown>)[key],
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ============================================================ the stylesheet
|
|
426
|
+
|
|
427
|
+
/** One stylesheet entry ready to apply. */
|
|
428
|
+
interface Entry {
|
|
429
|
+
/** The position in the sheet (default rules first, negative). */
|
|
430
|
+
readonly index: number;
|
|
431
|
+
/** Cytoscape.js's own default rule. */
|
|
432
|
+
readonly tool: boolean;
|
|
433
|
+
/** The selector as written. */
|
|
434
|
+
readonly text: string;
|
|
435
|
+
readonly selector: CyjsSelector;
|
|
436
|
+
readonly properties: readonly (readonly [string, unknown])[];
|
|
437
|
+
readonly dashOnly: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The entries of a stylesheet given as JSON (an array of `{ selector, style }`) or as text.
|
|
442
|
+
* @param style - the stylesheet
|
|
443
|
+
* @param notes - where problems are noted
|
|
444
|
+
* @returns the entries in order
|
|
445
|
+
*/
|
|
446
|
+
function sheetEntries(style: unknown, notes: StyleNotes): CyjsSheetEntry[] {
|
|
447
|
+
if (typeof style === "string") {
|
|
448
|
+
const { entries, stop } = parseCyjsStylesheetText(style);
|
|
449
|
+
if (stop !== null) {
|
|
450
|
+
notes.add(STYLE_ISSUE.UNREADABLE, null, "stylesheet", { sample: stop });
|
|
451
|
+
}
|
|
452
|
+
return entries;
|
|
453
|
+
}
|
|
454
|
+
if (!Array.isArray(style)) {
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
const out: CyjsSheetEntry[] = [];
|
|
458
|
+
for (const item of style) {
|
|
459
|
+
if (!isObject(item)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const props = item.style ?? item.css;
|
|
463
|
+
const properties: [string, unknown][] = [];
|
|
464
|
+
if (isObject(props)) {
|
|
465
|
+
for (const name of Object.keys(props)) {
|
|
466
|
+
properties.push([name, props[name]]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
out.push({ selector: item.selector, properties, dashOnly: false });
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ============================================================ matching
|
|
475
|
+
|
|
476
|
+
/** The most nested combinators a selector may hold before it is noted and matched to nothing. */
|
|
477
|
+
const MAX_SELECTOR_NESTING = 64;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* How deep a query nests.
|
|
481
|
+
* @param q - the query
|
|
482
|
+
* @param depth - the depth so far
|
|
483
|
+
* @returns the depth, capped just above the limit
|
|
484
|
+
*/
|
|
485
|
+
function nesting(q: CyjsQuery, depth = 0): number {
|
|
486
|
+
if (depth > MAX_SELECTOR_NESTING) {
|
|
487
|
+
return depth;
|
|
488
|
+
}
|
|
489
|
+
let max = depth;
|
|
490
|
+
for (const c of q.checks) {
|
|
491
|
+
for (const sub of subQueries(c)) {
|
|
492
|
+
max = Math.max(max, nesting(sub, depth + 1));
|
|
493
|
+
if (max > MAX_SELECTOR_NESTING) {
|
|
494
|
+
return max;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return max;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The queries a check holds.
|
|
503
|
+
* @param c - the check
|
|
504
|
+
* @returns its sub-queries
|
|
505
|
+
*/
|
|
506
|
+
function subQueries(c: CyjsCheck): CyjsQuery[] {
|
|
507
|
+
switch (c.type) {
|
|
508
|
+
case "child":
|
|
509
|
+
case "parent":
|
|
510
|
+
return [c.parent, c.child];
|
|
511
|
+
case "descendant":
|
|
512
|
+
case "ancestor":
|
|
513
|
+
return [c.ancestor, c.descendant];
|
|
514
|
+
case "split":
|
|
515
|
+
return [c.left, c.right, c.subject];
|
|
516
|
+
case "directedEdge":
|
|
517
|
+
case "nodeSource":
|
|
518
|
+
case "nodeTarget":
|
|
519
|
+
return [c.source, c.target];
|
|
520
|
+
case "undirectedEdge":
|
|
521
|
+
return [c.nodes[0], c.nodes[1]];
|
|
522
|
+
case "nodeNeighbor":
|
|
523
|
+
return [c.node, c.neighbor];
|
|
524
|
+
default:
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Cytoscape.js's comparison of a field with a selector's value.
|
|
531
|
+
* @param field - the element's value
|
|
532
|
+
* @param op - the base operator
|
|
533
|
+
* @param not - the `!` flag
|
|
534
|
+
* @param ci - the `@` flag
|
|
535
|
+
* @param value - the selector's value
|
|
536
|
+
* @returns whether it matches
|
|
537
|
+
*/
|
|
538
|
+
function valCmp(field: unknown, op: string, not: boolean, ci: boolean, value: string | number): boolean {
|
|
539
|
+
let a: unknown = field;
|
|
540
|
+
let b: unknown = value;
|
|
541
|
+
const isFieldStr = typeof field === "string";
|
|
542
|
+
const isFieldNum = typeof field === "number" && !Number.isNaN(field);
|
|
543
|
+
let fs: string | null = null;
|
|
544
|
+
let vs: string | null = null;
|
|
545
|
+
if (isFieldStr || typeof value === "string" || ci) {
|
|
546
|
+
fs = !isFieldStr && !isFieldNum ? "" : String(field);
|
|
547
|
+
vs = String(value);
|
|
548
|
+
}
|
|
549
|
+
if (ci && fs !== null && vs !== null) {
|
|
550
|
+
fs = fs.toLowerCase();
|
|
551
|
+
vs = vs.toLowerCase();
|
|
552
|
+
a = fs;
|
|
553
|
+
b = vs;
|
|
554
|
+
}
|
|
555
|
+
let m = false;
|
|
556
|
+
let ineq = false;
|
|
557
|
+
switch (op) {
|
|
558
|
+
case "*=":
|
|
559
|
+
m = fs !== null && vs !== null && fs.includes(vs);
|
|
560
|
+
break;
|
|
561
|
+
case "$=":
|
|
562
|
+
m = fs !== null && vs !== null && fs.endsWith(vs);
|
|
563
|
+
break;
|
|
564
|
+
case "^=":
|
|
565
|
+
m = fs !== null && vs !== null && fs.startsWith(vs);
|
|
566
|
+
break;
|
|
567
|
+
case "=":
|
|
568
|
+
m = a === b;
|
|
569
|
+
break;
|
|
570
|
+
case ">":
|
|
571
|
+
ineq = true;
|
|
572
|
+
m = (a as number) > (b as number);
|
|
573
|
+
break;
|
|
574
|
+
case ">=":
|
|
575
|
+
ineq = true;
|
|
576
|
+
m = (a as number) >= (b as number);
|
|
577
|
+
break;
|
|
578
|
+
case "<":
|
|
579
|
+
ineq = true;
|
|
580
|
+
m = (a as number) < (b as number);
|
|
581
|
+
break;
|
|
582
|
+
case "<=":
|
|
583
|
+
ineq = true;
|
|
584
|
+
m = (a as number) <= (b as number);
|
|
585
|
+
break;
|
|
586
|
+
default:
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
if (not && ((field !== null && field !== undefined) || !ineq)) {
|
|
590
|
+
m = !m;
|
|
591
|
+
}
|
|
592
|
+
return m;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** The checks of an element's place in the graph: its parents, children and edges. */
|
|
596
|
+
const STRUCTURE_CHECKS: ReadonlySet<string> = new Set([
|
|
597
|
+
"child",
|
|
598
|
+
"parent",
|
|
599
|
+
"descendant",
|
|
600
|
+
"ancestor",
|
|
601
|
+
"split",
|
|
602
|
+
"directedEdge",
|
|
603
|
+
"undirectedEdge",
|
|
604
|
+
"nodeSource",
|
|
605
|
+
"nodeTarget",
|
|
606
|
+
"nodeNeighbor",
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
/** States whose value changes while Cytoscape.js runs; they are evaluated as at load and noted. */
|
|
610
|
+
const RUNTIME_STATES: ReadonlySet<string> = new Set([
|
|
611
|
+
"active",
|
|
612
|
+
"inactive",
|
|
613
|
+
"grabbed",
|
|
614
|
+
"free",
|
|
615
|
+
"animated",
|
|
616
|
+
"unanimated",
|
|
617
|
+
"visible",
|
|
618
|
+
"hidden",
|
|
619
|
+
"transparent",
|
|
620
|
+
"backgrounding",
|
|
621
|
+
"nonbackgrounding",
|
|
622
|
+
]);
|
|
623
|
+
|
|
624
|
+
/** Thrown inside a selector match when the counted budget runs out partway through it. */
|
|
625
|
+
class BudgetStop extends Error {}
|
|
626
|
+
|
|
627
|
+
/** How many units of work a match does between checks of the budget and the signal. */
|
|
628
|
+
const CHECK_EVERY = 4096;
|
|
629
|
+
|
|
630
|
+
/** Evaluates selectors over one graph. */
|
|
631
|
+
class Matcher {
|
|
632
|
+
readonly degree: Int32Array;
|
|
633
|
+
readonly indegree: Int32Array;
|
|
634
|
+
readonly outdegree: Int32Array;
|
|
635
|
+
work = 0;
|
|
636
|
+
/** Where the work is charged while a match runs; null between matches. */
|
|
637
|
+
#charge: { kind: StyleTarget; budget: StyleWorkBudget; signal?: AbortSignal } | null = null;
|
|
638
|
+
#charged = 0;
|
|
639
|
+
/**
|
|
640
|
+
* The answers of nested sub-queries for one match, per element: 0 unknown, 1 true, 2 false. Each sub-query is
|
|
641
|
+
* then evaluated at most once per element, so a nested compound selector costs polynomial, not exponential, time.
|
|
642
|
+
*/
|
|
643
|
+
readonly #memo = { node: new Map<CyjsQuery, Uint8Array>(), edge: new Map<CyjsQuery, Uint8Array>() };
|
|
644
|
+
|
|
645
|
+
constructor(
|
|
646
|
+
readonly g: CyjsGraph,
|
|
647
|
+
readonly notes: StyleNotes,
|
|
648
|
+
) {
|
|
649
|
+
const n = g.nodes.length;
|
|
650
|
+
this.degree = new Int32Array(n);
|
|
651
|
+
this.indegree = new Int32Array(n);
|
|
652
|
+
this.outdegree = new Int32Array(n);
|
|
653
|
+
for (let e = 0; e < g.edges.length; e++) {
|
|
654
|
+
const s = g.source[e];
|
|
655
|
+
const t = g.target[e];
|
|
656
|
+
this.degree[s] += s === t ? 2 : 1;
|
|
657
|
+
if (s !== t) {
|
|
658
|
+
this.degree[t]++;
|
|
659
|
+
}
|
|
660
|
+
this.outdegree[s]++;
|
|
661
|
+
this.indegree[t]++;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Whether an element matches a query.
|
|
667
|
+
* @param q - the query
|
|
668
|
+
* @param kind - the element's kind
|
|
669
|
+
* @param i - the element
|
|
670
|
+
* @returns true when every check matches
|
|
671
|
+
*/
|
|
672
|
+
query(q: CyjsQuery, kind: StyleTarget, i: number): boolean {
|
|
673
|
+
for (const c of q.checks) {
|
|
674
|
+
this.work++;
|
|
675
|
+
if (this.#charge !== null && this.work - this.#charged >= CHECK_EVERY) {
|
|
676
|
+
this.settle();
|
|
677
|
+
}
|
|
678
|
+
if (!this.check(c, kind, i)) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Starts one selector match: its work is charged to the budget as it runs, and the sub-query answers are fresh.
|
|
687
|
+
* @param kind - the target the work is charged to
|
|
688
|
+
* @param budget - the counted budget
|
|
689
|
+
* @param signal - cancels the match
|
|
690
|
+
*/
|
|
691
|
+
begin(kind: StyleTarget, budget: StyleWorkBudget, signal?: AbortSignal): void {
|
|
692
|
+
this.#charge = { kind, budget, signal };
|
|
693
|
+
this.#charged = this.work;
|
|
694
|
+
this.#memo.node.clear();
|
|
695
|
+
this.#memo.edge.clear();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Ends a match: its remaining work is charged (never throwing) and its sub-query answers are dropped. */
|
|
699
|
+
end(): void {
|
|
700
|
+
const charge = this.#charge;
|
|
701
|
+
this.#charge = null;
|
|
702
|
+
charge?.budget.spend(charge.kind, this.work - this.#charged);
|
|
703
|
+
this.#charged = this.work;
|
|
704
|
+
this.#memo.node.clear();
|
|
705
|
+
this.#memo.edge.clear();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Charges the work done since the last charge; throws BudgetStop once the budget is exhausted.
|
|
710
|
+
* @throws BudgetStop - when the target's budget is exhausted
|
|
711
|
+
*/
|
|
712
|
+
settle(): void {
|
|
713
|
+
const charge = this.#charge;
|
|
714
|
+
if (charge === null) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
charge.budget.spend(charge.kind, this.work - this.#charged);
|
|
718
|
+
this.#charged = this.work;
|
|
719
|
+
charge.signal?.throwIfAborted();
|
|
720
|
+
if (charge.budget.exhausted(charge.kind)) {
|
|
721
|
+
throw new BudgetStop();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Whether an element matches a nested sub-query, remembered for the rest of the match.
|
|
727
|
+
* @param q - the sub-query
|
|
728
|
+
* @param kind - the element's kind
|
|
729
|
+
* @param i - the element
|
|
730
|
+
* @returns true when every check matches
|
|
731
|
+
*/
|
|
732
|
+
sub(q: CyjsQuery, kind: StyleTarget, i: number): boolean {
|
|
733
|
+
const memo = this.#memo[kind];
|
|
734
|
+
let seen = memo.get(q);
|
|
735
|
+
if (seen === undefined) {
|
|
736
|
+
seen = new Uint8Array(kind === "node" ? this.g.nodes.length : this.g.edges.length);
|
|
737
|
+
memo.set(q, seen);
|
|
738
|
+
}
|
|
739
|
+
if (seen[i] !== 0) {
|
|
740
|
+
return seen[i] === 1;
|
|
741
|
+
}
|
|
742
|
+
const m = this.query(q, kind, i);
|
|
743
|
+
seen[i] = m ? 1 : 2;
|
|
744
|
+
return m;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Whether an element matches one check.
|
|
749
|
+
* @param c - the check
|
|
750
|
+
* @param kind - the element's kind
|
|
751
|
+
* @param i - the element
|
|
752
|
+
* @returns true when it matches
|
|
753
|
+
*/
|
|
754
|
+
check(c: CyjsCheck, kind: StyleTarget, i: number): boolean {
|
|
755
|
+
return STRUCTURE_CHECKS.has(c.type) ? this.structureCheck(c, kind, i) : this.elementCheck(c, kind, i);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Whether an element matches a check of its own: its group, state, id, classes or data.
|
|
760
|
+
* @param c - the check
|
|
761
|
+
* @param kind - the element's kind
|
|
762
|
+
* @param i - the element
|
|
763
|
+
* @returns true when it matches
|
|
764
|
+
*/
|
|
765
|
+
private elementCheck(c: CyjsCheck, kind: StyleTarget, i: number): boolean {
|
|
766
|
+
const { g } = this;
|
|
767
|
+
const e = kind === "node" ? g.nodes[i] : g.edges[i];
|
|
768
|
+
switch (c.type) {
|
|
769
|
+
case "group":
|
|
770
|
+
return c.value === "*" || c.value === `${kind}s`;
|
|
771
|
+
case "state":
|
|
772
|
+
return this.state(c.value, kind, i, e);
|
|
773
|
+
case "id":
|
|
774
|
+
return e.id === c.value;
|
|
775
|
+
case "class":
|
|
776
|
+
return e.classes.has(c.value);
|
|
777
|
+
case "exist":
|
|
778
|
+
return selectorField(e, c.field) !== undefined;
|
|
779
|
+
case "bool": {
|
|
780
|
+
const v = selectorField(e, c.field);
|
|
781
|
+
if (c.op === "?") {
|
|
782
|
+
return Boolean(v);
|
|
783
|
+
}
|
|
784
|
+
return c.op === "!" ? !v : v === undefined;
|
|
785
|
+
}
|
|
786
|
+
case "compare":
|
|
787
|
+
return valCmp(selectorField(e, c.field), c.op, c.not, c.ci, c.value);
|
|
788
|
+
case "meta": {
|
|
789
|
+
const v = kind === "node" ? this[c.field][i] : undefined;
|
|
790
|
+
return valCmp(v, c.op, c.not, c.ci, c.value);
|
|
791
|
+
}
|
|
792
|
+
case "true":
|
|
793
|
+
return true;
|
|
794
|
+
default:
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Whether an element matches a check of its place in the graph: its parents, children and edges.
|
|
801
|
+
* @param c - the check
|
|
802
|
+
* @param kind - the element's kind
|
|
803
|
+
* @param i - the element
|
|
804
|
+
* @returns true when it matches
|
|
805
|
+
*/
|
|
806
|
+
private structureCheck(c: CyjsCheck, kind: StyleTarget, i: number): boolean {
|
|
807
|
+
const { g } = this;
|
|
808
|
+
switch (c.type) {
|
|
809
|
+
case "child":
|
|
810
|
+
return (
|
|
811
|
+
this.sub(c.child, kind, i) &&
|
|
812
|
+
kind === "node" &&
|
|
813
|
+
g.parent[i] >= 0 &&
|
|
814
|
+
this.sub(c.parent, "node", g.parent[i])
|
|
815
|
+
);
|
|
816
|
+
case "parent":
|
|
817
|
+
return (
|
|
818
|
+
this.sub(c.parent, kind, i) &&
|
|
819
|
+
kind === "node" &&
|
|
820
|
+
g.children[i].some((k) => this.sub(c.child, "node", k))
|
|
821
|
+
);
|
|
822
|
+
case "descendant":
|
|
823
|
+
return this.sub(c.descendant, kind, i) && kind === "node" && this.someAncestor(i, c.ancestor);
|
|
824
|
+
case "ancestor":
|
|
825
|
+
return this.sub(c.ancestor, kind, i) && kind === "node" && this.someDescendant(i, c.descendant);
|
|
826
|
+
case "split":
|
|
827
|
+
return this.sub(c.subject, kind, i) && this.sub(c.left, kind, i) && this.sub(c.right, kind, i);
|
|
828
|
+
case "directedEdge":
|
|
829
|
+
return (
|
|
830
|
+
kind === "edge" &&
|
|
831
|
+
this.sub(c.source, "node", g.source[i]) &&
|
|
832
|
+
this.sub(c.target, "node", g.target[i])
|
|
833
|
+
);
|
|
834
|
+
case "undirectedEdge": {
|
|
835
|
+
if (kind !== "edge") {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
const [a, b] = c.nodes;
|
|
839
|
+
const s = g.source[i];
|
|
840
|
+
const t = g.target[i];
|
|
841
|
+
return (
|
|
842
|
+
(this.sub(a, "node", s) && this.sub(b, "node", t)) ||
|
|
843
|
+
(this.sub(b, "node", s) && this.sub(a, "node", t))
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
case "nodeSource":
|
|
847
|
+
return (
|
|
848
|
+
kind === "node" &&
|
|
849
|
+
this.sub(c.source, kind, i) &&
|
|
850
|
+
g.incident[i].some((ed) => g.source[ed] === i && this.sub(c.target, "node", g.target[ed]))
|
|
851
|
+
);
|
|
852
|
+
case "nodeTarget":
|
|
853
|
+
return (
|
|
854
|
+
kind === "node" &&
|
|
855
|
+
this.sub(c.target, kind, i) &&
|
|
856
|
+
g.incident[i].some((ed) => g.target[ed] === i && this.sub(c.source, "node", g.source[ed]))
|
|
857
|
+
);
|
|
858
|
+
case "nodeNeighbor":
|
|
859
|
+
return (
|
|
860
|
+
kind === "node" &&
|
|
861
|
+
this.sub(c.node, kind, i) &&
|
|
862
|
+
g.incident[i].some((ed) => {
|
|
863
|
+
const other = g.source[ed] === i ? g.target[ed] : g.source[ed];
|
|
864
|
+
return this.sub(c.neighbor, "node", other);
|
|
865
|
+
})
|
|
866
|
+
);
|
|
867
|
+
default:
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Whether an ancestor of a node matches a query (cycle-safe: parents never form a cycle).
|
|
874
|
+
* @param i - the node
|
|
875
|
+
* @param q - the query
|
|
876
|
+
* @returns true when one does
|
|
877
|
+
*/
|
|
878
|
+
someAncestor(i: number, q: CyjsQuery): boolean {
|
|
879
|
+
for (let a = this.g.parent[i]; a >= 0; a = this.g.parent[a]) {
|
|
880
|
+
if (this.sub(q, "node", a)) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Whether a descendant of a node matches a query, walked without recursion.
|
|
889
|
+
* @param i - the node
|
|
890
|
+
* @param q - the query
|
|
891
|
+
* @returns true when one does
|
|
892
|
+
*/
|
|
893
|
+
someDescendant(i: number, q: CyjsQuery): boolean {
|
|
894
|
+
const stack = [...this.g.children[i]];
|
|
895
|
+
while (stack.length > 0) {
|
|
896
|
+
const k = stack.pop() as number;
|
|
897
|
+
if (this.sub(q, "node", k)) {
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
for (const c of this.g.children[k]) {
|
|
901
|
+
stack.push(c);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* A state selector, evaluated as at load.
|
|
909
|
+
* @param s - the state
|
|
910
|
+
* @param kind - the element's kind
|
|
911
|
+
* @param i - the element
|
|
912
|
+
* @param e - the element
|
|
913
|
+
* @returns whether the element is in the state
|
|
914
|
+
*/
|
|
915
|
+
state(s: string, kind: StyleTarget, i: number, e: CyjsElement): boolean {
|
|
916
|
+
const { g } = this;
|
|
917
|
+
if (RUNTIME_STATES.has(s)) {
|
|
918
|
+
this.notes.add(STYLE_ISSUE.STATE_ASSUMED, kind, `:${s}`, { sample: `:${s}` });
|
|
919
|
+
}
|
|
920
|
+
const node = kind === "node";
|
|
921
|
+
switch (s) {
|
|
922
|
+
case "selected":
|
|
923
|
+
return e.selected;
|
|
924
|
+
case "unselected":
|
|
925
|
+
return !e.selected;
|
|
926
|
+
case "selectable":
|
|
927
|
+
return e.selectable;
|
|
928
|
+
case "unselectable":
|
|
929
|
+
return !e.selectable;
|
|
930
|
+
case "locked":
|
|
931
|
+
return e.locked;
|
|
932
|
+
case "unlocked":
|
|
933
|
+
return !e.locked;
|
|
934
|
+
case "grabbable":
|
|
935
|
+
return e.grabbable;
|
|
936
|
+
case "ungrabbable":
|
|
937
|
+
return !e.grabbable;
|
|
938
|
+
case "free":
|
|
939
|
+
case "inside":
|
|
940
|
+
case "unanimated":
|
|
941
|
+
case "inactive":
|
|
942
|
+
case "nonbackgrounding":
|
|
943
|
+
case "visible":
|
|
944
|
+
return true;
|
|
945
|
+
case "parent":
|
|
946
|
+
return node && g.children[i].length > 0;
|
|
947
|
+
case "childless":
|
|
948
|
+
return node && g.children[i].length === 0;
|
|
949
|
+
case "child":
|
|
950
|
+
case "nonorphan":
|
|
951
|
+
return node && g.parent[i] >= 0;
|
|
952
|
+
case "orphan":
|
|
953
|
+
return node && g.parent[i] < 0;
|
|
954
|
+
case "compound":
|
|
955
|
+
return node
|
|
956
|
+
? g.children[i].length > 0
|
|
957
|
+
: g.children[g.source[i]].length > 0 || g.children[g.target[i]].length > 0;
|
|
958
|
+
case "loop":
|
|
959
|
+
return !node && g.source[i] === g.target[i];
|
|
960
|
+
case "simple":
|
|
961
|
+
return !node && g.source[i] !== g.target[i];
|
|
962
|
+
default:
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** Indexes of one target's elements: by class, by id, and by value per field (built on first use). */
|
|
969
|
+
class Indexes {
|
|
970
|
+
readonly byClass = new Map<unknown, number[]>();
|
|
971
|
+
readonly byId = new Map<string, number>();
|
|
972
|
+
readonly byValue = new Map<string, Map<string, number[]>>();
|
|
973
|
+
/** Per element, the stamp of the last selector that matched it. */
|
|
974
|
+
readonly marks: Int32Array;
|
|
975
|
+
/** The last stamp handed out. */
|
|
976
|
+
stamp = 0;
|
|
977
|
+
|
|
978
|
+
constructor(
|
|
979
|
+
readonly elements: readonly CyjsElement[],
|
|
980
|
+
readonly spend: (ops: number) => void,
|
|
981
|
+
) {
|
|
982
|
+
this.marks = new Int32Array(elements.length);
|
|
983
|
+
for (let i = 0; i < elements.length; i++) {
|
|
984
|
+
for (const c of elements[i].classes) {
|
|
985
|
+
let list = this.byClass.get(c);
|
|
986
|
+
if (list === undefined) {
|
|
987
|
+
list = [];
|
|
988
|
+
this.byClass.set(c, list);
|
|
989
|
+
}
|
|
990
|
+
list.push(i);
|
|
991
|
+
}
|
|
992
|
+
if (!this.byId.has(elements[i].id)) {
|
|
993
|
+
this.byId.set(elements[i].id, i);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
spend(elements.length);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* The elements whose field is `===` a value.
|
|
1001
|
+
* @param field - the field
|
|
1002
|
+
* @param value - the value
|
|
1003
|
+
* @returns the element indexes
|
|
1004
|
+
*/
|
|
1005
|
+
equal(field: string, value: string | number): readonly number[] {
|
|
1006
|
+
let m = this.byValue.get(field);
|
|
1007
|
+
if (m === undefined) {
|
|
1008
|
+
m = new Map();
|
|
1009
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
1010
|
+
const v = selectorField(this.elements[i], field);
|
|
1011
|
+
if (typeof v !== "string" && typeof v !== "number") {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const k = `${typeof v}:${String(v)}`;
|
|
1015
|
+
let list = m.get(k);
|
|
1016
|
+
if (list === undefined) {
|
|
1017
|
+
list = [];
|
|
1018
|
+
m.set(k, list);
|
|
1019
|
+
}
|
|
1020
|
+
list.push(i);
|
|
1021
|
+
}
|
|
1022
|
+
this.byValue.set(field, m);
|
|
1023
|
+
this.spend(this.elements.length);
|
|
1024
|
+
}
|
|
1025
|
+
if (typeof value === "number" && Number.isNaN(value)) {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
return m.get(`${typeof value}:${String(value)}`) ?? [];
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* The candidates of one query for a target: the element list of its most selective indexed check, or null when
|
|
1034
|
+
* every element must be tested.
|
|
1035
|
+
* @param q - the query
|
|
1036
|
+
* @param kind - the target
|
|
1037
|
+
* @param ix - the target's indexes
|
|
1038
|
+
* @returns the candidates, or null
|
|
1039
|
+
*/
|
|
1040
|
+
function candidates(q: CyjsQuery, kind: StyleTarget, ix: Indexes): readonly number[] | null {
|
|
1041
|
+
let best: readonly number[] | null = null;
|
|
1042
|
+
for (const c of q.checks) {
|
|
1043
|
+
let list: readonly number[] | null = null;
|
|
1044
|
+
if (c.type === "group" && c.value !== "*" && c.value !== `${kind}s`) {
|
|
1045
|
+
return [];
|
|
1046
|
+
}
|
|
1047
|
+
if (c.type === "class") {
|
|
1048
|
+
list = ix.byClass.get(c.value) ?? [];
|
|
1049
|
+
} else if (c.type === "id") {
|
|
1050
|
+
const at = ix.byId.get(c.value);
|
|
1051
|
+
list = at === undefined ? [] : [at];
|
|
1052
|
+
} else if (c.type === "compare" && c.op === "=" && !c.not && !c.ci) {
|
|
1053
|
+
list = ix.equal(c.field, c.value);
|
|
1054
|
+
}
|
|
1055
|
+
if (list !== null && (best === null || list.length < best.length)) {
|
|
1056
|
+
best = list;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return best;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// ============================================================ the engine
|
|
1063
|
+
|
|
1064
|
+
/** A cell of the cascade: a flat value, a failed mapper (the default), or a value Cytoscape.js cannot draw. */
|
|
1065
|
+
type Cell = CyjsFlat | "reset" | "fn" | typeof CYJS_THROWS;
|
|
1066
|
+
|
|
1067
|
+
/** One property of one entry, parsed. */
|
|
1068
|
+
interface EntryProperty {
|
|
1069
|
+
readonly name: string;
|
|
1070
|
+
readonly spec: CyjsPropertySpec;
|
|
1071
|
+
readonly parsed: CyjsParsed;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** One target's result. */
|
|
1075
|
+
export interface CyjsTargetStyles {
|
|
1076
|
+
/** The element count (in snapshot order when a snapshot was given). */
|
|
1077
|
+
readonly n: number;
|
|
1078
|
+
/** What Cytoscape.js draws, per neutral property, by element index. */
|
|
1079
|
+
readonly values: Partial<Record<string, StyleColumn>>;
|
|
1080
|
+
/** The stylesheet's rules, bottom to top. */
|
|
1081
|
+
readonly rules: StyleRule[];
|
|
1082
|
+
/** The style columns the rules read. */
|
|
1083
|
+
readonly columns: Map<string, StyleColumn>;
|
|
1084
|
+
/** The data columns the rules or the values were read from. */
|
|
1085
|
+
readonly rawColumns: string[];
|
|
1086
|
+
/** Each element's Cytoscape.js element, by index (null where the core holds none). */
|
|
1087
|
+
readonly elements: readonly (CyjsElement | null)[];
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** The input of the engine. */
|
|
1091
|
+
export interface CyjsStyleInput {
|
|
1092
|
+
/** The elements: an array of element JSON, or `{ nodes, edges }`. */
|
|
1093
|
+
readonly elements: unknown;
|
|
1094
|
+
/** The stylesheet: an array of `{ selector, style }` (or `css`), or a stylesheet written as text. */
|
|
1095
|
+
readonly style: unknown;
|
|
1096
|
+
/**
|
|
1097
|
+
* The graph the styles are for. Elements are matched to its nodes by id and to its edges in order, and the
|
|
1098
|
+
* values come out indexed like it; its columns let a selector or a mapper become a predicate or a mapping over
|
|
1099
|
+
* graph-io's columns. Without it, values are indexed like the core holds the elements.
|
|
1100
|
+
*/
|
|
1101
|
+
readonly snapshot?: GraphSnapshot;
|
|
1102
|
+
/** Whether Cytoscape.js's own default rules (edges 3px wide, gray compound parents) are applied; default true. */
|
|
1103
|
+
readonly toolDefaults?: boolean;
|
|
1104
|
+
/**
|
|
1105
|
+
* What a live core draws, per element JSON object (`ele.style()` beside `ele.json()`): taken as the truth over what
|
|
1106
|
+
* the stylesheet gives, and the only source for a property whose stylesheet value is a function.
|
|
1107
|
+
*/
|
|
1108
|
+
readonly rendered?: ReadonlyMap<object, Readonly<Record<string, unknown>>>;
|
|
1109
|
+
/** Where problems are noted. */
|
|
1110
|
+
readonly notes?: StyleNotes;
|
|
1111
|
+
/** The counted work budget. */
|
|
1112
|
+
readonly budget?: StyleWorkBudget;
|
|
1113
|
+
/** Cancels the work. */
|
|
1114
|
+
readonly signal?: AbortSignal;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** The engine's result. */
|
|
1118
|
+
export interface CyjsStyles {
|
|
1119
|
+
/** Nodes. */
|
|
1120
|
+
readonly node: CyjsTargetStyles;
|
|
1121
|
+
/** Edges. */
|
|
1122
|
+
readonly edge: CyjsTargetStyles;
|
|
1123
|
+
/** The elements as the core holds them. */
|
|
1124
|
+
readonly graph: CyjsGraph;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const POSITIONS: ReadonlyMap<string, string> = new Map([
|
|
1128
|
+
["top,left", "top-left"],
|
|
1129
|
+
["top,center", "top"],
|
|
1130
|
+
["top,right", "top-right"],
|
|
1131
|
+
["center,left", "left"],
|
|
1132
|
+
["center,center", "center"],
|
|
1133
|
+
["center,right", "right"],
|
|
1134
|
+
["bottom,left", "bottom-left"],
|
|
1135
|
+
["bottom,center", "bottom"],
|
|
1136
|
+
["bottom,right", "bottom-right"],
|
|
1137
|
+
]);
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Applies a Cytoscape.js stylesheet to its elements as Cytoscape.js 3.34 does.
|
|
1141
|
+
* @param input - the elements, the stylesheet, the graph and the options
|
|
1142
|
+
* @returns what is drawn per element and the stylesheet's rules, per target
|
|
1143
|
+
*/
|
|
1144
|
+
export function evaluateCytoscapeJs(input: CyjsStyleInput): CyjsStyles {
|
|
1145
|
+
const notes = input.notes ?? new StyleNotes();
|
|
1146
|
+
const budget = input.budget ?? new StyleBudget();
|
|
1147
|
+
const graph = cyjsGraph(input.elements);
|
|
1148
|
+
const sheet: Entry[] = [];
|
|
1149
|
+
if (input.toolDefaults !== false) {
|
|
1150
|
+
CYJS_DEFAULT_STYLESHEET.forEach((d, k) => {
|
|
1151
|
+
sheet.push({
|
|
1152
|
+
index: k - CYJS_DEFAULT_STYLESHEET.length,
|
|
1153
|
+
tool: true,
|
|
1154
|
+
text: d.selector,
|
|
1155
|
+
selector: parseCyjsSelector(d.selector),
|
|
1156
|
+
properties: Object.entries(d.style),
|
|
1157
|
+
dashOnly: true,
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
const raw = sheetEntries(input.style, notes);
|
|
1162
|
+
let invalid = 0;
|
|
1163
|
+
let deep = 0;
|
|
1164
|
+
raw.forEach((r, k) => {
|
|
1165
|
+
let selector = parseCyjsSelector(r.selector);
|
|
1166
|
+
if (selector.invalid) {
|
|
1167
|
+
invalid++;
|
|
1168
|
+
}
|
|
1169
|
+
if (selector.queries.some((q) => nesting(q) > MAX_SELECTOR_NESTING)) {
|
|
1170
|
+
deep++;
|
|
1171
|
+
selector = { queries: [], invalid: true };
|
|
1172
|
+
}
|
|
1173
|
+
sheet.push({
|
|
1174
|
+
index: k,
|
|
1175
|
+
tool: false,
|
|
1176
|
+
text: typeof r.selector === "string" ? r.selector : "",
|
|
1177
|
+
selector,
|
|
1178
|
+
properties: r.properties,
|
|
1179
|
+
dashOnly: r.dashOnly,
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
if (invalid > 0) {
|
|
1183
|
+
notes.add(STYLE_ISSUE.SELECTOR, null, "selector", { count: invalid, sample: "invalid" });
|
|
1184
|
+
}
|
|
1185
|
+
if (deep > 0) {
|
|
1186
|
+
notes.add(STYLE_ISSUE.SELECTOR, null, "selector", { count: deep, sample: "nested too deep" });
|
|
1187
|
+
}
|
|
1188
|
+
const matcher = new Matcher(graph, notes);
|
|
1189
|
+
const node = evaluateTarget("node", graph, sheet, matcher, input, notes, budget);
|
|
1190
|
+
const edge = evaluateTarget("edge", graph, sheet, matcher, input, notes, budget);
|
|
1191
|
+
return { node, edge, graph };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* The order of the output: for each output index, the core's element (or -1).
|
|
1196
|
+
* @param kind - the target
|
|
1197
|
+
* @param graph - the core's elements
|
|
1198
|
+
* @param snapshot - the graph, when given
|
|
1199
|
+
* @param notes - where problems are noted
|
|
1200
|
+
* @returns the core element of each output index
|
|
1201
|
+
*/
|
|
1202
|
+
function outputOrder(
|
|
1203
|
+
kind: StyleTarget,
|
|
1204
|
+
graph: CyjsGraph,
|
|
1205
|
+
snapshot: GraphSnapshot | undefined,
|
|
1206
|
+
notes: StyleNotes,
|
|
1207
|
+
): Int32Array {
|
|
1208
|
+
const els = kind === "node" ? graph.nodes : graph.edges;
|
|
1209
|
+
if (snapshot === undefined) {
|
|
1210
|
+
return Int32Array.from(els, (_e, i) => i);
|
|
1211
|
+
}
|
|
1212
|
+
if (kind === "node") {
|
|
1213
|
+
const order = new Int32Array(snapshot.nodeCount).fill(-1);
|
|
1214
|
+
for (let i = 0; i < els.length; i++) {
|
|
1215
|
+
const at = snapshotIndex(snapshot, els[i].id);
|
|
1216
|
+
if (at >= 0 && order[at] < 0) {
|
|
1217
|
+
order[at] = i;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return order;
|
|
1221
|
+
}
|
|
1222
|
+
const order = new Int32Array(snapshot.edgeCount).fill(-1);
|
|
1223
|
+
const nodeAt = outputOrder("node", graph, snapshot, notes);
|
|
1224
|
+
const coreToSnap = new Int32Array(graph.nodes.length).fill(-1);
|
|
1225
|
+
nodeAt.forEach((c, s) => {
|
|
1226
|
+
if (c >= 0) {
|
|
1227
|
+
coreToSnap[c] = s;
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
let j = 0;
|
|
1231
|
+
for (let e = 0; e < els.length && j < snapshot.edgeCount; e++) {
|
|
1232
|
+
if (
|
|
1233
|
+
coreToSnap[graph.source[e]] === snapshot.edgeSource(j) &&
|
|
1234
|
+
coreToSnap[graph.target[e]] === snapshot.edgeTarget(j)
|
|
1235
|
+
) {
|
|
1236
|
+
order[j++] = e;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (j < snapshot.edgeCount) {
|
|
1240
|
+
notes.add(STYLE_ISSUE.UNRESOLVED, "edge", "elements", { count: snapshot.edgeCount - j, sample: "edge order" });
|
|
1241
|
+
}
|
|
1242
|
+
return order;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* A node's index in the graph by its Cytoscape.js id (text), trying the id as a number too.
|
|
1247
|
+
* @param snapshot - the graph
|
|
1248
|
+
* @param id - the id
|
|
1249
|
+
* @returns the index, or -1
|
|
1250
|
+
*/
|
|
1251
|
+
function snapshotIndex(snapshot: GraphSnapshot, id: string): number {
|
|
1252
|
+
if (snapshot.ids.has(id)) {
|
|
1253
|
+
return snapshot.ids.indexOf(id);
|
|
1254
|
+
}
|
|
1255
|
+
const n = Number(id);
|
|
1256
|
+
return id.trim() !== "" && Number.isFinite(n) && snapshot.ids.has(n) ? snapshot.ids.indexOf(n) : -1;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** The cascade's winning cells, per Cytoscape.js property, by core element. */
|
|
1260
|
+
type Cells = Map<string, (Cell | undefined)[]>;
|
|
1261
|
+
|
|
1262
|
+
/** One entry applied to a target: its elements and its parsed properties. */
|
|
1263
|
+
interface Applied {
|
|
1264
|
+
readonly entry: Entry;
|
|
1265
|
+
readonly list: readonly number[];
|
|
1266
|
+
readonly props: EntryProperty[];
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* A property's cells, created on first use.
|
|
1271
|
+
* @param cells - the cells
|
|
1272
|
+
* @param name - the Cytoscape.js property
|
|
1273
|
+
* @param n - the element count
|
|
1274
|
+
* @returns the property's cells
|
|
1275
|
+
*/
|
|
1276
|
+
function cellsOf(cells: Cells, name: string, n: number): (Cell | undefined)[] {
|
|
1277
|
+
let c = cells.get(name);
|
|
1278
|
+
if (c === undefined) {
|
|
1279
|
+
c = new Array<Cell | undefined>(n);
|
|
1280
|
+
cells.set(name, c);
|
|
1281
|
+
}
|
|
1282
|
+
return c;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Applies the sheet to one target.
|
|
1287
|
+
* @param kind - nodes or edges
|
|
1288
|
+
* @param graph - the core's elements
|
|
1289
|
+
* @param sheet - the entries
|
|
1290
|
+
* @param matcher - the selector evaluator
|
|
1291
|
+
* @param input - the input
|
|
1292
|
+
* @param notes - where problems are noted
|
|
1293
|
+
* @param budget - the counted budget
|
|
1294
|
+
* @returns the target's values and rules
|
|
1295
|
+
*/
|
|
1296
|
+
function evaluateTarget(
|
|
1297
|
+
kind: StyleTarget,
|
|
1298
|
+
graph: CyjsGraph,
|
|
1299
|
+
sheet: readonly Entry[],
|
|
1300
|
+
matcher: Matcher,
|
|
1301
|
+
input: CyjsStyleInput,
|
|
1302
|
+
notes: StyleNotes,
|
|
1303
|
+
budget: StyleWorkBudget,
|
|
1304
|
+
): CyjsTargetStyles {
|
|
1305
|
+
const els = kind === "node" ? graph.nodes : graph.edges;
|
|
1306
|
+
const spend = (ops: number): void => {
|
|
1307
|
+
budget.spend(kind, ops);
|
|
1308
|
+
};
|
|
1309
|
+
const { cells, applied } = cascade(kind, els, sheet, matcher, input, notes, budget);
|
|
1310
|
+
const order = outputOrder(kind, graph, input.snapshot, notes);
|
|
1311
|
+
const coreToOut = new Int32Array(els.length).fill(-1);
|
|
1312
|
+
order.forEach((c, k) => {
|
|
1313
|
+
if (c >= 0) {
|
|
1314
|
+
coreToOut[c] = k;
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
applyBypasses(kind, els, cells, coreToOut, notes);
|
|
1318
|
+
if (kind === "edge") {
|
|
1319
|
+
forceLoopCurves(graph, cells.get("curve-style"));
|
|
1320
|
+
}
|
|
1321
|
+
const drawn = drawnValues(kind, els, cells, order, input.rendered, notes, spend);
|
|
1322
|
+
if (kind === "edge") {
|
|
1323
|
+
straightenUnpairedBeziers(graph, curveStyles(graph, cells, input.rendered), coreToOut, drawn.curved);
|
|
1324
|
+
}
|
|
1325
|
+
const built = buildRules(kind, applied, { els, order, coreToOut }, input, drawn, { matcher, notes, spend });
|
|
1326
|
+
return {
|
|
1327
|
+
n: order.length,
|
|
1328
|
+
values: drawn,
|
|
1329
|
+
rules: built.rules,
|
|
1330
|
+
columns: built.columns,
|
|
1331
|
+
rawColumns: built.rawColumns,
|
|
1332
|
+
elements: Array.from(order, (i) => (i >= 0 ? els[i] : null)),
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* The cascade over one target: per property, the cell of the last rule that sets it, per element.
|
|
1338
|
+
* @param kind - nodes or edges
|
|
1339
|
+
* @param els - the core's elements of the target
|
|
1340
|
+
* @param sheet - the entries
|
|
1341
|
+
* @param matcher - the selector evaluator
|
|
1342
|
+
* @param input - the input
|
|
1343
|
+
* @param notes - where problems are noted
|
|
1344
|
+
* @param budget - the counted budget
|
|
1345
|
+
* @returns the cells and the entries applied
|
|
1346
|
+
*/
|
|
1347
|
+
function cascade(
|
|
1348
|
+
kind: StyleTarget,
|
|
1349
|
+
els: readonly CyjsElement[],
|
|
1350
|
+
sheet: readonly Entry[],
|
|
1351
|
+
matcher: Matcher,
|
|
1352
|
+
input: CyjsStyleInput,
|
|
1353
|
+
notes: StyleNotes,
|
|
1354
|
+
budget: StyleWorkBudget,
|
|
1355
|
+
): { cells: Cells; applied: Applied[] } {
|
|
1356
|
+
const n = els.length;
|
|
1357
|
+
const specs = cyjsProperties(kind);
|
|
1358
|
+
const spend = (ops: number): void => {
|
|
1359
|
+
budget.spend(kind, ops);
|
|
1360
|
+
};
|
|
1361
|
+
const ix = new Indexes(els, spend);
|
|
1362
|
+
const cells: Cells = new Map();
|
|
1363
|
+
const notCarried = new Map<string, number>();
|
|
1364
|
+
const applied: Applied[] = [];
|
|
1365
|
+
let stopped = false;
|
|
1366
|
+
for (const entry of sheet) {
|
|
1367
|
+
input.signal?.throwIfAborted();
|
|
1368
|
+
const props = entryProperties(entry, kind, specs, notes, notCarried);
|
|
1369
|
+
if (props.length === 0) {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const list = budget.exhausted(kind)
|
|
1373
|
+
? null
|
|
1374
|
+
: budgetedMatch(entry.selector, { kind, n }, matcher, ix, input, budget);
|
|
1375
|
+
if (list === null) {
|
|
1376
|
+
stopped = true;
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
spend(list.length);
|
|
1380
|
+
applied.push({ entry, list, props });
|
|
1381
|
+
for (const p of props) {
|
|
1382
|
+
const col = cellsOf(cells, p.name, n);
|
|
1383
|
+
for (const i of list) {
|
|
1384
|
+
col[i] = cellFor(p, els[i]);
|
|
1385
|
+
}
|
|
1386
|
+
spend(list.length);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (stopped) {
|
|
1390
|
+
notes.add(STYLE_ISSUE.BUDGET, kind, null, { sample: "stylesheet" });
|
|
1391
|
+
}
|
|
1392
|
+
for (const [name, count] of notCarried) {
|
|
1393
|
+
notes.add(STYLE_ISSUE.NOT_CARRIED, kind, name, { count, sample: name });
|
|
1394
|
+
}
|
|
1395
|
+
return { cells, applied };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* The elements a selector matches, within the counted budget.
|
|
1400
|
+
* @param sel - the selector
|
|
1401
|
+
* @param target - the target and its element count
|
|
1402
|
+
* @param target.kind - nodes or edges
|
|
1403
|
+
* @param target.n - the element count
|
|
1404
|
+
* @param matcher - the evaluator
|
|
1405
|
+
* @param ix - the target's indexes
|
|
1406
|
+
* @param input - the input, whose signal cancels the match
|
|
1407
|
+
* @param budget - the counted budget
|
|
1408
|
+
* @returns the element indexes, or null when the budget ran out partway through
|
|
1409
|
+
*/
|
|
1410
|
+
function budgetedMatch(
|
|
1411
|
+
sel: CyjsSelector,
|
|
1412
|
+
target: { readonly kind: StyleTarget; readonly n: number },
|
|
1413
|
+
matcher: Matcher,
|
|
1414
|
+
ix: Indexes,
|
|
1415
|
+
input: CyjsStyleInput,
|
|
1416
|
+
budget: StyleWorkBudget,
|
|
1417
|
+
): readonly number[] | null {
|
|
1418
|
+
matcher.begin(target.kind, budget, input.signal);
|
|
1419
|
+
try {
|
|
1420
|
+
return matchList(sel, target.kind, target.n, matcher, ix);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (!(error instanceof BudgetStop)) {
|
|
1423
|
+
throw error;
|
|
1424
|
+
}
|
|
1425
|
+
return null;
|
|
1426
|
+
} finally {
|
|
1427
|
+
matcher.end();
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Each element's own `style` (a bypass) over every rule; a mapper there is refused, as Cytoscape.js refuses it.
|
|
1433
|
+
* @param kind - nodes or edges
|
|
1434
|
+
* @param els - the core's elements of the target
|
|
1435
|
+
* @param cells - the cells; changed in place
|
|
1436
|
+
* @param coreToOut - the output index of each core element
|
|
1437
|
+
* @param notes - where problems are noted
|
|
1438
|
+
*/
|
|
1439
|
+
function applyBypasses(
|
|
1440
|
+
kind: StyleTarget,
|
|
1441
|
+
els: readonly CyjsElement[],
|
|
1442
|
+
cells: Cells,
|
|
1443
|
+
coreToOut: Int32Array,
|
|
1444
|
+
notes: StyleNotes,
|
|
1445
|
+
): void {
|
|
1446
|
+
const specs = cyjsProperties(kind);
|
|
1447
|
+
for (let i = 0; i < els.length; i++) {
|
|
1448
|
+
const { json } = els[i];
|
|
1449
|
+
const bypass = json.style ?? json.css;
|
|
1450
|
+
if (!isObject(bypass)) {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
for (const name0 of Object.keys(bypass)) {
|
|
1454
|
+
const name = cyjsPropertyName(name0);
|
|
1455
|
+
const spec = specs.get(name);
|
|
1456
|
+
const value = bypass[name0];
|
|
1457
|
+
if (spec === undefined || value === "" || value === null) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const parsed = parseCyjsValue(spec, value, "bypass");
|
|
1461
|
+
if (parsed === null || parsed === CYJS_THROWS || !isFlat(parsed)) {
|
|
1462
|
+
notes.add(STYLE_ISSUE.BAD_VALUE, kind, spec.property, { sample: value, element: coreToOut[i] });
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
cellsOf(cells, name, els.length)[i] = parsed;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* What is drawn per neutral property, in output order: the cells as neutral values, overridden by what a live core
|
|
1472
|
+
* reports, with the label alignments and the visibility parts combined. Properties with no value are left out.
|
|
1473
|
+
* @param kind - nodes or edges
|
|
1474
|
+
* @param els - the core's elements of the target
|
|
1475
|
+
* @param cells - the cascade's cells
|
|
1476
|
+
* @param order - the core element of each output index
|
|
1477
|
+
* @param rendered - what a live core reports per element JSON, when given
|
|
1478
|
+
* @param notes - where problems are noted
|
|
1479
|
+
* @param spend - spends from the budget
|
|
1480
|
+
* @returns the values by property
|
|
1481
|
+
*/
|
|
1482
|
+
function drawnValues(
|
|
1483
|
+
kind: StyleTarget,
|
|
1484
|
+
els: readonly CyjsElement[],
|
|
1485
|
+
cells: Cells,
|
|
1486
|
+
order: Int32Array,
|
|
1487
|
+
rendered: CyjsStyleInput["rendered"],
|
|
1488
|
+
notes: StyleNotes,
|
|
1489
|
+
spend: (ops: number) => void,
|
|
1490
|
+
): Record<string, (StyleValue | null)[]> {
|
|
1491
|
+
const out = order.length;
|
|
1492
|
+
const values: Record<string, (StyleValue | null)[]> = {};
|
|
1493
|
+
const words = new Map<string, (string | null)[]>();
|
|
1494
|
+
const drawnOf: DrawnRead = { kind, els, cells, order, rendered, notes };
|
|
1495
|
+
for (const [name, spec] of cyjsProperties(kind)) {
|
|
1496
|
+
if (!cells.has(name) && rendered === undefined) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const col = drawnColumn(drawnOf, name, spec);
|
|
1500
|
+
spend(2 * out);
|
|
1501
|
+
if (spec.property.startsWith("_")) {
|
|
1502
|
+
words.set(
|
|
1503
|
+
spec.property,
|
|
1504
|
+
col.map((v) => (typeof v === "string" ? v : null)),
|
|
1505
|
+
);
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
values[spec.property] ??= new Array<StyleValue | null>(out).fill(null);
|
|
1509
|
+
const target = values[spec.property];
|
|
1510
|
+
col.forEach((v, k) => {
|
|
1511
|
+
if (v !== null && v !== undefined) {
|
|
1512
|
+
target[k] = v;
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
combineParts(kind, out, words, values, notes);
|
|
1517
|
+
const drawn: Record<string, (StyleValue | null)[]> = {};
|
|
1518
|
+
for (const [p, col] of Object.entries(values)) {
|
|
1519
|
+
if (col.some((v) => v !== null)) {
|
|
1520
|
+
drawn[p] = col;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return drawn;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/** What drawnColumn() reads. */
|
|
1527
|
+
interface DrawnRead {
|
|
1528
|
+
readonly kind: StyleTarget;
|
|
1529
|
+
readonly els: readonly CyjsElement[];
|
|
1530
|
+
readonly cells: Cells;
|
|
1531
|
+
readonly order: Int32Array;
|
|
1532
|
+
readonly rendered: CyjsStyleInput["rendered"];
|
|
1533
|
+
readonly notes: StyleNotes;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* One Cytoscape.js property's neutral value per output element: its cell, overridden by what a live core reports.
|
|
1538
|
+
* @param d - the target's cells and what a live core reports
|
|
1539
|
+
* @param name - the property
|
|
1540
|
+
* @param spec - its spec
|
|
1541
|
+
* @returns the values, in output order
|
|
1542
|
+
*/
|
|
1543
|
+
function drawnColumn(d: DrawnRead, name: string, spec: CyjsPropertySpec): (StyleValue | null | undefined)[] {
|
|
1544
|
+
const { kind, order, notes, rendered } = d;
|
|
1545
|
+
const out = order.length;
|
|
1546
|
+
const col = new Array<StyleValue | null | undefined>(out).fill(null);
|
|
1547
|
+
const cellCol = d.cells.get(name);
|
|
1548
|
+
for (let k = 0; k < out && cellCol !== undefined; k++) {
|
|
1549
|
+
const c = order[k] >= 0 ? cellCol[order[k]] : undefined;
|
|
1550
|
+
if (c !== undefined) {
|
|
1551
|
+
col[k] = neutralOfCell(spec, name, c, { notes, target: kind, property: spec.property, element: k });
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (rendered !== undefined) {
|
|
1555
|
+
overrideRendered(d, rendered, name, spec, col);
|
|
1556
|
+
}
|
|
1557
|
+
return col;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Overrides a property's values with what a live core reports.
|
|
1562
|
+
* @param d - the target's cells
|
|
1563
|
+
* @param rendered - what a live core reports
|
|
1564
|
+
* @param name - the property
|
|
1565
|
+
* @param spec - its spec
|
|
1566
|
+
* @param col - the values, in output order, changed in place
|
|
1567
|
+
*/
|
|
1568
|
+
function overrideRendered(
|
|
1569
|
+
d: DrawnRead,
|
|
1570
|
+
rendered: NonNullable<CyjsStyleInput["rendered"]>,
|
|
1571
|
+
name: string,
|
|
1572
|
+
spec: CyjsPropertySpec,
|
|
1573
|
+
col: (StyleValue | null | undefined)[],
|
|
1574
|
+
): void {
|
|
1575
|
+
const { kind, order, notes } = d;
|
|
1576
|
+
for (let k = 0; k < order.length; k++) {
|
|
1577
|
+
const r = order[k] >= 0 ? rendered.get(d.els[order[k]].json) : undefined;
|
|
1578
|
+
if (r !== undefined && Object.hasOwn(r, name)) {
|
|
1579
|
+
const site = { notes, target: kind, property: spec.property, element: k };
|
|
1580
|
+
const v = cyjsRendered(kind, name, r[name], site);
|
|
1581
|
+
// a live core reports "" for a label it does not draw
|
|
1582
|
+
col[k] = v === undefined || (v === "" && spec.type === "text") ? null : v;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Each edge's curve style as Cytoscape.js draws it: what a live core reports, else the winning cell.
|
|
1589
|
+
* @param graph - the core's elements
|
|
1590
|
+
* @param cells - the winning cells
|
|
1591
|
+
* @param rendered - what a live core reports, when given
|
|
1592
|
+
* @returns the curve style per edge, in core order
|
|
1593
|
+
*/
|
|
1594
|
+
function curveStyles(graph: CyjsGraph, cells: Cells, rendered: CyjsStyleInput["rendered"]): unknown[] {
|
|
1595
|
+
const col = cells.get("curve-style");
|
|
1596
|
+
return graph.edges.map((el, e) => {
|
|
1597
|
+
const r = rendered?.get(el.json);
|
|
1598
|
+
if (r !== undefined && Object.hasOwn(r, "curve-style")) {
|
|
1599
|
+
return r["curve-style"];
|
|
1600
|
+
}
|
|
1601
|
+
const c = col?.[e];
|
|
1602
|
+
return typeof c === "object" && c.kind === "word" ? c.value : undefined;
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* Cytoscape.js bows a `bezier` edge only to keep it apart from the other `bezier` edges joining the same two nodes:
|
|
1608
|
+
* it draws a lone one, and the middle one of an odd number, straight. Loops stay curved.
|
|
1609
|
+
* @param graph - the core's elements
|
|
1610
|
+
* @param styles - each edge's curve style, in core order
|
|
1611
|
+
* @param coreToOut - each edge's output index, or -1
|
|
1612
|
+
* @param curved - the drawn `curved` values in output order, changed in place
|
|
1613
|
+
*/
|
|
1614
|
+
function straightenUnpairedBeziers(
|
|
1615
|
+
graph: CyjsGraph,
|
|
1616
|
+
styles: readonly unknown[],
|
|
1617
|
+
coreToOut: Int32Array,
|
|
1618
|
+
curved: (StyleValue | null)[] | undefined,
|
|
1619
|
+
): void {
|
|
1620
|
+
if (curved === undefined) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
for (const bundle of bezierBundles(graph, styles)) {
|
|
1624
|
+
const k = bundle.length % 2 === 1 ? coreToOut[bundle[(bundle.length - 1) / 2]] : -1;
|
|
1625
|
+
if (k >= 0) {
|
|
1626
|
+
curved[k] = false;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* The `bezier` edges that are not loops, grouped by the two nodes they join (either way round), in core order.
|
|
1633
|
+
* @param graph - the core's elements
|
|
1634
|
+
* @param styles - each edge's curve style, in core order
|
|
1635
|
+
* @returns the groups
|
|
1636
|
+
*/
|
|
1637
|
+
function bezierBundles(graph: CyjsGraph, styles: readonly unknown[]): Iterable<number[]> {
|
|
1638
|
+
const bundles = new Map<string, number[]>();
|
|
1639
|
+
for (let e = 0; e < graph.edges.length; e++) {
|
|
1640
|
+
const s = graph.source[e];
|
|
1641
|
+
const t = graph.target[e];
|
|
1642
|
+
if (s === t || styles[e] !== "bezier") {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
const key = s < t ? `${s} ${t}` : `${t} ${s}`;
|
|
1646
|
+
const bundle = bundles.get(key);
|
|
1647
|
+
if (bundle === undefined) {
|
|
1648
|
+
bundles.set(key, [e]);
|
|
1649
|
+
} else {
|
|
1650
|
+
bundle.push(e);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return bundles.values();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Cytoscape.js draws every loop curved: a loop whose curve style is anything but `bezier` becomes `bezier`, except
|
|
1658
|
+
* `haystack`, which Cytoscape.js leaves as it is because it is the property's own default.
|
|
1659
|
+
* @param graph - the core's elements
|
|
1660
|
+
* @param col - the winning curve style per edge, if any rule set one
|
|
1661
|
+
*/
|
|
1662
|
+
function forceLoopCurves(graph: CyjsGraph, col: (Cell | undefined)[] | undefined): void {
|
|
1663
|
+
if (col === undefined) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
for (let e = 0; e < graph.edges.length; e++) {
|
|
1667
|
+
const c = col[e];
|
|
1668
|
+
if (
|
|
1669
|
+
graph.source[e] === graph.target[e] &&
|
|
1670
|
+
typeof c === "object" &&
|
|
1671
|
+
c.kind === "word" &&
|
|
1672
|
+
c.value !== "bezier" &&
|
|
1673
|
+
c.value !== "haystack"
|
|
1674
|
+
) {
|
|
1675
|
+
col[e] = { kind: "word", value: "bezier", str: "bezier" };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* The properties of one entry that apply to a target, parsed.
|
|
1682
|
+
* @param entry - the entry
|
|
1683
|
+
* @param kind - the target
|
|
1684
|
+
* @param specs - the target's carried properties
|
|
1685
|
+
* @param notes - where problems are noted
|
|
1686
|
+
* @param notCarried - counts of property names not carried
|
|
1687
|
+
* @returns the parsed properties, later duplicates last
|
|
1688
|
+
*/
|
|
1689
|
+
function entryProperties(
|
|
1690
|
+
entry: Entry,
|
|
1691
|
+
kind: StyleTarget,
|
|
1692
|
+
specs: ReadonlyMap<string, CyjsPropertySpec>,
|
|
1693
|
+
notes: StyleNotes,
|
|
1694
|
+
notCarried: Map<string, number>,
|
|
1695
|
+
): EntryProperty[] {
|
|
1696
|
+
const out: EntryProperty[] = [];
|
|
1697
|
+
for (const [name0, value] of entry.properties) {
|
|
1698
|
+
const name = entry.dashOnly ? dashOnlyName(name0) : cyjsPropertyName(name0);
|
|
1699
|
+
const spec = specs.get(name);
|
|
1700
|
+
if (spec === undefined) {
|
|
1701
|
+
if (kind === "node" && !CYJS_EDGE_ONLY.has(name) && !entry.tool) {
|
|
1702
|
+
notCarried.set(name, (notCarried.get(name) ?? 0) + 1);
|
|
1703
|
+
}
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const parsed = parseCyjsValue(spec, value, "rule");
|
|
1707
|
+
if (parsed === null || parsed === CYJS_THROWS) {
|
|
1708
|
+
// a rule's own value: noted for the target as a whole, so per-element notes keep their element lists
|
|
1709
|
+
if (!entry.tool) {
|
|
1710
|
+
notes.add(STYLE_ISSUE.BAD_VALUE, kind, null, { sample: `${name}: ${String(value)}` });
|
|
1711
|
+
}
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
out.push({ name, spec, parsed });
|
|
1715
|
+
}
|
|
1716
|
+
return out;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* A property name of an entry Cytoscape.js reads in dash form only: `content` is `label`, any other name as it is.
|
|
1721
|
+
* @param name - the name as written
|
|
1722
|
+
* @returns the name
|
|
1723
|
+
*/
|
|
1724
|
+
function dashOnlyName(name: string): string {
|
|
1725
|
+
return name === "content" ? "label" : name;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/** Edge properties a node never carries: not noted as dropped on nodes. */
|
|
1729
|
+
const CYJS_EDGE_ONLY: ReadonlySet<string> = new Set([
|
|
1730
|
+
"line-color",
|
|
1731
|
+
"line-style",
|
|
1732
|
+
"target-arrow-shape",
|
|
1733
|
+
"source-arrow-shape",
|
|
1734
|
+
"target-arrow-color",
|
|
1735
|
+
"source-arrow-color",
|
|
1736
|
+
"curve-style",
|
|
1737
|
+
"arrow-scale",
|
|
1738
|
+
"source-label",
|
|
1739
|
+
"target-label",
|
|
1740
|
+
]);
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* The elements of a target a selector matches, in order.
|
|
1744
|
+
* @param sel - the selector
|
|
1745
|
+
* @param kind - the target
|
|
1746
|
+
* @param n - the element count
|
|
1747
|
+
* @param matcher - the evaluator
|
|
1748
|
+
* @param ix - the target's indexes
|
|
1749
|
+
* @returns the element indexes
|
|
1750
|
+
*/
|
|
1751
|
+
function matchList(sel: CyjsSelector, kind: StyleTarget, n: number, matcher: Matcher, ix: Indexes): readonly number[] {
|
|
1752
|
+
if (sel.queries.length === 0) {
|
|
1753
|
+
return [];
|
|
1754
|
+
}
|
|
1755
|
+
// one stamp per call marks the elements found, so a selector costs what its candidates cost, not the graph
|
|
1756
|
+
const stamp = ++ix.stamp;
|
|
1757
|
+
const marked = ix.marks;
|
|
1758
|
+
const hits: number[] = [];
|
|
1759
|
+
let scanned = false;
|
|
1760
|
+
const visit = (q: CyjsQuery, i: number): void => {
|
|
1761
|
+
if (marked[i] !== stamp && matcher.query(q, kind, i)) {
|
|
1762
|
+
marked[i] = stamp;
|
|
1763
|
+
hits.push(i);
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
for (const q of sel.queries) {
|
|
1767
|
+
const cands = candidates(q, kind, ix);
|
|
1768
|
+
if (cands === null) {
|
|
1769
|
+
scanned = true;
|
|
1770
|
+
for (let i = 0; i < n; i++) {
|
|
1771
|
+
visit(q, i);
|
|
1772
|
+
}
|
|
1773
|
+
} else {
|
|
1774
|
+
matcher.work += cands.length;
|
|
1775
|
+
for (const i of cands) {
|
|
1776
|
+
visit(q, i);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (scanned || sel.queries.length > 1) {
|
|
1781
|
+
hits.sort((a, b) => a - b);
|
|
1782
|
+
}
|
|
1783
|
+
matcher.work += hits.length;
|
|
1784
|
+
return hits;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* The cell a property gives one element: its constant, or its mapper evaluated.
|
|
1789
|
+
* @param p - the property
|
|
1790
|
+
* @param e - the element
|
|
1791
|
+
* @returns the cell
|
|
1792
|
+
*/
|
|
1793
|
+
function cellFor(p: EntryProperty, e: CyjsElement): Cell {
|
|
1794
|
+
const v = p.parsed;
|
|
1795
|
+
if (v.kind === "data" || v.kind === "mapData") {
|
|
1796
|
+
return applyCyjsMapper(p.spec, v, mapperData(e)) ?? "reset";
|
|
1797
|
+
}
|
|
1798
|
+
if (v.kind === "fn") {
|
|
1799
|
+
return "fn";
|
|
1800
|
+
}
|
|
1801
|
+
return v;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/** Parsed property defaults, per property name. */
|
|
1805
|
+
const DEFAULT_CELLS = new Map<string, CyjsFlat | null>();
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* A property's Cytoscape.js default as a flat value.
|
|
1809
|
+
* @param name - the property
|
|
1810
|
+
* @param spec - its spec
|
|
1811
|
+
* @returns the default
|
|
1812
|
+
*/
|
|
1813
|
+
function defaultCell(name: string, spec: CyjsPropertySpec): CyjsFlat | null {
|
|
1814
|
+
const key = `${spec.type}|${name}`;
|
|
1815
|
+
if (!DEFAULT_CELLS.has(key)) {
|
|
1816
|
+
const raw = CYJS_PROPERTY_DEFAULTS.get(name);
|
|
1817
|
+
const parsed = raw === undefined ? null : parseCyjsValue(spec, raw, "flat");
|
|
1818
|
+
DEFAULT_CELLS.set(key, parsed === null || parsed === CYJS_THROWS || !isFlat(parsed) ? null : parsed);
|
|
1819
|
+
}
|
|
1820
|
+
return DEFAULT_CELLS.get(key) ?? null;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Whether a parsed value is flat.
|
|
1825
|
+
* @param v - the value
|
|
1826
|
+
* @returns true for a color, number or word
|
|
1827
|
+
*/
|
|
1828
|
+
function isFlat(v: CyjsParsed): v is CyjsFlat {
|
|
1829
|
+
return v.kind === "color" || v.kind === "number" || v.kind === "word";
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* The neutral value of a cell.
|
|
1834
|
+
* @param spec - the property
|
|
1835
|
+
* @param name - its Cytoscape.js name
|
|
1836
|
+
* @param c - the cell
|
|
1837
|
+
* @param site - where it is read
|
|
1838
|
+
* @returns the value, null for none, or undefined when it cannot be evaluated (noted)
|
|
1839
|
+
*/
|
|
1840
|
+
function neutralOfCell(spec: CyjsPropertySpec, name: string, c: Cell, site: CanonSite): StyleValue | null | undefined {
|
|
1841
|
+
if (c === CYJS_THROWS) {
|
|
1842
|
+
site.notes.add(STYLE_ISSUE.BAD_VALUE, site.target, site.property, {
|
|
1843
|
+
sample: "unstyleable",
|
|
1844
|
+
element: site.element,
|
|
1845
|
+
});
|
|
1846
|
+
return undefined;
|
|
1847
|
+
}
|
|
1848
|
+
if (c === "fn") {
|
|
1849
|
+
site.notes.add(STYLE_ISSUE.LAYER_FLATTENED, site.target, site.property, {
|
|
1850
|
+
sample: "FUNCTION",
|
|
1851
|
+
element: site.element,
|
|
1852
|
+
});
|
|
1853
|
+
return undefined;
|
|
1854
|
+
}
|
|
1855
|
+
const flat = c === "reset" ? defaultCell(name, spec) : c;
|
|
1856
|
+
if (flat === null) {
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
return cyjsNeutral(spec, flat, site);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* Combines the label alignments into `labelPosition` and `visibility` with `display` into `visible`.
|
|
1864
|
+
* @param kind - the target
|
|
1865
|
+
* @param n - the element count
|
|
1866
|
+
* @param words - the parts by internal name
|
|
1867
|
+
* @param values - the values; changed in place
|
|
1868
|
+
* @param notes - where problems are noted
|
|
1869
|
+
*/
|
|
1870
|
+
function combineParts(
|
|
1871
|
+
kind: StyleTarget,
|
|
1872
|
+
n: number,
|
|
1873
|
+
words: ReadonlyMap<string, (string | null)[]>,
|
|
1874
|
+
values: Record<string, (StyleValue | null)[]>,
|
|
1875
|
+
notes: StyleNotes,
|
|
1876
|
+
): void {
|
|
1877
|
+
const va = words.get("_valign");
|
|
1878
|
+
const ha = words.get("_halign");
|
|
1879
|
+
if (kind === "node" && (va !== undefined || ha !== undefined)) {
|
|
1880
|
+
values.labelPosition = Array.from({ length: n }, (_, i) =>
|
|
1881
|
+
labelPosition(va?.[i] ?? null, ha?.[i] ?? null, (sample) => {
|
|
1882
|
+
notes.add(STYLE_ISSUE.UNKNOWN_VALUE, kind, "labelPosition", { sample, element: i });
|
|
1883
|
+
}),
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
const vis = words.get("_visibility");
|
|
1887
|
+
const disp = words.get("_display");
|
|
1888
|
+
if (vis !== undefined || disp !== undefined) {
|
|
1889
|
+
const col = new Array<StyleValue | null>(n).fill(null);
|
|
1890
|
+
for (let i = 0; i < n; i++) {
|
|
1891
|
+
const a = vis?.[i] ?? null;
|
|
1892
|
+
const b = disp?.[i] ?? null;
|
|
1893
|
+
if (a === null && b === null) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
col[i] = a !== "hidden" && b !== "none";
|
|
1897
|
+
}
|
|
1898
|
+
values.visible = col;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* A node label's position from its vertical and horizontal alignments; an `-inside` alignment is read as its
|
|
1904
|
+
* outside one, noted.
|
|
1905
|
+
* @param v - the vertical alignment, or null
|
|
1906
|
+
* @param h - the horizontal alignment, or null
|
|
1907
|
+
* @param note - notes an alignment read as another
|
|
1908
|
+
* @returns the position, or null
|
|
1909
|
+
*/
|
|
1910
|
+
function labelPosition(v: string | null, h: string | null, note: (sample: string) => void): StyleValue | null {
|
|
1911
|
+
if (v === null && h === null) {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
const vv = (v ?? "top").replace("-inside", "");
|
|
1915
|
+
const hh = (h ?? "center").replace("-inside", "");
|
|
1916
|
+
if ((v ?? "").endsWith("-inside") || (h ?? "").endsWith("-inside")) {
|
|
1917
|
+
note(`${v ?? ""} ${h ?? ""}`);
|
|
1918
|
+
}
|
|
1919
|
+
return POSITIONS.get(`${vv},${hh}`) ?? null;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// ============================================================ rules
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* The stylesheet as neutral rules over graph-io's columns and the style columns it adds.
|
|
1926
|
+
* @param kind - the target
|
|
1927
|
+
* @param applied - the entries with their matched elements and properties
|
|
1928
|
+
* @param core - the core's elements, the core element of each output index and the output index of each core element
|
|
1929
|
+
* (or -1)
|
|
1930
|
+
* @param core.els - the core's elements
|
|
1931
|
+
* @param core.order - the core element of each output index
|
|
1932
|
+
* @param core.coreToOut - the output index of each core element, or -1
|
|
1933
|
+
* @param input - the input (the graph, the rendered values)
|
|
1934
|
+
* @param truth - what is drawn per property, by output index
|
|
1935
|
+
* @param env - the evaluator (degrees), where problems are noted and how the budget is spent
|
|
1936
|
+
* @param env.matcher - the evaluator
|
|
1937
|
+
* @param env.notes - where problems are noted
|
|
1938
|
+
* @param env.spend - spends from the budget
|
|
1939
|
+
* @returns the rules, the style columns and the data columns they read
|
|
1940
|
+
*/
|
|
1941
|
+
function buildRules(
|
|
1942
|
+
kind: StyleTarget,
|
|
1943
|
+
applied: readonly { entry: Entry; list: readonly number[]; props: EntryProperty[] }[],
|
|
1944
|
+
core: { readonly els: readonly CyjsElement[]; readonly order: Int32Array; readonly coreToOut: Int32Array },
|
|
1945
|
+
input: CyjsStyleInput,
|
|
1946
|
+
truth: Readonly<Record<string, readonly (StyleValue | null)[]>>,
|
|
1947
|
+
env: { readonly matcher: Matcher; readonly notes: StyleNotes; readonly spend: (ops: number) => void },
|
|
1948
|
+
): { rules: StyleRule[]; columns: Map<string, StyleColumn>; rawColumns: string[] } {
|
|
1949
|
+
const { els, order, coreToOut } = core;
|
|
1950
|
+
const { matcher, notes, spend } = env;
|
|
1951
|
+
const out = order.length;
|
|
1952
|
+
const columns = new Map<string, StyleColumn>();
|
|
1953
|
+
const raw = new Set<string>();
|
|
1954
|
+
const ctx: RuleContext = {
|
|
1955
|
+
kind,
|
|
1956
|
+
els,
|
|
1957
|
+
coreToOut,
|
|
1958
|
+
out,
|
|
1959
|
+
snapshot: input.snapshot,
|
|
1960
|
+
truth: input.rendered === undefined ? null : truth,
|
|
1961
|
+
matcher,
|
|
1962
|
+
notes,
|
|
1963
|
+
spend,
|
|
1964
|
+
columns,
|
|
1965
|
+
raw,
|
|
1966
|
+
members: new Map(),
|
|
1967
|
+
faithful: new Map(),
|
|
1968
|
+
degrees: new Map(),
|
|
1969
|
+
};
|
|
1970
|
+
const drafts: Draft[] = [];
|
|
1971
|
+
for (const a of applied) {
|
|
1972
|
+
const props = a.props.filter((p) => !p.spec.property.startsWith("_"));
|
|
1973
|
+
if (props.length === 0 || a.list.length === 0) {
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
drafts.push(draftOf(ctx, a.entry, a.list, props));
|
|
1977
|
+
}
|
|
1978
|
+
const rules = collapse(ctx, drafts);
|
|
1979
|
+
return { rules, columns, rawColumns: [...raw] };
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/** The state shared while rules are built. */
|
|
1983
|
+
interface RuleContext {
|
|
1984
|
+
readonly kind: StyleTarget;
|
|
1985
|
+
readonly els: readonly CyjsElement[];
|
|
1986
|
+
readonly coreToOut: Int32Array;
|
|
1987
|
+
readonly out: number;
|
|
1988
|
+
readonly snapshot: GraphSnapshot | undefined;
|
|
1989
|
+
/** What a live core drew, when given: the values a function-valued property is copied from. */
|
|
1990
|
+
readonly truth: Readonly<Record<string, readonly (StyleValue | null)[]>> | null;
|
|
1991
|
+
readonly matcher: Matcher;
|
|
1992
|
+
readonly notes: StyleNotes;
|
|
1993
|
+
readonly spend: (ops: number) => void;
|
|
1994
|
+
readonly columns: Map<string, StyleColumn>;
|
|
1995
|
+
readonly raw: Set<string>;
|
|
1996
|
+
readonly members: Map<string, string>;
|
|
1997
|
+
readonly faithful: Map<string, boolean>;
|
|
1998
|
+
readonly degrees: Map<string, boolean>;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/** A rule with what collapsing needs. */
|
|
2002
|
+
interface Draft {
|
|
2003
|
+
/** Builds the rule (and its predicate's column) when it stays a rule of its own. */
|
|
2004
|
+
readonly rule: () => StyleRule;
|
|
2005
|
+
/** The rule's id. */
|
|
2006
|
+
readonly id: string;
|
|
2007
|
+
/** Its constants. */
|
|
2008
|
+
readonly set: Readonly<Record<string, StyleValue>>;
|
|
2009
|
+
/** Cytoscape.js's own default rule. */
|
|
2010
|
+
readonly tool: boolean;
|
|
2011
|
+
/** For a rule of one class, one id or one value test that only sets values: its kind and key. */
|
|
2012
|
+
readonly single: { readonly sig: string; readonly key: Scalar } | null;
|
|
2013
|
+
/** The matched core elements. */
|
|
2014
|
+
readonly list: readonly number[];
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* One entry as a rule.
|
|
2019
|
+
* @param ctx - the shared state
|
|
2020
|
+
* @param entry - the entry
|
|
2021
|
+
* @param list - the matched core elements
|
|
2022
|
+
* @param props - the carried properties
|
|
2023
|
+
* @returns the draft
|
|
2024
|
+
*/
|
|
2025
|
+
function draftOf(ctx: RuleContext, entry: Entry, list: readonly number[], props: readonly EntryProperty[]): Draft {
|
|
2026
|
+
const setting = new Map<string, StyleValue>();
|
|
2027
|
+
const mapping = new Map<string, Mapping>();
|
|
2028
|
+
let flattened = false;
|
|
2029
|
+
const id = entry.tool
|
|
2030
|
+
? `${ctx.kind}.cytoscape-js.${String(entry.index + CYJS_DEFAULT_STYLESHEET.length)}`
|
|
2031
|
+
: `${ctx.kind}.sheet.${String(entry.index)}`;
|
|
2032
|
+
for (const p of props) {
|
|
2033
|
+
// a later duplicate of a property replaces the earlier one
|
|
2034
|
+
setting.delete(p.spec.property);
|
|
2035
|
+
mapping.delete(p.spec.property);
|
|
2036
|
+
const v = p.parsed;
|
|
2037
|
+
if (v.kind === "fn") {
|
|
2038
|
+
const copied = functionCopy(ctx, entry, list, p.spec.property);
|
|
2039
|
+
if (copied !== null) {
|
|
2040
|
+
mapping.set(p.spec.property, copied);
|
|
2041
|
+
flattened = true;
|
|
2042
|
+
}
|
|
2043
|
+
} else if (isFlat(v)) {
|
|
2044
|
+
const neutral = cyjsNeutral(p.spec, v, {
|
|
2045
|
+
notes: new StyleNotes(),
|
|
2046
|
+
target: ctx.kind,
|
|
2047
|
+
property: p.spec.property,
|
|
2048
|
+
});
|
|
2049
|
+
if (neutral !== undefined) {
|
|
2050
|
+
setting.set(p.spec.property, neutral);
|
|
2051
|
+
}
|
|
2052
|
+
} else {
|
|
2053
|
+
mapping.set(p.spec.property, mapperMapping(ctx, entry, list, p));
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
const set = Object.fromEntries(setting);
|
|
2057
|
+
const map = Object.fromEntries(mapping);
|
|
2058
|
+
const hasMap = mapping.size > 0;
|
|
2059
|
+
// the predicate is built only for a rule that is not collapsed into a run: a run's rules need no column each
|
|
2060
|
+
const rule = (): StyleRule => {
|
|
2061
|
+
const where = predicateOf(ctx, entry, list);
|
|
2062
|
+
return {
|
|
2063
|
+
id,
|
|
2064
|
+
target: ctx.kind,
|
|
2065
|
+
role: roleOf(hasMap, where),
|
|
2066
|
+
origin: entry.tool ? "tool" : "declared",
|
|
2067
|
+
where,
|
|
2068
|
+
...(setting.size > 0 ? { set } : {}),
|
|
2069
|
+
...(hasMap ? { map } : {}),
|
|
2070
|
+
facts: flattened ? { elements: list.length, reason: "FUNCTION" } : { elements: list.length },
|
|
2071
|
+
};
|
|
2072
|
+
};
|
|
2073
|
+
return {
|
|
2074
|
+
rule,
|
|
2075
|
+
id,
|
|
2076
|
+
set,
|
|
2077
|
+
tool: entry.tool,
|
|
2078
|
+
single: hasMap || entry.tool ? null : singleTest(entry.selector, ctx.kind),
|
|
2079
|
+
list,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/**
|
|
2084
|
+
* A function-valued property: its values as the live core drew them, copied per element into a style column.
|
|
2085
|
+
* @param ctx - the shared state
|
|
2086
|
+
* @param entry - the entry
|
|
2087
|
+
* @param list - the matched core elements
|
|
2088
|
+
* @param property - the neutral property
|
|
2089
|
+
* @returns the passthrough of the copy, or null without a live core's values
|
|
2090
|
+
*/
|
|
2091
|
+
function functionCopy(ctx: RuleContext, entry: Entry, list: readonly number[], property: string): Mapping | null {
|
|
2092
|
+
const drawn = ctx.truth?.[property];
|
|
2093
|
+
if (drawn === undefined) {
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
const key = `function.${String(entry.index)}.${property}`;
|
|
2097
|
+
const col = new Array<StyleValue | null>(ctx.out).fill(null);
|
|
2098
|
+
for (const i of list) {
|
|
2099
|
+
const k = ctx.coreToOut[i];
|
|
2100
|
+
if (k >= 0) {
|
|
2101
|
+
col[k] = drawn[k];
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
ctx.columns.set(key, col);
|
|
2105
|
+
return { kind: "passthrough", by: { style: key } };
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* A rule's role from what it does.
|
|
2110
|
+
* @param hasMap - whether it maps values
|
|
2111
|
+
* @param where - its predicate
|
|
2112
|
+
* @returns "mapping", "defaults" (constants on every element) or "group"
|
|
2113
|
+
*/
|
|
2114
|
+
function roleOf(hasMap: boolean, where: Predicate): string {
|
|
2115
|
+
if (hasMap) {
|
|
2116
|
+
return "mapping";
|
|
2117
|
+
}
|
|
2118
|
+
return "all" in where ? "defaults" : "group";
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* The varying test of a single-query selector, for collapsing runs: its last class, id or value test, with the other
|
|
2123
|
+
* tests (a shared scope such as `[graphty_import = "a"]`) in the signature so a run only joins rules that agree on them.
|
|
2124
|
+
* @param sel - the selector
|
|
2125
|
+
* @param kind - the target
|
|
2126
|
+
* @returns the run signature and the rule's key, or null
|
|
2127
|
+
*/
|
|
2128
|
+
function singleTest(sel: CyjsSelector, kind: StyleTarget): { sig: string; key: Scalar } | null {
|
|
2129
|
+
if (sel.invalid || sel.queries.length !== 1) {
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
const checks = sel.queries[0].checks.filter(
|
|
2133
|
+
(c) => !(c.type === "group" && (c.value === "*" || c.value === `${kind}s`)),
|
|
2134
|
+
);
|
|
2135
|
+
const c = checks.at(-1);
|
|
2136
|
+
if (c === undefined || checks.slice(0, -1).some((x) => x.type !== "compare" && x.type !== "class")) {
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
const context = JSON.stringify(checks.slice(0, -1));
|
|
2140
|
+
if (c.type === "class" && typeof c.value === "string") {
|
|
2141
|
+
return { sig: `class\u0000${context}`, key: c.value };
|
|
2142
|
+
}
|
|
2143
|
+
if (c.type === "id") {
|
|
2144
|
+
return { sig: `id\u0000${context}`, key: c.value };
|
|
2145
|
+
}
|
|
2146
|
+
if (c.type === "compare" && c.op === "=" && !c.not && !c.ci) {
|
|
2147
|
+
return { sig: `eq\u0000${context}\u0000${c.field}`, key: c.value };
|
|
2148
|
+
}
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* Whether graph-io's column holds exactly what a selector reads for every element: the same value (`===`) where the
|
|
2154
|
+
* raw data has a scalar, nothing where it has none, and no raw null or false (a selector sees those, a column does
|
|
2155
|
+
* not).
|
|
2156
|
+
* @param ctx - the shared state
|
|
2157
|
+
* @param field - the field
|
|
2158
|
+
* @returns true when a predicate over the column selects what the selector selects
|
|
2159
|
+
*/
|
|
2160
|
+
function faithful(ctx: RuleContext, field: string): boolean {
|
|
2161
|
+
const known = ctx.faithful.get(field);
|
|
2162
|
+
if (known !== undefined) {
|
|
2163
|
+
return known;
|
|
2164
|
+
}
|
|
2165
|
+
let ok = ctx.snapshot !== undefined;
|
|
2166
|
+
const table = ctx.kind === "node" ? ctx.snapshot?.nodes : ctx.snapshot?.edges;
|
|
2167
|
+
const has = table?.names().includes(field) ?? false;
|
|
2168
|
+
for (let c = 0; ok && c < ctx.els.length; c++) {
|
|
2169
|
+
const k = ctx.coreToOut[c];
|
|
2170
|
+
const rv = selectorField(ctx.els[c], field);
|
|
2171
|
+
if (rv === null || rv === false) {
|
|
2172
|
+
ok = false;
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
const gv = k >= 0 && has && table !== undefined ? plainCell(table.value(field, k)) : null;
|
|
2176
|
+
if (rv === undefined ? gv !== null : rv !== gv) {
|
|
2177
|
+
ok = false;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
ctx.spend(ctx.els.length);
|
|
2181
|
+
ctx.faithful.set(field, ok);
|
|
2182
|
+
return ok;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/**
|
|
2186
|
+
* Whether the graph's degree equals Cytoscape.js's for every node.
|
|
2187
|
+
* @param ctx - the shared state
|
|
2188
|
+
* @param which - the degree
|
|
2189
|
+
* @returns true when a predicate over the computed degree selects what the selector selects
|
|
2190
|
+
*/
|
|
2191
|
+
function degreeAgrees(ctx: RuleContext, which: "degree" | "indegree" | "outdegree"): boolean {
|
|
2192
|
+
const known = ctx.degrees.get(which);
|
|
2193
|
+
if (known !== undefined) {
|
|
2194
|
+
return known;
|
|
2195
|
+
}
|
|
2196
|
+
let ok = ctx.snapshot !== undefined && ctx.kind === "node" && (which === "degree" || ctx.snapshot.directed);
|
|
2197
|
+
if (ok && ctx.snapshot !== undefined) {
|
|
2198
|
+
const mine = degreeValues(ctx.snapshot, which);
|
|
2199
|
+
for (let c = 0; c < ctx.els.length; c++) {
|
|
2200
|
+
const k = ctx.coreToOut[c];
|
|
2201
|
+
if (k < 0 || mine[k] !== ctx.matcher[which][c]) {
|
|
2202
|
+
ok = false;
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
ok &&= ctx.els.length === ctx.out;
|
|
2207
|
+
}
|
|
2208
|
+
ctx.degrees.set(which, ok);
|
|
2209
|
+
return ok;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
/**
|
|
2213
|
+
* A selector as a short predicate when one expresses it exactly, else a membership style column.
|
|
2214
|
+
* @param ctx - the shared state
|
|
2215
|
+
* @param entry - the entry
|
|
2216
|
+
* @param list - the matched core elements
|
|
2217
|
+
* @returns the predicate
|
|
2218
|
+
*/
|
|
2219
|
+
function predicateOf(ctx: RuleContext, entry: Entry, list: readonly number[]): Predicate {
|
|
2220
|
+
const short = shortPredicate(ctx, entry.selector);
|
|
2221
|
+
if (short !== null) {
|
|
2222
|
+
return short;
|
|
2223
|
+
}
|
|
2224
|
+
if (list.length === ctx.els.length && ctx.els.length === ctx.out) {
|
|
2225
|
+
return { all: true };
|
|
2226
|
+
}
|
|
2227
|
+
const sig = `${entry.tool ? "tool" : "file"}\u0000${entry.text}`;
|
|
2228
|
+
let key = ctx.members.get(sig);
|
|
2229
|
+
if (key === undefined) {
|
|
2230
|
+
key = `member.${String(ctx.members.size + 1)}`;
|
|
2231
|
+
ctx.members.set(sig, key);
|
|
2232
|
+
const col = new Array<StyleValue | null>(ctx.out).fill(null);
|
|
2233
|
+
for (const i of list) {
|
|
2234
|
+
const k = ctx.coreToOut[i];
|
|
2235
|
+
if (k >= 0) {
|
|
2236
|
+
col[k] = true;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
ctx.spend(ctx.out + list.length);
|
|
2240
|
+
ctx.columns.set(key, col);
|
|
2241
|
+
ctx.notes.add(STYLE_ISSUE.SELECTOR_AS_COLUMN, ctx.kind, key, { sample: entry.text });
|
|
2242
|
+
}
|
|
2243
|
+
return { has: { style: key } };
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/**
|
|
2247
|
+
* The short predicate of a selector, or null: every query of one alternative with at most 8 tests, each a group,
|
|
2248
|
+
* a value test or an existence test over a column graph-io holds exactly, or a degree test the graph computes the
|
|
2249
|
+
* same way; several alternatives only as equal tests of one field (`in`).
|
|
2250
|
+
* @param ctx - the shared state
|
|
2251
|
+
* @param sel - the selector
|
|
2252
|
+
* @returns the predicate, or null
|
|
2253
|
+
*/
|
|
2254
|
+
function shortPredicate(ctx: RuleContext, sel: CyjsSelector): Predicate | null {
|
|
2255
|
+
if (sel.invalid) {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
const alts: Predicate[][] = [];
|
|
2259
|
+
for (const q of sel.queries) {
|
|
2260
|
+
const terms = queryTerms(ctx, q);
|
|
2261
|
+
if (terms === null) {
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
if (terms !== "never") {
|
|
2265
|
+
alts.push(terms);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (alts.length === 0) {
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
if (alts.some((t) => t.length === 0)) {
|
|
2272
|
+
return ctx.els.length === ctx.out ? { all: true } : null;
|
|
2273
|
+
}
|
|
2274
|
+
if (alts.length === 1) {
|
|
2275
|
+
const t = alts[0];
|
|
2276
|
+
if (t.length > 8) {
|
|
2277
|
+
return null;
|
|
2278
|
+
}
|
|
2279
|
+
return t.length === 1 ? t[0] : { and: t };
|
|
2280
|
+
}
|
|
2281
|
+
return equalTests(alts);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* A query's checks as short predicate terms.
|
|
2286
|
+
* @param ctx - the shared state
|
|
2287
|
+
* @param q - the query
|
|
2288
|
+
* @returns the terms; "never" when its group test excludes the target; null when a check has no short term
|
|
2289
|
+
*/
|
|
2290
|
+
function queryTerms(ctx: RuleContext, q: CyjsQuery): Predicate[] | "never" | null {
|
|
2291
|
+
const terms: Predicate[] = [];
|
|
2292
|
+
let never = false;
|
|
2293
|
+
for (const c of q.checks) {
|
|
2294
|
+
if (c.type === "group") {
|
|
2295
|
+
never ||= c.value !== "*" && c.value !== `${ctx.kind}s`;
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
const t = termOf(ctx, c);
|
|
2299
|
+
if (t === null) {
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
terms.push(t);
|
|
2303
|
+
}
|
|
2304
|
+
return never ? "never" : terms;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/**
|
|
2308
|
+
* Several alternatives as one `in` predicate: only equal tests of one column, at most 8.
|
|
2309
|
+
* @param alts - the alternatives' terms
|
|
2310
|
+
* @returns the predicate, or null
|
|
2311
|
+
*/
|
|
2312
|
+
function equalTests(alts: readonly Predicate[][]): Predicate | null {
|
|
2313
|
+
if (alts.length > 8 || !alts.every((t) => t.length === 1 && "eq" in t[0])) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
const by = (alts[0][0] as { eq: readonly [ColumnRef, Scalar] }).eq[0];
|
|
2317
|
+
const values: Scalar[] = [];
|
|
2318
|
+
for (const t of alts) {
|
|
2319
|
+
const [r, v] = (t[0] as { eq: readonly [ColumnRef, Scalar] }).eq;
|
|
2320
|
+
if (JSON.stringify(r) !== JSON.stringify(by)) {
|
|
2321
|
+
return null;
|
|
2322
|
+
}
|
|
2323
|
+
values.push(v);
|
|
2324
|
+
}
|
|
2325
|
+
return { in: [by, values] };
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
/**
|
|
2329
|
+
* One check as a short predicate term, or null.
|
|
2330
|
+
* @param ctx - the shared state
|
|
2331
|
+
* @param c - the check
|
|
2332
|
+
* @returns the term, or null
|
|
2333
|
+
*/
|
|
2334
|
+
function termOf(ctx: RuleContext, c: CyjsCheck): Predicate | null {
|
|
2335
|
+
switch (c.type) {
|
|
2336
|
+
case "compare":
|
|
2337
|
+
if (c.op !== "=" || c.ci || !faithful(ctx, c.field)) {
|
|
2338
|
+
return null;
|
|
2339
|
+
}
|
|
2340
|
+
return c.not ? { not: { eq: [{ data: c.field }, c.value] } } : { eq: [{ data: c.field }, c.value] };
|
|
2341
|
+
case "exist":
|
|
2342
|
+
return faithful(ctx, c.field) ? { has: { data: c.field } } : null;
|
|
2343
|
+
case "bool":
|
|
2344
|
+
return c.op === "^" && faithful(ctx, c.field) ? { not: { has: { data: c.field } } } : null;
|
|
2345
|
+
case "meta":
|
|
2346
|
+
if (c.ci || !degreeAgrees(ctx, c.field)) {
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2349
|
+
return degreeTerm(c.field, c.op, c.not, c.value);
|
|
2350
|
+
default:
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* A degree test as a predicate over the computed degree, so it follows later edits: `=` as `eq`, and an order test
|
|
2357
|
+
* as the degrees it keeps or the degrees it leaves out (degrees are whole numbers from 0), at most 8 of them.
|
|
2358
|
+
* @param field - the degree
|
|
2359
|
+
* @param op - the comparison
|
|
2360
|
+
* @param not - whether the test is negated
|
|
2361
|
+
* @param value - the number compared with
|
|
2362
|
+
* @returns the predicate, or null when it needs more than 8 values
|
|
2363
|
+
*/
|
|
2364
|
+
function degreeTerm(
|
|
2365
|
+
field: "degree" | "indegree" | "outdegree",
|
|
2366
|
+
op: string,
|
|
2367
|
+
not: boolean,
|
|
2368
|
+
value: number,
|
|
2369
|
+
): Predicate | null {
|
|
2370
|
+
const ref: ColumnRef = { computed: field };
|
|
2371
|
+
if (op === "=") {
|
|
2372
|
+
return not ? { not: { eq: [ref, value] } } : { eq: [ref, value] };
|
|
2373
|
+
}
|
|
2374
|
+
// The whole numbers from 0 the test leaves out (`>`, `>=`) or keeps (`<`, `<=`).
|
|
2375
|
+
let below: number;
|
|
2376
|
+
if (op === ">" || op === "<=") {
|
|
2377
|
+
below = Math.floor(value) + 1;
|
|
2378
|
+
} else if (op === ">=" || op === "<") {
|
|
2379
|
+
below = Math.ceil(value);
|
|
2380
|
+
} else {
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
if (!Number.isFinite(below) || below < 1 || below > 8) {
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
const values = Array.from({ length: below }, (_, k) => k);
|
|
2387
|
+
const keeps = op === "<" || op === "<=";
|
|
2388
|
+
const inSet: Predicate = { in: [ref, values] };
|
|
2389
|
+
return keeps !== not ? inSet : { not: inSet };
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
/**
|
|
2393
|
+
* A mapper as a neutral mapping over graph-io's column when that gives Cytoscape.js's values for every element the
|
|
2394
|
+
* rule applies to, else the mapper's per-element values as a style column (`value.<rule>.<property>`).
|
|
2395
|
+
* @param ctx - the shared state
|
|
2396
|
+
* @param entry - the entry
|
|
2397
|
+
* @param list - the matched core elements
|
|
2398
|
+
* @param p - the property
|
|
2399
|
+
* @returns the mapping
|
|
2400
|
+
*/
|
|
2401
|
+
function mapperMapping(ctx: RuleContext, entry: Entry, list: readonly number[], p: EntryProperty): Mapping {
|
|
2402
|
+
const v = p.parsed as CyjsParsed & { kind: "data" | "mapData" };
|
|
2403
|
+
const want = new Array<StyleValue | null>(list.length);
|
|
2404
|
+
const site: CanonSite = { notes: new StyleNotes(), target: ctx.kind, property: p.spec.property };
|
|
2405
|
+
for (let k = 0; k < list.length; k++) {
|
|
2406
|
+
const c = cellFor(p, ctx.els[list[k]]);
|
|
2407
|
+
const neutral = neutralOfCell(p.spec, p.name, c, site);
|
|
2408
|
+
want[k] = neutral ?? null;
|
|
2409
|
+
}
|
|
2410
|
+
ctx.spend(list.length * 2);
|
|
2411
|
+
const candidate = candidateMapping(ctx, v, p);
|
|
2412
|
+
if (candidate !== null && ctx.snapshot !== undefined) {
|
|
2413
|
+
const exact = givesWanted(
|
|
2414
|
+
ctx,
|
|
2415
|
+
ctx.snapshot,
|
|
2416
|
+
candidate,
|
|
2417
|
+
{ field: v.field, property: p.spec.property },
|
|
2418
|
+
list,
|
|
2419
|
+
want,
|
|
2420
|
+
);
|
|
2421
|
+
ctx.spend(list.length);
|
|
2422
|
+
if (exact) {
|
|
2423
|
+
ctx.raw.add(v.field);
|
|
2424
|
+
return candidate;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
const key = `value.${entry.tool ? "tool" : String(entry.index)}.${p.spec.property}`;
|
|
2428
|
+
const col = new Array<StyleValue | null>(ctx.out).fill(null);
|
|
2429
|
+
for (let k = 0; k < list.length; k++) {
|
|
2430
|
+
const o = ctx.coreToOut[list[k]];
|
|
2431
|
+
if (o >= 0) {
|
|
2432
|
+
col[o] = want[k];
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
ctx.columns.set(key, col);
|
|
2436
|
+
return { kind: "passthrough", by: { style: key } };
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
/**
|
|
2440
|
+
* Whether a mapping over graph-io's column gives Cytoscape.js's value for every element a rule applies to.
|
|
2441
|
+
* @param ctx - the shared state
|
|
2442
|
+
* @param snapshot - the graph
|
|
2443
|
+
* @param candidate - the mapping
|
|
2444
|
+
* @param what - the column and the neutral property
|
|
2445
|
+
* @param what.field - the column
|
|
2446
|
+
* @param what.property - the neutral property
|
|
2447
|
+
* @param list - the matched core elements
|
|
2448
|
+
* @param want - Cytoscape.js's value of each, in list order
|
|
2449
|
+
* @returns true when it gives every one
|
|
2450
|
+
*/
|
|
2451
|
+
function givesWanted(
|
|
2452
|
+
ctx: RuleContext,
|
|
2453
|
+
snapshot: GraphSnapshot,
|
|
2454
|
+
candidate: (Mapping & { kind: "passthrough" }) | (Mapping & { kind: "continuous" }),
|
|
2455
|
+
what: { readonly field: string; readonly property: string },
|
|
2456
|
+
list: readonly number[],
|
|
2457
|
+
want: readonly (StyleValue | null)[],
|
|
2458
|
+
): boolean {
|
|
2459
|
+
const table = ctx.kind === "node" ? snapshot.nodes : snapshot.edges;
|
|
2460
|
+
if (!table.names().includes(what.field)) {
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
const canon = canonicalFor(ctx.kind, what.property);
|
|
2464
|
+
for (let k = 0; k < list.length; k++) {
|
|
2465
|
+
const o = ctx.coreToOut[list[k]];
|
|
2466
|
+
const cell = o < 0 ? null : plainCell(table.value(what.field, o));
|
|
2467
|
+
let got: StyleValue | null = null;
|
|
2468
|
+
if (cell !== null && !Array.isArray(cell)) {
|
|
2469
|
+
got = candidate.kind === "passthrough" ? canon(cell) : evalContinuous(candidate.points, numberOf(cell));
|
|
2470
|
+
}
|
|
2471
|
+
if (o >= 0 && got !== want[k]) {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* The neutral mapping a mapper would be over its field, before it is checked.
|
|
2480
|
+
* @param ctx - the shared state
|
|
2481
|
+
* @param v - the mapper
|
|
2482
|
+
* @param p - the property
|
|
2483
|
+
* @returns a passthrough for `data()`, a two-point continuous mapping for `mapData()`, or null
|
|
2484
|
+
*/
|
|
2485
|
+
function candidateMapping(
|
|
2486
|
+
ctx: RuleContext,
|
|
2487
|
+
v: CyjsParsed & { kind: "data" | "mapData" },
|
|
2488
|
+
p: EntryProperty,
|
|
2489
|
+
): (Mapping & { kind: "passthrough" }) | (Mapping & { kind: "continuous" }) | null {
|
|
2490
|
+
if (v.kind === "data") {
|
|
2491
|
+
return { kind: "passthrough", by: { data: v.field } };
|
|
2492
|
+
}
|
|
2493
|
+
if (v.fieldMin === v.fieldMax) {
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
const site: CanonSite = { notes: new StyleNotes(), target: ctx.kind, property: p.spec.property };
|
|
2497
|
+
const a = cyjsNeutral(p.spec, v.min, site);
|
|
2498
|
+
const b = cyjsNeutral(p.spec, v.max, site);
|
|
2499
|
+
if (a === undefined || b === undefined) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
const lo: ContinuousPoint = { at: v.fieldMin, below: a, value: a, above: a };
|
|
2503
|
+
const hi: ContinuousPoint = { at: v.fieldMax, below: b, value: b, above: b };
|
|
2504
|
+
return { kind: "continuous", by: { data: v.field }, points: v.fieldMin < v.fieldMax ? [lo, hi] : [hi, lo] };
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
/**
|
|
2508
|
+
* Collapses runs of three or more rules that each test one class, one id or one value of the same field and only set
|
|
2509
|
+
* values into ONE mapping rule: per property, a winner column holds the key (class, id or value) of the last rule in
|
|
2510
|
+
* the run that matches the element and sets the property (the cascade's winner), and a discrete table maps each key
|
|
2511
|
+
* to that rule's value. A run keyed by a value whose column graph-io holds exactly reads that column instead.
|
|
2512
|
+
* @param ctx - the shared state
|
|
2513
|
+
* @param drafts - the rules in order
|
|
2514
|
+
* @returns the rules
|
|
2515
|
+
*/
|
|
2516
|
+
function collapse(ctx: RuleContext, drafts: readonly Draft[]): StyleRule[] {
|
|
2517
|
+
const out: StyleRule[] = [];
|
|
2518
|
+
let run = 0;
|
|
2519
|
+
let i = 0;
|
|
2520
|
+
while (i < drafts.length) {
|
|
2521
|
+
const sig = drafts[i].single?.sig ?? null;
|
|
2522
|
+
let j = i + 1;
|
|
2523
|
+
while (sig !== null && j < drafts.length && drafts[j].single?.sig === sig) {
|
|
2524
|
+
j++;
|
|
2525
|
+
}
|
|
2526
|
+
if (sig === null || j - i < 3) {
|
|
2527
|
+
for (const d of drafts.slice(i, j)) {
|
|
2528
|
+
const rule = d.rule();
|
|
2529
|
+
// Cytoscape.js's own default rule over every element: the writing tool's constants already say it
|
|
2530
|
+
if (!(d.tool && "all" in rule.where)) {
|
|
2531
|
+
out.push(rule);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
run++;
|
|
2536
|
+
out.push(runRule(ctx, drafts.slice(i, j), sig, run));
|
|
2537
|
+
}
|
|
2538
|
+
i = j;
|
|
2539
|
+
}
|
|
2540
|
+
return out;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
/**
|
|
2544
|
+
* A run of rules collapsed into one mapping rule.
|
|
2545
|
+
* @param ctx - the shared state
|
|
2546
|
+
* @param members - the run's rules, in order
|
|
2547
|
+
* @param sig - their shared signature
|
|
2548
|
+
* @param run - the run's number
|
|
2549
|
+
* @returns the rule
|
|
2550
|
+
*/
|
|
2551
|
+
function runRule(ctx: RuleContext, members: readonly Draft[], sig: string, run: number): StyleRule {
|
|
2552
|
+
const props = new Set(members.flatMap((d) => Object.keys(d.set)));
|
|
2553
|
+
// a value run with no shared tests may read the field itself
|
|
2554
|
+
const parts = sig.split("\u0000");
|
|
2555
|
+
const field = parts[0] === "eq" && parts[1] === "[]" ? parts[2] : null;
|
|
2556
|
+
const map: Record<string, Mapping> = {};
|
|
2557
|
+
for (const p of props) {
|
|
2558
|
+
map[p] = runMapping(ctx, members, p, { field, key: `winner.${String(run)}.${p}` });
|
|
2559
|
+
}
|
|
2560
|
+
const first = members[0].id.split(".").pop() ?? "";
|
|
2561
|
+
return {
|
|
2562
|
+
id: `${ctx.kind}.sheet.by.${parts[0] === "eq" ? "value" : parts[0]}.${first}`,
|
|
2563
|
+
target: ctx.kind,
|
|
2564
|
+
role: "mapping",
|
|
2565
|
+
origin: "declared",
|
|
2566
|
+
where: { all: true },
|
|
2567
|
+
map,
|
|
2568
|
+
facts: { elements: members.reduce((sum, d) => sum + d.list.length, 0) },
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
/**
|
|
2573
|
+
* One property of a collapsed run: a winner column holds the key of the last rule in the run that matches the
|
|
2574
|
+
* element and sets the property (the cascade's winner), and a discrete table maps each key to that rule's value; a
|
|
2575
|
+
* run keyed by a value whose column graph-io holds exactly reads that column instead.
|
|
2576
|
+
* @param ctx - the shared state
|
|
2577
|
+
* @param members - the run's rules, in order
|
|
2578
|
+
* @param p - the property
|
|
2579
|
+
* @param names - the field a value run tests (null for another run) and the winner column's key
|
|
2580
|
+
* @param names.field - the field a value run tests, or null
|
|
2581
|
+
* @param names.key - the winner column's key
|
|
2582
|
+
* @returns the mapping
|
|
2583
|
+
*/
|
|
2584
|
+
function runMapping(
|
|
2585
|
+
ctx: RuleContext,
|
|
2586
|
+
members: readonly Draft[],
|
|
2587
|
+
p: string,
|
|
2588
|
+
names: { readonly field: string | null; readonly key: string },
|
|
2589
|
+
): Mapping {
|
|
2590
|
+
const winner = new Array<StyleValue | null>(ctx.out).fill(null);
|
|
2591
|
+
const table = new Map<string, [Scalar, StyleValue]>();
|
|
2592
|
+
for (const d of members) {
|
|
2593
|
+
const value = (d.set as Record<string, StyleValue | undefined>)[p];
|
|
2594
|
+
if (value === undefined || d.single === null) {
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
const { key } = d.single;
|
|
2598
|
+
table.set(`${typeof key}:${String(key)}`, [key, value]);
|
|
2599
|
+
for (const c of d.list) {
|
|
2600
|
+
const k = ctx.coreToOut[c];
|
|
2601
|
+
if (k >= 0) {
|
|
2602
|
+
winner[k] = key;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
ctx.spend(d.list.length);
|
|
2606
|
+
}
|
|
2607
|
+
const entries = [...table.values()];
|
|
2608
|
+
// a table key that names a later rule's value: the table maps each key to the last rule's value for it
|
|
2609
|
+
const byKey = new Map(entries.map(([k, v]) => [`${typeof k}:${String(k)}`, v]));
|
|
2610
|
+
const { field } = names;
|
|
2611
|
+
if (field !== null && faithful(ctx, field) && sameByData(ctx, field, winner, byKey)) {
|
|
2612
|
+
return { kind: "discrete", by: { data: field }, table: entries };
|
|
2613
|
+
}
|
|
2614
|
+
ctx.columns.set(names.key, winner);
|
|
2615
|
+
return { kind: "discrete", by: { style: names.key }, table: entries };
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
/**
|
|
2619
|
+
* Whether a discrete table keyed by a column's own values gives every element its winner's value.
|
|
2620
|
+
* @param ctx - the shared state
|
|
2621
|
+
* @param field - the column
|
|
2622
|
+
* @param winner - each element's winning key, or null
|
|
2623
|
+
* @param byKey - the table
|
|
2624
|
+
* @returns true when reading the column gives the same values
|
|
2625
|
+
*/
|
|
2626
|
+
function sameByData(
|
|
2627
|
+
ctx: RuleContext,
|
|
2628
|
+
field: string,
|
|
2629
|
+
winner: readonly (StyleValue | null)[],
|
|
2630
|
+
byKey: ReadonlyMap<string, StyleValue>,
|
|
2631
|
+
): boolean {
|
|
2632
|
+
const table = ctx.kind === "node" ? ctx.snapshot?.nodes : ctx.snapshot?.edges;
|
|
2633
|
+
if (!table?.names().includes(field)) {
|
|
2634
|
+
return false;
|
|
2635
|
+
}
|
|
2636
|
+
for (let k = 0; k < ctx.out; k++) {
|
|
2637
|
+
const v = plainCell(table.value(field, k));
|
|
2638
|
+
const viaData =
|
|
2639
|
+
typeof v === "string" || typeof v === "number" ? (byKey.get(`${typeof v}:${String(v)}`) ?? null) : null;
|
|
2640
|
+
const w = winner[k];
|
|
2641
|
+
const viaWinner = w === null ? null : (byKey.get(`${typeof w}:${String(w)}`) ?? null);
|
|
2642
|
+
if (viaData !== viaWinner) {
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return true;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// ============================================================ the importer path
|
|
2650
|
+
|
|
2651
|
+
/**
|
|
2652
|
+
* A Cytoscape.js file's styles as an importer's `styles()` returns them: what Cytoscape.js draws per element and the
|
|
2653
|
+
* stylesheet's rules, with Cytoscape.js as the writing tool whose defaults fill what the stylesheet leaves unset.
|
|
2654
|
+
* @param input - the elements, the stylesheet, the graph and the options
|
|
2655
|
+
* @returns the styles for graph-io's planner
|
|
2656
|
+
*/
|
|
2657
|
+
export function cytoscapeJsStyles(input: CyjsStyleInput & { readonly snapshot: GraphSnapshot }): ImporterStyles {
|
|
2658
|
+
const notes = input.notes ?? new StyleNotes();
|
|
2659
|
+
const r = evaluateCytoscapeJs({ ...input, notes });
|
|
2660
|
+
const output = (t: CyjsTargetStyles): ReaderOutput => ({ values: t.values, rules: t.rules, columns: t.columns });
|
|
2661
|
+
return {
|
|
2662
|
+
canonical: true,
|
|
2663
|
+
dialect: "cytoscape-js",
|
|
2664
|
+
tool: "cytoscape-js",
|
|
2665
|
+
units: "px",
|
|
2666
|
+
node: output(r.node),
|
|
2667
|
+
edge: output(r.edge),
|
|
2668
|
+
notes: notes.list(),
|
|
2669
|
+
};
|
|
2670
|
+
}
|