`, an ``, an `` and a ``
+ */
+ public function testTriesTheNextAncestorOrSiblingAfterAFailure($selector, $expected)
+ {
+ $path = [
+ $this->frame('', 1, 1, [], []),
+ $this->frame('SECTION', 1, 1, [], []),
+ $this->frame('ARTICLE', 1, 1, [], []),
+ $this->frame('DIV', 1, 1, [], []),
+ $this->frame('ARTICLE', 1, 1, [], [['H2'], ['H1'], ['H2'], ['UL']]),
+ $this->frame('P', 5, 1, [], []),
+ ];
+
+ $this->assertSame($expected, $this->matcher->matches($this->compiler->compile($selector), $path));
+ }
+
+ /**
+ * A selector whose left part fails where it is first tried, and whether it matches in the end
+ *
+ * @return array[]
+ */
+ public function retriedSelectors()
+ {
+ return [
+ 'a child of a further ancestor' => ['section > article p', true],
+ 'a child of no ancestor' => ['div > section p', false],
+ 'the second of two siblings is adjacent to the one it needs' => ['h1 + h2 ~ p', true],
+ 'no sibling is adjacent to the one it needs' => ['ul + h2 ~ p', false],
+ 'the second of two siblings follows the one it needs' => ['h1 ~ h2 ~ p', true],
+ 'no sibling follows the one it needs' => ['ul ~ h1 ~ p', false],
+ 'a sibling tag the parent has no child of' => ['h3 ~ p', false],
+ 'an ancestor chain that fails everywhere' => ['p article section div p', false],
+ ];
+ }
+
+ /**
+ * A chain of descendant combinators whose leftmost compound names the nearest ancestor fails without trying every
+ * combination of ancestors. On a path 50 deep, `.x div div div div div p` has nearly two million of them
+ */
+ public function testGivesUpOnADescendantChainOnceNoAncestorCanMatch()
+ {
+ $path = [$this->frame('', 1, 1, [], [])];
+ for ($depth = 1; $depth < 50; $depth++) {
+ $path[] = $this->frame('DIV', 1, 1, $depth === 49 ? ['CLASS' => 'X'] : [], []);
+ }
+ $path[] = $this->frame('P', 1, 1, [], []);
+
+ $start = microtime(true);
+ $this->assertFalse($this->matcher->matches($this->compiler->compile('.x div div div div div p'), $path));
+ $this->assertTrue($this->matcher->matches($this->compiler->compile('div div div div div.x p'), $path));
+ $this->assertLessThan(1, microtime(true) - $start);
+ }
+
+ /**
+ * A general sibling combinator finds a sibling at the start of a long run of them, and skips a tag its parent has
+ * no child of, without walking back through every sibling for each element. Matching each of 5,000 paragraphs
+ * after an `` walks 12.5 million siblings that way
+ */
+ public function testFindsAnEarlierSiblingWithoutWalkingThroughEachOne()
+ {
+ $children = [['H2']];
+ for ($i = 0; $i < 5000; $i++) {
+ $children[] = ['P'];
+ }
+ $parent = $this->frame('', 1, 1, [], $children);
+ $heading = $this->compiler->compile('h2 ~ p');
+ $missing = $this->compiler->compile('h3 ~ p');
+
+ $start = microtime(true);
+ for ($i = 1; $i <= 5000; $i++) {
+ $path = [$parent, $this->frame('P', $i + 1, $i, [], [])];
+ $this->assertTrue($this->matcher->matches($heading, $path));
+ $this->assertFalse($this->matcher->matches($missing, $path));
+ }
+ $this->assertLessThan(1, microtime(true) - $start);
+ }
+
/**
* The open elements the selectors are matched against: `
- `, the second item of a `
` that follows
* an `` and a `
` in `
`, which follows an ``, a `
` and a `
` in
diff --git a/tests/Mpdf/CssManagerTest.php b/tests/Mpdf/CssManagerTest.php
index ecc771b3f..13ea9f102 100644
--- a/tests/Mpdf/CssManagerTest.php
+++ b/tests/Mpdf/CssManagerTest.php
@@ -19,7 +19,8 @@ protected function set_up()
$this->tempDir = sys_get_temp_dir() . '/mpdf_test_' . uniqid();
mkdir($this->tempDir);
- $this->mpdf = new Mpdf(['tempDir' => $this->tempDir]);
+ // The merges below read the legacy cascade's keys and trees
+ $this->mpdf = new Mpdf(['tempDir' => $this->tempDir, 'cssMode' => CssMode::LEGACY]);
$this->mpdf->setBasePath($this->tempDir);
// Use reflection to access private cssManager property
@@ -903,4 +904,35 @@ public function testKeepsTheCompiledRulesOfEveryStylesheetRead()
$this->assertSame(['COLOR' => 'red'], $rules->rule($candidates[0])[1]);
$this->assertSame(['COLOR' => 'green'], $rules->rule($candidates[1])[1]);
}
+
+ /**
+ * The default stylesheet's rules are kept apart from the document's, and a document's stylesheet never adds to them
+ */
+ public function testKeepsTheDefaultStylesheetsRulesApart()
+ {
+ $default = $this->cssManager->getDefaultRules()->candidates('UL', '', []);
+
+ $this->cssManager->readDefaultCss('ul > ul { margin-top: 0; }');
+ $this->cssManager->readCss('');
+
+ $defaultRules = array_values(array_diff($this->cssManager->getDefaultRules()->candidates('UL', '', []), $default));
+ $documentRules = $this->cssManager->getRules()->candidates('UL', '', []);
+ $this->assertCount(1, $defaultRules);
+ $this->assertCount(1, $documentRules);
+ $this->assertSame(['MARGIN-TOP' => '0'], $this->cssManager->getDefaultRules()->rule($defaultRules[0])[1]);
+ $this->assertSame(['COLOR' => 'red'], $this->cssManager->getRules()->rule($documentRules[0])[1]);
+ $this->assertSame([], $this->cssManager->getDefaultRules()->candidates('LI', '', []));
+ }
+
+ /**
+ * The default stylesheet is read into the same stores a document's stylesheet is, for what reads them directly
+ */
+ public function testReadsTheDefaultStylesheetIntoTheSharedStores()
+ {
+ $this->cssManager->readDefaultCss('.note { color: red; } div p { margin: 0; } @page { margin-left: 1cm; }');
+
+ $this->assertSame('red', $this->cssManager->CSS['CLASS>>NOTE']['COLOR']);
+ $this->assertArrayHasKey('@PAGE', $this->cssManager->CSS);
+ $this->assertArrayHasKey('P', $this->cssManager->cascadeCSS['DIV']);
+ }
}
diff --git a/tests/Mpdf/DescendantLangSelectorTest.php b/tests/Mpdf/DescendantLangSelectorTest.php
index c5ba3cf0d..7c27d9c75 100644
--- a/tests/Mpdf/DescendantLangSelectorTest.php
+++ b/tests/Mpdf/DescendantLangSelectorTest.php
@@ -9,7 +9,7 @@
* in block content and in tables.
*
* A regional language such as fr-CA falls back to the rule for its short code, fr, when no rule names it in full,
- * as the simple lang rules do.
+ * as the simple lang rules do. That is how the legacy cascade matches them, and these run under it.
*/
class DescendantLangSelectorTest extends TestCase
{
@@ -31,7 +31,7 @@ class DescendantLangSelectorTest extends TestCase
*/
public function testTheRuleColoursTheText($css, $html, $expected)
{
- $colours = $this->textColours('' . $html);
+ $colours = $this->textColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertArrayHasKey('text', $colours);
$this->assertSame($expected, $colours['text']);
diff --git a/tests/Mpdf/DrawnStyles.php b/tests/Mpdf/DrawnStyles.php
index bc117adc3..18b94c879 100644
--- a/tests/Mpdf/DrawnStyles.php
+++ b/tests/Mpdf/DrawnStyles.php
@@ -40,6 +40,20 @@ private function drawnColours($html, array $config = [])
return $this->keyedByText($mpdf, $mpdf->drawnColours);
}
+ /**
+ * Each piece of text named is drawn, in the colour given for it
+ *
+ * @param array $expected Pieces of text and the operator that should set each one's colour
+ * @param array $colours What was drawn, as drawnColours() gives it
+ */
+ private function assertDrawnInColours(array $expected, array $colours)
+ {
+ foreach ($expected as $text => $colour) {
+ $this->assertArrayHasKey($text, $colours, sprintf('"%s" is not drawn', $text));
+ $this->assertSame($colour, $colours[$text], sprintf('"%s" is drawn in the wrong colour', $text));
+ }
+ }
+
/**
* Keys what was recorded of each piece of text a document drew by the text itself, so a test can look a piece up
* by what it says
diff --git a/tests/Mpdf/InlineAncestorSelectorTest.php b/tests/Mpdf/InlineAncestorSelectorTest.php
index 7abfeedab..b3315c4ff 100644
--- a/tests/Mpdf/InlineAncestorSelectorTest.php
+++ b/tests/Mpdf/InlineAncestorSelectorTest.php
@@ -117,7 +117,7 @@ public function selectors()
*/
public function testLeavesToTheLegacyEngineWhatItMatches($css, $html, $expected)
{
- $colours = $this->drawnColours('' . $html);
+ $colours = $this->drawnColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertSame($expected, $colours['text']);
}
@@ -166,7 +166,8 @@ public function legacyMatches()
public function testAppliesRulesMatchedThroughAnInlineAncestorAfterTheLegacyOnes()
{
$colours = $this->drawnColours(
- 'text
'
+ 'text
',
+ ['cssMode' => CssMode::LEGACY]
);
$this->assertSame(self::BLUE, $colours['text']);
diff --git a/tests/Mpdf/PageStreams.php b/tests/Mpdf/PageStreams.php
index a70ef2640..2daf8fd58 100644
--- a/tests/Mpdf/PageStreams.php
+++ b/tests/Mpdf/PageStreams.php
@@ -263,12 +263,13 @@ private function clipAndPlacement($stream)
*
* @param string $html
* @param int $page Counted from 0
+ * @param array $config
*
* @return array[]
*/
- private function imagePlacements($html, $page = 0)
+ private function imagePlacements($html, $page = 0, $config = [])
{
- $pages = $this->pages($this->render($html));
+ $pages = $this->pages($this->render($html, $config));
$this->assertArrayHasKey($page, $pages);
preg_match_all('/([-\d.]+) 0 0 ([-\d.]+) ([-\d.]+) [-\d.]+ cm \/I\d+ Do/', $pages[$page], $matches, PREG_SET_ORDER);
@@ -281,12 +282,13 @@ private function imagePlacements($html, $page = 0)
* The width of the only image on the first page, in millimetres
*
* @param string $html
+ * @param array $config
*
* @return float
*/
- private function drawnWidth($html)
+ private function drawnWidth($html, $config = [])
{
- $placements = $this->imagePlacements($html);
+ $placements = $this->imagePlacements($html, 0, $config);
$this->assertCount(1, $placements);
return $placements[0]['w'];
@@ -296,12 +298,13 @@ private function drawnWidth($html)
* The fill colour each piece of text on the first page is drawn in, keyed by that text
*
* @param string $html
+ * @param array $config
*
* @return string[]
*/
- private function textColours($html)
+ private function textColours($html, $config = [])
{
- $pages = $this->pages($this->render($html));
+ $pages = $this->pages($this->render($html, $config));
preg_match_all('/q ([\d. ]+ (?:rg|g)) .*?\((.*?)\) Tj/', $pages[0], $drawn, PREG_SET_ORDER);
$colours = [];
diff --git a/tests/Mpdf/PositionedBlockSelectorStandardCascadeTest.php b/tests/Mpdf/PositionedBlockSelectorStandardCascadeTest.php
new file mode 100644
index 000000000..73479720d
--- /dev/null
+++ b/tests/Mpdf/PositionedBlockSelectorStandardCascadeTest.php
@@ -0,0 +1,20 @@
+ CssMode::STANDARD];
+ }
+}
diff --git a/tests/Mpdf/PositionedBlockSelectorTest.php b/tests/Mpdf/PositionedBlockSelectorTest.php
index c0f002f1a..59be3c153 100644
--- a/tests/Mpdf/PositionedBlockSelectorTest.php
+++ b/tests/Mpdf/PositionedBlockSelectorTest.php
@@ -20,6 +20,17 @@ class PositionedBlockSelectorTest extends TestCase
const BLUE = '0.000 0.000 1.000 rg';
const BLACK = '0.000 g';
+ /**
+ * The configuration the documents are written with: the legacy cascade. A subclass runs the tests under the
+ * standard one
+ *
+ * @return array
+ */
+ protected function config()
+ {
+ return ['cssMode' => CssMode::LEGACY];
+ }
+
/**
* A descendant rule naming the block's class reaches a paragraph and a table cell inside it
*/
@@ -31,7 +42,7 @@ public function testClassDescendantRulesReachTheContentOfAnAbsoluteBlock()
.box { color: #0000ff; }
flow p
flow td
- abs p
abs td
abs span');
+ abs p
abs td
abs span', $this->config());
$this->assertSame(self::GREEN, $colours['flow p']);
$this->assertSame(self::RED, $colours['flow td']);
@@ -46,7 +57,7 @@ public function testClassDescendantRulesReachTheContentOfAnAbsoluteBlock()
public function testIdDescendantRulesReachTheContentOfAFixedBlock()
{
$colours = $this->textColours('
- fixed p
');
+ fixed p
', $this->config());
$this->assertSame(self::RED, $colours['fixed p']);
}
@@ -57,7 +68,7 @@ public function testIdDescendantRulesReachTheContentOfAFixedBlock()
public function testTheContentIsAChildOfTheBlockItself()
{
$colours = $this->textColours('
- direct p
nested p
');
+ direct p
nested p
', $this->config());
$this->assertSame(self::BLACK, $colours['direct p']);
$this->assertSame(self::RED, $colours['nested p']);
@@ -70,7 +81,7 @@ public function testTheRulesOfOneBlockDoNotReachTheNext()
{
$colours = $this->textColours('
boxed p
- plain p
');
+ plain p
', $this->config());
$this->assertSame(self::GREEN, $colours['boxed p']);
$this->assertSame(self::BLACK, $colours['plain p']);
diff --git a/tests/Mpdf/RotatedCellFontSizeTest.php b/tests/Mpdf/RotatedCellFontSizeTest.php
new file mode 100644
index 000000000..1e0664d6e
--- /dev/null
+++ b/tests/Mpdf/RotatedCellFontSizeTest.php
@@ -0,0 +1,42 @@
+pages($this->render($html))[0], $sizes);
+
+ $this->assertSame(['9.000'], $sizes[1]);
+ }
+
+ /**
+ * Tables that set a font size, with a rotated cell
+ *
+ * @return array[]
+ */
+ public function tables()
+ {
+ return [
+ 'the size in the table\'s style' => ['Rotated
'],
+ 'the size in a rule for the table' => ['Plain Rotated
'],
+ ];
+ }
+}
diff --git a/tests/Mpdf/StructuralSelectorTest.php b/tests/Mpdf/StructuralSelectorTest.php
index de717a4eb..6d463d949 100644
--- a/tests/Mpdf/StructuralSelectorTest.php
+++ b/tests/Mpdf/StructuralSelectorTest.php
@@ -268,7 +268,7 @@ public function selectors()
*/
public function testAppliesMatchedRulesBySpecificityThenPosition($css, $html, $expected)
{
- $colours = $this->drawnColours('' . $html);
+ $colours = $this->drawnColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertSame($expected, $colours['text']);
}
diff --git a/tests/Mpdf/StyledElementPathTest.php b/tests/Mpdf/StyledElementPathTest.php
index f9a6e2903..8223dc418 100644
--- a/tests/Mpdf/StyledElementPathTest.php
+++ b/tests/Mpdf/StyledElementPathTest.php
@@ -6,7 +6,8 @@
/**
* The open elements the CSS merger matches compiled rules against, for each element it styles: the element opened
- * by a start tag, a block opened again after a forced page break, a positioned block and the content written in it
+ * by a start tag, a block opened again after a forced page break, a positioned block and the content written in it.
+ * These run under the legacy cascade, which asks for the path only when a compiled rule is filed under the element
*/
class StyledElementPathTest extends TestCase
{
@@ -22,7 +23,7 @@ class StyledElementPathTest extends TestCase
*/
public function testGivesThePathToTheElementBeingStyled($html, array $expected)
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML($html);
$this->assertSame($expected, $mpdf->styledPaths);
@@ -38,7 +39,7 @@ public function testGivesThePathToTheElementBeingStyled($html, array $expected)
*/
public function testGivesThePositionTheElementHasOnItsPath($html)
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML($html . ''
. 'a b
c d
');
@@ -85,7 +86,7 @@ public function documents()
*/
public function testLetsGoOfTheElementOnceItsStartTagIsRead()
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML('a
b
');
$this->assertNull($mpdf->getStyledElementPath());
@@ -96,7 +97,7 @@ public function testLetsGoOfTheElementOnceItsStartTagIsRead()
*/
public function testGivesNoPathForASpanWrappedAroundARunOfAnotherScript()
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => '', 'autoScriptToLang' => true]);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => '', 'autoScriptToLang' => true, 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML('abc שלום def
');
$this->assertSame([null, '>P>SPAN'], $mpdf->styledPaths);
diff --git a/tests/Mpdf/TableCellDescendantSelectorStandardCascadeTest.php b/tests/Mpdf/TableCellDescendantSelectorStandardCascadeTest.php
new file mode 100644
index 000000000..34e121328
--- /dev/null
+++ b/tests/Mpdf/TableCellDescendantSelectorStandardCascadeTest.php
@@ -0,0 +1,20 @@
+ CssMode::STANDARD];
+ }
+}
diff --git a/tests/Mpdf/TableCellDescendantSelectorTest.php b/tests/Mpdf/TableCellDescendantSelectorTest.php
index 0aa9f7899..826034917 100644
--- a/tests/Mpdf/TableCellDescendantSelectorTest.php
+++ b/tests/Mpdf/TableCellDescendantSelectorTest.php
@@ -12,6 +12,17 @@ class TableCellDescendantSelectorTest extends \Yoast\PHPUnitPolyfills\TestCases\
const RED = '1.000 0.000 0.000 rg';
+ /**
+ * The configuration the documents are written with: the legacy cascade. A subclass runs the tests under the
+ * standard one
+ *
+ * @return array
+ */
+ protected function config()
+ {
+ return ['cssMode' => CssMode::LEGACY];
+ }
+
/**
* A table of one cell holding the given content, in a div, under the given stylesheet
*
@@ -60,7 +71,7 @@ public static function matchingRules()
*/
public function testARuleReachesAPictureInTheCell($selector, $content)
{
- $this->assertEqualsWithDelta(20, $this->drawnWidth($this->table($selector . ' { max-width: 20mm; }', $content)), 0.01);
+ $this->assertEqualsWithDelta(20, $this->drawnWidth($this->table($selector . ' { max-width: 20mm; }', $content), $this->config()), 0.01);
}
/**
@@ -68,7 +79,7 @@ public function testARuleReachesAPictureInTheCell($selector, $content)
*/
public function testARuleForAnotherCellDoesNotMatch()
{
- $width = $this->drawnWidth($this->table('td.other img { max-width: 20mm; }', self::image()));
+ $width = $this->drawnWidth($this->table('td.other img { max-width: 20mm; }', self::image()), $this->config());
$this->assertEqualsWithDelta(292 * 25.4 / 96, $width, 0.01);
}
@@ -94,7 +105,7 @@ public static function textRules()
*/
public function testARuleReachesTextInTheCell($selector, $content)
{
- $colours = $this->textColours($this->table($selector . ' { color: #ff0000; }', $content));
+ $colours = $this->textColours($this->table($selector . ' { color: #ff0000; }', $content), $this->config());
$this->assertSame(self::RED, $colours['red']);
}
diff --git a/tests/Snapshots/DescendantLangSelectorSnapshotTest.php b/tests/Snapshots/DescendantLangSelectorSnapshotTest.php
index ce036484e..456728ef3 100644
--- a/tests/Snapshots/DescendantLangSelectorSnapshotTest.php
+++ b/tests/Snapshots/DescendantLangSelectorSnapshotTest.php
@@ -4,7 +4,8 @@
/**
* Descendant rules whose last part names a language, such as `div :lang(fr)`, `div [lang=fr]` and `div p:lang(fr)`,
- * in block content, inline content and tables, and a regional language falling back to the rule for its short code.
+ * in block content, inline content and tables, and a regional language falling back to the rule for its short code,
+ * under the legacy cascade.
*
* @group snapshot
*/
@@ -107,7 +108,8 @@ public function generatePdf()
mpdf = $this->createMpdf();
+ // A regional language named in full winning over its short code, whatever the order, is the legacy cascade's
+ $this->mpdf = $this->createMpdf(['cssMode' => \Mpdf\CssMode::LEGACY]);
$this->mpdf->WriteHTML($html);
}
diff --git a/tests/Snapshots/FrameworkCascadeLegacySnapshotTest.php b/tests/Snapshots/FrameworkCascadeLegacySnapshotTest.php
new file mode 100644
index 000000000..15e72f501
--- /dev/null
+++ b/tests/Snapshots/FrameworkCascadeLegacySnapshotTest.php
@@ -0,0 +1,21 @@
+cascade();
+ }
+
+ /**
+ * The components, each under a caption saying what the standard and the legacy cascade draw
+ */
+ public function generatePdf()
+ {
+ ob_start();
+ ?>
+
+
+ A framework's stylesheet
+ Written with cssMode set to cascade(); ?>.
+
+ Navbar
+ #brand against .navbar > .container a. Standard: "Brand" is yellow and bold, the id beating the child rule. Legacy: "Brand" is grey like the other links, though still bold, because a rule only the matcher reads is applied after the id.
+
+
+ Card
+ h5.card-title against .card h5, as heavy and written later. Standard: the title is blue. Legacy: it is black, the descendant rule coming last.
+ .lead against article p. Standard: the lead paragraph is large and grey, a class outweighing two tags. Legacy: it is the size and colour of the paragraph after it.
+
+ Card title
+ A lead paragraph, set larger than the text after it.
The body text of the card.
+
+
+ Sidebar navigation
+ #sidebar a against .nav .nav-link. Standard: the links are purple and bold, an id ancestor outweighing two classes. Legacy: they are blue and bold, the rule lifted from the nearer .nav coming last.
+
+
+ List group
+ .list-group-item.active against .list-group .list-group-item, as heavy and written later. Standard: the active item is white on blue. Legacy: it is white on light grey, the descendant rule's background coming last, so its text all but disappears.
+
+ - An item
+ - The active item
+ - Another item
+
+
+ Table
+ td.text-end against .table td, as heavy and written later. Standard: the amounts are right-aligned. Legacy: they are left-aligned, the descendant rule coming last. "Hosting", the third row counting the header, is striped grey in both.
+
+ Item Amount
+ Consulting 1,200.00
+ Hosting 300.00
+ Support 150.00
+
+
+ Utilities and components
+ .muted, written after .title. Standard: "Muted title" is light grey and bold. Legacy: it is blue and bold, classes applying in alphabetical order.
+ Muted title
+ mpdf = $this->createMpdf(['cssMode' => $this->cascade()]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/Snapshots/FrameworkCascadeStandardSnapshotTest.php b/tests/Snapshots/FrameworkCascadeStandardSnapshotTest.php
new file mode 100644
index 000000000..fd8eb5b5f
--- /dev/null
+++ b/tests/Snapshots/FrameworkCascadeStandardSnapshotTest.php
@@ -0,0 +1,21 @@
+
+
+
+
+ The standard cascade
+
+ #id beats p.class: the line is green.
+ An id against a tag with a class
+
+ #id beats div.class div p: the line is green.
+ An id against a descendant rule
+
+ .class.class beats body p.class: the line is green.
+ Two classes against body and a tag with a class
+
+ .class p beats div div div p, although the divs are nearer: the line is green.
+ A class ancestor against three tag ancestors
+
+ .c5a is written after .c5b, so it wins whichever order the classes are in: both lines are green.
+ Classes a then b
Classes b then a
+
+ .c6a, then .c6b, then .c6a again: the second .c6a comes last, and the line is green.
+ A selector written twice
+
+ A rule in a later style block beats one as heavy in an earlier block: the line is green.
+ A rule in a later stylesheet
+
+ :where() counts nothing, so div.class p beats :where(#id) p: the line is green.
+ :where() against a class
+
+ The inline style beats #id: the line is green.
+ Inline style against an id
+
+ ul.class beats the default stylesheet's ul ul, which counts as the built-in stylesheet's: the nested list keeps the 4mm gap below it, so "Outer two" is well below "Inner two".
+
+ - Outer one
- Inner one
- Inner two
- Outer two
+
+
+ <hr color> beats the grey of the built-in defaults: the rule is green.
+
+
+ td#id beats table.class tr td: the middle cell has a thick green border, drawn over its neighbours' thin red ones.
+ Left Middle Right
+ mpdf = $this->createMpdf(['cssMode' => \Mpdf\CssMode::STANDARD]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/data/snapshots/framework-cascade-legacy.pdf b/tests/data/snapshots/framework-cascade-legacy.pdf
new file mode 100644
index 000000000..6e54d92e3
Binary files /dev/null and b/tests/data/snapshots/framework-cascade-legacy.pdf differ
diff --git a/tests/data/snapshots/framework-cascade-standard.pdf b/tests/data/snapshots/framework-cascade-standard.pdf
new file mode 100644
index 000000000..805ab9454
Binary files /dev/null and b/tests/data/snapshots/framework-cascade-standard.pdf differ
diff --git a/tests/data/snapshots/page-break-avoid-content.pdf b/tests/data/snapshots/page-break-avoid-content.pdf
index 1b0b528fb..7497dc2a8 100644
Binary files a/tests/data/snapshots/page-break-avoid-content.pdf and b/tests/data/snapshots/page-break-avoid-content.pdf differ
diff --git a/tests/data/snapshots/page-break-avoid-floats.pdf b/tests/data/snapshots/page-break-avoid-floats.pdf
index e2adc0af1..7da0d3ee5 100644
Binary files a/tests/data/snapshots/page-break-avoid-floats.pdf and b/tests/data/snapshots/page-break-avoid-floats.pdf differ
diff --git a/tests/data/snapshots/positioned-html.pdf b/tests/data/snapshots/positioned-html.pdf
index 80b27020e..2343ab40c 100644
Binary files a/tests/data/snapshots/positioned-html.pdf and b/tests/data/snapshots/positioned-html.pdf differ
diff --git a/tests/data/snapshots/rtl.pdf b/tests/data/snapshots/rtl.pdf
index 40fe553b9..724d684aa 100644
Binary files a/tests/data/snapshots/rtl.pdf and b/tests/data/snapshots/rtl.pdf differ
diff --git a/tests/data/snapshots/standard-cascade.pdf b/tests/data/snapshots/standard-cascade.pdf
new file mode 100644
index 000000000..8f561bf9b
Binary files /dev/null and b/tests/data/snapshots/standard-cascade.pdf differ
diff --git a/tests/data/snapshots/table.pdf b/tests/data/snapshots/table.pdf
index 69a1fe048..93b183e04 100644
Binary files a/tests/data/snapshots/table.pdf and b/tests/data/snapshots/table.pdf differ
diff --git a/tests/data/snapshots/toc-and-index.pdf b/tests/data/snapshots/toc-and-index.pdf
index 1e8ad6242..d1762981c 100644
Binary files a/tests/data/snapshots/toc-and-index.pdf and b/tests/data/snapshots/toc-and-index.pdf differ
` and a ``
+ */
+ public function testTriesTheNextAncestorOrSiblingAfterAFailure($selector, $expected)
+ {
+ $path = [
+ $this->frame('', 1, 1, [], []),
+ $this->frame('SECTION', 1, 1, [], []),
+ $this->frame('ARTICLE', 1, 1, [], []),
+ $this->frame('DIV', 1, 1, [], []),
+ $this->frame('ARTICLE', 1, 1, [], [['H2'], ['H1'], ['H2'], ['UL']]),
+ $this->frame('P', 5, 1, [], []),
+ ];
+
+ $this->assertSame($expected, $this->matcher->matches($this->compiler->compile($selector), $path));
+ }
+
+ /**
+ * A selector whose left part fails where it is first tried, and whether it matches in the end
+ *
+ * @return array[]
+ */
+ public function retriedSelectors()
+ {
+ return [
+ 'a child of a further ancestor' => ['section > article p', true],
+ 'a child of no ancestor' => ['div > section p', false],
+ 'the second of two siblings is adjacent to the one it needs' => ['h1 + h2 ~ p', true],
+ 'no sibling is adjacent to the one it needs' => ['ul + h2 ~ p', false],
+ 'the second of two siblings follows the one it needs' => ['h1 ~ h2 ~ p', true],
+ 'no sibling follows the one it needs' => ['ul ~ h1 ~ p', false],
+ 'a sibling tag the parent has no child of' => ['h3 ~ p', false],
+ 'an ancestor chain that fails everywhere' => ['p article section div p', false],
+ ];
+ }
+
+ /**
+ * A chain of descendant combinators whose leftmost compound names the nearest ancestor fails without trying every
+ * combination of ancestors. On a path 50 deep, `.x div div div div div p` has nearly two million of them
+ */
+ public function testGivesUpOnADescendantChainOnceNoAncestorCanMatch()
+ {
+ $path = [$this->frame('', 1, 1, [], [])];
+ for ($depth = 1; $depth < 50; $depth++) {
+ $path[] = $this->frame('DIV', 1, 1, $depth === 49 ? ['CLASS' => 'X'] : [], []);
+ }
+ $path[] = $this->frame('P', 1, 1, [], []);
+
+ $start = microtime(true);
+ $this->assertFalse($this->matcher->matches($this->compiler->compile('.x div div div div div p'), $path));
+ $this->assertTrue($this->matcher->matches($this->compiler->compile('div div div div div.x p'), $path));
+ $this->assertLessThan(1, microtime(true) - $start);
+ }
+
+ /**
+ * A general sibling combinator finds a sibling at the start of a long run of them, and skips a tag its parent has
+ * no child of, without walking back through every sibling for each element. Matching each of 5,000 paragraphs
+ * after an `` walks 12.5 million siblings that way
+ */
+ public function testFindsAnEarlierSiblingWithoutWalkingThroughEachOne()
+ {
+ $children = [['H2']];
+ for ($i = 0; $i < 5000; $i++) {
+ $children[] = ['P'];
+ }
+ $parent = $this->frame('', 1, 1, [], $children);
+ $heading = $this->compiler->compile('h2 ~ p');
+ $missing = $this->compiler->compile('h3 ~ p');
+
+ $start = microtime(true);
+ for ($i = 1; $i <= 5000; $i++) {
+ $path = [$parent, $this->frame('P', $i + 1, $i, [], [])];
+ $this->assertTrue($this->matcher->matches($heading, $path));
+ $this->assertFalse($this->matcher->matches($missing, $path));
+ }
+ $this->assertLessThan(1, microtime(true) - $start);
+ }
+
/**
* The open elements the selectors are matched against: `
- `, the second item of a `
` that follows
* an `` and a `
` in `
`, which follows an ``, a `
` and a `
` in
diff --git a/tests/Mpdf/CssManagerTest.php b/tests/Mpdf/CssManagerTest.php
index ecc771b3f..13ea9f102 100644
--- a/tests/Mpdf/CssManagerTest.php
+++ b/tests/Mpdf/CssManagerTest.php
@@ -19,7 +19,8 @@ protected function set_up()
$this->tempDir = sys_get_temp_dir() . '/mpdf_test_' . uniqid();
mkdir($this->tempDir);
- $this->mpdf = new Mpdf(['tempDir' => $this->tempDir]);
+ // The merges below read the legacy cascade's keys and trees
+ $this->mpdf = new Mpdf(['tempDir' => $this->tempDir, 'cssMode' => CssMode::LEGACY]);
$this->mpdf->setBasePath($this->tempDir);
// Use reflection to access private cssManager property
@@ -903,4 +904,35 @@ public function testKeepsTheCompiledRulesOfEveryStylesheetRead()
$this->assertSame(['COLOR' => 'red'], $rules->rule($candidates[0])[1]);
$this->assertSame(['COLOR' => 'green'], $rules->rule($candidates[1])[1]);
}
+
+ /**
+ * The default stylesheet's rules are kept apart from the document's, and a document's stylesheet never adds to them
+ */
+ public function testKeepsTheDefaultStylesheetsRulesApart()
+ {
+ $default = $this->cssManager->getDefaultRules()->candidates('UL', '', []);
+
+ $this->cssManager->readDefaultCss('ul > ul { margin-top: 0; }');
+ $this->cssManager->readCss('');
+
+ $defaultRules = array_values(array_diff($this->cssManager->getDefaultRules()->candidates('UL', '', []), $default));
+ $documentRules = $this->cssManager->getRules()->candidates('UL', '', []);
+ $this->assertCount(1, $defaultRules);
+ $this->assertCount(1, $documentRules);
+ $this->assertSame(['MARGIN-TOP' => '0'], $this->cssManager->getDefaultRules()->rule($defaultRules[0])[1]);
+ $this->assertSame(['COLOR' => 'red'], $this->cssManager->getRules()->rule($documentRules[0])[1]);
+ $this->assertSame([], $this->cssManager->getDefaultRules()->candidates('LI', '', []));
+ }
+
+ /**
+ * The default stylesheet is read into the same stores a document's stylesheet is, for what reads them directly
+ */
+ public function testReadsTheDefaultStylesheetIntoTheSharedStores()
+ {
+ $this->cssManager->readDefaultCss('.note { color: red; } div p { margin: 0; } @page { margin-left: 1cm; }');
+
+ $this->assertSame('red', $this->cssManager->CSS['CLASS>>NOTE']['COLOR']);
+ $this->assertArrayHasKey('@PAGE', $this->cssManager->CSS);
+ $this->assertArrayHasKey('P', $this->cssManager->cascadeCSS['DIV']);
+ }
}
diff --git a/tests/Mpdf/DescendantLangSelectorTest.php b/tests/Mpdf/DescendantLangSelectorTest.php
index c5ba3cf0d..7c27d9c75 100644
--- a/tests/Mpdf/DescendantLangSelectorTest.php
+++ b/tests/Mpdf/DescendantLangSelectorTest.php
@@ -9,7 +9,7 @@
* in block content and in tables.
*
* A regional language such as fr-CA falls back to the rule for its short code, fr, when no rule names it in full,
- * as the simple lang rules do.
+ * as the simple lang rules do. That is how the legacy cascade matches them, and these run under it.
*/
class DescendantLangSelectorTest extends TestCase
{
@@ -31,7 +31,7 @@ class DescendantLangSelectorTest extends TestCase
*/
public function testTheRuleColoursTheText($css, $html, $expected)
{
- $colours = $this->textColours('' . $html);
+ $colours = $this->textColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertArrayHasKey('text', $colours);
$this->assertSame($expected, $colours['text']);
diff --git a/tests/Mpdf/DrawnStyles.php b/tests/Mpdf/DrawnStyles.php
index bc117adc3..18b94c879 100644
--- a/tests/Mpdf/DrawnStyles.php
+++ b/tests/Mpdf/DrawnStyles.php
@@ -40,6 +40,20 @@ private function drawnColours($html, array $config = [])
return $this->keyedByText($mpdf, $mpdf->drawnColours);
}
+ /**
+ * Each piece of text named is drawn, in the colour given for it
+ *
+ * @param array $expected Pieces of text and the operator that should set each one's colour
+ * @param array $colours What was drawn, as drawnColours() gives it
+ */
+ private function assertDrawnInColours(array $expected, array $colours)
+ {
+ foreach ($expected as $text => $colour) {
+ $this->assertArrayHasKey($text, $colours, sprintf('"%s" is not drawn', $text));
+ $this->assertSame($colour, $colours[$text], sprintf('"%s" is drawn in the wrong colour', $text));
+ }
+ }
+
/**
* Keys what was recorded of each piece of text a document drew by the text itself, so a test can look a piece up
* by what it says
diff --git a/tests/Mpdf/InlineAncestorSelectorTest.php b/tests/Mpdf/InlineAncestorSelectorTest.php
index 7abfeedab..b3315c4ff 100644
--- a/tests/Mpdf/InlineAncestorSelectorTest.php
+++ b/tests/Mpdf/InlineAncestorSelectorTest.php
@@ -117,7 +117,7 @@ public function selectors()
*/
public function testLeavesToTheLegacyEngineWhatItMatches($css, $html, $expected)
{
- $colours = $this->drawnColours('' . $html);
+ $colours = $this->drawnColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertSame($expected, $colours['text']);
}
@@ -166,7 +166,8 @@ public function legacyMatches()
public function testAppliesRulesMatchedThroughAnInlineAncestorAfterTheLegacyOnes()
{
$colours = $this->drawnColours(
- 'text
'
+ 'text
',
+ ['cssMode' => CssMode::LEGACY]
);
$this->assertSame(self::BLUE, $colours['text']);
diff --git a/tests/Mpdf/PageStreams.php b/tests/Mpdf/PageStreams.php
index a70ef2640..2daf8fd58 100644
--- a/tests/Mpdf/PageStreams.php
+++ b/tests/Mpdf/PageStreams.php
@@ -263,12 +263,13 @@ private function clipAndPlacement($stream)
*
* @param string $html
* @param int $page Counted from 0
+ * @param array $config
*
* @return array[]
*/
- private function imagePlacements($html, $page = 0)
+ private function imagePlacements($html, $page = 0, $config = [])
{
- $pages = $this->pages($this->render($html));
+ $pages = $this->pages($this->render($html, $config));
$this->assertArrayHasKey($page, $pages);
preg_match_all('/([-\d.]+) 0 0 ([-\d.]+) ([-\d.]+) [-\d.]+ cm \/I\d+ Do/', $pages[$page], $matches, PREG_SET_ORDER);
@@ -281,12 +282,13 @@ private function imagePlacements($html, $page = 0)
* The width of the only image on the first page, in millimetres
*
* @param string $html
+ * @param array $config
*
* @return float
*/
- private function drawnWidth($html)
+ private function drawnWidth($html, $config = [])
{
- $placements = $this->imagePlacements($html);
+ $placements = $this->imagePlacements($html, 0, $config);
$this->assertCount(1, $placements);
return $placements[0]['w'];
@@ -296,12 +298,13 @@ private function drawnWidth($html)
* The fill colour each piece of text on the first page is drawn in, keyed by that text
*
* @param string $html
+ * @param array $config
*
* @return string[]
*/
- private function textColours($html)
+ private function textColours($html, $config = [])
{
- $pages = $this->pages($this->render($html));
+ $pages = $this->pages($this->render($html, $config));
preg_match_all('/q ([\d. ]+ (?:rg|g)) .*?\((.*?)\) Tj/', $pages[0], $drawn, PREG_SET_ORDER);
$colours = [];
diff --git a/tests/Mpdf/PositionedBlockSelectorStandardCascadeTest.php b/tests/Mpdf/PositionedBlockSelectorStandardCascadeTest.php
new file mode 100644
index 000000000..73479720d
--- /dev/null
+++ b/tests/Mpdf/PositionedBlockSelectorStandardCascadeTest.php
@@ -0,0 +1,20 @@
+ CssMode::STANDARD];
+ }
+}
diff --git a/tests/Mpdf/PositionedBlockSelectorTest.php b/tests/Mpdf/PositionedBlockSelectorTest.php
index c0f002f1a..59be3c153 100644
--- a/tests/Mpdf/PositionedBlockSelectorTest.php
+++ b/tests/Mpdf/PositionedBlockSelectorTest.php
@@ -20,6 +20,17 @@ class PositionedBlockSelectorTest extends TestCase
const BLUE = '0.000 0.000 1.000 rg';
const BLACK = '0.000 g';
+ /**
+ * The configuration the documents are written with: the legacy cascade. A subclass runs the tests under the
+ * standard one
+ *
+ * @return array
+ */
+ protected function config()
+ {
+ return ['cssMode' => CssMode::LEGACY];
+ }
+
/**
* A descendant rule naming the block's class reaches a paragraph and a table cell inside it
*/
@@ -31,7 +42,7 @@ public function testClassDescendantRulesReachTheContentOfAnAbsoluteBlock()
.box { color: #0000ff; }
flow p
flow td
- abs p
abs td
abs span');
+ abs p
abs td
abs span', $this->config());
$this->assertSame(self::GREEN, $colours['flow p']);
$this->assertSame(self::RED, $colours['flow td']);
@@ -46,7 +57,7 @@ public function testClassDescendantRulesReachTheContentOfAnAbsoluteBlock()
public function testIdDescendantRulesReachTheContentOfAFixedBlock()
{
$colours = $this->textColours('
- fixed p
');
+ fixed p
', $this->config());
$this->assertSame(self::RED, $colours['fixed p']);
}
@@ -57,7 +68,7 @@ public function testIdDescendantRulesReachTheContentOfAFixedBlock()
public function testTheContentIsAChildOfTheBlockItself()
{
$colours = $this->textColours('
- direct p
nested p
');
+ direct p
nested p
', $this->config());
$this->assertSame(self::BLACK, $colours['direct p']);
$this->assertSame(self::RED, $colours['nested p']);
@@ -70,7 +81,7 @@ public function testTheRulesOfOneBlockDoNotReachTheNext()
{
$colours = $this->textColours('
boxed p
- plain p
');
+ plain p
', $this->config());
$this->assertSame(self::GREEN, $colours['boxed p']);
$this->assertSame(self::BLACK, $colours['plain p']);
diff --git a/tests/Mpdf/RotatedCellFontSizeTest.php b/tests/Mpdf/RotatedCellFontSizeTest.php
new file mode 100644
index 000000000..1e0664d6e
--- /dev/null
+++ b/tests/Mpdf/RotatedCellFontSizeTest.php
@@ -0,0 +1,42 @@
+pages($this->render($html))[0], $sizes);
+
+ $this->assertSame(['9.000'], $sizes[1]);
+ }
+
+ /**
+ * Tables that set a font size, with a rotated cell
+ *
+ * @return array[]
+ */
+ public function tables()
+ {
+ return [
+ 'the size in the table\'s style' => ['Rotated
'],
+ 'the size in a rule for the table' => ['Plain Rotated
'],
+ ];
+ }
+}
diff --git a/tests/Mpdf/StructuralSelectorTest.php b/tests/Mpdf/StructuralSelectorTest.php
index de717a4eb..6d463d949 100644
--- a/tests/Mpdf/StructuralSelectorTest.php
+++ b/tests/Mpdf/StructuralSelectorTest.php
@@ -268,7 +268,7 @@ public function selectors()
*/
public function testAppliesMatchedRulesBySpecificityThenPosition($css, $html, $expected)
{
- $colours = $this->drawnColours('' . $html);
+ $colours = $this->drawnColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertSame($expected, $colours['text']);
}
diff --git a/tests/Mpdf/StyledElementPathTest.php b/tests/Mpdf/StyledElementPathTest.php
index f9a6e2903..8223dc418 100644
--- a/tests/Mpdf/StyledElementPathTest.php
+++ b/tests/Mpdf/StyledElementPathTest.php
@@ -6,7 +6,8 @@
/**
* The open elements the CSS merger matches compiled rules against, for each element it styles: the element opened
- * by a start tag, a block opened again after a forced page break, a positioned block and the content written in it
+ * by a start tag, a block opened again after a forced page break, a positioned block and the content written in it.
+ * These run under the legacy cascade, which asks for the path only when a compiled rule is filed under the element
*/
class StyledElementPathTest extends TestCase
{
@@ -22,7 +23,7 @@ class StyledElementPathTest extends TestCase
*/
public function testGivesThePathToTheElementBeingStyled($html, array $expected)
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML($html);
$this->assertSame($expected, $mpdf->styledPaths);
@@ -38,7 +39,7 @@ public function testGivesThePathToTheElementBeingStyled($html, array $expected)
*/
public function testGivesThePositionTheElementHasOnItsPath($html)
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML($html . ''
. 'a b
c d
');
@@ -85,7 +86,7 @@ public function documents()
*/
public function testLetsGoOfTheElementOnceItsStartTagIsRead()
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML('a
b
');
$this->assertNull($mpdf->getStyledElementPath());
@@ -96,7 +97,7 @@ public function testLetsGoOfTheElementOnceItsStartTagIsRead()
*/
public function testGivesNoPathForASpanWrappedAroundARunOfAnotherScript()
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => '', 'autoScriptToLang' => true]);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => '', 'autoScriptToLang' => true, 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML('abc שלום def
');
$this->assertSame([null, '>P>SPAN'], $mpdf->styledPaths);
diff --git a/tests/Mpdf/TableCellDescendantSelectorStandardCascadeTest.php b/tests/Mpdf/TableCellDescendantSelectorStandardCascadeTest.php
new file mode 100644
index 000000000..34e121328
--- /dev/null
+++ b/tests/Mpdf/TableCellDescendantSelectorStandardCascadeTest.php
@@ -0,0 +1,20 @@
+ CssMode::STANDARD];
+ }
+}
diff --git a/tests/Mpdf/TableCellDescendantSelectorTest.php b/tests/Mpdf/TableCellDescendantSelectorTest.php
index 0aa9f7899..826034917 100644
--- a/tests/Mpdf/TableCellDescendantSelectorTest.php
+++ b/tests/Mpdf/TableCellDescendantSelectorTest.php
@@ -12,6 +12,17 @@ class TableCellDescendantSelectorTest extends \Yoast\PHPUnitPolyfills\TestCases\
const RED = '1.000 0.000 0.000 rg';
+ /**
+ * The configuration the documents are written with: the legacy cascade. A subclass runs the tests under the
+ * standard one
+ *
+ * @return array
+ */
+ protected function config()
+ {
+ return ['cssMode' => CssMode::LEGACY];
+ }
+
/**
* A table of one cell holding the given content, in a div, under the given stylesheet
*
@@ -60,7 +71,7 @@ public static function matchingRules()
*/
public function testARuleReachesAPictureInTheCell($selector, $content)
{
- $this->assertEqualsWithDelta(20, $this->drawnWidth($this->table($selector . ' { max-width: 20mm; }', $content)), 0.01);
+ $this->assertEqualsWithDelta(20, $this->drawnWidth($this->table($selector . ' { max-width: 20mm; }', $content), $this->config()), 0.01);
}
/**
@@ -68,7 +79,7 @@ public function testARuleReachesAPictureInTheCell($selector, $content)
*/
public function testARuleForAnotherCellDoesNotMatch()
{
- $width = $this->drawnWidth($this->table('td.other img { max-width: 20mm; }', self::image()));
+ $width = $this->drawnWidth($this->table('td.other img { max-width: 20mm; }', self::image()), $this->config());
$this->assertEqualsWithDelta(292 * 25.4 / 96, $width, 0.01);
}
@@ -94,7 +105,7 @@ public static function textRules()
*/
public function testARuleReachesTextInTheCell($selector, $content)
{
- $colours = $this->textColours($this->table($selector . ' { color: #ff0000; }', $content));
+ $colours = $this->textColours($this->table($selector . ' { color: #ff0000; }', $content), $this->config());
$this->assertSame(self::RED, $colours['red']);
}
diff --git a/tests/Snapshots/DescendantLangSelectorSnapshotTest.php b/tests/Snapshots/DescendantLangSelectorSnapshotTest.php
index ce036484e..456728ef3 100644
--- a/tests/Snapshots/DescendantLangSelectorSnapshotTest.php
+++ b/tests/Snapshots/DescendantLangSelectorSnapshotTest.php
@@ -4,7 +4,8 @@
/**
* Descendant rules whose last part names a language, such as `div :lang(fr)`, `div [lang=fr]` and `div p:lang(fr)`,
- * in block content, inline content and tables, and a regional language falling back to the rule for its short code.
+ * in block content, inline content and tables, and a regional language falling back to the rule for its short code,
+ * under the legacy cascade.
*
* @group snapshot
*/
@@ -107,7 +108,8 @@ public function generatePdf()
mpdf = $this->createMpdf();
+ // A regional language named in full winning over its short code, whatever the order, is the legacy cascade's
+ $this->mpdf = $this->createMpdf(['cssMode' => \Mpdf\CssMode::LEGACY]);
$this->mpdf->WriteHTML($html);
}
diff --git a/tests/Snapshots/FrameworkCascadeLegacySnapshotTest.php b/tests/Snapshots/FrameworkCascadeLegacySnapshotTest.php
new file mode 100644
index 000000000..15e72f501
--- /dev/null
+++ b/tests/Snapshots/FrameworkCascadeLegacySnapshotTest.php
@@ -0,0 +1,21 @@
+cascade();
+ }
+
+ /**
+ * The components, each under a caption saying what the standard and the legacy cascade draw
+ */
+ public function generatePdf()
+ {
+ ob_start();
+ ?>
+
+
+ A framework's stylesheet
+ Written with cssMode set to cascade(); ?>.
+
+ Navbar
+ #brand against .navbar > .container a. Standard: "Brand" is yellow and bold, the id beating the child rule. Legacy: "Brand" is grey like the other links, though still bold, because a rule only the matcher reads is applied after the id.
+
+
+ Card
+ h5.card-title against .card h5, as heavy and written later. Standard: the title is blue. Legacy: it is black, the descendant rule coming last.
+ .lead against article p. Standard: the lead paragraph is large and grey, a class outweighing two tags. Legacy: it is the size and colour of the paragraph after it.
+
+ Card title
+ A lead paragraph, set larger than the text after it.
The body text of the card.
+
+
+ Sidebar navigation
+ #sidebar a against .nav .nav-link. Standard: the links are purple and bold, an id ancestor outweighing two classes. Legacy: they are blue and bold, the rule lifted from the nearer .nav coming last.
+
+
+ List group
+ .list-group-item.active against .list-group .list-group-item, as heavy and written later. Standard: the active item is white on blue. Legacy: it is white on light grey, the descendant rule's background coming last, so its text all but disappears.
+
+ - An item
+ - The active item
+ - Another item
+
+
+ Table
+ td.text-end against .table td, as heavy and written later. Standard: the amounts are right-aligned. Legacy: they are left-aligned, the descendant rule coming last. "Hosting", the third row counting the header, is striped grey in both.
+
+ Item Amount
+ Consulting 1,200.00
+ Hosting 300.00
+ Support 150.00
+
+
+ Utilities and components
+ .muted, written after .title. Standard: "Muted title" is light grey and bold. Legacy: it is blue and bold, classes applying in alphabetical order.
+ Muted title
+ mpdf = $this->createMpdf(['cssMode' => $this->cascade()]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/Snapshots/FrameworkCascadeStandardSnapshotTest.php b/tests/Snapshots/FrameworkCascadeStandardSnapshotTest.php
new file mode 100644
index 000000000..fd8eb5b5f
--- /dev/null
+++ b/tests/Snapshots/FrameworkCascadeStandardSnapshotTest.php
@@ -0,0 +1,21 @@
+
+
+
+
+ The standard cascade
+
+ #id beats p.class: the line is green.
+ An id against a tag with a class
+
+ #id beats div.class div p: the line is green.
+ An id against a descendant rule
+
+ .class.class beats body p.class: the line is green.
+ Two classes against body and a tag with a class
+
+ .class p beats div div div p, although the divs are nearer: the line is green.
+ A class ancestor against three tag ancestors
+
+ .c5a is written after .c5b, so it wins whichever order the classes are in: both lines are green.
+ Classes a then b
Classes b then a
+
+ .c6a, then .c6b, then .c6a again: the second .c6a comes last, and the line is green.
+ A selector written twice
+
+ A rule in a later style block beats one as heavy in an earlier block: the line is green.
+ A rule in a later stylesheet
+
+ :where() counts nothing, so div.class p beats :where(#id) p: the line is green.
+ :where() against a class
+
+ The inline style beats #id: the line is green.
+ Inline style against an id
+
+ ul.class beats the default stylesheet's ul ul, which counts as the built-in stylesheet's: the nested list keeps the 4mm gap below it, so "Outer two" is well below "Inner two".
+
+ - Outer one
- Inner one
- Inner two
- Outer two
+
+
+ <hr color> beats the grey of the built-in defaults: the rule is green.
+
+
+ td#id beats table.class tr td: the middle cell has a thick green border, drawn over its neighbours' thin red ones.
+ Left Middle Right
+ mpdf = $this->createMpdf(['cssMode' => \Mpdf\CssMode::STANDARD]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/data/snapshots/framework-cascade-legacy.pdf b/tests/data/snapshots/framework-cascade-legacy.pdf
new file mode 100644
index 000000000..6e54d92e3
Binary files /dev/null and b/tests/data/snapshots/framework-cascade-legacy.pdf differ
diff --git a/tests/data/snapshots/framework-cascade-standard.pdf b/tests/data/snapshots/framework-cascade-standard.pdf
new file mode 100644
index 000000000..805ab9454
Binary files /dev/null and b/tests/data/snapshots/framework-cascade-standard.pdf differ
diff --git a/tests/data/snapshots/page-break-avoid-content.pdf b/tests/data/snapshots/page-break-avoid-content.pdf
index 1b0b528fb..7497dc2a8 100644
Binary files a/tests/data/snapshots/page-break-avoid-content.pdf and b/tests/data/snapshots/page-break-avoid-content.pdf differ
diff --git a/tests/data/snapshots/page-break-avoid-floats.pdf b/tests/data/snapshots/page-break-avoid-floats.pdf
index e2adc0af1..7da0d3ee5 100644
Binary files a/tests/data/snapshots/page-break-avoid-floats.pdf and b/tests/data/snapshots/page-break-avoid-floats.pdf differ
diff --git a/tests/data/snapshots/positioned-html.pdf b/tests/data/snapshots/positioned-html.pdf
index 80b27020e..2343ab40c 100644
Binary files a/tests/data/snapshots/positioned-html.pdf and b/tests/data/snapshots/positioned-html.pdf differ
diff --git a/tests/data/snapshots/rtl.pdf b/tests/data/snapshots/rtl.pdf
index 40fe553b9..724d684aa 100644
Binary files a/tests/data/snapshots/rtl.pdf and b/tests/data/snapshots/rtl.pdf differ
diff --git a/tests/data/snapshots/standard-cascade.pdf b/tests/data/snapshots/standard-cascade.pdf
new file mode 100644
index 000000000..8f561bf9b
Binary files /dev/null and b/tests/data/snapshots/standard-cascade.pdf differ
diff --git a/tests/data/snapshots/table.pdf b/tests/data/snapshots/table.pdf
index 69a1fe048..93b183e04 100644
Binary files a/tests/data/snapshots/table.pdf and b/tests/data/snapshots/table.pdf differ
diff --git a/tests/data/snapshots/toc-and-index.pdf b/tests/data/snapshots/toc-and-index.pdf
index 1e8ad6242..d1762981c 100644
Binary files a/tests/data/snapshots/toc-and-index.pdf and b/tests/data/snapshots/toc-and-index.pdf differ
` walks 12.5 million siblings that way + */ + public function testFindsAnEarlierSiblingWithoutWalkingThroughEachOne() + { + $children = [['H2']]; + for ($i = 0; $i < 5000; $i++) { + $children[] = ['P']; + } + $parent = $this->frame('', 1, 1, [], $children); + $heading = $this->compiler->compile('h2 ~ p'); + $missing = $this->compiler->compile('h3 ~ p'); + + $start = microtime(true); + for ($i = 1; $i <= 5000; $i++) { + $path = [$parent, $this->frame('P', $i + 1, $i, [], [])]; + $this->assertTrue($this->matcher->matches($heading, $path)); + $this->assertFalse($this->matcher->matches($missing, $path)); + } + $this->assertLessThan(1, microtime(true) - $start); + } + /** * The open elements the selectors are matched against: `
- ` that follows
* an `
- An item +
- The active item +
- Another item +
- Outer one
- Inner one
- Inner two
- Outer two
` and a `
` in `
`, a `
` and a `
` in
diff --git a/tests/Mpdf/CssManagerTest.php b/tests/Mpdf/CssManagerTest.php
index ecc771b3f..13ea9f102 100644
--- a/tests/Mpdf/CssManagerTest.php
+++ b/tests/Mpdf/CssManagerTest.php
@@ -19,7 +19,8 @@ protected function set_up()
$this->tempDir = sys_get_temp_dir() . '/mpdf_test_' . uniqid();
mkdir($this->tempDir);
- $this->mpdf = new Mpdf(['tempDir' => $this->tempDir]);
+ // The merges below read the legacy cascade's keys and trees
+ $this->mpdf = new Mpdf(['tempDir' => $this->tempDir, 'cssMode' => CssMode::LEGACY]);
$this->mpdf->setBasePath($this->tempDir);
// Use reflection to access private cssManager property
@@ -903,4 +904,35 @@ public function testKeepsTheCompiledRulesOfEveryStylesheetRead()
$this->assertSame(['COLOR' => 'red'], $rules->rule($candidates[0])[1]);
$this->assertSame(['COLOR' => 'green'], $rules->rule($candidates[1])[1]);
}
+
+ /**
+ * The default stylesheet's rules are kept apart from the document's, and a document's stylesheet never adds to them
+ */
+ public function testKeepsTheDefaultStylesheetsRulesApart()
+ {
+ $default = $this->cssManager->getDefaultRules()->candidates('UL', '', []);
+
+ $this->cssManager->readDefaultCss('ul > ul { margin-top: 0; }');
+ $this->cssManager->readCss('');
+
+ $defaultRules = array_values(array_diff($this->cssManager->getDefaultRules()->candidates('UL', '', []), $default));
+ $documentRules = $this->cssManager->getRules()->candidates('UL', '', []);
+ $this->assertCount(1, $defaultRules);
+ $this->assertCount(1, $documentRules);
+ $this->assertSame(['MARGIN-TOP' => '0'], $this->cssManager->getDefaultRules()->rule($defaultRules[0])[1]);
+ $this->assertSame(['COLOR' => 'red'], $this->cssManager->getRules()->rule($documentRules[0])[1]);
+ $this->assertSame([], $this->cssManager->getDefaultRules()->candidates('LI', '', []));
+ }
+
+ /**
+ * The default stylesheet is read into the same stores a document's stylesheet is, for what reads them directly
+ */
+ public function testReadsTheDefaultStylesheetIntoTheSharedStores()
+ {
+ $this->cssManager->readDefaultCss('.note { color: red; } div p { margin: 0; } @page { margin-left: 1cm; }');
+
+ $this->assertSame('red', $this->cssManager->CSS['CLASS>>NOTE']['COLOR']);
+ $this->assertArrayHasKey('@PAGE', $this->cssManager->CSS);
+ $this->assertArrayHasKey('P', $this->cssManager->cascadeCSS['DIV']);
+ }
}
diff --git a/tests/Mpdf/DescendantLangSelectorTest.php b/tests/Mpdf/DescendantLangSelectorTest.php
index c5ba3cf0d..7c27d9c75 100644
--- a/tests/Mpdf/DescendantLangSelectorTest.php
+++ b/tests/Mpdf/DescendantLangSelectorTest.php
@@ -9,7 +9,7 @@
* in block content and in tables.
*
* A regional language such as fr-CA falls back to the rule for its short code, fr, when no rule names it in full,
- * as the simple lang rules do.
+ * as the simple lang rules do. That is how the legacy cascade matches them, and these run under it.
*/
class DescendantLangSelectorTest extends TestCase
{
@@ -31,7 +31,7 @@ class DescendantLangSelectorTest extends TestCase
*/
public function testTheRuleColoursTheText($css, $html, $expected)
{
- $colours = $this->textColours('' . $html);
+ $colours = $this->textColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertArrayHasKey('text', $colours);
$this->assertSame($expected, $colours['text']);
diff --git a/tests/Mpdf/DrawnStyles.php b/tests/Mpdf/DrawnStyles.php
index bc117adc3..18b94c879 100644
--- a/tests/Mpdf/DrawnStyles.php
+++ b/tests/Mpdf/DrawnStyles.php
@@ -40,6 +40,20 @@ private function drawnColours($html, array $config = [])
return $this->keyedByText($mpdf, $mpdf->drawnColours);
}
+ /**
+ * Each piece of text named is drawn, in the colour given for it
+ *
+ * @param array text text flow p abs p abs p fixed p fixed p direct p nested p direct p nested p boxed p plain p plain p a b abc שלום def Written with cssMode set to cascade(); ?>. #brand against .navbar > .container a. Standard: "Brand" is yellow and bold, the id beating the child rule. Legacy: "Brand" is grey like the other links, though still bold, because a rule only the matcher reads is applied after the id. h5.card-title against .card h5, as heavy and written later. Standard: the title is blue. Legacy: it is black, the descendant rule coming last. .lead against article p. Standard: the lead paragraph is large and grey, a class outweighing two tags. Legacy: it is the size and colour of the paragraph after it. A lead paragraph, set larger than the text after it. The body text of the card. #sidebar a against .nav .nav-link. Standard: the links are purple and bold, an id ancestor outweighing two classes. Legacy: they are blue and bold, the rule lifted from the nearer .nav coming last. .list-group-item.active against .list-group .list-group-item, as heavy and written later. Standard: the active item is white on blue. Legacy: it is white on light grey, the descendant rule's background coming last, so its text all but disappears. td.text-end against .table td, as heavy and written later. Standard: the amounts are right-aligned. Legacy: they are left-aligned, the descendant rule coming last. "Hosting", the third row counting the header, is striped grey in both. .muted, written after .title. Standard: "Muted title" is light grey and bold. Legacy: it is blue and bold, classes applying in alphabetical order. Muted title #id beats p.class: the line is green. An id against a tag with a class #id beats div.class div p: the line is green. An id against a descendant rule .class.class beats body p.class: the line is green. Two classes against body and a tag with a class .class p beats div div div p, although the divs are nearer: the line is green. A class ancestor against three tag ancestors .c5a is written after .c5b, so it wins whichever order the classes are in: both lines are green. Classes a then b Classes b then a .c6a, then .c6b, then .c6a again: the second .c6a comes last, and the line is green. A selector written twice A rule in a later style block beats one as heavy in an earlier block: the line is green. A rule in a later stylesheet :where() counts nothing, so div.class p beats :where(#id) p: the line is green. :where() against a class The inline style beats #id: the line is green. Inline style against an id ul.class beats the default stylesheet's ul ul, which counts as the built-in stylesheet's: the nested list keeps the 4mm gap below it, so "Outer two" is well below "Inner two". <hr color> beats the grey of the built-in defaults: the rule is green. td#id beats table.class tr td: the middle cell has a thick green border, drawn over its neighbours' thin red ones.flow td
abs spanabs td
abs spanabs td
'],
+ 'the size in a rule for the table' => ['Rotated
'],
+ ];
+ }
+}
diff --git a/tests/Mpdf/StructuralSelectorTest.php b/tests/Mpdf/StructuralSelectorTest.php
index de717a4eb..6d463d949 100644
--- a/tests/Mpdf/StructuralSelectorTest.php
+++ b/tests/Mpdf/StructuralSelectorTest.php
@@ -268,7 +268,7 @@ public function selectors()
*/
public function testAppliesMatchedRulesBySpecificityThenPosition($css, $html, $expected)
{
- $colours = $this->drawnColours('' . $html);
+ $colours = $this->drawnColours('' . $html, ['cssMode' => CssMode::LEGACY]);
$this->assertSame($expected, $colours['text']);
}
diff --git a/tests/Mpdf/StyledElementPathTest.php b/tests/Mpdf/StyledElementPathTest.php
index f9a6e2903..8223dc418 100644
--- a/tests/Mpdf/StyledElementPathTest.php
+++ b/tests/Mpdf/StyledElementPathTest.php
@@ -6,7 +6,8 @@
/**
* The open elements the CSS merger matches compiled rules against, for each element it styles: the element opened
- * by a start tag, a block opened again after a forced page break, a positioned block and the content written in it
+ * by a start tag, a block opened again after a forced page break, a positioned block and the content written in it.
+ * These run under the legacy cascade, which asks for the path only when a compiled rule is filed under the element
*/
class StyledElementPathTest extends TestCase
{
@@ -22,7 +23,7 @@ class StyledElementPathTest extends TestCase
*/
public function testGivesThePathToTheElementBeingStyled($html, array $expected)
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML($html);
$this->assertSame($expected, $mpdf->styledPaths);
@@ -38,7 +39,7 @@ public function testGivesThePathToTheElementBeingStyled($html, array $expected)
*/
public function testGivesThePositionTheElementHasOnItsPath($html)
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML($html . ''
. 'Plain Rotated a b
');
@@ -85,7 +86,7 @@ public function documents()
*/
public function testLetsGoOfTheElementOnceItsStartTagIsRead()
{
- $mpdf = new StyledPathRecordingMpdf(['mode' => 'c']);
+ $mpdf = new StyledPathRecordingMpdf(['mode' => 'c', 'cssMode' => CssMode::LEGACY]);
$mpdf->WriteHTML('c d A framework's stylesheet
+ Navbar
+ Card
+ Card title
+ Sidebar navigation
+ List group
+
+
+
+ Table
+
+
+
+
+ Item Amount
+ Consulting 1,200.00
+ Hosting 300.00
+ Support 150.00 Utilities and components
+ The standard cascade
+
+
+
+ mpdf = $this->createMpdf(['cssMode' => \Mpdf\CssMode::STANDARD]);
+
+ $this->mpdf->WriteHTML($html);
+ }
+
+}
diff --git a/tests/data/snapshots/framework-cascade-legacy.pdf b/tests/data/snapshots/framework-cascade-legacy.pdf
new file mode 100644
index 000000000..6e54d92e3
Binary files /dev/null and b/tests/data/snapshots/framework-cascade-legacy.pdf differ
diff --git a/tests/data/snapshots/framework-cascade-standard.pdf b/tests/data/snapshots/framework-cascade-standard.pdf
new file mode 100644
index 000000000..805ab9454
Binary files /dev/null and b/tests/data/snapshots/framework-cascade-standard.pdf differ
diff --git a/tests/data/snapshots/page-break-avoid-content.pdf b/tests/data/snapshots/page-break-avoid-content.pdf
index 1b0b528fb..7497dc2a8 100644
Binary files a/tests/data/snapshots/page-break-avoid-content.pdf and b/tests/data/snapshots/page-break-avoid-content.pdf differ
diff --git a/tests/data/snapshots/page-break-avoid-floats.pdf b/tests/data/snapshots/page-break-avoid-floats.pdf
index e2adc0af1..7da0d3ee5 100644
Binary files a/tests/data/snapshots/page-break-avoid-floats.pdf and b/tests/data/snapshots/page-break-avoid-floats.pdf differ
diff --git a/tests/data/snapshots/positioned-html.pdf b/tests/data/snapshots/positioned-html.pdf
index 80b27020e..2343ab40c 100644
Binary files a/tests/data/snapshots/positioned-html.pdf and b/tests/data/snapshots/positioned-html.pdf differ
diff --git a/tests/data/snapshots/rtl.pdf b/tests/data/snapshots/rtl.pdf
index 40fe553b9..724d684aa 100644
Binary files a/tests/data/snapshots/rtl.pdf and b/tests/data/snapshots/rtl.pdf differ
diff --git a/tests/data/snapshots/standard-cascade.pdf b/tests/data/snapshots/standard-cascade.pdf
new file mode 100644
index 000000000..8f561bf9b
Binary files /dev/null and b/tests/data/snapshots/standard-cascade.pdf differ
diff --git a/tests/data/snapshots/table.pdf b/tests/data/snapshots/table.pdf
index 69a1fe048..93b183e04 100644
Binary files a/tests/data/snapshots/table.pdf and b/tests/data/snapshots/table.pdf differ
diff --git a/tests/data/snapshots/toc-and-index.pdf b/tests/data/snapshots/toc-and-index.pdf
index 1e8ad6242..d1762981c 100644
Binary files a/tests/data/snapshots/toc-and-index.pdf and b/tests/data/snapshots/toc-and-index.pdf differ
Left Middle Right