lex_babel/formats/linetreeviz/
mod.rs

1//! Line Tree Visualization - Collapsed tree representation
2//!
3//! This format provides the same visual tree structure as treeviz but collapses
4//! homogeneous container nodes (Paragraph, List) with their children by showing
5//! combined parent+child icons (e.g., ¶ ↵ for Paragraph/TextLine, ☰ • for List/ListItem).
6//!
7//! ## Example
8//!
9//! ```text
10//! ⧉ Document (0 annotations, 2 items)
11//! ├─ § Session Title
12//! │ ├─ ¶ ↵ First line of paragraph
13//! │ └─ ¶ ↵ Second line of paragraph
14//! └─ ☰ • List item 1
15//!   └─ ☰ • List item 2
16//! ```
17//!
18//! ## Key Differences from treeviz
19//!
20//! - Collapses Paragraph containers with TextLine children (shows `¶ ↵` not separate nodes)
21//! - Collapses List containers with ListItem children (shows `☰ •` not separate nodes)
22//! - Uses VisualStructure trait to identify collapsible containers
23//! - Shares icon mapping with treeviz
24
25use super::icons::get_icon;
26use crate::error::FormatError;
27use crate::format::Format;
28use lex_core::lex::ast::traits::{AstNode, Container, VisualStructure};
29use lex_core::lex::ast::{ContentItem, Document};
30use std::collections::HashMap;
31
32/// Format a single ContentItem node with collapsing logic
33fn format_content_item(
34    item: &ContentItem,
35    prefix: &str,
36    child_index: usize,
37    child_count: usize,
38    show_linum: bool,
39) -> String {
40    let mut output = String::new();
41    let is_last = child_index == child_count - 1;
42    let connector = if is_last { "└─" } else { "├─" };
43
44    // Check if this node collapses with its children using the VisualStructure trait
45    let collapses = match item {
46        ContentItem::Paragraph(p) => p.collapses_with_children(),
47        ContentItem::List(l) => l.collapses_with_children(),
48        ContentItem::Session(s) => s.collapses_with_children(),
49        ContentItem::Definition(d) => d.collapses_with_children(),
50        ContentItem::Annotation(a) => a.collapses_with_children(),
51        ContentItem::VerbatimBlock(v) => v.collapses_with_children(),
52        _ => false,
53    };
54
55    if collapses {
56        // Get parent info
57        let parent_icon = get_icon(item.node_type());
58        let children: Vec<&dyn AstNode> = match item {
59            ContentItem::Paragraph(p) => p.lines.iter().map(|l| l as &dyn AstNode).collect(),
60            ContentItem::List(l) => l.items.iter().map(|i| i as &dyn AstNode).collect(),
61            _ => Vec::new(),
62        };
63
64        // Show children with combined parent+child icons, using the parent's connector
65        for (i, child) in children.iter().enumerate() {
66            let child_is_last = i == children.len() - 1;
67            let child_icon = get_icon(child.node_type());
68
69            // For the first child, use the parent's connector; for subsequent children get indented
70            if i == 0 {
71                let linum_prefix = if show_linum {
72                    format!("{:02} ", child.range().start.line + 1)
73                } else {
74                    String::new()
75                };
76
77                output.push_str(&format!(
78                    "{}{}{} {} {} {}\n",
79                    linum_prefix,
80                    prefix,
81                    connector,
82                    parent_icon,
83                    child_icon,
84                    child.display_label()
85                ));
86            } else {
87                // Subsequent children get indented with the parent's continuation
88                let child_prefix = format!("{}{}", prefix, if is_last { "  " } else { "│ " });
89                let child_connector = if child_is_last { "└─" } else { "├─" };
90                let linum_prefix = if show_linum {
91                    format!("{:02} ", child.range().start.line + 1)
92                } else {
93                    String::new()
94                };
95
96                output.push_str(&format!(
97                    "{}{}{} {} {} {}\n",
98                    linum_prefix,
99                    child_prefix,
100                    child_connector,
101                    parent_icon,
102                    child_icon,
103                    child.display_label()
104                ));
105            }
106
107            // Process grandchildren if any (for nested structures within collapsed items)
108            // For now, we don't handle this case as TextLine and basic ListItem don't have children
109        }
110    } else {
111        // Normal node - show as usual
112        let icon = get_icon(item.node_type());
113        let linum_prefix = if show_linum {
114            format!("{:02} ", item.range().start.line + 1)
115        } else {
116            String::new()
117        };
118
119        output.push_str(&format!(
120            "{}{}{} {} {}\n",
121            linum_prefix,
122            prefix,
123            connector,
124            icon,
125            item.display_label()
126        ));
127
128        // Process children
129        let children = match item {
130            ContentItem::Session(s) => s.children(),
131            ContentItem::Definition(d) => d.children(),
132            ContentItem::ListItem(li) => li.children(),
133            ContentItem::Annotation(a) => a.children(),
134            _ => &[],
135        };
136
137        if !children.is_empty() {
138            let child_prefix = format!("{}{}", prefix, if is_last { "  " } else { "│ " });
139            for (i, child) in children.iter().enumerate() {
140                output.push_str(&format_content_item(
141                    child,
142                    &child_prefix,
143                    i,
144                    children.len(),
145                    show_linum,
146                ));
147            }
148        }
149    }
150
151    output
152}
153
154/// Convert a document to linetreeviz string
155pub fn to_linetreeviz_str(doc: &Document) -> String {
156    to_linetreeviz_str_with_params(doc, &HashMap::new())
157}
158
159/// Convert a document to linetreeviz string with optional parameters
160///
161/// # Parameters
162///
163/// - `"ast-full"`: When set to `"true"`, includes all AST node properties
164///   Note: Currently this parameter is not fully implemented for linetreeviz
165pub fn to_linetreeviz_str_with_params(doc: &Document, params: &HashMap<String, String>) -> String {
166    let show_linum = params
167        .get("show-linum")
168        .map(|v| v != "false")
169        .unwrap_or(false);
170
171    let icon = get_icon("Document");
172    let mut output = format!(
173        "{} Document ({} annotations, {} items)\n",
174        icon,
175        doc.annotations.len(),
176        doc.root.children.len()
177    );
178
179    let children = &doc.root.children;
180    for (i, child) in children.iter().enumerate() {
181        output.push_str(&format_content_item(
182            child,
183            "",
184            i,
185            children.len(),
186            show_linum,
187        ));
188    }
189
190    output
191}
192
193/// Format implementation for line tree visualization
194pub struct LinetreevizFormat;
195
196impl Format for LinetreevizFormat {
197    fn name(&self) -> &str {
198        "linetreeviz"
199    }
200
201    fn description(&self) -> &str {
202        "Tree visualization with collapsed containers (Paragraph/List)"
203    }
204
205    fn file_extensions(&self) -> &[&str] {
206        &["linetree"]
207    }
208
209    fn supports_serialization(&self) -> bool {
210        true
211    }
212
213    fn supports_parsing(&self) -> bool {
214        false
215    }
216
217    fn serialize(&self, doc: &Document) -> Result<String, FormatError> {
218        Ok(to_linetreeviz_str(doc))
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_icon_mapping() {
228        assert_eq!(get_icon("Session"), "§");
229        assert_eq!(get_icon("TextLine"), "↵");
230        assert_eq!(get_icon("ListItem"), "•");
231        assert_eq!(get_icon("Definition"), "≔");
232        assert_eq!(get_icon("Paragraph"), "¶");
233        assert_eq!(get_icon("List"), "☰");
234    }
235}