Skip to content

Commit eaa7dfe

Browse files
authored
fix: Escape html-like labels in DotRenderer (#2383)
fixes #2286 ![Graphviz Online](https://github.com/user-attachments/assets/6100474a-3a37-4c5f-9bfa-48f641f122aa) It looks like there's a graphviz bug that causes nodes to be too narrow when using the monospace font. I'll open a separate issue to make a workaround for that.
1 parent aa45448 commit eaa7dfe

File tree

3 files changed

+160
-14
lines changed

3 files changed

+160
-14
lines changed

hugr-py/src/hugr/hugr/render.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Visualise HUGR using graphviz."""
22

3+
import html
34
from collections.abc import Iterable
45
from dataclasses import dataclass, field
56

@@ -101,7 +102,9 @@ def render(self, hugr: Hugr) -> Digraph:
101102
"margin": "0",
102103
"bgcolor": self.config.palette.background,
103104
}
104-
if not (name := hugr[hugr.module_root].metadata.get("name", None)):
105+
if name := hugr[hugr.module_root].metadata.get("name", None):
106+
name = html.escape(name)
107+
else:
105108
name = ""
106109

107110
graph = gv.Digraph(name, strict=False)
@@ -215,7 +218,8 @@ def _viz_node(self, node: Node, hugr: Hugr, graph: Digraph) -> None:
215218
meta = hugr[node].metadata
216219
if len(meta) > 0:
217220
data = "<BR/><BR/>" + "<BR/>".join(
218-
f"{key}: {value}" for key, value in meta.items()
221+
html.escape(key) + ": " + html.escape(value)
222+
for key, value in meta.items()
219223
)
220224
else:
221225
data = ""
@@ -236,6 +240,7 @@ def _viz_node(self, node: Node, hugr: Hugr, graph: Digraph) -> None:
236240
op_name = op.op_def().name
237241
else:
238242
op_name = op.name()
243+
op_name = html.escape(op_name)
239244

240245
label_config = {
241246
"node_back_color": self.config.palette.node,
@@ -286,7 +291,7 @@ def _viz_link(
286291
label = ""
287292
match kind:
288293
case ValueKind(ty):
289-
label = str(ty)
294+
label = html.escape(str(ty))
290295
color = self.config.palette.edge
291296
case OrderKind():
292297
color = self.config.palette.dark

hugr-py/tests/__snapshots__/test_hugr_build.ambr

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@
940940
<TD>
941941
<TABLE BORDER="0" CELLBORDER="0">
942942
<TR><TD><FONT POINT-SIZE="11.0" FACE="monospace"
943-
COLOR="black"><B>Const(Function(body=Hugr(module_root=Node(0), entrypoint=Node(4), _nodes=[NodeData(op=Module(), parent=None, metadata={}), NodeData(op=FuncDefn(f_name='main', inputs=[Qubit], params=[]), parent=Node(0), metadata={}), NodeData(op=Input(types=[Qubit]), parent=Node(1), metadata={}), NodeData(op=Output(), parent=Node(1), metadata={}), NodeData(op=DFG(inputs=[Qubit]), parent=Node(1), metadata={}), NodeData(op=Input(types=[Qubit]), parent=Node(4), metadata={}), NodeData(op=Output(), parent=Node(4), metadata={}), NodeData(op=Noop(Qubit), parent=Node(4), metadata={})], _links=BiMap({_SubPort(port=OutPort(Node(2), 0), sub_offset=0): _SubPort(port=InPort(Node(4), 0), sub_offset=0), _SubPort(port=OutPort(Node(5), 0), sub_offset=0): _SubPort(port=InPort(Node(7), 0), sub_offset=0), _SubPort(port=OutPort(Node(7), 0), sub_offset=0): _SubPort(port=InPort(Node(6), 0), sub_offset=0), _SubPort(port=OutPort(Node(4), 0), sub_offset=0): _SubPort(port=InPort(Node(3), 0), sub_offset=0)}), _free_nodes=[])))</B></FONT></TD></TR>
943+
COLOR="black"><B>Const(Function(body=Hugr(module_root=Node(0), entrypoint=Node(4), _nodes=[NodeData(op=Module(), parent=None, metadata={}), NodeData(op=FuncDefn(f_name=&#x27;main&#x27;, inputs=[Qubit], params=[]), parent=Node(0), metadata={}), NodeData(op=Input(types=[Qubit]), parent=Node(1), metadata={}), NodeData(op=Output(), parent=Node(1), metadata={}), NodeData(op=DFG(inputs=[Qubit]), parent=Node(1), metadata={}), NodeData(op=Input(types=[Qubit]), parent=Node(4), metadata={}), NodeData(op=Output(), parent=Node(4), metadata={}), NodeData(op=Noop(Qubit), parent=Node(4), metadata={})], _links=BiMap({_SubPort(port=OutPort(Node(2), 0), sub_offset=0): _SubPort(port=InPort(Node(4), 0), sub_offset=0), _SubPort(port=OutPort(Node(5), 0), sub_offset=0): _SubPort(port=InPort(Node(7), 0), sub_offset=0), _SubPort(port=OutPort(Node(7), 0), sub_offset=0): _SubPort(port=InPort(Node(6), 0), sub_offset=0), _SubPort(port=OutPort(Node(4), 0), sub_offset=0): _SubPort(port=InPort(Node(3), 0), sub_offset=0)}), _free_nodes=[])))</B></FONT></TD></TR>
944944
</TABLE>
945945
</TD>
946946
</TR>
@@ -1100,7 +1100,7 @@
11001100
}
11011101
2:"out.0" -> 4:"in.0" [label=Qubit arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
11021102
7:"out.0" -> 8:"in.0" [label="" arrowhead=none arrowsize=1.0 color="#77CEEF" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1103-
8:"out.0" -> 9:"in.0" [label="Qubit -> Qubit" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1103+
8:"out.0" -> 9:"in.0" [label="Qubit -&gt; Qubit" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
11041104
5:"out.0" -> 9:"in.1" [label=Qubit arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
11051105
5:"out.-1" -> 8:"in.-1" [label="" arrowhead=none arrowsize=1.0 color=black fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
11061106
9:"out.0" -> 6:"in.0" [label=Qubit arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
@@ -1109,6 +1109,127 @@
11091109

11101110
'''
11111111
# ---
1112+
# name: test_html_labels
1113+
'''
1114+
digraph "&lt;i&gt;Module Root&lt;/i&gt;" {
1115+
bgcolor=white margin=0 nodesep=0.15 rankdir="" ranksep=0.1
1116+
subgraph cluster0 {
1117+
subgraph cluster1 {
1118+
2 [label=<
1119+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="1" CELLPADDING="1"
1120+
BGCOLOR="#ACCBF9" COLOR="white">
1121+
1122+
<TR>
1123+
<TD>
1124+
<TABLE BORDER="0" CELLBORDER="0">
1125+
<TR><TD><FONT POINT-SIZE="11.0" FACE="monospace"
1126+
COLOR="black"><B>Input</B></FONT></TD></TR>
1127+
</TABLE>
1128+
</TD>
1129+
</TR>
1130+
1131+
<TR>
1132+
<TD>
1133+
<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="3" CELLPADDING="2">
1134+
<TR>
1135+
<TD BGCOLOR="white" COLOR="#1CADE4" PORT="out.0" BORDER="1"><FONT POINT-SIZE="10.0" FACE="monospace" COLOR="black">0</FONT></TD>
1136+
</TR>
1137+
</TABLE>
1138+
</TD>
1139+
</TR>
1140+
1141+
</TABLE>
1142+
> shape=plain]
1143+
3 [label=<
1144+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="1" CELLPADDING="1"
1145+
BGCOLOR="#ACCBF9" COLOR="white">
1146+
1147+
<TR>
1148+
<TD>
1149+
<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="3" CELLPADDING="2">
1150+
<TR>
1151+
<TD BGCOLOR="white" COLOR="#1CADE4" PORT="in.0" BORDER="1"><FONT POINT-SIZE="10.0" FACE="monospace" COLOR="black">0</FONT></TD>
1152+
</TR>
1153+
</TABLE>
1154+
</TD>
1155+
</TR>
1156+
1157+
<TR>
1158+
<TD>
1159+
<TABLE BORDER="0" CELLBORDER="0">
1160+
<TR><TD><FONT POINT-SIZE="11.0" FACE="monospace"
1161+
COLOR="black"><B>Output</B></FONT></TD></TR>
1162+
</TABLE>
1163+
</TD>
1164+
</TR>
1165+
1166+
</TABLE>
1167+
> shape=plain]
1168+
4 [label=<
1169+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="1" CELLPADDING="1"
1170+
BGCOLOR="#ACCBF9" COLOR="white">
1171+
1172+
<TR>
1173+
<TD>
1174+
<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="3" CELLPADDING="2">
1175+
<TR>
1176+
<TD BGCOLOR="white" COLOR="#1CADE4" PORT="in.0" BORDER="1"><FONT POINT-SIZE="10.0" FACE="monospace" COLOR="black">0</FONT></TD>
1177+
</TR>
1178+
</TABLE>
1179+
</TD>
1180+
</TR>
1181+
1182+
<TR>
1183+
<TD>
1184+
<TABLE BORDER="0" CELLBORDER="0">
1185+
<TR><TD><FONT POINT-SIZE="11.0" FACE="monospace"
1186+
COLOR="black"><B>Some</B></FONT></TD></TR>
1187+
</TABLE>
1188+
</TD>
1189+
</TR>
1190+
1191+
</TABLE>
1192+
> shape=plain]
1193+
1 [label=<
1194+
<TABLE BORDER="2" CELLBORDER="0" CELLSPACING="1" CELLPADDING="1"
1195+
BGCOLOR="#1CADE4" COLOR="#F4A261">
1196+
1197+
<TR>
1198+
<TD>
1199+
<TABLE BORDER="0" CELLBORDER="0">
1200+
<TR><TD><FONT POINT-SIZE="11.0" FACE="monospace"
1201+
COLOR="black"><B><b>[FuncDefn(&lt;jupyter-notebook&gt;)]</b></B><BR/><BR/>label: &lt;b&gt;Bold Label&lt;/b&gt;<BR/>&lt;other-label&gt;: &lt;i&gt;Italic Label&lt;/i&gt;</FONT></TD></TR>
1202+
</TABLE>
1203+
</TD>
1204+
</TR>
1205+
1206+
</TABLE>
1207+
> shape=plain]
1208+
color="#F4A261" label="" margin=10 penwidth=2
1209+
}
1210+
0 [label=<
1211+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="1" CELLPADDING="1"
1212+
BGCOLOR="#1CADE4" COLOR="#1CADE4">
1213+
1214+
<TR>
1215+
<TD>
1216+
<TABLE BORDER="0" CELLBORDER="0">
1217+
<TR><TD><FONT POINT-SIZE="11.0" FACE="monospace"
1218+
COLOR="black"><B>Module</B><BR/><BR/>name: &lt;i&gt;Module Root&lt;/i&gt;</FONT></TD></TR>
1219+
</TABLE>
1220+
</TD>
1221+
</TR>
1222+
1223+
</TABLE>
1224+
> shape=plain]
1225+
color="#1CADE4" label="" margin=10 penwidth=1
1226+
}
1227+
2:"out.0" -> 4:"in.0" [label=Bool arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1228+
2:"out.0" -> 3:"in.0" [label=Bool arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1229+
}
1230+
1231+
'''
1232+
# ---
11121233
# name: test_insert_nested
11131234
'''
11141235
digraph {
@@ -1854,14 +1975,14 @@
18541975
> shape=plain]
18551976
color="#1CADE4" label="" margin=10 penwidth=1
18561977
}
1857-
2:"out.0" -> 4:"in.0" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1858-
2:"out.1" -> 4:"in.1" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1859-
5:"out.0" -> 7:"in.0" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1860-
5:"out.1" -> 7:"in.1" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1861-
7:"out.0" -> 6:"in.0" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1862-
7:"out.1" -> 6:"in.1" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1863-
4:"out.0" -> 3:"in.0" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1864-
4:"out.1" -> 3:"in.1" [label="int<5>" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1978+
2:"out.0" -> 4:"in.0" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1979+
2:"out.1" -> 4:"in.1" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1980+
5:"out.0" -> 7:"in.0" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1981+
5:"out.1" -> 7:"in.1" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1982+
7:"out.0" -> 6:"in.0" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1983+
7:"out.1" -> 6:"in.1" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1984+
4:"out.0" -> 3:"in.0" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
1985+
4:"out.1" -> 3:"in.1" [label="int&lt;5&gt;" arrowhead=none arrowsize=1.0 color="#1CADE4" fontcolor=black fontname=monospace fontsize=9 penwidth=1.5]
18651986
}
18661987

18671988
'''

hugr-py/tests/test_hugr_build.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import hugr.ops as ops
66
import hugr.tys as tys
77
import hugr.val as val
8-
from hugr.build.dfg import Dfg, _ancestral_sibling
8+
from hugr.build.dfg import Dfg, Function, _ancestral_sibling
99
from hugr.build.function import Module
1010
from hugr.hugr import Hugr
1111
from hugr.hugr.node_port import Node, _SubPort
@@ -404,3 +404,23 @@ def test_option() -> None:
404404
dfg.set_outputs(b)
405405

406406
validate(dfg.hugr)
407+
408+
409+
def test_html_labels(snapshot) -> None:
410+
"""Ensures that HTML-like labels can be processed correctly by both the builder and
411+
the renderer.
412+
"""
413+
f = Function(
414+
"<jupyter-notebook>",
415+
[tys.Bool],
416+
)
417+
f.metadata["label"] = "<b>Bold Label</b>"
418+
f.metadata["<other-label>"] = "<i>Italic Label</i>"
419+
420+
f.hugr[f.hugr.module_root].metadata["name"] = "<i>Module Root</i>"
421+
422+
b = f.inputs()[0]
423+
f.add_op(ops.Some(tys.Bool), b)
424+
f.set_outputs(b)
425+
426+
validate(f.hugr, snap=snapshot)

0 commit comments

Comments
 (0)