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

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