oxidite_template/
parser.rs1use crate::Result;
2use regex::Regex;
3
4#[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
16pub 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 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 if source.starts_with("{{") {
52 return self.parse_variable(source);
53 }
54
55 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 source.starts_with("{% if ") {
85 return self.parse_if(source);
86 }
87
88 if source.starts_with("{% for ") {
90 return self.parse_for(source);
91 }
92
93 if source.starts_with("{% block ") {
95 return self.parse_block(source);
96 }
97
98 if source.starts_with("{% extends ") {
100 return self.parse_extends(source);
101 }
102
103 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 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 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 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 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; } else {
203 nesting -= 1;
204 if nesting == 0 {
205 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; 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 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 Some((source.to_string(), source.len()))
288 }
289 }
290}