1use crate::core::view::{EdgeId, GraphView, NodeId};
11use std::fmt::Write as _;
12
13#[derive(Debug, Clone, Default)]
18pub struct DotAttributes {
19 attributes: Vec<(String, String)>,
20}
21
22impl DotAttributes {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
30 let key = key.into();
31 let value = value.into();
32 match self.attributes.iter_mut().find(|(k, _)| *k == key) {
33 Some(slot) => slot.1 = value,
34 None => self.attributes.push((key, value)),
35 }
36 self
37 }
38
39 pub fn label(self, label: impl Into<String>) -> Self {
41 self.set("label", label)
42 }
43
44 pub fn is_empty(&self) -> bool {
46 self.attributes.is_empty()
47 }
48
49 fn render(&self) -> String {
51 self.attributes
52 .iter()
53 .map(|(key, value)| format!("{}={}", key, quote(value)))
54 .collect::<Vec<_>>()
55 .join(", ")
56 }
57}
58
59pub trait DotStyle: GraphView {
65 fn node_attributes(&self, node: NodeId) -> DotAttributes {
67 DotAttributes::new().label(node.to_string())
68 }
69
70 fn edge_attributes(&self, _edge: EdgeId) -> DotAttributes {
72 DotAttributes::new()
73 }
74
75 fn graph_attributes(&self) -> DotAttributes {
77 DotAttributes::new()
78 }
79}
80
81pub trait ToDot: GraphView {
83 fn to_dot_with<S: DotStyle>(&self, name: &str, style: &S) -> String
87 where
88 Self: Sized,
89 {
90 let (keyword, connector) = if self.is_directed() {
92 ("digraph", "->")
93 } else {
94 ("graph", "--")
95 };
96
97 let mut out = String::new();
98 let _ = writeln!(out, "{} {} {{", keyword, quote(name));
99
100 let graph_attributes = style.graph_attributes();
101 if !graph_attributes.is_empty() {
102 let _ = writeln!(out, " graph [{}];", graph_attributes.render());
103 }
104
105 for node in self.node_ids() {
106 let attributes = style.node_attributes(node);
107 if attributes.is_empty() {
108 let _ = writeln!(out, " {};", node.index());
109 } else {
110 let _ = writeln!(out, " {} [{}];", node.index(), attributes.render());
111 }
112 }
113
114 for node in self.node_ids() {
117 for link in self.links(node) {
118 if !self.is_directed() && link.target < node {
119 continue;
120 }
121 let attributes = style.edge_attributes(link.edge);
122 let line = format!(" {} {} {}", node.index(), connector, link.target.index());
123 if attributes.is_empty() {
124 let _ = writeln!(out, "{};", line);
125 } else {
126 let _ = writeln!(out, "{} [{}];", line, attributes.render());
127 }
128 }
129 }
130
131 out.push_str("}\n");
132 out
133 }
134
135 fn to_dot(&self, name: &str) -> String
137 where
138 Self: Sized + DotStyle,
139 {
140 self.to_dot_with(name, self)
141 }
142}
143
144impl<G: GraphView + ?Sized> ToDot for G {}
145
146fn quote(value: &str) -> String {
148 let mut out = String::with_capacity(value.len() + 2);
149 out.push('"');
150 for c in value.chars() {
151 match c {
152 '"' => out.push_str("\\\""),
153 '\\' => out.push_str("\\\\"),
154 '\n' => out.push_str("\\n"),
155 _ => out.push(c),
156 }
157 }
158 out.push('"');
159 out
160}
161
162#[cfg(test)]
163#[cfg(feature = "graph")]
164mod tests {
165 use super::*;
166 use crate::core::EdgeFiltered;
167 use crate::graph::Graph;
168
169 #[test]
170 fn test_an_undirected_graph_uses_the_undirected_syntax() {
171 let mut graph = Graph::<u32, &str, &str>::new(false);
172 let a = graph.add_node("A");
173 let b = graph.add_node("B");
174 graph.add_edge(a, b, 1, "rail").unwrap();
175
176 let dot = graph.to_dot("routes");
177 assert!(dot.starts_with("graph \"routes\" {\n"));
178 assert!(dot.contains("0 -- 1"));
179 assert!(!dot.contains("->"));
180 assert!(dot.ends_with("}\n"));
181 }
182
183 #[test]
184 fn test_a_directed_graph_uses_arrows() {
185 let mut graph = Graph::<u32, &str, &str>::new(true);
186 let a = graph.add_node("A");
187 let b = graph.add_node("B");
188 graph.add_edge(a, b, 1, "rail").unwrap();
189
190 let dot = graph.to_dot("flow");
191 assert!(dot.starts_with("digraph \"flow\" {\n"));
192 assert!(dot.contains("0 -> 1"));
193 }
194
195 #[test]
198 fn test_each_undirected_edge_is_emitted_once() {
199 let mut graph = Graph::<u32, &str, &str>::new(false);
200 let a = graph.add_node("A");
201 let b = graph.add_node("B");
202 graph.add_edge(a, b, 1, "rail").unwrap();
203
204 let dot = graph.to_dot("routes");
205 assert_eq!(dot.matches("--").count(), 1);
206 }
207
208 #[test]
209 fn test_parallel_edges_are_all_drawn() {
210 let mut graph = Graph::<u32, &str, &str>::new(false);
211 let a = graph.add_node("A");
212 let b = graph.add_node("B");
213 graph.add_edge(a, b, 1, "rail").unwrap();
214 graph.add_edge(a, b, 2, "road").unwrap();
215
216 assert_eq!(graph.to_dot("routes").matches("0 -- 1").count(), 2);
217 }
218
219 #[test]
220 fn test_labels_are_quoted_and_escaped() {
221 let mut graph = Graph::<u32, String, String>::new(false);
222 let a = graph.add_node("say \"hi\"".into());
223 let b = graph.add_node("two\nlines".into());
224 graph.add_edge(a, b, 1, "a\\b".into()).unwrap();
225
226 let dot = graph.to_dot_with("routes", &graph);
227 assert!(dot.contains(r#"\"hi\""#), "{}", dot);
228 assert!(dot.contains(r"two\nlines"), "{}", dot);
229 assert!(dot.contains(r"a\\b"), "{}", dot);
230 }
231
232 #[test]
233 fn test_a_filtered_view_renders_only_what_it_admits() {
234 let mut graph = Graph::<u32, &str, &str>::new(false);
235 let a = graph.add_node("A");
236 let b = graph.add_node("B");
237 let c = graph.add_node("C");
238 let rail = graph.add_edge(a, b, 1, "rail").unwrap();
239 graph.add_edge(b, c, 1, "road").unwrap();
240
241 let rail_only = EdgeFiltered::new(&graph, move |edge| edge == rail);
242 let dot = rail_only.to_dot_with("rail", &graph);
243 assert_eq!(dot.matches("--").count(), 1);
244 assert!(dot.contains(" 2 ["));
246 }
247
248 #[test]
249 fn test_an_empty_graph_still_produces_a_valid_document() {
250 let graph = Graph::<u32, &str, &str>::new(false);
251 assert_eq!(graph.to_dot("empty"), "graph \"empty\" {\n}\n");
252 }
253}