Files
piratepoet/lib/Util/CSS.php
2017-03-26 18:45:07 -04:00

277 lines
8.1 KiB
PHP

<?php
namespace MailPoet\Util;
use csstidy;
/*
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 {
private $parsed_css = array();
public static function splitMediaQueries($css) {
$start = 0;
$queries = '';
while(($start = strpos($css, "@media", $start)) !== false) {
// stack to manage brackets
$s = array();
// 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 array($css, $queries);
}
public function parseCSS($text) {
$css = new csstidy();
$css->settings['compress_colors'] = false;
$css->parse($text);
$rules = array();
$position = 0;
foreach($css->css as $declarations) {
foreach($declarations as $selectors => $properties) {
foreach(explode(",", $selectors) as $selector) {
$rules[] = array(
'position' => $position,
'specificity' => self::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;
}
/**
* 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
*/
public static function calculateCSSSpecifity($selector) {
// cleanup selector
$selector = str_replace(array('>', '+'), array(' > ', ' + '), $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"))
*/
public static function styleToArray($str) {
$array = 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"
*/
public static function arrayToStyle($array) {
$parts = array();
foreach($array as $k => $v) {
$parts[] = "$k:$v";
}
return implode(';', $parts);
}
/*
* The core of the algorithm, takes a URL and returns the HTML found there with the CSS inlined.
* If you pass $contents then the original HTML is not downloaded and $contents is used instead.
* $url is mandatory as it is used to resolve the links to the stylesheets found in the HTML.
*/
function inlineCSS($url, $contents=null) {
$html = \pQuery::parseStr($contents);
if(!is_object($html)) {
return false;
}
$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) = self::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 order of specificity.
// 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);
// 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) {
foreach($html->query($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(self::styleToArray($node->style));
echo "\n\nNew style:\n";
print_r(array_merge(self::styleToArray($node->style), $rule['properties']));
echo "</PRE>";
die();
}//*/
// Unserialize the style array, merge the rule's CSS into it...
$nodeStyles = self::styleToArray($node->style);
$style = array_merge($nodeStyles, $rule['properties']);
// !important node styles should take precedence over other styles
$style = array_merge($style, preg_grep("/important/i", $nodeStyles));
// And put the CSS back as a string!
$node->style = self::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
foreach ($rules as $rule) {
foreach($rule['properties'] as $key => $value) {
if(strpos($value, '!important') !== false) {
foreach($html->find($rule['selector']) as $node) {
$style = self::styleToArray($node->style);
$style[$key] = $value;
$node->style = self::arrayToStyle($style);
// remove all !important tags (inlined styles take precedent over others anyway)
$node->style = str_replace("!important", "", $node->style);
}
}
}
}
// Let simple_html_dom give us back our HTML with inline CSS!
return (string)$html;
}
}