orbit/parser/
template.rs

1//! Parser for template sections of .orbit files
2
3use super::{
4    ast::{AttributeValue, TemplateNode},
5    tokenizer::{Token, Tokenizer},
6};
7use std::collections::HashMap;
8
9/// Parses template sections in .orbit files
10pub struct TemplateParser<'a> {
11    tokenizer: Tokenizer<'a>,
12}
13
14impl<'a> TemplateParser<'a> {
15    /// Create a new template parser
16    pub fn new(input: &'a str) -> Self {
17        Self {
18            tokenizer: Tokenizer::new(input),
19        }
20    }
21
22    /// Parse the template section into an AST
23    pub fn parse(&mut self) -> Result<TemplateNode, String> {
24        match self.tokenizer.next_token() {
25            Token::OpenTag(tag) => self.parse_element(tag),
26            token => Err(format!("Expected opening tag, got {token:?}")),
27        }
28    }
29
30    /// Parse an element node
31    fn parse_element(&mut self, tag: String) -> Result<TemplateNode, String> {
32        let mut attributes = HashMap::new();
33        let mut events = HashMap::new();
34        let mut children = Vec::new();
35
36        loop {
37            match self.tokenizer.next_token() {
38                Token::AttrName(name) => match self.tokenizer.next_token() {
39                    Token::Equal => match self.tokenizer.next_token() {
40                        Token::String(value) => {
41                            // Check if this is an event handler (@click, @input, etc.)
42                            if name.starts_with('@') {
43                                let event_name = name.trim_start_matches('@');
44                                events.insert(event_name.to_string(), value);
45                            } else {
46                                attributes.insert(name, AttributeValue::Static(value));
47                            }
48                        }
49                        Token::ExprStart => {
50                            let expr = self.parse_expression()?;
51                            attributes.insert(name, AttributeValue::Dynamic(expr));
52                        }
53                        token => return Err(format!("Expected attribute value, got {token:?}")),
54                    },
55                    token => return Err(format!("Expected =, got {token:?}")),
56                },
57                Token::CloseTag(close_tag) => {
58                    if close_tag != tag {
59                        return Err(format!("Mismatched tags: {tag} and {close_tag}"));
60                    }
61                    break;
62                }
63                Token::SelfClosingTag(_) => break,
64                Token::Text(text) => {
65                    // Only add non-whitespace text nodes
66                    if !text.trim().is_empty() {
67                        children.push(TemplateNode::Text(text));
68                    }
69                }
70                Token::Identifier(text) => {
71                    children.push(TemplateNode::Text(text));
72                }
73                Token::Number(text) => {
74                    children.push(TemplateNode::Text(text));
75                }
76                Token::Plus => {
77                    children.push(TemplateNode::Text("+".to_string()));
78                }
79                Token::Minus => {
80                    children.push(TemplateNode::Text("-".to_string()));
81                }
82                Token::Star => {
83                    children.push(TemplateNode::Text("*".to_string()));
84                }
85                Token::Slash => {
86                    children.push(TemplateNode::Text("/".to_string()));
87                }
88                Token::Comma => {
89                    children.push(TemplateNode::Text(",".to_string()));
90                }
91                Token::ExprStart => {
92                    let expr = self.parse_expression()?;
93                    children.push(TemplateNode::Expression(expr));
94                }
95                Token::OpenTag(child_tag) => {
96                    children.push(self.parse_element(child_tag)?);
97                }
98                Token::Eof => return Err("Unexpected end of template".to_string()),
99                token => return Err(format!("Unexpected token: {token:?}")),
100            }
101        }
102
103        Ok(TemplateNode::Element {
104            tag,
105            attributes,
106            events,
107            children,
108        })
109    }
110    /// Parse an expression inside {{ }}
111    fn parse_expression(&mut self) -> Result<String, String> {
112        let mut expr = String::new();
113        let mut prev_was_operator = false;
114        let mut prev_was_identifier = false;
115
116        loop {
117            match self.tokenizer.next_token() {
118                Token::ExprEnd => break,
119                Token::Identifier(ident) => {
120                    if prev_was_identifier {
121                        expr.push(' ');
122                    }
123                    expr.push_str(&ident);
124                    prev_was_identifier = true;
125                    prev_was_operator = false;
126                }
127                Token::Dot => {
128                    expr.push('.');
129                    prev_was_identifier = false;
130                    prev_was_operator = false;
131                }
132                Token::Number(num) => {
133                    if prev_was_identifier {
134                        expr.push(' ');
135                    }
136                    expr.push_str(&num);
137                    prev_was_identifier = true;
138                    prev_was_operator = false;
139                }
140                Token::String(str) => {
141                    expr.push_str(&format!("\"{str}\""));
142                    prev_was_identifier = true;
143                    prev_was_operator = false;
144                }
145                Token::Plus => {
146                    if !prev_was_operator && !expr.is_empty() {
147                        expr.push(' ');
148                    }
149                    expr.push('+');
150                    expr.push(' ');
151                    prev_was_identifier = false;
152                    prev_was_operator = true;
153                }
154                Token::Minus => {
155                    if !prev_was_operator && !expr.is_empty() {
156                        expr.push(' ');
157                    }
158                    expr.push('-');
159                    expr.push(' ');
160                    prev_was_identifier = false;
161                    prev_was_operator = true;
162                }
163                Token::Star => {
164                    if !prev_was_operator && !expr.is_empty() {
165                        expr.push(' ');
166                    }
167                    expr.push('*');
168                    expr.push(' ');
169                    prev_was_identifier = false;
170                    prev_was_operator = true;
171                }
172                Token::Slash => {
173                    if !prev_was_operator && !expr.is_empty() {
174                        expr.push(' ');
175                    }
176                    expr.push('/');
177                    expr.push(' ');
178                    prev_was_identifier = false;
179                    prev_was_operator = true;
180                }
181                Token::OpenParen => {
182                    expr.push('(');
183                    prev_was_identifier = false;
184                    prev_was_operator = false;
185                }
186                Token::CloseParen => {
187                    expr.push(')');
188                    prev_was_identifier = true;
189                    prev_was_operator = false;
190                }
191                Token::Comma => {
192                    expr.push(',');
193                    expr.push(' ');
194                    prev_was_identifier = false;
195                    prev_was_operator = false;
196                }
197                Token::Eof => return Err("Unclosed expression".to_string()),
198                token => return Err(format!("Unexpected token in expression: {token:?}")),
199            }
200        }
201
202        Ok(expr)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_parse_simple_element() {
212        let input = r#"<div class="greeting">Hello</div>"#;
213        let mut parser = TemplateParser::new(input);
214        let node = parser.parse().unwrap();
215
216        match node {
217            TemplateNode::Element {
218                tag,
219                attributes,
220                events,
221                children,
222            } => {
223                assert_eq!(tag, "div");
224                assert_eq!(attributes.len(), 1);
225                assert_eq!(events.len(), 0);
226                assert_eq!(children.len(), 1);
227
228                match &attributes.get("class").unwrap() {
229                    AttributeValue::Static(value) => assert_eq!(value, "greeting"),
230                    _ => panic!("Expected static attribute"),
231                }
232
233                match &children[0] {
234                    TemplateNode::Text(text) => assert_eq!(text, "Hello"),
235                    _ => panic!("Expected text node"),
236                }
237            }
238            _ => panic!("Expected element node"),
239        }
240    }
241
242    #[test]
243    fn test_parse_expression() {
244        let input = r#"<div>{{ count + 1 }}</div>"#;
245        let mut parser = TemplateParser::new(input);
246        let node = parser.parse().unwrap();
247
248        match node {
249            TemplateNode::Element {
250                tag,
251                attributes,
252                events,
253                children,
254            } => {
255                assert_eq!(tag, "div");
256                assert_eq!(attributes.len(), 0);
257                assert_eq!(events.len(), 0);
258                assert_eq!(children.len(), 1);
259
260                match &children[0] {
261                    TemplateNode::Expression(expr) => assert_eq!(expr, "count + 1"),
262                    _ => panic!("Expected expression node"),
263                }
264            }
265            _ => panic!("Expected element node"),
266        }
267    }
268
269    #[test]
270    fn test_parse_event_handler() {
271        let input = r#"<button @click="increment">+</button>"#;
272        let mut parser = TemplateParser::new(input);
273        let node = parser.parse().unwrap();
274
275        match node {
276            TemplateNode::Element {
277                tag,
278                attributes,
279                events,
280                children,
281            } => {
282                assert_eq!(tag, "button");
283                assert_eq!(attributes.len(), 0);
284                assert_eq!(events.len(), 1);
285                assert_eq!(children.len(), 1);
286
287                assert_eq!(events.get("click").unwrap(), "increment");
288
289                match &children[0] {
290                    TemplateNode::Text(text) => assert_eq!(text, "+"),
291                    _ => panic!("Expected text node"),
292                }
293            }
294            _ => panic!("Expected element node"),
295        }
296    }
297}