Description

Yarte stands for Yet Another Rust Template Engine, is the fastest template engine. Uses a Handlebars-like syntax, well known and intuitive for most developers. Yarte is an optimized, and easy-to-use rust crate, with which developers can create logic around them HTML templates using conditionals, loops, rust code and using templates composition with partials.

Yarte is intended to be more than just a substitute for PHP, I want to encompass all GUI types with a front end based on Handlebars, HTML5 and Rust.

The process is tedious and very complicated, so certain expendable parts are omitted and will be subject to several refactors before first release.

Derive attributes

  • src: template sources
  • path: path to sources relative to template directory
  • print: all, ast or code display debug info. Overridden by config file print option.
  • recursion: default: 128 Set limits of partial deep, can produce stackoverflow at compile time

Rules

  • Only use }} or {{\ for expressions or blocks (If you want to use them in any place, you are free to implement a tokenizer that includes the syntax of yarte and rust and do PR)
  • Default template extension is hbs, since it includes much of the language (If you want to better IDE support, you are free to write a plugin with yrt extension)

Getting started

As yarte files are not compiler-specific, you have to add:

Cargo.toml

[build-dependencies]
yarte = "0.15"

build.rs

fn main() {
    yarte::recompile::when_changed();
}

In order to recompile when something in the template directory changed;

Yarte templates look like regular text, with embedded yarte expressions. Create a simple Yarte template called hello.hbs in your template directory.

<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{body}}
  </div>
</div>

#![allow(unused_variables)]
fn main() {
use yarte::*;

struct Card<'a> {
    title: &'a str,
    body: &'a str,
}

fn foo() -> String {
    let my_card = Card {
        title: "My Title",
        body: "My Body",
    };

    auto!(ywrite_html!(String, "{{> hello my_card }}"))
}
}

will write in the formatter the following string:

<div class="entry">
  <h1> My Title </h1>
  <div class="body">
    My Body
  </div>
</div>

Config File

Right now, a Yarte configuration file can have the following:

  • main (general configuration - optional): with attribute

    • dir: name of template directory. If no value is given, a default directory templates will be used. If the defined directory is not found, an error will prompt.
    • debug: type of output of debug mode. The code and/or ast generated by Yarte can be visualize, to do so, at most one of three possible values has to be given: code, ast, or all.
  • partials (partials aliasing - optional): each entry must be of the type name_alias = "./alias/path/", where ./ makes reference to dir value. Path must exist, or error will be prompt. If the tag partials doesn't exist no aliasing will be possible.

  • debug (debugging configuration - optional): in order to visualize clearly generated code in a debugging environment Yarte gives it a tabulated format, and the possibility to see the number line, use a colour theme. Options are the following:

    • number_line (default: false): Boolean, if set to true number lines will appear in debug-mode.
    • theme (default: zenburn): String, color theme used in debugging environment. Possible values are:
      • DarkNeon,
      • GitHub,
      • Monokai Extended,
      • Monokai Extended Bright,
      • Monokai Extended Light,
      • Monokai Extended Origin,
      • OneHalfDark,
      • OneHalfLight,
      • Sublime Snazzy,
      • TwoDark,
      • zenburn
    • grid (default: false): Boolean
    • header (default: false): Boolean
    • paging (default: false): Boolean
    • short (default: true): Boolean, if set to false to verbose

Example of a config file

[main]
dir = "templates"
debug = "all"

[partials]
alias = "./deep/more/deep"

[debug]
theme = "zenburn"
number_line = true
grid = true
header = true
paging = false
short = false

With this configuration, the user can call alias in a partial instance with {{> alias context}} or {{> alias}} if the current context is well defined.

Meta programming

Yarte incorporates a meta programming system parallel to Rust's. Which means that it evaluates all Rust expressions at compilation time and together with the partials and the partials block create complex compilations in which you can use recursion, modules, conditional, loops, arrays and ranges.

The methods that can be used are listed in the documentation for v_eval.

All undefined variables at compile time will be None in the evaluator.

Templating

Yarte uses opening characters {{ and closing characters }} to parse the inside depending on the feature used. Most of the features are defined by Handlebars such as paths, comments, html, helpers and partials. Others such as adding rust code to a template, are obviously defined by Yarte. Each of these features have a symbol associated to it (# { R >) that is added after the opening characters, for example {{# used for helpers. If no symbol is added Yarte will interpret inside code as a valid rust expression.

Let's say we want to use the following template template.html

<h1> Hello, {{name}}! </h1>

Now we create a struct with the variable name


#![allow(unused_variables)]
fn main() {
#[derive(Template)]
#[template(path = "template.html")]
struct HelloTemplate<'a> {
    name: &'a str,
}
}

If we now render the template with "world" as value of name,


#![allow(unused_variables)]
fn main() {
HelloTemplate { 
    name: "world" 
}
.call().unwrap()
}

Comments

In order to add comments to your Yarte code use {{!-- or {{! after the opening templating tag and use --!}} or !}}, respectively, as a closing clause.

{{!   Comments can be written  !}}
{{!--  in two different ways --!}}

Comments will appear on the debug output. In release, comments are removed and stream is optimized. Whitespaces around the comment block will be ignored.

Helpers

Helpers are given a context and a function to template the context.

Helpers allow the user to create blocks with a pre-defined functionality with certain context. Yarte provides some built-in helpers, but users can still define theirs.

Conditional helper

If helper

The conditional helper must start with an if block, followed by the condition, {{#if condition}}, where condition is a valid rust expression (otherwise an error will be thrown). Inside an if block, users can use as many else if statements as they want and one else statement to create basic logic in the template, without using #, for example, {{else}} or {{else if condotion}}. In order to close the if block, following the helper syntax, {{/if}} is used.

{{#if isLiked}}
  Liked!
{{else if isSeen}}
  Seen!
{{else}}
  Sorry ...
{{/if}}

In the example above if variable isLiked is interpreted as true, Liked! will be parsed. If isLiked is interpreted as false and isSeen as true then Seen!, otherwise Sorry... will be shown. So having conditional around your HTML code is as intuitive as it should be.

Unless helper

The unless helper is equivalent to a negated if statement, for that reason, negated unless statements are not allowed and error will be prompt.

{{#unless isAdministrator-}} 
  Ask administrator.
{{~/unless}}

Each helper

In order to iterate over a vector of objects, an each helper can be used, with the following syntax:

{{#each into_iter}} 
    {{~# if first ~}}
        {{ index }} 
    {{~ else ~}}
        {{ index0 }} 
    {{~/if }} {{ this }} 
{{~/each}}

Associated variables such as this, first, index, index0 and struct fields are automatically generated and can be used without declaring them.

With helper

The with helper sets the scope/context to be any specified structure, using syntax {{#with context}} {{/with}}.

For example, in the following lines of code we want to set the context inside the with block to be author, defined in the template instead of the 'main' context.


#![allow(unused_variables)]
fn main() {
let author = Author {
    name: "J. R. R. Tolkien"
};
}
{{#with author}}
  <p>{{name}}</p>
{{/with}}

@helpers are a group of functions implemented on Formatter to format most common template structures. Are made to avoid reallocation by creating a single output stream on a single highly abstract writer. Adapt to the language of handlebars by hinting at common expressions headed by the character '@' and the name of @helper

Per example:

{{ @json obj }}

Json

You can serialize json in your template with serde::Serialize


#![allow(unused_variables)]
fn main() {
use serde::Serialize;
#[derive(Template)]
#[template(path = "foo")]
struct Foo<S: Serialize> {
    foo: S
}
}
{{ @json foo }}
{{ @json_pretty foo }}

Don't escape html characters.

If you are looking to paint it as html text (like "Text" in <h1>Text</h1>):

<h1>{{ serde_json::to_string(&foo).map_err(|_| yarte::Error)? }}</h1>

HTML

Yarte HTML-escapes values returned by a {{ expression }}. If you don't want Yarte to escape a value, use the "triple-stash", {{{. For example having the following struct:


#![allow(unused_variables)]
fn main() {
let t = CardTemplate {
  title: "All about <p> Tags",
  body: "<p>This is a post about &lt;p&gt; tags</p>"
};
}

and the following template:

<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{{body}}}
  </div>
</div>

will result in:

<div class="entry">
  <h1>All About &lt;p&gt; Tags</h1>
  <div class="body">
    <p>This is a post about &lt;p&gt; tags</p>
  </div>
</div>

Let

Let statements it can only be used with {{ not with {{{, and tries to imitate rust let statement.

A let statement introduces a new set of variables, given by a pattern. The pattern is followed optionally by a type annotation and then optionally by an initializer expression. When no type annotation is given, the compiler will infer the type, or signal an error if insufficient type information is available for definite inference. Any variables introduced by a variable declaration are visible from the point of declaration until the end of the enclosing block scope. -- Rust Documentation

Whitespaces before and after the block will be ignored.

{{ let doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>() }}
{{ let doubled: Vec<usize> = a.iter().map(|x| x * 2).collect() }}

{{#each doubled ~}}
    {{ this + 1 }}
{{~/each}}

Inside the expression you can use tuple and slice decomposition, mutability and types:

{{ let a = |a: &str| a.repeat(2) }}

{{ let mut a = name.chars() }}

{{ let (mut h, t)  = name.split_at(1) }}

Partial

Partials is the tool that Yarte provides for template composition and is a one line expression of type {{> partial_path scope attr=val,...}}. The performance is the same as writing it and the same as an inline code. The partials and their arguments are generated as much as possible within the same &'static str.

Path

The path of a partial is the file path with respect to the file using the partial. Also the config file can be used to create aliases (explained in the aliasing section). The partial template file will use the context used in attribute scope.

Attributes

Attributes in partials are assignation where right-hand side if the equal sign must be an expression of type path, field, or index. These attributes will be used to reference expression's values and use them inside the partial. In a partial, Yarte will first try to look the value in the attributes and if there is no existing attribute, the given scope must have it.

*Note: In this section we are making reference to attributes which are assignations (not attribute scope or path).

Scope

Attribute scope is the only attribute that is not an assignation and the only one that a partial must have.

  • If scope is not given the default context will be the parent's, otherwise scope can only be an expression of type path, field, or index.

  • When attribute scope is given, the parent scope cannot be access using super::.

The following partial will use file ..tempaltes/templates/partial.hbs, and the parent scope to fill he template:

{{> ../templates/partial }}

Now the same file will be used but since a scope is defined, the parent scope will be overriding expr_scope:

{{> ../templates expr_scope }}

Overriding super top scope (self always reference parent scope) Literals are put inline and pre-escaped when specified ({{ }}).

{{> partial var = bar, lit = "foo" }}

Overriding parent scope and override super (self always reference expr_scope) Literals are put inline and pre-escaped when specified ({{ }}).

{{> partial expr_scope, var = bar, lit = "foo" }}

Partial Block

This block syntax may also be used to pass templates to the partial, which can be executed by the specially named partial, @partial-block. A template of

{{#> layout ~}}
  My Content
{{~/layout }}

with the layout partial containing

Site Content {{> @partial-block }}

Would render

Site Content My Content

When called in this manner, the block will execute under the context of the partial at the time of the call. Depth-ed paths and block parameters operate relative to the partial block rather than the partial template.

{{#each children }}
  {{#> childEntry }}
    {{value}}
  {{/childEntry }}
{{/each }}

Will render this.value from this template, not the partial.

Recursion

TODO

Aliasing

Aliasing is used to make life easier to developers when referencing to a partial template. This is done in the configuration file yarte.toml.

This is explained in more detail in section Config File.

*Note: Aliases make reference to dir in the configuration file.

Raw

If escaping Yarte code is ever needed, the {{R }} {{/R }} block can be used and Yarte code inside the block will be escaped like in the following example:

  {{~R }}{{#each example}}{{/each}}   {{~/R }}

will be render to:

{{#each example}}{{/each}}

Rust code

Yarte provides you with the possibility to use basic raw rust code within the HTML files. There are three important facts to take in consideration when using rust code in your template, the scope they act upon, resolve ident of variables.

  • The usage if this feature is limited by its context, meaning that created variables of a scope, will live only in that scope. Also, a valid rust expression, so it will act like Rust.

  • Resolve:

    • Resolution uses hierarchical nomenclature system, and its most important function is to 'translate' names depending on the context where it lives. Contexts are defined by the root, helpers, partials, and rust blocks. Created variables in these blocks will be removed from the scope when block finishes. If a variable that already existed is redefined, the first will be overwritten by the second one, losing the original value. Be careful with pre-defined variables like first, this, index, index0, _index_[0-9]+ or _n at tuple context to make reference to the n-th item.

    • Constants and static variables must be upper-cased with underscores, N_ITER.

    • Paths of type \*\*::\*\*::\*\* can be use without using reserved word super.

    • self refers to the root scope (first parent). Note that in partials this would be the current partial in use.

    • Substitution will take into account super, locals, etc. So keep track of the context created.

Hello, {{#each conditions}}
    {{~#if let Some(check) = cond }}
        {{~#if check }}
            {{ let cond = if check { "&foo" } else { "&"} }}
            {{
                if check {
                    cond
                } else if let Some(cond) = key.cond {
                    if cond {
                        "1"
                    } else {
                        "2"
                    }
                } else {
                   "for"
                }
            }}
        {{~ else if let Some(_) = cond }}
        {{~ else if let Some(cond) = key.check }}
            {{~#if cond ~}}
                baa
            {{~/if }}
        {{~ else ~}}
            {{ cond.is_some() }}
        {{~/if~}}
        {{ cond.is_some() && true }}
    {{~else if let Some(cond) = check }}
        {{~#if cond ~}}
            bar
        {{~/if}}
    {{~ else ~}}
        None
    {{~/if}}
{{~/each}}!
{{ unsafe { s.get_unchecked(0) } }}

Super scope

In Yarte you will be able to call parents of the actual scope, making parent scopes available in all child

Hello, {{#each this~}}
        {{#each this.as_bytes() ~}}
            {{ super::index0 }} {{ super::super::this[0] }}
        {{~/each }}{{ super::this[0] }}
{{~/each}}!
Hello, {{#each this~}}
    {{#with this}}{{ super::hold }}{{ hold }}{{/with}}
{{~/each}}!
Hello, {{#each this~}}
    {{#with this}}{{ super::hold }}{{ hold }}{{ super::index }}{{/with}}
{{~/each}}!

Whitespace control

Yarte provides the possibility to erase the unneeded blanks when templating using the character ~ in any block. Only first white characters and last whitespaces of a block will be ignored. Characters interpreted as whitespaces will as specified in rust.

Let's say we have a struct define as follows:


#![allow(unused_variables)]
fn main() {
#[derive(Template)]
#[template(path = "hello.html")]
struct CardTemplate<'a> {
    users: Vec<User<'a>>,
}

struct User<'a> {
    valid: bool,
    name: &'a str,
}
}

and we create a new CardTemplate


#![allow(unused_variables)]
fn main() {
let t = CardTemplate {
    users: vec![
        User { 
            name: "Tom",
            valid: true,
        },
    ],
};
}

Now we will use ~ in an if statement and ignore unnecessary whitespaces.

{{~# each users ~}}
    {{~# if valid ~}}
        <h1>Hello, {{ name }}</h1>
    {{~/if }}
{{~/each}}

This will output

<h1>Hello, Tom</h1> 

In the other hand if we don't ignore whitespaces:

{{~# each users ~}}
    {{# if valid }}
        <h1>Hello, {{ name }}</h1>
    {{/if }}
{{~/each}}

then this will be the output


    <h1>Hello, Tom</h1> 
    

Special cases

The are some especial cases where Yarte will ignore whitespaces before and after in some special cases by default. These cases are when writing comments, locals(such as let expressions), and whitespaces at the end of the file