tinytemplate 1.0.2

Simple, lightweight template engine
Documentation
//! This module implements the bytecode interpreter that actually renders the templates.

use compiler::TemplateCompiler;
use error::Error::*;
use error::*;
use format;
use instruction::{Instruction, PathSlice};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Write;
use std::slice;
use ValueFormatter;

/// Enum defining the different kinds of records on the context stack.
enum ContextElement<'render, 'template> {
    /// Object contexts shadow everything below them on the stack, because every name is looked up
    /// in this object.
    Object(&'render Value),
    /// Named contexts shadow only one name. Any path that starts with that name is looked up in
    /// this object, and all others are passed on down the stack.
    Named(&'template str, &'render Value),
    /// Iteration contexts shadow one name with the current value of the iteration. They also
    /// store the iteration state. The two usizes are the index of the current value and the length
    /// of the array that we're iterating over.
    Iteration(
        &'template str,
        &'render Value,
        usize,
        usize,
        slice::Iter<'render, Value>,
    ),
}

/// Helper struct which mostly exists so that I have somewhere to put functions that access the
/// rendering context stack.
struct RenderContext<'render, 'template> {
    original_text: &'template str,
    context_stack: Vec<ContextElement<'render, 'template>>,
}
impl<'render, 'template> RenderContext<'render, 'template> {
    /// Look up the given path in the context stack and return the value (if found) or an error (if
    /// not)
    fn lookup(&self, path: PathSlice) -> Result<&'render Value> {
        for stack_layer in self.context_stack.iter().rev() {
            match stack_layer {
                ContextElement::Object(obj) => return self.lookup_in(path, obj),
                ContextElement::Named(name, obj) => {
                    if *name == path[0] {
                        return self.lookup_in(&path[1..], obj);
                    }
                }
                ContextElement::Iteration(name, obj, _, _, _) => {
                    if *name == path[0] {
                        return self.lookup_in(&path[1..], obj);
                    }
                }
            }
        }
        panic!("Attempted to do a lookup with an empty context stack. That shouldn't be possible.")
    }

    /// Look up a path within a given value object and return the resulting value (if found) or
    /// an error (if not)
    fn lookup_in(&self, path: PathSlice, object: &'render Value) -> Result<&'render Value> {
        let mut current = object;
        for step in path.iter() {
            match current.get(step) {
                Some(next) => current = next,
                None => return Err(lookup_error(self.original_text, step, path, current)),
            }
        }
        Ok(current)
    }

    /// Look up the index and length values for the top iteration context on the stack.
    fn lookup_index(&self) -> Result<(usize, usize)> {
        for stack_layer in self.context_stack.iter().rev() {
            match stack_layer {
                ContextElement::Iteration(_, _, index, length, _) => return Ok((*index, *length)),
                _ => continue,
            }
        }
        Err(GenericError {
            msg: "Used @index outside of a foreach block.".to_string(),
        })
    }
}

/// Structure representing a parsed template. It holds the bytecode program for rendering the
/// template as well as the length of the original template string, which is used as a guess to
/// pre-size the output string buffer.
pub(crate) struct Template<'template> {
    original_text: &'template str,
    instructions: Vec<Instruction<'template>>,
    template_len: usize,
}
impl<'template> Template<'template> {
    /// Create a Template from the given template string.
    pub fn compile(text: &'template str) -> Result<Template> {
        Ok(Template {
            original_text: text,
            template_len: text.len(),
            instructions: TemplateCompiler::new(text).compile()?,
        })
    }

    /// Render this template into a string and return it (or any error if one is encountered).
    pub fn render(
        &self,
        context: &Value,
        template_registry: &HashMap<&str, Template>,
        formatter_registry: &HashMap<&str, Box<ValueFormatter>>,
    ) -> Result<String> {
        // The length of the original template seems like a reasonable guess at the length of the
        // output.
        let mut output = String::with_capacity(self.template_len);
        self.render_into(context, template_registry, formatter_registry, &mut output)?;
        Ok(output)
    }

    /// Render this template into a given string. Used for calling other templates.
    pub fn render_into(
        &self,
        context: &Value,
        template_registry: &HashMap<&str, Template>,
        formatter_registry: &HashMap<&str, Box<ValueFormatter>>,
        output: &mut String,
    ) -> Result<()> {
        let mut program_counter = 0;
        let mut render_context = RenderContext {
            original_text: self.original_text,
            context_stack: vec![ContextElement::Object(context)],
        };

        while program_counter < self.instructions.len() {
            match &self.instructions[program_counter] {
                Instruction::Literal(text) => {
                    output.push_str(text);
                    program_counter += 1;
                }
                Instruction::Value(path) => {
                    let first = *path.first().unwrap();
                    if first.starts_with('@') {
                        // Currently we just hard-code the special @-keywords and have special
                        // lookup functions to use them because there are lifetime complexities with
                        // looking up values that don't live for as long as the given context object.
                        match first {
                            "@index" => {
                                write!(output, "{}", render_context.lookup_index()?.0).unwrap()
                            }
                            "@first" => {
                                write!(output, "{}", render_context.lookup_index()?.0 == 0).unwrap()
                            }
                            "@last" => {
                                let (index, length) = render_context.lookup_index()?;
                                write!(output, "{}", index == length).unwrap()
                            }
                            _ => panic!(), // This should have been caught by the parser.
                        }
                    } else {
                        let value_to_render = render_context.lookup(path)?;
                        format(value_to_render, output)?;
                    }
                    program_counter += 1;
                }
                Instruction::FormattedValue(path, name) => {
                    // The @ keywords aren't supported for formatted values. Should they be?
                    let value_to_render = render_context.lookup(path)?;
                    match formatter_registry.get(name) {
                        Some(formatter) => {
                            let formatter_result = formatter(value_to_render, output);
                            if let Err(err) = formatter_result {
                                return Err(called_formatter_error(self.original_text, name, err));
                            }
                        }
                        None => return Err(unknown_formatter(self.original_text, name)),
                    }
                    program_counter += 1;
                }
                Instruction::Branch(path, negate, target) => {
                    let first = *path.first().unwrap();
                    let mut truthy = if first.starts_with('@') {
                        match first {
                            "@index" => render_context.lookup_index()?.0 != 0,
                            "@first" => render_context.lookup_index()?.0 == 0,
                            "@last" => {
                                let (index, length) = render_context.lookup_index()?;
                                index == length
                            }
                            _ => panic!(), // This should have been caught by the parser.
                        }
                    } else {
                        let value_to_render = render_context.lookup(path)?;
                        match value_to_render {
                            Value::Null => false,
                            Value::Bool(b) => *b,
                            Value::Number(n) => match n.as_f64() {
                                Some(float) => float == 0.0,
                                None => {
                                    return Err(truthiness_error(self.original_text, path));
                                }
                            },
                            Value::String(s) => !s.is_empty(),
                            Value::Array(arr) => !arr.is_empty(),
                            Value::Object(_) => true,
                        }
                    };
                    if *negate {
                        truthy = !truthy;
                    }

                    if truthy {
                        program_counter = *target;
                    } else {
                        program_counter += 1;
                    }
                }
                Instruction::PushNamedContext(path, name) => {
                    let context_value = render_context.lookup(path)?;
                    render_context
                        .context_stack
                        .push(ContextElement::Named(name, context_value));
                    program_counter += 1;
                }
                Instruction::PushIterationContext(path, name) => {
                    // We push a context with an invalid index and no value and then wait for the
                    // following Iterate instruction to set the index and value properly.
                    let context_value = render_context.lookup(path)?;
                    match context_value {
                        Value::Array(ref arr) => {
                            render_context.context_stack.push(ContextElement::Iteration(
                                name,
                                &Value::Null,
                                ::std::usize::MAX,
                                arr.len(),
                                arr.iter(),
                            ))
                        }
                        _ => return Err(not_iterable_error(self.original_text, path)),
                    };
                    program_counter += 1;
                }
                Instruction::PopContext => {
                    render_context.context_stack.pop();
                    program_counter += 1;
                }
                Instruction::Goto(target) => {
                    program_counter = *target;
                }
                Instruction::Iterate(target) => {
                    match render_context.context_stack.last_mut() {
                        Some(ContextElement::Iteration(_, val, index, _, iter)) => {
                            match iter.next() {
                                Some(new_val) => {
                                    *val = new_val;
                                    // On the first iteration, this will be usize::MAX so it will
                                    // wrap around to zero.
                                    *index = index.wrapping_add(1);
                                    program_counter += 1;
                                }
                                None => {
                                    program_counter = *target;
                                }
                            }
                        }
                        _ => panic!("Malformed program."),
                    };
                }
                Instruction::Call(template_name, path) => {
                    let context_value = render_context.lookup(path)?;
                    match template_registry.get(template_name) {
                        Some(templ) => {
                            let called_templ_result = templ.render_into(
                                context_value,
                                template_registry,
                                formatter_registry,
                                output,
                            );
                            if let Err(err) = called_templ_result {
                                return Err(called_template_error(
                                    self.original_text,
                                    template_name,
                                    err,
                                ));
                            }
                        }
                        None => return Err(unknown_template(self.original_text, template_name)),
                    }
                    program_counter += 1;
                }
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use compiler::TemplateCompiler;

    fn compile(text: &'static str) -> Template<'static> {
        Template {
            original_text: text,
            template_len: text.len(),
            instructions: TemplateCompiler::new(text).compile().unwrap(),
        }
    }

    #[derive(Serialize)]
    struct NestedContext {
        value: usize,
    }

    #[derive(Serialize)]
    struct TestContext {
        number: usize,
        string: &'static str,
        boolean: bool,
        null: Option<usize>,
        array: Vec<usize>,
        nested: NestedContext,
        escapes: &'static str,
    }

    fn context() -> Value {
        let ctx = TestContext {
            number: 5,
            string: "test",
            boolean: true,
            null: None,
            array: vec![1, 2, 3],
            nested: NestedContext { value: 10 },
            escapes: "1:< 2:> 3:& 4:' 5:\"",
        };
        ::serde_json::to_value(&ctx).unwrap()
    }

    fn other_templates() -> HashMap<&'static str, Template<'static>> {
        let mut map = HashMap::new();
        map.insert("my_macro", compile("{value}"));
        map
    }

    fn format(value: &Value, output: &mut String) -> Result<()> {
        output.push_str("{");
        ::format(value, output)?;
        output.push_str("}");
        Ok(())
    }

    fn formatters() -> HashMap<&'static str, Box<ValueFormatter>> {
        let mut map = HashMap::<&'static str, Box<ValueFormatter>>::new();
        map.insert("my_formatter", Box::new(format));
        map
    }

    #[test]
    fn test_literal() {
        let template = compile("Hello!");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Hello!", &string);
    }

    #[test]
    fn test_value() {
        let template = compile("{ number }");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("5", &string);
    }

    #[test]
    fn test_path() {
        let template = compile("The number of the day is { nested.value }.");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("The number of the day is 10.", &string);
    }

    #[test]
    fn test_if_taken() {
        let template = compile("{{ if boolean }}Hello!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Hello!", &string);
    }

    #[test]
    fn test_if_untaken() {
        let template = compile("{{ if null }}Hello!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("", &string);
    }

    #[test]
    fn test_if_else_taken() {
        let template = compile("{{ if boolean }}Hello!{{ else }}Goodbye!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Hello!", &string);
    }

    #[test]
    fn test_if_else_untaken() {
        let template = compile("{{ if null }}Hello!{{ else }}Goodbye!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Goodbye!", &string);
    }

    #[test]
    fn test_ifnot_taken() {
        let template = compile("{{ if not boolean }}Hello!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("", &string);
    }

    #[test]
    fn test_ifnot_untaken() {
        let template = compile("{{ if not null }}Hello!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Hello!", &string);
    }

    #[test]
    fn test_ifnot_else_taken() {
        let template = compile("{{ if not boolean }}Hello!{{ else }}Goodbye!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Goodbye!", &string);
    }

    #[test]
    fn test_ifnot_else_untaken() {
        let template = compile("{{ if not null }}Hello!{{ else }}Goodbye!{{ endif }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Hello!", &string);
    }

    #[test]
    fn test_nested_ifs() {
        let template = compile(
            "{{ if boolean }}Hi, {{ if null }}there!{{ else }}Hello!{{ endif }}{{ endif }}",
        );
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("Hi, Hello!", &string);
    }

    #[test]
    fn test_with() {
        let template = compile("{{ with nested as n }}{ n.value } { number }{{endwith}}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("10 5", &string);
    }

    #[test]
    fn test_for_loop() {
        let template = compile("{{ for a in array }}{ a }{{ endfor }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("123", &string);
    }

    #[test]
    fn test_for_loop_index() {
        let template = compile("{{ for a in array }}{ @index }{{ endfor }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("012", &string);
    }

    #[test]
    fn test_for_loop_first() {
        let template =
            compile("{{ for a in array }}{{if @first }}{ @index }{{ endif }}{{ endfor }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("0", &string);
    }

    #[test]
    fn test_whitespace_stripping_value() {
        let template = compile("1  \n\t   {- number -}  \n   1");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("151", &string);
    }

    #[test]
    fn test_call() {
        let template = compile("{{ call my_macro with nested }}");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("10", &string);
    }

    #[test]
    fn test_formatter() {
        let template = compile("{ nested.value | my_formatter }");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("{10}", &string);
    }

    #[test]
    fn test_unknown() {
        let template = compile("{ foobar }");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap_err();
    }

    #[test]
    fn test_escaping() {
        let template = compile("{ escapes }");
        let context = context();
        let template_registry = other_templates();
        let formatter_registry = formatters();
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("1:&lt; 2:&gt; 3:&amp; 4:&#39; 5:&quot;", &string);
    }

    #[test]
    fn test_unescaped() {
        let template = compile("{ escapes | unescaped }");
        let context = context();
        let template_registry = other_templates();
        let mut formatter_registry = formatters();
        formatter_registry.insert("unescaped", Box::new(::format_unescaped));
        let string = template
            .render(&context, &template_registry, &formatter_registry)
            .unwrap();
        assert_eq!("1:< 2:> 3:& 4:' 5:\"", &string);
    }
}