PageRenderTime 70ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 1ms

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

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