コード例 #1
0
 /**
  * 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();
     }
 }
コード例 #2
0
 /**
  * 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();
 }