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/2021/CRD-css-syntax-3-20211224/#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 | } |