Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 477
0.00% covered (danger)
0.00%
0 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
csstidy
0.00% covered (danger)
0.00%
0 / 473
0.00% covered (danger)
0.00%
0 / 26
73170
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 csstidy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_cfg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 _load_template
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 set_cfg
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 _add_token
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
20
 log
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 _unicode
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
420
 write_page
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 write
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 load_template
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 parse_from_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_token
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 parse
0.00% covered (danger)
0.00%
0 / 256
0.00% covered (danger)
0.00%
0 / 1
21170
 explode_selectors
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 escaped
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 css_add_property
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 css_new_media_section
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 css_new_selector
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
90
 css_new_property
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 merge_css_blocks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 is_important
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 gvw_important
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 property_is_next
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 property_is_valid
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 parse_string_list
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
182
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * CSSTidy - CSS Parser and Optimiser
4 *
5 * CSS Parser class
6 *
7 * Copyright 2005, 2006, 2007 Florian Schmitz
8 *
9 * This file is part of CSSTidy.
10 *
11 *   CSSTidy is free software; you can redistribute it and/or modify
12 *   it under the terms of the GNU Lesser General Public License as published by
13 *   the Free Software Foundation; either version 2.1 of the License, or
14 *   (at your option) any later version.
15 *
16 *   CSSTidy is distributed in the hope that it will be useful,
17 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
18 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 *   GNU Lesser General Public License for more details.
20 *
21 *   You should have received a copy of the GNU Lesser General Public License
22 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
23 *
24 * @license https://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
25 * @package csstidy
26 * @author Florian Schmitz (floele at gmail dot com) 2005-2007
27 * @author Brett Zamir (brettz9 at yahoo dot com) 2007
28 * @author Nikolay Matsievsky (speed at webo dot name) 2009-2010
29 * @author Cedric Morin (cedric at yterium dot com) 2010
30 */
31
32// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
33
34/**
35 * Defines ctype functions if required
36 *
37 * @version 1.0
38 */
39require_once __DIR__ . '/class.csstidy-ctype.php';
40
41/**
42 * Various CSS data needed for correct optimisations etc.
43 *
44 * @version 1.3
45 */
46require __DIR__ . '/data.inc.php';
47
48/**
49 * Contains a class for printing CSS code
50 *
51 * @version 1.0
52 */
53require __DIR__ . '/class.csstidy-print.php';
54
55/**
56 * Contains a class for optimising CSS code
57 *
58 * @version 1.0
59 */
60require __DIR__ . '/class.csstidy-optimise.php';
61
62/**
63 * CSS Parser class
64
65 * This class represents a CSS parser which reads CSS code and saves it in an array.
66 * In opposite to most other CSS parsers, it does not use regular expressions and
67 * thus has full CSS2 support and a higher reliability.
68 * Additional to that it applies some optimisations and fixes to the CSS code.
69 * An online version should be available here: https://cdburnerxp.se/cssparse/css_optimiser.php
70 *
71 * @package csstidy
72 * @author Florian Schmitz (floele at gmail dot com) 2005-2006
73 * @version 1.3.1
74 */
75#[AllowDynamicProperties]
76class csstidy { // phpcs:ignore
77
78    /**
79     * Saves the parsed CSS. This array is empty if preserve_css is on.
80     *
81     * @var array
82     * @access public
83     */
84    public $css = array();
85    /**
86     * Saves the parsed CSS (raw)
87     *
88     * @var array
89     * @access private
90     */
91    public $tokens = array();
92    /**
93     * Printer class
94     *
95     * @see csstidy_print
96     * @var object
97     * @access public
98     */
99    public $print;
100    /**
101     * Optimiser class
102     *
103     * @see csstidy_optimise
104     * @var object
105     * @access private
106     */
107    public $optimise;
108    /**
109     * Saves the CSS charset (@charset)
110     *
111     * @var string
112     * @access private
113     */
114    public $charset = '';
115    /**
116     * Saves all @import URLs
117     *
118     * @var array
119     * @access private
120     */
121    public $import = array();
122    /**
123     * Saves the namespace
124     *
125     * @var string
126     * @access private
127     */
128    public $namespace = '';
129    /**
130     * Contains the version of csstidy
131     *
132     * @var string
133     * @access private
134     */
135    public $version = '1.3';
136    /**
137     * Stores the settings
138     *
139     * @var array
140     * @access private
141     */
142    public $settings = array();
143    /**
144     * Saves the parser-status.
145     *
146     * Possible values:
147     * - is = in selector
148     * - ip = in property
149     * - iv = in value
150     * - instr = in string (started at " or ' or ( )
151     * - ic = in comment (ignore everything)
152     * - at = in @-block
153     *
154     * @var string
155     * @access private
156     */
157    public $status = 'is';
158    /**
159     * Saves the current at rule (@media)
160     *
161     * @var string
162     * @access private
163     */
164    public $at = '';
165    /**
166     * Saves the current selector
167     *
168     * @var string
169     * @access private
170     */
171    public $selector = '';
172    /**
173     * Saves the current property
174     *
175     * @var string
176     * @access private
177     */
178    public $property = '';
179    /**
180     * Saves the position of , in selectors
181     *
182     * @var array
183     * @access private
184     */
185    public $sel_separate = array();
186    /**
187     * Saves the current value
188     *
189     * @var string
190     * @access private
191     */
192    public $value = '';
193    /**
194     * Saves the current sub-value
195     *
196     * Example for a subvalue:
197     * background:url(foo.png) red no-repeat;
198     * "url(foo.png)", "red", and  "no-repeat" are subvalues,
199     * separated by whitespace
200     *
201     * @var string
202     * @access private
203     */
204    public $sub_value = '';
205    /**
206     * Array which saves all subvalues for a property.
207     *
208     * @var array
209     * @see sub_value
210     * @access private
211     */
212    public $sub_value_arr = array();
213    /**
214     * Saves the stack of characters that opened the current strings
215     *
216     * @var array
217     * @access private
218     */
219    public $str_char = array();
220    /**
221     * Current strings.
222     *
223     * @var array
224     * @access private
225     */
226    public $cur_string = array();
227    /**
228     * Status from which the parser switched to ic or instr
229     *
230     * @var array
231     * @access private
232     */
233    public $from = array();
234    /**
235    /**
236     * =true if in invalid at-rule
237     *
238     * @var bool
239     * @access private
240     */
241    public $invalid_at = false;
242    /**
243     * =true if something has been added to the current selector
244     *
245     * @var bool
246     * @access private
247     */
248    public $added = false;
249    /**
250     * Array which saves the message log
251     *
252     * @var array
253     * @access private
254     */
255    public $log = array();
256    /**
257     * Saves the line number
258     *
259     * @var integer
260     * @access private
261     */
262    public $line = 1;
263    /**
264     * Marks if we need to leave quotes for a string
265     *
266     * @var array
267     * @access private
268     */
269    public $quoted_string = array();
270
271    /**
272     * List of tokens
273     *
274     * @var string
275     */
276    public $tokens_list = '';
277
278    /**
279     * Loads standard template and sets default settings.
280     *
281     * @access private
282     * @version 1.3
283     */
284    public function __construct() {
285        $this->settings['remove_bslash']        = true;
286        $this->settings['compress_colors']      = true;
287        $this->settings['compress_font-weight'] = true;
288        $this->settings['lowercase_s']          = false;
289
290        /*
291        1 common shorthands optimization
292        2 + font property optimization
293        3 + background property optimization
294         */
295        $this->settings['optimise_shorthands'] = 1;
296        $this->settings['remove_last_;']       = true;
297        /* rewrite all properties with low case, better for later gzip OK, safe*/
298        $this->settings['case_properties'] = 1;
299
300        /*
301         * sort properties in alpabetic order, better for later gzip
302         * but can cause trouble in case of overiding same propertie or using hack
303         */
304        $this->settings['sort_properties'] = false;
305
306        /*
307        1, 3, 5, etc -- enable sorting selectors inside @media: a{}b{}c{}
308        2, 5, 8, etc -- enable sorting selectors inside one CSS declaration: a,b,c{}
309        preserve order by default cause it can break functionnality
310         */
311        $this->settings['sort_selectors'] = 0;
312        /* is dangeroues to be used: CSS is broken sometimes */
313        $this->settings['merge_selectors'] = 0;
314        /* preserve or not browser hacks */
315        $this->settings['discard_invalid_selectors']  = false;
316        $this->settings['discard_invalid_properties'] = false;
317        $this->settings['css_level']                  = 'CSS2.1';
318        $this->settings['preserve_css']               = false;
319        $this->settings['timestamp']                  = false;
320        $this->settings['template']                   = ''; // say that propertie exist.
321        $this->set_cfg( 'template', 'default' ); // call load_template.
322        /* Tells csstidy_optimise to keep leading zeros on decimal numbers, e.g., 0.7 */
323        $this->settings['preserve_leading_zeros'] = false;
324        $this->optimise                           = new csstidy_optimise( $this );
325
326        $this->tokens_list = & $GLOBALS['csstidy']['tokens'];
327    }
328
329    /**
330     * Call the construct function.
331     */
332    public function csstidy() {
333        $this->__construct();
334    }
335
336    /**
337     * Get the value of a setting.
338     *
339     * @param string $setting - the settings.
340     * @access public
341     * @return mixed
342     * @version 1.0
343     */
344    public function get_cfg( $setting ) {
345        if ( isset( $this->settings[ $setting ] ) ) {
346            return $this->settings[ $setting ];
347        }
348        return false;
349    }
350
351    /**
352     * Load a template
353     *
354     * @param string $template used by set_cfg to load a template via a configuration setting.
355     * @access private
356     * @version 1.4
357     */
358    public function _load_template( $template ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
359        switch ( $template ) {
360            case 'default':
361                $this->load_template( 'default' );
362                break;
363
364            case 'highest':
365                $this->load_template( 'highest_compression' );
366                break;
367
368            case 'high':
369                $this->load_template( 'high_compression' );
370                break;
371
372            case 'low':
373                $this->load_template( 'low_compression' );
374                break;
375
376            default:
377                $this->load_template( $template );
378                break;
379        }
380    }
381
382    /**
383     * Set the value of a setting.
384     *
385     * @param string $setting - the setting.
386     * @param mixed  $value - the value we're setting.
387     * @access public
388     * @return bool
389     * @version 1.0
390     */
391    public function set_cfg( $setting, $value = null ) {
392        if ( is_array( $setting ) && null === $value ) {
393            foreach ( $setting as $setprop => $setval ) {
394                $this->settings[ $setprop ] = $setval;
395            }
396            if ( array_key_exists( 'template', $setting ) ) {
397                $this->_load_template( $this->settings['template'] );
398            }
399            return true;
400        } elseif ( isset( $this->settings[ $setting ] ) && '' !== $value ) {
401            $this->settings[ $setting ] = $value;
402            if ( 'template' === $setting ) {
403                $this->_load_template( $this->settings['template'] );
404            }
405            return true;
406        }
407        return false;
408    }
409
410    /**
411     * Adds a token to $this->tokens
412     *
413     * @param mixed  $type - the type.
414     * @param string $data - data.
415     * @param bool   $do add a token even if preserve_css is off.
416     * @access private
417     * @version 1.0
418     */
419    public function _add_token( $type, $data, $do = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
420        if ( $this->get_cfg( 'preserve_css' ) || $do ) {
421            $this->tokens[] = array( $type, ( COMMENT === $type ) ? $data : trim( $data ) );
422        }
423    }
424
425    /**
426     * Add a message to the message log
427     *
428     * @param string  $message - the message.
429     * @param string  $type - the type of message.
430     * @param integer $line - the line.
431     * @access private
432     * @version 1.0
433     */
434    public function log( $message, $type, $line = -1 ) {
435        if ( -1 === $line ) {
436            $line = $this->line;
437        }
438        $line = (int) $line;
439        $add  = array(
440            'm' => $message,
441            't' => $type,
442        );
443        if ( ! isset( $this->log[ $line ] ) || ! in_array( $add, $this->log[ $line ], true ) ) {
444            $this->log[ $line ][] = $add;
445        }
446    }
447
448    /**
449     * Parse unicode notations and find a replacement character
450     *
451     * @param string  $string - a string.
452     * @param integer $i - counting integer.
453     * @access private
454     * @return string
455     * @version 1.2
456     */
457    public function _unicode( &$string, &$i ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
458        ++$i;
459        $add      = '';
460        $replaced = false;
461
462        while ( $i < strlen( $string ) && ( ctype_xdigit( $string[ $i ] ) || ctype_space( $string[ $i ] ) ) && strlen( $add ) < 6 ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
463            $add .= $string[ $i ];
464
465            if ( ctype_space( $string[ $i ] ) ) {
466                break;
467            }
468            ++$i;
469        }
470
471        if ( hexdec( $add ) > 47 && hexdec( $add ) < 58 || hexdec( $add ) > 64 && hexdec( $add ) < 91 || hexdec( $add ) > 96 && hexdec( $add ) < 123 ) {
472            $this->log( 'Replaced unicode notation: Changed \\' . $add . ' to ' . chr( hexdec( $add ) ), 'Information' );
473            $add      = chr( hexdec( $add ) );
474            $replaced = true;
475        } else {
476            $add = trim( '\\' . $add );
477        }
478
479        if ( @ctype_xdigit( $string[ $i + 1 ] ) && ctype_space( $string[ $i ] )
480                        && ! $replaced || ! ctype_space( $string[ $i ] ) ) {
481            --$i;
482        }
483
484        if ( '\\' !== $add || ! $this->get_cfg( 'remove_bslash' ) || strpos( $this->tokens_list, $string[ $i + 1 ] ) !== false ) {
485            return $add;
486        }
487
488        if ( '\\' === $add ) {
489            $this->log( 'Removed unnecessary backslash', 'Information' );
490        }
491        return '';
492    }
493
494    /**
495     * Write formatted output to a file
496     *
497     * @param string $filename - the file na,e.
498     * @param string $doctype when printing formatted, is a shorthand for the document type.
499     * @param bool   $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet.
500     * @param string $title when printing formatted, is the title to be added in the head of the document.
501     * @param string $lang when printing formatted, gives a two-letter language code to be added to the output.
502     * @access public
503     * @version 1.4
504     */
505    public function write_page( $filename, $doctype = 'xhtml1.1', $externalcss = true, $title = '', $lang = 'en' ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
506        $this->write( $filename, true );
507    }
508
509    /**
510     * Write plain output to a file
511     *
512     * @param string $filename the file name.
513     * @param bool   $formatted whether to print formatted or not.
514     * @param string $doctype when printing formatted, is a shorthand for the document type.
515     * @param bool   $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet.
516     * @param string $title when printing formatted, is the title to be added in the head of the document.
517     * @param string $lang when printing formatted, gives a two-letter language code to be added to the output.
518     * @param bool   $pre_code whether to add pre and code tags around the code (for light HTML formatted templates).
519     * @access public
520     * @version 1.4
521     */
522    public function write( $filename, $formatted = false, $doctype = 'xhtml1.1', $externalcss = true, $title = '', $lang = 'en', $pre_code = true ) {
523        $filename .= ( $formatted ) ? '.xhtml' : '.css';
524
525        if ( ! is_dir( 'temp' ) ) {
526            $madedir = mkdir( 'temp' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
527            if ( ! $madedir ) {
528                print 'Could not make directory "temp" in ' . __DIR__;
529                exit( 0 );
530            }
531        }
532        $handle = fopen( 'temp/' . $filename, 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
533        if ( $handle ) {
534            if ( ! $formatted ) {
535                fwrite( $handle, $this->print->plain() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
536            } else {
537                fwrite( $handle, $this->print->formatted_page( $doctype, $externalcss, $title, $lang, $pre_code ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
538            }
539        }
540        fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
541    }
542
543    /**
544     * Loads a new template
545     *
546     * @param string $content either filename (if $from_file == true), content of a template file, "high_compression", "highest_compression", "low_compression", or "default".
547     * @param bool   $from_file uses $content as filename if true.
548     * @access public
549     * @version 1.1
550     * @see http://csstidy.sourceforge.net/templates.php
551     */
552    public function load_template( $content, $from_file = true ) {
553        $predefined_templates = & $GLOBALS['csstidy']['predefined_templates'];
554        if ( 'high_compression' === $content || 'default' === $content || 'highest_compression' === $content || 'low_compression' === $content ) {
555            $this->template = $predefined_templates[ $content ];
556            return;
557        }
558
559        if ( $from_file ) {
560            $content = strip_tags( file_get_contents( $content ), '<span>' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
561        }
562        $content  = str_replace( "\r\n", "\n", $content ); // Unify newlines (because the output also only uses \n).
563        $template = explode( '|', $content );
564
565        $this->template = array_replace( $this->template, $template );
566    }
567
568    /**
569     * Starts parsing from URL
570     *
571     * @param string $url - the URL.
572     * @access public
573     * @version 1.0
574     */
575    public function parse_from_url( $url ) {
576        return $this->parse( @file_get_contents( $url ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
577    }
578
579    /**
580     * Checks if there is a token at the current position
581     *
582     * @param string  $string - the string we're checking.
583     * @param integer $i - an int.
584     * @access public
585     * @version 1.11
586     */
587    public function is_token( &$string, $i ) {
588        return ( strpos( $this->tokens_list, $string[ $i ] ) !== false && ! self::escaped( $string, $i ) );
589    }
590
591    /**
592     * Parses CSS in $string. The code is saved as array in $this->css
593     *
594     * @param string $string the CSS code.
595     * @access public
596     * @return bool
597     * @version 1.1
598     */
599    public function parse( $string ) {
600        // Temporarily set locale to en_US in order to handle floats properly.
601        $old = @setlocale( LC_ALL, 0 );
602        @setlocale( LC_ALL, 'C' );
603
604        // PHP bug? Settings need to be refreshed in PHP4.
605        $this->print = new csstidy_print( $this );
606
607        $at_rules                 = & $GLOBALS['csstidy']['at_rules'];
608        $quoted_string_properties = & $GLOBALS['csstidy']['quoted_string_properties'];
609
610        $this->css              = array();
611        $this->print->input_css = $string;
612        $string                 = str_replace( "\r\n", "\n", $string ) . ' ';
613        $cur_comment            = '';
614
615        for ( $i = 0, $size = strlen( $string ); $i < $size; $i++ ) {
616            if ( "\n" === $string[ $i ] || "\r" === $string[ $i ] ) {
617                ++$this->line;
618            }
619
620            switch ( $this->status ) {
621                /* Case in at-block */
622                case 'at':
623                    if ( self::is_token( $string, $i ) ) {
624                        if ( '/' === $string[ $i ] && '*' === @$string[ $i + 1 ] ) {
625                            $this->status = 'ic';
626                            ++$i;
627                            $this->from[] = 'at';
628                        } elseif ( '{' === $string[ $i ] ) {
629                            $this->status = 'is';
630                            $this->at     = $this->css_new_media_section( $this->at );
631                            $this->_add_token( AT_START, $this->at );
632                        } elseif ( ',' === $string[ $i ] ) {
633                            $this->at = trim( $this->at ) . ',';
634                        } elseif ( '\\' === $string[ $i ] ) {
635                            $this->at .= $this->_unicode( $string, $i );
636                        } elseif ( in_array( $string[ $i ], array( '(', ')', ':', '.', '/' ), true ) ) {
637                            // fix for complicated media, i.e @media screen and (-webkit-min-device-pixel-ratio:1.5)
638                            // '/' is included for ratios in Opera: (-o-min-device-pixel-ratio: 3/2).
639                            $this->at .= $string[ $i ];
640                        }
641                    } else {
642                        $lastpos = strlen( $this->at ) - 1;
643                        if ( ! ( ( ctype_space( $this->at[ $lastpos ] ) || self::is_token( $this->at, $lastpos ) && ',' === $this->at[ $lastpos ] ) && ctype_space( $string[ $i ] ) ) ) {
644                            $this->at .= $string[ $i ];
645                        }
646                    }
647                    break;
648
649                /* Case in-selector */
650                case 'is':
651                    if ( self::is_token( $string, $i ) ) {
652                        if ( '/' === $string[ $i ] && '*' === @$string[ $i + 1 ] && '' === trim( $this->selector ) ) {
653                            $this->status = 'ic';
654                            ++$i;
655                            $this->from[] = 'is';
656                        } elseif ( '@' === $string[ $i ] && '' === trim( $this->selector ) ) {
657                            // Check for at-rule.
658                            $this->invalid_at = true;
659                            foreach ( $at_rules as $name => $type ) {
660                                if ( ! strcasecmp( substr( $string, $i + 1, strlen( $name ) ), $name ) ) {
661                                    ( 'at' === $type ) ? $this->at = '@' . $name : $this->selector = '@' . $name;
662                                    $this->status                  = $type;
663                                    $i                            += strlen( $name );
664                                    $this->invalid_at              = false;
665                                }
666                            }
667
668                            if ( $this->invalid_at ) {
669                                $this->selector  = '@';
670                                $invalid_at_name = '';
671                                for ( $j = $i + 1; $j < $size; ++$j ) {
672                                    if ( ! ctype_alpha( $string[ $j ] ) ) {
673                                        break;
674                                    }
675                                    $invalid_at_name .= $string[ $j ];
676                                }
677                                $this->log( 'Invalid @-rule: ' . $invalid_at_name . ' (removed)', 'Warning' );
678                            }
679                        } elseif ( ( '"' === $string[ $i ] || "'" === $string[ $i ] ) ) {
680                            $this->cur_string[] = $string[ $i ];
681                            $this->status       = 'instr';
682                            $this->str_char[]   = $string[ $i ];
683                            $this->from[]       = 'is';
684                            /* fixing CSS3 attribute selectors, i.e. a[href$=".mp3" */
685                            $this->quoted_string[] = ( '=' === $string[ $i - 1 ] );
686                        } elseif ( $this->invalid_at && ';' === $string[ $i ] ) {
687                            $this->invalid_at = false;
688                            $this->status     = 'is';
689                        } elseif ( '{' === $string[ $i ] ) {
690                            $this->status = 'ip';
691                            if ( '' === $this->at ) {
692                                $this->at = $this->css_new_media_section( DEFAULT_AT );
693                            }
694                            $this->selector = $this->css_new_selector( $this->at, $this->selector );
695                            $this->_add_token( SEL_START, $this->selector );
696                            $this->added = false;
697                        } elseif ( '}' === $string[ $i ] ) {
698                            $this->_add_token( AT_END, $this->at );
699                            $this->at           = '';
700                            $this->selector     = '';
701                            $this->sel_separate = array();
702                        } elseif ( ',' === $string[ $i ] ) {
703                            $this->selector       = trim( $this->selector ) . ',';
704                            $this->sel_separate[] = strlen( $this->selector );
705                        } elseif ( '\\' === $string[ $i ] ) {
706                            $this->selector .= $this->_unicode( $string, $i );
707                        } elseif ( '*' === $string[ $i ] && @in_array( $string[ $i + 1 ], array( '.', '#', '[', ':' ), true ) ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElseif
708                            // remove unnecessary universal selector, FS#147.
709                        } else {
710                            $this->selector .= $string[ $i ];
711                        }
712                    } else {
713                        $lastpos = strlen( $this->selector ) - 1;
714                        if ( -1 === $lastpos || ! ( ( ctype_space( $this->selector[ $lastpos ] ) || self::is_token( $this->selector, $lastpos ) && ',' === $this->selector[ $lastpos ] ) && ctype_space( $string[ $i ] ) ) ) {
715                            $this->selector .= $string[ $i ];
716                        } elseif ( ctype_space( $string[ $i ] ) && $this->get_cfg( 'preserve_css' ) && ! $this->get_cfg( 'merge_selectors' ) ) {
717                            $this->selector .= $string[ $i ];
718                        }
719                    }
720                    break;
721
722                /* Case in-property */
723                case 'ip':
724                    if ( self::is_token( $string, $i ) ) {
725                        if ( ( ':' === $string[ $i ] || '=' === $string[ $i ] ) && '' !== $this->property ) {
726                            $this->status = 'iv';
727                            if ( ! $this->get_cfg( 'discard_invalid_properties' ) || self::property_is_valid( $this->property ) ) {
728                                $this->property = $this->css_new_property( $this->at, $this->selector, $this->property );
729                                $this->_add_token( PROPERTY, $this->property );
730                            }
731                        } elseif ( '/' === $string[ $i ] && '*' === @$string[ $i + 1 ] && '' === $this->property ) {
732                            $this->status = 'ic';
733                            ++$i;
734                            $this->from[] = 'ip';
735                        } elseif ( '}' === $string[ $i ] ) {
736                            $this->explode_selectors();
737                            $this->status     = 'is';
738                            $this->invalid_at = false;
739                            $this->_add_token( SEL_END, $this->selector );
740                            $this->selector = '';
741                            $this->property = '';
742                        } elseif ( ';' === $string[ $i ] ) {
743                            $this->property = '';
744                        } elseif ( '\\' === $string[ $i ] ) {
745                            $this->property .= $this->_unicode( $string, $i );
746                        } elseif ( '' === $this->property && ! ctype_space( $string[ $i ] ) ) {
747                            // else this is dumb IE a hack, keep it.
748                            $this->property .= $string[ $i ];
749                        }
750                    } elseif ( ! ctype_space( $string[ $i ] ) ) {
751                        $this->property .= $string[ $i ];
752                    }
753                    break;
754
755                /* Case in-value */
756                case 'iv':
757                    $pn = ( ( "\n" === $string[ $i ] || "\r" === $string[ $i ] ) && $this->property_is_next( $string, $i + 1 ) || strlen( $string ) - 1 === $i );
758                    if ( ( self::is_token( $string, $i ) || $pn ) && ( ! ( ',' === $string[ $i ] && ! ctype_space( $string[ $i + 1 ] ) ) ) ) {
759                        if ( '/' === $string[ $i ] && '*' === @$string[ $i + 1 ] ) {
760                            $this->status = 'ic';
761                            ++$i;
762                            $this->from[] = 'iv';
763                        } elseif ( ( '"' === $string[ $i ] || "'" === $string[ $i ] || '(' === $string[ $i ] ) ) {
764                            $this->cur_string[]    = $string[ $i ];
765                            $this->str_char[]      = ( '(' === $string[ $i ] ) ? ')' : $string[ $i ];
766                            $this->status          = 'instr';
767                            $this->from[]          = 'iv';
768                            $this->quoted_string[] = in_array( strtolower( $this->property ), $quoted_string_properties, true );
769                        } elseif ( ',' === $string[ $i ] ) {
770                            $this->sub_value = trim( $this->sub_value ) . ',';
771                        } elseif ( '\\' === $string[ $i ] ) {
772                            $this->sub_value .= $this->_unicode( $string, $i );
773                        } elseif ( ';' === $string[ $i ] || $pn ) {
774                            if ( '@' === $this->selector[0] && isset( $at_rules[ substr( $this->selector, 1 ) ] ) && 'iv' === $at_rules[ substr( $this->selector, 1 ) ] ) {
775                                $this->status = 'is';
776
777                                switch ( $this->selector ) {
778                                    case '@charset':
779                                        /* Add quotes to charset */
780                                        $this->sub_value_arr[] = '"' . trim( $this->sub_value ) . '"';
781                                        $this->charset         = $this->sub_value_arr[0];
782                                        break;
783                                    case '@namespace':
784                                        /* Add quotes to namespace */
785                                        $this->sub_value_arr[] = '"' . trim( $this->sub_value ) . '"';
786                                        $this->namespace       = implode( ' ', $this->sub_value_arr );
787                                        break;
788                                    case '@import':
789                                        $this->sub_value = trim( $this->sub_value );
790
791                                        if ( empty( $this->sub_value_arr ) ) {
792                                            // Quote URLs in imports only if they're not already inside url() and not already quoted.
793                                            if ( ! str_starts_with( $this->sub_value, 'url(' ) ) {
794                                                if ( ! ( substr( $this->sub_value, -1 ) === $this->sub_value[0] && in_array( $this->sub_value[0], array( "'", '"' ), true ) ) ) {
795                                                    $this->sub_value = '"' . $this->sub_value . '"';
796                                                }
797                                            }
798                                        }
799
800                                        $this->sub_value_arr[] = $this->sub_value;
801                                        $this->import[]        = implode( ' ', $this->sub_value_arr );
802                                        break;
803                                }
804
805                                $this->sub_value_arr = array();
806                                $this->sub_value     = '';
807                                $this->selector      = '';
808                                $this->sel_separate  = array();
809                            } else {
810                                $this->status = 'ip';
811                            }
812                        } elseif ( '}' !== $string[ $i ] ) {
813                            $this->sub_value .= $string[ $i ];
814                        }
815                        if ( ( '}' === $string[ $i ] || ';' === $string[ $i ] || $pn ) && ! empty( $this->selector ) ) {
816                            if ( '' === $this->at ) {
817                                $this->at = $this->css_new_media_section( DEFAULT_AT );
818                            }
819
820                            // case settings.
821                            if ( $this->get_cfg( 'lowercase_s' ) ) {
822                                $this->selector = strtolower( $this->selector );
823                            }
824                            $this->property = strtolower( $this->property );
825
826                            $this->optimise->subvalue();
827                            if ( '' !== $this->sub_value ) {
828                                if ( str_starts_with( $this->sub_value, 'format' ) ) {
829                                    $format_strings = self::parse_string_list( substr( $this->sub_value, 7, -1 ) );
830                                    if ( ! $format_strings ) {
831                                        $this->sub_value = '';
832                                    } else {
833                                        $this->sub_value = 'format(';
834
835                                        foreach ( $format_strings as $format_string ) {
836                                            $this->sub_value .= '"' . str_replace( '"', '\\"', $format_string ) . '",';
837                                        }
838
839                                        $this->sub_value = substr( $this->sub_value, 0, -1 ) . ')';
840                                    }
841                                }
842                                if ( '' !== $this->sub_value ) {
843                                    $this->sub_value_arr[] = $this->sub_value;
844                                }
845                                $this->sub_value = '';
846                            }
847
848                            $this->value = array_shift( $this->sub_value_arr );
849                            while ( $this->sub_value_arr ) {
850                                $this->value .= ' ' . array_shift( $this->sub_value_arr );
851                            }
852
853                            $this->optimise->value();
854
855                            $valid = self::property_is_valid( $this->property );
856                            if ( ( ! $this->invalid_at || $this->get_cfg( 'preserve_css' ) ) && ( ! $this->get_cfg( 'discard_invalid_properties' ) || $valid ) ) {
857                                $this->css_add_property( $this->at, $this->selector, $this->property, $this->value );
858                                $this->_add_token( VALUE, $this->value );
859                                $this->optimise->shorthands();
860                            }
861                            if ( ! $valid ) {
862                                if ( $this->get_cfg( 'discard_invalid_properties' ) ) {
863                                    $this->log( 'Removed invalid property: ' . $this->property, 'Warning' );
864                                } else {
865                                    $this->log( 'Invalid property in ' . strtoupper( $this->get_cfg( 'css_level' ) ) . ': ' . $this->property, 'Warning' );
866                                }
867                            }
868
869                            $this->property      = '';
870                            $this->sub_value_arr = array();
871                            $this->value         = '';
872                        }
873                        if ( '}' === $string[ $i ] ) {
874                            $this->explode_selectors();
875                            $this->_add_token( SEL_END, $this->selector );
876                            $this->status     = 'is';
877                            $this->invalid_at = false;
878                            $this->selector   = '';
879                        }
880                    } elseif ( ! $pn ) {
881                        $this->sub_value .= $string[ $i ];
882
883                        if ( ctype_space( $string[ $i ] ) || ',' === $string[ $i ] ) {
884                            $this->optimise->subvalue();
885                            if ( '' !== $this->sub_value ) {
886                                $this->sub_value_arr[] = $this->sub_value;
887                                $this->sub_value       = '';
888                            }
889                        }
890                    }
891                    break;
892
893                /* Case in string */
894                case 'instr':
895                    $_str_char   = $this->str_char[ count( $this->str_char ) - 1 ];
896                    $_cur_string = $this->cur_string[ count( $this->cur_string ) - 1 ];
897                    $temp_add    = $string[ $i ];
898
899                    // Add another string to the stack. Strings can't be nested inside of quotes, only parentheses, but
900                    // parentheticals can be nested more than once.
901                    if ( ')' === $_str_char && ( '(' === $string[ $i ] || '"' === $string[ $i ] || '\'' === $string[ $i ] ) && ! self::escaped( $string, $i ) ) {
902                        $this->cur_string[]    = $string[ $i ];
903                        $this->str_char[]      = $string[ $i ] === '(' ? ')' : $string[ $i ];
904                        $this->from[]          = 'instr';
905                        $this->quoted_string[] = ! ( '(' === $string[ $i ] );
906                        continue 2;
907                    }
908
909                    if ( ')' !== $_str_char && ( "\n" === $string[ $i ] || "\r" === $string[ $i ] ) && ! ( '\\' === $string[ $i - 1 ] && ! self::escaped( $string, $i - 1 ) ) ) {
910                        $temp_add = '\\A';
911                        $this->log( 'Fixed incorrect newline in string', 'Warning' );
912                    }
913
914                    $_cur_string .= $temp_add;
915
916                    if ( $string[ $i ] === $_str_char && ! self::escaped( $string, $i ) ) {
917                        $_quoted_string = array_pop( $this->quoted_string );
918
919                        $this->status = array_pop( $this->from );
920
921                        if ( ! preg_match( '|[' . implode( '', $GLOBALS['csstidy']['whitespace'] ) . ']|uis', $_cur_string ) && 'content' !== $this->property ) {
922                            if ( ! $_quoted_string ) {
923                                if ( ')' !== $_str_char ) {
924                                    // Convert properties like
925                                    // font-family: 'Arial';
926                                    // to
927                                    // font-family: Arial;
928                                    // or
929                                    // url("abc")
930                                    // to
931                                    // url(abc).
932                                    $_cur_string = substr( $_cur_string, 1, -1 );
933                                }
934                            } else {
935                                $_quoted_string = false;
936                            }
937                        }
938
939                        array_pop( $this->cur_string );
940                        array_pop( $this->str_char );
941
942                        if ( ')' === $_str_char ) {
943                            $_cur_string = '(' . trim( substr( $_cur_string, 1, -1 ) ) . ')';
944                        }
945
946                        if ( 'iv' === $this->status ) {
947                            // phpcs:disable Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoNewLine
948                            // WPCOM hack: prevents CSSTidy from removing spaces after commas inside
949                            // declaration's values.
950                            // For more information, see D74626-code.
951                            /*if ( ! $_quoted_string ) {
952                                if ( strpos( $_cur_string, ',' ) !== false ) {
953                                    // we can on only remove space next to ','.
954                                    $_cur_string = implode( ',', array_map( 'trim', explode( ',', $_cur_string ) ) );
955                                }
956                                // and multiple spaces (too expensive).
957                                if ( strpos( $_cur_string, '  ' ) !== false ) {
958                                    $_cur_string = preg_replace( ',\s+,', ' ', $_cur_string );
959                                }
960                            }*/
961                            // phpcs:enable Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoNewLine
962                            $this->sub_value .= $_cur_string;
963                        } elseif ( 'is' === $this->status ) {
964                            $this->selector .= $_cur_string;
965                        } elseif ( 'instr' === $this->status ) {
966                            $this->cur_string[ count( $this->cur_string ) - 1 ] .= $_cur_string;
967                        }
968                    } else {
969                        $this->cur_string[ count( $this->cur_string ) - 1 ] = $_cur_string;
970                    }
971                    break;
972
973                /* Case in-comment */
974                case 'ic':
975                    if ( '*' === $string[ $i ] && '/' === $string[ $i + 1 ] ) {
976                        $this->status = array_pop( $this->from );
977                        ++$i;
978                        $this->_add_token( COMMENT, $cur_comment );
979                        $cur_comment = '';
980                    } else {
981                        $cur_comment .= $string[ $i ];
982                    }
983                    break;
984            }
985        }
986
987        $this->optimise->postparse();
988
989        $this->print->_reset();
990
991        @setlocale( LC_ALL, $old ); // Set locale back to original setting.
992
993        return ! ( empty( $this->css ) && empty( $this->import ) && empty( $this->charset ) && empty( $this->tokens ) && empty( $this->namespace ) );
994    }
995
996    /**
997     * Explodes selectors
998     *
999     * @access private
1000     * @version 1.0
1001     */
1002    public function explode_selectors() {
1003        // Explode multiple selectors.
1004        if ( $this->get_cfg( 'merge_selectors' ) === 1 ) {
1005            $new_sels             = array();
1006            $lastpos              = 0;
1007            $this->sel_separate[] = strlen( $this->selector );
1008            foreach ( $this->sel_separate as $num => $pos ) {
1009                if ( count( $this->sel_separate ) - 1 === $num ) {
1010                    ++$pos;
1011                }
1012
1013                $new_sels[] = substr( $this->selector, $lastpos, $pos - $lastpos - 1 );
1014                $lastpos    = $pos;
1015            }
1016
1017            if ( count( $new_sels ) > 1 ) {
1018                foreach ( $new_sels as $selector ) {
1019                    if ( isset( $this->css[ $this->at ][ $this->selector ] ) ) {
1020                        $this->merge_css_blocks( $this->at, $selector, $this->css[ $this->at ][ $this->selector ] );
1021                    }
1022                }
1023                unset( $this->css[ $this->at ][ $this->selector ] );
1024            }
1025        }
1026        $this->sel_separate = array();
1027    }
1028
1029    /**
1030     * Checks if a character is escaped (and returns true if it is)
1031     *
1032     * @param string  $string - the string.
1033     * @param integer $pos - the position.
1034     * @access public
1035     * @return bool
1036     * @version 1.02
1037     */
1038    public static function escaped( &$string, $pos ) {
1039        return ! ( @( '\\' !== $string[ $pos - 1 ] ) || self::escaped( $string, $pos - 1 ) );
1040    }
1041
1042    /**
1043     * Adds a property with value to the existing CSS code
1044     *
1045     * @param string $media - the media.
1046     * @param string $selector - the selector.
1047     * @param string $property - the property.
1048     * @param string $new_val - new value.
1049     * @access private
1050     * @version 1.2
1051     */
1052    public function css_add_property( $media, $selector, $property, $new_val ) {
1053        if ( $this->get_cfg( 'preserve_css' ) || '' === trim( $new_val ) ) {
1054            return;
1055        }
1056
1057        $this->added = true;
1058        if ( isset( $this->css[ $media ][ $selector ][ $property ] ) ) {
1059            if ( ( self::is_important( $this->css[ $media ][ $selector ][ $property ] ) && self::is_important( $new_val ) ) || ! self::is_important( $this->css[ $media ][ $selector ][ $property ] ) ) {
1060                $this->css[ $media ][ $selector ][ $property ] = trim( $new_val );
1061            }
1062        } else {
1063            $this->css[ $media ][ $selector ][ $property ] = trim( $new_val );
1064        }
1065    }
1066
1067    /**
1068     * Start a new media section.
1069     * Check if the media is not already known,
1070     * else rename it with extra spaces
1071     * to avoid merging
1072     *
1073     * @param string $media - the media.
1074     * @return string
1075     */
1076    public function css_new_media_section( $media ) {
1077        if ( $this->get_cfg( 'preserve_css' ) ) {
1078            return $media;
1079        }
1080
1081        // if the last @media is the same as this keep it.
1082        if ( ! $this->css || ! is_array( $this->css ) || empty( $this->css ) ) {
1083            return $media;
1084        }
1085        end( $this->css );
1086        $at = current( $this->css );
1087        if ( $at === $media ) {
1088            return $media;
1089        }
1090        while ( isset( $this->css[ $media ] ) ) {
1091            if ( is_numeric( $media ) ) {
1092                ++$media;
1093            } else {
1094                $media .= ' ';
1095            }
1096        }
1097        return $media;
1098    }
1099
1100    /**
1101     * Start a new selector.
1102     * If already referenced in this media section,
1103     * rename it with extra space to avoid merging
1104     * except if merging is required,
1105     * or last selector is the same (merge siblings)
1106     *
1107     * Never merge @font-face
1108     *
1109     * @param string $media - the media.
1110     * @param string $selector - the selector.
1111     * @return string
1112     */
1113    public function css_new_selector( $media, $selector ) {
1114        if ( $this->get_cfg( 'preserve_css' ) ) {
1115            return $selector;
1116        }
1117        $selector = trim( $selector );
1118        if ( strncmp( $selector, '@font-face', 10 ) !== 0 ) {
1119            if ( $this->settings['merge_selectors'] ) {
1120                return $selector;
1121            }
1122
1123            if ( ! $this->css || ! isset( $this->css[ $media ] ) || ! $this->css[ $media ] ) {
1124                return $selector;
1125            }
1126
1127            // if last is the same, keep it.
1128            end( $this->css[ $media ] );
1129            $sel = current( $this->css[ $media ] );
1130            if ( $sel === $selector ) {
1131                return $selector;
1132            }
1133        }
1134
1135        while ( isset( $this->css[ $media ][ $selector ] ) ) {
1136            $selector .= ' ';
1137        }
1138        return $selector;
1139    }
1140
1141    /**
1142     * Start a new propertie.
1143     * If already references in this selector,
1144     * rename it with extra space to avoid override
1145     *
1146     * @param string $media - the media.
1147     * @param string $selector - the selector.
1148     * @param string $property - the property.
1149     * @return string
1150     */
1151    public function css_new_property( $media, $selector, $property ) {
1152        if ( $this->get_cfg( 'preserve_css' ) ) {
1153            return $property;
1154        }
1155        if ( ! $this->css || ! isset( $this->css[ $media ][ $selector ] ) || ! $this->css[ $media ][ $selector ] ) {
1156            return $property;
1157        }
1158
1159        while ( isset( $this->css[ $media ][ $selector ][ $property ] ) ) {
1160            $property .= ' ';
1161        }
1162
1163        return $property;
1164    }
1165
1166    /**
1167     * Adds CSS to an existing media/selector
1168     *
1169     * @param string $media - the media.
1170     * @param string $selector - the selector.
1171     * @param array  $css_add - css being added.
1172     * @access private
1173     * @version 1.1
1174     */
1175    public function merge_css_blocks( $media, $selector, $css_add ) {
1176        foreach ( $css_add as $property => $value ) {
1177            $this->css_add_property( $media, $selector, $property, $value );
1178        }
1179    }
1180
1181    /**
1182     * Checks if $value is !important.
1183     *
1184     * @param string $value - the value.
1185     * @return bool
1186     * @access public
1187     * @version 1.0
1188     */
1189    public static function is_important( &$value ) {
1190        return ( ! strcasecmp( substr( str_replace( $GLOBALS['csstidy']['whitespace'], '', $value ), -10, 10 ), '!important' ) );
1191    }
1192
1193    /**
1194     * Returns a value without !important
1195     *
1196     * @param string $value - the value.
1197     * @return string
1198     * @access public
1199     * @version 1.0
1200     */
1201    public static function gvw_important( $value ) {
1202        if ( self::is_important( $value ) ) {
1203            $value = trim( $value );
1204            $value = substr( $value, 0, -9 );
1205            $value = trim( $value );
1206            $value = substr( $value, 0, -1 );
1207            $value = trim( $value );
1208            return $value;
1209        }
1210        return $value;
1211    }
1212
1213    /**
1214     * Checks if the next word in a string from pos is a CSS property
1215     *
1216     * @param string  $istring - if it's a string.
1217     * @param integer $pos - position.
1218     * @return bool
1219     * @access private
1220     * @version 1.2
1221     */
1222    public function property_is_next( $istring, $pos ) {
1223        $all_properties = & $GLOBALS['csstidy']['all_properties'];
1224        $istring        = substr( $istring, $pos, strlen( $istring ) - $pos );
1225        $pos            = strpos( $istring, ':' );
1226        if ( false === $pos ) {
1227            return false;
1228        }
1229        $istring = strtolower( trim( substr( $istring, 0, $pos ) ) );
1230        if ( isset( $all_properties[ $istring ] ) ) {
1231            $this->log( 'Added semicolon to the end of declaration', 'Warning' );
1232            return true;
1233        }
1234        return false;
1235    }
1236
1237    /**
1238     * Checks if a property is valid
1239     *
1240     * @param string $property - the property.
1241     * @return bool
1242     * @access public
1243     * @version 1.0
1244     */
1245    public function property_is_valid( $property ) {
1246        $property = strtolower( $property );
1247        if ( in_array( trim( $property ), $GLOBALS['csstidy']['multiple_properties'], true ) ) {
1248            $property = trim( $property );
1249        }
1250        $all_properties = & $GLOBALS['csstidy']['all_properties'];
1251        return ( isset( $all_properties[ $property ] ) && strpos( $all_properties[ $property ], strtoupper( $this->get_cfg( 'css_level' ) ) ) !== false );
1252    }
1253
1254    /**
1255     * Accepts a list of strings (e.g., the argument to format() in a @font-face src property)
1256     * and returns a list of the strings.  Converts things like:
1257     *
1258     * Format(abc) => format("abc")
1259     * format(abc def) => format("abc","def")
1260     * format(abc "def") => format("abc","def")
1261     * format(abc, def, ghi) => format("abc","def","ghi")
1262     * format("abc",'def') => format("abc","def")
1263     * format("abc, def, ghi") => format("abc, def, ghi")
1264     *
1265     * @param string $value - the value.
1266     * @return array
1267     */
1268    public function parse_string_list( $value ) {
1269        $value = trim( $value );
1270
1271        // Case: if it's empty.
1272        if ( ! $value ) {
1273            return array();
1274        }
1275
1276        $strings = array();
1277
1278        $in_str         = false;
1279        $current_string = '';
1280
1281        for ( $i = 0, $_len = strlen( $value ); $i < $_len; $i++ ) {
1282            if ( ( ',' === $value[ $i ] || ' ' === $value[ $i ] ) && true === $in_str ) {
1283                $in_str         = false;
1284                $strings[]      = $current_string;
1285                $current_string = '';
1286            } elseif ( '"' === $value[ $i ] || "'" === $value[ $i ] ) {
1287                if ( $in_str === $value[ $i ] ) {
1288                    $strings[]      = $current_string;
1289                    $in_str         = false;
1290                    $current_string = '';
1291                    continue;
1292                } elseif ( ! $in_str ) {
1293                    $in_str = $value[ $i ];
1294                }
1295            } elseif ( $in_str ) {
1296                $current_string .= $value[ $i ];
1297            } elseif ( ! preg_match( '/[\s,]/', $value[ $i ] ) ) {
1298                $in_str         = true;
1299                $current_string = $value[ $i ];
1300            }
1301        }
1302
1303        if ( $current_string ) {
1304            $strings[] = $current_string;
1305        }
1306
1307        return $strings;
1308    }
1309}