24 days of Rust - tera

tera is a pure Rust templating engine. Those familiar with Django, Jinja2 or Twig will feel right at home with tera, as its syntax is very similar. tera is still very young and may not be as feature complete as, say, Jinja2. However it is actively developed and the maintainer is very open to new contributions, so feel free to tackle some issues!

Basic templates

Let's use tera to render a simple HTML template.

Note: The templates are parsed when Tera::new() is called, so if any of the templates contains invalid syntax, the function will panic right there. Keep that in mind!

extern crate tera;

use tera::{Context, Tera};

const LIPSUM: &'static str =
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut \
     labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco \
     laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in \
     voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat \
     cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";

fn main() {
    let tera = Tera::new("templates/**/*");
    let mut ctx = Context::new();
    ctx.add("title", &"hello world!");
    ctx.add("content", &LIPSUM);
    ctx.add("todos",
            &vec!["buy milk", "walk the dog", "write about tera"]);
    let rendered = tera.render("index.html", ctx).expect("Failed to render template");
    println!("{}", rendered);
}

We pass variables from Rust code to the template using a Context struct. The add() method takes variable name and a reference to the value. Values can be basic Rust types, vectors or custom structs (provided that they implement the Serialize trait from serde.)

Our index.html looks like this:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>{{ title|title }}</title>
</head>
<body>
<h1>{{ title|title }}</h1>
<h2>{{ content|wordcount }} words</h2>
<p>{{ content }}</p>
{% if todos|length > 1 %}
    <ul>
    {% for todo in todos %}
        <li>{{ todo }}</li>
    {% endfor %}
    </ul>
{% endif %}
</body>
</html>

We use {{ ... }} in the templates to refer to variables, while {% ... %} syntax is for flow control tags such as loops and blocks. The excellent Django docs describe this syntax in more detail. tera templates follow the same conventions.

$ cargo run
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<h2>69 words</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum</p>
<ul>
    <li>buy milk</li>
    <li>walk the dog</li>
    <li>write about tera</li>
    </ul>
</body>
</html>

Custom template filters

Template filters are expressions that affect how variables are displayed. We can apply them using the pipe character | after variable name, for example {{ content|lower }}. tera provides a selection of built-in template filters, such as trim, wordcount, pluralize or reverse. But the power of template engines lies in their extensibility. We can add our own template filters to cover the specific needs of our application. (Or to make life easier for frontend developers on the team.)

A template filter is just a Rust function that has the following type:

pub type FilterFn = fn(Value, HashMap<String, Value>) -> TeraResult<Value>;

Let's use the markdown crate to render Markdown to HTML on the fly.

extern crate markdown;
extern crate tera;

use std::collections::HashMap;
use tera::{Context, Tera, TeraResult, Value, to_value};

pub fn markdown_filter(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
    let s = try_get_value!("markdown", "value", String, value);
    Ok(to_value(markdown::to_html(s.as_str())))
}

We're using here a try_get_value! macro, which isn't currently a part of tera's public API (because value serializing may change; at the moment tera is using serde_json). I've copied it from tera sources but decided to omit its definition from the example for clarity. We try to read our input as a String and if that succeeds, we pass the string slice to markdown::to_html(). The rest is just bookkeeping - convert the rendered HTML back to a Value and wrap in Ok. Let's see it in action:

tera.register_filter("markdown", markdown_filter);
let mut ctx = Context::new();
ctx.add("content", &"**bold** and `beautiful`");
let rendered = tera.render("blog.html", ctx).expect("Failed to render template");
println!("{}", rendered);

And the template:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    {{ content|markdown|safe }}
</body>
</html>

We can chain filters one after another. Here we're using the built-in safe filter to disable auto-escaping of the generated HTML.

$ cargo run
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <p><strong>bold</strong> and <code>beautiful</code></p>

</body>
</html>

Not only HTML

The Django template docs contain a following paragraph (near the beginning, so that's important for them):

Why use a text-based template instead of an XML-based one (like Zope’s TAL)? We wanted Django’s template language to be usable for more than just XML/HTML templates. At World Online, we use it for emails, JavaScript and CSV. You can use the template language for any text-based format.

Oh, and one more thing: making humans edit XML is sadistic!

tera shares the same philosophy. We don't have to emit HTML and HTML only. In the following example we're rendering a map of configuration values into an .ini file:

let mut config = HashMap::new();
config.insert("hostname", "127.0.0.1");
config.insert("user", "root");
config.insert("email", "NAME@example.com");
let mut ctx = Context::new();
ctx.add("config", &config);
let rendered = tera.render("config.ini", ctx).expect("Failed to render template");
println!("{}", rendered);

config.ini:

[system]
{% if config.user != "anonymous" %}
user={{ config.user }}
{% endif %}

[network]
hostname={{ config.hostname }}
email={{ config.email|replace(from="NAME", to=config.user) }}

We can access HashMap members in the template using dot notation, as in config.user. This example also demonstrates that filters can take extra arguments, like replace.

And the result:

$ cargo run
[system]
user=root

[network]
hostname=127.0.0.1
email=root@example.com

Further reading


Photo by Moyan Brenn and shared under the Creative Commons Attribution 2.0 Generic License. See https://www.flickr.com/photos/aigle_dore/21882255758/