Skip to main content

determa_state/
export.rs

1//! Mermaid `stateDiagram-v2` export (SPEC §12). Renders the static structure of a
2//! machine; with a `state_config`, highlights the active leaves and their ancestors.
3
4use crate::machine::{HistoryKind, Machine, NodeId, StateKind};
5use std::collections::HashSet;
6
7/// Export the static structure (no live highlight).
8pub fn mermaid(m: &Machine) -> String {
9    mermaid_with_config(m, &HashSet::new())
10}
11
12/// Export with `active_paths` highlighted (active leaves + their ancestors).
13pub 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}