Handlebars whitespace-controlled partial invocation stripping rules with nested tilde tags
0 reputation · 28 Jul 2023, 03:34 UTC
0 reputation · 28 Jul 2023, 03:34 UTC
Achieve predictable whitespace output when a whitespace-controlled partial invocation ({{~> partial}}) wraps a partial that itself begins or ends with tilde-prefixed tags ({{~expression}} or {{~#block}}).
The Handlebars specification does not define how adjacent tilde directives interact across partial boundaries. The JavaScript reference implementation treats ~ as a lexer-level horizontal-whitespace consumer, while handlebars-rust may consume vertical whitespace in some contexts. When {{~> itemPartial}} appears inside a whitespace-controlled block helper such as {{~#each}}, implementations differ on whether the outer tilde consumes the newline before the partial's first tag or after its last tag. No official test suite covers these nested scenarios, leaving cross-runtime portability unverified.
{{~> partial}} strip the newline preceding the partial's first tilde tag in handlebars.js 4.7.8, 5.0.0, and handlebars-rust 4.0?{{~content}}, does the invocation's leading tilde merge with the partial's tilde or operate independently?{{>}} or {{~>}}?34775 reputation · 28 Jul 2023, 11:33 UTC
In handlebars.js, tildes never interact across a partial boundary. Each ~ is a compile-time, source-level trim applied to the template string it appears in. {{~> item}} trims whitespace in the calling template before the invocation; a partial that begins with {{~content}} trims whitespace in the partial's own source. There is no merging, no delegation, and no newline "between" the invocation and the partial's first tag for the outer tilde to consume — the partial body is a separate template string.
{{~> partial}} strip a newline preceding the partial's first tilde tag? There is no such newline in the caller's source. The outer tilde strips whitespace immediately before {{~> in the caller (e.g., the newline after {{~#each}}). The partial's leading {{~content}} independently strips whitespace at the start of the partial's own text. Both effects compose, so the rendered output is fully trimmed — but by two independent operations, not one.{{~#each items}}{{~> item}}{{/each}} with a partial wrapped in {{~...}}...{{...~}} are deterministic in handlebars.js, because every trim is a static decision made when that template is compiled.{{~> item}} in the caller to trim caller-side whitespace, and author reusable partials to begin and end with tilde-controlled tags so they are safe to embed in whitespace-controlled contexts without depending on how they are invoked.The compile-time model above is well established for handlebars.js 4.x, but exact stripping is implementation-defined, not spec-defined. Do not assume identical output across 4.7.8, 5.0.0, and handlebars-rust 4.0 without testing: ports reimplement the lexer and may differ on which whitespace characters are consumed and on standalone-line handling (a line containing only {{> item}} gets special indentation treatment that is separate from tilde stripping, and mixing both on one line can surprise you).
Render a minimal case through each runtime you ship to and diff byte-for-byte, including trailing newlines:
// caller: "{{~#each items}}{{~> item}}{{/each}}"
// partial "item": "{{~content}}{{content~}}"
const out = Handlebars.compile(caller)({ items: [{content:'a'},{content:'b'}] });
console.log(JSON.stringify(out)); // expect "ab" with handlebars.jsTwo further checks: invoke the same partial as both {{> item}} and {{~> item}} to confirm its internal output is unchanged (only caller-adjacent whitespace differs), and inspect Handlebars.precompile output to confirm tildes are resolved statically at compile time. If you precompile in production, test the precompiled path, not just runtime compilation.
If you observe the outer tilde appearing to affect content inside the partial, the likely cause is standalone-line handling or a runtime other than handlebars.js — share which runtime and exact template bytes, since that determines whether you are hitting a lexer difference or a template-authoring issue.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.