Repository navigation
Support custom properties and var() #595
Description
Part of #552 (step 28, missing support, high priority). Depends on #547 (step 26 of #520).
Custom properties (--brand: #0a58ca) and var() are not supported. Bootstrap 5, Tailwind 3+ and most design-token stylesheets set their colours, spacing and fonts this way. Under those stylesheets, a PDF loses every value that goes through a variable.
Measured on gravitypdf at 60b82630, PHP 8.5.4.
| Rule | Browser | mPDF today | After #565 |
|---|---|---|---|
:root { --w: 50mm } div { width: var(--w) } |
50 mm | full width | dropped: full width |
div { color: #0f0 } p { --c: #f00; color: var(--c) } |
red | black, overriding the inherited green | dropped: green |
:root { --x: 3mm } p { margin: 1mm 2mm var(--x) 4mm } |
bottom margin 3 mm | bottom margin 0 | dropped: default margins |
p { color: var(--missing, #00f) } |
blue, the fallback | black | dropped |
Why this waits for #547. A custom property is inherited, and var() is substituted at computed-value time. var(--c) on an element takes --c from the element itself if it declares it, or from its nearest ancestor that does. Only then is the declaration parsed as the property it belongs to. mPDF has no per-element computed values to take --c from today. #533 adds the stack of open elements, and #547 gives each frame the computed values of the inherited properties. Custom properties are one more entry in that frame. Under the legacy cascade, inherited values travel through several separate channels (#539), and adding custom properties to each of them is not worth doing.
Today's parsing, which has to change.
CssParser::parseCssProperties()upper-cases every property name (src/Css/CssParser.php:313), andNormalizeProperties::normalize()lower-cases most values (src/Css/NormalizeProperties.php:62). Custom property names and values are case-sensitive:--Mainand--mainare different properties. Store them as written.- Declarations are split on
;and:withexplode(), so a value holding either inside a string or brackets is cut. A custom property's value can hold almost any tokens. Parse stylesheets with a tokenizer instead of a brace regex #536, the stylesheet tokenizer, handles this properly; until then, split outside strings and brackets. - Drop a declaration mPDF cannot read so the value it would replace still applies #565 drops any declaration that calls
var(). Once substitution runs, it has to run first, and the check applies to the substituted value.
Fix.
- Keep custom properties apart from the other properties, both in the matched rules and in inline styles, keyed by their exact name.
- When an element's style is resolved under the opt-in cascade:
- Start from the parent frame's custom properties.
- Apply the element's own.
- Substitute
var(--x)andvar(--x, fallback)in every other declaration, recursively, and detect cycles. - Normalise and validate each result as if it had been written out.
- A declaration whose
var()cannot be resolved is invalid at computed-value time. It becomesunset, the inherited value for inherited properties and the initial value otherwise. It does not fall back to an earlier declaration.
- Shorthands with
var()are expanded after substitution, not before. - Custom properties set on
:rootorhtmlapply to the whole document; Match:root,html,a:linkanda:visited, which have a fixed answer in a PDF #529 makes:rootmatch.
var() inside calc() works once #594 lands.
Tests. Definition on :root, on an ancestor, and on the element itself. A fallback, a nested fallback, and a cycle. A shorthand, a colour, and a length inside calc(). Names that differ only in case. Values containing ;, : and a url(). Inheritance into a table cell and a positioned block. Everything under the opt-in cascade, and a check that the legacy cascade leaves output unchanged.
Activity