diff --git a/CHANGELOG-GRAVITYPDF.md b/CHANGELOG-GRAVITYPDF.md index 35e803e3f..76528bf61 100644 --- a/CHANGELOG-GRAVITYPDF.md +++ b/CHANGELOG-GRAVITYPDF.md @@ -61,6 +61,12 @@ Read this section before upgrading from upstream mPDF. Each entry says what chan * `initial` gives the property's initial value, such as `currentColor` for a border's colour, and the document's default font and size for `font-family` and `font-size`. `unset` is `inherit` for an inherited property and `initial` for any other. * `revert` and `revert-layer` give the value of the built-in defaults and the default stylesheet, such as a heading's size, a list's margin or a cell's padding, and are `unset` where those give none. An HTML attribute such as `cellpadding` counts as an author rule, so `revert` goes past it too. * A shorthand passes the keyword to each of its longhands, so `border: inherit` takes every side's width, style and colour, and `font: initial` resets the size, family, style, weight, line height and variants. +* **A table inherits from the block around it**, as in a browser, under the standard `cssMode`. It used to start from the document's defaults, so `
` around a table left its cells black and in the body's font. Now a table takes every inherited property from the block, list item, header or positioned block it is in, and a nested table takes its cell's `text-align` and `line-height` too. [#543] [#656] + * A table inside an inline element, as in `……
…
`, takes the element's style, as a block does since [#653]. The text after the table, up to the element's end tag, is drawn in that style again. Before, it lost it, and in a table cell the style ran on past the end tag. + * `table { font-size: 80% }` is taken of the block's size, not the document's. + * The built-in default style no longer gives a table `line-height: 1.2`, `hyphens: manual` and `font-kerning: auto`, which a browser's does not, so a table inherits them. A table in a block with no `line-height` of its own now takes `normal`, and its rows are drawn as far apart as a paragraph's lines, at `normalLineheight`, instead of at 1.2. To keep the old spacing, add `table { line-height: 1.2; }` to the stylesheet. + * A `th` in a table in a right-aligned block is aligned right, since it inherits an alignment. + * Set `'cssMode' => \Mpdf\CssMode::LEGACY` to keep tables starting from the document's defaults. New features ------------ @@ -918,3 +924,5 @@ These changes do not change output. [#653]: https://github.com/GravityPDF/mpdf/pull/653 [#542]: https://github.com/GravityPDF/mpdf/issues/542 [#655]: https://github.com/GravityPDF/mpdf/pull/655 +[#543]: https://github.com/GravityPDF/mpdf/issues/543 +[#656]: https://github.com/GravityPDF/mpdf/pull/656 diff --git a/src/Css/CssMerger.php b/src/Css/CssMerger.php index cd1103bdc..980fbc64c 100644 --- a/src/Css/CssMerger.php +++ b/src/Css/CssMerger.php @@ -51,6 +51,13 @@ class CssMerger */ const BORDER_SIDE_PROPERTY = '/^(BORDER-(?:TOP|RIGHT|BOTTOM|LEFT))(?:-(WIDTH|STYLE|COLOR))?$/'; + /** + * The inherited properties the built-in default style (DefaultCss) sets on a table, with their values. A browser's + * default style sets none, so under the standard cascade a table takes them from what it is in. A value changed + * through the defaultCSS option still applies + */ + const TABLE_INHERITED_DEFAULTS = ['LINE-HEIGHT' => '1.2', 'HYPHENS' => 'manual', 'FONT-KERNING' => 'auto']; + /** * @var \Mpdf\Mpdf */ @@ -772,7 +779,8 @@ protected function mergeBlockCascadingCss($inherit, $tag, $attr, $classes) } /** - * Merge the properties a block inherits from the block it is opened in. + * Merge the properties a block inherits from the block it is opened in. Under the standard cascade, a table that + * is not in a cell inherits from the block it is opened in too. * * @param string $inherit Inheritance type (TOPTABLE, TABLE, BLOCK) * @param string $tag HTML tag name @@ -780,11 +788,12 @@ protected function mergeBlockCascadingCss($inherit, $tag, $attr, $classes) */ protected function mergeInheritedBlockProperties($inherit, $tag) { - if ($inherit !== 'BLOCK') { + $table = $inherit === 'TOPTABLE' && $this->mpdf->cssMode === CssMode::STANDARD; + if ($inherit !== 'BLOCK' && !$table) { return; } - $previousBlockLevel = $this->getBlockLevel(); + $previousBlockLevel = $this->getBlockLevel($inherit); $previousBlock = isset($this->mpdf->blk[$previousBlockLevel]) ? $this->mpdf->blk[$previousBlockLevel] : []; // Block properties which are inherited @@ -868,6 +877,11 @@ protected function mergeInheritedBlockProperties($inherit, $tag) } $this->cssProperties = array_merge($this->cssProperties, $converted); // mPDF 5.7.1 } + + if ($table) { + // Not the margin collapse, column background, text decoration or vertical-align a child block takes + $this->cssProperties = InheritedProperties::of($this->cssProperties, InheritedProperties::names()); + } } /** @@ -955,7 +969,12 @@ protected function mergeDefaultCss($tag) return; } - $zp = $this->normalizeProperties->normalize($this->mpdf->defaultCSS[$tag]); + $defaults = $this->mpdf->defaultCSS[$tag]; + if ($tag === 'TABLE' && $this->mpdf->cssMode === CssMode::STANDARD) { + $defaults = array_diff_assoc($defaults, self::TABLE_INHERITED_DEFAULTS); + } + + $zp = $this->normalizeProperties->normalize($defaults); if (is_array($zp)) { // Default overwrites Inherited $this->cssProperties = array_merge($this->cssProperties, $zp); // !! Note other way round !! $this->mergeBorderProperties($zp); diff --git a/src/Css/InheritedProperties.php b/src/Css/InheritedProperties.php index 26c052cb7..5b3adca95 100644 --- a/src/Css/InheritedProperties.php +++ b/src/Css/InheritedProperties.php @@ -10,17 +10,21 @@ * * - a block to its child blocks: CssMerger::mergeInheritedBlockProperties() reads TEXT back from the block's saved * text state through InlinePropertyConverter, and BLOCK from the block's level of the block stack; - * - inline elements to a block opened inside them: BlockTag sets their text state aside on the enclosing block, and - * blockTextState() gives it to mergeInheritedBlockProperties() in place of the enclosing block's own; + * - inline elements to a block or table opened inside them: BlockTag and Table set their text state aside on the + * enclosing block, and blockTextState() gives it to mergeInheritedBlockProperties() in place of the enclosing + * block's own; * - a positioned block to its content: Mpdf::WriteFixedPosHTML() puts TEXT and BLOCK from the block's merged CSS on * the
that stands in for the block; + * - a block to a table in it: CssMerger::mergeInheritedBlockProperties() gives the table TEXT and BLOCK from the block, + * as it does a child block; * - a table to its cells: Table::open() puts TEXT and text-align from the table's merged CSS in * base_table_properties, which each cell merges under its own CSS. The table also hands on text-align, * line-height, the line stacking and direction through its own fields; * - a row group and a row to their cells: THead, TBody, TFoot and Tr put TEXT and BLOCK from their merged CSS over * base_table_properties, and each cell merges what its row holds under its own CSS; * - a table cell to a table nested in it: Table::open() starts the nested table's base_table_properties and default - * font from the cell's text state, read back through InlinePropertyConverter. + * font from the cell's text state, read back through InlinePropertyConverter, and merges the table's CSS over the + * cell's line-height and text-align. * * To carry another property, add it to TEXT when it lives in the text state that Mpdf::saveInlineProperties() saves, * and make InlinePropertyConverter::convert() read it back. Otherwise add it to BLOCK, and carry it on the block stack @@ -131,8 +135,9 @@ public static function of(array $properties, array $names) } /** - * The text state a block opened inside the block at a level inherits: that of the inline elements it opened in, - * which BlockTag sets aside on the enclosing block under CssMode::STANDARD, or else the enclosing block's own + * The text state a block or table opened inside the block at a level inherits: that of the inline elements it + * opened in, which BlockTag and Table set aside on the enclosing block under CssMode::STANDARD, or else the + * enclosing block's own * * @param array $blocks The block stack, as Mpdf::$blk * @param int $level diff --git a/src/Tag/BlockTag.php b/src/Tag/BlockTag.php index 4d986d6ab..ea7a3b25b 100644 --- a/src/Tag/BlockTag.php +++ b/src/Tag/BlockTag.php @@ -1409,11 +1409,7 @@ public function close(&$ahtml, &$ihtml) $this->mpdf->lastblocklevelchange = -1; // Reset Inline-type properties - if (isset($this->mpdf->blk[$this->mpdf->blklvl]['openInline'])) { - $this->putBackOpenInlineElements(); - } elseif (isset($this->mpdf->blk[$this->mpdf->blklvl]['InlineProperties'])) { - $this->mpdf->restoreInlineProperties($this->mpdf->blk[$this->mpdf->blklvl]['InlineProperties']); - } + $this->restoreBlockTextState(); $this->mpdf->x = $this->mpdf->lMargin + $this->mpdf->blk[$this->mpdf->blklvl]['outer_left_margin']; @@ -1436,44 +1432,4 @@ public function close(&$ahtml, &$ihtml) } } - /** - * Sets aside the inline elements a block opens inside, under CssMode::STANDARD: their saved states and the text - * state they set go on the enclosing block. The block inherits that text state, and close() puts both back, so the - * text after the block is drawn in their style and their end tags restore what was there before them. - * - * A block opened a second time, as a kept block laid out again or a block reopened after a forced page break, finds - * them already set aside and keeps them, as by then the text state has changed - */ - private function setOpenInlineElementsAside() - { - $block = &$this->mpdf->blk[$this->mpdf->blklvl]; - - if (!isset($block['openInline']) && array_filter($this->mpdf->InlineProperties)) { - $block['openInline'] = [ - 'properties' => $this->mpdf->InlineProperties, - 'state' => $this->mpdf->saveInlineProperties(), - ]; - - // The line before the block is printed next, and its text with no colour or link of its own is drawn in the - // current state - if (isset($block['InlineProperties'])) { - $this->mpdf->restoreInlineProperties($block['InlineProperties']); - } - } - - $this->mpdf->InlineProperties = []; - } - - /** - * Puts back the inline elements setOpenInlineElementsAside() set aside on the block that is current again - */ - private function putBackOpenInlineElements() - { - $block = &$this->mpdf->blk[$this->mpdf->blklvl]; - - $this->mpdf->InlineProperties = $block['openInline']['properties']; - $this->mpdf->restoreInlineProperties($block['openInline']['state']); - unset($block['openInline']); - } - } diff --git a/src/Tag/Table.php b/src/Tag/Table.php index 808dac0ef..f88374c7a 100644 --- a/src/Tag/Table.php +++ b/src/Tag/Table.php @@ -23,8 +23,13 @@ public function open($attr, &$ahtml, &$ihtml) $this->mpdf->colvAlign = ''; } // *COLUMNS* - // The page break comes first, so the blocks it opens again on the new page are started below if (!$this->mpdf->tableLevel) { + if ($this->mpdf->cssMode === CssMode::STANDARD) { + // It inherits the inline elements it is opened in, as a block does, set aside before the page break + $this->setOpenInlineElementsAside(); + } + + // The page break comes first, so the blocks it opens again on the new page are started below $preview = $this->cssManager->PreviewBlockCSS('TABLE', $attr); if ($this->forcesPageBreak($preview, 'PAGE-BREAK-BEFORE')) { $this->forcePageBreak(strtoupper($preview['PAGE-BREAK-BEFORE'])); @@ -53,6 +58,10 @@ public function open($attr, &$ahtml, &$ihtml) if ($this->mpdf->tableLevel) { // i.e. now a nested table coming... // Save current level table $this->mpdf->cell['PARENTCELL'] = $this->mpdf->saveInlineProperties(); + if ($this->mpdf->cssMode === CssMode::STANDARD) { + // The inline elements open in the cell, whose end tags after the nested table restore what was before them + $this->mpdf->cell['PARENTINLINE'] = $this->mpdf->InlineProperties; + } $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['baseProperties'] = $this->mpdf->base_table_properties; $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['cells'] = $this->mpdf->cell; $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['currrow'] = $this->mpdf->row; @@ -61,6 +70,7 @@ public function open($attr, &$ahtml, &$ihtml) $this->mpdf->tableLevel++; $this->cssManager->tbCSSlvl++; + $cellInherited = []; if ($this->mpdf->tableLevel > 1) { // inherit table properties from cell in which nested if ($this->mpdf->cssMode === CssMode::LEGACY) { //$this->mpdf->base_table_properties['FONT-KERNING'] = ($this->mpdf->textvar & TextVars::FC_KERNING); // mPDF 6 @@ -75,6 +85,14 @@ public function open($attr, &$ahtml, &$ihtml) $cellLineHeight = $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['cellLineHeight']; $cellLineStackingStrategy = $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['cellLineStackingStrategy']; $cellLineStackingShift = $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['cellLineStackingShift']; + if ($this->mpdf->cssMode === CssMode::STANDARD) { + // It inherits the cell's line-height and text-align through its merged CSS too + $outerTable = $this->mpdf->table[$this->mpdf->tableLevel - 1][$this->mpdf->tbctr[$this->mpdf->tableLevel - 1]]; + $cellInherited = array_filter([ + 'LINE-HEIGHT' => $cellLineHeight, + 'TEXT-ALIGN' => isset($outerTable['cellTextAlign']) ? $outerTable['cellTextAlign'] : '', + ]); + } } if (isset($this->mpdf->tbctr[$this->mpdf->tableLevel])) { @@ -171,7 +189,7 @@ public function open($attr, &$ahtml, &$ihtml) if ($this->cssManager->tbCSSlvl == 1) { $properties = $this->cssManager->MergeCSS('TOPTABLE', 'TABLE', $attr); } else { - $properties = $this->cssManager->MergeCSS('TABLE', 'TABLE', $attr); + $properties = $this->cssManager->MergeCSS('TABLE', 'TABLE', $attr, $cellInherited); } $w = ''; @@ -307,9 +325,15 @@ public function open($attr, &$ahtml, &$ihtml) $this->mpdf->base_table_properties['FONT-FAMILY'] = $this->mpdf->FontFamily; if (isset($properties['FONT-SIZE'])) { - $parentSize = $this->mpdf->tableLevel > 1 - ? $this->sizeConverter->convert($this->mpdf->base_table_properties['FONT-SIZE']) - : $this->mpdf->default_font_size / Mpdf::SCALE; + $inheritedState = InheritedProperties::blockTextState($this->mpdf->blk, $this->mpdf->blklvl); + if ($this->mpdf->tableLevel > 1) { + $parentSize = $this->sizeConverter->convert($this->mpdf->base_table_properties['FONT-SIZE']); + } elseif ($this->mpdf->cssMode === CssMode::STANDARD && isset($inheritedState['size'])) { + // Its size is relative to the size it inherits, the block's or the inline elements' it is opened in + $parentSize = $inheritedState['size']; + } else { + $parentSize = $this->mpdf->default_font_size / Mpdf::SCALE; + } $mmsize = $this->sizeConverter->convertFontSizeToMm($properties['FONT-SIZE'], $parentSize); if ($mmsize) { $this->mpdf->default_font_size = $mmsize * Mpdf::SCALE; @@ -728,6 +752,10 @@ public function close(&$ahtml, &$ihtml) } unset($this->mpdf->cell['PARENTCELL']); } + if (isset($this->mpdf->cell['PARENTINLINE'])) { + $this->mpdf->InlineProperties = $this->mpdf->cell['PARENTINLINE']; + unset($this->mpdf->cell['PARENTINLINE']); + } $this->mpdf->row = $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['currrow']; $this->mpdf->col = $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['currcol']; $objattr = []; @@ -1246,9 +1274,7 @@ public function close(&$ahtml, &$ihtml) $this->mpdf->SetFont($this->mpdf->default_font, '', 0, false); $this->mpdf->SetLineHeight(); - if (isset($this->mpdf->blk[$this->mpdf->blklvl]['InlineProperties'])) { - $this->mpdf->restoreInlineProperties($this->mpdf->blk[$this->mpdf->blklvl]['InlineProperties']); - } + $this->restoreBlockTextState(); if ($page_break_after) { $this->forcePageBreak($page_break_after); diff --git a/src/Tag/Tag.php b/src/Tag/Tag.php index 4057fae2a..9671d2fc7 100644 --- a/src/Tag/Tag.php +++ b/src/Tag/Tag.php @@ -318,4 +318,50 @@ abstract public function open($attr, &$ahtml, &$ihtml); abstract public function close(&$ahtml, &$ihtml); + /** + * Sets aside the inline elements a block or table opens inside, under CssMode::STANDARD: their saved states and the + * text state they set go on the enclosing block. The block or table inherits that text state, and + * restoreBlockTextState() puts both back when it closes, so the text after it is drawn in their style and their end tags restore what was there before them. + * + * A block opened a second time, as a kept block laid out again or a block reopened after a forced page break, finds + * them already set aside and keeps them, as by then the text state has changed + */ + protected function setOpenInlineElementsAside() + { + $block = &$this->mpdf->blk[$this->mpdf->blklvl]; + + if (!isset($block['openInline']) && array_filter($this->mpdf->InlineProperties)) { + $block['openInline'] = [ + 'properties' => $this->mpdf->InlineProperties, + 'state' => $this->mpdf->saveInlineProperties(), + ]; + + // The line before the block or table is printed next, and its text with no colour or link of its own is drawn in the + // current state + if (isset($block['InlineProperties'])) { + $this->mpdf->restoreInlineProperties($block['InlineProperties']); + } + } + + $this->mpdf->InlineProperties = []; + } + + /** + * Restores, when a block or table closes, the text state of the block that is current again: that of the inline + * elements setOpenInlineElementsAside() set aside on it, which are put back, or else the block's own + */ + protected function restoreBlockTextState() + { + $state = InheritedProperties::blockTextState($this->mpdf->blk, $this->mpdf->blklvl); + if ($state !== null) { + $this->mpdf->restoreInlineProperties($state); + } + + $block = &$this->mpdf->blk[$this->mpdf->blklvl]; + if (isset($block['openInline'])) { + $this->mpdf->InlineProperties = $block['openInline']['properties']; + unset($block['openInline']); + } + } + } diff --git a/src/Tag/Td.php b/src/Tag/Td.php index 3b8df4e98..231984589 100644 --- a/src/Tag/Td.php +++ b/src/Tag/Td.php @@ -176,6 +176,8 @@ public function open($attr, &$ahtml, &$ihtml) if ($this->mpdf->cssMode === CssMode::STANDARD) { $properties = $this->mergeInheritedCss($tag, $attr, isset($table['rowInherited']) ? $table['rowInherited'] : $this->mpdf->base_table_properties); + // For a table nested in the cell, which inherits it + $table['cellTextAlign'] = isset($properties['TEXT-ALIGN']) ? $properties['TEXT-ALIGN'] : ''; } else { $properties = $this->cssManager->MergeCSS('TABLE', $tag, $attr); $properties = Arrays::uniqueRecursiveMerge($this->mpdf->base_table_properties, $properties); diff --git a/tests/Mpdf/BlockInsideInlineTest.php b/tests/Mpdf/BlockInsideInlineTest.php index dbb1ac4a2..39ed1ace7 100644 --- a/tests/Mpdf/BlockInsideInlineTest.php +++ b/tests/Mpdf/BlockInsideInlineTest.php @@ -5,9 +5,9 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** - * A block opened inside inline elements inherits their state, and the text after the block is read in their state - * again, until their end tags. A link around a block covers the block's text. Under legacy the block starts from the - * enclosing block's state, and so does the text after it (#541). + * A block or table opened inside inline elements inherits their state, and the text after it is read in their state + * again, until their end tags. A link around a block covers the block's text. Under legacy the block or table starts + * from the enclosing block's state, and so does the text after it (#541, #543). */ class BlockInsideInlineTest extends TestCase { @@ -191,6 +191,35 @@ public function documents() $loses, ['xx' => 1, 'qq' => 2], ], + 'a table in a span' => [ + 'xx
qq
yy
zz', + $inherits, + $loses, + ], + 'a table in a span in a block' => [ + '
xx
qq
yy
zz
', + ['xx' => $g, 'qq' => $g, 'yy' => $g, 'zz' => $b], + // Legacy starts the table from the document's defaults + ['xx' => $g, 'qq' => $p, 'yy' => $b, 'zz' => $b], + ], + 'a table in a span in a cell' => [ + $this->inContext('table cell', 'xx
qq
yy
zz'), + $inherits, + // Legacy: the nested table takes the cell's colour but not its weight + ['xx' => $g, 'qq' => ['0.000 1.000 0.000 rg', ''], 'yy' => $g, 'zz' => $g], + ], + 'a table in a span in a kept block that is laid out again on the next page' => [ + self::FILLER . '

ww

xx
qq
yy
zz
', + $inherits, + $loses, + ['ww' => 2, 'qq' => 2], + ], + 'the table in a span breaks the page before it' => [ + 'xx
qq
yy
zz', + $inherits, + $loses, + ['xx' => 1, 'qq' => 2], + ], 'the block in a span breaks the page after it' => [ 'xx
qq
yy
zz', $inherits, diff --git a/tests/Mpdf/CascadeOrderTest.php b/tests/Mpdf/CascadeOrderTest.php index 110cb826f..afa605f60 100644 --- a/tests/Mpdf/CascadeOrderTest.php +++ b/tests/Mpdf/CascadeOrderTest.php @@ -337,18 +337,19 @@ public function testADocumentWhoseRulesDoNotCompeteIsDrawnAlike($html) } /** - * Documents in which no two rules set the same property of an element + * Documents in which no two rules set the same property of an element. Their tables set the line-height the legacy + * mode gives a table, which the standard mode would take from body * * @return array[] */ public function uncontestedDocuments() { return [ - 'collapsed borders of cells, rows and the table' => ['' + 'collapsed borders of cells, rows and the table' => ['' . '
ab
cd
'], - 'a border from a cell rule against one as wide from the border attribute, which the rule wins' => ['' + 'a border from a cell rule against one as wide from the border attribute, which the rule wins' => ['' . '
abc
'], - 'separate borders and padding' => ['' + 'separate borders and padding' => ['' . '
h
a
'], 'headings, lists and links' => ['' . '

Title

  1. one
  2. two link
'], diff --git a/tests/Mpdf/InheritedPropertiesTest.php b/tests/Mpdf/InheritedPropertiesTest.php index 8e447de32..94b484f54 100644 --- a/tests/Mpdf/InheritedPropertiesTest.php +++ b/tests/Mpdf/InheritedPropertiesTest.php @@ -62,6 +62,13 @@ class InheritedPropertiesTest extends TestCase 'block to child block after a forced page break' => '

zz

qq

', 'positioned block to child block' => '

qq

', 'table to cell' => '
qq
', + 'block to table cell' => '
qq
', + 'list item to table cell' => '', + 'body to table cell' => '
qq
', + 'header block to table cell' => '
qq

body

', + 'positioned block to table cell' => '
qq
', + 'kept block to table cell' => '{FILLER}

zz

qq
', + 'block to table cell after a forced page break' => '

zz

qq
', 'row to cell' => '
qq
', 'row rule to cell' => '
qq
', 'tbody to cell' => '
qq
', @@ -82,8 +89,9 @@ class InheritedPropertiesTest extends TestCase * What legacy's channels drop, by context. An inline element hands a block opened inside it only its language * override, which the block does not reset. A block hands its child blocks no text shadow. A positioned block hands * its content no word spacing, hyphenation or outline, and its child blocks no text shadow. A table hands its cells - * no font variant, feature setting, language override, transform, shadow or outline. A row group or a row hands its - * cells nothing. A cell hands the cells of a table nested in it only its spacing and its language override + * no font variant, feature setting, language override, transform, shadow or outline. A table starts from the + * document's defaults, whatever block it is in. A row group or a row hands its cells nothing. A cell hands the + * cells of a table nested in it only its spacing and its language override */ const LEGACY_DROPPED = [ 'block to child block' => ['TEXT-SHADOW'], @@ -108,6 +116,13 @@ class InheritedPropertiesTest extends TestCase 'FONT-VARIANT-ALTERNATES', 'FONT-FEATURE-SETTINGS', 'FONT-LANGUAGE-OVERRIDE', 'TEXT-TRANSFORM', 'TEXT-SHADOW', 'TEXT-OUTLINE', 'TEXT-OUTLINE-COLOR', 'TEXT-OUTLINE-WIDTH', ], + 'block to table cell' => self::LEGACY_TABLE_DROPPED, + 'list item to table cell' => self::LEGACY_TABLE_DROPPED, + 'body to table cell' => self::LEGACY_BODY_TABLE_DROPPED, + 'header block to table cell' => self::LEGACY_TABLE_DROPPED, + 'positioned block to table cell' => self::LEGACY_TABLE_DROPPED, + 'kept block to table cell' => self::LEGACY_TABLE_DROPPED, + 'block to table cell after a forced page break' => self::LEGACY_TABLE_DROPPED, 'row to cell' => InheritedProperties::TEXT, 'row rule to cell' => InheritedProperties::TEXT, 'tbody to cell' => InheritedProperties::TEXT, @@ -121,16 +136,34 @@ class InheritedPropertiesTest extends TestCase 'row to nested table cell' => InheritedProperties::TEXT, 'outer row to nested table cell' => InheritedProperties::TEXT, 'cell to nested table cell' => [ - 'COLOR', 'FONT-FAMILY', 'FONT-SIZE', 'FONT-STYLE', 'FONT-WEIGHT', 'FONT-VARIANT-POSITION', 'FONT-VARIANT-CAPS', - 'FONT-VARIANT-LIGATURES', 'FONT-VARIANT-NUMERIC', 'FONT-VARIANT-ALTERNATES', 'FONT-FEATURE-SETTINGS', - 'TEXT-TRANSFORM', 'TEXT-SHADOW', 'TEXT-OUTLINE', 'TEXT-OUTLINE-COLOR', 'TEXT-OUTLINE-WIDTH', + 'COLOR', 'FONT-FAMILY', 'FONT-SIZE', 'FONT-STYLE', 'FONT-WEIGHT', 'FONT-KERNING', 'FONT-VARIANT-POSITION', + 'FONT-VARIANT-CAPS', 'FONT-VARIANT-LIGATURES', 'FONT-VARIANT-NUMERIC', 'FONT-VARIANT-ALTERNATES', + 'FONT-FEATURE-SETTINGS', 'TEXT-TRANSFORM', 'TEXT-SHADOW', 'HYPHENS', 'TEXT-OUTLINE', 'TEXT-OUTLINE-COLOR', + 'TEXT-OUTLINE-WIDTH', ], ]; /** - * What the default style of a table resets, whatever the table sits in (#543) + * What legacy hands a table's cells from the block the table is in: only a font language override, which the text + * state keeps through the table */ - const TABLE_DEFAULTS = ['FONT-KERNING', 'HYPHENS']; + const LEGACY_TABLE_DROPPED = [ + 'COLOR', 'FONT-FAMILY', 'FONT-SIZE', 'FONT-STYLE', 'FONT-WEIGHT', 'FONT-KERNING', 'FONT-VARIANT-POSITION', + 'FONT-VARIANT-CAPS', 'FONT-VARIANT-LIGATURES', 'FONT-VARIANT-NUMERIC', 'FONT-VARIANT-ALTERNATES', + 'FONT-FEATURE-SETTINGS', 'LETTER-SPACING', 'WORD-SPACING', 'TEXT-TRANSFORM', 'TEXT-SHADOW', 'HYPHENS', + 'TEXT-OUTLINE', 'TEXT-OUTLINE-COLOR', 'TEXT-OUTLINE-WIDTH', + ]; + + /** + * What legacy hands a table's cells from : the font family and size, which set the defaults a table + * starts from, and a font language override + */ + const LEGACY_BODY_TABLE_DROPPED = [ + 'COLOR', 'FONT-STYLE', 'FONT-WEIGHT', 'FONT-KERNING', 'FONT-VARIANT-POSITION', 'FONT-VARIANT-CAPS', + 'FONT-VARIANT-LIGATURES', 'FONT-VARIANT-NUMERIC', 'FONT-VARIANT-ALTERNATES', 'FONT-FEATURE-SETTINGS', + 'LETTER-SPACING', 'WORD-SPACING', 'TEXT-TRANSFORM', 'TEXT-SHADOW', 'HYPHENS', 'TEXT-OUTLINE', + 'TEXT-OUTLINE-COLOR', 'TEXT-OUTLINE-WIDTH', + ]; /** * The text states read for each document, so that each is written once @@ -321,10 +354,6 @@ public function testOfTakesTheNamedPropertiesInTheirOrder() */ private function carries($mode, $context, $property) { - if (in_array($context, ['cell to nested table cell', 'outer row to nested table cell'], true) && in_array($property, self::TABLE_DEFAULTS, true)) { - return false; - } - if ($mode === CssMode::STANDARD || !array_key_exists($context, self::LEGACY_DROPPED)) { return true; } diff --git a/tests/Mpdf/StringWidthDecodingTest.php b/tests/Mpdf/StringWidthDecodingTest.php index 86d40d5de..cf13f7568 100644 --- a/tests/Mpdf/StringWidthDecodingTest.php +++ b/tests/Mpdf/StringWidthDecodingTest.php @@ -244,7 +244,7 @@ private function hyphenated() private function drawnLines($html) { $mpdf = new TextRecordingMpdf(['mode' => 'utf-8', 'format' => [120, 400]]); - $mpdf->WriteHTML('
' . $html . '
'); + $mpdf->WriteHTML($html); $lines = $mpdf->drawnText; $mpdf->cleanup(); diff --git a/tests/Mpdf/TableInheritsFromBlockTest.php b/tests/Mpdf/TableInheritsFromBlockTest.php new file mode 100644 index 000000000..007ca5ad6 --- /dev/null +++ b/tests/Mpdf/TableInheritsFromBlockTest.php @@ -0,0 +1,256 @@ + '
qq
ww

zz

', + 'list item' => '
  • qq
    ww

zz

', + 'positioned block' => '
qq
ww

zz

', + 'kept block' => '{FILLER}
qq
ww

zz

', + 'cell' => '
qq
ww

zz

', + ]; + + /** + * A declaration of each inherited property a cell lays its lines out by that moves the text + */ + const SAMPLES = [ + 'TEXT-ALIGN' => 'text-align: right', + 'LINE-HEIGHT' => 'line-height: 3', + 'DIRECTION' => 'direction: rtl', + ]; + + /** + * What legacy hands a table's cells from what the table is in: the direction of a block in the flow, and the + * alignment and direction of a cell + */ + const LEGACY_CARRIED = [ + 'block' => ['DIRECTION'], + 'list item' => ['DIRECTION'], + 'kept block' => ['DIRECTION'], + 'cell' => ['TEXT-ALIGN', 'DIRECTION'], + ]; + + /** + * The cell's lines are laid out as when it sets the value itself, or under legacy as when nothing sets it, but for + * what legacy hands on too + * + * @dataProvider blockPropertiesInContexts + * + * @param string $mode + * @param string $context + * @param string $property + */ + public function testTheCellLaysItsLinesOutByTheInheritedValue($mode, $context, $property) + { + $declaration = self::SAMPLES[$property]; + + $initial = $this->drawnBoxes($mode, $context, '', ''); + $own = $this->drawnBoxes($mode, $context, '', $declaration); + $inherited = $this->drawnBoxes($mode, $context, $declaration, ''); + + $this->assertNotEquals($initial, $own, 'The sample value does not move the text'); + + $legacyCarries = array_key_exists($context, self::LEGACY_CARRIED) && in_array($property, self::LEGACY_CARRIED[$context], true); + if ($mode === CssMode::STANDARD || $legacyCarries) { + $this->assertEquals($own, $inherited); + } else { + $this->assertEquals($initial, $inherited); + } + } + + /** + * Each of text-align, line-height and direction from each kind of block, under both modes + * + * @return array[] + */ + public function blockPropertiesInContexts() + { + $data = []; + foreach ([CssMode::STANDARD, CssMode::LEGACY] as $mode) { + foreach (array_keys(self::CONTEXTS) as $context) { + foreach (array_keys(self::SAMPLES) as $property) { + // The cell of a nested table is laid out left to right whatever its direction + if ($context === 'cell' && $property === 'DIRECTION') { + continue; + } + $data[$mode . ': ' . $context . ': ' . $property] = [$mode, $context, $property]; + } + } + } + + return $data; + } + + /** + * A table in a block that sets line-height: normal, or sets none, is laid out in the standard mode as one whose + * cells set normal, which is drawn as a paragraph's is, from normalLineheight. A positioned block that sets none + * hands its content normal as that number, as it does its paragraphs. In the legacy mode the table keeps its own + * line-height of 1.2 + */ + public function testALineHeightOfNormalIsDrawnAsAParagraphsInTheStandardMode() + { + $lineHeights = [CssMode::STANDARD => 'line-height: normal', CssMode::LEGACY => 'line-height: 1.2']; + foreach ($lineHeights as $mode => $lineHeight) { + foreach (['block', 'list item', 'positioned block', 'cell'] as $context) { + $expected = $this->drawnBoxes($mode, $context, '', $lineHeight); + $this->assertEquals($expected, $this->drawnBoxes($mode, $context, 'line-height: normal', ''), $mode . ': ' . $context); + if ($mode === CssMode::STANDARD && $context === 'positioned block') { + $expected = $this->drawnBoxes($mode, $context, '', 'line-height: 1.33'); + } + $this->assertEquals($expected, $this->drawnBoxes($mode, $context, '', ''), $mode . ': ' . $context . ', setting none'); + } + } + + $this->assertNotEquals( + $this->drawnBoxes(CssMode::STANDARD, 'block', '', 'line-height: 1.2'), + $this->drawnBoxes(CssMode::STANDARD, 'block', '', ''), + 'A table should no longer be drawn at 1.2' + ); + } + + /** + * A table's font size is relative to the block's or the cell's it is in, and rem to the document's, whatever + * size the table sets + * + * @dataProvider fontSizes + * + * @param string $html + * @param float $standard The size qq is drawn at under standard, in points + * @param float $legacy The size under legacy + */ + public function testATablesFontSizeIsRelativeToWhatItIsIn($html, $standard, $legacy) + { + foreach ([CssMode::STANDARD => $standard, CssMode::LEGACY => $legacy] as $mode => $expected) { + $mpdf = $this->drawDocument($html, ['cssMode' => $mode]); + + $this->assertEqualsWithDelta($expected, $this->keyedByText($mpdf, $mpdf->drawnFontSize)['qq'], 0.001, $mode); + } + } + + /** + * Percentages, ems and rems on tables and cells, in blocks and cells, with the near misses + * + * @return array[] + */ + public function fontSizes() + { + return [ + 'a percentage of the block' => ['
qq
', 16.0, 8.8], + 'ems of the block' => ['
qq
', 30.0, 16.5], + 'a percentage of the list item' => ['
  • qq
', 10.0, 5.5], + 'a percentage of the cell' => ['
qq
', 10.0, 5.5], + 'a percentage of a span' => ['zz
qq
', 10.0, 5.5], + 'the block\'s size, inherited' => ['
qq
', 20.0, 11.0], + 'rem in a cell of a sized table' => ['
qq
', 16.5, 30.0], + 'rem in a table in a sized block' => ['
qq
', 11.0, 11.0], + 'rem on a table' => ['
qq
', 22.0, 22.0], + 'rem in a nested table' => ['
qq
', 11.0, 20.0], + 'rem in a cell follows html' => ['
qq
', 16.0, 8.0], + 'near miss: a size in points on the table' => ['
qq
', 8.0, 8.0], + 'near miss: a block after the one that sets the size' => ['
zz
qq
', 5.5, 5.5], + ]; + } + + /** + * A th in a table in a right-aligned block or cell is right-aligned, and centred where nothing sets an alignment + * + * @dataProvider headerCells + * + * @param string $html + * @param string $standard Where qq is drawn under standard: L, C or R + * @param string $legacy Where it is drawn under legacy + */ + public function testAHeaderCellTakesTheAlignmentOfTheBlock($html, $standard, $legacy) + { + foreach ([CssMode::STANDARD => $standard, CssMode::LEGACY => $legacy] as $mode => $expected) { + $mpdf = $this->drawDocument($html, ['cssMode' => $mode]); + $qq = $this->keyedByText($mpdf, $mpdf->drawnBoxes)['qq']; + $reference = $this->keyedByText($mpdf, $mpdf->drawnBoxes)['zz']; + + $this->assertSame($expected, $this->alignment($qq, $reference), $mode); + } + } + + /** + * Header cells in right-aligned blocks and cells, with the near misses. Each table has a row of three cells, 20mm + * wide, whose middle cell holds zz centred, above the th holding qq that spans them + * + * @return array[] + */ + public function headerCells() + { + $table = '' + . '
 zz 
qq
'; + + return [ + 'a th in a right-aligned block' => ['
' . strtr($table, ['{TH}' => '']) . '
', 'R', 'C'], + 'a th in a right-aligned cell' => ['
' . strtr($table, ['{TH}' => '']) . '
', 'R', 'C'], + 'near miss: a th in a block that sets no alignment' => ['
' . strtr($table, ['{TH}' => '']) . '
', 'C', 'C'], + 'near miss: a th that sets its own alignment' => ['
' . strtr($table, ['{TH}' => 'style="text-align: left"']) . '
', 'L', 'L'], + ]; + } + + /** + * Where the lines of the cell's text are drawn, and where the paragraph after it is + * + * @param string $mode + * @param string $context + * @param string $block The declarations of what the table is in + * @param string $cell The declarations of the cell + * + * @return array[] The page, left and right edges and top of qq, ww and zz, rounded + */ + private function drawnBoxes($mode, $context, $block, $cell) + { + $html = strtr(self::CONTEXTS[$context], [ + '{A}' => $block, + '{D}' => $cell, + // Too little of the first page is left for the kept block, which is laid out again on the second + '{FILLER}' => str_repeat('

filler

', 44), + ]); + $mpdf = $this->drawDocument($html, ['cssMode' => $mode]); + $boxes = $this->keyedByText($mpdf, $mpdf->drawnBoxes); + + return array_map(function ($box) { + return array_map(function ($value) { + return round($value, 3); + }, $box); + }, array_intersect_key($boxes, array_flip(['qq', 'ww', 'zz']))); + } + + /** + * Where a piece of text sits in a cell as wide as the row above it, whose middle cell is centred on the row + * + * @param array $box What the piece of text was drawn in: page, left, right, top + * @param array $reference The box of the text in the middle cell of the row above + * + * @return string L, C or R + */ + private function alignment(array $box, array $reference) + { + $centre = ($box[1] + $box[2]) / 2; + $referenceCentre = ($reference[1] + $reference[2]) / 2; + if (abs($centre - $referenceCentre) < 1) { + return 'C'; + } + + return $centre < $referenceCentre ? 'L' : 'R'; + } +} diff --git a/tests/Mpdf/UniversalSelectorTest.php b/tests/Mpdf/UniversalSelectorTest.php index 9db7d862e..9cae898bb 100644 --- a/tests/Mpdf/UniversalSelectorTest.php +++ b/tests/Mpdf/UniversalSelectorTest.php @@ -317,11 +317,12 @@ public function testPaintsThePageFromAUniversalBackground() * A reset that zeroes every margin and padding reaches the default margins of paragraphs, headings, lists and * blockquotes, a list's indent and a cell's padding, as in a browser: the text is drawn where it is drawn with the * same declarations written on each element. In the legacy mode it is dropped, and the text is drawn where it is - * with no stylesheet + * with no stylesheet. The table sets the line-height the legacy mode gives it, which the standard mode would take + * from body */ public function testAResetReachesTheDefaultMarginsAndPadding() { - $html = 'HeadingFirst paragraph

Second paragraph

' + $html = 'HeadingFirst paragraph

Second paragraph

' . 'BulletNumber' . 'TermDescriptionQuote' . 'CellLast paragraph

'; diff --git a/tests/Snapshots/AttributeSelectorSnapshotTest.php b/tests/Snapshots/AttributeSelectorSnapshotTest.php index 738a3b069..8d060f3ab 100644 --- a/tests/Snapshots/AttributeSelectorSnapshotTest.php +++ b/tests/Snapshots/AttributeSelectorSnapshotTest.php @@ -34,6 +34,7 @@ public function generatePdf()

mPDF

Basic HTML Example

This file demonstrates most of the HTML elements. diff --git a/tests/Snapshots/BlockInsideInlineCatalogueSnapshot.php b/tests/Snapshots/BlockInsideInlineCatalogueSnapshot.php index 890f4eff7..657f61aee 100644 --- a/tests/Snapshots/BlockInsideInlineCatalogueSnapshot.php +++ b/tests/Snapshots/BlockInsideInlineCatalogueSnapshot.php @@ -36,6 +36,7 @@ public function generatePdf() ob_start(); ?> ' + $html = '' . '

Active push buttons

' . '

A button with an empty value has no caption. Its field name is never shown.

' . '
'; diff --git a/tests/Snapshots/EmptyTextAreaSnapshotTest.php b/tests/Snapshots/EmptyTextAreaSnapshotTest.php index 60a1a3e39..c79cc04dc 100644 --- a/tests/Snapshots/EmptyTextAreaSnapshotTest.php +++ b/tests/Snapshots/EmptyTextAreaSnapshotTest.php @@ -27,6 +27,7 @@ public function generatePdf() ob_start(); ?> diff --git a/tests/Snapshots/ExifOrientationGreyscaleSnapshotTest.php b/tests/Snapshots/ExifOrientationGreyscaleSnapshotTest.php index 16c371659..ee3d464b4 100644 --- a/tests/Snapshots/ExifOrientationGreyscaleSnapshotTest.php +++ b/tests/Snapshots/ExifOrientationGreyscaleSnapshotTest.php @@ -27,6 +27,7 @@ public function generatePdf() ob_start(); ?> ' . $this->getWriter()->write($xml)); $this->mpdf->SetEmbeddedInvoice(new FacturX($xml)); } diff --git a/tests/Snapshots/FixedPseudoClassSnapshotTest.php b/tests/Snapshots/FixedPseudoClassSnapshotTest.php index fe5357410..7e212471b 100644 --- a/tests/Snapshots/FixedPseudoClassSnapshotTest.php +++ b/tests/Snapshots/FixedPseudoClassSnapshotTest.php @@ -29,6 +29,7 @@ public function generatePdf() ob_start(); ?> ' + $html = '' . '

Form fields

'; foreach ($rows as $label => $field) { $html .= ''; diff --git a/tests/Snapshots/FormFieldsShapedSnapshotTest.php b/tests/Snapshots/FormFieldsShapedSnapshotTest.php index c97d8940f..7934c0038 100644 --- a/tests/Snapshots/FormFieldsShapedSnapshotTest.php +++ b/tests/Snapshots/FormFieldsShapedSnapshotTest.php @@ -46,6 +46,7 @@ public function generatePdf() ob_start(); ?> ' . $this->getWriter()->write($this->getXml())); } } diff --git a/tests/Snapshots/LargeSubtableSnapshotTest.php b/tests/Snapshots/LargeSubtableSnapshotTest.php index b5a0fe6cb..c417c8342 100644 --- a/tests/Snapshots/LargeSubtableSnapshotTest.php +++ b/tests/Snapshots/LargeSubtableSnapshotTest.php @@ -80,6 +80,7 @@ private function style() ob_start(); ?> diff --git a/tests/Snapshots/ParentRelativeFontReportSnapshot.php b/tests/Snapshots/ParentRelativeFontReportSnapshot.php index aed055e4d..568b3364e 100644 --- a/tests/Snapshots/ParentRelativeFontReportSnapshot.php +++ b/tests/Snapshots/ParentRelativeFontReportSnapshot.php @@ -35,6 +35,7 @@ public function generatePdf() ob_start(); ?> + +

Kiln firing notes

+

Written with cssMode set to cascade(); ?>.

+ +

Firing summary

+

The callout is a dark block that sets pale sans-serif text spaced 0.2mm. Standard: the table in it has pale, spaced, sans-serif cells too. Legacy: its cells are in the body's dark serif, hard to read on the dark background.

+
+

Three firings this month, all to cone 6.

+
' . $label . '' . $field . '
+ + +
Bisque1,000 °C9 hours
Glaze1,222 °C11 hours
+
+ +

Loading steps

+

Each list item is green and italic. Standard: the table in the second step is green and italic too. Legacy: it is dark and upright.

+
    +
  1. Stack the shelves on three posts.
  2. +
  3. Space the pots by size: + + + +
    Mugs1 cm apart
    Bowls2 cm apart
    +
  4. +
+ +

From the kiln log

+

The quotation sets 12pt brown text, 1.8 apart and aligned right, and its table sets font-size: 75%. Standard: the table's cells are 9pt, brown, 1.8 apart and aligned right, and its th is aligned right. Legacy: they are 7.5pt, a quarter below the document's 10pt, dark, 1.2 apart and aligned left, and the th is centred.

+
+ The last load came out even from top to bottom. + + + +
ShelfCone
Top
(near the lid)
6
+
+ +

Panel

+

The panel cell sets slate monospace text aligned right, 2 apart, and holds a nested table. Standard: the nested cells are slate monospace, aligned right and 2 apart, and its th is aligned right. Legacy: they are in the body's dark serif, aligned right and 1.2 apart, and its th is centred.

+ + +
Cone chart + + + +
ConeTemperature
5
mid-fire
1,196 °C
+
+ mpdf = $this->createMpdf(['cssMode' => $this->cascade()]); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/Snapshots/TableInBlocksArticleStandardSnapshotTest.php b/tests/Snapshots/TableInBlocksArticleStandardSnapshotTest.php new file mode 100644 index 000000000..7b5e09415 --- /dev/null +++ b/tests/Snapshots/TableInBlocksArticleStandardSnapshotTest.php @@ -0,0 +1,21 @@ + + + +

Tables inherit from the block around them

+ +
+

color: #c00000 and font-family: monospace on a div. The table's cells are red and monospaced.

+
First cellSecond cell
+
+ + + +
+

font-size: 20pt on a div, and font-size: 50% on the table in it. The cells are 10pt.

+
10pt10pt
+
+ +

font-size: 20pt on a table, and font-size: 1rem on its second cell. The second cell is 11pt, the size of html, which no rule sets.

+
20pt11pt
+ +
+

line-height: 2.5 and text-align: right on a div. The cells' lines are 2.5 apart, and the th and the td are aligned right.

+
HeaderFirst line
Second line
+
+ +

text-align: right and line-height: 2.5 on a cell holding a nested table. The nested th and td are aligned right, and the td's lines are 2.5 apart.

+
+
Nested headerFirst line
Second line
+
+ mpdf = $this->createMpdf(); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/Snapshots/TableOfContentsAndIndexSnapshotTest.php b/tests/Snapshots/TableOfContentsAndIndexSnapshotTest.php index 7c86b2efc..92df542c2 100644 --- a/tests/Snapshots/TableOfContentsAndIndexSnapshotTest.php +++ b/tests/Snapshots/TableOfContentsAndIndexSnapshotTest.php @@ -57,6 +57,7 @@ public function generatePdf() $html = '