aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Kotur <qtr@google.com>2021-03-16 20:53:03 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-03-16 20:53:03 +0000
commitde7eb62629384efaffc4b2a3e81aa90e34f6c49c (patch)
tree8c8f80847f7d638f01ed6c859752486cefcbeb9a
parentff1733c4286076e5b99cef5037303197148bab1e (diff)
parent67956555e17dfdc47424ae152c9e9692d49ed29c (diff)
downloadtinytemplate-de7eb62629384efaffc4b2a3e81aa90e34f6c49c.tar.gz
Initial import of tinytemplate-1.1.0. am: 393f6b5e85 am: 08b231ea7d am: 5d5511edf1 am: 67956555e1
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/tinytemplate/+/1622421 Change-Id: I04431c2f244d0765fe30d9f1c5dd42dcb2709091
-rw-r--r--.cargo_vcs_info.json5
-rwxr-xr-x.gitignore3
-rwxr-xr-x.travis.yml36
-rwxr-xr-xCHANGELOG.md39
-rwxr-xr-xCONTRIBUTING.md67
-rw-r--r--Cargo.toml41
-rwxr-xr-xCargo.toml.orig27
-rwxr-xr-xREADME.md133
-rwxr-xr-xbenches/benchmarks.rs58
-rwxr-xr-xci/install.sh9
-rwxr-xr-xci/script.sh9
-rwxr-xr-xsrc/compiler.rs653
-rwxr-xr-xsrc/error.rs220
-rwxr-xr-xsrc/instruction.rs66
-rwxr-xr-xsrc/lib.rs260
-rwxr-xr-xsrc/syntax.rs184
-rwxr-xr-xsrc/template.rs848
17 files changed, 2658 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..97e8581
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,5 @@
+{
+ "git": {
+ "sha1": "3833799e58db60b87ebfe5a1e2dbe4fc0729621a"
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..6936990
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+Cargo.lock
diff --git a/.travis.yml b/.travis.yml
new file mode 100755
index 0000000..acd6984
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,36 @@
+sudo: true
+
+language: rust
+
+cache: cargo
+
+dist: xenial
+
+rust:
+ - stable
+
+os:
+ - linux
+
+matrix:
+ include:
+ - os: linux
+ - os: linux
+ rust: 1.36.0
+ - os: linux
+ env: RUSTFMT=yes
+ - os: linux
+ env: CLIPPY=yes
+
+install:
+ - bash ci/install.sh
+
+script:
+ - bash ci/script.sh
+
+branches:
+ only: master
+
+notifications:
+ email:
+ on_success: never
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100755
index 0000000..b02a229
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,39 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [1.1.0] - 2020-05-31
+ - Added `TinyTemplate::set_default_formatter` which, for example, allows to dissable HTML-scaping
+
+## [1.0.4] - 2020-04-25
+### Added
+- Added `@root` keyword which allows printing, branching on or iterating over the root context
+ object. This is saves having to wrap simple context values in a struct.
+
+## [1.0.3] - 2019-12-26
+### Fixed
+- Fixed the @last keyword never evaluating to true
+- Fixed numeric values being truthy when zero, rather than when non-zero.
+
+## [1.0.2] - 2019-05-16
+### Fixed
+- Fixed possible panic when compiling templates with escaped curly braces.
+
+## [1.0.1] - 2019-01-19
+### Added
+- Added support for older versions of Rust (back to 1.26).
+
+## 1.0.0 - 2019-01-19
+### Added
+- Initial release on Crates.io.
+
+[Unreleased]: https://github.com/bheisler/TinyTemplate/compare/1.0.3...HEAD
+[1.0.1]: https://github.com/bheisler/TinyTemplate/compare/1.0.0...1.0.1
+[1.0.2]: https://github.com/bheisler/TinyTemplate/compare/1.0.1...1.0.2
+[1.0.3]: https://github.com/bheisler/TinyTemplate/compare/1.0.2...1.0.3
+[1.0.4]: https://github.com/bheisler/TinyTemplate/compare/1.0.3...1.0.4
+[1.1.0]: https://github.com/bheisler/TinyTemplate/compare/1.0.4...1.1.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100755
index 0000000..e6af4b7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,67 @@
+# Contributing to TinyTemplate
+
+## Ideas, Experiences and Questions
+
+The easiest way to contribute to TinyTemplate is to use it and report your experiences, ask questions and contribute ideas. We'd love to hear your thoughts on how to make TinyTemplate better, or your comments on why you are or are not currently using it.
+
+Issues, ideas, requests and questions should be posted on the issue tracker at:
+
+https://github.com/bheisler/TinyTemplate/issues
+
+## Code
+
+Pull requests are welcome, though please raise an issue or post a comment for discussion first. We're happy to assist new contributors.
+
+If you're not sure what to work on, try checking the [Beginner label](https://github.com/bheisler/TinyTemplate/labels/Beginner)
+
+To make changes to the code, fork the repo and clone it:
+
+`git clone git@github.com:your-username/TinyTemplate.git`
+
+Then make your changes to the code. When you're done, run the tests:
+
+```
+cargo test
+```
+
+It's a good idea to run clippy and fix any warnings as well:
+
+```
+rustup component add clippy-preview
+cargo clippy
+```
+
+Finally, run Rustfmt to maintain a common code style:
+
+```
+rustup component add rustfmt-preview
+cargo fmt
+```
+
+Don't forget to update the CHANGELOG.md file and any appropriate documentation. Once you're finished, push to your fork and submit a pull request. We try to respond to new issues and pull requests quickly, so if there hasn't been any response for more than a few days feel free to ping @bheisler.
+
+Some things that will increase the chance that your pull request is accepted:
+
+* Write tests
+* Clearly document public methods, with examples if possible
+* Write a good commit message
+
+Good documentation is one of the core goals of the TinyTemplate project, so new code in pull requests should have clear and complete documentation.
+
+## Github Labels
+
+TinyTemplate uses a simple set of labels to track issues. Most important are the difficulty labels:
+
+- Beginner - Suitable for people new to TinyTemplate
+- Intermediate - More challenging, likely involves some non-obvious design decisions or knowledge of CUDA
+- Bigger Project - Large and/or complex project such as designing a safe, Rusty wrapper around a complex part of the CUDA API
+
+Additionally, there are a few other noteworthy labels:
+
+- Breaking Change - Fixing this will have to wait until the next breaking-change release
+- Enhancement - Enhancements to existing functionality or documentation
+- Help Wanted - Input and ideas requested
+
+## Code of Conduct
+
+We follow the [Rust Code of Conduct](http://www.rust-lang.org/conduct.html).
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..22f2454
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,41 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "tinytemplate"
+version = "1.1.0"
+authors = ["Brook Heisler <brookheisler@gmail.com>"]
+description = "Simple, lightweight template engine"
+readme = "README.md"
+keywords = ["template", "html"]
+categories = ["template-engine"]
+license = "Apache-2.0 OR MIT"
+repository = "https://github.com/bheisler/TinyTemplate"
+
+[[bench]]
+name = "benchmarks"
+harness = false
+[dependencies.serde]
+version = "1.0"
+
+[dependencies.serde_json]
+version = "1.0"
+[dev-dependencies.criterion]
+version = "0.3"
+
+[dev-dependencies.serde_derive]
+version = "1.0"
+[badges.maintenance]
+status = "passively-maintained"
+
+[badges.travis-ci]
+repository = "bheisler/TinyTemplate"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100755
index 0000000..2c48a14
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,27 @@
+[package]
+name = "tinytemplate"
+version = "1.1.0"
+authors = ["Brook Heisler <brookheisler@gmail.com>"]
+
+description = "Simple, lightweight template engine"
+repository = "https://github.com/bheisler/TinyTemplate"
+readme = "README.md"
+license = "Apache-2.0 OR MIT"
+keywords = ["template", "html"]
+categories = ["template-engine"]
+
+[badges]
+travis-ci = { repository = "bheisler/TinyTemplate" }
+maintenance = { status = "passively-maintained" }
+
+[dependencies]
+serde = "1.0"
+serde_json = "1.0"
+
+[dev-dependencies]
+serde_derive = "1.0"
+criterion = "0.3"
+
+[[bench]]
+name = "benchmarks"
+harness = false
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..186c5b5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,133 @@
+<h1 align="center">TinyTemplate</h1>
+
+<div align="center">Minimal Lightweight Text Templating</div>
+
+<div align="center">
+ <a href="https://docs.rs/tinytemplate/">API Documentation</a>
+ |
+ <a href="https://github.com/bheisler/TinyTemplate/blob/master/CHANGELOG.md">Changelog</a>
+</div>
+
+<div align="center">
+ <a href="https://travis-ci.org/bheisler/TinyTemplate">
+ <img src="https://travis-ci.org/bheisler/TinyTemplate.svg?branch=master" alt="Travis-CI">
+ </a>
+ |
+ <a href="https://crates.io/crates/tinytemplate">
+ <img src="https://img.shields.io/crates/v/tinytemplate.svg" alt=Crates.io">
+ </a>
+</div>
+
+TinyTemplate is a small, minimalistic text templating system with limited dependencies.
+
+## Table of Contents
+- [Table of Contents](#table-of-contents)
+ - [Goals](#goals)
+ - [Why TinyTemplate?](#why-tinytemplate)
+ - [Quickstart](#quickstart)
+ - [Compatibility Policy](#compatibility-policy)
+ - [Contributing](#contributing)
+ - [Maintenance](#maintenance)
+ - [License](#license)
+
+### Goals
+
+ The primary design goals are:
+
+ - __Small__: TinyTemplate deliberately does not support many features of more powerful template engines.
+ - __Simple__: TinyTemplate presents a minimal but well-documented user-facing API.
+ - __Lightweight__: TinyTemplate has minimal required dependencies.
+
+Non-goals include:
+
+- __Extensibility__: TinyTemplate supports custom value formatters, but that is all.
+- __Performance__: TinyTemplate provides decent performance, but other template engines are faster.
+
+### Why TinyTemplate?
+
+I created TinyTemplate after noticing that none of the existing template libraries really suited my
+needs for Criterion.rs. Some had large dependency trees to support features that I didn't use. Some
+required adding a build script to convert templates into code at runtime, in search of extreme
+performance that I didn't need. Some had elaborate macro-based DSL's to generate HTML, where I just
+wanted plain text with some markup. Some expect the templates to be provided in a directory of text
+files, but I wanted the template to be included in the binary. I just wanted something small and
+minimal with good documentation but there was nothing like that out there so I wrote my own.
+
+TinyTemplate is well-suited to generating HTML reports and similar text files. It could be used for
+generating HTML or other text in a web-server, but for more-complex use cases another template
+engine may be a better fit.
+
+### Quickstart
+
+First, add TinyTemplate and serde-derive to your `Cargo.toml` file:
+
+```toml
+[dependencies]
+tinytemplate = "1.0"
+serde_derive = "1.0"
+```
+
+Then add this code to "src.rs":
+
+```rust
+#[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<dyn Error>> {
+ let mut tt = TinyTemplate::new();
+ tt.add_template("hello", TEMPLATE)?;
+
+ let context = Context {
+ name: "World".to_string(),
+ };
+
+ let rendered = tt.render("hello", &context)?;
+ println!("{}", rendered);
+
+ Ok(())
+}
+```
+
+This should print "Hello World!" to stdout.
+
+### Compatibility Policy
+
+TinyTemplate supports the last three stable minor releases of Rust. At time of writing, this means
+Rust 1.38 or later. Older versions may work, but are not tested or guaranteed.
+
+Currently, the oldest version of Rust believed to work is 1.36. Future versions of TinyTemplate may
+break support for such old versions, and this will not be considered a breaking change. If you
+require TinyTemplate to work on old versions of Rust, you will need to stick to a
+specific patch version of TinyTemplate.
+
+### Contributing
+
+Thanks for your interest! Contributions are welcome.
+
+Issues, feature requests, questions and bug reports should be reported via the issue tracker above.
+In particular, becuase TinyTemplate aims to be well-documented, please report anything you find
+confusing or incorrect in the documentation.
+
+Code or documentation improvements in the form of pull requests are also welcome. Please file or
+comment on an issue to allow for discussion before doing a lot of work, though.
+
+For more details, see the [CONTRIBUTING.md file](https://github.com/bheisler/TinyTemplate/blob/master/CONTRIBUTING.md).
+
+### Maintenance
+
+TinyTemplate was created and is currently maintained by Brook Heisler (@bheisler).
+
+### License
+
+TinyTemplate is dual-licensed under the Apache 2.0 license and the MIT license.
diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs
new file mode 100755
index 0000000..5170f98
--- /dev/null
+++ b/benches/benchmarks.rs
@@ -0,0 +1,58 @@
+#[macro_use]
+extern crate criterion;
+extern crate tinytemplate;
+#[macro_use]
+extern crate serde_derive;
+
+use criterion::Criterion;
+use tinytemplate::TinyTemplate;
+
+static TABLE_SOURCE: &'static str = "<html>
+ {{ for row in table }}
+ <tr>{{ for value in row }}<td>{value}</td>{{ endfor }}</tr>
+ {{ endfor }}
+</html>";
+
+#[derive(Serialize)]
+struct TableContext {
+ table: Vec<Vec<usize>>,
+}
+
+fn make_table_context(size: usize) -> TableContext {
+ let mut table = Vec::with_capacity(size);
+ for _ in 0..size {
+ let mut inner = Vec::with_capacity(size);
+ for i in 0..size {
+ inner.push(i);
+ }
+ table.push(inner);
+ }
+ TableContext { table }
+}
+
+fn parse(criterion: &mut Criterion) {
+ criterion.bench_function("parse-table", |b| {
+ b.iter(|| {
+ let mut tt = TinyTemplate::new();
+ tt.add_template("table", TABLE_SOURCE).unwrap()
+ });
+ });
+}
+
+fn render(criterion: &mut Criterion) {
+ let mut tt = TinyTemplate::new();
+ tt.add_template("table", TABLE_SOURCE).unwrap();
+
+ criterion.bench_function_over_inputs(
+ "render-table",
+ move |b, size| {
+ let data = make_table_context(*size);
+
+ b.iter(|| tt.render("table", &data).unwrap());
+ },
+ vec![1usize, 5, 10, 50, 100, 200],
+ );
+}
+
+criterion_group!(benchmarks, parse, render);
+criterion_main!(benchmarks);
diff --git a/ci/install.sh b/ci/install.sh
new file mode 100755
index 0000000..56bb887
--- /dev/null
+++ b/ci/install.sh
@@ -0,0 +1,9 @@
+set -ex
+
+if [ "$RUSTFMT" = "yes" ]; then
+ rustup component add rustfmt-preview
+fi
+
+if [ "$CLIPPY" = "yes" ]; then
+ rustup component add clippy-preview
+fi \ No newline at end of file
diff --git a/ci/script.sh b/ci/script.sh
new file mode 100755
index 0000000..996013c
--- /dev/null
+++ b/ci/script.sh
@@ -0,0 +1,9 @@
+set -ex
+
+if [ "$RUSTFMT" = "yes" ]; then
+ cargo fmt --all -- --check
+elif [ "$CLIPPY" = "yes" ]; then
+ cargo clippy --all -- -D warnings
+else
+ cargo test
+fi \ No newline at end of file
diff --git a/src/compiler.rs b/src/compiler.rs
new file mode 100755
index 0000000..5b7e721
--- /dev/null
+++ b/src/compiler.rs
@@ -0,0 +1,653 @@
+#![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};
+
+/// 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('.').collect::<Vec<_>>())
+ } else if KNOWN_KEYWORDS.iter().any(|k| *k == text) {
+ Ok(vec![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!["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!["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!["foo", "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!["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!["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!["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!["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!["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!["bar", "baz"], "foo"),
+ &instructions[0]
+ );
+ assert_eq!(&Iterate(4), &instructions[1]);
+ assert_eq!(&Value(vec!["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!["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!["name"], true, 5), &instructions[1]);
+ assert_eq!(&Literal(""), &instructions[2]);
+ assert_eq!(&Value(vec!["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!["value"]), &instructions[0]);
+ assert_eq!(&Value(vec!["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!["foo", "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!["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/src/error.rs b/src/error.rs
new file mode 100755
index 0000000..92f4890
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,220 @@
+//! 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/src/instruction.rs b/src/instruction.rs
new file mode 100755
index 0000000..9bb79e3
--- /dev/null
+++ b/src/instruction.rs
@@ -0,0 +1,66 @@
+/// 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.
+
+/// Sequence of named steps used for looking up values in the context
+pub(crate) type Path<'template> = Vec<&'template str>;
+
+/// Path, but as a slice.
+pub(crate) type PathSlice<'a, 'template> = &'a [&'template str];
+
+/// 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/src/lib.rs b/src/lib.rs
new file mode 100755
index 0000000..396be21
--- /dev/null
+++ b/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 {
+ '>' => "&gt;",
+ '<' => "&lt;",
+ '&' => "&amp;",
+ '\'' => "&#39;",
+ '"' => "&quot;",
+ _ => 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/src/syntax.rs b/src/syntax.rs
new file mode 100755
index 0000000..2e8eedd
--- /dev/null
+++ b/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/src/template.rs b/src/template.rs
new file mode 100755
index 0000000..acc4b81
--- /dev/null
+++ b/src/template.rs
@@ -0,0 +1,848 @@
+//! This module implements the bytecode interpreter that actually renders the templates.
+
+use compiler::TemplateCompiler;
+use error::Error::*;
+use error::*;
+use instruction::{Instruction, PathSlice};
+use serde_json::Value;
+use std::collections::HashMap;
+use std::fmt::Write;
+use std::slice;
+use ValueFormatter;
+
+/// Enum defining the different kinds of records on the context stack.
+enum ContextElement<'render, 'template> {
+ /// Object contexts shadow everything below them on the stack, because every name is looked up
+ /// in this object.
+ Object(&'render Value),
+ /// Named contexts shadow only one name. Any path that starts with that name is looked up in
+ /// this object, and all others are passed on down the stack.
+ Named(&'template str, &'render Value),
+ /// Iteration contexts shadow one name with the current value of the iteration. They also
+ /// store the iteration state. The two usizes are the index of the current value and the length
+ /// of the array that we're iterating over.
+ Iteration(
+ &'template str,
+ &'render Value,
+ usize,
+ usize,
+ slice::Iter<'render, Value>,
+ ),
+}
+
+/// Helper struct which mostly exists so that I have somewhere to put functions that access the
+/// rendering context stack.
+struct RenderContext<'render, 'template> {
+ original_text: &'template str,
+ context_stack: Vec<ContextElement<'render, 'template>>,
+}
+impl<'render, 'template> RenderContext<'render, 'template> {
+ /// Look up the given path in the context stack and return the value (if found) or an error (if
+ /// not)
+ fn lookup(&self, path: PathSlice) -> Result<&'render Value> {
+ for stack_layer in self.context_stack.iter().rev() {
+ match stack_layer {
+ ContextElement::Object(obj) => return self.lookup_in(path, obj),
+ ContextElement::Named(name, obj) => {
+ if *name == path[0] {
+ return self.lookup_in(&path[1..], obj);
+ }
+ }
+ ContextElement::Iteration(name, obj, _, _, _) => {
+ if *name == path[0] {
+ return self.lookup_in(&path[1..], obj);
+ }
+ }
+ }
+ }
+ panic!("Attempted to do a lookup with an empty context stack. That shouldn't be possible.")
+ }
+
+ /// Look up a path within a given value object and return the resulting value (if found) or
+ /// an error (if not)
+ fn lookup_in(&self, path: PathSlice, object: &'render Value) -> Result<&'render Value> {
+ let mut current = object;
+ for step in path.iter() {
+ match current.get(step) {
+ Some(next) => current = next,
+ None => return Err(lookup_error(self.original_text, step, path, current)),
+ }
+ }
+ Ok(current)
+ }
+
+ /// Look up the index and length values for the top iteration context on the stack.
+ fn lookup_index(&self) -> Result<(usize, usize)> {
+ for stack_layer in self.context_stack.iter().rev() {
+ match stack_layer {
+ ContextElement::Iteration(_, _, index, length, _) => return Ok((*index, *length)),
+ _ => continue,
+ }
+ }
+ Err(GenericError {
+ msg: "Used @index outside of a foreach block.".to_string(),
+ })
+ }
+
+ /// 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.
+ 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('@') {
+ 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 {
+ "@root" => render_context.lookup_root()?,
+ 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: &[&str]) -> 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:&lt; 2:&gt; 3:&amp; 4:&#39; 5:&quot;", &string);
+ }
+
+ #[test]
+ fn test_unescaped() {
+ let template = compile("{ escapes | unescaped }");
+ let context = context();
+ let template_registry = other_templates();
+ let mut formatter_registry = formatters();
+ formatter_registry.insert("unescaped", Box::new(::format_unescaped));
+ let string = template
+ .render(
+ &context,
+ &template_registry,
+ &formatter_registry,
+ &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);
+ }
+}