@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,1741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOT styles: every node's, cluster's and edge's attributes read the way Graphviz draws them, with Graphviz's
|
|
3
|
+
* defaults beneath, and a graph's styles written back as Graphviz attributes.
|
|
4
|
+
*
|
|
5
|
+
* Reading follows Graphviz's own rules, measured against `dot` 2.43:
|
|
6
|
+
*
|
|
7
|
+
* - Labels: a node with no `label` shows its name (`\N`), as one tool-default rule over a style column holding each
|
|
8
|
+
* node's name. Escapes are applied in Graphviz's two passes; an HTML label shows its text, a record label its
|
|
9
|
+
* fields, and a `point` draws no label.
|
|
10
|
+
* - Fills: `point` is always filled; `filled` and `radial` fill any shape, `striped` only boxes, `wedged` only
|
|
11
|
+
* ellipses; an empty `fillcolor` falls back to `color`, then light gray.
|
|
12
|
+
* - Colors through Graphviz's color rules: a `colorscheme` applies to its own object only, an unknown color is black,
|
|
13
|
+
* `none` and `invis` are transparent, and a color list is read as Graphviz draws it for that use.
|
|
14
|
+
* - Outlines: `plain`, `plaintext` and `none` draw none. `setlinewidth(n)` is a pen width, `bold` a width of 2.
|
|
15
|
+
* - Sizes: exact with `fixedsize`, an empty label or a `point`; otherwise minimums Graphviz grows to fit the label,
|
|
16
|
+
* carried and noted as `W_STYLE_SIZE_MINIMUM`.
|
|
17
|
+
* - Arrows per `dir`, `arrowhead` and `arrowtail`, with the graph's kind deciding the default: a target arrow on
|
|
18
|
+
* directed edges, as one tool-default rule.
|
|
19
|
+
* - A cluster's container node holds the cluster's attributes: its fill (`bgcolor`, or the fill when `filled`) and
|
|
20
|
+
* outline (`pencolor`, else `color`) are the cluster's, it is drawn as a rectangle, and it has no label by default.
|
|
21
|
+
* Cluster membership is offered to the planner, so clusters whose nodes share a fill become one rule.
|
|
22
|
+
*
|
|
23
|
+
* Writing: Graphviz attributes from what graphty draws (`fillcolor` with `style=filled`, `width` and `height` with
|
|
24
|
+
* `fixedsize=true`, `penwidth`, `style`, arrows with `dir`, font attributes, graph `bgcolor`) replace every style
|
|
25
|
+
* attribute the reader reads. The `graphty.styles` block is the quoted graph attribute `"graphty.styles"`, and the
|
|
26
|
+
* per-element values its rules read are quoted `"graphty.style.<target>.<key>"` attributes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { GraphSnapshot } from "@graphty/graph-format";
|
|
30
|
+
|
|
31
|
+
import { isWritableDotText } from "../../common/escape.js";
|
|
32
|
+
import { inferTextDtype } from "../../common/text.js";
|
|
33
|
+
import { canonColor, colorOpacity } from "../../styles/canon/color.js";
|
|
34
|
+
import { canonEnum } from "../../styles/canon/enums.js";
|
|
35
|
+
import { canonNumber, DOT_POINTS_PER_INCH, num6 } from "../../styles/canon/number.js";
|
|
36
|
+
import { type CanonSite, MAX_STYLE_VALUE_LENGTH, noteAt } from "../../styles/canon/site.js";
|
|
37
|
+
import { type DotEscapeNames, dotLabelText, htmlLabelText, recordLabelText } from "../../styles/canon/text.js";
|
|
38
|
+
import { STYLE_ISSUE } from "../../styles/codes.js";
|
|
39
|
+
import { toolDefaults } from "../../styles/defaults.js";
|
|
40
|
+
import { StyleApproximations } from "../../styles/io/approximations.js";
|
|
41
|
+
import {
|
|
42
|
+
blockStyles,
|
|
43
|
+
parseStyleBlock,
|
|
44
|
+
STYLE_BLOCK_NAME,
|
|
45
|
+
styleBlockLosses,
|
|
46
|
+
writeStyleBlock,
|
|
47
|
+
} from "../../styles/io/block.js";
|
|
48
|
+
import type { ExporterStyleInput } from "../../styles/io/writer.js";
|
|
49
|
+
import { StyleNotes } from "../../styles/notes.js";
|
|
50
|
+
import { styleFingerprint } from "../../styles/plan/features.js";
|
|
51
|
+
import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
|
|
52
|
+
import type {
|
|
53
|
+
ImporterStyles,
|
|
54
|
+
StyleColumn,
|
|
55
|
+
StyleFeature,
|
|
56
|
+
StyleReadContext,
|
|
57
|
+
StyleRule,
|
|
58
|
+
StyleTarget,
|
|
59
|
+
StyleValue,
|
|
60
|
+
StyleWriteCapabilities,
|
|
61
|
+
} from "../../styles/types.js";
|
|
62
|
+
import type { LossNote } from "../../types.js";
|
|
63
|
+
import { type ValueColumns, valueColumns } from "../graphml/styles-yed.js";
|
|
64
|
+
import { CLUSTER_COLUMN, LABEL_ATTRIBUTE } from "./names.js";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The graph attribute holding the `graphty.styles` block of a DOT file graphty wrote.
|
|
68
|
+
* @category Built-in formats
|
|
69
|
+
*/
|
|
70
|
+
export { STYLE_BLOCK_NAME as DOT_STYLE_BLOCK_ATTRIBUTE } from "../../styles/io/block.js";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What the DOT writer holds natively: Graphviz's node, cluster and edge attributes, and the graph's `bgcolor`. The
|
|
74
|
+
* `graphty.styles` block and the per-element values its rules read are quoted attributes.
|
|
75
|
+
* @category Built-in formats
|
|
76
|
+
*/
|
|
77
|
+
export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
|
|
78
|
+
node: Object.freeze([
|
|
79
|
+
"fill",
|
|
80
|
+
"fillOpacity",
|
|
81
|
+
"borderColor",
|
|
82
|
+
"borderWidth",
|
|
83
|
+
"borderStyle",
|
|
84
|
+
"shape",
|
|
85
|
+
"width",
|
|
86
|
+
"height",
|
|
87
|
+
"label",
|
|
88
|
+
"labelColor",
|
|
89
|
+
"labelSize",
|
|
90
|
+
"labelFont",
|
|
91
|
+
"tooltip",
|
|
92
|
+
"visible",
|
|
93
|
+
] as const),
|
|
94
|
+
edge: Object.freeze([
|
|
95
|
+
"color",
|
|
96
|
+
"opacity",
|
|
97
|
+
"width",
|
|
98
|
+
"lineStyle",
|
|
99
|
+
"sourceArrow",
|
|
100
|
+
"targetArrow",
|
|
101
|
+
"arrowSize",
|
|
102
|
+
"label",
|
|
103
|
+
"sourceLabel",
|
|
104
|
+
"targetLabel",
|
|
105
|
+
"labelColor",
|
|
106
|
+
"labelSize",
|
|
107
|
+
"labelFont",
|
|
108
|
+
"tooltip",
|
|
109
|
+
"visible",
|
|
110
|
+
] as const),
|
|
111
|
+
graph: Object.freeze(["background"] as const),
|
|
112
|
+
block: "native",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** The style column holding the name each node shows when it has no label (Graphviz's `\N`). */
|
|
116
|
+
const NAME_KEY = "name";
|
|
117
|
+
|
|
118
|
+
/** The style column marking directed edges, which Graphviz draws with an arrow at the target. */
|
|
119
|
+
const DIRECTED_KEY = "directed";
|
|
120
|
+
|
|
121
|
+
/** The style column marking a cluster's container node. */
|
|
122
|
+
const CLUSTER_KEY = "cluster";
|
|
123
|
+
|
|
124
|
+
/** Every node and cluster attribute the reader reads; the writer replaces them all. */
|
|
125
|
+
const NODE_ATTRIBUTES: readonly string[] = [
|
|
126
|
+
"colorscheme",
|
|
127
|
+
"color",
|
|
128
|
+
"fillcolor",
|
|
129
|
+
"bgcolor",
|
|
130
|
+
"pencolor",
|
|
131
|
+
"style",
|
|
132
|
+
"shape",
|
|
133
|
+
"width",
|
|
134
|
+
"height",
|
|
135
|
+
"fixedsize",
|
|
136
|
+
"regular",
|
|
137
|
+
"penwidth",
|
|
138
|
+
"fontname",
|
|
139
|
+
"fontsize",
|
|
140
|
+
"fontcolor",
|
|
141
|
+
"tooltip",
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/** Every edge attribute the reader reads; the writer replaces them all. */
|
|
145
|
+
const EDGE_ATTRIBUTES: readonly string[] = [
|
|
146
|
+
"colorscheme",
|
|
147
|
+
"color",
|
|
148
|
+
"style",
|
|
149
|
+
"penwidth",
|
|
150
|
+
"fontname",
|
|
151
|
+
"fontsize",
|
|
152
|
+
"fontcolor",
|
|
153
|
+
"tooltip",
|
|
154
|
+
"edgetooltip",
|
|
155
|
+
"arrowhead",
|
|
156
|
+
"arrowtail",
|
|
157
|
+
"dir",
|
|
158
|
+
"arrowsize",
|
|
159
|
+
"headlabel",
|
|
160
|
+
"taillabel",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
/** Attributes the reader notes as not carried. */
|
|
164
|
+
const NOTED_ATTRIBUTES: readonly string[] = ["peripheries", "xlabel"];
|
|
165
|
+
|
|
166
|
+
const CIRCLE_LIKE: ReadonlySet<string> = new Set(["circle", "doublecircle", "mcircle", "square", "msquare"]);
|
|
167
|
+
const BOX_LIKE: ReadonlySet<string> = new Set(["box", "rect", "rectangle", "square"]);
|
|
168
|
+
const ELLIPSE_LIKE: ReadonlySet<string> = new Set(["", "ellipse", "oval", "circle"]);
|
|
169
|
+
const NO_OUTLINE: ReadonlySet<string> = new Set(["plain", "plaintext", "none"]);
|
|
170
|
+
const RECORD: ReadonlySet<string> = new Set(["record", "mrecord"]);
|
|
171
|
+
const TRUE_TEXTS: ReadonlySet<string> = new Set(["true", "yes", "shape"]);
|
|
172
|
+
|
|
173
|
+
/** A point's size when the file gives none: 0.05 inch. */
|
|
174
|
+
const POINT_SIZE = 0.05;
|
|
175
|
+
|
|
176
|
+
/** The size in inches Graphviz gives a circle, a square or a `regular` node that states none. */
|
|
177
|
+
const REGULAR_SIZE = 0.5;
|
|
178
|
+
|
|
179
|
+
/** One attribute of an element as text, or null when it is unset. */
|
|
180
|
+
type Attr = (name: string) => string | null;
|
|
181
|
+
|
|
182
|
+
/** One element's canonical value of a property; null leaves it unset. */
|
|
183
|
+
type Put = (property: string, value: StyleValue | null) => void;
|
|
184
|
+
|
|
185
|
+
/** Where a value of an element was read, by property. */
|
|
186
|
+
type At = (property: string) => CanonSite;
|
|
187
|
+
|
|
188
|
+
/** What a node's label escapes and its defaults depend on. */
|
|
189
|
+
interface NodeContext {
|
|
190
|
+
/** The node's name (its id as text). */
|
|
191
|
+
readonly name: string;
|
|
192
|
+
/** The graph's name, for `\G`. */
|
|
193
|
+
readonly graph: string;
|
|
194
|
+
/** Whether the node is a cluster's container. */
|
|
195
|
+
readonly cluster: boolean;
|
|
196
|
+
/** The HTML strings of the file, for Graphviz's shared string pool. */
|
|
197
|
+
readonly html: ReadonlySet<string>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** What an edge's label escapes depend on. */
|
|
201
|
+
interface EdgeContext {
|
|
202
|
+
/** The names `\E`, `\T`, `\H` and `\G` stand for. */
|
|
203
|
+
readonly names: DotEscapeNames;
|
|
204
|
+
/** Whether the edge is directed. */
|
|
205
|
+
readonly directed: boolean;
|
|
206
|
+
/** The HTML strings of the file. */
|
|
207
|
+
readonly html: ReadonlySet<string>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether a text is an HTML string (balanced angle brackets around the whole text), which graph-io keeps with its
|
|
212
|
+
* brackets and DOT writes bare as `<...>`.
|
|
213
|
+
* @param text - the text
|
|
214
|
+
* @returns true for an HTML string
|
|
215
|
+
*/
|
|
216
|
+
export function isHtmlString(text: string): boolean {
|
|
217
|
+
if (text.length < 2 || !text.startsWith("<") || !text.endsWith(">")) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
let depth = 0;
|
|
221
|
+
for (let i = 0; i < text.length; i++) {
|
|
222
|
+
const c = text.codePointAt(i);
|
|
223
|
+
if (c === 0x3c) {
|
|
224
|
+
depth++;
|
|
225
|
+
} else if (c === 0x3e) {
|
|
226
|
+
depth--;
|
|
227
|
+
if (depth === 0 && i !== text.length - 1) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return depth === 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The value of an attribute cell as text, or null when it is unset.
|
|
237
|
+
* @param table - the node, edge or graph table
|
|
238
|
+
* @param name - the attribute
|
|
239
|
+
* @param i - the row
|
|
240
|
+
* @returns the text
|
|
241
|
+
*/
|
|
242
|
+
function cellText(table: GraphSnapshot["nodes"], name: string, i: number): string | null {
|
|
243
|
+
if (!table.has(name)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const v = plainCell(table.value(name, i));
|
|
247
|
+
if (typeof v === "string") {
|
|
248
|
+
return v;
|
|
249
|
+
}
|
|
250
|
+
return typeof v === "number" || typeof v === "boolean" ? String(v) : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* A text, or null when it is empty.
|
|
255
|
+
* @param v - the text
|
|
256
|
+
* @returns the text
|
|
257
|
+
*/
|
|
258
|
+
function nonEmpty(v: string | null): string | null {
|
|
259
|
+
return v === null || v === "" ? null : v;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The tokens of a `style` value: split on commas, lowercased, blanks before a token skipped (blanks after it make it
|
|
264
|
+
* a token Graphviz does not know, so they are kept).
|
|
265
|
+
* @param raw - the value
|
|
266
|
+
* @param at - where it was read
|
|
267
|
+
* @returns the tokens
|
|
268
|
+
*/
|
|
269
|
+
function styleTokens(raw: string | null, at: CanonSite): string[] {
|
|
270
|
+
if (raw === null) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
if (raw.length > MAX_STYLE_VALUE_LENGTH) {
|
|
274
|
+
noteAt(at, STYLE_ISSUE.VALUE_TOO_LONG, `${String(raw.length)} characters`);
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
return raw
|
|
278
|
+
.toLowerCase()
|
|
279
|
+
.split(",")
|
|
280
|
+
.map((t) => t.replace(/^[ \t\r\n]+/, ""))
|
|
281
|
+
.filter((t) => t !== "");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The pen width a style's `setlinewidth(n)` gives, or null.
|
|
286
|
+
* @param tokens - the style's tokens
|
|
287
|
+
* @returns the width's text
|
|
288
|
+
*/
|
|
289
|
+
function setLineWidth(tokens: readonly string[]): string | null {
|
|
290
|
+
for (const t of tokens) {
|
|
291
|
+
if (t.startsWith("setlinewidth(") && t.endsWith(")")) {
|
|
292
|
+
return t.slice("setlinewidth(".length, -1).trim();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether an attribute is one of Graphviz's true spellings (`true`, `yes`, a non-zero number; `fixedsize` also
|
|
300
|
+
* takes `shape`).
|
|
301
|
+
* @param raw - the value
|
|
302
|
+
* @returns true when set
|
|
303
|
+
*/
|
|
304
|
+
function isTrue(raw: string | null): boolean {
|
|
305
|
+
if (raw === null) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const t = raw.trim().toLowerCase();
|
|
309
|
+
return TRUE_TEXTS.has(t) || (/^-?\d+$/.test(t) && Number(t) !== 0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The text a label draws: an HTML label's text (also for a quoted label equal to an HTML label's content elsewhere
|
|
314
|
+
* in the file, since Graphviz 2.43 marks HTML on a pooled string), a record's fields, the escapes applied.
|
|
315
|
+
* @param raw - the label as the file gives it
|
|
316
|
+
* @param names - what the escapes stand for
|
|
317
|
+
* @param html - the file's HTML strings
|
|
318
|
+
* @param record - whether the node is a record
|
|
319
|
+
* @param at - where it was read
|
|
320
|
+
* @returns the text
|
|
321
|
+
*/
|
|
322
|
+
function labelText(
|
|
323
|
+
raw: string,
|
|
324
|
+
names: DotEscapeNames,
|
|
325
|
+
html: ReadonlySet<string>,
|
|
326
|
+
record: boolean,
|
|
327
|
+
at: CanonSite,
|
|
328
|
+
): string {
|
|
329
|
+
if (isHtmlString(raw)) {
|
|
330
|
+
return htmlLabelText(raw, at);
|
|
331
|
+
}
|
|
332
|
+
if (html.has(`<${raw}>`)) {
|
|
333
|
+
return htmlLabelText(`<${raw}>`, at);
|
|
334
|
+
}
|
|
335
|
+
return dotLabelText(record ? recordLabelText(raw, at) : raw, names, at);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Reads a color into a property and its alpha into the property's opacity.
|
|
340
|
+
* @param raw - the color
|
|
341
|
+
* @param scheme - the object's color scheme
|
|
342
|
+
* @param use - how Graphviz draws it
|
|
343
|
+
* @param property - the color property
|
|
344
|
+
* @param opacity - its opacity property, or null when the alpha is dropped
|
|
345
|
+
* @param put - receives the values
|
|
346
|
+
* @param at - the sites
|
|
347
|
+
* @returns the alpha, or null when the color is absent
|
|
348
|
+
*/
|
|
349
|
+
function readColor(
|
|
350
|
+
raw: string | null,
|
|
351
|
+
scheme: string | null,
|
|
352
|
+
use: "line" | "fill" | "edge",
|
|
353
|
+
property: string,
|
|
354
|
+
opacity: string | null,
|
|
355
|
+
put: Put,
|
|
356
|
+
at: At,
|
|
357
|
+
): number | null {
|
|
358
|
+
if (raw === null) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const c = canonColor(raw, "graphviz", at(property), { scheme, use });
|
|
362
|
+
if (c === null) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
put(property, c.color);
|
|
366
|
+
if (opacity !== null) {
|
|
367
|
+
put(opacity, c.alpha);
|
|
368
|
+
} else if (c.alpha > 0 || property !== "borderColor") {
|
|
369
|
+
// a transparent outline is no outline (the caller sets its width to 0); any other alpha is dropped, noted
|
|
370
|
+
colorOpacity(c, null, at(property), false);
|
|
371
|
+
}
|
|
372
|
+
return c.alpha;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* A length in points as the shortest inches (at least 6 significant digits) that read back as the same length.
|
|
377
|
+
* @param points - the length
|
|
378
|
+
* @returns the inches
|
|
379
|
+
*/
|
|
380
|
+
function inchesOf(points: number): number {
|
|
381
|
+
const want = num6(points);
|
|
382
|
+
for (let digits = 6; digits < 17; digits++) {
|
|
383
|
+
const inches = Number((points / DOT_POINTS_PER_INCH).toPrecision(digits));
|
|
384
|
+
if (num6(inches * DOT_POINTS_PER_INCH) === want) {
|
|
385
|
+
return inches + 0;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return points / DOT_POINTS_PER_INCH;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A size attribute in inches, or null with a note when it is not a non-negative number.
|
|
393
|
+
* @param raw - the value
|
|
394
|
+
* @param at - where it was read
|
|
395
|
+
* @returns the inches
|
|
396
|
+
*/
|
|
397
|
+
function inches(raw: string | null, at: CanonSite): number | null {
|
|
398
|
+
if (raw === null) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const v = raw.trim() === "" ? Number.NaN : Number(raw);
|
|
402
|
+
if (!Number.isFinite(v) || v < 0) {
|
|
403
|
+
noteAt(at, STYLE_ISSUE.BAD_VALUE, raw.slice(0, MAX_STYLE_VALUE_LENGTH));
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
return v;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Reads the attributes every element shares: font, tooltip, visibility.
|
|
411
|
+
* @param get - the element's attributes
|
|
412
|
+
* @param tokens - its style tokens
|
|
413
|
+
* @param scheme - its color scheme
|
|
414
|
+
* @param names - what the tooltip's escapes stand for
|
|
415
|
+
* @param put - receives the values
|
|
416
|
+
* @param at - the sites
|
|
417
|
+
*/
|
|
418
|
+
function readCommon(
|
|
419
|
+
get: Attr,
|
|
420
|
+
tokens: readonly string[],
|
|
421
|
+
scheme: string | null,
|
|
422
|
+
names: DotEscapeNames,
|
|
423
|
+
put: Put,
|
|
424
|
+
at: At,
|
|
425
|
+
): void {
|
|
426
|
+
if (tokens.includes("invis")) {
|
|
427
|
+
put("visible", false);
|
|
428
|
+
}
|
|
429
|
+
readColor(nonEmpty(get("fontcolor")), scheme, "line", "labelColor", null, put, at);
|
|
430
|
+
const size = get("fontsize");
|
|
431
|
+
put("labelSize", size === null ? null : canonNumber(size, at("labelSize")));
|
|
432
|
+
const font = get("fontname");
|
|
433
|
+
if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
|
|
434
|
+
put("labelFont", font);
|
|
435
|
+
}
|
|
436
|
+
const tip = get("tooltip") ?? get("edgetooltip");
|
|
437
|
+
if (tip !== null) {
|
|
438
|
+
put("tooltip", dotLabelText(tip, names, at("tooltip")));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** What the parts of readNode() read: the node's attributes and what is worked out from them once. */
|
|
443
|
+
interface NodeRead {
|
|
444
|
+
readonly get: Attr;
|
|
445
|
+
readonly cx: NodeContext;
|
|
446
|
+
readonly put: Put;
|
|
447
|
+
readonly at: At;
|
|
448
|
+
readonly tokens: readonly string[];
|
|
449
|
+
/** The shape as written, or null. */
|
|
450
|
+
readonly shapeRaw: string | null;
|
|
451
|
+
/** The shape, lowercase ("" when none is written). */
|
|
452
|
+
readonly shape: string;
|
|
453
|
+
/** Whether it is a point (never a cluster). */
|
|
454
|
+
readonly point: boolean;
|
|
455
|
+
readonly scheme: string | null;
|
|
456
|
+
readonly color: string | null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Reads one node, or one cluster's container, the way Graphviz draws it.
|
|
461
|
+
* @param get - its attributes
|
|
462
|
+
* @param cx - its name and kind
|
|
463
|
+
* @param put - receives the values
|
|
464
|
+
* @param at - the sites
|
|
465
|
+
*/
|
|
466
|
+
function readNode(get: Attr, cx: NodeContext, put: Put, at: At): void {
|
|
467
|
+
const tokens = styleTokens(get("style"), at("visible"));
|
|
468
|
+
const shapeRaw = nonEmpty(get("shape"));
|
|
469
|
+
const shape = (shapeRaw ?? "").toLowerCase();
|
|
470
|
+
const n: NodeRead = {
|
|
471
|
+
get,
|
|
472
|
+
cx,
|
|
473
|
+
put,
|
|
474
|
+
at,
|
|
475
|
+
tokens,
|
|
476
|
+
shapeRaw,
|
|
477
|
+
shape,
|
|
478
|
+
point: !cx.cluster && shape === "point",
|
|
479
|
+
scheme: get("colorscheme"),
|
|
480
|
+
color: nonEmpty(get("color")),
|
|
481
|
+
};
|
|
482
|
+
// in a node's label \E stands for nothing (measured with dot 2.43); \T and \H stand for their letters
|
|
483
|
+
const names: DotEscapeNames = cx.cluster ? { graph: cx.name } : { node: cx.name, graph: cx.graph, edge: "" };
|
|
484
|
+
readCommon(get, tokens, n.scheme, names, put, at);
|
|
485
|
+
if (cx.cluster) {
|
|
486
|
+
readClusterFill(n);
|
|
487
|
+
} else {
|
|
488
|
+
readNodeFill(n);
|
|
489
|
+
}
|
|
490
|
+
readNodeOutline(n);
|
|
491
|
+
readNodeShape(n);
|
|
492
|
+
// label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
|
|
493
|
+
const label = n.point ? null : get(LABEL_ATTRIBUTE);
|
|
494
|
+
if (label !== null) {
|
|
495
|
+
put("label", labelText(label, names, cx.html, RECORD.has(shape), at("label")));
|
|
496
|
+
}
|
|
497
|
+
if (!cx.cluster) {
|
|
498
|
+
readSize(get, shape, n.point, label ?? (n.point ? null : cx.name), put, at);
|
|
499
|
+
}
|
|
500
|
+
if (Number(get("peripheries")) > 1 || nonEmpty(get("xlabel")) !== null) {
|
|
501
|
+
noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, nonEmpty(get("xlabel")) === null ? "peripheries" : "xlabel");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Reads a cluster's fill: its fill color when filled, else its background.
|
|
507
|
+
* @param n - the cluster
|
|
508
|
+
*/
|
|
509
|
+
function readClusterFill(n: NodeRead): void {
|
|
510
|
+
const { get, tokens } = n;
|
|
511
|
+
const filled = tokens.includes("filled") || tokens.includes("radial") || tokens.includes("striped");
|
|
512
|
+
const bg = nonEmpty(get("bgcolor"));
|
|
513
|
+
const raw = filled ? (nonEmpty(get("fillcolor")) ?? n.color ?? bg ?? "lightgrey") : bg;
|
|
514
|
+
readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Reads a node's fill: a point is always filled; a striped fill fills a box and a wedged one an ellipse only.
|
|
519
|
+
* @param n - the node
|
|
520
|
+
*/
|
|
521
|
+
function readNodeFill(n: NodeRead): void {
|
|
522
|
+
const { shape, point } = n;
|
|
523
|
+
const fillcolor = nonEmpty(n.get("fillcolor"));
|
|
524
|
+
const filled =
|
|
525
|
+
point ||
|
|
526
|
+
n.tokens.some(
|
|
527
|
+
(t) =>
|
|
528
|
+
t === "filled" ||
|
|
529
|
+
t === "radial" ||
|
|
530
|
+
(t === "striped" && BOX_LIKE.has(shape)) ||
|
|
531
|
+
(t === "wedged" && ELLIPSE_LIKE.has(shape)),
|
|
532
|
+
);
|
|
533
|
+
if (filled) {
|
|
534
|
+
const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
|
|
535
|
+
readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
|
|
536
|
+
} else if (fillcolor !== null) {
|
|
537
|
+
noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Reads a node's or cluster's outline: its color, width and dashes.
|
|
543
|
+
* @param n - the node
|
|
544
|
+
*/
|
|
545
|
+
function readNodeOutline(n: NodeRead): void {
|
|
546
|
+
const { get, cx, put, at, tokens } = n;
|
|
547
|
+
if (!cx.cluster && NO_OUTLINE.has(n.shape)) {
|
|
548
|
+
put("borderWidth", 0);
|
|
549
|
+
} else {
|
|
550
|
+
const outline = cx.cluster ? (nonEmpty(get("pencolor")) ?? n.color) : n.color;
|
|
551
|
+
const alpha = readColor(outline, n.scheme, "line", "borderColor", null, put, at);
|
|
552
|
+
const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
|
|
553
|
+
if (alpha === 0) {
|
|
554
|
+
put("borderWidth", 0);
|
|
555
|
+
} else if (width !== null) {
|
|
556
|
+
put("borderWidth", penWidth(width, get("penwidth") !== null, at("borderWidth")));
|
|
557
|
+
} else if (tokens.includes("bold")) {
|
|
558
|
+
put("borderWidth", 2);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (tokens.includes("dashed")) {
|
|
562
|
+
put("borderStyle", "dashed");
|
|
563
|
+
} else if (tokens.includes("dotted")) {
|
|
564
|
+
put("borderStyle", "dotted");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is.
|
|
570
|
+
* @param n - the node
|
|
571
|
+
*/
|
|
572
|
+
function readNodeShape(n: NodeRead): void {
|
|
573
|
+
const rounded = n.tokens.includes("rounded");
|
|
574
|
+
if (n.cx.cluster) {
|
|
575
|
+
n.put("shape", rounded ? "round-rectangle" : "rectangle");
|
|
576
|
+
} else if (n.shapeRaw !== null) {
|
|
577
|
+
n.put(
|
|
578
|
+
"shape",
|
|
579
|
+
rounded && BOX_LIKE.has(n.shape)
|
|
580
|
+
? "round-rectangle"
|
|
581
|
+
: canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape")),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* A pen width as Graphviz draws it: a negative `penwidth` is raised to 0, its minimum, and noted.
|
|
588
|
+
* @param raw - the `penwidth` or `setlinewidth` value
|
|
589
|
+
* @param attribute - whether it is the `penwidth` attribute
|
|
590
|
+
* @param site - where it was read
|
|
591
|
+
* @returns the width, or null when it does not parse
|
|
592
|
+
*/
|
|
593
|
+
function penWidth(raw: string, attribute: boolean, site: CanonSite): number | null {
|
|
594
|
+
const value = Number(raw.trim());
|
|
595
|
+
if (attribute && Number.isFinite(value) && value < 0) {
|
|
596
|
+
noteAt(site, STYLE_ISSUE.BAD_VALUE, raw.slice(0, MAX_STYLE_VALUE_LENGTH));
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
return canonNumber(raw, site);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Reads a node's width and height: exact where Graphviz draws them exactly (`fixedsize`, an empty label that is not
|
|
604
|
+
* a record's, a point), otherwise minimums it grows to fit the label, noted.
|
|
605
|
+
* @param get - the node's attributes
|
|
606
|
+
* @param shape - its shape, lowercase
|
|
607
|
+
* @param point - whether it is a point
|
|
608
|
+
* @param label - the label it shows, or null
|
|
609
|
+
* @param put - receives the values
|
|
610
|
+
* @param at - the sites
|
|
611
|
+
*/
|
|
612
|
+
function readSize(get: Attr, shape: string, point: boolean, label: string | null, put: Put, at: At): void {
|
|
613
|
+
let w = inches(get("width"), at("width"));
|
|
614
|
+
let h = inches(get("height"), at("height"));
|
|
615
|
+
const same = point || CIRCLE_LIKE.has(shape) || isTrue(get("regular"));
|
|
616
|
+
if (shape === "plain") {
|
|
617
|
+
// plain is plaintext with its width and height forced to 0: exactly the size of its label
|
|
618
|
+
w = h = 0;
|
|
619
|
+
} else if (w === null && h === null) {
|
|
620
|
+
if (point) {
|
|
621
|
+
put("width", num6(POINT_SIZE * DOT_POINTS_PER_INCH));
|
|
622
|
+
put("height", num6(POINT_SIZE * DOT_POINTS_PER_INCH));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (!same) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// a regular shape takes the smaller of the default width and height (0.5 inch) for both
|
|
629
|
+
w = h = REGULAR_SIZE;
|
|
630
|
+
}
|
|
631
|
+
let ww = w;
|
|
632
|
+
let hh = h;
|
|
633
|
+
if (point) {
|
|
634
|
+
ww = hh = Math.min(w ?? Infinity, h ?? Infinity);
|
|
635
|
+
} else if (same) {
|
|
636
|
+
ww = hh = Math.max(w ?? 0, h ?? 0);
|
|
637
|
+
}
|
|
638
|
+
const exact = point || isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape));
|
|
639
|
+
if (!exact) {
|
|
640
|
+
noteAt(at("width"), STYLE_ISSUE.SIZE_MINIMUM, "grows to fit its label");
|
|
641
|
+
}
|
|
642
|
+
put("width", ww === null ? null : canonNumber(ww, at("width"), DOT_POINTS_PER_INCH));
|
|
643
|
+
put("height", hh === null ? null : canonNumber(hh, at("height"), DOT_POINTS_PER_INCH));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Reads one edge the way Graphviz draws it.
|
|
648
|
+
* @param get - its attributes
|
|
649
|
+
* @param cx - its names and direction
|
|
650
|
+
* @param put - receives the values
|
|
651
|
+
* @param at - the sites
|
|
652
|
+
*/
|
|
653
|
+
function readEdge(get: Attr, cx: EdgeContext, put: Put, at: At): void {
|
|
654
|
+
const tokens = styleTokens(get("style"), at("visible"));
|
|
655
|
+
const scheme = get("colorscheme");
|
|
656
|
+
readCommon(get, tokens, scheme, cx.names, put, at);
|
|
657
|
+
readColor(nonEmpty(get("color")), scheme, "edge", "color", "opacity", put, at);
|
|
658
|
+
readEdgeLine(get, tokens, put, at);
|
|
659
|
+
readEdgeArrows(get, cx, put, at);
|
|
660
|
+
for (const [attribute, property] of [
|
|
661
|
+
[LABEL_ATTRIBUTE, "label"],
|
|
662
|
+
["headlabel", "targetLabel"],
|
|
663
|
+
["taillabel", "sourceLabel"],
|
|
664
|
+
] as const) {
|
|
665
|
+
const raw = get(attribute);
|
|
666
|
+
if (raw !== null) {
|
|
667
|
+
put(property, labelText(raw, cx.names, cx.html, false, at(property)));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (nonEmpty(get("xlabel")) !== null) {
|
|
671
|
+
noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Reads an edge's line: its width and dashes.
|
|
677
|
+
* @param get - its attributes
|
|
678
|
+
* @param tokens - its style tokens
|
|
679
|
+
* @param put - receives the values
|
|
680
|
+
* @param at - the sites
|
|
681
|
+
*/
|
|
682
|
+
function readEdgeLine(get: Attr, tokens: readonly string[], put: Put, at: At): void {
|
|
683
|
+
const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
|
|
684
|
+
if (width !== null) {
|
|
685
|
+
put("width", penWidth(width, get("penwidth") !== null, at("width")));
|
|
686
|
+
} else if (tokens.includes("bold")) {
|
|
687
|
+
put("width", 2);
|
|
688
|
+
}
|
|
689
|
+
if (tokens.includes("dashed")) {
|
|
690
|
+
put("lineStyle", "dashed");
|
|
691
|
+
} else if (tokens.includes("dotted")) {
|
|
692
|
+
put("lineStyle", "dotted");
|
|
693
|
+
} else if (tokens.includes("solid") || tokens.includes("bold")) {
|
|
694
|
+
put("lineStyle", "solid");
|
|
695
|
+
} else if (tokens.includes("tapered")) {
|
|
696
|
+
put("lineStyle", "other:tapered");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Reads an edge's arrows and their size.
|
|
702
|
+
* @param get - its attributes
|
|
703
|
+
* @param cx - its names and direction
|
|
704
|
+
* @param put - receives the values
|
|
705
|
+
* @param at - the sites
|
|
706
|
+
*/
|
|
707
|
+
function readEdgeArrows(get: Attr, cx: EdgeContext, put: Put, at: At): void {
|
|
708
|
+
const dirRaw = nonEmpty(get("dir"));
|
|
709
|
+
const head = nonEmpty(get("arrowhead"));
|
|
710
|
+
const tail = nonEmpty(get("arrowtail"));
|
|
711
|
+
// with none of these the tool-default rule draws the arrow of the graph's kind
|
|
712
|
+
if (dirRaw !== null || head !== null || tail !== null) {
|
|
713
|
+
const dir = (dirRaw ?? (cx.directed ? "forward" : "none")).trim().toLowerCase();
|
|
714
|
+
const forward = dir === "forward" || dir === "both";
|
|
715
|
+
const back = dir === "back" || dir === "both";
|
|
716
|
+
put("targetArrow", forward ? canonEnum("arrow", head ?? "normal", "graphviz", at("targetArrow")) : "none");
|
|
717
|
+
put("sourceArrow", back ? canonEnum("arrow", tail ?? "normal", "graphviz", at("sourceArrow")) : "none");
|
|
718
|
+
}
|
|
719
|
+
const size = get("arrowsize");
|
|
720
|
+
if (size !== null) {
|
|
721
|
+
put("arrowSize", canonNumber(size, at("arrowSize")));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* The HTML strings of a file: its HTML node names and labels, for Graphviz's shared string pool.
|
|
727
|
+
* @param snapshot - the graph
|
|
728
|
+
* @returns the strings, with their brackets
|
|
729
|
+
*/
|
|
730
|
+
function htmlStrings(snapshot: GraphSnapshot): Set<string> {
|
|
731
|
+
const out = new Set<string>();
|
|
732
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
733
|
+
const id = snapshot.ids.idOf(i);
|
|
734
|
+
if (typeof id === "string" && isHtmlString(id)) {
|
|
735
|
+
out.add(id);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
for (const [table, n] of [
|
|
739
|
+
[snapshot.nodes, snapshot.nodeCount],
|
|
740
|
+
[snapshot.edges, snapshot.edgeCount],
|
|
741
|
+
] as const) {
|
|
742
|
+
for (const name of [LABEL_ATTRIBUTE, "headlabel", "taillabel"].filter((c) => table.has(c))) {
|
|
743
|
+
addHtmlCells(table, name, n, out);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Adds the HTML strings of one label column.
|
|
751
|
+
* @param table - the node or edge table
|
|
752
|
+
* @param name - the column
|
|
753
|
+
* @param n - its rows
|
|
754
|
+
* @param out - receives the strings
|
|
755
|
+
*/
|
|
756
|
+
function addHtmlCells(table: GraphSnapshot["nodes"], name: string, n: number, out: Set<string>): void {
|
|
757
|
+
for (let i = 0; i < n; i++) {
|
|
758
|
+
const v = cellText(table, name, i);
|
|
759
|
+
if (v !== null && isHtmlString(v)) {
|
|
760
|
+
out.add(v);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Which edges are directed: every edge of a directed graph, or as its direction column says.
|
|
767
|
+
* @param snapshot - the graph
|
|
768
|
+
* @returns per edge
|
|
769
|
+
*/
|
|
770
|
+
function directedEdges(snapshot: GraphSnapshot): boolean[] {
|
|
771
|
+
const out = new Array<boolean>(snapshot.edgeCount).fill(snapshot.directed);
|
|
772
|
+
const col = snapshot.directed ? snapshot.edges.byRole("directed") : null;
|
|
773
|
+
if (col !== null) {
|
|
774
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
775
|
+
const v = plainCell(col.value(e));
|
|
776
|
+
if (typeof v === "boolean") {
|
|
777
|
+
out[e] = v;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Whether a node is a cluster's container.
|
|
786
|
+
* @param snapshot - the graph
|
|
787
|
+
* @param i - the node
|
|
788
|
+
* @returns true for a container
|
|
789
|
+
*/
|
|
790
|
+
function isCluster(snapshot: GraphSnapshot, i: number): boolean {
|
|
791
|
+
return snapshot.nodes.has(CLUSTER_COLUMN) && snapshot.nodes.value(CLUSTER_COLUMN, i) === true;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Per element, what its labels' escapes and defaults depend on.
|
|
796
|
+
* @param snapshot - the graph
|
|
797
|
+
* @param written - the direction every edge is written with when the graph mixes both, or null to read it per edge
|
|
798
|
+
* @returns the contexts
|
|
799
|
+
*/
|
|
800
|
+
function contexts(
|
|
801
|
+
snapshot: GraphSnapshot,
|
|
802
|
+
written: boolean | null = null,
|
|
803
|
+
): {
|
|
804
|
+
readonly node: (i: number) => NodeContext;
|
|
805
|
+
readonly edge: (e: number) => EdgeContext;
|
|
806
|
+
} {
|
|
807
|
+
const html = htmlStrings(snapshot);
|
|
808
|
+
const graph = snapshot.meta.name ?? "";
|
|
809
|
+
const own = directedEdges(snapshot);
|
|
810
|
+
// a graph mixing both directions is written with one, every edge then drawn as that one
|
|
811
|
+
const directed = written === null || own.every((d) => d === snapshot.directed) ? own : own.map(() => written);
|
|
812
|
+
const name = (i: number): string => String(snapshot.ids.idOf(i));
|
|
813
|
+
return {
|
|
814
|
+
node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html }),
|
|
815
|
+
edge: (e) => {
|
|
816
|
+
const tail = name(snapshot.edgeSource(e));
|
|
817
|
+
const head = name(snapshot.edgeTarget(e));
|
|
818
|
+
return {
|
|
819
|
+
names: { graph, tail, head, edge: `${tail}${directed[e] ? "->" : "--"}${head}` },
|
|
820
|
+
directed: directed[e],
|
|
821
|
+
html,
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* The label a node shows when it has none of its own: its name, escapes applied (an HTML name shows its text); null
|
|
829
|
+
* for a cluster's container and a point.
|
|
830
|
+
* @param cx - the node
|
|
831
|
+
* @param shape - its shape, lowercase
|
|
832
|
+
* @param notes - where an HTML name is noted
|
|
833
|
+
* @returns the label, or null
|
|
834
|
+
*/
|
|
835
|
+
function defaultLabel(cx: NodeContext, shape: string, notes: StyleNotes): string | null {
|
|
836
|
+
if (cx.cluster || shape === "point") {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
const at: CanonSite = { notes, target: "node", property: "label" };
|
|
840
|
+
return isHtmlString(cx.name)
|
|
841
|
+
? htmlLabelText(cx.name, at)
|
|
842
|
+
: dotLabelText(String.raw`\N`, { node: cx.name, graph: cx.graph }, at);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* The columns of a table that hold styles: Graphviz's style attributes and graphty's per-element style values.
|
|
847
|
+
* @param table - the node or edge table
|
|
848
|
+
* @param attributes - the style attributes of the table
|
|
849
|
+
* @returns the column names
|
|
850
|
+
*/
|
|
851
|
+
function styleColumns(table: GraphSnapshot["nodes"], attributes: readonly string[]): string[] {
|
|
852
|
+
const own = new Set([...attributes, ...NOTED_ATTRIBUTES]);
|
|
853
|
+
return [...table.names()].filter((c) => own.has(c) || c.startsWith("graphty.style."));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* The styles of a DOT file, read the way Graphviz draws it, with Graphviz's defaults beneath: an unfilled black
|
|
858
|
+
* ellipse 54 x 36 labeled with its name, black edges with a target arrow in a directed graph. A file graphty wrote
|
|
859
|
+
* gives back its `graphty.styles` block.
|
|
860
|
+
* @param snapshot - the graph the import built
|
|
861
|
+
* @param context - the work budget
|
|
862
|
+
* @returns the styles
|
|
863
|
+
*/
|
|
864
|
+
export function dotStyles(snapshot: GraphSnapshot, context: StyleReadContext): ImporterStyles {
|
|
865
|
+
const notes = new StyleNotes();
|
|
866
|
+
const cx = contexts(snapshot);
|
|
867
|
+
const node = valueColumns(snapshot.nodeCount);
|
|
868
|
+
const nameColumn = new Array<StyleValue | null>(snapshot.nodeCount).fill(null);
|
|
869
|
+
const clusterColumn = new Array<StyleValue | null>(snapshot.nodeCount).fill(null);
|
|
870
|
+
let named = 0;
|
|
871
|
+
let clusters = 0;
|
|
872
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
873
|
+
context.budget.spend("node", NODE_ATTRIBUTES.length);
|
|
874
|
+
const get: Attr = (name) => cellText(snapshot.nodes, name, i);
|
|
875
|
+
const ncx = cx.node(i);
|
|
876
|
+
const at: At = (property) => ({ notes, target: "node", property, element: i, budget: context.budget });
|
|
877
|
+
readNode(
|
|
878
|
+
get,
|
|
879
|
+
ncx,
|
|
880
|
+
(p, v) => {
|
|
881
|
+
node.put(i, p, v);
|
|
882
|
+
},
|
|
883
|
+
at,
|
|
884
|
+
);
|
|
885
|
+
const shown = defaultLabel(ncx, (get("shape") ?? "").toLowerCase(), notes);
|
|
886
|
+
if (shown !== null) {
|
|
887
|
+
nameColumn[i] = shown;
|
|
888
|
+
named++;
|
|
889
|
+
}
|
|
890
|
+
if (ncx.cluster) {
|
|
891
|
+
clusterColumn[i] = true;
|
|
892
|
+
clusters++;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const edge = valueColumns(snapshot.edgeCount);
|
|
896
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
897
|
+
context.budget.spend("edge", EDGE_ATTRIBUTES.length);
|
|
898
|
+
const get: Attr = (name) => cellText(snapshot.edges, name, e);
|
|
899
|
+
const at: At = (property) => ({ notes, target: "edge", property, element: e, budget: context.budget });
|
|
900
|
+
readEdge(
|
|
901
|
+
get,
|
|
902
|
+
cx.edge(e),
|
|
903
|
+
(p, v) => {
|
|
904
|
+
edge.put(e, p, v);
|
|
905
|
+
},
|
|
906
|
+
at,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const graph: Record<string, StyleValue> = {};
|
|
910
|
+
const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
|
|
911
|
+
if (snapshot.graph.has("bgcolor") && bg !== null) {
|
|
912
|
+
readColor(
|
|
913
|
+
bg,
|
|
914
|
+
cellText(snapshot.graph, "colorscheme", 0),
|
|
915
|
+
"fill",
|
|
916
|
+
"background",
|
|
917
|
+
null,
|
|
918
|
+
(p, v) => {
|
|
919
|
+
if (v !== null) {
|
|
920
|
+
graph[p] = v;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
(property) => ({ notes, target: "graph", property }),
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const blockText = cellText(snapshot.graph, STYLE_BLOCK_NAME, 0);
|
|
927
|
+
if (blockText !== null) {
|
|
928
|
+
exactFromBlock(snapshot, blockText, { node: node.values, edge: edge.values }, cx);
|
|
929
|
+
}
|
|
930
|
+
// Graphviz's defaults that are rules: each node shows its name, and a directed edge has an arrow at its target
|
|
931
|
+
const nodeRules: StyleRule[] = [];
|
|
932
|
+
const nodeColumns = new Map<string, StyleColumn>();
|
|
933
|
+
if (named > 0) {
|
|
934
|
+
nodeColumns.set(NAME_KEY, nameColumn);
|
|
935
|
+
nodeRules.push({
|
|
936
|
+
id: "node.tool.name-label",
|
|
937
|
+
target: "node",
|
|
938
|
+
role: "passthrough",
|
|
939
|
+
origin: "tool",
|
|
940
|
+
where: { has: { style: NAME_KEY } },
|
|
941
|
+
map: { label: { kind: "passthrough", by: { style: NAME_KEY } } },
|
|
942
|
+
facts: { elements: named, reason: "NODE_NAME" },
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
const features: StyleFeature[] = clusters > 0 ? [{ ref: { style: CLUSTER_KEY }, values: clusterColumn }] : [];
|
|
946
|
+
const edgeRules: StyleRule[] = [];
|
|
947
|
+
const edgeColumns = new Map<string, StyleColumn>();
|
|
948
|
+
const directed = directedEdges(snapshot);
|
|
949
|
+
if (directed.some(Boolean)) {
|
|
950
|
+
edgeColumns.set(
|
|
951
|
+
DIRECTED_KEY,
|
|
952
|
+
directed.map((d) => (d ? true : null)),
|
|
953
|
+
);
|
|
954
|
+
edgeRules.push({
|
|
955
|
+
id: "edge.tool.arrow",
|
|
956
|
+
target: "edge",
|
|
957
|
+
role: "group",
|
|
958
|
+
origin: "tool",
|
|
959
|
+
where: { has: { style: DIRECTED_KEY } },
|
|
960
|
+
set: { targetArrow: "triangle" },
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
canonical: true,
|
|
965
|
+
dialect: "graphviz",
|
|
966
|
+
tool: "graphviz",
|
|
967
|
+
units: "px",
|
|
968
|
+
node: { values: node.values, rules: nodeRules, columns: nodeColumns, features },
|
|
969
|
+
edge: { values: edge.values, rules: edgeRules, columns: edgeColumns },
|
|
970
|
+
graph,
|
|
971
|
+
notes: notes.list(),
|
|
972
|
+
readFrom: {
|
|
973
|
+
node: styleColumns(snapshot.nodes, NODE_ATTRIBUTES),
|
|
974
|
+
edge: styleColumns(snapshot.edges, EDGE_ATTRIBUTES),
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// ============================================================ writing
|
|
980
|
+
|
|
981
|
+
/** One element's value of a property. */
|
|
982
|
+
type Get = (property: string) => StyleValue | null;
|
|
983
|
+
|
|
984
|
+
/** Records a value written as the nearest one Graphviz has. */
|
|
985
|
+
type Approximate = (property: string, value: StyleValue) => void;
|
|
986
|
+
|
|
987
|
+
/** A written attribute value: text, or a number written as one. */
|
|
988
|
+
type Written = string | number;
|
|
989
|
+
|
|
990
|
+
/** Neutral shapes by the Graphviz shape that draws them exactly. */
|
|
991
|
+
const SHAPES: ReadonlyMap<string, string> = new Map([
|
|
992
|
+
["ellipse", "ellipse"],
|
|
993
|
+
["rectangle", "box"],
|
|
994
|
+
["round-rectangle", "box"],
|
|
995
|
+
["triangle", "triangle"],
|
|
996
|
+
["diamond", "diamond"],
|
|
997
|
+
["pentagon", "pentagon"],
|
|
998
|
+
["hexagon", "hexagon"],
|
|
999
|
+
["octagon", "octagon"],
|
|
1000
|
+
["parallelogram", "parallelogram"],
|
|
1001
|
+
["trapezoid", "trapezium"],
|
|
1002
|
+
["star", "star"],
|
|
1003
|
+
["none", "none"],
|
|
1004
|
+
]);
|
|
1005
|
+
|
|
1006
|
+
/** Neutral arrows by the Graphviz arrow that draws them. */
|
|
1007
|
+
const ARROWS: ReadonlyMap<string, string> = new Map([
|
|
1008
|
+
["none", "none"],
|
|
1009
|
+
["triangle", "normal"],
|
|
1010
|
+
["triangle-open", "onormal"],
|
|
1011
|
+
["vee", "vee"],
|
|
1012
|
+
["tee", "tee"],
|
|
1013
|
+
["diamond", "diamond"],
|
|
1014
|
+
["diamond-open", "odiamond"],
|
|
1015
|
+
["circle", "dot"],
|
|
1016
|
+
["circle-open", "odot"],
|
|
1017
|
+
["square", "box"],
|
|
1018
|
+
["crow", "crow"],
|
|
1019
|
+
["half", "lnormal"],
|
|
1020
|
+
["inverted", "inv"],
|
|
1021
|
+
]);
|
|
1022
|
+
|
|
1023
|
+
/** Line and border styles Graphviz draws exactly; the others are written as the nearest. */
|
|
1024
|
+
const STROKES: ReadonlyMap<string, string> = new Map([
|
|
1025
|
+
["dashed", "dashed"],
|
|
1026
|
+
["dotted", "dotted"],
|
|
1027
|
+
]);
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* The attribute values Graphviz draws when an attribute is absent, from the tool-defaults table: the writer leaves
|
|
1031
|
+
* them out, so a file states what differs from Graphviz's drawing (and a size stays one Graphviz may grow).
|
|
1032
|
+
* @param target - nodes or edges
|
|
1033
|
+
* @returns the default text of each attribute
|
|
1034
|
+
*/
|
|
1035
|
+
function graphvizDefaults(target: StyleTarget): ReadonlyMap<string, Written> {
|
|
1036
|
+
const t = toolDefaults("graphviz")?.[target] as Readonly<Record<string, { readonly value: unknown } | undefined>>;
|
|
1037
|
+
const out = new Map<string, Written>();
|
|
1038
|
+
const add = (attribute: string, property: string, scale = 1): void => {
|
|
1039
|
+
const v = t[property]?.value;
|
|
1040
|
+
if (typeof v === "number") {
|
|
1041
|
+
out.set(attribute, num6(v / scale));
|
|
1042
|
+
} else if (typeof v === "string") {
|
|
1043
|
+
out.set(attribute, v);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
add("fontcolor", "labelColor");
|
|
1047
|
+
add("fontsize", "labelSize");
|
|
1048
|
+
add("fontname", "labelFont");
|
|
1049
|
+
if (target === "node") {
|
|
1050
|
+
add("color", "borderColor");
|
|
1051
|
+
add("pencolor", "borderColor");
|
|
1052
|
+
add("penwidth", "borderWidth");
|
|
1053
|
+
add("shape", "shape");
|
|
1054
|
+
add("width", "width", DOT_POINTS_PER_INCH);
|
|
1055
|
+
add("height", "height", DOT_POINTS_PER_INCH);
|
|
1056
|
+
} else {
|
|
1057
|
+
add("color", "color");
|
|
1058
|
+
add("penwidth", "width");
|
|
1059
|
+
add("arrowsize", "arrowSize");
|
|
1060
|
+
}
|
|
1061
|
+
return out;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const DEFAULT_ATTRIBUTES: Readonly<Record<StyleTarget, ReadonlyMap<string, Written>>> = {
|
|
1065
|
+
node: graphvizDefaults("node"),
|
|
1066
|
+
edge: graphvizDefaults("edge"),
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Leaves out every attribute whose value is the one Graphviz draws without it.
|
|
1071
|
+
* @param out - the attributes, changed in place
|
|
1072
|
+
* @param target - nodes or edges
|
|
1073
|
+
* @returns the attributes
|
|
1074
|
+
*/
|
|
1075
|
+
function withoutDefaults(out: Map<string, Written>, target: StyleTarget): Map<string, Written> {
|
|
1076
|
+
for (const [attribute, value] of DEFAULT_ATTRIBUTES[target]) {
|
|
1077
|
+
if (out.get(attribute) === value) {
|
|
1078
|
+
out.delete(attribute);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (!out.has("width") && !out.has("height")) {
|
|
1082
|
+
out.delete("fixedsize");
|
|
1083
|
+
}
|
|
1084
|
+
return out;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* A color as Graphviz writes it: `#rrggbb`, or `#rrggbbaa` with an opacity below 1.
|
|
1089
|
+
* @param color - the canonical color
|
|
1090
|
+
* @param opacity - the opacity, or null
|
|
1091
|
+
* @returns the text
|
|
1092
|
+
*/
|
|
1093
|
+
function gvColor(color: string, opacity: StyleValue | null): string {
|
|
1094
|
+
if (typeof opacity !== "number" || opacity >= 1) {
|
|
1095
|
+
return color;
|
|
1096
|
+
}
|
|
1097
|
+
const a = Math.max(0, Math.min(255, Math.round(opacity * 255)));
|
|
1098
|
+
return color + a.toString(16).padStart(2, "0");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* A label's text as a DOT label: backslashes and line breaks escaped, so Graphviz's escape passes give the text back.
|
|
1103
|
+
* @param text - the text
|
|
1104
|
+
* @returns the label
|
|
1105
|
+
*/
|
|
1106
|
+
function escapeLabel(text: string): string {
|
|
1107
|
+
return text.replaceAll("\\", String.raw`\\`).replaceAll("\n", String.raw`\n`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* A shape, line or arrow value in Graphviz's spelling.
|
|
1112
|
+
* @param table - the exact spellings
|
|
1113
|
+
* @param value - the neutral value
|
|
1114
|
+
* @param nearest - the spelling written when there is no exact one
|
|
1115
|
+
* @param property - the property, for the approximation note
|
|
1116
|
+
* @param approximate - records approximations
|
|
1117
|
+
* @returns the spelling
|
|
1118
|
+
*/
|
|
1119
|
+
function spelled(
|
|
1120
|
+
table: ReadonlyMap<string, string>,
|
|
1121
|
+
value: string,
|
|
1122
|
+
nearest: string,
|
|
1123
|
+
property: string,
|
|
1124
|
+
approximate: Approximate,
|
|
1125
|
+
): string {
|
|
1126
|
+
const exact = table.get(value);
|
|
1127
|
+
if (exact !== undefined) {
|
|
1128
|
+
return exact;
|
|
1129
|
+
}
|
|
1130
|
+
if (value.startsWith("other:")) {
|
|
1131
|
+
return value.slice("other:".length);
|
|
1132
|
+
}
|
|
1133
|
+
approximate(property, value);
|
|
1134
|
+
return nearest;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* The font, tooltip and visibility attributes every element shares.
|
|
1139
|
+
* @param get - the element's values
|
|
1140
|
+
* @param out - the attributes, added to
|
|
1141
|
+
* @param tokens - the style tokens, added to
|
|
1142
|
+
*/
|
|
1143
|
+
function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): void {
|
|
1144
|
+
const color = get("labelColor");
|
|
1145
|
+
if (typeof color === "string") {
|
|
1146
|
+
out.set("fontcolor", color);
|
|
1147
|
+
}
|
|
1148
|
+
const size = get("labelSize");
|
|
1149
|
+
if (typeof size === "number") {
|
|
1150
|
+
out.set("fontsize", size);
|
|
1151
|
+
}
|
|
1152
|
+
const font = get("labelFont");
|
|
1153
|
+
if (typeof font === "string") {
|
|
1154
|
+
out.set("fontname", font);
|
|
1155
|
+
}
|
|
1156
|
+
const tip = get("tooltip");
|
|
1157
|
+
if (typeof tip === "string") {
|
|
1158
|
+
out.set("tooltip", escapeLabel(tip));
|
|
1159
|
+
}
|
|
1160
|
+
if (get("visible") === false) {
|
|
1161
|
+
tokens.push("invis");
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* A node's or cluster's Graphviz attributes from its values.
|
|
1167
|
+
* @param get - the node's values
|
|
1168
|
+
* @param cx - the node's name and kind
|
|
1169
|
+
* @param approximate - records approximations
|
|
1170
|
+
* @param notes - where an HTML name is noted
|
|
1171
|
+
* @returns the attributes
|
|
1172
|
+
*/
|
|
1173
|
+
function nodeAttributes(get: Get, cx: NodeContext, approximate: Approximate, notes: StyleNotes): Map<string, Written> {
|
|
1174
|
+
const out = new Map<string, Written>();
|
|
1175
|
+
const tokens: string[] = [];
|
|
1176
|
+
writeCommon(get, out, tokens);
|
|
1177
|
+
writeNodeOutline(get, cx, approximate, out, tokens);
|
|
1178
|
+
const gvShape = writeNodeShape(get, cx, approximate, out, tokens);
|
|
1179
|
+
if (!cx.cluster) {
|
|
1180
|
+
writeNodeSize(get, out);
|
|
1181
|
+
}
|
|
1182
|
+
// the label is written where it differs from what Graphviz shows without one (the name), "" for none
|
|
1183
|
+
const label = get("label");
|
|
1184
|
+
const shown = defaultLabel(cx, gvShape, notes);
|
|
1185
|
+
if (typeof label === "string" && label !== shown) {
|
|
1186
|
+
out.set(LABEL_ATTRIBUTE, escapeLabel(label));
|
|
1187
|
+
} else if (label === null && shown !== null) {
|
|
1188
|
+
out.set(LABEL_ATTRIBUTE, "");
|
|
1189
|
+
}
|
|
1190
|
+
if (tokens.length > 0) {
|
|
1191
|
+
out.set("style", tokens.join(","));
|
|
1192
|
+
}
|
|
1193
|
+
return withoutDefaults(out, "node");
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* A node's or cluster's fill and outline attributes.
|
|
1198
|
+
* @param get - the node's values
|
|
1199
|
+
* @param cx - the node's name and kind
|
|
1200
|
+
* @param approximate - records approximations
|
|
1201
|
+
* @param out - the attributes, added to
|
|
1202
|
+
* @param tokens - the style tokens, added to
|
|
1203
|
+
*/
|
|
1204
|
+
function writeNodeOutline(
|
|
1205
|
+
get: Get,
|
|
1206
|
+
cx: NodeContext,
|
|
1207
|
+
approximate: Approximate,
|
|
1208
|
+
out: Map<string, Written>,
|
|
1209
|
+
tokens: string[],
|
|
1210
|
+
): void {
|
|
1211
|
+
const fill = get("fill");
|
|
1212
|
+
const fillOpacity = get("fillOpacity");
|
|
1213
|
+
if (typeof fill === "string" && fillOpacity !== 0) {
|
|
1214
|
+
tokens.push("filled");
|
|
1215
|
+
out.set("fillcolor", gvColor(fill, fillOpacity));
|
|
1216
|
+
}
|
|
1217
|
+
const border = get("borderColor");
|
|
1218
|
+
if (typeof border === "string") {
|
|
1219
|
+
out.set(cx.cluster ? "pencolor" : "color", border);
|
|
1220
|
+
}
|
|
1221
|
+
const borderWidth = get("borderWidth");
|
|
1222
|
+
if (typeof borderWidth === "number") {
|
|
1223
|
+
out.set("penwidth", borderWidth);
|
|
1224
|
+
}
|
|
1225
|
+
const borderStyle = get("borderStyle");
|
|
1226
|
+
if (typeof borderStyle === "string" && borderStyle !== "solid") {
|
|
1227
|
+
const nearest = borderStyle.includes("dash") ? "dashed" : "solid";
|
|
1228
|
+
tokens.push(spelled(STROKES, borderStyle, nearest, "borderStyle", approximate));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* A node's shape attribute; a cluster is always a rectangle, rounded or not, and any other shape is approximated.
|
|
1234
|
+
* @param get - the node's values
|
|
1235
|
+
* @param cx - the node's name and kind
|
|
1236
|
+
* @param approximate - records approximations
|
|
1237
|
+
* @param out - the attributes, added to
|
|
1238
|
+
* @param tokens - the style tokens, added to
|
|
1239
|
+
* @returns the Graphviz shape written, "" for none
|
|
1240
|
+
*/
|
|
1241
|
+
function writeNodeShape(
|
|
1242
|
+
get: Get,
|
|
1243
|
+
cx: NodeContext,
|
|
1244
|
+
approximate: Approximate,
|
|
1245
|
+
out: Map<string, Written>,
|
|
1246
|
+
tokens: string[],
|
|
1247
|
+
): string {
|
|
1248
|
+
const shape = get("shape");
|
|
1249
|
+
if (typeof shape !== "string") {
|
|
1250
|
+
return "";
|
|
1251
|
+
}
|
|
1252
|
+
if (shape === "round-rectangle") {
|
|
1253
|
+
tokens.push("rounded");
|
|
1254
|
+
}
|
|
1255
|
+
if (cx.cluster) {
|
|
1256
|
+
if (shape !== "rectangle" && shape !== "round-rectangle") {
|
|
1257
|
+
approximate("shape", shape);
|
|
1258
|
+
}
|
|
1259
|
+
return "";
|
|
1260
|
+
}
|
|
1261
|
+
const gvShape = spelled(SHAPES, shape, shape === "vee" ? "invtriangle" : "box", "shape", approximate);
|
|
1262
|
+
out.set("shape", gvShape);
|
|
1263
|
+
return gvShape;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* A node's width and height, fixed so Graphviz does not grow them to fit the label.
|
|
1268
|
+
* @param get - the node's values
|
|
1269
|
+
* @param out - the attributes, added to
|
|
1270
|
+
*/
|
|
1271
|
+
function writeNodeSize(get: Get, out: Map<string, Written>): void {
|
|
1272
|
+
const width = get("width");
|
|
1273
|
+
const height = get("height");
|
|
1274
|
+
if (typeof width === "number" || typeof height === "number") {
|
|
1275
|
+
out.set("fixedsize", "true");
|
|
1276
|
+
}
|
|
1277
|
+
if (typeof width === "number") {
|
|
1278
|
+
out.set("width", inchesOf(width));
|
|
1279
|
+
}
|
|
1280
|
+
if (typeof height === "number") {
|
|
1281
|
+
out.set("height", inchesOf(height));
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* An edge's Graphviz attributes from its values.
|
|
1287
|
+
* @param get - the edge's values
|
|
1288
|
+
* @param directed - whether the edge is directed (its default arrows)
|
|
1289
|
+
* @param approximate - records approximations
|
|
1290
|
+
* @returns the attributes
|
|
1291
|
+
*/
|
|
1292
|
+
function edgeAttributes(get: Get, directed: boolean, approximate: Approximate): Map<string, Written> {
|
|
1293
|
+
const out = new Map<string, Written>();
|
|
1294
|
+
const tokens: string[] = [];
|
|
1295
|
+
writeCommon(get, out, tokens);
|
|
1296
|
+
const color = get("color");
|
|
1297
|
+
if (typeof color === "string") {
|
|
1298
|
+
out.set("color", gvColor(color, get("opacity")));
|
|
1299
|
+
}
|
|
1300
|
+
const width = get("width");
|
|
1301
|
+
if (typeof width === "number") {
|
|
1302
|
+
out.set("penwidth", width);
|
|
1303
|
+
}
|
|
1304
|
+
const line = get("lineStyle");
|
|
1305
|
+
if (typeof line === "string" && line !== "solid") {
|
|
1306
|
+
tokens.push(spelled(STROKES, line, line.includes("dash") ? "dashed" : "solid", "lineStyle", approximate));
|
|
1307
|
+
}
|
|
1308
|
+
writeEdgeArrows(get, directed, approximate, out);
|
|
1309
|
+
const arrowSize = get("arrowSize");
|
|
1310
|
+
if (typeof arrowSize === "number") {
|
|
1311
|
+
out.set("arrowsize", arrowSize);
|
|
1312
|
+
}
|
|
1313
|
+
for (const [property, attribute] of [
|
|
1314
|
+
["label", LABEL_ATTRIBUTE],
|
|
1315
|
+
["targetLabel", "headlabel"],
|
|
1316
|
+
["sourceLabel", "taillabel"],
|
|
1317
|
+
] as const) {
|
|
1318
|
+
const v = get(property);
|
|
1319
|
+
if (typeof v === "string") {
|
|
1320
|
+
out.set(attribute, escapeLabel(v));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (tokens.length > 0) {
|
|
1324
|
+
out.set("style", tokens.join(","));
|
|
1325
|
+
}
|
|
1326
|
+
return withoutDefaults(out, "edge");
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* An edge's arrow attributes, written where they differ from the graph's own: a target arrow on a directed edge.
|
|
1331
|
+
* @param get - the edge's values
|
|
1332
|
+
* @param directed - whether the edge is directed
|
|
1333
|
+
* @param approximate - records approximations
|
|
1334
|
+
* @param out - the attributes, added to
|
|
1335
|
+
*/
|
|
1336
|
+
function writeEdgeArrows(get: Get, directed: boolean, approximate: Approximate, out: Map<string, Written>): void {
|
|
1337
|
+
const source = get("sourceArrow");
|
|
1338
|
+
const target = get("targetArrow");
|
|
1339
|
+
const tail = typeof source === "string" && source !== "none";
|
|
1340
|
+
const head = typeof target === "string" && target !== "none";
|
|
1341
|
+
const plain = directed ? head && !tail && target === "triangle" : !head && !tail;
|
|
1342
|
+
if (plain || (source === null && target === null)) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
let dir = "none";
|
|
1346
|
+
if (head && tail) {
|
|
1347
|
+
dir = "both";
|
|
1348
|
+
} else if (head) {
|
|
1349
|
+
dir = "forward";
|
|
1350
|
+
} else if (tail) {
|
|
1351
|
+
dir = "back";
|
|
1352
|
+
}
|
|
1353
|
+
out.set("dir", dir);
|
|
1354
|
+
if (head) {
|
|
1355
|
+
out.set("arrowhead", spelled(ARROWS, target, "normal", "targetArrow", approximate));
|
|
1356
|
+
}
|
|
1357
|
+
if (tail) {
|
|
1358
|
+
out.set("arrowtail", spelled(ARROWS, source, "normal", "sourceArrow", approximate));
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Each element's values of the properties the writer writes, as a getter per element.
|
|
1364
|
+
* @param picture - each element's value of a property
|
|
1365
|
+
* @param target - nodes or edges
|
|
1366
|
+
* @returns the getter
|
|
1367
|
+
*/
|
|
1368
|
+
function getter(
|
|
1369
|
+
picture: (target: StyleTarget, property: string) => readonly (StyleValue | null)[],
|
|
1370
|
+
target: StyleTarget,
|
|
1371
|
+
): (i: number) => Get {
|
|
1372
|
+
const columns = new Map<string, readonly (StyleValue | null)[]>();
|
|
1373
|
+
for (const p of DOT_STYLE_CAPABILITIES[target]) {
|
|
1374
|
+
columns.set(p, picture(target, p));
|
|
1375
|
+
}
|
|
1376
|
+
return (i) => (property) => columns.get(property)?.[i] ?? null;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* For a file graphty wrote: where a value read from the attributes is what the writer writes for the block's own
|
|
1381
|
+
* value (a hexagon written as a hexagon, a pentagon-like vee as an inverted triangle, an opacity rounded to 255 steps,
|
|
1382
|
+
* a size rounded in inches), the block's exact value is the truth, so a re-import gives back the same rules; a value
|
|
1383
|
+
* another program changed since stays the truth.
|
|
1384
|
+
* @param snapshot - the graph
|
|
1385
|
+
* @param text - the block's text
|
|
1386
|
+
* @param read - the values read, changed in place
|
|
1387
|
+
* @param read.node - the node values
|
|
1388
|
+
* @param read.edge - the edge values
|
|
1389
|
+
* @param cx - the elements' contexts
|
|
1390
|
+
*/
|
|
1391
|
+
function exactFromBlock(
|
|
1392
|
+
snapshot: GraphSnapshot,
|
|
1393
|
+
text: string,
|
|
1394
|
+
read: { readonly node: ValueColumns; readonly edge: ValueColumns },
|
|
1395
|
+
cx: ReturnType<typeof contexts>,
|
|
1396
|
+
): void {
|
|
1397
|
+
const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
|
|
1398
|
+
const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "dot") : null;
|
|
1399
|
+
if (ex === null) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const notes = new StyleNotes();
|
|
1403
|
+
for (const target of ["node", "edge"] as const) {
|
|
1404
|
+
const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
|
|
1405
|
+
const get = getter((t, p) => resolveStyles(ex, t, p as never, snapshot), target);
|
|
1406
|
+
const values = read[target];
|
|
1407
|
+
for (let i = 0; i < n; i++) {
|
|
1408
|
+
const want = get(i);
|
|
1409
|
+
const back = writtenAndReadBack(target, i, want, cx, notes);
|
|
1410
|
+
for (const property of DOT_STYLE_CAPABILITIES[target]) {
|
|
1411
|
+
const native = values[property]?.[i] ?? null;
|
|
1412
|
+
const w = want(property);
|
|
1413
|
+
if (native !== w && native === (back.get(property) ?? null)) {
|
|
1414
|
+
values[property] ??= new Array<StyleValue | null>(n).fill(null);
|
|
1415
|
+
values[property][i] = w;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* An element's values written as the writer writes them and read back as the reader reads them.
|
|
1424
|
+
* @param target - nodes or edges
|
|
1425
|
+
* @param i - the element
|
|
1426
|
+
* @param want - its values
|
|
1427
|
+
* @param cx - the elements' contexts
|
|
1428
|
+
* @param notes - a scratch for the notes the round trip makes
|
|
1429
|
+
* @returns the values read back, by property
|
|
1430
|
+
*/
|
|
1431
|
+
function writtenAndReadBack(
|
|
1432
|
+
target: StyleTarget,
|
|
1433
|
+
i: number,
|
|
1434
|
+
want: Get,
|
|
1435
|
+
cx: ReturnType<typeof contexts>,
|
|
1436
|
+
notes: StyleNotes,
|
|
1437
|
+
): Map<string, StyleValue> {
|
|
1438
|
+
const ignore: Approximate = () => undefined;
|
|
1439
|
+
const back = new Map<string, StyleValue>();
|
|
1440
|
+
const put: Put = (property, value) => {
|
|
1441
|
+
if (value !== null) {
|
|
1442
|
+
back.set(property, value);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
const at: At = (property) => ({ notes, target, property });
|
|
1446
|
+
if (target === "node") {
|
|
1447
|
+
const ncx = cx.node(i);
|
|
1448
|
+
const attrs = nodeAttributes(want, ncx, ignore, notes);
|
|
1449
|
+
readNode((name) => textOf(attrs.get(name)), ncx, put, at);
|
|
1450
|
+
} else {
|
|
1451
|
+
const ecx = cx.edge(i);
|
|
1452
|
+
const attrs = edgeAttributes(want, ecx.directed, ignore);
|
|
1453
|
+
readEdge((name) => textOf(attrs.get(name)), ecx, put, at);
|
|
1454
|
+
}
|
|
1455
|
+
return back;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* A written value as the text the importer would read back.
|
|
1460
|
+
* @param v - the value
|
|
1461
|
+
* @returns its text, or null
|
|
1462
|
+
*/
|
|
1463
|
+
function textOf(v: Written | undefined): string | null {
|
|
1464
|
+
return v === undefined ? null : String(v);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Whether a style column written as a DOT attribute reads back as the same values: DOT values are untyped and the
|
|
1469
|
+
* importer infers a column's type from its text, so a text column whose every value looks like a number does not.
|
|
1470
|
+
* @param col - the values
|
|
1471
|
+
* @returns true when it reads back unchanged
|
|
1472
|
+
*/
|
|
1473
|
+
function readsBack(col: StyleColumn): boolean {
|
|
1474
|
+
const vals = col.filter((v): v is StyleValue => v !== null);
|
|
1475
|
+
if (vals.every((v) => typeof v === "number")) {
|
|
1476
|
+
return vals.every((v) => Number.isFinite(v));
|
|
1477
|
+
}
|
|
1478
|
+
if (vals.every((v) => typeof v === "boolean")) {
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
if (vals.every((v) => typeof v === "string")) {
|
|
1482
|
+
return vals.some((v) => inferTextDtype(v) === "string") && vals.every((v) => isWritableDotText(v));
|
|
1483
|
+
}
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* A `graphty.styles` block's JSON with every quote and backslash inside its strings written as a `\u` escape, so the
|
|
1489
|
+
* text holds no backslash before a quote, which a DOT quoted string cannot carry; it parses to the same value.
|
|
1490
|
+
* @param json - the block
|
|
1491
|
+
* @returns the block
|
|
1492
|
+
*/
|
|
1493
|
+
function dotSafeJson(json: string): string {
|
|
1494
|
+
return json.replaceAll(/\\(["\\])/g, (_m, c: string) => (c === '"' ? String.raw`\u0022` : String.raw`\u005c`));
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** A DOT export with styles worked out. */
|
|
1498
|
+
interface DotStyledExport {
|
|
1499
|
+
/** The graph to write: Graphviz attributes written from the styles, the block and the per-element values added. */
|
|
1500
|
+
readonly snapshot: GraphSnapshot;
|
|
1501
|
+
/** W_STYLE_APPROXIMATED for every value written as the nearest one Graphviz has. */
|
|
1502
|
+
readonly notes: readonly LossNote[];
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Works out a DOT export with styles: every node's, cluster's and edge's Graphviz attributes from what graphty draws,
|
|
1507
|
+
* in place of every style attribute it had; the graph's `bgcolor`; the `graphty.styles` block as the graph attribute
|
|
1508
|
+
* `"graphty.styles"` and the per-element values its rules read as `"graphty.style.<target>.<key>"` attributes. A
|
|
1509
|
+
* style column whose values would not read back as written (text that looks like numbers) goes inside the block
|
|
1510
|
+
* instead, with all the others.
|
|
1511
|
+
* @param snapshot - the graph handed to the exporter
|
|
1512
|
+
* @param input - the styles, or undefined when none are written
|
|
1513
|
+
* @param onMixedDirection - the exporter's option: the direction a graph mixing both is written with
|
|
1514
|
+
* @returns the styled export, or null without styles
|
|
1515
|
+
*/
|
|
1516
|
+
export function dotStyledExport(
|
|
1517
|
+
snapshot: GraphSnapshot,
|
|
1518
|
+
input: ExporterStyleInput | undefined,
|
|
1519
|
+
onMixedDirection?: unknown,
|
|
1520
|
+
): DotStyledExport | null {
|
|
1521
|
+
if (input === undefined) {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
const approximated = new StyleApproximations();
|
|
1525
|
+
const replaced: LossNote[] = [];
|
|
1526
|
+
const out = snapshot.withColumns();
|
|
1527
|
+
const written = onMixedDirection === "directed" || onMixedDirection === "undirected";
|
|
1528
|
+
const cx = contexts(snapshot, written ? onMixedDirection === "directed" : null);
|
|
1529
|
+
const notes = new StyleNotes();
|
|
1530
|
+
for (const target of ["node", "edge"] as const) {
|
|
1531
|
+
const table = target === "node" ? out.nodes : out.edges;
|
|
1532
|
+
const read = dataRead(input.rules.styles.rules, target);
|
|
1533
|
+
const approximate: Approximate = (property, value) => {
|
|
1534
|
+
approximated.add(target, property, value);
|
|
1535
|
+
};
|
|
1536
|
+
const columns = attributeColumns(snapshot, input, target, { cx, approximate, notes, read });
|
|
1537
|
+
replaced.push(...removeStyleAttributes(table, target, columns, read));
|
|
1538
|
+
setAttributeColumns(table, columns);
|
|
1539
|
+
}
|
|
1540
|
+
const { background } = input.rules.styles.graph;
|
|
1541
|
+
if (out.graph.has("bgcolor")) {
|
|
1542
|
+
out.graph.remove("bgcolor");
|
|
1543
|
+
}
|
|
1544
|
+
if (typeof background === "string") {
|
|
1545
|
+
out.graph.set("bgcolor", [background], { dtype: "string" });
|
|
1546
|
+
}
|
|
1547
|
+
const inline = [...input.tags.node.values(), ...input.tags.edge.values()].some((col) => !readsBack(col));
|
|
1548
|
+
if (!inline) {
|
|
1549
|
+
setTagColumns(out, input);
|
|
1550
|
+
}
|
|
1551
|
+
const block = inline ? writeStyleBlock(input.rules, { inlineColumns: true, snapshot }) : input.block;
|
|
1552
|
+
out.graph.set(STYLE_BLOCK_NAME, [dotSafeJson(block)], { dtype: "string" });
|
|
1553
|
+
const losses = approximated.notes("Graphviz");
|
|
1554
|
+
if (inline) {
|
|
1555
|
+
losses.push(...styleBlockLosses(block, input.rules.styles.rules.length, "size"));
|
|
1556
|
+
}
|
|
1557
|
+
return { snapshot: out, notes: [...replaced, ...losses] };
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/** What attributeColumns() reads besides the graph and the styles. */
|
|
1561
|
+
interface AttributeWrite {
|
|
1562
|
+
/** The elements' contexts. */
|
|
1563
|
+
readonly cx: ReturnType<typeof contexts>;
|
|
1564
|
+
/** Records approximations. */
|
|
1565
|
+
readonly approximate: Approximate;
|
|
1566
|
+
/** Where an HTML name is noted. */
|
|
1567
|
+
readonly notes: StyleNotes;
|
|
1568
|
+
/** The data columns the target's rules read. */
|
|
1569
|
+
readonly read: ReadonlySet<string>;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Every element's Graphviz attributes of a target, as columns.
|
|
1574
|
+
* @param snapshot - the graph handed to the exporter
|
|
1575
|
+
* @param input - the styles
|
|
1576
|
+
* @param target - nodes or edges
|
|
1577
|
+
* @param w - the contexts, the recorders and the columns the rules read
|
|
1578
|
+
* @returns the attribute columns, by name
|
|
1579
|
+
*/
|
|
1580
|
+
function attributeColumns(
|
|
1581
|
+
snapshot: GraphSnapshot,
|
|
1582
|
+
input: ExporterStyleInput,
|
|
1583
|
+
target: StyleTarget,
|
|
1584
|
+
w: AttributeWrite,
|
|
1585
|
+
): Map<string, (Written | null)[]> {
|
|
1586
|
+
const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
|
|
1587
|
+
const get = getter((t, p) => input.picture(t, p as never), target);
|
|
1588
|
+
const columns = new Map<string, (Written | null)[]>();
|
|
1589
|
+
for (let i = 0; i < n; i++) {
|
|
1590
|
+
const attrs =
|
|
1591
|
+
target === "node"
|
|
1592
|
+
? nodeAttributes(get(i), w.cx.node(i), w.approximate, w.notes)
|
|
1593
|
+
: edgeAttributes(get(i), w.cx.edge(i).directed, w.approximate);
|
|
1594
|
+
// no label is written label="" (Graphviz would show the name), which a rule reading the label column
|
|
1595
|
+
// reads back as the empty label
|
|
1596
|
+
if (w.read.has(LABEL_ATTRIBUTE) && attrs.get(LABEL_ATTRIBUTE) === "" && get(i)("label") === null) {
|
|
1597
|
+
w.approximate("label", "");
|
|
1598
|
+
}
|
|
1599
|
+
for (const [name, value] of attrs) {
|
|
1600
|
+
let col = columns.get(name);
|
|
1601
|
+
if (col === undefined) {
|
|
1602
|
+
col = new Array<Written | null>(n).fill(null);
|
|
1603
|
+
columns.set(name, col);
|
|
1604
|
+
}
|
|
1605
|
+
col[i] = value;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return columns;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Removes every column Graphviz reads as how to draw: the picture writes them, so a data column of the same name (a
|
|
1613
|
+
* `label` attribute graphty does not draw) would draw something graphty does not. One a rule reads, whose values the
|
|
1614
|
+
* picture does not write, is lost to the rule, and said so.
|
|
1615
|
+
* @param table - the node or edge table, changed in place
|
|
1616
|
+
* @param target - nodes or edges
|
|
1617
|
+
* @param columns - the attributes the picture writes
|
|
1618
|
+
* @param read - the data columns the rules read
|
|
1619
|
+
* @returns the notes of the columns lost to a rule
|
|
1620
|
+
*/
|
|
1621
|
+
function removeStyleAttributes(
|
|
1622
|
+
table: GraphSnapshot["nodes"],
|
|
1623
|
+
target: StyleTarget,
|
|
1624
|
+
columns: ReadonlyMap<string, (Written | null)[]>,
|
|
1625
|
+
read: ReadonlySet<string>,
|
|
1626
|
+
): LossNote[] {
|
|
1627
|
+
const attributes = target === "node" ? NODE_ATTRIBUTES : EDGE_ATTRIBUTES;
|
|
1628
|
+
const replaced: LossNote[] = [];
|
|
1629
|
+
for (const name of table.names()) {
|
|
1630
|
+
if (!attributes.includes(name) && name !== LABEL_ATTRIBUTE && !name.startsWith("graphty.style.")) {
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
const differs = read.has(name) ? replacedRows(table, name, columns.get(name)) : 0;
|
|
1634
|
+
if (differs > 0) {
|
|
1635
|
+
replaced.push(
|
|
1636
|
+
Object.freeze({
|
|
1637
|
+
code: DOT_ATTRIBUTE_CLASH_CODE,
|
|
1638
|
+
message: `${target} column "${name}" is not written: the ${name} attribute holds what graphty draws (${String(differs)} ${target}${differs === 1 ? "" : "s"} differ)`,
|
|
1639
|
+
column: name,
|
|
1640
|
+
count: differs,
|
|
1641
|
+
}),
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
table.remove(name);
|
|
1645
|
+
}
|
|
1646
|
+
return replaced;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Sets the attribute columns the picture writes: the label with its role, numbers as f64, the rest as text.
|
|
1651
|
+
* @param table - the node or edge table, changed in place
|
|
1652
|
+
* @param columns - the attributes
|
|
1653
|
+
*/
|
|
1654
|
+
function setAttributeColumns(table: GraphSnapshot["nodes"], columns: ReadonlyMap<string, (Written | null)[]>): void {
|
|
1655
|
+
for (const [name, col] of columns) {
|
|
1656
|
+
if (name === LABEL_ATTRIBUTE) {
|
|
1657
|
+
table.set(name, col, { dtype: "string", role: "label" }, { replaceRole: true });
|
|
1658
|
+
} else if (col.every((v) => v === null || typeof v === "number")) {
|
|
1659
|
+
table.set(name, col, { dtype: "f64" });
|
|
1660
|
+
} else {
|
|
1661
|
+
table.set(
|
|
1662
|
+
name,
|
|
1663
|
+
col.map((v) => (v === null ? null : String(v))),
|
|
1664
|
+
{ dtype: "string" },
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Sets the per-element values the rules read as `"graphty.style.<target>.<key>"` attributes.
|
|
1672
|
+
* @param out - the graph, changed in place
|
|
1673
|
+
* @param input - the styles
|
|
1674
|
+
*/
|
|
1675
|
+
function setTagColumns(out: GraphSnapshot, input: ExporterStyleInput): void {
|
|
1676
|
+
for (const target of ["node", "edge"] as const) {
|
|
1677
|
+
const table = target === "node" ? out.nodes : out.edges;
|
|
1678
|
+
for (const [name, col] of input.tags[target]) {
|
|
1679
|
+
const kinds = new Set(col.filter((v) => v !== null).map((v) => typeof v));
|
|
1680
|
+
let dtype: "string" | "bool" | "f64" = "string";
|
|
1681
|
+
if (kinds.has("number")) {
|
|
1682
|
+
dtype = "f64";
|
|
1683
|
+
} else if (kinds.has("boolean")) {
|
|
1684
|
+
dtype = "bool";
|
|
1685
|
+
}
|
|
1686
|
+
table.set(name, col, { dtype });
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* The data columns the rules of a target read.
|
|
1693
|
+
* @param rules - the rules
|
|
1694
|
+
* @param target - nodes or edges
|
|
1695
|
+
* @returns the column names
|
|
1696
|
+
*/
|
|
1697
|
+
function dataRead(rules: readonly StyleRule[], target: StyleTarget): Set<string> {
|
|
1698
|
+
const out = new Set<string>();
|
|
1699
|
+
for (const { ext: _ext, ...rule } of rules.filter((r) => r.target === target)) {
|
|
1700
|
+
JSON.stringify(rule, (_k, v: unknown) => {
|
|
1701
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length === 1) {
|
|
1702
|
+
const { data } = v as { data?: unknown };
|
|
1703
|
+
if (typeof data === "string") {
|
|
1704
|
+
out.add(data);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return v;
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
return out;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* A data column named like an attribute the export writes is not written: the exporter's `DOT_LOSS.ATTRIBUTE_CLASH`.
|
|
1715
|
+
*/
|
|
1716
|
+
export const DOT_ATTRIBUTE_CLASH_CODE = "W_DOT_ATTRIBUTE_CLASH";
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* How many rows of a data column the attribute written in its place does not hold as it is.
|
|
1720
|
+
* @param table - the table
|
|
1721
|
+
* @param name - the column
|
|
1722
|
+
* @param written - the attribute's values, or undefined when none is written
|
|
1723
|
+
* @returns the rows that differ
|
|
1724
|
+
*/
|
|
1725
|
+
function replacedRows(
|
|
1726
|
+
table: GraphSnapshot["nodes"],
|
|
1727
|
+
name: string,
|
|
1728
|
+
written: readonly (Written | null)[] | undefined,
|
|
1729
|
+
): number {
|
|
1730
|
+
const column = table.get(name);
|
|
1731
|
+
let n = 0;
|
|
1732
|
+
for (let i = 0; column !== null && i < column.length; i++) {
|
|
1733
|
+
const cell = column.isSet(i) ? plainCell(column.value(i)) : null;
|
|
1734
|
+
const w = written?.[i] ?? null;
|
|
1735
|
+
const text = typeof cell === "string" ? cell : JSON.stringify(cell);
|
|
1736
|
+
if (cell !== null && (w === null || String(w) !== text)) {
|
|
1737
|
+
n++;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return n;
|
|
1741
|
+
}
|