Skip to content

fix: table rendering fidelity round 2 (SD-3308, SD-3035)#3658

Draft
tupizz wants to merge 12 commits into
mainfrom
tadeu/sd-3028-table-fidelity-2
Draft

fix: table rendering fidelity round 2 (SD-3308, SD-3035)#3658
tupizz wants to merge 12 commits into
mainfrom
tadeu/sd-3028-table-fidelity-2

Conversation

@tupizz
Copy link
Copy Markdown
Contributor

@tupizz tupizz commented Jun 5, 2026

Summary

Second round of table rendering fidelity fixes for the SD-3028 epic, following #3617. One PR holding all round-2 fixes as separate semantic commits; further confirmed gaps from the epic plan will land here as additional commits.

Linear: SD-3028, SD-3308, SD-3035

Commits

fix(style-engine): surface table style base tcPr as the wholeTable layer (SD-3035)

A table style's base-level w:tcPr shading (e.g. fill F2F2F2) is the wholeTable conditional layer per ECMA-376 17.7.6: Word paints it on every cell of a table referencing the style. The cascade only read tableStyleProperties[region] and never the style definition's base-level tableCellProperties, so style-only cell fills were dropped and such tables rendered with no background. The fix collects the base-level props into the wholeTable chain while walking the basedOn hierarchy, ordered so explicit wholeTable entries beat base-level within one definition, leaf styles beat ancestors, bands and regions sit above wholeTable, and inline shading wins over everything.

fix(painter): clamp double borders to CSS-visible width (SD-3308)

First step of the double-border work: clamp the rendered width so CSS draws two rules instead of collapsing to a single line. Superseded in behavior by the band-width commit below, kept as its own commit for review clarity.

fix(layout): double border band width and row reservation (SD-3308)

Word renders w:val="double" at three times the authored w:sz (sz is the per-rule width; band = rule + gap + rule, measured from Word output). Word also reserves the full band height in the table's vertical extent: dotted (2px band) and double (6px band) sz12 tables have identical content regions and pitch differing by exactly the band delta.

Three coordinated changes sharing one source of truth:

  • contracts: getBorderBandWidthPx (none 0, thick 2x min 3px, double 3x min 3px, else authored width), consumed by both painter and measuring so paint and geometry can never disagree.
  • painter: applyBorder emits the full 3x double band.
  • measuring: per-gridline row-height reservation in collapsed mode (each row reserves its top gridline, last row also the bottom edge; tblPrEx row overrides and cell tcBorders included). Hairline bands at or below 2px reserve nothing, keeping thin-border corpus geometry byte-stable; wider bands reserve band minus 1px.

Verification

  • Fixture gates (spec-grounded mutation set with Word-rendered baselines):
    • tblStyle_applied, style_plus_direct_border_overrides, style_plus_width_interactions: all cells paint F2F2F2, matching Word pixel values.
    • double_or_dotted_borders: 6px double bands on every edge, zero clipped lines, rows grow exactly by the reservation; dotted and dashed unchanged.
  • Word geometry check on sd-2343-table-border-widths: band-8 row pitch in Word is 43px at 100dpi; ours moved from 33px to 41px (remaining delta is the pre-existing font-metric difference shared by all tables).
  • Layout corpus (476 docs): style commit zero changes; band commit changes 8 docs, six of them border fixtures with pure height diffs in the fix direction, two carrying unrelated font-substitution noise vs the npm reference. Visual compare passed.
  • Suites: style-engine 146, painter-dom 1244, measuring-dom 351 (one pre-existing text-width flake also fails on the clean tree), super-editor 16136.
  • Regression locks: banding and conditional-region fixtures byte-identical (the four single-region "spurious band" verdicts were disproven by pixel probes; banding is deliberately untouched).

tupizz added 2 commits June 5, 2026 12:45
A table style's base-level <w:tcPr> (e.g. <w:shd w:fill="F2F2F2"/>) is the
wholeTable conditional layer per ECMA-376 17.7.6: Word paints it on every
cell of a table referencing the style. The translator stores it on the
style definition's own tableCellProperties, a sibling of
tableStyleProperties, but resolveConditionalProps only read
tableStyleProperties[region], so style-only cell fills were dropped and
such tables rendered with no background.

Collect the base-level tableCellProperties into the wholeTable chain while
walking the basedOn hierarchy, ordered so an explicit
tableStyleProperties.wholeTable entry still wins within one definition, a
leaf style's base props beat any ancestor's, and inline cell shading wins
over everything.

Verified against the SD-3035 mutation fixtures: tblStyle_applied,
style_plus_direct_border_overrides, and style_plus_width_interactions now
paint F2F2F2 on all cells, matching Word's render pixel values. Banding
and conditional-region fixtures (first/last row/column, banded rows or
columns) are byte-identical before and after, since bands and regions sit
above wholeTable in the cascade. Layout corpus compare: 476 docs, zero
changes.
CSS `double` only renders two distinct rules when the border is at least
3px wide (1px rule + 1px gap + 1px rule). The painter emitted the authored
width verbatim, so a typical w:val="double" w:sz="12" border (~2px) was
collapsed by the browser into a single solid-looking line, while Word
always renders two parallel rules for double borders.

Clamp the rendered width up to 3px when the resolved CSS style is double,
keeping authored widths that are already 3px or wider. Every border paint
path funnels through applyBorder, so cell borders, outer table edges, and
continuation rows are all covered by the one change.

The prior unit test asserted the collapsed 2px output; the expectation flip
to 3px is deliberate. Verified against the SD-3308 mutation fixture
(double_or_dotted_borders.docx): the double table now shows two rules on
every outer and interior edge, dotted and dashed render unchanged.
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 5, 2026

SD-3308

SD-3035

SD-3028

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

tupizz added 4 commits June 5, 2026 13:30
Word renders w:val="double" at three times the authored w:sz: sz is the
width of EACH rule and the painted band is rule + gap + rule. Measured
against Word output, the dotted sz12 (2px band) and double sz12 (6px band)
tables have identical content regions and their row pitch differs by
exactly the band delta, so Word reserves the full band height in the
table's vertical extent. SuperDoc painted double borders at the floor
width and reserved no row height, leaving fat bands a single thin line
and, when widened, overlapping row content.

Three coordinated changes:

- contracts: add getBorderBandWidthPx, the single source of truth for
  rendered band width (none 0, thick 2x min 3px, double 3x min 3px, else
  authored width), so paint and measurement can never disagree.
- painter: applyBorder delegates its width math to the shared helper and
  now emits the full 3x double band.
- measuring: replace the drifted local copy of the width logic with the
  shared helper, and reserve per-gridline band heights in collapsed mode.
  Each row reserves its top gridline, the last row also the bottom edge,
  with row-level tblPrEx overrides and cell tcBorders included (max across
  candidates approximates the 17.4.66 winner). Bands at or below 2px
  reserve nothing, keeping hairline-border geometry byte-stable across the
  corpus; wider bands reserve band minus 1px, the same slack every
  bordered table already absorbs. Painter cells are border-box, so the
  reserved height lets band and content coexist exactly like Word.

Verification: the double fixture renders 6px bands on all 12 edges with
zero clipped lines and rows growing exactly by the reservation; Word row
pitch on sd-2343-table-border-widths band-8 rows is 43px at 100dpi, ours
moved from 33px to 41px. Layout corpus: 8/476 docs changed, six of them
border fixtures with pure height diffs in the fix direction (the other
two carry unrelated font-substitution noise vs the npm reference); visual
compare passed. Suites: painter-dom 1244, measuring-dom 351 plus one
pre-existing text-width flake also failing on the clean tree.
A pure-auto table (autofit layout, auto/nil/absent tblW, and no cell
anywhere carrying a concrete width preference) was pinned to its authored
grid sum by the auto-grid width budget, stretching it to the grid width.
Word recomputes layout for these tables on open and content-sizes them:
each column takes its max-content width and the table ends at the content
demand, capped by the available width. The stored w:tblGrid is only a
Word layout cache for organically authored documents, which always carry
tcW; a grid with no width preferences anywhere is not authoritative.

Scope is deliberately narrow so every grid-trusting path keeps its proven
behavior: tables claimed by preserveAutoGrid (non-uniform grids),
preserveExplicitAutoGrid, explicit tblW, any concrete tcW, or grids wider
than the available width (preserved overflow per SD-1239 and the overhang
behavior) are untouched. The layout corpus confirms the blast radius:
zero corpus documents change, since every real-world pure-auto table has
a non-uniform cached grid.

On the content-size path, columns also reserve their owned vertical
border band (left gridline, last column also the right edge) since
border-box cells subtract the band from the text box, and spanning cells
top up their covered columns to their max-content demand so span text
keeps Word's line breaks.

Two legacy tests asserted the grid pin for pure-auto shapes and were
updated to the Word behavior with their regression intent preserved
(synthesized runtime columns, no upscaling beyond content). The SD-1239
wide-grid preservation test passes unchanged.

Verified against fresh Word renders of the showcase fixture: all five
sections now match, including the auto double-border table (Word ~165px,
ours 157px) and the gridSpan/vMerge table (Word ~250px one line per cell,
ours 252px one line per cell). Fixture gate auto_or_nil_widths
content-sizes to 124px. Suites: measuring-dom 351 passing plus the
pre-existing text-width flake, painter-dom 1244.
CSS double borders miter diagonally where two sides of the same element
meet and land on fractional device pixels (table row heights are
fractional), so the two rules rendered with uneven weights and notched
corner joins. Word draws both rules at even weight and crosses them
squarely at junctions; verified against 300dpi Word probe renders (1x1,
1x2, 2x1 double-border tables), which also confirm the macro geometry is
one rule-gap-rule band per edge with shared interior edges drawn once,
exactly the existing single-owner model.

Cells keep their CSS double border with a TRANSPARENT color so border-box
layout (content inset and band reservation) is untouched; the visible
rules are absolutely positioned strip overlays per owned edge, snapped to
integer pixels, painted as two solid rules with the band gap between.
Strips from adjacent owned edges overlap squarely at corners, matching
Word's junctions.

Verified at 3x zoom against the Word render: even rule weights, square
crossings, identical band structure. painter-dom suite: 1245 passing.
Word does not paint w:val="double" as a self-contained two-rule band per
edge. Magnified 300dpi renders of the same file show it decomposes the
double border into CLOSED RECTANGLES: one single-rule outline at the table
boundary plus one complete single-rule inner rectangle per cell, the
band's gap separating them, rules joined squarely at the corners. A
per-edge band (CSS double or strip overlays) puts the rules in the right
positions but the wrong connectivity: junctions render as crossings
instead of nested boxes, which reads entirely differently when zoomed.

Replace the strip overlays with the Word decomposition: each cell paints
one inner rectangle with ALL FOUR sides sourced from the cell's EFFECTIVE
borders (the single-owner-suppressed set only describes who paints a
shared band, not which sides exist; every surrounding double edge
contributes a side to the rectangle). Ownership picks the band face: rules
sit inset band minus rule on sides whose band lives in this cell, and
extend rule px past the box on interior sides whose band lives in the
neighbor, so the two rules of a shared band land exactly on its faces with
per-cell attribution. The table fragment paints the outline rule at the
boundary for double outer borders, skipping broken edges on continuation
fragments. Cells keep a transparent CSS double border so border-box
layout, content insets, and the row band reservation are unchanged.

Verified at 3x zoom against the 300dpi Word render of the same file:
outer outline, four closed per-cell boxes, square joins, rules on the
band faces. painter-dom suite: 1245 passing.
@tupizz tupizz force-pushed the tadeu/sd-3028-table-fidelity-2 branch from 1d50a82 to f1bed4b Compare June 5, 2026 17:46
tupizz added 6 commits June 5, 2026 16:54
Word renders triple and the nine thinThick* ST_Border styles as multi-rule
bands composed of nested rectangles, measured from 300dpi Word probes at
sz 4/12/24:

- triple: [w, w, w, w, w]
- thinThickSmallGap: [w, 0.75pt, 0.75pt] (thickThin mirrors)
- thinThickMediumGap: [w, w/2, w/2] (mirrors)
- thinThickLargeGap: [1.5pt, w, 0.75pt] (gap scales; mirrors)
- thinThickThin*: thin rules around a scaled center per family

contracts getBorderBandProfile encodes the measured segments and becomes the
band source of truth (getBorderBandWidthPx delegates, double unchanged). The
painter generalizes the double-border nested-rectangle path: outline paints
the outer-face rule, the per-cell inner rectangle paints the inner-face rule,
and 3-rule bands add a middle rectangle inset by outer rule + gap so corners
join cleanly. dashSmallGap routes to css dashed (same approximation class as
dotDash). The layout-adapter whitelists and 17.4.66 weight tables cover the
new styles. (SD-3308)
Word probes (single-cell auto tables, band styles x sz 4/12/24) pin three
sizing rules SuperDoc was missing:

- A single-column pure-auto grid is not a Word layout cache: Word
  content-sizes the table on open. preserveAutoGrid no longer claims
  single-column grids with no tcW anywhere (hasNonUniformGrid treats
  length <= 1 as non-uniform, which pinned the stale grid at 200px where
  Word renders ~70px).
- A content-sized column grows by HALF the border band per adjacent
  gridline, not a full band per side: resolveColumnBandAllowances halves
  each edge contribution. The painted band then eats the other half back
  from the padding: renderTableCell compresses horizontal padding by
  band/2 on compound sides (floor 0), matching Word's measured leftover
  margin = padding - band/2.
- Padding compresses to zero but text never clips: the solver floors the
  column at text + 2x band via text-only bounds threaded from
  table-autofit-metrics (horizontalInsets) into accumulateBounds.

Net effect: column = text + max(padding + band, 2x band), verified against
Word renders of all 18 probe tables; the residual width delta is a constant
font-metrics difference. Corpus layout compare vs the previous commit: 476
docs, zero changes (no corpus doc carries these shapes). (SD-3308)
Word writes w:tcW on every cell it inserts; the concrete cell width marks
the stored grid as a real layout cache. SuperDoc's createTable only set the
PM colwidth attr, which feeds the grid but not the cell width preference,
so the SD-3309 pure-auto classifier content-sized freshly inserted tables
to their text instead of keeping the requested full-width columns.

Sets tableCellProperties.cellWidth (dxa twips) alongside colwidth, matching
Word's emission and the existing import shape, so inserted tables keep
their geometry and export real tcW values.

Fixes the behavior CI failure in drag-selection-into-table-feedback
(SD-2676 spec): the drag endpoint at the last cell's line midpoint landed
mid-text once the inserted table content-sized, so the selection stopped
at "R4C1 " instead of covering the cell. Verified failing without this
change and passing with it on chromium; all 83 table behavior specs pass.
…grid

Two Word behaviors measured from 300dpi probes of the triple fixtures:

- An interior compound band is CENTERED on the gridline (spans
  gridline -band/2 .. +band/2), so both adjacent cells keep equal content
  widths. SuperDoc placed the whole band inside the owner cell, making the
  left cell a full band wider than the right. Each adjacent cell now
  carries HALF the band as its transparent CSS border
  (borderBandOverridesPx), the inner rectangles place their divider-facing
  rules at the straddled band's faces, and the padding compression skips
  straddled edges (the half-band the column was granted covers them).
- The MIDDLE rule of 3-rule bands (triple, thinThickThin*) is a continuous
  grid: it runs unbroken through perpendicular band crossings and meets
  the boundary band's middle ring squarely. Per-cell middle rectangles
  break at every intersection, so table-level 3-rule borders now paint a
  fragment-level ring plus full-length center strips, and the per-cell
  middle rectangle is suppressed for those sides (kept for cell-level-only
  compound borders).

Verified in-browser against the Word probes: divider rules symmetric
around the gridline, equal cell boxes, continuous middle grid. Painter
suite 1256 tests.
A row whose cells are ALL row-spanning (vMerge start or continuation)
measured 0 high: row base heights only count rowspan=1 cells, and the
span constraints were already satisfied by the neighboring rows, so
nothing raised it. The row collapsed and the next row painted over it
(showcase: head/only-real-cell/new rendered as two rows where Word
renders three).

In OOXML the vMerge continuation cells are real w:tc elements holding an
empty paragraph and Word sizes the row from them, but the import merges
those cells away. The measurer now grants every spanned row with no
height of its own the spanning cell's first-line height (non-text spans
such as logo images fall back to an even share so a spanning picture
never doubles a header).

Corpus layout compare vs the previous commit: 1 changed doc
(sd-1797-autofit-tables, an invisible boundary redistribution inside
spanned cells; the visible region matches a fresh Word render).
swapCellBordersLR returns CellBorders | undefined; the straddle code read
rectBorders.left/right without narrowing, failing tsc -b under the
references config (TS18048). Fall back to the unswapped specs, which the
function only skips when given undefined input.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants