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