Skip to content

Fix jsx template stripping the space between adjacent text lines (#359)#364

Open
brainkim wants to merge 2 commits into
mainfrom
fix/jsx-text-whitespace
Open

Fix jsx template stripping the space between adjacent text lines (#359)#364
brainkim wants to merge 2 commits into
mainfrom
fix/jsx-text-whitespace

Conversation

@brainkim

@brainkim brainkim commented Jun 23, 2026

Copy link
Copy Markdown
Member

Fixes #359.

The bug

The jsx tagged template collapses a newline between two text runs to nothing, where standard JSX collapses it to a single space. Multi-line prose loses the spaces at its line breaks:

jsx`<p>alpha
beta</p>`
// before: "<p>alphabeta</p>"
// after:  "<p>alpha beta</p>"

The fix

In src/jsx-tag.ts the children tokenizer trimmed trailing whitespace off a text run before every newline, treating a text–text break the same as an element-adjacent one. When a newline follows non-empty text and the next significant character in the span is not < (the next token is text, not an element), collapse the break to a single space instead of stripping it. Everything else is unchanged.

Whitespace parity (verified against esbuild + tsc)

I checked Crank's output after this fix against both transpilers the issue cites. They agree, and Crank now matches them on every in-content case:

source Crank (fixed) esbuild / tsc
alpha⏎beta (text–text) alpha beta alpha beta
alpha⏎<b/> / <b/>⏎beta (element-adjacent) stripped ✅ stripped
a b (interior spaces) a b a b (not collapsed)
a⏎{X} / {X}⏎b (expression-adjacent) no space ✅ no space
Hello⏎World (first-line leading) Hello World Hello World
a⇥b (tab in text) a⇥b a⇥b (both preserve tabs)
<b/>···<i/> (ws-only, one line) preserved ✅ preserved
<b/>⏎<i/> (ws-only, across newline) removed ✅ removed

The one intentional difference is template-edge whitespace: Crank trims leading/trailing whitespace at the very start and end of the whole template (e.g. jsx\

⏎`), where JSX-in-a-fragment would preserve it. This is by design — the template body has no enclosing element boundary, and authors universally pad it with newlines/indentation for readability — and is covered by the existing top-level strings/newlines and whitespace` tests.

Tests

Added a #359 regression test and a parity test block in test/jsx-tag.ts covering the table above. Full suite passes; tsc --noEmit, ESLint, and Prettier clean.

🤖 Generated with Claude Code

brainkim and others added 2 commits June 22, 2026 23:39
The children tokenizer trimmed trailing whitespace off a text run before
every newline, treating a text–text break the same as an element-adjacent
one. Standard JSX collapses an interior text–text newline to a single space
while stripping whitespace next to an element, so multi-line prose written
with the `jsx` template tag lost the spaces at its line breaks
(`<p>alpha⏎beta</p>` rendered `alphabeta`).

Collapse the break to a single space only when the newline follows non-empty
text and the next significant character in the span is not `<` (i.e. the next
token is text). Element-adjacent stripping, escaped newlines, blank lines, and
indentation are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a test block covering the cases verified against esbuild and tsc:
interior spaces preserved (not collapsed), a newline before an expression
stripped with no space, first-line leading whitespace preserved, and
whitespace-only text between elements kept on one line but removed across a
newline. With #359's fix, Crank matches both reference transpilers on all of
these.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brainkim

Copy link
Copy Markdown
Member Author

Whitespace parity investigation (for the record)

Before merging I wanted to know whether the jsx tag is now exactly JSX-faithful, so I stopped reasoning from Babel's algorithm and measured against ground truth: every case below was transpiled through both reference transpilers the issue cites — esbuild (--loader=jsx) and tsc --jsx react — and compared to Crank's output on this branch. esbuild and tsc agree with each other throughout.

Result: parity on every in-content case

source Crank (this PR) esbuild + tsc
alpha⏎beta (text–text) alpha beta alpha beta
alpha⏎<b/> / <b/>⏎beta (element-adjacent) stripped stripped
a b (interior spaces) a b a b (neither collapses)
a⏎{X} / {X}⏎b (expression-adjacent) no space no space
Hello⏎World (first-line leading) Hello World Hello World
a⇥b (tab in text) a⇥b a⇥b
<b/>···<i/> (ws-only, one line) preserved preserved
<b/>⏎<i/> (ws-only, across a newline) removed removed

These are locked in by the matches JSX whitespace handling (verified against esbuild + tsc) test block.

Two findings worth recording

  • Tabs in text are not a divergence. Babel converts tab→space in text nodes, but esbuild and tsc both preserve tabs — same as Crank. So there is nothing to change here; the de-facto reference behavior already matches.
  • Expression-adjacent newlines are not a special case. JSX strips the newline there too (the text node's only non-empty line is the adjacent one), so Crank's behavior matches rather than being a "conservative" limitation.

The one intentional difference

Template-edge whitespace. Crank trims leading/trailing whitespace at the very start and end of the whole template (jsx\

⏎`), where JSX-in-a-fragment would preserve it. This stays divergent on purpose: a template body has no enclosing element boundary, and authors pad it with newlines/indentation for readability — preserving it would inject spurious whitespace text nodes into every multi-line template. It's covered by the existing top-level stringsandnewlines and whitespace` tests.

The escaped-newline \ affordance is inherent to template literals (no JSX analog) and is kept.

Bottom line: #359's fix brings the jsx tag to full whitespace parity with both reference transpilers for in-content text; the only remaining difference is the deliberate, necessary template-edge trimming.

@brainkim brainkim mentioned this pull request Jun 24, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

jsx template strips the space between adjacent text lines (diverges from JSX whitespace rule)

1 participant