From 57c6b19c1bf827a951637b48e4e8fbad1fd70589 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 28 Sep 2026 15:26:37 +0200 Subject: [PATCH] css+layout: implement word-break:break-all and overflow-wrap:break-word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unbreakable "word" wider than its own line always overflowed its container instead of wrapping. Confirmed on three independent real pages: pkg.go.dev's own file/directory listings (word-break:break-all and :break-word), news.ycombinator.com's story headline links (word-break:break-word), and developer.mozilla.org, which sets overflow-wrap:break-word on itself, inherited page-wide. Both properties (and word-break:break-word's deprecated alias) OR into one css.Style.BreaksOverlongWords() check, kept as two separate fields rather than one shared boolean so an unrelated word-break:normal can never silently cancel an independent overflow-wrap:break-word from a different declaration. layoutInline's splitOverlongWord tries splitting the leading overlong item at a rune boundary right before forceOne's own unconditional overflow, on exactly the item forceOne would otherwise place whole. Scoped to exactly the one evidenced effect (avoid overflow), not break-all's stronger "break eagerly" behaviour, overflow-wrap:anywhere's intrinsic-sizing effect, word-break:keep-all, or splitting within a glued run — none of which have a confirmed real-world trigger here. Also documents columns/column-count (found the same round, real trigger on pkg.go.dev's own file list, but a genuinely new multi-column layout algorithm — deferred to its own round) and a pre-existing, unrelated forceOne/glueRun inconsistency found while testing this (an overflowing glued run splits at its glue seam; not fixed, no confirmed trigger). Co-Authored-By: Claude Sonnet 5 --- FIDELITY.md | 12 ++ bench/REPORT.md | 22 +-- bench/out/caniuse.com.png | Bin 973630 -> 969040 bytes ...veloper.mozilla.org_en-US_docs_Web_CSS.png | Bin 1294899 -> 1216636 bytes bench/out/github.com_golang_go.png | Bin 1704584 -> 1705258 bytes bench/out/news.ycombinator.com.png | Bin 837243 -> 838891 bytes bench/out/react.dev.png | Bin 1258609 -> 1261825 bytes bench/out/tailwindcss.com.png | Bin 1050193 -> 1033652 bytes bench/results.json | 84 ++++++------ css/parse.go | 31 +++++ css/parse_test.go | 129 ++++++++++++++++++ css/value.go | 62 +++++++++ layout/layout.go | 75 ++++++++++ layout/layout_test.go | 126 +++++++++++++++++ layout/linebreak_test.go | 9 ++ 15 files changed, 497 insertions(+), 53 deletions(-) diff --git a/FIDELITY.md b/FIDELITY.md index 951ac61..8b2c481 100644 --- a/FIDELITY.md +++ b/FIDELITY.md @@ -18,6 +18,17 @@ The committed PNGs under `testdata/renders/` back every claim here. Reproduce them with the commands at the bottom. The measured-vs-Chrome numbers live in [`bench/REPORT.md`](bench/REPORT.md). +## 2026-09-28 (round 143) — `word-break`/`overflow-wrap` were entirely unimplemented, so an unbreakable long token always overflowed its container instead of wrapping — CONFIRMED real, independent usage on THREE corpus pages, one of them (developer.mozilla.org) setting it on `html` itself (engine#243) + +The user chose a new method for this round: rather than sweeping a corpus for JS API usage or auditing this project's own "Known gaps" for staleness (round 142's method, now exhausted), a systematic CSS-property audit — checking this engine's actual property support against a structured reference. Delegated the bulk of the cross-referencing to a background agent, which reported two candidates surviving verification against real corpus CSS: `columns`/`column-count` (a genuinely new multi-column layout mode, only one confirmed trigger — pkg.go.dev's own file list — and comparable in scope to this session's other deliberately-deferred larger features, so left as a documented future gap rather than attempted this round) and `word-break`/`overflow-wrap` (three independent triggers, no new layout algorithm needed — implemented this round). + +- **Independently re-verified every claim before acting**: confirmed via `grep` that neither property nor any backing `Style` field existed anywhere in `css`/`layout`/`paint`; re-fetched all three cited pages' live CSS directly (not trusting the agent's quotes) — pkg.go.dev/net/http's `.UnitFiles-fileList{columns:12.5rem 5;...word-break:break-all}` and `.UnitDirectories-pathCell{...word-break:break-all}`/`.UnitDirectories td{...word-break:break-word}`, news.ycombinator.com's `.title a{word-break:break-word}` (every story headline link), and developer.mozilla.org's own `html{...overflow-wrap:break-word;...}` (inherited page-wide, confirmed via its actual served `styles-global.css`). +- **Scoped to exactly the one evidenced effect, not the full spec surface**: both properties, and `word-break:break-word`'s deprecated alias, now OR into one `css.Style.BreaksOverlongWords()` check (`WordBreakAll`/`OverflowWrapAnywhere` are separate fields, not collapsed into one at parse time, so an unrelated `word-break:normal` on one declaration can never silently cancel an independent `overflow-wrap:break-word` from another — a real cascade-correctness bug the obvious single-field design would have had). Only the one visible effect every confirmed trigger actually needs is modelled: a single unbreakable "word" wider than its own line splits at a rune boundary instead of overflowing. NOT modelled, disclosed: `break-all`'s stronger spec behaviour of breaking eagerly even when a word already fits elsewhere (no evidenced trigger, and a much larger blast radius on ordinary greedy-wrap line counts); `overflow-wrap:anywhere`'s distinct intrinsic-sizing effect; `word-break:keep-all` (this engine has no CJK line-breaking to suppress in the first place); and splitting WITHIN a glued run (`152,3†`-shaped, engine#149) rather than a single lone word — no confirmed trigger for that combination either. +- **Fixed** (`css/value.go`/`css/parse.go`: new inherited `WordBreakAll`/`OverflowWrapAnywhere` fields, wired through `inheritFrom` and the explicit-`inherit` handler like every other inherited property; `layout/layout.go`'s new `splitOverlongWord`, tried in `layoutInline`'s main loop right before `forceOne`'s own unconditional overflow, on exactly the same leading item `forceOne` would otherwise place whole). +- **A test bug caught along the way, not shipped**: an initial `TestWordBreakAllDoesNotSplitAGluedRun` wrongly assumed a glued run (`152,3†`-shaped) that must overflow stays together on one line — it does not: `forceOne` places only the run's FIRST item (it has never called `glueRun` at all), splitting the run at its own glue seam, a genuine PRE-EXISTING inconsistency with `glueRun`'s own documented invariant. Confirmed unrelated to this round (the identical split reproduces with no word-break/overflow-wrap property set anywhere) and NOT fixed here — no confirmed real-world page trigger for an unbreakable glued run wider than its own container was found; the test now pins the actual current behaviour instead of asserting something false. +- **Two new test files' worth of coverage**: `css/parse_test.go` (`TestApplyWordBreak`/`TestApplyOverflowWrap`/`TestBreaksOverlongWords` — every value, the deprecated alias, `unset`, the explicit `inherit` keyword, and `inheritFrom`) and `layout/layout_test.go` (six tests using the existing `fakeMeasurer`'s exact 10px-per-rune convention: the unaffected default, both properties independently reaching the same split, no effect on already-fitting text, inheritance to a descendant, the single-rune-cannot-split edge, and the glued-run non-split above). Git-stash-confirmed: reverting `css/parse.go`/`css/value.go`/`layout/layout.go` alone (keeping the new tests) fails to even COMPILE (`s.WordBreakAll undefined`) — the strongest class of regression signal. +- **Simplified `splitOverlongWord` after the coverage floor caught unreachable defensive code**: an initial version defensively skipped a leading `LineBreak` item and guarded an empty `items` slice, mirroring `forceOne`'s own symmetric checks — but `layoutInline` only ever calls it once `wrapOneLine` has already confirmed `items[0]` itself is a non-break item that does not fit, making both checks provably dead code. `layout`'s 100% coverage floor caught this immediately (87% on the new function); removed rather than tested around, per this session's own standing rule against validating conditions that cannot occur. +- **Bench confirmed a real, directly-attributable win on the ONE page whose own trigger applies site-wide**: developer.mozilla.org's own SSIM rose 0.6028→0.6369 (pixdiff 18.10%→13.60%) — this round's single largest movement, and exactly the page whose `overflow-wrap:break-word` sits on `html` itself, reaching every long unbreakable token anywhere in its prose and code samples, not just one narrow widget. pkg.go.dev stayed exactly flat (0.71557, unchanged) — checked directly rather than assumed: a live re-render confirmed its own Source Files list has no filename long enough to need splitting on this specific page, and its `.UnitFiles-fileList`'s OTHER property, `columns:12.5rem 5`, is the still-unimplemented, separate gap actually keeping that list single-column (documented above, not this round's fix). news.ycombinator.com's small -0.0023 and react.dev's -0.0019 are both within the same live-content noise band already established across many rounds for these two frequently-changing pages (neither's own current front-page/homepage content combines a long-enough bare token with a narrow-enough container to exercise this fix either way). github.com/golang/go (-0.00012), tailwindcss.com (+0.0008) and caniuse.com (+0.0030) are ordinary rounding/live-fetch noise; example.com, en.wikipedia.org and go.dev/blog byte-identical. ## 2026-09-28 (round 142) — two of this engine's OWN "Known gaps" bullets were stale, one of them contradicted by a bullet 130 lines below it in the SAME section — no code change, documentation only (engine#242) Continuing round 141's method (check this engine's own "Known gaps" claims against the current code rather than trust them), swept the remainder of the section for the same pattern. Three dependency-drift leads came up empty first: `dop251/goja`'s ES2018 `for await...of` gap (upstream issue #498 confirmed still open; the one newer available goja commit only adds unrelated fixes) and the Go Mono comma-glyph bug (re-reproduced live, still happens exactly as documented at 10-14px; neither `go-opentype/fonts` nor `go-opentype/opentype` has landed anything touching glyph outlines or rasterisation since round 64 found it) are both still accurate. `github.com/srwiley/oksvg`'s pinned commit IS its own upstream's latest — genuinely unmaintained, nothing to bump to. @@ -5092,6 +5103,7 @@ columns. ## Known gaps (updated 2026-08-30 — see the note above on why this drifted) +- **Multi-column layout (`columns`/`column-count`/`column-width`) is entirely unimplemented — no `layout/` code lays content out into multiple text columns at all.** Found round 143 (2026-09-28) via a systematic CSS-property audit rather than corpus-sweeping: confirmed live on pkg.go.dev/net/http itself, `.UnitFiles-fileList{columns:12.5rem 5;...}` on the package's own "Source Files" `