All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
1.2.3 Hoisted Closures - 2026-04-10
- Improved rendering performance by hoisting the closures for block bodies and
{{else}}clauses, avoiding unnecessary re-allocation on repeated invocations.
- Failure to call ambiguous helper (e.g.
{{foo}}) instrictmode. blockParamscount inHelperOptionsfor inverse helper calls.
1.2.2 Faithful Dispatch - 2026-04-05
- Better aligned compiler and runtime structure with Handlebars.js,
fixing numerous edge cases related to helpers and
@datavariables.
../expressions inside{{else}}blocks of{{#if}},{{#unless}},{{#with}}, and sections invokingblockHelperMissingresolved to the wrong context level.- A missing helper called via multi-segment path in a subexpression or
@datavariable failed to invokehelperMissing. - A non-function context property used as a helper (e.g.
{{foo "arg"}}wherefoois not a closure) incorrectly calledhelperMissingrather than throwing a distinct error. - No error thrown when calling a missing helper via a multi-segment path with arguments (e.g.
{{foo.bar "arg"}}). - Closures in context data could not be used as block helpers (e.g.
{{#fn}}...{{/fn}}wherefnis a closure). - Closures in context data or
@datavariables failed to be passedHelperOptionsas the last argument in certain cases. - Templates with hash arguments on complex paths (e.g.
{{foo.bar arg=val}}) were not compiled correctly. - Closures in context data were not invoked when accessed via a multi-segment path (e.g.
{{foo.bar}}), or via a literal path (e.g.{{"foo"}}) inknownHelpersOnlymode. @datavariables incorrectly took priority over helpers with the same name.knownHelpersOnlywas not enforced for@dataexpressions or complex paths used with arguments.
1.2.1 Optimal Simplification - 2026-04-02
- Updated to PHP Handlebars Parser 2.0, which removed unnecessary options state from the parser and made it possible to reuse the same parser instance when compiling multiple templates (e.g. for runtime partials). The PHP Handlebars API hasn't changed, but it now performs better and has significantly lower memory usage when compiling two or more templates.
Options,HelperOptions, andSafeStringare nowfinal, since there's no reason to ever extend them.
- Unnecessary internal
StringObjectclass.
1.2.0 Data Frames - 2026-03-30
Handlebars::createFrame(): creates a child@dataframe inheriting fields from a parent frame, equivalent toHandlebars.createFrame()in Handlebars.js.
- To align with Handlebars.js,
@datavariables passed tofn()orinverse()by block helpers are no longer automatically merged with parent data and@root. For example, if a helper callsfn()with['data' => ['index' => 0]]as the second parameter,@indexwill now be the only@datavariable inside the block. To set@-prefixed variables while still inheriting parent@datavariables, callHandlebars::createFrame($options->data)to create an isolated child frame. Then assign new keys to it before passing it to thedataoption offn()orinverse(). Handlebars::escapeExpression()now usesstrtr()instead ofstr_replace()for better performance.
- Block param path lookups and literal path lookups (e.g.
{{"foo"}},{{#"foo"}}) instrictmode no longer incorrectly throw when the key exists but its value isnull. - Inline partials defined inside an
{{else}}block no longer leak into the surrounding scope.
1.1.0 Dynamic Partial Resolution - 2026-03-26
HelperOptions::hasPartial(): check whether a named partial is registered at runtime.HelperOptions::registerPartial(): register a compiled partial closure from within a helper, enabling the same lazy-loading pattern asHandlebars.registerPartial()in Handlebars.js (#5, zordius/lightncandy#296).
- Nested
{{> @partial-block}}calls from runtime partials. - Failover rendering for
{{> partial}}fallback{{/partial}}blocks where the partial is also called conditionally earlier in the template. isset($options->fn)andisset($options->inverse)now correctly returntruefor all block helper calls, even when the block is inverted or lacks an{{else}}clause.- Closures at complex paths without any arguments (e.g.
{{#obj.fn}}) are no longer passed aHelperOptionsargument (matching Handlebars.js behavior). - Inverted sections with literal block paths (e.g.
{{^"foo"}}) now correctly route throughblockHelperMissing. - With
knownHelpersOnlyenabled, inverted sections now correctly skip dispatch to unregistered runtime helpers. ../expressions inside an{{else}}body now correctly resolve to the block helper's scope when there is no enclosing block context..lengthlookup on block param variables and instrictmode.
1.0.1 Root SubExpression - 2026-03-24
- Support for sub-expressions that are
PathExpressionroots (e.g.{{(my-helper foo).bar}}). - Compilation of multi-segment
if/unlessconditions (#15). - Helper argument handling in
strictmode. assumeObjectserrors now align better with Handlebars.js.
1.0.0 AST Compiler - 2026-03-22
Rewrote the parser and compiler to use an abstract syntax tree, based on the same lexical analysis and grammar specification as Handlebars.js. This eliminates a large class of edge cases and parsing bugs that the old regex-based approach failed to handle correctly.
This release is 35-40% faster than v0.9.9 and LightnCandy at compiling and executing complex templates, and uses almost 30% less memory. The code is also significantly simpler and easier to maintain.
- Support for nested inline partials.
- Support for closures in data and helper arguments.
helperMissingandblockHelperMissinghooks: handle calls to unknown helpers with the same API as in Handlebars.js, replacing the oldhelperResolveroption.knownHelperscompile option: tell the compiler which helpers will be available at runtime for more efficient execution (helper existence checks can be skipped).assumeObjectscompile option: a subset ofstrictmode that generates optimized templates when the data inputs are known to be safe.- Support for deprecated
{{person/firstname}}path expressions for parity with Handlebars.js (avoid using this syntax in new code, though).
- Custom helpers must now be passed at runtime when invoking a template (via the
helpersruntime option key), rather than via theOptionsobject passed tocompileorprecompile. This is a significant optimization, since it eliminates the overhead of reading and tokenizing PHP files to extract helper functions. It also enables sharing helper closures across multiple templates and renders, and removes limitations on what they can access and do (e.g. it resolves zordius/lightncandy#342). - Exceptions thrown by custom helpers are no longer caught and re-thrown, so the original exception can now be caught in your own code for easier debugging (#13).
- The
partialResolverclosure signature no longer receives an internalContextargument. Now only the partial name is passed. knownHelpersOnlynow works as in Handlebars.js, and an exception will be thrown if the template uses a helper which is not in theknownHelperslist.- Updated various error messages to align with those output by Handlebars.js.
Options::$helpers: instead pass custom helpers when invoking a template, using thehelperskey in the runtime options array (the second argument to the template closure).Options::$helperResolver: use thehelperMissing/blockHelperMissingruntime helpers instead.
- Fatal error with deeply nested
else ifusing custom helper (#2). - Incorrect rendering of float values (#11).
- Conditional
@partial-blockexpressions. - Support for
@partial-blockin nested partials (zordius/lightncandy#292). - Ability to precompile partials and pass them at runtime (zordius/lightncandy#341).
- Fatal error when a string parameter to a partial includes curly braces (zordius/lightncandy#316).
- Behavior when modifying root context in a custom helper (zordius/lightncandy#350).
- Escaping of block params and partial names.
- Inline partials defined inside a
{{#with}}or other block leaking out of that block's scope after it closes. - Numerous other bugs related to scoping, block params, inverted block helpers, section iteration, and depth-relative paths.
0.9.9 Stringable Conditions - 2025-10-15
- Allow
Stringablevariables inifstatements (#8).
- Raw lookup when key doesn't exist (#3).
- Spacing and undefined variable for each block in partial (#7).
0.9.8 String Escaping - 2025-05-20
Handlebars::escapeExpression()method (equivalent to theHandlebars.escapeExpression()utility function in Handlebars.js).
- Unnecessary
$escapeparameter on SafeString constructor.
- Nested else if validation (fixes zordius/lightncandy#313).
- Escaping multiple double quotes (fixes zordius/lightncandy#298).
- Single-quoted string parsing and compiling.
0.9.7 Resolvers - 2025-05-04
helperResolverandpartialResolvercompile options for dynamic handling of partials and helpers.
0.9.6 Partial Indentation - 2025-04-20
- Indentation of nested partials (fixes zordius/lightncandy#349).
- Parsing hash options containing line breaks (fixes zordius/lightncandy#310).
- Parameter type error in strict mode.
- Parsing raw block helper params.
0.9.5 Block Parameter Parsing - 2025-03-30
- Parsing block parameters with extra surrounding whitespace (fixes zordius/lightncandy#371).
0.9.4 String Arguments - 2025-03-23
- Parsing single-quoted string arguments (fixes zordius/lightncandy#281, zordius/lightncandy#357, zordius/lightncandy#367).
0.9.3 Raw Block Parsing - 2025-03-20
- Correctly parse handlebars after raw block (fixes zordius/lightncandy#344).
0.9.2 Arrow Function Helpers - 2025-03-19
- Support for arrow function helpers (fixes zordius/lightncandy#366).
- Parse error when using length with
@root(from zordius/lightncandy#370).
0.9.1 Better Return Type - 2025-03-18
- Detailed return annotation for
compile()method.
0.9.0 Modern Cleanup - 2025-03-18
Initial release after forking from LightnCandy 1.2.6.
- New
compilemethod which takes a template string and options and returns an executableClosure.
- PHP 8.2+ is now required.
- Replaced compile options array with
Optionsobject. - Replaced helper options array with
HelperOptionsobject. - Renamed old
compilemethod toprecompile. - Replaced
preparemethod with much fastertemplatemethod, and removed dependency on URL include and filesystem write access.
- Rendering data in
{{else}}of{{#each}}(from zordius/lightncandy#369). - Parsing strings with escaped quotes and parentheses (based on zordius/lightncandy#358).
- Argument count for built-in helpers is now validated.
- Custom autoloader.
- Used feature tracking.
- Option to change delimiters.
partialresolveroption.compilePartialmethod.prepartialcallback option.renderexoption to inject compiled code.- Option to change runtime class.
- HTML documentation.
- Dozens of unnecessary feature flags.