perf(html): stop a sheet repeating itself, and keep a cell's text on one line - #824
Merged
Conversation
andiwand
force-pushed
the
feat/sheet-html-dedup
branch
from
September 6, 2026 09:09
628553d to
147efb1
Compare
andiwand
force-pushed
the
feat/sheet-html-dedup
branch
4 times, most recently
from
September 6, 2026 10:23
f4728f1 to
fbe23cc
Compare
…one line A rendered sheet spent more than half its bytes on `style` attributes it wrote over and over — 26 distinct blocks across 259,957 attributes in `Supervised_Business_Register_300425.ods` — and four nodes per cell before any content. It also broke every cell's text into lines, which no spreadsheet does unless the file says to, and the second line then painted over the row below. - A style block becomes a class defined once in `<head>`, named the first time it is written. The class names itself three times, for the specificity an inline attribute had. `<head>` has to name the classes before the cells that use them and the renderer streams, so a spreadsheet's body goes into a buffer and the head out in front of it — one walk, where a second would cost as much as the first. The buffer is capped at 8 MB, past which a block first seen stays inline. - A cell holding one plain string drops the run around it, and where the file states no row height the block too. A stated row height keeps the block: nothing else holds the row to it, since `contain:size`, `max-height`, `overflow` and `content-visibility` are all ignored on a table cell. - `style:wrap-option`, `alignment@wrapText` and the XF `fWrap` bit are read into a new `TableCellStyle::wrap_text`, off by default in every format. A line too long for its cell runs over the empty cells beside it and is cut where the next one has content — bounded with `clip-path`, so it never paints over it — and out onto the canvas where nothing follows. `max-width:0` keeps such a line from widening the column, which `table-layout:fixed` alone does not. - Clicking a cell that is cut raises it: the block goes out of flow, sized to the string and over its neighbours, so no row moves. Clicking inside it is for the text; clicking elsewhere, or Escape, puts it back. Read-only, the reference corpus's sheets fall from 374 MB to 202 MB, and the 500,000 rendered cells of the register from 121 MB to 38 MB — 1.5 M inline styles down to none. 254 of 314 spreadsheet views render pixel-identically; the 60 that move are the ones that used to wrap. Rendering takes as long as it did, and peaks about 4% higher. Closes #822. Closes #238. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUh7UU43PbVPEBq2LVtoVN
andiwand
force-pushed
the
feat/sheet-html-dedup
branch
from
September 6, 2026 10:36
fbe23cc to
6f2dadd
Compare
andiwand
added a commit
that referenced
this pull request
Sep 6, 2026
Covers the regeneration #824 needed and the pdf rename this branch makes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUh7UU43PbVPEBq2LVtoVN
andiwand
added a commit
that referenced
this pull request
Sep 6, 2026
#829) * refactor(html): share one style writer between the sheet and pdf views `AtomicStyles` in `pdf_file.cpp` and `StyleRegistry` were the same class written twice: a declaration→class map, a first-seen order, and a rule writer. One class now serves both, with the two things that actually differ as parameters. - `Rank`: whether a rule has to outrank the stylesheets around it. A sheet's classes stand in for inline styles and name themselves three times for the specificity they took over; a pdf page's compete with nothing. - `Digits`: base 36 names the first 36 of a prefix in one character, but only where no prefix extends another. The sheet view uses one prefix and takes it; the pdf view keeps decimal, because `w` at 1008 spells `ws0`, which is also `ws` at 0 — and because `strip_width_class` reads a width class back out of the class string as `w` and digits. The pdf views' class names are numbered from 0 rather than 1. The rules are the same set, renamed: 802 pdf views render identically, and the html is 0.47% smaller. Nothing else moves — every sheet view is byte-identical. * test(data): advance the reference-output pins Covers the regeneration #824 needed and the pdf rename this branch makes. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
Closes #822 and #238 ("handle spreadsheet cell overflow" — html tables and
ods/xlsx differ on overflow, which is what the wrap and spill rules settle).
A rendered sheet spent more than half its bytes on
styleattributes it wroteover and over — 26 distinct blocks across 259,957 attributes in
Supervised_Business_Register_300425.ods— and four nodes per cell before anycontent. It also broke every cell's text into lines, which no spreadsheet does
unless the file says to, and the second line then painted over the row below.
Everything here is a spreadsheet view. A text document, a presentation and a
drawing emit byte-identical html.
A repeated style block becomes a class
StyleRegistrycounts every block a view writes and names the ones written morethan once,
<head>defining them once:The class names itself three times. That is not decoration: an inline
styleoutranks every rule in our own stylesheets, and
.c4alone would losevertical-alignto.odr-sheet>tbody>tr>tdand a cell border to.odr-gridlines-soft .odr-sheet td. Three classes buy the specificity backwhile staying under
!important, which is what the dark sheet relies on.A block is named the first time it is written.
<head>has to name the classesbefore the cells that use them and the renderer streams, so a spreadsheet's body
is written into a buffer and the head goes out in front of it — one walk, where
a second would have cost as much as the first. The buffer is capped at 8 MB:
past that it is released early, and a block first seen afterwards stays inline,
which is what an unnamed one would have been anyway. That is the same shape the
pdf view has, where a page model holds the classes until the head is written.
A cell holding one plain string writes no box of its own
td > x-p > x-s > textwas four nodes before any content, and thex-sstylemostly repeated the
x-pabove it. The run always goes; the block goes toowhere the file states no row height.
Where it states one, the block stays — it is the only thing that holds the row
to that height. Measured in Chrome:
contain: size,contain: strict,max-height,overflow: hiddenon the cell or the row, andcontent-visibilityare all ignored on internal table elements, and a row'sheightis a minimum by spec. Whether a line would have fitted depends on fontmetrics the renderer does not have, so guessing was not an option either.
A cell keeps its text on one line
style:wrap-option(ODF),alignment@wrapText(OOXML) and the XFfWrapbit(XLS) are read into a new
TableCellStyle::wrap_text, mirrored in the Python,JNI and Apple bindings. All three formats default to no wrapping, which is what
Calc and Excel show.
A line too long for its cell then runs over the cells beside it while they hold
nothing, and is cut where the next one has something to show — bounded with
clip-path: inset(0 -Npx 0 0)over the measured width of the blank run, so itnever paints over that content — and out onto the canvas where nothing follows
it at all. A cell the file does wrap keeps its block and clips at the row, so
a wrapped line no longer paints over the row below either.
max-width:0on the cell is what keeps such a line from widening its column:table-layout: fixedstill sizes the table from content, and without it onelong string took a column from 85 px to 3139 px. It is emitted only where the
file states a column width, since a column that states none is exactly its
content's width.
odr-public/ods/overflow.ods, before and after — the file that documents this:Clicking a cut cell raises it
The block goes out of flow, sized to the string and painted over its neighbours,
so no row moves. A click inside it is for the text — selecting it, and one day
editing it — not for the cell; a click anywhere else, or Escape, puts it back.
A cell that writes its string without a block gets a wrapper from the script for
as long as it is up.
Numbers
Read-only, the config the Android viewer renders with:
Supervised_Business_Register_300425.ods, 500,000 cellsstyleattributesVerification
compare-html --driver chromeover all 314 ods/xlsx/xls/csv/numbers views ofthe public and private corpora: 254 identical, 60 different — every one of
the 60 a sheet that used to wrap. An earlier run of the dedup and the run fold
alone was 298/298 identical, in both the read-only and the editable config.
odr_test: 1452 passed, 6 pre-existing skips.identical against the two-pass version it replaces.
html_test.cppoff an inline flat-ODS string, andodr-public/ods/file_example_ODS_100.odspinned to theread-onlyreference-output variant.
test/browser/sheet/— 14 checks for the raise behaviour, in the repo'sexisting idiom for script-only behaviour (
servelifts the css and js out offrontend.cpp, so what runs is what ships). All pass in Chrome.DeferredBufferlives inutil/stream_utilwith three unit tests of its own.Reference output
Regenerated: 361 files, every one a spreadsheet view, plus
resources/spreadsheet*.{css,js}— the last of which also catches up withpre-existing drift from #817 and #820. Not pushed yet, so
test/data.cmakestill points at the old revisions.