Skip to main content

oxidite_template/
parser.rs

1use crate::Result;
2use regex::Regex;
3
4/// Template AST nodes
5#[derive(Debug, Clone, PartialEq)]
6pub enum TemplateNode {
7    Text(String),
8    Variable { name: String, filters: Vec<String> },
9    If { condition: String, then_branch: Vec<TemplateNode>, else_branch: Option<Vec<TemplateNode>> },
10    For { item: String, iterable: String, body: Vec<TemplateNode> },
11    Block { name: String, body: Vec<TemplateNode> },
12    Extends(String),
13    Include(String),
14}
15
16/// Template parser
17pub struct Parser {
18    source: String,
19}
20
21impl Parser {
22    pub fn new(source: &str) -> Self {
23        Self {
24            source: source.to_string(),
25        }
26    }
27
28    pub fn parse(&self) -> Result<Vec<TemplateNode>> {
29        let mut nodes = Vec::new();
30        let mut pos = 0;
31        let source = self.source.as_str();
32
33        while pos < source.len() {
34            // Try to parse template tags
35            if let Some((node, new_pos)) = self.parse_tag(&source[pos..])? {
36                nodes.push(node);
37                pos += new_pos;
38            } else if let Some((text, new_pos)) = self.parse_text(&source[pos..]) {
39                nodes.push(TemplateNode::Text(text));
40                pos += new_pos;
41            } else {
42                break;
43            }
44        }
45
46        Ok(nodes)
47    }
48
49    fn parse_tag(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
50        // Variable: {{ variable | filter }}
51        if source.starts_with("{{") {
52            return self.parse_variable(source);
53        }
54
55        // Control structures: {% if %}, {% for %}, {% block %}, {% extends %}, {% include %}
56        if source.starts_with("{%") {
57            return self.parse_control(source);
58        }
59
60        Ok(None)
61    }
62
63    fn parse_variable(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
64        let re = Regex::new(r"\{\{\s*([a-zA-Z0-9_.]+)(\s*\|\s*([a-zA-Z0-9_]+))?\s*\}\}").unwrap();
65        
66        if let Some(cap) = re.captures(source) {
67            let full_match = cap.get(0).unwrap();
68            let var_name = cap.get(1).unwrap().as_str().to_string();
69            let filter = cap.get(3).map(|m| vec![m.as_str().to_string()]).unwrap_or_default();
70
71            let node = TemplateNode::Variable {
72                name: var_name,
73                filters: filter,
74            };
75
76            return Ok(Some((node, full_match.end())));
77        }
78
79        Ok(None)
80    }
81
82    fn parse_control(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
83        // {% if condition %}
84        if source.starts_with("{% if ") {
85            return self.parse_if(source);
86        }
87
88        // {% for item in iterable %}
89        if source.starts_with("{% for ") {
90            return self.parse_for(source);
91        }
92
93        // {% block name %}
94        if source.starts_with("{% block ") {
95            return self.parse_block(source);
96        }
97
98        // {% extends "template" %}
99        if source.starts_with("{% extends ") {
100            return self.parse_extends(source);
101        }
102
103        // {% include "template" %}
104        if source.starts_with("{% include ") {
105            return self.parse_include(source);
106        }
107
108        Ok(None)
109    }
110
111    fn parse_if(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
112        let re_if = Regex::new(r"\{%\s*if\s+([a-zA-Z0-9_.]+)\s*%\}").unwrap();
113        
114        if let Some(cap) = re_if.captures(source) {
115            let condition = cap.get(1).unwrap().as_str().to_string();
116            let start_pos = cap.get(0).unwrap().end();
117
118            // Find {% endif %}
119            let endif_pattern = "{% endif %}";
120            let else_pattern = "{% else %}";
121
122            if let Some(endif_pos) = source[start_pos..].find(endif_pattern) {
123                let body_source = &source[start_pos..start_pos + endif_pos];
124                
125                // Check for {% else %}
126                let (then_branch, else_branch) = if let Some(else_pos) = body_source.find(else_pattern) {
127                    let then_source = &body_source[..else_pos];
128                    let else_source = &body_source[else_pos + else_pattern.len()..];
129                    
130                    let parser_then = Parser::new(then_source);
131                    let parser_else = Parser::new(else_source);
132                    
133                    (parser_then.parse()?, Some(parser_else.parse()?))
134                } else {
135                    let parser = Parser::new(body_source);
136                    (parser.parse()?, None)
137                };
138
139                let node = TemplateNode::If {
140                    condition,
141                    then_branch,
142                    else_branch,
143                };
144
145                let total_len = start_pos + endif_pos + endif_pattern.len();
146                return Ok(Some((node, total_len)));
147            }
148        }
149
150        Ok(None)
151    }
152
153    fn parse_for(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
154        let re_for = Regex::new(r"\{%\s*for\s+([a-zA-Z0-9_]+)\s+in\s+([a-zA-Z0-9_.]+)\s*%\}").unwrap();
155        
156        if let Some(cap) = re_for.captures(source) {
157            let item = cap.get(1).unwrap().as_str().to_string();
158            let iterable = cap.get(2).unwrap().as_str().to_string();
159            let start_pos = cap.get(0).unwrap().end();
160
161            // Find {% endfor %}
162            let endfor_pattern = "{% endfor %}";
163            if let Some(endfor_pos) = source[start_pos..].find(endfor_pattern) {
164                let body_source = &source[start_pos..start_pos + endfor_pos];
165                let parser = Parser::new(body_source);
166                let body = parser.parse()?;
167
168                let node = TemplateNode::For {
169                    item,
170                    iterable,
171                    body,
172                };
173
174                let total_len = start_pos + endfor_pos + endfor_pattern.len();
175                return Ok(Some((node, total_len)));
176            }
177        }
178
179        Ok(None)
180    }
181
182    fn parse_block(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
183        let re_block = Regex::new(r"\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}").unwrap();
184        
185        if let Some(cap) = re_block.captures(source) {
186            let name = cap.get(1).unwrap().as_str().to_string();
187            let start_pos = cap.get(0).unwrap().end();
188
189            // Find matching {% endblock %}
190            let mut nesting = 1;
191            let mut current_pos = start_pos;
192            
193            while nesting > 0 {
194                 let next_open = source[current_pos..].find("{% block ");
195                 let next_close = source[current_pos..].find("{% endblock %}");
196                 
197                 match (next_open, next_close) {
198                     (Some(open), Some(close)) => {
199                         if open < close {
200                             nesting += 1;
201                             current_pos += open + 9; // length of "{% block "
202                         } else {
203                             nesting -= 1;
204                             if nesting == 0 {
205                                 // Found matching endblock
206                                 let endblock_pos = current_pos + close;
207                                 let body_source = &source[start_pos..endblock_pos];
208                                 let parser = Parser::new(body_source);
209                                 let body = parser.parse()?;
210                                 
211                                 let total_len = endblock_pos + 14; // length of "{% endblock %}"
212                                 return Ok(Some((TemplateNode::Block { name, body }, total_len)));
213                             }
214                             current_pos += close + 14;
215                         }
216                     },
217                     (None, Some(close)) => {
218                         nesting -= 1;
219                         if nesting == 0 {
220                             let endblock_pos = current_pos + close;
221                             let body_source = &source[start_pos..endblock_pos];
222                             let parser = Parser::new(body_source);
223                             let body = parser.parse()?;
224                             let total_len = endblock_pos + 14;
225                             return Ok(Some((TemplateNode::Block { name, body }, total_len)));
226                         }
227                         current_pos += close + 14;
228                     },
229                     (Some(open), None) => {
230                         nesting += 1;
231                         current_pos += open + 9;
232                     },
233                     (None, None) => break,
234                 }
235            }
236        }
237
238        Ok(None)
239    }
240
241    fn parse_extends(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
242        let re_extends = Regex::new(r#"\{%\s*extends\s+"([^"]+)"\s*%\}"#).unwrap();
243        
244        if let Some(cap) = re_extends.captures(source) {
245            let template = cap.get(1).unwrap().as_str().to_string();
246            let len = cap.get(0).unwrap().len();
247            
248            return Ok(Some((TemplateNode::Extends(template), len)));
249        }
250
251        Ok(None)
252    }
253
254    fn parse_include(&self, source: &str) -> Result<Option<(TemplateNode, usize)>> {
255        let re_include = Regex::new(r#"\{%\s*include\s+"([^"]+)"\s*%\}"#).unwrap();
256        
257        if let Some(cap) = re_include.captures(source) {
258            let template = cap.get(1).unwrap().as_str().to_string();
259            let len = cap.get(0).unwrap().len();
260            
261            return Ok(Some((TemplateNode::Include(template), len)));
262        }
263
264        Ok(None)
265    }
266
267    fn parse_text(&self, source: &str) -> Option<(String, usize)> {
268        // Find next template tag
269        let pos_var = source.find("{{");
270        let pos_tag = source.find("{%");
271
272        let next_tag = match (pos_var, pos_tag) {
273            (Some(v), Some(t)) => Some(std::cmp::min(v, t)),
274            (Some(v), None) => Some(v),
275            (None, Some(t)) => Some(t),
276            (None, None) => None,
277        };
278        
279        if let Some(pos) = next_tag {
280            if pos > 0 {
281                Some((source[..pos].to_string(), pos))
282            } else {
283                None
284            }
285        } else {
286            // No more tags, rest is text
287            Some((source.to_string(), source.len()))
288        }
289    }
290}