Skip to content

Read each HTML presentational attribute only on the elements HTML defines it for, under any stylesheet rule - #641

Merged
jakejackson1 merged 1 commit into
gravitypdffrom
feature/531-presentational-attributes
Sep 30, 2026
Merged

jakejackson1 merged 1 commit into
gravitypdffrom
feature/531-presentational-attributes

Conversation

@jakejackson1

Copy link
Copy Markdown
Member

Closes #531. Part of #520 (step 11).

Stacked on #633 (feature/535-legacy-css-mode), which merges first.

Problem

#631 moved the presentational attributes after the built-in defaults under standard, as author rules of no specificity. Two gaps were left.

1. Every attribute was read on every element. Measured on feature/535-legacy-css-mode (f1b1c530), standard:

HTML Browser mPDF
<a href color="#f00"> blue link red link
<span color="#f00">, <p color="#f00">, <td color="#f00"> black red
<div width="50mm"> full width 50mm

2. Some tag handlers read the attribute again after the merge, over the stylesheet, so a rule could not beat it:

HTML Browser mPDF
p { text-align: left } <p align="right"> left right
td { text-align: left } <td align="right"> left right
td { white-space: normal } <td nowrap> wraps one line
<table border="1" style="border: none"> no table border 1px black
caption { caption-side: top } <caption align="bottom"> top bottom
hr { width: 80% } <hr width="50%"> in a table cell 80% 50%
input { text-align: right } <input align="center"> right centred

Some attributes HTML defines did nothing, in both modes:

  • <div align="center"><p>: the paragraph was not centred.
  • <hr align>, <hr size>, <img align> and <img border> were ignored.
  • <table border="2"> was ignored.
  • <li type="square"> lost to the disc the item inherits.

Change

All of this applies under cssMode standard only. Under legacy, output is unchanged, and the snapshot suite shows no legacy fixture changed.

Mpdf\Css\PresentationalHints gives the properties an element's attributes set. Each attribute is read on the elements the Rendering section of the HTML Living Standard defines it for:

Attribute Elements Property
dir, lang any direction, lang
color font, hr color
face, size font font-family, font-size (mPDF's size table, unchanged)
width img, table, td, th, hr, and mPDF's meter, progress width
height img, table, thead, tbody, tfoot, tr, td, th, meter, progress height
valign thead, tbody, tfoot, tr, td, th vertical-align
align div (middle as center), p, h1–h6 text-align
align table parts (middle/absmiddle as center; char as mPDF's decimal alignment on td/th) text-align
align caption (bottom) caption-side
align hr margin-left/margin-right
align img float (left, right) or vertical-align
vspace, hspace img, input type=image margins
border table (any width; 1px if not a number), img (above 0) a solid black border
nowrap td, th white-space: nowrap
size hr height, in px
type ol, ul, li (1 a A i I as written; none disc circle square in any case) list-style-type

mPDF's own tags that run the cascade (barcode, dottab, textcircle) keep every attribute they read before, through the old code.

CssMerger::mergeInCascadeOrder() merges the hints in the same place as before. mergeInlineAttributes() (legacy) shares ofFont() and is otherwise unchanged.

<table border>: the table's own border is a hint on the table. Each cell takes a 1px black border of its own, as a browser's table[border] td rule gives it, below any rule for the cells. A rule for the table's border no longer removes the cells' borders, and the cells no longer copy a rule's border from the table. In collapsed tables a wider table border wins the outer edges, as in a browser.

Tag handlers that read attributes directly

This answers the plan's open question. Gated under standard through Tag::appliesStandardCascade(), because they beat the cascade:

  • Td: align (including char) and nowrap. Both now come through the properties.
  • BlockTag: block-align from align on every block, and <caption align="bottom">.
  • Hr: the W-PERCENT taken from the width attribute in a table cell.
  • Table: border. Also table_border_attr_set, which made cells copy the table's border.
  • Input: align (mPDF's own use, aligning an active field's text). A text-align rule now beats it, as it already did for textarea.

Left as they are, because they only fall back to the attribute when no property is set, so the cascade order is already right:

  • Td: valign, width, height, bgcolor, dir.
  • Tr: bgcolor.
  • Table: width, bgcolor, dir, cellpadding (applied by mergeTableSpecificCss()), and align (table['a'], which margin rules still override).
  • Hr: width, align, color outside tables.
  • Meter: width, height.
  • TextArea: align.
  • BlockTag: the type switches, which setCSS overrides.

mPDF-only attributes (autosize, rotate, text-rotate, columns valign, form attributes, the page and header tags) are unchanged.

Left out, for later

  • <table align="left|right"> floats the table in a browser. mPDF only positions it.
  • bgcolor and text on body, bgcolor on thead/tbody/tfoot, and background on tables and cells are not read in either mode.
  • <tr align> sets the row's text-align, but rows do not pass it on to their cells yet (tr, tbody, thead and tfoot pass none of their properties to their cells #542).
  • Blocks inside a table cell take the cell's alignment in both modes, so <td><p align="right"> is not right-aligned.
  • <font size> keeps mPDF's size table: 3 is small, not medium as in a browser.
  • The handlers still decide for themselves which direct reads to skip. A registry the handlers could ask ("does the cascade consume this attribute here?") would keep the two in step. That touches every handler again, so it is left out here.

Tests

  • PresentationalHintsTest (unit, 105 cases) covers each attribute on each element it applies to and on elements it must be ignored on, plus the border width parsing.
  • PresentationalAttributeTest renders every case under both modes, each with a must-match, a near miss and a stylesheet rule beating the attribute:
    • the colour of text (font, a, span, p, div, td, h3);
    • hr color/width/align/size, including in a table cell;
    • align on p, h2, div (inherited), address, section, td, th and thead;
    • nowrap, valign, bgcolor on a table, a row and a cell, and cellpadding;
    • table border: 0, 1 and 3, against rules for the table and the cells, and collapsed;
    • caption align;
    • img width/height/hspace/vspace/align/border;
    • <div width>;
    • <input align> in active forms;
    • ol/ul/li type;
    • <barcode color>.
  • testInEveryContext puts font color, a/span/p color, p align against a rule, div align and hr color width align in a block, a table cell, a header, a footer, a positioned block, a page-break-inside: avoid block that unwinds, and after a forced page break, under both modes.
  • LineRecordingMpdf records each Line() with its colour and width. ListMarkerRecordingMpdf also records the marker type, and PageStreams::placementsIn() is split out of imagePlacements().
  • StandardBlockTagsTest runs its block-align check under legacy, and adds one for standard: only div takes align, as its text-align.
  • CascadeOrderTest: the "as wide as the border attribute" case uses 1px rather than 0.264583mm. Under standard the table's border is a CSS border, which collapsed tables compare by width, and 0.264583mm is a hair narrower than 1px.

With the source changes reverted, every standard case of PresentationalAttributeTest that differs from legacy fails, and no legacy case does.

Snapshots

  • New: presentational-attributes. One attribute per caption, under standard.
  • New: email-invoice-template-legacy and email-invoice-template-standard. An invoice built like an HTML email (nested tables, bgcolor, align, valign, cellpadding, border, font color, nowrap), with a newer stylesheet laid over it that restyles some of those. The captions say what each mode draws.
  • Changed: barcode. Its stylesheet sets table.items { border: 3px solid #880000 } on a <table border="1">. The outer border is now the rule's thick dark red, and the cells keep a thin black border, as in a browser. Before, the attribute replaced the rule, and the table and its cells were all 1px black.

Checks

  • composer test: OK. 6410 tests, 31 skipped for missing optional packages or ungenerated data. The snapshot suite ran with Imagick and Ghostscript.
  • composer cs: clean.
  • PHPStan 2.2: no errors.
  • Every changed file passes php -l 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 feature/531-presentational-attributes branch from 361f517 to 8874512 Compare September 30, 2026 08:10
Base automatically changed from feature/535-legacy-css-mode to gravitypdf September 30, 2026 09:33
@jakejackson1
jakejackson1 force-pushed the feature/531-presentational-attributes branch from 8874512 to 9a8d6df Compare September 30, 2026 10:10
…ines it for, under any stylesheet rule

In standard mode, color, width, height, valign, vspace, hspace, align, border, nowrap, size and type are hints of
no specificity on the elements the HTML Rendering section gives them to. <a color>, <p color> and <div width> no
longer apply. The tag handlers no longer read align, nowrap, a table's border, <caption align> or <hr width> again
over the stylesheet. <div align> is inherited, and <hr align>, <hr size>, <img align>, <img border>, <thead align>,
<li type> and wider table borders take effect. Legacy mode is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Built-in defaults override HTML attributes, so <hr color> and <hr width> are ignored

1 participant