Skip to content

Pass the inherited properties of rows and row groups to their cells - #655

Merged
jakejackson1 merged 1 commit into
gravitypdffrom
fix/542-row-properties-to-cells
Oct 4, 2026
Merged

jakejackson1 merged 1 commit into
gravitypdffrom
fix/542-row-properties-to-cells

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Closes #542. Part of #520 (step 21).

Follows #648, now merged.

Problem

Measured on #648's head, 4fd4c2bf. When color, font-*, letter-spacing, word-spacing, text-transform, text-shadow, text-align, line-height or direction is set on a tr, thead, tbody or tfoot, whether by a rule or by style="", it does not reach the text of the cells. Only the row background works. The one exception is a thead or tfoot's text-align, which mPDF v7 passed to td cells through thead_textalign_default. It also sets the cells' weight and style, but Reset() then clears them.

HTML Cell text today
<tr style="color:#f00; font-size:20pt"> black, 11pt
<tbody style="font-size:150%"><tr style="font-size:50%"> 11pt
<style>tbody{color:#f00}</style>, rows written straight into the table black
<thead style="text-align:right"> with a th centred

Change

Under CssMode::STANDARD:

  • Row groups. THead, TBody and TFoot keep what their cells inherit on the table's state as rowGroupInherited. That is InheritedProperties::names() from the row group's merged CSS, laid over base_table_properties.
  • Rows. Tr lays its own properties over the row group's, as rowInherited.
  • Implied tbody. A row written straight into a table is in a tbody that the HTML leaves out, as in the element stack. So Tr merges the rules for that TBODY through a new CssManager::previewTableCss(), which merges without side effects, as previewBlockCss() does. TracksOpenElements::opensInImpliedTbody() shares the stack's own test for it.
  • Font sizes. A row group's font-size is resolved against the table's. A row's is resolved against its row group's, and a cell's own is resolved against its row's, since setCSS() reads a cell's size against the table's.
  • Cells. Td passes the row's set to CssMerger::merge() as a new $inherited layer, under everything else. It replaces the Arrays::uniqueRecursiveMerge() with base_table_properties. The cell's defaults, rules and style then go over it, so a rule on the cell still wins.
  • th. It keeps its bold default, since that is the cell's own UA rule, as in a browser. HTML's rendering rules centre a th only when the text-align of its parent row is the initial value. Firefox (-moz-center-or-inherit) and Chrome (-internal-center) both do this. So under standard, a th that inherits a text-align takes it over the default centring. To let that include the table's own text-align, Table::open() adds text-align to base_table_properties. For a td, that gives the same alignment as the table's txta field. A rule that sets text-align on the th still wins.

This changes output wherever a standard-mode document styles rows or row groups. Their text now looks as it does in a browser.

<tr align="right"> isn't covered here. #641 turns it into a TEXT-ALIGN hint among the row's merged properties, which this change then carries to the cells. #641 isn't on this branch, so the tests cover the CSS path.

Compatibility

Every change is gated. Under CssMode::LEGACY, Td merges as before and the row parts store nothing.

To prove legacy is unchanged, I rendered all 144 snapshot documents under legacy, with the mode forced on every document, from #648's head and from this branch. They are byte-identical. The existing fixtures, which run in their own modes, all pass unchanged.

Tests

  • InheritedPropertiesTest: 12 new contexts in Carry every inherited text property through every channel that hands inherited values on #648's matrix, each checking every TEXT property under both modes:
    • a row, by style and by a class rule;
    • a tbody, thead and tfoot;
    • a rule for the implied tbody, read in its second row;
    • a row in a header, in a list item, in a positioned block, and in a page-break-inside: avoid block laid out again on the next page;
    • a row of a nested table;
    • an outer row reaching a nested table's cells through its cell.
  • Legacy expects nothing carried. The nested ones skip font-kerning and hyphens, which the default table style resets (Let a table inherit from the block around it, not only from the document defaults #543).
  • RowPropertiesToCellsTest:
    • text-align, line-height and direction from each row part, compared by where the cell's lines are drawn. Under legacy only the thead/tfoot text-align is carried, as before.
    • Precedence: cell over row, row over row group, row group over table, a class rule on a tbody, and a table > tbody rule on the implied tbody.
    • Near misses:
      • a sibling row;
      • a row rule for another class;
      • a thead before the implied tbody;
      • a tbody before another tbody;
      • a tbody rule and a thead cell;
      • a nested table's row and the outer table's next cell;
      • the previous table's row.
    • th:
      • bold in a row set to font-weight: normal;
      • a td bold in a bold row;
      • right-aligned in a right-aligned row, thead or table;
      • left-aligned in a left row inside a right table;
      • centred when nothing sets an alignment;
      • its own text-align, and a th { text-align: center } rule, still winning.
    • Font sizes: % through the table, row group, row and cell; em on a row group; a cell's em and % of its row; a cell in points; and the near miss of a sibling row's size.
    • A thead repeated on the second page, and a tbody running onto it, keep their colours on both pages.
  • Against the unchanged source:
    • 250 standard matrix cases fail, and every legacy case passes;
    • 34 cases of RowPropertiesToCellsTest fail, all under standard.

Snapshots

  • New row-properties-to-cells (standard), one case per caption:
    • a row's colour;
    • a tbody's font and transform;
    • a thead's alignment and weight against a th's defaults;
    • a tfoot's weight and colour;
    • a tbody rule on rows with no tbody tag;
    • a 150% row group and an 80% row;
    • a cell's own colour over its row's.
  • New row-styled-invoice-standard and row-styled-invoice-legacy: an invoice with a dark thead in white capitals, striped rows by class, an overdue row, a discount row holding a nested breakdown, a tfoot of totals, and payment terms whose rows sit straight in the table. The captions say what each mode draws.
  • No existing fixture changed.

I rendered each fixture and looked at it.

Interactions with #651 and #652

  • Resolve inherit, initial, unset and revert in the standard CSS mode #652 records CssMerger::merge()'s result as a frame's cascaded values. A cell's merge under standard now includes what it inherits from the table and its row, so inherit on a cell reads its row's value.
  • Draw currentColor, and a border or shadow with no colour, in the element's colour #651 took a cell's color: currentColor from base_table_properties['COLOR'], which is the table's colour, not the row's. inheritedColor() now reads the inherited layer the cell was merged over first, so a cell's currentColor is its row's or row group's colour. CurrentColorTest::testACellsCurrentColorIsItsRowsColour covers a row, a tbody, a thead and the implied tbody, for color and for a border. Borders already took the row's colour, because the cell's merged COLOR holds it.
  • The same gap was there one level up: a tr with color: currentColor, or a row border in currentColor, took the table's colour, not its row group's. Tr now merges the row over what its row group hands it, as Td merges a cell over its row, so inherit on a row also reads its row group. CurrentColorTest::testARowsCurrentColorIsItsRowGroupsColour covers a tbody, a thead and the implied tbody.

Checks

  • composer test: OK. 7203 tests, 31 skipped for missing optional packages or ungenerated data. The snapshot suite ran with Imagick and Ghostscript, with no skips.
  • composer cs: clean.
  • PHPStan: no errors.
  • The changed files lint under PHP 5.6.

Rebased after #633 to #646

#648 and this PR were rebased onto gravitypdf.

No test changed. Gates: composer test 9112 tests, 31 skipped; composer cs clean; PHPStan no errors.

Rebased after #648, #651, #652 and #653

The stacked #648 commit is dropped, since #648 was squash-merged.

Gates: composer test 11505 tests, 30 skipped; no snapshot fixture changed; composer cs clean; PHPStan no errors; the changed files lint under PHP 5.6.

🤖 Generated with Claude Code

@jakejackson1 jakejackson1 added bug Something isn't working create-upstream-pr labels Sep 30, 2026
@jakejackson1
jakejackson1 force-pushed the fix/542-row-properties-to-cells branch from d044058 to dcca660 Compare September 30, 2026 10:38
@jakejackson1
jakejackson1 force-pushed the fix/539-inherited-channels branch 2 times, most recently from d7baa1a to 4e5b37c Compare September 30, 2026 11:47
@jakejackson1
jakejackson1 force-pushed the fix/542-row-properties-to-cells branch from dcca660 to 090dfc9 Compare September 30, 2026 11:48
@jakejackson1
jakejackson1 force-pushed the fix/539-inherited-channels branch from 4e5b37c to a4f8701 Compare September 30, 2026 17:51
Base automatically changed from fix/539-inherited-channels to gravitypdf September 30, 2026 17:57
@jakejackson1
jakejackson1 force-pushed the fix/542-row-properties-to-cells branch from 090dfc9 to d7bcd60 Compare October 4, 2026 21:21
Under the standard cssMode, a tr, thead, tbody or tfoot hands every
inherited property to its cells, over the table's and under the cell's
own. A tbody rule reaches rows written straight into a table, a row's
relative font size is taken of its row group's, and a th is centred only
when it inherits no text-align, as HTML's rendering rules have it.
Legacy output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the fix/542-row-properties-to-cells branch from d7bcd60 to b31d661 Compare October 4, 2026 21:28
@jakejackson1
jakejackson1 merged commit 122ac42 into gravitypdf Oct 4, 2026
9 checks passed
@jakejackson1
jakejackson1 deleted the fix/542-row-properties-to-cells branch October 4, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working create-upstream-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tr, tbody, thead and tfoot pass none of their properties to their cells

1 participant