Skip to content

Fix whitespace handling for chained {{else if}} blocks#31

Merged
NullVoxPopuli merged 2 commits intohandlebars-lang:masterfrom
theodorejb:whitespace-fixes
Apr 30, 2026
Merged

Fix whitespace handling for chained {{else if}} blocks#31
NullVoxPopuli merged 2 commits intohandlebars-lang:masterfrom
theodorejb:whitespace-fixes

Conversation

@theodorejb
Copy link
Copy Markdown
Contributor

Two related bugs affecting templates that use chained {{else if}} branches:

1. Standalone close-tag indent not stripped from all chained else-if branches

When a block with {{else if}} branches was written on its own line (standalone), omitLeft only stripped the trailing indent from the first branch's body. Every other execution path kept an extra indent before the closing tag's content. The Program handler now walks the full inverse chain so all branches are treated consistently. Resolves handlebars-lang/handlebars.js#1716

This is also an issue I ran into when developing PHP Handlebars Parser, which implements the same whitespace control logic.

2. Tilde whitespace control in one branch affecting another

prepareBlock propagated closeStrip one chain level at a time during grammar reduction, so a tilde on {{~else if}} leaked into the closeStrip of the next block in the chain, causing unintended whitespace stripping in unrelated branches. The fix walks the full chain in the single outer prepareBlock call (where close is the actual closing tag) and stamps the correct closeStrip onto every block, rather than letting each intermediate reduction overwrite it with the wrong strip. Resolves handlebars-lang/handlebars.js#2031

Once this is merged I can open a separate pull request against handlebars.js adding the following test cases to spec/whitespace-control.js (which I've already confirmed pass with this branch):

it('GH-1716: should strip trailing indent from chained else if blocks', function () {
	expectTemplate(
	  "{{#if a}}\n {{#if a.b}}\n no\n {{else if a.b}}\n no\n {{else}}\n yes\n {{/if}}\n after\n{{/if}}"
	)
	  .withInput({ a: {c: true} })
	  .toCompileTo(' yes\n after\n');
});

it('GH-2031: whitespace control in one else if branch should not affect another branch', function () {
  var string = "{{#if a}}\na\n{{else if b}}\nb\n{{else if c}}\nc\n{{~else if d}}\nd\n{{else if e}}\ne\n{{else if f}}\nf{{/if}}";
	expectTemplate(string)
	  .withInput({ c: 1 })
	  .toCompileTo("c");

	expectTemplate(string)
	  .withInput({ d: 1 })
	  .toCompileTo("d\n");
	
	expectTemplate(string)
	  .withInput({ e: 1 })
	  .toCompileTo("e\n");
});

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

can tests not be added in this repo?

@theodorejb
Copy link
Copy Markdown
Contributor Author

Only AST-level tests can be added here, not tests of the expected rendering.

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

sounds valid tho

@theodorejb
Copy link
Copy Markdown
Contributor Author

@NullVoxPopuli What sounds valid?

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

adding an AST-level test 🎉 just to have something to validate against and prevent regression

When a block with chained {{else if}} branches was on its own line,
omitLeft only stripped the trailing indent from the first branch's
body. Walk the full inverse chain so every execution path loses the
close-tag indent.

Resolves handlebars-lang/handlebars.js#1716
prepareBlock propagated the closeStrip of a chained inverseAndProgram
one level at a time, so a tilde on {{~else if}} leaked into the
closeStrip of the next block in the chain, causing unintended
whitespace stripping in unrelated branches.

When close is the actual closing tag (not another chain node), walk
the full chain and set closeStrip to the real closing tag's strip on
every block, rather than letting each intermediate chain step
overwrite it with the wrong strip.

Resolves handlebars-lang/handlebars.js#2031
@theodorejb
Copy link
Copy Markdown
Contributor Author

@NullVoxPopuli I added AST tests now to validate both fixes.

@jaylinski
Copy link
Copy Markdown
Member

@NullVoxPopuli Sorry for hijacking the conversation, but I noticed that you added this parser to ember.js: emberjs/ember.js@181d95d

Which repository will be the main one, or do you maintain both independently?

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

so this repo is still for handlebars, but the PR you found for ember is exclusively for ember, and ember will not use the handlebars-parser repo anymore -- so if you're using ember, you may want to PR there, if handlebars proper, here is still a good location

@theodorejb
Copy link
Copy Markdown
Contributor Author

Does anything prevent this from being merged?

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

Do the tests def fail without the fix? <3

@theodorejb
Copy link
Copy Markdown
Contributor Author

Yes, I confirmed both the AST tests as well as the proposed Handlebars.js tests in the description fail without this patch.

@NullVoxPopuli NullVoxPopuli added the bug Something isn't working label Apr 30, 2026
@NullVoxPopuli NullVoxPopuli merged commit dbc968b into handlebars-lang:master Apr 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Whitespace control in one {{else if}} branch also affects a different branch Whitespace issue when using else if

3 participants