Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
Juxtaposition
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
2 / 2
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 generateMatches
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2/**
3 * @file
4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
5 */
6
7namespace Wikimedia\CSS\Grammar;
8
9use Iterator;
10use Wikimedia\CSS\Objects\ComponentValueList;
11use Wikimedia\CSS\Objects\Token;
12use Wikimedia\CSS\Util;
13
14/**
15 * Matcher that groups other matchers (juxtaposition)
16 * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#component-combinators
17 * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#comb-comma
18 */
19class Juxtaposition extends Matcher {
20    /** @var Matcher[] */
21    protected $matchers;
22
23    /** @var bool Whether non-empty matches are comma-separated */
24    protected $commas;
25
26    /**
27     * @param Matcher[] $matchers
28     * @param bool $commas Whether matches are comma-separated
29     */
30    public function __construct( array $matchers, $commas = false ) {
31        Util::assertAllInstanceOf( $matchers, Matcher::class, '$matchers' );
32        $this->matchers = $matchers;
33        $this->commas = (bool)$commas;
34    }
35
36    /** @inheritDoc */
37    protected function generateMatches( ComponentValueList $values, $start, array $options ) {
38        $used = [];
39
40        // Match each of our matchers in turn, pushing each one onto a stack as
41        // we process it and popping a match once it's exhausted.
42        $stack = [
43            [
44                new GrammarMatch( $values, $start, 0 ),
45                $start,
46                $this->matchers[0]->generateMatches( $values, $start, $options ),
47                false
48            ]
49        ];
50        do {
51            /** @var $lastEnd int */
52            /** @var $iter Iterator<GrammarMatch> */
53            /** @var $needEmpty bool */
54            [ , $lastEnd, $iter, $needEmpty ] = $stack[count( $stack ) - 1];
55
56            // If the top of the stack has no more matches, pop it and loop.
57            if ( !$iter->valid() ) {
58                array_pop( $stack );
59                continue;
60            }
61
62            // Find the next match for the current top of the stack.
63            $match = $iter->current();
64            $iter->next();
65
66            // In some cases, we can only match if the rest of the pattern
67            // is empty. If we're in that situation, ignore all non-empty
68            // matches.
69            if ( $needEmpty && $match->getLength() !== 0 ) {
70                continue;
71            }
72
73            $thisEnd = $nextFrom = $match->getNext();
74
75            // Dealing with commas is a bit tricky. There are three cases:
76            // 1. If the current match is empty, don't look for a following
77            // comma now and reset $thisEnd to $lastEnd.
78            // 2. If there is a comma following, update $nextFrom to be after
79            // the comma.
80            // 3. If there's no comma following, every subsequent Matcher must
81            // be empty in order for the group as a whole to match, so set
82            // the flag.
83            // Unlike '#', this doesn't specify skipping whitespace around the
84            // commas if the production isn't already skipping whitespace.
85            if ( $this->commas ) {
86                if ( $match->getLength() === 0 ) {
87                    $thisEnd = $lastEnd;
88                } elseif ( isset( $values[$nextFrom] ) && $values[$nextFrom] instanceof Token &&
89                    // @phan-suppress-next-line PhanNonClassMethodCall False positive
90                    $values[$nextFrom]->type() === Token::T_COMMA
91                ) {
92                    $nextFrom = $this->next( $values, $nextFrom, $options );
93                } else {
94                    $needEmpty = true;
95                }
96            }
97
98            // If we ran out of Matchers, yield the final position. Otherwise,
99            // push the next matcher onto the stack.
100            if ( count( $stack ) >= count( $this->matchers ) ) {
101                $newMatch = $this->makeMatch( $values, $start, $thisEnd, $match, $stack );
102                $mid = $newMatch->getUniqueID();
103                if ( !isset( $used[$mid] ) ) {
104                    $used[$mid] = 1;
105                    yield $newMatch;
106                }
107            } else {
108                $stack[] = [
109                    $match,
110                    $thisEnd,
111                    $this->matchers[count( $stack )]->generateMatches( $values, $nextFrom, $options ),
112                    $needEmpty
113                ];
114            }
115        } while ( $stack );
116    }
117}