Commit 1d1d807e authored by Sam Hemelryk's avatar Sam Hemelryk
Browse files

MDL-29941 csslib: Made optimisation an experimental option

parent 8589a4a5
......@@ -385,6 +385,37 @@ $CFG->admin = 'admin';
//
// $CFG->extramemorylimit = 1G;
//
// The CSS files the Moodle produces can be extremely large and complex, especially
// if you are using a custom theme that builds upon several other themes.
// In Moodle 2.2 a CSS optimiser was added as an experimental feature for advanced
// users. The CSS optimiser organises the CSS in order to reduce the overall number
// of rules and styles being sent to the client. It does this by collating the
// CSS before it is cached removing excess styles and rules and stripping out any
// extraneous content such as comments and empty rules.
// The following settings are used to enable and control the optimisation.
//
// Enable the CSS optimiser. This will only optimise the CSS if themedesignermode
// is not enabled.
//
// $CFG->cssoptimise = true;
//
// If set then CSS will also be optimised when themedesignermode is enabled.
// This is useful if you are a theme designer and want some help optimising your
// CSS.
//
// $CFG->cssoptimisedebug = true;
//
// If set the CSS optimiser will add stats about the optimisation to the top of
// the optimised CSS file. You can then inspect the CSS to see the affect the CSS
// optimiser is having.
//
// $CFG->cssoptimisestats = true;
//
// If set the CSS that is optimised will still retain a minamilistic formatting
// so that anyone wanting to can still clearly read it.
//
// $CFG->cssoptimisepretty = true;
//
//=========================================================================
// 8. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
......
......@@ -30,14 +30,39 @@
* @param array $cssfiles
*/
function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
$css = '';
foreach ($cssfiles as $file) {
$css .= "\n".file_get_contents($file);
}
$css = $theme->post_process($css);
global $CFG;
if (!empty($CFG->cssoptimise)) {
// This is an experimental feature introduced in Moodle 2.2
// The CSS optimiser organises the CSS in order to reduce the overall number
// of rules and styles being sent to the client. It does this by collating
// the CSS before it is cached removing excess styles and rules and stripping
// out any extraneous content such as comments and empty rules.
$optimiser = new css_optimiser;
$css = '';
foreach ($cssfiles as $file) {
$css .= file_get_contents($file)."\n";
}
$css = $theme->post_process($css);
$css = $optimiser->process($css);
$optimiser = new css_optimiser;
$css = $optimiser->process($css);
// If cssoptimisestats is set then stats from the optimisation are collected
// and output at the beginning of the CSS
if (!empty($CFG->cssoptimisestats)) {
$css = $optimiser->output_stats_css().$css;
}
} else {
// This is the default behaviour.
// The cssoptimise setting was introduced in Moodle 2.2 and will hopefully
// in the future be changed from an experimental setting to the default.
// The css_minify_css will method will use the Minify library remove
// comments, additional whitespace and other minor measures to reduce the
// the overall CSS being sent.
// However it has the distinct disadvantage of having to minify the CSS
// before running the post process functions. Potentially things may break
// here if theme designers try to push things with CSS post processing.
$css = $theme->post_process(css_minify_css($cssfiles));
}
check_dir_exists(dirname($csspath));
$fp = fopen($csspath, 'w');
......@@ -104,6 +129,7 @@ function css_send_cached_css($csspath, $rev) {
* @param string CSS
*/
function css_send_uncached_css($css) {
global $CFG;
header('Content-Disposition: inline; filename="styles_debug.php"');
header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
......@@ -115,9 +141,18 @@ function css_send_uncached_css($css) {
if (is_array($css)) {
$css = implode("\n\n", $css);
}
$css = str_replace("\n", "\r\n", $css);
$optimiser = new css_optimiser;
echo $optimiser->process($css);
if (!empty($CFG->cssoptimise) && !empty($CFG->cssoptimisedebug)) {
$css = str_replace("\n", "\r\n", $css);
$optimiser = new css_optimiser;
$css = $optimiser->process($css);
if (!empty($CFG->cssoptimisestats)) {
$css = $optimiser->output_stats_css().$css;
}
}
echo $css;
die;
}
......@@ -130,6 +165,55 @@ function css_send_css_not_found() {
die('CSS was not found, sorry.');
}
function css_minify_css($files) {
global $CFG;
set_include_path($CFG->libdir . '/minify/lib' . PATH_SEPARATOR . get_include_path());
require_once('Minify.php');
if (0 === stripos(PHP_OS, 'win')) {
Minify::setDocRoot(); // IIS may need help
}
// disable all caching, we do it in moodle
Minify::setCache(null, false);
$options = array(
'bubbleCssImports' => false,
// Don't gzip content we just want text for storage
'encodeOutput' => false,
// Maximum age to cache, not used but required
'maxAge' => (60*60*24*20),
// The files to minify
'files' => $files,
// Turn orr URI rewriting
'rewriteCssUris' => false,
// This returns the CSS rather than echoing it for display
'quiet' => true
);
$result = Minify::serve('Files', $options);
return $result['content'];
}
/**
* Given a value determines if it is a valid CSS colour
*
* @param string $value
* @return bool
*/
function css_is_colour($value) {
$value = trim($value);
if (preg_match('/^#([a-fA-F0-9]{1,6})$/', $value)) {
return true;
} else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
return true;
} else if (preg_match('#^(rgb|hsl)\s*\(\s*\d{1,3}\%?\s*,\s*\d{1,3}\%?\s*,\s*\d{1,3}\%?\s*\)$#', $value)) {
return true;
} else if (preg_match('#^(rgb|hsl)a\s*\(\s*\d{1,3}\%?\s*,\s*\d{1,3}\%?\s*,\s*\d{1,3}\%?\s*,\s*\d(\.\d+)?\s*\)$#', $value)) {
return true;
}
return false;
}
/**
* A basic CSS optimiser that strips out unwanted things and then processing the
* CSS organising styles and moving duplicates and useless CSS.
......@@ -192,9 +276,9 @@ class css_optimiser {
);
$imports = array();
$charset = false;
$currentprocess = self::PROCESSING_START;
$currentstyle = css_rule::init();
$currentrule = css_rule::init();
$currentselector = css_selector::init();
$inquotes = false; // ' or "
$inbraces = false; // {
......@@ -278,9 +362,9 @@ class css_optimiser {
if ($inbrackets) {
continue 3;
}
$currentselector->add($buffer);
$currentstyle->add_selector($currentselector);
$currentrule->add_selector($currentselector);
$currentselector = css_selector::init();
$currentprocess = self::PROCESSING_STYLES;
......@@ -301,7 +385,7 @@ class css_optimiser {
continue 3;
}
$currentselector->add($buffer);
$currentstyle->add_selector($currentselector);
$currentrule->add_selector($currentselector);
$currentselector = css_selector::init();
$buffer = '';
continue 3;
......@@ -323,17 +407,17 @@ class css_optimiser {
}
switch ($char) {
case ';':
$currentstyle->add_style($buffer);
$currentrule->add_style($buffer);
$buffer = '';
$inquotes = false;
continue 3;
case '}':
$currentstyle->add_style($buffer);
$this->rawselectors += $currentstyle->get_selector_count();
$currentrule->add_style($buffer);
$this->rawselectors += $currentrule->get_selector_count();
$currentmedia->add_rule($currentstyle);
$currentmedia->add_rule($currentrule);
$currentstyle = css_rule::init();
$currentrule = css_rule::init();
$currentprocess = self::PROCESSING_SELECTORS;
$this->rawrules++;
$buffer = '';
......@@ -362,9 +446,6 @@ class css_optimiser {
$this->optimisedstrlen = strlen($css);
$this->timecomplete = microtime(true);
if (!empty($CFG->cssincludestats)) {
$css = $this->output_stats_css().$css;
}
return trim($css);
}
......@@ -373,7 +454,6 @@ class css_optimiser {
* @return string
*/
public function get_stats() {
$stats = array(
'timestart' => $this->timestart,
'timecomplete' => $this->timecomplete,
......@@ -384,7 +464,7 @@ class css_optimiser {
'rawrules' => $this->rawrules,
'optimisedstrlen' => $this->optimisedstrlen,
'optimisedrules' => $this->optimisedrules,
'optimiedselectors' => $this->optimisedselectors,
'optimisedselectors' => $this->optimisedselectors,
'improvementstrlen' => round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%',
'improvementrules' => round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%',
'improvementselectors' => round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%',
......@@ -407,20 +487,20 @@ class css_optimiser {
$computedcss = "/****************************************\n";
$computedcss .= " *------- CSS Optimisation stats --------\n";
$computedcss .= " * ".date('r')."\n";
$computedcss .= " * {$stats[commentsincss]} \t comments removed\n";
$computedcss .= " * Optimisation took {$stats[timetaken]} seconds\n";
$computedcss .= " * {$stats['commentsincss']} \t comments removed\n";
$computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n";
$computedcss .= " *--------------- before ----------------\n";
$computedcss .= " * {$stats[rawstrlen]} \t chars read in\n";
$computedcss .= " * {$stats[rawrules]} \t rules read in\n";
$computedcss .= " * {$stats[rawselectors]} \t total selectors\n";
$computedcss .= " * {$stats['rawstrlen']} \t chars read in\n";
$computedcss .= " * {$stats['rawrules']} \t rules read in\n";
$computedcss .= " * {$stats['rawselectors']} \t total selectors\n";
$computedcss .= " *---------------- after ----------------\n";
$computedcss .= " * {$stats[optimisedstrlen]} \t chars once optimized\n";
$computedcss .= " * {$stats[optimisedrules]} \t optimized rules\n";
$computedcss .= " * {$stats[optimisedselectors]} \t total selectors once optimized\n";
$computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n";
$computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n";
$computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n";
$computedcss .= " *---------------- stats ----------------\n";
$computedcss .= " * {$stats[strlenimprovement]}% \t reduction in chars\n";
$computedcss .= " * {$stats[ruleimprovement]}% \t reduction in rules\n";
$computedcss .= " * {$stats[selectorimprovement]}% \t reduction in selectors\n";
$computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n";
$computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n";
$computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n";
$computedcss .= " ****************************************/\n\n";
return $computedcss;
......@@ -601,6 +681,154 @@ class css_optimiser {
);
}
/**
* Used to prepare CSS strings
*
* @package moodlecore
* @copyright 2011 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class css_writer {
/**
* The current indent level
* @var int
*/
protected static $indent = 0;
/**
* Returns true if the output should still maintain minimum formatting.
* @return bool
*/
protected static function is_pretty() {
global $CFG;
return (!empty($CFG->cssoptimisepretty));
}
/**
* Returns the indenting char to use for indenting things nicely.
* @return string
*/
protected static function get_indent() {
if (self::is_pretty()) {
return str_repeat(" ", self::$indent);
}
return '';
}
/**
* Increases the current indent
*/
protected static function increase_indent() {
self::$indent++;
}
/**
* Descreases the current indent
*/
protected static function decrease_indent() {
self::$indent--;
}
/**
* Returns the string to use as a separator
* @return string
*/
protected static function get_separator() {
return (self::is_pretty())?"\n":' ';
}
/**
* Returns CSS for media
*
* @param string $typestring
* @param array $rules An array of css_rule objects
* @return string
*/
public static function media($typestring, array &$rules) {
$nl = self::get_separator();
$output = '';
if ($typestring !== 'all') {
$output .= $nl.$nl."@media {$typestring} {".$nl;
self::increase_indent();
}
foreach ($rules as $rule) {
$output .= $rule->out().$nl;
}
if ($typestring !== 'all') {
self::decrease_indent();
$output .= '}';
}
return $output;
}
/**
* Returns CSS for a rule
*
* @param string $selector
* @param string $styles
* @return string
*/
public static function rule($selector, $styles) {
$css = self::get_indent()."{$selector}{{$styles}}";
return $css;
}
/**
* Returns CSS for the selectors of a rule
*
* @param array $selectors Array of css_selector objects
* @return string
*/
public static function selectors(array $selectors) {
$nl = self::get_separator();
$selectorstrings = array();
foreach ($selectors as $selector) {
$selectorstrings[] = $selector->out();
}
return join(','.$nl, $selectorstrings);
}
/**
* Returns a selector given the components that make it up.
*
* @param array $components
* @return string
*/
public static function selector(array $components) {
return trim(join(' ', $components));
}
/**
*
* @param array $styles Array of css_style objects
* @return type
*/
public static function styles(array $styles) {
$bits = array();
foreach ($styles as $style) {
$bits[] = $style->out();
}
return join('', $bits);
}
/**
* Returns a style CSS
*
* @param string $name
* @param string $value
* @param bool $important
* @return string
*/
public static function style($name, $value, $important = false) {
if ($important && strpos($value, '!important') === false) {
$value .= ' !important';
}
return "{$name}:{$value};";
}
}
/**
* A structure to represent a CSS selector.
*
......@@ -627,7 +855,7 @@ class css_selector {
/**
* Initialises a new CSS selector
* @return css_selector
* @return css_selector
*/
public static function init() {
return new css_selector();
......@@ -665,7 +893,7 @@ class css_selector {
* @return string
*/
public function out() {
return trim(join(' ', $this->selectors));
return css_writer::selector($this->selectors);
}
}
......@@ -743,6 +971,8 @@ class css_rule {
if (isset($name) && isset($value) && $name !== '' && $value !== '') {
$style = css_style::init($name, $value);
}
} else if ($style instanceof css_style) {
$style = clone($style);
}
if ($style instanceof css_style) {
$name = $style->get_name();
......@@ -751,6 +981,10 @@ class css_rule {
} else {
$this->styles[$name] = $style;
}
} else if (is_array($style)) {
foreach ($style as $astyle) {
$this->add_style($astyle);
}
}
}
......@@ -765,34 +999,6 @@ class css_rule {
}
}
/**
* Returns all of the styles as a single string that can be used in a CSS
* rule.
*
* @return string
*/
protected function get_style_sting() {
$bits = array();
foreach ($this->styles as $style) {
$bits[] = $style->out();
}
return join('', $bits);
}
/**
* Returns all of the selectors as a single string that can be used in a
* CSS rule
*
* @return string
*/
protected function get_selector_string() {
$selectors = array();
foreach ($this->selectors as $selector) {
$selectors[] = $selector->out();
}
return join(",\n", $selectors);
}
/**
* Returns the array of selectors
* @return array
......@@ -814,11 +1020,34 @@ class css_rule {
* @return string
*/
public function out() {
$css = $this->get_selector_string();
$css .= '{';
$css .= $this->get_style_sting();
$css .= '}';
return $css;
$selectors = css_writer::selectors($this->selectors);
$styles = css_writer::styles($this->get_consolidated_styles());
return css_writer::rule($selectors, $styles);
}
public function get_consolidated_styles() {
$finalstyles = array();
$consolidate = array();
foreach ($this->styles as $style) {
$consolidatetoclass = $style->consolidate_to();
if (!empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) {
$class = 'css_style_'.$consolidatetoclass;
if (!array_key_exists($class, $consolidate)) {
$consolidate[$class] = array();
}
$consolidate[$class][] = $style;
} else {
$finalstyles[] = $style;
}
}
foreach ($consolidate as $class => $styles) {
$styles = $class::consolidate($styles);
foreach ($styles as $style) {
$finalstyles[] = $style;
}
}
return $finalstyles;
}
/**
......@@ -854,8 +1083,7 @@ class css_rule {
* @return string
*/
public function get_style_hash() {
$styles = $this->get_style_sting();
return md5($styles);
return md5(css_writer::styles($this->styles));
}
/**
......@@ -863,8 +1091,7 @@ class css_rule {
* @return string
*/
public function get_selector_hash() {
$selector = $this->get_selector_string();
return md5($selector);
return md5(css_writer::selectors($this->selectors));
}
/**
......@@ -988,20 +1215,7 @@ class css_media {
* @return string
*/
public function out() {
$output = '';
$types = join(',', $this->types);
if ($types !== 'all') {
$output .= "\n\n/***** New media declaration *****/\n";
$output .= "@media {$types} {\n";
}
foreach ($this->rules as $rule) {
$output .= $rule->out()."\n";
}
if ($types !== 'all') {
$output .= '}';
$output .= "\n/***** Media declaration end for $types *****/";
}
return $output;
return css_writer::media(join(',', $this->types), $this->rules);
}
/**
......@@ -1049,7 +1263,7 @@ abstract class css_style {
*
* @param type $name
* @param type $value
* @return css_style_generic
* @return css_style_generic
*/
public static function init($name, $value) {
$specificclass = 'css_style_'.preg_replace('#[^a-zA-Z0-9]+#', '', $name);
......@@ -1116,12 +1330,10 @@ abstract class css_style {
* @return string
*/
public function out($value = null) {
if ($value === null) {
if (is_null($value)) {
$value = $this->get_value();
} else if ($this->important && strpos($value, '!important') === false) {
$value .= ' !important';
}
return "{$this->name}:{$value};";
return css_writer::style($this->name, $value, $this->important);
}
/**
......@@ -1134,6 +1346,10 @@ abstract class css_style {
protected function clean_value($value) {
return $value;
}
public function consolidate_to() {
return null;
}
}
/**
......@@ -1218,6 +1434,635 @@ class css_style_color extends css_style {
}
}
class css_style_margin extends css_style {
public static function init($value) {
$value = preg_replace('#\s+#', ' ', $value);
$bits = explode(' ', $value, 4);
$top = $right = $bottom = $left = null;
if (count($bits) > 0) {
$top = $right = $bottom = $left = array_shift($bits);
}
if (count($bits) > 0) {
$right = $left = array_shift($bits);
}