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
77            // If the top of the stack has more matches, process the next one.
78            if ( $iter->valid() ) {
79                $match = $iter->current();
80                $iter->next();
81
82                // If we have unused matchers to try after this one, do so.
83                // Otherwise, yield and continue with the current one.
84                if ( $matchers ) {
85                    $stack[] = [ $match, $matchers, new ArrayIterator( $matchers ), null, new EmptyIterator ];
86                } else {
87                    $newMatch = $this->makeMatch( $values, $start, $match->getNext(), $match, $stack );
88                    $mid = $newMatch->getUniqueID();
89                    if ( !isset( $used[$mid] ) ) {
90                        $used[$mid] = 1;
91                        yield $newMatch;
92                    }
93                }
94                continue;
95            }
96
97            // We ran out of matches for the current top of the stack. Pop it,
98            // and put $curMatcher back into $matchers, so it can be tried again
99            // at a later position.
100            array_pop( $stack );
101            if ( $curMatcher ) {
102                $matchers[$matcherIter->key()] = $curMatcher;
103                $matcherIter->next();
104            }
105
106            $fromPos = $lastMatch->getNext();
107
108            // If there are more matchers to try, pull the next one out of
109            // $matchers and try it at the current position. Otherwise, maybe
110            // yield the current position and backtrack.
111            if ( $matcherIter->valid() ) {
112                $curMatcher = $matcherIter->current();
113                unset( $matchers[$matcherIter->key()] );
114                $iter = $curMatcher->generateMatches( $values, $fromPos, $options );
115                $stack[] = [ $lastMatch, $matchers, $matcherIter, $curMatcher, $iter ];
116            } elseif ( $stack && !$this->all ) {
117                $newMatch = $this->makeMatch( $values, $start, $fromPos, $lastMatch, $stack );
118                $mid = $newMatch->getUniqueID();
119                if ( !isset( $used[$mid] ) ) {
120                    $used[$mid] = 1;
121                    yield $newMatch;
122                }
123            }
124        } while ( $stack );
125    }
126}