4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
7 namespace Wikimedia\CSS\Sanitizer;
9 use Wikimedia\CSS\Grammar\Alternative;
10 use Wikimedia\CSS\Grammar\BlockMatcher;
11 use Wikimedia\CSS\Grammar\DelimMatcher;
12 use Wikimedia\CSS\Grammar\FunctionMatcher;
13 use Wikimedia\CSS\Grammar\Juxtaposition;
14 use Wikimedia\CSS\Grammar\KeywordMatcher;
15 use Wikimedia\CSS\Grammar\Matcher;
16 use Wikimedia\CSS\Grammar\MatcherFactory;
17 use Wikimedia\CSS\Grammar\Quantifier;
18 use Wikimedia\CSS\Grammar\TokenMatcher;
19 use Wikimedia\CSS\Grammar\UnorderedGroup;
20 use Wikimedia\CSS\Objects\Token;
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.
28 class StylePropertySanitizer extends PropertySanitizer {
30 /** @var Matcher[][] */
31 protected $cache = [];
34 * @param MatcherFactory $matcherFactory Factory for Matchers
36 public function __construct( MatcherFactory $matcherFactory ) {
37 parent::__construct( [], $matcherFactory->cssWideKeywords() );
39 $this->addKnownProperties( [
40 // https://www.w3.org/TR/2016/CR-css-cascade-3-20160519/#all-shorthand
41 'all' => $matcherFactory->cssWideKeywords(),
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' ),
52 // https://www.w3.org/TR/2013/WD-css3-page-20130314/#using-named-pages
53 'page' => $matcherFactory->ident(),
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 ) );
83 * Properties from CSS 2.1
84 * @see https://www.w3.org/TR/2011/REC-CSS2-20110607/
85 * @note Omits properties that have been replaced by a CSS3 module
86 * @param MatcherFactory $matcherFactory Factory for Matchers
87 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
89 protected function css2( MatcherFactory $matcherFactory ) {
90 // @codeCoverageIgnoreStart
91 if ( isset( $this->cache[__METHOD__] ) ) {
92 return $this->cache[__METHOD__];
94 // @codeCoverageIgnoreEnd
98 $none = new KeywordMatcher( 'none' );
99 $auto = new KeywordMatcher( 'auto' );
100 $autoLength = new Alternative( [ $auto, $matcherFactory->length() ] );
101 $autoLengthPct = new Alternative( [ $auto, $matcherFactory->lengthPercentage() ] );
103 // https://www.w3.org/TR/2011/REC-CSS2-20110607/box.html
104 $props['margin-top'] = $autoLengthPct;
105 $props['margin-bottom'] = $autoLengthPct;
106 $props['margin-left'] = $autoLengthPct;
107 $props['margin-right'] = $autoLengthPct;
108 $props['margin'] = Quantifier::count( $autoLengthPct, 1, 4 );
109 $props['padding-top'] = $matcherFactory->lengthPercentage();
110 $props['padding-bottom'] = $matcherFactory->lengthPercentage();
111 $props['padding-left'] = $matcherFactory->lengthPercentage();
112 $props['padding-right'] = $matcherFactory->lengthPercentage();
113 $props['padding'] = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 4 );
115 // https://www.w3.org/TR/2011/REC-CSS2-20110607/visuren.html
116 $props['float'] = new KeywordMatcher( [ 'left', 'right', 'none' ] );
117 $props['clear'] = new KeywordMatcher( [ 'none', 'left', 'right', 'both' ] );
119 // https://www.w3.org/TR/2011/REC-CSS2-20110607/visudet.html
120 $props['width'] = $autoLengthPct;
121 $props['min-width'] = $matcherFactory->lengthPercentage();
122 $props['max-width'] = new Alternative( [ $none, $matcherFactory->lengthPercentage() ] );
123 $props['height'] = $autoLengthPct;
124 $props['min-height'] = $matcherFactory->lengthPercentage();
125 $props['max-height'] = $props['max-width'];
126 $props['line-height'] = new Alternative( [
127 new KeywordMatcher( 'normal' ),
128 $matcherFactory->length(),
129 $matcherFactory->numberPercentage(),
131 $props['vertical-align'] = new Alternative( [
132 new KeywordMatcher( [
133 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom'
135 $matcherFactory->lengthPercentage(),
138 // https://www.w3.org/TR/2011/REC-CSS2-20110607/visufx.html
139 $props['clip'] = new Alternative( [
140 $auto, new FunctionMatcher( 'rect', Quantifier::hash( $autoLength, 4, 4 ) ),
142 $props['visibility'] = new KeywordMatcher( [ 'visible', 'hidden', 'collapse' ] );
144 // https://www.w3.org/TR/2011/REC-CSS2-20110607/generate.html
145 $props['list-style-type'] = new KeywordMatcher( [
146 'disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman',
147 'lower-greek', 'lower-latin', 'upper-latin', 'armenian', 'georgian', 'lower-alpha',
148 'upper-alpha', 'none'
150 $props['content'] = new Alternative( [
151 new KeywordMatcher( [ 'normal', 'none' ] ),
152 Quantifier::plus( new Alternative( [
153 $matcherFactory->string(),
154 $matcherFactory->image(), // Replaces <url> per https://www.w3.org/TR/css3-images/#placement
155 new FunctionMatcher( 'counter', new Juxtaposition( [
156 $matcherFactory->ident(),
157 Quantifier::optional( $props['list-style-type'] ),
159 new FunctionMatcher( 'counters', new Juxtaposition( [
160 $matcherFactory->ident(),
161 $matcherFactory->string(),
162 Quantifier::optional( $props['list-style-type'] ),
164 new FunctionMatcher( 'attr', $matcherFactory->ident() ),
165 new KeywordMatcher( [ 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote' ] ),
168 $props['quotes'] = new Alternative( [
169 $none, Quantifier::plus( new Juxtaposition( [
170 $matcherFactory->string(), $matcherFactory->string()
173 $props['counter-reset'] = new Alternative( [
175 Quantifier::plus( new Juxtaposition( [
176 $matcherFactory->ident(), Quantifier::optional( $matcherFactory->integer() )
179 $props['counter-increment'] = $props['counter-reset'];
180 $props['list-style-image'] = new Alternative( [
182 $matcherFactory->image() // Replaces <url> per https://www.w3.org/TR/css3-images/#placement
184 $props['list-style-position'] = new KeywordMatcher( [ 'inside', 'outside' ] );
185 $props['list-style'] = UnorderedGroup::someOf( [
186 $props['list-style-type'], $props['list-style-position'], $props['list-style-image']
189 // https://www.w3.org/TR/2011/REC-CSS2-20110607/tables.html
190 $props['caption-side'] = new KeywordMatcher( [ 'top', 'bottom' ] );
191 $props['table-layout'] = new KeywordMatcher( [ 'auto', 'fixed' ] );
192 $props['border-collapse'] = new KeywordMatcher( [ 'collapse', 'separate' ] );
193 $props['border-spacing'] = Quantifier::count( $matcherFactory->length(), 1, 2 );
194 $props['empty-cells'] = new KeywordMatcher( [ 'show', 'hide' ] );
196 $this->cache[__METHOD__] = $props;
201 * Properties for CSS Display Module Level 3
202 * @see https://www.w3.org/TR/2017/WD-css-display-3-20170126/
203 * @param MatcherFactory $matcherFactory Factory for Matchers
204 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
206 protected function cssDisplay3( MatcherFactory $matcherFactory ) {
207 // @codeCoverageIgnoreStart
208 if ( isset( $this->cache[__METHOD__] ) ) {
209 return $this->cache[__METHOD__];
211 // @codeCoverageIgnoreEnd
215 $props['display'] = new Alternative( [
216 UnorderedGroup::someOf( [ // <display-outside> || <display-inside>
217 new KeywordMatcher( [ 'block', 'inline', 'run-in' ] ),
218 new KeywordMatcher( [ 'flow', 'flow-root', 'table', 'flex', 'grid', 'ruby' ] ),
220 UnorderedGroup::allOf( [ // <display-listitem>
221 new KeywordMatcher( 'list-item' ),
222 Quantifier::optional( new KeywordMatcher( [ 'block', 'inline', 'run-in' ] ) ),
223 Quantifier::optional( new KeywordMatcher( [ 'flow', 'flow-root' ] ) ),
225 new KeywordMatcher( [
226 // <display-internal>
227 'table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-cell',
228 'table-column-group', 'table-column', 'table-caption', 'ruby-base', 'ruby-text',
229 'ruby-base-container', 'ruby-text-container',
233 'inline-block', 'inline-list-item', 'inline-table', 'inline-flex', 'inline-grid',
234 // https://www.w3.org/TR/2017/CR-css-grid-1-20170209/
239 $this->cache[__METHOD__] = $props;
244 * Properties for CSS Positioned Layout Module Level 3
245 * @see https://www.w3.org/TR/2016/WD-css-position-3-20160517/
246 * @param MatcherFactory $matcherFactory Factory for Matchers
247 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
249 protected function cssPosition3( MatcherFactory $matcherFactory ) {
250 // @codeCoverageIgnoreStart
251 if ( isset( $this->cache[__METHOD__] ) ) {
252 return $this->cache[__METHOD__];
254 // @codeCoverageIgnoreEnd
256 $auto = new KeywordMatcher( 'auto' );
257 $autoLengthPct = new Alternative( [ $auto, $matcherFactory->lengthPercentage() ] );
261 $props['position'] = new KeywordMatcher( [
262 'static', 'relative', 'absolute', 'sticky', 'fixed'
264 $props['top'] = $autoLengthPct;
265 $props['right'] = $autoLengthPct;
266 $props['bottom'] = $autoLengthPct;
267 $props['left'] = $autoLengthPct;
268 $props['offset-before'] = $autoLengthPct;
269 $props['offset-after'] = $autoLengthPct;
270 $props['offset-start'] = $autoLengthPct;
271 $props['offset-end'] = $autoLengthPct;
272 $props['z-index'] = new Alternative( [ $auto, $matcherFactory->integer() ] );
274 $this->cache[__METHOD__] = $props;
279 * Properties for CSS Color Module Level 3
280 * @see https://www.w3.org/TR/2011/REC-css3-color-20110607/
281 * @param MatcherFactory $matcherFactory Factory for Matchers
282 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
284 protected function cssColor3( MatcherFactory $matcherFactory ) {
285 // @codeCoverageIgnoreStart
286 if ( isset( $this->cache[__METHOD__] ) ) {
287 return $this->cache[__METHOD__];
289 // @codeCoverageIgnoreEnd
292 $props['color'] = $matcherFactory->color();
293 $props['opacity'] = $matcherFactory->number();
295 $this->cache[__METHOD__] = $props;
300 * Data types for backgrounds
301 * @param MatcherFactory $matcherFactory Factory for Matchers
304 protected function backgroundTypes( MatcherFactory $matcherFactory ) {
305 // @codeCoverageIgnoreStart
306 if ( isset( $this->cache[__METHOD__] ) ) {
307 return $this->cache[__METHOD__];
309 // @codeCoverageIgnoreEnd
313 $types['bgrepeat'] = new Alternative( [
314 new KeywordMatcher( [ 'repeat-x', 'repeat-y' ] ),
315 Quantifier::count( new KeywordMatcher( [ 'repeat', 'space', 'round', 'no-repeat' ] ), 1, 2 ),
317 $types['bgsize'] = new Alternative( [
318 Quantifier::count( new Alternative( [
319 $matcherFactory->lengthPercentage(),
320 new KeywordMatcher( 'auto' )
322 new KeywordMatcher( [ 'cover', 'contain' ] )
324 $types['boxKeywords'] = [ 'border-box', 'padding-box', 'content-box' ];
326 $this->cache[__METHOD__] = $types;
331 * Properties for CSS Backgrounds and Borders Module Level 3
332 * @see https://www.w3.org/TR/2014/CR-css3-background-20140909/
333 * @param MatcherFactory $matcherFactory Factory for Matchers
334 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
336 protected function cssBorderBackground3( MatcherFactory $matcherFactory ) {
337 // @codeCoverageIgnoreStart
338 if ( isset( $this->cache[__METHOD__] ) ) {
339 return $this->cache[__METHOD__];
341 // @codeCoverageIgnoreEnd
345 $types = $this->backgroundTypes( $matcherFactory );
346 $slash = new DelimMatcher( '/' );
347 $bgimage = new Alternative( [ new KeywordMatcher( 'none' ), $matcherFactory->image() ] );
348 $bgrepeat = $types['bgrepeat'];
349 $bgattach = new KeywordMatcher( [ 'scroll', 'fixed', 'local' ] );
350 $position = $matcherFactory->position();
351 $box = new KeywordMatcher( $types['boxKeywords'] );
352 $bgsize = $types['bgsize'];
353 $bglayer = UnorderedGroup::someOf( [
356 $position, Quantifier::optional( new Juxtaposition( [ $slash, $bgsize ] ) )
363 $finalBglayer = UnorderedGroup::someOf( [
366 $position, Quantifier::optional( new Juxtaposition( [ $slash, $bgsize ] ) )
372 $matcherFactory->color(),
375 $props['background-color'] = $matcherFactory->color();
376 $props['background-image'] = Quantifier::hash( $bgimage );
377 $props['background-repeat'] = Quantifier::hash( $bgrepeat );
378 $props['background-attachment'] = Quantifier::hash( $bgattach );
379 $props['background-position'] = Quantifier::hash( $position );
380 $props['background-clip'] = Quantifier::hash( $box );
381 $props['background-origin'] = $props['background-clip'];
382 $props['background-size'] = Quantifier::hash( $bgsize );
383 $props['background'] = new Juxtaposition(
384 [ Quantifier::hash( $bglayer, 0, INF ), $finalBglayer ], true
387 $lineStyle = new KeywordMatcher( [
388 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'
390 $lineWidth = new Alternative( [
391 new KeywordMatcher( [ 'thin', 'medium', 'thick' ] ), $matcherFactory->length(),
393 $borderCombo = UnorderedGroup::someOf( [ $lineWidth, $lineStyle, $matcherFactory->color() ] );
394 $radius = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 2 );
395 $radius4 = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 4 );
397 $props['border-top-color'] = $matcherFactory->color();
398 $props['border-right-color'] = $matcherFactory->color();
399 $props['border-bottom-color'] = $matcherFactory->color();
400 $props['border-left-color'] = $matcherFactory->color();
401 $props['border-color'] = Quantifier::count( $matcherFactory->color(), 1, 4 );
402 $props['border-top-style'] = $lineStyle;
403 $props['border-right-style'] = $lineStyle;
404 $props['border-bottom-style'] = $lineStyle;
405 $props['border-left-style'] = $lineStyle;
406 $props['border-style'] = Quantifier::count( $lineStyle, 1, 4 );
407 $props['border-top-width'] = $lineWidth;
408 $props['border-right-width'] = $lineWidth;
409 $props['border-bottom-width'] = $lineWidth;
410 $props['border-left-width'] = $lineWidth;
411 $props['border-width'] = Quantifier::count( $lineWidth, 1, 4 );
412 $props['border-top'] = $borderCombo;
413 $props['border-right'] = $borderCombo;
414 $props['border-bottom'] = $borderCombo;
415 $props['border-left'] = $borderCombo;
416 $props['border'] = $borderCombo;
417 $props['border-top-left-radius'] = $radius;
418 $props['border-top-right-radius'] = $radius;
419 $props['border-bottom-left-radius'] = $radius;
420 $props['border-bottom-right-radius'] = $radius;
421 $props['border-radius'] = new Juxtaposition( [
422 $radius4, Quantifier::optional( new Juxtaposition( [ $slash, $radius4 ] ) )
424 $props['border-image-source'] = new Alternative( [
425 new KeywordMatcher( 'none' ),
426 $matcherFactory->image()
428 $props['border-image-slice'] = UnorderedGroup::allOf( [
429 Quantifier::count( $matcherFactory->numberPercentage(), 1, 4 ),
430 Quantifier::optional( new KeywordMatcher( 'fill' ) ),
432 $props['border-image-width'] = Quantifier::count( new Alternative( [
433 $matcherFactory->length(),
434 $matcherFactory->percentage(),
435 $matcherFactory->number(),
436 new KeywordMatcher( 'auto' ),
438 $props['border-image-outset'] = Quantifier::count( new Alternative( [
439 $matcherFactory->length(),
440 $matcherFactory->number(),
442 $props['border-image-repeat'] = Quantifier::count( new KeywordMatcher( [
443 'stretch', 'repeat', 'round', 'space'
445 $props['border-image'] = UnorderedGroup::someOf( [
446 $props['border-image-source'],
448 $props['border-image-slice'],
449 Quantifier::optional( new Alternative( [
450 new Juxtaposition( [ $slash, $props['border-image-width'] ] ),
453 Quantifier::optional( $props['border-image-width'] ),
455 $props['border-image-outset']
459 $props['border-image-repeat']
462 $props['box-shadow'] = new Alternative( [
463 new KeywordMatcher( 'none' ),
464 Quantifier::hash( UnorderedGroup::allOf( [
465 Quantifier::optional( new KeywordMatcher( 'inset' ) ),
466 Quantifier::count( $matcherFactory->length(), 2, 4 ),
467 Quantifier::optional( $matcherFactory->color() ),
471 $this->cache[__METHOD__] = $props;
476 * Properties for CSS Image Values and Replaced Content Module Level 3
477 * @see https://www.w3.org/TR/2012/CR-css3-images-20120417/
478 * @param MatcherFactory $matcherFactory Factory for Matchers
479 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
481 protected function cssImages3( MatcherFactory $matcherFactory ) {
482 // @codeCoverageIgnoreStart
483 if ( isset( $this->cache[__METHOD__] ) ) {
484 return $this->cache[__METHOD__];
486 // @codeCoverageIgnoreEnd
490 $props['object-fit'] = new KeywordMatcher( [ 'fill', 'contain', 'cover', 'none', 'scale-down' ] );
491 $props['object-position'] = $matcherFactory->position();
492 $props['image-resolution'] = UnorderedGroup::allOf( [
493 UnorderedGroup::someOf( [
494 new KeywordMatcher( 'from-image' ),
495 $matcherFactory->resolution(),
497 Quantifier::optional( new KeywordMatcher( 'snap' ) )
499 $props['image-orientation'] = $matcherFactory->angle();
501 $this->cache[__METHOD__] = $props;
506 * Properties for CSS Fonts Module Level 3
507 * @see https://www.w3.org/TR/2013/CR-css-fonts-3-20131003/
508 * @param MatcherFactory $matcherFactory Factory for Matchers
509 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
511 protected function cssFonts3( MatcherFactory $matcherFactory ) {
512 // @codeCoverageIgnoreStart
513 if ( isset( $this->cache[__METHOD__] ) ) {
514 return $this->cache[__METHOD__];
516 // @codeCoverageIgnoreEnd
518 $css2 = $this->css2( $matcherFactory );
521 $matchData = FontFaceAtRuleSanitizer::fontMatchData( $matcherFactory );
523 // Note: <generic-family> is syntactically a subset of <family-name>,
524 // so no point in separately listing it.
525 $props['font-family'] = Quantifier::hash( $matchData['familyName'] );
526 $props['font-weight'] = new Alternative( [
527 new KeywordMatcher( [ 'normal', 'bold', 'bolder', 'lighter' ] ),
528 new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
529 return $t->typeFlag() === 'integer' && preg_match( '/^[1-9]00$/', $t->representation() );
532 $props['font-stretch'] = $matchData['font-stretch'];
533 $props['font-style'] = $matchData['font-style'];
534 $props['font-size'] = new Alternative( [
535 new KeywordMatcher( [
536 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller'
538 $matcherFactory->lengthPercentage(),
540 $props['font-size-adjust'] = new Alternative( [
541 new KeywordMatcher( 'none' ), $matcherFactory->number()
543 $props['font'] = new Alternative( [
545 Quantifier::optional( UnorderedGroup::someOf( [
546 $props['font-style'],
547 new KeywordMatcher( [ 'normal', 'small-caps' ] ),
548 $props['font-weight'],
549 $props['font-stretch'],
552 Quantifier::optional( new Juxtaposition( [
553 new DelimMatcher( '/' ),
554 $css2['line-height'],
556 $props['font-family'],
558 new KeywordMatcher( [ 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar' ] )
560 $props['font-synthesis'] = new Alternative( [
561 new KeywordMatcher( 'none' ),
562 UnorderedGroup::someOf( [
563 new KeywordMatcher( 'weight' ),
564 new KeywordMatcher( 'style' ),
567 $props['font-kerning'] = new KeywordMatcher( [ 'auto', 'normal', 'none' ] );
568 $props['font-variant-ligatures'] = new Alternative( [
569 new KeywordMatcher( [ 'normal', 'none' ] ),
570 UnorderedGroup::someOf( $matchData['ligatures'] )
572 $props['font-variant-position'] = new KeywordMatcher( [ 'normal', 'sub', 'super' ] );
573 $props['font-variant-caps'] = new KeywordMatcher(
574 array_merge( [ 'normal' ], $matchData['capsKeywords'] )
576 $props['font-variant-numeric'] = new Alternative( [
577 new KeywordMatcher( 'normal' ),
578 UnorderedGroup::someOf( $matchData['numeric'] )
580 $props['font-variant-alternates'] = new Alternative( [
581 new KeywordMatcher( 'normal' ),
582 UnorderedGroup::someOf( $matchData['alt'] )
584 $props['font-variant-east-asian'] = new Alternative( [
585 new KeywordMatcher( 'normal' ),
586 UnorderedGroup::someOf( $matchData['eastAsian'] )
588 $props['font-variant'] = $matchData['font-variant'];
589 $props['font-feature-settings'] = $matchData['font-feature-settings'];
590 $props['font-language-override'] = new Alternative( [
591 new KeywordMatcher( 'normal' ),
592 new TokenMatcher( Token::T_STRING, function ( Token $t ) {
593 return preg_match( '/^[A-Z]{3}$/', $t->value() );
597 $this->cache[__METHOD__] = $props;
602 * Properties for CSS Multi-column Layout Module
603 * @see https://www.w3.org/TR/2011/CR-css3-multicol-20110412/
604 * @param MatcherFactory $matcherFactory Factory for Matchers
605 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
607 protected function cssMulticol( MatcherFactory $matcherFactory ) {
608 // @codeCoverageIgnoreStart
609 if ( isset( $this->cache[__METHOD__] ) ) {
610 return $this->cache[__METHOD__];
612 // @codeCoverageIgnoreEnd
614 $borders = $this->cssBorderBackground3( $matcherFactory );
615 $breaks = $this->cssBreak3( $matcherFactory );
618 $auto = new KeywordMatcher( 'auto' );
619 $normal = new KeywordMatcher( 'normal' );
621 $props['column-width'] = new Alternative( [ $matcherFactory->length(), $auto ] );
622 $props['column-count'] = new Alternative( [ $matcherFactory->integer(), $auto ] );
623 $props['columns'] = UnorderedGroup::someOf( [ $props['column-width'], $props['column-count'] ] );
624 $props['column-gap'] = new Alternative( [ $matcherFactory->length(), $normal ] );
625 // Copy these from similar items in the Border module
626 $props['column-rule-color'] = $borders['border-right-color'];
627 $props['column-rule-style'] = $borders['border-right-style'];
628 $props['column-rule-width'] = $borders['border-right-width'];
629 $props['column-rule'] = $borders['border-right'];
630 $props['column-span'] = new KeywordMatcher( [ 'none', 'all' ] );
631 $props['column-fill'] = new KeywordMatcher( [ 'auto', 'balance' ] );
633 // Copy these from cssBreak3(), the duplication is allowed as long as
634 // they're the identical Matcher object.
635 $props['break-before'] = $breaks['break-before'];
636 $props['break-after'] = $breaks['break-after'];
637 $props['break-inside'] = $breaks['break-inside'];
639 $this->cache[__METHOD__] = $props;
644 * Properties for CSS Overflow Module Level 3
645 * @see https://www.w3.org/TR/2016/WD-css-overflow-3-20160531/
646 * @param MatcherFactory $matcherFactory Factory for Matchers
647 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
649 protected function cssOverflow3( MatcherFactory $matcherFactory ) {
650 // @codeCoverageIgnoreStart
651 if ( isset( $this->cache[__METHOD__] ) ) {
652 return $this->cache[__METHOD__];
654 // @codeCoverageIgnoreEnd
658 $props['overflow'] = new KeywordMatcher( [ 'visible', 'hidden', 'clip', 'scroll', 'auto' ] );
659 $props['overflow-x'] = $props['overflow'];
660 $props['overflow-y'] = $props['overflow'];
661 $props['max-lines'] = new Alternative( [
662 new KeywordMatcher( 'none' ), $matcherFactory->integer()
665 $this->cache[__METHOD__] = $props;
670 * Properties for CSS Basic User Interface Module Level 4
671 * @see https://www.w3.org/TR/2017/CR-css-ui-3-20170302/
672 * @see https://www.w3.org/TR/2015/WD-css-ui-4-20150922/
673 * @param MatcherFactory $matcherFactory Factory for Matchers
674 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
676 protected function cssUI4( MatcherFactory $matcherFactory ) {
677 // @codeCoverageIgnoreStart
678 if ( isset( $this->cache[__METHOD__] ) ) {
679 return $this->cache[__METHOD__];
681 // @codeCoverageIgnoreEnd
683 $border = $this->cssBorderBackground3( $matcherFactory );
686 $props['box-sizing'] = new KeywordMatcher( [ 'content-box', 'border-box' ] );
687 // Copy these from similar border properties
688 $props['outline-width'] = $border['border-top-width'];
689 $props['outline-style'] = new Alternative( [
690 new KeywordMatcher( 'auto' ), $border['border-top-style']
692 $props['outline-color'] = new Alternative( [
693 new KeywordMatcher( 'invert' ), $matcherFactory->color()
695 $props['outline'] = UnorderedGroup::someOf( [
696 $props['outline-width'], $props['outline-style'], $props['outline-color']
698 $props['outline-offset'] = $matcherFactory->length();
699 $props['resize'] = new KeywordMatcher( [ 'none', 'both', 'horizontal', 'vertical' ] );
700 $props['text-overflow'] = Quantifier::count( new Alternative( [
701 new KeywordMatcher( [ 'clip', 'ellipsis', 'fade' ] ),
702 new FunctionMatcher( 'fade', $matcherFactory->lengthPercentage() ),
703 // Including <string> and count that were removed in the latest UI3
704 // but added in the UI4 editor's draft.
705 $matcherFactory->string(),
707 $props['cursor'] = new Juxtaposition( [
708 Quantifier::star( new Juxtaposition( [
709 $matcherFactory->image(),
710 Quantifier::optional( new Juxtaposition( [
711 $matcherFactory->number(), $matcherFactory->number()
713 $matcherFactory->comma(),
715 new KeywordMatcher( [
716 'auto', 'default', 'none', 'context-menu', 'help', 'pointer', 'progress', 'wait', 'cell',
717 'crosshair', 'text', 'vertical-text', 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab',
718 'grabbing', 'e-resize', 'n-resize', 'ne-resize', 'nw-resize', 's-resize', 'se-resize',
719 'sw-resize', 'w-resize', 'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize', 'col-resize',
720 'row-resize', 'all-scroll', 'zoom-in', 'zoom-out',
723 $props['caret-color'] = new Alternative( [
724 new KeywordMatcher( 'auto' ), $matcherFactory->color()
726 // Skipping caret-animation, it has been removed in the latest editor's draft
727 $props['caret-shape'] = new KeywordMatcher( [ 'auto', 'bar', 'block', 'underscore' ] );
728 $props['caret'] = UnorderedGroup::someOf( [ $props['caret-color'], $props['caret-shape'] ] );
729 $props['nav-up'] = new Alternative( [
730 new KeywordMatcher( 'auto' ),
732 $matcherFactory->cssID(),
733 Quantifier::optional( new Alternative( [
734 new KeywordMatcher( [ 'current', 'root' ] ),
735 $matcherFactory->string(),
739 $props['nav-right'] = $props['nav-up'];
740 $props['nav-down'] = $props['nav-up'];
741 $props['nav-left'] = $props['nav-up'];
743 $props['user-select'] = new KeywordMatcher( [ 'auto', 'text', 'none', 'contain', 'all' ] );
744 // Seems potentially useful enough to let the prefixed versions work.
745 $props['-moz-user-select'] = $props['user-select'];
746 $props['-ms-user-select'] = $props['user-select'];
747 $props['-webkit-user-select'] = $props['user-select'];
749 $props['appearance'] = new KeywordMatcher( [ 'auto', 'none' ] );
751 $this->cache[__METHOD__] = $props;
756 * Properties for CSS Compositing and Blending Level 1
757 * @see https://www.w3.org/TR/2015/CR-compositing-1-20150113/
758 * @param MatcherFactory $matcherFactory Factory for Matchers
759 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
761 protected function cssCompositing1( MatcherFactory $matcherFactory ) {
762 // @codeCoverageIgnoreStart
763 if ( isset( $this->cache[__METHOD__] ) ) {
764 return $this->cache[__METHOD__];
766 // @codeCoverageIgnoreEnd
770 $props['mix-blend-mode'] = new KeywordMatcher( [
771 'normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn',
772 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'
774 $props['isolation'] = new KeywordMatcher( [ 'auto', 'isolate' ] );
776 // The linked spec incorrectly has this without the hash, despite the
777 // textual description and examples showing it as such. The draft has it fixed.
778 $props['background-blend-mode'] = Quantifier::hash( $props['mix-blend-mode'] );
780 $this->cache[__METHOD__] = $props;
785 * Properties for CSS Writing Modes Level 3
786 * @see https://www.w3.org/TR/2015/CR-css-writing-modes-3-20151215/
787 * @param MatcherFactory $matcherFactory Factory for Matchers
788 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
790 protected function cssWritingModes3( MatcherFactory $matcherFactory ) {
791 // @codeCoverageIgnoreStart
792 if ( isset( $this->cache[__METHOD__] ) ) {
793 return $this->cache[__METHOD__];
795 // @codeCoverageIgnoreEnd
799 $props['direction'] = new KeywordMatcher( [ 'ltr', 'rtl' ] );
800 $props['unicode-bidi'] = new KeywordMatcher( [
801 'normal', 'embed', 'isolate', 'bidi-override', 'isolate-override', 'plaintext'
803 $props['writing-mode'] = new KeywordMatcher( [
804 'horizontal-tb', 'vertical-rl', 'vertical-lr', 'sideways-rl', 'sideways-lr'
806 $props['text-orientation'] = new KeywordMatcher( [ 'mixed', 'upright', 'sideways' ] );
807 $props['text-combine-upright'] = new Alternative( [
808 new KeywordMatcher( [ 'none', 'all' ] ),
810 new KeywordMatcher( 'digits' ),
811 Quantifier::optional( $matcherFactory->integer() )
815 $this->cache[__METHOD__] = $props;
820 * Transitions and animations share these functions
821 * @param MatcherFactory $matcherFactory Factory for Matchers
824 protected function transitionTimingFunction( MatcherFactory $matcherFactory ) {
825 // @codeCoverageIgnoreStart
826 if ( isset( $this->cache[__METHOD__] ) ) {
827 return $this->cache[__METHOD__];
829 // @codeCoverageIgnoreEnd
831 $timingFunction = new Alternative( [
832 new KeywordMatcher( [
833 'ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end'
835 new FunctionMatcher( 'steps', new Juxtaposition( [
836 $matcherFactory->integer(),
837 Quantifier::optional( new KeywordMatcher( [ 'start', 'end' ] ) ),
839 new FunctionMatcher( 'cubic-bezier', Quantifier::hash( $matcherFactory->number(), 4, 4 ) ),
842 $this->cache[__METHOD__] = $timingFunction;
843 return $timingFunction;
847 * Properties for CSS Transitions
848 * @see https://www.w3.org/TR/2013/WD-css3-transitions-20131119/
849 * @param MatcherFactory $matcherFactory Factory for Matchers
850 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
852 protected function cssTransitions( MatcherFactory $matcherFactory ) {
853 // @codeCoverageIgnoreStart
854 if ( isset( $this->cache[__METHOD__] ) ) {
855 return $this->cache[__METHOD__];
857 // @codeCoverageIgnoreEnd
860 $none = new KeywordMatcher( 'none' );
861 $timingFunction = $this->transitionTimingFunction( $matcherFactory );
863 $props['transition-property'] = new Alternative( [
864 $none, Quantifier::hash( $matcherFactory->ident() )
866 $props['transition-duration'] = Quantifier::hash( $matcherFactory->time() );
867 $props['transition-timing-function'] = Quantifier::hash( $timingFunction );
868 $props['transition-delay'] = Quantifier::hash( $matcherFactory->time() );
869 $props['transition'] = Quantifier::hash( UnorderedGroup::someOf( [
870 $matcherFactory->ident(), // none and <single-transition-property> are grammatically the same
871 $matcherFactory->time(),
873 $matcherFactory->time(),
876 $this->cache[__METHOD__] = $props;
881 * Properties for CSS Animations
882 * @see https://www.w3.org/TR/2013/WD-css3-animations-20130219/
883 * @param MatcherFactory $matcherFactory Factory for Matchers
884 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
886 protected function cssAnimations( MatcherFactory $matcherFactory ) {
887 // @codeCoverageIgnoreStart
888 if ( isset( $this->cache[__METHOD__] ) ) {
889 return $this->cache[__METHOD__];
891 // @codeCoverageIgnoreEnd
894 $timingFunction = $this->transitionTimingFunction( $matcherFactory );
895 $count = new Alternative( [
896 new KeywordMatcher( 'infinite' ),
897 $matcherFactory->number()
899 $direction = new KeywordMatcher( [ 'normal', 'reverse', 'alternate', 'alternate-reverse' ] );
900 $playState = new KeywordMatcher( [ 'running', 'paused' ] );
901 $fillMode = new KeywordMatcher( [ 'none', 'forwards', 'backwards', 'both' ] );
903 $props['animation-name'] = Quantifier::hash( $matcherFactory->ident() );
904 $props['animation-duration'] = Quantifier::hash( $matcherFactory->time() );
905 $props['animation-timing-function'] = Quantifier::hash( $timingFunction );
906 $props['animation-iteration-count'] = Quantifier::hash( $count );
907 $props['animation-direction'] = Quantifier::hash( $direction );
908 $props['animation-play-state'] = Quantifier::hash( $playState );
909 $props['animation-delay'] = Quantifier::hash( $matcherFactory->time() );
910 $props['animation-fill-mode'] = Quantifier::hash( $fillMode );
911 $props['animation'] = Quantifier::hash( UnorderedGroup::someOf( [
912 $matcherFactory->ident(),
913 $matcherFactory->time(),
915 $matcherFactory->time(),
922 $this->cache[__METHOD__] = $props;
927 * Properties for CSS Flexible Box Layout Module Level 1
928 * @see https://www.w3.org/TR/2016/CR-css-flexbox-1-20160526/
929 * @note Omits align-* and justify-* properties redefined by self::cssAlign3()
930 * @param MatcherFactory $matcherFactory Factory for Matchers
931 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
933 protected function cssFlexbox3( MatcherFactory $matcherFactory ) {
934 // @codeCoverageIgnoreStart
935 if ( isset( $this->cache[__METHOD__] ) ) {
936 return $this->cache[__METHOD__];
938 // @codeCoverageIgnoreEnd
941 $props['flex-direction'] = new KeywordMatcher( [
942 'row', 'row-reverse', 'column', 'column-reverse'
944 $props['flex-wrap'] = new KeywordMatcher( [ 'nowrap', 'wrap', 'wrap-reverse' ] );
945 $props['flex-flow'] = UnorderedGroup::someOf( [ $props['flex-direction'], $props['flex-wrap'] ] );
946 $props['order'] = $matcherFactory->integer();
947 $props['flex-grow'] = $matcherFactory->number();
948 $props['flex-shrink'] = $matcherFactory->number();
949 $props['flex-basis'] = new Alternative( [
950 new KeywordMatcher( [ 'content', 'auto' ] ),
951 $matcherFactory->lengthPercentage(),
953 $props['flex'] = new Alternative( [
954 new KeywordMatcher( 'none' ),
955 UnorderedGroup::someOf( [
956 new Juxtaposition( [ $props['flex-grow'], Quantifier::optional( $props['flex-shrink'] ) ] ),
957 $props['flex-basis'],
961 $this->cache[__METHOD__] = $props;
966 * Properties for CSS Transforms Module Level 1
967 * @see https://www.w3.org/TR/2013/WD-css-transforms-1-20131126/
968 * @param MatcherFactory $matcherFactory Factory for Matchers
969 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
971 protected function cssTransforms1( MatcherFactory $matcherFactory ) {
972 // @codeCoverageIgnoreStart
973 if ( isset( $this->cache[__METHOD__] ) ) {
974 return $this->cache[__METHOD__];
976 // @codeCoverageIgnoreEnd
979 $a = $matcherFactory->angle();
980 $n = $matcherFactory->number();
981 $l = $matcherFactory->length();
982 $v = new Alternative( [ $l, $n ] );
983 $lp = $matcherFactory->lengthPercentage();
984 $olp = Quantifier::optional( $lp );
985 $center = new KeywordMatcher( 'center' );
986 $leftRight = new KeywordMatcher( [ 'left', 'right' ] );
987 $topBottom = new KeywordMatcher( [ 'top', 'bottom' ] );
989 $props['transform'] = new Alternative( [
990 new KeywordMatcher( 'none' ),
991 Quantifier::plus( new Alternative( [
992 new FunctionMatcher( 'matrix', Quantifier::hash( $n, 6, 6 ) ),
993 new FunctionMatcher( 'translate', Quantifier::hash( $v, 1, 2 ) ),
994 new FunctionMatcher( 'translateX', $v ),
995 new FunctionMatcher( 'translateY', $v ),
996 new FunctionMatcher( 'scale', Quantifier::hash( $n, 1, 2 ) ),
997 new FunctionMatcher( 'scaleX', $n ),
998 new FunctionMatcher( 'scaleY', $n ),
999 new FunctionMatcher( 'rotate', $a ),
1000 new FunctionMatcher( 'skew', Quantifier::hash( $a, 1, 2 ) ),
1001 new FunctionMatcher( 'skewX', $a ),
1002 new FunctionMatcher( 'skewY', $a ),
1003 new FunctionMatcher( 'matrix3d', Quantifier::hash( $n, 16, 16 ) ),
1004 new FunctionMatcher( 'translate3d', new Juxtaposition( [ $v, $v, $l ], true ) ),
1005 new FunctionMatcher( 'translateZ', $l ),
1006 new FunctionMatcher( 'scale3d', Quantifier::hash( $n, 3, 3 ) ),
1007 new FunctionMatcher( 'scaleZ', $n ),
1008 new FunctionMatcher( 'rotate3d', new Juxtaposition( [ $n, $n, $n, $a ], true ) ),
1009 new FunctionMatcher( 'rotateX', $a ),
1010 new FunctionMatcher( 'rotateY', $a ),
1011 new FunctionMatcher( 'rotateZ', $a ),
1012 new FunctionMatcher( 'perspective', $l ),
1015 $props['transform-origin'] = new Alternative( [
1016 new Alternative( [ $center, $leftRight, $topBottom, $lp ] ),
1017 new Juxtaposition( [
1018 new Alternative( [ $center, $leftRight, $lp ] ),
1019 new Alternative( [ $center, $topBottom, $lp ] ),
1022 UnorderedGroup::allOf( [
1023 new Alternative( [ $center, $leftRight ] ),
1024 new Juxtaposition( [ new Alternative( [ $center, $topBottom ] ), $olp ] ),
1027 $props['transform-style'] = new KeywordMatcher( [ 'flat', 'preserve-3d' ] );
1028 $props['perspective'] = new Alternative( [ new KeywordMatcher( 'none' ), $l ] );
1029 $props['perspective-origin'] = new Alternative( [
1030 new Alternative( [ $center, $leftRight, $topBottom, $lp ] ),
1031 new Juxtaposition( [
1032 new Alternative( [ $center, $leftRight, $lp ] ),
1033 new Alternative( [ $center, $topBottom, $lp ] ),
1035 UnorderedGroup::allOf( [
1036 new Alternative( [ $center, $leftRight ] ),
1037 new Alternative( [ $center, $topBottom ] ),
1040 $props['backface-visibility'] = new KeywordMatcher( [ 'visible', 'hidden' ] );
1042 $this->cache[__METHOD__] = $props;
1047 * Properties for CSS Text Module Level 3
1048 * @see https://www.w3.org/TR/2013/WD-css-text-3-20131010/
1049 * @param MatcherFactory $matcherFactory Factory for Matchers
1050 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1052 protected function cssText3( MatcherFactory $matcherFactory ) {
1053 // @codeCoverageIgnoreStart
1054 if ( isset( $this->cache[__METHOD__] ) ) {
1055 return $this->cache[__METHOD__];
1057 // @codeCoverageIgnoreEnd
1061 $props['text-transform'] = new KeywordMatcher( [
1062 'none', 'capitalize', 'uppercase', 'lowercase', 'full-width'
1064 $props['white-space'] = new KeywordMatcher( [
1065 'normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line'
1067 $props['tab-size'] = new Alternative( [ $matcherFactory->integer(), $matcherFactory->length() ] );
1068 $props['line-break'] = new KeywordMatcher( [ 'auto', 'loose', 'normal', 'strict' ] );
1069 $props['word-break'] = new KeywordMatcher( [ 'normal', 'keep-all', 'break-all' ] );
1070 $props['hyphens'] = new KeywordMatcher( [ 'none', 'manual', 'auto' ] );
1071 $props['word-wrap'] = new KeywordMatcher( [ 'normal', 'break-word' ] );
1072 $props['overflow-wrap'] = $props['word-wrap'];
1073 $props['text-align'] = new Alternative( [
1074 new KeywordMatcher( [ 'start', 'end', 'left', 'right', 'center', 'justify', 'match-parent' ] ),
1075 new Juxtaposition( [ new KeywordMatcher( 'start' ), new KeywordMatcher( 'end' ) ] ),
1077 $props['text-align-last'] = new KeywordMatcher( [
1078 'auto', 'start', 'end', 'left', 'right', 'center', 'justify'
1080 $props['text-justify'] = new KeywordMatcher( [ 'auto', 'none', 'inter-word', 'distribute' ] );
1081 $props['word-spacing'] = new Alternative( [
1082 new KeywordMatcher( 'normal' ),
1083 $matcherFactory->lengthPercentage()
1085 $props['letter-spacing'] = new Alternative( [
1086 new KeywordMatcher( 'normal' ),
1087 $matcherFactory->length()
1089 $props['text-indent'] = UnorderedGroup::allOf( [
1090 $matcherFactory->lengthPercentage(),
1091 Quantifier::optional( new KeywordMatcher( 'hanging' ) ),
1092 Quantifier::optional( new KeywordMatcher( 'each-line' ) ),
1094 $props['hanging-punctuation'] = new Alternative( [
1095 new KeywordMatcher( 'none' ),
1096 UnorderedGroup::someOf( [
1097 new KeywordMatcher( 'first' ),
1098 new KeywordMatcher( [ 'force-end', 'allow-end' ] ),
1099 new KeywordMatcher( 'last' ),
1103 $this->cache[__METHOD__] = $props;
1108 * Properties for CSS ext Decoration Module Level 3
1109 * @see https://www.w3.org/TR/2013/CR-css-text-decor-3-20130801/
1110 * @param MatcherFactory $matcherFactory Factory for Matchers
1111 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1113 protected function cssTextDecor3( MatcherFactory $matcherFactory ) {
1114 // @codeCoverageIgnoreStart
1115 if ( isset( $this->cache[__METHOD__] ) ) {
1116 return $this->cache[__METHOD__];
1118 // @codeCoverageIgnoreEnd
1122 $props['text-decoration-line'] = new Alternative( [
1123 new KeywordMatcher( 'none' ),
1124 UnorderedGroup::someOf( [
1125 new KeywordMatcher( 'underline' ),
1126 new KeywordMatcher( 'overline' ),
1127 new KeywordMatcher( 'line-through' ),
1128 // new KeywordMatcher( 'blink' ), // NOOO!!!
1131 $props['text-decoration-color'] = $matcherFactory->color();
1132 $props['text-decoration-style'] = new KeywordMatcher( [
1133 'solid', 'double', 'dotted', 'dashed', 'wavy'
1135 $props['text-decoration'] = UnorderedGroup::someOf( [
1136 $props['text-decoration-line'],
1137 $props['text-decoration-style'],
1138 $props['text-decoration-color'],
1140 $props['text-decoration-skip'] = new Alternative( [
1141 new KeywordMatcher( 'none' ),
1142 UnorderedGroup::someOf( [
1143 new KeywordMatcher( 'objects' ),
1144 new KeywordMatcher( 'spaces' ),
1145 new KeywordMatcher( 'ink' ),
1146 new KeywordMatcher( 'edges' ),
1147 new KeywordMatcher( 'box-decoration' ),
1150 $props['text-underline-position'] = new Alternative( [
1151 new KeywordMatcher( 'auto' ),
1152 UnorderedGroup::someOf( [
1153 new KeywordMatcher( 'under' ),
1154 new KeywordMatcher( [ 'left', 'right' ] ),
1157 $props['text-emphasis-style'] = new Alternative( [
1158 new KeywordMatcher( 'none' ),
1159 UnorderedGroup::someOf( [
1160 new KeywordMatcher( [ 'filled', 'open' ] ),
1161 new KeywordMatcher( [ 'dot', 'circle', 'double-circle', 'triangle', 'sesame' ] )
1163 $matcherFactory->string(),
1165 $props['text-emphasis-color'] = $matcherFactory->color();
1166 $props['text-emphasis'] = UnorderedGroup::someOf( [
1167 $props['text-emphasis-style'],
1168 $props['text-emphasis-color'],
1170 $props['text-emphasis-position'] = UnorderedGroup::allOf( [
1171 new KeywordMatcher( [ 'over', 'under' ] ),
1172 new KeywordMatcher( [ 'right', 'left' ] ),
1174 $props['text-shadow'] = new Alternative( [
1175 new KeywordMatcher( 'none' ),
1176 Quantifier::hash( UnorderedGroup::allOf( [
1177 Quantifier::count( $matcherFactory->length(), 2, 3 ),
1178 Quantifier::optional( $matcherFactory->color() ),
1182 $this->cache[__METHOD__] = $props;
1187 * Properties for CSS Box Alignment Module Level 3
1188 * @see https://www.w3.org/TR/2017/WD-css-align-3-20170215/
1189 * @param MatcherFactory $matcherFactory Factory for Matchers
1190 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1192 protected function cssAlign3( MatcherFactory $matcherFactory ) {
1193 // @codeCoverageIgnoreStart
1194 if ( isset( $this->cache[__METHOD__] ) ) {
1195 return $this->cache[__METHOD__];
1197 // @codeCoverageIgnoreEnd
1200 $normal = new KeywordMatcher( 'normal' );
1201 $normalStretch = new KeywordMatcher( [ 'normal', 'stretch' ] );
1202 $autoNormalStretch = new KeywordMatcher( [ 'auto', 'normal', 'stretch' ] );
1203 $overflowPosition = Quantifier::optional( new KeywordMatcher( [ 'safe', 'unsafe' ] ) );
1204 $selfPosition = new KeywordMatcher( [
1205 'center', 'start', 'end', 'self-start', 'self-end', 'flex-start', 'flex-end', 'left', 'right'
1207 $overflowAndSelfPosition = UnorderedGroup::allOf( [ $overflowPosition, $selfPosition ] );
1208 $baselinePosition = new Juxtaposition( [
1209 Quantifier::optional( new KeywordMatcher( [ 'first', 'last' ] ) ),
1210 new KeywordMatcher( 'baseline' )
1212 $contentDistribution = new KeywordMatcher( [
1213 'space-between', 'space-around', 'space-evenly', 'stretch'
1215 $contentPosition = new KeywordMatcher( [
1216 'center', 'start', 'end', 'flex-start', 'flex-end', 'left', 'right'
1219 $props['align-content'] = new Alternative( [
1222 UnorderedGroup::someOf( [
1223 $contentDistribution,
1224 UnorderedGroup::allOf( [ $overflowPosition, $contentPosition ] ),
1227 $props['justify-content'] = $props['align-content'];
1228 $props['place-content'] = Quantifier::count( new Alternative( [
1231 $contentDistribution,
1234 $props['align-self'] = new Alternative( [
1237 $overflowAndSelfPosition,
1239 $props['justify-self'] = $props['align-self'];
1240 $props['place-self'] = Quantifier::count( new Alternative( [
1245 $props['align-items'] = new Alternative( [
1248 $overflowAndSelfPosition,
1250 $props['justify-items'] = new Alternative( [
1253 $overflowAndSelfPosition,
1254 UnorderedGroup::allOf( [
1255 new KeywordMatcher( 'legacy' ),
1256 new KeywordMatcher( [ 'left', 'right', 'center' ] ),
1259 $props['place-items'] = new Juxtaposition( [
1260 new Alternative( [ $normalStretch, $baselinePosition, $selfPosition ] ),
1261 Quantifier::optional( new Alternative( [
1262 $autoNormalStretch, $baselinePosition, $selfPosition
1266 $this->cache[__METHOD__] = $props;
1271 * Properties for CSS Fragmentation Module Level 3
1272 * @see https://www.w3.org/TR/2017/CR-css-break-3-20170209/
1273 * @param MatcherFactory $matcherFactory Factory for Matchers
1274 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1276 protected function cssBreak3( MatcherFactory $matcherFactory ) {
1277 // @codeCoverageIgnoreStart
1278 if ( isset( $this->cache[__METHOD__] ) ) {
1279 return $this->cache[__METHOD__];
1281 // @codeCoverageIgnoreEnd
1284 $props['break-before'] = new KeywordMatcher( [
1285 'auto', 'avoid', 'avoid-page', 'page', 'left', 'right', 'recto', 'verso', 'avoid-column',
1286 'column', 'avoid-region', 'region'
1288 $props['break-after'] = $props['break-before'];
1289 $props['break-inside'] = new KeywordMatcher( [
1290 'auto', 'avoid', 'avoid-page', 'avoid-column', 'avoid-region'
1292 $props['orphans'] = $matcherFactory->integer();
1293 $props['widows'] = $matcherFactory->integer();
1294 $props['box-decoration-break'] = new KeywordMatcher( [ 'slice', 'clone' ] );
1295 $props['page-break-before'] = new KeywordMatcher( [
1296 'auto', 'always', 'avoid', 'left', 'right'
1298 $props['page-break-after'] = $props['page-break-before'];
1299 $props['page-break-inside'] = new KeywordMatcher( [ 'auto', 'avoid' ] );
1301 $this->cache[__METHOD__] = $props;
1306 * Properties for CSS Speech Module
1307 * @see https://www.w3.org/TR/2012/CR-css3-speech-20120320/
1308 * @param MatcherFactory $matcherFactory Factory for Matchers
1309 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1311 protected function cssSpeech( MatcherFactory $matcherFactory ) {
1312 // @codeCoverageIgnoreStart
1313 if ( isset( $this->cache[__METHOD__] ) ) {
1314 return $this->cache[__METHOD__];
1316 // @codeCoverageIgnoreEnd
1319 $decibel = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1320 return !strcasecmp( $t->unit(), 'dB' );
1323 $props['voice-volume'] = new Alternative( [
1324 new KeywordMatcher( 'silent' ),
1325 UnorderedGroup::someOf( [
1326 new KeywordMatcher( [ 'x-soft', 'soft', 'medium', 'loud', 'x-loud' ] ),
1330 $props['voice-balance'] = new Alternative( [
1331 $matcherFactory->number(),
1332 new KeywordMatcher( [ 'left', 'center', 'right', 'leftwards', 'rightwards' ] ),
1334 $props['speak'] = new KeywordMatcher( [ 'auto', 'none', 'normal' ] );
1335 $props['speak-as'] = new Alternative( [
1336 new KeywordMatcher( 'normal' ),
1337 UnorderedGroup::someOf( [
1338 new KeywordMatcher( 'spell-out' ),
1339 new KeywordMatcher( 'digits' ),
1340 new KeywordMatcher( [ 'literal-punctuation', 'no-punctuation' ] ),
1343 $props['pause-before'] = new Alternative( [
1344 $matcherFactory->time(),
1345 new KeywordMatcher( [ 'none', 'x-weak', 'weak', 'medium', 'strong', 'x-strong' ] ),
1347 $props['pause-after'] = $props['pause-before'];
1348 $props['pause'] = new Juxtaposition( [
1349 $props['pause-before'],
1350 Quantifier::optional( $props['pause-after'] )
1352 $props['rest-before'] = $props['pause-before'];
1353 $props['rest-after'] = $props['pause-after'];
1354 $props['rest'] = $props['pause'];
1355 $props['cue-before'] = new Alternative( [
1356 new Juxtaposition( [ $matcherFactory->url( 'audio' ), Quantifier::optional( $decibel ) ] ),
1357 new KeywordMatcher( 'none' )
1359 $props['cue-after'] = $props['cue-before'];
1360 $props['cue'] = new Juxtaposition( [
1361 $props['cue-before'],
1362 Quantifier::optional( $props['cue-after'] )
1364 $props['voice-family'] = new Alternative( [
1365 Quantifier::hash( new Alternative( [
1366 new Alternative( [ // <name>
1367 $matcherFactory->string(),
1368 Quantifier::plus( $matcherFactory->ident() ),
1370 new Juxtaposition( [ // <generic-voice>
1371 Quantifier::optional( new KeywordMatcher( [ 'child', 'young', 'old' ] ) ),
1372 new KeywordMatcher( [ 'male', 'female', 'neutral' ] ),
1373 Quantifier::optional( $matcherFactory->integer() ),
1376 new KeywordMatcher( 'preserve' )
1378 $props['voice-rate'] = UnorderedGroup::someOf( [
1379 new KeywordMatcher( [ 'normal', 'x-slow', 'slow', 'medium', 'fast', 'x-fast' ] ),
1380 $matcherFactory->percentage()
1382 $props['voice-pitch'] = new Alternative( [
1383 UnorderedGroup::allOf( [
1384 new KeywordMatcher( 'absolute' ),
1385 $matcherFactory->frequency(),
1387 UnorderedGroup::someOf( [
1388 new KeywordMatcher( [ 'x-low', 'low', 'medium', 'high', 'x-high' ] ),
1390 $matcherFactory->frequency(),
1391 new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1392 return !strcasecmp( $t->unit(), 'st' );
1394 $matcherFactory->percentage()
1398 $props['voice-range'] = $props['voice-pitch'];
1399 $props['voice-stress'] = new KeywordMatcher( [
1400 'normal', 'strong', 'moderate', 'none', 'reduced'
1402 $props['voice-duration'] = new Alternative( [
1403 new KeywordMatcher( 'auto' ),
1404 $matcherFactory->time()
1407 $this->cache[__METHOD__] = $props;
1412 * Properties for CSS Grid Layout Module Level 1
1413 * @see https://www.w3.org/TR/2017/CR-css-grid-1-20170209/
1414 * @param MatcherFactory $matcherFactory Factory for Matchers
1415 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1417 protected function cssGrid1( MatcherFactory $matcherFactory ) {
1418 // @codeCoverageIgnoreStart
1419 if ( isset( $this->cache[__METHOD__] ) ) {
1420 return $this->cache[__METHOD__];
1422 // @codeCoverageIgnoreEnd
1425 $comma = $matcherFactory->comma();
1426 $slash = new DelimMatcher( '/' );
1427 $lineNamesO = Quantifier::optional( new BlockMatcher(
1428 Token::T_LEFT_BRACKET, Quantifier::star( $matcherFactory->ident() )
1430 $trackBreadth = new Alternative( [
1431 $matcherFactory->lengthPercentage(),
1432 new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1433 return $t->value() >= 0 && !strcasecmp( $t->unit(), 'fr' );
1435 new KeywordMatcher( [ 'min-content', 'max-content', 'auto' ] )
1437 $inflexibleBreadth = new Alternative( [
1438 $matcherFactory->lengthPercentage(),
1439 new KeywordMatcher( [ 'min-content', 'max-content', 'auto' ] )
1441 $fixedBreadth = $matcherFactory->lengthPercentage();
1442 $trackSize = new Alternative( [
1444 new FunctionMatcher( 'minmax',
1445 new Juxtaposition( [ $inflexibleBreadth, $trackBreadth ], true )
1447 new FunctionMatcher( 'fit-content', $matcherFactory->lengthPercentage() )
1449 $fixedSize = new Alternative( [
1451 new FunctionMatcher( 'minmax', new Juxtaposition( [ $fixedBreadth, $trackBreadth ], true ) ),
1452 new FunctionMatcher( 'minmax',
1453 new Juxtaposition( [ $inflexibleBreadth, $fixedBreadth ], true )
1456 $trackRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [
1457 $matcherFactory->integer(),
1459 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $trackSize ] ) ),
1462 $autoRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [
1463 new KeywordMatcher( [ 'auto-fill', 'auto-fit' ] ),
1465 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $fixedSize ] ) ),
1468 $fixedRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [
1469 $matcherFactory->integer(),
1471 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $fixedSize ] ) ),
1474 $trackList = new Juxtaposition( [
1475 Quantifier::plus( new Juxtaposition( [
1476 $lineNamesO, new Alternative( [ $trackSize, $trackRepeat ] )
1480 $autoTrackList = new Juxtaposition( [
1481 Quantifier::star( new Juxtaposition( [
1482 $lineNamesO, new Alternative( [ $fixedSize, $fixedRepeat ] )
1486 Quantifier::star( new Juxtaposition( [
1487 $lineNamesO, new Alternative( [ $fixedSize, $fixedRepeat ] )
1491 $explicitTrackList = new Juxtaposition( [
1492 Quantifier::plus( new Juxtaposition( [ $lineNamesO, $trackSize ] ) ),
1495 $autoDense = UnorderedGroup::allOf( [
1496 new KeywordMatcher( 'auto-flow' ),
1497 Quantifier::optional( new KeywordMatcher( 'dense' ) )
1500 $props['grid-template-columns'] = new Alternative( [
1501 new KeywordMatcher( 'none' ), $trackList, $autoTrackList
1503 $props['grid-template-rows'] = $props['grid-template-columns'];
1504 $props['grid-template-areas'] = new Alternative( [
1505 new KeywordMatcher( 'none' ),
1506 Quantifier::plus( $matcherFactory->string() ),
1508 $props['grid-template'] = new Alternative( [
1509 new KeywordMatcher( 'none' ),
1510 new Juxtaposition( [ $props['grid-template-rows'], $slash, $props['grid-template-columns'] ] ),
1511 new Juxtaposition( [
1512 Quantifier::plus( new Juxtaposition( [
1513 $lineNamesO, $matcherFactory->string(), Quantifier::optional( $trackSize ), $lineNamesO
1515 Quantifier::optional( new Juxtaposition( [ $slash, $explicitTrackList ] ) ),
1518 $props['grid-auto-columns'] = Quantifier::plus( $trackSize );
1519 $props['grid-auto-rows'] = $props['grid-auto-columns'];
1520 $props['grid-auto-flow'] = UnorderedGroup::someOf( [
1521 new KeywordMatcher( [ 'row', 'column' ] ),
1522 new KeywordMatcher( 'dense' )
1524 $props['grid'] = new Alternative( [
1525 $props['grid-template'],
1526 new Juxtaposition( [
1527 $props['grid-template-rows'],
1530 Quantifier::optional( $props['grid-auto-columns'] ),
1532 new Juxtaposition( [
1534 Quantifier::optional( $props['grid-auto-rows'] ),
1536 $props['grid-template-columns'],
1540 $gridLine = new Alternative( [
1541 new KeywordMatcher( 'auto' ),
1542 $matcherFactory->ident(),
1543 UnorderedGroup::allOf( [
1544 $matcherFactory->integer(),
1545 Quantifier::optional( $matcherFactory->ident() )
1547 UnorderedGroup::allOf( [
1548 new KeywordMatcher( 'span' ),
1549 UnorderedGroup::someOf( [
1550 $matcherFactory->integer(),
1551 $matcherFactory->ident(),
1555 $props['grid-row-start'] = $gridLine;
1556 $props['grid-column-start'] = $gridLine;
1557 $props['grid-row-end'] = $gridLine;
1558 $props['grid-column-end'] = $gridLine;
1559 $props['grid-row'] = new Juxtaposition( [
1560 $gridLine, Quantifier::optional( new Juxtaposition( [ $slash, $gridLine ] ) )
1562 $props['grid-column'] = $props['grid-row'];
1563 $props['grid-area'] = new Juxtaposition( [
1564 $gridLine, Quantifier::count( new Juxtaposition( [ $slash, $gridLine ] ), 0, 3 )
1567 $props['grid-row-gap'] = $matcherFactory->lengthPercentage();
1568 $props['grid-column-gap'] = $matcherFactory->lengthPercentage();
1569 $props['grid-gap'] = new Juxtaposition( [
1570 $props['grid-row-gap'], Quantifier::optional( $props['grid-column-gap'] )
1573 // Grid uses Flexbox's order property too. Copying is ok as long as
1574 // it's the identical object.
1575 $props['order'] = $this->cssFlexbox3( $matcherFactory )['order'];
1577 $this->cache[__METHOD__] = $props;
1582 * Properties for CSS Filter Effects Module Level 1
1583 * @see https://www.w3.org/TR/2014/WD-filter-effects-1-20141125/
1584 * @param MatcherFactory $matcherFactory Factory for Matchers
1585 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1587 protected function cssFilter1( MatcherFactory $matcherFactory ) {
1588 // @codeCoverageIgnoreStart
1589 if ( isset( $this->cache[__METHOD__] ) ) {
1590 return $this->cache[__METHOD__];
1592 // @codeCoverageIgnoreEnd
1596 $props['filter'] = new Alternative( [
1597 new KeywordMatcher( 'none' ),
1598 Quantifier::plus( new Alternative( [
1599 new FunctionMatcher( 'blur', $matcherFactory->length() ),
1600 new FunctionMatcher( 'brightness', $matcherFactory->numberPercentage() ),
1601 new FunctionMatcher( 'contrast', $matcherFactory->numberPercentage() ),
1602 new FunctionMatcher( 'drop-shadow', new Juxtaposition( [
1603 Quantifier::count( $matcherFactory->length(), 2, 3 ),
1604 Quantifier::optional( $matcherFactory->color() ),
1606 new FunctionMatcher( 'grayscale', $matcherFactory->numberPercentage() ),
1607 new FunctionMatcher( 'hue-rotate', $matcherFactory->angle() ),
1608 new FunctionMatcher( 'invert', $matcherFactory->numberPercentage() ),
1609 new FunctionMatcher( 'opacity', $matcherFactory->numberPercentage() ),
1610 new FunctionMatcher( 'saturate', $matcherFactory->numberPercentage() ),
1611 new FunctionMatcher( 'sepia', $matcherFactory->numberPercentage() ),
1612 $matcherFactory->url( 'svg' ),
1615 $props['flood-color'] = $matcherFactory->color();
1616 $props['flood-opacity'] = $matcherFactory->numberPercentage();
1617 $props['color-interpolation-filters'] = new KeywordMatcher( [ 'auto', 'sRGB', 'linearRGB' ] );
1618 $props['lighting-color'] = $matcherFactory->color();
1620 $this->cache[__METHOD__] = $props;
1625 * Shapes and masking share these basic shapes
1626 * @see https://www.w3.org/TR/2014/CR-css-shapes-1-20140320/#basic-shape-functions
1627 * @param MatcherFactory $matcherFactory Factory for Matchers
1630 protected function basicShapes( MatcherFactory $matcherFactory ) {
1631 // @codeCoverageIgnoreStart
1632 if ( isset( $this->cache[__METHOD__] ) ) {
1633 return $this->cache[__METHOD__];
1635 // @codeCoverageIgnoreEnd
1637 $border = $this->cssBorderBackground3( $matcherFactory );
1638 $sa = $matcherFactory->lengthPercentage();
1639 $sr = new Alternative( [
1641 new KeywordMatcher( [ 'closest-side', 'farthest-side' ] ),
1644 $basicShape = new Alternative( [
1645 new FunctionMatcher( 'inset', new Juxtaposition( [
1646 Quantifier::count( $sa, 1, 4 ),
1647 Quantifier::optional( new Juxtaposition( [
1648 new KeywordMatcher( 'round' ), $border['border-radius']
1651 new FunctionMatcher( 'circle', new Juxtaposition( [
1652 Quantifier::optional( $sr ),
1653 Quantifier::optional( new Juxtaposition( [
1654 new KeywordMatcher( 'at' ), $matcherFactory->position()
1657 new FunctionMatcher( 'ellipse', new Juxtaposition( [
1658 Quantifier::optional( Quantifier::count( $sr, 2, 2 ) ),
1659 Quantifier::optional( new Juxtaposition( [
1660 new KeywordMatcher( 'at' ), $matcherFactory->position()
1663 new FunctionMatcher( 'polygon', new Juxtaposition( [
1664 Quantifier::optional( new KeywordMatcher( [ 'nonzero', 'evenodd' ] ) ),
1665 Quantifier::hash( Quantifier::count( $sa, 2, 2 ) ),
1669 $this->cache[__METHOD__] = $basicShape;
1674 * Properties for CSS Shapes Module Level 1
1675 * @see https://www.w3.org/TR/2014/CR-css-shapes-1-20140320/
1676 * @param MatcherFactory $matcherFactory Factory for Matchers
1677 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1679 protected function cssShapes1( MatcherFactory $matcherFactory ) {
1680 // @codeCoverageIgnoreStart
1681 if ( isset( $this->cache[__METHOD__] ) ) {
1682 return $this->cache[__METHOD__];
1684 // @codeCoverageIgnoreEnd
1686 $shapeBoxKW = $this->backgroundTypes( $matcherFactory )['boxKeywords'];
1687 $shapeBoxKW[] = 'margin-box';
1691 $props['shape-outside'] = new Alternative( [
1692 new KeywordMatcher( 'none' ),
1693 UnorderedGroup::someOf( [
1694 $this->basicShapes( $matcherFactory ),
1695 new KeywordMatcher( $shapeBoxKW ),
1697 $matcherFactory->url( 'image' ),
1699 $props['shape-image-threshold'] = $matcherFactory->number();
1700 $props['shape-margin'] = $matcherFactory->lengthPercentage();
1702 $this->cache[__METHOD__] = $props;
1707 * Properties for CSS Masking Module Level 1
1708 * @see https://www.w3.org/TR/2014/CR-css-masking-1-20140826/
1709 * @param MatcherFactory $matcherFactory Factory for Matchers
1710 * @return Matcher[] Array mapping declaration names (lowercase) to Matchers for the values
1712 protected function cssMasking1( MatcherFactory $matcherFactory ) {
1713 // @codeCoverageIgnoreStart
1714 if ( isset( $this->cache[__METHOD__] ) ) {
1715 return $this->cache[__METHOD__];
1717 // @codeCoverageIgnoreEnd
1719 $slash = new DelimMatcher( '/' );
1720 $bgtypes = $this->backgroundTypes( $matcherFactory );
1721 $bg = $this->cssBorderBackground3( $matcherFactory );
1722 $geometryBoxKeywords = array_merge( $bgtypes['boxKeywords'], [
1723 'margin-box', 'fill-box', 'stroke-box', 'view-box'
1725 $geometryBox = new KeywordMatcher( $geometryBoxKeywords );
1726 $maskRef = new Alternative( [
1727 new KeywordMatcher( 'none' ),
1728 $matcherFactory->image(),
1729 $matcherFactory->url( 'svg' ),
1731 $maskMode = new KeywordMatcher( [ 'alpha', 'luminance', 'auto' ] );
1732 $maskClip = new KeywordMatcher( array_merge( $geometryBoxKeywords, [ 'no-clip' ] ) );
1733 $maskComposite = new KeywordMatcher( [ 'add', 'subtract', 'intersect', 'exclude' ] );
1737 $props['clip-path'] = new Alternative( [
1738 $matcherFactory->url( 'svg' ),
1739 UnorderedGroup::someOf( [
1740 $this->basicShapes( $matcherFactory ),
1743 new KeywordMatcher( 'none' ),
1745 $props['clip-rule'] = new KeywordMatcher( [ 'nonzero', 'evenodd' ] );
1746 $props['mask-image'] = Quantifier::hash( $maskRef );
1747 $props['mask-mode'] = Quantifier::hash( $maskMode );
1748 $props['mask-repeat'] = $bg['background-repeat'];
1749 $props['mask-position'] = Quantifier::hash( $matcherFactory->position() );
1750 $props['mask-clip'] = Quantifier::hash( $maskClip );
1751 $props['mask-origin'] = Quantifier::hash( $geometryBox );
1752 $props['mask-size'] = $bg['background-size'];
1753 $props['mask-composite'] = Quantifier::hash( $maskComposite );
1754 $props['mask'] = Quantifier::hash( UnorderedGroup::someOf( [
1755 new Juxtaposition( [ $maskRef, Quantifier::optional( $maskMode ) ] ),
1756 new Juxtaposition( [
1757 $matcherFactory->position(),
1758 Quantifier::optional( new Juxtaposition( [ $slash, $bgtypes['bgsize'] ] ) ),
1760 $bgtypes['bgrepeat'],
1765 $props['mask-border-source'] = new Alternative( [
1766 new KeywordMatcher( 'none' ),
1767 $matcherFactory->image(),
1769 $props['mask-border-mode'] = new KeywordMatcher( [ 'luminance', 'alpha' ] );
1770 $props['mask-border-slice'] = new Juxtaposition( [ // Different from border-image-slice, sigh
1771 Quantifier::count( $matcherFactory->numberPercentage(), 1, 4 ),
1772 Quantifier::optional( new KeywordMatcher( 'fill' ) ),
1774 $props['mask-border-width'] = $bg['border-image-width'];
1775 $props['mask-border-outset'] = $bg['border-image-outset'];
1776 $props['mask-border-repeat'] = $bg['border-image-repeat'];
1777 $props['mask-border'] = UnorderedGroup::someOf( [
1778 $props['mask-border-source'],
1779 new Juxtaposition( [
1780 $props['mask-border-slice'],
1781 Quantifier::optional( new Juxtaposition( [
1783 Quantifier::optional( $props['mask-border-width'] ),
1784 Quantifier::optional( new Juxtaposition( [
1786 $props['mask-border-outset'],
1790 $props['mask-border-repeat'],
1791 $props['mask-border-mode'],
1793 $props['mask-type'] = new KeywordMatcher( [ 'luminance', 'alpha' ] );
1795 $this->cache[__METHOD__] = $props;