| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- <?php
- namespace MailPoetVendor;
- if (!defined('ABSPATH')) exit;
- use MailPoet\Util\pQuery\DomNode;
- use MailPoet\Util\pQuery\pQuery;
- use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
- /*
- Copyright 2013-2014, François-Marie de Jouvencel
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- /*
- * A class to inline CSS.
- *
- * It honours !important attributes and doesn't choke on complex styles.
- *
- *
- */
- class CSS {
- /**
- * @param string $contents
- * @return DomNode
- */
- function inlineCSS($contents) {
- $html = pQuery::parseStr($contents);
- if (!$html instanceof DomNode) {
- throw new \InvalidArgumentException('Error parsing contents.');
- }
- $css_blocks = '';
- // Find all <style> blocks and cut styles from them (leaving media queries)
- foreach ($html->query('style') as $style) {
- list($_css_to_parse, $_css_to_keep) = $this->splitMediaQueries($style->getInnerText());
- $css_blocks .= $_css_to_parse;
- if (!empty($_css_to_keep)) {
- $style->setInnerText($_css_to_keep);
- } else {
- $style->setOuterText('');
- }
- }
- $raw_css = '';
- if (!empty($css_blocks)) {
- $raw_css .= $css_blocks;
- }
- // Get the CSS rules by decreasing specificity (the most specific rule first).
- // This is an array with, amongst other things, the keys 'properties', which hold the CSS properties
- // and the 'selector', which holds the CSS selector
- $rules = $this->parseCSS($raw_css);
- $nodes_map = [];
- // We loop over each rule by increasing order of specificity, find the nodes matching the selector
- // and apply the CSS properties
- foreach ($rules as $rule) {
- if (!isset($nodes_map[$rule['selector']])) {
- $nodes_map[$rule['selector']] = $html->query($rule['selector']);
- }
- foreach ($nodes_map[$rule['selector']] as $node) {
- // I'm leaving this for debug purposes, it has proved useful.
- /*
- if ($node->already_styled === 'yes')
- {
- echo "<PRE>";
- echo "Rule:\n";
- print_r($rule);
- echo "\n\nOld style:\n";
- echo $node->style."\n";
- print_r($this->styleToArray($node->style));
- echo "\n\nNew style:\n";
- print_r(array_merge($this->styleToArray($node->style), $rule['properties']));
- echo "</PRE>";
- die();
- }//*/
- // Unserialize the style array, merge the rule's CSS into it...
- $nodeStyles = $this->styleToArray($node->style);
- $style = array_merge($rule['properties'], $nodeStyles);
- // And put the CSS back as a string!
- $node->style = $this->arrayToStyle($style);
- // I'm leaving this for debug purposes, it has proved useful.
- /*
- if ($rule['selector'] === 'table.table-recap td')
- {
- $node->already_styled = 'yes';
- }//*/
- }
- }
- // Now a tricky part: do a second pass with only stuff marked !important
- // because !important properties do not care about specificity, except when fighting
- // against another !important property
- // We need to start with a rule with lowest specificity
- $rules = array_reverse($rules);
- foreach ($rules as $rule) {
- foreach ($rule['properties'] as $key => $value) {
- if (strpos($value, '!important') === false) {
- continue;
- }
- foreach ($nodes_map[$rule['selector']] as $node) {
- $style = $this->styleToArray($node->style);
- $style[$key] = $value;
- $node->style = $this->arrayToStyle($style);
- // remove all !important tags (inlined styles take precedent over others anyway)
- $node->style = str_replace("!important", "", $node->style);
- }
- }
- }
- return $html;
- }
- function parseCSS($text) {
- $css = new csstidy();
- $css->settings['compress_colors'] = false;
- $css->parse($text);
- $rules = [];
- $position = 0;
- foreach ($css->css as $declarations) {
- foreach ($declarations as $selectors => $properties) {
- foreach (explode(",", $selectors) as $selector) {
- $rules[] = [
- 'position' => $position,
- 'specificity' => $this->calculateCSSSpecifity($selector),
- 'selector' => $selector,
- 'properties' => $properties,
- ];
- }
- $position += 1;
- }
- }
- usort($rules, function($a, $b) {
- if ($a['specificity'] > $b['specificity']) {
- return -1;
- } else if ($a['specificity'] < $b['specificity']) {
- return 1;
- } else {
- if ($a['position'] > $b['position']) {
- return -1;
- } else {
- return 1;
- }
- }
- });
- return $rules;
- }
- /*
- * Merges two CSS inline styles strings into one.
- * If both styles defines same property the property from second styles will be used.
- */
- function mergeInlineStyles($styles_1, $styles_2) {
- $merged_styles = array_merge($this->styleToArray($styles_1), $this->styleToArray($styles_2));
- return $this->arrayToStyle($merged_styles);
- }
- private function splitMediaQueries($css) {
- $start = 0;
- $queries = '';
- while (($start = strpos($css, "@media", $start)) !== false) {
- // stack to manage brackets
- $s = [];
- // get the first opening bracket
- $i = strpos($css, "{", $start);
- // if $i is false, then there is probably a css syntax error
- if ($i !== false) {
- // push bracket onto stack
- array_push($s, $css[$i]);
- // move past first bracket
- $i++;
- while (!empty($s)) {
- // if the character is an opening bracket, push it onto the stack, otherwise pop the stack
- if ($css[$i] == "{") {
- array_push($s, "{");
- } else if ($css[$i] == "}") {
- array_pop($s);
- }
- $i++;
- }
- $queries .= substr($css, $start - 1, $i + 1 - $start) . "\n";
- $css = substr($css, 0, $start - 1) . substr($css, $i);
- $i = $start;
- }
- }
- return [$css, $queries];
- }
- /**
- * The following function fomes from CssToInlineStyles.php - here is the original licence FOR THIS FUNCTION
- *
- * CSS to Inline Styles class
- *
- * @author Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
- * @version 1.2.1
- * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
- * @license BSD License
- */
- private function calculateCSSSpecifity($selector) {
- // cleanup selector
- $selector = str_replace(['>', '+'], [' > ', ' + '], $selector);
- // init var
- $specifity = 0;
- // split the selector into chunks based on spaces
- $chunks = explode(' ', $selector);
- // loop chunks
- foreach ($chunks as $chunk) {
- // an ID is important, so give it a high specifity
- if (strstr($chunk, '#') !== false) $specifity += 100;
- // classes are more important than a tag, but less important then an ID
- elseif (strstr($chunk, '.')) $specifity += 10;
- // anything else isn't that important
- else $specifity += 1;
- }
- // return
- return $specifity;
- }
- /*
- * Turns a CSS style string (like: "border: 1px solid black; color:red")
- * into an array of properties (like: array("border" => "1px solid black", "color" => "red"))
- */
- private function styleToArray($str) {
- $str = EHelper::unescapeHtmlStyleAttr($str);
- $array = [];
- if (trim($str) === '') return $array;
- foreach (explode(';', $str) as $kv) {
- if ($kv === '') {
- continue;
- }
- list($selector, $rule) = explode(':', $kv, 2);
- $array[trim($selector)] = trim($rule);
- }
- return $array;
- }
- /*
- * Reverses what styleToArray does, see above.
- * array("border" => "1px solid black", "color" => "red") yields "border: 1px solid black; color:red"
- */
- private function arrayToStyle($array) {
- $parts = [];
- foreach ($array as $k => $v) {
- $parts[] = "$k:$v";
- }
- return EHelper::escapeHtmlStyleAttr(implode(';', $parts));
- }
- }
|