1<?php 2/** 3 * @file 4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0 5 */ 6 7namespace Wikimedia\CSS\Sanitizer; 8 9use Wikimedia\CSS\Grammar\Alternative; 10use Wikimedia\CSS\Grammar\BlockMatcher; 11use Wikimedia\CSS\Grammar\DelimMatcher; 12use Wikimedia\CSS\Grammar\FunctionMatcher; 13use Wikimedia\CSS\Grammar\Juxtaposition; 14use Wikimedia\CSS\Grammar\KeywordMatcher; 15use Wikimedia\CSS\Grammar\Matcher; 16use Wikimedia\CSS\Grammar\MatcherFactory; 17use Wikimedia\CSS\Grammar\Quantifier; 18use Wikimedia\CSS\Grammar\TokenMatcher; 19use Wikimedia\CSS\Grammar\UnorderedGroup; 20use Wikimedia\CSS\Objects\Token; 21 22/** 23 * Sanitizes a Declaration representing a CSS style property 24 * @note This intentionally doesn't support 25 * [cascading variables](https://www.w3.org/TR/css-variables/) since that 26 * seems impossible to securely sanitize. 27 */ 28class StylePropertySanitizer extends PropertySanitizer { 29 30 /** @var mixed[] */ 31 protected $cache = []; 32 33 /** 34 * @param MatcherFactory $matcherFactory Factory for Matchers 35 */ 36 public function __construct( MatcherFactory $matcherFactory ) { 37 parent::__construct( [], $matcherFactory->cssWideKeywords() ); 38 39 $this->addKnownProperties( [ 40 // https://www.w3.org/TR/2016/CR-css-cascade-3-20160519/#all-shorthand 41 'all' => $matcherFactory->cssWideKeywords(), 42 43 // https://www.w3.org/TR/2015/REC-pointerevents-20150224/#the-touch-action-css-property 44 'touch-action' => new Alternative( [ 45 new KeywordMatcher( [ 'auto', 'none', 'manipulation' ] ), 46 UnorderedGroup::someOf( [ 47 new KeywordMatcher( 'pan-x' ), 48 new KeywordMatcher( 'pan-y' ), 49 ] ), 50 ] ), 51 52 // https://www.w3.org/TR/2013/WD-css3-page-20130314/#using-named-pages 53 'page' => $matcherFactory->ident(), 54 ] ); 55 $this->addKnownProperties( $this->css2( $matcherFactory ) ); 56 $this->addKnownProperties( $this->cssDisplay3( $matcherFactory ) ); 57 $this->addKnownProperties( $this->cssPosition3( $matcherFactory ) ); 58 $this->addKnownProperties( $this->cssColor3( $matcherFactory ) ); 59 $this->addKnownProperties( $this->cssBorderBackground3( $matcherFactory ) ); 60 $this->addKnownProperties( $this->cssImages3( $matcherFactory ) ); 61 $this->addKnownProperties( $this->cssFonts3( $matcherFactory ) ); 62 $this->addKnownProperties( $this->cssMulticol( $matcherFactory ) ); 63 $this->addKnownProperties( $this->cssOverflow3( $matcherFactory ) ); 64 $this->addKnownProperties( $this->cssUI4( $matcherFactory ) ); 65 $this->addKnownProperties( $this->cssCompositing1( $matcherFactory ) ); 66 $this->addKnownProperties( $this->cssWritingModes3( $matcherFactory ) ); 67 $this->addKnownProperties( $this->cssTransitions( $matcherFactory ) ); 68 $this->addKnownProperties( $this->cssAnimations( $matcherFactory ) ); 69 $this->addKnownProperties( $this->cssFlexbox3( $matcherFactory ) ); 70 $this->addKnownProperties( $this->cssTransforms1( $matcherFactory ) ); 71 $this->addKnownProperties( $this->cssText3( $matcherFactory ) ); 72 $this->addKnownProperties( $this->cssTextDecor3( $matcherFactory ) ); 73 $this->addKnownProperties( $this->cssAlign3( $matcherFactory ) ); 74 $this->addKnownProperties( $this->cssBreak3( $matcherFactory ) ); 75 $this->addKnownProperties( $this->cssSpeech( $matcherFactory ) ); 76 $this->addKnownProperties( $this->cssGrid1( $matcherFactory ) ); 77 $this->addKnownProperties( $this->cssFilter1( $matcherFactory ) ); 78 $this->addKnownProperties( $this->cssShapes1( $matcherFactory ) ); 79 $this->addKnownProperties( $this->cssMasking1( $matcherFactory ) ); 80 $this->addKnownProperties( $this->cssSizing3( $matcherFactory ) ); 81 } 82 83 /** 84 * Properties from CSS 2.1 85 * @see https://www.w3.org/TR/2011/REC-CSS2-20110607/ 86 * @note Omits properties that have been replaced by a CSS3 module 87 * @param MatcherFactory $matcherFactory Factory for Matchers 88 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 89 */ 90 protected function css2( MatcherFactory $matcherFactory ) { 91 // @codeCoverageIgnoreStart 92 if ( isset( $this->cache[__METHOD__] ) ) { 93 return $this->cache[__METHOD__]; 94 } 95 // @codeCoverageIgnoreEnd 96 97 $props = []; 98 99 $none = new KeywordMatcher( 'none' ); 100 $auto = new KeywordMatcher( 'auto' ); 101 $autoLength = new Alternative( [ $auto, $matcherFactory->length() ] ); 102 $autoLengthPct = new Alternative( [ $auto, $matcherFactory->lengthPercentage() ] ); 103 104 // https://www.w3.org/TR/2011/REC-CSS2-20110607/box.html 105 $props['margin-top'] = $autoLengthPct; 106 $props['margin-bottom'] = $autoLengthPct; 107 $props['margin-left'] = $autoLengthPct; 108 $props['margin-right'] = $autoLengthPct; 109 $props['margin'] = Quantifier::count( $autoLengthPct, 1, 4 ); 110 $props['padding-top'] = $matcherFactory->lengthPercentage(); 111 $props['padding-bottom'] = $matcherFactory->lengthPercentage(); 112 $props['padding-left'] = $matcherFactory->lengthPercentage(); 113 $props['padding-right'] = $matcherFactory->lengthPercentage(); 114 $props['padding'] = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 4 ); 115 116 // https://www.w3.org/TR/2011/REC-CSS2-20110607/visuren.html 117 $props['float'] = new KeywordMatcher( [ 'left', 'right', 'none' ] ); 118 $props['clear'] = new KeywordMatcher( [ 'none', 'left', 'right', 'both' ] ); 119 120 // https://www.w3.org/TR/2011/REC-CSS2-20110607/visudet.html 121 $props['line-height'] = new Alternative( [ 122 new KeywordMatcher( 'normal' ), 123 $matcherFactory->length(), 124 $matcherFactory->numberPercentage(), 125 ] ); 126 $props['vertical-align'] = new Alternative( [ 127 new KeywordMatcher( [ 128 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom' 129 ] ), 130 $matcherFactory->lengthPercentage(), 131 ] ); 132 133 // https://www.w3.org/TR/2011/REC-CSS2-20110607/visufx.html 134 $props['clip'] = new Alternative( [ 135 $auto, new FunctionMatcher( 'rect', Quantifier::hash( $autoLength, 4, 4 ) ), 136 ] ); 137 $props['visibility'] = new KeywordMatcher( [ 'visible', 'hidden', 'collapse' ] ); 138 139 // https://www.w3.org/TR/2011/REC-CSS2-20110607/generate.html 140 $props['list-style-type'] = new KeywordMatcher( [ 141 'disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 142 'lower-greek', 'lower-latin', 'upper-latin', 'armenian', 'georgian', 'lower-alpha', 143 'upper-alpha', 'none' 144 ] ); 145 $props['content'] = new Alternative( [ 146 new KeywordMatcher( [ 'normal', 'none' ] ), 147 Quantifier::plus( new Alternative( [ 148 $matcherFactory->string(), 149 $matcherFactory->image(), // Replaces <url> per https://www.w3.org/TR/css3-images/#placement 150 new FunctionMatcher( 'counter', new Juxtaposition( [ 151 $matcherFactory->ident(), 152 Quantifier::optional( $props['list-style-type'] ), 153 ], true ) ), 154 new FunctionMatcher( 'counters', new Juxtaposition( [ 155 $matcherFactory->ident(), 156 $matcherFactory->string(), 157 Quantifier::optional( $props['list-style-type'] ), 158 ], true ) ), 159 new FunctionMatcher( 'attr', $matcherFactory->ident() ), 160 new KeywordMatcher( [ 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote' ] ), 161 ] ) ) 162 ] ); 163 $props['quotes'] = new Alternative( [ 164 $none, Quantifier::plus( new Juxtaposition( [ 165 $matcherFactory->string(), $matcherFactory->string() 166 ] ) ), 167 ] ); 168 $props['counter-reset'] = new Alternative( [ 169 $none, 170 Quantifier::plus( new Juxtaposition( [ 171 $matcherFactory->ident(), Quantifier::optional( $matcherFactory->integer() ) 172 ] ) ), 173 ] ); 174 $props['counter-increment'] = $props['counter-reset']; 175 $props['list-style-image'] = new Alternative( [ 176 $none, 177 $matcherFactory->image() // Replaces <url> per https://www.w3.org/TR/css3-images/#placement 178 ] ); 179 $props['list-style-position'] = new KeywordMatcher( [ 'inside', 'outside' ] ); 180 $props['list-style'] = UnorderedGroup::someOf( [ 181 $props['list-style-type'], $props['list-style-position'], $props['list-style-image'] 182 ] ); 183 184 // https://www.w3.org/TR/2011/REC-CSS2-20110607/tables.html 185 $props['caption-side'] = new KeywordMatcher( [ 'top', 'bottom' ] ); 186 $props['table-layout'] = new KeywordMatcher( [ 'auto', 'fixed' ] ); 187 $props['border-collapse'] = new KeywordMatcher( [ 'collapse', 'separate' ] ); 188 $props['border-spacing'] = Quantifier::count( $matcherFactory->length(), 1, 2 ); 189 $props['empty-cells'] = new KeywordMatcher( [ 'show', 'hide' ] ); 190 191 $this->cache[__METHOD__] = $props; 192 return $props; 193 } 194 195 /** 196 * Properties for CSS Display Module Level 3 197 * @see https://www.w3.org/TR/2018/WD-css-display-3-20180420/ 198 * @param MatcherFactory $matcherFactory Factory for Matchers 199 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 200 */ 201 protected function cssDisplay3( MatcherFactory $matcherFactory ) { 202 // @codeCoverageIgnoreStart 203 if ( isset( $this->cache[__METHOD__] ) ) { 204 return $this->cache[__METHOD__]; 205 } 206 // @codeCoverageIgnoreEnd 207 208 $props = []; 209 210 $displayOutside = new KeywordMatcher( [ 'block', 'inline', 'run-in' ] ); 211 212 $props['display'] = new Alternative( [ 213 UnorderedGroup::someOf( [ // <display-outside> || <display-inside> 214 $displayOutside, 215 new KeywordMatcher( [ 'flow', 'flow-root', 'table', 'flex', 'grid', 'ruby' ] ), 216 ] ), 217 UnorderedGroup::allOf( [ // <display-listitem> 218 Quantifier::optional( $displayOutside ), 219 Quantifier::optional( new KeywordMatcher( [ 'flow', 'flow-root' ] ) ), 220 new KeywordMatcher( 'list-item' ), 221 ] ), 222 new KeywordMatcher( [ 223 // <display-internal> 224 'table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-cell', 225 'table-column-group', 'table-column', 'table-caption', 'ruby-base', 'ruby-text', 226 'ruby-base-container', 'ruby-text-container', 227 // <display-box> 228 'contents', 'none', 229 // <display-legacy> 230 'inline-block', 'inline-table', 'inline-flex', 'inline-grid', 231 ] ), 232 ] ); 233 234 $this->cache[__METHOD__] = $props; 235 return $props; 236 } 237 238 /** 239 * Properties for CSS Positioned Layout Module Level 3 240 * @see https://www.w3.org/TR/2016/WD-css-position-3-20160517/ 241 * @param MatcherFactory $matcherFactory Factory for Matchers 242 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 243 */ 244 protected function cssPosition3( MatcherFactory $matcherFactory ) { 245 // @codeCoverageIgnoreStart 246 if ( isset( $this->cache[__METHOD__] ) ) { 247 return $this->cache[__METHOD__]; 248 } 249 // @codeCoverageIgnoreEnd 250 251 $auto = new KeywordMatcher( 'auto' ); 252 $autoLengthPct = new Alternative( [ $auto, $matcherFactory->lengthPercentage() ] ); 253 254 $props = []; 255 256 $props['position'] = new KeywordMatcher( [ 257 'static', 'relative', 'absolute', 'sticky', 'fixed' 258 ] ); 259 $props['top'] = $autoLengthPct; 260 $props['right'] = $autoLengthPct; 261 $props['bottom'] = $autoLengthPct; 262 $props['left'] = $autoLengthPct; 263 $props['offset-before'] = $autoLengthPct; 264 $props['offset-after'] = $autoLengthPct; 265 $props['offset-start'] = $autoLengthPct; 266 $props['offset-end'] = $autoLengthPct; 267 $props['z-index'] = new Alternative( [ $auto, $matcherFactory->integer() ] ); 268 269 $this->cache[__METHOD__] = $props; 270 return $props; 271 } 272 273 /** 274 * Properties for CSS Color Module Level 3 275 * @see https://www.w3.org/TR/2018/PR-css-color-3-20180315/ 276 * @param MatcherFactory $matcherFactory Factory for Matchers 277 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 278 */ 279 protected function cssColor3( MatcherFactory $matcherFactory ) { 280 // @codeCoverageIgnoreStart 281 if ( isset( $this->cache[__METHOD__] ) ) { 282 return $this->cache[__METHOD__]; 283 } 284 // @codeCoverageIgnoreEnd 285 286 $props = []; 287 $props['color'] = $matcherFactory->color(); 288 $props['opacity'] = $matcherFactory->number(); 289 290 $this->cache[__METHOD__] = $props; 291 return $props; 292 } 293 294 /** 295 * Data types for backgrounds 296 * @param MatcherFactory $matcherFactory Factory for Matchers 297 * @return array 298 */ 299 protected function backgroundTypes( MatcherFactory $matcherFactory ) { 300 // @codeCoverageIgnoreStart 301 if ( isset( $this->cache[__METHOD__] ) ) { 302 return $this->cache[__METHOD__]; 303 } 304 // @codeCoverageIgnoreEnd 305 306 $types = []; 307 308 $types['bgrepeat'] = new Alternative( [ 309 new KeywordMatcher( [ 'repeat-x', 'repeat-y' ] ), 310 Quantifier::count( new KeywordMatcher( [ 'repeat', 'space', 'round', 'no-repeat' ] ), 1, 2 ), 311 ] ); 312 $types['bgsize'] = new Alternative( [ 313 Quantifier::count( new Alternative( [ 314 $matcherFactory->lengthPercentage(), 315 new KeywordMatcher( 'auto' ) 316 ] ), 1, 2 ), 317 new KeywordMatcher( [ 'cover', 'contain' ] ) 318 ] ); 319 $types['boxKeywords'] = [ 'border-box', 'padding-box', 'content-box' ]; 320 321 $this->cache[__METHOD__] = $types; 322 return $types; 323 } 324 325 /** 326 * Properties for CSS Backgrounds and Borders Module Level 3 327 * @see https://www.w3.org/TR/2017/CR-css-backgrounds-3-20171017/ 328 * @param MatcherFactory $matcherFactory Factory for Matchers 329 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 330 */ 331 protected function cssBorderBackground3( MatcherFactory $matcherFactory ) { 332 // @codeCoverageIgnoreStart 333 if ( isset( $this->cache[__METHOD__] ) ) { 334 return $this->cache[__METHOD__]; 335 } 336 // @codeCoverageIgnoreEnd 337 338 $props = []; 339 340 $types = $this->backgroundTypes( $matcherFactory ); 341 $slash = new DelimMatcher( '/' ); 342 $bgimage = new Alternative( [ new KeywordMatcher( 'none' ), $matcherFactory->image() ] ); 343 $bgrepeat = $types['bgrepeat']; 344 $bgattach = new KeywordMatcher( [ 'scroll', 'fixed', 'local' ] ); 345 $position = $matcherFactory->position(); 346 $box = new KeywordMatcher( $types['boxKeywords'] ); 347 $bgsize = $types['bgsize']; 348 $bglayer = UnorderedGroup::someOf( [ 349 $bgimage, 350 new Juxtaposition( [ 351 $position, Quantifier::optional( new Juxtaposition( [ $slash, $bgsize ] ) ) 352 ] ), 353 $bgrepeat, 354 $bgattach, 355 $box, 356 $box, 357 ] ); 358 $finalBglayer = UnorderedGroup::someOf( [ 359 $matcherFactory->color(), 360 $bgimage, 361 new Juxtaposition( [ 362 $position, Quantifier::optional( new Juxtaposition( [ $slash, $bgsize ] ) ) 363 ] ), 364 $bgrepeat, 365 $bgattach, 366 $box, 367 $box, 368 ] ); 369 370 $props['background-color'] = $matcherFactory->color(); 371 $props['background-image'] = Quantifier::hash( $bgimage ); 372 $props['background-repeat'] = Quantifier::hash( $bgrepeat ); 373 $props['background-attachment'] = Quantifier::hash( $bgattach ); 374 $props['background-position'] = Quantifier::hash( $position ); 375 $props['background-clip'] = Quantifier::hash( $box ); 376 $props['background-origin'] = $props['background-clip']; 377 $props['background-size'] = Quantifier::hash( $bgsize ); 378 $props['background'] = new Juxtaposition( 379 [ Quantifier::hash( $bglayer, 0, INF ), $finalBglayer ], true 380 ); 381 382 $lineStyle = new KeywordMatcher( [ 383 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset' 384 ] ); 385 $lineWidth = new Alternative( [ 386 new KeywordMatcher( [ 'thin', 'medium', 'thick' ] ), $matcherFactory->length(), 387 ] ); 388 $borderCombo = UnorderedGroup::someOf( [ $lineWidth, $lineStyle, $matcherFactory->color() ] ); 389 $radius = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 2 ); 390 $radius4 = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 4 ); 391 392 $props['border-top-color'] = $matcherFactory->color(); 393 $props['border-right-color'] = $matcherFactory->color(); 394 $props['border-bottom-color'] = $matcherFactory->color(); 395 $props['border-left-color'] = $matcherFactory->color(); 396 $props['border-color'] = Quantifier::count( $matcherFactory->color(), 1, 4 ); 397 $props['border-top-style'] = $lineStyle; 398 $props['border-right-style'] = $lineStyle; 399 $props['border-bottom-style'] = $lineStyle; 400 $props['border-left-style'] = $lineStyle; 401 $props['border-style'] = Quantifier::count( $lineStyle, 1, 4 ); 402 $props['border-top-width'] = $lineWidth; 403 $props['border-right-width'] = $lineWidth; 404 $props['border-bottom-width'] = $lineWidth; 405 $props['border-left-width'] = $lineWidth; 406 $props['border-width'] = Quantifier::count( $lineWidth, 1, 4 ); 407 $props['border-top'] = $borderCombo; 408 $props['border-right'] = $borderCombo; 409 $props['border-bottom'] = $borderCombo; 410 $props['border-left'] = $borderCombo; 411 $props['border'] = $borderCombo; 412 $props['border-top-left-radius'] = $radius; 413 $props['border-top-right-radius'] = $radius; 414 $props['border-bottom-left-radius'] = $radius; 415 $props['border-bottom-right-radius'] = $radius; 416 $props['border-radius'] = new Juxtaposition( [ 417 $radius4, Quantifier::optional( new Juxtaposition( [ $slash, $radius4 ] ) ) 418 ] ); 419 $props['border-image-source'] = new Alternative( [ 420 new KeywordMatcher( 'none' ), 421 $matcherFactory->image() 422 ] ); 423 $props['border-image-slice'] = UnorderedGroup::allOf( [ 424 Quantifier::count( $matcherFactory->numberPercentage(), 1, 4 ), 425 Quantifier::optional( new KeywordMatcher( 'fill' ) ), 426 ] ); 427 $props['border-image-width'] = Quantifier::count( new Alternative( [ 428 $matcherFactory->length(), 429 $matcherFactory->percentage(), 430 $matcherFactory->number(), 431 new KeywordMatcher( 'auto' ), 432 ] ), 1, 4 ); 433 $props['border-image-outset'] = Quantifier::count( new Alternative( [ 434 $matcherFactory->length(), 435 $matcherFactory->number(), 436 ] ), 1, 4 ); 437 $props['border-image-repeat'] = Quantifier::count( new KeywordMatcher( [ 438 'stretch', 'repeat', 'round', 'space' 439 ] ), 1, 2 ); 440 $props['border-image'] = UnorderedGroup::someOf( [ 441 $props['border-image-source'], 442 new Juxtaposition( [ 443 $props['border-image-slice'], 444 Quantifier::optional( new Alternative( [ 445 new Juxtaposition( [ $slash, $props['border-image-width'] ] ), 446 new Juxtaposition( [ 447 $slash, 448 Quantifier::optional( $props['border-image-width'] ), 449 $slash, 450 $props['border-image-outset'] 451 ] ) 452 ] ) ) 453 ] ), 454 $props['border-image-repeat'] 455 ] ); 456 457 $props['box-shadow'] = new Alternative( [ 458 new KeywordMatcher( 'none' ), 459 Quantifier::hash( UnorderedGroup::allOf( [ 460 Quantifier::optional( new KeywordMatcher( 'inset' ) ), 461 Quantifier::count( $matcherFactory->length(), 2, 4 ), 462 Quantifier::optional( $matcherFactory->color() ), 463 ] ) ) 464 ] ); 465 466 $this->cache[__METHOD__] = $props; 467 return $props; 468 } 469 470 /** 471 * Properties for CSS Image Values and Replaced Content Module Level 3 472 * @see https://www.w3.org/TR/2012/CR-css3-images-20120417/ 473 * @param MatcherFactory $matcherFactory Factory for Matchers 474 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 475 */ 476 protected function cssImages3( MatcherFactory $matcherFactory ) { 477 // @codeCoverageIgnoreStart 478 if ( isset( $this->cache[__METHOD__] ) ) { 479 return $this->cache[__METHOD__]; 480 } 481 // @codeCoverageIgnoreEnd 482 483 $props = []; 484 485 $props['object-fit'] = new KeywordMatcher( [ 'fill', 'contain', 'cover', 'none', 'scale-down' ] ); 486 $props['object-position'] = $matcherFactory->position(); 487 $props['image-resolution'] = UnorderedGroup::allOf( [ 488 UnorderedGroup::someOf( [ 489 new KeywordMatcher( 'from-image' ), 490 $matcherFactory->resolution(), 491 ] ), 492 Quantifier::optional( new KeywordMatcher( 'snap' ) ) 493 ] ); 494 $props['image-orientation'] = $matcherFactory->angle(); 495 496 $this->cache[__METHOD__] = $props; 497 return $props; 498 } 499 500 /** 501 * Properties for CSS Fonts Module Level 3 502 * @see https://www.w3.org/TR/2018/CR-css-fonts-3-20180315/ 503 * @param MatcherFactory $matcherFactory Factory for Matchers 504 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 505 */ 506 protected function cssFonts3( MatcherFactory $matcherFactory ) { 507 // @codeCoverageIgnoreStart 508 if ( isset( $this->cache[__METHOD__] ) ) { 509 return $this->cache[__METHOD__]; 510 } 511 // @codeCoverageIgnoreEnd 512 513 $css2 = $this->css2( $matcherFactory ); 514 $props = []; 515 516 $matchData = FontFaceAtRuleSanitizer::fontMatchData( $matcherFactory ); 517 518 // Note: <generic-family> is syntactically a subset of <family-name>, 519 // so no point in separately listing it. 520 $props['font-family'] = Quantifier::hash( $matchData['familyName'] ); 521 $props['font-weight'] = new Alternative( [ 522 new KeywordMatcher( [ 'normal', 'bold', 'bolder', 'lighter' ] ), 523 $matchData['numWeight'], 524 ] ); 525 $props['font-stretch'] = $matchData['font-stretch']; 526 $props['font-style'] = $matchData['font-style']; 527 $props['font-size'] = new Alternative( [ 528 new KeywordMatcher( [ 529 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller' 530 ] ), 531 $matcherFactory->lengthPercentage(), 532 ] ); 533 $props['font-size-adjust'] = new Alternative( [ 534 new KeywordMatcher( 'none' ), $matcherFactory->number() 535 ] ); 536 $props['font'] = new Alternative( [ 537 new Juxtaposition( [ 538 Quantifier::optional( UnorderedGroup::someOf( [ 539 $props['font-style'], 540 new KeywordMatcher( [ 'normal', 'small-caps' ] ), 541 $props['font-weight'], 542 $props['font-stretch'], 543 ] ) ), 544 $props['font-size'], 545 Quantifier::optional( new Juxtaposition( [ 546 new DelimMatcher( '/' ), 547 $css2['line-height'], 548 ] ) ), 549 $props['font-family'], 550 ] ), 551 new KeywordMatcher( [ 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar' ] ) 552 ] ); 553 $props['font-synthesis'] = new Alternative( [ 554 new KeywordMatcher( 'none' ), 555 UnorderedGroup::someOf( [ 556 new KeywordMatcher( 'weight' ), 557 new KeywordMatcher( 'style' ), 558 ] ) 559 ] ); 560 $props['font-kerning'] = new KeywordMatcher( [ 'auto', 'normal', 'none' ] ); 561 $props['font-variant-ligatures'] = new Alternative( [ 562 new KeywordMatcher( [ 'normal', 'none' ] ), 563 UnorderedGroup::someOf( $matchData['ligatures'] ) 564 ] ); 565 $props['font-variant-position'] = new KeywordMatcher( 566 array_merge( [ 'normal' ], $matchData['positionKeywords'] ) 567 ); 568 $props['font-variant-caps'] = new KeywordMatcher( 569 array_merge( [ 'normal' ], $matchData['capsKeywords'] ) 570 ); 571 $props['font-variant-numeric'] = new Alternative( [ 572 new KeywordMatcher( 'normal' ), 573 UnorderedGroup::someOf( $matchData['numeric'] ) 574 ] ); 575 $props['font-variant-alternates'] = new Alternative( [ 576 new KeywordMatcher( 'normal' ), 577 UnorderedGroup::someOf( $matchData['alt'] ) 578 ] ); 579 $props['font-variant-east-asian'] = new Alternative( [ 580 new KeywordMatcher( 'normal' ), 581 UnorderedGroup::someOf( $matchData['eastAsian'] ) 582 ] ); 583 $props['font-variant'] = $matchData['font-variant']; 584 $props['font-feature-settings'] = $matchData['font-feature-settings']; 585 586 $this->cache[__METHOD__] = $props; 587 return $props; 588 } 589 590 /** 591 * Properties for CSS Multi-column Layout Module 592 * @see https://www.w3.org/TR/2017/WD-css-multicol-1-20171005/ 593 * @param MatcherFactory $matcherFactory Factory for Matchers 594 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 595 */ 596 protected function cssMulticol( MatcherFactory $matcherFactory ) { 597 // @codeCoverageIgnoreStart 598 if ( isset( $this->cache[__METHOD__] ) ) { 599 return $this->cache[__METHOD__]; 600 } 601 // @codeCoverageIgnoreEnd 602 603 $borders = $this->cssBorderBackground3( $matcherFactory ); 604 $props = []; 605 606 $auto = new KeywordMatcher( 'auto' ); 607 $normal = new KeywordMatcher( 'normal' ); 608 609 $props['column-width'] = new Alternative( array_merge( 610 [ $matcherFactory->length(), $auto ], 611 // Additional values from https://www.w3.org/TR/2018/WD-css-sizing-3-20180304/ 612 $this->getSizingAdditions( $matcherFactory ) 613 ) ); 614 $props['column-count'] = new Alternative( [ $matcherFactory->integer(), $auto ] ); 615 $props['columns'] = UnorderedGroup::someOf( [ $props['column-width'], $props['column-count'] ] ); 616 // Copy these from similar items in the Border module 617 $props['column-rule-color'] = $borders['border-right-color']; 618 $props['column-rule-style'] = $borders['border-right-style']; 619 $props['column-rule-width'] = $borders['border-right-width']; 620 $props['column-rule'] = $borders['border-right']; 621 $props['column-span'] = new KeywordMatcher( [ 'none', 'all' ] ); 622 $props['column-fill'] = new KeywordMatcher( [ 'auto', 'balance', 'balance-all' ] ); 623 624 // Copy these from cssBreak3(), the duplication is allowed as long as 625 // they're the identical Matcher object. 626 $breaks = $this->cssBreak3( $matcherFactory ); 627 $props['break-before'] = $breaks['break-before']; 628 $props['break-after'] = $breaks['break-after']; 629 $props['break-inside'] = $breaks['break-inside']; 630 631 // And one from cssAlign3 632 $props['column-gap'] = $this->cssAlign3( $matcherFactory )['column-gap']; 633 634 $this->cache[__METHOD__] = $props; 635 return $props; 636 } 637 638 /** 639 * Properties for CSS Overflow Module Level 3 640 * @see https://www.w3.org/TR/2016/WD-css-overflow-3-20160531/ 641 * @param MatcherFactory $matcherFactory Factory for Matchers 642 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 643 */ 644 protected function cssOverflow3( MatcherFactory $matcherFactory ) { 645 // @codeCoverageIgnoreStart 646 if ( isset( $this->cache[__METHOD__] ) ) { 647 return $this->cache[__METHOD__]; 648 } 649 // @codeCoverageIgnoreEnd 650 651 $props = []; 652 653 $props['overflow'] = new KeywordMatcher( [ 'visible', 'hidden', 'clip', 'scroll', 'auto' ] ); 654 $props['overflow-x'] = $props['overflow']; 655 $props['overflow-y'] = $props['overflow']; 656 $props['max-lines'] = new Alternative( [ 657 new KeywordMatcher( 'none' ), $matcherFactory->integer() 658 ] ); 659 660 $this->cache[__METHOD__] = $props; 661 return $props; 662 } 663 664 /** 665 * Properties for CSS Basic User Interface Module Level 4 666 * @see https://www.w3.org/TR/2017/PR-css-ui-3-20171214/ 667 * @see https://www.w3.org/TR/2017/WD-css-ui-4-20171222/ 668 * @param MatcherFactory $matcherFactory Factory for Matchers 669 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 670 */ 671 protected function cssUI4( MatcherFactory $matcherFactory ) { 672 // @codeCoverageIgnoreStart 673 if ( isset( $this->cache[__METHOD__] ) ) { 674 return $this->cache[__METHOD__]; 675 } 676 // @codeCoverageIgnoreEnd 677 678 $border = $this->cssBorderBackground3( $matcherFactory ); 679 $props = []; 680 681 $props['box-sizing'] = new KeywordMatcher( [ 'content-box', 'border-box' ] ); 682 // Copy these from similar border properties 683 $props['outline-width'] = $border['border-top-width']; 684 $props['outline-style'] = new Alternative( [ 685 new KeywordMatcher( 'auto' ), $border['border-top-style'] 686 ] ); 687 $props['outline-color'] = new Alternative( [ 688 new KeywordMatcher( 'invert' ), $matcherFactory->color() 689 ] ); 690 $props['outline'] = UnorderedGroup::someOf( [ 691 $props['outline-width'], $props['outline-style'], $props['outline-color'] 692 ] ); 693 $props['outline-offset'] = $matcherFactory->length(); 694 $props['resize'] = new KeywordMatcher( [ 'none', 'both', 'horizontal', 'vertical' ] ); 695 $props['text-overflow'] = Quantifier::count( new Alternative( [ 696 new KeywordMatcher( [ 'clip', 'ellipsis', 'fade' ] ), 697 new FunctionMatcher( 'fade', $matcherFactory->lengthPercentage() ), 698 $matcherFactory->string(), 699 ] ), 1, 2 ); 700 $props['cursor'] = new Juxtaposition( [ 701 Quantifier::star( new Juxtaposition( [ 702 $matcherFactory->image(), 703 Quantifier::optional( new Juxtaposition( [ 704 $matcherFactory->number(), $matcherFactory->number() 705 ] ) ), 706 $matcherFactory->comma(), 707 ] ) ), 708 new KeywordMatcher( [ 709 'auto', 'default', 'none', 'context-menu', 'help', 'pointer', 'progress', 'wait', 'cell', 710 'crosshair', 'text', 'vertical-text', 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab', 711 'grabbing', 'e-resize', 'n-resize', 'ne-resize', 'nw-resize', 's-resize', 'se-resize', 712 'sw-resize', 'w-resize', 'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize', 'col-resize', 713 'row-resize', 'all-scroll', 'zoom-in', 'zoom-out', 714 ] ), 715 ] ); 716 $props['caret-color'] = new Alternative( [ 717 new KeywordMatcher( 'auto' ), $matcherFactory->color() 718 ] ); 719 $props['caret-shape'] = new KeywordMatcher( [ 'auto', 'bar', 'block', 'underscore' ] ); 720 $props['caret'] = UnorderedGroup::someOf( [ $props['caret-color'], $props['caret-shape'] ] ); 721 $props['nav-up'] = new Alternative( [ 722 new KeywordMatcher( 'auto' ), 723 new Juxtaposition( [ 724 $matcherFactory->cssID(), 725 Quantifier::optional( new Alternative( [ 726 new KeywordMatcher( [ 'current', 'root' ] ), 727 $matcherFactory->string(), 728 ] ) ) 729 ] ) 730 ] ); 731 $props['nav-right'] = $props['nav-up']; 732 $props['nav-down'] = $props['nav-up']; 733 $props['nav-left'] = $props['nav-up']; 734 735 $props['user-select'] = new KeywordMatcher( [ 'auto', 'text', 'none', 'contain', 'all' ] ); 736 // Seems potentially useful enough to let the prefixed versions work. 737 $props['-moz-user-select'] = $props['user-select']; 738 $props['-ms-user-select'] = $props['user-select']; 739 $props['-webkit-user-select'] = $props['user-select']; 740 741 $props['appearance'] = new KeywordMatcher( [ 'auto', 'none' ] ); 742 743 $this->cache[__METHOD__] = $props; 744 return $props; 745 } 746 747 /** 748 * Properties for CSS Compositing and Blending Level 1 749 * @see https://www.w3.org/TR/2015/CR-compositing-1-20150113/ 750 * @param MatcherFactory $matcherFactory Factory for Matchers 751 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 752 */ 753 protected function cssCompositing1( MatcherFactory $matcherFactory ) { 754 // @codeCoverageIgnoreStart 755 if ( isset( $this->cache[__METHOD__] ) ) { 756 return $this->cache[__METHOD__]; 757 } 758 // @codeCoverageIgnoreEnd 759 760 $props = []; 761 762 $props['mix-blend-mode'] = new KeywordMatcher( [ 763 'normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 764 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity' 765 ] ); 766 $props['isolation'] = new KeywordMatcher( [ 'auto', 'isolate' ] ); 767 768 // The linked spec incorrectly has this without the hash, despite the 769 // textual description and examples showing it as such. The draft has it fixed. 770 $props['background-blend-mode'] = Quantifier::hash( $props['mix-blend-mode'] ); 771 772 $this->cache[__METHOD__] = $props; 773 return $props; 774 } 775 776 /** 777 * Properties for CSS Writing Modes Level 3 778 * @see https://www.w3.org/TR/2017/CR-css-writing-modes-3-20171207/ 779 * @param MatcherFactory $matcherFactory Factory for Matchers 780 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 781 */ 782 protected function cssWritingModes3( MatcherFactory $matcherFactory ) { 783 // @codeCoverageIgnoreStart 784 if ( isset( $this->cache[__METHOD__] ) ) { 785 return $this->cache[__METHOD__]; 786 } 787 // @codeCoverageIgnoreEnd 788 789 $props = []; 790 791 $props['direction'] = new KeywordMatcher( [ 'ltr', 'rtl' ] ); 792 $props['unicode-bidi'] = new KeywordMatcher( [ 793 'normal', 'embed', 'isolate', 'bidi-override', 'isolate-override', 'plaintext' 794 ] ); 795 $props['writing-mode'] = new KeywordMatcher( [ 796 'horizontal-tb', 'vertical-rl', 'vertical-lr', 797 ] ); 798 $props['text-orientation'] = new KeywordMatcher( [ 'mixed', 'upright', 'sideways' ] ); 799 $props['text-combine-upright'] = new KeywordMatcher( [ 'none', 'all' ] ); 800 801 $this->cache[__METHOD__] = $props; 802 return $props; 803 } 804 805 /** 806 * Properties for CSS Transitions 807 * @see https://www.w3.org/TR/2017/WD-css-transitions-1-20171130/ 808 * @param MatcherFactory $matcherFactory Factory for Matchers 809 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 810 */ 811 protected function cssTransitions( MatcherFactory $matcherFactory ) { 812 // @codeCoverageIgnoreStart 813 if ( isset( $this->cache[__METHOD__] ) ) { 814 return $this->cache[__METHOD__]; 815 } 816 // @codeCoverageIgnoreEnd 817 818 $props = []; 819 $property = new Alternative( [ 820 new KeywordMatcher( [ 'all' ] ), 821 $matcherFactory->customIdent( [ 'none' ] ), 822 ] ); 823 $none = new KeywordMatcher( 'none' ); 824 $singleTimingFunction = $matcherFactory->cssSingleTimingFunction(); 825 826 $props['transition-property'] = new Alternative( [ 827 $none, Quantifier::hash( $property ) 828 ] ); 829 $props['transition-duration'] = Quantifier::hash( $matcherFactory->time() ); 830 $props['transition-timing-function'] = Quantifier::hash( $singleTimingFunction ); 831 $props['transition-delay'] = Quantifier::hash( $matcherFactory->time() ); 832 $props['transition'] = Quantifier::hash( UnorderedGroup::someOf( [ 833 new Alternative( [ $none, $property ] ), 834 $matcherFactory->time(), 835 $singleTimingFunction, 836 $matcherFactory->time(), 837 ] ) ); 838 839 $this->cache[__METHOD__] = $props; 840 return $props; 841 } 842 843 /** 844 * Properties for CSS Animations 845 * @see https://www.w3.org/TR/2017/WD-css-animations-1-20171130/ 846 * @param MatcherFactory $matcherFactory Factory for Matchers 847 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 848 */ 849 protected function cssAnimations( MatcherFactory $matcherFactory ) { 850 // @codeCoverageIgnoreStart 851 if ( isset( $this->cache[__METHOD__] ) ) { 852 return $this->cache[__METHOD__]; 853 } 854 // @codeCoverageIgnoreEnd 855 856 $props = []; 857 $name = new Alternative( [ 858 new KeywordMatcher( [ 'none' ] ), 859 $matcherFactory->customIdent( [ 'none' ] ), 860 $matcherFactory->string(), 861 ] ); 862 $singleTimingFunction = $matcherFactory->cssSingleTimingFunction(); 863 $count = new Alternative( [ 864 new KeywordMatcher( 'infinite' ), 865 $matcherFactory->number() 866 ] ); 867 $direction = new KeywordMatcher( [ 'normal', 'reverse', 'alternate', 'alternate-reverse' ] ); 868 $playState = new KeywordMatcher( [ 'running', 'paused' ] ); 869 $fillMode = new KeywordMatcher( [ 'none', 'forwards', 'backwards', 'both' ] ); 870 871 $props['animation-name'] = Quantifier::hash( $name ); 872 $props['animation-duration'] = Quantifier::hash( $matcherFactory->time() ); 873 $props['animation-timing-function'] = Quantifier::hash( $singleTimingFunction ); 874 $props['animation-iteration-count'] = Quantifier::hash( $count ); 875 $props['animation-direction'] = Quantifier::hash( $direction ); 876 $props['animation-play-state'] = Quantifier::hash( $playState ); 877 $props['animation-delay'] = Quantifier::hash( $matcherFactory->time() ); 878 $props['animation-fill-mode'] = Quantifier::hash( $fillMode ); 879 $props['animation'] = Quantifier::hash( UnorderedGroup::someOf( [ 880 $matcherFactory->time(), 881 $singleTimingFunction, 882 $matcherFactory->time(), 883 $count, 884 $direction, 885 $fillMode, 886 $playState, 887 $name, 888 ] ) ); 889 890 $this->cache[__METHOD__] = $props; 891 return $props; 892 } 893 894 /** 895 * Properties for CSS Flexible Box Layout Module Level 1 896 * @see https://www.w3.org/TR/2017/CR-css-flexbox-1-20171019/ 897 * @param MatcherFactory $matcherFactory Factory for Matchers 898 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 899 */ 900 protected function cssFlexbox3( MatcherFactory $matcherFactory ) { 901 // @codeCoverageIgnoreStart 902 if ( isset( $this->cache[__METHOD__] ) ) { 903 return $this->cache[__METHOD__]; 904 } 905 // @codeCoverageIgnoreEnd 906 907 $props = []; 908 $props['flex-direction'] = new KeywordMatcher( [ 909 'row', 'row-reverse', 'column', 'column-reverse' 910 ] ); 911 $props['flex-wrap'] = new KeywordMatcher( [ 'nowrap', 'wrap', 'wrap-reverse' ] ); 912 $props['flex-flow'] = UnorderedGroup::someOf( [ $props['flex-direction'], $props['flex-wrap'] ] ); 913 $props['order'] = $matcherFactory->integer(); 914 $props['flex-grow'] = $matcherFactory->number(); 915 $props['flex-shrink'] = $matcherFactory->number(); 916 $props['flex-basis'] = new Alternative( [ 917 new KeywordMatcher( [ 'content' ] ), 918 $this->cssSizing3( $matcherFactory )['width'] 919 ] ); 920 $props['flex'] = new Alternative( [ 921 new KeywordMatcher( 'none' ), 922 UnorderedGroup::someOf( [ 923 new Juxtaposition( [ $props['flex-grow'], Quantifier::optional( $props['flex-shrink'] ) ] ), 924 $props['flex-basis'], 925 ] ) 926 ] ); 927 928 // The alignment module supersedes the ones in flexbox. Copying is ok as long as 929 // it's the identical object. 930 $align = $this->cssAlign3( $matcherFactory ); 931 $props['justify-content'] = $align['justify-content']; 932 $props['align-items'] = $align['align-items']; 933 $props['align-self'] = $align['align-self']; 934 $props['align-content'] = $align['align-content']; 935 936 $this->cache[__METHOD__] = $props; 937 return $props; 938 } 939 940 /** 941 * Properties for CSS Transforms Module Level 1 and CSS 3D Transforms Level 3 942 * 943 * Combined because they both define "transform" 944 * 945 * @see https://www.w3.org/TR/2017/WD-css-transforms-1-20171130/ 946 * @see https://www.w3.org/TR/2009/WD-css3-3d-transforms-20090320/ 947 * @param MatcherFactory $matcherFactory Factory for Matchers 948 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 949 */ 950 protected function cssTransforms1( MatcherFactory $matcherFactory ) { 951 // @codeCoverageIgnoreStart 952 if ( isset( $this->cache[__METHOD__] ) ) { 953 return $this->cache[__METHOD__]; 954 } 955 // @codeCoverageIgnoreEnd 956 957 $props = []; 958 $a = $matcherFactory->angle(); 959 $n = $matcherFactory->number(); 960 $lp = $matcherFactory->lengthPercentage(); 961 $ol = Quantifier::optional( $matcherFactory->length() ); 962 $center = new KeywordMatcher( 'center' ); 963 $leftRight = new KeywordMatcher( [ 'left', 'right' ] ); 964 $topBottom = new KeywordMatcher( [ 'top', 'bottom' ] ); 965 966 $props['transform'] = new Alternative( [ 967 new KeywordMatcher( 'none' ), 968 Quantifier::plus( new Alternative( [ 969 // From https://www.w3.org/TR/2017/WD-css-transforms-1-20171130/#transform-functions 970 new FunctionMatcher( 'matrix', Quantifier::hash( $n, 6, 6 ) ), 971 new FunctionMatcher( 'translate', Quantifier::hash( $lp, 1, 2 ) ), 972 new FunctionMatcher( 'translateX', $lp ), 973 new FunctionMatcher( 'translateY', $lp ), 974 new FunctionMatcher( 'scale', Quantifier::hash( $n, 1, 2 ) ), 975 new FunctionMatcher( 'scaleX', $n ), 976 new FunctionMatcher( 'scaleY', $n ), 977 new FunctionMatcher( 'rotate', $a ), 978 new FunctionMatcher( 'skew', Quantifier::hash( $a, 1, 2 ) ), 979 new FunctionMatcher( 'skewX', $a ), 980 new FunctionMatcher( 'skewY', $a ), 981 982 // From https://www.w3.org/TR/2009/WD-css3-3d-transforms-20090320/#transform-functions 983 new FunctionMatcher( 'matrix3d', Quantifier::hash( $n, 16, 16 ) ), 984 new FunctionMatcher( 'translate3d', Quantifier::hash( $lp, 3, 3 ) ), 985 new FunctionMatcher( 'translateZ', $lp ), 986 new FunctionMatcher( 'scale3d', Quantifier::hash( $n, 3, 3 ) ), 987 new FunctionMatcher( 'scaleZ', $n ), 988 new FunctionMatcher( 'rotate3d', new Juxtaposition( [ $n, $n, $n, $a ], true ) ), 989 new FunctionMatcher( 'rotateX', $a ), 990 new FunctionMatcher( 'rotateY', $a ), 991 new FunctionMatcher( 'rotateZ', $a ), 992 new FunctionMatcher( 'perspective', $n ), 993 ] ) ) 994 ] ); 995 996 // From https://www.w3.org/TR/2017/WD-css-transforms-1-20171130/ 997 $props['transform-origin'] = new Alternative( [ 998 new Alternative( [ $center, $leftRight, $topBottom, $lp ] ), 999 new Juxtaposition( [ 1000 new Alternative( [ $center, $leftRight, $lp ] ), 1001 new Alternative( [ $center, $topBottom, $lp ] ), 1002 $ol 1003 ] ), 1004 new Juxtaposition( [ 1005 UnorderedGroup::allOf( [ 1006 new Alternative( [ $center, $leftRight ] ), 1007 new Alternative( [ $center, $topBottom ] ), 1008 ] ), 1009 $ol, 1010 ] ) 1011 ] ); 1012 $props['transform-box'] = new KeywordMatcher( [ 'border-box', 'fill-box', 'view-box' ] ); 1013 1014 // From https://www.w3.org/TR/2009/WD-css3-3d-transforms-20090320/ 1015 $props['transform-style'] = new KeywordMatcher( [ 'flat', 'preserve-3d' ] ); 1016 $props['perspective'] = new Alternative( [ new KeywordMatcher( 'none' ), $n ] ); 1017 $props['perspective-origin'] = new Alternative( [ 1018 new Juxtaposition( [ 1019 new Alternative( [ $center, $leftRight, $lp ] ), 1020 Quantifier::optional( new Alternative( [ $center, $topBottom, $lp ] ) ), 1021 ] ), 1022 UnorderedGroup::someOf( [ 1023 new Alternative( [ $center, $leftRight ] ), 1024 new Alternative( [ $center, $topBottom ] ), 1025 ] ) 1026 ] ); 1027 $props['backface-visibility'] = new KeywordMatcher( [ 'visible', 'hidden' ] ); 1028 1029 $this->cache[__METHOD__] = $props; 1030 return $props; 1031 } 1032 1033 /** 1034 * Properties for CSS Text Module Level 3 1035 * @see https://www.w3.org/TR/2017/WD-css-text-3-20170822/ 1036 * @param MatcherFactory $matcherFactory Factory for Matchers 1037 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1038 */ 1039 protected function cssText3( MatcherFactory $matcherFactory ) { 1040 // @codeCoverageIgnoreStart 1041 if ( isset( $this->cache[__METHOD__] ) ) { 1042 return $this->cache[__METHOD__]; 1043 } 1044 // @codeCoverageIgnoreEnd 1045 1046 $props = []; 1047 1048 $props['text-transform'] = new KeywordMatcher( [ 1049 'none', 'capitalize', 'uppercase', 'lowercase', 'full-width' 1050 ] ); 1051 $props['white-space'] = new KeywordMatcher( [ 1052 'normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line' 1053 ] ); 1054 $props['tab-size'] = new Alternative( [ $matcherFactory->number(), $matcherFactory->length() ] ); 1055 $props['line-break'] = new KeywordMatcher( [ 'auto', 'loose', 'normal', 'strict', 'anywhere' ] ); 1056 $props['word-break'] = new KeywordMatcher( [ 'normal', 'keep-all', 'break-all', 'break-word' ] ); 1057 $props['hyphens'] = new KeywordMatcher( [ 'none', 'manual', 'auto' ] ); 1058 $props['word-wrap'] = new Alternative( [ 1059 new KeywordMatcher( [ 'normal' ] ), 1060 UnorderedGroup::someOf( [ 1061 new KeywordMatcher( [ 'break-word' ] ), 1062 new KeywordMatcher( [ 'break-spaces' ] ), 1063 ] ) 1064 ] ); 1065 $props['overflow-wrap'] = $props['word-wrap']; 1066 $props['text-align'] = new KeywordMatcher( [ 1067 'start', 'end', 'left', 'right', 'center', 'justify', 'match-parent', 'justify-all' 1068 ] ); 1069 $props['text-align-all'] = new KeywordMatcher( [ 1070 'start', 'end', 'left', 'right', 'center', 'justify', 'match-parent' 1071 ] ); 1072 $props['text-align-last'] = new KeywordMatcher( [ 1073 'auto', 'start', 'end', 'left', 'right', 'center', 'justify' 1074 ] ); 1075 $props['text-justify'] = new KeywordMatcher( [ 1076 'auto', 'none', 'inter-word', 'inter-character' 1077 ] ); 1078 $props['word-spacing'] = new Alternative( [ 1079 new KeywordMatcher( 'normal' ), 1080 $matcherFactory->lengthPercentage() 1081 ] ); 1082 $props['letter-spacing'] = new Alternative( [ 1083 new KeywordMatcher( 'normal' ), 1084 $matcherFactory->length() 1085 ] ); 1086 $props['text-indent'] = UnorderedGroup::allOf( [ 1087 $matcherFactory->lengthPercentage(), 1088 Quantifier::optional( new KeywordMatcher( 'hanging' ) ), 1089 Quantifier::optional( new KeywordMatcher( 'each-line' ) ), 1090 ] ); 1091 $props['hanging-punctuation'] = new Alternative( [ 1092 new KeywordMatcher( 'none' ), 1093 UnorderedGroup::someOf( [ 1094 new KeywordMatcher( 'first' ), 1095 new KeywordMatcher( [ 'force-end', 'allow-end' ] ), 1096 new KeywordMatcher( 'last' ), 1097 ] ) 1098 ] ); 1099 1100 $this->cache[__METHOD__] = $props; 1101 return $props; 1102 } 1103 1104 /** 1105 * Properties for CSS ext Decoration Module Level 3 1106 * @see https://www.w3.org/TR/2013/CR-css-text-decor-3-20130801/ 1107 * @param MatcherFactory $matcherFactory Factory for Matchers 1108 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1109 */ 1110 protected function cssTextDecor3( MatcherFactory $matcherFactory ) { 1111 // @codeCoverageIgnoreStart 1112 if ( isset( $this->cache[__METHOD__] ) ) { 1113 return $this->cache[__METHOD__]; 1114 } 1115 // @codeCoverageIgnoreEnd 1116 1117 $props = []; 1118 1119 $props['text-decoration-line'] = new Alternative( [ 1120 new KeywordMatcher( 'none' ), 1121 UnorderedGroup::someOf( [ 1122 new KeywordMatcher( 'underline' ), 1123 new KeywordMatcher( 'overline' ), 1124 new KeywordMatcher( 'line-through' ), 1125 // new KeywordMatcher( 'blink' ), // NOOO!!! 1126 ] ) 1127 ] ); 1128 $props['text-decoration-color'] = $matcherFactory->color(); 1129 $props['text-decoration-style'] = new KeywordMatcher( [ 1130 'solid', 'double', 'dotted', 'dashed', 'wavy' 1131 ] ); 1132 $props['text-decoration'] = UnorderedGroup::someOf( [ 1133 $props['text-decoration-line'], 1134 $props['text-decoration-style'], 1135 $props['text-decoration-color'], 1136 ] ); 1137 $props['text-decoration-skip'] = new Alternative( [ 1138 new KeywordMatcher( 'none' ), 1139 UnorderedGroup::someOf( [ 1140 new KeywordMatcher( 'objects' ), 1141 new KeywordMatcher( 'spaces' ), 1142 new KeywordMatcher( 'ink' ), 1143 new KeywordMatcher( 'edges' ), 1144 new KeywordMatcher( 'box-decoration' ), 1145 ] ) 1146 ] ); 1147 $props['text-underline-position'] = new Alternative( [ 1148 new KeywordMatcher( 'auto' ), 1149 UnorderedGroup::someOf( [ 1150 new KeywordMatcher( 'under' ), 1151 new KeywordMatcher( [ 'left', 'right' ] ), 1152 ] ) 1153 ] ); 1154 $props['text-emphasis-style'] = new Alternative( [ 1155 new KeywordMatcher( 'none' ), 1156 UnorderedGroup::someOf( [ 1157 new KeywordMatcher( [ 'filled', 'open' ] ), 1158 new KeywordMatcher( [ 'dot', 'circle', 'double-circle', 'triangle', 'sesame' ] ) 1159 ] ), 1160 $matcherFactory->string(), 1161 ] ); 1162 $props['text-emphasis-color'] = $matcherFactory->color(); 1163 $props['text-emphasis'] = UnorderedGroup::someOf( [ 1164 $props['text-emphasis-style'], 1165 $props['text-emphasis-color'], 1166 ] ); 1167 $props['text-emphasis-position'] = UnorderedGroup::allOf( [ 1168 new KeywordMatcher( [ 'over', 'under' ] ), 1169 new KeywordMatcher( [ 'right', 'left' ] ), 1170 ] ); 1171 $props['text-shadow'] = new Alternative( [ 1172 new KeywordMatcher( 'none' ), 1173 Quantifier::hash( UnorderedGroup::allOf( [ 1174 Quantifier::count( $matcherFactory->length(), 2, 3 ), 1175 Quantifier::optional( $matcherFactory->color() ), 1176 ] ) ) 1177 ] ); 1178 1179 $this->cache[__METHOD__] = $props; 1180 return $props; 1181 } 1182 1183 /** 1184 * Properties for CSS Box Alignment Module Level 3 1185 * @see https://www.w3.org/TR/2018/WD-css-align-3-20180423/ 1186 * @param MatcherFactory $matcherFactory Factory for Matchers 1187 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1188 */ 1189 protected function cssAlign3( MatcherFactory $matcherFactory ) { 1190 // @codeCoverageIgnoreStart 1191 if ( isset( $this->cache[__METHOD__] ) ) { 1192 return $this->cache[__METHOD__]; 1193 } 1194 // @codeCoverageIgnoreEnd 1195 1196 $props = []; 1197 $normal = new KeywordMatcher( 'normal' ); 1198 $normalStretch = new KeywordMatcher( [ 'normal', 'stretch' ] ); 1199 $autoNormalStretch = new KeywordMatcher( [ 'auto', 'normal', 'stretch' ] ); 1200 $overflowPosition = Quantifier::optional( new KeywordMatcher( [ 'safe', 'unsafe' ] ) ); 1201 $baselinePosition = new Juxtaposition( [ 1202 Quantifier::optional( new KeywordMatcher( [ 'first', 'last' ] ) ), 1203 new KeywordMatcher( 'baseline' ) 1204 ] ); 1205 $contentDistribution = new KeywordMatcher( [ 1206 'space-between', 'space-around', 'space-evenly', 'stretch' 1207 ] ); 1208 $overflowAndSelfPosition = new Juxtaposition( [ 1209 $overflowPosition, 1210 new KeywordMatcher( [ 1211 'center', 'start', 'end', 'self-start', 'self-end', 'flex-start', 'flex-end', 1212 ] ), 1213 ] ); 1214 $overflowAndSelfPositionLR = new Juxtaposition( [ 1215 $overflowPosition, 1216 new KeywordMatcher( [ 1217 'center', 'start', 'end', 'self-start', 'self-end', 'flex-start', 'flex-end', 'left', 'right', 1218 ] ), 1219 ] ); 1220 $overflowAndContentPos = new Juxtaposition( [ 1221 $overflowPosition, 1222 new KeywordMatcher( [ 'center', 'start', 'end', 'flex-start', 'flex-end' ] ), 1223 ] ); 1224 $overflowAndContentPosLR = new Juxtaposition( [ 1225 $overflowPosition, 1226 new KeywordMatcher( [ 'center', 'start', 'end', 'flex-start', 'flex-end', 'left', 'right' ] ), 1227 ] ); 1228 1229 $props['align-content'] = new Alternative( [ 1230 $normal, 1231 $baselinePosition, 1232 $contentDistribution, 1233 $overflowAndContentPos, 1234 ] ); 1235 $props['justify-content'] = new Alternative( [ 1236 $normal, 1237 $contentDistribution, 1238 $overflowAndContentPosLR, 1239 ] ); 1240 $props['place-content'] = new Juxtaposition( [ 1241 $props['align-content'], Quantifier::optional( $props['justify-content'] ) 1242 ] ); 1243 $props['align-self'] = new Alternative( [ 1244 $autoNormalStretch, 1245 $baselinePosition, 1246 $overflowAndSelfPosition, 1247 ] ); 1248 $props['justify-self'] = new Alternative( [ 1249 $autoNormalStretch, 1250 $baselinePosition, 1251 $overflowAndSelfPositionLR, 1252 ] ); 1253 $props['place-self'] = new Juxtaposition( [ 1254 $props['align-self'], Quantifier::optional( $props['justify-self'] ) 1255 ] ); 1256 $props['align-items'] = new Alternative( [ 1257 $normalStretch, 1258 $baselinePosition, 1259 $overflowAndSelfPosition, 1260 ] ); 1261 $props['justify-items'] = new Alternative( [ 1262 $normalStretch, 1263 $baselinePosition, 1264 $overflowAndSelfPositionLR, 1265 new KeywordMatcher( 'legacy' ), 1266 UnorderedGroup::allOf( [ 1267 new KeywordMatcher( 'legacy' ), 1268 new KeywordMatcher( [ 'left', 'right', 'center' ] ), 1269 ] ), 1270 ] ); 1271 $props['place-items'] = new Juxtaposition( [ 1272 $props['align-items'], Quantifier::optional( $props['justify-items'] ) 1273 ] ); 1274 $props['row-gap'] = new Alternative( [ $normal, $matcherFactory->lengthPercentage() ] ); 1275 $props['column-gap'] = $props['row-gap']; 1276 $props['gap'] = new Juxtaposition( [ 1277 $props['row-gap'], Quantifier::optional( $props['column-gap'] ) 1278 ] ); 1279 1280 $this->cache[__METHOD__] = $props; 1281 return $props; 1282 } 1283 1284 /** 1285 * Properties for CSS Fragmentation Module Level 3 1286 * @see https://www.w3.org/TR/2017/CR-css-break-3-20170209/ 1287 * @param MatcherFactory $matcherFactory Factory for Matchers 1288 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1289 */ 1290 protected function cssBreak3( MatcherFactory $matcherFactory ) { 1291 // @codeCoverageIgnoreStart 1292 if ( isset( $this->cache[__METHOD__] ) ) { 1293 return $this->cache[__METHOD__]; 1294 } 1295 // @codeCoverageIgnoreEnd 1296 1297 $props = []; 1298 $props['break-before'] = new KeywordMatcher( [ 1299 'auto', 'avoid', 'avoid-page', 'page', 'left', 'right', 'recto', 'verso', 'avoid-column', 1300 'column', 'avoid-region', 'region' 1301 ] ); 1302 $props['break-after'] = $props['break-before']; 1303 $props['break-inside'] = new KeywordMatcher( [ 1304 'auto', 'avoid', 'avoid-page', 'avoid-column', 'avoid-region' 1305 ] ); 1306 $props['orphans'] = $matcherFactory->integer(); 1307 $props['widows'] = $matcherFactory->integer(); 1308 $props['box-decoration-break'] = new KeywordMatcher( [ 'slice', 'clone' ] ); 1309 $props['page-break-before'] = new KeywordMatcher( [ 1310 'auto', 'always', 'avoid', 'left', 'right' 1311 ] ); 1312 $props['page-break-after'] = $props['page-break-before']; 1313 $props['page-break-inside'] = new KeywordMatcher( [ 'auto', 'avoid' ] ); 1314 1315 $this->cache[__METHOD__] = $props; 1316 return $props; 1317 } 1318 1319 /** 1320 * Properties for CSS Speech Module 1321 * @see https://www.w3.org/TR/2012/CR-css3-speech-20120320/ 1322 * @param MatcherFactory $matcherFactory Factory for Matchers 1323 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1324 */ 1325 protected function cssSpeech( MatcherFactory $matcherFactory ) { 1326 // @codeCoverageIgnoreStart 1327 if ( isset( $this->cache[__METHOD__] ) ) { 1328 return $this->cache[__METHOD__]; 1329 } 1330 // @codeCoverageIgnoreEnd 1331 1332 $props = []; 1333 $decibel = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) { 1334 return !strcasecmp( $t->unit(), 'dB' ); 1335 } ); 1336 1337 $props['voice-volume'] = new Alternative( [ 1338 new KeywordMatcher( 'silent' ), 1339 UnorderedGroup::someOf( [ 1340 new KeywordMatcher( [ 'x-soft', 'soft', 'medium', 'loud', 'x-loud' ] ), 1341 $decibel 1342 ] ), 1343 ] ); 1344 $props['voice-balance'] = new Alternative( [ 1345 $matcherFactory->number(), 1346 new KeywordMatcher( [ 'left', 'center', 'right', 'leftwards', 'rightwards' ] ), 1347 ] ); 1348 $props['speak'] = new KeywordMatcher( [ 'auto', 'none', 'normal' ] ); 1349 $props['speak-as'] = new Alternative( [ 1350 new KeywordMatcher( 'normal' ), 1351 UnorderedGroup::someOf( [ 1352 new KeywordMatcher( 'spell-out' ), 1353 new KeywordMatcher( 'digits' ), 1354 new KeywordMatcher( [ 'literal-punctuation', 'no-punctuation' ] ), 1355 ] ) 1356 ] ); 1357 $props['pause-before'] = new Alternative( [ 1358 $matcherFactory->time(), 1359 new KeywordMatcher( [ 'none', 'x-weak', 'weak', 'medium', 'strong', 'x-strong' ] ), 1360 ] ); 1361 $props['pause-after'] = $props['pause-before']; 1362 $props['pause'] = new Juxtaposition( [ 1363 $props['pause-before'], 1364 Quantifier::optional( $props['pause-after'] ) 1365 ] ); 1366 $props['rest-before'] = $props['pause-before']; 1367 $props['rest-after'] = $props['pause-after']; 1368 $props['rest'] = $props['pause']; 1369 $props['cue-before'] = new Alternative( [ 1370 new Juxtaposition( [ $matcherFactory->url( 'audio' ), Quantifier::optional( $decibel ) ] ), 1371 new KeywordMatcher( 'none' ) 1372 ] ); 1373 $props['cue-after'] = $props['cue-before']; 1374 $props['cue'] = new Juxtaposition( [ 1375 $props['cue-before'], 1376 Quantifier::optional( $props['cue-after'] ) 1377 ] ); 1378 $props['voice-family'] = new Alternative( [ 1379 Quantifier::hash( new Alternative( [ 1380 new Alternative( [ // <name> 1381 $matcherFactory->string(), 1382 Quantifier::plus( $matcherFactory->ident() ), 1383 ] ), 1384 new Juxtaposition( [ // <generic-voice> 1385 Quantifier::optional( new KeywordMatcher( [ 'child', 'young', 'old' ] ) ), 1386 new KeywordMatcher( [ 'male', 'female', 'neutral' ] ), 1387 Quantifier::optional( $matcherFactory->integer() ), 1388 ] ), 1389 ] ) ), 1390 new KeywordMatcher( 'preserve' ) 1391 ] ); 1392 $props['voice-rate'] = UnorderedGroup::someOf( [ 1393 new KeywordMatcher( [ 'normal', 'x-slow', 'slow', 'medium', 'fast', 'x-fast' ] ), 1394 $matcherFactory->percentage() 1395 ] ); 1396 $props['voice-pitch'] = new Alternative( [ 1397 UnorderedGroup::allOf( [ 1398 new KeywordMatcher( 'absolute' ), 1399 $matcherFactory->frequency(), 1400 ] ), 1401 UnorderedGroup::someOf( [ 1402 new KeywordMatcher( [ 'x-low', 'low', 'medium', 'high', 'x-high' ] ), 1403 new Alternative( [ 1404 $matcherFactory->frequency(), 1405 new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) { 1406 return !strcasecmp( $t->unit(), 'st' ); 1407 } ), 1408 $matcherFactory->percentage() 1409 ] ), 1410 ] ), 1411 ] ); 1412 $props['voice-range'] = $props['voice-pitch']; 1413 $props['voice-stress'] = new KeywordMatcher( [ 1414 'normal', 'strong', 'moderate', 'none', 'reduced' 1415 ] ); 1416 $props['voice-duration'] = new Alternative( [ 1417 new KeywordMatcher( 'auto' ), 1418 $matcherFactory->time() 1419 ] ); 1420 1421 $this->cache[__METHOD__] = $props; 1422 return $props; 1423 } 1424 1425 /** 1426 * Properties for CSS Grid Layout Module Level 1 1427 * @see https://www.w3.org/TR/2017/CR-css-grid-1-20171214/ 1428 * @param MatcherFactory $matcherFactory Factory for Matchers 1429 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1430 */ 1431 protected function cssGrid1( MatcherFactory $matcherFactory ) { 1432 // @codeCoverageIgnoreStart 1433 if ( isset( $this->cache[__METHOD__] ) ) { 1434 return $this->cache[__METHOD__]; 1435 } 1436 // @codeCoverageIgnoreEnd 1437 1438 $props = []; 1439 $comma = $matcherFactory->comma(); 1440 $slash = new DelimMatcher( '/' ); 1441 $customIdent = $matcherFactory->customIdent( [ 'span' ] ); 1442 $lineNamesO = Quantifier::optional( new BlockMatcher( 1443 Token::T_LEFT_BRACKET, Quantifier::star( $customIdent ) 1444 ) ); 1445 $trackBreadth = new Alternative( [ 1446 $matcherFactory->lengthPercentage(), 1447 new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) { 1448 return $t->value() >= 0 && !strcasecmp( $t->unit(), 'fr' ); 1449 } ), 1450 new KeywordMatcher( [ 'min-content', 'max-content', 'auto' ] ) 1451 ] ); 1452 $inflexibleBreadth = new Alternative( [ 1453 $matcherFactory->lengthPercentage(), 1454 new KeywordMatcher( [ 'min-content', 'max-content', 'auto' ] ) 1455 ] ); 1456 $fixedBreadth = $matcherFactory->lengthPercentage(); 1457 $trackSize = new Alternative( [ 1458 $trackBreadth, 1459 new FunctionMatcher( 'minmax', 1460 new Juxtaposition( [ $inflexibleBreadth, $trackBreadth ], true ) 1461 ), 1462 new FunctionMatcher( 'fit-content', $matcherFactory->lengthPercentage() ) 1463 ] ); 1464 $fixedSize = new Alternative( [ 1465 $fixedBreadth, 1466 new FunctionMatcher( 'minmax', new Juxtaposition( [ $fixedBreadth, $trackBreadth ], true ) ), 1467 new FunctionMatcher( 'minmax', 1468 new Juxtaposition( [ $inflexibleBreadth, $fixedBreadth ], true ) 1469 ), 1470 ] ); 1471 $trackRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [ 1472 $matcherFactory->integer(), 1473 $comma, 1474 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $trackSize ] ) ), 1475 $lineNamesO 1476 ] ) ); 1477 $autoRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [ 1478 new KeywordMatcher( [ 'auto-fill', 'auto-fit' ] ), 1479 $comma, 1480 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $fixedSize ] ) ), 1481 $lineNamesO 1482 ] ) ); 1483 $fixedRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [ 1484 $matcherFactory->integer(), 1485 $comma, 1486 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $fixedSize ] ) ), 1487 $lineNamesO 1488 ] ) ); 1489 $trackList = new Juxtaposition( [ 1490 Quantifier::plus( new Juxtaposition( [ 1491 $lineNamesO, new Alternative( [ $trackSize, $trackRepeat ] ) 1492 ] ) ), 1493 $lineNamesO 1494 ] ); 1495 $autoTrackList = new Juxtaposition( [ 1496 Quantifier::star( new Juxtaposition( [ 1497 $lineNamesO, new Alternative( [ $fixedSize, $fixedRepeat ] ) 1498 ] ) ), 1499 $lineNamesO, 1500 $autoRepeat, 1501 Quantifier::star( new Juxtaposition( [ 1502 $lineNamesO, new Alternative( [ $fixedSize, $fixedRepeat ] ) 1503 ] ) ), 1504 $lineNamesO, 1505 ] ); 1506 $explicitTrackList = new Juxtaposition( [ 1507 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $trackSize ] ) ), 1508 $lineNamesO 1509 ] ); 1510 $autoDense = UnorderedGroup::allOf( [ 1511 new KeywordMatcher( 'auto-flow' ), 1512 Quantifier::optional( new KeywordMatcher( 'dense' ) ) 1513 ] ); 1514 1515 $props['grid-template-columns'] = new Alternative( [ 1516 new KeywordMatcher( 'none' ), $trackList, $autoTrackList 1517 ] ); 1518 $props['grid-template-rows'] = $props['grid-template-columns']; 1519 $props['grid-template-areas'] = new Alternative( [ 1520 new KeywordMatcher( 'none' ), 1521 Quantifier::plus( $matcherFactory->string() ), 1522 ] ); 1523 $props['grid-template'] = new Alternative( [ 1524 new KeywordMatcher( 'none' ), 1525 new Juxtaposition( [ $props['grid-template-rows'], $slash, $props['grid-template-columns'] ] ), 1526 new Juxtaposition( [ 1527 Quantifier::plus( new Juxtaposition( [ 1528 $lineNamesO, $matcherFactory->string(), Quantifier::optional( $trackSize ), $lineNamesO 1529 ] ) ), 1530 Quantifier::optional( new Juxtaposition( [ $slash, $explicitTrackList ] ) ), 1531 ] ) 1532 ] ); 1533 $props['grid-auto-columns'] = Quantifier::plus( $trackSize ); 1534 $props['grid-auto-rows'] = $props['grid-auto-columns']; 1535 $props['grid-auto-flow'] = UnorderedGroup::someOf( [ 1536 new KeywordMatcher( [ 'row', 'column' ] ), 1537 new KeywordMatcher( 'dense' ) 1538 ] ); 1539 $props['grid'] = new Alternative( [ 1540 $props['grid-template'], 1541 new Juxtaposition( [ 1542 $props['grid-template-rows'], 1543 $slash, 1544 $autoDense, 1545 Quantifier::optional( $props['grid-auto-columns'] ), 1546 ] ), 1547 new Juxtaposition( [ 1548 $autoDense, 1549 Quantifier::optional( $props['grid-auto-rows'] ), 1550 $slash, 1551 $props['grid-template-columns'], 1552 ] ) 1553 ] ); 1554 1555 $gridLine = new Alternative( [ 1556 new KeywordMatcher( 'auto' ), 1557 $customIdent, 1558 UnorderedGroup::allOf( [ 1559 $matcherFactory->integer(), 1560 Quantifier::optional( $customIdent ) 1561 ] ), 1562 UnorderedGroup::allOf( [ 1563 new KeywordMatcher( 'span' ), 1564 UnorderedGroup::someOf( [ 1565 $matcherFactory->integer(), 1566 $customIdent, 1567 ] ) 1568 ] ) 1569 ] ); 1570 $props['grid-row-start'] = $gridLine; 1571 $props['grid-column-start'] = $gridLine; 1572 $props['grid-row-end'] = $gridLine; 1573 $props['grid-column-end'] = $gridLine; 1574 $props['grid-row'] = new Juxtaposition( [ 1575 $gridLine, Quantifier::optional( new Juxtaposition( [ $slash, $gridLine ] ) ) 1576 ] ); 1577 $props['grid-column'] = $props['grid-row']; 1578 $props['grid-area'] = new Juxtaposition( [ 1579 $gridLine, Quantifier::count( new Juxtaposition( [ $slash, $gridLine ] ), 0, 3 ) 1580 ] ); 1581 1582 // Replaced by the alignment module 1583 $align = $this->cssAlign3( $matcherFactory ); 1584 $props['grid-row-gap'] = $align['row-gap']; 1585 $props['grid-column-gap'] = $align['column-gap']; 1586 $props['grid-gap'] = $align['gap']; 1587 1588 // Also these are copied from the alignment module. Copying is ok as long as 1589 // it's the identical object. 1590 $props['row-gap'] = $align['row-gap']; 1591 $props['column-gap'] = $align['column-gap']; 1592 $props['gap'] = $align['gap']; 1593 $props['justify-self'] = $align['justify-self']; 1594 $props['justify-items'] = $align['justify-items']; 1595 $props['align-self'] = $align['align-self']; 1596 $props['align-items'] = $align['align-items']; 1597 $props['justify-content'] = $align['justify-content']; 1598 $props['align-content'] = $align['align-content']; 1599 1600 // Grid uses Flexbox's order property too. Copying is ok as long as 1601 // it's the identical object. 1602 $props['order'] = $this->cssFlexbox3( $matcherFactory )['order']; 1603 1604 $this->cache[__METHOD__] = $props; 1605 return $props; 1606 } 1607 1608 /** 1609 * Properties for CSS Filter Effects Module Level 1 1610 * @see https://www.w3.org/TR/2014/WD-filter-effects-1-20141125/ 1611 * @param MatcherFactory $matcherFactory Factory for Matchers 1612 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1613 */ 1614 protected function cssFilter1( MatcherFactory $matcherFactory ) { 1615 // @codeCoverageIgnoreStart 1616 if ( isset( $this->cache[__METHOD__] ) ) { 1617 return $this->cache[__METHOD__]; 1618 } 1619 // @codeCoverageIgnoreEnd 1620 1621 $props = []; 1622 1623 $props['filter'] = new Alternative( [ 1624 new KeywordMatcher( 'none' ), 1625 Quantifier::plus( new Alternative( [ 1626 new FunctionMatcher( 'blur', $matcherFactory->length() ), 1627 new FunctionMatcher( 'brightness', $matcherFactory->numberPercentage() ), 1628 new FunctionMatcher( 'contrast', $matcherFactory->numberPercentage() ), 1629 new FunctionMatcher( 'drop-shadow', new Juxtaposition( [ 1630 Quantifier::count( $matcherFactory->length(), 2, 3 ), 1631 Quantifier::optional( $matcherFactory->color() ), 1632 ] ) ), 1633 new FunctionMatcher( 'grayscale', $matcherFactory->numberPercentage() ), 1634 new FunctionMatcher( 'hue-rotate', $matcherFactory->angle() ), 1635 new FunctionMatcher( 'invert', $matcherFactory->numberPercentage() ), 1636 new FunctionMatcher( 'opacity', $matcherFactory->numberPercentage() ), 1637 new FunctionMatcher( 'saturate', $matcherFactory->numberPercentage() ), 1638 new FunctionMatcher( 'sepia', $matcherFactory->numberPercentage() ), 1639 $matcherFactory->url( 'svg' ), 1640 ] ) ) 1641 ] ); 1642 $props['flood-color'] = $matcherFactory->color(); 1643 $props['flood-opacity'] = $matcherFactory->numberPercentage(); 1644 $props['color-interpolation-filters'] = new KeywordMatcher( [ 'auto', 'sRGB', 'linearRGB' ] ); 1645 $props['lighting-color'] = $matcherFactory->color(); 1646 1647 $this->cache[__METHOD__] = $props; 1648 return $props; 1649 } 1650 1651 /** 1652 * Shapes and masking share these basic shapes 1653 * @see https://www.w3.org/TR/2014/CR-css-shapes-1-20140320/#basic-shape-functions 1654 * @param MatcherFactory $matcherFactory Factory for Matchers 1655 * @return Matcher 1656 */ 1657 protected function basicShapes( MatcherFactory $matcherFactory ) { 1658 // @codeCoverageIgnoreStart 1659 if ( isset( $this->cache[__METHOD__] ) ) { 1660 return $this->cache[__METHOD__]; 1661 } 1662 // @codeCoverageIgnoreEnd 1663 1664 $border = $this->cssBorderBackground3( $matcherFactory ); 1665 $sa = $matcherFactory->lengthPercentage(); 1666 $sr = new Alternative( [ 1667 $sa, 1668 new KeywordMatcher( [ 'closest-side', 'farthest-side' ] ), 1669 ] ); 1670 1671 $basicShape = new Alternative( [ 1672 new FunctionMatcher( 'inset', new Juxtaposition( [ 1673 Quantifier::count( $sa, 1, 4 ), 1674 Quantifier::optional( new Juxtaposition( [ 1675 new KeywordMatcher( 'round' ), $border['border-radius'] 1676 ] ) ) 1677 ] ) ), 1678 new FunctionMatcher( 'circle', new Juxtaposition( [ 1679 Quantifier::optional( $sr ), 1680 Quantifier::optional( new Juxtaposition( [ 1681 new KeywordMatcher( 'at' ), $matcherFactory->position() 1682 ] ) ) 1683 ] ) ), 1684 new FunctionMatcher( 'ellipse', new Juxtaposition( [ 1685 Quantifier::optional( Quantifier::count( $sr, 2, 2 ) ), 1686 Quantifier::optional( new Juxtaposition( [ 1687 new KeywordMatcher( 'at' ), $matcherFactory->position() 1688 ] ) ) 1689 ] ) ), 1690 new FunctionMatcher( 'polygon', new Juxtaposition( [ 1691 Quantifier::optional( new KeywordMatcher( [ 'nonzero', 'evenodd' ] ) ), 1692 Quantifier::hash( Quantifier::count( $sa, 2, 2 ) ), 1693 ], true ) ), 1694 ] ); 1695 1696 $this->cache[__METHOD__] = $basicShape; 1697 return $basicShape; 1698 } 1699 1700 /** 1701 * Properties for CSS Shapes Module Level 1 1702 * @see https://www.w3.org/TR/2014/CR-css-shapes-1-20140320/ 1703 * @param MatcherFactory $matcherFactory Factory for Matchers 1704 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1705 */ 1706 protected function cssShapes1( MatcherFactory $matcherFactory ) { 1707 // @codeCoverageIgnoreStart 1708 if ( isset( $this->cache[__METHOD__] ) ) { 1709 return $this->cache[__METHOD__]; 1710 } 1711 // @codeCoverageIgnoreEnd 1712 1713 $shapeBoxKW = $this->backgroundTypes( $matcherFactory )['boxKeywords']; 1714 $shapeBoxKW[] = 'margin-box'; 1715 1716 $props = []; 1717 1718 $props['shape-outside'] = new Alternative( [ 1719 new KeywordMatcher( 'none' ), 1720 UnorderedGroup::someOf( [ 1721 $this->basicShapes( $matcherFactory ), 1722 new KeywordMatcher( $shapeBoxKW ), 1723 ] ), 1724 $matcherFactory->url( 'image' ), 1725 ] ); 1726 $props['shape-image-threshold'] = $matcherFactory->number(); 1727 $props['shape-margin'] = $matcherFactory->lengthPercentage(); 1728 1729 $this->cache[__METHOD__] = $props; 1730 return $props; 1731 } 1732 1733 /** 1734 * Properties for CSS Masking Module Level 1 1735 * @see https://www.w3.org/TR/2014/CR-css-masking-1-20140826/ 1736 * @param MatcherFactory $matcherFactory Factory for Matchers 1737 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1738 */ 1739 protected function cssMasking1( MatcherFactory $matcherFactory ) { 1740 // @codeCoverageIgnoreStart 1741 if ( isset( $this->cache[__METHOD__] ) ) { 1742 return $this->cache[__METHOD__]; 1743 } 1744 // @codeCoverageIgnoreEnd 1745 1746 $slash = new DelimMatcher( '/' ); 1747 $bgtypes = $this->backgroundTypes( $matcherFactory ); 1748 $bg = $this->cssBorderBackground3( $matcherFactory ); 1749 $geometryBoxKeywords = array_merge( $bgtypes['boxKeywords'], [ 1750 'margin-box', 'fill-box', 'stroke-box', 'view-box' 1751 ] ); 1752 $geometryBox = new KeywordMatcher( $geometryBoxKeywords ); 1753 $maskRef = new Alternative( [ 1754 new KeywordMatcher( 'none' ), 1755 $matcherFactory->image(), 1756 $matcherFactory->url( 'svg' ), 1757 ] ); 1758 $maskMode = new KeywordMatcher( [ 'alpha', 'luminance', 'auto' ] ); 1759 $maskClip = new KeywordMatcher( array_merge( $geometryBoxKeywords, [ 'no-clip' ] ) ); 1760 $maskComposite = new KeywordMatcher( [ 'add', 'subtract', 'intersect', 'exclude' ] ); 1761 1762 $props = []; 1763 1764 $props['clip-path'] = new Alternative( [ 1765 $matcherFactory->url( 'svg' ), 1766 UnorderedGroup::someOf( [ 1767 $this->basicShapes( $matcherFactory ), 1768 $geometryBox, 1769 ] ), 1770 new KeywordMatcher( 'none' ), 1771 ] ); 1772 $props['clip-rule'] = new KeywordMatcher( [ 'nonzero', 'evenodd' ] ); 1773 $props['mask-image'] = Quantifier::hash( $maskRef ); 1774 $props['mask-mode'] = Quantifier::hash( $maskMode ); 1775 $props['mask-repeat'] = $bg['background-repeat']; 1776 $props['mask-position'] = Quantifier::hash( $matcherFactory->position() ); 1777 $props['mask-clip'] = Quantifier::hash( $maskClip ); 1778 $props['mask-origin'] = Quantifier::hash( $geometryBox ); 1779 $props['mask-size'] = $bg['background-size']; 1780 $props['mask-composite'] = Quantifier::hash( $maskComposite ); 1781 $props['mask'] = Quantifier::hash( UnorderedGroup::someOf( [ 1782 new Juxtaposition( [ $maskRef, Quantifier::optional( $maskMode ) ] ), 1783 new Juxtaposition( [ 1784 $matcherFactory->position(), 1785 Quantifier::optional( new Juxtaposition( [ $slash, $bgtypes['bgsize'] ] ) ), 1786 ] ), 1787 $bgtypes['bgrepeat'], 1788 $geometryBox, 1789 $maskClip, 1790 $maskComposite, 1791 ] ) ); 1792 $props['mask-border-source'] = new Alternative( [ 1793 new KeywordMatcher( 'none' ), 1794 $matcherFactory->image(), 1795 ] ); 1796 $props['mask-border-mode'] = new KeywordMatcher( [ 'luminance', 'alpha' ] ); 1797 $props['mask-border-slice'] = new Juxtaposition( [ // Different from border-image-slice, sigh 1798 Quantifier::count( $matcherFactory->numberPercentage(), 1, 4 ), 1799 Quantifier::optional( new KeywordMatcher( 'fill' ) ), 1800 ] ); 1801 $props['mask-border-width'] = $bg['border-image-width']; 1802 $props['mask-border-outset'] = $bg['border-image-outset']; 1803 $props['mask-border-repeat'] = $bg['border-image-repeat']; 1804 $props['mask-border'] = UnorderedGroup::someOf( [ 1805 $props['mask-border-source'], 1806 new Juxtaposition( [ 1807 $props['mask-border-slice'], 1808 Quantifier::optional( new Juxtaposition( [ 1809 $slash, 1810 Quantifier::optional( $props['mask-border-width'] ), 1811 Quantifier::optional( new Juxtaposition( [ 1812 $slash, 1813 $props['mask-border-outset'], 1814 ] ) ), 1815 ] ) ), 1816 ] ), 1817 $props['mask-border-repeat'], 1818 $props['mask-border-mode'], 1819 ] ); 1820 $props['mask-type'] = new KeywordMatcher( [ 'luminance', 'alpha' ] ); 1821 1822 $this->cache[__METHOD__] = $props; 1823 return $props; 1824 } 1825 1826 /** 1827 * Additional keywords and functions from CSS Intrinsic and Extrinsic Sizing Level 3 1828 * @see https://www.w3.org/TR/2018/WD-css-sizing-3-20180304/ 1829 * @param MatcherFactory $matcherFactory Factory for Matchers 1830 * @return Matcher[] Array of matchers 1831 */ 1832 protected function getSizingAdditions( MatcherFactory $matcherFactory ) { 1833 if ( !isset( $this->cache[__METHOD__] ) ) { 1834 $lengthPct = $matcherFactory->lengthPercentage(); 1835 $this->cache[__METHOD__] = [ 1836 new KeywordMatcher( [ 1837 'max-content', 'min-content', 1838 // Browser-prefixed versions of the keywords, needed by Firefox and iOS Safari as of May 2018 1839 '-moz-max-content', '-moz-min-content', 1840 '-webkit-max-content', '-webkit-min-content', 1841 ] ), 1842 new FunctionMatcher( 'fit-content', $lengthPct ), 1843 // Browser-prefixed versions of the function, needed by Firefox and iOS Safari as of May 2018 1844 new FunctionMatcher( '-moz-fit-content', $lengthPct ), 1845 new FunctionMatcher( '-webkit-fit-content', $lengthPct ), 1846 ]; 1847 } 1848 return $this->cache[__METHOD__]; 1849 } 1850 1851 /** 1852 * Properties for CSS Intrinsic and Extrinsic Sizing Level 3 1853 * @see https://www.w3.org/TR/2018/WD-css-sizing-3-20180304/ 1854 * @param MatcherFactory $matcherFactory Factory for Matchers 1855 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values 1856 */ 1857 protected function cssSizing3( MatcherFactory $matcherFactory ) { 1858 // @codeCoverageIgnoreStart 1859 if ( isset( $this->cache[__METHOD__] ) ) { 1860 return $this->cache[__METHOD__]; 1861 } 1862 // @codeCoverageIgnoreEnd 1863 1864 $none = new KeywordMatcher( 'none' ); 1865 $auto = new KeywordMatcher( 'auto' ); 1866 $lengthPct = $matcherFactory->lengthPercentage(); 1867 $sizingValues = array_merge( [ $lengthPct ], $this->getSizingAdditions( $matcherFactory ) ); 1868 1869 $props = []; 1870 $props['width'] = new Alternative( array_merge( [ $auto ], $sizingValues ) ); 1871 $props['min-width'] = $props['width']; 1872 $props['max-width'] = new Alternative( array_merge( [ $none ], $sizingValues ) ); 1873 $props['height'] = $props['width']; 1874 $props['min-height'] = $props['min-width']; 1875 $props['max-height'] = $props['max-width']; 1876 1877 // Copying is ok as long as it's the identical object. 1878 $props['box-sizing'] = $this->cssUI4( $matcherFactory )['box-sizing']; 1879 1880 $this->cache[__METHOD__] = $props; 1881 return $props; 1882 } 1883} 1884