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