4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
7 namespace Wikimedia\CSS\Grammar;
9 use Wikimedia\CSS\Objects\ComponentValueList;
10 use Wikimedia\CSS\Objects\Token;
13 * Matcher that matches a sub-Matcher a certain number of times
14 * ("?", "*", "+", "#", "{A,B}" multipliers)
15 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#component-multipliers
17 class Quantifier extends Matcher {
24 /** @var bool Whether matches are comma-separated */
28 * @param Matcher $matcher
29 * @param int|float $min Minimum number of matches
30 * @param int|float $max Maximum number of matches
31 * @param bool $commas Whether matches are comma-separated
33 public function __construct( Matcher $matcher, $min, $max, $commas ) {
34 $this->matcher = $matcher;
37 $this->commas = (bool)$commas;
41 * Implements "?": 0 or 1 matches
42 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-opt
43 * @param Matcher $matcher
46 public static function optional( Matcher $matcher ) {
47 return new static( $matcher, 0, 1, false );
51 * Implements "*": 0 or more matches
52 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-zero-plus
53 * @param Matcher $matcher
56 public static function star( Matcher $matcher ) {
57 return new static( $matcher, 0, INF, false );
61 * Implements "+": 1 or more matches
62 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-one-plus
63 * @param Matcher $matcher
66 public static function plus( Matcher $matcher ) {
67 return new static( $matcher, 1, INF, false );
71 * Implements "{A,B}": Between A and B matches
72 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-num-range
73 * @param Matcher $matcher
74 * @param int|float $min Minimum number of matches
75 * @param int|float $max Maximum number of matches
78 public static function count( Matcher $matcher, $min, $max ) {
79 return new static( $matcher, $min, $max, false );
83 * Implements "#" and "#{A,B}": Between A and B matches, comma-separated
84 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-comma
85 * @param Matcher $matcher
86 * @param int|float $min Minimum number of matches
87 * @param int|float $max Maximum number of matches
90 public static function hash( Matcher $matcher, $min = 1, $max = INF ) {
91 return new static( $matcher, $min, $max, true );
94 protected function generateMatches( ComponentValueList $values, $start, array $options ) {
97 // Maintain a stack of matches for backtracking purposes.
99 [ new Match( $values, $start, 0 ), $this->matcher->generateMatches( $values, $start, $options ) ]
102 /** @var $lastMatch Match */
103 /** @var $iter \Iterator<Match> */
104 list( $lastMatch, $iter ) = $stack[count( $stack ) - 1];
106 // If the top of the stack has no more matches, pop it, maybe
107 // yield the last matched position, and loop.
108 if ( !$iter->valid() ) {
110 $ct = count( $stack );
111 $pos = $lastMatch->getNext();
112 if ( $ct >= $this->min && $ct <= $this->max ) {
113 $newMatch = $this->makeMatch( $values, $start, $pos, $lastMatch, $stack );
114 $mid = $newMatch->getUniqueID();
115 if ( !isset( $used[$mid] ) ) {
123 // Find the next match for the current top of the stack.
124 $match = $iter->current();
127 // Quantifiers don't work well when the quantified thing can be empty.
128 if ( $match->getLength() === 0 ) {
129 throw new \UnexpectedValueException( 'Empty match in quantifier!' );
132 $nextFrom = $match->getNext();
134 // There can only be more matches after this one if we haven't
135 // reached our maximum yet.
136 $canBeMore = count( $stack ) < $this->max;
138 // Commas are slightly tricky:
139 // 1. If there is a following comma, start the next Matcher after it.
140 // 2. If not, there can't be any more Matchers following.
141 // And in either case optional whitespace is always allowed.
142 if ( $this->commas ) {
144 if ( isset( $values[$n] ) && $values[$n] instanceof Token &&
145 $values[$n]->type() === Token::T_WHITESPACE
147 $n = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options );
149 if ( isset( $values[$n] ) && $values[$n] instanceof Token &&
150 $values[$n]->type() === Token::T_COMMA
152 $nextFrom = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options );
158 // If there can be more matches, push another one onto the stack
159 // and try it. Otherwise yield and continue with the current match.
161 $stack[] = [ $match, $this->matcher->generateMatches( $values, $nextFrom, $options ) ];
163 $ct = count( $stack );
164 $pos = $match->getNext();
165 if ( $ct >= $this->min && $ct <= $this->max ) {
166 $newMatch = $this->makeMatch( $values, $start, $pos, $match, $stack );
167 $mid = $newMatch->getUniqueID();
168 if ( !isset( $used[$mid] ) ) {