Use Rhai scripting for helpers
masterscript_helper feature flag, you can define custom helpers using the Rhai scripting language. This allows for template development and logic implementation without needing to modify and recompile your Rust code.repository·master·Indexed 23 days ago
https://github.com/sunng87/handlebars-rustA 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.
script_helper feature flag, you can define custom helpers using the Rhai scripting language. This allows for template development and logic implementation without needing to modify and recompile your Rust code.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.handlebars WASM modules include a command-line interface (CLI) tool designed to render Handlebars templates using a provided JSON input.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(())
}~) 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.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:
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(())
}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.
The Path enum represents how data is accessed within a template. It has two primary variants:
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.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).
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.
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.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(())
}),
);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).
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.
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.
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));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);