Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
1191 / 1191 |
|
100.00% |
29 / 29 |
CRAP | |
100.00% |
1 / 1 |
StylePropertySanitizer | |
100.00% |
1191 / 1191 |
|
100.00% |
29 / 29 |
58 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
1 | |||
css2 | |
100.00% |
82 / 82 |
|
100.00% |
1 / 1 |
2 | |||
cssDisplay3 | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
2 | |||
cssPosition3 | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
2 | |||
cssColor3 | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
backgroundTypes | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
cssBorderBackground3 | |
100.00% |
124 / 124 |
|
100.00% |
1 / 1 |
2 | |||
cssImages3 | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
2 | |||
cssFonts3 | |
100.00% |
66 / 66 |
|
100.00% |
1 / 1 |
2 | |||
cssMulticol | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
2 | |||
cssOverflow3 | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
cssUI4 | |
100.00% |
57 / 57 |
|
100.00% |
1 / 1 |
2 | |||
cssCompositing1 | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
cssWritingModes4 | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
2 | |||
cssTransitions | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
2 | |||
cssAnimations | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
2 | |||
cssFlexbox3 | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
2 | |||
cssTransforms1 | |
100.00% |
49 / 49 |
|
100.00% |
1 / 1 |
2 | |||
cssText3 | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
2 | |||
cssTextDecor3 | |
100.00% |
52 / 52 |
|
100.00% |
1 / 1 |
2 | |||
cssAlign3 | |
100.00% |
84 / 84 |
|
100.00% |
1 / 1 |
2 | |||
cssBreak3 | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
2 | |||
cssGrid1 | |
100.00% |
157 / 157 |
|
100.00% |
1 / 1 |
3 | |||
cssFilter1 | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
2 | |||
basicShapes | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
2 | |||
cssShapes1 | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
cssMasking1 | |
100.00% |
75 / 75 |
|
100.00% |
1 / 1 |
2 | |||
getSizingAdditions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
cssSizing3 | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 |
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 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 | } |