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