Selaa lähdekoodia

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).

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4277 a333f486-631f-4898-b8df-5754b55c2be0
romainq 9 vuotta sitten
vanhempi
commit
5842acff64

+ 4 - 2
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 .= "</ul>\n";
 			$sTestBody .= "</p>\n";
 			$sTestBody .= "</div>\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);

+ 8 - 0
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)',

+ 7 - 1
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);
 	}

+ 12 - 0
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 {
+}

+ 24 - 0
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

+ 31 - 0
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/

+ 92 - 0
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

+ 78 - 0
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.

+ 1020 - 0
lib/emogrifier/Classes/Emogrifier.php

@@ -0,0 +1,1020 @@
+<?php
+namespace Pelago;
+
+/**
+ * This class provides functions for converting CSS styles into inline style attributes in your HTML code.
+ *
+ * For more information, please see the README.md file.
+ *
+ * @version 1.0.0
+ *
+ * @author Cameron Brooks
+ * @author Jaime Prado
+ * @author Oliver Klee <typo3-coding@oliverklee.de>
+ * @author Roman Ožana <ozana@omdesign.cz>
+ */
+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 = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
+    /**
+     * @var string
+     */
+    const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
+    /**
+     * @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 <style> blocks in the HTML passed to this class should be parsed.
+     *
+     * If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML
+     * via inline styles.
+     *
+     * If set to false, the <style> blocks will be left as they are in the HTML.
+     *
+     * @var bool
+     */
+    private $isStyleBlocksParsingEnabled = true;
+    /**
+     * Determines whether elements with the `display: none` property are
+     * removed from the DOM.
+     *
+     * @var bool
+     */
+    private $shouldKeepInvisibleNodes = true;
+    /**
+     * The constructor.
+     *
+     * @param string $html the HTML to emogrify, must be UTF-8-encoded
+     * @param string $css the CSS to merge, must be UTF-8-encoded
+     */
+    public function __construct($html = '', $css = '')
+    {
+        $this->setHtml($html);
+        $this->setCss($css);
+    }
+    /**
+     * The destructor.
+     */
+    public function __destruct()
+    {
+        $this->purgeVisitedNodes();
+    }
+    /**
+     * Sets the HTML to emogrify.
+     *
+     * @param string $html the HTML to emogrify, must be UTF-8-encoded
+     *
+     * @return void
+     */
+    public function setHtml($html)
+    {
+        $this->html = $html;
+    }
+    /**
+     * Sets the CSS to merge with the HTML.
+     *
+     * @param string $css the CSS to merge, must be UTF-8-encoded
+     *
+     * @return void
+     */
+    public function setCss($css)
+    {
+        $this->css = $css;
+    }
+    /**
+     * Applies $this->css to $this->html and returns the HTML with the CSS
+     * applied.
+     *
+     * This method places the CSS inline.
+     *
+     * @return string
+     *
+     * @throws \BadMethodCallException
+     */
+    public function emogrify()
+    {
+        if ($this->html === '') {
+            throw new \BadMethodCallException('Please set some HTML first before calling emogrify.', 1390393096);
+        }
+        $xmlDocument = $this->createXmlDocument();
+        $this->process($xmlDocument);
+        return $xmlDocument->saveHTML();
+    }
+    /**
+     * Applies $this->css to $this->html and returns only the HTML content
+     * within the <body> tag.
+     *
+     * This method places the CSS inline.
+     *
+     * @return string
+     *
+     * @throws \BadMethodCallException
+     */
+    public function emogrifyBodyContent()
+    {
+        if ($this->html === '') {
+            throw new \BadMethodCallException('Please set some HTML first before calling emogrify.', 1390393096);
+        }
+        $xmlDocument = $this->createXmlDocument();
+        $this->process($xmlDocument);
+        $innerDocument = new \DOMDocument();
+        foreach ($xmlDocument->documentElement->getElementsByTagName('body')->item(0)->childNodes as $childNode) {
+            $innerDocument->appendChild($innerDocument->importNode($childNode, true));
+        }
+        return $innerDocument->saveHTML();
+    }
+    /**
+     * Applies $this->css to $xmlDocument.
+     *
+     * This method places the CSS inline.
+     *
+     * @param \DOMDocument $xmlDocument
+     *
+     * @return void
+     */
+    protected function process(\DOMDocument $xmlDocument)
+    {
+        $xpath = new \DOMXPath($xmlDocument);
+        $this->clearAllCaches();
+        // Before be begin processing the CSS file, parse the document and normalize all existing CSS attributes.
+        // This changes 'DISPLAY: none' to 'display: none'.
+        // We wouldn't have to do this if DOMXPath supported XPath 2.0.
+        // Also store a reference of nodes with existing inline styles so we don't overwrite them.
+        $this->purgeVisitedNodes();
+        $nodesWithStyleAttributes = $xpath->query('//*[@style]');
+        if ($nodesWithStyleAttributes !== false) {
+            /** @var \DOMElement $node */
+            foreach ($nodesWithStyleAttributes as $node) {
+                if ($this->isInlineStyleAttributesParsingEnabled) {
+                    $this->normalizeStyleAttributes($node);
+                } else {
+                    $node->removeAttribute('style');
+                }
+            }
+        }
+        // grab any existing style blocks from the html and append them to the existing CSS
+        // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
+        $allCss = $this->css;
+        if ($this->isStyleBlocksParsingEnabled) {
+            $allCss .= $this->getCssFromAllStyleNodes($xpath);
+        }
+        $cssParts = $this->splitCssAndMediaQuery($allCss);
+        $excludedNodes = $this->getNodesToExclude($xpath);
+        $cssRules = $this->parseCssRules($cssParts['css']);
+        foreach ($cssRules as $cssRule) {
+            // query the body for the xpath selector
+            $nodesMatchingCssSelectors = $xpath->query($this->translateCssToXpath($cssRule['selector']));
+            // ignore invalid selectors
+            if ($nodesMatchingCssSelectors === false) {
+                continue;
+            }
+            /** @var \DOMElement $node */
+            foreach ($nodesMatchingCssSelectors as $node) {
+                if (in_array($node, $excludedNodes, true)) {
+                    continue;
+                }
+                // if it has a style attribute, get it, process it, and append (overwrite) new stuff
+                if ($node->hasAttribute('style')) {
+                    // break it up into an associative array
+                    $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
+                } else {
+                    $oldStyleDeclarations = array();
+                }
+                $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']);
+                $node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations));
+            }
+        }
+        if ($this->isInlineStyleAttributesParsingEnabled) {
+            $this->fillStyleAttributesWithMergedStyles();
+        }
+        if ($this->shouldKeepInvisibleNodes) {
+            $this->removeInvisibleNodes($xpath);
+        }
+        $this->copyCssWithMediaToStyleNode($xmlDocument, $xpath, $cssParts['media']);
+    }
+    /**
+     * Extracts and parses the individual rules from a CSS string.
+     *
+     * @param string $css a string of raw CSS code
+     *
+     * @return string[][] an array of string sub-arrays with the keys
+     *         "selector" (the CSS selector(s), e.g., "*" or "h1"),
+     *         "declarationsBLock" (the semicolon-separated CSS declarations for that selector(s),
+     *         e.g., "color: red; height: 4px;"),
+     *         and "line" (the line number e.g. 42)
+     */
+    private function parseCssRules($css)
+    {
+        $cssKey = md5($css);
+        if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) {
+            // process the CSS file for selectors and definitions
+            preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $css, $matches, PREG_SET_ORDER);
+            $cssRules = array();
+            /** @var string[] $cssRule */
+            foreach ($matches as $key => $cssRule) {
+                $cssDeclaration = trim($cssRule[2]);
+                if ($cssDeclaration === '') {
+                    continue;
+                }
+                $selectors = explode(',', $cssRule[1]);
+                foreach ($selectors as $selector) {
+                    // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
+                    // only allow structural pseudo-classes
+                    if (strpos($selector, ':') !== false && !preg_match('/:\\S+\\-(child|type\\()/i', $selector)) {
+                        continue;
+                    }
+                    $cssRules[] = array('selector' => trim($selector), 'declarationsBlock' => $cssDeclaration, 'line' => $key);
+                }
+            }
+            usort($cssRules, array($this, 'sortBySelectorPrecedence'));
+            $this->caches[self::CACHE_KEY_CSS][$cssKey] = $cssRules;
+        }
+        return $this->caches[self::CACHE_KEY_CSS][$cssKey];
+    }
+    /**
+     * Disables the parsing of inline styles.
+     *
+     * @return void
+     */
+    public function disableInlineStyleAttributesParsing()
+    {
+        $this->isInlineStyleAttributesParsingEnabled = false;
+    }
+    /**
+     * Disables the parsing of <style> blocks.
+     *
+     * @return void
+     */
+    public function disableStyleBlocksParsing()
+    {
+        $this->isStyleBlocksParsingEnabled = false;
+    }
+    /**
+     * Disables the removal of elements with `display: none` properties.
+     *
+     * @return void
+     */
+    public function disableInvisibleNodeRemoval()
+    {
+        $this->shouldKeepInvisibleNodes = false;
+    }
+    /**
+     * Clears all caches.
+     *
+     * @return void
+     */
+    private function clearAllCaches()
+    {
+        $this->clearCache(self::CACHE_KEY_CSS);
+        $this->clearCache(self::CACHE_KEY_SELECTOR);
+        $this->clearCache(self::CACHE_KEY_XPATH);
+        $this->clearCache(self::CACHE_KEY_CSS_DECLARATIONS_BLOCK);
+        $this->clearCache(self::CACHE_KEY_COMBINED_STYLES);
+    }
+    /**
+     * Clears a single cache by key.
+     *
+     * @param int $key the cache key, must be CACHE_KEY_CSS, CACHE_KEY_SELECTOR, CACHE_KEY_XPATH
+     *                 or CACHE_KEY_CSS_DECLARATION_BLOCK
+     *
+     * @return void
+     *
+     * @throws \InvalidArgumentException
+     */
+    private function clearCache($key)
+    {
+        $allowedCacheKeys = array(self::CACHE_KEY_CSS, self::CACHE_KEY_SELECTOR, self::CACHE_KEY_XPATH, self::CACHE_KEY_CSS_DECLARATIONS_BLOCK, self::CACHE_KEY_COMBINED_STYLES);
+        if (!in_array($key, $allowedCacheKeys, true)) {
+            throw new \InvalidArgumentException('Invalid cache key: ' . $key, 1391822035);
+        }
+        $this->caches[$key] = array();
+    }
+    /**
+     * Purges the visited nodes.
+     *
+     * @return void
+     */
+    private function purgeVisitedNodes()
+    {
+        $this->visitedNodes = array();
+        $this->styleAttributesForNodes = array();
+    }
+    /**
+     * Marks a tag for removal.
+     *
+     * There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them.
+     * In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document.
+     *
+     * Note: The tags will not be removed if they have any content.
+     *
+     * @param string $tagName the tag name, e.g., "p"
+     *
+     * @return void
+     */
+    public function addUnprocessableHtmlTag($tagName)
+    {
+        $this->unprocessableHtmlTags[] = $tagName;
+    }
+    /**
+     * Drops a tag from the removal list.
+     *
+     * @param string $tagName the tag name, e.g., "p"
+     *
+     * @return void
+     */
+    public function removeUnprocessableHtmlTag($tagName)
+    {
+        $key = array_search($tagName, $this->unprocessableHtmlTags, true);
+        if ($key !== false) {
+            unset($this->unprocessableHtmlTags[$key]);
+        }
+    }
+    /**
+     * Marks a media query type to keep.
+     *
+     * @param string $mediaName the media type name, e.g., "braille"
+     *
+     * @return void
+     */
+    public function addAllowedMediaType($mediaName)
+    {
+        $this->allowedMediaTypes[$mediaName] = true;
+    }
+    /**
+     * Drops a media query type from the allowed list.
+     *
+     * @param string $mediaName the tag name, e.g., "braille"
+     *
+     * @return void
+     */
+    public function removeAllowedMediaType($mediaName)
+    {
+        if (isset($this->allowedMediaTypes[$mediaName])) {
+            unset($this->allowedMediaTypes[$mediaName]);
+        }
+    }
+    /**
+     * Adds a selector to exclude nodes from emogrification.
+     *
+     * Any nodes that match the selector will not have their style altered.
+     *
+     * @param string $selector the selector to exclude, e.g., ".editor"
+     *
+     * @return void
+     */
+    public function addExcludedSelector($selector)
+    {
+        $this->excludedSelectors[$selector] = true;
+    }
+    /**
+     * No longer excludes the nodes matching this selector from emogrification.
+     *
+     * @param string $selector the selector to no longer exclude, e.g., ".editor"
+     *
+     * @return void
+     */
+    public function removeExcludedSelector($selector)
+    {
+        if (isset($this->excludedSelectors[$selector])) {
+            unset($this->excludedSelectors[$selector]);
+        }
+    }
+    /**
+     * This removes styles from your email that contain display:none.
+     * We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only
+     * supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase,
+     * not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE")
+     * to lowercase.
+     *
+     * @param \DOMXPath $xpath
+     *
+     * @return void
+     */
+    private function removeInvisibleNodes(\DOMXPath $xpath)
+    {
+        $nodesWithStyleDisplayNone = $xpath->query('//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]');
+        if ($nodesWithStyleDisplayNone->length === 0) {
+            return;
+        }
+        // The checks on parentNode and is_callable below ensure that if we've deleted the parent node,
+        // we don't try to call removeChild on a nonexistent child node
+        /** @var \DOMNode $node */
+        foreach ($nodesWithStyleDisplayNone as $node) {
+            if ($node->parentNode && is_callable(array($node->parentNode, 'removeChild'))) {
+                $node->parentNode->removeChild($node);
+            }
+        }
+    }
+    /**
+     * Normalizes the value of the "style" attribute and saves it.
+     *
+     * @param \DOMElement $node
+     *
+     * @return void
+     */
+    private function normalizeStyleAttributes(\DOMElement $node)
+    {
+        $normalizedOriginalStyle = preg_replace_callback('/[A-z\\-]+(?=\\:)/S', function (array $m) {
+            return strtolower($m[0]);
+        }, $node->getAttribute('style'));
+        // in order to not overwrite existing style attributes in the HTML, we
+        // have to save the original HTML styles
+        $nodePath = $node->getNodePath();
+        if (!isset($this->styleAttributesForNodes[$nodePath])) {
+            $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle);
+            $this->visitedNodes[$nodePath] = $node;
+        }
+        $node->setAttribute('style', $normalizedOriginalStyle);
+    }
+    /**
+     * Merges styles from styles attributes and style nodes and applies them to the attribute nodes
+     *
+     * @return void
+     */
+    private function fillStyleAttributesWithMergedStyles()
+    {
+        foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
+            $node = $this->visitedNodes[$nodePath];
+            $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
+            $node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($currentStyleAttributes, $styleAttributesForNode));
+        }
+    }
+    /**
+     * This method merges old or existing name/value array with new name/value array
+     * and then generates a string of the combined style suitable for placing inline.
+     * This becomes the single point for CSS string generation allowing for consistent
+     * CSS output no matter where the CSS originally came from.
+     *
+     * @param string[] $oldStyles
+     * @param string[] $newStyles
+     *
+     * @return string
+     */
+    private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles)
+    {
+        $combinedStyles = array_merge($oldStyles, $newStyles);
+        $cacheKey = serialize($combinedStyles);
+        if (isset($this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey])) {
+            return $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey];
+        }
+        foreach ($oldStyles as $attributeName => $attributeValue) {
+            if (isset($newStyles[$attributeName]) && strtolower(substr($attributeValue, -10)) === '!important') {
+                $combinedStyles[$attributeName] = $attributeValue;
+            }
+        }
+        $style = '';
+        foreach ($combinedStyles as $attributeName => $attributeValue) {
+            $style .= strtolower(trim($attributeName)) . ': ' . trim($attributeValue) . '; ';
+        }
+        $trimmedStyle = rtrim($style);
+        $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle;
+        return $trimmedStyle;
+    }
+    /**
+     * Applies $css to $xmlDocument, limited to the media queries that actually apply to the document.
+     *
+     * @param \DOMDocument $xmlDocument the document to match against
+     * @param \DOMXPath $xpath
+     * @param string $css a string of CSS
+     *
+     * @return void
+     */
+    private function copyCssWithMediaToStyleNode(\DOMDocument $xmlDocument, \DOMXPath $xpath, $css)
+    {
+        if ($css === '') {
+            return;
+        }
+        $mediaQueriesRelevantForDocument = array();
+        foreach ($this->extractMediaQueriesFromCss($css) as $mediaQuery) {
+            foreach ($this->parseCssRules($mediaQuery['css']) as $selector) {
+                if ($this->existsMatchForCssSelector($xpath, $selector['selector'])) {
+                    $mediaQueriesRelevantForDocument[] = $mediaQuery['query'];
+                    break;
+                }
+            }
+        }
+        $this->addStyleElementToDocument($xmlDocument, implode($mediaQueriesRelevantForDocument));
+    }
+    /**
+     * Extracts the media queries from $css.
+     *
+     * @param string $css
+     *
+     * @return string[][] numeric array with string sub-arrays with the keys "css" and "query"
+     */
+    private function extractMediaQueriesFromCss($css)
+    {
+        preg_match_all('#(?<query>@media[^{]*\\{(?<css>(.*?)\\})(\\s*)\\})#s', $css, $mediaQueries);
+        $result = array();
+        foreach (array_keys($mediaQueries['css']) as $key) {
+            $result[] = array('css' => $mediaQueries['css'][$key], 'query' => $mediaQueries['query'][$key]);
+        }
+        return $result;
+    }
+    /**
+     * Checks whether there is at least one matching element for $cssSelector.
+     *
+     * @param \DOMXPath $xpath
+     * @param string $cssSelector
+     *
+     * @return bool
+     */
+    private function existsMatchForCssSelector(\DOMXPath $xpath, $cssSelector)
+    {
+        $nodesMatchingSelector = $xpath->query($this->translateCssToXpath($cssSelector));
+        return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
+    }
+    /**
+     * Returns CSS content.
+     *
+     * @param \DOMXPath $xpath
+     *
+     * @return string
+     */
+    private function getCssFromAllStyleNodes(\DOMXPath $xpath)
+    {
+        $styleNodes = $xpath->query('//style');
+        if ($styleNodes === false) {
+            return '';
+        }
+        $css = '';
+        /** @var \DOMNode $styleNode */
+        foreach ($styleNodes as $styleNode) {
+            $css .= '
+
+' . $styleNode->nodeValue;
+            $styleNode->parentNode->removeChild($styleNode);
+        }
+        return $css;
+    }
+    /**
+     * Adds a style element with $css to $document.
+     *
+     * This method is protected to allow overriding.
+     *
+     * @see https://github.com/jjriv/emogrifier/issues/103
+     *
+     * @param \DOMDocument $document
+     * @param string $css
+     *
+     * @return void
+     */
+    protected function addStyleElementToDocument(\DOMDocument $document, $css)
+    {
+        $styleElement = $document->createElement('style', $css);
+        $styleAttribute = $document->createAttribute('type');
+        $styleAttribute->value = 'text/css';
+        $styleElement->appendChild($styleAttribute);
+        $head = $this->getOrCreateHeadElement($document);
+        $head->appendChild($styleElement);
+    }
+    /**
+     * Returns the existing or creates a new head element in $document.
+     *
+     * @param \DOMDocument $document
+     *
+     * @return \DOMNode the head element
+     */
+    private function getOrCreateHeadElement(\DOMDocument $document)
+    {
+        $head = $document->getElementsByTagName('head')->item(0);
+        if ($head === null) {
+            $head = $document->createElement('head');
+            $html = $document->getElementsByTagName('html')->item(0);
+            $html->insertBefore($head, $document->getElementsByTagName('body')->item(0));
+        }
+        return $head;
+    }
+    /**
+     * Splits input CSS code to an array where:
+     *
+     * - key "css" will be contains clean CSS code
+     * - key "media" will be contains all valuable media queries
+     *
+     * Example:
+     *
+     * The CSS code
+     *
+     *   "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
+     *
+     * will be parsed into the following array:
+     *
+     *   "css" => "h1 { color:red; }"
+     *   "media" => "@media { h1 {}}"
+     *
+     * @param string $css
+     *
+     * @return string[]
+     */
+    private function splitCssAndMediaQuery($css)
+    {
+        $cssWithoutComments = preg_replace('/\\/\\*.*\\*\\//sU', '', $css);
+        $mediaTypesExpression = '';
+        if (!empty($this->allowedMediaTypes)) {
+            $mediaTypesExpression = '|' . implode('|', array_keys($this->allowedMediaTypes));
+        }
+        $media = '';
+        $cssForAllowedMediaTypes = preg_replace_callback('#@media\\s+(?:only\\s)?(?:[\\s{\\(]' . $mediaTypesExpression . ')\\s?[^{]+{.*}\\s*}\\s*#misU', function ($matches) use(&$media) {
+            $media .= $matches[0];
+        }, $cssWithoutComments);
+        // filter the CSS
+        $search = array('import directives' => '/^\\s*@import\\s[^;]+;/misU', 'remaining media enclosures' => '/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU');
+        $cleanedCss = preg_replace($search, '', $cssForAllowedMediaTypes);
+        return array('css' => $cleanedCss, 'media' => $media);
+    }
+    /**
+     * Creates a DOMDocument instance with the current HTML.
+     *
+     * @return \DOMDocument
+     */
+    private function createXmlDocument()
+    {
+        $xmlDocument = new \DOMDocument();
+        $xmlDocument->encoding = 'UTF-8';
+        $xmlDocument->strictErrorChecking = false;
+        $xmlDocument->formatOutput = true;
+        $libXmlState = libxml_use_internal_errors(true);
+        $xmlDocument->loadHTML($this->getUnifiedHtml());
+        libxml_clear_errors();
+        libxml_use_internal_errors($libXmlState);
+        $xmlDocument->normalizeDocument();
+        return $xmlDocument;
+    }
+    /**
+     * Returns the HTML with the unprocessable HTML tags removed and
+     * with added document type and Content-Type meta tag if needed.
+     *
+     * @return string the unified HTML
+     *
+     * @throws \BadMethodCallException
+     */
+    private function getUnifiedHtml()
+    {
+        $htmlWithoutUnprocessableTags = $this->removeUnprocessableTags($this->html);
+        $htmlWithDocumentType = $this->ensureDocumentType($htmlWithoutUnprocessableTags);
+        return $this->addContentTypeMetaTag($htmlWithDocumentType);
+    }
+    /**
+     * Removes the unprocessable tags from $html (if this feature is enabled).
+     *
+     * @param string $html
+     *
+     * @return string the reworked HTML with the unprocessable tags removed
+     */
+    private function removeUnprocessableTags($html)
+    {
+        if (empty($this->unprocessableHtmlTags)) {
+            return $html;
+        }
+        $unprocessableHtmlTags = implode('|', $this->unprocessableHtmlTags);
+        return preg_replace('/<\\/?(' . $unprocessableHtmlTags . ')[^>]*>/i', '', $html);
+    }
+    /**
+     * Makes sure that the passed HTML has a document type.
+     *
+     * @param string $html
+     *
+     * @return string HTML with document type
+     */
+    private function ensureDocumentType($html)
+    {
+        $hasDocumentType = stripos($html, '<!DOCTYPE') !== false;
+        if ($hasDocumentType) {
+            return $html;
+        }
+        return self::DEFAULT_DOCUMENT_TYPE . $html;
+    }
+    /**
+     * Adds a Content-Type meta tag for the charset.
+     *
+     * @param string $html
+     *
+     * @return string the HTML with the meta tag added
+     */
+    private function addContentTypeMetaTag($html)
+    {
+        $hasContentTypeMetaTag = stristr($html, 'Content-Type') !== false;
+        if ($hasContentTypeMetaTag) {
+            return $html;
+        }
+        // We are trying to insert the meta tag to the right spot in the DOM.
+        // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
+        $hasHeadTag = stripos($html, '<head') !== false;
+        $hasHtmlTag = stripos($html, '<html') !== false;
+        if ($hasHeadTag) {
+            $reworkedHtml = preg_replace('/<head(.*?)>/i', '<head$1>' . self::CONTENT_TYPE_META_TAG, $html);
+        } elseif ($hasHtmlTag) {
+            $reworkedHtml = preg_replace('/<html(.*?)>/i', '<html$1><head>' . self::CONTENT_TYPE_META_TAG . '</head>', $html);
+        } else {
+            $reworkedHtml = self::CONTENT_TYPE_META_TAG . $html;
+        }
+        return $reworkedHtml;
+    }
+    /**
+     * @param string[] $a
+     * @param string[] $b
+     *
+     * @return int
+     */
+    private function sortBySelectorPrecedence(array $a, array $b)
+    {
+        $precedenceA = $this->getCssSelectorPrecedence($a['selector']);
+        $precedenceB = $this->getCssSelectorPrecedence($b['selector']);
+        // We want these sorted in ascending order so selectors with lesser precedence get processed first and
+        // selectors with greater precedence get sorted last.
+        $precedenceForEquals = $a['line'] < $b['line'] ? -1 : 1;
+        $precedenceForNotEquals = $precedenceA < $precedenceB ? -1 : 1;
+        return $precedenceA === $precedenceB ? $precedenceForEquals : $precedenceForNotEquals;
+    }
+    /**
+     * @param string $selector
+     *
+     * @return int
+     */
+    private function getCssSelectorPrecedence($selector)
+    {
+        $selectorKey = md5($selector);
+        if (!isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) {
+            $precedence = 0;
+            $value = 100;
+            // ids: worth 100, classes: worth 10, elements: worth 1
+            $search = array('\\#', '\\.', '');
+            foreach ($search as $s) {
+                if (trim($selector) === '') {
+                    break;
+                }
+                $number = 0;
+                $selector = preg_replace('/' . $s . '\\w+/', '', $selector, -1, $number);
+                $precedence += $value * $number;
+                $value /= 10;
+            }
+            $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
+        }
+        return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
+    }
+    /**
+     * Maps a CSS selector to an XPath query string.
+     *
+     * @see http://plasmasturm.org/log/444/
+     *
+     * @param string $cssSelector a CSS selector
+     *
+     * @return string the corresponding XPath selector
+     */
+    private function translateCssToXpath($cssSelector)
+    {
+        $paddedSelector = ' ' . $cssSelector . ' ';
+        $lowercasePaddedSelector = preg_replace_callback('/\\s+\\w+\\s+/', function (array $matches) {
+            return strtolower($matches[0]);
+        }, $paddedSelector);
+        $trimmedLowercaseSelector = trim($lowercasePaddedSelector);
+        $xpathKey = md5($trimmedLowercaseSelector);
+        if (!isset($this->caches[self::CACHE_KEY_XPATH][$xpathKey])) {
+            $cssSelectorMatches = array('child' => '/\\s+>\\s+/', 'adjacent sibling' => '/\\s+\\+\\s+/', 'descendant' => '/\\s+/', ':first-child' => '/([^\\/]+):first-child/i', ':last-child' => '/([^\\/]+):last-child/i', 'attribute only' => '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/', 'attribute' => '/(\\w)\\[(\\w+)\\]/', 'exact attribute' => '/(\\w)\\[(\\w+)\\=[\'"]?(\\w+)[\'"]?\\]/');
+            $xPathReplacements = array('child' => '/', 'adjacent sibling' => '/following-sibling::*[1]/self::', 'descendant' => '//', ':first-child' => '\\1/*[1]', ':last-child' => '\\1/*[last()]', 'attribute only' => '*[@\\1]', 'attribute' => '\\1[@\\2]', 'exact attribute' => '\\1[@\\2="\\3"]');
+            $roughXpath = '//' . preg_replace($cssSelectorMatches, $xPathReplacements, $trimmedLowercaseSelector);
+            $xpathWithIdAttributeMatchers = preg_replace_callback(self::ID_ATTRIBUTE_MATCHER, array($this, 'matchIdAttributes'), $roughXpath);
+            $xpathWithIdAttributeAndClassMatchers = preg_replace_callback(self::CLASS_ATTRIBUTE_MATCHER, array($this, 'matchClassAttributes'), $xpathWithIdAttributeMatchers);
+            // Advanced selectors are going to require a bit more advanced emogrification.
+            // When we required PHP 5.3, we could do this with closures.
+            $xpathWithIdAttributeAndClassMatchers = preg_replace_callback('/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i', array($this, 'translateNthChild'), $xpathWithIdAttributeAndClassMatchers);
+            $finalXpath = preg_replace_callback('/([^\\/]+):nth-of-type\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i', array($this, 'translateNthOfType'), $xpathWithIdAttributeAndClassMatchers);
+            $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey] = $finalXpath;
+        }
+        return $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey];
+    }
+    /**
+     * @param string[] $match
+     *
+     * @return string
+     */
+    private function matchIdAttributes(array $match)
+    {
+        return ($match[1] !== '' ? $match[1] : '*') . '[@id="' . $match[2] . '"]';
+    }
+    /**
+     * @param string[] $match
+     *
+     * @return string
+     */
+    private function matchClassAttributes(array $match)
+    {
+        return ($match[1] !== '' ? $match[1] : '*') . '[contains(concat(" ",@class," "),concat(" ","' . implode('"," "))][contains(concat(" ",@class," "),concat(" ","', explode('.', substr($match[2], 1))) . '"," "))]';
+    }
+    /**
+     * @param string[] $match
+     *
+     * @return string
+     */
+    private function translateNthChild(array $match)
+    {
+        $parseResult = $this->parseNth($match);
+        if (isset($parseResult[self::MULTIPLIER])) {
+            if ($parseResult[self::MULTIPLIER] < 0) {
+                $parseResult[self::MULTIPLIER] = abs($parseResult[self::MULTIPLIER]);
+                $xPathExpression = sprintf('*[(last() - position()) mod %u = %u]/self::%s', $parseResult[self::MULTIPLIER], $parseResult[self::INDEX], $match[1]);
+            } else {
+                $xPathExpression = sprintf('*[position() mod %u = %u]/self::%s', $parseResult[self::MULTIPLIER], $parseResult[self::INDEX], $match[1]);
+            }
+        } else {
+            $xPathExpression = sprintf('*[%u]/self::%s', $parseResult[self::INDEX], $match[1]);
+        }
+        return $xPathExpression;
+    }
+    /**
+     * @param string[] $match
+     *
+     * @return string
+     */
+    private function translateNthOfType(array $match)
+    {
+        $parseResult = $this->parseNth($match);
+        if (isset($parseResult[self::MULTIPLIER])) {
+            if ($parseResult[self::MULTIPLIER] < 0) {
+                $parseResult[self::MULTIPLIER] = abs($parseResult[self::MULTIPLIER]);
+                $xPathExpression = sprintf('%s[(last() - position()) mod %u = %u]', $match[1], $parseResult[self::MULTIPLIER], $parseResult[self::INDEX]);
+            } else {
+                $xPathExpression = sprintf('%s[position() mod %u = %u]', $match[1], $parseResult[self::MULTIPLIER], $parseResult[self::INDEX]);
+            }
+        } else {
+            $xPathExpression = sprintf('%s[%u]', $match[1], $parseResult[self::INDEX]);
+        }
+        return $xPathExpression;
+    }
+    /**
+     * @param string[] $match
+     *
+     * @return int[]
+     */
+    private function parseNth(array $match)
+    {
+        if (in_array(strtolower($match[2]), array('even', 'odd'), true)) {
+            // we have "even" or "odd"
+            $index = strtolower($match[2]) === 'even' ? 0 : 1;
+            return array(self::MULTIPLIER => 2, self::INDEX => $index);
+        }
+        if (stripos($match[2], 'n') === false) {
+            // if there is a multiplier
+            $index = (int) str_replace(' ', '', $match[2]);
+            return array(self::INDEX => $index);
+        }
+        if (isset($match[3])) {
+            $multipleTerm = str_replace($match[3], '', $match[2]);
+            $index = (int) str_replace(' ', '', $match[3]);
+        } else {
+            $multipleTerm = $match[2];
+            $index = 0;
+        }
+        $multiplier = str_ireplace('n', '', $multipleTerm);
+        if ($multiplier === '') {
+            $multiplier = 1;
+        } elseif ($multiplier === '0') {
+            return array(self::INDEX => $index);
+        } else {
+            $multiplier = (int) $multiplier;
+        }
+        while ($index < 0) {
+            $index += abs($multiplier);
+        }
+        return array(self::MULTIPLIER => $multiplier, self::INDEX => $index);
+    }
+    /**
+     * Parses a CSS declaration block into property name/value pairs.
+     *
+     * Example:
+     *
+     * The declaration block
+     *
+     *   "color: #000; font-weight: bold;"
+     *
+     * will be parsed into the following array:
+     *
+     *   "color" => "#000"
+     *   "font-weight" => "bold"
+     *
+     * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
+     *
+     * @return string[]
+     *         the CSS declarations with the property names as array keys and the property values as array values
+     */
+    private function parseCssDeclarationsBlock($cssDeclarationsBlock)
+    {
+        if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) {
+            return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
+        }
+        $properties = array();
+        $declarations = preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
+        foreach ($declarations as $declaration) {
+            $matches = array();
+            if (!preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/', trim($declaration), $matches)) {
+                continue;
+            }
+            $propertyName = strtolower($matches[1]);
+            $propertyValue = $matches[2];
+            $properties[$propertyName] = $propertyValue;
+        }
+        $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties;
+        return $properties;
+    }
+    /**
+     * Find the nodes that are not to be emogrified.
+     *
+     * @param \DOMXPath $xpath
+     *
+     * @return \DOMElement[]
+     */
+    private function getNodesToExclude(\DOMXPath $xpath)
+    {
+        $excludedNodes = array();
+        foreach (array_keys($this->excludedSelectors) as $selectorToExclude) {
+            foreach ($xpath->query($this->translateCssToXpath($selectorToExclude)) as $node) {
+                $excludedNodes[] = $node;
+            }
+        }
+        return $excludedNodes;
+    }
+}

+ 136 - 0
lib/emogrifier/Configuration/PhpCodeSniffer/Standards/Emogrifier/ruleset.xml

@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ruleset name="PPW Coding Standard">
+    <description>This is the coding standard used for the Emogrifier code.
+        This standard has been tested with to work with PHP_CodeSniffer >= 2.3.0.
+    </description>
+
+    <!--The complete PSR-2 ruleset-->
+    <rule ref="PSR2"/>
+
+    <!-- Arrays -->
+    <rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
+    <rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
+
+    <!-- Classes -->
+    <rule ref="Generic.Classes.DuplicateClassName"/>
+    <rule ref="Squiz.Classes.ClassFileName"/>
+    <rule ref="Squiz.Classes.DuplicateProperty"/>
+    <rule ref="Squiz.Classes.LowercaseClassKeywords"/>
+    <rule ref="Squiz.Classes.SelfMemberReference"/>
+
+    <!-- Code analysis -->
+    <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
+    <rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
+    <rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
+    <rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
+    <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
+    <rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
+    <rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
+    <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
+
+    <!-- Commenting -->
+    <rule ref="Generic.Commenting.Fixme"/>
+    <rule ref="Generic.Commenting.Todo"/>
+    <rule ref="PEAR.Commenting.InlineComment"/>
+    <rule ref="Squiz.Commenting.DocCommentAlignment"/>
+    <rule ref="Squiz.Commenting.EmptyCatchComment"/>
+    <rule ref="Squiz.Commenting.FunctionCommentThrowTag"/>
+    <rule ref="Squiz.Commenting.PostStatementComment"/>
+    <rule ref="TYPO3SniffPool.Commenting.ClassComment"/>
+    <rule ref="TYPO3SniffPool.Commenting.DoubleSlashCommentsInNewLine"/>
+    <rule ref="TYPO3SniffPool.Commenting.SpaceAfterDoubleSlash"/>
+
+    <!-- Control structures -->
+    <rule ref="PEAR.ControlStructures.ControlSignature"/>
+    <rule ref="TYPO3SniffPool.ControlStructures.DisallowEachInLoopCondition"/>
+    <rule ref="TYPO3SniffPool.ControlStructures.DisallowElseIfConstruct"/>
+    <rule ref="TYPO3SniffPool.ControlStructures.ExtraBracesByAssignmentInLoop"/>
+    <rule ref="TYPO3SniffPool.ControlStructures.SwitchDeclaration"/>
+    <rule ref="TYPO3SniffPool.ControlStructures.TernaryConditionalOperator"/>
+    <rule ref="TYPO3SniffPool.ControlStructures.UnusedVariableInForEachLoop"/>
+
+    <!-- Debug -->
+    <rule ref="Generic.Debug.ClosureLinter"/>
+    <rule ref="TYPO3SniffPool.Debug.DebugCode"/>
+
+    <!-- Files -->
+    <rule ref="Generic.Files.OneClassPerFile"/>
+    <rule ref="Generic.Files.OneInterfacePerFile"/>
+    <rule ref="TYPO3SniffPool.Files.FileExtension"/>
+    <rule ref="TYPO3SniffPool.Files.Filename"/>
+    <rule ref="TYPO3SniffPool.Files.IncludingFile"/>
+    <rule ref="Zend.Files.ClosingTag"/>
+
+    <!-- Formatting -->
+    <rule ref="Generic.Formatting.SpaceAfterCast"/>
+    <rule ref="PEAR.Formatting.MultiLineAssignment"/>
+
+    <!-- Functions -->
+    <rule ref="Generic.Functions.CallTimePassByReference"/>
+    <rule ref="Squiz.Functions.FunctionDuplicateArgument"/>
+    <rule ref="Squiz.Functions.GlobalFunction"/>
+
+    <!-- Metrics -->
+    <!-- Enable this rule when the cyclomatic complexity of all methods is sufficiently low. -->
+    <!--<rule ref="Generic.Metrics.CyclomaticComplexity"/>-->
+    <rule ref="Generic.Metrics.NestingLevel"/>
+
+    <!-- Naming conventions -->
+    <rule ref="Generic.NamingConventions.ConstructorName"/>
+    <rule ref="PEAR.NamingConventions.ValidClassName"/>
+    <rule ref="TYPO3SniffPool.NamingConventions.ValidFunctionName"/>
+    <rule ref="TYPO3SniffPool.NamingConventions.ValidVariableName"/>
+
+    <!-- Objects -->
+    <rule ref="Squiz.Objects.ObjectMemberComma"/>
+
+    <!-- Operators -->
+    <rule ref="Squiz.Operators.IncrementDecrementUsage"/>
+    <rule ref="Squiz.Operators.ValidLogicalOperators"/>
+
+    <!-- PHP -->
+    <rule ref="Generic.PHP.CharacterBeforePHPOpeningTag"/>
+    <rule ref="Generic.PHP.DeprecatedFunctions"/>
+    <rule ref="Generic.PHP.DisallowShortOpenTag"/>
+    <rule ref="Generic.PHP.ForbiddenFunctions"/>
+    <rule ref="Generic.PHP.NoSilencedErrors"/>
+    <rule ref="Squiz.PHP.CommentedOutCode">
+        <properties>
+            <property name="maxPercentage" value="70"/>
+        </properties>
+    </rule>
+    <rule ref="Squiz.PHP.DisallowMultipleAssignments"/>
+    <rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
+    <rule ref="Squiz.PHP.DiscouragedFunctions"/>
+    <rule ref="Squiz.PHP.Eval"/>
+    <rule ref="Squiz.PHP.ForbiddenFunctions"/>
+    <rule ref="Squiz.PHP.GlobalKeyword"/>
+    <rule ref="Squiz.PHP.Heredoc"/>
+    <rule ref="Squiz.PHP.InnerFunctions"/>
+    <rule ref="Squiz.PHP.LowercasePHPFunctions"/>
+    <rule ref="Squiz.PHP.NonExecutableCode"/>
+
+    <!-- Scope -->
+    <rule ref="Squiz.Scope.MemberVarScope"/>
+    <rule ref="Squiz.Scope.StaticThisUsage"/>
+    <rule ref="TYPO3SniffPool.Scope.AlwaysReturn">
+        <exclude-pattern>*/Tests/*</exclude-pattern>
+    </rule>
+
+    <!--Strings-->
+    <rule ref="Squiz.Strings.DoubleQuoteUsage"/>
+    <rule ref="TYPO3SniffPool.Strings.ConcatenationSpacing"/>
+    <rule ref="TYPO3SniffPool.Strings.UnnecessaryStringConcat"/>
+
+    <!-- Whitespace -->
+    <rule ref="PEAR.WhiteSpace.ObjectOperatorIndent"/>
+    <rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/>
+    <rule ref="Squiz.WhiteSpace.CastSpacing"/>
+    <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
+    <rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
+    <rule ref="Squiz.WhiteSpace.PropertyLabelSpacing"/>
+    <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
+    <rule ref="TYPO3SniffPool.WhiteSpace.NoWhitespaceAtInDecrement"/>
+    <rule ref="TYPO3SniffPool.WhiteSpace.ScopeClosingBrace"/>
+    <rule ref="TYPO3SniffPool.WhiteSpace.WhitespaceAfterCommentSigns"/>
+</ruleset>

+ 21 - 0
lib/emogrifier/LICENSE

@@ -0,0 +1,21 @@
+Emogrifier is copyright (c) 2008-2014 Pelago and licensed under the MIT license.
+
+
+The MIT License (MIT)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 198 - 0
lib/emogrifier/README.md

@@ -0,0 +1,198 @@
+# Emogrifier
+
+[![Build Status](https://travis-ci.org/jjriv/emogrifier.svg?branch=master)](https://travis-ci.org/jjriv/emogrifier)
+[![Latest Stable Version](https://poser.pugx.org/pelago/emogrifier/v/stable.svg)](https://packagist.org/packages/pelago/emogrifier)
+[![Total Downloads](https://poser.pugx.org/pelago/emogrifier/downloads.svg)](https://packagist.org/packages/pelago/emogrifier)
+[![Latest Unstable Version](https://poser.pugx.org/pelago/emogrifier/v/unstable.svg)](https://packagist.org/packages/pelago/emogrifier)
+[![License](https://poser.pugx.org/pelago/emogrifier/license.svg)](https://packagist.org/packages/pelago/emogrifier)
+
+_n. e•mog•ri•fi•er [\ē-'mä-grƏ-,fī-Ər\] - a utility for changing completely the
+nature or appearance of HTML email, esp. in a particularly fantastic or bizarre
+manner_
+
+Emogrifier converts CSS styles into inline style attributes in your HTML code.
+This ensures proper display on email and mobile device readers that lack
+stylesheet support.
+
+This utility was developed as part of [Intervals](http://www.myintervals.com/)
+to deal with the problems posed by certain email clients (namely Outlook 2007
+and GoogleMail) when it comes to the way they handle styling contained in HTML
+emails. As many web developers and designers already know, certain email
+clients are notorious for their lack of CSS support. While attempts are being
+made to develop common [email standards](http://www.email-standards.org/),
+implementation is still a ways off.
+
+The primary problem with uncooperative email clients is that most tend to only
+regard inline CSS, discarding all `<style>` elements and links to stylesheets
+in `<link>` elements. Emogrifier solves this problem by converting CSS styles
+into inline style attributes in your HTML code.
+
+- [How it works](#how-it-works)
+- [Usage](#usage)
+- [Installing with Composer](#installing-with-composer)
+- [Usage](#usage)
+- [Supported CSS selectors](#supported-css-selectors)
+- [Caveats](#caveats)
+- [Maintainer](#maintainer)
+- [Contributing](#contributing)
+
+
+## How it Works
+
+Emogrifier automagically transmogrifies your HTML by parsing your CSS and
+inserting your CSS definitions into tags within your HTML based on your CSS
+selectors.
+
+
+## Usage
+
+First, you provide Emogrifier with the HTML and CSS you would like to merge.
+This can happen directly during instantiation:
+
+    $html = '<html><h1>Hello world!</h1></html>';
+    $css = 'h1 {font-size: 32px;}';
+    $emogrifier = new \Pelago\Emogrifier($html, $css);
+
+You could also use the setters for providing this data after instantiation:
+
+    $emogrifier = new \Pelago\Emogrifier();
+
+    $html = '<html><h1>Hello world!</h1></html>';
+    $css = 'h1 {font-size: 32px;}';
+
+    $emogrifier->setHtml($html);
+    $emogrifier->setCss($css);
+
+After you have set the HTML and CSS, you can call the `emogrify` method to
+merge both:
+
+    $mergedHtml = $emogrifier->emogrify();
+
+Emogrifier automatically adds a Content-Type meta tag to set the charset for
+the document (if it is not provided).
+
+If you would like to get back only the content of the BODY element instead of
+the complete HTML document, you can use the `emogrifyBodyContent` instead:
+
+    $bodyContent = $emogrifier->emogrifyBodyContent();
+
+
+## Options
+
+There are several options that you can set on the Emogrifier object before
+calling the `emogrify` method:
+
+* `$emogrifier->disableStyleBlocksParsing()` - By default, Emogrifier will grab
+  all `<style>` blocks in the HTML and will apply the CSS styles as inline
+  "style" attributes to the HTML. The `<style>` blocks will then be removed
+  from the HTML. If you want to disable this functionality so that Emogrifier
+  leaves these `<style>` blocks in the HTML and does not parse them, you should
+  use this option.
+* `$emogrifier->disableInlineStylesParsing()` - By default, Emogrifier
+  preserves all of the "style" attributes on tags in the HTML you pass to it.
+  However if you want to discard all existing inline styles in the HTML before
+  the CSS is applied, you should use this option.
+* `$emogrifier->disableInvisibleNodeRemoval()` - By default, Emogrifier removes
+  elements from the DOM that have the style attribute `display: none;`.  If
+  you would like to keep invisible elements in the DOM, use this option.
+* `$emogrifier->addAllowedMediaType(string $mediaName)` - By default, Emogrifier
+  will keep only media types `all`, `screen` and `print`. If you want to keep
+  some others, you can use this method to define them.
+* `$emogrifier->removeAllowedMediaType(string $mediaName)` - You can use this
+  method to remove media types that Emogrifier keeps.
+* `$emogrifier->addExcludedSelector(string $selector)` - Keeps elements from
+  being affected by emogrification.
+
+
+## Requirements
+
+* PHP from 5.4 to 7.0 (with the mbstring extension)
+* or HHVM
+
+
+## Installing with Composer
+
+Download the [`composer.phar`](https://getcomposer.org/composer.phar) locally
+or install [Composer](https://getcomposer.org/) globally:
+
+    curl -s https://getcomposer.org/installer | php
+
+Run the following command for a local installation:
+
+    php composer.phar require pelago/emogrifier:@dev
+
+Or for a global installation, run the following command:
+
+    composer require pelago/emogrifier:@dev
+
+You can also add follow lines to your `composer.json` and run the
+`composer update` command:
+
+    "require": {
+      "pelago/emogrifier": "@dev"
+    }
+
+See https://getcomposer.org/ for more information and documentation.
+
+
+## Supported CSS selectors
+
+Emogrifier currently support the following
+[CSS selectors](http://www.w3.org/TR/CSS2/selector.html):
+
+ * ID
+ * class
+ * type
+ * descendant
+ * child
+ * adjacent
+ * attribute presence
+ * attribute value
+ * attribute only
+ * first-child
+ * last-child
+
+The following selectors are not implemented yet:
+
+ * universal
+
+
+## Caveats
+
+* Emogrifier requires the HTML and the CSS to be UTF-8. Encodings like
+  ISO8859-1 or ISO8859-15 are not supported.
+* Emogrifier now preserves all valuable @media queries. Media queries
+  can be very useful in responsive email design. See
+  [media query support](https://litmus.com/help/email-clients/media-query-support/).
+* Emogrifier will grab existing inline style attributes _and_ will
+  grab `<style>` blocks from your HTML, but it will not grab CSS files
+  referenced in <link> elements. (The problem email clients are going to ignore
+  these tags anyway, so why leave them in your HTML?)
+* Even with styles inline, certain CSS properties are ignored by certain email
+  clients. For more information, refer to these resources:
+    * [http://www.email-standards.org/](http://www.email-standards.org/)
+    * [https://www.campaignmonitor.com/css/](https://www.campaignmonitor.com/css/)
+    * [http://templates.mailchimp.com/resources/email-client-css-support/](http://templates.mailchimp.com/resources/email-client-css-support/)
+* All CSS attributes that apply to a node will be applied, even if they are
+  redundant. For example, if you define a font attribute _and_ a font-size
+  attribute, both attributes will be applied to that node (in other words, the
+  more specific attribute will not be combined into the more general
+  attribute).
+* There's a good chance you might encounter problems if your HTML is not
+  well-formed and valid (DOMDocument might complain). If you get problems like
+  this, consider running your HTML through
+  [Tidy](http://php.net/manual/en/book.tidy.php) before you pass it to
+  Emogrifier.
+* Emogrifier automatically converts the provided (X)HTML into HTML5, i.e.,
+  self-closing tags will lose their slash. To keep your HTML valid, it is
+  recommended to use HTML5 instead of one of the XHTML variants.
+* Emogrifier only supports CSS1 level selectors and a few CSS2 level selectors
+  (but not all of them). It does not support pseudo selectors. (Emogrifier
+  works by converting CSS selectors to XPath selectors, and pseudo selectors
+  cannot be converted accurately).
+
+
+## Maintainer
+
+Emogrifier is maintained by the good people at
+[Pelago](http://www.pelagodesign.com/), info AT pelagodesign DOT com.

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 403 - 0
lib/emogrifier/Tests/Unit/EmogrifierTest.php


+ 46 - 0
lib/emogrifier/composer.json

@@ -0,0 +1,46 @@
+{
+    "name": "pelago/emogrifier",
+    "description": "Converts CSS styles into inline style attributes in your HTML code",
+    "tags": ["email", "css", "pre-processing"],
+    "license": "MIT",
+    "homepage": "http://www.pelagodesign.com/sidecar/emogrifier/",
+    "authors": [
+        {
+            "name": "John Reeve",
+            "email": "jreeve@pelagodesign.com"
+        },
+        {
+            "name": "Cameron Brooks"
+        },
+        {
+            "name": "Jaime Prado"
+        },
+        {
+            "name": "Oliver Klee",
+            "email": "typo3-coding@oliverklee.de"
+        },
+        {
+            "name": "Roman Ožana",
+            "email": "ozana@omdesign.cz"
+        }
+    ],
+    "require": {
+        "php": ">=5.4.0",
+        "ext-mbstring": "*"
+    },
+    "require-dev": {
+        "squizlabs/php_codesniffer": "2.3.4",
+        "typo3-ci/typo3sniffpool": "2.1.1",
+        "phpunit/phpunit": "4.8.11"
+    },
+    "autoload": {
+        "psr-4": {
+            "Pelago\\": "Classes/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.1.x-dev"
+        }
+    }
+}

+ 24 - 0
setup/licenses/community-licences.xml

@@ -2378,4 +2378,28 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 </pre>]]></text>
   </license>
+    <license>
+        <product>Emogrifier</product>
+        <author>Pelago</author>
+        <license_type>MIT</license_type>
+        <text><![CDATA[<pre>Copyright (c) 2008-2014 Pelago, https://github.com/jjriv/emogrifier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+</pre>]]></text>
+    </license>
 </licenses>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä