*/ private $unprocessableHtmlTags = array('wbr'); /** * @var array */ private $caches = array( self::CACHE_KEY_CSS => array(), self::CACHE_KEY_SELECTOR => array(), self::CACHE_KEY_XPATH => array(), ); /** * the visited nodes with the XPath paths as array keys * * @var array<\DOMNode> */ 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(); /** * This attribute applies to the case where you want to preserve your original text encoding. * * By default, emogrifier translates your text into HTML entities for two reasons: * * 1. Because of client incompatibilities, it is better practice to send out HTML entities rather than unicode over email. * * 2. It translates any illegal XML characters that DOMDocument cannot work with. * * If you would like to preserve your original encoding, set this attribute to TRUE. * * @var boolean */ public $preserveEncoding = FALSE; /** * 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; } /** * 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); } /** * Clears a single cache by key. * * @param integer $key the cache key, must be CACHE_KEY_CSS, CACHE_KEY_SELECTOR or CACHE_KEY_XPATH * * @return void * * @throws \InvalidArgumentException */ private function clearCache($key) { $allowedCacheKeys = array(self::CACHE_KEY_CSS, self::CACHE_KEY_SELECTOR, self::CACHE_KEY_XPATH); 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]); } } /** * Applies the CSS you submit to the HTML you submit. * * 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(); $xpath = new \DOMXPath($xmlDocument); $this->clearAllCaches(); // before be begin processing the CSS file, parse the document and normalize all existing CSS attributes (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) { $callback = create_function('$m', 'return strtolower($m[0]);'); /** @var $nodeWithStyleAttribute \DOMNode */ foreach ($nodesWithStyleAttributes as $node) { $normalizedOriginalStyle = preg_replace_callback( '/[A-z\\-]+(?=\\:)/S', $callback, $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->parseCssDeclarationBlock($normalizedOriginalStyle); $this->visitedNodes[$nodePath] = $node; } $node->setAttribute('style', $normalizedOriginalStyle); } } // 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) $css = $this->css; $styleNodes = $xpath->query('//style'); if ($styleNodes !== FALSE) { /** @var $styleNode \DOMNode */ foreach ($styleNodes as $styleNode) { // append the css $css .= "\n\n" . $styleNode->nodeValue; // remove the