Make text whitespace symmetric with the browser, with whitespace-* classes to opt out - #356
Make text whitespace symmetric with the browser, with whitespace-* classes to opt out#356gwleuverink wants to merge 11 commits into
Conversation
…wn its file lifecycle
…inheritance match every other form
…er-form attribution
|
If you want to see the difference on a device rather than in the diff: I pushed the repro app at https://github.com/trailhead-labs/air-repro. Clone it, |
…ing the CSS value it implements
|
The rest of the white-space family (nowrap, pre, break-spaces) is left out on purpose. The three shipped solve the reported problem, happen to be the part PHP can own. wrapping included: pre-wrap is named pre-wrap because native text always wraps, and CSS pre means no wrapping. The other three all need a native no-wrap prop first, and I can't really make a case for them: nowrap's job on native is truncation (truncate / line-clamp-* would be the better follow-up), and break-spaces can't be done faithfully on either platform. |
Fixes #336.
The report:
<text>{{ $msg }}</text>collapses newlines while:text="$msg"preserves them, so user-generated text renders differently depending on which authoring form you picked. Reproduced identically on both platforms, and across every form: plain<text>,<native:text>,<x-native-text>, slot and attribute.Plain Blade never touches whitespace, I checked: slot and prop come through byte-identical. The browser collapses at paint time via CSS white-space, so nothing is ever destroyed and the two sources can't diverge. EDGE collapsed at capture time in PHP instead, and only on the slot path, which is both destructive and asymmetric.
This PR makes the whole thing behave like the web. There is one whitespace policy, applied where slot and attribute text merge, and it treats both sources identically. The default collapses, exactly what CSS white-space normal does. The new whitespace-pre-line and whitespace-pre-wrap classes opt out, and whitespace-normal exists to say the default explicitly. It's pre-wrap rather than pre on purpose: native text always soft-wraps, and per the CSS table pre means no wrapping while pre-wrap means preserve and wrap, which is exactly what this renders. Verified against MDN and measured in a real browser before naming it. In practice:
That default is also what keeps hand-wrapped template prose working, the same way a
<p>does:Nested runs inherit the closest classed ancestor the way white-space cascades, a run's own class wins, and runs collapse without trimming, the browser trims at block boundaries only. So inline separators keep their spacing:
Making every form identical also surfaced two pre-existing bugs. A self-closing
<text />nested inside a paired<text>used to escape the run system entirely and render as a misplaced sibling; it's a proper in-order run now. And<x-native-text>had the same escape when nested, which also meant it couldn't inherit a parent's whitespace class; it now runs the exact frame cycle a precompiled<text>does. Parity across all seven authoring forms, nested and top-level, prop and slot, is pinned by matrix tests that name the diverging form when they fail.The policy runs at capture time in PHP, not paint time on the device. Paint time is the browser's model, but SwiftUI and Compose render strings verbatim, so paint time here would mean implementing CSS collapsing twice in Swift and Kotlin plus a wire prop plus cross-run edge logic in both renderers. The split lands where it belongs: content shaping in PHP, layout behaviour native. That's also why whitespace-nowrap, whitespace-pre and break-spaces are left out, all three need a native no-wrap prop before they can exist truthfully.
The breaking part:
:textno longer preserves newlines by default, and that's what people rely on today to keep them. The migration is one class:The classes and the flip ship together, so the escape hatch exists the moment the default changes. Worth a changelog callout.
Known siblings, deliberately untouched: button labels keep their own inline collapse in the precompiler, programmatic
Text::make()->text(...)ships strings verbatim with the classes documented as a Blade-only capture policy, and EDGE elements inside a plain Blade component slot emit ahead of the component's wrapper, a pre-existing capture-order quirk. The whitespace policy resolves correctly through such slots (tested); the placement itself, including the inverse case of a native<text>inside an<x-native-text>slot, predates this branch byte-for-byte and is pinned consciously where tests touch it.Verification. 21 tests cover every class on every source and form, run inheritance and per-run override, separator runs, nbsp (which must never collapse, and doesn't, in either CSS or this implementation), delivery through Blade component slots, the exact byte shape of the defaults, and the seven-form parity matrix; the original set was proven red on main. Full suite green. On device: a 16-case repro screen rendered identically on the Android emulator and iOS simulator, checked row by row against a plain HTML page rendering the same strings under real CSS white-space.