1use std::{collections::HashMap, fmt::Display, io::Write};
2
3#[derive(Debug)]
4pub struct TemplateParseError {
5 message: String,
6}
7
8impl std::fmt::Display for TemplateParseError {
9 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10 write!(f, "Failed to parse template: {}", self.message)
11 }
12}
13
14impl<T: AsRef<str>> From<T> for TemplateParseError {
15 fn from(value: T) -> Self {
16 Self {
17 message: value.as_ref().to_string(),
18 }
19 }
20}
21
22impl std::error::Error for TemplateParseError {}
23
24type Result<T> = std::result::Result<T, TemplateParseError>;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27enum Token<'a> {
28 OBracket,
29 CBracket,
30 Bang,
31 If,
32 Var(&'a [u8]),
33 Else,
34 EndIf,
35 Text(&'a [u8]),
36 Invalid(usize, char),
37}
38
39impl Display for Token<'_> {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Token::OBracket => write!(f, "{{%"),
43 Token::CBracket => write!(f, "%}}"),
44 Token::Bang => write!(f, "!"),
45 Token::If => write!(f, "if"),
46 Token::Var(var) => write!(f, "{} (variable)", String::from_utf8_lossy(var)),
47 Token::Else => write!(f, "else"),
48 Token::EndIf => write!(f, "endif"),
49 Token::Text(_) => write!(f, "(text)"),
50 Token::Invalid(col, token) => {
51 write!(f, "invalid token {token} at {col}",)
52 }
53 }
54 }
55}
56
57const TRUE: &[u8] = b"true";
58const FALSE: &[u8] = b"false";
59const KEYWORDS: &[(&[u8], Token)] = &[
60 (b"if", Token::If),
61 (b"else", Token::Else),
62 (b"endif", Token::EndIf),
63];
64
65struct Lexer<'a> {
66 bytes: &'a [u8],
67 len: usize,
68 cursor: usize,
69 in_bracket: bool,
70}
71
72impl<'a> Lexer<'a> {
73 fn new(bytes: &'a [u8]) -> Self {
74 let len = bytes.len();
75 Self {
76 len,
77 bytes,
78 cursor: 0,
79 in_bracket: false,
80 }
81 }
82
83 fn current_char(&self) -> char {
84 self.bytes[self.cursor] as char
85 }
86
87 fn next_char(&self) -> char {
88 self.bytes[self.cursor + 1] as char
89 }
90
91 fn skip_whitespace(&mut self) {
92 while self.cursor < self.len && self.current_char().is_whitespace() {
93 self.cursor += 1;
94 }
95 }
96
97 fn is_symbol_start(&self) -> bool {
98 let c = self.current_char();
99 c.is_alphabetic() || c == '_'
100 }
101
102 fn is_symbol(&self) -> bool {
103 let c = self.current_char();
104 c.is_alphanumeric() || c == '_'
105 }
106
107 fn read_symbol(&mut self) -> &'a [u8] {
108 let start = self.cursor;
109 while self.is_symbol() {
110 self.cursor += 1;
111 }
112 let end = self.cursor - 1;
113 &self.bytes[start..=end]
114 }
115
116 fn next(&mut self) -> Option<Token<'a>> {
117 if self.in_bracket {
118 self.skip_whitespace();
119 }
120
121 if self.cursor >= self.len {
122 return None;
123 }
124
125 if self.current_char() == '{' && self.next_char() == '%' {
126 self.in_bracket = true;
127 self.cursor += 2;
128 return Some(Token::OBracket);
129 }
130
131 if self.current_char() == '%' && self.next_char() == '}' {
132 self.in_bracket = false;
133 self.cursor += 2;
134 return Some(Token::CBracket);
135 }
136
137 if self.current_char() == '!' {
138 self.cursor += 1;
139 return Some(Token::Bang);
140 }
141
142 if self.in_bracket {
143 if self.is_symbol_start() {
144 let symbol = self.read_symbol();
145 for (keyword, t) in KEYWORDS {
146 if *keyword == symbol {
147 return Some(*t);
148 }
149 }
150
151 return Some(Token::Var(symbol));
152 } else {
153 self.cursor += 1;
154 return Some(Token::Invalid(self.cursor, self.current_char()));
155 }
156 }
157
158 if !self.in_bracket {
159 let start = self.cursor;
160 while !(self.current_char() == '{' && self.next_char() == '%') {
161 self.cursor += 1;
162
163 if self.cursor >= self.len {
164 break;
165 }
166 }
167 let end = self.cursor - 1;
168 return Some(Token::Text(&self.bytes[start..=end]));
169 }
170
171 None
172 }
173}
174
175impl<'a> Iterator for Lexer<'a> {
176 type Item = Token<'a>;
177
178 fn next(&mut self) -> Option<Self::Item> {
179 self.next()
180 }
181}
182
183fn is_truthy(value: &[u8]) -> bool {
184 match value {
185 TRUE => true,
186 FALSE => false,
187 _ => !value.is_empty(),
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
192enum Stmt<'a> {
193 Text(&'a [u8]),
194 Var(&'a [u8]),
195 If {
196 var: &'a [u8],
197 negated: bool,
198 condition: Vec<Stmt<'a>>,
199 else_condition: Option<Vec<Stmt<'a>>>,
200 },
201}
202
203impl<'a> Stmt<'a> {
204 fn execute<V, T>(&self, out: &mut T, data: &HashMap<&str, V>) -> Result<()>
205 where
206 T: Write,
207 V: AsRef<[u8]>,
208 {
209 match self {
210 Stmt::Text(t) => {
211 out.write_all(t).map_err(|e| e.to_string())?;
212 }
213 Stmt::Var(var) => {
214 let var = std::str::from_utf8(var).map_err(|e| e.to_string())?;
215 let value = data
216 .get(var)
217 .ok_or_else(|| format!("Unrecognized variable: {var}"))?;
218 out.write_all(value.as_ref()).map_err(|e| e.to_string())?;
219 }
220 Stmt::If {
221 var,
222 negated,
223 condition,
224 else_condition,
225 } => {
226 let var = std::str::from_utf8(var).map_err(|e| e.to_string())?;
227 let value = data
228 .get(var)
229 .ok_or_else(|| format!("Unrecognized variable: {var}"))?;
230 let value = value.as_ref();
231
232 let truthy = is_truthy(value);
233 let evaluated = if (truthy && !negated) || (!truthy && *negated) {
234 condition
235 } else if let Some(else_condition) = else_condition {
236 else_condition
237 } else {
238 return Ok(());
240 };
241
242 for stmt in evaluated {
243 stmt.execute(out, data)?;
244 }
245 }
246 }
247
248 Ok(())
249 }
250}
251
252struct Parser<'a> {
253 tokens: &'a [Token<'a>],
254 len: usize,
255 cursor: usize,
256}
257
258impl<'a> Parser<'a> {
259 fn new(tokens: &'a [Token<'a>]) -> Self {
260 Self {
261 len: tokens.len(),
262 tokens,
263 cursor: 0,
264 }
265 }
266
267 fn current_token(&self) -> Token<'a> {
268 self.tokens[self.cursor]
269 }
270
271 fn skip_brackets(&mut self) {
272 if self.cursor < self.len {
273 while self.current_token() == Token::OBracket || self.current_token() == Token::CBracket {
274 self.cursor += 1;
275
276 if self.cursor >= self.len {
277 break;
278 }
279 }
280 }
281 }
282
283 fn consume_text(&mut self) -> Option<&'a [u8]> {
284 if let Token::Text(text) = self.current_token() {
285 self.cursor += 1;
286 Some(text)
287 } else {
288 None
289 }
290 }
291
292 fn consume_var(&mut self) -> Option<&'a [u8]> {
293 if let Token::Var(var) = self.current_token() {
294 self.cursor += 1;
295 Some(var)
296 } else {
297 None
298 }
299 }
300
301 fn consume_if(&mut self) -> Result<Option<Stmt<'a>>> {
302 if self.current_token() == Token::If {
303 self.cursor += 1;
304
305 let negated = if self.current_token() == Token::Bang {
306 self.cursor += 1;
307 true
308 } else {
309 false
310 };
311
312 let var = self.consume_var().ok_or_else(|| {
313 format!(
314 "expected variable after if, found: {}",
315 self.current_token()
316 )
317 })?;
318
319 let mut condition = Vec::new();
320 while self.current_token() != Token::Else || self.current_token() != Token::EndIf {
321 match self.next()? {
322 Some(stmt) => condition.push(stmt),
323 None => break,
324 }
325 }
326
327 let else_condition = if self.current_token() == Token::Else {
328 self.cursor += 1;
329
330 let mut else_condition = Vec::new();
331 while self.current_token() != Token::EndIf {
332 match self.next()? {
333 Some(stmt) => else_condition.push(stmt),
334 None => break,
335 }
336 }
337
338 Some(else_condition)
339 } else {
340 None
341 };
342
343 if self.current_token() == Token::EndIf {
344 self.cursor += 1;
345 } else {
346 return Err(format!("expected endif, found: {}", self.current_token()).into());
347 }
348
349 Ok(Some(Stmt::If {
350 var,
351 negated,
352 condition,
353 else_condition,
354 }))
355 } else {
356 Ok(None)
357 }
358 }
359
360 fn next(&mut self) -> Result<Option<Stmt<'a>>> {
361 self.skip_brackets();
362
363 if self.cursor >= self.len {
364 return Ok(None);
365 }
366
367 if let t @ Token::Invalid(_, _) = self.current_token() {
368 return Err(t.to_string().into());
369 }
370
371 let text = self.consume_text();
372 if text.is_some() {
373 return Ok(text.map(Stmt::Text));
374 }
375
376 let var = self.consume_var();
377 if var.is_some() {
378 return Ok(var.map(Stmt::Var));
379 }
380
381 let if_ = self.consume_if()?;
382 if if_.is_some() {
383 return Ok(if_);
384 }
385
386 Ok(None)
387 }
388}
389
390pub fn render<T, V>(template: T, data: &HashMap<&str, V>) -> Result<String>
391where
392 T: AsRef<[u8]>,
393 V: AsRef<[u8]>,
394{
395 let template = template.as_ref();
396 let tokens: Vec<Token> = Lexer::new(template).collect();
397 let mut parser = Parser::new(&tokens);
398 let mut stmts: Vec<Stmt> = Vec::new();
399 while let Some(stmt) = parser.next()? {
400 stmts.push(stmt);
401 }
402 let mut out = Vec::new();
403 for stmt in stmts {
404 stmt.execute(&mut out, data)?;
405 }
406
407 String::from_utf8(out).map_err(|e| e.to_string().into())
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn it_replaces_variable() {
416 let template = "<html>Hello {% name %}</html>";
417 let data: HashMap<&str, &str> = [("name", "world")].into();
418 let rendered = render(template, &data).expect("it should render");
419 assert_eq!(rendered, "<html>Hello world</html>")
420 }
421
422 #[test]
423 fn it_replaces_variable_with_new_lines() {
424 let template = r#"
425 <html>
426 <h1>Hello<h2>
427 <em>{% name %}</em>
428 </html>"#;
429 let data: HashMap<&str, &str> = [("name", "world")].into();
430 let rendered = render(template, &data).expect("it should render");
431 let expected = r#"
432 <html>
433 <h1>Hello<h2>
434 <em>world</em>
435 </html>"#;
436 assert_eq!(rendered, expected)
437 }
438
439 #[test]
440 fn it_performs_condition() {
441 let template = "<html>Hello {% if alpha %}alpha{% else %}stable{% endif %}</html>";
442 let data: HashMap<&str, &str> = [("alpha", "true")].into();
443 let rendered = render(template, &data).expect("it should render");
444 assert_eq!(rendered, "<html>Hello alpha</html>")
445 }
446
447 #[test]
448 fn it_performs_else_condition() {
449 let template = "<html>Hello {% if alpha %}alpha{% else %}stable{% endif %}</html>";
450 let data: HashMap<&str, &str> = [("alpha", "false")].into();
451 let rendered = render(template, &data).expect("it should render");
452 assert_eq!(rendered, "<html>Hello stable</html>")
453 }
454
455 #[test]
456 fn it_performs_condition_with_new_lines() {
457 let template = r#"
458 <html>
459 <h1>Hello<h2>{% if alpha %}
460 <em>alpha</em>{% else %}
461 <em>stable</em>{% endif %}
462 </html>"#;
463 let data: HashMap<&str, &str> = [("alpha", "true")].into();
464 let rendered = render(template, &data).expect("it should render");
465 let expected = r#"
466 <html>
467 <h1>Hello<h2>
468 <em>alpha</em>
469 </html>"#;
470 assert_eq!(rendered, expected)
471 }
472
473 #[test]
474 fn it_replaces_variable_within_if() {
475 let template = r#"
476 <html>
477 <h1>Hello<h2>{% if alpha %}
478 <em>{% alpha_str %}</em>{% else %}
479 <em>stable</em>{% endif %}
480 </html>"#;
481 let data: HashMap<&str, &str> = [("alpha", "true"), ("alpha_str", "hello alpha")].into();
482 let rendered = render(template, &data).expect("it should render");
483 let expected = r#"
484 <html>
485 <h1>Hello<h2>
486 <em>hello alpha</em>
487 </html>"#;
488 assert_eq!(rendered, expected)
489 }
490
491 #[test]
492 fn it_performs_nested_conditions() {
493 let template = r#"
494 <html>
495 <h1>Hello<h2>{% if alpha %}
496 <em>{% alpha_str %}</em>{% else %}
497 <em>{% if beta %}beta{%else%}stable{%endif%}</em>{% endif %}
498 </html>"#;
499 let data: HashMap<&str, &str> = [
500 ("alpha", "false"),
501 ("beta", "true"),
502 ("alpha_str", "hello alpha"),
503 ]
504 .into();
505 let rendered = render(template, &data).expect("it should render");
506 let expected = r#"
507 <html>
508 <h1>Hello<h2>
509 <em>beta</em>
510 </html>"#;
511 assert_eq!(rendered, expected)
512 }
513
514 #[test]
515 fn truthy_and_falsy() {
516 let template = "<html>Hello {% if beforeDevCommand %}{% beforeDevCommand %}{% endif %}</html>";
517 let data: HashMap<&str, &str> = [("beforeDevCommand", "pnpm run")].into();
518 let rendered = render(template, &data).expect("it should render");
519 assert_eq!(rendered, "<html>Hello pnpm run</html>");
520
521 let template = "<html>Hello {% if beforeDevCommand %}{% beforeDevCommand %}{% endif %}</html>";
522 let data: HashMap<&str, &str> = [("beforeDevCommand", "")].into();
523 let rendered = render(template, &data).expect("it should render");
524 assert_eq!(rendered, "<html>Hello </html>");
525 }
526
527 #[test]
528 fn negated_value() {
529 let template = "<html>Hello {% if !name %}world{% else %}{ %name% }{%endif %}</html>";
530 let data: HashMap<&str, &str> = [("name", "")].into();
531 let rendered = render(template, &data).expect("it should render");
532 assert_eq!(rendered, "<html>Hello world</html>");
533
534 let template = "<html>Hello {% if !name %}world{% else %}{% name %}{%endif %}</html>";
535 let data: HashMap<&str, &str> = [("name", "farm")].into();
536 let rendered = render(template, &data).expect("it should render");
537 assert_eq!(rendered, "<html>Hello farm</html>");
538
539 let template = "<html>Hello {% if !render %}world{% else %}{% name %}{%endif %}</html>";
540 let data: HashMap<&str, &str> = [("render", "true"), ("name", "farm")].into();
541 let rendered = render(template, &data).expect("it should render");
542 assert_eq!(rendered, "<html>Hello farm</html>");
543
544 let template = "<html>Hello {% if !render %}world{% else %}{% name %}{%endif %}</html>";
545 let data: HashMap<&str, &str> = [("render", "false"), ("name", "farm")].into();
546 let rendered = render(template, &data).expect("it should render");
547 assert_eq!(rendered, "<html>Hello world</html>");
548 }
549
550 #[test]
551 #[should_panic]
552 fn it_panics() {
553 let template = "<html>Hello {% name }</html>";
554 let data: HashMap<&str, &str> = [("name", "world")].into();
555 render(template, &data).unwrap();
556 }
557}