pipecircuit_visualization/
render.rs1use 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('"', """)
164}