tera/
template.rs

1use std::collections::HashMap;
2
3use crate::errors::{Error, Result};
4use crate::parser::ast::{Block, MacroDefinition, Node};
5use crate::parser::{parse, remove_whitespace};
6
7/// This is the parsed equivalent of a template file.
8/// It also does some pre-processing to ensure it does as little as possible at runtime
9/// Not meant to be used directly.
10#[derive(Debug, Clone)]
11pub struct Template {
12    /// Name of the template, usually very similar to the path
13    pub name: String,
14    /// Original path of the file. A template doesn't necessarily have
15    /// a file associated with it though so it's optional.
16    pub path: Option<String>,
17    /// Parsed AST, after whitespace removal
18    pub ast: Vec<Node>,
19    /// Whether this template came from a call to `Tera::extend`, so we do
20    /// not remove it when we are doing a template reload
21    pub from_extend: bool,
22
23    /// Macros defined in that file: name -> definition ast
24    pub macros: HashMap<String, MacroDefinition>,
25    /// (filename, namespace) for the macros imported in that file
26    pub imported_macro_files: Vec<(String, String)>,
27
28    /// Only used during initial parsing. Rendering will use `self.parents`
29    pub parent: Option<String>,
30    /// Only used during initial parsing. Rendering will use `self.blocks_definitions`
31    pub blocks: HashMap<String, Block>,
32
33    // Below are filled when all templates have been parsed so we know the full hierarchy of templates
34    /// The full list of parent templates
35    pub parents: Vec<String>,
36    /// The definition of all the blocks for the current template and the definition of those blocks
37    /// in parent templates if there are some.
38    /// Needed for super() to work without having to find them each time.
39    /// The type corresponds to the following `block_name -> [(template name, definition)]`
40    /// The order of the Vec is from the first in hierarchy to the current template and the template
41    /// name is needed in order to load its macros if necessary.
42    pub blocks_definitions: HashMap<String, Vec<(String, Block)>>,
43}
44
45impl Template {
46    /// Parse the template string given
47    pub fn new(tpl_name: &str, tpl_path: Option<String>, input: &str) -> Result<Template> {
48        let ast = remove_whitespace(parse(input)?, None);
49
50        // First we want all the blocks used in that template
51        // This is recursive as we can have blocks inside blocks
52        let mut blocks = HashMap::new();
53        fn find_blocks(ast: &[Node], blocks: &mut HashMap<String, Block>) -> Result<()> {
54            for node in ast {
55                match *node {
56                    Node::Block(_, ref block, _) => {
57                        if blocks.contains_key(&block.name) {
58                            return Err(Error::msg(format!(
59                                "Block `{}` is duplicated",
60                                block.name
61                            )));
62                        }
63
64                        blocks.insert(block.name.to_string(), block.clone());
65                        find_blocks(&block.body, blocks)?;
66                    }
67                    _ => continue,
68                };
69            }
70
71            Ok(())
72        }
73        find_blocks(&ast, &mut blocks)?;
74
75        // And now we find the potential parent and everything macro related (definition, import)
76        let mut macros = HashMap::new();
77        let mut imported_macro_files = vec![];
78        let mut parent = None;
79
80        for node in &ast {
81            match *node {
82                Node::Extends(_, ref name) => parent = Some(name.to_string()),
83                Node::MacroDefinition(_, ref macro_def, _) => {
84                    if macros.contains_key(&macro_def.name) {
85                        return Err(Error::msg(format!(
86                            "Macro `{}` is duplicated",
87                            macro_def.name
88                        )));
89                    }
90                    macros.insert(macro_def.name.clone(), macro_def.clone());
91                }
92                Node::ImportMacro(_, ref tpl_name, ref namespace) => {
93                    imported_macro_files.push((tpl_name.to_string(), namespace.to_string()));
94                }
95                _ => continue,
96            }
97        }
98
99        Ok(Template {
100            name: tpl_name.to_string(),
101            path: tpl_path,
102            ast,
103            parent,
104            blocks,
105            macros,
106            imported_macro_files,
107            parents: vec![],
108            blocks_definitions: HashMap::new(),
109            from_extend: false,
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::Template;
117
118    #[test]
119    fn can_parse_ok_template() {
120        Template::new("hello", None, "Hello {{ world }}.").unwrap();
121    }
122
123    #[test]
124    fn can_find_parent_template() {
125        let tpl = Template::new("hello", None, "{% extends \"base.html\" %}").unwrap();
126
127        assert_eq!(tpl.parent.unwrap(), "base.html".to_string());
128    }
129
130    #[test]
131    fn can_find_blocks() {
132        let tpl = Template::new(
133            "hello",
134            None,
135            "{% extends \"base.html\" %}{% block hey %}{% endblock hey %}",
136        )
137        .unwrap();
138
139        assert_eq!(tpl.parent.unwrap(), "base.html".to_string());
140        assert!(tpl.blocks.contains_key("hey"));
141    }
142
143    #[test]
144    fn can_find_nested_blocks() {
145        let tpl = Template::new(
146            "hello",
147            None,
148            "{% extends \"base.html\" %}{% block hey %}{% block extrahey %}{% endblock extrahey %}{% endblock hey %}",
149        ).unwrap();
150
151        assert_eq!(tpl.parent.unwrap(), "base.html".to_string());
152        assert!(tpl.blocks.contains_key("hey"));
153        assert!(tpl.blocks.contains_key("extrahey"));
154    }
155
156    #[test]
157    fn can_find_macros() {
158        let tpl = Template::new("hello", None, "{% macro hey() %}{% endmacro hey %}").unwrap();
159        assert!(tpl.macros.contains_key("hey"));
160    }
161
162    #[test]
163    fn can_find_imported_macros() {
164        let tpl = Template::new("hello", None, "{% import \"macros.html\" as macros %}").unwrap();
165        assert_eq!(
166            tpl.imported_macro_files,
167            vec![("macros.html".to_string(), "macros".to_string())]
168        );
169    }
170}