Update pelago/emogrifier to 4.0
authorTim Düsterhus <duesterhus@woltlab.com>
Thu, 19 Nov 2020 13:30:40 +0000 (14:30 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 19 Nov 2020 13:30:40 +0000 (14:30 +0100)
40 files changed:
wcfsetup/install/files/lib/system/api/composer.json
wcfsetup/install/files/lib/system/api/composer.lock
wcfsetup/install/files/lib/system/api/composer/InstalledVersions.php
wcfsetup/install/files/lib/system/api/composer/autoload_psr4.php
wcfsetup/install/files/lib/system/api/composer/autoload_static.php
wcfsetup/install/files/lib/system/api/composer/installed.json
wcfsetup/install/files/lib/system/api/composer/installed.php
wcfsetup/install/files/lib/system/api/composer/platform_check.php
wcfsetup/install/files/lib/system/api/pelago/emogrifier/.github/CONTRIBUTING.md [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/.gitignore [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/.travis.yml [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/CHANGELOG.md
wcfsetup/install/files/lib/system/api/pelago/emogrifier/CODE_OF_CONDUCT.md [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/README.md
wcfsetup/install/files/lib/system/api/pelago/emogrifier/composer.json
wcfsetup/install/files/lib/system/api/pelago/emogrifier/config/php-cs-fixer.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/config/phpmd.xml [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/phpcs.xml.dist [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/CssInliner.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/CssConcatenator.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/CssInliner.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/AbstractHtmlProcessor.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/CssToAttributeConverter.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlNormalizer.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlPruner.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/ArrayIntersector.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/CssConcatenator.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Support/Traits/AssertCss.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/CssInlinerTest.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/CssConcatenatorTest.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/AbstractHtmlProcessorTest.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/CssToAttributeConverterTest.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/Fixtures/TestingHtmlProcessor.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/HtmlNormalizerTest.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/EmogrifierTest.php [deleted file]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Support/Traits/AssertCssTest.php [deleted file]

index 53caffb14733ca64b7a338a165f129b5ddc16a75..24d419216da6b5b67ecc381343998d3aec76aec0 100644 (file)
@@ -10,7 +10,7 @@
     "require": {
         "ezyang/htmlpurifier": "4.13.*",
         "erusev/parsedown": "1.7.*",
-        "pelago/emogrifier": "2.1.*",
+        "pelago/emogrifier": "4.0.*",
         "chrisjean/php-ico": "1.0.*",
         "true/punycode": "~2.0",
         "pear/net_idna2": "^0.2.0",
index da84909330d85b6e579b2ca60e7d9de906b543bc..0f0ab507e9758da1313d29176a10d47039c255cd 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "11c5b4995ff3a595d75c77350b9556ec",
+    "content-hash": "88f8ac80c8e871d978df5d19cf119c8e",
     "packages": [
         {
             "name": "chrisjean/php-ico",
         },
         {
             "name": "pelago/emogrifier",
-            "version": "v2.1.1",
+            "version": "v4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/MyIntervals/emogrifier.git",
-                "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983"
+                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8ee7fb5ad772915451ed3415c1992bd3697d4983",
-                "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983",
+                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/f6fd679303c6e6861b5ff29af221f684729d8fd9",
+                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
-                "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
-                "symfony/css-selector": "^3.4.0 || ^4.0.0"
+                "php": "~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4",
+                "symfony/css-selector": "^3.4.32 || ^4.3.5 || ^5.0"
             },
             "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.2.0",
-                "phpmd/phpmd": "^2.6.0",
-                "phpunit/phpunit": "^4.8.0",
-                "squizlabs/php_codesniffer": "^3.3.2"
+                "grogy/php-parallel-lint": "^1.1.0",
+                "phpunit/phpunit": "^6.5.14",
+                "psalm/plugin-phpunit": "^0.5.8",
+                "slevomat/coding-standard": "^4.0.0",
+                "squizlabs/php_codesniffer": "^3.5.1",
+                "vimeo/psalm": "^3.2.12"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.1.x-dev"
+                    "dev-master": "5.0.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Pelago\\": "src/"
+                    "Pelago\\Emogrifier\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "John Reeve",
-                    "email": "jreeve@pelagodesign.com"
-                },
-                {
-                    "name": "Cameron Brooks"
-                },
-                {
-                    "name": "Jaime Prado"
-                },
                 {
                     "name": "Oliver Klee",
                     "email": "github@oliverklee.de"
                     "name": "Zoli Szabó",
                     "email": "zoli.szabo+github@gmail.com"
                 },
+                {
+                    "name": "John Reeve",
+                    "email": "jreeve@pelagodesign.com"
+                },
                 {
                     "name": "Jake Hotson",
                     "email": "jake@qzdesign.co.uk"
+                },
+                {
+                    "name": "Cameron Brooks"
+                },
+                {
+                    "name": "Jaime Prado"
                 }
             ],
             "description": "Converts CSS styles into inline style attributes in your HTML code",
                 "email",
                 "pre-processing"
             ],
-            "time": "2018-12-10T10:36:30+00:00"
+            "support": {
+                "issues": "https://github.com/MyIntervals/emogrifier/issues",
+                "source": "https://github.com/MyIntervals/emogrifier"
+            },
+            "time": "2020-06-12T12:55:03+00:00"
         },
         {
             "name": "psr/http-client",
index 64f6adb556fc368fcad0dc642fc1267edf343c7e..03e40ce82ba9c407167768fd74178be51af88113 100644 (file)
@@ -116,12 +116,12 @@ private static $installed = array (
     ),
     'pelago/emogrifier' => 
     array (
-      'pretty_version' => 'v2.1.1',
-      'version' => '2.1.1.0',
+      'pretty_version' => 'v4.0.0',
+      'version' => '4.0.0.0',
       'aliases' => 
       array (
       ),
-      'reference' => '8ee7fb5ad772915451ed3415c1992bd3697d4983',
+      'reference' => 'f6fd679303c6e6861b5ff29af221f684729d8fd9',
     ),
     'psr/http-client' => 
     array (
index b810a52444f21e4f4f0e6dcf1e5a6ba88454928d..824dc84ca6d3c4dc35e9c8b956e92cf4ee579473 100644 (file)
@@ -12,7 +12,7 @@ return array(
     'ScssPhp\\ScssPhp\\' => array($vendorDir . '/scssphp/scssphp/src'),
     'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
     'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
-    'Pelago\\' => array($vendorDir . '/pelago/emogrifier/src'),
+    'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'),
     'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
     'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
     'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
index 8abb57a3ca2860e8d0bbbc2d6e869d0bd61060fa..d81f4fb95d3162b973a60d4e7ba708dbbdfaed02 100644 (file)
@@ -30,7 +30,7 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d
         array (
             'Psr\\Http\\Message\\' => 17,
             'Psr\\Http\\Client\\' => 16,
-            'Pelago\\' => 7,
+            'Pelago\\Emogrifier\\' => 18,
             'ParagonIE\\ConstantTime\\' => 23,
         ),
         'G' => 
@@ -66,7 +66,7 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d
         array (
             0 => __DIR__ . '/..' . '/psr/http-client/src',
         ),
-        'Pelago\\' => 
+        'Pelago\\Emogrifier\\' => 
         array (
             0 => __DIR__ . '/..' . '/pelago/emogrifier/src',
         ),
index 30446fb034ddd39493b1b26eea4abb839626deff..954c8fe154ff3e909ff7873137ff6d5140acd65b 100644 (file)
         },
         {
             "name": "pelago/emogrifier",
-            "version": "v2.1.1",
-            "version_normalized": "2.1.1.0",
+            "version": "v4.0.0",
+            "version_normalized": "4.0.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/MyIntervals/emogrifier.git",
-                "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983"
+                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8ee7fb5ad772915451ed3415c1992bd3697d4983",
-                "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983",
+                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/f6fd679303c6e6861b5ff29af221f684729d8fd9",
+                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
-                "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
-                "symfony/css-selector": "^3.4.0 || ^4.0.0"
+                "php": "~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4",
+                "symfony/css-selector": "^3.4.32 || ^4.3.5 || ^5.0"
             },
             "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.2.0",
-                "phpmd/phpmd": "^2.6.0",
-                "phpunit/phpunit": "^4.8.0",
-                "squizlabs/php_codesniffer": "^3.3.2"
-            },
-            "time": "2018-12-10T10:36:30+00:00",
+                "grogy/php-parallel-lint": "^1.1.0",
+                "phpunit/phpunit": "^6.5.14",
+                "psalm/plugin-phpunit": "^0.5.8",
+                "slevomat/coding-standard": "^4.0.0",
+                "squizlabs/php_codesniffer": "^3.5.1",
+                "vimeo/psalm": "^3.2.12"
+            },
+            "time": "2020-06-12T12:55:03+00:00",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.1.x-dev"
+                    "dev-master": "5.0.x-dev"
                 }
             },
             "installation-source": "dist",
             "autoload": {
                 "psr-4": {
-                    "Pelago\\": "src/"
+                    "Pelago\\Emogrifier\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "John Reeve",
-                    "email": "jreeve@pelagodesign.com"
-                },
-                {
-                    "name": "Cameron Brooks"
-                },
-                {
-                    "name": "Jaime Prado"
-                },
                 {
                     "name": "Oliver Klee",
                     "email": "github@oliverklee.de"
                     "name": "Zoli Szabó",
                     "email": "zoli.szabo+github@gmail.com"
                 },
+                {
+                    "name": "John Reeve",
+                    "email": "jreeve@pelagodesign.com"
+                },
                 {
                     "name": "Jake Hotson",
                     "email": "jake@qzdesign.co.uk"
+                },
+                {
+                    "name": "Cameron Brooks"
+                },
+                {
+                    "name": "Jaime Prado"
                 }
             ],
             "description": "Converts CSS styles into inline style attributes in your HTML code",
                 "email",
                 "pre-processing"
             ],
+            "support": {
+                "issues": "https://github.com/MyIntervals/emogrifier/issues",
+                "source": "https://github.com/MyIntervals/emogrifier"
+            },
             "install-path": "../pelago/emogrifier"
         },
         {
             "install-path": "../true/punycode"
         }
     ],
-    "dev": true
+    "dev": true,
+    "dev-package-names": []
 }
index 55dcfcf5be3f6e496fde6f6c1b8cdbe5f7e6dd8f..b2473602fa04a146eb344de22a17be46cdefde19 100644 (file)
     ),
     'pelago/emogrifier' => 
     array (
-      'pretty_version' => 'v2.1.1',
-      'version' => '2.1.1.0',
+      'pretty_version' => 'v4.0.0',
+      'version' => '4.0.0.0',
       'aliases' => 
       array (
       ),
-      'reference' => '8ee7fb5ad772915451ed3415c1992bd3697d4983',
+      'reference' => 'f6fd679303c6e6861b5ff29af221f684729d8fd9',
     ),
     'psr/http-client' => 
     array (
index 1e56f6ecbcd7e606c05e990c5302dfc1385339d1..a8b98d5ceb1e2651c6b52984bfd44cfb97347d10 100644 (file)
@@ -5,22 +5,22 @@
 $issues = array();
 
 if (!(PHP_VERSION_ID >= 70205)) {
-    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION  .  '.';
-}
-
-$missingExtensions = array();
-extension_loaded('ctype') || $missingExtensions[] = 'ctype';
-extension_loaded('dom') || $missingExtensions[] = 'dom';
-extension_loaded('gd') || $missingExtensions[] = 'gd';
-extension_loaded('json') || $missingExtensions[] = 'json';
-extension_loaded('libxml') || $missingExtensions[] = 'libxml';
-extension_loaded('mbstring') || $missingExtensions[] = 'mbstring';
-
-if ($missingExtensions) {
-    $issues[] = 'Your Composer dependencies require the following PHP extensions to be installed: ' . implode(', ', $missingExtensions);
+    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
 }
 
 if ($issues) {
-    echo 'Composer detected issues in your platform:' . "\n\n" . implode("\n", $issues);
-    exit(104);
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+        } elseif (!headers_sent()) {
+            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+        }
+    }
+    trigger_error(
+        'Composer detected issues in your platform: ' . implode(' ', $issues),
+        E_USER_ERROR
+    );
 }
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/.github/CONTRIBUTING.md b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/.github/CONTRIBUTING.md
deleted file mode 100644 (file)
index 6287d65..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-# Contributing to Emogrifier
-
-Those that wish to contribute bug fixes, new features, refactorings and
-clean-up to Emogrifier are more than welcome.
-
-When you contribute, please take the following things into account:
-
-
-## Contributor Code of Conduct
-
-Please note that this project is released with a
-[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this
-project, you agree to abide by its terms.
-
-
-## General workflow
-
-This is the workflow for contributing changes to Emogrifier:
-
-1. [Fork the Emogrifier Git repository](https://guides.github.com/activities/forking/).
-2. Clone your forked repository and
-   [install the development dependencies](#install-the-development-dependencies).
-3. Add a local remote "upstream" so you will be able to
-   [synchronize your fork with the original Emogrifier repository](https://help.github.com/articles/syncing-a-fork/).
-4. Create a local branch for your changes.
-5. [Add unit tests for your changes](#unit-test-your-changes).
-   These tests should fail without your changes.
-6. Add your changes. Your added unit tests now should pass, and no other tests
-   should be broken. Check that your changes follow the same
-   [coding style](#coding-style) as the rest of the project.
-7. Add a changelog entry.
-8. [Commit](#git-commits) and push your changes.
-9. [Create a pull request](https://help.github.com/articles/about-pull-requests/)
-   for your changes. Check that the Travis build is green. (If it is not, fix the
-   problems listed by Travis.)
-10. [Request a review](https://help.github.com/articles/about-pull-request-reviews/)
-    from @oliverklee.
-11. Together with him, polish your changes until they are ready to be merged.
-
-
-## About code reviews
-
-After you have submitted a pull request, the Emogrifier team will review your
-changes. This will probably result in quite a few comments on ways to improve
-your pull request. The Emogrifier project receives contributions from
-developers around the world, so we need the code to be the most consistent,
-readable, and maintainable that it can be.
-
-Please do not feel frustrated by this - instead please view this both as our
-contribution to your pull request as well as a way to learn more about
-improving code quality.
-
-If you would like to know whether an idea would fit in the general strategy of
-the Emogrifier project or would like to get feedback on the best architecture
-for your ideas, we propose you open a ticket first and discuss your ideas there
-first before investing a lot of time in writing code.
-
-
-## Install the development dependencies
-
-To install the development dependencies (PHPUnit and PHP_CodeSniffer), please
-run the following commands:
-
-```shell
-composer install
-composer require --dev slevomat/coding-standard:^4.0
-```
-
-Note that the development dependencies (in particular, for PHP_CodeSniffer)
-require PHP 7.0 or later.  The second command installs the PHP_CodeSniffer
-dependencies and should be omitted if specifically testing against an earlier
-version of PHP, however you will not be able to run the static code analysis.
-
-
-## Unit-test your changes
-
-Please cover all changes with unit tests and make sure that your code does not
-break any existing tests. We will only merge pull requests that include full
-code coverage of the fixed bugs and the new features.
-
-To run the existing PHPUnit tests, run this command:
-
-```shell
-composer ci:tests:unit
-```
-
-
-## Coding Style
-
-Please use the same coding style (PSR-2) as the rest of the code. Indentation
-is four spaces.
-
-We will only merge pull requests that follow the project's coding style.
-
-Please check your code with the provided static code analysis tools:
-
-```shell
-composer ci:static
-```
-
-Please make your code clean, well-readable and easy to understand.
-
-If you add new methods or fields, please add proper PHPDoc for the new
-methods/fields. Please use grammatically correct, complete sentences in the
-code documentation.
-
-You can autoformat your code using the following command:
-
-```shell
-composer php:fix
-```
-
-
-## Git commits
-
-Commit message should have a <= 50 character summary, optionally followed by a
-blank line and a more in depth description of 79 characters per line.
-
-Please use grammatically correct, complete sentences in the commit messages.
-
-Also, please prefix the subject line of the commit message with either
-[FEATURE], [TASK], [BUGFIX] OR [CLEANUP]. This makes it faster to see what
-a commit is about.
-
-
-## Creating pull requests (PRs)
-
-When you create a pull request, please
-[make your PR editable](https://github.com/blog/2247-improving-collaboration-with-forks).
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/.gitignore b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/.gitignore
deleted file mode 100644 (file)
index 8bfbb85..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-#########################
-# global ignore file
-########################
-# ignoring temporary files (left by e.g. vim)
-# ignoring by common IDE's used directories/files
-# dont ignore .rej and .orig as we want to see/clean files after conflict resolution
-#
-# for local exclude patterns please edit .git/info/exclude
-#
-*~
-*.bak
-*.idea
-*.project
-*.swp
-.buildpath
-.cache
-.project
-.session
-.settings
-.TemporaryItems
-.webprj
-nbproject
-/.php_cs.cache
-/vendor/
-composer.lock
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/.travis.yml b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/.travis.yml
deleted file mode 100644 (file)
index f3a6c22..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-sudo: false
-
-language: php
-
-php:
-- 5.5
-- 5.6
-- 7.0
-- 7.1
-- 7.2
-- 7.3
-
-cache:
-  directories:
-  - vendor
-  - $HOME/.composer/cache
-
-env:
-  matrix:
-  - DEPENDENCIES_PREFERENCE="--prefer-lowest"
-  - DEPENDENCIES_PREFERENCE=""
-
-before_install:
-- phpenv config-rm xdebug.ini || echo "xdebug not available"
-
-install:
-- >
-  export IGNORE_PLATFORM_REQS="$(composer php:version |grep -q '^7.3' && printf -- --ignore-platform-reqs)";
-  echo;
-  echo "Updating the dependencies";
-  composer update $IGNORE_PLATFORM_REQS --with-dependencies $DEPENDENCIES_PREFERENCE;
-  composer show;
-
-script:
-- >
-  echo;
-  echo "Validating the composer.json";
-  composer validate --no-check-all --no-check-lock --strict;
-
-- >
-  echo;
-  echo "Linting all PHP files";
-  composer ci:php:lint;
-
-- >
-  echo;
-  echo "Running the unit tests";
-  composer ci:tests:unit;
-
-- >
-  echo;
-  echo "Running PHPMD";
-  composer ci:php:md;
-
-- >
-  echo;
-  function version_gte() { test "$(printf '%s\n' "$@" | sort -n -t. -r | head -n 1)" = "$1"; };
-  if version_gte $(composer php:version) 7; then
-    echo "Installing slevomat/coding-standard only for PHP 7.x";
-    composer require $IGNORE_PLATFORM_REQS --dev slevomat/coding-standard:^4.0 $DEPENDENCIES_PREFERENCE;
-    echo "Running PHP_CodeSniffer";
-    composer ci:php:sniff;
-  else
-    echo "Skipped PHP_CodeSniffer due to insufficient PHP version: $(composer php:version)";
-  fi;
index a3866a4bbdaec13baedb59fd538d423eb8b04390..37e336020b1ffdf4a109ea5dd158db45b10f79c5 100644 (file)
@@ -10,11 +10,278 @@ This project adheres to [Semantic Versioning](https://semver.org/).
 ### Changed
 
 ### Deprecated
+- Support for PHP 7.1 will be removed in Emogrifier 6.0.
 
 ### Removed
 
 ### Fixed
 
+## 4.0.0
+
+### Added
+- Extract and inject `@font-face` rules into head
+  ([#870](https://github.com/MyIntervals/emogrifier/pull/870))
+- Test tag omission in conformant supplied HTML
+  ([#868](https://github.com/MyIntervals/emogrifier/pull/868))
+- Check for missing return type hint annotations in the code sniffs
+  ([#860](https://github.com/MyIntervals/emogrifier/pull/860))
+- Support `:only-of-type` (with a type)
+  ([#849](https://github.com/MyIntervals/emogrifier/issues/849),
+  [#856](https://github.com/MyIntervals/emogrifier/pull/856))
+- Configuration setting methods now all return `$this` to allow chaining
+  ([#824](https://github.com/MyIntervals/emogrifier/pull/824),
+  [#854](https://github.com/MyIntervals/emogrifier/pull/854))
+- Disable php-cs-fixer Yoda conditions
+  ([#791](https://github.com/MyIntervals/emogrifier/issues/791),
+  [#794](https://github.com/MyIntervals/emogrifier/pull/794))
+- Check the code with psalm
+  ([#537](https://github.com/MyIntervals/emogrifier/issues/537),
+  [#779](https://github.com/MyIntervals/emogrifier/pull/779))
+- Composer script to run tests with `--stop-on-failure`
+  ([#782](https://github.com/MyIntervals/emogrifier/pull/782))
+- Test universal selector with combinators
+  ([#776](https://github.com/MyIntervals/emogrifier/pull/776))
+
+### Changed
+- Normalize DOCTYPE declaration according to polyglot markup recommendation
+  ([#866](https://github.com/MyIntervals/emogrifier/pull/866))
+- Upgrade to V2 of the PHP setup GitHub action
+  ([#861](https://github.com/MyIntervals/emogrifier/pull/861))
+- Move the development tools to Phive
+  ([#850](https://github.com/MyIntervals/emogrifier/pull/850),
+  [#851](https://github.com/MyIntervals/emogrifier/pull/851))
+- Switch the parallel linting to a maintained fork
+  ([#842](https://github.com/MyIntervals/emogrifier/pull/842))
+- Move continuous integration from Travis CI to GitHub actions
+  ([#832](https://github.com/MyIntervals/emogrifier/pull/832),
+  [#834](https://github.com/MyIntervals/emogrifier/pull/834),
+  [#838](https://github.com/MyIntervals/emogrifier/pull/838),
+  [#839](https://github.com/MyIntervals/emogrifier/pull/839),
+  [#840](https://github.com/MyIntervals/emogrifier/pull/840),
+  [#841](https://github.com/MyIntervals/emogrifier/pull/841),
+  [#843](https://github.com/MyIntervals/emogrifier/pull/843),
+  [#846](https://github.com/MyIntervals/emogrifier/pull/846),
+  [#849](https://github.com/MyIntervals/emogrifier/pull/849))
+- Clean up the folder structure and autoloading configuration
+  ([#529](https://github.com/MyIntervals/emogrifier/issues/529),
+  [#785](https://github.com/MyIntervals/emogrifier/pull/785))
+- Use `self` as the return type for `fromHtml`
+  ([#784](https://github.com/MyIntervals/emogrifier/pull/784))
+- Make use of PHP 7.0 language features
+  ([#777](https://github.com/MyIntervals/emogrifier/pull/777))
+
+### Deprecated
+- Support for PHP 7.0 will be removed in Emogrifier 5.0.
+
+### Removed
+- Drop support for Symfony versions that have reached their end of life
+  ([#847](https://github.com/MyIntervals/emogrifier/pull/847))
+- Drop the `Emogrifier` class
+  ([#774](https://github.com/MyIntervals/emogrifier/pull/774))
+- Drop support for PHP 5.6
+  ([#773](https://github.com/MyIntervals/emogrifier/pull/773))
+
+### Fixed
+- Allow `:last-of-type` etc. without type, without causing exception
+  ([#875](https://github.com/MyIntervals/emogrifier/pull/875))
+- Make sure to use the Composer-installed development tools
+  ([#862](https://github.com/MyIntervals/emogrifier/pull/862),
+  [#865](https://github.com/MyIntervals/emogrifier/pull/865))
+- Add missing `<head>` element when there's a `<header>` element
+  ([#844](https://github.com/MyIntervals/emogrifier/pull/844),
+  [#853](https://github.com/MyIntervals/emogrifier/pull/853))
+- Fix mapping width/height when decimal is used
+  ([#845](https://github.com/MyIntervals/emogrifier/pull/845))
+- Actually use the specified PHP version on GitHub actions
+  ([#836](https://github.com/MyIntervals/emogrifier/pull/836))
+- Support `ci:php:lint` on Windows
+  ([#740](https://github.com/MyIntervals/emogrifier/issues/740),
+  [#780](https://github.com/MyIntervals/emogrifier/pull/780))
+
+## 3.1.0
+
+### Added
+- Add support for PHP 7.4
+  ([#821](https://github.com/MyIntervals/emogrifier/pull/821),
+  [#829](https://github.com/MyIntervals/emogrifier/pull/829))
+
+### Changed
+- Upgrade to Symfony 5.0
+  ([#820](https://github.com/MyIntervals/emogrifier/pull/820))
+
+## 3.0.0
+
+### Added
+- Test and document excluding entire subtree with `addExcludedSelector()`
+  ([#347](https://github.com/MyIntervals/emogrifier/issues/347),
+  [#768](https://github.com/MyIntervals/emogrifier/pull/768))
+- Test that rules with `:optional` or `:required` are copied to the `<style>`
+  element ([#748](https://github.com/MyIntervals/emogrifier/issues/748),
+  [#765](https://github.com/MyIntervals/emogrifier/pull/765))
+- Test that rules with `:only-of-type` are copied to the `<style>` element
+  ([#748](https://github.com/MyIntervals/emogrifier/issues/748),
+  [#760](https://github.com/MyIntervals/emogrifier/pull/760))
+- Support `:last-of-type`
+  ([#748](https://github.com/MyIntervals/emogrifier/issues/748),
+  [#758](https://github.com/MyIntervals/emogrifier/pull/758))
+- Support `:first-of-type`
+  ([#748](https://github.com/MyIntervals/emogrifier/issues/748),
+  [#757](https://github.com/MyIntervals/emogrifier/pull/757))
+- Support `:empty`
+  ([#748](https://github.com/MyIntervals/emogrifier/issues/748),
+  [#756](https://github.com/MyIntervals/emogrifier/pull/756))
+- Test that rules with `:any-link` are copied to the `<style>` element
+  ([#748](https://github.com/MyIntervals/emogrifier/issues/748),
+  [#755](https://github.com/MyIntervals/emogrifier/pull/755))
+- Support and test `:only-child`
+  ([#747](https://github.com/MyIntervals/emogrifier/issues/747),
+  [#754](https://github.com/MyIntervals/emogrifier/pull/754))
+- Support and test `:nth-last-of-type`
+  ([#747](https://github.com/MyIntervals/emogrifier/issues/747),
+  [#751](https://github.com/MyIntervals/emogrifier/pull/751))
+- Support and test `:nth-last-child`
+  ([#747](https://github.com/MyIntervals/emogrifier/issues/747),
+  [#750](https://github.com/MyIntervals/emogrifier/pull/750))
+- Support and test general sibling combinator
+  ([#723](https://github.com/MyIntervals/emogrifier/issues/723),
+  [#745](https://github.com/MyIntervals/emogrifier/pull/745))
+- Test universal selector with combinators
+  ([#723](https://github.com/MyIntervals/emogrifier/issues/723),
+  [#743](https://github.com/MyIntervals/emogrifier/pull/743))
+- Preserve `display: none` elements with `-emogrifier-keep` class
+  ([#252](https://github.com/MyIntervals/emogrifier/issues/252),
+  [#737](https://github.com/MyIntervals/emogrifier/pull/737))
+- Preserve valid `@import` rules
+  ([#338](https://github.com/MyIntervals/emogrifier/issues/338),
+  [#334](https://github.com/MyIntervals/emogrifier/pull/334),
+  [#732](https://github.com/MyIntervals/emogrifier/pull/732),
+  [#735](https://github.com/MyIntervals/emogrifier/pull/735))
+- Add `HtmlPruner::removeRedundantClassesAfterCssInlined`
+  ([#380](https://github.com/MyIntervals/emogrifier/issues/380),
+  [#724](https://github.com/MyIntervals/emogrifier/pull/724))
+- Check on Travis that PHP-CS-Fixer will not change anything
+  ([#727](https://github.com/MyIntervals/emogrifier/pull/727))
+- Support `:not(…)` as an entire selector
+  ([#469](https://github.com/MyIntervals/emogrifier/issues/469),
+  [#725](https://github.com/MyIntervals/emogrifier/pull/725))
+- Add `HtmlPruner::removeRedundantClasses`
+  ([#380](https://github.com/MyIntervals/emogrifier/issues/380),
+  [#708](https://github.com/MyIntervals/emogrifier/pull/708))
+- Support multiple attributes selectors
+  ([#385](https://github.com/MyIntervals/emogrifier/issues/385),
+  [#721](https://github.com/MyIntervals/emogrifier/pull/721))
+- Support `> :first-child` and `> :last-child` in selectors
+  ([#384](https://github.com/MyIntervals/emogrifier/issues/384),
+  [#720](https://github.com/MyIntervals/emogrifier/pull/720))
+- Add an `ArrayIntersector` class
+  ([#708](https://github.com/MyIntervals/emogrifier/pull/708),
+  [#710](https://github.com/MyIntervals/emogrifier/pull/710))
+- Add `CssInliner::getMatchingUninlinableSelectors`
+  ([#380](https://github.com/MyIntervals/emogrifier/issues/380),
+  [#707](https://github.com/MyIntervals/emogrifier/pull/707))
+- Add tests for `:nth-child` and `:nth-of-type`
+  ([#71](https://github.com/MyIntervals/emogrifier/issues/71),
+  [#698](https://github.com/MyIntervals/emogrifier/pull/698))
+
+### Changed
+- Relax the dependency on `symfony/css-selector`
+  ([#762](https://github.com/MyIntervals/emogrifier/pull/762))
+- Rename `HtmlPruner::removeInvisibleNodes` to
+  `HtmlPruner::removeElementsWithDisplayNone`
+  ([#717](https://github.com/MyIntervals/emogrifier/issues/717),
+  [#718](https://github.com/MyIntervals/emogrifier/pull/718))
+- Mark the utility classes as internal
+  ([#715](https://github.com/MyIntervals/emogrifier/pull/715))
+- Move utility classes to the `Pelago\Emogrifier\Utilities` namespace
+  ([#712](https://github.com/MyIntervals/emogrifier/pull/712))
+- Make the `$css` parameter of the `inlineCss` method optional
+  ([#700](https://github.com/MyIntervals/emogrifier/pull/700))
+- Update the development dependencies
+  ([#691](https://github.com/MyIntervals/emogrifier/pull/691))
+
+### Deprecated
+- Support for PHP 5.6 will be removed in Emogrifier 4.0.
+- Deprecate the `Emogrifier` class
+  ([#701](https://github.com/MyIntervals/emogrifier/pull/701))
+
+### Removed
+- Drop `enableCssToHtmlMapping` and `disableInvisibleNodeRemoval`
+  ([#692](https://github.com/MyIntervals/emogrifier/pull/692))
+- Drop support for PHP 5.5
+  ([#690](https://github.com/MyIntervals/emogrifier/pull/690))
+
+### Fixed
+- Fix PhpStorm code inspection warnings
+  ([#729](https://github.com/MyIntervals/emogrifier/issues/729),
+  [#770](https://github.com/MyIntervals/emogrifier/pull/770))
+- Uppercase type combined with class or ID in selector
+  ([#590](https://github.com/MyIntervals/emogrifier/issues/590),
+  [#769](https://github.com/MyIntervals/emogrifier/pull/769))
+- Dynamic pseudo-class combined with static one (rules copied to `<style>`
+  element, [#746](https://github.com/MyIntervals/emogrifier/pull/746))
+- Descendant attribute selectors (such as `html input[disabled]`)
+  ([#375](https://github.com/MyIntervals/emogrifier/pull/375),
+  [#709](https://github.com/MyIntervals/emogrifier/pull/709))
+- Attribute selectors with hyphen in attribute name
+  ([#284](https://github.com/MyIntervals/emogrifier/issues/284),
+  [#540](https://github.com/MyIntervals/emogrifier/pull/540),
+  [#704](https://github.com/MyIntervals/emogrifier/pull/702))
+- Attribute selectors with space, hyphen, colon, semicolon or (most) other
+  non-alphanumeric characters in attribute value
+  ([#284](https://github.com/MyIntervals/emogrifier/issues/284),
+  [#333](https://github.com/MyIntervals/emogrifier/issues/333),
+  [#550](https://github.com/MyIntervals/emogrifier/issues/550),
+  [#540](https://github.com/MyIntervals/emogrifier/pull/540),
+  [#704](https://github.com/MyIntervals/emogrifier/pull/702))
+- Don’t create empty `style` attributes for unparsable declarations
+  ([#259](https://github.com/MyIntervals/emogrifier/issues/259),
+  [#702](https://github.com/MyIntervals/emogrifier/pull/702))
+- Allow `:not(:behavioural-pseudo-class)` in selectors
+  ([#697](https://github.com/MyIntervals/emogrifier/pull/697),
+  [#703](https://github.com/MyIntervals/emogrifier/pull/703))
+
+## 2.2.0
+
+### Added
+- Add a `HtmlPruner` class
+  ([#679](https://github.com/MyIntervals/emogrifier/pull/679))
+- Add `AbstractHtmlProcessor::fromDomDocument`
+  ([#676](https://github.com/MyIntervals/emogrifier/pull/676))
+- Add `AbstractHtmlProcessor::fromHtml`
+  ([#675](https://github.com/MyIntervals/emogrifier/pull/675))
+
+### Changed
+- Make the closures static
+  ([#674](https://github.com/MyIntervals/emogrifier/pull/674))
+- Keep `<wbr>` elements by default with `CssInliner`
+  ([#665](https://github.com/MyIntervals/emogrifier/pull/665))
+- Make the `CssInliner` inherit `AbstractHtmlProcessor`
+  ([#660](https://github.com/MyIntervals/emogrifier/pull/660))
+- Separate `CssInliner::inlineCss` and the rendering
+  ([#654](https://github.com/MyIntervals/emogrifier/pull/654))
+
+### Removed
+- Drop the removal of unprocessable tags from `CssInliner`
+  ([#685](https://github.com/MyIntervals/emogrifier/pull/685))
+- Drop the removal of invisible nodes from `CssInliner`
+  ([#684](https://github.com/MyIntervals/emogrifier/pull/684))
+
+### Fixed
+- Remove opening `<body>` tag from `body` content when element has attribute(s)
+  ([#677](https://github.com/MyIntervals/emogrifier/issues/677),
+  [#683](https://github.com/MyIntervals/emogrifier/pull/683))
+- Keep development files out of the Composer packages
+  ([#678](https://github.com/MyIntervals/emogrifier/pull/678))
+- Call all static methods statically in `CssConcatenator`
+  ([#670](https://github.com/MyIntervals/emogrifier/pull/670))
+- Support all HTML5 self-closing tags, including `<embed>`, `<source>`,
+  `<track>` and `<wbr>`
+  ([#653](https://github.com/MyIntervals/emogrifier/pull/653))
+- Remove all "unprocessable" (e.g. `<wbr>`) tags
+  ([#650](https://github.com/MyIntervals/emogrifier/pull/650))
+- Correct translated xpath of `:nth-child` selector
+  ([#648](https://github.com/MyIntervals/emogrifier/pull/648))
+
 ## 2.1.1
 
 ### Changed
@@ -36,7 +303,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
   ([#633](https://github.com/MyIntervals/emogrifier/pull/633))
 - Add a `getDomDocument()` method
   ([#630](https://github.com/MyIntervals/emogrifier/pull/630))
-- Add a Composer script for PHP CS Fixer 
+- Add a Composer script for PHP CS Fixer
   ([#607](https://github.com/MyIntervals/emogrifier/pull/607))
 - Copy matching rules with dynamic pseudo-classes or pseudo-elements in
   selectors to the style element
@@ -44,18 +311,18 @@ This project adheres to [Semantic Versioning](https://semver.org/).
   [#562](https://github.com/MyIntervals/emogrifier/pull/562),
   [#567](https://github.com/MyIntervals/emogrifier/pull/567))
 - Add a CssToAttributeConverter
-  ([#546](https://github.com/jjriv/emogrifier/pull/546))
+  ([#546](https://github.com/MyIntervals/emogrifier/pull/546))
 - Expose the DOMDocument in AbstractHtmlProcessor
-  ([#520](https://github.com/jjriv/emogrifier/pull/520))
+  ([#520](https://github.com/MyIntervals/emogrifier/pull/520))
 - Add an HtmlNormalizer class
-  ([#513](https://github.com/jjriv/emogrifier/pull/513),
-  [#516](https://github.com/jjriv/emogrifier/pull/516))
+  ([#513](https://github.com/MyIntervals/emogrifier/pull/513),
+  [#516](https://github.com/MyIntervals/emogrifier/pull/516))
 - Add a CssInliner class
-  ([#514](https://github.com/jjriv/emogrifier/pull/514),
-  [#522](https://github.com/jjriv/emogrifier/pull/522))
+  ([#514](https://github.com/MyIntervals/emogrifier/pull/514),
+  [#522](https://github.com/MyIntervals/emogrifier/pull/522))
 - Composer scripts for the various CI build steps
 - Validate the composer.json on Travis
-  ([#476](https://github.com/jjriv/emogrifier/pull/476))
+  ([#476](https://github.com/MyIntervals/emogrifier/pull/476))
 
 ### Changed
 - Mark the work-in-progress classes as `@internal`
@@ -72,20 +339,20 @@ This project adheres to [Semantic Versioning](https://semver.org/).
 - Add type hint checking to the code sniffs
   ([#566](https://github.com/MyIntervals/emogrifier/pull/566))
 - Check the code with PHPMD
-  ([#561](https://github.com/jjriv/emogrifier/pull/561))
+  ([#561](https://github.com/MyIntervals/emogrifier/pull/561))
 - Add the cyclomatic complexity to the checked code sniffs
-  ([#558](https://github.com/jjriv/emogrifier/pull/558))
+  ([#558](https://github.com/MyIntervals/emogrifier/pull/558))
 - Use the Symfony CSS selector component
-  ([#540](https://github.com/jjriv/emogrifier/pull/540))
+  ([#540](https://github.com/MyIntervals/emogrifier/pull/540))
 
 ### Deprecated
 - Support for PHP 5.5 will be removed in Emogrifier 3.0.
 - Support for PHP 5.6 will be removed in Emogrifier 4.0.
 - The removal of invisible nodes will be removed in Emogrifier 3.0.
-  ([#473](https://github.com/jjriv/emogrifier/pull/473))
+  ([#473](https://github.com/MyIntervals/emogrifier/pull/473))
 - Converting CSS styles to (non-CSS) HTML attributes will be removed
   in Emogrifier 3.0. Please use the new CssToAttributeConverter instead.
-  ([#474](https://github.com/jjriv/emogrifier/pull/474))
+  ([#474](https://github.com/MyIntervals/emogrifier/pull/474))
 - Emogrifier 3.x.y will be the last release that supports usage without
   Composer (i.e., you can still require the class file).
   Starting with version 4.0, Emogrifier will only work with Composer.
@@ -124,7 +391,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
   ignored, [#507](https://github.com/MyIntervals/emogrifier/pull/507))
 - Allow attribute selectors in descendants
   ([#506](https://github.com/MyIntervals/emogrifier/pull/506),
-  [#381](https://github.com/MyIntervals/emogrifier/issues/381))
+  [#381](https://github.com/MyIntervals/emogrifier/issues/381),
+  [#443](https://github.com/MyIntervals/emogrifier/issues/443))
 - Allow adjacent sibling CSS selector combinator in minified CSS
   ([#505](https://github.com/MyIntervals/emogrifier/pull/505))
 - Allow CSS property values containing newlines
@@ -134,7 +402,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
 
 ### Added
 - Support for CSS :not() selector
-  ([#431](https://github.com/jjriv/emogrifier/pull/431))
+  ([#431](https://github.com/MyIntervals/emogrifier/pull/431))
 - Automatically remove !important annotations from final inline style declarations
   ([#420](https://github.com/MyIntervals/emogrifier/pull/420))
 - Automatically move `<style>` block from `<head>` to `<body>`
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/CODE_OF_CONDUCT.md b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/CODE_OF_CONDUCT.md
deleted file mode 100644 (file)
index c7bc5d9..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-# Contributor Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age,
-body size, disability, ethnicity, gender identity and expression, level of
-experience, nationality, personal appearance, race, religion, or sexual
-identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or
-  advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
-  address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an
-appointed representative at an online or offline event. Representation of a
-project may be further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at (emogrifier at myintervals dot com).
-All complaints will be reviewed and investigated and will result in a response
-that is deemed necessary and appropriate to the circumstances. The project team
-is obligated to maintain confidentiality with regard to the reporter of an
-incident. Further details of specific enforcement policies may be posted
-separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 1.4, available at
-[http://contributor-covenant.org/version/1/4/][version].
-
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
index eee5b79c40e961bc2a6f8a427e9710bb0cb4d2c1..a9540373ef549bf87c37723cf5bf4da354fcf138 100644 (file)
@@ -1,6 +1,6 @@
 # Emogrifier
 
-[![Build Status](https://travis-ci.org/MyIntervals/emogrifier.svg?branch=master)](https://travis-ci.org/MyIntervals/emogrifier)
+[![Build Status](https://github.com/MyIntervals/emogrifier/workflows/CI/badge.svg?branch=master)](https://github.com/MyIntervals/emogrifier/actions/)
 [![Latest Stable Version](https://poser.pugx.org/pelago/emogrifier/v/stable.svg)](https://packagist.org/packages/pelago/emogrifier)
 [![Total Downloads](https://poser.pugx.org/pelago/emogrifier/downloads.svg)](https://packagist.org/packages/pelago/emogrifier)
 [![Latest Unstable Version](https://poser.pugx.org/pelago/emogrifier/v/unstable.svg)](https://packagist.org/packages/pelago/emogrifier)
@@ -30,11 +30,9 @@ into inline style attributes in your HTML code.
 - [How it works](#how-it-works)
 - [Installation](#installation)
 - [Usage](#usage)
-- [Options](#options)
-- [Installing with Composer](#installing-with-composer)
 - [Supported CSS selectors](#supported-css-selectors)
 - [Caveats](#caveats)
-- [Processing HTML](#processing-html)
+- [Steps to release a new version](#steps-to-release-a-new-version)
 - [Maintainers](#maintainers)
 
 ## How it Works
@@ -45,120 +43,244 @@ selectors.
 
 ## Installation
 
-For installing emogrifier, either add pelago/emogrifier to your
-project's composer.json, or you can use composer as below:
+For installing emogrifier, either add `pelago/emogrifier` to the `require`
+section in your project's `composer.json`, or you can use composer as below:
 
 ```bash
 composer require pelago/emogrifier
 ```
 
+See https://getcomposer.org/ for more information and documentation.
+
 ## Usage
 
-First, you provide Emogrifier with the HTML and CSS you would like to merge.
-This can happen directly during instantiation:
+### Inlining Css
+
+The most basic way to use the `CssInliner` class is to create an instance with
+the original HTML, inline the external CSS, and then get back the resulting
+HTML:
+
+```php
+use Pelago\Emogrifier\CssInliner;
+
+…
+
+$visualHtml = CssInliner::fromHtml($html)->inlineCss($css)->render();
+```
+
+If there is no external CSS file and all CSS is located within `<style>`
+elements in the HTML, you can omit the `$css` parameter:
+
+```php
+$visualHtml = CssInliner::fromHtml($html)->inlineCss()->render();
+```
+
+If you would like to get back only the content of the `<body>` element instead of
+the complete HTML document, you can use the `renderBodyContent` method instead:
+
+```php
+$bodyContent = $visualHtml = CssInliner::fromHtml($html)->inlineCss()
+  ->renderBodyContent();
+```
+
+If you would like to modify the inlining process with any of the available
+[options](#options), you will need to call the corresponding methods
+before inlining the CSS. The code then would look like this:
+
+```php
+$visualHtml = CssInliner::fromHtml($html)->disableStyleBlocksParsing()
+  ->inlineCss($css)->render();
+```
+
+There are also some other HTML-processing classes available
+(all of which are subclasses of `AbstractHtmlProcessor`) which you can use
+to further change the HTML after inlining the CSS.
+(For more details on the classes, please have a look at the sections below.)
+`CssInliner` and all HTML-processing classes can share the same `DOMDocument`
+instance to work on:
+
+```php
+use Pelago\Emogrifier\CssInliner;
+use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
+use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
+
+…
+
+$cssInliner = CssInliner::fromHtml($html)->inlineCss($css);
+$domDocument = $cssInliner->getDomDocument();
+HtmlPruner::fromDomDocument($domDocument)->removeElementsWithDisplayNone()
+  ->removeRedundantClassesAfterCssInlined($cssInliner);
+$finalHtml = CssToAttributeConverter::fromDomDocument($domDocument)
+  ->convertCssToVisualAttributes()->render();
+```
+
+### Normalizing and cleaning up HTML
+
+The `HtmlNormalizer` class normalizes the given HTML in the following ways:
+
+- add a document type (HTML5) if missing
+- disentangle incorrectly nested tags
+- add HEAD and BODY elements (if they are missing)
+- reformat the HTML
+
+The class can be used like this:
 
 ```php
-$html = '<html><h1>Hello world!</h1></html>';
-$css = 'h1 {font-size: 32px;}';
-$emogrifier = new \Pelago\Emogrifier($html, $css);
+use Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer;
+
+…
+
+$cleanHtml = HtmlNormalizer::fromHtml($rawHtml)->render();
 ```
 
-You could also use the setters for providing this data after instantiation:
+### Converting CSS styles to visual HTML attributes
+
+The `CssToAttributeConverter` converts a few style attributes values to visual
+HTML attributes. This allows to get at least a bit of visual styling for email
+clients that do not support CSS well. For example, `style="width: 100px"`
+will be converted to `width="100"`.
+
+The class can be used like this:
 
 ```php
-$emogrifier = new \Pelago\Emogrifier();
+use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
 
-$html = '<html><h1>Hello world!</h1></html>';
-$css = 'h1 {font-size: 32px;}';
+…
 
-$emogrifier->setHtml($html);
-$emogrifier->setCss($css);
+$visualHtml = CssToAttributeConverter::fromHtml($rawHtml)
+  ->convertCssToVisualAttributes()->render();
 ```
 
-After you have set the HTML and CSS, you can call the `emogrify` method to
-merge both:
+You can also have the `CssToAttributeConverter` work on a `DOMDocument`:
 
 ```php
-$mergedHtml = $emogrifier->emogrify();
+$visualHtml = CssToAttributeConverter::fromDomDocument($domDocument)
+  ->convertCssToVisualAttributes()->render();
 ```
 
-Emogrifier automatically adds a Content-Type meta tag to set the charset for
-the document (if it is not provided).
+### Removing redundant content and attributes from the HTML
+
+The `HtmlPruner` class can reduce the size of the HTML by removing elements with
+a `display: none` style declaration, and/or removing classes from `class`
+attributes that are not required.
 
-If you would like to get back only the content of the BODY element instead of
-the complete HTML document, you can use the `emogrifyBodyContent` instead:
+It can be used like this:
 
 ```php
-$bodyContent = $emogrifier->emogrifyBodyContent();
+use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
+
+…
+
+$prunedHtml = HtmlPruner::fromHtml($html)->removeElementsWithDisplayNone()
+  ->removeRedundantClasses($classesToKeep)->render();
 ```
 
-## Options
+The `removeRedundantClasses` method accepts a whitelist of names of classes that
+should be retained.  If this is a post-processing step after inlining CSS, you
+can alternatively use `removeRedundantClassesAfterCssInlined`, passing it the
+`CssInliner` instance that has inlined the CSS (and having the `HtmlPruner` work
+on the `DOMDocument`).  This will use information from the `CssInliner` to
+determine which classes are still required (namely, those used in uninlinable
+rules that have been copied to a `<style>` element):
+
+```php
+$prunedHtml = HtmlPruner::fromDomDocument($cssInliner->getDomDocument())
+  ->removeElementsWithDisplayNone()
+  ->removeRedundantClassesAfterCssInlined($cssInliner)->render();
+```
 
-There are several options that you can set on the Emogrifier object before
-calling the `emogrify` method:
+The `removeElementsWithDisplayNone` method will not remove any elements which
+have the class `-emogrifier-keep`.  So if, for example, there are elements which
+by default have `display: none` but are revealed by an `@media` rule, or which
+are intended as a preheader, you can add that class to those elements.  The
+paragraph in this HTML snippet will not be removed even though it has
+`display: none` (which has presumably been applied by `CssInliner::inlineCss()`
+from a CSS rule `.preheader { display: none; }`):
+
+```html
+<p class="preheader -emogrifier-keep" style="display: none;">
+  Hello World!
+</p>
+```
 
-* `$emogrifier->disableStyleBlocksParsing()` - By default, Emogrifier will grab
+The `removeRedundantClassesAfterCssInlined` (or `removeRedundantClasses`)
+method, if invoked after `removeElementsWithDisplayNone`, will remove the
+`-emogrifier-keep` class.
+
+### Options
+
+There are several options that you can set on the `CssInliner` instance before
+calling the `inlineCss` method:
+
+* `->disableStyleBlocksParsing()` - By default, `CssInliner` will grab
   all `<style>` blocks in the HTML and will apply the CSS styles as inline
   "style" attributes to the HTML. The `<style>` blocks will then be removed
-  from the HTML. If you want to disable this functionality so that Emogrifier
+  from the HTML. If you want to disable this functionality so that `CssInliner`
   leaves these `<style>` blocks in the HTML and does not parse them, you should
   use this option. If you use this option, the contents of the `<style>` blocks
-  will _not_ be applied as inline styles and any CSS you want Emogrifier to
+  will _not_ be applied as inline styles and any CSS you want `CssInliner` to
   use must be passed in as described in the [Usage section](#usage) above.
-* `$emogrifier->disableInlineStylesParsing()` - By default, Emogrifier
+* `->disableInlineStyleAttributesParsing()` - By default, `CssInliner`
   preserves all of the "style" attributes on tags in the HTML you pass to it.
   However if you want to discard all existing inline styles in the HTML before
   the CSS is applied, you should use this option.
-* `$emogrifier->disableInvisibleNodeRemoval()` - By default, Emogrifier removes
-  elements from the DOM that have the style attribute `display: none;`.  If
-  you would like to keep invisible elements in the DOM, use this option.
-  Note: This option will be removed in Emogrifier 3.0. HTML tags with
-  `display: none;` then will always be retained.
-* `$emogrifier->addAllowedMediaType(string $mediaName)` - By default, Emogrifier
+* `->addAllowedMediaType(string $mediaName)` - By default, `CssInliner`
   will keep only media types `all`, `screen` and `print`. If you want to keep
   some others, you can use this method to define them.
-* `$emogrifier->removeAllowedMediaType(string $mediaName)` - You can use this
+* `->removeAllowedMediaType(string $mediaName)` - You can use this
   method to remove media types that Emogrifier keeps.
-* `$emogrifier->addExcludedSelector(string $selector)` - Keeps elements from
-  being affected by emogrification.
-* `$emogrifier->enableCssToHtmlMapping()` - Some email clients don't support CSS
-  well, even if inline and prefer HTML attributes. This function allows you to
-  put properties such as height, width, background color and font color in your
-  CSS while the transformed content will have all the available HTML
-  attributes set. This option will be removed in Emogrifier 3.0. Please use the
-  `CssToAttributeConverter` class instead.
+* `->addExcludedSelector(string $selector)` - Keeps elements from
+  being affected by CSS inlining.  Note that only elements matching the supplied
+  selector(s) will be excluded from CSS inlining, not necessarily their
+  descendants.  If you wish to exclude an entire subtree, you should provide
+  selector(s) which will match all elements in the subtree, for example by using
+  the universal selector:
+  ```php
+  $cssInliner->addExcludedSelector('.message-preview');
+  $cssInliner->addExcludedSelector('.message-preview *');
+  ```
 
-## Installing with Composer
+### Migrating from the dropped `Emogrifier` class to the `CssInliner` class
 
-Download the [`composer.phar`](https://getcomposer.org/composer.phar) locally
-or install [Composer](https://getcomposer.org/) globally:
+#### Minimal example
 
-```bash
-curl -s https://getcomposer.org/installer | php
+Old code using `Emogrifier`:
+
+```php
+$emogrifier = new Emogrifier($html);
+$html = $emogrifier->emogrify();
 ```
 
-Run the following command for a local installation:
+New code using `CssInliner`:
 
-```bash
-php composer.phar require pelago/emogrifier:^2.1.0
+```php
+$html = CssInliner::fromHtml($html)->inlineCss()->render();
 ```
 
-Or for a global installation, run the following command:
+NB: In this example, the old code removes elements with `display: none;`
+while the new code does not, as the default behaviors of the old and
+the new class differ in this regard.
 
-```bash
-composer require pelago/emogrifier:^2.1.0
-```
+#### More complex example
 
-You can also add follow lines to your `composer.json` and run the
-`composer update` command:
+Old code using `Emogrifier`:
 
-```json
-"require": {
-  "pelago/emogrifier": "^2.1.0"
-}
+```php
+$emogrifier = new Emogrifier($html, $css);
+$emogrifier->enableCssToHtmlMapping();
+
+$html = $emogrifier->emogrify();
 ```
 
-See https://getcomposer.org/ for more information and documentation.
+New code using `CssInliner` and family:
+
+```php
+$domDocument = CssInliner::fromHtml($html)->inlineCss($css)->getDomDocument();
+
+HtmlPruner::fromDomDocument($domDocument)->removeElementsWithDisplayNone(),
+$html = CssToAttributeConverter::fromDomDocument($domDocument)
+  ->convertCssToVisualAttributes()->render();
+```
 
 ## Supported CSS selectors
 
@@ -168,7 +290,7 @@ Emogrifier currently supports the following
  * [type](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)
  * [class](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors)
  * [ID](https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors)
- * [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors):
+ * [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors)
  * [attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors):
     * presence
     * exact value match
@@ -178,42 +300,101 @@ Emogrifier currently supports the following
     * value with `$` (suffix match)
     * value with `*` (substring match)
  * [adjacent](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_selectors)
+ * [general sibling](https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator)
  * [child](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors)
  * [descendant](https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_selectors)
  * [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
+   * [empty](https://developer.mozilla.org/en-US/docs/Web/CSS/:empty)
    * [first-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child)
+   * [first-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type)
+     (with a type, e.g. `p:first-of-type` but not `*:first-of-type` which will
+     currently be treated as `*:not(*)`)
    * [last-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child)
+   * [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
+     (with a type &ndash; without a type, it will be treated as `:not(*)`)
    * [not()](https://developer.mozilla.org/en-US/docs/Web/CSS/:not)
+   * [nth-child()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child)
+   * [nth-last-child()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child)
+   * [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
+     (with a type &ndash; without a type, it will be treated as `:not(*)`)
+   * [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
+     (with a type &ndash; without a type, it will be applied as if `:nth-child`)
+   * [only-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child)
+   * [only-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type)
+     (with a type &ndash; without a type, it will be applied as if `:only-child`
+     or `:not(*)`, depending on version constraints for `symfony/css-selector`)
 
 The following selectors are not implemented yet:
 
- * [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors)
  * [case-insensitive attribute value](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#case-insensitive)
- * [general sibling](https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_selectors)
- * [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
-   (some of them will never be supported)
+ * static [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
+   * [first-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type)
+     without a type (declarations discarded)
+   * [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
+     without a type (declarations discarded)
+   * [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
+     without a type (declarations discarded)
+   * [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
+     without a type (will behave as `:nth-child()`)
+   * [only-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type)
+     without a type (will behave as `:only-child()` or `:not(*)`)
+   * any pseudo-classes not listed above as supported – rules involving them
+     will nonetheless be preserved and copied to a `<style>` element in the 
+     HTML – including (but not necessarily limited to) the following:
+     * [any-link](https://developer.mozilla.org/en-US/docs/Web/CSS/:any-link)
+     * [optional](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional)
+     * [required](https://developer.mozilla.org/en-US/docs/Web/CSS/:required)
+     
+Rules involving the following selectors cannot be applied as inline styles.
+They will, however, be preserved and copied to a `<style>` element in the HTML:
+     
+ * dynamic [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
+   (such as `:hover`)
  * [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)
+   (such as `::after`)
 
 ## Caveats
 
 * Emogrifier requires the HTML and the CSS to be UTF-8. Encodings like
   ISO8859-1 or ISO8859-15 are not supported.
-* Emogrifier now preserves all valuable @media queries. Media queries
-  can be very useful in responsive email design. See
+* Emogrifier preserves all valuable `@media` rules.  Media queries can be very
+  useful in responsive email design.  See
   [media query support](https://litmus.com/help/email-clients/media-query-support/).
+  However, in order for them to be effective, you may need to add `!important`
+  to some of the declarations within them so that they will override CSS styles
+  that have been inlined.  For example, with the following CSS, the `font-size`
+  declaration in the `@media` rule would not override the font size for `p`
+  elements from the preceding rule after that has been inlined as 
+  `<p style="font-size: 16px;">` in the HTML, without the `!important` directive
+  (even though `!important` would not be necessary if the CSS were not inlined):
+  ```css
+  p {
+    font-size: 16px;
+  }
+  @media (max-width: 640px) {
+    p {
+      font-size: 14px !important;
+    }
+  } 
+  ```
+* Emogrifier cannot inline CSS rules involving selectors with pseudo-elements
+  (such as `::after`) or dynamic pseudo-classes (such as `:hover`) – it is
+  impossible.  However, such rules will be preserved and copied to a `<style>`
+  element, as for `@media` rules.  The same caveat about the possible need for
+  the `!important` directive also applies with pseudo-classes.
 * Emogrifier will grab existing inline style attributes _and_ will
   grab `<style>` blocks from your HTML, but it will not grab CSS files
-  referenced in <link> elements. (The problem email clients are going to ignore
-  these tags anyway, so why leave them in your HTML?)
+  referenced in `<link>` elements or `@import` rules (though it will leave them
+  intact for email clients that support them).
 * Even with styles inline, certain CSS properties are ignored by certain email
   clients. For more information, refer to these resources:
     * [http://www.email-standards.org/](http://www.email-standards.org/)
     * [https://www.campaignmonitor.com/css/](https://www.campaignmonitor.com/css/)
     * [http://templates.mailchimp.com/resources/email-client-css-support/](http://templates.mailchimp.com/resources/email-client-css-support/)
-* All CSS attributes that apply to a node will be applied, even if they are
+* All CSS attributes that apply to an element will be applied, even if they are
   redundant. For example, if you define a font attribute _and_ a font-size
-  attribute, both attributes will be applied to that node (in other words, the
-  more specific attribute will not be combined into the more general
+  attribute, both attributes will be applied to that element (in other words,
+  the more specific attribute will not be combined into the more general
   attribute).
 * There's a good chance you might encounter problems if your HTML is not
   well-formed and valid (DOMDocument might complain). If you get problems like
@@ -223,65 +404,18 @@ The following selectors are not implemented yet:
 * Emogrifier automatically converts the provided (X)HTML into HTML5, i.e.,
   self-closing tags will lose their slash. To keep your HTML valid, it is
   recommended to use HTML5 instead of one of the XHTML variants.
-* Emogrifier only supports CSS1 level selectors and a few CSS2 level selectors
-  (but not all of them). It does not support pseudo selectors. (Emogrifier
-  works by converting CSS selectors to XPath selectors, and pseudo selectors
-  cannot be converted accurately).
-
-## Processing HTML
-
-The Emogrifier package also provides classes for (post-)processing the HTML
-generated by `emogrify` (and it also works on any other HTML).
-
-### Normalizing and cleaning up HTML
-
-The `HtmlNormalizer` class normalizes the given HTML in the following ways:
-
-- add a document type (HTML5) if missing
-- disentangle incorrectly nested tags
-- add HEAD and BODY elements (if they are missing)
-- reformat the HTML
-
-The class can be used like this:
-
-```php
-$normalizer = new \Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer($rawHtml);
-$cleanHtml = $normalizer->render();
-```
-
-### Converting CSS styles to visual HTML attributes
-
-The `CssToAttributeConverter` converts a few style attributes values to visual
-HTML attributes. This allows to get at least a bit of visual styling for email
-clients that do not support CSS well. For example, `style="width: 100px"`
-will be converted to `width="100"`.
-
-The class can be used like this:
-
-```php
-$converter = new \Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter($rawHtml);
-$visualHtml = $converter->convertCssToVisualAttributes()->render();
-```
-
-### Technology preview of new classes
-
-Currently, a refactoring effort is underway, aiming towards replacing the
-grown-over-time `Emogrifier` class with the new `CssInliner` class and moving
-additional HTML processing into separate `CssProcessor` classes (which will
-inherit from `AbstractHtmlProcessor`). You can try the new classes, but be
-aware that the APIs of the new classes still are subject to change. 
 
 ## Steps to release a new version
 
-1. Create a pull request "Prepare release of version x.y.z" with the following
-   changes.
 1. In the [composer.json](composer.json), update the `branch-alias` entry to
    point to the release _after_ the upcoming release.
-1. In the [README.md](README.md), update the version numbers in the section
-   [Installing with Composer](#installing-with-composer).
-1. In the [CHANGELOG.md](CHANGELOG.md), set the version number and remove any
-   empty sections.
+1. In the [CHANGELOG.md](CHANGELOG.md), create a new section with subheadings
+   for changes _after_ the upcoming release, set the version number for the
+   upcoming release, and remove any empty sections.
+1. Create a pull request "Prepare release of version x.y.z" with those
+   changes.
 1. Have the pull request reviewed and merged.
+1. Tag the new release.
 1. In the [Releases tab](https://github.com/MyIntervals/emogrifier/releases),
    create a new release and copy the change log entries to the new release.
 1. Post about the new release on social media.
index b2949f9bbd2f1ef09b7141183bc8f9d37a9f96b7..e1161abedebd46c68965fcbc80d69b13d7b78e4c 100644 (file)
         "source": "https://github.com/MyIntervals/emogrifier"
     },
     "require": {
-        "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
+        "php": "~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4",
         "ext-dom": "*",
         "ext-libxml": "*",
-        "symfony/css-selector": "^3.4.0 || ^4.0.0"
+        "symfony/css-selector": "^3.4.32 || ^4.3.5 || ^5.0"
     },
     "require-dev": {
-        "friendsofphp/php-cs-fixer": "^2.2.0",
-        "squizlabs/php_codesniffer": "^3.3.2",
-        "phpmd/phpmd": "^2.6.0",
-        "phpunit/phpunit": "^4.8.0"
+        "grogy/php-parallel-lint": "^1.1.0",
+        "phpunit/phpunit": "^6.5.14",
+        "psalm/plugin-phpunit": "^0.5.8",
+        "slevomat/coding-standard": "^4.0.0",
+        "squizlabs/php_codesniffer": "^3.5.1",
+        "vimeo/psalm": "^3.2.12"
     },
     "autoload": {
         "psr-4": {
-            "Pelago\\": "src/"
+            "Pelago\\Emogrifier\\": "src/"
         }
     },
     "autoload-dev": {
         "psr-4": {
-            "Pelago\\Tests\\": "tests/"
+            "Pelago\\Emogrifier\\Tests\\": "tests/"
         }
     },
     "prefer-stable": true,
     "config": {
+        "sort-packages": true,
         "preferred-install": {
             "*": "dist"
         }
     },
     "scripts": {
         "php:version": "php -v | grep -Po 'PHP\\s++\\K(?:\\d++\\.)*+\\d++(?:-\\w++)?+'",
-        "php:fix": "php-cs-fixer --config=config/php-cs-fixer.php fix config/ src/ tests/",
-        "ci:php:lint": "find config src tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l",
-        "ci:php:sniff": "phpcs config src tests",
-        "ci:php:md": "phpmd src text config/phpmd.xml",
-        "ci:tests:unit": "phpunit tests/",
+        "php:fix": "\"./tools/php-cs-fixer\" --config=config/php-cs-fixer.php fix config/ src/ tests/",
+        "ci:php:lint": "\"vendor/bin/parallel-lint\" config src tests",
+        "ci:php:sniff": "\"vendor/bin/phpcs\" config src tests",
+        "ci:php:fixer": "\"./tools/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff-format=udiff config/ src/ tests/",
+        "ci:php:md": "\"./tools/phpmd\" src text config/phpmd.xml",
+        "ci:php:psalm": "\"vendor/bin/psalm\" --show-info=false",
+        "ci:tests:unit": "\"vendor/bin/phpunit\" tests/",
+        "ci:tests:sof": "\"vendor/bin/phpunit\" tests/ --stop-on-failure",
         "ci:tests": [
             "@ci:tests:unit"
         ],
@@ -80,7 +86,9 @@
         "ci:static": [
             "@ci:php:lint",
             "@ci:php:sniff",
-            "@ci:php:md"
+            "@ci:php:fixer",
+            "@ci:php:md",
+            "@ci:php:psalm"
         ],
         "ci": [
             "@ci:static",
@@ -89,7 +97,7 @@
     },
     "extra": {
         "branch-alias": {
-            "dev-master": "2.1.x-dev"
+            "dev-master": "5.0.x-dev"
         }
     }
 }
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/config/php-cs-fixer.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/config/php-cs-fixer.php
deleted file mode 100644 (file)
index 4e379ee..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-if (PHP_SAPI !== 'cli') {
-    die('This script supports command line usage only. Please check your command.');
-}
-
-return \PhpCsFixer\Config::create()
-    ->setRiskyAllowed(true)
-    ->setRules(
-        [
-            // copied from the TYPO3 Core
-            '@PSR2' => true,
-            '@DoctrineAnnotation' => true,
-            'no_leading_import_slash' => true,
-            'no_trailing_comma_in_singleline_array' => true,
-            'no_singleline_whitespace_before_semicolons' => true,
-            'no_unused_imports' => true,
-            'concat_space' => ['spacing' => 'one'],
-            'no_whitespace_in_blank_line' => true,
-            'ordered_imports' => true,
-            'single_quote' => true,
-            'no_empty_statement' => true,
-            'no_extra_consecutive_blank_lines' => true,
-            'phpdoc_no_package' => true,
-            'phpdoc_scalar' => true,
-            'no_blank_lines_after_phpdoc' => true,
-            'array_syntax' => ['syntax' => 'short'],
-            'whitespace_after_comma_in_array' => true,
-            'function_typehint_space' => true,
-            'hash_to_slash_comment' => true,
-            'no_alias_functions' => true,
-            'lowercase_cast' => true,
-            'no_leading_namespace_whitespace' => true,
-            'native_function_casing' => true,
-            'no_short_bool_cast' => true,
-            'no_unneeded_control_parentheses' => true,
-            'phpdoc_trim' => true,
-            'no_superfluous_elseif' => true,
-            'no_useless_else' => true,
-            'phpdoc_types' => true,
-            'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
-            'return_type_declaration' => ['space_before' => 'none'],
-            'cast_spaces' => ['space' => 'none'],
-            'declare_equal_normalize' => ['space' => 'single'],
-            'dir_constant' => true,
-
-            // additional rules
-            'combine_consecutive_issets' => true,
-            'combine_consecutive_unsets' => true,
-            'compact_nullable_typehint' => true,
-            // PHP >= 7.0
-            // 'declare_strict_types' => true,
-            'elseif' => true,
-            'encoding' => true,
-            'escape_implicit_backslashes' => ['single_quoted' => true],
-            'is_null' => true,
-            'linebreak_after_opening_tag' => true,
-            'magic_constant_casing' => true,
-            'method_separation' => true,
-            'modernize_types_casting' => true,
-            // not yet, but maybe later to improve performance
-            // 'native_function_invocation' => true,
-            'new_with_braces' => true,
-            'no_blank_lines_after_class_opening' => true,
-            'no_empty_comment' => true,
-            'no_empty_phpdoc' => true,
-            'no_extra_blank_lines' => true,
-            'no_multiline_whitespace_before_semicolons' => true,
-            'no_php4_constructor' => true,
-            'no_short_echo_tag' => true,
-            'no_spaces_after_function_name' => true,
-            'no_spaces_inside_parenthesis' => true,
-            'no_unneeded_curly_braces' => true,
-            'no_useless_return' => true,
-            'no_whitespace_before_comma_in_array' => true,
-            'php_unit_construct' => true,
-            'php_unit_fqcn_annotation' => true,
-            'php_unit_set_up_tear_down_visibility' => true,
-            'phpdoc_add_missing_param_annotation' => true,
-            'phpdoc_indent' => true,
-            'phpdoc_separation' => true,
-            'semicolon_after_instruction' => true,
-            'short_scalar_cast' => true,
-            'space_after_semicolon' => true,
-            'standardize_not_equals' => true,
-            'psr4' => true,
-            'ternary_operator_spaces' => true,
-            // PHP >= 7.0
-            // 'ternary_to_null_coalescing' => true,
-            'trailing_comma_in_multiline_array' => true,
-            'unary_operator_spaces' => true,
-        ]
-    );
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/config/phpmd.xml b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/config/phpmd.xml
deleted file mode 100644 (file)
index 569409b..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0"?>
-<ruleset name="phpList">
-    <description>
-        PHPMD rules for Emogrifier
-    </description>
-
-    <!-- The commented-out rules will be enabled once the code does not generate any warnings anymore. -->
-
-    <rule ref="rulesets/cleancode.xml/BooleanArgumentFlag"/>
-    <rule ref="rulesets/cleancode.xml/StaticAccess"/>
-
-    <rule ref="rulesets/codesize.xml/CyclomaticComplexity"/>
-    <rule ref="rulesets/codesize.xml/NPathComplexity"/>
-    <rule ref="rulesets/codesize.xml/ExcessiveMethodLength"/>
-    <!--<rule ref="rulesets/codesize.xml/ExcessiveClassLength"/>-->
-    <!--<rule ref="rulesets/codesize.xml/ExcessiveParameterList"/>-->
-    <rule ref="rulesets/codesize.xml/ExcessivePublicCount"/>
-    <!--<rule ref="rulesets/codesize.xml/TooManyFields"/>-->
-    <!--<rule ref="rulesets/codesize.xml/TooManyMethods"/>-->
-    <!--<rule ref="rulesets/codesize.xml/TooManyPublicMethods"/>-->
-    <!--<rule ref="rulesets/codesize.xml/ExcessiveClassComplexity"/>-->
-
-    <rule ref="rulesets/controversial.xml/Superglobals"/>
-    <rule ref="rulesets/controversial.xml/CamelCaseClassName"/>
-    <rule ref="rulesets/controversial.xml/CamelCasePropertyName"/>
-    <rule ref="rulesets/controversial.xml/CamelCaseMethodName"/>
-    <rule ref="rulesets/controversial.xml/CamelCaseParameterName"/>
-    <rule ref="rulesets/controversial.xml/CamelCaseVariableName"/>
-
-    <rule ref="rulesets/design.xml/ExitExpression"/>
-    <rule ref="rulesets/design.xml/EvalExpression"/>
-    <rule ref="rulesets/design.xml/GotoStatement"/>
-    <rule ref="rulesets/design.xml/NumberOfChildren"/>
-    <rule ref="rulesets/design.xml/DepthOfInheritance"/>
-    <rule ref="rulesets/design.xml/CouplingBetweenObjects"/>
-    <rule ref="rulesets/design.xml/DevelopmentCodeFragment"/>
-
-    <!--<rule ref="rulesets/naming.xml/ShortVariable"/>-->
-    <!--<rule ref="rulesets/naming.xml/LongVariable"/>-->
-    <rule ref="rulesets/naming.xml/ShortMethodName"/>
-    <rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass"/>
-    <rule ref="rulesets/naming.xml/ConstantNamingConventions"/>
-    <rule ref="rulesets/naming.xml/BooleanGetMethodName"/>
-
-    <rule ref="rulesets/unusedcode.xml/UnusedPrivateField"/>
-    <rule ref="rulesets/unusedcode.xml/UnusedLocalVariable"/>
-    <!--<rule ref="rulesets/unusedcode.xml/UnusedPrivateMethod"/>-->
-    <!--<rule ref="rulesets/unusedcode.xml/UnusedFormalParameter"/>-->
-</ruleset>
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/phpcs.xml.dist b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/phpcs.xml.dist
deleted file mode 100644 (file)
index d87f19c..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ruleset name="Coding Standard">
-    <description>
-        This standard requires PHP_CodeSniffer >= 3.2.0.
-    </description>
-
-    <config name="installed_paths" value="../../slevomat/coding-standard"/>
-
-    <!--The complete PSR-2 ruleset-->
-    <rule ref="PSR2"/>
-
-    <!-- Arrays -->
-    <rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
-    <rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
-    <rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"/>
-
-    <!-- Classes -->
-    <rule ref="Generic.Classes.DuplicateClassName"/>
-    <rule ref="Squiz.Classes.ClassFileName"/>
-    <rule ref="Squiz.Classes.DuplicateProperty"/>
-    <rule ref="Squiz.Classes.LowercaseClassKeywords"/>
-    <rule ref="Squiz.Classes.SelfMemberReference"/>
-
-    <!-- Code analysis -->
-    <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
-    <rule ref="Generic.CodeAnalysis.AssignmentInCondition"/>
-    <rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
-    <rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
-    <rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
-    <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
-    <rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
-    <rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
-    <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
-
-    <!-- Commenting -->
-    <rule ref="Generic.Commenting.Fixme"/>
-    <rule ref="Generic.Commenting.Todo"/>
-    <rule ref="PEAR.Commenting.InlineComment"/>
-    <rule ref="Squiz.Commenting.DocCommentAlignment"/>
-    <rule ref="Squiz.Commenting.EmptyCatchComment"/>
-    <rule ref="Squiz.Commenting.FunctionComment">
-        <!-- Allow PHP-5-compatible type hinting. -->
-        <exclude name="Squiz.Commenting.FunctionComment.ScalarTypeHintMissing"/>
-        <!-- Allow no comment for self-describing parameter and exception class names. -->
-        <exclude name="Squiz.Commenting.FunctionComment.MissingParamComment"/>
-        <exclude name="Squiz.Commenting.FunctionComment.EmptyThrows"/>
-        <!-- Allow "int" rather than "integer", etc., in PHPDoc. -->
-        <exclude name="Squiz.Commenting.FunctionComment.IncorrectParamVarName"/>
-        <exclude name="Squiz.Commenting.FunctionComment.InvalidReturn"/>
-        <!-- Allow "@return" to be omitted (for methods which do not return a value). -->
-        <exclude name="Squiz.Commenting.FunctionComment.MissingReturn"/>
-        <!-- Allow parameter type, name and comment not all vertically aligned. -->
-        <exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType"/>
-        <exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamName"/>
-        <!-- Allow parameter and exception descriptions which are not full sentences. -->
-        <exclude name="Squiz.Commenting.FunctionComment.ParamCommentNotCapital"/>
-        <exclude name="Squiz.Commenting.FunctionComment.ParamCommentFullStop"/>
-        <exclude name="Squiz.Commenting.FunctionComment.ThrowsNotCapital"/>
-        <exclude name="Squiz.Commenting.FunctionComment.ThrowsNoFullStop"/>
-    </rule>
-    <rule ref="Squiz.Commenting.FunctionCommentThrowTag"/>
-    <rule ref="Squiz.Commenting.PostStatementComment"/>
-
-    <!-- Control structures -->
-    <rule ref="PEAR.ControlStructures.ControlSignature"/>
-
-    <!-- Debug -->
-    <rule ref="Generic.Debug.ClosureLinter"/>
-
-    <!-- Files -->
-    <rule ref="Generic.Files.OneClassPerFile"/>
-    <rule ref="Generic.Files.OneInterfacePerFile"/>
-    <rule ref="Generic.Files.OneObjectStructurePerFile"/>
-    <rule ref="Zend.Files.ClosingTag"/>
-
-    <!-- Formatting -->
-    <rule ref="Generic.Formatting.NoSpaceAfterCast"/>
-    <rule ref="PEAR.Formatting.MultiLineAssignment"/>
-
-    <!-- Functions -->
-    <rule ref="Generic.Functions.CallTimePassByReference"/>
-    <rule ref="SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions"/>
-    <rule ref="Squiz.Functions.FunctionDuplicateArgument"/>
-    <rule ref="Squiz.Functions.GlobalFunction"/>
-
-    <!-- Metrics -->
-    <rule ref="Generic.Metrics.CyclomaticComplexity"/>
-    <rule ref="Generic.Metrics.NestingLevel"/>
-
-    <!-- Naming conventions -->
-    <rule ref="Generic.NamingConventions.ConstructorName"/>
-    <rule ref="PEAR.NamingConventions.ValidClassName"/>
-
-    <!-- Objects -->
-    <rule ref="Squiz.Objects.ObjectMemberComma"/>
-
-    <!-- Operators -->
-    <rule ref="Squiz.Operators.IncrementDecrementUsage"/>
-    <rule ref="Squiz.Operators.ValidLogicalOperators"/>
-
-    <!-- PHP -->
-    <rule ref="Generic.PHP.BacktickOperator"/>
-    <rule ref="Generic.PHP.CharacterBeforePHPOpeningTag"/>
-    <rule ref="Generic.PHP.DeprecatedFunctions"/>
-    <rule ref="Generic.PHP.DisallowAlternativePHPTags"/>
-    <rule ref="Generic.PHP.DisallowShortOpenTag"/>
-    <rule ref="Generic.PHP.DiscourageGoto"/>
-    <rule ref="Generic.PHP.ForbiddenFunctions"/>
-    <rule ref="Generic.PHP.NoSilencedErrors"/>
-    <rule ref="Squiz.PHP.CommentedOutCode">
-        <properties>
-            <property name="maxPercentage" value="70"/>
-        </properties>
-    </rule>
-    <rule ref="Squiz.PHP.DisallowMultipleAssignments"/>
-    <rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
-    <rule ref="Squiz.PHP.DiscouragedFunctions"/>
-    <rule ref="Squiz.PHP.Eval"/>
-    <rule ref="Squiz.PHP.GlobalKeyword"/>
-    <rule ref="Squiz.PHP.Heredoc"/>
-    <rule ref="Squiz.PHP.InnerFunctions"/>
-    <rule ref="Squiz.PHP.LowercasePHPFunctions"/>
-    <rule ref="Squiz.PHP.NonExecutableCode"/>
-
-    <!-- Scope -->
-    <rule ref="Squiz.Scope.MemberVarScope"/>
-    <rule ref="Squiz.Scope.StaticThisUsage"/>
-
-    <!--Strings-->
-    <rule ref="Squiz.Strings.DoubleQuoteUsage"/>
-
-    <!-- Whitespace -->
-    <rule ref="PEAR.WhiteSpace.ObjectOperatorIndent"/>
-    <rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/>
-    <rule ref="Squiz.WhiteSpace.CastSpacing"/>
-    <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
-    <rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
-    <rule ref="Squiz.WhiteSpace.PropertyLabelSpacing"/>
-    <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
-</ruleset>
\ No newline at end of file
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/CssInliner.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/CssInliner.php
new file mode 100644 (file)
index 0000000..30dd635
--- /dev/null
@@ -0,0 +1,1171 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier;
+
+use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
+use Pelago\Emogrifier\Utilities\CssConcatenator;
+use Symfony\Component\CssSelector\CssSelectorConverter;
+use Symfony\Component\CssSelector\Exception\ParseException;
+
+/**
+ * This class provides functions for converting CSS styles into inline style attributes in your HTML code.
+ *
+ * For Emogrifier 3.0.0, this will be the successor to the \Pelago\Emogrifier class (which then will be deprecated).
+ *
+ * For more information, please see the README.md file.
+ *
+ * @author Cameron Brooks
+ * @author Jaime Prado
+ * @author Oliver Klee <github@oliverklee.de>
+ * @author Roman Ožana <ozana@omdesign.cz>
+ * @author Sander Kruger <s.kruger@invessel.com>
+ * @author Zoli Szabó <zoli.szabo+github@gmail.com>
+ */
+class CssInliner extends AbstractHtmlProcessor
+{
+    /**
+     * @var int
+     */
+    const CACHE_KEY_CSS = 0;
+
+    /**
+     * @var int
+     */
+    const CACHE_KEY_SELECTOR = 1;
+
+    /**
+     * @var int
+     */
+    const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 2;
+
+    /**
+     * @var int
+     */
+    const CACHE_KEY_COMBINED_STYLES = 3;
+
+    /**
+     * Regular expression component matching a static pseudo class in a selector, without the preceding ":",
+     * for which the applicable elements can be determined (by converting the selector to an XPath expression).
+     * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead
+     * group, as appropriate for the usage context.)
+     *
+     * @var string
+     */
+    const PSEUDO_CLASS_MATCHER = 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)';
+
+    /**
+     * @var bool[]
+     */
+    private $excludedSelectors = [];
+
+    /**
+     * @var bool[]
+     */
+    private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true];
+
+    /**
+     * @var mixed[]
+     */
+    private $caches = [
+        self::CACHE_KEY_CSS => [],
+        self::CACHE_KEY_SELECTOR => [],
+        self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
+        self::CACHE_KEY_COMBINED_STYLES => [],
+    ];
+
+    /**
+     * @var CssSelectorConverter
+     */
+    private $cssSelectorConverter = null;
+
+    /**
+     * the visited nodes with the XPath paths as array keys
+     *
+     * @var \DOMElement[]
+     */
+    private $visitedNodes = [];
+
+    /**
+     * the styles to apply to the nodes with the XPath paths as array keys for the outer array
+     * and the attribute names/values as key/value pairs for the inner array
+     *
+     * @var string[][]
+     */
+    private $styleAttributesForNodes = [];
+
+    /**
+     * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
+     * If set to false, the value of the style attributes will be discarded.
+     *
+     * @var bool
+     */
+    private $isInlineStyleAttributesParsingEnabled = true;
+
+    /**
+     * Determines whether the <style> blocks in the HTML passed to this class should be parsed.
+     *
+     * If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML
+     * via inline styles.
+     *
+     * If set to false, the <style> blocks will be left as they are in the HTML.
+     *
+     * @var bool
+     */
+    private $isStyleBlocksParsingEnabled = true;
+
+    /**
+     * For calculating selector precedence order.
+     * Keys are a regular expression part to match before a CSS name.
+     * Values are a multiplier factor per match to weight specificity.
+     *
+     * @var int[]
+     */
+    private $selectorPrecedenceMatchers = [
+        // IDs: worth 10000
+        '\\#' => 10000,
+        // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100
+        '(?:\\.|\\[|(?<!:):(?!not\\())' => 100,
+        // elements (not attribute values or `:not`), pseudo-elements: worth 1
+        '(?:(?<![="\':\\w\\-])|::)' => 1,
+    ];
+
+    /**
+     * array of data describing CSS rules which apply to the document but cannot be inlined, in the format returned by
+     * `parseCssRules`
+     *
+     * @var string[][]
+     */
+    private $matchingUninlinableCssRules = null;
+
+    /**
+     * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them.
+     *
+     * @var bool
+     */
+    private $debug = false;
+
+    /**
+     * Inlines the given CSS into the existing HTML.
+     *
+     * @param string $css the CSS to inline, must be UTF-8-encoded
+     *
+     * @return self fluent interface
+     *
+     * @throws ParseException
+     */
+    public function inlineCss(string $css = ''): self
+    {
+        $this->clearAllCaches();
+        $this->purgeVisitedNodes();
+
+        $this->normalizeStyleAttributesOfAllNodes();
+
+        $combinedCss = $css;
+        // grab any existing style blocks from the HTML and append them to the existing CSS
+        // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
+        if ($this->isStyleBlocksParsingEnabled) {
+            $combinedCss .= $this->getCssFromAllStyleNodes();
+        }
+
+        $cssWithoutComments = $this->removeCssComments($combinedCss);
+        list($cssWithoutCommentsCharsetOrImport, $cssImportRules)
+            = $this->extractImportAndCharsetRules($cssWithoutComments);
+        list($cssWithoutCommentsCharsetImportOrFontFace, $cssFontFaces)
+            = $this->extractFontFaceRules($cssWithoutCommentsCharsetOrImport);
+
+        $uninlinableCss = $cssImportRules . $cssFontFaces;
+
+        $excludedNodes = $this->getNodesToExclude();
+        $cssRules = $this->parseCssRules($cssWithoutCommentsCharsetImportOrFontFace);
+        $cssSelectorConverter = $this->getCssSelectorConverter();
+        foreach ($cssRules['inlinable'] as $cssRule) {
+            try {
+                $nodesMatchingCssSelectors = $this->xPath->query($cssSelectorConverter->toXPath($cssRule['selector']));
+            } catch (ParseException $e) {
+                if ($this->debug) {
+                    throw $e;
+                }
+                continue;
+            }
+
+            /** @var \DOMElement $node */
+            foreach ($nodesMatchingCssSelectors as $node) {
+                if (\in_array($node, $excludedNodes, true)) {
+                    continue;
+                }
+                $this->copyInlinableCssToStyleAttribute($node, $cssRule);
+            }
+        }
+
+        if ($this->isInlineStyleAttributesParsingEnabled) {
+            $this->fillStyleAttributesWithMergedStyles();
+        }
+
+        $this->removeImportantAnnotationFromAllInlineStyles();
+
+        $this->determineMatchingUninlinableCssRules($cssRules['uninlinable']);
+        $this->copyUninlinableCssToStyleNode($uninlinableCss);
+
+        return $this;
+    }
+
+    /**
+     * Disables the parsing of inline styles.
+     *
+     * @return self fluent interface
+     */
+    public function disableInlineStyleAttributesParsing(): self
+    {
+        $this->isInlineStyleAttributesParsingEnabled = false;
+
+        return $this;
+    }
+
+    /**
+     * Disables the parsing of <style> blocks.
+     *
+     * @return self fluent interface
+     */
+    public function disableStyleBlocksParsing(): self
+    {
+        $this->isStyleBlocksParsingEnabled = false;
+
+        return $this;
+    }
+
+    /**
+     * Marks a media query type to keep.
+     *
+     * @param string $mediaName the media type name, e.g., "braille"
+     *
+     * @return self fluent interface
+     */
+    public function addAllowedMediaType(string $mediaName): self
+    {
+        $this->allowedMediaTypes[$mediaName] = true;
+
+        return $this;
+    }
+
+    /**
+     * Drops a media query type from the allowed list.
+     *
+     * @param string $mediaName the tag name, e.g., "braille"
+     *
+     * @return self fluent interface
+     */
+    public function removeAllowedMediaType(string $mediaName): self
+    {
+        if (isset($this->allowedMediaTypes[$mediaName])) {
+            unset($this->allowedMediaTypes[$mediaName]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Adds a selector to exclude nodes from emogrification.
+     *
+     * Any nodes that match the selector will not have their style altered.
+     *
+     * @param string $selector the selector to exclude, e.g., ".editor"
+     *
+     * @return self fluent interface
+     */
+    public function addExcludedSelector(string $selector): self
+    {
+        $this->excludedSelectors[$selector] = true;
+
+        return $this;
+    }
+
+    /**
+     * No longer excludes the nodes matching this selector from emogrification.
+     *
+     * @param string $selector the selector to no longer exclude, e.g., ".editor"
+     *
+     * @return self fluent interface
+     */
+    public function removeExcludedSelector(string $selector): self
+    {
+        if (isset($this->excludedSelectors[$selector])) {
+            unset($this->excludedSelectors[$selector]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Sets the debug mode.
+     *
+     * @param bool $debug set to true to enable debug mode
+     *
+     * @return self fluent interface
+     */
+    public function setDebug(bool $debug): self
+    {
+        $this->debug = $debug;
+
+        return $this;
+    }
+
+    /**
+     * Gets the array of selectors present in the CSS provided to `inlineCss()` for which the declarations could not be
+     * applied as inline styles, but which may affect elements in the HTML.  The relevant CSS will have been placed in a
+     * `<style>` element.  The selectors may include those used within `@media` rules or those involving dynamic
+     * pseudo-classes (such as `:hover`) or pseudo-elements (such as `::after`).
+     *
+     * @return string[]
+     *
+     * @throws \BadMethodCallException if `inlineCss` has not been called first
+     */
+    public function getMatchingUninlinableSelectors(): array
+    {
+        if ($this->matchingUninlinableCssRules === null) {
+            throw new \BadMethodCallException('inlineCss must be called first', 1568385221);
+        }
+
+        return \array_column($this->matchingUninlinableCssRules, 'selector');
+    }
+
+    /**
+     * Clears all caches.
+     *
+     * @return void
+     */
+    private function clearAllCaches()
+    {
+        $this->caches = [
+            self::CACHE_KEY_CSS => [],
+            self::CACHE_KEY_SELECTOR => [],
+            self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
+            self::CACHE_KEY_COMBINED_STYLES => [],
+        ];
+    }
+
+    /**
+     * Purges the visited nodes.
+     *
+     * @return void
+     */
+    private function purgeVisitedNodes()
+    {
+        $this->visitedNodes = [];
+        $this->styleAttributesForNodes = [];
+    }
+
+    /**
+     * Parses the document and normalizes all existing CSS attributes.
+     * This changes 'DISPLAY: none' to 'display: none'.
+     * We wouldn't have to do this if DOMXPath supported XPath 2.0.
+     * Also stores a reference of nodes with existing inline styles so we don't overwrite them.
+     *
+     * @return void
+     */
+    private function normalizeStyleAttributesOfAllNodes()
+    {
+        /** @var \DOMElement $node */
+        foreach ($this->getAllNodesWithStyleAttribute() as $node) {
+            if ($this->isInlineStyleAttributesParsingEnabled) {
+                $this->normalizeStyleAttributes($node);
+            }
+            // Remove style attribute in every case, so we can add them back (if inline style attributes
+            // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules;
+            // else original inline style rules may remain at the beginning of the final inline style definition
+            // of a node, which may give not the desired results
+            $node->removeAttribute('style');
+        }
+    }
+
+    /**
+     * Returns a list with all DOM nodes that have a style attribute.
+     *
+     * @return \DOMNodeList
+     */
+    private function getAllNodesWithStyleAttribute(): \DOMNodeList
+    {
+        return $this->xPath->query('//*[@style]');
+    }
+
+    /**
+     * Normalizes the value of the "style" attribute and saves it.
+     *
+     * @param \DOMElement $node
+     *
+     * @return void
+     */
+    private function normalizeStyleAttributes(\DOMElement $node)
+    {
+        $normalizedOriginalStyle = \preg_replace_callback(
+            '/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S',
+            static function (array $m) {
+                return \strtolower($m[0]);
+            },
+            $node->getAttribute('style')
+        );
+
+        // in order to not overwrite existing style attributes in the HTML, we
+        // have to save the original HTML styles
+        $nodePath = $node->getNodePath();
+        if (!isset($this->styleAttributesForNodes[$nodePath])) {
+            $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle);
+            $this->visitedNodes[$nodePath] = $node;
+        }
+
+        $node->setAttribute('style', $normalizedOriginalStyle);
+    }
+
+    /**
+     * Parses a CSS declaration block into property name/value pairs.
+     *
+     * Example:
+     *
+     * The declaration block
+     *
+     *   "color: #000; font-weight: bold;"
+     *
+     * will be parsed into the following array:
+     *
+     *   "color" => "#000"
+     *   "font-weight" => "bold"
+     *
+     * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
+     *
+     * @return string[]
+     *         the CSS declarations with the property names as array keys and the property values as array values
+     */
+    private function parseCssDeclarationsBlock(string $cssDeclarationsBlock): array
+    {
+        if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) {
+            return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
+        }
+
+        $properties = [];
+        foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) {
+            $matches = [];
+            if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
+                continue;
+            }
+
+            $propertyName = \strtolower($matches[1]);
+            $propertyValue = $matches[2];
+            $properties[$propertyName] = $propertyValue;
+        }
+        $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties;
+
+        return $properties;
+    }
+
+    /**
+     * Returns CSS content.
+     *
+     * @return string
+     */
+    private function getCssFromAllStyleNodes(): string
+    {
+        $styleNodes = $this->xPath->query('//style');
+        if ($styleNodes === false) {
+            return '';
+        }
+
+        $css = '';
+        /** @var \DOMNode $styleNode */
+        foreach ($styleNodes as $styleNode) {
+            $css .= "\n\n" . $styleNode->nodeValue;
+            $styleNode->parentNode->removeChild($styleNode);
+        }
+
+        return $css;
+    }
+
+    /**
+     * Removes comments from the supplied CSS.
+     *
+     * @param string $css
+     *
+     * @return string CSS with the comments removed
+     */
+    private function removeCssComments(string $css): string
+    {
+        return \preg_replace('%/\\*[^*]*+(?:\\*(?!/)[^*]*+)*+\\*/%', '', $css);
+    }
+
+    /**
+     * Extracts `@import` and `@charset` rules from the supplied CSS.  These rules must not be preceded by any other
+     * rules, or they will be ignored.  (From the CSS 2.1 specification: "CSS 2.1 user agents must ignore any '@import'
+     * rule that occurs inside a block or after any non-ignored statement other than an @charset or an @import rule."
+     * Note also that `@charset` is case sensitive whereas `@import` is not.)
+     *
+     * @param string $css CSS with comments removed
+     *
+     * @return string[] The first element is the CSS with the valid `@import` and `@charset` rules removed.  The second
+     * element contains a concatenation of the valid `@import` rules, each followed by whatever whitespace followed it
+     * in the original CSS (so that either unminified or minified formatting is preserved); if there were no `@import`
+     * rules, it will be an empty string.  The (valid) `@charset` rules are discarded.
+     */
+    private function extractImportAndCharsetRules(string $css): array
+    {
+        $possiblyModifiedCss = $css;
+        $importRules = '';
+
+        while (
+            \preg_match(
+                '/^\\s*+(@((?i)import(?-i)|charset)\\s[^;]++;\\s*+)/',
+                $possiblyModifiedCss,
+                $matches
+            )
+        ) {
+            list($fullMatch, $atRuleAndFollowingWhitespace, $atRuleName) = $matches;
+
+            if (\strtolower($atRuleName) === 'import') {
+                $importRules .= $atRuleAndFollowingWhitespace;
+            }
+
+            $possiblyModifiedCss = \substr($possiblyModifiedCss, \strlen($fullMatch));
+        }
+
+        return [$possiblyModifiedCss, $importRules];
+    }
+
+    /**
+     * Extracts `@font-face` rules from the supplied CSS.  Note that `@font-face` rules can be placed anywhere in your
+     * CSS and are not case sensitive.
+     *
+     * @param string $css CSS with comments, import and charset removed
+     *
+     * @return string[] The first element is the CSS with the valid `@font-face` rules removed.  The second
+     * element contains a concatenation of the valid `@font-face` rules, each followed by whatever whitespace followed
+     * it in the original CSS (so that either unminified or minified formatting is preserved); if there were no
+     * `@font-face` rules, it will be an empty string.
+     */
+    private function extractFontFaceRules(string $css): array
+    {
+        $possiblyModifiedCss = $css;
+        $fontFaces = '';
+
+        while (
+            \preg_match(
+                '/(@font-face[^}]++}\\s*+)/i',
+                $possiblyModifiedCss,
+                $matches
+            )
+        ) {
+            list($fullMatch, $atRuleAndFollowingWhitespace) = $matches;
+
+            if (\stripos($fullMatch, 'font-family') !== false && \stripos($fullMatch, 'src') !== false) {
+                $fontFaces .= $atRuleAndFollowingWhitespace;
+            }
+
+            $possiblyModifiedCss = \str_replace($fullMatch, '', $possiblyModifiedCss);
+        }
+
+        return [$possiblyModifiedCss, $fontFaces];
+    }
+
+    /**
+     * Find the nodes that are not to be emogrified.
+     *
+     * @return \DOMElement[]
+     *
+     * @throws ParseException
+     */
+    private function getNodesToExclude(): array
+    {
+        $excludedNodes = [];
+        foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) {
+            try {
+                $matchingNodes = $this->xPath->query($this->getCssSelectorConverter()->toXPath($selectorToExclude));
+            } catch (ParseException $e) {
+                if ($this->debug) {
+                    throw $e;
+                }
+                continue;
+            }
+            foreach ($matchingNodes as $node) {
+                $excludedNodes[] = $node;
+            }
+        }
+
+        return $excludedNodes;
+    }
+
+    /**
+     * @return CssSelectorConverter
+     */
+    private function getCssSelectorConverter(): CssSelectorConverter
+    {
+        if ($this->cssSelectorConverter === null) {
+            $this->cssSelectorConverter = new CssSelectorConverter();
+        }
+
+        return $this->cssSelectorConverter;
+    }
+
+    /**
+     * Extracts and parses the individual rules from a CSS string.
+     *
+     * @param string $css a string of raw CSS code with comments removed
+     *
+     * @return string[][][] A 2-entry array with the key "inlinable" containing rules which can be inlined as `style`
+     *         attributes and the key "uninlinable" containing rules which cannot.  Each value is an array of string
+     *         sub-arrays with the keys
+     *         "media" (the media query string, e.g. "@media screen and (max-width: 480px)",
+     *         or an empty string if not from a `@media` rule),
+     *         "selector" (the CSS selector, e.g., "*" or "header h1"),
+     *         "hasUnmatchablePseudo" (true if that selector contains pseudo-elements or dynamic pseudo-classes
+     *         such that the declarations cannot be applied inline),
+     *         "declarationsBlock" (the semicolon-separated CSS declarations for that selector,
+     *         e.g., "color: red; height: 4px;"),
+     *         and "line" (the line number e.g. 42)
+     */
+    private function parseCssRules(string $css): array
+    {
+        $cssKey = \md5($css);
+        if (isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) {
+            return $this->caches[self::CACHE_KEY_CSS][$cssKey];
+        }
+
+        $matches = $this->getCssRuleMatches($css);
+
+        $cssRules = [
+            'inlinable' => [],
+            'uninlinable' => [],
+        ];
+        /** @var string[][] $matches */
+        /** @var string[] $cssRule */
+        foreach ($matches as $key => $cssRule) {
+            $cssDeclaration = \trim($cssRule['declarations']);
+            if ($cssDeclaration === '') {
+                continue;
+            }
+
+            foreach (\explode(',', $cssRule['selectors']) as $selector) {
+                // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
+                // only allow structural pseudo-classes
+                $hasPseudoElement = \strpos($selector, '::') !== false;
+                $hasUnsupportedPseudoClass = (bool)\preg_match(
+                    '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i',
+                    $selector
+                );
+                $hasUnmatchablePseudo = $hasPseudoElement || $hasUnsupportedPseudoClass;
+
+                $parsedCssRule = [
+                    'media' => $cssRule['media'],
+                    'selector' => \trim($selector),
+                    'hasUnmatchablePseudo' => $hasUnmatchablePseudo,
+                    'declarationsBlock' => $cssDeclaration,
+                    // keep track of where it appears in the file, since order is important
+                    'line' => $key,
+                ];
+                $ruleType = ($cssRule['media'] === '' && !$hasUnmatchablePseudo) ? 'inlinable' : 'uninlinable';
+                $cssRules[$ruleType][] = $parsedCssRule;
+            }
+        }
+
+        \usort($cssRules['inlinable'], [$this, 'sortBySelectorPrecedence']);
+
+        $this->caches[self::CACHE_KEY_CSS][$cssKey] = $cssRules;
+
+        return $cssRules;
+    }
+
+    /**
+     * @param string[] $a
+     * @param string[] $b
+     *
+     * @return int
+     */
+    private function sortBySelectorPrecedence(array $a, array $b): int
+    {
+        $precedenceA = $this->getCssSelectorPrecedence($a['selector']);
+        $precedenceB = $this->getCssSelectorPrecedence($b['selector']);
+
+        // We want these sorted in ascending order so selectors with lesser precedence get processed first and
+        // selectors with greater precedence get sorted last.
+        $precedenceForEquals = ($a['line'] < $b['line'] ? -1 : 1);
+        $precedenceForNotEquals = ($precedenceA < $precedenceB ? -1 : 1);
+        return ($precedenceA === $precedenceB) ? $precedenceForEquals : $precedenceForNotEquals;
+    }
+
+    /**
+     * @param string $selector
+     *
+     * @return int
+     */
+    private function getCssSelectorPrecedence(string $selector): int
+    {
+        $selectorKey = \md5($selector);
+        if (isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) {
+            return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
+        }
+
+        $precedence = 0;
+        foreach ($this->selectorPrecedenceMatchers as $matcher => $value) {
+            if (\trim($selector) === '') {
+                break;
+            }
+            $number = 0;
+            $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number);
+            $precedence += ($value * $number);
+        }
+        $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
+
+        return $precedence;
+    }
+
+    /**
+     * Parses a string of CSS into the media query, selectors and declarations for each ruleset in order.
+     *
+     * @param string $css CSS with comments removed
+     *
+     * @return string[][] Array of string sub-arrays with the keys
+     *         "media" (the media query string, e.g. "@media screen and (max-width: 480px)",
+     *         or an empty string if not from an `@media` rule),
+     *         "selectors" (the CSS selector(s), e.g., "*" or "h1, h2"),
+     *         "declarations" (the semicolon-separated CSS declarations for that/those selector(s),
+     *         e.g., "color: red; height: 4px;"),
+     */
+    private function getCssRuleMatches(string $css): array
+    {
+        $splitCss = $this->splitCssAndMediaQuery($css);
+
+        $ruleMatches = [];
+        foreach ($splitCss as $cssPart) {
+            // process each part for selectors and definitions
+            \preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $cssPart['css'], $matches, PREG_SET_ORDER);
+
+            /** @var string[][] $matches */
+            foreach ($matches as $cssRule) {
+                $ruleMatches[] = [
+                    'media' => $cssPart['media'],
+                    'selectors' => $cssRule[1],
+                    'declarations' => $cssRule[2],
+                ];
+            }
+        }
+
+        return $ruleMatches;
+    }
+
+    /**
+     * Splits input CSS code into an array of parts for different media queries, in order.
+     * Each part is an array where:
+     *
+     * - key "css" will contain clean CSS code (for @media rules this will be the group rule body within "{...}")
+     * - key "media" will contain "@media " followed by the media query list, for all allowed media queries,
+     *   or an empty string for CSS not within a media query
+     *
+     * Example:
+     *
+     * The CSS code
+     *
+     *   "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
+     *
+     * will be parsed into the following array:
+     *
+     *   0 => [
+     *     "css" => "h1 { color:red; }",
+     *     "media" => ""
+     *   ],
+     *   1 => [
+     *     "css" => " h1 {}",
+     *     "media" => "@media "
+     *   ]
+     *
+     * @param string $css
+     *
+     * @return string[][]
+     */
+    private function splitCssAndMediaQuery(string $css): array
+    {
+        $mediaTypesExpression = '';
+        if (!empty($this->allowedMediaTypes)) {
+            $mediaTypesExpression = '|' . \implode('|', \array_keys($this->allowedMediaTypes));
+        }
+
+        $mediaRuleBodyMatcher = '[^{]*+{(?:[^{}]*+{.*})?\\s*+}\\s*+';
+
+        $cssSplitForAllowedMediaTypes = \preg_split(
+            '#(@media\\s++(?:only\\s++)?+(?:(?=[{(])' . $mediaTypesExpression . ')' . $mediaRuleBodyMatcher
+            . ')#misU',
+            $css,
+            -1,
+            PREG_SPLIT_DELIM_CAPTURE
+        );
+
+        // filter the CSS outside/between allowed @media rules
+        $cssCleaningMatchers = [
+            'import/charset directives' => '/\\s*+@(?:import|charset)\\s[^;]++;/i',
+            'remaining media enclosures' => '/\\s*+@media\\s' . $mediaRuleBodyMatcher . '/isU',
+        ];
+
+        $splitCss = [];
+        foreach ($cssSplitForAllowedMediaTypes as $index => $cssPart) {
+            $isMediaRule = $index % 2 !== 0;
+            if ($isMediaRule) {
+                \preg_match('/^([^{]*+){(.*)}[^}]*+$/s', $cssPart, $matches);
+                $splitCss[] = [
+                    'css' => $matches[2],
+                    'media' => $matches[1],
+                ];
+            } else {
+                $cleanedCss = \trim(\preg_replace($cssCleaningMatchers, '', $cssPart));
+                if ($cleanedCss !== '') {
+                    $splitCss[] = [
+                        'css' => $cleanedCss,
+                        'media' => '',
+                    ];
+                }
+            }
+        }
+        return $splitCss;
+    }
+
+    /**
+     * Copies $cssRule into the style attribute of $node.
+     *
+     * Note: This method does not check whether $cssRule matches $node.
+     *
+     * @param \DOMElement $node
+     * @param string[][] $cssRule
+     *
+     * @return void
+     */
+    private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule)
+    {
+        /** @var string $declarationsBlock */
+        $declarationsBlock = $cssRule['declarationsBlock'];
+        $newStyleDeclarations = $this->parseCssDeclarationsBlock($declarationsBlock);
+        if ($newStyleDeclarations === []) {
+            return;
+        }
+
+        // if it has a style attribute, get it, process it, and append (overwrite) new stuff
+        if ($node->hasAttribute('style')) {
+            // break it up into an associative array
+            $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
+        } else {
+            $oldStyleDeclarations = [];
+        }
+        $node->setAttribute(
+            'style',
+            $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)
+        );
+    }
+
+    /**
+     * This method merges old or existing name/value array with new name/value array
+     * and then generates a string of the combined style suitable for placing inline.
+     * This becomes the single point for CSS string generation allowing for consistent
+     * CSS output no matter where the CSS originally came from.
+     *
+     * @param string[] $oldStyles
+     * @param string[] $newStyles
+     *
+     * @return string
+     */
+    private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles): string
+    {
+        $cacheKey = \serialize([$oldStyles, $newStyles]);
+        if (isset($this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey])) {
+            return $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey];
+        }
+
+        // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed
+        foreach ($oldStyles as $attributeName => $attributeValue) {
+            if (!isset($newStyles[$attributeName])) {
+                continue;
+            }
+
+            $newAttributeValue = $newStyles[$attributeName];
+            if (
+                $this->attributeValueIsImportant($attributeValue)
+                && !$this->attributeValueIsImportant($newAttributeValue)
+            ) {
+                unset($newStyles[$attributeName]);
+            } else {
+                unset($oldStyles[$attributeName]);
+            }
+        }
+
+        $combinedStyles = \array_merge($oldStyles, $newStyles);
+
+        $style = '';
+        foreach ($combinedStyles as $attributeName => $attributeValue) {
+            $style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; ';
+        }
+        $trimmedStyle = \rtrim($style);
+
+        $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle;
+
+        return $trimmedStyle;
+    }
+
+    /**
+     * Checks whether $attributeValue is marked as !important.
+     *
+     * @param string $attributeValue
+     *
+     * @return bool
+     */
+    private function attributeValueIsImportant(string $attributeValue): bool
+    {
+        return \strtolower(\substr(\trim($attributeValue), -10)) === '!important';
+    }
+
+    /**
+     * Merges styles from styles attributes and style nodes and applies them to the attribute nodes
+     *
+     * @return void
+     */
+    private function fillStyleAttributesWithMergedStyles()
+    {
+        foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
+            $node = $this->visitedNodes[$nodePath];
+            $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
+            $node->setAttribute(
+                'style',
+                $this->generateStyleStringFromDeclarationsArrays(
+                    $currentStyleAttributes,
+                    $styleAttributesForNode
+                )
+            );
+        }
+    }
+
+    /**
+     * Searches for all nodes with a style attribute and removes the "!important" annotations out of
+     * the inline style declarations, eventually by rearranging declarations.
+     *
+     * @return void
+     */
+    private function removeImportantAnnotationFromAllInlineStyles()
+    {
+        foreach ($this->getAllNodesWithStyleAttribute() as $node) {
+            $this->removeImportantAnnotationFromNodeInlineStyle($node);
+        }
+    }
+
+    /**
+     * Removes the "!important" annotations out of the inline style declarations,
+     * eventually by rearranging declarations.
+     * Rearranging needed when !important shorthand properties are followed by some of their
+     * not !important expanded-version properties.
+     * For example "font: 12px serif !important; font-size: 13px;" must be reordered
+     * to "font-size: 13px; font: 12px serif;" in order to remain correct.
+     *
+     * @param \DOMElement $node
+     *
+     * @return void
+     */
+    private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node)
+    {
+        $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
+        $regularStyleDeclarations = [];
+        $importantStyleDeclarations = [];
+        foreach ($inlineStyleDeclarations as $property => $value) {
+            if ($this->attributeValueIsImportant($value)) {
+                $importantStyleDeclarations[$property] = \trim(\str_replace('!important', '', $value));
+            } else {
+                $regularStyleDeclarations[$property] = $value;
+            }
+        }
+        $inlineStyleDeclarationsInNewOrder = \array_merge(
+            $regularStyleDeclarations,
+            $importantStyleDeclarations
+        );
+        $node->setAttribute(
+            'style',
+            $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder)
+        );
+    }
+
+    /**
+     * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array.
+     *
+     * @param string[] $styleDeclarations
+     *
+     * @return string
+     */
+    private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations): string
+    {
+        return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations);
+    }
+
+    /**
+     * Determines which of `$cssRules` actually apply to `$this->domDocument`, and sets them in
+     * `$this->matchingUninlinableCssRules`.
+     *
+     * @param string[][] $cssRules the "uninlinable" array of CSS rules returned by `parseCssRules`
+     *
+     * @return void
+     */
+    private function determineMatchingUninlinableCssRules(array $cssRules)
+    {
+        $this->matchingUninlinableCssRules = \array_filter($cssRules, [$this, 'existsMatchForSelectorInCssRule']);
+    }
+
+    /**
+     * Checks whether there is at least one matching element for the CSS selector contained in the `selector` element
+     * of the provided CSS rule.
+     *
+     * Any dynamic pseudo-classes will be assumed to apply. If the selector matches a pseudo-element,
+     * it will test for a match with its originating element.
+     *
+     * @param string[] $cssRule
+     *
+     * @return bool
+     *
+     * @throws ParseException
+     */
+    private function existsMatchForSelectorInCssRule(array $cssRule): bool
+    {
+        $selector = $cssRule['selector'];
+        if ($cssRule['hasUnmatchablePseudo']) {
+            $selector = $this->removeUnmatchablePseudoComponents($selector);
+        }
+        return $this->existsMatchForCssSelector($selector);
+    }
+
+    /**
+     * Checks whether there is at least one matching element for $cssSelector.
+     * When not in debug mode, it returns true also for invalid selectors (because they may be valid,
+     * just not implemented/recognized yet by Emogrifier).
+     *
+     * @param string $cssSelector
+     *
+     * @return bool
+     *
+     * @throws ParseException
+     */
+    private function existsMatchForCssSelector(string $cssSelector): bool
+    {
+        try {
+            $nodesMatchingSelector = $this->xPath->query($this->getCssSelectorConverter()->toXPath($cssSelector));
+        } catch (ParseException $e) {
+            if ($this->debug) {
+                throw $e;
+            }
+            return true;
+        }
+
+        return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
+    }
+
+    /**
+     * Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary.
+     * If such a pseudo-component is within the argument of `:not`, the entire `:not` component is removed or replaced.
+     *
+     * @param string $selector
+     *
+     * @return string Selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply,
+     *                or in the case of pseudo-elements will match their originating element.
+     */
+    private function removeUnmatchablePseudoComponents(string $selector): string
+    {
+        // The regex allows nested brackets via `(?2)`.
+        // A space is temporarily prepended because the callback can't determine if the match was at the very start.
+        $selectorWithoutNots = \ltrim(\preg_replace_callback(
+            '/(\\s?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i',
+            [$this, 'replaceUnmatchableNotComponent'],
+            ' ' . $selector
+        ));
+
+        $pseudoComponentMatcher = ':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+';
+        return \preg_replace(
+            ['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'],
+            ['$1*', ''],
+            $selectorWithoutNots
+        );
+    }
+
+    /**
+     * Helps `removeUnmatchablePseudoComponents()` replace or remove a selector `:not(...)` component if its argument
+     * contains pseudo-elements or dynamic pseudo-classes.
+     *
+     * @param string[] $matches array of elements matched by the regular expression
+     *
+     * @return string the full match if there were no unmatchable pseudo components within; otherwise, any preceding
+     *         whitespace followed by "*", or an empty string if there was no preceding whitespace
+     */
+    private function replaceUnmatchableNotComponent(array $matches): string
+    {
+        list($notComponentWithAnyPrecedingWhitespace, $anyPrecedingWhitespace, $notArgumentInBrackets) = $matches;
+
+        $hasUnmatchablePseudo = \preg_match(
+            '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-:]/i',
+            $notArgumentInBrackets
+        );
+
+        if ($hasUnmatchablePseudo) {
+            return $anyPrecedingWhitespace !== '' ? $anyPrecedingWhitespace . '*' : '';
+        }
+        return $notComponentWithAnyPrecedingWhitespace;
+    }
+
+    /**
+     * Applies `$this->matchingUninlinableCssRules` to `$this->domDocument` by placing them as CSS in a `<style>`
+     * element.
+     *
+     * @param string $uninlinableCss This may contain any `@import` or `@font-face` rules that should precede the CSS
+     *        placed in the `<style>` element.  If there are no unlinlinable CSS rules to copy there, a `<style>`
+     *        element will be created containing just `$uninlinableCss`.  `$uninlinableCss` may be an empty string;
+     *        if it is, and there are no unlinlinable CSS rules, an empty `<style>` element will not be created.
+     *
+     * @return void
+     */
+    private function copyUninlinableCssToStyleNode(string $uninlinableCss)
+    {
+        $css = $uninlinableCss;
+
+        // avoid including unneeded class dependency if there are no rules
+        if ($this->matchingUninlinableCssRules !== []) {
+            $cssConcatenator = new CssConcatenator();
+            foreach ($this->matchingUninlinableCssRules as $cssRule) {
+                $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']);
+            }
+            $css .= $cssConcatenator->getCss();
+        }
+
+        // avoid adding empty style element
+        if ($css !== '') {
+            $this->addStyleElementToDocument($css);
+        }
+    }
+
+    /**
+     * Adds a style element with $css to $this->domDocument.
+     *
+     * This method is protected to allow overriding.
+     *
+     * @see https://github.com/MyIntervals/emogrifier/issues/103
+     *
+     * @param string $css
+     *
+     * @return void
+     */
+    protected function addStyleElementToDocument(string $css)
+    {
+        $styleElement = $this->domDocument->createElement('style', $css);
+        $styleAttribute = $this->domDocument->createAttribute('type');
+        $styleAttribute->value = 'text/css';
+        $styleElement->appendChild($styleAttribute);
+
+        $headElement = $this->getHeadElement();
+        $headElement->appendChild($styleElement);
+    }
+
+    /**
+     * Returns the HEAD element.
+     *
+     * This method assumes that there always is a HEAD element.
+     *
+     * @return \DOMElement
+     */
+    private function getHeadElement(): \DOMElement
+    {
+        return $this->domDocument->getElementsByTagName('head')->item(0);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier.php
deleted file mode 100644 (file)
index 08f5eb6..0000000
+++ /dev/null
@@ -1,2020 +0,0 @@
-<?php
-
-namespace Pelago;
-
-/**
- * This class provides functions for converting CSS styles into inline style attributes in your HTML code.
- *
- * For more information, please see the README.md file.
- *
- * @author Cameron Brooks
- * @author Jaime Prado
- * @author Oliver Klee <github@oliverklee.de>
- * @author Roman Ožana <ozana@omdesign.cz>
- * @author Sander Kruger <s.kruger@invessel.com>
- * @author Zoli Szabó <zoli.szabo+github@gmail.com>
- */
-class Emogrifier
-{
-    /**
-     * @var int
-     */
-    const CACHE_KEY_CSS = 0;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_SELECTOR = 1;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_XPATH = 2;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 3;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_COMBINED_STYLES = 4;
-
-    /**
-     * for calculating nth-of-type and nth-child selectors
-     *
-     * @var int
-     */
-    const INDEX = 0;
-
-    /**
-     * for calculating nth-of-type and nth-child selectors
-     *
-     * @var int
-     */
-    const MULTIPLIER = 1;
-
-    /**
-     * @var string
-     */
-    const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/';
-
-    /**
-     * @var string
-     */
-    const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/';
-
-    /**
-     * Regular expression component matching a static pseudo class in a selector, without the preceding ":",
-     * for which the applicable elements can be determined (by converting the selector to an XPath expression).
-     * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead
-     * group, as appropriate for the usage context.)
-     *
-     * @var string
-     */
-    const PSEUDO_CLASS_MATCHER = '\\S+\\-(?:child|type\\()|not\\([[:ascii:]]*\\)';
-
-    /**
-     * @var string
-     */
-    const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
-
-    /**
-     * @var string
-     */
-    const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
-
-    /**
-     * @var \DOMDocument
-     */
-    protected $domDocument = null;
-
-    /**
-     * @var string
-     */
-    private $css = '';
-
-    /**
-     * @var bool[]
-     */
-    private $excludedSelectors = [];
-
-    /**
-     * @var string[]
-     */
-    private $unprocessableHtmlTags = ['wbr'];
-
-    /**
-     * @var bool[]
-     */
-    private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true];
-
-    /**
-     * @var mixed[]
-     */
-    private $caches = [
-        self::CACHE_KEY_CSS => [],
-        self::CACHE_KEY_SELECTOR => [],
-        self::CACHE_KEY_XPATH => [],
-        self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
-        self::CACHE_KEY_COMBINED_STYLES => [],
-    ];
-
-    /**
-     * the visited nodes with the XPath paths as array keys
-     *
-     * @var \DOMElement[]
-     */
-    private $visitedNodes = [];
-
-    /**
-     * the styles to apply to the nodes with the XPath paths as array keys for the outer array
-     * and the attribute names/values as key/value pairs for the inner array
-     *
-     * @var string[][]
-     */
-    private $styleAttributesForNodes = [];
-
-    /**
-     * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
-     * If set to false, the value of the style attributes will be discarded.
-     *
-     * @var bool
-     */
-    private $isInlineStyleAttributesParsingEnabled = true;
-
-    /**
-     * Determines whether the <style> blocks in the HTML passed to this class should be parsed.
-     *
-     * If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML
-     * via inline styles.
-     *
-     * If set to false, the <style> blocks will be left as they are in the HTML.
-     *
-     * @var bool
-     */
-    private $isStyleBlocksParsingEnabled = true;
-
-    /**
-     * Determines whether elements with the `display: none` property are
-     * removed from the DOM.
-     *
-     * @var bool
-     */
-    private $shouldRemoveInvisibleNodes = true;
-
-    /**
-     * For calculating selector precedence order.
-     * Keys are a regular expression part to match before a CSS name.
-     * Values are a multiplier factor per match to weight specificity.
-     *
-     * @var int[]
-     */
-    private $selectorPrecedenceMatchers = [
-        // IDs: worth 10000
-        '\\#' => 10000,
-        // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100
-        '(?:\\.|\\[|(?<!:):(?!not\\())' => 100,
-        // elements (not attribute values or `:not`), pseudo-elements: worth 1
-        '(?:(?<![="\':\\w\\-])|::)' => 1,
-    ];
-
-    /**
-     * @var string[]
-     */
-    private $xPathRules = [
-        // attribute presence
-        '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/' => '*[@\\1]',
-        // type and attribute exact value
-        '/(\\w)\\[(\\w+)\\=[\'"]?([\\w\\s]+)[\'"]?\\]/' => '\\1[@\\2="\\3"]',
-        // type and attribute value with ~ (one word within a whitespace-separated list of words)
-        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\~\\=[\\s]*[\'"]?([\\w\\-_\\/]+)[\'"]?\\]/'
-        => '\\1[contains(concat(" ", @\\2, " "), concat(" ", "\\3", " "))]',
-        // type and attribute value with | (either exact value match or prefix followed by a hyphen)
-        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\|\\=[\\s]*[\'"]?([\\w\\-_\\s\\/]+)[\'"]?\\]/'
-        => '\\1[@\\2="\\3" or starts-with(@\\2, concat("\\3", "-"))]',
-        // type and attribute value with ^ (prefix match)
-        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\^\\=[\\s]*[\'"]?([\\w\\-_\\/]+)[\'"]?\\]/' => '\\1[starts-with(@\\2, "\\3")]',
-        // type and attribute value with * (substring match)
-        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\*\\=[\\s]*[\'"]?([\\w\\-_\\s\\/:;]+)[\'"]?\\]/' => '\\1[contains(@\\2, "\\3")]',
-        // adjacent sibling
-        '/\\s*\\+\\s*/' => '/following-sibling::*[1]/self::',
-        // child
-        '/\\s*>\\s*/' => '/',
-        // descendant (don't match spaces within already translated XPath predicates)
-        '/\\s+(?![^\\[\\]]*+\\])/' => '//',
-        // type and :first-child
-        '/([^\\/]+):first-child/i' => '*[1]/self::\\1',
-        // type and :last-child
-        '/([^\\/]+):last-child/i' => '*[last()]/self::\\1',
-
-        // The following matcher will break things if it is placed before the adjacent matcher.
-        // So one of the matchers matches either too much or not enough.
-        // type and attribute value with $ (suffix match)
-        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\$\\=[\\s]*[\'"]?([\\w\\-_\\s\\/]+)[\'"]?\\]/'
-        => '\\1[substring(@\\2, string-length(@\\2) - string-length("\\3") + 1) = "\\3"]',
-    ];
-
-    /**
-     * Determines whether CSS styles that have an equivalent HTML attribute
-     * should be mapped and attached to those elements.
-     *
-     * @var bool
-     */
-    private $shouldMapCssToHtml = false;
-
-    /**
-     * This multi-level array contains simple mappings of CSS properties to
-     * HTML attributes. If a mapping only applies to certain HTML nodes or
-     * only for certain values, the mapping is an object with a whitelist
-     * of nodes and values.
-     *
-     * @var mixed[][]
-     */
-    private $cssToHtmlMap = [
-        'background-color' => [
-            'attribute' => 'bgcolor',
-        ],
-        'text-align' => [
-            'attribute' => 'align',
-            'nodes' => ['p', 'div', 'td'],
-            'values' => ['left', 'right', 'center', 'justify'],
-        ],
-        'float' => [
-            'attribute' => 'align',
-            'nodes' => ['table', 'img'],
-            'values' => ['left', 'right'],
-        ],
-        'border-spacing' => [
-            'attribute' => 'cellspacing',
-            'nodes' => ['table'],
-        ],
-    ];
-
-    /**
-     * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them.
-     *
-     * @var bool
-     */
-    private $debug = false;
-
-    /**
-     * @param string $unprocessedHtml the HTML to process, must be UTF-8-encoded
-     * @param string $css the CSS to merge, must be UTF-8-encoded
-     */
-    public function __construct($unprocessedHtml = '', $css = '')
-    {
-        if ($unprocessedHtml !== '') {
-            $this->setHtml($unprocessedHtml);
-        }
-        $this->setCss($css);
-    }
-
-    /**
-     * Sets the HTML to process.
-     *
-     * @param string $html the HTML to process, must be UTF-encoded, must not be empty
-     *
-     * @return void
-     *
-     * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
-     */
-    public function setHtml($html)
-    {
-        if (!\is_string($html)) {
-            throw new \InvalidArgumentException('The provided HTML must be a string.', 1540403913);
-        }
-        if ($html === '') {
-            throw new \InvalidArgumentException('The provided HTML must not be empty.', 1540403910);
-        }
-
-        $this->createUnifiedDomDocument($html);
-    }
-
-    /**
-     * Provides access to the internal DOMDocument representation of the HTML in its current state.
-     *
-     * @return \DOMDocument
-     */
-    public function getDomDocument()
-    {
-        return $this->domDocument;
-    }
-
-    /**
-     * Sets the CSS to merge with the HTML.
-     *
-     * @param string $css the CSS to merge, must be UTF-8-encoded
-     *
-     * @return void
-     */
-    public function setCss($css)
-    {
-        $this->css = $css;
-    }
-
-    /**
-     * Renders the normalized and processed HTML.
-     *
-     * @return string
-     */
-    protected function render()
-    {
-        return $this->domDocument->saveHTML();
-    }
-
-    /**
-     * Renders the content of the BODY element of the normalized and processed HTML.
-     *
-     * @return string
-     */
-    protected function renderBodyContent()
-    {
-        $bodyNodeHtml = $this->domDocument->saveHTML($this->getBodyElement());
-
-        return \str_replace(['<body>', '</body>'], '', $bodyNodeHtml);
-    }
-
-    /**
-     * Returns the BODY element.
-     *
-     * This method assumes that there always is a BODY element.
-     *
-     * @return \DOMElement
-     */
-    private function getBodyElement()
-    {
-        return $this->domDocument->getElementsByTagName('body')->item(0);
-    }
-
-    /**
-     * Returns the HEAD element.
-     *
-     * This method assumes that there always is a HEAD element.
-     *
-     * @return \DOMElement
-     */
-    private function getHeadElement()
-    {
-        return $this->domDocument->getElementsByTagName('head')->item(0);
-    }
-
-    /**
-     * Applies $this->css to the given HTML and returns the HTML with the CSS
-     * applied.
-     *
-     * This method places the CSS inline.
-     *
-     * @return string
-     *
-     * @throws \BadMethodCallException
-     */
-    public function emogrify()
-    {
-        $this->assertExistenceOfHtml();
-
-        $this->process();
-
-        return $this->render();
-    }
-
-    /**
-     * Applies $this->css to the given HTML and returns only the HTML content
-     * within the <body> tag.
-     *
-     * This method places the CSS inline.
-     *
-     * @return string
-     *
-     * @throws \BadMethodCallException
-     */
-    public function emogrifyBodyContent()
-    {
-        $this->assertExistenceOfHtml();
-
-        $this->process();
-
-        return $this->renderBodyContent();
-    }
-
-    /**
-     * Checks that some HTML has been set, and throws an exception otherwise.
-     *
-     * @return void
-     *
-     * @throws \BadMethodCallException
-     */
-    private function assertExistenceOfHtml()
-    {
-        if ($this->domDocument === null) {
-            throw new \BadMethodCallException('Please set some HTML first.', 1390393096);
-        }
-    }
-
-    /**
-     * Creates a DOM document from the given HTML and stores it in $this->domDocument.
-     *
-     * The DOM document will always have a BODY element.
-     *
-     * @param string $html
-     *
-     * @return void
-     */
-    private function createUnifiedDomDocument($html)
-    {
-        $this->createRawDomDocument($html);
-        $this->ensureExistenceOfBodyElement();
-    }
-
-    /**
-     * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
-     *
-     * @param string $html
-     *
-     * @return void
-     */
-    private function createRawDomDocument($html)
-    {
-        $domDocument = new \DOMDocument();
-        $domDocument->encoding = 'UTF-8';
-        $domDocument->strictErrorChecking = false;
-        $domDocument->formatOutput = true;
-        $libXmlState = \libxml_use_internal_errors(true);
-        $domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
-        \libxml_clear_errors();
-        \libxml_use_internal_errors($libXmlState);
-        $domDocument->normalizeDocument();
-
-        $this->domDocument = $domDocument;
-    }
-
-    /**
-     * Returns the HTML with added document type and Content-Type meta tag if needed,
-     * ensuring that the HTML will be good for creating a DOM document from it.
-     *
-     * @param string $html
-     *
-     * @return string the unified HTML
-     */
-    private function prepareHtmlForDomConversion($html)
-    {
-        $htmlWithDocumentType = $this->ensureDocumentType($html);
-
-        return $this->addContentTypeMetaTag($htmlWithDocumentType);
-    }
-
-    /**
-     * Applies $this->css to $this->domDocument.
-     *
-     * This method places the CSS inline.
-     *
-     * @return void
-     *
-     * @throws \InvalidArgumentException
-     */
-    protected function process()
-    {
-        $this->clearAllCaches();
-        $this->purgeVisitedNodes();
-
-        $xPath = new \DOMXPath($this->domDocument);
-        \set_error_handler([$this, 'handleXpathQueryWarnings'], E_WARNING);
-        $this->removeUnprocessableTags();
-        $this->normalizeStyleAttributesOfAllNodes($xPath);
-
-        // grab any existing style blocks from the html and append them to the existing CSS
-        // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
-        $allCss = $this->css;
-        if ($this->isStyleBlocksParsingEnabled) {
-            $allCss .= $this->getCssFromAllStyleNodes($xPath);
-        }
-
-        $excludedNodes = $this->getNodesToExclude($xPath);
-        $cssRules = $this->parseCssRules($allCss);
-        foreach ($cssRules['inlineable'] as $cssRule) {
-            // There's no real way to test "PHP Warning" output generated by the following XPath query unless PHPUnit
-            // converts it to an exception. Unfortunately, this would only apply to tests and not work for production
-            // executions, which can still flood logs/output unnecessarily. Instead, Emogrifier's error handler should
-            // always throw an exception and it must be caught here and only rethrown if in debug mode.
-            try {
-                // \DOMXPath::query will always return a DOMNodeList or throw an exception when errors are caught.
-                $nodesMatchingCssSelectors = $xPath->query($this->translateCssToXpath($cssRule['selector']));
-            } catch (\InvalidArgumentException $e) {
-                if ($this->debug) {
-                    throw $e;
-                }
-                continue;
-            }
-
-            /** @var \DOMElement $node */
-            foreach ($nodesMatchingCssSelectors as $node) {
-                if (\in_array($node, $excludedNodes, true)) {
-                    continue;
-                }
-                $this->copyInlineableCssToStyleAttribute($node, $cssRule);
-            }
-        }
-
-        if ($this->isInlineStyleAttributesParsingEnabled) {
-            $this->fillStyleAttributesWithMergedStyles();
-        }
-        $this->postProcess($xPath);
-
-        $this->removeImportantAnnotationFromAllInlineStyles($xPath);
-
-        $this->copyUninlineableCssToStyleNode($xPath, $cssRules['uninlineable']);
-
-        \restore_error_handler();
-    }
-
-    /**
-     * Applies some optional post-processing to the HTML in the DOM document.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function postProcess(\DOMXPath $xPath)
-    {
-        if ($this->shouldMapCssToHtml) {
-            $this->mapAllInlineStylesToHtmlAttributes($xPath);
-        }
-        if ($this->shouldRemoveInvisibleNodes) {
-            $this->removeInvisibleNodes($xPath);
-        }
-    }
-
-    /**
-     * Searches for all nodes with a style attribute, transforms the CSS found
-     * to HTML attributes and adds those attributes to each node.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function mapAllInlineStylesToHtmlAttributes(\DOMXPath $xPath)
-    {
-        /** @var \DOMElement $node */
-        foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
-            $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-            $this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node);
-        }
-    }
-
-    /**
-     * Searches for all nodes with a style attribute and removes the "!important" annotations out of
-     * the inline style declarations, eventually by rearranging declarations.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function removeImportantAnnotationFromAllInlineStyles(\DOMXPath $xPath)
-    {
-        foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
-            $this->removeImportantAnnotationFromNodeInlineStyle($node);
-        }
-    }
-
-    /**
-     * Removes the "!important" annotations out of the inline style declarations,
-     * eventually by rearranging declarations.
-     * Rearranging needed when !important shorthand properties are followed by some of their
-     * not !important expanded-version properties.
-     * For example "font: 12px serif !important; font-size: 13px;" must be reordered
-     * to "font-size: 13px; font: 12px serif;" in order to remain correct.
-     *
-     * @param \DOMElement $node
-     *
-     * @return void
-     */
-    private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node)
-    {
-        $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-        $regularStyleDeclarations = [];
-        $importantStyleDeclarations = [];
-        foreach ($inlineStyleDeclarations as $property => $value) {
-            if ($this->attributeValueIsImportant($value)) {
-                $importantStyleDeclarations[$property] = \trim(\str_replace('!important', '', $value));
-            } else {
-                $regularStyleDeclarations[$property] = $value;
-            }
-        }
-        $inlineStyleDeclarationsInNewOrder = \array_merge(
-            $regularStyleDeclarations,
-            $importantStyleDeclarations
-        );
-        $node->setAttribute(
-            'style',
-            $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder)
-        );
-    }
-
-    /**
-     * Returns a list with all DOM nodes that have a style attribute.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return \DOMNodeList
-     */
-    private function getAllNodesWithStyleAttribute(\DOMXPath $xPath)
-    {
-        return $xPath->query('//*[@style]');
-    }
-
-    /**
-     * Applies $styles to $node.
-     *
-     * This method maps CSS styles to HTML attributes and adds those to the
-     * node.
-     *
-     * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return void
-     */
-    private function mapCssToHtmlAttributes(array $styles, \DOMElement $node)
-    {
-        foreach ($styles as $property => $value) {
-            // Strip !important indicator
-            $value = \trim(\str_replace('!important', '', $value));
-            $this->mapCssToHtmlAttribute($property, $value, $node);
-        }
-    }
-
-    /**
-     * Tries to apply the CSS style to $node as an attribute.
-     *
-     * This method maps a CSS rule to HTML attributes and adds those to the node.
-     *
-     * @param string $property the name of the CSS property to map
-     * @param string $value the value of the style rule to map
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return void
-     */
-    private function mapCssToHtmlAttribute($property, $value, \DOMElement $node)
-    {
-        if (!$this->mapSimpleCssProperty($property, $value, $node)) {
-            $this->mapComplexCssProperty($property, $value, $node);
-        }
-    }
-
-    /**
-     * Looks up the CSS property in the mapping table and maps it if it matches the conditions.
-     *
-     * @param string $property the name of the CSS property to map
-     * @param string $value the value of the style rule to map
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return bool true if the property can be mapped using the simple mapping table
-     */
-    private function mapSimpleCssProperty($property, $value, \DOMElement $node)
-    {
-        if (!isset($this->cssToHtmlMap[$property])) {
-            return false;
-        }
-
-        $mapping = $this->cssToHtmlMap[$property];
-        $nodesMatch = !isset($mapping['nodes']) || \in_array($node->nodeName, $mapping['nodes'], true);
-        $valuesMatch = !isset($mapping['values']) || \in_array($value, $mapping['values'], true);
-        if (!$nodesMatch || !$valuesMatch) {
-            return false;
-        }
-
-        $node->setAttribute($mapping['attribute'], $value);
-
-        return true;
-    }
-
-    /**
-     * Maps CSS properties that need special transformation to an HTML attribute.
-     *
-     * @param string $property the name of the CSS property to map
-     * @param string $value the value of the style rule to map
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return void
-     */
-    private function mapComplexCssProperty($property, $value, \DOMElement $node)
-    {
-        switch ($property) {
-            case 'background':
-                $this->mapBackgroundProperty($node, $value);
-                break;
-            case 'width':
-                // intentional fall-through
-            case 'height':
-                $this->mapWidthOrHeightProperty($node, $value, $property);
-                break;
-            case 'margin':
-                $this->mapMarginProperty($node, $value);
-                break;
-            case 'border':
-                $this->mapBorderProperty($node, $value);
-                break;
-            default:
-        }
-    }
-
-    /**
-     * Maps the "background" CSS property to visual HTML attributes.
-     *
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     *
-     * @return void
-     */
-    private function mapBackgroundProperty(\DOMElement $node, $value)
-    {
-        // parse out the color, if any
-        $styles = \explode(' ', $value);
-        $first = $styles[0];
-        if (!\is_numeric($first[0]) && \strpos($first, 'url') !== 0) {
-            // as this is not a position or image, assume it's a color
-            $node->setAttribute('bgcolor', $first);
-        }
-    }
-
-    /**
-     * Maps the "width" or "height" CSS properties to visual HTML attributes.
-     *
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     * @param string $property the name of the CSS property to map
-     *
-     * @return void
-     */
-    private function mapWidthOrHeightProperty(\DOMElement $node, $value, $property)
-    {
-        // only parse values in px and %, but not values like "auto"
-        if (\preg_match('/^\\d+(px|%)$/', $value)) {
-            // Remove 'px'. This regex only conserves numbers and %.
-            $number = \preg_replace('/[^0-9.%]/', '', $value);
-            $node->setAttribute($property, $number);
-        }
-    }
-
-    /**
-     * Maps the "margin" CSS property to visual HTML attributes.
-     *
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     *
-     * @return void
-     */
-    private function mapMarginProperty(\DOMElement $node, $value)
-    {
-        if (!$this->isTableOrImageNode($node)) {
-            return;
-        }
-
-        $margins = $this->parseCssShorthandValue($value);
-        if ($margins['left'] === 'auto' && $margins['right'] === 'auto') {
-            $node->setAttribute('align', 'center');
-        }
-    }
-
-    /**
-     * Maps the "border" CSS property to visual HTML attributes.
-     *
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     *
-     * @return void
-     */
-    private function mapBorderProperty(\DOMElement $node, $value)
-    {
-        if (!$this->isTableOrImageNode($node)) {
-            return;
-        }
-
-        if ($value === 'none' || $value === '0') {
-            $node->setAttribute('border', '0');
-        }
-    }
-
-    /**
-     * Checks whether $node is a table or img element.
-     *
-     * @param \DOMElement $node
-     *
-     * @return bool
-     */
-    private function isTableOrImageNode(\DOMElement $node)
-    {
-        return $node->nodeName === 'table' || $node->nodeName === 'img';
-    }
-
-    /**
-     * Parses a shorthand CSS value and splits it into individual values
-     *
-     * @param string $value a string of CSS value with 1, 2, 3 or 4 sizes
-     *                      For example: padding: 0 auto;
-     *                      '0 auto' is split into top: 0, left: auto, bottom: 0,
-     *                      right: auto.
-     *
-     * @return string[] an array of values for top, right, bottom and left (using these as associative array keys)
-     */
-    private function parseCssShorthandValue($value)
-    {
-        $values = \preg_split('/\\s+/', $value);
-
-        $css = [];
-        $css['top'] = $values[0];
-        $css['right'] = (\count($values) > 1) ? $values[1] : $css['top'];
-        $css['bottom'] = (\count($values) > 2) ? $values[2] : $css['top'];
-        $css['left'] = (\count($values) > 3) ? $values[3] : $css['right'];
-
-        return $css;
-    }
-
-    /**
-     * Extracts and parses the individual rules from a CSS string.
-     *
-     * @param string $css a string of raw CSS code
-     *
-     * @return string[][][] A 2-entry array with the key "inlineable" containing rules which can be inlined as `style`
-     *         attributes and the key "uninlineable" containing rules which cannot.  Each value is an array of string
-     *         sub-arrays with the keys
-     *         "media" (the media query string, e.g. "@media screen and (max-width: 480px)",
-     *         or an empty string if not from a `@media` rule),
-     *         "selector" (the CSS selector, e.g., "*" or "header h1"),
-     *         "hasUnmatchablePseudo" (true if that selector contains psuedo-elements or dynamic pseudo-classes
-     *         such that the declarations cannot be applied inline),
-     *         "declarationsBlock" (the semicolon-separated CSS declarations for that selector,
-     *         e.g., "color: red; height: 4px;"),
-     *         and "line" (the line number e.g. 42)
-     */
-    private function parseCssRules($css)
-    {
-        $cssKey = \md5($css);
-        if (!isset($this->caches[static::CACHE_KEY_CSS][$cssKey])) {
-            $matches = $this->getCssRuleMatches($css);
-
-            $cssRules = [
-                'inlineable' => [],
-                'uninlineable' => [],
-            ];
-            /** @var string[][] $matches */
-            /** @var string[] $cssRule */
-            foreach ($matches as $key => $cssRule) {
-                $cssDeclaration = \trim($cssRule['declarations']);
-                if ($cssDeclaration === '') {
-                    continue;
-                }
-
-                $selectors = \explode(',', $cssRule['selectors']);
-                foreach ($selectors as $selector) {
-                    // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
-                    // only allow structural pseudo-classes
-                    $hasPseudoElement = \strpos($selector, '::') !== false;
-                    $hasUnsupportedPseudoClass = (bool)\preg_match(
-                        '/:(?!' . static::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i',
-                        $selector
-                    );
-                    $hasUnmatchablePseudo = $hasPseudoElement || $hasUnsupportedPseudoClass;
-
-                    $parsedCssRule = [
-                        'media' => $cssRule['media'],
-                        'selector' => \trim($selector),
-                        'hasUnmatchablePseudo' => $hasUnmatchablePseudo,
-                        'declarationsBlock' => $cssDeclaration,
-                        // keep track of where it appears in the file, since order is important
-                        'line' => $key,
-                    ];
-                    $ruleType = ($cssRule['media'] === '' && !$hasUnmatchablePseudo) ? 'inlineable' : 'uninlineable';
-                    $cssRules[$ruleType][] = $parsedCssRule;
-                }
-            }
-
-            \usort($cssRules['inlineable'], [$this, 'sortBySelectorPrecedence']);
-
-            $this->caches[static::CACHE_KEY_CSS][$cssKey] = $cssRules;
-        }
-
-        return $this->caches[static::CACHE_KEY_CSS][$cssKey];
-    }
-
-    /**
-     * Parses a string of CSS into the media query, selectors and declarations for each ruleset in order.
-     *
-     * @param string $css
-     *
-     * @return string[][] Array of string sub-arrays with the keys
-     *         "media" (the media query string, e.g. "@media screen and (max-width: 480px)",
-     *         or an empty string if not from an `@media` rule),
-     *         "selectors" (the CSS selector(s), e.g., "*" or "h1, h2"),
-     *         "declarations" (the semicolon-separated CSS declarations for that/those selector(s),
-     *         e.g., "color: red; height: 4px;"),
-     */
-    private function getCssRuleMatches($css)
-    {
-        $ruleMatches = [];
-
-        $splitCss = $this->splitCssAndMediaQuery($css);
-        foreach ($splitCss as $cssPart) {
-            // process each part for selectors and definitions
-            \preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $cssPart['css'], $matches, PREG_SET_ORDER);
-
-            /** @var string[][] $matches */
-            foreach ($matches as $cssRule) {
-                $ruleMatches[] = [
-                    'media' => $cssPart['media'],
-                    'selectors' => $cssRule[1],
-                    'declarations' => $cssRule[2],
-                ];
-            }
-        }
-
-        return $ruleMatches;
-    }
-
-    /**
-     * Disables the parsing of inline styles.
-     *
-     * @return void
-     */
-    public function disableInlineStyleAttributesParsing()
-    {
-        $this->isInlineStyleAttributesParsingEnabled = false;
-    }
-
-    /**
-     * Disables the parsing of <style> blocks.
-     *
-     * @return void
-     */
-    public function disableStyleBlocksParsing()
-    {
-        $this->isStyleBlocksParsingEnabled = false;
-    }
-
-    /**
-     * Disables the removal of elements with `display: none` properties.
-     *
-     * @deprecated will be removed in Emogrifier 3.0
-     *
-     * @return void
-     */
-    public function disableInvisibleNodeRemoval()
-    {
-        $this->shouldRemoveInvisibleNodes = false;
-    }
-
-    /**
-     * Enables the attachment/override of HTML attributes for which a
-     * corresponding CSS property has been set.
-     *
-     * @deprecated will be removed in Emogrifier 3.0, use the CssToAttributeConverter instead
-     *
-     * @return void
-     */
-    public function enableCssToHtmlMapping()
-    {
-        $this->shouldMapCssToHtml = true;
-    }
-
-    /**
-     * Clears all caches.
-     *
-     * @return void
-     */
-    private function clearAllCaches()
-    {
-        $this->caches = [
-            static::CACHE_KEY_CSS => [],
-            static::CACHE_KEY_SELECTOR => [],
-            static::CACHE_KEY_XPATH => [],
-            static::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
-            static::CACHE_KEY_COMBINED_STYLES => [],
-        ];
-    }
-
-    /**
-     * Purges the visited nodes.
-     *
-     * @return void
-     */
-    private function purgeVisitedNodes()
-    {
-        $this->visitedNodes = [];
-        $this->styleAttributesForNodes = [];
-    }
-
-    /**
-     * Marks a tag for removal.
-     *
-     * There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them.
-     * In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document.
-     *
-     * Note: The tags will not be removed if they have any content.
-     *
-     * @param string $tagName the tag name, e.g., "p"
-     *
-     * @return void
-     */
-    public function addUnprocessableHtmlTag($tagName)
-    {
-        $this->unprocessableHtmlTags[] = $tagName;
-    }
-
-    /**
-     * Drops a tag from the removal list.
-     *
-     * @param string $tagName the tag name, e.g., "p"
-     *
-     * @return void
-     */
-    public function removeUnprocessableHtmlTag($tagName)
-    {
-        $key = \array_search($tagName, $this->unprocessableHtmlTags, true);
-        if ($key !== false) {
-            unset($this->unprocessableHtmlTags[$key]);
-        }
-    }
-
-    /**
-     * Marks a media query type to keep.
-     *
-     * @param string $mediaName the media type name, e.g., "braille"
-     *
-     * @return void
-     */
-    public function addAllowedMediaType($mediaName)
-    {
-        $this->allowedMediaTypes[$mediaName] = true;
-    }
-
-    /**
-     * Drops a media query type from the allowed list.
-     *
-     * @param string $mediaName the tag name, e.g., "braille"
-     *
-     * @return void
-     */
-    public function removeAllowedMediaType($mediaName)
-    {
-        if (isset($this->allowedMediaTypes[$mediaName])) {
-            unset($this->allowedMediaTypes[$mediaName]);
-        }
-    }
-
-    /**
-     * Adds a selector to exclude nodes from emogrification.
-     *
-     * Any nodes that match the selector will not have their style altered.
-     *
-     * @param string $selector the selector to exclude, e.g., ".editor"
-     *
-     * @return void
-     */
-    public function addExcludedSelector($selector)
-    {
-        $this->excludedSelectors[$selector] = true;
-    }
-
-    /**
-     * No longer excludes the nodes matching this selector from emogrification.
-     *
-     * @param string $selector the selector to no longer exclude, e.g., ".editor"
-     *
-     * @return void
-     */
-    public function removeExcludedSelector($selector)
-    {
-        if (isset($this->excludedSelectors[$selector])) {
-            unset($this->excludedSelectors[$selector]);
-        }
-    }
-
-    /**
-     * This removes styles from your email that contain display:none.
-     * We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only
-     * supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase,
-     * not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE")
-     * to lowercase.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function removeInvisibleNodes(\DOMXPath $xPath)
-    {
-        $nodesWithStyleDisplayNone = $xPath->query(
-            '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'
-        );
-        if ($nodesWithStyleDisplayNone->length === 0) {
-            return;
-        }
-
-        // The checks on parentNode and is_callable below ensure that if we've deleted the parent node,
-        // we don't try to call removeChild on a nonexistent child node
-        /** @var \DOMNode $node */
-        foreach ($nodesWithStyleDisplayNone as $node) {
-            if ($node->parentNode && \is_callable([$node->parentNode, 'removeChild'])) {
-                $node->parentNode->removeChild($node);
-            }
-        }
-    }
-
-    /**
-     * Parses the document and normalizes all existing CSS attributes.
-     * This changes 'DISPLAY: none' to 'display: none'.
-     * We wouldn't have to do this if DOMXPath supported XPath 2.0.
-     * Also stores a reference of nodes with existing inline styles so we don't overwrite them.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function normalizeStyleAttributesOfAllNodes(\DOMXPath $xPath)
-    {
-        /** @var \DOMElement $node */
-        foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
-            if ($this->isInlineStyleAttributesParsingEnabled) {
-                $this->normalizeStyleAttributes($node);
-            }
-            // Remove style attribute in every case, so we can add them back (if inline style attributes
-            // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules;
-            // else original inline style rules may remain at the beginning of the final inline style definition
-            // of a node, which may give not the desired results
-            $node->removeAttribute('style');
-        }
-    }
-
-    /**
-     * Normalizes the value of the "style" attribute and saves it.
-     *
-     * @param \DOMElement $node
-     *
-     * @return void
-     */
-    private function normalizeStyleAttributes(\DOMElement $node)
-    {
-        $normalizedOriginalStyle = \preg_replace_callback(
-            '/[A-z\\-]+(?=\\:)/S',
-            function (array $m) {
-                return \strtolower($m[0]);
-            },
-            $node->getAttribute('style')
-        );
-
-        // in order to not overwrite existing style attributes in the HTML, we
-        // have to save the original HTML styles
-        $nodePath = $node->getNodePath();
-        if (!isset($this->styleAttributesForNodes[$nodePath])) {
-            $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle);
-            $this->visitedNodes[$nodePath] = $node;
-        }
-
-        $node->setAttribute('style', $normalizedOriginalStyle);
-    }
-
-    /**
-     * Merges styles from styles attributes and style nodes and applies them to the attribute nodes
-     *
-     * @return void
-     */
-    private function fillStyleAttributesWithMergedStyles()
-    {
-        foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
-            $node = $this->visitedNodes[$nodePath];
-            $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-            $node->setAttribute(
-                'style',
-                $this->generateStyleStringFromDeclarationsArrays(
-                    $currentStyleAttributes,
-                    $styleAttributesForNode
-                )
-            );
-        }
-    }
-
-    /**
-     * This method merges old or existing name/value array with new name/value array
-     * and then generates a string of the combined style suitable for placing inline.
-     * This becomes the single point for CSS string generation allowing for consistent
-     * CSS output no matter where the CSS originally came from.
-     *
-     * @param string[] $oldStyles
-     * @param string[] $newStyles
-     *
-     * @return string
-     */
-    private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles)
-    {
-        $cacheKey = \serialize([$oldStyles, $newStyles]);
-        if (isset($this->caches[static::CACHE_KEY_COMBINED_STYLES][$cacheKey])) {
-            return $this->caches[static::CACHE_KEY_COMBINED_STYLES][$cacheKey];
-        }
-
-        // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed
-        foreach ($oldStyles as $attributeName => $attributeValue) {
-            if (!isset($newStyles[$attributeName])) {
-                continue;
-            }
-
-            $newAttributeValue = $newStyles[$attributeName];
-            if ($this->attributeValueIsImportant($attributeValue)
-                && !$this->attributeValueIsImportant($newAttributeValue)
-            ) {
-                unset($newStyles[$attributeName]);
-            } else {
-                unset($oldStyles[$attributeName]);
-            }
-        }
-
-        $combinedStyles = \array_merge($oldStyles, $newStyles);
-
-        $style = '';
-        foreach ($combinedStyles as $attributeName => $attributeValue) {
-            $style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; ';
-        }
-        $trimmedStyle = \rtrim($style);
-
-        $this->caches[static::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle;
-
-        return $trimmedStyle;
-    }
-
-    /**
-     * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array.
-     *
-     * @param string[] $styleDeclarations
-     *
-     * @return string
-     */
-    private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations)
-    {
-        return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations);
-    }
-
-    /**
-     * Checks whether $attributeValue is marked as !important.
-     *
-     * @param string $attributeValue
-     *
-     * @return bool
-     */
-    private function attributeValueIsImportant($attributeValue)
-    {
-        return \strtolower(\substr(\trim($attributeValue), -10)) === '!important';
-    }
-
-    /**
-     * Copies $cssRule into the style attribute of $node.
-     *
-     * Note: This method does not check whether $cssRule matches $node.
-     *
-     * @param \DOMElement $node
-     * @param string[][] $cssRule
-     *
-     * @return void
-     */
-    private function copyInlineableCssToStyleAttribute(\DOMElement $node, array $cssRule)
-    {
-        // if it has a style attribute, get it, process it, and append (overwrite) new stuff
-        if ($node->hasAttribute('style')) {
-            // break it up into an associative array
-            $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-        } else {
-            $oldStyleDeclarations = [];
-        }
-        $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']);
-        $node->setAttribute(
-            'style',
-            $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)
-        );
-    }
-
-    /**
-     * Applies $cssRules to $this->domDocument, limited to the rules that actually apply to the document.
-     *
-     * @param \DOMXPath $xPath
-     * @param string[][] $cssRules The "uninlineable" array of CSS rules returned by `parseCssRules`
-     *
-     * @return void
-     */
-    private function copyUninlineableCssToStyleNode(\DOMXPath $xPath, array $cssRules)
-    {
-        $cssRulesRelevantForDocument = \array_filter(
-            $cssRules,
-            function (array $cssRule) use ($xPath) {
-                $selector = $cssRule['selector'];
-                if ($cssRule['hasUnmatchablePseudo']) {
-                    $selector = $this->removeUnmatchablePseudoComponents($selector);
-                }
-                return $this->existsMatchForCssSelector($xPath, $selector);
-            }
-        );
-
-        if ($cssRulesRelevantForDocument === []) {
-            // avoid adding empty style element (or including unneeded class dependency)
-            return;
-        }
-
-        // support use without autoload
-        if (!\class_exists('Pelago\\Emogrifier\\CssConcatenator')) {
-            require_once __DIR__ . '/Emogrifier/CssConcatenator.php';
-        }
-
-        $cssConcatenator = new Emogrifier\CssConcatenator();
-        foreach ($cssRulesRelevantForDocument as $cssRule) {
-            $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']);
-        }
-
-        $this->addStyleElementToDocument($cssConcatenator->getCss());
-    }
-
-    /**
-     * Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary.
-     *
-     * @param string $selector
-     *
-     * @return string Selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply,
-     *                or in the case of pseudo-elements will match their originating element.
-     */
-    private function removeUnmatchablePseudoComponents($selector)
-    {
-        $pseudoComponentMatcher = ':(?!' . static::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+';
-        return \preg_replace(
-            ['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'],
-            ['$1*', ''],
-            $selector
-        );
-    }
-
-    /**
-     * Checks whether there is at least one matching element for $cssSelector.
-     * When not in debug mode, it returns true also for invalid selectors (because they may be valid,
-     * just not implemented/recognized yet by Emogrifier).
-     *
-     * @param \DOMXPath $xPath
-     * @param string $cssSelector
-     *
-     * @return bool
-     *
-     * @throws \InvalidArgumentException
-     */
-    private function existsMatchForCssSelector(\DOMXPath $xPath, $cssSelector)
-    {
-        try {
-            $nodesMatchingSelector = $xPath->query($this->translateCssToXpath($cssSelector));
-        } catch (\InvalidArgumentException $e) {
-            if ($this->debug) {
-                throw $e;
-            }
-            return true;
-        }
-
-        return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
-    }
-
-    /**
-     * Returns CSS content.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return string
-     */
-    private function getCssFromAllStyleNodes(\DOMXPath $xPath)
-    {
-        $styleNodes = $xPath->query('//style');
-
-        if ($styleNodes === false) {
-            return '';
-        }
-
-        $css = '';
-        /** @var \DOMNode $styleNode */
-        foreach ($styleNodes as $styleNode) {
-            $css .= "\n\n" . $styleNode->nodeValue;
-            $styleNode->parentNode->removeChild($styleNode);
-        }
-
-        return $css;
-    }
-
-    /**
-     * Adds a style element with $css to $this->domDocument.
-     *
-     * This method is protected to allow overriding.
-     *
-     * @see https://github.com/jjriv/emogrifier/issues/103
-     *
-     * @param string $css
-     *
-     * @return void
-     */
-    protected function addStyleElementToDocument($css)
-    {
-        $styleElement = $this->domDocument->createElement('style', $css);
-        $styleAttribute = $this->domDocument->createAttribute('type');
-        $styleAttribute->value = 'text/css';
-        $styleElement->appendChild($styleAttribute);
-
-        $headElement = $this->getHeadElement();
-        $headElement->appendChild($styleElement);
-    }
-
-    /**
-     * Checks that $this->domDocument has a BODY element and adds it if it is missing.
-     *
-     * @return void
-     */
-    private function ensureExistenceOfBodyElement()
-    {
-        if ($this->domDocument->getElementsByTagName('body')->item(0) !== null) {
-            return;
-        }
-
-        $htmlElement = $this->domDocument->getElementsByTagName('html')->item(0);
-        $htmlElement->appendChild($this->domDocument->createElement('body'));
-    }
-
-    /**
-     * Splits input CSS code into an array of parts for different media querues, in order.
-     * Each part is an array where:
-     *
-     * - key "css" will contain clean CSS code (for @media rules this will be the group rule body within "{...}")
-     * - key "media" will contain "@media " followed by the media query list, for all allowed media queries,
-     *   or an empty string for CSS not within a media query
-     *
-     * Example:
-     *
-     * The CSS code
-     *
-     *   "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
-     *
-     * will be parsed into the following array:
-     *
-     *   0 => [
-     *     "css" => "h1 { color:red; }",
-     *     "media" => ""
-     *   ],
-     *   1 => [
-     *     "css" => " h1 {}",
-     *     "media" => "@media "
-     *   ]
-     *
-     * @param string $css
-     *
-     * @return string[][]
-     */
-    private function splitCssAndMediaQuery($css)
-    {
-        $cssWithoutComments = \preg_replace('/\\/\\*.*\\*\\//sU', '', $css);
-
-        $mediaTypesExpression = '';
-        if (!empty($this->allowedMediaTypes)) {
-            $mediaTypesExpression = '|' . \implode('|', \array_keys($this->allowedMediaTypes));
-        }
-
-        $mediaRuleBodyMatcher = '[^{]*+{(?:[^{}]*+{.*})?\\s*+}\\s*+';
-
-        $cssSplitForAllowedMediaTypes = \preg_split(
-            '#(@media\\s++(?:only\\s++)?+(?:(?=[{\\(])' . $mediaTypesExpression . ')' . $mediaRuleBodyMatcher
-            . ')#misU',
-            $cssWithoutComments,
-            -1,
-            PREG_SPLIT_DELIM_CAPTURE
-        );
-
-        // filter the CSS outside/between allowed @media rules
-        $cssCleaningMatchers = [
-            'import/charset directives' => '/\\s*+@(?:import|charset)\\s[^;]++;/i',
-            'remaining media enclosures' => '/\\s*+@media\\s' . $mediaRuleBodyMatcher . '/isU',
-        ];
-
-        $splitCss = [];
-        foreach ($cssSplitForAllowedMediaTypes as $index => $cssPart) {
-            $isMediaRule = $index % 2 !== 0;
-            if ($isMediaRule) {
-                \preg_match('/^([^{]*+){(.*)}[^}]*+$/s', $cssPart, $matches);
-                $splitCss[] = [
-                    'css' => $matches[2],
-                    'media' => $matches[1],
-                ];
-            } else {
-                $cleanedCss = \trim(\preg_replace($cssCleaningMatchers, '', $cssPart));
-                if ($cleanedCss !== '') {
-                    $splitCss[] = [
-                        'css' => $cleanedCss,
-                        'media' => '',
-                    ];
-                }
-            }
-        }
-        return $splitCss;
-    }
-
-    /**
-     * Removes empty unprocessable tags from the DOM document.
-     *
-     * @return void
-     */
-    private function removeUnprocessableTags()
-    {
-        foreach ($this->unprocessableHtmlTags as $tagName) {
-            $nodes = $this->domDocument->getElementsByTagName($tagName);
-            /** @var \DOMNode $node */
-            foreach ($nodes as $node) {
-                $hasContent = $node->hasChildNodes() || $node->hasChildNodes();
-                if (!$hasContent) {
-                    $node->parentNode->removeChild($node);
-                }
-            }
-        }
-    }
-
-    /**
-     * Makes sure that the passed HTML has a document type.
-     *
-     * @param string $html
-     *
-     * @return string HTML with document type
-     */
-    private function ensureDocumentType($html)
-    {
-        $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false;
-        if ($hasDocumentType) {
-            return $html;
-        }
-
-        return static::DEFAULT_DOCUMENT_TYPE . $html;
-    }
-
-    /**
-     * Adds a Content-Type meta tag for the charset.
-     *
-     * This method also ensures that there is a HEAD element.
-     *
-     * @param string $html
-     *
-     * @return string the HTML with the meta tag added
-     */
-    private function addContentTypeMetaTag($html)
-    {
-        $hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false;
-        if ($hasContentTypeMetaTag) {
-            return $html;
-        }
-
-        // We are trying to insert the meta tag to the right spot in the DOM.
-        // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
-        $hasHeadTag = \stripos($html, '<head') !== false;
-        $hasHtmlTag = \stripos($html, '<html') !== false;
-
-        if ($hasHeadTag) {
-            $reworkedHtml = \preg_replace('/<head(.*?)>/i', '<head$1>' . static::CONTENT_TYPE_META_TAG, $html);
-        } elseif ($hasHtmlTag) {
-            $reworkedHtml = \preg_replace(
-                '/<html(.*?)>/i',
-                '<html$1><head>' . static::CONTENT_TYPE_META_TAG . '</head>',
-                $html
-            );
-        } else {
-            $reworkedHtml = static::CONTENT_TYPE_META_TAG . $html;
-        }
-
-        return $reworkedHtml;
-    }
-
-    /**
-     * @param string[] $a
-     * @param string[] $b
-     *
-     * @return int
-     */
-    private function sortBySelectorPrecedence(array $a, array $b)
-    {
-        $precedenceA = $this->getCssSelectorPrecedence($a['selector']);
-        $precedenceB = $this->getCssSelectorPrecedence($b['selector']);
-
-        // We want these sorted in ascending order so selectors with lesser precedence get processed first and
-        // selectors with greater precedence get sorted last.
-        $precedenceForEquals = ($a['line'] < $b['line'] ? -1 : 1);
-        $precedenceForNotEquals = ($precedenceA < $precedenceB ? -1 : 1);
-        return ($precedenceA === $precedenceB) ? $precedenceForEquals : $precedenceForNotEquals;
-    }
-
-    /**
-     * @param string $selector
-     *
-     * @return int
-     */
-    private function getCssSelectorPrecedence($selector)
-    {
-        $selectorKey = \md5($selector);
-        if (!isset($this->caches[static::CACHE_KEY_SELECTOR][$selectorKey])) {
-            $precedence = 0;
-            foreach ($this->selectorPrecedenceMatchers as $matcher => $value) {
-                if (\trim($selector) === '') {
-                    break;
-                }
-                $number = 0;
-                $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number);
-                $precedence += ($value * $number);
-            }
-            $this->caches[static::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
-        }
-
-        return $this->caches[static::CACHE_KEY_SELECTOR][$selectorKey];
-    }
-
-    /**
-     * Maps a CSS selector to an XPath query string.
-     *
-     * @see http://plasmasturm.org/log/444/
-     *
-     * @param string $cssSelector a CSS selector
-     *
-     * @return string the corresponding XPath selector
-     */
-    private function translateCssToXpath($cssSelector)
-    {
-        $paddedSelector = ' ' . $cssSelector . ' ';
-        $lowercasePaddedSelector = \preg_replace_callback(
-            '/\\s+\\w+\\s+/',
-            function (array $matches) {
-                return \strtolower($matches[0]);
-            },
-            $paddedSelector
-        );
-        $trimmedLowercaseSelector = \trim($lowercasePaddedSelector);
-        $xPathKey = \md5($trimmedLowercaseSelector);
-        if (isset($this->caches[static::CACHE_KEY_XPATH][$xPathKey])) {
-            return $this->caches[static::CACHE_KEY_SELECTOR][$xPathKey];
-        }
-
-        $hasNotSelector = (bool)\preg_match(
-            '/^([^:]+):not\\(\\s*([[:ascii:]]+)\\s*\\)$/',
-            $trimmedLowercaseSelector,
-            $matches
-        );
-        if (!$hasNotSelector) {
-            $xPath = '//' . $this->translateCssToXpathPass($trimmedLowercaseSelector);
-        } else {
-            /** @var string[] $matches */
-            list(, $partBeforeNot, $notContents) = $matches;
-            $xPath = '//' . $this->translateCssToXpathPass($partBeforeNot) .
-                '[not(' . $this->translateCssToXpathPassInline($notContents) . ')]';
-        }
-        $this->caches[static::CACHE_KEY_SELECTOR][$xPathKey] = $xPath;
-
-        return $this->caches[static::CACHE_KEY_SELECTOR][$xPathKey];
-    }
-
-    /**
-     * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector.
-     *
-     * @param string $trimmedLowercaseSelector
-     *
-     * @return string
-     */
-    private function translateCssToXpathPass($trimmedLowercaseSelector)
-    {
-        return $this->translateCssToXpathPassWithMatchClassAttributesCallback(
-            $trimmedLowercaseSelector,
-            [$this, 'matchClassAttributes']
-        );
-    }
-
-    /**
-     * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector for inline usage.
-     *
-     * @param string $trimmedLowercaseSelector
-     *
-     * @return string
-     */
-    private function translateCssToXpathPassInline($trimmedLowercaseSelector)
-    {
-        return $this->translateCssToXpathPassWithMatchClassAttributesCallback(
-            $trimmedLowercaseSelector,
-            [$this, 'matchClassAttributesInline']
-        );
-    }
-
-    /**
-     * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector while using
-     * $matchClassAttributesCallback as to match the class attributes.
-     *
-     * @param string $trimmedLowercaseSelector
-     * @param callable $matchClassAttributesCallback
-     *
-     * @return string
-     */
-    private function translateCssToXpathPassWithMatchClassAttributesCallback(
-        $trimmedLowercaseSelector,
-        callable $matchClassAttributesCallback
-    ) {
-        $roughXpath = \preg_replace(\array_keys($this->xPathRules), $this->xPathRules, $trimmedLowercaseSelector);
-        $xPathWithIdAttributeMatchers = \preg_replace_callback(
-            static::ID_ATTRIBUTE_MATCHER,
-            [$this, 'matchIdAttributes'],
-            $roughXpath
-        );
-        $xPathWithIdAttributeAndClassMatchers = \preg_replace_callback(
-            static::CLASS_ATTRIBUTE_MATCHER,
-            $matchClassAttributesCallback,
-            $xPathWithIdAttributeMatchers
-        );
-
-        // Advanced selectors are going to require a bit more advanced emogrification.
-        $xPathWithIdAttributeAndClassMatchers = \preg_replace_callback(
-            '/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
-            [$this, 'translateNthChild'],
-            $xPathWithIdAttributeAndClassMatchers
-        );
-        $finalXpath = \preg_replace_callback(
-            '/([^\\/]+):nth-of-type\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
-            [$this, 'translateNthOfType'],
-            $xPathWithIdAttributeAndClassMatchers
-        );
-
-        return $finalXpath;
-    }
-
-    /**
-     * @param string[] $match
-     *
-     * @return string
-     */
-    private function matchIdAttributes(array $match)
-    {
-        return ($match[1] !== '' ? $match[1] : '*') . '[@id="' . $match[2] . '"]';
-    }
-
-    /**
-     * @param string[] $match
-     *
-     * @return string xPath class attribute query wrapped in element selector
-     */
-    private function matchClassAttributes(array $match)
-    {
-        return ($match[1] !== '' ? $match[1] : '*') . '[' . $this->matchClassAttributesInline($match) . ']';
-    }
-
-    /**
-     * @param string[] $match
-     *
-     * @return string xPath class attribute query
-     */
-    private function matchClassAttributesInline(array $match)
-    {
-        return 'contains(concat(" ",@class," "),concat(" ","' .
-            \implode(
-                '"," "))][contains(concat(" ",@class," "),concat(" ","',
-                \explode('.', \substr($match[2], 1))
-            ) . '"," "))';
-    }
-
-    /**
-     * @param string[] $match
-     *
-     * @return string
-     */
-    private function translateNthChild(array $match)
-    {
-        $parseResult = $this->parseNth($match);
-
-        if (isset($parseResult[static::MULTIPLIER])) {
-            if ($parseResult[static::MULTIPLIER] < 0) {
-                $parseResult[static::MULTIPLIER] = \abs($parseResult[static::MULTIPLIER]);
-                $xPathExpression = \sprintf(
-                    '*[(last() - position()) mod %1%u = %2$u]/static::%3$s',
-                    $parseResult[static::MULTIPLIER],
-                    $parseResult[static::INDEX],
-                    $match[1]
-                );
-            } else {
-                $xPathExpression = \sprintf(
-                    '*[position() mod %1$u = %2$u]/static::%3$s',
-                    $parseResult[static::MULTIPLIER],
-                    $parseResult[static::INDEX],
-                    $match[1]
-                );
-            }
-        } else {
-            $xPathExpression = \sprintf('*[%1$u]/static::%2$s', $parseResult[static::INDEX], $match[1]);
-        }
-
-        return $xPathExpression;
-    }
-
-    /**
-     * @param string[] $match
-     *
-     * @return string
-     */
-    private function translateNthOfType(array $match)
-    {
-        $parseResult = $this->parseNth($match);
-
-        if (isset($parseResult[static::MULTIPLIER])) {
-            if ($parseResult[static::MULTIPLIER] < 0) {
-                $parseResult[static::MULTIPLIER] = \abs($parseResult[static::MULTIPLIER]);
-                $xPathExpression = \sprintf(
-                    '%1$s[(last() - position()) mod %2$u = %3$u]',
-                    $match[1],
-                    $parseResult[static::MULTIPLIER],
-                    $parseResult[static::INDEX]
-                );
-            } else {
-                $xPathExpression = \sprintf(
-                    '%1$s[position() mod %2$u = %3$u]',
-                    $match[1],
-                    $parseResult[static::MULTIPLIER],
-                    $parseResult[static::INDEX]
-                );
-            }
-        } else {
-            $xPathExpression = \sprintf('%1$s[%2$u]', $match[1], $parseResult[static::INDEX]);
-        }
-
-        return $xPathExpression;
-    }
-
-    /**
-     * @param string[] $match
-     *
-     * @return int[]
-     */
-    private function parseNth(array $match)
-    {
-        if (\in_array(\strtolower($match[2]), ['even', 'odd'], true)) {
-            // we have "even" or "odd"
-            $index = \strtolower($match[2]) === 'even' ? 0 : 1;
-            return [static::MULTIPLIER => 2, static::INDEX => $index];
-        }
-        if (\stripos($match[2], 'n') === false) {
-            // if there is a multiplier
-            $index = (int)\str_replace(' ', '', $match[2]);
-            return [static::INDEX => $index];
-        }
-
-        if (isset($match[3])) {
-            $multipleTerm = \str_replace($match[3], '', $match[2]);
-            $index = (int)\str_replace(' ', '', $match[3]);
-        } else {
-            $multipleTerm = $match[2];
-            $index = 0;
-        }
-
-        $multiplier = \str_ireplace('n', '', $multipleTerm);
-
-        if ($multiplier === '') {
-            $multiplier = 1;
-        } elseif ($multiplier === '0') {
-            return [static::INDEX => $index];
-        } else {
-            $multiplier = (int)$multiplier;
-        }
-
-        while ($index < 0) {
-            $index += \abs($multiplier);
-        }
-
-        return [static::MULTIPLIER => $multiplier, static::INDEX => $index];
-    }
-
-    /**
-     * Parses a CSS declaration block into property name/value pairs.
-     *
-     * Example:
-     *
-     * The declaration block
-     *
-     *   "color: #000; font-weight: bold;"
-     *
-     * will be parsed into the following array:
-     *
-     *   "color" => "#000"
-     *   "font-weight" => "bold"
-     *
-     * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
-     *
-     * @return string[]
-     *         the CSS declarations with the property names as array keys and the property values as array values
-     */
-    private function parseCssDeclarationsBlock($cssDeclarationsBlock)
-    {
-        if (isset($this->caches[static::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) {
-            return $this->caches[static::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
-        }
-
-        $properties = [];
-        $declarations = \preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
-
-        foreach ($declarations as $declaration) {
-            $matches = [];
-            if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
-                continue;
-            }
-
-            $propertyName = \strtolower($matches[1]);
-            $propertyValue = $matches[2];
-            $properties[$propertyName] = $propertyValue;
-        }
-        $this->caches[static::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties;
-
-        return $properties;
-    }
-
-    /**
-     * Find the nodes that are not to be emogrified.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return \DOMElement[]
-     *
-     * @throws \InvalidArgumentException
-     */
-    private function getNodesToExclude(\DOMXPath $xPath)
-    {
-        $excludedNodes = [];
-        foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) {
-            try {
-                $matchingNodes = $xPath->query($this->translateCssToXpath($selectorToExclude));
-            } catch (\InvalidArgumentException $e) {
-                if ($this->debug) {
-                    throw $e;
-                }
-                continue;
-            }
-            foreach ($matchingNodes as $node) {
-                $excludedNodes[] = $node;
-            }
-        }
-
-        return $excludedNodes;
-    }
-
-    /**
-     * Handles invalid xPath expression warnings, generated during the process() method,
-     * during querying \DOMDocument and trigger an \InvalidArgumentException with an invalid selector
-     * or \RuntimeException, depending on the source of the warning.
-     *
-     * @param int $type
-     * @param string $message
-     * @param string $file
-     * @param int $line
-     * @param array $context
-     *
-     * @return bool always false
-     *
-     * @throws \InvalidArgumentException
-     * @throws \RuntimeException
-     */
-    public function handleXpathQueryWarnings(// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
-        $type,
-        $message,
-        $file,
-        $line,
-        array $context
-    ) {
-        $selector = '';
-        if (isset($context['cssRule']['selector'])) {
-            // warnings generated by invalid/unrecognized selectors in method process()
-            $selector = $context['cssRule']['selector'];
-        } elseif (isset($context['selectorToExclude'])) {
-            // warnings generated by invalid/unrecognized selectors in method getNodesToExclude()
-            $selector = $context['selectorToExclude'];
-        } elseif (isset($context['cssSelector'])) {
-            // warnings generated by invalid/unrecognized selectors in method existsMatchForCssSelector()
-            $selector = $context['cssSelector'];
-        }
-
-        if ($selector !== '') {
-            throw new \InvalidArgumentException(
-                \sprintf('%1$s in selector >> %2$s << in %3$s on line %4$u', $message, $selector, $file, $line),
-                1509279985
-            );
-        }
-
-        // Catches eventual warnings generated by method getAllNodesWithStyleAttribute()
-        if (isset($context['xPath'])) {
-            throw new \RuntimeException(
-                \sprintf('%1$s in %2$s on line %3$u', $message, $file, $line),
-                1509280067
-            );
-        }
-
-        // the normal error handling continues when handler return false
-        return false;
-    }
-
-    /**
-     * Sets the debug mode.
-     *
-     * @param bool $debug set to true to enable debug mode
-     *
-     * @return void
-     */
-    public function setDebug($debug)
-    {
-        $this->debug = $debug;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/CssConcatenator.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/CssConcatenator.php
deleted file mode 100644 (file)
index 3ebd33b..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-<?php
-
-namespace Pelago\Emogrifier;
-
-/**
- * Facilitates building a CSS string by appending rule blocks one at a time, checking whether the media query,
- * selectors, or declarations block are the same as those from the preceding block and combining blocks in such cases.
- *
- * Example:
- *  $concatenator = new CssConcatenator();
- *  $concatenator->append(['body'], 'color: blue;');
- *  $concatenator->append(['body'], 'font-size: 16px;');
- *  $concatenator->append(['p'], 'margin: 1em 0;');
- *  $concatenator->append(['ul', 'ol'], 'margin: 1em 0;');
- *  $concatenator->append(['body'], 'font-size: 14px;', '@media screen and (max-width: 400px)');
- *  $concatenator->append(['ul', 'ol'], 'margin: 0.75em 0;', '@media screen and (max-width: 400px)');
- *  $css = $concatenator->getCss();
- *
- * `$css` (if unminified) would contain the following CSS:
- * ` body {
- * `   color: blue;
- * `   font-size: 16px;
- * ` }
- * ` p, ul, ol {
- * `   margin: 1em 0;
- * ` }
- * ` @media screen and (max-width: 400px) {
- * `   body {
- * `     font-size: 14px;
- * `   }
- * `   ul, ol {
- * `     margin: 0.75em 0;
- * `   }
- * ` }
- *
- * @author Jake Hotson <jake.github@qzdesign.co.uk>
- */
-class CssConcatenator
-{
-    /**
-     * Array of media rules in order.  Each element is an object with the following properties:
-     * - string `media` - The media query string, e.g. "@media screen and (max-width:639px)", or an empty string for
-     *   rules not within a media query block;
-     * - \stdClass[] `ruleBlocks` - Array of rule blocks in order, where each element is an object with the following
-     *   properties:
-     *   - mixed[] `selectorsAsKeys` - Array whose keys are selectors for the rule block (values are of no
-     *     significance);
-     *   - string `declarationsBlock` - The property declarations, e.g. "margin-top: 0.5em; padding: 0".
-     *
-     * @var \stdClass[]
-     */
-    private $mediaRules = [];
-
-    /**
-     * Appends a declaration block to the CSS.
-     *
-     * @param string[] $selectors Array of selectors for the rule, e.g. ["ul", "ol", "p:first-child"].
-     * @param string $declarationsBlock The property declarations, e.g. "margin-top: 0.5em; padding: 0".
-     * @param string $media The media query for the rule, e.g. "@media screen and (max-width:639px)",
-     *                      or an empty string if none.
-     */
-    public function append(array $selectors, $declarationsBlock, $media = '')
-    {
-        $selectorsAsKeys = \array_flip($selectors);
-
-        $mediaRule = $this->getOrCreateMediaRuleToAppendTo($media);
-        $lastRuleBlock = \end($mediaRule->ruleBlocks);
-
-        $hasSameDeclarationsAsLastRule = $lastRuleBlock !== false
-            && $declarationsBlock === $lastRuleBlock->declarationsBlock;
-        if ($hasSameDeclarationsAsLastRule) {
-            $lastRuleBlock->selectorsAsKeys += $selectorsAsKeys;
-        } else {
-            $hasSameSelectorsAsLastRule = $lastRuleBlock !== false
-                && static::hasEquivalentSelectors($selectorsAsKeys, $lastRuleBlock->selectorsAsKeys);
-            if ($hasSameSelectorsAsLastRule) {
-                $lastDeclarationsBlockWithoutSemicolon = \rtrim(\rtrim($lastRuleBlock->declarationsBlock), ';');
-                $lastRuleBlock->declarationsBlock = $lastDeclarationsBlockWithoutSemicolon . ';' . $declarationsBlock;
-            } else {
-                $mediaRule->ruleBlocks[] = (object)\compact('selectorsAsKeys', 'declarationsBlock');
-            }
-        }
-    }
-
-    /**
-     * @return string
-     */
-    public function getCss()
-    {
-        return \implode('', \array_map([$this, 'getMediaRuleCss'], $this->mediaRules));
-    }
-
-    /**
-     * @param string $media The media query for rules to be appended, e.g. "@media screen and (max-width:639px)",
-     *                      or an empty string if none.
-     *
-     * @return \stdClass Object with properties as described for elements of `$mediaRules`.
-     */
-    private function getOrCreateMediaRuleToAppendTo($media)
-    {
-        $lastMediaRule = \end($this->mediaRules);
-        if ($lastMediaRule !== false && $media === $lastMediaRule->media) {
-            return $lastMediaRule;
-        }
-
-        $newMediaRule = (object)[
-            'media' => $media,
-            'ruleBlocks' => [],
-        ];
-        $this->mediaRules[] = $newMediaRule;
-        return $newMediaRule;
-    }
-
-    /**
-     * Tests if two sets of selectors are equivalent (i.e. the same selectors, possibly in a different order).
-     *
-     * @param mixed[] $selectorsAsKeys1 Array in which the selectors are the keys, and the values are of no
-     *                                  significance.
-     * @param mixed[] $selectorsAsKeys2 Another such array.
-     *
-     * @return bool
-     */
-    private static function hasEquivalentSelectors(array $selectorsAsKeys1, array $selectorsAsKeys2)
-    {
-        return \count($selectorsAsKeys1) === \count($selectorsAsKeys2)
-            && \count($selectorsAsKeys1) === \count($selectorsAsKeys1 + $selectorsAsKeys2);
-    }
-
-    /**
-     * @param \stdClass $mediaRule Object with properties as described for elements of `$mediaRules`.
-     *
-     * @return string CSS for the media rule.
-     */
-    private static function getMediaRuleCss(\stdClass $mediaRule)
-    {
-        $css = \implode('', \array_map([static::class, 'getRuleBlockCss'], $mediaRule->ruleBlocks));
-        if ($mediaRule->media !== '') {
-            $css = $mediaRule->media . '{' . $css . '}';
-        }
-        return $css;
-    }
-
-    /**
-     * @param \stdClass $ruleBlock Object with properties as described for elements of the `ruleBlocks` property of
-     *                            elements of `$mediaRules`.
-     *
-     * @return string CSS for the rule block.
-     */
-    private static function getRuleBlockCss(\stdClass $ruleBlock)
-    {
-        $selectors = \array_keys($ruleBlock->selectorsAsKeys);
-        return \implode(',', $selectors) . '{' . $ruleBlock->declarationsBlock . '}';
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/CssInliner.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/CssInliner.php
deleted file mode 100644 (file)
index 847767e..0000000
+++ /dev/null
@@ -1,1346 +0,0 @@
-<?php
-
-namespace Pelago\Emogrifier;
-
-use Symfony\Component\CssSelector\CssSelectorConverter;
-use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
-
-/**
- * This class provides functions for converting CSS styles into inline style attributes in your HTML code.
- *
- * For Emogrifier 3.0.0, this will be the successor to the \Pelago\Emogrifier class (which then will be deprecated).
- *
- * For more information, please see the README.md file.
- *
- * @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
- *
- * @author Cameron Brooks
- * @author Jaime Prado
- * @author Oliver Klee <github@oliverklee.de>
- * @author Roman Ožana <ozana@omdesign.cz>
- * @author Sander Kruger <s.kruger@invessel.com>
- * @author Zoli Szabó <zoli.szabo+github@gmail.com>
- */
-class CssInliner
-{
-    /**
-     * @var int
-     */
-    const CACHE_KEY_CSS = 0;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_SELECTOR = 1;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 2;
-
-    /**
-     * @var int
-     */
-    const CACHE_KEY_COMBINED_STYLES = 3;
-
-    /**
-     * Regular expression component matching a static pseudo class in a selector, without the preceding ":",
-     * for which the applicable elements can be determined (by converting the selector to an XPath expression).
-     * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead
-     * group, as appropriate for the usage context.)
-     *
-     * @var string
-     */
-    const PSEUDO_CLASS_MATCHER = '\\S+\\-(?:child|type\\()|not\\([[:ascii:]]*\\)';
-
-    /**
-     * @var string
-     */
-    const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
-
-    /**
-     * @var string
-     */
-    const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
-
-    /**
-     * @var \DOMDocument
-     */
-    protected $domDocument = null;
-
-    /**
-     * @var string
-     */
-    private $css = '';
-
-    /**
-     * @var bool[]
-     */
-    private $excludedSelectors = [];
-
-    /**
-     * @var string[]
-     */
-    private $unprocessableHtmlTags = ['wbr'];
-
-    /**
-     * @var bool[]
-     */
-    private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true];
-
-    /**
-     * @var mixed[]
-     */
-    private $caches = [
-        self::CACHE_KEY_CSS => [],
-        self::CACHE_KEY_SELECTOR => [],
-        self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
-        self::CACHE_KEY_COMBINED_STYLES => [],
-    ];
-
-    /**
-     * @var CssSelectorConverter
-     */
-    private $cssSelectorConverter = null;
-
-    /**
-     * the visited nodes with the XPath paths as array keys
-     *
-     * @var \DOMElement[]
-     */
-    private $visitedNodes = [];
-
-    /**
-     * the styles to apply to the nodes with the XPath paths as array keys for the outer array
-     * and the attribute names/values as key/value pairs for the inner array
-     *
-     * @var string[][]
-     */
-    private $styleAttributesForNodes = [];
-
-    /**
-     * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
-     * If set to false, the value of the style attributes will be discarded.
-     *
-     * @var bool
-     */
-    private $isInlineStyleAttributesParsingEnabled = true;
-
-    /**
-     * Determines whether the <style> blocks in the HTML passed to this class should be parsed.
-     *
-     * If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML
-     * via inline styles.
-     *
-     * If set to false, the <style> blocks will be left as they are in the HTML.
-     *
-     * @var bool
-     */
-    private $isStyleBlocksParsingEnabled = true;
-
-    /**
-     * Determines whether elements with the `display: none` property are
-     * removed from the DOM.
-     *
-     * @var bool
-     */
-    private $shouldRemoveInvisibleNodes = true;
-
-    /**
-     * For calculating selector precedence order.
-     * Keys are a regular expression part to match before a CSS name.
-     * Values are a multiplier factor per match to weight specificity.
-     *
-     * @var int[]
-     */
-    private $selectorPrecedenceMatchers = [
-        // IDs: worth 10000
-        '\\#' => 10000,
-        // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100
-        '(?:\\.|\\[|(?<!:):(?!not\\())' => 100,
-        // elements (not attribute values or `:not`), pseudo-elements: worth 1
-        '(?:(?<![="\':\\w\\-])|::)' => 1,
-    ];
-
-    /**
-     * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them.
-     *
-     * @var bool
-     */
-    private $debug = false;
-
-    /**
-     * @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty
-     *
-     * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
-     */
-    public function __construct($unprocessedHtml)
-    {
-        if (!\is_string($unprocessedHtml)) {
-            throw new \InvalidArgumentException('The provided HTML must be a string.', 1540403176);
-        }
-        if ($unprocessedHtml === '') {
-            throw new \InvalidArgumentException('The provided HTML must not be empty.', 1540403181);
-        }
-
-        $this->cssSelectorConverter = new CssSelectorConverter();
-
-        $this->setHtml($unprocessedHtml);
-    }
-
-    /**
-     * Sets the HTML to process.
-     *
-     * @param string $html the HTML to process, must be UTF-8-encoded
-     *
-     * @return void
-     */
-    private function setHtml($html)
-    {
-        $this->createUnifiedDomDocument($html);
-    }
-
-    /**
-     * Provides access to the internal DOMDocument representation of the HTML in its current state.
-     *
-     * @return \DOMDocument
-     */
-    public function getDomDocument()
-    {
-        return $this->domDocument;
-    }
-
-    /**
-     * Sets the CSS to merge with the HTML.
-     *
-     * @param string $css the CSS to merge, must be UTF-8-encoded
-     *
-     * @return void
-     */
-    public function setCss($css)
-    {
-        $this->css = $css;
-    }
-
-    /**
-     * Renders the normalized and processed HTML.
-     *
-     * @return string
-     */
-    public function render()
-    {
-        return $this->domDocument->saveHTML();
-    }
-
-    /**
-     * Renders the content of the BODY element of the normalized and processed HTML.
-     *
-     * @return string
-     */
-    public function renderBodyContent()
-    {
-        $bodyNodeHtml = $this->domDocument->saveHTML($this->getBodyElement());
-
-        return \str_replace(['<body>', '</body>'], '', $bodyNodeHtml);
-    }
-
-    /**
-     * Returns the BODY element.
-     *
-     * This method assumes that there always is a BODY element.
-     *
-     * @return \DOMElement
-     */
-    private function getBodyElement()
-    {
-        return $this->domDocument->getElementsByTagName('body')->item(0);
-    }
-
-    /**
-     * Returns the HEAD element.
-     *
-     * This method assumes that there always is a HEAD element.
-     *
-     * @return \DOMElement
-     */
-    private function getHeadElement()
-    {
-        return $this->domDocument->getElementsByTagName('head')->item(0);
-    }
-
-    /**
-     * Applies $this->css to the given HTML and returns the HTML with the CSS
-     * applied.
-     *
-     * This method places the CSS inline.
-     *
-     * @return string
-     *
-     * @throws SyntaxErrorException
-     */
-    public function emogrify()
-    {
-        $this->process();
-
-        return $this->render();
-    }
-
-    /**
-     * Applies $this->css to the given HTML and returns only the HTML content
-     * within the <body> tag.
-     *
-     * This method places the CSS inline.
-     *
-     * @return string
-     *
-     * @throws SyntaxErrorException
-     */
-    public function emogrifyBodyContent()
-    {
-        $this->process();
-
-        return $this->renderBodyContent();
-    }
-
-    /**
-     * Creates a DOM document from the given HTML and stores it in $this->domDocument.
-     *
-     * The DOM document will always have a BODY element and a document type.
-     *
-     * @param string $html
-     *
-     * @return void
-     */
-    private function createUnifiedDomDocument($html)
-    {
-        $this->createRawDomDocument($html);
-        $this->ensureExistenceOfBodyElement();
-    }
-
-    /**
-     * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
-     *
-     * @param string $html
-     *
-     * @return void
-     */
-    private function createRawDomDocument($html)
-    {
-        $domDocument = new \DOMDocument();
-        $domDocument->encoding = 'UTF-8';
-        $domDocument->strictErrorChecking = false;
-        $domDocument->formatOutput = true;
-        $libXmlState = \libxml_use_internal_errors(true);
-        $domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
-        \libxml_clear_errors();
-        \libxml_use_internal_errors($libXmlState);
-        $domDocument->normalizeDocument();
-
-        $this->domDocument = $domDocument;
-    }
-
-    /**
-     * Returns the HTML with added document type and Content-Type meta tag if needed,
-     * ensuring that the HTML will be good for creating a DOM document from it.
-     *
-     * @param string $html
-     *
-     * @return string the unified HTML
-     */
-    private function prepareHtmlForDomConversion($html)
-    {
-        $htmlWithDocumentType = $this->ensureDocumentType($html);
-
-        return $this->addContentTypeMetaTag($htmlWithDocumentType);
-    }
-
-    /**
-     * Applies $this->css to $this->domDocument.
-     *
-     * This method places the CSS inline.
-     *
-     * @return void
-     *
-     * @throws SyntaxErrorException
-     */
-    protected function process()
-    {
-        $this->clearAllCaches();
-        $this->purgeVisitedNodes();
-
-        $xPath = new \DOMXPath($this->domDocument);
-        $this->removeUnprocessableTags();
-        $this->normalizeStyleAttributesOfAllNodes($xPath);
-
-        // grab any existing style blocks from the html and append them to the existing CSS
-        // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
-        $allCss = $this->css;
-        if ($this->isStyleBlocksParsingEnabled) {
-            $allCss .= $this->getCssFromAllStyleNodes($xPath);
-        }
-
-        $excludedNodes = $this->getNodesToExclude($xPath);
-        $cssRules = $this->parseCssRules($allCss);
-        foreach ($cssRules['inlineable'] as $cssRule) {
-            try {
-                $nodesMatchingCssSelectors = $xPath->query($this->cssSelectorConverter->toXPath($cssRule['selector']));
-            } catch (SyntaxErrorException $e) {
-                if ($this->debug) {
-                    throw $e;
-                }
-                continue;
-            }
-
-            /** @var \DOMElement $node */
-            foreach ($nodesMatchingCssSelectors as $node) {
-                if (\in_array($node, $excludedNodes, true)) {
-                    continue;
-                }
-                $this->copyInlineableCssToStyleAttribute($node, $cssRule);
-            }
-        }
-
-        if ($this->isInlineStyleAttributesParsingEnabled) {
-            $this->fillStyleAttributesWithMergedStyles();
-        }
-        $this->postProcess($xPath);
-
-        $this->removeImportantAnnotationFromAllInlineStyles($xPath);
-
-        $this->copyUninlineableCssToStyleNode($xPath, $cssRules['uninlineable']);
-    }
-
-    /**
-     * Applies some optional post-processing to the HTML in the DOM document.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function postProcess(\DOMXPath $xPath)
-    {
-        if ($this->shouldRemoveInvisibleNodes) {
-            $this->removeInvisibleNodes($xPath);
-        }
-    }
-
-    /**
-     * Searches for all nodes with a style attribute and removes the "!important" annotations out of
-     * the inline style declarations, eventually by rearranging declarations.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function removeImportantAnnotationFromAllInlineStyles(\DOMXPath $xPath)
-    {
-        foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
-            $this->removeImportantAnnotationFromNodeInlineStyle($node);
-        }
-    }
-
-    /**
-     * Removes the "!important" annotations out of the inline style declarations,
-     * eventually by rearranging declarations.
-     * Rearranging needed when !important shorthand properties are followed by some of their
-     * not !important expanded-version properties.
-     * For example "font: 12px serif !important; font-size: 13px;" must be reordered
-     * to "font-size: 13px; font: 12px serif;" in order to remain correct.
-     *
-     * @param \DOMElement $node
-     *
-     * @return void
-     */
-    private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node)
-    {
-        $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-        $regularStyleDeclarations = [];
-        $importantStyleDeclarations = [];
-        foreach ($inlineStyleDeclarations as $property => $value) {
-            if ($this->attributeValueIsImportant($value)) {
-                $importantStyleDeclarations[$property] = \trim(\str_replace('!important', '', $value));
-            } else {
-                $regularStyleDeclarations[$property] = $value;
-            }
-        }
-        $inlineStyleDeclarationsInNewOrder = \array_merge(
-            $regularStyleDeclarations,
-            $importantStyleDeclarations
-        );
-        $node->setAttribute(
-            'style',
-            $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder)
-        );
-    }
-
-    /**
-     * Returns a list with all DOM nodes that have a style attribute.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return \DOMNodeList
-     */
-    private function getAllNodesWithStyleAttribute(\DOMXPath $xPath)
-    {
-        return $xPath->query('//*[@style]');
-    }
-
-    /**
-     * Extracts and parses the individual rules from a CSS string.
-     *
-     * @param string $css a string of raw CSS code
-     *
-     * @return string[][][] A 2-entry array with the key "inlineable" containing rules which can be inlined as `style`
-     *         attributes and the key "uninlineable" containing rules which cannot.  Each value is an array of string
-     *         sub-arrays with the keys
-     *         "media" (the media query string, e.g. "@media screen and (max-width: 480px)",
-     *         or an empty string if not from a `@media` rule),
-     *         "selector" (the CSS selector, e.g., "*" or "header h1"),
-     *         "hasUnmatchablePseudo" (true if that selector contains psuedo-elements or dynamic pseudo-classes
-     *         such that the declarations cannot be applied inline),
-     *         "declarationsBlock" (the semicolon-separated CSS declarations for that selector,
-     *         e.g., "color: red; height: 4px;"),
-     *         and "line" (the line number e.g. 42)
-     */
-    private function parseCssRules($css)
-    {
-        $cssKey = \md5($css);
-        if (!isset($this->caches[static::CACHE_KEY_CSS][$cssKey])) {
-            $matches = $this->getCssRuleMatches($css);
-
-            $cssRules = [
-                'inlineable' => [],
-                'uninlineable' => [],
-            ];
-            /** @var string[][] $matches */
-            /** @var string[] $cssRule */
-            foreach ($matches as $key => $cssRule) {
-                $cssDeclaration = \trim($cssRule['declarations']);
-                if ($cssDeclaration === '') {
-                    continue;
-                }
-
-                $selectors = \explode(',', $cssRule['selectors']);
-                foreach ($selectors as $selector) {
-                    // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
-                    // only allow structural pseudo-classes
-                    $hasPseudoElement = \strpos($selector, '::') !== false;
-                    $hasUnsupportedPseudoClass = (bool)\preg_match(
-                        '/:(?!' . static::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i',
-                        $selector
-                    );
-                    $hasUnmatchablePseudo = $hasPseudoElement || $hasUnsupportedPseudoClass;
-
-                    $parsedCssRule = [
-                        'media' => $cssRule['media'],
-                        'selector' => \trim($selector),
-                        'hasUnmatchablePseudo' => $hasUnmatchablePseudo,
-                        'declarationsBlock' => $cssDeclaration,
-                        // keep track of where it appears in the file, since order is important
-                        'line' => $key,
-                    ];
-                    $ruleType = ($cssRule['media'] === '' && !$hasUnmatchablePseudo) ? 'inlineable' : 'uninlineable';
-                    $cssRules[$ruleType][] = $parsedCssRule;
-                }
-            }
-
-            \usort($cssRules['inlineable'], [$this, 'sortBySelectorPrecedence']);
-
-            $this->caches[static::CACHE_KEY_CSS][$cssKey] = $cssRules;
-        }
-
-        return $this->caches[static::CACHE_KEY_CSS][$cssKey];
-    }
-
-    /**
-     * Parses a string of CSS into the media query, selectors and declarations for each ruleset in order.
-     *
-     * @param string $css
-     *
-     * @return string[][] Array of string sub-arrays with the keys
-     *         "media" (the media query string, e.g. "@media screen and (max-width: 480px)",
-     *         or an empty string if not from an `@media` rule),
-     *         "selectors" (the CSS selector(s), e.g., "*" or "h1, h2"),
-     *         "declarations" (the semicolon-separated CSS declarations for that/those selector(s),
-     *         e.g., "color: red; height: 4px;"),
-     */
-    private function getCssRuleMatches($css)
-    {
-        $ruleMatches = [];
-
-        $splitCss = $this->splitCssAndMediaQuery($css);
-        foreach ($splitCss as $cssPart) {
-            // process each part for selectors and definitions
-            \preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $cssPart['css'], $matches, PREG_SET_ORDER);
-
-            /** @var string[][] $matches */
-            foreach ($matches as $cssRule) {
-                $ruleMatches[] = [
-                    'media' => $cssPart['media'],
-                    'selectors' => $cssRule[1],
-                    'declarations' => $cssRule[2],
-                ];
-            }
-        }
-
-        return $ruleMatches;
-    }
-
-    /**
-     * Disables the parsing of inline styles.
-     *
-     * @return void
-     */
-    public function disableInlineStyleAttributesParsing()
-    {
-        $this->isInlineStyleAttributesParsingEnabled = false;
-    }
-
-    /**
-     * Disables the parsing of <style> blocks.
-     *
-     * @return void
-     */
-    public function disableStyleBlocksParsing()
-    {
-        $this->isStyleBlocksParsingEnabled = false;
-    }
-
-    /**
-     * Disables the removal of elements with `display: none` properties.
-     *
-     * @deprecated will be removed in Emogrifier 3.0
-     *
-     * @return void
-     */
-    public function disableInvisibleNodeRemoval()
-    {
-        $this->shouldRemoveInvisibleNodes = false;
-    }
-
-    /**
-     * Clears all caches.
-     *
-     * @return void
-     */
-    private function clearAllCaches()
-    {
-        $this->caches = [
-            static::CACHE_KEY_CSS => [],
-            static::CACHE_KEY_SELECTOR => [],
-            static::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
-            static::CACHE_KEY_COMBINED_STYLES => [],
-        ];
-    }
-
-    /**
-     * Purges the visited nodes.
-     *
-     * @return void
-     */
-    private function purgeVisitedNodes()
-    {
-        $this->visitedNodes = [];
-        $this->styleAttributesForNodes = [];
-    }
-
-    /**
-     * Marks a tag for removal.
-     *
-     * There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them.
-     * In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document.
-     *
-     * Note: The tags will not be removed if they have any content.
-     *
-     * @param string $tagName the tag name, e.g., "p"
-     *
-     * @return void
-     */
-    public function addUnprocessableHtmlTag($tagName)
-    {
-        $this->unprocessableHtmlTags[] = $tagName;
-    }
-
-    /**
-     * Drops a tag from the removal list.
-     *
-     * @param string $tagName the tag name, e.g., "p"
-     *
-     * @return void
-     */
-    public function removeUnprocessableHtmlTag($tagName)
-    {
-        $key = \array_search($tagName, $this->unprocessableHtmlTags, true);
-        if ($key !== false) {
-            unset($this->unprocessableHtmlTags[$key]);
-        }
-    }
-
-    /**
-     * Marks a media query type to keep.
-     *
-     * @param string $mediaName the media type name, e.g., "braille"
-     *
-     * @return void
-     */
-    public function addAllowedMediaType($mediaName)
-    {
-        $this->allowedMediaTypes[$mediaName] = true;
-    }
-
-    /**
-     * Drops a media query type from the allowed list.
-     *
-     * @param string $mediaName the tag name, e.g., "braille"
-     *
-     * @return void
-     */
-    public function removeAllowedMediaType($mediaName)
-    {
-        if (isset($this->allowedMediaTypes[$mediaName])) {
-            unset($this->allowedMediaTypes[$mediaName]);
-        }
-    }
-
-    /**
-     * Adds a selector to exclude nodes from emogrification.
-     *
-     * Any nodes that match the selector will not have their style altered.
-     *
-     * @param string $selector the selector to exclude, e.g., ".editor"
-     *
-     * @return void
-     */
-    public function addExcludedSelector($selector)
-    {
-        $this->excludedSelectors[$selector] = true;
-    }
-
-    /**
-     * No longer excludes the nodes matching this selector from emogrification.
-     *
-     * @param string $selector the selector to no longer exclude, e.g., ".editor"
-     *
-     * @return void
-     */
-    public function removeExcludedSelector($selector)
-    {
-        if (isset($this->excludedSelectors[$selector])) {
-            unset($this->excludedSelectors[$selector]);
-        }
-    }
-
-    /**
-     * This removes styles from your email that contain display:none.
-     * We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only
-     * supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase,
-     * not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE")
-     * to lowercase.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function removeInvisibleNodes(\DOMXPath $xPath)
-    {
-        $nodesWithStyleDisplayNone = $xPath->query(
-            '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'
-        );
-        if ($nodesWithStyleDisplayNone->length === 0) {
-            return;
-        }
-
-        // The checks on parentNode and is_callable below ensure that if we've deleted the parent node,
-        // we don't try to call removeChild on a nonexistent child node
-        /** @var \DOMNode $node */
-        foreach ($nodesWithStyleDisplayNone as $node) {
-            if ($node->parentNode && \is_callable([$node->parentNode, 'removeChild'])) {
-                $node->parentNode->removeChild($node);
-            }
-        }
-    }
-
-    /**
-     * Parses the document and normalizes all existing CSS attributes.
-     * This changes 'DISPLAY: none' to 'display: none'.
-     * We wouldn't have to do this if DOMXPath supported XPath 2.0.
-     * Also stores a reference of nodes with existing inline styles so we don't overwrite them.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return void
-     */
-    private function normalizeStyleAttributesOfAllNodes(\DOMXPath $xPath)
-    {
-        /** @var \DOMElement $node */
-        foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
-            if ($this->isInlineStyleAttributesParsingEnabled) {
-                $this->normalizeStyleAttributes($node);
-            }
-            // Remove style attribute in every case, so we can add them back (if inline style attributes
-            // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules;
-            // else original inline style rules may remain at the beginning of the final inline style definition
-            // of a node, which may give not the desired results
-            $node->removeAttribute('style');
-        }
-    }
-
-    /**
-     * Normalizes the value of the "style" attribute and saves it.
-     *
-     * @param \DOMElement $node
-     *
-     * @return void
-     */
-    private function normalizeStyleAttributes(\DOMElement $node)
-    {
-        $normalizedOriginalStyle = \preg_replace_callback(
-            '/[A-z\\-]+(?=\\:)/S',
-            function (array $m) {
-                return \strtolower($m[0]);
-            },
-            $node->getAttribute('style')
-        );
-
-        // in order to not overwrite existing style attributes in the HTML, we
-        // have to save the original HTML styles
-        $nodePath = $node->getNodePath();
-        if (!isset($this->styleAttributesForNodes[$nodePath])) {
-            $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle);
-            $this->visitedNodes[$nodePath] = $node;
-        }
-
-        $node->setAttribute('style', $normalizedOriginalStyle);
-    }
-
-    /**
-     * Merges styles from styles attributes and style nodes and applies them to the attribute nodes
-     *
-     * @return void
-     */
-    private function fillStyleAttributesWithMergedStyles()
-    {
-        foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
-            $node = $this->visitedNodes[$nodePath];
-            $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-            $node->setAttribute(
-                'style',
-                $this->generateStyleStringFromDeclarationsArrays(
-                    $currentStyleAttributes,
-                    $styleAttributesForNode
-                )
-            );
-        }
-    }
-
-    /**
-     * This method merges old or existing name/value array with new name/value array
-     * and then generates a string of the combined style suitable for placing inline.
-     * This becomes the single point for CSS string generation allowing for consistent
-     * CSS output no matter where the CSS originally came from.
-     *
-     * @param string[] $oldStyles
-     * @param string[] $newStyles
-     *
-     * @return string
-     */
-    private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles)
-    {
-        $cacheKey = \serialize([$oldStyles, $newStyles]);
-        if (isset($this->caches[static::CACHE_KEY_COMBINED_STYLES][$cacheKey])) {
-            return $this->caches[static::CACHE_KEY_COMBINED_STYLES][$cacheKey];
-        }
-
-        // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed
-        foreach ($oldStyles as $attributeName => $attributeValue) {
-            if (!isset($newStyles[$attributeName])) {
-                continue;
-            }
-
-            $newAttributeValue = $newStyles[$attributeName];
-            if ($this->attributeValueIsImportant($attributeValue)
-                && !$this->attributeValueIsImportant($newAttributeValue)
-            ) {
-                unset($newStyles[$attributeName]);
-            } else {
-                unset($oldStyles[$attributeName]);
-            }
-        }
-
-        $combinedStyles = \array_merge($oldStyles, $newStyles);
-
-        $style = '';
-        foreach ($combinedStyles as $attributeName => $attributeValue) {
-            $style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; ';
-        }
-        $trimmedStyle = \rtrim($style);
-
-        $this->caches[static::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle;
-
-        return $trimmedStyle;
-    }
-
-    /**
-     * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array.
-     *
-     * @param string[] $styleDeclarations
-     *
-     * @return string
-     */
-    private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations)
-    {
-        return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations);
-    }
-
-    /**
-     * Checks whether $attributeValue is marked as !important.
-     *
-     * @param string $attributeValue
-     *
-     * @return bool
-     */
-    private function attributeValueIsImportant($attributeValue)
-    {
-        return \strtolower(\substr(\trim($attributeValue), -10)) === '!important';
-    }
-
-    /**
-     * Applies $cssRules to $this->domDocument, limited to the rules that actually apply to the document.
-     *
-     * @param \DOMXPath $xPath
-     * @param string[][] $cssRules The "uninlineable" array of CSS rules returned by `parseCssRules`
-     *
-     * @return void
-     */
-    private function copyUninlineableCssToStyleNode(\DOMXPath $xPath, array $cssRules)
-    {
-        $cssRulesRelevantForDocument = \array_filter(
-            $cssRules,
-            function (array $cssRule) use ($xPath) {
-                $selector = $cssRule['selector'];
-                if ($cssRule['hasUnmatchablePseudo']) {
-                    $selector = $this->removeUnmatchablePseudoComponents($selector);
-                }
-                return $this->existsMatchForCssSelector($xPath, $selector);
-            }
-        );
-
-        if ($cssRulesRelevantForDocument === []) {
-            // avoid adding empty style element (or including unneeded class dependency)
-            return;
-        }
-
-        $cssConcatenator = new CssConcatenator();
-        foreach ($cssRulesRelevantForDocument as $cssRule) {
-            $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']);
-        }
-
-        $this->addStyleElementToDocument($cssConcatenator->getCss());
-    }
-
-    /**
-     * Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary.
-     *
-     * @param string $selector
-     *
-     * @return string Selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply,
-     *                or in the case of pseudo-elements will match their originating element.
-     */
-    private function removeUnmatchablePseudoComponents($selector)
-    {
-        $pseudoComponentMatcher = ':(?!' . static::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+';
-        return \preg_replace(
-            ['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'],
-            ['$1*', ''],
-            $selector
-        );
-    }
-
-    /**
-     * Copies $cssRule into the style attribute of $node.
-     *
-     * Note: This method does not check whether $cssRule matches $node.
-     *
-     * @param \DOMElement $node
-     * @param string[][] $cssRule
-     *
-     * @return void
-     */
-    private function copyInlineableCssToStyleAttribute(\DOMElement $node, array $cssRule)
-    {
-        // if it has a style attribute, get it, process it, and append (overwrite) new stuff
-        if ($node->hasAttribute('style')) {
-            // break it up into an associative array
-            $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-        } else {
-            $oldStyleDeclarations = [];
-        }
-        $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']);
-        $node->setAttribute(
-            'style',
-            $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)
-        );
-    }
-
-    /**
-     * Checks whether there is at least one matching element for $cssSelector.
-     * When not in debug mode, it returns true also for invalid selectors (because they may be valid,
-     * just not implemented/recognized yet by Emogrifier).
-     *
-     * @param \DOMXPath $xPath
-     * @param string $cssSelector
-     *
-     * @return bool
-     *
-     * @throws SyntaxErrorException
-     */
-    private function existsMatchForCssSelector(\DOMXPath $xPath, $cssSelector)
-    {
-        try {
-            $nodesMatchingSelector = $xPath->query($this->cssSelectorConverter->toXPath($cssSelector));
-        } catch (SyntaxErrorException $e) {
-            if ($this->debug) {
-                throw $e;
-            }
-            return true;
-        }
-
-        return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
-    }
-
-    /**
-     * Returns CSS content.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return string
-     */
-    private function getCssFromAllStyleNodes(\DOMXPath $xPath)
-    {
-        $styleNodes = $xPath->query('//style');
-
-        if ($styleNodes === false) {
-            return '';
-        }
-
-        $css = '';
-        /** @var \DOMNode $styleNode */
-        foreach ($styleNodes as $styleNode) {
-            $css .= "\n\n" . $styleNode->nodeValue;
-            $styleNode->parentNode->removeChild($styleNode);
-        }
-
-        return $css;
-    }
-
-    /**
-     * Adds a style element with $css to $this->domDocument.
-     *
-     * This method is protected to allow overriding.
-     *
-     * @see https://github.com/jjriv/emogrifier/issues/103
-     *
-     * @param string $css
-     *
-     * @return void
-     */
-    protected function addStyleElementToDocument($css)
-    {
-        $styleElement = $this->domDocument->createElement('style', $css);
-        $styleAttribute = $this->domDocument->createAttribute('type');
-        $styleAttribute->value = 'text/css';
-        $styleElement->appendChild($styleAttribute);
-
-        $headElement = $this->getHeadElement();
-        $headElement->appendChild($styleElement);
-    }
-
-    /**
-     * Checks that $this->domDocument has a BODY element and adds it if it is missing.
-     *
-     * @return void
-     */
-    private function ensureExistenceOfBodyElement()
-    {
-        if ($this->domDocument->getElementsByTagName('body')->item(0) !== null) {
-            return;
-        }
-
-        $htmlElement = $this->domDocument->getElementsByTagName('html')->item(0);
-        $htmlElement->appendChild($this->domDocument->createElement('body'));
-    }
-
-    /**
-     * Splits input CSS code into an array of parts for different media querues, in order.
-     * Each part is an array where:
-     *
-     * - key "css" will contain clean CSS code (for @media rules this will be the group rule body within "{...}")
-     * - key "media" will contain "@media " followed by the media query list, for all allowed media queries,
-     *   or an empty string for CSS not within a media query
-     *
-     * Example:
-     *
-     * The CSS code
-     *
-     *   "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
-     *
-     * will be parsed into the following array:
-     *
-     *   0 => [
-     *     "css" => "h1 { color:red; }",
-     *     "media" => ""
-     *   ],
-     *   1 => [
-     *     "css" => " h1 {}",
-     *     "media" => "@media "
-     *   ]
-     *
-     * @param string $css
-     *
-     * @return string[][]
-     */
-    private function splitCssAndMediaQuery($css)
-    {
-        $cssWithoutComments = \preg_replace('/\\/\\*.*\\*\\//sU', '', $css);
-
-        $mediaTypesExpression = '';
-        if (!empty($this->allowedMediaTypes)) {
-            $mediaTypesExpression = '|' . \implode('|', \array_keys($this->allowedMediaTypes));
-        }
-
-        $mediaRuleBodyMatcher = '[^{]*+{(?:[^{}]*+{.*})?\\s*+}\\s*+';
-
-        $cssSplitForAllowedMediaTypes = \preg_split(
-            '#(@media\\s++(?:only\\s++)?+(?:(?=[{\\(])' . $mediaTypesExpression . ')' . $mediaRuleBodyMatcher
-            . ')#misU',
-            $cssWithoutComments,
-            -1,
-            PREG_SPLIT_DELIM_CAPTURE
-        );
-
-        // filter the CSS outside/between allowed @media rules
-        $cssCleaningMatchers = [
-            'import/charset directives' => '/\\s*+@(?:import|charset)\\s[^;]++;/i',
-            'remaining media enclosures' => '/\\s*+@media\\s' . $mediaRuleBodyMatcher . '/isU',
-        ];
-
-        $splitCss = [];
-        foreach ($cssSplitForAllowedMediaTypes as $index => $cssPart) {
-            $isMediaRule = $index % 2 !== 0;
-            if ($isMediaRule) {
-                \preg_match('/^([^{]*+){(.*)}[^}]*+$/s', $cssPart, $matches);
-                $splitCss[] = [
-                    'css' => $matches[2],
-                    'media' => $matches[1],
-                ];
-            } else {
-                $cleanedCss = \trim(\preg_replace($cssCleaningMatchers, '', $cssPart));
-                if ($cleanedCss !== '') {
-                    $splitCss[] = [
-                        'css' => $cleanedCss,
-                        'media' => '',
-                    ];
-                }
-            }
-        }
-        return $splitCss;
-    }
-
-    /**
-     * Removes empty unprocessable tags from the DOM document.
-     *
-     * @return void
-     */
-    private function removeUnprocessableTags()
-    {
-        foreach ($this->unprocessableHtmlTags as $tagName) {
-            $nodes = $this->domDocument->getElementsByTagName($tagName);
-            /** @var \DOMNode $node */
-            foreach ($nodes as $node) {
-                $hasContent = $node->hasChildNodes() || $node->hasChildNodes();
-                if (!$hasContent) {
-                    $node->parentNode->removeChild($node);
-                }
-            }
-        }
-    }
-
-    /**
-     * Makes sure that the passed HTML has a document type.
-     *
-     * @param string $html
-     *
-     * @return string HTML with document type
-     */
-    private function ensureDocumentType($html)
-    {
-        $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false;
-        if ($hasDocumentType) {
-            return $html;
-        }
-
-        return static::DEFAULT_DOCUMENT_TYPE . $html;
-    }
-
-    /**
-     * Adds a Content-Type meta tag for the charset.
-     *
-     * This method also ensures that there is a HEAD element.
-     *
-     * @param string $html
-     *
-     * @return string the HTML with the meta tag added
-     */
-    private function addContentTypeMetaTag($html)
-    {
-        $hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false;
-        if ($hasContentTypeMetaTag) {
-            return $html;
-        }
-
-        // We are trying to insert the meta tag to the right spot in the DOM.
-        // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
-        $hasHeadTag = \stripos($html, '<head') !== false;
-        $hasHtmlTag = \stripos($html, '<html') !== false;
-
-        if ($hasHeadTag) {
-            $reworkedHtml = \preg_replace('/<head(.*?)>/i', '<head$1>' . static::CONTENT_TYPE_META_TAG, $html);
-        } elseif ($hasHtmlTag) {
-            $reworkedHtml = \preg_replace(
-                '/<html(.*?)>/i',
-                '<html$1><head>' . static::CONTENT_TYPE_META_TAG . '</head>',
-                $html
-            );
-        } else {
-            $reworkedHtml = static::CONTENT_TYPE_META_TAG . $html;
-        }
-
-        return $reworkedHtml;
-    }
-
-    /**
-     * @param string[] $a
-     * @param string[] $b
-     *
-     * @return int
-     */
-    private function sortBySelectorPrecedence(array $a, array $b)
-    {
-        $precedenceA = $this->getCssSelectorPrecedence($a['selector']);
-        $precedenceB = $this->getCssSelectorPrecedence($b['selector']);
-
-        // We want these sorted in ascending order so selectors with lesser precedence get processed first and
-        // selectors with greater precedence get sorted last.
-        $precedenceForEquals = ($a['line'] < $b['line'] ? -1 : 1);
-        $precedenceForNotEquals = ($precedenceA < $precedenceB ? -1 : 1);
-        return ($precedenceA === $precedenceB) ? $precedenceForEquals : $precedenceForNotEquals;
-    }
-
-    /**
-     * @param string $selector
-     *
-     * @return int
-     */
-    private function getCssSelectorPrecedence($selector)
-    {
-        $selectorKey = \md5($selector);
-        if (!isset($this->caches[static::CACHE_KEY_SELECTOR][$selectorKey])) {
-            $precedence = 0;
-            foreach ($this->selectorPrecedenceMatchers as $matcher => $value) {
-                if (\trim($selector) === '') {
-                    break;
-                }
-                $number = 0;
-                $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number);
-                $precedence += ($value * $number);
-            }
-            $this->caches[static::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
-        }
-
-        return $this->caches[static::CACHE_KEY_SELECTOR][$selectorKey];
-    }
-
-    /**
-     * Parses a CSS declaration block into property name/value pairs.
-     *
-     * Example:
-     *
-     * The declaration block
-     *
-     *   "color: #000; font-weight: bold;"
-     *
-     * will be parsed into the following array:
-     *
-     *   "color" => "#000"
-     *   "font-weight" => "bold"
-     *
-     * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
-     *
-     * @return string[]
-     *         the CSS declarations with the property names as array keys and the property values as array values
-     */
-    private function parseCssDeclarationsBlock($cssDeclarationsBlock)
-    {
-        if (isset($this->caches[static::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) {
-            return $this->caches[static::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
-        }
-
-        $properties = [];
-        $declarations = \preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
-
-        foreach ($declarations as $declaration) {
-            $matches = [];
-            if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
-                continue;
-            }
-
-            $propertyName = \strtolower($matches[1]);
-            $propertyValue = $matches[2];
-            $properties[$propertyName] = $propertyValue;
-        }
-        $this->caches[static::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties;
-
-        return $properties;
-    }
-
-    /**
-     * Find the nodes that are not to be emogrified.
-     *
-     * @param \DOMXPath $xPath
-     *
-     * @return \DOMElement[]
-     *
-     * @throws SyntaxErrorException
-     */
-    private function getNodesToExclude(\DOMXPath $xPath)
-    {
-        $excludedNodes = [];
-        foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) {
-            try {
-                $matchingNodes = $xPath->query($this->cssSelectorConverter->toXPath($selectorToExclude));
-            } catch (SyntaxErrorException $e) {
-                if ($this->debug) {
-                    throw $e;
-                }
-                continue;
-            }
-            foreach ($matchingNodes as $node) {
-                $excludedNodes[] = $node;
-            }
-        }
-
-        return $excludedNodes;
-    }
-
-    /**
-     * Sets the debug mode.
-     *
-     * @param bool $debug set to true to enable debug mode
-     *
-     * @return void
-     */
-    public function setDebug($debug)
-    {
-        $this->debug = $debug;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php
deleted file mode 100644 (file)
index c5a25ee..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-<?php
-
-namespace Pelago\Emogrifier\HtmlProcessor;
-
-/**
- * Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
- *
- * The "vanilla" subclass is the HtmlNormalizer.
- *
- * @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-abstract class AbstractHtmlProcessor
-{
-    /**
-     * @var string
-     */
-    const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
-
-    /**
-     * @var string
-     */
-    const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
-
-    /**
-     * @var \DOMDocument
-     */
-    protected $domDocument = null;
-
-    /**
-     * @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty
-     *
-     * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
-     */
-    public function __construct($unprocessedHtml)
-    {
-        if (!\is_string($unprocessedHtml)) {
-            throw new \InvalidArgumentException('The provided HTML must be a string.', 1515459744);
-        }
-        if ($unprocessedHtml === '') {
-            throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647);
-        }
-
-        $this->setHtml($unprocessedHtml);
-    }
-
-    /**
-     * Sets the HTML to process.
-     *
-     * @param string $html the HTML to process, must be UTF-8-encoded
-     *
-     * @return void
-     */
-    private function setHtml($html)
-    {
-        $this->createUnifiedDomDocument($html);
-    }
-
-    /**
-     * Provides access to the internal DOMDocument representation of the HTML in its current state.
-     *
-     * @return \DOMDocument
-     */
-    public function getDomDocument()
-    {
-        return $this->domDocument;
-    }
-
-    /**
-     * Renders the normalized and processed HTML.
-     *
-     * @return string
-     */
-    public function render()
-    {
-        return $this->domDocument->saveHTML();
-    }
-
-    /**
-     * Renders the content of the BODY element of the normalized and processed HTML.
-     *
-     * @return string
-     */
-    public function renderBodyContent()
-    {
-        $bodyNodeHtml = $this->domDocument->saveHTML($this->getBodyElement());
-
-        return \str_replace(['<body>', '</body>'], '', $bodyNodeHtml);
-    }
-
-    /**
-     * Returns the BODY element.
-     *
-     * This method assumes that there always is a BODY element.
-     *
-     * @return \DOMElement
-     */
-    private function getBodyElement()
-    {
-        return $this->domDocument->getElementsByTagName('body')->item(0);
-    }
-
-    /**
-     * Creates a DOM document from the given HTML and stores it in $this->domDocument.
-     *
-     * The DOM document will always have a BODY element and a document type.
-     *
-     * @param string $html
-     *
-     * @return void
-     */
-    private function createUnifiedDomDocument($html)
-    {
-        $this->createRawDomDocument($html);
-        $this->ensureExistenceOfBodyElement();
-    }
-
-    /**
-     * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
-     *
-     * @param string $html
-     *
-     * @return void
-     */
-    private function createRawDomDocument($html)
-    {
-        $domDocument = new \DOMDocument();
-        $domDocument->strictErrorChecking = false;
-        $domDocument->formatOutput = true;
-        $libXmlState = \libxml_use_internal_errors(true);
-        $domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
-        \libxml_clear_errors();
-        \libxml_use_internal_errors($libXmlState);
-
-        $this->domDocument = $domDocument;
-    }
-
-    /**
-     * Returns the HTML with added document type and Content-Type meta tag if needed,
-     * ensuring that the HTML will be good for creating a DOM document from it.
-     *
-     * @param string $html
-     *
-     * @return string the unified HTML
-     */
-    private function prepareHtmlForDomConversion($html)
-    {
-        $htmlWithDocumentType = $this->ensureDocumentType($html);
-
-        return $this->addContentTypeMetaTag($htmlWithDocumentType);
-    }
-
-    /**
-     * Makes sure that the passed HTML has a document type.
-     *
-     * @param string $html
-     *
-     * @return string HTML with document type
-     */
-    private function ensureDocumentType($html)
-    {
-        $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false;
-        if ($hasDocumentType) {
-            return $html;
-        }
-
-        return static::DEFAULT_DOCUMENT_TYPE . $html;
-    }
-
-    /**
-     * Adds a Content-Type meta tag for the charset.
-     *
-     * This method also ensures that there is a HEAD element.
-
-     * @param string $html
-     *
-     * @return string the HTML with the meta tag added
-     */
-    private function addContentTypeMetaTag($html)
-    {
-        $hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false;
-        if ($hasContentTypeMetaTag) {
-            return $html;
-        }
-
-        // We are trying to insert the meta tag to the right spot in the DOM.
-        // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
-        $hasHeadTag = \stripos($html, '<head') !== false;
-        $hasHtmlTag = \stripos($html, '<html') !== false;
-
-        if ($hasHeadTag) {
-            $reworkedHtml = \preg_replace('/<head(.*?)>/i', '<head$1>' . static::CONTENT_TYPE_META_TAG, $html);
-        } elseif ($hasHtmlTag) {
-            $reworkedHtml = \preg_replace(
-                '/<html(.*?)>/i',
-                '<html$1><head>' . static::CONTENT_TYPE_META_TAG . '</head>',
-                $html
-            );
-        } else {
-            $reworkedHtml = static::CONTENT_TYPE_META_TAG . $html;
-        }
-
-        return $reworkedHtml;
-    }
-
-    /**
-     * Checks that $this->domDocument has a BODY element and adds it if it is missing.
-     *
-     * @return void
-     */
-    private function ensureExistenceOfBodyElement()
-    {
-        if ($this->domDocument->getElementsByTagName('body')->item(0) !== null) {
-            return;
-        }
-
-        $htmlElement = $this->domDocument->getElementsByTagName('html')->item(0);
-        $htmlElement->appendChild($this->domDocument->createElement('body'));
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php
deleted file mode 100644 (file)
index 97a94b1..0000000
+++ /dev/null
@@ -1,320 +0,0 @@
-<?php
-
-namespace Pelago\Emogrifier\HtmlProcessor;
-
-/**
- * This HtmlProcessor can convert style HTML attributes to the corresponding other visual HTML attributes,
- * e.g. it converts style="width: 100px" to width="100".
- *
- * It will only add attributes, but leaves the style attribute untouched.
- *
- * To trigger the conversion, call the convertCssToVisualAttributes method.
- *
- * @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-class CssToAttributeConverter extends AbstractHtmlProcessor
-{
-    /**
-     * This multi-level array contains simple mappings of CSS properties to
-     * HTML attributes. If a mapping only applies to certain HTML nodes or
-     * only for certain values, the mapping is an object with a whitelist
-     * of nodes and values.
-     *
-     * @var mixed[][]
-     */
-    private $cssToHtmlMap = [
-        'background-color' => [
-            'attribute' => 'bgcolor',
-        ],
-        'text-align' => [
-            'attribute' => 'align',
-            'nodes' => ['p', 'div', 'td'],
-            'values' => ['left', 'right', 'center', 'justify'],
-        ],
-        'float' => [
-            'attribute' => 'align',
-            'nodes' => ['table', 'img'],
-            'values' => ['left', 'right'],
-        ],
-        'border-spacing' => [
-            'attribute' => 'cellspacing',
-            'nodes' => ['table'],
-        ],
-    ];
-
-    /**
-     * @var string[][]
-     */
-    private static $parsedCssCache = [];
-
-    /**
-     * Maps the CSS from the style nodes to visual HTML attributes.
-     *
-     * @return CssToAttributeConverter fluent interface
-     */
-    public function convertCssToVisualAttributes()
-    {
-        /** @var \DOMElement $node */
-        foreach ($this->getAllNodesWithStyleAttribute() as $node) {
-            $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
-            $this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node);
-        }
-
-        return $this;
-    }
-
-    /**
-     * Returns a list with all DOM nodes that have a style attribute.
-     *
-     * @return \DOMNodeList
-     */
-    private function getAllNodesWithStyleAttribute()
-    {
-        $xPath = new \DOMXPath($this->domDocument);
-
-        return $xPath->query('//*[@style]');
-    }
-
-    /**
-     * Parses a CSS declaration block into property name/value pairs.
-     *
-     * Example:
-     *
-     * The declaration block
-     *
-     *   "color: #000; font-weight: bold;"
-     *
-     * will be parsed into the following array:
-     *
-     *   "color" => "#000"
-     *   "font-weight" => "bold"
-     *
-     * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
-     *
-     * @return string[]
-     *         the CSS declarations with the property names as array keys and the property values as array values
-     */
-    private function parseCssDeclarationsBlock($cssDeclarationsBlock)
-    {
-        if (isset(self::$parsedCssCache[$cssDeclarationsBlock])) {
-            return self::$parsedCssCache[$cssDeclarationsBlock];
-        }
-
-        $properties = [];
-        $declarations = \preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
-
-        foreach ($declarations as $declaration) {
-            $matches = [];
-            if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
-                continue;
-            }
-
-            $propertyName = \strtolower($matches[1]);
-            $propertyValue = $matches[2];
-            $properties[$propertyName] = $propertyValue;
-        }
-        self::$parsedCssCache[$cssDeclarationsBlock] = $properties;
-
-        return $properties;
-    }
-
-    /**
-     * Applies $styles to $node.
-     *
-     * This method maps CSS styles to HTML attributes and adds those to the
-     * node.
-     *
-     * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return void
-     */
-    private function mapCssToHtmlAttributes(array $styles, \DOMElement $node)
-    {
-        foreach ($styles as $property => $value) {
-            // Strip !important indicator
-            $value = \trim(\str_replace('!important', '', $value));
-            $this->mapCssToHtmlAttribute($property, $value, $node);
-        }
-    }
-
-    /**
-     * Tries to apply the CSS style to $node as an attribute.
-     *
-     * This method maps a CSS rule to HTML attributes and adds those to the node.
-     *
-     * @param string $property the name of the CSS property to map
-     * @param string $value the value of the style rule to map
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return void
-     */
-    private function mapCssToHtmlAttribute($property, $value, \DOMElement $node)
-    {
-        if (!$this->mapSimpleCssProperty($property, $value, $node)) {
-            $this->mapComplexCssProperty($property, $value, $node);
-        }
-    }
-
-    /**
-     * Looks up the CSS property in the mapping table and maps it if it matches the conditions.
-     *
-     * @param string $property the name of the CSS property to map
-     * @param string $value the value of the style rule to map
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return bool true if the property can be mapped using the simple mapping table
-     */
-    private function mapSimpleCssProperty($property, $value, \DOMElement $node)
-    {
-        if (!isset($this->cssToHtmlMap[$property])) {
-            return false;
-        }
-
-        $mapping = $this->cssToHtmlMap[$property];
-        $nodesMatch = !isset($mapping['nodes']) || \in_array($node->nodeName, $mapping['nodes'], true);
-        $valuesMatch = !isset($mapping['values']) || \in_array($value, $mapping['values'], true);
-        if (!$nodesMatch || !$valuesMatch) {
-            return false;
-        }
-
-        $node->setAttribute($mapping['attribute'], $value);
-
-        return true;
-    }
-
-    /**
-     * Maps CSS properties that need special transformation to an HTML attribute.
-     *
-     * @param string $property the name of the CSS property to map
-     * @param string $value the value of the style rule to map
-     * @param \DOMElement $node node to apply styles to
-     *
-     * @return void
-     */
-    private function mapComplexCssProperty($property, $value, \DOMElement $node)
-    {
-        switch ($property) {
-            case 'background':
-                $this->mapBackgroundProperty($node, $value);
-                break;
-            case 'width':
-                // intentional fall-through
-            case 'height':
-                $this->mapWidthOrHeightProperty($node, $value, $property);
-                break;
-            case 'margin':
-                $this->mapMarginProperty($node, $value);
-                break;
-            case 'border':
-                $this->mapBorderProperty($node, $value);
-                break;
-            default:
-        }
-    }
-
-    /**
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     *
-     * @return void
-     */
-    private function mapBackgroundProperty(\DOMElement $node, $value)
-    {
-        // parse out the color, if any
-        $styles = \explode(' ', $value);
-        $first = $styles[0];
-        if (!\is_numeric($first[0]) && \strpos($first, 'url') !== 0) {
-            // as this is not a position or image, assume it's a color
-            $node->setAttribute('bgcolor', $first);
-        }
-    }
-
-    /**
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     * @param string $property the name of the CSS property to map
-     *
-     * @return void
-     */
-    private function mapWidthOrHeightProperty(\DOMElement $node, $value, $property)
-    {
-        // only parse values in px and %, but not values like "auto"
-        if (!\preg_match('/^(\\d+)(px|%)$/', $value)) {
-            return;
-        }
-
-        $number = \preg_replace('/[^0-9.%]/', '', $value);
-        $node->setAttribute($property, $number);
-    }
-
-    /**
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     *
-     * @return void
-     */
-    private function mapMarginProperty(\DOMElement $node, $value)
-    {
-        if (!$this->isTableOrImageNode($node)) {
-            return;
-        }
-
-        $margins = $this->parseCssShorthandValue($value);
-        if ($margins['left'] === 'auto' && $margins['right'] === 'auto') {
-            $node->setAttribute('align', 'center');
-        }
-    }
-
-    /**
-     * @param \DOMElement $node node to apply styles to
-     * @param string $value the value of the style rule to map
-     *
-     * @return void
-     */
-    private function mapBorderProperty(\DOMElement $node, $value)
-    {
-        if (!$this->isTableOrImageNode($node)) {
-            return;
-        }
-
-        if ($value === 'none' || $value === '0') {
-            $node->setAttribute('border', '0');
-        }
-    }
-
-    /**
-     * @param \DOMElement $node
-     *
-     * @return bool
-     */
-    private function isTableOrImageNode(\DOMElement $node)
-    {
-        return $node->nodeName === 'table' || $node->nodeName === 'img';
-    }
-
-    /**
-     * Parses a shorthand CSS value and splits it into individual values
-     *
-     * @param string $value a string of CSS value with 1, 2, 3 or 4 sizes
-     *                      For example: padding: 0 auto;
-     *                      '0 auto' is split into top: 0, left: auto, bottom: 0,
-     *                      right: auto.
-     *
-     * @return string[] an array of values for top, right, bottom and left (using these as associative array keys)
-     */
-    private function parseCssShorthandValue($value)
-    {
-        $values = \preg_split('/\\s+/', $value);
-
-        $css = [];
-        $css['top'] = $values[0];
-        $css['right'] = (\count($values) > 1) ? $values[1] : $css['top'];
-        $css['bottom'] = (\count($values) > 2) ? $values[2] : $css['top'];
-        $css['left'] = (\count($values) > 3) ? $values[3] : $css['right'];
-
-        return $css;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php
deleted file mode 100644 (file)
index ad3abff..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace Pelago\Emogrifier\HtmlProcessor;
-
-/**
- * Normalizes HTML:
- * - add a document type (HTML5) if missing
- * - disentangle incorrectly nested tags
- * - add HEAD and BODY elements (if they are missing)
- * - reformat the HTML
- *
- * @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-class HtmlNormalizer extends AbstractHtmlProcessor
-{
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/AbstractHtmlProcessor.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/AbstractHtmlProcessor.php
new file mode 100644 (file)
index 0000000..10de9c6
--- /dev/null
@@ -0,0 +1,347 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier\HtmlProcessor;
+
+/**
+ * Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
+ *
+ * The "vanilla" subclass is the HtmlNormalizer.
+ *
+ * @author Oliver Klee <github@oliverklee.de>
+ */
+abstract class AbstractHtmlProcessor
+{
+    /**
+     * @var string
+     */
+    const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
+
+    /**
+     * @var string
+     */
+    const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
+
+    /**
+     * @var string Regular expression part to match tag names that PHP's DOMDocument implementation is not aware are
+     *      self-closing. These are mostly HTML5 elements, but for completeness <command> (obsolete) and <keygen>
+     *      (deprecated) are also included.
+     *
+     * @see https://bugs.php.net/bug.php?id=73175
+     */
+    const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)';
+
+    /**
+     * @var \DOMDocument|null
+     */
+    protected $domDocument = null;
+
+    /**
+     * @var \DOMXPath
+     */
+    protected $xPath = null;
+
+    /**
+     * The constructor.
+     *
+     * Please use `::fromHtml` or `::fromDomDocument` instead.
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * Builds a new instance from the given HTML.
+     *
+     * @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
+     */
+    public static function fromHtml(string $unprocessedHtml): self
+    {
+        if ($unprocessedHtml === '') {
+            throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647);
+        }
+
+        $instance = new static();
+        $instance->setHtml($unprocessedHtml);
+
+        return $instance;
+    }
+
+    /**
+     * Builds a new instance from the given DOM document.
+     *
+     * @param \DOMDocument $document a DOM document returned by getDomDocument() of another instance
+     *
+     * @return static
+     */
+    public static function fromDomDocument(\DOMDocument $document): self
+    {
+        $instance = new static();
+        $instance->setDomDocument($document);
+
+        return $instance;
+    }
+
+    /**
+     * Sets the HTML to process.
+     *
+     * @param string $html the HTML to process, must be UTF-8-encoded
+     *
+     * @return void
+     */
+    private function setHtml(string $html)
+    {
+        $this->createUnifiedDomDocument($html);
+    }
+
+    /**
+     * Provides access to the internal DOMDocument representation of the HTML in its current state.
+     *
+     * @return \DOMDocument
+     *
+     * @throws \UnexpectedValueException
+     */
+    public function getDomDocument(): \DOMDocument
+    {
+        if ($this->domDocument === null) {
+            throw new \UnexpectedValueException(
+                (
+                    self::class .
+                    '::setDomDocument() has not yet been called on ' .
+                    static::class
+                ),
+                1570472239
+            );
+        }
+
+        return $this->domDocument;
+    }
+
+    /**
+     * @param \DOMDocument $domDocument
+     *
+     * @return void
+     */
+    private function setDomDocument(\DOMDocument $domDocument)
+    {
+        $this->domDocument = $domDocument;
+        $this->xPath = new \DOMXPath($this->domDocument);
+    }
+
+    /**
+     * Renders the normalized and processed HTML.
+     *
+     * @return string
+     */
+    public function render(): string
+    {
+        $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML();
+
+        return $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
+    }
+
+    /**
+     * Renders the content of the BODY element of the normalized and processed HTML.
+     *
+     * @return string
+     */
+    public function renderBodyContent(): string
+    {
+        $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML($this->getBodyElement());
+        $bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
+
+        return \preg_replace('%</?+body(?:\\s[^>]*+)?+>%', '', $bodyNodeHtml);
+    }
+
+    /**
+     * Eliminates any invalid closing tags for void elements from the given HTML.
+     *
+     * @param string $html
+     *
+     * @return string
+     */
+    private function removeSelfClosingTagsClosingTags(string $html): string
+    {
+        return \preg_replace('%</' . static::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html);
+    }
+
+    /**
+     * Returns the BODY element.
+     *
+     * This method assumes that there always is a BODY element.
+     *
+     * @return \DOMElement
+     */
+    private function getBodyElement(): \DOMElement
+    {
+        return $this->getDomDocument()->getElementsByTagName('body')->item(0);
+    }
+
+    /**
+     * Creates a DOM document from the given HTML and stores it in $this->domDocument.
+     *
+     * The DOM document will always have a BODY element and a document type.
+     *
+     * @param string $html
+     *
+     * @return void
+     */
+    private function createUnifiedDomDocument(string $html)
+    {
+        $this->createRawDomDocument($html);
+        $this->ensureExistenceOfBodyElement();
+    }
+
+    /**
+     * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
+     *
+     * @param string $html
+     *
+     * @return void
+     */
+    private function createRawDomDocument(string $html)
+    {
+        $domDocument = new \DOMDocument();
+        $domDocument->strictErrorChecking = false;
+        $domDocument->formatOutput = true;
+        $libXmlState = \libxml_use_internal_errors(true);
+        $domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
+        \libxml_clear_errors();
+        \libxml_use_internal_errors($libXmlState);
+
+        $this->setDomDocument($domDocument);
+    }
+
+    /**
+     * Returns the HTML with added document type, Content-Type meta tag, and self-closing slashes, if needed,
+     * ensuring that the HTML will be good for creating a DOM document from it.
+     *
+     * @param string $html
+     *
+     * @return string the unified HTML
+     */
+    private function prepareHtmlForDomConversion(string $html): string
+    {
+        $htmlWithSelfClosingSlashes = $this->ensurePhpUnrecognizedSelfClosingTagsAreXml($html);
+        $htmlWithDocumentType = $this->ensureDocumentType($htmlWithSelfClosingSlashes);
+
+        return $this->addContentTypeMetaTag($htmlWithDocumentType);
+    }
+
+    /**
+     * Makes sure that the passed HTML has a document type, with lowercase "html".
+     *
+     * @param string $html
+     *
+     * @return string HTML with document type
+     */
+    private function ensureDocumentType(string $html): string
+    {
+        $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false;
+        if ($hasDocumentType) {
+            return $this->normalizeDocumentType($html);
+        }
+
+        return static::DEFAULT_DOCUMENT_TYPE . $html;
+    }
+
+    /**
+     * Makes sure the document type in the passed HTML has lowercase "html".
+     *
+     * @param string $html
+     *
+     * @return string HTML with normalized document type
+     */
+    private function normalizeDocumentType(string $html): string
+    {
+        // Limit to replacing the first occurrence: as an optimization; and in case an example exists as unescaped text.
+        return \preg_replace(
+            '/<!DOCTYPE\\s++html(?=[\\s>])/i',
+            '<!DOCTYPE html',
+            $html,
+            1
+        );
+    }
+
+    /**
+     * Adds a Content-Type meta tag for the charset.
+     *
+     * This method also ensures that there is a HEAD element.
+     *
+     * @param string $html
+     *
+     * @return string the HTML with the meta tag added
+     */
+    private function addContentTypeMetaTag(string $html): string
+    {
+        $hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false;
+        if ($hasContentTypeMetaTag) {
+            return $html;
+        }
+
+        // We are trying to insert the meta tag to the right spot in the DOM.
+        // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
+        $hasHeadTag = \preg_match('/<head[\\s>]/i', $html);
+        $hasHtmlTag = \stripos($html, '<html') !== false;
+
+        if ($hasHeadTag) {
+            $reworkedHtml = \preg_replace(
+                '/<head(?=[\\s>])([^>]*+)>/i',
+                '<head$1>' . static::CONTENT_TYPE_META_TAG,
+                $html
+            );
+        } elseif ($hasHtmlTag) {
+            $reworkedHtml = \preg_replace(
+                '/<html(.*?)>/i',
+                '<html$1><head>' . static::CONTENT_TYPE_META_TAG . '</head>',
+                $html
+            );
+        } else {
+            $reworkedHtml = static::CONTENT_TYPE_META_TAG . $html;
+        }
+
+        return $reworkedHtml;
+    }
+
+    /**
+     * Makes sure that any self-closing tags not recognized as such by PHP's DOMDocument implementation have a
+     * self-closing slash.
+     *
+     * @param string $html
+     *
+     * @return string HTML with problematic tags converted.
+     */
+    private function ensurePhpUnrecognizedSelfClosingTagsAreXml(string $html): string
+    {
+        return \preg_replace(
+            '%<' . static::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\\b[^>]*+(?<!/)(?=>)%',
+            '$0/',
+            $html
+        );
+    }
+
+    /**
+     * Checks that $this->domDocument has a BODY element and adds it if it is missing.
+     *
+     * @return void
+     *
+     * @throws \UnexpectedValueException
+     */
+    private function ensureExistenceOfBodyElement()
+    {
+        if ($this->getDomDocument()->getElementsByTagName('body')->item(0) !== null) {
+            return;
+        }
+
+        $htmlElement = $this->getDomDocument()->getElementsByTagName('html')->item(0);
+        if ($htmlElement === null) {
+            throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853);
+        }
+        $htmlElement->appendChild($this->getDomDocument()->createElement('body'));
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/CssToAttributeConverter.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/CssToAttributeConverter.php
new file mode 100644 (file)
index 0000000..f87307e
--- /dev/null
@@ -0,0 +1,318 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier\HtmlProcessor;
+
+/**
+ * This HtmlProcessor can convert style HTML attributes to the corresponding other visual HTML attributes,
+ * e.g. it converts style="width: 100px" to width="100".
+ *
+ * It will only add attributes, but leaves the style attribute untouched.
+ *
+ * To trigger the conversion, call the convertCssToVisualAttributes method.
+ *
+ * @author Oliver Klee <github@oliverklee.de>
+ */
+class CssToAttributeConverter extends AbstractHtmlProcessor
+{
+    /**
+     * This multi-level array contains simple mappings of CSS properties to
+     * HTML attributes. If a mapping only applies to certain HTML nodes or
+     * only for certain values, the mapping is an object with a whitelist
+     * of nodes and values.
+     *
+     * @var mixed[][]
+     */
+    private $cssToHtmlMap = [
+        'background-color' => [
+            'attribute' => 'bgcolor',
+        ],
+        'text-align' => [
+            'attribute' => 'align',
+            'nodes' => ['p', 'div', 'td'],
+            'values' => ['left', 'right', 'center', 'justify'],
+        ],
+        'float' => [
+            'attribute' => 'align',
+            'nodes' => ['table', 'img'],
+            'values' => ['left', 'right'],
+        ],
+        'border-spacing' => [
+            'attribute' => 'cellspacing',
+            'nodes' => ['table'],
+        ],
+    ];
+
+    /**
+     * @var string[][]
+     */
+    private static $parsedCssCache = [];
+
+    /**
+     * Maps the CSS from the style nodes to visual HTML attributes.
+     *
+     * @return self fluent interface
+     */
+    public function convertCssToVisualAttributes(): self
+    {
+        /** @var \DOMElement $node */
+        foreach ($this->getAllNodesWithStyleAttribute() as $node) {
+            $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
+            $this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns a list with all DOM nodes that have a style attribute.
+     *
+     * @return \DOMNodeList
+     */
+    private function getAllNodesWithStyleAttribute(): \DOMNodeList
+    {
+        return $this->xPath->query('//*[@style]');
+    }
+
+    /**
+     * Parses a CSS declaration block into property name/value pairs.
+     *
+     * Example:
+     *
+     * The declaration block
+     *
+     *   "color: #000; font-weight: bold;"
+     *
+     * will be parsed into the following array:
+     *
+     *   "color" => "#000"
+     *   "font-weight" => "bold"
+     *
+     * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
+     *
+     * @return string[]
+     *         the CSS declarations with the property names as array keys and the property values as array values
+     */
+    private function parseCssDeclarationsBlock(string $cssDeclarationsBlock): array
+    {
+        if (isset(self::$parsedCssCache[$cssDeclarationsBlock])) {
+            return self::$parsedCssCache[$cssDeclarationsBlock];
+        }
+
+        $properties = [];
+        foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) {
+            $matches = [];
+            if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
+                continue;
+            }
+
+            $propertyName = \strtolower($matches[1]);
+            $propertyValue = $matches[2];
+            $properties[$propertyName] = $propertyValue;
+        }
+        self::$parsedCssCache[$cssDeclarationsBlock] = $properties;
+
+        return $properties;
+    }
+
+    /**
+     * Applies $styles to $node.
+     *
+     * This method maps CSS styles to HTML attributes and adds those to the
+     * node.
+     *
+     * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
+     * @param \DOMElement $node node to apply styles to
+     *
+     * @return void
+     */
+    private function mapCssToHtmlAttributes(array $styles, \DOMElement $node)
+    {
+        foreach ($styles as $property => $value) {
+            // Strip !important indicator
+            $value = \trim(\str_replace('!important', '', $value));
+            $this->mapCssToHtmlAttribute($property, $value, $node);
+        }
+    }
+
+    /**
+     * Tries to apply the CSS style to $node as an attribute.
+     *
+     * This method maps a CSS rule to HTML attributes and adds those to the node.
+     *
+     * @param string $property the name of the CSS property to map
+     * @param string $value the value of the style rule to map
+     * @param \DOMElement $node node to apply styles to
+     *
+     * @return void
+     */
+    private function mapCssToHtmlAttribute(string $property, string $value, \DOMElement $node)
+    {
+        if (!$this->mapSimpleCssProperty($property, $value, $node)) {
+            $this->mapComplexCssProperty($property, $value, $node);
+        }
+    }
+
+    /**
+     * Looks up the CSS property in the mapping table and maps it if it matches the conditions.
+     *
+     * @param string $property the name of the CSS property to map
+     * @param string $value the value of the style rule to map
+     * @param \DOMElement $node node to apply styles to
+     *
+     * @return bool true if the property can be mapped using the simple mapping table
+     */
+    private function mapSimpleCssProperty(string $property, string $value, \DOMElement $node): bool
+    {
+        if (!isset($this->cssToHtmlMap[$property])) {
+            return false;
+        }
+
+        $mapping = $this->cssToHtmlMap[$property];
+        $nodesMatch = !isset($mapping['nodes']) || \in_array($node->nodeName, $mapping['nodes'], true);
+        $valuesMatch = !isset($mapping['values']) || \in_array($value, $mapping['values'], true);
+        $canBeMapped = $nodesMatch && $valuesMatch;
+        if ($canBeMapped) {
+            $node->setAttribute($mapping['attribute'], $value);
+        }
+
+        return $canBeMapped;
+    }
+
+    /**
+     * Maps CSS properties that need special transformation to an HTML attribute.
+     *
+     * @param string $property the name of the CSS property to map
+     * @param string $value the value of the style rule to map
+     * @param \DOMElement $node node to apply styles to
+     *
+     * @return void
+     */
+    private function mapComplexCssProperty(string $property, string $value, \DOMElement $node)
+    {
+        switch ($property) {
+            case 'background':
+                $this->mapBackgroundProperty($node, $value);
+                break;
+            case 'width':
+                // intentional fall-through
+            case 'height':
+                $this->mapWidthOrHeightProperty($node, $value, $property);
+                break;
+            case 'margin':
+                $this->mapMarginProperty($node, $value);
+                break;
+            case 'border':
+                $this->mapBorderProperty($node, $value);
+                break;
+            default:
+        }
+    }
+
+    /**
+     * @param \DOMElement $node node to apply styles to
+     * @param string $value the value of the style rule to map
+     *
+     * @return void
+     */
+    private function mapBackgroundProperty(\DOMElement $node, string $value)
+    {
+        // parse out the color, if any
+        $styles = \explode(' ', $value, 2);
+        $first = $styles[0];
+        if (\is_numeric($first[0]) || \strncmp($first, 'url', 3) === 0) {
+            return;
+        }
+
+        // as this is not a position or image, assume it's a color
+        $node->setAttribute('bgcolor', $first);
+    }
+
+    /**
+     * @param \DOMElement $node node to apply styles to
+     * @param string $value the value of the style rule to map
+     * @param string $property the name of the CSS property to map
+     *
+     * @return void
+     */
+    private function mapWidthOrHeightProperty(\DOMElement $node, string $value, string $property)
+    {
+        // only parse values in px and %, but not values like "auto"
+        if (!\preg_match('/^(\\d+)(\\.(\\d+))?(px|%)$/', $value)) {
+            return;
+        }
+
+        $number = \preg_replace('/[^0-9.%]/', '', $value);
+        $node->setAttribute($property, $number);
+    }
+
+    /**
+     * @param \DOMElement $node node to apply styles to
+     * @param string $value the value of the style rule to map
+     *
+     * @return void
+     */
+    private function mapMarginProperty(\DOMElement $node, string $value)
+    {
+        if (!$this->isTableOrImageNode($node)) {
+            return;
+        }
+
+        $margins = $this->parseCssShorthandValue($value);
+        if ($margins['left'] === 'auto' && $margins['right'] === 'auto') {
+            $node->setAttribute('align', 'center');
+        }
+    }
+
+    /**
+     * @param \DOMElement $node node to apply styles to
+     * @param string $value the value of the style rule to map
+     *
+     * @return void
+     */
+    private function mapBorderProperty(\DOMElement $node, string $value)
+    {
+        if (!$this->isTableOrImageNode($node)) {
+            return;
+        }
+
+        if ($value === 'none' || $value === '0') {
+            $node->setAttribute('border', '0');
+        }
+    }
+
+    /**
+     * @param \DOMElement $node
+     *
+     * @return bool
+     */
+    private function isTableOrImageNode(\DOMElement $node): bool
+    {
+        return $node->nodeName === 'table' || $node->nodeName === 'img';
+    }
+
+    /**
+     * Parses a shorthand CSS value and splits it into individual values
+     *
+     * @param string $value a string of CSS value with 1, 2, 3 or 4 sizes
+     *                      For example: padding: 0 auto;
+     *                      '0 auto' is split into top: 0, left: auto, bottom: 0,
+     *                      right: auto.
+     *
+     * @return string[] an array of values for top, right, bottom and left (using these as associative array keys)
+     */
+    private function parseCssShorthandValue(string $value): array
+    {
+        /** @var string[] $values */
+        $values = \preg_split('/\\s+/', $value);
+
+        $css = [];
+        $css['top'] = $values[0];
+        $css['right'] = (\count($values) > 1) ? $values[1] : $css['top'];
+        $css['bottom'] = (\count($values) > 2) ? $values[2] : $css['top'];
+        $css['left'] = (\count($values) > 3) ? $values[3] : $css['right'];
+
+        return $css;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlNormalizer.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlNormalizer.php
new file mode 100644 (file)
index 0000000..aaa770f
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier\HtmlProcessor;
+
+/**
+ * Normalizes HTML:
+ * - add a document type (HTML5) if missing
+ * - disentangle incorrectly nested tags
+ * - add HEAD and BODY elements (if they are missing)
+ * - reformat the HTML
+ *
+ * @author Oliver Klee <github@oliverklee.de>
+ */
+class HtmlNormalizer extends AbstractHtmlProcessor
+{
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlPruner.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlPruner.php
new file mode 100644 (file)
index 0000000..05455d6
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier\HtmlProcessor;
+
+use Pelago\Emogrifier\CssInliner;
+use Pelago\Emogrifier\Utilities\ArrayIntersector;
+
+/**
+ * This class can remove things from HTML.
+ *
+ * @author Oliver Klee <github@oliverklee.de>
+ * @author Jake Hotson <jake.github@qzdesign.co.uk>
+ */
+class HtmlPruner extends AbstractHtmlProcessor
+{
+    /**
+     * We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only
+     * supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase,
+     * not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE")
+     * to lowercase.
+     *
+     * @var string
+     */
+    const DISPLAY_NONE_MATCHER
+        = '//*[@style and contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")'
+        . ' and not(@class and contains(concat(" ", normalize-space(@class), " "), " -emogrifier-keep "))]';
+
+    /**
+     * Removes elements that have a "display: none;" style.
+     *
+     * @return self fluent interface
+     */
+    public function removeElementsWithDisplayNone(): self
+    {
+        $elementsWithStyleDisplayNone = $this->xPath->query(self::DISPLAY_NONE_MATCHER);
+        if ($elementsWithStyleDisplayNone->length === 0) {
+            return $this;
+        }
+
+        /** @var \DOMNode $element */
+        foreach ($elementsWithStyleDisplayNone as $element) {
+            $parentNode = $element->parentNode;
+            if ($parentNode !== null) {
+                $parentNode->removeChild($element);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Removes classes that are no longer required (e.g. because there are no longer any CSS rules that reference them)
+     * from `class` attributes.
+     *
+     * Note that this does not inspect the CSS, but expects to be provided with a list of classes that are still in use.
+     *
+     * This method also has the (presumably beneficial) side-effect of minifying (removing superfluous whitespace from)
+     * `class` attributes.
+     *
+     * @param string[] $classesToKeep names of classes that should not be removed
+     *
+     * @return self fluent interface
+     */
+    public function removeRedundantClasses(array $classesToKeep = []): self
+    {
+        $elementsWithClassAttribute = $this->xPath->query('//*[@class]');
+
+        if ($classesToKeep !== []) {
+            $this->removeClassesFromElements($elementsWithClassAttribute, $classesToKeep);
+        } else {
+            // Avoid unnecessary processing if there are no classes to keep.
+            $this->removeClassAttributeFromElements($elementsWithClassAttribute);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Removes classes from the `class` attribute of each element in `$elements`, except any in `$classesToKeep`,
+     * removing the `class` attribute itself if the resultant list is empty.
+     *
+     * @param \DOMNodeList $elements
+     * @param string[] $classesToKeep
+     *
+     * @return void
+     */
+    private function removeClassesFromElements(\DOMNodeList $elements, array $classesToKeep)
+    {
+        $classesToKeepIntersector = new ArrayIntersector($classesToKeep);
+
+        /** @var \DOMElement $element */
+        foreach ($elements as $element) {
+            $elementClasses = \preg_split('/\\s++/', \trim($element->getAttribute('class')));
+            $elementClassesToKeep = $classesToKeepIntersector->intersectWith($elementClasses);
+            if ($elementClassesToKeep !== []) {
+                $element->setAttribute('class', \implode(' ', $elementClassesToKeep));
+            } else {
+                $element->removeAttribute('class');
+            }
+        }
+    }
+
+    /**
+     * Removes the `class` attribute from each element in `$elements`.
+     *
+     * @param \DOMNodeList $elements
+     *
+     * @return void
+     */
+    private function removeClassAttributeFromElements(\DOMNodeList $elements)
+    {
+        /** @var \DOMElement $element */
+        foreach ($elements as $element) {
+            $element->removeAttribute('class');
+        }
+    }
+
+    /**
+     * After CSS has been inlined, there will likely be some classes in `class` attributes that are no longer referenced
+     * by any remaining (uninlinable) CSS.  This method removes such classes.
+     *
+     * Note that it does not inspect the remaining CSS, but uses information readily available from the `CssInliner`
+     * instance about the CSS rules that could not be inlined.
+     *
+     * @param CssInliner $cssInliner object instance that performed the CSS inlining
+     *
+     * @return self fluent interface
+     *
+     * @throws \BadMethodCallException if `inlineCss` has not first been called on `$cssInliner`
+     */
+    public function removeRedundantClassesAfterCssInlined(CssInliner $cssInliner): self
+    {
+        $classesToKeepAsKeys = [];
+        foreach ($cssInliner->getMatchingUninlinableSelectors() as $selector) {
+            \preg_match_all('/\\.(-?+[_a-zA-Z][\\w\\-]*+)/', $selector, $matches);
+            $classesToKeepAsKeys += \array_fill_keys($matches[1], true);
+        }
+
+        $this->removeRedundantClasses(\array_keys($classesToKeepAsKeys));
+
+        return $this;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/ArrayIntersector.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/ArrayIntersector.php
new file mode 100644 (file)
index 0000000..424a75b
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier\Utilities;
+
+/**
+ * When computing many array intersections using the same array, it is more efficient to use `array_flip()` first and
+ * then `array_intersect_key()`, than `array_intersect()`.  See the discussion at
+ * {@link https://stackoverflow.com/questions/6329211/php-array-intersect-efficiency Stack Overflow} for more
+ * information.
+ *
+ * Of course, this is only possible if the arrays contain integer or string values, and either don't contain duplicates,
+ * or that fact that duplicates will be removed does not matter.
+ *
+ * This class takes care of the detail.
+ *
+ * @internal
+ *
+ * @author Jake Hotson <jake.github@qzdesign.co.uk>
+ */
+class ArrayIntersector
+{
+    /**
+     * the array with which the object was constructed, with all its keys exchanged with their associated values
+     *
+     * @var (int|string)[]
+     */
+    private $invertedArray;
+
+    /**
+     * Constructs the object with the array that will be reused for many intersection computations.
+     *
+     * @param (int|string)[] $array
+     */
+    public function __construct(array $array)
+    {
+        $this->invertedArray = \array_flip($array);
+    }
+
+    /**
+     * Computes the intersection of `$array` and the array with which this object was constructed.
+     *
+     * @param (int|string)[] $array
+     *
+     * @return (int|string)[] Returns an array containing all of the values in `$array` whose values exist in the array
+     *         with which this object was constructed.  Note that keys are preserved, order is maintained, but
+     *         duplicates are removed.
+     */
+    public function intersectWith(array $array): array
+    {
+        $invertedArray = \array_flip($array);
+
+        $invertedIntersection = \array_intersect_key($invertedArray, $this->invertedArray);
+
+        return \array_flip($invertedIntersection);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/CssConcatenator.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/CssConcatenator.php
new file mode 100644 (file)
index 0000000..6450c52
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pelago\Emogrifier\Utilities;
+
+/**
+ * Facilitates building a CSS string by appending rule blocks one at a time, checking whether the media query,
+ * selectors, or declarations block are the same as those from the preceding block and combining blocks in such cases.
+ *
+ * Example:
+ *  $concatenator = new CssConcatenator();
+ *  $concatenator->append(['body'], 'color: blue;');
+ *  $concatenator->append(['body'], 'font-size: 16px;');
+ *  $concatenator->append(['p'], 'margin: 1em 0;');
+ *  $concatenator->append(['ul', 'ol'], 'margin: 1em 0;');
+ *  $concatenator->append(['body'], 'font-size: 14px;', '@media screen and (max-width: 400px)');
+ *  $concatenator->append(['ul', 'ol'], 'margin: 0.75em 0;', '@media screen and (max-width: 400px)');
+ *  $css = $concatenator->getCss();
+ *
+ * `$css` (if unminified) would contain the following CSS:
+ * ` body {
+ * `   color: blue;
+ * `   font-size: 16px;
+ * ` }
+ * ` p, ul, ol {
+ * `   margin: 1em 0;
+ * ` }
+ * ` @media screen and (max-width: 400px) {
+ * `   body {
+ * `     font-size: 14px;
+ * `   }
+ * `   ul, ol {
+ * `     margin: 0.75em 0;
+ * `   }
+ * ` }
+ *
+ * @internal
+ *
+ * @author Jake Hotson <jake.github@qzdesign.co.uk>
+ */
+class CssConcatenator
+{
+    /**
+     * Array of media rules in order.  Each element is an object with the following properties:
+     * - string `media` - The media query string, e.g. "@media screen and (max-width:639px)", or an empty string for
+     *   rules not within a media query block;
+     * - \stdClass[] `ruleBlocks` - Array of rule blocks in order, where each element is an object with the following
+     *   properties:
+     *   - mixed[] `selectorsAsKeys` - Array whose keys are selectors for the rule block (values are of no
+     *     significance);
+     *   - string `declarationsBlock` - The property declarations, e.g. "margin-top: 0.5em; padding: 0".
+     *
+     * @var \stdClass[]
+     */
+    private $mediaRules = [];
+
+    /**
+     * Appends a declaration block to the CSS.
+     *
+     * @param string[] $selectors Array of selectors for the rule, e.g. ["ul", "ol", "p:first-child"].
+     * @param string $declarationsBlock The property declarations, e.g. "margin-top: 0.5em; padding: 0".
+     * @param string $media The media query for the rule, e.g. "@media screen and (max-width:639px)",
+     *                      or an empty string if none.
+     */
+    public function append(array $selectors, string $declarationsBlock, string $media = '')
+    {
+        $selectorsAsKeys = \array_flip($selectors);
+
+        $mediaRule = $this->getOrCreateMediaRuleToAppendTo($media);
+        $lastRuleBlock = \end($mediaRule->ruleBlocks);
+
+        $hasSameDeclarationsAsLastRule = $lastRuleBlock !== false
+            && $declarationsBlock === $lastRuleBlock->declarationsBlock;
+        if ($hasSameDeclarationsAsLastRule) {
+            $lastRuleBlock->selectorsAsKeys += $selectorsAsKeys;
+        } else {
+            $hasSameSelectorsAsLastRule = $lastRuleBlock !== false
+                && self::hasEquivalentSelectors($selectorsAsKeys, $lastRuleBlock->selectorsAsKeys);
+            if ($hasSameSelectorsAsLastRule) {
+                $lastDeclarationsBlockWithoutSemicolon = \rtrim(\rtrim($lastRuleBlock->declarationsBlock), ';');
+                $lastRuleBlock->declarationsBlock = $lastDeclarationsBlockWithoutSemicolon . ';' . $declarationsBlock;
+            } else {
+                $mediaRule->ruleBlocks[] = (object)\compact('selectorsAsKeys', 'declarationsBlock');
+            }
+        }
+    }
+
+    /**
+     * @return string
+     */
+    public function getCss(): string
+    {
+        return \implode('', \array_map([self::class, 'getMediaRuleCss'], $this->mediaRules));
+    }
+
+    /**
+     * @param string $media The media query for rules to be appended, e.g. "@media screen and (max-width:639px)",
+     *                      or an empty string if none.
+     *
+     * @return \stdClass Object with properties as described for elements of `$mediaRules`.
+     */
+    private function getOrCreateMediaRuleToAppendTo(string $media): \stdClass
+    {
+        $lastMediaRule = \end($this->mediaRules);
+        if ($lastMediaRule !== false && $media === $lastMediaRule->media) {
+            return $lastMediaRule;
+        }
+
+        $newMediaRule = (object)[
+            'media' => $media,
+            'ruleBlocks' => [],
+        ];
+        $this->mediaRules[] = $newMediaRule;
+        return $newMediaRule;
+    }
+
+    /**
+     * Tests if two sets of selectors are equivalent (i.e. the same selectors, possibly in a different order).
+     *
+     * @param mixed[] $selectorsAsKeys1 Array in which the selectors are the keys, and the values are of no
+     *                                  significance.
+     * @param mixed[] $selectorsAsKeys2 Another such array.
+     *
+     * @return bool
+     */
+    private static function hasEquivalentSelectors(array $selectorsAsKeys1, array $selectorsAsKeys2): bool
+    {
+        return \count($selectorsAsKeys1) === \count($selectorsAsKeys2)
+            && \count($selectorsAsKeys1) === \count($selectorsAsKeys1 + $selectorsAsKeys2);
+    }
+
+    /**
+     * @param \stdClass $mediaRule Object with properties as described for elements of `$mediaRules`.
+     *
+     * @return string CSS for the media rule.
+     */
+    private static function getMediaRuleCss(\stdClass $mediaRule): string
+    {
+        $css = \implode('', \array_map([self::class, 'getRuleBlockCss'], $mediaRule->ruleBlocks));
+        if ($mediaRule->media !== '') {
+            $css = $mediaRule->media . '{' . $css . '}';
+        }
+        return $css;
+    }
+
+    /**
+     * @param \stdClass $ruleBlock Object with properties as described for elements of the `ruleBlocks` property of
+     *                            elements of `$mediaRules`.
+     *
+     * @return string CSS for the rule block.
+     */
+    private static function getRuleBlockCss(\stdClass $ruleBlock): string
+    {
+        $selectors = \array_keys($ruleBlock->selectorsAsKeys);
+        return \implode(',', $selectors) . '{' . $ruleBlock->declarationsBlock . '}';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Support/Traits/AssertCss.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Support/Traits/AssertCss.php
deleted file mode 100644 (file)
index 959ee64..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Support\Traits;
-
-/**
- * Provides assertion methods for use with CSS content where whitespace may vary.
- *
- * @author Jake Hotson <jake.github@qzdesign.co.uk>
- */
-trait AssertCss
-{
-    /**
-     * Processing of @media rules may involve removal of some unnecessary whitespace from the CSS placed in the <style>
-     * element added to the docuemnt, due to the way that certain parts are `trim`med.  Notably, whitespace either side
-     * of "{", "}" and "," or at the beginning of the CSS may be removed.
-     *
-     * This method helps takes care of that, by converting a search needle for an exact match into a regular expression
-     * that allows for such whitespace removal, so that the tests themselves do not need to be written less humanly
-     * readable and can use inputs containing extra whitespace.
-     *
-     * @param string $needle Needle that would be used with `assertContains` or `assertNotContains`.
-     *
-     * @return string Needle to use with `assertRegExp` or `assertNotRegExp` instead.
-     */
-    private static function getCssNeedleRegExp($needle)
-    {
-        $needleMatcher = \preg_replace_callback(
-            '/\\s*+([{},])\\s*+|(^\\s++)|(>)\\s*+|(?:(?!\\s*+[{},]|^\\s)[^>])++/',
-            function (array $matches) {
-                if (isset($matches[1]) && $matches[1] !== '') {
-                    // matched possibly some whitespace, followed by "{", "}" or ",", then possibly more whitespace
-                    return '\\s*+' . \preg_quote($matches[1], '/') . '\\s*+';
-                }
-                if (isset($matches[2]) && $matches[2] !== '') {
-                    // matched whitespace at start
-                    return '\\s*+';
-                }
-                if (isset($matches[3]) && $matches[3] !== '') {
-                    // matched ">" (e.g. end of <style> tag) followed by possibly some whitespace
-                    return \preg_quote($matches[3], '/') . '\\s*+';
-                }
-                // matched any other sequence which could not overlap with the above
-                return \preg_quote($matches[0], '/');
-            },
-            $needle
-        );
-        return '/' . $needleMatcher . '/';
-    }
-
-    /**
-     * Like `assertContains` but allows for removal of some unnecessary whitespace from the CSS.
-     *
-     * @param string $needle
-     * @param string $haystack
-     */
-    private static function assertContainsCss($needle, $haystack)
-    {
-        static::assertRegExp(
-            static::getCssNeedleRegExp($needle),
-            $haystack,
-            'Plain text needle: "' . $needle . '"'
-        );
-    }
-
-    /**
-     * Like `assertNotContains` and also enforces the assertion with removal of some unnecessary whitespace from the
-     * CSS.
-     *
-     * @param string $needle
-     * @param string $haystack
-     */
-    private static function assertNotContainsCss($needle, $haystack)
-    {
-        static::assertNotRegExp(
-            static::getCssNeedleRegExp($needle),
-            $haystack,
-            'Plain text needle: "' . $needle . '"'
-        );
-    }
-
-    /**
-     * Asserts that a string of CSS occurs exactly a certain number of times in the result, allowing for removal of some
-     * unnecessary whitespace.
-     *
-     * @param int $expectedCount
-     * @param string $needle
-     * @param string $haystack
-     */
-    private static function assertContainsCssCount(
-        $expectedCount,
-        $needle,
-        $haystack
-    ) {
-        static::assertSame(
-            $expectedCount,
-            \preg_match_all(static::getCssNeedleRegExp($needle), $haystack),
-            'Plain text needle: "' . $needle . "\"\nHaystack: \"" . $haystack . '"'
-        );
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/CssInlinerTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/CssInlinerTest.php
deleted file mode 100644 (file)
index e6fb9c1..0000000
+++ /dev/null
@@ -1,2930 +0,0 @@
-<?php
-
-namespace Pelago\Emogrifer\Tests\Unit;
-
-use Pelago\Emogrifier\CssInliner;
-use Pelago\Tests\Support\Traits\AssertCss;
-
-/**
- * Test case.
- *
- * @author Oliver Klee <github@oliverklee.de>
- * @author Zoli Szabó <zoli.szabo+github@gmail.com>
- */
-class CssInlinerTest extends \PHPUnit_Framework_TestCase
-{
-    use AssertCss;
-
-    /**
-     * @var string Common HTML markup with a variety of elements and attributes for testing with
-     */
-    const COMMON_TEST_HTML = '
-        <html>
-            <body>
-                <p class="p-1"><span>some text</span></p>
-                <p class="p-2"><span title="bonjour">some</span> text</p>
-                <p class="p-3"><span title="buenas dias">some</span> more text</p>
-                <p class="p-4" id="p4"><span title="avez-vous">some</span> more <span id="text">text</span></p>
-                <p class="p-5 additional-class"><span title="buenas dias bom dia">some</span> more text</p>
-                <p class="p-6"><span title="title: subtitle; author">some</span> more text</p>
-            </body>
-        </html>
-    ';
-
-    /**
-     * @var string
-     */
-    private $html5DocumentType = '<!DOCTYPE html>';
-
-    /**
-     * Builds a subject with the given HTML and debug mode enabled.
-     *
-     * @param string $html
-     *
-     * @return CssInliner
-     */
-    private function buildDebugSubject($html)
-    {
-        $subject = new CssInliner($html);
-        $subject->setDebug(true);
-
-        return $subject;
-    }
-
-    /**
-     * @test
-     */
-    public function renderFormatsGivenHtml()
-    {
-        $rawHtml = '<!DOCTYPE HTML>' .
-            '<html>' .
-            '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' .
-            '<body></body>' .
-            '</html>';
-        $formattedHtml = "<!DOCTYPE HTML>\n" .
-            "<html>\n" .
-            '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . "\n" .
-            "<body></body>\n" .
-            "</html>\n";
-
-        $subject = $this->buildDebugSubject($rawHtml);
-
-        static::assertSame($formattedHtml, $subject->render());
-    }
-
-    /**
-     * @test
-     */
-    public function renderBodyContentForEmptyBodyReturnsEmptyString()
-    {
-        $subject = $this->buildDebugSubject('<html><body></body></html>');
-
-        $result = $subject->renderBodyContent();
-
-        static::assertSame('', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderBodyContentReturnsBodyContent()
-    {
-        $bodyContent = '<p>Hello world</p>';
-        $subject = $this->buildDebugSubject('<html><body>' . $bodyContent . '</body></html>');
-
-        $result = $subject->renderBodyContent();
-
-        static::assertSame($bodyContent, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function getDomDocumentReturnsDomDocument()
-    {
-        $subject = new CssInliner('<html></html>');
-
-        static::assertInstanceOf(\DOMDocument::class, $subject->getDomDocument());
-    }
-
-    /**
-     * @test
-     */
-    public function getDomDocumentWithNormalizedHtmlRepresentsTheGivenHtml()
-    {
-        $html = "<!DOCTYPE html>\n<html>\n<head>" .
-            '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' .
-            "</head>\n<body>\n<br>\n</body>\n</html>\n";
-        $subject = new CssInliner($html);
-
-        $domDocument = $subject->getDomDocument();
-
-        self::assertSame($html, $domDocument->saveHTML());
-    }
-
-    /**
-     * @test
-     *
-     * @return array[]
-     */
-    public function nonHtmlDataProvider()
-    {
-        return [
-            'empty string' => [''],
-            'null' => [null],
-            'integer' => [2],
-            'float' => [3.14159],
-            'object' => [new \stdClass()],
-        ];
-    }
-
-    /**
-     * @test
-     * @expectedException \InvalidArgumentException
-     *
-     * @param mixed $html
-     *
-     * @dataProvider nonHtmlDataProvider
-     */
-    public function constructorWithNoHtmlDataThrowsException($html)
-    {
-        new CssInliner($html);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutHtmlTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'body content only' => ['<p>Hello</p>'],
-            'HEAD element' => ['<head></head>'],
-            'BODY element' => ['<body></body>'],
-            'HEAD AND BODY element' => ['<head></head><body></body>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutHtmlTagDataProvider
-     */
-    public function renderAddsMissingHtmlTag($html)
-    {
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->render();
-
-        static::assertContains('<html>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutHeadTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'body content only' => ['<p>Hello</p>'],
-            'BODY element' => ['<body></body>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutHeadTagDataProvider
-     */
-    public function renderAddsMissingHeadTag($html)
-    {
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->render();
-
-        static::assertContains('<head>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutBodyTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'HEAD element' => ['<head></head>'],
-            'body content only' => ['<p>Hello</p>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutBodyTagDataProvider
-     */
-    public function renderAddsMissingBodyTag($html)
-    {
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->render();
-
-        static::assertContains('<body>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderPutsMissingBodyElementAroundBodyContent()
-    {
-        $subject = $this->buildDebugSubject('<p>Hello</p>');
-
-        $result = $subject->render();
-
-        static::assertContains('<body><p>Hello</p></body>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function specialCharactersDataProvider()
-    {
-        return [
-            'template markers with dollar signs & square brackets' => ['$[USER:NAME]$'],
-            'UTF-8 umlauts' => ['Küss die Hand, schöne Frau.'],
-            'HTML entities' => ['a &amp; b &gt; c'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $codeNotToBeChanged
-     *
-     * @dataProvider specialCharactersDataProvider
-     */
-    public function renderKeepsSpecialCharacters($codeNotToBeChanged)
-    {
-        $html = '<html><p>' . $codeNotToBeChanged . '</p></html>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->render();
-
-        static::assertContains($codeNotToBeChanged, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addsMissingHtml5DocumentType()
-    {
-        $subject = $this->buildDebugSubject('<html><h1>foo</h1></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<!DOCTYPE html>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $codeNotToBeChanged
-     *
-     * @dataProvider specialCharactersDataProvider
-     */
-    public function emogrifyBodyContentKeepsSpecialCharacters($codeNotToBeChanged)
-    {
-        $html = '<html><p>' . $codeNotToBeChanged . '</p></html>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->emogrifyBodyContent();
-
-        static::assertContains($codeNotToBeChanged, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function documentTypeDataProvider()
-    {
-        return [
-            'HTML5' => ['<!DOCTYPE html>'],
-            'XHTML 1 strict' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' .
-                '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
-            ],
-            'HTML 4 transitional' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' .
-                '"http://www.w3.org/TR/REC-html40/loose.dtd">',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function renderForHtmlWithDocumentTypeKeepsDocumentType($documentType)
-    {
-        $html = $documentType . '<html></html>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->render();
-
-        static::assertContains($documentType, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderAddsMissingContentTypeMetaTag()
-    {
-        $subject = $this->buildDebugSubject('<p>Hello</p>');
-
-        $result = $subject->render();
-
-        static::assertContains('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderNotAddsSecondContentTypeMetaTag()
-    {
-        $html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->render();
-
-        $numberOfContentTypeMetaTags = \substr_count($result, 'Content-Type');
-        static::assertSame(1, $numberOfContentTypeMetaTags);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultRemovesWbrTag()
-    {
-        $html = '<html>foo<wbr/>bar</html>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('<wbr', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addUnprocessableTagRemovesEmptyTag()
-    {
-        $subject = $this->buildDebugSubject('<html><p></p></html>');
-
-        $subject->addUnprocessableHtmlTag('p');
-        $result = $subject->emogrify();
-
-        static::assertNotContains('<p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addUnprocessableTagNotRemovesNonEmptyTag()
-    {
-        $subject = $this->buildDebugSubject('<html><p>foobar</p></html>');
-
-        $subject->addUnprocessableHtmlTag('p');
-        $result = $subject->emogrify();
-
-        static::assertContains('<p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function removeUnprocessableHtmlTagKeepsTagAgainAgain()
-    {
-        $subject = $this->buildDebugSubject('<html><p></p></html>');
-
-        $subject->addUnprocessableHtmlTag('p');
-        $subject->removeUnprocessableHtmlTag('p');
-        $result = $subject->emogrify();
-
-        static::assertContains('<p>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function matchedCssDataProvider()
-    {
-        // The sprintf placeholders %1$s and %2$s will automatically be replaced with CSS declarations
-        // like 'color: red;' or 'text-align: left;'.
-        return [
-            'two declarations from one rule can apply to the same element' => [
-                'html { %1$s %2$s }',
-                '<html style="%1$s %2$s">',
-            ],
-            'two identical matchers with different rules get combined' => [
-                'p { %1$s } p { %2$s }',
-                '<p class="p-1" style="%1$s %2$s">',
-            ],
-            'two different matchers rules matching the same element get combined' => [
-                'p { %1$s } .p-1 { %2$s }',
-                '<p class="p-1" style="%1$s %2$s">',
-            ],
-            'type => one element' => ['html { %1$s }', '<html style="%1$s">'],
-            'type (case-insensitive) => one element' => ['HTML { %1$s }', '<html style="%1$s">'],
-            'type => first matching element' => ['p { %1$s }', '<p class="p-1" style="%1$s">'],
-            'type => second matching element' => ['p { %1$s }', '<p class="p-2" style="%1$s">'],
-            'class => with class' => ['.p-2 { %1$s }', '<p class="p-2" style="%1$s">'],
-            'two classes s=> with both classes' => [
-                '.p-5.additional-class { %1$s }',
-                '<p class="p-5 additional-class" style="%1$s">',
-            ],
-            'type & class => type with class' => ['p.p-2 { %1$s }', '<p class="p-2" style="%1$s">'],
-            'ID => with ID' => ['#p4 { %1$s }', '<p class="p-4" id="p4" style="%1$s">'],
-            'type & ID => type with ID' => ['p#p4 { %1$s }', '<p class="p-4" id="p4" style="%1$s">'],
-            'universal => HTML' => ['* { %1$s }', '<html style="%1$s">'],
-            'attribute presence => with attribute' => ['[title] { %1$s }', '<span title="bonjour" style="%1$s">'],
-            'attribute exact value, double quotes => with exact attribute match' => [
-                '[title="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'attribute exact value, single quotes => with exact match' => [
-                '[title=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            // broken: attribute exact value without quotes => with exact match
-            // broken: attribute exact two-word value, double quotes => with exact attribute value match
-            // broken: attribute exact two-word value, single quotes => with exact attribute value match
-            // broken: attribute exact value with ~, double quotes => exact attribute match
-            // broken: attribute exact value with ~, single quotes => exact attribute match
-            // broken: attribute exact value with ~, no quotes => exact attribute match
-            // broken: attribute value with |, double quotes => with exact match
-            // broken: attribute value with |, single quotes => with exact match
-            // broken: attribute value with |, no quotes => with exact match
-            // broken: attribute value with ^, double quotes => with exact match
-            // broken: attribute value with ^, single quotes => with exact match
-            // broken: attribute value with ^, no quotes => with exact match
-            // broken: attribute value with $, double quotes => with exact match
-            // broken: attribute value with $, single quotes => with exact match
-            // broken: attribute value with $, no quotes => with exact match
-            // broken: attribute value with *, double quotes => with exact match
-            // broken: attribute value with *, single quotes => with exact match
-            // broken: attribute value with *, no quotes => with exact match
-            // broken: type & attribute presence => with type & attribute
-            'type & attribute exact value, double quotes => with type & exact attribute value match' => [
-                'span[title="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute exact value, single quotes => with type & exact attribute value match' => [
-                'span[title=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute exact value without quotes => with type & exact attribute value match' => [
-                'span[title=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute exact two-word value, double quotes => with type & exact attribute value match' => [
-                'span[title="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute exact four-word value, double quotes => with type & exact attribute value match' => [
-                'span[title="buenas dias bom dia"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute exact two-word value, single quotes => with type & exact attribute value match' => [
-                'span[title=\'buenas dias\'] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute exact four-word value, single quotes => with type & exact attribute value match' => [
-                'span[title=\'buenas dias bom dia\'] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & exact attribute match' => [
-                'span[title~="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ~, single quotes => with type & exact attribute match' => [
-                'span[title~=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ~, no quotes => with type & exact attribute match' => [
-                'span[title~=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 1st of 2 in attribute' => [
-                'span[title~="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 2nd of 2 in attribute' => [
-                'span[title~="dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 1st of 4 in attribute' => [
-                'span[title~="buenas"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 2nd of 4 in attribute' => [
-                'span[title~="dias"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as last of 4 in attribute' => [
-                'span[title~="dia"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with |, double quotes => with exact match' => [
-                'span[title|="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with |, single quotes => with exact match' => [
-                'span[title|=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with |, no quotes => with exact match' => [
-                'span[title|=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & two-word attribute value with |, double quotes => with exact match' => [
-                'span[title|="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with |, double quotes => with match before hyphen & another word' => [
-                'span[title|="avez"] { %1$s }',
-                '<span title="avez-vous" style="%1$s">',
-            ],
-            'type & attribute value with ^, double quotes => with exact match' => [
-                'span[title^="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ^, single quotes => with exact match' => [
-                'span[title^=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ^, no quotes => with exact match' => [
-                'span[title^=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            // broken: type & two-word attribute value with ^, double quotes => with exact match
-            'type & attribute value with ^, double quotes => with prefix math' => [
-                'span[title^="bon"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ^, double quotes => with match before another word' => [
-                'span[title^="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with $, double quotes => with exact match' => [
-                'span[title$="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with $, single quotes => with exact match' => [
-                'span[title$=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with $, no quotes => with exact match' => [
-                'span[title$=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & two-word attribute value with $, double quotes => with exact match' => [
-                'span[title$="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with $, double quotes => with suffix math' => [
-                'span[title$="jour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with $, double quotes => with match after another word' => [
-                'span[title$="dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & two-word attribute value with *, double quotes => with exact match' => [
-                'span[title*="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with prefix math' => [
-                'span[title*="bon"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with suffix math' => [
-                'span[title*="jour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with substring math' => [
-                'span[title*="njo"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with match before another word' => [
-                'span[title*="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with match after another word' => [
-                'span[title*="dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & special characters attribute value with *, double quotes => with substring match' => [
-                'span[title*=": subtitle; author"] { %1$s }',
-                '<span title="title: subtitle; author" style="%1$s">',
-            ],
-            'adjacent => 2nd of many' => ['p + p { %1$s }', '<p class="p-2" style="%1$s">'],
-            'adjacent => last of many' => ['p + p { %1$s }', '<p class="p-6" style="%1$s">'],
-            'adjacent (without space after +) => last of many' => ['p +p { %1$s }', '<p class="p-6" style="%1$s">'],
-            'adjacent (without space before +) => last of many' => ['p+ p { %1$s }', '<p class="p-6" style="%1$s">'],
-            'adjacent (without space before or after +) => last of many' => [
-                'p+p { %1$s }',
-                '<p class="p-6" style="%1$s">',
-            ],
-            'child (with spaces around >) => direct child' => ['p > span { %1$s }', '<span style="%1$s">'],
-            'child (without space after >) => direct child' => ['p >span { %1$s }', '<span style="%1$s">'],
-            'child (without space before >) => direct child' => ['p> span { %1$s }', '<span style="%1$s">'],
-            'child (without space before or after >) => direct child' => ['p>span { %1$s }', '<span style="%1$s">'],
-            'descendant => child' => ['p span { %1$s }', '<span style="%1$s">'],
-            'descendant => grandchild' => ['body span { %1$s }', '<span style="%1$s">'],
-            // broken: descendent attribute presence => with attribute
-            // broken: descendent attribute exact value => with exact attribute match
-            // broken: descendent type & attribute presence => with type & attribute
-            'descendent type & attribute exact value => with type & exact attribute match' => [
-                'body span[title="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'descendent type & attribute exact two-word value => with type & exact attribute match' => [
-                'body span[title="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'descendent type & attribute value with ~ => with type & exact attribute match' => [
-                'body span[title~="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'descendent type & attribute value with ~ => with type & word as 1st of 2 in attribute' => [
-                'body span[title~="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'descendant of type & class: type & attribute exact value, no quotes => with type & exact match (#381)' => [
-                'p.p-2 span[title=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'descendant of attribute presence => parent with attribute' => [
-                '[class] span { %1$s }',
-                '<p class="p-1"><span style="%1$s">',
-            ],
-            'descendant of attribute exact value => parent with type & exact attribute match' => [
-                '[id="p4"] span { %1$s }',
-                '<p class="p-4" id="p4"><span title="avez-vous" style="%1$s">',
-            ],
-            // broken: descendant of type & attribute presence => parent with type & attribute
-            'descendant of type & attribute exact value => parent with type & exact attribute match' => [
-                'p[id="p4"] span { %1$s }',
-                '<p class="p-4" id="p4"><span title="avez-vous" style="%1$s">',
-            ],
-            // broken: descendant of type & attribute exact two-word value => parent with type & exact attribute match
-            //         (exact match doesn't currently match hyphens, which would be needed to match the class attribute)
-            'descendant of type & attribute value with ~ => parent with type & exact attribute match' => [
-                'p[class~="p-1"] span { %1$s }',
-                '<p class="p-1"><span style="%1$s">',
-            ],
-            'descendant of type & attribute value with ~ => parent with type & word as 1st of 2 in attribute' => [
-                'p[class~="p-5"] span { %1$s }',
-                '<p class="p-5 additional-class"><span title="buenas dias bom dia" style="%1$s">',
-            ],
-            // broken: first-child => 1st of many
-            'type & :first-child => 1st of many' => ['p:first-child { %1$s }', '<p class="p-1" style="%1$s">'],
-            // broken: last-child => last of many
-            'type & :last-child => last of many' => ['p:last-child { %1$s }', '<p class="p-6" style="%1$s">'],
-            // broken: :not with type => other type
-            // broken: :not with class => no class
-            // broken: :not with class => other class
-            'type & :not with class => without class' => ['span:not(.foo) { %1$s }', '<span style="%1$s">'],
-            'type & :not with class => with other class' => ['p:not(.foo) { %1$s }', '<p class="p-1" style="%1$s">'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css CSS statements, potentially with %1$s and $2$s placeholders for a CSS declaration
-     * @param string $expectedHtml HTML, potentially with %1$s and $2$s placeholders for a CSS declaration
-     *
-     * @dataProvider matchedCssDataProvider
-     */
-    public function emogrifyAppliesCssToMatchingElements($css, $expectedHtml)
-    {
-        $cssDeclaration1 = 'color: red;';
-        $cssDeclaration2 = 'text-align: left;';
-        $subject = $this->buildDebugSubject(static::COMMON_TEST_HTML);
-        $subject->setCss(\sprintf($css, $cssDeclaration1, $cssDeclaration2));
-
-        $result = $subject->emogrify();
-
-        static::assertContains(\sprintf($expectedHtml, $cssDeclaration1, $cssDeclaration2), $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function nonMatchedCssDataProvider()
-    {
-        // The sprintf placeholders %1$s and %2$s will automatically be replaced with CSS declarations
-        // like 'color: red;' or 'text-align: left;'.
-        return [
-            'type => not other type' => ['html { %1$s }', '<body>'],
-            'class => not other class' => ['.p-2 { %1$s }', '<p class="p-1">'],
-            'class => not without class' => ['.p-2 { %1$s }', '<body>'],
-            'two classes => not only first class' => ['.p-1.another-class { %1$s }', '<p class="p-1">'],
-            'two classes => not only second class' => ['.another-class.p-1 { %1$s }', '<p class="p-1">'],
-            'type & class => not only type' => ['html.p-1 { %1$s }', '<html>'],
-            'type & class => not only class' => ['html.p-1 { %1$s }', '<p class="p-1">'],
-            'ID => not other ID' => ['#yeah { %1$s }', '<p class="p-4" id="p4">'],
-            'ID => not without ID' => ['#yeah { %1$s }', '<span>'],
-            'type & ID => not other type with that ID' => ['html#p4 { %1$s }', '<p class="p-4" id="p4">'],
-            'type & ID => not that type with other ID' => ['p#p5 { %1$s }', '<p class="p-4" id="p4">'],
-            'attribute presence => not element without that attribute' => ['[title] { %1$s }', '<span>'],
-            'attribute exact value => not element without that attribute' => ['[title="bonjour"] { %1$s }', '<span>'],
-            'attribute exact value => not element with different attribute value' => [
-                '[title="hi"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'attribute exact value => not element with only substring match in attribute value' => [
-                '[title="njo"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with ~ => not element with only prefix match in attribute value' => [
-                'span[title~="bon"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with |, double quotes => not element with match after another word & hyphen' => [
-                'span[title|="vous"] { %1$s }',
-                '<span title="avez-vous">',
-            ],
-            'type & attribute value with ^ => not element with only substring match in attribute value' => [
-                'span[title^="njo"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with ^, double quotes => not element with only suffix match in attribute value' => [
-                'span[title^="jour"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with $ => not element with only substring match in attribute value' => [
-                'span[title$="njo"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with $, double quotes => not element with only prefix match in attribute value' => [
-                'span[title$="bon"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with * => not element with different attribute value' => [
-                'span[title*="hi"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'adjacent => not 1st of many' => ['p + p { %1$s }', '<p class="p-1">'],
-            'child => not grandchild' => ['html > span { %1$s }', '<span>'],
-            'child => not parent' => ['span > html { %1$s }', '<html>'],
-            'descendant => not sibling' => ['span span { %1$s }', '<span>'],
-            'descendant => not parent' => ['p body { %1$s }', '<body>'],
-            'type & :first-child => not 2nd of many' => ['p:first-child { %1$s }', '<p class="p-2">'],
-            'type & :first-child => not last of many' => ['p:first-child { %1$s }', '<p class="p-6">'],
-            'type & :last-child => not 1st of many' => ['p:last-child { %1$s }', '<p class="p-1">'],
-            'type & :last-child => not 2nd of many' => ['p:last-child { %1$s }', '<p class="p-2">'],
-            'type & :not with class => not with class' => ['p:not(.p-1) { %1$s }', '<p class="p-1">'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css CSS statements, potentially with %1$s and $2$s placeholders for a CSS declaration
-     * @param string $expectedHtml HTML, potentially with %1$s and $2$s placeholders for a CSS declaration
-     *
-     * @dataProvider nonMatchedCssDataProvider
-     */
-    public function emogrifyNotAppliesCssToNonMatchingElements($css, $expectedHtml)
-    {
-        $cssDeclaration1 = 'color: red;';
-        $cssDeclaration2 = 'text-align: left;';
-        $subject = $this->buildDebugSubject(static::COMMON_TEST_HTML);
-        $subject->setCss(\sprintf($css, $cssDeclaration1, $cssDeclaration2));
-
-        $result = $subject->emogrify();
-
-        static::assertContains(\sprintf($expectedHtml, $cssDeclaration1, $cssDeclaration2), $result);
-    }
-
-    /**
-     * Provides data to test the following selector specificity ordering:
-     *     * < t < 2t < . < .+t < .+2t < 2. < 2.+t < 2.+2t
-     *     < # < #+t < #+2t < #+. < #+.+t < #+.+2t < #+2. < #+2.+t < #+2.+2t
-     *     < 2# < 2#+t < 2#+2t < 2#+. < 2#+.+t < 2#+.+2t < 2#+2. < 2#+2.+t < 2#+2.+2t
-     * where '*' is the universal selector, 't' is a type selector, '.' is a class selector, and '#' is an ID selector.
-     *
-     * Also confirm up to 99 class selectors are supported (much beyond this would require a more complex comparator).
-     *
-     * Specificity ordering for selectors involving pseudo-classes, attributes and `:not` is covered through the
-     * combination of these tests and the equal specificity tests and thus does not require explicit separate testing.
-     *
-     * @return string[][]
-     */
-    public function differentCssSelectorSpecificityDataProvider()
-    {
-        /**
-         * @var string[] Selectors targeting `<span id="text">` with increasing specificity
-         */
-        $selectors = [
-            'universal' => '*',
-            'type' => 'span',
-            '2 types' => 'p span',
-            'class' => '.p-4 *',
-            'class & type' => '.p-4 span',
-            'class & 2 types' => 'p.p-4 span',
-            '2 classes' => '.p-4.p-4 *',
-            '2 classes & type' => '.p-4.p-4 span',
-            '2 classes & 2 types' => 'p.p-4.p-4 span',
-            'ID' => '#text',
-            'ID & type' => 'span#text',
-            'ID & 2 types' => 'p span#text',
-            'ID & class' => '.p-4 #text',
-            'ID & class & type' => '.p-4 span#text',
-            'ID & class & 2 types' => 'p.p-4 span#text',
-            'ID & 2 classes' => '.p-4.p-4 #text',
-            'ID & 2 classes & type' => '.p-4.p-4 span#text',
-            'ID & 2 classes & 2 types' => 'p.p-4.p-4 span#text',
-            '2 IDs' => '#p4 #text',
-            '2 IDs & type' => '#p4 span#text',
-            '2 IDs & 2 types' => 'p#p4 span#text',
-            '2 IDs & class' => '.p-4#p4 #text',
-            '2 IDs & class & type' => '.p-4#p4 span#text',
-            '2 IDs & class & 2 types' => 'p.p-4#p4 span#text',
-            '2 IDs & 2 classes' => '.p-4.p-4#p4 #text',
-            '2 IDs & 2 classes & type' => '.p-4.p-4#p4 span#text',
-            '2 IDs & 2 classes & 2 types' => 'p.p-4.p-4#p4 span#text',
-        ];
-
-        $datasets = [];
-        $previousSelector = '';
-        $previousDescription = '';
-        foreach ($selectors as $description => $selector) {
-            if ($previousSelector !== '') {
-                $datasets[$description . ' more specific than ' . $previousDescription] = [
-                    '<span id="text"',
-                    $previousSelector,
-                    $selector,
-                ];
-            }
-            $previousSelector = $selector;
-            $previousDescription = $description;
-        }
-
-        // broken: class more specific than 99 types (requires support for chaining `:not(h1):not(h1)...`)
-        $datasets['ID more specific than 99 classes'] = [
-            '<p class="p-4" id="p4"',
-            \str_repeat('.p-4', 99),
-            '#p4',
-        ];
-
-        return $datasets;
-    }
-
-    /**
-     * @test
-     *
-     * @param string $matchedTagPart Tag expected to be matched by both selectors, without the closing '>',
-     *                               e.g. '<p class="p-1"'
-     * @param string $lessSpecificSelector A selector expression
-     * @param string $moreSpecificSelector Some other, more specific selector expression
-     *
-     * @dataProvider differentCssSelectorSpecificityDataProvider
-     */
-    public function emogrifyAppliesMoreSpecificCssSelectorToMatchingElements(
-        $matchedTagPart,
-        $lessSpecificSelector,
-        $moreSpecificSelector
-    ) {
-        $subject = $this->buildDebugSubject(static::COMMON_TEST_HTML);
-        $subject->setCss(
-            $lessSpecificSelector . ' { color: red; } ' .
-            $moreSpecificSelector . ' { color: green; } ' .
-            $moreSpecificSelector . ' { background-color: green; } ' .
-            $lessSpecificSelector . ' { background-color: red; }'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains($matchedTagPart . ' style="color: green; background-color: green;"', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function equalCssSelectorSpecificityDataProvider()
-    {
-        return [
-            // pseudo-class
-            'pseudo-class as specific as class' => ['<p class="p-1"', '*:first-child', '.p-1'],
-            'type & pseudo-class as specific as type & class' => ['<p class="p-1"', 'p:first-child', 'p.p-1'],
-            'class & pseudo-class as specific as two classes' => ['<p class="p-1"', '.p-1:first-child', '.p-1.p-1'],
-            'ID & pseudo-class as specific as ID & class' => [
-                '<span title="avez-vous"',
-                '#p4 *:first-child',
-                '#p4.p-4 *',
-            ],
-            '2 types & 2 classes & 2 IDs & pseudo-class as specific as 2 types & 3 classes & 2 IDs' => [
-                '<span id="text"',
-                'p.p-4.p-4#p4 span#text:last-child',
-                'p.p-4.p-4.p-4#p4 span#text',
-            ],
-            // attribute
-            'attribute as specific as class' => ['<span title="bonjour"', '[title="bonjour"]', '.p-2 *'],
-            'type & attribute as specific as type & class' => [
-                '<span title="bonjour"',
-                'span[title="bonjour"]',
-                '.p-2 span',
-            ],
-            'class & attribute as specific as two classes' => ['<p class="p-4" id="p4"', '.p-4[id="p4"]', '.p-4.p-4'],
-            'ID & attribute as specific as ID & class' => ['<p class="p-4" id="p4"', '#p4[id="p4"]', '#p4.p-4'],
-            '2 types & 2 classes & 2 IDs & attribute as specific as 2 types & 3 classes & 2 IDs' => [
-                '<span id="text"',
-                'p.p-4.p-4#p4[id="p4"] span#text',
-                'p.p-4.p-4.p-4#p4 span#text',
-            ],
-            // :not
-            // ideally these tests would be more minimal with just combinators and universal selectors in the :not
-            // argument, however Symfony CssSelector only supports simple (single-element) selectors here
-            ':not with type as specific as type and universal' => ['<p class="p-1"', '*:not(html)', 'html *'],
-            'type & :not with type as specific as 2 types' => ['<p class="p-1"', 'p:not(html)', 'html p'],
-            'class & :not with type as specific as type & class' => ['<p class="p-1"', '.p-1:not(html)', 'html .p-1'],
-            'ID & :not with type as specific as type & ID' => ['<p class="p-4" id="p4"', '#p4:not(html)', 'html #p4'],
-            '2 types & 2 classes & 2 IDs & :not with type as specific as 3 types & 2 classes & 2 IDs' => [
-                '<span id="text"',
-                'p.p-4.p-4#p4 span#text:not(html)',
-                'html p.p-4.p-4#p4 span#text',
-            ],
-            // argument of :not
-            ':not with type as specific as type' => ['<p class="p-1"', '*:not(h1)', 'p'],
-            ':not with class as specific as class' => ['<p class="p-1"', '*:not(.p-2)', '.p-1'],
-            ':not with ID as specific as ID' => ['<p class="p-4" id="p4"', '*:not(#p1)', '#p4'],
-            // broken: :not with 2 types & 2 classes & 2 IDs as specific as 2 types & 2 classes & 2 IDs
-            //         (`*:not(.p-1 #p1)`, i.e. with both class and ID, causes "Invalid type in selector")
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $matchedTagPart Tag expected to be matched by both selectors, without the closing '>',
-     *                               e.g. '<p class="p-1"'
-     * @param string $selector1 A selector expression
-     * @param string $selector2 Some other, equally specific selector expression
-     *
-     * @dataProvider equalCssSelectorSpecificityDataProvider
-     */
-    public function emogrifyAppliesLaterEquallySpecificCssSelectorToMatchingElements(
-        $matchedTagPart,
-        $selector1,
-        $selector2
-    ) {
-        $subject = $this->buildDebugSubject(static::COMMON_TEST_HTML);
-        $subject->setCss(
-            $selector1 . ' { color: red; } ' .
-            $selector2 . ' { color: green; } ' .
-            $selector2 . ' { background-color: red; } ' .
-            $selector1 . ' { background-color: green; }'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains($matchedTagPart . ' style="color: green; background-color: green;"', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function cssDeclarationWhitespaceDroppingDataProvider()
-    {
-        return [
-            'no whitespace, trailing semicolon' => ['color:#000;'],
-            'no whitespace, no trailing semicolon' => ['color:#000'],
-            'space after colon, no trailing semicolon' => ['color: #000'],
-            'space before colon, no trailing semicolon' => ['color :#000'],
-            'space before property name, no trailing semicolon' => [' color:#000'],
-            'space before trailing semicolon' => [' color:#000 ;'],
-            'space after trailing semicolon' => [' color:#000; '],
-            'space after property value, no trailing semicolon' => [' color:#000 '],
-            'space after property value, trailing semicolon' => [' color:#000; '],
-            'newline before property name, trailing semicolon' => ["\ncolor:#000;"],
-            'newline after property semicolon' => ["color:#000;\n"],
-            'newline before colon, trailing semicolon' => ["color\n:#000;"],
-            'newline after colon, trailing semicolon' => ["color:\n#000;"],
-            'newline after semicolon' => ["color:#000\n;"],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $cssDeclaration the CSS declaration block (without the curly braces)
-     *
-     * @dataProvider cssDeclarationWhitespaceDroppingDataProvider
-     */
-    public function emogrifyTrimsWhitespaceFromCssDeclarations($cssDeclaration)
-    {
-        $subject = $this->buildDebugSubject('<html></html>');
-        $subject->setCss('html {' . $cssDeclaration . '}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<html style="color: #000;">', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function formattedCssDeclarationDataProvider()
-    {
-        return [
-            'one declaration' => ['color: #000;', 'color: #000;'],
-            'one declaration with dash in property name' => ['font-weight: bold;', 'font-weight: bold;'],
-            'one declaration with space in property value' => ['margin: 0 4px;', 'margin: 0 4px;'],
-            'two declarations separated by semicolon' => ['color: #000;width: 3px;', 'color: #000; width: 3px;'],
-            'two declarations separated by semicolon & space'
-            => ['color: #000; width: 3px;', 'color: #000; width: 3px;'],
-            'two declarations separated by semicolon & linefeed' => [
-                "color: #000;\nwidth: 3px;",
-                'color: #000; width: 3px;',
-            ],
-            'two declarations separated by semicolon & Windows line ending' => [
-                "color: #000;\r\nwidth: 3px;",
-                'color: #000; width: 3px;',
-            ],
-            'one declaration with leading dash in property name' => [
-                '-webkit-text-size-adjust:none;',
-                '-webkit-text-size-adjust: none;',
-            ],
-            'one declaration with linefeed in property value' => [
-                "text-shadow:\n1px 1px 3px #000,\n1px 1px 1px #000;",
-                "text-shadow: 1px 1px 3px #000,\n1px 1px 1px #000;",
-            ],
-            'one declaration with Windows line ending in property value' => [
-                "text-shadow:\r\n1px 1px 3px #000,\r\n1px 1px 1px #000;",
-                "text-shadow: 1px 1px 3px #000,\r\n1px 1px 1px #000;",
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
-     * @param string $expectedStyleAttributeContent the expected value of the style attribute
-     *
-     * @dataProvider formattedCssDeclarationDataProvider
-     */
-    public function emogrifyFormatsCssDeclarations($cssDeclarationBlock, $expectedStyleAttributeContent)
-    {
-        $subject = $this->buildDebugSubject('<html></html>');
-        $subject->setCss('html {' . $cssDeclarationBlock . '}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<html style="' . $expectedStyleAttributeContent . '">', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function invalidDeclarationDataProvider()
-    {
-        return [
-            'missing dash in property name' => ['font weight: bold;'],
-            'invalid character in property name' => ['-9webkit-text-size-adjust:none;'],
-            'missing :' => ['-webkit-text-size-adjust none'],
-            'missing value' => ['-webkit-text-size-adjust :'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
-     *
-     * @dataProvider invalidDeclarationDataProvider
-     */
-    public function emogrifyDropsInvalidCssDeclaration($cssDeclarationBlock)
-    {
-        $subject = $this->buildDebugSubject('<html></html>');
-        $subject->setCss('html {' . $cssDeclarationBlock . '}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<html style="">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleAttributes()
-    {
-        $styleAttribute = 'style="color: #ccc;"';
-        $subject = $this->buildDebugSubject('<html ' . $styleAttribute . '></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertContains($styleAttribute, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAddsNewCssBeforeExistingStyle()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $subject = $this->buildDebugSubject('<html style="' . $styleAttributeValue . '"></html>');
-        $cssDeclarations = 'margin: 0 2px;';
-        $css = 'html {' . $cssDeclarations . '}';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContains('style="' . $cssDeclarations . ' ' . $styleAttributeValue . '"', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyCanMatchMinifiedCss()
-    {
-        $subject = $this->buildDebugSubject('<html><p></p></html>');
-        $subject->setCss('p{color:blue;}html{color:red;}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<html style="color: red;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
-    {
-        $subject = $this->buildDebugSubject('<html style="COLOR:#ccc;"></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('style="color: #ccc;"', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyLowercasesAttributeNamesFromPassedInCss()
-    {
-        $subject = $this->buildDebugSubject('<html></html>');
-        $subject->setCss('html {mArGiN:0 2pX;}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('style="margin: 0 2pX;"', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
-    {
-        $cssDeclaration = "content: 'Hello World';";
-        $subject = $this->buildDebugSubject('<html><body><p>target</p></body></html>');
-        $subject->setCss('p {' . $cssDeclaration . '}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="' . $cssDeclaration . '">target</p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
-    {
-        $cssDeclaration = "content: 'Hello World';";
-        $subject = $this->buildDebugSubject(
-            '<html><head><style>p {' . $cssDeclaration . '}</style></head><body><p>target</p></body></html>'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="' . $cssDeclaration . '">target</p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyRemovesStyleNodes()
-    {
-        $subject = $this->buildDebugSubject('<html><style type="text/css"></style></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('<style', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \Symfony\Component\CssSelector\Exception\SyntaxErrorException
-     */
-    public function emogrifyInDebugModeForInvalidCssSelectorThrowsException()
-    {
-        $subject = new CssInliner(
-            '<html><style type="text/css">p{color:red;} <style data-x="1">html{cursor:text;}</style></html>'
-        );
-        $subject->setDebug(true);
-
-        $subject->emogrify();
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeIgnoresInvalidCssSelectors()
-    {
-        $html = '<html><style type="text/css">' .
-            'p{color:red;} <style data-x="1">html{cursor:text;} p{background-color:blue;}</style> ' .
-            '<body><p></p></body></html>';
-        $subject = new CssInliner($html);
-        $subject->setDebug(false);
-
-        $html = $subject->emogrify();
-
-        static::assertContains('color: red', $html);
-        static::assertContains('background-color: blue', $html);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultIgnoresInvalidCssSelectors()
-    {
-        $html = '<html><style type="text/css">' .
-            'p{color:red;} <style data-x="1">html{cursor:text;} p{background-color:blue;}</style> ' .
-            '<body><p></p></body></html>';
-        $subject = new CssInliner($html);
-
-        $html = $subject->emogrify();
-        static::assertContains('color: red', $html);
-        static::assertContains('background-color: blue', $html);
-    }
-
-    /**
-     * Data provider for things that should be left out when applying the CSS.
-     *
-     * @return string[][]
-     */
-    public function unneededCssThingsDataProvider()
-    {
-        return [
-            'CSS comments with one asterisk' => ['p {color: #000;/* black */}', 'black'],
-            'CSS comments with two asterisks' => ['p {color: #000;/** black */}', 'black'],
-            '@import directive' => ['@import "foo.css";', '@import'],
-            'two @import directives, minified' => ['@import "foo.css";@import "bar.css";', '@import'],
-            '@charset directive' => ['@charset "UTF-8";', '@charset'],
-            'style in "aural" media type rule' => ['@media aural {p {color: #000;}}', '#000'],
-            'style in "braille" media type rule' => ['@media braille {p {color: #000;}}', '#000'],
-            'style in "embossed" media type rule' => ['@media embossed {p {color: #000;}}', '#000'],
-            'style in "handheld" media type rule' => ['@media handheld {p {color: #000;}}', '#000'],
-            'style in "projection" media type rule' => ['@media projection {p {color: #000;}}', '#000'],
-            'style in "speech" media type rule' => ['@media speech {p {color: #000;}}', '#000'],
-            'style in "tty" media type rule' => ['@media tty {p {color: #000;}}', '#000'],
-            'style in "tv" media type rule' => ['@media tv {p {color: #000;}}', '#000'],
-            'style in "tv" media type rule with extra spaces' => [
-                '  @media  tv  {  p  {  color  :  #000  ;  }  }  ',
-                '#000',
-            ],
-            'style in "tv" media type rule with linefeeds' => [
-                "\n@media\ntv\n{\np\n{\ncolor\n:\n#000\n;\n}\n}\n",
-                '#000',
-            ],
-            'style in "tv" media type rule with Windows line endings' => [
-                "\r\n@media\r\ntv\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000\r\n;\r\n}\r\n}\r\n",
-                '#000',
-            ],
-            'style in "only tv" media type rule' => ['@media only tv {p {color: #000;}}', '#000'],
-            'style in "only tv" media type rule with extra spaces' => [
-                '  @media  only  tv  {  p  {  color  :  #000  ;  }  }  ',
-                '#000',
-            ],
-            'style in "only tv" media type rule with linefeeds' => [
-                "\n@media\nonly\ntv\n{\np\n{\ncolor\n:\n#000\n;\n}\n}\n",
-                '#000',
-            ],
-            'style in "only tv" media type rule with Windows line endings' => [
-                "\r\n@media\r\nonly\r\ntv\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000\r\n;\r\n}\r\n}\r\n",
-                '#000',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $unneededCss
-     * @param string $markerNotExpectedInHtml
-     *
-     * @dataProvider unneededCssThingsDataProvider
-     */
-    public function emogrifyFiltersUnneededCssThings($unneededCss, $markerNotExpectedInHtml)
-    {
-        $subject = $this->buildDebugSubject('<html><p>foo</p></html>');
-        $subject->setCss($unneededCss);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains($markerNotExpectedInHtml, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $unneededCss
-     *
-     * @dataProvider unneededCssThingsDataProvider
-     */
-    public function emogrifyMatchesRuleAfterUnneededCssThing($unneededCss)
-    {
-        $subject = $this->buildDebugSubject('<html><body></body></html>');
-        $subject->setCss($unneededCss . ' body { color: green; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<body style="color: green;">', $result);
-    }
-
-    /**
-     * Data provider for media rules.
-     *
-     * @return string[][]
-     */
-    public function mediaRulesDataProvider()
-    {
-        return [
-            'style in "only all" media type rule' => ['@media only all {p {color: #000;}}'],
-            'style in "only screen" media type rule' => ['@media only screen {p {color: #000;}}'],
-            'style in "only screen" media type rule with extra spaces'
-            => ['  @media  only  screen  {  p  {  color  :  #000;  }  }  '],
-            'style in "only screen" media type rule with linefeeds'
-            => ["\n@media\nonly\nscreen\n{\np\n{\ncolor\n:\n#000;\n}\n}\n"],
-            'style in "only screen" media type rule with Windows line endings'
-            => ["\r\n@media\r\nonly\r\nscreen\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000;\r\n}\r\n}\r\n"],
-            'style in media type rule' => ['@media {p {color: #000;}}'],
-            'style in media type rule with extra spaces' => ['  @media  {  p  {  color  :  #000;  }  }  '],
-            'style in media type rule with linefeeds' => ["\n@media\n{\np\n{\ncolor\n:\n#000;\n}\n}\n"],
-            'style in media type rule with Windows line endings'
-            => ["\r\n@media\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000;\r\n}\r\n}\r\n"],
-            'style in "screen" media type rule' => ['@media screen {p {color: #000;}}'],
-            'style in "screen" media type rule with extra spaces'
-            => ['  @media  screen  {  p  {  color  :  #000;  }  }  '],
-            'style in "screen" media type rule with linefeeds'
-            => ["\n@media\nscreen\n{\np\n{\ncolor\n:\n#000;\n}\n}\n"],
-            'style in "screen" media type rule with Windows line endings'
-            => ["\r\n@media\r\nscreen\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000;\r\n}\r\n}\r\n"],
-            'style in "print" media type rule' => ['@media print {p {color: #000;}}'],
-            'style in "all" media type rule' => ['@media all {p {color: #000;}}'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider mediaRulesDataProvider
-     */
-    public function emogrifyKeepsMediaRules($css)
-    {
-        $subject = $this->buildDebugSubject('<html><p>foo</p></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function orderedRulesAndSurroundingCssDataProvider()
-    {
-        $possibleSurroundingCss = [
-            'nothing' => '',
-            'space' => ' ',
-            'linefeed' => "\n",
-            'Windows line ending' => "\r\n",
-            'comment' => '/* hello */',
-            'other non-matching CSS' => 'h6 { color: #f00; }',
-            'other matching CSS' => 'p { color: #f00; }',
-            'disallowed media rule' => '@media tv { p { color: #f00; } }',
-            'allowed but non-matching media rule' => '@media screen { h6 { color: #f00; } }',
-            'non-matching CSS with pseudo-component' => 'h6:hover { color: #f00; }',
-        ];
-        $possibleCssBefore = $possibleSurroundingCss + [
-                '@import' => '@import "foo.css";',
-                '@charset' => '@charset "UTF-8";',
-            ];
-
-        $datasetsSurroundingCss = [];
-        foreach ($possibleCssBefore as $descriptionBefore => $cssBefore) {
-            foreach ($possibleSurroundingCss as $descriptionBetween => $cssBetween) {
-                foreach ($possibleSurroundingCss as $descriptionAfter => $cssAfter) {
-                    // every combination would be a ridiculous c.1000 datasets - choose a select few
-                    // test all possible CSS before once
-                    if (($cssBetween === '' && $cssAfter === '')
-                        // test all possible CSS between once
-                        || ($cssBefore === '' && $cssAfter === '')
-                        // test all possible CSS after once
-                        || ($cssBefore === '' && $cssBetween === '')
-                        // test with each possible CSS in all three positions
-                        || ($cssBefore === $cssBetween && $cssBetween === $cssAfter)
-                    ) {
-                        $description = ' with ' . $descriptionBefore . ' before, '
-                            . $descriptionBetween . ' between, '
-                            . $descriptionAfter . ' after';
-                        $datasetsSurroundingCss[$description] = [$cssBefore, $cssBetween, $cssAfter];
-                    }
-                }
-            }
-        }
-
-        $datasets = [];
-        foreach ($datasetsSurroundingCss as $description => $datasetSurroundingCss) {
-            $datasets += [
-                'two media rules' . $description => \array_merge(
-                    ['@media all { p { color: #333; } }', '@media print { p { color: #000; } }'],
-                    $datasetSurroundingCss
-                ),
-                'two rules involving pseudo-components' . $description => \array_merge(
-                    ['a:hover { color: blue; }', 'a:active { color: green; }'],
-                    $datasetSurroundingCss
-                ),
-                'media rule followed by rule involving pseudo-components' . $description => \array_merge(
-                    ['@media screen { p { color: #000; } }', 'a:hover { color: green; }'],
-                    $datasetSurroundingCss
-                ),
-                'rule involving pseudo-components followed by media rule' . $description => \array_merge(
-                    ['a:hover { color: green; }', '@media screen { p { color: #000; } }'],
-                    $datasetSurroundingCss
-                ),
-            ];
-        }
-        return $datasets;
-    }
-
-    /**
-     * @test
-     *
-     * @param string $rule1
-     * @param string $rule2
-     * @param string $cssBefore CSS to insert before the first rule
-     * @param string $cssBetween CSS to insert between the rules
-     * @param string $cssAfter CSS to insert after the second rule
-     *
-     * @dataProvider orderedRulesAndSurroundingCssDataProvider
-     */
-    public function emogrifyKeepsRulesCopiedToStyleElementInSpecifiedOrder(
-        $rule1,
-        $rule2,
-        $cssBefore,
-        $cssBetween,
-        $cssAfter
-    ) {
-        $subject = $this->buildDebugSubject('<html><p><a>foo</a></p></html>');
-        $subject->setCss($cssBefore . $rule1 . $cssBetween . $rule2 . $cssAfter);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss($rule1 . $rule2, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function removeAllowedMediaTypeRemovesStylesForTheGivenMediaType()
-    {
-        $css = '@media screen { html { some-property: value; } }';
-        $subject = $this->buildDebugSubject('<html></html>');
-        $subject->setCss($css);
-        $subject->removeAllowedMediaType('screen');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addAllowedMediaTypeKeepsStylesForTheGivenMediaType()
-    {
-        $css = '@media braille { html { some-property: value; } }';
-        $subject = $this->buildDebugSubject('<html></html>');
-        $subject->setCss($css);
-        $subject->addAllowedMediaType('braille');
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingHeadElementContent()
-    {
-        $subject = $this->buildDebugSubject('<html><head><!-- original content --></head></html>');
-        $subject->setCss('@media all { html { some-property: value; } }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<!-- original content -->', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleElementWithMedia()
-    {
-        $html = $this->html5DocumentType . '<html><head><!-- original content --></head><body></body></html>';
-        $subject = $this->buildDebugSubject($html);
-        $subject->setCss('@media all { html { some-property: value; } }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<style type="text/css">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleElementWithMediaInHead()
-    {
-        $style = '<style type="text/css">@media all { html {  color: red; } }</style>';
-        $html = '<html><head>' . $style . '</head><body></body></html>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->emogrify();
-
-        static::assertRegExp('/<head>.*<style.*<\\/head>/s', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleElementWithMediaOutOfBody()
-    {
-        $style = '<style type="text/css">@media all { html {  color: red; } }</style>';
-        $html = '<html><head>' . $style . '</head><body></body></html>';
-        $subject = $this->buildDebugSubject($html);
-
-        $result = $subject->emogrify();
-
-        static::assertNotRegExp('/<body>.*<style/s', $result);
-    }
-
-    /**
-     * Valid media query which need to be preserved
-     *
-     * @return string[][]
-     */
-    public function validMediaPreserveDataProvider()
-    {
-        return [
-            'style in "only screen and size" media type rule' => [
-                '@media only screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "screen size" media type rule' => [
-                '@media screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "only screen and screen size" media type rule' => [
-                '@media only screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "all and screen size" media type rule' => [
-                '@media all and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "only all and" media type rule' => [
-                '@media only all and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "all" media type rule' => ['@media all {p {color: #000;}}'],
-            'style in "only screen" media type rule' => ['@media only screen { h1 { color:red; } }'],
-            'style in "only all" media type rule' => ['@media only all { h1 { color:red; } }'],
-            'style in "screen" media type rule' => ['@media screen { h1 { color:red; } }'],
-            'style in "print" media type rule' => ['@media print { * { color:#000 !important; } }'],
-            'style in media type rule without specification' => ['@media { h1 { color:red; } }'],
-            'style with multiple media type rules' => [
-                '@media all { p { color: #000; } }' .
-                '@media only screen { h1 { color:red; } }' .
-                '@media only all { h1 { color:red; } }' .
-                '@media print { * { color:#000 !important; } }' .
-                '@media { h1 { color:red; } }',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyWithValidMediaQueryContainsInnerCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1><p></p></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss('<style type="text/css">' . $css . '</style>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyWithValidMinifiedMediaQueryContainsInnerCss($css)
-    {
-        // Minify CSS by removing unnecessary whitespace.
-        $css = \preg_replace('/\\s*{\\s*/', '{', $css);
-        $css = \preg_replace('/;?\\s*}\\s*/', '}', $css);
-        $css = \preg_replace('/@media{/', '@media {', $css);
-
-        $subject = $this->buildDebugSubject('<html><h1></h1><p></p></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<style type="text/css">' . $css . '</style>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyForHtmlWithValidMediaQueryContainsInnerCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><style type="text/css">' . $css . '</style><h1></h1><p></p></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss('<style type="text/css">' . $css . '</style>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyWithValidMediaQueryNotContainsInlineCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * Invalid media query which need to be strip
-     *
-     * @return string[][]
-     */
-    public function invalidMediaPreserveDataProvider()
-    {
-        return [
-            'style in "braille" type rule' => ['@media braille { h1 { color:red; } }'],
-            'style in "embossed" type rule' => ['@media embossed { h1 { color:red; } }'],
-            'style in "handheld" type rule' => ['@media handheld { h1 { color:red; } }'],
-            'style in "projection" type rule' => ['@media projection { h1 { color:red; } }'],
-            'style in "speech" type rule' => ['@media speech { h1 { color:red; } }'],
-            'style in "tty" type rule' => ['@media tty { h1 { color:red; } }'],
-            'style in "tv" type rule' => ['@media tv { h1 { color:red; } }'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyWithInvalidMediaQueryNotContainsInnerCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyWithInvalidMediaQueryNotContainsInlineCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyFromHtmlWithInvalidMediaQueryNotContainsInnerCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><style type="text/css">' . $css . '</style><h1></h1></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyFromHtmlWithInvalidMediaQueryNotContainsInlineCss($css)
-    {
-        $subject = $this->buildDebugSubject('<html><style type="text/css">' . $css . '</style><h1></h1></html>');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyIgnoresEmptyMediaQuery()
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss('@media screen {} @media tv { h1 { color: red; } }');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-        static::assertNotContains('@media screen', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyIgnoresMediaQueryWithWhitespaceOnly()
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss('@media screen { } @media tv { h1 { color: red; } }');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-        static::assertNotContains('@media screen', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function mediaTypeDataProvider()
-    {
-        return [
-            'disallowed type' => ['tv'],
-            'allowed type' => ['screen'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     *
-     * @dataProvider mediaTypeDataProvider
-     */
-    public function emogrifyKeepsMediaRuleAfterEmptyMediaRule($emptyRuleMediaType)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss('@media ' . $emptyRuleMediaType . ' {} @media all { h1 { color: red; } }');
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss('@media all { h1 { color: red; } }', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     *
-     * @dataProvider mediaTypeDataProvider
-     */
-    public function emogrifyNotKeepsUnneededMediaRuleAfterEmptyMediaRule($emptyRuleMediaType)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss('@media ' . $emptyRuleMediaType . ' {} @media speech { h1 { color: red; } }');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @param string[] $precedingSelectorComponents Array of selectors to which each type of pseudo-component is
-     *                                              appended to create a selector for a CSS rule.
-     *                                              Keys are human-readable descriptions.
-     *
-     * @return string[][]
-     */
-    private function getCssRuleDatasetsWithSelectorPseudoComponents(array $precedingSelectorComponents)
-    {
-        $rulesComponents = [
-            'pseudo-element' => [
-                'selectorPseudoComponent' => '::after',
-                'declarationsBlock' => 'content: "bar";',
-            ],
-            'CSS2 pseudo-element' => [
-                'selectorPseudoComponent' => ':after',
-                'declarationsBlock' => 'content: "bar";',
-            ],
-            'hyphenated pseudo-element' => [
-                'selectorPseudoComponent' => '::first-letter',
-                'declarationsBlock' => 'color: green;',
-            ],
-            'pseudo-class' => [
-                'selectorPseudoComponent' => ':hover',
-                'declarationsBlock' => 'color: green;',
-            ],
-            'hyphenated pseudo-class' => [
-                'selectorPseudoComponent' => ':read-only',
-                'declarationsBlock' => 'color: green;',
-            ],
-            'pseudo-class with parameter' => [
-                'selectorPseudoComponent' => ':lang(en)',
-                'declarationsBlock' => 'color: green;',
-            ],
-        ];
-
-        $datasets = [];
-        foreach ($precedingSelectorComponents as $precedingComponentDescription => $precedingSelectorComponent) {
-            foreach ($rulesComponents as $pseudoComponentDescription => $ruleComponents) {
-                $datasets[$precedingComponentDescription . ' ' . $pseudoComponentDescription] = [
-                    $precedingSelectorComponent . $ruleComponents['selectorPseudoComponent']
-                    . ' { ' . $ruleComponents['declarationsBlock'] . ' }',
-                ];
-            }
-        }
-        return $datasets;
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function matchingSelectorWithPseudoComponentCssRuleDataProvider()
-    {
-        $datasetsWithSelectorPseudoComponents = $this->getCssRuleDatasetsWithSelectorPseudoComponents(
-            [
-                'lone' => '',
-                'type &' => 'a',
-                'class &' => '.a',
-                'ID &' => '#a',
-                'attribute &' => 'a[href="a"]',
-                'static pseudo-class &' => 'a:first-child',
-                'ancestor &' => 'p ',
-                'ancestor & type &' => 'p a',
-            ]
-        );
-        $datasetsWithCombinedPseudoSelectors = [
-            'pseudo-class & descendant' => ['p:hover a { color: green; }'],
-            'pseudo-class & pseudo-element' => ['a:hover::after { content: "bar"; }'],
-            'pseudo-element & pseudo-class' => ['a::after:hover { content: "bar"; }'],
-            'two pseudo-classes' => ['a:focus:hover { color: green; }'],
-        ];
-
-        return \array_merge($datasetsWithSelectorPseudoComponents, $datasetsWithCombinedPseudoSelectors);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider matchingSelectorWithPseudoComponentCssRuleDataProvider
-     */
-    public function emogrifyKeepsRuleWithPseudoComponentInMatchingSelector($css)
-    {
-        $subject = $this->buildDebugSubject('<html><p><a id="a" class="a" href="a">foo</a></p></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        self::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function nonMatchingSelectorWithPseudoComponentCssRuleDataProvider()
-    {
-        $datasetsWithSelectorPseudoComponents = $this->getCssRuleDatasetsWithSelectorPseudoComponents(
-            [
-                'type &' => 'b',
-                'class &' => '.b',
-                'ID &' => '#b',
-                'attribute &' => 'a[href="b"]',
-                'static pseudo-class &' => 'a:not(.a)',
-                'ancestor &' => 'ul ',
-                'ancestor & type &' => 'p b',
-            ]
-        );
-        $datasetsWithCombinedPseudoSelectors = [
-            'pseudo-class & descendant' => ['ul:hover a { color: green; }'],
-            'pseudo-class & pseudo-element' => ['b:hover::after { content: "bar"; }'],
-            'pseudo-element & pseudo-class' => ['b::after:hover { content: "bar"; }'],
-            'two pseudo-classes' => ['input:focus:hover { color: green; }'],
-        ];
-
-        return \array_merge($datasetsWithSelectorPseudoComponents, $datasetsWithCombinedPseudoSelectors);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider nonMatchingSelectorWithPseudoComponentCssRuleDataProvider
-     */
-    public function emogrifyNotKeepsRuleWithPseudoComponentInNonMatchingSelector($css)
-    {
-        $subject = $this->buildDebugSubject('<html><p><a id="a" class="a" href="#">foo</a></p></html>');
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        self::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsRuleInMediaQueryWithPseudoComponentInMatchingSelector()
-    {
-        $subject = $this->buildDebugSubject('<html><a>foo</a></html>');
-        $css = '@media screen { a:hover { color: green; } }';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        self::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotKeepsRuleInMediaQueryWithPseudoComponentInNonMatchingSelector()
-    {
-        $subject = $this->buildDebugSubject('<html><a>foo</a></html>');
-        $css = '@media screen { b:hover { color: green; } }';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        self::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsRuleWithPseudoComponentInMultipleMatchingSelectorsFromSingleRule()
-    {
-        $subject = $this->buildDebugSubject('<html><p>foo</p><a>bar</a></html>');
-        $css = 'p:hover, a:hover { color: green; }';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsOnlyMatchingSelectorsWithPseudoComponentFromSingleRule()
-    {
-        $subject = $this->buildDebugSubject('<html><a>foo</a></html>');
-        $subject->setCss('p:hover, a:hover { color: green; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss('<style type="text/css">a:hover { color: green; }</style>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesCssToMatchingElementsAndKeepsRuleWithPseudoComponentFromSingleRule()
-    {
-        $subject = $this->buildDebugSubject('<html><p>foo</p><a>bar</a></html>');
-        $subject->setCss('p, a:hover { color: green; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="color: green;">', $result);
-        static::assertContainsCss('<style type="text/css">a:hover { color: green; }</style>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function mediaTypesDataProvider()
-    {
-        return [
-            'disallowed type after disallowed type' => ['tv', 'speech'],
-            'allowed type after disallowed type' => ['tv', 'all'],
-            'disallowed type after allowed type' => ['screen', 'tv'],
-            'allowed type after allowed type' => ['screen', 'all'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     * @param string $mediaType
-     *
-     * @dataProvider mediaTypesDataProvider
-     */
-    public function emogrifyAppliesCssBetweenEmptyMediaRuleAndMediaRule($emptyRuleMediaType, $mediaType)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss(
-            '@media ' . $emptyRuleMediaType . ' {} h1 { color: green; } @media ' . $mediaType
-            . ' { h1 { color: red; } }'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<h1 style="color: green;">', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     * @param string $mediaType
-     *
-     * @dataProvider mediaTypesDataProvider
-     */
-    public function emogrifyAppliesCssBetweenEmptyMediaRuleAndMediaRuleWithCssAfter($emptyRuleMediaType, $mediaType)
-    {
-        $subject = $this->buildDebugSubject('<html><h1></h1></html>');
-        $subject->setCss(
-            '@media ' . $emptyRuleMediaType . ' {} h1 { color: green; } @media ' . $mediaType
-            . ' { h1 { color: red; } } h1 { font-size: 24px; }'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<h1 style="color: green; font-size: 24px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesCssFromStyleNodes()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $subject = $this->buildDebugSubject(
-            '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<html style="' . $styleAttributeValue . '">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $subject = $this->buildDebugSubject(
-            '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>'
-        );
-        $subject->disableStyleBlocksParsing();
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles()
-    {
-        $styleAttributeValue = 'text-align: center;';
-        $subject = $this->buildDebugSubject(
-            '<html><head><style type="text/css">p { color: #ccc; }</style></head>' .
-            '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>'
-        );
-        $subject->disableStyleBlocksParsing();
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="' . $styleAttributeValue . '">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
-    {
-        $subject = $this->buildDebugSubject('<html style="color: #ccc;"></html>');
-        $subject->disableInlineStyleAttributesParsing();
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('<html style', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $subject = $this->buildDebugSubject(
-            '<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>' .
-            '<body><p style="text-align: center;">paragraph</p></body></html>'
-        );
-        $subject->disableInlineStyleAttributesParsing();
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="' . $styleAttributeValue . '">', $result);
-    }
-
-    /**
-     * Emogrify was handling case differently for passed-in CSS vs. CSS parsed from style blocks.
-     *
-     * @test
-     */
-    public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><head><style>#topWrap p {padding-bottom: 1px;PADDING-TOP: 0;}</style></head>' .
-            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="padding-bottom: 1px; padding-top: 0; text-align: center;">', $result);
-    }
-
-    /**
-     * Style block CSS overrides values.
-     *
-     * @test
-     */
-    public function emogrifyMergesCssWithMixedCaseAttribute()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><head><style>#topWrap p {padding-bottom: 3px;PADDING-TOP: 1px;}</style></head>' .
-            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
-        );
-        $subject->setCss('p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains(
-            '<p style="margin: 0; padding-bottom: 3px; padding-top: 1px; text-align: center;">',
-            $result
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyMergesCssWithMixedUnits()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><head><style>#topWrap p {margin:0;padding-bottom: 1px;}</style></head>' .
-            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
-        );
-        $subject->setCss('p { margin: 1px; padding-bottom:0;}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 0; padding-bottom: 1px; text-align: center;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultRemovesElementsWithDisplayNoneFromExternalCss()
-    {
-        $subject = $this->buildDebugSubject('<html><body><div class="foo"></div></body></html>');
-        $subject->setCss('div.foo { display: none; }');
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('<div class="foo"></div>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultRemovesElementsWithDisplayNoneInStyleAttribute()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><body><div class="foobar" style="display: none;"></div>' .
-            '</body></html>'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('<div', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAfterDisableInvisibleNodeRemovalPreservesInvisibleElements()
-    {
-        $subject = $this->buildDebugSubject('<html><body><div class="foo"></div></body></html>');
-        $subject->setCss('div.foo { display: none; }');
-
-        $subject->disableInvisibleNodeRemoval();
-        $result = $subject->emogrify();
-
-        static::assertContains('<div class="foo" style="display: none;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsCssMediaQueriesWithCssCommentAfterMediaQuery()
-    {
-        $subject = $this->buildDebugSubject('<html><body></body></html>');
-        $subject->setCss(
-            '@media only screen and (max-width: 480px) { body { color: #ffffff } /* some comment */ }'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('@media only screen and (max-width: 480px)', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function renderConvertsXmlSelfClosingTagsToNonXmlSelfClosingTag($documentType)
-    {
-        $subject = $this->buildDebugSubject(
-            $documentType . '<html><body><br/></body></html>'
-        );
-
-        $result = $subject->render();
-
-        static::assertContains('<br>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderAutomaticallyClosesUnclosedTag()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></body></html>');
-
-        $result = $subject->render();
-
-        static::assertContains('<body><p></p></body>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderReturnsCompleteHtmlDocument()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></p></body></html>');
-
-        $result = $subject->render();
-
-        static::assertSame(
-            $this->html5DocumentType . "\n" .
-            "<html>\n" .
-            '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . "\n" .
-            "<body><p></p></body>\n" .
-            "</html>\n",
-            $result
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyBodyContentReturnsBodyContentFromHtml()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></p></body></html>');
-
-        $result = $subject->emogrifyBodyContent();
-
-        static::assertSame('<p></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyBodyContentReturnsBodyContentFromPartialContent()
-    {
-        $subject = $this->buildDebugSubject('<p></p>');
-
-        $result = $subject->emogrifyBodyContent();
-
-        static::assertSame('<p></p>', $result);
-    }
-
-    /**
-     * Sets HTML of subject to boilerplate HTML with a single `<p>` in `<body>` and empty `<head>`
-     *
-     * @param string $style Optional value for the style attribute of the `<p>` element
-     *
-     * @return CssInliner
-     */
-    private function buildSubjectWithBoilerplateHtml($style = '')
-    {
-        $html = '<html><head></head><body><p';
-        if ($style !== '') {
-            $html .= ' style="' . $style . '"';
-        }
-        $html .= '>some content</p></body></html>';
-
-        return $this->buildDebugSubject($html);
-    }
-
-    /**
-     * @test
-     */
-    public function importantInExternalCssOverwritesInlineCss()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml('margin: 2px;');
-        $subject->setCss('p { margin: 1px !important; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 1px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function importantInExternalCssKeepsInlineCssForOtherAttributes()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml('margin: 2px; text-align: center;');
-        $subject->setCss('p { margin: 1px !important; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="text-align: center; margin: 1px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function importantIsCaseInsensitive()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml('margin: 2px;');
-        $subject->setCss('p { margin: 1px !ImPorTant; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 1px !ImPorTant;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function secondImportantStyleOverwritesFirstOne()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml();
-        $subject->setCss('p { margin: 1px !important; } p { margin: 2px !important; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function secondNonImportantStyleOverwritesFirstOne()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml();
-        $subject->setCss('p { margin: 1px; } p { margin: 2px; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function secondNonImportantStyleNotOverwritesFirstImportantOne()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml();
-        $subject->setCss('p { margin: 1px !important; } p { margin: 2px; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 1px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesLaterShorthandStyleAfterIndividualStyle()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml();
-        $subject->setCss('p { margin-top: 1px; } p { margin: 2px; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin-top: 1px; margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesLaterOverridingStyleAfterStyleAfterOverriddenStyle()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml();
-        $subject->setCss('p { margin-top: 1px; } p { margin: 2px; } p { margin-top: 3px; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px; margin-top: 3px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesInlineOverridingStyleAfterCssStyleAfterOverriddenCssStyle()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml('margin-top: 3px;');
-        $subject->setCss('p { margin-top: 1px; } p { margin: 2px; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px; margin-top: 3px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesLaterInlineOverridingStyleAfterEarlierInlineStyle()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml('margin: 2px; margin-top: 3px;');
-        $subject->setCss('p { margin-top: 1px; }');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px; margin-top: 3px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function irrelevantMediaQueriesAreRemoved()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></p></body></html>');
-        $uselessQuery = '@media all and (max-width: 500px) { em { color:red; } }';
-        $subject->setCss($uselessQuery);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function relevantMediaQueriesAreRetained()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></p></body></html>');
-        $usefulQuery = '@media all and (max-width: 500px) { p { color:red; } }';
-        $subject->setCss($usefulQuery);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss($usefulQuery, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function importantStyleRuleFromInlineCssOverwritesImportantStyleRuleFromExternalCss()
-    {
-        $subject = $this->buildSubjectWithBoilerplateHtml('margin: 2px !important; text-align: center;');
-        $subject->setCss('p { margin: 1px !important; padding: 1px;}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="padding: 1px; text-align: center; margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addExcludedSelectorRemovesMatchingElementsFromEmogrification()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p class="x"></p></body></html>');
-        $subject->setCss('p { margin: 0; }');
-
-        $subject->addExcludedSelector('p.x');
-        $result = $subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addExcludedSelectorExcludesMatchingElementEventWithWhitespaceAroundSelector()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p class="x"></p></body></html>');
-        $subject->setCss('p { margin: 0; }');
-
-        $subject->addExcludedSelector(' p.x ');
-        $result = $subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addExcludedSelectorKeepsNonMatchingElementsInEmogrification()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></p></body></html>');
-        $subject->setCss('p { margin: 0; }');
-
-        $subject->addExcludedSelector('p.x');
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="margin: 0;"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function removeExcludedSelectorGetsMatchingElementsToBeEmogrifiedAgain()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p class="x"></p></body></html>');
-        $subject->setCss('p { margin: 0; }');
-
-        $subject->addExcludedSelector('p.x');
-        $subject->removeExcludedSelector('p.x');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p class="x" style="margin: 0;"></p>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \Symfony\Component\CssSelector\Exception\SyntaxErrorException
-     */
-    public function emogrifyInDebugModeForInvalidExcludedSelectorThrowsException()
-    {
-        $subject = new CssInliner('<html></html>');
-        $subject->setDebug(true);
-
-        $subject->addExcludedSelector('..p');
-        $subject->emogrify();
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeIgnoresInvalidExcludedSelector()
-    {
-        $subject = new CssInliner('<html><p class="x"></p></html>');
-        $subject->setDebug(false);
-
-        $subject->addExcludedSelector('..p');
-        $result = $subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeIgnoresOnlyInvalidExcludedSelector()
-    {
-        $subject = new CssInliner('<html><p class="x"></p><p class="y"></p><p class="z"></p></html>');
-        $subject->setDebug(false);
-
-        $subject->setCss('p { color: red };');
-        $subject->addExcludedSelector('p.x');
-        $subject->addExcludedSelector('..p');
-        $subject->addExcludedSelector('p.z');
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-        static::assertContains('<p class="y" style="color: red;"></p>', $result);
-        static::assertContains('<p class="z"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emptyMediaQueriesAreRemoved()
-    {
-        $subject = $this->buildDebugSubject('<html><body><p></p></body></html>');
-        $emptyQuery = '@media all and (max-width: 500px) { }';
-        $subject->setCss($emptyQuery);
-
-        $result = $subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function multiLineMediaQueryWithWindowsLineEndingsIsAppliedOnlyOnce()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><body>' .
-            '<p class="medium">medium</p>' .
-            '<p class="small">small</p>' .
-            '</body></html>'
-        );
-        $css = "@media all {\r\n" .
-            ".medium {font-size:18px;}\r\n" .
-            ".small {font-size:14px;}\r\n" .
-            '}';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCssCount(1, $css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function multiLineMediaQueryWithUnixLineEndingsIsAppliedOnlyOnce()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><body>' .
-            '<p class="medium">medium</p>' .
-            '<p class="small">small</p>' .
-            '</body></html>'
-        );
-        $css = "@media all {\n" .
-            ".medium {font-size:18px;}\n" .
-            ".small {font-size:14px;}\n" .
-            '}';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCssCount(1, $css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function multipleMediaQueriesAreAppliedOnlyOnce()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><body>' .
-            '<p class="medium">medium</p>' .
-            '<p class="small">small</p>' .
-            '</body></html>'
-        );
-        $css = "@media all {\n" .
-            ".medium {font-size:18px;}\n" .
-            ".small {font-size:14px;}\n" .
-            '}' .
-            "@media screen {\n" .
-            ".medium {font-size:24px;}\n" .
-            ".small {font-size:18px;}\n" .
-            '}';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCssCount(1, $css, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function dataUriMediaTypeDataProvider()
-    {
-        return [
-            'nothing' => [''],
-            ';charset=utf-8' => [';charset=utf-8'],
-            ';base64' => [';base64'],
-            ';charset=utf-8;base64' => [';charset=utf-8;base64'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $dataUriMediaType
-     *
-     * @dataProvider dataUriMediaTypeDataProvider
-     */
-    public function dataUrisAreConserved($dataUriMediaType)
-    {
-        $subject = $this->buildDebugSubject('<html></html>');
-        $styleRule = 'background-image: url(data:image/png' . $dataUriMediaType .
-            ',iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAABUk' .
-            'lEQVQ4y81UsY6CQBCdWXBjYWFMjEgAE0piY8c38B9+iX+ksaHCgs5YWEhIrJCQYGJBomiC7lzhVcfqEa+5KXfey3s783bRdd00TR' .
-            'VFAQAAICJEhN/q8Xjoug7D4RA+qsFgwDjn9QYiTiaT+Xx+OByOx+NqtapjWq0WjEajekPTtCAIiIiIyrKMoqiOMQxDlVqyLMt1XQ' .
-            'A4nU6z2Wy9XkthEnK/3zdN8znC/X7v+36WZfJ7120vFos4joUQRHS5XDabzXK5bGrbtu1er/dtTFU1TWu3202VHceZTqe3242Itt' .
-            'ut53nj8bip8m6345wLIQCgKIowDIuikAoz6Wm3233mjHPe6XRe5UROJqImIWPwh/pvZMbYM2GKorx5oUw6m+v1miTJ+XzO8/x+v7' .
-            '+UtizrM8+GYahVVSFik9/jxy6rqlJN02SM1cmI+GbbQghd178AAO2FXws6LwMAAAAASUVORK5CYII=);';
-        $subject->setCss('html {' . $styleRule . '}');
-
-        $result = $subject->emogrify();
-
-        static::assertContains(
-            '<html style="' . $styleRule . '">',
-            $result
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifierIgnoresPseudoClassCombinedWithPseudoElement()
-    {
-        $subject = $this->buildDebugSubject('<html><body><div></div></body></html>');
-        $subject->setCss('div:last-child::after {float: right;}');
-
-        $html = $subject->emogrify();
-
-        static::assertContains('<div></div>', $html);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsInlineStylePriorityVersusStyleBlockRules()
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><head><style>p {padding:10px};</style></head><body><p style="padding-left:20px;"></p></body></html>'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="padding: 10px; padding-left: 20px;">', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function cssForImportantRuleRemovalDataProvider()
-    {
-        return [
-            'one !important rule only' => [
-                'width: 1px !important',
-                'width: 1px;',
-            ],
-            'multiple !important rules only' => [
-                'width: 1px !important; height: 1px !important',
-                'width: 1px; height: 1px;',
-            ],
-            'multiple declarations, one !important rule at the beginning' => [
-                'width: 1px !important; height: 1px; color: red',
-                'height: 1px; color: red; width: 1px;',
-            ],
-            'multiple declarations, one !important rule somewhere in the middle' => [
-                'height: 1px; width: 1px !important; color: red',
-                'height: 1px; color: red; width: 1px;',
-            ],
-            'multiple declarations, one !important rule at the end' => [
-                'height: 1px; color: red; width: 1px !important',
-                'height: 1px; color: red; width: 1px;',
-            ],
-            'multiple declarations, multiple !important rules at the beginning' => [
-                'width: 1px !important; height: 1px !important; color: red; float: left',
-                'color: red; float: left; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple consecutive !important rules somewhere in the middle (#1)' => [
-                'color: red; width: 1px !important; height: 1px !important; float: left',
-                'color: red; float: left; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple consecutive !important rules somewhere in the middle (#2)' => [
-                'color: red; width: 1px !important; height: 1px !important; float: left; clear: both',
-                'color: red; float: left; clear: both; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple not consecutive !important rules somewhere in the middle' => [
-                'color: red; width: 1px !important; clear: both; height: 1px !important; float: left',
-                'color: red; clear: both; float: left; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple !important rules at the end' => [
-                'color: red; float: left; width: 1px !important; height: 1px !important',
-                'color: red; float: left; width: 1px; height: 1px;',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $originalStyleAttributeContent
-     * @param string $expectedStyleAttributeContent
-     *
-     * @dataProvider cssForImportantRuleRemovalDataProvider
-     */
-    public function emogrifyRemovesImportantRule($originalStyleAttributeContent, $expectedStyleAttributeContent)
-    {
-        $subject = $this->buildDebugSubject(
-            '<html><head><body><p style="' . $originalStyleAttributeContent . '"></p></body></html>'
-        );
-
-        $result = $subject->emogrify();
-
-        static::assertContains('<p style="' . $expectedStyleAttributeContent . '">', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \Symfony\Component\CssSelector\Exception\SyntaxErrorException
-     */
-    public function emogrifyInDebugModeForInvalidSelectorsInMediaQueryBlocksThrowsException()
-    {
-        $subject = new CssInliner('<html></html>');
-        $subject->setDebug(true);
-
-        $subject->setCss('@media screen {p^^ {color: red;}}');
-
-        $subject->emogrify();
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeKeepsInvalidOrUnrecognizedSelectorsInMediaQueryBlocks()
-    {
-        $subject = new CssInliner('<html></html>');
-        $subject->setDebug(false);
-
-        $css = '@media screen {p^^ {color: red;}}';
-        $subject->setCss($css);
-
-        $result = $subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/CssConcatenatorTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/CssConcatenatorTest.php
deleted file mode 100644 (file)
index c73d72e..0000000
+++ /dev/null
@@ -1,317 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit\Emogrifier;
-
-use Pelago\Emogrifier\CssConcatenator;
-
-/**
- * Test case.
- *
- * @author Jake Hotson <jake.github@qzdesign.co.uk>
- */
-class CssConcatenatorTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * @var CssConcatenator
-     */
-    private $subject = null;
-
-    /**
-     * @return void
-     */
-    protected function setUp()
-    {
-        $this->subject = new CssConcatenator();
-    }
-
-    /**
-     * @test
-     */
-    public function getCssInitiallyReturnsEmptyString()
-    {
-        $result = $this->subject->getCss();
-
-        static::assertSame('', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function appendSetsFirstRule()
-    {
-        $this->subject->append(['p'], 'color: green;');
-
-        $result = $this->subject->getCss();
-
-        static::assertSame('p{color: green;}', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function appendWithMediaQuerySetsFirstRuleInMediaRule()
-    {
-        $this->subject->append(['p'], 'color: green;', '@media screen');
-
-        $result = $this->subject->getCss();
-
-        static::assertSame('@media screen{p{color: green;}}', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function equivalentSelectorsDataProvider()
-    {
-        return [
-            'one selector' => [['p'], ['p']],
-            'two selectors' => [
-                ['p', 'ul'],
-                ['p', 'ul'],
-            ],
-            'two selectors in different order' => [
-                ['p', 'ul'],
-                ['ul', 'p'],
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string[] $selectors1
-     * @param string[] $selectors2
-     *
-     * @dataProvider equivalentSelectorsDataProvider
-     */
-    public function appendCombinesRulesWithEquivalentSelectors(array $selectors1, array $selectors2)
-    {
-        $this->subject->append($selectors1, 'color: green;');
-        $this->subject->append($selectors2, 'font-size: 16px;');
-
-        $result = $this->subject->getCss();
-
-        $expectedResult = \implode(',', $selectors1) . '{color: green;font-size: 16px;}';
-
-        static::assertSame($expectedResult, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function appendInsertsSemicolonCombiningRulesWithoutTrailingSemicolon()
-    {
-        $this->subject->append(['p'], 'color: green');
-        $this->subject->append(['p'], 'font-size: 16px');
-
-        $result = $this->subject->getCss();
-
-        static::assertSame('p{color: green;font-size: 16px}', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function differentSelectorsDataProvider()
-    {
-        return [
-            'single selectors' => [
-                ['p'],
-                ['ul'],
-                ['p', 'ul'],
-            ],
-            'single selector and an entirely different pair' => [
-                ['p'],
-                ['ul', 'ol'],
-                ['p', 'ul', 'ol'],
-            ],
-            'single selector and a superset pair' => [
-                ['p'],
-                ['p', 'ul'],
-                ['p', 'ul'],
-            ],
-            'pair of selectors and an entirely different single' => [
-                ['p', 'ul'],
-                ['ol'],
-                ['p', 'ul', 'ol'],
-            ],
-            'pair of selectors and a subset single' => [
-                ['p', 'ul'],
-                ['ul'],
-                ['p', 'ul'],
-            ],
-            'entirely different pairs of selectors' => [
-                ['p', 'ul'],
-                ['ol', 'h1'],
-                ['p', 'ul', 'ol', 'h1'],
-            ],
-            'pairs of selectors with one common' => [
-                ['p', 'ul'],
-                ['ul', 'ol'],
-                ['p', 'ul', 'ol'],
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string[] $selectors1
-     * @param string[] $selectors2
-     * @param string[] $combinedSelectors
-     *
-     * @dataProvider differentSelectorsDataProvider
-     */
-    public function appendCombinesSameRulesWithDifferentSelectors(
-        array $selectors1,
-        array $selectors2,
-        array $combinedSelectors
-    ) {
-        $this->subject->append($selectors1, 'color: green;');
-        $this->subject->append($selectors2, 'color: green;');
-
-        $result = $this->subject->getCss();
-
-        $expectedResult = \implode(',', $combinedSelectors) . '{color: green;}';
-
-        static::assertSame($expectedResult, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string[] $selectors1
-     * @param string[] $selectors2
-     *
-     * @dataProvider differentSelectorsDataProvider
-     */
-    public function appendNotCombinesDifferentRulesWithDifferentSelectors(array $selectors1, array $selectors2)
-    {
-        $this->subject->append($selectors1, 'color: green;');
-        $this->subject->append($selectors2, 'font-size: 16px;');
-
-        $result = $this->subject->getCss();
-
-        $expectedResult = \implode(',', $selectors1) . '{color: green;}'
-            . \implode(',', $selectors2) . '{font-size: 16px;}';
-
-        static::assertSame($expectedResult, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function appendCombinesRulesForSameMediaQueryInMediaRule()
-    {
-        $this->subject->append(['p'], 'color: green;', '@media screen');
-        $this->subject->append(['ul'], 'font-size: 16px;', '@media screen');
-
-        $result = $this->subject->getCss();
-
-        static::assertSame('@media screen{p{color: green;}ul{font-size: 16px;}}', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string[] $selectors1
-     * @param string[] $selectors2
-     *
-     * @dataProvider equivalentSelectorsDataProvider
-     */
-    public function appendCombinesRulesWithEquivalentSelectorsWithinMediaRule(array $selectors1, array $selectors2)
-    {
-        $this->subject->append($selectors1, 'color: green;', '@media screen');
-        $this->subject->append($selectors2, 'font-size: 16px;', '@media screen');
-
-        $result = $this->subject->getCss();
-
-        $expectedResult = '@media screen{' . \implode(',', $selectors1) . '{color: green;font-size: 16px;}}';
-
-        static::assertSame($expectedResult, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string[] $selectors1
-     * @param string[] $selectors2
-     * @param string[] $combinedSelectors
-     *
-     * @dataProvider differentSelectorsDataProvider
-     */
-    public function appendCombinesSameRulesWithDifferentSelectorsWithinMediaRule(
-        array $selectors1,
-        array $selectors2,
-        array $combinedSelectors
-    ) {
-        $this->subject->append($selectors1, 'color: green;', '@media screen');
-        $this->subject->append($selectors2, 'color: green;', '@media screen');
-
-        $result = $this->subject->getCss();
-
-        $expectedResult = '@media screen{' . \implode(',', $combinedSelectors) . '{color: green;}}';
-
-        static::assertSame($expectedResult, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function appendNotCombinesRulesForDifferentMediaQueryInMediaRule()
-    {
-        $this->subject->append(['p'], 'color: green;', '@media screen');
-        $this->subject->append(['p'], 'color: green;', '@media print');
-
-        $result = $this->subject->getCss();
-
-        static::assertSame('@media screen{p{color: green;}}@media print{p{color: green;}}', $result);
-    }
-
-    /**
-     * @return mixed[][]
-     */
-    public function combinableRulesDataProvider()
-    {
-        return [
-            'same selectors' => [['p'], 'color: green;', ['p'], 'font-size: 16px;', ''],
-            'same declarations block' => [['p'], 'color: green;', ['ul'], 'color: green;', ''],
-            'same media query' => [['p'], 'color: green;', ['ul'], 'font-size: 16px;', '@media screen'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param array $rule1Selectors
-     * @param string $rule1DeclarationsBlock
-     * @param array $rule2Selectors
-     * @param string $rule2DeclarationsBlock
-     * @param string $media
-     *
-     * @dataProvider combinableRulesDataProvider
-     */
-    public function appendNotCombinesNonadjacentRules(
-        array $rule1Selectors,
-        $rule1DeclarationsBlock,
-        array $rule2Selectors,
-        $rule2DeclarationsBlock,
-        $media
-    ) {
-        $this->subject->append($rule1Selectors, $rule1DeclarationsBlock, $media);
-        $this->subject->append(['.intervening'], '-intervening-property: 0;');
-        $this->subject->append($rule2Selectors, $rule2DeclarationsBlock, $media);
-
-        $result = $this->subject->getCss();
-
-        $expectedRule1Css = \implode(',', $rule1Selectors) . '{' . $rule1DeclarationsBlock . '}';
-        $expectedRule2Css = \implode(',', $rule2Selectors) . '{' . $rule2DeclarationsBlock . '}';
-        if ($media !== '') {
-            $expectedRule1Css = $media . '{' . $expectedRule1Css . '}';
-            $expectedRule2Css = $media . '{' . $expectedRule2Css . '}';
-        }
-        $expectedResult = $expectedRule1Css . '.intervening{-intervening-property: 0;}' . $expectedRule2Css;
-
-        static::assertSame($expectedResult, $result);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/AbstractHtmlProcessorTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/AbstractHtmlProcessorTest.php
deleted file mode 100644 (file)
index f5e1f1a..0000000
+++ /dev/null
@@ -1,385 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor;
-
-use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
-use Pelago\Tests\Unit\Emogrifier\HtmlProcessor\Fixtures\TestingHtmlProcessor;
-
-/**
- * Test case.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-class AbstractHtmlProcessorTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * @test
-     */
-    public function fixtureIsAbstractHtmlProcessor()
-    {
-        static::assertInstanceOf(AbstractHtmlProcessor::class, new TestingHtmlProcessor('<html></html>'));
-    }
-
-    /**
-     * @test
-     */
-    public function reformatsHtml()
-    {
-        $rawHtml = '<!DOCTYPE HTML>' .
-            '<html>' .
-            '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' .
-            '<body></body>' .
-            '</html>';
-        $formattedHtml = "<!DOCTYPE HTML>\n" .
-            "<html>\n" .
-            '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . "\n" .
-            "<body></body>\n" .
-            "</html>\n";
-
-        $subject = new TestingHtmlProcessor($rawHtml);
-
-        static::assertSame($formattedHtml, $subject->render());
-    }
-
-    /**
-     * @return array[]
-     */
-    public function nonHtmlDataProvider()
-    {
-        return [
-            'empty string' => [''],
-            'null' => [null],
-            'integer' => [2],
-            'float' => [3.14159],
-            'object' => [new \stdClass()],
-        ];
-    }
-
-    /**
-     * @test
-     * @expectedException \InvalidArgumentException
-     *
-     * @param mixed $html
-     *
-     * @dataProvider nonHtmlDataProvider
-     */
-    public function constructorWithNoHtmlDataThrowsException($html)
-    {
-        new TestingHtmlProcessor($html);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function invalidHtmlDataProvider()
-    {
-        return [
-            'broken nesting gets nested' => ['<b><i></b></i>', '<b><i></i></b>'],
-            'partial opening tag gets closed' => ['<b', '<b></b>'],
-            'only opening tag gets closed' => ['<b>', '<b></b>'],
-            'only closing tag gets removed' => ['foo</b> bar', 'foo bar'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $input
-     * @param string $expectedHtml
-     *
-     * @dataProvider invalidHtmlDataProvider
-     */
-    public function renderRepairsBrokenHtml($input, $expectedHtml)
-    {
-        $subject = new TestingHtmlProcessor($input);
-        $result = $subject->render();
-
-        static::assertContains($expectedHtml, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutHtmlTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'body content only' => ['<p>Hello</p>'],
-            'HEAD element' => ['<head></head>'],
-            'BODY element' => ['<body></body>'],
-            'HEAD AND BODY element' => ['<head></head><body></body>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutHtmlTagDataProvider
-     */
-    public function addsMissingHtmlTag($html)
-    {
-        $subject = new TestingHtmlProcessor($html);
-
-        $result = $subject->render();
-
-        static::assertContains('<html>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutHeadTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'body content only' => ['<p>Hello</p>'],
-            'BODY element' => ['<body></body>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutHeadTagDataProvider
-     */
-    public function addsMissingHeadTag($html)
-    {
-        $subject = new TestingHtmlProcessor($html);
-
-        $result = $subject->render();
-
-        static::assertContains('<head>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutBodyTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'HEAD element' => ['<head></head>'],
-            'body content only' => ['<p>Hello</p>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutBodyTagDataProvider
-     */
-    public function addsMissingBodyTag($html)
-    {
-        $subject = new TestingHtmlProcessor($html);
-
-        $result = $subject->render();
-
-        static::assertContains('<body>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function putsMissingBodyElementAroundBodyContent()
-    {
-        $subject = new TestingHtmlProcessor('<p>Hello</p>');
-
-        $result = $subject->render();
-
-        static::assertContains('<body><p>Hello</p></body>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function specialCharactersDataProvider()
-    {
-        return [
-            'template markers with dollar signs & square brackets' => ['$[USER:NAME]$'],
-            'UTF-8 umlauts' => ['Küss die Hand, schöne Frau.'],
-            'HTML entities' => ['a &amp; b &gt; c'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $codeNotToBeChanged
-     *
-     * @dataProvider specialCharactersDataProvider
-     */
-    public function keepsSpecialCharacters($codeNotToBeChanged)
-    {
-        $html = '<html><p>' . $codeNotToBeChanged . '</p></html>';
-        $subject = new TestingHtmlProcessor($html);
-
-        $result = $subject->render();
-
-        static::assertContains($codeNotToBeChanged, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addsMissingHtml5DocumentType()
-    {
-        $subject = new TestingHtmlProcessor('<html></html>');
-
-        $result = $subject->render();
-
-        static::assertContains('<!DOCTYPE html>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function documentTypeDataProvider()
-    {
-        return [
-            'HTML5' => ['<!DOCTYPE html>'],
-            'XHTML 1.0 strict' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' .
-                '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
-            ],
-            'XHTML 1.0 transitional' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ' .
-                '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
-            ],
-            'HTML 4 transitional' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' .
-                '"http://www.w3.org/TR/REC-html40/loose.dtd">',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function keepsExistingDocumentType($documentType)
-    {
-        $html = $documentType . '<html></html>';
-        $subject = new TestingHtmlProcessor($html);
-
-        $result = $subject->render();
-
-        static::assertContains($documentType, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addsMissingContentTypeMetaTag()
-    {
-        $subject = new TestingHtmlProcessor('<p>Hello</p>');
-
-        $result = $subject->render();
-
-        static::assertContains('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function notAddsSecondContentTypeMetaTag()
-    {
-        $html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>';
-        $subject = new TestingHtmlProcessor($html);
-
-        $result = $subject->render();
-
-        $numberOfContentTypeMetaTags = \substr_count($result, 'Content-Type');
-        static::assertSame(1, $numberOfContentTypeMetaTags);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function convertsXmlSelfClosingTagsToNonXmlSelfClosingTag($documentType)
-    {
-        $subject = new TestingHtmlProcessor($documentType . '<html><body><br/></body></html>');
-
-        $result = $subject->render();
-
-        static::assertContains('<body><br></body>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function keepsNonXmlSelfClosingTags($documentType)
-    {
-        $subject = new TestingHtmlProcessor($documentType . '<html><body><br></body></html>');
-
-        $result = $subject->render();
-
-        static::assertContains('<body><br></body>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderBodyContentForEmptyBodyReturnsEmptyString()
-    {
-        $subject = new TestingHtmlProcessor('<html><body></body></html>');
-
-        $result = $subject->renderBodyContent();
-
-        static::assertSame('', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function renderBodyContentReturnsBodyContent()
-    {
-        $bodyContent = '<p>Hello world</p>';
-        $subject = new TestingHtmlProcessor('<html><body>' . $bodyContent . '</body></html>');
-
-        $result = $subject->renderBodyContent();
-
-        static::assertSame($bodyContent, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function getDomDocumentReturnsDomDocument()
-    {
-        $subject = new TestingHtmlProcessor('<html></html>');
-
-        static::assertInstanceOf(\DOMDocument::class, $subject->getDomDocument());
-    }
-
-    /**
-     * @test
-     */
-    public function getDomDocumentWithNormalizedHtmlRepresentsTheGivenHtml()
-    {
-        $html = "<!DOCTYPE html>\n<html>\n<head>" .
-            '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' .
-            "</head>\n<body>\n<br>\n</body>\n</html>\n";
-        $subject = new TestingHtmlProcessor($html);
-
-        $domDocument = $subject->getDomDocument();
-
-        self::assertSame($html, $domDocument->saveHTML());
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/CssToAttributeConverterTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/CssToAttributeConverterTest.php
deleted file mode 100644 (file)
index b961a29..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor;
-
-use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
-use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
-
-/**
- * Test case.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-class CssToAttributeConverterTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * @test
-     */
-    public function classIsAbstractHtmlProcessor()
-    {
-        static::assertInstanceOf(AbstractHtmlProcessor::class, new CssToAttributeConverter('<html></html>'));
-    }
-
-    /**
-     * @test
-     */
-    public function renderWithoutConvertCssToVisualAttributesCallNotAddsVisuablAttributes()
-    {
-        $html = '<html style="text-align: right;"></html>';
-        $subject = new CssToAttributeConverter($html);
-
-        static::assertContains('<html style="text-align: right;">', $subject->render());
-    }
-
-    /**
-     * @test
-     */
-    public function convertCssToVisualAttributesUsesFluentInterface()
-    {
-        $html = '<html style="text-align: right;"></html>';
-        $subject = new CssToAttributeConverter($html);
-
-        static::assertSame($subject, $subject->convertCssToVisualAttributes());
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function matchingCssToHtmlMappingDataProvider()
-    {
-        return [
-            'background-color => bgcolor' => ['<p style="background-color: red;">hi</p>', 'bgcolor="red"'],
-            'background-color with !important => bgcolor' => [
-                '<p style="background-color: red !important;">hi</p>',
-                'bgcolor="red"',
-            ],
-            'p.text-align => align' => ['<p style="text-align: left;">hi</p>', 'align="left"'],
-            'div.text-align => align' => ['<div style="text-align: left;">hi</div>', 'align="left"'],
-            'td.text-align => align' => [
-                '<table><tr><td style="text-align: left;">hi</td></tr></table>',
-                'align="left',
-            ],
-            'text-align: left => align=left' => ['<p style="text-align: left;">hi</p>', 'align="left"'],
-            'text-align: right => align=right' => ['<p style="text-align: right;">hi</p>', 'align="right"'],
-            'text-align: center => align=center' => ['<p style="text-align: center;">hi</p>', 'align="center"'],
-            'text-align: justify => align:justify' => ['<p style="text-align: justify;">hi</p>', 'align="justify"'],
-            'img.float: right => align=right' => ['<img style="float: right;">', 'align="right"'],
-            'img.float: left => align=left' => ['<img style="float: left;">', 'align="left"'],
-            'table.float: right => align=right' => ['<table style="float: right;"></table>', 'align="right"'],
-            'table.float: left => align=left' => ['<table style="float: left;"></table>', 'align="left"'],
-            'table.border-spacing: 0 => cellspacing=0' => [
-                '<table style="border-spacing: 0;"></table>',
-                'cellspacing="0"',
-            ],
-            'background => bgcolor' => ['<p style="background: red top;">Bonjour</p>', 'bgcolor="red"'],
-            'width with px' => ['<p style="width: 100px;">Hi</p>', 'width="100"'],
-            'width with %' => ['<p style="width: 50%;">Hi</p>', 'width="50%"'],
-            'height with px' => ['<p style="height: 100px;">Hi</p>', 'height="100"'],
-            'height with %' => ['<p style="height: 50%;">Hi</p>', 'height="50%"'],
-            'img.margin: 0 auto (horizontal centering) => align=center' => [
-                '<img style="margin: 0 auto;">',
-                'align="center"',
-            ],
-            'img.margin: auto (horizontal centering) => align=center' => [
-                '<img style="margin: auto;">',
-                'align="center"',
-            ],
-            'img.margin: 10 auto 30 auto (horizontal centering) => align=center' => [
-                '<img style="margin: 10 auto 30 auto;">',
-                'align="center"',
-            ],
-            'table.margin: 0 auto (horizontal centering) => align=center' => [
-                '<table style="margin: 0 auto;"></table>',
-                'align="center"',
-            ],
-            'table.margin: auto (horizontal centering) => align=center' => [
-                '<table style="margin: auto;"></table>',
-                'align="center"',
-            ],
-            'table.margin: 10 auto 30 auto (horizontal centering) => align=center' => [
-                '<table style="margin: 10 auto 30 auto;"></table>',
-                'align="center"',
-            ],
-            'img.border: none => border=0' => ['<img style="border: none;">', 'border="0"'],
-            'img.border: 0 => border=0' => ['<img style="border: none;">', 'border="0"'],
-            'table.border: none => border=0' => ['<table style="border: none;"></table>', 'border="0"'],
-            'table.border: 0 => border=0' => ['<table style="border: 0;"></table>', 'border="0"'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $body The HTML
-     * @param string $attributes The attributes that are expected on the element
-     *
-     * @dataProvider matchingCssToHtmlMappingDataProvider
-     */
-    public function convertCssToVisualAttributesMapsSuitableCssToHtml($body, $attributes)
-    {
-        $subject = new CssToAttributeConverter('<html><body>' . $body . '</body></html>');
-
-        $subject->convertCssToVisualAttributes();
-        $html = $subject->render();
-
-        static::assertContains($attributes, $html);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function notMatchingCssToHtmlMappingDataProvider()
-    {
-        return [
-            'background URL' => ['<p style="background: url(bg.png);">Hello</p>'],
-            'background URL with position' => ['<p style="background: url(bg.png) top;">Hello</p>'],
-            'p.margin: 10 5 30 auto (no horizontal centering)' => ['<img style="margin: 10 5 30 auto;">'],
-            'p.margin: auto' => ['<p style="margin: auto;">Hi</p>'],
-            'p.border: none' => ['<p style="border: none;">Hi</p>'],
-            'img.border: 1px solid black' => ['<img style="border: 1px solid black;">'],
-            'span.text-align' => ['<span style="text-align: justify;">Hi</span>'],
-            'text-align: inherit' => ['<p style="text-align: inherit;">Hi</p>'],
-            'span.float' => ['<span style="float: right;">Hi</span>'],
-            'float: none' => ['<table style="float: none;"></table>'],
-            'p.border-spacing' => ['<p style="border-spacing: 5px;">Hi</p>'],
-            'height: auto' => ['<img src="logo.png" alt="" style="height: auto;">'],
-            'width: auto' => ['<img src="logo.png" alt="" style="width: auto;">'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $body the HTML
-     *
-     * @dataProvider notMatchingCssToHtmlMappingDataProvider
-     */
-    public function convertCssToVisualAttributesNotMapsUnsuitableCssToHtml($body)
-    {
-        $subject = new CssToAttributeConverter('<html><body>' . $body . '</body></html>');
-
-        $subject->convertCssToVisualAttributes();
-        $html = $subject->render();
-
-        static::assertContains($body, $html);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/Fixtures/TestingHtmlProcessor.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/Fixtures/TestingHtmlProcessor.php
deleted file mode 100644 (file)
index 112cf54..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor\Fixtures;
-
-use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
-
-/**
- * Fixture class for AbstractHtmlProcessor.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-class TestingHtmlProcessor extends AbstractHtmlProcessor
-{
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/HtmlNormalizerTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Emogrifier/HtmlProcessor/HtmlNormalizerTest.php
deleted file mode 100644 (file)
index 802e13b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor;
-
-use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
-use Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer;
-
-/**
- * Test case.
- *
- * @author Oliver Klee <github@oliverklee.de>
- */
-class HtmlNormalizerTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * @test
-     */
-    public function classIsAbstractHtmlProcessor()
-    {
-        static::assertInstanceOf(AbstractHtmlProcessor::class, new HtmlNormalizer('<html></html>'));
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/EmogrifierTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/EmogrifierTest.php
deleted file mode 100644 (file)
index af6bc45..0000000
+++ /dev/null
@@ -1,3114 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit;
-
-use Pelago\Emogrifier;
-use Pelago\Tests\Support\Traits\AssertCss;
-
-/**
- * Test case.
- *
- * @author Oliver Klee <github@oliverklee.de>
- * @author Zoli Szabó <zoli.szabo+github@gmail.com>
- */
-class EmogrifierTest extends \PHPUnit_Framework_TestCase
-{
-    use AssertCss;
-
-    /**
-     * @var string Common HTML markup with a variety of elements and attributes for testing with
-     */
-    const COMMON_TEST_HTML = '
-        <html>
-            <body>
-                <p class="p-1"><span>some text</span></p>
-                <p class="p-2"><span title="bonjour">some</span> text</p>
-                <p class="p-3"><span title="buenas dias">some</span> more text</p>
-                <p class="p-4" id="p4"><span title="avez-vous">some</span> more <span id="text">text</span></p>
-                <p class="p-5 additional-class"><span title="buenas dias bom dia">some</span> more text</p>
-                <p class="p-6"><span title="title: subtitle; author">some</span> more text</p>
-            </body>
-        </html>
-    ';
-
-    /**
-     * @var string
-     */
-    private $html5DocumentType = '<!DOCTYPE html>';
-
-    /**
-     * @var Emogrifier
-     */
-    private $subject = null;
-
-    /**
-     * Sets up the test case.
-     *
-     * @return void
-     */
-    protected function setUp()
-    {
-        $this->subject = new Emogrifier();
-        $this->subject->setDebug(true);
-    }
-
-    /**
-     * @test
-     */
-    public function getDomDocumentReturnsDomDocument()
-    {
-        $subject = new Emogrifier('<html></html>');
-
-        static::assertInstanceOf(\DOMDocument::class, $subject->getDomDocument());
-    }
-
-    /**
-     * @test
-     */
-    public function getDomDocumentWithNormalizedHtmlRepresentsTheGivenHtml()
-    {
-        $html = "<!DOCTYPE html>\n<html>\n<head>" .
-            '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' .
-            "</head>\n<body>\n<br>\n</body>\n</html>\n";
-        $subject = new Emogrifier($html);
-
-        $domDocument = $subject->getDomDocument();
-
-        self::assertSame($html, $domDocument->saveHTML());
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \BadMethodCallException
-     */
-    public function emogrifyForNoDataSetThrowsException()
-    {
-        $this->subject->emogrify();
-    }
-
-    /**
-     * @return array[]
-     */
-    public function nonHtmlDataProvider()
-    {
-        return [
-            'empty string' => [''],
-            'null' => [null],
-            'integer' => [2],
-            'float' => [3.14159],
-            'object' => [new \stdClass()],
-        ];
-    }
-
-    /**
-     * @test
-     * @expectedException \InvalidArgumentException
-     *
-     * @param mixed $html
-     * @dataProvider nonHtmlDataProvider
-     */
-    public function setHtmlNoHtmlDataThrowsException($html)
-    {
-        $this->subject->setHtml($html);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \BadMethodCallException
-     */
-    public function emogrifyBodyContentForNoDataSetThrowsException()
-    {
-        $this->subject->emogrifyBodyContent();
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutHtmlTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'body content only' => ['<p>Hello</p>'],
-            'HEAD element' => ['<head></head>'],
-            'BODY element' => ['<body></body>'],
-            'HEAD AND BODY element' => ['<head></head><body></body>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutHtmlTagDataProvider
-     */
-    public function emogrifyAddsMissingHtmlTag($html)
-    {
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<html>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutHeadTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'body content only' => ['<p>Hello</p>'],
-            'BODY element' => ['<body></body>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutHeadTagDataProvider
-     */
-    public function emogrifyAddsMissingHeadTag($html)
-    {
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<head>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithoutBodyTagDataProvider()
-    {
-        return [
-            'doctype only' => ['<!DOCTYPE html>'],
-            'HEAD element' => ['<head></head>'],
-            'body content only' => ['<p>Hello</p>'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $html
-     *
-     * @dataProvider contentWithoutBodyTagDataProvider
-     */
-    public function emogrifyAddsMissingBodyTag($html)
-    {
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<body>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyPutsMissingBodyElementAroundBodyContent()
-    {
-        $this->subject->setHtml('<p>Hello</p>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<body><p>Hello</p></body>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function specialCharactersDataProvider()
-    {
-        return [
-            'template markers with dollar signs & square brackets' => ['$[USER:NAME]$'],
-            'UTF-8 umlauts' => ['Küss die Hand, schöne Frau.'],
-            'HTML entities' => ['a &amp; b &gt; c'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $codeNotToBeChanged
-     *
-     * @dataProvider specialCharactersDataProvider
-     */
-    public function emogrifyKeepsSpecialCharacters($codeNotToBeChanged)
-    {
-        $html = '<html><p>' . $codeNotToBeChanged . '</p></html>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains($codeNotToBeChanged, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $codeNotToBeChanged
-     *
-     * @dataProvider specialCharactersDataProvider
-     */
-    public function emogrifyBodyContentKeepsSpecialCharacters($codeNotToBeChanged)
-    {
-        $html = '<html><p>' . $codeNotToBeChanged . '</p></html>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrifyBodyContent();
-
-        static::assertContains($codeNotToBeChanged, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addsMissingHtml5DocumentType()
-    {
-        $this->subject->setHtml('<html></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<!DOCTYPE html>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function documentTypeDataProvider()
-    {
-        return [
-            'HTML5' => ['<!DOCTYPE html>'],
-            'XHTML 1 strict' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' .
-                '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
-            ],
-            'HTML 4 transitional' => [
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' .
-                '"http://www.w3.org/TR/REC-html40/loose.dtd">',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function emogrifyForHtmlWithDocumentTypeKeepsDocumentType($documentType)
-    {
-        $html = $documentType . '<html></html>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains($documentType, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAddsMissingContentTypeMetaTag()
-    {
-        $this->subject->setHtml('<p>Hello</p>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotAddsSecondContentTypeMetaTag()
-    {
-        $html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        $numberOfContentTypeMetaTags = \substr_count($result, 'Content-Type');
-        static::assertSame(1, $numberOfContentTypeMetaTags);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultRemovesWbrTag()
-    {
-        $html = '<html>foo<wbr/>bar</html>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('<wbr', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addUnprocessableTagRemovesEmptyTag()
-    {
-        $this->subject->setHtml('<html><p></p></html>');
-
-        $this->subject->addUnprocessableHtmlTag('p');
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('<p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addUnprocessableTagNotRemovesNonEmptyTag()
-    {
-        $this->subject->setHtml('<html><p>foobar</p></html>');
-
-        $this->subject->addUnprocessableHtmlTag('p');
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function removeUnprocessableHtmlTagKeepsTagAgainAgain()
-    {
-        $this->subject->setHtml('<html><p></p></html>');
-
-        $this->subject->addUnprocessableHtmlTag('p');
-        $this->subject->removeUnprocessableHtmlTag('p');
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function matchedCssDataProvider()
-    {
-        // The sprintf placeholders %1$s and %2$s will automatically be replaced with CSS declarations
-        // like 'color: red;' or 'text-align: left;'.
-        return [
-            'two declarations from one rule can apply to the same element' => [
-                'html { %1$s %2$s }',
-                '<html style="%1$s %2$s">',
-            ],
-            'two identical matchers with different rules get combined' => [
-                'p { %1$s } p { %2$s }',
-                '<p class="p-1" style="%1$s %2$s">',
-            ],
-            'two different matchers rules matching the same element get combined' => [
-                'p { %1$s } .p-1 { %2$s }',
-                '<p class="p-1" style="%1$s %2$s">',
-            ],
-            'type => one element' => ['html { %1$s }', '<html style="%1$s">'],
-            'type (case-insensitive) => one element' => ['HTML { %1$s }', '<html style="%1$s">'],
-            'type => first matching element' => ['p { %1$s }', '<p class="p-1" style="%1$s">'],
-            'type => second matching element' => ['p { %1$s }', '<p class="p-2" style="%1$s">'],
-            'class => with class' => ['.p-2 { %1$s }', '<p class="p-2" style="%1$s">'],
-            'two classes s=> with both classes' => [
-                '.p-5.additional-class { %1$s }',
-                '<p class="p-5 additional-class" style="%1$s">',
-            ],
-            'type & class => type with class' => ['p.p-2 { %1$s }', '<p class="p-2" style="%1$s">'],
-            'ID => with ID' => ['#p4 { %1$s }', '<p class="p-4" id="p4" style="%1$s">'],
-            'type & ID => type with ID' => ['p#p4 { %1$s }', '<p class="p-4" id="p4" style="%1$s">'],
-            'universal => HTML' => ['* { %1$s }', '<html style="%1$s">'],
-            'attribute presence => with attribute' => ['[title] { %1$s }', '<span title="bonjour" style="%1$s">'],
-            'attribute exact value, double quotes => with exact attribute match' => [
-                '[title="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'attribute exact value, single quotes => with exact match' => [
-                '[title=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            // broken: attribute exact value without quotes => with exact match
-            // broken: attribute exact two-word value, double quotes => with exact attribute value match
-            // broken: attribute exact two-word value, single quotes => with exact attribute value match
-            // broken: attribute exact value with ~, double quotes => exact attribute match
-            // broken: attribute exact value with ~, single quotes => exact attribute match
-            // broken: attribute exact value with ~, no quotes => exact attribute match
-            // broken: attribute value with |, double quotes => with exact match
-            // broken: attribute value with |, single quotes => with exact match
-            // broken: attribute value with |, no quotes => with exact match
-            // broken: attribute value with ^, double quotes => with exact match
-            // broken: attribute value with ^, single quotes => with exact match
-            // broken: attribute value with ^, no quotes => with exact match
-            // broken: attribute value with $, double quotes => with exact match
-            // broken: attribute value with $, single quotes => with exact match
-            // broken: attribute value with $, no quotes => with exact match
-            // broken: attribute value with *, double quotes => with exact match
-            // broken: attribute value with *, single quotes => with exact match
-            // broken: attribute value with *, no quotes => with exact match
-            // broken: type & attribute presence => with type & attribute
-            'type & attribute exact value, double quotes => with type & exact attribute value match' => [
-                'span[title="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute exact value, single quotes => with type & exact attribute value match' => [
-                'span[title=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute exact value without quotes => with type & exact attribute value match' => [
-                'span[title=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute exact two-word value, double quotes => with type & exact attribute value match' => [
-                'span[title="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute exact four-word value, double quotes => with type & exact attribute value match' => [
-                'span[title="buenas dias bom dia"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute exact two-word value, single quotes => with type & exact attribute value match' => [
-                'span[title=\'buenas dias\'] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute exact four-word value, single quotes => with type & exact attribute value match' => [
-                'span[title=\'buenas dias bom dia\'] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & exact attribute match' => [
-                'span[title~="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ~, single quotes => with type & exact attribute match' => [
-                'span[title~=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ~, no quotes => with type & exact attribute match' => [
-                'span[title~=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 1st of 2 in attribute' => [
-                'span[title~="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 2nd of 2 in attribute' => [
-                'span[title~="dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 1st of 4 in attribute' => [
-                'span[title~="buenas"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as 2nd of 4 in attribute' => [
-                'span[title~="dias"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with ~, double quotes => with type & word as last of 4 in attribute' => [
-                'span[title~="dia"] { %1$s }',
-                '<span title="buenas dias bom dia" style="%1$s">',
-            ],
-            'type & attribute value with |, double quotes => with exact match' => [
-                'span[title|="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with |, single quotes => with exact match' => [
-                'span[title|=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with |, no quotes => with exact match' => [
-                'span[title|=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & two-word attribute value with |, double quotes => with exact match' => [
-                'span[title|="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with |, double quotes => with match before hyphen & another word' => [
-                'span[title|="avez"] { %1$s }',
-                '<span title="avez-vous" style="%1$s">',
-            ],
-            'type & attribute value with ^, double quotes => with exact match' => [
-                'span[title^="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ^, single quotes => with exact match' => [
-                'span[title^=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ^, no quotes => with exact match' => [
-                'span[title^=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            // broken: type & two-word attribute value with ^, double quotes => with exact match
-            'type & attribute value with ^, double quotes => with prefix math' => [
-                'span[title^="bon"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with ^, double quotes => with match before another word' => [
-                'span[title^="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with $, double quotes => with exact match' => [
-                'span[title$="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with $, single quotes => with exact match' => [
-                'span[title$=\'bonjour\'] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with $, no quotes => with exact match' => [
-                'span[title$=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & two-word attribute value with $, double quotes => with exact match' => [
-                'span[title$="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with $, double quotes => with suffix math' => [
-                'span[title$="jour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with $, double quotes => with match after another word' => [
-                'span[title$="dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & two-word attribute value with *, double quotes => with exact match' => [
-                'span[title*="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with prefix math' => [
-                'span[title*="bon"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with suffix math' => [
-                'span[title*="jour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with substring math' => [
-                'span[title*="njo"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with match before another word' => [
-                'span[title*="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & attribute value with *, double quotes => with match after another word' => [
-                'span[title*="dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'type & special characters attribute value with *, double quotes => with substring match' => [
-                'span[title*=": subtitle; author"] { %1$s }',
-                '<span title="title: subtitle; author" style="%1$s">',
-            ],
-            'adjacent => 2nd of many' => ['p + p { %1$s }', '<p class="p-2" style="%1$s">'],
-            'adjacent => last of many' => ['p + p { %1$s }', '<p class="p-6" style="%1$s">'],
-            'adjacent (without space after +) => last of many' => ['p +p { %1$s }', '<p class="p-6" style="%1$s">'],
-            'adjacent (without space before +) => last of many' => ['p+ p { %1$s }', '<p class="p-6" style="%1$s">'],
-            'adjacent (without space before or after +) => last of many' => [
-                'p+p { %1$s }',
-                '<p class="p-6" style="%1$s">',
-            ],
-            'child (with spaces around >) => direct child' => ['p > span { %1$s }', '<span style="%1$s">'],
-            'child (without space after >) => direct child' => ['p >span { %1$s }', '<span style="%1$s">'],
-            'child (without space before >) => direct child' => ['p> span { %1$s }', '<span style="%1$s">'],
-            'child (without space before or after >) => direct child' => ['p>span { %1$s }', '<span style="%1$s">'],
-            'descendant => child' => ['p span { %1$s }', '<span style="%1$s">'],
-            'descendant => grandchild' => ['body span { %1$s }', '<span style="%1$s">'],
-            // broken: descendent attribute presence => with attribute
-            // broken: descendent attribute exact value => with exact attribute match
-            // broken: descendent type & attribute presence => with type & attribute
-            'descendent type & attribute exact value => with type & exact attribute match' => [
-                'body span[title="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'descendent type & attribute exact two-word value => with type & exact attribute match' => [
-                'body span[title="buenas dias"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'descendent type & attribute value with ~ => with type & exact attribute match' => [
-                'body span[title~="bonjour"] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'descendent type & attribute value with ~ => with type & word as 1st of 2 in attribute' => [
-                'body span[title~="buenas"] { %1$s }',
-                '<span title="buenas dias" style="%1$s">',
-            ],
-            'descendant of type & class: type & attribute exact value, no quotes => with type & exact match (#381)' => [
-                'p.p-2 span[title=bonjour] { %1$s }',
-                '<span title="bonjour" style="%1$s">',
-            ],
-            'descendant of attribute presence => parent with attribute' => [
-                '[class] span { %1$s }',
-                '<p class="p-1"><span style="%1$s">',
-            ],
-            'descendant of attribute exact value => parent with type & exact attribute match' => [
-                '[id="p4"] span { %1$s }',
-                '<p class="p-4" id="p4"><span title="avez-vous" style="%1$s">',
-            ],
-            // broken: descendant of type & attribute presence => parent with type & attribute
-            'descendant of type & attribute exact value => parent with type & exact attribute match' => [
-                'p[id="p4"] span { %1$s }',
-                '<p class="p-4" id="p4"><span title="avez-vous" style="%1$s">',
-            ],
-            // broken: descendant of type & attribute exact two-word value => parent with type & exact attribute match
-            //         (exact match doesn't currently match hyphens, which would be needed to match the class attribute)
-            'descendant of type & attribute value with ~ => parent with type & exact attribute match' => [
-                'p[class~="p-1"] span { %1$s }',
-                '<p class="p-1"><span style="%1$s">',
-            ],
-            'descendant of type & attribute value with ~ => parent with type & word as 1st of 2 in attribute' => [
-                'p[class~="p-5"] span { %1$s }',
-                '<p class="p-5 additional-class"><span title="buenas dias bom dia" style="%1$s">',
-            ],
-            // broken: first-child => 1st of many
-            'type & :first-child => 1st of many' => ['p:first-child { %1$s }', '<p class="p-1" style="%1$s">'],
-            // broken: last-child => last of many
-            'type & :last-child => last of many' => ['p:last-child { %1$s }', '<p class="p-6" style="%1$s">'],
-            // broken: :not with type => other type
-            // broken: :not with class => no class
-            // broken: :not with class => other class
-            'type & :not with class => without class' => ['span:not(.foo) { %1$s }', '<span style="%1$s">'],
-            'type & :not with class => with other class' => ['p:not(.foo) { %1$s }', '<p class="p-1" style="%1$s">'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css CSS statements, potentially with %1$s and $2$s placeholders for a CSS declaration
-     * @param string $expectedHtml HTML, potentially with %1$s and $2$s placeholders for a CSS declaration
-     *
-     * @dataProvider matchedCssDataProvider
-     */
-    public function emogrifyAppliesCssToMatchingElements($css, $expectedHtml)
-    {
-        $cssDeclaration1 = 'color: red;';
-        $cssDeclaration2 = 'text-align: left;';
-        $this->subject->setHtml(static::COMMON_TEST_HTML);
-        $this->subject->setCss(\sprintf($css, $cssDeclaration1, $cssDeclaration2));
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains(\sprintf($expectedHtml, $cssDeclaration1, $cssDeclaration2), $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function nonMatchedCssDataProvider()
-    {
-        // The sprintf placeholders %1$s and %2$s will automatically be replaced with CSS declarations
-        // like 'color: red;' or 'text-align: left;'.
-        return [
-            'type => not other type' => ['html { %1$s }', '<body>'],
-            'class => not other class' => ['.p-2 { %1$s }', '<p class="p-1">'],
-            'class => not without class' => ['.p-2 { %1$s }', '<body>'],
-            'two classes => not only first class' => ['.p-1.another-class { %1$s }', '<p class="p-1">'],
-            'two classes => not only second class' => ['.another-class.p-1 { %1$s }', '<p class="p-1">'],
-            'type & class => not only type' => ['html.p-1 { %1$s }', '<html>'],
-            'type & class => not only class' => ['html.p-1 { %1$s }', '<p class="p-1">'],
-            'ID => not other ID' => ['#yeah { %1$s }', '<p class="p-4" id="p4">'],
-            'ID => not without ID' => ['#yeah { %1$s }', '<span>'],
-            'type & ID => not other type with that ID' => ['html#p4 { %1$s }', '<p class="p-4" id="p4">'],
-            'type & ID => not that type with other ID' => ['p#p5 { %1$s }', '<p class="p-4" id="p4">'],
-            'attribute presence => not element without that attribute' => ['[title] { %1$s }', '<span>'],
-            'attribute exact value => not element without that attribute' => ['[title="bonjour"] { %1$s }', '<span>'],
-            'attribute exact value => not element with different attribute value' => [
-                '[title="hi"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'attribute exact value => not element with only substring match in attribute value' => [
-                '[title="njo"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with ~ => not element with only prefix match in attribute value' => [
-                'span[title~="bon"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with |, double quotes => not element with match after another word & hyphen' => [
-                'span[title|="vous"] { %1$s }',
-                '<span title="avez-vous">',
-            ],
-            'type & attribute value with ^ => not element with only substring match in attribute value' => [
-                'span[title^="njo"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with ^, double quotes => not element with only suffix match in attribute value' => [
-                'span[title^="jour"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with $ => not element with only substring match in attribute value' => [
-                'span[title$="njo"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with $, double quotes => not element with only prefix match in attribute value' => [
-                'span[title$="bon"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'type & attribute value with * => not element with different attribute value' => [
-                'span[title*="hi"] { %1$s }',
-                '<span title="bonjour">',
-            ],
-            'adjacent => not 1st of many' => ['p + p { %1$s }', '<p class="p-1">'],
-            'child => not grandchild' => ['html > span { %1$s }', '<span>'],
-            'child => not parent' => ['span > html { %1$s }', '<html>'],
-            'descendant => not sibling' => ['span span { %1$s }', '<span>'],
-            'descendant => not parent' => ['p body { %1$s }', '<body>'],
-            'type & :first-child => not 2nd of many' => ['p:first-child { %1$s }', '<p class="p-2">'],
-            'type & :first-child => not last of many' => ['p:first-child { %1$s }', '<p class="p-6">'],
-            'type & :last-child => not 1st of many' => ['p:last-child { %1$s }', '<p class="p-1">'],
-            'type & :last-child => not 2nd of many' => ['p:last-child { %1$s }', '<p class="p-2">'],
-            'type & :not with class => not with class' => ['p:not(.p-1) { %1$s }', '<p class="p-1">'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css CSS statements, potentially with %1$s and $2$s placeholders for a CSS declaration
-     * @param string $expectedHtml HTML, potentially with %1$s and $2$s placeholders for a CSS declaration
-     *
-     * @dataProvider nonMatchedCssDataProvider
-     */
-    public function emogrifyNotAppliesCssToNonMatchingElements($css, $expectedHtml)
-    {
-        $cssDeclaration1 = 'color: red;';
-        $cssDeclaration2 = 'text-align: left;';
-        $this->subject->setHtml(static::COMMON_TEST_HTML);
-        $this->subject->setCss(\sprintf($css, $cssDeclaration1, $cssDeclaration2));
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains(\sprintf($expectedHtml, $cssDeclaration1, $cssDeclaration2), $result);
-    }
-
-    /**
-     * Provides data to test the following selector specificity ordering:
-     *     * < t < 2t < . < .+t < .+2t < 2. < 2.+t < 2.+2t
-     *     < # < #+t < #+2t < #+. < #+.+t < #+.+2t < #+2. < #+2.+t < #+2.+2t
-     *     < 2# < 2#+t < 2#+2t < 2#+. < 2#+.+t < 2#+.+2t < 2#+2. < 2#+2.+t < 2#+2.+2t
-     * where '*' is the universal selector, 't' is a type selector, '.' is a class selector, and '#' is an ID selector.
-     *
-     * Also confirm up to 99 class selectors are supported (much beyond this would require a more complex comparator).
-     *
-     * Specificity ordering for selectors involving pseudo-classes, attributes and `:not` is covered through the
-     * combination of these tests and the equal specificity tests and thus does not require explicit separate testing.
-     *
-     * @return string[][]
-     */
-    public function differentCssSelectorSpecificityDataProvider()
-    {
-        /**
-         * @var string[] Selectors targeting `<span id="text">` with increasing specificity
-         */
-        $selectors = [
-            'universal' => '*',
-            'type' => 'span',
-            '2 types' => 'p span',
-            'class' => '.p-4 *',
-            'class & type' => '.p-4 span',
-            'class & 2 types' => 'p.p-4 span',
-            '2 classes' => '.p-4.p-4 *',
-            '2 classes & type' => '.p-4.p-4 span',
-            '2 classes & 2 types' => 'p.p-4.p-4 span',
-            'ID' => '#text',
-            'ID & type' => 'span#text',
-            'ID & 2 types' => 'p span#text',
-            'ID & class' => '.p-4 #text',
-            'ID & class & type' => '.p-4 span#text',
-            'ID & class & 2 types' => 'p.p-4 span#text',
-            'ID & 2 classes' => '.p-4.p-4 #text',
-            'ID & 2 classes & type' => '.p-4.p-4 span#text',
-            'ID & 2 classes & 2 types' => 'p.p-4.p-4 span#text',
-            '2 IDs' => '#p4 #text',
-            '2 IDs & type' => '#p4 span#text',
-            '2 IDs & 2 types' => 'p#p4 span#text',
-            '2 IDs & class' => '.p-4#p4 #text',
-            '2 IDs & class & type' => '.p-4#p4 span#text',
-            '2 IDs & class & 2 types' => 'p.p-4#p4 span#text',
-            '2 IDs & 2 classes' => '.p-4.p-4#p4 #text',
-            '2 IDs & 2 classes & type' => '.p-4.p-4#p4 span#text',
-            '2 IDs & 2 classes & 2 types' => 'p.p-4.p-4#p4 span#text',
-        ];
-
-        $datasets = [];
-        $previousSelector = '';
-        $previousDescription = '';
-        foreach ($selectors as $description => $selector) {
-            if ($previousSelector !== '') {
-                $datasets[$description . ' more specific than ' . $previousDescription] = [
-                    '<span id="text"',
-                    $previousSelector,
-                    $selector,
-                ];
-            }
-            $previousSelector = $selector;
-            $previousDescription = $description;
-        }
-
-        // broken: class more specific than 99 types (requires support for chaining `:not(h1):not(h1)...`)
-        $datasets['ID more specific than 99 classes'] = [
-            '<p class="p-4" id="p4"',
-            \str_repeat('.p-4', 99),
-            '#p4',
-        ];
-
-        return $datasets;
-    }
-
-    /**
-     * @test
-     *
-     * @param string $matchedTagPart Tag expected to be matched by both selectors, without the closing '>',
-     *                               e.g. '<p class="p-1"'
-     * @param string $lessSpecificSelector A selector expression
-     * @param string $moreSpecificSelector Some other, more specific selector expression
-     *
-     * @dataProvider differentCssSelectorSpecificityDataProvider
-     */
-    public function emogrifyAppliesMoreSpecificCssSelectorToMatchingElements(
-        $matchedTagPart,
-        $lessSpecificSelector,
-        $moreSpecificSelector
-    ) {
-        $this->subject->setHtml(static::COMMON_TEST_HTML);
-        $this->subject->setCss(
-            $lessSpecificSelector . ' { color: red; } ' .
-            $moreSpecificSelector . ' { color: green; } ' .
-            $moreSpecificSelector . ' { background-color: green; } ' .
-            $lessSpecificSelector . ' { background-color: red; }'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains($matchedTagPart . ' style="color: green; background-color: green;"', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function equalCssSelectorSpecificityDataProvider()
-    {
-        return [
-            // pseudo-class
-            'pseudo-class as specific as class' => ['<p class="p-1"', '*:first-child', '.p-1'],
-            'type & pseudo-class as specific as type & class' => ['<p class="p-1"', 'p:first-child', 'p.p-1'],
-            'class & pseudo-class as specific as two classes' => ['<p class="p-1"', '.p-1:first-child', '.p-1.p-1'],
-            'ID & pseudo-class as specific as ID & class' => [
-                '<span title="avez-vous"',
-                '#p4 *:first-child',
-                '#p4.p-4 *',
-            ],
-            '2 types & 2 classes & 2 IDs & pseudo-class as specific as 2 types & 3 classes & 2 IDs' => [
-                '<span id="text"',
-                'p.p-4.p-4#p4 span#text:last-child',
-                'p.p-4.p-4.p-4#p4 span#text',
-            ],
-            // attribute
-            'attribute as specific as class' => ['<span title="bonjour"', '[title="bonjour"]', '.p-2 *'],
-            'type & attribute as specific as type & class' => [
-                '<span title="bonjour"',
-                'span[title="bonjour"]',
-                '.p-2 span',
-            ],
-            'class & attribute as specific as two classes' => ['<p class="p-4" id="p4"', '.p-4[id="p4"]', '.p-4.p-4'],
-            'ID & attribute as specific as ID & class' => ['<p class="p-4" id="p4"', '#p4[id="p4"]', '#p4.p-4'],
-            '2 types & 2 classes & 2 IDs & attribute as specific as 2 types & 3 classes & 2 IDs' => [
-                '<span id="text"',
-                'p.p-4.p-4#p4[id="p4"] span#text',
-                'p.p-4.p-4.p-4#p4 span#text',
-            ],
-            // :not
-            // ideally these tests would be more minimal with just combinators and universal selectors in the :not
-            // argument, however Symfony CssSelector only supports simple (single-element) selectors here
-            ':not with type as specific as type and universal' => ['<p class="p-1"', '*:not(html)', 'html *'],
-            'type & :not with type as specific as 2 types' => ['<p class="p-1"', 'p:not(html)', 'html p'],
-            'class & :not with type as specific as type & class' => ['<p class="p-1"', '.p-1:not(html)', 'html .p-1'],
-            'ID & :not with type as specific as type & ID' => ['<p class="p-4" id="p4"', '#p4:not(html)', 'html #p4'],
-            '2 types & 2 classes & 2 IDs & :not with type as specific as 3 types & 2 classes & 2 IDs' => [
-                '<span id="text"',
-                'p.p-4.p-4#p4 span#text:not(html)',
-                'html p.p-4.p-4#p4 span#text',
-            ],
-            // argument of :not
-            ':not with type as specific as type' => ['<p class="p-1"', '*:not(h1)', 'p'],
-            ':not with class as specific as class' => ['<p class="p-1"', '*:not(.p-2)', '.p-1'],
-            ':not with ID as specific as ID' => ['<p class="p-4" id="p4"', '*:not(#p1)', '#p4'],
-            // broken: :not with 2 types & 2 classes & 2 IDs as specific as 2 types & 2 classes & 2 IDs
-            //         (`*:not(.p-1 #p1)`, i.e. with both class and ID, causes "Invalid type in selector")
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $matchedTagPart Tag expected to be matched by both selectors, without the closing '>',
-     *                               e.g. '<p class="p-1"'
-     * @param string $selector1 A selector expression
-     * @param string $selector2 Some other, equally specific selector expression
-     *
-     * @dataProvider equalCssSelectorSpecificityDataProvider
-     */
-    public function emogrifyAppliesLaterEquallySpecificCssSelectorToMatchingElements(
-        $matchedTagPart,
-        $selector1,
-        $selector2
-    ) {
-        $this->subject->setHtml(static::COMMON_TEST_HTML);
-        $this->subject->setCss(
-            $selector1 . ' { color: red; } ' .
-            $selector2 . ' { color: green; } ' .
-            $selector2 . ' { background-color: red; } ' .
-            $selector1 . ' { background-color: green; }'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains($matchedTagPart . ' style="color: green; background-color: green;"', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function cssDeclarationWhitespaceDroppingDataProvider()
-    {
-        return [
-            'no whitespace, trailing semicolon' => ['color:#000;'],
-            'no whitespace, no trailing semicolon' => ['color:#000'],
-            'space after colon, no trailing semicolon' => ['color: #000'],
-            'space before colon, no trailing semicolon' => ['color :#000'],
-            'space before property name, no trailing semicolon' => [' color:#000'],
-            'space before trailing semicolon' => [' color:#000 ;'],
-            'space after trailing semicolon' => [' color:#000; '],
-            'space after property value, no trailing semicolon' => [' color:#000 '],
-            'space after property value, trailing semicolon' => [' color:#000; '],
-            'newline before property name, trailing semicolon' => ["\ncolor:#000;"],
-            'newline after property semicolon' => ["color:#000;\n"],
-            'newline before colon, trailing semicolon' => ["color\n:#000;"],
-            'newline after colon, trailing semicolon' => ["color:\n#000;"],
-            'newline after semicolon' => ["color:#000\n;"],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $cssDeclaration the CSS declaration block (without the curly braces)
-     *
-     * @dataProvider cssDeclarationWhitespaceDroppingDataProvider
-     */
-    public function emogrifyTrimsWhitespaceFromCssDeclarations($cssDeclaration)
-    {
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss('html {' . $cssDeclaration . '}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<html style="color: #000;">', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function formattedCssDeclarationDataProvider()
-    {
-        return [
-            'one declaration' => ['color: #000;', 'color: #000;'],
-            'one declaration with dash in property name' => ['font-weight: bold;', 'font-weight: bold;'],
-            'one declaration with space in property value' => ['margin: 0 4px;', 'margin: 0 4px;'],
-            'two declarations separated by semicolon' => ['color: #000;width: 3px;', 'color: #000; width: 3px;'],
-            'two declarations separated by semicolon & space'
-            => ['color: #000; width: 3px;', 'color: #000; width: 3px;'],
-            'two declarations separated by semicolon & linefeed' => [
-                "color: #000;\nwidth: 3px;",
-                'color: #000; width: 3px;',
-            ],
-            'two declarations separated by semicolon & Windows line ending' => [
-                "color: #000;\r\nwidth: 3px;",
-                'color: #000; width: 3px;',
-            ],
-            'one declaration with leading dash in property name' => [
-                '-webkit-text-size-adjust:none;',
-                '-webkit-text-size-adjust: none;',
-            ],
-            'one declaration with linefeed in property value' => [
-                "text-shadow:\n1px 1px 3px #000,\n1px 1px 1px #000;",
-                "text-shadow: 1px 1px 3px #000,\n1px 1px 1px #000;",
-            ],
-            'one declaration with Windows line ending in property value' => [
-                "text-shadow:\r\n1px 1px 3px #000,\r\n1px 1px 1px #000;",
-                "text-shadow: 1px 1px 3px #000,\r\n1px 1px 1px #000;",
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
-     * @param string $expectedStyleAttributeContent the expected value of the style attribute
-     *
-     * @dataProvider formattedCssDeclarationDataProvider
-     */
-    public function emogrifyFormatsCssDeclarations($cssDeclarationBlock, $expectedStyleAttributeContent)
-    {
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss('html {' . $cssDeclarationBlock . '}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<html style="' . $expectedStyleAttributeContent . '">', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function invalidDeclarationDataProvider()
-    {
-        return [
-            'missing dash in property name' => ['font weight: bold;'],
-            'invalid character in property name' => ['-9webkit-text-size-adjust:none;'],
-            'missing :' => ['-webkit-text-size-adjust none'],
-            'missing value' => ['-webkit-text-size-adjust :'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
-     *
-     * @dataProvider invalidDeclarationDataProvider
-     */
-    public function emogrifyDropsInvalidCssDeclaration($cssDeclarationBlock)
-    {
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss('html {' . $cssDeclarationBlock . '}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<html style="">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleAttributes()
-    {
-        $styleAttribute = 'style="color: #ccc;"';
-        $this->subject->setHtml('<html ' . $styleAttribute . '></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains($styleAttribute, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAddsNewCssBeforeExistingStyle()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $this->subject->setHtml('<html style="' . $styleAttributeValue . '"></html>');
-        $cssDeclarations = 'margin: 0 2px;';
-        $css = 'html {' . $cssDeclarations . '}';
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('style="' . $cssDeclarations . ' ' . $styleAttributeValue . '"', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyCanMatchMinifiedCss()
-    {
-        $this->subject->setHtml('<html><p></p></html>');
-        $this->subject->setCss('p{color:blue;}html{color:red;}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<html style="color: red;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
-    {
-        $this->subject->setHtml('<html style="COLOR:#ccc;"></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('style="color: #ccc;"', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyLowercasesAttributeNamesFromPassedInCss()
-    {
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss('html {mArGiN:0 2pX;}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('style="margin: 0 2pX;"', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
-    {
-        $cssDeclaration = "content: 'Hello World';";
-        $this->subject->setHtml('<html><body><p>target</p></body></html>');
-        $this->subject->setCss('p {' . $cssDeclaration . '}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="' . $cssDeclaration . '">target</p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
-    {
-        $cssDeclaration = "content: 'Hello World';";
-        $this->subject->setHtml(
-            '<html><head><style>p {' . $cssDeclaration . '}</style></head><body><p>target</p></body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="' . $cssDeclaration . '">target</p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyRemovesStyleNodes()
-    {
-        $this->subject->setHtml('<html><style type="text/css"></style></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('<style', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \InvalidArgumentException
-     */
-    public function emogrifyInDebugModeForInvalidCssSelectorThrowsException()
-    {
-        $this->subject->setDebug(true);
-
-        $this->subject->setHtml(
-            '<html><style type="text/css">p{color:red;} <style data-x="1">html{cursor:text;}</style></html>'
-        );
-
-        $this->subject->emogrify();
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeIgnoresInvalidCssSelectors()
-    {
-        $this->subject->setDebug(false);
-
-        $html = '<html><style type="text/css">' .
-            'p{color:red;} <style data-x="1">html{cursor:text;} p{background-color:blue;}</style> ' .
-            '<body><p></p></body></html>';
-        $this->subject->setHtml($html);
-
-        $html = $this->subject->emogrify();
-
-        static::assertContains('color: red', $html);
-        static::assertContains('background-color: blue', $html);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultIgnoresInvalidCssSelectors()
-    {
-        $subject = new Emogrifier();
-
-        $html = '<html><style type="text/css">' .
-            'p{color:red;} <style data-x="1">html{cursor:text;} p{background-color:blue;}</style> ' .
-            '<body><p></p></body></html>';
-        $subject->setHtml($html);
-
-        $html = $subject->emogrify();
-        static::assertContains('color: red', $html);
-        static::assertContains('background-color: blue', $html);
-    }
-
-    /**
-     * Data provider for things that should be left out when applying the CSS.
-     *
-     * @return string[][]
-     */
-    public function unneededCssThingsDataProvider()
-    {
-        return [
-            'CSS comments with one asterisk' => ['p {color: #000;/* black */}', 'black'],
-            'CSS comments with two asterisks' => ['p {color: #000;/** black */}', 'black'],
-            '@import directive' => ['@import "foo.css";', '@import'],
-            'two @import directives, minified' => ['@import "foo.css";@import "bar.css";', '@import'],
-            '@charset directive' => ['@charset "UTF-8";', '@charset'],
-            'style in "aural" media type rule' => ['@media aural {p {color: #000;}}', '#000'],
-            'style in "braille" media type rule' => ['@media braille {p {color: #000;}}', '#000'],
-            'style in "embossed" media type rule' => ['@media embossed {p {color: #000;}}', '#000'],
-            'style in "handheld" media type rule' => ['@media handheld {p {color: #000;}}', '#000'],
-            'style in "projection" media type rule' => ['@media projection {p {color: #000;}}', '#000'],
-            'style in "speech" media type rule' => ['@media speech {p {color: #000;}}', '#000'],
-            'style in "tty" media type rule' => ['@media tty {p {color: #000;}}', '#000'],
-            'style in "tv" media type rule' => ['@media tv {p {color: #000;}}', '#000'],
-            'style in "tv" media type rule with extra spaces' => [
-                '  @media  tv  {  p  {  color  :  #000  ;  }  }  ',
-                '#000',
-            ],
-            'style in "tv" media type rule with linefeeds' => [
-                "\n@media\ntv\n{\np\n{\ncolor\n:\n#000\n;\n}\n}\n",
-                '#000',
-            ],
-            'style in "tv" media type rule with Windows line endings' => [
-                "\r\n@media\r\ntv\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000\r\n;\r\n}\r\n}\r\n",
-                '#000',
-            ],
-            'style in "only tv" media type rule' => ['@media only tv {p {color: #000;}}', '#000'],
-            'style in "only tv" media type rule with extra spaces' => [
-                '  @media  only  tv  {  p  {  color  :  #000  ;  }  }  ',
-                '#000',
-            ],
-            'style in "only tv" media type rule with linefeeds' => [
-                "\n@media\nonly\ntv\n{\np\n{\ncolor\n:\n#000\n;\n}\n}\n",
-                '#000',
-            ],
-            'style in "only tv" media type rule with Windows line endings' => [
-                "\r\n@media\r\nonly\r\ntv\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000\r\n;\r\n}\r\n}\r\n",
-                '#000',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $unneededCss
-     * @param string $markerNotExpectedInHtml
-     *
-     * @dataProvider unneededCssThingsDataProvider
-     */
-    public function emogrifyFiltersUnneededCssThings($unneededCss, $markerNotExpectedInHtml)
-    {
-        $this->subject->setHtml('<html><p>foo</p></html>');
-        $this->subject->setCss($unneededCss);
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains($markerNotExpectedInHtml, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $unneededCss
-     *
-     * @dataProvider unneededCssThingsDataProvider
-     */
-    public function emogrifyMatchesRuleAfterUnneededCssThing($unneededCss)
-    {
-        $this->subject->setHtml('<html><body></body></html>');
-        $this->subject->setCss($unneededCss . ' body { color: green; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<body style="color: green;">', $result);
-    }
-
-    /**
-     * Data provider for media rules.
-     *
-     * @return string[][]
-     */
-    public function mediaRulesDataProvider()
-    {
-        return [
-            'style in "only all" media type rule' => ['@media only all {p {color: #000;}}'],
-            'style in "only screen" media type rule' => ['@media only screen {p {color: #000;}}'],
-            'style in "only screen" media type rule with extra spaces'
-            => ['  @media  only  screen  {  p  {  color  :  #000;  }  }  '],
-            'style in "only screen" media type rule with linefeeds'
-            => ["\n@media\nonly\nscreen\n{\np\n{\ncolor\n:\n#000;\n}\n}\n"],
-            'style in "only screen" media type rule with Windows line endings'
-            => ["\r\n@media\r\nonly\r\nscreen\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000;\r\n}\r\n}\r\n"],
-            'style in media type rule' => ['@media {p {color: #000;}}'],
-            'style in media type rule with extra spaces' => ['  @media  {  p  {  color  :  #000;  }  }  '],
-            'style in media type rule with linefeeds' => ["\n@media\n{\np\n{\ncolor\n:\n#000;\n}\n}\n"],
-            'style in media type rule with Windows line endings'
-            => ["\r\n@media\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000;\r\n}\r\n}\r\n"],
-            'style in "screen" media type rule' => ['@media screen {p {color: #000;}}'],
-            'style in "screen" media type rule with extra spaces'
-            => ['  @media  screen  {  p  {  color  :  #000;  }  }  '],
-            'style in "screen" media type rule with linefeeds'
-            => ["\n@media\nscreen\n{\np\n{\ncolor\n:\n#000;\n}\n}\n"],
-            'style in "screen" media type rule with Windows line endings'
-            => ["\r\n@media\r\nscreen\r\n{\r\np\r\n{\r\ncolor\r\n:\r\n#000;\r\n}\r\n}\r\n"],
-            'style in "print" media type rule' => ['@media print {p {color: #000;}}'],
-            'style in "all" media type rule' => ['@media all {p {color: #000;}}'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider mediaRulesDataProvider
-     */
-    public function emogrifyKeepsMediaRules($css)
-    {
-        $this->subject->setHtml('<html><p>foo</p></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function orderedRulesAndSurroundingCssDataProvider()
-    {
-        $possibleSurroundingCss = [
-            'nothing' => '',
-            'space' => ' ',
-            'linefeed' => "\n",
-            'Windows line ending' => "\r\n",
-            'comment' => '/* hello */',
-            'other non-matching CSS' => 'h6 { color: #f00; }',
-            'other matching CSS' => 'p { color: #f00; }',
-            'disallowed media rule' => '@media tv { p { color: #f00; } }',
-            'allowed but non-matching media rule' => '@media screen { h6 { color: #f00; } }',
-            'non-matching CSS with pseudo-component' => 'h6:hover { color: #f00; }',
-        ];
-        $possibleCssBefore = $possibleSurroundingCss + [
-                '@import' => '@import "foo.css";',
-                '@charset' => '@charset "UTF-8";',
-            ];
-
-        $datasetsSurroundingCss = [];
-        foreach ($possibleCssBefore as $descriptionBefore => $cssBefore) {
-            foreach ($possibleSurroundingCss as $descriptionBetween => $cssBetween) {
-                foreach ($possibleSurroundingCss as $descriptionAfter => $cssAfter) {
-                    // every combination would be a ridiculous c.1000 datasets - choose a select few
-                    // test all possible CSS before once
-                    if (($cssBetween === '' && $cssAfter === '')
-                        // test all possible CSS between once
-                        || ($cssBefore === '' && $cssAfter === '')
-                        // test all possible CSS after once
-                        || ($cssBefore === '' && $cssBetween === '')
-                        // test with each possible CSS in all three positions
-                        || ($cssBefore === $cssBetween && $cssBetween === $cssAfter)
-                    ) {
-                        $description = ' with ' . $descriptionBefore . ' before, '
-                            . $descriptionBetween . ' between, '
-                            . $descriptionAfter . ' after';
-                        $datasetsSurroundingCss[$description] = [$cssBefore, $cssBetween, $cssAfter];
-                    }
-                }
-            }
-        }
-
-        $datasets = [];
-        foreach ($datasetsSurroundingCss as $description => $datasetSurroundingCss) {
-            $datasets += [
-                'two media rules' . $description => \array_merge(
-                    ['@media all { p { color: #333; } }', '@media print { p { color: #000; } }'],
-                    $datasetSurroundingCss
-                ),
-                'two rules involving pseudo-components' . $description => \array_merge(
-                    ['a:hover { color: blue; }', 'a:active { color: green; }'],
-                    $datasetSurroundingCss
-                ),
-                'media rule followed by rule involving pseudo-components' . $description => \array_merge(
-                    ['@media screen { p { color: #000; } }', 'a:hover { color: green; }'],
-                    $datasetSurroundingCss
-                ),
-                'rule involving pseudo-components followed by media rule' . $description => \array_merge(
-                    ['a:hover { color: green; }', '@media screen { p { color: #000; } }'],
-                    $datasetSurroundingCss
-                ),
-            ];
-        }
-        return $datasets;
-    }
-
-    /**
-     * @test
-     *
-     * @param string $rule1
-     * @param string $rule2
-     * @param string $cssBefore CSS to insert before the first rule
-     * @param string $cssBetween CSS to insert between the rules
-     * @param string $cssAfter CSS to insert after the second rule
-     *
-     * @dataProvider orderedRulesAndSurroundingCssDataProvider
-     */
-    public function emogrifyKeepsRulesCopiedToStyleElementInSpecifiedOrder(
-        $rule1,
-        $rule2,
-        $cssBefore,
-        $cssBetween,
-        $cssAfter
-    ) {
-        $this->subject->setHtml('<html><p><a>foo</a></p></html>');
-        $this->subject->setCss($cssBefore . $rule1 . $cssBetween . $rule2 . $cssAfter);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss($rule1 . $rule2, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function removeAllowedMediaTypeRemovesStylesForTheGivenMediaType()
-    {
-        $css = '@media screen { html { some-property: value; } }';
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss($css);
-        $this->subject->removeAllowedMediaType('screen');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addAllowedMediaTypeKeepsStylesForTheGivenMediaType()
-    {
-        $css = '@media braille { html { some-property: value; } }';
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss($css);
-        $this->subject->addAllowedMediaType('braille');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingHeadElementContent()
-    {
-        $this->subject->setHtml('<html><head><!-- original content --></head></html>');
-        $this->subject->setCss('@media all { html { some-property: value; } }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<!-- original content -->', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleElementWithMedia()
-    {
-        $html = $this->html5DocumentType . '<html><head><!-- original content --></head><body></body></html>';
-        $this->subject->setHtml($html);
-        $this->subject->setCss('@media all { html { some-property: value; } }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<style type="text/css">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleElementWithMediaInHead()
-    {
-        $style = '<style type="text/css">@media all { html {  color: red; } }</style>';
-        $html = '<html><head>' . $style . '</head><body></body></html>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertRegExp('/<head>.*<style.*<\\/head>/s', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsExistingStyleElementWithMediaOutOfBody()
-    {
-        $style = '<style type="text/css">@media all { html {  color: red; } }</style>';
-        $html = '<html><head>' . $style . '</head><body></body></html>';
-        $this->subject->setHtml($html);
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotRegExp('/<body>.*<style/s', $result);
-    }
-
-    /**
-     * Valid media query which need to be preserved
-     *
-     * @return string[][]
-     */
-    public function validMediaPreserveDataProvider()
-    {
-        return [
-            'style in "only screen and size" media type rule' => [
-                '@media only screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "screen size" media type rule' => [
-                '@media screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "only screen and screen size" media type rule' => [
-                '@media only screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "all and screen size" media type rule' => [
-                '@media all and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "only all and" media type rule' => [
-                '@media only all and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }',
-            ],
-            'style in "all" media type rule' => ['@media all {p {color: #000;}}'],
-            'style in "only screen" media type rule' => ['@media only screen { h1 { color:red; } }'],
-            'style in "only all" media type rule' => ['@media only all { h1 { color:red; } }'],
-            'style in "screen" media type rule' => ['@media screen { h1 { color:red; } }'],
-            'style in "print" media type rule' => ['@media print { * { color:#000 !important; } }'],
-            'style in media type rule without specification' => ['@media { h1 { color:red; } }'],
-            'style with multiple media type rules' => [
-                '@media all { p { color: #000; } }' .
-                '@media only screen { h1 { color:red; } }' .
-                '@media only all { h1 { color:red; } }' .
-                '@media print { * { color:#000 !important; } }' .
-                '@media { h1 { color:red; } }',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyWithValidMediaQueryContainsInnerCss($css)
-    {
-        $this->subject->setHtml('<html><h1></h1><p></p></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss('<style type="text/css">' . $css . '</style>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyWithValidMinifiedMediaQueryContainsInnerCss($css)
-    {
-        // Minify CSS by removing unnecessary whitespace.
-        $css = \preg_replace('/\\s*{\\s*/', '{', $css);
-        $css = \preg_replace('/;?\\s*}\\s*/', '}', $css);
-        $css = \preg_replace('/@media{/', '@media {', $css);
-
-        $this->subject->setHtml('<html><h1></h1><p></p></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<style type="text/css">' . $css . '</style>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyForHtmlWithValidMediaQueryContainsInnerCss($css)
-    {
-        $this->subject->setHtml('<html><style type="text/css">' . $css . '</style><h1></h1><p></p></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss('<style type="text/css">' . $css . '</style>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider validMediaPreserveDataProvider
-     */
-    public function emogrifyWithValidMediaQueryNotContainsInlineCss($css)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * Invalid media query which need to be strip
-     *
-     * @return string[][]
-     */
-    public function invalidMediaPreserveDataProvider()
-    {
-        return [
-            'style in "braille" type rule' => ['@media braille { h1 { color:red; } }'],
-            'style in "embossed" type rule' => ['@media embossed { h1 { color:red; } }'],
-            'style in "handheld" type rule' => ['@media handheld { h1 { color:red; } }'],
-            'style in "projection" type rule' => ['@media projection { h1 { color:red; } }'],
-            'style in "speech" type rule' => ['@media speech { h1 { color:red; } }'],
-            'style in "tty" type rule' => ['@media tty { h1 { color:red; } }'],
-            'style in "tv" type rule' => ['@media tv { h1 { color:red; } }'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyWithInvalidMediaQueryNotContainsInnerCss($css)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyWithInvalidMediaQueryNotContainsInlineCss($css)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyFromHtmlWithInvalidMediaQueryNotContainsInnerCss($css)
-    {
-        $this->subject->setHtml('<html><style type="text/css">' . $css . '</style><h1></h1></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider invalidMediaPreserveDataProvider
-     */
-    public function emogrifyFromHtmlWithInvalidMediaQueryNotContainsInlineCss($css)
-    {
-        $this->subject->setHtml('<html><style type="text/css">' . $css . '</style><h1></h1></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyIgnoresEmptyMediaQuery()
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss('@media screen {} @media tv { h1 { color: red; } }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-        static::assertNotContains('@media screen', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyIgnoresMediaQueryWithWhitespaceOnly()
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss('@media screen { } @media tv { h1 { color: red; } }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-        static::assertNotContains('@media screen', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function mediaTypeDataProvider()
-    {
-        return [
-            'disallowed type' => ['tv'],
-            'allowed type' => ['screen'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     *
-     * @dataProvider mediaTypeDataProvider
-     */
-    public function emogrifyKeepsMediaRuleAfterEmptyMediaRule($emptyRuleMediaType)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss('@media ' . $emptyRuleMediaType . ' {} @media all { h1 { color: red; } }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss('@media all { h1 { color: red; } }', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     *
-     * @dataProvider mediaTypeDataProvider
-     */
-    public function emogrifyNotKeepsUnneededMediaRuleAfterEmptyMediaRule($emptyRuleMediaType)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss('@media ' . $emptyRuleMediaType . ' {} @media speech { h1 { color: red; } }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @param string[] $precedingSelectorComponents Array of selectors to which each type of pseudo-component is
-     *                                              appended to create a selector for a CSS rule.
-     *                                              Keys are human-readable descriptions.
-     *
-     * @return string[][]
-     */
-    private function getCssRuleDatasetsWithSelectorPseudoComponents(array $precedingSelectorComponents)
-    {
-        $rulesComponents = [
-            'pseudo-element' => [
-                'selectorPseudoComponent' => '::after',
-                'declarationsBlock' => 'content: "bar";',
-            ],
-            'CSS2 pseudo-element' => [
-                'selectorPseudoComponent' => ':after',
-                'declarationsBlock' => 'content: "bar";',
-            ],
-            'hyphenated pseudo-element' => [
-                'selectorPseudoComponent' => '::first-letter',
-                'declarationsBlock' => 'color: green;',
-            ],
-            'pseudo-class' => [
-                'selectorPseudoComponent' => ':hover',
-                'declarationsBlock' => 'color: green;',
-            ],
-            'hyphenated pseudo-class' => [
-                'selectorPseudoComponent' => ':read-only',
-                'declarationsBlock' => 'color: green;',
-            ],
-            'pseudo-class with parameter' => [
-                'selectorPseudoComponent' => ':lang(en)',
-                'declarationsBlock' => 'color: green;',
-            ],
-        ];
-
-        $datasets = [];
-        foreach ($precedingSelectorComponents as $precedingComponentDescription => $precedingSelectorComponent) {
-            foreach ($rulesComponents as $pseudoComponentDescription => $ruleComponents) {
-                $datasets[$precedingComponentDescription . ' ' . $pseudoComponentDescription] = [
-                    $precedingSelectorComponent . $ruleComponents['selectorPseudoComponent']
-                    . ' { ' . $ruleComponents['declarationsBlock'] . ' }',
-                ];
-            }
-        }
-        return $datasets;
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function matchingSelectorWithPseudoComponentCssRuleDataProvider()
-    {
-        $datasetsWithSelectorPseudoComponents = $this->getCssRuleDatasetsWithSelectorPseudoComponents(
-            [
-                'lone' => '',
-                'type &' => 'a',
-                'class &' => '.a',
-                'ID &' => '#a',
-                'attribute &' => 'a[href="a"]',
-                'static pseudo-class &' => 'a:first-child',
-                'ancestor &' => 'p ',
-                'ancestor & type &' => 'p a',
-            ]
-        );
-        $datasetsWithCombinedPseudoSelectors = [
-            'pseudo-class & descendant' => ['p:hover a { color: green; }'],
-            'pseudo-class & pseudo-element' => ['a:hover::after { content: "bar"; }'],
-            'pseudo-element & pseudo-class' => ['a::after:hover { content: "bar"; }'],
-            'two pseudo-classes' => ['a:focus:hover { color: green; }'],
-        ];
-
-        return \array_merge($datasetsWithSelectorPseudoComponents, $datasetsWithCombinedPseudoSelectors);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider matchingSelectorWithPseudoComponentCssRuleDataProvider
-     */
-    public function emogrifyKeepsRuleWithPseudoComponentInMatchingSelector($css)
-    {
-        $this->subject->setHtml('<html><p><a id="a" class="a" href="a">foo</a></p></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        self::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function nonMatchingSelectorWithPseudoComponentCssRuleDataProvider()
-    {
-        $datasetsWithSelectorPseudoComponents = $this->getCssRuleDatasetsWithSelectorPseudoComponents(
-            [
-                'type &' => 'b',
-                'class &' => '.b',
-                'ID &' => '#b',
-                'attribute &' => 'a[href="b"]',
-                'static pseudo-class &' => 'a:not(.a)',
-                'ancestor &' => 'ul ',
-                'ancestor & type &' => 'p b',
-            ]
-        );
-        $datasetsWithCombinedPseudoSelectors = [
-            'pseudo-class & descendant' => ['ul:hover a { color: green; }'],
-            'pseudo-class & pseudo-element' => ['b:hover::after { content: "bar"; }'],
-            'pseudo-element & pseudo-class' => ['b::after:hover { content: "bar"; }'],
-            'two pseudo-classes' => ['input:focus:hover { color: green; }'],
-        ];
-
-        return \array_merge($datasetsWithSelectorPseudoComponents, $datasetsWithCombinedPseudoSelectors);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $css
-     *
-     * @dataProvider nonMatchingSelectorWithPseudoComponentCssRuleDataProvider
-     */
-    public function emogrifyNotKeepsRuleWithPseudoComponentInNonMatchingSelector($css)
-    {
-        $this->subject->setHtml('<html><p><a id="a" class="a" href="#">foo</a></p></html>');
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        self::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsRuleInMediaQueryWithPseudoComponentInMatchingSelector()
-    {
-        $this->subject->setHtml('<html><a>foo</a></html>');
-        $css = '@media screen { a:hover { color: green; } }';
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        self::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotKeepsRuleInMediaQueryWithPseudoComponentInNonMatchingSelector()
-    {
-        $this->subject->setHtml('<html><a>foo</a></html>');
-        $css = '@media screen { b:hover { color: green; } }';
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        self::assertNotContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsRuleWithPseudoComponentInMultipleMatchingSelectorsFromSingleRule()
-    {
-        $this->subject->setHtml('<html><p>foo</p><a>bar</a></html>');
-        $css = 'p:hover, a:hover { color: green; }';
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsOnlyMatchingSelectorsWithPseudoComponentFromSingleRule()
-    {
-        $this->subject->setHtml('<html><a>foo</a></html>');
-        $this->subject->setCss('p:hover, a:hover { color: green; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss('<style type="text/css">a:hover { color: green; }</style>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesCssToMatchingElementsAndKeepsRuleWithPseudoComponentFromSingleRule()
-    {
-        $this->subject->setHtml('<html><p>foo</p><a>bar</a></html>');
-        $this->subject->setCss('p, a:hover { color: green; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="color: green;">', $result);
-        static::assertContainsCss('<style type="text/css">a:hover { color: green; }</style>', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function mediaTypesDataProvider()
-    {
-        return [
-            'disallowed type after disallowed type' => ['tv', 'speech'],
-            'allowed type after disallowed type' => ['tv', 'all'],
-            'disallowed type after allowed type' => ['screen', 'tv'],
-            'allowed type after allowed type' => ['screen', 'all'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     * @param string $mediaType
-     *
-     * @dataProvider mediaTypesDataProvider
-     */
-    public function emogrifyAppliesCssBetweenEmptyMediaRuleAndMediaRule($emptyRuleMediaType, $mediaType)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss(
-            '@media ' . $emptyRuleMediaType . ' {} h1 { color: green; } @media ' . $mediaType
-            . ' { h1 { color: red; } }'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<h1 style="color: green;">', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $emptyRuleMediaType
-     * @param string $mediaType
-     *
-     * @dataProvider mediaTypesDataProvider
-     */
-    public function emogrifyAppliesCssBetweenEmptyMediaRuleAndMediaRuleWithCssAfter($emptyRuleMediaType, $mediaType)
-    {
-        $this->subject->setHtml('<html><h1></h1></html>');
-        $this->subject->setCss(
-            '@media ' . $emptyRuleMediaType . ' {} h1 { color: green; } @media ' . $mediaType
-            . ' { h1 { color: red; } } h1 { font-size: 24px; }'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<h1 style="color: green; font-size: 24px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesCssFromStyleNodes()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $this->subject->setHtml('<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<html style="' . $styleAttributeValue . '">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $this->subject->setHtml('<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>');
-        $this->subject->disableStyleBlocksParsing();
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('style=', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles()
-    {
-        $styleAttributeValue = 'text-align: center;';
-        $this->subject->setHtml(
-            '<html><head><style type="text/css">p { color: #ccc; }</style></head>' .
-            '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>'
-        );
-        $this->subject->disableStyleBlocksParsing();
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="' . $styleAttributeValue . '">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
-    {
-        $this->subject->setHtml('<html style="color: #ccc;"></html>');
-        $this->subject->disableInlineStyleAttributesParsing();
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('<html style', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
-    {
-        $styleAttributeValue = 'color: #ccc;';
-        $this->subject->setHtml(
-            '<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>' .
-            '<body><p style="text-align: center;">paragraph</p></body></html>'
-        );
-        $this->subject->disableInlineStyleAttributesParsing();
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="' . $styleAttributeValue . '">', $result);
-    }
-
-    /**
-     * Emogrify was handling case differently for passed-in CSS vs. CSS parsed from style blocks.
-     *
-     * @test
-     */
-    public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
-    {
-        $this->subject->setHtml(
-            '<html><head><style>#topWrap p {padding-bottom: 1px;PADDING-TOP: 0;}</style></head>' .
-            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="padding-bottom: 1px; padding-top: 0; text-align: center;">', $result);
-    }
-
-    /**
-     * Style block CSS overrides values.
-     *
-     * @test
-     */
-    public function emogrifyMergesCssWithMixedCaseAttribute()
-    {
-        $this->subject->setHtml(
-            '<html><head><style>#topWrap p {padding-bottom: 3px;PADDING-TOP: 1px;}</style></head>' .
-            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
-        );
-        $this->subject->setCss('p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains(
-            '<p style="margin: 0; padding-bottom: 3px; padding-top: 1px; text-align: center;">',
-            $result
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyMergesCssWithMixedUnits()
-    {
-        $this->subject->setHtml(
-            '<html><head><style>#topWrap p {margin:0;padding-bottom: 1px;}</style></head>' .
-            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
-        );
-        $this->subject->setCss('p { margin: 1px; padding-bottom:0;}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 0; padding-bottom: 1px; text-align: center;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultRemovesElementsWithDisplayNoneFromExternalCss()
-    {
-        $this->subject->setHtml('<html><body><div class="foo"></div></body></html>');
-        $this->subject->setCss('div.foo { display: none; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('<div class="foo"></div>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyByDefaultRemovesElementsWithDisplayNoneInStyleAttribute()
-    {
-        $this->subject->setHtml(
-            '<html><body><div class="foobar" style="display: none;"></div>' .
-            '</body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('<div', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAfterDisableInvisibleNodeRemovalPreservesInvisibleElements()
-    {
-        $this->subject->setHtml('<html><body><div class="foo"></div></body></html>');
-        $this->subject->setCss('div.foo { display: none; }');
-
-        $this->subject->disableInvisibleNodeRemoval();
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<div class="foo" style="display: none;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsCssMediaQueriesWithCssCommentAfterMediaQuery()
-    {
-        $this->subject->setHtml('<html><body></body></html>');
-        $this->subject->setCss(
-            '@media only screen and (max-width: 480px) { body { color: #ffffff } /* some comment */ }'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('@media only screen and (max-width: 480px)', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $documentType
-     *
-     * @dataProvider documentTypeDataProvider
-     */
-    public function emogrifyConvertsXmlSelfClosingTagsToNonXmlSelfClosingTag($documentType)
-    {
-        $this->subject->setHtml(
-            $documentType . '<html><body><br/></body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<br>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAutomaticallyClosesUnclosedTag()
-    {
-        $this->subject->setHtml('<html><body><p></body></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<body><p></p></body>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyReturnsCompleteHtmlDocument()
-    {
-        $this->subject->setHtml('<html><body><p></p></body></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertSame(
-            $this->html5DocumentType . "\n" .
-            "<html>\n" .
-            '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . "\n" .
-            "<body><p></p></body>\n" .
-            "</html>\n",
-            $result
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyBodyContentReturnsBodyContentFromHtml()
-    {
-        $this->subject->setHtml('<html><body><p></p></body></html>');
-
-        $result = $this->subject->emogrifyBodyContent();
-
-        static::assertSame('<p></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyBodyContentReturnsBodyContentFromPartialContent()
-    {
-        $this->subject->setHtml('<p></p>');
-
-        $result = $this->subject->emogrifyBodyContent();
-
-        static::assertSame('<p></p>', $result);
-    }
-
-    /**
-     * Sets HTML of subject to boilerplate HTML with a single `<p>` in `<body>` and empty `<head>`
-     *
-     * @param string $style Optional value for the style attribute of the `<p>` element
-     *
-     * @return void
-     */
-    private function setSubjectBoilerplateHtml($style = '')
-    {
-        $html = '<html><head></head><body><p';
-        if ($style !== '') {
-            $html .= ' style="' . $style . '"';
-        }
-        $html .= '>some content</p></body></html>';
-        $this->subject->setHtml($html);
-    }
-
-    /**
-     * @test
-     */
-    public function importantInExternalCssOverwritesInlineCss()
-    {
-        $this->setSubjectBoilerplateHtml('margin: 2px;');
-        $this->subject->setCss('p { margin: 1px !important; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 1px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function importantInExternalCssKeepsInlineCssForOtherAttributes()
-    {
-        $this->setSubjectBoilerplateHtml('margin: 2px; text-align: center;');
-        $this->subject->setCss('p { margin: 1px !important; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="text-align: center; margin: 1px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function importantIsCaseInsensitive()
-    {
-        $this->setSubjectBoilerplateHtml('margin: 2px;');
-        $this->subject->setCss('p { margin: 1px !ImPorTant; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 1px !ImPorTant;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function secondImportantStyleOverwritesFirstOne()
-    {
-        $this->setSubjectBoilerplateHtml();
-        $this->subject->setCss('p { margin: 1px !important; } p { margin: 2px !important; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function secondNonImportantStyleOverwritesFirstOne()
-    {
-        $this->setSubjectBoilerplateHtml();
-        $this->subject->setCss('p { margin: 1px; } p { margin: 2px; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function secondNonImportantStyleNotOverwritesFirstImportantOne()
-    {
-        $this->setSubjectBoilerplateHtml();
-        $this->subject->setCss('p { margin: 1px !important; } p { margin: 2px; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 1px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesLaterShorthandStyleAfterIndividualStyle()
-    {
-        $this->setSubjectBoilerplateHtml();
-        $this->subject->setCss('p { margin-top: 1px; } p { margin: 2px; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin-top: 1px; margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesLaterOverridingStyleAfterStyleAfterOverriddenStyle()
-    {
-        $this->setSubjectBoilerplateHtml();
-        $this->subject->setCss('p { margin-top: 1px; } p { margin: 2px; } p { margin-top: 3px; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px; margin-top: 3px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesInlineOverridingStyleAfterCssStyleAfterOverriddenCssStyle()
-    {
-        $this->setSubjectBoilerplateHtml('margin-top: 3px;');
-        $this->subject->setCss('p { margin-top: 1px; } p { margin: 2px; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px; margin-top: 3px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyAppliesLaterInlineOverridingStyleAfterEarlierInlineStyle()
-    {
-        $this->setSubjectBoilerplateHtml('margin: 2px; margin-top: 3px;');
-        $this->subject->setCss('p { margin-top: 1px; }');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 2px; margin-top: 3px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function irrelevantMediaQueriesAreRemoved()
-    {
-        $uselessQuery = '@media all and (max-width: 500px) { em { color:red; } }';
-        $this->subject->setCss($uselessQuery);
-        $this->subject->setHtml('<html><body><p></p></body></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function relevantMediaQueriesAreRetained()
-    {
-        $usefulQuery = '@media all and (max-width: 500px) { p { color:red; } }';
-        $this->subject->setCss($usefulQuery);
-        $this->subject->setHtml('<html><body><p></p></body></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss($usefulQuery, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function importantStyleRuleFromInlineCssOverwritesImportantStyleRuleFromExternalCss()
-    {
-        $this->setSubjectBoilerplateHtml('margin: 2px !important; text-align: center;');
-        $this->subject->setCss('p { margin: 1px !important; padding: 1px;}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="padding: 1px; text-align: center; margin: 2px;">', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addExcludedSelectorRemovesMatchingElementsFromEmogrification()
-    {
-        $this->subject->setHtml('<html><body><p class="x"></p></body></html>');
-        $this->subject->setCss('p { margin: 0; }');
-
-        $this->subject->addExcludedSelector('p.x');
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addExcludedSelectorExcludesMatchingElementEventWithWhitespaceAroundSelector()
-    {
-        $this->subject->setHtml('<html><body><p class="x"></p></body></html>');
-        $this->subject->setCss('p { margin: 0; }');
-
-        $this->subject->addExcludedSelector(' p.x ');
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function addExcludedSelectorKeepsNonMatchingElementsInEmogrification()
-    {
-        $this->subject->setHtml('<html><body><p></p></body></html>');
-        $this->subject->setCss('p { margin: 0; }');
-
-        $this->subject->addExcludedSelector('p.x');
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="margin: 0;"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function removeExcludedSelectorGetsMatchingElementsToBeEmogrifiedAgain()
-    {
-        $this->subject->setHtml('<html><body><p class="x"></p></body></html>');
-        $this->subject->setCss('p { margin: 0; }');
-
-        $this->subject->addExcludedSelector('p.x');
-        $this->subject->removeExcludedSelector('p.x');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p class="x" style="margin: 0;"></p>', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \InvalidArgumentException
-     */
-    public function emogrifyInDebugModeForInvalidExcludedSelectorThrowsException()
-    {
-        $this->subject->setDebug(true);
-
-        $this->subject->setHtml('<html></html>');
-        $this->subject->addExcludedSelector('..p');
-
-        $this->subject->emogrify();
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeIgnoresInvalidExcludedSelector()
-    {
-        $this->subject->setDebug(false);
-
-        $this->subject->setHtml('<html><p class="x"></p></html>');
-        $this->subject->addExcludedSelector('..p');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeIgnoresOnlyInvalidExcludedSelector()
-    {
-        $this->subject->setDebug(false);
-
-        $this->subject->setHtml('<html><p class="x"></p><p class="y"></p><p class="z"></p></html>');
-        $this->subject->setCss('p { color: red };');
-        $this->subject->addExcludedSelector('p.x');
-        $this->subject->addExcludedSelector('..p');
-        $this->subject->addExcludedSelector('p.z');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p class="x"></p>', $result);
-        static::assertContains('<p class="y" style="color: red;"></p>', $result);
-        static::assertContains('<p class="z"></p>', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function emptyMediaQueriesAreRemoved()
-    {
-        $emptyQuery = '@media all and (max-width: 500px) { }';
-        $this->subject->setCss($emptyQuery);
-        $this->subject->setHtml('<html><body><p></p></body></html>');
-
-        $result = $this->subject->emogrify();
-
-        static::assertNotContains('@media', $result);
-    }
-
-    /**
-     * @test
-     */
-    public function multiLineMediaQueryWithWindowsLineEndingsIsAppliedOnlyOnce()
-    {
-        $css = "@media all {\r\n" .
-            ".medium {font-size:18px;}\r\n" .
-            ".small {font-size:14px;}\r\n" .
-            '}';
-        $this->subject->setCss($css);
-        $this->subject->setHtml(
-            '<html><body>' .
-            '<p class="medium">medium</p>' .
-            '<p class="small">small</p>' .
-            '</body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCssCount(1, $css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function multiLineMediaQueryWithUnixLineEndingsIsAppliedOnlyOnce()
-    {
-        $css = "@media all {\n" .
-            ".medium {font-size:18px;}\n" .
-            ".small {font-size:14px;}\n" .
-            '}';
-        $this->subject->setCss($css);
-        $this->subject->setHtml(
-            '<html><body>' .
-            '<p class="medium">medium</p>' .
-            '<p class="small">small</p>' .
-            '</body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCssCount(1, $css, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function multipleMediaQueriesAreAppliedOnlyOnce()
-    {
-        $css = "@media all {\n" .
-            ".medium {font-size:18px;}\n" .
-            ".small {font-size:14px;}\n" .
-            '}' .
-            "@media screen {\n" .
-            ".medium {font-size:24px;}\n" .
-            ".small {font-size:18px;}\n" .
-            '}';
-        $this->subject->setCss($css);
-        $this->subject->setHtml(
-            '<html><body>' .
-            '<p class="medium">medium</p>' .
-            '<p class="small">small</p>' .
-            '</body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCssCount(1, $css, $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function dataUriMediaTypeDataProvider()
-    {
-        return [
-            'nothing' => [''],
-            ';charset=utf-8' => [';charset=utf-8'],
-            ';base64' => [';base64'],
-            ';charset=utf-8;base64' => [';charset=utf-8;base64'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $dataUriMediaType
-     *
-     * @dataProvider dataUriMediaTypeDataProvider
-     */
-    public function dataUrisAreConserved($dataUriMediaType)
-    {
-        $this->subject->setHtml('<html></html>');
-        $styleRule = 'background-image: url(data:image/png' . $dataUriMediaType .
-            ',iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAABUk' .
-            'lEQVQ4y81UsY6CQBCdWXBjYWFMjEgAE0piY8c38B9+iX+ksaHCgs5YWEhIrJCQYGJBomiC7lzhVcfqEa+5KXfey3s783bRdd00TR' .
-            'VFAQAAICJEhN/q8Xjoug7D4RA+qsFgwDjn9QYiTiaT+Xx+OByOx+NqtapjWq0WjEajekPTtCAIiIiIyrKMoqiOMQxDlVqyLMt1XQ' .
-            'A4nU6z2Wy9XkthEnK/3zdN8znC/X7v+36WZfJ7120vFos4joUQRHS5XDabzXK5bGrbtu1er/dtTFU1TWu3202VHceZTqe3242Itt' .
-            'ut53nj8bip8m6345wLIQCgKIowDIuikAoz6Wm3233mjHPe6XRe5UROJqImIWPwh/pvZMbYM2GKorx5oUw6m+v1miTJ+XzO8/x+v7' .
-            '+UtizrM8+GYahVVSFik9/jxy6rqlJN02SM1cmI+GbbQghd178AAO2FXws6LwMAAAAASUVORK5CYII=);';
-        $this->subject->setCss('html {' . $styleRule . '}');
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains(
-            '<html style="' . $styleRule . '">',
-            $result
-        );
-    }
-
-    /**
-     * Data provider for CSS to HTML mapping.
-     *
-     * @return string[][]
-     */
-    public function matchingCssToHtmlMappingDataProvider()
-    {
-        return [
-            'background-color => bgcolor'
-            => ['<p>hi</p>', 'p {background-color: red;}', 'p', 'bgcolor="red"'],
-            'background-color (with !important) => bgcolor'
-            => ['<p>hi</p>', 'p {background-color: red !important;}', 'p', 'bgcolor="red"'],
-            'p.text-align => align'
-            => ['<p>hi</p>', 'p {text-align: justify;}', 'p', 'align="'],
-            'div.text-align => align'
-            => ['<div>hi</div>', 'div {text-align: justify;}', 'div', 'align="'],
-            'td.text-align => align'
-            => ['<table><tr><td>hi</td></tr></table>', 'td {text-align: justify;}', 'td', 'align="'],
-            'text-align: left => align=left'
-            => ['<p>hi</p>', 'p {text-align: left;}', 'p', 'align="left"'],
-            'text-align: right => align=right'
-            => ['<p>hi</p>', 'p {text-align: right;}', 'p', 'align="right"'],
-            'text-align: center => align=center'
-            => ['<p>hi</p>', 'p {text-align: center;}', 'p', 'align="center"'],
-            'text-align: justify => align:justify'
-            => ['<p>hi</p>', 'p {text-align: justify;}', 'p', 'align="justify"'],
-            'img.float: right => align=right'
-            => ['<img>', 'img {float: right;}', 'img', 'align="right"'],
-            'img.float: left => align=left'
-            => ['<img>', 'img {float: left;}', 'img', 'align="left"'],
-            'table.float: right => align=right'
-            => ['<table></table>', 'table {float: right;}', 'table', 'align="right"'],
-            'table.float: left => align=left'
-            => ['<table></table>', 'table {float: left;}', 'table', 'align="left"'],
-            'table.border-spacing: 0 => cellspacing=0'
-            => ['<table><tr><td></td></tr></table>', 'table {border-spacing: 0;}', 'table', 'cellspacing="0"'],
-            'background => bgcolor'
-            => ['<p>Bonjour</p>', 'p {background: red top;}', 'p', 'bgcolor="red"'],
-            'width with px'
-            => ['<p>Hello</p>', 'p {width: 100px;}', 'p', 'width="100"'],
-            'width with %'
-            => ['<p>Hello</p>', 'p {width: 50%;}', 'p', 'width="50%"'],
-            'height with px'
-            => ['<p>Hello</p>', 'p {height: 100px;}', 'p', 'height="100"'],
-            'height with %'
-            => ['<p>Hello</p>', 'p {height: 50%;}', 'p', 'height="50%"'],
-            'img.margin: 0 auto (= horizontal centering) => align=center'
-            => ['<img>', 'img {margin: 0 auto;}', 'img', 'align="center"'],
-            'img.margin: auto (= horizontal centering) => align=center'
-            => ['<img>', 'img {margin: auto;}', 'img', 'align="center"'],
-            'img.margin: 10 auto 30 auto (= horizontal centering) => align=center'
-            => ['<img>', 'img {margin: 10 auto 30 auto;}', 'img', 'align="center"'],
-            'table.margin: 0 auto (= horizontal centering) => align=center'
-            => ['<table></table>', 'table {margin: 0 auto;}', 'table', 'align="center"'],
-            'table.margin: auto (= horizontal centering) => align=center'
-            => ['<table></table>', 'table {margin: auto;}', 'table', 'align="center"'],
-            'table.margin: 10 auto 30 auto (= horizontal centering) => align=center'
-            => ['<table></table>', 'table {margin: 10 auto 30 auto;}', 'table', 'align="center"'],
-            'img.border: none => border=0'
-            => ['<img>', 'img {border: none;}', 'img', 'border="0"'],
-            'img.border: 0 => border=0'
-            => ['<img>', 'img {border: none;}', 'img', 'border="0"'],
-            'table.border: none => border=0'
-            => ['<table></table>', 'table {border: none;}', 'table', 'border="0"'],
-            'table.border: 0 => border=0'
-            => ['<table></table>', 'table {border: none;}', 'table', 'border="0"'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $body The HTML
-     * @param string $css The complete CSS
-     * @param string $tagName The name of the tag that should be modified
-     * @param string $attributes The attributes that are expected on the element
-     *
-     * @dataProvider matchingCssToHtmlMappingDataProvider
-     */
-    public function emogrifierMapsSuitableCssToHtmlIfFeatureIsEnabled($body, $css, $tagName, $attributes)
-    {
-        $this->subject->setHtml('<html><body>' . $body . '</body></html>');
-        $this->subject->setCss($css);
-
-        $this->subject->enableCssToHtmlMapping();
-        $html = $this->subject->emogrify();
-
-        static::assertRegExp('/<' . \preg_quote($tagName, '/') . '[^>]+' . \preg_quote($attributes, '/') . '/', $html);
-    }
-
-    /**
-     * Data provider for CSS to HTML mapping.
-     *
-     * @return string[][]
-     */
-    public function notMatchingCssToHtmlMappingDataProvider()
-    {
-        return [
-            'background URL'
-            => ['<p>Hello</p>', 'p {background: url(bg.png);}', 'bgcolor'],
-            'background URL with position'
-            => ['<p>Hello</p>', 'p {background: url(bg.png) top;}', 'bgcolor'],
-            'img.margin: 10 5 30 auto (= no horizontal centering)'
-            => ['<img>', 'img {margin: 10 5 30 auto;}', 'align'],
-            'p.margin: auto'
-            => ['<p>Bonjour</p>', 'p {margin: auto;}', 'align'],
-            'p.border: none'
-            => ['<p>Bonjour</p>', 'p {border: none;}', 'border'],
-            'img.border: 1px solid black'
-            => ['<p>Bonjour</p>', 'p {border: 1px solid black;}', 'border'],
-            'span.text-align'
-            => ['<span>hi</span>', 'span {text-align: justify;}', 'align'],
-            'text-align: inherit'
-            => ['<p>hi</p>', 'p {text-align: inherit;}', 'align'],
-            'span.float'
-            => ['<span>hi</span>', 'span {float: right;}', 'align'],
-            'float: none'
-            => ['<table></table>', 'table {float: none;}', 'align'],
-            'p.border-spacing'
-            => ['<p>Hello</p>', 'p {border-spacing: 5px;}', 'cellspacing'],
-            'height: auto'
-            => ['<img src="logo.png" alt="">', 'img {width: 110px; height: auto;}', 'height'],
-            'width: auto'
-            => ['<img src="logo.png" alt="">', 'img {width: auto; height: 110px;}', 'width'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $body the HTML
-     * @param string $css the complete CSS
-     * @param string $attribute the attribute that must not be present on this element
-     *
-     * @dataProvider notMatchingCssToHtmlMappingDataProvider
-     */
-    public function emogrifierNotMapsUnsuitableCssToHtmlIfFeatureIsEnabled($body, $css, $attribute)
-    {
-        $this->subject->setHtml('<html><body>' . $body . '</body></html>');
-        $this->subject->setCss($css);
-
-        $this->subject->enableCssToHtmlMapping();
-        $html = $this->subject->emogrify();
-
-        static::assertNotContains(
-            $attribute . '=',
-            $html
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifierNotMapsCssToHtmlIfFeatureIsNotEnabled()
-    {
-        $this->subject->setHtml('<html><body><img></body></html>');
-        $this->subject->setCss('img {float: right;}');
-
-        $html = $this->subject->emogrify();
-
-        static::assertNotContains(
-            'align=',
-            $html
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifierIgnoresPseudoClassCombinedWithPseudoElement()
-    {
-        $this->subject->setHtml('<html><body><div></div></body></html>');
-        $this->subject->setCss('div:last-child::after {float: right;}');
-
-        $html = $this->subject->emogrify();
-
-        static::assertContains('<div></div>', $html);
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyKeepsInlineStylePriorityVersusStyleBlockRules()
-    {
-        $this->subject->setHtml(
-            '<html><head><style>p {padding:10px};</style></head><body><p style="padding-left:20px;"></p></body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="padding: 10px; padding-left: 20px;">', $result);
-    }
-
-    /**
-     * Asserts that $html contains a $tagName tag with the $attribute attribute.
-     *
-     * @param string $html the HTML string we are searching in
-     * @param string $tagName the HTML tag we are looking for
-     * @param string $attribute the attribute we are looking for (with or even without a value)
-     */
-    private function assertHtmlStringContainsTagWithAttribute($html, $tagName, $attribute)
-    {
-        static::assertTrue(
-            \preg_match('/<' . \preg_quote($tagName, '/') . '[^>]+' . \preg_quote($attribute, '/') . '/', $html) > 0
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyPrefersInlineStyleOverCssBlockStyleForHtmlAttributesMapping()
-    {
-        $this->subject->setHtml(
-            '<html><head><style>p {width:1px}</style></head><body><p style="width:2px"></p></body></html>'
-        );
-        $this->subject->enableCssToHtmlMapping();
-
-        $result = $this->subject->emogrify();
-
-        $this->assertHtmlStringContainsTagWithAttribute($result, 'p', 'width="2"');
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyCorrectsHtmlAttributesMappingWhenMultipleMatchingRulesAndLastRuleIsAuto()
-    {
-        $this->subject->setHtml(
-            '<html><head><style>p {width:1px}</style></head><body><p class="autoWidth"></p></body></html>'
-        );
-        $this->subject->setCss('p.autoWidth {width:auto}');
-        $this->subject->enableCssToHtmlMapping();
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p class="autoWidth" style="width: auto;">', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function cssForImportantRuleRemovalDataProvider()
-    {
-        return [
-            'one !important rule only' => [
-                'width: 1px !important',
-                'width: 1px;',
-            ],
-            'multiple !important rules only' => [
-                'width: 1px !important; height: 1px !important',
-                'width: 1px; height: 1px;',
-            ],
-            'multiple declarations, one !important rule at the beginning' => [
-                'width: 1px !important; height: 1px; color: red',
-                'height: 1px; color: red; width: 1px;',
-            ],
-            'multiple declarations, one !important rule somewhere in the middle' => [
-                'height: 1px; width: 1px !important; color: red',
-                'height: 1px; color: red; width: 1px;',
-            ],
-            'multiple declarations, one !important rule at the end' => [
-                'height: 1px; color: red; width: 1px !important',
-                'height: 1px; color: red; width: 1px;',
-            ],
-            'multiple declarations, multiple !important rules at the beginning' => [
-                'width: 1px !important; height: 1px !important; color: red; float: left',
-                'color: red; float: left; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple consecutive !important rules somewhere in the middle (#1)' => [
-                'color: red; width: 1px !important; height: 1px !important; float: left',
-                'color: red; float: left; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple consecutive !important rules somewhere in the middle (#2)' => [
-                'color: red; width: 1px !important; height: 1px !important; float: left; clear: both',
-                'color: red; float: left; clear: both; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple not consecutive !important rules somewhere in the middle' => [
-                'color: red; width: 1px !important; clear: both; height: 1px !important; float: left',
-                'color: red; clear: both; float: left; width: 1px; height: 1px;',
-            ],
-            'multiple declarations, multiple !important rules at the end' => [
-                'color: red; float: left; width: 1px !important; height: 1px !important',
-                'color: red; float: left; width: 1px; height: 1px;',
-            ],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $originalStyleAttributeContent
-     * @param string $expectedStyleAttributeContent
-     *
-     * @dataProvider cssForImportantRuleRemovalDataProvider
-     */
-    public function emogrifyRemovesImportantRule($originalStyleAttributeContent, $expectedStyleAttributeContent)
-    {
-        $this->subject->setHtml(
-            '<html><head><body><p style="' . $originalStyleAttributeContent . '"></p></body></html>'
-        );
-
-        $result = $this->subject->emogrify();
-
-        static::assertContains('<p style="' . $expectedStyleAttributeContent . '">', $result);
-    }
-
-    /**
-     * @test
-     *
-     * @expectedException \InvalidArgumentException
-     */
-    public function emogrifyInDebugModeForInvalidSelectorsInMediaQueryBlocksThrowsException()
-    {
-        $this->subject->setDebug(true);
-
-        $this->subject->setHtml('<html></html>');
-        $this->subject->setCss('@media screen {p^^ {color: red;}}');
-
-        $this->subject->emogrify();
-    }
-
-    /**
-     * @test
-     */
-    public function emogrifyNotInDebugModeKeepsInvalidOrUnrecognizedSelectorsInMediaQueryBlocks()
-    {
-        $this->subject->setDebug(false);
-
-        $this->subject->setHtml('<html></html>');
-        $css = '@media screen {p^^ {color: red;}}';
-        $this->subject->setCss($css);
-
-        $result = $this->subject->emogrify();
-
-        static::assertContainsCss($css, $result);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Support/Traits/AssertCssTest.php b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/tests/Unit/Support/Traits/AssertCssTest.php
deleted file mode 100644 (file)
index 6366ee1..0000000
+++ /dev/null
@@ -1,333 +0,0 @@
-<?php
-
-namespace Pelago\Tests\Unit\Support\Traits;
-
-use Pelago\Tests\Support\Traits\AssertCss;
-
-/**
- * Test case.
- *
- * @author Jake Hotson <jake.github@qzdesign.co.uk>
- */
-class AssertCssTest extends \PHPUnit_Framework_TestCase
-{
-    use AssertCss;
-
-    /**
-     * @test
-     */
-    public function getCssNeedleRegExpEscapesAllSpecialCharacters()
-    {
-        $needle = '.\\+*?[^]$(){}=!<>|:-/';
-
-        $result = static::getCssNeedleRegExp($needle);
-
-        $resultWithWhitespaceMatchersRemoved = \str_replace('\\s*+', '', $result);
-
-        static::assertSame(
-            '/' . \preg_quote($needle, '/') . '/',
-            $resultWithWhitespaceMatchersRemoved
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function getCssNeedleRegExpNotEscapesNonSpecialCharacters()
-    {
-        $needle = \implode('', \array_merge(\range('a', 'z'), \range('A', 'Z'), \range('0 ', '9 ')))
-            . "\r\n\t `¬\"£%&_;'@~,";
-
-        $result = static::getCssNeedleRegExp($needle);
-
-        $resultWithWhitespaceMatchersRemoved = \str_replace('\\s*+', '', $result);
-
-        static::assertSame(
-            '/' . $needle . '/',
-            $resultWithWhitespaceMatchersRemoved
-        );
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function contentWithOptionalWhitespaceDataProvider()
-    {
-        return [
-            '"{" alone' => ['{', ''],
-            '"}" alone' => ['}', ''],
-            '"," alone' => [',', ''],
-            '"{" with non-special character' => ['{', 'a'],
-            '"{" with two non-special characters' => ['{', 'a0'],
-            '"{" with special character' => ['{', '.'],
-            '"{" with two special characters' => ['{', '.+'],
-            '"{" with special character and non-special character' => ['{', '.a'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $contentToInsertAround
-     * @param string $otherContent
-     *
-     * @dataProvider contentWithOptionalWhitespaceDataProvider
-     */
-    public function getCssNeedleRegExpInsertsOptionalWhitespace($contentToInsertAround, $otherContent)
-    {
-        $result = static::getCssNeedleRegExp($otherContent . $contentToInsertAround . $otherContent);
-
-        $quotedOtherContent = \preg_quote($otherContent, '/');
-        $expectedResult = '/' . $quotedOtherContent . '\\s*+' . \preg_quote($contentToInsertAround, '/') . '\\s*+'
-            . $quotedOtherContent . '/';
-
-        static::assertSame($expectedResult, $result);
-    }
-
-    /**
-     * @test
-     */
-    public function getCssNeedleRegExpReplacesWhitespaceAtStartWithOptionalWhitespace()
-    {
-        $result = static::getCssNeedleRegExp(' a');
-
-        static::assertSame('/\\s*+a/', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function styleTagDataProvider()
-    {
-        return [
-            'without space after' => ['<style>a'],
-            'one space after' => ['<style> a'],
-            'two spaces after' => ['<style>  a'],
-            'linefeed after' => ["<style>\na"],
-            'Windows line ending after' => ["<style>\r\na"],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     *
-     * @dataProvider styleTagDataProvider
-     */
-    public function getCssNeedleRegExpInsertsOptionalWhitespaceAfterStyleTag($needle)
-    {
-        $result = static::getCssNeedleRegExp($needle);
-
-        static::assertSame('/\\<style\\>\\s*+a/', $result);
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function needleFoundDataProvider()
-    {
-        $cssStrings = [
-            'unminified CSS' => 'html, body { color: green; }',
-            'minified CSS' => 'html,body{color: green;}',
-            'CSS with extra spaces' => '  html  ,  body  {  color: green;  }',
-            'CSS with linefeeds' => "\nhtml\n,\nbody\n{\ncolor: green;\n}",
-            'CSS with Windows line endings' => "\r\nhtml\r\n,\r\nbody\r\n{\r\ncolor: green;\r\n}",
-        ];
-
-        $datasets = [];
-        foreach ($cssStrings as $needleDescription => $needle) {
-            foreach ($cssStrings as $haystackDescription => $haystack) {
-                $description = $needleDescription . ' in ' . $haystackDescription;
-                $datasets[$description] = [$needle, $haystack];
-                $datasets[$description . ' in <style> tag'] = [
-                    '<style>' . $needle . '</style>',
-                    '<style>' . $haystack . '</style>',
-                ];
-            }
-        }
-        return $datasets;
-    }
-
-    /**
-     * @return string[][]
-     */
-    public function needleNotFoundDataProvider()
-    {
-        return [
-            'CSS part with "{" not in CSS' => ['p {', 'body { color: green; }'],
-            'CSS part with "}" not in CSS' => ['color: red; }', 'body { color: green; }'],
-            'CSS part with "," not in CSS' => ['html, body', 'body { color: green; }'],
-        ];
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     */
-    public function assertContainsCssPassesTestIfNeedleFound($needle, $haystack)
-    {
-        static::assertContainsCss($needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleNotFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertContainsCssFailsTestIfNeedleNotFound($needle, $haystack)
-    {
-        static::assertContainsCss($needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleNotFoundDataProvider
-     */
-    public function assertNotContainsCssPassesTestIfNeedleNotFound($needle, $haystack)
-    {
-        static::assertNotContainsCss($needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertNotContainsCssFailsTestIfNeedleFound($needle, $haystack)
-    {
-        static::assertNotContainsCss($needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleNotFoundDataProvider
-     */
-    public function assertContainsCssCountPassesTestExpectingZeroIfNeedleNotFound($needle, $haystack)
-    {
-        static::assertContainsCssCount(0, $needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertContainsCssCountFailsTestExpectingZeroIfNeedleFound($needle, $haystack)
-    {
-        static::assertContainsCssCount(0, $needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     */
-    public function assertContainsCssCountPassesTestExpectingOneIfNeedleFound($needle, $haystack)
-    {
-        static::assertContainsCssCount(1, $needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleNotFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertContainsCssCountFailsTestExpectingOneIfNeedleNotFound($needle, $haystack)
-    {
-        static::assertContainsCssCount(1, $needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertContainsCssCountFailsTestExpectingOneIfNeedleFoundTwice($needle, $haystack)
-    {
-        static::assertContainsCssCount(1, $needle, $haystack . $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     */
-    public function assertContainsCssCountPassesTestExpectingTwoIfNeedleFoundTwice($needle, $haystack)
-    {
-        static::assertContainsCssCount(2, $needle, $haystack . $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleNotFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertContainsCssCountFailsTestExpectingTwoIfNeedleNotFound($needle, $haystack)
-    {
-        static::assertContainsCssCount(2, $needle, $haystack);
-    }
-
-    /**
-     * @test
-     *
-     * @param string $needle
-     * @param string $haystack
-     *
-     * @dataProvider needleFoundDataProvider
-     *
-     * @expectedException \PHPUnit_Framework_ExpectationFailedException
-     */
-    public function assertContainsCssCountFailsTestExpectingTwoIfNeedleFoundOnlyOnce($needle, $haystack)
-    {
-        static::assertContainsCssCount(2, $needle, $haystack);
-    }
-}