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 
65 extern crate serde;
66 extern crate serde_json;
67 
68 #[cfg(test)]
69 #[cfg_attr(test, macro_use)]
70 extern crate serde_derive;
71 
72 mod compiler;
73 pub mod error;
74 mod instruction;
75 pub mod syntax;
76 mod template;
77 
78 use error::*;
79 use serde::Serialize;
80 use serde_json::Value;
81 use std::collections::HashMap;
82 use std::fmt::Write;
83 use template::Template;
84 
85 /// Type alias for closures which can be used as value formatters.
86 pub type ValueFormatter = dyn Fn(&Value, &mut String) -> Result<()>;
87 
88 /// Appends `value` to `output`, performing HTML-escaping in the process.
escape(value: &str, output: &mut String)89 pub 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                     '>' => "&gt;",
99                     '<' => "&lt;",
100                     '&' => "&amp;",
101                     '\'' => "&#39;",
102                     '"' => "&quot;",
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.
format(value: &Value, output: &mut String) -> Result<()>127 pub 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.
format_unescaped(value: &Value, output: &mut String) -> Result<()>147 pub 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.
169 pub struct TinyTemplate<'template> {
170     templates: HashMap<&'template str, Template<'template>>,
171     formatters: HashMap<&'template str, Box<ValueFormatter>>,
172     default_formatter: &'template ValueFormatter,
173 }
174 impl<'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".
new() -> TinyTemplate<'template>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.
add_template(&mut self, name: &'template str, text: &'template str) -> Result<()>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
set_default_formatter<F>(&mut self, formatter: &'template F) where F: 'static + Fn(&Value, &mut String) -> Result<()>,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.
add_formatter<F>(&mut self, name: &'template str, formatter: F) where F: 'static + Fn(&Value, &mut String) -> Result<()>,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`.
render<C>(&self, template: &str, context: &C) -> Result<String> where C: Serialize,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 }
230 impl<'template> Default for TinyTemplate<'template> {
default() -> TinyTemplate<'template>231     fn default() -> TinyTemplate<'template> {
232         TinyTemplate::new()
233     }
234 }
235 
236 #[cfg(test)]
237 mod 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]
test_set_default_formatter()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