Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
229 / 229 |
|
100.00% |
26 / 26 |
CRAP | |
100.00% |
1 / 1 |
Parser | |
100.00% |
229 / 229 |
|
100.00% |
26 / 26 |
108 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromDataSource | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
newFromTokens | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
consumeToken | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
consumeTokenAndWhitespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getParseErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clearParseErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseError | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parseStylesheet | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
parseRuleList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parseRule | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
parseDeclaration | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
parseDeclarationList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parseDeclarationOrAtRuleList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parseComponentValue | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
parseComponentValueList | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
parseCommaSeparatedComponentValueList | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
consumeRuleList | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
10 | |||
consumeDeclarationOrAtRuleList | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
13 | |||
consumeDeclaration | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
16 | |||
consumeAtRule | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
consumeQualifiedRule | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
consumeComponentValue | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
8 | |||
consumeSimpleBlock | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
consumeFunction | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * @file |
4 | * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0 |
5 | */ |
6 | |
7 | namespace Wikimedia\CSS\Parser; |
8 | |
9 | use Wikimedia\CSS\Objects\AtRule; |
10 | use Wikimedia\CSS\Objects\ComponentValue; |
11 | use Wikimedia\CSS\Objects\ComponentValueList; |
12 | use Wikimedia\CSS\Objects\CSSFunction; |
13 | use Wikimedia\CSS\Objects\Declaration; |
14 | use Wikimedia\CSS\Objects\DeclarationList; |
15 | use Wikimedia\CSS\Objects\DeclarationOrAtRuleList; |
16 | use Wikimedia\CSS\Objects\QualifiedRule; |
17 | use Wikimedia\CSS\Objects\Rule; |
18 | use Wikimedia\CSS\Objects\RuleList; |
19 | use Wikimedia\CSS\Objects\SimpleBlock; |
20 | use Wikimedia\CSS\Objects\Stylesheet; |
21 | use Wikimedia\CSS\Objects\Token; |
22 | |
23 | // Note: While reading the code below, you might find that my calls to |
24 | // consumeToken() don't match what the spec says, and I don't ever "reconsume" a |
25 | // token. It turns out that the spec is overcomplicated and confused with |
26 | // respect to the "current input token" and the "next input token". It turns |
27 | // out things are pretty simple: every "consume an X" is called with the |
28 | // current input token being the first token of X, and returns with the current |
29 | // input token being the last token of X (or EOF if X ends at EOF). |
30 | |
31 | // Also, of note is that, since our Tokenizer can only return a stream of tokens |
32 | // rather than a stream of component values, the consume functions here only |
33 | // consider tokens. ComponentValueList::toTokenArray() may be used to convert a |
34 | // list of component values to a list of tokens if necessary. |
35 | |
36 | /** |
37 | * Parse CSS into a structure for further processing. |
38 | * |
39 | * This implements the CSS Syntax Module Level 3 candidate recommendation. |
40 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/ |
41 | * |
42 | * The usual entry points are: |
43 | * - Parser::parseStylesheet() to parse a stylesheet or the contents of a <style> tag. |
44 | * - Parser::parseDeclarationList() to parse an inline style attribute |
45 | */ |
46 | class Parser { |
47 | /** |
48 | * Maximum depth of nested ComponentValues |
49 | * |
50 | * Arbitrary number that seems like it should be enough |
51 | */ |
52 | private const CV_DEPTH_LIMIT = 100; |
53 | |
54 | /** @var Tokenizer */ |
55 | protected $tokenizer; |
56 | |
57 | /** @var Token|null The most recently consumed token */ |
58 | protected $currentToken = null; |
59 | |
60 | /** @var array Parse errors. Each error is [ string $tag, int $line, int $pos ] */ |
61 | protected $parseErrors = []; |
62 | |
63 | /** @var int Recursion depth, incremented in self::consumeComponentValue() */ |
64 | protected $cvDepth = 0; |
65 | |
66 | /** |
67 | * @param Tokenizer $tokenizer CSS Tokenizer |
68 | */ |
69 | public function __construct( Tokenizer $tokenizer ) { |
70 | $this->tokenizer = $tokenizer; |
71 | } |
72 | |
73 | /** |
74 | * Create a Parser for a CSS string |
75 | * @param string $source CSS to parse. |
76 | * @param array $options Configuration options, see DataSourceTokenizer::__construct(). Also, |
77 | * - convert: (array) If specified, detect the encoding as defined in the |
78 | * CSS spec. The value is passed as the $encodings argument to |
79 | * Encoder::convert(). |
80 | * @return static |
81 | */ |
82 | public static function newFromString( $source, array $options = [] ) { |
83 | if ( isset( $options['convert'] ) ) { |
84 | $source = Encoder::convert( $source, $options['convert'] ); |
85 | } |
86 | return static::newFromDataSource( new StringDataSource( $source ), $options ); |
87 | } |
88 | |
89 | /** |
90 | * Create a Parser for a CSS DataSource |
91 | * @param DataSource $source CSS to parse. |
92 | * @param array $options Configuration options, see DataSourceTokenizer::__construct(). |
93 | * @return static |
94 | */ |
95 | public static function newFromDataSource( DataSource $source, array $options = [] ) { |
96 | $tokenizer = new DataSourceTokenizer( $source, $options ); |
97 | return new static( $tokenizer ); |
98 | } |
99 | |
100 | /** |
101 | * Create a Parser for a list of Tokens |
102 | * @param Token[] $tokens Token-stream to parse |
103 | * @param Token|null $eof EOF-token |
104 | * @return static |
105 | */ |
106 | public static function newFromTokens( array $tokens, ?Token $eof = null ) { |
107 | $tokenizer = new TokenListTokenizer( $tokens, $eof ); |
108 | return new static( $tokenizer ); |
109 | } |
110 | |
111 | /** |
112 | * Consume a token |
113 | */ |
114 | protected function consumeToken() { |
115 | if ( !$this->currentToken || $this->currentToken->type() !== Token::T_EOF ) { |
116 | $this->currentToken = $this->tokenizer->consumeToken(); |
117 | |
118 | // Copy any parse errors encountered |
119 | foreach ( $this->tokenizer->getParseErrors() as $error ) { |
120 | $this->parseErrors[] = $error; |
121 | } |
122 | $this->tokenizer->clearParseErrors(); |
123 | } |
124 | } |
125 | |
126 | /** |
127 | * Consume a token, also consuming any following whitespace (and comments) |
128 | */ |
129 | protected function consumeTokenAndWhitespace() { |
130 | do { |
131 | $this->consumeToken(); |
132 | } while ( $this->currentToken->type() === Token::T_WHITESPACE ); |
133 | } |
134 | |
135 | /** |
136 | * Return all parse errors seen so far |
137 | * @return array Array of [ string $tag, int $line, int $pos, ... ] |
138 | */ |
139 | public function getParseErrors() { |
140 | return $this->parseErrors; |
141 | } |
142 | |
143 | /** |
144 | * Clear parse errors |
145 | */ |
146 | public function clearParseErrors() { |
147 | $this->parseErrors = []; |
148 | } |
149 | |
150 | /** |
151 | * Record a parse error |
152 | * @param string $tag Error tag |
153 | * @param Token $token Report the error as starting at this token. |
154 | * @param array $data Extra data about the error. |
155 | */ |
156 | protected function parseError( $tag, Token $token, array $data = [] ) { |
157 | [ $line, $pos ] = $token->getPosition(); |
158 | $this->parseErrors[] = array_merge( [ $tag, $line, $pos ], $data ); |
159 | } |
160 | |
161 | /** |
162 | * Parse a stylesheet |
163 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-stylesheet |
164 | * @return Stylesheet |
165 | */ |
166 | public function parseStylesheet() { |
167 | // Move to the first token |
168 | $this->consumeToken(); |
169 | $list = $this->consumeRuleList( true ); |
170 | |
171 | return new Stylesheet( $list ); |
172 | } |
173 | |
174 | /** |
175 | * Parse a list of rules |
176 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-list-of-rules |
177 | * @return RuleList |
178 | */ |
179 | public function parseRuleList() { |
180 | // Move to the first token |
181 | $this->consumeToken(); |
182 | return $this->consumeRuleList( false ); |
183 | } |
184 | |
185 | /** |
186 | * Parse a rule |
187 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-rule |
188 | * @return Rule|null |
189 | */ |
190 | public function parseRule() { |
191 | // 1. |
192 | $this->consumeTokenAndWhitespace(); |
193 | |
194 | // 2. |
195 | if ( $this->currentToken->type() === Token::T_EOF ) { |
196 | // "return a syntax error"? |
197 | $this->parseError( 'unexpected-eof', $this->currentToken ); |
198 | return null; |
199 | } |
200 | |
201 | if ( $this->currentToken->type() === Token::T_AT_KEYWORD ) { |
202 | $rule = $this->consumeAtRule(); |
203 | } else { |
204 | $rule = $this->consumeQualifiedRule(); |
205 | if ( !$rule ) { |
206 | return null; |
207 | } |
208 | } |
209 | |
210 | // 3. |
211 | $this->consumeTokenAndWhitespace(); |
212 | |
213 | // 4. |
214 | if ( $this->currentToken->type() === Token::T_EOF ) { |
215 | return $rule; |
216 | } |
217 | |
218 | // "return a syntax error"? |
219 | $this->parseError( 'expected-eof', $this->currentToken ); |
220 | |
221 | return null; |
222 | } |
223 | |
224 | /** |
225 | * Parse a declaration |
226 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-declaration |
227 | * @return Declaration|null |
228 | */ |
229 | public function parseDeclaration() { |
230 | // 1. |
231 | $this->consumeTokenAndWhitespace(); |
232 | |
233 | // 2. |
234 | if ( $this->currentToken->type() !== Token::T_IDENT ) { |
235 | // "return a syntax error"? |
236 | $this->parseError( 'expected-ident', $this->currentToken ); |
237 | return null; |
238 | } |
239 | |
240 | // 3. |
241 | // Declarations always run to EOF, no need to check. |
242 | return $this->consumeDeclaration(); |
243 | } |
244 | |
245 | /** |
246 | * Parse a list of declarations |
247 | * @note This is not the entry point the standard calls "parse a list of declarations", |
248 | * see self::parseDeclarationOrAtRuleList() |
249 | * @return DeclarationList |
250 | */ |
251 | public function parseDeclarationList() { |
252 | // Move to the first token |
253 | $this->consumeToken(); |
254 | return $this->consumeDeclarationOrAtRuleList( false ); |
255 | } |
256 | |
257 | /** |
258 | * Parse a list of declarations and at-rules |
259 | * @note This is the entry point the standard calls "parse a list of declarations" |
260 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-list-of-declarations |
261 | * @return DeclarationOrAtRuleList |
262 | */ |
263 | public function parseDeclarationOrAtRuleList() { |
264 | // Move to the first token |
265 | $this->consumeToken(); |
266 | return $this->consumeDeclarationOrAtRuleList(); |
267 | } |
268 | |
269 | /** |
270 | * Parse a (non-whitespace) component value |
271 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-component-value |
272 | * @return ComponentValue|null |
273 | */ |
274 | public function parseComponentValue() { |
275 | // 1. |
276 | $this->consumeTokenAndWhitespace(); |
277 | |
278 | // 2. |
279 | if ( $this->currentToken->type() === Token::T_EOF ) { |
280 | // "return a syntax error"? |
281 | $this->parseError( 'unexpected-eof', $this->currentToken ); |
282 | return null; |
283 | } |
284 | |
285 | // 3. |
286 | $value = $this->consumeComponentValue(); |
287 | |
288 | // 4. |
289 | $this->consumeTokenAndWhitespace(); |
290 | |
291 | // 5. |
292 | if ( $this->currentToken->type() === Token::T_EOF ) { |
293 | return $value; |
294 | } |
295 | |
296 | // "return a syntax error"? |
297 | $this->parseError( 'expected-eof', $this->currentToken ); |
298 | |
299 | return null; |
300 | } |
301 | |
302 | /** |
303 | * Parse a list of component values |
304 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-list-of-component-values |
305 | * @return ComponentValueList |
306 | */ |
307 | public function parseComponentValueList() { |
308 | $list = new ComponentValueList(); |
309 | while ( true ) { |
310 | // Move to the first/next token |
311 | $this->consumeToken(); |
312 | $value = $this->consumeComponentValue(); |
313 | if ( $value instanceof Token && $value->type() === Token::T_EOF ) { |
314 | break; |
315 | } |
316 | $list->add( $value ); |
317 | } |
318 | |
319 | return $list; |
320 | } |
321 | |
322 | /** |
323 | * Parse a comma-separated list of component values |
324 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-comma-separated-list-of-component-values |
325 | * @return ComponentValueList[] |
326 | */ |
327 | public function parseCommaSeparatedComponentValueList() { |
328 | $lists = []; |
329 | do { |
330 | $list = new ComponentValueList(); |
331 | while ( true ) { |
332 | // Move to the first/next token |
333 | $this->consumeToken(); |
334 | $value = $this->consumeComponentValue(); |
335 | if ( $value instanceof Token && |
336 | ( $value->type() === Token::T_EOF || $value->type() === Token::T_COMMA ) |
337 | ) { |
338 | break; |
339 | } |
340 | $list->add( $value ); |
341 | } |
342 | $lists[] = $list; |
343 | } while ( $value->type() === Token::T_COMMA ); |
344 | |
345 | return $lists; |
346 | } |
347 | |
348 | /** |
349 | * Consume a list of rules |
350 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-list-of-rules |
351 | * @param bool $topLevel Determines the behavior when CDO and CDC tokens are encountered |
352 | * @return RuleList |
353 | */ |
354 | protected function consumeRuleList( $topLevel ) { |
355 | // @phan-suppress-previous-line PhanPluginNeverReturnMethod |
356 | $list = new RuleList(); |
357 | // @phan-suppress-next-line PhanInfiniteLoop |
358 | while ( true ) { |
359 | $rule = false; |
360 | switch ( $this->currentToken->type() ) { |
361 | case Token::T_WHITESPACE: |
362 | break; |
363 | |
364 | case Token::T_EOF: |
365 | break 2; |
366 | |
367 | case Token::T_CDO: |
368 | case Token::T_CDC: |
369 | if ( !$topLevel ) { |
370 | $rule = $this->consumeQualifiedRule(); |
371 | } |
372 | // Else, do nothing |
373 | break; |
374 | |
375 | case Token::T_AT_KEYWORD: |
376 | $rule = $this->consumeAtRule(); |
377 | break; |
378 | |
379 | default: |
380 | $rule = $this->consumeQualifiedRule(); |
381 | break; |
382 | } |
383 | |
384 | if ( $rule ) { |
385 | $list->add( $rule ); |
386 | } |
387 | $this->consumeToken(); |
388 | } |
389 | |
390 | // @phan-suppress-next-line PhanPluginUnreachableCode Reached by break 2 |
391 | return $list; |
392 | } |
393 | |
394 | /** |
395 | * Consume a list of declarations and at-rules |
396 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-list-of-declarations |
397 | * @param bool $allowAtRules Whether to allow at-rules. This flag is not in |
398 | * the spec and is used to implement the non-spec self::parseDeclarationList(). |
399 | * @return DeclarationOrAtRuleList|DeclarationList |
400 | */ |
401 | protected function consumeDeclarationOrAtRuleList( $allowAtRules = true ) { |
402 | // @phan-suppress-previous-line PhanPluginNeverReturnMethod |
403 | $list = $allowAtRules ? new DeclarationOrAtRuleList() : new DeclarationList(); |
404 | // @phan-suppress-next-line PhanInfiniteLoop |
405 | while ( true ) { |
406 | $declaration = false; |
407 | switch ( $this->currentToken->type() ) { |
408 | case Token::T_WHITESPACE: |
409 | break; |
410 | |
411 | case Token::T_SEMICOLON: |
412 | $declaration = null; |
413 | break; |
414 | |
415 | case Token::T_EOF: |
416 | break 2; |
417 | |
418 | case Token::T_AT_KEYWORD: |
419 | if ( $allowAtRules ) { |
420 | $declaration = $this->consumeAtRule(); |
421 | } else { |
422 | $this->parseError( 'unexpected-token-in-declaration-list', $this->currentToken ); |
423 | $this->consumeAtRule(); |
424 | $declaration = null; |
425 | } |
426 | break; |
427 | |
428 | case Token::T_IDENT: |
429 | $cvs = []; |
430 | do { |
431 | $cvs[] = $this->consumeComponentValue(); |
432 | $this->consumeToken(); |
433 | } while ( |
434 | $this->currentToken->type() !== Token::T_SEMICOLON && |
435 | $this->currentToken->type() !== Token::T_EOF |
436 | ); |
437 | $tokens = ( new ComponentValueList( $cvs ) )->toTokenArray(); |
438 | $parser = static::newFromTokens( $tokens, $this->currentToken ); |
439 | // Load that first token |
440 | $parser->consumeToken(); |
441 | $declaration = $parser->consumeDeclaration(); |
442 | // Propagate any errors |
443 | $this->parseErrors = array_merge( $this->parseErrors, $parser->parseErrors ); |
444 | break; |
445 | |
446 | default: |
447 | $this->parseError( 'unexpected-token-in-declaration-list', $this->currentToken ); |
448 | do { |
449 | $this->consumeComponentValue(); |
450 | $this->consumeToken(); |
451 | } while ( |
452 | $this->currentToken->type() !== Token::T_SEMICOLON && |
453 | $this->currentToken->type() !== Token::T_EOF |
454 | ); |
455 | $declaration = null; |
456 | break; |
457 | } |
458 | |
459 | if ( $declaration ) { |
460 | $list->add( $declaration ); |
461 | } |
462 | $this->consumeToken(); |
463 | } |
464 | |
465 | // @phan-suppress-next-line PhanPluginUnreachableCode Reached by break 2 |
466 | return $list; |
467 | } |
468 | |
469 | /** |
470 | * Consume a declaration |
471 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-declaration |
472 | * @return Declaration|null |
473 | */ |
474 | protected function consumeDeclaration() { |
475 | $declaration = new Declaration( $this->currentToken ); |
476 | |
477 | // 1. |
478 | $this->consumeTokenAndWhitespace(); |
479 | |
480 | // 2. and 3. |
481 | if ( $this->currentToken->type() !== Token::T_COLON ) { |
482 | $this->parseError( 'expected-colon', $this->currentToken ); |
483 | return null; |
484 | } |
485 | $this->consumeTokenAndWhitespace(); |
486 | |
487 | // 4. |
488 | $value = $declaration->getValue(); |
489 | $l1 = $l2 = -1; |
490 | while ( $this->currentToken->type() !== Token::T_EOF ) { |
491 | $value->add( $this->consumeComponentValue() ); |
492 | if ( $this->currentToken->type() !== Token::T_WHITESPACE ) { |
493 | $l1 = $l2; |
494 | $l2 = $value->count() - 1; |
495 | } |
496 | $this->consumeToken(); |
497 | } |
498 | |
499 | // 5. and part of 6. |
500 | // @phan-suppress-next-line PhanSuspiciousValueComparison False positive about $l1 is -1 |
501 | $v1 = $l1 >= 0 ? $value[$l1] : null; |
502 | $v2 = $l2 >= 0 ? $value[$l2] : null; |
503 | if ( $v1 instanceof Token && |
504 | $v1->type() === Token::T_DELIM && |
505 | $v1->value() === '!' && |
506 | $v2 instanceof Token && |
507 | $v2->type() === Token::T_IDENT && |
508 | !strcasecmp( $v2->value(), 'important' ) |
509 | ) { |
510 | // This removes the "!" and "important" (5), and also any whitespace between/after (6) |
511 | while ( isset( $value[$l1] ) ) { |
512 | $value->remove( $l1 ); |
513 | } |
514 | $declaration->setImportant( true ); |
515 | } |
516 | |
517 | // Rest of 6. |
518 | $i = $value->count(); |
519 | // @phan-suppress-next-line PhanNonClassMethodCall False positive |
520 | while ( --$i >= 0 && $value[$i] instanceof Token && $value[$i]->type() === Token::T_WHITESPACE ) { |
521 | $value->remove( $i ); |
522 | } |
523 | |
524 | // 7. |
525 | return $declaration; |
526 | } |
527 | |
528 | /** |
529 | * Consume an at-rule |
530 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-at-rule |
531 | * @return AtRule |
532 | * @suppress PhanPluginNeverReturnMethod due to break 2; |
533 | */ |
534 | protected function consumeAtRule() { |
535 | $rule = new AtRule( $this->currentToken ); |
536 | $this->consumeToken(); |
537 | // @phan-suppress-next-line PhanInfiniteLoop |
538 | while ( true ) { |
539 | switch ( $this->currentToken->type() ) { |
540 | case Token::T_SEMICOLON: |
541 | break 2; |
542 | |
543 | case Token::T_EOF: |
544 | if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) { |
545 | $this->parseError( 'unexpected-eof-in-rule', $this->currentToken ); |
546 | } |
547 | break 2; |
548 | |
549 | case Token::T_LEFT_BRACE: |
550 | $rule->setBlock( $this->consumeSimpleBlock() ); |
551 | break 2; |
552 | |
553 | // Spec has "simple block with an associated token of <{-token>" here, but that isn't possible |
554 | // because it's not a Token. |
555 | |
556 | default: |
557 | $rule->getPrelude()->add( $this->consumeComponentValue() ); |
558 | break; |
559 | } |
560 | $this->consumeToken(); |
561 | } |
562 | |
563 | // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2; |
564 | return $rule; |
565 | } |
566 | |
567 | /** |
568 | * Consume a qualified rule |
569 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-qualified-rule |
570 | * @return QualifiedRule|null |
571 | */ |
572 | protected function consumeQualifiedRule() { |
573 | $rule = new QualifiedRule( $this->currentToken ); |
574 | while ( true ) { |
575 | switch ( $this->currentToken->type() ) { |
576 | case Token::T_EOF: |
577 | if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) { |
578 | $this->parseError( 'unexpected-eof-in-rule', $this->currentToken ); |
579 | } |
580 | return null; |
581 | |
582 | case Token::T_LEFT_BRACE: |
583 | $rule->setBlock( $this->consumeSimpleBlock() ); |
584 | break 2; |
585 | |
586 | // Spec has "simple block with an associated token of <{-token>" here, but that isn't possible |
587 | // because it's not a Token. |
588 | |
589 | default: |
590 | $rule->getPrelude()->add( $this->consumeComponentValue() ); |
591 | break; |
592 | } |
593 | $this->consumeToken(); |
594 | } |
595 | |
596 | // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2; |
597 | return $rule; |
598 | } |
599 | |
600 | /** |
601 | * Consume a component value |
602 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-component-value |
603 | * @return ComponentValue |
604 | */ |
605 | protected function consumeComponentValue() { |
606 | if ( ++$this->cvDepth > static::CV_DEPTH_LIMIT ) { |
607 | $this->parseError( 'recursion-depth-exceeded', $this->currentToken ); |
608 | // There's no way to safely recover from this without more recursion. |
609 | // So just eat the rest of the input, then return a |
610 | // specially-flagged EOF, so we can avoid 100 "unexpected EOF" |
611 | // errors. |
612 | $position = $this->currentToken->getPosition(); |
613 | while ( $this->currentToken->type() !== Token::T_EOF ) { |
614 | $this->consumeToken(); |
615 | } |
616 | $this->currentToken = new Token( Token::T_EOF, [ |
617 | 'position' => $position, |
618 | 'typeFlag' => 'recursion-depth-exceeded' |
619 | ] ); |
620 | } |
621 | |
622 | switch ( $this->currentToken->type() ) { |
623 | case Token::T_LEFT_BRACE: |
624 | case Token::T_LEFT_BRACKET: |
625 | case Token::T_LEFT_PAREN: |
626 | $ret = $this->consumeSimpleBlock(); |
627 | break; |
628 | |
629 | case Token::T_FUNCTION: |
630 | $ret = $this->consumeFunction(); |
631 | break; |
632 | |
633 | default: |
634 | $ret = $this->currentToken; |
635 | break; |
636 | } |
637 | |
638 | $this->cvDepth--; |
639 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable $ret always set |
640 | return $ret; |
641 | } |
642 | |
643 | /** |
644 | * Consume a simple block |
645 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-simple-block |
646 | * @return SimpleBlock |
647 | * @suppress PhanPluginNeverReturnMethod due to break 2; |
648 | */ |
649 | protected function consumeSimpleBlock() { |
650 | $block = new SimpleBlock( $this->currentToken ); |
651 | $endTokenType = $block->getEndTokenType(); |
652 | $this->consumeToken(); |
653 | // @phan-suppress-next-line PhanInfiniteLoop |
654 | while ( true ) { |
655 | switch ( $this->currentToken->type() ) { |
656 | case Token::T_EOF: |
657 | if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) { |
658 | $this->parseError( 'unexpected-eof-in-block', $this->currentToken ); |
659 | } |
660 | break 2; |
661 | |
662 | case $endTokenType: |
663 | break 2; |
664 | |
665 | default: |
666 | $block->getValue()->add( $this->consumeComponentValue() ); |
667 | break; |
668 | } |
669 | $this->consumeToken(); |
670 | } |
671 | |
672 | // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2; |
673 | return $block; |
674 | } |
675 | |
676 | /** |
677 | * Consume a function |
678 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-function |
679 | * @return CSSFunction |
680 | * @suppress PhanPluginNeverReturnMethod due to break 2; |
681 | */ |
682 | protected function consumeFunction() { |
683 | $function = new CSSFunction( $this->currentToken ); |
684 | $this->consumeToken(); |
685 | |
686 | // @phan-suppress-next-line PhanInfiniteLoop |
687 | while ( true ) { |
688 | switch ( $this->currentToken->type() ) { |
689 | case Token::T_EOF: |
690 | if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) { |
691 | $this->parseError( 'unexpected-eof-in-function', $this->currentToken ); |
692 | } |
693 | break 2; |
694 | |
695 | case Token::T_RIGHT_PAREN: |
696 | break 2; |
697 | |
698 | default: |
699 | $function->getValue()->add( $this->consumeComponentValue() ); |
700 | break; |
701 | } |
702 | $this->consumeToken(); |
703 | } |
704 | |
705 | // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2; |
706 | return $function; |
707 | } |
708 | |
709 | // @codeCoverageIgnoreEnd |
710 | } |