Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
37 / 37
Wikimedia\CSS\Grammar\Juxtaposition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
2 / 2
13
100.00% covered (success)
100.00%
37 / 37
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 generateMatches
100.00% covered (success)
100.00%
1 / 1
12
100.00% covered (success)
100.00%
33 / 33
<?php
/**
 * @file
 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
 */
namespace Wikimedia\CSS\Grammar;
use Wikimedia\CSS\Objects\ComponentValueList;
use Wikimedia\CSS\Objects\Token;
use Wikimedia\CSS\Util;
/**
 * Matcher that groups other matchers (juxtaposition)
 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#component-combinators
 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#comb-comma
 */
class Juxtaposition extends Matcher {
    /** @var Matcher[] */
    protected $matchers;
    /** @var bool Whether non-empty matches are comma-separated */
    protected $commas;
    /**
     * @param Matcher[] $matchers
     * @param bool $commas Whether matches are comma-separated
     */
    public function __construct( array $matchers, $commas = false ) {
        Util::assertAllInstanceOf( $matchers, Matcher::class, '$matchers' );
        $this->matchers = $matchers;
        $this->commas = (bool)$commas;
    }
    /** @inheritDoc */
    protected function generateMatches( ComponentValueList $values, $start, array $options ) {
        $used = [];
        // Match each of our matchers in turn, pushing each one onto a stack as
        // we process it and popping a match once its exhausted.
        $stack = [
            [
                new Match( $values, $start, 0 ),
                $start,
                $this->matchers[0]->generateMatches( $values, $start, $options ),
                false
            ]
        ];
        do {
            /** @var $lastMatch Match */
            /** @var $lastEnd int */
            /** @var $iter \Iterator<Match> */
            /** @var $needEmpty bool */
            list( $lastMatch, $lastEnd, $iter, $needEmpty ) = $stack[count( $stack ) - 1];
            // If the top of the stack has no more matches, pop it and loop.
            if ( !$iter->valid() ) {
                array_pop( $stack );
                continue;
            }
            // Find the next match for the current top of the stack.
            $match = $iter->current();
            $iter->next();
            // In some cases, we can only match if the rest of the pattern
            // is empty. If we're in that situation, ignore all non-empty
            // matches.
            if ( $needEmpty && $match->getLength() !== 0 ) {
                continue;
            }
            $thisEnd = $nextFrom = $match->getNext();
            // Dealing with commas is a bit tricky. There are three cases:
            // 1. If the current match is empty, don't look for a following
            // comma now and reset $thisEnd to $lastEnd.
            // 2. If there is a comma following, update $nextFrom to be after
            // the comma.
            // 3. If there's no comma following, every subsequent Matcher must
            // be empty in order for the group as a whole to match, so set
            // the flag.
            // Unlike '#', this doesn't specify skipping whitespace around the
            // commas if the production isn't already skipping whitespace.
            if ( $this->commas ) {
                if ( $match->getLength() === 0 ) {
                    $thisEnd = $lastEnd;
                } else {
                    if ( isset( $values[$nextFrom] ) && $values[$nextFrom] instanceof Token &&
                        $values[$nextFrom]->type() === Token::T_COMMA
                    ) {
                        $nextFrom = $this->next( $values, $nextFrom, $options );
                    } else {
                        $needEmpty = true;
                    }
                }
            }
            // If we ran out of Matchers, yield the final position. Otherwise
            // push the next matcher onto the stack.
            if ( count( $stack ) >= count( $this->matchers ) ) {
                $newMatch = $this->makeMatch( $values, $start, $thisEnd, $match, $stack );
                $mid = $newMatch->getUniqueID();
                if ( !isset( $used[$mid] ) ) {
                    $used[$mid] = 1;
                    yield $newMatch;
                }
            } else {
                $stack[] = [
                    $match,
                    $thisEnd,
                    $this->matchers[count( $stack )]->generateMatches( $values, $nextFrom, $options ),
                    $needEmpty
                ];
            }
        } while ( $stack );
    }
}