ryuji_rust/
ryuji.rs

1use std::collections::{ HashMap, VecDeque };
2use std::fmt;
3use std::fs;
4use std::convert::TryFrom;
5
6#[cfg(feature = "hashmap_json")]
7use serde::Serialize;
8
9#[derive(Debug)]
10pub enum ErrorKind {
11  InvalidFileExtension,
12  IllegalVarName(String),
13  VarNotFound(String),
14  BadArgument(String),
15  MissingEndFor,
16  MissingEndIf,
17  RecursionTooDeep,
18}
19
20impl fmt::Display for ErrorKind {
21  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22    match self {
23      ErrorKind::InvalidFileExtension => write!(f, "Invalid file extension (must start with '.')"),
24      ErrorKind::IllegalVarName(var_name) => write!(f, "Illegal variable name: '{}'", var_name),
25      ErrorKind::VarNotFound(var_name) => write!(f, "Variable '{}' not found", var_name),
26      ErrorKind::BadArgument(missing_message) => write!(f, "{}", missing_message),
27      ErrorKind::MissingEndFor => write!(f, "`for:` statement missing `[[ endfor ]]`"),
28      ErrorKind::MissingEndIf => write!(f, "`if:` statement missing `[[ endif ]]`"),
29      ErrorKind::RecursionTooDeep => write!(f, "`component:` statement recursion too deep (>5)"),
30    }
31  }
32}
33
34#[derive(Debug, PartialEq)]
35pub struct FileExtension {
36  file_extension: String,
37}
38
39//todo: add errors
40impl FileExtension {
41  pub fn new(file_extension: String) -> Result<Self, ErrorKind> {
42    if file_extension.starts_with(".") {
43      Ok(FileExtension {
44        file_extension,
45      })
46    } else {
47      Err(ErrorKind::InvalidFileExtension)
48    }
49  }
50
51  pub fn get_string_ref(&self) -> &String {
52    &self.file_extension
53  }
54}
55
56impl TryFrom<String> for FileExtension {
57  type Error = ErrorKind;
58
59  fn try_from(a: String) -> Result<Self, ErrorKind> {
60    FileExtension::new(a)
61  }
62}
63
64impl From<FileExtension> for String {
65  fn from(file_extension: FileExtension) -> String {
66    file_extension.file_extension
67  }
68}
69
70
71#[derive(Clone, PartialEq)]
72#[cfg_attr(feature = "hashmap_json", derive(Debug))]
73pub enum VarValue {
74  Bool(bool),
75  String(String),
76  F64(f64),
77  U32(u32),
78  Vec(Vec<VarValue>),
79  HashMap(HashMap<String, VarValue>),
80}
81
82impl VarValue {
83  pub fn is_truthy(&self) -> bool {
84    match self {
85      Self::Bool(boolean) => *boolean,
86      Self::String(string) => string == "",
87      Self::F64(decimal) => *decimal != 0.0,
88      Self::U32(integer) => *integer != 0,
89      Self::Vec(vector) => vector.len() > 0,
90      Self::HashMap(hashmap) => hashmap.keys().len() > 0,
91    }
92  }
93}
94
95impl fmt::Display for VarValue {
96  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97    match self {
98      VarValue::Bool(boolean) => write!(f, "{}", boolean.to_string()),
99      VarValue::String(string) => write!(f, "{}", string),
100      VarValue::F64(decimal) => write!(f, "{}", decimal.to_string()),
101      VarValue::U32(integer) => write!(f, "{}", integer.to_string()),
102      VarValue::Vec(vector) => write!(f, "{:?}", vector.iter().map(|a| format!("{}", a)).collect::<Vec<String>>()),
103      VarValue::HashMap(_hashmap) => write!(f, "Enable the `hashmap_json` crate feature"),
104    }
105  }
106}
107
108#[cfg(feature = "hashmap_json")]
109impl fmt::Display for VarValue {
110  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111    match self {
112      VarValue::Bool(boolean) => write!(f, "{}", boolean.to_string()),
113      VarValue::String(string) => write!(f, "{}", string),
114      VarValue::F64(decimal) => write!(f, "{}", decimal.to_string()),
115      VarValue::U32(integer) => write!(f, "{}", integer.to_string()),
116      VarValue::Vec(vector) => write!(f, "{:?}", vector.iter().map(|a| format!("{}", a)).collect::<Vec<String>>()),
117      VarValue::HashMap(hashmap) => write!(f, "{}", serde_json::to_string(hashmap)),
118    }
119  }
120}
121
122#[derive(Clone, Debug, PartialEq)]
123pub struct SyntaxMatch {
124  pub content: String,
125  pub index: usize, //start index in text
126}
127
128pub struct ForLoopInfo {
129  index: usize,
130  total: usize,
131  current: usize,
132  var_value: Vec<VarValue>, //value we are looping over
133  iter_var_name: Option<String>,
134  index_var_name: Option<String>,
135}
136
137pub type Vars = HashMap<String, VarValue>;
138
139pub struct Renderer {
140  pub templates_dir: String,
141  pub components_dir: String,
142  pub file_extension: FileExtension,
143}
144
145impl Renderer {
146  pub fn new(templates_dir: String, components_dir: String, file_extension: FileExtension) -> Self {
147    Self {
148      templates_dir,
149      components_dir,
150      file_extension,
151    }
152  }
153
154  pub fn concat_path(path1: &String, path2: &String) -> String {
155    if path1.ends_with("/") && path2.starts_with("/") {
156      let mut path1: String = path1.clone();
157      path1.truncate(path1.len()-1);
158      format!("{}{}", path1, path2)
159    } else if !path1.ends_with("/") && !path2.starts_with("/") {
160      format!("{}/{}", path1, path2)
161    } else {
162      format!("{}{}", path1, path2)
163    }
164  }
165
166  pub fn sanitize(text: &String) -> String {
167    text.replace("<", "&lt;").replace(">", "&gt;")
168  }
169
170  pub fn check_var_name_legality(var_name: &String, dot_allowed: bool) -> Result<(), ErrorKind> {
171    let mut legal_chars: Vec<char> = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', '/', '.'];
172    if !dot_allowed {
173      legal_chars.pop();
174    }
175    //if any of them are not in the legal chars
176    let fail: bool = var_name.chars().any(|c| !legal_chars.contains(&c.to_ascii_lowercase()));
177    if fail {
178      Err(ErrorKind::IllegalVarName(var_name.clone()))
179    } else {
180      Ok(())
181    }
182  }
183
184  pub fn find_syntax_matches(template_content: &String) -> Vec<SyntaxMatch> {
185    let legal_chars: Vec<char> = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', '.', ':', '-', '!'];
186    let mut matches: Vec<SyntaxMatch> = Vec::new();
187    //"[[  ]]"
188    let chars: Vec<char> = template_content.chars().collect();
189    let mut in_match: bool = false;
190    let mut match_index: usize = 0; //start index of match
191    for index in 0..chars.len() {
192      let current_char: char = chars[index];
193      if index > 1 && index < chars.len()-2 {
194        if current_char == ' ' && chars[index-1] == '[' && chars[index-2] == '[' {
195          in_match = true;
196          match_index = index-2;
197        } else if in_match && chars[index] == ' ' && chars[index+1] == ']' && chars[index+2] == ']' {
198          in_match = false;
199          matches.push(SyntaxMatch {
200            index: match_index,
201            content: chars[match_index..index+3].iter().collect(),
202          });
203        } else if in_match && !legal_chars.contains(&current_char.to_ascii_lowercase()) {
204          in_match = false;
205        }
206      }
207    }
208    matches
209  }
210
211  pub fn get_var(var_name: String, vars: &Vars) -> Result<&VarValue, ErrorKind> {
212    Self::check_var_name_legality(&var_name, true)?;
213    let mut parts: VecDeque<&str> = var_name.split(".").into_iter().collect();
214    let part_uno: &str = parts.pop_front().unwrap();
215    let var_value_unwrapped: &Option<&VarValue> = &vars.get(part_uno);
216    if var_value_unwrapped.is_none() {
217      //bad
218      return Err(ErrorKind::VarNotFound(var_name));
219    }
220    let mut var_value = var_value_unwrapped.unwrap();
221    for part in parts {
222      if let VarValue::HashMap(var_value_hashmap) = &var_value {
223        let var_value_hashmap_unwrapped: Option<&VarValue> = var_value_hashmap.get(part);
224        if var_value_hashmap_unwrapped.is_none() {
225          //bad
226          return Err(ErrorKind::VarNotFound(var_name));
227        }
228        var_value = var_value_hashmap_unwrapped.unwrap();
229      } else {
230        //bad
231        return Err(ErrorKind::VarNotFound(var_name));
232      }
233    }
234    Ok(var_value)
235  }
236
237  pub fn render(&self, template_contents: String, vars: &mut Vars, recursion_layer: Option<usize>) -> Result<String, ErrorKind> {
238    let recursion_layer: usize = recursion_layer.unwrap_or(0);
239    let syntax_matches: Vec<SyntaxMatch> = Self::find_syntax_matches(&template_contents);
240    if syntax_matches.len() == 0 {
241      return Ok(template_contents);
242    }
243    let mut rendered: String = template_contents[0..syntax_matches[0].index].to_string();
244    let mut for_loops: Vec<ForLoopInfo> = vec![];
245    let mut index: usize = 0;
246    let mut iterations_: usize = 0;
247    loop {
248      if index == syntax_matches.len() {
249        break;
250      }
251      if iterations_ > 75000 {
252        println!("Passed 75000 iterations while rendering, infinite loop?");
253      }
254      let syntax_match: &SyntaxMatch = &syntax_matches[index];
255      let exp_parts: Vec<&str> = syntax_match.content[3..syntax_match.content.len()-3].split(":").collect();
256      if exp_parts.len() < 1 {
257        return Err(ErrorKind::BadArgument("An empty '[[ ]]' is not valid".to_string()));
258      }
259      if exp_parts[0] == "component" {
260        //we do not want get into an infinite recursion loop with components referring to each other
261        if recursion_layer > 5 {
262          return Err(ErrorKind::RecursionTooDeep);
263        }
264        if exp_parts.len() != 2 {
265          return Err(ErrorKind::BadArgument("`component:` statement missing component name (second arg), or more than two args".to_string()));
266        }
267        let mut file_name: String = exp_parts[1].to_string();
268        if !file_name.contains(".") {
269          file_name += self.file_extension.get_string_ref();
270        }
271        rendered += &self.render_template(Self::concat_path(&self.components_dir, &file_name), vars, Some(recursion_layer+1))?;
272      } else if exp_parts[0] == "for" {
273        let mut already_exists: bool = false;
274        let most_recent: Option<&ForLoopInfo> = for_loops.last();
275        if most_recent.is_some() {
276          if most_recent.unwrap().index == index {
277            //for loop already exists, just continue and do nothing
278            already_exists = true;
279          }
280        }
281        if !already_exists {
282          //variables in for loops are not scoped because that would be too much work
283          if exp_parts.len() < 2 {
284            return Err(ErrorKind::BadArgument("`for:` statement missing variable name to loop over (second arg)".to_string()));
285          }
286          let var_name: String = exp_parts[1].to_string();
287          let var_value: &VarValue = Self::get_var(var_name, &vars)?;
288          if let VarValue::Vec(vec_value) = var_value {
289            let vec_value_cloned = vec_value.clone();
290            let vec_length: usize = vec_value.len().clone();
291            let iter_var_name: Option<String>;
292            if exp_parts.len() >= 3 {
293              //set iter variable (optional) (you know, the "post" in "for post in posts")
294              //(I don't know what the actual name of that thing is)
295              let iter_var_name_: String = exp_parts[2].to_string();
296              Self::check_var_name_legality(&iter_var_name_, false)?;
297              iter_var_name = Some(iter_var_name_.clone());
298              //if vec is empty, that is handled later on
299              if vec_value.len() > 0 {
300                vars.insert(iter_var_name_, vec_value[0].clone());
301              }
302            } else {
303              iter_var_name = None;
304            }
305            let index_var_name: Option<String>;
306            if exp_parts.len() >= 4 {
307              //set index count
308              let index_var_name_: String = exp_parts[3].to_string();
309              Self::check_var_name_legality(&index_var_name_, false)?;
310              index_var_name = Some(index_var_name_.clone());
311              vars.insert(index_var_name_, VarValue::U32(0));
312            } else {
313              index_var_name = None;
314            }
315            if exp_parts.len() >= 5 {
316              //set max count
317              let max_var_name: String = exp_parts[4].to_string();
318              Self::check_var_name_legality(&max_var_name, false)?;
319              vars.insert(max_var_name, VarValue::U32(vec_length as u32-1));
320            }
321            for_loops.push(ForLoopInfo {
322              index,
323              total: vec_length,
324              current: 0,
325              var_value: vec_value_cloned,
326              iter_var_name,
327              index_var_name,
328            });
329            //make sure thing we are iterating over isn't empty
330            if vec_length == 0 {
331              //skip straight to the endfor
332              let sliced: Vec<SyntaxMatch> = syntax_matches[index+1..syntax_matches.len()].to_vec();
333              let mut new_index: Option<usize> = None;
334              let mut extra_fors: usize = 0;
335              for i in 0..sliced.len() {
336                let match_content: &String = &sliced[i].content;
337                if match_content.starts_with("[[ for:") {
338                  extra_fors += 1;
339                } else if match_content == "[[ endfor ]]" {
340                  if extra_fors == 0 {
341                    new_index = Some(i);
342                    break;
343                  }
344                  extra_fors -= 1;
345                }
346              }
347              if new_index.is_none() {
348                //`for:` statement missing `[[ endfor ]]`
349                return Err(ErrorKind::MissingEndFor);
350              }
351              index += new_index.unwrap()+1;
352              continue;
353            }
354          } else {
355            return Err(ErrorKind::BadArgument("variable being looped over in `for:` statement is not a vector".to_string()));
356          }
357        }
358      } else if exp_parts[0] == "endfor" {
359        //check if for loop is over, if not, go back to for
360        let for_loops_len: usize = for_loops.len();
361        let current_loop: &mut ForLoopInfo = &mut for_loops[for_loops_len-1];
362        current_loop.current += 1;
363        if current_loop.current >= current_loop.total {
364          //for loop ended, onwards! oh yeah, also remove the current for loop info
365          for_loops.pop();
366        } else {
367          //update iter var
368          if current_loop.iter_var_name.is_some() {
369            vars.insert(current_loop.iter_var_name.clone().unwrap(), current_loop.var_value[current_loop.current].clone());
370          }
371          if current_loop.index_var_name.is_some() {
372            vars.insert(current_loop.index_var_name.clone().unwrap(), VarValue::U32(current_loop.current.clone() as u32));
373          }
374          //go back to start of for loop index
375          index = current_loop.index;
376          continue;
377        }
378      } else if exp_parts[0] == "if" {
379        if exp_parts.len() < 2 {
380          return Err(ErrorKind::BadArgument("`if:` statement missing variable name (second arg)".to_string()));
381        }
382        let var_name: String = exp_parts[1].to_string();
383        let var_value: &VarValue = Self::get_var(var_name, &vars)?;
384        let condition_pass: bool;
385        if exp_parts.len() == 2 {
386          //make sure var is truthy
387          if var_value.is_truthy() {
388            condition_pass = true;
389          } else {
390            condition_pass = false;
391          }
392        } else if exp_parts.len() == 3 {
393          //compare with second var
394          let mut var_name2: String = exp_parts[2].to_string();
395          let mut if_not: bool = false;
396          if var_name2.starts_with("!") {
397            var_name2 = var_name2[1..var_name2.len()].to_string();
398            if_not = true;
399          }
400          let var_value2: &VarValue = Self::get_var(var_name2, &vars)?;
401          if if_not {
402            //make sure the two compared variables are NOT equal
403            if var_value != var_value2 {
404              condition_pass = true;
405            } else {
406              condition_pass = false;
407            }
408          } else {
409            //regular comparison statement
410            if var_value == var_value2 {
411              condition_pass = true;
412            } else {
413              condition_pass = false;
414            }
415          }
416        } else {
417          return Err(ErrorKind::BadArgument("`if:` statement cannot have more than 3 args".to_string()));
418        }
419        if !condition_pass { //failed condition
420          //skip to the endif
421          let sliced: Vec<SyntaxMatch> = syntax_matches[index+1..syntax_matches.len()].to_vec();
422          let mut new_index: Option<usize> = None;
423          let mut extra_ifs: usize = 0;
424          for i in 0..sliced.len() {
425            let match_content: &String = &sliced[i].content;
426            if match_content.starts_with("[[ if:") {
427              extra_ifs += 1;
428            } else if match_content == "[[ endif ]]" {
429              if extra_ifs == 0 {
430                new_index = Some(i);
431                break;
432              }
433              extra_ifs -= 1;
434            }
435          }
436          if new_index.is_none() {
437            //`if:` statement missing `[[ endif ]]`
438            return Err(ErrorKind::MissingEndIf);
439          }
440          index += new_index.unwrap()+1;
441          continue;
442        }
443      } else if exp_parts[0] == "endif" {
444        //yup, nothing here
445      } else { //html:<variable name> or <variable name>
446        //variable
447        let var_name: String;
448        if exp_parts[0] == "html" {
449          if exp_parts.len() != 2 {
450            return Err(ErrorKind::BadArgument("`html:` statement missing variable name, the second arg, or has more than two args".to_string()));
451          }
452          var_name = exp_parts[1].to_string();
453        } else {
454          var_name = exp_parts[0].to_string();
455        }
456        //convert to string
457        let var_value_string: String = (Self::get_var(var_name, &vars)?).to_string();
458        //add indentation
459        let current_last = rendered.split("\n").last().unwrap();
460        let mut indentation: usize = 0;
461        for i in 0..current_last.len() {
462          if current_last.chars().nth(i).unwrap() != ' ' {
463            break;
464          }
465          indentation += 1;
466        }
467        let mut var_lines: VecDeque<&str> = var_value_string.split("\n").into_iter().collect();
468        let var_first: &str = var_lines.pop_front().unwrap();
469        //append spaces
470        let var_value: String;
471        if var_lines.len() == 0 {
472          var_value = var_first.to_string();
473        } else {
474          var_value = format!("{}\n{}", var_first, var_lines.into_iter().map(
475            |var_line| {
476              " ".repeat(indentation)+var_line
477            }
478          ).collect::<Vec<String>>().join("\n"));
479        }
480        if exp_parts[0] == "html" {
481          //variable but not sanitized
482          rendered += &var_value;
483        } else {
484          rendered += &Self::sanitize(&var_value);
485        }
486      }
487      if index != syntax_matches.len()-1 {
488        //add the html that comes after this, up until the next template syntax match thing
489        rendered += &template_contents[syntax_match.index+syntax_match.content.len()..syntax_matches[index+1].index];
490      } else {
491        //last index, add all the way till end of template
492        rendered += &template_contents[syntax_match.index+syntax_match.content.len()..template_contents.len()];
493      }
494      index += 1;
495      iterations_ += 1;
496    }
497    Ok(rendered)
498  }
499
500  pub fn render_template(&self, template_name: String, vars: &mut Vars, recursion_layer: Option<usize>) -> Result<String, ErrorKind> {
501    let mut template_file_name = template_name;
502    if !template_file_name.contains(".") {
503      template_file_name += self.file_extension.get_string_ref();
504    }
505    let content: String = fs::read_to_string(Self::concat_path(&self.templates_dir, &template_file_name)).unwrap();
506    self.render(content, vars, recursion_layer)
507  }
508}