/** * Set the stylesheet reader instance * * @param \Crossjoin\Css\Reader\CssString|string $stylesheet * @return $this * * @throws \InvalidArgumentException */ public function setStylesheetReader($stylesheet) { if (is_string($stylesheet)) { $stylesheet = new StylesheetReader($stylesheet); } if (!$stylesheet instanceof StylesheetReader) { throw new InvalidArgumentException('The argument 0 of the [setStylesheetReader] method expects to be a string of CSS or a [Crossjoin\\Css\\Reader\\CssString]'); } $this->reader = $stylesheet; $this->reader->setEnvironmentEncoding($this->getCharset()); return $this; }
/** * Prepares the mail HTML/text content. */ protected function prepareContent() { $html = $this->getHtmlContent(); if (class_exists("\\DOMDocument")) { $doc = new \DOMDocument(); $doc->loadHTML($html); } else { throw new \RuntimeException("Required extension 'dom' seems to be missing."); } // Extract styles and remove style tags (optionally added again later). // // IMPORTANT: The style nodes need to be saved in an array first, or // the replacement won't work correctly. $styleString = ""; $styleNodes = []; foreach ($doc->getElementsByTagName('style') as $styleNode) { $styleNodes[] = $styleNode; } foreach ($styleNodes as $styleNode) { $skip = false; // Check if type is 'text/css' (defaults to it if not) $typeAttribute = $styleNode->attributes->getNamedItem("type"); if ($typeAttribute !== null && (string) $typeAttribute->nodeValue !== "text/css") { $skip = true; } // Check media type is allowed (defaults to 'all') if ($skip === false) { $mediaAttribute = $styleNode->attributes->getNamedItem("media"); if ($mediaAttribute !== null) { $mediaAttribute = str_replace(" ", "", (string) $mediaAttribute->nodeValue); $mediaTypes = explode(",", $mediaAttribute); if (!in_array("all", $mediaTypes) && !in_array("screen", $mediaTypes)) { $skip = true; } } } // Add CSS if allowed if ($skip === false) { $styleString .= $styleNode->nodeValue . "\r\n"; } $styleNode->parentNode->removeChild($styleNode); } // Prepare some variables $xpath = new \DOMXpath($doc); $reader = new CssString($styleString); $reader->setEnvironmentEncoding($this->getCharset()); $rules = $reader->getStyleSheet()->getRules(); // Set pseudo classes that can be set in a style attribute // and that are supported by the Symfony CssSelector (doesn't support CSS4 yet). $allowedPseudoClasses = [StyleSelector::PSEUDO_CLASS_FIRST_CHILD, StyleSelector::PSEUDO_CLASS_ROOT, StyleSelector::PSEUDO_CLASS_NTH_CHILD, StyleSelector::PSEUDO_CLASS_NTH_LAST_CHILD, StyleSelector::PSEUDO_CLASS_NTH_OF_TYPE, StyleSelector::PSEUDO_CLASS_NTH_LAST_OF_TYPE, StyleSelector::PSEUDO_CLASS_LAST_CHILD, StyleSelector::PSEUDO_CLASS_FIRST_OF_TYPE, StyleSelector::PSEUDO_CLASS_LAST_OF_TYPE, StyleSelector::PSEUDO_CLASS_ONLY_CHILD, StyleSelector::PSEUDO_CLASS_ONLY_OF_TYPE, StyleSelector::PSEUDO_CLASS_EMPTY, StyleSelector::PSEUDO_CLASS_NOT]; // Extract all relevant style declarations $selectors = []; foreach ($this->getRelevantStyleRules($rules) as $styleRule) { foreach ($styleRule->getSelectors() as $selector) { // Check if the selector contains pseudo classes/elements that cannot // be mapped to elements $skip = false; foreach ($selector->getPseudoClasses() as $pseudoClass) { if (!in_array($pseudoClass, $allowedPseudoClasses)) { $skip = true; break; } } if ($skip === false) { $specificity = $selector->getSpecificity(); if (!isset($selectors[$specificity])) { $selectors[$specificity] = []; } $selectorString = $selector->getValue(); if (!isset($selectors[$specificity][$selectorString])) { $selectors[$specificity][$selectorString] = []; } foreach ($styleRule->getDeclarations() as $declaration) { $selectors[$specificity][$selectorString][] = $declaration; } } } } // Get all specificity values (to process the declarations in the correct order, // without sorting the array by key, which perhaps could result in a changed // order of selectors within the specificity). $specificityKeys = array_keys($selectors); sort($specificityKeys); // Temporary remove all existing style attributes, because they always have the highest priority // and are added again after all styles have been applied to the elements $elements = $xpath->query("descendant-or-self::*[@style]"); /** @var \DOMElement $element */ foreach ($elements as $element) { if ($element->attributes !== null) { $styleAttribute = $element->attributes->getNamedItem("style"); $styleValue = ""; if ($styleAttribute !== null) { $styleValue = (string) $styleAttribute->nodeValue; } if ($styleValue !== "") { $element->setAttribute('data-pre-mailer-original-style', $styleValue); $element->removeAttribute('style'); } } } // Process all style declarations in the correct order foreach ($specificityKeys as $specificityKey) { /** @var StyleDeclaration[] $declarations */ foreach ($selectors[$specificityKey] as $selectorString => $declarations) { $xpathQuery = CssSelector::toXPath($selectorString); $elements = $xpath->query($xpathQuery); /** @var \DOMElement $element */ foreach ($elements as $element) { if ($element->attributes !== null) { $styleAttribute = $element->attributes->getNamedItem("style"); $styleValue = ""; if ($styleAttribute !== null) { $styleValue = (string) $styleAttribute->nodeValue; } $concat = $styleValue === "" ? "" : ";"; foreach ($declarations as $declaration) { $styleValue .= $concat . $declaration->getProperty() . ":" . $declaration->getValue(); $concat = ";"; } $element->setAttribute('style', $styleValue); } } } } // Add temporarily removed style attributes again, after all styles have been applied to the elements $elements = $xpath->query("descendant-or-self::*[@data-pre-mailer-original-style]"); /** @var \DOMElement $element */ foreach ($elements as $element) { if ($element->attributes !== null) { $styleAttribute = $element->attributes->getNamedItem("style"); $styleValue = ""; if ($styleAttribute !== null) { $styleValue = (string) $styleAttribute->nodeValue; } $originalStyleAttribute = $element->attributes->getNamedItem("data-pre-mailer-original-style"); $originalStyleValue = ""; if ($originalStyleAttribute !== null) { $originalStyleValue = (string) $originalStyleAttribute->nodeValue; } if ($styleValue !== "" || $originalStyleValue !== "") { $styleValue = ($styleValue !== "" ? $styleValue . ";" : "") . $originalStyleValue; $element->setAttribute('style', $styleValue); $element->removeAttribute('data-pre-mailer-original-style'); } } } // Optionally remove class attributes in HTML tags $optionHtmlClasses = $this->getOption(self::OPTION_HTML_CLASSES); if ($optionHtmlClasses === self::OPTION_HTML_CLASSES_REMOVE) { $nodesWithClass = []; foreach ($xpath->query('descendant-or-self::*[@class]') as $nodeWithClass) { $nodesWithClass[] = $nodeWithClass; } /** @var \DOMElement $nodeWithClass */ foreach ($nodesWithClass as $nodeWithClass) { $nodeWithClass->removeAttribute('class'); } } // Optionally remove HTML comments $optionHtmlComments = $this->getOption(self::OPTION_HTML_COMMENTS); if ($optionHtmlComments === self::OPTION_HTML_COMMENTS_REMOVE) { $commentNodes = []; foreach ($xpath->query('//comment()') as $comment) { $commentNodes[] = $comment; } foreach ($commentNodes as $commentNode) { $commentNode->parentNode->removeChild($commentNode); } } // Write XPath document back to DOM document $newDoc = $xpath->document; // Generate text version (before adding the styles again) $this->text = $this->prepareText($newDoc); // Optionally add styles tag to the HEAD or the BODY of the document $optionStyleTag = $this->getOption(self::OPTION_STYLE_TAG); if ($optionStyleTag === self::OPTION_STYLE_TAG_BODY || $optionStyleTag === self::OPTION_STYLE_TAG_HEAD) { $cssWriterClass = $this->getOption(self::OPTION_CSS_WRITER_CLASS); /** @var WriterAbstract $cssWriter */ $cssWriter = new $cssWriterClass($reader->getStyleSheet()); $styleNode = $newDoc->createElement("style"); $styleNode->nodeValue = $cssWriter->getContent(); if ($optionStyleTag === self::OPTION_STYLE_TAG_BODY) { /** @var \DOMNode $bodyNode */ foreach ($newDoc->getElementsByTagName('body') as $bodyNode) { $bodyNode->insertBefore($styleNode, $bodyNode->firstChild); break; } } elseif ($optionStyleTag === self::OPTION_STYLE_TAG_HEAD) { /** @var \DOMNode $headNode */ foreach ($newDoc->getElementsByTagName('head') as $headNode) { $headNode->appendChild($styleNode); break; } } } // Generate HTML version $this->html = $newDoc->saveHTML(); }