Преглед изворни кода

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 година
родитељ
комит
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($sSubject))  $oLog->Set('subject', $sSubject);
 			if (isset($sBody))     $oLog->Set('body', $sBody);
 			if (isset($sBody))     $oLog->Set('body', $sBody);
 		}
 		}
+		$sStyles = file_get_contents(APPROOT.'css/email.css');
+		$sStyles .= MetaModel::GetConfig()->Get('email_css');
 
 
 		$oEmail = new EMail();
 		$oEmail = new EMail();
 
 
@@ -344,7 +346,7 @@ class ActionEmail extends ActionNotification
 			$sTestBody .= "</ul>\n";
 			$sTestBody .= "</ul>\n";
 			$sTestBody .= "</p>\n";
 			$sTestBody .= "</p>\n";
 			$sTestBody .= "</div>\n";
 			$sTestBody .= "</div>\n";
-			$oEmail->SetBody($sTestBody);
+			$oEmail->SetBody($sTestBody, 'text/html', $sStyles);
 			$oEmail->SetRecipientTO($this->Get('test_recipient'));
 			$oEmail->SetRecipientTO($this->Get('test_recipient'));
 			$oEmail->SetRecipientFrom($this->Get('test_recipient'));
 			$oEmail->SetRecipientFrom($this->Get('test_recipient'));
 			$oEmail->SetReferences($sReference);
 			$oEmail->SetReferences($sReference);
@@ -353,7 +355,7 @@ class ActionEmail extends ActionNotification
 		else
 		else
 		{
 		{
 			$oEmail->SetSubject($sSubject);
 			$oEmail->SetSubject($sSubject);
-			$oEmail->SetBody($sBody);
+			$oEmail->SetBody($sBody, 'text/html', $sStyles);
 			$oEmail->SetRecipientTO($sTo);
 			$oEmail->SetRecipientTO($sTo);
 			$oEmail->SetRecipientCC($sCC);
 			$oEmail->SetRecipientCC($sCC);
 			$oEmail->SetRecipientBCC($sBCC);
 			$oEmail->SetRecipientBCC($sBCC);

+ 8 - 0
core/config.class.inc.php

@@ -404,6 +404,14 @@ class Config
 			'source_of_value' => '',
 			'source_of_value' => '',
 			'show_in_conf_sample' => false,
 			'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(
 		'apc_cache.enabled' => array(
 			'type' => 'bool',
 			'type' => 'bool',
 			'description' => 'If set, the APC cache is allowed (the PHP extension must also be active)',
 			'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);
 		$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_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
 		$this->m_oMessage->setBody($sBody, $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.

Разлика између датотеке није приказан због своје велике величине
+ 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.
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 </pre>]]></text>
 </pre>]]></text>
   </license>
   </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>
 </licenses>

Неке датотеке нису приказане због велике количине промена