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('

fixed p

'); $this->assertSame(self::RED, $colours['fixed p']); @@ -56,7 +56,7 @@ public function testIdDescendantRulesReachTheContentOfAFixedBlock() */ public function testTheContentIsAChildOfTheBlockItself() { - $colours = $this->colours(' + $colours = $this->textColours('

direct p

nested p

'); $this->assertSame(self::BLACK, $colours['direct p']); @@ -68,7 +68,7 @@ public function testTheContentIsAChildOfTheBlockItself() */ public function testTheRulesOfOneBlockDoNotReachTheNext() { - $colours = $this->colours(' + $colours = $this->textColours('

boxed p

plain p

'); @@ -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 . '
' . $content . '
'; + } + + /** + * 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', '
' . self::image() . '
'], + ]; + } + + /** + * @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, plainPlain, 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

  • First item
  • Second item

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.

+ + + + +
+ Outer bold + + +
Inner bold
+
+ +

Header, body and footer cells

+

White on blue in the header, green notes in the body and brown italic in the footer.

+ + + + + + + + + + + + +
RowDescription
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.

+
+
Fixed cell text
+
+ 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