vendor/dompdf/dompdf/src/Css/Stylesheet.php line 1011

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