ere_core/visualization/
latex_graph.rs

1use crate::visualization::layout::{BuildLayout, DAGLayout};
2
3pub fn escape_latex(text: impl AsRef<str>) -> String {
4    return text
5        .as_ref()
6        .chars()
7        .map(|c| match c {
8            '\\' => r"{\textbackslash}".to_string(),
9            '&' => r"\&".to_string(),
10            '%' => r"\%".to_string(),
11            '$' => r"\$".to_string(),
12            '#' => r"\#".to_string(),
13            '_' => r"\_".to_string(),
14            '{' => r"\{".to_string(),
15            '}' => r"\}".to_string(),
16            '~' => r"{\textasciitilde}".to_string(),
17            '^' => r"{\textasciicircum}".to_string(),
18            c => c.to_string(),
19        })
20        .collect();
21}
22
23pub struct LatexGraphTransition {
24    pub(crate) to: usize,
25    /// The label is a valid latex-encoded string to be inserted at the label.
26    pub(crate) label: String,
27}
28impl LatexGraphTransition {
29    pub fn display_in_line(&self, from: usize) -> String {
30        let label = &self.label;
31        let bend = match self.to.cmp(&from) {
32            std::cmp::Ordering::Less => "[bend left] ",
33            std::cmp::Ordering::Equal => "[loop below]",
34            std::cmp::Ordering::Greater => "[bend left] ",
35        };
36        format!(
37            "\\path[->] (q{from}) edge {bend} node {{{label}}} (q{});\n",
38            self.to
39        )
40    }
41    pub fn display_straight(&self, from: usize) -> String {
42        let label = &self.label;
43        format!(
44            "\\path[->] (q{from}) edge node {{{label}}} (q{});\n",
45            self.to
46        )
47    }
48}
49
50pub struct LatexGraphState {
51    /// The label is a valid latex-encoded string to be inserted at the label.
52    pub(crate) label: String,
53    pub(crate) transitions: Vec<LatexGraphTransition>,
54    pub(crate) initial: bool,
55    pub(crate) accept: bool,
56}
57impl LatexGraphState {
58    /// All states are just in a horizontal line
59    pub fn display_in_line(&self, idx: usize) -> String {
60        let mut modifiers = String::new();
61        if self.initial {
62            modifiers += ", initial";
63        }
64        if self.accept {
65            modifiers += ", accepting"
66        }
67        let label = escape_latex(&self.label);
68        if idx == 0 {
69            return format!("\\node[state{modifiers}](q0){{{label}}};\n",);
70        } else {
71            return format!(
72                "\\node[state{modifiers}, right of=q{}](q{idx}){{{label}}};\n",
73                idx - 1,
74            );
75        }
76    }
77
78    pub fn display_at(&self, idx: usize, x: f64, y: f64) -> String {
79        let mut modifiers = String::new();
80        if self.initial {
81            modifiers += ", initial";
82        }
83        if self.accept {
84            modifiers += ", accepting"
85        }
86        let label = escape_latex(&self.label);
87        return format!("\\node[state{modifiers}](q{idx}) at ({x}, {y}) {{{label}}};\n",);
88    }
89}
90
91/// Used for tikz visualizations of NFA-like graphs
92pub struct LatexGraph {
93    pub(crate) states: Vec<LatexGraphState>,
94}
95impl LatexGraph {
96    /// Writes a LaTeX TikZ representation to visualize the graph.
97    ///
98    /// If `include_doc` is `true`, will include the headers.
99    /// Otherwise, you should include `\usepackage{tikz}` and `\usetikzlibrary{automata, positioning}`.
100    pub fn to_tikz(&self, include_doc: bool) -> String {
101        let layout = self.pick_layout().map(|layout| layout.layout());
102
103        let mut text_parts: Vec<String> = Vec::new();
104        if include_doc {
105            text_parts.push(
106                "\\documentclass{standalone}\n\\usepackage{tikz}\n\\usetikzlibrary{automata, positioning}\n\\begin{document}\n"
107                .into(),
108            );
109        }
110        text_parts.push("\\begin{tikzpicture}[node distance=2cm, auto]\n".into());
111
112        let mut transition_parts = Vec::new();
113
114        for (i, state) in self.states.iter().enumerate() {
115            if let Some(layout) = layout.as_ref() {
116                let (x, y) = layout[i];
117                text_parts.push(state.display_at(i, x * 2.0, y * 2.0));
118            } else {
119                text_parts.push(state.display_in_line(i));
120            }
121
122            for tr in &state.transitions {
123                if let Some(layout) = layout.as_ref() {
124                    let x = layout[i].0;
125                    let x_next = layout[tr.to].0;
126                    if x_next - x > 1.0 {
127                        transition_parts.push(tr.display_in_line(i));
128                    } else {
129                        transition_parts.push(tr.display_straight(i));
130                    }
131                } else {
132                    transition_parts.push(tr.display_in_line(i));
133                }
134            }
135        }
136        text_parts.extend_from_slice(&transition_parts);
137
138        text_parts.push("\\end{tikzpicture}\n".into());
139        if include_doc {
140            text_parts.push("\\end{document}\n".into());
141        }
142        return text_parts.into_iter().collect();
143    }
144
145    fn pick_layout<'a>(&'a self) -> Option<Box<dyn BuildLayout + 'a>> {
146        if let Some(dag) = DAGLayout::new(self) {
147            return Some(Box::new(dag));
148        }
149
150        // TODO: more layouts
151        return None;
152    }
153}