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:

<!-- /template/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>
<!-- /template/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:

<!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.)

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

HTML escaping is on by default, 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 for other formats

Using Oxiplate to build TOML, JSON, XML, RTF, or [insert format here] files?

You can switch the default escaper for all of your files:

# /oxiplate.toml
default_escaper_group = "html"

Or switch it just for the document you're in:

unimplemented!("Syntax not yet implemented and subject to change!")
{% default_escaper_group json %}
{
    "name": "{{ name }}",
    "age": {{ number: age }},
}

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:

{# 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:

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

Replace parent contents

You can choose to replace the contents of the parent block with a block with the same name:

  {# your-content.html.oxip -#}

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

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

For the same effect, you can be explicit with extends(replace):

  {# your-content.html.oxip -#}

- {% extends "layout.html.oxip" %}
+ {% extends(replace) "layout.html.oxip" %}

  {% block content %}
    <p>Replaced content.</p>
  {% endblock %}

Prefix parent contents

To prefix the contents of the parent, you can use extends(prefix):

  {# your-content.html.oxip -#}

- {% extends "layout.html.oxip" %}
+ {% extends(prefix) "layout.html.oxip" %}
+
+ {% block content %}
+   <p>Prefix.</p>
+ {% endblock %}
  <!DOCTYPE html>
  <main>
+   <p>Prefix.</p>
    <p>Parent content.</p>
  </main>

Suffix parent contents

To suffix the contents of the parent, you can use extends(suffix):

  {# your-content.html.oxip -#}

- {% extends "layout.html.oxip" %}
+ {% extends(suffix) "layout.html.oxip" %}
+
+ {% block content %}
+   <p>Suffix.</p>
+ {% endblock %}
  <!DOCTYPE html>
  <main>
    <p>Parent content.</p>
+   <p>Suffix.</p>
  </main>

Surround parent contents

To surround the contents of the parent, you can use extends(surround) and {% parent %}:

  {# your-content.html.oxip -#}

- {% extends "layout.html.oxip" %}
+ {% extends(surround) "layout.html.oxip" %}
+
+ {% block 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

TODO