Stilts
Stilts is a templating engine designed for the rust programming language that makes use of rust's compile time to keep templates safe and correct.
About This "Book"
This is a manual for using the Stilts templating engine in code. It will cover all the details necessary for a programmer to use and even abuse the engine to accomplish their goals. This documentation will not cover certain aspects of related materials when it would detract from the explanation of a Stilts concept. It will however link to reading on said related materials whenever possible. Ideally this document can be understood by anybody even people who fall outside the target audience.
Target Audience
- A Rust programmer.
- If not you'll want to have rust book as a reference.
- Needs a Template Engine usually to write HTML for a website, but Stilts works in other use cases as well.
Use Cases
Stilts is primarily designed for templating HTML in web projects and has default settings configured to cater to that use case. However, it certainly can be used and modified to serve any kind of templating purpose. As with any tool or library the design decisions made in Stilts require making certain tradeoffs.
Stilts is designed for use in systems where templates are tightly coupled with code. The most common case is web design, but there are other cases where this tight coupling can be useful. For instance a project which generates some generic code based on a few parameters. If the code generated follows a specific rigid structure and is tweaked by some inputs, then Stilts can work well for that.
Benefits
- Compile time guarantees
- Stilts generates rust code based off of the templates you write which is then run through the rust compiler meaning you maintain all the guarantees provided by the rust compiler.
- Pure rust inside templates
- Stilts is focused on making development in rust as simple and flexible as possible. Therefore, you are able to write arbitrary rust code anywhere inside your templates.
- Performant render times
- This, while not a primary focus of the Stilts engine, is a nice benefit you get when most of the work is done at compile time see performance.
Drawbacks
- No creation of templates on the fly at runtime
- This can be a big downside for many potential use cases. Many tools need to apply template rules to arbitrary text that is recieved at runtime, which Stilts simply cannot do due to its very nature.
- Longer iteration times
- Iterating on your design is important especially when working with UI/UX, so impairing iteration times can be a big problem for some people. Stilts impairs iteration times by forcing your entire application to recompile when minor template changes need to be made. It is however possible to reduce iterating friction.
- No Cross Language support
- Stilts is as rust first and only system. Similar projects could be made for other languages, but they would not follow the syntax or rules that rust enforces. As such if you are looking for a templating engine that can be used across multiple programming languages Stilts cannot fill that role. Look at something like Jinja2 which has implementations in many languages with consistent syntax.
Stilts cannot perform runtime template creation / parsing, if you need that you should look for some other engines here. Some notable runtime engines would include Tera, Handlebars, Liquid, and Minijinja.
Important Mentions
The Stilts templating engine takes major inspiration from Askama. Askama is a more mature library with more history and support backing it, which Stilts does not have. However, Stilts provides features that I believe are worth the change.
It took a lot of research on procedural macros to figure out how to get this to work so big thanks to these resources.
Getting Started
This section will cover instructions for using Stilts as your templating engine. These instructions are intended to provide more of an introduction into usage of Stilts, rather than a strict set of rules which must be followed. When followed these steps will lead to working code, but it is also important to play around and discover. One of the best learning tools is discovery.
How to Create Templates
Requirements:
- Rust Installed
This includes access to the following commands:
- cargo
- A text editor
- (Optional) One that can be specialized for coding rust
- Access to a command prompt or terminal emulator
- On Windows the default command prompt or powershell will work fine
- On Linux most distributions provide a default terminal emulator
- On macOS the terminal app will work
Instructions:
-
Create a new rust project. Depending on what kinds of tools you have installed there are a few ways to create a new rust project, the most common is by using cargo.
To create a project with cargo open your terminal emulator Using the
cargo
tool create a new project for these instructions it will be calledhellostilts
. Run the following commands to create the project and enter the project directory.cargo new hellostilts cd hellostilts
This will create a new directory named
hellostilts
with contents that look like this:📁 hellostilts/ ├── 📁 src/ │ └── main.rs └── Cargo.toml
-
Add Stilts as a Dependency. In order to make use out of Stilts you'll need to add it to your project dependencies. The simplest method is to once again use
cargo
.cargo add stilts
-
Create a Template Directory. Inside the new project create a directory named
templates
, this is where our future template code will be created. You can do this via a file explorer or using the command:mkdir templates
After creating the folder your project folder should look like this:
📁 hellostilts/ ├── 📁 src/ │ └── main.rs ├── 📁 templates/ └── Cargo.toml
-
Write the Template Code. Inside the newly created
templates
folder create and edit your first template file. It can be named anything but for these instructions it will be calledindex.html
Write something like this into the file:<ul> {% for name in names %} <li>Hello {% name %}!</li> {% end %} </ul>
Finally, your project directory should look like this which is all the required files:
📁 hellostilts/ ├── 📁 src/ │ └── main.rs ├── 📁 templates/ │ └── index.html └── Cargo.toml
-
Write the Rust Code. Now you have created a template that can be understood by the Stilts engine. Next it just has to be used in code. In the
src/main.rs
file that was made when your project was created, write the following:use stilts::Template; #[derive(Template)] #[stilts(path = "index.html")] struct Index<'s> { names: Vec<&'s str> } fn main() { let template = Index { names: vec![ "Jack", "Grant", "Amber", "Alex" ], }; println!("{}", template.render().unwrap()); }
-
Run The Program. You have almost rendered a template! The final step is to compile and run the program. Thanks to
cargo
it is a simple single step!cargo run
Now the output of that program should look a little something like:
<ul> <li>Hello Jack!</li> <li>Hello Grant!</li> <li>Hello Amber!</li> <li>Hello Alex!</li> </ul>
The Template Macro
In previous instructions the rust code made use of the template derive macro.
Invoking this macro is how our template code gets compiled into our rust code.
This macro performs a few different things depending on what options are provided,
however the core function is converting template code into a Template
trait implementation.
In the instructions the path
argument was used to load a template from a file.
This is the most common way of defining templates. Arguments are provided using
the same macro syntax and the stilts
prefix followed by the args to provide.
#[derive(Template)] // Use the derive macro
#[stilts(path = "index.html")] // Provides arguments to the derive macro
struct Example; // The item which the trait is implemented on
Macro Arguments
The template derive macro has multiple arguments which can be used to tweak how the macro generates the template code. Some arguments are used to override behavior described in the configuration section.
Either path or content must be specified
- path: The path relative to the template root of the template to render
- content: The direct contents of the template provided by a string literal
- escape: Override the escaper detected by file extension with a specified one
- trim: Override the trim behavior defined in your config
- block: Only render the contents of a specific block within the template specified by path or content
Examples:
Standard use case
#[derive(Template)]
#[stilts(path = "index.html")]
struct MyTemplate {
my_data: String,
}
Using content instead of path
use stilts::Template;
#[derive(Template)]
#[stilts(content = "My {% data %} Template")]
struct MyInlineTemplate {
data: String,
}
An example of setting the trim and escape to something else. This forces
Stilts to not trim whitespace around expressions, and to use the Empty
escaper which does no escaping at all.
use stilts::Template;
#[derive(Template)]
#[stilts(
content = "Templates are fun",
trim = false,
escape = ::stilts::escaping::Empty
)]
struct MyOverridenTemplate {
my_data: String,
}
Only rendering a single block
#[derive(Template)]
#[stilts(path = "index.html", block = "popup")]
struct MyTemplate {
my_data: String,
}
Stilts Language
Stilts is a templating engine and as such supports many features that popular template engines support. Here is where each feature will be outlined and explained in detail with examples.
Vocabulary
To render something in the context of a template is to write it into the data that is output by the engine that is the completed template and provided data.
A template is made up of two components content and expressions. Expressions are further divided into multiple categories but they either control the logic of the template rendering or are replaced by values while rendering. The content of a template is the static text that is manipulated by the engine as defined by the expressions within that template.
All expressions in Stilts are directly comparable to their rust counterparts. This allows nearly unlimited freedom in how users can manipulate their templates.
In Stilts an expression is either a single piece of code surrounded by the delimiters {%
and %}
e.g.
{% include "other.html" %}
Or it is a multi-expression block which has an opening and ending expression
{% if show_this %}
<a></a>
{% end %}
Any item that requires an {% end %}
expression will be referred to as a block in Stilts.
Expression Table
Here is a quick reference to the different expressions Stilts has, if this is your first reading you should just continue reading and not skip ahead, this is provided as a convenience.
Rust Expressions
Display
A display expression is one that has already been shown earlier in the book and is the simplest of all the expressions. It instructs the engine to write some variable data into the template at a specific point.
One of the most simple uses is to write the contents of a variable into the template.
<span>{% my_variable %}</span>
It actually allows any arbitrary rust expression inside the delimiters. The previous link leads to a technical definition of rust expressions if you need more of a primer on how they are used see the book. A rust expression is a separate concept from a Stilts expression. A Stilts expression only exists within the context of a template, and must be surrounded by the stilts delimiters.
For a display expression to be valid the rust expression within the delimiters must evaluate to a type which implements
Display
. That is however the only limitation, there is
absolutely no limitation on syntax. For example inside the delimiters here is a fairly complex rust.
<script>
let data = {% my_data.iter().filter(|x| x.allowed).collect::<Vec<_>>().json() %};
</script>
As a general rule code between the delimiters is not required to be on a single line, so the previous example could just as easily be formatted as follows.
<script>
let data = {% my_data.iter()
.filter(|x| x.allowed)
.collect::<Vec<_>>()
.json() %};
</script>
There is no rule on how to properly format template code, so that comes down to aesthetic preference.
The other thing these examples show off is the json
function, this is one of a few convenience
functions that Stilts provides via "Extension Traits".
Statement
A statement expression is very similar to a display expression except that it does not
render anything to the template. It is rust code that gets run at that point during template
rendering but does not insert anything into the template. The way this works is by using
a rust statement to distinguish whether
there is a value to be rendered or not.
In rust expressions must always produce a value, statements however produce no values.
This mechanism should be familiar to most rust programmers, as it is how return
can be omitted
at the end of functions by just ending the function with an expression.
For example by simply adding a semicolon to the previous display expression it becomes a statement. Doing this causes the value to not be rendered to the output.
{% my_data.iter()
.filter(|x| x.allowed)
.collect::<Vec<_>>(); %}
Why would you want to write template expressions that neither render a value nor affect the render logic? Well the answer is variable declaration/modification and "side effects". If you need to introduce a variable for any reason you can do so using a statement. As for side effects those are probably more rare than variable, but if some action needs to be performed without altering the template then use a statement.
In the following example we declare a mutable variable data using a statement, then remove an element from the array without affecting the template by using another statement.
{% let mut data = my_data.iter()
.filter(|x| x.allowed)
.collect::<Vec<_>>(); %}
<div>Some templatate content</div>
{% data.pop(); %}
<a>{% data.pop().unwrap().name %}</a>
Control Expressions
Control expressions are those which can control the logic of how content is displayed within a template. For instance conditionally rendering a section of a template depending on the value of a variable, or rendering a section multiple times based on a list of items. For most control expressions they share (structure/properties/token stream) with their rust counterpart.
If
A Stilts if block can be used to change what parts of a template are rendered based on some value.
For example, we can render a link only if some data is present to display the link.
{% if data.is_some() %}
<a href="{% data.unwrap().href %}">{% data.unwrap().name %}</a>
{% end %}
Now depending on whether data.is_some()
is true
or false
the template
will either render the stuff inside the if block or not.
But having to unwrap multiple times is cumbersome, thankfully rust provides the if let convention for that. Stilts if blocks are basically equivalent to standard rust if statements, so any valid rust is valid in Stilts.
{% if let Some(value) = data %}
<a href="{% value.href %}">{% value.name %}</a>
{% end %}
Often it is useful to render something for multiple different cases for this you can use else if
and else
.
{% if let Some(value) = data %}
<a href="{% value.href %}">{% value.name %}</a>
{% else if let Some(value) = other %}
<button onclick="{% value.clicked %}">{% value.name %}</button>
{% else %}
<span>No Data</span>
{% end %}
Match
A match block is used in much the same way as an if block, but match blocks can be used to pattern match. The match block is functionally equivalent to a rust match.
{% match data %}
{% when Some(value) if !value.is_empty() %}
<a href="{% value.href %}">{% value.name %}</a>
{% when Some(value) %}
<button onclick="noValue">{% value.name %}</button>
{% when None %}
<span>No Data</span>
{% end %}
Just like their rust counterparts Stilts matches are exhaustive, meaning that all possible cases must be covered by the match arms. If you need, you can use the wildcard catch-all to provide a "default" case.
{% match data %}
{% when Some() if !value.is_empty() %}
<a href="{% value.href %}">{% value.name %}</a>
{% when _ %}
{% end %}
For
The for block is an expression which is used to repeat parts of a template multiple times. Again like the above blocks it is the same as the rust equivalent for loop.
This will loop over the items in a collection and render the contents for each item in the collection.
<table>
{% for row in table %}
<tr>
{% for col in row %}
<td>{% col %}</td>
{% end %}
</tr>
{% end %}
</table>
Macro
The macro block defines a section of template code that can be manually called to render in multiple locations and with different arguments. This is most useful for reducing code duplication within a template.
Macros are not similar to rust macros, instead they are more like functions which can take args and will always output template code.
This is a simple example where we have to do the same thing twice but with two different sets of data, and in two different locations within the template. We could write the whole for loop twice, or we could use a macro!
{% macro list_users(users: &[User]) %}
<ul>
{% for user in users %}
<li>
{% user.name %}
</li>
{% end %}
</ul>
{% end %}
<div class="active">
{% call list_users(active_users) %}
</div>
<div class="inactive">
{% call list_users(inactive_users) %}
</div>
Inheritance Expressions
Many good templating engines have some form of inheritance. Or a method by which templates can be built upon one another. Thus increasing reusability, and reducing repetition. Stilts implements a system based upon the template engines that inspired it.
Extends
The extends expression informs the Stilts engine that the current template extends the functionality of another template. The current template becomes a "child" of the template in the extends expression. A child template is any template which invokes the extends expression in order to inherit from a "parent" template. A parent template is any template which has child templates that extend from it. A template can be at once a parent and a child template if both those conditions are met.
The extending of functionality in the simplest case means that the content of the current template is added to the end of the parent template and that is how it gets rendered. It does however get more complicated with block expressions.
base.html
Hello from the parent!
child.html
{% extends "base.html" %}
Hello from the child!
Output
This is what the output of rendering the child template would look like
Hello from the parent!
Hello from the child!
Block
Distinct from the concept of block expressions a stilts inheritance block is used to define secions of templates which can both be overriden by a potential child template and overrides a potential parent.
The same expression is used in parent and child templates to perform their related tasks. On the parent defining a block means providing a section that any child templates can override, while in the child template defining a block means overridding the block that is defined by the parent.
A block works in conjunction with the extends expressions to provide an inheritance structure to reduce template code duplication. This is best accomplished by writing most boilerplate into a base template that other child templates are able to extend and overwrite pieces of to create their own functionality.
A parent/base template defines as many blocks as it wants wherever it wants. It can even put code into those blocks to provide default data in case a child template does not override the block.
base.html
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<title>This is extensible!</title>
{% end %}
</head>
<body>
{% block body %}
{% end %}
</body>
</html>
The child template when defining the same blocks is now overriding the blocks as defined by the parent template. This means the code inside the child blocks is essentially injected into the parent at the block definition.
child.html
{% extends "base.html" %}
{% block head %}
{% super() %} <!-- Take note of this expression it is explained below -->
<script>
</script>
{% end %}
{% block body %}
<button>Hello World!</button>
{% end %}
The {% super() %}
expression is a special expression which
can only be used inside blocks which allows the child template to bring back the content
of the parent block. If super is not called the content within the block defined by the
parent is completely overriden by the child template.
This is what the output of rendering the child template would look like. Since the child
template used the super expression in the head
block the content of the parent template
was preserved while rendering the child.
Output
<!DOCTYPE html>
<html lang="en">
<head>
<title>This is extensible!</title>
<script>
</script>
</head>
<body>
<button>Hello World!</button>
</body>
</html>
Partial Rendering
Another special feature of blocks within a template is their ability to be rendered independently.
Take the previous base.html
as an example to declare that template in a rust app the code would
look like the following.
#[derive(Template)]
#[stilts(path = "base.html")]
struct BaseTemplate {}
However there is commonly a need to render pieces of a larger template as a component. Breaking very small pieces of template out into another file can lead to lots of small files being difficult to manage. So to aleviate this issue stilts allows defining templates which only render a single block.
#[derive(Template)]
#[stilts(path = "base.html", block = "body")]
struct BodyOnly {}
This technique is useful for partial updates of a webpage when smaller components
need to be re-rendered server side. For larger or more complex components used in
multiple places the include
expression should be preffered.
Include
An include expression is used to add the content from another template into a template at a specified point in the template.
For example say you have a base template that all other templates inherit from
you can still break out some bits into smaller chunks specified in other files.
In this example we include two other template files header.html
, and socials.html
.
The contents of those two files would be inserted at the invocation of include.
<!DOCTYPE html>
<html lang="en">
<head>
<!--A bunch of metadata and script stuff-->
</head>
<body>
<header>{% include "header.html" %}</header>
<main>{% block main %}{% end %}</main>
<footer>{% include "socials.html" %}</footer>
</body>
</html>
A new feature that Stilts adds to include expressions is the ability to specify arguments. By default, include expressions will drag in all the variables required by them into the base template. You can however avoid this by setting the values inside the template.
The arguments are simply added on at the end between a pair of curly braces, and it follows the rust struct literal syntax.
<!DOCTYPE html>
<html lang="en">
<head>
<!--A bunch of metadata and script stuff-->
</head>
<body>
<header>{% include "header.html" {
links: &[
("Home", "/"),
("Social Media", "http://external.website")
],
active: "/"
} %}</header>
<main>{% block main %}{% end %}</main>
<footer>{% include "socials.html" %}</footer>
</body>
</html>
Configuration
Stilts can be configured to meet many potential requirements. Stilts is
configured via your projects existing Cargo.toml
file which already is
your rust config. All configuration is done using package.metadata
which is a cargo feature that allows Stilts to define a custom configuration
within your existing cargo project. To modify the configuration set values
in the package.metadata.stilts
field in your project config, e.g.
# Cargo.toml
[package.metadata.stilts]
trim = true
Here is a list of configuration options, what they do, and their defaults:
- template_dir: Sets the root directory that Stilts looks in to find your templates.
Default: "$CARGO_MANIFEST_DIR/templates"
- trim: Trims whitespace from the beginning and end of each piece of template content
in between expressions.
Default: false
- delimiters: Sets what delimiters Stilts uses when parsing templates.
Default: ["{%", "%}"]
- writer_name: Sets the name of the variable used when generating the template rendering code.
Default: "_w"
- escape: A table of paths to types that implement
Escaper
, and the list of file extensions which that implementation will be applied to.Default: { "::stilts::escaping::Html" = ["html", "htm"] }
So the default configuration would look like this in the context of a full Cargo.toml
file.
[package.metadata.stilts]
template_dir = "$CARGO_MANIFEST_DIR/templates"
trim = false
delimiters = ["{%", "%}"]
writer_name = "_w"
[package.metadata.stilts.escape]
"::stilts::escaping::Html" = ["html", "htm"]
Escaping
Stilts implements an opt-out escaping scheme for templates. By default, the only escaping mechanism is for HTML files, which is the major use case for Stilts. Custom schemes can be added to the configuration as seen above. Stilts also provides a method of excluding whole templates and single display expressions from being escaped if so desired.
The HTML escaping follows OWASP standards of replacing the following characters with safe versions: &
, <
, >
, "
, '
, /
The above configuration section shows how users can add escapers to the opt-out system of stilts, but it does not describe how to actually implement an escaper. Below is a custom implementation that replaces a curse word with stars. This is meant only as an example of how to create a custom escaper.
use std::fmt::{self, Display};
use stilts::escaping::Escaper;
struct HorrificSwear;
impl Escaper for HorrificSwear {
fn fmt<T: Display + ?Sized>(
&self,
value: &T,
f: &mut fmt::Formatter<'_>
) -> fmt::Result {
let s = value.to_string();
let safe = s.replace("heck", "**ck") // clearly much better
f.write_str(&safe)
}
}
Once you have that done simply add it to your config as follows to make it operate on all the file extensions listed for its entry.
[package.metadata.stilts.escape]
"::my_crate::HorrificSwear" = ["txt", "md"]
Design Iteration
Design iteration is an important part of the design process. Friction in that process often causes users to move on to other projects. This section will cover a few methods of reducing development iteration time friction caused by Stilts.
This friction is unfortunately fundamental to how Stilts operates as an engine. The compile time guarantees are provided by the rust compiler, meaning that whatever crate contains templates must recompile when templates change.
The techniques for reducing this friction can be broadly categorized into 3 groups: Live Reload, Change Watching, and Compilation Speed.
Live Reload
A method of automatically refreshing changes on the frontend of a design for the designer to view when changes are made to a code base. When using Stilts this methodology works much better when combined with a change watcher.
- Tower Livereload is a library which can be added to any web server that makes use of the tower ecosystem. It injects code to automatically refresh the browser when it detects the server go down and come back.
Change Watching
A system that watches for file changes inside your project and automatically causes a recompilation based on that.
-
Bacon is a wonderful tool which watches for rust source code file changes. This requires some configuration to use in conjunction with Stilts. Namely, bacon must be told to also watch the
templates
directory, and to kill the running process and restart instead of wait and restart.You can configure this in a global config or at the project level in a file called
bacon.toml
but here is an example config that works for Stilts.[jobs.watch] command = ["cargo", "run"] on_change_strategy = "kill_then_restart" watch = ["templates/"]
Then all you have to do is run
bacon watch
and code changes will automatically result in a recompilation and rerun. -
Watchexec is a great and fairly simple tool which watches files and runs a command when it detects changes. It can be used without configuration with a simple single command.
watchexec -r -e rs,html,css,js cargo run
Will watch for changes in files with the extensions: rs, HTML, CSS, or JS and run the command
cargo run
while restarting the existing process that was already running. -
Cargo Watch Is not recommended by the project author anymore due to lack of time to support the project, however it still works very well. It is the most straightforward to use as a single simple command with no special flags works out of the box for Stilts projects.
cargo watch
Compiliation Speed
Stilts requires a full recompilation of your source code anytime a change is made to your templates. This means that reducing compilation times will increase the speed with which you can iterate on your designs. There are multiple methods of doing this and many of them can be combined to add on top of eachother.
- Break you code up into multiple crates. One simple performance improvement can be to break code into multiple crates. There isn't an exact science to this, and it may not be a good idea for very simple projects. The reason this works however is that the rust compiler launches multiple threads to perform compilation in parallel, but the unit of compilation is the crate. Meaning that splitting code into multiple crates makes for better parallel compilation.
- Use the mold linker. This is simply a tool switch from the default linker rust uses to another which performs the same task but faster. Linking is the final step in compilation, and any performance increase is welcome. The mold readme has a section on how to use it for rust.
- Use the rustc cranelift backend. This replaces another component in the compilation process. This time it replaces the backend of the compiler which generates the low level objects that get linked together. Rust uses LLVM by default but cranelift can sometimes be faster. The downside of this currently is that it requires using nightly rust.
Extension Traits
Extension traits are an existing concept in rust used to add functionality to types. Stilts defines a few extension traits which are imported into the template rendering scope automatically.
Currently, there are three traits exposing 5 methods which can be used to change how a variable is rendered. You can view the trait docs to see how the traits are defined and implemented, but this page will cover the basics of how to use them.
DebugExt
This trait is implemented for any type that implements Debug
.
It adds a method debug
which makes stilts render the type using its Debug
implementation instead
of it's Display
implementation which is the default.
Example
{% name.debug() %}
DisplayExt
This is implemented on any type that implements Display
and is provides multiple functions.
The functions currently provided by this trait are:
safe
Which marks the value as safe to render without running through a sanitizer.lowercase
Changes the output of the type to all lowercase.uppercase
Changes the output of the type to all uppercase.
Warning Only use the
safe
function on data that is verifiably HTML safe. Not following this rule opens you up to XSS attacks! Anything involving user input is an example of where you want to be very careful usingsafe
.
SerializeExt
This only provides one function, and it is implemented on any type which implements
serde Serialize
.
It adds the json
function which converts the type into a JSON
string. This is most useful for adding data to a javascript script inside the template.
Example
One thing with the default escaping scheme you will usually also have to mark the JSON output
as safe so that the quotation marks don't get replaced with "
. Be sure to only do this
if you can trust the data! Most data submitted by a user should not be marked as safe unless
it has already also been processed and made safe.
<script>
const DATA = {% my_template_data.json().safe() %};
</script>
Performance
While performance is not a current target for improvement in Stilts, it does perform well when compared to other rust template engines. The tests ran were modified and updated from these template benchmarks, which have not been updated in some time. The benchmark code will be released to open source soon to provide better insight into methodology, but it hasn't changed much from the linked benchmarks.
Stilts underperforms slightly when compared to other compiled template engines, but it still greatly outperforms runtime engines. This result is at least partially expected, other compiled template engines are able to employ certain optimizations at compile time, that have not been implemented in Stilts.
References
[1] Rust Team, “Rust Programming Language,” Rust-lang.org, 2018. https://www.rust-lang.org/
[2] “The Rust Programming Language - The Rust Programming Language,” doc.rust-lang.org. https://doc.rust-lang.org/stable/book/
[3] Wikipedia Contributors, “Template processor,” Wikipedia, Feb. 14, 2024. https://en.wikipedia.org/wiki/Template_processor
[4] B. Kampmann, C. Morgan, T. Nakata, and I. Ahmed, “Templating» AWWY?,” Are We Web Yet? https://www.arewewebyet.org/topics/templating/
[5] D. Ochtman, “GitHub - djc/askama: Type-safe, compiled Jinja-like templates for Rust,” GitHub, Mar. 06, 2023. https://github.com/djc/askama
[6] D. Tolnay, “GitHub - dtolnay/proc-macro-workshop: Learn to write Rust procedural macros [Rust Latam conference, Montevideo Uruguay, March 2019],” GitHub, 2019. https://github.com/dtolnay/proc-macro-workshop
[7] “The Rust Reference,” Rust-lang.org, 2015. https://doc.rust-lang.org/reference/
[8] D. Tolnay, “syn - Rust,” Docs.rs, 2024. https://docs.rs/syn/latest/syn/
[9] M. Dossinger, “Are we (I)DE yet?,” Areweideyet.com, 2019. https://areweideyet.com/
[10] Wikipedia Contributers, “Terminal emulator,” Wikipedia, Dec. 25, 2022. https://en.wikipedia.org/wiki/Terminal_emulator
[11] “Cargo Guide - The Cargo Book,” Rust-lang.org, 2024. https://doc.rust-lang.org/cargo/guide/
[12] rosetta-rs, “GitHub - rosetta-rs/template-benchmarks-rs: Collected benchmarks for templating crates written in Rust,” GitHub, 2023. https://github.com/rosetta-rs/template-benchmarks-rs
[13] L. Gaskin, “GitHub - leotaku/tower-livereload: Tower middleware to automatically reload your web browser during development,” GitHub, 2022. https://github.com/leotaku/tower-livereload
[14] “tower - Rust,” Docs.rs, 2024. https://docs.rs/tower/latest/tower/
[15] D. Séguret, “GitHub - Canop/bacon: background rust code check,” GitHub, Sep. 14, 2024. https://github.com/Canop/bacon
[16] F. Saparelli, “GitHub - watchexec/watchexec: Executes commands in response to file modifications —— Maintenance status: on hold. I have no time for OSS currently; back late 2024.,” GitHub, Jul. 02, 2024. https://github.com/watchexec/watchexec
[17] F. Saparelli, “GitHub - watchexec/cargo-watch: Watches over your Cargo project’s source.,” GitHub, Oct. 02, 2024. https://github.com/watchexec/cargo-watch
[18] R. Ueyama, “mold: A Modern Linker,” GitHub, Dec. 07, 2023. https://github.com/rui314/mold
[19] rust-lang, “GitHub - rust-lang/rustc_codegen_cranelift: Cranelift based backend for rustc,” GitHub, 2018. https://github.com/rust-lang/rustc_codegen_cranelift
[20] “Jinja — Jinja Documentation (3.1.x),” Palletsprojects.com, 2024. https://jinja.palletsprojects.com