PluginProbe
TablePress – Tables in WordPress made easy / 3.3.2
TablePress – Tables in WordPress made easy v3.3.2
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.3.2, at libraries/csstidy/class.csstidy_optimise.php

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