create_farm/utils/
lte.rs

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          // no need to do anything, return early
239          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}