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]
135 pub fn with_attr_getters(
136 graph: G,
137 config: &'a [Config],
138 get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
139 get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
140 ) -> Self {
141 let config = Configs::extract(config);
142 Dot {
143 graph,
144 get_edge_attributes,
145 get_node_attributes,
146 config,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum RankDir {
156 TB,
158 BT,
160 LR,
162 RL,
164}
165
166#[non_exhaustive]
170#[derive(Debug, PartialEq, Eq)]
171pub enum Config {
172 NodeIndexLabel,
174 EdgeIndexLabel,
176 EdgeNoLabel,
178 NodeNoLabel,
180 GraphContentOnly,
182 RankDir(RankDir),
184}
185macro_rules! make_config_struct {
186 ($($variant:ident,)*) => {
187 #[allow(non_snake_case)]
188 #[derive(Default)]
189 struct Configs {
190 $($variant: bool,)*
191 RankDir: Option<RankDir>,
192 }
193 impl Configs {
194 #[inline]
195 fn extract(configs: &[Config]) -> Self {
196 let mut conf = Self::default();
197 for c in configs {
198 match c {
199 $(Config::$variant => conf.$variant = true,)*
200 Config::RankDir(dir) => conf.RankDir = Some(*dir),
201 }
202 }
203 conf
204 }
205 }
206 }
207}
208make_config_struct!(
209 NodeIndexLabel,
210 EdgeIndexLabel,
211 EdgeNoLabel,
212 NodeNoLabel,
213 GraphContentOnly,
214);
215
216impl<G> Dot<'_, G>
218where
219 G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp,
220{
221 pub fn graph_fmt<NF, EF>(
222 &self,
223 f: &mut fmt::Formatter,
224 node_fmt: NF,
225 edge_fmt: EF,
226 ) -> fmt::Result
227 where
228 NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result,
229 EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result,
230 {
231 let g = self.graph;
232 if !self.config.GraphContentOnly {
233 writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?;
234 }
235
236 if let Some(rank_dir) = &self.config.RankDir {
237 let value = match rank_dir {
238 RankDir::TB => "TB",
239 RankDir::BT => "BT",
240 RankDir::LR => "LR",
241 RankDir::RL => "RL",
242 };
243 writeln!(f, "{INDENT}rankdir=\"{value}\"")?;
244 }
245
246 for node in g.node_references() {
248 write!(f, "{}{} [ ", INDENT, g.to_index(node.id()),)?;
249 if !self.config.NodeNoLabel {
250 write!(f, "label = \"")?;
251 if self.config.NodeIndexLabel {
252 write!(f, "{}", g.to_index(node.id()))?;
253 } else {
254 Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?;
255 }
256 write!(f, "\" ")?;
257 }
258 writeln!(f, "{}]", (self.get_node_attributes)(g, node))?;
259 }
260 for (i, edge) in g.edge_references().enumerate() {
262 write!(
263 f,
264 "{}{} {} {} [ ",
265 INDENT,
266 g.to_index(edge.source()),
267 EDGE[g.is_directed() as usize],
268 g.to_index(edge.target()),
269 )?;
270 if !self.config.EdgeNoLabel {
271 write!(f, "label = \"")?;
272 if self.config.EdgeIndexLabel {
273 write!(f, "{i}")?;
274 } else {
275 Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?;
276 }
277 write!(f, "\" ")?;
278 }
279 writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?;
280 }
281
282 if !self.config.GraphContentOnly {
283 writeln!(f, "}}")?;
284 }
285 Ok(())
286 }
287}
288
289impl<G> fmt::Display for Dot<'_, G>
290where
291 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
292 G::EdgeWeight: fmt::Display,
293 G::NodeWeight: fmt::Display,
294{
295 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
296 self.graph_fmt(f, fmt::Display::fmt, fmt::Display::fmt)
297 }
298}
299
300impl<G> fmt::LowerHex for Dot<'_, G>
301where
302 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
303 G::EdgeWeight: fmt::LowerHex,
304 G::NodeWeight: fmt::LowerHex,
305{
306 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
307 self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt)
308 }
309}
310
311impl<G> fmt::UpperHex for Dot<'_, G>
312where
313 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
314 G::EdgeWeight: fmt::UpperHex,
315 G::NodeWeight: fmt::UpperHex,
316{
317 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
318 self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt)
319 }
320}
321
322impl<G> fmt::Debug for Dot<'_, G>
323where
324 G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
325 G::EdgeWeight: fmt::Debug,
326 G::NodeWeight: fmt::Debug,
327{
328 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
329 self.graph_fmt(f, fmt::Debug::fmt, fmt::Debug::fmt)
330 }
331}
332
333struct Escaper<W>(W);
335
336impl<W> fmt::Write for Escaper<W>
337where
338 W: fmt::Write,
339{
340 fn write_str(&mut self, s: &str) -> fmt::Result {
341 for c in s.chars() {
342 self.write_char(c)?;
343 }
344 Ok(())
345 }
346
347 fn write_char(&mut self, c: char) -> fmt::Result {
348 match c {
349 '"' | '\\' => self.0.write_char('\\')?,
350 '\n' => return self.0.write_str("\\l"),
352 _ => {}
353 }
354 self.0.write_char(c)
355 }
356}
357
358struct Escaped<T>(T);
360
361impl<T> fmt::Display for Escaped<T>
362where
363 T: fmt::Display,
364{
365 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366 if f.alternate() {
367 writeln!(&mut Escaper(f), "{:#}", &self.0)
368 } else {
369 write!(&mut Escaper(f), "{}", &self.0)
370 }
371 }
372}
373
374struct FnFmt<'a, T, F>(&'a T, F);
376
377impl<'a, T, F> fmt::Display for FnFmt<'a, T, F>
378where
379 F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result,
380{
381 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
382 self.1(self.0, f)
383 }
384}
385
386#[cfg(feature = "dot_parser")]
387#[macro_use]
388pub mod dot_parser;
389
390#[cfg(test)]
391mod test {
392 use alloc::{format, string::String};
393 use core::fmt::Write;
394
395 use super::{Config, Dot, Escaper, RankDir};
396 use crate::prelude::Graph;
397 use crate::visit::NodeRef;
398
399 #[test]
400 fn test_escape() {
401 let mut buff = String::new();
402 {
403 let mut e = Escaper(&mut buff);
404 let _ = e.write_str("\" \\ \n");
405 }
406 assert_eq!(buff, "\\\" \\\\ \\l");
407 }
408
409 fn simple_graph() -> Graph<&'static str, &'static str> {
410 let mut graph = Graph::<&str, &str>::new();
411 let a = graph.add_node("A");
412 let b = graph.add_node("B");
413 graph.add_edge(a, b, "edge_label");
414 graph
415 }
416
417 #[test]
418 fn test_nodeindexlable_option() {
419 let graph = simple_graph();
420 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeIndexLabel]));
421 assert_eq!(dot, "digraph {\n 0 [ label = \"0\" ]\n 1 [ label = \"1\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n");
422 }
423
424 #[test]
425 fn test_edgeindexlable_option() {
426 let graph = simple_graph();
427 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeIndexLabel]));
428 assert_eq!(dot, "digraph {\n 0 [ label = \"\\\"A\\\"\" ]\n 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"0\" ]\n}\n");
429 }
430
431 #[test]
432 fn test_edgenolable_option() {
433 let graph = simple_graph();
434 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
435 assert_eq!(dot, "digraph {\n 0 [ label = \"\\\"A\\\"\" ]\n 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ ]\n}\n");
436 }
437
438 #[test]
439 fn test_nodenolable_option() {
440 let graph = simple_graph();
441 let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeNoLabel]));
442 assert_eq!(
443 dot,
444 "digraph {\n 0 [ ]\n 1 [ ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
445 );
446 }
447
448 #[test]
449 fn test_rankdir_bt_option() {
450 let graph = simple_graph();
451 let dot = format!(
452 "{:?}",
453 Dot::with_config(&graph, &[Config::RankDir(RankDir::TB)])
454 );
455 assert_eq!(
456 dot,
457 "digraph {\n rankdir=\"TB\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
458 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
459 );
460 }
461
462 #[test]
463 fn test_rankdir_tb_option() {
464 let graph = simple_graph();
465 let dot = format!(
466 "{:?}",
467 Dot::with_config(&graph, &[Config::RankDir(RankDir::BT)])
468 );
469 assert_eq!(
470 dot,
471 "digraph {\n rankdir=\"BT\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
472 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
473 );
474 }
475
476 #[test]
477 fn test_rankdir_lr_option() {
478 let graph = simple_graph();
479 let dot = format!(
480 "{:?}",
481 Dot::with_config(&graph, &[Config::RankDir(RankDir::LR)])
482 );
483 assert_eq!(
484 dot,
485 "digraph {\n rankdir=\"LR\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
486 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
487 );
488 }
489
490 #[test]
491 fn test_rankdir_rl_option() {
492 let graph = simple_graph();
493 let dot = format!(
494 "{:?}",
495 Dot::with_config(&graph, &[Config::RankDir(RankDir::RL)])
496 );
497 assert_eq!(
498 dot,
499 "digraph {\n rankdir=\"RL\"\n 0 [ label = \"\\\"A\\\"\" ]\n \
500 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
501 );
502 }
503
504 #[test]
505 fn test_with_attr_getters() {
506 let graph = simple_graph();
507 let dot = format!(
508 "{:?}",
509 Dot::with_attr_getters(
510 &graph,
511 &[Config::NodeNoLabel, Config::EdgeNoLabel],
512 &|_, er| format!("label = \"{}\"", er.weight().to_uppercase()),
513 &|_, nr| format!("label = \"{}\"", nr.weight().to_lowercase()),
514 ),
515 );
516 assert_eq!(dot, "digraph {\n 0 [ label = \"a\"]\n 1 [ label = \"b\"]\n 0 -> 1 [ label = \"EDGE_LABEL\"]\n}\n");
517 }
518}