PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / csstidy / class.csstidy_optimise.php

class.csstidy_optimise.php in TablePress – Tables in WordPress made easy 3.4, at libraries/csstidy/class.csstidy_optimise.php

1,087 lines 33.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CSSTidy Optimising PHP Class
4 *
5 * @package TablePress
6 * @subpackage CSS
7 * @author Florian Schmitz, Brett Zamir, Nikolay Matsievsky, Cedric Morin, Christopher Finke, Mark Scherer, Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 // Prohibit direct script loading.
14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
15
16 /**
17 * CSS Optimising Class
18 *
19 * This class optimises CSS data generated by CSSTidy.
20 *
21 * @package CSSTidy
22 * @version 1.0
23 */
24 class TablePress_CSSTidy_Optimise {
25
26 /**
27 * TablePress_CSSTidy instance.
28 *
29 * @since 1.0.0
30 */
31 public \TablePress_CSSTidy $parser;
32
33 /**
34 * The parsed CSS.
35 *
36 * @since 1.0.0
37 */
38 public array $css = array();
39
40 /**
41 * The current sub-value.
42 *
43 * @since 1.0.0
44 */
45 public string $sub_value = '';
46
47 /**
48 * The current at rule (@media).
49 *
50 * @since 1.0.0
51 */
52 public string $at = '';
53
54 /**
55 * The current selector.
56 *
57 * @since 1.0.0
58 */
59 public string $selector = '';
60
61 /**
62 * The current property.
63 *
64 * @since 1.0.0
65 */
66 public string $property = '';
67
68 /**
69 * The current value.
70 *
71 * @since 1.0.0
72 */
73 public string $value = '';
74
75 /**
76 * Constructor.
77 *
78 * @since 1.0.0
79 *
80 * @param TablePress_CSSTidy $csstidy Instance of the TablePress_CSSTidy class.
81 */
82 public function __construct( TablePress_CSSTidy $csstidy ) {
83 $this->parser = $csstidy;
84 $this->css = &$csstidy->css;
85 $this->sub_value = &$csstidy->sub_value;
86 $this->at = &$csstidy->at;
87 $this->selector = &$csstidy->selector;
88 $this->property = &$csstidy->property;
89 $this->value = &$csstidy->value;
90 }
91
92 /**
93 * Optimises $css after parsing.
94 *
95 * @since 1.0.0
96 */
97 public function postparse(): void {
98 if ( $this->parser->get_cfg( 'preserve_css' ) ) {
99 return;
100 }
101
102 if ( 2 === (int) $this->parser->get_cfg( 'merge_selectors' ) ) {
103 foreach ( $this->css as $medium => $value ) {
104 if ( is_array( $value ) ) {
105 $this->merge_selectors( $this->css[ $medium ] );
106 }
107 }
108 }
109
110 if ( $this->parser->get_cfg( 'discard_invalid_selectors' ) ) {
111 foreach ( $this->css as $medium => $value ) {
112 if ( is_array( $value ) ) {
113 $this->discard_invalid_selectors( $this->css[ $medium ] );
114 }
115 }
116 }
117
118 if ( $this->parser->get_cfg( 'optimise_shorthands' ) > 0 ) {
119 foreach ( $this->css as $medium => $value ) {
120 if ( is_array( $value ) ) {
121 foreach ( $value as $selector => $value1 ) {
122 $this->css[ $medium ][ $selector ] = $this->merge_4value_shorthands( $this->css[ $medium ][ $selector ] );
123
124 if ( $this->parser->get_cfg( 'optimise_shorthands' ) < 2 ) {
125 continue;
126 }
127
128 $this->css[ $medium ][ $selector ] = $this->merge_font( $this->css[ $medium ][ $selector ] );
129
130 if ( $this->parser->get_cfg( 'optimise_shorthands' ) < 3 ) {
131 continue;
132 }
133
134 $this->css[ $medium ][ $selector ] = $this->merge_bg( $this->css[ $medium ][ $selector ] );
135 if ( empty( $this->css[ $medium ][ $selector ] ) ) {
136 unset( $this->css[ $medium ][ $selector ] );
137 }
138 }
139 }
140 }
141 }
142 }
143
144 /**
145 * Optimises values
146 *
147 * @since 1.0.0
148 */
149 public function value(): void {
150 $shorthands = &$this->parser->data['csstidy']['shorthands'];
151
152 // Optimise shorthand properties.
153 if ( isset( $shorthands[ $this->property ] ) && $this->parser->get_cfg( 'optimise_shorthands' ) > 0 ) {
154 $temp = $this->shorthand( $this->value ); // FIXME - move.
155 if ( $temp !== $this->value ) {
156 $this->parser->log( 'Optimised shorthand notation (' . $this->property . '): Changed "' . $this->value . '" to "' . $temp . '"', 'Information' );
157 }
158 $this->value = $temp;
159 }
160
161 // Remove whitespace at !important.
162 if ( $this->value !== $this->compress_important( $this->value ) ) {
163 $this->parser->log( 'Optimised !important', 'Information' );
164 }
165 }
166
167 /**
168 * Optimises shorthands.
169 *
170 * @since 1.0.0
171 */
172 public function shorthands(): void {
173 $shorthands = &$this->parser->data['csstidy']['shorthands'];
174
175 if ( ! $this->parser->get_cfg( 'optimise_shorthands' ) || $this->parser->get_cfg( 'preserve_css' ) ) {
176 return;
177 }
178
179 if ( 'font' === $this->property && $this->parser->get_cfg( 'optimise_shorthands' ) > 1 ) {
180 $this->css[ $this->at ][ $this->selector ]['font'] = '';
181 $this->parser->merge_css_blocks( $this->at, $this->selector, $this->dissolve_short_font( $this->value ) );
182 }
183 if ( 'background' === $this->property && $this->parser->get_cfg( 'optimise_shorthands' ) > 2 ) {
184 $this->css[ $this->at ][ $this->selector ]['background'] = '';
185 $this->parser->merge_css_blocks( $this->at, $this->selector, $this->dissolve_short_bg( $this->value ) );
186 }
187 if ( isset( $shorthands[ $this->property ] ) ) {
188 $this->parser->merge_css_blocks( $this->at, $this->selector, $this->dissolve_4value_shorthands( $this->property, $this->value ) );
189 if ( is_array( $shorthands[ $this->property ] ) ) {
190 $this->css[ $this->at ][ $this->selector ][ $this->property ] = '';
191 }
192 }
193 }
194
195 /**
196 * Optimises a sub-value.
197 *
198 * @since 1.0.0
199 */
200 public function subvalue(): void {
201 $replace_colors = &$this->parser->data['csstidy']['replace_colors'];
202
203 $this->sub_value = trim( $this->sub_value );
204 if ( '' === $this->sub_value ) { // caution : '0'.
205 return;
206 }
207
208 $important = '';
209 if ( $this->parser->is_important( $this->sub_value ) ) {
210 $important = ' !important';
211 }
212 $this->sub_value = $this->parser->gvw_important( $this->sub_value );
213
214 // Compress font-weight.
215 if ( 'font-weight' === $this->property && $this->parser->get_cfg( 'compress_font-weight' ) ) {
216 if ( 'bold' === $this->sub_value ) {
217 $this->sub_value = '700';
218 $this->parser->log( 'Optimised font-weight: Changed "bold" to "700"', 'Information' );
219 } elseif ( 'normal' === $this->sub_value ) {
220 $this->sub_value = '400';
221 $this->parser->log( 'Optimised font-weight: Changed "normal" to "400"', 'Information' );
222 }
223 }
224
225 $temp = $this->compress_numbers( $this->sub_value );
226 if ( 0 !== strcasecmp( $temp, $this->sub_value ) ) {
227 if ( strlen( $temp ) > strlen( $this->sub_value ) ) {
228 $this->parser->log( 'Fixed invalid number: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Warning' );
229 } else {
230 $this->parser->log( 'Optimised number: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Information' );
231 }
232 $this->sub_value = $temp;
233 }
234 if ( $this->parser->get_cfg( 'compress_colors' ) ) {
235 $temp = $this->cut_color( $this->sub_value );
236 if ( $temp !== $this->sub_value ) {
237 if ( isset( $replace_colors[ $this->sub_value ] ) ) {
238 $this->parser->log( 'Fixed invalid color name: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Warning' );
239 } else {
240 $this->parser->log( 'Optimised color: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Information' );
241 }
242 $this->sub_value = $temp;
243 }
244 }
245 $this->sub_value .= $important;
246 }
247
248 /**
249 * Compresses shorthand values.
250 *
251 * Example: `margin: 1px 1px 1px 1px` will become `margin: 1px`.
252 *
253 * @since 1.0.0
254 *
255 * @param string $value Shorthand value.
256 * @return string Compressed value.
257 */
258 public function shorthand( string $value ): string {
259 $important = '';
260 if ( $this->parser->is_important( $value ) ) {
261 $values = $this->parser->gvw_important( $value );
262 $important = ' !important';
263 } else {
264 $values = $value;
265 }
266
267 $values = explode( ' ', $values );
268 switch ( count( $values ) ) {
269 case 4:
270 if ( $values[0] === $values[1] && $values[0] === $values[2] && $values[0] === $values[3] ) {
271 return $values[0] . $important;
272 } elseif ( $values[1] === $values[3] && $values[0] === $values[2] ) {
273 return $values[0] . ' ' . $values[1] . $important;
274 } elseif ( $values[1] === $values[3] ) {
275 return $values[0] . ' ' . $values[1] . ' ' . $values[2] . $important;
276 }
277 break;
278 case 3:
279 if ( $values[0] === $values[1] && $values[0] === $values[2] ) {
280 return $values[0] . $important;
281 } elseif ( $values[0] === $values[2] ) {
282 return $values[0] . ' ' . $values[1] . $important;
283 }
284 break;
285 case 2:
286 if ( $values[0] === $values[1] ) {
287 return $values[0] . $important;
288 }
289 break;
290 }
291
292 return $value;
293 }
294
295 /**
296 * Removes unnecessary whitespace in ! important.
297 *
298 * @since 1.0.0
299 *
300 * @param string $a_string String.
301 * @return string Cleaned string.
302 */
303 public function compress_important( string &$a_string ): string {
304 if ( $this->parser->is_important( $a_string ) ) {
305 $a_string = $this->parser->gvw_important( $a_string ) . ' !important';
306 }
307 return $a_string;
308 }
309
310 /**
311 * Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
312 *
313 * @since 1.0.0
314 *
315 * @param string $color Color value.
316 * @return string Compressed color.
317 */
318 public function cut_color( string $color ): string {
319 $replace_colors = &$this->parser->data['csstidy']['replace_colors'];
320
321 // If it's a string, don't touch it!
322 if ( str_starts_with( $color, "'" ) || str_starts_with( $color, '"' ) ) {
323 return $color;
324 }
325
326 // Complex gradient expressions.
327 if ( str_contains( $color, '(' ) && 0 !== strncasecmp( $color, 'rgb(', 4 ) && 0 !== strncasecmp( $color, 'rgba(', 5 ) ) {
328 // Don't touch properties within MSIE filters, those are too sensitive.
329 if ( false !== stripos( $color, 'progid:' ) ) {
330 return $color;
331 }
332 preg_match_all( ',rgba?\([^)]+\),i', $color, $matches, PREG_SET_ORDER );
333 if ( count( $matches ) ) {
334 foreach ( $matches as $m ) {
335 $color = str_replace( $m[0], $this->cut_color( $m[0] ), $color );
336 }
337 }
338 preg_match_all( ',#[0-9a-f]{6}(?=[^0-9a-f]),i', $color, $matches, PREG_SET_ORDER );
339 if ( count( $matches ) ) {
340 foreach ( $matches as $m ) {
341 $color = str_replace( $m[0], $this->cut_color( $m[0] ), $color );
342 }
343 }
344 return $color;
345 }
346
347 // rgb(0,0,0) -> #000000 (or #000 in this case later).
348 if (
349 // Be sure to not corrupt a rgb with calc() value.
350 ( 0 === strncasecmp( $color, 'rgb(', 4 ) && false === strpos( $color, '(', 4 ) ) || ( 0 === strncasecmp( $color, 'rgba(', 5 ) && false === strpos( $color, '(', 5 ) )
351 ) {
352 $color_tmp = explode( '(', $color, 2 );
353 $color_tmp = rtrim( end( $color_tmp ), ')' );
354 if ( str_contains( $color_tmp, '/' ) ) {
355 $color_tmp = explode( '/', $color_tmp, 2 );
356 $color_parts = explode( ' ', trim( reset( $color_tmp ) ), 3 );
357 while ( count( $color_parts ) < 3 ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
358 $color_parts[] = 0;
359 }
360 $color_parts[] = end( $color_tmp );
361 } else {
362 $color_parts = explode( ',', $color_tmp, 4 );
363 }
364 $color_parts_count = count( $color_parts );
365 for ( $i = 0; $i < $color_parts_count; $i++ ) {
366 $color_parts[ $i ] = trim( $color_parts[ $i ] );
367 if ( str_ends_with( $color_parts[ $i ], '%' ) ) {
368 $color_parts[ $i ] = round( ( 255 * intval( $color_parts[ $i ] ) ) / 100 );
369 } elseif ( $i > 2 ) {
370 // 4th argument is alpha layer between 0 and 1 (if not %).
371 $color_parts[ $i ] = round( 255 * floatval( $color_parts[ $i ] ) );
372 }
373 $color_parts[ $i ] = intval( $color_parts[ $i ] );
374 if ( $color_parts[ $i ] > 255 ) {
375 $color_parts[ $i ] = 255;
376 }
377 }
378 $color = '#';
379 // 3 or 4 parts depending on alpha layer.
380 $nb = min( max( count( $color_parts ), 3 ), 4 );
381 for ( $i = 0; $i < $nb; $i++ ) {
382 if ( ! isset( $color_parts[ $i ] ) ) {
383 $color_parts[ $i ] = 0;
384 }
385 if ( $color_parts[ $i ] < 16 ) {
386 $color .= '0' . dechex( $color_parts[ $i ] );
387 } else {
388 $color .= dechex( $color_parts[ $i ] );
389 }
390 }
391 }
392
393 // Fix bad color names.
394 if ( isset( $replace_colors[ strtolower( $color ) ] ) ) {
395 $color = $replace_colors[ strtolower( $color ) ];
396 }
397
398 if ( 7 === strlen( $color ) ) {
399 // #aabbcc -> #abc
400 $color_temp = strtolower( $color );
401 if ( '#' === $color_temp[0] && $color_temp[1] === $color_temp[2] && $color_temp[3] === $color_temp[4] && $color_temp[5] === $color_temp[6] ) {
402 $color = '#' . $color[1] . $color[3] . $color[5];
403 }
404 } elseif ( 9 === strlen( $color ) ) {
405 // #aabbccdd -> #abcd
406 $color_temp = strtolower( $color );
407 if ( '#' === $color_temp[0] && $color_temp[1] === $color_temp[2] && $color_temp[3] === $color_temp[4] && $color_temp[5] === $color_temp[6] && $color_temp[7] === $color_temp[8] ) {
408 $color = '#' . $color[1] . $color[3] . $color[5] . $color[7];
409 }
410 }
411
412 switch ( strtolower( $color ) ) {
413 /* color name -> hex code */
414 case 'black':
415 return '#000';
416 case 'fuchsia':
417 return '#f0f';
418 case 'white':
419 return '#fff';
420 case 'yellow':
421 return '#ff0';
422
423 /* hex code -> color name */
424 case '#800000':
425 return 'maroon';
426 case '#ffa500':
427 return 'orange';
428 case '#808000':
429 return 'olive';
430 case '#800080':
431 return 'purple';
432 case '#008000':
433 return 'green';
434 case '#000080':
435 return 'navy';
436 case '#008080':
437 return 'teal';
438 case '#c0c0c0':
439 return 'silver';
440 case '#808080':
441 return 'gray';
442 case '#f00':
443 return 'red';
444 }
445
446 return $color;
447 }
448
449 /**
450 * Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1).
451 *
452 * @since 1.0.0
453 *
454 * @param string $subvalue Value.
455 * @return string Compressed value.
456 */
457 public function compress_numbers( string $subvalue ): string {
458 $unit_values = &$this->parser->data['csstidy']['unit_values'];
459 $color_values = &$this->parser->data['csstidy']['color_values'];
460
461 // for font:1em/1em sans-serif...;.
462 if ( 'font' === $this->property ) {
463 $temp = explode( '/', $subvalue );
464 } else {
465 $temp = array( $subvalue );
466 }
467
468 $temp_count = count( $temp );
469 for ( $l = 0; $l < $temp_count; $l++ ) {
470 // If we are not dealing with a number at this point, do not optimize anything.
471 $number = $this->analyse_css_number( $temp[ $l ] );
472 if ( false === $number ) {
473 return $subvalue;
474 }
475
476 // Fix bad colors.
477 if ( in_array( $this->property, $color_values, true ) ) {
478 if ( 3 === strlen( $temp[ $l ] ) || 6 === strlen( $temp[ $l ] ) ) {
479 $temp[ $l ] = '#' . $temp[ $l ];
480 } else {
481 $temp[ $l ] = '0';
482 }
483 continue;
484 }
485
486 if ( abs( $number[0] ) > 0 ) {
487 if ( '' === $number[1] && in_array( $this->property, $unit_values, true ) ) {
488 $number[1] = 'px';
489 }
490 } elseif ( 's' !== $number[1] && 'ms' !== $number[1] ) {
491 $number[1] = '';
492 }
493
494 $temp[ $l ] = $number[0] . $number[1];
495 }
496
497 return ( count( $temp ) > 1 ) ? $temp[0] . '/' . $temp[1] : $temp[0];
498 }
499
500 /**
501 * Checks if a given string is a CSS valid number. If it is, an array containing the value and unit is returned.
502 *
503 * @since 1.0.0
504 *
505 * @param string $a_string String.
506 * @return array{int|string, string}|false ('unit' if unit is found or '' if no unit exists, number value) or false if no number.
507 */
508 public function analyse_css_number( string $a_string ) /* : array|false */ {
509 // Most simple checks first.
510 if ( 0 === strlen( $a_string ) || ctype_alpha( $a_string[0] ) ) {
511 return false;
512 }
513
514 $units = &$this->parser->data['csstidy']['units'];
515 $return = array( 0, '' );
516
517 $return[0] = (float) $a_string;
518 if ( abs( $return[0] ) > 0 && abs( $return[0] ) < 1 ) {
519 if ( $return[0] < 0 ) {
520 $return[0] = '-' . ltrim( substr( (string) $return[0], 1 ), '0' );
521 } else {
522 $return[0] = ltrim( (string) $return[0], '0' );
523 }
524 }
525
526 // Look for unit and split from value if exists.
527 foreach ( $units as $unit ) {
528 $expect_unit_at = strlen( $a_string ) - strlen( $unit );
529 if ( ! ( $unit_in_string = stristr( $a_string, $unit ) ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found,Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
530 continue;
531 }
532 $actual_position = strpos( $a_string, $unit_in_string );
533 if ( $expect_unit_at === $actual_position ) {
534 $return[1] = $unit;
535 $a_string = substr( $a_string, 0, - strlen( $unit ) );
536 break;
537 }
538 }
539 if ( ! is_numeric( $a_string ) ) {
540 return false;
541 }
542 return $return;
543 }
544
545 /**
546 * Merges selectors with same properties. Example: a{color:red} b{color:red} -> a,b{color:red}
547 * Very basic and has at least one bug. Hopefully there is a replacement soon.
548 *
549 * @since 1.0.0
550 *
551 * @param array<string, mixed> $an_array List of selectors. This parameter is modified by reference.
552 */
553 public function merge_selectors( array &$an_array ): void {
554 $css = $an_array;
555 foreach ( $css as $key => $value ) {
556 if ( ! isset( $css[ $key ] ) ) {
557 continue;
558 }
559
560 // Check if properties also exist in another selector.
561 $keys = array();
562 // PHP bug (?) without $css = $an_array; here.
563 foreach ( $css as $selector => $vali ) {
564 if ( $selector === $key ) {
565 continue;
566 }
567
568 if ( $css[ $key ] === $vali ) {
569 $keys[] = $selector;
570 }
571 }
572
573 if ( ! empty( $keys ) ) {
574 $newsel = $key;
575 unset( $css[ $key ] );
576 foreach ( $keys as $selector ) {
577 unset( $css[ $selector ] );
578 $newsel .= ',' . $selector;
579 }
580 $css[ $newsel ] = $value;
581 }
582 }
583 $an_array = $css;
584 }
585
586 /**
587 * Removes invalid selectors and their corresponding rule-sets as
588 * defined by 4.1.7 in REC-CSS2. This is a very rudimentary check
589 * and should be replaced by a full-blown parsing algorithm or
590 * regular expression.
591 *
592 * @since 1.0.0
593 *
594 * @param array<string, mixed> $an_array [description].
595 */
596 public function discard_invalid_selectors( array &$an_array ): void {
597 foreach ( $an_array as $selector => $decls ) {
598 $ok = true;
599 $selectors = array_map( 'trim', explode( ',', $selector ) );
600 foreach ( $selectors as $s ) {
601 $simple_selectors = preg_split( '/\s*[+>~\s]\s*/', $s );
602 foreach ( $simple_selectors as $ss ) {
603 if ( '' === $ss ) {
604 $ok = false;
605 }
606 // Could also check $ss for internal structure, but that probably would be too slow.
607 }
608 }
609 if ( ! $ok ) {
610 unset( $an_array[ $selector ] );
611 }
612 }
613 }
614
615 /**
616 * Dissolves properties like padding:10px 10px 10px to padding-top:10px;padding-bottom:10px;...
617 *
618 * @since 1.0.0
619 *
620 * @param string $property [description].
621 * @param string $value [description].
622 * @return array [description]
623 */
624 public function dissolve_4value_shorthands( string $property, string $value ): array {
625 $return = array();
626
627 $shorthands = &$this->parser->data['csstidy']['shorthands'];
628 if ( ! is_array( $shorthands[ $property ] ) ) {
629 $return[ $property ] = $value;
630 return $return;
631 }
632
633 $important = '';
634 if ( $this->parser->is_important( $value ) ) {
635 $value = $this->parser->gvw_important( $value );
636 $important = ' !important';
637 }
638 $values = explode( ' ', $value );
639
640 if ( 4 === count( $values ) ) {
641 for ( $i = 0; $i < 4; $i++ ) {
642 $return[ $shorthands[ $property ][ $i ] ] = $values[ $i ] . $important;
643 }
644 } elseif ( 3 === count( $values ) ) {
645 $return[ $shorthands[ $property ][0] ] = $values[0] . $important;
646 $return[ $shorthands[ $property ][1] ] = $values[1] . $important;
647 $return[ $shorthands[ $property ][3] ] = $values[1] . $important;
648 $return[ $shorthands[ $property ][2] ] = $values[2] . $important;
649 } elseif ( 2 === count( $values ) ) {
650 for ( $i = 0; $i < 4; $i++ ) {
651 $return[ $shorthands[ $property ][ $i ] ] = ( 0 !== $i % 2 ) ? $values[1] . $important : $values[0] . $important;
652 }
653 } else {
654 for ( $i = 0; $i < 4; $i++ ) {
655 $return[ $shorthands[ $property ][ $i ] ] = $values[0] . $important;
656 }
657 }
658
659 return $return;
660 }
661
662 /**
663 * Explodes a string as explode() does, however, not if $sep is escaped or within a string.
664 *
665 * @since 1.0.0
666 *
667 * @param string $sep Separator.
668 * @param string $a_string String.
669 * @return array [description]
670 */
671 public function explode_ws( string $sep, string $a_string ): array {
672 $status = 'st';
673 $to = '';
674
675 $output = array();
676 $num = 0;
677 for ( $i = 0, $len = strlen( $a_string ); $i < $len; $i++ ) {
678 switch ( $status ) {
679 case 'st':
680 if ( $a_string[ $i ] === $sep && ! $this->parser->escaped( $a_string, $i ) ) {
681 ++$num;
682 } elseif ( '"' === $a_string[ $i ] || "'" === $a_string[ $i ] || ( '(' === $a_string[ $i ] && ! $this->parser->escaped( $a_string, $i ) ) ) {
683 $status = 'str';
684 $to = ( '(' === $a_string[ $i ] ) ? ')' : $a_string[ $i ];
685 ( isset( $output[ $num ] ) ) ? $output[ $num ] .= $a_string[ $i ] : $output[ $num ] = $a_string[ $i ];
686 } else {
687 ( isset( $output[ $num ] ) ) ? $output[ $num ] .= $a_string[ $i ] : $output[ $num ] = $a_string[ $i ];
688 }
689 break;
690
691 case 'str':
692 if ( $a_string[ $i ] === $to && ! $this->parser->escaped( $a_string, $i ) ) {
693 $status = 'st';
694 }
695 ( isset( $output[ $num ] ) ) ? $output[ $num ] .= $a_string[ $i ] : $output[ $num ] = $a_string[ $i ];
696 break;
697 }
698 }
699
700 if ( isset( $output[0] ) ) {
701 return $output;
702 } else {
703 return array( $output );
704 }
705 }
706
707 /**
708 * Merges Shorthand properties again, the opposite of dissolve_4value_shorthands().
709 *
710 * @since 1.0.0
711 *
712 * @param array<string, mixed> $an_array [description].
713 * @return array<string, mixed> [description]
714 */
715 public function merge_4value_shorthands( array $an_array ): array {
716 $return = $an_array;
717 $shorthands = &$this->parser->data['csstidy']['shorthands'];
718
719 foreach ( $shorthands as $key => $value ) {
720 if ( 0 !== $value && isset( $an_array[ $value[0] ], $an_array[ $value[1] ], $an_array[ $value[2] ], $an_array[ $value[3] ] ) ) {
721 $return[ $key ] = '';
722
723 $important = '';
724 for ( $i = 0; $i < 4; $i++ ) {
725 $val = $an_array[ $value[ $i ] ];
726 if ( $this->parser->is_important( $val ) ) {
727 $important = ' !important';
728 $return[ $key ] .= $this->parser->gvw_important( $val ) . ' ';
729 } else {
730 $return[ $key ] .= $val . ' ';
731 }
732 unset( $return[ $value[ $i ] ] );
733 }
734 $return[ $key ] = $this->shorthand( trim( $return[ $key ] . $important ) );
735 }
736 }
737 return $return;
738 }
739
740 /**
741 * Dissolve background property.
742 *
743 * @todo Full CSS3 compliance.
744 *
745 * @since 1.0.0
746 *
747 * @param string $str_value String value.
748 * @return array<string, string|null> Array.
749 */
750 public function dissolve_short_bg( string $str_value ): array {
751 // Don't try to explode background gradient!
752 if ( false !== stripos( $str_value, 'gradient(' ) ) {
753 return array( 'background' => $str_value );
754 }
755
756 $background_prop_default = &$this->parser->data['csstidy']['background_prop_default'];
757 $repeat = array( 'repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'space' );
758 $attachment = array( 'scroll', 'fixed', 'local' );
759 $clip = array( 'border', 'padding' );
760 $origin = array( 'border', 'padding', 'content' );
761 $pos = array( 'top', 'center', 'bottom', 'left', 'right' );
762 $important = '';
763 $return = array(
764 'background-image' => null,
765 'background-size' => null,
766 'background-repeat' => null,
767 'background-position' => null,
768 'background-attachment' => null,
769 'background-clip' => null,
770 'background-origin' => null,
771 'background-color' => null,
772 );
773
774 if ( $this->parser->is_important( $str_value ) ) {
775 $important = ' !important';
776 $str_value = $this->parser->gvw_important( $str_value );
777 }
778
779 $have = array();
780 $str_value = $this->explode_ws( ',', $str_value );
781 $str_value_count = count( $str_value );
782 for ( $i = 0; $i < $str_value_count; $i++ ) {
783 $have['clip'] = false;
784 $have['pos'] = false;
785 $have['color'] = false;
786 $have['bg'] = false;
787
788 if ( is_array( $str_value[ $i ] ) ) {
789 $str_value[ $i ] = $str_value[ $i ][0];
790 }
791 $str_value[ $i ] = $this->explode_ws( ' ', trim( $str_value[ $i ] ) );
792
793 $str_value_i_count = count( $str_value[ $i ] );
794 for ( $j = 0; $j < $str_value_i_count; $j++ ) {
795 if ( false === $have['bg'] && ( str_starts_with( $str_value[ $i ][ $j ], 'url(' ) || 'none' === $str_value[ $i ][ $j ] ) ) {
796 $return['background-image'] .= $str_value[ $i ][ $j ] . ',';
797 $have['bg'] = true;
798 } elseif ( in_array( $str_value[ $i ][ $j ], $repeat, true ) ) {
799 $return['background-repeat'] .= $str_value[ $i ][ $j ] . ',';
800 } elseif ( in_array( $str_value[ $i ][ $j ], $attachment, true ) ) {
801 $return['background-attachment'] .= $str_value[ $i ][ $j ] . ',';
802 } elseif ( in_array( $str_value[ $i ][ $j ], $clip, true ) && ! $have['clip'] ) {
803 $return['background-clip'] .= $str_value[ $i ][ $j ] . ',';
804 $have['clip'] = true;
805 } elseif ( in_array( $str_value[ $i ][ $j ], $origin, true ) ) {
806 $return['background-origin'] .= $str_value[ $i ][ $j ] . ',';
807 } elseif ( '(' === $str_value[ $i ][ $j ][0] ) {
808 $return['background-size'] .= substr( $str_value[ $i ][ $j ], 1, -1 ) . ',';
809 } elseif ( in_array( $str_value[ $i ][ $j ], $pos, true ) || is_numeric( $str_value[ $i ][ $j ][0] ) || is_null( $str_value[ $i ][ $j ][0] ) || '-' === $str_value[ $i ][ $j ][0] || '.' === $str_value[ $i ][ $j ][0] ) {
810 $return['background-position'] .= $str_value[ $i ][ $j ];
811 if ( ! $have['pos'] ) {
812 $return['background-position'] .= ' ';
813 } else {
814 $return['background-position'] .= ',';
815 }
816 $have['pos'] = true;
817 } elseif ( ! $have['color'] ) {
818 $return['background-color'] .= $str_value[ $i ][ $j ] . ',';
819 $have['color'] = true;
820 }
821 }
822 }
823
824 foreach ( $background_prop_default as $bg_prop => $default_value ) {
825 if ( null !== $return[ $bg_prop ] ) {
826 $return[ $bg_prop ] = substr( $return[ $bg_prop ], 0, -1 ) . $important;
827 } else {
828 $return[ $bg_prop ] = $default_value . $important;
829 }
830 }
831 return $return;
832 }
833
834 /**
835 * Merges all background properties.
836 *
837 * @todo Full CSS3 compliance.
838 *
839 * @since 1.0.0
840 *
841 * @param array<string, mixed> $input_css CSS.
842 * @return array<string, mixed> Array.
843 */
844 public function merge_bg( array $input_css ): array {
845 $background_prop_default = &$this->parser->data['csstidy']['background_prop_default'];
846 // Max number of background images. CSS3 not yet fully implemented.
847 $number_of_values = @max( count( $this->explode_ws( ',', $input_css['background-image'] ) ), count( $this->explode_ws( ',', $input_css['background-color'] ) ), 1 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
848 // Array with background images to check if BG image exists.
849 $bg_img_array = @$this->explode_ws( ',', $this->parser->gvw_important( $input_css['background-image'] ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
850 $new_bg_value = '';
851 $important = '';
852
853 // If background properties is here and not empty, don't try anything.
854 if ( isset( $input_css['background'] ) && $input_css['background'] ) {
855 return $input_css;
856 }
857
858 for ( $i = 0; $i < $number_of_values; $i++ ) {
859 foreach ( $background_prop_default as $bg_property => $default_value ) {
860 // Skip if property does not exist.
861 if ( ! isset( $input_css[ $bg_property ] ) ) {
862 continue;
863 }
864
865 $cur_value = $input_css[ $bg_property ];
866 // Skip all optimisation if gradient() somewhere.
867 if ( false !== stripos( $cur_value, 'gradient(' ) ) {
868 return $input_css;
869 }
870
871 // Skip some properties if there is no background image.
872 if ( ( ! isset( $bg_img_array[ $i ] ) || 'none' === $bg_img_array[ $i ] )
873 && ( 'background-size' === $bg_property || 'background-position' === $bg_property || 'background-attachment' === $bg_property || 'background-repeat' === $bg_property ) ) {
874 continue;
875 }
876
877 // Remove !important.
878 if ( $this->parser->is_important( $cur_value ) ) {
879 $important = ' !important';
880 $cur_value = $this->parser->gvw_important( $cur_value );
881 }
882
883 // Do not add default values.
884 if ( $cur_value === $default_value ) {
885 continue;
886 }
887
888 $temp = $this->explode_ws( ',', $cur_value );
889
890 if ( isset( $temp[ $i ] ) ) {
891 if ( 'background-size' === $bg_property ) {
892 $new_bg_value .= '(' . $temp[ $i ] . ') ';
893 } else {
894 $new_bg_value .= $temp[ $i ] . ' ';
895 }
896 }
897 }
898
899 $new_bg_value = trim( $new_bg_value );
900 if ( $i !== $number_of_values - 1 ) {
901 $new_bg_value .= ',';
902 }
903 }
904
905 // Delete all background properties.
906 foreach ( $background_prop_default as $bg_property => $default_value ) {
907 unset( $input_css[ $bg_property ] );
908 }
909
910 // Add new background property.
911 if ( '' !== $new_bg_value ) {
912 $input_css['background'] = $new_bg_value . $important;
913 } elseif ( isset( $input_css['background'] ) ) {
914 $input_css['background'] = 'none';
915 }
916
917 return $input_css;
918 }
919
920 /**
921 * Dissolve font property.
922 *
923 * @since 1.0.0
924 *
925 * @param string $str_value [description].
926 * @return array<string, string|null> [description]
927 */
928 public function dissolve_short_font( string $str_value ): array {
929 $font_prop_default = &$this->parser->data['csstidy']['font_prop_default'];
930 $font_weight = array( 'normal', 'bold', 'bolder', 'lighter', 100, 200, 300, 400, 500, 600, 700, 800, 900 );
931 $font_variant = array( 'normal', 'small-caps' );
932 $font_style = array( 'normal', 'italic', 'oblique' );
933 $important = '';
934 $return = array(
935 'font-style' => null,
936 'font-variant' => null,
937 'font-weight' => null,
938 'font-size' => null,
939 'line-height' => null,
940 'font-family' => null,
941 );
942
943 if ( $this->parser->is_important( $str_value ) ) {
944 $important = ' !important';
945 $str_value = $this->parser->gvw_important( $str_value );
946 }
947
948 $have = array();
949 $have['style'] = false;
950 $have['variant'] = false;
951 $have['weight'] = false;
952 $have['size'] = false;
953 // Detects if font-family consists of several words w/o quotes.
954 $multiwords = false;
955
956 // Workaround with multiple font-families.
957 $str_value = $this->explode_ws( ',', trim( $str_value ) );
958
959 $str_value[0] = $this->explode_ws( ' ', trim( $str_value[0] ) );
960
961 $str_value_0_count = count( $str_value[0] );
962 for ( $j = 0; $j < $str_value_0_count; $j++ ) {
963 if ( false === $have['weight'] && in_array( $str_value[0][ $j ], $font_weight, false ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse
964 $return['font-weight'] = $str_value[0][ $j ];
965 $have['weight'] = true;
966 } elseif ( false === $have['variant'] && in_array( $str_value[0][ $j ], $font_variant, true ) ) {
967 $return['font-variant'] = $str_value[0][ $j ];
968 $have['variant'] = true;
969 } elseif ( false === $have['style'] && in_array( $str_value[0][ $j ], $font_style, true ) ) {
970 $return['font-style'] = $str_value[0][ $j ];
971 $have['style'] = true;
972 } elseif ( false === $have['size'] && ( is_numeric( $str_value[0][ $j ][0] ) || is_null( $str_value[0][ $j ][0] ) || '.' === $str_value[0][ $j ][0] ) ) {
973 $size = $this->explode_ws( '/', trim( $str_value[0][ $j ] ) );
974 $return['font-size'] = $size[0];
975 if ( isset( $size[1] ) ) {
976 $return['line-height'] = $size[1];
977 } else {
978 $return['line-height'] = ''; // Don't add 'normal'!
979 }
980 $have['size'] = true;
981 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
982 if ( isset( $return['font-family'] ) ) {
983 $return['font-family'] .= ' ' . $str_value[0][ $j ];
984 $multiwords = true;
985 } else {
986 $return['font-family'] = $str_value[0][ $j ];
987 }
988 }
989 }
990 // Add quotes if we have several words in font-family.
991 if ( $multiwords ) {
992 $return['font-family'] = '"' . $return['font-family'] . '"';
993 }
994 $i = 1;
995 while ( isset( $str_value[ $i ] ) ) {
996 $return['font-family'] .= ',' . trim( $str_value[ $i ] );
997 ++$i;
998 }
999
1000 // Fix for font-size 100 and higher.
1001 if ( false === $have['size'] && isset( $return['font-weight'] ) && is_numeric( $return['font-weight'][0] ) ) {
1002 $return['font-size'] = $return['font-weight'];
1003 unset( $return['font-weight'] );
1004 }
1005
1006 foreach ( $font_prop_default as $font_prop => $default_value ) {
1007 if ( null !== $return[ $font_prop ] ) {
1008 $return[ $font_prop ] = $return[ $font_prop ] . $important;
1009 } else {
1010 $return[ $font_prop ] = $default_value . $important;
1011 }
1012 }
1013 return $return;
1014 }
1015
1016 /**
1017 * Merges all fonts properties.
1018 *
1019 * @since 1.0.0
1020 *
1021 * @param array<string, string> $input_css [description].
1022 * @return array<string, string> [description]
1023 */
1024 public function merge_font( array $input_css ): array {
1025 $font_prop_default = &$this->parser->data['csstidy']['font_prop_default'];
1026 $new_font_value = '';
1027 $important = '';
1028 // Skip if no font-family and font-size set.
1029 if ( isset( $input_css['font-family'], $input_css['font-size'] ) && 'inherit' !== $input_css['font-family'] ) {
1030 // Fix several words in font-family - add quotes.
1031 $families = explode( ',', $input_css['font-family'] );
1032 $result_families = array();
1033 foreach ( $families as $family ) {
1034 $family = trim( $family );
1035 $len = strlen( $family );
1036 if ( str_contains( $family, ' ' ) &&
1037 ! ( ( '"' === $family[0] && '"' === $family[ $len - 1 ] ) ||
1038 ( "'" === $family[0] && "'" === $family[ $len - 1 ] ) ) ) {
1039 $family = '"' . $family . '"';
1040 }
1041 $result_families[] = $family;
1042 }
1043 $input_css['font-family'] = implode( ',', $result_families );
1044 foreach ( $font_prop_default as $font_property => $default_value ) {
1045 // Skip if property does not exist.
1046 if ( ! isset( $input_css[ $font_property ] ) ) {
1047 continue;
1048 }
1049
1050 $cur_value = $input_css[ $font_property ];
1051
1052 // Skip if default value is used.
1053 if ( $cur_value === $default_value ) {
1054 continue;
1055 }
1056
1057 // Remove !important.
1058 if ( $this->parser->is_important( $cur_value ) ) {
1059 $important = ' !important';
1060 $cur_value = $this->parser->gvw_important( $cur_value );
1061 }
1062
1063 $new_font_value .= $cur_value;
1064 // Add delimiter.
1065 $new_font_value .= ( 'font-size' === $font_property && isset( $input_css['line-height'] ) ) ? '/' : ' ';
1066 }
1067
1068 $new_font_value = trim( $new_font_value );
1069
1070 // Delete all font properties.
1071 foreach ( $font_prop_default as $font_property => $default_value ) {
1072 if ( 'font' !== $font_property || ! $new_font_value ) {
1073 unset( $input_css[ $font_property ] );
1074 }
1075 }
1076
1077 // Add new font property.
1078 if ( '' !== $new_font_value ) {
1079 $input_css['font'] = $new_font_value . $important;
1080 }
1081 }
1082
1083 return $input_css;
1084 }
1085
1086 } // class TablePress_CSSTidy_Optimise
1087