rusty_handlebars_parser/
expression.rs

1//! Handlebars expression parsing
2//!
3//! This module provides functionality for parsing Handlebars expressions from template strings.
4//! It handles various types of expressions including variables, blocks, comments, and escaped content.
5//!
6//! # Expression Types
7//!
8//! The module supports the following types of expressions:
9//! - Variables: `{{name}}`
10//! - HTML-escaped variables: `{{{name}}}`
11//! - Block helpers: `{{#helper}}...{{/helper}}`
12//! - Comments: `{{! comment }}` or `{{!-- comment --}}`
13//! - Escaped content: `\{{name}}` or `{{{{name}}}}this bit here is not parsed {{not_interpolated}} and output raw{{{{/name}}}}`
14//!
15//! # Examples
16//!
17//! ```rust
18//! use rusty_handlebars_parser::expression::{Expression, ExpressionType};
19//!
20//! let template = "Hello {{name}}!";
21//! let expr = Expression::from(template).unwrap().unwrap();
22//! assert_eq!(expr.expression_type, ExpressionType::HtmlEscaped);
23//! assert_eq!(expr.content, "name");
24//! ```
25
26use std::{cmp::min, fmt::Display};
27
28use crate::error::{Result, ParseError};
29
30/// Types of Handlebars expressions
31#[derive(Debug, Clone, Copy)]
32pub enum ExpressionType{
33    /// Comment expression: `{{! comment }}`
34    Comment, HtmlEscaped, Raw, Open, Close, Escaped
35}
36
37/// Represents a parsed Handlebars expression
38#[derive(Debug, Clone, Copy)]
39pub struct Expression<'a>{
40    /// The type of expression
41    pub expression_type: ExpressionType,
42    /// Text before the expression
43    pub prefix: &'a str,
44    /// The expression content
45    pub content: &'a str,
46    /// Text after the expression
47    pub postfix: &'a str,
48    /// The complete expression including delimiters
49    pub raw: &'a str
50}
51
52/// Safely extracts a substring of specified length
53#[inline]
54fn nibble(src: &str, start: usize, len: usize) -> Result<usize>{
55    let end = start + len; 
56    if end >= src.len(){
57        return Err(ParseError::unclosed(src));
58    }
59    Ok(end)
60}
61
62impl<'a> Expression<'a>{
63    /// Creates a new expression by finding its closing delimiter
64    fn close(expression_type: ExpressionType, preffix: &'a str, start: &'a str, end: &'static str) -> Result<Self>{
65        match start.find(end){
66            Some(mut pos) => {
67                if pos == 0{
68                    return Err(ParseError { message: format!("empty block near {}", preffix) });
69                }
70                let mut postfix = &start[pos + end.len() ..];
71                if &start[pos - 1 .. pos] == "~"{
72                    postfix = postfix.trim_start();
73                    pos -= 1;
74                } 
75                Ok(Self { expression_type, prefix: preffix, content: &start[.. pos], postfix, raw: &start[.. pos + end.len()] })
76            },
77            None => Err(ParseError::unclosed(preffix))
78        }
79    }
80
81    /// Parses a comment expression
82    fn check_comment(preffix: &'a str, start: &'a str) -> Result<Self>{
83        if let Some(pos) = start.find("--"){
84            if pos == 0{
85                return Self::close(ExpressionType::Comment, preffix, &start[2 ..], "--}}");
86            }
87        }
88        Self::close(ExpressionType::Comment, preffix, start, "}}")
89    }
90
91    /// Finds the closing delimiter for an escaped expression
92    fn find_closing_escape(open: Expression<'a>) -> Result<Self>{
93        let mut postfix = open.postfix;
94        let mut from: usize = 0;
95        loop{
96            let candidate = postfix.find("{{{{/").ok_or(ParseError::unclosed(&open.raw))?;
97            let start = candidate + 5;
98            let remains = &postfix[start ..];
99            let close = remains.find("}}}}").ok_or(ParseError::unclosed(&open.raw))?;
100            let end = start + close + 4;
101            if &remains[.. close] == open.content{
102                return Ok(Self{
103                    expression_type: ExpressionType::Escaped,
104                    prefix: open.prefix,
105                    content: &open.postfix[.. from + candidate],
106                    postfix: &postfix[end ..],
107                    raw: open.raw
108                })
109            }
110            from += end;
111            postfix = &postfix[from ..];
112        }
113    }
114
115    /// Parses the next expression from a template string
116    pub fn from(src: &'a str) -> Result<Option<Self>>{
117        match src.find("{{"){
118            Some(start) => {
119                let mut second = nibble(src, start, 3)?;
120                if start > 0 && &src[start - 1 .. start] == "\\"{
121                    return Ok(Some(Self::close(ExpressionType::Escaped, &src[.. start - 1], &src[second - 1 ..], "}}")?));
122                }
123                let mut prefix = &src[.. start];
124                let mut marker = &src[start + 2 .. second];
125                if marker == "~"{
126                    prefix = prefix.trim_end();
127                    second = nibble(src, second, 1)?;
128                    marker = &src[start + 3 .. second];
129                }
130                Ok(Some(match marker{
131                    "{" => {
132                        let next = nibble(src, second, 1)?;
133                        let char = &src[second .. next];
134                        if char == "{"{
135                            second = next;
136                            let next = nibble(src, second, 1)?;
137                            if &src[second .. next] == "~"{
138                                second = next;
139                                prefix = prefix.trim_end();
140                            }
141                            return Ok(Some(Self::find_closing_escape(Self::close(ExpressionType::Escaped, prefix, &src[second ..], "}}}}")?)?));
142                        }
143                        if char == "~"{
144                            second = next;
145                            prefix = prefix.trim_end();
146                        }
147                        Self::close(ExpressionType::Raw, prefix, &src[second ..], "}}}")?
148                    },
149                    "!" => Self::check_comment(prefix, &src[second ..])?,
150                    "#" => Self::close(ExpressionType::Open, prefix, &src[second ..], "}}")?,
151                    "/" => Self::close(ExpressionType::Close, prefix, &src[second ..], "}}")?,
152                    _ => Self::close(ExpressionType::HtmlEscaped, prefix, &src[second - 1 ..], "}}")?
153                }))
154            },
155            None => Ok(None)
156        }
157    }
158
159    /// Parses the next expression after this one
160    pub fn next(&self) -> Result<Option<Self>>{
161        Self::from(self.postfix)
162    }
163
164    /// Returns a string containing the expression and its surrounding context
165    pub fn around(&self) -> &str{
166        let len = self.raw.len();
167        if len == 0{
168            return self.raw;
169        }
170        let start = self.prefix.len();
171        let end = start + self.content.len() + 16;
172        return &self.raw[min(len - 1, if start > 16{ start - 16 } else {0}) .. min(self.raw.len(), end)];
173    }
174}
175
176impl<'a> Display for Expression<'a>{
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        f.write_str(self.raw)
179    }
180}