1use std::collections::HashMap;
5
6use portgraph::render::{EdgeStyle, NodeStyle, PortStyle, PresentationStyle};
7use portgraph::{LinkView, MultiPortGraph, NodeIndex, PortIndex, PortView};
8
9use crate::core::HugrNode;
10use crate::hugr::internal::HugrInternals;
11use crate::ops::{NamedOp, OpType};
12use crate::types::EdgeKind;
13use crate::{Hugr, HugrView, Node};
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20#[deprecated(note = "Use `MermaidFormatter` instead")]
21pub struct RenderConfig<N = Node> {
22 pub node_indices: bool,
24 pub port_offsets_in_edges: bool,
26 pub type_labels_in_edges: bool,
28 pub entrypoint: Option<N>,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct MermaidFormatter<'h, H: HugrInternals + ?Sized = Hugr> {
35 hugr: &'h H,
37 node_labels: NodeLabel<H::Node>,
39 port_offsets_in_edges: bool,
41 type_labels_in_edges: bool,
43 entrypoint: Option<H::Node>,
45}
46
47impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
48 #[allow(deprecated)]
50 pub fn from_render_config(config: RenderConfig<H::Node>, hugr: &'h H) -> Self {
51 let node_labels = if config.node_indices {
52 NodeLabel::Numeric
53 } else {
54 NodeLabel::None
55 };
56 Self {
57 hugr,
58 node_labels,
59 port_offsets_in_edges: config.port_offsets_in_edges,
60 type_labels_in_edges: config.type_labels_in_edges,
61 entrypoint: config.entrypoint,
62 }
63 }
64
65 pub fn new(hugr: &'h H) -> Self {
67 Self {
68 hugr,
69 node_labels: NodeLabel::Numeric,
70 port_offsets_in_edges: true,
71 type_labels_in_edges: true,
72 entrypoint: None,
73 }
74 }
75
76 pub fn entrypoint(&self) -> Option<H::Node> {
78 self.entrypoint
79 }
80
81 pub fn node_labels(&self) -> &NodeLabel<H::Node> {
83 &self.node_labels
84 }
85
86 pub fn port_offsets(&self) -> bool {
88 self.port_offsets_in_edges
89 }
90
91 pub fn type_labels(&self) -> bool {
93 self.type_labels_in_edges
94 }
95
96 pub fn with_node_labels(mut self, node_labels: NodeLabel<H::Node>) -> Self {
98 self.node_labels = node_labels;
99 self
100 }
101
102 pub fn with_port_offsets(mut self, show: bool) -> Self {
104 self.port_offsets_in_edges = show;
105 self
106 }
107
108 pub fn with_type_labels(mut self, show: bool) -> Self {
110 self.type_labels_in_edges = show;
111 self
112 }
113
114 pub fn with_entrypoint(mut self, entrypoint: impl Into<Option<H::Node>>) -> Self {
116 self.entrypoint = entrypoint.into();
117 self
118 }
119
120 pub fn finish(self) -> String
122 where
123 H: HugrView,
124 {
125 self.hugr.mermaid_string_with_formatter(self)
126 }
127
128 pub(crate) fn with_hugr<NewH: HugrInternals<Node = H::Node>>(
129 self,
130 hugr: &NewH,
131 ) -> MermaidFormatter<'_, NewH> {
132 let MermaidFormatter {
133 hugr: _,
134 node_labels,
135 port_offsets_in_edges,
136 type_labels_in_edges,
137 entrypoint,
138 } = self;
139 MermaidFormatter {
140 hugr,
141 node_labels,
142 port_offsets_in_edges,
143 type_labels_in_edges,
144 entrypoint,
145 }
146 }
147}
148
149#[derive(Debug, thiserror::Error)]
152pub enum UnsupportedRenderConfig {
153 #[error("Custom node labels are not supported in the `RenderConfig` struct")]
155 CustomNodeLabels,
156}
157
158#[allow(deprecated)]
159impl<'h, H: HugrInternals + ?Sized> TryFrom<MermaidFormatter<'h, H>> for RenderConfig<H::Node> {
160 type Error = UnsupportedRenderConfig;
161
162 fn try_from(value: MermaidFormatter<'h, H>) -> Result<Self, Self::Error> {
163 if matches!(value.node_labels, NodeLabel::Custom(_)) {
164 return Err(UnsupportedRenderConfig::CustomNodeLabels);
165 }
166 let node_indices = matches!(value.node_labels, NodeLabel::Numeric);
167 Ok(Self {
168 node_indices,
169 port_offsets_in_edges: value.port_offsets_in_edges,
170 type_labels_in_edges: value.type_labels_in_edges,
171 entrypoint: value.entrypoint,
172 })
173 }
174}
175
176macro_rules! impl_mermaid_formatter_from {
177 ($t:ty, $($lifetime:tt)?) => {
178 impl<'h, $($lifetime,)? H: HugrView> From<MermaidFormatter<'h, $t>> for MermaidFormatter<'h, H> {
179 fn from(value: MermaidFormatter<'h, $t>) -> Self {
180 let MermaidFormatter {
181 hugr,
182 node_labels,
183 port_offsets_in_edges,
184 type_labels_in_edges,
185 entrypoint,
186 } = value;
187 MermaidFormatter {
188 hugr,
189 node_labels,
190 port_offsets_in_edges,
191 type_labels_in_edges,
192 entrypoint,
193 }
194 }
195 }
196 };
197}
198
199impl_mermaid_formatter_from!(&'hh H, 'hh);
200impl_mermaid_formatter_from!(&'hh mut H, 'hh);
201impl_mermaid_formatter_from!(std::rc::Rc<H>,);
202impl_mermaid_formatter_from!(std::sync::Arc<H>,);
203impl_mermaid_formatter_from!(Box<H>,);
204
205impl<'h, H: HugrView + ToOwned> From<MermaidFormatter<'h, std::borrow::Cow<'_, H>>>
206 for MermaidFormatter<'h, H>
207{
208 fn from(value: MermaidFormatter<'h, std::borrow::Cow<'_, H>>) -> Self {
209 let MermaidFormatter {
210 hugr,
211 node_labels,
212 port_offsets_in_edges,
213 type_labels_in_edges,
214 entrypoint,
215 } = value;
216 MermaidFormatter {
217 hugr,
218 node_labels,
219 port_offsets_in_edges,
220 type_labels_in_edges,
221 entrypoint,
222 }
223 }
224}
225
226#[derive(Default, Clone, Debug, PartialEq, Eq)]
228pub enum NodeLabel<N: HugrNode = Node> {
229 None,
231 #[default]
233 Numeric,
234 Custom(HashMap<N, String>),
236}
237
238#[allow(deprecated)]
239impl<N> Default for RenderConfig<N> {
240 fn default() -> Self {
241 Self {
242 node_indices: true,
243 port_offsets_in_edges: true,
244 type_labels_in_edges: true,
245 entrypoint: None,
246 }
247 }
248}
249
250pub(in crate::hugr) fn node_style<'a>(
252 h: &'a Hugr,
253 formatter: MermaidFormatter<'a>,
254) -> Box<dyn FnMut(NodeIndex) -> NodeStyle + 'a> {
255 fn node_name(h: &Hugr, n: NodeIndex) -> String {
256 match h.get_optype(n.into()) {
257 OpType::FuncDecl(f) => format!("FuncDecl: \"{}\"", f.func_name()),
258 OpType::FuncDefn(f) => format!("FuncDefn: \"{}\"", f.func_name()),
259 op => op.name().to_string(),
260 }
261 }
262
263 let mut entrypoint_style = PresentationStyle::default();
264 entrypoint_style.stroke = Some("#832561".to_string());
265 entrypoint_style.stroke_width = Some("3px".to_string());
266 let entrypoint = formatter.entrypoint.map(Node::into_portgraph);
267
268 match formatter.node_labels {
269 NodeLabel::Numeric => Box::new(move |n| {
270 if Some(n) == entrypoint {
271 NodeStyle::boxed(format!(
272 "({ni}) [**{name}**]",
273 ni = n.index(),
274 name = node_name(h, n)
275 ))
276 .with_attrs(entrypoint_style.clone())
277 } else {
278 NodeStyle::boxed(format!(
279 "({ni}) {name}",
280 ni = n.index(),
281 name = node_name(h, n)
282 ))
283 }
284 }),
285 NodeLabel::None => Box::new(move |n| {
286 if Some(n) == entrypoint {
287 NodeStyle::boxed(format!("[**{name}**]", name = node_name(h, n)))
288 .with_attrs(entrypoint_style.clone())
289 } else {
290 NodeStyle::boxed(node_name(h, n))
291 }
292 }),
293 NodeLabel::Custom(labels) => Box::new(move |n| {
294 if Some(n) == entrypoint {
295 NodeStyle::boxed(format!(
296 "({label}) [**{name}**]",
297 label = labels.get(&n.into()).unwrap_or(&n.index().to_string()),
298 name = node_name(h, n)
299 ))
300 .with_attrs(entrypoint_style.clone())
301 } else {
302 NodeStyle::boxed(format!(
303 "({label}) {name}",
304 label = labels.get(&n.into()).unwrap_or(&n.index().to_string()),
305 name = node_name(h, n)
306 ))
307 }
308 }),
309 }
310}
311
312pub(in crate::hugr) fn port_style(h: &Hugr) -> Box<dyn FnMut(PortIndex) -> PortStyle + '_> {
314 let graph = &h.graph;
315 Box::new(move |port| {
316 let node = graph.port_node(port).unwrap();
317 let optype = h.get_optype(node.into());
318 let offset = graph.port_offset(port).unwrap();
319 match optype.port_kind(offset).unwrap() {
320 EdgeKind::Function(pf) => PortStyle::new(html_escape::encode_text(&format!("{pf}"))),
321 EdgeKind::Const(ty) | EdgeKind::Value(ty) => {
322 PortStyle::new(html_escape::encode_text(&format!("{ty}")))
323 }
324 EdgeKind::StateOrder => {
325 if graph.port_links(port).count() > 0 {
326 PortStyle::text("", false)
327 } else {
328 PortStyle::Hidden
329 }
330 }
331 _ => PortStyle::text("", true),
332 }
333 })
334}
335
336#[allow(clippy::type_complexity)]
338pub(in crate::hugr) fn edge_style<'a>(
339 h: &'a Hugr,
340 config: MermaidFormatter<'_>,
341) -> Box<
342 dyn FnMut(
343 <MultiPortGraph<u32, u32, u32> as LinkView>::LinkEndpoint,
344 <MultiPortGraph<u32, u32, u32> as LinkView>::LinkEndpoint,
345 ) -> EdgeStyle
346 + 'a,
347> {
348 let graph = &h.graph;
349 Box::new(move |src, tgt| {
350 let src_node = graph.port_node(src).unwrap();
351 let src_optype = h.get_optype(src_node.into());
352 let src_offset = graph.port_offset(src).unwrap();
353 let tgt_offset = graph.port_offset(tgt).unwrap();
354
355 let port_kind = src_optype.port_kind(src_offset).unwrap();
356
357 let style = match port_kind {
361 EdgeKind::StateOrder => EdgeStyle::Dotted,
362 EdgeKind::ControlFlow => EdgeStyle::Dashed,
363 EdgeKind::Const(_) | EdgeKind::Function(_) | EdgeKind::Value(_) => EdgeStyle::Solid,
364 };
365
366 fn type_label(e: EdgeKind) -> Option<String> {
368 match e {
369 EdgeKind::Const(ty) | EdgeKind::Value(ty) => Some(format!("{ty}")),
370 EdgeKind::Function(pf) => Some(format!("{pf}")),
371 _ => None,
372 }
373 }
374 let label = match (
377 config.port_offsets_in_edges,
378 type_label(port_kind).filter(|_| config.type_labels_in_edges),
379 ) {
380 (true, Some(ty)) => {
381 format!("{}:{}\n{ty}", src_offset.index(), tgt_offset.index())
382 }
383 (true, _) => format!("{}:{}", src_offset.index(), tgt_offset.index()),
384 (false, Some(ty)) => ty.to_string(),
385 _ => return style,
386 };
387 style.with_label(label)
388 })
389}
390
391#[cfg(test)]
392mod tests {
393 use crate::{NodeIndex, builder::test::simple_dfg_hugr};
394
395 use super::*;
396
397 #[cfg_attr(miri, ignore)] #[test]
399 fn test_custom_node_labels() {
400 let h = simple_dfg_hugr();
401 let node_labels = h
402 .nodes()
403 .map(|n| (n, format!("node_{}", n.index())))
404 .collect();
405 let config = h
406 .mermaid_format()
407 .with_node_labels(NodeLabel::Custom(node_labels));
408 insta::assert_snapshot!(h.mermaid_string_with_formatter(config));
409 }
410
411 #[test]
412 fn convert_full_render_config_to_render_config() {
413 let h = simple_dfg_hugr();
414 let config: MermaidFormatter =
415 MermaidFormatter::new(&h).with_node_labels(NodeLabel::Custom(HashMap::new()));
416 #[allow(deprecated)]
417 {
418 assert!(RenderConfig::try_from(config).is_err());
419 }
420 }
421}