Update pelago/emogrifier to 5.0
authorTim Düsterhus <duesterhus@woltlab.com>
Tue, 24 Nov 2020 14:45:28 +0000 (15:45 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Tue, 24 Nov 2020 14:45:28 +0000 (15:45 +0100)
14 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/installed.json
wcfsetup/install/files/lib/system/api/composer/installed.php
wcfsetup/install/files/lib/system/api/pelago/emogrifier/CHANGELOG.md
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/phpunit.xml [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/CssInliner.php
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/AbstractHtmlProcessor.php
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/CssToAttributeConverter.php
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/HtmlProcessor/HtmlPruner.php
wcfsetup/install/files/lib/system/api/pelago/emogrifier/src/Utilities/CssConcatenator.php

index 24d419216da6b5b67ecc381343998d3aec76aec0..bb275e6451996173f14a724a72ceea60651dad6e 100644 (file)
@@ -10,7 +10,7 @@
     "require": {
         "ezyang/htmlpurifier": "4.13.*",
         "erusev/parsedown": "1.7.*",
-        "pelago/emogrifier": "4.0.*",
+        "pelago/emogrifier": "5.0.*",
         "chrisjean/php-ico": "1.0.*",
         "true/punycode": "~2.0",
         "pear/net_idna2": "^0.2.0",
index 0f0ab507e9758da1313d29176a10d47039c255cd..736fc36318fe9cfae78fcf21875cf4644639dfdf 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": "88f8ac80c8e871d978df5d19cf119c8e",
+    "content-hash": "0930361ee03b18bc90818e4c0c6afddd",
     "packages": [
         {
             "name": "chrisjean/php-ico",
         },
         {
             "name": "pelago/emogrifier",
-            "version": "v4.0.0",
+            "version": "v5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/MyIntervals/emogrifier.git",
-                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9"
+                "reference": "b43b650880d189b0ada61d95d0729c7424b1752d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/f6fd679303c6e6861b5ff29af221f684729d8fd9",
-                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9",
+                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/b43b650880d189b0ada61d95d0729c7424b1752d",
+                "reference": "b43b650880d189b0ada61d95d0729c7424b1752d",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
-                "php": "~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4",
-                "symfony/css-selector": "^3.4.32 || ^4.3.5 || ^5.0"
+                "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0",
+                "symfony/css-selector": "^3.4.32 || ^4.4 || ^5.1"
             },
             "require-dev": {
-                "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"
+                "php-parallel-lint/php-parallel-lint": "^1.2.0",
+                "slevomat/coding-standard": "^6.4.1",
+                "squizlabs/php_codesniffer": "^3.5.8"
             },
             "type": "library",
             "extra": {
                 "issues": "https://github.com/MyIntervals/emogrifier/issues",
                 "source": "https://github.com/MyIntervals/emogrifier"
             },
-            "time": "2020-06-12T12:55:03+00:00"
+            "time": "2020-11-23T18:37:25+00:00"
         },
         {
             "name": "psr/http-client",
index 03e40ce82ba9c407167768fd74178be51af88113..42ca8ce191d17cdb5d0d7c89dfd6a5e64989e74a 100644 (file)
@@ -116,12 +116,12 @@ private static $installed = array (
     ),
     'pelago/emogrifier' => 
     array (
-      'pretty_version' => 'v4.0.0',
-      'version' => '4.0.0.0',
+      'pretty_version' => 'v5.0.0',
+      'version' => '5.0.0.0',
       'aliases' => 
       array (
       ),
-      'reference' => 'f6fd679303c6e6861b5ff29af221f684729d8fd9',
+      'reference' => 'b43b650880d189b0ada61d95d0729c7424b1752d',
     ),
     'psr/http-client' => 
     array (
index 954c8fe154ff3e909ff7873137ff6d5140acd65b..86edc8309458ec13f1a5ef4c86f1ebd90c29c236 100644 (file)
         },
         {
             "name": "pelago/emogrifier",
-            "version": "v4.0.0",
-            "version_normalized": "4.0.0.0",
+            "version": "v5.0.0",
+            "version_normalized": "5.0.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/MyIntervals/emogrifier.git",
-                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9"
+                "reference": "b43b650880d189b0ada61d95d0729c7424b1752d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/f6fd679303c6e6861b5ff29af221f684729d8fd9",
-                "reference": "f6fd679303c6e6861b5ff29af221f684729d8fd9",
+                "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/b43b650880d189b0ada61d95d0729c7424b1752d",
+                "reference": "b43b650880d189b0ada61d95d0729c7424b1752d",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
-                "php": "~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4",
-                "symfony/css-selector": "^3.4.32 || ^4.3.5 || ^5.0"
+                "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0",
+                "symfony/css-selector": "^3.4.32 || ^4.4 || ^5.1"
             },
             "require-dev": {
-                "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",
+                "php-parallel-lint/php-parallel-lint": "^1.2.0",
+                "slevomat/coding-standard": "^6.4.1",
+                "squizlabs/php_codesniffer": "^3.5.8"
+            },
+            "time": "2020-11-23T18:37:25+00:00",
             "type": "library",
             "extra": {
                 "branch-alias": {
index b2473602fa04a146eb344de22a17be46cdefde19..34b82497eddcc9d9f9f036dcc5ce83080f9a929e 100644 (file)
     ),
     'pelago/emogrifier' => 
     array (
-      'pretty_version' => 'v4.0.0',
-      'version' => '4.0.0.0',
+      'pretty_version' => 'v5.0.0',
+      'version' => '5.0.0.0',
       'aliases' => 
       array (
       ),
-      'reference' => 'f6fd679303c6e6861b5ff29af221f684729d8fd9',
+      'reference' => 'b43b650880d189b0ada61d95d0729c7424b1752d',
     ),
     'psr/http-client' => 
     array (
index 37e336020b1ffdf4a109ea5dd158db45b10f79c5..3c455a1a6d071e5d267bb26bf94f3f027d5c46db 100644 (file)
@@ -9,12 +9,71 @@ This project adheres to [Semantic Versioning](https://semver.org/).
 
 ### Changed
 
+### Deprecated
+
+### Removed
+
+### Fixed
+
+## 5.0.0
+
+### Added
+- Add an `.editorconfig` file
+  ([#940](https://github.com/MyIntervals/emogrifier/pull/940))
+- Support PHP 8.0
+  ([#926](https://github.com/MyIntervals/emogrifier/pull/926))
+- Run the CI build once a week
+  ([#933](https://github.com/MyIntervals/emogrifier/pull/933))
+- Move more development tools to PHIVE
+  ([#894](https://github.com/MyIntervals/emogrifier/pull/894),
+  [#907](https://github.com/MyIntervals/emogrifier/pull/907))
+
+### Changed
+- Automatically add a backslash for global functions
+  ([#909](https://github.com/MyIntervals/emogrifier/pull/909))
+- Update the development tools
+  ([#898](https://github.com/MyIntervals/emogrifier/pull/898),
+  [#895](https://github.com/MyIntervals/emogrifier/pull/895))
+- Upgrade to PHPUnit 7.5
+  ([#888](https://github.com/MyIntervals/emogrifier/pull/888))
+- Enforce constant visibility
+  ([#892](https://github.com/MyIntervals/emogrifier/pull/892))
+- Rename the PHPCS configuration file
+  ([#891](https://github.com/MyIntervals/emogrifier/pull/891),
+  [#896](https://github.com/MyIntervals/emogrifier/pull/896))
+- Make use of PHP 7.1 language features
+  ([#883](https://github.com/MyIntervals/emogrifier/pull/883))
+
 ### Deprecated
 - Support for PHP 7.1 will be removed in Emogrifier 6.0.
 
 ### Removed
+- Drop support for Symfony 4.3 and 5.0
+  ([#936](https://github.com/MyIntervals/emogrifier/pull/936))
+- Stop checking `tests/` with Psalm
+  ([#885](https://github.com/MyIntervals/emogrifier/pull/885))
+- Drop support for PHP 7.0
+  ([#880](https://github.com/MyIntervals/emogrifier/pull/880))
 
 ### Fixed
+- Fix a nonsensical code example in the README
+  ([#920](https://github.com/MyIntervals/emogrifier/issues/920),
+  [#935](https://github.com/MyIntervals/emogrifier/pull/935))
+- Remove `!important` from `style` attributes also when uppercase, mixed case or
+  having whitespace after `!`
+  ([#911](https://github.com/MyIntervals/emogrifier/pull/911))
+- Copy rules using `:...of-type` without a type to the `<style>` element
+  ([#904](https://github.com/MyIntervals/emogrifier/pull/904))
+- Support combinator followed by dynamic pseudo-class in minified CSS
+  ([#903](https://github.com/MyIntervals/emogrifier/pull/903))
+- Preserve all uninlinable (or otherwise unprocessed) at-rules
+  ([#899](https://github.com/MyIntervals/emogrifier/pull/899))
+- Allow Windows CLI to run development tools installed through PHIVE
+  ([#900](https://github.com/MyIntervals/emogrifier/pull/900))
+- Switch to a maintained package for parallel PHP linting
+  ([#884](https://github.com/MyIntervals/emogrifier/pull/884))
+- Add `.0` version suffixes to PHP version requirements
+  ([#881](https://github.com/MyIntervals/emogrifier/pull/881))
 
 ## 4.0.0
 
@@ -47,7 +106,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
   ([#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
+- 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
index a9540373ef549bf87c37723cf5bf4da354fcf138..fea1536d42ba3dff406c958e77db5bd5caf826ae 100644 (file)
@@ -277,7 +277,7 @@ New code using `CssInliner` and family:
 ```php
 $domDocument = CssInliner::fromHtml($html)->inlineCss($css)->getDomDocument();
 
-HtmlPruner::fromDomDocument($domDocument)->removeElementsWithDisplayNone(),
+HtmlPruner::fromDomDocument($domDocument)->removeElementsWithDisplayNone();
 $html = CssToAttributeConverter::fromDomDocument($domDocument)
   ->convertCssToVisualAttributes()->render();
 ```
@@ -307,43 +307,41 @@ Emogrifier currently supports the following
    * [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(*)`)
+     (with a type, e.g. `p:first-of-type` but not `*:first-of-type`)
    * [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(*)`)
+     (with a type)
    * [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(*)`)
+     (with a type)
    * [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`)
+     (with a type)
    * [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`)
+     (with a type)
 
 The following selectors are not implemented yet:
 
  * [case-insensitive attribute value](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#case-insensitive)
- * static [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
+ * static [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/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)
    * [first-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type)
-     without a type (declarations discarded)
+     without a type
    * [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
-     without a type (declarations discarded)
+     without a type
    * [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
-     without a type (declarations discarded)
+     without a type
    * [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
-     without a type (will behave as `:nth-child()`)
+     without a type
    * [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)
+     without a type
+   * [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:
index e1161abedebd46c68965fcbc80d69b13d7b78e4c..a91e69cf1d10c71007939693fdcc882cc959245e 100644 (file)
         "source": "https://github.com/MyIntervals/emogrifier"
     },
     "require": {
-        "php": "~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4",
+        "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0",
         "ext-dom": "*",
         "ext-libxml": "*",
-        "symfony/css-selector": "^3.4.32 || ^4.3.5 || ^5.0"
+        "symfony/css-selector": "^3.4.32 || ^4.4 || ^5.1"
     },
     "require-dev": {
-        "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"
+        "php-parallel-lint/php-parallel-lint": "^1.2.0",
+        "slevomat/coding-standard": "^6.4.1",
+        "squizlabs/php_codesniffer": "^3.5.8"
     },
     "autoload": {
         "psr-4": {
     },
     "scripts": {
         "php:version": "php -v | grep -Po 'PHP\\s++\\K(?:\\d++\\.)*+\\d++(?:-\\w++)?+'",
-        "php:fix": "\"./tools/php-cs-fixer\" --config=config/php-cs-fixer.php fix config/ src/ tests/",
+        "php:fix": "\"./tools/php-cs-fixer.phar\" --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:php:fixer": "\"./tools/php-cs-fixer.phar\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff-format=udiff config/ src/ tests/",
+        "ci:php:md": "\"./tools/phpmd.phar\" src text config/phpmd.xml",
+        "ci:php:psalm": "\"./tools/psalm.phar\" --show-info=false",
+        "ci:tests:unit": "\"./tools/phpunit.phar\"",
+        "ci:tests:sof": "\"./tools/phpunit.phar\" --stop-on-failure",
         "ci:tests": [
             "@ci:tests:unit"
         ],
@@ -93,7 +90,8 @@
         "ci": [
             "@ci:static",
             "@ci:dynamic"
-        ]
+        ],
+        "phive:update:phpunit": "echo y | \"./tools/phive.phar\" --no-progress update phpunit"
     },
     "extra": {
         "branch-alias": {
diff --git a/wcfsetup/install/files/lib/system/api/pelago/emogrifier/phpunit.xml b/wcfsetup/install/files/lib/system/api/pelago/emogrifier/phpunit.xml
new file mode 100644 (file)
index 0000000..c009533
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         colors="true"
+         verbose="true">
+    <testsuites>
+        <testsuite name="default">
+            <directory suffix="Test.php">tests</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
index 30dd635cba91c334a981e075c2464e4983f61653..5459df474e13b408c419cabce077051f72d05088 100644 (file)
@@ -28,22 +28,34 @@ class CssInliner extends AbstractHtmlProcessor
     /**
      * @var int
      */
-    const CACHE_KEY_CSS = 0;
+    private const CACHE_KEY_CSS = 0;
 
     /**
      * @var int
      */
-    const CACHE_KEY_SELECTOR = 1;
+    private const CACHE_KEY_SELECTOR = 1;
 
     /**
      * @var int
      */
-    const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 2;
+    private const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 2;
 
     /**
      * @var int
      */
-    const CACHE_KEY_COMBINED_STYLES = 3;
+    private const CACHE_KEY_COMBINED_STYLES = 3;
+
+    /**
+     * This regular expression pattern will match any uninlinable at-rule with nested statements, along with any
+     * whitespace immediately following.  Currently, any at-rule apart from `@media` is considered uninlinable.  The
+     * first capturing group matches the at sign and identifier (e.g. `@font-face`).  The second capturing group matches
+     * the nested statements along with their enclosing curly brackets (i.e. `{...}`), and via `(?2)` will match deeper
+     * nested blocks recursively.
+     *
+     * @var string
+     */
+    private const UNINLINABLE_AT_RULE_MATCHER
+        = '/(@(?!media\\b)[\\w\\-]++)[^\\{]*+(\\{[^\\{\\}]*+(?:(?2)[^\\{\\}]*+)*+\\})\\s*+/i';
 
     /**
      * Regular expression component matching a static pseudo class in a selector, without the preceding ":",
@@ -53,7 +65,23 @@ class CssInliner extends AbstractHtmlProcessor
      *
      * @var string
      */
-    const PSEUDO_CLASS_MATCHER = 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)';
+    private const PSEUDO_CLASS_MATCHER
+        = 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)';
+
+    /**
+     * This regular expression componenet matches an `...of-type` pseudo class name, without the preceding ":".  These
+     * pseudo-classes can currently online be inlined if they have an associated type in the selector expression.
+     *
+     * @var string
+     */
+    private const OF_TYPE_PSEUDO_CLASS_MATCHER = '(?:first|last|nth(?:-last)?+|only)-of-type';
+
+    /**
+     * regular expression component to match a selector combinator
+     *
+     * @var string
+     */
+    private const COMBINATOR_MATCHER = '(?:\\s++|\\s*+[>+~]\\s*+)(?=[[:alpha:]_\\-.#*:\\[])';
 
     /**
      * @var bool[]
@@ -153,7 +181,8 @@ class CssInliner extends AbstractHtmlProcessor
      *
      * @return self fluent interface
      *
-     * @throws ParseException
+     * @throws ParseException in debug mode, if an invalid selector is encountered
+     * @throws \RuntimeException in debug mode, if an internal PCRE error occurs
      */
     public function inlineCss(string $css = ''): self
     {
@@ -170,15 +199,15 @@ class CssInliner extends AbstractHtmlProcessor
         }
 
         $cssWithoutComments = $this->removeCssComments($combinedCss);
-        list($cssWithoutCommentsCharsetOrImport, $cssImportRules)
+        [$cssWithoutCommentsCharsetOrImport, $cssImportRules]
             = $this->extractImportAndCharsetRules($cssWithoutComments);
-        list($cssWithoutCommentsCharsetImportOrFontFace, $cssFontFaces)
-            = $this->extractFontFaceRules($cssWithoutCommentsCharsetOrImport);
+        [$cssWithoutCommentsOrUninlinableAtRules, $cssAtRules]
+            = $this->extractUninlinableCssAtRules($cssWithoutCommentsCharsetOrImport);
 
-        $uninlinableCss = $cssImportRules . $cssFontFaces;
+        $uninlinableCss = $cssImportRules . $cssAtRules;
 
         $excludedNodes = $this->getNodesToExclude();
-        $cssRules = $this->parseCssRules($cssWithoutCommentsCharsetImportOrFontFace);
+        $cssRules = $this->parseCssRules($cssWithoutCommentsOrUninlinableAtRules);
         $cssSelectorConverter = $this->getCssSelectorConverter();
         foreach ($cssRules['inlinable'] as $cssRule) {
             try {
@@ -332,10 +361,8 @@ class CssInliner extends AbstractHtmlProcessor
 
     /**
      * Clears all caches.
-     *
-     * @return void
      */
-    private function clearAllCaches()
+    private function clearAllCaches(): void
     {
         $this->caches = [
             self::CACHE_KEY_CSS => [],
@@ -347,10 +374,8 @@ class CssInliner extends AbstractHtmlProcessor
 
     /**
      * Purges the visited nodes.
-     *
-     * @return void
      */
-    private function purgeVisitedNodes()
+    private function purgeVisitedNodes(): void
     {
         $this->visitedNodes = [];
         $this->styleAttributesForNodes = [];
@@ -361,10 +386,8 @@ class CssInliner extends AbstractHtmlProcessor
      * 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()
+    private function normalizeStyleAttributesOfAllNodes(): void
     {
         /** @var \DOMElement $node */
         foreach ($this->getAllNodesWithStyleAttribute() as $node) {
@@ -393,10 +416,8 @@ class CssInliner extends AbstractHtmlProcessor
      * Normalizes the value of the "style" attribute and saves it.
      *
      * @param \DOMElement $node
-     *
-     * @return void
      */
-    private function normalizeStyleAttributes(\DOMElement $node)
+    private function normalizeStyleAttributes(\DOMElement $node): void
     {
         $normalizedOriginalStyle = \preg_replace_callback(
             '/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S',
@@ -471,10 +492,12 @@ class CssInliner extends AbstractHtmlProcessor
         }
 
         $css = '';
-        /** @var \DOMNode $styleNode */
         foreach ($styleNodes as $styleNode) {
             $css .= "\n\n" . $styleNode->nodeValue;
-            $styleNode->parentNode->removeChild($styleNode);
+            $parentNode = $styleNode->parentNode;
+            if ($parentNode instanceof \DOMNode) {
+                $parentNode->removeChild($styleNode);
+            }
         }
 
         return $css;
@@ -501,9 +524,9 @@ class CssInliner extends AbstractHtmlProcessor
      * @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.
+     *         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
     {
@@ -517,7 +540,7 @@ class CssInliner extends AbstractHtmlProcessor
                 $matches
             )
         ) {
-            list($fullMatch, $atRuleAndFollowingWhitespace, $atRuleName) = $matches;
+            [$fullMatch, $atRuleAndFollowingWhitespace, $atRuleName] = $matches;
 
             if (\strtolower($atRuleName) === 'import') {
                 $importRules .= $atRuleAndFollowingWhitespace;
@@ -530,38 +553,58 @@ class CssInliner extends AbstractHtmlProcessor
     }
 
     /**
-     * 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.
+     * Extracts uninlinable at-rules with nested statements (i.e. a block enclosed in curly brackets) from the supplied
+     * CSS.  Currently, any such at-rule apart from `@media` is considered uninlinable.  These rules can be placed
+     * anywhere in the CSS and are not case sensitive.  `@font-face` rules will be checked for validity, though other
+     * at-rules will be assumed to be valid.
      *
      * @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.
+     * @return string[] The first element is the CSS with the at-rules removed.  The second element contains a
+     *                  concatenation of the valid at-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
+     *                  at-rules, it will be an empty string.
      */
-    private function extractFontFaceRules(string $css): array
+    private function extractUninlinableCssAtRules(string $css): array
     {
         $possiblyModifiedCss = $css;
-        $fontFaces = '';
+        $atRules = '';
 
         while (
             \preg_match(
-                '/(@font-face[^}]++}\\s*+)/i',
+                self::UNINLINABLE_AT_RULE_MATCHER,
                 $possiblyModifiedCss,
                 $matches
             )
         ) {
-            list($fullMatch, $atRuleAndFollowingWhitespace) = $matches;
+            [$fullMatch, $atRuleName] = $matches;
 
-            if (\stripos($fullMatch, 'font-family') !== false && \stripos($fullMatch, 'src') !== false) {
-                $fontFaces .= $atRuleAndFollowingWhitespace;
+            if ($this->isValidAtRule($atRuleName, $fullMatch)) {
+                $atRules .= $fullMatch;
             }
 
             $possiblyModifiedCss = \str_replace($fullMatch, '', $possiblyModifiedCss);
         }
 
-        return [$possiblyModifiedCss, $fontFaces];
+        return [$possiblyModifiedCss, $atRules];
+    }
+
+    /**
+     * Tests if an at-rule is valid.  Currently only `@font-face` rules are checked for validity; others are assumed to
+     * be valid.
+     *
+     * @param string $atIdentifier name of the at-rule with the preceding at sign
+     * @param string $rule full content of the rule, including the at-identifier
+     *
+     * @return bool
+     */
+    private function isValidAtRule(string $atIdentifier, string $rule): bool
+    {
+        if (\strcasecmp($atIdentifier, '@font-face') === 0) {
+            return \stripos($rule, 'font-family') !== false && \stripos($rule, 'src') !== false;
+        }
+
+        return true;
     }
 
     /**
@@ -633,8 +676,6 @@ class CssInliner extends AbstractHtmlProcessor
             'inlinable' => [],
             'uninlinable' => [],
         ];
-        /** @var string[][] $matches */
-        /** @var string[] $cssRule */
         foreach ($matches as $key => $cssRule) {
             $cssDeclaration = \trim($cssRule['declarations']);
             if ($cssDeclaration === '') {
@@ -645,11 +686,7 @@ class CssInliner extends AbstractHtmlProcessor
                 // 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;
+                $hasUnmatchablePseudo = $hasPseudoElement || $this->hasUnsupportedPseudoClass($selector);
 
                 $parsedCssRule = [
                     'media' => $cssRule['media'],
@@ -671,6 +708,53 @@ class CssInliner extends AbstractHtmlProcessor
         return $cssRules;
     }
 
+    /**
+     * Tests if a selector contains a pseudo-class which would mean it cannot be converted to an XPath expression for
+     * inlining CSS declarations.
+     *
+     * Any pseudo class that does not match {@see PSEUDO_CLASS_MATCHER} cannot be converted.  Additionally, `...of-type`
+     * pseudo-classes cannot be converted if they are not associated with a type selector.
+     *
+     * @param string $selector
+     *
+     * @return bool
+     */
+    private function hasUnsupportedPseudoClass(string $selector): bool
+    {
+        if (\preg_match('/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector)) {
+            return true;
+        }
+
+        if (!\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selector)) {
+            return false;
+        }
+
+        foreach (\preg_split('/' . self::COMBINATOR_MATCHER . '/', $selector) as $selectorPart) {
+            if ($this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Tests if part of a selector contains an `...of-type` pseudo-class such that it cannot be converted to an XPath
+     * expression.
+     *
+     * @param string $selectorPart part of a selector which has been split up at combinators
+     *
+     * @return bool `true` if the selector part does not have a type but does have an `...of-type` pseudo-class
+     */
+    private function selectorPartHasUnsupportedOfTypePseudoClass(string $selectorPart): bool
+    {
+        if (\preg_match('/^[\\w\\-]/', $selectorPart)) {
+            return false;
+        }
+
+        return (bool)\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorPart);
+    }
+
     /**
      * @param string[] $a
      * @param string[] $b
@@ -708,7 +792,7 @@ class CssInliner extends AbstractHtmlProcessor
             }
             $number = 0;
             $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number);
-            $precedence += ($value * $number);
+            $precedence += ($value * (int)$number);
         }
         $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
 
@@ -720,7 +804,7 @@ class CssInliner extends AbstractHtmlProcessor
      *
      * @param string $css CSS with comments removed
      *
-     * @return string[][] Array of string sub-arrays with the keys
+     * @return array<array-key, array<string, 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"),
@@ -736,7 +820,7 @@ class CssInliner extends AbstractHtmlProcessor
             // process each part for selectors and definitions
             \preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $cssPart['css'], $matches, PREG_SET_ORDER);
 
-            /** @var string[][] $matches */
+            /** @var string[] $cssRule */
             foreach ($matches as $cssRule) {
                 $ruleMatches[] = [
                     'media' => $cssPart['media'],
@@ -830,10 +914,8 @@ class CssInliner extends AbstractHtmlProcessor
      *
      * @param \DOMElement $node
      * @param string[][] $cssRule
-     *
-     * @return void
      */
-    private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule)
+    private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule): void
     {
         /** @var string $declarationsBlock */
         $declarationsBlock = $cssRule['declarationsBlock'];
@@ -912,15 +994,13 @@ class CssInliner extends AbstractHtmlProcessor
      */
     private function attributeValueIsImportant(string $attributeValue): bool
     {
-        return \strtolower(\substr(\trim($attributeValue), -10)) === '!important';
+        return (bool)\preg_match('/!\\s*+important$/i', $attributeValue);
     }
 
     /**
      * Merges styles from styles attributes and style nodes and applies them to the attribute nodes
-     *
-     * @return void
      */
-    private function fillStyleAttributesWithMergedStyles()
+    private function fillStyleAttributesWithMergedStyles(): void
     {
         foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
             $node = $this->visitedNodes[$nodePath];
@@ -939,9 +1019,9 @@ class CssInliner extends AbstractHtmlProcessor
      * 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
+     * @throws \RuntimeException
      */
-    private function removeImportantAnnotationFromAllInlineStyles()
+    private function removeImportantAnnotationFromAllInlineStyles(): void
     {
         foreach ($this->getAllNodesWithStyleAttribute() as $node) {
             $this->removeImportantAnnotationFromNodeInlineStyle($node);
@@ -958,16 +1038,16 @@ class CssInliner extends AbstractHtmlProcessor
      *
      * @param \DOMElement $node
      *
-     * @return void
+     * @throws \RuntimeException
      */
-    private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node)
+    private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node): void
     {
         $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
         $regularStyleDeclarations = [];
         $importantStyleDeclarations = [];
         foreach ($inlineStyleDeclarations as $property => $value) {
             if ($this->attributeValueIsImportant($value)) {
-                $importantStyleDeclarations[$property] = \trim(\str_replace('!important', '', $value));
+                $importantStyleDeclarations[$property] = $this->pregReplace('/\\s*+!\\s*+important$/i', '', $value);
             } else {
                 $regularStyleDeclarations[$property] = $value;
             }
@@ -999,10 +1079,8 @@ class CssInliner extends AbstractHtmlProcessor
      * `$this->matchingUninlinableCssRules`.
      *
      * @param string[][] $cssRules the "uninlinable" array of CSS rules returned by `parseCssRules`
-     *
-     * @return void
      */
-    private function determineMatchingUninlinableCssRules(array $cssRules)
+    private function determineMatchingUninlinableCssRules(array $cssRules): void
     {
         $this->matchingUninlinableCssRules = \array_filter($cssRules, [$this, 'existsMatchForSelectorInCssRule']);
     }
@@ -1068,17 +1146,33 @@ class CssInliner extends AbstractHtmlProcessor
         // 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',
+            '/([\\s>+~]?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i',
             [$this, 'replaceUnmatchableNotComponent'],
             ' ' . $selector
         ));
 
-        $pseudoComponentMatcher = ':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+';
-        return \preg_replace(
-            ['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'],
-            ['$1*', ''],
+        $selectorWithoutUnmatchablePseudoComponents = $this->removeSelectorComponents(
+            ':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+',
             $selectorWithoutNots
         );
+
+        if (
+            !\preg_match(
+                '/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i',
+                $selectorWithoutUnmatchablePseudoComponents
+            )
+        ) {
+            return $selectorWithoutUnmatchablePseudoComponents;
+        }
+        return \implode('', \array_map(
+            [$this, 'removeUnsupportedOfTypePseudoClasses'],
+            \preg_split(
+                '/(' . self::COMBINATOR_MATCHER . ')/',
+                $selectorWithoutUnmatchablePseudoComponents,
+                -1,
+                PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
+            )
+        ));
     }
 
     /**
@@ -1088,21 +1182,55 @@ class CssInliner extends AbstractHtmlProcessor
      * @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
+     *         combinator followed by "*", or an empty string if there was no preceding combinator
      */
     private function replaceUnmatchableNotComponent(array $matches): string
     {
-        list($notComponentWithAnyPrecedingWhitespace, $anyPrecedingWhitespace, $notArgumentInBrackets) = $matches;
+        [$notComponentWithAnyPrecedingCombinator, $anyPrecedingCombinator, $notArgumentInBrackets] = $matches;
+
+        if ($this->hasUnsupportedPseudoClass($notArgumentInBrackets)) {
+            return $anyPrecedingCombinator !== '' ? $anyPrecedingCombinator . '*' : '';
+        }
+        return $notComponentWithAnyPrecedingCombinator;
+    }
 
-        $hasUnmatchablePseudo = \preg_match(
-            '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-:]/i',
-            $notArgumentInBrackets
+    /**
+     * Removes components from a CSS selector, replacing them with "*" if necessary.
+     *
+     * @param string $matcher regular expression part to match the components to remove
+     * @param string $selector
+     *
+     * @return string selector which will match the relevant DOM elements if the removed components are assumed to apply
+     *         (or in the case of pseudo-elements will match their originating element)
+     */
+    private function removeSelectorComponents(string $matcher, string $selector): string
+    {
+        return \preg_replace(
+            ['/([\\s>+~]|^)' . $matcher . '/i', '/' . $matcher . '/i'],
+            ['$1*', ''],
+            $selector
         );
+    }
 
-        if ($hasUnmatchablePseudo) {
-            return $anyPrecedingWhitespace !== '' ? $anyPrecedingWhitespace . '*' : '';
+    /**
+     * Removes any `...-of-type` pseudo-classes from part of a CSS selector, if it does not have a type, replacing them
+     * with "*" if necessary.
+     *
+     * @param string $selectorPart part of a selector which has been split up at combinators
+     *
+     * @return string selector part which will match the relevant DOM elements if the pseudo-classes are assumed to
+     *         apply
+     */
+    private function removeUnsupportedOfTypePseudoClasses(string $selectorPart): string
+    {
+        if (!$this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
+            return $selectorPart;
         }
-        return $notComponentWithAnyPrecedingWhitespace;
+
+        return $this->removeSelectorComponents(
+            ':(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')(?:\\([^\\)]*+\\))?+',
+            $selectorPart
+        );
     }
 
     /**
@@ -1113,10 +1241,8 @@ class CssInliner extends AbstractHtmlProcessor
      *        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)
+    private function copyUninlinableCssToStyleNode(string $uninlinableCss): void
     {
         $css = $uninlinableCss;
 
@@ -1143,10 +1269,8 @@ class CssInliner extends AbstractHtmlProcessor
      * @see https://github.com/MyIntervals/emogrifier/issues/103
      *
      * @param string $css
-     *
-     * @return void
      */
-    protected function addStyleElementToDocument(string $css)
+    protected function addStyleElementToDocument(string $css): void
     {
         $styleElement = $this->domDocument->createElement('style', $css);
         $styleAttribute = $this->domDocument->createAttribute('type');
@@ -1168,4 +1292,60 @@ class CssInliner extends AbstractHtmlProcessor
     {
         return $this->domDocument->getElementsByTagName('head')->item(0);
     }
+
+    /**
+     * Wraps `preg_replace`.  If an error occurs (which is highly unlikely), either it is logged and the original
+     * `$subject` is returned, or in debug mode an exception is thrown.
+     *
+     * This method does not currently allow `$subject` (and return value) to be an array, because a means of telling
+     * Psalm that a method returns the same type a particular parameter has not been found (though it knows this for
+     * `preg_replace`); nor does it currently support the optional parameters.
+     *
+     * @param string|string[] $pattern
+     * @param string|string[] $replacement
+     * @param string $subject
+     *
+     * @return string
+     *
+     * @throws \RuntimeException
+     */
+    private function pregReplace($pattern, $replacement, string $subject): string
+    {
+        $result = \preg_replace($pattern, $replacement, $subject);
+
+        if ($result === null) {
+            $this->logOrThrowPregLastError();
+            $result = $subject;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Obtains the name of the error constant for `preg_last_error` (based on code posted at
+     * {@see https://www.php.net/manual/en/function.preg-last-error.php#124124}) and puts it into an error message
+     * which is either passed to `trigger_error` (in non-debug mode) or an exception which is thrown (in debug mode).
+     *
+     * @throws \RuntimeException
+     */
+    private function logOrThrowPregLastError(): void
+    {
+        $pcreConstants = \get_defined_constants(true)['pcre'];
+        $pcreErrorConstantNames = \is_array($pcreConstants) ? \array_flip(\array_filter(
+            $pcreConstants,
+            function (string $key): bool {
+                return \substr($key, -6) === '_ERROR';
+            },
+            ARRAY_FILTER_USE_KEY
+        )) : [];
+
+        $pregLastError = \preg_last_error();
+        $message = 'PCRE regex execution error `' . (string)($pcreErrorConstantNames[$pregLastError] ?? $pregLastError)
+            . '`';
+
+        if ($this->debug) {
+            throw new \RuntimeException($message, 1592870147);
+        }
+        \trigger_error($message);
+    }
 }
index 10de9c663809cff98bb14ed43995434e4517fa70..479e6e347df168130e526f21eb386276426139ec 100644 (file)
@@ -16,12 +16,12 @@ abstract class AbstractHtmlProcessor
     /**
      * @var string
      */
-    const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
+    protected const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
 
     /**
      * @var string
      */
-    const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
+    protected 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
@@ -30,7 +30,7 @@ abstract class AbstractHtmlProcessor
      *
      * @see https://bugs.php.net/bug.php?id=73175
      */
-    const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)';
+    protected const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)';
 
     /**
      * @var \DOMDocument|null
@@ -91,10 +91,8 @@ abstract class AbstractHtmlProcessor
      * Sets the HTML to process.
      *
      * @param string $html the HTML to process, must be UTF-8-encoded
-     *
-     * @return void
      */
-    private function setHtml(string $html)
+    private function setHtml(string $html): void
     {
         $this->createUnifiedDomDocument($html);
     }
@@ -124,10 +122,8 @@ abstract class AbstractHtmlProcessor
 
     /**
      * @param \DOMDocument $domDocument
-     *
-     * @return void
      */
-    private function setDomDocument(\DOMDocument $domDocument)
+    private function setDomDocument(\DOMDocument $domDocument): void
     {
         $this->domDocument = $domDocument;
         $this->xPath = new \DOMXPath($this->domDocument);
@@ -188,10 +184,8 @@ abstract class AbstractHtmlProcessor
      * The DOM document will always have a BODY element and a document type.
      *
      * @param string $html
-     *
-     * @return void
      */
-    private function createUnifiedDomDocument(string $html)
+    private function createUnifiedDomDocument(string $html): void
     {
         $this->createRawDomDocument($html);
         $this->ensureExistenceOfBodyElement();
@@ -201,10 +195,8 @@ abstract class AbstractHtmlProcessor
      * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
      *
      * @param string $html
-     *
-     * @return void
      */
-    private function createRawDomDocument(string $html)
+    private function createRawDomDocument(string $html): void
     {
         $domDocument = new \DOMDocument();
         $domDocument->strictErrorChecking = false;
@@ -328,11 +320,9 @@ abstract class AbstractHtmlProcessor
     /**
      * Checks that $this->domDocument has a BODY element and adds it if it is missing.
      *
-     * @return void
-     *
      * @throws \UnexpectedValueException
      */
-    private function ensureExistenceOfBodyElement()
+    private function ensureExistenceOfBodyElement(): void
     {
         if ($this->getDomDocument()->getElementsByTagName('body')->item(0) !== null) {
             return;
index f87307e1dac59fbf99a679f917a90606a535caef..473c5be41712d1789e3582cf95faf1fd2f74706e 100644 (file)
@@ -124,10 +124,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
      *
      * @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)
+    private function mapCssToHtmlAttributes(array $styles, \DOMElement $node): void
     {
         foreach ($styles as $property => $value) {
             // Strip !important indicator
@@ -144,10 +142,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
      * @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)
+    private function mapCssToHtmlAttribute(string $property, string $value, \DOMElement $node): void
     {
         if (!$this->mapSimpleCssProperty($property, $value, $node)) {
             $this->mapComplexCssProperty($property, $value, $node);
@@ -186,10 +182,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
      * @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)
+    private function mapComplexCssProperty(string $property, string $value, \DOMElement $node): void
     {
         switch ($property) {
             case 'background':
@@ -213,10 +207,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
     /**
      * @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)
+    private function mapBackgroundProperty(\DOMElement $node, string $value): void
     {
         // parse out the color, if any
         $styles = \explode(' ', $value, 2);
@@ -233,10 +225,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
      * @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)
+    private function mapWidthOrHeightProperty(\DOMElement $node, string $value, string $property): void
     {
         // only parse values in px and %, but not values like "auto"
         if (!\preg_match('/^(\\d+)(\\.(\\d+))?(px|%)$/', $value)) {
@@ -250,10 +240,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
     /**
      * @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)
+    private function mapMarginProperty(\DOMElement $node, string $value): void
     {
         if (!$this->isTableOrImageNode($node)) {
             return;
@@ -268,10 +256,8 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
     /**
      * @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)
+    private function mapBorderProperty(\DOMElement $node, string $value): void
     {
         if (!$this->isTableOrImageNode($node)) {
             return;
@@ -296,9 +282,7 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
      * 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.
+     *        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)
      */
index 05455d65dbf4e57dce226139a2dfa1cd63062764..5f22b48b8d48ea459c132e7196d16386ca3cab47 100644 (file)
@@ -23,7 +23,7 @@ class HtmlPruner extends AbstractHtmlProcessor
      *
      * @var string
      */
-    const DISPLAY_NONE_MATCHER
+    private 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 "))]';
 
@@ -39,7 +39,6 @@ class HtmlPruner extends AbstractHtmlProcessor
             return $this;
         }
 
-        /** @var \DOMNode $element */
         foreach ($elementsWithStyleDisplayNone as $element) {
             $parentNode = $element->parentNode;
             if ($parentNode !== null) {
@@ -83,10 +82,8 @@ class HtmlPruner extends AbstractHtmlProcessor
      *
      * @param \DOMNodeList $elements
      * @param string[] $classesToKeep
-     *
-     * @return void
      */
-    private function removeClassesFromElements(\DOMNodeList $elements, array $classesToKeep)
+    private function removeClassesFromElements(\DOMNodeList $elements, array $classesToKeep): void
     {
         $classesToKeepIntersector = new ArrayIntersector($classesToKeep);
 
@@ -106,10 +103,8 @@ class HtmlPruner extends AbstractHtmlProcessor
      * Removes the `class` attribute from each element in `$elements`.
      *
      * @param \DOMNodeList $elements
-     *
-     * @return void
      */
-    private function removeClassAttributeFromElements(\DOMNodeList $elements)
+    private function removeClassAttributeFromElements(\DOMNodeList $elements): void
     {
         /** @var \DOMElement $element */
         foreach ($elements as $element) {
index 6450c52e375e67c93ab54bca40b3e9e9db613770..698f9cdd43eed695c1ed279029385a9b137ce7a9 100644 (file)
@@ -61,9 +61,9 @@ class CssConcatenator
      * @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.
+     *        or an empty string if none.
      */
-    public function append(array $selectors, string $declarationsBlock, string $media = '')
+    public function append(array $selectors, string $declarationsBlock, string $media = ''): void
     {
         $selectorsAsKeys = \array_flip($selectors);
 
@@ -96,7 +96,7 @@ class CssConcatenator
 
     /**
      * @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.
+     *        or an empty string if none.
      *
      * @return \stdClass Object with properties as described for elements of `$mediaRules`.
      */
@@ -119,7 +119,7 @@ class CssConcatenator
      * 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.
+     *        significance.
      * @param mixed[] $selectorsAsKeys2 Another such array.
      *
      * @return bool
@@ -146,7 +146,7 @@ class CssConcatenator
 
     /**
      * @param \stdClass $ruleBlock Object with properties as described for elements of the `ruleBlocks` property of
-     *                            elements of `$mediaRules`.
+     *        elements of `$mediaRules`.
      *
      * @return string CSS for the rule block.
      */