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

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