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

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

1,364 lines 40.7 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 * Loads a new template.
476 *
477 * @since 1.0.0
478 *
479 * @link https://csstidy.sourceforge.net/templates.php
480 *
481 * @param string $content Either file name (if $from_file is true), content of a template file, "default", "low", high", or "highest".
482 * @param bool $from_file Optional. Uses $content as filename if true.
483 */
484 protected function load_template( string $content, bool $from_file = true ): void {
485 if ( in_array( $content, array( 'default', 'low', 'high', 'highest' ), true ) ) {
486 $this->template = $this->data['csstidy']['predefined_templates'][ $content ];
487 return;
488 }
489
490 if ( $from_file ) {
491 $content = strip_tags( file_get_contents( $content ), '<span>' );
492 }
493
494 // Unify newlines (because the output also only uses \n).
495 $content = str_replace( "\r\n", "\n", $content );
496 $this->template = explode( '|', $content );
497 }
498
499 /**
500 * Starts parsing from URL.
501 *
502 * @since 1.0.0
503 *
504 * @param string $url URL.
505 * @return bool Whether the CSS code could be parsed successfully.
506 */
507 protected function parse_from_url( string $url ): bool {
508 $data = file_get_contents( $url );
509 if ( false === $data ) {
510 return false;
511 }
512 return $this->parse( $data );
513 }
514
515 /**
516 * Checks if there is a token at the current position.
517 *
518 * @since 1.0.0
519 *
520 * @param string $a_string String.
521 * @param int $i i.
522 * @return bool [return value]
523 */
524 protected function is_token( string &$a_string, int $i ): bool {
525 return ( str_contains( $this->tokens_list, $a_string[ $i ] ) && ! $this->escaped( $a_string, $i ) );
526 }
527
528 /**
529 * Parses CSS in a string. The output is saved as an array in $this->css.
530 *
531 * @since 1.0.0
532 *
533 * @param string $a_string The CSS code.
534 * @return bool Whether the CSS code could be parsed successfully.
535 */
536 public function parse( string $a_string ): bool {
537 // Temporarily set locale to en_US in order to handle floats properly.
538 $old = @setlocale( LC_ALL, 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
539 @setlocale( LC_ALL, 'C' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
540
541 $at_rules = &$this->data['csstidy']['at_rules'];
542 $quoted_string_properties = &$this->data['csstidy']['quoted_string_properties'];
543
544 $this->css = array();
545 $this->print->input_css = $a_string;
546 $a_string = str_replace( "\r\n", "\n", $a_string ) . ' ';
547 $cur_comment = '';
548 $cur_at = '';
549
550 for ( $i = 0, $size = strlen( $a_string ); $i < $size; $i++ ) {
551 if ( "\n" === $a_string[ $i ] || "\r" === $a_string[ $i ] ) {
552 ++$this->line;
553 }
554
555 switch ( $this->status ) {
556 /* Case in at-block */
557 case 'at':
558 if ( $this->is_token( $a_string, $i ) ) {
559 if ( '/' === $a_string[ $i ] && '*' === @$a_string[ $i + 1 ] ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
560 $this->status = 'ic';
561 ++$i;
562 $this->from[] = 'at';
563 } elseif ( '{' === $a_string[ $i ] ) {
564 $this->status = 'is';
565 $this->at = $this->css_new_media_section( $this->at, $cur_at );
566 $this->_add_token( self::AT_START, $this->at );
567 } elseif ( ',' === $a_string[ $i ] ) {
568 $cur_at = trim( $cur_at ) . ',';
569 } elseif ( '\\' === $a_string[ $i ] ) {
570 $cur_at .= $this->_unicode( $a_string, $i );
571 } elseif ( in_array( $a_string[ $i ], array( '(', ')', ':', '.', '/' ), true ) ) {
572 // Fix for complicated media, i.e @media screen and (-webkit-min-device-pixel-ratio:1.5).
573 // '/' is included for ratios in Opera: (-o-min-device-pixel-ratio: 3/2).
574 $cur_at .= $a_string[ $i ];
575 }
576 } else {
577 $lastpos = strlen( $cur_at ) - 1;
578 if ( ! ( ( ctype_space( $cur_at[ $lastpos ] ) || ( $this->is_token( $cur_at, $lastpos ) && ',' === $cur_at[ $lastpos ] ) ) && ctype_space( $a_string[ $i ] ) ) ) {
579 $cur_at .= $a_string[ $i ];
580 }
581 }
582 break;
583 /* Case in-selector */
584 case 'is':
585 if ( $this->is_token( $a_string, $i ) ) {
586 if ( '/' === $a_string[ $i ] && '*' === @$a_string[ $i + 1 ] && '' === trim( $this->selector ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
587 $this->status = 'ic';
588 ++$i;
589 $this->from[] = 'is';
590 } elseif ( '@' === $a_string[ $i ] && '' === trim( $this->selector ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
591 // Check for at-rule.
592 $this->invalid_at = true;
593 foreach ( $at_rules as $name => $type ) {
594 if ( ! strcasecmp( substr( $a_string, $i + 1, strlen( $name ) ), $name ) ) {
595 if ( 'at' === $type ) {
596 $cur_at = '@' . $name;
597 } else {
598 $this->selector = '@' . $name;
599 }
600 if ( 'atis' === $type ) {
601 $this->next_selector_at = ( $this->next_selector_at ? $this->next_selector_at : ( $this->at ? $this->at : self::DEFAULT_AT ) );
602 $this->at = $this->css_new_media_section( $this->at, ' ', true );
603 $type = 'is';
604 }
605 $this->status = $type;
606 $i += strlen( $name );
607 $this->invalid_at = false;
608 break;
609 }
610 }
611
612 if ( $this->invalid_at ) {
613 $this->selector = '@';
614 $invalid_at_name = '';
615 for ( $j = $i + 1; $j < $size; ++$j ) {
616 if ( ! ctype_alpha( $a_string[ $j ] ) ) {
617 break;
618 }
619 $invalid_at_name .= $a_string[ $j ];
620 }
621 $this->log( 'Invalid @-rule: ' . $invalid_at_name . ' (removed)', 'Warning' );
622 }
623 } elseif ( ( '"' === $a_string[ $i ] || "'" === $a_string[ $i ] ) ) {
624 $this->cur_string[] = $a_string[ $i ];
625 $this->status = 'instr';
626 $this->str_char[] = $a_string[ $i ];
627 $this->from[] = 'is';
628 /* Fixing CSS3 attribute selectors, i.e. a[href$=".mp3" */
629 $this->quoted_string[] = ( '=' === $a_string[ $i - 1 ] );
630 } elseif ( $this->invalid_at && ';' === $a_string[ $i ] ) {
631 $this->invalid_at = false;
632 $this->status = 'is';
633 if ( $this->next_selector_at ) {
634 $this->at = $this->css_close_media_section( $this->at );
635 $this->at = $this->css_new_media_section( $this->at, $this->next_selector_at );
636 $this->next_selector_at = '';
637 }
638 } elseif ( '{' === $a_string[ $i ] ) {
639 $this->status = 'ip';
640 if ( '' === $this->at ) {
641 $this->at = $this->css_new_media_section( $this->at, self::DEFAULT_AT );
642 }
643 $this->selector = $this->css_new_selector( $this->at, $this->selector );
644 $this->_add_token( self::SEL_START, $this->selector );
645 $this->added = false;
646 } elseif ( '}' === $a_string[ $i ] ) {
647 $this->_add_token( self::AT_END, $this->at );
648 $this->at = $this->css_close_media_section( $this->at );
649 $this->selector = '';
650 $this->sel_separate = array();
651 } elseif ( ',' === $a_string[ $i ] ) {
652 $this->selector = trim( $this->selector ) . ',';
653 $this->sel_separate[] = strlen( $this->selector );
654 } elseif ( '\\' === $a_string[ $i ] ) {
655 $this->selector .= $this->_unicode( $a_string, $i );
656 } 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
657 // Remove unnecessary universal selector, FS#147, but not comment in selector.
658 } else {
659 $this->selector .= $a_string[ $i ];
660 }
661 } else {
662 $lastpos = strlen( $this->selector ) - 1;
663 if ( -1 === $lastpos || ! ( ( ctype_space( $this->selector[ $lastpos ] ) || ( $this->is_token( $this->selector, $lastpos ) && ',' === $this->selector[ $lastpos ] ) ) && ctype_space( $a_string[ $i ] ) ) ) {
664 $this->selector .= $a_string[ $i ];
665 }
666 }
667 break;
668 /* Case in-property */
669 case 'ip':
670 if ( $this->is_token( $a_string, $i ) ) {
671 if ( ( ':' === $a_string[ $i ] || '=' === $a_string[ $i ] ) && '' !== $this->property ) {
672 $this->status = 'iv';
673 if ( ! $this->get_cfg( 'discard_invalid_properties' ) || $this->property_is_valid( $this->property ) ) {
674 $this->property = $this->css_new_property( $this->at, $this->selector, $this->property );
675 $this->property = strtolower( $this->property );
676 $this->_add_token( self::PROPERTY, $this->property );
677 }
678 } elseif ( '/' === $a_string[ $i ] && '*' === @$a_string[ $i + 1 ] && '' === $this->property ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
679 $this->status = 'ic';
680 ++$i;
681 $this->from[] = 'ip';
682 } elseif ( '}' === $a_string[ $i ] ) {
683 $this->explode_selectors();
684 $this->status = 'is';
685 $this->invalid_at = false;
686 $this->_add_token( self::SEL_END, $this->selector );
687 $this->selector = '';
688 $this->property = '';
689 if ( $this->next_selector_at ) {
690 $this->at = $this->css_close_media_section( $this->at );
691 $this->at = $this->css_new_media_section( $this->at, $this->next_selector_at );
692 $this->next_selector_at = '';
693 }
694 } elseif ( ';' === $a_string[ $i ] ) {
695 $this->property = '';
696 } elseif ( '\\' === $a_string[ $i ] ) {
697 $this->property .= $this->_unicode( $a_string, $i );
698 } elseif ( ( '' === $this->property && ! ctype_space( $a_string[ $i ] ) ) || ( '/' === $this->property || '/' === $a_string[ $i ] ) ) {
699 // else this is dumb IE a hack, keep it.
700 // including /.
701 $this->property .= $a_string[ $i ];
702 }
703 } elseif ( ! ctype_space( $a_string[ $i ] ) ) {
704 $this->property .= $a_string[ $i ];
705 }
706 break;
707 /* Case in-value */
708 case 'iv':
709 $pn = ( ( ( "\n" === $a_string[ $i ] || "\r" === $a_string[ $i ] ) && $this->property_is_next( $a_string, $i + 1 ) ) || ( strlen( $a_string ) - 1 ) === $i );
710 if ( ( $this->is_token( $a_string, $i ) || $pn ) && ( ! ( ',' === $a_string[ $i ] && ! ctype_space( $a_string[ $i + 1 ] ) ) ) ) {
711 if ( '/' === $a_string[ $i ] && '*' === @$a_string[ $i + 1 ] ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
712 $this->status = 'ic';
713 ++$i;
714 $this->from[] = 'iv';
715 } elseif ( ( '"' === $a_string[ $i ] || "'" === $a_string[ $i ] || '(' === $a_string[ $i ] ) ) {
716 $this->cur_string[] = $a_string[ $i ];
717 $this->str_char[] = ( '(' === $a_string[ $i ] ) ? ')' : $a_string[ $i ];
718 $this->status = 'instr';
719 $this->from[] = 'iv';
720 $this->quoted_string[] = in_array( strtolower( $this->property ), $quoted_string_properties, true );
721 } elseif ( ',' === $a_string[ $i ] ) {
722 $this->sub_value = trim( $this->sub_value ) . ',';
723 } elseif ( '\\' === $a_string[ $i ] ) {
724 $this->sub_value .= $this->_unicode( $a_string, $i );
725 } elseif ( ';' === $a_string[ $i ] || $pn ) {
726 if ( '@' === $this->selector[0] && isset( $at_rules[ substr( $this->selector, 1 ) ] ) && 'iv' === $at_rules[ substr( $this->selector, 1 ) ] ) {
727 $this->status = 'is';
728
729 switch ( $this->selector ) {
730 case '@charset':
731 // Add quotes to charset.
732 $this->sub_value_arr[] = '"' . trim( $this->sub_value ) . '"';
733 $this->charset = $this->sub_value_arr[0];
734 break;
735 case '@namespace':
736 // Add quotes to namespace.
737 $this->sub_value_arr[] = '"' . trim( $this->sub_value ) . '"';
738 $this->namespace = implode( ' ', $this->sub_value_arr );
739 break;
740 case '@import':
741 $this->sub_value = trim( $this->sub_value );
742
743 if ( empty( $this->sub_value_arr ) ) {
744 // Quote URLs in imports only if they're not already inside url() and not already quoted.
745 if ( ! str_starts_with( $this->sub_value, 'url(' ) ) {
746 if ( ! str_ends_with( $this->sub_value, $this->sub_value[0] ) && in_array( $this->sub_value[0], array( "'", '"' ), true ) ) {
747 $this->sub_value = '"' . $this->sub_value . '"';
748 }
749 }
750 }
751
752 $this->sub_value_arr[] = $this->sub_value;
753 $this->import[] = implode( ' ', $this->sub_value_arr );
754 break;
755 }
756
757 $this->sub_value_arr = array();
758 $this->sub_value = '';
759 $this->selector = '';
760 $this->sel_separate = array();
761 } else {
762 $this->status = 'ip';
763 }
764 } elseif ( '}' !== $a_string[ $i ] ) {
765 $this->sub_value .= $a_string[ $i ];
766 }
767 if ( ( '}' === $a_string[ $i ] || ';' === $a_string[ $i ] || $pn ) && ! empty( $this->selector ) ) {
768 if ( '' === $this->at ) {
769 $this->at = $this->css_new_media_section( $this->at, self::DEFAULT_AT );
770 }
771
772 // Case settings.
773 if ( $this->get_cfg( 'lowercase_s' ) ) {
774 $this->selector = strtolower( $this->selector );
775 }
776 $this->property = strtolower( $this->property );
777
778 $this->optimise->subvalue();
779 if ( '' !== $this->sub_value ) {
780 $this->sub_value_arr[] = $this->sub_value;
781 $this->sub_value = '';
782 }
783
784 $this->value = '';
785 while ( count( $this->sub_value_arr ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
786 $sub = array_shift( $this->sub_value_arr );
787 if ( strstr( $this->selector, 'font-face' ) ) {
788 $sub = $this->quote_font_format( $sub );
789 }
790
791 if ( '' !== $sub ) {
792 if ( strlen( $this->value ) && ( ! str_ends_with( $this->value, ',' ) || $this->get_cfg( 'preserve_css' ) ) ) {
793 $this->value .= ' ';
794 }
795 $this->value .= $sub;
796 }
797 }
798
799 $this->optimise->value();
800
801 $valid = $this->property_is_valid( $this->property );
802 if ( ( ! $this->invalid_at || $this->get_cfg( 'preserve_css' ) ) && ( ! $this->get_cfg( 'discard_invalid_properties' ) || $valid ) ) {
803 $this->css_add_property( $this->at, $this->selector, $this->property, $this->value );
804 $this->_add_token( self::VALUE, $this->value );
805 $this->optimise->shorthands();
806 }
807 if ( ! $valid ) {
808 if ( $this->get_cfg( 'discard_invalid_properties' ) ) {
809 $this->log( 'Removed invalid property: ' . $this->property, 'Warning' );
810 } else {
811 $this->log( 'Invalid property in ' . strtoupper( $this->get_cfg( 'css_level' ) ) . ': ' . $this->property, 'Warning' );
812 }
813 }
814
815 $this->property = '';
816 $this->sub_value_arr = array();
817 $this->value = '';
818 }
819 if ( '}' === $a_string[ $i ] ) {
820 $this->explode_selectors();
821 $this->_add_token( self::SEL_END, $this->selector );
822 $this->status = 'is';
823 $this->invalid_at = false;
824 $this->selector = '';
825 if ( $this->next_selector_at ) {
826 $this->at = $this->css_close_media_section( $this->at );
827 $this->at = $this->css_new_media_section( $this->at, $this->next_selector_at );
828 $this->next_selector_at = '';
829 }
830 }
831 } elseif ( ! $pn ) {
832 $this->sub_value .= $a_string[ $i ];
833
834 if ( ctype_space( $a_string[ $i ] ) || ',' === $a_string[ $i ] ) {
835 $this->optimise->subvalue();
836 if ( '' !== $this->sub_value ) {
837 $this->sub_value_arr[] = $this->sub_value;
838 $this->sub_value = '';
839 }
840 }
841 }
842 break;
843 /* Case in string */
844 case 'instr':
845 $_str_char = $this->str_char[ count( $this->str_char ) - 1 ];
846 $_cur_string = $this->cur_string[ count( $this->cur_string ) - 1 ];
847 $_quoted_string = $this->quoted_string[ count( $this->quoted_string ) - 1 ];
848 $temp_add = $a_string[ $i ];
849
850 // Add another string to the stack. Strings can't be nested inside of quotes, only parentheses,
851 // but parentheticals can be nested more than once.
852 if ( ')' === $_str_char && ( '(' === $a_string[ $i ] || '"' === $a_string[ $i ] || '\\' === $a_string[ $i ] ) && ! $this->escaped( $a_string, $i ) ) {
853 $this->cur_string[] = $a_string[ $i ];
854 $this->str_char[] = ( '(' === $a_string[ $i ] ) ? ')' : $a_string[ $i ];
855 $this->from[] = 'instr';
856 $this->quoted_string[] = ( ')' === $_str_char && '(' !== $a_string[ $i ] && '(' === trim( $_cur_string ) ) ? $_quoted_string : ( '(' !== $a_string[ $i ] );
857 continue 2;
858 }
859
860 if ( ')' !== $_str_char && ( "\n" === $a_string[ $i ] || "\r" === $a_string[ $i ] ) && ! ( '\\' === $a_string[ $i - 1 ] && ! $this->escaped( $a_string, $i - 1 ) ) ) {
861 $temp_add = '\\A';
862 $this->log( 'Fixed incorrect newline in string', 'Warning' );
863 }
864
865 $_cur_string .= $temp_add;
866
867 if ( $a_string[ $i ] === $_str_char && ! $this->escaped( $a_string, $i ) ) {
868 $this->status = array_pop( $this->from );
869
870 if ( ! preg_match( '|[' . implode( '', $this->data['csstidy']['whitespace'] ) . ']|uis', $_cur_string ) && 'content' !== $this->property ) {
871 if ( ! $_quoted_string ) {
872 if ( ')' !== $_str_char ) {
873 /*
874 * Convert properties like
875 * font-family: 'Arial';
876 * to
877 * font-family: Arial;
878 * or
879 * url("abc")
880 * to
881 * url(abc)
882 */
883 $_cur_string = substr( $_cur_string, 1, -1 );
884 }
885 } else {
886 $_quoted_string = false;
887 }
888 }
889
890 array_pop( $this->cur_string );
891 array_pop( $this->quoted_string );
892 array_pop( $this->str_char );
893
894 if ( ')' === $_str_char ) {
895 $_cur_string = '(' . trim( substr( $_cur_string, 1, -1 ) ) . ')';
896 }
897
898 if ( 'iv' === $this->status ) {
899 if ( ! $_quoted_string ) {
900 if ( str_contains( $_cur_string, ',' ) ) {
901 // We can on only remove space next to ','.
902 $_cur_string = implode( ',', array_map( 'trim', explode( ',', $_cur_string ) ) );
903 }
904 // and multiple spaces (too expensive).
905 if ( str_contains( $_cur_string, ' ' ) ) {
906 $_cur_string = preg_replace( ',\s+,', ' ', $_cur_string );
907 }
908 }
909 $this->sub_value .= $_cur_string;
910 } elseif ( 'is' === $this->status ) {
911 $this->selector .= $_cur_string;
912 } elseif ( 'instr' === $this->status ) {
913 $this->cur_string[ count( $this->cur_string ) - 1 ] .= $_cur_string;
914 }
915 } else {
916 $this->cur_string[ count( $this->cur_string ) - 1 ] = $_cur_string;
917 }
918 break;
919 /* Case in-comment */
920 case 'ic':
921 if ( '*' === $a_string[ $i ] && '/' === $a_string[ $i + 1 ] ) {
922 $this->status = array_pop( $this->from );
923 ++$i;
924 if ( strlen( $cur_comment ) > 1 && str_starts_with( $cur_comment, '!' ) ) {
925 $this->_add_token( self::IMPORTANT_COMMENT, $cur_comment );
926 $this->css_add_important_comment( $cur_comment );
927 } else {
928 $this->_add_token( self::COMMENT, $cur_comment );
929 }
930 $cur_comment = '';
931 } else {
932 $cur_comment .= $a_string[ $i ];
933 }
934 break;
935 }
936 }
937
938 $this->optimise->postparse();
939 $this->print->_reset();
940
941 // Set locale back to original setting.
942 @setlocale( LC_ALL, $old ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
943
944 return ! ( empty( $this->css ) && empty( $this->import ) && empty( $this->charset ) && empty( $this->tokens ) && empty( $this->namespace ) );
945 }
946
947 /**
948 * Quoting: format() in font-face needs quoted values for some browser (FF at least).
949 *
950 * @since 1.0.0
951 *
952 * @param string $value Value.
953 * @return string String.
954 */
955 protected function quote_font_format( string $value ): string {
956 if ( str_starts_with( $value, 'format' ) ) {
957 $p = strpos( $value, ')', 7 );
958 $end = substr( $value, $p );
959 $format_strings = $this->parse_string_list( substr( $value, 7, $p - 7 ) );
960 if ( ! $format_strings ) {
961 $value = '';
962 } else {
963 $value = 'format(';
964 foreach ( $format_strings as $format_string ) {
965 $value .= '"' . str_replace( '"', '\\"', $format_string ) . '",';
966 }
967 $value = substr( $value, 0, -1 ) . $end;
968 }
969 }
970 return $value;
971 }
972
973 /**
974 * Explodes selectors.
975 *
976 * @since 1.0.0
977 */
978 protected function explode_selectors(): void {
979 // Explode multiple selectors.
980 if ( 1 === $this->get_cfg( 'merge_selectors' ) ) {
981 $new_sels = array();
982 $lastpos = 0;
983 $this->sel_separate[] = strlen( $this->selector );
984 foreach ( $this->sel_separate as $num => $pos ) {
985 if ( ( count( $this->sel_separate ) - 1 ) === $num ) {
986 ++$pos;
987 }
988
989 $new_sels[] = substr( $this->selector, $lastpos, $pos - $lastpos - 1 );
990 $lastpos = $pos;
991 }
992
993 if ( count( $new_sels ) > 1 ) {
994 foreach ( $new_sels as $selector ) {
995 if ( isset( $this->css[ $this->at ][ $this->selector ] ) ) {
996 $this->merge_css_blocks( $this->at, $selector, $this->css[ $this->at ][ $this->selector ] );
997 }
998 }
999 unset( $this->css[ $this->at ][ $this->selector ] );
1000 }
1001 }
1002 $this->sel_separate = array();
1003 }
1004
1005 /**
1006 * Checks if a character is escaped.
1007 *
1008 * @since 1.0.0
1009 *
1010 * @param string $a_string String.
1011 * @param int $pos Position.
1012 * @return bool Whether a character is escaped.
1013 */
1014 public function escaped( string &$a_string, int $pos ): bool {
1015 return $pos ? ! ( @( '\\' !== $a_string[ $pos - 1 ] ) || $this->escaped( $a_string, $pos - 1 ) ) : false; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1016 }
1017
1018 /**
1019 * Adds an important comment to the CSS code (one we want to keep when minifying).
1020 *
1021 * @since 1.10.0
1022 *
1023 * @param string $comment CSS Comment.
1024 */
1025 protected function css_add_important_comment( string $comment ): void {
1026 if ( $this->get_cfg( 'preserve_css' ) || '' === trim( $comment ) ) {
1027 return;
1028 }
1029 if ( ! isset( $this->css['!'] ) ) {
1030 $this->css['!'] = '';
1031 } else {
1032 $this->css['!'] .= "\n";
1033 }
1034 $this->css['!'] .= $comment;
1035 }
1036
1037 /**
1038 * Adds a property with value to the existing CSS code.
1039 *
1040 * @since 1.0.0
1041 *
1042 * @param string $media Media.
1043 * @param string $selector Selector.
1044 * @param string $property Property.
1045 * @param string $new_val New value.
1046 */
1047 protected function css_add_property( string $media, string $selector, string $property, string $new_val ): void {
1048 if ( $this->get_cfg( 'preserve_css' ) || '' === trim( $new_val ) ) {
1049 return;
1050 }
1051
1052 $this->added = true;
1053 if ( isset( $this->css[ $media ][ $selector ][ $property ] ) ) {
1054 if ( ( $this->is_important( $this->css[ $media ][ $selector ][ $property ] ) && $this->is_important( $new_val ) ) || ! $this->is_important( $this->css[ $media ][ $selector ][ $property ] ) ) {
1055 $this->css[ $media ][ $selector ][ $property ] = trim( $new_val );
1056 }
1057 } else {
1058 $this->css[ $media ][ $selector ][ $property ] = trim( $new_val );
1059 }
1060 }
1061
1062 /**
1063 * Checks if a current media section is the continuation of the last one.
1064 * If not increase the name of the media section to avoid a merging.
1065 *
1066 * @since 1.10.0
1067 *
1068 * @param int|string $media Media.
1069 * @return int|string [return value]
1070 */
1071 protected function css_check_last_media_section_or_inc( $media ) /* : int|string */ {
1072 // Are we starting?
1073 if ( empty( $this->css ) || ! is_array( $this->css ) ) {
1074 return $media;
1075 }
1076 // If the last @media is the same as this, keep it.
1077 $at = array_key_last( $this->css );
1078 if ( $at === $media ) {
1079 return $media;
1080 }
1081 // Otherwise increase the section in the array.
1082 while ( isset( $this->css[ $media ] ) ) {
1083 if ( is_numeric( $media ) ) {
1084 ++$media;
1085 } else {
1086 $media .= ' ';
1087 }
1088 }
1089 return $media;
1090 }
1091
1092 /**
1093 * Starts a new media section.
1094 *
1095 * Check if the media is not already known, else rename it with extra spaces to avoid merging.
1096 *
1097 * @since 1.0.0
1098 *
1099 * @param string|int $current_media Media.
1100 * @param string $new_media Media.
1101 * @param bool $at_root Optional. Default false.
1102 * @return int|string [return value]
1103 */
1104 protected function css_new_media_section( $current_media, string $new_media, bool $at_root = false ) /* : int|string */ {
1105 if ( $this->get_cfg( 'preserve_css' ) ) {
1106 return $new_media;
1107 }
1108 // If we already are in a media and CSS level is 3, manage nested medias.
1109 if ( $current_media
1110 && ! $at_root
1111 // Numeric $current_media means self::DEFAULT_AT or inc.
1112 && ! is_numeric( $current_media )
1113 && str_starts_with( $this->get_cfg( 'css_level' ), 'CSS3' ) ) {
1114 $new_media = rtrim( $current_media ) . '{' . rtrim( $new_media );
1115 }
1116 return $this->css_check_last_media_section_or_inc( $new_media );
1117 }
1118
1119 /**
1120 * Closes a media section.
1121 *
1122 * Finds the parent media we were in before or the root.
1123 *
1124 * @since 1.10.0
1125 *
1126 * @param string $current_media Current Media.
1127 * @return string [return value]
1128 */
1129 protected function css_close_media_section( string $current_media ): string {
1130 if ( $this->get_cfg( 'preserve_css' ) ) {
1131 return '';
1132 }
1133 if ( str_contains( $current_media, '{' ) ) {
1134 $current_media = explode( '{', $current_media );
1135 array_pop( $current_media );
1136 $current_media = implode( '{', $current_media );
1137 return $current_media;
1138 }
1139
1140 return '';
1141 }
1142
1143 /**
1144 * Starts a new selector.
1145 *
1146 * If already referenced in this media section, rename it with extra space to avoid merging,
1147 * except if merging is required, or last selector is the same (merge siblings).
1148 * Never merge @font-face.
1149 *
1150 * @since 1.0.0
1151 *
1152 * @param string $media Media.
1153 * @param string $selector Selector.
1154 * @return string [return value]
1155 */
1156 protected function css_new_selector( string $media, string $selector ): string {
1157 if ( $this->get_cfg( 'preserve_css' ) ) {
1158 return $selector;
1159 }
1160 $selector = trim( $selector );
1161 if ( ! str_starts_with( $selector, '@font-face' ) ) {
1162 if ( false !== $this->settings['merge_selectors'] ) {
1163 return $selector;
1164 }
1165
1166 if ( empty( $this->css ) || ! isset( $this->css[ $media ] ) || ! $this->css[ $media ] ) {
1167 return $selector;
1168 }
1169
1170 // If last is the same, keep it.
1171 $sel = array_key_last( $this->css[ $media ] );
1172 if ( $sel === $selector ) {
1173 return $selector;
1174 }
1175 }
1176
1177 while ( isset( $this->css[ $media ][ $selector ] ) ) {
1178 $selector .= ' ';
1179 }
1180 return $selector;
1181 }
1182
1183 /**
1184 * Starts a new property.
1185 *
1186 * If already referenced in this selector, rename it with extra space to avoid override.
1187 *
1188 * @since 1.0.0
1189 *
1190 * @param string $media Media.
1191 * @param string $selector Selector.
1192 * @param string $property Property.
1193 * @return string [return value]
1194 */
1195 protected function css_new_property( string $media, string $selector, string $property ): string {
1196 if ( $this->get_cfg( 'preserve_css' ) ) {
1197 return $property;
1198 }
1199 if ( empty( $this->css ) || ! isset( $this->css[ $media ][ $selector ] ) || ! $this->css[ $media ][ $selector ] ) {
1200 return $property;
1201 }
1202
1203 while ( isset( $this->css[ $media ][ $selector ][ $property ] ) ) {
1204 $property .= ' ';
1205 }
1206
1207 return $property;
1208 }
1209
1210 /**
1211 * Adds CSS to an existing media/selector.
1212 *
1213 * @since 1.0.0
1214 *
1215 * @param string $media Media.
1216 * @param string $selector Selector.
1217 * @param array<string, string> $css_add Additional CSS.
1218 */
1219 public function merge_css_blocks( string $media, string $selector, array $css_add ): void {
1220 foreach ( $css_add as $property => $value ) {
1221 $this->css_add_property( $media, $selector, $property, $value );
1222 }
1223 }
1224
1225 /**
1226 * Checks if $value is !important.
1227 *
1228 * @since 1.0.0
1229 *
1230 * @param string $value Value.
1231 * @return bool Whether the value has the !important keyword.
1232 */
1233 public function is_important( string &$value ): bool {
1234 return (
1235 str_contains( $value, '!' ) // Quick test.
1236 && ! strcasecmp( substr( str_replace( $this->data['csstidy']['whitespace'], '', $value ), -10, 10 ), '!important' ) );
1237 }
1238
1239 /**
1240 * Returns a value without !important.
1241 *
1242 * @since 1.0.0
1243 *
1244 * @param string $value Value.
1245 * @return string Value without the !important;
1246 */
1247 public function gvw_important( string $value ): string {
1248 if ( $this->is_important( $value ) ) {
1249 $value = trim( $value );
1250 $value = substr( $value, 0, -9 );
1251 $value = trim( $value );
1252 $value = substr( $value, 0, -1 );
1253 $value = trim( $value );
1254 return $value;
1255 }
1256 return $value;
1257 }
1258
1259 /**
1260 * Checks if the next word in a string from pos is a CSS property.
1261 *
1262 * @since 1.0.0
1263 *
1264 * @param string $istring String.
1265 * @param int $pos Position.
1266 * @return bool [return value]
1267 */
1268 protected function property_is_next( string $istring, int $pos ): bool {
1269 $all_properties = &$this->data['csstidy']['all_properties'];
1270 $istring = substr( $istring, $pos, strlen( $istring ) - $pos );
1271 $pos = strpos( $istring, ':' );
1272 if ( false === $pos ) {
1273 return false;
1274 }
1275 $istring = strtolower( trim( substr( $istring, 0, $pos ) ) );
1276 if ( isset( $all_properties[ $istring ] ) ) {
1277 $this->log( 'Added semicolon to the end of declaration', 'Warning' );
1278 return true;
1279 }
1280 return false;
1281 }
1282
1283 /**
1284 * Checks if a property is valid.
1285 *
1286 * @since 1.0.0
1287 *
1288 * @param string $property Property.
1289 * @return bool Whether the property is valid.
1290 */
1291 public function property_is_valid( string $property ): bool {
1292 $property = strtolower( $property );
1293 if ( str_starts_with( $property, '--' ) ) {
1294 $property = '--custom'; // Replace custom properties with a temporary placeholder that is marked as valid in the list of properties.
1295 } elseif ( in_array( trim( $property ), $this->data['csstidy']['multiple_properties'], true ) ) {
1296 $property = trim( $property );
1297 }
1298 $all_properties = &$this->data['csstidy']['all_properties'];
1299 return isset( $all_properties[ $property ] ) && str_contains( $all_properties[ $property ], strtoupper( $this->get_cfg( 'css_level' ) ) );
1300 }
1301
1302 /**
1303 * Accepts a list of strings (e.g. the argument to format() in a @font-face src property)
1304 * and returns a list of the strings. Converts things like:
1305 * format(abc) => format("abc")
1306 * format(abc def) => format("abc","def")
1307 * format(abc "def") => format("abc","def")
1308 * format(abc, def, ghi) => format("abc","def","ghi")
1309 * format("abc",'def') => format("abc","def")
1310 * format("abc, def, ghi") => format("abc, def, ghi")
1311 *
1312 * @since 1.0.0
1313 *
1314 * @param string $value [description].
1315 * @return string[] [description]
1316 */
1317 public function parse_string_list( string $value ): array {
1318 $value = trim( $value );
1319
1320 // Case: empty.
1321 if ( ! $value ) {
1322 return array();
1323 }
1324
1325 $a_strings = array();
1326
1327 $in_str = false;
1328 $current_string = '';
1329
1330 for ( $i = 0, $_len = strlen( $value ); $i < $_len; $i++ ) {
1331 if ( ( ',' === $value[ $i ] || ' ' === $value[ $i ] ) && true === $in_str ) {
1332 $in_str = false;
1333 $a_strings[] = $current_string;
1334 $current_string = '';
1335 } elseif ( '"' === $value[ $i ] || "'" === $value[ $i ] ) {
1336 if ( $in_str === $value[ $i ] ) {
1337 $a_strings[] = $current_string;
1338 $in_str = false;
1339 $current_string = '';
1340 continue;
1341 } elseif ( ! $in_str ) {
1342 $in_str = $value[ $i ];
1343 }
1344 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
1345 if ( $in_str ) {
1346 $current_string .= $value[ $i ];
1347 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
1348 if ( ! preg_match( '/[\s,]/', $value[ $i ] ) ) {
1349 $in_str = true;
1350 $current_string = $value[ $i ];
1351 }
1352 }
1353 }
1354 }
1355
1356 if ( $current_string ) {
1357 $a_strings[] = $current_string;
1358 }
1359
1360 return $a_strings;
1361 }
1362
1363 } // class TablePress_CSSTidy
1364