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, ¶ms);
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}