handlebars-rust

repository·master·Indexed 23 days ago

https://github.com/sunng87/handlebars-rust

A Rust implementation of the Handlebars templating language (version 6.4.3). It provides a system to isolate Rust logic from HTML, supports extensible helpers via the handlebars_helper! macro or HelperDef trait, and is compatible with WebAssembly. Key features include template inheritance, partials, a dev_mode for auto-reloading, and optional Rhai scripting for helpers. It includes a WASM CLI for rendering templates with JSON input.

Tokens
12.5K
Snippets
31
Records
68
Agent score
79%

What's inside handlebars-rust

  1. Template inheritance and partials

    master
    The library supports template inheritance (often referred to as partials). This allows you to define a skeleton page (containing elements like headers and footers) and embed specific content pages into that parent structure, promoting template reuse.
  2. Enable auto-reload with `dev_mode`

    master
    When developing templates, you can enable dev_mode. This allows handlebars-rust to automatically reload any templates or scripts that are loaded from files or directories, enabling you to see changes without restarting your Rust server.
  3. Quick Start with handlebars-rust

    master

    To use handlebars-rust, initialize a new Handlebars instance. You can either render a template string directly without registration using render_template, or register a template with a specific name using register_template_string and then render it by name using render.

    Note: This example requires serde_json for data injection.

    use handlebars::Handlebars;
    use serde_json::json;
    use std::error::Error;
    
    fn main() -> Result<(), Box<dyn Error>> {
        let mut reg = Handlebars::new();
        // render without register
        println!(
            "{}",
            reg.render_template("Hello {{name}}", &json!({"name": "foo"}))?
        );
    
        // register template using given name
        reg.register_template_string("tpl_1", "Good afternoon, {{name}}")?;
        println!("{}", reg.render("tpl_1", &json!({"name": "foo"}))?);
    
        Ok(())
    }
  4. Handle whitespace omission with tilde (`~`)

    master
    Handlebars supports whitespace control using the tilde (~) character. When a tilde is used in an expression (e.g., {{name~}} or {{~name}}), it instructs the engine to omit surrounding whitespace. The compiler handles this by producing RawString elements that include the preserved whitespace or by adjusting the template structure to omit it.
  5. Implement and use Decorators

    master

    A Decorator is used to modify the rendering environment at a specific point in a template. Decorators are invoked using the {{*name}} syntax.

    Currently, decorators share the same definition as helpers, but they are intended for side effects that affect subsequent rendering, such as:

    1. Updating context data: Changing the data available to the template for the remainder of the render process.
    2. Defining local helpers: Overriding or registering new helper behaviors that are only active from the decorator's position until the end of the template (or until unregistered).

    To implement a decorator, you can either implement the DecoratorDef trait or simply pass a closure/function that matches the expected signature.

    use handlebars::*;
    
    // Example: A decorator that updates context data
    fn update_data<'reg: 'rc, 'rc>(_: &Decorator, _: &Handlebars, ctx: &Context, rc: &mut RenderContext) 
            -> Result<(), RenderError> {
        let mut new_ctx = ctx.clone();
        {
            let mut data = new_ctx.data_mut();
            if let Some(ref mut m) = data.as_object_mut() {
                m.insert("hello".to_string(), to_json("world"));
            }
        }
        rc.set_context(new_ctx);
        Ok(())
    }
  6. Understand `ScopedJson` and `PathAndJson` abstractions

    master

    The library uses ScopedJson and PathAndJson to track the origin and location of data during template rendering:

    ScopedJson<'rc>

    Represents a JSON value with its lifecycle context:

    • Constant(&Json): A hardcoded JSON value within the template.
    • Context(&Json, Vec<String>): A reference to a value in the provided data context, including its full path (as a Vec<String>).
    • Derived(Json): An owned JSON value computed during the rendering process.
    • Missing: Represents a value that could not be found.

    PathAndJson<'rc>

    A wrapper that combines a ScopedJson with an optional relative_path. This is useful for tracking where a value sits relative to the current scope in a template.

  7. Understand the `Path` enum variants

    master

    The Path enum represents how data is accessed within a template. It has two primary variants:

    1. Relative((Vec<PathSeg>, String)): Represents a standard path traversal. It contains a vector of PathSeg (segments) and the original raw string. Example: a/b/c.
    2. Local((usize, String, String)): Represents a local variable access, often using the @ prefix. It contains:
      • usize: The traversal level (how many ../ steps were taken).
      • String: The name of the local variable.
      • String: The original raw string.

    PathSeg can be either a Named(String) segment or a Ruled(Rule) segment (representing special tokens like root or upward traversal).

  8. Configure strict mode and helper missing behavior

    master

    The Registry allows you to control how missing data or missing helpers are handled using set_strict_mode and by registering a custom helperMissing helper.

    • Strict Mode: When set_strict_mode(true) is called, attempting to render a template with a missing key (that is neither in the context nor registered as a helper) will return an Err.
    • Custom helperMissing: You can register a custom helper named helperMissing to define a fallback behavior (e.g., printing a specific message) when a helper or key cannot be resolved. This is only effective when strict mode is disabled.

    Note: By default, if a key is not found in the context and no helper exists, the engine outputs nothing.

    // Enable strict mode to catch missing keys as errors
    r.set_strict_mode(true);
    
    // Register a custom fallback for missing helpers/keys
    r.register_helper(
        "helperMissing",
        Box::new(|h, _, _, _, out| {
            let name = h.name();
            write!(out, "{name} not resolved")?;
            Ok(())
        }),
    );
  9. Implement custom helpers with HelperDef

    master

    To create custom helpers in handlebars-rust, you can implement the HelperDef trait. There are two primary ways to define a helper depending on whether you need to return a value (composable) or write directly to the output (non-composable).

    1. Inline Helpers (Composable)

    Use call_inner to implement a helper that returns a ScopedJson. These are ideal for subexpressions (e.g., {{foo value=(bar 1)}}) because the returned value maintains its type information. If you only implement call_inner, the default call implementation handles escaping and outputting the result.

    2. Complex/Output Helpers (Non-composable)

    Implement the call method directly if you need access to the Output trait to write custom strings or render child templates (like #if or #each). Note that helpers defined this way are not easily composable in subexpressions because they write raw strings to the output, and the engine must attempt to parse them back as JSON.

    3. Bare Functions

    You can use a simple function as a helper without implementing a struct, thanks to the unboxed_closure support for functions matching the helper signature.

    /// Define an inline helper
    use handlebars::*;
    
    fn upper(h: &Helper< '_>, _: &Handlebars<'_>, _: &Context, rc:
        &mut RenderContext<'_, '_>, out: &mut dyn Output) 
        -> HelperResult {
       // get parameter from helper or throw an error
       let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
       out.write(param.to_uppercase().as_ref())?;
       Ok(())
    }
    
    /// Define block helper
    fn dummy_block<'reg, 'rc>(
        h: &Helper<'rc>,
        r: &'reg Handlebars<'reg>,
        ctx: &'rc Context,
        rc: &mut RenderContext<'reg, 'rc>,
        out: &mut dyn Output,
    ) -> HelperResult {
        h.template()
            .map(|t| t.render(r, ctx, rc, out))
            .unwrap_or(Ok(()))
    }
    
    /// Define helper function using macro
    handlebars_helper!(plus: |x: i64, y: i64| x + y);
    
    let mut hbs = Handlebars::new();
    hbs.register_helper("plus", Box::new(plus));
  10. Enable recursive path resolution

    master

    By default, path resolution in templates follows standard Handlebars rules. If you need to access properties from a parent context while iterating through a child context (e.g., inside an {{#each}} block), you can enable recursive lookup using set_recursive_lookup(true).

    Without recursive lookup, an {{outer}} variable inside an {{#each children}} block would only look for outer within the individual child object. With recursive lookup enabled, it can traverse up the data tree to find outer in the parent context.

    let mut r = Registry::new();
    // Enable the ability to look up parent context variables during iteration
    r.set_recursive_lookup(true);