1use crate::machine::{HistoryKind, Machine, NodeId, StateKind};
5use std::collections::HashSet;
6
7pub fn mermaid(m: &Machine) -> String {
9 mermaid_with_config(m, &HashSet::new())
10}
11
12pub fn mermaid_with_config(m: &Machine, active_paths: &HashSet<String>) -> String {
14 let mut out = String::from("stateDiagram-v2\n");
15 emit(m, m.top, &mut out, 0);
16 if !active_paths.is_empty() {
17 out.push_str(" classDef active fill:#9f9,stroke:#3a3\n");
18 let mut classes: Vec<String> = m
19 .states
20 .iter()
21 .filter(|s| active_paths.contains(&s.path))
22 .map(|s| s.id.clone())
23 .collect();
24 classes.sort();
25 classes.dedup();
26 if !classes.is_empty() {
27 out.push_str(&format!(" class {} active\n", classes.join(",")));
28 }
29 }
30 out
31}
32
33fn pad(out: &mut String, indent: usize) {
34 for _ in 0..indent {
35 out.push_str(" ");
36 }
37}
38
39fn emit(m: &Machine, n: NodeId, out: &mut String, indent: usize) {
40 let sd = m.get(n);
41 let is_top = n == m.top;
42 match sd.kind {
43 StateKind::Composite => {
44 if !is_top {
45 pad(out, indent);
46 out.push_str(&format!("state {} {{\n", sd.id));
47 }
48 if let Some(init) = &sd.initial {
49 pad(out, indent + 1);
50 out.push_str(&format!("[*] --> {}\n", m.get(init.target).id));
51 }
52 for &c in &sd.children {
53 emit(m, c, out, indent + 1);
54 }
55 emit_transitions(m, n, out, indent + 1);
56 if !is_top {
57 pad(out, indent);
58 out.push_str("}\n");
59 }
60 }
61 StateKind::Orthogonal => {
62 pad(out, indent);
63 out.push_str(&format!("state {} {{\n", sd.id));
64 let regs = sd.regions.clone();
65 for (ri, r) in regs.iter().enumerate() {
66 if ri > 0 {
67 pad(out, indent + 1);
68 out.push_str("--\n");
69 }
70 pad(out, indent + 1);
71 out.push_str(&format!("[*] --> {}\n", m.get(r.initial.target).id));
72 for &s in &r.states {
73 emit(m, s, out, indent + 2);
74 }
75 }
76 emit_transitions(m, n, out, indent + 1);
77 pad(out, indent);
78 out.push_str("}\n");
79 }
80 StateKind::Final => {
81 pad(out, indent);
82 out.push_str(&format!("{} --> [*]\n", sd.id));
83 emit_transitions(m, n, out, indent);
84 }
85 StateKind::Simple | StateKind::Choice => {
86 emit_transitions(m, n, out, indent);
87 }
88 }
89 if !matches!(sd.history, HistoryKind::None) {
90 let h = if matches!(sd.history, HistoryKind::Deep) { "deep" } else { "shallow" };
91 pad(out, indent);
92 out.push_str(&format!("note right of {}: history {}\n", sd.id, h));
93 }
94}
95
96fn emit_transitions(m: &Machine, src: NodeId, out: &mut String, indent: usize) {
97 let src_id = m.get(src).id.clone();
98 for (ev, list) in &m.get(src).on_events {
99 for t in list {
100 if let Some(tgt) = t.target {
101 let mut label = ev.clone();
102 if let Some(g) = &t.guard {
103 label.push_str(&format!(" [{g}]"));
104 }
105 pad(out, indent);
106 out.push_str(&format!("{src_id} --> {} : {label}\n", m.get(tgt).id));
107 }
108 }
109 }
110 for a in &m.get(src).after {
111 if let Some(tgt) = a.target {
112 pad(out, indent);
113 out.push_str(&format!(
114 "{} --> {} : after({}ms)\n",
115 src_id,
116 m.get(tgt).id,
117 a.duration_ms
118 ));
119 }
120 }
121}