Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Oxiplate: Template engine for Rust

This project is experimental and features described in this book may not yet be implemented, or may be implemented in a different way.

Oxiplate is a template engine that uses a derive macro to compile templates into Rust code for the best performance. It focuses on straightforward escaping and powerful whitespace control.

Getting started

Include Oxiplate in your project:

cargo add oxiplate

Create a couple templates in the /template directory:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ title }} - {{ site_name }}</title>
    </head>
    <body>
        <header>
            <h1>{{ site_name }}</h1>
        </header>
        <main>
            {% block content %}{% endblock %}
        </main>
    </body>
</html>
{% extends "layout.html.oxip" %}

{% block content %}
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
{% endblock %}

Build the template and output it:

use oxiplate::Oxiplate;

#[derive(Oxiplate)]
#[oxiplate = "index.html.oxip"]
struct Homepage {
    site_name: &'static str,
    title: &'static str,
    message: &'static str,
}

fn main() {
    let template = Homepage {
        site_name: "Oxiplate Documentation"
        title: "Oxiplate Example",
        message: "Hello world!",
    };

    print!("{}", template);
}

Which should output something like:

<!DOCTYPE html>
<html>
    <head>
        <title>Oxiplate Example - Oxiplate Documentation</title>
    </head>
    <body>
        <header>
            <h1>Oxiplate Documentation</h1>
        </header>
        <main>
    <h1>Oxiplate Example</h1>
    <p>Hello world!</p>
        </main>
    </body>
</html>

Template introduction

The syntax for Oxiplate templates is similar to many other systems, but the terminology may be slightly different. Over the next several chapters, you'll be introduced to the terminology Oxiplate uses and what functionality templates built with it supports.

Templates are made up of two types of data: static text and tags.

Tags

Tags start with { and end with } with one or more characters between to define the type of tag and any contained logic.

Writs

Writs are expressions wrapped with {{ and }} that will be evaluated and output into the template:

Hello {{ name }}!
Hello Luna!

Statements

Statements are wrapped with {% and %} and include variable assignments and control structures:

{% if user.is_some() %}<a href="/account/">Account</a>{% endif %}
<a href="/login/">Log In</a>

Comments

Comments are text wrapped with {# and #} that won't appear in the final template:

Hello world.{# Comments are ignored. #}
Hello world.

Whitespace control

All tags support the following whitespace control characters:

  • - (U+002D HYPHEN-MINUS) will remove all matched whitespace
  • _ (U+005F LOW LINE) will replace all matched whitespace with a single space (U+0020 SPACE)

To adjust whitespace before the tag, the whitespace control character must be added immediately following the opening {{, {%, or {#.

To adjust whitespace after the tag, the whitespace control character must be added immediately before the closing }}, %}, or #}.

If no whitespace control character is present, the matched whitespace will be left as-is.

For example, a {{- "b" _}} c would become ab c.

Short tags

There are also a couple short tags available for controlling whitespace elsewhere in templates:

  • {-} will remove all surrounding whitespace
  • {_} will replace all surrounding whitespace with a single space (U+0020 SPACE)

For example:

<p>{-}
    Hello {_}
    world! {-}
</p>

will become:

<p>Hello world!</p>

Writs

A writ is an expression wrapped with {{ and }} that will be evaluated and output into the template.

Hello {{ name }}!
Hello Luna!

But writs support any expression:

{{ a }} + {{ b }} = {{ a + b }}
1 + 2 = 3

No matter how complicated:

{{ (user.name() | upper) ~ " (" ~ (user.company() | lower) ~ ")" }}
CASPER (sloths and stuff, inc.)

Expressions will be explained in more detail in a later chapter.

Escaping

Eventually you'll likely want to escape user-provided text for safe usage within a markup language. Set a default escaper group and manually specify the escaper anywhere the default escaper for the group won't work:

escaper_groups.html.escaper = "::oxiplate::escapers::HtmlEscaper"
<a href="/{{ attr: user.username }}" title="Visit {{ attr: user.name }}'s profile">
    {{ user.name }}
</a>
<a href="/toya_the_sequoia" title="Visit Isabelle &#34;Cat &amp; Mouse&#34; Toya's profile">
    Isabelle "Cat &amp; Mouse" Toya
</a>

Read more about escaping in the next chapter.

Escaping

Escaping values ensures user-generated content can be safely used within trusted markup without causing unintended side-effects.

In Oxiplate, escapers are infallible; they must always successfully output a safe string for inclusion in the provided context. Sometimes this means all unacceptable character sequences will be escaped, while other times it could mean they are replaced or removed entirely. This makes escapers improper for contexts where doing so could change the correctness of the output, like a JSON object value where raw output in conjuction with known valid output is better.

An example

Hello {{ name }}!

HTML escaping is on by default for .html and .html.oxip files, so if a user provides this as their name in the example above:

<script>alert('oh no');</script>

It would be safely escaped (even if it may look pretty strange):

Hello &lt;script>alert('oh no');&lt;/script>!

You can use a different escape method whenever you want, like for HTML attributes:

<a href="/{{ attr: handle }}" title="{{ attr: name }}">{{ name }}</a>

If you need to skip escaping, you can do that:

<aside>{{ raw: your_html }}</aside>

And if you want to be explicit, {{ name }} and {{ text: name }} are equivalent.

Escaping templates without matching file extensions

Using Oxiplate to build inline templates, or templates that don't use file extensions that cleanly match up with escapers?

You can switch the fallback escaper for all of your templates:

fallback_escaper_group = "html"

Or switch it for the template you're in:

default_escaper_group is not yet implemented (#39).

{% default_escaper_group json %}
{
    "greeting": "Hello {{ name }}!",
}

Require specifying the escaper

Oxiplate can be configured to require all writs to specify which escaper to use, rather than falling back to the default escaper for the current escaper group:

require_specifying_escaper = true

Statements

Extends statements extend a template with the option to prepend, replace, append, or surround any block from the parent template.

If statements add branching to templates with if, else if, and else.

For statements bring iteration to templates with for and else.

Extending templates with extends and block

Start with a template that contains one or more blocks:

<!DOCTYPE html>
<main>
{%- block content %}
  <p>Parent content.</p>
{% endblock -%}
</main>

Then create a template to extend it:

{% extends "layout.html.oxip" %}

This is essentially the same as using layout.html.oxip directly:

<!DOCTYPE html>
<main>
  <p>Parent content.</p>
</main>

Adding to or replacing block content

Anything you add to a block of the same name in a child template will replace the content of the parent block:

  {% extends "layout.html.oxip" %}

+ {% block content %}
+   <p>Replaced content.</p>
+ {% endblock %}
  <!DOCTYPE html>
  <main>
-   <p>Parent content.</p>
+   <p>Replaced content.</p>
  </main>

The parent's content can be kept by using the {% parent %} tag in the block:

  {% extends "layout.html.oxip" %}+

+ {% block(surround) content %}
+   <p>Prefix.</p>
+   {% parent %}
+   <p>Suffix.</p>
+ {% endblock %}
  <!DOCTYPE html>
  <main>
+   <p>Prefix.</p>
    <p>Parent content.</p>
+   <p>Suffix.</p>
  </main>

Branching with if, else if, and else

#[derive(Oxiplate)]
#[oxiplate = "template.html.oxip"]
struct YourStruct {
    count: i64,
}
print!("{}", YourStruct {
    count: 19,
});
<p>
    {%- if count < 0 -%}
        {{ count }} is negative
    {%- elseif count > 0 -%}
        {{ count }} is positive
    {%- else -%}
        {{ count }} is zero
    {%- endif -%}
</p>
<p>19 is positive</p>

Iterating with for and else

<ul>
  {% for name in names %}
    <li>{{ name }}
  {% else %}
    <li><em>No names found</em>
  {% endfor %}
</ul>

Could produce something like:

<ul>
  <li>Jasmine
  <li>Malachi
  <li>Imogen
</ul>

Or if names was empty:

<ul>
  <li><em>No names found</em>
</ul>

Expressions

Expressions can be a lone literal like "A string." or a more complicated calculation or comparison. While expressions often evaluate to strings for output in writs, they can also be mathematical equations and comparisons for branching logic.

Literals

Oxiplate supports many of the same literals Rust itself does:

  • String (e.g., "This is a string.")
  • Boolean (i.e., true or false)
  • Integer (e.g., 19)

With improved support planned:

  • Float (#22; e.g., 1.9e1)
  • Binary (#19; e.g., 0b10011)
  • Octal (#20; e.g., 0o23)
  • Hexadecimal (#21; e.g., 0x13)
  • Underscore number separators (#18; e.g., 1_000_000)

Variables, fields, and functions

Variables cannot be named self or super.

Oxiplate accesses variables, fields, and functions similarly to Rust:

{{ foo }}
{{ foo.bar }}
{{ foo.hello() }}

All data available to templates is stored in the struct that referenced the template, or within the template itself. Local variables override those set for the template. Therefore, self. is neither needed nor allowed; it will be implied when a local variable of the same name doesn't exist.

Filters

Filters are not yet implemented (#26).

Filters modify expressions that precede them:

{{ "foo"|upper }}

FOO

Behind the scenes, filters are functions that are passed the result of the expression as the first argument. Additional arguments can be passed to the filter directly:

{{ "hello world"|replace("hello", "goodbye") }}

goodbye world

Operators

Unless otherwise specified, the operators behave the same as they do in Rust.

Math:

  • +
  • -
  • *
  • /
  • %

Comparison:

  • ==
  • !=
  • >
  • <
  • >=
  • <=

Logic:

  • ||
  • &&

Other:

  • ~: Concatenate the left and right sides into a single string.