MDL-65759 library: Update php-css-parser to 8.3.0
authorMathew May <mathewm@hotmail.co.nz>
Thu, 13 Jun 2019 00:40:23 +0000 (08:40 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Thu, 20 Jun 2019 00:16:13 +0000 (08:16 +0800)
20 files changed:
lib/php-css-parser/CSSList/AtRuleBlockList.php
lib/php-css-parser/CSSList/CSSList.php
lib/php-css-parser/CSSList/Document.php
lib/php-css-parser/OutputFormat.php
lib/php-css-parser/Parser.php
lib/php-css-parser/Parsing/ParserState.php [new file with mode: 0644]
lib/php-css-parser/Property/AtRule.php
lib/php-css-parser/Rule/Rule.php
lib/php-css-parser/RuleSet/DeclarationBlock.php
lib/php-css-parser/RuleSet/RuleSet.php
lib/php-css-parser/Value/CSSFunction.php
lib/php-css-parser/Value/CSSString.php
lib/php-css-parser/Value/CalcFunction.php [new file with mode: 0644]
lib/php-css-parser/Value/CalcRuleValueList.php [new file with mode: 0644]
lib/php-css-parser/Value/Color.php
lib/php-css-parser/Value/LineName.php [new file with mode: 0644]
lib/php-css-parser/Value/Size.php
lib/php-css-parser/Value/URL.php
lib/php-css-parser/Value/Value.php
lib/thirdpartylibs.xml

index a1ff8f8..24e79f0 100644 (file)
@@ -35,9 +35,11 @@ class AtRuleBlockList extends CSSBlockList implements AtRule {
                if($sArgs) {
                        $sArgs = ' ' . $sArgs;
                }
-               $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
+               $sResult  = $oOutputFormat->sBeforeAtRuleBlock;
+               $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
                $sResult .= parent::render($oOutputFormat);
                $sResult .= '}';
+               $sResult .= $oOutputFormat->sAfterAtRuleBlock;
                return $sResult;
        }
 
index f9986eb..d883df8 100644 (file)
@@ -2,11 +2,22 @@
 
 namespace Sabberworm\CSS\CSSList;
 
+use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\SourceException;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Property\AtRule;
+use Sabberworm\CSS\Property\Charset;
+use Sabberworm\CSS\Property\CSSNamespace;
+use Sabberworm\CSS\Property\Import;
+use Sabberworm\CSS\Property\Selector;
 use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\RuleSet\AtRuleSet;
 use Sabberworm\CSS\RuleSet\DeclarationBlock;
 use Sabberworm\CSS\RuleSet\RuleSet;
-use Sabberworm\CSS\Property\Selector;
-use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Value\CSSString;
+use Sabberworm\CSS\Value\URL;
+use Sabberworm\CSS\Value\Value;
 
 /**
  * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
@@ -24,6 +35,147 @@ abstract class CSSList implements Renderable, Commentable {
                $this->iLineNo = $iLineNo;
        }
 
+       public static function parseList(ParserState $oParserState, CSSList $oList) {
+               $bIsRoot = $oList instanceof Document;
+               if(is_string($oParserState)) {
+                       $oParserState = new ParserState($oParserState);
+               }
+               $bLenientParsing = $oParserState->getSettings()->bLenientParsing;
+               while(!$oParserState->isEnd()) {
+                       $comments = $oParserState->consumeWhiteSpace();
+                       $oListItem = null;
+                       if($bLenientParsing) {
+                               try {
+                                       $oListItem = self::parseListItem($oParserState, $oList);
+                               } catch (UnexpectedTokenException $e) {
+                                       $oListItem = false;
+                               }
+                       } else {
+                               $oListItem = self::parseListItem($oParserState, $oList);
+                       }
+                       if($oListItem === null) {
+                               // List parsing finished
+                               return;
+                       }
+                       if($oListItem) {
+                               $oListItem->setComments($comments);
+                               $oList->append($oListItem);
+                       }
+                       $oParserState->consumeWhiteSpace();
+               }
+               if(!$bIsRoot && !$bLenientParsing) {
+                       throw new SourceException("Unexpected end of document", $oParserState->currentLine());
+               }
+       }
+
+       private static function parseListItem(ParserState $oParserState, CSSList $oList) {
+               $bIsRoot = $oList instanceof Document;
+               if ($oParserState->comes('@')) {
+                       $oAtRule = self::parseAtRule($oParserState);
+                       if($oAtRule instanceof Charset) {
+                               if(!$bIsRoot) {
+                                       throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
+                               }
+                               if(count($oList->getContents()) > 0) {
+                                       throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
+                               }
+                               $oParserState->setCharset($oAtRule->getCharset()->getString());
+                       }
+                       return $oAtRule;
+               } else if ($oParserState->comes('}')) {
+                       $oParserState->consume('}');
+                       if ($bIsRoot) {
+                               if ($oParserState->getSettings()->bLenientParsing) {
+                                       while ($oParserState->comes('}')) $oParserState->consume('}');
+                                       return DeclarationBlock::parse($oParserState);
+                               } else {
+                                       throw new SourceException("Unopened {", $oParserState->currentLine());
+                               }
+                       } else {
+                               return null;
+                       }
+               } else {
+                       return DeclarationBlock::parse($oParserState);
+               }
+       }
+
+       private static function parseAtRule(ParserState $oParserState) {
+               $oParserState->consume('@');
+               $sIdentifier = $oParserState->parseIdentifier();
+               $iIdentifierLineNum = $oParserState->currentLine();
+               $oParserState->consumeWhiteSpace();
+               if ($sIdentifier === 'import') {
+                       $oLocation = URL::parse($oParserState);
+                       $oParserState->consumeWhiteSpace();
+                       $sMediaQuery = null;
+                       if (!$oParserState->comes(';')) {
+                               $sMediaQuery = $oParserState->consumeUntil(';');
+                       }
+                       $oParserState->consume(';');
+                       return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
+               } else if ($sIdentifier === 'charset') {
+                       $sCharset = CSSString::parse($oParserState);
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume(';');
+                       return new Charset($sCharset, $iIdentifierLineNum);
+               } else if (self::identifierIs($sIdentifier, 'keyframes')) {
+                       $oResult = new KeyFrame($iIdentifierLineNum);
+                       $oResult->setVendorKeyFrame($sIdentifier);
+                       $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
+                       CSSList::parseList($oParserState, $oResult);
+                       return $oResult;
+               } else if ($sIdentifier === 'namespace') {
+                       $sPrefix = null;
+                       $mUrl = Value::parsePrimitiveValue($oParserState);
+                       if (!$oParserState->comes(';')) {
+                               $sPrefix = $mUrl;
+                               $mUrl = Value::parsePrimitiveValue($oParserState);
+                       }
+                       $oParserState->consume(';');
+                       if ($sPrefix !== null && !is_string($sPrefix)) {
+                               throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
+                       }
+                       if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
+                               throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
+                       }
+                       return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
+               } else {
+                       //Unknown other at rule (font-face or such)
+                       $sArgs = trim($oParserState->consumeUntil('{', false, true));
+                       if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
+                               if($oParserState->getSettings()->bLenientParsing) {
+                                       return NULL;
+                               } else {
+                                       throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
+                               }
+                       }
+                       $bUseRuleSet = true;
+                       foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
+                               if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
+                                       $bUseRuleSet = false;
+                                       break;
+                               }
+                       }
+                       if($bUseRuleSet) {
+                               $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
+                               RuleSet::parseRuleSet($oParserState, $oAtRule);
+                       } else {
+                               $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
+                               CSSList::parseList($oParserState, $oAtRule);
+                       }
+                       return $oAtRule;
+               }
+       }
+
+               /**
+        * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
+        */
+       private static function identifierIs($sIdentifier, $sMatch) {
+               return (strcasecmp($sIdentifier, $sMatch) === 0)
+                       ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
+       }
+
+
        /**
         * @return int
         */
@@ -31,27 +183,39 @@ abstract class CSSList implements Renderable, Commentable {
                return $this->iLineNo;
        }
 
+       /**
+        * Prepend item to list of contents.
+        *
+        * @param object $oItem Item.
+        */
+       public function prepend($oItem) {
+               array_unshift($this->aContents, $oItem);
+       }
+
+       /**
+        * Append item to list of contents.
+        *
+        * @param object $oItem Item.
+        */
        public function append($oItem) {
                $this->aContents[] = $oItem;
        }
 
        /**
-        * Insert an item before its sibling.
+        * Splice the list of contents.
         *
-        * @param mixed $oItem The item.
-        * @param mixed $oSibling The sibling.
+        * @param int       $iOffset      Offset.
+        * @param int       $iLength      Length. Optional.
+        * @param RuleSet[] $mReplacement Replacement. Optional.
         */
-       public function insert($oItem, $oSibling) {
-               $iIndex = array_search($oSibling, $this->aContents);
-               if ($iIndex === false) {
-                       return $this->append($oItem);
-               }
-               array_splice($this->aContents, $iIndex, 0, array($oItem));
+       public function splice($iOffset, $iLength = null, $mReplacement = null) {
+               array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
        }
 
        /**
         * Removes an item from the CSS list.
         * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
+        * @return bool Whether the item was removed.
         */
        public function remove($oItemToRemove) {
                $iKey = array_search($oItemToRemove, $this->aContents, true);
@@ -62,6 +226,19 @@ abstract class CSSList implements Renderable, Commentable {
                return false;
        }
 
+       /**
+        * Replaces an item from the CSS list.
+        * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
+        */
+       public function replace($oOldItem, $oNewItem) {
+               $iKey = array_search($oOldItem, $this->aContents, true);
+               if ($iKey !== false) {
+                       array_splice($this->aContents, $iKey, 1, $oNewItem);
+                       return true;
+               }
+               return false;
+       }
+
        /**
         * Set the contents.
         * @param array $aContents Objects to set as content.
index bd4a23e..873df75 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Sabberworm\CSS\CSSList;
 
+use Sabberworm\CSS\Parsing\ParserState;
+
 /**
  * The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
  */
@@ -14,6 +16,12 @@ class Document extends CSSBlockList {
                parent::__construct($iLineNo);
        }
 
+       public static function parse(ParserState $oParserState) {
+               $oDocument = new Document($oParserState->currentLine());
+               CSSList::parseList($oParserState, $oDocument);
+               return $oDocument;
+       }
+
        /**
         * Gets all DeclarationBlock objects recursively.
         */
index 1b17984..f7ebb5a 100644 (file)
@@ -4,6 +4,11 @@ namespace Sabberworm\CSS;
 
 use Sabberworm\CSS\Parsing\OutputException;
 
+/**
+ * Class OutputFormat
+ *
+ * @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule.
+ */
 class OutputFormat {
        /**
        * Value format
@@ -35,6 +40,10 @@ class OutputFormat {
        public $sSpaceAfterBlocks = '';
        public $sSpaceBetweenBlocks = "\n";
 
+       // Content injected in and around @-rule blocks.
+       public $sBeforeAtRuleBlock = '';
+       public $sAfterAtRuleBlock = '';
+
        // This is what’s printed before and after the comma if a declaration block contains multiple selectors.
        public $sSpaceBeforeSelectorSeparator = '';
        public $sSpaceAfterSelectorSeparator = ' ';
@@ -43,7 +52,12 @@ class OutputFormat {
        public $sSpaceAfterListArgumentSeparator = '';
        
        public $sSpaceBeforeOpeningBrace = ' ';
-       
+
+       // Content injected in and around declaration blocks.
+       public $sBeforeDeclarationBlock = '';
+       public $sAfterDeclarationBlockSelectors = '';
+       public $sAfterDeclarationBlock = '';
+
        /**
        * Indentation
        */
@@ -141,17 +155,36 @@ class OutputFormat {
        public function level() {
                return $this->iIndentationLevel;
        }
-       
+
+       /**
+        * Create format.
+        *
+        * @return OutputFormat Format.
+        */
        public static function create() {
                return new OutputFormat();
        }
-       
+
+       /**
+        * Create compact format.
+        *
+        * @return OutputFormat Format.
+        */
        public static function createCompact() {
-               return self::create()->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
+               $format = self::create();
+               $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
+               return $format;
        }
-       
+
+       /**
+        * Create pretty format.
+        *
+        * @return OutputFormat Format.
+        */
        public static function createPretty() {
-               return self::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+               $format = self::create();
+               $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+               return $format;
        }
 }
 
@@ -286,4 +319,4 @@ class OutputFormatter {
        private function indent() {
                return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
        }
-}
\ No newline at end of file
+}
index 65ea2f0..2520cb3 100644 (file)
@@ -2,41 +2,14 @@
 
 namespace Sabberworm\CSS;
 
-use Sabberworm\CSS\CSSList\CSSList;
 use Sabberworm\CSS\CSSList\Document;
-use Sabberworm\CSS\CSSList\KeyFrame;
-use Sabberworm\CSS\Parsing\SourceException;
-use Sabberworm\CSS\Property\AtRule;
-use Sabberworm\CSS\Property\Import;
-use Sabberworm\CSS\Property\Charset;
-use Sabberworm\CSS\Property\CSSNamespace;
-use Sabberworm\CSS\RuleSet\AtRuleSet;
-use Sabberworm\CSS\CSSList\AtRuleBlockList;
-use Sabberworm\CSS\RuleSet\DeclarationBlock;
-use Sabberworm\CSS\Value\CSSFunction;
-use Sabberworm\CSS\Value\RuleValueList;
-use Sabberworm\CSS\Value\Size;
-use Sabberworm\CSS\Value\Color;
-use Sabberworm\CSS\Value\URL;
-use Sabberworm\CSS\Value\CSSString;
-use Sabberworm\CSS\Rule\Rule;
-use Sabberworm\CSS\Parsing\UnexpectedTokenException;
-use Sabberworm\CSS\Comment\Comment;
+use Sabberworm\CSS\Parsing\ParserState;
 
 /**
  * Parser class parses CSS from text into a data structure.
  */
 class Parser {
-
-       private $sText;
-       private $aText;
-       private $iCurrentPosition;
-       private $oParserSettings;
-       private $sCharset;
-       private $iLength;
-       private $blockRules;
-       private $aSizeUnits;
-       private $iLineNo;
+       private $oParserState;
 
        /**
         * Parser constructor.
@@ -47,682 +20,22 @@ class Parser {
         * @param int $iLineNo
         */
        public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
-               $this->sText = $sText;
-               $this->iCurrentPosition = 0;
-               $this->iLineNo = $iLineNo;
                if ($oParserSettings === null) {
                        $oParserSettings = Settings::create();
                }
-               $this->oParserSettings = $oParserSettings;
-               $this->blockRules = explode('/', AtRule::BLOCK_RULES);
-
-               foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
-                       $iSize = strlen($val);
-                       if(!isset($this->aSizeUnits[$iSize])) {
-                               $this->aSizeUnits[$iSize] = array();
-                       }
-                       $this->aSizeUnits[$iSize][strtolower($val)] = $val;
-               }
-               ksort($this->aSizeUnits, SORT_NUMERIC);
+               $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
        }
 
        public function setCharset($sCharset) {
-               $this->sCharset = $sCharset;
-               $this->aText = $this->strsplit($this->sText);
-               $this->iLength = count($this->aText);
+               $this->oParserState->setCharset($sCharset);
        }
 
        public function getCharset() {
-               return $this->sCharset;
+               $this->oParserState->getCharset();
        }
 
        public function parse() {
-               $this->setCharset($this->oParserSettings->sDefaultCharset);
-               $oResult = new Document($this->iLineNo);
-               $this->parseDocument($oResult);
-               return $oResult;
-       }
-
-       private function parseDocument(Document $oDocument) {
-               $this->parseList($oDocument, true);
-       }
-
-       private function parseList(CSSList $oList, $bIsRoot = false) {
-               while (!$this->isEnd()) {
-                       $comments = $this->consumeWhiteSpace();
-                       $oListItem = null;
-                       if($this->oParserSettings->bLenientParsing) {
-                               try {
-                                       $oListItem = $this->parseListItem($oList, $bIsRoot);
-                               } catch (UnexpectedTokenException $e) {
-                                       $oListItem = false;
-                               }
-                       } else {
-                               $oListItem = $this->parseListItem($oList, $bIsRoot);
-                       }
-                       if($oListItem === null) {
-                               // List parsing finished
-                               return;
-                       }
-                       if($oListItem) {
-                               $oListItem->setComments($comments);
-                               $oList->append($oListItem);
-                       }
-               }
-               if (!$bIsRoot) {
-                       throw new SourceException("Unexpected end of document", $this->iLineNo);
-               }
-       }
-       
-       private function parseListItem(CSSList $oList, $bIsRoot = false) {
-               if ($this->comes('@')) {
-                       $oAtRule = $this->parseAtRule();
-                       if($oAtRule instanceof Charset) {
-                               if(!$bIsRoot) {
-                                       throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $this->iLineNo);
-                               }
-                               if(count($oList->getContents()) > 0) {
-                                       throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo);
-                               }
-                               $this->setCharset($oAtRule->getCharset()->getString());
-                       }
-                       return $oAtRule;
-               } else if ($this->comes('}')) {
-                       $this->consume('}');
-                       if ($bIsRoot) {
-                               throw new SourceException("Unopened {", $this->iLineNo);
-                       } else {
-                               return null;
-                       }
-               } else {
-                       return $this->parseSelector();
-               }
-       }
-
-       private function parseAtRule() {
-               $this->consume('@');
-               $sIdentifier = $this->parseIdentifier(false);
-               $iIdentifierLineNum = $this->iLineNo;
-               $this->consumeWhiteSpace();
-               if ($sIdentifier === 'import') {
-                       $oLocation = $this->parseURLValue();
-                       $this->consumeWhiteSpace();
-                       $sMediaQuery = null;
-                       if (!$this->comes(';')) {
-                               $sMediaQuery = $this->consumeUntil(';');
-                       }
-                       $this->consume(';');
-                       return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
-               } else if ($sIdentifier === 'charset') {
-                       $sCharset = $this->parseStringValue();
-                       $this->consumeWhiteSpace();
-                       $this->consume(';');
-                       return new Charset($sCharset, $iIdentifierLineNum);
-               } else if ($this->identifierIs($sIdentifier, 'keyframes')) {
-                       $oResult = new KeyFrame($iIdentifierLineNum);
-                       $oResult->setVendorKeyFrame($sIdentifier);
-                       $oResult->setAnimationName(trim($this->consumeUntil('{', false, true)));
-                       $this->parseList($oResult);
-                       return $oResult;
-               } else if ($sIdentifier === 'namespace') {
-                       $sPrefix = null;
-                       $mUrl = $this->parsePrimitiveValue();
-                       if (!$this->comes(';')) {
-                               $sPrefix = $mUrl;
-                               $mUrl = $this->parsePrimitiveValue();
-                       }
-                       $this->consume(';');
-                       if ($sPrefix !== null && !is_string($sPrefix)) {
-                               throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
-                       }
-                       if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
-                               throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
-                       }
-                       return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
-               } else {
-                       //Unknown other at rule (font-face or such)
-                       $sArgs = trim($this->consumeUntil('{', false, true));
-                       $bUseRuleSet = true;
-                       foreach($this->blockRules as $sBlockRuleName) {
-                               if($this->identifierIs($sIdentifier, $sBlockRuleName)) {
-                                       $bUseRuleSet = false;
-                                       break;
-                               }
-                       }
-                       if($bUseRuleSet) {
-                               $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
-                               $this->parseRuleSet($oAtRule);
-                       } else {
-                               $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
-                               $this->parseList($oAtRule);
-                       }
-                       return $oAtRule;
-               }
-       }
-
-       private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
-               $sResult = $this->parseCharacter(true);
-               if ($sResult === null) {
-                       throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
-               }
-               $sCharacter = null;
-               while (($sCharacter = $this->parseCharacter(true)) !== null) {
-                       $sResult .= $sCharacter;
-               }
-               if ($bIgnoreCase) {
-                       $sResult = $this->strtolower($sResult);
-               }
-               if ($bAllowFunctions && $this->comes('(')) {
-                       $this->consume('(');
-                       $aArguments = $this->parseValue(array('=', ' ', ','));
-                       $sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo);
-                       $this->consume(')');
-               }
-               return $sResult;
-       }
-
-       private function parseStringValue() {
-               $sBegin = $this->peek();
-               $sQuote = null;
-               if ($sBegin === "'") {
-                       $sQuote = "'";
-               } else if ($sBegin === '"') {
-                       $sQuote = '"';
-               }
-               if ($sQuote !== null) {
-                       $this->consume($sQuote);
-               }
-               $sResult = "";
-               $sContent = null;
-               if ($sQuote === null) {
-                       //Unquoted strings end in whitespace or with braces, brackets, parentheses
-                       while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) {
-                               $sResult .= $this->parseCharacter(false);
-                       }
-               } else {
-                       while (!$this->comes($sQuote)) {
-                               $sContent = $this->parseCharacter(false);
-                               if ($sContent === null) {
-                                       throw new SourceException("Non-well-formed quoted string {$this->peek(3)}", $this->iLineNo);
-                               }
-                               $sResult .= $sContent;
-                       }
-                       $this->consume($sQuote);
-               }
-               return new CSSString($sResult, $this->iLineNo);
-       }
-
-       private function parseCharacter($bIsForIdentifier) {
-               if ($this->peek() === '\\') {
-                       if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
-                               // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
-                               return null;
-                       }
-                       $this->consume('\\');
-                       if ($this->comes('\n') || $this->comes('\r')) {
-                               return '';
-                       }
-                       if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
-                               return $this->consume(1);
-                       }
-                       $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
-                       if ($this->strlen($sUnicode) < 6) {
-                               //Consume whitespace after incomplete unicode escape
-                               if (preg_match('/\\s/isSu', $this->peek())) {
-                                       if ($this->comes('\r\n')) {
-                                               $this->consume(2);
-                                       } else {
-                                               $this->consume(1);
-                                       }
-                               }
-                       }
-                       $iUnicode = intval($sUnicode, 16);
-                       $sUtf32 = "";
-                       for ($i = 0; $i < 4; ++$i) {
-                               $sUtf32 .= chr($iUnicode & 0xff);
-                               $iUnicode = $iUnicode >> 8;
-                       }
-                       return iconv('utf-32le', $this->sCharset, $sUtf32);
-               }
-               if ($bIsForIdentifier) {
-                       $peek = ord($this->peek());
-                       // Ranges: a-z A-Z 0-9 - _
-                       if (($peek >= 97 && $peek <= 122) ||
-                               ($peek >= 65 && $peek <= 90) ||
-                               ($peek >= 48 && $peek <= 57) ||
-                               ($peek === 45) ||
-                               ($peek === 95) ||
-                               ($peek > 0xa1)) {
-                               return $this->consume(1);
-                       }
-               } else {
-                       return $this->consume(1);
-               }
-               return null;
-       }
-
-       private function parseSelector() {
-               $aComments = array();
-               $oResult = new DeclarationBlock($this->iLineNo);
-               $oResult->setSelector($this->consumeUntil('{', false, true, $aComments));
-               $oResult->setComments($aComments);
-               $this->parseRuleSet($oResult);
-               return $oResult;
-       }
-
-       private function parseRuleSet($oRuleSet) {
-               while ($this->comes(';')) {
-                       $this->consume(';');
-               }
-               while (!$this->comes('}')) {
-                       $oRule = null;
-                       if($this->oParserSettings->bLenientParsing) {
-                               try {
-                                       $oRule = $this->parseRule();
-                               } catch (UnexpectedTokenException $e) {
-                                       try {
-                                               $sConsume = $this->consumeUntil(array("\n", ";", '}'), true);
-                                               // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
-                                               if($this->streql(substr($sConsume, -1), '}')) {
-                                                       --$this->iCurrentPosition;
-                                               } else {
-                                                       while ($this->comes(';')) {
-                                                               $this->consume(';');
-                                                       }
-                                               }
-                                       } catch (UnexpectedTokenException $e) {
-                                               // We’ve reached the end of the document. Just close the RuleSet.
-                                               return;
-                                       }
-                               }
-                       } else {
-                               $oRule = $this->parseRule();
-                       }
-                       if($oRule) {
-                               $oRuleSet->addRule($oRule);
-                       }
-               }
-               $this->consume('}');
-       }
-
-       private function parseRule() {
-               $aComments = $this->consumeWhiteSpace();
-               $oRule = new Rule($this->parseIdentifier(), $this->iLineNo);
-               $oRule->setComments($aComments);
-               $oRule->addComments($this->consumeWhiteSpace());
-               $this->consume(':');
-               $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule()));
-               $oRule->setValue($oValue);
-               if ($this->oParserSettings->bLenientParsing) {
-                       while ($this->comes('\\')) {
-                               $this->consume('\\');
-                               $oRule->addIeHack($this->consume());
-                               $this->consumeWhiteSpace();
-                       }
-               }
-               if ($this->comes('!')) {
-                       $this->consume('!');
-                       $this->consumeWhiteSpace();
-                       $this->consume('important');
-                       $oRule->setIsImportant(true);
-               }
-               while ($this->comes(';')) {
-                       $this->consume(';');
-               }
-               return $oRule;
-       }
-
-       private function parseValue($aListDelimiters) {
-               $aStack = array();
-               $this->consumeWhiteSpace();
-               //Build a list of delimiters and parsed values
-               while (!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')') || $this->comes('\\'))) {
-                       if (count($aStack) > 0) {
-                               $bFoundDelimiter = false;
-                               foreach ($aListDelimiters as $sDelimiter) {
-                                       if ($this->comes($sDelimiter)) {
-                                               array_push($aStack, $this->consume($sDelimiter));
-                                               $this->consumeWhiteSpace();
-                                               $bFoundDelimiter = true;
-                                               break;
-                                       }
-                               }
-                               if (!$bFoundDelimiter) {
-                                       //Whitespace was the list delimiter
-                                       array_push($aStack, ' ');
-                               }
-                       }
-                       array_push($aStack, $this->parsePrimitiveValue());
-                       $this->consumeWhiteSpace();
-               }
-               //Convert the list to list objects
-               foreach ($aListDelimiters as $sDelimiter) {
-                       if (count($aStack) === 1) {
-                               return $aStack[0];
-                       }
-                       $iStartPosition = null;
-                       while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
-                               $iLength = 2; //Number of elements to be joined
-                               for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
-                                       if ($sDelimiter !== $aStack[$i]) {
-                                               break;
-                                       }
-                               }
-                               $oList = new RuleValueList($sDelimiter, $this->iLineNo);
-                               for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
-                                       $oList->addListComponent($aStack[$i]);
-                               }
-                               array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
-                       }
-               }
-               return $aStack[0];
-       }
-
-       private static function listDelimiterForRule($sRule) {
-               if (preg_match('/^font($|-)/', $sRule)) {
-                       return array(',', '/', ' ');
-               }
-               return array(',', ' ', '/');
-       }
-
-       private function parsePrimitiveValue() {
-               $oValue = null;
-               $this->consumeWhiteSpace();
-               if (is_numeric($this->peek()) || ($this->comes('-.') && is_numeric($this->peek(1, 2))) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) {
-                       $oValue = $this->parseNumericValue();
-               } else if ($this->comes('#') || $this->comes('rgb', true) || $this->comes('hsl', true)) {
-                       $oValue = $this->parseColorValue();
-               } else if ($this->comes('url', true)) {
-                       $oValue = $this->parseURLValue();
-               } else if ($this->comes("'") || $this->comes('"')) {
-                       $oValue = $this->parseStringValue();
-               } else if ($this->comes("progid:") && $this->oParserSettings->bLenientParsing) {
-                       $oValue = $this->parseMicrosoftFilter();
-               } else {
-                       $oValue = $this->parseIdentifier(true, false);
-               }
-               $this->consumeWhiteSpace();
-               return $oValue;
-       }
-
-       private function parseNumericValue($bForColor = false) {
-               $sSize = '';
-               if ($this->comes('-')) {
-                       $sSize .= $this->consume('-');
-               }
-               while (is_numeric($this->peek()) || $this->comes('.')) {
-                       if ($this->comes('.')) {
-                               $sSize .= $this->consume('.');
-                       } else {
-                               $sSize .= $this->consume(1);
-                       }
-               }
-
-               $sUnit = null;
-               foreach ($this->aSizeUnits as $iLength => &$aValues) {
-                       $sKey = strtolower($this->peek($iLength));
-                       if(array_key_exists($sKey, $aValues)) {
-                               if (($sUnit = $aValues[$sKey]) !== null) {
-                                       $this->consume($iLength);
-                                       break;
-                               }
-                       }
-               }
-               return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo);
-       }
-
-       private function parseColorValue() {
-               $aColor = array();
-               if ($this->comes('#')) {
-                       $this->consume('#');
-                       $sValue = $this->parseIdentifier(false);
-                       if ($this->strlen($sValue) === 3) {
-                               $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
-                       }
-                       $aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $this->iLineNo), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $this->iLineNo), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $this->iLineNo));
-               } else {
-                       $sColorMode = $this->parseIdentifier(false);
-                       $this->consumeWhiteSpace();
-                       $this->consume('(');
-                       $iLength = $this->strlen($sColorMode);
-                       for ($i = 0; $i < $iLength; ++$i) {
-                               $this->consumeWhiteSpace();
-                               $aColor[$sColorMode[$i]] = $this->parseNumericValue(true);
-                               $this->consumeWhiteSpace();
-                               if ($i < ($iLength - 1)) {
-                                       $this->consume(',');
-                               }
-                       }
-                       $this->consume(')');
-               }
-               return new Color($aColor, $this->iLineNo);
-       }
-
-       private function parseMicrosoftFilter() {
-               $sFunction = $this->consumeUntil('(', false, true);
-               $aArguments = $this->parseValue(array(',', '='));
-               return new CSSFunction($sFunction, $aArguments, ',', $this->iLineNo);
-       }
-
-       private function parseURLValue() {
-               $bUseUrl = $this->comes('url', true);
-               if ($bUseUrl) {
-                       $this->consume('url');
-                       $this->consumeWhiteSpace();
-                       $this->consume('(');
-               }
-               $this->consumeWhiteSpace();
-               $oResult = new URL($this->parseStringValue(), $this->iLineNo);
-               if ($bUseUrl) {
-                       $this->consumeWhiteSpace();
-                       $this->consume(')');
-               }
-               return $oResult;
-       }
-
-       /**
-        * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
-        */
-       private function identifierIs($sIdentifier, $sMatch) {
-               return (strcasecmp($sIdentifier, $sMatch) === 0)
-                       ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
-       }
-
-       private function comes($sString, $bCaseInsensitive = false) {
-               $sPeek = $this->peek(strlen($sString));
-               return ($sPeek == '')
-                       ? false
-                       : $this->streql($sPeek, $sString, $bCaseInsensitive);
-       }
-
-       private function peek($iLength = 1, $iOffset = 0) {
-               $iOffset += $this->iCurrentPosition;
-               if ($iOffset >= $this->iLength) {
-                       return '';
-               }
-               return $this->substr($iOffset, $iLength);
-       }
-
-       private function consume($mValue = 1) {
-               if (is_string($mValue)) {
-                       $iLineCount = substr_count($mValue, "\n");
-                       $iLength = $this->strlen($mValue);
-                       if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
-                               throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
-                       }
-                       $this->iLineNo += $iLineCount;
-                       $this->iCurrentPosition += $this->strlen($mValue);
-                       return $mValue;
-               } else {
-                       if ($this->iCurrentPosition + $mValue > $this->iLength) {
-                               throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
-                       }
-                       $sResult = $this->substr($this->iCurrentPosition, $mValue);
-                       $iLineCount = substr_count($sResult, "\n");
-                       $this->iLineNo += $iLineCount;
-                       $this->iCurrentPosition += $mValue;
-                       return $sResult;
-               }
-       }
-
-       private function consumeExpression($mExpression, $iMaxLength = null) {
-               $aMatches = null;
-               $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
-               if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
-                       return $this->consume($aMatches[0][0]);
-               }
-               throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
-       }
-
-       private function consumeWhiteSpace() {
-               $comments = array();
-               do {
-                       while (preg_match('/\\s/isSu', $this->peek()) === 1) {
-                               $this->consume(1);
-                       }
-                       if($this->oParserSettings->bLenientParsing) {
-                               try {
-                                       $oComment = $this->consumeComment();
-                               } catch(UnexpectedTokenException $e) {
-                                       // When we can’t find the end of a comment, we assume the document is finished.
-                                       $this->iCurrentPosition = $this->iLength;
-                                       return;
-                               }
-                       } else {
-                               $oComment = $this->consumeComment();
-                       }
-                       if ($oComment !== false) {
-                               $comments[] = $oComment;
-                       }
-               } while($oComment !== false);
-               return $comments;
-       }
-
-       /**
-        * @return false|Comment
-        */
-       private function consumeComment() {
-               $mComment = false;
-               if ($this->comes('/*')) {
-                       $iLineNo = $this->iLineNo;
-                       $this->consume(1);
-                       $mComment = '';
-                       while (($char = $this->consume(1)) !== '') {
-                               $mComment .= $char;
-                               if ($this->comes('*/')) {
-                                       $this->consume(2);
-                                       break;
-                               }
-                       }
-               }
-
-               if ($mComment !== false) {
-                       // We skip the * which was included in the comment.
-                       return new Comment(substr($mComment, 1), $iLineNo);
-               }
-
-               return $mComment;
-       }
-
-       private function isEnd() {
-               return $this->iCurrentPosition >= $this->iLength;
-       }
-
-       private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
-               $aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
-               $out = '';
-               $start = $this->iCurrentPosition;
-
-               while (($char = $this->consume(1)) !== '') {
-                       if (in_array($char, $aEnd)) {
-                               if ($bIncludeEnd) {
-                                       $out .= $char;
-                               } elseif (!$consumeEnd) {
-                                       $this->iCurrentPosition -= $this->strlen($char);
-                               }
-                               return $out;
-                       }
-                       $out .= $char;
-                       if ($comment = $this->consumeComment()) {
-                               $comments[] = $comment;
-                       }
-               }
-
-               $this->iCurrentPosition = $start;
-               throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
-       }
-
-       private function inputLeft() {
-               return $this->substr($this->iCurrentPosition, -1);
-       }
-
-       private function substr($iStart, $iLength) {
-               if ($iLength < 0) {
-                       $iLength = $this->iLength - $iStart + $iLength;
-               }
-               if ($iStart + $iLength > $this->iLength) {
-                       $iLength = $this->iLength - $iStart;
-               }
-               $sResult = '';
-               while ($iLength > 0) {
-                       $sResult .= $this->aText[$iStart];
-                       $iStart++;
-                       $iLength--;
-               }
-               return $sResult;
-       }
-
-       private function strlen($sString) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       return mb_strlen($sString, $this->sCharset);
-               } else {
-                       return strlen($sString);
-               }
-       }
-
-       private function streql($sString1, $sString2, $bCaseInsensitive = true) {
-               if($bCaseInsensitive) {
-                       return $this->strtolower($sString1) === $this->strtolower($sString2);
-               } else {
-                       return $sString1 === $sString2;
-               }
-       }
-
-       private function strtolower($sString) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       return mb_strtolower($sString, $this->sCharset);
-               } else {
-                       return strtolower($sString);
-               }
-       }
-
-       private function strsplit($sString) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       if ($this->streql($this->sCharset, 'utf-8')) {
-                               return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
-                       } else {
-                               $iLength = mb_strlen($sString, $this->sCharset);
-                               $aResult = array();
-                               for ($i = 0; $i < $iLength; ++$i) {
-                                       $aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
-                               }
-                               return $aResult;
-                       }
-               } else {
-                       if($sString === '') {
-                               return array();
-                       } else {
-                               return str_split($sString);
-                       }
-               }
-       }
-
-       private function strpos($sString, $sNeedle, $iOffset) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
-               } else {
-                       return strpos($sString, $sNeedle, $iOffset);
-               }
+               return Document::parse($this->oParserState);
        }
 
 }
diff --git a/lib/php-css-parser/Parsing/ParserState.php b/lib/php-css-parser/Parsing/ParserState.php
new file mode 100644 (file)
index 0000000..4305c9a
--- /dev/null
@@ -0,0 +1,310 @@
+<?php
+namespace Sabberworm\CSS\Parsing;
+
+use Sabberworm\CSS\Comment\Comment;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Settings;
+
+class ParserState {
+       private $oParserSettings;
+
+       private $sText;
+
+       private $aText;
+       private $iCurrentPosition;
+       private $sCharset;
+       private $iLength;
+       private $iLineNo;
+
+       public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) {
+               $this->oParserSettings = $oParserSettings;
+               $this->sText = $sText;
+               $this->iCurrentPosition = 0;
+               $this->iLineNo = $iLineNo;
+               $this->setCharset($this->oParserSettings->sDefaultCharset);
+       }
+
+       public function setCharset($sCharset) {
+               $this->sCharset = $sCharset;
+               $this->aText = $this->strsplit($this->sText);
+               $this->iLength = count($this->aText);
+       }
+
+       public function getCharset() {
+               $this->oParserHelper->getCharset();
+               return $this->sCharset;
+       }
+
+       public function currentLine() {
+               return $this->iLineNo;
+       }
+
+       public function getSettings() {
+               return $this->oParserSettings;
+       }
+
+       public function parseIdentifier($bIgnoreCase = true) {
+               $sResult = $this->parseCharacter(true);
+               if ($sResult === null) {
+                       throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
+               }
+               $sCharacter = null;
+               while (($sCharacter = $this->parseCharacter(true)) !== null) {
+                       $sResult .= $sCharacter;
+               }
+               if ($bIgnoreCase) {
+                       $sResult = $this->strtolower($sResult);
+               }
+               return $sResult;
+       }
+
+       public function parseCharacter($bIsForIdentifier) {
+               if ($this->peek() === '\\') {
+                       if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
+                               // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
+                               return null;
+                       }
+                       $this->consume('\\');
+                       if ($this->comes('\n') || $this->comes('\r')) {
+                               return '';
+                       }
+                       if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
+                               return $this->consume(1);
+                       }
+                       $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
+                       if ($this->strlen($sUnicode) < 6) {
+                               //Consume whitespace after incomplete unicode escape
+                               if (preg_match('/\\s/isSu', $this->peek())) {
+                                       if ($this->comes('\r\n')) {
+                                               $this->consume(2);
+                                       } else {
+                                               $this->consume(1);
+                                       }
+                               }
+                       }
+                       $iUnicode = intval($sUnicode, 16);
+                       $sUtf32 = "";
+                       for ($i = 0; $i < 4; ++$i) {
+                               $sUtf32 .= chr($iUnicode & 0xff);
+                               $iUnicode = $iUnicode >> 8;
+                       }
+                       return iconv('utf-32le', $this->sCharset, $sUtf32);
+               }
+               if ($bIsForIdentifier) {
+                       $peek = ord($this->peek());
+                       // Ranges: a-z A-Z 0-9 - _
+                       if (($peek >= 97 && $peek <= 122) ||
+                               ($peek >= 65 && $peek <= 90) ||
+                               ($peek >= 48 && $peek <= 57) ||
+                               ($peek === 45) ||
+                               ($peek === 95) ||
+                               ($peek > 0xa1)) {
+                               return $this->consume(1);
+                       }
+               } else {
+                       return $this->consume(1);
+               }
+               return null;
+       }
+
+       public function consumeWhiteSpace() {
+               $comments = array();
+               do {
+                       while (preg_match('/\\s/isSu', $this->peek()) === 1) {
+                               $this->consume(1);
+                       }
+                       if($this->oParserSettings->bLenientParsing) {
+                               try {
+                                       $oComment = $this->consumeComment();
+                               } catch(UnexpectedTokenException $e) {
+                                       // When we can’t find the end of a comment, we assume the document is finished.
+                                       $this->iCurrentPosition = $this->iLength;
+                                       return;
+                               }
+                       } else {
+                               $oComment = $this->consumeComment();
+                       }
+                       if ($oComment !== false) {
+                               $comments[] = $oComment;
+                       }
+               } while($oComment !== false);
+               return $comments;
+       }
+
+       public function comes($sString, $bCaseInsensitive = false) {
+               $sPeek = $this->peek(strlen($sString));
+               return ($sPeek == '')
+                       ? false
+                       : $this->streql($sPeek, $sString, $bCaseInsensitive);
+       }
+
+       public function peek($iLength = 1, $iOffset = 0) {
+               $iOffset += $this->iCurrentPosition;
+               if ($iOffset >= $this->iLength) {
+                       return '';
+               }
+               return $this->substr($iOffset, $iLength);
+       }
+
+       public function consume($mValue = 1) {
+               if (is_string($mValue)) {
+                       $iLineCount = substr_count($mValue, "\n");
+                       $iLength = $this->strlen($mValue);
+                       if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
+                               throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
+                       }
+                       $this->iLineNo += $iLineCount;
+                       $this->iCurrentPosition += $this->strlen($mValue);
+                       return $mValue;
+               } else {
+                       if ($this->iCurrentPosition + $mValue > $this->iLength) {
+                               throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
+                       }
+                       $sResult = $this->substr($this->iCurrentPosition, $mValue);
+                       $iLineCount = substr_count($sResult, "\n");
+                       $this->iLineNo += $iLineCount;
+                       $this->iCurrentPosition += $mValue;
+                       return $sResult;
+               }
+       }
+
+       public function consumeExpression($mExpression, $iMaxLength = null) {
+               $aMatches = null;
+               $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
+               if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
+                       return $this->consume($aMatches[0][0]);
+               }
+               throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
+       }
+
+       /**
+        * @return false|Comment
+        */
+       public function consumeComment() {
+               $mComment = false;
+               if ($this->comes('/*')) {
+                       $iLineNo = $this->iLineNo;
+                       $this->consume(1);
+                       $mComment = '';
+                       while (($char = $this->consume(1)) !== '') {
+                               $mComment .= $char;
+                               if ($this->comes('*/')) {
+                                       $this->consume(2);
+                                       break;
+                               }
+                       }
+               }
+
+               if ($mComment !== false) {
+                       // We skip the * which was included in the comment.
+                       return new Comment(substr($mComment, 1), $iLineNo);
+               }
+
+               return $mComment;
+       }
+
+       public function isEnd() {
+               return $this->iCurrentPosition >= $this->iLength;
+       }
+
+       public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
+               $aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
+               $out = '';
+               $start = $this->iCurrentPosition;
+
+               while (($char = $this->consume(1)) !== '') {
+                       if (in_array($char, $aEnd)) {
+                               if ($bIncludeEnd) {
+                                       $out .= $char;
+                               } elseif (!$consumeEnd) {
+                                       $this->iCurrentPosition -= $this->strlen($char);
+                               }
+                               return $out;
+                       }
+                       $out .= $char;
+                       if ($comment = $this->consumeComment()) {
+                               $comments[] = $comment;
+                       }
+               }
+
+               $this->iCurrentPosition = $start;
+               throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
+       }
+
+       private function inputLeft() {
+               return $this->substr($this->iCurrentPosition, -1);
+       }
+
+       public function streql($sString1, $sString2, $bCaseInsensitive = true) {
+               if($bCaseInsensitive) {
+                       return $this->strtolower($sString1) === $this->strtolower($sString2);
+               } else {
+                       return $sString1 === $sString2;
+               }
+       }
+
+       public function backtrack($iAmount) {
+               $this->iCurrentPosition -= $iAmount;
+       }
+
+       public function strlen($sString) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       return mb_strlen($sString, $this->sCharset);
+               } else {
+                       return strlen($sString);
+               }       
+       }       
+
+       private function substr($iStart, $iLength) {
+               if ($iLength < 0) {
+                       $iLength = $this->iLength - $iStart + $iLength;
+               }       
+               if ($iStart + $iLength > $this->iLength) {
+                       $iLength = $this->iLength - $iStart;
+               }       
+               $sResult = '';
+               while ($iLength > 0) {
+                       $sResult .= $this->aText[$iStart];
+                       $iStart++;
+                       $iLength--;
+               }       
+               return $sResult;
+       }
+
+       private function strtolower($sString) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       return mb_strtolower($sString, $this->sCharset);
+               } else {
+                       return strtolower($sString);
+               }
+       }
+
+       private function strsplit($sString) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       if ($this->streql($this->sCharset, 'utf-8')) {
+                               return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
+                       } else {
+                               $iLength = mb_strlen($sString, $this->sCharset);
+                               $aResult = array();
+                               for ($i = 0; $i < $iLength; ++$i) {
+                                       $aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
+                               }
+                               return $aResult;
+                       }
+               } else {
+                       if($sString === '') {
+                               return array();
+                       } else {
+                               return str_split($sString);
+                       }
+               }
+       }
+
+       private function strpos($sString, $sNeedle, $iOffset) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
+               } else {
+                       return strpos($sString, $sNeedle, $iOffset);
+               }
+       }
+}
\ No newline at end of file
index de3eea1..b20c8c6 100644 (file)
@@ -6,9 +6,10 @@ use Sabberworm\CSS\Renderable;
 use Sabberworm\CSS\Comment\Commentable;
 
 interface AtRule extends Renderable, Commentable {
-       const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
        // Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule.
-       const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; //…and more font-specific ones (to be used inside font-feature-values)
+       const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
+       // …and more font-specific ones (to be used inside font-feature-values)
+       const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
        
        public function atRuleName();
        public function atRuleArgs();
index 3e48537..3fa031b 100644 (file)
@@ -2,10 +2,11 @@
 
 namespace Sabberworm\CSS\Rule;
 
+use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Parsing\ParserState;
 use Sabberworm\CSS\Renderable;
 use Sabberworm\CSS\Value\RuleValueList;
 use Sabberworm\CSS\Value\Value;
-use Sabberworm\CSS\Comment\Commentable;
 
 /**
  * RuleSets contains Rule objects which always have a key and a value.
@@ -29,6 +30,44 @@ class Rule implements Renderable, Commentable {
                $this->aComments = array();
        }
 
+       public static function parse(ParserState $oParserState) {
+               $aComments = $oParserState->consumeWhiteSpace();
+               $oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine());
+               $oRule->setComments($aComments);
+               $oRule->addComments($oParserState->consumeWhiteSpace());
+               $oParserState->consume(':');
+               $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
+               $oRule->setValue($oValue);
+               if ($oParserState->getSettings()->bLenientParsing) {
+                       while ($oParserState->comes('\\')) {
+                               $oParserState->consume('\\');
+                               $oRule->addIeHack($oParserState->consume());
+                               $oParserState->consumeWhiteSpace();
+                       }
+               }
+               $oParserState->consumeWhiteSpace();
+               if ($oParserState->comes('!')) {
+                       $oParserState->consume('!');
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume('important');
+                       $oRule->setIsImportant(true);
+               }
+               $oParserState->consumeWhiteSpace();
+               while ($oParserState->comes(';')) {
+                       $oParserState->consume(';');
+               }
+               $oParserState->consumeWhiteSpace();
+
+               return $oRule;
+       }
+
+       private static function listDelimiterForRule($sRule) {
+               if (preg_match('/^font($|-)/', $sRule)) {
+                       return array(',', '/', ' ');
+               }
+               return array(',', ' ', '/');
+       }
+
        /**
         * @return int
         */
index e18f5d8..6614b1d 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Sabberworm\CSS\RuleSet;
 
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\OutputException;
 use Sabberworm\CSS\Property\Selector;
 use Sabberworm\CSS\Rule\Rule;
 use Sabberworm\CSS\Value\RuleValueList;
@@ -9,7 +11,6 @@ use Sabberworm\CSS\Value\Value;
 use Sabberworm\CSS\Value\Size;
 use Sabberworm\CSS\Value\Color;
 use Sabberworm\CSS\Value\URL;
-use Sabberworm\CSS\Parsing\OutputException;
 
 /**
  * Declaration blocks are the parts of a css file which denote the rules belonging to a selector.
@@ -24,6 +25,16 @@ class DeclarationBlock extends RuleSet {
                $this->aSelectors = array();
        }
 
+       public static function parse(ParserState $oParserState) {
+               $aComments = array();
+               $oResult = new DeclarationBlock($oParserState->currentLine());
+               $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments));
+               $oResult->setComments($aComments);
+               RuleSet::parseRuleSet($oParserState, $oResult);
+               return $oResult;
+       }
+
+
        public function setSelectors($mSelector) {
                if (is_array($mSelector)) {
                        $this->aSelectors = $mSelector;
@@ -65,6 +76,11 @@ class DeclarationBlock extends RuleSet {
                $this->setSelectors($mSelector);
        }
 
+       /**
+        * Get selectors.
+        *
+        * @return Selector[] Selectors.
+        */
        public function getSelectors() {
                return $this->aSelectors;
        }
@@ -599,9 +615,13 @@ class DeclarationBlock extends RuleSet {
                        // If all the selectors have been removed, this declaration block becomes invalid
                        throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
                }
-               $sResult = $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors) . $oOutputFormat->spaceBeforeOpeningBrace() . '{';
+               $sResult = $oOutputFormat->sBeforeDeclarationBlock;
+               $sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
+               $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
+               $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
                $sResult .= parent::render($oOutputFormat);
                $sResult .= '}';
+               $sResult .= $oOutputFormat->sAfterDeclarationBlock;
                return $sResult;
        }
 
index 124be88..e5d5e41 100644 (file)
@@ -2,9 +2,11 @@
 
 namespace Sabberworm\CSS\RuleSet;
 
-use Sabberworm\CSS\Rule\Rule;
-use Sabberworm\CSS\Renderable;
 use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\Rule\Rule;
 
 /**
  * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
@@ -22,6 +24,41 @@ abstract class RuleSet implements Renderable, Commentable {
                $this->aComments = array();
        }
 
+       public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) {
+               while ($oParserState->comes(';')) {
+                       $oParserState->consume(';');
+               }
+               while (!$oParserState->comes('}')) {
+                       $oRule = null;
+                       if($oParserState->getSettings()->bLenientParsing) {
+                               try {
+                                       $oRule = Rule::parse($oParserState);
+                               } catch (UnexpectedTokenException $e) {
+                                       try {
+                                               $sConsume = $oParserState->consumeUntil(array("\n", ";", '}'), true);
+                                               // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
+                                               if($oParserState->streql(substr($sConsume, -1), '}')) {
+                                                       $oParserState->backtrack(1);
+                                               } else {
+                                                       while ($oParserState->comes(';')) {
+                                                               $oParserState->consume(';');
+                                                       }
+                                               }
+                                       } catch (UnexpectedTokenException $e) {
+                                               // We’ve reached the end of the document. Just close the RuleSet.
+                                               return;
+                                       }
+                               }
+                       } else {
+                               $oRule = Rule::parse($oParserState);
+                       }
+                       if($oRule) {
+                               $oRuleSet->addRule($oRule);
+                       }
+               }
+               $oParserState->consume('}');
+       }
+
        /**
         * @return int
         */
@@ -52,6 +89,7 @@ abstract class RuleSet implements Renderable, Commentable {
         * @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
         * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font.
         * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array().
+        * @return Rule[] Rules.
         */
        public function getRules($mRule = null) {
                if ($mRule instanceof Rule) {
@@ -69,7 +107,7 @@ abstract class RuleSet implements Renderable, Commentable {
 
        /**
         * Override all the rules of this set.
-        * @param array $aRules The rules to override with.
+        * @param Rule[] $aRules The rules to override with.
         */
        public function setRules(array $aRules) {
                $this->aRules = array();
@@ -82,6 +120,7 @@ abstract class RuleSet implements Renderable, Commentable {
         * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
         * @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
         * Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both.
+        * @return Rule[] Rules.
         */
        public function getRulesAssoc($mRule = null) {
                $aResult = array();
@@ -92,9 +131,9 @@ abstract class RuleSet implements Renderable, Commentable {
        }
 
        /**
-       * Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
- * @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
-       */
+        * Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
       * @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
+        */
        public function removeRule($mRule) {
                if($mRule instanceof Rule) {
                        $sRule = $mRule->getRule();
index 3633abc..941df23 100644 (file)
@@ -4,7 +4,7 @@ namespace Sabberworm\CSS\Value;
 
 class CSSFunction extends ValueList {
 
-       private $sName;
+       protected $sName;
 
        public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) {
                if($aArguments instanceof RuleValueList) {
@@ -37,4 +37,4 @@ class CSSFunction extends ValueList {
                return "{$this->sName}({$aArguments})";
        }
 
-}
\ No newline at end of file
+}
index b070008..9f9c050 100644 (file)
@@ -2,6 +2,9 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\SourceException;
+
 class CSSString extends PrimitiveValue {
 
        private $sString;
@@ -11,6 +14,37 @@ class CSSString extends PrimitiveValue {
                parent::__construct($iLineNo);
        }
 
+       public static function parse(ParserState $oParserState) {
+               $sBegin = $oParserState->peek();
+               $sQuote = null;
+               if ($sBegin === "'") {
+                       $sQuote = "'";
+               } else if ($sBegin === '"') {
+                       $sQuote = '"';
+               }
+               if ($sQuote !== null) {
+                       $oParserState->consume($sQuote);
+               }
+               $sResult = "";
+               $sContent = null;
+               if ($sQuote === null) {
+                       // Unquoted strings end in whitespace or with braces, brackets, parentheses
+                       while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
+                               $sResult .= $oParserState->parseCharacter(false);
+                       }
+               } else {
+                       while (!$oParserState->comes($sQuote)) {
+                               $sContent = $oParserState->parseCharacter(false);
+                               if ($sContent === null) {
+                                       throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine());
+                               }
+                               $sResult .= $sContent;
+                       }
+                       $oParserState->consume($sQuote);
+               }
+               return new CSSString($sResult, $oParserState->currentLine());
+       }
+
        public function setString($sString) {
                $this->sString = $sString;
        }
diff --git a/lib/php-css-parser/Value/CalcFunction.php b/lib/php-css-parser/Value/CalcFunction.php
new file mode 100644 (file)
index 0000000..9247520
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace Sabberworm\CSS\Value;
+
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+
+class CalcFunction extends CSSFunction {
+       const T_OPERAND  = 1;
+       const T_OPERATOR = 2;
+
+       public static function parse(ParserState $oParserState) {
+               $aOperators = array('+', '-', '*', '/');
+               $sFunction = trim($oParserState->consumeUntil('(', false, true));
+               $oCalcList = new CalcRuleValueList($oParserState->currentLine());
+               $oList = new RuleValueList(',', $oParserState->currentLine());
+               $iNestingLevel = 0;
+               $iLastComponentType = NULL;
+               while(!$oParserState->comes(')') || $iNestingLevel > 0) {
+                       $oParserState->consumeWhiteSpace();
+                       if ($oParserState->comes('(')) {
+                               $iNestingLevel++;
+                               $oCalcList->addListComponent($oParserState->consume(1));
+                               continue;
+                       } else if ($oParserState->comes(')')) {
+                               $iNestingLevel--;
+                               $oCalcList->addListComponent($oParserState->consume(1));
+                               continue;
+                       }
+                       if ($iLastComponentType != CalcFunction::T_OPERAND) {
+                               $oVal = Value::parsePrimitiveValue($oParserState);
+                               $oCalcList->addListComponent($oVal);
+                               $iLastComponentType = CalcFunction::T_OPERAND;
+                       } else {
+                               if (in_array($oParserState->peek(), $aOperators)) {
+                                       if (($oParserState->comes('-') || $oParserState->comes('+'))) {
+                                               if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) {
+                                                       throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
+                                               }
+                                       }
+                                       $oCalcList->addListComponent($oParserState->consume(1));
+                                       $iLastComponentType = CalcFunction::T_OPERATOR;
+                               } else {
+                                       throw new UnexpectedTokenException(
+                                               sprintf(
+                                                       'Next token was expected to be an operand of type %s. Instead "%s" was found.',
+                                                       implode(', ', $aOperators),
+                                                       $oVal
+                                               ),
+                                               '',
+                                               'custom',
+                                               $oParserState->currentLine()
+                                       );
+                               }
+                       }
+               }
+               $oList->addListComponent($oCalcList);
+               $oParserState->consume(')');
+               return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
+       }
+
+}
diff --git a/lib/php-css-parser/Value/CalcRuleValueList.php b/lib/php-css-parser/Value/CalcRuleValueList.php
new file mode 100644 (file)
index 0000000..bde8a9d
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace Sabberworm\CSS\Value;
+
+class CalcRuleValueList extends RuleValueList {
+       public function __construct($iLineNo = 0) {
+               parent::__construct(array(), ',', $iLineNo);
+       }
+
+       public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
+               return $oOutputFormat->implode(' ', $this->aComponents);
+       }
+
+}
index e05b924..c6ed9b1 100644 (file)
@@ -2,12 +2,66 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+
 class Color extends CSSFunction {
 
        public function __construct($aColor, $iLineNo = 0) {
                parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
        }
 
+       public static function parse(ParserState $oParserState) {
+               $aColor = array();
+               if ($oParserState->comes('#')) {
+                       $oParserState->consume('#');
+                       $sValue = $oParserState->parseIdentifier(false);
+                       if ($oParserState->strlen($sValue) === 3) {
+                               $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
+                       } else if ($oParserState->strlen($sValue) === 4) {
+                               $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3];
+                       }
+
+                       if ($oParserState->strlen($sValue) === 8) {
+                               $aColor = array(
+                                       'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
+                                       'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
+                                       'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
+                                       'a' => new Size(round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine())
+                               );
+                       } else {
+                               $aColor = array(
+                                       'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
+                                       'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
+                                       'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine())
+                               );
+                       }
+               } else {
+                       $sColorMode = $oParserState->parseIdentifier(true);
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume('(');
+                       $iLength = $oParserState->strlen($sColorMode);
+                       for ($i = 0; $i < $iLength; ++$i) {
+                               $oParserState->consumeWhiteSpace();
+                               $aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
+                               $oParserState->consumeWhiteSpace();
+                               if ($i < ($iLength - 1)) {
+                                       $oParserState->consume(',');
+                               }
+                       }
+                       $oParserState->consume(')');
+               }
+               return new Color($aColor, $oParserState->currentLine());
+       }
+
+       private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) {
+               $fFromRange = $fFromMax - $fFromMin;
+               $fToRange = $fToMax - $fToMin;
+               $fMultiplier = $fToRange / $fFromRange;
+               $fNewVal = $fVal - $fFromMin;
+               $fNewVal *= $fMultiplier;
+               return $fNewVal + $fToMin;
+       }
+
        public function getColor() {
                return $this->aComponents;
        }
diff --git a/lib/php-css-parser/Value/LineName.php b/lib/php-css-parser/Value/LineName.php
new file mode 100644 (file)
index 0000000..eb7392d
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace Sabberworm\CSS\Value;
+
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+
+class LineName extends ValueList {
+       public function __construct($aComponents = array(), $iLineNo = 0) {
+               parent::__construct($aComponents, ' ', $iLineNo);
+       }
+
+       public static function parse(ParserState $oParserState) {
+               $oParserState->consume('[');
+               $oParserState->consumeWhiteSpace();
+               $aNames = array();
+               do {
+                       if($oParserState->getSettings()->bLenientParsing) {
+                               try {
+                                       $aNames[] = $oParserState->parseIdentifier();
+                               } catch(UnexpectedTokenException $e) {}
+                       } else {
+                               $aNames[] = $oParserState->parseIdentifier();
+                       }
+                       $oParserState->consumeWhiteSpace();
+               } while (!$oParserState->comes(']'));
+               $oParserState->consume(']');
+               return new LineName($aNames, $oParserState->currentLine());
+       }
+
+
+
+       public function __toString() {
+               return $this->render(new \Sabberworm\CSS\OutputFormat());
+       }
+
+       public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
+               return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']';
+       }
+
+}
index 9ad5eb0..f65246b 100644 (file)
@@ -2,12 +2,16 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+
 class Size extends PrimitiveValue {
 
        const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport)
        const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr';
        const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz';
 
+       private static $SIZE_UNITS = null;
+
        private $fSize;
        private $sUnit;
        private $bIsColorComponent;
@@ -19,6 +23,51 @@ class Size extends PrimitiveValue {
                $this->bIsColorComponent = $bIsColorComponent;
        }
 
+       public static function parse(ParserState $oParserState, $bIsColorComponent = false) {
+               $sSize = '';
+               if ($oParserState->comes('-')) {
+                       $sSize .= $oParserState->consume('-');
+               }
+               while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
+                       if ($oParserState->comes('.')) {
+                               $sSize .= $oParserState->consume('.');
+                       } else {
+                               $sSize .= $oParserState->consume(1);
+                       }
+               }
+
+               $sUnit = null;
+               $aSizeUnits = self::getSizeUnits();
+               foreach($aSizeUnits as $iLength => &$aValues) {
+                       $sKey = strtolower($oParserState->peek($iLength));
+                       if(array_key_exists($sKey, $aValues)) {
+                               if (($sUnit = $aValues[$sKey]) !== null) {
+                                       $oParserState->consume($iLength);
+                                       break;
+                               }
+                       }
+               }
+               return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine());
+       }
+
+       private static function getSizeUnits() {
+               if(self::$SIZE_UNITS === null) {
+                       self::$SIZE_UNITS = array();
+                       foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
+                               $iSize = strlen($val);
+                               if(!isset(self::$SIZE_UNITS[$iSize])) {
+                                       self::$SIZE_UNITS[$iSize] = array();
+                               }
+                               self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
+                       }
+
+                       // FIXME: Should we not order the longest units first?
+                       ksort(self::$SIZE_UNITS, SORT_NUMERIC);
+               }
+
+               return self::$SIZE_UNITS;
+       }
+
        public function setUnit($sUnit) {
                $this->sUnit = $sUnit;
        }
index 02cf581..b4f37e1 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
 
 class URL extends PrimitiveValue {
 
@@ -12,6 +13,23 @@ class URL extends PrimitiveValue {
                $this->oURL = $oURL;
        }
 
+       public static function parse(ParserState $oParserState) {
+               $bUseUrl = $oParserState->comes('url', true);
+               if ($bUseUrl) {
+                       $oParserState->consume('url');
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume('(');
+               }
+               $oParserState->consumeWhiteSpace();
+               $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
+               if ($bUseUrl) {
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume(')');
+               }
+               return $oResult;
+       }
+
+
        public function setURL(CSSString $oURL) {
                $this->oURL = $oURL;
        }
index 5d30bd9..fccc26b 100644 (file)
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
 use Sabberworm\CSS\Renderable;
 
 abstract class Value implements Renderable {
-    protected $iLineNo;
-
-    public function __construct($iLineNo = 0) {
-        $this->iLineNo = $iLineNo;
-    }
-    
-    /**
-     * @return int
-     */
-    public function getLineNo() {
-        return $this->iLineNo;
-    }
-
-    //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
+       protected $iLineNo;
+
+       public function __construct($iLineNo = 0) {
+               $this->iLineNo = $iLineNo;
+       }
+
+       public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) {
+               $aStack = array();
+               $oParserState->consumeWhiteSpace();
+               //Build a list of delimiters and parsed values
+               while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) {
+                       if (count($aStack) > 0) {
+                               $bFoundDelimiter = false;
+                               foreach ($aListDelimiters as $sDelimiter) {
+                                       if ($oParserState->comes($sDelimiter)) {
+                                               array_push($aStack, $oParserState->consume($sDelimiter));
+                                               $oParserState->consumeWhiteSpace();
+                                               $bFoundDelimiter = true;
+                                               break;
+                                       }
+                               }
+                               if (!$bFoundDelimiter) {
+                                       //Whitespace was the list delimiter
+                                       array_push($aStack, ' ');
+                               }
+                       }
+                       array_push($aStack, self::parsePrimitiveValue($oParserState));
+                       $oParserState->consumeWhiteSpace();
+               }
+               //Convert the list to list objects
+               foreach ($aListDelimiters as $sDelimiter) {
+                       if (count($aStack) === 1) {
+                               return $aStack[0];
+                       }
+                       $iStartPosition = null;
+                       while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
+                               $iLength = 2; //Number of elements to be joined
+                               for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
+                                       if ($sDelimiter !== $aStack[$i]) {
+                                               break;
+                                       }
+                               }
+                               $oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
+                               for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
+                                       $oList->addListComponent($aStack[$i]);
+                               }
+                               array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
+                       }
+               }
+               if (!isset($aStack[0])) {
+                       throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
+               }
+               return $aStack[0];
+       }
+
+       public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) {
+               $sResult = $oParserState->parseIdentifier($bIgnoreCase);
+
+               if ($oParserState->comes('(')) {
+                       $oParserState->consume('(');
+                       $aArguments = Value::parseValue($oParserState, array('=', ' ', ','));
+                       $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
+                       $oParserState->consume(')');
+               }
+
+               return $sResult;
+       }
+
+       public static function parsePrimitiveValue(ParserState $oParserState) {
+               $oValue = null;
+               $oParserState->consumeWhiteSpace();
+               if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) {
+                       $oValue = Size::parse($oParserState);
+               } else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
+                       $oValue = Color::parse($oParserState);
+               } else if ($oParserState->comes('url', true)) {
+                       $oValue = URL::parse($oParserState);
+               } else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) {
+                       $oValue = CalcFunction::parse($oParserState);
+               } else if ($oParserState->comes("'") || $oParserState->comes('"')) {
+                       $oValue = CSSString::parse($oParserState);
+               } else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
+                       $oValue = self::parseMicrosoftFilter($oParserState);
+               } else if ($oParserState->comes("[")) {
+                       $oValue = LineName::parse($oParserState);
+               } else if ($oParserState->comes("U+")) {
+                       $oValue = self::parseUnicodeRangeValue($oParserState);
+               } else {
+                       $oValue = self::parseIdentifierOrFunction($oParserState);
+               }
+               $oParserState->consumeWhiteSpace();
+               return $oValue;
+       }
+
+       private static function parseMicrosoftFilter(ParserState $oParserState) {
+               $sFunction = $oParserState->consumeUntil('(', false, true);
+               $aArguments = Value::parseValue($oParserState, array(',', '='));
+               return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
+       }
+
+       private static function parseUnicodeRangeValue(ParserState $oParserState) {
+               $iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits
+               $sRange = "";
+               $oParserState->consume("U+");
+               do {
+                       if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them
+                       $sRange .= $oParserState->consume(1);
+               } while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
+               return "U+{$sRange}";
+       }
+       
+       /**
+        * @return int
+        */
+       public function getLineNo() {
+               return $this->iLineNo;
+       }
+
+       //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
        //public abstract function __toString();
        //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
 }
index ed721af..f59e9c7 100644 (file)
     <location>php-css-parser</location>
     <name>PHP-CSS-Parser</name>
     <license>MIT</license>
-    <version>8.1.0</version>
+    <version>8.3.0</version>
   </library>
   <library>
     <location>rtlcss</location>