xdot/
layout.rs

1use graphviz_rust::{
2    cmd::{CommandArg, Format, Layout},
3    dot_structures::{Attribute, Graph, Id},
4    printer::PrinterContext,
5};
6use nom::{Finish, error::Error as NomError};
7use thiserror::Error;
8
9mod graph_ext;
10
11use self::graph_ext::{Elem, GraphExt};
12use super::{
13    ATTR_NAMES,
14    xdot_parse::{ShapeDraw, parse},
15};
16
17/// Error wrapping possible errors that can occur when running [draw_graph].
18#[derive(Error, Debug)]
19pub enum LayoutError {
20    #[error("failed to run xdot")]
21    Layout(#[from] std::io::Error),
22    #[error("failed to parse dot")]
23    Decode(std::string::FromUtf8Error),
24    #[error("failed to parse dot")]
25    ParseDot(String),
26    #[error("failed to parse xdot attributes")]
27    ParseXDot(#[from] NomError<String>),
28}
29impl From<NomError<&str>> for LayoutError {
30    fn from(e: NomError<&str>) -> Self {
31        nom2owned(e).into()
32    }
33}
34fn nom2owned(e: NomError<&str>) -> NomError<String> {
35    NomError {
36        input: e.input.to_owned(),
37        code: e.code,
38    }
39}
40
41/// Run `xdot` layout algorithm on a [Graph](graphviz_rust::dot_structures::Graph) and extract all [ShapeDraw] operations.
42pub fn layout_and_draw_graph(graph: Graph) -> Result<Vec<ShapeDraw>, LayoutError> {
43    let layed_out = layout_graph(graph)?;
44    Ok(draw_graph(layed_out)?)
45}
46
47fn layout_graph(graph: Graph) -> Result<Graph, LayoutError> {
48    let mut ctx = PrinterContext::default();
49    let layed_out = graphviz_rust::exec(
50        graph,
51        &mut ctx,
52        vec![
53            CommandArg::Layout(Layout::Dot),
54            CommandArg::Format(Format::Xdot),
55        ],
56    )?;
57    let layed_out = String::from_utf8(layed_out).map_err(LayoutError::Decode)?;
58    // println!("{}", &layed_out);
59    graphviz_rust::parse(&layed_out).map_err(LayoutError::ParseDot)
60}
61
62/// Extract [ShapeDraw] operations from a graph annotated with `xdot` draw attributes.
63pub fn draw_graph(graph: Graph) -> Result<Vec<ShapeDraw>, NomError<String>> {
64    Ok(graph
65        .iter_elems()
66        .map(handle_elem)
67        .collect::<Result<Vec<_>, _>>()
68        .map_err(nom2owned)?
69        .into_iter()
70        .flatten()
71        .collect::<Vec<_>>())
72}
73
74fn handle_elem(elem: Elem) -> Result<Vec<ShapeDraw>, NomError<&str>> {
75    let attributes: &[Attribute] = match elem {
76        Elem::Edge(edge) => edge.attributes.as_ref(),
77        Elem::Node(node) => node.attributes.as_ref(),
78    };
79    let mut shapes = vec![];
80    for attr in attributes.iter() {
81        if let Id::Plain(ref attr_name) = attr.0 {
82            if !ATTR_NAMES.contains(&attr_name.as_str()) {
83                continue;
84            }
85            if let Id::Escaped(ref attr_val_raw) = attr.1 {
86                let attr_val = dot_unescape(attr_val_raw)?;
87                dbg!(&attr_name, &attr_val);
88                let mut new = parse(attr_val)?;
89                shapes.append(&mut new);
90            }
91        }
92    }
93    Ok(shapes)
94}
95
96fn dot_unescape(input: &str) -> Result<&str, NomError<&str>> {
97    use nom::{
98        bytes::complete::{tag, take_while},
99        combinator::eof,
100        sequence::{delimited, terminated},
101    };
102    // TODO: actually unescape
103    let (_, inner) = terminated(
104        delimited(tag("\""), take_while(|c| c != '\\' && c != '\"'), tag("\"")),
105        eof,
106    )(input)
107    .finish()?;
108    Ok(inner)
109}
110
111#[test]
112fn test_dot_unescape() {
113    assert_eq!(dot_unescape("\"\""), Ok(""));
114    assert_eq!(dot_unescape("\"xy\""), Ok("xy"));
115    assert!(dot_unescape("\"\"\"").is_err());
116    assert!(dot_unescape("\"\\\"").is_err()); // so far no actual escape support
117}