1<?php 2/** 3 * @package dompdf 4 * @link http://dompdf.github.com/ 5 * @author Benj Carson <benjcarson@digitaljunkies.ca> 6 * @author Helmut Tischer <htischer@weihenstephan.org> 7 * @author Fabien Ménager <fabien.menager@gmail.com> 8 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 9 */ 10namespace Dompdf\Css; 11 12use DOMElement; 13use DOMXPath; 14use Dompdf\Dompdf; 15use Dompdf\Helpers; 16use Dompdf\Exception; 17use Dompdf\FontMetrics; 18use Dompdf\Frame\FrameTree; 19 20/** 21 * The master stylesheet class 22 * 23 * The Stylesheet class is responsible for parsing stylesheets and style 24 * tags/attributes. It also acts as a registry of the individual Style 25 * objects generated by the current set of loaded CSS files and style 26 * elements. 27 * 28 * @see Style 29 * @package dompdf 30 */ 31class Stylesheet 32{ 33 /** 34 * The location of the default built-in CSS file. 35 */ 36 const DEFAULT_STYLESHEET = "/lib/res/html.css"; 37 38 /** 39 * User agent stylesheet origin 40 * 41 * @var int 42 */ 43 const ORIG_UA = 1; 44 45 /** 46 * User normal stylesheet origin 47 * 48 * @var int 49 */ 50 const ORIG_USER = 2; 51 52 /** 53 * Author normal stylesheet origin 54 * 55 * @var int 56 */ 57 const ORIG_AUTHOR = 3; 58 59 /* 60 * The highest possible specificity is 0x01000000 (and that is only for author 61 * stylesheets, as it is for inline styles). Origin precedence can be achieved by 62 * adding multiples of 0x10000000 to the actual specificity. Important 63 * declarations are handled in Style; though technically they should be handled 64 * here so that user important declarations can be made to take precedence over 65 * user important declarations, this doesn't matter in practice as Dompdf does 66 * not support user stylesheets, and user agent stylesheets can not include 67 * important declarations. 68 */ 69 private static $_stylesheet_origins = array( 70 self::ORIG_UA => 0x00000000, // user agent declarations 71 self::ORIG_USER => 0x10000000, // user normal declarations 72 self::ORIG_AUTHOR => 0x30000000, // author normal declarations 73 ); 74 75 /* 76 * Non-CSS presentational hints (i.e. HTML 4 attributes) are handled as if added 77 * to the beginning of an author stylesheet, i.e. anything in author stylesheets 78 * should override them. 79 */ 80 const SPEC_NON_CSS = 0x20000000; 81 82 /** 83 * Current dompdf instance 84 * 85 * @var Dompdf 86 */ 87 private $_dompdf; 88 89 /** 90 * Array of currently defined styles 91 * 92 * @var Style[] 93 */ 94 private $_styles; 95 96 /** 97 * Base protocol of the document being parsed 98 * Used to handle relative urls. 99 * 100 * @var string 101 */ 102 private $_protocol; 103 104 /** 105 * Base hostname of the document being parsed 106 * Used to handle relative urls. 107 * 108 * @var string 109 */ 110 private $_base_host; 111 112 /** 113 * Base path of the document being parsed 114 * Used to handle relative urls. 115 * 116 * @var string 117 */ 118 private $_base_path; 119 120 /** 121 * The styles defined by @page rules 122 * 123 * @var array<Style> 124 */ 125 private $_page_styles; 126 127 /** 128 * List of loaded files, used to prevent recursion 129 * 130 * @var array 131 */ 132 private $_loaded_files; 133 134 /** 135 * Current stylesheet origin 136 * 137 * @var int 138 */ 139 private $_current_origin = self::ORIG_UA; 140 141 /** 142 * Accepted CSS media types 143 * List of types and parsing rules for future extensions: 144 * http://www.w3.org/TR/REC-html40/types.html 145 * screen, tty, tv, projection, handheld, print, braille, aural, all 146 * The following are non standard extensions for undocumented specific environments. 147 * static, visual, bitmap, paged, dompdf 148 * Note, even though the generated pdf file is intended for print output, 149 * the desired content might be different (e.g. screen or projection view of html file). 150 * Therefore allow specification of content by dompdf setting Options::defaultMediaType. 151 * If given, replace media "print" by Options::defaultMediaType. 152 * (Previous version $ACCEPTED_MEDIA_TYPES = $ACCEPTED_GENERIC_MEDIA_TYPES + $ACCEPTED_DEFAULT_MEDIA_TYPE) 153 */ 154 static $ACCEPTED_DEFAULT_MEDIA_TYPE = "print"; 155 static $ACCEPTED_GENERIC_MEDIA_TYPES = array("all", "static", "visual", "bitmap", "paged", "dompdf"); 156 static $VALID_MEDIA_TYPES = array("all", "aural", "bitmap", "braille", "dompdf", "embossed", "handheld", "paged", "print", "projection", "screen", "speech", "static", "tty", "tv", "visual"); 157 158 /** 159 * @var FontMetrics 160 */ 161 private $fontMetrics; 162 163 /** 164 * The class constructor. 165 * 166 * The base protocol, host & path are initialized to those of 167 * the current script. 168 */ 169 function __construct(Dompdf $dompdf) 170 { 171 $this->_dompdf = $dompdf; 172 $this->setFontMetrics($dompdf->getFontMetrics()); 173 $this->_styles = array(); 174 $this->_loaded_files = array(); 175 $script = __FILE__; 176 if(isset($_SERVER["SCRIPT_FILENAME"])){ 177 $script = $_SERVER["SCRIPT_FILENAME"]; 178 } 179 list($this->_protocol, $this->_base_host, $this->_base_path) = Helpers::explode_url($script); 180 $this->_page_styles = array("base" => new Style($this)); 181 } 182 183 /** 184 * Set the base protocol 185 * 186 * @param string $protocol 187 */ 188 function set_protocol($protocol) 189 { 190 $this->_protocol = $protocol; 191 } 192 193 /** 194 * Set the base host 195 * 196 * @param string $host 197 */ 198 function set_host($host) 199 { 200 $this->_base_host = $host; 201 } 202 203 /** 204 * Set the base path 205 * 206 * @param string $path 207 */ 208 function set_base_path($path) 209 { 210 $this->_base_path = $path; 211 } 212 213 /** 214 * Return the Dompdf object 215 * 216 * @return Dompdf 217 */ 218 function get_dompdf() 219 { 220 return $this->_dompdf; 221 } 222 223 /** 224 * Return the base protocol for this stylesheet 225 * 226 * @return string 227 */ 228 function get_protocol() 229 { 230 return $this->_protocol; 231 } 232 233 /** 234 * Return the base host for this stylesheet 235 * 236 * @return string 237 */ 238 function get_host() 239 { 240 return $this->_base_host; 241 } 242 243 /** 244 * Return the base path for this stylesheet 245 * 246 * @return string 247 */ 248 function get_base_path() 249 { 250 return $this->_base_path; 251 } 252 253 /** 254 * Return the array of page styles 255 * 256 * @return Style[] 257 */ 258 function get_page_styles() 259 { 260 return $this->_page_styles; 261 } 262 263 /** 264 * Add a new Style object to the stylesheet 265 * add_style() adds a new Style object to the current stylesheet, or 266 * merges a new Style with an existing one. 267 * 268 * @param string $key the Style's selector 269 * @param Style $style the Style to be added 270 * 271 * @throws \Dompdf\Exception 272 */ 273 function add_style($key, Style $style) 274 { 275 if (!is_string($key)) { 276 throw new Exception("CSS rule must be keyed by a string."); 277 } 278 279 if (!isset($this->_styles[$key])) { 280 $this->_styles[$key] = array(); 281 } 282 $new_style = clone $style; 283 $new_style->set_origin($this->_current_origin); 284 $this->_styles[$key][] = $new_style; 285 } 286 287 /** 288 * lookup a specific Style collection 289 * 290 * lookup() returns the Style collection specified by $key, or null if the Style is 291 * not found. 292 * 293 * @param string $key the selector of the requested Style 294 * @return Style 295 * 296 * @Fixme _styles is a two dimensional array. It should produce wrong results 297 */ 298 function lookup($key) 299 { 300 if (!isset($this->_styles[$key])) { 301 return null; 302 } 303 304 return $this->_styles[$key]; 305 } 306 307 /** 308 * create a new Style object associated with this stylesheet 309 * 310 * @param Style $parent The style of this style's parent in the DOM tree 311 * @return Style 312 */ 313 function create_style(Style $parent = null) 314 { 315 return new Style($this, $this->_current_origin); 316 } 317 318 /** 319 * load and parse a CSS string 320 * 321 * @param string $css 322 * @param int $origin 323 */ 324 function load_css(&$css, $origin = self::ORIG_AUTHOR) 325 { 326 if ($origin) { 327 $this->_current_origin = $origin; 328 } 329 $this->_parse_css($css); 330 } 331 332 333 /** 334 * load and parse a CSS file 335 * 336 * @param string $file 337 * @param int $origin 338 */ 339 function load_css_file($file, $origin = self::ORIG_AUTHOR) 340 { 341 if ($origin) { 342 $this->_current_origin = $origin; 343 } 344 345 // Prevent circular references 346 if (isset($this->_loaded_files[$file])) { 347 return; 348 } 349 350 $this->_loaded_files[$file] = true; 351 352 if (strpos($file, "data:") === 0) { 353 $parsed = Helpers::parse_data_uri($file); 354 $css = $parsed["data"]; 355 } else { 356 $parsed_url = Helpers::explode_url($file); 357 358 list($this->_protocol, $this->_base_host, $this->_base_path, $filename) = $parsed_url; 359 360 // Fix submitted by Nick Oostveen for aliased directory support: 361 if ($this->_protocol == "") { 362 $file = $this->_base_path . $filename; 363 } else { 364 $file = Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $filename); 365 } 366 367 list($css, $http_response_header) = Helpers::getFileContent($file, $this->_dompdf->getHttpContext()); 368 369 $good_mime_type = true; 370 371 // See http://the-stickman.com/web-development/php/getting-http-response-headers-when-using-file_get_contents/ 372 if (isset($http_response_header) && !$this->_dompdf->getQuirksmode()) { 373 foreach ($http_response_header as $_header) { 374 if (preg_match("@Content-Type:\s*([\w/]+)@i", $_header, $matches) && 375 ($matches[1] !== "text/css") 376 ) { 377 $good_mime_type = false; 378 } 379 } 380 } 381 382 if (!$good_mime_type || $css == "") { 383 Helpers::record_warnings(E_USER_WARNING, "Unable to load css file $file", __FILE__, __LINE__); 384 return; 385 } 386 } 387 388 $this->_parse_css($css); 389 } 390 391 /** 392 * @link http://www.w3.org/TR/CSS21/cascade.html#specificity 393 * 394 * @param string $selector 395 * @param int $origin : 396 * - Stylesheet::ORIG_UA: user agent style sheet 397 * - Stylesheet::ORIG_USER: user style sheet 398 * - Stylesheet::ORIG_AUTHOR: author style sheet 399 * 400 * @return int 401 */ 402 private function _specificity($selector, $origin = self::ORIG_AUTHOR) 403 { 404 // http://www.w3.org/TR/CSS21/cascade.html#specificity 405 // ignoring the ":" pseudoclass modifiers 406 // also ignored in _css_selector_to_xpath 407 408 $a = ($selector === "!attr") ? 1 : 0; 409 410 $b = min(mb_substr_count($selector, "#"), 255); 411 412 $c = min(mb_substr_count($selector, ".") + 413 mb_substr_count($selector, "["), 255); 414 415 $d = min(mb_substr_count($selector, " ") + 416 mb_substr_count($selector, ">") + 417 mb_substr_count($selector, "+"), 255); 418 419 //If a normal element name is at the beginning of the string, 420 //a leading whitespace might have been removed on whitespace collapsing and removal 421 //therefore there might be one whitespace less as selected element names 422 //this can lead to a too small specificity 423 //see _css_selector_to_xpath 424 425 if (!in_array($selector[0], array(" ", ">", ".", "#", "+", ":", "[")) && $selector !== "*") { 426 $d++; 427 } 428 429 if ($this->_dompdf->getOptions()->getDebugCss()) { 430 /*DEBUGCSS*/ 431 print "<pre>\n"; 432 /*DEBUGCSS*/ 433 printf("_specificity(): 0x%08x \"%s\"\n", self::$_stylesheet_origins[$origin] + (($a << 24) | ($b << 16) | ($c << 8) | ($d)), $selector); 434 /*DEBUGCSS*/ 435 print "</pre>"; 436 } 437 438 return self::$_stylesheet_origins[$origin] + (($a << 24) | ($b << 16) | ($c << 8) | ($d)); 439 } 440 441 /** 442 * Converts a CSS selector to an XPath query. 443 * 444 * @param string $selector 445 * @param bool $first_pass 446 * 447 * @throws Exception 448 * @return array 449 */ 450 private function _css_selector_to_xpath($selector, $first_pass = false) 451 { 452 453 // Collapse white space and strip whitespace around delimiters 454 //$search = array("/\\s+/", "/\\s+([.>#+:])\\s+/"); 455 //$replace = array(" ", "\\1"); 456 //$selector = preg_replace($search, $replace, trim($selector)); 457 458 // Initial query (non-absolute) 459 $query = "//"; 460 461 // Will contain :before and :after 462 $pseudo_elements = array(); 463 464 // Will contain :link, etc 465 $pseudo_classes = array(); 466 467 // Parse the selector 468 //$s = preg_split("/([ :>.#+])/", $selector, -1, PREG_SPLIT_DELIM_CAPTURE); 469 470 $delimiters = array(" ", ">", ".", "#", "+", ":", "[", "("); 471 472 // Add an implicit * at the beginning of the selector 473 // if it begins with an attribute selector 474 if ($selector[0] === "[") { 475 $selector = "*$selector"; 476 } 477 478 // Add an implicit space at the beginning of the selector if there is no 479 // delimiter there already. 480 if (!in_array($selector[0], $delimiters)) { 481 $selector = " $selector"; 482 } 483 484 $tok = ""; 485 $len = mb_strlen($selector); 486 $i = 0; 487 488 while ($i < $len) { 489 490 $s = $selector[$i]; 491 $i++; 492 493 // Eat characters up to the next delimiter 494 $tok = ""; 495 $in_attr = false; 496 $in_func = false; 497 498 while ($i < $len) { 499 $c = $selector[$i]; 500 $c_prev = $selector[$i - 1]; 501 502 if (!$in_func && !$in_attr && in_array($c, $delimiters) && !(($c == $c_prev) == ":")) { 503 break; 504 } 505 506 if ($c_prev === "[") { 507 $in_attr = true; 508 } 509 if ($c_prev === "(") { 510 $in_func = true; 511 } 512 513 $tok .= $selector[$i++]; 514 515 if ($in_attr && $c === "]") { 516 $in_attr = false; 517 break; 518 } 519 if ($in_func && $c === ")") { 520 $in_func = false; 521 break; 522 } 523 } 524 525 switch ($s) { 526 527 case " ": 528 case ">": 529 // All elements matching the next token that are direct children of 530 // the current token 531 $expr = $s === " " ? "descendant" : "child"; 532 533 if (mb_substr($query, -1, 1) !== "/") { 534 $query .= "/"; 535 } 536 537 // Tag names are case-insensitive 538 $tok = strtolower($tok); 539 540 if (!$tok) { 541 $tok = "*"; 542 } 543 544 $query .= "$expr::$tok"; 545 $tok = ""; 546 break; 547 548 case ".": 549 case "#": 550 // All elements matching the current token with a class/id equal to 551 // the _next_ token. 552 553 $attr = $s === "." ? "class" : "id"; 554 555 // empty class/id == * 556 if (mb_substr($query, -1, 1) === "/") { 557 $query .= "*"; 558 } 559 560 // Match multiple classes: $tok contains the current selected 561 // class. Search for class attributes with class="$tok", 562 // class=".* $tok .*" and class=".* $tok" 563 564 // This doesn't work because libxml only supports XPath 1.0... 565 //$query .= "[matches(@$attr,\"^${tok}\$|^${tok}[ ]+|[ ]+${tok}\$|[ ]+${tok}[ ]+\")]"; 566 567 // Query improvement by Michael Sheakoski <michael@mjsdigital.com>: 568 $query .= "[contains(concat(' ', @$attr, ' '), concat(' ', '$tok', ' '))]"; 569 $tok = ""; 570 break; 571 572 case "+": 573 // All sibling elements that follow the current token 574 if (mb_substr($query, -1, 1) !== "/") { 575 $query .= "/"; 576 } 577 578 $query .= "following-sibling::$tok"; 579 $tok = ""; 580 break; 581 582 case ":": 583 $i2 = $i - strlen($tok) - 2; // the char before ":" 584 if (($i2 < 0 || !isset($selector[$i2]) || (in_array($selector[$i2], $delimiters) && $selector[$i2] != ":")) && substr($query, -1) != "*") { 585 $query .= "*"; 586 } 587 588 $last = false; 589 590 // Pseudo-classes 591 switch ($tok) { 592 593 case "first-child": 594 $query .= "[1]"; 595 $tok = ""; 596 break; 597 598 case "last-child": 599 $query .= "[not(following-sibling::*)]"; 600 $tok = ""; 601 break; 602 603 case "first-of-type": 604 $query .= "[position() = 1]"; 605 $tok = ""; 606 break; 607 608 case "last-of-type": 609 $query .= "[position() = last()]"; 610 $tok = ""; 611 break; 612 613 // an+b, n, odd, and even 614 /** @noinspection PhpMissingBreakStatementInspection */ 615 case "nth-last-of-type": 616 $last = true; 617 case "nth-of-type": 618 //FIXME: this fix-up is pretty ugly, would parsing the selector in reverse work better generally? 619 $descendant_delimeter = strrpos($query, "::"); 620 $isChild = substr($query, $descendant_delimeter-5, 5) == "child"; 621 $el = substr($query, $descendant_delimeter+2); 622 $query = substr($query, 0, strrpos($query, "/")) . ($isChild ? "/" : "//") . $el; 623 624 $pseudo_classes[$tok] = true; 625 $p = $i + 1; 626 $nth = trim(mb_substr($selector, $p, strpos($selector, ")", $i) - $p)); 627 628 // 1 629 if (preg_match("/^\d+$/", $nth)) { 630 $condition = "position() = $nth"; 631 } // odd 632 elseif ($nth === "odd") { 633 $condition = "(position() mod 2) = 1"; 634 } // even 635 elseif ($nth === "even") { 636 $condition = "(position() mod 2) = 0"; 637 } // an+b 638 else { 639 $condition = $this->_selector_an_plus_b($nth, $last); 640 } 641 642 $query .= "[$condition]"; 643 $tok = ""; 644 break; 645 /** @noinspection PhpMissingBreakStatementInspection */ 646 case "nth-last-child": 647 $last = true; 648 case "nth-child": 649 //FIXME: this fix-up is pretty ugly, would parsing the selector in reverse work better generally? 650 $descendant_delimeter = strrpos($query, "::"); 651 $isChild = substr($query, $descendant_delimeter-5, 5) == "child"; 652 $el = substr($query, $descendant_delimeter+2); 653 $query = substr($query, 0, strrpos($query, "/")) . ($isChild ? "/" : "//") . "*"; 654 655 $pseudo_classes[$tok] = true; 656 $p = $i + 1; 657 $nth = trim(mb_substr($selector, $p, strpos($selector, ")", $i) - $p)); 658 659 // 1 660 if (preg_match("/^\d+$/", $nth)) { 661 $condition = "position() = $nth"; 662 } // odd 663 elseif ($nth === "odd") { 664 $condition = "(position() mod 2) = 1"; 665 } // even 666 elseif ($nth === "even") { 667 $condition = "(position() mod 2) = 0"; 668 } // an+b 669 else { 670 $condition = $this->_selector_an_plus_b($nth, $last); 671 } 672 673 $query .= "[$condition]"; 674 if ($el != "*") { 675 $query .= "[name() = '$el']"; 676 } 677 $tok = ""; 678 break; 679 680 //TODO: bit of a hack attempt at matches support, currently only matches against elements 681 case "matches": 682 $pseudo_classes[$tok] = true; 683 $p = $i + 1; 684 $matchList = trim(mb_substr($selector, $p, strpos($selector, ")", $i) - $p)); 685 686 // Tag names are case-insensitive 687 $elements = array_map("trim", explode(",", strtolower($matchList))); 688 foreach ($elements as &$element) { 689 $element = "name() = '$element'"; 690 } 691 692 $query .= "[" . implode(" or ", $elements) . "]"; 693 $tok = ""; 694 break; 695 696 case "link": 697 $query .= "[@href]"; 698 $tok = ""; 699 break; 700 701 case "first-line": 702 case ":first-line": 703 case "first-letter": 704 case ":first-letter": 705 // TODO 706 $el = trim($tok, ":"); 707 $pseudo_elements[$el] = true; 708 break; 709 710 // N/A 711 case "focus": 712 case "active": 713 case "hover": 714 case "visited": 715 $query .= "[false()]"; 716 $tok = ""; 717 break; 718 719 /* Pseudo-elements */ 720 case "before": 721 case ":before": 722 case "after": 723 case ":after": 724 $pos = trim($tok, ":"); 725 $pseudo_elements[$pos] = true; 726 if (!$first_pass) { 727 $query .= "/*[@$pos]"; 728 } 729 730 $tok = ""; 731 break; 732 733 case "empty": 734 $query .= "[not(*) and not(normalize-space())]"; 735 $tok = ""; 736 break; 737 738 case "disabled": 739 case "checked": 740 $query .= "[@$tok]"; 741 $tok = ""; 742 break; 743 744 case "enabled": 745 $query .= "[not(@disabled)]"; 746 $tok = ""; 747 break; 748 749 // the selector is not handled, until we support all possible selectors force an empty set (silent failure) 750 default: 751 $query = "/../.."; // go up two levels because generated content starts on the body element 752 $tok = ""; 753 break; 754 } 755 756 break; 757 758 case "[": 759 // Attribute selectors. All with an attribute matching the following token(s) 760 $attr_delimiters = array("=", "]", "~", "|", "$", "^", "*"); 761 $tok_len = mb_strlen($tok); 762 $j = 0; 763 764 $attr = ""; 765 $op = ""; 766 $value = ""; 767 768 while ($j < $tok_len) { 769 if (in_array($tok[$j], $attr_delimiters)) { 770 break; 771 } 772 $attr .= $tok[$j++]; 773 } 774 775 switch ($tok[$j]) { 776 777 case "~": 778 case "|": 779 case "$": 780 case "^": 781 case "*": 782 $op .= $tok[$j++]; 783 784 if ($tok[$j] !== "=") { 785 throw new Exception("Invalid CSS selector syntax: invalid attribute selector: $selector"); 786 } 787 788 $op .= $tok[$j]; 789 break; 790 791 case "=": 792 $op = "="; 793 break; 794 795 } 796 797 // Read the attribute value, if required 798 if ($op != "") { 799 $j++; 800 while ($j < $tok_len) { 801 if ($tok[$j] === "]") { 802 break; 803 } 804 $value .= $tok[$j++]; 805 } 806 } 807 808 if ($attr == "") { 809 throw new Exception("Invalid CSS selector syntax: missing attribute name"); 810 } 811 812 $value = trim($value, "\"'"); 813 814 switch ($op) { 815 816 case "": 817 $query .= "[@$attr]"; 818 break; 819 820 case "=": 821 $query .= "[@$attr=\"$value\"]"; 822 break; 823 824 case "~=": 825 // FIXME: this will break if $value contains quoted strings 826 // (e.g. [type~="a b c" "d e f"]) 827 $values = explode(" ", $value); 828 $query .= "["; 829 830 foreach ($values as $val) { 831 $query .= "@$attr=\"$val\" or "; 832 } 833 834 $query = rtrim($query, " or ") . "]"; 835 break; 836 837 case "|=": 838 $values = explode("-", $value); 839 $query .= "["; 840 841 foreach ($values as $val) { 842 $query .= "starts-with(@$attr, \"$val\") or "; 843 } 844 845 $query = rtrim($query, " or ") . "]"; 846 break; 847 848 case "$=": 849 $query .= "[substring(@$attr, string-length(@$attr)-" . (strlen($value) - 1) . ")=\"$value\"]"; 850 break; 851 852 case "^=": 853 $query .= "[starts-with(@$attr,\"$value\")]"; 854 break; 855 856 case "*=": 857 $query .= "[contains(@$attr,\"$value\")]"; 858 break; 859 } 860 861 break; 862 } 863 } 864 $i++; 865 866// case ":": 867// // Pseudo selectors: ignore for now. Partially handled directly 868// // below. 869 870// // Skip until the next special character, leaving the token as-is 871// while ( $i < $len ) { 872// if ( in_array($selector[$i], $delimiters) ) 873// break; 874// $i++; 875// } 876// break; 877 878// default: 879// // Add the character to the token 880// $tok .= $selector[$i++]; 881// break; 882// } 883 884// } 885 886 887 // Trim the trailing '/' from the query 888 if (mb_strlen($query) > 2) { 889 $query = rtrim($query, "/"); 890 } 891 892 return array("query" => $query, "pseudo_elements" => $pseudo_elements); 893 } 894 895 /** 896 * https://github.com/tenderlove/nokogiri/blob/master/lib/nokogiri/css/xpath_visitor.rb 897 * 898 * @param $expr 899 * @param bool $last 900 * @return string 901 */ 902 protected function _selector_an_plus_b($expr, $last = false) 903 { 904 $expr = preg_replace("/\s/", "", $expr); 905 if (!preg_match("/^(?P<a>-?[0-9]*)?n(?P<b>[-+]?[0-9]+)?$/", $expr, $matches)) { 906 return "false()"; 907 } 908 909 $a = ((isset($matches["a"]) && $matches["a"] !== "") ? intval($matches["a"]) : 1); 910 $b = ((isset($matches["b"]) && $matches["b"] !== "") ? intval($matches["b"]) : 0); 911 912 $position = ($last ? "(last()-position()+1)" : "position()"); 913 914 if ($b == 0) { 915 return "($position mod $a) = 0"; 916 } else { 917 $compare = (($a < 0) ? "<=" : ">="); 918 $b2 = -$b; 919 if ($b2 >= 0) { 920 $b2 = "+$b2"; 921 } 922 return "($position $compare $b) and ((($position $b2) mod " . abs($a) . ") = 0)"; 923 } 924 } 925 926 /** 927 * applies all current styles to a particular document tree 928 * 929 * apply_styles() applies all currently loaded styles to the provided 930 * {@link FrameTree}. Aside from parsing CSS, this is the main purpose 931 * of this class. 932 * 933 * @param \Dompdf\Frame\FrameTree $tree 934 */ 935 function apply_styles(FrameTree $tree) 936 { 937 // Use XPath to select nodes. This would be easier if we could attach 938 // Frame objects directly to DOMNodes using the setUserData() method, but 939 // we can't do that just yet. Instead, we set a _node attribute_ in 940 // Frame->set_id() and use that as a handle on the Frame object via 941 // FrameTree::$_registry. 942 943 // We create a scratch array of styles indexed by frame id. Once all 944 // styles have been assigned, we order the cached styles by specificity 945 // and create a final style object to assign to the frame. 946 947 // FIXME: this is not particularly robust... 948 949 $styles = array(); 950 $xp = new DOMXPath($tree->get_dom()); 951 $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss(); 952 953 // Add generated content 954 foreach ($this->_styles as $selector => $selector_styles) { 955 /** @var Style $style */ 956 foreach ($selector_styles as $style) { 957 if (strpos($selector, ":before") === false && strpos($selector, ":after") === false) { 958 continue; 959 } 960 961 $query = $this->_css_selector_to_xpath($selector, true); 962 963 // Retrieve the nodes, limit to body for generated content 964 //TODO: If we use a context node can we remove the leading dot? 965 $nodes = @$xp->query('.' . $query["query"]); 966 if ($nodes == null) { 967 Helpers::record_warnings(E_USER_WARNING, "The CSS selector '$selector' is not valid", __FILE__, __LINE__); 968 continue; 969 } 970 971 /** @var \DOMElement $node */ 972 foreach ($nodes as $node) { 973 // Only DOMElements get styles 974 if ($node->nodeType != XML_ELEMENT_NODE) { 975 continue; 976 } 977 978 foreach (array_keys($query["pseudo_elements"], true, true) as $pos) { 979 // Do not add a new pseudo element if another one already matched 980 if ($node->hasAttribute("dompdf_{$pos}_frame_id")) { 981 continue; 982 } 983 984 if (($src = $this->_image($style->get_prop('content'))) !== "none") { 985 $new_node = $node->ownerDocument->createElement("img_generated"); 986 $new_node->setAttribute("src", $src); 987 } else { 988 $new_node = $node->ownerDocument->createElement("dompdf_generated"); 989 } 990 991 $new_node->setAttribute($pos, $pos); 992 $new_frame_id = $tree->insert_node($node, $new_node, $pos); 993 $node->setAttribute("dompdf_{$pos}_frame_id", $new_frame_id); 994 } 995 } 996 } 997 } 998 999 // Apply all styles in stylesheet 1000 foreach ($this->_styles as $selector => $selector_styles) { 1001 /** @var Style $style */ 1002 foreach ($selector_styles as $style) { 1003 $query = $this->_css_selector_to_xpath($selector); 1004 1005 // Retrieve the nodes 1006 $nodes = @$xp->query($query["query"]); 1007 if ($nodes == null) { 1008 Helpers::record_warnings(E_USER_WARNING, "The CSS selector '$selector' is not valid", __FILE__, __LINE__); 1009 continue; 1010 } 1011 1012 $spec = $this->_specificity($selector, $style->get_origin()); 1013 1014 foreach ($nodes as $node) { 1015 // Retrieve the node id 1016 // Only DOMElements get styles 1017 if ($node->nodeType != XML_ELEMENT_NODE) { 1018 continue; 1019 } 1020 1021 $id = $node->getAttribute("frame_id"); 1022 1023 // Assign the current style to the scratch array 1024 $styles[$id][$spec][] = $style; 1025 } 1026 } 1027 } 1028 1029 // Set the page width, height, and orientation based on the canvas paper size 1030 $canvas = $this->_dompdf->getCanvas(); 1031 $paper_width = $canvas->get_width(); 1032 $paper_height = $canvas->get_height(); 1033 $paper_orientation = ($paper_width > $paper_height ? "landscape" : "portrait"); 1034 1035 if ($this->_page_styles["base"] && is_array($this->_page_styles["base"]->size)) { 1036 $paper_width = $this->_page_styles['base']->size[0]; 1037 $paper_height = $this->_page_styles['base']->size[1]; 1038 $paper_orientation = ($paper_width > $paper_height ? "landscape" : "portrait"); 1039 } 1040 1041 // Now create the styles and assign them to the appropriate frames. (We 1042 // iterate over the tree using an implicit FrameTree iterator.) 1043 $root_flg = false; 1044 foreach ($tree->get_frames() as $frame) { 1045 // Helpers::pre_r($frame->get_node()->nodeName . ":"); 1046 if (!$root_flg && $this->_page_styles["base"]) { 1047 $style = $this->_page_styles["base"]; 1048 } else { 1049 $style = $this->create_style(); 1050 } 1051 1052 // Find nearest DOMElement parent 1053 $p = $frame; 1054 while ($p = $p->get_parent()) { 1055 if ($p->get_node()->nodeType == XML_ELEMENT_NODE) { 1056 break; 1057 } 1058 } 1059 1060 // Styles can only be applied directly to DOMElements; anonymous 1061 // frames inherit from their parent 1062 if ($frame->get_node()->nodeType != XML_ELEMENT_NODE) { 1063 if ($p) { 1064 $style->inherit($p->get_style()); 1065 } 1066 1067 $frame->set_style($style); 1068 continue; 1069 } 1070 1071 $id = $frame->get_id(); 1072 1073 // Handle HTML 4.0 attributes 1074 AttributeTranslator::translate_attributes($frame); 1075 if (($str = $frame->get_node()->getAttribute(AttributeTranslator::$_style_attr)) !== "") { 1076 $styles[$id][self::SPEC_NON_CSS][] = $this->_parse_properties($str); 1077 } 1078 1079 // Locate any additional style attributes 1080 if (($str = $frame->get_node()->getAttribute("style")) !== "") { 1081 // Destroy CSS comments 1082 $str = preg_replace("'/\*.*?\*/'si", "", $str); 1083 1084 $spec = $this->_specificity("!attr", self::ORIG_AUTHOR); 1085 $styles[$id][$spec][] = $this->_parse_properties($str); 1086 } 1087 1088 // Grab the applicable styles 1089 if (isset($styles[$id])) { 1090 1091 /** @var array[][] $applied_styles */ 1092 $applied_styles = $styles[$frame->get_id()]; 1093 1094 // Sort by specificity 1095 ksort($applied_styles); 1096 1097 if ($DEBUGCSS) { 1098 $debug_nodename = $frame->get_node()->nodeName; 1099 print "<pre>\n[$debug_nodename\n"; 1100 foreach ($applied_styles as $spec => $arr) { 1101 printf("specificity: 0x%08x\n", $spec); 1102 /** @var Style $s */ 1103 foreach ($arr as $s) { 1104 print "[\n"; 1105 $s->debug_print(); 1106 print "]\n"; 1107 } 1108 } 1109 } 1110 1111 // Merge the new styles with the inherited styles 1112 $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES; 1113 $acceptedmedia[] = $this->_dompdf->getOptions()->getDefaultMediaType(); 1114 foreach ($applied_styles as $arr) { 1115 /** @var Style $s */ 1116 foreach ($arr as $s) { 1117 $media_queries = $s->get_media_queries(); 1118 foreach ($media_queries as $media_query) { 1119 list($media_query_feature, $media_query_value) = $media_query; 1120 // if any of the Style's media queries fail then do not apply the style 1121 //TODO: When the media query logic is fully developed we should not apply the Style when any of the media queries fail or are bad, per https://www.w3.org/TR/css3-mediaqueries/#error-handling 1122 if (in_array($media_query_feature, self::$VALID_MEDIA_TYPES)) { 1123 if ((strlen($media_query_feature) === 0 && !in_array($media_query, $acceptedmedia)) || (in_array($media_query, $acceptedmedia) && $media_query_value == "not")) { 1124 continue (3); 1125 } 1126 } else { 1127 switch ($media_query_feature) { 1128 case "height": 1129 if ($paper_height !== (float)$style->length_in_pt($media_query_value)) { 1130 continue (3); 1131 } 1132 break; 1133 case "min-height": 1134 if ($paper_height < (float)$style->length_in_pt($media_query_value)) { 1135 continue (3); 1136 } 1137 break; 1138 case "max-height": 1139 if ($paper_height > (float)$style->length_in_pt($media_query_value)) { 1140 continue (3); 1141 } 1142 break; 1143 case "width": 1144 if ($paper_width !== (float)$style->length_in_pt($media_query_value)) { 1145 continue (3); 1146 } 1147 break; 1148 case "min-width": 1149 //if (min($paper_width, $media_query_width) === $paper_width) { 1150 if ($paper_width < (float)$style->length_in_pt($media_query_value)) { 1151 continue (3); 1152 } 1153 break; 1154 case "max-width": 1155 //if (max($paper_width, $media_query_width) === $paper_width) { 1156 if ($paper_width > (float)$style->length_in_pt($media_query_value)) { 1157 continue (3); 1158 } 1159 break; 1160 case "orientation": 1161 if ($paper_orientation !== $media_query_value) { 1162 continue (3); 1163 } 1164 break; 1165 default: 1166 Helpers::record_warnings(E_USER_WARNING, "Unknown media query: $media_query_feature", __FILE__, __LINE__); 1167 break; 1168 } 1169 } 1170 } 1171 1172 $style->merge($s); 1173 } 1174 } 1175 } 1176 1177 // Inherit parent's styles if required 1178 if ($p) { 1179 1180 if ($DEBUGCSS) { 1181 print "inherit:\n"; 1182 print "[\n"; 1183 $p->get_style()->debug_print(); 1184 print "]\n"; 1185 } 1186 1187 $style->inherit($p->get_style()); 1188 } 1189 1190 if ($DEBUGCSS) { 1191 print "DomElementStyle:\n"; 1192 print "[\n"; 1193 $style->debug_print(); 1194 print "]\n"; 1195 print "/$debug_nodename]\n</pre>"; 1196 } 1197 1198 /*DEBUGCSS print: see below different print debugging method 1199 Helpers::pre_r($frame->get_node()->nodeName . ":"); 1200 echo "<pre>"; 1201 echo $style; 1202 echo "</pre>";*/ 1203 $frame->set_style($style); 1204 1205 if (!$root_flg && $this->_page_styles["base"]) { 1206 $root_flg = true; 1207 1208 // set the page width, height, and orientation based on the parsed page style 1209 if ($style->size !== "auto") { 1210 list($paper_width, $paper_height) = $style->size; 1211 } 1212 $paper_width = $paper_width - (float)$style->length_in_pt($style->margin_left) - (float)$style->length_in_pt($style->margin_right); 1213 $paper_height = $paper_height - (float)$style->length_in_pt($style->margin_top) - (float)$style->length_in_pt($style->margin_bottom); 1214 $paper_orientation = ($paper_width > $paper_height ? "landscape" : "portrait"); 1215 } 1216 } 1217 1218 // We're done! Clean out the registry of all styles since we 1219 // won't be needing this later. 1220 foreach (array_keys($this->_styles) as $key) { 1221 $this->_styles[$key] = null; 1222 unset($this->_styles[$key]); 1223 } 1224 1225 } 1226 1227 /** 1228 * parse a CSS string using a regex parser 1229 * Called by {@link Stylesheet::parse_css()} 1230 * 1231 * @param string $str 1232 * 1233 * @throws Exception 1234 */ 1235 private function _parse_css($str) 1236 { 1237 1238 $str = trim($str); 1239 1240 // Destroy comments and remove HTML comments 1241 $css = preg_replace(array( 1242 "'/\*.*?\*/'si", 1243 "/^<!--/", 1244 "/-->$/" 1245 ), "", $str); 1246 1247 // FIXME: handle '{' within strings, e.g. [attr="string {}"] 1248 1249 // Something more legible: 1250 $re = 1251 "/\s* # Skip leading whitespace \n" . 1252 "( @([^\s{]+)\s*([^{;]*) (?:;|({)) )? # Match @rules followed by ';' or '{' \n" . 1253 "(?(1) # Only parse sub-sections if we're in an @rule... \n" . 1254 " (?(4) # ...and if there was a leading '{' \n" . 1255 " \s*( (?:(?>[^{}]+) ({)? # Parse rulesets and individual @page rules \n" . 1256 " (?(6) (?>[^}]*) }) \s*)+? \n" . 1257 " ) \n" . 1258 " }) # Balancing '}' \n" . 1259 "| # Branch to match regular rules (not preceded by '@') \n" . 1260 "([^{]*{[^}]*})) # Parse normal rulesets \n" . 1261 "/xs"; 1262 1263 if (preg_match_all($re, $css, $matches, PREG_SET_ORDER) === false) { 1264 // An error occurred 1265 throw new Exception("Error parsing css file: preg_match_all() failed."); 1266 } 1267 1268 // After matching, the array indices are set as follows: 1269 // 1270 // [0] => complete text of match 1271 // [1] => contains '@import ...;' or '@media {' if applicable 1272 // [2] => text following @ for cases where [1] is set 1273 // [3] => media types or full text following '@import ...;' 1274 // [4] => '{', if present 1275 // [5] => rulesets within media rules 1276 // [6] => '{', within media rules 1277 // [7] => individual rules, outside of media rules 1278 // 1279 1280 $media_query_regex = "/(?:((only|not)?\s*(" . implode("|", self::$VALID_MEDIA_TYPES) . "))|(\s*\(\s*((?:(min|max)-)?([\w\-]+))\s*(?:\:\s*(.*?)\s*)?\)))/isx"; 1281 1282 //Helpers::pre_r($matches); 1283 foreach ($matches as $match) { 1284 $match[2] = trim($match[2]); 1285 1286 if ($match[2] !== "") { 1287 // Handle @rules 1288 switch ($match[2]) { 1289 1290 case "import": 1291 $this->_parse_import($match[3]); 1292 break; 1293 1294 case "media": 1295 $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES; 1296 $acceptedmedia[] = $this->_dompdf->getOptions()->getDefaultMediaType(); 1297 1298 $media_queries = preg_split("/\s*,\s*/", mb_strtolower(trim($match[3]))); 1299 foreach ($media_queries as $media_query) { 1300 if (in_array($media_query, $acceptedmedia)) { 1301 //if we have a media type match go ahead and parse the stylesheet 1302 $this->_parse_sections($match[5]); 1303 break; 1304 } elseif (!in_array($media_query, self::$VALID_MEDIA_TYPES)) { 1305 // otherwise conditionally parse the stylesheet assuming there are parseable media queries 1306 if (preg_match_all($media_query_regex, $media_query, $media_query_matches, PREG_SET_ORDER) !== false) { 1307 $mq = array(); 1308 foreach ($media_query_matches as $media_query_match) { 1309 if (empty($media_query_match[1]) === false) { 1310 $media_query_feature = strtolower($media_query_match[3]); 1311 $media_query_value = strtolower($media_query_match[2]); 1312 $mq[] = array($media_query_feature, $media_query_value); 1313 } else if (empty($media_query_match[4]) === false) { 1314 $media_query_feature = strtolower($media_query_match[5]); 1315 $media_query_value = (array_key_exists(8, $media_query_match) ? strtolower($media_query_match[8]) : null); 1316 $mq[] = array($media_query_feature, $media_query_value); 1317 } 1318 } 1319 $this->_parse_sections($match[5], $mq); 1320 break; 1321 } 1322 } 1323 } 1324 break; 1325 1326 case "page": 1327 //This handles @page to be applied to page oriented media 1328 //Note: This has a reduced syntax: 1329 //@page { margin:1cm; color:blue; } 1330 //Not a sequence of styles like a full.css, but only the properties 1331 //of a single style, which is applied to the very first "root" frame before 1332 //processing other styles of the frame. 1333 //Working properties: 1334 // margin (for margin around edge of paper) 1335 // font-family (default font of pages) 1336 // color (default text color of pages) 1337 //Non working properties: 1338 // border 1339 // padding 1340 // background-color 1341 //Todo:Reason is unknown 1342 //Other properties (like further font or border attributes) not tested. 1343 //If a border or background color around each paper sheet is desired, 1344 //assign it to the <body> tag, possibly only for the css of the correct media type. 1345 1346 // If the page has a name, skip the style. 1347 $page_selector = trim($match[3]); 1348 1349 $key = null; 1350 switch ($page_selector) { 1351 case "": 1352 $key = "base"; 1353 break; 1354 1355 case ":left": 1356 case ":right": 1357 case ":odd": 1358 case ":even": 1359 /** @noinspection PhpMissingBreakStatementInspection */ 1360 case ":first": 1361 $key = $page_selector; 1362 1363 default: 1364 break 2; 1365 } 1366 1367 // Store the style for later... 1368 if (empty($this->_page_styles[$key])) { 1369 $this->_page_styles[$key] = $this->_parse_properties($match[5]); 1370 } else { 1371 $this->_page_styles[$key]->merge($this->_parse_properties($match[5])); 1372 } 1373 break; 1374 1375 case "font-face": 1376 $this->_parse_font_face($match[5]); 1377 break; 1378 1379 default: 1380 // ignore everything else 1381 break; 1382 } 1383 1384 continue; 1385 } 1386 1387 if ($match[7] !== "") { 1388 $this->_parse_sections($match[7]); 1389 } 1390 1391 } 1392 } 1393 1394 /** 1395 * See also style.cls Style::_image(), refactoring?, works also for imported css files 1396 * 1397 * @param $val 1398 * @return string 1399 */ 1400 protected function _image($val) 1401 { 1402 $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss(); 1403 $parsed_url = "none"; 1404 1405 if (mb_strpos($val, "url") === false) { 1406 $path = "none"; //Don't resolve no image -> otherwise would prefix path and no longer recognize as none 1407 } else { 1408 $val = preg_replace("/url\(\s*['\"]?([^'\")]+)['\"]?\s*\)/", "\\1", trim($val)); 1409 1410 // Resolve the url now in the context of the current stylesheet 1411 $parsed_url = Helpers::explode_url($val); 1412 if ($parsed_url["protocol"] == "" && $this->get_protocol() == "") { 1413 if ($parsed_url["path"][0] === '/' || $parsed_url["path"][0] === '\\') { 1414 $path = $_SERVER["DOCUMENT_ROOT"] . '/'; 1415 } else { 1416 $path = $this->get_base_path(); 1417 } 1418 1419 $path .= $parsed_url["path"] . $parsed_url["file"]; 1420 $path = realpath($path); 1421 // If realpath returns FALSE then specifically state that there is no background image 1422 // FIXME: Is this causing problems for imported CSS files? There are some './none' references when running the test cases. 1423 if (!$path) { 1424 $path = 'none'; 1425 } 1426 } else { 1427 $path = Helpers::build_url($this->get_protocol(), 1428 $this->get_host(), 1429 $this->get_base_path(), 1430 $val); 1431 } 1432 } 1433 1434 if ($DEBUGCSS) { 1435 print "<pre>[_image\n"; 1436 print_r($parsed_url); 1437 print $this->get_protocol() . "\n" . $this->get_base_path() . "\n" . $path . "\n"; 1438 print "_image]</pre>";; 1439 } 1440 1441 return $path; 1442 } 1443 1444 /** 1445 * parse @import{} sections 1446 * 1447 * @param string $url the url of the imported CSS file 1448 */ 1449 private function _parse_import($url) 1450 { 1451 $arr = preg_split("/[\s\n,]/", $url, -1, PREG_SPLIT_NO_EMPTY); 1452 $url = array_shift($arr); 1453 $accept = false; 1454 1455 if (count($arr) > 0) { 1456 $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES; 1457 $acceptedmedia[] = $this->_dompdf->getOptions()->getDefaultMediaType(); 1458 1459 // @import url media_type [media_type...] 1460 foreach ($arr as $type) { 1461 if (in_array(mb_strtolower(trim($type)), $acceptedmedia)) { 1462 $accept = true; 1463 break; 1464 } 1465 } 1466 1467 } else { 1468 // unconditional import 1469 $accept = true; 1470 } 1471 1472 if ($accept) { 1473 // Store our current base url properties in case the new url is elsewhere 1474 $protocol = $this->_protocol; 1475 $host = $this->_base_host; 1476 $path = $this->_base_path; 1477 1478 // $url = str_replace(array('"',"url", "(", ")"), "", $url); 1479 // If the protocol is php, assume that we will import using file:// 1480 // $url = Helpers::build_url($protocol == "php://" ? "file://" : $protocol, $host, $path, $url); 1481 // Above does not work for subfolders and absolute urls. 1482 // Todo: As above, do we need to replace php or file to an empty protocol for local files? 1483 1484 $url = $this->_image($url); 1485 1486 $this->load_css_file($url); 1487 1488 // Restore the current base url 1489 $this->_protocol = $protocol; 1490 $this->_base_host = $host; 1491 $this->_base_path = $path; 1492 } 1493 1494 } 1495 1496 /** 1497 * parse @font-face{} sections 1498 * http://www.w3.org/TR/css3-fonts/#the-font-face-rule 1499 * 1500 * @param string $str CSS @font-face rules 1501 */ 1502 private function _parse_font_face($str) 1503 { 1504 $descriptors = $this->_parse_properties($str); 1505 1506 preg_match_all("/(url|local)\s*\([\"\']?([^\"\'\)]+)[\"\']?\)\s*(format\s*\([\"\']?([^\"\'\)]+)[\"\']?\))?/i", $descriptors->src, $src); 1507 1508 $sources = array(); 1509 $valid_sources = array(); 1510 1511 foreach ($src[0] as $i => $value) { 1512 $source = array( 1513 "local" => strtolower($src[1][$i]) === "local", 1514 "uri" => $src[2][$i], 1515 "format" => strtolower($src[4][$i]), 1516 "path" => Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $src[2][$i]), 1517 ); 1518 1519 if (!$source["local"] && in_array($source["format"], array("", "truetype"))) { 1520 $valid_sources[] = $source; 1521 } 1522 1523 $sources[] = $source; 1524 } 1525 1526 // No valid sources 1527 if (empty($valid_sources)) { 1528 return; 1529 } 1530 1531 $style = array( 1532 "family" => $descriptors->get_font_family_raw(), 1533 "weight" => $descriptors->font_weight, 1534 "style" => $descriptors->font_style, 1535 ); 1536 1537 $this->getFontMetrics()->registerFont($style, $valid_sources[0]["path"], $this->_dompdf->getHttpContext()); 1538 } 1539 1540 /** 1541 * parse regular CSS blocks 1542 * 1543 * _parse_properties() creates a new Style object based on the provided 1544 * CSS rules. 1545 * 1546 * @param string $str CSS rules 1547 * @return Style 1548 */ 1549 private function _parse_properties($str) 1550 { 1551 $properties = preg_split("/;(?=(?:[^\(]*\([^\)]*\))*(?![^\)]*\)))/", $str); 1552 $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss(); 1553 1554 if ($DEBUGCSS) { 1555 print '[_parse_properties'; 1556 } 1557 1558 // Create the style 1559 $style = new Style($this, Stylesheet::ORIG_AUTHOR); 1560 1561 foreach ($properties as $prop) { 1562 // If the $prop contains an url, the regex may be wrong 1563 // @todo: fix the regex so that it works every time 1564 /*if (strpos($prop, "url(") === false) { 1565 if (preg_match("/([a-z-]+)\s*:\s*[^:]+$/i", $prop, $m)) 1566 $prop = $m[0]; 1567 }*/ 1568 //A css property can have " ! important" appended (whitespace optional) 1569 //strip this off to decode core of the property correctly. 1570 //Pass on in the style to allow proper handling: 1571 //!important properties can only be overridden by other !important ones. 1572 //$style->$prop_name = is a shortcut of $style->__set($prop_name,$value);. 1573 //If no specific set function available, set _props["prop_name"] 1574 //style is always copied completely, or $_props handled separately 1575 //Therefore set a _important_props["prop_name"]=true to indicate the modifier 1576 1577 /* Instead of short code, prefer the typical case with fast code 1578 $important = preg_match("/(.*?)!\s*important/",$prop,$match); 1579 if ( $important ) { 1580 $prop = $match[1]; 1581 } 1582 $prop = trim($prop); 1583 */ 1584 if ($DEBUGCSS) print '('; 1585 1586 $important = false; 1587 $prop = trim($prop); 1588 1589 if (substr($prop, -9) === 'important') { 1590 $prop_tmp = rtrim(substr($prop, 0, -9)); 1591 1592 if (substr($prop_tmp, -1) === '!') { 1593 $prop = rtrim(substr($prop_tmp, 0, -1)); 1594 $important = true; 1595 } 1596 } 1597 1598 if ($prop === "") { 1599 if ($DEBUGCSS) print 'empty)'; 1600 continue; 1601 } 1602 1603 $i = mb_strpos($prop, ":"); 1604 if ($i === false) { 1605 if ($DEBUGCSS) print 'novalue' . $prop . ')'; 1606 continue; 1607 } 1608 1609 $prop_name = rtrim(mb_strtolower(mb_substr($prop, 0, $i))); 1610 $value = ltrim(mb_substr($prop, $i + 1)); 1611 if ($DEBUGCSS) print $prop_name . ':=' . $value . ($important ? '!IMPORTANT' : '') . ')'; 1612 //New style, anyway empty 1613 //if ($important || !$style->important_get($prop_name) ) { 1614 //$style->$prop_name = array($value,$important); 1615 //assignment might be replaced by overloading through __set, 1616 //and overloaded functions might check _important_props, 1617 //therefore set _important_props first. 1618 if ($important) { 1619 $style->important_set($prop_name); 1620 } 1621 //For easier debugging, don't use overloading of assignments with __set 1622 $style->$prop_name = $value; 1623 //$style->props_set($prop_name, $value); 1624 } 1625 if ($DEBUGCSS) print '_parse_properties]'; 1626 1627 return $style; 1628 } 1629 1630 /** 1631 * parse selector + rulesets 1632 * 1633 * @param string $str CSS selectors and rulesets 1634 * @param array $media_queries 1635 */ 1636 private function _parse_sections($str, $media_queries = array()) 1637 { 1638 // Pre-process: collapse all whitespace and strip whitespace around '>', 1639 // '.', ':', '+', '#' 1640 1641 $patterns = array("/[\\s\n]+/", "/\\s+([>.:+#])\\s+/"); 1642 $replacements = array(" ", "\\1"); 1643 $str = preg_replace($patterns, $replacements, $str); 1644 $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss(); 1645 1646 $sections = explode("}", $str); 1647 if ($DEBUGCSS) print '[_parse_sections'; 1648 foreach ($sections as $sect) { 1649 $i = mb_strpos($sect, "{"); 1650 if ($i === false) { continue; } 1651 1652 //$selectors = explode(",", mb_substr($sect, 0, $i)); 1653 $selectors = preg_split("/,(?![^\(]*\))/", mb_substr($sect, 0, $i),0, PREG_SPLIT_NO_EMPTY); 1654 if ($DEBUGCSS) print '[section'; 1655 1656 $style = $this->_parse_properties(trim(mb_substr($sect, $i + 1))); 1657 1658 // Assign it to the selected elements 1659 foreach ($selectors as $selector) { 1660 $selector = trim($selector); 1661 1662 if ($selector == "") { 1663 if ($DEBUGCSS) print '#empty#'; 1664 continue; 1665 } 1666 if ($DEBUGCSS) print '#' . $selector . '#'; 1667 //if ($DEBUGCSS) { if (strpos($selector,'p') !== false) print '!!!p!!!#'; } 1668 1669 //FIXME: tag the selector with a hash of the media query to separate it from non-conditional styles (?), xpath comments are probably not what we want to do here 1670 if (count($media_queries) > 0) { 1671 $style->set_media_queries($media_queries); 1672 } 1673 $this->add_style($selector, $style); 1674 } 1675 1676 if ($DEBUGCSS) { 1677 print 'section]'; 1678 } 1679 } 1680 1681 if ($DEBUGCSS) { 1682 print '_parse_sections]'; 1683 } 1684 } 1685 1686 /** 1687 * @return string 1688 */ 1689 public static function getDefaultStylesheet() 1690 { 1691 $dir = realpath(__DIR__ . "/../.."); 1692 return $dir . self::DEFAULT_STYLESHEET; 1693 } 1694 1695 /** 1696 * @param FontMetrics $fontMetrics 1697 * @return $this 1698 */ 1699 public function setFontMetrics(FontMetrics $fontMetrics) 1700 { 1701 $this->fontMetrics = $fontMetrics; 1702 return $this; 1703 } 1704 1705 /** 1706 * @return FontMetrics 1707 */ 1708 public function getFontMetrics() 1709 { 1710 return $this->fontMetrics; 1711 } 1712 1713 /** 1714 * dumps the entire stylesheet as a string 1715 * 1716 * Generates a string of each selector and associated style in the 1717 * Stylesheet. Useful for debugging. 1718 * 1719 * @return string 1720 */ 1721 function __toString() 1722 { 1723 $str = ""; 1724 foreach ($this->_styles as $selector => $selector_styles) { 1725 /** @var Style $style */ 1726 foreach ($selector_styles as $style) { 1727 $str .= "$selector => " . $style->__toString() . "\n"; 1728 } 1729 } 1730 1731 return $str; 1732 } 1733} 1734