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