Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
51 / 51 |
|
100.00% |
2 / 2 |
CRAP | |
100.00% |
1 / 1 |
AnythingMatcher | |
100.00% |
51 / 51 |
|
100.00% |
2 / 2 |
32 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
generateMatches | |
100.00% |
42 / 42 |
|
100.00% |
1 / 1 |
27 |
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 InvalidArgumentException; |
10 | use UnexpectedValueException; |
11 | use Wikimedia\CSS\Objects\ComponentValueList; |
12 | use Wikimedia\CSS\Objects\CSSFunction; |
13 | use Wikimedia\CSS\Objects\SimpleBlock; |
14 | use Wikimedia\CSS\Objects\Token; |
15 | |
16 | /** |
17 | * Matcher that matches anything except bad strings, bad urls, and unmatched |
18 | * left-paren, left-brace, or left-bracket. |
19 | * @warning Be very careful using this! |
20 | * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#any-value |
21 | */ |
22 | class AnythingMatcher extends Matcher { |
23 | |
24 | /** @var bool */ |
25 | protected $toplevel; |
26 | |
27 | /** @var string '', '*', or '+' */ |
28 | protected $quantifier; |
29 | |
30 | /** @var Matcher[] */ |
31 | protected $matchers; |
32 | |
33 | /** |
34 | * @param array $options |
35 | * - toplevel: (bool) If true, disallows some extra tokens (i.e. it's the |
36 | * draft's `<declaration-value>` instead of `<any-value>`) |
37 | * - quantifier: (string) Set to '*' or '+' to work like `<value>*` or |
38 | * `<value>+` but without backtracking. Note this will probably fail to |
39 | * match correctly if anything else is supposed to come after the |
40 | * AnythingMatcher, i.e. only use this where there's nothing else to the |
41 | * end of the input. |
42 | * @note To properly match the draft's `<declaration-value>` or |
43 | * `<any-value>`, specify '+' for the 'quantifier' option. |
44 | */ |
45 | public function __construct( array $options = [] ) { |
46 | $this->toplevel = !empty( $options['toplevel'] ); |
47 | $this->quantifier = $options['quantifier'] ?? ''; |
48 | if ( !in_array( $this->quantifier, [ '', '+', '*' ], true ) ) { |
49 | throw new InvalidArgumentException( 'Invalid quantifier' ); |
50 | } |
51 | |
52 | $recurse = !$this->toplevel && $this->quantifier === '*' |
53 | ? $this : new static( [ 'quantifier' => '*' ] ); |
54 | $this->matchers[Token::T_FUNCTION] = new FunctionMatcher( null, $recurse ); |
55 | foreach ( [ Token::T_LEFT_PAREN, Token::T_LEFT_BRACE, Token::T_LEFT_BRACKET ] as $delim ) { |
56 | $this->matchers[$delim] = new BlockMatcher( $delim, $recurse ); |
57 | } |
58 | } |
59 | |
60 | /** @inheritDoc */ |
61 | protected function generateMatches( ComponentValueList $values, $start, array $options ) { |
62 | $origStart = $start; |
63 | $lastMatch = $this->quantifier === '*' ? $this->makeMatch( $values, $start, $start ) : null; |
64 | do { |
65 | $newMatch = null; |
66 | $cv = $values[$start] ?? null; |
67 | if ( $cv instanceof Token ) { |
68 | switch ( $cv->type() ) { |
69 | case Token::T_BAD_STRING: |
70 | case Token::T_BAD_URL: |
71 | case Token::T_RIGHT_PAREN: |
72 | case Token::T_RIGHT_BRACE: |
73 | case Token::T_RIGHT_BRACKET: |
74 | case Token::T_EOF: |
75 | // Not allowed |
76 | break; |
77 | |
78 | case Token::T_SEMICOLON: |
79 | if ( !$this->toplevel ) { |
80 | $newMatch = $this->makeMatch( |
81 | $values, $origStart, $this->next( $values, $start, $options ), $lastMatch |
82 | ); |
83 | } |
84 | break; |
85 | |
86 | case Token::T_DELIM: |
87 | if ( !$this->toplevel || $cv->value() !== '!' ) { |
88 | $newMatch = $this->makeMatch( |
89 | $values, $origStart, $this->next( $values, $start, $options ), $lastMatch |
90 | ); |
91 | } |
92 | break; |
93 | |
94 | case Token::T_WHITESPACE: |
95 | // If we encounter whitespace, assume it's significant. |
96 | $newMatch = $this->makeMatch( |
97 | $values, $origStart, $this->next( $values, $start, $options ), |
98 | new GrammarMatch( $values, $start, 1, 'significantWhitespace' ), |
99 | [ [ $lastMatch ] ] |
100 | ); |
101 | break; |
102 | |
103 | case Token::T_FUNCTION: |
104 | case Token::T_LEFT_PAREN: |
105 | case Token::T_LEFT_BRACE: |
106 | case Token::T_LEFT_BRACKET: |
107 | // Should never happen |
108 | // @codeCoverageIgnoreStart |
109 | throw new UnexpectedValueException( "How did a \"{$cv->type()}\" token get here?" ); |
110 | // @codeCoverageIgnoreEnd |
111 | |
112 | default: |
113 | $newMatch = $this->makeMatch( |
114 | $values, $origStart, $this->next( $values, $start, $options ), $lastMatch |
115 | ); |
116 | break; |
117 | } |
118 | } elseif ( $cv instanceof CSSFunction || $cv instanceof SimpleBlock ) { |
119 | $tok = $cv instanceof SimpleBlock ? $cv->getStartTokenType() : Token::T_FUNCTION; |
120 | // We know there's only one way for the submatcher to match, so just grab the first one |
121 | $match = $this->matchers[$tok] |
122 | ->generateMatches( new ComponentValueList( [ $cv ] ), 0, $options ) |
123 | ->current(); |
124 | if ( $match ) { |
125 | $newMatch = $this->makeMatch( |
126 | $values, $origStart, $this->next( $values, $start, $options ), $match, [ [ $lastMatch ] ] |
127 | ); |
128 | } |
129 | } |
130 | if ( $newMatch ) { |
131 | $lastMatch = $newMatch; |
132 | $start = $newMatch->getNext(); |
133 | } |
134 | } while ( $this->quantifier !== '' && $newMatch ); |
135 | |
136 | if ( $lastMatch ) { |
137 | yield $lastMatch; |
138 | } |
139 | } |
140 | } |