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

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