暫無描述

CSS.php 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. namespace MailPoetVendor;
  3. if (!defined('ABSPATH')) exit;
  4. use MailPoet\Util\pQuery\DomNode;
  5. use MailPoet\Util\pQuery\pQuery;
  6. use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
  7. /*
  8. Copyright 2013-2014, François-Marie de Jouvencel
  9. This program is free software: you can redistribute it and/or modify
  10. it under the terms of the GNU General Public License as published by
  11. the Free Software Foundation, either version 3 of the License, or
  12. (at your option) any later version.
  13. This program is distributed in the hope that it will be useful,
  14. but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. GNU General Public License for more details.
  17. You should have received a copy of the GNU General Public License
  18. along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /*
  21. * A class to inline CSS.
  22. *
  23. * It honours !important attributes and doesn't choke on complex styles.
  24. *
  25. *
  26. */
  27. class CSS {
  28. /**
  29. * @param string $contents
  30. * @return DomNode
  31. */
  32. function inlineCSS($contents) {
  33. $html = pQuery::parseStr($contents);
  34. if (!$html instanceof DomNode) {
  35. throw new \InvalidArgumentException('Error parsing contents.');
  36. }
  37. $css_blocks = '';
  38. // Find all <style> blocks and cut styles from them (leaving media queries)
  39. foreach ($html->query('style') as $style) {
  40. list($_css_to_parse, $_css_to_keep) = $this->splitMediaQueries($style->getInnerText());
  41. $css_blocks .= $_css_to_parse;
  42. if (!empty($_css_to_keep)) {
  43. $style->setInnerText($_css_to_keep);
  44. } else {
  45. $style->setOuterText('');
  46. }
  47. }
  48. $raw_css = '';
  49. if (!empty($css_blocks)) {
  50. $raw_css .= $css_blocks;
  51. }
  52. // Get the CSS rules by decreasing specificity (the most specific rule first).
  53. // This is an array with, amongst other things, the keys 'properties', which hold the CSS properties
  54. // and the 'selector', which holds the CSS selector
  55. $rules = $this->parseCSS($raw_css);
  56. $nodes_map = [];
  57. // We loop over each rule by increasing order of specificity, find the nodes matching the selector
  58. // and apply the CSS properties
  59. foreach ($rules as $rule) {
  60. if (!isset($nodes_map[$rule['selector']])) {
  61. $nodes_map[$rule['selector']] = $html->query($rule['selector']);
  62. }
  63. foreach ($nodes_map[$rule['selector']] as $node) {
  64. // I'm leaving this for debug purposes, it has proved useful.
  65. /*
  66. if ($node->already_styled === 'yes')
  67. {
  68. echo "<PRE>";
  69. echo "Rule:\n";
  70. print_r($rule);
  71. echo "\n\nOld style:\n";
  72. echo $node->style."\n";
  73. print_r($this->styleToArray($node->style));
  74. echo "\n\nNew style:\n";
  75. print_r(array_merge($this->styleToArray($node->style), $rule['properties']));
  76. echo "</PRE>";
  77. die();
  78. }//*/
  79. // Unserialize the style array, merge the rule's CSS into it...
  80. $nodeStyles = $this->styleToArray($node->style);
  81. $style = array_merge($rule['properties'], $nodeStyles);
  82. // And put the CSS back as a string!
  83. $node->style = $this->arrayToStyle($style);
  84. // I'm leaving this for debug purposes, it has proved useful.
  85. /*
  86. if ($rule['selector'] === 'table.table-recap td')
  87. {
  88. $node->already_styled = 'yes';
  89. }//*/
  90. }
  91. }
  92. // Now a tricky part: do a second pass with only stuff marked !important
  93. // because !important properties do not care about specificity, except when fighting
  94. // against another !important property
  95. // We need to start with a rule with lowest specificity
  96. $rules = array_reverse($rules);
  97. foreach ($rules as $rule) {
  98. foreach ($rule['properties'] as $key => $value) {
  99. if (strpos($value, '!important') === false) {
  100. continue;
  101. }
  102. foreach ($nodes_map[$rule['selector']] as $node) {
  103. $style = $this->styleToArray($node->style);
  104. $style[$key] = $value;
  105. $node->style = $this->arrayToStyle($style);
  106. // remove all !important tags (inlined styles take precedent over others anyway)
  107. $node->style = str_replace("!important", "", $node->style);
  108. }
  109. }
  110. }
  111. return $html;
  112. }
  113. function parseCSS($text) {
  114. $css = new csstidy();
  115. $css->settings['compress_colors'] = false;
  116. $css->parse($text);
  117. $rules = [];
  118. $position = 0;
  119. foreach ($css->css as $declarations) {
  120. foreach ($declarations as $selectors => $properties) {
  121. foreach (explode(",", $selectors) as $selector) {
  122. $rules[] = [
  123. 'position' => $position,
  124. 'specificity' => $this->calculateCSSSpecifity($selector),
  125. 'selector' => $selector,
  126. 'properties' => $properties,
  127. ];
  128. }
  129. $position += 1;
  130. }
  131. }
  132. usort($rules, function($a, $b) {
  133. if ($a['specificity'] > $b['specificity']) {
  134. return -1;
  135. } else if ($a['specificity'] < $b['specificity']) {
  136. return 1;
  137. } else {
  138. if ($a['position'] > $b['position']) {
  139. return -1;
  140. } else {
  141. return 1;
  142. }
  143. }
  144. });
  145. return $rules;
  146. }
  147. /*
  148. * Merges two CSS inline styles strings into one.
  149. * If both styles defines same property the property from second styles will be used.
  150. */
  151. function mergeInlineStyles($styles_1, $styles_2) {
  152. $merged_styles = array_merge($this->styleToArray($styles_1), $this->styleToArray($styles_2));
  153. return $this->arrayToStyle($merged_styles);
  154. }
  155. private function splitMediaQueries($css) {
  156. $start = 0;
  157. $queries = '';
  158. while (($start = strpos($css, "@media", $start)) !== false) {
  159. // stack to manage brackets
  160. $s = [];
  161. // get the first opening bracket
  162. $i = strpos($css, "{", $start);
  163. // if $i is false, then there is probably a css syntax error
  164. if ($i !== false) {
  165. // push bracket onto stack
  166. array_push($s, $css[$i]);
  167. // move past first bracket
  168. $i++;
  169. while (!empty($s)) {
  170. // if the character is an opening bracket, push it onto the stack, otherwise pop the stack
  171. if ($css[$i] == "{") {
  172. array_push($s, "{");
  173. } else if ($css[$i] == "}") {
  174. array_pop($s);
  175. }
  176. $i++;
  177. }
  178. $queries .= substr($css, $start - 1, $i + 1 - $start) . "\n";
  179. $css = substr($css, 0, $start - 1) . substr($css, $i);
  180. $i = $start;
  181. }
  182. }
  183. return [$css, $queries];
  184. }
  185. /**
  186. * The following function fomes from CssToInlineStyles.php - here is the original licence FOR THIS FUNCTION
  187. *
  188. * CSS to Inline Styles class
  189. *
  190. * @author Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
  191. * @version 1.2.1
  192. * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
  193. * @license BSD License
  194. */
  195. private function calculateCSSSpecifity($selector) {
  196. // cleanup selector
  197. $selector = str_replace(['>', '+'], [' > ', ' + '], $selector);
  198. // init var
  199. $specifity = 0;
  200. // split the selector into chunks based on spaces
  201. $chunks = explode(' ', $selector);
  202. // loop chunks
  203. foreach ($chunks as $chunk) {
  204. // an ID is important, so give it a high specifity
  205. if (strstr($chunk, '#') !== false) $specifity += 100;
  206. // classes are more important than a tag, but less important then an ID
  207. elseif (strstr($chunk, '.')) $specifity += 10;
  208. // anything else isn't that important
  209. else $specifity += 1;
  210. }
  211. // return
  212. return $specifity;
  213. }
  214. /*
  215. * Turns a CSS style string (like: "border: 1px solid black; color:red")
  216. * into an array of properties (like: array("border" => "1px solid black", "color" => "red"))
  217. */
  218. private function styleToArray($str) {
  219. $str = EHelper::unescapeHtmlStyleAttr($str);
  220. $array = [];
  221. if (trim($str) === '') return $array;
  222. foreach (explode(';', $str) as $kv) {
  223. if ($kv === '') {
  224. continue;
  225. }
  226. list($selector, $rule) = explode(':', $kv, 2);
  227. $array[trim($selector)] = trim($rule);
  228. }
  229. return $array;
  230. }
  231. /*
  232. * Reverses what styleToArray does, see above.
  233. * array("border" => "1px solid black", "color" => "red") yields "border: 1px solid black; color:red"
  234. */
  235. private function arrayToStyle($array) {
  236. $parts = [];
  237. foreach ($array as $k => $v) {
  238. $parts[] = "$k:$v";
  239. }
  240. return EHelper::escapeHtmlStyleAttr(implode(';', $parts));
  241. }
  242. }