1use alloc::string::String;
4use core::fmt::{self, Display, Write};
5
6use crate::visit::{
7 EdgeRef, GraphProp, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef,
8};
9
10pub struct Dot<'a, G>
52where
53 G: IntoEdgeReferences + IntoNodeReferences,
54{
55 graph: G,
56 get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
57 get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
58 config: Configs,
59}
60
61static TYPE: [&str; 2] = ["graph", "digraph"];
62static EDGE: [&str; 2] = ["--", "->"];
63static INDENT: &str = " ";
64
65impl<'a, G> Dot<'a, G>
66where
67 G: IntoNodeReferences + IntoEdgeReferences,
68{
69 #[inline]
71 pub fn new(graph: G) -> Self {
72 Self::with_config(graph, &[])
73 }
74
75 #[inline]
77 pub fn with_config(graph: G, config: &'a [Config]) -> Self {
78 Self::with_attr_getters(graph, config, &|_, _| String::new(), &|_, _| String::new())
79 }
80
81 #[inline]
82 pub fn with_attr_getters(
83 graph: G,
84 config: &'a [Config],
85 get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
86 get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
87 ) -> Self {
88 let config = Configs::extract(config);
89 Dot {
90 graph,
91 get_edge_attributes,
92 get_node_attributes,
93 config,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum RankDir {
103 TB,
105 BT,
107 LR,
109 RL,
111}
112
113#[non_exhaustive]
117#[derive(Debug, PartialEq, Eq)]
118pub enum Config {
119 NodeIndexLabel,
121 EdgeIndexLabel,
123 EdgeNoLabel,
125 NodeNoLabel,
127 GraphContentOnly,
129 RankDir(RankDir),
131}
132macro_rules! make_config_struct {
133 ($($variant:ident,)*) => {
134 #[allow(non_snake_case)]
135 #[derive(Default)]
136 struct Configs {
137 $($variant: bool,)*
138 RankDir: Option<RankDir>,
139 }
140 impl Configs {
141 #[inline]
142 fn extract(configs: &[Config]) -> Self {
143 let mut conf = Self::default();
144 for c in configs {
145 match c {
146 $(Config::$variant => conf.$variant = true,)*
147 Config::RankDir(dir) => conf.RankDir = Some(*dir),
148 }
149 }
150 conf
151 }
152 }
153 }
154}
155make_config_struct!(
156 NodeIndexLabel,
157 EdgeIndexLabel,
158 EdgeNoLabel,
159 NodeNoLabel,
160 GraphContentOnly,
161);
162
163impl<G> Dot<'_, G>
164where
165 G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp,
166{
167 fn graph_fmt<NF, EF>(&self, f: &mut fmt::Formatter, node_fmt: NF, edge_fmt: EF) -> fmt::Result
168 where
169 NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result,
170 EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result,
171 {
172 let g = self.graph;
173 if !self.config.GraphContentOnly {
174 writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?;
175 }
176
177 if let Some(rank_dir) = &self.config.RankDir {
178 let value = match rank_dir {
179 RankDir::TB => "TB",
180 RankDir::BT => "BT",
181 RankDir::LR => "LR",
182 RankDir::RL => "RL",
183 };
184 writeln!(f, "{}rankdir=\"{}\"", INDENT, value)?;
185 }
186
187 for node in g.node_references() {
189 write!(f, "{}{} [ ", INDENT, g.to_index(node.id()),)?;
190 if !self.config.NodeNoLabel {
191 write!(f, "label = \"")?;
192 if self.config.NodeIndexLabel {
193 write!(f, "{}", g.to_index(node.id()))?;
194 } else {
195 Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?;
196 }
197 write!(f, "\" ")?;
198 }
199 writeln!(f, "{}]", (self.get_node_attributes)(g, node))?;
200 }
201 for (i, edge) in g.edge_references().enumerate() {
203 write!(
204 f,
205 "{}{} {} {} [ ",
206 INDENT,
207 g.to_index(edge.source()),
208 EDGE[g.is_directed() as usize],
209 g.to_index(edge.target()),
210 )?;
211 if !self.config.EdgeNoLabel {
212 write!(f, "label = \"")?;
213 if self.config.EdgeIndexLabel {
214 write!(f, "{}", i)?;
215 } else {
216 Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?;
217 }
218 write!(f, "\" ")?;
219 }
220 writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?;
221 }
222
223 if !self.config.GraphContentOnly {
224 writeln!(f, "}}")?;
225 }
226 Ok(())
227 }
228}
229
230impl<G> fmt::Display for Dot<'_, G>
231where
232 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
233 G::EdgeWeight: fmt::Display,
234 G::NodeWeight: fmt::Display,
235{
236 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
237 self.graph_fmt(f, fmt::Display::fmt, fmt::Display::fmt)
238 }
239}
240
241impl<G> fmt::LowerHex for Dot<'_, G>
242where
243 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
244 G::EdgeWeight: fmt::LowerHex,
245 G::NodeWeight: fmt::LowerHex,
246{
247 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
248 self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt)
249 }
250}
251
252impl<G> fmt::UpperHex for Dot<'_, G>
253where
254 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
255 G::EdgeWeight: fmt::UpperHex,
256 G::NodeWeight: fmt::UpperHex,
257{
258 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
259 self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt)
260 }
261}
262
263impl<G> fmt::Debug for Dot<'_, G>
264where
265 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
266 G::EdgeWeight: fmt::Debug,
267 G::NodeWeight: fmt::Debug,
268{
269 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
270 self.graph_fmt(f, fmt::Debug::fmt, fmt::Debug::fmt)
271 }
272}
273
274struct Escaper<W>(W);
276
277impl<W> fmt::Write for Escaper<W>
278where
279 W: fmt::Write,
280{
281 fn write_str(&mut self, s: &str) -> fmt::Result {
282 for c in s.chars() {
283 self.write_char(c)?;
284 }
285 Ok(())
286 }
287
288 fn write_char(&mut self, c: char) -> fmt::Result {
289 match c {
290 '"' | '\\' => self.0.write_char('\\')?,
291 '\n' => return self.0.write_str("\\l"),
293 _ => {}
294 }
295 self.0.write_char(c)
296 }
297}
298
299struct Escaped<T>(T);
301
302impl<T> fmt::Display for Escaped<T>
303where
304 T: fmt::Display,
305{
306 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
307 if f.alternate() {
308 writeln!(&mut Escaper(f), "{:#}", &self.0)
309 } else {
310 write!(&mut Escaper(f), "{}", &self.0)
311 }
312 }
313}
314
315struct FnFmt<'a, T, F>(&'a T, F);
317
318impl<'a, T, F> fmt::Display for FnFmt<'a, T, F>
319where
320 F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result,
321{
322 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
323 self.1(self.0, f)
324 }
325}
326
327#[cfg(test)]
328mod test {
329 use alloc::{format, string::String};
330 use core::fmt::Write;
331
332 use super::{Config, Dot, Escaper, RankDir};
333 use crate::prelude::Graph;
334 use crate::visit::NodeRef;
335
336 #[test]
337 fn test_escape() {
338 let mut buff = String::new();
339 {
340 let mut e = Escaper(&mut buff);
341 let _ = e.write_str("\" \\ \n");
342 }
343 assert_eq!(buff, "\\\" \\\\ \\l");
344 }
345
346 fn simple_graph() -> Graph<&'static str, &'static str> {
347 let mut graph = Graph::<&str, &str>::new();
348 let a = graph.add_node("A");
349 let b = graph.add_node("B");
350 graph.add_edge(a, b, "edge_label");
351 graph
352 }
353
354 #[test]
355 fn test_nodeindexlable_option() {
356 let graph = simple_graph();
357 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeIndexLabel]));
358 assert_eq!(dot, "digraph {\n 0 [ label = \"0\" ]\n 1 [ label = \"1\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n");
359 }
360
361 #[test]
362 fn test_edgeindexlable_option() {
363 let graph = simple_graph();
364 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeIndexLabel]));
365 assert_eq!(dot, "digraph {\n 0 [ label = \"\\\"A\\\"\" ]\n 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"0\" ]\n}\n");
366 }
367
368 #[test]
369 fn test_edgenolable_option() {
370 let graph = simple_graph();
371 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
372 assert_eq!(dot, "digraph {\n 0 [ label = \"\\\"A\\\"\" ]\n 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ ]\n}\n");
373 }
374
375 #[test]
376 fn test_nodenolable_option() {
377 let graph = simple_graph();
378 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeNoLabel]));
379 assert_eq!(
380 dot,
381 "digraph {\n 0 [ ]\n 1 [ ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
382 );
383 }
384
385 #[test]
386 fn test_rankdir_bt_option() {
387 let graph = simple_graph();
388 let dot = format!(
389 "{:?}",
390 Dot::with_config(&graph, &[Config::RankDir(RankDir::TB)])
391 );
392 assert_eq!(
393 dot,
394 "digraph {\n rankdir=\"TB\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
395 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
396 );
397 }
398
399 #[test]
400 fn test_rankdir_tb_option() {
401 let graph = simple_graph();
402 let dot = format!(
403 "{:?}",
404 Dot::with_config(&graph, &[Config::RankDir(RankDir::BT)])
405 );
406 assert_eq!(
407 dot,
408 "digraph {\n rankdir=\"BT\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
409 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
410 );
411 }
412
413 #[test]
414 fn test_rankdir_lr_option() {
415 let graph = simple_graph();
416 let dot = format!(
417 "{:?}",
418 Dot::with_config(&graph, &[Config::RankDir(RankDir::LR)])
419 );
420 assert_eq!(
421 dot,
422 "digraph {\n rankdir=\"LR\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
423 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
424 );
425 }
426
427 #[test]
428 fn test_rankdir_rl_option() {
429 let graph = simple_graph();
430 let dot = format!(
431 "{:?}",
432 Dot::with_config(&graph, &[Config::RankDir(RankDir::RL)])
433 );
434 assert_eq!(
435 dot,
436 "digraph {\n rankdir=\"RL\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
437 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
438 );
439 }
440
441 #[test]
442 fn test_with_attr_getters() {
443 let graph = simple_graph();
444 let dot = format!(
445 "{:?}",
446 Dot::with_attr_getters(
447 &graph,
448 &[Config::NodeNoLabel, Config::EdgeNoLabel],
449 &|_, er| format!("label = \"{}\"", er.weight().to_uppercase()),
450 &|_, nr| format!("label = \"{}\"", nr.weight().to_lowercase()),
451 ),
452 );
453 assert_eq!(dot, "digraph {\n 0 [ label = \"a\"]\n 1 [ label = \"b\"]\n 0 -> 1 [ label = \"EDGE_LABEL\"]\n}\n");
454 }
455}