/** * parse a CSS string using a regex parser * * Called by {@link Stylesheet::parse_css()} * * @param string $str */ private function _parse_css($str) { // Destroy comments $css = preg_replace("'/\\*.*?\\*/'si", "", $str); // FIXME: handle '{' within strings, e.g. [attr="string {}"] // Something more legible: $re = "/\\s* # Skip leading whitespace \n" . "( @([^\\s]+)\\s+([^{;]*) (?:;|({)) )? # Match @rules followed by ';' or '{' \n" . "(?(1) # Only parse sub-sections if we're in an @rule... \n" . " (?(4) # ...and if there was a leading '{' \n" . " \\s*( (?:(?>[^{}]+) ({)? # Parse rulesets and individual @page rules \n" . " (?(6) (?>[^}]*) }) \\s*)+? \n" . " ) \n" . " }) # Balancing '}' \n" . "| # Branch to match regular rules (not preceeded by '@')\n" . "([^{]*{[^}]*})) # Parse normal rulesets\n" . "/xs"; if (preg_match_all($re, $css, $matches, PREG_SET_ORDER) === false) { // An error occured throw new DOMPDF_Exception("Error parsing css file: preg_match_all() failed."); } // After matching, the array indicies are set as follows: // // [0] => complete text of match // [1] => contains '@import ...;' or '@media {' if applicable // [2] => text following @ for cases where [1] is set // [3] => media types or full text following '@import ...;' // [4] => '{', if present // [5] => rulesets within media rules // [6] => '{', within media rules // [7] => individual rules, outside of media rules // //pre_r($matches); foreach ($matches as $match) { $match[2] = trim($match[2]); if ($match[2] !== "") { // Handle @rules switch ($match[2]) { case "import": $this->_parse_import($match[3]); break; case "media": if (in_array(mb_strtolower(trim($match[3])), self::$ACCEPTED_MEDIA_TYPES)) { $this->_parse_sections($match[5]); } break; case "page": // Store the style for later... if (is_null($this->_page_style)) { $this->_page_style = $this->_parse_properties($match[5]); } else { $this->_page_style->merge($this->_parse_properties($match[5])); } break; default: // ignore everything else break; } continue; } if ($match[7] !== "") { $this->_parse_sections($match[7]); } } }
/** * parse a CSS string using a regex parser * * Called by {@link Stylesheet::parse_css()} * * @param string $str */ private function _parse_css($str) { $str = trim($str); // Destroy comments and remove HTML comments $css = preg_replace(array("'/\\*.*?\\*/'si", "/^<!--/", "/-->\$/"), "", $str); // FIXME: handle '{' within strings, e.g. [attr="string {}"] // Something more legible: $re = "/\\s* # Skip leading whitespace \n" . "( @([^\\s]+)\\s+([^{;]*) (?:;|({)) )? # Match @rules followed by ';' or '{' \n" . "(?(1) # Only parse sub-sections if we're in an @rule... \n" . " (?(4) # ...and if there was a leading '{' \n" . " \\s*( (?:(?>[^{}]+) ({)? # Parse rulesets and individual @page rules \n" . " (?(6) (?>[^}]*) }) \\s*)+? \n" . " ) \n" . " }) # Balancing '}' \n" . "| # Branch to match regular rules (not preceeded by '@')\n" . "([^{]*{[^}]*})) # Parse normal rulesets\n" . "/xs"; if (preg_match_all($re, $css, $matches, PREG_SET_ORDER) === false) { // An error occured throw new DOMPDF_Exception("Error parsing css file: preg_match_all() failed."); } // After matching, the array indicies are set as follows: // // [0] => complete text of match // [1] => contains '@import ...;' or '@media {' if applicable // [2] => text following @ for cases where [1] is set // [3] => media types or full text following '@import ...;' // [4] => '{', if present // [5] => rulesets within media rules // [6] => '{', within media rules // [7] => individual rules, outside of media rules // //pre_r($matches); foreach ($matches as $match) { $match[2] = trim($match[2]); if ($match[2] !== "") { // Handle @rules switch ($match[2]) { case "import": $this->_parse_import($match[3]); break; case "media": $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES; if (defined("DOMPDF_DEFAULT_MEDIA_TYPE")) { $acceptedmedia[] = DOMPDF_DEFAULT_MEDIA_TYPE; } else { $acceptedmedia[] = self::$ACCEPTED_DEFAULT_MEDIA_TYPE; } if (in_array(mb_strtolower(trim($match[3])), $acceptedmedia)) { $this->_parse_sections($match[5]); } break; case "page": //This handles @page to be applied to page oriented media //Note: This has a reduced syntax: //@page { margin:1cm; color:blue; } //Not a sequence of styles like a full.css, but only the properties //of a single style, which is applied to the very first "root" frame before //processing other styles of the frame. //Working properties: // margin (for margin around edge of paper) // font-family (default font of pages) // color (default text color of pages) //Non working properties: // border // padding // background-color //Todo:Reason is unknown //Other properties (like further font or border attributes) not tested. //If a border or background color around each paper sheet is desired, //assign it to the <body> tag, possibly only for the css of the correct media type. // If the page has a name, skip the style. if ($match[3] !== "") { return; } // Store the style for later... if (is_null($this->_page_style)) { $this->_page_style = $this->_parse_properties($match[5]); } else { $this->_page_style->merge($this->_parse_properties($match[5])); } break; case "font-face": $this->_parse_font_face($match[5]); break; default: // ignore everything else break; } continue; } if ($match[7] !== "") { $this->_parse_sections($match[7]); } } }
/** * merge() should not merge style if the states are different */ public function testMerge_doesNotMergeStyles_ifStatesAreDifferent() { $style1 = new Style(); $style2 = new Style(); // $style2->getDocument()->setSomething(); $style2->getSection()->setIndex(999); $style2->getParagraph()->setIndex(999); $style2->getCharacter()->setIsBold(true); $style2->merge($style1); // $this->assertNotSame($style1->getDocument(), $style2->getDocument()); $this->assertNotSame($style1->getSection(), $style2->getSection()); $this->assertNotSame($style1->getParagraph(), $style2->getParagraph()); $this->assertNotSame($style1->getCharacter(), $style2->getCharacter()); return; }