4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
7 namespace Wikimedia\CSS\Grammar;
9 use Wikimedia\CSS\Objects\ComponentValueList;
10 use Wikimedia\CSS\Objects\Token;
11 use Wikimedia\CSS\Util;
14 * Matcher that groups other matchers (juxtaposition)
15 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#component-combinators
16 * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#comb-comma
18 class Juxtaposition extends Matcher {
22 /** @var bool Whether non-empty matches are comma-separated */
26 * @param Matcher[] $matchers
27 * @param bool $commas Whether matches are comma-separated
29 public function __construct( array $matchers, $commas = false ) {
30 Util::assertAllInstanceOf( $matchers, Matcher::class, '$matchers' );
31 $this->matchers = $matchers;
32 $this->commas = (bool)$commas;
35 protected function generateMatches( ComponentValueList $values, $start, array $options ) {
38 // Match each of our matchers in turn, pushing each one onto a stack as
39 // we process it and popping a match once its exhausted.
42 new Match( $values, $start, 0 ),
44 $this->matchers[0]->generateMatches( $values, $start, $options ),
49 /** @var $lastMatch Match */
50 /** @var $lastEnd int */
51 /** @var $iter \Iterator<Match> */
52 /** @var $needEmpty bool */
53 list( $lastMatch, $lastEnd, $iter, $needEmpty ) = $stack[count( $stack ) - 1];
55 // If the top of the stack has no more matches, pop it and loop.
56 if ( !$iter->valid() ) {
61 // Find the next match for the current top of the stack.
62 $match = $iter->current();
65 // In some cases, we can only match if the rest of the pattern
66 // is empty. If we're in that situation, ignore all non-empty
68 if ( $needEmpty && $match->getLength() !== 0 ) {
72 $thisEnd = $nextFrom = $match->getNext();
74 // Dealing with commas is a bit tricky. There are three cases:
75 // 1. If the current match is empty, don't look for a following
76 // comma now and reset $thisEnd to $lastEnd.
77 // 2. If there is a comma following, update $nextFrom to be after
79 // 3. If there's no comma following, every subsequent Matcher must
80 // be empty in order for the group as a whole to match, so set
82 // Unlike '#', this doesn't specify skipping whitespace around the
83 // commas if the production isn't already skipping whitespace.
84 if ( $this->commas ) {
85 if ( $match->getLength() === 0 ) {
88 if ( isset( $values[$nextFrom] ) && $values[$nextFrom] instanceof Token &&
89 $values[$nextFrom]->type() === Token::T_COMMA
91 $nextFrom = $this->next( $values, $nextFrom, $options );
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] ) ) {
111 $this->matchers[count( $stack )]->generateMatches( $values, $nextFrom, $options ),