15 releases (9 breaking)
0.60.1 | Sep 27, 2023 |
---|---|
0.52.0 | Sep 4, 2023 |
0.40.1 | Jul 29, 2023 |
#266 in Template engine
185KB
3.5K
SLoC
A minimal and fast template engine.
Ban is a template engine that compiles templates at runtime. It supports the basic features you might expect, it's easy to use, and it tries to provide good error messages.
This is a work in progress, view the documentation for more details.
lib.rs
:
A minimal and fast template engine.
Ban is a template engine that compiles templates at runtime. It supports the basic features you might expect, it's easy to use, and it tries to provide good error messages.
Features
- Common logical constructs (
if
,let
, andfor
). - User-defined filters to transform content.
- An optional standard library providing filters for common functionality, like HTML escaping.
- Multiple strategies for template inheritance.
- Block/extends - divide a template up into blocks that can be overridden by child templates.
- Include - render another template at the specified location.
- Custom delimiters.
🦊 Note
Ban is still in development, and is not ready to be used.
I only document features here that are actually implemented, but even then,
information may be incomplete until v1.
Usage
Create a new Engine
with ban::default
,
or if you want to use custom delimiters, use the Builder
type and the ban::new
method.
let engine = ban::default();
The Engine
type provides a place for you to register filters and store other
templates that you can call with the include
block.
Compile
Use the Engine
to compile a Template
.
let engine = ban::default();
let template = engine.compile("hello (( name ))!").is_ok();
Create a Store
The Template
we just compiled has a single expression that wants to render something
called "name".
To render this, we will need to supply a Store
instance containing a value
for that identifier.
use ban::Store;
let mut store = Store::new();
store.insert_must("name", "taylor");
Render
Now that we have a Store
containing the data our Template
wants to use, we can use
the Engine
to render it.
use ban::Store;
let engine = ban::default();
let template = engine.compile("hello, (( name ))!").unwrap();
let mut store = Store::new();
store.insert_must("name", "taylor");
let result = engine.render(&template, &store).unwrap();
assert_eq!(result, "hello, taylor!");
Syntax
This section provides an overview of expressions and the different blocks you can use.
Ban should be familiar if you've used other template engines.
Expressions
Expressions let you render content from the Store
, or literal values
like strings and numbers. They look like this:
(( name ))
Or, if you want to mutate the "name" variable using filters:
(( name | to_lowercase | left 3 ))
Filters
Filters
can be used in expressions to
transform data.
The input for the first filter comes from the value on the far left. It travels through each filter from left to right, and the output of the final filter in the chain is rendered.
Filters may accept any number of arguments, or none. They may be named or anonymous.
Named arguments require a colon between the name and value:
(( name | tag name: "taylor", age: 25 ))
Anonymous arguments work the same way, but have no explicit name:
(( name | tag "taylor", 25 ))
Both variants require arguments to be separated with a comma.
See the filter
module for more information.
If
If blocks allow conditional rendering based on a series of expressions.
(* if true *)
hello
(* else if false *)
goodbye
(* end *)
You can compare two values, or provide just one. If the value is truthy, the block will execute.
(* if 100 *)
hello
(* end *)
Here's a cheatsheet for truthy values:
Type | Truthy When |
---|---|
String | String is not empty. |
Number | Number is greater than zero. |
Array | Array is not empty. |
Object | Object is not empty. |
Boolean | Boolean is true. |
You can use the not
keyword to negate:
(* if not false && 500 > 10 || true *)
hello
(* end *)
Examples
use ban::Store;
let mut engine = ban::default();
let template = engine
.compile("(* if first > second *)hello(* else *)goodbye(* end *)")
.unwrap();
let store = Store::new()
.with_must("first", 100)
.with_must("second", 10);
let result = engine.render(&template, &store).unwrap();
assert_eq!(result, "hello");
For
For blocks allow iteration over a value.
(* for item in inventory *)
Name: (( item.name ))
(* end *)
You can provide a single identifier as seen above, or two:
(* for i, item in inventory *)
Item number: (( i | add 1 )) // <-- Zero indexed, so add one!
Name: (( item.name ))
(* end *)
The values held by the identifiers depends on the type you are iterating on:
Type | Value for i |
Value for item |
---|---|---|
String | Index of character. | Character of string. |
Array | Index of element. | Element of array. |
Object | Object key. | Object value. |
Examples
use ban::{filter::serde::json, Store};
let mut engine = ban::default();
let template = engine
.compile("(* for item in inventory *)(( item )), (* end *)")
.unwrap();
let store = Store::new()
.with_must("inventory", json!(["sword", "shield"]));
let result = engine.render(&template, &store).unwrap();
assert_eq!(result, "sword, shield, ");
Let
Let expressions allow assignment of a value to an identifier.
(* let name = "taylor" *)
The left side of the expression must be an identifier, meaning an unquoted string, but the right side can be an identifier or literal value.
You can also use filters in let expressions!
(* if is_admin *)
(* let name = "admin" *)
(* else *)
(* let name = user.name | to_lowercase *)
(* end *)
Hello, (( name )).
Assignments made within a for block are scoped to the block:
(* for item in inventory *)
(* let name = item.name *)
Name: (( name ))
(* end *)
Last item name: (( name )). // <-- Error, "name" is not in scope!
Examples
use ban::Store;
let mut engine = ban::default();
let template = engine
.compile("hello, (* let name = \"taylor\" -*) (( name ))!")
.unwrap();
let store = Store::new();
let result = engine.render(&template, &store).unwrap();
assert_eq!(result, "hello, taylor!");
Include
Include expressions allow other templates to be rendered.
(* include header *)
If you call another template this way, it will have access to the same store as the template that called it.
You can pass arguments, similar to filters:
(* include header name: data.name, age: data.age *)
When you pass arguments to an included template, it will have access to those values and nothing else.
Examples
use ban::{filter::serde::json, Store};
let mut engine = ban::default();
engine
.insert_template_must("header", "hello, (( name ))! - (( age ))")
.unwrap();
let template = engine
.compile(r#"(* include header name: data.name, age: data.age *)"#)
.unwrap();
let store = Store::new()
.with_must("data", json!({"name": "taylor", "age": 25}));
let result = engine.render(&template, &store);
assert_eq!(result.unwrap(), "hello, taylor! - 25");
Extends
Extends expressions allow templates to extend one another.
(* extends parent *)
A template extends another template when the "extends" expression is found first in the template source.
When Ban renders an extended template, all of the blocks found in the source are collected and carried to the parent. Assuming the parent template is not also extended, the blocks are rendered there.
When a block expression is found in a parent (non-extended) template, Ban will render the matching block, if it has one.
When no matching block is found, any data inside of the block is rendered instead as a default value.
use ban::{Engine, Store};
let mut engine = Engine::default();
engine
.insert_template_must(
"first",
r#"hello (* block "🦊" *)(* end *), (* block greeting *)doing well?(* end *)"#,
)
.unwrap();
engine
.insert_template_must(
"second",
r#"(* extends first *)(* block "🦊" *)(( name ))(* end *)"#,
)
.unwrap();
let store = Store::new().with_must("name", "taylor");
let template = engine.get_template("second").unwrap();
assert_eq!(
engine.render(&template, &store).unwrap(),
"hello taylor, doing well?"
);
View the examples/inheritance directory for a full illustration.
Delimiters
Use the Builder
type to create an Engine
that recognizes a different
set of delimiters.
use ban::{Engine, Builder, Store};
let engine = Engine::new(
Builder::new()
.with_expression(">>", "<<")
.with_block("{@", "@}")
.with_whitespace(&'~')
.to_syntax(),
);
let template = engine
.compile("{@ if true ~@} Hello, >> name <<!{@ end @}")
.unwrap();
let store = Store::new().with_must("name", "taylor");
let result = engine.render(&template, &store).unwrap();
assert_eq!(result, "Hello, taylor!")
Dependencies
~1.5–2.3MB
~39K SLoC