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