diff --git a/src/Css/CssMerger.php b/src/Css/CssMerger.php
index b71391331..f7603916a 100644
--- a/src/Css/CssMerger.php
+++ b/src/Css/CssMerger.php
@@ -697,6 +697,12 @@ protected function mergeDescendantSelectors($inherit, $tag, $attr, $classes)
return;
}
+ // Content of a table cell pushes no block level, so its descendant rules are the cell's
+ if (isset($this->cssManager->tablecascadeCSS[$this->cssManager->tbCSSlvl])) {
+ $this->mergeDescendantCss($this->cssManager->tablecascadeCSS[$this->cssManager->tbCSSlvl], $tag, $attr, $classes);
+ return;
+ }
+
$level = $this->getBlockLevel($inherit);
if (!isset($this->mpdf->blk[$level]['cascadeCSS'])) {
return;
diff --git a/tests/Mpdf/ImagePercentageInTableCellTest.php b/tests/Mpdf/ImagePercentageInTableCellTest.php
index fffc7f7f1..ff5cdf527 100644
--- a/tests/Mpdf/ImagePercentageInTableCellTest.php
+++ b/tests/Mpdf/ImagePercentageInTableCellTest.php
@@ -56,40 +56,6 @@ private function middleOfThree($content)
return $this->table([['33.3333%', 'one'], ['33.3333%', $content], ['', 'three']]);
}
- /**
- * Where each image on a page is placed, keyed w/h/x in millimetres
- *
- * @param string $html
- * @param int $page Counted from 0
- *
- * @return array[]
- */
- private function placements($html, $page = 0)
- {
- $pages = $this->pages($this->render($html));
- $this->assertArrayHasKey($page, $pages);
- preg_match_all('/([-\d.]+) 0 0 ([-\d.]+) ([-\d.]+) [-\d.]+ cm \/I\d+ Do/', $pages[$page], $matches, PREG_SET_ORDER);
-
- return array_map(function ($match) {
- return ['w' => $match[1] / Mpdf::SCALE, 'h' => $match[2] / Mpdf::SCALE, 'x' => $match[3] / Mpdf::SCALE];
- }, $matches);
- }
-
- /**
- * The width of the only image on the first page, in millimetres
- *
- * @param string $html
- *
- * @return float
- */
- private function drawnWidth($html)
- {
- $placements = $this->placements($html);
- $this->assertCount(1, $placements);
-
- return $placements[0]['w'];
- }
-
/**
* Lengths given as percentages, each in the middle of three 60mm columns, and an absolute length that means the
* same in a cell as anywhere else
@@ -138,7 +104,7 @@ public function testThePictureLeavesTheColumnWidthsAlone()
{
$html = $this->table([['20%', 'one'], ['20%', $this->image('max-width: 100%')], ['60%', 'three']]);
- $placement = $this->placements($html)[0];
+ $placement = $this->imagePlacements($html)[0];
$this->assertEqualsWithDelta(36, $placement['w'], 0.05);
$this->assertEqualsWithDelta(15 + 36, $placement['x'], 0.05);
}
@@ -164,7 +130,7 @@ public function testInANestedTableAPercentageIsOfTheInnerCell()
. $this->image('width: 100%') . '';
$html = $this->table([['50%', 'one'], ['50%', $inner]]);
- $placement = $this->placements($html)[0];
+ $placement = $this->imagePlacements($html)[0];
$this->assertEqualsWithDelta(45, $placement['w'], 0.05);
$this->assertEqualsWithDelta(15 + 90 + 45, $placement['x'], 0.05);
}
@@ -224,8 +190,8 @@ public function testARotatedTableMovedToANewPageSizesItsPictureAgainstTheCell()
. '
' . $this->image('width: 70mm') . ' | '
. '' . $this->image('max-width: 100%') . ' | ';
- $this->assertEqualsWithDelta(40, $this->placements($table)[1]['w'], 0.05, 'at the top of a page');
- $this->assertEqualsWithDelta(40, $this->placements('' . $table, 1)[1]['w'], 0.05, 'moved to a new page');
+ $this->assertEqualsWithDelta(40, $this->imagePlacements($table)[1]['w'], 0.05, 'at the top of a page');
+ $this->assertEqualsWithDelta(40, $this->imagePlacements('' . $table, 1)[1]['w'], 0.05, 'moved to a new page');
}
/**
diff --git a/tests/Mpdf/PageStreams.php b/tests/Mpdf/PageStreams.php
index b576fd11e..e2c275601 100644
--- a/tests/Mpdf/PageStreams.php
+++ b/tests/Mpdf/PageStreams.php
@@ -254,6 +254,60 @@ private function clipAndPlacement($stream)
return [$matches[1], ['w' => $cm[0], 'h' => $cm[3], 'x' => $cm[4], 'y' => $cm[5]]];
}
+ /**
+ * Where each image on a page is placed, keyed w/h/x in millimetres
+ *
+ * @param string $html
+ * @param int $page Counted from 0
+ *
+ * @return array[]
+ */
+ private function imagePlacements($html, $page = 0)
+ {
+ $pages = $this->pages($this->render($html));
+ $this->assertArrayHasKey($page, $pages);
+ preg_match_all('/([-\d.]+) 0 0 ([-\d.]+) ([-\d.]+) [-\d.]+ cm \/I\d+ Do/', $pages[$page], $matches, PREG_SET_ORDER);
+
+ return array_map(function ($match) {
+ return ['w' => $match[1] / Mpdf::SCALE, 'h' => $match[2] / Mpdf::SCALE, 'x' => $match[3] / Mpdf::SCALE];
+ }, $matches);
+ }
+
+ /**
+ * The width of the only image on the first page, in millimetres
+ *
+ * @param string $html
+ *
+ * @return float
+ */
+ private function drawnWidth($html)
+ {
+ $placements = $this->imagePlacements($html);
+ $this->assertCount(1, $placements);
+
+ return $placements[0]['w'];
+ }
+
+ /**
+ * The fill colour each piece of text on the first page is drawn in, keyed by that text
+ *
+ * @param string $html
+ *
+ * @return string[]
+ */
+ private function textColours($html)
+ {
+ $pages = $this->pages($this->render($html));
+ preg_match_all('/q ([\d. ]+ (?:rg|g)) .*?\((.*?)\) Tj/', $pages[0], $drawn, PREG_SET_ORDER);
+
+ $colours = [];
+ foreach ($drawn as $text) {
+ $colours[$text[2]] = $text[1];
+ }
+
+ return $colours;
+ }
+
/**
* $needle appears $count times in the string for page $page and not at all in the others
*/
diff --git a/tests/Mpdf/PositionedBlockSelectorTest.php b/tests/Mpdf/PositionedBlockSelectorTest.php
index 453e0567f..c0f002f1a 100644
--- a/tests/Mpdf/PositionedBlockSelectorTest.php
+++ b/tests/Mpdf/PositionedBlockSelectorTest.php
@@ -25,7 +25,7 @@ class PositionedBlockSelectorTest extends TestCase
*/
public function testClassDescendantRulesReachTheContentOfAnAbsoluteBlock()
{
- $colours = $this->colours('
+ $colours = $this->textColours('
');
$this->assertSame(self::RED, $colours['fixed p']);
@@ -56,7 +56,7 @@ public function testIdDescendantRulesReachTheContentOfAFixedBlock()
*/
public function testTheContentIsAChildOfTheBlockItself()
{
- $colours = $this->colours('
+ $colours = $this->textColours('
');
$this->assertSame(self::BLACK, $colours['direct p']);
@@ -68,7 +68,7 @@ public function testTheContentIsAChildOfTheBlockItself()
*/
public function testTheRulesOfOneBlockDoNotReachTheNext()
{
- $colours = $this->colours('
+ $colours = $this->textColours('
');
@@ -76,24 +76,4 @@ public function testTheRulesOfOneBlockDoNotReachTheNext()
$this->assertSame(self::BLACK, $colours['plain p']);
}
- /**
- * The fill colour each piece of text on the first page is drawn in, keyed by that text
- *
- * @param string $html
- *
- * @return string[]
- */
- private function colours($html)
- {
- $pages = $this->pages($this->render($html));
- preg_match_all('/q ([\d. ]+ (?:rg|g)) .*?\((.*?)\) Tj/', $pages[0], $drawn, PREG_SET_ORDER);
-
- $colours = [];
- foreach ($drawn as $text) {
- $colours[$text[2]] = $text[1];
- }
-
- return $colours;
- }
-
}
diff --git a/tests/Mpdf/TableCellDescendantSelectorTest.php b/tests/Mpdf/TableCellDescendantSelectorTest.php
new file mode 100644
index 000000000..0aa9f7899
--- /dev/null
+++ b/tests/Mpdf/TableCellDescendantSelectorTest.php
@@ -0,0 +1,102 @@
+' . $css . '';
+ }
+
+ /**
+ * A picture 292px wide, 77.3mm at 96dpi
+ *
+ * @return string
+ */
+ private static function image()
+ {
+ return '
';
+ }
+
+ /**
+ * Rules that should reach a picture in the cell, and the cell content holding it
+ *
+ * @return array[]
+ */
+ public static function matchingRules()
+ {
+ return [
+ 'cell and tag' => ['td img', self::image()],
+ 'the whole chain' => ['table tr td img', self::image()],
+ 'classes' => ['.t .r .c .x', self::image()],
+ 'cell class and tag' => ['td.c img', self::image()],
+ 'a block outside the table' => ['div.d img', self::image()],
+ 'a table nested in the cell' => ['td.c img', ''],
+ ];
+ }
+
+ /**
+ * @dataProvider matchingRules
+ *
+ * @param string $selector
+ * @param string $content
+ */
+ public function testARuleReachesAPictureInTheCell($selector, $content)
+ {
+ $this->assertEqualsWithDelta(20, $this->drawnWidth($this->table($selector . ' { max-width: 20mm; }', $content)), 0.01);
+ }
+
+ /**
+ * A rule for a cell of another class leaves the picture alone
+ */
+ public function testARuleForAnotherCellDoesNotMatch()
+ {
+ $width = $this->drawnWidth($this->table('td.other img { max-width: 20mm; }', self::image()));
+
+ $this->assertEqualsWithDelta(292 * 25.4 / 96, $width, 0.01);
+ }
+
+ /**
+ * Rules that should colour the word "red" in the cell
+ *
+ * @return array[]
+ */
+ public static function textRules()
+ {
+ return [
+ 'inline' => ['td span', 'plain red'],
+ 'block' => ['td p', 'red
'],
+ ];
+ }
+
+ /**
+ * @dataProvider textRules
+ *
+ * @param string $selector
+ * @param string $content
+ */
+ public function testARuleReachesTextInTheCell($selector, $content)
+ {
+ $colours = $this->textColours($this->table($selector . ' { color: #ff0000; }', $content));
+
+ $this->assertSame(self::RED, $colours['red']);
+ }
+
+}
diff --git a/tests/Snapshots/TableCellDescendantSelectorSnapshotTest.php b/tests/Snapshots/TableCellDescendantSelectorSnapshotTest.php
new file mode 100644
index 000000000..01628c7e8
--- /dev/null
+++ b/tests/Snapshots/TableCellDescendantSelectorSnapshotTest.php
@@ -0,0 +1,142 @@
+
+
+
+ Descendant rules reaching into table cells
+
+ Pictures sized by td img and td.thumb img
+ Three 60mm columns. The first two pictures fill their cells; the thumbnail stops at 20mm with rounded corners.
+
+
+ Tag and class chains
+ Red bold words from table.chain tr td span; yellow highlight from .grid .row .cell .mark.
+
+
+ | Plain, red and bold, plain |
+ Plain, highlighted, both |
+
+
+
+ Blocks in a cell, chosen by the cell's class
+ Blue italic paragraph and green list items in td.note; magenta paragraph in td.plain; nothing in the unclassed cell.
+
+
+ A note paragraph |
+ A plain paragraph |
+ An unstyled paragraph |
+
+
+
+ Rules naming a block outside the table
+ Purple underlined emphasis from div.report td em; the div.elsewhere rule, which would make it large and red, does not apply.
+
+
+ | Some emphasised words |
+
+
+
+ A table nested in a cell
+ Orange bold in the inner table from td.outer td b; the bold directly in the outer cell, not in a td inside it, stays black.
+
+
+ Header, body and footer cells
+ White on blue in the header, green notes in the body and brown italic in the footer.
+
+
+ | Row | Description |
+
+
+ | End of the table |
+
+
+
+ | Row (note ) |
+
+
+
+
+ A table in a positioned block
+ White on teal words from #side td span, in the block positioned at the bottom right of this page.
+
+ mpdf = $this->createMpdf();
+ $this->mpdf->SetBasePath(__DIR__ . '/../data');
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/data/snapshots/image-border-radius.pdf b/tests/data/snapshots/image-border-radius.pdf
index 7ea831e33..73f781e88 100644
Binary files a/tests/data/snapshots/image-border-radius.pdf and b/tests/data/snapshots/image-border-radius.pdf differ
diff --git a/tests/data/snapshots/table-cell-descendant-selector.pdf b/tests/data/snapshots/table-cell-descendant-selector.pdf
new file mode 100644
index 000000000..056e20efb
Binary files /dev/null and b/tests/data/snapshots/table-cell-descendant-selector.pdf differ