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

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

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