Skip to main content

pipecircuit_visualization/
render.rs

1use crate::{DiagramNode, StateMachineDiagram};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub enum DiagramTarget {
5    TextTree,
6    AnsiTextTree,
7    MermaidFlowchart,
8    GraphvizDot,
9}
10
11pub(super) fn render(diagram: &StateMachineDiagram, target: DiagramTarget) -> String {
12    match target {
13        DiagramTarget::TextTree => text_tree(diagram, false),
14        DiagramTarget::AnsiTextTree => text_tree(diagram, true),
15        DiagramTarget::MermaidFlowchart => mermaid(diagram),
16        DiagramTarget::GraphvizDot => graphviz(diagram),
17    }
18}
19
20fn text_tree(diagram: &StateMachineDiagram, ansi: bool) -> String {
21    let mut rendering = String::new();
22    push_role_marker(&mut rendering, "●", ansi);
23    rendering.push_str(" state machine  ");
24    push_role_marker(&mut rendering, "▸", ansi);
25    rendering.push_str(" step input  ");
26    push_role_marker(&mut rendering, "◆", ansi);
27    rendering.push_str(" outcome\n\n");
28    push_role_marker(&mut rendering, "●", ansi);
29    rendering.push(' ');
30    rendering.push_str(&diagram.name);
31    rendering.push('\n');
32    for (index, step) in diagram.steps.iter().enumerate() {
33        let final_step = index == diagram.steps.len().saturating_sub(1);
34        rendering.push_str(if final_step {
35            "└── "
36        } else {
37            "├── "
38        });
39        push_role_marker(&mut rendering, "▸", ansi);
40        rendering.push(' ');
41        rendering.push_str(&step.input);
42        rendering.push('\n');
43        let prefix = if final_step { "    " } else { "│   " };
44        render_text_outcome(&mut rendering, &step.outcome, prefix, ansi);
45    }
46    rendering
47}
48
49fn push_role_marker(rendering: &mut String, marker: &str, ansi: bool) {
50    if ansi {
51        rendering.push_str("\u{1b}[38;5;114m");
52    }
53    rendering.push_str(marker);
54    if ansi {
55        rendering.push_str("\u{1b}[0m");
56    }
57}
58
59fn render_text_outcome(rendering: &mut String, outcome: &DiagramNode, prefix: &str, ansi: bool) {
60    rendering.push_str(prefix);
61    rendering.push_str("└── ");
62    push_role_marker(rendering, "◆", ansi);
63    rendering.push(' ');
64    rendering.push_str(&outcome.label);
65    rendering.push('\n');
66    let mut child_prefix = String::from(prefix);
67    child_prefix.push_str("    ");
68    for (index, child) in outcome.children.iter().enumerate() {
69        render_text_node(
70            rendering,
71            child,
72            &child_prefix,
73            index == outcome.children.len().saturating_sub(1),
74        );
75    }
76}
77
78fn render_text_node(rendering: &mut String, node: &DiagramNode, prefix: &str, final_node: bool) {
79    rendering.push_str(prefix);
80    rendering.push_str(if final_node {
81        "└── "
82    } else {
83        "├── "
84    });
85    rendering.push_str(&node.label);
86    rendering.push('\n');
87    let mut child_prefix = String::from(prefix);
88    child_prefix.push_str(if final_node { "    " } else { "│   " });
89    for (index, child) in node.children.iter().enumerate() {
90        render_text_node(
91            rendering,
92            child,
93            &child_prefix,
94            index == node.children.len().saturating_sub(1),
95        );
96    }
97}
98
99struct FlatGraph {
100    nodes: Vec<String>,
101    edges: Vec<(usize, usize)>,
102}
103
104impl FlatGraph {
105    fn from_diagram(diagram: &StateMachineDiagram) -> Self {
106        let mut graph = Self {
107            nodes: vec![diagram.name.clone()],
108            edges: Vec::new(),
109        };
110        for step in &diagram.steps {
111            let input = graph.push_node(step.input.clone());
112            graph.edges.push((0, input));
113            graph.push_tree(input, &step.outcome);
114        }
115        graph
116    }
117
118    fn push_node(&mut self, label: String) -> usize {
119        let identifier = self.nodes.len();
120        self.nodes.push(label);
121        identifier
122    }
123
124    fn push_tree(&mut self, parent: usize, node: &DiagramNode) {
125        let identifier = self.push_node(node.label.clone());
126        self.edges.push((parent, identifier));
127        for child in &node.children {
128            self.push_tree(identifier, child);
129        }
130    }
131}
132
133fn mermaid(diagram: &StateMachineDiagram) -> String {
134    let graph = FlatGraph::from_diagram(diagram);
135    let mut rendering = String::from("flowchart LR\n");
136    for (identifier, label) in graph.nodes.iter().enumerate() {
137        rendering.push_str(&format!("    n{identifier}[\"{}\"]\n", escape_label(label)));
138    }
139    for (from, to) in graph.edges {
140        rendering.push_str(&format!("    n{from} --> n{to}\n"));
141    }
142    rendering
143}
144
145fn graphviz(diagram: &StateMachineDiagram) -> String {
146    let graph = FlatGraph::from_diagram(diagram);
147    let mut rendering = format!("digraph \"{}\" {{\n", escape_label(&diagram.name));
148    rendering.push_str("    rankdir=LR;\n");
149    for (identifier, label) in graph.nodes.iter().enumerate() {
150        rendering.push_str(&format!(
151            "    n{identifier} [label=\"{}\"];\n",
152            escape_label(label)
153        ));
154    }
155    for (from, to) in graph.edges {
156        rendering.push_str(&format!("    n{from} -> n{to};\n"));
157    }
158    rendering.push_str("}\n");
159    rendering
160}
161
162fn escape_label(label: &str) -> String {
163    label.replace('"', "&quot;")
164}