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

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