1<?php
2/**
3 * CSSTidy - CSS Parser and Optimiser
4 *
5 * CSS Optimising Class
6 * This class optimises CSS data generated by csstidy.
7 *
8 * This file is part of CSSTidy.
9 *
10 * CSSTidy is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * CSSTidy is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with CSSTidy; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
23 *
24 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
25 * @package csstidy
26 * @author Florian Schmitz (floele at gmail dot com) 2005-2006
27 */
28
29/**
30 * CSS Optimising Class
31 *
32 * This class optimises CSS data generated by csstidy.
33 *
34 * @package csstidy
35 * @author Florian Schmitz (floele at gmail dot com) 2005-2006
36 * @version 1.0
37 */
38
39class csstidy_optimise
40{
41    /**
42     * Constructor
43     * @param array $css contains the class csstidy
44     * @access private
45     * @version 1.0
46     */
47    function csstidy_optimise(&$css)
48    {
49        $this->parser    =& $css;
50        $this->css       =& $css->css;
51        $this->sub_value =& $css->sub_value;
52        $this->at        =& $css->at;
53        $this->selector  =& $css->selector;
54        $this->property  =& $css->property;
55        $this->value     =& $css->value;
56    }
57
58    /**
59     * Optimises $css after parsing
60     * @access public
61     * @version 1.0
62     */
63    function postparse()
64    {
65        if ($this->parser->get_cfg('preserve_css')) {
66            return;
67        }
68
69        if ($this->parser->get_cfg('merge_selectors') == 2)
70        {
71            foreach ($this->css as $medium => $value)
72            {
73                $this->merge_selectors($this->css[$medium]);
74            }
75        }
76
77        if ($this->parser->get_cfg('optimise_shorthands') > 0)
78        {
79            foreach ($this->css as $medium => $value)
80            {
81                foreach ($value as $selector => $value1)
82                {
83                    $this->css[$medium][$selector] = csstidy_optimise::merge_4value_shorthands($this->css[$medium][$selector]);
84
85                    if ($this->parser->get_cfg('optimise_shorthands') < 2) {
86                        continue;
87                    }
88
89                    $this->css[$medium][$selector] = csstidy_optimise::merge_bg($this->css[$medium][$selector]);
90                    if (empty($this->css[$medium][$selector])) {
91                        unset($this->css[$medium][$selector]);
92                    }
93                }
94            }
95        }
96    }
97
98    /**
99     * Optimises values
100     * @access public
101     * @version 1.0
102     */
103    function value()
104    {
105        $shorthands =& $GLOBALS['csstidy']['shorthands'];
106
107        // optimise shorthand properties
108        if(isset($shorthands[$this->property]))
109        {
110            $temp = csstidy_optimise::shorthand($this->value); // FIXME - move
111            if($temp != $this->value)
112            {
113                $this->parser->log('Optimised shorthand notation ('.$this->property.'): Changed "'.$this->value.'" to "'.$temp.'"','Information');
114            }
115            $this->value = $temp;
116        }
117
118        // Remove whitespace at ! important
119        if($this->value != $this->compress_important($this->value))
120        {
121            $this->parser->log('Optimised !important','Information');
122        }
123    }
124
125    /**
126     * Optimises shorthands
127     * @access public
128     * @version 1.0
129     */
130    function shorthands()
131    {
132        $shorthands =& $GLOBALS['csstidy']['shorthands'];
133
134        if(!$this->parser->get_cfg('optimise_shorthands') || $this->parser->get_cfg('preserve_css')) {
135            return;
136        }
137
138        if($this->property == 'background' && $this->parser->get_cfg('optimise_shorthands') > 1)
139        {
140            unset($this->css[$this->at][$this->selector]['background']);
141            $this->parser->merge_css_blocks($this->at,$this->selector,csstidy_optimise::dissolve_short_bg($this->value));
142        }
143        if(isset($shorthands[$this->property]))
144        {
145            $this->parser->merge_css_blocks($this->at,$this->selector,csstidy_optimise::dissolve_4value_shorthands($this->property,$this->value));
146            if(is_array($shorthands[$this->property]))
147            {
148                unset($this->css[$this->at][$this->selector][$this->property]);
149            }
150        }
151    }
152
153    /**
154     * Optimises a sub-value
155     * @access public
156     * @version 1.0
157     */
158    function subvalue()
159    {
160        $replace_colors =& $GLOBALS['csstidy']['replace_colors'];
161
162        $this->sub_value = trim($this->sub_value);
163        if($this->sub_value == '') // caution : '0'
164        {
165            return;
166        }
167
168        // Compress font-weight
169        if($this->property == 'font-weight' && $this->parser->get_cfg('compress_font-weight'))
170        {
171            $important = '';
172            if(csstidy::is_important($this->sub_value))
173            {
174                $important = ' !important';
175                $this->sub_value = csstidy::gvw_important($this->sub_value);
176            }
177            if($this->sub_value == 'bold')
178            {
179                $this->sub_value = '700'.$important;
180                $this->parser->log('Optimised font-weight: Changed "bold" to "700"','Information');
181            }
182            else if($this->sub_value == 'normal')
183            {
184                $this->sub_value = '400'.$important;
185                $this->parser->log('Optimised font-weight: Changed "normal" to "400"','Information');
186            }
187        }
188
189        $temp = $this->compress_numbers($this->sub_value);
190        if($temp != $this->sub_value)
191        {
192            if(strlen($temp) > strlen($this->sub_value)) {
193                $this->parser->log('Fixed invalid number: Changed "'.$this->sub_value.'" to "'.$temp.'"','Warning');
194            } else {
195                $this->parser->log('Optimised number: Changed "'.$this->sub_value.'" to "'.$temp.'"','Information');
196            }
197            $this->sub_value = $temp;
198        }
199        if($this->parser->get_cfg('compress_colors'))
200        {
201            $temp = $this->cut_color($this->sub_value);
202            if($temp !== $this->sub_value)
203            {
204                if(isset($replace_colors[$this->sub_value])) {
205                    $this->parser->log('Fixed invalid color name: Changed "'.$this->sub_value.'" to "'.$temp.'"','Warning');
206                } else {
207                    $this->parser->log('Optimised color: Changed "'.$this->sub_value.'" to "'.$temp.'"','Information');
208                }
209                $this->sub_value = $temp;
210            }
211        }
212    }
213
214    /**
215     * Compresses shorthand values. Example: margin:1px 1px 1px 1px -> margin:1px
216     * @param string $value
217     * @access public
218     * @return string
219     * @version 1.0
220     */
221    function shorthand($value)
222    {
223        $important = '';
224        if(csstidy::is_important($value))
225        {
226            $values = csstidy::gvw_important($value);
227            $important = ' !important';
228        }
229        else $values = $value;
230
231        $values = explode(' ',$values);
232        switch(count($values))
233        {
234            case 4:
235            if($values[0] == $values[1] && $values[0] == $values[2] && $values[0] == $values[3])
236            {
237                return $values[0].$important;
238            }
239            elseif($values[1] == $values[3] && $values[0] == $values[2])
240            {
241                return $values[0].' '.$values[1].$important;
242            }
243            elseif($values[1] == $values[3])
244            {
245                return $values[0].' '.$values[1].' '.$values[2].$important;
246            }
247            break;
248
249            case 3:
250            if($values[0] == $values[1] && $values[0] == $values[2])
251            {
252                return $values[0].$important;
253            }
254            elseif($values[0] == $values[2])
255            {
256                return $values[0].' '.$values[1].$important;
257            }
258            break;
259
260            case 2:
261            if($values[0] == $values[1])
262            {
263                return $values[0].$important;
264            }
265            break;
266        }
267
268        return $value;
269    }
270
271    /**
272     * Removes unnecessary whitespace in ! important
273     * @param string $string
274     * @return string
275     * @access public
276     * @version 1.1
277     */
278    function compress_important(&$string)
279    {
280        if(csstidy::is_important($string))
281        {
282            $string = csstidy::gvw_important($string) . ' !important';
283        }
284        return $string;
285    }
286
287    /**
288     * Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
289     * @param string $color
290     * @return string
291     * @version 1.1
292     */
293    function cut_color($color)
294    {
295        $replace_colors =& $GLOBALS['csstidy']['replace_colors'];
296
297        // rgb(0,0,0) -> #000000 (or #000 in this case later)
298        if(strtolower(substr($color,0,4)) == 'rgb(')
299        {
300            $color_tmp = substr($color,4,strlen($color)-5);
301            $color_tmp = explode(',',$color_tmp);
302            for ( $i = 0; $i < count($color_tmp); $i++ )
303            {
304                $color_tmp[$i] = trim ($color_tmp[$i]);
305                if(substr($color_tmp[$i],-1) == '%')
306                {
307                    $color_tmp[$i] = round((255*$color_tmp[$i])/100);
308                }
309                if($color_tmp[$i]>255) $color_tmp[$i] = 255;
310            }
311            $color = '#';
312            for ($i = 0; $i < 3; $i++ )
313            {
314                if($color_tmp[$i]<16) {
315                    $color .= '0' . dechex($color_tmp[$i]);
316                } else {
317                    $color .= dechex($color_tmp[$i]);
318                }
319            }
320        }
321
322        // Fix bad color names
323        if(isset($replace_colors[strtolower($color)]))
324        {
325            $color = $replace_colors[strtolower($color)];
326        }
327
328        // #aabbcc -> #abc
329        if(strlen($color) == 7)
330        {
331            $color_temp = strtolower($color);
332            if($color_temp{0} == '#' && $color_temp{1} == $color_temp{2} && $color_temp{3} == $color_temp{4} && $color_temp{5} == $color_temp{6})
333            {
334                $color = '#'.$color{1}.$color{3}.$color{5};
335            }
336        }
337
338        switch(strtolower($color))
339        {
340            /* color name -> hex code */
341            case 'black': return '#000';
342            case 'fuchsia': return '#F0F';
343            case 'white': return '#FFF';
344            case 'yellow': return '#FF0';
345
346            /* hex code -> color name */
347            case '#800000': return 'maroon';
348            case '#ffa500': return 'orange';
349            case '#808000': return 'olive';
350            case '#800080': return 'purple';
351            case '#008000': return 'green';
352            case '#000080': return 'navy';
353            case '#008080': return 'teal';
354            case '#c0c0c0': return 'silver';
355            case '#808080': return 'gray';
356            case '#f00': return 'red';
357        }
358
359        return $color;
360    }
361
362    /**
363     * Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
364     * @param string $subvalue
365     * @return string
366     * @version 1.2
367     */
368    function compress_numbers($subvalue)
369    {
370        $units =& $GLOBALS['csstidy']['units'];
371        $number_values =& $GLOBALS['csstidy']['number_values'];
372        $color_values =& $GLOBALS['csstidy']['color_values'];
373
374        // for font:1em/1em sans-serif...;
375        if($this->property == 'font')
376        {
377            $temp = explode('/',$subvalue);
378        }
379        else
380        {
381            $temp = array($subvalue);
382        }
383        for ($l = 0; $l < count($temp); $l++)
384        {
385            // continue if no numeric value
386            if (!(strlen($temp[$l]) > 0 && ( is_numeric($temp[$l]{0}) || $temp[$l]{0} == '+' || $temp[$l]{0} == '-' ) ))
387            {
388                continue;
389            }
390
391            // Fix bad colors
392            if (in_array($this->property, $color_values))
393            {
394                $temp[$l] = '#'.$temp[$l];
395            }
396
397            if (floatval($temp[$l]) == 0)
398            {
399                $temp[$l] = '0';
400            }
401            else
402            {
403                $unit_found = FALSE;
404                for ($m = 0, $size_4 = count($units); $m < $size_4; $m++)
405                {
406                    if (strpos(strtolower($temp[$l]),$units[$m]) !== FALSE)
407                    {
408                        $temp[$l] = floatval($temp[$l]).$units[$m];
409                        $unit_found = TRUE;
410                        break;
411                    }
412                }
413                if (!$unit_found && !in_array($this->property,$number_values,TRUE))
414                {
415                    $temp[$l] = floatval($temp[$l]).'px';
416                }
417                else if (!$unit_found)
418                {
419                    $temp[$l] = floatval($temp[$l]);
420                }
421            }
422        }
423
424        return ((count($temp) > 1) ? $temp[0].'/'.$temp[1] : $temp[0]);
425    }
426
427    /**
428     * Merges selectors with same properties. Example: a{color:red} b{color:red} -> a,b{color:red}
429     * Very basic and has at least one bug. Hopefully there is a replacement soon.
430     * @param array $array
431     * @return array
432     * @access public
433     * @version 1.2
434     */
435    function merge_selectors(&$array)
436    {
437        $css = $array;
438        foreach($css as $key => $value)
439        {
440            if(!isset($css[$key]))
441            {
442                continue;
443            }
444            $newsel = '';
445
446            // Check if properties also exist in another selector
447            $keys = array();
448            // PHP bug (?) without $css = $array; here
449            foreach($css as $selector => $vali)
450            {
451                if($selector == $key)
452                {
453                    continue;
454                }
455
456                if($css[$key] === $vali)
457                {
458                    $keys[] = $selector;
459                }
460            }
461
462            if(!empty($keys))
463            {
464                $newsel = $key;
465                unset($css[$key]);
466                foreach($keys as $selector)
467                {
468                    unset($css[$selector]);
469                    $newsel .= ','.$selector;
470                }
471                $css[$newsel] = $value;
472            }
473        }
474        $array = $css;
475    }
476
477    /**
478     * Dissolves properties like padding:10px 10px 10px to padding-top:10px;padding-bottom:10px;...
479     * @param string $property
480     * @param string $value
481     * @return array
482     * @version 1.0
483     * @see merge_4value_shorthands()
484     */
485    function dissolve_4value_shorthands($property,$value)
486    {
487        $shorthands =& $GLOBALS['csstidy']['shorthands'];
488        if(!is_array($shorthands[$property]))
489        {
490            $return[$property] = $value;
491            return $return;
492        }
493
494        $important = '';
495        if(csstidy::is_important($value))
496        {
497            $value = csstidy::gvw_important($value);
498            $important = ' !important';
499        }
500        $values = explode(' ',$value);
501
502
503        $return = array();
504        if(count($values) == 4)
505        {
506            for($i=0;$i<4;$i++)
507            {
508                $return[$shorthands[$property][$i]] = $values[$i].$important;
509            }
510        }
511        elseif(count($values) == 3)
512        {
513            $return[$shorthands[$property][0]] = $values[0].$important;
514            $return[$shorthands[$property][1]] = $values[1].$important;
515            $return[$shorthands[$property][3]] = $values[1].$important;
516            $return[$shorthands[$property][2]] = $values[2].$important;
517        }
518        elseif(count($values) == 2)
519        {
520            for($i=0;$i<4;$i++)
521            {
522                $return[$shorthands[$property][$i]] = (($i % 2 != 0)) ? $values[1].$important : $values[0].$important;
523            }
524        }
525        else
526        {
527            for($i=0;$i<4;$i++)
528            {
529                $return[$shorthands[$property][$i]] = $values[0].$important;
530            }
531        }
532
533        return $return;
534    }
535
536    /**
537     * Explodes a string as explode() does, however, not if $sep is escaped or within a string.
538     * @param string $sep seperator
539     * @param string $string
540     * @return array
541     * @version 1.0
542     */
543    function explode_ws($sep,$string)
544    {
545        $status = 'st';
546        $to = '';
547
548        $output = array();
549        $num = 0;
550        for($i = 0, $len = strlen($string);$i < $len; $i++)
551        {
552            switch($status)
553            {
554                case 'st':
555                if($string{$i} == $sep && !csstidy::escaped($string,$i))
556                {
557                    ++$num;
558                }
559                elseif($string{$i} == '"' || $string{$i} == '\'' || $string{$i} == '(' && !csstidy::escaped($string,$i))
560                {
561                    $status = 'str';
562                    $to = ($string{$i} == '(') ? ')' : $string{$i};
563                    (isset($output[$num])) ? $output[$num] .= $string{$i} : $output[$num] = $string{$i};
564                }
565                else
566                {
567                    (isset($output[$num])) ? $output[$num] .= $string{$i} : $output[$num] = $string{$i};
568                }
569                break;
570
571                case 'str':
572                if($string{$i} == $to && !csstidy::escaped($string,$i))
573                {
574                    $status = 'st';
575                }
576                (isset($output[$num])) ? $output[$num] .= $string{$i} : $output[$num] = $string{$i};
577                break;
578            }
579        }
580
581        if(isset($output[0]))
582        {
583            return $output;
584        }
585        else
586        {
587            return array($output);
588        }
589    }
590
591    /**
592     * Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
593     * @param array $array
594     * @return array
595     * @version 1.2
596     * @see dissolve_4value_shorthands()
597     */
598    function merge_4value_shorthands($array)
599    {
600        $return = $array;
601        $shorthands =& $GLOBALS['csstidy']['shorthands'];
602
603        foreach($shorthands as $key => $value)
604        {
605            if(isset($array[$value[0]]) && isset($array[$value[1]])
606            && isset($array[$value[2]]) && isset($array[$value[3]]) && $value !== 0)
607            {
608                $return[$key] = '';
609
610                $important = '';
611                for($i = 0; $i < 4; $i++)
612                {
613                    $val = $array[$value[$i]];
614                    if(csstidy::is_important($val))
615                    {
616                        $important = '!important';
617                        $return[$key] .= csstidy::gvw_important($val).' ';
618                    }
619                    else
620                    {
621                        $return[$key] .= $val.' ';
622                    }
623                    unset($return[$value[$i]]);
624                }
625                $return[$key] = csstidy_optimise::shorthand(trim($return[$key].$important));
626            }
627        }
628        return $return;
629    }
630
631    /**
632     * Dissolve background property
633     * @param string $str_value
634     * @return array
635     * @version 1.0
636     * @see merge_bg()
637     * @todo full CSS 3 compliance
638     */
639    function dissolve_short_bg($str_value)
640    {
641        $background_prop_default =& $GLOBALS['csstidy']['background_prop_default'];
642        $repeat = array('repeat','repeat-x','repeat-y','no-repeat','space');
643        $attachment = array('scroll','fixed','local');
644        $clip = array('border','padding');
645        $origin = array('border','padding','content');
646        $pos = array('top','center','bottom','left','right');
647        $important = '';
648        $return = array('background-image' => NULL,'background-size' => NULL,'background-repeat' => NULL,'background-position' => NULL,'background-attachment'=>NULL,'background-clip' => NULL,'background-origin' => NULL,'background-color' => NULL);
649
650        if(csstidy::is_important($str_value))
651        {
652            $important = ' !important';
653            $str_value = csstidy::gvw_important($str_value);
654        }
655
656        $str_value = csstidy_optimise::explode_ws(',',$str_value);
657        for($i = 0; $i < count($str_value); $i++)
658        {
659            $have['clip'] = FALSE; $have['pos'] = FALSE;
660            $have['color'] = FALSE; $have['bg'] = FALSE;
661
662            $str_value[$i] = csstidy_optimise::explode_ws(' ',trim($str_value[$i]));
663
664            for($j = 0; $j < count($str_value[$i]); $j++)
665            {
666                if($have['bg'] === FALSE && (substr($str_value[$i][$j],0,4) == 'url(' || $str_value[$i][$j] === 'none'))
667                {
668                    $return['background-image'] .= $str_value[$i][$j].',';
669                    $have['bg'] = TRUE;
670                }
671                elseif(in_array($str_value[$i][$j],$repeat,TRUE))
672                {
673                    $return['background-repeat'] .= $str_value[$i][$j].',';
674                }
675                elseif(in_array($str_value[$i][$j],$attachment,TRUE))
676                {
677                    $return['background-attachment'] .= $str_value[$i][$j].',';
678                }
679                elseif(in_array($str_value[$i][$j],$clip,TRUE) && !$have['clip'])
680                {
681                    $return['background-clip'] .= $str_value[$i][$j].',';
682                    $have['clip'] = TRUE;
683                }
684                elseif(in_array($str_value[$i][$j],$origin,TRUE))
685                {
686                    $return['background-origin'] .= $str_value[$i][$j].',';
687                }
688                elseif($str_value[$i][$j]{0} == '(')
689                {
690                    $return['background-size'] .= substr($str_value[$i][$j],1,-1).',';
691                }
692                elseif(in_array($str_value[$i][$j],$pos,TRUE) || is_numeric($str_value[$i][$j]{0}) || $str_value[$i][$j]{0} === NULL)
693                {
694                    $return['background-position'] .= $str_value[$i][$j];
695                    if(!$have['pos']) $return['background-position'] .= ' '; else $return['background-position'].= ',';
696                    $have['pos'] = TRUE;
697                }
698                elseif(!$have['color'])
699                {
700                    $return['background-color'] .= $str_value[$i][$j].',';
701                    $have['color'] = TRUE;
702                }
703            }
704        }
705
706        foreach($background_prop_default as $bg_prop => $default_value)
707        {
708            if($return[$bg_prop] !== NULL)
709            {
710                $return[$bg_prop] = substr($return[$bg_prop],0,-1).$important;
711            }
712            else $return[$bg_prop] = $default_value.$important;
713        }
714        return $return;
715    }
716
717    /**
718     * Merges all background properties
719     * @param array $input_css
720     * @return array
721     * @version 1.0
722     * @see dissolve_short_bg()
723     * @todo full CSS 3 compliance
724     */
725    function merge_bg($input_css)
726    {
727        $background_prop_default =& $GLOBALS['csstidy']['background_prop_default'];
728        // Max number of background images. CSS3 not yet fully implemented
729        $number_of_values = @max(count(csstidy_optimise::explode_ws(',',$input_css['background-image'])),count(csstidy_optimise::explode_ws(',',$input_css['background-color'])),1);
730        // Array with background images to check if BG image exists
731        $bg_img_array = @csstidy_optimise::explode_ws(',',csstidy::gvw_important($input_css['background-image']));
732        $new_bg_value = '';
733        $important = '';
734
735        for($i = 0; $i < $number_of_values; $i++)
736        {
737            foreach($background_prop_default as $bg_property => $default_value)
738            {
739                // Skip if property does not exist
740                if(!isset($input_css[$bg_property]))
741                {
742                    continue;
743                }
744
745                $cur_value = $input_css[$bg_property];
746
747                // Skip some properties if there is no background image
748                if((!isset($bg_img_array[$i]) || $bg_img_array[$i] === 'none')
749                    && ($bg_property === 'background-size' || $bg_property === 'background-position'
750                    || $bg_property === 'background-attachment' || $bg_property === 'background-repeat'))
751                {
752                    continue;
753                }
754
755                // Remove !important
756                if(csstidy::is_important($cur_value))
757                {
758                    $important = ' !important';
759                    $cur_value = csstidy::gvw_important($cur_value);
760                }
761
762                // Do not add default values
763                if($cur_value === $default_value)
764                {
765                    continue;
766                }
767
768                $temp = csstidy_optimise::explode_ws(',',$cur_value);
769
770                if(isset($temp[$i]))
771                {
772                    if($bg_property == 'background-size')
773                    {
774                        $new_bg_value .= '('.$temp[$i].') ';
775                    }
776                    else
777                    {
778                        $new_bg_value .= $temp[$i].' ';
779                    }
780                }
781            }
782
783            $new_bg_value = trim($new_bg_value);
784            if($i != $number_of_values-1) $new_bg_value .= ',';
785        }
786
787        // Delete all background-properties
788        foreach($background_prop_default as $bg_property => $default_value)
789        {
790            unset($input_css[$bg_property]);
791        }
792
793        // Add new background property
794        if($new_bg_value !== '') $input_css['background'] = $new_bg_value.$important;
795
796        return $input_css;
797    }
798}
799?>