]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - vendor/wikimedia/css-sanitizer/src/Grammar/Quantifier.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / vendor / wikimedia / css-sanitizer / src / Grammar / Quantifier.php
1 <?php
2 /**
3  * @file
4  * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
5  */
6
7 namespace Wikimedia\CSS\Grammar;
8
9 use Wikimedia\CSS\Objects\ComponentValueList;
10 use Wikimedia\CSS\Objects\Token;
11
12 /**
13  * Matcher that matches a sub-Matcher a certain number of times
14  * ("?", "*", "+", "#", "{A,B}" multipliers)
15  * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#component-multipliers
16  */
17 class Quantifier extends Matcher {
18         /** @var Matcher */
19         protected $matcher;
20
21         /** @var int */
22         protected $min, $max;
23
24         /** @var bool Whether matches are comma-separated */
25         protected $commas;
26
27         /**
28          * @param Matcher $matcher
29          * @param int|float $min Minimum number of matches
30          * @param int|float $max Maximum number of matches
31          * @param bool $commas Whether matches are comma-separated
32          */
33         public function __construct( Matcher $matcher, $min, $max, $commas ) {
34                 $this->matcher = $matcher;
35                 $this->min = $min;
36                 $this->max = $max;
37                 $this->commas = (bool)$commas;
38         }
39
40         /**
41          * Implements "?": 0 or 1 matches
42          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-opt
43          * @param Matcher $matcher
44          * @return static
45          */
46         public static function optional( Matcher $matcher ) {
47                 return new static( $matcher, 0, 1, false );
48         }
49
50         /**
51          * Implements "*": 0 or more matches
52          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-zero-plus
53          * @param Matcher $matcher
54          * @return static
55          */
56         public static function star( Matcher $matcher ) {
57                 return new static( $matcher, 0, INF, false );
58         }
59
60         /**
61          * Implements "+": 1 or more matches
62          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-one-plus
63          * @param Matcher $matcher
64          * @return static
65          */
66         public static function plus( Matcher $matcher ) {
67                 return new static( $matcher, 1, INF, false );
68         }
69
70         /**
71          * Implements "{A,B}": Between A and B matches
72          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-num-range
73          * @param Matcher $matcher
74          * @param int|float $min Minimum number of matches
75          * @param int|float $max Maximum number of matches
76          * @return static
77          */
78         public static function count( Matcher $matcher, $min, $max ) {
79                 return new static( $matcher, $min, $max, false );
80         }
81
82         /**
83          * Implements "#" and "#{A,B}": Between A and B matches, comma-separated
84          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#mult-comma
85          * @param Matcher $matcher
86          * @param int|float $min Minimum number of matches
87          * @param int|float $max Maximum number of matches
88          * @return static
89          */
90         public static function hash( Matcher $matcher, $min = 1, $max = INF ) {
91                 return new static( $matcher, $min, $max, true );
92         }
93
94         protected function generateMatches( ComponentValueList $values, $start, array $options ) {
95                 $used = [];
96
97                 // Maintain a stack of matches for backtracking purposes.
98                 $stack = [
99                         [ new Match( $values, $start, 0 ), $this->matcher->generateMatches( $values, $start, $options ) ]
100                 ];
101                 do {
102                         /** @var $lastMatch Match */
103                         /** @var $iter \Iterator<Match> */
104                         list( $lastMatch, $iter ) = $stack[count( $stack ) - 1];
105
106                         // If the top of the stack has no more matches, pop it, maybe
107                         // yield the last matched position, and loop.
108                         if ( !$iter->valid() ) {
109                                 array_pop( $stack );
110                                 $ct = count( $stack );
111                                 $pos = $lastMatch->getNext();
112                                 if ( $ct >= $this->min && $ct <= $this->max ) {
113                                         $newMatch = $this->makeMatch( $values, $start, $pos, $lastMatch, $stack );
114                                         $mid = $newMatch->getUniqueID();
115                                         if ( !isset( $used[$mid] ) ) {
116                                                 $used[$mid] = 1;
117                                                 yield $newMatch;
118                                         }
119                                 }
120                                 continue;
121                         }
122
123                         // Find the next match for the current top of the stack.
124                         $match = $iter->current();
125                         $iter->next();
126
127                         // Quantifiers don't work well when the quantified thing can be empty.
128                         if ( $match->getLength() === 0 ) {
129                                 throw new \UnexpectedValueException( 'Empty match in quantifier!' );
130                         }
131
132                         $nextFrom = $match->getNext();
133
134                         // There can only be more matches after this one if we haven't
135                         // reached our maximum yet.
136                         $canBeMore = count( $stack ) < $this->max;
137
138                         // Commas are slightly tricky:
139                         //  1. If there is a following comma, start the next Matcher after it.
140                         //  2. If not, there can't be any more Matchers following.
141                         // And in either case optional whitespace is always allowed.
142                         if ( $this->commas ) {
143                                 $n = $nextFrom;
144                                 if ( isset( $values[$n] ) && $values[$n] instanceof Token &&
145                                         $values[$n]->type() === Token::T_WHITESPACE
146                                 ) {
147                                         $n = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options );
148                                 }
149                                 if ( isset( $values[$n] ) && $values[$n] instanceof Token &&
150                                         $values[$n]->type() === Token::T_COMMA
151                                 ) {
152                                         $nextFrom = $this->next( $values, $n, [ 'skip-whitespace' => true ] + $options );
153                                 } else {
154                                         $canBeMore = false;
155                                 }
156                         }
157
158                         // If there can be more matches, push another one onto the stack
159                         // and try it. Otherwise yield and continue with the current match.
160                         if ( $canBeMore ) {
161                                 $stack[] = [ $match, $this->matcher->generateMatches( $values, $nextFrom, $options ) ];
162                         } else {
163                                 $ct = count( $stack );
164                                 $pos = $match->getNext();
165                                 if ( $ct >= $this->min && $ct <= $this->max ) {
166                                         $newMatch = $this->makeMatch( $values, $start, $pos, $match, $stack );
167                                         $mid = $newMatch->getUniqueID();
168                                         if ( !isset( $used[$mid] ) ) {
169                                                 $used[$mid] = 1;
170                                                 yield $newMatch;
171                                         }
172                                 }
173                         }
174                 } while ( $stack );
175         }
176 }