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 "Cat & Mouse" Toya's profile">
Isabelle "Cat & 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 <script>alert('oh no');</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
orfalse
) - 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.