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

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

Maj de CSSTidy en v1.6.3

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