layout/gv/
builder.rs

1//! A graph builder that converts parsed AST trees to graphs.
2
3use super::record::record_builder;
4use crate::adt::dag::NodeHandle;
5use crate::adt::map::ScopedMap;
6use crate::core::base::Orientation;
7use crate::core::color::Color;
8use crate::core::style::*;
9use crate::gv::parser::ast;
10use crate::std_shapes::render::get_shape_size;
11use crate::std_shapes::shapes::ShapeKind;
12use crate::std_shapes::shapes::*;
13use crate::topo::layout::VisualGraph;
14use std::collections::HashMap;
15
16type PropertyList = HashMap<String, String>;
17
18// The methods in this file are responsible for converting the parsed Graphviz
19// AST into the VisualGraph data-structure that we use for layout and rendering
20// of the graph.
21
22#[derive(Debug)]
23struct EdgeDesc {
24    from: String,
25    to: String,
26    props: PropertyList,
27    is_directed: bool,
28    from_port: Option<String>,
29    to_port: Option<String>,
30}
31
32/// This class constructs a visual graph from the parsed AST.
33#[derive(Debug)]
34pub struct GraphBuilder {
35    // This records the state of the top-level graph.
36    global_state: PropertyList,
37    // This keeps track of the construction order of the nodes, because
38    // hashmap does not maintain a persistent iteration order.
39    node_order: Vec<String>,
40    // Maps node names to their property list.
41    nodes: HashMap<String, PropertyList>,
42    // A list of edge properties.
43    edges: Vec<EdgeDesc>,
44    /// Scopes that maintain the property list that changes as we enter and
45    /// leave different regions of the graph.
46    global_attr: ScopedMap<String, String>,
47    node_attr: ScopedMap<String, String>,
48    edge_attr: ScopedMap<String, String>,
49}
50impl Default for GraphBuilder {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl GraphBuilder {
57    pub fn new() -> Self {
58        Self {
59            global_state: PropertyList::new(),
60            node_order: Vec::new(),
61            nodes: HashMap::new(),
62            edges: Vec::new(),
63            global_attr: ScopedMap::new(),
64            node_attr: ScopedMap::new(),
65            edge_attr: ScopedMap::new(),
66        }
67    }
68    pub fn visit_graph(&mut self, graph: &ast::Graph) {
69        self.global_attr.push();
70        self.node_attr.push();
71        self.edge_attr.push();
72        for stmt in &graph.list.list {
73            self.visit_stmt(stmt);
74        }
75
76        // TODO: we dump the property list when we close the scope. This is not
77        // correct for sub graphs.
78        self.global_state = self.global_attr.flatten();
79
80        self.global_attr.pop();
81        self.node_attr.pop();
82        self.edge_attr.pop();
83    }
84    fn visit_stmt(&mut self, stmt: &ast::Stmt) {
85        match stmt {
86            ast::Stmt::Edge(e) => {
87                self.visit_edge(e);
88            }
89            ast::Stmt::Node(n) => {
90                self.visit_node(n);
91            }
92            ast::Stmt::Attribute(a) => {
93                self.visit_att(a);
94            }
95            ast::Stmt::SubGraph(g) => {
96                self.visit_graph(g);
97            }
98        }
99    }
100
101    fn visit_edge(&mut self, e: &ast::EdgeStmt) {
102        self.edge_attr.push();
103
104        for att in e.list.iter() {
105            self.edge_attr.insert(&att.0, &att.1);
106        }
107
108        self.init_node_with_name(&e.from.name, false);
109
110        let mut prev = &e.from.name;
111        for dest in &e.to {
112            let curr = &dest.0.name;
113            self.init_node_with_name(curr, false);
114
115            let has_arrow = matches!(dest.1, ast::ArrowKind::Arrow);
116            let prop_list = self.edge_attr.flatten();
117
118            let edge = EdgeDesc {
119                from: prev.clone(),
120                to: curr.clone(),
121                props: prop_list,
122                is_directed: has_arrow,
123                from_port: e.from.port.clone(),
124                to_port: dest.0.port.clone(),
125            };
126            self.edges.push(edge);
127            prev = curr;
128        }
129        self.edge_attr.pop();
130    }
131
132    // If \p overwrite is set then we are declaring a node. This means that
133    // we need to update the properties that already exist.
134    fn init_node_with_name(&mut self, name: &str, overwrite: bool) {
135        let node_attr = self.node_attr.flatten();
136
137        if let Option::Some(prop_list) = self.nodes.get_mut(name) {
138            if !overwrite {
139                return;
140            }
141            for p in node_attr {
142                prop_list.insert(p.0, p.1);
143            }
144        } else {
145            self.node_order.push(name.to_string());
146            self.nodes.insert(name.to_string(), node_attr);
147        }
148    }
149
150    fn visit_node(&mut self, n: &ast::NodeStmt) {
151        self.node_attr.push();
152
153        for att in n.list.iter() {
154            self.node_attr.insert(&att.0, &att.1);
155        }
156
157        self.init_node_with_name(&n.id.name, true);
158        self.node_attr.pop();
159    }
160
161    fn visit_att(&mut self, att: &ast::AttrStmt) {
162        match att.target {
163            ast::AttrStmtTarget::Graph => {
164                for att in att.list.iter() {
165                    self.global_attr.insert(&att.0, &att.1);
166                }
167            }
168            ast::AttrStmtTarget::Node => {
169                for att in att.list.iter() {
170                    self.node_attr.insert(&att.0, &att.1);
171                }
172            }
173            ast::AttrStmtTarget::Edge => {
174                for att in att.list.iter() {
175                    self.edge_attr.insert(&att.0, &att.1);
176                }
177            }
178        }
179    }
180
181    pub fn get(&self) -> VisualGraph {
182        let mut dir = Orientation::TopToBottom;
183
184        // Set the graph orientation based on the 'rankdir' property.
185        if let Option::Some(rd) = self.global_state.get("rankdir") {
186            if rd == "LR" {
187                dir = Orientation::LeftToRight;
188            }
189        }
190
191        let mut vg = VisualGraph::new(dir);
192
193        // Keeps track of the newly created nodes and indexes them by name.
194        let mut node_map: HashMap<String, NodeHandle> = HashMap::new();
195
196        assert_eq!(self.nodes.len(), self.node_order.len());
197
198        // Create and register all of the nodes.
199        for node_name in self.node_order.iter() {
200            let node_prop = self.nodes.get(node_name).unwrap();
201
202            let shape =
203                Self::get_shape_from_attributes(dir, node_prop, node_name);
204            let handle = vg.add_node(shape);
205            node_map.insert(node_name.to_string(), handle);
206        }
207
208        // Create and register all of the edges.
209        for edge_prop in &self.edges {
210            let shape = Self::get_arrow_from_attributes(
211                &edge_prop.props,
212                edge_prop.is_directed,
213                edge_prop.from_port.clone(),
214                edge_prop.to_port.clone(),
215            );
216            let from = node_map.get(&edge_prop.from).unwrap();
217            let to = node_map.get(&edge_prop.to).unwrap();
218            vg.add_edge(shape, *from, *to);
219        }
220
221        vg
222    }
223
224    fn get_arrow_from_attributes(
225        lst: &PropertyList,
226        has_arrow: bool,
227        from_port: Option<String>,
228        to_port: Option<String>,
229    ) -> Arrow {
230        let mut line_width = 1;
231        let mut font_size: usize = 14;
232        let start = LineEndKind::None;
233        let end = if has_arrow {
234            LineEndKind::Arrow
235        } else {
236            LineEndKind::None
237        };
238        let mut label = String::from("");
239        let mut color = String::from("black");
240        let mut line_style = LineStyleKind::Normal;
241
242        if let Option::Some(val) = lst.get(&"label".to_string()) {
243            label = val.clone();
244        }
245
246        if let Option::Some(stl) = lst.get(&"style".to_string()) {
247            if stl == "dashed" {
248                line_style = LineStyleKind::Dashed;
249            }
250        }
251
252        if let Option::Some(x) = lst.get(&"color".to_string()) {
253            color = x.clone();
254            color = Self::normalize_color(color);
255        }
256
257        if let Option::Some(pw) = lst.get(&"penwidth".to_string()) {
258            if let Result::Ok(x) = pw.parse::<usize>() {
259                line_width = x;
260            } else {
261                #[cfg(feature = "log")]
262                log::info!("Can't parse integer \"{}\"", pw);
263            }
264        }
265
266        if let Option::Some(fx) = lst.get(&"fontsize".to_string()) {
267            if let Result::Ok(x) = fx.parse::<usize>() {
268                font_size = x;
269            } else {
270                #[cfg(feature = "log")]
271                log::info!("Can't parse integer \"{}\"", fx);
272            }
273        }
274
275        let color = Color::fast(&color);
276        let look = StyleAttr::new(color, line_width, None, 0, font_size);
277        Arrow::new(start, end, line_style, &label, &look, &from_port, &to_port)
278    }
279
280    /// Convert the color to some color that we can handle.
281    fn normalize_color(color: String) -> String {
282        let mut color = color;
283        if let Option::Some(idx) = color.find(':') {
284            color = color[0..idx].to_string();
285        }
286        if color == "transparent" {
287            color = "white".to_string();
288        }
289        color
290    }
291
292    fn get_shape_from_attributes(
293        dir: Orientation,
294        lst: &PropertyList,
295        default_name: &str,
296    ) -> Element {
297        let mut label = default_name.to_string();
298        let mut edge_color = String::from("black");
299        let mut fill_color = String::from("white");
300        let mut font_size: usize = 14;
301        let mut line_width: usize = 1;
302        let mut make_xy_same = false;
303        let mut rounded_corder_value = 0;
304
305        if let Option::Some(val) = lst.get(&"label".to_string()) {
306            label = val.clone();
307        }
308
309        let mut shape = ShapeKind::Circle(label.clone());
310
311        // Set the shape.
312        if let Option::Some(val) = lst.get(&"shape".to_string()) {
313            match &val[..] {
314                "box" => {
315                    shape = ShapeKind::Box(label);
316                    make_xy_same = false;
317                }
318                "doublecircle" => {
319                    shape = ShapeKind::DoubleCircle(label);
320                    make_xy_same = true;
321                }
322                "record" => {
323                    shape = record_builder(&label);
324                }
325                "Mrecord" => {
326                    rounded_corder_value = 15;
327                    shape = record_builder(&label);
328                }
329                _ => shape = ShapeKind::Circle(label),
330            }
331        }
332
333        if let Option::Some(x) = lst.get(&"color".to_string()) {
334            edge_color = x.clone();
335            edge_color = Self::normalize_color(edge_color);
336        }
337
338        if let Option::Some(style) = lst.get(&"style".to_string()) {
339            if style == "filled" && !lst.contains_key("fillcolor") {
340                fill_color = "lightgray".to_string();
341            }
342        }
343
344        if let Option::Some(x) = lst.get(&"fillcolor".to_string()) {
345            fill_color = x.clone();
346            fill_color = Self::normalize_color(fill_color);
347        }
348
349        if let Option::Some(fx) = lst.get(&"fontsize".to_string()) {
350            if let Result::Ok(x) = fx.parse::<usize>() {
351                font_size = x;
352            } else {
353                #[cfg(feature = "log")]
354                log::info!("Can't parse integer \"{}\"", fx);
355            }
356        }
357
358        if let Option::Some(pw) = lst.get(&"width".to_string()) {
359            if let Result::Ok(x) = pw.parse::<usize>() {
360                line_width = x;
361            } else {
362                #[cfg(feature = "log")]
363                log::info!("Can't parse integer \"{}\"", pw);
364            }
365        }
366
367        // We flip the orientation before we create the shape. In graphs that
368        // grow top down the records grow to the left.
369        let dir = dir.flip();
370
371        let sz = get_shape_size(dir, &shape, font_size, make_xy_same);
372        let look = StyleAttr::new(
373            Color::fast(&edge_color),
374            line_width,
375            Option::Some(Color::fast(&fill_color)),
376            rounded_corder_value,
377            font_size,
378        );
379        Element::create(shape, look, dir, sz)
380    }
381}