| Crates.io | upon |
| lib.rs | upon |
| version | 0.10.0 |
| created_at | 2022-03-26 11:48:00.412559+00 |
| updated_at | 2025-06-27 08:55:05.16234+00 |
| description | A simple, powerful template engine with minimal dependencies and configurable delimiters |
| homepage | |
| repository | https://github.com/rossmacarthur/upon |
| max_upload_size | |
| id | 556681 |
| size | 364,669 |
A simple, powerful template engine with minimal dependencies and configurable delimiters.
{{ user.name }}{% if user.enabled %} ... {% endif %}{% for user in users %} ... {% endfor %}{% include "nested" %}<? user.name ?>, (( if user.enabled )){{ user.name | replace: "\t", " " }}{{ user.name | escape_html }}String or any std::io::Write implementorserde serializable valuesupon::value!{ name: "John", age: 42 }{:#}It’s true there are already a lot of template engines for Rust!
I created upon because I required a template engine that had runtime
compiled templates, configurable syntax delimiters and minimal dependencies.
I also didn’t need support for arbitrary expressions in the template syntax
but occasionally I needed something more flexible than outputting simple
values (hence functions). Performance was also a concern for me, template
engines like Handlebars and Tera have a lot of features but can be up to
five to seven times slower to render than engines like TinyTemplate.
Basically I wanted something like TinyTemplate with support for configurable delimiters and user defined functions. The syntax is inspired by template engines like Liquid and Jinja.
Currently the minimum supported version for upon is Rust 1.66. The MSRV
will only ever be increased in a breaking release.
First, add the crate to your Cargo manifest.
cargo add upon
Now construct an Engine. The engine stores the syntax config, functions,
formatters, and compiled templates. Generally, you only need to construct
one engine during the lifetime of a program.
let engine = upon::Engine::new();
Next, add_template(..) is used to compile and store a
template in the engine.
engine.add_template("hello", "Hello {{ user.name }}!")?;
Finally, the template is rendered by fetching it using
template(..), calling
render(..) and rendering to a string.
let result = engine
.template("hello")
.render(upon::value!{ user: { name: "John Smith" }})
.to_string()?;
assert_eq!(result, "Hello John Smith!");
syntax module documentation outlines the template syntax.functions module documentation describes functions and how they
work.fmt module documentation contains information on value formatters.examples/ directory in the repository contains some more
concrete code examples.The following crate features are available.
functions (enabled by default) — Enables support for functions in
templates (see Engine::add_function). This does not affect value
formatters (see Engine::add_formatter). Disabling this will improve
compile times.
serde (enabled by default) — Enables all serde support and pulls
in the serde crate as a dependency. If disabled then you can use
render_from(..) to render templates and
construct the context using Value’s From impls.
syntax (disabled by default) — Enables support for configuring
custom delimiters in templates (see Engine::with_syntax) and pulls in
the aho-corasick crate.
unicode (enabled by default) — Enables unicode support and pulls
in the unicode-ident and
unicode-width crates. If disabled then unicode
identifiers will no longer be allowed in templates and .chars().count()
will be used in error formatting.
To disable all features or to use a subset you need to set default-features = false in your Cargo manifest and then enable the features that you would
like. For example to use serde but disable functions and
unicode you would do the following.
[dependencies]
upon = { version = "...", default-features = false, features = ["serde"] }
You can include other templates by name using {% include .. %}.
let mut engine = upon::Engine::new();
engine.add_template("hello", "Hello {{ user.name }}!")?;
engine.add_template("goodbye", "Goodbye {{ user.name }}!")?;
engine.add_template("nested", "{% include \"hello\" %}\n{% include \"goodbye\" %}")?;
let result = engine.template("nested")
.render(upon::value!{ user: { name: "John Smith" }})
.to_string()?;
assert_eq!(result, "Hello John Smith!\nGoodbye John Smith!");
Instead of rendering to a string it is possible to render the template to
any std::io::Write implementor using
to_writer(..).
use std::io;
let mut engine = upon::Engine::new();
engine.add_template("hello", "Hello {{ user.name }}!")?;
let mut stdout = io::BufWriter::new(io::stdout());
engine
.template("hello")
.render(upon::value!{ user: { name: "John Smith" }})
.to_writer(&mut stdout)?;
// Prints: Hello John Smith!
If the lifetime of the template source is shorter than the engine lifetime
or you don’t need to store the compiled template then you can also use the
compile(..) function to return the template directly.
let template = engine.compile("Hello {{ user.name }}!")?;
let result = template
.render(&engine, upon::value!{ user: { name: "John Smith" }})
.to_string()?;
assert_eq!(result, "Hello John Smith!");
The compile(..) function can also be used in
conjunction with a custom template store which can allow for more advanced
use cases. For example: relative template paths or controlling template
access.
let mut store = std::collections::HashMap::<&str, upon::Template>::new();
store.insert("hello", engine.compile("Hello {{ user.name }}!")?);
store.insert("goodbye", engine.compile("Goodbye {{ user.name }}!")?);
store.insert("nested", engine.compile("{% include \"hello\" %}\n{% include \"goodbye\" %}")?);
let result = store.get("nested")
.unwrap()
.render(&engine, upon::value!{ user: { name: "John Smith" }})
.with_template_fn(|name| {
store
.get(name)
.ok_or_else(|| String::from("template not found"))
})
.to_string()?;
assert_eq!(result, "Hello John Smith!\nGoodbye John Smith!");
upon was benchmarked against several popular template rendering engines in the
Rust ecosystem. Obviously, each of these engines has a completely different
feature set so the benchmark just compares the performance of some of the
features that they share.
Benchmarking was done using criterion.
Host
Licensed under either of
at your option.