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