guppy/petgraph_support/
dot.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use petgraph::{
5    prelude::*,
6    visit::{GraphProp, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef},
7};
8use std::fmt::{self, Write};
9
10static INDENT: &str = "    ";
11
12/// A visitor interface for formatting graph labels.
13pub trait DotVisitor<NR, ER> {
14    /// Visits this node. The implementation may output a label for this node to the given
15    /// `DotWrite`.
16    fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result;
17
18    /// Visits this edge. The implementation may output a label for this edge to the given
19    /// `DotWrite`.
20    fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result;
21
22    // TODO: allow more customizations? more labels, colors etc to be set?
23}
24
25/// A visitor for formatting graph labels that outputs `fmt::Display` impls for node and edge
26/// weights.
27///
28/// This visitor will escape backslashes.
29#[derive(Copy, Clone, Debug)]
30pub struct DisplayVisitor;
31
32impl<NR, ER> DotVisitor<NR, ER> for DisplayVisitor
33where
34    NR: NodeRef,
35    ER: EdgeRef,
36    NR::Weight: fmt::Display,
37    ER::Weight: fmt::Display,
38{
39    fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result {
40        write!(f, "{}", node.weight())
41    }
42
43    fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result {
44        write!(f, "{}", edge.weight())
45    }
46}
47
48impl<NR, ER, T> DotVisitor<NR, ER> for &T
49where
50    T: DotVisitor<NR, ER>,
51{
52    fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result {
53        (*self).visit_node(node, f)
54    }
55
56    fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result {
57        (*self).visit_edge(edge, f)
58    }
59}
60
61#[derive(Clone, Debug)]
62pub struct DotFmt<G, V> {
63    graph: G,
64    visitor: V,
65}
66
67impl<G, V> DotFmt<G, V>
68where
69    for<'a> &'a G: IntoEdgeReferences + IntoNodeReferences + GraphProp + NodeIndexable,
70    for<'a> V:
71        DotVisitor<<&'a G as IntoNodeReferences>::NodeRef, <&'a G as IntoEdgeReferences>::EdgeRef>,
72{
73    /// Creates a new formatter for this graph.
74    #[allow(dead_code)]
75    pub fn new(graph: G, visitor: V) -> Self {
76        Self { graph, visitor }
77    }
78
79    /// Outputs a graphviz-compatible representation of this graph to the given formatter.
80    pub fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        writeln!(f, "{} {{", graph_type(&self.graph))?;
82
83        for node in self.graph.node_references() {
84            write!(
85                f,
86                "{}{} [label=\"",
87                INDENT,
88                (&self.graph).to_index(node.id())
89            )?;
90            self.visitor.visit_node(node, &mut DotWrite::new(f))?;
91            writeln!(f, "\"]")?;
92        }
93
94        let edge_str = edge_str(&self.graph);
95        for edge in self.graph.edge_references() {
96            write!(
97                f,
98                "{}{} {} {} [label=\"",
99                INDENT,
100                (&self.graph).to_index(edge.source()),
101                edge_str,
102                (&self.graph).to_index(edge.target())
103            )?;
104            self.visitor.visit_edge(edge, &mut DotWrite::new(f))?;
105            writeln!(f, "\"]")?;
106        }
107
108        writeln!(f, "}}")
109    }
110}
111
112impl<G, V> fmt::Display for DotFmt<G, V>
113where
114    for<'a> &'a G: IntoEdgeReferences + IntoNodeReferences + GraphProp + NodeIndexable,
115    for<'a> V:
116        DotVisitor<<&'a G as IntoNodeReferences>::NodeRef, <&'a G as IntoEdgeReferences>::EdgeRef>,
117{
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        self.fmt(f)
120    }
121}
122
123/// A write target for `Dot` graphs. Use with the `write!` macro.
124pub struct DotWrite<'a, 'b> {
125    f: &'a mut fmt::Formatter<'b>,
126    escape_backslashes: bool,
127}
128
129impl<'a, 'b> DotWrite<'a, 'b> {
130    fn new(f: &'a mut fmt::Formatter<'b>) -> Self {
131        Self {
132            f,
133            escape_backslashes: true,
134        }
135    }
136
137    /// Sets a config option for whether backslashes should be escaped. Defaults to `true`.
138    ///
139    /// This can be set to `false` if the visitor knows to output graphviz control characters.
140    #[allow(dead_code)]
141    pub fn set_escape_backslashes(&mut self, escape_backslashes: bool) {
142        self.escape_backslashes = escape_backslashes;
143    }
144
145    /// Glue for usage of the `write!` macro.
146    ///
147    /// This method should generally not be invoked manually, but rather through `write!` or similar
148    /// macros (`println!`, `format!` etc).
149    ///
150    /// Defining this inherent method allows `write!` to work without callers needing to import the
151    /// `std::fmt::Write` trait.
152    pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
153        // Forward to the fmt::Write impl.
154        Write::write_fmt(self, args)
155    }
156}
157
158impl Write for DotWrite<'_, '_> {
159    fn write_str(&mut self, s: &str) -> fmt::Result {
160        for c in s.chars() {
161            self.write_char(c)?;
162        }
163        Ok(())
164    }
165
166    fn write_char(&mut self, c: char) -> fmt::Result {
167        match c {
168            '"' => self.f.write_str(r#"\""#),
169            // \l is for left-justified newlines (\n means center-justified newlines)
170            '\n' => self.f.write_str(r"\l"),
171            // Backslashes are only escaped if the config is set.
172            '\\' if self.escape_backslashes => self.f.write_str(r"\\"),
173            // Other escapes like backslashes are passed through.
174            c => self.f.write_char(c),
175        }
176    }
177}
178
179fn graph_type<G: GraphProp>(graph: G) -> &'static str {
180    if graph.is_directed() {
181        "digraph"
182    } else {
183        "graph"
184    }
185}
186
187fn edge_str<G: GraphProp>(graph: G) -> &'static str {
188    if graph.is_directed() { "->" } else { "--" }
189}