` instead of ``) * - quantifier: (string) Set to '*' or '+' to work like `*` or * `+` but without backtracking. Note this will probably fail to * match correctly if anything else is supposed to come after the * AnythingMatcher, i.e. only use this where there's nothing else to the * end of the input. * @note To properly match the draft's `` or * ``, specify '+' for the 'quantifier' option. */ public function __construct( array $options = [] ) { $this->toplevel = !empty( $options['toplevel'] ); $this->quantifier = isset( $options['quantifier'] ) ? $options['quantifier'] : ''; if ( !in_array( $this->quantifier, [ '', '+', '*' ], true ) ) { throw new \InvalidArgumentException( 'Invalid quantifier' ); } $recurse = !$this->toplevel && $this->quantifier === '*' ? $this : new static( [ 'quantifier' => '*' ] ); $this->matchers[Token::T_FUNCTION] = new FunctionMatcher( null, $recurse ); foreach ( [ Token::T_LEFT_PAREN, Token::T_LEFT_BRACE, Token::T_LEFT_BRACKET ] as $delim ) { $this->matchers[$delim] = new BlockMatcher( $delim, $recurse ); } } protected function generateMatches( ComponentValueList $values, $start, array $options ) { $origStart = $start; $lastMatch = $this->quantifier === '*' ? $this->makeMatch( $values, $start, $start ) : null; do { $newMatch = null; $cv = isset( $values[$start] ) ? $values[$start] : null; if ( $cv instanceof Token ) { switch ( $cv->type() ) { case Token::T_BAD_STRING: case Token::T_BAD_URL: case Token::T_RIGHT_PAREN: case Token::T_RIGHT_BRACE: case Token::T_RIGHT_BRACKET: case Token::T_EOF: // Not allowed break; case Token::T_SEMICOLON: if ( !$this->toplevel ) { $newMatch = $this->makeMatch( $values, $origStart, $this->next( $values, $start, $options ), $lastMatch ); } break; case Token::T_DELIM: if ( !$this->toplevel || $cv->value() !== '!' ) { $newMatch = $this->makeMatch( $values, $origStart, $this->next( $values, $start, $options ), $lastMatch ); } break; case Token::T_WHITESPACE: // If we encounter whitespace, assume it's significant. $newMatch = $this->makeMatch( $values, $origStart, $this->next( $values, $start, $options ), new Match( $values, $start, 1, 'significantWhitespace' ), [ [ $lastMatch ] ] ); break; case Token::T_FUNCTION: case Token::T_LEFT_PAREN: case Token::T_LEFT_BRACE: case Token::T_LEFT_BRACKET: // Should never happen // @codeCoverageIgnoreStart throw new \UnexpectedValueException( "How did a \"{$cv->type()}\" token get here?" ); // @codeCoverageIgnoreEnd default: $newMatch = $this->makeMatch( $values, $origStart, $this->next( $values, $start, $options ), $lastMatch ); break; } } elseif ( $cv instanceof CSSFunction || $cv instanceof SimpleBlock ) { $tok = $cv instanceof SimpleBlock ? $cv->getStartTokenType() : Token::T_FUNCTION; // We know there's only one way for the submatcher to match, so just grab the first one $match = $this->matchers[$tok] ->generateMatches( new ComponentValueList( [ $cv ] ), 0, $options ) ->current(); if ( $match ) { $newMatch = $this->makeMatch( $values, $origStart, $this->next( $values, $start, $options ), $match, [ [ $lastMatch ] ] ); } } if ( $newMatch ) { $lastMatch = $newMatch; $start = $newMatch->getNext(); } } while ( $this->quantifier !== '' && $newMatch ); if ( $lastMatch ) { yield $lastMatch; } } }