bc_envelope/format/
mermaid.rs

1use std::{cell::RefCell, collections::HashSet, rc::Rc};
2
3use bc_components::{Digest, DigestProvider};
4
5use super::FormatContextOpt;
6use crate::{
7    EdgeType, Envelope, base::envelope::EnvelopeCase, with_format_context,
8};
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)]
11pub enum MermaidOrientation {
12    #[default]
13    LeftToRight,
14    TopToBottom,
15    RightToLeft,
16    BottomToTop,
17}
18
19impl std::fmt::Display for MermaidOrientation {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            MermaidOrientation::LeftToRight => write!(f, "LR"),
23            MermaidOrientation::TopToBottom => write!(f, "TB"),
24            MermaidOrientation::RightToLeft => write!(f, "RL"),
25            MermaidOrientation::BottomToTop => write!(f, "BT"),
26        }
27    }
28}
29
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)]
31pub enum MermaidTheme {
32    #[default]
33    Default,
34    Neutral,
35    Dark,
36    Forest,
37    Base,
38}
39
40impl std::fmt::Display for MermaidTheme {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            MermaidTheme::Default => write!(f, "default"),
44            MermaidTheme::Neutral => write!(f, "neutral"),
45            MermaidTheme::Dark => write!(f, "dark"),
46            MermaidTheme::Forest => write!(f, "forest"),
47            MermaidTheme::Base => write!(f, "base"),
48        }
49    }
50}
51
52#[derive(Clone)]
53pub struct MermaidFormatOpts<'a> {
54    hide_nodes: bool,
55    monochrome: bool,
56    theme: MermaidTheme,
57    orientation: MermaidOrientation,
58    highlighting_target: HashSet<Digest>,
59    context: FormatContextOpt<'a>,
60}
61
62impl Default for MermaidFormatOpts<'_> {
63    fn default() -> Self {
64        Self {
65            hide_nodes: false,
66            monochrome: false,
67            theme: MermaidTheme::default(),
68            orientation: MermaidOrientation::default(),
69            highlighting_target: HashSet::new(),
70            context: FormatContextOpt::Global,
71        }
72    }
73}
74
75impl<'a> MermaidFormatOpts<'a> {
76    /// Sets whether to hide NODE identifiers in the tree representation
77    /// (default is false).
78    pub fn hide_nodes(mut self, hide: bool) -> Self {
79        self.hide_nodes = hide;
80        self
81    }
82
83    /// When set to true, the tree representation will use a monochrome
84    /// color scheme (default is false).
85    pub fn monochrome(mut self, monochrome: bool) -> Self {
86        self.monochrome = monochrome;
87        self
88    }
89
90    /// Sets the theme for the tree representation (default is Default).
91    pub fn theme(mut self, theme: MermaidTheme) -> Self {
92        self.theme = theme;
93        self
94    }
95
96    /// Sets the orientation of the tree representation (default is
97    /// LeftToRight).
98    pub fn orientation(mut self, orientation: MermaidOrientation) -> Self {
99        self.orientation = orientation;
100        self
101    }
102
103    /// Sets the set of digests to highlight in the tree representation.
104    pub fn highlighting_target(mut self, target: HashSet<Digest>) -> Self {
105        self.highlighting_target = target;
106        self
107    }
108
109    /// Sets the formatting context for the tree representation.
110    pub fn context(mut self, context: FormatContextOpt<'a>) -> Self {
111        self.context = context;
112        self
113    }
114}
115
116/// Support for tree-formatting envelopes.
117impl Envelope {
118    pub fn mermaid_format(&self) -> String {
119        self.mermaid_format_opt(&MermaidFormatOpts::default())
120    }
121
122    pub fn mermaid_format_opt(&self, opts: &MermaidFormatOpts<'_>) -> String {
123        let elements: RefCell<Vec<Rc<MermaidElement>>> =
124            RefCell::new(Vec::new());
125        let next_id = RefCell::new(0);
126        let visitor = |envelope: &Envelope,
127                       level: usize,
128                       incoming_edge: EdgeType,
129                       parent: Option<Rc<MermaidElement>>|
130         -> (_, bool) {
131            let id = *next_id.borrow_mut();
132            *next_id.borrow_mut() += 1;
133            let elem = Rc::new(MermaidElement::new(
134                id,
135                level,
136                envelope.clone(),
137                incoming_edge,
138                !opts.hide_nodes,
139                opts.highlighting_target.contains(&envelope.digest()),
140                parent.clone(),
141            ));
142            elements.borrow_mut().push(elem.clone());
143            (Some(elem), false)
144        };
145        let s = self.clone();
146        s.walk(opts.hide_nodes, None, &visitor);
147
148        let elements = elements.borrow();
149
150        let mut element_ids: HashSet<usize> =
151            elements.iter().map(|e| e.id).collect();
152
153        let mut lines = vec![
154            format!(
155                "%%{{ init: {{ 'theme': '{}', 'flowchart': {{ 'curve': 'basis' }} }} }}%%",
156                opts.theme
157            ),
158            format!("graph {}", opts.orientation),
159        ];
160
161        let mut node_styles: Vec<String> = Vec::new();
162        let mut link_styles: Vec<String> = Vec::new();
163        let mut link_index = 0;
164
165        for element in elements.iter() {
166            let indent = "    ".repeat(element.level);
167            let content = if let Some(parent) = element.parent.as_ref() {
168                let mut this_link_styles = Vec::new();
169                if !opts.monochrome
170                    && let Some(color) =
171                        element.incoming_edge.link_stroke_color()
172                {
173                    this_link_styles.push(format!("stroke:{}", color));
174                }
175
176                if element.is_highlighted && parent.is_highlighted {
177                    this_link_styles.push("stroke-width:4px".to_string());
178                } else {
179                    this_link_styles.push("stroke-width:2px".to_string());
180                }
181                if !this_link_styles.is_empty() {
182                    link_styles.push(format!(
183                        "linkStyle {} {}",
184                        link_index,
185                        this_link_styles.join(",")
186                    ));
187                }
188                link_index += 1;
189                element.format_edge(&mut element_ids)
190            } else {
191                element.format_node(&mut element_ids)
192            };
193            let mut this_node_styles = Vec::new();
194            if !opts.monochrome {
195                let stroke_color = element.envelope.node_color();
196                this_node_styles.push(format!("stroke:{}", stroke_color));
197            }
198            if element.is_highlighted {
199                this_node_styles.push("stroke-width:6px".to_string());
200            } else {
201                this_node_styles.push("stroke-width:4px".to_string());
202            }
203            if !this_node_styles.is_empty() {
204                node_styles.push(format!(
205                    "style {} {}",
206                    element.id,
207                    this_node_styles.join(",")
208                ));
209            }
210            lines.push(format!("{}{}", indent, content));
211        }
212
213        for style in node_styles {
214            lines.push(style);
215        }
216
217        for style in link_styles {
218            lines.push(style);
219        }
220
221        lines.join("\n")
222    }
223}
224
225/// Represents an element in the tree representation of an envelope.
226#[derive(Debug)]
227struct MermaidElement {
228    id: usize,
229    /// Indentation level of the element in the tree
230    level: usize,
231    /// The envelope element
232    envelope: Envelope,
233    /// The type of edge connecting this element to its parent
234    incoming_edge: EdgeType,
235    /// Whether to show the element's ID (digest)
236    show_id: bool,
237    /// Whether this element should be highlighted in the output
238    is_highlighted: bool,
239    /// The parent element in the tree, if any
240    parent: Option<Rc<MermaidElement>>,
241}
242
243impl MermaidElement {
244    fn new(
245        id: usize,
246        level: usize,
247        envelope: Envelope,
248        incoming_edge: EdgeType,
249        show_id: bool,
250        is_highlighted: bool,
251        parent: Option<Rc<MermaidElement>>,
252    ) -> Self {
253        Self {
254            id,
255            level,
256            envelope,
257            incoming_edge,
258            show_id,
259            is_highlighted,
260            parent,
261        }
262    }
263
264    fn format_node(&self, element_ids: &mut HashSet<usize>) -> String {
265        if element_ids.contains(&self.id) {
266            element_ids.remove(&self.id);
267            let mut lines: Vec<String> = Vec::new();
268            let summary = with_format_context!(|ctx| {
269                self.envelope
270                    .summary(20, ctx)
271                    .replace('"', "&quot;")
272                    .to_string()
273            });
274            lines.push(summary);
275            if self.show_id {
276                let id = self.envelope.digest().short_description();
277                lines.push(id);
278            }
279            let lines = lines.join("<br>");
280            let (frame_l, frame_r) = self.envelope.mermaid_frame();
281            let id = self.id;
282            format!(r#"{id}{frame_l}"{lines}"{frame_r}"#)
283        } else {
284            format!("{}", self.id)
285        }
286    }
287
288    fn format_edge(&self, element_ids: &mut HashSet<usize>) -> String {
289        let parent = self.parent.as_ref().unwrap();
290        let arrow = if let Some(label) = self.incoming_edge.label() {
291            format!("-- {} -->", label)
292        } else {
293            "-->".to_string()
294        };
295        format!(
296            "{} {} {}",
297            parent.format_node(element_ids),
298            arrow,
299            self.format_node(element_ids)
300        )
301    }
302}
303
304impl Envelope {
305    #[rustfmt::skip]
306    fn mermaid_frame(&self) -> (&str, &str) {
307        match self.case() {
308            EnvelopeCase::Node { .. }       => ("((", "))"),
309            EnvelopeCase::Leaf { .. }       => ("[",  "]"),
310            EnvelopeCase::Wrapped { .. }    => ("[/", "\\]"),
311            EnvelopeCase::Assertion(..)     => ("([", "])"),
312            EnvelopeCase::Elided(..)        => ("{{", "}}"),
313            #[cfg(feature = "known_value")]
314            EnvelopeCase::KnownValue { .. } => ("[/", "/]"),
315            #[cfg(feature = "encrypt")]
316            EnvelopeCase::Encrypted(..)     => (">",  "]"),
317            #[cfg(feature = "compress")]
318            EnvelopeCase::Compressed(..)    => ("[[", "]]"),
319        }
320    }
321
322    #[rustfmt::skip]
323    fn node_color(&self) -> &'static str {
324        match self.case() {
325            EnvelopeCase::Node { .. }       => "red",
326            EnvelopeCase::Leaf { .. }       => "teal",
327            EnvelopeCase::Wrapped { .. }    => "blue",
328            EnvelopeCase::Assertion(..)     => "green",
329            EnvelopeCase::Elided(..)        => "gray",
330            #[cfg(feature = "known_value")]
331            EnvelopeCase::KnownValue { .. } => "goldenrod",
332            #[cfg(feature = "encrypt")]
333            EnvelopeCase::Encrypted(..)     => "coral",
334            #[cfg(feature = "compress")]
335            EnvelopeCase::Compressed(..)    => "purple",
336        }
337    }
338}
339
340impl EdgeType {
341    pub fn link_stroke_color(&self) -> Option<&'static str> {
342        match self {
343            EdgeType::Subject => Some("red"),
344            EdgeType::Content => Some("blue"),
345            EdgeType::Predicate => Some("cyan"),
346            EdgeType::Object => Some("magenta"),
347            _ => None,
348        }
349    }
350}