Ejemplo n.º 1
0
 /**
  * Adds a comment.
  *
  * @param string $comment
  * @return $this
  */
 public function addComment($comment)
 {
     if (is_string($comment)) {
         $comment = Placeholder::replaceCommentPlaceholders($comment, true);
         $this->comments[] = $comment;
     } else {
         throw new \InvalidArgumentException("Invalid type '" . gettype($comment) . "' for argument 'comment' given.");
     }
     return $this;
 }
Ejemplo n.º 2
0
 /**
  * @param string|null $ruleString
  * @param StyleSheet $styleSheet
  */
 public function __construct($ruleString = null, StyleSheet $styleSheet = null)
 {
     if ($styleSheet !== null) {
         $this->setStyleSheet($styleSheet);
     }
     if ($ruleString !== null) {
         $ruleString = Placeholder::replaceStringsAndComments($ruleString);
         $this->parseRuleString($ruleString);
     }
 }
Ejemplo n.º 3
0
 /**
  * @param string $keyframeList
  * @param StyleSheet $styleSheet
  */
 public function __construct($keyframeList = "", StyleSheet $styleSheet = null)
 {
     if ($styleSheet !== null) {
         $this->setStyleSheet($styleSheet);
     }
     if ($keyframeList !== "") {
         $keyframeList = Placeholder::replaceStringsAndComments($keyframeList);
         $this->parseKeyframeList($keyframeList);
     }
 }
Ejemplo n.º 4
0
 /**
  * Checks the selector value.
  *
  * @param $value
  * @return bool
  */
 public function checkValue(&$value)
 {
     if (is_string($value)) {
         $value = Placeholder::replaceStringsAndComments($value);
         $value = Placeholder::removeCommentPlaceholders($value, true);
         $value = preg_replace('/[ ]+/', ' ', $value);
         $value = Placeholder::replaceStringPlaceholders($value, true);
         return true;
     } else {
         throw new \InvalidArgumentException("Invalid type '" . gettype($value) . "' for argument 'value' given.");
     }
 }
Ejemplo n.º 5
0
 /**
  * Parses the charset rule.
  *
  * @param string $ruleString
  */
 protected function parseRuleString($ruleString)
 {
     if (is_string($ruleString)) {
         // Check for valid rule format
         // (with vendor prefix check to match e.g. "@-moz-document")
         if (preg_match('/^[ \\r\\n\\t\\f]*@(' . self::getVendorPrefixRegExp("/") . ')?document[ \\r\\n\\t\\f]+(.*)$/i', $ruleString, $matches)) {
             $vendorPrefix = $matches[1];
             $ruleString = trim($matches[2], " \r\n\t\f");
             $charset = $this->getCharset();
             $inFunction = false;
             $isEscaped = false;
             $conditions = [];
             $currentCondition = "";
             $currentValue = "";
             for ($i = 0, $j = mb_strlen($ruleString, $charset); $i < $j; $i++) {
                 $char = mb_substr($ruleString, $i, 1, $charset);
                 if ($char === "\\") {
                     if ($isEscaped === false) {
                         $isEscaped = true;
                     } else {
                         $isEscaped = false;
                     }
                 } else {
                     if ($char === "(") {
                         if ($isEscaped === false) {
                             $inFunction = true;
                             continue;
                         } else {
                             $currentValue .= $char;
                         }
                     } else {
                         if ($char === ")") {
                             if ($isEscaped === false) {
                                 $conditions[$currentCondition] = trim($currentValue, " \r\n\t\f");
                                 $currentCondition = "";
                                 $currentValue = "";
                                 $inFunction = false;
                                 continue;
                             } else {
                                 $currentValue .= $char;
                             }
                         } else {
                             if ($char === "," || $char === " ") {
                                 if ($currentCondition === "" && $currentValue === "") {
                                     continue;
                                 } elseif ($currentValue !== "") {
                                     $currentValue .= $char;
                                 } else {
                                     // something wrong here...
                                 }
                             } else {
                                 if ($inFunction === false) {
                                     $currentCondition .= $char;
                                 } else {
                                     $currentValue .= $char;
                                 }
                             }
                         }
                     }
                 }
                 // Reset escaped flag
                 if ($isEscaped === true && $char !== "\\") {
                     $isEscaped = false;
                 }
             }
             foreach ($conditions as $key => $value) {
                 $conditions[$key] = Placeholder::replaceStringPlaceholders($value, true);
             }
             if (isset($conditions["url"])) {
                 $this->setUrl($conditions["url"]);
             }
             if (isset($conditions["url-prefix"])) {
                 $this->setUrlPrefix($conditions["url-prefix"]);
             }
             if (isset($conditions["domain"])) {
                 $this->setDomain($conditions["domain"]);
             }
             if (isset($conditions["regexp"])) {
                 $this->setRegexp($conditions["regexp"]);
             }
             if ($vendorPrefix !== "") {
                 $this->setVendorPrefix($vendorPrefix);
             }
         } else {
             throw new \InvalidArgumentException("Invalid format for @document rule.");
         }
     } else {
         throw new \InvalidArgumentException("Invalid type '" . gettype($ruleString) . "' for argument 'ruleString' given. String expected.");
     }
 }
Ejemplo n.º 6
0
 /**
  * Checks the declaration value.
  *
  * @param string $value
  * @return bool
  */
 public function checkValue(&$value)
 {
     if (is_string($value)) {
         $value = Placeholder::replaceStringsAndComments($value);
         $value = Placeholder::removeCommentPlaceholders($value, true);
         $value = Placeholder::replaceStringPlaceholders($value, true);
         $value = trim($value);
         if ($value !== '') {
             return true;
         } else {
             $this->setIsValid(false);
         }
     } else {
         throw new \InvalidArgumentException("Invalid type '" . gettype($value) . "' for argument 'value' given.");
     }
 }
Ejemplo n.º 7
0
 /**
  * Parses the CSS source content.
  */
 protected function parseCss()
 {
     // Init variables
     $this->styleSheet = new StyleSheet();
     $cssContent = "";
     // Prepare CSS content to allow easy parsing;
     // temporarily replace all strings.
     $blockCount = 0;
     $ruleCount = 0;
     $ruleBlock = 0;
     $inBrackets = false;
     $charsetIgnored = true;
     $charsetReplaced = false;
     if (($handle = $this->getCssResource()) !== false) {
         // Determine charset in the correct order, defined in
         // http://www.w3.org/TR/css-syntax-3/#input-byte-stream
         $charset = null;
         $fileContainsBom = false;
         if (($firstLine = fgets($handle)) === false) {
             $firstLine = "";
         }
         // Check for a BOM and use it, if it exists. "The decode algorithm gives precedence to a byte order mark
         // (BOM), and only uses the fallback when none is found."
         $bom = pack("CCC", 0xef, 0xbb, 0xbf);
         if (strlen($firstLine) >= 3 && strncmp($firstLine, $bom, 3) === 0) {
             $charset = "UTF-8";
             $fileContainsBom = true;
         } else {
             // Fallback 1: The encoding defined in HTTP or equivalent protocol
             $charset = $this->getProtocolEncoding();
             // Fallback 2: The charset as defined in the CSS file
             if ($charset === null) {
                 if (preg_match('/^@charset\\s+(["\'])([-a-zA-Z0-9_]+)\\g{1}/i', $firstLine, $matches)) {
                     $charset = $matches[2];
                     $charsetIgnored = false;
                     // Auto-correction of the defined charset.
                     //"If the return value was utf-16be or utf-16le, use utf-8 as the fallback encoding".
                     if (in_array(strtoupper($charset), ["UTF-16BE", "UTF-16LE"])) {
                         $charset = "UTF-8";
                         $charsetReplaced = true;
                     }
                 }
             }
             // Fallback 3: The environment encoding of the referencing document
             if ($charset === null) {
                 $charset = $this->getEnvironmentEncoding();
             }
             // Fallback 4: Default to UTF-8
             if ($charset === null) {
                 $charset = "UTF-8";
             }
         }
         $this->setCharset($charset);
         // Set position back to the beginning (but skip BOMs)
         fseek($handle, $fileContainsBom ? 3 : 0);
         while (($css = fgets($handle)) !== false) {
             // Required check to avoid errors when the encoding of the
             // file doesn't match the set/detected charset.
             if (mb_check_encoding($css, $charset) === false) {
                 throw new \RuntimeException("Invalid '{$charset}' encoding in CSS file.");
             }
             if (preg_match('/[^\\x00-\\x7f]/', $css)) {
                 $isAscii = false;
                 $strLen = mb_strlen($css, $charset);
             } else {
                 $isAscii = true;
                 $strLen = strlen($css);
             }
             for ($i = 0, $j = $strLen; $i < $j; $i++) {
                 if ($isAscii === true) {
                     $char = $css[$i];
                 } else {
                     $char = mb_substr($css, $i, 1, $charset);
                 }
                 if ($char === "{") {
                     $blockCount++;
                     $cssContent .= "\n_BLOCKSTART_" . $blockCount . "_\n";
                     if ($ruleCount > $ruleBlock) {
                         $ruleBlock++;
                     }
                 } else {
                     if ($char === "}") {
                         $cssContent .= "\n_BLOCKEND_" . $blockCount . "_\n";
                         $blockCount--;
                         if ($blockCount < $ruleCount) {
                             if ($ruleCount > 0) {
                                 $cssContent .= "\n_RULEEND_" . $ruleCount . "_\n";
                                 $ruleCount--;
                             }
                         }
                         if ($ruleCount > 0) {
                             $ruleBlock--;
                         }
                     } elseif ($char === ";") {
                         $cssContent .= $char;
                         if ($ruleCount > 0 && $ruleBlock === 0) {
                             $cssContent .= "\n_RULEEND_1_\n";
                             $ruleCount--;
                         }
                     } else {
                         // Start new at-rule, but only if we are not in brackets, which still can occur, although we
                         // replaced all strings, e.g. in this case: "background: url(/images/myimage-@1x.png)".
                         if ($char === "@" && $inBrackets === false) {
                             if ($ruleCount > 0 && $blockCount === 0) {
                                 $errorCss = Placeholder::replaceCommentPlaceholders(Placeholder::replaceStringPlaceholders($css));
                                 throw new \RuntimeException("Parse error near '{$errorCss}'.");
                             }
                             $ruleCount++;
                             $cssContent .= "\n_RULESTART_" . $ruleCount . "_\n";
                             // Replace all white-space characters within rule definitions by normal space to get
                             // one line only
                         } elseif ($ruleCount >= $blockCount && in_array($char, ["\r", "\n", "\t", "\f"])) {
                             $char = " ";
                         } elseif ($char === "(") {
                             $inBrackets = true;
                         } elseif ($char === ")") {
                             $inBrackets = false;
                         }
                         $cssContent .= $char;
                     }
                 }
             }
         }
         // Auto-correction as required by CSS specs
         while ($blockCount > 0) {
             $cssContent .= "\n_BLOCKEND_" . $blockCount . "_\n";
             $blockCount--;
         }
         while ($ruleCount > 0) {
             $cssContent .= "\n_RULEEND_" . $ruleCount . "_\n";
             $ruleCount--;
         }
     }
     // Prettify...
     $cssContent = preg_replace('/;/', ";\n", $cssContent);
     $cssContent = preg_replace('/[\\t\\f]+/', "", $cssContent);
     $cssContent = preg_replace('/[ ]+/', " ", $cssContent);
     $cssContent = preg_replace('/(\\n)[ ]|[ ](\\n)/', "\\1\\2", $cssContent);
     $cssContent = preg_replace('/(?<!_)[ \\t\\n\\r\\f]*(:)[ \\t\\n\\r\\f]*/', "\\1", $cssContent);
     $cssContent = preg_replace('/([\\r\\n])+/', "\\1", $cssContent);
     $cssContent = preg_replace('/^\\n|\\n$/', "", $cssContent);
     $cssContent = preg_replace('/^(_COMMENT_[a-f0-9]{32}_)([^\\r\\n]+)/m', "\\1\n\\2", $cssContent);
     // Parse
     $lines = explode("\n", $cssContent);
     $ruleCount = 0;
     $blockCount = 0;
     $lastRuleContainers = [$this->styleSheet];
     $lastRuleSet = null;
     // Prepare vendor prefix regular expression
     $vendorPrefixRegExp = RuleAbstract::getVendorPrefixRegExp("/");
     $comment = null;
     $atRuleCharsetAllowed = true;
     $atRuleImportAllowed = true;
     $atRuleNamespaceAllowed = true;
     foreach ($lines as $line) {
         if (preg_match('/^(?J)(?:_(?P<type>RULESTART|RULEEND|BLOCKSTART|BLOCKEND)_\\d+_|_(?P<type>COMMENT)_[a-f0-9]{32}_)/', $line, $matches)) {
             if ($matches['type'] === 'RULESTART') {
                 $ruleCount++;
             } elseif ($matches['type'] === 'RULEEND') {
                 $ruleCount--;
                 if ($ruleCount === $blockCount) {
                     // Current rule finished
                 }
             } elseif ($matches['type'] === 'BLOCKSTART') {
                 $blockCount++;
             } elseif ($matches['type'] === 'BLOCKEND') {
                 $blockCount--;
                 if ($blockCount === $ruleCount) {
                     if ($comment !== null) {
                         /** @var AtRuleAbstract $lastRuleSet */
                         $lastRuleSet->addComment($comment);
                         $comment = null;
                     }
                     // Current rule set finished
                     $lastRuleSet = null;
                 } else {
                     if ($comment !== null) {
                         /** @var AtRuleAbstract[] $lastRuleContainers */
                         $lastRuleContainers[$ruleCount]->addComment($comment);
                         $comment = null;
                     }
                 }
             } elseif ($matches['type'] === 'COMMENT') {
                 $comment = rtrim($line);
             }
         } else {
             if ($blockCount < $ruleCount) {
                 // New rule opened
                 if (preg_match('/^@(' . $vendorPrefixRegExp . ')?([a-zA-Z_]{1}(?:[-a-zA-Z0-9_]*|[^[:ascii:]*]))/i', trim($line), $matches)) {
                     $identifier = mb_strtolower($matches[2], $this->getCharset());
                     switch ($identifier) {
                         case "charset":
                             $atRule = new CharsetRule($line, $this->styleSheet);
                             break;
                         case "import":
                             $atRule = new ImportRule($line, $this->styleSheet);
                             break;
                         case "namespace":
                             $atRule = new NamespaceRule($line, $this->styleSheet);
                             break;
                         case "media":
                             $atRule = new MediaRule($line, $this->styleSheet);
                             break;
                         case "supports":
                             $atRule = new SupportsRule($line, $this->styleSheet);
                             break;
                         case "document":
                             $atRule = new DocumentRule($line, $this->styleSheet);
                             break;
                         case "font-face":
                             $atRule = new FontFaceRule($line, $this->styleSheet);
                             break;
                         case "page":
                             $atRule = new PageRule($line, $this->styleSheet);
                             break;
                         case "keyframes":
                             $atRule = new KeyframesRule($line, $this->styleSheet);
                             break;
                         default:
                             throw new \InvalidArgumentException("Unknown at rule identifier '{$identifier}'.");
                     }
                     // Add vendor prefix
                     if ($matches[1] !== "") {
                         $vendorPrefix = mb_strtolower($matches[1], $this->getCharset());
                         $atRule->setVendorPrefix($vendorPrefix);
                     }
                 } else {
                     throw new \InvalidArgumentException("Invalid rule format in '{$line}'.");
                 }
                 // IMPORTANT:
                 // - The @charset rule must be the first element in the style sheet and not be preceded by any
                 //   character.
                 // - Any @import rules must precede all other types of rules, except @charset rules (and other
                 //   @import rules).
                 // - Any @namespace rules must follow all @charset and @import rules (and other @namespace rules)
                 //   and precede all other non-ignored at-rules and style rules in a style-sheet.
                 if ($atRule instanceof CharsetRule) {
                     if ($atRuleCharsetAllowed === false) {
                         // As defined by CSS specs, the rule has been ignored, du to an invalid position in the
                         // style sheet. E.g. @charset must be the first content of the file, @import must be first
                         // or follow @charset or @import, and @namespace can only follow to @charset, @import or
                         // @namespace.
                         $atRule->setIsValid(false);
                         $atRule->addValidationError("Ignored @charset rule, because at wrong position in style sheet.");
                     } elseif ($charsetIgnored === true) {
                         // As defined by CSS specs, the charset rule has been ignored, due to charset information
                         // from other sources (e.g. BOMs in the file or defined protocol encoding).
                         $atRule->setIsValid(false);
                         $atRule->addValidationError("Ignored @charset rule, because charset got from other source with higher priority.");
                         $atRuleCharsetAllowed = false;
                     } elseif ($charsetReplaced === true) {
                         // As defined by CSS specs, the charset defined by the charset rule has been replaced with
                         // "UTF-8", because an UTF-16* charset has been used.
                         $atRule->setIsValid(false);
                         $atRule->addValidationError("Replaced charset in @charset rule with 'UTF-8', because defined charset is invalid.");
                         $atRuleCharsetAllowed = false;
                     } else {
                         $atRuleCharsetAllowed = false;
                     }
                 } elseif ($atRule instanceof ImportRule) {
                     // As defined by CSS specs, the rule has been ignored, du to an invalid position in the style
                     // sheet. E.g. @charset must be the first content of the file, @import must be first or follow
                     // @charset or @import, and @namespace can only follow to @charset, @import or @namespace.
                     if ($atRuleImportAllowed === false) {
                         $atRule->setIsValid(false);
                         $atRule->addValidationError("Ignored @import rule, because at wrong position in style sheet.");
                     }
                     $atRuleCharsetAllowed = false;
                 } elseif ($atRule instanceof NamespaceRule) {
                     // As defined by CSS specs, the rule has been ignored, du to an invalid position in the style
                     // sheet. E.g. @charset must be the first content of the file, @import must be first or follow
                     // @charset or @import, and @namespace can only follow to @charset, @import or @namespace.
                     if ($atRuleNamespaceAllowed === false) {
                         $atRule->setIsValid(false);
                         $atRule->addValidationError("Ignored @namespace rule, because at wrong position in style sheet.");
                     }
                     $atRuleCharsetAllowed = false;
                     $atRuleImportAllowed = false;
                 } else {
                     $atRuleCharsetAllowed = false;
                     $atRuleImportAllowed = false;
                     $atRuleNamespaceAllowed = false;
                 }
                 $lastRuleContainers[$ruleCount - 1]->addRule($atRule);
                 if ($atRule instanceof AtRuleConditionalAbstract) {
                     $lastRuleContainers[$ruleCount] = $atRule;
                 } elseif ($atRule instanceof KeyframesRule) {
                     $lastRuleContainers[$ruleCount] = $atRule;
                 } elseif ($atRule instanceof FontFaceRule) {
                     $lastRuleContainers[$ruleCount] = $atRule;
                     $lastRuleSet = $atRule;
                 } elseif ($atRule instanceof PageRule) {
                     $lastRuleContainers[$ruleCount] = $atRule;
                     $lastRuleSet = $atRule;
                 }
                 // Not all at-rules contain other rule, e.g. in @page rules the rules are mixed with the
                 // at-rule, so they directly contain declarations - this is filtered by checking for the
                 // HasRulesInterface here.
             } elseif ($blockCount === $ruleCount && $lastRuleContainers[$ruleCount] instanceof HasRulesInterface) {
                 // New rule set opened
                 if ($lastRuleContainers[$ruleCount] instanceof KeyframesRule) {
                     $ruleSet = new KeyframesRuleSet($line, $this->styleSheet);
                 } else {
                     $ruleSet = new StyleRuleSet($line, $this->styleSheet);
                 }
                 if ($comment !== null) {
                     $ruleSet->addComment($comment);
                     $comment = null;
                 }
                 $lastRuleContainers[$ruleCount]->addRule($ruleSet);
                 $lastRuleSet = $ruleSet;
                 $atRuleCharsetAllowed = false;
             } elseif ($blockCount >= $ruleCount) {
                 // New declaration
                 if ($lastRuleSet !== null) {
                     $line = preg_replace('/[\\s;]+$/', '', $line);
                     $invalidDeclaration = false;
                     if (strpos($line, ":") === false) {
                         $property = $line;
                         $value = "";
                         $invalidDeclaration = true;
                     } else {
                         list($property, $value) = explode(":", $line, 2);
                     }
                     $declaration = null;
                     if ($lastRuleContainers[$ruleCount] instanceof StyleSheet) {
                         $declaration = new StyleDeclaration($property, $value, $this->styleSheet);
                     } elseif ($lastRuleContainers[$ruleCount] instanceof AtRuleConditionalAbstract) {
                         $declaration = new StyleDeclaration($property, $value, $this->styleSheet);
                     } elseif ($lastRuleContainers[$ruleCount] instanceof KeyframesRule) {
                         $declaration = new KeyframesDeclaration($property, $value, $this->styleSheet);
                     } elseif ($lastRuleContainers[$ruleCount] instanceof FontFaceRule) {
                         $declaration = new FontFaceDeclaration($property, $value, $this->styleSheet);
                     } elseif ($lastRuleContainers[$ruleCount] instanceof PageRule) {
                         $declaration = new PageDeclaration($property, $value, $this->styleSheet);
                     }
                     if ($declaration !== null) {
                         if ($comment !== null) {
                             $declaration->addComment($comment);
                             $comment = null;
                         }
                         if ($invalidDeclaration === true) {
                             $declaration->setIsValid(false);
                             $declaration->addValidationError("Parse error. Invalid declaration at '{$line}'.");
                         }
                         $lastRuleSet->addDeclaration($declaration);
                     }
                 }
                 $atRuleCharsetAllowed = false;
             }
         }
     }
 }
Ejemplo n.º 8
0
 /**
  * Parses the namespace rule.
  *
  * @param string $ruleString
  */
 protected function parseRuleString($ruleString)
 {
     if (is_string($ruleString)) {
         $charset = $this->getCharset();
         // Remove at-rule and unnecessary white-spaces
         $ruleString = preg_replace('/^[ \\r\\n\\t\\f]*@namespace[ \\r\\n\\t\\f]+/i', '', $ruleString);
         $ruleString = trim($ruleString, " \r\n\t\f");
         // Remove trailing semicolon
         $ruleString = rtrim($ruleString, ";");
         $isEscaped = false;
         $inFunction = false;
         $parts = [];
         $currentPart = "";
         for ($i = 0, $j = mb_strlen($ruleString, $charset); $i < $j; $i++) {
             $char = mb_substr($ruleString, $i, 1, $charset);
             if ($char === "\\") {
                 if ($isEscaped === false) {
                     $isEscaped = true;
                 } else {
                     $isEscaped = false;
                 }
             } else {
                 if ($char === " ") {
                     if ($isEscaped === false) {
                         if ($inFunction == false) {
                             $currentPart = trim($currentPart, " \r\n\t\f");
                             if ($currentPart !== "") {
                                 $parts[] = trim($currentPart, " \r\n\t\f");
                                 $currentPart = "";
                             }
                         }
                     } else {
                         $currentPart .= $char;
                     }
                 } elseif ($isEscaped === false && $char === "(") {
                     $inFunction = true;
                     $currentPart .= $char;
                 } elseif ($isEscaped === false && $char === ")") {
                     $inFunction = false;
                     $currentPart .= $char;
                 } else {
                     $currentPart .= $char;
                 }
             }
             // Reset escaped flag
             if ($isEscaped === true && $char !== "\\") {
                 $isEscaped = false;
             }
         }
         if ($currentPart !== "") {
             $currentPart = trim($currentPart, " \r\n\t\f");
             if ($currentPart !== "") {
                 $parts[] = trim($currentPart, " \r\n\t\f");
             }
         }
         foreach ($parts as $key => $value) {
             $parts[$key] = Placeholder::replaceStringPlaceholders($value);
         }
         $countParts = count($parts);
         if ($countParts === 2) {
             $this->setPrefix($parts[0]);
             // Get URL value
             $name = Url::extractUrl($parts[1]);
             $this->setName($name);
         } elseif ($countParts === 1) {
             // Get URL value
             $name = Url::extractUrl($parts[0]);
             $this->setName($name);
         } else {
             // ERROR
         }
     } else {
         throw new \InvalidArgumentException("Invalid type '" . gettype($ruleString) . "' for argument 'ruleString' given.");
     }
 }