Crate hairy

source ·
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 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 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 the null value is considered a nop (without generating an error). Other values are considered an error, except when the sjs (script-js) or js escape modes are used.
  • Boolean conditionals with {{if expr}}contents{{end}}. Contents is displayed if value evaluates to true. Keywords else and elseif are also supported: {{if expr1}}a{{elseif expr2}}b{{else}}c{{endif}}. If expr is true the then branch is outputted, if null or false the else branch is taken, otherwise an error is generated.
  • Let conditionals with {{if let x = expr}}a{{end}}. If expr is non null contents is displayed, otherwise (on non error) the else branch is displayed. else and elseif 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 key variable. If variable is of the form (i,v), then the current index number is stored in the variable i.
  • 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 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: missing fields are 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. 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 supports html, url, and none (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