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