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