template_nest/
lib.rs

1//! Template engine for Rust.
2//!
3//! For more details on the idea behind `Template::Nest` read:
4//! - <https://metacpan.org/pod/Template::Nest#DESCRIPTION>
5//! - <https://pypi.org/project/template-nest/>
6
7//! # Examples
8//!
9//! More examples in `examples/` directory.
10//!
11//! Place these files in `templates` directory:
12//! `templates/00-simple-page.html`:
13//! ```html
14//! <!DOCTYPE html>
15//! <html lang="en">
16//!   <head>
17//!     <meta charset="utf-8">
18//!     <meta name="viewport" content="width=device-width, initial-scale=1">
19//!     <title>Simple Page</title>
20//!   </head>
21//!   <body>
22//!     <p>A fairly simple page to test the performance of Template::Nest.</p>
23//!     <p><!--% variable %--></p>
24//!     <!--% simple_component %-->
25//!   </body>
26//! </html>
27//! ```
28//!
29//! `templates/00-simple-page.html`:
30//! ```html
31//! <p><!--% variable %--></p>
32//! ```
33//!
34//! Those templates can be used in a template hash which is passed to
35//! TemplateNest::render to render a page:
36//! ```rust
37//! use template_nest::TemplateNest;
38//! use template_nest::{filling, Filling};
39//! use std::collections::HashMap;
40//!
41//! let nest = TemplateNest::new("templates").unwrap();
42//! let simple_page = filling!(
43//!     "TEMPLATE": "00-simple-page",
44//!     "variable": "Simple Variable",
45//!     "simple_component":  {
46//!         "TEMPLATE":"01-simple-component",
47//!         "variable": "Simple Variable in Simple Component"
48//!     }
49//! );
50//! println!("{}", nest.render(&simple_page).unwrap());
51//! ```
52
53use std::collections::{HashMap, HashSet};
54use std::path::PathBuf;
55use std::{fs, io};
56
57use regex::Regex;
58use thiserror::Error;
59
60/// Represents a variable in a template hash, can be a string, another template
61/// hash or an array of template hash.
62pub enum Filling {
63    Text(String),
64    List(Vec<Filling>),
65    Template(HashMap<String, Filling>),
66}
67
68impl Filling {
69    /// inserts into a Template, returns an Err if the enum if not of Template
70    /// variant.
71    pub fn insert(&mut self, variable: String, to_insert: Filling) -> Result<(), &'static str> {
72        match self {
73            Filling::Template(ref mut map) => {
74                map.insert(variable, to_insert);
75                Ok(())
76            }
77            _ => Err("Cannot insert into non-Template variant"),
78        }
79    }
80
81    /// push into a List, returns an Err if the enum if not of List variant.
82    pub fn push(&mut self, to_push: Filling) -> Result<(), &'static str> {
83        match self {
84            Filling::List(ref mut list) => {
85                list.push(to_push);
86                Ok(())
87            }
88            _ => Err("Cannot push into non-List variant"),
89        }
90    }
91}
92
93#[derive(Error, Debug)]
94pub enum TemplateNestError {
95    #[error("expected template directory at `{0}`")]
96    TemplateDirNotFound(String),
97
98    #[error("expected template file at `{0}`")]
99    TemplateFileNotFound(String),
100
101    #[error("error reading: `{0}`")]
102    TemplateFileReadError(#[from] io::Error),
103
104    #[error("encountered hash with no name label (name label: `{0}`)")]
105    NoNameLabel(String),
106
107    #[error("encountered hash with invalid name label type (name label: `{0}`)")]
108    InvalidNameLabel(String),
109
110    #[error("bad params in template hash, variable not present in template file: `{0}`")]
111    BadParams(String),
112}
113
114/// Renders a template hash to produce an output.
115pub struct TemplateNest<'a> {
116    /// Delimiters used in the template. It is a tuple of two strings,
117    /// representing the start and end delimiters.
118    pub delimiters: (&'a str, &'a str),
119
120    /// Name label used to identify the template to be used.
121    pub label: &'a str,
122
123    /// Template extension, appended on label to identify the template.
124    pub extension: &'a str,
125
126    /// Directory where templates are located.
127    pub directory: PathBuf,
128
129    /// Prepend & Append a string to every template which is helpful in
130    /// identifying which template the output text came from.
131    pub show_labels: bool,
132
133    /// Used in conjunction with show_labels. If the template is HTML then use
134    /// '<!--', '-->'.
135    pub comment_delimiters: (&'a str, &'a str),
136
137    /// Intended to improve readability when inspecting nested templates.
138    pub fixed_indent: bool,
139
140    /// If True, then an attempt to populate a template with a variable that
141    /// doesn't exist (i.e. name not found in template file) results in an
142    /// error.
143    pub die_on_bad_params: bool,
144
145    /// Escapes a token delimiter, i.e. if set to '\' then prefixing the token
146    /// delimiters with '\' means it won't be considered a variable.
147    ///
148    /// <!--% token %-->  => is a variable
149    /// \<!--% token %--> => is not a variable. ('\' is removed from output)
150    pub token_escape_char: &'a str,
151
152    /// Provide a hash of default values that are substituted if template hash
153    /// does not provide a value.
154    pub defaults: HashMap<String, Filling>,
155}
156
157/// Represents an indexed template file.
158struct TemplateFileIndex {
159    /// Contents of the file.
160    contents: String,
161
162    /// Variables in the template file.
163    variables: Vec<TemplateFileVariable>,
164
165    /// Variable names in the template file.
166    variable_names: HashSet<String>,
167}
168
169/// Represents the variables in a template file.
170struct TemplateFileVariable {
171    name: String,
172
173    /// Start & End positions of the complete variable string. i.e. including
174    /// the delimeters.
175    start_position: usize,
176    end_position: usize,
177
178    /// Indent level of the variable.
179    indent_level: usize,
180
181    /// If true then this variable was escaped with token_escape_char, we just
182    /// need to remove the escape character.
183    escaped_token: bool,
184}
185
186impl Default for TemplateNest<'_> {
187    fn default() -> Self {
188        TemplateNest {
189            label: "TEMPLATE",
190            extension: "html",
191            show_labels: false,
192            fixed_indent: false,
193            die_on_bad_params: false,
194            directory: "templates".into(),
195            delimiters: ("<!--%", "%-->"),
196            comment_delimiters: ("<!--", "-->"),
197            token_escape_char: "",
198            defaults: HashMap::new(),
199        }
200    }
201}
202
203impl TemplateNest<'_> {
204    /// Creates a new instance of TemplateNest with the specified directory.
205    pub fn new(directory_str: &str) -> Result<Self, TemplateNestError> {
206        let directory = PathBuf::from(directory_str);
207        if !directory.is_dir() {
208            return Err(TemplateNestError::TemplateDirNotFound(
209                directory_str.to_string(),
210            ));
211        }
212
213        Ok(Self {
214            directory,
215            ..Default::default()
216        })
217    }
218
219    /// Given a template name, returns the "index" of the template file, it
220    /// contains the contents of the file and all the variables that are
221    /// present.
222    fn index(&self, template_name: &str) -> Result<TemplateFileIndex, TemplateNestError> {
223        let file = self
224            .directory
225            .join(format!("{}.{}", template_name, self.extension));
226        if !file.is_file() {
227            return Err(TemplateNestError::TemplateFileNotFound(
228                file.display().to_string(),
229            ));
230        }
231
232        let contents = match fs::read_to_string(&file) {
233            Ok(file_contents) => file_contents,
234            Err(err) => {
235                return Err(TemplateNestError::TemplateFileReadError(err));
236            }
237        };
238
239        let mut variable_names = HashSet::new();
240        let mut variables = vec![];
241        // Capture all the variables in the template.
242        let re = Regex::new(&format!("{}(.+?){}", self.delimiters.0, self.delimiters.1)).unwrap();
243        for cap in re.captures_iter(&contents) {
244            let whole_capture = cap.get(0).unwrap();
245            let start_position = whole_capture.start();
246
247            // If token_escape_char is set then look behind for it and if we
248            // find the escape char then we're only going to remove the escape
249            // char and not remove this variable.
250            //
251            // The variable can be at the beginning of the file, that will mean
252            // calculating escape_char_start results in an overflow.
253            if !self.token_escape_char.is_empty() && start_position > self.token_escape_char.len() {
254                let escape_char_start = start_position - self.token_escape_char.len();
255                if &contents[escape_char_start..start_position] == self.token_escape_char {
256                    variables.push(TemplateFileVariable {
257                        indent_level: 0,
258                        name: "".to_string(),
259                        escaped_token: true,
260                        start_position: escape_char_start,
261                        end_position: escape_char_start + self.token_escape_char.len(),
262                    });
263                    continue;
264                }
265            }
266
267            // If fixed_indent is enable then record the indent level for this
268            // variable. To get the indent level we look at each character in
269            // reverse from the start position of the variable until we find a
270            // newline character.
271            let indent_level = match self.fixed_indent {
272                true => {
273                    let newline_position = &contents[..start_position].rfind('\n').unwrap_or(0);
274                    start_position - newline_position - 1
275                }
276                false => 0,
277            };
278
279            let variable_name = cap[1].trim();
280            variable_names.insert(variable_name.to_string());
281            variables.push(TemplateFileVariable {
282                indent_level,
283                start_position,
284                end_position: whole_capture.end(),
285                name: variable_name.to_string(),
286                escaped_token: false,
287            });
288        }
289
290        let file_index = TemplateFileIndex {
291            variable_names,
292            contents,
293            variables,
294        };
295        Ok(file_index)
296    }
297
298    /// Given a TemplateHash, it parses the TemplateHash and renders a String
299    /// output.
300    pub fn render(&self, filling: &Filling) -> Result<String, TemplateNestError> {
301        match filling {
302            Filling::Text(text) => Ok(text.to_string()),
303            Filling::List(list) => {
304                let mut render = "".to_string();
305                for f in list {
306                    render.push_str(&self.render(f)?);
307                }
308                Ok(render)
309            }
310            Filling::Template(template_hash) => {
311                let template_label: &Filling = template_hash
312                    .get(self.label)
313                    .ok_or(TemplateNestError::NoNameLabel(self.label.to_string()))?;
314
315                // template_name must contain a string, it cannot be a template hash or
316                // a vec of template hash.
317                if let Filling::Text(name) = template_label {
318                    let template_index = self.index(name)?;
319
320                    // Check for bad params.
321                    if self.die_on_bad_params {
322                        for name in template_hash.keys() {
323                            // If a variable in template_hash is not present in
324                            // the template file and it's not the template label
325                            // then it's a bad param.
326                            if !template_index.variable_names.contains(name) && name != self.label {
327                                return Err(TemplateNestError::BadParams(name.to_string()));
328                            }
329                        }
330                    }
331
332                    let mut rendered = String::from(&template_index.contents);
333
334                    // Iterate through all variables in reverse. We do this because we
335                    // don't want to mess up all the indexed positions.
336                    for var in template_index.variables.iter().rev() {
337                        // If the variable was escaped then we just remove the token, not the variable.
338                        if var.escaped_token {
339                            rendered.replace_range(var.start_position..var.end_position, "");
340                            continue;
341                        }
342
343                        // If the variable doesn't exist in template hash then
344                        // replace it by an empty string.
345                        let mut render = "".to_string();
346
347                        // Look for the variable in template_hash, if it's not
348                        // provided then we look at defaults HashMap, and then
349                        // considering variable namespacing.
350                        if let Some(value) = template_hash
351                            .get(&var.name)
352                            .or_else(|| self.defaults.get(&var.name))
353                        {
354                            let mut r: String = self.render(value)?;
355
356                            // If fixed_indent is set then get the indent level
357                            // and replace all newlines in the rendered string.
358                            if self.fixed_indent && var.indent_level != 0 {
359                                let replacement = format!("\n{}", " ".repeat(var.indent_level));
360                                r = r.replace('\n', &replacement);
361                            }
362
363                            render.push_str(&r);
364                        }
365
366                        rendered.replace_range(var.start_position..var.end_position, &render);
367                    }
368
369                    // Add lables to the rendered string if show_labels is true.
370                    if self.show_labels {
371                        rendered.replace_range(
372                            0..0,
373                            &format!(
374                                "{} BEGIN {} {}\n",
375                                self.comment_delimiters.0, name, self.comment_delimiters.1
376                            ),
377                        );
378                        rendered.replace_range(
379                            rendered.len()..rendered.len(),
380                            &format!(
381                                "{} END {} {}\n",
382                                self.comment_delimiters.0, name, self.comment_delimiters.1
383                            ),
384                        );
385                    }
386
387                    // Trim trailing without cloning `rendered'.
388                    let len_withoutcrlf = rendered.trim_end().len();
389                    rendered.truncate(len_withoutcrlf);
390
391                    Ok(rendered)
392                } else {
393                    Err(TemplateNestError::InvalidNameLabel(self.label.to_string()))
394                }
395            }
396        }
397    }
398}
399
400// The below macros are adapted from the json-rust macros (https://docs.rs/json/latest/json/#macros)
401#[macro_export]
402macro_rules! filling_list {
403    //[] => ($crate::JsonValue::new_array());
404    [] => { "".to_string() };
405
406    // Handles for token tree items
407    [@ITEM($( $i:expr, )*) $item:tt, $( $cont:tt )+] => {
408        $crate::filling_list!(
409            @ITEM($( $i, )* $crate::filling_text!($item), )
410                $( $cont )*
411        )
412    };
413    (@ITEM($( $i:expr, )*) $item:tt,) => ({
414        $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
415    });
416    (@ITEM($( $i:expr, )*) $item:tt) => ({
417        $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
418    });
419
420    // Handles for expression items
421    [@ITEM($( $i:expr, )*) $item:expr, $( $cont:tt )+] => {
422        $crate::filling_list!(
423            @ITEM($( $i, )* $crate::filling_text!($item), )
424                $( $cont )*
425        )
426    };
427    (@ITEM($( $i:expr, )*) $item:expr,) => ({
428        $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
429    });
430    (@ITEM($( $i:expr, )*) $item:expr) => ({
431        $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
432    });
433
434    // Construct the actual array
435    (@END $( $i:expr, )*) => ({
436        let size = 0 $( + {let _ = &$i; 1} )*;
437        let mut vec: Vec<Filling> = Vec::with_capacity(size);
438
439        $(
440            vec.push($i);
441        )*
442
443            $crate::Filling::List( vec )
444    });
445
446    // Entry point to the macro
447    ($( $cont:tt )+) => {
448        $crate::filling_list!(@ITEM() $($cont)*)
449    };
450}
451
452/// Helper macro for converting types into `Filling::Text`. It's used internally
453/// by the `filling!` and `filling_list!` macros.
454#[macro_export]
455macro_rules! filling_text {
456    //( null ) => { $crate::Null };
457    ( null ) => { "".to_string() };
458    ( [$( $token:tt )*] ) => {
459        // 10
460        $crate::filling_list![ $( $token )* ]
461    };
462    ( {$( $token:tt )*} ) => {
463        $crate::filling!{ $( $token )* }
464    };
465    { $value:expr } => { $crate::Filling::Text($value.to_string()) };
466}
467
468/// Helper macro for creating instances of `Filling`.
469#[macro_export]
470macro_rules! filling {
471    {} => { "".to_string() };
472
473    // Handles for different types of keys
474    (@ENTRY($( $k:expr => $v:expr, )*) $key:ident: $( $cont:tt )*) => {
475        $crate::filling!(@ENTRY($( $k => $v, )*) stringify!($key) => $($cont)*)
476    };
477    (@ENTRY($( $k:expr => $v:expr, )*) $key:literal: $( $cont:tt )*) => {
478        $crate::filling!(@ENTRY($( $k => $v, )*) $key => $($cont)*)
479    };
480    (@ENTRY($( $k:expr => $v:expr, )*) [$key:expr]: $( $cont:tt )*) => {
481        $crate::filling!(@ENTRY($( $k => $v, )*) $key => $($cont)*)
482    };
483
484    // Handles for token tree values
485    (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt, $( $cont:tt )+) => {
486        $crate::filling!(
487            @ENTRY($( $k => $v, )* $key => $crate::filling_text!($value), )
488                $( $cont )*
489        )
490    };
491    (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt,) => ({
492        $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
493    });
494    (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt) => ({
495        $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
496    });
497
498    // Handles for expression values
499    (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr, $( $cont:tt )+) => {
500        $crate::filling!(
501            @ENTRY($( $k => $v, )* $key => $crate::filling_text!($value), )
502                $( $cont )*
503        )
504    };
505    (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr,) => ({
506        $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
507    });
508
509    (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr) => ({
510        $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
511    });
512
513    // Construct the actual object
514    (@END $( $k:expr => $v:expr, )*) => ({
515        let mut params : HashMap<String, Filling> = Default::default();
516        $(
517            params.insert($k.to_string(), $v);
518        )*
519            let template = $crate::Filling::Template( params );
520        template
521    });
522
523    // Entry point to the macro
524    ($key:tt: $( $cont:tt )+) => {
525        $crate::filling!(@ENTRY() $key: $($cont)*)
526    };
527
528    // Legacy macro
529    ($( $k:expr => $v:expr, )*) => {
530        $crate::filling!(@END $( $k => $crate::filling_text!($v), )*)
531    };
532    ($( $k:expr => $v:expr ),*) => {
533        $crate::filling!(@END $( $k => $crate::filling_text!($v), )*)
534    };
535}