Skip to content

Formatter: fix attribute spacing lost on elements nested in ERB blocks - #2148

Open
vjymisal0 wants to merge 2 commits into
marcoroth:mainfrom
vjymisal0:fix-erb-content-preserving-attribute-spacing
Open

Formatter: fix attribute spacing lost on elements nested in ERB blocks#2148
vjymisal0 wants to merge 2 commits into
marcoroth:mainfrom
vjymisal0:fix-erb-content-preserving-attribute-spacing

Conversation

@vjymisal0

Copy link
Copy Markdown

Summary

Fixes #2142.

When the formatter encounters a content-preserving element (e.g. <pre>) whose body contains an ERB block or conditional, it falls back to IdentityPrinter to reconstruct that ERB-wrapped subtree byte-for-byte (javascript/packages/formatter/src/format-printer.tsvisitContentPreservingBody).

IdentityPrinter.visitHTMLOpenTagNode (javascript/packages/printer/src/identity-printer.ts) wrote the tag name and then its attribute children back-to-back with no separator. Without the parser's track_whitespace: true option (which the formatter does not pass), the AST simply has no node for the whitespace between a tag name and its first attribute, or between attributes — that gap isn't represented at all. So any HTML element with attributes nested inside an ERB block lost its attribute spacing:

<pre><% if condition %><span class="x">x</span><% end %></pre>

formatted to:

<pre><% if condition %><spanclass="x">x</span><% end %></pre>

This isn't just a cosmetic diff — <spanclass="x"> is a different (bogus custom) element than <span class="x">, so the formatter silently changes rendered output.

Root cause

Confirmed by inspecting the parsed AST directly: Herb.parse(source) (default options, no track_whitespace) returns open_tag.children containing only the HTMLAttributeNodes — there is no node covering the single space between the tag name and the first attribute, or between attributes. IdentityPrinter assumed children could be printed contiguously, which only holds when track_whitespace: true is used (in that mode an explicit WhitespaceNode fills the gap).

Fix

IdentityPrinter.visitHTMLOpenTagNode now tracks the end position of the last thing it wrote and compares it against each child's start position. If there's a gap (no node covers it — the track_whitespace: false case), it writes a single separating space. If the positions already line up (an explicit WhitespaceNode already covers the gap, i.e. track_whitespace: true), nothing extra is written.

How I found and verified this

  • Reproduced against the published @herb-tools/formatter@0.10.3 + @herb-tools/node-wasm@0.10.3 packages with the exact example from Formatter: Attribute spacing is lost inside ERB blocks in pre elements #2142 — confirmed the <spanclass="x"> corruption.
  • Traced the parsed AST (with and without track_whitespace) to find the actual gap in node coverage described above.
  • Applied the same fix to the bundled/published printer code and re-ran the repro — output now round-trips correctly, including multi-attribute elements, ERB attribute splats (<div <%= attrs %>>), and interpolated attribute values, and formatting is idempotent (format(format(x)) === format(x)) in every case I tried.
  • Applied the source fix to this branch and ran the actual test suites locally:
    • javascript/packages/printer: vitest run — 116/116 real assertions pass (unchanged from baseline; the 45 failing test files are pre-existing, unrelated to this change — they fail to import in this environment because ERBEndNode.build requires the generated nodes.ts from the Ruby/C codegen pipeline, which isn't available without the full C toolchain).
    • javascript/packages/formatter: vitest run — 1372/1480 passing, 2 more than baseline (the 2 new regression tests below). The 101 remaining failures are pre-existing and unrelated (CLI-binary tests needing a built executable, Tailwind class-sorter tests needing a Tailwind config) — verified identical failure set with and without this change.
  • Added two regression tests to javascript/packages/formatter/test/html/content-preserving-tags.test.ts: the exact case from Formatter: Attribute spacing is lost inside ERB blocks in pre elements #2142, plus a multi-attribute variant.

Test plan

  • cd javascript/packages/printer && vitest run — no regressions (116/116 real assertions pass, matching baseline)
  • cd javascript/packages/formatter && vitest run test/html/content-preserving-tags.test.ts test/erb/content-preserving-blocks.test.ts test/erb/whitespace-preservation.test.ts — 94/94 pass, including the 2 new regression tests
  • cd javascript/packages/formatter && vitest run (full suite) — no new failures vs. baseline

When formatting a content-preserving element (e.g. <pre>) whose body
contains an ERB block/conditional, the formatter falls back to
IdentityPrinter to reconstruct that ERB-wrapped subtree byte-for-byte.

Without track_whitespace: true, the parser doesn't emit a node for the
whitespace that separates a tag name from its first attribute, or the
whitespace between attributes - that gap simply isn't represented in the
AST. IdentityPrinter.visitHTMLOpenTagNode wrote the tag name and then its
children back-to-back with no separator, so any HTML element with
attributes inside an ERB block lost its attribute spacing:

    <pre><% if condition %><span class="x">x</span><% end %></pre>

formatted to:

    <pre><% if condition %><spanclass="x">x</span><% end %></pre>

which changes browser behavior (the elements aren't equivalent - the
formatted version creates a bogus spanclass custom element and drops
the class attribute). Confirmed against @herb-tools/formatter 0.10.3
per the report in marcoroth#2142.

Fix IdentityPrinter.visitHTMLOpenTagNode to compare each child's start
position against the end of the previously written node, restoring a
single separating space whenever the AST has left a gap. When whitespace
tracking is enabled the gap is already covered by an explicit
WhitespaceNode, so no extra space is added in that case (verified via the
printer package's own IndentPrinter/track_whitespace test suite, which
continues to pass unchanged).

Added regression tests to
javascript/packages/formatter/test/html/content-preserving-tags.test.ts
covering both a single-attribute and multi-attribute element nested in an
ERB block.

Tested via vitest run in javascript/packages/printer (116/116 passing,
matching baseline) and javascript/packages/formatter (1372/1480 passing,
2 more than baseline - the 2 new regression tests; the remaining failures
are pre-existing and unrelated, mostly CLI-binary and Tailwind-sorter
tests that need a built binary/tailwind config not available in this
sandbox).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added formatter @herb-tools/formatter for HTML+ERB templates typescript TypeScript source across the javascript/ packages printer @herb-tools/printer AST printing and lossless reconstruction labels Aug 10, 2026

@marcoroth marcoroth left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @vjymisal0, thanks for the pull request!

Would you mind looking at the failing tests, as they seem directly related to your changes, thank you! 🙏🏼

@vjymisal0

Copy link
Copy Markdown
Author

Noting for the record: the 'main' CI check is failing on javascript/packages/rewriter/test/action-view-tag-helper-to-html.test.ts (expected "<div class="content">" but got "<div class="content">" — a double space), which looks directly related to this PR's attribute-spacing fix. I wasn't able to check out this branch in my current environment (the repo has filenames containing '?' that are invalid on Windows filesystems), so flagging this test failure for a fix rather than guessing at one.

…dy prints its own whitespace

IdentityPrinter's tag-open gap-filling compared node positions to decide
whether to insert a separating space between children. Synthetic
WhitespaceNode children inserted by autofix rules (e.g. html-no-space-in-tag,
erb-no-trailing-whitespace) don't have positions that line up with the
surrounding nodes, so the gap-fill logic inserted an extra space in addition
to the space the WhitespaceNode itself prints, producing doubled spaces like
'<div  />' instead of '<div />'.

WhitespaceNode children are now always skipped by the gap-fill heuristic
since they already print the correct whitespace themselves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

formatter @herb-tools/formatter for HTML+ERB templates printer @herb-tools/printer AST printing and lossless reconstruction typescript TypeScript source across the javascript/ packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formatter: Attribute spacing is lost inside ERB blocks in pre elements

3 participants