]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - vendor/wikimedia/css-sanitizer/src/Sanitizer/StylePropertySanitizer.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / vendor / wikimedia / css-sanitizer / src / Sanitizer / StylePropertySanitizer.php
1 <?php
2 /**
3  * @file
4  * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
5  */
6
7 namespace Wikimedia\CSS\Sanitizer;
8
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;
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  */
28 class StylePropertySanitizer extends PropertySanitizer {
29
30         /** @var Matcher[][] */
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         }
81
82         /**
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
88          */
89         protected function css2( MatcherFactory $matcherFactory ) {
90                 // @codeCoverageIgnoreStart
91                 if ( isset( $this->cache[__METHOD__] ) ) {
92                         return $this->cache[__METHOD__];
93                 }
94                 // @codeCoverageIgnoreEnd
95
96                 $props = [];
97
98                 $none = new KeywordMatcher( 'none' );
99                 $auto = new KeywordMatcher( 'auto' );
100                 $autoLength = new Alternative( [ $auto, $matcherFactory->length() ] );
101                 $autoLengthPct = new Alternative( [ $auto, $matcherFactory->lengthPercentage() ] );
102
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 );
114
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' ] );
118
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(),
130                 ] );
131                 $props['vertical-align'] = new Alternative( [
132                         new KeywordMatcher( [
133                                 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom'
134                         ] ),
135                         $matcherFactory->lengthPercentage(),
136                 ] );
137
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 ) ),
141                 ] );
142                 $props['visibility'] = new KeywordMatcher( [ 'visible', 'hidden', 'collapse' ] );
143
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'
149                 ] );
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'] ),
158                                 ], true ) ),
159                                 new FunctionMatcher( 'counters', new Juxtaposition( [
160                                         $matcherFactory->ident(),
161                                         $matcherFactory->string(),
162                                         Quantifier::optional( $props['list-style-type'] ),
163                                 ], true ) ),
164                                 new FunctionMatcher( 'attr', $matcherFactory->ident() ),
165                                 new KeywordMatcher( [ 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote' ] ),
166                         ] ) )
167                 ] );
168                 $props['quotes'] = new Alternative( [
169                         $none, Quantifier::plus( new Juxtaposition( [
170                                 $matcherFactory->string(), $matcherFactory->string()
171                         ] ) ),
172                 ] );
173                 $props['counter-reset'] = new Alternative( [
174                         $none,
175                         Quantifier::plus( new Juxtaposition( [
176                                 $matcherFactory->ident(), Quantifier::optional( $matcherFactory->integer() )
177                         ] ) ),
178                 ] );
179                 $props['counter-increment'] = $props['counter-reset'];
180                 $props['list-style-image'] = new Alternative( [
181                         $none,
182                         $matcherFactory->image() // Replaces <url> per https://www.w3.org/TR/css3-images/#placement
183                 ] );
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']
187                 ] );
188
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' ] );
195
196                 $this->cache[__METHOD__] = $props;
197                 return $props;
198         }
199
200         /**
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
205          */
206         protected function cssDisplay3( MatcherFactory $matcherFactory ) {
207                 // @codeCoverageIgnoreStart
208                 if ( isset( $this->cache[__METHOD__] ) ) {
209                         return $this->cache[__METHOD__];
210                 }
211                 // @codeCoverageIgnoreEnd
212
213                 $props = [];
214
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' ] ),
219                         ] ),
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' ] ) ),
224                         ] ),
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',
230                                 // <display-box>
231                                 'contents', 'none',
232                                 // <display-legacy>
233                                 'inline-block', 'inline-list-item', 'inline-table', 'inline-flex', 'inline-grid',
234                                 // https://www.w3.org/TR/2017/CR-css-grid-1-20170209/
235                                 'subgrid',
236                         ] ),
237                 ] );
238
239                 $this->cache[__METHOD__] = $props;
240                 return $props;
241         }
242
243         /**
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
248          */
249         protected function cssPosition3( MatcherFactory $matcherFactory ) {
250                 // @codeCoverageIgnoreStart
251                 if ( isset( $this->cache[__METHOD__] ) ) {
252                         return $this->cache[__METHOD__];
253                 }
254                 // @codeCoverageIgnoreEnd
255
256                 $auto = new KeywordMatcher( 'auto' );
257                 $autoLengthPct = new Alternative( [ $auto, $matcherFactory->lengthPercentage() ] );
258
259                 $props = [];
260
261                 $props['position'] = new KeywordMatcher( [
262                         'static', 'relative', 'absolute', 'sticky', 'fixed'
263                 ] );
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() ] );
273
274                 $this->cache[__METHOD__] = $props;
275                 return $props;
276         }
277
278         /**
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
283          */
284         protected function cssColor3( MatcherFactory $matcherFactory ) {
285                 // @codeCoverageIgnoreStart
286                 if ( isset( $this->cache[__METHOD__] ) ) {
287                         return $this->cache[__METHOD__];
288                 }
289                 // @codeCoverageIgnoreEnd
290
291                 $props = [];
292                 $props['color'] = $matcherFactory->color();
293                 $props['opacity'] = $matcherFactory->number();
294
295                 $this->cache[__METHOD__] = $props;
296                 return $props;
297         }
298
299         /**
300          * Data types for backgrounds
301          * @param MatcherFactory $matcherFactory Factory for Matchers
302          * @return array
303          */
304         protected function backgroundTypes( MatcherFactory $matcherFactory ) {
305                 // @codeCoverageIgnoreStart
306                 if ( isset( $this->cache[__METHOD__] ) ) {
307                         return $this->cache[__METHOD__];
308                 }
309                 // @codeCoverageIgnoreEnd
310
311                 $types = [];
312
313                 $types['bgrepeat'] = new Alternative( [
314                         new KeywordMatcher( [ 'repeat-x', 'repeat-y' ] ),
315                         Quantifier::count( new KeywordMatcher( [ 'repeat', 'space', 'round', 'no-repeat' ] ), 1, 2 ),
316                 ] );
317                 $types['bgsize'] = new Alternative( [
318                         Quantifier::count( new Alternative( [
319                                 $matcherFactory->lengthPercentage(),
320                                 new KeywordMatcher( 'auto' )
321                         ] ), 1, 2 ),
322                         new KeywordMatcher( [ 'cover', 'contain' ] )
323                 ] );
324                 $types['boxKeywords'] = [ 'border-box', 'padding-box', 'content-box' ];
325
326                 $this->cache[__METHOD__] = $types;
327                 return $types;
328         }
329
330         /**
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
335          */
336         protected function cssBorderBackground3( MatcherFactory $matcherFactory ) {
337                 // @codeCoverageIgnoreStart
338                 if ( isset( $this->cache[__METHOD__] ) ) {
339                         return $this->cache[__METHOD__];
340                 }
341                 // @codeCoverageIgnoreEnd
342
343                 $props = [];
344
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( [
354                         $bgimage,
355                         new Juxtaposition( [
356                                 $position, Quantifier::optional( new Juxtaposition( [ $slash, $bgsize ] ) )
357                         ] ),
358                         $bgrepeat,
359                         $bgattach,
360                         $box,
361                         $box,
362                 ] );
363                 $finalBglayer = UnorderedGroup::someOf( [
364                         $bgimage,
365                         new Juxtaposition( [
366                                 $position, Quantifier::optional( new Juxtaposition( [ $slash, $bgsize ] ) )
367                         ] ),
368                         $bgrepeat,
369                         $bgattach,
370                         $box,
371                         $box,
372                         $matcherFactory->color(),
373                 ] );
374
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
385                 );
386
387                 $lineStyle = new KeywordMatcher( [
388                         'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'
389                 ] );
390                 $lineWidth = new Alternative( [
391                         new KeywordMatcher( [ 'thin', 'medium', 'thick' ] ), $matcherFactory->length(),
392                 ] );
393                 $borderCombo = UnorderedGroup::someOf( [ $lineWidth, $lineStyle, $matcherFactory->color() ] );
394                 $radius = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 2 );
395                 $radius4 = Quantifier::count( $matcherFactory->lengthPercentage(), 1, 4 );
396
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 ] ) )
423                 ] );
424                 $props['border-image-source'] = new Alternative( [
425                         new KeywordMatcher( 'none' ),
426                         $matcherFactory->image()
427                 ] );
428                 $props['border-image-slice'] = UnorderedGroup::allOf( [
429                         Quantifier::count( $matcherFactory->numberPercentage(), 1, 4 ),
430                         Quantifier::optional( new KeywordMatcher( 'fill' ) ),
431                 ] );
432                 $props['border-image-width'] = Quantifier::count( new Alternative( [
433                         $matcherFactory->length(),
434                         $matcherFactory->percentage(),
435                         $matcherFactory->number(),
436                         new KeywordMatcher( 'auto' ),
437                 ] ), 1, 4 );
438                 $props['border-image-outset'] = Quantifier::count( new Alternative( [
439                         $matcherFactory->length(),
440                         $matcherFactory->number(),
441                 ] ), 1, 4 );
442                 $props['border-image-repeat'] = Quantifier::count( new KeywordMatcher( [
443                         'stretch', 'repeat', 'round', 'space'
444                 ] ), 1, 2 );
445                 $props['border-image'] = UnorderedGroup::someOf( [
446                         $props['border-image-source'],
447                         new Juxtaposition( [
448                                 $props['border-image-slice'],
449                                 Quantifier::optional( new Alternative( [
450                                         new Juxtaposition( [ $slash, $props['border-image-width'] ] ),
451                                         new Juxtaposition( [
452                                                 $slash,
453                                                 Quantifier::optional( $props['border-image-width'] ),
454                                                 $slash,
455                                                 $props['border-image-outset']
456                                         ] )
457                                 ] ) )
458                         ] ),
459                         $props['border-image-repeat']
460                 ] );
461
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() ),
468                         ] ) )
469                 ] );
470
471                 $this->cache[__METHOD__] = $props;
472                 return $props;
473         }
474
475         /**
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
480          */
481         protected function cssImages3( MatcherFactory $matcherFactory ) {
482                 // @codeCoverageIgnoreStart
483                 if ( isset( $this->cache[__METHOD__] ) ) {
484                         return $this->cache[__METHOD__];
485                 }
486                 // @codeCoverageIgnoreEnd
487
488                 $props = [];
489
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(),
496                         ] ),
497                         Quantifier::optional( new KeywordMatcher( 'snap' ) )
498                 ] );
499                 $props['image-orientation'] = $matcherFactory->angle();
500
501                 $this->cache[__METHOD__] = $props;
502                 return $props;
503         }
504
505         /**
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
510          */
511         protected function cssFonts3( MatcherFactory $matcherFactory ) {
512                 // @codeCoverageIgnoreStart
513                 if ( isset( $this->cache[__METHOD__] ) ) {
514                         return $this->cache[__METHOD__];
515                 }
516                 // @codeCoverageIgnoreEnd
517
518                 $css2 = $this->css2( $matcherFactory );
519                 $props = [];
520
521                 $matchData = FontFaceAtRuleSanitizer::fontMatchData( $matcherFactory );
522
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() );
530                         } ),
531                 ] );
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'
537                         ] ),
538                         $matcherFactory->lengthPercentage(),
539                 ] );
540                 $props['font-size-adjust'] = new Alternative( [
541                         new KeywordMatcher( 'none' ), $matcherFactory->number()
542                 ] );
543                 $props['font'] = new Alternative( [
544                         new Juxtaposition( [
545                                 Quantifier::optional( UnorderedGroup::someOf( [
546                                         $props['font-style'],
547                                         new KeywordMatcher( [ 'normal', 'small-caps' ] ),
548                                         $props['font-weight'],
549                                         $props['font-stretch'],
550                                 ] ) ),
551                                 $props['font-size'],
552                                 Quantifier::optional( new Juxtaposition( [
553                                         new DelimMatcher( '/' ),
554                                         $css2['line-height'],
555                                 ] ) ),
556                                 $props['font-family'],
557                         ] ),
558                         new KeywordMatcher( [ 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar' ] )
559                 ] );
560                 $props['font-synthesis'] = new Alternative( [
561                         new KeywordMatcher( 'none' ),
562                         UnorderedGroup::someOf( [
563                                 new KeywordMatcher( 'weight' ),
564                                 new KeywordMatcher( 'style' ),
565                         ] )
566                 ] );
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'] )
571                 ] );
572                 $props['font-variant-position'] = new KeywordMatcher( [ 'normal', 'sub', 'super' ] );
573                 $props['font-variant-caps'] = new KeywordMatcher(
574                         array_merge( [ 'normal' ], $matchData['capsKeywords'] )
575                 );
576                 $props['font-variant-numeric'] = new Alternative( [
577                         new KeywordMatcher( 'normal' ),
578                         UnorderedGroup::someOf( $matchData['numeric'] )
579                 ] );
580                 $props['font-variant-alternates'] = new Alternative( [
581                         new KeywordMatcher( 'normal' ),
582                         UnorderedGroup::someOf( $matchData['alt'] )
583                 ] );
584                 $props['font-variant-east-asian'] = new Alternative( [
585                         new KeywordMatcher( 'normal' ),
586                         UnorderedGroup::someOf( $matchData['eastAsian'] )
587                 ] );
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() );
594                         } ),
595                 ] );
596
597                 $this->cache[__METHOD__] = $props;
598                 return $props;
599         }
600
601         /**
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
606          */
607         protected function cssMulticol( MatcherFactory $matcherFactory ) {
608                 // @codeCoverageIgnoreStart
609                 if ( isset( $this->cache[__METHOD__] ) ) {
610                         return $this->cache[__METHOD__];
611                 }
612                 // @codeCoverageIgnoreEnd
613
614                 $borders = $this->cssBorderBackground3( $matcherFactory );
615                 $breaks = $this->cssBreak3( $matcherFactory );
616                 $props = [];
617
618                 $auto = new KeywordMatcher( 'auto' );
619                 $normal = new KeywordMatcher( 'normal' );
620
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' ] );
632
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'];
638
639                 $this->cache[__METHOD__] = $props;
640                 return $props;
641         }
642
643         /**
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
648          */
649         protected function cssOverflow3( MatcherFactory $matcherFactory ) {
650                 // @codeCoverageIgnoreStart
651                 if ( isset( $this->cache[__METHOD__] ) ) {
652                         return $this->cache[__METHOD__];
653                 }
654                 // @codeCoverageIgnoreEnd
655
656                 $props = [];
657
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()
663                 ] );
664
665                 $this->cache[__METHOD__] = $props;
666                 return $props;
667         }
668
669         /**
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
675          */
676         protected function cssUI4( MatcherFactory $matcherFactory ) {
677                 // @codeCoverageIgnoreStart
678                 if ( isset( $this->cache[__METHOD__] ) ) {
679                         return $this->cache[__METHOD__];
680                 }
681                 // @codeCoverageIgnoreEnd
682
683                 $border = $this->cssBorderBackground3( $matcherFactory );
684                 $props = [];
685
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']
691                 ] );
692                 $props['outline-color'] = new Alternative( [
693                         new KeywordMatcher( 'invert' ), $matcherFactory->color()
694                 ] );
695                 $props['outline'] = UnorderedGroup::someOf( [
696                         $props['outline-width'], $props['outline-style'], $props['outline-color']
697                 ] );
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(),
706                 ] ), 1, 2 );
707                 $props['cursor'] = new Juxtaposition( [
708                         Quantifier::star( new Juxtaposition( [
709                                 $matcherFactory->image(),
710                                 Quantifier::optional( new Juxtaposition( [
711                                         $matcherFactory->number(), $matcherFactory->number()
712                                 ] ) ),
713                                 $matcherFactory->comma(),
714                         ] ) ),
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',
721                         ] ),
722                 ] );
723                 $props['caret-color'] = new Alternative( [
724                         new KeywordMatcher( 'auto' ), $matcherFactory->color()
725                 ] );
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' ),
731                         new Juxtaposition( [
732                                 $matcherFactory->cssID(),
733                                 Quantifier::optional( new Alternative( [
734                                         new KeywordMatcher( [ 'current', 'root' ] ),
735                                         $matcherFactory->string(),
736                                 ] ) )
737                         ] )
738                 ] );
739                 $props['nav-right'] = $props['nav-up'];
740                 $props['nav-down'] = $props['nav-up'];
741                 $props['nav-left'] = $props['nav-up'];
742
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'];
748
749                 $props['appearance'] = new KeywordMatcher( [ 'auto', 'none' ] );
750
751                 $this->cache[__METHOD__] = $props;
752                 return $props;
753         }
754
755         /**
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
760          */
761         protected function cssCompositing1( MatcherFactory $matcherFactory ) {
762                 // @codeCoverageIgnoreStart
763                 if ( isset( $this->cache[__METHOD__] ) ) {
764                         return $this->cache[__METHOD__];
765                 }
766                 // @codeCoverageIgnoreEnd
767
768                 $props = [];
769
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'
773                 ] );
774                 $props['isolation'] = new KeywordMatcher( [ 'auto', 'isolate' ] );
775
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'] );
779
780                 $this->cache[__METHOD__] = $props;
781                 return $props;
782         }
783
784         /**
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
789          */
790         protected function cssWritingModes3( MatcherFactory $matcherFactory ) {
791                 // @codeCoverageIgnoreStart
792                 if ( isset( $this->cache[__METHOD__] ) ) {
793                         return $this->cache[__METHOD__];
794                 }
795                 // @codeCoverageIgnoreEnd
796
797                 $props = [];
798
799                 $props['direction'] = new KeywordMatcher( [ 'ltr', 'rtl' ] );
800                 $props['unicode-bidi'] = new KeywordMatcher( [
801                         'normal', 'embed', 'isolate', 'bidi-override', 'isolate-override', 'plaintext'
802                 ] );
803                 $props['writing-mode'] = new KeywordMatcher( [
804                         'horizontal-tb', 'vertical-rl', 'vertical-lr', 'sideways-rl', 'sideways-lr'
805                 ] );
806                 $props['text-orientation'] = new KeywordMatcher( [ 'mixed', 'upright', 'sideways' ] );
807                 $props['text-combine-upright'] = new Alternative( [
808                         new KeywordMatcher( [ 'none', 'all' ] ),
809                         new Juxtaposition( [
810                                 new KeywordMatcher( 'digits' ),
811                                 Quantifier::optional( $matcherFactory->integer() )
812                         ] )
813                 ] );
814
815                 $this->cache[__METHOD__] = $props;
816                 return $props;
817         }
818
819         /**
820          * Transitions and animations share these functions
821          * @param MatcherFactory $matcherFactory Factory for Matchers
822          * @return Matcher
823          */
824         protected function transitionTimingFunction( MatcherFactory $matcherFactory ) {
825                 // @codeCoverageIgnoreStart
826                 if ( isset( $this->cache[__METHOD__] ) ) {
827                         return $this->cache[__METHOD__];
828                 }
829                 // @codeCoverageIgnoreEnd
830
831                 $timingFunction = new Alternative( [
832                         new KeywordMatcher( [
833                                 'ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end'
834                         ] ),
835                         new FunctionMatcher( 'steps', new Juxtaposition( [
836                                 $matcherFactory->integer(),
837                                 Quantifier::optional( new KeywordMatcher( [ 'start', 'end' ] ) ),
838                         ], true ) ),
839                         new FunctionMatcher( 'cubic-bezier', Quantifier::hash( $matcherFactory->number(), 4, 4 ) ),
840                 ] );
841
842                 $this->cache[__METHOD__] = $timingFunction;
843                 return $timingFunction;
844         }
845
846         /**
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
851          */
852         protected function cssTransitions( MatcherFactory $matcherFactory ) {
853                 // @codeCoverageIgnoreStart
854                 if ( isset( $this->cache[__METHOD__] ) ) {
855                         return $this->cache[__METHOD__];
856                 }
857                 // @codeCoverageIgnoreEnd
858
859                 $props = [];
860                 $none = new KeywordMatcher( 'none' );
861                 $timingFunction = $this->transitionTimingFunction( $matcherFactory );
862
863                 $props['transition-property'] = new Alternative( [
864                         $none, Quantifier::hash( $matcherFactory->ident() )
865                 ] );
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(),
872                         $timingFunction,
873                         $matcherFactory->time(),
874                 ] ) );
875
876                 $this->cache[__METHOD__] = $props;
877                 return $props;
878         }
879
880         /**
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
885          */
886         protected function cssAnimations( MatcherFactory $matcherFactory ) {
887                 // @codeCoverageIgnoreStart
888                 if ( isset( $this->cache[__METHOD__] ) ) {
889                         return $this->cache[__METHOD__];
890                 }
891                 // @codeCoverageIgnoreEnd
892
893                 $props = [];
894                 $timingFunction = $this->transitionTimingFunction( $matcherFactory );
895                 $count = new Alternative( [
896                         new KeywordMatcher( 'infinite' ),
897                         $matcherFactory->number()
898                 ] );
899                 $direction = new KeywordMatcher( [ 'normal', 'reverse', 'alternate', 'alternate-reverse' ] );
900                 $playState = new KeywordMatcher( [ 'running', 'paused' ] );
901                 $fillMode = new KeywordMatcher( [ 'none', 'forwards', 'backwards', 'both' ] );
902
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(),
914                         $timingFunction,
915                         $matcherFactory->time(),
916                         $count,
917                         $direction,
918                         $fillMode,
919                         $playState
920                 ] ) );
921
922                 $this->cache[__METHOD__] = $props;
923                 return $props;
924         }
925
926         /**
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
932          */
933         protected function cssFlexbox3( MatcherFactory $matcherFactory ) {
934                 // @codeCoverageIgnoreStart
935                 if ( isset( $this->cache[__METHOD__] ) ) {
936                         return $this->cache[__METHOD__];
937                 }
938                 // @codeCoverageIgnoreEnd
939
940                 $props = [];
941                 $props['flex-direction'] = new KeywordMatcher( [
942                         'row', 'row-reverse', 'column', 'column-reverse'
943                 ] );
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(),
952                 ] );
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'],
958                         ] )
959                 ] );
960
961                 $this->cache[__METHOD__] = $props;
962                 return $props;
963         }
964
965         /**
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
970          */
971         protected function cssTransforms1( MatcherFactory $matcherFactory ) {
972                 // @codeCoverageIgnoreStart
973                 if ( isset( $this->cache[__METHOD__] ) ) {
974                         return $this->cache[__METHOD__];
975                 }
976                 // @codeCoverageIgnoreEnd
977
978                 $props = [];
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' ] );
988
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 ),
1013                         ] ) )
1014                 ] );
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 ] ),
1020                                 $olp
1021                         ] ),
1022                         UnorderedGroup::allOf( [
1023                                 new Alternative( [ $center, $leftRight ] ),
1024                                 new Juxtaposition( [ new Alternative( [ $center, $topBottom ] ), $olp ] ),
1025                         ] )
1026                 ] );
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 ] ),
1034                         ] ),
1035                         UnorderedGroup::allOf( [
1036                                 new Alternative( [ $center, $leftRight ] ),
1037                                 new Alternative( [ $center, $topBottom ] ),
1038                         ] )
1039                 ] );
1040                 $props['backface-visibility'] = new KeywordMatcher( [ 'visible', 'hidden' ] );
1041
1042                 $this->cache[__METHOD__] = $props;
1043                 return $props;
1044         }
1045
1046         /**
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
1051          */
1052         protected function cssText3( MatcherFactory $matcherFactory ) {
1053                 // @codeCoverageIgnoreStart
1054                 if ( isset( $this->cache[__METHOD__] ) ) {
1055                         return $this->cache[__METHOD__];
1056                 }
1057                 // @codeCoverageIgnoreEnd
1058
1059                 $props = [];
1060
1061                 $props['text-transform'] = new KeywordMatcher( [
1062                         'none', 'capitalize', 'uppercase', 'lowercase', 'full-width'
1063                 ] );
1064                 $props['white-space'] = new KeywordMatcher( [
1065                         'normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line'
1066                 ] );
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' ) ] ),
1076                 ] );
1077                 $props['text-align-last'] = new KeywordMatcher( [
1078                         'auto', 'start', 'end', 'left', 'right', 'center', 'justify'
1079                 ] );
1080                 $props['text-justify'] = new KeywordMatcher( [ 'auto', 'none', 'inter-word', 'distribute' ] );
1081                 $props['word-spacing'] = new Alternative( [
1082                         new KeywordMatcher( 'normal' ),
1083                         $matcherFactory->lengthPercentage()
1084                 ] );
1085                 $props['letter-spacing'] = new Alternative( [
1086                         new KeywordMatcher( 'normal' ),
1087                         $matcherFactory->length()
1088                 ] );
1089                 $props['text-indent'] = UnorderedGroup::allOf( [
1090                         $matcherFactory->lengthPercentage(),
1091                         Quantifier::optional( new KeywordMatcher( 'hanging' ) ),
1092                         Quantifier::optional( new KeywordMatcher( 'each-line' ) ),
1093                 ] );
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' ),
1100                         ] )
1101                 ] );
1102
1103                 $this->cache[__METHOD__] = $props;
1104                 return $props;
1105         }
1106
1107         /**
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
1112          */
1113         protected function cssTextDecor3( MatcherFactory $matcherFactory ) {
1114                 // @codeCoverageIgnoreStart
1115                 if ( isset( $this->cache[__METHOD__] ) ) {
1116                         return $this->cache[__METHOD__];
1117                 }
1118                 // @codeCoverageIgnoreEnd
1119
1120                 $props = [];
1121
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!!!
1129                         ] )
1130                 ] );
1131                 $props['text-decoration-color'] = $matcherFactory->color();
1132                 $props['text-decoration-style'] = new KeywordMatcher( [
1133                         'solid', 'double', 'dotted', 'dashed', 'wavy'
1134                 ] );
1135                 $props['text-decoration'] = UnorderedGroup::someOf( [
1136                         $props['text-decoration-line'],
1137                         $props['text-decoration-style'],
1138                         $props['text-decoration-color'],
1139                 ] );
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' ),
1148                         ] )
1149                 ] );
1150                 $props['text-underline-position'] = new Alternative( [
1151                         new KeywordMatcher( 'auto' ),
1152                         UnorderedGroup::someOf( [
1153                                 new KeywordMatcher( 'under' ),
1154                                 new KeywordMatcher( [ 'left', 'right' ] ),
1155                         ] )
1156                 ] );
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' ] )
1162                         ] ),
1163                         $matcherFactory->string(),
1164                 ] );
1165                 $props['text-emphasis-color'] = $matcherFactory->color();
1166                 $props['text-emphasis'] = UnorderedGroup::someOf( [
1167                         $props['text-emphasis-style'],
1168                         $props['text-emphasis-color'],
1169                 ] );
1170                 $props['text-emphasis-position'] = UnorderedGroup::allOf( [
1171                         new KeywordMatcher( [ 'over', 'under' ] ),
1172                         new KeywordMatcher( [ 'right', 'left' ] ),
1173                 ] );
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() ),
1179                         ] ) )
1180                 ] );
1181
1182                 $this->cache[__METHOD__] = $props;
1183                 return $props;
1184         }
1185
1186         /**
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
1191          */
1192         protected function cssAlign3( MatcherFactory $matcherFactory ) {
1193                 // @codeCoverageIgnoreStart
1194                 if ( isset( $this->cache[__METHOD__] ) ) {
1195                         return $this->cache[__METHOD__];
1196                 }
1197                 // @codeCoverageIgnoreEnd
1198
1199                 $props = [];
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'
1206                 ] );
1207                 $overflowAndSelfPosition = UnorderedGroup::allOf( [ $overflowPosition, $selfPosition ] );
1208                 $baselinePosition = new Juxtaposition( [
1209                         Quantifier::optional( new KeywordMatcher( [ 'first', 'last' ] ) ),
1210                         new KeywordMatcher( 'baseline' )
1211                 ] );
1212                 $contentDistribution = new KeywordMatcher( [
1213                         'space-between', 'space-around', 'space-evenly', 'stretch'
1214                 ] );
1215                 $contentPosition = new KeywordMatcher( [
1216                         'center', 'start', 'end', 'flex-start', 'flex-end', 'left', 'right'
1217                 ] );
1218
1219                 $props['align-content'] = new Alternative( [
1220                         $normal,
1221                         $baselinePosition,
1222                         UnorderedGroup::someOf( [
1223                                 $contentDistribution,
1224                                 UnorderedGroup::allOf( [ $overflowPosition, $contentPosition ] ),
1225                         ] )
1226                 ] );
1227                 $props['justify-content'] = $props['align-content'];
1228                 $props['place-content'] = Quantifier::count( new Alternative( [
1229                         $normal,
1230                         $baselinePosition,
1231                         $contentDistribution,
1232                         $contentPosition,
1233                 ] ), 1, 2 );
1234                 $props['align-self'] = new Alternative( [
1235                         $autoNormalStretch,
1236                         $baselinePosition,
1237                         $overflowAndSelfPosition,
1238                 ] );
1239                 $props['justify-self'] = $props['align-self'];
1240                 $props['place-self'] = Quantifier::count( new Alternative( [
1241                         $autoNormalStretch,
1242                         $baselinePosition,
1243                         $selfPosition,
1244                 ] ), 1, 2 );
1245                 $props['align-items'] = new Alternative( [
1246                         $normalStretch,
1247                         $baselinePosition,
1248                         $overflowAndSelfPosition,
1249                 ] );
1250                 $props['justify-items'] = new Alternative( [
1251                         $autoNormalStretch,
1252                         $baselinePosition,
1253                         $overflowAndSelfPosition,
1254                         UnorderedGroup::allOf( [
1255                                 new KeywordMatcher( 'legacy' ),
1256                                 new KeywordMatcher( [ 'left', 'right', 'center' ] ),
1257                         ] ),
1258                 ] );
1259                 $props['place-items'] = new Juxtaposition( [
1260                         new Alternative( [ $normalStretch, $baselinePosition, $selfPosition ] ),
1261                         Quantifier::optional( new Alternative( [
1262                                 $autoNormalStretch, $baselinePosition, $selfPosition
1263                         ] ) ),
1264                 ] );
1265
1266                 $this->cache[__METHOD__] = $props;
1267                 return $props;
1268         }
1269
1270         /**
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
1275          */
1276         protected function cssBreak3( MatcherFactory $matcherFactory ) {
1277                 // @codeCoverageIgnoreStart
1278                 if ( isset( $this->cache[__METHOD__] ) ) {
1279                         return $this->cache[__METHOD__];
1280                 }
1281                 // @codeCoverageIgnoreEnd
1282
1283                 $props = [];
1284                 $props['break-before'] = new KeywordMatcher( [
1285                         'auto', 'avoid', 'avoid-page', 'page', 'left', 'right', 'recto', 'verso', 'avoid-column',
1286                         'column', 'avoid-region', 'region'
1287                 ] );
1288                 $props['break-after'] = $props['break-before'];
1289                 $props['break-inside'] = new KeywordMatcher( [
1290                         'auto', 'avoid', 'avoid-page', 'avoid-column', 'avoid-region'
1291                 ] );
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'
1297                 ] );
1298                 $props['page-break-after'] = $props['page-break-before'];
1299                 $props['page-break-inside'] = new KeywordMatcher( [ 'auto', 'avoid' ] );
1300
1301                 $this->cache[__METHOD__] = $props;
1302                 return $props;
1303         }
1304
1305         /**
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
1310          */
1311         protected function cssSpeech( MatcherFactory $matcherFactory ) {
1312                 // @codeCoverageIgnoreStart
1313                 if ( isset( $this->cache[__METHOD__] ) ) {
1314                         return $this->cache[__METHOD__];
1315                 }
1316                 // @codeCoverageIgnoreEnd
1317
1318                 $props = [];
1319                 $decibel = new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1320                         return !strcasecmp( $t->unit(), 'dB' );
1321                 } );
1322
1323                 $props['voice-volume'] = new Alternative( [
1324                         new KeywordMatcher( 'silent' ),
1325                         UnorderedGroup::someOf( [
1326                                 new KeywordMatcher( [ 'x-soft', 'soft', 'medium', 'loud', 'x-loud' ] ),
1327                                 $decibel
1328                         ] ),
1329                 ] );
1330                 $props['voice-balance'] = new Alternative( [
1331                         $matcherFactory->number(),
1332                         new KeywordMatcher( [ 'left', 'center', 'right', 'leftwards', 'rightwards' ] ),
1333                 ] );
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' ] ),
1341                         ] )
1342                 ] );
1343                 $props['pause-before'] = new Alternative( [
1344                         $matcherFactory->time(),
1345                         new KeywordMatcher( [ 'none', 'x-weak', 'weak', 'medium', 'strong', 'x-strong' ] ),
1346                 ] );
1347                 $props['pause-after'] = $props['pause-before'];
1348                 $props['pause'] = new Juxtaposition( [
1349                         $props['pause-before'],
1350                         Quantifier::optional( $props['pause-after'] )
1351                 ] );
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' )
1358                 ] );
1359                 $props['cue-after'] = $props['cue-before'];
1360                 $props['cue'] = new Juxtaposition( [
1361                         $props['cue-before'],
1362                         Quantifier::optional( $props['cue-after'] )
1363                 ] );
1364                 $props['voice-family'] = new Alternative( [
1365                         Quantifier::hash( new Alternative( [
1366                                 new Alternative( [ // <name>
1367                                         $matcherFactory->string(),
1368                                         Quantifier::plus( $matcherFactory->ident() ),
1369                                 ] ),
1370                                 new Juxtaposition( [ // <generic-voice>
1371                                         Quantifier::optional( new KeywordMatcher( [ 'child', 'young', 'old' ] ) ),
1372                                         new KeywordMatcher( [ 'male', 'female', 'neutral' ] ),
1373                                         Quantifier::optional( $matcherFactory->integer() ),
1374                                 ] ),
1375                         ] ) ),
1376                         new KeywordMatcher( 'preserve' )
1377                 ] );
1378                 $props['voice-rate'] = UnorderedGroup::someOf( [
1379                         new KeywordMatcher( [ 'normal', 'x-slow', 'slow', 'medium', 'fast', 'x-fast' ] ),
1380                         $matcherFactory->percentage()
1381                 ] );
1382                 $props['voice-pitch'] = new Alternative( [
1383                         UnorderedGroup::allOf( [
1384                                 new KeywordMatcher( 'absolute' ),
1385                                 $matcherFactory->frequency(),
1386                         ] ),
1387                         UnorderedGroup::someOf( [
1388                                 new KeywordMatcher( [ 'x-low', 'low', 'medium', 'high', 'x-high' ] ),
1389                                 new Alternative( [
1390                                         $matcherFactory->frequency(),
1391                                         new TokenMatcher( Token::T_DIMENSION, function ( Token $t ) {
1392                                                 return !strcasecmp( $t->unit(), 'st' );
1393                                         } ),
1394                                         $matcherFactory->percentage()
1395                                 ] ),
1396                         ] ),
1397                 ] );
1398                 $props['voice-range'] = $props['voice-pitch'];
1399                 $props['voice-stress'] = new KeywordMatcher( [
1400                         'normal', 'strong', 'moderate', 'none', 'reduced'
1401                 ] );
1402                 $props['voice-duration'] = new Alternative( [
1403                         new KeywordMatcher( 'auto' ),
1404                         $matcherFactory->time()
1405                 ] );
1406
1407                 $this->cache[__METHOD__] = $props;
1408                 return $props;
1409         }
1410
1411         /**
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
1416          */
1417         protected function cssGrid1( MatcherFactory $matcherFactory ) {
1418                 // @codeCoverageIgnoreStart
1419                 if ( isset( $this->cache[__METHOD__] ) ) {
1420                         return $this->cache[__METHOD__];
1421                 }
1422                 // @codeCoverageIgnoreEnd
1423
1424                 $props = [];
1425                 $comma = $matcherFactory->comma();
1426                 $slash = new DelimMatcher( '/' );
1427                 $lineNamesO = Quantifier::optional( new BlockMatcher(
1428                         Token::T_LEFT_BRACKET, Quantifier::star( $matcherFactory->ident() )
1429                 ) );
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' );
1434                         } ),
1435                         new KeywordMatcher( [ 'min-content', 'max-content', 'auto' ] )
1436                 ] );
1437                 $inflexibleBreadth = new Alternative( [
1438                         $matcherFactory->lengthPercentage(),
1439                         new KeywordMatcher( [ 'min-content', 'max-content', 'auto' ] )
1440                 ] );
1441                 $fixedBreadth = $matcherFactory->lengthPercentage();
1442                 $trackSize = new Alternative( [
1443                         $trackBreadth,
1444                         new FunctionMatcher( 'minmax',
1445                                 new Juxtaposition( [ $inflexibleBreadth, $trackBreadth ], true )
1446                         ),
1447                         new FunctionMatcher( 'fit-content', $matcherFactory->lengthPercentage() )
1448                 ] );
1449                 $fixedSize = new Alternative( [
1450                         $fixedBreadth,
1451                         new FunctionMatcher( 'minmax', new Juxtaposition( [ $fixedBreadth, $trackBreadth ], true ) ),
1452                         new FunctionMatcher( 'minmax',
1453                                 new Juxtaposition( [ $inflexibleBreadth, $fixedBreadth ], true )
1454                         ),
1455                 ] );
1456                 $trackRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [
1457                         $matcherFactory->integer(),
1458                         $comma,
1459                         Quantifier::plus( new Juxtaposition( [ $lineNamesO, $trackSize ] ) ),
1460                         $lineNamesO
1461                 ] ) );
1462                 $autoRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [
1463                         new KeywordMatcher( [ 'auto-fill', 'auto-fit' ] ),
1464                         $comma,
1465                         Quantifier::plus( new Juxtaposition( [ $lineNamesO, $fixedSize ] ) ),
1466                         $lineNamesO
1467                 ] ) );
1468                 $fixedRepeat = new FunctionMatcher( 'repeat', new Juxtaposition( [
1469                         $matcherFactory->integer(),
1470                         $comma,
1471                         Quantifier::plus( new Juxtaposition( [ $lineNamesO, $fixedSize ] ) ),
1472                         $lineNamesO
1473                 ] ) );
1474                 $trackList = new Juxtaposition( [
1475                         Quantifier::plus( new Juxtaposition( [
1476                                 $lineNamesO, new Alternative( [ $trackSize, $trackRepeat ] )
1477                         ] ) ),
1478                         $lineNamesO
1479                 ] );
1480                 $autoTrackList = new Juxtaposition( [
1481                         Quantifier::star( new Juxtaposition( [
1482                                 $lineNamesO, new Alternative( [ $fixedSize, $fixedRepeat ] )
1483                         ] ) ),
1484                         $lineNamesO,
1485                         $autoRepeat,
1486                         Quantifier::star( new Juxtaposition( [
1487                                 $lineNamesO, new Alternative( [ $fixedSize, $fixedRepeat ] )
1488                         ] ) ),
1489                         $lineNamesO,
1490                 ] );
1491                 $explicitTrackList = new Juxtaposition( [
1492                         Quantifier::plus( new Juxtaposition( [ $lineNamesO, $trackSize ] ) ),
1493                         $lineNamesO
1494                 ] );
1495                 $autoDense = UnorderedGroup::allOf( [
1496                         new KeywordMatcher( 'auto-flow' ),
1497                         Quantifier::optional( new KeywordMatcher( 'dense' ) )
1498                 ] );
1499
1500                 $props['grid-template-columns'] = new Alternative( [
1501                         new KeywordMatcher( 'none' ), $trackList, $autoTrackList
1502                 ] );
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() ),
1507                 ] );
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
1514                                 ] ) ),
1515                                 Quantifier::optional( new Juxtaposition( [ $slash, $explicitTrackList ] ) ),
1516                         ] )
1517                 ] );
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' )
1523                 ] );
1524                 $props['grid'] = new Alternative( [
1525                         $props['grid-template'],
1526                         new Juxtaposition( [
1527                                 $props['grid-template-rows'],
1528                                 $slash,
1529                                 $autoDense,
1530                                 Quantifier::optional( $props['grid-auto-columns'] ),
1531                         ] ),
1532                         new Juxtaposition( [
1533                                 $autoDense,
1534                                 Quantifier::optional( $props['grid-auto-rows'] ),
1535                                 $slash,
1536                                 $props['grid-template-columns'],
1537                         ] )
1538                 ] );
1539
1540                 $gridLine = new Alternative( [
1541                         new KeywordMatcher( 'auto' ),
1542                         $matcherFactory->ident(),
1543                         UnorderedGroup::allOf( [
1544                                 $matcherFactory->integer(),
1545                                 Quantifier::optional( $matcherFactory->ident() )
1546                         ] ),
1547                         UnorderedGroup::allOf( [
1548                                 new KeywordMatcher( 'span' ),
1549                                 UnorderedGroup::someOf( [
1550                                         $matcherFactory->integer(),
1551                                         $matcherFactory->ident(),
1552                                 ] )
1553                         ] )
1554                 ] );
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 ] ) )
1561                 ] );
1562                 $props['grid-column'] = $props['grid-row'];
1563                 $props['grid-area'] = new Juxtaposition( [
1564                         $gridLine, Quantifier::count( new Juxtaposition( [ $slash, $gridLine ] ), 0, 3 )
1565                 ] );
1566
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'] )
1571                 ] );
1572
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'];
1576
1577                 $this->cache[__METHOD__] = $props;
1578                 return $props;
1579         }
1580
1581         /**
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
1586          */
1587         protected function cssFilter1( MatcherFactory $matcherFactory ) {
1588                 // @codeCoverageIgnoreStart
1589                 if ( isset( $this->cache[__METHOD__] ) ) {
1590                         return $this->cache[__METHOD__];
1591                 }
1592                 // @codeCoverageIgnoreEnd
1593
1594                 $props = [];
1595
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() ),
1605                                 ] ) ),
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' ),
1613                         ] ) )
1614                 ] );
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();
1619
1620                 $this->cache[__METHOD__] = $props;
1621                 return $props;
1622         }
1623
1624         /**
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
1628          * @return Matcher
1629          */
1630         protected function basicShapes( MatcherFactory $matcherFactory ) {
1631                 // @codeCoverageIgnoreStart
1632                 if ( isset( $this->cache[__METHOD__] ) ) {
1633                         return $this->cache[__METHOD__];
1634                 }
1635                 // @codeCoverageIgnoreEnd
1636
1637                 $border = $this->cssBorderBackground3( $matcherFactory );
1638                 $sa = $matcherFactory->lengthPercentage();
1639                 $sr = new Alternative( [
1640                         $sa,
1641                         new KeywordMatcher( [ 'closest-side', 'farthest-side' ] ),
1642                 ] );
1643
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']
1649                                 ] ) )
1650                         ] ) ),
1651                         new FunctionMatcher( 'circle', new Juxtaposition( [
1652                                 Quantifier::optional( $sr ),
1653                                 Quantifier::optional( new Juxtaposition( [
1654                                         new KeywordMatcher( 'at' ), $matcherFactory->position()
1655                                 ] ) )
1656                         ] ) ),
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()
1661                                 ] ) )
1662                         ] ) ),
1663                         new FunctionMatcher( 'polygon', new Juxtaposition( [
1664                                 Quantifier::optional( new KeywordMatcher( [ 'nonzero', 'evenodd' ] ) ),
1665                                 Quantifier::hash( Quantifier::count( $sa, 2, 2 ) ),
1666                         ], true ) ),
1667                 ] );
1668
1669                 $this->cache[__METHOD__] = $basicShape;
1670                 return $basicShape;
1671         }
1672
1673         /**
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
1678          */
1679         protected function cssShapes1( MatcherFactory $matcherFactory ) {
1680                 // @codeCoverageIgnoreStart
1681                 if ( isset( $this->cache[__METHOD__] ) ) {
1682                         return $this->cache[__METHOD__];
1683                 }
1684                 // @codeCoverageIgnoreEnd
1685
1686                 $shapeBoxKW = $this->backgroundTypes( $matcherFactory )['boxKeywords'];
1687                 $shapeBoxKW[] = 'margin-box';
1688
1689                 $props = [];
1690
1691                 $props['shape-outside'] = new Alternative( [
1692                         new KeywordMatcher( 'none' ),
1693                         UnorderedGroup::someOf( [
1694                                 $this->basicShapes( $matcherFactory ),
1695                                 new KeywordMatcher( $shapeBoxKW ),
1696                         ] ),
1697                         $matcherFactory->url( 'image' ),
1698                 ] );
1699                 $props['shape-image-threshold'] = $matcherFactory->number();
1700                 $props['shape-margin'] = $matcherFactory->lengthPercentage();
1701
1702                 $this->cache[__METHOD__] = $props;
1703                 return $props;
1704         }
1705
1706         /**
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
1711          */
1712         protected function cssMasking1( MatcherFactory $matcherFactory ) {
1713                 // @codeCoverageIgnoreStart
1714                 if ( isset( $this->cache[__METHOD__] ) ) {
1715                         return $this->cache[__METHOD__];
1716                 }
1717                 // @codeCoverageIgnoreEnd
1718
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'
1724                 ] );
1725                 $geometryBox = new KeywordMatcher( $geometryBoxKeywords );
1726                 $maskRef = new Alternative( [
1727                         new KeywordMatcher( 'none' ),
1728                         $matcherFactory->image(),
1729                         $matcherFactory->url( 'svg' ),
1730                 ] );
1731                 $maskMode = new KeywordMatcher( [ 'alpha', 'luminance', 'auto' ] );
1732                 $maskClip = new KeywordMatcher( array_merge( $geometryBoxKeywords, [ 'no-clip' ] ) );
1733                 $maskComposite = new KeywordMatcher( [ 'add', 'subtract', 'intersect', 'exclude' ] );
1734
1735                 $props = [];
1736
1737                 $props['clip-path'] = new Alternative( [
1738                         $matcherFactory->url( 'svg' ),
1739                         UnorderedGroup::someOf( [
1740                                 $this->basicShapes( $matcherFactory ),
1741                                 $geometryBox,
1742                         ] ),
1743                         new KeywordMatcher( 'none' ),
1744                 ] );
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'] ] ) ),
1759                         ] ),
1760                         $bgtypes['bgrepeat'],
1761                         $geometryBox,
1762                         $maskClip,
1763                         $maskComposite,
1764                 ] ) );
1765                 $props['mask-border-source'] = new Alternative( [
1766                         new KeywordMatcher( 'none' ),
1767                         $matcherFactory->image(),
1768                 ] );
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' ) ),
1773                 ] );
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( [
1782                                         $slash,
1783                                         Quantifier::optional( $props['mask-border-width'] ),
1784                                         Quantifier::optional( new Juxtaposition( [
1785                                                 $slash,
1786                                                 $props['mask-border-outset'],
1787                                         ] ) ),
1788                                 ] ) ),
1789                         ] ),
1790                         $props['mask-border-repeat'],
1791                         $props['mask-border-mode'],
1792                 ] );
1793                 $props['mask-type'] = new KeywordMatcher( [ 'luminance', 'alpha' ] );
1794
1795                 $this->cache[__METHOD__] = $props;
1796                 return $props;
1797         }
1798 }