npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

handlebars-jle

v1.0.14

Published

Faster Handlebars templating with JSON Logic Engine

Readme

handlebars-jle

Fast Handlebars-style templates powered by json-logic-engine.

handlebars-jle parses a template directly into a JSON Logic tree. You can inspect or store that tree, execute it without dynamic code generation, or let json-logic-engine compile it into an optimized JavaScript function.

import { Handlebars } from 'handlebars-jle'

const handlebars = new Handlebars()
const render = handlebars.compile('Hello, {{name}}!')

render({ name: 'Ada' }) // "Hello, Ada!"

Why use it?

  • Fast reusable templates. Parse and compile once, then render repeatedly.
  • Portable intermediate form. Templates lower to ordinary JSON Logic data.
  • Compiled or interpreted execution. Interpreted mode works in environments that prohibit eval and new Function.
  • Synchronous and asynchronous engines. Async helpers are first-class.
  • Grammar-native whitespace handling. {{~ ... ~}} and standalone block lines are handled by the generated parser, without a preprocessing pass.
  • JSON Logic extensibility. Helpers are json-logic-engine methods and can participate in its determinism and compilation optimizations.

This is a focused Handlebars-compatible language, not a drop-in replacement for every feature and extension in Handlebars.js. See Compatibility.

Installation

npm install handlebars-jle

json-logic-engine is installed as a runtime dependency. The grammar toolkit is only used while building this package; the generated parser and its small runtime are bundled into the published output.

Core API

Compile and render

import { Handlebars } from 'handlebars-jle'

const handlebars = new Handlebars()

const render = handlebars.compile(`
{{#if account}}
  Welcome, {{name}}.
{{else}}
  Please create an account.
{{/if}}
`)

render({ account: true, name: 'Ada' })

Templates escape interpolated HTML by default. Disable that per template when the input is trusted:

const renderTrusted = handlebars.compile('{{html}}', { noEscape: true })
renderTrusted({ html: '<strong>Hello</strong>' })

Triple braces are the template-level alternative:

{{{html}}}

Compile to JSON Logic

compileToJSON() exposes the intermediate representation without building a renderer:

import { compileToJSON } from 'handlebars-jle'

compileToJSON('Hello, {{name}}!')
// {
//   cat: [
//     'Hello, ',
//     { escape: { val: 'name' } },
//     '!'
//   ]
// }

With noEscape: true, the interpolation is emitted as { val: 'name' }.

Interpreted mode

Compiled mode is the default. Interpreted mode retains the same API but runs the JSON Logic tree directly:

const handlebars = new Handlebars({ interpreted: true })
const render = handlebars.compile('Hello, {{name}}!')

render({ name: 'Ada' })

Use interpreted mode when runtime code generation is unavailable or unwanted.

Template syntax

Conditions

{{#if age}}
  {{name}} is {{age}} years old.
{{else if dateOfBirth}}
  {{name}} was born on {{dateOfBirth}}.
{{else}}
  {{name}}'s age is unknown.
{{/if}}

unless and the inverse marker ^ are also supported:

{{#unless active}}Inactive{{^}}Active{{/unless}}

Iteration

Arrays, iterable values, maps, and objects can be iterated with each:

{{#each children}}
  {{@index}}: {{name}} is {{age}}
{{/each}}
render({
  children: [
    { name: 'John', age: 5 },
    { name: 'Jane', age: 7 }
  ]
})

Inside an object or map iteration, @key identifies the current key. The special values @first and @last are available as well.

Parent contexts use ../:

{{#each teams}}
  {{#each players}}
    {{../name}}: {{name}}
  {{/each}}
{{/each}}

Block parameters are supported:

{{#each people as |person index|}}
  {{index}}: {{person.name}}
{{/each}}

with

with accepts a context value, hash arguments, or both:

{{#with person}}
  {{name}} is {{age}} years old.
{{/with}}
{{#with name='Ada' role='engineer'}}
  {{name}} is an {{role}}.
{{/with}}

Subexpressions and hash arguments

Helpers can be nested with parentheses and invoked with positional or hash arguments:

{{uppercase (default displayName name)}}
{{json (obj name=name age=age)}}

Whitespace control

Tildes trim adjacent whitespace:

Hello   {{~name}}!
{{#if ready~}}

  Ready
{{~/if}}

Standalone block-open, else, and block-close lines consume their indentation and line ending. Literal content inside the block keeps its own indentation and newlines.

Escaping a complete element with a backslash leaves it literal, including balanced nested blocks:

\{{name}}
\{{#if ready}}yes{{else}}no{{/if}}

Helpers

Helpers are methods on the underlying JSON Logic engine:

const handlebars = new Handlebars()

handlebars.engine.addMethod(
  'addOne',
  ([value]) => value + 1,
  { sync: true, deterministic: true }
)

const render = handlebars.compile('{{addOne age}}')
render({ age: 5 }) // "6"

Accurate method metadata allows json-logic-engine to optimize the surrounding tree. For a helper that accepts one unwrapped argument, its optimizeUnary option can also avoid an argument-array allocation.

The package registers template-oriented helpers and aliases including each, with, match, escape, json, obj/object, arr/array, uppercase, lowercase, truncate, isArray, and common named arithmetic/comparison aliases.

Asynchronous helpers

Use AsyncHandlebars when any helper can return a promise:

import { AsyncHandlebars } from 'handlebars-jle'

const handlebars = new AsyncHandlebars()

handlebars.engine.addMethod('fetchUsers', async ([url]) => {
  const response = await fetch(url)
  return response.json()
})

const render = await handlebars.compileAsync(`
{{#each (fetchUsers url)}}
  @{{username}} - {{name}}
{{/each}}
`)

console.log(await render({ url: 'https://example.com/users' }))

compile() is also available on AsyncHandlebars; it returns an async wrapper that resolves compilation lazily on its first call. compileAsync() resolves the compiled renderer up front and has less steady-state wrapper overhead.

If every helper is synchronous, prefer Handlebars.

Partials

Register partials on an engine instance:

const handlebars = new Handlebars()

handlebars.register('greeting', 'Hello, {{name}}!')
const render = handlebars.compile('{{>greeting name="Ada"}}')

render({}) // "Hello, Ada!"

Deterministic partials may be evaluated or inlined by json-logic-engine when the surrounding data is known. Partials also work in interpreted and asynchronous modes.

Inline partials use a block instead of Handlebars decorator syntax:

{{#inline "greeting"}}Hello, {{name}}!{{/inline}}
{{>greeting name="Ada"}}

Inline partial registration currently belongs to the engine rather than a lexically isolated template scope. Treat this feature as experimental when unrelated templates share one engine instance.

Execution modes

| Class | Option/method | Result | | --- | --- | --- | | Handlebars | default | Compiled synchronous renderer | | Handlebars | { interpreted: true } | Interpreted synchronous renderer | | AsyncHandlebars | compile() | Lazily compiled async renderer | | AsyncHandlebars | compileAsync() | Promise of a compiled async renderer | | AsyncHandlebars | { interpreted: true } | Interpreted async renderer |

Content Security Policy (CSP)

Strict CSP configurations commonly omit 'unsafe-eval' from script-src, which prevents runtime code generation through eval or new Function. Use interpreted mode in those environments:

import { Handlebars } from 'handlebars-jle'

const handlebars = new Handlebars({ interpreted: true })
const render = handlebars.compile('Hello, {{name}}!')

render({ name: 'Ada' })

In interpreted mode, compile() still parses the template into JSON Logic, but the returned renderer evaluates that tree with engine.run() instead of generating JavaScript. Registered partials follow the same interpreted path. Async templates are CSP-compatible as well when the async engine is explicitly placed in interpreted mode:

import { AsyncHandlebars } from 'handlebars-jle'

const handlebars = new AsyncHandlebars({ interpreted: true })
handlebars.engine.addMethod('awaitableHelper', async ([value]) => value)
const render = handlebars.compile('{{awaitableHelper value}}')

await render({ value: 42 })

The default compiled mode uses json-logic-engine's dynamic compiler and therefore requires a policy that permits runtime code generation. AsyncHandlebars alone does not change that: compileAsync() is compiled mode unless the instance was created with { interpreted: true }.

CSP compatibility is not a sandbox for untrusted templates. A template can invoke helpers registered on its engine, so applications accepting templates from untrusted authors should expose only an intentionally restricted helper set and validate their broader security model separately.

Compatibility

The commonly used interpolation, triple-brace, helper, subexpression, hash argument, if/unless, else if, each, with, partial, block-parameter, parent traversal, and whitespace-control forms are implemented and covered by the test suite.

Known differences from Handlebars.js include:

  • This package emits JSON Logic rather than the Handlebars compiler AST.
  • It intentionally includes additional JSON Logic-oriented helpers and aliases.
  • with accepts hash arguments as a convenient way to construct a context.
  • Inline partials use {{#inline ...}} and are currently engine-scoped.
  • An ancestor iterator's metadata is addressed as ../@index rather than @../index.
  • Block parameters use recursive lookup and can inhibit some inline compiler optimizations.
  • Custom block helpers are json-logic-engine methods; Handlebars.js helper and decorator APIs are not drop-in interfaces.

If exact Handlebars.js edge-case compatibility is a requirement, validate the templates your application uses before migrating.

Performance

The repository includes a public benchmark suite comparing handlebars-jle with Handlebars.js and Kibana's kbn-handlebars. It covers interpolation, branches, iteration, context traversal, partials, and a substantial YAML template—not just a single favorable expression.

Selected results from a Node run of one million renders, with each template compiled once before its render loop:

| Benchmark | Compiled JLE vs Handlebars.js | Interpreted JLE vs Handlebars.js | | --- | ---: | ---: | | SimpleEach | 30.9× faster | 8.8× faster | | Nested traversal | 26.5× faster | 9.6× faster | | Broad feature example | 21.9× faster | 3.4× faster | | Large YAML template | 28.9× faster | 4.0× faster | | Partials | 92.1× faster | 5.8× faster | | @first / @last iteration | 37.9× faster | 5.7× faster |

The smallest cases can produce much larger ratios because json-logic-engine can collapse and inline more of the work; they are deliberately omitted from the highlights above. Async mode is benchmarked too, but its promise overhead makes it most relevant when a template actually needs asynchronous helpers.

These numbers are highlights from one machine, not portable guarantees. The relative result varies with the template, data, Node version, and warmup. The benchmark models the intended usage: compile once and reuse the renderer.

Run the repository benchmarks with:

npm run bench:parser
cd bench && node --expose-gc index.js

Set BENCH_ITER to change the default one-million-render sample size.

Development

The source grammar lives in parser/handlebars.gram. The generated parser is intentionally ignored by Git and recreated before tests and production builds.

npm install
npm test
npm run build

The production bundles include the generated parser runtime and leave json-logic-engine external as the package dependency.

License

MIT