diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-18 02:49:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-18 02:49:50 +0000 |
commit | 9835e2ae736235810b4ea1c162ca5e65c547e770 (patch) | |
tree | 3fcebf40ed70e581d776a8a4c65923e8ec20e026 /vendor/tinytemplate/src | |
parent | Releasing progress-linux version 1.70.0+dfsg2-1~progress7.99u1. (diff) | |
download | rustc-9835e2ae736235810b4ea1c162ca5e65c547e770.tar.xz rustc-9835e2ae736235810b4ea1c162ca5e65c547e770.zip |
Merging upstream version 1.71.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/tinytemplate/src')
-rwxr-xr-x | vendor/tinytemplate/src/compiler.rs | 698 | ||||
-rwxr-xr-x | vendor/tinytemplate/src/error.rs | 246 | ||||
-rwxr-xr-x | vendor/tinytemplate/src/instruction.rs | 85 | ||||
-rwxr-xr-x | vendor/tinytemplate/src/lib.rs | 260 | ||||
-rwxr-xr-x | vendor/tinytemplate/src/syntax.rs | 184 | ||||
-rwxr-xr-x | vendor/tinytemplate/src/template.rs | 944 |
6 files changed, 2417 insertions, 0 deletions
diff --git a/vendor/tinytemplate/src/compiler.rs b/vendor/tinytemplate/src/compiler.rs new file mode 100755 index 000000000..df37947df --- /dev/null +++ b/vendor/tinytemplate/src/compiler.rs @@ -0,0 +1,698 @@ +#![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]);
+ }
+}
diff --git a/vendor/tinytemplate/src/error.rs b/vendor/tinytemplate/src/error.rs new file mode 100755 index 000000000..730c64891 --- /dev/null +++ b/vendor/tinytemplate/src/error.rs @@ -0,0 +1,246 @@ +//! Module containing the error type returned by TinyTemplate if an error occurs.
+
+use instruction::{path_to_str, PathSlice};
+use serde_json::Error as SerdeJsonError;
+use serde_json::Value;
+use std::error::Error as StdError;
+use std::fmt;
+
+/// Enum representing the potential errors that TinyTemplate can encounter.
+#[derive(Debug)]
+pub enum Error {
+ ParseError {
+ msg: String,
+ line: usize,
+ column: usize,
+ },
+ RenderError {
+ msg: String,
+ line: usize,
+ column: usize,
+ },
+ SerdeError {
+ err: SerdeJsonError,
+ },
+ GenericError {
+ msg: String,
+ },
+ StdFormatError {
+ err: fmt::Error,
+ },
+ CalledTemplateError {
+ name: String,
+ err: Box<Error>,
+ line: usize,
+ column: usize,
+ },
+ CalledFormatterError {
+ name: String,
+ err: Box<Error>,
+ line: usize,
+ column: usize,
+ },
+
+ #[doc(hidden)]
+ __NonExhaustive,
+}
+impl From<SerdeJsonError> for Error {
+ fn from(err: SerdeJsonError) -> Error {
+ Error::SerdeError { err }
+ }
+}
+impl From<fmt::Error> for Error {
+ fn from(err: fmt::Error) -> Error {
+ Error::StdFormatError { err }
+ }
+}
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::ParseError { msg, line, column } => write!(
+ f,
+ "Failed to parse the template (line {}, column {}). Reason: {}",
+ line, column, msg
+ ),
+ Error::RenderError { msg, line, column } => {
+ write!(
+ f,
+ "Encountered rendering error on line {}, column {}. Reason: {}",
+ line, column, msg
+ )
+ }
+ Error::SerdeError { err } => {
+ write!(f, "Unexpected serde error while converting the context to a serde_json::Value. Error: {}", err)
+ }
+ Error::GenericError { msg } => {
+ write!(f, "{}", msg)
+ }
+ Error::StdFormatError { err } => {
+ write!(f, "Unexpected formatting error: {}", err)
+ }
+ Error::CalledTemplateError {
+ name,
+ err,
+ line,
+ column,
+ } => {
+ write!(
+ f,
+ "Call to sub-template \"{}\" on line {}, column {} failed. Reason: {}",
+ name, line, column, err
+ )
+ }
+ Error::CalledFormatterError {
+ name,
+ err,
+ line,
+ column,
+ } => {
+ write!(
+ f,
+ "Call to value formatter \"{}\" on line {}, column {} failed. Reason: {}",
+ name, line, column, err
+ )
+ }
+ Error::__NonExhaustive => unreachable!(),
+ }
+ }
+}
+impl StdError for Error {
+ fn description(&self) -> &str {
+ match self {
+ Error::ParseError { .. } => "ParseError",
+ Error::RenderError { .. } => "RenderError",
+ Error::SerdeError { .. } => "SerdeError",
+ Error::GenericError { msg } => &msg,
+ Error::StdFormatError { .. } => "StdFormatError",
+ Error::CalledTemplateError { .. } => "CalledTemplateError",
+ Error::CalledFormatterError { .. } => "CalledFormatterError",
+ Error::__NonExhaustive => unreachable!(),
+ }
+ }
+}
+
+pub type Result<T> = ::std::result::Result<T, Error>;
+
+pub(crate) fn lookup_error(source: &str, step: &str, path: PathSlice, current: &Value) -> Error {
+ let avail_str = if let Value::Object(object_map) = current {
+ let mut avail_str = " Available values at this level are ".to_string();
+ for (i, key) in object_map.keys().enumerate() {
+ if i > 0 {
+ avail_str.push_str(", ");
+ }
+ avail_str.push('\'');
+ avail_str.push_str(key);
+ avail_str.push('\'');
+ }
+ avail_str
+ } else {
+ "".to_string()
+ };
+
+ let (line, column) = get_offset(source, step);
+
+ Error::RenderError {
+ msg: format!(
+ "Failed to find value '{}' from path '{}'.{}",
+ step,
+ path_to_str(path),
+ avail_str
+ ),
+ line,
+ column,
+ }
+}
+
+pub(crate) fn truthiness_error(source: &str, path: PathSlice) -> Error {
+ let (line, column) = get_offset(source, path.last().unwrap());
+ Error::RenderError {
+ msg: format!(
+ "Path '{}' produced a value which could not be checked for truthiness.",
+ path_to_str(path)
+ ),
+ line,
+ column,
+ }
+}
+
+pub(crate) fn unprintable_error() -> Error {
+ Error::GenericError {
+ msg: "Expected a printable value but found array or object.".to_string(),
+ }
+}
+
+pub(crate) fn not_iterable_error(source: &str, path: PathSlice) -> Error {
+ let (line, column) = get_offset(source, path.last().unwrap());
+ Error::RenderError {
+ msg: format!(
+ "Expected an array for path '{}' but found a non-iterable value.",
+ path_to_str(path)
+ ),
+ line,
+ column,
+ }
+}
+
+pub(crate) fn unknown_template(source: &str, name: &str) -> Error {
+ let (line, column) = get_offset(source, name);
+ Error::RenderError {
+ msg: format!("Tried to call an unknown template '{}'", name),
+ line,
+ column,
+ }
+}
+
+pub(crate) fn unknown_formatter(source: &str, name: &str) -> Error {
+ let (line, column) = get_offset(source, name);
+ Error::RenderError {
+ msg: format!("Tried to call an unknown formatter '{}'", name),
+ line,
+ column,
+ }
+}
+
+pub(crate) fn called_template_error(source: &str, template_name: &str, err: Error) -> Error {
+ let (line, column) = get_offset(source, template_name);
+ Error::CalledTemplateError {
+ name: template_name.to_string(),
+ err: Box::new(err),
+ line,
+ column,
+ }
+}
+
+pub(crate) fn called_formatter_error(source: &str, formatter_name: &str, err: Error) -> Error {
+ let (line, column) = get_offset(source, formatter_name);
+ Error::CalledFormatterError {
+ name: formatter_name.to_string(),
+ err: Box::new(err),
+ line,
+ column,
+ }
+}
+
+/// Find the line number and column of the target string within the source string. Will panic if
+/// target is not a substring of source.
+pub(crate) fn get_offset(source: &str, target: &str) -> (usize, usize) {
+ let offset = target.as_ptr() as isize - source.as_ptr() as isize;
+ let to_scan = &source[0..(offset as usize)];
+
+ let mut line = 1;
+ let mut column = 0;
+
+ for byte in to_scan.bytes() {
+ match byte as char {
+ '\n' => {
+ line += 1;
+ column = 0;
+ }
+ _ => {
+ column += 1;
+ }
+ }
+ }
+
+ (line, column)
+}
diff --git a/vendor/tinytemplate/src/instruction.rs b/vendor/tinytemplate/src/instruction.rs new file mode 100755 index 000000000..0e1981465 --- /dev/null +++ b/vendor/tinytemplate/src/instruction.rs @@ -0,0 +1,85 @@ +use std::ops::Deref;
+
+/// TinyTemplate implements a simple bytecode interpreter for its template engine. Instructions
+/// for this interpreter are represented by the Instruction enum and typically contain various
+/// parameters such as the path to context values or name strings.
+///
+/// In TinyTemplate, the template string itself is assumed to be statically available (or at least
+/// longer-lived than the TinyTemplate instance) so paths and instructions simply borrow string
+/// slices from the template text. These string slices can then be appended directly to the output
+/// string.
+
+/// Enum for a step in a path which optionally contains a parsed index.
+#[derive(Eq, PartialEq, Debug, Clone)]
+pub(crate) enum PathStep<'template> {
+ Name(&'template str),
+ Index(&'template str, usize),
+}
+impl<'template> Deref for PathStep<'template> {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ match self {
+ PathStep::Name(s) => s,
+ PathStep::Index(s, _) => s,
+ }
+ }
+}
+
+/// Sequence of named steps used for looking up values in the context
+pub(crate) type Path<'template> = Vec<PathStep<'template>>;
+
+/// Path, but as a slice.
+pub(crate) type PathSlice<'a, 'template> = &'a [PathStep<'template>];
+
+/// Enum representing the bytecode instructions.
+#[derive(Eq, PartialEq, Debug, Clone)]
+pub(crate) enum Instruction<'template> {
+ /// Emit a literal string into the output buffer
+ Literal(&'template str),
+
+ /// Look up the value for the given path and render it into the output buffer using the default
+ /// formatter
+ Value(Path<'template>),
+
+ /// Look up the value for the given path and pass it to the formatter with the given name
+ FormattedValue(Path<'template>, &'template str),
+
+ /// Look up the value at the given path and jump to the given instruction index if that value
+ /// is truthy (if the boolean is true) or falsy (if the boolean is false)
+ Branch(Path<'template>, bool, usize),
+
+ /// Push a named context on the stack, shadowing only that name.
+ PushNamedContext(Path<'template>, &'template str),
+
+ /// Push an iteration context on the stack, shadowing the given name with the current value from
+ /// the vec pointed to by the path. The current value will be updated by the Iterate instruction.
+ /// This is always generated before an Iterate instruction which actually starts the iterator.
+ PushIterationContext(Path<'template>, &'template str),
+
+ /// Pop a context off the stack
+ PopContext,
+
+ /// Advance the topmost iterator on the context stack by one and update that context. If the
+ /// iterator is empty, jump to the given instruction.
+ Iterate(usize),
+
+ /// Unconditionally jump to the given instruction. Used to skip else blocks and repeat loops.
+ Goto(usize),
+
+ /// Look up the named template and render it into the output buffer with the value pointed to
+ /// by the path as its context.
+ Call(&'template str, Path<'template>),
+}
+
+/// Convert a path back into a dotted string.
+pub(crate) fn path_to_str(path: PathSlice) -> String {
+ let mut path_str = "".to_string();
+ for (i, step) in path.iter().enumerate() {
+ if i > 0 {
+ path_str.push('.');
+ }
+ path_str.push_str(step);
+ }
+ path_str
+}
diff --git a/vendor/tinytemplate/src/lib.rs b/vendor/tinytemplate/src/lib.rs new file mode 100755 index 000000000..396be217c --- /dev/null +++ b/vendor/tinytemplate/src/lib.rs @@ -0,0 +1,260 @@ +//! ## TinyTemplate +//! +//! TinyTemplate is a minimal templating library originally designed for use in [Criterion.rs]. +//! It deliberately does not provide all of the features of a full-power template engine, but in +//! return it provides a simple API, clear templating syntax, decent performance and very few +//! dependencies. +//! +//! ## Features +//! +//! The most important features are as follows (see the [syntax](syntax/index.html) module for full +//! details on the template syntax): +//! +//! * Rendering values - `{ myvalue }` +//! * Conditionals - `{{ if foo }}Foo is true{{ else }}Foo is false{{ endif }}` +//! * Loops - `{{ for value in row }}{value}{{ endfor }}` +//! * Customizable value formatters `{ value | my_formatter }` +//! * Macros `{{ call my_template with foo }}` +//! +//! ## Restrictions +//! +//! TinyTemplate was designed with the assumption that the templates are available as static strings, +//! either using string literals or the `include_str!` macro. Thus, it borrows `&str` slices from the +//! template text itself and uses them during the rendering process. Although it is possible to use +//! TinyTemplate with template strings loaded at runtime, this is not recommended. +//! +//! Additionally, TinyTemplate can only render templates into Strings. If you need to render a +//! template directly to a socket or file, TinyTemplate may not be right for you. +//! +//! ## Example +//! +//! ``` +//! #[macro_use] +//! extern crate serde_derive; +//! extern crate tinytemplate; +//! +//! use tinytemplate::TinyTemplate; +//! use std::error::Error; +//! +//! #[derive(Serialize)] +//! struct Context { +//! name: String, +//! } +//! +//! static TEMPLATE : &'static str = "Hello {name}!"; +//! +//! pub fn main() -> Result<(), Box<Error>> { +//! let mut tt = TinyTemplate::new(); +//! tt.add_template("hello", TEMPLATE)?; +//! +//! let context = Context { +//! name: "World".to_string(), +//! }; +//! +//! let rendered = tt.render("hello", &context)?; +//! # assert_eq!("Hello World!", &rendered); +//! println!("{}", rendered); +//! +//! Ok(()) +//! } +//! ``` +//! +//! [Criterion.rs]: https://github.com/bheisler/criterion.rs +//! + +extern crate serde; +extern crate serde_json; + +#[cfg(test)] +#[cfg_attr(test, macro_use)] +extern crate serde_derive; + +mod compiler; +pub mod error; +mod instruction; +pub mod syntax; +mod template; + +use error::*; +use serde::Serialize; +use serde_json::Value; +use std::collections::HashMap; +use std::fmt::Write; +use template::Template; + +/// Type alias for closures which can be used as value formatters. +pub type ValueFormatter = dyn Fn(&Value, &mut String) -> Result<()>; + +/// Appends `value` to `output`, performing HTML-escaping in the process. +pub fn escape(value: &str, output: &mut String) { + // Algorithm taken from the rustdoc source code. + let value_str = value; + let mut last_emitted = 0; + for (i, ch) in value.bytes().enumerate() { + match ch as char { + '<' | '>' | '&' | '\'' | '"' => { + output.push_str(&value_str[last_emitted..i]); + let s = match ch as char { + '>' => ">", + '<' => "<", + '&' => "&", + '\'' => "'", + '"' => """, + _ => unreachable!(), + }; + output.push_str(s); + last_emitted = i + 1; + } + _ => {} + } + } + + if last_emitted < value_str.len() { + output.push_str(&value_str[last_emitted..]); + } +} + +/// The format function is used as the default value formatter for all values unless the user +/// specifies another. It is provided publicly so that it can be called as part of custom formatters. +/// Values are formatted as follows: +/// +/// * `Value::Null` => the empty string +/// * `Value::Bool` => true|false +/// * `Value::Number` => the number, as formatted by `serde_json`. +/// * `Value::String` => the string, HTML-escaped +/// +/// Arrays and objects are not formatted, and attempting to do so will result in a rendering error. +pub fn format(value: &Value, output: &mut String) -> Result<()> { + match value { + Value::Null => Ok(()), + Value::Bool(b) => { + write!(output, "{}", b)?; + Ok(()) + } + Value::Number(n) => { + write!(output, "{}", n)?; + Ok(()) + } + Value::String(s) => { + escape(s, output); + Ok(()) + } + _ => Err(unprintable_error()), + } +} + +/// Identical to [`format`](fn.format.html) except that this does not perform HTML escaping. +pub fn format_unescaped(value: &Value, output: &mut String) -> Result<()> { + match value { + Value::Null => Ok(()), + Value::Bool(b) => { + write!(output, "{}", b)?; + Ok(()) + } + Value::Number(n) => { + write!(output, "{}", n)?; + Ok(()) + } + Value::String(s) => { + output.push_str(s); + Ok(()) + } + _ => Err(unprintable_error()), + } +} + +/// The TinyTemplate struct is the entry point for the TinyTemplate library. It contains the +/// template and formatter registries and provides functions to render templates as well as to +/// register templates and formatters. +pub struct TinyTemplate<'template> { + templates: HashMap<&'template str, Template<'template>>, + formatters: HashMap<&'template str, Box<ValueFormatter>>, + default_formatter: &'template ValueFormatter, +} +impl<'template> TinyTemplate<'template> { + /// Create a new TinyTemplate registry. The returned registry contains no templates, and has + /// [`format_unescaped`](fn.format_unescaped.html) registered as a formatter named "unescaped". + pub fn new() -> TinyTemplate<'template> { + let mut tt = TinyTemplate { + templates: HashMap::default(), + formatters: HashMap::default(), + default_formatter: &format, + }; + tt.add_formatter("unescaped", format_unescaped); + tt + } + + /// Parse and compile the given template, then register it under the given name. + pub fn add_template(&mut self, name: &'template str, text: &'template str) -> Result<()> { + let template = Template::compile(text)?; + self.templates.insert(name, template); + Ok(()) + } + + /// Changes the default formatter from [`format`](fn.format.html) to `formatter`. Usefull in combination with [`format_unescaped`](fn.format_unescaped.html) to deactivate HTML-escaping + pub fn set_default_formatter<F>(&mut self, formatter: &'template F) + where + F: 'static + Fn(&Value, &mut String) -> Result<()>, + { + self.default_formatter = formatter; + } + + /// Register the given formatter function under the given name. + pub fn add_formatter<F>(&mut self, name: &'template str, formatter: F) + where + F: 'static + Fn(&Value, &mut String) -> Result<()>, + { + self.formatters.insert(name, Box::new(formatter)); + } + + /// Render the template with the given name using the given context object. The context + /// object must implement `serde::Serialize` as it will be converted to `serde_json::Value`. + pub fn render<C>(&self, template: &str, context: &C) -> Result<String> + where + C: Serialize, + { + let value = serde_json::to_value(context)?; + match self.templates.get(template) { + Some(tmpl) => tmpl.render( + &value, + &self.templates, + &self.formatters, + self.default_formatter, + ), + None => Err(Error::GenericError { + msg: format!("Unknown template '{}'", template), + }), + } + } +} +impl<'template> Default for TinyTemplate<'template> { + fn default() -> TinyTemplate<'template> { + TinyTemplate::new() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Serialize)] + struct Context { + name: String, + } + + static TEMPLATE: &'static str = "Hello {name}!"; + + #[test] + pub fn test_set_default_formatter() { + let mut tt = TinyTemplate::new(); + tt.add_template("hello", TEMPLATE).unwrap(); + tt.set_default_formatter(&format_unescaped); + + let context = Context { + name: "<World>".to_string(), + }; + + let rendered = tt.render("hello", &context).unwrap(); + assert_eq!(rendered, "Hello <World>!") + } +} diff --git a/vendor/tinytemplate/src/syntax.rs b/vendor/tinytemplate/src/syntax.rs new file mode 100755 index 000000000..2e8eedd1e --- /dev/null +++ b/vendor/tinytemplate/src/syntax.rs @@ -0,0 +1,184 @@ +//! Documentation of TinyTemplate's template syntax. +//! +//! ### Context Types +//! +//! TinyTemplate uses `serde_json`'s Value structure to represent the context. Therefore, any +//! `Serializable` structure can be used as a context. All values in such structures are mapped to +//! their JSON representations - booleans, numbers, strings, arrays, objects and nulls. +//! +//! ### Values +//! +//! Template values are marked with `{...}`. For example, this will look up the "name" field in +//! the context structure and insert it into the rendered string: +//! +//! ```text +//! Hello, {name}, how are you? +//! ``` +//! +//! Optionally, a value formatter may be provided. One formatter, "unescaped", is provided by +//! default. Any other formatters must be registered with the +//! [`TinyTemplate.add_formatter`](../struct.TinyTemplate.html#method.add_formatter) +//! function prior to rendering or an error will be generated. This will call the formatter function +//! registered as "percent_formatter" with the value of the "percentage" field: +//! +//! ```text +//! Give it {percentage | percent_formatter}! +//! ``` +//! +//! The value may be a dotted path through a hierarchy of context objects. This will look up the +//! "friend" field in the context structure, then substitute the "name" field from the "friend" +//! object. +//! +//! ```text +//! And hello to {friend.name} as well! +//! ``` +//! +//! Additionally, you may use the `@root` keyword to refer to the root object of your context. +//! Since TinyTemplate can't normally print complex context objects, this is only useful if the +//! context is a simple object like an integer or string. +//! +//! ### Conditionals +//! +//! TinyTemplate blocks are marked with `{{...}}` - double-braces where values are single-braces. +//! +//! Conditionals are denoted by "{{ if path }}...{{ else }}...{{ endif }}". The Else block is +//! optional. Else-if is not currently supported. If "path" evaluates to a truthy expression +//! (true if boolean, non-zero if numeric, non-empty for strings and arrays, and non-null for +//! objects) then the section of the template between "if" and "else" is evaluated, otherwise the +//! section between "else" and "endif" (if present) is evaluated. +//! +//! ```text +//! {{ if user.is_birthday }} +//! Happy Birthday! +//! {{ else }} +//! Have a nice day! +//! {{ endif }} +//! ``` +//! +//! The condition can be negated by using "{{ if not path }}": +//! +//! ```text +//! {{ if not user.is_birthday }} +//! Have a nice day! +//! {{ else }} +//! Happy Birthday! +//! {{ endif }} +//! ``` +//! +//! If desired, the `@root` keyword can be used to branch on the root context object. +//! +//! ### Loops +//! +//! TinyTemplate supports iterating over the values of arrays. Only arrays are supported. Loops +//! are denoted by "{{ for value_name in value.path }}...{{ endfor }}". The section of the template between +//! the two tags will be executed once for each value in the array denoted by "value.path". +//! +//! ```text +//! Hello to {{ for name in guests }} +//! {name} +//! {{ endfor }} +//! ``` +//! +//! If the iteration value chosen in the "for" tag is the same as that of a regular context value, +//! the name in the tag will shadow the context value for the scope of the loop. For nested loops, +//! inner loops will shadow the values of outer loops. +//! +//! ```text +//! {{ for person in guests }} +//! Hello to {person}{{ for person in person.friends }} and your friend {person}{{ endfor }} +//! {{ endfor }} +//! ``` +//! +//! There are three special values which are available within a loop: +//! +//! * `@index` - zero-based index of the current value within the array. +//! * `@first` - true if this is the first iteration of the loop, otherwise false. +//! * `@last` - true if this is the last iteration of the loop, otherwise false. +//! +//! ```text +//! Hello to {{ for name in guests -}} +//! { @index }. {name}, +//! {{- endfor }} +//! ``` +//! +//! +//! In case of nested loops, these values refer to the innermost loop which contains them. +//! +//! If the root context object is an array, the `@root` keyword can be used to iterate over the +//! root object. +//! +//! ### With Blocks +//! +//! Templates can use with blocks to partially shadows the outer context, the same way that +//! for-loops do. These are formed like so: +//! +//! "{{ with path.to.value as name }}..{{ endwith }}"" +//! +//! For example: +//! +//! ```text +//! {{ with person.spouse as s }} +//! Hello { s.name }! +//! {{ endwith }} +//! ``` +//! +//! This looks up "person.spouse" and adds that to the context as "s" within the block. Only the +//! name "s" is shadowed within the with block and otherwise the outer context is still accessible. +//! +//! ### Trimming Whitespace +//! +//! If a block tag, comment or value tag includes a "-" character at the start, the trailing +//! whitespace of the previous text section will be skipped in the output. Likewise, if the tag +//! ends with a "-", the leading whitespace of the following text will be skipped. +//! +//! ```text +//! Hello { friend.name -} +//! , how are you? +//! +//! {{- if status.good }} I am fine. {{- endif }} +//! ``` +//! +//! This will print "Hello friend, how are you? I am fine." without the newlines or extra spaces. +//! +//! ### Calling other Templates +//! +//! Templates may call other templates by name. The other template must have been registered using +//! the [`TinyTemplate.add_template`](../struct.TinyTemplate.html#method.add_template) function +//! before rendering or an error will be generated. This is done with the "call" tag: +//! +//! "{{ call template_name with path.to.context }}" +//! +//! The call tag has no closing tag. This will look up the "path.to.context" path in the current +//! context, then render the "template_name" template using the value at that path as the context +//! for the other template. The string produced by the called template is then inserted into the +//! output from the calling template. This can be used for a limited form of template code reuse. +//! +//! ### Comments +//! +//! Comments in the templates are denoted by "{# comment text #}". Comments will be skipped when +//! rendering the template, though whitespace adjacent to comments will not be stripped unless the +//! "-" is added. For example: +//! +//! ```text +//! Hello +//! +//! {#- This is a comment #} world! +//! ``` +//! +//! This will print "Hello world!". +//! +//! ### Escaping Curly Braces +//! +//! If your template contains opening curly-braces (`{`), they must be escaped using a leading `\` +//! character. For example: +//! +//! ```text +//! h2 \{ +//! font-size: {fontsize}; +//! } +//! ``` +//! +//! If using a string literal in rust source code, the `\` itself must be escaped, producing `\\{`. +//! + +// There's nothing here, this module is solely for documentation. diff --git a/vendor/tinytemplate/src/template.rs b/vendor/tinytemplate/src/template.rs new file mode 100755 index 000000000..6f0162d22 --- /dev/null +++ b/vendor/tinytemplate/src/template.rs @@ -0,0 +1,944 @@ +//! This module implements the bytecode interpreter that actually renders the templates. + +use compiler::TemplateCompiler; +use error::Error::*; +use error::*; +use instruction::{Instruction, PathSlice, PathStep}; +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() { + if let PathStep::Index(_, n) = step { + if let Some(next) = current.get(n) { + current = next; + continue; + } + } + + let step: &str = &*step; + + 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(), + }) + } + + /// Look up the root context object + fn lookup_root(&self) -> Result<&'render Value> { + match self.context_stack.get(0) { + Some(ContextElement::Object(obj)) => Ok(obj), + Some(_) => { + panic!("Expected Object value at root of context stack, but was something else.") + } + None => panic!( + "Attempted to do a lookup with an empty context stack. That shouldn't be possible." + ), + } + } +} + +/// 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>>, + default_formatter: &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, + default_formatter, + &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>>, + default_formatter: &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. + let first: &str = &*first; + 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 - 1).unwrap() + } + "@root" => { + let value_to_render = render_context.lookup_root()?; + default_formatter(value_to_render, output)?; + } + _ => panic!(), // This should have been caught by the parser. + } + } else { + let value_to_render = render_context.lookup(path)?; + default_formatter(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('@') { + let first: &str = &*first; + 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 - 1) + } + "@root" => self.value_is_truthy(render_context.lookup_root()?, path)?, + other => panic!("Unknown keyword {}", other), // This should have been caught by the parser. + } + } else { + let value_to_render = render_context.lookup(path)?; + self.value_is_truthy(value_to_render, path)? + }; + 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 first = path.first().unwrap(); + let context_value = match first { + PathStep::Name("@root") => render_context.lookup_root()?, + PathStep::Name(other) if other.starts_with('@') => { + return Err(not_iterable_error(self.original_text, path)) + } + _ => 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, + default_formatter, + 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(()) + } + + fn value_is_truthy(&self, value: &Value, path: PathSlice) -> Result<bool> { + let truthy = match value { + 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, + }; + Ok(truthy) + } +} + +#[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 + } + + pub fn default_formatter() -> &'static ValueFormatter { + &::format + } + + #[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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .unwrap(); + assert_eq!("0", &string); + } + + #[test] + fn test_for_loop_last() { + let template = + compile("{{ for a in array }}{{ if @last}}{ @index }{{ endif }}{{ endfor }}"); + let context = context(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("2", &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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .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, + &default_formatter(), + ) + .unwrap(); + assert_eq!("1:< 2:> 3:& 4:' 5:"", &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, + &default_formatter(), + ) + .unwrap(); + assert_eq!("1:< 2:> 3:& 4:' 5:\"", &string); + } + + #[test] + fn test_root_print() { + let template = compile("{ @root }"); + let context = "Hello World!"; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("Hello World!", &string); + } + + #[test] + fn test_root_branch() { + let template = compile("{{ if @root }}Hello World!{{ endif }}"); + let context = true; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("Hello World!", &string); + } + + #[test] + fn test_root_iterate() { + let template = compile("{{ for a in @root }}{ a }{{ endfor }}"); + let context = vec!["foo", "bar"]; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("foobar", &string); + } + + #[test] + fn test_number_truthiness_zero() { + let template = compile("{{ if @root }}truthy{{else}}not truthy{{ endif }}"); + let context = 0; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("not truthy", &string); + } + + #[test] + fn test_number_truthiness_one() { + let template = compile("{{ if @root }}truthy{{else}}not truthy{{ endif }}"); + let context = 1; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("truthy", &string); + } + + #[test] + fn test_indexed_paths() { + #[derive(Serialize)] + struct Context { + foo: (usize, usize), + } + + let template = compile("{ foo.1 }{ foo.0 }"); + let context = Context { foo: (123, 456) }; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("456123", &string); + } + + #[test] + fn test_indexed_paths_fall_back_to_string_lookup() { + #[derive(Serialize)] + struct Context { + foo: HashMap<&'static str, usize>, + } + + let template = compile("{ foo.1 }{ foo.0 }"); + let mut foo = HashMap::new(); + foo.insert("0", 123); + foo.insert("1", 456); + let context = Context { foo }; + let context = ::serde_json::to_value(&context).unwrap(); + let template_registry = other_templates(); + let formatter_registry = formatters(); + let string = template + .render( + &context, + &template_registry, + &formatter_registry, + &default_formatter(), + ) + .unwrap(); + assert_eq!("456123", &string); + } +} |