ricecoder_generation/templates/
parser.rs

1//! Template syntax parser
2//!
3//! Parses template syntax, validates structure, extracts placeholders,
4//! and detects conditionals, loops, and includes.
5
6use crate::models::Placeholder;
7use crate::templates::error::TemplateError;
8use std::collections::HashSet;
9
10/// Represents a parsed template element
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TemplateElement {
13    /// Plain text content
14    Text(String),
15    /// Placeholder: {{name}} or {{Name}} or {{name_snake}} etc.
16    Placeholder(String),
17    /// Conditional block: {{#if condition}}...{{/if}}
18    Conditional {
19        /// Condition expression
20        condition: String,
21        /// Content inside the conditional
22        content: Vec<TemplateElement>,
23    },
24    /// Loop block: {{#each items}}...{{/each}}
25    Loop {
26        /// Variable name to iterate over
27        variable: String,
28        /// Content inside the loop
29        content: Vec<TemplateElement>,
30    },
31    /// Include/partial: {{> partial_name}}
32    Include(String),
33}
34
35/// Parsed template structure
36#[derive(Debug, Clone)]
37pub struct ParsedTemplate {
38    /// Template elements
39    pub elements: Vec<TemplateElement>,
40    /// Extracted placeholders
41    pub placeholders: Vec<Placeholder>,
42    /// All placeholder names found
43    pub placeholder_names: HashSet<String>,
44}
45
46/// Template parser
47pub struct TemplateParser;
48
49impl TemplateParser {
50    /// Parse template content and extract structure
51    ///
52    /// # Arguments
53    /// * `content` - Template content to parse
54    ///
55    /// # Returns
56    /// Parsed template structure or error with line number
57    pub fn parse(content: &str) -> Result<ParsedTemplate, TemplateError> {
58        let mut parser = Parser::new(content);
59        parser.parse()
60    }
61
62    /// Extract all placeholders from template content
63    ///
64    /// # Arguments
65    /// * `content` - Template content
66    ///
67    /// # Returns
68    /// Vector of placeholder names found
69    pub fn extract_placeholders(content: &str) -> Result<Vec<String>, TemplateError> {
70        let parsed = Self::parse(content)?;
71        Ok(parsed.placeholder_names.into_iter().collect())
72    }
73
74    /// Detect if template has conditionals
75    pub fn has_conditionals(content: &str) -> Result<bool, TemplateError> {
76        Ok(content.contains("{{#if") && content.contains("{{/if}}"))
77    }
78
79    /// Detect if template has loops
80    pub fn has_loops(content: &str) -> Result<bool, TemplateError> {
81        Ok(content.contains("{{#each") && content.contains("{{/each}}"))
82    }
83
84    /// Detect if template has includes
85    pub fn has_includes(content: &str) -> Result<bool, TemplateError> {
86        Ok(content.contains("{{>"))
87    }
88}
89
90/// Internal parser state machine
91struct Parser {
92    content: String,
93    position: usize,
94    line: usize,
95    placeholder_names: HashSet<String>,
96}
97
98impl Parser {
99    fn new(content: &str) -> Self {
100        Self {
101            content: content.to_string(),
102            position: 0,
103            line: 1,
104            placeholder_names: HashSet::new(),
105        }
106    }
107
108    fn parse(&mut self) -> Result<ParsedTemplate, TemplateError> {
109        let elements = self.parse_elements()?;
110        let placeholders = self.extract_placeholder_definitions();
111
112        Ok(ParsedTemplate {
113            elements,
114            placeholders,
115            placeholder_names: self.placeholder_names.clone(),
116        })
117    }
118
119    fn parse_elements(&mut self) -> Result<Vec<TemplateElement>, TemplateError> {
120        let mut elements = Vec::new();
121
122        while self.position < self.content.len() {
123            if self.peek_char() == Some('{') && self.peek_ahead(1) == Some('{') {
124                // Handle template syntax
125                let element = self.parse_template_syntax()?;
126                elements.push(element);
127            } else {
128                // Handle plain text
129                let text = self.parse_text();
130                if !text.is_empty() {
131                    elements.push(TemplateElement::Text(text));
132                }
133            }
134        }
135
136        Ok(elements)
137    }
138
139    fn parse_template_syntax(&mut self) -> Result<TemplateElement, TemplateError> {
140        self.consume_char()?; // {
141        self.consume_char()?; // {
142
143        match self.peek_char() {
144            Some('#') => self.parse_block(),
145            Some('>') => self.parse_include(),
146            Some(_) => self.parse_placeholder(),
147            None => Err(TemplateError::InvalidSyntax {
148                line: self.line,
149                message: "Unexpected end of template".to_string(),
150            }),
151        }
152    }
153
154    fn parse_block(&mut self) -> Result<TemplateElement, TemplateError> {
155        self.consume_char()?; // #
156
157        let block_type = self.read_until_whitespace_or_char('}')?;
158
159        match block_type.as_str() {
160            "if" => self.parse_conditional(),
161            "each" => self.parse_loop(),
162            _ => Err(TemplateError::InvalidSyntax {
163                line: self.line,
164                message: format!("Unknown block type: {}", block_type),
165            }),
166        }
167    }
168
169    fn parse_conditional(&mut self) -> Result<TemplateElement, TemplateError> {
170        self.skip_whitespace();
171        let condition = self.read_until_string("}}")?;
172        self.consume_string("}}")?;
173
174        let content = self.parse_until_end_block("if")?;
175
176        Ok(TemplateElement::Conditional { condition, content })
177    }
178
179    fn parse_loop(&mut self) -> Result<TemplateElement, TemplateError> {
180        self.skip_whitespace();
181        let variable = self.read_until_whitespace_or_char('}')?;
182        self.skip_whitespace();
183        self.consume_string("}}")?;
184
185        let content = self.parse_until_end_block("each")?;
186
187        Ok(TemplateElement::Loop { variable, content })
188    }
189
190    fn parse_include(&mut self) -> Result<TemplateElement, TemplateError> {
191        self.consume_char()?; // >
192        self.skip_whitespace();
193
194        let partial_name = self.read_until_string("}}")?;
195        self.consume_string("}}")?;
196
197        Ok(TemplateElement::Include(partial_name))
198    }
199
200    fn parse_placeholder(&mut self) -> Result<TemplateElement, TemplateError> {
201        let placeholder_name = self.read_until_string("}}")?;
202        self.consume_string("}}")?;
203
204        // Extract base name (without case suffix)
205        let base_name = self.extract_base_name(&placeholder_name);
206        self.placeholder_names.insert(base_name);
207
208        Ok(TemplateElement::Placeholder(placeholder_name))
209    }
210
211    fn parse_text(&mut self) -> String {
212        let mut text = String::new();
213
214        while let Some(ch) = self.peek_char() {
215            if ch == '{' && self.peek_ahead(1) == Some('{') {
216                break;
217            }
218
219            if ch == '\n' {
220                self.line += 1;
221            }
222
223            text.push(ch);
224            self.position += 1;
225        }
226
227        text
228    }
229
230    fn parse_until_end_block(
231        &mut self,
232        block_type: &str,
233    ) -> Result<Vec<TemplateElement>, TemplateError> {
234        let mut elements = Vec::new();
235        let end_marker = format!("{{{{/{}}}}}", block_type);
236
237        while self.position < self.content.len() {
238            if self.content[self.position..].starts_with(&end_marker) {
239                self.position += end_marker.len();
240                return Ok(elements);
241            }
242
243            if self.peek_char() == Some('{') && self.peek_ahead(1) == Some('{') {
244                let element = self.parse_template_syntax()?;
245                elements.push(element);
246            } else {
247                let text = self.parse_text();
248                if !text.is_empty() {
249                    elements.push(TemplateElement::Text(text));
250                }
251            }
252        }
253
254        Err(TemplateError::InvalidSyntax {
255            line: self.line,
256            message: format!("Unclosed {{{{#{}}}}}", block_type),
257        })
258    }
259
260    fn extract_base_name(&self, placeholder: &str) -> String {
261        // Remove case suffixes to get base name
262        // {{Name}} -> name
263        // {{name_snake}} -> name
264        // {{name-kebab}} -> name
265        // {{nameCamel}} -> name
266        // {{NAME}} -> name
267
268        let placeholder = placeholder.trim();
269
270        // Handle snake_case - take everything before first underscore
271        if let Some(pos) = placeholder.find('_') {
272            return placeholder[..pos].to_lowercase();
273        }
274
275        // Handle kebab-case - take everything before first hyphen
276        if let Some(pos) = placeholder.find('-') {
277            return placeholder[..pos].to_lowercase();
278        }
279
280        // For all-uppercase (like NAME), just lowercase it
281        if placeholder
282            .chars()
283            .all(|c| c.is_uppercase() || !c.is_alphabetic())
284        {
285            return placeholder.to_lowercase();
286        }
287
288        // Handle PascalCase and camelCase
289        // Extract the base word (everything before the first uppercase letter after the first char)
290        let mut base = String::new();
291        let mut chars = placeholder.chars().peekable();
292
293        // First character
294        if let Some(first) = chars.next() {
295            base.push(first.to_lowercase().next().unwrap_or(first));
296        }
297
298        // Remaining characters until we hit an uppercase letter
299        while let Some(&ch) = chars.peek() {
300            if ch.is_uppercase() {
301                break;
302            }
303            base.push(ch);
304            chars.next();
305        }
306
307        if base.is_empty() {
308            placeholder.to_lowercase()
309        } else {
310            base
311        }
312    }
313
314    fn peek_char(&self) -> Option<char> {
315        self.content.chars().nth(self.position)
316    }
317
318    fn peek_ahead(&self, offset: usize) -> Option<char> {
319        self.content.chars().nth(self.position + offset)
320    }
321
322    fn consume_char(&mut self) -> Result<char, TemplateError> {
323        match self.peek_char() {
324            Some(ch) => {
325                self.position += 1;
326                if ch == '\n' {
327                    self.line += 1;
328                }
329                Ok(ch)
330            }
331            None => Err(TemplateError::InvalidSyntax {
332                line: self.line,
333                message: "Unexpected end of template".to_string(),
334            }),
335        }
336    }
337
338    fn consume_string(&mut self, s: &str) -> Result<(), TemplateError> {
339        for ch in s.chars() {
340            if self.consume_char()? != ch {
341                return Err(TemplateError::InvalidSyntax {
342                    line: self.line,
343                    message: format!("Expected '{}'", s),
344                });
345            }
346        }
347        Ok(())
348    }
349
350    fn read_until_string(&mut self, delimiter: &str) -> Result<String, TemplateError> {
351        let mut result = String::new();
352
353        while self.position < self.content.len() {
354            if self.content[self.position..].starts_with(delimiter) {
355                return Ok(result);
356            }
357
358            result.push(self.consume_char()?);
359        }
360
361        Err(TemplateError::InvalidSyntax {
362            line: self.line,
363            message: format!("Unterminated template element, expected '{}'", delimiter),
364        })
365    }
366
367    fn read_until_whitespace_or_char(&mut self, ch: char) -> Result<String, TemplateError> {
368        let mut result = String::new();
369
370        while let Some(c) = self.peek_char() {
371            if c.is_whitespace() || c == ch {
372                return Ok(result);
373            }
374            result.push(self.consume_char()?);
375        }
376
377        Ok(result)
378    }
379
380    fn skip_whitespace(&mut self) {
381        while let Some(ch) = self.peek_char() {
382            if ch.is_whitespace() {
383                if ch == '\n' {
384                    self.line += 1;
385                }
386                self.position += 1;
387            } else {
388                break;
389            }
390        }
391    }
392
393    fn extract_placeholder_definitions(&self) -> Vec<Placeholder> {
394        self.placeholder_names
395            .iter()
396            .map(|name| Placeholder {
397                name: name.clone(),
398                description: format!("Placeholder: {}", name),
399                default: None,
400                required: true,
401            })
402            .collect()
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_parse_simple_placeholder() {
412        let content = "Hello {{name}}";
413        let result = TemplateParser::parse(content).unwrap();
414        assert_eq!(result.placeholder_names.len(), 1);
415        assert!(result.placeholder_names.contains("name"));
416    }
417
418    #[test]
419    fn test_parse_multiple_placeholders() {
420        let content = "{{Name}} is a {{type}}";
421        let result = TemplateParser::parse(content).unwrap();
422        assert_eq!(result.placeholder_names.len(), 2);
423        assert!(result.placeholder_names.contains("name"));
424        assert!(result.placeholder_names.contains("type"));
425    }
426
427    #[test]
428    fn test_parse_case_variations() {
429        let content = "{{Name}} {{name}} {{NAME}} {{name_snake}} {{name-kebab}} {{nameCamel}}";
430        let result = TemplateParser::parse(content).unwrap();
431        assert_eq!(result.placeholder_names.len(), 1);
432        assert!(result.placeholder_names.contains("name"));
433    }
434
435    #[test]
436    fn test_parse_conditional() {
437        let content = "{{#if condition}}content{{/if}}";
438        let result = TemplateParser::parse(content).unwrap();
439        assert!(!result.elements.is_empty());
440    }
441
442    #[test]
443    fn test_parse_loop() {
444        let content = "{{#each items}}{{name}}{{/each}}";
445        let result = TemplateParser::parse(content).unwrap();
446        assert!(result.placeholder_names.contains("name"));
447    }
448
449    #[test]
450    fn test_extract_placeholders() {
451        let content = "{{Name}} and {{description}}";
452        let placeholders = TemplateParser::extract_placeholders(content).unwrap();
453        assert_eq!(placeholders.len(), 2);
454    }
455
456    #[test]
457    fn test_has_conditionals() {
458        let content = "{{#if test}}yes{{/if}}";
459        assert!(TemplateParser::has_conditionals(content).unwrap());
460    }
461
462    #[test]
463    fn test_has_loops() {
464        let content = "{{#each items}}{{name}}{{/each}}";
465        assert!(TemplateParser::has_loops(content).unwrap());
466    }
467
468    #[test]
469    fn test_has_includes() {
470        let content = "{{> partial}}";
471        assert!(TemplateParser::has_includes(content).unwrap());
472    }
473
474    #[test]
475    fn test_unclosed_placeholder_error() {
476        let content = "Hello {{name";
477        let result = TemplateParser::parse(content);
478        assert!(result.is_err());
479    }
480
481    #[test]
482    fn test_unclosed_conditional_error() {
483        let content = "{{#if test}}content";
484        let result = TemplateParser::parse(content);
485        assert!(result.is_err());
486    }
487}