1<?php 2/** 3 * Class for importing and using CSS (new version). 4 * Partly uses code from the old version, e.g. css_declaration. 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author LarsDW223 8 */ 9 10/** 11 * Class css_attribute_selector. 12 * Simple storage class to save exactly one CSS attribute selector. 13 * 14 * @package CSS\CSSAttributeSelector 15 */ 16class css_attribute_selector { 17 /** var The namespace to which this attribute selector belongs */ 18 protected $namespaze = NULL; 19 /** var The attribute name */ 20 protected $attribute = NULL; 21 /** var The attribute selector operator */ 22 protected $operator = NULL; 23 /** var The attribute selector value */ 24 protected $value = NULL; 25 26 /** 27 * Construct the selector from $attribute_string. 28 * 29 * @param string $attribute_string String containing the selector 30 */ 31 public function __construct($attribute_string) { 32 $attribute_string = trim ($attribute_string, '[] '); 33 $found = strpos ($attribute_string, '|'); 34 if ($found !== false && 35 $attribute_string [$found+1] == '=') { 36 $found = strpos ($attribute_string, '|', $found+1); 37 } 38 if ($found !== false) { 39 if ($found > 0) { 40 $this->namespaze = substr ($attribute_string, 0, $found); 41 } 42 $attribute_string = substr ($attribute_string, $found + 1); 43 } 44 $found = strpos ($attribute_string, '='); 45 if ($found === false) { 46 $this->attribute = $attribute_string; 47 } else { 48 if (ctype_alpha($attribute_string [$found-1])) { 49 $this->attribute = substr($attribute_string, 0, $found); 50 $this->operator = '='; 51 $this->value = substr($attribute_string, $found + 1); 52 } else { 53 $this->attribute = substr($attribute_string, 0, $found - 1); 54 $this->operator = $attribute_string [$found-1].$attribute_string [$found]; 55 $this->value = substr($attribute_string, $found + 1); 56 } 57 $this->value = trim ($this->value, '"'); 58 } 59 } 60 61 /** 62 * The function checks if this atrribute selector matches the 63 * attributes given in $attributes as key - value pairs. 64 * 65 * @param string $attributes String containing the selector 66 * @return boolean 67 */ 68 public function matches (array $attributes=NULL) { 69 if (!isset($this->operator)) { 70 // Attribute should be present 71 return isset($attributes) && array_key_exists($this->attribute, $attributes); 72 } else { 73 switch ($this->operator) { 74 case '=': 75 // Attribute should have exactly the value $this->value 76 if ($attributes [$this->attribute] == $this->value) { 77 return true; 78 } else { 79 return false; 80 } 81 break; 82 83 case '~=': 84 // Attribute value should contain the word $this->value 85 $words = preg_split ('/\s/', $attributes [$this->attribute]); 86 if (array_search($this->value, $words) !== false) { 87 return true; 88 } else { 89 return false; 90 } 91 break; 92 93 case '|=': 94 // Attribute value should contain the word $this->value 95 // or a word starting with $this->value.'-' 96 $with_hypen = $this->value.'-'; 97 $length = strlen ($with_hypen); 98 if ($attributes [$this->attribute] == $this->value || 99 strncmp($attributes [$this->attribute], $with_hypen, $length) == 0) { 100 return true; 101 } 102 break; 103 104 case '^=': 105 // Attribute value should contain 106 // a word starting with $this->value 107 $length = strlen ($this->value); 108 if (strncmp($attributes [$this->attribute], $this->value, $length) == 0) { 109 return true; 110 } 111 break; 112 113 case '$=': 114 // Attribute value should contain 115 // a word ending with $this->value 116 $length = -1 * strlen ($this->value); 117 if (substr($attributes [$this->attribute], $length) == $this->value) { 118 return true; 119 } 120 break; 121 122 case '*=': 123 // Attribute value should include $this->value 124 if (strpos($attributes [$this->attribute], $this->value) !== false) { 125 return true; 126 } 127 break; 128 } 129 } 130 return false; 131 } 132 133 /** 134 * The function returns a string representation of this attribute 135 * selector (only for debugging purpose). 136 * 137 * @return string 138 */ 139 public function toString () { 140 $returnstring = '['; 141 if (!empty($this->namespaze)) { 142 $returnstring .= $this->namespaze.'|'; 143 } 144 $returnstring .= $this->attribute.$this->operator.$this->value; 145 $returnstring .= ']'; 146 return $returnstring; 147 } 148} 149 150/** 151 * Class css_simple_selector 152 * Simple storage class to save a simple CSS selector. 153 * 154 * @package CSS\CSSSimpleSelector 155 */ 156class css_simple_selector { 157 /** var Element name/Type of this simple selector */ 158 protected $type = NULL; 159 /** var Pseudo element which this selector matches */ 160 protected $pseudo_element = NULL; 161 /** var Id which this selector matches */ 162 protected $id = NULL; 163 /** var Classes which this selector matches */ 164 protected $classes = array(); 165 /** var Pseudo classes which this selector matches */ 166 protected $pseudo_classes = array(); 167 /** var Attributes which this selector matches */ 168 protected $attributes = array(); 169 /** var Specificity of this selector */ 170 protected $specificity = 0; 171 172 /** 173 * Internal function that checks if $sign is a sign that 174 * separates/identifies the different parts of an simple selector. 175 * 176 * @param character $sign 177 */ 178 protected function isSpecialSign ($sign) { 179 switch ($sign) { 180 case '.': 181 case '[': 182 case '#': 183 case ':': 184 return true; 185 } 186 return false; 187 } 188 189 /** 190 * Construct the simple selector from $simple_selector_string. 191 * 192 * @param string $simple_selector_string String containing the selector 193 */ 194 public function __construct($simple_selector_string) { 195 $pos = 0; 196 $simple_selector_string = trim ($simple_selector_string); 197 $max = strlen ($simple_selector_string); 198 if ($max == 0) { 199 $this->type = '*'; 200 return; 201 } 202 203 $a = 0; 204 $b = 0; 205 $c = 0; 206 207 $content = ''; 208 $first_sign = ''; 209 $first = true; 210 $pseudo_element = false; 211 while ($pos < $max) { 212 $sign = $simple_selector_string [$pos]; 213 if ($this->isSpecialSign ($sign)) { 214 if ($pos == 0) { 215 $first_sign = $sign; 216 } else { 217 // Found the end. 218 if (empty($first_sign)) { 219 // Element name/type 220 $this->type = $content; 221 if ($content != '*') { 222 $c++; 223 } 224 } else if ($first_sign == '.') { 225 // Class 226 $this->classes[] = $content; 227 $b++; 228 } else if ($first_sign == '#') { 229 // ID 230 $this->id = $content; 231 $a++; 232 } else if ($first_sign == ':') { 233 //if ($next_sign != ':') { 234 if (!$pseudo_element) { 235 // Pseudo class 236 $this->pseudo_classes[] = $content; 237 $b++; 238 } else { 239 // Pseudo element 240 $this->pseudo_element = $content; 241 $c++; 242 } 243 } else if ($first_sign == '[') { 244 $this->attributes [] = new css_attribute_selector($content); 245 $b++; 246 } 247 $first_sign = $sign; 248 $next_sign = $simple_selector_string [$pos+1]; 249 if ($first_sign == ':' && $next_sign == ':') { 250 $pseudo_element = true; 251 $pos++; 252 } else { 253 $pseudo_element = false; 254 } 255 $content = ''; 256 } 257 } else { 258 $content .= $sign; 259 } 260 $pos++; 261 } 262 263 // If $content is not empty then parse it 264 if (!empty($content)) { 265 if (empty($first_sign)) { 266 // Element name/type 267 $this->type = $content; 268 if ($content != '*') { 269 $c++; 270 } 271 } else if ($first_sign == '.') { 272 // Class 273 $this->classes[] = $content; 274 $b++; 275 } else if ($first_sign == '#') { 276 // ID 277 $this->id = $content; 278 $a++; 279 } else if ($first_sign == ':') { 280 if ($next_sign != ':') { 281 // Pseudo class 282 $this->pseudo_classes[] = $content; 283 $b++; 284 } else { 285 // Pseudo element 286 $this->pseudo_element = $content; 287 $c++; 288 } 289 } else if ($first_sign == '[') { 290 $this->attributes [] = new css_attribute_selector($content); 291 $b++; 292 } 293 } 294 295 // Calculate specificity 296 $this->specificity = $a * 100 + $b *10 + $c; 297 } 298 299 /** 300 * The functions checks wheter this simple selector matches the given 301 * $element or not. $element must support the interface iElementCSSMatchable 302 * to enable this class to do the CSS selector matching. 303 * 304 * @param iElementCSSMatchable $element Element to check 305 * @return boolean 306 */ 307 public function matches_entry (iElementCSSMatchable $element) { 308 $element_attrs = $element->iECSSM_getAttributes(); 309 310 // Match type/element 311 if (!empty($this->type) && 312 $this->type != '*' && 313 $this->type != $element->iECSSM_getName()) { 314 return false; 315 } 316 317 // Match class(es) 318 if (count($this->classes) > 0) { 319 if (empty($element_attrs ['class'])) { 320 return false; 321 } 322 $comp = explode (' ', $element_attrs ['class']); 323 foreach ($this->classes as $search) { 324 if (array_search($search, $comp) === false) { 325 return false; 326 } 327 } 328 } 329 330 // Match id 331 if (!empty($this->id) && 332 !empty($element_attrs ['id']) && 333 $this->id != $element_attrs ['id']) { 334 return false; 335 } 336 337 // Match attributes 338 foreach ($this->attributes as $attr_sel) { 339 if ($attr_sel->matches ($element_attrs) === false) { 340 return false; 341 } 342 } 343 344 // Match pseudo class(es) 345 if (count($this->pseudo_classes) > 0) { 346 foreach ($this->pseudo_classes as $search) { 347 if ($element->iECSSM_has_pseudo_class($search) == false) { 348 return false; 349 } 350 } 351 } 352 353 // Match pseudo element 354 if (!empty($this->pseudo_element)) { 355 if ($element->iECSSM_has_pseudo_element($this->pseudo_element) == false) { 356 return false; 357 } 358 } 359 360 return true; 361 } 362 363 /** 364 * The function returns a string representation of this simple 365 * selector (only for debugging purpose). 366 * 367 * @return string 368 */ 369 public function toString () { 370 $returnstring = ''; 371 if (!empty($this->type)) { 372 $returnstring .= $this->type; 373 } 374 if (!empty($this->id)) { 375 $returnstring .= '#'.$this->id; 376 } 377 foreach ($this->classes as $class) { 378 $returnstring .= '.'.$class; 379 } 380 foreach ($this->attributes as $attr_sel) { 381 $returnstring .= $attr_sel->toString(); 382 } 383 return $returnstring; 384 } 385 386 /** 387 * Return the specificity of this simple selector. 388 * 389 * @return integer 390 */ 391 public function getSpecificity () { 392 return $this->specificity; 393 } 394} 395 396/** 397 * Class css_selector. 398 * Storage class to save a complete CSS selector. 399 * The class can also store multiple selectors, e.g. like 'h1 , h2, h3 {...}' 400 * 401 * @package CSS\CSSSelector 402 */ 403class css_selector { 404 /** var Known combinators */ 405 static protected $combinators = ' ,>+~'; 406 /** var Brackets */ 407 static protected $brackets = '[]'; 408 /** var String from which this selector was created */ 409 protected $selector_string = NULL; 410 /** var Array with parsed selector(s) */ 411 protected $selectors_parsed = array(); 412 /** var Specificity of this selector */ 413 protected $specificity = array(); 414 415 /** 416 * Construct the selector from $selector_string. 417 * 418 * @param string $selector_string String containing the selector 419 */ 420 public function __construct($selector_string) { 421 $selector_string = str_replace("\n", '', $selector_string); 422 $this->selector_string = trim($selector_string); 423 424 $pos = 0; 425 $max = strlen($this->selector_string); 426 $current = ''; 427 $selector = array(); 428 $specificity = 0; 429 $size = 0; 430 $in_brackets = false; 431 $separators = self::$combinators.self::$brackets; 432 while ($pos < $max) { 433 $sign = $this->selector_string [$pos]; 434 $result = strpos ($separators, $sign); 435 if ($sign == '[') { 436 $in_brackets = true; 437 } 438 if ($result === false || $in_brackets == true) { 439 // No combinator 440 $current .= $sign; 441 $pos++; 442 443 if ($sign == ']') { 444 $in_brackets = false; 445 } 446 } else { 447 // Parse current 448 $selector [$size]['selector'] = new css_simple_selector($current); 449 $specificity += $selector [$size]['selector']->getSpecificity(); 450 $size++; 451 $current = ''; 452 453 $combinator = $sign; 454 $pos++; 455 while ($pos < $max) { 456 $sign = $this->selector_string[$pos]; 457 if (strpos (self::$combinators, $sign) === false) { 458 break; 459 } 460 $combinator .= $sign; 461 $pos++; 462 } 463 if (ctype_space($combinator)) { 464 $selector [$size]['combinator'] = ' '; 465 $size++; 466 } else { 467 $combinator = trim ($combinator, ' '); 468 if ($combinator != ',') { 469 $selector [$size]['combinator'] = $combinator[0]; 470 $size++; 471 } else { 472 $this->selectors_parsed [] = $selector; 473 $this->specificity [] = $specificity; 474 $selector = array(); 475 $size = 0; 476 $specificity = 0; 477 } 478 } 479 } 480 } 481 if (!empty($current)) { 482 $selector [$size]['selector'] = new css_simple_selector($current); 483 $specificity += $selector [$size]['selector']->getSpecificity(); 484 $this->selectors_parsed [] = $selector; 485 $this->specificity [] = $specificity; 486 } 487 } 488 489 /** 490 * The function checks if the combined simple selectors in $selector 491 * match $element or not. $element must support the interface iElementCSSMatchable 492 * to enable this class to do the CSS selector matching. 493 * 494 * @param array $selector Internal selector array 495 * @param iElementCSSMatchable $element Element to check 496 * @return boolean 497 */ 498 protected function selector_matches (array $selector, iElementCSSMatchable $element) { 499 $combinator = ''; 500 $found = 0; 501 $size = count($selector); 502 if ($size == 0 ) { 503 return false; 504 } 505 506 // First entry should be a selector 507 if (!isset($selector [$size-1]['selector'])) { 508 // No! (Error) 509 return false; 510 } 511 512 // Start comparison with the current element 513 $simple = $selector [$size-1]['selector']; 514 if ($simple->matches_entry ($element) == false) { 515 // If the current open element does not match then there is no match 516 return false; 517 } 518 if ($size == 1) { 519 // We are finished already 520 return true; 521 } 522 523 // Next entry should be a combinator 524 if (!isset($selector [$size-2]['combinator'])) { 525 // No! (Error) 526 return false; 527 } 528 $combinator = $selector [$size-2]['combinator']; 529 530 $start_search = $element; 531 for ($index = $size-3 ; $index >= 0 ; $index--) { 532 // If we get here but start_search is already negative then there are 533 // selectors left but no more subjects/element to match. 534 if ($start_search < 0) { 535 return false; 536 } 537 if (empty($selector [$index]['combinator'])) { 538 $simple = $selector [$index]['selector']; 539 switch ($combinator) { 540 case ' ': 541 // Find any parent, parent's parent... that matches our simple selector 542 do { 543 $parent = $start_search->iECSSM_getParent(); 544 if (!isset($parent)) { 545 return false; 546 } 547 $start_search = $parent; 548 $is_match = $simple->matches_entry ($parent); 549 if ($is_match == true) { 550 // Found match. Stop this search. 551 break; 552 } 553 }while (isset($parent)); 554 555 // Did we find anything? 556 if (!$is_match) { 557 // No. 558 return false; 559 } 560 $start_search = $parent; 561 break; 562 563 case '>': 564 // Check if we have a parent and if it matches our simple selector 565 $parent = $start_search->iECSSM_getParent(); 566 if (!isset($parent)) { 567 return false; 568 } 569 if ($simple->matches_entry ($parent) == false) { 570 // No match. 571 return false; 572 } 573 $start_search = $parent; 574 break; 575 576 case '+': 577 // Immediate preceding sibling must match our simple selector 578 $sibling = $start_search->iECSSM_getPrecedingSibling(); 579 if (!isset($sibling)) { 580 return false; 581 } 582 if ($simple->matches_entry ($sibling) == false) { 583 // No match. 584 return false; 585 } 586 $start_search = $sibling; 587 break; 588 589 case '~': 590 // One of the preceding siblings must match our simple selector 591 do { 592 $sibling = $start_search->iECSSM_getPrecedingSibling(); 593 if (!isset($sibling)) { 594 return false; 595 } 596 $start_search = $sibling; 597 if ($simple->matches_entry ($sibling) == true) { 598 // Found match. Stop this search. 599 break; 600 } 601 }while (isset($sibling)); 602 603 // Did we find anything? 604 if (!isset($sibling)) { 605 // No. 606 return false; 607 } 608 $start_search = $sibling; 609 break; 610 611 // We won't get the combinator ',' here cause that is 612 // handled at construction time by creating an array of selectors 613 //case ',': 614 // break; 615 } 616 } else { 617 $combinator = $selector [$index]['combinator']; 618 } 619 } 620 621 // If we get here then everything matches! 622 return true; 623 } 624 625 /** 626 * The functions checks wheter any selector stored in this object 627 * match the given $element or not. $element must support the interface 628 * iElementCSSMatchable to enable this class to do the CSS selector matching. 629 * 630 * @param iElementCSSMatchable $element Element to check 631 * @param integer $specificity Specificity of matching selector 632 * @return boolean 633 */ 634 public function matches (iElementCSSMatchable $element, &$specificity) { 635 $size = count ($this->selectors_parsed); 636 $match = false; 637 $specificity = 0; 638 for ($index = 0 ; $index < $size ; $index++) { 639 if ($this->selector_matches ($this->selectors_parsed [$index], $element) == true) { 640 if ($this->specificity [$index] > $specificity) { 641 $specificity = $this->specificity [$index]; 642 } 643 $match = true; 644 } 645 } 646 return $match; 647 } 648 649 /** 650 * The function returns a string representation of this 651 * selector (only for debugging purpose). 652 * 653 * @return string 654 */ 655 public function toString () { 656 $returnstring = ''; 657 $max = count($this->selectors_parsed); 658 $index_parsed = 0; 659 foreach ($this->selectors_parsed as $selector) { 660 $size = count($selector); 661 for ($index = 0 ; $index < $size ; $index++) { 662 if ( isset($selector [$index]['combinator']) ) { 663 if ($selector [$index]['combinator'] == ' ') { 664 $returnstring .= ' '; 665 } else { 666 $returnstring .= ' '.$selector [$index]['combinator'].' '; 667 } 668 } else { 669 $simple = $selector [$index]['selector']; 670 $returnstring .= $simple->toString(); 671 if ($index < $size-1) { 672 $returnstring .= ' '; 673 } 674 } 675 } 676 $index_parsed++; 677 if ($index_parsed < $max) { 678 $returnstring .= ','; 679 } 680 } 681 return $returnstring; 682 } 683} 684 685/** 686 * Class css_rule_new. 687 * 688 * @package CSS\CSSRuleNew 689 */ 690class css_rule_new { 691 /** @var Media selector to which this rule belongs */ 692 protected $media = NULL; 693 /** @var Selector string from which this rule was created */ 694 protected $selector = NULL; 695 /** @var Array of css_declaration objects */ 696 protected $declarations = array (); 697 698 /** 699 * Construct rule from strings $selector and $decls. 700 * 701 * @param string $selector String containing the selector 702 * @param string $decls String containing the declarations 703 * @param string|null $media String containing the media selector 704 */ 705 public function __construct($selector, $decls, $media = NULL) { 706 707 $this->media = trim ($media); 708 //print ("\nNew rule: ".$media."\n"); //Debuging 709 710 // Create/parse selector 711 $this->selector = new css_selector ($selector); 712 713 $decls = trim ($decls, '{}'); 714 715 // Parse declarations 716 $pos = 0; 717 $end = strlen ($decls); 718 while ( $pos < $end ) { 719 $colon = strpos ($decls, ':', $pos); 720 if ( $colon === false ) { 721 break; 722 } 723 $semi = strpos ($decls, ';', $colon + 1); 724 if ( $semi === false ) { 725 break; 726 } 727 728 $property = substr ($decls, $pos, $colon - $pos); 729 $property = trim($property); 730 731 $value = substr ($decls, $colon + 1, $semi - ($colon + 1)); 732 $value = trim ($value); 733 $values = preg_split ('/\s+/', $value); 734 $value = ''; 735 foreach ($values as $part) { 736 if ( $part != '!important' ) { 737 $value .= ' '.$part; 738 } 739 } 740 $value = trim($value); 741 742 // Create new declaration 743 $declaration = new css_declaration ($property, $value); 744 $this->declarations [] = $declaration; 745 746 // Handle CSS shorthands, e.g. 'border' 747 if ( $declaration->isShorthand () === true ) { 748 $declaration->explode ($this->declarations); 749 } 750 751 $pos = $semi + 1; 752 } 753 } 754 755 /** 756 * The function returns a string representation of this 757 * rule (only for debugging purpose). 758 * 759 * @return string 760 */ 761 public function toString () { 762 $returnString = ''; 763 $returnString .= "Media= \"".$this->media."\"\n"; 764 $returnString .= $this->selector->toString().' '; 765 $returnString .= "{\n"; 766 foreach ($this->declarations as $declaration) { 767 $returnString .= $declaration->getProperty ().':'.$declaration->getValue ().";\n"; 768 } 769 $returnString .= "}\n"; 770 return $returnString; 771 } 772 773 /** 774 * The functions checks wheter this rule matches the given $element 775 * or not. $element must support the interface iElementCSSMatchable 776 * to enable this class to do the CSS selector matching. 777 * 778 * @param iElementCSSMatchable $element Element to check 779 * @param integer $specificity Specificity of matching selector 780 * @param string $media Media selector to match 781 * @return boolean 782 */ 783 public function matches(iElementCSSMatchable $element, &$specificity, $media = NULL) { 784 $media = trim($media); 785 if ( !empty($this->media) && $media !== $this->media ) { 786 // Wrong media 787 //print ("\nNo-Match ".$this->media."==".$media); //Debuging 788 return false; 789 } 790 791 // The rules does match if the selector does match 792 $result = $this->selector->matches($element, $specificity); 793 794 return $result; 795 } 796 797 /** 798 * The function returns the value of property $name or null if a 799 * property with that name does not exist in this rule. 800 * 801 * @param string $name The property name 802 * @return string|null 803 */ 804 public function getProperty ($name) { 805 foreach ($this->declarations as $declaration) { 806 if ( $name == $declaration->getProperty () ) { 807 return $declaration->getValue (); 808 } 809 } 810 return NULL; 811 } 812 813 /** 814 * The function stores all properties of this rule in the array 815 * $values as key - value pairs, e.g. $values ['color'] = 'red'; 816 * 817 * @param array $values Array for property storage 818 * @return null 819 */ 820 public function getProperties (&$values) { 821 foreach ($this->declarations as $declaration) { 822 $property = $declaration->getProperty (); 823 $value = $declaration->getValue (); 824 $values [$property] = $value; 825 } 826 return NULL; 827 } 828 829 /** 830 * The function calls $callback for each property stored in this 831 * rule containing a length value. The return value of $callback 832 * is saved as the new property value. 833 * 834 * @param callable $callback 835 */ 836 public function adjustLengthValues ($callback) { 837 foreach ($this->declarations as $declaration) { 838 $declaration->adjustLengthValues ($callback, $this); 839 } 840 } 841 842 /** 843 * The function calls $callback for each property stored in this 844 * rule containing a URL reference. The return value of $callback 845 * is saved as the new property value. 846 * 847 * @param callable $callback 848 */ 849 public function replaceURLPrefixes ($callback) { 850 foreach ($this->declarations as $declaration) { 851 $declaration->replaceURLPrefixes ($callback); 852 } 853 } 854} 855 856/** 857 * Class cssimportnew 858 * 859 * @package CSS\CSSImportNew 860 */ 861class cssimportnew { 862 /** var Imported raw CSS code */ 863 protected $raw; 864 /** @var Array of css_rule_new */ 865 protected $rules = array (); 866 /** @var Actually set media selector */ 867 protected $media = NULL; 868 869 /** 870 * Import CSS code from string $contents. 871 * Returns true on success or false if any error occured during CSS parsing. 872 * 873 * @param string $contents 874 * @return boolean 875 */ 876 function importFromString($contents) { 877 $this->deleteComments ($contents); 878 return $this->importFromStringInternal ($contents); 879 } 880 881 /** 882 * Delete comments in $contents. All comments are overwritten with spaces. 883 * The '&' is required. DO NOT DELETE!!! 884 * 885 * @param $contents 886 */ 887 protected function deleteComments (&$contents) { 888 // Delete all comments first 889 $pos = 0; 890 $max = strlen ($contents); 891 $in_comment = false; 892 while ( $pos < $max ) { 893 if ( ($pos+1) < $max && 894 $contents [$pos] == '/' && 895 $contents [$pos+1] == '*' ) { 896 $in_comment = true; 897 898 $contents [$pos] = ' '; 899 $contents [$pos+1] = ' '; 900 $pos += 2; 901 continue; 902 } 903 if ( ($pos+1) < $max && 904 $contents [$pos] == '*' && 905 $contents [$pos+1] == '/' && 906 $in_comment === true ) { 907 $in_comment = false; 908 909 $contents [$pos] = ' '; 910 $contents [$pos+1] = ' '; 911 $pos += 2; 912 continue; 913 } 914 if ( $in_comment === true ) { 915 $contents [$pos] = ' '; 916 } 917 $pos++; 918 } 919 } 920 921 /** 922 * Set the media selector to use for CSS matching to $media. 923 * 924 * @param string $media 925 */ 926 public function setMedia($media) { 927 $this->media = $media; 928 } 929 930 /** 931 * Return the actually set media selector. 932 * 933 * @return string 934 */ 935 public function getMedia() { 936 return $this->media; 937 } 938 939 /** 940 * Internal function that imports CSS code from string $contents. 941 * (The function is calling itself recursively) 942 * 943 * @param string $contents 944 * @param string|null $media Actually valid media selector 945 * @param integer $processed Position to which $contents were parsed 946 * @return bool 947 */ 948 protected function importFromStringInternal($contents, $media = NULL, &$processed = NULL) { 949 // Find all CSS rules 950 $pos = 0; 951 $max = strlen ($contents); 952 while ( $pos < $max ) { 953 $bracket_open = strpos ($contents, '{', $pos); 954 if ( $bracket_open === false ) { 955 return false; 956 } 957 $bracket_close = strpos ($contents, '}', $pos); 958 if ( $bracket_close === false ) { 959 return false; 960 } 961 962 // If this is a nested call we might hit a closing } for the media section 963 // which was the reason for this function call. In this case break and return. 964 if ( $bracket_close < $bracket_open ) { 965 $pos = $bracket_close + 1; 966 break; 967 } 968 969 // Get the part before the open bracket and the last closing bracket 970 // (or the start of the string). 971 $before_open_bracket = substr ($contents, $pos, $bracket_open - $pos); 972 973 // Is it a @something rule? 974 $before_open_bracket = trim ($before_open_bracket); 975 $at_rule_pos = stripos($before_open_bracket, '@'); 976 if ( $at_rule_pos !== false ) { 977 $at_rule_end = stripos($before_open_bracket, ' '); 978 979 // Yes, decode content as normal rules with @something ... { ... } 980 $at_rule_name = substr ($before_open_bracket, $at_rule_pos, $at_rule_end - $at_rule_pos); 981 if ($at_rule_name == '@media') { 982 $at_rule_name = substr ($before_open_bracket, $at_rule_end); 983 } 984 $contents_in_media = substr ($contents, $bracket_open + 1); 985 986 $nested_processed = 0; 987 $result = $this->importFromStringInternal ($contents_in_media, $at_rule_name, $nested_processed); 988 if ( $result !== true ) { 989 // Stop parsing on error. 990 return false; 991 } 992 unset ($at_rule_name); 993 $pos = $bracket_open + 1 + $nested_processed; 994 } else { 995 996 // No, decode rule the normal way selector { ... } 997 // The selector is stored in $before_open_bracket 998 $decls = substr ($contents, $bracket_open + 1, $bracket_close - $bracket_open); 999 $this->rules [] = new css_rule_new ($before_open_bracket, $decls, $media); 1000 1001 $pos = $bracket_close + 1; 1002 } 1003 } 1004 if ( isset($processed) ) { 1005 $processed = $pos; 1006 } 1007 return true; 1008 } 1009 1010 /** 1011 * Import CSS code from file filename. 1012 * Returns true on success or false if any error occured during CSS parsing. 1013 * 1014 * @param string $filename 1015 * @return boolean 1016 */ 1017 function importFromFile($filename) { 1018 // Try to read in the file content 1019 if ( empty($filename) ) { 1020 return false; 1021 } 1022 1023 $handle = fopen($filename, "rb"); 1024 if ( $handle === false ) { 1025 return false; 1026 } 1027 1028 $contents = fread($handle, filesize($filename)); 1029 fclose($handle); 1030 if ( $contents === false ) { 1031 return false; 1032 } 1033 1034 return $this->importFromString ($contents); 1035 } 1036 1037 /** 1038 * Return the original CSS code that was imported. 1039 * 1040 * @return string 1041 */ 1042 public function getRaw () { 1043 return $this->raw; 1044 } 1045 1046 /** 1047 * Get the value of CSS property for element $element. 1048 * If $element is not matched by any rule or the rule(s) matching 1049 * do not contain the property $name then null is returned. 1050 * 1051 * @param string $name Name of queried property 1052 * @param iElementCSSMatchable $element Element to match 1053 * @return string|null 1054 */ 1055 public function getPropertyForElement ($name, iElementCSSMatchable $element) { 1056 if ( empty ($name) ) { 1057 return NULL; 1058 } 1059 1060 $value = NULL; 1061 $highest = 0; 1062 foreach ($this->rules as $rule) { 1063 $matched = $rule->matches($element, $specificity, $this->media); 1064 if ( $matched !== false ) { 1065 $current = $rule->getProperty ($name); 1066 1067 // Only accept the property value if the current specificity of the matched 1068 // rule/selector is higher or equal than the highest one. 1069 if ( !empty ($current) && $specificity >= $highest) { 1070 $highest = $specificity; 1071 $value = $current; 1072 } 1073 } 1074 } 1075 1076 return $value; 1077 } 1078 1079 /** 1080 * Get all properties for element $element and store them in $dest. 1081 * Properties are stored as key -value pairs, e.g. $dest ['color'] = 'red'; 1082 * If $element is not matched by any rule then array $dest will be 1083 * empty (if it was empty before the call!). 1084 * 1085 * @param array $dest Property storage 1086 * @param iElementCSSMatchable $element Element to match 1087 * @param ODTUnits $units ODTUnits object for conversion 1088 * @param boolean $inherit Enable/disable inheritance 1089 * @return string|null 1090 */ 1091 public function getPropertiesForElement (&$dest, iElementCSSMatchable $element, ODTUnits $units, $inherit=true) { 1092 if (!isset($element)) { 1093 return; 1094 } 1095 1096 $highest = array(); 1097 $temp = array(); 1098 foreach ($this->rules as $rule) { 1099 $matched = $rule->matches ($element, $specificity, $this->media); 1100 if ( $matched !== false ) { 1101 $current = array(); 1102 $rule->getProperties ($current); 1103 1104 // Only accept a property value if the current specificity of the matched 1105 // rule/selector is higher or equal than the highest one. 1106 foreach ($current as $property => $value) { 1107 if (isset($highest [$property]) && $specificity >= $highest [$property]) { 1108 $highest [$property] = $specificity; 1109 $temp [$property] = $value; 1110 } 1111 } 1112 } 1113 } 1114 1115 // Add inline style properties if present (always have highest specificity): 1116 // Create rule with selector '*' (doesn't matter) and inline style declarations 1117 $attributes = $element->iECSSM_getAttributes(); 1118 if (!empty($attributes ['style'])) { 1119 $rule = new css_rule ('*', $attributes ['style']); 1120 $rule->getProperties ($temp); 1121 } 1122 1123 if ($inherit) { 1124 // Now calculate absolute values and inherit values from parents 1125 $this->calculateAndInherit ($temp, $element, $units); 1126 unset($temp ['calculated']); 1127 } 1128 1129 $dest = $temp; 1130 } 1131 1132 /** 1133 * Get the value of CSS property for element $parent. If $parent has 1134 * no match for the property with name $key then return the value of 1135 * the property for $parent's parents. 1136 * 1137 * @param string $key Name of queried property 1138 * @param iElementCSSMatchable $parent Element to match 1139 * @return string|null 1140 */ 1141 protected function getParentsValue($key, iElementCSSMatchable $parent) { 1142 $properties = $parent->getProperties (); 1143 if (isset($properties [$key])) { 1144 return $properties [$key]; 1145 } 1146 1147 $parentsParent = $parent->iECSSM_getParent(); 1148 if (isset($parentsParent)) { 1149 return $this->getParentsValue($key, $parentsParent); 1150 } 1151 1152 return NULL; 1153 } 1154 1155 /** 1156 * The function calculates the absolute values for the relative 1157 * property values of element $element and store them in $properties. 1158 * 1159 * @param array $properties Property storage 1160 * @param iElementCSSMatchable $element Element to match 1161 * @param ODTUnits $units ODTUnits object for conversion 1162 */ 1163 protected function calculate (array &$properties, iElementCSSMatchable $element, ODTUnits $units) { 1164 if (isset($properties ['calculated']) && $properties ['calculated'] == '1') { 1165 // Already done 1166 return; 1167 } 1168 1169 $properties ['calculated'] = '1'; 1170 $parent = $element->iECSSM_getParent(); 1171 1172 // First get absolute font-size in points for 1173 // conversion of relative units 1174 if (isset($parent)) { 1175 $font_size = $this->getParentsValue('font-size', $parent); 1176 } 1177 if (isset($font_size)) { 1178 // Use the parents value 1179 // (It is assumed that the value is already calculated to an absolute 1180 // value. That's why the loops in calculateAndInherit() must run backwards 1181 $base_font_size_in_pt = $units->getDigits($font_size); 1182 } else { 1183 // If there is no parent value use global setting 1184 $base_font_size_in_pt = $units->getPixelPerEm ().'px'; 1185 $base_font_size_in_pt = $units->toPoints($base_font_size_in_pt, 'y'); 1186 $base_font_size_in_pt = $units->getDigits($base_font_size_in_pt); 1187 } 1188 1189 // Do we have font-size or line-height set? 1190 if (isset($properties ['font-size']) || isset($properties ['line-height'])) { 1191 if (isset($properties ['font-size'])) { 1192 $font_size_unit = $units->stripDigits($properties ['font-size']); 1193 $font_size_digits = $units->getDigits($properties ['font-size']); 1194 if ($font_size_unit == '%' || $font_size_unit == 'em') { 1195 $base_font_size_in_pt = $units->getAbsoluteValue ($properties ['font-size'], $base_font_size_in_pt); 1196 $properties ['font-size'] = $base_font_size_in_pt.'pt'; 1197 1198 } elseif ($font_size_unit != 'pt') { 1199 $properties ['font-size'] = $units->toPoints($properties ['font-size'], 'y'); 1200 $base_font_size_in_pt = $units->getDigits($properties ['font-size']); 1201 } else { 1202 $base_font_size_in_pt = $units->getDigits($properties ['font-size']); 1203 } 1204 } 1205 1206 // Convert relative line-heights to absolute 1207 if (isset($properties ['line-height'])) { 1208 $line_height_unit = $units->stripDigits($properties ['line-height']); 1209 $line_height_digits = $units->getDigits($properties ['line-height']); 1210 if ($line_height_unit == '%') { 1211 $properties ['line-height'] = (($line_height_digits * $base_font_size_in_pt)/100).'pt'; 1212 } elseif (empty($line_height_unit)) { 1213 $properties ['line-height'] = ($line_height_digits * $base_font_size_in_pt).'pt'; 1214 } 1215 } 1216 } 1217 1218 // Calculate all other absolute values 1219 // (NOT 'width' as it depends on the encapsulating element, 1220 // and not 'font-size' and 'line-height' => already done above 1221 foreach ($properties as $key => $value) { 1222 switch ($key) { 1223 case 'width': 1224 case 'font-size': 1225 case 'line-height': 1226 // Do nothing. 1227 break; 1228 case 'margin': 1229 case 'margin-left': 1230 case 'margin-right': 1231 case 'margin-top': 1232 case 'margin-bottom': 1233 // Do nothing. 1234 // We do not know the size of the surrounding element. 1235 break; 1236 default: 1237 // Convert '%' or 'em' value based on determined font-size 1238 $unit = $units->stripDigits($value); 1239 if ($unit == '%' || $unit == 'em') { 1240 $value = $units->getAbsoluteValue ($value, $base_font_size_in_pt); 1241 $properties [$key] = $value.'pt'; 1242 } 1243 break; 1244 } 1245 } 1246 1247 $element->setProperties($properties); 1248 } 1249 1250 /** 1251 * The function inherits all properties of the $parents into array 1252 * $dest. $parents is an array of elements (iElementCSSMatchable). 1253 * 1254 * @param array $dest Property storage 1255 * @param array $parents Parents to inherit from 1256 */ 1257 protected function inherit (array &$dest, array $parents) { 1258 // Inherit properties of all parents 1259 // (MUST be done backwards!) 1260 $max = count ($parents); 1261 foreach ($parents as $parent) { 1262 $properties = $parent->getProperties (); 1263 foreach ($properties as $key => $value) { 1264 if ($dest [$key] == 'inherit') { 1265 $dest [$key] = $value; 1266 } else { 1267 if (strncmp($key, 'background', strlen('background')) == 0) { 1268 // The property may not be inherited 1269 continue; 1270 } 1271 if (strncmp($key, 'border', strlen('border')) == 0) { 1272 // The property may not be inherited 1273 continue; 1274 } 1275 if (strncmp($key, 'padding', strlen('padding')) == 0) { 1276 // The property may not be inherited 1277 continue; 1278 } 1279 if (strncmp($key, 'margin', strlen('margin')) == 0) { 1280 // The property may not be inherited 1281 continue; 1282 } 1283 if (strncmp($key, 'outline', strlen('outline')) == 0) { 1284 // The property may not be inherited 1285 continue; 1286 } 1287 if (strncmp($key, 'counter', strlen('counter')) == 0) { 1288 // The property may not be inherited 1289 continue; 1290 } 1291 if (strncmp($key, 'page-break', strlen('page-break')) == 0) { 1292 // The property may not be inherited 1293 continue; 1294 } 1295 if (strncmp($key, 'cue', strlen('cue')) == 0) { 1296 // The property may not be inherited 1297 continue; 1298 } 1299 if (strncmp($key, 'pause', strlen('pause')) == 0) { 1300 // The property may not be inherited 1301 continue; 1302 } 1303 if (strpos($key, 'width') !== false) { 1304 // The property may not be inherited 1305 continue; 1306 } 1307 if (strpos($key, 'height') !== false) { 1308 // The property may not be inherited 1309 continue; 1310 } 1311 switch ($key) { 1312 case 'text-decoration': 1313 case 'text-shadow': 1314 case 'display': 1315 case 'table-layout': 1316 case 'vertical-align': 1317 case 'visibility': 1318 case 'position': 1319 case 'top': 1320 case 'right': 1321 case 'bottom': 1322 case 'left': 1323 case 'float': 1324 case 'clear': 1325 case 'z-index': 1326 case 'unicode-bidi': 1327 case 'overflow': 1328 case 'clip': 1329 case 'visibility': 1330 case 'content': 1331 case 'marker-offset': 1332 case 'play-during': 1333 // The property may not be inherited 1334 break; 1335 default: 1336 if (!isset($dest [$key]) || $dest [$key] == 'inherit') { 1337 $dest [$key] = $value; 1338 } 1339 break; 1340 } 1341 } 1342 } 1343 } 1344 } 1345 1346 /** 1347 * Main function performing calculation and inheritance for element 1348 * $element. Properties are stored in $dest. 1349 * 1350 * @param array $dest Property storage 1351 * @param array $element Element to match 1352 * @param ODTUnits $units ODTUnits object for conversion 1353 */ 1354 protected function calculateAndInherit (array &$dest, iElementCSSMatchable $element, ODTUnits $units) { 1355 $parents = array(); 1356 $parent = $element->iECSSM_getParent(); 1357 while (isset($parent)) { 1358 $parents [] = $parent; 1359 $parent = $parent->iECSSM_getParent(); 1360 } 1361 1362 // Determine properties of all parents if not done yet 1363 // and calculate absolute values 1364 // (MUST be done backwards!) 1365 $max = count ($parents); 1366 for ($index = $max-1 ; $index >= 0 ; $index--) { 1367 $properties = $parents [$index]->getProperties (); 1368 if (!isset($properties)) { 1369 $properties = array(); 1370 $this->getPropertiesForElement ($properties, $parents [$index], $units, false); 1371 $parents [$index]->setProperties ($properties); 1372 } 1373 if (!isset($properties ['calculated'])) { 1374 $this->calculate($properties, $parents [$index], $units); 1375 } 1376 } 1377 1378 // Calculate our own absolute values 1379 $this->calculate($dest, $element, $units); 1380 1381 // Inherit values from our parents 1382 $this->inherit($dest, $parents); 1383 } 1384 1385 /** 1386 * Return a string representation of all imported rules. 1387 * (String can be large) 1388 * 1389 * @return string 1390 */ 1391 public function rulesToString () { 1392 $returnString = ''; 1393 foreach ($this->rules as $rule) { 1394 $returnString .= $rule->toString (); 1395 } 1396 return $returnString; 1397 } 1398 1399 /** 1400 * The function strips the 'url(...)' part from an URL reference 1401 * and puts a $replacement path in front of the rest. 1402 * 1403 * @param string $URL Original URL reference 1404 * @param string $replacement Replacement path to set 1405 * @return string 1406 */ 1407 public static function replaceURLPrefix ($URL, $replacement) { 1408 if ( !empty ($URL) && !empty ($replacement) ) { 1409 // Replace 'url(...)' with $replacement 1410 $URL = substr ($URL, 3); 1411 $URL = trim ($URL, '()'); 1412 $URL = $replacement.$URL; 1413 } 1414 return $URL; 1415 } 1416 1417 /** 1418 * The function calls $callback for each imported property 1419 * containing a length value. The return value of $callback 1420 * is saved as the new property value. 1421 * 1422 * @param callable $callback 1423 */ 1424 public function adjustLengthValues ($callback) { 1425 foreach ($this->rules as $rule) { 1426 $rule->adjustLengthValues ($callback); 1427 } 1428 } 1429 1430 /** 1431 * The function calls $callback for each property imported 1432 * containing a URL reference. The return value of $callback 1433 * is saved as the new property value. 1434 * 1435 * @param callable $callback 1436 */ 1437 public function replaceURLPrefixes ($callback) { 1438 foreach ($this->rules as $rule) { 1439 $rule->replaceURLPrefixes ($callback); 1440 } 1441 } 1442} 1443