I've refactored this blog and some other web apps in Rust that I maintain from Jinja templates to a type-safe Rust macro.
To illustrate the difference, let's start with a concrete Jinja template example:
<style>
.comment {
border-left: thin var(--secondary-bg) solid;
padding-left: 10px;
}
</style>
<div class="comment">
<p>
<strong>{{ name }}</strong>
{% if site %}
(<a href="https://{{ site }}" target="_blank">{{ site }}</a>)
{% endif %}
</p>
{{ html_content | safe }}
</div>
This Jinja template uses variable replacement with {{ ... }}, control flow with {% if ... %}...{% endif %} and some
kind of functional transformation (called "filters") with ... | ....
It works and the Rust crate minijinja is pretty good. However, as it's common in template languages, the type system is weak and doesn't help catch common mistakes at all! To know which variables are used you have to carefully read the code. Since the IDE cannot understand the usage of struct fields, actions like "show usages" do not list them.
This makes evolving and refactoring more cumbersome, and specially awkward in a Rust codebase, where the rest of the code is well-typed.
For a while I had in mind the idea to migrate to a typed html macro in Rust. I've compared some options and went with maud. Check below the above example rewritten:
use maud::{html, Markup, PreEscaped};
fn render(name: &str, site: Option<&str>, html_content: &str) -> Markup {
html! {
style {
(PreEscaped(r#"
.comment {
border-left: thin var(--secondary-bg) solid;
padding-left: 10px;
}"#))
}
div.comment {
p {
strong { (name) }
@if let Some(site) = site {
" ("
a href={ "https://" (site) } target="_blank" {
(site)
}
")"
}
}
(PreEscaped(html_content))
}
}
}
It may look funny at first, but the macro seems pretty readable to me now. The docs are well written with many examples and the language is quite small. A crash course from Jinja's syntax:
- instead of
<div class="hey">...</div>usediv.hey { ... } - instead of
{{ ... }}use(...). The parenthesis is a Rust expression, and the IDE understands it well! - instead of
{% if ... %}...{% endif %]use@if ... { ... }. Again, Rust can be used, so if-let can be used to handle optional data, for example - instead of filters, just use Rust expressions. For example, the type
PreEscaped(...)disables HTML-escaping for the given data
The compiler runs some maud code that parses and converts each html! { ... } call directly to Rust code, just as if
you had written string manipulation code by hand. The example above will be translated to:
fn render(name: &str, site: Option<&str>, html_content: &str) -> Markup {
let mut out = String::with_capacity(923usize);
out.push_str("<style>");
render_to!(&PreEscaped(".comment {
border-left: thin var(--secondary-bg) solid;
padding-left: 10px;
}"), &mut out);
out.push_str("</style><div class=\"comment\"><p><strong>");
render_to!(&name, &mut out);
out.push_str("</strong>");
if let Some(site) = site {
out.push_str(" (<a href=\"https://");
render_to!(&site, &mut out);
out.push_str("\" target=\"_blank\">");
render_to!(&site, &mut out);
out.push_str("</a>)");
}
out.push_str("</p>");
render_to!(&PreEscaped(html_content), &mut out);
out.push_str("</div>");
Markup(out)
}
Quite direct and powerful! After some days of using it, I can report on some downsides:
- editing CSS and JS inside a string it not fun. I try to keep them small (which is a good thing!). If I need more
"space" I simply do
style { (PreEscaped(include_str!("file.css"))) } - with Jinja, the file is mostly HTML and the IDE has good support for it. Thinks like auto-complete, MDN docs, go to CSS selector, etc do not work with maud
Initially, I thought that it would be slow to compile, but so far I could not notice it. For my taste, the trade-offs are worth it.
For fun, I've added to the footer of this blog how long the server took to render each page. Currently, it's usually around 0.1 ms (that is, 10 000 faster than a second). Check for you self in the footer of this page :)
I've compared the median time of rendering the home page of this blog in minijinja against maud over 5 runs:
minijinja: 1.0 msmaud: 0.07 ms
fast!