Skip to main content

lex_babel/formats/treeviz/
mod.rs

1//! Treeviz formatter for AST nodes
2//!
3//! Treeviz is a visual representation of the AST, design specifically for document trees.
4//! It features a visual tree and line based output. For a version that matches, each line to source line, see the ./linetreeviz module.
5//! helpful for formats that are primarely line oriented (like text).
6//!
7//! It encodes the node structure as indentation, with 2 white spaces per level of nesting.
8//!
9//! So the format is :
10//! <indentation>(per level) <icon><space><label> (truncated to 30 characters)
11//!
12//! Example: (truncation not withstanding)
13//!
14//!   ¶ This is a two-lined para…
15// │    ↵ This is a two-lined pa…
16// │    ↵ First, a simple defini…
17// │  ≔ Root Definition
18// │    ¶ This definition contai…
19// │      ↵ This definition cont…
20// │    ☰ 2 items
21// │      • - Item 1 in definiti…
22// │      • - Item 2 in definiti…
23// │  ¶ This is a marker annotat…
24// │    ↵ This is a marker annot…
25// │  § 1. Primary Session {{ses…
26// │    ¶ This session acts as t…
27// │      ↵ This session acts as…
28
29//! Icons
30//!     Core elements:
31//!         Document: ⧉
32//!         Session: §
33//!         SessionTitle: ⊤
34//!         Annotation: '"'
35//!         Paragraph: ¶
36//!         List: ☰
37//!         ListItem: •
38//!         Verbatim: 𝒱
39//!         ForeingLine: ℣
40//!         Definition: ≔
41//!     Container elements:
42//!         SessionContainer: Ψ
43//!         ContentContainer: ➔
44//!         Content: ⊤
45//!     Spans:
46//!         Text: ◦
47//!         TextLine: ↵
48//!     Inlines (not yet implemented, leave here for now)
49//!         Italic: 𝐼
50//!         Bold: 𝐁
51//!         Code: ƒ
52//!         Math (not yet implemented, leave here for now)
53//!         Math: √
54//!     References (not yet implemented, leave here for now)
55//!         Reference: ⊕
56//!         ReferenceFile: /
57//!         ReferenceCitation: †
58//!         ReferenceCitationAuthor: "@"
59//!         ReferenceCitationPage: ◫
60//!         ReferenceToCome: ⋯
61//!         ReferenceUnknown: ∅
62//!         ReferenceFootnote: ³
63//!         ReferenceSession: #
64
65use super::icons::get_icon;
66use crate::error::FormatError;
67use crate::format::Format;
68use lex_core::lex::ast::trait_helpers::try_as_container;
69use lex_core::lex::ast::traits::{AstNode, Container, VisualStructure};
70use lex_core::lex::ast::{ContentItem, Document};
71use std::collections::HashMap;
72
73/// Format a single ContentItem node
74fn format_content_item(
75    item: &ContentItem,
76    prefix: &str,
77    child_index: usize,
78    child_count: usize,
79    include_all: bool,
80    show_linum: bool,
81) -> String {
82    let mut output = String::new();
83    let is_last = child_index == child_count - 1;
84    let connector = if is_last { "└─" } else { "├─" };
85    let icon = get_icon(item.node_type());
86
87    let linum_prefix = if show_linum {
88        format!("{:02} ", item.range().start.line + 1)
89    } else {
90        String::new()
91    };
92
93    output.push_str(&format!(
94        "{}{}{} {} {}\n",
95        linum_prefix,
96        prefix,
97        connector,
98        icon,
99        item.display_label()
100    ));
101
102    let child_prefix = format!("{}{}", prefix, if is_last { "  " } else { "│ " });
103
104    // Handle include_all: show visual headers using traits
105    if include_all {
106        if item.has_visual_header() {
107            if let Some(container) = try_as_container(item) {
108                let header = container.label();
109                // Use the parent node's icon for the header (no synthetic type needed)
110                let header_icon = get_icon(item.node_type());
111                output.push_str(&format!("{child_prefix}├─ {header_icon} {header}\n"));
112            }
113        }
114
115        // Handle special cases that need more than just the header
116        match item {
117            ContentItem::Session(s) => {
118                // Show session annotations
119                for (i, ann) in s.annotations.iter().enumerate() {
120                    let ann_item = ContentItem::Annotation(ann.clone());
121                    output.push_str(&format_content_item(
122                        &ann_item,
123                        &child_prefix,
124                        i + 1,
125                        s.annotations.len() + s.children().len(),
126                        include_all,
127                        show_linum,
128                    ));
129                }
130            }
131            ContentItem::ListItem(li) => {
132                // Show marker as synthetic child
133                let marker_icon = get_icon("Marker");
134                output.push_str(&format!(
135                    "{}├─ {} {}\n",
136                    child_prefix,
137                    marker_icon,
138                    li.marker.as_string()
139                ));
140
141                // Show text content
142                for (i, text_part) in li.text.iter().enumerate() {
143                    let text_icon = get_icon("Text");
144                    let connector = if i == li.text.len() - 1 && li.children().is_empty() {
145                        "└─"
146                    } else {
147                        "├─"
148                    };
149                    output.push_str(&format!(
150                        "{}{} {} {}\n",
151                        child_prefix,
152                        connector,
153                        text_icon,
154                        text_part.as_string()
155                    ));
156                }
157
158                // Show list item annotations
159                for ann in &li.annotations {
160                    let ann_item = ContentItem::Annotation(ann.clone());
161                    output.push_str(&format_content_item(
162                        &ann_item,
163                        &child_prefix,
164                        0,
165                        1,
166                        include_all,
167                        show_linum,
168                    ));
169                }
170            }
171            ContentItem::Definition(d) => {
172                // Show definition annotations
173                for ann in &d.annotations {
174                    let ann_item = ContentItem::Annotation(ann.clone());
175                    output.push_str(&format_content_item(
176                        &ann_item,
177                        &child_prefix,
178                        0,
179                        1,
180                        include_all,
181                        show_linum,
182                    ));
183                }
184            }
185            ContentItem::Annotation(a) => {
186                // Show parameters (label already shown by get_visual_header)
187                for param in &a.data.parameters {
188                    let param_icon = get_icon("Parameter");
189                    output.push_str(&format!(
190                        "{}├─ {} {}={}\n",
191                        child_prefix, param_icon, param.key, param.value
192                    ));
193                }
194            }
195            _ => {}
196        }
197    }
198
199    // Process regular children using Container trait
200    match item {
201        ContentItem::VerbatimBlock(v) => {
202            // Handle verbatim groups
203            let mut group_output = String::new();
204            for (idx, group) in v.group().enumerate() {
205                let group_label = if v.group_len() == 1 {
206                    group.subject.as_string().to_string()
207                } else {
208                    format!(
209                        "{} (group {} of {})",
210                        group.subject.as_string(),
211                        idx + 1,
212                        v.group_len()
213                    )
214                };
215                let group_icon = get_icon("VerbatimGroup");
216                let is_last_group = idx == v.group_len() - 1;
217                let group_connector = if is_last_group { "└─" } else { "├─" };
218
219                group_output.push_str(&format!(
220                    "{child_prefix}{group_connector} {group_icon} {group_label}\n"
221                ));
222
223                let group_child_prefix = format!(
224                    "{}{}",
225                    child_prefix,
226                    if is_last_group { "  " } else { "│ " }
227                );
228
229                for (i, child) in group.children.iter().enumerate() {
230                    group_output.push_str(&format_content_item(
231                        child,
232                        &group_child_prefix,
233                        i,
234                        group.children.len(),
235                        include_all,
236                        show_linum,
237                    ));
238                }
239            }
240            output + &group_output
241        }
242        _ => {
243            // Use Container trait to get children for all other types
244            if let Some(container) = try_as_container(item) {
245                output
246                    + &format_children(container.children(), &child_prefix, include_all, show_linum)
247            } else {
248                // Leaf nodes have no children
249                output
250            }
251        }
252    }
253}
254
255fn format_children(
256    children: &[ContentItem],
257    prefix: &str,
258    include_all: bool,
259    show_linum: bool,
260) -> String {
261    let mut output = String::new();
262    let child_count = children.len();
263    for (i, child) in children.iter().enumerate() {
264        output.push_str(&format_content_item(
265            child,
266            prefix,
267            i,
268            child_count,
269            include_all,
270            show_linum,
271        ));
272    }
273    output
274}
275
276pub fn to_treeviz_str(doc: &Document) -> String {
277    to_treeviz_str_with_params(doc, &HashMap::new())
278}
279
280/// Convert a document to treeviz string with optional parameters
281///
282/// # Parameters
283///
284/// - `"ast-full"`: When set to `"true"`, includes all AST node properties:
285///   * Document-level annotations
286///   * Session titles (as SessionTitle nodes)
287///   * List item markers and text (as Marker and Text nodes)
288///   * Definition subjects (as Subject nodes)
289///   * Annotation labels and parameters (as Label and Parameter nodes)
290///
291/// # Examples
292///
293/// ```ignore
294/// use std::collections::HashMap;
295///
296/// // Normal view (content only)
297/// let output = to_treeviz_str_with_params(&doc, &HashMap::new());
298///
299/// // Full AST view (all properties)
300/// let mut params = HashMap::new();
301/// params.insert("ast-full".to_string(), "true".to_string());
302/// let output = to_treeviz_str_with_params(&doc, &params);
303/// ```
304pub fn to_treeviz_str_with_params(doc: &Document, params: &HashMap<String, String>) -> String {
305    // Check if ast-full parameter is set to true
306    let include_all = params
307        .get("ast-full")
308        .map(|v| v.to_lowercase() == "true")
309        .unwrap_or(false);
310
311    let show_linum = params
312        .get("show-linum")
313        .map(|v| v != "false")
314        .unwrap_or(false);
315
316    let icon = get_icon("Document");
317    let mut output = format!(
318        "{} Document ({} annotations, {} items)\n",
319        icon,
320        doc.annotations.len(),
321        doc.root.children.len()
322    );
323
324    // If include_all, show document-level annotations
325    if include_all {
326        for annotation in &doc.annotations {
327            let ann_item = ContentItem::Annotation(annotation.clone());
328            output.push_str(&format_content_item(
329                &ann_item,
330                "",
331                0,
332                1,
333                include_all,
334                show_linum,
335            ));
336        }
337    }
338
339    // Show document children (flattened from root session)
340    let children = &doc.root.children;
341    output + &format_children(children, "", include_all, show_linum)
342}
343
344/// Format implementation for treeviz format
345pub struct TreevizFormat;
346
347impl Format for TreevizFormat {
348    fn name(&self) -> &str {
349        "treeviz"
350    }
351
352    fn description(&self) -> &str {
353        "Visual tree representation with indentation and Unicode icons"
354    }
355
356    fn file_extensions(&self) -> &[&str] {
357        &["tree", "treeviz"]
358    }
359
360    fn supports_serialization(&self) -> bool {
361        true
362    }
363
364    fn serialize(&self, doc: &Document) -> Result<String, FormatError> {
365        Ok(to_treeviz_str(doc))
366    }
367}