#![allow(deprecated)] | |
/// The compiler module houses the code which parses and compiles templates. TinyTemplate implements | |
/// a simple bytecode interpreter (see the [instruction] module for more details) to render templates. | |
/// The [`TemplateCompiler`](struct.TemplateCompiler.html) struct is responsible for parsing the | |
/// template strings and generating the appropriate bytecode instructions. | |
use error::Error::*; | |
use error::{get_offset, Error, Result}; | |
use instruction::{Instruction, Path, PathStep}; | |
/// The end point of a branch or goto instruction is not known. | |
const UNKNOWN: usize = ::std::usize::MAX; | |
/// The compiler keeps a stack of the open blocks so that it can ensure that blocks are closed in | |
/// the right order. The Block type is a simple enumeration of the kinds of blocks that could be | |
/// open. It may contain the instruction index corresponding to the start of the block. | |
enum Block { | |
Branch(usize), | |
For(usize), | |
With, | |
} | |
/// List of the known @-keywords so that we can error if the user spells them wrong. | |
static KNOWN_KEYWORDS: [&str; 4] = ["@index", "@first", "@last", "@root"]; | |
/// The TemplateCompiler struct is responsible for parsing a template string and generating bytecode | |
/// instructions based on it. The parser is a simple hand-written pattern-matching parser with no | |
/// recursion, which makes it relatively easy to read. | |
pub(crate) struct TemplateCompiler<'template> { | |
original_text: &'template str, | |
remaining_text: &'template str, | |
instructions: Vec<Instruction<'template>>, | |
block_stack: Vec<(&'template str, Block)>, | |
/// When we see a `{foo -}` or similar, we need to remember to left-trim the next text block we | |
/// encounter. | |
trim_next: bool, | |
} | |
impl<'template> TemplateCompiler<'template> { | |
/// Create a new template compiler to parse and compile the given template. | |
pub fn new(text: &'template str) -> TemplateCompiler<'template> { | |
TemplateCompiler { | |
original_text: text, | |
remaining_text: text, | |
instructions: vec![], | |
block_stack: vec![], | |
trim_next: false, | |
} | |
} | |
/// Consume the template compiler to parse the template and return the generated bytecode. | |
pub fn compile(mut self) -> Result<Vec<Instruction<'template>>> { | |
while !self.remaining_text.is_empty() { | |
// Comment, denoted by {# comment text #} | |
if self.remaining_text.starts_with("{#") { | |
self.trim_next = false; | |
let tag = self.consume_tag("#}")?; | |
let comment = tag[2..(tag.len() - 2)].trim(); | |
if comment.starts_with('-') { | |
self.trim_last_whitespace(); | |
} | |
if comment.ends_with('-') { | |
self.trim_next_whitespace(); | |
} | |
// Block tag. Block tags are wrapped in {{ }} and always have one word at the start | |
// to identify which kind of tag it is. Depending on the tag type there may be more. | |
} else if self.remaining_text.starts_with("{{") { | |
self.trim_next = false; | |
let (discriminant, rest) = self.consume_block()?; | |
match discriminant { | |
"if" => { | |
let (path, negated) = if rest.starts_with("not") { | |
(self.parse_path(&rest[4..])?, true) | |
} else { | |
(self.parse_path(rest)?, false) | |
}; | |
self.block_stack | |
.push((discriminant, Block::Branch(self.instructions.len()))); | |
self.instructions | |
.push(Instruction::Branch(path, !negated, UNKNOWN)); | |
} | |
"else" => { | |
self.expect_empty(rest)?; | |
let num_instructions = self.instructions.len() + 1; | |
self.close_branch(num_instructions, discriminant)?; | |
self.block_stack | |
.push((discriminant, Block::Branch(self.instructions.len()))); | |
self.instructions.push(Instruction::Goto(UNKNOWN)) | |
} | |
"endif" => { | |
self.expect_empty(rest)?; | |
let num_instructions = self.instructions.len(); | |
self.close_branch(num_instructions, discriminant)?; | |
} | |
"with" => { | |
let (path, name) = self.parse_with(rest)?; | |
let instruction = Instruction::PushNamedContext(path, name); | |
self.instructions.push(instruction); | |
self.block_stack.push((discriminant, Block::With)); | |
} | |
"endwith" => { | |
self.expect_empty(rest)?; | |
if let Some((_, Block::With)) = self.block_stack.pop() { | |
self.instructions.push(Instruction::PopContext) | |
} else { | |
return Err(self.parse_error( | |
discriminant, | |
"Found a closing endwith that doesn't match with a preceeding with.".to_string() | |
)); | |
} | |
} | |
"for" => { | |
let (path, name) = self.parse_for(rest)?; | |
self.instructions | |
.push(Instruction::PushIterationContext(path, name)); | |
self.block_stack | |
.push((discriminant, Block::For(self.instructions.len()))); | |
self.instructions.push(Instruction::Iterate(UNKNOWN)); | |
} | |
"endfor" => { | |
self.expect_empty(rest)?; | |
let num_instructions = self.instructions.len() + 1; | |
let goto_target = self.close_for(num_instructions, discriminant)?; | |
self.instructions.push(Instruction::Goto(goto_target)); | |
self.instructions.push(Instruction::PopContext); | |
} | |
"call" => { | |
let (name, path) = self.parse_call(rest)?; | |
self.instructions.push(Instruction::Call(name, path)); | |
} | |
_ => { | |
return Err(self.parse_error( | |
discriminant, | |
format!("Unknown block type '{}'", discriminant), | |
)); | |
} | |
} | |
// Values, of the form { dotted.path.to.value.in.context } | |
// Note that it is not (currently) possible to escape curly braces in the templates to | |
// prevent them from being interpreted as values. | |
} else if self.remaining_text.starts_with('{') { | |
self.trim_next = false; | |
let (path, name) = self.consume_value()?; | |
let instruction = match name { | |
Some(name) => Instruction::FormattedValue(path, name), | |
None => Instruction::Value(path), | |
}; | |
self.instructions.push(instruction); | |
// All other text - just consume characters until we see a { | |
} else { | |
let mut escaped = false; | |
loop { | |
let mut text = self.consume_text(escaped); | |
if self.trim_next { | |
text = text.trim_left(); | |
self.trim_next = false; | |
} | |
escaped = text.ends_with('\\'); | |
if escaped { | |
text = &text[0..(text.len() - 1)]; | |
} | |
self.instructions.push(Instruction::Literal(text)); | |
if !escaped { | |
break; | |
} | |
} | |
} | |
} | |
if let Some((text, _)) = self.block_stack.pop() { | |
return Err(self.parse_error( | |
text, | |
"Expected block-closing tag, but reached the end of input.".to_string(), | |
)); | |
} | |
Ok(self.instructions) | |
} | |
/// Splits a string into a list of named segments which can later be used to look up values in the | |
/// context. | |
fn parse_path(&self, text: &'template str) -> Result<Path<'template>> { | |
if !text.starts_with('@') { | |
Ok(text | |
.split('.') | |
.map(|s| match s.parse::<usize>() { | |
Ok(n) => PathStep::Index(s, n), | |
Err(_) => PathStep::Name(s), | |
}) | |
.collect::<Vec<_>>()) | |
} else if KNOWN_KEYWORDS.iter().any(|k| *k == text) { | |
Ok(vec![PathStep::Name(text)]) | |
} else { | |
Err(self.parse_error(text, format!("Invalid keyword name '{}'", text))) | |
} | |
} | |
/// Finds the line number and column where an error occurred. Location is the substring of | |
/// self.original_text where the error was found, and msg is the error message. | |
fn parse_error(&self, location: &str, msg: String) -> Error { | |
let (line, column) = get_offset(self.original_text, location); | |
ParseError { msg, line, column } | |
} | |
/// Tags which should have no text after the discriminant use this to raise an error if | |
/// text is found. | |
fn expect_empty(&self, text: &str) -> Result<()> { | |
if text.is_empty() { | |
Ok(()) | |
} else { | |
Err(self.parse_error(text, format!("Unexpected text '{}'", text))) | |
} | |
} | |
/// Close the branch that is on top of the block stack by setting its target instruction | |
/// and popping it from the stack. Returns an error if the top of the block stack is not a | |
/// branch. | |
fn close_branch(&mut self, new_target: usize, discriminant: &str) -> Result<()> { | |
let branch_block = self.block_stack.pop(); | |
if let Some((_, Block::Branch(index))) = branch_block { | |
match &mut self.instructions[index] { | |
Instruction::Branch(_, _, target) => { | |
*target = new_target; | |
Ok(()) | |
} | |
Instruction::Goto(target) => { | |
*target = new_target; | |
Ok(()) | |
} | |
_ => panic!(), | |
} | |
} else { | |
Err(self.parse_error( | |
discriminant, | |
"Found a closing endif or else which doesn't match with a preceding if." | |
.to_string(), | |
)) | |
} | |
} | |
/// Close the for loop that is on top of the block stack by setting its target instruction and | |
/// popping it from the stack. Returns an error if the top of the stack is not a for loop. | |
/// Returns the index of the loop's Iterate instruction for further processing. | |
fn close_for(&mut self, new_target: usize, discriminant: &str) -> Result<usize> { | |
let branch_block = self.block_stack.pop(); | |
if let Some((_, Block::For(index))) = branch_block { | |
match &mut self.instructions[index] { | |
Instruction::Iterate(target) => { | |
*target = new_target; | |
Ok(index) | |
} | |
_ => panic!(), | |
} | |
} else { | |
Err(self.parse_error( | |
discriminant, | |
"Found a closing endfor which doesn't match with a preceding for.".to_string(), | |
)) | |
} | |
} | |
/// Advance the cursor to the next { and return the consumed text. If `escaped` is true, skips | |
/// a { at the start of the text. | |
fn consume_text(&mut self, escaped: bool) -> &'template str { | |
let search_substr = if escaped { | |
&self.remaining_text[1..] | |
} else { | |
self.remaining_text | |
}; | |
let mut position = search_substr | |
.find('{') | |
.unwrap_or_else(|| search_substr.len()); | |
if escaped { | |
position += 1; | |
} | |
let (text, remaining) = self.remaining_text.split_at(position); | |
self.remaining_text = remaining; | |
text | |
} | |
/// Advance the cursor to the end of the value tag and return the value's path and optional | |
/// formatter name. | |
fn consume_value(&mut self) -> Result<(Path<'template>, Option<&'template str>)> { | |
let tag = self.consume_tag("}")?; | |
let mut tag = tag[1..(tag.len() - 1)].trim(); | |
if tag.starts_with('-') { | |
tag = tag[1..].trim(); | |
self.trim_last_whitespace(); | |
} | |
if tag.ends_with('-') { | |
tag = tag[0..tag.len() - 1].trim(); | |
self.trim_next_whitespace(); | |
} | |
if let Some(index) = tag.find('|') { | |
let (path_str, name_str) = tag.split_at(index); | |
let name = name_str[1..].trim(); | |
let path = self.parse_path(path_str.trim())?; | |
Ok((path, Some(name))) | |
} else { | |
Ok((self.parse_path(tag)?, None)) | |
} | |
} | |
/// Right-trim whitespace from the last text block we parsed. | |
fn trim_last_whitespace(&mut self) { | |
if let Some(Instruction::Literal(text)) = self.instructions.last_mut() { | |
*text = text.trim_right(); | |
} | |
} | |
/// Make a note to left-trim whitespace from the next text block we parse. | |
fn trim_next_whitespace(&mut self) { | |
self.trim_next = true; | |
} | |
/// Advance the cursor to the end of the current block tag and return the discriminant substring | |
/// and the rest of the text in the tag. Also handles trimming whitespace where needed. | |
fn consume_block(&mut self) -> Result<(&'template str, &'template str)> { | |
let tag = self.consume_tag("}}")?; | |
let mut block = tag[2..(tag.len() - 2)].trim(); | |
if block.starts_with('-') { | |
block = block[1..].trim(); | |
self.trim_last_whitespace(); | |
} | |
if block.ends_with('-') { | |
block = block[0..block.len() - 1].trim(); | |
self.trim_next_whitespace(); | |
} | |
let discriminant = block.split_whitespace().next().unwrap_or(block); | |
let rest = block[discriminant.len()..].trim(); | |
Ok((discriminant, rest)) | |
} | |
/// Advance the cursor to after the given expected_close string and return the text in between | |
/// (including the expected_close characters), or return an error message if we reach the end | |
/// of a line of text without finding it. | |
fn consume_tag(&mut self, expected_close: &str) -> Result<&'template str> { | |
if let Some(line) = self.remaining_text.lines().next() { | |
if let Some(pos) = line.find(expected_close) { | |
let (tag, remaining) = self.remaining_text.split_at(pos + expected_close.len()); | |
self.remaining_text = remaining; | |
Ok(tag) | |
} else { | |
Err(self.parse_error( | |
line, | |
format!( | |
"Expected a closing '{}' but found end-of-line instead.", | |
expected_close | |
), | |
)) | |
} | |
} else { | |
Err(self.parse_error( | |
self.remaining_text, | |
format!( | |
"Expected a closing '{}' but found end-of-text instead.", | |
expected_close | |
), | |
)) | |
} | |
} | |
/// Parse a with tag to separate the value path from the (optional) name. | |
fn parse_with(&self, with_text: &'template str) -> Result<(Path<'template>, &'template str)> { | |
if let Some(index) = with_text.find(" as ") { | |
let (path_str, name_str) = with_text.split_at(index); | |
let path = self.parse_path(path_str.trim())?; | |
let name = name_str[" as ".len()..].trim(); | |
Ok((path, name)) | |
} else { | |
Err(self.parse_error( | |
with_text, | |
format!( | |
"Expected 'as <path>' in with block, but found \"{}\" instead", | |
with_text | |
), | |
)) | |
} | |
} | |
/// Parse a for tag to separate the value path from the name. | |
fn parse_for(&self, for_text: &'template str) -> Result<(Path<'template>, &'template str)> { | |
if let Some(index) = for_text.find(" in ") { | |
let (name_str, path_str) = for_text.split_at(index); | |
let name = name_str.trim(); | |
let path = self.parse_path(path_str[" in ".len()..].trim())?; | |
Ok((path, name)) | |
} else { | |
Err(self.parse_error( | |
for_text, | |
format!("Unable to parse for block text '{}'", for_text), | |
)) | |
} | |
} | |
/// Parse a call tag to separate the template name and context value. | |
fn parse_call(&self, call_text: &'template str) -> Result<(&'template str, Path<'template>)> { | |
if let Some(index) = call_text.find(" with ") { | |
let (name_str, path_str) = call_text.split_at(index); | |
let name = name_str.trim(); | |
let path = self.parse_path(path_str[" with ".len()..].trim())?; | |
Ok((name, path)) | |
} else { | |
Err(self.parse_error( | |
call_text, | |
format!("Unable to parse call block text '{}'", call_text), | |
)) | |
} | |
} | |
} | |
#[cfg(test)] | |
mod test { | |
use super::*; | |
use instruction::Instruction::*; | |
fn compile(text: &'static str) -> Result<Vec<Instruction<'static>>> { | |
TemplateCompiler::new(text).compile() | |
} | |
#[test] | |
fn test_compile_literal() { | |
let text = "Test String"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(1, instructions.len()); | |
assert_eq!(&Literal(text), &instructions[0]); | |
} | |
#[test] | |
fn test_compile_value() { | |
let text = "{ foobar }"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(1, instructions.len()); | |
assert_eq!(&Value(vec![PathStep::Name("foobar")]), &instructions[0]); | |
} | |
#[test] | |
fn test_compile_value_with_formatter() { | |
let text = "{ foobar | my_formatter }"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(1, instructions.len()); | |
assert_eq!( | |
&FormattedValue(vec![PathStep::Name("foobar")], "my_formatter"), | |
&instructions[0] | |
); | |
} | |
#[test] | |
fn test_dotted_path() { | |
let text = "{ foo.bar }"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(1, instructions.len()); | |
assert_eq!( | |
&Value(vec![PathStep::Name("foo"), PathStep::Name("bar")]), | |
&instructions[0] | |
); | |
} | |
#[test] | |
fn test_indexed_path() { | |
let text = "{ foo.0.bar }"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(1, instructions.len()); | |
assert_eq!( | |
&Value(vec![ | |
PathStep::Name("foo"), | |
PathStep::Index("0", 0), | |
PathStep::Name("bar") | |
]), | |
&instructions[0] | |
); | |
} | |
#[test] | |
fn test_mixture() { | |
let text = "Hello { name }, how are you?"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(3, instructions.len()); | |
assert_eq!(&Literal("Hello "), &instructions[0]); | |
assert_eq!(&Value(vec![PathStep::Name("name")]), &instructions[1]); | |
assert_eq!(&Literal(", how are you?"), &instructions[2]); | |
} | |
#[test] | |
fn test_if_endif() { | |
let text = "{{ if foo }}Hello!{{ endif }}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(2, instructions.len()); | |
assert_eq!( | |
&Branch(vec![PathStep::Name("foo")], true, 2), | |
&instructions[0] | |
); | |
assert_eq!(&Literal("Hello!"), &instructions[1]); | |
} | |
#[test] | |
fn test_if_not_endif() { | |
let text = "{{ if not foo }}Hello!{{ endif }}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(2, instructions.len()); | |
assert_eq!( | |
&Branch(vec![PathStep::Name("foo")], false, 2), | |
&instructions[0] | |
); | |
assert_eq!(&Literal("Hello!"), &instructions[1]); | |
} | |
#[test] | |
fn test_if_else_endif() { | |
let text = "{{ if foo }}Hello!{{ else }}Goodbye!{{ endif }}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(4, instructions.len()); | |
assert_eq!( | |
&Branch(vec![PathStep::Name("foo")], true, 3), | |
&instructions[0] | |
); | |
assert_eq!(&Literal("Hello!"), &instructions[1]); | |
assert_eq!(&Goto(4), &instructions[2]); | |
assert_eq!(&Literal("Goodbye!"), &instructions[3]); | |
} | |
#[test] | |
fn test_with() { | |
let text = "{{ with foo as bar }}Hello!{{ endwith }}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(3, instructions.len()); | |
assert_eq!( | |
&PushNamedContext(vec![PathStep::Name("foo")], "bar"), | |
&instructions[0] | |
); | |
assert_eq!(&Literal("Hello!"), &instructions[1]); | |
assert_eq!(&PopContext, &instructions[2]); | |
} | |
#[test] | |
fn test_foreach() { | |
let text = "{{ for foo in bar.baz }}{ foo }{{ endfor }}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(5, instructions.len()); | |
assert_eq!( | |
&PushIterationContext(vec![PathStep::Name("bar"), PathStep::Name("baz")], "foo"), | |
&instructions[0] | |
); | |
assert_eq!(&Iterate(4), &instructions[1]); | |
assert_eq!(&Value(vec![PathStep::Name("foo")]), &instructions[2]); | |
assert_eq!(&Goto(1), &instructions[3]); | |
assert_eq!(&PopContext, &instructions[4]); | |
} | |
#[test] | |
fn test_strip_whitespace_value() { | |
let text = "Hello, {- name -} , how are you?"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(3, instructions.len()); | |
assert_eq!(&Literal("Hello,"), &instructions[0]); | |
assert_eq!(&Value(vec![PathStep::Name("name")]), &instructions[1]); | |
assert_eq!(&Literal(", how are you?"), &instructions[2]); | |
} | |
#[test] | |
fn test_strip_whitespace_block() { | |
let text = "Hello, {{- if name -}} {name} {{- endif -}} , how are you?"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(6, instructions.len()); | |
assert_eq!(&Literal("Hello,"), &instructions[0]); | |
assert_eq!( | |
&Branch(vec![PathStep::Name("name")], true, 5), | |
&instructions[1] | |
); | |
assert_eq!(&Literal(""), &instructions[2]); | |
assert_eq!(&Value(vec![PathStep::Name("name")]), &instructions[3]); | |
assert_eq!(&Literal(""), &instructions[4]); | |
assert_eq!(&Literal(", how are you?"), &instructions[5]); | |
} | |
#[test] | |
fn test_comment() { | |
let text = "Hello, {# foo bar baz #} there!"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(2, instructions.len()); | |
assert_eq!(&Literal("Hello, "), &instructions[0]); | |
assert_eq!(&Literal(" there!"), &instructions[1]); | |
} | |
#[test] | |
fn test_strip_whitespace_comment() { | |
let text = "Hello, \t\n {#- foo bar baz -#} \t there!"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(2, instructions.len()); | |
assert_eq!(&Literal("Hello,"), &instructions[0]); | |
assert_eq!(&Literal("there!"), &instructions[1]); | |
} | |
#[test] | |
fn test_strip_whitespace_followed_by_another_tag() { | |
let text = "{value -}{value} Hello"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(3, instructions.len()); | |
assert_eq!(&Value(vec![PathStep::Name("value")]), &instructions[0]); | |
assert_eq!(&Value(vec![PathStep::Name("value")]), &instructions[1]); | |
assert_eq!(&Literal(" Hello"), &instructions[2]); | |
} | |
#[test] | |
fn test_call() { | |
let text = "{{ call my_macro with foo.bar }}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(1, instructions.len()); | |
assert_eq!( | |
&Call( | |
"my_macro", | |
vec![PathStep::Name("foo"), PathStep::Name("bar")] | |
), | |
&instructions[0] | |
); | |
} | |
#[test] | |
fn test_curly_brace_escaping() { | |
let text = "body \\{ \nfont-size: {fontsize} \n}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(4, instructions.len()); | |
assert_eq!(&Literal("body "), &instructions[0]); | |
assert_eq!(&Literal("{ \nfont-size: "), &instructions[1]); | |
assert_eq!(&Value(vec![PathStep::Name("fontsize")]), &instructions[2]); | |
assert_eq!(&Literal(" \n}"), &instructions[3]); | |
} | |
#[test] | |
fn test_unclosed_tags() { | |
let tags = vec![ | |
"{", | |
"{ foo.bar", | |
"{ foo.bar\n }", | |
"{{", | |
"{{ if foo.bar", | |
"{{ if foo.bar \n}}", | |
"{#", | |
"{# if foo.bar", | |
"{# if foo.bar \n#}", | |
]; | |
for tag in tags { | |
compile(tag).unwrap_err(); | |
} | |
} | |
#[test] | |
fn test_mismatched_blocks() { | |
let text = "{{ if foo }}{{ with bar }}{{ endif }} {{ endwith }}"; | |
compile(text).unwrap_err(); | |
} | |
#[test] | |
fn test_disallows_invalid_keywords() { | |
let text = "{ @foo }"; | |
compile(text).unwrap_err(); | |
} | |
#[test] | |
fn test_diallows_unknown_block_type() { | |
let text = "{{ foobar }}"; | |
compile(text).unwrap_err(); | |
} | |
#[test] | |
fn test_parse_error_line_column_num() { | |
let text = "\n\n\n{{ foobar }}"; | |
let err = compile(text).unwrap_err(); | |
if let ParseError { line, column, .. } = err { | |
assert_eq!(4, line); | |
assert_eq!(3, column); | |
} else { | |
panic!("Should have returned a parse error"); | |
} | |
} | |
#[test] | |
fn test_parse_error_on_unclosed_if() { | |
let text = "{{ if foo }}"; | |
compile(text).unwrap_err(); | |
} | |
#[test] | |
fn test_parse_escaped_open_curly_brace() { | |
let text: &str = r"hello \{world}"; | |
let instructions = compile(text).unwrap(); | |
assert_eq!(2, instructions.len()); | |
assert_eq!(&Literal("hello "), &instructions[0]); | |
assert_eq!(&Literal("{world}"), &instructions[1]); | |
} | |
} |