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