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?>