From 9a0a33f7513260880d1b29b7c19b5d1d5d33ad3d Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Fri, 5 Jun 2026 12:45:47 -0300 Subject: [PATCH] fix(style-engine): surface table style base tcPr as the wholeTable layer A table style's base-level (e.g. ) 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. --- .../style-engine/src/ooxml/index.test.ts | 86 +++++++++++++++++++ .../style-engine/src/ooxml/index.ts | 9 ++ 2 files changed, 95 insertions(+) diff --git a/packages/layout-engine/style-engine/src/ooxml/index.test.ts b/packages/layout-engine/style-engine/src/ooxml/index.test.ts index 7cd3505210..0286290c44 100644 --- a/packages/layout-engine/style-engine/src/ooxml/index.test.ts +++ b/packages/layout-engine/style-engine/src/ooxml/index.test.ts @@ -886,6 +886,92 @@ describe('ooxml - resolveTableCellProperties basedOn tblStylePr inheritance', () }); }); +// ────────────────────────────────────────────────────────────────────────────── +// Style base-level tcPr as the wholeTable layer (ECMA-376 17.7.6, SD-3035) +// A table style's base-level is stored on the style +// def's own tableCellProperties (sibling of tableStyleProperties) and IS the +// wholeTable conditional layer. Word paints it on every cell. +// ────────────────────────────────────────────────────────────────────────────── + +describe('ooxml - style base-level tcPr surfaces as wholeTable (SD-3035)', () => { + const interiorCell = (styleId: string) => ({ + tableProperties: { tableStyleId: styleId, tblLook: { noHBand: true, noVBand: true } }, + rowIndex: 1, + cellIndex: 1, + numRows: 3, + numCells: 3, + }); + + it('resolves a base-level shading with no explicit wholeTable region', () => { + const styles = { + ...emptyStyles, + styles: { + CondStyle: { + type: 'table', + tableProperties: {}, + tableCellProperties: { shading: { val: 'clear', color: 'auto', fill: 'F2F2F2' } }, + }, + }, + }; + const result = resolveTableCellProperties(null, interiorCell('CondStyle'), styles); + expect(result.shading).toEqual({ val: 'clear', color: 'auto', fill: 'F2F2F2' }); + }); + + it('leaf base-level shading beats an ancestor base-level shading via basedOn', () => { + const styles = { + ...emptyStyles, + styles: { + BaseStyle: { + type: 'table', + tableProperties: {}, + tableCellProperties: { shading: { fill: 'AAAAAA' } }, + }, + LeafStyle: { + type: 'table', + basedOn: 'BaseStyle', + tableProperties: {}, + tableCellProperties: { shading: { fill: 'F2F2F2' } }, + }, + }, + }; + const result = resolveTableCellProperties(null, interiorCell('LeafStyle'), styles); + expect(result.shading).toEqual({ fill: 'F2F2F2' }); + }); + + it('an explicit tableStyleProperties.wholeTable entry beats the base-level tcPr', () => { + const styles = { + ...emptyStyles, + styles: { + CondStyle: { + type: 'table', + tableProperties: {}, + tableCellProperties: { shading: { fill: 'BASE99' } }, + tableStyleProperties: { + wholeTable: { tableCellProperties: { shading: { fill: 'EXPL77' } } }, + }, + }, + }, + }; + const result = resolveTableCellProperties(null, interiorCell('CondStyle'), styles); + expect(result.shading).toEqual({ fill: 'EXPL77' }); + }); + + it('inline cell shading still wins over the base-level wholeTable fill', () => { + const styles = { + ...emptyStyles, + styles: { + CondStyle: { + type: 'table', + tableProperties: {}, + tableCellProperties: { shading: { fill: 'F2F2F2' } }, + }, + }, + }; + const result = resolveTableCellProperties({ shading: { fill: '4472C4' } }, interiorCell('CondStyle'), styles); + expect(result.shading).toEqual({ fill: '4472C4' }); + }); +}); + // ────────────────────────────────────────────────────────────────────────────── // cnfStyle supplementing index-based conditional type detection // ────────────────────────────────────────────────────────────────────────────── diff --git a/packages/layout-engine/style-engine/src/ooxml/index.ts b/packages/layout-engine/style-engine/src/ooxml/index.ts index 0791ee9472..0f28d8cbd4 100644 --- a/packages/layout-engine/style-engine/src/ooxml/index.ts +++ b/packages/layout-engine/style-engine/src/ooxml/index.ts @@ -483,6 +483,15 @@ function resolveConditionalProps( const def: StyleDefinition | undefined = translatedLinkedStyles.styles?.[currentId]; const props = def?.tableStyleProperties?.[styleType]?.[propertyType] as T | undefined; if (props) chain.push(props); + // ECMA-376 17.7.6: a table style's BASE-LEVEL (stored on the def's own + // tableCellProperties, a sibling of tableStyleProperties) IS the wholeTable + // conditional layer; Word paints e.g. its w:shd on every cell. Pushed after the + // explicit wholeTable entry so, post-reverse, the explicit entry still wins within + // one def while a leaf's base props beat any ancestor's. (SD-3035) + if (styleType === 'wholeTable' && propertyType === 'tableCellProperties') { + const baseProps = def?.tableCellProperties as T | undefined; + if (baseProps) chain.push(baseProps); + } currentId = def?.basedOn; } if (chain.length === 0) return undefined;