Handlebars, Glimmer and what this parser expects
Handlebars is the logic-light template language behind many email templates, static site generators and server-rendered views, and Glimmer is the stricter dialect Ember.js compiles its components from. The formatter here is Prettier with its glimmer parser, the combination Ember projects commonly use for templates. It reads both the HTML and the mustaches into one tree, so a {{#each}} inside a <table> is indented as part of the table rather than as stray text.
Because the parser is Glimmer, it is stricter than the Handlebars runtime in a few places:
- Partials are rejected.
{{> header}}stops with “Handlebars partials are not supported”, since Ember replaced partials with components. Templates for Express, Handlebars.js email builders or Ghost themes that rely on partials cannot be formatted here as they are. - HTML must be well formed. Glimmer does not apply the browser’s implied end tags, so
<div><p></div>is an error rather than something it quietly repairs. - Ember syntax is first-class. Angle-bracket components such as
<UserCard @user={{this.user}} />, named arguments, element modifiers like{{on "click" this.select}}, block paramsas |item|and sub-expressions all format correctly.
Why line breaks in the input matter
The Glimmer printer is whitespace-sensitive. A space or newline between two tags is content, because it can show up as a gap in the rendered page, so the formatter will not invent whitespace that was not there. When a template arrives as one long line with tags glued together, the output keeps them glued and breaks lines inside the tags instead, producing shapes like <td\n>. If you have a minified template, put line breaks between the block-level elements first, then format; the result will be indented the way you expect.
Whitespace control markers such as {{~#if user~}} are preserved, as are triple-stash {{{rawHtml}}} outputs.
One trap to watch for: an attribute value with a missing closing quote, as in <p class="note>, can make the output come back empty instead of producing an error. If the output pane is blank, look for an unbalanced double quote first.
Quotes, comments and settings
There are no format-specific options. Indent in the toolbar sets the indentation width or switches to tabs, and lines wrap at 80 columns. String literals inside mustaches are printed with double quotes, so {{t 'checkout.title'}} becomes {{t "checkout.title"}}; inside an attribute value they switch to single quotes so they do not clash with the attribute’s own. Long-form comments {{!-- note --}} are shortened to {{! note }} when the text does not contain }}. When a helper call such as {{format-currency item.price currency="EUR"}} would push a line past the limit, its arguments and hash pairs are placed on separate lines inside the mustache.
Templates starting with {{#if}}, {{#each}}, {{#unless}} or {{#with}} are detected automatically. Ctrl/Cmd+Enter formats the template; Ctrl/Cmd+Shift+C copies it. Formatting happens locally, so customer-facing email templates with unreleased copy are not uploaded anywhere.
Examples
Order summary with nested blocks
Each block is indented under its parent, and the single-quoted string inside the class attribute is kept.
<div class="order-summary">
<h2>Order {{order.number}}</h2>
{{#if order.items.length}}
<ul>
{{#each order.items as |item|}}
<li class="{{if item.late 'late'}}">{{item.qty}} x {{item.name}}</li>
{{/each}}
</ul>
{{else}}
<p class="empty">Your cart is empty.</p>
{{/if}}
</div><div class="order-summary">
<h2>Order {{order.number}}</h2>
{{#if order.items.length}}
<ul>
{{#each order.items as |item|}}
<li class="{{if item.late 'late'}}">{{item.qty}} x {{item.name}}</li>
{{/each}}
</ul>
{{else}}
<p class="empty">Your cart is empty.</p>
{{/if}}
</div>
Ember component with named arguments
The long opening tag is split one argument per line, with the modifier and block params kept in order.
<UserCard @user={{this.currentUser}} @onSave={{this.saveProfile}} @showAvatar={{true}} class="profile-card" {{on "click" this.select}} as |card|>
<card.title>{{this.currentUser.name}}</card.title>
</UserCard><UserCard
@user={{this.currentUser}}
@onSave={{this.saveProfile}}
@showAvatar={{true}}
class="profile-card"
{{on "click" this.select}}
as |card|
>
<card.title>{{this.currentUser.name}}</card.title>
</UserCard>
Conditional chain with else if
The else if branch is recognised as part of the same block and every branch body is indented by four spaces.
{{#if invoice.paid}}
<span class="badge ok">Paid {{format-date invoice.paidAt}}</span>
{{else if invoice.overdue}}
<span class="badge danger">Overdue by {{invoice.daysOverdue}} days</span>
{{else}}
<span class="badge">Due {{format-date invoice.dueAt}}</span>
{{/if}}{{#if invoice.paid}}
<span class="badge ok">Paid {{format-date invoice.paidAt}}</span>
{{else if invoice.overdue}}
<span class="badge danger">Overdue by {{invoice.daysOverdue}} days</span>
{{else}}
<span class="badge">Due {{format-date invoice.dueAt}}</span>
{{/if}}
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Expecting 'OPEN_INVERSE_CHAIN', 'INVERSE', 'OPEN_ENDBLOCK', got 'EOF' | A block helper such as {{#if}} or {{#each}} was opened and the template ended before its {{/if}} or {{/each}}. | Add the matching closing tag for the block that is still open. |
if doesn't match each - 1:3 | Blocks are closed in the wrong order or with the wrong name, for example {{#if}} closed by {{/each}}. | Make each closing tag name the block it closes, innermost block first. |
Expecting 'OPEN_SEXPR', 'ID', 'OPEN_ARRAY', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID' | A mustache was opened but not closed, as in <p>{{name</p>, or an empty {{ }} has nothing inside it. | Close the expression with }} and make sure it contains a path or helper name. |
Handlebars partials are not supported | The template uses {{> partialName}}. The Glimmer parser follows Ember, which has no partials. | Temporarily replace partial calls with a placeholder such as {{partial-header}} before formatting, then switch them back. |
Cannot read properties of undefined (reading 'blockParams') | The template uses a Mustache inverted section, {{^items}}…{{/items}}, which Glimmer does not understand. | Rewrite it as {{#unless items}}…{{/unless}} if the template runs on Handlebars, which supports both forms. |
Closing tag </div> did not match last open tag <p> (on line 1) | An element inside was left unclosed. Glimmer requires every tag to be closed explicitly. | Close the inner element before its parent, here by adding </p>. |
Frequently asked questions
Can it format plain Handlebars.js templates, not just Ember?
Yes, as long as they avoid partials and use well-formed HTML. Blocks, helpers, sub-expressions, whitespace control and triple-stash output all work the same in both dialects.
Why does the output have strange line breaks inside tags?
The input had no whitespace between elements, and the formatter will not add whitespace that could change the rendered page. Add line breaks between block-level elements in the input and format again.
Does it support Mustache templates?
Partly. Variables and sections such as {{#items}}…{{/items}} parse as ordinary blocks, but inverted sections written {{^items}} and partials are not Glimmer syntax and fail.
Are there options for quotes or attribute wrapping?
No. The only settings that apply are the toolbar indentation and the fixed 80-column width.