Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions FIDELITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<sup>†</sup>`-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<sup>†</sup>`-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.
Expand Down Expand Up @@ -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" `<ul>` — real Chrome lays its ~29 filenames out in 5 narrow columns side by side; this engine stacks them in one long single column instead (directly re-verified via a live re-render this same round, alongside the unrelated `word-break` fix — see the round-143 log entry above). Not attempted this round: a real multi-column implementation needs a genuinely new layout algorithm (splitting content into N columns, balancing column heights, handling a break mid-content) — comparable in scope to this session's other deliberately-deferred larger features (Shadow DOM, the reskin-orphaned-node architecture), not a narrow bug fix alongside `word-break`'s much smaller change. Revisit as its own round.
- **react.dev's own hydration fails with a real React error (#418, "the initial UI does not match what was rendered on the server"), which cascades into a SECOND, internal React crash during its own unmount/remount recovery and unmounts the whole app to nothing — this engine's own empty-page guard then falls back to the pre-hydration SSR skeleton, the long-documented "reskin gap."** Found live (round 82, 2026-09-22): this is a NEW, more precisely characterised failure mode, distinct from round 71's `ChunkLoadError` theory — confirmed unrelated by fixing round 71's own flagged root cause (`setTimeout` discarding its requested delay, fixed this same round) and re-checking: the live JS log was byte-for-byte identical before and after that fix. One plausible theory (a `prefers-color-scheme` client/server mismatch via `matchMedia`) was checked and ruled out — this engine already deliberately resolves that query to match react.dev's own dark-theme strategy. Minified production React strips the hydration-mismatch diff that would name the exact divergent DOM node, so finding the actual cause needs a materially larger investigation than fits in one round; not chased further, not fixed here.
- **goja (this engine's JS runtime) does not support ES2018 `for await (... of ...)` asynchronous-iteration syntax.** Found live on tailwindcss.com (round 77, 2026-09-21): a real Next.js chunk shipped as a plain classic script (correctly recognised as such by this engine — no type-attribute mishandling involved) fails to compile at all because its own code legitimately uses `for await` inside a real `async function`. Isolated to the exact byte via the real fetched chunk, and reproduced from scratch with a minimal, from-nothing repro containing only the `for await` construct. Confirmed as a genuine, currently-open upstream gap (dop251/goja#498, "Upgrade to latest version of ECMAScript 2021/2022") rather than a local misconfiguration — there is no JS parser in this repo to patch; forking or patching goja itself is outside this engine's own scope. Not fixed, not fixable here.
- **`display:inline-flex`/`inline-grid`/`inline-table` lose their "inline"
Expand Down
22 changes: 11 additions & 11 deletions bench/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- AUTO-GENERATED: this header, the results table and the montage list are regenerated by `go run ./cmd/compare`. The hand-written analysis below the BEGIN ANALYSIS marker is preserved across re-runs. -->

**Generated:** 2026-09-28 12:22 UTC
**Generated:** 2026-09-28 13:24 UTC
**Viewport:** 1024×768, device-scale 1
**Timing:** median of N=5 runs after 1 warmup, wall-clock incl. network fetch
**Chrome:** `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
Expand All @@ -11,16 +11,16 @@
<!-- BEGIN RESULTS TABLE -->
| URL | SSIM | pixdiff % | webengine ms | chrome ms | speed× | region | status |
|-----|-----:|----------:|-------------:|----------:|-------:|:------:|:------:|
| example.com/ | 0.954 | 1.5 | 46.6 | 1292.8 | 27.76 | 1024×768 | ok |
| en.wikipedia.org/wiki/Go_(programming_language) | 0.436 | 21.3 | 2749.9 | 3775.9 | 1.37 | 1024×2500 | ok |
| pkg.go.dev/net/http | 0.716 | 11.3 | 5326.6 | 3582.9 | 0.67 | 1024×2500 | ok |
| go.dev/blog/ | 0.689 | 16.8 | 4264.1 | 1860.1 | 0.44 | 1024×1439 | ok |
| react.dev/ | 0.726 | 32.9 | 2037.2 | 2033.2 | 1.00 | 1024×2500 | ok |
| news.ycombinator.com/ | 0.606 | 14.2 | 1222.1 | 1989.3 | 1.63 | 1024×1077 | ok |
| developer.mozilla.org/en-US/docs/Web/CSS | 0.603 | 18.1 | 1115.4 | 1547.7 | 1.39 | 1009×2500 | ok |
| github.com/golang/go | 0.638 | 14.2 | 3081.3 | 1989.4 | 0.65 | 1024×2491 | ok |
| tailwindcss.com/ | 0.739 | 11.3 | 7667.0 | 1716.3 | 0.22 | 1024×2500 | ok |
| caniuse.com/ | 0.655 | 18.3 | 7474.2 | 5225.8 | 0.70 | 1024×1740 | ok |
| example.com/ | 0.954 | 1.5 | 46.7 | 1346.0 | 28.84 | 1024×768 | ok |
| en.wikipedia.org/wiki/Go_(programming_language) | 0.436 | 21.3 | 2852.8 | 4123.9 | 1.45 | 1024×2500 | ok |
| pkg.go.dev/net/http | 0.716 | 11.3 | 5499.0 | 3650.6 | 0.66 | 1024×2500 | ok |
| go.dev/blog/ | 0.689 | 16.8 | 4265.3 | 2044.6 | 0.48 | 1024×1439 | ok |
| react.dev/ | 0.725 | 33.0 | 2395.1 | 2198.0 | 0.92 | 1024×2500 | ok |
| news.ycombinator.com/ | 0.604 | 14.3 | 1186.8 | 2092.8 | 1.76 | 1024×1077 | ok |
| developer.mozilla.org/en-US/docs/Web/CSS | 0.637 | 13.6 | 1097.6 | 1669.7 | 1.52 | 1009×2500 | ok |
| github.com/golang/go | 0.638 | 14.2 | 3336.3 | 2177.4 | 0.65 | 1024×2491 | ok |
| tailwindcss.com/ | 0.740 | 11.2 | 7736.3 | 1805.0 | 0.23 | 1024×2500 | ok |
| caniuse.com/ | 0.658 | 18.1 | 4829.5 | 3306.0 | 0.68 | 1024×1740 | ok |
<!-- END RESULTS TABLE -->

Speed× is `chrome_ms / webengine_ms`: >1 means webengine is faster.
Expand Down
Binary file modified bench/out/caniuse.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/developer.mozilla.org_en-US_docs_Web_CSS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/github.com_golang_go.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/news.ycombinator.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/react.dev.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/tailwindcss.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 42 additions & 42 deletions bench/results.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"url": "https://example.com/",
"webengine_ms": 46.575,
"chrome_ms": 1292.756,
"speed_ratio": 27.756435856146002,
"webengine_ms": 46.667,
"chrome_ms": 1345.998,
"speed_ratio": 28.842608267083808,
"ssim": 0.9541451179596087,
"pixdiff_pct": 1.53350830078125,
"region_w": 1024,
Expand All @@ -14,9 +14,9 @@
},
{
"url": "https://en.wikipedia.org/wiki/Go_(programming_language)",
"webengine_ms": 2749.907,
"chrome_ms": 3775.87,
"speed_ratio": 1.3730900717733363,
"webengine_ms": 2852.814,
"chrome_ms": 4123.914,
"speed_ratio": 1.4455600680591163,
"ssim": 0.43620978650197445,
"pixdiff_pct": 21.2776171875,
"region_w": 1024,
Expand All @@ -27,9 +27,9 @@
},
{
"url": "https://pkg.go.dev/net/http",
"webengine_ms": 5326.601,
"chrome_ms": 3582.92,
"speed_ratio": 0.6726465901988905,
"webengine_ms": 5499.012,
"chrome_ms": 3650.607,
"speed_ratio": 0.6638659817436297,
"ssim": 0.7155686319990039,
"pixdiff_pct": 11.3370703125,
"region_w": 1024,
Expand All @@ -40,9 +40,9 @@
},
{
"url": "https://go.dev/blog/",
"webengine_ms": 4264.126,
"chrome_ms": 1860.149,
"speed_ratio": 0.43623218450861906,
"webengine_ms": 4265.321,
"chrome_ms": 2044.595,
"speed_ratio": 0.4793531366103513,
"ssim": 0.688936244874563,
"pixdiff_pct": 16.781062695448227,
"region_w": 1024,
Expand All @@ -53,11 +53,11 @@
},
{
"url": "https://react.dev/",
"webengine_ms": 2037.17,
"chrome_ms": 2033.175,
"speed_ratio": 0.998038946185149,
"ssim": 0.7264293856170049,
"pixdiff_pct": 32.9206640625,
"webengine_ms": 2395.07,
"chrome_ms": 2197.996,
"speed_ratio": 0.9177168099470997,
"ssim": 0.72456508425769,
"pixdiff_pct": 32.97015625,
"region_w": 1024,
"region_h": 2500,
"montage": "out/react.dev.png",
Expand All @@ -66,11 +66,11 @@
},
{
"url": "https://news.ycombinator.com/",
"webengine_ms": 1222.051,
"chrome_ms": 1989.256,
"speed_ratio": 1.627801131049359,
"ssim": 0.6064343869625353,
"pixdiff_pct": 14.161153667595173,
"webengine_ms": 1186.776,
"chrome_ms": 2092.841,
"speed_ratio": 1.7634675793915615,
"ssim": 0.6041222981256633,
"pixdiff_pct": 14.254820247214484,
"region_w": 1024,
"region_h": 1077,
"montage": "out/news.ycombinator.com.png",
Expand All @@ -79,11 +79,11 @@
},
{
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS",
"webengine_ms": 1115.404,
"chrome_ms": 1547.717,
"speed_ratio": 1.387584229570631,
"ssim": 0.6028225690863096,
"pixdiff_pct": 18.10097125867195,
"webengine_ms": 1097.583,
"chrome_ms": 1669.748,
"speed_ratio": 1.5212954282272957,
"ssim": 0.6369356196501483,
"pixdiff_pct": 13.601189296333002,
"region_w": 1009,
"region_h": 2500,
"montage": "out/developer.mozilla.org_en-US_docs_Web_CSS.png",
Expand All @@ -92,11 +92,11 @@
},
{
"url": "https://github.com/golang/go",
"webengine_ms": 3081.304,
"chrome_ms": 1989.418,
"speed_ratio": 0.6456415855105501,
"ssim": 0.6380408123751875,
"pixdiff_pct": 14.242640693998393,
"webengine_ms": 3336.274,
"chrome_ms": 2177.437,
"speed_ratio": 0.6526553274701059,
"ssim": 0.6379212290663879,
"pixdiff_pct": 14.246757075471697,
"region_w": 1024,
"region_h": 2491,
"montage": "out/github.com_golang_go.png",
Expand All @@ -105,11 +105,11 @@
},
{
"url": "https://tailwindcss.com/",
"webengine_ms": 7667.014,
"chrome_ms": 1716.28,
"speed_ratio": 0.22385246720561616,
"ssim": 0.739270644528742,
"pixdiff_pct": 11.284296874999999,
"webengine_ms": 7736.263,
"chrome_ms": 1804.972,
"speed_ratio": 0.23331316424997445,
"ssim": 0.7400666328938399,
"pixdiff_pct": 11.239375,
"region_w": 1024,
"region_h": 2500,
"montage": "out/tailwindcss.com.png",
Expand All @@ -118,11 +118,11 @@
},
{
"url": "https://caniuse.com/",
"webengine_ms": 7474.199,
"chrome_ms": 5225.83,
"speed_ratio": 0.6991826147524304,
"ssim": 0.6554154883161695,
"pixdiff_pct": 18.3160470545977,
"webengine_ms": 4829.49,
"chrome_ms": 3305.954,
"speed_ratio": 0.6845348059525955,
"ssim": 0.658432581792879,
"pixdiff_pct": 18.102830908764368,
"region_w": 1024,
"region_h": 1740,
"montage": "out/caniuse.com.png",
Expand Down
31 changes: 31 additions & 0 deletions css/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,33 @@ func (s *Style) apply(d Declaration, emRef float64, parent *Style) {
s.TextWrapNowrap = n
s.TextWrapBalance = b
}
case "word-break":
if lv == "unset" { // unset is inherit for an inherited property
s.inheritProperty(d.Property, parent)
} else {
switch lv {
case "break-all":
s.WordBreakAll = true
case "break-word":
// Deprecated alias for `normal` + `overflow-wrap:anywhere` —
// see OverflowWrapAnywhere's own doc comment.
s.WordBreakAll = false
s.OverflowWrapAnywhere = true
case "normal", "keep-all":
s.WordBreakAll = false
}
}
case "overflow-wrap", "word-wrap":
if lv == "unset" { // unset is inherit for an inherited property
s.inheritProperty(d.Property, parent)
} else {
switch lv {
case "break-word", "anywhere":
s.OverflowWrapAnywhere = true
case "normal":
s.OverflowWrapAnywhere = false
}
}
case "-webkit-line-clamp", "line-clamp":
// Not inherited, so "unset"/"initial"/"none" all just reset to 0 (no
// clamp) — no inheritProperty special case needed, unlike the
Expand Down Expand Up @@ -1349,6 +1376,10 @@ func (s *Style) inheritProperty(prop string, parent *Style) {
case "text-wrap":
s.TextWrapBalance = parent.TextWrapBalance
s.TextWrapNowrap = parent.TextWrapNowrap
case "word-break":
s.WordBreakAll = parent.WordBreakAll
case "overflow-wrap", "word-wrap":
s.OverflowWrapAnywhere = parent.OverflowWrapAnywhere
// The break properties are not inherited by default, but an explicit
// `inherit` still copies the parent's computed value, per CSS Cascade.
case "break-before", "page-break-before":
Expand Down
Loading
Loading