matcher = $matcher; $this->min = $min; $this->max = $max; $this->commas = (bool)$commas; } /** * Implements "?": 0 or 1 matches * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-opt * @param Matcher $matcher * @return static */ public static function optional( Matcher $matcher ) { return new static( $matcher, 0, 1, false ); } /** * Implements "*": 0 or more matches * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-zero-plus * @param Matcher $matcher * @return static */ public static function star( Matcher $matcher ) { return new static( $matcher, 0, INF, false ); } /** * Implements "+": 1 or more matches * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-one-plus * @param Matcher $matcher * @return static */ public static function plus( Matcher $matcher ) { return new static( $matcher, 1, INF, false ); } /** * Implements "{A,B}": Between A and B matches * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-num-range * @param Matcher $matcher * @param int|float $min Minimum number of matches * @param int|float $max Maximum number of matches * @return static */ public static function count( Matcher $matcher, $min, $max ) { return new static( $matcher, $min, $max, false ); } /** * Implements "#" and "#{A,B}": Between A and B matches, comma-separated * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-comma * @param Matcher $matcher * @param int|float $min Minimum number of matches * @param int|float $max Maximum number of matches * @return static */ public static function hash( Matcher $matcher, $min = 1, $max = INF ) { return new static( $matcher, $min, $max, true ); } protected function generateMatches( ComponentValueList $values, $start, array $options ) { $used = []; // Maintain a stack of matches for backtracking purposes. $stack = [ [ new Match( $values, $start, 0 ), $this->matcher->generateMatches( $values, $start, $options ) ] ]; do { /** @var $lastMatch Match */ /** @var $iter \Iterator */ list( $lastMatch, $iter ) = $stack[count( $stack ) - 1]; // If the top of the stack has no more matches, pop it, maybe // yield the last matched position, and loop. if ( !$iter->valid() ) { array_pop( $stack ); $ct = count( $stack ); $pos = $lastMatch->getNext(); if ( $ct >= $this->min && $ct <= $this->max ) { $newMatch = $this->makeMatch( $values, $start, $pos, $lastMatch, $stack ); $mid = $newMatch->getUniqueID(); if ( !isset( $used[$mid] ) ) { $used[$mid] = 1; yield $newMatch; } } continue; } // Find the next match for the current top of the stack. $match = $iter->current(); $iter->next(); // Quantifiers don't work well when the quantified thing can be empty. if ( $match->getLength() === 0 ) { throw new \UnexpectedValueException( 'Empty match in quantifier!' ); } $nextFrom = $match->getNext(); // There can only be more matches after this one if we haven't // reached our maximum yet. $canBeMore = count( $stack ) < $this->max; // Commas are slightly tricky: // 1. If there is a following comma, start the next Matcher after it. // 2. If not, there can't be any more Matchers following. // And in either case optional whitespace is always allowed. if ( $this->commas ) { $n = $nextFrom; if ( isset( $values[$n] ) && $values[$n] instanceof Token && $values[$n]->type() === Token::T_WHITESPACE ) { $n = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options ); } if ( isset( $values[$n] ) && $values[$n] instanceof Token && $values[$n]->type() === Token::T_COMMA ) { $nextFrom = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options ); } else { $canBeMore = false; } } // If there can be more matches, push another one onto the stack // and try it. Otherwise yield and continue with the current match. if ( $canBeMore ) { $stack[] = [ $match, $this->matcher->generateMatches( $values, $nextFrom, $options ) ]; } else { $ct = count( $stack ); $pos = $match->getNext(); if ( $ct >= $this->min && $ct <= $this->max ) { $newMatch = $this->makeMatch( $values, $start, $pos, $match, $stack ); $mid = $newMatch->getUniqueID(); if ( !isset( $used[$mid] ) ) { $used[$mid] = 1; yield $newMatch; } } } } while ( $stack ); } }