forge_tree/parser/
tree_parser.rs

1use crate::parser::{ItemType, ProjectStructure, StructureItem};
2use crate::{Result, ForgeTreeError};
3use std::collections::HashMap;
4
5pub struct TreeParser;
6
7impl TreeParser {
8    pub fn new() -> Self {
9        Self
10    }
11
12    pub fn parse(&self, input: &str) -> Result<ProjectStructure> {
13        let lines: Vec<&str> = input.lines().collect();
14        if lines.is_empty() {
15            return Err(ForgeTreeError::Parse("Empty input".to_string()));
16        }
17
18        let root_name = self.extract_root_name(&lines)?;
19        
20        // Parse all lines after the root
21        let child_lines: Vec<&str> = lines[1..].iter()
22            .filter(|line| !line.trim().is_empty())
23            .copied()
24            .collect();
25        
26        let items = self.parse_structure(&child_lines)?;
27
28        Ok(ProjectStructure {
29            root: root_name,
30            items,
31            variables: HashMap::new(),
32        })
33    }
34
35    fn extract_root_name(&self, lines: &[&str]) -> Result<String> {
36        let first_line = lines.first()
37            .ok_or_else(|| ForgeTreeError::Parse("No root directory found".to_string()))?;
38        
39        let name = first_line.trim().trim_end_matches('/');
40        if name.is_empty() {
41            return Err(ForgeTreeError::Parse("Invalid root directory name".to_string()));
42        }
43        
44        Ok(name.to_string())
45    }
46
47    fn parse_structure(&self, lines: &[&str]) -> Result<Vec<StructureItem>> {
48        let mut items = Vec::new();
49        let mut i = 0;
50
51        while i < lines.len() {
52            let line = lines[i];
53            let current_depth = self.get_depth(line);
54            
55            let (name, is_directory) = self.parse_line(line)?;
56            let mut item = StructureItem {
57                name: name.clone(),
58                path: name,
59                item_type: if is_directory { ItemType::Directory } else { ItemType::File },
60                template: None,
61                content: None,
62                children: Vec::new(),
63            };
64
65            // Collect children (lines with greater depth)
66            i += 1;
67            let mut child_lines = Vec::new();
68            
69            while i < lines.len() {
70                let child_line = lines[i];
71                let child_depth = self.get_depth(child_line);
72                
73                // If we hit a line at same or less depth, stop collecting children
74                if child_depth <= current_depth {
75                    break;
76                }
77                
78                child_lines.push(child_line);
79                i += 1;
80            }
81
82            // Recursively parse children
83            if !child_lines.is_empty() {
84                item.children = self.parse_structure(&child_lines)?;
85                item.item_type = ItemType::Directory; // Has children, must be directory
86            }
87
88            items.push(item);
89        }
90
91        Ok(items)
92    }
93
94    fn get_depth(&self, line: &str) -> usize {
95        let mut depth = 0;
96        
97        for ch in line.chars() {
98            match ch {
99                '├' | '└' | '│' => depth += 1,
100                '─' | ' ' => continue, // Skip dashes and spaces
101                _ => break, // Stop at first non-tree character
102            }
103        }
104        
105        depth
106    }
107
108    fn parse_line(&self, line: &str) -> Result<(String, bool)> {
109        // Skip tree characters by counting characters, not bytes
110        let content = line.chars()
111            .skip_while(|&ch| ch == '│' || ch == '├' || ch == '└' || ch == '─' || ch == ' ')
112            .collect::<String>()
113            .trim()
114            .to_string();
115        
116        if content.is_empty() {
117            return Err(ForgeTreeError::Parse(format!("Empty name in line: {}", line)));
118        }
119
120        // Determine if it's a directory or file
121        let is_directory = content.ends_with('/') || !content.contains('.');
122        let clean_name = content.trim_end_matches('/').to_string();
123        
124        Ok((clean_name, is_directory))
125    }
126}
127
128impl Default for TreeParser {
129    fn default() -> Self {
130        Self::new()
131    }
132}