]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - vendor/wikimedia/css-sanitizer/src/Grammar/MatcherFactory.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / wikimedia / css-sanitizer / src / Grammar / MatcherFactory.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\Token;
10
11 /**
12  * Factory for predefined Grammar matchers
13  * @note For security, the attr() and var() functions are not supported.
14  */
15 class MatcherFactory {
16         /** @var MatcherFactory|null */
17         private static $instance = null;
18
19         /** @var Matcher[] Cache of constructed matchers */
20         protected $cache = [];
21
22         /** @var string[] length units */
23         protected static $lengthUnits = [ 'em', 'ex', 'ch', 'rem', 'vw', 'vh',
24                 'vmin', 'vmax', 'cm', 'mm', 'Q', 'in', 'pc', 'pt', 'px' ];
25
26         /** @var string[] angle units */
27         protected static $angleUnits = [ 'deg', 'grad', 'rad', 'turn' ];
28
29         /** @var string[] time units */
30         protected static $timeUnits = [ 's', 'ms' ];
31
32         /** @var string[] frequency units */
33         protected static $frequencyUnits = [ 'Hz', 'kHz' ];
34
35         /**
36          * Return a static instance of the factory
37          * @return MatcherFactory
38          */
39         public static function singleton() {
40                 if ( !self::$instance ) {
41                         self::$instance = new self();
42                 }
43                 return self::$instance;
44         }
45
46         /**
47          * Matcher for optional whitespace
48          * @return Matcher
49          */
50         public function optionalWhitespace() {
51                 if ( !isset( $this->cache[__METHOD__] ) ) {
52                         $this->cache[__METHOD__] = new WhitespaceMatcher( [ 'significant' => false ] );
53                 }
54                 return $this->cache[__METHOD__];
55         }
56
57         /**
58          * Matcher for required whitespace
59          * @return Matcher
60          */
61         public function significantWhitespace() {
62                 if ( !isset( $this->cache[__METHOD__] ) ) {
63                         $this->cache[__METHOD__] = new WhitespaceMatcher( [ 'significant' => true ] );
64                 }
65                 return $this->cache[__METHOD__];
66         }
67
68         /**
69          * Matcher for a comma
70          * @return Matcher
71          */
72         public function comma() {
73                 if ( !isset( $this->cache[__METHOD__] ) ) {
74                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_COMMA );
75                 }
76                 return $this->cache[__METHOD__];
77         }
78
79         /**
80          * Matcher for an arbitrary identifier
81          * @return Matcher
82          */
83         public function ident() {
84                 if ( !isset( $this->cache[__METHOD__] ) ) {
85                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_IDENT );
86                 }
87                 return $this->cache[__METHOD__];
88         }
89
90         /**
91          * Matcher for a string
92          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#strings
93          * @warning If the string will be used as a URL, use self::urlstring() instead.
94          * @return Matcher
95          */
96         public function string() {
97                 if ( !isset( $this->cache[__METHOD__] ) ) {
98                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_STRING );
99                 }
100                 return $this->cache[__METHOD__];
101         }
102
103         /**
104          * Matcher for a string containing a URL
105          * @param string $type Type of resource referenced, e.g. "image" or "audio".
106          *  Not used here, but might be used by a subclass to validate the URL more strictly.
107          * @return Matcher
108          */
109         public function urlstring( $type ) {
110                 return $this->string();
111         }
112
113         /**
114          * Matcher for a URL
115          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#urls
116          * @param string $type Type of resource referenced, e.g. "image" or "audio".
117          *  Not used here, but might be used by a subclass to validate the URL more strictly.
118          * @return Matcher
119          */
120         public function url( $type ) {
121                 if ( !isset( $this->cache[__METHOD__] ) ) {
122                         $this->cache[__METHOD__] = new UrlMatcher();
123                 }
124                 return $this->cache[__METHOD__];
125         }
126
127         /**
128          * CSS-wide value keywords
129          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#common-keywords
130          * @return Matcher
131          */
132         public function cssWideKeywords() {
133                 if ( !isset( $this->cache[__METHOD__] ) ) {
134                         $this->cache[__METHOD__] = new KeywordMatcher( [ 'initial', 'inherit', 'unset' ] );
135                 }
136                 return $this->cache[__METHOD__];
137         }
138
139         /**
140          * Add calc() support to a basic type matcher
141          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#calc-notation
142          * @param Matcher $typeMatcher Matcher for the type
143          * @param string $type Type being matched
144          * @return Matcher
145          */
146         public function calc( Matcher $typeMatcher, $type ) {
147                 if ( $type === 'integer' ) {
148                         $num = $this->rawInteger();
149                 } else {
150                         $num = $this->rawNumber();
151                 }
152
153                 $ows = $this->optionalWhitespace();
154                 $ws = $this->significantWhitespace();
155
156                 // Definitions are recursive. This will be used by reference and later
157                 // will be replaced.
158                 $calcValue = new NothingMatcher();
159
160                 if ( $type === 'integer' ) {
161                         // Division will always resolve to a number, making the expression
162                         // invalid, so don't allow it.
163                         $calcProduct = new Juxtaposition( [
164                                 &$calcValue,
165                                 Quantifier::star( new Juxtaposition( [ $ows, new DelimMatcher( '*' ), $ows, &$calcValue ] ) )
166                         ] );
167                 } else {
168                         $calcProduct = new Juxtaposition( [
169                                 &$calcValue,
170                                 Quantifier::star( new Alternative( [
171                                         new Juxtaposition( [ $ows, new DelimMatcher( '*' ), $ows, &$calcValue ] ),
172                                         new Juxtaposition( [ $ows, new DelimMatcher( '/' ), $ows, $this->rawNumber() ] ),
173                                 ] ) ),
174                         ] );
175                 }
176
177                 $calcSum = new Juxtaposition( [
178                         $ows,
179                         $calcProduct,
180                         Quantifier::star( new Juxtaposition( [
181                                 $ws, new DelimMatcher( [ '+', '-' ] ), $ws, $calcProduct
182                         ] ) ),
183                         $ows,
184                 ] );
185
186                 $calcFunc = new FunctionMatcher( 'calc', $calcSum );
187
188                 if ( $num === $typeMatcher ) {
189                         $calcValue = new Alternative( [
190                                 $typeMatcher,
191                                 new BlockMatcher( Token::T_LEFT_PAREN, $calcSum ),
192                                 $calcFunc,
193                         ] );
194                 } else {
195                         $calcValue = new Alternative( [
196                                 $num,
197                                 $typeMatcher,
198                                 new BlockMatcher( Token::T_LEFT_PAREN, $calcSum ),
199                                 $calcFunc,
200                         ] );
201                 }
202
203                 return new Alternative( [ $typeMatcher, $calcFunc ] );
204         }
205
206         /**
207          * Matcher for an integer value, without calc()
208          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#integers
209          * @return Matcher
210          */
211         protected function rawInteger() {
212                 if ( !isset( $this->cache[__METHOD__] ) ) {
213                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
214                                 // The spec says it must match /^[+-]\d+$/, but the tokenizer
215                                 // should have marked any other number token as a 'number'
216                                 // anyway so let's not bother checking.
217                                 return $t->typeFlag() === 'integer';
218                         } );
219                 }
220                 return $this->cache[__METHOD__];
221         }
222
223         /**
224          * Matcher for an integer value
225          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#integers
226          * @return Matcher
227          */
228         public function integer() {
229                 if ( !isset( $this->cache[__METHOD__] ) ) {
230                         $this->cache[__METHOD__] = $this->calc( $this->rawInteger(), 'integer' );
231                 }
232                 return $this->cache[__METHOD__];
233         }
234
235         /**
236          * Matcher for a real number, without calc()
237          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#numbers
238          * @return Matcher
239          */
240         public function rawNumber() {
241                 if ( !isset( $this->cache[__METHOD__] ) ) {
242                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_NUMBER );
243                 }
244                 return $this->cache[__METHOD__];
245         }
246
247         /**
248          * Matcher for a real number
249          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#numbers
250          * @return Matcher
251          */
252         public function number() {
253                 if ( !isset( $this->cache[__METHOD__] ) ) {
254                         $this->cache[__METHOD__] = $this->calc( $this->rawNumber(), 'number' );
255                 }
256                 return $this->cache[__METHOD__];
257         }
258
259         /**
260          * Matcher for a percentage value, without calc()
261          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#percentages
262          * @return Matcher
263          */
264         public function rawPercentage() {
265                 if ( !isset( $this->cache[__METHOD__] ) ) {
266                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_PERCENTAGE );
267                 }
268                 return $this->cache[__METHOD__];
269         }
270
271         /**
272          * Matcher for a percentage value
273          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#percentages
274          * @return Matcher
275          */
276         public function percentage() {
277                 if ( !isset( $this->cache[__METHOD__] ) ) {
278                         $this->cache[__METHOD__] = $this->calc( $this->rawPercentage(), 'percentage' );
279                 }
280                 return $this->cache[__METHOD__];
281         }
282
283         /**
284          * Matcher for a length-percentage value
285          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-length-percentage
286          * @return Matcher
287          */
288         public function lengthPercentage() {
289                 if ( !isset( $this->cache[__METHOD__] ) ) {
290                         $this->cache[__METHOD__] = $this->calc(
291                                 new Alternative( [ $this->rawLength(), $this->rawPercentage() ] ),
292                                 'length'
293                         );
294                 }
295                 return $this->cache[__METHOD__];
296         }
297
298         /**
299          * Matcher for a frequency-percentage value
300          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-frequency-percentage
301          * @return Matcher
302          */
303         public function frequencyPercentage() {
304                 if ( !isset( $this->cache[__METHOD__] ) ) {
305                         $this->cache[__METHOD__] = $this->calc(
306                                 new Alternative( [ $this->rawFrequency(), $this->rawPercentage() ] ),
307                                 'frequency'
308                         );
309                 }
310                 return $this->cache[__METHOD__];
311         }
312
313         /**
314          * Matcher for a angle-percentage value
315          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-angle-percentage
316          * @return Matcher
317          */
318         public function anglePercentage() {
319                 if ( !isset( $this->cache[__METHOD__] ) ) {
320                         $this->cache[__METHOD__] = $this->calc(
321                                 new Alternative( [ $this->rawAngle(), $this->rawPercentage() ] ),
322                                 'angle'
323                         );
324                 }
325                 return $this->cache[__METHOD__];
326         }
327
328         /**
329          * Matcher for a time-percentage value
330          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-time-percentage
331          * @return Matcher
332          */
333         public function timePercentage() {
334                 if ( !isset( $this->cache[__METHOD__] ) ) {
335                         $this->cache[__METHOD__] = $this->calc(
336                                 new Alternative( [ $this->rawTime(), $this->rawPercentage() ] ),
337                                 'time'
338                         );
339                 }
340                 return $this->cache[__METHOD__];
341         }
342
343         /**
344          * Matcher for a number-percentage value
345          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-number-percentage
346          * @return Matcher
347          */
348         public function numberPercentage() {
349                 if ( !isset( $this->cache[__METHOD__] ) ) {
350                         $this->cache[__METHOD__] = $this->calc(
351                                 new Alternative( [ $this->rawNumber(), $this->rawPercentage() ] ),
352                                 'number'
353                         );
354                 }
355                 return $this->cache[__METHOD__];
356         }
357
358         /**
359          * Matcher for a dimension value
360          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#dimensions
361          * @return Matcher
362          */
363         public function dimension() {
364                 if ( !isset( $this->cache[__METHOD__] ) ) {
365                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION );
366                 }
367                 return $this->cache[__METHOD__];
368         }
369
370         /**
371          * Matches the number 0
372          * @return Matcher
373          */
374         protected function zero() {
375                 if ( !isset( $this->cache[__METHOD__] ) ) {
376                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
377                                 return $t->value() === 0 || $t->value() === 0.0;
378                         } );
379                 }
380                 return $this->cache[__METHOD__];
381         }
382
383         /**
384          * Matcher for a length value, without calc()
385          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#lengths
386          * @return Matcher
387          */
388         protected function rawLength() {
389                 if ( !isset( $this->cache[__METHOD__] ) ) {
390                         $unitsRe = '/^(' . join( '|', self::$lengthUnits ) . ')$/i';
391
392                         $this->cache[__METHOD__] = new Alternative( [
393                                 $this->zero(),
394                                 new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) use ( $unitsRe ) {
395                                         return preg_match( $unitsRe, $t->unit() );
396                                 } ),
397                         ] );
398                 }
399                 return $this->cache[__METHOD__];
400         }
401
402         /**
403          * Matcher for a length value
404          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#lengths
405          * @return Matcher
406          */
407         public function length() {
408                 if ( !isset( $this->cache[__METHOD__] ) ) {
409                         $this->cache[__METHOD__] = $this->calc( $this->rawLength(), 'length' );
410                 }
411                 return $this->cache[__METHOD__];
412         }
413
414         /**
415          * Matcher for an angle value, without calc()
416          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#angles
417          * @return Matcher
418          */
419         protected function rawAngle() {
420                 if ( !isset( $this->cache[__METHOD__] ) ) {
421                         $unitsRe = '/^(' . join( '|', self::$angleUnits ) . ')$/i';
422
423                         $this->cache[__METHOD__] = new Alternative( [
424                                 $this->zero(),
425                                 new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) use ( $unitsRe ) {
426                                         return preg_match( $unitsRe, $t->unit() );
427                                 } ),
428                         ] );
429                 }
430                 return $this->cache[__METHOD__];
431         }
432
433         /**
434          * Matcher for an angle value
435          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#angles
436          * @return Matcher
437          */
438         public function angle() {
439                 if ( !isset( $this->cache[__METHOD__] ) ) {
440                         $this->cache[__METHOD__] = $this->calc( $this->rawAngle(), 'angle' );
441                 }
442                 return $this->cache[__METHOD__];
443         }
444
445         /**
446          * Matcher for a duration (time) value, without calc()
447          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#time
448          * @return Matcher
449          */
450         protected function rawTime() {
451                 if ( !isset( $this->cache[__METHOD__] ) ) {
452                         $unitsRe = '/^(' . join( '|', self::$timeUnits ) . ')$/i';
453
454                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION,
455                                 function ( Token $t ) use ( $unitsRe ) {
456                                         return preg_match( $unitsRe, $t->unit() );
457                                 }
458                         );
459                 }
460                 return $this->cache[__METHOD__];
461         }
462
463         /**
464          * Matcher for a duration (time) value
465          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#time
466          * @return Matcher
467          */
468         public function time() {
469                 if ( !isset( $this->cache[__METHOD__] ) ) {
470                         $this->cache[__METHOD__] = $this->calc( $this->rawTime(), 'time' );
471                 }
472                 return $this->cache[__METHOD__];
473         }
474
475         /**
476          * Matcher for a frequency value, without calc()
477          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#frequency
478          * @return Matcher
479          */
480         protected function rawFrequency() {
481                 if ( !isset( $this->cache[__METHOD__] ) ) {
482                         $unitsRe = '/^(' . join( '|', self::$frequencyUnits ) . ')$/i';
483
484                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION,
485                                 function ( Token $t ) use ( $unitsRe ) {
486                                         return preg_match( $unitsRe, $t->unit() );
487                                 }
488                         );
489                 }
490                 return $this->cache[__METHOD__];
491         }
492
493         /**
494          * Matcher for a frequency value
495          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#frequency
496          * @return Matcher
497          */
498         public function frequency() {
499                 if ( !isset( $this->cache[__METHOD__] ) ) {
500                         $this->cache[__METHOD__] = $this->calc( $this->rawFrequency(), 'frequency' );
501                 }
502                 return $this->cache[__METHOD__];
503         }
504
505         /**
506          * Matcher for a resolution value
507          * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#resolution
508          * @return Matcher
509          */
510         public function resolution() {
511                 if ( !isset( $this->cache[__METHOD__] ) ) {
512                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
513                                 return preg_match( '/^(dpi|dpcm|dppx)$/i', $t->unit() );
514                         } );
515                 }
516                 return $this->cache[__METHOD__];
517         }
518
519         /**
520          * Matchers for color functions
521          * @return Matcher[]
522          */
523         protected function colorFuncs() {
524                 if ( !isset( $this->cache[__METHOD__] ) ) {
525                         $i = $this->integer();
526                         $n = $this->number();
527                         $p = $this->percentage();
528                         $this->cache[__METHOD__] = [
529                                 new FunctionMatcher( 'rgb', new Alternative( [
530                                         Quantifier::hash( $i, 3, 3 ),
531                                         Quantifier::hash( $p, 3, 3 ),
532                                 ] ) ),
533                                 new FunctionMatcher( 'rgba', new Alternative( [
534                                         new Juxtaposition( [ $i, $i, $i, $n ], true ),
535                                         new Juxtaposition( [ $p, $p, $p, $n ], true ),
536                                 ] ) ),
537                                 new FunctionMatcher( 'hsl', new Juxtaposition( [ $n, $p, $p ], true ) ),
538                                 new FunctionMatcher( 'hsla', new Juxtaposition( [ $n, $p, $p, $n ], true ) ),
539                         ];
540                 }
541                 return $this->cache[__METHOD__];
542         }
543
544         /**
545          * Matcher for a color value
546          * @see https://www.w3.org/TR/2011/REC-css3-color-20110607/#colorunits
547          * @return Matcher
548          */
549         public function color() {
550                 if ( !isset( $this->cache[__METHOD__] ) ) {
551                         $this->cache[__METHOD__] = new Alternative( array_merge( [
552                                 new KeywordMatcher( [
553                                         // Basic colors
554                                         'aqua', 'black', 'blue', 'fuchsia', 'gray', 'green',
555                                         'lime', 'maroon', 'navy', 'olive', 'purple', 'red',
556                                         'silver', 'teal', 'white', 'yellow',
557                                         // Extended colors
558                                         'aliceblue', 'antiquewhite', 'aquamarine', 'azure',
559                                         'beige', 'bisque', 'blanchedalmond', 'blueviolet', 'brown',
560                                         'burlywood', 'cadetblue', 'chartreuse', 'chocolate',
561                                         'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan',
562                                         'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray',
563                                         'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta',
564                                         'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
565                                         'darksalmon', 'darkseagreen', 'darkslateblue',
566                                         'darkslategray', 'darkslategrey', 'darkturquoise',
567                                         'darkviolet', 'deeppink', 'deepskyblue', 'dimgray',
568                                         'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite',
569                                         'forestgreen', 'gainsboro', 'ghostwhite', 'gold',
570                                         'goldenrod', 'greenyellow', 'grey', 'honeydew', 'hotpink',
571                                         'indianred', 'indigo', 'ivory', 'khaki', 'lavender',
572                                         'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue',
573                                         'lightcoral', 'lightcyan', 'lightgoldenrodyellow',
574                                         'lightgray', 'lightgreen', 'lightgrey', 'lightpink',
575                                         'lightsalmon', 'lightseagreen', 'lightskyblue',
576                                         'lightslategray', 'lightslategrey', 'lightsteelblue',
577                                         'lightyellow', 'limegreen', 'linen', 'magenta',
578                                         'mediumaquamarine', 'mediumblue', 'mediumorchid',
579                                         'mediumpurple', 'mediumseagreen', 'mediumslateblue',
580                                         'mediumspringgreen', 'mediumturquoise', 'mediumvioletred',
581                                         'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
582                                         'navajowhite', 'oldlace', 'olivedrab', 'orange',
583                                         'orangered', 'orchid', 'palegoldenrod', 'palegreen',
584                                         'paleturquoise', 'palevioletred', 'papayawhip',
585                                         'peachpuff', 'peru', 'pink', 'plum', 'powderblue',
586                                         'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
587                                         'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue',
588                                         'slateblue', 'slategray', 'slategrey', 'snow',
589                                         'springgreen', 'steelblue', 'tan', 'thistle', 'tomato',
590                                         'turquoise', 'violet', 'wheat', 'whitesmoke',
591                                         'yellowgreen',
592                                         // Other keywords. Intentionally omitting the deprecated system colors.
593                                         'transparent', 'currentColor',
594                                 ] ),
595                                 new TokenMatcher( Token::T_HASH, function ( Token $t ) {
596                                         return preg_match( '/^([0-9a-f]{3}|[0-9a-f]{6})$/i', $t->value() );
597                                 } ),
598                         ], $this->colorFuncs() ) );
599                 }
600                 return $this->cache[__METHOD__];
601         }
602
603         /**
604          * Matcher for an image value
605          * @see https://www.w3.org/TR/2012/CR-css3-images-20120417/#image-values
606          * @return Matcher
607          */
608         public function image() {
609                 if ( !isset( $this->cache[__METHOD__] ) ) {
610                         // https://www.w3.org/TR/2012/CR-css3-images-20120417/#image-list-type
611                         // Note the undefined <element-reference> production has been dropped from the Editor's Draft.
612                         $imageDecl = new Alternative( [
613                                 $this->url( 'image' ),
614                                 $this->urlstring( 'image' ),
615                         ] );
616
617                         // https://www.w3.org/TR/2012/CR-css3-images-20120417/#gradients
618                         $c = $this->comma();
619                         $colorStops = Quantifier::hash( new Juxtaposition( [
620                                 $this->color(),
621                                 // Not really <length-percentage>, but grammatically the same
622                                 Quantifier::optional( $this->lengthPercentage() ),
623                         ] ), 2, INF );
624                         $atPosition = new Juxtaposition( [ new KeywordMatcher( 'at' ), $this->position() ] );
625
626                         $linearGradient = new Juxtaposition( [
627                                 Quantifier::optional( new Juxtaposition( [
628                                         new Alternative( [
629                                                 $this->angle(),
630                                                 new Juxtaposition( [ new KeywordMatcher( 'to' ), UnorderedGroup::someOf( [
631                                                         new KeywordMatcher( [ 'left', 'right' ] ),
632                                                         new KeywordMatcher( [ 'top', 'bottom' ] ),
633                                                 ] ) ] )
634                                         ] ),
635                                         $c
636                                 ] ) ),
637                                 $colorStops,
638                         ] );
639                         $radialGradient = new Juxtaposition( [
640                                 Quantifier::optional( new Juxtaposition( [
641                                         new Alternative( [
642                                                 new Juxtaposition( [
643                                                         new Alternative( [
644                                                                 UnorderedGroup::someOf( [ new KeywordMatcher( 'circle' ), $this->length() ] ),
645                                                                 UnorderedGroup::someOf( [
646                                                                         new KeywordMatcher( 'ellipse' ),
647                                                                         // Not really <length-percentage>, but grammatically the same
648                                                                         Quantifier::count( $this->lengthPercentage(), 2, 2 )
649                                                                 ] ),
650                                                                 UnorderedGroup::someOf( [
651                                                                         new KeywordMatcher( [ 'circle', 'ellipse' ] ),
652                                                                         new KeywordMatcher( [
653                                                                                 'closest-side', 'farthest-side', 'closest-corner', 'farthest-corner'
654                                                                         ] ),
655                                                                 ] ),
656                                                         ] ),
657                                                         Quantifier::optional( $atPosition ),
658                                                 ] ),
659                                                 $atPosition
660                                         ] ),
661                                         $c
662                                 ] ) ),
663                                 $colorStops,
664                         ] );
665
666                         // Putting it all together
667                         $this->cache[__METHOD__] = new Alternative( [
668                                 $this->url( 'image' ),
669                                 new FunctionMatcher( 'image', new Juxtaposition( [
670                                         Quantifier::star( new Juxtaposition( [ $imageDecl, $c ] ) ),
671                                         new Alternative( [ $imageDecl, $this->color() ] ),
672                                 ] ) ),
673                                 new FunctionMatcher( 'linear-gradient', $linearGradient ),
674                                 new FunctionMatcher( 'radial-gradient', $radialGradient ),
675                                 new FunctionMatcher( 'repeating-linear-gradient', $linearGradient ),
676                                 new FunctionMatcher( 'repeating-radial-gradient', $radialGradient ),
677                         ] );
678                 }
679                 return $this->cache[__METHOD__];
680         }
681
682         /**
683          * Matcher for a position value
684          * @see https://www.w3.org/TR/2014/CR-css3-background-20140909/#ltpositiongt
685          * @return Matcher
686          */
687         public function position() {
688                 if ( !isset( $this->cache[__METHOD__] ) ) {
689                         $lp = $this->lengthPercentage();
690                         $olp = Quantifier::optional( $lp );
691                         $center = new KeywordMatcher( 'center' );
692                         $leftRight = new KeywordMatcher( [ 'left', 'right' ] );
693                         $topBottom = new KeywordMatcher( [ 'top', 'bottom' ] );
694
695                         $this->cache[__METHOD__] = new Alternative( [
696                                 new Alternative( [ $center, $leftRight, $topBottom, $lp ] ),
697                                 new Juxtaposition( [
698                                         new Alternative( [ $center, $leftRight, $lp ] ),
699                                         new Alternative( [ $center, $topBottom, $lp ] ),
700                                 ] ),
701                                 UnorderedGroup::allOf( [
702                                         new Alternative( [ $center, new Juxtaposition( [ $leftRight, $olp ] ) ] ),
703                                         new Alternative( [ $center, new Juxtaposition( [ $topBottom, $olp ] ) ] ),
704                                 ] ),
705                         ] );
706                 }
707                 return $this->cache[__METHOD__];
708         }
709
710         /**
711          * Matcher for a CSS media query
712          * @see https://www.w3.org/TR/2016/WD-mediaqueries-4-20160706/#mq-syntax
713          * @param bool $strict Only allow defined query types
714          * @return Matcher
715          */
716         public function cssMediaQuery( $strict = true ) {
717                 $key = __METHOD__ . ':' . ( $strict ? 'strict' : 'unstrict' );
718                 if ( !isset( $this->cache[$key] ) ) {
719                         if ( $strict ) {
720                                 $generalEnclosed = new NothingMatcher();
721
722                                 $mediaType = new KeywordMatcher( [
723                                         'all', 'print', 'screen', 'speech',
724                                         // deprecated
725                                         'tty', 'tv', 'projection', 'handheld', 'braille', 'embossed', 'aural'
726                                 ] );
727
728                                 $rangeFeatures = [
729                                         'width', 'height', 'aspect-ratio', 'resolution', 'color', 'color-index', 'monochrome',
730                                         // deprecated
731                                         'device-width', 'device-height', 'device-aspect-ratio'
732                                 ];
733                                 $discreteFeatures = [
734                                         'orientation', 'scan', 'grid', 'update', 'overflow-block', 'overflow-inline', 'color-gamut',
735                                         'pointer', 'hover', 'any-pointer', 'any-hover', 'scripting'
736                                 ];
737                                 $mfName = new KeywordMatcher( array_merge(
738                                         $rangeFeatures,
739                                         array_map( function ( $f ) {
740                                                 return "min-$f";
741                                         }, $rangeFeatures ),
742                                         array_map( function ( $f ) {
743                                                 return "max-$f";
744                                         }, $rangeFeatures ),
745                                         $discreteFeatures
746                                 ) );
747                         } else {
748                                 $anythingPlus = new AnythingMatcher( [ 'quantifier' => '+' ] );
749                                 $generalEnclosed = new Alternative( [
750                                         new FunctionMatcher( null, $anythingPlus ),
751                                         new BlockMatcher( Token::T_LEFT_PAREN,
752                                                 new Juxtaposition( [ $this->ident(), $anythingPlus ] )
753                                         ),
754                                 ] );
755                                 $mediaType = $this->ident();
756                                 $mfName = $this->ident();
757                         }
758
759                         $posInt = $this->calc(
760                                 new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
761                                         return $t->typeFlag() === 'integer' && preg_match( '/^\+?\d+$/', $t->representation() );
762                                 } ),
763                                 'integer'
764                         );
765                         $eq = new DelimMatcher( '=' );
766                         $oeq = Quantifier::optional( new Juxtaposition( [ new NoWhitespace, $eq ] ) );
767                         $ltgteq = Quantifier::optional( new Alternative( [
768                                 $eq,
769                                 new Juxtaposition( [ new DelimMatcher( [ '<', '>' ] ), $oeq ] ),
770                         ] ) );
771                         $lteq = new Juxtaposition( [ new DelimMatcher( '<' ), $oeq ] );
772                         $gteq = new Juxtaposition( [ new DelimMatcher( '>' ), $oeq ] );
773                         $mfValue = new Alternative( [
774                                 $this->number(),
775                                 $this->dimension(),
776                                 $this->ident(),
777                                 new Juxtaposition( [ $posInt, new DelimMatcher( '/' ), $posInt ] ),
778                         ] );
779
780                         $mediaInParens = new NothingMatcher(); // temporary
781                         $mediaNot = new Juxtaposition( [ new KeywordMatcher( 'not' ), &$mediaInParens ] );
782                         $mediaAnd = new Juxtaposition( [
783                                 &$mediaInParens,
784                                 Quantifier::plus( new Juxtaposition( [ new KeywordMatcher( 'and' ), &$mediaInParens ] ) )
785                         ] );
786                         $mediaOr = new Juxtaposition( [
787                                 &$mediaInParens,
788                                 Quantifier::plus( new Juxtaposition( [ new KeywordMatcher( 'or' ), &$mediaInParens ] ) )
789                         ] );
790                         $mediaCondition = new Alternative( [ $mediaNot, $mediaAnd, $mediaOr, &$mediaInParens ] );
791                         $mediaConditionWithoutOr = new Alternative( [ $mediaNot, $mediaAnd, &$mediaInParens ] );
792                         $mediaFeature = new BlockMatcher( Token::T_LEFT_PAREN, new Alternative( [
793                                 new Juxtaposition( [ $mfName, new TokenMatcher( Token::T_COLON ), $mfValue ] ), // <mf-plain>
794                                 $mfName, // <mf-boolean>
795                                 new Juxtaposition( [ $mfName, $ltgteq, $mfValue ] ), // <mf-range>, 1st alternative
796                                 new Juxtaposition( [ $mfValue, $ltgteq, $mfName ] ), // <mf-range>, 2nd alternative
797                                 new Juxtaposition( [ $mfValue, $lteq, $mfName, $lteq, $mfValue ] ), // <mf-range>, 3rd alt
798                                 new Juxtaposition( [ $mfValue, $gteq, $mfName, $gteq, $mfValue ] ), // <mf-range>, 4th alt
799                         ] ) );
800                         $mediaInParens = new Alternative( [
801                                 new BlockMatcher( Token::T_LEFT_PAREN, $mediaCondition ),
802                                 $mediaFeature,
803                                 $generalEnclosed,
804                         ] );
805
806                         $this->cache[$key] = new Alternative( [
807                                 $mediaCondition,
808                                 new Juxtaposition( [
809                                         Quantifier::optional( new KeywordMatcher( [ 'not', 'only' ] ) ),
810                                         $mediaType,
811                                         Quantifier::optional( new Juxtaposition( [
812                                                 new KeywordMatcher( 'and' ),
813                                                 $mediaConditionWithoutOr,
814                                         ] ) )
815                                 ] )
816                         ] );
817                 }
818
819                 return $this->cache[$key];
820         }
821
822         /**
823          * Matcher for a CSS media query list
824          * @see https://www.w3.org/TR/2016/WD-mediaqueries-4-20160706/#mq-syntax
825          * @param bool $strict Only allow defined query types
826          * @return Matcher
827          */
828         public function cssMediaQueryList( $strict = true ) {
829                 $key = __METHOD__ . ':' . ( $strict ? 'strict' : 'unstrict' );
830                 if ( !isset( $this->cache[$key] ) ) {
831                         $this->cache[$key] = Quantifier::hash( $this->cssMediaQuery( $strict ), 0, INF );
832                 }
833
834                 return $this->cache[$key];
835         }
836
837         /************************************************************************//**
838          * @name   CSS Selectors Level 3
839          * @{
840          *
841          * https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#w3cselgrammar
842          */
843
844         /**
845          * List of selectors
846          *
847          *     selector [ COMMA S* selector ]*
848          *
849          * Capturing is set up for the `selector`s.
850          *
851          * @return Matcher
852          */
853         public function cssSelectorList() {
854                 if ( !isset( $this->cache[__METHOD__] ) ) {
855                         // Technically the spec doesn't allow whitespace before the comma,
856                         // but I'd guess every browser does. So just use Quantifier::hash.
857                         $selector = $this->cssSelector()->capture( 'selector' );
858                         $this->cache[__METHOD__] = Quantifier::hash( $selector );
859                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
860                 }
861                 return $this->cache[__METHOD__];
862         }
863
864         /**
865          * A single selector
866          *
867          *     simple_selector_sequence [ combinator simple_selector_sequence ]*
868          *
869          * Capturing is set up for the `simple_selector_sequence`s (as 'simple') and `combinator`.
870          *
871          * @return Matcher
872          */
873         public function cssSelector() {
874                 if ( !isset( $this->cache[__METHOD__] ) ) {
875                         $simple = $this->cssSimpleSelectorSeq()->capture( 'simple' );
876                         $this->cache[__METHOD__] = new Juxtaposition( [
877                                 $simple,
878                                 Quantifier::star( new Juxtaposition( [
879                                         $this->cssCombinator()->capture( 'combinator' ),
880                                         $simple,
881                                 ] ) )
882                         ] );
883                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
884                 }
885                 return $this->cache[__METHOD__];
886         }
887
888         /**
889          * A CSS combinator
890          *
891          *     PLUS S* | GREATER S* | TILDE S* | S+
892          *
893          * (combinators can be surrounded by whitespace)
894          *
895          * @return Matcher
896          */
897         public function cssCombinator() {
898                 if ( !isset( $this->cache[__METHOD__] ) ) {
899                         $this->cache[__METHOD__] = new Alternative( [
900                                 new Juxtaposition( [
901                                         $this->optionalWhitespace(),
902                                         new DelimMatcher( [ '+', '>', '~' ] ),
903                                         $this->optionalWhitespace(),
904                                 ] ),
905                                 $this->significantWhitespace(),
906                         ] );
907                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
908                 }
909                 return $this->cache[__METHOD__];
910         }
911
912         /**
913          * A simple selector sequence
914          *
915          *     [ type_selector | universal ]
916          *     [ HASH | class | attrib | pseudo | negation ]*
917          *     | [ HASH | class | attrib | pseudo | negation ]+
918          *
919          * The following captures are set:
920          *  - element: [ type_selector | universal ]
921          *  - id: HASH
922          *  - class: class
923          *  - attrib: attrib
924          *  - pseudo: pseudo
925          *  - negation: negation
926          *
927          * @return Matcher
928          */
929         public function cssSimpleSelectorSeq() {
930                 if ( !isset( $this->cache[__METHOD__] ) ) {
931                         $hashEtc = new Alternative( [
932                                 $this->cssID()->capture( 'id' ),
933                                 $this->cssClass()->capture( 'class' ),
934                                 $this->cssAttrib()->capture( 'attrib' ),
935                                 $this->cssPseudo()->capture( 'pseudo' ),
936                                 $this->cssNegation()->capture( 'negation' ),
937                         ] );
938
939                         $this->cache[__METHOD__] = new Alternative( [
940                                 new Juxtaposition( [
941                                         Alternative::create( [
942                                                 $this->cssTypeSelector(),
943                                                 $this->cssUniversal(),
944                                         ] )->capture( 'element' ),
945                                         Quantifier::star( $hashEtc )
946                                 ] ),
947                                 Quantifier::plus( $hashEtc )
948                         ] );
949                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
950                 }
951                 return $this->cache[__METHOD__];
952         }
953
954         /**
955          * A type selector (i.e. a tag name)
956          *
957          *     [ namespace_prefix ] ? element_name
958          *
959          * where element_name is
960          *
961          *     IDENT
962          *
963          * @return Matcher
964          */
965         public function cssTypeSelector() {
966                 if ( !isset( $this->cache[__METHOD__] ) ) {
967                         $this->cache[__METHOD__] = new Juxtaposition( [
968                                 $this->cssOptionalNamespacePrefix(),
969                                 new TokenMatcher( Token::T_IDENT )
970                         ] );
971                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
972                 }
973                 return $this->cache[__METHOD__];
974         }
975
976         /**
977          * A namespace prefix
978          *
979          *      [ IDENT | '*' ]? '|'
980          *
981          * @return Matcher
982          */
983         public function cssNamespacePrefix() {
984                 if ( !isset( $this->cache[__METHOD__] ) ) {
985                         $this->cache[__METHOD__] = new Juxtaposition( [
986                                 Quantifier::optional( new Alternative( [
987                                         $this->ident(),
988                                         new DelimMatcher( [ '*' ] ),
989                                 ] ) ),
990                                 new DelimMatcher( [ '|' ] ),
991                         ] );
992                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
993                 }
994                 return $this->cache[__METHOD__];
995         }
996
997         /**
998          * An optional namespace prefix
999          *
1000          *     [ namespace_prefix ]?
1001          *
1002          * @return Matcher
1003          */
1004         private function cssOptionalNamespacePrefix() {
1005                 if ( !isset( $this->cache[__METHOD__] ) ) {
1006                         $this->cache[__METHOD__] = Quantifier::optional( $this->cssNamespacePrefix() );
1007                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1008                 }
1009                 return $this->cache[__METHOD__];
1010         }
1011
1012         /**
1013          * The universal selector
1014          *
1015          *     [ namespace_prefix ]? '*'
1016          *
1017          * @return Matcher
1018          */
1019         public function cssUniversal() {
1020                 if ( !isset( $this->cache[__METHOD__] ) ) {
1021                         $this->cache[__METHOD__] = new Juxtaposition( [
1022                                 $this->cssOptionalNamespacePrefix(),
1023                                 new DelimMatcher( [ '*' ] )
1024                         ] );
1025                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1026                 }
1027                 return $this->cache[__METHOD__];
1028         }
1029
1030         /**
1031          * An ID selector
1032          *
1033          *     HASH
1034          *
1035          * @return Matcher
1036          */
1037         public function cssID() {
1038                 if ( !isset( $this->cache[__METHOD__] ) ) {
1039                         $this->cache[__METHOD__] = new TokenMatcher( Token::T_HASH, function ( Token $t ) {
1040                                 return $t->typeFlag() === 'id';
1041                         } );
1042                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1043                 }
1044                 return $this->cache[__METHOD__];
1045         }
1046
1047         /**
1048          * A class selector
1049          *
1050          *     '.' IDENT
1051          *
1052          * @return Matcher
1053          */
1054         public function cssClass() {
1055                 if ( !isset( $this->cache[__METHOD__] ) ) {
1056                         $this->cache[__METHOD__] = new Juxtaposition( [
1057                                 new DelimMatcher( [ '.' ] ),
1058                                 $this->ident()
1059                         ] );
1060                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1061                 }
1062                 return $this->cache[__METHOD__];
1063         }
1064
1065         /**
1066          * An attribute selector
1067          *
1068          *     '[' S* [ namespace_prefix ]? IDENT S*
1069          *         [ [ PREFIXMATCH |
1070          *             SUFFIXMATCH |
1071          *             SUBSTRINGMATCH |
1072          *             '=' |
1073          *             INCLUDES |
1074          *             DASHMATCH ] S* [ IDENT | STRING ] S*
1075          *         ]? ']'
1076          *
1077          * Captures are set for the attribute, test, and value. Note that these
1078          * captures will probably be relative to the contents of the SimpleBlock
1079          * that this matcher matches!
1080          *
1081          * @return Matcher
1082          */
1083         public function cssAttrib() {
1084                 if ( !isset( $this->cache[__METHOD__] ) ) {
1085                         // An attribute is going to be parsed by the parser as a
1086                         // SimpleBlock, so that's what we need to look for here.
1087
1088                         $this->cache[__METHOD__] = new BlockMatcher( Token::T_LEFT_BRACKET,
1089                                 new Juxtaposition( [
1090                                         $this->optionalWhitespace(),
1091                                         Juxtaposition::create( [
1092                                                 $this->cssOptionalNamespacePrefix(),
1093                                                 $this->ident(),
1094                                         ] )->capture( 'attribute' ),
1095                                         $this->optionalWhitespace(),
1096                                         Quantifier::optional( new Juxtaposition( [
1097                                                 Alternative::create( [
1098                                                         new TokenMatcher( Token::T_PREFIX_MATCH ),
1099                                                         new TokenMatcher( Token::T_SUFFIX_MATCH ),
1100                                                         new TokenMatcher( Token::T_SUBSTRING_MATCH ),
1101                                                         new DelimMatcher( [ '=' ] ),
1102                                                         new TokenMatcher( Token::T_INCLUDE_MATCH ),
1103                                                         new TokenMatcher( Token::T_DASH_MATCH ),
1104                                                 ] )->capture( 'test' ),
1105                                                 $this->optionalWhitespace(),
1106                                                 Alternative::create( [
1107                                                         $this->ident(),
1108                                                         $this->string(),
1109                                                 ] )->capture( 'value' ),
1110                                                 $this->optionalWhitespace(),
1111                                         ] ) ),
1112                                 ] )
1113                         );
1114                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1115                 }
1116                 return $this->cache[__METHOD__];
1117         }
1118
1119         /**
1120          * A pseudo-class or pseudo-element
1121          *
1122          *     ':' ':'? [ IDENT | functional_pseudo ]
1123          *
1124          * Although this actually only matches the pseudo-selectors defined in the
1125          * following sources:
1126          * - https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#pseudo-classes
1127          * - https://www.w3.org/TR/2016/WD-css-pseudo-4-20160607/
1128          *
1129          * @return Matcher
1130          */
1131         public function cssPseudo() {
1132                 if ( !isset( $this->cache[__METHOD__] ) ) {
1133                         $colon = new TokenMatcher( Token::T_COLON );
1134                         $ows = $this->optionalWhitespace();
1135                         $anplusb = new Juxtaposition( [ $ows, $this->cssANplusB(), $ows ] );
1136                         $this->cache[__METHOD__] = new Alternative( [
1137                                 new Juxtaposition( [
1138                                         $colon,
1139                                         new Alternative( [
1140                                                 new KeywordMatcher( [
1141                                                         'link', 'visited', 'hover', 'active', 'focus', 'target', 'enabled', 'disabled', 'checked',
1142                                                         'indeterminate', 'root', 'first-child', 'last-child', 'first-of-type',
1143                                                         'last-of-type', 'only-child', 'only-of-type', 'empty',
1144                                                         // CSS2-compat elements with class syntax
1145                                                         'first-line', 'first-letter', 'before', 'after',
1146                                                 ] ),
1147                                                 new FunctionMatcher( 'lang', new Juxtaposition( [ $ows, $this->ident(), $ows ] ) ),
1148                                                 new FunctionMatcher( 'nth-child', $anplusb ),
1149                                                 new FunctionMatcher( 'nth-last-child', $anplusb ),
1150                                                 new FunctionMatcher( 'nth-of-type', $anplusb ),
1151                                                 new FunctionMatcher( 'nth-last-of-type', $anplusb ),
1152                                         ] ),
1153                                 ] ),
1154                                 new Juxtaposition( [
1155                                         $colon,
1156                                         $colon,
1157                                         new KeywordMatcher( [
1158                                                 'first-line', 'first-letter', 'before', 'after', 'selection', 'inactive-selection',
1159                                                 'spelling-error', 'grammar-error', 'placeholder'
1160                                         ] ),
1161                                 ] ),
1162                         ] );
1163                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1164                 }
1165                 return $this->cache[__METHOD__];
1166         }
1167
1168         /**
1169          * An "AN+B" form
1170          *
1171          * https://www.w3.org/TR/2014/CR-css-syntax-3-20140220/#anb
1172          *
1173          * @return Matcher
1174          */
1175         public function cssANplusB() {
1176                 if ( !isset( $this->cache[__METHOD__] ) ) {
1177                         // Quoth the spec:
1178                         //  > The An+B notation was originally defined using a slightly
1179                         //  > different tokenizer than the rest of CSS, resulting in a
1180                         //  > somewhat odd definition when expressed in terms of CSS tokens.
1181                         // That's a bit of an understatement
1182
1183                         $plus = new DelimMatcher( [ '+' ] );
1184                         $plusQ = Quantifier::optional( new DelimMatcher( [ '+' ] ) );
1185                         $n = new KeywordMatcher( [ 'n' ] );
1186                         $dashN = new KeywordMatcher( [ '-n' ] );
1187                         $nDash = new KeywordMatcher( [ 'n-' ] );
1188                         $plusQN = new Juxtaposition( [ $plusQ, $n ] );
1189                         $plusQNDash = new Juxtaposition( [ $plusQ, $nDash ] );
1190                         $nDimension = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1191                                 return $t->typeFlag() === 'integer' && !strcasecmp( $t->unit(), 'n' );
1192                         } );
1193                         $nDashDimension = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1194                                 return $t->typeFlag() === 'integer' && !strcasecmp( $t->unit(), 'n-' );
1195                         } );
1196                         $nDashDigitDimension = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1197                                 return $t->typeFlag() === 'integer' && preg_match( '/^n-\d+$/i', $t->unit() );
1198                         } );
1199                         $nDashDigitIdent = new TokenMatcher( Token::T_IDENT, function ( Token $t ) {
1200                                 return preg_match( '/^n-\d+$/i', $t->value() );
1201                         } );
1202                         $dashNDashDigitIdent = new TokenMatcher( Token::T_IDENT, function ( Token $t ) {
1203                                 return preg_match( '/^-n-\d+$/i', $t->value() );
1204                         } );
1205                         $signedInt = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
1206                                 return $t->typeFlag() === 'integer' && preg_match( '/^[+-]/', $t->representation() );
1207                         } );
1208                         $signlessInt = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
1209                                 return $t->typeFlag() === 'integer' && preg_match( '/^\d/', $t->representation() );
1210                         } );
1211                         $plusOrMinus = new DelimMatcher( [ '+', '-' ] );
1212                         $S = $this->optionalWhitespace();
1213
1214                         $this->cache[__METHOD__] = new Alternative( [
1215                                 new KeywordMatcher( [ 'odd', 'even' ] ),
1216                                 new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
1217                                         return $t->typeFlag() === 'integer';
1218                                 } ),
1219                                 $nDimension,
1220                                 $plusQN,
1221                                 $dashN,
1222                                 $nDashDigitDimension,
1223                                 new Juxtaposition( [ $plusQ, $nDashDigitIdent ] ),
1224                                 $dashNDashDigitIdent,
1225                                 new Juxtaposition( [ $nDimension, $S, $signedInt ] ),
1226                                 new Juxtaposition( [ $plusQN, $S, $signedInt ] ),
1227                                 new Juxtaposition( [ $dashN, $S, $signedInt ] ),
1228                                 new Juxtaposition( [ $nDashDimension, $S, $signlessInt ] ),
1229                                 new Juxtaposition( [ $plusQNDash, $S, $signlessInt ] ),
1230                                 new Juxtaposition( [ new KeywordMatcher( [ '-n-' ] ), $S, $signlessInt ] ),
1231                                 new Juxtaposition( [ $nDimension, $S, $plusOrMinus, $S, $signlessInt ] ),
1232                                 new Juxtaposition( [ $plusQN, $S, $plusOrMinus, $S, $signlessInt ] ),
1233                                 new Juxtaposition( [ $dashN, $S, $plusOrMinus, $S, $signlessInt ] )
1234                         ] );
1235                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1236                 }
1237                 return $this->cache[__METHOD__];
1238         }
1239
1240         /**
1241          * A negation
1242          *
1243          *     ':' not( S* [ type_selector | universal | HASH | class | attrib | pseudo ] S* ')'
1244          *
1245          * @return Matcher
1246          */
1247         public function cssNegation() {
1248                 if ( !isset( $this->cache[__METHOD__] ) ) {
1249                         // A negation is going to be parsed by the parser as a colon
1250                         // followed by a CSSFunction, so that's what we need to look for
1251                         // here.
1252
1253                         $this->cache[__METHOD__] = new Juxtaposition( [
1254                                 new TokenMatcher( Token::T_COLON ),
1255                                 new FunctionMatcher( 'not',
1256                                         new Juxtaposition( [
1257                                                 $this->optionalWhitespace(),
1258                                                 new Alternative( [
1259                                                         $this->cssTypeSelector(),
1260                                                         $this->cssUniversal(),
1261                                                         $this->cssID(),
1262                                                         $this->cssClass(),
1263                                                         $this->cssAttrib(),
1264                                                         $this->cssPseudo(),
1265                                                 ] ),
1266                                                 $this->optionalWhitespace(),
1267                                         ] )
1268                                 )
1269                         ] );
1270                         $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
1271                 }
1272                 return $this->cache[__METHOD__];
1273         }
1274
1275         /**@}*/
1276
1277 }
1278
1279 /**
1280  * For really cool vim folding this needs to be at the end:
1281  * vim: foldmarker=@{,@} foldmethod=marker
1282  */