tinytemplate_async/
lib.rs

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_async;
35//!
36//! use tinytemplate_async::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<dyn Error>> {
47//!     let mut tt = TinyTemplate::new();
48//!     tt.add_template("hello".to_string(), TEMPLATE.to_string())?;
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 std::sync::Arc;
84use template::Template;
85
86/// Type alias for closures which can be used as value formatters.
87pub type ValueFormatter = dyn Fn(&Value, &mut String) -> Result<()> + Send + Sync;
88
89/// Appends `value` to `output`, performing HTML-escaping in the process.
90pub fn escape(value: &str, output: &mut String) {
91    // Algorithm taken from the rustdoc source code.
92    let value_str = value;
93    let mut last_emitted = 0;
94    for (i, ch) in value.bytes().enumerate() {
95        match ch as char {
96            '<' | '>' | '&' | '\'' | '"' => {
97                output.push_str(&value_str[last_emitted..i]);
98                let s = match ch as char {
99                    '>' => "&gt;",
100                    '<' => "&lt;",
101                    '&' => "&amp;",
102                    '\'' => "&#39;",
103                    '"' => "&quot;",
104                    _ => unreachable!(),
105                };
106                output.push_str(s);
107                last_emitted = i + 1;
108            }
109            _ => {}
110        }
111    }
112
113    if last_emitted < value_str.len() {
114        output.push_str(&value_str[last_emitted..]);
115    }
116}
117
118/// The format function is used as the default value formatter for all values unless the user
119/// specifies another. It is provided publicly so that it can be called as part of custom formatters.
120/// Values are formatted as follows:
121///
122/// * `Value::Null` => the empty string
123/// * `Value::Bool` => true|false
124/// * `Value::Number` => the number, as formatted by `serde_json`.
125/// * `Value::String` => the string, HTML-escaped
126///
127/// Arrays and objects are not formatted, and attempting to do so will result in a rendering error.
128pub fn format(value: &Value, output: &mut String) -> Result<()> {
129    match value {
130        Value::Null => Ok(()),
131        Value::Bool(b) => {
132            write!(output, "{}", b)?;
133            Ok(())
134        }
135        Value::Number(n) => {
136            write!(output, "{}", n)?;
137            Ok(())
138        }
139        Value::String(s) => {
140            escape(s, output);
141            Ok(())
142        }
143        _ => Err(unprintable_error()),
144    }
145}
146
147/// Identical to [`format`](fn.format.html) except that this does not perform HTML escaping.
148pub fn format_unescaped(value: &Value, output: &mut String) -> Result<()> {
149    match value {
150        Value::Null => Ok(()),
151        Value::Bool(b) => {
152            write!(output, "{}", b)?;
153            Ok(())
154        }
155        Value::Number(n) => {
156            write!(output, "{}", n)?;
157            Ok(())
158        }
159        Value::String(s) => {
160            output.push_str(s);
161            Ok(())
162        }
163        _ => Err(unprintable_error()),
164    }
165}
166
167/// The TinyTemplate struct is the entry point for the TinyTemplate library. It contains the
168/// template and formatter registries and provides functions to render templates as well as to
169/// register templates and formatters.
170pub struct TinyTemplate {
171    templates: HashMap<String, Template>,
172    formatters: HashMap<String, Box<ValueFormatter>>,
173    default_formatter: Arc<ValueFormatter>,
174}
175impl TinyTemplate {
176    /// Create a new TinyTemplate registry. The returned registry contains no templates, and has
177    /// [`format_unescaped`](fn.format_unescaped.html) registered as a formatter named "unescaped".
178    pub fn new() -> TinyTemplate {
179        let mut tt = TinyTemplate {
180            templates: HashMap::default(),
181            formatters: HashMap::default(),
182            default_formatter: Arc::new(format),
183        };
184        tt.add_formatter("unescaped".to_string(), format_unescaped);
185        tt
186    }
187
188    /// Parse and compile the given template, then register it under the given name.
189    pub fn add_template(&mut self, name: String, text: String) -> Result<()> {
190        let template = Template::compile(text)?;
191        self.templates.insert(name, template);
192        Ok(())
193    }
194
195    /// 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
196    pub fn set_default_formatter<F>(&mut self, formatter: F)
197    where
198        F: Fn(&Value, &mut String) -> Result<()> + 'static + Send + Sync,
199    {
200        self.default_formatter = Arc::from(formatter);
201    }
202
203    /// Register the given formatter function under the given name.
204    pub fn add_formatter<F>(&mut self, name: String, formatter: F)
205    where
206        F: 'static + Fn(&Value, &mut String) -> Result<()> + Send + Sync,
207    {
208        self.formatters.insert(name, Box::new(formatter));
209    }
210
211    /// Render the template with the given name using the given context object. The context
212    /// object must implement `serde::Serialize` as it will be converted to `serde_json::Value`.
213    pub fn render<C>(&self, template: &str, context: &C) -> Result<String>
214    where
215        C: Serialize,
216    {
217        let value = serde_json::to_value(context)?;
218        match self.templates.get(template) {
219            Some(tmpl) => tmpl.render(
220                &value,
221                &self.templates,
222                &self.formatters,
223                self.default_formatter.clone(),
224            ),
225            None => Err(Error::GenericError {
226                msg: format!("Unknown template '{}'", template),
227            }),
228        }
229    }
230}
231impl Default for TinyTemplate {
232    fn default() -> TinyTemplate {
233        TinyTemplate::new()
234    }
235}
236
237#[cfg(test)]
238mod test {
239    use super::*;
240
241    #[derive(Serialize)]
242    struct Context {
243        name: String,
244    }
245
246    static TEMPLATE: &'static str = "Hello {name}!";
247
248    #[test]
249    pub fn test_set_default_formatter() {
250        let mut tt = TinyTemplate::new();
251        tt.add_template("hello".to_string(), TEMPLATE.to_string())
252            .unwrap();
253        tt.set_default_formatter(&format_unescaped);
254
255        let context = Context {
256            name: "<World>".to_string(),
257        };
258
259        let rendered = tt.render("hello", &context).unwrap();
260        assert_eq!(rendered, "Hello <World>!")
261    }
262}