From 396c4564b4327156b3b84fe3bf09e98fe4383491 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Mon, 4 Jul 2016 15:06:28 +0000 Subject: [PATCH] HTML formatting: TWO fixes in one! Fixed a bug introduced in 2.3.0-beta: the stylesheet cannot be defined within the email templates (aka ActionEmail) anymore. Instead, a default (ready for use) stylesheet is provided into /css/email.css and it can be overriden by the configuration parameter email_css. The fix consists in transforming the stylesheet into inline style... which fixes a limitation of gmail and Outlook that support only the inline styles. The implementation relies on a new library: emogrifier. This library has been changed (home-made utility) to be compatible with PHP 5.3 (declaration of arrays). SVN:trunk[4277] --- core/action.class.inc.php | 6 +- core/config.class.inc.php | 8 + core/email.class.inc.php | 8 +- css/email.css | 12 + lib/emogrifier/.gitignore | 24 + lib/emogrifier/.travis.yml | 31 + lib/emogrifier/CHANGELOG.md | 92 ++ lib/emogrifier/CONTRIBUTING.md | 78 ++ lib/emogrifier/Classes/Emogrifier.php | 1020 ++++++++++++++ .../Standards/Emogrifier/ruleset.xml | 136 ++ lib/emogrifier/LICENSE | 21 + lib/emogrifier/README.md | 198 +++ lib/emogrifier/Tests/Unit/EmogrifierTest.php | 1221 +++++++++++++++++ lib/emogrifier/composer.json | 46 + setup/licenses/community-licences.xml | 24 + 15 files changed, 2922 insertions(+), 3 deletions(-) create mode 100644 css/email.css create mode 100644 lib/emogrifier/.gitignore create mode 100644 lib/emogrifier/.travis.yml create mode 100644 lib/emogrifier/CHANGELOG.md create mode 100644 lib/emogrifier/CONTRIBUTING.md create mode 100644 lib/emogrifier/Classes/Emogrifier.php create mode 100644 lib/emogrifier/Configuration/PhpCodeSniffer/Standards/Emogrifier/ruleset.xml create mode 100644 lib/emogrifier/LICENSE create mode 100644 lib/emogrifier/README.md create mode 100644 lib/emogrifier/Tests/Unit/EmogrifierTest.php create mode 100644 lib/emogrifier/composer.json diff --git a/core/action.class.inc.php b/core/action.class.inc.php index 5e65a974b..df5367985 100644 --- a/core/action.class.inc.php +++ b/core/action.class.inc.php @@ -324,6 +324,8 @@ class ActionEmail extends ActionNotification if (isset($sSubject)) $oLog->Set('subject', $sSubject); if (isset($sBody)) $oLog->Set('body', $sBody); } + $sStyles = file_get_contents(APPROOT.'css/email.css'); + $sStyles .= MetaModel::GetConfig()->Get('email_css'); $oEmail = new EMail(); @@ -344,7 +346,7 @@ class ActionEmail extends ActionNotification $sTestBody .= "\n"; $sTestBody .= "

\n"; $sTestBody .= "\n"; - $oEmail->SetBody($sTestBody); + $oEmail->SetBody($sTestBody, 'text/html', $sStyles); $oEmail->SetRecipientTO($this->Get('test_recipient')); $oEmail->SetRecipientFrom($this->Get('test_recipient')); $oEmail->SetReferences($sReference); @@ -353,7 +355,7 @@ class ActionEmail extends ActionNotification else { $oEmail->SetSubject($sSubject); - $oEmail->SetBody($sBody); + $oEmail->SetBody($sBody, 'text/html', $sStyles); $oEmail->SetRecipientTO($sTo); $oEmail->SetRecipientCC($sCC); $oEmail->SetRecipientBCC($sBCC); diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 05dc55c58..5ea6218cf 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -404,6 +404,14 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ), + 'email_css' => array( + 'type' => 'string', + 'description' => 'CSS that will override the standard stylesheet used for the notifications', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), 'apc_cache.enabled' => array( 'type' => 'bool', 'description' => 'If set, the APC cache is allowed (the PHP extension must also be active)', diff --git a/core/email.class.inc.php b/core/email.class.inc.php index 3586d4569..d0b8e14f9 100644 --- a/core/email.class.inc.php +++ b/core/email.class.inc.php @@ -305,8 +305,14 @@ IssueLog::Info(__METHOD__.' '.$this->m_oMessage->toString()); $this->AddToHeader('References', $sReferences); } - public function SetBody($sBody, $sMimeType = 'text/html') + public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null) { + if (($sMimeType === 'text/html') && ($sCustomStyles !== null)) + { + require_once(APPROOT.'lib/emogrifier/classes/emogrifier.php'); + $emogrifier = new \Pelago\Emogrifier($sBody, $sCustomStyles); + $sBody = $emogrifier->emogrify(); // Adds html/body tags if not already present + } $this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType); $this->m_oMessage->setBody($sBody, $sMimeType); } diff --git a/css/email.css b/css/email.css new file mode 100644 index 000000000..3a27e6d01 --- /dev/null +++ b/css/email.css @@ -0,0 +1,12 @@ +/* Note: only CSS1 is supported here (see the limitations of emogrifier: https://github.com/jjriv/emogrifier/) */ +.caselog_header { + padding: 3px; + border-top: 1px solid #fff; + background-color: #ddd; + padding-left: 16px; + width: 100%; +} +.caselog_header_date { +} +.caselog_header_user { +} \ No newline at end of file diff --git a/lib/emogrifier/.gitignore b/lib/emogrifier/.gitignore new file mode 100644 index 000000000..6be926125 --- /dev/null +++ b/lib/emogrifier/.gitignore @@ -0,0 +1,24 @@ +######################### +# global ignore file +######################## +# ignoring temporary files (left by e.g. vim) +# ignoring by common IDE's used directories/files +# dont ignore .rej and .orig as we want to see/clean files after conflict resolution +# +# for local exclude patterns please edit .git/info/exclude +# +*~ +*.bak +*.idea +*.project +*.swp +.buildpath +.cache +.project +.session +.settings +.TemporaryItems +.webprj +nbproject +/vendor/ +composer.lock diff --git a/lib/emogrifier/.travis.yml b/lib/emogrifier/.travis.yml new file mode 100644 index 000000000..961cb92de --- /dev/null +++ b/lib/emogrifier/.travis.yml @@ -0,0 +1,31 @@ +sudo: false + +language: php + +cache: + directories: + - vendor + +env: + global: + secure: nOIIWvxRsDlkg+5H21dmVeqvFbweOAk3l3ZiyZO1m5XuGuuZR9yj10oOudee8m0hzJ7e9eoZ+dfB3t8lmK0fTRTB6w0G7RuGiQb89ief3Zhs1vOveYOgS5yfTMRym57iluxsLeCe7AxWmy7+0fWAvx1qL7bKp+THGK9yv/aj9eM= + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +before_script: + - composer install + - vendor/bin/phpcs --config-set encoding utf-8 + - if [ "$GITHUB_COMPOSER_AUTH" ]; then composer config -g github-oauth.github.com $GITHUB_COMPOSER_AUTH; fi + +script: + # Run PHP lint on all PHP files. + - find Classes/ Tests/ -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l + # Check the coding style. + - vendor/bin/phpcs --standard=Configuration/PhpCodeSniffer/Standards/Emogrifier/ Classes/ Tests/ + # Run the unit tests. + - vendor/bin/phpunit Tests/ diff --git a/lib/emogrifier/CHANGELOG.md b/lib/emogrifier/CHANGELOG.md new file mode 100644 index 000000000..fcad58b97 --- /dev/null +++ b/lib/emogrifier/CHANGELOG.md @@ -0,0 +1,92 @@ +# Emogrifier Change Log + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +Emogrifier is in a pre-1.0 state. This means that its APIs and behavior are +subject to breaking changes without deprecation notices. + + +## [1.0.0][] (2015-10-15) + +### Added +- Add branch alias ([#231](https://github.com/jjriv/emogrifier/pull/231)) +- Remove media queries which do not impact the document + ([#217](https://github.com/jjriv/emogrifier/pull/217)) +- Allow elements to be excluded from emogrification + ([#215](https://github.com/jjriv/emogrifier/pull/215)) +- Handle !important ([#214](https://github.com/jjriv/emogrifier/pull/214)) +- emogrifyBodyContent() method + ([#206](https://github.com/jjriv/emogrifier/pull/206)) +- Cache combinedStyles ([#211](https://github.com/jjriv/emogrifier/pull/211)) +- Allow user to define media types to keep + ([#200](https://github.com/jjriv/emogrifier/pull/200)) +- Ignore invalid CSS selectors + ([#194](https://github.com/jjriv/emogrifier/pull/194)) +- isRemoveDisplayNoneEnabled option + ([#162](https://github.com/jjriv/emogrifier/pull/162)) +- Allow disabling of "inline style" and "style block" parsing + ([#156](https://github.com/jjriv/emogrifier/pull/156)) +- Preserve @media if necessary + ([#62](https://github.com/jjriv/emogrifier/pull/62)) +- Add extraction of style blocks within the HTML +- Add several new pseudo-selectors (first-child, last-child, nth-child, + and nth-of-type) + + +### Changed +- Make HTML5 the default document type + ([#245](https://github.com/jjriv/emogrifier/pull/245)) +- Make copyCssWithMediaToStyleNode private + ([#218](https://github.com/jjriv/emogrifier/pull/218)) +- Stop encoding umlauts and dollar signs + ([#170](https://github.com/jjriv/emogrifier/pull/170)) +- Convert the classes to namespaces + ([#41](https://github.com/jjriv/emogrifier/pull/41)) + + +### Deprecated +- Support for PHP 5.4 will be removed in Emogrifier 2.0. + + +### Removed +- Drop support for PHP 5.3 + ([#114](https://github.com/jjriv/emogrifier/pull/114)) +- Support for character sets other than UTF-8 was removed. + + +### Fixed +- Fix failing tests on Windows due to line endings + ([#263](https://github.com/jjriv/emogrifier/pull/263)) +- Parsing CSS declaration blocks + ([#261](https://github.com/jjriv/emogrifier/pull/261)) +- Fix first-child and last-child selectors + ([#257](https://github.com/jjriv/emogrifier/pull/257)) +- Fix parsing of CSS for data URIs + ([#243](https://github.com/jjriv/emogrifier/pull/243)) +- Fix multi-line media queries + ([#241](https://github.com/jjriv/emogrifier/pull/241)) +- Keep CSS media queries even if followed by CSS comments + ([#201](https://github.com/jjriv/emogrifier/pull/201)) +- Fix CSS selectors with exact attribute only + ([#197](https://github.com/jjriv/emogrifier/pull/197)) +- Properly handle UTF-8 characters and entities + ([#189](https://github.com/jjriv/emogrifier/pull/189)) +- Add mbstring extension to composer.json + ([#93](https://github.com/jjriv/emogrifier/pull/93)) +- Prevent incorrectly capitalized CSS selectors from being stripped + ([#85](https://github.com/jjriv/emogrifier/pull/85)) +- Fix CSS selectors with exact attribute only + ([#197](https://github.com/jjriv/emogrifier/pull/197)) +- Wrong selector extraction from minified CSS + ([#69](https://github.com/jjriv/emogrifier/pull/69)) +- Restore libxml error handler state after clearing + ([#65](https://github.com/jjriv/emogrifier/pull/65)) +- Ignore all warnings produced by DOMDocument::loadHTML() + ([#63](https://github.com/jjriv/emogrifier/pull/63)) +- Style tags in HTML cause an Xpath invalid query error + ([#60](https://github.com/jjriv/emogrifier/pull/60)) +- Fix PHP warnings with PHP 5.5 + ([#26](https://github.com/jjriv/emogrifier/pull/26)) +- Make removal of invisible nodes operate in a case-insensitive manner +- Fix a bug that was overwriting existing inline styles from the original HTML diff --git a/lib/emogrifier/CONTRIBUTING.md b/lib/emogrifier/CONTRIBUTING.md new file mode 100644 index 000000000..95294bfb4 --- /dev/null +++ b/lib/emogrifier/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing to Emogrifier + +Those that wish to contribute bug fixes, new features, refactorings and +clean-up to Emogrifier are more than welcome. + +When you contribute, please take the following things into account: + + +## General workflow + +After you have submitted a pull request, the Emogrifier team will review your +changes. This will probably result in quite a few comments on ways to improve +your pull request. The Emogrifier project receives contributions from +developers around the world, so we need the code to be the most consistent, +readable, and maintainable that it can be. + +Please do not feel frustrated by this - instead please view this both as our +contribution to your pull request as well as a way to learn more about +improving code quality. + +If you would like to know whether an idea would fit in the general strategy of +the Emogrifier project or would like to get feedback on the best architecture +for your ideas, we propose you open a ticket first and discuss your ideas there +first before investing a lot of time in writing code. + + +## Install the development dependencies + +To install the development dependencies (PHPUnit and PHP_CodeSniffer), please +run the following command: + + composer install + + +## Unit-test your changes + +Please cover all changes with unit tests and make sure that your code does not +break any existing tests. We will only merge pull request that include full +code coverage of the fixed bugs and the new features. + +To run the existing PHPUnit tests, run this command: + + vendor/bin/phpunit Tests/ + + +## Coding Style + +Please use the same coding style (PSR-2) as the rest of the code. Indentation +is four spaces. + +We will only merge pull requests that follow the project's coding style. + +Please check your code with the provided PHP_CodeSniffer standard: + + vendor/bin/phpcs --standard=Configuration/PhpCodeSniffer/Standards/Emogrifier/ Classes/ Tests/ + +Please make your code clean, well-readable and easy to understand. + +If you add new methods or fields, please add proper PHPDoc for the new +methods/fields. Please use grammatically correct, complete sentences in the +code documentation. + + +## Git commits + +Git commits should have a <= 50 character summary, optionally followed by a +blank line and a more in depth description of 79 characters per line. + +[Please squash related commits together](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html). + +If you already have a commit and work on it, you can also +[amend the first commit](https://nathanhoad.net/git-amend-your-last-commit). + +Please use grammatically correct, complete sentences in the commit messages. + +Also, please prefix the subject line of the commit message with either +[FEATURE], [TASK], [BUGFIX] OR [CLEANUP]. This makes it faster to see what +a commit is about. \ No newline at end of file diff --git a/lib/emogrifier/Classes/Emogrifier.php b/lib/emogrifier/Classes/Emogrifier.php new file mode 100644 index 000000000..adf7bf639 --- /dev/null +++ b/lib/emogrifier/Classes/Emogrifier.php @@ -0,0 +1,1020 @@ + + * @author Roman Ožana + */ +class Emogrifier +{ + /** + * @var int + */ + const CACHE_KEY_CSS = 0; + /** + * @var int + */ + const CACHE_KEY_SELECTOR = 1; + /** + * @var int + */ + const CACHE_KEY_XPATH = 2; + /** + * @var int + */ + const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 3; + /** + * @var int + */ + const CACHE_KEY_COMBINED_STYLES = 4; + /** + * for calculating nth-of-type and nth-child selectors + * + * @var int + */ + const INDEX = 0; + /** + * for calculating nth-of-type and nth-child selectors + * + * @var int + */ + const MULTIPLIER = 1; + /** + * @var string + */ + const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/'; + /** + * @var string + */ + const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/'; + /** + * @var string + */ + const CONTENT_TYPE_META_TAG = ''; + /** + * @var string + */ + const DEFAULT_DOCUMENT_TYPE = ''; + /** + * @var string + */ + private $html = ''; + /** + * @var string + */ + private $css = ''; + /** + * @var bool[] + */ + private $excludedSelectors = array(); + /** + * @var string[] + */ + private $unprocessableHtmlTags = array('wbr'); + /** + * @var bool[] + */ + private $allowedMediaTypes = array('all' => true, 'screen' => true, 'print' => true); + /** + * @var array[] + */ + private $caches = array(self::CACHE_KEY_CSS => array(), self::CACHE_KEY_SELECTOR => array(), self::CACHE_KEY_XPATH => array(), self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => array(), self::CACHE_KEY_COMBINED_STYLES => array()); + /** + * the visited nodes with the XPath paths as array keys + * + * @var \DOMElement[] + */ + private $visitedNodes = array(); + /** + * the styles to apply to the nodes with the XPath paths as array keys for the outer array + * and the attribute names/values as key/value pairs for the inner array + * + * @var array[] + */ + private $styleAttributesForNodes = array(); + /** + * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved. + * If set to false, the value of the style attributes will be discarded. + * + * @var bool + */ + private $isInlineStyleAttributesParsingEnabled = true; + /** + * Determines whether the

target

'; + $this->subject->setHtml($html); + self::assertContains('

target

', $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyRemovesStyleNodes() + { + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + self::assertNotContains(''; + $this->subject->setHtml($html); + $hasError = false; + set_error_handler(function ($errorNumber, $errorMessage) use(&$hasError) { + if ($errorMessage === 'DOMXPath::query(): Invalid expression') { + return true; + } + $hasError = true; + return true; + }); + $this->subject->emogrify(); + restore_error_handler(); + self::assertFalse($hasError); + } + /** + * Data provider for things that should be left out when applying the CSS. + * + * @return array[] + */ + public function unneededCssThingsDataProvider() + { + return array('CSS comments with one asterisk' => array('p {color: #000;/* black */}', 'black'), 'CSS comments with two asterisks' => array('p {color: #000;/** black */}', 'black'), '@import directive' => array('@import "foo.css";', '@import'), 'style in "aural" media type rule' => array('@media aural {p {color: #000;}}', '#000'), 'style in "braille" media type rule' => array('@media braille {p {color: #000;}}', '#000'), 'style in "embossed" media type rule' => array('@media embossed {p {color: #000;}}', '#000'), 'style in "handheld" media type rule' => array('@media handheld {p {color: #000;}}', '#000'), 'style in "projection" media type rule' => array('@media projection {p {color: #000;}}', '#000'), 'style in "speech" media type rule' => array('@media speech {p {color: #000;}}', '#000'), 'style in "tty" media type rule' => array('@media tty {p {color: #000;}}', '#000'), 'style in "tv" media type rule' => array('@media tv {p {color: #000;}}', '#000')); + } + /** + * @test + * + * @param string $css + * @param string $markerNotExpectedInHtml + * + * @dataProvider unneededCssThingsDataProvider + */ + public function emogrifyFiltersUnneededCssThings($css, $markerNotExpectedInHtml) + { + $html = $this->html5DocumentType . '

foo

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertNotContains($markerNotExpectedInHtml, $this->subject->emogrify()); + } + /** + * Data provider for media rules. + * + * @return array[] + */ + public function mediaRulesDataProvider() + { + return array('style in "only all" media type rule' => array('@media only all {p {color: #000;}}'), 'style in "only screen" media type rule' => array('@media only screen {p {color: #000;}}'), 'style in media type rule' => array('@media {p {color: #000;}}'), 'style in "screen" media type rule' => array('@media screen {p {color: #000;}}'), 'style in "print" media type rule' => array('@media print {p {color: #000;}}'), 'style in "all" media type rule' => array('@media all {p {color: #000;}}')); + } + /** + * @test + * + * @param string $css + * + * @dataProvider mediaRulesDataProvider + */ + public function emogrifyKeepsMediaRules($css) + { + $html = $this->html5DocumentType . '

foo

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertContains($css, $this->subject->emogrify()); + } + /** + * @test + */ + public function removeAllowedMediaTypeRemovesStylesForTheGivenMediaType() + { + $css = '@media screen { html {} }'; + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->setCss($css); + $this->subject->removeAllowedMediaType('screen'); + self::assertNotContains($css, $this->subject->emogrify()); + } + /** + * @test + */ + public function addAllowedMediaTypeKeepsStylesForTheGivenMediaType() + { + $css = '@media braille { html { some-property: value; } }'; + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->setCss($css); + $this->subject->addAllowedMediaType('braille'); + self::assertContains($css, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyAddsMissingHeadElement() + { + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->setCss('@media all { html {} }'); + self::assertContains('', $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyKeepExistingHeadElementContent() + { + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->setCss('@media all { html {} }'); + self::assertContains('', $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyKeepExistingHeadElementAddStyleElement() + { + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->setCss('@media all { html {} }'); + self::assertContains('

'; + $this->subject->setHtml($html); + self::assertContains($css, $this->subject->emogrify()); + } + /** + * @test + * + * @param string $css + * + * @dataProvider validMediaPreserveDataProvider + */ + public function emogrifyWithValidMediaQueryNotContainsInlineCss($css) + { + $html = $this->html5DocumentType . PHP_EOL . '

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertNotContains('style="color:red"', $this->subject->emogrify()); + } + /** + * Invalid media query which need to be strip + * + * @return array[] + */ + public function invalidMediaPreserveDataProvider() + { + return array('style in "braille" type rule' => array('@media braille { h1 { color:red; } }'), 'style in "embossed" type rule' => array('@media embossed { h1 { color:red; } }'), 'style in "handheld" type rule' => array('@media handheld { h1 { color:red; } }'), 'style in "projection" type rule' => array('@media projection { h1 { color:red; } }'), 'style in "speech" type rule' => array('@media speech { h1 { color:red; } }'), 'style in "tty" type rule' => array('@media tty { h1 { color:red; } }'), 'style in "tv" type rule' => array('@media tv { h1 { color:red; } }')); + } + /** + * @test + * + * @param string $css + * + * @dataProvider invalidMediaPreserveDataProvider + */ + public function emogrifyWithInvalidMediaQueryaNotContainsInnerCss($css) + { + $html = $this->html5DocumentType . PHP_EOL . '

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertNotContains($css, $this->subject->emogrify()); + } + /** + * @test + * + * @param string $css + * + * @dataProvider invalidMediaPreserveDataProvider + */ + public function emogrifyWithInValidMediaQueryNotContainsInlineCss($css) + { + $html = $this->html5DocumentType . PHP_EOL . '

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertNotContains('style="color: red"', $this->subject->emogrify()); + } + /** + * @test + * + * @param string $css + * + * @dataProvider invalidMediaPreserveDataProvider + */ + public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInnerCss($css) + { + $html = $this->html5DocumentType . PHP_EOL . '

'; + $this->subject->setHtml($html); + self::assertNotContains($css, $this->subject->emogrify()); + } + /** + * @test + * + * @param string $css + * + * @dataProvider invalidMediaPreserveDataProvider + */ + public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInlineCss($css) + { + $html = $this->html5DocumentType . PHP_EOL . '

'; + $this->subject->setHtml($html); + self::assertNotContains('style="color: red"', $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyAppliesCssFromStyleNodes() + { + $styleAttributeValue = 'color: #ccc;'; + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + self::assertContains('', $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks() + { + $styleAttributeValue = 'color: #ccc;'; + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->disableStyleBlocksParsing(); + self::assertNotContains('', $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles() + { + $styleAttributeValue = 'text-align: center;'; + $html = $this->html5DocumentType . '' . '

paragraph

'; + $expected = '

'; + $this->subject->setHtml($html); + $this->subject->disableStyleBlocksParsing(); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles() + { + $styleAttributeValue = 'color: #ccc;'; + $html = $this->html5DocumentType . ''; + $this->subject->setHtml($html); + $this->subject->disableInlineStyleAttributesParsing(); + self::assertNotContains('subject->emogrify()); + } + /** + * @test + */ + public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles() + { + $styleAttributeValue = 'color: #ccc;'; + $html = $this->html5DocumentType . '' . '

paragraph

'; + $expected = '

'; + $this->subject->setHtml($html); + $this->subject->disableInlineStyleAttributesParsing(); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyAppliesCssWithUpperCaseSelector() + { + $html = $this->html5DocumentType . '

paragraph

'; + $expected = '

'; + $this->subject->setHtml($html); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks. + * @test + */ + public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock() + { + $html = $this->html5DocumentType . '' . '

some content

'; + $expected = '

'; + $this->subject->setHtml($html); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * Passed in CSS sets the order, but style block CSS overrides values. + * @test + */ + public function emogrifyMergesCssWithMixedCaseAttribute() + { + $css = 'p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}'; + $html = $this->html5DocumentType . '' . '

some content

'; + $expected = '

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyMergesCssWithMixedUnits() + { + $css = 'p { margin: 1px; padding-bottom:0;}'; + $html = $this->html5DocumentType . '' . '

some content

'; + $expected = '

'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyByDefaultRemovesElementsWithDisplayNoneFromExternalCss() + { + $css = 'div.foo { display: none; }'; + $html = $this->html5DocumentType . '

'; + $expected = '
'; + $this->subject->setHtml($html); + $this->subject->setCss($css); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyByDefaultRemovesElementsWithDisplayNoneInStyleAttribute() + { + $html = $this->html5DocumentType . '
' . ''; + $expected = '
'; + $this->subject->setHtml($html); + self::assertContains($expected, $this->subject->emogrify()); + } + /** + * @test + */ + public function emogrifyAfterDisableInvisibleNodeRemovalPreservesInvisibleElements() + { + $css = 'div.foo { display: none; }'; + $html = $this->html5DocumentType . '
'; + $expected = '