--- /dev/null
+<?php
+/**
+ * @file
+ * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
+ */
+
+namespace Wikimedia\CSS\Grammar;
+
+use Wikimedia\CSS\Objects\Token;
+
+/**
+ * Factory for predefined Grammar matchers
+ * @note For security, the attr() and var() functions are not supported.
+ */
+class MatcherFactory {
+ /** @var MatcherFactory|null */
+ private static $instance = null;
+
+ /** @var Matcher[] Cache of constructed matchers */
+ protected $cache = [];
+
+ /** @var string[] length units */
+ protected static $lengthUnits = [ 'em', 'ex', 'ch', 'rem', 'vw', 'vh',
+ 'vmin', 'vmax', 'cm', 'mm', 'Q', 'in', 'pc', 'pt', 'px' ];
+
+ /** @var string[] angle units */
+ protected static $angleUnits = [ 'deg', 'grad', 'rad', 'turn' ];
+
+ /** @var string[] time units */
+ protected static $timeUnits = [ 's', 'ms' ];
+
+ /** @var string[] frequency units */
+ protected static $frequencyUnits = [ 'Hz', 'kHz' ];
+
+ /**
+ * Return a static instance of the factory
+ * @return MatcherFactory
+ */
+ public static function singleton() {
+ if ( !self::$instance ) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Matcher for optional whitespace
+ * @return Matcher
+ */
+ public function optionalWhitespace() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new WhitespaceMatcher( [ 'significant' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for required whitespace
+ * @return Matcher
+ */
+ public function significantWhitespace() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new WhitespaceMatcher( [ 'significant' => true ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a comma
+ * @return Matcher
+ */
+ public function comma() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_COMMA );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for an arbitrary identifier
+ * @return Matcher
+ */
+ public function ident() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_IDENT );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a string
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#strings
+ * @warning If the string will be used as a URL, use self::urlstring() instead.
+ * @return Matcher
+ */
+ public function string() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_STRING );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a string containing a URL
+ * @param string $type Type of resource referenced, e.g. "image" or "audio".
+ * Not used here, but might be used by a subclass to validate the URL more strictly.
+ * @return Matcher
+ */
+ public function urlstring( $type ) {
+ return $this->string();
+ }
+
+ /**
+ * Matcher for a URL
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#urls
+ * @param string $type Type of resource referenced, e.g. "image" or "audio".
+ * Not used here, but might be used by a subclass to validate the URL more strictly.
+ * @return Matcher
+ */
+ public function url( $type ) {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new UrlMatcher();
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * CSS-wide value keywords
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#common-keywords
+ * @return Matcher
+ */
+ public function cssWideKeywords() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new KeywordMatcher( [ 'initial', 'inherit', 'unset' ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Add calc() support to a basic type matcher
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#calc-notation
+ * @param Matcher $typeMatcher Matcher for the type
+ * @param string $type Type being matched
+ * @return Matcher
+ */
+ public function calc( Matcher $typeMatcher, $type ) {
+ if ( $type === 'integer' ) {
+ $num = $this->rawInteger();
+ } else {
+ $num = $this->rawNumber();
+ }
+
+ $ows = $this->optionalWhitespace();
+ $ws = $this->significantWhitespace();
+
+ // Definitions are recursive. This will be used by reference and later
+ // will be replaced.
+ $calcValue = new NothingMatcher();
+
+ if ( $type === 'integer' ) {
+ // Division will always resolve to a number, making the expression
+ // invalid, so don't allow it.
+ $calcProduct = new Juxtaposition( [
+ &$calcValue,
+ Quantifier::star( new Juxtaposition( [ $ows, new DelimMatcher( '*' ), $ows, &$calcValue ] ) )
+ ] );
+ } else {
+ $calcProduct = new Juxtaposition( [
+ &$calcValue,
+ Quantifier::star( new Alternative( [
+ new Juxtaposition( [ $ows, new DelimMatcher( '*' ), $ows, &$calcValue ] ),
+ new Juxtaposition( [ $ows, new DelimMatcher( '/' ), $ows, $this->rawNumber() ] ),
+ ] ) ),
+ ] );
+ }
+
+ $calcSum = new Juxtaposition( [
+ $ows,
+ $calcProduct,
+ Quantifier::star( new Juxtaposition( [
+ $ws, new DelimMatcher( [ '+', '-' ] ), $ws, $calcProduct
+ ] ) ),
+ $ows,
+ ] );
+
+ $calcFunc = new FunctionMatcher( 'calc', $calcSum );
+
+ if ( $num === $typeMatcher ) {
+ $calcValue = new Alternative( [
+ $typeMatcher,
+ new BlockMatcher( Token::T_LEFT_PAREN, $calcSum ),
+ $calcFunc,
+ ] );
+ } else {
+ $calcValue = new Alternative( [
+ $num,
+ $typeMatcher,
+ new BlockMatcher( Token::T_LEFT_PAREN, $calcSum ),
+ $calcFunc,
+ ] );
+ }
+
+ return new Alternative( [ $typeMatcher, $calcFunc ] );
+ }
+
+ /**
+ * Matcher for an integer value, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#integers
+ * @return Matcher
+ */
+ protected function rawInteger() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
+ // The spec says it must match /^[+-]\d+$/, but the tokenizer
+ // should have marked any other number token as a 'number'
+ // anyway so let's not bother checking.
+ return $t->typeFlag() === 'integer';
+ } );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for an integer value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#integers
+ * @return Matcher
+ */
+ public function integer() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawInteger(), 'integer' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a real number, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#numbers
+ * @return Matcher
+ */
+ public function rawNumber() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_NUMBER );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a real number
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#numbers
+ * @return Matcher
+ */
+ public function number() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawNumber(), 'number' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a percentage value, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#percentages
+ * @return Matcher
+ */
+ public function rawPercentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_PERCENTAGE );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a percentage value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#percentages
+ * @return Matcher
+ */
+ public function percentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawPercentage(), 'percentage' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a length-percentage value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-length-percentage
+ * @return Matcher
+ */
+ public function lengthPercentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc(
+ new Alternative( [ $this->rawLength(), $this->rawPercentage() ] ),
+ 'length'
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a frequency-percentage value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-frequency-percentage
+ * @return Matcher
+ */
+ public function frequencyPercentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc(
+ new Alternative( [ $this->rawFrequency(), $this->rawPercentage() ] ),
+ 'frequency'
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a angle-percentage value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-angle-percentage
+ * @return Matcher
+ */
+ public function anglePercentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc(
+ new Alternative( [ $this->rawAngle(), $this->rawPercentage() ] ),
+ 'angle'
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a time-percentage value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-time-percentage
+ * @return Matcher
+ */
+ public function timePercentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc(
+ new Alternative( [ $this->rawTime(), $this->rawPercentage() ] ),
+ 'time'
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a number-percentage value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#typedef-number-percentage
+ * @return Matcher
+ */
+ public function numberPercentage() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc(
+ new Alternative( [ $this->rawNumber(), $this->rawPercentage() ] ),
+ 'number'
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a dimension value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#dimensions
+ * @return Matcher
+ */
+ public function dimension() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matches the number 0
+ * @return Matcher
+ */
+ protected function zero() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
+ return $t->value() === 0 || $t->value() === 0.0;
+ } );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a length value, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#lengths
+ * @return Matcher
+ */
+ protected function rawLength() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $unitsRe = '/^(' . join( '|', self::$lengthUnits ) . ')$/i';
+
+ $this->cache[__METHOD__] = new Alternative( [
+ $this->zero(),
+ new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) use ( $unitsRe ) {
+ return preg_match( $unitsRe, $t->unit() );
+ } ),
+ ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a length value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#lengths
+ * @return Matcher
+ */
+ public function length() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawLength(), 'length' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for an angle value, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#angles
+ * @return Matcher
+ */
+ protected function rawAngle() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $unitsRe = '/^(' . join( '|', self::$angleUnits ) . ')$/i';
+
+ $this->cache[__METHOD__] = new Alternative( [
+ $this->zero(),
+ new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) use ( $unitsRe ) {
+ return preg_match( $unitsRe, $t->unit() );
+ } ),
+ ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for an angle value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#angles
+ * @return Matcher
+ */
+ public function angle() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawAngle(), 'angle' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a duration (time) value, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#time
+ * @return Matcher
+ */
+ protected function rawTime() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $unitsRe = '/^(' . join( '|', self::$timeUnits ) . ')$/i';
+
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION,
+ function ( Token $t ) use ( $unitsRe ) {
+ return preg_match( $unitsRe, $t->unit() );
+ }
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a duration (time) value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#time
+ * @return Matcher
+ */
+ public function time() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawTime(), 'time' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a frequency value, without calc()
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#frequency
+ * @return Matcher
+ */
+ protected function rawFrequency() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $unitsRe = '/^(' . join( '|', self::$frequencyUnits ) . ')$/i';
+
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION,
+ function ( Token $t ) use ( $unitsRe ) {
+ return preg_match( $unitsRe, $t->unit() );
+ }
+ );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a frequency value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#frequency
+ * @return Matcher
+ */
+ public function frequency() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = $this->calc( $this->rawFrequency(), 'frequency' );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a resolution value
+ * @see https://www.w3.org/TR/2016/CR-css-values-3-20160929/#resolution
+ * @return Matcher
+ */
+ public function resolution() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
+ return preg_match( '/^(dpi|dpcm|dppx)$/i', $t->unit() );
+ } );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matchers for color functions
+ * @return Matcher[]
+ */
+ protected function colorFuncs() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $i = $this->integer();
+ $n = $this->number();
+ $p = $this->percentage();
+ $this->cache[__METHOD__] = [
+ new FunctionMatcher( 'rgb', new Alternative( [
+ Quantifier::hash( $i, 3, 3 ),
+ Quantifier::hash( $p, 3, 3 ),
+ ] ) ),
+ new FunctionMatcher( 'rgba', new Alternative( [
+ new Juxtaposition( [ $i, $i, $i, $n ], true ),
+ new Juxtaposition( [ $p, $p, $p, $n ], true ),
+ ] ) ),
+ new FunctionMatcher( 'hsl', new Juxtaposition( [ $n, $p, $p ], true ) ),
+ new FunctionMatcher( 'hsla', new Juxtaposition( [ $n, $p, $p, $n ], true ) ),
+ ];
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a color value
+ * @see https://www.w3.org/TR/2011/REC-css3-color-20110607/#colorunits
+ * @return Matcher
+ */
+ public function color() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new Alternative( array_merge( [
+ new KeywordMatcher( [
+ // Basic colors
+ 'aqua', 'black', 'blue', 'fuchsia', 'gray', 'green',
+ 'lime', 'maroon', 'navy', 'olive', 'purple', 'red',
+ 'silver', 'teal', 'white', 'yellow',
+ // Extended colors
+ 'aliceblue', 'antiquewhite', 'aquamarine', 'azure',
+ 'beige', 'bisque', 'blanchedalmond', 'blueviolet', 'brown',
+ 'burlywood', 'cadetblue', 'chartreuse', 'chocolate',
+ 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan',
+ 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray',
+ 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta',
+ 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
+ 'darksalmon', 'darkseagreen', 'darkslateblue',
+ 'darkslategray', 'darkslategrey', 'darkturquoise',
+ 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray',
+ 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite',
+ 'forestgreen', 'gainsboro', 'ghostwhite', 'gold',
+ 'goldenrod', 'greenyellow', 'grey', 'honeydew', 'hotpink',
+ 'indianred', 'indigo', 'ivory', 'khaki', 'lavender',
+ 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue',
+ 'lightcoral', 'lightcyan', 'lightgoldenrodyellow',
+ 'lightgray', 'lightgreen', 'lightgrey', 'lightpink',
+ 'lightsalmon', 'lightseagreen', 'lightskyblue',
+ 'lightslategray', 'lightslategrey', 'lightsteelblue',
+ 'lightyellow', 'limegreen', 'linen', 'magenta',
+ 'mediumaquamarine', 'mediumblue', 'mediumorchid',
+ 'mediumpurple', 'mediumseagreen', 'mediumslateblue',
+ 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred',
+ 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
+ 'navajowhite', 'oldlace', 'olivedrab', 'orange',
+ 'orangered', 'orchid', 'palegoldenrod', 'palegreen',
+ 'paleturquoise', 'palevioletred', 'papayawhip',
+ 'peachpuff', 'peru', 'pink', 'plum', 'powderblue',
+ 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
+ 'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue',
+ 'slateblue', 'slategray', 'slategrey', 'snow',
+ 'springgreen', 'steelblue', 'tan', 'thistle', 'tomato',
+ 'turquoise', 'violet', 'wheat', 'whitesmoke',
+ 'yellowgreen',
+ // Other keywords. Intentionally omitting the deprecated system colors.
+ 'transparent', 'currentColor',
+ ] ),
+ new TokenMatcher( Token::T_HASH, function ( Token $t ) {
+ return preg_match( '/^([0-9a-f]{3}|[0-9a-f]{6})$/i', $t->value() );
+ } ),
+ ], $this->colorFuncs() ) );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for an image value
+ * @see https://www.w3.org/TR/2012/CR-css3-images-20120417/#image-values
+ * @return Matcher
+ */
+ public function image() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ // https://www.w3.org/TR/2012/CR-css3-images-20120417/#image-list-type
+ // Note the undefined <element-reference> production has been dropped from the Editor's Draft.
+ $imageDecl = new Alternative( [
+ $this->url( 'image' ),
+ $this->urlstring( 'image' ),
+ ] );
+
+ // https://www.w3.org/TR/2012/CR-css3-images-20120417/#gradients
+ $c = $this->comma();
+ $colorStops = Quantifier::hash( new Juxtaposition( [
+ $this->color(),
+ // Not really <length-percentage>, but grammatically the same
+ Quantifier::optional( $this->lengthPercentage() ),
+ ] ), 2, INF );
+ $atPosition = new Juxtaposition( [ new KeywordMatcher( 'at' ), $this->position() ] );
+
+ $linearGradient = new Juxtaposition( [
+ Quantifier::optional( new Juxtaposition( [
+ new Alternative( [
+ $this->angle(),
+ new Juxtaposition( [ new KeywordMatcher( 'to' ), UnorderedGroup::someOf( [
+ new KeywordMatcher( [ 'left', 'right' ] ),
+ new KeywordMatcher( [ 'top', 'bottom' ] ),
+ ] ) ] )
+ ] ),
+ $c
+ ] ) ),
+ $colorStops,
+ ] );
+ $radialGradient = new Juxtaposition( [
+ Quantifier::optional( new Juxtaposition( [
+ new Alternative( [
+ new Juxtaposition( [
+ new Alternative( [
+ UnorderedGroup::someOf( [ new KeywordMatcher( 'circle' ), $this->length() ] ),
+ UnorderedGroup::someOf( [
+ new KeywordMatcher( 'ellipse' ),
+ // Not really <length-percentage>, but grammatically the same
+ Quantifier::count( $this->lengthPercentage(), 2, 2 )
+ ] ),
+ UnorderedGroup::someOf( [
+ new KeywordMatcher( [ 'circle', 'ellipse' ] ),
+ new KeywordMatcher( [
+ 'closest-side', 'farthest-side', 'closest-corner', 'farthest-corner'
+ ] ),
+ ] ),
+ ] ),
+ Quantifier::optional( $atPosition ),
+ ] ),
+ $atPosition
+ ] ),
+ $c
+ ] ) ),
+ $colorStops,
+ ] );
+
+ // Putting it all together
+ $this->cache[__METHOD__] = new Alternative( [
+ $this->url( 'image' ),
+ new FunctionMatcher( 'image', new Juxtaposition( [
+ Quantifier::star( new Juxtaposition( [ $imageDecl, $c ] ) ),
+ new Alternative( [ $imageDecl, $this->color() ] ),
+ ] ) ),
+ new FunctionMatcher( 'linear-gradient', $linearGradient ),
+ new FunctionMatcher( 'radial-gradient', $radialGradient ),
+ new FunctionMatcher( 'repeating-linear-gradient', $linearGradient ),
+ new FunctionMatcher( 'repeating-radial-gradient', $radialGradient ),
+ ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a position value
+ * @see https://www.w3.org/TR/2014/CR-css3-background-20140909/#ltpositiongt
+ * @return Matcher
+ */
+ public function position() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $lp = $this->lengthPercentage();
+ $olp = Quantifier::optional( $lp );
+ $center = new KeywordMatcher( 'center' );
+ $leftRight = new KeywordMatcher( [ 'left', 'right' ] );
+ $topBottom = new KeywordMatcher( [ 'top', 'bottom' ] );
+
+ $this->cache[__METHOD__] = new Alternative( [
+ new Alternative( [ $center, $leftRight, $topBottom, $lp ] ),
+ new Juxtaposition( [
+ new Alternative( [ $center, $leftRight, $lp ] ),
+ new Alternative( [ $center, $topBottom, $lp ] ),
+ ] ),
+ UnorderedGroup::allOf( [
+ new Alternative( [ $center, new Juxtaposition( [ $leftRight, $olp ] ) ] ),
+ new Alternative( [ $center, new Juxtaposition( [ $topBottom, $olp ] ) ] ),
+ ] ),
+ ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * Matcher for a CSS media query
+ * @see https://www.w3.org/TR/2016/WD-mediaqueries-4-20160706/#mq-syntax
+ * @param bool $strict Only allow defined query types
+ * @return Matcher
+ */
+ public function cssMediaQuery( $strict = true ) {
+ $key = __METHOD__ . ':' . ( $strict ? 'strict' : 'unstrict' );
+ if ( !isset( $this->cache[$key] ) ) {
+ if ( $strict ) {
+ $generalEnclosed = new NothingMatcher();
+
+ $mediaType = new KeywordMatcher( [
+ 'all', 'print', 'screen', 'speech',
+ // deprecated
+ 'tty', 'tv', 'projection', 'handheld', 'braille', 'embossed', 'aural'
+ ] );
+
+ $rangeFeatures = [
+ 'width', 'height', 'aspect-ratio', 'resolution', 'color', 'color-index', 'monochrome',
+ // deprecated
+ 'device-width', 'device-height', 'device-aspect-ratio'
+ ];
+ $discreteFeatures = [
+ 'orientation', 'scan', 'grid', 'update', 'overflow-block', 'overflow-inline', 'color-gamut',
+ 'pointer', 'hover', 'any-pointer', 'any-hover', 'scripting'
+ ];
+ $mfName = new KeywordMatcher( array_merge(
+ $rangeFeatures,
+ array_map( function ( $f ) {
+ return "min-$f";
+ }, $rangeFeatures ),
+ array_map( function ( $f ) {
+ return "max-$f";
+ }, $rangeFeatures ),
+ $discreteFeatures
+ ) );
+ } else {
+ $anythingPlus = new AnythingMatcher( [ 'quantifier' => '+' ] );
+ $generalEnclosed = new Alternative( [
+ new FunctionMatcher( null, $anythingPlus ),
+ new BlockMatcher( Token::T_LEFT_PAREN,
+ new Juxtaposition( [ $this->ident(), $anythingPlus ] )
+ ),
+ ] );
+ $mediaType = $this->ident();
+ $mfName = $this->ident();
+ }
+
+ $posInt = $this->calc(
+ new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
+ return $t->typeFlag() === 'integer' && preg_match( '/^\+?\d+$/', $t->representation() );
+ } ),
+ 'integer'
+ );
+ $eq = new DelimMatcher( '=' );
+ $oeq = Quantifier::optional( new Juxtaposition( [ new NoWhitespace, $eq ] ) );
+ $ltgteq = Quantifier::optional( new Alternative( [
+ $eq,
+ new Juxtaposition( [ new DelimMatcher( [ '<', '>' ] ), $oeq ] ),
+ ] ) );
+ $lteq = new Juxtaposition( [ new DelimMatcher( '<' ), $oeq ] );
+ $gteq = new Juxtaposition( [ new DelimMatcher( '>' ), $oeq ] );
+ $mfValue = new Alternative( [
+ $this->number(),
+ $this->dimension(),
+ $this->ident(),
+ new Juxtaposition( [ $posInt, new DelimMatcher( '/' ), $posInt ] ),
+ ] );
+
+ $mediaInParens = new NothingMatcher(); // temporary
+ $mediaNot = new Juxtaposition( [ new KeywordMatcher( 'not' ), &$mediaInParens ] );
+ $mediaAnd = new Juxtaposition( [
+ &$mediaInParens,
+ Quantifier::plus( new Juxtaposition( [ new KeywordMatcher( 'and' ), &$mediaInParens ] ) )
+ ] );
+ $mediaOr = new Juxtaposition( [
+ &$mediaInParens,
+ Quantifier::plus( new Juxtaposition( [ new KeywordMatcher( 'or' ), &$mediaInParens ] ) )
+ ] );
+ $mediaCondition = new Alternative( [ $mediaNot, $mediaAnd, $mediaOr, &$mediaInParens ] );
+ $mediaConditionWithoutOr = new Alternative( [ $mediaNot, $mediaAnd, &$mediaInParens ] );
+ $mediaFeature = new BlockMatcher( Token::T_LEFT_PAREN, new Alternative( [
+ new Juxtaposition( [ $mfName, new TokenMatcher( Token::T_COLON ), $mfValue ] ), // <mf-plain>
+ $mfName, // <mf-boolean>
+ new Juxtaposition( [ $mfName, $ltgteq, $mfValue ] ), // <mf-range>, 1st alternative
+ new Juxtaposition( [ $mfValue, $ltgteq, $mfName ] ), // <mf-range>, 2nd alternative
+ new Juxtaposition( [ $mfValue, $lteq, $mfName, $lteq, $mfValue ] ), // <mf-range>, 3rd alt
+ new Juxtaposition( [ $mfValue, $gteq, $mfName, $gteq, $mfValue ] ), // <mf-range>, 4th alt
+ ] ) );
+ $mediaInParens = new Alternative( [
+ new BlockMatcher( Token::T_LEFT_PAREN, $mediaCondition ),
+ $mediaFeature,
+ $generalEnclosed,
+ ] );
+
+ $this->cache[$key] = new Alternative( [
+ $mediaCondition,
+ new Juxtaposition( [
+ Quantifier::optional( new KeywordMatcher( [ 'not', 'only' ] ) ),
+ $mediaType,
+ Quantifier::optional( new Juxtaposition( [
+ new KeywordMatcher( 'and' ),
+ $mediaConditionWithoutOr,
+ ] ) )
+ ] )
+ ] );
+ }
+
+ return $this->cache[$key];
+ }
+
+ /**
+ * Matcher for a CSS media query list
+ * @see https://www.w3.org/TR/2016/WD-mediaqueries-4-20160706/#mq-syntax
+ * @param bool $strict Only allow defined query types
+ * @return Matcher
+ */
+ public function cssMediaQueryList( $strict = true ) {
+ $key = __METHOD__ . ':' . ( $strict ? 'strict' : 'unstrict' );
+ if ( !isset( $this->cache[$key] ) ) {
+ $this->cache[$key] = Quantifier::hash( $this->cssMediaQuery( $strict ), 0, INF );
+ }
+
+ return $this->cache[$key];
+ }
+
+ /************************************************************************//**
+ * @name CSS Selectors Level 3
+ * @{
+ *
+ * https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#w3cselgrammar
+ */
+
+ /**
+ * List of selectors
+ *
+ * selector [ COMMA S* selector ]*
+ *
+ * Capturing is set up for the `selector`s.
+ *
+ * @return Matcher
+ */
+ public function cssSelectorList() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ // Technically the spec doesn't allow whitespace before the comma,
+ // but I'd guess every browser does. So just use Quantifier::hash.
+ $selector = $this->cssSelector()->capture( 'selector' );
+ $this->cache[__METHOD__] = Quantifier::hash( $selector );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A single selector
+ *
+ * simple_selector_sequence [ combinator simple_selector_sequence ]*
+ *
+ * Capturing is set up for the `simple_selector_sequence`s (as 'simple') and `combinator`.
+ *
+ * @return Matcher
+ */
+ public function cssSelector() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $simple = $this->cssSimpleSelectorSeq()->capture( 'simple' );
+ $this->cache[__METHOD__] = new Juxtaposition( [
+ $simple,
+ Quantifier::star( new Juxtaposition( [
+ $this->cssCombinator()->capture( 'combinator' ),
+ $simple,
+ ] ) )
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A CSS combinator
+ *
+ * PLUS S* | GREATER S* | TILDE S* | S+
+ *
+ * (combinators can be surrounded by whitespace)
+ *
+ * @return Matcher
+ */
+ public function cssCombinator() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new Alternative( [
+ new Juxtaposition( [
+ $this->optionalWhitespace(),
+ new DelimMatcher( [ '+', '>', '~' ] ),
+ $this->optionalWhitespace(),
+ ] ),
+ $this->significantWhitespace(),
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A simple selector sequence
+ *
+ * [ type_selector | universal ]
+ * [ HASH | class | attrib | pseudo | negation ]*
+ * | [ HASH | class | attrib | pseudo | negation ]+
+ *
+ * The following captures are set:
+ * - element: [ type_selector | universal ]
+ * - id: HASH
+ * - class: class
+ * - attrib: attrib
+ * - pseudo: pseudo
+ * - negation: negation
+ *
+ * @return Matcher
+ */
+ public function cssSimpleSelectorSeq() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $hashEtc = new Alternative( [
+ $this->cssID()->capture( 'id' ),
+ $this->cssClass()->capture( 'class' ),
+ $this->cssAttrib()->capture( 'attrib' ),
+ $this->cssPseudo()->capture( 'pseudo' ),
+ $this->cssNegation()->capture( 'negation' ),
+ ] );
+
+ $this->cache[__METHOD__] = new Alternative( [
+ new Juxtaposition( [
+ Alternative::create( [
+ $this->cssTypeSelector(),
+ $this->cssUniversal(),
+ ] )->capture( 'element' ),
+ Quantifier::star( $hashEtc )
+ ] ),
+ Quantifier::plus( $hashEtc )
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A type selector (i.e. a tag name)
+ *
+ * [ namespace_prefix ] ? element_name
+ *
+ * where element_name is
+ *
+ * IDENT
+ *
+ * @return Matcher
+ */
+ public function cssTypeSelector() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new Juxtaposition( [
+ $this->cssOptionalNamespacePrefix(),
+ new TokenMatcher( Token::T_IDENT )
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A namespace prefix
+ *
+ * [ IDENT | '*' ]? '|'
+ *
+ * @return Matcher
+ */
+ public function cssNamespacePrefix() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new Juxtaposition( [
+ Quantifier::optional( new Alternative( [
+ $this->ident(),
+ new DelimMatcher( [ '*' ] ),
+ ] ) ),
+ new DelimMatcher( [ '|' ] ),
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * An optional namespace prefix
+ *
+ * [ namespace_prefix ]?
+ *
+ * @return Matcher
+ */
+ private function cssOptionalNamespacePrefix() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = Quantifier::optional( $this->cssNamespacePrefix() );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * The universal selector
+ *
+ * [ namespace_prefix ]? '*'
+ *
+ * @return Matcher
+ */
+ public function cssUniversal() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new Juxtaposition( [
+ $this->cssOptionalNamespacePrefix(),
+ new DelimMatcher( [ '*' ] )
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * An ID selector
+ *
+ * HASH
+ *
+ * @return Matcher
+ */
+ public function cssID() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new TokenMatcher( Token::T_HASH, function ( Token $t ) {
+ return $t->typeFlag() === 'id';
+ } );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A class selector
+ *
+ * '.' IDENT
+ *
+ * @return Matcher
+ */
+ public function cssClass() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $this->cache[__METHOD__] = new Juxtaposition( [
+ new DelimMatcher( [ '.' ] ),
+ $this->ident()
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * An attribute selector
+ *
+ * '[' S* [ namespace_prefix ]? IDENT S*
+ * [ [ PREFIXMATCH |
+ * SUFFIXMATCH |
+ * SUBSTRINGMATCH |
+ * '=' |
+ * INCLUDES |
+ * DASHMATCH ] S* [ IDENT | STRING ] S*
+ * ]? ']'
+ *
+ * Captures are set for the attribute, test, and value. Note that these
+ * captures will probably be relative to the contents of the SimpleBlock
+ * that this matcher matches!
+ *
+ * @return Matcher
+ */
+ public function cssAttrib() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ // An attribute is going to be parsed by the parser as a
+ // SimpleBlock, so that's what we need to look for here.
+
+ $this->cache[__METHOD__] = new BlockMatcher( Token::T_LEFT_BRACKET,
+ new Juxtaposition( [
+ $this->optionalWhitespace(),
+ Juxtaposition::create( [
+ $this->cssOptionalNamespacePrefix(),
+ $this->ident(),
+ ] )->capture( 'attribute' ),
+ $this->optionalWhitespace(),
+ Quantifier::optional( new Juxtaposition( [
+ Alternative::create( [
+ new TokenMatcher( Token::T_PREFIX_MATCH ),
+ new TokenMatcher( Token::T_SUFFIX_MATCH ),
+ new TokenMatcher( Token::T_SUBSTRING_MATCH ),
+ new DelimMatcher( [ '=' ] ),
+ new TokenMatcher( Token::T_INCLUDE_MATCH ),
+ new TokenMatcher( Token::T_DASH_MATCH ),
+ ] )->capture( 'test' ),
+ $this->optionalWhitespace(),
+ Alternative::create( [
+ $this->ident(),
+ $this->string(),
+ ] )->capture( 'value' ),
+ $this->optionalWhitespace(),
+ ] ) ),
+ ] )
+ );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A pseudo-class or pseudo-element
+ *
+ * ':' ':'? [ IDENT | functional_pseudo ]
+ *
+ * Although this actually only matches the pseudo-selectors defined in the
+ * following sources:
+ * - https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#pseudo-classes
+ * - https://www.w3.org/TR/2016/WD-css-pseudo-4-20160607/
+ *
+ * @return Matcher
+ */
+ public function cssPseudo() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ $colon = new TokenMatcher( Token::T_COLON );
+ $ows = $this->optionalWhitespace();
+ $anplusb = new Juxtaposition( [ $ows, $this->cssANplusB(), $ows ] );
+ $this->cache[__METHOD__] = new Alternative( [
+ new Juxtaposition( [
+ $colon,
+ new Alternative( [
+ new KeywordMatcher( [
+ 'link', 'visited', 'hover', 'active', 'focus', 'target', 'enabled', 'disabled', 'checked',
+ 'indeterminate', 'root', 'first-child', 'last-child', 'first-of-type',
+ 'last-of-type', 'only-child', 'only-of-type', 'empty',
+ // CSS2-compat elements with class syntax
+ 'first-line', 'first-letter', 'before', 'after',
+ ] ),
+ new FunctionMatcher( 'lang', new Juxtaposition( [ $ows, $this->ident(), $ows ] ) ),
+ new FunctionMatcher( 'nth-child', $anplusb ),
+ new FunctionMatcher( 'nth-last-child', $anplusb ),
+ new FunctionMatcher( 'nth-of-type', $anplusb ),
+ new FunctionMatcher( 'nth-last-of-type', $anplusb ),
+ ] ),
+ ] ),
+ new Juxtaposition( [
+ $colon,
+ $colon,
+ new KeywordMatcher( [
+ 'first-line', 'first-letter', 'before', 'after', 'selection', 'inactive-selection',
+ 'spelling-error', 'grammar-error', 'placeholder'
+ ] ),
+ ] ),
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * An "AN+B" form
+ *
+ * https://www.w3.org/TR/2014/CR-css-syntax-3-20140220/#anb
+ *
+ * @return Matcher
+ */
+ public function cssANplusB() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ // Quoth the spec:
+ // > The An+B notation was originally defined using a slightly
+ // > different tokenizer than the rest of CSS, resulting in a
+ // > somewhat odd definition when expressed in terms of CSS tokens.
+ // That's a bit of an understatement
+
+ $plus = new DelimMatcher( [ '+' ] );
+ $plusQ = Quantifier::optional( new DelimMatcher( [ '+' ] ) );
+ $n = new KeywordMatcher( [ 'n' ] );
+ $dashN = new KeywordMatcher( [ '-n' ] );
+ $nDash = new KeywordMatcher( [ 'n-' ] );
+ $plusQN = new Juxtaposition( [ $plusQ, $n ] );
+ $plusQNDash = new Juxtaposition( [ $plusQ, $nDash ] );
+ $nDimension = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
+ return $t->typeFlag() === 'integer' && !strcasecmp( $t->unit(), 'n' );
+ } );
+ $nDashDimension = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
+ return $t->typeFlag() === 'integer' && !strcasecmp( $t->unit(), 'n-' );
+ } );
+ $nDashDigitDimension = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
+ return $t->typeFlag() === 'integer' && preg_match( '/^n-\d+$/i', $t->unit() );
+ } );
+ $nDashDigitIdent = new TokenMatcher( Token::T_IDENT, function ( Token $t ) {
+ return preg_match( '/^n-\d+$/i', $t->value() );
+ } );
+ $dashNDashDigitIdent = new TokenMatcher( Token::T_IDENT, function ( Token $t ) {
+ return preg_match( '/^-n-\d+$/i', $t->value() );
+ } );
+ $signedInt = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
+ return $t->typeFlag() === 'integer' && preg_match( '/^[+-]/', $t->representation() );
+ } );
+ $signlessInt = new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
+ return $t->typeFlag() === 'integer' && preg_match( '/^\d/', $t->representation() );
+ } );
+ $plusOrMinus = new DelimMatcher( [ '+', '-' ] );
+ $S = $this->optionalWhitespace();
+
+ $this->cache[__METHOD__] = new Alternative( [
+ new KeywordMatcher( [ 'odd', 'even' ] ),
+ new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
+ return $t->typeFlag() === 'integer';
+ } ),
+ $nDimension,
+ $plusQN,
+ $dashN,
+ $nDashDigitDimension,
+ new Juxtaposition( [ $plusQ, $nDashDigitIdent ] ),
+ $dashNDashDigitIdent,
+ new Juxtaposition( [ $nDimension, $S, $signedInt ] ),
+ new Juxtaposition( [ $plusQN, $S, $signedInt ] ),
+ new Juxtaposition( [ $dashN, $S, $signedInt ] ),
+ new Juxtaposition( [ $nDashDimension, $S, $signlessInt ] ),
+ new Juxtaposition( [ $plusQNDash, $S, $signlessInt ] ),
+ new Juxtaposition( [ new KeywordMatcher( [ '-n-' ] ), $S, $signlessInt ] ),
+ new Juxtaposition( [ $nDimension, $S, $plusOrMinus, $S, $signlessInt ] ),
+ new Juxtaposition( [ $plusQN, $S, $plusOrMinus, $S, $signlessInt ] ),
+ new Juxtaposition( [ $dashN, $S, $plusOrMinus, $S, $signlessInt ] )
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**
+ * A negation
+ *
+ * ':' not( S* [ type_selector | universal | HASH | class | attrib | pseudo ] S* ')'
+ *
+ * @return Matcher
+ */
+ public function cssNegation() {
+ if ( !isset( $this->cache[__METHOD__] ) ) {
+ // A negation is going to be parsed by the parser as a colon
+ // followed by a CSSFunction, so that's what we need to look for
+ // here.
+
+ $this->cache[__METHOD__] = new Juxtaposition( [
+ new TokenMatcher( Token::T_COLON ),
+ new FunctionMatcher( 'not',
+ new Juxtaposition( [
+ $this->optionalWhitespace(),
+ new Alternative( [
+ $this->cssTypeSelector(),
+ $this->cssUniversal(),
+ $this->cssID(),
+ $this->cssClass(),
+ $this->cssAttrib(),
+ $this->cssPseudo(),
+ ] ),
+ $this->optionalWhitespace(),
+ ] )
+ )
+ ] );
+ $this->cache[__METHOD__]->setDefaultOptions( [ 'skip-whitespace' => false ] );
+ }
+ return $this->cache[__METHOD__];
+ }
+
+ /**@}*/
+
+}
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=@{,@} foldmethod=marker
+ */