Skip to content

Engine: Strip whitespace between consecutive end tags after expression blocks#1492

Merged
marcoroth merged 11 commits intomarcoroth:mainfrom
joelhawksley:block-bug
Mar 27, 2026
Merged

Engine: Strip whitespace between consecutive end tags after expression blocks#1492
marcoroth merged 11 commits intomarcoroth:mainfrom
joelhawksley:block-bug

Conversation

@joelhawksley
Copy link
Copy Markdown
Contributor

@joelhawksley joelhawksley commented Mar 24, 2026

Fix: Strip whitespace between consecutive <% end %> tags after expression blocks

Co-written with Claude Opus 4.6 - I've reviewed the fix and generally think it's right, but there might be a simpler solution?

Problem

Herb emits @output_buffer.safe_append= for whitespace between consecutive <% end %> tags that Erubi (with trim: true) strips. This breaks CaptureHelper#capture in Rails 8.2 for helpers like form_tag that return HTML via the block's return value rather than calling concat.

When Herb adds @output_buffer.safe_append=' ' between end statements:

  1. The block's Ruby return value changes from the form HTML to @raw_buffer (the return of safe_append=)
  2. The capture buffer gets whitespace (e.g. " ")
  3. buffer.presence returns nil (whitespace is .blank?)
  4. The value fallback is just @raw_buffer (which only contains whitespace)
  5. The form HTML is completely lost

Triggering template:

<% outer do %>
  <% form_tag("/t") do %>
    <%= tag.p do %>
      text
    <% end %>
  <% end %>
<% end %>

Herb compiled (before fix):

    end )
 _buf << '  '.freeze; end; _buf << '\n'.freeze; end

Erubi compiled (and Herb after fix):

    end )
   end 
 end

Root Cause

In preceding_token_ends_with_newline?, :expr_block_end was grouped with expression tokens in a blanket return false list. This prevented at_line_start? from recognizing that a <% end %> tag following an expression block's closing end is at line start — so the subsequent code tag was treated as inline, and the whitespace between them was emitted as buffer appends instead of being trimmed.

Fix

Remove :expr_block_end from the blanket return false list and instead check whether the expr_block_end token's value actually ends with "\n" (which it does when the end tag was on its own line). This lets at_line_start? correctly trigger trim behavior for subsequent code-only tags.

Comment thread lib/herb/engine/compiler.rb Outdated
@marcoroth marcoroth changed the title whitespace between consecutive end tags after expression block is trimmed Engine: whitespace between consecutive end tags after expression block is trimmed Mar 24, 2026
@joelhawksley joelhawksley marked this pull request as ready for review March 25, 2026 17:00
@joelhawksley joelhawksley requested a review from marcoroth March 25, 2026 17:01
@joelhawksley
Copy link
Copy Markdown
Contributor Author

@marcoroth green!

marcoroth added a commit that referenced this pull request Mar 26, 2026
This pull request adds a new `--trim` CLI flag for the `compile` and
`render` commands in the `herb` Ruby CLI. When invoked with the `--trim`
flag, the CLI passes `trim: true` to the `Herb::Engine.new` call when
compiling and rendering a template.

Discovered while reviewing #1492.
marcoroth added a commit that referenced this pull request Mar 26, 2026
This pull request introduces a new `enforce_actionview_erubi_equality`
keyword argument for the `assert_evaluated_snapshot` test helper. This
option makes sure the `Herb::Engine` evaluated output matches the
`Erubi::Engine` evaluated output through ActionView.

The `enforce_actionview_erubi_equality` is automatically enabled when
`enforce_erubi_equality` is being passed, so this now runs automatically
on all existing test that have `enforce_erubi_equality: true`.

If, for some reason, the output should differ we can opt-out of
`enforce_actionview_erubi_equality` by doing the following:
```ruby
assert_evaluated_snapshot(template, {}, enforce_erubi_equality: true, enforce_actionview_erubi_equality: false)
```

Obviously, just passing `enforce_actionview_erubi_equality: true` also
works. In that case we only enforcing the equality of the ActionView
evaluated engine output:

 ```ruby
assert_evaluated_snapshot(template, {},
enforce_actionview_erubi_equality: true)
```

Introducing this option allows us to properly test #1492.
Comment thread test/engine/whitespace_trimming_test.rb
joelhawksley and others added 4 commits March 26, 2026 08:54
Co-authored-by: Marco Roth <marco.roth@intergga.ch>
Signed-off-by: Joel Hawksley <joelhawksley@github.com>
@joelhawksley joelhawksley requested a review from marcoroth March 26, 2026 17:27
@marcoroth marcoroth changed the title Engine: whitespace between consecutive end tags after expression block is trimmed Engine: Fix whitespace trimming between consecutive end tags after expression blocks Mar 27, 2026
@marcoroth marcoroth changed the title Engine: Fix whitespace trimming between consecutive end tags after expression blocks Engine: Strip whitespace between consecutive end tags after expression blocks Mar 27, 2026
@marcoroth marcoroth merged commit 9cf49bb into marcoroth:main Mar 27, 2026
24 checks passed
@marcoroth
Copy link
Copy Markdown
Owner

Thank you @joelhawksley! 🙏🏼

@marcoroth marcoroth added this to the v1.0.0 milestone Mar 27, 2026
marcoroth pushed a commit that referenced this pull request Mar 30, 2026
… tag (#1493)

In addition to #1492, this PR fixes:

```html+erb
A<%= -%>
 <% if true %>
B
<% end %>
```

| Engine | Output     |
| ------- | --------- |
| Erubi    | "AB\n"     |
| Herb    | "A \nB\n" |
@joelhawksley joelhawksley deleted the block-bug branch March 30, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants