hairy 0.1.0

Compiled text templates (not unlike Mustache and Handlebars), with support for expressions and custom functions inside such expressions.
Documentation

The hair crate provides text templates, not unlike mustache and handlebars (and the earlier ctemplate original), but a bit different. All errors are treated as hard errors and are reported. So missing values are not silently ignored. Also, the templates support evaluation: {{=expression}}. The supported expressions are from the [expry] crate, which provided executing expression on binary encoded JSON-like values.

Syntax

In short the features and syntax:

  • Variables can be declared at top of an input file with type key: expry pairs (JSON is a valid expry). type can be either inline (value is replaced during compilation of the template) or default (which is a default value that is added to the evaluation time value). The first non-parsable line signals the end of the variables.
  • Values can be outputted with {{=value}} (supports expressions, see below). Escape mode can be specified with {{=value:escape_mode}}. Values are processed by a user-definable escaper that can take the escape mode into account. Normally only strings and numbers are displayed, and the null value is considered a nop. Other values are considered an error, except when the sjs (script-js) or js escape mode is used.
  • Conditionals with {{if value}}contents{{end}}. Contents is displayed if value evaluates to true.
  • Iterators with {{for variable in name}}content{{end}}, which can be used to iterate over (arrays of) any type. The contents of the array is available in the loop body under key variable;
  • Template definition with {{define name}}content{{end}}. Templates can have optional default values (in an object) with {{define name defaults object}}content{{end}}. Defaults are resolved at template compile time, using the global context given to the compile function;
  • Template instantiation with {{call name}} or {{call name with value}}. name can be an expression. If the name starts with a *, name can be an expression that resolves to a string (which is treated as a template name). If the name starts with **, name should be an expression that resolves to a binary form template code (as produced by the compile functions). If the with syntax is used, only the data specified in value is passed along (so the current context is not automatically transfered/merged, to do that use for value {...this, key: value}). This is done to avoid errors.
  • Error handling is different from normal Mustache: missing fields is always considered an error. Errors can be suppressed with the expr ??? alternative try syntax (on error in expr, alternative will be executed). Shorthand for expr ??? null is expr ??? (which can be used in loops with {{for i in someField???}}, which ignores errors if the field is not found).

Some other template systems have the ability to iterate without variables, in effect making the object fields available in the current context. Although this is at first appealing, this makes these templates error-prone. Types can differ, depending on the presence of fields in the object iterated over (which can change for each iteration). One of the design goals of hair is error handling. To fall back to the value in the global context on error such as field not found, the following expression can be used: {{iteration_variable.foo???foo}}. This makes the whole expression explicit in its intent.

Escaping

Normally, all strings that are dynamically outputted (using an evaluating {{=..}} statement) are automatically 'escaped' using the escaper as given to an argument to the hairy_eval function. However, often you will want different escape modes depending on the input context. For example in HTML, output may not normally contain < or >, but output inside attributes in HTML tags are normally escaped like URLs. Although users can specify for every output the escape mode by appending a :html or :url escaping mode to an expression, this is error-prone. Auto escaping is therefore a safer alternative, by automatically deciding the escape mode from the input. The general advise is to escape defensively. hair functions support an argument that is used to look up the escaping mode for a certain position in the input.

Easy interface

The 'easy' interface if you just want to quickly use an HTML template, with auto escaping of the input, and returning a nicely formatted error that can be presented to the user. To evaluate, use [hairy_eval_html], so the proper escaper is used.

Note that although [hairy_compile_html] and [hairy_eval_html] are easier to use, they are somewhat slower. For top performance please use the [hairy_compile] and the [hairy_eval] functions.

Example using the simple interface

use hairy::*;
use expry::*;
use std::io::Write;
let template = r#"foobar = {{=foovar .. barvar}}"#;
let result = hairy_compile_html(template, "test.tpl", None, 0);
match result {
Ok(parsed) => {
let value = value!({
"foovar": "foo",
"barvar": "bar",
}).to_vec(false);
match hairy_eval_html(parsed.to_ref(), value.to_ref()) {
Ok(output) => { std::io::stdout().write_all(&output); },
Err(err) => { eprintln!("{}", err); },
}
},
Err(err) => {
eprintln!("{}", err);
assert!(false); // to check this example
}
}

Enclosing other template files

The value can contain other template compiled with hairy_compile_html. So if value body contains the bytecode of such a template, that template can be invoked from the 'main' template by using the template expression {{call **body with argument}}.

use hairy::*;
use expry::*;
use std::io::Write;
let main_template = r#"<html><title>{{=title}}</title><body>{{call **body with {title,foobarvar}}}</body></html>"#;
let main = hairy_compile_html(main_template, "main.tpl", None, 0).unwrap();
let child_template = r#"<p>title of this page = {{=title}}</p><p>foobar = {{=foobarvar}}"#;
let child = hairy_compile_html(child_template, "child.tpl", None, 0).unwrap();
let value = value!({
"body": child,
"foobarvar": "foobar",
"title": "my title",
}).to_vec(false);
match hairy_eval_html(main.to_ref(), value.to_ref()) {
Ok(output) => { std::io::stderr().write_all(&output).unwrap(); },
Err(err) => {
eprintln!("{}", err);
assert!(false); // to check this example
},
}