Changeset 106563 in spip-zone


Ignore:
Timestamp:
Oct 8, 2017, 3:41:30 PM (23 months ago)
Author:
arnaud.berard@…
Message:
  • mise a jour de la lib emogrifier
Location:
_plugins_/emogrifier/trunk/lib/emogrifier
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • _plugins_/emogrifier/trunk/lib/emogrifier/CHANGELOG.md

    r98616 r106563  
    44This project adheres to [Semantic Versioning](http://semver.org/).
    55
    6 Emogrifier is in a pre-1.0 state. This means that its APIs and behavior are
    7 subject to breaking changes without deprecation notices.
     6
     7## x.y.z (unreleased)
     8
     9### Added
     10- PHP 7.2 support ([#398](https://github.com/jjriv/emogrifier/pull/398))
     11  - Allow PHP 7.2 in composer.json, cleaner PHP version constraint
     12  - Test in Travis for PHP 7.2
     13- Debug mode. Throw debug exceptions only if debug is active.
     14  ([#392](https://github.com/jjriv/emogrifier/pull/392))
    815
    916
    10 ## [1.0.0][] (2015-10-15)
     17### Changed
     18
     19
     20### Deprecated
     21- Support for PHP 5.5 will be removed in Emogrifier 3.0.
     22- Support for PHP 5.4 will be removed in Emogrifier 2.0.
     23
     24
     25### Removed
     26- Drop support for HHVM
     27  ([#386](https://github.com/jjriv/emogrifier/pull/386))
     28
     29
     30### Fixed
     31- Fix media regex parsing
     32  ([#402](https://github.com/MyIntervals/emogrifier/pull/402))
     33- Silence purposefully ignored PHP Warnings
     34  ([#400](https://github.com/MyIntervals/emogrifier/pull/400))
     35
     36
     37### Security
     38
     39
     40
     41## 1.2.0 (2017-03-02)
     42
     43### Added
     44- Handling invalid xPath expression warnings
     45  ([#361](https://github.com/jjriv/emogrifier/pull/361))
     46
     47
     48### Deprecated
     49- Support for PHP 5.5 will be removed in Emogrifier 3.0.
     50- Support for PHP 5.4 will be removed in Emogrifier 2.0.
     51
     52
     53### Fixed
     54- Allow colon (:) and semi-colon (;) when using the *= selector
     55  ([#371](https://github.com/jjriv/emogrifier/pull/371))
     56- Ignore "auto" width and height
     57  ([#365](https://github.com/jjriv/emogrifier/pull/365))
     58
     59
     60
     61## 1.1.0 (2016-09-18)
     62
     63### Added
     64- Add support for PHP 7.1
     65  ([#342](https://github.com/jjriv/emogrifier/pull/342))
     66- Support the attr|=value selector
     67  ([#337](https://github.com/jjriv/emogrifier/pull/337))
     68- Support the attr*=value selector
     69  ([#330](https://github.com/jjriv/emogrifier/pull/330))
     70- Support the attr$=value selector
     71  ([#329](https://github.com/jjriv/emogrifier/pull/329))
     72- Support the attr^=value selector
     73  ([#324](https://github.com/jjriv/emogrifier/pull/324))
     74- Support the attr~=value selector
     75  ([#323](https://github.com/jjriv/emogrifier/pull/323))
     76- Add CSS to HTML attribute mapper
     77  ([#288](https://github.com/jjriv/emogrifier/pull/288))
     78
     79
     80### Changed
     81- Remove composer dependency from PHP mbstring extension
     82  (Actual code dependency were removed a lot of time ago)
     83  ([#295](https://github.com/jjriv/emogrifier/pull/295))
     84
     85
     86### Deprecated
     87- Support for PHP 5.5 will be removed in Emogrifier 3.0.
     88- Support for PHP 5.4 will be removed in Emogrifier 2.0.
     89
     90
     91### Fixed
     92- Method emogrifyBodyContent() doesn't keeps utf8 umlauts
     93  ([#349](https://github.com/jjriv/emogrifier/pull/349))
     94- Ignore value with words more than one in the attribute selector
     95  ([#327](https://github.com/jjriv/emogrifier/pull/327))
     96- Ignore spaces around the > in the direct child selector
     97  ([#322](https://github.com/jjriv/emogrifier/pull/322))
     98- Ignore empty media queries
     99  ([#307](https://github.com/jjriv/emogrifier/pull/307))
     100  ([#237](https://github.com/jjriv/emogrifier/issues/237))
     101- Ignore pseudo-class when combined with pseudo-element
     102  ([#308](https://github.com/jjriv/emogrifier/pull/308))
     103- First-child and last-child selectors are broken
     104  ([#293](https://github.com/jjriv/emogrifier/pull/293))
     105- Second !important rule needs to overwrite the first one
     106  ([#292](https://github.com/jjriv/emogrifier/pull/292))
     107
     108
     109
     110## 1.0.0 (2015-10-15)
    11111
    12112### Added
  • _plugins_/emogrifier/trunk/lib/emogrifier/CONTRIBUTING.md

    r98616 r106563  
    77
    88
     9## Contributor Code of Conduct
     10
     11Please note that this project is released with a
     12[Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this
     13project, you agree to abide by its terms.
     14
     15
    916## General workflow
     17
     18This is the workflow for contributing changes to Emogrifier:
     19
     201. [Fork the Emogrifier Git repository](https://guides.github.com/activities/forking/).
     212. Clone your forked repository and
     22   [install the development dependencies](#install-the-development-dependencies).
     233. Add a local remote "upstream" so you will be able to
     24   [synchronize your fork with the original Emogrifier repository](https://help.github.com/articles/syncing-a-fork/).
     254. Create a local branch for your changes.
     265. [Add unit tests for your changes](#unit-test-your-changes).
     27   These tests should fail without your changes.
     286. Add your changes. Your added unit tests now should pass, and no other tests
     29   should be broken. Check that your changes follow the same
     30   [coding style](#coding-style) as the rest of the project.
     317. Add a changelog entry.
     328. [Commit](#git-commits) and push your changes.
     339. [Create a pull request](https://help.github.com/articles/about-pull-requests/)
     34   for your changes. Check that the Travis build is green. (If it is not, fix the
     35   problems listed by Travis.)
     3610. [Request a review](https://help.github.com/articles/about-pull-request-reviews/)
     37    from @oliverklee.
     3811. Together with him, polish your changes until they are ready to be merged.
     39
     40
     41## About code reviews
    1042
    1143After you have submitted a pull request, the Emogrifier team will review your
     
    6496## Git commits
    6597
    66 Git commits should have a <= 50 character summary, optionally followed by a
     98Commit message should have a <= 50 character summary, optionally followed by a
    6799blank line and a more in depth description of 79 characters per line.
    68100
     
    77109[FEATURE], [TASK], [BUGFIX] OR [CLEANUP]. This makes it faster to see what
    78110a commit is about.
     111
     112
     113## Creating pull requests (PRs)
     114
     115When you create a pull request, please
     116[make your PR editable](https://github.com/blog/2247-improving-collaboration-with-forks).
  • _plugins_/emogrifier/trunk/lib/emogrifier/Classes/Emogrifier.php

    r98616 r106563  
    77 * For more information, please see the README.md file.
    88 *
    9  * @version 1.0.0
     9 * @version 1.2.0
    1010 *
    1111 * @author Cameron Brooks
     
    1313 * @author Oliver Klee <typo3-coding@oliverklee.de>
    1414 * @author Roman Ožana <ozana@omdesign.cz>
     15 * @author Sander Kruger <s.kruger@invessel.com>
    1516 */
    1617class Emogrifier
     
    101102
    102103    /**
    103      * @var array[]
     104     * @var mixed[]
    104105     */
    105106    private $caches = [
     
    122123     * and the attribute names/values as key/value pairs for the inner array
    123124     *
    124      * @var array[]
     125     * @var string[][]
    125126     */
    126127    private $styleAttributesForNodes = [];
     
    153154     */
    154155    private $shouldKeepInvisibleNodes = true;
     156
     157    /**
     158     * @var string[]
     159     */
     160    private $xPathRules = [
     161        // child
     162        '/\\s*>\\s*/'                              => '/',
     163        // adjacent sibling
     164        '/\\s+\\+\\s+/'                            => '/following-sibling::*[1]/self::',
     165        // descendant
     166        '/\\s+(?=.*[^\\]]{1}$)/'                   => '//',
     167        // :first-child
     168        '/([^\\/]+):first-child/i'                 => '*[1]/self::\\1',
     169        // :last-child
     170        '/([^\\/]+):last-child/i'                  => '*[last()]/self::\\1',
     171        // attribute only
     172        '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/'  => '*[@\\1]',
     173        // attribute
     174        '/(\\w)\\[(\\w+)\\]/'                      => '\\1[@\\2]',
     175        // exact attribute
     176        '/(\\w)\\[(\\w+)\\=[\'"]?([\\w\\s]+)[\'"]?\\]/' => '\\1[@\\2="\\3"]',
     177        // element attribute~=
     178        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\~\\=[\\s]*[\'"]?([\\w-_\\/]+)[\'"]?\\]/'
     179            => '\\1[contains(concat(" ", @\\2, " "), concat(" ", "\\3", " "))]',
     180        // element attribute^=
     181        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\^\\=[\\s]*[\'"]?([\\w-_\\/]+)[\'"]?\\]/' => '\\1[starts-with(@\\2, "\\3")]',
     182        // element attribute*=
     183        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\*\\=[\\s]*[\'"]?([\\w-_\\s\\/:;]+)[\'"]?\\]/' => '\\1[contains(@\\2, "\\3")]',
     184        // element attribute$=
     185        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\$\\=[\\s]*[\'"]?([\\w-_\\s\\/]+)[\'"]?\\]/'
     186            => '\\1[substring(@\\2, string-length(@\\2) - string-length("\\3") + 1) = "\\3"]',
     187        // element attribute|=
     188        '/([\\w\\*]+)\\[(\\w+)[\\s]*\\|\\=[\\s]*[\'"]?([\\w-_\\s\\/]+)[\'"]?\\]/'
     189            => '\\1[@\\2="\\3" or starts-with(@\\2, concat("\\3", "-"))]',
     190    ];
     191
     192    /**
     193     * Determines whether CSS styles that have an equivalent HTML attribute
     194     * should be mapped and attached to those elements.
     195     *
     196     * @var bool
     197     */
     198    private $shouldMapCssToHtml = false;
     199
     200    /**
     201     * This multi-level array contains simple mappings of CSS properties to
     202     * HTML attributes. If a mapping only applies to certain HTML nodes or
     203     * only for certain values, the mapping is an object with a whitelist
     204     * of nodes and values.
     205     *
     206     * @var mixed[][]
     207     */
     208    private $cssToHtmlMap = [
     209        'background-color' => [
     210            'attribute' => 'bgcolor',
     211        ],
     212        'text-align' => [
     213            'attribute' => 'align',
     214            'nodes' => ['p', 'div', 'td'],
     215            'values' => ['left', 'right', 'center', 'justify'],
     216        ],
     217        'float' => [
     218            'attribute' => 'align',
     219            'nodes' => ['table', 'img'],
     220            'values' => ['left', 'right'],
     221        ],
     222        'border-spacing' => [
     223            'attribute' => 'cellspacing',
     224            'nodes' => ['table'],
     225        ],
     226    ];
     227
     228    /**
     229     * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them.
     230     *
     231     * @var bool
     232     */
     233    private $debug = false;
    155234
    156235    /**
     
    244323        }
    245324
    246         return $innerDocument->saveHTML();
     325        return html_entity_decode($innerDocument->saveHTML());
    247326    }
    248327
     
    255334     *
    256335     * @return void
     336     *
     337     * @throws \InvalidArgumentException
    257338     */
    258339    protected function process(\DOMDocument $xmlDocument)
    259340    {
    260         $xpath = new \DOMXPath($xmlDocument);
     341        $xPath = new \DOMXPath($xmlDocument);
    261342        $this->clearAllCaches();
    262343
     
    267348        $this->purgeVisitedNodes();
    268349
    269         $nodesWithStyleAttributes = $xpath->query('//*[@style]');
     350        set_error_handler([$this, 'handleXpathError'], E_WARNING);
     351
     352        $nodesWithStyleAttributes = $xPath->query('//*[@style]');
    270353        if ($nodesWithStyleAttributes !== false) {
    271354            /** @var \DOMElement $node */
     
    284367
    285368        if ($this->isStyleBlocksParsingEnabled) {
    286             $allCss .= $this->getCssFromAllStyleNodes($xpath);
     369            $allCss .= $this->getCssFromAllStyleNodes($xPath);
    287370        }
    288371
    289372        $cssParts = $this->splitCssAndMediaQuery($allCss);
    290         $excludedNodes = $this->getNodesToExclude($xpath);
     373        $excludedNodes = $this->getNodesToExclude($xPath);
    291374        $cssRules = $this->parseCssRules($cssParts['css']);
    292375        foreach ($cssRules as $cssRule) {
    293             // query the body for the xpath selector
    294             $nodesMatchingCssSelectors = $xpath->query($this->translateCssToXpath($cssRule['selector']));
    295             // ignore invalid selectors
    296             if ($nodesMatchingCssSelectors === false) {
     376            // There's no real way to test "PHP Warning" output generated by the following XPath query unless PHPUnit
     377            // converts it to an exception. Unfortunately, this would only apply to tests and not work for production
     378            // executions, which can still flood logs/output unnecessarily. Instead, Emogrifier's error handler should
     379            // always throw an exception and it must be caught here and only rethrown if in debug mode.
     380            try {
     381                // \DOMXPath::query will always return a DOMNodeList or an exception when errors are caught.
     382                $nodesMatchingCssSelectors = $xPath->query($this->translateCssToXpath($cssRule['selector']));
     383            } catch (\InvalidArgumentException $e) {
     384                if ($this->debug) {
     385                    throw ($e);
     386                }
    297387                continue;
    298388            }
     
    312402                }
    313403                $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']);
     404                if ($this->shouldMapCssToHtml) {
     405                    $this->mapCssToHtmlAttributes($newStyleDeclarations, $node);
     406                }
    314407                $node->setAttribute(
    315408                    'style',
     
    319412        }
    320413
     414        restore_error_handler();
     415
    321416        if ($this->isInlineStyleAttributesParsingEnabled) {
    322417            $this->fillStyleAttributesWithMergedStyles();
     
    324419
    325420        if ($this->shouldKeepInvisibleNodes) {
    326             $this->removeInvisibleNodes($xpath);
    327         }
    328 
    329         $this->copyCssWithMediaToStyleNode($xmlDocument, $xpath, $cssParts['media']);
     421            $this->removeInvisibleNodes($xPath);
     422        }
     423
     424        $this->copyCssWithMediaToStyleNode($xmlDocument, $xPath, $cssParts['media']);
     425    }
     426
     427    /**
     428     * Applies $styles to $node.
     429     *
     430     * This method maps CSS styles to HTML attributes and adds those to the
     431     * node.
     432     *
     433     * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
     434     * @param \DOMNode $node   node to apply styles to
     435     *
     436     * @return void
     437     */
     438    private function mapCssToHtmlAttributes(array $styles, \DOMNode $node)
     439    {
     440        foreach ($styles as $property => $value) {
     441            // Strip !important indicator
     442            $value = trim(str_replace('!important', '', $value));
     443            $this->mapCssToHtmlAttribute($property, $value, $node);
     444        }
     445    }
     446
     447    /**
     448     * Tries to apply the CSS style to $node as an attribute.
     449     *
     450     * This method maps a CSS rule to HTML attributes and adds those to the node.
     451     *
     452     * @param string $property the name of the CSS property to map
     453     * @param string $value    the value of the style rule to map
     454     * @param \DOMNode $node   node to apply styles to
     455     *
     456     * @return void
     457     */
     458    private function mapCssToHtmlAttribute($property, $value, \DOMNode $node)
     459    {
     460        if (!$this->mapSimpleCssProperty($property, $value, $node)) {
     461            $this->mapComplexCssProperty($property, $value, $node);
     462        }
     463    }
     464
     465    /**
     466     * Looks up the CSS property in the mapping table and maps it if it matches the conditions.
     467     *
     468     * @param string $property the name of the CSS property to map
     469     * @param string $value    the value of the style rule to map
     470     * @param \DOMNode $node   node to apply styles to
     471     *
     472     * @return bool true if the property cab be mapped using the simple mapping table
     473     */
     474    private function mapSimpleCssProperty($property, $value, \DOMNode $node)
     475    {
     476        if (!isset($this->cssToHtmlMap[$property])) {
     477            return false;
     478        }
     479
     480        $mapping = $this->cssToHtmlMap[$property];
     481        $nodesMatch = !isset($mapping['nodes']) || in_array($node->nodeName, $mapping['nodes'], true);
     482        $valuesMatch = !isset($mapping['values']) || in_array($value, $mapping['values'], true);
     483        if (!$nodesMatch || !$valuesMatch) {
     484            return false;
     485        }
     486
     487        $node->setAttribute($mapping['attribute'], $value);
     488
     489        return true;
     490    }
     491
     492    /**
     493     * Maps CSS properties that need special transformation to an HTML attribute.
     494     *
     495     * @param string $property the name of the CSS property to map
     496     * @param string $value    the value of the style rule to map
     497     * @param \DOMNode $node   node to apply styles to
     498     *
     499     * @return void
     500     */
     501    private function mapComplexCssProperty($property, $value, \DOMNode $node)
     502    {
     503        $nodeName = $node->nodeName;
     504        $isTable = $nodeName === 'table';
     505        $isImage = $nodeName === 'img';
     506        $isTableOrImage = $isTable || $isImage;
     507
     508        switch ($property) {
     509            case 'background':
     510                // Parse out the color, if any
     511                $styles = explode(' ', $value);
     512                $first = $styles[0];
     513                if (!is_numeric(substr($first, 0, 1)) && substr($first, 0, 3) !== 'url') {
     514                    // This is not a position or image, assume it's a color
     515                    $node->setAttribute('bgcolor', $first);
     516                }
     517                break;
     518            case 'width':
     519                // intentional fall-through
     520            case 'height':
     521                // Only parse values in px and %, but not values like "auto".
     522                if (preg_match('/^\d+(px|%)$/', $value)) {
     523                    // Remove 'px'. This regex only conserves numbers and %
     524                    $number = preg_replace('/[^0-9.%]/', '', $value);
     525                    $node->setAttribute($property, $number);
     526                }
     527                break;
     528            case 'margin':
     529                if ($isTableOrImage) {
     530                    $margins = $this->parseCssShorthandValue($value);
     531                    if ($margins['left'] === 'auto' && $margins['right'] === 'auto') {
     532                        $node->setAttribute('align', 'center');
     533                    }
     534                }
     535                break;
     536            case 'border':
     537                if ($isTableOrImage) {
     538                    if ($value === 'none' || $value === '0') {
     539                        $node->setAttribute('border', '0');
     540                    }
     541                }
     542                break;
     543            default:
     544        }
     545    }
     546
     547    /**
     548     * Parses a shorthand CSS value and splits it into individual values
     549     *
     550     * @param string $value a string of CSS value with 1, 2, 3 or 4 sizes
     551     *                      For example: padding: 0 auto;
     552     *                      '0 auto' is split into top: 0, left: auto, bottom: 0,
     553     *                      right: auto.
     554     *
     555     * @return string[] an array of values for top, right, bottom and left (using these as associative array keys)
     556     */
     557    private function parseCssShorthandValue($value)
     558    {
     559        $values = preg_split('/\\s+/', $value);
     560
     561        $css = [];
     562        $css['top'] = $values[0];
     563        $css['right'] = (count($values) > 1) ? $values[1] : $css['top'];
     564        $css['bottom'] = (count($values) > 2) ? $values[2] : $css['top'];
     565        $css['left'] = (count($values) > 3) ? $values[3] : $css['right'];
     566
     567        return $css;
    330568    }
    331569
     
    360598                    // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
    361599                    // only allow structural pseudo-classes
    362                     if (strpos($selector, ':') !== false && !preg_match('/:\\S+\\-(child|type\\()/i', $selector)) {
     600                    $hasPseudoElement = strpos($selector, '::') !== false;
     601                    $hasAnyPseudoClass = (bool) preg_match('/:[a-zA-Z]/', $selector);
     602                    $hasSupportedPseudoClass = (bool) preg_match('/:\\S+\\-(child|type\\()/i', $selector);
     603                    if ($hasPseudoElement || ($hasAnyPseudoClass && !$hasSupportedPseudoClass)) {
    363604                        continue;
    364605                    }
     
    409650    {
    410651        $this->shouldKeepInvisibleNodes = false;
     652    }
     653
     654    /**
     655     * Enables the attachment/override of HTML attributes for which a
     656     * corresponding CSS property has been set.
     657     *
     658     * @return void
     659     */
     660    public function enableCssToHtmlMapping()
     661    {
     662        $this->shouldMapCssToHtml = true;
    411663    }
    412664
     
    555807     * to lowercase.
    556808     *
    557      * @param \DOMXPath $xpath
    558      *
    559      * @return void
    560      */
    561     private function removeInvisibleNodes(\DOMXPath $xpath)
    562     {
    563         $nodesWithStyleDisplayNone = $xpath->query(
     809     * @param \DOMXPath $xPath
     810     *
     811     * @return void
     812     */
     813    private function removeInvisibleNodes(\DOMXPath $xPath)
     814    {
     815        $nodesWithStyleDisplayNone = $xPath->query(
    564816            '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'
    565817        );
     
    646898
    647899        foreach ($oldStyles as $attributeName => $attributeValue) {
    648             if (isset($newStyles[$attributeName]) && strtolower(substr($attributeValue, -10)) === '!important') {
     900            if (!isset($newStyles[$attributeName])) {
     901                continue;
     902            }
     903
     904            $newAttributeValue = $newStyles[$attributeName];
     905            if ($this->attributeValueIsImportant($attributeValue)
     906                && !$this->attributeValueIsImportant($newAttributeValue)
     907            ) {
    649908                $combinedStyles[$attributeName] = $attributeValue;
    650909            }
     
    663922
    664923    /**
     924     * Checks whether $attributeValue is marked as !important.
     925     *
     926     * @param string $attributeValue
     927     *
     928     * @return bool
     929     */
     930    private function attributeValueIsImportant($attributeValue)
     931    {
     932        return strtolower(substr(trim($attributeValue), -10)) === '!important';
     933    }
     934
     935    /**
    665936     * Applies $css to $xmlDocument, limited to the media queries that actually apply to the document.
    666937     *
    667938     * @param \DOMDocument $xmlDocument the document to match against
    668      * @param \DOMXPath $xpath
     939     * @param \DOMXPath $xPath
    669940     * @param string $css a string of CSS
    670941     *
    671942     * @return void
    672943     */
    673     private function copyCssWithMediaToStyleNode(\DOMDocument $xmlDocument, \DOMXPath $xpath, $css)
     944    private function copyCssWithMediaToStyleNode(\DOMDocument $xmlDocument, \DOMXPath $xPath, $css)
    674945    {
    675946        if ($css === '') {
     
    681952        foreach ($this->extractMediaQueriesFromCss($css) as $mediaQuery) {
    682953            foreach ($this->parseCssRules($mediaQuery['css']) as $selector) {
    683                 if ($this->existsMatchForCssSelector($xpath, $selector['selector'])) {
     954                if ($this->existsMatchForCssSelector($xPath, $selector['selector'])) {
    684955                    $mediaQueriesRelevantForDocument[] = $mediaQuery['query'];
    685956                    break;
     
    692963
    693964    /**
    694      * Extracts the media queries from $css.
     965     * Extracts the media queries from $css while skipping empty media queries.
    695966     *
    696967     * @param string $css
     
    700971    private function extractMediaQueriesFromCss($css)
    701972    {
    702         preg_match_all('#(?<query>@media[^{]*\\{(?<css>(.*?)\\})(\\s*)\\})#s', $css, $mediaQueries);
    703         $result = [];
    704         foreach (array_keys($mediaQueries['css']) as $key) {
    705             $result[] = [
    706                 'css' => $mediaQueries['css'][$key],
    707                 'query' => $mediaQueries['query'][$key],
    708             ];
    709         }
    710         return $result;
     973        preg_match_all('/@media\\b[^{]*({((?:[^{}]+|(?1))*)})/', $css, $rawMediaQueries, PREG_SET_ORDER);
     974        $parsedQueries = [];
     975
     976        foreach ($rawMediaQueries as $mediaQuery) {
     977            if ($mediaQuery[2] !== '') {
     978                $parsedQueries[] = [
     979                    'css'   => $mediaQuery[2],
     980                    'query' => $mediaQuery[0],
     981                ];
     982            }
     983        }
     984
     985        return $parsedQueries;
    711986    }
    712987
     
    714989     * Checks whether there is at least one matching element for $cssSelector.
    715990     *
    716      * @param \DOMXPath $xpath
     991     * @param \DOMXPath $xPath
    717992     * @param string $cssSelector
    718993     *
    719994     * @return bool
    720995     */
    721     private function existsMatchForCssSelector(\DOMXPath $xpath, $cssSelector)
    722     {
    723         $nodesMatchingSelector = $xpath->query($this->translateCssToXpath($cssSelector));
     996    private function existsMatchForCssSelector(\DOMXPath $xPath, $cssSelector)
     997    {
     998        $nodesMatchingSelector = $xPath->query($this->translateCssToXpath($cssSelector));
    724999
    7251000        return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
     
    7291004     * Returns CSS content.
    7301005     *
    731      * @param \DOMXPath $xpath
     1006     * @param \DOMXPath $xPath
    7321007     *
    7331008     * @return string
    7341009     */
    735     private function getCssFromAllStyleNodes(\DOMXPath $xpath)
    736     {
    737         $styleNodes = $xpath->query('//style');
     1010    private function getCssFromAllStyleNodes(\DOMXPath $xPath)
     1011    {
     1012        $styleNodes = $xPath->query('//style');
    7381013
    7391014        if ($styleNodes === false) {
     
    8261101        $media = '';
    8271102        $cssForAllowedMediaTypes = preg_replace_callback(
    828             '#@media\\s+(?:only\\s)?(?:[\\s{\\(]' . $mediaTypesExpression . ')\\s?[^{]+{.*}\\s*}\\s*#misU',
     1103            '#@media\\s+(?:only\\s)?(?:[\\s{\\(]\\s*' . $mediaTypesExpression . ')\\s*[^{]*+{.*}\\s*}\\s*#misU',
    8291104            function ($matches) use (&$media) {
    8301105                $media .= $matches[0];
     
    9311206        if ($hasContentTypeMetaTag) {
    9321207            return $html;
    933 
    9341208        }
    9351209
     
    10211295        );
    10221296        $trimmedLowercaseSelector = trim($lowercasePaddedSelector);
    1023         $xpathKey = md5($trimmedLowercaseSelector);
    1024         if (!isset($this->caches[self::CACHE_KEY_XPATH][$xpathKey])) {
    1025             $cssSelectorMatches = [
    1026                 'child'            => '/\\s+>\\s+/',
    1027                 'adjacent sibling' => '/\\s+\\+\\s+/',
    1028                 'descendant'       => '/\\s+/',
    1029                 ':first-child'     => '/([^\\/]+):first-child/i',
    1030                 ':last-child'      => '/([^\\/]+):last-child/i',
    1031                 'attribute only'   => '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/',
    1032                 'attribute'        => '/(\\w)\\[(\\w+)\\]/',
    1033                 'exact attribute'  => '/(\\w)\\[(\\w+)\\=[\'"]?(\\w+)[\'"]?\\]/',
    1034             ];
    1035             $xPathReplacements = [
    1036                 'child'            => '/',
    1037                 'adjacent sibling' => '/following-sibling::*[1]/self::',
    1038                 'descendant'       => '//',
    1039                 ':first-child'     => '\\1/*[1]',
    1040                 ':last-child'      => '\\1/*[last()]',
    1041                 'attribute only'   => '*[@\\1]',
    1042                 'attribute'        => '\\1[@\\2]',
    1043                 'exact attribute'  => '\\1[@\\2="\\3"]',
    1044             ];
    1045 
    1046             $roughXpath = '//' . preg_replace($cssSelectorMatches, $xPathReplacements, $trimmedLowercaseSelector);
    1047 
    1048             $xpathWithIdAttributeMatchers = preg_replace_callback(
     1297        $xPathKey = md5($trimmedLowercaseSelector);
     1298        if (!isset($this->caches[self::CACHE_KEY_XPATH][$xPathKey])) {
     1299            $roughXpath = '//' . preg_replace(
     1300                array_keys($this->xPathRules),
     1301                $this->xPathRules,
     1302                $trimmedLowercaseSelector
     1303            );
     1304            $xPathWithIdAttributeMatchers = preg_replace_callback(
    10491305                self::ID_ATTRIBUTE_MATCHER,
    10501306                [$this, 'matchIdAttributes'],
    10511307                $roughXpath
    10521308            );
    1053             $xpathWithIdAttributeAndClassMatchers = preg_replace_callback(
     1309            $xPathWithIdAttributeAndClassMatchers = preg_replace_callback(
    10541310                self::CLASS_ATTRIBUTE_MATCHER,
    10551311                [$this, 'matchClassAttributes'],
    1056                 $xpathWithIdAttributeMatchers
     1312                $xPathWithIdAttributeMatchers
    10571313            );
    10581314
    10591315            // Advanced selectors are going to require a bit more advanced emogrification.
    10601316            // When we required PHP 5.3, we could do this with closures.
    1061             $xpathWithIdAttributeAndClassMatchers = preg_replace_callback(
     1317            $xPathWithIdAttributeAndClassMatchers = preg_replace_callback(
    10621318                '/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
    10631319                [$this, 'translateNthChild'],
    1064                 $xpathWithIdAttributeAndClassMatchers
     1320                $xPathWithIdAttributeAndClassMatchers
    10651321            );
    10661322            $finalXpath = preg_replace_callback(
    10671323                '/([^\\/]+):nth-of-type\\(\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
    10681324                [$this, 'translateNthOfType'],
    1069                 $xpathWithIdAttributeAndClassMatchers
     1325                $xPathWithIdAttributeAndClassMatchers
    10701326            );
    10711327
    1072             $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey] = $finalXpath;
    1073         }
    1074         return $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey];
     1328            $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey] = $finalXpath;
     1329        }
     1330        return $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey];
    10751331    }
    10761332
     
    11721428    private function parseNth(array $match)
    11731429    {
    1174         if (in_array(strtolower($match[2]), ['even','odd'], true)) {
     1430        if (in_array(strtolower($match[2]), ['even', 'odd'], true)) {
    11751431            // we have "even" or "odd"
    11761432            $index = strtolower($match[2]) === 'even' ? 0 : 1;
     
    12541510     * Find the nodes that are not to be emogrified.
    12551511     *
    1256      * @param \DOMXPath $xpath
     1512     * @param \DOMXPath $xPath
    12571513     *
    12581514     * @return \DOMElement[]
    12591515     */
    1260     private function getNodesToExclude(\DOMXPath $xpath)
     1516    private function getNodesToExclude(\DOMXPath $xPath)
    12611517    {
    12621518        $excludedNodes = [];
    12631519        foreach (array_keys($this->excludedSelectors) as $selectorToExclude) {
    1264             foreach ($xpath->query($this->translateCssToXpath($selectorToExclude)) as $node) {
     1520            foreach ($xPath->query($this->translateCssToXpath($selectorToExclude)) as $node) {
    12651521                $excludedNodes[] = $node;
    12661522            }
     
    12691525        return $excludedNodes;
    12701526    }
     1527
     1528    /**
     1529     * Handles invalid xPath expression warnings, generated by process() method,
     1530     * during querying \DOMDocument and trigger \InvalidArgumentException
     1531     * with invalid selector.
     1532     *
     1533     * @param int $type
     1534     * @param string $message
     1535     * @param string $file
     1536     * @param int $line
     1537     * @param array $context
     1538     *
     1539     * @return bool always false
     1540     *
     1541     * @throws \InvalidArgumentException
     1542     */
     1543    public function handleXpathError($type, $message, $file, $line, array $context)
     1544    {
     1545        // Do not tie this conditional to debug mode as it causes unnecessary output. See further explanation in
     1546        // ::process() where this exception is caught.
     1547        if ($type === E_WARNING && isset($context['cssRule']['selector'])) {
     1548            throw new \InvalidArgumentException(
     1549                sprintf(
     1550                    '%s in selector >> %s << in %s on line %s',
     1551                    $message,
     1552                    $context['cssRule']['selector'],
     1553                    $file,
     1554                    $line
     1555                )
     1556            );
     1557        }
     1558
     1559        // the normal error handling continues when handler return false
     1560        return false;
     1561    }
     1562
     1563    /**
     1564     * Sets the debug mode.
     1565     *
     1566     * @param bool $debug set to true to enable debug mode
     1567     *
     1568     * @return void
     1569     */
     1570    public function setDebug($debug)
     1571    {
     1572        $this->debug = $debug;
     1573    }
    12711574}
  • _plugins_/emogrifier/trunk/lib/emogrifier/Configuration/PhpCodeSniffer/Standards/Emogrifier/ruleset.xml

    r98616 r106563  
    11<?xml version="1.0" encoding="UTF-8"?>
    2 <ruleset name="PPW Coding Standard">
     2<ruleset name="Emogrifier Coding Standard">
    33    <description>This is the coding standard used for the Emogrifier code.
    44        This standard has been tested with to work with PHP_CodeSniffer >= 2.3.0.
     
    3737    <rule ref="Squiz.Commenting.FunctionCommentThrowTag"/>
    3838    <rule ref="Squiz.Commenting.PostStatementComment"/>
    39     <rule ref="TYPO3SniffPool.Commenting.ClassComment"/>
    40     <rule ref="TYPO3SniffPool.Commenting.DoubleSlashCommentsInNewLine"/>
    41     <rule ref="TYPO3SniffPool.Commenting.SpaceAfterDoubleSlash"/>
    4239
    4340    <!-- Control structures -->
    4441    <rule ref="PEAR.ControlStructures.ControlSignature"/>
    45     <rule ref="TYPO3SniffPool.ControlStructures.DisallowEachInLoopCondition"/>
    46     <rule ref="TYPO3SniffPool.ControlStructures.DisallowElseIfConstruct"/>
    47     <rule ref="TYPO3SniffPool.ControlStructures.ExtraBracesByAssignmentInLoop"/>
    48     <rule ref="TYPO3SniffPool.ControlStructures.SwitchDeclaration"/>
    49     <rule ref="TYPO3SniffPool.ControlStructures.TernaryConditionalOperator"/>
    50     <rule ref="TYPO3SniffPool.ControlStructures.UnusedVariableInForEachLoop"/>
    5142
    5243    <!-- Debug -->
    5344    <rule ref="Generic.Debug.ClosureLinter"/>
    54     <rule ref="TYPO3SniffPool.Debug.DebugCode"/>
    5545
    5646    <!-- Files -->
    5747    <rule ref="Generic.Files.OneClassPerFile"/>
    5848    <rule ref="Generic.Files.OneInterfacePerFile"/>
    59     <rule ref="TYPO3SniffPool.Files.FileExtension"/>
    60     <rule ref="TYPO3SniffPool.Files.Filename"/>
    61     <rule ref="TYPO3SniffPool.Files.IncludingFile"/>
    6249    <rule ref="Zend.Files.ClosingTag"/>
    6350
     
    7966    <rule ref="Generic.NamingConventions.ConstructorName"/>
    8067    <rule ref="PEAR.NamingConventions.ValidClassName"/>
    81     <rule ref="TYPO3SniffPool.NamingConventions.ValidFunctionName"/>
    82     <rule ref="TYPO3SniffPool.NamingConventions.ValidVariableName"/>
    8368
    8469    <!-- Objects -->
     
    11499    <rule ref="Squiz.Scope.MemberVarScope"/>
    115100    <rule ref="Squiz.Scope.StaticThisUsage"/>
    116     <rule ref="TYPO3SniffPool.Scope.AlwaysReturn">
    117         <exclude-pattern>*/Tests/*</exclude-pattern>
    118     </rule>
    119101
    120102    <!--Strings-->
    121103    <rule ref="Squiz.Strings.DoubleQuoteUsage"/>
    122     <rule ref="TYPO3SniffPool.Strings.ConcatenationSpacing"/>
    123     <rule ref="TYPO3SniffPool.Strings.UnnecessaryStringConcat"/>
    124104
    125105    <!-- Whitespace -->
     
    131111    <rule ref="Squiz.WhiteSpace.PropertyLabelSpacing"/>
    132112    <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
    133     <rule ref="TYPO3SniffPool.WhiteSpace.NoWhitespaceAtInDecrement"/>
    134     <rule ref="TYPO3SniffPool.WhiteSpace.ScopeClosingBrace"/>
    135     <rule ref="TYPO3SniffPool.WhiteSpace.WhitespaceAfterCommentSigns"/>
    136113</ruleset>
  • _plugins_/emogrifier/trunk/lib/emogrifier/README.md

    r98616 r106563  
    11# Emogrifier
    22
    3 [![Build Status](https://travis-ci.org/jjriv/emogrifier.svg?branch=master)](https://travis-ci.org/jjriv/emogrifier)
     3[![Build Status](https://travis-ci.org/MyIntervals/emogrifier.svg?branch=master)](https://travis-ci.org/MyIntervals/emogrifier)
    44[![Latest Stable Version](https://poser.pugx.org/pelago/emogrifier/v/stable.svg)](https://packagist.org/packages/pelago/emogrifier)
    55[![Total Downloads](https://poser.pugx.org/pelago/emogrifier/downloads.svg)](https://packagist.org/packages/pelago/emogrifier)
     
    2929
    3030- [How it works](#how-it-works)
     31- [Installation](#installation)
    3132- [Usage](#usage)
     33- [Options](#options)
    3234- [Installing with Composer](#installing-with-composer)
    33 - [Usage](#usage)
    3435- [Supported CSS selectors](#supported-css-selectors)
    3536- [Caveats](#caveats)
    3637- [Maintainer](#maintainer)
    37 - [Contributing](#contributing)
    3838
    3939
     
    4343inserting your CSS definitions into tags within your HTML based on your CSS
    4444selectors.
     45
     46
     47## Installation
     48
     49For installing emogrifier, either add pelago/emogrifier to your
     50project's composer.json, or you can use composer as below:
     51
     52```
     53composer require pelago/emogrifier
     54```
    4555
    4656
     
    8898  from the HTML. If you want to disable this functionality so that Emogrifier
    8999  leaves these `<style>` blocks in the HTML and does not parse them, you should
    90   use this option.
     100  use this option. If you use this option, the contents of the `<style>` blocks
     101  will _not_ be applied as inline styles and any CSS you want Emogrifier to
     102  use must be passed in as described in the [Usage section](#usage) above.
    91103* `$emogrifier->disableInlineStylesParsing()` - By default, Emogrifier
    92104  preserves all of the "style" attributes on tags in the HTML you pass to it.
     
    103115* `$emogrifier->addExcludedSelector(string $selector)` - Keeps elements from
    104116  being affected by emogrification.
    105 
    106 
    107 ## Requirements
    108 
    109 * PHP from 5.4 to 7.0 (with the mbstring extension)
    110 * or HHVM
     117* `$emogrifier->enableCssToHtmlMapping()` - Some email clients don't support CSS
     118  well, even if inline and prefer HTML attributes. This function allows you to
     119  put properties such as height, width, background color and font color in your
     120  CSS while the transformed content will have all the available HTML tags set.
    111121
    112122
     
    149159 * attribute presence
    150160 * attribute value
     161 * attribute value with |
     162 * attribute value with ~
     163 * attribute value with ^
     164 * attribute value with *
     165 * attribute value with $
    151166 * attribute only
    152167 * first-child
     
    156171
    157172 * universal
     173 * pseudo-elements (will never be supported)
    158174
    159175
     
    193209
    194210
    195 ## Maintainer
    196 
    197 Emogrifier is maintained by the good people at
    198 [Pelago](http://www.pelagodesign.com/), info AT pelagodesign DOT com.
     211## Maintainers
     212
     213* [Oliver Klee](https://github.com/oliverklee)
     214* [John Reeve](https://github.com/jjriv)
  • _plugins_/emogrifier/trunk/lib/emogrifier/Tests/Unit/EmogrifierTest.php

    r98616 r106563  
    104104        $this->subject->setHtml('<p>Hello</p>');
    105105
    106         $emogrifiedHtml = $this->subject->emogrify();
    107 
    108         self::assertContains(
    109             '<html>',
    110             $emogrifiedHtml
    111         );
     106        $result = $this->subject->emogrify();
     107
     108        self::assertContains('<html>', $result);
    112109    }
    113110
     
    119116        $this->subject->setHtml('<head><title>Hello</title></head><p>World</p>');
    120117
    121         $emogrifiedHtml = $this->subject->emogrify();
    122 
    123         self::assertContains(
    124             '<html>',
    125             $emogrifiedHtml
    126         );
     118        $result = $this->subject->emogrify();
     119
     120        self::assertContains('<html>', $result);
    127121    }
    128122
     
    134128        $this->subject->setHtml('<p>Hello</p>');
    135129
    136         $emogrifiedHtml = $this->subject->emogrify();
    137 
    138         self::assertContains(
    139             '<head>',
    140             $emogrifiedHtml
    141         );
     130        $result = $this->subject->emogrify();
     131
     132        self::assertContains('<head>', $result);
    142133    }
    143134
     
    149140        $this->subject->setHtml('<html></head><p>World</p></html>');
    150141
    151         $emogrifiedHtml = $this->subject->emogrify();
    152 
    153         self::assertContains(
    154             '<head>',
    155             $emogrifiedHtml
    156         );
     142        $result = $this->subject->emogrify();
     143
     144        self::assertContains('<head>', $result);
    157145    }
    158146
     
    163151    {
    164152        $templateMarker = '$[USER:NAME]$';
    165 
    166         $html = $this->html5DocumentType . '<html><p>' . $templateMarker . '</p></html>';
     153        $html = '<html><p>' . $templateMarker . '</p></html>';
    167154        $this->subject->setHtml($html);
    168155
    169         self::assertContains(
    170             $templateMarker,
    171             $this->subject->emogrify()
    172         );
     156        $result = $this->subject->emogrify();
     157
     158        self::assertContains($templateMarker, $result);
    173159    }
    174160
     
    179165    {
    180166        $umlautString = 'Küss die Hand, schöne Frau.';
    181 
    182167        $html = $this->html5DocumentType . '<html><p>' . $umlautString . '</p></html>';
    183168        $this->subject->setHtml($html);
    184169
    185         self::assertContains(
    186             $umlautString,
    187             $this->subject->emogrify()
    188         );
     170        $result = $this->subject->emogrify();
     171
     172        self::assertContains($umlautString, $result);
    189173    }
    190174
     
    195179    {
    196180        $umlautString = 'Öösel läks õunu täis ämber uhkelt ümber.';
    197 
    198181        $html = $this->xhtml1StrictDocumentType . '<html<p>' . $umlautString . '</p></html>';
    199182        $this->subject->setHtml($html);
    200183
    201         self::assertContains(
    202             $umlautString,
    203             $this->subject->emogrify()
    204         );
     184        $result = $this->subject->emogrify();
     185
     186        self::assertContains($umlautString, $result);
    205187    }
    206188
     
    211193    {
    212194        $umlautString = 'Öösel läks õunu täis ämber uhkelt ümber.';
    213 
    214195        $html = $this->html4TransitionalDocumentType . '<html><p>' . $umlautString . '</p></html>';
    215196        $this->subject->setHtml($html);
    216197
    217         self::assertContains(
    218             $umlautString,
    219             $umlautString,
    220             $this->subject->emogrify()
    221         );
     198        $result = $this->subject->emogrify();
     199
     200        self::assertContains($umlautString, $result);
    222201    }
    223202
     
    228207    {
    229208        $entityString = 'a &amp; b &gt; c';
    230 
     209        $html = '<html><p>' . $entityString . '</p></html>';
     210        $this->subject->setHtml($html);
     211
     212        $result = $this->subject->emogrify();
     213
     214        self::assertContains($entityString, $result);
     215    }
     216
     217    /**
     218     * @test
     219     */
     220    public function emogrifyKeepsHtmlEntitiesInXhtml()
     221    {
     222        $entityString = 'a &amp; b &gt; c';
     223        $html = $this->xhtml1StrictDocumentType . '<html<p>' . $entityString . '</p></html>';
     224        $this->subject->setHtml($html);
     225
     226        $result = $this->subject->emogrify();
     227
     228        self::assertContains($entityString, $result);
     229    }
     230
     231    /**
     232     * @test
     233     */
     234    public function emogrifyKeepsHtmlEntitiesInHtml4()
     235    {
     236        $entityString = 'a &amp; b &gt; c';
    231237        $html = $this->html5DocumentType . '<html><p>' . $entityString . '</p></html>';
    232238        $this->subject->setHtml($html);
    233239
    234         self::assertContains(
    235             $entityString,
    236             $this->subject->emogrify()
    237         );
    238     }
    239 
    240     /**
    241      * @test
    242      */
    243     public function emogrifyKeepsHtmlEntitiesInXhtml()
    244     {
    245         $entityString = 'a &amp; b &gt; c';
    246 
    247         $html = $this->xhtml1StrictDocumentType . '<html<p>' . $entityString . '</p></html>';
    248         $this->subject->setHtml($html);
    249 
    250         self::assertContains(
    251             $entityString,
    252             $this->subject->emogrify()
    253         );
    254     }
    255 
    256     /**
    257      * @test
    258      */
    259     public function emogrifyKeepsHtmlEntitiesInHtml4()
    260     {
    261         $entityString = 'a &amp; b &gt; c';
    262 
    263         $html = $this->html4TransitionalDocumentType . '<html><p>' . $entityString . '</p></html>';
    264         $this->subject->setHtml($html);
    265 
    266         self::assertContains(
    267             $entityString,
    268             $entityString,
    269             $this->subject->emogrify()
    270         );
     240        $result = $this->subject->emogrify();
     241
     242        self::assertContains($entityString, $result);
    271243    }
    272244
     
    277249    {
    278250        $umlautString = 'Küss die Hand, schöne Frau.';
    279 
    280251        $html = '<html><head></head><p>' . $umlautString . '</p></html>';
    281252        $this->subject->setHtml($html);
    282253
    283         self::assertContains(
    284             $umlautString,
    285             $this->subject->emogrify()
    286         );
     254        $result = $this->subject->emogrify();
     255
     256        self::assertContains($umlautString, $result);
    287257    }
    288258
     
    293263    {
    294264        $umlautString = 'Küss die Hand, schöne Frau.';
    295 
    296265        $html = '<p>' . $umlautString . '</p>';
    297266        $this->subject->setHtml($html);
    298267
    299         self::assertContains(
    300             $umlautString,
    301             $this->subject->emogrify()
    302         );
     268        $result = $this->subject->emogrify();
     269
     270        self::assertContains($umlautString, $result);
    303271    }
    304272
     
    309277    {
    310278        $umlautString = 'Küss die Hand, schöne Frau.';
    311 
    312279        $html = '<html><p>' . $umlautString . '</p></html>';
    313280        $this->subject->setHtml($html);
    314281
    315         self::assertContains(
    316             $umlautString,
    317             $this->subject->emogrify()
    318         );
     282        $result = $this->subject->emogrify();
     283
     284        self::assertContains($umlautString, $result);
    319285    }
    320286
     
    325291    {
    326292        $umlautString = 'Küss die Hand, schöne Frau.';
    327 
    328293        $html = '<head></head><p>' . $umlautString . '</p>';
    329294        $this->subject->setHtml($html);
    330295
    331         self::assertContains(
    332             $umlautString,
    333             $this->subject->emogrify()
    334         );
     296        $result = $this->subject->emogrify();
     297
     298        self::assertContains($umlautString, $result);
    335299    }
    336300
     
    344308        $this->subject->setCss('');
    345309
    346         self::assertContains(
    347             $this->html5DocumentType,
    348             $this->subject->emogrify()
    349         );
     310        $result = $this->subject->emogrify();
     311
     312        self::assertContains($this->html5DocumentType, $result);
    350313    }
    351314
     
    358321        $this->subject->setHtml($html);
    359322
    360         self::assertContains(
    361             $this->xhtml1StrictDocumentType,
    362             $this->subject->emogrify()
    363         );
     323        $result = $this->subject->emogrify();
     324
     325        self::assertContains($this->xhtml1StrictDocumentType, $result);
    364326    }
    365327
     
    372334        $this->subject->setHtml($html);
    373335
    374         self::assertContains(
    375             $this->html5DocumentType,
    376             $this->subject->emogrify()
    377         );
     336        $result = $this->subject->emogrify();
     337
     338        self::assertContains($this->html5DocumentType, $result);
    378339    }
    379340
     
    383344    public function emogrifyAddsContentTypeMetaTag()
    384345    {
    385         $html = $this->html5DocumentType . '<p>Hello</p>';
    386         $this->subject->setHtml($html);
    387 
    388         self::assertContains(
    389             '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">',
    390             $this->subject->emogrify()
    391         );
     346        $this->subject->setHtml('<p>Hello</p>');
     347
     348        $result = $this->subject->emogrify();
     349
     350        self::assertContains('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', $result);
    392351    }
    393352
     
    397356    public function emogrifyForExistingContentTypeMetaTagNotAddsSecondContentTypeMetaTag()
    398357    {
    399         $html = $this->html5DocumentType .
    400             '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' .
     358        $html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' .
    401359            '<body><p>Hello</p></body></html>';
    402360        $this->subject->setHtml($html);
    403361
    404362        $numberOfContentTypeMetaTags = substr_count($this->subject->emogrify(), 'Content-Type');
    405         self::assertSame(
    406             1,
    407             $numberOfContentTypeMetaTags
    408         );
     363
     364        self::assertSame(1, $numberOfContentTypeMetaTags);
    409365    }
    410366
     
    414370    public function emogrifyByDefaultRemovesWbrTag()
    415371    {
    416         $html = $this->html5DocumentType . '<html>foo<wbr/>bar</html>';
     372        $html = '<html>foo<wbr/>bar</html>';
    417373        $this->subject->setHtml($html);
    418374
    419         self::assertContains(
    420             'foobar',
    421             $this->subject->emogrify()
    422         );
     375        $result = $this->subject->emogrify();
     376
     377        self::assertContains('foobar', $result);
    423378    }
    424379
     
    428383    public function addUnprocessableTagCausesGivenEmptyTagToBeRemoved()
    429384    {
     385        $this->subject->setHtml('<html><p></p></html>');
     386
    430387        $this->subject->addUnprocessableHtmlTag('p');
    431 
    432         $html = $this->html5DocumentType . '<html><p></p></html>';
    433         $this->subject->setHtml($html);
    434 
    435         self::assertNotContains(
    436             '<p>',
    437             $this->subject->emogrify()
    438         );
     388        $result = $this->subject->emogrify();
     389
     390        self::assertNotContains('<p>', $result);
    439391    }
    440392
     
    444396    public function addUnprocessableTagNotRemovesGivenTagWithContent()
    445397    {
     398        $this->subject->setHtml('<html><p>foobar</p></html>');
     399
    446400        $this->subject->addUnprocessableHtmlTag('p');
    447 
    448         $html = $this->html5DocumentType . '<html><p>foobar</p></html>';
    449         $this->subject->setHtml($html);
    450 
    451         self::assertContains(
    452             '<p>',
    453             $this->subject->emogrify()
    454         );
     401        $result = $this->subject->emogrify();
     402
     403        self::assertContains('<p>', $result);
    455404    }
    456405
     
    460409    public function removeUnprocessableHtmlTagCausesTagToStayAgain()
    461410    {
     411        $this->subject->setHtml('<html><p>foo<br/><span>bar</span></p></html>');
     412
    462413        $this->subject->addUnprocessableHtmlTag('p');
    463414        $this->subject->removeUnprocessableHtmlTag('p');
    464 
    465         $html = $this->html5DocumentType . '<html><p>foo<br/><span>bar</span></p></html>';
    466         $this->subject->setHtml($html);
    467 
    468         self::assertContains(
    469             '<p>',
    470             $this->subject->emogrify()
    471         );
     415        $result = $this->subject->emogrify();
     416
     417        self::assertContains('<p>', $result);
    472418    }
    473419
     
    477423    public function emogrifyCanAddMatchingElementRuleOnHtmlElementFromCss()
    478424    {
    479         $html = $this->html5DocumentType . '<html></html>';
    480         $this->subject->setHtml($html);
     425        $this->subject->setHtml('<html></html>');
    481426        $styleRule = 'color: #000;';
    482427        $this->subject->setCss('html {' . $styleRule . '}');
    483428
     429        $result = $this->subject->emogrify();
     430
    484431        self::assertContains(
    485432            '<html style="' . $styleRule . '">',
    486             $this->subject->emogrify()
     433            $result
    487434        );
    488435    }
     
    493440    public function emogrifyNotAddsNotMatchingElementRuleOnHtmlElementFromCss()
    494441    {
    495         $html = $this->html5DocumentType . '<html></html>';
    496         $this->subject->setHtml($html);
     442        $this->subject->setHtml('<html></html>');
    497443        $this->subject->setCss('p {color:#000;}');
    498444
    499         self::assertContains(
    500             '<html>',
    501             $this->subject->emogrify()
    502         );
     445        $result = $this->subject->emogrify();
     446
     447        self::assertContains('<html>', $result);
    503448    }
    504449
     
    508453    public function emogrifyCanMatchTwoElements()
    509454    {
    510         $html = $this->html5DocumentType . '<html><p></p><p></p></html>';
    511         $this->subject->setHtml($html);
     455        $this->subject->setHtml('<html><p></p><p></p></html>');
    512456        $styleRule = 'color: #000;';
    513457        $this->subject->setCss('p {' . $styleRule . '}');
    514458
     459        $result = $this->subject->emogrify();
     460
    515461        self::assertSame(
    516462            2,
    517             substr_count($this->subject->emogrify(), '<p style="' . $styleRule . '">')
     463            substr_count($result, '<p style="' . $styleRule . '">')
    518464        );
    519465    }
     
    524470    public function emogrifyCanAssignTwoStyleRulesFromSameMatcherToElement()
    525471    {
    526         $html = $this->html5DocumentType . '<html><p></p></html>';
    527         $this->subject->setHtml($html);
     472        $this->subject->setHtml('<html><p></p></html>');
    528473        $styleRulesIn = 'color:#000; text-align:left;';
     474        $this->subject->setCss('p {' . $styleRulesIn . '}');
     475
     476        $result = $this->subject->emogrify();
     477
    529478        $styleRulesOut = 'color: #000; text-align: left;';
    530         $this->subject->setCss('p {' . $styleRulesIn . '}');
    531 
    532479        self::assertContains(
    533480            '<p style="' . $styleRulesOut . '">',
    534             $this->subject->emogrify()
     481            $result
    535482        );
    536483    }
     
    541488    public function emogrifyCanMatchAttributeOnlySelector()
    542489    {
    543         $html = $this->html5DocumentType . '<html><p hidden="hidden"></p></html>';
    544         $this->subject->setHtml($html);
     490        $this->subject->setHtml('<html><p hidden="hidden"></p></html>');
    545491        $this->subject->setCss('[hidden] { color:red; }');
    546492
    547         self::assertContains(
    548             '<p hidden="hidden" style="color: red;">',
    549             $this->subject->emogrify()
    550         );
     493        $result = $this->subject->emogrify();
     494
     495        self::assertContains('<p hidden="hidden" style="color: red;">', $result);
    551496    }
    552497
     
    556501    public function emogrifyCanAssignStyleRulesFromTwoIdenticalMatchersToElement()
    557502    {
    558         $html = $this->html5DocumentType . '<html><p></p></html>';
    559         $this->subject->setHtml($html);
     503        $this->subject->setHtml('<html><p></p></html>');
    560504        $styleRule1 = 'color: #000;';
    561505        $styleRule2 = 'text-align: left;';
    562506        $this->subject->setCss('p {' . $styleRule1 . '}  p {' . $styleRule2 . '}');
    563507
     508        $result = $this->subject->emogrify();
     509
    564510        self::assertContains(
    565511            '<p style="' . $styleRule1 . ' ' . $styleRule2 . '">',
    566             $this->subject->emogrify()
     512            $result
    567513        );
    568514    }
     
    573519    public function emogrifyCanAssignStyleRulesFromTwoDifferentMatchersToElement()
    574520    {
    575         $html = $this->html5DocumentType . '<html><p class="x"></p></html>';
    576         $this->subject->setHtml($html);
     521        $this->subject->setHtml('<html><p class="x"></p></html>');
    577522        $styleRule1 = 'color: #000;';
    578523        $styleRule2 = 'text-align: left;';
    579524        $this->subject->setCss('p {' . $styleRule1 . '} .x {' . $styleRule2 . '}');
    580525
     526        $result = $this->subject->emogrify();
     527
    581528        self::assertContains(
    582529            '<p class="x" style="' . $styleRule1 . ' ' . $styleRule2 . '">',
    583             $this->subject->emogrify()
     530            $result
    584531        );
    585532    }
     
    612559            'child selector P > SPAN matches direct child'
    613560                => ['p > span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'],
    614             'child selector BODY > SPAN not matches grandchild'
     561            'child selector P > SPAN matches direct child without space after >'
     562                => ['p >span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'],
     563            'child selector P > SPAN matches direct child without space before >'
     564                => ['p> span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'],
     565            'child selector P > SPAN matches direct child without space before or after >'
     566                => ['p>span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'],
     567            'child selector BODY > SPAN does not match grandchild'
    615568                => ['body > span {' . $styleRule . '} ', '#<span>#'],
    616             'adjacent selector P + P not matches first P' => ['p + p {' . $styleRule . '} ', '#<p class="p-1">#'],
     569            'adjacent selector P + P does not match first P' => ['p + p {' . $styleRule . '} ', '#<p class="p-1">#'],
    617570            'adjacent selector P + P matches second P'
    618571                => ['p + p {' . $styleRule . '} ', '#<p class="p-2" style="' . $styleRule . '">#'],
     
    627580            'attribute presence selector SPAN[title] matches element with matching attribute'
    628581                => ['span[title] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'],
    629             'attribute presence selector SPAN[title] not matches element without any attributes'
     582            'attribute presence selector SPAN[title] does not match element without any attributes'
    630583                => ['span[title] {' . $styleRule . '} ', '#<span>#'],
    631584            'attribute value selector [id="html"] matches element with matching attribute value' => [
     
    635588                'span[title="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
    636589            ],
    637             'attribute value selector SPAN[title] not matches element with other attribute value'
     590            'attribute value selector SPAN[title] matches element with matching attribute value two words' => [
     591                'span[title="buenas dias"] {' . $styleRule . '} ', '#<span title="buenas dias" '
     592                    . $styleAttribute . '>#'
     593            ],
     594            'attribute value selector SPAN[title] matches element with matching attribute value four words' => [
     595                'span[title="buenas dias bom dia"] {' . $styleRule . '} ', '#<span title="buenas dias bom dia" '
     596                    . $styleAttribute . '>#'
     597            ],
     598            'attribute value selector SPAN[title~] matches element with an attribute value with just that word' => [
     599                'span[title~="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     600            ],
     601            'attribute value selector SPAN[title~] matches element with attribute value with that word as 2nd of 2' => [
     602                'span[title~="dias"] {' . $styleRule . '} ', '#<span title="buenas dias" ' . $styleAttribute . '>#'
     603            ],
     604            'attribute value selector SPAN[title~] matches element with attribute value with that word as 1st of 2' => [
     605                'span[title~="buenas"] {' . $styleRule . '} ', '#<span title="buenas dias" ' . $styleAttribute . '>#'
     606            ],
     607            'attribute value selector SPAN[title*] matches element with an attribute value with just that word' => [
     608                'span[title*="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     609            ],
     610            'attribute value selector SPAN[title*] matches element with attribute value with that word as 2nd of 2' => [
     611                'span[title*="dias"] {' . $styleRule . '} ', '#<span title="buenas dias" ' . $styleAttribute . '>#'
     612            ],
     613            'attribute value selector SPAN[title*] matches element with an attribute value with parts two words' => [
     614                'span[title*="enas di"] {' . $styleRule . '} ', '#<span title="buenas dias" ' . $styleAttribute . '>#'
     615            ],
     616            'attribute value selector SPAN[title*] matches element with an attribute value with odd characters' => [
     617                'span[title*=": subtitle; author"] {' . $styleRule . '} ', '#<span title="title: subtitle; author" '
     618                    . $styleAttribute . '>#'
     619            ],
     620            'attribute value selector SPAN[title^] matches element with attribute value that is exactly that word' => [
     621                'span[title^="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     622            ],
     623            'attribute value selector SPAN[title^] matches element with an attribute value that begins that word' => [
     624                'span[title^="bonj"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     625            ],
     626            'attribute value selector SPAN[title^] matches element with an attribute value that begins that word '
     627            . 'and contains other words' => [
     628                'span[title^="buenas"] {' . $styleRule . '} ', '#<span title="buenas dias" ' . $styleAttribute . '>#'
     629            ],
     630            'attribute value selector SPAN[title$] matches element with attribute value that is exactly that word' => [
     631                'span[title$="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     632            ],
     633            'attribute value selector SPAN[title$] matches element with an attribute value with two words' => [
     634                'span[title$="buenas dias"] {' . $styleRule . '} ', '#<span title="buenas dias" '
     635                    . $styleAttribute . '>#'
     636            ],
     637            'attribute value selector SPAN[title$] matches element with an attribute value that end that word' => [
     638                'span[title$="jour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     639            ],
     640            'attribute value selector SPAN[title$] matches element with an attribute value that end that word '
     641            . 'and contains other words' => [
     642                'span[title$="dias"] {' . $styleRule . '} ', '#<span title="buenas dias" ' . $styleAttribute . '>#'
     643            ],
     644            'attribute value selector SPAN[title|] matches element with attribute value that is exactly that word' => [
     645                'span[title|="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
     646            ],
     647            'attribute value selector SPAN[title|] matches element with an attribute value with two words' => [
     648                'span[title|="buenas dias"] {' . $styleRule . '} ', '#<span title="buenas dias" '
     649                    . $styleAttribute . '>#'
     650            ],
     651            'attribute value selector SPAN[title|] matches element with an attribute value with 2 words with hypen' => [
     652                'span[title|="avez"] {' . $styleRule . '} ', '#<span title="avez-vous" ' . $styleAttribute . '>#'
     653            ],
     654            'attribute value selector SPAN[title] does not match element with other attribute value'
    638655                => ['span[title="bonjour"] {' . $styleRule . '} ', '#<span title="buenas dias">#'],
    639             'attribute value selector SPAN[title] not matches element without any attributes'
     656            'attribute value selector SPAN[title] does not match element without any attributes'
    640657                => ['span[title="bonjour"] {' . $styleRule . '} ', '#<span>#'],
    641             'BODY:first-child matches first child'
    642                 => ['body:first-child {' . $styleRule . '} ', '#<p class="p-1" style="' . $styleRule . '">#'],
    643             'BODY:first-child not matches middle child'
    644                 => ['body:first-child {' . $styleRule . '} ', '#<p class="p-2">#'],
    645             'BODY:first-child not matches last child'
    646                 => ['body:first-child {' . $styleRule . '} ', '#<p class="p-3">#'],
    647             'BODY:last-child not matches first child' => ['body:last-child {' . $styleRule . '} ', '#<p class="p-1">#'],
    648             'BODY:last-child not matches middle child'
    649                 => ['body:last-child {' . $styleRule . '} ', '#<p class="p-2">#'],
    650             'BODY:last-child matches last child'
    651                 => ['body:last-child {' . $styleRule . '} ', '#<p class="p-3" style="' . $styleRule . '">#'],
     658            'P:first-child matches first child with matching tag'
     659                => ['p:first-child {' . $styleRule . '} ', '#<p class="p-1" style="' . $styleRule . '">#'],
     660            'DIV:first-child does not match first child with mismatching tag'
     661                => ['div:first-child {' . $styleRule . '} ', '#<p class="p-1">#'],
     662            'P:first-child does not match middle child'
     663                => ['p:first-child {' . $styleRule . '} ', '#<p class="p-2">#'],
     664            'P:first-child does not match last child'
     665                => ['p:first-child {' . $styleRule . '} ', '#<p class="p-6">#'],
     666            'P:last-child does not match first child' => ['p:last-child {' . $styleRule . '} ', '#<p class="p-1">#'],
     667            'P:last-child does not match middle child'
     668                => ['p:last-child {' . $styleRule . '} ', '#<p class="p-3">#'],
     669            'P:last-child matches last child'
     670                => ['p:last-child {' . $styleRule . '} ', '#<p class="p-6" style="' . $styleRule . '">#'],
     671            'DIV:last-child does not match last child with mismatching tag'
     672                => ['div:last-child {' . $styleRule . '} ', '#<p class="p-6">#'],
    652673        ];
    653674    }
     
    663684    public function emogrifierMatchesSelectors($css, $htmlRegularExpression)
    664685    {
    665         $html = $this->html5DocumentType .
    666             '<html id="html">' .
     686        $html = '<html id="html">' .
    667687            '  <body>' .
    668688            '    <p class="p-1"><span>some text</span></p>' .
    669689            '    <p class="p-2"><span title="bonjour">some</span> text</p>' .
    670690            '    <p class="p-3"><span title="buenas dias">some</span> more text</p>' .
     691            '    <p class="p-4"><span title="avez-vous">some</span> more text</p>' .
     692            '    <p class="p-5"><span title="buenas dias bom dia">some</span> more text</p>' .
     693            '    <p class="p-6"><span title="title: subtitle; author">some</span> more text</p>' .
    671694            '  </body>' .
    672695            '</html>';
     
    676699        $result = $this->subject->emogrify();
    677700
    678         self::assertRegExp(
    679             $htmlRegularExpression,
    680             $result
    681         );
     701        self::assertRegExp($htmlRegularExpression, $result);
    682702    }
    683703
     
    719739        $expectedStyleAttributeContent
    720740    ) {
    721         $html = $this->html5DocumentType . '<html></html>';
    722         $css = 'html {' . $cssDeclaration . '}';
    723         $this->subject->setHtml($html);
    724         $this->subject->setCss($css);
     741        $this->subject->setHtml('<html></html>');
     742        $this->subject->setCss('html {' . $cssDeclaration . '}');
    725743
    726744        $result = $this->subject->emogrify();
     
    768786    public function emogrifyFormatsCssDeclarations($cssDeclarationBlock, $expectedStyleAttributeContent)
    769787    {
    770         $html = $this->html5DocumentType . '<html></html>';
    771         $css = 'html {' . $cssDeclarationBlock . '}';
    772 
    773         $this->subject->setHtml($html);
    774         $this->subject->setCss($css);
     788        $this->subject->setHtml('<html></html>');
     789        $this->subject->setCss('html {' . $cssDeclarationBlock . '}');
     790
     791        $result = $this->subject->emogrify();
    775792
    776793        self::assertContains(
    777794            'html style="' . $expectedStyleAttributeContent . '">',
    778             $this->subject->emogrify()
     795            $result
    779796        );
    780797    }
     
    804821    public function emogrifyDropsInvalidDeclaration($cssDeclarationBlock)
    805822    {
    806         $html = $this->html5DocumentType . '<html></html>';
    807         $css = 'html {' . $cssDeclarationBlock . '}';
    808 
    809         $this->subject->setHtml($html);
    810         $this->subject->setCss($css);
    811 
    812         self::assertContains(
    813             '<html style="">',
    814             $this->subject->emogrify()
    815         );
     823        $this->subject->setHtml('<html></html>');
     824        $this->subject->setCss('html {' . $cssDeclarationBlock . '}');
     825
     826        $result = $this->subject->emogrify();
     827
     828        self::assertContains('<html style="">', $result);
    816829    }
    817830
     
    822835    {
    823836        $styleAttribute = 'style="color: #ccc;"';
    824         $html = $this->html5DocumentType . '<html ' . $styleAttribute . '></html>';
    825         $this->subject->setHtml($html);
    826 
    827         self::assertContains(
    828             $styleAttribute,
    829             $this->subject->emogrify()
    830         );
     837        $this->subject->setHtml('<html ' . $styleAttribute . '></html>');
     838
     839        $result = $this->subject->emogrify();
     840
     841        self::assertContains($styleAttribute, $result);
    831842    }
    832843
     
    837848    {
    838849        $styleAttributeValue = 'color: #ccc;';
    839         $html = $this->html5DocumentType . '<html style="' . $styleAttributeValue . '"></html>';
    840         $this->subject->setHtml($html);
    841 
     850        $this->subject->setHtml('<html style="' . $styleAttributeValue . '"></html>');
    842851        $cssDeclarations = 'margin: 0 2px;';
    843852        $css = 'html {' . $cssDeclarations . '}';
    844853        $this->subject->setCss($css);
    845854
     855        $result = $this->subject->emogrify();
     856
    846857        self::assertContains(
    847858            'style="' . $styleAttributeValue . ' ' . $cssDeclarations . '"',
    848             $this->subject->emogrify()
     859            $result
    849860        );
    850861    }
     
    855866    public function emogrifyCanMatchMinifiedCss()
    856867    {
    857         $html = $this->html5DocumentType . '<html><p></p></html>';
     868        $this->subject->setHtml('<html><p></p></html>');
     869        $this->subject->setCss('p{color:blue;}html{color:red;}');
     870
     871        $result = $this->subject->emogrify();
     872
     873        self::assertContains('<html style="color: red;">', $result);
     874    }
     875
     876    /**
     877     * @test
     878     */
     879    public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
     880    {
     881        $this->subject->setHtml('<html style="COLOR:#ccc;"></html>');
     882
     883        $result = $this->subject->emogrify();
     884
     885        self::assertContains('style="color: #ccc;"', $result);
     886    }
     887
     888    /**
     889     * @test
     890     */
     891    public function emogrifyLowerCasesAttributeNames()
     892    {
     893        $this->subject->setHtml('<html></html>');
     894        $this->subject->setCss('html {mArGiN:0 2pX;}');
     895
     896        $result = $this->subject->emogrify();
     897
     898        self::assertContains('style="margin: 0 2pX;"', $result);
     899    }
     900
     901    /**
     902     * @test
     903     */
     904    public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
     905    {
     906        $cssDeclaration = 'content: \'Hello World\';';
     907        $this->subject->setHtml('<html><body><p>target</p></body></html>');
     908        $this->subject->setCss('p {' . $cssDeclaration . '}');
     909
     910        $result = $this->subject->emogrify();
     911
     912        self::assertContains(
     913            '<p style="' . $cssDeclaration . '">target</p>',
     914            $result
     915        );
     916    }
     917
     918    /**
     919     * @test
     920     */
     921    public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
     922    {
     923        $cssDeclaration = 'content: \'Hello World\';';
     924        $this->subject->setHtml(
     925            '<html><head><style>p {' . $cssDeclaration . '}</style></head><body><p>target</p></body></html>'
     926        );
     927
     928        $result = $this->subject->emogrify();
     929
     930        self::assertContains(
     931            '<p style="' . $cssDeclaration . '">target</p>',
     932            $result
     933        );
     934    }
     935
     936    /**
     937     * @test
     938     */
     939    public function emogrifyRemovesStyleNodes()
     940    {
     941        $this->subject->setHtml('<html><style type="text/css"></style></html>');
     942
     943        $result = $this->subject->emogrify();
     944
     945        self::assertNotContains('<style', $result);
     946    }
     947
     948    /**
     949     * @test
     950     *
     951     * @expectedException \InvalidArgumentException
     952     */
     953    public function emogrifyInDebugModeForInvalidCssSelectorThrowsException()
     954    {
     955        $this->subject->setHtml(
     956            '<html><style type="text/css">p{color:red;} <style data-x="1">html{cursor:text;}</style></html>'
     957        );
     958
     959        $this->subject->setDebug(true);
     960        $this->subject->emogrify();
     961    }
     962
     963    /**
     964     * @test
     965     */
     966    public function emogrifyNotInDebugModeIgnoresInvalidCssSelectors()
     967    {
     968        $html = '<html><style type="text/css">' .
     969            'p{color:red;} <style data-x="1">html{cursor:text;} p{background-color:blue;}</style> ' .
     970            '<body><p></p></body></html>';
     971
    858972        $this->subject->setHtml($html);
    859         $this->subject->setCss('p{color:blue;}html{color:red;}');
    860 
    861         self::assertContains(
    862             '<html style="color: red;">',
    863             $this->subject->emogrify()
    864         );
    865     }
    866 
    867     /**
    868      * @test
    869      */
    870     public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
    871     {
    872         $html = $this->html5DocumentType . '<html style="COLOR:#ccc;"></html>';
    873         $this->subject->setHtml($html);
    874 
    875         self::assertContains(
    876             'style="color: #ccc;"',
    877             $this->subject->emogrify()
    878         );
    879     }
    880 
    881     /**
    882      * @test
    883      */
    884     public function emogrifyLowerCasesAttributeNames()
    885     {
    886         $html = $this->html5DocumentType . '<html></html>';
    887         $this->subject->setHtml($html);
    888         $cssIn = 'html {mArGiN:0 2pX;}';
    889         $cssOut = 'margin: 0 2pX;';
    890         $this->subject->setCss($cssIn);
    891 
    892         self::assertContains(
    893             'style="' . $cssOut . '"',
    894             $this->subject->emogrify()
    895         );
    896     }
    897 
    898     /**
    899      * @test
    900      */
    901     public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
    902     {
    903         $css = 'content: \'Hello World\';';
    904         $html = $this->html5DocumentType . '<html><body><p>target</p></body></html>';
    905         $this->subject->setHtml($html);
    906         $this->subject->setCss('p {' . $css . '}');
    907 
    908         self::assertContains(
    909             '<p style="' . $css . '">target</p>',
    910             $this->subject->emogrify()
    911         );
    912     }
    913 
    914     /**
    915      * @test
    916      */
    917     public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
    918     {
    919         $css = 'content: \'Hello World\';';
    920         $html = $this->html5DocumentType . '<html><head><style>p {' .
    921             $css . '}</style></head><body><p>target</p></body></html>';
    922         $this->subject->setHtml($html);
    923 
    924         self::assertContains(
    925             '<p style="' . $css . '">target</p>',
    926             $this->subject->emogrify()
    927         );
    928     }
    929 
    930     /**
    931      * @test
    932      */
    933     public function emogrifyRemovesStyleNodes()
    934     {
    935         $html = $this->html5DocumentType . '<html><style type="text/css"></style></html>';
    936         $this->subject->setHtml($html);
    937 
    938         self::assertNotContains(
    939             '<style>',
    940             $this->subject->emogrify()
    941         );
    942     }
    943 
    944     /**
    945      * @test
    946      */
    947     public function emogrifyIgnoresInvalidCssSelector()
    948     {
    949         $html = $this->html5DocumentType .
    950             '<html><style type="text/css">p{color:red;} <style data-x="1">html{cursor:text;}</style></html>';
    951         $this->subject->setHtml($html);
    952 
    953         $hasError = false;
    954         set_error_handler(function ($errorNumber, $errorMessage) use (&$hasError) {
    955             if ($errorMessage === 'DOMXPath::query(): Invalid expression') {
    956                 return true;
    957             }
    958 
    959             $hasError = true;
    960             return true;
    961         });
    962 
    963         $this->subject->emogrify();
    964         restore_error_handler();
    965 
    966         self::assertFalse(
    967             $hasError
    968         );
     973
     974        $html = $this->subject->emogrify();
     975        self::assertContains('color: red', $html);
     976        self::assertContains('background-color: blue', $html);
    969977    }
    970978
     
    972980     * Data provider for things that should be left out when applying the CSS.
    973981     *
    974      * @return array[]
     982     * @return string[][]
    975983     */
    976984    public function unneededCssThingsDataProvider()
     
    10011009    public function emogrifyFiltersUnneededCssThings($css, $markerNotExpectedInHtml)
    10021010    {
    1003         $html = $this->html5DocumentType . '<html><p>foo</p></html>';
    1004         $this->subject->setHtml($html);
     1011        $this->subject->setHtml('<html><p>foo</p></html>');
    10051012        $this->subject->setCss($css);
    10061013
    1007         self::assertNotContains(
    1008             $markerNotExpectedInHtml,
    1009             $this->subject->emogrify()
    1010         );
     1014        $result = $this->subject->emogrify();
     1015
     1016        self::assertNotContains($markerNotExpectedInHtml, $result);
    10111017    }
    10121018
     
    10141020     * Data provider for media rules.
    10151021     *
    1016      * @return array[]
     1022     * @return string[][]
    10171023     */
    10181024    public function mediaRulesDataProvider()
     
    10371043    public function emogrifyKeepsMediaRules($css)
    10381044    {
    1039           $html = $this->html5DocumentType . '<html><p>foo</p></html>';
    1040           $this->subject->setHtml($html);
    1041           $this->subject->setCss($css);
    1042 
    1043           self::assertContains(
    1044               $css,
    1045               $this->subject->emogrify()
    1046           );
     1045        $this->subject->setHtml('<html><p>foo</p></html>');
     1046        $this->subject->setCss($css);
     1047
     1048        $result = $this->subject->emogrify();
     1049
     1050        self::assertContains($css, $result);
    10471051    }
    10481052
     
    10531057    {
    10541058        $css = '@media screen { html {} }';
    1055 
    1056         $html = $this->html5DocumentType . '<html></html>';
    1057         $this->subject->setHtml($html);
     1059        $this->subject->setHtml('<html></html>');
    10581060        $this->subject->setCss($css);
    10591061        $this->subject->removeAllowedMediaType('screen');
    10601062
    1061         self::assertNotContains(
    1062             $css,
    1063             $this->subject->emogrify()
    1064         );
     1063        $result = $this->subject->emogrify();
     1064
     1065        self::assertNotContains($css, $result);
    10651066    }
    10661067
     
    10711072    {
    10721073        $css = '@media braille { html { some-property: value; } }';
    1073 
    1074         $html = $this->html5DocumentType . '<html></html>';
    1075         $this->subject->setHtml($html);
     1074        $this->subject->setHtml('<html></html>');
    10761075        $this->subject->setCss($css);
    10771076        $this->subject->addAllowedMediaType('braille');
    10781077
    1079         self::assertContains(
    1080             $css,
    1081             $this->subject->emogrify()
    1082         );
     1078        $result = $this->subject->emogrify();
     1079
     1080        self::assertContains($css, $result);
    10831081    }
    10841082
     
    10881086    public function emogrifyAddsMissingHeadElement()
    10891087    {
    1090         $html = $this->html5DocumentType . '<html></html>';
    1091         $this->subject->setHtml($html);
     1088        $this->subject->setHtml('<html></html>');
    10921089        $this->subject->setCss('@media all { html {} }');
    10931090
    1094         self::assertContains(
    1095             '<head>',
    1096             $this->subject->emogrify()
    1097         );
     1091        $result = $this->subject->emogrify();
     1092
     1093        self::assertContains('<head>', $result);
    10981094    }
    10991095
     
    11021098     */
    11031099    public function emogrifyKeepExistingHeadElementContent()
     1100    {
     1101        $this->subject->setHtml('<html><head><!-- original content --></head></html>');
     1102        $this->subject->setCss('@media all { html {} }');
     1103
     1104        $result = $this->subject->emogrify();
     1105
     1106        self::assertContains('<!-- original content -->', $result);
     1107    }
     1108
     1109    /**
     1110     * @test
     1111     */
     1112    public function emogrifyKeepExistingHeadElementAddStyleElement()
    11041113    {
    11051114        $html = $this->html5DocumentType . '<html><head><!-- original content --></head></html>';
     
    11071116        $this->subject->setCss('@media all { html {} }');
    11081117
    1109         self::assertContains(
    1110             '<!-- original content -->',
    1111             $this->subject->emogrify()
    1112         );
    1113     }
    1114 
    1115     /**
    1116      * @test
    1117      */
    1118     public function emogrifyKeepExistingHeadElementAddStyleElement()
    1119     {
    1120         $html = $this->html5DocumentType . '<html><head><!-- original content --></head></html>';
    1121         $this->subject->setHtml($html);
    1122         $this->subject->setCss('@media all { html {} }');
    1123 
    1124         self::assertContains(
    1125             '<style type="text/css">',
    1126             $this->subject->emogrify()
    1127         );
     1118        $result = $this->subject->emogrify();
     1119
     1120        self::assertContains('<style type="text/css">', $result);
    11281121    }
    11291122
     
    11311124     * Valid media query which need to be preserved
    11321125     *
    1133      * @return array[]
     1126     * @return string[][]
    11341127     */
    11351128    public function validMediaPreserveDataProvider()
     
    11551148            'style in "only all" media type rule' => ['@media only all { h1 { color:red; } }'],
    11561149            'style in "screen" media type rule' => ['@media screen { h1 { color:red; } }'],
     1150            'style in "print" media type rule' => ['@media print { * { color:#000 !important; } }'],
    11571151            'style in media type rule without specification' => ['@media { h1 { color:red; } }'],
     1152            'style with multiple media type rules' => [
     1153                '@media all { p { color: #000; } }' .
     1154                '@media only screen { h1 { color:red; } }' .
     1155                '@media only all { h1 { color:red; } }' .
     1156                '@media print { * { color:#000 !important; } }' .
     1157                '@media { h1 { color:red; } }'
     1158            ],
    11581159        ];
    11591160    }
     
    11681169    public function emogrifyWithValidMediaQueryContainsInnerCss($css)
    11691170    {
    1170         $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1><p></p></html>';
    1171         $this->subject->setHtml($html);
     1171        $this->subject->setHtml('<html><h1></h1><p></p></html>');
    11721172        $this->subject->setCss($css);
    11731173
    1174         self::assertContains(
    1175             $css,
    1176             $this->subject->emogrify()
    1177         );
     1174        $result = $this->subject->emogrify();
     1175
     1176        self::assertContains('<style type="text/css">' . $css . '</style>', $result);
    11781177    }
    11791178
     
    11851184     * @dataProvider validMediaPreserveDataProvider
    11861185     */
     1186    public function emogrifyWithValidMinifiedMediaQueryContainsInnerCss($css)
     1187    {
     1188        // Minify CSS by removing unnecessary whitespace.
     1189        $css = preg_replace('/\\s*{\\s*/', '{', $css);
     1190        $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
     1191        $css = preg_replace('/@media{/', '@media {', $css);
     1192
     1193        $this->subject->setHtml('<html><h1></h1><p></p></html>');
     1194        $this->subject->setCss($css);
     1195
     1196        $result = $this->subject->emogrify();
     1197
     1198        self::assertContains('<style type="text/css">' . $css . '</style>', $result);
     1199    }
     1200
     1201    /**
     1202     * @test
     1203     *
     1204     * @param string $css
     1205     *
     1206     * @dataProvider validMediaPreserveDataProvider
     1207     */
    11871208    public function emogrifyForHtmlWithValidMediaQueryContainsInnerCss($css)
    11881209    {
    1189         $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css .
    1190             '</style><h1></h1><p></p></html>';
    1191         $this->subject->setHtml($html);
    1192 
    1193         self::assertContains(
    1194             $css,
    1195             $this->subject->emogrify()
    1196         );
     1210        $this->subject->setHtml('<html><style type="text/css">' . $css . '</style><h1></h1><p></p></html>');
     1211
     1212        $result = $this->subject->emogrify();
     1213
     1214        self::assertContains('<style type="text/css">' . $css . '</style>', $result);
    11971215    }
    11981216
     
    12061224    public function emogrifyWithValidMediaQueryNotContainsInlineCss($css)
    12071225    {
    1208         $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
    1209         $this->subject->setHtml($html);
     1226        $this->subject->setHtml('<html><h1></h1></html>');
    12101227        $this->subject->setCss($css);
    12111228
    1212         self::assertNotContains(
    1213             'style="color:red"',
    1214             $this->subject->emogrify()
    1215         );
     1229        $result = $this->subject->emogrify();
     1230
     1231        self::assertNotContains('style="color:red"', $result);
    12161232    }
    12171233
     
    12191235     * Invalid media query which need to be strip
    12201236     *
    1221      * @return array[]
     1237     * @return string[][]
    12221238     */
    12231239    public function invalidMediaPreserveDataProvider()
     
    12411257     * @dataProvider invalidMediaPreserveDataProvider
    12421258     */
    1243     public function emogrifyWithInvalidMediaQueryaNotContainsInnerCss($css)
    1244     {
    1245         $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
    1246         $this->subject->setHtml($html);
     1259    public function emogrifyWithInvalidMediaQueryNotContainsInnerCss($css)
     1260    {
     1261        $this->subject->setHtml('<html><h1></h1></html>');
    12471262        $this->subject->setCss($css);
    12481263
    1249         self::assertNotContains(
    1250             $css,
    1251             $this->subject->emogrify()
    1252         );
     1264        $result = $this->subject->emogrify();
     1265
     1266        self::assertNotContains($css, $result);
    12531267    }
    12541268
     
    12621276    public function emogrifyWithInValidMediaQueryNotContainsInlineCss($css)
    12631277    {
    1264         $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
    1265         $this->subject->setHtml($html);
     1278        $this->subject->setHtml('<html><h1></h1></html>');
    12661279        $this->subject->setCss($css);
    12671280
    1268         self::assertNotContains(
    1269             'style="color: red"',
    1270             $this->subject->emogrify()
    1271         );
     1281        $result = $this->subject->emogrify();
     1282
     1283        self::assertNotContains('style="color: red"', $result);
    12721284    }
    12731285
     
    12811293    public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInnerCss($css)
    12821294    {
    1283         $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css .
    1284             '</style><h1></h1></html>';
    1285         $this->subject->setHtml($html);
    1286 
    1287         self::assertNotContains(
    1288             $css,
    1289             $this->subject->emogrify()
    1290         );
     1295        $this->subject->setHtml('<html><style type="text/css">' . $css . '</style><h1></h1></html>');
     1296
     1297        $result = $this->subject->emogrify();
     1298
     1299        self::assertNotContains($css, $result);
    12911300    }
    12921301
     
    13001309    public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInlineCss($css)
    13011310    {
    1302         $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css .
    1303             '</style><h1></h1></html>';
    1304         $this->subject->setHtml($html);
    1305 
    1306         self::assertNotContains(
    1307             'style="color: red"',
    1308             $this->subject->emogrify()
    1309         );
     1311        $this->subject->setHtml('<html><style type="text/css">' . $css . '</style><h1></h1></html>');
     1312
     1313        $result = $this->subject->emogrify();
     1314
     1315        self::assertNotContains('style="color: red"', $result);
     1316    }
     1317
     1318    /**
     1319     * @test
     1320     */
     1321    public function emogrifyIgnoresEmptyMediaQuery()
     1322    {
     1323        $this->subject->setHtml('<html><h1></h1></html>');
     1324        $this->subject->setCss('@media screen {} @media tv { h1 { color: red; } }');
     1325
     1326        $result = $this->subject->emogrify();
     1327
     1328        self::assertNotContains('style="color: red"', $result);
     1329        self::assertNotContains('@media screen', $result);
     1330    }
     1331
     1332    /**
     1333     * @test
     1334     */
     1335    public function emogrifyIgnoresMediaQueryWithWhitespaceOnly()
     1336    {
     1337        $this->subject->setHtml('<html><h1></h1></html>');
     1338        $this->subject->setCss('@media screen { } @media tv { h1 { color: red; } }');
     1339
     1340        $result = $this->subject->emogrify();
     1341
     1342        self::assertNotContains('style="color: red"', $result);
     1343        self::assertNotContains('@media screen', $result);
    13101344    }
    13111345
     
    13161350    {
    13171351        $styleAttributeValue = 'color: #ccc;';
    1318         $html = $this->html5DocumentType .
    1319             '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
    1320         $this->subject->setHtml($html);
     1352        $this->subject->setHtml('<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>');
     1353
     1354        $result = $this->subject->emogrify();
    13211355
    13221356        self::assertContains(
    13231357            '<html style="' . $styleAttributeValue . '">',
    1324             $this->subject->emogrify()
     1358            $result
    13251359        );
    13261360    }
     
    13321366    {
    13331367        $styleAttributeValue = 'color: #ccc;';
    1334         $html = $this->html5DocumentType .
    1335             '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
    1336         $this->subject->setHtml($html);
     1368        $this->subject->setHtml('<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>');
    13371369        $this->subject->disableStyleBlocksParsing();
     1370
     1371        $result = $this->subject->emogrify();
    13381372
    13391373        self::assertNotContains(
    13401374            '<html style="' . $styleAttributeValue . '">',
    1341             $this->subject->emogrify()
     1375            $result
    13421376        );
    13431377    }
     
    13491383    {
    13501384        $styleAttributeValue = 'text-align: center;';
    1351         $html = $this->html5DocumentType . '<html><head><style type="text/css">p { color: #ccc; }</style></head>' .
    1352             '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>';
    1353         $expected = '<p style="' . $styleAttributeValue . '">';
    1354         $this->subject->setHtml($html);
     1385        $this->subject->setHtml(
     1386            '<html><head><style type="text/css">p { color: #ccc; }</style></head>' .
     1387            '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>'
     1388        );
    13551389        $this->subject->disableStyleBlocksParsing();
    13561390
     1391        $result = $this->subject->emogrify();
     1392
    13571393        self::assertContains(
    1358             $expected,
    1359             $this->subject->emogrify()
     1394            '<p style="' . $styleAttributeValue . '">',
     1395            $result
    13601396        );
    13611397    }
     
    13661402    public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
    13671403    {
     1404        $this->subject->setHtml('<html style="color: #ccc;"></html>');
     1405        $this->subject->disableInlineStyleAttributesParsing();
     1406
     1407        $result = $this->subject->emogrify();
     1408
     1409        self::assertNotContains('<html style', $result);
     1410    }
     1411
     1412    /**
     1413     * @test
     1414     */
     1415    public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
     1416    {
    13681417        $styleAttributeValue = 'color: #ccc;';
    1369         $html = $this->html5DocumentType . '<html style="' . $styleAttributeValue . '"></html>';
    1370         $this->subject->setHtml($html);
     1418        $this->subject->setHtml(
     1419            '<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>' .
     1420            '<body><p style="text-align: center;">paragraph</p></body></html>'
     1421        );
    13711422        $this->subject->disableInlineStyleAttributesParsing();
    13721423
    1373         self::assertNotContains(
    1374             '<html style',
    1375             $this->subject->emogrify()
    1376         );
    1377     }
    1378 
    1379     /**
    1380      * @test
    1381      */
    1382     public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
    1383     {
    1384         $styleAttributeValue = 'color: #ccc;';
    1385         $html = $this->html5DocumentType .
    1386             '<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>' .
    1387             '<body><p style="text-align: center;">paragraph</p></body></html>';
    1388         $expected = '<p style="' . $styleAttributeValue . '">';
    1389         $this->subject->setHtml($html);
    1390         $this->subject->disableInlineStyleAttributesParsing();
     1424        $result = $this->subject->emogrify();
    13911425
    13921426        self::assertContains(
    1393             $expected,
    1394             $this->subject->emogrify()
     1427            '<p style="' . $styleAttributeValue . '">',
     1428            $result
    13951429        );
    13961430    }
     
    14011435    public function emogrifyAppliesCssWithUpperCaseSelector()
    14021436    {
    1403         $html = $this->html5DocumentType .
    1404             '<html><style type="text/css">P { color:#ccc; }</style><body><p>paragraph</p></body></html>';
    1405         $expected = '<p style="color: #ccc;">';
    1406         $this->subject->setHtml($html);
     1437        $this->subject->setHtml(
     1438            '<html><style type="text/css">P { color:#ccc; }</style><body><p>paragraph</p></body></html>'
     1439        );
     1440
     1441        $result = $this->subject->emogrify();
     1442
     1443        self::assertContains('<p style="color: #ccc;">', $result);
     1444    }
     1445
     1446    /**
     1447     * Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks.
     1448     * @test
     1449     */
     1450    public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
     1451    {
     1452        $this->subject->setHtml(
     1453            '<html><head><style>#topWrap p {padding-bottom: 1px;PADDING-TOP: 0;}</style></head>' .
     1454            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
     1455        );
     1456
     1457        $result = $this->subject->emogrify();
     1458
     1459        self::assertContains('<p style="text-align: center; padding-bottom: 1px; padding-top: 0;">', $result);
     1460    }
     1461
     1462    /**
     1463     * Passed in CSS sets the order, but style block CSS overrides values.
     1464     * @test
     1465     */
     1466    public function emogrifyMergesCssWithMixedCaseAttribute()
     1467    {
     1468        $this->subject->setHtml(
     1469            '<html><head><style>#topWrap p {padding-bottom: 3px;PADDING-TOP: 1px;}</style></head>' .
     1470            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
     1471        );
     1472        $this->subject->setCss('p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}');
     1473
     1474        $result = $this->subject->emogrify();
    14071475
    14081476        self::assertContains(
    1409             $expected,
    1410             $this->subject->emogrify()
    1411         );
    1412     }
    1413 
    1414     /**
    1415      * Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks.
    1416      * @test
    1417      */
    1418     public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
    1419     {
    1420         $html = $this->html5DocumentType .
    1421             '<html><head><style>#topWrap p {padding-bottom: 1px;PADDING-TOP: 0;}</style></head>' .
    1422             '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
    1423         $expected = '<p style="text-align: center; padding-bottom: 1px; padding-top: 0;">';
    1424         $this->subject->setHtml($html);
    1425 
    1426         self::assertContains(
    1427             $expected,
    1428             $this->subject->emogrify()
    1429         );
    1430     }
    1431 
    1432     /**
    1433      * Passed in CSS sets the order, but style block CSS overrides values.
    1434      * @test
    1435      */
    1436     public function emogrifyMergesCssWithMixedCaseAttribute()
    1437     {
    1438         $css = 'p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}';
    1439         $html = $this->html5DocumentType .
    1440             '<html><head><style>#topWrap p {padding-bottom: 3px;PADDING-TOP: 1px;}</style></head>' .
    1441             '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
    1442         $expected = '<p style="text-align: center; margin: 0; padding-top: 1px; padding-bottom: 3px;">';
    1443         $this->subject->setHtml($html);
    1444         $this->subject->setCss($css);
    1445 
    1446         self::assertContains(
    1447             $expected,
    1448             $this->subject->emogrify()
     1477            '<p style="text-align: center; margin: 0; padding-top: 1px; padding-bottom: 3px;">',
     1478            $result
    14491479        );
    14501480    }
     
    14551485    public function emogrifyMergesCssWithMixedUnits()
    14561486    {
    1457         $css = 'p { margin: 1px; padding-bottom:0;}';
    1458         $html = $this->html5DocumentType .
     1487        $this->subject->setHtml(
    14591488            '<html><head><style>#topWrap p {margin:0;padding-bottom: 1px;}</style></head>' .
    1460             '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
    1461         $expected = '<p style="text-align: center; margin: 0; padding-bottom: 1px;">';
    1462         $this->subject->setHtml($html);
    1463         $this->subject->setCss($css);
    1464 
    1465         self::assertContains(
    1466             $expected,
    1467             $this->subject->emogrify()
    1468         );
     1489            '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>'
     1490        );
     1491        $this->subject->setCss('p { margin: 1px; padding-bottom:0;}');
     1492
     1493        $result = $this->subject->emogrify();
     1494
     1495        self::assertContains('<p style="text-align: center; margin: 0; padding-bottom: 1px;">', $result);
    14691496    }
    14701497
     
    14741501    public function emogrifyByDefaultRemovesElementsWithDisplayNoneFromExternalCss()
    14751502    {
    1476         $css = 'div.foo { display: none; }';
    1477         $html = $this->html5DocumentType . '<html><body><div class="bar"></div><div class="foo"></div></body></html>';
    1478 
    1479         $expected = '<div class="bar"></div>';
    1480 
    1481         $this->subject->setHtml($html);
    1482         $this->subject->setCss($css);
    1483 
    1484         self::assertContains(
    1485             $expected,
    1486             $this->subject->emogrify()
    1487         );
     1503        $this->subject->setHtml('<html><body><div class="bar"></div><div class="foo"></div></body></html>');
     1504        $this->subject->setCss('div.foo { display: none; }');
     1505
     1506        $result = $this->subject->emogrify();
     1507
     1508        self::assertContains('<div class="bar"></div>', $result);
    14881509    }
    14891510
     
    14931514    public function emogrifyByDefaultRemovesElementsWithDisplayNoneInStyleAttribute()
    14941515    {
    1495         $html = $this->html5DocumentType .
     1516        $this->subject->setHtml(
    14961517            '<html><body><div class="bar"></div><div class="foobar" style="display: none;"></div>' .
    1497             '</body></html>';
    1498 
    1499         $expected = '<div class="bar"></div>';
    1500 
    1501         $this->subject->setHtml($html);
    1502 
    1503         self::assertContains(
    1504             $expected,
    1505             $this->subject->emogrify()
    1506         );
     1518            '</body></html>'
     1519        );
     1520
     1521        $result = $this->subject->emogrify();
     1522
     1523        self::assertContains('<div class="bar"></div>', $result);
    15071524    }
    15081525
     
    15121529    public function emogrifyAfterDisableInvisibleNodeRemovalPreservesInvisibleElements()
    15131530    {
    1514         $css = 'div.foo { display: none; }';
    1515         $html = $this->html5DocumentType . '<html><body><div class="bar"></div><div class="foo"></div></body></html>';
    1516 
    1517         $expected = '<div class="foo" style="display: none;">';
    1518 
    1519         $this->subject->setHtml($html);
    1520         $this->subject->setCss($css);
     1531        $this->subject->setHtml('<html><body><div class="bar"></div><div class="foo"></div></body></html>');
     1532        $this->subject->setCss('div.foo { display: none; }');
     1533
    15211534        $this->subject->disableInvisibleNodeRemoval();
    1522 
    1523         self::assertContains(
    1524             $expected,
    1525             $this->subject->emogrify()
    1526         );
     1535        $result = $this->subject->emogrify();
     1536
     1537        self::assertContains('<div class="foo" style="display: none;">', $result);
    15271538    }
    15281539
     
    15321543    public function emogrifyKeepsCssMediaQueriesWithCssCommentAfterMediaQuery()
    15331544    {
    1534         $css = '@media only screen and (max-width: 480px) { body { color: #ffffff } /* some comment */ }';
    1535         $html = $this->html5DocumentType . '<html><body></body></html>';
    1536 
    1537         $expected = '@media only screen and (max-width: 480px)';
    1538         $this->subject->setHtml($html);
    1539         $this->subject->setCss($css);
    1540 
    1541         self::assertContains(
    1542             $expected,
    1543             $this->subject->emogrify()
    1544         );
     1545        $this->subject->setHtml('<html><body></body></html>');
     1546        $this->subject->setCss(
     1547            '@media only screen and (max-width: 480px) { body { color: #ffffff } /* some comment */ }'
     1548        );
     1549
     1550        $result = $this->subject->emogrify();
     1551
     1552        self::assertContains('@media only screen and (max-width: 480px)', $result);
    15451553    }
    15461554
     
    15521560        $this->subject->setHtml($this->xhtml1StrictDocumentType . '<html><body><br/></body></html>');
    15531561
    1554         self::assertContains(
    1555             '<body><br></body>',
    1556             $this->subject->emogrify()
    1557         );
     1562        $result = $this->subject->emogrify();
     1563
     1564        self::assertContains('<body><br></body>', $result);
    15581565    }
    15591566
     
    15651572        $this->subject->setHtml($this->html5DocumentType . '<html><body><br></body></html>');
    15661573
    1567         self::assertContains(
    1568             '<body><br></body>',
    1569             $this->subject->emogrify()
    1570         );
     1574        $result = $this->subject->emogrify();
     1575
     1576        self::assertContains('<body><br></body>', $result);
    15711577    }
    15721578
     
    15781584        $this->subject->setHtml($this->html5DocumentType . '<html><body><br/></body></html>');
    15791585
    1580         self::assertContains(
    1581             '<body><br></body>',
    1582             $this->subject->emogrify()
    1583         );
     1586        $result = $this->subject->emogrify();
     1587
     1588        self::assertContains('<body><br></body>', $result);
    15841589    }
    15851590
     
    15891594    public function emogrifyAutomaticallyClosesUnclosedTag()
    15901595    {
    1591         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></body></html>');
    1592 
    1593         self::assertContains(
    1594             '<body><p></p></body>',
    1595             $this->subject->emogrify()
    1596         );
     1596        $this->subject->setHtml('<html><body><p></body></html>');
     1597
     1598        $result = $this->subject->emogrify();
     1599
     1600        self::assertContains('<body><p></p></body>', $result);
    15971601    }
    15981602
     
    16021606    public function emogrifyReturnsCompleteHtmlDocument()
    16031607    {
    1604         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
     1608        $this->subject->setHtml('<html><body><p></p></body></html>');
     1609
     1610        $result = $this->subject->emogrify();
    16051611
    16061612        self::assertSame(
     
    16101616            '<body><p></p></body>' . self::LF .
    16111617            '</html>' . self::LF,
    1612             $this->subject->emogrify()
     1618            $result
    16131619        );
    16141620    }
     
    16191625    public function emogrifyBodyContentReturnsBodyContentFromHtml()
    16201626    {
    1621         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
     1627        $this->subject->setHtml('<html><body><p></p></body></html>');
     1628
     1629        $result = $this->subject->emogrifyBodyContent();
     1630
    16221631        self::assertSame(
    16231632            '<p></p>' . self::LF,
    1624             $this->subject->emogrifyBodyContent()
     1633            $result
    16251634        );
    16261635    }
     
    16321641    {
    16331642        $this->subject->setHtml('<p></p>');
     1643
     1644        $result = $this->subject->emogrifyBodyContent();
     1645
    16341646        self::assertSame(
    16351647            '<p></p>' . self::LF,
    1636             $this->subject->emogrifyBodyContent()
    1637         );
     1648            $result
     1649        );
     1650    }
     1651
     1652    /**
     1653     * @test
     1654     */
     1655    public function emogrifyBodyContentKeepsUtf8Umlauts()
     1656    {
     1657        $umlautString = 'Küss die Hand, schöne Frau.';
     1658        $this->subject->setHtml('<p>' . $umlautString . '</p>');
     1659
     1660        $result = $this->subject->emogrifyBodyContent();
     1661
     1662        self::assertContains($umlautString, $result);
    16381663    }
    16391664
     
    16431668    public function importantInExternalCssOverwritesInlineCss()
    16441669    {
    1645         $css = 'p { margin: 1px !important; }';
    1646         $html = $this->html5DocumentType .
    1647             '<html><head</head><body><p style="margin: 2px;">some content</p></body></html>';
    1648         $expected = '<p style="margin: 1px !important;">';
    1649         $this->subject->setHtml($html);
    1650         $this->subject->setCss($css);
     1670        $this->subject->setHtml('<html><head</head><body><p style="margin: 2px;">some content</p></body></html>');
     1671        $this->subject->setCss('p { margin: 1px !important; }');
     1672
     1673        $result = $this->subject->emogrify();
     1674
     1675        self::assertContains('<p style="margin: 1px !important;">', $result);
     1676    }
     1677
     1678    /**
     1679     * @test
     1680     */
     1681    public function importantInExternalCssKeepsInlineCssForOtherAttributes()
     1682    {
     1683        $this->subject->setHtml(
     1684            '<html><head</head><body><p style="margin: 2px; text-align: center;">some content</p></body></html>'
     1685        );
     1686        $this->subject->setCss('p { margin: 1px !important; }');
     1687
     1688        $result = $this->subject->emogrify();
     1689
     1690        self::assertContains('<p style="margin: 1px !important; text-align: center;">', $result);
     1691    }
     1692
     1693    /**
     1694     * @test
     1695     */
     1696    public function emogrifyHandlesImportantStyleTagCaseInsensitive()
     1697    {
     1698        $this->subject->setHtml('<html><head</head><body><p style="margin: 2px;">some content</p></body></html>');
     1699        $this->subject->setCss('p { margin: 1px !ImPorTant; }');
     1700
     1701        $result = $this->subject->emogrify();
     1702
     1703        self::assertContains('<p style="margin: 1px !ImPorTant;">', $result);
     1704    }
     1705
     1706    /**
     1707     * @test
     1708     */
     1709    public function secondImportantStyleOverwritesFirstOne()
     1710    {
     1711        $this->subject->setHtml('<html><head</head><body><p>some content</p></body></html>');
     1712        $this->subject->setCss('p { margin: 1px !important; } p { margin: 2px !important; }');
     1713
     1714        $result = $this->subject->emogrify();
    16511715
    16521716        self::assertContains(
    1653             $expected,
    1654             $this->subject->emogrify()
    1655         );
    1656     }
    1657 
    1658     /**
    1659      * @test
    1660      */
    1661     public function importantInExternalCssKeepsInlineCssForOtherAttributes()
    1662     {
    1663         $css = 'p { margin: 1px !important; }';
    1664         $html = $this->html5DocumentType .
    1665             '<html><head</head><body><p style="margin: 2px; text-align: center;">some content</p></body></html>';
    1666         $expected = '<p style="margin: 1px !important; text-align: center;">';
    1667         $this->subject->setHtml($html);
    1668         $this->subject->setCss($css);
     1717            '<p style="margin: 2px !important;">',
     1718            $result
     1719        );
     1720    }
     1721
     1722    /**
     1723     * @test
     1724     */
     1725    public function secondNonImportantStyleOverwritesFirstOne()
     1726    {
     1727        $this->subject->setHtml('<html><head</head><body><p>some content</p></body></html>');
     1728        $this->subject->setCss('p { margin: 1px; } p { margin: 2px; }');
     1729
     1730        $result = $this->subject->emogrify();
    16691731
    16701732        self::assertContains(
    1671             $expected,
    1672             $this->subject->emogrify()
    1673         );
    1674     }
    1675 
    1676     /**
    1677      * @test
    1678      */
    1679     public function emogrifyHandlesImportantStyleTagCaseInsensitive()
    1680     {
    1681         $css = 'p { margin: 1px !ImPorTant; }';
    1682         $html = $this->html5DocumentType .
    1683             '<html><head</head><body><p style="margin: 2px;">some content</p></body></html>';
    1684         $expected = '<p style="margin: 1px !ImPorTant;">';
    1685         $this->subject->setHtml($html);
    1686         $this->subject->setCss($css);
     1733            '<p style="margin: 2px;">',
     1734            $result
     1735        );
     1736    }
     1737
     1738    /**
     1739     * @test
     1740     */
     1741    public function secondNonImportantStyleNotOverwritesFirstImportantOne()
     1742    {
     1743        $this->subject->setHtml('<html><head</head><body><p>some content</p></body></html>');
     1744        $this->subject->setCss('p { margin: 1px !important; } p { margin: 2px; }');
     1745
     1746        $result = $this->subject->emogrify();
    16871747
    16881748        self::assertContains(
    1689             $expected,
    1690             $this->subject->emogrify()
     1749            '<p style="margin: 1px !important;">',
     1750            $result
    16911751        );
    16921752    }
     
    16991759        $uselessQuery = '@media all and (max-width: 500px) { em { color:red; } }';
    17001760        $this->subject->setCss($uselessQuery);
    1701 
    1702         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
    1703         $result = $this->subject->emogrify();
    1704 
    1705         self::assertNotContains(
    1706             $uselessQuery,
    1707             $result
    1708         );
     1761        $this->subject->setHtml('<html><body><p></p></body></html>');
     1762
     1763        $result = $this->subject->emogrify();
     1764
     1765        self::assertNotContains($uselessQuery, $result);
    17091766    }
    17101767
     
    17161773        $usefulQuery = '@media all and (max-width: 500px) { p { color:red; } }';
    17171774        $this->subject->setCss($usefulQuery);
    1718 
    1719         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
    1720         $result = $this->subject->emogrify();
    1721 
    1722         self::assertContains(
    1723             $usefulQuery,
    1724             $result
    1725         );
     1775        $this->subject->setHtml('<html><body><p></p></body></html>');
     1776
     1777        $result = $this->subject->emogrify();
     1778
     1779        self::assertContains($usefulQuery, $result);
    17261780    }
    17271781
     
    17311785    public function importantStyleRuleFromInlineCssOverwritesImportantStyleRuleFromExternalCss()
    17321786    {
    1733         $css = 'p { margin: 1px !important; padding: 1px;}';
    1734         $html = $this->html5DocumentType .
    1735             '<html><head</head><body><p style="margin: 2px !important; text-align: center;">some content</p>' .
    1736             '</body></html>';
    1737         $expected = '<p style="margin: 2px !important; text-align: center; padding: 1px;">';
    1738         $this->subject->setHtml($html);
    1739         $this->subject->setCss($css);
    1740 
    1741         self::assertContains(
    1742             $expected,
    1743             $this->subject->emogrify()
    1744         );
     1787        $this->subject->setHtml(
     1788            '<html><head</head><body>' .
     1789            '<p style="margin: 2px !important; text-align: center;">some content</p>' .
     1790            '</body></html>'
     1791        );
     1792        $this->subject->setCss('p { margin: 1px !important; padding: 1px;}');
     1793
     1794        $result = $this->subject->emogrify();
     1795
     1796        self::assertContains('<p style="margin: 2px !important; text-align: center; padding: 1px;">', $result);
    17451797    }
    17461798
     
    17501802    public function addExcludedSelectorRemovesMatchingElementsFromEmogrification()
    17511803    {
    1752         $css = 'p { margin: 0; }';
    1753         $this->subject->setHtml($this->html5DocumentType . '<html><body><p class="x"></p></body></html>');
    1754         $this->subject->setCss($css);
     1804        $this->subject->setHtml('<html><body><p class="x"></p></body></html>');
     1805        $this->subject->setCss('p { margin: 0; }');
     1806
    17551807        $this->subject->addExcludedSelector('p.x');
    1756         $html = $this->subject->emogrify();
    1757 
    1758         self::assertContains(
    1759             '<p class="x"></p>',
    1760             $html
    1761         );
     1808        $result = $this->subject->emogrify();
     1809
     1810        self::assertContains('<p class="x"></p>', $result);
    17621811    }
    17631812
     
    17671816    public function addExcludedSelectorExcludesMatchingElementEventWithWhitespaceAroundSelector()
    17681817    {
    1769         $css = 'p { margin: 0; }';
    1770         $this->subject->setHtml($this->html5DocumentType . '<html><body><p class="x"></p></body></html>');
    1771         $this->subject->setCss($css);
     1818        $this->subject->setHtml('<html><body><p class="x"></p></body></html>');
     1819        $this->subject->setCss('p { margin: 0; }');
     1820
    17721821        $this->subject->addExcludedSelector(' p.x ');
    1773         $html = $this->subject->emogrify();
    1774 
    1775         self::assertContains(
    1776             '<p class="x"></p>',
    1777             $html
    1778         );
     1822        $result = $this->subject->emogrify();
     1823
     1824        self::assertContains('<p class="x"></p>', $result);
    17791825    }
    17801826
     
    17841830    public function addExcludedSelectorKeepsNonMatchingElementsInEmogrification()
    17851831    {
    1786         $css = 'p { margin: 0; }';
    1787         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
    1788         $this->subject->setCss($css);
     1832        $this->subject->setHtml('<html><body><p></p></body></html>');
     1833        $this->subject->setCss('p { margin: 0; }');
     1834
    17891835        $this->subject->addExcludedSelector('p.x');
    1790         $html = $this->subject->emogrify();
    1791 
    1792         self::assertContains(
    1793             '<p style="margin: 0;"></p>',
    1794             $html
    1795         );
     1836        $result = $this->subject->emogrify();
     1837
     1838        self::assertContains('<p style="margin: 0;"></p>', $result);
    17961839    }
    17971840
     
    18011844    public function removeExcludedSelectorGetsMatchingElementsToBeEmogrifiedAgain()
    18021845    {
    1803         $css = 'p { margin: 0; }';
    1804         $this->subject->setHtml($this->html5DocumentType . '<html><body><p class="x"></p></body></html>');
    1805         $this->subject->setCss($css);
     1846        $this->subject->setHtml('<html><body><p class="x"></p></body></html>');
     1847        $this->subject->setCss('p { margin: 0; }');
     1848
    18061849        $this->subject->addExcludedSelector('p.x');
    18071850        $this->subject->removeExcludedSelector('p.x');
    1808         $html = $this->subject->emogrify();
    1809 
    1810         self::assertContains(
    1811             '<p class="x" style="margin: 0;"></p>',
    1812             $html
    1813         );
     1851
     1852        $result = $this->subject->emogrify();
     1853
     1854        self::assertContains('<p class="x" style="margin: 0;"></p>', $result);
    18141855    }
    18151856
     
    18211862        $emptyQuery = '@media all and (max-width: 500px) { }';
    18221863        $this->subject->setCss($emptyQuery);
    1823 
    1824         $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
    1825         $result = $this->subject->emogrify();
    1826 
    1827         self::assertNotContains(
    1828             $emptyQuery,
    1829             $result
    1830         );
     1864        $this->subject->setHtml('<html><body><p></p></body></html>');
     1865
     1866        $result = $this->subject->emogrify();
     1867
     1868        self::assertNotContains($emptyQuery, $result);
    18311869    }
    18321870
     
    18411879            '}';
    18421880        $this->subject->setCss($css);
    1843         $this->subject->setHtml($this->html5DocumentType . '<html><body>' .
     1881        $this->subject->setHtml(
     1882            '<html><body>' .
    18441883            '<p class="medium">medium</p>' .
    18451884            '<p class="small">small</p>' .
    1846             '</body></html>');
     1885            '</body></html>'
     1886        );
    18471887
    18481888        $result = $this->subject->emogrify();
     
    18651905        $this->subject->setCss($css);
    18661906        $this->subject->setHtml(
    1867             $this->html5DocumentType . '<html><body>' .
     1907            '<html><body>' .
    18681908            '<p class="medium">medium</p>' .
    18691909            '<p class="small">small</p>' .
     
    18851925    {
    18861926        $css = "@media all {\n" .
    1887             ".medium {font-size:18px;\n" .
     1927            ".medium {font-size:18px;}\n" .
    18881928            ".small {font-size:14px;}\n" .
    18891929            '}' .
     
    18941934        $this->subject->setCss($css);
    18951935        $this->subject->setHtml(
    1896             $this->html5DocumentType . '<html><body>' .
     1936            '<html><body>' .
    18971937            '<p class="medium">medium</p>' .
    18981938            '<p class="small">small</p>' .
     
    19281968    public function dataUrisAreConserved($dataUriMediaType)
    19291969    {
    1930         $html = $this->html5DocumentType . '<html></html>';
    1931         $this->subject->setHtml($html);
     1970        $this->subject->setHtml('<html></html>');
    19321971        $styleRule = 'background-image: url(data:image/png' . $dataUriMediaType .
    19331972            ',iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAABUk' .
     
    19461985        );
    19471986    }
     1987
     1988    /**
     1989     * Data provider for CSS to HTML mapping.
     1990     *
     1991     * @return string[][]
     1992     */
     1993    public function matchingCssToHtmlMappingDataProvider()
     1994    {
     1995        return [
     1996            'background-color => bgcolor'
     1997                => ['<p>hi</p>', 'p {background-color: red;}', 'p', 'bgcolor="red"'],
     1998            'background-color (with !important) => bgcolor'
     1999                => ['<p>hi</p>', 'p {background-color: red !important;}', 'p', 'bgcolor="red"'],
     2000            'p.text-align => align'
     2001                => ['<p>hi</p>', 'p {text-align: justify;}', 'p', 'align="'],
     2002            'div.text-align => align'
     2003                => ['<div>hi</div>', 'div {text-align: justify;}', 'div', 'align="'],
     2004            'td.text-align => align'
     2005                => ['<table><tr><td>hi</td></tr></table>', 'td {text-align: justify;}', 'td', 'align="'],
     2006            'text-align: left => align=left'
     2007                => ['<p>hi</p>', 'p {text-align: left;}', 'p', 'align="left"'],
     2008            'text-align: right => align=right'
     2009                => ['<p>hi</p>', 'p {text-align: right;}', 'p', 'align="right"'],
     2010            'text-align: center => align=center'
     2011                => ['<p>hi</p>', 'p {text-align: center;}', 'p', 'align="center"'],
     2012            'text-align: justify => align:justify'
     2013                => ['<p>hi</p>', 'p {text-align: justify;}', 'p', 'align="justify"'],
     2014            'img.float: right => align=right'
     2015                => ['<img>', 'img {float: right;}', 'img', 'align="right"'],
     2016            'img.float: left => align=left'
     2017                => ['<img>', 'img {float: left;}', 'img', 'align="left"'],
     2018            'table.float: right => align=right'
     2019                => ['<table></table>', 'table {float: right;}', 'table', 'align="right"'],
     2020            'table.float: left => align=left'
     2021                => ['<table></table>', 'table {float: left;}', 'table', 'align="left"'],
     2022            'table.border-spacing: 0 => cellspacing=0'
     2023                => ['<table><tr><td></td></tr></table>', 'table {border-spacing: 0;}', 'table', 'cellspacing="0"'],
     2024            'background => bgcolor'
     2025                => ['<p>Bonjour</p>', 'p {background: red top;}', 'p', 'bgcolor="red"'],
     2026            'width with px'
     2027                => ['<p>Hello</p>', 'p {width: 100px;}', 'p', 'width="100"'],
     2028            'width with %'
     2029                => ['<p>Hello</p>', 'p {width: 50%;}', 'p', 'width="50%"'],
     2030            'height with px'
     2031                => ['<p>Hello</p>', 'p {height: 100px;}', 'p', 'height="100"'],
     2032            'height with %'
     2033                => ['<p>Hello</p>', 'p {height: 50%;}', 'p', 'height="50%"'],
     2034            'img.margin: 0 auto (= horizontal centering) => align=center'
     2035                => ['<img>', 'img {margin: 0 auto;}', 'img', 'align="center"'],
     2036            'img.margin: auto (= horizontal centering) => align=center'
     2037                => ['<img>', 'img {margin: auto;}', 'img', 'align="center"'],
     2038            'img.margin: 10 auto 30 auto (= horizontal centering) => align=center'
     2039                => ['<img>', 'img {margin: 10 auto 30 auto;}', 'img', 'align="center"'],
     2040            'table.margin: 0 auto (= horizontal centering) => align=center'
     2041                => ['<table></table>', 'table {margin: 0 auto;}', 'table', 'align="center"'],
     2042            'table.margin: auto (= horizontal centering) => align=center'
     2043                => ['<table></table>', 'table {margin: auto;}', 'table', 'align="center"'],
     2044            'table.margin: 10 auto 30 auto (= horizontal centering) => align=center'
     2045                => ['<table></table>', 'table {margin: 10 auto 30 auto;}', 'table', 'align="center"'],
     2046            'img.border: none => border=0'
     2047                => ['<img>', 'img {border: none;}', 'img', 'border="0"'],
     2048            'img.border: 0 => border=0'
     2049                => ['<img>', 'img {border: none;}', 'img', 'border="0"'],
     2050            'table.border: none => border=0'
     2051                => ['<table></table>', 'table {border: none;}', 'table', 'border="0"'],
     2052            'table.border: 0 => border=0'
     2053                => ['<table></table>', 'table {border: none;}', 'table', 'border="0"'],
     2054        ];
     2055    }
     2056
     2057    /**
     2058     * @test
     2059     * @param string $body          The HTML
     2060     * @param string $css           The complete CSS
     2061     * @param string $tagName       The name of the tag that should be modified
     2062     * @param string $attributes    The attributes that are expected on the element
     2063     *
     2064     * @dataProvider matchingCssToHtmlMappingDataProvider
     2065     */
     2066    public function emogrifierMapsSuitableCssToHtmlIfFeatureIsEnabled($body, $css, $tagName, $attributes)
     2067    {
     2068        $this->subject->setHtml('<html><body>' . $body . '</body></html>');
     2069        $this->subject->setCss($css);
     2070
     2071        $this->subject->enableCssToHtmlMapping();
     2072        $html = $this->subject->emogrify();
     2073
     2074        self::assertContains(
     2075            '<' . $tagName . ' ' . $attributes,
     2076            $html
     2077        );
     2078    }
     2079
     2080    /**
     2081     * Data provider for CSS to HTML mapping.
     2082     *
     2083     * @return string[][]
     2084     */
     2085    public function notMatchingCssToHtmlMappingDataProvider()
     2086    {
     2087        return [
     2088            'background URL'
     2089                => ['<p>Hello</p>', 'p {background: url(bg.png);}', 'bgcolor'],
     2090            'background URL with position'
     2091                => ['<p>Hello</p>', 'p {background: url(bg.png) top;}', 'bgcolor'],
     2092            'img.margin: 10 5 30 auto (= no horizontal centering)'
     2093                => ['<img>', 'img {margin: 10 5 30 auto;}', 'align'],
     2094            'p.margin: auto'
     2095                => ['<p>Bonjour</p>', 'p {margin: auto;}', 'align'],
     2096            'p.border: none'
     2097                => ['<p>Bonjour</p>', 'p {border: none;}', 'border'],
     2098            'img.border: 1px solid black'
     2099                => ['<p>Bonjour</p>', 'p {border: 1px solid black;}', 'border'],
     2100            'span.text-align'
     2101                => ['<span>hi</span>', 'span {text-align: justify;}', 'align'],
     2102            'text-align: inherit'
     2103                => ['<p>hi</p>', 'p {text-align: inherit;}', 'align'],
     2104            'span.float'
     2105                => ['<span>hi</span>', 'span {float: right;}', 'align'],
     2106            'float: none'
     2107                => ['<table></table>', 'table {float: none;}', 'align'],
     2108            'p.border-spacing'
     2109                => ['<p>Hello</p>', 'p {border-spacing: 5px;}', 'cellspacing'],
     2110            'height: auto'
     2111                => ['<img src="logo.png" alt="">', 'img {width: 110px; height: auto;}', 'height'],
     2112            'width: auto'
     2113                => ['<img src="logo.png" alt="">', 'img {width: auto; height: 110px;}', 'width'],
     2114        ];
     2115    }
     2116
     2117    /**
     2118     * @test
     2119     * @param string $body      the HTML
     2120     * @param string $css       the complete CSS
     2121     * @param string $attribute the attribute that must not be present on this element
     2122     *
     2123     * @dataProvider notMatchingCssToHtmlMappingDataProvider
     2124     */
     2125    public function emogrifierNotMapsUnsuitableCssToHtmlIfFeatureIsEnabled($body, $css, $attribute)
     2126    {
     2127        $this->subject->setHtml('<html><body>' . $body . '</body></html>');
     2128        $this->subject->setCss($css);
     2129
     2130        $this->subject->enableCssToHtmlMapping();
     2131        $html = $this->subject->emogrify();
     2132
     2133        self::assertNotContains(
     2134            $attribute . '="',
     2135            $html
     2136        );
     2137    }
     2138
     2139    /**
     2140     * @test
     2141     */
     2142    public function emogrifierNotMapsCssToHtmlIfFeatureIsNotEnabled()
     2143    {
     2144        $this->subject->setHtml('<html><body><img></body></html>');
     2145        $this->subject->setCss('img {float: right;}');
     2146
     2147        $html = $this->subject->emogrify();
     2148
     2149        self::assertNotContains(
     2150            '<img align="right',
     2151            $html
     2152        );
     2153    }
     2154
     2155    /**
     2156     * @test
     2157     */
     2158    public function emogrifierIgnoresPseudoClassCombinedWithPseudoElement()
     2159    {
     2160        $this->subject->setHtml('<html><body><div></div></body></html>');
     2161        $this->subject->setCss('div:last-child::after {float: right;}');
     2162
     2163        $html = $this->subject->emogrify();
     2164
     2165        self::assertContains('<div></div>', $html);
     2166    }
    19482167}
  • _plugins_/emogrifier/trunk/lib/emogrifier/composer.json

    r98616 r106563  
    22    "name": "pelago/emogrifier",
    33    "description": "Converts CSS styles into inline style attributes in your HTML code",
    4     "tags": ["email", "css", "pre-processing"],
     4    "keywords": [
     5        "email",
     6        "css",
     7        "pre-processing"
     8    ],
     9    "homepage": "https://www.myintervals.com/emogrifier.php",
    510    "license": "MIT",
    6     "homepage": "http://www.pelagodesign.com/sidecar/emogrifier/",
    711    "authors": [
    812        {
     
    2630    ],
    2731    "require": {
    28         "php": ">=5.4.0",
    29         "ext-mbstring": "*"
     32        "php": "^5.4.0 || ~7.0.0 || ~7.1.0 || ~7.2.0"
    3033    },
    3134    "require-dev": {
    32         "squizlabs/php_codesniffer": "2.3.4",
    33         "typo3-ci/typo3sniffpool": "2.1.1",
    34         "phpunit/phpunit": "4.8.11"
     35        "squizlabs/php_codesniffer": "2.6.0",
     36        "phpunit/phpunit": "4.8.27"
    3537    },
    3638    "autoload": {
     
    4143    "extra": {
    4244        "branch-alias": {
    43             "dev-master": "1.1.x-dev"
     45            "dev-master": "1.3.x-dev"
    4446        }
    4547    }
Note: See TracChangeset for help on using the changeset viewer.