Skip to content

Instantly share code, notes, and snippets.

@alexsavio
Created October 21, 2023 07:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexsavio/7192457f48cc19544e00585d3a434fec to your computer and use it in GitHub Desktop.
Save alexsavio/7192457f48cc19544e00585d3a434fec to your computer and use it in GitHub Desktop.
A blog post on how to write Handlebars.rs helpers

Author: Alexandre Manhães Savio alexsavio@gmaill.com

Date: 10.10.2023

Handlebars is a modern and extensible templating solution originally created in the JavaScript world. It’s used by many popular frameworks like Ember.js and Chaplin. It’s also ported to some other platforms such as Java.

Handlebars is a template rendering engine but lacks extensive filters and transformers for context data during rendering. Like many such libraries, Handlebars allows you to create custom operations, called 'helpers'.

Here I will write an instruction set on how to write custom helpers for handlebars-rust.

Types of helpers

There are 2 types of template helpers: inline and block helpers. The inline helpers are the template operators to transform variables or calculate conditions based on them. Block helper is like #if or #each which has a inner template and an optional inverse template (the template in the else branch).

How to write custom helpers

You can also create your own helpers with Rust. There are 4 ways of implementing template helpers:

  1. using a structure impl HelperDef,
  2. using a bare function,
  3. using the handlebars_helper , and
  4. using the rhai scripting language.

I am going to focus here on 1 because I like structs, and 3 because it's straightforward helpers for variable transformation.

How to use the handlebars_helper macro

In most cases you just need some simple function to call from templates to transform or do simple computation on variables. For that you can use the handlebars_helper! macro.

use handlebars::*;

let mut hbs = Handlebars::new();

// integer addition
handlebars_helper!(plus: |x: i64, y: i64| x + y);
hbs.register_helper("plus", Box::new(plus));

// string operations
handlebars_helper!(to_upper_case: |v: str| v.to_uppercase());
handlebars.register_helper("to_upper_case", Box::new(to_upper_case))

How to use create impl HelperDef

You can implement the HelperDef trait to define your own custom helpers. The handlebars_helper! macro uses it.

pub trait HelperDef {
    // Provided methods
    fn call_inner<'reg: 'rc, 'rc>(
        &self,
        _: &Helper<'reg, 'rc>,
        _: &'reg Registry<'reg>,
        _: &'rc Context,
        _: &mut RenderContext<'reg, 'rc>
    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> { ... }
    fn call<'reg: 'rc, 'rc>(
        &self,
        h: &Helper<'reg, 'rc>,
        r: &'reg Registry<'reg>,
        ctx: &'rc Context,
        rc: &mut RenderContext<'reg, 'rc>,
        out: &mut dyn Output
    ) -> HelperResult { ... }
}

By default, you can use a bare function as a helper definition because it supports unboxed_closure. If you have stateful or configurable helper, you can create a struct to implement HelperDef.

I would suggest you to use the HelperDef trait in case you want to do something more intricate with regards variable states ( like the example below for isdefined), or if you want to access global template context to transform list or map variables for instance.

pub struct IsDefined;

impl HelperDef for IsDefined {
    fn call_inner<'reg: 'rc, 'rc>(
        &self,
        h: &Helper<'reg, 'rc>,
        _: &'reg Handlebars,
        _: &'rc Context,
        _: &mut RenderContext<'reg, 'rc>,
    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
        let params = h.params();
        if params.len() != 1 {
            return Err(RenderError::new(
                "isdefined: requires one parameter".to_owned(),
            ));
        }
        let result = h.param(0)
            .and_then(|x| {
                if x.is_value_missing() {
                    Some(false)
                } else {
                    Some(true)
                }
            })
            .ok_or_else(|| RenderError::new("isdefined: Couldn't read parameter".to_owned()))?;

        Ok(ScopedJson::Derived(JsonValue::from(result)))
    }
}

handlebars.register_helper("isdefined", Box::new(IsDefined));

You can retrieve useful information from the HelperDef trait arguments.

  • &Helper: current helper template information, contains name, params, hashes and nested template
  • &Registry: the global registry, you can find templates by name from registry
  • &Context: the whole data to render, in most case you can use data from Helper
  • &mut RenderContext: you can access data or modify variables (starts with @)/partials in render context, for example, @index of #each. See its document for detail.
  • &mut dyn Output: where you write output to

How to test your custom helpers

Helpers are transformation functions used in your templates. It can be useful to test that your helpers are giving the results your expect. Here is how you can do it to test the helpers based on macros:

pub fn register_helpers(handlebars: &mut Handlebars) {
    {
        handlebars_helper!(to_lower_case: |v: str| v.to_lowercase());
        handlebars.register_helper("to_lower_case", Box::new(to_lower_case))
    }
    {
        handlebars_helper!(to_upper_case: |v: str| v.to_uppercase());
        handlebars.register_helper("to_upper_case", Box::new(to_upper_case))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;

    use handlebars::{no_escape, Handlebars};

    pub fn new_handlebars<'reg>() -> Handlebars<'reg> {
        let mut handlebars = Handlebars::new();
        handlebars.set_strict_mode(true);
        handlebars.register_escape_fn(no_escape);
        register_helpers(&mut handlebars);
        handlebars
    }

    #[macro_export]
    macro_rules! assert_renders {
        ($($arg:expr),+$(,)?) => {{
            use std::collections::HashMap;
            let vs: HashMap<String, String> = HashMap::new();
            let mut handlebars = new_handlebars();
            $({
                let sample: (&str, &str) = $arg;
                handlebars.register_template_string(&sample.0, &sample.0).expect("register_template_string");
                assert_eq!(handlebars.render(&sample.0, &vs).expect("render"), sample.1.to_owned());
            })*
            Ok(())
        }}
    }

    #[test]
    fn test_register_string_helpers() -> Result<(), Box<dyn Error>> {
        assert_renders![
            (r##"{{ to_lower_case "Hello foo-bars" }}"##, r##"hello foo-bars"##),
            (r##"{{ to_upper_case "Hello foo-bars" }}"##, r##"HELLO FOO-BARS"##)
        ]
    }
}

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment