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:

bash
cargo add oxiplate

Create a couple templates in the /template directory:

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

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

Build the template and output it:

/src/main.rs
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:

html
<!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:

oxip
Hello {{ name }}!
text
Hello Luna!

Statements

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

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

Comments

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

oxip
Hello world.{# Comments are ignored. #}
text
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:

html.oxip
<h1>
    {{- title _}}
    -
    {{_ site_name -}}
</h1>

will become:

html
<h1>Whitespace control - Oxiplate Documentation</h1>

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:

html.oxip
<p>{-}
    Hello {_}
    world! {-}
</p>

will become:

html
<p>Hello world!</p>

Writs

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

oxip
Hello {{ name }}!
text
Hello Luna!

But writs support any expression:

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

No matter how complicated:

oxip
{{ (user.name() | upper) ~ " (" ~ (user.company() | lower) ~ ")" }}
text
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:

/oxiplate.toml
escaper_groups.html.escaper = "::oxiplate::escapers::HtmlEscaper"
html.oxip
<a href="/{{ attr: user.username }}" title="Visit {{ attr: user.name }}'s profile">
    {{ user.name }}
</a>
html
<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

html.oxip
<p>Hello {{ name }}!</p>

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:

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

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

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

You can use a different escape method when appropriate, like for HTML attributes:

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

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

html.oxip
<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 specify the escaper from the attribute:

rust
#[derive(Oxiplate)]
#[oxiplate_inline(html: "{{ name }}")]
struct Data { name: &'static str }

You can also set a fallback escaper for any of your templates that don't specify an escaper group:

/oxiplate.toml
fallback_escaper_group = "html"

And finally, you can set the escaper group for the template you're in with the default escaper group statement.

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:

/oxiplate.toml
require_specifying_escaper = true

Statements

Default escaper statements sets or replaces the default escaper for a template.

{% default_escaper_group NAME %}
{% replace_escaper_group NAME %}

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

{% extends PATH %}
{% block NAME %}
    CONTENT
{% parent %}
    CONTENT
{% endblock %}

Include statements include the contents of a template built using the variables from the current scope.

{% include PATH %}

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

{% if [let PATTERN =] EXPRESSION %}
    CONTENT
{% elseif [let PATTERN =] EXPRESSION %}
    CONTENT
{% else %}
    CONTENT
{% endif %}

Match statements add pattern matching with match and case.

{% match EXPRESSION %}
{% case PATTERN %}
    ...
{% endmatch %}

For statements bring iteration to templates with for and else.

{% for PATTERN in EXPRESSION %}
    {% if EXPRESSION %}
        {% continue %}
    {% elseif EXPRESSION %}
        {% break %}
    {% endif %}

    CONTENT
{% endfor %}

Let statements assign the result of an expression to a variable.

{% let NAME = EXPRESSION %}

Setting the default escaper group from within a template

Normally, the default escaper group for a template is inferred from the file's extension.

For cases where the file extension doesn't correlate to the contents of the file (e.g., using .oxip instead of .html.oxip), the default group can be set within a template:

oxip
{% default_escaper_group html %}
<h1 title="{{ attr: title }}">{{ title }}</h1>

But when the escaper group is inferred incorrectly from the file extension (e.g., .tmpl is set to html by default), the default group has to be replaced instead:

html
{% replace_escaper_group json %}
{
    "greeting": "Hello {{ name }}!",
}

Extending templates with extends and block

Start with a template that contains one or more blocks:

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

Then create a template to extend it:

your-content.html.oxip
{% extends "layout.html.oxip" %}

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

html
<!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:

your-content.html.oxip
  {% extends "layout.html.oxip" %}

+ {% block content %}
+   <p>Replaced content.</p>
+ {% endblock %}
diff
  <!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:

your-content.html.oxip
  {% extends "layout.html.oxip" %}+

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

Including contents of a template with include

rust
#[derive(Oxiplate)]
#[oxiplate = "template.html.oxip"]
struct YourStruct {
    menu_links: [(&'static str, &'static str)],
    title: &'static str,
}
rust
print!("{}", YourStruct {
    menu_links: [
        ("/", "Home"),
        ("/about/", "About"),
    ],
    title: "Oxiplate",
});
template.html.oxip
<!DOCTYPE html>
<nav>{% include "menu.html.oxip" %}</nav>
<main>
    <h1>{{ title }}</h1>
    ...
</main>
menu.html.oxip
<ul>
    {%- for (href, text) in menu_links -%}
        <li><a href="{{ attr: link.href }}">{{ link.text }}</a>
    {%- endfor -%}
</ul>
html
<!DOCTYPE html>
<nav><ul><li><a href="/">Home</a><a href="/about/">About</a></ul></nav>
<main>
    <h1>Oxiplate</h1>
    ...
</main>

Branching with if, elseif, and else

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

if let and elseif let

Similarly to Rust, let can be used in if and elseif statements.

rust
#[derive(Oxiplate)]
#[oxiplate_inline(html: r#"
<p>
    {%- if let Some(count) = count -%}
        The count is {{ count }}.
    {%- else -%}
        No count provided.
    {%- endif -%}
</p>
"#)]
struct YourStruct {
    count: Option<i64>,
}

assert_eq!("<p>The count is 19.</p>", format!("{}", YourStruct {
    count: Some(19),
}));

Iterating with for and else

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

Could produce something like:

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

Or if names was empty:

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

Pattern matching with match and case

rust
#[derive(Oxiplate)]
#[oxiplate = "page.oxip"]
struct YourStruct {
    value: isize,
}
rust
print!("{}", YourStruct {
    value: -19
});
page.oxip
{% match value %}
{%- case ..0 -%}
    Less than zero
{%- case 0 -%}
    Zero
{%- case .. -%}
    Greater than zero
{%- endmatch %}
page.txt
Less than zero

Setting local variables with let

rust
#[derive(Oxiplate)]
#[oxiplate = "page.html.oxip"]
struct YourStruct {
    name: &'static str,
    company: &'static str,
}
rust
print!("{}", YourStruct {
    name: "Felix",
    company: "ABC Shipping",
});
page.html.oxip
{% let display_name = name ~ " (" ~ company ~ ")" -%}
<h1 title="{{ attr: display_name }}">{{ display_name }}</h1>
html
<h1 title="Felix (ABC Shipping)">Felix (ABC Shipping)</h1>

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)
  • Float (e.g., 1.9e1)
  • Binary (e.g., 0b10011)
  • Octal (e.g., 0o23)
  • Hexadecimal (e.g., 0x13)
  • Underscore number separators (e.g., 1_000_000)

Variables, fields, and functions

Variables cannot be named self, super, or oxiplate_formatter.

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

oxip
{{ 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 modify expressions that precede them:

oxip
{{ "foo" | upper }}

FOO

Behind the scenes, filters are functions in the filters_for_oxiplate module at the root of your crate that are passed the result of the expression as the first argument. Additional arguments can be passed to the filter directly:

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

goodbye world

Cow prefix for more efficient string conversion

Expressions and filters can be prefixed with the cow prefix (>) to convert string-like values into ::oxiplate_traits::CowStr which filters can use to retrieve the generated Cow<str> via CowStr::cow_str(). This conversion happens more efficiently than using Display and the cow prefix helps template writers avoid fragile boilerplate.

oxip
{{ >"hello world" | >replace(>19, >89) | shorten(19) }}

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.