Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
UnorderedGroup
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
4 / 4
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
 allOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 someOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 generateMatches
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
9
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 ArrayIterator;
10use EmptyIterator;
11use Iterator;
12use Wikimedia\CSS\Objects\ComponentValueList;
13use Wikimedia\CSS\Util;
14
15/**
16 * Matcher that groups other matchers without ordering ("&&" and "||" combiners)
17 * @see https://www.w3.org/TR/2019/CR-css-values-3-20190606/#component-combinators
18 */
19class UnorderedGroup extends Matcher {
20    /** @var Matcher[] */
21    protected $matchers;
22
23    /** @var bool Whether all matchers must be used */
24    protected $all;
25
26    /**
27     * @param Matcher[] $matchers
28     * @param bool $all Whether all matchers must be used
29     */
30    public function __construct( array $matchers, $all ) {
31        Util::assertAllInstanceOf( $matchers, Matcher::class, '$matchers' );
32        $this->matchers = $matchers;
33        $this->all = (bool)$all;
34    }
35
36    /**
37     * Implements "&&": All of the options, in any order
38     * @param Matcher[] $matchers
39     * @return static
40     */
41    public static function allOf( array $matchers ) {
42        return new static( $matchers, true );
43    }
44
45    /**
46     * Implements "||": One or more of the options, in any order
47     * @param Matcher[] $matchers
48     * @return static
49     */
50    public static function someOf( array $matchers ) {
51        return new static( $matchers, false );
52    }
53
54    /** @inheritDoc */
55    protected function generateMatches( ComponentValueList $values, $start, array $options ) {
56        $used = [];
57
58        // As each Matcher is used, push it onto the stack along with the set
59        // of remaining matchers.
60        $stack = [
61            [
62                new GrammarMatch( $values, $start, 0 ),
63                $this->matchers,
64                new ArrayIterator( $this->matchers ),
65                null,
66                new EmptyIterator
67            ]
68        ];
69        do {
70            /** @var $lastMatch GrammarMatch */
71            /** @var $matchers Matcher[] */
72            /** @var $matcherIter Iterator<Matcher> */
73            /** @var $curMatcher Matcher|null */
74            /** @var $iter Iterator<GrammarMatch> */
75            [ $lastMatch, $matchers, $matcherIter, $curMatcher, $iter ] = $stack[count( $stack ) - 1];
76            // Ignore EmptyIterator here
77            '@phan-var Iterator $iter';
78
79            // If the top of the stack has more matches, process the next one.
80            if ( $iter->valid() ) {
81                $match = $iter->current();
82                $iter->next();
83
84                // If we have unused matchers to try after this one, do so.
85                // Otherwise, yield and continue with the current one.
86                if ( $matchers ) {
87                    $stack[] = [ $match, $matchers, new ArrayIterator( $matchers ), null, new EmptyIterator ];
88                } else {
89                    $newMatch = $this->makeMatch( $values, $start, $match->getNext(), $match, $stack );
90                    $mid = $newMatch->getUniqueID();
91                    if ( !isset( $used[$mid] ) ) {
92                        $used[$mid] = 1;
93                        yield $newMatch;
94                    }
95                }
96                continue;
97            }
98
99            // We ran out of matches for the current top of the stack. Pop it,
100            // and put $curMatcher back into $matchers, so it can be tried again
101            // at a later position.
102            array_pop( $stack );
103            if ( $curMatcher ) {
104                $matchers[$matcherIter->key()] = $curMatcher;
105                $matcherIter->next();
106            }
107
108            $fromPos = $lastMatch->getNext();
109
110            // If there are more matchers to try, pull the next one out of
111            // $matchers and try it at the current position. Otherwise, maybe
112            // yield the current position and backtrack.
113            if ( $matcherIter->valid() ) {
114                $curMatcher = $matcherIter->current();
115                unset( $matchers[$matcherIter->key()] );
116                $iter = $curMatcher->generateMatches( $values, $fromPos, $options );
117                $stack[] = [ $lastMatch, $matchers, $matcherIter, $curMatcher, $iter ];
118            } elseif ( $stack && !$this->all ) {
119                $newMatch = $this->makeMatch( $values, $start, $fromPos, $lastMatch, $stack );
120                $mid = $newMatch->getUniqueID();
121                if ( !isset( $used[$mid] ) ) {
122                    $used[$mid] = 1;
123                    yield $newMatch;
124                }
125            }
126        } while ( $stack );
127    }
128}