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

Last change on this file since 81332 was 81332, checked in by cedric@…, 6 years ago

Upgrade de la librairie CSSTidy en v1.5.2

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