Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
54 / 54 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
Quantifier | |
100.00% |
54 / 54 |
|
100.00% |
7 / 7 |
23 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
optional | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
star | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
plus | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
count | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hash | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
generateMatches | |
100.00% |
45 / 45 |
|
100.00% |
1 / 1 |
17 |
1 | <?php |
2 | /** |
3 | * @file |
4 | * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0 |
5 | */ |
6 | |
7 | namespace Wikimedia\CSS\Grammar; |
8 | |
9 | use Iterator; |
10 | use UnexpectedValueException; |
11 | use Wikimedia\CSS\Objects\ComponentValueList; |
12 | use Wikimedia\CSS\Objects\Token; |
13 | |
14 | /** |
15 | * Matcher that matches a sub-Matcher a certain number of times |
16 | * ("?", "*", "+", "#", "{A,B}" multipliers) |
17 | * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#component-multipliers |
18 | */ |
19 | class Quantifier extends Matcher { |
20 | /** @var Matcher */ |
21 | protected $matcher; |
22 | |
23 | /** @var int */ |
24 | protected $min; |
25 | |
26 | /** @var int */ |
27 | protected $max; |
28 | |
29 | /** @var bool Whether matches are comma-separated */ |
30 | protected $commas; |
31 | |
32 | /** |
33 | * @param Matcher $matcher |
34 | * @param int|float $min Minimum number of matches |
35 | * @param int|float $max Maximum number of matches |
36 | * @param bool $commas Whether matches are comma-separated |
37 | */ |
38 | public function __construct( Matcher $matcher, $min, $max, $commas ) { |
39 | $this->matcher = $matcher; |
40 | $this->min = $min; |
41 | $this->max = $max; |
42 | $this->commas = (bool)$commas; |
43 | } |
44 | |
45 | /** |
46 | * Implements "?": 0 or 1 matches |
47 | * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#mult-opt |
48 | * @param Matcher $matcher |
49 | * @return static |
50 | */ |
51 | public static function optional( Matcher $matcher ) { |
52 | return new static( $matcher, 0, 1, false ); |
53 | } |
54 | |
55 | /** |
56 | * Implements "*": 0 or more matches |
57 | * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#mult-zero-plus |
58 | * @param Matcher $matcher |
59 | * @return static |
60 | */ |
61 | public static function star( Matcher $matcher ) { |
62 | return new static( $matcher, 0, INF, false ); |
63 | } |
64 | |
65 | /** |
66 | * Implements "+": 1 or more matches |
67 | * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#mult-one-plus |
68 | * @param Matcher $matcher |
69 | * @return static |
70 | */ |
71 | public static function plus( Matcher $matcher ) { |
72 | return new static( $matcher, 1, INF, false ); |
73 | } |
74 | |
75 | /** |
76 | * Implements "{A,B}": Between A and B matches |
77 | * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#mult-num-range |
78 | * @param Matcher $matcher |
79 | * @param int|float $min Minimum number of matches |
80 | * @param int|float $max Maximum number of matches |
81 | * @return static |
82 | */ |
83 | public static function count( Matcher $matcher, $min, $max ) { |
84 | return new static( $matcher, $min, $max, false ); |
85 | } |
86 | |
87 | /** |
88 | * Implements "#" and "#{A,B}": Between A and B matches, comma-separated |
89 | * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#mult-comma |
90 | * @param Matcher $matcher |
91 | * @param int|float $min Minimum number of matches |
92 | * @param int|float $max Maximum number of matches |
93 | * @return static |
94 | */ |
95 | public static function hash( Matcher $matcher, $min = 1, $max = INF ) { |
96 | return new static( $matcher, $min, $max, true ); |
97 | } |
98 | |
99 | /** @inheritDoc */ |
100 | protected function generateMatches( ComponentValueList $values, $start, array $options ) { |
101 | $used = []; |
102 | |
103 | // Maintain a stack of matches for backtracking purposes. |
104 | $stack = [ |
105 | [ |
106 | new GrammarMatch( $values, $start, 0 ), |
107 | $this->matcher->generateMatches( $values, $start, $options ) |
108 | ] |
109 | ]; |
110 | do { |
111 | /** @var $lastMatch GrammarMatch */ |
112 | /** @var $iter Iterator<GrammarMatch> */ |
113 | [ $lastMatch, $iter ] = $stack[count( $stack ) - 1]; |
114 | |
115 | // If the top of the stack has no more matches, pop it, maybe |
116 | // yield the last matched position, and loop. |
117 | if ( !$iter->valid() ) { |
118 | array_pop( $stack ); |
119 | $ct = count( $stack ); |
120 | $pos = $lastMatch->getNext(); |
121 | if ( $ct >= $this->min && $ct <= $this->max ) { |
122 | $newMatch = $this->makeMatch( $values, $start, $pos, $lastMatch, $stack ); |
123 | $mid = $newMatch->getUniqueID(); |
124 | if ( !isset( $used[$mid] ) ) { |
125 | $used[$mid] = 1; |
126 | yield $newMatch; |
127 | } |
128 | } |
129 | continue; |
130 | } |
131 | |
132 | // Find the next match for the current top of the stack. |
133 | $match = $iter->current(); |
134 | $iter->next(); |
135 | |
136 | // Quantifiers don't work well when the quantified thing can be empty. |
137 | if ( $match->getLength() === 0 ) { |
138 | throw new UnexpectedValueException( 'Empty match in quantifier!' ); |
139 | } |
140 | |
141 | $nextFrom = $match->getNext(); |
142 | |
143 | // There can only be more matches after this one if we haven't |
144 | // reached our maximum yet. |
145 | $canBeMore = count( $stack ) < $this->max; |
146 | |
147 | // Commas are slightly tricky: |
148 | // 1. If there is a following comma, start the next Matcher after it. |
149 | // 2. If not, there can't be any more Matchers following. |
150 | // And in either case optional whitespace is always allowed. |
151 | if ( $this->commas ) { |
152 | $n = $nextFrom; |
153 | if ( isset( $values[$n] ) && $values[$n] instanceof Token && |
154 | // @phan-suppress-next-line PhanNonClassMethodCall False positive |
155 | $values[$n]->type() === Token::T_WHITESPACE |
156 | ) { |
157 | $n = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options ); |
158 | } |
159 | if ( isset( $values[$n] ) && $values[$n] instanceof Token && |
160 | // @phan-suppress-next-line PhanNonClassMethodCall False positive |
161 | $values[$n]->type() === Token::T_COMMA |
162 | ) { |
163 | $nextFrom = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options ); |
164 | } else { |
165 | $canBeMore = false; |
166 | } |
167 | } |
168 | |
169 | // If there can be more matches, push another one onto the stack |
170 | // and try it. Otherwise, yield and continue with the current match. |
171 | if ( $canBeMore ) { |
172 | $stack[] = [ $match, $this->matcher->generateMatches( $values, $nextFrom, $options ) ]; |
173 | } else { |
174 | $ct = count( $stack ); |
175 | $pos = $match->getNext(); |
176 | if ( $ct >= $this->min && $ct <= $this->max ) { |
177 | $newMatch = $this->makeMatch( $values, $start, $pos, $match, $stack ); |
178 | $mid = $newMatch->getUniqueID(); |
179 | if ( !isset( $used[$mid] ) ) { |
180 | $used[$mid] = 1; |
181 | yield $newMatch; |
182 | } |
183 | } |
184 | } |
185 | } while ( $stack ); |
186 | } |
187 | } |