standing in for a positioned block has had the block's rules, through its style
$outerElement = $this->styledElement;
- $this->styledElement = !$standIn && $this->isDocumentElement($tag, $attr) ? ['path' => $this->openElements, 'tag' => $tag, 'attr' => $attr] : null;
+ $this->styledElement = !$standIn && $this->isDocumentElement($tag, $attr) ? ['path' => $this->openElements, 'tag' => $tag, 'attr' => $attr, 'computed' => null] : null;
$this->tag->OpenTag($tag, $attr, $a, $i); // mPDF 6
// Unless it put back a page-break-inside: avoid block, which rewinds the parser to that block's start
// tag and the open elements to how they were there
@@ -15223,7 +15226,7 @@ function WriteHTML($html, $mode = HTMLParserMode::DEFAULT_MODE, $init = true, $c
if ($standIn) {
$standIn = false;
} else {
- $this->startElement($tag, $attr, $selfClosing);
+ $this->startElement($tag, $attr, $selfClosing, $this->styledElement === null ? null : $this->styledElement['computed']);
}
}
// Held on to, the copy of the stack would make recording each closed child copy its parent's record
@@ -15411,9 +15414,12 @@ function WriteFixedPosHTML($html, $x, $y, $w, $h, $overflow = 'visible', $boundi
$this->blk[1]['attr'] = $attr;
$this->Reset();
$outerElement = $this->styledElement;
- $this->styledElement = $this->fixedPosBlockElements === null ? null : ['path' => $this->fixedPosBlockElements, 'tag' => null, 'attr' => []];
+ $this->styledElement = $this->fixedPosBlockElements === null ? null : ['path' => $this->fixedPosBlockElements, 'tag' => null, 'attr' => [], 'computed' => null];
$p = $this->cssManager->MergeCSS('BLOCK', $tag, $attr);
$this->styledElement = $outerElement;
+ if ($this->cssMode === CssMode::STANDARD) {
+ $this->setFixedPosBlockComputed($p);
+ }
$this->fixedPosBlockCascadeCSS = $this->blk[1]['cascadeCSS'];
if (isset($p['ROTATE'])) {
$rotate = Rotation::angle($p['ROTATE'], [90, -90, 180]);
@@ -17906,6 +17912,10 @@ function _setBorderLine($b, $k = 1)
{
$this->SetLineWidth($b['w'] / $k);
$this->SetDColor($b['c']);
+ // A colour mPDF cannot read, such as a CSS-wide keyword in the legacy CSS mode, converts to nothing
+ if (!isset($b['c'][0])) {
+ return;
+ }
if ($b['c'][0] == 5) { // RGBa
$this->SetAlpha(ord($b['c'][4]) / 100, 'Normal', false, 'S'); // mPDF 5.7.2
} elseif ($b['c'][0] == 6) { // CMYKa
diff --git a/src/Tag/Img.php b/src/Tag/Img.php
index 797e65c59..8231b7f5a 100644
--- a/src/Tag/Img.php
+++ b/src/Tag/Img.php
@@ -129,8 +129,10 @@ public function open($attr, &$ahtml, &$ihtml)
$objattr['border_right'] = $this->mpdf->border_details($properties['BORDER-RIGHT']);
}
- if (isset($properties['VERTICAL-ALIGN'])) {
- $objattr['vertical-align'] = $this->getAlign($properties['VERTICAL-ALIGN']);
+ // A value mPDF does not read, such as a length or a CSS-wide keyword in the legacy CSS mode, leaves the default
+ $align = isset($properties['VERTICAL-ALIGN']) ? $this->getAlign($properties['VERTICAL-ALIGN']) : '';
+ if ($align !== '') {
+ $objattr['vertical-align'] = $align;
}
$percent = [];
$w = $this->length($properties, $attr, 'WIDTH', 0, $percent, 'w');
diff --git a/src/TracksOpenElements.php b/src/TracksOpenElements.php
index e91e213e6..29f1b541c 100644
--- a/src/TracksOpenElements.php
+++ b/src/TracksOpenElements.php
@@ -24,6 +24,9 @@ trait TracksOpenElements
/** @var string The language the or tag gives the document, which the document's frame carries */
private $documentLang = '';
+ /** @var array|null The properties merged for , which the document's frame carries as its computed values */
+ private $documentComputed;
+
/** @var array[]|null The open elements as they stood inside the positioned block being written, for its content */
private $fixedPosBlockElements;
@@ -43,10 +46,11 @@ trait TracksOpenElements
private $openElements;
/**
- * @var array|null The element whose CSS is being merged: the open elements it sits in (path) and its tag and
- * attributes (attr), or, for an element that is already open, the open elements down to it and a null tag. Null
- * while no element of the document is being styled. WriteHTML() sets it while a start tag's handler runs, reopenBlock() while a block is opened again, and
- * WriteFixedPosHTML() while a positioned block's own CSS is merged. See getStyledElementPath()
+ * @var array|null The element whose CSS is being merged: the open elements it sits in (path), its tag and
+ * attributes (attr), and the properties merged for it (computed), or, for an element that is already open, the
+ * open elements down to it and a null tag. Null while no element of the document is being styled. WriteHTML() sets
+ * it while a start tag's handler runs, reopenBlock() while a block is opened again, and WriteFixedPosHTML() while a
+ * positioned block's own CSS is merged. See getStyledElementPath()
*/
private $styledElement;
@@ -130,7 +134,11 @@ trait TracksOpenElements
* found. Null when that is not known, as for an element still open at the end of a WriteHTML() call that
* leaves it open, and in the legacy CSS mode, which reads nothing ahead
* - empty: whether it has no children and no text, white space included. Null when that is not known
- * - computed: its computed values, not filled in yet
+ * - computed: under the standard CSS mode, the properties CssMerger merged for it, with the CSS-wide keywords
+ * resolved, keyed by the uppercased longhands the normalised declarations name. The document's frame carries
+ * those of . Null for an element whose CSS was not merged, such as the tbody a table's rows are put in
+ * when the HTML leaves it out, and under the legacy mode. `inherit` on a property that is not inherited reads
+ * the parent's
*
* @return array[]
*/
@@ -231,6 +239,47 @@ private function withChildFrame(array $path, $tag, array $attr)
return $path;
}
+ /**
+ * Keeps the properties merged for the element whose CSS is being merged, for its frame on the stack. CssMerger
+ * calls it under the standard CSS mode, once it has resolved the CSS-wide keywords. An element already open, such
+ * as a block a forced page break opens again, keeps the frame it has
+ *
+ * @param array $properties
+ */
+ public function recordStyledElementComputed(array $properties)
+ {
+ if ($this->styledElement !== null && $this->styledElement['tag'] !== null) {
+ $this->styledElement['computed'] = $properties;
+ }
+ }
+
+ /**
+ * Keeps the properties merged for , which the document's frame carries as its computed values, in the
+ * stack being written and in each stack a header, footer or new document starts from. WriteHTML() calls it under
+ * the standard CSS mode when it merges them
+ *
+ * @param array $properties
+ */
+ private function setDocumentComputed(array $properties)
+ {
+ $this->documentComputed = $properties;
+ $this->openElements[0]['computed'] = $properties;
+ }
+
+ /**
+ * Keeps the properties merged for the positioned block being written, for the frame its content is written in.
+ * Its start tag in the flow only previews its CSS, so WriteFixedPosHTML() calls it under the standard CSS mode
+ * once it merges them
+ *
+ * @param array $properties
+ */
+ private function setFixedPosBlockComputed(array $properties)
+ {
+ if ($this->fixedPosBlockElements !== null) {
+ $this->fixedPosBlockElements[count($this->fixedPosBlockElements) - 1]['computed'] = $properties;
+ }
+ }
+
/**
* Makes the stack a document starts from: a single frame standing for the document, with nothing written into it
* yet. The constructor starts from one, and so does a WriteHTML() call that starts a new document, such as the one
@@ -241,7 +290,10 @@ private function withChildFrame(array $path, $tag, array $attr)
*/
private function newOpenElementStack()
{
- return [$this->newElementFrame('', [], $this->documentLang, 1, 1, '')];
+ $document = $this->newElementFrame('', [], $this->documentLang, 1, 1, '');
+ $document['computed'] = $this->documentComputed;
+
+ return [$document];
}
/**
@@ -287,8 +339,9 @@ private function newElementFrame($tag, array $attr, $lang, $nthChild, $nthOfType
* @param string $tag
* @param string[] $attr
* @param bool $selfClosing Whether the start tag ends with a slash
+ * @param array|null $computed The properties merged for it, as recordStyledElementComputed() kept them
*/
- private function startElement($tag, array $attr, $selfClosing)
+ private function startElement($tag, array $attr, $selfClosing, $computed = null)
{
if ($tag === '' || isset(self::$substitutionTags[$tag])) {
return;
@@ -301,6 +354,7 @@ private function startElement($tag, array $attr, $selfClosing)
}
$frame = $this->newChildFrame($this->openElements[$parent], $tag, $attr);
+ $frame['computed'] = $computed;
if ($selfClosing || isset(self::$voidTags[$tag])) {
$this->recordClosedElement($frame);
@@ -497,7 +551,7 @@ private function reopenBlock(array $block, $depth)
$arr = [];
$i = 0;
$outerElement = $this->styledElement;
- $this->styledElement = $depth === null ? null : ['path' => array_slice($this->openElements, 0, $depth + 1), 'tag' => null, 'attr' => []];
+ $this->styledElement = $depth === null ? null : ['path' => array_slice($this->openElements, 0, $depth + 1), 'tag' => null, 'attr' => [], 'computed' => null];
$this->tag->OpenTag($block['tag'], $block['attr'], $arr, $i);
$this->styledElement = $outerElement;
}
diff --git a/tests/Mpdf/Color/ColorConverterTest.php b/tests/Mpdf/Color/ColorConverterTest.php
index a238c884c..95724910d 100644
--- a/tests/Mpdf/Color/ColorConverterTest.php
+++ b/tests/Mpdf/Color/ColorConverterTest.php
@@ -3,6 +3,7 @@
namespace Mpdf\Color;
use Mockery;
+use Mpdf\CssMode;
use Mpdf\Mpdf;
class ColorConverterTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
@@ -197,6 +198,19 @@ public function colorValuesProvider()
];
}
+ /**
+ * The legacy CSS mode reads inherit as transparent. The standard mode resolves it before a colour is converted, so
+ * there it is not a colour at all
+ */
+ public function testInheritIsTransparentOnlyInTheLegacyMode()
+ {
+ $this->mpdf->cssMode = CssMode::LEGACY;
+ $this->assertFalse($this->converter->convert('inherit'));
+
+ $this->mpdf->cssMode = CssMode::STANDARD;
+ $this->assertSame('', $this->converter->convert('inherit'));
+ }
+
public function testConvertUnknownSpotColor()
{
$this->expectException(\Mpdf\MpdfException::class);
diff --git a/tests/Mpdf/Css/NormalizePropertiesTest.php b/tests/Mpdf/Css/NormalizePropertiesTest.php
index 0cbb26f6c..afb1ae2a4 100644
--- a/tests/Mpdf/Css/NormalizePropertiesTest.php
+++ b/tests/Mpdf/Css/NormalizePropertiesTest.php
@@ -246,7 +246,6 @@ public function providerBorderString()
['solid rgb(204 0 0) 0.5mm', '0.5mm solid rgb(204,0,0)'],
['transparent 1px solid', '1px solid transparent'],
['SOLID 2MM #C00', '2mm solid #c00'],
- ['inherit', 'medium none currentcolor'],
];
}
@@ -465,10 +464,6 @@ public function providerBackground()
'none',
['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => ''],
],
- 'a keyword on its own' => [
- 'inherit',
- ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => ''],
- ],
'a -webkit- gradient keeps its prefix for its legacy angles' => [
'-webkit-linear-gradient(left, red, blue)',
['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => '-webkit-linear-gradient(left, red, blue)'],
@@ -794,6 +789,8 @@ public function declarationsProvider()
'transparent' => ['BACKGROUND-COLOR', 'transparent', true],
'currentColor' => ['BORDER-TOP-COLOR', 'currentColor', true],
'inherit' => ['COLOR', 'inherit', true],
+ 'revert' => ['COLOR', 'revert', true],
+ 'revert-layer' => ['BACKGROUND-COLOR', 'revert-layer', true],
'a colour with an inline !important' => ['COLOR', '#00f !important', true],
'a word that is not a colour' => ['COLOR', 'bogus', false],
'none as a background colour' => ['BACKGROUND-COLOR', 'none', false],
@@ -822,6 +819,141 @@ public function testNormalizeDropsThePlusSignOfALength()
$this->assertSame('1.5', $result['LINE-HEIGHT']);
}
+ /**
+ * In the standard CSS mode a CSS-wide keyword, in any case, is passed on to a property mPDF reads as it is, for
+ * CssMerger to resolve. text-outline is read as itself and its two parts
+ *
+ * @dataProvider keywordProvider
+ *
+ * @param string $property
+ * @param string $value
+ * @param string[] $longhands
+ */
+ public function testAKeywordIsPassedOnToEachLonghand($property, $value, array $longhands)
+ {
+ $this->assertSame(array_fill_keys($longhands, strtolower($value)), $this->normalizeProperties->normalize([$property => $value]));
+ }
+
+ /**
+ * A property that is not a shorthand of the ones fullShorthandProvider() gives, with a CSS-wide keyword, and the
+ * properties it is passed on to
+ *
+ * @return array[]
+ */
+ public function keywordProvider()
+ {
+ return [
+ 'font-family' => ['FONT-FAMILY', 'inherit', ['FONT-FAMILY']],
+ 'background-image' => ['BACKGROUND-IMAGE', 'initial', ['BACKGROUND-IMAGE']],
+ 'text-outline' => ['TEXT-OUTLINE', 'unset', ['TEXT-OUTLINE', 'TEXT-OUTLINE-WIDTH', 'TEXT-OUTLINE-COLOR']],
+ 'a longhand' => ['COLOR', 'INITIAL', ['COLOR']],
+ ];
+ }
+
+ /**
+ * In the standard CSS mode a keyword, in any case, is passed on to each longhand a full value of the shorthand
+ * writes, so the keyword and the value it resolves to set the same properties. A break-* property is read as its
+ * page-break-* property
+ *
+ * @dataProvider fullShorthandProvider
+ *
+ * @param string $property
+ * @param string $value A value that names every part of the shorthand
+ * @param string $keyword
+ */
+ public function testAKeywordReachesTheLonghandsAValueWrites($property, $value, $keyword)
+ {
+ $written = array_fill_keys(array_keys($this->normalizeProperties->normalize([$property => $value])), strtolower($keyword));
+ $passedOn = $this->normalizeProperties->normalize([$property => $keyword]);
+ ksort($written);
+ ksort($passedOn);
+
+ $this->assertSame($written, $passedOn);
+ }
+
+ /**
+ * Each shorthand the keywords are passed on through, with a value that names every part, and a keyword
+ *
+ * @return string[][]
+ */
+ public function fullShorthandProvider()
+ {
+ return [
+ 'font' => ['FONT', 'italic bold small-caps 12pt/1.5 serif', 'initial'],
+ 'font-variant' => ['FONT-VARIANT', 'normal', 'unset'],
+ 'margin' => ['MARGIN', '1mm 2mm 3mm 4mm', 'revert'],
+ 'padding' => ['PADDING', '1mm', 'Inherit'],
+ 'border' => ['BORDER', '1mm solid red', 'inherit'],
+ 'border-top' => ['BORDER-TOP', '1mm solid red', 'inherit'],
+ 'border-right' => ['BORDER-RIGHT', '1mm solid red', 'initial'],
+ 'border-bottom' => ['BORDER-BOTTOM', '1mm solid red', 'unset'],
+ 'border-left' => ['BORDER-LEFT', '1mm solid red', 'revert'],
+ 'border-width' => ['BORDER-WIDTH', '1mm', 'initial'],
+ 'border-style' => ['BORDER-STYLE', 'solid', 'revert-layer'],
+ 'border-color' => ['BORDER-COLOR', 'red', 'unset'],
+ 'border-spacing' => ['BORDER-SPACING', '1mm 2mm', 'inherit'],
+ 'border-radius' => ['BORDER-RADIUS', '1mm 2mm 3mm 4mm / 1mm', 'initial'],
+ 'a corner' => ['BORDER-TOP-LEFT-RADIUS', '1mm 2mm', 'inherit'],
+ 'background' => ['BACKGROUND', 'red url(a.png) no-repeat left top / cover padding-box content-box', 'inherit'],
+ 'list-style' => ['LIST-STYLE', 'square inside url(a.png)', 'revert-layer'],
+ 'break-before' => ['BREAK-BEFORE', 'page', 'inherit'],
+ ];
+ }
+
+ /**
+ * In the standard CSS mode a shorthand with a keyword among other parts is dropped, as a browser drops it, and a
+ * keyword in an @page rule's size is not passed on
+ *
+ * @dataProvider keywordAmongPartsProvider
+ *
+ * @param string $property
+ * @param string $value
+ */
+ public function testAShorthandWithAKeywordAmongItsPartsIsDropped($property, $value)
+ {
+ $this->assertSame([], $this->normalizeProperties->normalize([$property => $value]));
+ }
+
+ /**
+ * Shorthands with a CSS-wide keyword among other parts, and an @page size that is a keyword
+ *
+ * @return string[][]
+ */
+ public function keywordAmongPartsProvider()
+ {
+ return [
+ 'border' => ['BORDER', 'inherit solid'],
+ 'margin' => ['MARGIN', '0 inherit'],
+ 'padding' => ['PADDING', 'initial 2mm'],
+ 'background' => ['BACKGROUND', 'red unset'],
+ 'font' => ['FONT', 'revert 12pt serif'],
+ 'an @page size' => ['SIZE', 'inherit'],
+ ];
+ }
+
+ /**
+ * A family name that is a keyword in quotes is a family name
+ */
+ public function testAQuotedKeywordIsAFamilyName()
+ {
+ $this->assertArrayHasKey('FONT-SIZE', $this->normalizeProperties->normalize(['FONT' => '12pt "inherit", serif']));
+ }
+
+ /**
+ * The legacy CSS mode reads a border or background shorthand with a CSS-wide keyword as its initial value, and
+ * leaves a keyword out of the properties it cannot read, such as font-family. As it resets no longhand the
+ * shorthand does not name, background sets only its colour and image
+ */
+ public function testTheLegacyModeReadsAKeywordShorthandAsItsInitialValue()
+ {
+ $this->mpdf->cssMode = CssMode::LEGACY;
+
+ $this->assertSame('medium none #000000', $this->normalizeProperties->normalize(['BORDER-TOP' => 'inherit'])['BORDER-TOP']);
+ $this->assertSame(['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => ''], $this->normalizeProperties->normalize(['BACKGROUND' => 'inherit']));
+ $this->assertSame([], $this->normalizeProperties->normalize(['FONT-FAMILY' => 'inherit']));
+ $this->assertFalse($this->normalizeProperties->canParse('COLOR', 'revert'));
+ }
+
/**
* font-variant: normal resets font-variant-position with the other longhands
*/
diff --git a/tests/Mpdf/CssWideKeywordContextTest.php b/tests/Mpdf/CssWideKeywordContextTest.php
new file mode 100644
index 000000000..93ca2b0f0
--- /dev/null
+++ b/tests/Mpdf/CssWideKeywordContextTest.php
@@ -0,0 +1,251 @@
+ ['#008000', '#ff0000', '#000000'],
+ 'font-size' => ['20pt', '8pt', '11pt'],
+ 'background-color' => ['#00ff00', '#0000ff', 'transparent'],
+ 'border' => ['1mm solid #00ff00', '0.5mm dashed #0000ff', 'none'],
+ 'padding' => ['6mm', '2mm', '0'],
+ ];
+
+ /**
+ * The values the built-in defaults give the subjects, by tag and property
+ */
+ const DEFAULTS = [
+ 'td' => ['padding' => '0.1em'],
+ ];
+
+ /**
+ * The keyword draws what the value it resolves to draws, in each context. A subject beside it with no keyword
+ * keeps its tag's rule
+ *
+ * @dataProvider keywordsInContexts
+ *
+ * @param string $mode
+ * @param string $context
+ * @param string $property
+ * @param string $keyword
+ * @param string|null $resolved The value the keyword should draw as, or null for as if it were not declared
+ */
+ public function testTheKeywordDrawsAsTheValueItResolvesTo($mode, $context, $property, $keyword, $resolved)
+ {
+ $this->assertSame(
+ $this->drawnPages($this->document($context, $property, $resolved), ['cssMode' => $mode]),
+ $this->drawnPages($this->document($context, $property, $keyword), ['cssMode' => $mode])
+ );
+ }
+
+ /**
+ * Under the standard mode, a keyword draws something other than its tag's rule, so each case above tests the
+ * keyword and not a property the context never draws
+ *
+ * @dataProvider standardKeywordsInContexts
+ *
+ * @param string $mode
+ * @param string $context
+ * @param string $property
+ * @param string $keyword
+ */
+ public function testTheKeywordOverridesItsTagsRule($mode, $context, $property, $keyword)
+ {
+ $this->assertNotSame(
+ $this->drawnPages($this->document($context, $property, null), ['cssMode' => $mode]),
+ $this->drawnPages($this->document($context, $property, $keyword), ['cssMode' => $mode])
+ );
+ }
+
+ /**
+ * Each context draws its subject where the context puts it, for every property: in the header, the footer or the
+ * positioned block, on the second page after a forced page break or once the kept block has been put back and laid
+ * out again, and in the flow of the first page otherwise
+ *
+ * @dataProvider modes
+ *
+ * @param string $mode
+ */
+ public function testEachContextDrawsItsSubject($mode)
+ {
+ foreach (array_keys($this->contexts()) as $context) {
+ foreach (array_keys(self::VALUES) as $property) {
+ $mpdf = $this->drawDocument($this->document($context, $property, 'inherit'), ['cssMode' => $mode]);
+ $mpdf->OutputBinaryData();
+
+ $this->assertDrawnInContext($context, $mpdf, ['subject']);
+ }
+ }
+ }
+
+ /**
+ * @return string[][] Each CSS mode
+ */
+ public function modes()
+ {
+ return [CssMode::STANDARD => [CssMode::STANDARD], CssMode::LEGACY => [CssMode::LEGACY]];
+ }
+
+ /**
+ * Every keyword, for each property, in every context, under each mode
+ *
+ * @return array[] Each [mode, context, property, keyword, the value it should draw as]
+ */
+ public function keywordsInContexts()
+ {
+ $rows = [];
+ foreach (array_keys($this->contexts()) as $context) {
+ foreach (array_keys(self::VALUES) as $property) {
+ // mPDF draws no padding on an inline element
+ if ($context === 'inline' && $property === 'padding') {
+ continue;
+ }
+
+ foreach (['inherit', 'initial', 'unset', 'revert', 'revert-layer'] as $keyword) {
+ $rows[$context . ', ' . $property . ': ' . $keyword] = [
+ $context,
+ $property,
+ $keyword,
+ $this->standardValue($context, $property, $keyword),
+ $this->legacyValue($property, $keyword),
+ ];
+ }
+ }
+ }
+
+ return $this->bothModes($rows);
+ }
+
+ /**
+ * The standard cases of keywordsInContexts()
+ *
+ * @return array[]
+ */
+ public function standardKeywordsInContexts()
+ {
+ return array_filter($this->keywordsInContexts(), function ($case) {
+ return $case[0] === CssMode::STANDARD;
+ });
+ }
+
+ /**
+ * What a keyword resolves to under the standard mode: inherit takes the ancestor's value, which the cases set on
+ * the subject's parent, and on the table as well as the row for a cell; initial the initial value; unset the one or
+ * the other as the property is inherited or not; and revert the built-in default, or unset where there is none
+ *
+ * @param string $context
+ * @param string $property
+ * @param string $keyword
+ *
+ * @return string
+ */
+ private function standardValue($context, $property, $keyword)
+ {
+ list($ancestor, , $initial) = self::VALUES[$property];
+ $inherited = in_array($property, ['color', 'font-size'], true);
+ $tag = $this->contexts()[$context][0];
+
+ $defaults = self::DEFAULTS;
+ if ($keyword === 'revert' || $keyword === 'revert-layer') {
+ if (isset($defaults[$tag][$property])) {
+ return $defaults[$tag][$property];
+ }
+ $keyword = 'unset';
+ }
+
+ if ($keyword === 'unset') {
+ $keyword = $inherited ? 'inherit' : 'initial';
+ }
+
+ return $keyword === 'inherit' ? $ancestor : $initial;
+ }
+
+ /**
+ * What a keyword draws as under the legacy mode, as measured: a colour keyword mPDF reads sets no colour, so the
+ * text keeps the colour in force, as currentColor does, which the legacy mode ignores in color; one it does not
+ * read (revert) is dropped. A background colour drawn as none covers a row's, as currentColor does, where
+ * transparent lets it show through. A font size or padding keyword sets none. A border keyword draws
+ * no border, except revert-layer, which is dropped
+ *
+ * @param string $property
+ * @param string $keyword
+ *
+ * @return string|null
+ */
+ private function legacyValue($property, $keyword)
+ {
+ $dropped = ['color' => ['revert', 'revert-layer'], 'background-color' => ['revert', 'revert-layer'], 'border' => ['revert-layer']];
+ if (isset($dropped[$property]) && in_array($keyword, $dropped[$property], true)) {
+ return null;
+ }
+
+ $values = ['color' => 'currentcolor', 'background-color' => 'currentcolor', 'font-size' => 'auto', 'border' => 'none', 'padding' => '0'];
+
+ return $values[$property];
+ }
+
+ /**
+ * The contexts the subjects are written in. {S} stands for the subjects, one with the class k and one without,
+ * and the class a marks the ancestor the value is inherited from: for a cell, both its row and its table
+ *
+ * @return array[] Each [the subjects' tag, the document]
+ */
+ private function contexts()
+ {
+ return [
+ 'block' => ['p', '
{S}
'],
+ 'inline' => ['em', '
text {S}
'],
+ 'table cell' => ['td', '
'],
+ 'nested table cell' => ['td', '
'],
+ 'list item' => ['li', '
'],
+ 'header' => ['p', '
{S}
body
'],
+ 'footer' => ['p', '
{S}
body
'],
+ 'positioned block' => ['p', '
{S}
'],
+ 'kept block' => ['p', str_repeat('
filler
', 27) . '
kept
{S}'],
+ 'forced page break' => ['p', '
'],
+ ];
+ }
+
+ /**
+ * A document in a context whose subject with the class k takes a value, or a keyword, over the rule for its tag
+ *
+ * @param string $context
+ * @param string $property
+ * @param string|null $value Null for no declaration
+ *
+ * @return string
+ */
+ private function document($context, $property, $value)
+ {
+ list($tag, $html) = $this->contexts()[$context];
+ list($ancestor, $rule) = self::VALUES[$property];
+
+ $css = '.a { ' . $property . ': ' . $ancestor . '; } .a ' . $tag . ' { ' . $property . ': ' . $rule . '; }';
+ if ($value !== null) {
+ $css .= ' .a .k { ' . $property . ': ' . $value . '; }';
+ }
+ $subjects = '<' . $tag . ' class="k">subject' . $tag . '><' . $tag . '>near miss' . $tag . '>';
+
+ return '' . str_replace('{S}', $subjects, $html);
+ }
+}
diff --git a/tests/Mpdf/CssWideKeywordTest.php b/tests/Mpdf/CssWideKeywordTest.php
new file mode 100644
index 000000000..80acafe9f
--- /dev/null
+++ b/tests/Mpdf/CssWideKeywordTest.php
@@ -0,0 +1,421 @@
+drawDocument('' . $html, ['cssMode' => $mode]);
+
+ $this->assertSame($expected, [$mpdf->drawnColours[0], round($mpdf->drawnFontSize[0], 6), $mpdf->drawnFontStyles[0]]);
+ }
+
+ /**
+ * The cases of GravityPDF/mpdf#546, with what the standard mode draws, as a browser does, and what the legacy mode
+ * draws, as measured
+ *
+ * @return array[]
+ */
+ public function measuredCases()
+ {
+ $block = '
';
+ $rows = [
+ 'color: inherit' => ['.a { color: #0f0; } p { color: red; } p.b { color: inherit; }', $block, [self::LIME, 11.0, ''], [self::DEFAULT_BLACK, 11.0, '']],
+ 'color: unset' => ['.a { color: #0f0; } p { color: red; } p.b { color: unset; }', $block, [self::LIME, 11.0, ''], [self::DEFAULT_BLACK, 11.0, '']],
+ 'color: revert' => ['.a { color: #0f0; } p { color: red; } p.b { color: revert; }', $block, [self::LIME, 11.0, ''], [self::RED, 11.0, '']],
+ 'font-size: inherit' => ['.a { font-size: 20pt; } p { font-size: 8pt; } p.b { font-size: inherit; }', $block, [self::DEFAULT_BLACK, 20.0, ''], [self::DEFAULT_BLACK, 11.0, '']],
+ 'font-weight: inherit' => ['.a { font-weight: bold; } p { font-weight: normal; } p.b { font-weight: inherit; }', $block, [self::DEFAULT_BLACK, 11.0, 'B'], [self::DEFAULT_BLACK, 11.0, '']],
+ 'color: inherit on a cell' => [
+ 'table { color: #0f0; } td { color: red; } td.b { color: inherit; }',
+ '
',
+ [self::LIME, 11.0, ''],
+ [self::DEFAULT_BLACK, 11.0, ''],
+ ],
+ ];
+
+ return $this->bothModes($rows);
+ }
+
+ /**
+ * border: inherit on a paragraph in a bordered block draws a second border in the standard mode, and none in the
+ * legacy mode
+ *
+ * @dataProvider modes
+ *
+ * @param string $mode
+ * @param int $sides How many border sides are drawn in red
+ */
+ public function testBorderInheritDrawsTheParentsBorder($mode, $sides)
+ {
+ $pages = $this->drawnPages('
', ['cssMode' => $mode]);
+
+ $this->assertSame($sides, substr_count($pages[0][0], '1.000 0.000 0.000 RG'));
+ }
+
+ /**
+ * The initial colour of a border is currentColor, so in the standard mode initial draws a side in the element's
+ * colour
+ */
+ public function testTheInitialBorderColourIsTheElementsColour()
+ {
+ $css = '.a { border: 1mm solid #00ff00; } .a p { color: #ff0000; border: 1mm solid #0000ff; } .a .k { {V} }';
+ $html = '
';
+
+ $this->assertSame(
+ $this->drawnPages('' . $html),
+ $this->drawnPages('' . $html)
+ );
+ }
+
+ /**
+ * @return array[] Each CSS mode, with how many red border sides the case above draws in it
+ */
+ public function modes()
+ {
+ return [CssMode::STANDARD => [CssMode::STANDARD, 8], CssMode::LEGACY => [CssMode::LEGACY, 4]];
+ }
+
+ /**
+ * The keyword draws as the declarations it resolves to
+ *
+ * @dataProvider shorthands
+ * @dataProvider cellsRowsAndTables
+ * @dataProvider builtInDefaults
+ * @dataProvider borderParts
+ * @dataProvider chains
+ *
+ * @param string $mode
+ * @param string $css With {V} for the subject's declarations
+ * @param string $html
+ * @param string $declarations The subject's declarations, with the keyword
+ * @param string $resolved What they should draw as
+ */
+ public function testTheKeywordDrawsAsWhatItResolvesTo($mode, $css, $html, $declarations, $resolved)
+ {
+ $this->assertSame(
+ $this->drawnPages('' . $html, ['cssMode' => $mode]),
+ $this->drawnPages('' . $html, ['cssMode' => $mode])
+ );
+ }
+
+ /**
+ * Each shorthand with each keyword. In the standard mode the keyword reaches each of its longhands. In the legacy
+ * mode a border or background shorthand draws as its initial value, except with revert-layer, which it drops, and
+ * font and list-style drop every keyword. border-color reads inherit as transparent and the other keywords as a
+ * colour it cannot read. A legacy column given as one value holds for every keyword
+ *
+ * @return array[]
+ */
+ public function shorthands()
+ {
+ $block = '
';
+ $list = '
';
+
+ $shorthands = [
+ 'font' => [
+ '.a { font: italic bold 20pt courier; } .a p { font: 8pt helvetica; } .a .k { {V} }',
+ $block,
+ ['inherit' => 'font: italic bold 20pt courier', 'initial' => 'font: 11pt ctimes', 'unset' => 'font: italic bold 20pt courier', 'revert' => 'font: italic bold 20pt courier'],
+ '',
+ ],
+ 'background' => [
+ '.a { background: #00ff00; } .a p { background: #0000ff; } .a .k { {V} }',
+ $block,
+ ['inherit' => 'background: #00ff00', 'initial' => 'background: none', 'unset' => 'background: none', 'revert' => 'background: none'],
+ ['inherit' => 'background: none', 'initial' => 'background: none', 'unset' => 'background: none', 'revert' => 'background: none', 'revert-layer' => ''],
+ ],
+ 'margin' => [
+ '.a { margin: 7mm; } .a p { margin: 2mm; } .a .k { {V} }',
+ $block,
+ ['inherit' => 'margin: 7mm', 'initial' => 'margin: 0', 'unset' => 'margin: 0', 'revert' => 'margin: 1.12em 0'],
+ 'margin: 0',
+ ],
+ 'border-width' => [
+ '.a { border: 2mm solid #00ff00; } .a p { border: 0.5mm solid #0000ff; } .a .k { {V} }',
+ $block,
+ ['inherit' => 'border-width: 2mm', 'initial' => 'border-width: medium', 'unset' => 'border-width: medium', 'revert' => 'border-width: medium'],
+ 'border-width: 0',
+ ],
+ 'border-style' => [
+ '.a { border: 1mm dotted #00ff00; } .a p { border: 1mm dashed #0000ff; } .a .k { {V} }',
+ $block,
+ ['inherit' => 'border-style: dotted', 'initial' => 'border-style: none', 'unset' => 'border-style: none', 'revert' => 'border-style: none'],
+ 'border-style: none',
+ ],
+ 'border-color' => [
+ '.a { border: 1mm solid #00ff00; } .a p { border: 1mm solid #0000ff; } .a .k { {V} }',
+ $block,
+ ['inherit' => 'border-color: #00ff00', 'initial' => 'border-color: #000000', 'unset' => 'border-color: #000000', 'revert' => 'border-color: #000000'],
+ ['inherit' => 'border-color: transparent', 'initial' => 'border-color: unreadable', 'unset' => 'border-color: unreadable', 'revert' => 'border-color: unreadable', 'revert-layer' => 'border-color: unreadable'],
+ ],
+ 'list-style' => [
+ '.a { list-style: square inside; } .a li { list-style: circle outside; } .a .k { {V} }',
+ $list,
+ ['inherit' => 'list-style: square inside', 'initial' => 'list-style: disc outside', 'unset' => 'list-style: square inside', 'revert' => 'list-style: square inside'],
+ '',
+ ],
+ ];
+
+ $rows = [];
+ foreach ($shorthands as $shorthand => $case) {
+ list($css, $html, $standard, $legacy) = $case;
+ $standard['revert-layer'] = $standard['revert'];
+ foreach (['inherit', 'initial', 'unset', 'revert', 'revert-layer'] as $keyword) {
+ $declaration = $shorthand . ': ' . $keyword;
+ $rows[$declaration] = [$css, $html, $declaration, $standard[$keyword], is_array($legacy) ? $legacy[$keyword] : $legacy];
+ }
+ }
+
+ return $this->bothModes($rows);
+ }
+
+ /**
+ * inherit on a cell takes a property that is not inherited from its row, not its table, and an inherited one
+ * from its row where the row sets it, and otherwise from its table. A row takes it from its row group, or from
+ * none where the HTML leaves the tbody out
+ *
+ * @return array[]
+ */
+ public function cellsRowsAndTables()
+ {
+ $row = '
';
+ $rows = '
';
+ $implied = '
';
+
+ return $this->bothModes([
+ 'a cell\'s background from its row' => [
+ '.t { background-color: #ffff00; } .r { background-color: #00ff00; } td { background-color: #0000ff; } td.k { {V} }',
+ $row,
+ 'background-color: inherit',
+ 'background-color: #00ff00',
+ 'background-color: currentcolor',
+ ],
+ 'a cell\'s border from its row, which has none' => [
+ '.t { border: 1mm solid #00ff00; } td { border: 0.5mm dashed #0000ff; } td.k { {V} }',
+ $row,
+ 'border: inherit',
+ 'border: none',
+ 'border: none',
+ ],
+ 'a cell\'s padding from its row, which has none' => [
+ '.t { padding: 5mm; } td { padding: 2mm; } td.k { {V} }',
+ $row,
+ 'padding: inherit',
+ 'padding: 0',
+ 'padding: 0',
+ ],
+ 'a cell\'s colour from its table' => [
+ '.t { color: #008000; } td { color: #ff0000; } td.k { {V} }',
+ $row,
+ 'color: inherit',
+ 'color: #008000',
+ 'color: currentcolor',
+ ],
+ 'a cell\'s colour from its row, over its table\'s' => [
+ '.t { color: #0000ff; } .r { color: #008000; } td { color: #ff0000; } td.k { {V} }',
+ $row,
+ 'color: inherit',
+ 'color: #008000',
+ 'color: currentcolor',
+ ],
+ 'a cell\'s font size from its table' => [
+ '.t { font-size: 16pt; } td { font-size: 8pt; } td.k { {V} }',
+ $row,
+ 'font-size: inherit',
+ 'font-size: 16pt',
+ 'font-size: auto',
+ ],
+ 'a row\'s background from its row group' => [
+ '.t { background-color: #ffff00; } .g { background-color: #00ff00; } tr { background-color: #0000ff; } tr.k { {V} }',
+ $rows,
+ 'background-color: inherit',
+ 'background-color: #00ff00',
+ 'background-color: currentcolor',
+ ],
+ 'a row\'s background in a tbody the HTML leaves out' => [
+ '.t { background-color: #ffff00; } tr { background-color: #0000ff; } tr.k { {V} }',
+ $implied,
+ 'background-color: inherit',
+ 'background-color: transparent',
+ 'background-color: currentcolor',
+ ],
+ ]);
+ }
+
+ /**
+ * revert gives an element the value the built-in defaults and the default stylesheet give it, over any author
+ * rule, and a presentational attribute counts as an author rule. With none, it is unset
+ *
+ * @return array[]
+ */
+ public function builtInDefaults()
+ {
+ return $this->bothModes([
+ 'a list\'s margin' => [
+ 'ul { margin: 5mm; } ul.k { {V} }',
+ '
',
+ 'margin: revert',
+ 'margin: 0.83em 0',
+ 'margin: 0',
+ ],
+ 'a nested list\'s margin, from the default stylesheet' => [
+ 'ul { margin: 5mm; } ul.k { {V} }',
+ '
',
+ 'margin: revert',
+ 'margin: 0',
+ 'margin: 0',
+ ],
+ 'a heading\'s size' => [
+ 'h1 { font-size: 9pt; } h1.k { {V} }',
+ '
subject
near miss
',
+ 'font-size: revert',
+ 'font-size: 2em',
+ 'font-size: auto',
+ ],
+ 'a header cell\'s weight' => [
+ 'th { font-weight: normal; } th.k { {V} }',
+ '
',
+ 'font-weight: revert',
+ 'font-weight: bold',
+ 'font-weight: normal',
+ ],
+ 'a cell\'s padding, over its table\'s cellpadding' => [
+ 'td { padding: 5mm; } td.k { {V} }',
+ '
',
+ 'padding: revert',
+ 'padding: 0.1em',
+ 'padding: 0',
+ ],
+ 'a paragraph\'s colour, which has no default' => [
+ 'div { color: #008000; } p { color: #ff0000; } p.k { {V} }',
+ '
',
+ 'color: revert',
+ 'color: #008000',
+ '',
+ ],
+ ]);
+ }
+
+ /**
+ * A keyword in a border side's width, style or colour resolves that part of the side, and a part declared after a
+ * border keyword still applies over it
+ *
+ * @return array[]
+ */
+ public function borderParts()
+ {
+ $block = '
';
+ $css = '.a { border: 1mm solid #00ff00; } .a p { border: 0.5mm dashed #0000ff; } .a .k { {V} }';
+
+ return $this->bothModes([
+ 'one side\'s colour' => [$css, $block, 'border-top-color: inherit', 'border-top-color: #00ff00', 'border-top-color: transparent'],
+ 'one side\'s width' => [$css, $block, 'border-left-width: inherit', 'border-left-width: 1mm', 'border-left-width: 0'],
+ 'one side\'s style' => [$css, $block, 'border-bottom-style: initial', 'border-bottom-style: none', 'border-bottom-style: none'],
+ 'one side' => [$css, $block, 'border-right: initial', 'border-right: none', 'border-right: none'],
+ 'a part after the keyword' => [
+ $css,
+ $block,
+ 'border: inherit; border-top-color: #ff0000',
+ 'border: 1mm solid #00ff00; border-top-color: #ff0000',
+ 'border: none; border-top-color: #ff0000',
+ ],
+ ]);
+ }
+
+ /**
+ * inherit reaches through an element that inherits the value itself, and at the top of the document takes the
+ * value body has, and on body the value html has. A block takes an inherited value mPDF does not hand it from its
+ * parent
+ *
+ * @return array[]
+ */
+ public function chains()
+ {
+ return $this->bothModes([
+ 'a background through a block' => [
+ '.a { background-color: #00ff00; } .a div { background-color: inherit; } .a p { background-color: #0000ff; } .a .k { {V} }',
+ '
',
+ 'background-color: inherit',
+ 'background-color: #00ff00',
+ 'background-color: currentcolor',
+ ],
+ 'a colour from body' => [
+ 'body { color: #008000; } p { color: #ff0000; } p.k { {V} }',
+ '
subject
near miss
',
+ 'color: inherit',
+ 'color: #008000',
+ 'color: currentcolor',
+ ],
+ 'a background from body' => [
+ 'body { background-color: #ffff00; } p { background-color: #0000ff; } p.k { {V} }',
+ '
subject
near miss
',
+ 'background-color: inherit',
+ 'background-color: #ffff00',
+ 'background-color: currentcolor',
+ ],
+ 'a nested list\'s markers, through the item it is in' => [
+ '.a { list-style: square; } .a ul { {V} }',
+ '
',
+ 'list-style: inherit',
+ 'list-style: square',
+ '',
+ ],
+ 'a text shadow, which mPDF does not pass from block to block' => [
+ '.a { text-shadow: 0.5mm 0.5mm #ff0000; } .a p { {V} }',
+ '
',
+ 'text-shadow: inherit',
+ 'text-shadow: 0.5mm 0.5mm #ff0000',
+ '',
+ ],
+ 'a colour from html, on body' => [
+ 'html { color: #008000; } body { color: #ff0000; } body { {V} }',
+ '
subject
',
+ 'color: inherit',
+ 'color: #008000',
+ 'color: currentcolor',
+ ],
+ 'a font size from html, on body' => [
+ 'html { font-size: 14pt; } body { font-size: 8pt; } body { {V} }',
+ '
subject
',
+ 'font-size: unset',
+ 'font-size: 14pt',
+ 'font-size: auto',
+ ],
+ 'a font size from body' => [
+ 'body { font-size: 14pt; } p { font-size: 8pt; } p.k { {V} }',
+ '
subject
near miss
',
+ 'font-size: inherit',
+ 'font-size: 14pt',
+ 'font-size: auto',
+ ],
+ ]);
+ }
+}
diff --git a/tests/Mpdf/DrawnStyles.php b/tests/Mpdf/DrawnStyles.php
index 608975795..b7e1c0503 100644
--- a/tests/Mpdf/DrawnStyles.php
+++ b/tests/Mpdf/DrawnStyles.php
@@ -114,6 +114,59 @@ private function assertDrawnInContext($context, TextRecordingMpdf $mpdf, array $
}
}
+ /**
+ * Everything a document draws, for comparing two documents that should draw the same: the content stream of
+ * each page, and each piece of text with its colour, font size, font style and box. The sizes and boxes are
+ * rounded, as a table keeps its font size in millimetres, which comes back a few units in the last place off
+ *
+ * @param string $html
+ * @param array $config Merged over the core-font mode
+ *
+ * @return array
+ */
+ private function drawnPages($html, array $config = [])
+ {
+ $mpdf = $this->drawDocument($html, $config);
+ $mpdf->SetCompression(false);
+
+ preg_match_all('/\d+ 0 obj\s*<<\/Length \d+>>\s*stream\n(.*?)\nendstream/s', $mpdf->Output('', 'S'), $streams);
+
+ $round = function ($number) {
+ return round($number, 6);
+ };
+
+ return [
+ $streams[1],
+ $mpdf->drawnText,
+ $mpdf->drawnColours,
+ array_map($round, $mpdf->drawnFontSize),
+ $mpdf->drawnFontStyles,
+ array_map(function ($box) use ($round) {
+ return array_map($round, $box);
+ }, $mpdf->drawnBoxes),
+ ];
+ }
+
+ /**
+ * Splits cases into one for each CSS mode
+ *
+ * @param array[] $rows Each case's arguments, ending with what it expects in the standard mode and in the legacy mode
+ *
+ * @return array[] Each case's arguments, led by the mode and ending with what it expects in that mode
+ */
+ private function bothModes(array $rows)
+ {
+ $cases = [];
+ foreach ($rows as $name => $row) {
+ $legacy = array_pop($row);
+ $standard = array_pop($row);
+ $cases[$name . ' (standard)'] = array_merge([CssMode::STANDARD], $row, [$standard]);
+ $cases[$name . ' (legacy)'] = array_merge([CssMode::LEGACY], $row, [$legacy]);
+ }
+
+ return $cases;
+ }
+
/**
* Keys what was recorded of each piece of text a document drew by the text itself, so a test can look a piece up
* by what it says
diff --git a/tests/Mpdf/OpenElementStackTest.php b/tests/Mpdf/OpenElementStackTest.php
index 81c38adf5..a0742fe8a 100644
--- a/tests/Mpdf/OpenElementStackTest.php
+++ b/tests/Mpdf/OpenElementStackTest.php
@@ -172,7 +172,6 @@ public function testAFrameDescribesItsElement()
$this->assertSame(['A', 'B'], $div['classes']);
$this->assertSame('de', $div['lang']);
$this->assertSame('Box', $div['attr']['TITLE']);
- $this->assertNull($div['computed']);
$p = $this->frameAt($mpdf, 'text', 2);
$this->assertSame('de', $p['lang']);
@@ -180,6 +179,31 @@ public function testAFrameDescribesItsElement()
$this->assertSame(['C'], $p['classes']);
}
+ /**
+ * In the standard CSS mode a frame carries the properties merged for its element, with the CSS-wide keywords
+ * resolved, and the document's frame carries those of body. A tbody the HTML leaves out is never styled. The
+ * legacy mode keeps none
+ */
+ public function testAFrameCarriesThePropertiesMergedForItsElement()
+ {
+ $html = ''
+ . '
';
+
+ $mpdf = $this->write($html);
+ $frames = $this->framesAt($mpdf, 'text');
+ $this->assertSame('#ff0', $frames[0]['computed']['BACKGROUND-COLOR']);
+ $this->assertSame('2mm', $frames[1]['computed']['PADDING-LEFT']);
+ $this->assertSame('2mm', $frames[2]['computed']['PADDING-LEFT']);
+ $this->assertNull($this->frameAt($mpdf, 'cell', 3)['computed']);
+ $this->assertSame('TBODY', $this->frameAt($mpdf, 'cell', 3)['tag']);
+
+ $mpdf = $this->mpdfRecording(['cssMode' => CssMode::LEGACY]);
+ $mpdf->WriteHTML($html, HTMLParserMode::DEFAULT_MODE, true, false);
+ foreach ($this->framesAt($mpdf, 'text') as $frame) {
+ $this->assertNull($frame['computed']);
+ }
+ }
+
/**
* Tables nested in cells keep their own rows and cells, and the outer cell is back on top after them
*/
diff --git a/tests/Snapshots/CssWideKeywordsInvoiceLegacySnapshotTest.php b/tests/Snapshots/CssWideKeywordsInvoiceLegacySnapshotTest.php
new file mode 100644
index 000000000..19bb3adeb
--- /dev/null
+++ b/tests/Snapshots/CssWideKeywordsInvoiceLegacySnapshotTest.php
@@ -0,0 +1,21 @@
+cssMode();
+ }
+
+ /**
+ * The invoice, each part under a caption saying what the standard and the legacy mode draw
+ */
+ public function generatePdf()
+ {
+ ob_start();
+ ?>
+
+
+
Written with cssMode set to cssMode(); ?>.
+
+
The heading: after a reset makes every h1 small, .invoice h1 reverts its size and margin. Standard: the title is large, with its built-in space below it. Legacy: it is small, and set right against the text below it.
+
The notice: its link takes the notice's red, bold text and drops the underline, and the reference inside it takes the notice's border. In both modes "pay online" is red, bold and not underlined: the legacy mode ignores a keyword on an inline element, which keeps the state of the text around it. Standard: "INV-0042" has a solid red border like the notice's. Legacy: it has no border.
+
The sender: strong is color: initial. Standard: "Blue Liquid Designs" is black. Legacy: it is dark blue like the text around it.
+
+
Invoice INV-0042
+
From Blue Liquid Designs, 1 Example Street.
+
Payment is due in 14 days: pay online quoting INV-0042.
+
+
+
The lines: the notes cell is color: unset, the total row's cells take the row's background, and the terms table nested in a cell reverts its cells' padding. Standard: the note is green, the table's colour; the total row is pale blue; the terms cells have thin padding. Legacy: the note is black; the total row's cells have no background at all, so the table's white shows; the terms cells have no padding.
+
+ | Item | Note | Amount |
+ | Design | Two rounds of changes | 1,200.00 |
+ | Hosting |
+
+ | 300.00 |
+ | Total | | 1,500.00 |
+
+
+
The steps: a nested list takes its parent list's square markers, and the current step takes the list's dark blue over the grey of the items. Standard: every marker is a square, and "Approve the design" is dark blue. Legacy: the nested items have the default markers of a nested list, and "Approve the design" is black.
+
+ - Send the brief
+ - Approve the design
+
+
+ - Launch
+
+ mpdf = $this->createMpdf(['cssMode' => $this->cssMode()]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/Snapshots/CssWideKeywordsInvoiceStandardSnapshotTest.php b/tests/Snapshots/CssWideKeywordsInvoiceStandardSnapshotTest.php
new file mode 100644
index 000000000..4970b78e2
--- /dev/null
+++ b/tests/Snapshots/CssWideKeywordsInvoiceStandardSnapshotTest.php
@@ -0,0 +1,21 @@
+
+
+
+
The CSS-wide keywords
+
+
color: inherit, unset and revert on a paragraph in a green box: all three lines are green.
+
inherit
unset
revert, with no built-in colour for a paragraph
+
+
color: initial on an emphasis in a green box: "initial" is black.
+
Green text initial and green again
+
+
font-size and font-weight: inherit: the line is as large and bold as the box's text.
+
+
+
font: initial: the line is in the document's default font and size, 9pt, and not bold. It stays red, as font does not set the colour.
+
+
+
border: inherit: the line has the box's solid green border, not a dashed red one.
+
+
+
border-top-color: inherit: the line's dashed border is green along the top and red on the other sides.
+
border-top-color: inherit
+
+
padding: inherit: the line's text sits 1.5mm inside its dashed red border.
+
+
+
background: inherit: the line's background is the box's pale green, not white.
+
+
+
border and background-color: unset, which are not inherited, so take their initial values: no border, and the box's pale green shows through.
+
+
+
list-style: inherit on the items of a list with square markers inside: both items have square markers inside the box.
+
+
+
+
+
margin: revert on the second list, after a rule that sets every list's margin to 0: the second list has the built-in space above and below it, the first none.
+
+
- A list with its built-in margin
+
Text after the lists.
+
+
font-size: revert on the second heading, after a rule that makes headings small: the second heading is its built-in size.
+
A small heading
+
A heading at its built-in size
+
+
A table with green text on pale yellow, whose row is pale green. The first cell's colour is inherited from the table: green. The second cell's background is inherited from its row: pale green, not the table's yellow. The third cell's padding reverts to the thin built-in padding. The header cell's weight reverts to bold.
+
+ | color: inherit |
+ background-color: inherit |
+ padding: revert |
+ font-weight: revert |
+
+ mpdf = $this->createMpdf(['cssMode' => \Mpdf\CssMode::STANDARD]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/data/snapshots/css-wide-keywords-invoice-legacy.pdf b/tests/data/snapshots/css-wide-keywords-invoice-legacy.pdf
new file mode 100644
index 000000000..db8aa3cff
Binary files /dev/null and b/tests/data/snapshots/css-wide-keywords-invoice-legacy.pdf differ
diff --git a/tests/data/snapshots/css-wide-keywords-invoice-standard.pdf b/tests/data/snapshots/css-wide-keywords-invoice-standard.pdf
new file mode 100644
index 000000000..143828648
Binary files /dev/null and b/tests/data/snapshots/css-wide-keywords-invoice-standard.pdf differ
diff --git a/tests/data/snapshots/css-wide-keywords.pdf b/tests/data/snapshots/css-wide-keywords.pdf
new file mode 100644
index 000000000..c61572606
Binary files /dev/null and b/tests/data/snapshots/css-wide-keywords.pdf differ