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