/** * 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) { /** @var $nodeWithStyleAttribute DOMNode */ foreach ($nodesWithStyleAttributes as $node) { $normalizedOriginalStyle = preg_replace_callback('/[A-z\\-]+(?=\\:)/S', array($this, 'strtolower'), $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) $allCss = $this->css; $allCss .= $this->getCssFromAllStyleNodes($xpath); $cssParts = $this->splitCssAndMediaQuery($allCss); self::$_media = ''; // reset $cssKey = md5($cssParts['css']); if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) { // process the CSS file for selectors and definitions preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $cssParts['css'], $matches, PREG_SET_ORDER); $allSelectors = array(); foreach ($matches as $key => $selectorString) { // if there is a blank definition, skip if (!strlen(trim($selectorString[2]))) { continue; } // else split by commas and duplicate attributes so we can sort by selector precedence $selectors = explode(',', $selectorString[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; } $allSelectors[] = array('selector' => trim($selector), 'attributes' => trim($selectorString[2]), 'line' => $key); } } // now sort the selectors by precedence usort($allSelectors, array($this, 'sortBySelectorPrecedence')); $this->caches[self::CACHE_KEY_CSS][$cssKey] = $allSelectors; } foreach ($this->caches[self::CACHE_KEY_CSS][$cssKey] as $value) { // query the body for the xpath selector $nodesMatchingCssSelectors = $xpath->query($this->translateCssToXpath($value['selector'])); /** @var $node \DOMNode */ foreach ($nodesMatchingCssSelectors as $node) { // 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->parseCssDeclarationBlock($node->getAttribute('style')); } else { $oldStyleDeclarations = array(); } $newStyleDeclarations = $this->parseCssDeclarationBlock($value['attributes']); $node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)); } } // now iterate through the nodes that contained inline styles in the original HTML foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) { $node = $this->visitedNodes[$nodePath]; $currentStyleAttributes = $this->parseCssDeclarationBlock($node->getAttribute('style')); $node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($currentStyleAttributes, $styleAttributesForNode)); } // 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. $nodesWithStyleDisplayNone = $xpath->query('//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'); // 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 if ($nodesWithStyleDisplayNone->length > 0) { /** @var $node \DOMNode */ foreach ($nodesWithStyleDisplayNone as $node) { if ($node->parentNode && is_callable(array($node->parentNode, 'removeChild'))) { $node->parentNode->removeChild($node); } } } $this->copyCssWithMediaToStyleNode($cssParts, $xmlDocument); if ($this->preserveEncoding) { if (function_exists('mb_convert_encoding')) { return mb_convert_encoding($xmlDocument->saveHTML(), self::ENCODING, 'HTML-ENTITIES'); } else { return htmlspecialchars_decode(utf8_encode(html_entity_decode($xmlDocument->saveHTML(), ENT_COMPAT, self::ENCODING))); } } else { return $xmlDocument->saveHTML(); } }
/** * 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); } self::$_media = ''; // reset $xmlDocument = $this->createXmlDocument(); $this->process($xmlDocument); return $xmlDocument->saveHTML(); }