1//! ## TinyTemplate
2//!
3//! TinyTemplate is a minimal templating library originally designed for use in [Criterion.rs].
4//! It deliberately does not provide all of the features of a full-power template engine, but in
5//! return it provides a simple API, clear templating syntax, decent performance and very few
6//! dependencies.
7//!
8//! ## Features
9//!
10//! The most important features are as follows (see the [syntax](syntax/index.html) module for full
11//! details on the template syntax):
12//!
13//! * Rendering values - `{ myvalue }`
14//! * Conditionals - `{{ if foo }}Foo is true{{ else }}Foo is false{{ endif }}`
15//! * Loops - `{{ for value in row }}{value}{{ endfor }}`
16//! * Customizable value formatters `{ value | my_formatter }`
17//! * Macros `{{ call my_template with foo }}`
18//!
19//! ## Restrictions
20//!
21//! TinyTemplate was designed with the assumption that the templates are available as static strings,
22//! either using string literals or the `include_str!` macro. Thus, it borrows `&str` slices from the
23//! template text itself and uses them during the rendering process. Although it is possible to use
24//! TinyTemplate with template strings loaded at runtime, this is not recommended.
25//!
26//! Additionally, TinyTemplate can only render templates into Strings. If you need to render a
27//! template directly to a socket or file, TinyTemplate may not be right for you.
28//!
29//! ## Example
30//!
31//! ```
32//! #[macro_use]
33//! extern crate serde_derive;
34//! extern crate tinytemplate;
35//!
36//! use tinytemplate::TinyTemplate;
37//! use std::error::Error;
38//!
39//! #[derive(Serialize)]
40//! struct Context {
41//! name: String,
42//! }
43//!
44//! static TEMPLATE : &'static str = "Hello {name}!";
45//!
46//! pub fn main() -> Result<(), Box<Error>> {
47//! let mut tt = TinyTemplate::new();
48//! tt.add_template("hello", TEMPLATE)?;
49//!
50//! let context = Context {
51//! name: "World".to_string(),
52//! };
53//!
54//! let rendered = tt.render("hello", &context)?;
55//! # assert_eq!("Hello World!", &rendered);
56//! println!("{}", rendered);
57//!
58//! Ok(())
59//! }
60//! ```
61//!
62//! [Criterion.rs]: https://github.com/bheisler/criterion.rs
63//!
64
65extern crate serde;
66extern crate serde_json;
67
68#[cfg(test)]
69#[cfg_attr(test, macro_use)]
70extern crate serde_derive;
71
72mod compiler;
73pub mod error;
74mod instruction;
75pub mod syntax;
76mod template;
77
78use error::*;
79use serde::Serialize;
80use serde_json::Value;
81use std::collections::HashMap;
82use std::fmt::Write;
83use template::Template;
84
85/// Type alias for closures which can be used as value formatters.
86pub type ValueFormatter = dyn Fn(&Value, &mut String) -> Result<()>;
87
88/// Appends `value` to `output`, performing HTML-escaping in the process.
89pub fn escape(value: &str, output: &mut String) {
90 // Algorithm taken from the rustdoc source code.
91 let value_str = value;
92 let mut last_emitted = 0;
93 for (i, ch) in value.bytes().enumerate() {
94 match ch as char {
95 '<' | '>' | '&' | '\'' | '"' => {
96 output.push_str(&value_str[last_emitted..i]);
97 let s = match ch as char {
98 '>' => ">",
99 '<' => "<",
100 '&' => "&",
101 '\'' => "'",
102 '"' => """,
103 _ => unreachable!(),
104 };
105 output.push_str(s);
106 last_emitted = i + 1;
107 }
108 _ => {}
109 }
110 }
111
112 if last_emitted < value_str.len() {
113 output.push_str(&value_str[last_emitted..]);
114 }
115}
116
117/// The format function is used as the default value formatter for all values unless the user
118/// specifies another. It is provided publicly so that it can be called as part of custom formatters.
119/// Values are formatted as follows:
120///
121/// * `Value::Null` => the empty string
122/// * `Value::Bool` => true|false
123/// * `Value::Number` => the number, as formatted by `serde_json`.
124/// * `Value::String` => the string, HTML-escaped
125///
126/// Arrays and objects are not formatted, and attempting to do so will result in a rendering error.
127pub fn format(value: &Value, output: &mut String) -> Result<()> {
128 match value {
129 Value::Null => Ok(()),
130 Value::Bool(b) => {
131 write!(output, "{}", b)?;
132 Ok(())
133 }
134 Value::Number(n) => {
135 write!(output, "{}", n)?;
136 Ok(())
137 }
138 Value::String(s) => {
139 escape(s, output);
140 Ok(())
141 }
142 _ => Err(unprintable_error()),
143 }
144}
145
146/// Identical to [`format`](fn.format.html) except that this does not perform HTML escaping.
147pub fn format_unescaped(value: &Value, output: &mut String) -> Result<()> {
148 match value {
149 Value::Null => Ok(()),
150 Value::Bool(b) => {
151 write!(output, "{}", b)?;
152 Ok(())
153 }
154 Value::Number(n) => {
155 write!(output, "{}", n)?;
156 Ok(())
157 }
158 Value::String(s) => {
159 output.push_str(s);
160 Ok(())
161 }
162 _ => Err(unprintable_error()),
163 }
164}
165
166/// The TinyTemplate struct is the entry point for the TinyTemplate library. It contains the
167/// template and formatter registries and provides functions to render templates as well as to
168/// register templates and formatters.
169pub struct TinyTemplate<'template> {
170 templates: HashMap<&'template str, Template<'template>>,
171 formatters: HashMap<&'template str, Box<ValueFormatter>>,
172 default_formatter: &'template ValueFormatter,
173}
174impl<'template> TinyTemplate<'template> {
175 /// Create a new TinyTemplate registry. The returned registry contains no templates, and has
176 /// [`format_unescaped`](fn.format_unescaped.html) registered as a formatter named "unescaped".
177 pub fn new() -> TinyTemplate<'template> {
178 let mut tt = TinyTemplate {
179 templates: HashMap::default(),
180 formatters: HashMap::default(),
181 default_formatter: &format,
182 };
183 tt.add_formatter("unescaped", format_unescaped);
184 tt
185 }
186
187 /// Parse and compile the given template, then register it under the given name.
188 pub fn add_template(&mut self, name: &'template str, text: &'template str) -> Result<()> {
189 let template = Template::compile(text)?;
190 self.templates.insert(name, template);
191 Ok(())
192 }
193
194 /// 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
195 pub fn set_default_formatter<F>(&mut self, formatter: &'template F)
196 where
197 F: 'static + Fn(&Value, &mut String) -> Result<()>,
198 {
199 self.default_formatter = formatter;
200 }
201
202 /// Register the given formatter function under the given name.
203 pub fn add_formatter<F>(&mut self, name: &'template str, formatter: F)
204 where
205 F: 'static + Fn(&Value, &mut String) -> Result<()>,
206 {
207 self.formatters.insert(name, Box::new(formatter));
208 }
209
210 /// Render the template with the given name using the given context object. The context
211 /// object must implement `serde::Serialize` as it will be converted to `serde_json::Value`.
212 pub fn render<C>(&self, template: &str, context: &C) -> Result<String>
213 where
214 C: Serialize,
215 {
216 let value = serde_json::to_value(context)?;
217 match self.templates.get(template) {
218 Some(tmpl) => tmpl.render(
219 &value,
220 &self.templates,
221 &self.formatters,
222 self.default_formatter,
223 ),
224 None => Err(Error::GenericError {
225 msg: format!("Unknown template '{}'", template),
226 }),
227 }
228 }
229}
230impl<'template> Default for TinyTemplate<'template> {
231 fn default() -> TinyTemplate<'template> {
232 TinyTemplate::new()
233 }
234}
235
236#[cfg(test)]
237mod test {
238 use super::*;
239
240 #[derive(Serialize)]
241 struct Context {
242 name: String,
243 }
244
245 static TEMPLATE: &'static str = "Hello {name}!";
246
247 #[test]
248 pub fn test_set_default_formatter() {
249 let mut tt = TinyTemplate::new();
250 tt.add_template("hello", TEMPLATE).unwrap();
251 tt.set_default_formatter(&format_unescaped);
252
253 let context = Context {
254 name: "<World>".to_string(),
255 };
256
257 let rendered = tt.render("hello", &context).unwrap();
258 assert_eq!(rendered, "Hello <World>!")
259 }
260}
261