Expand description
boilerplate is a template engine.
Templates are format agnostic, and can be used to generate HTML, Markdown, or any other text format.
The template syntax is very simple, and interpolations and control flow are Rust code, so you don’t have to learn a separate template language.
Templates are checked at compile time, so any error that the Rust compiler can catch can’t make it into production.
There are two ways to use boilerplate.
One way is with boilerplate::Boilerplate, a derive macro, which creates a
Display implementation on a context type that you provide. The template
text is stored in a separate file, and reads variables from the context
type when rendered.
The other way is with boilerplate::boilerplate, a function-like macro,
which reads template text from a string literal, and reads variables from
the local scope when rendered.
Use boilerplate::Boilerplate if you want to put your template text in a
separate file, or if you need HTML escaping, and boilerplate::boilerplate
if you want to put your template in a string literal.
boilerplate is very simple, requires no runtime dependencies, and is
usable in a no_std environment.
§Quick Start
Add boilerplate to your project’s Cargo.toml:
[dependencies]
boilerplate = "*"Create a template in templates/quick-start.txt:
Foo is {{self.n}}!Define, instantiate, and render the template context:
#[derive(boilerplate::Boilerplate)]
struct QuickStartTxt {
n: u32,
}
assert_eq!(QuickStartTxt { n: 10 }.to_string(), "Foo is 10!\n");§Template File Locations
By default, template file paths are relative to the crate root and derived from the context name using the following steps:
1. QuickStartTxt # get template context name
2. Quick Start Txt # split words
3. quick start txt # convert to lowercase
3. quick start.txt # replace final space with period
4. quick-start.txt # replace remaining spaces with dashes
6. templates/quick-start.txt # add templates directory§Custom Filename
#[derive(boilerplate::Boilerplate)]
#[boilerplate(filename = "quick-start.txt")]
struct Context {
n: u32,
}
assert_eq!(Context { n: 10 }.to_string(), "Foo is 10!\n");§Inline Templates
Templates contents can be read from a string:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Hello, world!")]
struct Context {}
assert_eq!(Context {}.to_string(), "Hello, world!");§Guide
Deriving boilerplate::Boilerplate on a type generates an implementation of
the Display trait, which can be printed or rendered to a string with
.to_string().
Rust code in templates is inserted into the generated Display::fmt,
function, which takes &self as an argument, so it can refer to the
template context instance using self.
§Text
Text is included in template output verbatim.
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Hello, world!")]
struct Context {}
assert_eq!(Context {}.to_string(), "Hello, world!");§Interpolation Blocks
Expressions inside {{…}} are interpolated into the template output:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Hello, {{self.name}}!")]
struct Context {
name: &'static str,
}
assert_eq!(Context { name: "Bob" }.to_string(), "Hello, Bob!");§Interpolation Lines
Expressions between $$ and the next newline are interpolated into the
template output:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "My favorite byte is $$ self.byte\n")]
struct Context {
byte: u8,
}
assert_eq!(Context { byte: 38 }.to_string(), "My favorite byte is 38\n");If there is no following newline, the end of the template text terminates the interpolation:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "My favorite byte is $$ self.byte")]
struct Context {
byte: u8,
}
assert_eq!(Context { byte: 38 }.to_string(), "My favorite byte is 38");This works for escaped templates as well:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "My favorite byte is $$ self.byte")]
struct ContextHtml {
byte: u8,
}
assert_eq!(ContextHtml { byte: 38 }.to_string(), "My favorite byte is 38");§Code Blocks
Code inside of {%…%} is included in the display function body:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Knock, knock!
{% if !self.alone { %}
Who's there?
{% } %}
")]
struct Context {
alone: bool,
}
assert_eq!(Context { alone: true }.to_string(), "Knock, knock!\n\n");
assert_eq!(Context { alone: false }.to_string(), "Knock, knock!\n\nWho's there?\n\n");§Code Lines
Code between %% and the next newline is included in the display function
body:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Knock, knock!
%% if !self.alone {
Who's there?
%% }
")]
struct Context {
alone: bool,
}
assert_eq!(Context { alone: true }.to_string(), "Knock, knock!\n");
assert_eq!(Context { alone: false }.to_string(), "Knock, knock!\nWho's there?\n");Code lines are often more legible than code blocks. Additionally, because
the \n at the end of a code line is stripped, the rendered templates may
include fewer unwanted newlines.
If no newline is present, the code line is terminated by the end of the template text:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "%% let _x = 2;")]
struct Context {}
assert_eq!(Context { }.to_string(), "");§Loops
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "{% for i in 0..5 { %}Hi!{% } %}")]
struct Context {}
assert_eq!(Context {}.to_string(), "Hi!Hi!Hi!Hi!Hi!");#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "%% for i in 0..10 {
{{ i }}
%% }
")]
struct Context {}
assert_eq!(Context {}.to_string(), "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n");#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "%% for i in 0..10 {
$$ i
%% }
")]
struct Context {}
assert_eq!(Context {}.to_string(), "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n");#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "%% for (i, value) in self.0.iter().enumerate() {
Value {{i}} is {{value}}
%% }
")]
struct Context(&'static [&'static str]);
assert_eq!(
Context(&["foo", "bar", "baz"]).to_string(),
"Value 0 is foo\nValue 1 is bar\nValue 2 is baz\n"
);§Match Statements
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = r#"%% match self.item {
%% Some("foo") => {
Found literal foo
%% }
%% Some(val) => {
Found {{ val }}
%% }
%% None => {}
%% }
"#)]
struct Context {
item: Option<&'static str>,
}
assert_eq!(
Context { item: Some("foo") }.to_string(),
"Found literal foo\n"
);
assert_eq!(Context { item: Some("bar") }.to_string(), "Found bar\n");
assert_eq!(Context { item: None }.to_string(), "");§Multiple Statements in an Interpolation
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "$$ { let x = !false; x }\n")]
struct Context {}
assert_eq!(Context {}.to_string(), "true\n");§The Empty Template
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "")]
struct Context {}
assert_eq!(Context {}.to_string(), "");§Escaping
If the template file path ends with an html, htm, or xml extension,
escaping is enabled.
#[derive(boilerplate::Boilerplate)]
struct EscapeHtml(&'static str);
assert_eq!(EscapeHtml("&").to_string(), "&\n");#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "$$ self.0\n")]
struct ContextHtml(&'static str);
assert_eq!(ContextHtml("&").to_string(), "&\n");The Trusted wrapper disables escaping for trusted values:
use boilerplate::Trusted;
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "$$ Trusted(self.0)\n")]
struct ContextHtml(&'static str);
assert_eq!(ContextHtml("&").to_string(), "&\n");§Generics
Context types may have lifetimes and generics;
use std::fmt::Display;
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "$$ self.content\n")]
struct Context<'a, T: Display> { content: &'a T }
assert_eq!(Context { content: &100 }.to_string(), "100\n");§Axum Integration
When the axum feature is enabled, templates will be provided with an
axum::response::IntoResponse implementation. The MIME type is deduced
from the template path defaulting to text/plain. If the MIME type is
text, charset=utf-8 will be added automatically, since all
boilerplate templates are UTF-8.
#[cfg(feature = "axum")]
{
use axum::response::IntoResponse;
#[derive(boilerplate::Boilerplate)]
struct GuessHtml {}
assert_eq!(
GuessHtml {}
.into_response()
.headers()
.get("content-type")
.unwrap(),
"text/html;charset=utf-8",
);
}#[cfg(feature = "axum")]
{
use axum::response::IntoResponse;
#[derive(boilerplate::Boilerplate)]
struct Guess {}
assert_eq!(
Guess {}
.into_response()
.headers()
.get("content-type")
.unwrap(),
"text/plain;charset=utf-8",
);
}The axum attribute can be used to suppress or force generation of
IntoResponse implementations, regardless of whether the axum feature is
enabled:
use axum::response::IntoResponse;
#[derive(boilerplate::Boilerplate)]
#[boilerplate(axum = true)]
struct GuessHtml {}
assert_eq!(
GuessHtml {}
.into_response()
.headers()
.get("content-type")
.unwrap(),
"text/html;charset=utf-8",
);use axum::response::IntoResponse;
#[derive(boilerplate::Boilerplate)]
#[boilerplate(axum = false)]
struct GuessHtml {}
GuessHtml {}.into_response();§Reloading Templates
When the reload feature is enabled, templates support a limited form of
hot-reloading.
Using #[derive(Boilerplate] derives both an implementation of Display,
and an implementation of the Boilerplate trait. Normally the
Boilerplate trait and its implementation can be ignored, but when the
reload feature is enabled, the Boilerplate trait includes
Boilerplate::reload which allows a template to be reloaded at runtime.
Boilerplate templates contain Rust code which is compiled ahead of time.
Consequently, the new template’s code blocks must match those of the
original template. If they do not, Boilerplate::reload will return an
error.
#[cfg(feature = "reload")]
{
// import the `Boilerplate` trait for the `reload` method
use boilerplate::Boilerplate;
#[derive(Boilerplate)]
#[boilerplate(text = "Hello, {{self.first}}!")]
struct Context {
first: &'static str,
last: &'static str,
}
let context = Context { first: "Bob", last: "Smith" };
assert_eq!(context.to_string(), "Hello, Bob!");
// Reload a compatible template:
let compatible_template = "Goodbye, {{self.first}}!";
assert_eq!(context.reload(compatible_template).unwrap().to_string(), "Goodbye, Bob!");
// Whitespace around code is allowed to be different:
let compatible_template = "Goodbye, {{ self.first }}!";
assert_eq!(context.reload(compatible_template).unwrap().to_string(), "Goodbye, Bob!");
// Text blocks can be removed entirely:
let incompatible_template = "{{ self.first }}";
assert_eq!(
context.reload(incompatible_template).unwrap().to_string(),
"Bob",
);
// Try to reload an incompatible template with different code:
let incompatible_template = "Goodbye, {{self.id}}!";
assert_eq!(
context.reload(incompatible_template).err().unwrap().to_string(),
"template blocks are not compatible: {{self.id}} != {{self.first}}",
);
// Try to reload an incompatible template with a different number of blocks:
let incompatible_template = "Goodbye, {{self.first}} {{self.last}}!";
assert_eq!(
context.reload(incompatible_template).err().unwrap().to_string(),
"new template has 5 blocks but old template has 3 blocks",
);
// Try to reload a template with invalid syntax:
let incompatible_template = "Goodbye, {{self.first";
assert_eq!(
context.reload(incompatible_template).err().unwrap().to_string(),
"failed to parse new template: unmatched `{{`",
);
}Mostly, non-code template text can be added, changed, and removed and still
be reload-compatible with the original. The only limitation is that text
blocks between {% ... %} and %% ... \n cannot be inserted or removed.
#[cfg(feature = "reload")]
{
// import the `Boilerplate` trait for the `reload` method
use boilerplate::Boilerplate;
#[derive(Boilerplate)]
#[boilerplate(text = "{% if self.condition { %}{% } %}")]
struct Context {
condition: bool,
}
let context = Context { condition: true };
// Reload a compatible template:
let compatible_template = "{% if self.condition { %}{% } %}";
assert_eq!(context.reload(compatible_template).unwrap().to_string(), "");
// Text between code blocks cannot be inserted:
let incompatible_template = "{% if self.condition { %} hello {% } %}";
assert_eq!(
context.reload(incompatible_template).err().unwrap().to_string(),
"new template has 5 blocks but old template has 4 blocks",
);
}Text between code blocks can be changed:
#[cfg(feature = "reload")]
{
// import the `Boilerplate` trait for the `reload` method
use boilerplate::Boilerplate;
#[derive(Boilerplate)]
#[boilerplate(text = "{% if self.condition { %}Hello!{% } %}")]
struct Context {
condition: bool,
}
let context = Context { condition: true };
assert_eq!(context.to_string(), "Hello!");
// Reload a compatible template:
let compatible_template = "{% if self.condition { %}Goodbye!{% } %}";
assert_eq!(context.reload(compatible_template).unwrap().to_string(), "Goodbye!");
}If a template was created from a file, you can call
Boilerplate::reload_from_path to reload it from its original location:
#[cfg(feature = "reload")]
{
// import the `Boilerplate` trait for the `reload_from_path` method
use boilerplate::Boilerplate;
#[derive(boilerplate::Boilerplate)]
struct QuickStartTxt {
n: u32,
}
assert_eq!(QuickStartTxt { n: 10 }.to_string(), "Foo is 10!\n");
assert_eq!(
QuickStartTxt { n: 10 }.reload_from_path().unwrap().to_string(),
"Foo is 10!\n",
);
}Boilerplate::reload_from_path will return Error::Path if the template was
created from a string literal:
#[cfg(feature = "reload")]
{
// import the `Boilerplate` trait for the `reload_from_path` method
use boilerplate::Boilerplate;
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Foo is {{ self.n }}!\n")]
struct Context {
n: u32,
}
assert_eq!(Context { n: 10 }.to_string(), "Foo is 10!\n");
assert_eq!(
Context { n: 10 }.reload_from_path().err().unwrap().to_string(),
"template has no path",
);
}§Function-like Macro
A function-like macro named boilerplate is also available, which can be
used without needing to define a context type.
use boilerplate::boilerplate;
let foo = true;
let bar: Result<&str, &str> = Ok("yassss");
let output = boilerplate!(
"%% if foo {
Foo was true!
%% }
%% match bar {
%% Ok(ok) => {
Pretty good: {{ ok }}
%% }
%% Err(err) => {
Not so great: {{ err }}
%% }
%% }
");
assert_eq!(output, "Foo was true!\nPretty good: yassss\n");§Nesting Templates
Since templates implement Display they can used in interpolations inside
other templates:
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Hello {{ self.0 }}!")]
struct OuterTxt(InnerTxt);
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "Mr. {{ self.0 }}")]
struct InnerTxt(&'static str);
assert_eq!(OuterTxt(InnerTxt("Miller")).to_string(), "Hello Mr. Miller!");This is especially useful when generating multiple HTML pages with unique
content, but with headers and footers that are common to all pages. Note
the use of boilerplate::Trusted to prevent escaping the inner HTML:
use {
boilerplate::Trusted,
std::fmt::Display,
};
trait Page: Display {
fn title(&self) -> &str;
}
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "<!doctype html>
<html>
<head>
<title>{{ self.0.title() }}</title>
</head>
<body>
{{ Trusted(&self.0) }}
</body>
</html>
")]
struct OuterHtml<T: Page>(T);
#[derive(boilerplate::Boilerplate)]
#[boilerplate(text = "<div>{{ self.0 }}</div>")]
struct InnerHtml(&'static str);
impl Page for InnerHtml {
fn title(&self) -> &str {
"awesome page"
}
}
assert_eq!(
OuterHtml(InnerHtml("awesome content")).to_string(),
"<!doctype html>
<html>
<head>
<title>awesome page</title>
</head>
<body>
<div>awesome content</div>
</body>
</html>
");Macros§
Structs§
- Trusted
- Disable escaping for the wrapped value.
Traits§
- Boilerplate
- The boilerplate trait, automatically implemented by the
Boilerplatederive macro. - Escape