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