/**
  * 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();
 }