Expand description
The hairy
crate provides text templates, not unlike mustache and handlebars (and the earlier ctemplate original), but a bit different, focussing on error handling.
Scoping is different: variables must specify explicitly which scope they use (by using a.b
syntax). This is to avoid implicit behaviour when variables are (accidentally) overwritten.
To catch errors early on, optionally types can be added, which are checked compile time.
All errors are treated as hard errors and are reported (including a stack trace).
So missing values and typing errors are not silently ignored.
Also, the templates support evaluation: {{=expression}}
.
The supported expressions are from the expry
crate, which allows executing expression on binary encoded JSON-like values (with support for defining custom functions).
Auto-escaping is applied to the output to avoid security issues (such as letting user input be exected as javascript).
§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 eitherinline
(value is replaced during compilation of the template) ordefault
(which is a default value that is added to the evaluation time value if the field does not exists). 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 thenull
value is considered a nop (without generating an error). Other values are considered an error, except when thesjs
(script-js) orjs
escape modes are used. - Boolean conditionals with
{{if expr}}contents{{end}}
. Contents is displayed if value evaluates totrue
. Keywordselse
andelseif
are also supported:{{if expr1}}a{{elseif expr2}}b{{else}}c{{endif}}
. Ifexpr
istrue
the then branch is outputted, ifnull
orfalse
the else branch is taken, otherwise an error is generated. - Let conditionals with
{{if let x = expr}}a{{end}}
. Ifexpr
is nonnull
contents is displayed, otherwise (on non error) the else branch is displayed.else
andelseif
are supported. - Iterators with
{{for variable in name}}content{{endfor}}
, which can be used to iterate over (arrays of) any type. The contents of the array is available in the loop body under keyvariable
. Ifvariable
is of the form(i,v)
, then the current index number is stored in the variablei
. - Template definition with
{{define name}}content{{enddefine}}
. Templates can have optional default values (in an object) with{{define name defaults object}}content{{enddefine}}
. 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 thewith
syntax is used, only the data specified invalue
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: missing fields are always considered an error.
Errors can be suppressed with the
expr ??? alternative
try syntax (on error inexpr
,alternative
will be executed). Shorthand forexpr ??? null
isexpr ???
, which can be used in loops with{{for i in someField???}}
, which ignores errors if the field is not found. Same for conditionals.
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 hairy
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.
Note that if extra variables are introduced with a loop or a if let
, they do not overwrite
the field with the same name in the this
object. The original value can be retrieved by using
this.name
if name
was given another value.
§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. hairy
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 expry_macros::*;
use std::io::Write;
let template = r#"foobar = {{=this.foovar .. this.barvar}}"#;
let mut options = HairyOptions::new();
let value = value!({
"foovar": "foo",
"barvar": "bar",
}).encode_to_vec();
options.set_named_dynamic_values(&[("this",value.to_ref())]);
let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
match result {
Ok(parsed) => {
match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
Ok(output) => { std::io::stdout().write_all(&output); },
Err(err) => { eprintln!("{}", err); },
}
},
Err(mut err) => {
eprintln!("{}", err);
panic!(); // 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))()}}
.
use hairy::*;
use expry::*;
use expry_macros::*;
use std::io::Write;
let main_template = r#"<html><title>{{=value.title}}</title><body>{{call ((value.body))({value.title,value.foobarvar})}}</body></html>"#;
let mut options = HairyCompileOptions::new();
options.set_dynamic_value_name_and_types(&[("value",expry_type!("{title:string,body:string,foobarvar:string}"))]);
let main = hairy_compile_html(main_template, "main.tpl", &options).unwrap();
let child_template = r#"<p>title of this page = {{=this.title}}</p><p>foobar = {{=this.foobarvar}}"#;
options.set_dynamic_value_name_and_types(&[("this", expry_type!("{title:string,foobarvar:string}"))]);
let child = hairy_compile_html(child_template, "child.tpl", &options).unwrap();
let value = value!({
"body": child,
"foobarvar": "foobar",
"title": "my title",
}).encode_to_vec();
let mut options = HairyEvalOptions::new();
options.values = vec![value.to_ref()];
match hairy_eval_html(main.to_ref(), &options) {
Ok(output) => { std::io::stderr().write_all(&output).unwrap(); },
Err(mut err) => {
eprintln!("{}", err);
panic!(); // to check this example
},
}
Structs§
- Reference to expry expression bytecode.
- Self-contained expry expression bytecode.
- Default escaper. Defaults to escaping in
html
mode. Currently supportshtml
,url
, andnone
(throws error on other escape modes). - Reference to a expry value.
- Self-contained expry value (in encoded form).
- Recognizes expressions by counting the
{
,}
,[
,]
,(
,)
, and"
. Does not support advanced string literals such as raw strings (e.g.r##" " "##
). Stops at first]
or}
or)
encountered that is not started inside the expression. - Hairy errors, including a stack trace so debugging is easier.
- Used for stack traces, contains the details of a call site.
- An provided implementation of the custom function handler, that directly throws an error when invoked.
- This spanner will try to recognize the content of a Hairy template (
{{..}}
). It counts the number of nested{
and}
. It does have support for recognizing embedded strings, so{{foo != "}"}}
will work. Raw strings or the like are not supported, instead you can use{foo != r#" \" "#}
. Stops at first}
encountered that is not started inside the expression.
Enums§
- Used for stack traces, to make distinction between call sites.
- The errors that can occur during Hairy template parsing. It can either be a lexer error (so a part of the input can not be translated to a token), or a parser error (a different token expected). As Hairy templates can contain expressions, these expressions can also trigger errors. These can either be in the parsing of the expressions, or during optimizing (which causes EvalErrors). Other errors are reporting using
Other
.
Traits§
Functions§
- Converts a Hairy template (in
reader
) to bytecode that can be evaluation later. To help debugging during later evaluation, afilename
can be added to the bytecode. A MemoryScope is used to reduce the number of allocated strings. The result stays valid as long as the MemoryScope is in the scope (forced by the lifetime system of Rust). The globals and custom functions are used when evaluation the defaults (for template calls and such). - The ‘easy’ interface if you just want to quickly use a HTML template, with auto escaping of the input, and returning a nicely formatted error that can be presented to the user. For more options, see the
hairy_compile
function. - Evaluate Hairy bytecode to generate output, using a
value
encoded as aexpry
object. - The ‘easy’ interface if you just want to quickly use a HTML template, with auto escaping of the input, and returning a nicely formatted error that can be presented to the user. For more options, see the
hairy_eval
function.