1<?php 2 3/** 4 * CSSTidy - CSS Parser and Optimiser 5 * 6 * CSS Optimising Class 7 * This class optimises CSS data generated by csstidy. 8 * 9 * Copyright 2005, 2006, 2007 Florian Schmitz 10 * 11 * This file is part of CSSTidy. 12 * 13 * CSSTidy is free software; you can redistribute it and/or modify 14 * it under the terms of the GNU Lesser General Public License as published by 15 * the Free Software Foundation; either version 2.1 of the License, or 16 * (at your option) any later version. 17 * 18 * CSSTidy is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU Lesser General Public License for more details. 22 * 23 * You should have received a copy of the GNU Lesser General Public License 24 * along with this program. If not, see <http://www.gnu.org/licenses/>. 25 * 26 * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License 27 * @package csstidy 28 * @author Florian Schmitz (floele at gmail dot com) 2005-2007 29 * @author Brett Zamir (brettz9 at yahoo dot com) 2007 30 * @author Nikolay Matsievsky (speed at webo dot name) 2009-2010 31 * @author Cedric Morin (cedric at yterium dot com) 2010-2012 32 */ 33 34/** 35 * CSS Optimising Class 36 * 37 * This class optimises CSS data generated by csstidy. 38 * 39 * @package csstidy 40 * @author Florian Schmitz (floele at gmail dot com) 2005-2006 41 * @version 1.0 42 */ 43class csstidy_optimise { 44 45 /** 46 * csstidy object 47 * @var object 48 */ 49 public $parser; 50 public $css; 51 public $sub_value; 52 public $at; 53 public $selector; 54 public $property; 55 public $value; 56 57 /** 58 * Constructor 59 * @param array $css contains the class csstidy 60 * @access private 61 * @version 1.0 62 */ 63 public function __construct($css) { 64 $this->parser = $css; 65 $this->css = & $css->css; 66 $this->sub_value = & $css->sub_value; 67 $this->at = & $css->at; 68 $this->selector = & $css->selector; 69 $this->property = & $css->property; 70 $this->value = & $css->value; 71 } 72 73 /** 74 * Optimises $css after parsing 75 * @access public 76 * @version 1.0 77 */ 78 public function postparse() { 79 80 if ($this->parser->get_cfg('reverse_left_and_right') > 0) { 81 82 foreach ($this->css as $medium => $selectors) { 83 if (is_array($selectors)) { 84 foreach ($selectors as $selector => $properties) { 85 $this->css[$medium][$selector] = $this->reverse_left_and_right($this->css[$medium][$selector]); 86 } 87 } 88 } 89 90 } 91 92 if ($this->parser->get_cfg('preserve_css')) { 93 return; 94 } 95 96 if ((int)$this->parser->get_cfg('merge_selectors') === 2) { 97 foreach ($this->css as $medium => $value) { 98 if (is_array($value)) { 99 $this->merge_selectors($this->css[$medium]); 100 } 101 } 102 } 103 104 if ($this->parser->get_cfg('discard_invalid_selectors')) { 105 foreach ($this->css as $medium => $value) { 106 if (is_array($value)) { 107 $this->discard_invalid_selectors($this->css[$medium]); 108 } 109 } 110 } 111 112 if ($this->parser->get_cfg('optimise_shorthands') > 0) { 113 foreach ($this->css as $medium => $value) { 114 if (is_array($value)) { 115 foreach ($value as $selector => $value1) { 116 $this->css[$medium][$selector] = $this->merge_4value_shorthands($this->css[$medium][$selector]); 117 $this->css[$medium][$selector] = $this->merge_4value_radius_shorthands($this->css[$medium][$selector]); 118 119 if ($this->parser->get_cfg('optimise_shorthands') < 2) { 120 continue; 121 } 122 123 $this->css[$medium][$selector] = $this->merge_font($this->css[$medium][$selector]); 124 125 if ($this->parser->get_cfg('optimise_shorthands') < 3) { 126 continue; 127 } 128 129 $this->css[$medium][$selector] = $this->merge_bg($this->css[$medium][$selector]); 130 if (empty($this->css[$medium][$selector])) { 131 unset($this->css[$medium][$selector]); 132 } 133 } 134 } 135 } 136 } 137 } 138 139 /** 140 * Optimises values 141 * @access public 142 * @version 1.0 143 */ 144 public function value() { 145 $shorthands = & $this->parser->data['csstidy']['shorthands']; 146 147 // optimise shorthand properties 148 if (isset($shorthands[$this->property])) { 149 $temp = $this->shorthand($this->value); // FIXME - move 150 if ($temp != $this->value) { 151 $this->parser->log('Optimised shorthand notation (' . $this->property . '): Changed "' . $this->value . '" to "' . $temp . '"', 'Information'); 152 } 153 $this->value = $temp; 154 } 155 156 // Remove whitespace at ! important 157 if ($this->value != $this->compress_important($this->value)) { 158 $this->parser->log('Optimised !important', 'Information'); 159 } 160 } 161 162 /** 163 * Optimises shorthands 164 * @access public 165 * @version 1.0 166 */ 167 public function shorthands() { 168 $shorthands = & $this->parser->data['csstidy']['shorthands']; 169 170 if (!$this->parser->get_cfg('optimise_shorthands') || $this->parser->get_cfg('preserve_css')) { 171 return; 172 } 173 174 if ($this->property === 'font' && $this->parser->get_cfg('optimise_shorthands') > 1) { 175 $this->css[$this->at][$this->selector]['font']=''; 176 $this->parser->merge_css_blocks($this->at, $this->selector, $this->dissolve_short_font($this->value)); 177 } 178 if ($this->property === 'background' && $this->parser->get_cfg('optimise_shorthands') > 2) { 179 $this->css[$this->at][$this->selector]['background']=''; 180 $this->parser->merge_css_blocks($this->at, $this->selector, $this->dissolve_short_bg($this->value)); 181 } 182 if (isset($shorthands[$this->property])) { 183 $this->parser->merge_css_blocks($this->at, $this->selector, $this->dissolve_4value_shorthands($this->property, $this->value)); 184 if (is_array($shorthands[$this->property])) { 185 $this->css[$this->at][$this->selector][$this->property] = ''; 186 } 187 } 188 } 189 190 /** 191 * Optimises a sub-value 192 * @access public 193 * @version 1.0 194 */ 195 public function subvalue() { 196 $replace_colors = & $this->parser->data['csstidy']['replace_colors']; 197 198 $this->sub_value = trim($this->sub_value); 199 if ($this->sub_value == '') { // caution : '0' 200 return; 201 } 202 203 $important = ''; 204 if ($this->parser->is_important($this->sub_value)) { 205 $important = '!important'; 206 } 207 $this->sub_value = $this->parser->gvw_important($this->sub_value); 208 209 // Compress font-weight 210 if ($this->property === 'font-weight' && $this->parser->get_cfg('compress_font-weight')) { 211 if ($this->sub_value === 'bold') { 212 $this->sub_value = '700'; 213 $this->parser->log('Optimised font-weight: Changed "bold" to "700"', 'Information'); 214 } elseif ($this->sub_value === 'normal') { 215 $this->sub_value = '400'; 216 $this->parser->log('Optimised font-weight: Changed "normal" to "400"', 'Information'); 217 } 218 } 219 220 $temp = $this->compress_numbers($this->sub_value); 221 if (strcasecmp($temp, $this->sub_value) !== 0) { 222 if (strlen($temp) > strlen($this->sub_value)) { 223 $this->parser->log('Fixed invalid number: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Warning'); 224 } else { 225 $this->parser->log('Optimised number: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Information'); 226 } 227 $this->sub_value = $temp; 228 } 229 if ($this->parser->get_cfg('compress_colors')) { 230 $temp = $this->cut_color($this->sub_value); 231 if ($temp !== $this->sub_value) { 232 if (isset($replace_colors[$this->sub_value])) { 233 $this->parser->log('Fixed invalid color name: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Warning'); 234 } else { 235 $this->parser->log('Optimised color: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Information'); 236 } 237 $this->sub_value = $temp; 238 } 239 } 240 $this->sub_value .= $important; 241 } 242 243 /** 244 * Compresses shorthand values. Example: margin:1px 1px 1px 1px -> margin:1px 245 * @param string $value 246 * @access public 247 * @return string 248 * @version 1.0 249 */ 250 public function shorthand($value) { 251 $important = ''; 252 if ($this->parser->is_important($value)) { 253 $values = $this->parser->gvw_important($value); 254 $important = '!important'; 255 } 256 else 257 $values = $value; 258 259 $values = explode(' ', $values); 260 switch (count($values)) { 261 case 4: 262 if ($values[0] == $values[1] && $values[0] == $values[2] && $values[0] == $values[3]) { 263 return $values[0] . $important; 264 } elseif ($values[1] == $values[3] && $values[0] == $values[2]) { 265 return $values[0] . ' ' . $values[1] . $important; 266 } elseif ($values[1] == $values[3]) { 267 return $values[0] . ' ' . $values[1] . ' ' . $values[2] . $important; 268 } 269 break; 270 271 case 3: 272 if ($values[0] == $values[1] && $values[0] == $values[2]) { 273 return $values[0] . $important; 274 } elseif ($values[0] == $values[2]) { 275 return $values[0] . ' ' . $values[1] . $important; 276 } 277 break; 278 279 case 2: 280 if ($values[0] == $values[1]) { 281 return $values[0] . $important; 282 } 283 break; 284 } 285 286 return $value; 287 } 288 289 /** 290 * Removes unnecessary whitespace in ! important 291 * @param string $string 292 * @return string 293 * @access public 294 * @version 1.1 295 */ 296 public function compress_important(&$string) { 297 if ($this->parser->is_important($string)) { 298 $important = $this->parser->get_cfg('space_before_important') ? ' !important' : '!important'; 299 $string = $this->parser->gvw_important($string) . $important; 300 } 301 return $string; 302 } 303 304 /** 305 * Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values. 306 * @param string $color 307 * @return string 308 * @version 1.1 309 */ 310 public function cut_color($color) { 311 $replace_colors = & $this->parser->data['csstidy']['replace_colors']; 312 313 // if it's a string, don't touch ! 314 if (strncmp($color, "'", 1) == 0 || strncmp($color, '"', 1) == 0) 315 return $color; 316 317 /* expressions complexes de type gradient */ 318 if (strpos($color, '(') !== false 319 && (strncasecmp($color, 'rgb(' ,4) !== 0 and strncasecmp($color, 'rgba(' ,5) !== 0)) { 320 // on ne touche pas aux couleurs dans les expression ms, c'est trop sensible 321 if (stripos($color, 'progid:') !== false) 322 return $color; 323 preg_match_all(",rgba?\([^)]+\),i", $color, $matches, PREG_SET_ORDER); 324 if (count($matches)) { 325 foreach ($matches as $m) { 326 $color = str_replace($m[0], $this->cut_color($m[0]), $color); 327 } 328 } 329 preg_match_all(",#[0-9a-f]{6}(?=[^0-9a-f]),i", $color, $matches, PREG_SET_ORDER); 330 if (count($matches)) { 331 foreach ($matches as $m) { 332 $color = str_replace($m[0],$this->cut_color($m[0]), $color); 333 } 334 } 335 return $color; 336 } 337 338 // rgb(0,0,0) -> #000000 (or #000 in this case later) 339 if ( 340 // be sure to not corrupt a rgb with calc() value 341 (strncasecmp($color, 'rgb(', 4)==0 and strpos($color, '(', 4) === false) 342 or (strncasecmp($color, 'rgba(', 5)==0 and strpos($color, '(', 5) === false) 343 ){ 344 $color_tmp = explode('(', $color, 2); 345 $color_tmp = rtrim(end($color_tmp), ')'); 346 if (strpos($color_tmp, '/') !== false) { 347 $color_tmp = explode('/', $color_tmp, 2); 348 $color_parts = explode(' ', trim(reset($color_tmp)), 3); 349 while (count($color_parts) < 3) { 350 $color_parts[] = 0; 351 } 352 $color_parts[] = end($color_tmp); 353 } 354 else { 355 $color_parts = explode(',', $color_tmp, 4); 356 } 357 for ($i = 0; $i < count($color_parts); $i++) { 358 $color_parts[$i] = trim($color_parts[$i]); 359 if (substr($color_parts[$i], -1) === '%') { 360 $color_parts[$i] = round((255 * intval($color_parts[$i])) / 100); 361 } elseif ($i>2) { 362 // 4th argument is alpga layer between 0 and 1 (if not %) 363 $color_parts[$i] = round((255 * floatval($color_parts[$i]))); 364 } 365 $color_parts[$i] = intval($color_parts[$i]); 366 if ($color_parts[$i] > 255){ 367 $color_parts[$i] = 255; 368 } 369 } 370 $color = '#'; 371 // 3 or 4 parts depending on alpha layer 372 $nb = min(max(count($color_parts), 3),4); 373 for ($i = 0; $i < $nb; $i++) { 374 if (!isset($color_parts[$i])) { 375 $color_parts[$i] = 0; 376 } 377 if ($color_parts[$i] < 16) { 378 $color .= '0' . dechex($color_parts[$i]); 379 } else { 380 $color .= dechex($color_parts[$i]); 381 } 382 } 383 } 384 385 // Fix bad color names 386 if (isset($replace_colors[strtolower($color)])) { 387 $color = $replace_colors[strtolower($color)]; 388 } 389 390 // #aabbcc -> #abc 391 if (strlen($color) == 7) { 392 $color_temp = strtolower($color); 393 if ($color_temp[0] === '#' && $color_temp[1] == $color_temp[2] && $color_temp[3] == $color_temp[4] && $color_temp[5] == $color_temp[6]) { 394 $color = '#' . $color[1] . $color[3] . $color[5]; 395 } 396 } 397 // #aabbccdd -> #abcd 398 elseif (strlen($color) == 9) { 399 $color_temp = strtolower($color); 400 if ($color_temp[0] === '#' && $color_temp[1] == $color_temp[2] && $color_temp[3] == $color_temp[4] && $color_temp[5] == $color_temp[6] && $color_temp[7] == $color_temp[8]) { 401 $color = '#' . $color[1] . $color[3] . $color[5] . $color[7]; 402 } 403 } 404 405 switch (strtolower($color)) { 406 /* color name -> hex code */ 407 case 'black': return '#000'; 408 case 'fuchsia': return '#f0f'; 409 case 'white': return '#fff'; 410 case 'yellow': return '#ff0'; 411 412 /* hex code -> color name */ 413 case '#800000': return 'maroon'; 414 case '#ffa500': return 'orange'; 415 case '#808000': return 'olive'; 416 case '#800080': return 'purple'; 417 case '#008000': return 'green'; 418 case '#000080': return 'navy'; 419 case '#008080': return 'teal'; 420 case '#c0c0c0': return 'silver'; 421 case '#808080': return 'gray'; 422 case '#f00': return 'red'; 423 } 424 425 return $color; 426 } 427 428 /** 429 * Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 ) 430 * @param string $subvalue 431 * @return string 432 * @version 1.2 433 */ 434 public function compress_numbers($subvalue) { 435 $unit_values = & $this->parser->data['csstidy']['unit_values']; 436 $color_values = & $this->parser->data['csstidy']['color_values']; 437 438 // for font:1em/1em sans-serif...; 439 if ($this->property === 'font') { 440 $temp = explode('/', $subvalue); 441 } else { 442 $temp = array($subvalue); 443 } 444 445 for ($l = 0; $l < count($temp); $l++) { 446 // if we are not dealing with a number at this point, do not optimise anything 447 $number = $this->AnalyseCssNumber($temp[$l]); 448 if ($number === false) { 449 return $subvalue; 450 } 451 452 // Fix bad colors 453 if (in_array($this->property, $color_values)) { 454 $temp[$l] = '#' . $temp[$l]; 455 continue; 456 } 457 458 if (abs($number[0]) > 0) { 459 if ($number[1] == '' && in_array($this->property, $unit_values, true)) { 460 $number[1] = 'px'; 461 } 462 } elseif ($number[1] != 's' && $number[1] != 'ms') { 463 $number[1] = ''; 464 } 465 466 $temp[$l] = $number[0] . $number[1]; 467 } 468 469 return ((count($temp) > 1) ? $temp[0] . '/' . $temp[1] : $temp[0]); 470 } 471 472 /** 473 * Checks if a given string is a CSS valid number. If it is, 474 * an array containing the value and unit is returned 475 * @param string $string 476 * @return array ('unit' if unit is found or '' if no unit exists, number value) or false if no number 477 */ 478 public function AnalyseCssNumber($string) { 479 // most simple checks first 480 if (strlen($string) == 0 || ctype_alpha($string[0])) { 481 return false; 482 } 483 484 $units = & $this->parser->data['csstidy']['units']; 485 $return = array(0, ''); 486 487 $return[0] = floatval($string); 488 if (abs($return[0]) > 0 && abs($return[0]) < 1) { 489 if ($return[0] < 0) { 490 $return[0] = '-' . ltrim(substr($return[0], 1), '0'); 491 } else { 492 $return[0] = ltrim($return[0], '0'); 493 } 494 } 495 496 // Look for unit and split from value if exists 497 foreach ($units as $unit) { 498 $expectUnitAt = strlen($string) - strlen($unit); 499 if (!($unitInString = stristr($string, $unit))) { // mb_strpos() fails with "false" 500 continue; 501 } 502 $actualPosition = strpos($string, $unitInString); 503 if ($expectUnitAt === $actualPosition) { 504 $return[1] = $unit; 505 $string = substr($string, 0, - strlen($unit)); 506 break; 507 } 508 } 509 if (!is_numeric($string)) { 510 return false; 511 } 512 return $return; 513 } 514 515 /** 516 * Merges selectors with same properties. Example: a{color:red} b{color:red} -> a,b{color:red} 517 * Very basic and has at least one bug. Hopefully there is a replacement soon. 518 * @param array $array 519 * @return array 520 * @access public 521 * @version 1.2 522 */ 523 public function merge_selectors(&$array) { 524 $css = $array; 525 foreach ($css as $key => $value) { 526 if (!isset($css[$key])) { 527 continue; 528 } 529 $newsel = ''; 530 531 // Check if properties also exist in another selector 532 $keys = array(); 533 // PHP bug (?) without $css = $array; here 534 foreach ($css as $selector => $vali) { 535 if ($selector == $key) { 536 continue; 537 } 538 539 if ($css[$key] === $vali) { 540 $keys[] = $selector; 541 } 542 } 543 544 if (!empty($keys)) { 545 $newsel = $key; 546 unset($css[$key]); 547 foreach ($keys as $selector) { 548 unset($css[$selector]); 549 $newsel .= ',' . $selector; 550 } 551 $css[$newsel] = $value; 552 } 553 } 554 $array = $css; 555 } 556 557 /** 558 * Removes invalid selectors and their corresponding rule-sets as 559 * defined by 4.1.7 in REC-CSS2. This is a very rudimentary check 560 * and should be replaced by a full-blown parsing algorithm or 561 * regular expression 562 * @version 1.4 563 */ 564 public function discard_invalid_selectors(&$array) { 565 $invalid = array('+' => true, '~' => true, ',' => true, '>' => true); 566 foreach ($array as $selector => $decls) { 567 $ok = true; 568 $selectors = array_map('trim', explode(',', $selector)); 569 foreach ($selectors as $s) { 570 $simple_selectors = preg_split('/\s*[+>~\s]\s*/', $s); 571 foreach ($simple_selectors as $ss) { 572 if ($ss === '') 573 $ok = false; 574 // could also check $ss for internal structure, 575 // but that probably would be too slow 576 } 577 } 578 if (!$ok) 579 unset($array[$selector]); 580 } 581 } 582 583 /** 584 * Dissolves properties like padding:10px 10px 10px to padding-top:10px;padding-bottom:10px;... 585 * @param string $property 586 * @param string $value 587 * @param array|null $shorthands 588 * @return array 589 * @version 1.0 590 * @see merge_4value_shorthands() 591 */ 592 public function dissolve_4value_shorthands($property, $value, $shorthands = null) { 593 if (is_null($shorthands)) { 594 $shorthands = & $this->parser->data['csstidy']['shorthands']; 595 } 596 if (!is_array($shorthands[$property])) { 597 $return[$property] = $value; 598 return $return; 599 } 600 601 $important = ''; 602 if ($this->parser->is_important($value)) { 603 $value = $this->parser->gvw_important($value); 604 $important = '!important'; 605 } 606 $values = explode(' ', $value); 607 608 609 $return = array(); 610 if (count($values) == 4) { 611 for ($i = 0; $i < 4; $i++) { 612 $return[$shorthands[$property][$i]] = $values[$i] . $important; 613 } 614 } elseif (count($values) == 3) { 615 $return[$shorthands[$property][0]] = $values[0] . $important; 616 $return[$shorthands[$property][1]] = $values[1] . $important; 617 $return[$shorthands[$property][3]] = $values[1] . $important; 618 $return[$shorthands[$property][2]] = $values[2] . $important; 619 } elseif (count($values) == 2) { 620 for ($i = 0; $i < 4; $i++) { 621 $return[$shorthands[$property][$i]] = (($i % 2 != 0)) ? $values[1] . $important : $values[0] . $important; 622 } 623 } else { 624 for ($i = 0; $i < 4; $i++) { 625 $return[$shorthands[$property][$i]] = $values[0] . $important; 626 } 627 } 628 629 return $return; 630 } 631 632 /** 633 * Dissolves radius properties like 634 * border-radius:10px 10px 10px / 1px 2px 635 * to border-top-left:10px 1px;border-top-right:10px 2x;... 636 * @param string $property 637 * @param string $value 638 * @return array 639 * @version 1.0 640 * @use dissolve_4value_shorthands() 641 * @see merge_4value_radius_shorthands() 642 */ 643 public function dissolve_4value_radius_shorthands($property, $value) { 644 $shorthands = & $this->parser->data['csstidy']['radius_shorthands']; 645 if (!is_array($shorthands[$property])) { 646 $return[$property] = $value; 647 return $return; 648 } 649 650 if (strpos($value, '/') !== false) { 651 $values = $this->explode_ws('/', $value); 652 if (count($values) == 2) { 653 $r[0] = $this->dissolve_4value_shorthands($property, trim($values[0]), $shorthands); 654 $r[1] = $this->dissolve_4value_shorthands($property, trim($values[1]), $shorthands); 655 $return = array(); 656 foreach ($r[0] as $p=>$v) { 657 $return[$p] = $v; 658 if ($r[1][$p] !== $v) { 659 $return[$p] .= ' ' . $r[1][$p]; 660 } 661 } 662 return $return; 663 } 664 } 665 666 $return = $this->dissolve_4value_shorthands($property, $value, $shorthands); 667 return $return; 668 } 669 670 /** 671 * Explodes a string as explode() does, however, not if $sep is escaped or within a string. 672 * @param string $sep seperator 673 * @param string $string 674 * @param bool $explode_in_parenthesis 675 * @return array 676 * @version 1.0 677 */ 678 public function explode_ws($sep, $string, $explode_in_parenthesis = false) { 679 $status = 'st'; 680 $to = ''; 681 682 $output = array( 683 0 => '', 684 ); 685 $num = 0; 686 for ($i = 0, $len = strlen($string); $i < $len; $i++) { 687 switch ($status) { 688 case 'st': 689 if ($string[$i] == $sep && !$this->parser->escaped($string, $i)) { 690 ++$num; 691 } elseif ($string[$i] === '"' || $string[$i] === '\'' || (!$explode_in_parenthesis && $string[$i] === '(') && !$this->parser->escaped($string, $i)) { 692 $status = 'str'; 693 $to = ($string[$i] === '(') ? ')' : $string[$i]; 694 (isset($output[$num])) ? $output[$num] .= $string[$i] : $output[$num] = $string[$i]; 695 } else { 696 (isset($output[$num])) ? $output[$num] .= $string[$i] : $output[$num] = $string[$i]; 697 } 698 break; 699 700 case 'str': 701 if ($string[$i] == $to && !$this->parser->escaped($string, $i)) { 702 $status = 'st'; 703 } 704 (isset($output[$num])) ? $output[$num] .= $string[$i] : $output[$num] = $string[$i]; 705 break; 706 } 707 } 708 709 return $output; 710 } 711 712 /** 713 * Merges Shorthand properties again, the opposite of dissolve_4value_shorthands() 714 * @param array $array 715 * @param array|null $shorthands 716 * @return array 717 * @version 1.2 718 * @see dissolve_4value_shorthands() 719 */ 720 public function merge_4value_shorthands($array, $shorthands = null) { 721 $return = $array; 722 if (is_null($shorthands)) { 723 $shorthands = & $this->parser->data['csstidy']['shorthands']; 724 } 725 726 foreach ($shorthands as $key => $value) { 727 if ($value !== 0 && isset($array[$value[0]]) && isset($array[$value[1]]) 728 && isset($array[$value[2]]) && isset($array[$value[3]])) { 729 $return[$key] = ''; 730 731 $important = ''; 732 for ($i = 0; $i < 4; $i++) { 733 $val = $array[$value[$i]]; 734 if ($this->parser->is_important($val)) { 735 $important = '!important'; 736 $return[$key] .= $this->parser->gvw_important($val) . ' '; 737 } else { 738 $return[$key] .= $val . ' '; 739 } 740 unset($return[$value[$i]]); 741 } 742 $return[$key] = $this->shorthand(trim($return[$key] . $important)); 743 } 744 } 745 return $return; 746 } 747 748 /** 749 * Merges Shorthand properties again, the opposite of dissolve_4value_shorthands() 750 * @param array $array 751 * @return array 752 * @version 1.2 753 * @use merge_4value_shorthands() 754 * @see dissolve_4value_radius_shorthands() 755 */ 756 public function merge_4value_radius_shorthands($array) { 757 $return = $array; 758 $shorthands = & $this->parser->data['csstidy']['radius_shorthands']; 759 760 foreach ($shorthands as $key => $value) { 761 if (isset($array[$value[0]]) && isset($array[$value[1]]) 762 && isset($array[$value[2]]) && isset($array[$value[3]]) && $value !== 0) { 763 $return[$key] = ''; 764 $a = array(); 765 for ($i = 0; $i < 4; $i++) { 766 $v = $this->explode_ws(' ', trim($array[$value[$i]])); 767 $a[0][$value[$i]] = reset($v); 768 $a[1][$value[$i]] = end($v); 769 } 770 $r = array(); 771 $r[0] = $this->merge_4value_shorthands($a[0], $shorthands); 772 $r[1] = $this->merge_4value_shorthands($a[1], $shorthands); 773 774 if (isset($r[0][$key]) and isset($r[1][$key])) { 775 $return[$key] = $r[0][$key]; 776 if ($r[1][$key] !== $r[0][$key]) { 777 $return[$key] .= ' / ' . $r[1][$key]; 778 } 779 for ($i = 0; $i < 4; $i++) { 780 unset($return[$value[$i]]); 781 } 782 } 783 } 784 } 785 return $return; 786 } 787 /** 788 * Dissolve background property 789 * @param string $str_value 790 * @return array 791 * @version 1.0 792 * @see merge_bg() 793 * @todo full CSS 3 compliance 794 */ 795 public function dissolve_short_bg($str_value) { 796 // don't try to explose background gradient ! 797 if (stripos($str_value, 'gradient(')!== false) 798 return array('background'=>$str_value); 799 800 $background_prop_default = & $this->parser->data['csstidy']['background_prop_default']; 801 $repeat = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'space'); 802 $attachment = array('scroll', 'fixed', 'local'); 803 $clip = array('border', 'padding'); 804 $origin = array('border', 'padding', 'content'); 805 $pos = array('top', 'center', 'bottom', 'left', 'right'); 806 $important = ''; 807 $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); 808 809 if ($this->parser->is_important($str_value)) { 810 $important = ' !important'; 811 $str_value = $this->parser->gvw_important($str_value); 812 } 813 814 $str_value = $this->explode_ws(',', $str_value); 815 for ($i = 0; $i < count($str_value); $i++) { 816 $have['clip'] = false; 817 $have['pos'] = false; 818 $have['color'] = false; 819 $have['bg'] = false; 820 821 if (is_array($str_value[$i])) { 822 $str_value[$i] = $str_value[$i][0]; 823 } 824 $str_value[$i] = $this->explode_ws(' ', trim($str_value[$i])); 825 826 for ($j = 0; $j < count($str_value[$i]); $j++) { 827 if ($have['bg'] === false && (substr($str_value[$i][$j], 0, 4) === 'url(' || $str_value[$i][$j] === 'none')) { 828 $return['background-image'] .= $str_value[$i][$j] . ','; 829 $have['bg'] = true; 830 } elseif (in_array($str_value[$i][$j], $repeat, true)) { 831 $return['background-repeat'] .= $str_value[$i][$j] . ','; 832 } elseif (in_array($str_value[$i][$j], $attachment, true)) { 833 $return['background-attachment'] .= $str_value[$i][$j] . ','; 834 } elseif (in_array($str_value[$i][$j], $clip, true) && !$have['clip']) { 835 $return['background-clip'] .= $str_value[$i][$j] . ','; 836 $have['clip'] = true; 837 } elseif (in_array($str_value[$i][$j], $origin, true)) { 838 $return['background-origin'] .= $str_value[$i][$j] . ','; 839 } elseif ($str_value[$i][$j][0] === '(') { 840 $return['background-size'] .= substr($str_value[$i][$j], 1, -1) . ','; 841 } elseif (in_array($str_value[$i][$j], $pos, true) || is_numeric($str_value[$i][$j][0]) || $str_value[$i][$j][0] === null || $str_value[$i][$j][0] === '-' || $str_value[$i][$j][0] === '.') { 842 $return['background-position'] .= $str_value[$i][$j]; 843 if (!$have['pos']) 844 $return['background-position'] .= ' '; else 845 $return['background-position'].= ','; 846 $have['pos'] = true; 847 } elseif (!$have['color']) { 848 $return['background-color'] .= $str_value[$i][$j] . ','; 849 $have['color'] = true; 850 } 851 } 852 } 853 854 foreach ($background_prop_default as $bg_prop => $default_value) { 855 if ($return[$bg_prop] !== null) { 856 $return[$bg_prop] = substr($return[$bg_prop], 0, -1) . $important; 857 } 858 else 859 $return[$bg_prop] = $default_value . $important; 860 } 861 return $return; 862 } 863 864 /** 865 * Merges all background properties 866 * @param array $input_css 867 * @return array 868 * @version 1.0 869 * @see dissolve_short_bg() 870 * @todo full CSS 3 compliance 871 */ 872 public function merge_bg($input_css) { 873 $background_prop_default = & $this->parser->data['csstidy']['background_prop_default']; 874 // Max number of background images. CSS3 not yet fully implemented 875 $number_of_values = @max(count($this->explode_ws(',', $input_css['background-image'])), count($this->explode_ws(',', $input_css['background-color'])), 1); 876 // Array with background images to check if BG image exists 877 $bg_img_array = @$this->explode_ws(',', $this->parser->gvw_important($input_css['background-image'])); 878 $new_bg_value = ''; 879 $important = ''; 880 881 // if background properties is here and not empty, don't try anything 882 if (isset($input_css['background']) && $input_css['background']) 883 return $input_css; 884 885 for ($i = 0; $i < $number_of_values; $i++) { 886 foreach ($background_prop_default as $bg_property => $default_value) { 887 // Skip if property does not exist 888 if (!isset($input_css[$bg_property])) { 889 continue; 890 } 891 892 $cur_value = $input_css[$bg_property]; 893 // skip all optimisation if gradient() somewhere 894 if (stripos($cur_value, 'gradient(') !== false) 895 return $input_css; 896 897 // Skip some properties if there is no background image 898 if ((!isset($bg_img_array[$i]) || $bg_img_array[$i] === 'none') 899 && ($bg_property === 'background-size' || $bg_property === 'background-position' 900 || $bg_property === 'background-attachment' || $bg_property === 'background-repeat')) { 901 continue; 902 } 903 904 // Remove !important 905 if ($this->parser->is_important($cur_value)) { 906 $important = ' !important'; 907 $cur_value = $this->parser->gvw_important($cur_value); 908 } 909 910 // Do not add default values 911 if ($cur_value === $default_value) { 912 continue; 913 } 914 915 $temp = $this->explode_ws(',', $cur_value); 916 917 if (isset($temp[$i])) { 918 if ($bg_property === 'background-size') { 919 $new_bg_value .= '(' . $temp[$i] . ') '; 920 } else { 921 $new_bg_value .= $temp[$i] . ' '; 922 } 923 } 924 } 925 926 $new_bg_value = trim($new_bg_value); 927 if ($i != $number_of_values - 1) 928 $new_bg_value .= ','; 929 } 930 931 // Delete all background-properties 932 foreach ($background_prop_default as $bg_property => $default_value) { 933 unset($input_css[$bg_property]); 934 } 935 936 // Add new background property 937 if ($new_bg_value !== '') 938 $input_css['background'] = $new_bg_value . $important; 939 elseif(isset ($input_css['background'])) 940 $input_css['background'] = 'none'; 941 942 return $input_css; 943 } 944 945 /** 946 * Dissolve font property 947 * @param string $str_value 948 * @return array 949 * @version 1.3 950 * @see merge_font() 951 */ 952 public function dissolve_short_font($str_value) { 953 $font_prop_default = & $this->parser->data['csstidy']['font_prop_default']; 954 $font_weight = array('normal', 'bold', 'bolder', 'lighter', 100, 200, 300, 400, 500, 600, 700, 800, 900); 955 $font_variant = array('normal', 'small-caps'); 956 $font_style = array('normal', 'italic', 'oblique'); 957 $important = ''; 958 $return = array('font-style' => null, 'font-variant' => null, 'font-weight' => null, 'font-size' => null, 'line-height' => null, 'font-family' => null); 959 960 if ($this->parser->is_important($str_value)) { 961 $important = '!important'; 962 $str_value = $this->parser->gvw_important($str_value); 963 } 964 965 $have['style'] = false; 966 $have['variant'] = false; 967 $have['weight'] = false; 968 $have['size'] = false; 969 // Detects if font-family consists of several words w/o quotes 970 $multiwords = false; 971 972 // Workaround with multiple font-family 973 $str_value = $this->explode_ws(',', trim($str_value)); 974 975 $str_value[0] = $this->explode_ws(' ', trim($str_value[0])); 976 977 for ($j = 0; $j < count($str_value[0]); $j++) { 978 if ($have['weight'] === false && in_array($str_value[0][$j], $font_weight)) { 979 $return['font-weight'] = $str_value[0][$j]; 980 $have['weight'] = true; 981 } elseif ($have['variant'] === false && in_array($str_value[0][$j], $font_variant)) { 982 $return['font-variant'] = $str_value[0][$j]; 983 $have['variant'] = true; 984 } elseif ($have['style'] === false && in_array($str_value[0][$j], $font_style)) { 985 $return['font-style'] = $str_value[0][$j]; 986 $have['style'] = true; 987 } elseif ($have['size'] === false && (is_numeric($str_value[0][$j][0]) || $str_value[0][$j][0] === null || $str_value[0][$j][0] === '.')) { 988 $size = $this->explode_ws('/', trim($str_value[0][$j])); 989 $return['font-size'] = $size[0]; 990 if (isset($size[1])) { 991 $return['line-height'] = $size[1]; 992 } else { 993 $return['line-height'] = ''; // don't add 'normal' ! 994 } 995 $have['size'] = true; 996 } else { 997 if (isset($return['font-family'])) { 998 $return['font-family'] .= ' ' . $str_value[0][$j]; 999 $multiwords = true; 1000 } else { 1001 $return['font-family'] = $str_value[0][$j]; 1002 } 1003 } 1004 } 1005 // add quotes if we have several qords in font-family 1006 if ($multiwords !== false) { 1007 $return['font-family'] = '"' . $return['font-family'] . '"'; 1008 } 1009 $i = 1; 1010 while (isset($str_value[$i])) { 1011 $return['font-family'] .= ',' . trim($str_value[$i]); 1012 $i++; 1013 } 1014 1015 // Fix for 100 and more font-size 1016 if ($have['size'] === false && isset($return['font-weight']) && 1017 is_numeric($return['font-weight'][0])) { 1018 $return['font-size'] = $return['font-weight']; 1019 unset($return['font-weight']); 1020 } 1021 1022 foreach ($font_prop_default as $font_prop => $default_value) { 1023 if ($return[$font_prop] !== null) { 1024 $return[$font_prop] = $return[$font_prop] . $important; 1025 } 1026 else 1027 $return[$font_prop] = $default_value . $important; 1028 } 1029 return $return; 1030 } 1031 1032 /** 1033 * Merges all fonts properties 1034 * @param array $input_css 1035 * @return array 1036 * @version 1.3 1037 * @see dissolve_short_font() 1038 */ 1039 public function merge_font($input_css) { 1040 $font_prop_default = & $this->parser->data['csstidy']['font_prop_default']; 1041 $new_font_value = ''; 1042 $important = ''; 1043 // Skip if not font-family and font-size set 1044 if (isset($input_css['font-family']) && isset($input_css['font-size']) && $input_css['font-family'] != 'inherit') { 1045 // fix several words in font-family - add quotes 1046 if (isset($input_css['font-family'])) { 1047 $families = explode(',', $input_css['font-family']); 1048 $result_families = array(); 1049 foreach ($families as $family) { 1050 $family = trim($family); 1051 $len = strlen($family); 1052 if (strpos($family, ' ') && 1053 !(($family[0] === '"' && $family[$len - 1] === '"') || 1054 ($family[0] === "'" && $family[$len - 1] === "'"))) { 1055 $family = '"' . $family . '"'; 1056 } 1057 $result_families[] = $family; 1058 } 1059 $input_css['font-family'] = implode(',', $result_families); 1060 } 1061 foreach ($font_prop_default as $font_property => $default_value) { 1062 1063 // Skip if property does not exist 1064 if (!isset($input_css[$font_property])) { 1065 continue; 1066 } 1067 1068 $cur_value = $input_css[$font_property]; 1069 1070 // Skip if default value is used 1071 if ($cur_value === $default_value) { 1072 continue; 1073 } 1074 1075 // Remove !important 1076 if ($this->parser->is_important($cur_value)) { 1077 $important = '!important'; 1078 $cur_value = $this->parser->gvw_important($cur_value); 1079 } 1080 1081 $new_font_value .= $cur_value; 1082 // Add delimiter 1083 $new_font_value .= ( $font_property === 'font-size' && 1084 isset($input_css['line-height'])) ? '/' : ' '; 1085 } 1086 1087 $new_font_value = trim($new_font_value); 1088 1089 // Delete all font-properties 1090 foreach ($font_prop_default as $font_property => $default_value) { 1091 if ($font_property !== 'font' || !$new_font_value) 1092 unset($input_css[$font_property]); 1093 } 1094 1095 // Add new font property 1096 if ($new_font_value !== '') { 1097 $input_css['font'] = $new_font_value . $important; 1098 } 1099 } 1100 1101 return $input_css; 1102 } 1103 1104 /** 1105 * Reverse left vs right in a list of properties/values 1106 * @param array $array 1107 * @return array 1108 */ 1109 public function reverse_left_and_right($array) { 1110 $return = array(); 1111 1112 // change left <-> right in properties name and values 1113 foreach ($array as $propertie => $value) { 1114 1115 if (method_exists($this, $m = 'reverse_left_and_right_' . str_replace('-','_',trim($propertie)))) { 1116 $value = $this->$m($value); 1117 } 1118 1119 // simple replacement for properties 1120 $propertie = str_ireplace(array('left', 'right' ,"\x1"), array("\x1", 'left', 'right') , $propertie); 1121 // be careful for values, not modifying protected or quoted valued 1122 foreach (array('left' => "\x1", 'right' => 'left', "\x1" => 'right') as $v => $r) { 1123 if (strpos($value, $v) !== false) { 1124 // attraper les left et right separes du reste (pas au milieu d'un mot) 1125 if (in_array($v, array('left', 'right') )) { 1126 $value = preg_replace(",\\b$v\\b,", "\x0" , $value); 1127 } 1128 else { 1129 $value = str_replace($v, "\x0" , $value); 1130 } 1131 $value = $this->explode_ws("\x0", $value . ' ', true); 1132 $value = rtrim(implode($r, $value)); 1133 $value = str_replace("\x0" , $v, $value); 1134 } 1135 } 1136 $return[$propertie] = $value; 1137 } 1138 1139 return $return; 1140 } 1141 1142 /** 1143 * Reversing 4 values shorthands properties 1144 * @param string $value 1145 * @return string 1146 */ 1147 public function reverse_left_and_right_4value_shorthands($property, $value) { 1148 $shorthands = & $this->parser->data['csstidy']['shorthands']; 1149 if (isset($shorthands[$property])) { 1150 $property_right = $shorthands[$property][1]; 1151 $property_left = $shorthands[$property][3]; 1152 $v = $this->dissolve_4value_shorthands($property, $value); 1153 if ($v[$property_left] !== $v[$property_right]) { 1154 $r = $v[$property_right]; 1155 $v[$property_right] = $v[$property_left]; 1156 $v[$property_left] = $r; 1157 $v = $this->merge_4value_shorthands($v); 1158 if (isset($v[$property])) { 1159 return $v[$property]; 1160 } 1161 } 1162 } 1163 return $value; 1164 } 1165 1166 /** 1167 * Reversing 4 values radius shorthands properties 1168 * @param string $value 1169 * @return string 1170 */ 1171 public function reverse_left_and_right_4value_radius_shorthands($property, $value) { 1172 $shorthands = & $this->parser->data['csstidy']['radius_shorthands']; 1173 if (isset($shorthands[$property])) { 1174 $v = $this->dissolve_4value_radius_shorthands($property, $value); 1175 if ($v[$shorthands[$property][0]] !== $v[$shorthands[$property][1]] 1176 or $v[$shorthands[$property][2]] !== $v[$shorthands[$property][3]]) { 1177 $r = array( 1178 $shorthands[$property][0] => $v[$shorthands[$property][1]], 1179 $shorthands[$property][1] => $v[$shorthands[$property][0]], 1180 $shorthands[$property][2] => $v[$shorthands[$property][3]], 1181 $shorthands[$property][3] => $v[$shorthands[$property][2]], 1182 ); 1183 $v = $this->merge_4value_radius_shorthands($r); 1184 if (isset($v[$property])) { 1185 return $v[$property]; 1186 } 1187 } 1188 } 1189 return $value; 1190 } 1191 1192 /** 1193 * Reversing margin shorthands 1194 * @param string $value 1195 * @return string 1196 */ 1197 public function reverse_left_and_right_margin($value) { 1198 return $this->reverse_left_and_right_4value_shorthands('margin', $value); 1199 } 1200 1201 /** 1202 * Reversing padding shorthands 1203 * @param string $value 1204 * @return string 1205 */ 1206 public function reverse_left_and_right_padding($value) { 1207 return $this->reverse_left_and_right_4value_shorthands('padding', $value); 1208 } 1209 1210 /** 1211 * Reversing border-color shorthands 1212 * @param string $value 1213 * @return string 1214 */ 1215 public function reverse_left_and_right_border_color($value) { 1216 return $this->reverse_left_and_right_4value_shorthands('border-color', $value); 1217 } 1218 1219 /** 1220 * Reversing border-style shorthands 1221 * @param string $value 1222 * @return string 1223 */ 1224 public function reverse_left_and_right_border_style($value) { 1225 return $this->reverse_left_and_right_4value_shorthands('border-style', $value); 1226 } 1227 1228 /** 1229 * Reversing border-width shorthands 1230 * @param string $value 1231 * @return string 1232 */ 1233 public function reverse_left_and_right_border_width($value) { 1234 return $this->reverse_left_and_right_4value_shorthands('border-width', $value); 1235 } 1236 1237 /** 1238 * Reversing border-radius shorthands 1239 * @param string $value 1240 * @return string 1241 */ 1242 public function reverse_left_and_right_border_radius($value) { 1243 return $this->reverse_left_and_right_4value_radius_shorthands('border-radius', $value); 1244 } 1245 1246 /** 1247 * Reversing border-radius shorthands 1248 * @param string $value 1249 * @return string 1250 */ 1251 public function reverse_left_and_right__moz_border_radius($value) { 1252 return $this->reverse_left_and_right_4value_radius_shorthands('border-radius', $value); 1253 } 1254 1255 /** 1256 * Reversing border-radius shorthands 1257 * @param string $value 1258 * @return string 1259 */ 1260 public function reverse_left_and_right__webkit_border_radius($value) { 1261 return $this->reverse_left_and_right_4value_radius_shorthands('border-radius', $value); 1262 } 1263 1264 1265 /** 1266 * Reversing background shorthands 1267 * @param string $value 1268 * @return string 1269 */ 1270 public function reverse_left_and_right_background($value) { 1271 $values = $this->dissolve_short_bg($value); 1272 if (isset($values['background-position']) and $values['background-position']) { 1273 $v = $this->reverse_left_and_right_background_position($values['background-position']); 1274 if ($v !== $values['background-position']) { 1275 if ($value == $values['background-position']) { 1276 return $v; 1277 } 1278 else { 1279 $values['background-position'] = $v; 1280 $x = $this->merge_bg($values); 1281 if (isset($x['background'])) { 1282 return $x['background']; 1283 } 1284 } 1285 } 1286 } 1287 return $value; 1288 } 1289 1290 /** 1291 * Reversing background position shorthands 1292 * @param string $value 1293 * @return string 1294 */ 1295 public function reverse_left_and_right_background_position_x($value) { 1296 return $this->reverse_left_and_right_background_position($value); 1297 } 1298 1299 /** 1300 * Reversing background position shorthands 1301 * @param string $value 1302 * @return string 1303 */ 1304 public function reverse_left_and_right_background_position($value) { 1305 // multiple background case 1306 if (strpos($value, ',') !== false) { 1307 $values = $this->explode_ws(',', $value); 1308 if (count($values) > 1) { 1309 foreach ($values as $k=>$v) { 1310 $values[$k] = $this->reverse_left_and_right_background_position($v); 1311 } 1312 return implode(',', $values); 1313 } 1314 } 1315 1316 // if no explicit left or right value 1317 if (stripos($value, 'left') === false and stripos($value, 'right') === false) { 1318 $values = $this->explode_ws(' ', trim($value)); 1319 $values = array_map('trim', $values); 1320 $values = array_filter($values, function ($v) { return strlen($v);}); 1321 $values = array_values($values); 1322 if (count($values) == 1) { 1323 if (in_array($value, array('center', 'top', 'bottom', 'inherit', 'initial', 'unset'))) { 1324 return $value; 1325 } 1326 return "left $value"; 1327 } 1328 if ($values[1] == 'top' or $values[1] == 'bottom') { 1329 if ($values[0] === 'center') { 1330 return $value; 1331 } 1332 return 'left ' . implode(' ', $values); 1333 } 1334 else { 1335 $last = array_pop($values); 1336 if ($last === 'center') { 1337 return $value; 1338 } 1339 return implode(' ', $values) . ' left ' . $last; 1340 } 1341 } 1342 1343 return $value; 1344 } 1345 1346}