` 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' => [
+ '
xxyyzz',
+ $inherits,
+ $loses,
+ ],
+ 'a table in a span in a block' => [
+ '
',
+ ['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', '
xxyyzz'),
+ $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 . '
',
+ $inherits,
+ $loses,
+ ['ww' => 2, 'qq' => 2],
+ ],
+ 'the table in a span breaks the page before it' => [
+ '
xxyyzz',
+ $inherits,
+ $loses,
+ ['xx' => 1, 'qq' => 2],
+ ],
'the block in a span breaks the page after it' => [
'
xxqq
yyzz',
$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' => [''
. '
'],
- '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' => [''
. '
'],
- 'separate borders and padding' => [''
+ 'separate borders and padding' => [''
. '
'],
'headings, lists and links' => [''
. '
Title
- one
- 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' => '
',
'positioned block to child block' => '
',
'table to cell' => '
',
+ 'block to table cell' => '
',
+ 'list item to table cell' => '
',
+ 'body to table cell' => '
',
+ 'header block to table cell' => '
body
',
+ 'positioned block to table cell' => '
',
+ 'kept block to table cell' => '{FILLER}
',
+ 'block to table cell after a forced page break' => '
',
'row to cell' => '
',
'row rule to cell' => '
',
'tbody to cell' => '
',
@@ -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 @@
+ '
zz
',
+ 'list item' => '
zz
',
+ 'positioned block' => '
zz
',
+ 'kept block' => '{FILLER}
zz
',
+ 'cell' => '
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' => ['
', 16.0, 8.8],
+ 'ems of the block' => ['
', 30.0, 16.5],
+ 'a percentage of the list item' => ['
', 10.0, 5.5],
+ 'a percentage of the cell' => ['
', 10.0, 5.5],
+ 'a percentage of a span' => ['
zz', 10.0, 5.5],
+ 'the block\'s size, inherited' => ['
', 20.0, 11.0],
+ 'rem in a cell of a sized table' => ['
', 16.5, 30.0],
+ 'rem in a table in a sized block' => ['
', 11.0, 11.0],
+ 'rem on a table' => ['
', 22.0, 22.0],
+ 'rem in a nested table' => ['
', 11.0, 20.0],
+ 'rem in a cell follows html' => ['
', 16.0, 8.0],
+ 'near miss: a size in points on the table' => ['
', 8.0, 8.0],
+ 'near miss: a block after the one that sets the size' => ['
zz
', 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 = '
';
+
+ 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 = '
Heading
First paragraph
Second paragraph
'
+ $html = '
Heading
First paragraph
Second paragraph
'
. '
- Number
'
. '
- Term
- Description
Quote
'
. '
Last 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.
'
. '