Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG-GRAVITYPDF.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Read this section before upgrading from upstream mPDF. Each entry says what chan
* Class and id names still match whatever their case.
* An author's `a { }` rule now reaches the links of the table of contents and the index too. Style `a.mpdf_toc_a` and `a.mpdf_index_link` to keep them plain.
* In legacy mode, a rule whose selector mPDF v7 could not read is dropped: child and sibling combinators, structural pseudo-classes outside tables, attribute selectors other than `[lang]`, `:lang()` through an ancestor, descendant rules through inline ancestors, and `:not()`, `:is()` and `:where()`. The `font`, `border` and `background` shorthands leave the longhands they do not name, as mPDF v7 did. Fixes to how values and selectors are read apply in both modes. [#633]
* **`!important` is honoured**, as in a browser, in standard mode. mPDF read a declaration marked `!important` as any other, so `p { color: green !important } #i { color: red }` drew `<p id="i">` red, and so did `style="color: red"` on a `<p>`. Now an important declaration beats every declaration that is not, the inline style included. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to read the flag as nothing, as mPDF v7 did. [mpdf/mpdf#1010] [mpdf/mpdf#1707] [#532] [#638]
* Important declarations apply after the inline style: those of the stylesheets by specificity and then order, then those of `style=""`, then those of the default stylesheet (`defaultCssFile`), as a browser's own important declarations beat a page's.
* A shorthand marked `!important` makes every longhand it sets important, so `border: 1mm solid green !important` beats a `border-top-color` of a more specific rule or of a later declaration in the same block.
* `@page` rules, `body` rules and the class rules SVG text looks up take an important declaration over a later one of the same selector that is not. An important declaration of a `body` rule beats `<body style>`.

New features
------------
Expand Down Expand Up @@ -155,6 +159,7 @@ Bugfixes
* Descendant rules naming a table, row or cell, such as `td img` or `td span`, matched nothing inside the cell. [#223] [#507]
* A descendant rule with a part mPDF cannot parse was cut short at that part and applied to the element before it. Such rules are now dropped. [#519]
* An inline `style` with `!important` drew no border for a `border` shorthand, dropped the bottom padding of a two-value `padding`, drew text at 0pt for a `font-size`, and ignored an image's `height` with a warning. The flag is now removed, as it is from a stylesheet. [mpdf/mpdf#1010] [mpdf/mpdf#1707] [#523] [#561]
* A declaration whose flag had a space after the bang, such as `margin: 5mm ! important`, kept the flag in its value: that margin set the right and left margins to `!` and the bottom one to `important`, a `font-size` was lost, and a `border` drew nothing. The flag is now found whatever its spacing and case, in stylesheets and inline styles, in either `cssMode`. [#532] [#638]
* Text styled with the `font` shorthand or `<font face>` came out as empty boxes when the family was not installed or had a space in its name, such as `font: 12pt Roboto` or `font: 16px "DejaVu Sans Mono"`. It was drawn in the first registered font, which is the emoji font in a full install. The family is now read as `font-family` reads it: a name is kept whole, the list is tried in order, and when mPDF knows none of it the text keeps the family it inherits. `font-family` itself also reads an unquoted name with spaces whole. [#552] [#560]
* A descendant rule ending in `:lang()` or `[lang]`, such as `div :lang(fr)`, `div p:lang(fr)` or `table [lang=fr]`, never applied. It now applies in block content and in tables, and a regional language such as `fr-CA` falls back to the rule for `fr`, as the simple lang rules do. [#525] [#559]
* A rule such as `td:nth-child(2):not(.x)` or `td:nth-child(2 of .x)` was applied to the second cell of every row, as if it were `td:nth-child(2)`. A rule with anything after an nth-child argument, or with an argument that is not a formula, is now dropped. [mpdf/mpdf#83] [#522] [#564]
Expand Down Expand Up @@ -792,6 +797,7 @@ These changes do not change output.
[#526]: https://github.com/GravityPDF/mpdf/issues/526
[#527]: https://github.com/GravityPDF/mpdf/issues/527
[#528]: https://github.com/GravityPDF/mpdf/issues/528
[#532]: https://github.com/GravityPDF/mpdf/issues/532
[#533]: https://github.com/GravityPDF/mpdf/issues/533
[#535]: https://github.com/GravityPDF/mpdf/issues/535
[#536]: https://github.com/GravityPDF/mpdf/issues/536
Expand Down Expand Up @@ -844,3 +850,4 @@ These changes do not change output.
[#633]: https://github.com/GravityPDF/mpdf/pull/633
[#635]: https://github.com/GravityPDF/mpdf/pull/635
[#634]: https://github.com/GravityPDF/mpdf/pull/634
[#638]: https://github.com/GravityPDF/mpdf/pull/638
40 changes: 21 additions & 19 deletions src/Css/CssMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,10 @@ public function merge($inherit, $tag, $attr)
/**
* Merges an element's CSS as standard mode orders it, in layers, each over the one before: the inherited
* values, the built-in defaults and the default stylesheet's rules, the presentational attributes as author rules
* of zero specificity, the author rules, and the inline style. The rules of each stylesheet apply by specificity,
* then in the order they were written.
* of zero specificity, the author rules, and the inline style; then the !important declarations of the author
* rules, of the inline style, and of the default stylesheet's rules. The rules of each stylesheet apply by
* specificity, then in the order they were written. Each rule that reaches a table cell draws its borders over
* those of its neighbours.
*
* @param string $inherit Inheritance context (BLOCK, INLINE, TABLE, TOPTABLE)
* @param string $tag HTML tag name
Expand Down Expand Up @@ -189,45 +191,45 @@ private function mergeInCascadeOrder($inherit, $tag, array $attr)

$id = isset($attr['ID']) ? $attr['ID'] : '';
$classes = $this->classesOf($attr);
$dominance = $tag === 'TD' || $tag === 'TH' ? 9 : false;

$this->mergeInheritedBlockProperties($inherit, $tag);

$this->mergeDefaultCss($tag);
$this->mergeMatchingRules($this->cssManager->getDefaultRules(), $tag, $id, $classes, $path);
list($defaultRules, $importantDefaultRules) = $this->cssManager->getDefaultRules()->matchingDeclarations($tag, $id, $classes, $path);
$this->mergeEach($defaultRules, $dominance);

$this->mergeInlineAttributes($tag, $attr);
$this->mergeTableSpecificCss($tag, $attr);

if ($tag === 'BODY') {
// Merged outside the document's elements, and written to by Mpdf::SetDefaultBodyCSS() and the like
$body = isset($this->cssManager->CSS['BODY']) ? $this->cssManager->CSS['BODY'] : [];
$this->setMergedCss($body, false);
$important = $this->cssManager->getImportantCss();
$importantDefault = $this->cssManager->getDefaultImportantCss();
$rules = [isset($this->cssManager->CSS['BODY']) ? $this->cssManager->CSS['BODY'] : []];
$importantRules = isset($important['BODY']) ? [$important['BODY']] : [];
$importantDefaultRules = isset($importantDefault['BODY']) ? [$importantDefault['BODY']] : [];
} else {
$this->mergeMatchingRules($this->cssManager->getRules(), $tag, $id, $classes, $path);
list($rules, $importantRules) = $this->cssManager->getRules()->matchingDeclarations($tag, $id, $classes, $path);
}
$this->mergeEach($rules, $dominance);

$this->mergeInlineStyle($tag, $attr);
list($inline, $importantInline) = isset($attr['STYLE']) ? $this->inlineStyleParser->parseByImportance($attr['STYLE']) : [[], []];
$this->mergeEach(array_merge([$inline], $importantRules, [$importantInline], $importantDefaultRules), $dominance);

return $this->cssProperties;
}

/**
* Merges the declarations of the rules in a rule set that an element matches, in the order they apply: by
* specificity, then by their position in the stylesheets. Each rule that reaches a table cell draws its borders
* over those of its neighbours
* Merges sets of properties, each over those before it
*
* @param RuleSet $rules
* @param string $tag HTML tag name, uppercased
* @param string $id Uppercased, or empty for none
* @param string[] $classes Uppercased
* @param callable $path Gives the open elements from the document down to the element, or null for none
* @param array[] $sets
* @param int|false $dominance The border dominance each set's borders take in a table cell, or false outside one
* @return void
*/
private function mergeMatchingRules(RuleSet $rules, $tag, $id, array $classes, callable $path)
private function mergeEach(array $sets, $dominance)
{
$dominance = $tag === 'TD' || $tag === 'TH' ? 9 : false;

foreach ($rules->matchingDeclarations($tag, $id, $classes, $path) as $properties) {
foreach ($sets as $properties) {
$this->setMergedCss($properties, false, $dominance);
}
}
Expand Down
94 changes: 69 additions & 25 deletions src/Css/CssParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ class CssParser
*/
private $css = [];

/**
* @var array[] The properties the !important declarations of the rules in $css set, under the same keys. Only
* standard mode keeps them apart
*/
private $importantCss = [];

/**
* CSS for cascaded selectors.
*
Expand Down Expand Up @@ -127,8 +133,8 @@ class CssParser
private $nthChildFormulas = ['TR' => [], 'TD' => [], 'TH' => []];

/**
* @var array[] The rules of the last CSS parsed for the matcher, each compiled: [compiled selector, declarations],
* in the order they were written
* @var array[] The rules of the last CSS parsed for the matcher, each compiled: [compiled selector, declarations,
* !important declarations], in the order they were written
*/
private $compiledRules = [];

Expand Down Expand Up @@ -160,6 +166,7 @@ public function __construct(
public function parse($html)
{
$this->css = [];
$this->importantCss = [];
$this->cascadeCSS = [];
$this->compiledRules = [];

Expand Down Expand Up @@ -215,6 +222,17 @@ public function getCss()
return $this->css;
}

/**
* The properties the !important declarations of the last CSS parsed set, under the keys getCss() uses. Legacy mode
* reads those declarations as any other, into getCss(), and leaves this empty
*
* @return array[]
*/
public function getImportantCss()
{
return $this->importantCss;
}

/**
* @return array
*/
Expand All @@ -226,7 +244,7 @@ public function getCascadeCss()
/**
* The rules of the last CSS parsed for the matcher, in standard mode. Legacy mode compiles none
*
* @return array[] Each [compiled selector, declarations], in the order they were written
* @return array[] Each [compiled selector, declarations, !important declarations], in the order they were written
*/
public function getCompiledRules()
{
Expand Down Expand Up @@ -299,14 +317,14 @@ private function processCssString($css)
*/
private function processRule($selectors, $declarations)
{
$classProperties = $this->parseCssProperties($declarations);
list($classProperties, $important) = $this->parseCssPropertiesByImportance($declarations);

if (strpbrk($selectors, "\t\n\r\f\v") !== false) {
$selectors = preg_replace(self::WHITESPACE, ' ', $selectors);
}

foreach ($this->selectorCompiler->splitList($selectors) as $selector) {
$this->processCssSelector($selector, $classProperties);
$this->processCssSelector($selector, $classProperties, $important);
}
}

Expand All @@ -315,9 +333,10 @@ private function processRule($selectors, $declarations)
*
* @param string $written Selector string, as written
* @param array $classProperties CSS properties
* @param array $important The properties its !important declarations set, which only standard mode reads apart
* @return void
*/
private function processCssSelector($written, $classProperties)
private function processCssSelector($written, $classProperties, array $important)
{
$selector = strtoupper($written);

Expand All @@ -337,10 +356,8 @@ private function processCssSelector($written, $classProperties)
$level = count($tags);
if (trim($tags[0]) === '@PAGE') {
$tag = $this->selectorParser->parsePageSelector($tags);
if ($tag && isset($this->css[$tag])) {
$this->css[$tag] = Arrays::uniqueRecursiveMerge($this->css[$tag], $classProperties);
} elseif ($tag) {
$this->css[$tag] = $classProperties;
if ($tag) {
$this->storeByKey($tag, $classProperties, $important);
}

return;
Expand All @@ -352,11 +369,12 @@ private function processCssSelector($written, $classProperties)
// Legacy mode applies simple rules from these keys, and standard mode keeps them for what reads
// CssManager::$CSS directly: BODY, and SVG's classes
if ($simple) {
$this->storeSimpleRule($tag, $classProperties);
$this->storeByKey($tag, $classProperties, $important);
$this->indexStoredKey($tag);
}

if ($this->mpdf->cssMode === CssMode::STANDARD) {
$this->compileRule($written, $classProperties);
$this->compileRule($written, $classProperties, $important);

return;
}
Expand All @@ -382,21 +400,20 @@ private function processCssSelector($written, $classProperties)
}

/**
* Stores a rule whose selector is one compound the legacy parser reads, under its key
* Stores a rule under its key: an @page rule, or one whose selector is one compound the legacy parser reads
*
* @param string $key A key SelectorParser::parseSimpleSelector() made, e.g. P or CLASS>>A
* @param string $key A key SelectorParser made, e.g. P, CLASS>>A or @PAGE
* @param array $classProperties
* @param array $important The properties its !important declarations set
* @return void
*/
private function storeSimpleRule($key, array $classProperties)
private function storeByKey($key, array $classProperties, array $important)
{
if (isset($this->css[$key])) {
$this->css[$key] = Arrays::uniqueRecursiveMerge($this->css[$key], $classProperties);
} else {
$this->css[$key] = $classProperties;
}
$this->css[$key] = Arrays::uniqueRecursiveMerge(isset($this->css[$key]) ? $this->css[$key] : [], $classProperties);

$this->indexStoredKey($key);
if ($important) {
$this->importantCss[$key] = Arrays::uniqueRecursiveMerge(isset($this->importantCss[$key]) ? $this->importantCss[$key] : [], $important);
}
}

/**
Expand Down Expand Up @@ -443,19 +460,20 @@ private function isLegacySelector(array $levels)
*
* @param string $selector As written
* @param array $classProperties
* @param array $important The properties its !important declarations set
* @return void
*/
private function compileRule($selector, array $classProperties)
private function compileRule($selector, array $classProperties, array $important)
{
if (!$classProperties) {
if (!$classProperties && !$important) {
return;
}

$compiled = $this->selectorCompiler->compile($selector);

// The universal selector changes which elements existing documents style, so it waits for #530
if ($compiled !== null && !$compiled['universal']) {
$this->compiledRules[] = [$compiled, $classProperties];
$this->compiledRules[] = [$compiled, $classProperties, $important];
}
}

Expand All @@ -466,6 +484,32 @@ private function compileRule($selector, array $classProperties)
* @return array Associative array of CSS properties
*/
public function parseCssProperties($rawStyles)
{
return $this->inlineStyleParser->parseDeclarations($this->declarationsOf($rawStyles));
}

/**
* The properties the normal declarations of a block set, and those its !important ones set. Legacy mode reads a
* declaration marked !important as any other
*
* @param string $rawStyles The declarations of a block, with its comments removed
* @return array[] [normal properties, important properties]
*/
private function parseCssPropertiesByImportance($rawStyles)
{
if ($this->mpdf->cssMode === CssMode::STANDARD) {
return $this->inlineStyleParser->parseDeclarationsByImportance($this->declarationsOf($rawStyles));
}

return [$this->parseCssProperties($rawStyles), []];
}

/**
* @param string $rawStyles The declarations of a block, with its comments removed
* @return string[][] Each [name, value], with the whitespace in the value read as spaces and its data URI images
* stored
*/
private function declarationsOf($rawStyles)
{
$declarations = $this->tokenizer->declarations($rawStyles);
foreach ($declarations as &$declaration) {
Expand All @@ -479,7 +523,7 @@ public function parseCssProperties($rawStyles)
}
unset($declaration);

return $this->inlineStyleParser->parseDeclarations($declarations);
return $declarations;
}

/**
Expand Down
Loading
Loading