source: spip-zone/_core_/branches/spip-3.2/plugins/compresseur/lib/csstidy/class.csstidy.php @ 111780

Last change on this file since 111780 was 111780, checked in by cedric@…, 11 months ago

Report de r111778 : Fix https://core.spip.net/issues/3987 : ne pas supprimer les commentaires de credit/licence commençant par un \! lors de la minification CSS

File size: 38.1 KB
Line 
1<?php
2
3/**
4 * CSSTidy - CSS Parser and Optimiser
5 *
6 * CSS Parser class
7 *
8 * Copyright 2005, 2006, 2007 Florian Schmitz
9 *
10 * This file is part of CSSTidy.
11 *
12 *   CSSTidy is free software; you can redistribute it and/or modify
13 *   it under the terms of the GNU Lesser General Public License as published by
14 *   the Free Software Foundation; either version 2.1 of the License, or
15 *   (at your option) any later version.
16 *
17 *   CSSTidy is distributed in the hope that it will be useful,
18 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
19 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 *   GNU Lesser General Public License for more details.
21 *
22 *   You should have received a copy of the GNU Lesser General Public License
23 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 *
25 * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
26 * @package csstidy
27 * @author Florian Schmitz (floele at gmail dot com) 2005-2007
28 * @author Brett Zamir (brettz9 at yahoo dot com) 2007
29 * @author Nikolay Matsievsky (speed at webo dot name) 2009-2010
30 * @author Cedric Morin (cedric at yterium dot com) 2010-2012
31 * @author Christopher Finke (cfinke at gmail.com) 2012
32 * @author Mark Scherer (remove $GLOBALS once and for all + PHP5.4 comp) 2012
33 */
34
35/**
36 * Defines ctype functions if required.
37 *
38 * @TODO: Make these methods of CSSTidy.
39 * @since 1.0.0
40 */
41if (!function_exists('ctype_space')){
42        /* ctype_space Check for whitespace character(s) */
43        function ctype_space($text){
44                return (1===preg_match("/^[ \r\n\t\f]+$/", $text));
45        }
46}
47if (!function_exists('ctype_alpha')){
48        /* ctype_alpha Check for alphabetic character(s) */
49        function ctype_alpha($text){
50                return (1===preg_match('/^[a-zA-Z]+$/', $text));
51        }
52}
53if (!function_exists('ctype_xdigit')){
54        /* ctype_xdigit Check for HEX character(s) */
55        function ctype_xdigit($text){
56                return (1===preg_match('/^[a-fA-F0-9]+$/', $text));
57        }
58}
59
60/**
61 * Defines constants
62 * @todo //TODO: make them class constants of csstidy
63 */
64define('AT_START',         1);
65define('AT_END',           2);
66define('SEL_START',        3);
67define('SEL_END',          4);
68define('PROPERTY',         5);
69define('VALUE',            6);
70define('COMMENT',          7);
71define('IMPORTANT_COMMENT',8);
72define('DEFAULT_AT',      41);
73
74/**
75 * Contains a class for printing CSS code
76 *
77 * @version 1.1.0
78 */
79require('class.csstidy_print.php');
80
81/**
82 * Contains a class for optimising CSS code
83 *
84 * @version 1.0
85 */
86require('class.csstidy_optimise.php');
87
88/**
89 * CSS Parser class
90 *
91 * This class represents a CSS parser which reads CSS code and saves it in an array.
92 * In opposite to most other CSS parsers, it does not use regular expressions and
93 * thus has full CSS2 support and a higher reliability.
94 * Additional to that it applies some optimisations and fixes to the CSS code.
95 * An online version should be available here: http://cdburnerxp.se/cssparse/css_optimiser.php
96 * @package csstidy
97 * @author Florian Schmitz (floele at gmail dot com) 2005-2006
98 * @version 1.6.4
99 */
100class csstidy {
101
102        /**
103         * Saves the parsed CSS. This array is empty if preserve_css is on.
104         * @var array
105         * @access public
106         */
107        public $css = array();
108        /**
109         * Saves the parsed CSS (raw)
110         * @var array
111         * @access private
112         */
113        public $tokens = array();
114        /**
115         * Printer class
116         * @see csstidy_print
117         * @var object
118         * @access public
119         */
120        public $print;
121        /**
122         * Optimiser class
123         * @see csstidy_optimise
124         * @var object
125         * @access private
126         */
127        public $optimise;
128        /**
129         * Saves the CSS charset (@charset)
130         * @var string
131         * @access private
132         */
133        public $charset = '';
134        /**
135         * Saves all @import URLs
136         * @var array
137         * @access private
138         */
139        public $import = array();
140        /**
141         * Saves the namespace
142         * @var string
143         * @access private
144         */
145        public $namespace = '';
146        /**
147         * Contains the version of csstidy
148         * @var string
149         * @access private
150         */
151        public $version = '1.6.4';
152        /**
153         * Stores the settings
154         * @var array
155         * @access private
156         */
157        public $settings = array();
158        /**
159         * Saves the parser-status.
160         *
161         * Possible values:
162         * - is = in selector
163         * - ip = in property
164         * - iv = in value
165         * - instr = in string (started at " or ' or ( )
166         * - ic = in comment (ignore everything)
167         * - at = in @-block
168         *
169         * @var string
170         * @access private
171         */
172        public $status = 'is';
173        /**
174         * Saves the current at rule (@media)
175         * @var string
176         * @access private
177         */
178        public $at = '';
179        /**
180         * Saves the at rule for next selector (during @font-face or other @)
181         * @var string
182         * @access private
183         */
184        public $next_selector_at = '';
185
186        /**
187         * Saves the current selector
188         * @var string
189         * @access private
190         */
191        public $selector = '';
192        /**
193         * Saves the current property
194         * @var string
195         * @access private
196         */
197        public $property = '';
198        /**
199         * Saves the position of , in selectors
200         * @var array
201         * @access private
202         */
203        public $sel_separate = array();
204        /**
205         * Saves the current value
206         * @var string
207         * @access private
208         */
209        public $value = '';
210        /**
211         * Saves the current sub-value
212         *
213         * Example for a subvalue:
214         * background:url(foo.png) red no-repeat;
215         * "url(foo.png)", "red", and  "no-repeat" are subvalues,
216         * seperated by whitespace
217         * @var string
218         * @access private
219         */
220        public $sub_value = '';
221        /**
222         * Array which saves all subvalues for a property.
223         * @var array
224         * @see sub_value
225         * @access private
226         */
227        public $sub_value_arr = array();
228        /**
229         * Saves the stack of characters that opened the current strings
230         * @var array
231         * @access private
232         */
233        public $str_char = array();
234        public $cur_string = array();
235        /**
236         * Status from which the parser switched to ic or instr
237         * @var array
238         * @access private
239         */
240        public $from = array();
241        /**
242        /**
243         * =true if in invalid at-rule
244         * @var bool
245         * @access private
246         */
247        public $invalid_at = false;
248        /**
249         * =true if something has been added to the current selector
250         * @var bool
251         * @access private
252         */
253        public $added = false;
254        /**
255         * Array which saves the message log
256         * @var array
257         * @access private
258         */
259        public $log = array();
260        /**
261         * Saves the line number
262         * @var integer
263         * @access private
264         */
265        public $line = 1;
266        /**
267         * Marks if we need to leave quotes for a string
268         * @var array
269         * @access private
270         */
271        public $quoted_string = array();
272
273        /**
274         * List of tokens
275         * @var string
276         */
277        public $tokens_list = "";
278
279        /**
280         * Various CSS Data for CSSTidy
281         * @var array
282         */
283        public $data = array();
284
285        /**
286         * Loads standard template and sets default settings
287         * @access private
288         * @version 1.3
289         */
290        public function __construct() {
291                $data = array();
292                include('data.inc.php');
293                $this->data = $data;
294
295                $this->settings['remove_bslash'] = true;
296                $this->settings['compress_colors'] = true;
297                $this->settings['compress_font-weight'] = true;
298                $this->settings['lowercase_s'] = false;
299                /*
300                        1 common shorthands optimization
301                        2 + font property optimization
302                        3 + background property optimization
303                 */
304                $this->settings['optimise_shorthands'] = 1;
305                $this->settings['remove_last_;'] = true;
306                $this->settings['space_before_important'] = false;
307                /* rewrite all properties with low case, better for later gzip OK, safe*/
308                $this->settings['case_properties'] = 1;
309                /* sort properties in alpabetic order, better for later gzip
310                 * but can cause trouble in case of overiding same propertie or using hack
311                 */
312                $this->settings['sort_properties'] = false;
313                /*
314                        1, 3, 5, etc -- enable sorting selectors inside @media: a{}b{}c{}
315                        2, 5, 8, etc -- enable sorting selectors inside one CSS declaration: a,b,c{}
316                        preserve order by default cause it can break functionnality
317                 */
318                $this->settings['sort_selectors'] = 0;
319                /* is dangeroues to be used: CSS is broken sometimes */
320                $this->settings['merge_selectors'] = 0;
321                /* preserve or not browser hacks */
322
323                /* Useful to produce a rtl css from a ltr one (or the opposite) */
324                $this->settings['reverse_left_and_right'] = 0;
325
326                $this->settings['discard_invalid_selectors'] = false;
327                $this->settings['discard_invalid_properties'] = false;
328                $this->settings['css_level'] = 'CSS3.0';
329                $this->settings['preserve_css'] = false;
330                $this->settings['timestamp'] = false;
331                $this->settings['template'] = ''; // say that propertie exist
332                $this->set_cfg('template','default'); // call load_template
333                $this->optimise = new csstidy_optimise($this);
334
335                $this->tokens_list = & $this->data['csstidy']['tokens'];
336        }
337
338        /**
339         * Get the value of a setting.
340         * @param string $setting
341         * @access public
342         * @return mixed
343         * @version 1.0
344         */
345        public function get_cfg($setting) {
346                if (isset($this->settings[$setting])) {
347                        return $this->settings[$setting];
348                }
349                return false;
350        }
351
352        /**
353         * Load a template
354         * @param string $template used by set_cfg to load a template via a configuration setting
355         * @access private
356         * @version 1.4
357         */
358        public function _load_template($template) {
359                switch ($template) {
360                        case 'default':
361                                $this->load_template('default');
362                                break;
363
364                        case 'highest':
365                                $this->load_template('highest_compression');
366                                break;
367
368                        case 'high':
369                                $this->load_template('high_compression');
370                                break;
371
372                        case 'low':
373                                $this->load_template('low_compression');
374                                break;
375
376                        default:
377                                $this->load_template($template);
378                                break;
379                }
380        }
381
382        /**
383         * Set the value of a setting.
384         * @param string $setting
385         * @param mixed $value
386         * @access public
387         * @return bool
388         * @version 1.0
389         */
390        public function set_cfg($setting, $value=null) {
391                if (is_array($setting) && $value === null) {
392                        foreach ($setting as $setprop => $setval) {
393                                $this->settings[$setprop] = $setval;
394                        }
395                        if (array_key_exists('template', $setting)) {
396                                $this->_load_template($this->settings['template']);
397                        }
398                        return true;
399                } elseif (isset($this->settings[$setting]) && $value !== '') {
400                        $this->settings[$setting] = $value;
401                        if ($setting === 'template') {
402                                $this->_load_template($this->settings['template']);
403                        }
404                        return true;
405                }
406                return false;
407        }
408
409        /**
410         * Adds a token to $this->tokens
411         * @param mixed $type
412         * @param string $data
413         * @param bool $do add a token even if preserve_css is off
414         * @access private
415         * @version 1.0
416         */
417        public function _add_token($type, $data, $do = false) {
418                if ($this->get_cfg('preserve_css') || $do) {
419                        $this->tokens[] = array($type, ($type == COMMENT or $type == IMPORTANT_COMMENT) ? $data : trim($data));
420                }
421        }
422
423        /**
424         * Add a message to the message log
425         * @param string $message
426         * @param string $type
427         * @param integer $line
428         * @access private
429         * @version 1.0
430         */
431        public function log($message, $type, $line = -1) {
432                if ($line === -1) {
433                        $line = $this->line;
434                }
435                $line = intval($line);
436                $add = array('m' => $message, 't' => $type);
437                if (!isset($this->log[$line]) || !in_array($add, $this->log[$line])) {
438                        $this->log[$line][] = $add;
439                }
440        }
441
442        /**
443         * Parse unicode notations and find a replacement character
444         * @param string $string
445         * @param integer $i
446         * @access private
447         * @return string
448         * @version 1.2
449         */
450        public function _unicode(&$string, &$i) {
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 (@ctype_xdigit($string{$i + 1}) && ctype_space($string{$i})
473                                                && !$replaced || !ctype_space($string{$i})) {
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 formatted output to a file
489         * @param string $filename
490         * @param string $doctype when printing formatted, is a shorthand for the document type
491         * @param bool $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet
492         * @param string $title when printing formatted, is the title to be added in the head of the document
493         * @param string $lang when printing formatted, gives a two-letter language code to be added to the output
494         * @access public
495         * @version 1.4
496         */
497        public function write_page($filename, $doctype='xhtml1.1', $externalcss=true, $title='', $lang='en') {
498                $this->write($filename, true);
499        }
500
501        /**
502         * Write plain output to a file
503         * @param string $filename
504         * @param bool $formatted whether to print formatted or not
505         * @param string $doctype when printing formatted, is a shorthand for the document type
506         * @param bool $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet
507         * @param string $title when printing formatted, is the title to be added in the head of the document
508         * @param string $lang when printing formatted, gives a two-letter language code to be added to the output
509         * @param bool $pre_code whether to add pre and code tags around the code (for light HTML formatted templates)
510         * @access public
511         * @version 1.4
512         */
513        public function write($filename, $formatted=false, $doctype='xhtml1.1', $externalcss=true, $title='', $lang='en', $pre_code=true) {
514                $filename .= ( $formatted) ? '.xhtml' : '.css';
515
516                if (!is_dir('temp')) {
517                        $madedir = mkdir('temp');
518                        if (!$madedir) {
519                                print 'Could not make directory "temp" in ' . dirname(__FILE__);
520                                exit;
521                        }
522                }
523                $handle = fopen('temp/' . $filename, 'w');
524                if ($handle) {
525                        if (!$formatted) {
526                                fwrite($handle, $this->print->plain());
527                        } else {
528                                fwrite($handle, $this->print->formatted_page($doctype, $externalcss, $title, $lang, $pre_code));
529                        }
530                }
531                fclose($handle);
532        }
533
534        /**
535         * Loads a new template
536         * @param string $content either filename (if $from_file == true), content of a template file, "high_compression", "highest_compression", "low_compression", or "default"
537         * @param bool $from_file uses $content as filename if true
538         * @access public
539         * @version 1.1
540         * @see http://csstidy.sourceforge.net/templates.php
541         */
542        public function load_template($content, $from_file=true) {
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                return $this->parse(@file_get_contents($url));
569        }
570
571        /**
572         * Checks if there is a token at the current position
573         * @param string $string
574         * @param integer $i
575         * @access public
576         * @version 1.11
577         */
578        public function is_token(&$string, $i) {
579                return (strpos($this->tokens_list, $string{$i}) !== false && !$this->escaped($string, $i));
580        }
581
582        /**
583         * Parses CSS in $string. The code is saved as array in $this->css
584         * @param string $string the CSS code
585         * @access public
586         * @return bool
587         * @version 1.1
588         */
589        public function parse($string) {
590                // Temporarily set locale to en_US in order to handle floats properly
591                $old = @setlocale(LC_ALL, 0);
592                @setlocale(LC_ALL, 'C');
593
594                // PHP bug? Settings need to be refreshed in PHP4
595                $this->print = new csstidy_print($this);
596                $this->optimise = new csstidy_optimise($this);
597
598                $all_properties = & $this->data['csstidy']['all_properties'];
599                $at_rules = & $this->data['csstidy']['at_rules'];
600                $quoted_string_properties = & $this->data['csstidy']['quoted_string_properties'];
601
602                $this->css = array();
603                $this->print->input_css = $string;
604                $string = str_replace("\r\n", "\n", $string) . ' ';
605                $cur_comment = '';
606
607                for ($i = 0, $size = strlen($string); $i < $size; $i++) {
608                        if ($string{$i} === "\n" || $string{$i} === "\r") {
609                                ++$this->line;
610                        }
611
612                        switch ($this->status) {
613                                /* Case in at-block */
614                                case 'at':
615                                        if ($this->is_token($string, $i)) {
616                                                if ($string{$i} === '/' && @$string{$i + 1} === '*') {
617                                                        $this->status = 'ic';
618                                                        ++$i;
619                                                        $this->from[] = 'at';
620                                                } elseif ($string{$i} === '{') {
621                                                        $this->status = 'is';
622                                                        $this->at = $this->css_new_media_section($this->at);
623                                                        $this->_add_token(AT_START, $this->at);
624                                                } elseif ($string{$i} === ',') {
625                                                        $this->at = trim($this->at) . ',';
626                                                } elseif ($string{$i} === '\\') {
627                                                        $this->at .= $this->_unicode($string, $i);
628                                                }
629                                                // fix for complicated media, i.e @media screen and (-webkit-min-device-pixel-ratio:1.5)
630                                                elseif (in_array($string{$i}, array('(', ')', ':', '.', '/'))) {
631                                                        $this->at .= $string{$i};
632                                                }
633                                        } else {
634                                                $lastpos = strlen($this->at) - 1;
635                                                if (!( (ctype_space($this->at{$lastpos}) || $this->is_token($this->at, $lastpos) && $this->at{$lastpos} === ',') && ctype_space($string{$i}))) {
636                                                        $this->at .= $string{$i};
637                                                }
638                                        }
639                                        break;
640
641                                /* Case in-selector */
642                                case 'is':
643                                        if ($this->is_token($string, $i)) {
644                                                if ($string{$i} === '/' && @$string{$i + 1} === '*' && trim($this->selector) == '') {
645                                                        $this->status = 'ic';
646                                                        ++$i;
647                                                        $this->from[] = 'is';
648                                                } elseif ($string{$i} === '@' && trim($this->selector) == '') {
649                                                        // Check for at-rule
650                                                        $this->invalid_at = true;
651                                                        foreach ($at_rules as $name => $type) {
652                                                                if (!strcasecmp(substr($string, $i + 1, strlen($name)), $name)) {
653                                                                        ($type === 'at') ? $this->at = '@' . $name : $this->selector = '@' . $name;
654                                                                        if ($type === 'atis') {
655                                                                                $this->next_selector_at = ($this->next_selector_at?$this->next_selector_at:($this->at?$this->at:DEFAULT_AT));
656                                                                                $this->at = $this->css_new_media_section(' ');
657                                                                                $type = 'is';
658                                                                        }
659                                                                        $this->status = $type;
660                                                                        $i += strlen($name);
661                                                                        $this->invalid_at = false;
662                                                                }
663                                                        }
664
665                                                        if ($this->invalid_at) {
666                                                                $this->selector = '@';
667                                                                $invalid_at_name = '';
668                                                                for ($j = $i + 1; $j < $size; ++$j) {
669                                                                        if (!ctype_alpha($string{$j})) {
670                                                                                break;
671                                                                        }
672                                                                        $invalid_at_name .= $string{$j};
673                                                                }
674                                                                $this->log('Invalid @-rule: ' . $invalid_at_name . ' (removed)', 'Warning');
675                                                        }
676                                                } elseif (($string{$i} === '"' || $string{$i} === "'")) {
677                                                        $this->cur_string[] = $string{$i};
678                                                        $this->status = 'instr';
679                                                        $this->str_char[] = $string{$i};
680                                                        $this->from[] = 'is';
681                                                        /* fixing CSS3 attribute selectors, i.e. a[href$=".mp3" */
682                                                        $this->quoted_string[] = ($string{$i - 1} === '=' );
683                                                } elseif ($this->invalid_at && $string{$i} === ';') {
684                                                        $this->invalid_at = false;
685                                                        $this->status = 'is';
686                                                        if ($this->next_selector_at) {
687                                                                $this->at = $this->css_new_media_section($this->next_selector_at);
688                                                                $this->next_selector_at = '';
689                                                        }
690                                                } elseif ($string{$i} === '{') {
691                                                        $this->status = 'ip';
692                                                        if ($this->at == '') {
693                                                                $this->at = $this->css_new_media_section(DEFAULT_AT);
694                                                        }
695                                                        $this->selector = $this->css_new_selector($this->at,$this->selector);
696                                                        $this->_add_token(SEL_START, $this->selector);
697                                                        $this->added = false;
698                                                } elseif ($string{$i} === '}') {
699                                                        $this->_add_token(AT_END, $this->at);
700                                                        $this->at = '';
701                                                        $this->selector = '';
702                                                        $this->sel_separate = array();
703                                                } elseif ($string{$i} === ',') {
704                                                        $this->selector = trim($this->selector) . ',';
705                                                        $this->sel_separate[] = strlen($this->selector);
706                                                } elseif ($string{$i} === '\\') {
707                                                        $this->selector .= $this->_unicode($string, $i);
708                                                } elseif ($string{$i} === '*' && @in_array($string{$i + 1}, array('.', '#', '[', ':')) && ($i==0 OR $string{$i - 1}!=='/')) {
709                                                        // remove unnecessary universal selector, FS#147, but not comment in selector
710                                                } else {
711                                                        $this->selector .= $string{$i};
712                                                }
713                                        } else {
714                                                $lastpos = strlen($this->selector) - 1;
715                                                if ($lastpos == -1 || !( (ctype_space($this->selector{$lastpos}) || $this->is_token($this->selector, $lastpos) && $this->selector{$lastpos} === ',') && ctype_space($string{$i}))) {
716                                                        $this->selector .= $string{$i};
717                                                }
718                                        }
719                                        break;
720
721                                /* Case in-property */
722                                case 'ip':
723                                        if ($this->is_token($string, $i)) {
724                                                if (($string{$i} === ':' || $string{$i} === '=') && $this->property != '') {
725                                                        $this->status = 'iv';
726                                                        if (!$this->get_cfg('discard_invalid_properties') || $this->property_is_valid($this->property)) {
727                                                                $this->property = $this->css_new_property($this->at,$this->selector,$this->property);
728                                                                $this->_add_token(PROPERTY, $this->property);
729                                                        }
730                                                } elseif ($string{$i} === '/' && @$string{$i + 1} === '*' && $this->property == '') {
731                                                        $this->status = 'ic';
732                                                        ++$i;
733                                                        $this->from[] = 'ip';
734                                                } elseif ($string{$i} === '}') {
735                                                        $this->explode_selectors();
736                                                        $this->status = 'is';
737                                                        $this->invalid_at = false;
738                                                        $this->_add_token(SEL_END, $this->selector);
739                                                        $this->selector = '';
740                                                        $this->property = '';
741                                                        if ($this->next_selector_at) {
742                                                                $this->at = $this->css_new_media_section($this->next_selector_at);
743                                                                $this->next_selector_at = '';
744                                                        }
745                                                } elseif ($string{$i} === ';') {
746                                                        $this->property = '';
747                                                } elseif ($string{$i} === '\\') {
748                                                        $this->property .= $this->_unicode($string, $i);
749                                                }
750                                                // else this is dumb IE a hack, keep it
751                                                // including //
752                                                elseif (($this->property === '' && !ctype_space($string{$i}))
753                                                        || ($this->property === '/' || $string{$i} === '/')) {
754                                                        $this->property .= $string{$i};
755                                                }
756                                        } elseif (!ctype_space($string{$i})) {
757                                                $this->property .= $string{$i};
758                                        }
759                                        break;
760
761                                /* Case in-value */
762                                case 'iv':
763                                        $pn = (($string{$i} === "\n" || $string{$i} === "\r") && $this->property_is_next($string, $i + 1) || $i == strlen($string) - 1);
764                                        if ($this->is_token($string, $i) || $pn) {
765                                                if ($string{$i} === '/' && @$string{$i + 1} === '*') {
766                                                        $this->status = 'ic';
767                                                        ++$i;
768                                                        $this->from[] = 'iv';
769                                                } elseif (($string{$i} === '"' || $string{$i} === "'" || $string{$i} === '(')) {
770                                                        $this->cur_string[] = $string{$i};
771                                                        $this->str_char[] = ($string{$i} === '(') ? ')' : $string{$i};
772                                                        $this->status = 'instr';
773                                                        $this->from[] = 'iv';
774                                                        $this->quoted_string[] = in_array(strtolower($this->property), $quoted_string_properties);
775                                                } elseif ($string{$i} === ',') {
776                                                        $this->sub_value = trim($this->sub_value) . ',';
777                                                } elseif ($string{$i} === '\\') {
778                                                        $this->sub_value .= $this->_unicode($string, $i);
779                                                } elseif ($string{$i} === ';' || $pn) {
780                                                        if ($this->selector{0} === '@' && isset($at_rules[substr($this->selector, 1)]) && $at_rules[substr($this->selector, 1)] === 'iv') {
781                                                                /* Add quotes to charset, import, namespace */
782                                                                $this->sub_value_arr[] = trim($this->sub_value);
783
784                                                                $this->status = 'is';
785
786                                                                switch ($this->selector) {
787                                                                        case '@charset': $this->charset = '"'.$this->sub_value_arr[0].'"';
788                                                                                break;
789                                                                        case '@namespace': $this->namespace = implode(' ', $this->sub_value_arr);
790                                                                                break;
791                                                                        case '@import': $this->import[] = implode(' ', $this->sub_value_arr);
792                                                                                break;
793                                                                }
794
795                                                                $this->sub_value_arr = array();
796                                                                $this->sub_value = '';
797                                                                $this->selector = '';
798                                                                $this->sel_separate = array();
799                                                        } else {
800                                                                $this->status = 'ip';
801                                                        }
802                                                } elseif ($string{$i} !== '}') {
803                                                        $this->sub_value .= $string{$i};
804                                                }
805                                                if (($string{$i} === '}' || $string{$i} === ';' || $pn) && !empty($this->selector)) {
806                                                        if ($this->at == '') {
807                                                                $this->at = $this->css_new_media_section(DEFAULT_AT);
808                                                        }
809
810                                                        // case settings
811                                                        if ($this->get_cfg('lowercase_s')) {
812                                                                $this->selector = strtolower($this->selector);
813                                                        }
814                                                        $this->property = strtolower($this->property);
815
816                                                        $this->optimise->subvalue();
817                                                        if ($this->sub_value != '') {
818                                                                $this->sub_value_arr[] = $this->sub_value;
819                                                                $this->sub_value = '';
820                                                        }
821
822                                                        $this->value = '';
823                                                        while (count($this->sub_value_arr)) {
824                                                                $sub = array_shift($this->sub_value_arr);
825                                                                if (strstr($this->selector, 'font-face')) {
826                                                                        $sub = $this->quote_font_format($sub);
827                                                                }
828
829                                                                if ($sub != '')
830                                                                        $this->value .= ((!strlen($this->value) || substr($this->value,-1,1) === ',')?'':' ').$sub;
831                                                        }
832
833                                                        $this->optimise->value();
834
835                                                        $valid = $this->property_is_valid($this->property);
836                                                        if ((!$this->invalid_at || $this->get_cfg('preserve_css')) && (!$this->get_cfg('discard_invalid_properties') || $valid)) {
837                                                                $this->css_add_property($this->at, $this->selector, $this->property, $this->value);
838                                                                $this->_add_token(VALUE, $this->value);
839                                                                $this->optimise->shorthands();
840                                                        }
841                                                        if (!$valid) {
842                                                                if ($this->get_cfg('discard_invalid_properties')) {
843                                                                        $this->log('Removed invalid property: ' . $this->property, 'Warning');
844                                                                } else {
845                                                                        $this->log('Invalid property in ' . strtoupper($this->get_cfg('css_level')) . ': ' . $this->property, 'Warning');
846                                                                }
847                                                        }
848
849                                                        $this->property = '';
850                                                        $this->sub_value_arr = array();
851                                                        $this->value = '';
852                                                }
853                                                if ($string{$i} === '}') {
854                                                        $this->explode_selectors();
855                                                        $this->_add_token(SEL_END, $this->selector);
856                                                        $this->status = 'is';
857                                                        $this->invalid_at = false;
858                                                        $this->selector = '';
859                                                        if ($this->next_selector_at) {
860                                                                $this->at = $this->css_new_media_section($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;
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(IMPORTANT_COMMENT, $cur_comment);
957                                                        $this->css_add_important_comment($cur_comment);
958                                                }
959                                                else {
960                                                        $this->_add_token(COMMENT, $cur_comment);
961                                                }
962                                                $cur_comment = '';
963                                        } else {
964                                                $cur_comment .= $string{$i};
965                                        }
966                                        break;
967                        }
968                }
969
970                $this->optimise->postparse();
971
972                $this->print->_reset();
973
974                @setlocale(LC_ALL, $old); // Set locale back to original setting
975
976                return!(empty($this->css) && empty($this->import) && empty($this->charset) && empty($this->tokens) && empty($this->namespace));
977        }
978
979
980        /**
981         * format() in font-face needs quoted values for somes browser (FF at least)
982         *
983         * @param $value
984         * @return string
985         */
986        public function quote_font_format($value) {
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                // Explode multiple selectors
1013                if ($this->get_cfg('merge_selectors') === 1) {
1014                        $new_sels = array();
1015                        $lastpos = 0;
1016                        $this->sel_separate[] = strlen($this->selector);
1017                        foreach ($this->sel_separate as $num => $pos) {
1018                                if ($num == count($this->sel_separate) - 1) {
1019                                        $pos += 1;
1020                                }
1021
1022                                $new_sels[] = substr($this->selector, $lastpos, $pos - $lastpos - 1);
1023                                $lastpos = $pos;
1024                        }
1025
1026                        if (count($new_sels) > 1) {
1027                                foreach ($new_sels as $selector) {
1028                                        if (isset($this->css[$this->at][$this->selector])) {
1029                                                $this->merge_css_blocks($this->at, $selector, $this->css[$this->at][$this->selector]);
1030                                        }
1031                                }
1032                                unset($this->css[$this->at][$this->selector]);
1033                        }
1034                }
1035                $this->sel_separate = array();
1036        }
1037
1038        /**
1039         * Checks if a character is escaped (and returns true if it is)
1040         * @param string $string
1041         * @param integer $pos
1042         * @access public
1043         * @return bool
1044         * @version 1.02
1045         */
1046        static function escaped(&$string, $pos) {
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                if ($this->get_cfg('preserve_css') || trim($comment) == '') {
1058                        return;
1059                }
1060                if (!isset($this->css['!'])) {
1061                        $this->css['!'] = '';
1062                }
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                if ($this->get_cfg('preserve_css') || trim($new_val) == '') {
1080                        return;
1081                }
1082
1083                $this->added = true;
1084                if (isset($this->css[$media][$selector][$property])) {
1085                        if (($this->is_important($this->css[$media][$selector][$property]) && $this->is_important($new_val)) || !$this->is_important($this->css[$media][$selector][$property])) {
1086                                $this->css[$media][$selector][$property] = trim($new_val);
1087                        }
1088                } else {
1089                        $this->css[$media][$selector][$property] = trim($new_val);
1090                }
1091        }
1092
1093        /**
1094         * Start a new media section.
1095         * Check if the media is not already known,
1096         * else rename it with extra spaces
1097         * to avoid merging
1098         *
1099         * @param string $media
1100         * @return string
1101         */
1102        public function css_new_media_section($media) {
1103                if ($this->get_cfg('preserve_css')) {
1104                        return $media;
1105                }
1106
1107                // if the last @media is the same as this
1108                // keep it
1109                if (!$this->css || !is_array($this->css) || empty($this->css)) {
1110                        return $media;
1111                }
1112                end($this->css);
1113                $at = key($this->css);
1114                if ($at == $media) {
1115                        return $media;
1116                }
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 selector.
1127         * If already referenced in this media section,
1128         * rename it with extra space to avoid merging
1129         * except if merging is required,
1130         * or last selector is the same (merge siblings)
1131         *
1132         * never merge @font-face
1133         *
1134         * @param string $media
1135         * @param string $selector
1136         * @return string
1137         */
1138        public function css_new_selector($media,$selector) {
1139                if ($this->get_cfg('preserve_css')) {
1140                        return $selector;
1141                }
1142                $selector = trim($selector);
1143                if (strncmp($selector,'@font-face',10)!=0) {
1144                        if ($this->settings['merge_selectors'] != false)
1145                                return $selector;
1146
1147                        if (!$this->css || !isset($this->css[$media]) || !$this->css[$media])
1148                                return $selector;
1149
1150                        // if last is the same, keep it
1151                        end($this->css[$media]);
1152                        $sel = key($this->css[$media]);
1153                        if ($sel == $selector) {
1154                                return $selector;
1155                        }
1156                }
1157
1158                while (isset($this->css[$media][$selector]))
1159                        $selector .= ' ';
1160                return $selector;
1161        }
1162
1163        /**
1164         * Start a new propertie.
1165         * If already references in this selector,
1166         * rename it with extra space to avoid override
1167         *
1168         * @param string $media
1169         * @param string $selector
1170         * @param string $property
1171         * @return string
1172         */
1173        public function css_new_property($media, $selector, $property) {
1174                if ($this->get_cfg('preserve_css')) {
1175                        return $property;
1176                }
1177                if (!$this->css || !isset($this->css[$media][$selector]) || !$this->css[$media][$selector])
1178                        return $property;
1179
1180                while (isset($this->css[$media][$selector][$property]))
1181                        $property .= ' ';
1182
1183                return $property;
1184        }
1185
1186        /**
1187         * Adds CSS to an existing media/selector
1188         * @param string $media
1189         * @param string $selector
1190         * @param array $css_add
1191         * @access private
1192         * @version 1.1
1193         */
1194        public function merge_css_blocks($media, $selector, $css_add) {
1195                foreach ($css_add as $property => $value) {
1196                        $this->css_add_property($media, $selector, $property, $value, false);
1197                }
1198        }
1199
1200        /**
1201         * Checks if $value is !important.
1202         * @param string $value
1203         * @return bool
1204         * @access public
1205         * @version 1.0
1206         */
1207        public function is_important(&$value) {
1208                return (
1209                        strpos($value, '!') !== false // quick test
1210                        AND !strcasecmp(substr(str_replace($this->data['csstidy']['whitespace'], '', $value), -10, 10), '!important'));
1211        }
1212
1213        /**
1214         * Returns a value without !important
1215         * @param string $value
1216         * @return string
1217         * @access public
1218         * @version 1.0
1219         */
1220        public function gvw_important($value) {
1221                if ($this->is_important($value)) {
1222                        $value = trim($value);
1223                        $value = substr($value, 0, -9);
1224                        $value = trim($value);
1225                        $value = substr($value, 0, -1);
1226                        $value = trim($value);
1227                        return $value;
1228                }
1229                return $value;
1230        }
1231
1232        /**
1233         * Checks if the next word in a string from pos is a CSS property
1234         * @param string $istring
1235         * @param integer $pos
1236         * @return bool
1237         * @access private
1238         * @version 1.2
1239         */
1240        public function property_is_next($istring, $pos) {
1241                $all_properties = & $this->data['csstidy']['all_properties'];
1242                $istring = substr($istring, $pos, strlen($istring) - $pos);
1243                $pos = strpos($istring, ':');
1244                if ($pos === false) {
1245                        return false;
1246                }
1247                $istring = strtolower(trim(substr($istring, 0, $pos)));
1248                if (isset($all_properties[$istring])) {
1249                        $this->log('Added semicolon to the end of declaration', 'Warning');
1250                        return true;
1251                }
1252                return false;
1253        }
1254
1255        /**
1256         * Checks if a property is valid
1257         * @param string $property
1258         * @return bool;
1259         * @access public
1260         * @version 1.0
1261         */
1262        public function property_is_valid($property) {
1263                if (in_array(trim($property), $this->data['csstidy']['multiple_properties'])) $property = trim($property);
1264                $all_properties = & $this->data['csstidy']['all_properties'];
1265                return (isset($all_properties[$property]) && strpos($all_properties[$property], strtoupper($this->get_cfg('css_level'))) !== false );
1266        }
1267
1268        /**
1269         * Accepts a list of strings (e.g., the argument to format() in a @font-face src property)
1270         * and returns a list of the strings.  Converts things like:
1271         *
1272         * format(abc) => format("abc")
1273         * format(abc def) => format("abc","def")
1274         * format(abc "def") => format("abc","def")
1275         * format(abc, def, ghi) => format("abc","def","ghi")
1276         * format("abc",'def') => format("abc","def")
1277         * format("abc, def, ghi") => format("abc, def, ghi")
1278         *
1279         * @param string
1280         * @return array
1281         */
1282
1283        public function parse_string_list($value) {
1284                $value = trim($value);
1285
1286                // Case: empty
1287                if (!$value) return array();
1288
1289                $strings = array();
1290
1291                $in_str = false;
1292                $current_string = '';
1293
1294                for ($i = 0, $_len = strlen($value); $i < $_len; $i++) {
1295                        if (($value{$i} === ',' || $value{$i} === ' ') && $in_str === true) {
1296                                $in_str = false;
1297                                $strings[] = $current_string;
1298                                $current_string = '';
1299                        } elseif ($value{$i} === '"' || $value{$i} === "'") {
1300                                if ($in_str === $value{$i}) {
1301                                        $strings[] = $current_string;
1302                                        $in_str = false;
1303                                        $current_string = '';
1304                                        continue;
1305                                } elseif (!$in_str) {
1306                                        $in_str = $value{$i};
1307                                }
1308                        } else {
1309                                if ($in_str) {
1310                                        $current_string .= $value{$i};
1311                                } else {
1312                                        if (!preg_match("/[\s,]/", $value{$i})) {
1313                                                $in_str = true;
1314                                                $current_string = $value{$i};
1315                                        }
1316                                }
1317                        }
1318                }
1319
1320                if ($current_string) {
1321                        $strings[] = $current_string;
1322                }
1323
1324                return $strings;
1325        }
1326}
Note: See TracBrowser for help on using the repository browser.