@graphty/graph-io 0.3.48 → 0.3.49
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-C2riR4NT.js → approximations-Bzn3L-je.js} +2 -2
- package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-Bzn3L-je.js.map} +1 -1
- package/dist/chunks/bends-BGXjv_U4.js +129 -0
- package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
- package/dist/chunks/{block-Cu22ewuS.js → block-iptrlMBb.js} +906 -164
- package/dist/chunks/block-iptrlMBb.js.map +1 -0
- package/dist/chunks/choose-CczZJaX_.js +717 -0
- package/dist/chunks/choose-CczZJaX_.js.map +1 -0
- package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
- package/dist/chunks/codes-BE2xir2k.js.map +1 -0
- package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
- package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
- package/dist/chunks/{context-cgmmOxZ2.js → context-C3p0RQxR.js} +5 -4
- package/dist/chunks/{context-cgmmOxZ2.js.map → context-C3p0RQxR.js.map} +1 -1
- package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
- package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
- package/dist/chunks/{credentials-BiQsnMTb.js → credentials-DXHx7nwR.js} +16 -14
- package/dist/chunks/credentials-DXHx7nwR.js.map +1 -0
- package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
- package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
- package/dist/chunks/descriptor-B28U25x4.js +60 -0
- package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
- package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-DLA5kINx.js} +7 -2
- package/dist/chunks/descriptor-DLA5kINx.js.map +1 -0
- package/dist/chunks/{emit-CD8xnY8s.js → emit-BwHQkZle.js} +1660 -133
- package/dist/chunks/emit-BwHQkZle.js.map +1 -0
- package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
- package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
- package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
- package/dist/chunks/export-Cha4dRFz.js.map +1 -0
- package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
- package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
- package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-D2CETOMK.js} +1222 -156
- package/dist/chunks/exporter-D2CETOMK.js.map +1 -0
- package/dist/chunks/{exporter-BIdaoIgh.js → exporter-D9yMtocA.js} +7 -7
- package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-D9yMtocA.js.map} +1 -1
- package/dist/chunks/{exporter-DtVU80Fa.js → exporter-_jrBTnos.js} +9 -8
- package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-_jrBTnos.js.map} +1 -1
- package/dist/chunks/{exporter-ChjL5fmP.js → exporter-nDfxIK5D.js} +9 -8
- package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-nDfxIK5D.js.map} +1 -1
- package/dist/chunks/format-BXiAb-1h.js +408 -0
- package/dist/chunks/format-BXiAb-1h.js.map +1 -0
- package/dist/chunks/{generic-CP6YD34u.js → generic-ymWCETAJ.js} +387 -31
- package/dist/chunks/generic-ymWCETAJ.js.map +1 -0
- package/dist/chunks/{importer-e3huMZbm.js → importer-B92x1xY5.js} +14 -10
- package/dist/chunks/importer-B92x1xY5.js.map +1 -0
- package/dist/chunks/{importer-B87_fmVx.js → importer-CT9mJ1nn.js} +326 -84
- package/dist/chunks/importer-CT9mJ1nn.js.map +1 -0
- package/dist/chunks/{importer-CB_zV5_L.js → importer-DaLTjbds.js} +116 -1102
- package/dist/chunks/importer-DaLTjbds.js.map +1 -0
- package/dist/chunks/{importer-DPZDN40-.js → importer-GREKMdIf.js} +25 -17
- package/dist/chunks/importer-GREKMdIf.js.map +1 -0
- package/dist/chunks/{importer-Bji2MeWr.js → importer-PqaaKKnY.js} +13 -10
- package/dist/chunks/{importer-Bji2MeWr.js.map → importer-PqaaKKnY.js.map} +1 -1
- package/dist/chunks/{importer-Cyy6UseH.js → importer-Vn5QvmmJ.js} +66 -13
- package/dist/chunks/importer-Vn5QvmmJ.js.map +1 -0
- package/dist/chunks/{importer-CVT-JyDE.js → importer-vRq4h3j5.js} +1469 -356
- package/dist/chunks/importer-vRq4h3j5.js.map +1 -0
- package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
- package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
- package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
- package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
- package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-Clf4QIyl.js} +282 -74
- package/dist/chunks/lexicon-Clf4QIyl.js.map +1 -0
- package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
- package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
- package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
- package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
- package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
- package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
- package/dist/chunks/round-trip-Zm3_ivgq.js +79 -0
- package/dist/chunks/round-trip-Zm3_ivgq.js.map +1 -0
- package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-EokFic1V.js} +5 -5
- package/dist/chunks/sigv4-EokFic1V.js.map +1 -0
- package/dist/chunks/{styles-CaABMoO1.js → styles-CCs214_3.js} +1237 -138
- package/dist/chunks/styles-CCs214_3.js.map +1 -0
- package/dist/chunks/styles-Cm62yyQX.js +2812 -0
- package/dist/chunks/styles-Cm62yyQX.js.map +1 -0
- package/dist/chunks/styles-DPQQIig4.js +1928 -0
- package/dist/chunks/styles-DPQQIig4.js.map +1 -0
- package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-UMFdNqze.js} +503 -172
- package/dist/chunks/styles-yed-UMFdNqze.js.map +1 -0
- package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
- package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
- package/dist/csv.js +7 -7
- package/dist/cx.js +2 -2
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +5 -4
- package/dist/dot.js.map +1 -1
- package/dist/gexf.js +42 -12
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +847 -193
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +232 -220
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +3 -3
- package/dist/json.js +3 -2
- package/dist/neo4j.js +6 -6
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/positions.js +45 -3
- package/dist/positions.js.map +1 -1
- package/dist/sources-cypher.js +138 -23
- package/dist/sources-cypher.js.map +1 -1
- package/dist/sources-ndex.js +2 -2
- package/dist/sources-rest.js +16 -6
- package/dist/sources-rest.js.map +1 -1
- package/dist/sources-sparql.d.ts +1 -0
- package/dist/sources-sparql.js +405 -0
- package/dist/sources-sparql.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +5 -1
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx/styles.d.ts +24 -1
- package/dist/src/formats/cx/styles.d.ts.map +1 -1
- package/dist/src/formats/cx/styles.js +117 -11
- package/dist/src/formats/cx/styles.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +1 -0
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cx2/styles.d.ts.map +1 -1
- package/dist/src/formats/cx2/styles.js +3 -1
- package/dist/src/formats/cx2/styles.js.map +1 -1
- package/dist/src/formats/cys/importer.d.ts.map +1 -1
- package/dist/src/formats/cys/importer.js +1 -0
- package/dist/src/formats/cys/importer.js.map +1 -1
- package/dist/src/formats/cys/styles.d.ts +2 -0
- package/dist/src/formats/cys/styles.d.ts.map +1 -1
- package/dist/src/formats/cys/styles.js +15 -7
- package/dist/src/formats/cys/styles.js.map +1 -1
- package/dist/src/formats/dot/drawing.d.ts +98 -0
- package/dist/src/formats/dot/drawing.d.ts.map +1 -0
- package/dist/src/formats/dot/drawing.js +734 -0
- package/dist/src/formats/dot/drawing.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +108 -31
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/label-places.d.ts +34 -0
- package/dist/src/formats/dot/label-places.d.ts.map +1 -0
- package/dist/src/formats/dot/label-places.js +179 -0
- package/dist/src/formats/dot/label-places.js.map +1 -0
- package/dist/src/formats/dot/record.d.ts +50 -0
- package/dist/src/formats/dot/record.d.ts.map +1 -0
- package/dist/src/formats/dot/record.js +275 -0
- package/dist/src/formats/dot/record.js.map +1 -0
- package/dist/src/formats/dot/styles.d.ts +62 -2
- package/dist/src/formats/dot/styles.d.ts.map +1 -1
- package/dist/src/formats/dot/styles.js +1454 -119
- package/dist/src/formats/dot/styles.js.map +1 -1
- package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
- package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
- package/dist/src/formats/gexf/dialog-metrics.js +59 -0
- package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +7 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +41 -5
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/preview.d.ts +77 -0
- package/dist/src/formats/gexf/preview.d.ts.map +1 -0
- package/dist/src/formats/gexf/preview.js +191 -0
- package/dist/src/formats/gexf/preview.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +2 -1
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gexf/styles.d.ts +58 -31
- package/dist/src/formats/gexf/styles.d.ts.map +1 -1
- package/dist/src/formats/gexf/styles.js +848 -225
- package/dist/src/formats/gexf/styles.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +5 -2
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +258 -71
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +1 -1
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +1 -1
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/gml/positions.d.ts +7 -0
- package/dist/src/formats/gml/positions.d.ts.map +1 -1
- package/dist/src/formats/gml/positions.js +29 -7
- package/dist/src/formats/gml/positions.js.map +1 -1
- package/dist/src/formats/gml/styles.d.ts.map +1 -1
- package/dist/src/formats/gml/styles.js +753 -101
- package/dist/src/formats/gml/styles.js.map +1 -1
- package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-plain.js +6 -10
- package/dist/src/formats/graphml/styles-plain.js.map +1 -1
- package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
- package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-yed.js +692 -161
- package/dist/src/formats/graphml/styles-yed.js.map +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.js +429 -64
- package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +3 -3
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +16 -6
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts +3 -2
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +39 -5
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +13 -2
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +42 -6
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +1 -1
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +1 -1
- package/dist/src/formats/json/index.js.map +1 -1
- package/dist/src/formats/json/positions.d.ts +5 -2
- package/dist/src/formats/json/positions.d.ts.map +1 -1
- package/dist/src/formats/json/positions.js +20 -4
- package/dist/src/formats/json/positions.js.map +1 -1
- package/dist/src/formats/json/styles.d.ts +12 -1
- package/dist/src/formats/json/styles.d.ts.map +1 -1
- package/dist/src/formats/json/styles.js +1755 -136
- package/dist/src/formats/json/styles.js.map +1 -1
- package/dist/src/formats/pajek/draw.d.ts +96 -0
- package/dist/src/formats/pajek/draw.d.ts.map +1 -0
- package/dist/src/formats/pajek/draw.js +372 -0
- package/dist/src/formats/pajek/draw.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +70 -51
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +9 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +233 -102
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/index.d.ts +1 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -1
- package/dist/src/formats/pajek/index.js.map +1 -1
- package/dist/src/formats/pajek/styles.d.ts +15 -7
- package/dist/src/formats/pajek/styles.d.ts.map +1 -1
- package/dist/src/formats/pajek/styles.js +1278 -114
- package/dist/src/formats/pajek/styles.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +10 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +26 -2
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +1 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +65 -5
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/styles.d.ts +23 -3
- package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
- package/dist/src/formats/xgmml/styles.js +277 -60
- package/dist/src/formats/xgmml/styles.js.map +1 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/positions/convert.d.ts.map +1 -1
- package/dist/src/positions/convert.js +11 -1
- package/dist/src/positions/convert.js.map +1 -1
- package/dist/src/positions/extract.d.ts.map +1 -1
- package/dist/src/positions/extract.js +53 -1
- package/dist/src/positions/extract.js.map +1 -1
- package/dist/src/positions/index.d.ts +1 -1
- package/dist/src/positions/index.d.ts.map +1 -1
- package/dist/src/positions/types.d.ts +59 -0
- package/dist/src/positions/types.d.ts.map +1 -1
- package/dist/src/sources/builtins.d.ts +1 -1
- package/dist/src/sources/builtins.d.ts.map +1 -1
- package/dist/src/sources/builtins.js +8 -2
- package/dist/src/sources/builtins.js.map +1 -1
- package/dist/src/sources/context.d.ts.map +1 -1
- package/dist/src/sources/context.js +1 -0
- package/dist/src/sources/context.js.map +1 -1
- package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
- package/dist/src/sources/cypher/descriptor.js +5 -0
- package/dist/src/sources/cypher/descriptor.js.map +1 -1
- package/dist/src/sources/cypher/index.d.ts.map +1 -1
- package/dist/src/sources/cypher/index.js +124 -18
- package/dist/src/sources/cypher/index.js.map +1 -1
- package/dist/src/sources/cypher/params.d.ts +1 -1
- package/dist/src/sources/cypher/params.d.ts.map +1 -1
- package/dist/src/sources/cypher/query-builder.d.ts +17 -0
- package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
- package/dist/src/sources/cypher/query-builder.js +31 -0
- package/dist/src/sources/cypher/query-builder.js.map +1 -1
- package/dist/src/sources/ids.d.ts +5 -0
- package/dist/src/sources/ids.d.ts.map +1 -1
- package/dist/src/sources/ids.js +5 -0
- package/dist/src/sources/ids.js.map +1 -1
- package/dist/src/sources/params.d.ts +3 -0
- package/dist/src/sources/params.d.ts.map +1 -1
- package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
- package/dist/src/sources/rest/descriptor.js +5 -0
- package/dist/src/sources/rest/descriptor.js.map +1 -1
- package/dist/src/sources/rest/source.d.ts.map +1 -1
- package/dist/src/sources/rest/source.js +15 -0
- package/dist/src/sources/rest/source.js.map +1 -1
- package/dist/src/sources/sigv4.d.ts +5 -3
- package/dist/src/sources/sigv4.d.ts.map +1 -1
- package/dist/src/sources/sigv4.js +6 -4
- package/dist/src/sources/sigv4.js.map +1 -1
- package/dist/src/sources/sparql/descriptor.d.ts +10 -0
- package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
- package/dist/src/sources/sparql/descriptor.js +64 -0
- package/dist/src/sources/sparql/descriptor.js.map +1 -0
- package/dist/src/sources/sparql/index.d.ts +28 -0
- package/dist/src/sources/sparql/index.d.ts.map +1 -0
- package/dist/src/sources/sparql/index.js +314 -0
- package/dist/src/sources/sparql/index.js.map +1 -0
- package/dist/src/sources/sparql/ntriples.d.ts +69 -0
- package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
- package/dist/src/sources/sparql/ntriples.js +180 -0
- package/dist/src/sources/sparql/ntriples.js.map +1 -0
- package/dist/src/sources/sparql/params.d.ts +27 -0
- package/dist/src/sources/sparql/params.d.ts.map +1 -0
- package/dist/src/sources/sparql/params.js +5 -0
- package/dist/src/sources/sparql/params.js.map +1 -0
- package/dist/src/sources/sparql/query.d.ts +72 -0
- package/dist/src/sources/sparql/query.d.ts.map +1 -0
- package/dist/src/sources/sparql/query.js +113 -0
- package/dist/src/sources/sparql/query.js.map +1 -0
- package/dist/src/sources/types.d.ts +31 -0
- package/dist/src/sources/types.d.ts.map +1 -1
- package/dist/src/styles/annotations.d.ts +61 -5
- package/dist/src/styles/annotations.d.ts.map +1 -1
- package/dist/src/styles/annotations.js +130 -22
- package/dist/src/styles/annotations.js.map +1 -1
- package/dist/src/styles/canon/bends.d.ts +46 -0
- package/dist/src/styles/canon/bends.d.ts.map +1 -1
- package/dist/src/styles/canon/bends.js +123 -0
- package/dist/src/styles/canon/bends.js.map +1 -1
- package/dist/src/styles/canon/color-tables.d.ts +6 -1
- package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
- package/dist/src/styles/canon/color-tables.js +10 -4
- package/dist/src/styles/canon/color-tables.js.map +1 -1
- package/dist/src/styles/canon/color.d.ts +3 -1
- package/dist/src/styles/canon/color.d.ts.map +1 -1
- package/dist/src/styles/canon/color.js +103 -3
- package/dist/src/styles/canon/color.js.map +1 -1
- package/dist/src/styles/canon/enums.d.ts +2 -2
- package/dist/src/styles/canon/enums.d.ts.map +1 -1
- package/dist/src/styles/canon/enums.js +81 -7
- package/dist/src/styles/canon/enums.js.map +1 -1
- package/dist/src/styles/canon/graphviz.d.ts +12 -3
- package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
- package/dist/src/styles/canon/graphviz.js +57 -9
- package/dist/src/styles/canon/graphviz.js.map +1 -1
- package/dist/src/styles/canon/number.d.ts +8 -0
- package/dist/src/styles/canon/number.d.ts.map +1 -1
- package/dist/src/styles/canon/number.js +18 -0
- package/dist/src/styles/canon/number.js.map +1 -1
- package/dist/src/styles/canon/text.d.ts +29 -2
- package/dist/src/styles/canon/text.d.ts.map +1 -1
- package/dist/src/styles/canon/text.js +156 -16
- package/dist/src/styles/canon/text.js.map +1 -1
- package/dist/src/styles/codes.d.ts +2 -0
- package/dist/src/styles/codes.d.ts.map +1 -1
- package/dist/src/styles/codes.js +2 -0
- package/dist/src/styles/codes.js.map +1 -1
- package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
- package/dist/src/styles/cyjs/cascade.js +1015 -79
- package/dist/src/styles/cyjs/cascade.js.map +1 -1
- package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
- package/dist/src/styles/cyjs/defaults-table.js +3 -0
- package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
- package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
- package/dist/src/styles/cyjs/emit.js +202 -9
- package/dist/src/styles/cyjs/emit.js.map +1 -1
- package/dist/src/styles/cyjs/face.d.ts +20 -0
- package/dist/src/styles/cyjs/face.d.ts.map +1 -0
- package/dist/src/styles/cyjs/face.js +332 -0
- package/dist/src/styles/cyjs/face.js.map +1 -0
- package/dist/src/styles/cyjs/mappers.d.ts +44 -1
- package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
- package/dist/src/styles/cyjs/mappers.js +461 -26
- package/dist/src/styles/cyjs/mappers.js.map +1 -1
- package/dist/src/styles/cyjs/routes.d.ts +53 -0
- package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
- package/dist/src/styles/cyjs/routes.js +360 -0
- package/dist/src/styles/cyjs/routes.js.map +1 -0
- package/dist/src/styles/cytoscape/charts.d.ts +23 -0
- package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/charts.js +560 -0
- package/dist/src/styles/cytoscape/charts.js.map +1 -0
- package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/emit.js +156 -48
- package/dist/src/styles/cytoscape/emit.js.map +1 -1
- package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
- package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/lexicon.js +387 -77
- package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
- package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
- package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/mappings.js +92 -0
- package/dist/src/styles/cytoscape/mappings.js.map +1 -1
- package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
- package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/resolve.js +623 -17
- package/dist/src/styles/cytoscape/resolve.js.map +1 -1
- package/dist/src/styles/defaults.d.ts +46 -26
- package/dist/src/styles/defaults.d.ts.map +1 -1
- package/dist/src/styles/defaults.js +308 -71
- package/dist/src/styles/defaults.js.map +1 -1
- package/dist/src/styles/index.d.ts +4 -3
- package/dist/src/styles/index.d.ts.map +1 -1
- package/dist/src/styles/index.js +3 -2
- package/dist/src/styles/index.js.map +1 -1
- package/dist/src/styles/internal.d.ts +1 -0
- package/dist/src/styles/internal.d.ts.map +1 -1
- package/dist/src/styles/internal.js +1 -0
- package/dist/src/styles/internal.js.map +1 -1
- package/dist/src/styles/io/generic.d.ts.map +1 -1
- package/dist/src/styles/io/generic.js +56 -16
- package/dist/src/styles/io/generic.js.map +1 -1
- package/dist/src/styles/io/round-trip.d.ts +56 -0
- package/dist/src/styles/io/round-trip.d.ts.map +1 -0
- package/dist/src/styles/io/round-trip.js +138 -0
- package/dist/src/styles/io/round-trip.js.map +1 -0
- package/dist/src/styles/io/writer.d.ts.map +1 -1
- package/dist/src/styles/io/writer.js +6 -1
- package/dist/src/styles/io/writer.js.map +1 -1
- package/dist/src/styles/notes.d.ts.map +1 -1
- package/dist/src/styles/notes.js +1 -0
- package/dist/src/styles/notes.js.map +1 -1
- package/dist/src/styles/plan/features.d.ts +1 -1
- package/dist/src/styles/plan/features.d.ts.map +1 -1
- package/dist/src/styles/plan/features.js +22 -2
- package/dist/src/styles/plan/features.js.map +1 -1
- package/dist/src/styles/plan/index.d.ts +1 -1
- package/dist/src/styles/plan/index.d.ts.map +1 -1
- package/dist/src/styles/plan/index.js +1 -1
- package/dist/src/styles/plan/index.js.map +1 -1
- package/dist/src/styles/plan/plan.d.ts.map +1 -1
- package/dist/src/styles/plan/plan.js +61 -4
- package/dist/src/styles/plan/plan.js.map +1 -1
- package/dist/src/styles/plan/resolve.d.ts +11 -0
- package/dist/src/styles/plan/resolve.d.ts.map +1 -1
- package/dist/src/styles/plan/resolve.js +40 -4
- package/dist/src/styles/plan/resolve.js.map +1 -1
- package/dist/src/styles/slots.d.ts +14 -0
- package/dist/src/styles/slots.d.ts.map +1 -0
- package/dist/src/styles/slots.js +20 -0
- package/dist/src/styles/slots.js.map +1 -0
- package/dist/src/styles/types.d.ts +622 -6
- package/dist/src/styles/types.d.ts.map +1 -1
- package/dist/src/styles/vocabulary.d.ts +3 -2
- package/dist/src/styles/vocabulary.d.ts.map +1 -1
- package/dist/src/styles/vocabulary.js +203 -0
- package/dist/src/styles/vocabulary.js.map +1 -1
- package/dist/styles.js +30 -239
- package/dist/styles.js.map +1 -1
- package/dist/xgmml.js +3 -3
- package/package.json +27 -7
- package/src/formats/cx/importer.ts +6 -1
- package/src/formats/cx/styles.ts +143 -9
- package/src/formats/cx2/importer.ts +1 -0
- package/src/formats/cx2/styles.ts +3 -1
- package/src/formats/cys/importer.ts +1 -0
- package/src/formats/cys/styles.ts +27 -5
- package/src/formats/dot/drawing.ts +799 -0
- package/src/formats/dot/importer.ts +120 -35
- package/src/formats/dot/label-places.ts +228 -0
- package/src/formats/dot/record.ts +336 -0
- package/src/formats/dot/styles.ts +1654 -153
- package/src/formats/gexf/dialog-metrics.ts +61 -0
- package/src/formats/gexf/importer.ts +50 -5
- package/src/formats/gexf/preview.ts +217 -0
- package/src/formats/gexf/schema.ts +2 -1
- package/src/formats/gexf/styles.ts +1099 -245
- package/src/formats/gml/importer.ts +291 -76
- package/src/formats/gml/index.ts +1 -1
- package/src/formats/gml/positions.ts +29 -7
- package/src/formats/gml/styles.ts +851 -109
- package/src/formats/graphml/styles-plain.ts +6 -15
- package/src/formats/graphml/styles-yed.ts +762 -173
- package/src/formats/graphml/styles-yfiles3.ts +485 -63
- package/src/formats/graphml/yfiles.ts +19 -6
- package/src/formats/json/dialect.ts +41 -5
- package/src/formats/json/importer.ts +59 -7
- package/src/formats/json/index.ts +1 -0
- package/src/formats/json/positions.ts +22 -5
- package/src/formats/json/styles.ts +2041 -185
- package/src/formats/pajek/draw.ts +458 -0
- package/src/formats/pajek/exporter.ts +81 -49
- package/src/formats/pajek/importer.ts +278 -103
- package/src/formats/pajek/index.ts +1 -0
- package/src/formats/pajek/styles.ts +1381 -133
- package/src/formats/pajek/syntax.ts +27 -2
- package/src/formats/xgmml/emit.ts +73 -5
- package/src/formats/xgmml/styles.ts +330 -71
- package/src/index.ts +5 -1
- package/src/positions/convert.ts +12 -1
- package/src/positions/extract.ts +55 -1
- package/src/positions/index.ts +1 -0
- package/src/positions/types.ts +60 -0
- package/src/sources/builtins.ts +8 -1
- package/src/sources/context.ts +1 -0
- package/src/sources/cypher/descriptor.ts +7 -1
- package/src/sources/cypher/index.ts +148 -18
- package/src/sources/cypher/params.ts +1 -1
- package/src/sources/cypher/query-builder.ts +33 -0
- package/src/sources/ids.ts +6 -0
- package/src/sources/params.ts +3 -0
- package/src/sources/rest/descriptor.ts +5 -0
- package/src/sources/rest/source.ts +16 -0
- package/src/sources/sigv4.ts +6 -4
- package/src/sources/sparql/descriptor.ts +67 -0
- package/src/sources/sparql/index.ts +356 -0
- package/src/sources/sparql/ntriples.ts +219 -0
- package/src/sources/sparql/params.ts +28 -0
- package/src/sources/sparql/query.ts +162 -0
- package/src/sources/types.ts +35 -0
- package/src/styles/annotations.ts +201 -27
- package/src/styles/canon/bends.ts +137 -0
- package/src/styles/canon/color-tables.ts +10 -4
- package/src/styles/canon/color.ts +105 -3
- package/src/styles/canon/enums.ts +98 -8
- package/src/styles/canon/graphviz.ts +71 -10
- package/src/styles/canon/number.ts +19 -0
- package/src/styles/canon/text.ts +187 -16
- package/src/styles/codes.ts +2 -0
- package/src/styles/cyjs/cascade.ts +1160 -84
- package/src/styles/cyjs/defaults-table.ts +3 -0
- package/src/styles/cyjs/emit.ts +233 -13
- package/src/styles/cyjs/face.ts +372 -0
- package/src/styles/cyjs/mappers.ts +492 -27
- package/src/styles/cyjs/routes.ts +432 -0
- package/src/styles/cytoscape/charts.ts +684 -0
- package/src/styles/cytoscape/emit.ts +170 -48
- package/src/styles/cytoscape/lexicon.ts +431 -78
- package/src/styles/cytoscape/mappings.ts +108 -0
- package/src/styles/cytoscape/resolve.ts +741 -17
- package/src/styles/defaults.ts +417 -108
- package/src/styles/index.ts +5 -0
- package/src/styles/internal.ts +1 -0
- package/src/styles/io/generic.ts +79 -23
- package/src/styles/io/round-trip.ts +202 -0
- package/src/styles/io/writer.ts +6 -1
- package/src/styles/notes.ts +1 -0
- package/src/styles/plan/features.ts +23 -2
- package/src/styles/plan/index.ts +1 -1
- package/src/styles/plan/plan.ts +69 -5
- package/src/styles/plan/resolve.ts +47 -4
- package/src/styles/slots.ts +25 -0
- package/src/styles/types.ts +633 -5
- package/src/styles/vocabulary.ts +211 -2
- package/dist/chunks/block-Cu22ewuS.js.map +0 -1
- package/dist/chunks/choose-Cdx6Enl2.js +0 -57
- package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
- package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
- package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
- package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
- package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
- package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
- package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
- package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
- package/dist/chunks/format-BMS-1Nxb.js +0 -64
- package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
- package/dist/chunks/generic-CP6YD34u.js.map +0 -1
- package/dist/chunks/importer-B87_fmVx.js.map +0 -1
- package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
- package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
- package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
- package/dist/chunks/importer-DPZDN40-.js.map +0 -1
- package/dist/chunks/importer-e3huMZbm.js.map +0 -1
- package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
- package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
- package/dist/chunks/styles-CaABMoO1.js.map +0 -1
- package/dist/chunks/styles-P_gmFAOi.js +0 -770
- package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
- package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
- package/dist/chunks/writer-DvHTl36J.js +0 -449
- package/dist/chunks/writer-DvHTl36J.js.map +0 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `none` and `invis` are transparent, and a color list is read as Graphviz draws it for that use.
|
|
14
14
|
* - Outlines: `plain`, `plaintext` and `none` draw none. `setlinewidth(n)` is a pen width, `bold` a width of 2.
|
|
15
15
|
* - Sizes: exact with `fixedsize`, an empty label or a `point`; otherwise minimums Graphviz grows to fit the label,
|
|
16
|
-
* carried
|
|
16
|
+
* carried as `labelFit: "inside"` with the label's padding (`margin`, else 8 by 4 points).
|
|
17
17
|
* - Arrows per `dir`, `arrowhead` and `arrowtail`, with the graph's kind deciding the default: a target arrow on
|
|
18
18
|
* directed edges, as one tool-default rule.
|
|
19
19
|
* - A cluster's container node holds the cluster's attributes: its fill (`bgcolor`, or the fill when `filled`) and
|
|
@@ -30,25 +30,27 @@ import type { GraphSnapshot } from "@graphty/graph-format";
|
|
|
30
30
|
|
|
31
31
|
import { isWritableDotText } from "../../common/escape.js";
|
|
32
32
|
import { inferTextDtype } from "../../common/text.js";
|
|
33
|
+
import { type BendPoint, canonBends, canonNodeOffsets, flattenCubic } from "../../styles/canon/bends.js";
|
|
33
34
|
import { canonColor, colorOpacity } from "../../styles/canon/color.js";
|
|
34
35
|
import { canonEnum } from "../../styles/canon/enums.js";
|
|
35
36
|
import { canonNumber, DOT_POINTS_PER_INCH, num6 } from "../../styles/canon/number.js";
|
|
36
37
|
import { type CanonSite, MAX_STYLE_VALUE_LENGTH, noteAt } from "../../styles/canon/site.js";
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
type DotEscapeNames,
|
|
40
|
+
dotLabelAlign,
|
|
41
|
+
dotLabelText,
|
|
42
|
+
htmlLabelSpans,
|
|
43
|
+
htmlLabelText,
|
|
44
|
+
recordLabelText,
|
|
45
|
+
} from "../../styles/canon/text.js";
|
|
38
46
|
import { STYLE_ISSUE } from "../../styles/codes.js";
|
|
39
47
|
import { toolDefaults } from "../../styles/defaults.js";
|
|
40
48
|
import { StyleApproximations } from "../../styles/io/approximations.js";
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
parseStyleBlock,
|
|
44
|
-
STYLE_BLOCK_NAME,
|
|
45
|
-
styleBlockLosses,
|
|
46
|
-
writeStyleBlock,
|
|
47
|
-
} from "../../styles/io/block.js";
|
|
49
|
+
import { STYLE_BLOCK_NAME, styleBlockLosses, writeStyleBlock } from "../../styles/io/block.js";
|
|
50
|
+
import { elementValues, takeFromBlock } from "../../styles/io/round-trip.js";
|
|
48
51
|
import { type ExporterStyleInput, sharedInExportCall } from "../../styles/io/writer.js";
|
|
49
52
|
import { StyleNotes } from "../../styles/notes.js";
|
|
50
|
-
import {
|
|
51
|
-
import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
|
|
53
|
+
import { plainCell } from "../../styles/plan/resolve.js";
|
|
52
54
|
import type {
|
|
53
55
|
ImporterStyles,
|
|
54
56
|
StyleColumn,
|
|
@@ -61,7 +63,19 @@ import type {
|
|
|
61
63
|
} from "../../styles/types.js";
|
|
62
64
|
import type { LossNote } from "../../types.js";
|
|
63
65
|
import { type ValueColumns, valueColumns } from "../graphml/styles-yed.js";
|
|
64
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
GRAPHVIZ_DECORATED_BOXES,
|
|
68
|
+
GRAPHVIZ_GLYPH_SHAPES,
|
|
69
|
+
GRAPHVIZ_POLYGON_SHAPES,
|
|
70
|
+
graphvizFont,
|
|
71
|
+
graphvizFontName,
|
|
72
|
+
graphvizGlyph,
|
|
73
|
+
graphvizPolygonCorners,
|
|
74
|
+
isGraphvizBox,
|
|
75
|
+
} from "./drawing.js";
|
|
76
|
+
import { type LabelPlaceContext, readLabelPlaces } from "./label-places.js";
|
|
77
|
+
import { CLUSTER_COLUMN, LABEL_ATTRIBUTE, POS_ATTRIBUTE } from "./names.js";
|
|
78
|
+
import { layoutRecord } from "./record.js";
|
|
65
79
|
|
|
66
80
|
/**
|
|
67
81
|
* The graph attribute holding the `graphty.styles` block of a DOT file graphty wrote.
|
|
@@ -89,6 +103,14 @@ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
|
|
|
89
103
|
"labelColor",
|
|
90
104
|
"labelSize",
|
|
91
105
|
"labelFont",
|
|
106
|
+
"labelWeight",
|
|
107
|
+
"labelItalic",
|
|
108
|
+
"labelPosition",
|
|
109
|
+
"shapePoints",
|
|
110
|
+
"shapePointsStretch",
|
|
111
|
+
"shapeParts",
|
|
112
|
+
"shapeLines",
|
|
113
|
+
"shapeLineWidth",
|
|
92
114
|
"tooltip",
|
|
93
115
|
"visible",
|
|
94
116
|
] as const),
|
|
@@ -99,6 +121,8 @@ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
|
|
|
99
121
|
"lineStyle",
|
|
100
122
|
"sourceArrow",
|
|
101
123
|
"targetArrow",
|
|
124
|
+
"sourceArrowColor",
|
|
125
|
+
"targetArrowColor",
|
|
102
126
|
"arrowSize",
|
|
103
127
|
"label",
|
|
104
128
|
"sourceLabel",
|
|
@@ -106,6 +130,8 @@ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
|
|
|
106
130
|
"labelColor",
|
|
107
131
|
"labelSize",
|
|
108
132
|
"labelFont",
|
|
133
|
+
"labelWeight",
|
|
134
|
+
"labelItalic",
|
|
109
135
|
"tooltip",
|
|
110
136
|
"visible",
|
|
111
137
|
] as const),
|
|
@@ -135,7 +161,12 @@ const NODE_ATTRIBUTES: readonly string[] = [
|
|
|
135
161
|
"height",
|
|
136
162
|
"fixedsize",
|
|
137
163
|
"regular",
|
|
164
|
+
"sides",
|
|
165
|
+
"distortion",
|
|
166
|
+
"skew",
|
|
167
|
+
"orientation",
|
|
138
168
|
"penwidth",
|
|
169
|
+
"gradientangle",
|
|
139
170
|
"fontname",
|
|
140
171
|
"fontsize",
|
|
141
172
|
"fontcolor",
|
|
@@ -157,8 +188,10 @@ const EDGE_ATTRIBUTES: readonly string[] = [
|
|
|
157
188
|
"arrowtail",
|
|
158
189
|
"dir",
|
|
159
190
|
"arrowsize",
|
|
191
|
+
"fillcolor",
|
|
160
192
|
"headlabel",
|
|
161
193
|
"taillabel",
|
|
194
|
+
"xlabel",
|
|
162
195
|
];
|
|
163
196
|
|
|
164
197
|
/** Attributes the reader notes as not carried. */
|
|
@@ -196,6 +229,10 @@ interface NodeContext {
|
|
|
196
229
|
readonly cluster: boolean;
|
|
197
230
|
/** The HTML strings of the file, for Graphviz's shared string pool. */
|
|
198
231
|
readonly html: ReadonlySet<string>;
|
|
232
|
+
/** Whether the graph's `rankdir` is LR or RL, which turns a record's fields top to bottom. */
|
|
233
|
+
readonly flip: boolean;
|
|
234
|
+
/** The graph's `quantum` in points (a node's padded label box is rounded up to a multiple of it), 0 for none. */
|
|
235
|
+
readonly quantum: number;
|
|
199
236
|
}
|
|
200
237
|
|
|
201
238
|
/** What an edge's label escapes depend on. */
|
|
@@ -336,13 +373,255 @@ function labelText(
|
|
|
336
373
|
return dotLabelText(record ? recordLabelText(raw, at) : raw, names, at);
|
|
337
374
|
}
|
|
338
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Reads how a plain label's lines line up (`\l`, `\r`); an HTML-like or a record label lines up its own way.
|
|
378
|
+
* @param raw - the label as the file gives it
|
|
379
|
+
* @param names - what the label's escapes stand for
|
|
380
|
+
* @param html - the file's HTML strings
|
|
381
|
+
* @param record - whether the node is a record
|
|
382
|
+
* @param put - receives the value
|
|
383
|
+
*/
|
|
384
|
+
function readLabelAlign(
|
|
385
|
+
raw: string,
|
|
386
|
+
names: DotEscapeNames,
|
|
387
|
+
html: ReadonlySet<string>,
|
|
388
|
+
record: boolean,
|
|
389
|
+
put: Put,
|
|
390
|
+
): void {
|
|
391
|
+
if (isHtmlString(raw) || html.has(`<${raw}>`)) {
|
|
392
|
+
// each <BR ALIGN> lines up the line before it: one alignment on every break lines up the label
|
|
393
|
+
const aligns = new Set(
|
|
394
|
+
[...raw.matchAll(/<br\b([^>]*)>/gi)].map(
|
|
395
|
+
(m) => /\balign\s*=\s*["']?(left|right|center)/i.exec(m[1])?.[1].toLowerCase() ?? "center",
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
// words after the last break are a line of their own, centered
|
|
399
|
+
const body = isHtmlString(raw) ? raw.slice(1, -1) : raw;
|
|
400
|
+
const last = /<br\b[^>]*>(?![\s\S]*<br\b)/i.exec(body);
|
|
401
|
+
const tail = last === null ? "" : body.slice(last.index + last[0].length);
|
|
402
|
+
if (stripTags(tail).trim() !== "") {
|
|
403
|
+
aligns.add("center");
|
|
404
|
+
}
|
|
405
|
+
const [only] = aligns;
|
|
406
|
+
if (aligns.size === 1 && only !== "center") {
|
|
407
|
+
put("labelTextAlign", only);
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (record) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const align = dotLabelAlign(raw, names);
|
|
415
|
+
if (align !== null) {
|
|
416
|
+
put("labelTextAlign", align);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Text with its tags (`<` to the next `>`) dropped; a `<` with no `>` after it, and the rest, kept.
|
|
422
|
+
* @param text - the text
|
|
423
|
+
* @returns the text without tags
|
|
424
|
+
*/
|
|
425
|
+
function stripTags(text: string): string {
|
|
426
|
+
let out = "";
|
|
427
|
+
let i = 0;
|
|
428
|
+
while (i < text.length) {
|
|
429
|
+
const open = text.indexOf("<", i);
|
|
430
|
+
const close = open < 0 ? -1 : text.indexOf(">", open + 1);
|
|
431
|
+
if (close < 0) {
|
|
432
|
+
return out + text.slice(i);
|
|
433
|
+
}
|
|
434
|
+
out += text.slice(i, open);
|
|
435
|
+
i = close + 1;
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Reads the font runs of an HTML-like label (`<B>`, `<I>`, `<FONT COLOR FACE POINT-SIZE>`) as Graphviz draws them:
|
|
442
|
+
* one run in a font of its own sets the label's own font (color, size, family, weight, slant), several are
|
|
443
|
+
* `labelRuns`. Underline, overline, strike-through, `<SUB>` and `<SUP>` have no neutral slot and draw plain.
|
|
444
|
+
* @param raw - the label as the file gives it
|
|
445
|
+
* @param html - the file's HTML strings
|
|
446
|
+
* @param scheme - the element's color scheme
|
|
447
|
+
* @param put - receives the values
|
|
448
|
+
* @param at - the sites
|
|
449
|
+
*/
|
|
450
|
+
function readHtmlRuns(raw: string, html: ReadonlySet<string>, scheme: string | null, put: Put, at: At): void {
|
|
451
|
+
if (!isHtmlString(raw) && !html.has(`<${raw}>`)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const markup = isHtmlString(raw) ? raw : `<${raw}>`;
|
|
455
|
+
const runs: Record<string, StyleValue>[] = [];
|
|
456
|
+
for (const span of htmlLabelSpans(markup)) {
|
|
457
|
+
const run = { text: span.text, ...htmlRunFont(span.tags, scheme, at("labelRuns")) };
|
|
458
|
+
const last = runs.at(-1);
|
|
459
|
+
if (last !== undefined && JSON.stringify({ ...last, text: "" }) === JSON.stringify({ ...run, text: "" })) {
|
|
460
|
+
last.text = `${String(last.text)}${run.text}`;
|
|
461
|
+
} else {
|
|
462
|
+
runs.push(run);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
trimBlankRuns(runs);
|
|
466
|
+
if (runs.every((r) => Object.keys(r).length === 1)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (runs.length === 1) {
|
|
470
|
+
const { text: _text, ...font } = runs[0];
|
|
471
|
+
for (const [property, value] of Object.entries(font)) {
|
|
472
|
+
put(RUN_PROPERTY[property], value);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
runs[0].text = String(runs[0].text).trimStart();
|
|
477
|
+
const end = runs.at(-1);
|
|
478
|
+
if (end !== undefined) {
|
|
479
|
+
end.text = String(end.text).trimEnd();
|
|
480
|
+
}
|
|
481
|
+
put("labelRuns", JSON.stringify(runs));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Drops the blank runs at a label's ends, as htmlLabelText trims its blanks.
|
|
486
|
+
* @param runs - the runs, trimmed in place
|
|
487
|
+
*/
|
|
488
|
+
function trimBlankRuns(runs: Record<string, StyleValue>[]): void {
|
|
489
|
+
while (runs.length > 0 && String(runs[0].text).trim() === "") {
|
|
490
|
+
runs.shift();
|
|
491
|
+
}
|
|
492
|
+
while (runs.length > 0 && String(runs.at(-1)?.text).trim() === "") {
|
|
493
|
+
runs.pop();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The label property a run's own font field sets when the whole label is one run. */
|
|
498
|
+
const RUN_PROPERTY: Readonly<Record<string, string>> = {
|
|
499
|
+
weight: "labelWeight",
|
|
500
|
+
italic: "labelItalic",
|
|
501
|
+
font: "labelFont",
|
|
502
|
+
size: "labelSize",
|
|
503
|
+
color: "labelColor",
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The font of a run inside HTML font tags, as `labelRuns` fields: bold, italic, and the innermost `FONT`'s face,
|
|
508
|
+
* point size and color.
|
|
509
|
+
* @param tags - the open tags, outermost first
|
|
510
|
+
* @param scheme - the element's color scheme
|
|
511
|
+
* @param site - where a bad color is noted
|
|
512
|
+
* @returns the fields that differ from the label's own font
|
|
513
|
+
*/
|
|
514
|
+
function htmlRunFont(tags: readonly string[], scheme: string | null, site: CanonSite): Record<string, StyleValue> {
|
|
515
|
+
const out: Record<string, StyleValue> = {};
|
|
516
|
+
for (const tag of tags) {
|
|
517
|
+
const name = /^<(\w+)/.exec(tag)?.[1].toLowerCase();
|
|
518
|
+
if (name === "b") {
|
|
519
|
+
out.weight = "bold";
|
|
520
|
+
} else if (name === "i") {
|
|
521
|
+
out.italic = true;
|
|
522
|
+
} else if (name === "font") {
|
|
523
|
+
for (const [attr, value] of tagAttributes(tag)) {
|
|
524
|
+
readFontAttribute(attr.toLowerCase(), value, scheme, site, out);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Reads one attribute of a `FONT` tag into a run's font fields: its face, point size or color.
|
|
533
|
+
* @param attr - the attribute's name, lowercase
|
|
534
|
+
* @param value - its value
|
|
535
|
+
* @param scheme - the element's color scheme
|
|
536
|
+
* @param site - where a bad color is noted
|
|
537
|
+
* @param out - the run's font fields, set
|
|
538
|
+
*/
|
|
539
|
+
function readFontAttribute(
|
|
540
|
+
attr: string,
|
|
541
|
+
value: string,
|
|
542
|
+
scheme: string | null,
|
|
543
|
+
site: CanonSite,
|
|
544
|
+
out: Record<string, StyleValue>,
|
|
545
|
+
): void {
|
|
546
|
+
if (attr === "face" && value.trim() !== "" && value.length <= MAX_STYLE_VALUE_LENGTH) {
|
|
547
|
+
const drawn = graphvizFont(value);
|
|
548
|
+
out.font = drawn.family;
|
|
549
|
+
if (drawn.bold) {
|
|
550
|
+
out.weight = "bold";
|
|
551
|
+
}
|
|
552
|
+
if (drawn.italic) {
|
|
553
|
+
out.italic = true;
|
|
554
|
+
}
|
|
555
|
+
} else if (attr === "point-size" && Number(value) > 0) {
|
|
556
|
+
out.size = Number(value);
|
|
557
|
+
} else if (attr === "color") {
|
|
558
|
+
const c = canonColor(value, "graphviz", site, { scheme, use: "text" });
|
|
559
|
+
if (c !== null) {
|
|
560
|
+
out.color = c.color;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Whether a character can be part of an HTML attribute's name (a word character or `-`). */
|
|
566
|
+
const ATTRIBUTE_NAME_CHAR = /^[\w-]$/;
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The quoted attributes of a tag, `name = "value"` or `name = 'value'`, in order; an unquoted or unclosed value is
|
|
570
|
+
* skipped. Scans the tag once (an attribute regex would backtrack on a long name with no `=`).
|
|
571
|
+
* @param tag - the tag
|
|
572
|
+
* @returns each attribute's name and value
|
|
573
|
+
*/
|
|
574
|
+
function tagAttributes(tag: string): [string, string][] {
|
|
575
|
+
const attributes: [string, string][] = [];
|
|
576
|
+
let i = 0;
|
|
577
|
+
while (i < tag.length) {
|
|
578
|
+
if (!ATTRIBUTE_NAME_CHAR.test(tag[i])) {
|
|
579
|
+
i++;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
let end = i;
|
|
583
|
+
while (end < tag.length && ATTRIBUTE_NAME_CHAR.test(tag[end])) {
|
|
584
|
+
end++;
|
|
585
|
+
}
|
|
586
|
+
const value = quotedValueAt(tag, end);
|
|
587
|
+
if (value !== null) {
|
|
588
|
+
attributes.push([tag.slice(i, end), value.text]);
|
|
589
|
+
}
|
|
590
|
+
i = value === null ? end : value.next;
|
|
591
|
+
}
|
|
592
|
+
return attributes;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* The quoted value of an attribute whose name ends at `at`: blanks, `=`, blanks, then text between matching quotes.
|
|
597
|
+
* @param tag - the tag
|
|
598
|
+
* @param at - where the name ends
|
|
599
|
+
* @returns the value and where reading goes on after its closing quote, or null when there is none
|
|
600
|
+
*/
|
|
601
|
+
function quotedValueAt(tag: string, at: number): { text: string; next: number } | null {
|
|
602
|
+
let i = at;
|
|
603
|
+
while (i < tag.length && /\s/.test(tag[i])) {
|
|
604
|
+
i++;
|
|
605
|
+
}
|
|
606
|
+
if (tag[i] !== "=") {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
i++;
|
|
610
|
+
while (i < tag.length && /\s/.test(tag[i])) {
|
|
611
|
+
i++;
|
|
612
|
+
}
|
|
613
|
+
const quote = tag[i];
|
|
614
|
+
const close = quote === '"' || quote === "'" ? tag.indexOf(quote, i + 1) : -1;
|
|
615
|
+
return close < 0 ? null : { text: tag.slice(i + 1, close), next: close + 1 };
|
|
616
|
+
}
|
|
617
|
+
|
|
339
618
|
/**
|
|
340
619
|
* Reads a color into a property and its alpha into the property's opacity.
|
|
341
620
|
* @param raw - the color
|
|
342
621
|
* @param scheme - the object's color scheme
|
|
343
622
|
* @param use - how Graphviz draws it
|
|
344
623
|
* @param property - the color property
|
|
345
|
-
* @param opacity - its opacity property, or null when the alpha is dropped
|
|
624
|
+
* @param opacity - its opacity property, or null when the alpha is dropped (an outline's is its borderOpacity)
|
|
346
625
|
* @param put - receives the values
|
|
347
626
|
* @param at - the sites
|
|
348
627
|
* @returns the alpha, or null when the color is absent
|
|
@@ -350,7 +629,7 @@ function labelText(
|
|
|
350
629
|
function readColor(
|
|
351
630
|
raw: string | null,
|
|
352
631
|
scheme: string | null,
|
|
353
|
-
use: "line" | "fill" | "edge",
|
|
632
|
+
use: "line" | "text" | "fill" | "edge",
|
|
354
633
|
property: string,
|
|
355
634
|
opacity: string | null,
|
|
356
635
|
put: Put,
|
|
@@ -364,10 +643,13 @@ function readColor(
|
|
|
364
643
|
return null;
|
|
365
644
|
}
|
|
366
645
|
put(property, c.color);
|
|
367
|
-
if (
|
|
646
|
+
if (property === "borderColor") {
|
|
647
|
+
// a transparent outline is no outline (the caller sets its width to 0); a part-transparent one is drawn so
|
|
648
|
+
put("borderOpacity", c.alpha > 0 && c.alpha < 1 ? c.alpha : null);
|
|
649
|
+
} else if (opacity !== null) {
|
|
368
650
|
put(opacity, c.alpha);
|
|
369
|
-
} else
|
|
370
|
-
//
|
|
651
|
+
} else {
|
|
652
|
+
// any other alpha is dropped, noted
|
|
371
653
|
colorOpacity(c, null, at(property), false);
|
|
372
654
|
}
|
|
373
655
|
return c.alpha;
|
|
@@ -424,15 +706,23 @@ function readCommon(
|
|
|
424
706
|
put: Put,
|
|
425
707
|
at: At,
|
|
426
708
|
): void {
|
|
427
|
-
|
|
709
|
+
// Graphviz takes `invisible` for `invis` too (emit.c `parse_style`)
|
|
710
|
+
if (tokens.includes("invis") || tokens.includes("invisible")) {
|
|
428
711
|
put("visible", false);
|
|
429
712
|
}
|
|
430
|
-
|
|
713
|
+
// a part-transparent font color draws its text so (`#377eb880`); an opaque one needs no opacity
|
|
714
|
+
const putFont: Put = (property, value) => {
|
|
715
|
+
put(property, property === "labelOpacity" && value === 1 ? null : value);
|
|
716
|
+
};
|
|
717
|
+
readColor(nonEmpty(get("fontcolor")), scheme, "text", "labelColor", "labelOpacity", putFont, at);
|
|
431
718
|
const size = get("fontsize");
|
|
432
719
|
put("labelSize", size === null ? null : canonNumber(size, at("labelSize")));
|
|
433
720
|
const font = get("fontname");
|
|
434
721
|
if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
|
|
435
|
-
|
|
722
|
+
const drawn = graphvizFont(font);
|
|
723
|
+
put("labelFont", drawn.family);
|
|
724
|
+
put("labelWeight", drawn.bold ? "bold" : null);
|
|
725
|
+
put("labelItalic", drawn.italic ? true : null);
|
|
436
726
|
}
|
|
437
727
|
const tip = get("tooltip") ?? get("edgetooltip");
|
|
438
728
|
if (tip !== null) {
|
|
@@ -457,6 +747,143 @@ interface NodeRead {
|
|
|
457
747
|
readonly color: string | null;
|
|
458
748
|
}
|
|
459
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Reads a node's label and, for a node that is not a cluster, its size and how it grows to hold the label.
|
|
752
|
+
* @param n - the node
|
|
753
|
+
* @param names - what the label's escapes stand for
|
|
754
|
+
*/
|
|
755
|
+
function readNodeLabelAndSize(n: NodeRead, names: DotEscapeNames): void {
|
|
756
|
+
const { get, cx, put, at, shape } = n;
|
|
757
|
+
// label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
|
|
758
|
+
const label = n.point ? null : get(LABEL_ATTRIBUTE);
|
|
759
|
+
const shown = label === null ? cx.name : labelText(label, names, cx.html, RECORD.has(shape), at("label"));
|
|
760
|
+
readNodeLabel(n, label, shown, names);
|
|
761
|
+
// plain is its label alone at size 0: with an empty label Graphviz draws nothing, filled or not; an HTML label
|
|
762
|
+
// with no words (a table holding an IMG) still draws its cells
|
|
763
|
+
const htmlLabel = label !== null && (isHtmlString(label) || cx.html.has(`<${label}>`));
|
|
764
|
+
if (shape === "plain" && shown === "" && !htmlLabel) {
|
|
765
|
+
put("visible", false);
|
|
766
|
+
}
|
|
767
|
+
if (cx.cluster) {
|
|
768
|
+
readClusterBox(get, put);
|
|
769
|
+
} else {
|
|
770
|
+
readNodeSize(n, label, shown, htmlLabel, names);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Reads a node's label, its alignment and runs, and where it sits.
|
|
776
|
+
* @param n - the node
|
|
777
|
+
* @param label - its label, or null when it has none
|
|
778
|
+
* @param shown - the text it shows
|
|
779
|
+
* @param names - what the label's escapes stand for
|
|
780
|
+
*/
|
|
781
|
+
function readNodeLabel(n: NodeRead, label: string | null, shown: string, names: DotEscapeNames): void {
|
|
782
|
+
const { get, cx, put, at, shape } = n;
|
|
783
|
+
// a cluster has no label of its own unless one is given, so an empty one is none
|
|
784
|
+
if (label !== null && !(cx.cluster && label === "")) {
|
|
785
|
+
put("label", shown);
|
|
786
|
+
readLabelAlign(label, names, cx.html, RECORD.has(shape), put);
|
|
787
|
+
readHtmlRuns(label, cx.html, n.scheme, put, at);
|
|
788
|
+
if (cx.cluster) {
|
|
789
|
+
put("labelPosition", clusterLabelPosition(get));
|
|
790
|
+
readClusterLabelPlace(get, put);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!cx.cluster && !n.point) {
|
|
794
|
+
const position = nodeLabelPosition(get);
|
|
795
|
+
put("labelPosition", position === "center" ? null : position);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Reads the size of a node that is not a cluster and how it grows to hold its label (a record's fields, or its
|
|
801
|
+
* label's fit).
|
|
802
|
+
* @param n - the node
|
|
803
|
+
* @param label - its label, or null when it has none
|
|
804
|
+
* @param shown - the text it shows
|
|
805
|
+
* @param htmlLabel - whether the label is HTML-like
|
|
806
|
+
* @param names - what the label's escapes stand for
|
|
807
|
+
*/
|
|
808
|
+
function readNodeSize(
|
|
809
|
+
n: NodeRead,
|
|
810
|
+
label: string | null,
|
|
811
|
+
shown: string,
|
|
812
|
+
htmlLabel: boolean,
|
|
813
|
+
names: DotEscapeNames,
|
|
814
|
+
): void {
|
|
815
|
+
const { get, put, at, shape } = n;
|
|
816
|
+
readSize(get, shape, n.point, put, at);
|
|
817
|
+
if (RECORD.has(shape) && label !== null && !htmlLabel) {
|
|
818
|
+
readRecord(n, label, names);
|
|
819
|
+
} else if (!n.point) {
|
|
820
|
+
readLabelFit(get, shape, label, shown, put);
|
|
821
|
+
readQuantum(n, label);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Graphviz's default node width and height, in points (0.75 and 0.5 inch). */
|
|
826
|
+
const DEFAULT_NODE_SIZE = { w: 54, h: 36 } as const;
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Reads a record node as Graphviz lays it out: its size (at least its fields' unless `fixedsize`), the dividers
|
|
830
|
+
* between its fields as lines over the node, and each field's text lines where Graphviz centers or justifies them,
|
|
831
|
+
* in place of one label.
|
|
832
|
+
* @param n - the node
|
|
833
|
+
* @param label - its record label
|
|
834
|
+
* @param names - what the label's escapes stand for
|
|
835
|
+
*/
|
|
836
|
+
function readRecord(n: NodeRead, label: string, names: DotEscapeNames): void {
|
|
837
|
+
const { get, put, at, cx } = n;
|
|
838
|
+
const pt = (attr: string, fallback: number): number => {
|
|
839
|
+
const v = inches(get(attr), at(attr === "width" ? "width" : "height"));
|
|
840
|
+
return v === null ? fallback : v * DOT_POINTS_PER_INCH;
|
|
841
|
+
};
|
|
842
|
+
const size = Number(get("fontsize") ?? 14);
|
|
843
|
+
const lay = layoutRecord(
|
|
844
|
+
label,
|
|
845
|
+
!cx.flip,
|
|
846
|
+
pt("width", DEFAULT_NODE_SIZE.w),
|
|
847
|
+
pt("height", DEFAULT_NODE_SIZE.h),
|
|
848
|
+
isTrue(get("fixedsize")),
|
|
849
|
+
Number.isFinite(size) && size > 0 ? size : 14,
|
|
850
|
+
(line) => dotLabelText(line, names),
|
|
851
|
+
);
|
|
852
|
+
const hw = lay.width / 2;
|
|
853
|
+
const hh = lay.height / 2;
|
|
854
|
+
const fx = (x: number): string => String(num6(hw > 0 ? x / hw : 0));
|
|
855
|
+
const fy = (y: number): string => String(num6(hh > 0 ? y / hh : 0));
|
|
856
|
+
put("width", num6(lay.width));
|
|
857
|
+
put("height", num6(lay.height));
|
|
858
|
+
put("label", "");
|
|
859
|
+
put(
|
|
860
|
+
"shapeLines",
|
|
861
|
+
lay.dividers.length === 0
|
|
862
|
+
? null
|
|
863
|
+
: lay.dividers.map(([x0, y0, x1, y1]) => `${fx(x0)},${fy(y0)} ${fx(x1)},${fy(y1)}`).join("; "),
|
|
864
|
+
);
|
|
865
|
+
put(
|
|
866
|
+
"shapeTexts",
|
|
867
|
+
lay.texts.length === 0
|
|
868
|
+
? null
|
|
869
|
+
: JSON.stringify(lay.texts.map((t) => [Number(fx(t.x)), Number(fy(t.y)), t.text])),
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Reads a laid-out cluster's size from its `bb` ("llx,lly,urx,ury" in points), the box Graphviz draws round its
|
|
875
|
+
* members and its label; a cluster with no `bb` is drawn round its members.
|
|
876
|
+
* @param get - the cluster's attributes
|
|
877
|
+
* @param put - receives the values
|
|
878
|
+
*/
|
|
879
|
+
function readClusterBox(get: Attr, put: Put): void {
|
|
880
|
+
const parts = (get("bb") ?? "").split(",").map(Number);
|
|
881
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] >= parts[0] && parts[3] >= parts[1]) {
|
|
882
|
+
put("width", num6(parts[2] - parts[0]));
|
|
883
|
+
put("height", num6(parts[3] - parts[1]));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
460
887
|
/**
|
|
461
888
|
* Reads one node, or one cluster's container, the way Graphviz draws it.
|
|
462
889
|
* @param get - its attributes
|
|
@@ -488,21 +915,72 @@ function readNode(get: Attr, cx: NodeContext, put: Put, at: At): void {
|
|
|
488
915
|
} else {
|
|
489
916
|
readNodeFill(n);
|
|
490
917
|
}
|
|
918
|
+
readNodeBands(n);
|
|
491
919
|
readNodeOutline(n);
|
|
492
920
|
readNodeShape(n);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
put("label", labelText(label, names, cx.html, RECORD.has(shape), at("label")));
|
|
921
|
+
readNodeLabelAndSize(n, names);
|
|
922
|
+
if (nonEmpty(get("xlabel")) !== null) {
|
|
923
|
+
noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
|
|
497
924
|
}
|
|
498
|
-
|
|
499
|
-
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Where a node's label sits: its `labelloc` puts it against the top or bottom of the node's box, inside it; it is
|
|
929
|
+
* centered by default.
|
|
930
|
+
* @param get - the node's attributes
|
|
931
|
+
* @returns the position
|
|
932
|
+
*/
|
|
933
|
+
function nodeLabelPosition(get: Attr): "inside-top" | "inside-bottom" | "center" {
|
|
934
|
+
const loc = (get("labelloc") ?? "").trim().toLowerCase();
|
|
935
|
+
if (loc.startsWith("t")) {
|
|
936
|
+
return "inside-top";
|
|
500
937
|
}
|
|
501
|
-
|
|
502
|
-
|
|
938
|
+
return loc.startsWith("b") ? "inside-bottom" : "center";
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** How far in from the cluster's side Graphviz draws a label justified left or right: half its label's padding. */
|
|
942
|
+
const CLUSTER_LABEL_INSET = 8;
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Where Graphviz draws a cluster's label beyond its position: at a stored `lp` (its center) inside the cluster's
|
|
946
|
+
* `bb`, else `CLUSTER_LABEL_INSET` points in from the side `labeljust` puts it against.
|
|
947
|
+
* @param get - the cluster's attributes
|
|
948
|
+
* @param put - receives the values
|
|
949
|
+
*/
|
|
950
|
+
function readClusterLabelPlace(get: Attr, put: Put): void {
|
|
951
|
+
const lp = (get("lp") ?? "").split(",").map(Number);
|
|
952
|
+
const bb = (get("bb") ?? "").split(",").map(Number);
|
|
953
|
+
if (lp.length === 2 && bb.length === 4 && [...lp, ...bb].every(Number.isFinite)) {
|
|
954
|
+
put("labelPosition", "center");
|
|
955
|
+
put("labelOffsetX", num6(lp[0] - (bb[0] + bb[2]) / 2));
|
|
956
|
+
put("labelOffsetY", num6((bb[1] + bb[3]) / 2 - lp[1]));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const just = (get("labeljust") ?? "").trim().toLowerCase();
|
|
960
|
+
if (just.startsWith("l")) {
|
|
961
|
+
put("labelOffsetX", CLUSTER_LABEL_INSET);
|
|
962
|
+
} else if (just.startsWith("r")) {
|
|
963
|
+
put("labelOffsetX", -CLUSTER_LABEL_INSET);
|
|
503
964
|
}
|
|
504
965
|
}
|
|
505
966
|
|
|
967
|
+
/**
|
|
968
|
+
* Where a cluster's label sits: inside the cluster's box (Graphviz grows the box to hold it), at the top unless
|
|
969
|
+
* `labelloc` is `b`, centered unless `labeljust` is `l` or `r`.
|
|
970
|
+
* @param get - the cluster's attributes
|
|
971
|
+
* @returns the position
|
|
972
|
+
*/
|
|
973
|
+
function clusterLabelPosition(
|
|
974
|
+
get: Attr,
|
|
975
|
+
): `inside-${"top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right"}` {
|
|
976
|
+
const loc = (get("labelloc") ?? "").trim().toLowerCase().startsWith("b") ? "inside-bottom" : "inside-top";
|
|
977
|
+
const just = (get("labeljust") ?? "").trim().toLowerCase();
|
|
978
|
+
if (just.startsWith("l")) {
|
|
979
|
+
return `${loc}-left`;
|
|
980
|
+
}
|
|
981
|
+
return just.startsWith("r") ? `${loc}-right` : loc;
|
|
982
|
+
}
|
|
983
|
+
|
|
506
984
|
/**
|
|
507
985
|
* Reads a cluster's fill: its fill color when filled, else its background.
|
|
508
986
|
* @param n - the cluster
|
|
@@ -513,6 +991,53 @@ function readClusterFill(n: NodeRead): void {
|
|
|
513
991
|
const bg = nonEmpty(get("bgcolor"));
|
|
514
992
|
const raw = filled ? (nonEmpty(get("fillcolor")) ?? n.color ?? bg ?? "lightgrey") : bg;
|
|
515
993
|
readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
|
|
994
|
+
if (!tokens.includes("striped")) {
|
|
995
|
+
readFillGradient(n, raw);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Reads a gradient fill (`fillcolor="a:b"`, `gradientangle`): Graphviz runs it from the list's first color to
|
|
1001
|
+
* its second, `gradientangle` degrees counter-clockwise from left to right (0 by default), which is the neutral
|
|
1002
|
+
* angle's clockwise turn negated, from 0 up to 360. `style=radial` runs it out from the middle instead. A second color
|
|
1003
|
+
* left out is black; colors after the second are not drawn. A first color's `;fraction` makes Graphviz switch colors
|
|
1004
|
+
* at that fraction, which its SVG writes as stops at the fraction less 0.001 and at the fraction.
|
|
1005
|
+
* @param n - the node or cluster
|
|
1006
|
+
* @param raw - its fill color value
|
|
1007
|
+
*/
|
|
1008
|
+
function readFillGradient(n: NodeRead, raw: string | null): void {
|
|
1009
|
+
const colon = raw === null ? -1 : raw.indexOf(":");
|
|
1010
|
+
if (raw === null || colon < 0) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const radial = n.tokens.includes("radial");
|
|
1014
|
+
if (radial) {
|
|
1015
|
+
// ponytail: a radial gradientangle moves Graphviz's focus off the middle; graphty's radial ramp has no focus
|
|
1016
|
+
n.put("fillGradientKind", "radial");
|
|
1017
|
+
}
|
|
1018
|
+
const frac = Number(raw.slice(0, colon).split(";")[1]);
|
|
1019
|
+
if (frac > 0 && frac < 1) {
|
|
1020
|
+
n.put("fillGradientStops", `${String(num6(frac - 0.001))} ${String(num6(frac))}`);
|
|
1021
|
+
}
|
|
1022
|
+
const second = raw
|
|
1023
|
+
.slice(colon + 1)
|
|
1024
|
+
.split(":")[0]
|
|
1025
|
+
.split(";")[0];
|
|
1026
|
+
const c = canonColor(second === "" ? "black" : second, "graphviz", n.at("fillGradient"), {
|
|
1027
|
+
scheme: n.scheme,
|
|
1028
|
+
use: "fill",
|
|
1029
|
+
});
|
|
1030
|
+
if (c === null) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
n.put("fillGradient", c.color);
|
|
1034
|
+
if (radial) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const angle = Number(nonEmpty(n.get("gradientangle")) ?? "0");
|
|
1038
|
+
// 0 up to 360: the neutral angle turns clockwise on screen, Graphviz's counter-clockwise
|
|
1039
|
+
const turned = Number.isFinite(angle) ? ((-angle % 360) + 360) % 360 : 0;
|
|
1040
|
+
n.put("fillGradientAngle", num6(turned) + 0);
|
|
516
1041
|
}
|
|
517
1042
|
|
|
518
1043
|
/**
|
|
@@ -520,9 +1045,104 @@ function readClusterFill(n: NodeRead): void {
|
|
|
520
1045
|
* @param n - the node
|
|
521
1046
|
*/
|
|
522
1047
|
function readNodeFill(n: NodeRead): void {
|
|
523
|
-
const {
|
|
1048
|
+
const { point } = n;
|
|
524
1049
|
const fillcolor = nonEmpty(n.get("fillcolor"));
|
|
525
|
-
|
|
1050
|
+
if (isFilled(n)) {
|
|
1051
|
+
const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
|
|
1052
|
+
readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
|
|
1053
|
+
// stripes on a box and wedges on an ellipse take the color list in place of a gradient
|
|
1054
|
+
const banded =
|
|
1055
|
+
(n.tokens.includes("striped") && BOX_LIKE.has(n.shape)) ||
|
|
1056
|
+
(n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape));
|
|
1057
|
+
if ((n.tokens.includes("filled") || n.tokens.includes("radial")) && !banded) {
|
|
1058
|
+
readFillGradient(n, raw);
|
|
1059
|
+
}
|
|
1060
|
+
} else if (fillcolor !== null) {
|
|
1061
|
+
noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Reads a node's striped or wedged fill (`style=striped` on a box, `style=wedged` on an ellipse) from its color
|
|
1067
|
+
* list, `color;fraction` separated by `:`: a color with no fraction shares what the others leave, the last color takes
|
|
1068
|
+
* what sized colors leave when none is unsized, and the shares stop at the whole
|
|
1069
|
+
* (https://graphviz.org/docs/attr-types/colorList/). Graphviz draws stripes left to right and
|
|
1070
|
+
* wedges counter-clockwise from 3 o'clock, which is the same pie as the wedges in reverse order clockwise from there.
|
|
1071
|
+
* @param n - the node
|
|
1072
|
+
*/
|
|
1073
|
+
function readNodeBands(n: NodeRead): void {
|
|
1074
|
+
// a cluster is a box, so it takes stripes too (and never wedges)
|
|
1075
|
+
const striped = n.tokens.includes("striped") && (n.cx.cluster || BOX_LIKE.has(n.shape));
|
|
1076
|
+
const wedged = !n.cx.cluster && n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape);
|
|
1077
|
+
const list = nonEmpty(n.get("fillcolor")) ?? n.color;
|
|
1078
|
+
if ((!striped && !wedged) || !list?.includes(":")) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const bands = colorBands(list, n.scheme, "fill", n.at(striped ? "stripeColors" : "pieColors"));
|
|
1082
|
+
if (bands.length === 0) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const order = striped ? bands : [...bands].reverse();
|
|
1086
|
+
const prefix = striped ? "stripe" : "pie";
|
|
1087
|
+
n.put(`${prefix}Colors`, order.map((b) => b.color).join(" "));
|
|
1088
|
+
n.put(`${prefix}Sizes`, order.map((b) => String(b.size)).join(" "));
|
|
1089
|
+
if (striped) {
|
|
1090
|
+
n.put("stripeDirection", "right");
|
|
1091
|
+
} else {
|
|
1092
|
+
n.put("pieFitBox", true);
|
|
1093
|
+
n.put("pieRotation", 90);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* The bands of a Graphviz color list (`color;fraction` separated by `:`), in order: a color with no fraction shares
|
|
1099
|
+
* what the others leave, the last color takes what sized colors leave when none is unsized, and the shares stop at
|
|
1100
|
+
* the whole (https://graphviz.org/docs/attr-types/colorList/, emit.c `parseSegs`).
|
|
1101
|
+
* @param list - the color list
|
|
1102
|
+
* @param scheme - the color scheme
|
|
1103
|
+
* @param use - how Graphviz draws the colors
|
|
1104
|
+
* @param site - where it was read
|
|
1105
|
+
* @returns each band's color and share of the whole
|
|
1106
|
+
*/
|
|
1107
|
+
function colorBands(
|
|
1108
|
+
list: string,
|
|
1109
|
+
scheme: string | null,
|
|
1110
|
+
use: "fill" | "edge",
|
|
1111
|
+
site: CanonSite,
|
|
1112
|
+
): { color: string; size: number }[] {
|
|
1113
|
+
const parts = list.split(":").map((part) => {
|
|
1114
|
+
const [color, fraction] = part.split(";");
|
|
1115
|
+
const c = canonColor(color.trim(), "graphviz", site, { scheme, use });
|
|
1116
|
+
const f = fraction === undefined ? Number.NaN : Number(fraction);
|
|
1117
|
+
return { color: c?.color ?? null, fraction: Number.isFinite(f) && f >= 0 ? f : null };
|
|
1118
|
+
});
|
|
1119
|
+
const kept = parts.flatMap((p) => (p.color === null ? [] : [{ color: p.color, fraction: p.fraction }]));
|
|
1120
|
+
const given = kept.reduce((sum, p) => sum + (p.fraction ?? 0), 0);
|
|
1121
|
+
const unsized = kept.filter((p) => p.fraction === null).length;
|
|
1122
|
+
const share = unsized > 0 ? Math.max(0, 1 - given) / unsized : 0;
|
|
1123
|
+
let used = 0;
|
|
1124
|
+
const bands = kept.flatMap((p) => {
|
|
1125
|
+
const size = Math.min(p.fraction ?? share, 1 - used);
|
|
1126
|
+
used += Math.max(size, 0);
|
|
1127
|
+
return size > 0 ? [{ color: p.color, size: num6(size) }] : [];
|
|
1128
|
+
});
|
|
1129
|
+
// every color sized and short of the whole: Graphviz gives what is left to the last one (emit.c parseSegs)
|
|
1130
|
+
const last = bands.at(-1);
|
|
1131
|
+
if (last !== undefined && unsized === 0 && used < 1) {
|
|
1132
|
+
last.size = num6(last.size + 1 - used);
|
|
1133
|
+
}
|
|
1134
|
+
return bands;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Whether Graphviz fills a node: a point always; `filled` and `radial` any shape, `striped` a box and `wedged` an
|
|
1139
|
+
* ellipse only.
|
|
1140
|
+
* @param n - the node
|
|
1141
|
+
* @returns true when its box or shape is filled
|
|
1142
|
+
*/
|
|
1143
|
+
function isFilled(n: NodeRead): boolean {
|
|
1144
|
+
const { shape, point } = n;
|
|
1145
|
+
return (
|
|
526
1146
|
point ||
|
|
527
1147
|
n.tokens.some(
|
|
528
1148
|
(t) =>
|
|
@@ -530,13 +1150,8 @@ function readNodeFill(n: NodeRead): void {
|
|
|
530
1150
|
t === "radial" ||
|
|
531
1151
|
(t === "striped" && BOX_LIKE.has(shape)) ||
|
|
532
1152
|
(t === "wedged" && ELLIPSE_LIKE.has(shape)),
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
|
|
536
|
-
readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
|
|
537
|
-
} else if (fillcolor !== null) {
|
|
538
|
-
noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
|
|
539
|
-
}
|
|
1153
|
+
)
|
|
1154
|
+
);
|
|
540
1155
|
}
|
|
541
1156
|
|
|
542
1157
|
/**
|
|
@@ -551,7 +1166,8 @@ function readNodeOutline(n: NodeRead): void {
|
|
|
551
1166
|
const outline = cx.cluster ? (nonEmpty(get("pencolor")) ?? n.color) : n.color;
|
|
552
1167
|
const alpha = readColor(outline, n.scheme, "line", "borderColor", null, put, at);
|
|
553
1168
|
const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
|
|
554
|
-
|
|
1169
|
+
// peripheries=0 draws no outline, on a node and on a cluster alike
|
|
1170
|
+
if (alpha === 0 || Number(nonEmpty(get("peripheries")) ?? "1") === 0) {
|
|
555
1171
|
put("borderWidth", 0);
|
|
556
1172
|
} else if (width !== null) {
|
|
557
1173
|
put("borderWidth", penWidth(width, get("penwidth") !== null, at("borderWidth")));
|
|
@@ -559,29 +1175,336 @@ function readNodeOutline(n: NodeRead): void {
|
|
|
559
1175
|
put("borderWidth", 2);
|
|
560
1176
|
}
|
|
561
1177
|
}
|
|
562
|
-
if (tokens.includes("dashed")) {
|
|
563
|
-
put("borderStyle", "dashed");
|
|
564
|
-
} else if (tokens.includes("dotted")) {
|
|
565
|
-
put("borderStyle", "dotted");
|
|
1178
|
+
if (tokens.includes("dashed")) {
|
|
1179
|
+
put("borderStyle", "dashed");
|
|
1180
|
+
} else if (tokens.includes("dotted")) {
|
|
1181
|
+
put("borderStyle", "dotted");
|
|
1182
|
+
}
|
|
1183
|
+
if (!cx.cluster) {
|
|
1184
|
+
readNodeRings(n);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** The outlines Graphviz draws round a node of these shapes when `peripheries` is not set. */
|
|
1189
|
+
export const SHAPE_PERIPHERIES: ReadonlyMap<string, number> = new Map([
|
|
1190
|
+
["doublecircle", 2],
|
|
1191
|
+
["doubleoctagon", 2],
|
|
1192
|
+
["tripleoctagon", 3],
|
|
1193
|
+
]);
|
|
1194
|
+
|
|
1195
|
+
/** How far apart Graphviz draws a node's outlines, center to center (`GAP`, lib/common/shapes.c). */
|
|
1196
|
+
export const PERIPHERY_GAP = 4;
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Reads a node's second outline (`peripheries=2`, `doublecircle`): Graphviz draws it `PERIPHERY_GAP` points outside
|
|
1200
|
+
* the first, in the same color, width and dashes. graphty draws one outer ring, so a third and later outline is
|
|
1201
|
+
* noted as not carried.
|
|
1202
|
+
* @param n - the node
|
|
1203
|
+
*/
|
|
1204
|
+
function readNodeRings(n: NodeRead): void {
|
|
1205
|
+
const raw = nonEmpty(n.get("peripheries"));
|
|
1206
|
+
const count = raw === null ? (SHAPE_PERIPHERIES.get(n.shape) ?? 1) : Math.trunc(Number(raw));
|
|
1207
|
+
if (Number.isNaN(count) || count <= 1 || NO_OUTLINE.has(n.shape)) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const c =
|
|
1211
|
+
n.color === null
|
|
1212
|
+
? null
|
|
1213
|
+
: canonColor(n.color, "graphviz", n.at("outerBorderColor"), {
|
|
1214
|
+
scheme: n.scheme,
|
|
1215
|
+
use: "line",
|
|
1216
|
+
});
|
|
1217
|
+
if (c !== null && c.alpha === 0) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const widthRaw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
|
|
1221
|
+
const parsed = widthRaw === null ? Number.NaN : Number(widthRaw);
|
|
1222
|
+
const unset = n.tokens.includes("bold") ? 2 : 1;
|
|
1223
|
+
const pen = Number.isFinite(parsed) ? Math.max(0, parsed) : unset;
|
|
1224
|
+
n.put("outerBorderColor", c?.color ?? "#000000");
|
|
1225
|
+
n.put("outerBorderOpacity", c !== null && c.alpha < 1 ? c.alpha : null);
|
|
1226
|
+
n.put("outerBorderWidth", num6(pen));
|
|
1227
|
+
// the gap between the outline's outer edge and the ring's inner edge
|
|
1228
|
+
n.put("outerBorderOffset", num6(Math.max(0, PERIPHERY_GAP - pen)));
|
|
1229
|
+
if (n.tokens.includes("dashed") || n.tokens.includes("dotted")) {
|
|
1230
|
+
n.put("outerBorderStyle", n.tokens.includes("dashed") ? "dashed" : "dotted");
|
|
1231
|
+
}
|
|
1232
|
+
if (count > 2) {
|
|
1233
|
+
noteAt(n.at("outerBorderWidth"), STYLE_ISSUE.NOT_CARRIED, `peripheries=${String(count)}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is. Graphviz draws
|
|
1239
|
+
* a striped box with square corners whatever `rounded` says.
|
|
1240
|
+
* @param n - the node
|
|
1241
|
+
*/
|
|
1242
|
+
function readNodeShape(n: NodeRead): void {
|
|
1243
|
+
const rounded = n.tokens.includes("rounded") && !n.tokens.includes("striped");
|
|
1244
|
+
const turned = turnedPolygon(n);
|
|
1245
|
+
if (n.cx.cluster) {
|
|
1246
|
+
n.put("shape", rounded ? "round-rectangle" : "rectangle");
|
|
1247
|
+
} else if (n.shape === "polygon") {
|
|
1248
|
+
readPolygon(n, rounded);
|
|
1249
|
+
} else if (turned !== null) {
|
|
1250
|
+
n.put("shape", "polygon");
|
|
1251
|
+
n.put("shapePoints", cornersText(graphvizDrawnCorners(turned)));
|
|
1252
|
+
n.put("shapePointsStretch", false);
|
|
1253
|
+
} else if (NAMED_OUTLINES.has(n.shape)) {
|
|
1254
|
+
n.put("shape", "polygon");
|
|
1255
|
+
n.put("shapePoints", NAMED_OUTLINES.get(n.shape) ?? null);
|
|
1256
|
+
// a polygon shape's corners are where Graphviz draws them, one reaching past its box too
|
|
1257
|
+
n.put("shapePointsStretch", false);
|
|
1258
|
+
} else if (GRAPHVIZ_GLYPH_SHAPES.has(n.shape)) {
|
|
1259
|
+
const size = (name: string): number | null => {
|
|
1260
|
+
const v = inches(n.get(name), n.at(name));
|
|
1261
|
+
return v === null ? null : v * DOT_POINTS_PER_INCH;
|
|
1262
|
+
};
|
|
1263
|
+
putGlyph(
|
|
1264
|
+
n.shape,
|
|
1265
|
+
size("width"),
|
|
1266
|
+
size("height"),
|
|
1267
|
+
n.put,
|
|
1268
|
+
() => {
|
|
1269
|
+
const raw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
|
|
1270
|
+
const pen = raw === null ? Number.NaN : Number(raw);
|
|
1271
|
+
if (Number.isFinite(pen)) {
|
|
1272
|
+
return Math.max(0, pen);
|
|
1273
|
+
}
|
|
1274
|
+
return n.tokens.includes("bold") ? 2 : 1;
|
|
1275
|
+
},
|
|
1276
|
+
() =>
|
|
1277
|
+
n.color === null
|
|
1278
|
+
? null
|
|
1279
|
+
: (canonColor(n.color, "graphviz", n.at("outerBorderColor"), { scheme: n.scheme, use: "line" })
|
|
1280
|
+
?.color ?? null),
|
|
1281
|
+
);
|
|
1282
|
+
} else if (n.shapeRaw !== null) {
|
|
1283
|
+
const shape =
|
|
1284
|
+
rounded && BOX_LIKE.has(n.shape)
|
|
1285
|
+
? "round-rectangle"
|
|
1286
|
+
: canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape"));
|
|
1287
|
+
// a filled shape=none is its box filled, with no outline (its border is 0): a rectangle, not a node with no body
|
|
1288
|
+
n.put("shape", shape === "none" && isFilled(n) ? "rectangle" : shape);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** The polygon shapes `orientation` turns, by their own sides and orientation (`shapes.c`). */
|
|
1293
|
+
const TURNABLE_POLYGONS: ReadonlyMap<string, { readonly sides: number; readonly orientation: number }> = new Map([
|
|
1294
|
+
...[...BOX_LIKE].map((name) => [name, { sides: 4, orientation: 0 }] as const),
|
|
1295
|
+
["diamond", { sides: 4, orientation: 45 }],
|
|
1296
|
+
["hexagon", { sides: 6, orientation: 0 }],
|
|
1297
|
+
["octagon", { sides: 8, orientation: 0 }],
|
|
1298
|
+
]);
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* The polygon a named polygon shape is when its `orientation` turns it off its own axes (a box turned 30 degrees),
|
|
1302
|
+
* or null when it is drawn as its shape: Graphviz adds the attribute to the shape's own orientation.
|
|
1303
|
+
* @param n - the node
|
|
1304
|
+
* @returns the polygon, or null
|
|
1305
|
+
*/
|
|
1306
|
+
function turnedPolygon(n: NodeRead): Parameters<typeof graphvizPolygonCorners>[0] | null {
|
|
1307
|
+
const turn = Number(nonEmpty(n.get("orientation")) ?? "0");
|
|
1308
|
+
const own = TURNABLE_POLYGONS.get(n.shape) ?? GRAPHVIZ_POLYGON_SHAPES.get(n.shape);
|
|
1309
|
+
if (own === undefined || !Number.isFinite(turn) || turn % 360 === 0) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
const polygon = {
|
|
1313
|
+
distortion: 0,
|
|
1314
|
+
skew: 0,
|
|
1315
|
+
...own,
|
|
1316
|
+
orientation: own.orientation + Math.max(-360, turn),
|
|
1317
|
+
};
|
|
1318
|
+
// a quarter turn of a box is a box again
|
|
1319
|
+
return isGraphvizBox(polygon) ? null : polygon;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Reads a `shape=polygon` node: a box when it is one (four sides, no distortion or skew, square to the axes), an
|
|
1324
|
+
* ellipse below three sides, else its corners as Graphviz places them.
|
|
1325
|
+
* @param n - the node
|
|
1326
|
+
* @param rounded - whether its style rounds a box's corners
|
|
1327
|
+
*/
|
|
1328
|
+
function readPolygon(n: NodeRead, rounded: boolean): void {
|
|
1329
|
+
const number = (name: string, fallback: number, least: number): number => {
|
|
1330
|
+
const raw = nonEmpty(n.get(name));
|
|
1331
|
+
const v = raw === null ? Number.NaN : Number(raw);
|
|
1332
|
+
return Number.isFinite(v) ? Math.max(least, v) : fallback;
|
|
1333
|
+
};
|
|
1334
|
+
const polygon = {
|
|
1335
|
+
sides: Math.trunc(number("sides", 4, 0)),
|
|
1336
|
+
distortion: number("distortion", 0, -100),
|
|
1337
|
+
skew: number("skew", 0, -100),
|
|
1338
|
+
orientation: number("orientation", 0, -360),
|
|
1339
|
+
};
|
|
1340
|
+
if (isGraphvizBox(polygon)) {
|
|
1341
|
+
n.put("shape", rounded ? "round-rectangle" : "rectangle");
|
|
1342
|
+
} else if (polygon.sides < 3) {
|
|
1343
|
+
n.put("shape", "ellipse");
|
|
1344
|
+
} else {
|
|
1345
|
+
n.put("shape", "polygon");
|
|
1346
|
+
n.put("shapePoints", cornersText(graphvizDrawnCorners(polygon)));
|
|
1347
|
+
n.put("shapePointsStretch", false);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* A Graphviz polygon's corners where Graphviz draws them, as fractions of its box's half width and half height: it
|
|
1353
|
+
* grows a polygon that falls short of the box along an axis until it reaches the box's sides, and keeps one that
|
|
1354
|
+
* reaches past them (a distortion, a skew, an egg) as it is (shapes.c `poly_init`: `scalex = bb.x / xmax`).
|
|
1355
|
+
* @param polygon - the polygon's attributes
|
|
1356
|
+
* @returns the corners
|
|
1357
|
+
*/
|
|
1358
|
+
function graphvizDrawnCorners(polygon: Parameters<typeof graphvizPolygonCorners>[0]): [number, number][] {
|
|
1359
|
+
const corners = graphvizPolygonCorners(polygon);
|
|
1360
|
+
const grow = (axis: 0 | 1): number => Math.max(1, 1 / Math.max(...corners.map((c) => Math.abs(c[axis]))));
|
|
1361
|
+
const [gx, gy] = [grow(0), grow(1)];
|
|
1362
|
+
return corners.map(([x, y]) => [x * gx, y * gy]);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Corners as a `shapePoints` value.
|
|
1367
|
+
* @param corners - the corners
|
|
1368
|
+
* @returns `x,y x,y ...`
|
|
1369
|
+
*/
|
|
1370
|
+
function cornersText(corners: readonly (readonly [number, number])[]): string {
|
|
1371
|
+
return corners.map(([x, y]) => `${num6(x)},${num6(y)}`).join(" ");
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** The outline of each Graphviz polygon shape graphty draws as a polygon of its corners, by shape name. */
|
|
1375
|
+
const NAMED_OUTLINES: ReadonlyMap<string, string> = new Map(
|
|
1376
|
+
[...GRAPHVIZ_POLYGON_SHAPES].map(([name, polygon]) => [name, cornersText(graphvizDrawnCorners(polygon))] as const),
|
|
1377
|
+
);
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* A Graphviz shape drawn as pieces of its own (`graphvizGlyph`) at a node's size, as the neutral values that draw it:
|
|
1381
|
+
* a polygon of its outline drawn where it says, its further pieces (`shapeParts`) and lines (`shapeLines`), each as
|
|
1382
|
+
* fractions of the node's half width and half height. An `underline` is filled with no line round it, its line alone
|
|
1383
|
+
* drawn in the pen's width.
|
|
1384
|
+
* @param shape - the shape name, lowercase
|
|
1385
|
+
* @param width - the node's width in points, or null for Graphviz's default
|
|
1386
|
+
* @param height - its height, or null
|
|
1387
|
+
* @param put - receives the values
|
|
1388
|
+
* @param pen - the node's pen width, for an outline drawn as a line alone and a ring
|
|
1389
|
+
* @param color - the node's pen color, for a ring
|
|
1390
|
+
*/
|
|
1391
|
+
export function putGlyph(
|
|
1392
|
+
shape: string,
|
|
1393
|
+
width: number | null,
|
|
1394
|
+
height: number | null,
|
|
1395
|
+
put: (property: string, value: StyleValue | null) => void,
|
|
1396
|
+
pen: () => number,
|
|
1397
|
+
color: () => string | null,
|
|
1398
|
+
): void {
|
|
1399
|
+
const text = glyphTexts(shape, width, height);
|
|
1400
|
+
if (text === null) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
put("shape", "polygon");
|
|
1404
|
+
put("shapePoints", text.outline);
|
|
1405
|
+
put("shapePointsStretch", false);
|
|
1406
|
+
put("shapeParts", text.parts);
|
|
1407
|
+
put("shapeLines", text.lines);
|
|
1408
|
+
if (text.unstroked) {
|
|
1409
|
+
put("shapeLineWidth", pen());
|
|
1410
|
+
put("borderWidth", 0);
|
|
1411
|
+
}
|
|
1412
|
+
if (text.ring !== null) {
|
|
1413
|
+
// drawn in the pen, its gap the distance less a pen's width (the outline's half and its own)
|
|
1414
|
+
put("outerBorderColor", color() ?? "#000000");
|
|
1415
|
+
put("outerBorderWidth", num6(pen()));
|
|
1416
|
+
put("outerBorderOffset", num6(Math.max(0, text.ring - pen())));
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* A Graphviz glyph's pieces as `shapePoints`, `shapeParts` and `shapeLines` text at a node's size, or null for a shape
|
|
1422
|
+
* that is not one.
|
|
1423
|
+
* @param shape - the shape name, lowercase
|
|
1424
|
+
* @param width - the node's width in points, or null for Graphviz's default
|
|
1425
|
+
* @param height - its height, or null
|
|
1426
|
+
* @returns the texts (parts and lines null when there are none)
|
|
1427
|
+
*/
|
|
1428
|
+
function glyphTexts(
|
|
1429
|
+
shape: string,
|
|
1430
|
+
width: number | null,
|
|
1431
|
+
height: number | null,
|
|
1432
|
+
): { outline: string; parts: string | null; lines: string | null; unstroked: boolean; ring: number | null } | null {
|
|
1433
|
+
const glyph = graphvizGlyph(shape, width, height);
|
|
1434
|
+
if (glyph === null) {
|
|
1435
|
+
return null;
|
|
566
1436
|
}
|
|
1437
|
+
// fractions of the half width and half height the glyph was laid out at
|
|
1438
|
+
const half = (v: number | null, fallback: number): number => (v !== null && v > 0 ? v : fallback) / 2;
|
|
1439
|
+
const [W, H] = [half(width, 54), half(height, 36)];
|
|
1440
|
+
const list = (points: readonly (readonly [number, number])[]): string =>
|
|
1441
|
+
cornersText(points.map(([x, y]) => [x / W, y / H] as const));
|
|
1442
|
+
const lists = (all: readonly (readonly (readonly [number, number])[])[]): string | null =>
|
|
1443
|
+
all.length === 0 ? null : all.map(list).join("; ");
|
|
1444
|
+
return {
|
|
1445
|
+
outline: list(glyph.outline),
|
|
1446
|
+
parts: lists(glyph.parts),
|
|
1447
|
+
lines: lists(glyph.lines),
|
|
1448
|
+
unstroked: glyph.unstroked === true,
|
|
1449
|
+
ring: glyph.ring ?? null,
|
|
1450
|
+
};
|
|
567
1451
|
}
|
|
568
1452
|
|
|
569
1453
|
/**
|
|
570
|
-
*
|
|
571
|
-
*
|
|
1454
|
+
* The corners of a Graphviz shape graphty draws as a polygon (`house`, `cylinder`, `note` ...), as a `shapePoints`
|
|
1455
|
+
* value, or undefined for a shape graphty draws by name.
|
|
1456
|
+
* @param shape - the shape name, lowercase
|
|
1457
|
+
* @returns the corners, or undefined
|
|
572
1458
|
*/
|
|
573
|
-
function
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1459
|
+
export function graphvizOutline(shape: string): string | undefined {
|
|
1460
|
+
return NAMED_OUTLINES.get(shape);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Whether Graphviz draws a shape with no outline (`plain`, `plaintext`, `none`).
|
|
1465
|
+
* @param shape - the shape name, lowercase
|
|
1466
|
+
* @returns true for a shape with no outline
|
|
1467
|
+
*/
|
|
1468
|
+
export function graphvizHasNoOutline(shape: string): boolean {
|
|
1469
|
+
return NO_OUTLINE.has(shape);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** The shape name of each outline in `NAMED_OUTLINES`, so a written node keeps its Graphviz shape. */
|
|
1473
|
+
const OUTLINE_NAMES: ReadonlyMap<string, string> = new Map([...NAMED_OUTLINES].map(([name, text]) => [text, name]));
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* The Graphviz glyph shape a polygon is at a node's size, its outline, parts and lines alike, so a written node keeps
|
|
1477
|
+
* its shape (a ribosome site and a protease site share their outline and differ in their stem).
|
|
1478
|
+
* @param get - the node's values
|
|
1479
|
+
* @param width - the node's width in points, or null
|
|
1480
|
+
* @param height - its height, or null
|
|
1481
|
+
* @returns the shape name and whether it is filled with no line round it, or undefined
|
|
1482
|
+
*/
|
|
1483
|
+
function glyphOf(
|
|
1484
|
+
get: Get,
|
|
1485
|
+
width: number | null,
|
|
1486
|
+
height: number | null,
|
|
1487
|
+
): { name: string; unstroked: boolean } | undefined {
|
|
1488
|
+
const text = (property: string): string | null => {
|
|
1489
|
+
const v = get(property);
|
|
1490
|
+
return typeof v === "string" ? v.trim() : null;
|
|
1491
|
+
};
|
|
1492
|
+
const outline = text("shapePoints");
|
|
1493
|
+
if (outline === null) {
|
|
1494
|
+
return undefined;
|
|
1495
|
+
}
|
|
1496
|
+
// a glyph with another number of corners cannot match: skip writing its texts, which is most of the work
|
|
1497
|
+
const corners = outline === "" ? 0 : outline.split(/\s+/).length;
|
|
1498
|
+
for (const name of GRAPHVIZ_GLYPH_SHAPES) {
|
|
1499
|
+
if (graphvizGlyph(name, width, height)?.outline.length !== corners) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const g = glyphTexts(name, width, height);
|
|
1503
|
+
if (g !== null && g.outline === outline && g.parts === text("shapeParts") && g.lines === text("shapeLines")) {
|
|
1504
|
+
return { name, unstroked: g.unstroked };
|
|
1505
|
+
}
|
|
584
1506
|
}
|
|
1507
|
+
return undefined;
|
|
585
1508
|
}
|
|
586
1509
|
|
|
587
1510
|
/**
|
|
@@ -602,15 +1525,14 @@ function penWidth(raw: string, attribute: boolean, site: CanonSite): number | nu
|
|
|
602
1525
|
|
|
603
1526
|
/**
|
|
604
1527
|
* Reads a node's width and height: exact where Graphviz draws them exactly (`fixedsize`, an empty label that is not
|
|
605
|
-
* a record's, a point), otherwise minimums it grows to fit the label
|
|
1528
|
+
* a record's, a point), otherwise minimums it grows to fit the label (`readLabelFit`).
|
|
606
1529
|
* @param get - the node's attributes
|
|
607
1530
|
* @param shape - its shape, lowercase
|
|
608
1531
|
* @param point - whether it is a point
|
|
609
|
-
* @param label - the label it shows, or null
|
|
610
1532
|
* @param put - receives the values
|
|
611
1533
|
* @param at - the sites
|
|
612
1534
|
*/
|
|
613
|
-
function readSize(get: Attr, shape: string, point: boolean,
|
|
1535
|
+
function readSize(get: Attr, shape: string, point: boolean, put: Put, at: At): void {
|
|
614
1536
|
let w = inches(get("width"), at("width"));
|
|
615
1537
|
let h = inches(get("height"), at("height"));
|
|
616
1538
|
const same = point || CIRCLE_LIKE.has(shape) || isTrue(get("regular"));
|
|
@@ -636,14 +1558,86 @@ function readSize(get: Attr, shape: string, point: boolean, label: string | null
|
|
|
636
1558
|
} else if (same) {
|
|
637
1559
|
ww = hh = Math.max(w ?? 0, h ?? 0);
|
|
638
1560
|
}
|
|
639
|
-
const exact = point || isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape));
|
|
640
|
-
if (!exact) {
|
|
641
|
-
noteAt(at("width"), STYLE_ISSUE.SIZE_MINIMUM, "grows to fit its label");
|
|
642
|
-
}
|
|
643
1561
|
put("width", ww === null ? null : canonNumber(ww, at("width"), DOT_POINTS_PER_INCH));
|
|
644
1562
|
put("height", hh === null ? null : canonNumber(hh, at("height"), DOT_POINTS_PER_INCH));
|
|
645
1563
|
}
|
|
646
1564
|
|
|
1565
|
+
/**
|
|
1566
|
+
* Reads the graph's `quantum` for a node that grows to hold its label: Graphviz rounds the padded label box up to a
|
|
1567
|
+
* multiple of it before fitting the outline round it (`poly_init`), so the node is at least one quantum across, an
|
|
1568
|
+
* ellipse's box times the square root of 2.
|
|
1569
|
+
* @param n - the node
|
|
1570
|
+
* @param label - its `label` attribute, or null
|
|
1571
|
+
*/
|
|
1572
|
+
function readQuantum(n: NodeRead, label: string | null): void {
|
|
1573
|
+
const { get, put, shape } = n;
|
|
1574
|
+
let scale: number | null = BOX_LIKE.has(shape) ? 1 : null;
|
|
1575
|
+
if (ELLIPSE_LIKE.has(shape)) {
|
|
1576
|
+
scale = Math.SQRT2;
|
|
1577
|
+
}
|
|
1578
|
+
const w = Number(get("width") ?? Number.NaN);
|
|
1579
|
+
const h = Number(get("height") ?? Number.NaN);
|
|
1580
|
+
// ponytail: one quantum is the least size; a label box over one quantum is not rounded up to the next, and only
|
|
1581
|
+
// boxes and ellipses with their size given are read (other polygons fit their box by factors of their own)
|
|
1582
|
+
if (n.cx.quantum === 0 || scale === null || label === "" || isTrue(get("fixedsize")) || !(w >= 0 && h >= 0)) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const least = n.cx.quantum * scale;
|
|
1586
|
+
put("width", num6(Math.max(w * DOT_POINTS_PER_INCH, least)));
|
|
1587
|
+
put("height", num6(Math.max(h * DOT_POINTS_PER_INCH, least)));
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/** Graphviz's padding around a node's label when it has no `margin`: 8 points left and right, 4 above and below. */
|
|
1591
|
+
const LABEL_PAD = { x: 8, y: 4 } as const;
|
|
1592
|
+
|
|
1593
|
+
/** Graphviz's line height, as a multiple of the font size (`LINESPACING`, lib/common/const.h). */
|
|
1594
|
+
const LINE_SPACING = 1.2;
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Reads how a node that is not drawn at a fixed size grows to hold its label (`poly_init`, lib/common/shapes.c):
|
|
1598
|
+
* its padded label box fits inside its outline, the `margin` (else 8 points by 4) on each side, and a circle, a
|
|
1599
|
+
* square or a `regular` node stays as wide as it is tall. The neutral label box is one font size per line, so the
|
|
1600
|
+
* rest of Graphviz's line height (1.2 font sizes) goes into the vertical padding.
|
|
1601
|
+
* @param get - the node's attributes
|
|
1602
|
+
* @param shape - its shape, lowercase
|
|
1603
|
+
* @param label - its `label` attribute, or null
|
|
1604
|
+
* @param shown - the label it shows
|
|
1605
|
+
* @param put - receives the values
|
|
1606
|
+
*/
|
|
1607
|
+
function readLabelFit(get: Attr, shape: string, label: string | null, shown: string, put: Put): void {
|
|
1608
|
+
if (isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape))) {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const lines = Math.max(1, shown.replace(/\n$/, "").split("\n").length);
|
|
1612
|
+
const size = Number(get("fontsize") ?? 14);
|
|
1613
|
+
const fontSize = Number.isFinite(size) && size > 0 ? size : 14;
|
|
1614
|
+
const margin = (get("margin") ?? "")
|
|
1615
|
+
.split(",")
|
|
1616
|
+
.filter((text) => text.trim() !== "")
|
|
1617
|
+
.map(Number)
|
|
1618
|
+
.filter(isLength);
|
|
1619
|
+
// plain is plaintext with a margin of 0
|
|
1620
|
+
const pad = shape === "plain" ? { x: 0, y: 0 } : LABEL_PAD;
|
|
1621
|
+
const padX = margin.length > 0 ? margin[0] * DOT_POINTS_PER_INCH : pad.x;
|
|
1622
|
+
const padY = margin.length > 0 ? (margin[1] ?? margin[0]) * DOT_POINTS_PER_INCH : pad.y;
|
|
1623
|
+
// a decorated box holds its label as a box does, exactly
|
|
1624
|
+
put("labelFit", GRAPHVIZ_DECORATED_BOXES.has(shape) ? "bounds" : "inside");
|
|
1625
|
+
put("labelPaddingX", num6(padX));
|
|
1626
|
+
put("labelPaddingY", num6(padY + ((LINE_SPACING - 1) * fontSize * lines) / 2));
|
|
1627
|
+
if (CIRCLE_LIKE.has(shape) || isTrue(get("regular"))) {
|
|
1628
|
+
put("labelFitSquare", true);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Whether a `margin` part is a usable length: a number at least 0.
|
|
1634
|
+
* @param value - the part
|
|
1635
|
+
* @returns whether it is
|
|
1636
|
+
*/
|
|
1637
|
+
function isLength(value: number): boolean {
|
|
1638
|
+
return Number.isFinite(value) && value >= 0;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
647
1641
|
/**
|
|
648
1642
|
* Reads one edge the way Graphviz draws it.
|
|
649
1643
|
* @param get - its attributes
|
|
@@ -656,6 +1650,7 @@ function readEdge(get: Attr, cx: EdgeContext, put: Put, at: At): void {
|
|
|
656
1650
|
const scheme = get("colorscheme");
|
|
657
1651
|
readCommon(get, tokens, scheme, cx.names, put, at);
|
|
658
1652
|
readColor(nonEmpty(get("color")), scheme, "edge", "color", "opacity", put, at);
|
|
1653
|
+
readEdgeSegments(nonEmpty(get("color")), scheme, put, at);
|
|
659
1654
|
readEdgeLine(get, tokens, put, at);
|
|
660
1655
|
readEdgeArrows(get, cx, put, at);
|
|
661
1656
|
for (const [attribute, property] of [
|
|
@@ -663,16 +1658,216 @@ function readEdge(get: Attr, cx: EdgeContext, put: Put, at: At): void {
|
|
|
663
1658
|
["headlabel", "targetLabel"],
|
|
664
1659
|
["taillabel", "sourceLabel"],
|
|
665
1660
|
] as const) {
|
|
666
|
-
|
|
1661
|
+
// an edge's xlabel is drawn beside its middle, as its label is: it is the label when there is no other
|
|
1662
|
+
const raw = attribute === LABEL_ATTRIBUTE ? (get(attribute) ?? nonEmpty(get("xlabel"))) : get(attribute);
|
|
667
1663
|
if (raw !== null) {
|
|
668
1664
|
put(property, labelText(raw, cx.names, cx.html, false, at(property)));
|
|
1665
|
+
if (property === "label") {
|
|
1666
|
+
readLabelAlign(raw, cx.names, cx.html, false, put);
|
|
1667
|
+
readHtmlRuns(raw, cx.html, scheme, put, at);
|
|
1668
|
+
}
|
|
669
1669
|
}
|
|
670
1670
|
}
|
|
671
|
-
if (nonEmpty(get("xlabel")) !== null) {
|
|
1671
|
+
if (get(LABEL_ATTRIBUTE) !== null && nonEmpty(get("xlabel")) !== null) {
|
|
672
1672
|
noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
|
|
673
1673
|
}
|
|
674
1674
|
}
|
|
675
1675
|
|
|
1676
|
+
/**
|
|
1677
|
+
* Reads an edge's colored segments: a color list with a `;fraction` in it (`red;0.3:blue`) draws the line in one piece
|
|
1678
|
+
* per color along its length, which is a linear gradient with a hard switch at each band's end. A list with no
|
|
1679
|
+
* fraction draws parallel lines, which graphty does not draw (the line is in the first color).
|
|
1680
|
+
* @param list - the edge's `color`
|
|
1681
|
+
* @param scheme - its color scheme
|
|
1682
|
+
* @param put - receives the values
|
|
1683
|
+
* @param at - the sites
|
|
1684
|
+
*/
|
|
1685
|
+
function readEdgeSegments(list: string | null, scheme: string | null, put: Put, at: At): void {
|
|
1686
|
+
if (list === null || !list.includes(":") || !list.includes(";")) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const bands = colorBands(list, scheme, "edge", at("gradientColors"));
|
|
1690
|
+
if (bands.length < 2) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const colors: string[] = [];
|
|
1694
|
+
const stops: number[] = [];
|
|
1695
|
+
let start = 0;
|
|
1696
|
+
for (const band of bands) {
|
|
1697
|
+
colors.push(band.color, band.color);
|
|
1698
|
+
stops.push(num6(start), num6(Math.min(1, start + band.size)));
|
|
1699
|
+
start += band.size;
|
|
1700
|
+
}
|
|
1701
|
+
put("gradientKind", "linear");
|
|
1702
|
+
put("gradientColors", colors.join(" "));
|
|
1703
|
+
put("gradientStops", stops.join(" "));
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* What an edge's label places are read against: the nodes' centers and sizes.
|
|
1708
|
+
* @param snapshot - the graph
|
|
1709
|
+
* @returns the context
|
|
1710
|
+
*/
|
|
1711
|
+
function labelPlaceContext(snapshot: GraphSnapshot): LabelPlaceContext {
|
|
1712
|
+
const column = snapshot.nodes.byRole("position");
|
|
1713
|
+
return {
|
|
1714
|
+
center: (i) => {
|
|
1715
|
+
const v = column?.isSet(i) === true ? (column.value(i) as ArrayLike<number>) : null;
|
|
1716
|
+
return v === null ? null : [v[0], v[1]];
|
|
1717
|
+
},
|
|
1718
|
+
node: (i, name) => nonEmpty(cellText(snapshot.nodes, name, i)),
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* The route Graphviz laid an edge along, from its `pos`: `[e,x,y] [s,x,y]` arrow tips, then the B-spline's `3n + 1`
|
|
1724
|
+
* control points (y up), flattened into bends between the two nodes' centers. Null for an edge Graphviz drew
|
|
1725
|
+
* straight, a `pos` that is not a spline, or nodes without positions.
|
|
1726
|
+
* @param snapshot - the graph
|
|
1727
|
+
* @param e - the edge
|
|
1728
|
+
* @param pos - its `pos` text
|
|
1729
|
+
* @returns the `bends` value, or null
|
|
1730
|
+
*/
|
|
1731
|
+
function splineBends(snapshot: GraphSnapshot, e: number, pos: string | null): string | null {
|
|
1732
|
+
if (pos === null) {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
const column = snapshot.nodes.byRole("position");
|
|
1736
|
+
const center = (i: number): BendPoint | null => {
|
|
1737
|
+
const v = column?.isSet(i) === true ? (column.value(i) as ArrayLike<number>) : null;
|
|
1738
|
+
return v === null ? null : [v[0], v[1]];
|
|
1739
|
+
};
|
|
1740
|
+
const [source, target] = [center(snapshot.edgeSource(e)), center(snapshot.edgeTarget(e))];
|
|
1741
|
+
if (source === null || target === null) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
// the arrow tips of the first spline's start and the last spline's end
|
|
1745
|
+
const tip = (spline: string | undefined, flag: "s" | "e"): BendPoint | null => {
|
|
1746
|
+
const token = (spline ?? "")
|
|
1747
|
+
.trim()
|
|
1748
|
+
.split(/\s+/)
|
|
1749
|
+
.find((t) => t.startsWith(`${flag},`));
|
|
1750
|
+
const [x, y] = (token ?? "").slice(2).split(",").map(Number);
|
|
1751
|
+
return token !== undefined && Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
|
|
1752
|
+
};
|
|
1753
|
+
const parts = pos.split(";");
|
|
1754
|
+
const tips = { start: tip(parts[0], "s"), end: tip(parts.at(-1), "e") };
|
|
1755
|
+
// ponytail: the splines of a concentrated edge (";"-separated) are joined end to end
|
|
1756
|
+
const splines = parts.map((spline) =>
|
|
1757
|
+
spline
|
|
1758
|
+
.trim()
|
|
1759
|
+
.split(/\s+/)
|
|
1760
|
+
.filter((t) => !/^[es],/.test(t))
|
|
1761
|
+
.map((t): BendPoint => {
|
|
1762
|
+
const [x, y] = t.split(",").map(Number);
|
|
1763
|
+
return [x, y];
|
|
1764
|
+
}),
|
|
1765
|
+
);
|
|
1766
|
+
return graphvizRouteBends(source, target, splines, tips);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/** How far apart Graphviz's neato lays the loops on one node, across (x) and along (y) its side, in points. */
|
|
1770
|
+
const GRAPHVIZ_LOOP_STEP = { x: 18, y: 9 } as const;
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* The self-loops Graphviz lays itself, with no `pos` of their own and no ports: neato's `selfRight` (splines.c), on
|
|
1774
|
+
* the node's right side, the k-th of a node's n loops two cubic curves from its center out to `rw + 18k` points right
|
|
1775
|
+
* of it and back, `9k / n` points above and then below its center line (measured on Graphviz 14.1.2's and 2.43's
|
|
1776
|
+
* drawings of nodes from 0.33 to 1 inch).
|
|
1777
|
+
* @param snapshot - the graph
|
|
1778
|
+
* @returns the bends of an edge that is such a loop, or null
|
|
1779
|
+
*/
|
|
1780
|
+
function graphvizLoops(snapshot: GraphSnapshot): { bendsOf: (e: number, get: Attr) => string | null } {
|
|
1781
|
+
const portless = (get: Attr): boolean =>
|
|
1782
|
+
nonEmpty(get("tailport")) === null &&
|
|
1783
|
+
nonEmpty(get("headport")) === null &&
|
|
1784
|
+
nonEmpty(get(POS_ATTRIBUTE)) === null;
|
|
1785
|
+
const onNode = new Map<number, number[]>();
|
|
1786
|
+
// ponytail: ortho routes a loop round the node's corners and none draws no edge; both keep the element's loop.
|
|
1787
|
+
const splines = (nonEmpty(cellText(snapshot.graph, "splines", 0)) ?? "").toLowerCase();
|
|
1788
|
+
// a file graphty wrote keeps its loops' look in its own block
|
|
1789
|
+
const laid =
|
|
1790
|
+
splines !== "ortho" && splines !== "none" && nonEmpty(cellText(snapshot.graph, STYLE_BLOCK_NAME, 0)) === null;
|
|
1791
|
+
for (let e = 0; e < snapshot.edgeCount && laid; e++) {
|
|
1792
|
+
const n = snapshot.edgeSource(e);
|
|
1793
|
+
if (n === snapshot.edgeTarget(e) && portless((name) => cellText(snapshot.edges, name, e))) {
|
|
1794
|
+
onNode.set(n, [...(onNode.get(n) ?? []), e]);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
const inches = (n: number, name: string, fallback: number): number => {
|
|
1798
|
+
const v = Number(nonEmpty(cellText(snapshot.nodes, name, n)) ?? Number.NaN);
|
|
1799
|
+
return (Number.isFinite(v) && v > 0 ? v : fallback) * DOT_POINTS_PER_INCH;
|
|
1800
|
+
};
|
|
1801
|
+
return {
|
|
1802
|
+
bendsOf: (e, get) => {
|
|
1803
|
+
const n = snapshot.edgeSource(e);
|
|
1804
|
+
const list = onNode.get(n);
|
|
1805
|
+
if (n !== snapshot.edgeTarget(e) || list === undefined || !portless(get)) {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
// ponytail: a node Graphviz grows to fit its label is taken at its stated width and height
|
|
1809
|
+
const [w, h] = [inches(n, "width", 0.75), inches(n, "height", 0.5)];
|
|
1810
|
+
const k = list.indexOf(e) + 1;
|
|
1811
|
+
const dx = w / 2 + GRAPHVIZ_LOOP_STEP.x * k;
|
|
1812
|
+
const dy = (GRAPHVIZ_LOOP_STEP.y * k) / list.length;
|
|
1813
|
+
const curve = flattenCubic([
|
|
1814
|
+
[0, 0],
|
|
1815
|
+
[dx / 3, dy],
|
|
1816
|
+
[dx, dy],
|
|
1817
|
+
[dx, 0],
|
|
1818
|
+
[dx, -dy],
|
|
1819
|
+
[dx / 3, -dy],
|
|
1820
|
+
[0, 0],
|
|
1821
|
+
]);
|
|
1822
|
+
return curve === null ? null : canonNodeOffsets([0, 0], [w, h], curve.slice(1, -1), false);
|
|
1823
|
+
},
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* The bends of a route Graphviz laid: B-splines of `3n + 1` control points each (y up), flattened into bends between
|
|
1829
|
+
* the two nodes' centers. Null for a route on the line between the centers, which is the straight edge it looks like,
|
|
1830
|
+
* or for control points that are not a spline. Also reads the `Line` of a GML file Graphviz's gv2gml wrote.
|
|
1831
|
+
* @param source - the source node's center
|
|
1832
|
+
* @param target - the target node's center
|
|
1833
|
+
* @param splines - the control points of each spline, joined end to end
|
|
1834
|
+
* @param tips - the arrow tips `pos` stores (`s,x,y` and `e,x,y`): the line runs on to each, along its arrow
|
|
1835
|
+
* @param tips.start - the tail arrow's tip, or null
|
|
1836
|
+
* @param tips.end - the head arrow's tip, or null
|
|
1837
|
+
* @returns the `bends` value, or null
|
|
1838
|
+
*/
|
|
1839
|
+
export function graphvizRouteBends(
|
|
1840
|
+
source: BendPoint,
|
|
1841
|
+
target: BendPoint,
|
|
1842
|
+
splines: readonly (readonly BendPoint[])[],
|
|
1843
|
+
tips?: { readonly start: BendPoint | null; readonly end: BendPoint | null },
|
|
1844
|
+
): string | null {
|
|
1845
|
+
const points: BendPoint[] = [];
|
|
1846
|
+
for (const controls of splines) {
|
|
1847
|
+
const flat = flattenCubic(controls);
|
|
1848
|
+
if (flat === null || flat.some(([x, y]) => !Number.isFinite(x) || !Number.isFinite(y))) {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
points.push(...flat);
|
|
1852
|
+
}
|
|
1853
|
+
// a tip where the spline already ends adds no point
|
|
1854
|
+
const apart = (a: BendPoint, b: BendPoint | undefined): boolean =>
|
|
1855
|
+
b === undefined || Math.hypot(a[0] - b[0], a[1] - b[1]) > 1e-6;
|
|
1856
|
+
const start = tips?.start ?? null;
|
|
1857
|
+
const end = tips?.end ?? null;
|
|
1858
|
+
if (start !== null && apart(start, points[0])) {
|
|
1859
|
+
points.unshift(start);
|
|
1860
|
+
}
|
|
1861
|
+
if (end !== null && apart(end, points.at(-1))) {
|
|
1862
|
+
points.push(end);
|
|
1863
|
+
}
|
|
1864
|
+
const [dx, dy] = [target[0] - source[0], target[1] - source[1]];
|
|
1865
|
+
const length = Math.hypot(dx, dy);
|
|
1866
|
+
const off = Math.max(...points.map(([x, y]) => Math.abs((x - source[0]) * dy - (y - source[1]) * dx)));
|
|
1867
|
+
// a route on the line between the centers is the straight edge it looks like
|
|
1868
|
+
return length > 0 && off / (length * length) > 1e-3 ? canonBends(source, target, points, false) : null;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
676
1871
|
/**
|
|
677
1872
|
* Reads an edge's line: its width and dashes.
|
|
678
1873
|
* @param get - its attributes
|
|
@@ -712,15 +1907,97 @@ function readEdgeArrows(get: Attr, cx: EdgeContext, put: Put, at: At): void {
|
|
|
712
1907
|
// with none of these the tool-default rule draws the arrow of the graph's kind
|
|
713
1908
|
if (dirRaw !== null || head !== null || tail !== null) {
|
|
714
1909
|
const dir = (dirRaw ?? (cx.directed ? "forward" : "none")).trim().toLowerCase();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
put("targetArrow", forward ? canonEnum("arrow", head ?? "normal", "graphviz", at("targetArrow")) : "none");
|
|
718
|
-
put("sourceArrow", back ? canonEnum("arrow", tail ?? "normal", "graphviz", at("sourceArrow")) : "none");
|
|
1910
|
+
readEdgeArrow("target", head, dir === "forward" || dir === "both", put, at);
|
|
1911
|
+
readEdgeArrow("source", tail, dir === "back" || dir === "both", put, at);
|
|
719
1912
|
}
|
|
720
1913
|
const size = get("arrowsize");
|
|
721
1914
|
if (size !== null) {
|
|
722
1915
|
put("arrowSize", canonNumber(size, at("arrowSize")));
|
|
723
1916
|
}
|
|
1917
|
+
// an edge's `fillcolor` fills its arrowheads (their outline stays the edge's color)
|
|
1918
|
+
const fill = nonEmpty(get("fillcolor"));
|
|
1919
|
+
for (const property of ["targetArrowColor", "sourceArrowColor"]) {
|
|
1920
|
+
readColor(fill, get("colorscheme"), "fill", property, null, put, at);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Reads one end's arrow: its shape, or none when the edge's direction draws no arrow there, and the side it keeps.
|
|
1926
|
+
* @param end - which end
|
|
1927
|
+
* @param raw - its `arrowhead` or `arrowtail`, or null
|
|
1928
|
+
* @param on - whether the edge's direction draws an arrow there
|
|
1929
|
+
* @param put - receives the values
|
|
1930
|
+
* @param at - the sites
|
|
1931
|
+
*/
|
|
1932
|
+
function readEdgeArrow(end: "target" | "source", raw: string | null, on: boolean, put: Put, at: At): void {
|
|
1933
|
+
const arrow = on ? graphvizArrowValue(raw ?? "normal", at(`${end}Arrow`)) : null;
|
|
1934
|
+
put(`${end}Arrow`, arrow === null ? "none" : arrow.value);
|
|
1935
|
+
if (arrow?.side !== null && arrow?.side !== undefined) {
|
|
1936
|
+
put(`${end}ArrowSide`, arrow.side);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/** The sides an `l` or `r` arrow modifier keeps, by their first letters. */
|
|
1941
|
+
const ARROW_SIDES = ["left", "right"] as const;
|
|
1942
|
+
|
|
1943
|
+
/** The open forms of Graphviz arrows that a neutral shape draws; the others are drawn filled. */
|
|
1944
|
+
const GRAPHVIZ_OPEN_ARROWS: ReadonlySet<string> = new Set(["normal", "diamond", "dot", "box", "inv"]);
|
|
1945
|
+
|
|
1946
|
+
/** The open Graphviz arrows the neutral vocabulary has no name for, by the value graphty draws them as. */
|
|
1947
|
+
const GRAPHVIZ_OTHER_ARROWS: ReadonlyMap<string, string> = new Map([
|
|
1948
|
+
["obox", "other:open-square"],
|
|
1949
|
+
["oinv", "other:open-inverted"],
|
|
1950
|
+
]);
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* The neutral arrow value Graphviz draws for an arrow name (`onormal`, `lbox`, `invempty`), with an open shape the
|
|
1954
|
+
* neutral vocabulary has no name for (`obox`, `oinv`) as the value graphty draws it as, and the side an `l` or `r`
|
|
1955
|
+
* keeps. Read so by the DOT reader and by the GML reader for gv2gml's `targetArrow` / `sourceArrow`.
|
|
1956
|
+
* @param raw - the arrow name
|
|
1957
|
+
* @param site - where it was read
|
|
1958
|
+
* @returns the value and its side (null for the whole shape)
|
|
1959
|
+
*/
|
|
1960
|
+
export function graphvizArrowValue(
|
|
1961
|
+
raw: string,
|
|
1962
|
+
site: CanonSite,
|
|
1963
|
+
): { readonly value: string | null; readonly side: "left" | "right" | null } {
|
|
1964
|
+
const first = graphvizArrow(raw);
|
|
1965
|
+
const other = first === null ? undefined : GRAPHVIZ_OTHER_ARROWS.get(first.name);
|
|
1966
|
+
return {
|
|
1967
|
+
value: other ?? canonEnum("arrow", first?.name ?? raw, "graphviz", site),
|
|
1968
|
+
side: first?.side ?? null,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* The arrow Graphviz draws at an edge's very end, from an arrow name of up to four shapes (`dotodot`,
|
|
1974
|
+
* `lteeoldiamond`): the first shape that is not `none`, with its `o` (open) kept where a neutral shape draws it, and
|
|
1975
|
+
* the side an `l` or `r` keeps alone ("clip the shape, leaving only the part to the left of the edge", looking along
|
|
1976
|
+
* the edge toward the arrow's tip).
|
|
1977
|
+
* @param raw - the arrow name
|
|
1978
|
+
* @returns the shape's own name and its side (null for the whole shape), or null for a name outside the grammar (a
|
|
1979
|
+
* synonym such as `empty`)
|
|
1980
|
+
*/
|
|
1981
|
+
function graphvizArrow(raw: string): { readonly name: string; readonly side: "left" | "right" | null } | null {
|
|
1982
|
+
let rest = raw.trim().toLowerCase();
|
|
1983
|
+
// the one synonym the grammar would misread (as `inv`): arrows.c's `invempty` is an open `inv`
|
|
1984
|
+
if (rest === "invempty") {
|
|
1985
|
+
rest = "oinv";
|
|
1986
|
+
}
|
|
1987
|
+
while (rest.length > 0) {
|
|
1988
|
+
const m = /^([olr]{0,3})(normal|crow|tee|box|diamond|dot|none|inv|vee|icurve|curve)/.exec(rest);
|
|
1989
|
+
if (m === null) {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
rest = rest.slice(m[0].length);
|
|
1993
|
+
const [, mods, shape] = m;
|
|
1994
|
+
if (shape !== "none") {
|
|
1995
|
+
const side = ARROW_SIDES.find((s) => mods.includes(s[0])) ?? null;
|
|
1996
|
+
const open = mods.includes("o") && GRAPHVIZ_OPEN_ARROWS.has(shape) ? "o" : "";
|
|
1997
|
+
return { name: open + shape, side };
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return null;
|
|
724
2001
|
}
|
|
725
2002
|
|
|
726
2003
|
/**
|
|
@@ -807,12 +2084,16 @@ function contexts(
|
|
|
807
2084
|
} {
|
|
808
2085
|
const html = htmlStrings(snapshot);
|
|
809
2086
|
const graph = snapshot.meta.name ?? "";
|
|
2087
|
+
const rankdir = (cellText(snapshot.graph, "rankdir", 0) ?? "").trim().toUpperCase();
|
|
2088
|
+
const flip = rankdir === "LR" || rankdir === "RL";
|
|
2089
|
+
const q = Number(cellText(snapshot.graph, "quantum", 0) ?? Number.NaN);
|
|
2090
|
+
const quantum = Number.isFinite(q) && q > 0 ? q * DOT_POINTS_PER_INCH : 0;
|
|
810
2091
|
const own = directedEdges(snapshot);
|
|
811
2092
|
// a graph mixing both directions is written with one, every edge then drawn as that one
|
|
812
2093
|
const directed = written === null || own.every((d) => d === snapshot.directed) ? own : own.map(() => written);
|
|
813
2094
|
const name = (i: number): string => String(snapshot.ids.idOf(i));
|
|
814
2095
|
return {
|
|
815
|
-
node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html }),
|
|
2096
|
+
node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html, flip, quantum }),
|
|
816
2097
|
edge: (e) => {
|
|
817
2098
|
const tail = name(snapshot.edgeSource(e));
|
|
818
2099
|
const head = name(snapshot.edgeTarget(e));
|
|
@@ -854,6 +2135,34 @@ function styleColumns(table: GraphSnapshot["nodes"], attributes: readonly string
|
|
|
854
2135
|
return [...table.names()].filter((c) => own.has(c) || c.startsWith("graphty.style."));
|
|
855
2136
|
}
|
|
856
2137
|
|
|
2138
|
+
/**
|
|
2139
|
+
* The graph's own styles: its `bgcolor` as its background.
|
|
2140
|
+
* @param snapshot - the graph
|
|
2141
|
+
* @param notes - where a bad color is noted
|
|
2142
|
+
* @returns the graph's values
|
|
2143
|
+
*/
|
|
2144
|
+
function graphBackground(snapshot: GraphSnapshot, notes: StyleNotes): Record<string, StyleValue> {
|
|
2145
|
+
const graph: Record<string, StyleValue> = {};
|
|
2146
|
+
const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
|
|
2147
|
+
if (!snapshot.graph.has("bgcolor") || bg === null) {
|
|
2148
|
+
return graph;
|
|
2149
|
+
}
|
|
2150
|
+
readColor(
|
|
2151
|
+
bg,
|
|
2152
|
+
cellText(snapshot.graph, "colorscheme", 0),
|
|
2153
|
+
"fill",
|
|
2154
|
+
"background",
|
|
2155
|
+
null,
|
|
2156
|
+
(p, v) => {
|
|
2157
|
+
if (v !== null) {
|
|
2158
|
+
graph[p] = v;
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
(property) => ({ notes, target: "graph", property }),
|
|
2162
|
+
);
|
|
2163
|
+
return graph;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
857
2166
|
/**
|
|
858
2167
|
* The styles of a DOT file, read the way Graphviz draws it, with Graphviz's defaults beneath: an unfilled black
|
|
859
2168
|
* ellipse 54 x 36 labeled with its name, black edges with a target arrow in a directed graph. A file graphty wrote
|
|
@@ -894,6 +2203,11 @@ export function dotStyles(snapshot: GraphSnapshot, context: StyleReadContext): I
|
|
|
894
2203
|
}
|
|
895
2204
|
}
|
|
896
2205
|
const edge = valueColumns(snapshot.edgeCount);
|
|
2206
|
+
const loops = graphvizLoops(snapshot);
|
|
2207
|
+
// `splines=none` and `splines=""` draw no edges at all (https://graphviz.org/docs/attrs/splines/)
|
|
2208
|
+
const splines = cellText(snapshot.graph, "splines", 0)?.trim().toLowerCase();
|
|
2209
|
+
const noEdges = splines === "" || splines === "none";
|
|
2210
|
+
const places = labelPlaceContext(snapshot);
|
|
897
2211
|
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
898
2212
|
context.budget.spend("edge", EDGE_ATTRIBUTES.length);
|
|
899
2213
|
const get: Attr = (name) => cellText(snapshot.edges, name, e);
|
|
@@ -906,24 +2220,18 @@ export function dotStyles(snapshot: GraphSnapshot, context: StyleReadContext): I
|
|
|
906
2220
|
},
|
|
907
2221
|
at,
|
|
908
2222
|
);
|
|
2223
|
+
if (noEdges) {
|
|
2224
|
+
edge.put(e, "visible", false);
|
|
2225
|
+
}
|
|
2226
|
+
const bends = splineBends(snapshot, e, get(POS_ATTRIBUTE)) ?? loops.bendsOf(e, get);
|
|
2227
|
+
if (bends !== null) {
|
|
2228
|
+
edge.put(e, "bends", bends);
|
|
2229
|
+
}
|
|
2230
|
+
readLabelPlaces(snapshot, places, e, get, (p, v) => {
|
|
2231
|
+
edge.put(e, p, v);
|
|
2232
|
+
});
|
|
909
2233
|
}
|
|
910
|
-
const graph
|
|
911
|
-
const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
|
|
912
|
-
if (snapshot.graph.has("bgcolor") && bg !== null) {
|
|
913
|
-
readColor(
|
|
914
|
-
bg,
|
|
915
|
-
cellText(snapshot.graph, "colorscheme", 0),
|
|
916
|
-
"fill",
|
|
917
|
-
"background",
|
|
918
|
-
null,
|
|
919
|
-
(p, v) => {
|
|
920
|
-
if (v !== null) {
|
|
921
|
-
graph[p] = v;
|
|
922
|
-
}
|
|
923
|
-
},
|
|
924
|
-
(property) => ({ notes, target: "graph", property }),
|
|
925
|
-
);
|
|
926
|
-
}
|
|
2234
|
+
const graph = graphBackground(snapshot, notes);
|
|
927
2235
|
const blockText = cellText(snapshot.graph, STYLE_BLOCK_NAME, 0);
|
|
928
2236
|
if (blockText !== null) {
|
|
929
2237
|
exactFromBlock(snapshot, blockText, { node: node.values, edge: edge.values }, cx);
|
|
@@ -1017,8 +2325,9 @@ const ARROWS: ReadonlyMap<string, string> = new Map([
|
|
|
1017
2325
|
["circle-open", "odot"],
|
|
1018
2326
|
["square", "box"],
|
|
1019
2327
|
["crow", "crow"],
|
|
1020
|
-
["half", "lnormal"],
|
|
1021
2328
|
["inverted", "inv"],
|
|
2329
|
+
["other:open-square", "obox"],
|
|
2330
|
+
["other:open-inverted", "oinv"],
|
|
1022
2331
|
]);
|
|
1023
2332
|
|
|
1024
2333
|
/** Line and border styles Graphviz draws exactly; the others are written as the nearest. */
|
|
@@ -1046,7 +2355,10 @@ function graphvizDefaults(target: StyleTarget): ReadonlyMap<string, Written> {
|
|
|
1046
2355
|
};
|
|
1047
2356
|
add("fontcolor", "labelColor");
|
|
1048
2357
|
add("fontsize", "labelSize");
|
|
1049
|
-
|
|
2358
|
+
const font = t.labelFont?.value;
|
|
2359
|
+
if (typeof font === "string") {
|
|
2360
|
+
out.set("fontname", graphvizFontName(font, false, false));
|
|
2361
|
+
}
|
|
1050
2362
|
if (target === "node") {
|
|
1051
2363
|
add("color", "borderColor");
|
|
1052
2364
|
add("pencolor", "borderColor");
|
|
@@ -1100,12 +2412,16 @@ function gvColor(color: string, opacity: StyleValue | null): string {
|
|
|
1100
2412
|
}
|
|
1101
2413
|
|
|
1102
2414
|
/**
|
|
1103
|
-
* A label's text as a DOT label: backslashes
|
|
2415
|
+
* A label's text as a DOT label: backslashes, line breaks and `&` escaped, so Graphviz's escape and entity passes
|
|
2416
|
+
* give the text back.
|
|
1104
2417
|
* @param text - the text
|
|
1105
2418
|
* @returns the label
|
|
1106
2419
|
*/
|
|
1107
2420
|
function escapeLabel(text: string): string {
|
|
1108
|
-
return text
|
|
2421
|
+
return text
|
|
2422
|
+
.replaceAll("&", "&")
|
|
2423
|
+
.replaceAll("\\", String.raw`\\`)
|
|
2424
|
+
.replaceAll("\n", String.raw`\n`);
|
|
1109
2425
|
}
|
|
1110
2426
|
|
|
1111
2427
|
/**
|
|
@@ -1138,10 +2454,11 @@ function spelled(
|
|
|
1138
2454
|
/**
|
|
1139
2455
|
* The font, tooltip and visibility attributes every element shares.
|
|
1140
2456
|
* @param get - the element's values
|
|
2457
|
+
* @param approximate - records approximations
|
|
1141
2458
|
* @param out - the attributes, added to
|
|
1142
2459
|
* @param tokens - the style tokens, added to
|
|
1143
2460
|
*/
|
|
1144
|
-
function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): void {
|
|
2461
|
+
function writeCommon(get: Get, approximate: Approximate, out: Map<string, Written>, tokens: string[]): void {
|
|
1145
2462
|
const color = get("labelColor");
|
|
1146
2463
|
if (typeof color === "string") {
|
|
1147
2464
|
out.set("fontcolor", color);
|
|
@@ -1151,8 +2468,20 @@ function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): voi
|
|
|
1151
2468
|
out.set("fontsize", size);
|
|
1152
2469
|
}
|
|
1153
2470
|
const font = get("labelFont");
|
|
2471
|
+
const bold = get("labelWeight") === "bold";
|
|
2472
|
+
const italic = get("labelItalic") === true;
|
|
1154
2473
|
if (typeof font === "string") {
|
|
1155
|
-
|
|
2474
|
+
const name = graphvizFontName(font, bold, italic);
|
|
2475
|
+
out.set("fontname", name);
|
|
2476
|
+
const drawn = graphvizFont(name);
|
|
2477
|
+
if (drawn.bold !== bold) {
|
|
2478
|
+
approximate("labelWeight", get("labelWeight") ?? "normal");
|
|
2479
|
+
}
|
|
2480
|
+
if (drawn.italic !== italic) {
|
|
2481
|
+
approximate("labelItalic", italic);
|
|
2482
|
+
}
|
|
2483
|
+
} else if (bold || italic) {
|
|
2484
|
+
approximate(bold ? "labelWeight" : "labelItalic", bold ? "bold" : true);
|
|
1156
2485
|
}
|
|
1157
2486
|
const tip = get("tooltip");
|
|
1158
2487
|
if (typeof tip === "string") {
|
|
@@ -1167,14 +2496,21 @@ function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): voi
|
|
|
1167
2496
|
* A node's or cluster's Graphviz attributes from its values.
|
|
1168
2497
|
* @param get - the node's values
|
|
1169
2498
|
* @param cx - the node's name and kind
|
|
2499
|
+
* @param raw - its attributes as data
|
|
1170
2500
|
* @param approximate - records approximations
|
|
1171
2501
|
* @param notes - where an HTML name is noted
|
|
1172
2502
|
* @returns the attributes
|
|
1173
2503
|
*/
|
|
1174
|
-
function nodeAttributes(
|
|
2504
|
+
function nodeAttributes(
|
|
2505
|
+
get: Get,
|
|
2506
|
+
cx: NodeContext,
|
|
2507
|
+
raw: Attr,
|
|
2508
|
+
approximate: Approximate,
|
|
2509
|
+
notes: StyleNotes,
|
|
2510
|
+
): Map<string, Written> {
|
|
1175
2511
|
const out = new Map<string, Written>();
|
|
1176
2512
|
const tokens: string[] = [];
|
|
1177
|
-
writeCommon(get, out, tokens);
|
|
2513
|
+
writeCommon(get, approximate, out, tokens);
|
|
1178
2514
|
writeNodeOutline(get, cx, approximate, out, tokens);
|
|
1179
2515
|
const gvShape = writeNodeShape(get, cx, approximate, out, tokens);
|
|
1180
2516
|
if (!cx.cluster) {
|
|
@@ -1188,12 +2524,30 @@ function nodeAttributes(get: Get, cx: NodeContext, approximate: Approximate, not
|
|
|
1188
2524
|
} else if (label === null && shown !== null) {
|
|
1189
2525
|
out.set(LABEL_ATTRIBUTE, "");
|
|
1190
2526
|
}
|
|
2527
|
+
noteLabelPosition(get("labelPosition"), cx, raw, approximate);
|
|
1191
2528
|
if (tokens.length > 0) {
|
|
1192
2529
|
out.set("style", tokens.join(","));
|
|
1193
2530
|
}
|
|
1194
2531
|
return withoutDefaults(out, "node");
|
|
1195
2532
|
}
|
|
1196
2533
|
|
|
2534
|
+
/**
|
|
2535
|
+
* Notes a label position Graphviz does not draw: a node's label sits where its own `labelloc` (kept as data) puts
|
|
2536
|
+
* it, and a cluster's where its `labelloc` and `labeljust` do.
|
|
2537
|
+
* @param position - the label's position
|
|
2538
|
+
* @param cx - the node's name and kind
|
|
2539
|
+
* @param raw - the node's attributes as data
|
|
2540
|
+
* @param approximate - records approximations
|
|
2541
|
+
*/
|
|
2542
|
+
function noteLabelPosition(position: StyleValue | null, cx: NodeContext, raw: Attr, approximate: Approximate): void {
|
|
2543
|
+
if (
|
|
2544
|
+
typeof position === "string" &&
|
|
2545
|
+
position !== (cx.cluster ? clusterLabelPosition(raw) : nodeLabelPosition(raw))
|
|
2546
|
+
) {
|
|
2547
|
+
approximate("labelPosition", position);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
1197
2551
|
/**
|
|
1198
2552
|
* A node's or cluster's fill and outline attributes.
|
|
1199
2553
|
* @param get - the node's values
|
|
@@ -1259,20 +2613,95 @@ function writeNodeShape(
|
|
|
1259
2613
|
}
|
|
1260
2614
|
return "";
|
|
1261
2615
|
}
|
|
2616
|
+
const size = (name: string): number | null => {
|
|
2617
|
+
const v = get(name);
|
|
2618
|
+
return typeof v === "number" ? v : null;
|
|
2619
|
+
};
|
|
2620
|
+
const glyph = shape === "polygon" ? glyphOf(get, size("width"), size("height")) : undefined;
|
|
2621
|
+
if (glyph !== undefined) {
|
|
2622
|
+
return writeGlyph(get, glyph, out);
|
|
2623
|
+
}
|
|
2624
|
+
approximateShapeLines(get, approximate);
|
|
2625
|
+
const polygon = shape === "polygon" ? writePolygon(get("shapePoints"), approximate, out) : null;
|
|
2626
|
+
if (polygon !== null) {
|
|
2627
|
+
// Graphviz draws its polygon's corners where it lays them, which read back unstretched
|
|
2628
|
+
if (get("shapePointsStretch") !== false) {
|
|
2629
|
+
approximate("shapePointsStretch", true);
|
|
2630
|
+
}
|
|
2631
|
+
return polygon;
|
|
2632
|
+
}
|
|
1262
2633
|
const gvShape = spelled(SHAPES, shape, shape === "vee" ? "invtriangle" : "box", "shape", approximate);
|
|
1263
2634
|
out.set("shape", gvShape);
|
|
1264
2635
|
return gvShape;
|
|
1265
2636
|
}
|
|
1266
2637
|
|
|
1267
2638
|
/**
|
|
1268
|
-
* A
|
|
2639
|
+
* A glyph's shape attribute, and its line's width as its pen's when it draws no outline.
|
|
2640
|
+
* @param get - the node's values
|
|
2641
|
+
* @param glyph - the glyph
|
|
2642
|
+
* @param out - the attributes, added to
|
|
2643
|
+
* @returns the Graphviz shape written
|
|
2644
|
+
*/
|
|
2645
|
+
function writeGlyph(get: Get, glyph: NonNullable<ReturnType<typeof glyphOf>>, out: Map<string, Written>): string {
|
|
2646
|
+
out.set("shape", glyph.name);
|
|
2647
|
+
// an underline's line is drawn in its pen, which draws no outline round it
|
|
2648
|
+
const line = get("shapeLineWidth");
|
|
2649
|
+
if (glyph.unstroked && typeof line === "number") {
|
|
2650
|
+
out.set("penwidth", line);
|
|
2651
|
+
}
|
|
2652
|
+
return glyph.name;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* Notes a node's pieces and lines of no Graphviz shape as approximated: Graphviz has no attribute for them.
|
|
2657
|
+
* @param get - the node's values
|
|
2658
|
+
* @param approximate - records approximations
|
|
2659
|
+
*/
|
|
2660
|
+
function approximateShapeLines(get: Get, approximate: Approximate): void {
|
|
2661
|
+
for (const property of ["shapeParts", "shapeLines", "shapeLineWidth"]) {
|
|
2662
|
+
const v = get(property);
|
|
2663
|
+
if (v !== null && v !== undefined) {
|
|
2664
|
+
approximate(property, v);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
/**
|
|
2670
|
+
* A polygon's shape attribute: the Graphviz shape whose outline it is, else `shape=polygon` with its number of sides.
|
|
2671
|
+
* @param points - its `shapePoints`
|
|
2672
|
+
* @param approximate - records approximations
|
|
2673
|
+
* @param out - the attributes, added to
|
|
2674
|
+
* @returns the Graphviz shape written, or null for corners too few to be a polygon
|
|
2675
|
+
*/
|
|
2676
|
+
function writePolygon(points: unknown, approximate: Approximate, out: Map<string, Written>): string | null {
|
|
2677
|
+
if (typeof points !== "string") {
|
|
2678
|
+
return null;
|
|
2679
|
+
}
|
|
2680
|
+
const named = OUTLINE_NAMES.get(points.trim());
|
|
2681
|
+
if (named !== undefined) {
|
|
2682
|
+
out.set("shape", named);
|
|
2683
|
+
return named;
|
|
2684
|
+
}
|
|
2685
|
+
// the corners themselves are not Graphviz attributes: the polygon keeps its number of sides
|
|
2686
|
+
const sides = points.trim().split(/\s+/).length;
|
|
2687
|
+
if (sides < 3) {
|
|
2688
|
+
return null;
|
|
2689
|
+
}
|
|
2690
|
+
out.set("shape", "polygon");
|
|
2691
|
+
out.set("sides", sides);
|
|
2692
|
+
approximate("shapePoints", points);
|
|
2693
|
+
return "polygon";
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/**
|
|
2697
|
+
* A node's width and height, fixed so Graphviz does not grow them to fit the label, unless the node grows to fit it.
|
|
1269
2698
|
* @param get - the node's values
|
|
1270
2699
|
* @param out - the attributes, added to
|
|
1271
2700
|
*/
|
|
1272
2701
|
function writeNodeSize(get: Get, out: Map<string, Written>): void {
|
|
1273
2702
|
const width = get("width");
|
|
1274
2703
|
const height = get("height");
|
|
1275
|
-
if (typeof width === "number" || typeof height === "number") {
|
|
2704
|
+
if ((typeof width === "number" || typeof height === "number") && typeof get("labelFit") !== "string") {
|
|
1276
2705
|
out.set("fixedsize", "true");
|
|
1277
2706
|
}
|
|
1278
2707
|
if (typeof width === "number") {
|
|
@@ -1293,7 +2722,7 @@ function writeNodeSize(get: Get, out: Map<string, Written>): void {
|
|
|
1293
2722
|
function edgeAttributes(get: Get, directed: boolean, approximate: Approximate): Map<string, Written> {
|
|
1294
2723
|
const out = new Map<string, Written>();
|
|
1295
2724
|
const tokens: string[] = [];
|
|
1296
|
-
writeCommon(get, out, tokens);
|
|
2725
|
+
writeCommon(get, approximate, out, tokens);
|
|
1297
2726
|
const color = get("color");
|
|
1298
2727
|
if (typeof color === "string") {
|
|
1299
2728
|
out.set("color", gvColor(color, get("opacity")));
|
|
@@ -1307,6 +2736,16 @@ function edgeAttributes(get: Get, directed: boolean, approximate: Approximate):
|
|
|
1307
2736
|
tokens.push(spelled(STROKES, line, line.includes("dash") ? "dashed" : "solid", "lineStyle", approximate));
|
|
1308
2737
|
}
|
|
1309
2738
|
writeEdgeArrows(get, directed, approximate, out);
|
|
2739
|
+
// Graphviz fills both arrowheads with the one `fillcolor`
|
|
2740
|
+
const head = get("targetArrowColor");
|
|
2741
|
+
const tail = get("sourceArrowColor");
|
|
2742
|
+
const fill = head ?? tail;
|
|
2743
|
+
if (typeof fill === "string") {
|
|
2744
|
+
out.set("fillcolor", fill);
|
|
2745
|
+
if (head !== null && tail !== null && head !== tail) {
|
|
2746
|
+
approximate("sourceArrowColor", tail);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
1310
2749
|
const arrowSize = get("arrowSize");
|
|
1311
2750
|
if (typeof arrowSize === "number") {
|
|
1312
2751
|
out.set("arrowsize", arrowSize);
|
|
@@ -1339,7 +2778,7 @@ function writeEdgeArrows(get: Get, directed: boolean, approximate: Approximate,
|
|
|
1339
2778
|
const target = get("targetArrow");
|
|
1340
2779
|
const tail = typeof source === "string" && source !== "none";
|
|
1341
2780
|
const head = typeof target === "string" && target !== "none";
|
|
1342
|
-
const plain = directed ? head && !tail && target === "triangle" : !head && !tail;
|
|
2781
|
+
const plain = directed ? head && !tail && target === "triangle" && get("targetArrowSide") === null : !head && !tail;
|
|
1343
2782
|
if (plain || (source === null && target === null)) {
|
|
1344
2783
|
return;
|
|
1345
2784
|
}
|
|
@@ -1352,29 +2791,23 @@ function writeEdgeArrows(get: Get, directed: boolean, approximate: Approximate,
|
|
|
1352
2791
|
dir = "back";
|
|
1353
2792
|
}
|
|
1354
2793
|
out.set("dir", dir);
|
|
2794
|
+
// a one-sided arrow is its shape with an `l` or `r` before it; the neutral half arrow, one barb, is nearest to the
|
|
2795
|
+
// left half of Graphviz's triangle, which reads back as that
|
|
2796
|
+
const arrow = (value: string, side: StyleValue | null, property: string): string => {
|
|
2797
|
+
if (value === "half") {
|
|
2798
|
+
approximate(property, value);
|
|
2799
|
+
approximate(`${property}Side`, value);
|
|
2800
|
+
return "lnormal";
|
|
2801
|
+
}
|
|
2802
|
+
const name = spelled(ARROWS, value, "normal", property, approximate);
|
|
2803
|
+
return side === "left" || side === "right" ? `${side[0]}${name}` : name;
|
|
2804
|
+
};
|
|
1355
2805
|
if (head) {
|
|
1356
|
-
out.set("arrowhead",
|
|
2806
|
+
out.set("arrowhead", arrow(target, get("targetArrowSide"), "targetArrow"));
|
|
1357
2807
|
}
|
|
1358
2808
|
if (tail) {
|
|
1359
|
-
out.set("arrowtail",
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
/**
|
|
1364
|
-
* Each element's values of the properties the writer writes, as a getter per element.
|
|
1365
|
-
* @param picture - each element's value of a property
|
|
1366
|
-
* @param target - nodes or edges
|
|
1367
|
-
* @returns the getter
|
|
1368
|
-
*/
|
|
1369
|
-
function getter(
|
|
1370
|
-
picture: (target: StyleTarget, property: string) => readonly (StyleValue | null)[],
|
|
1371
|
-
target: StyleTarget,
|
|
1372
|
-
): (i: number) => Get {
|
|
1373
|
-
const columns = new Map<string, readonly (StyleValue | null)[]>();
|
|
1374
|
-
for (const p of DOT_STYLE_CAPABILITIES[target]) {
|
|
1375
|
-
columns.set(p, picture(target, p));
|
|
2809
|
+
out.set("arrowtail", arrow(source, get("sourceArrowSide"), "sourceArrow"));
|
|
1376
2810
|
}
|
|
1377
|
-
return (i) => (property) => columns.get(property)?.[i] ?? null;
|
|
1378
2811
|
}
|
|
1379
2812
|
|
|
1380
2813
|
/**
|
|
@@ -1395,29 +2828,17 @@ function exactFromBlock(
|
|
|
1395
2828
|
read: { readonly node: ValueColumns; readonly edge: ValueColumns },
|
|
1396
2829
|
cx: ReturnType<typeof contexts>,
|
|
1397
2830
|
): void {
|
|
1398
|
-
const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
|
|
1399
|
-
const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "dot") : null;
|
|
1400
|
-
if (ex === null) {
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
2831
|
const notes = new StyleNotes();
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
const back = writtenAndReadBack(target, i, want, cx, notes);
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
if (native !== w && native === (back.get(property) ?? null)) {
|
|
1415
|
-
values[property] ??= new Array<StyleValue | null>(n).fill(null);
|
|
1416
|
-
values[property][i] = w;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
2832
|
+
takeFromBlock(snapshot, text, "dot", (target) => ({
|
|
2833
|
+
values: read[target],
|
|
2834
|
+
properties: DOT_STYLE_CAPABILITIES[target],
|
|
2835
|
+
writes: DOT_STYLE_CAPABILITIES[target],
|
|
2836
|
+
readBack: (i, want) => {
|
|
2837
|
+
const raw: Attr = (name) => cellText(target === "node" ? snapshot.nodes : snapshot.edges, name, i);
|
|
2838
|
+
const back = writtenAndReadBack(target, i, want, { cx, raw }, notes);
|
|
2839
|
+
return (property) => back.get(property) ?? null;
|
|
2840
|
+
},
|
|
2841
|
+
}));
|
|
1421
2842
|
}
|
|
1422
2843
|
|
|
1423
2844
|
/**
|
|
@@ -1425,7 +2846,9 @@ function exactFromBlock(
|
|
|
1425
2846
|
* @param target - nodes or edges
|
|
1426
2847
|
* @param i - the element
|
|
1427
2848
|
* @param want - its values
|
|
1428
|
-
* @param
|
|
2849
|
+
* @param data - the elements' contexts, and the element's attributes as data (what the writer leaves as it is)
|
|
2850
|
+
* @param data.cx - the elements' contexts
|
|
2851
|
+
* @param data.raw - the element's attributes as data
|
|
1429
2852
|
* @param notes - a scratch for the notes the round trip makes
|
|
1430
2853
|
* @returns the values read back, by property
|
|
1431
2854
|
*/
|
|
@@ -1433,9 +2856,10 @@ function writtenAndReadBack(
|
|
|
1433
2856
|
target: StyleTarget,
|
|
1434
2857
|
i: number,
|
|
1435
2858
|
want: Get,
|
|
1436
|
-
cx: ReturnType<typeof contexts
|
|
2859
|
+
data: { readonly cx: ReturnType<typeof contexts>; readonly raw: Attr },
|
|
1437
2860
|
notes: StyleNotes,
|
|
1438
2861
|
): Map<string, StyleValue> {
|
|
2862
|
+
const { cx, raw } = data;
|
|
1439
2863
|
const ignore: Approximate = () => undefined;
|
|
1440
2864
|
const back = new Map<string, StyleValue>();
|
|
1441
2865
|
const put: Put = (property, value) => {
|
|
@@ -1446,8 +2870,10 @@ function writtenAndReadBack(
|
|
|
1446
2870
|
const at: At = (property) => ({ notes, target, property });
|
|
1447
2871
|
if (target === "node") {
|
|
1448
2872
|
const ncx = cx.node(i);
|
|
1449
|
-
const attrs = nodeAttributes(want, ncx, ignore, notes);
|
|
1450
|
-
|
|
2873
|
+
const attrs = nodeAttributes(want, ncx, raw, ignore, notes);
|
|
2874
|
+
// a cluster's label position stays in its data (`labelloc`, `labeljust`), which the writer leaves alone
|
|
2875
|
+
const kept = (name: string): string | null => (name === "labelloc" || name === "labeljust" ? raw(name) : null);
|
|
2876
|
+
readNode((name) => (attrs.has(name) ? textOf(attrs.get(name)) : kept(name)), ncx, put, at);
|
|
1451
2877
|
} else {
|
|
1452
2878
|
const ecx = cx.edge(i);
|
|
1453
2879
|
const attrs = edgeAttributes(want, ecx.directed, ignore);
|
|
@@ -1600,30 +3026,105 @@ function attributeColumns(
|
|
|
1600
3026
|
w: AttributeWrite,
|
|
1601
3027
|
): Map<string, (Written | null)[]> {
|
|
1602
3028
|
const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
|
|
1603
|
-
const get =
|
|
3029
|
+
const get = elementValues((t, p) => input.picture(t, p as never), target, DOT_STYLE_CAPABILITIES[target]);
|
|
1604
3030
|
const columns = new Map<string, (Written | null)[]>();
|
|
1605
3031
|
for (let i = 0; i < n; i++) {
|
|
1606
3032
|
const attrs =
|
|
1607
3033
|
target === "node"
|
|
1608
|
-
? nodeAttributes(
|
|
3034
|
+
? nodeAttributes(
|
|
3035
|
+
get(i),
|
|
3036
|
+
w.cx.node(i),
|
|
3037
|
+
(name) => cellText(snapshot.nodes, name, i),
|
|
3038
|
+
w.approximate,
|
|
3039
|
+
w.notes,
|
|
3040
|
+
)
|
|
1609
3041
|
: edgeAttributes(get(i), w.cx.edge(i).directed, w.approximate);
|
|
3042
|
+
// a self-loop's route is not written: Graphviz lays the loop itself, which is what reads back
|
|
3043
|
+
if (target === "edge" && snapshot.edgeSource(i) === snapshot.edgeTarget(i)) {
|
|
3044
|
+
w.approximate("bends", "self-loop");
|
|
3045
|
+
}
|
|
1610
3046
|
// no label is written label="" (Graphviz would show the name), which a rule reading the label column
|
|
1611
3047
|
// reads back as the empty label
|
|
1612
3048
|
if (w.read.has(LABEL_ATTRIBUTE) && attrs.get(LABEL_ATTRIBUTE) === "" && get(i)("label") === null) {
|
|
1613
3049
|
w.approximate("label", "");
|
|
1614
3050
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
columns.set(name, col);
|
|
1620
|
-
}
|
|
1621
|
-
col[i] = value;
|
|
1622
|
-
}
|
|
3051
|
+
setColumnValues(columns, attrs, i, n);
|
|
3052
|
+
}
|
|
3053
|
+
if (target === "node") {
|
|
3054
|
+
stopInheritance(snapshot, w.cx, columns, input.rules.styles.graph.background);
|
|
1623
3055
|
}
|
|
1624
3056
|
return columns;
|
|
1625
3057
|
}
|
|
1626
3058
|
|
|
3059
|
+
/**
|
|
3060
|
+
* Puts one element's attributes into the columns, adding a column for an attribute not seen before.
|
|
3061
|
+
* @param columns - the columns, by name
|
|
3062
|
+
* @param attrs - the element's attributes
|
|
3063
|
+
* @param i - the element
|
|
3064
|
+
* @param n - how many elements there are
|
|
3065
|
+
*/
|
|
3066
|
+
function setColumnValues(
|
|
3067
|
+
columns: Map<string, (Written | null)[]>,
|
|
3068
|
+
attrs: ReadonlyMap<string, Written>,
|
|
3069
|
+
i: number,
|
|
3070
|
+
n: number,
|
|
3071
|
+
): void {
|
|
3072
|
+
for (const [name, value] of attrs) {
|
|
3073
|
+
let col = columns.get(name);
|
|
3074
|
+
if (col === undefined) {
|
|
3075
|
+
col = new Array<Written | null>(n).fill(null);
|
|
3076
|
+
columns.set(name, col);
|
|
3077
|
+
}
|
|
3078
|
+
col[i] = value;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
/** What a cluster draws when neither it nor a graph around it sets the attribute: Graphviz's defaults. */
|
|
3083
|
+
const CLUSTER_UNSET: ReadonlyMap<string, Written> = new Map<string, Written>([
|
|
3084
|
+
["style", ""],
|
|
3085
|
+
["penwidth", 1],
|
|
3086
|
+
["color", ""],
|
|
3087
|
+
["pencolor", ""],
|
|
3088
|
+
["fillcolor", ""],
|
|
3089
|
+
["bgcolor", ""],
|
|
3090
|
+
["fontname", "Times-Roman"],
|
|
3091
|
+
["fontsize", 14],
|
|
3092
|
+
["fontcolor", "black"],
|
|
3093
|
+
[LABEL_ATTRIBUTE, ""],
|
|
3094
|
+
]);
|
|
3095
|
+
|
|
3096
|
+
/**
|
|
3097
|
+
* Gives every cluster its own value of each attribute a graph around it sets, Graphviz's default where the cluster
|
|
3098
|
+
* has none, since Graphviz gives a cluster the attributes of the graphs around it (the root's are kept as data).
|
|
3099
|
+
* @param snapshot - the graph handed to the exporter
|
|
3100
|
+
* @param cx - the elements' contexts
|
|
3101
|
+
* @param columns - the node attributes written, by name, changed in place
|
|
3102
|
+
* @param background - the background the writer gives the root graph (its `bgcolor`), which a cluster would inherit
|
|
3103
|
+
*/
|
|
3104
|
+
function stopInheritance(
|
|
3105
|
+
snapshot: GraphSnapshot,
|
|
3106
|
+
cx: ReturnType<typeof contexts>,
|
|
3107
|
+
columns: Map<string, (Written | null)[]>,
|
|
3108
|
+
background: unknown,
|
|
3109
|
+
): void {
|
|
3110
|
+
const clusters = Array.from({ length: snapshot.nodeCount }, (_, i) => i).filter((i) => cx.node(i).cluster);
|
|
3111
|
+
for (const [name, unset] of CLUSTER_UNSET) {
|
|
3112
|
+
const col = columns.get(name);
|
|
3113
|
+
const enclosing =
|
|
3114
|
+
cellText(snapshot.graph, name, 0) !== null ||
|
|
3115
|
+
(name === "bgcolor" && typeof background === "string") ||
|
|
3116
|
+
clusters.some((i) => (col?.[i] ?? null) !== null);
|
|
3117
|
+
if (!enclosing) {
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
const filled = col ?? new Array<Written | null>(snapshot.nodeCount).fill(null);
|
|
3121
|
+
for (const i of clusters) {
|
|
3122
|
+
filled[i] ??= unset;
|
|
3123
|
+
}
|
|
3124
|
+
columns.set(name, filled);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
1627
3128
|
/**
|
|
1628
3129
|
* Removes every column Graphviz reads as how to draw: the picture writes them, so a data column of the same name (a
|
|
1629
3130
|
* `label` attribute graphty does not draw) would draw something graphty does not. One a rule reads, whose values the
|