$buffer .= $char;
}
+ foreach ($medias as $media) {
+ $this->optimise($media);
+ }
+ $css = $this->produce_css($charset, $imports, $medias, $keyframes);
+
+ $this->timecomplete = microtime(true);
+ return trim($css);
+ }
+
+ protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
$css = '';
if (!empty($charset)) {
$imports[] = $charset;
$css .= "\n\n";
}
+ $cssreset = array();
+ $cssstandard = array();
+ $csskeyframes = array();
+
// Process each media declaration individually
foreach ($medias as $media) {
- $media->organise_rules_by_selectors();
- $this->optimisedrules += $media->count_rules();
- $this->optimisedselectors += $media->count_selectors();
- if ($media->has_errors()) {
- $this->errors += $media->get_errors();
- }
// If this declaration applies to all media types
if (in_array('all', $media->get_types())) {
// Collect all rules that represet reset rules and remove them from the media object at the same time.
// can't end up out of order because of optimisation.
$resetrules = $media->get_reset_rules(true);
if (!empty($resetrules)) {
- $css .= css_writer::media('all', $resetrules);
+ $cssreset[] = css_writer::media('all', $resetrules);
}
}
- }
-
- // Now process every media object and produce CSS for it.
- foreach ($medias as $media) {
- $css .= $media->out();
+ // Get the standard cSS
+ $cssstandard[] = $media->out();
}
// Finally if there are any keyframe declarations process them now.
if (count($keyframes) > 0) {
- $css .= "\n";
foreach ($keyframes as $keyframe) {
$this->optimisedrules += $keyframe->count_rules();
$this->optimisedselectors += $keyframe->count_selectors();
if ($keyframe->has_errors()) {
$this->errors += $keyframe->get_errors();
}
- $css .= $keyframe->out();
+ $csskeyframes[] = $keyframe->out();
}
}
+ // Join it all together
+ $css .= join('', $cssreset);
+ $css .= join('', $cssstandard);
+ $css .= join('', $csskeyframes);
+
+ // Record the strlenght of the now optimised CSS.
$this->optimisedstrlen = strlen($css);
- $this->timecomplete = microtime(true);
- return trim($css);
+ // Return the now produced CSS
+ return $css;
+ }
+
+ /**
+ * Optimises the CSS rules within a rule collection of one form or another
+ *
+ * @param css_rule_collection $media
+ * @return void This function acts in reference
+ */
+ protected function optimise(css_rule_collection $media) {
+ $media->organise_rules_by_selectors();
+ $this->optimisedrules += $media->count_rules();
+ $this->optimisedselectors += $media->count_selectors();
+ if ($media->has_errors()) {
+ $this->errors += $media->get_errors();
+ }
}
/**
$output = '';
if ($typestring !== 'all') {
- $output .= $nl.$nl."@media {$typestring} {".$nl;
+ $output .= "\n@media {$typestring} {".$nl;
self::increase_indent();
}
foreach ($rules as $rule) {
public static function keyframe($for, $name, array &$rules) {
$nl = self::get_separator();
- $output = $nl."@{$for} {$name} {";
+ $output = "\n@{$for} {$name} {";
foreach ($rules as $rule) {
$output .= $rule->out();
}
public function organise_rules_by_selectors() {
$optimised = array();
$beforecount = count($this->rules);
+ $lasthash = null;
+ $lastrule = null;
foreach ($this->rules as $rule) {
$hash = $rule->get_style_hash();
- if (!array_key_exists($hash, $optimised)) {
- $optimised[$hash] = clone($rule);
- } else {
+ if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
foreach ($rule->get_selectors() as $selector) {
- $optimised[$hash]->add_selector($selector);
+ $lastrule->add_selector($selector);
}
+ continue;
}
+ $lastrule = clone($rule);
+ $lasthash = $hash;
+ $optimised[] = $lastrule;
+ }
+ $this->rules = array();
+ foreach ($optimised as $optimised) {
+ $this->rules[$optimised->get_selector_hash()] = $optimised;
}
- $this->rules = $optimised;
$aftercount = count($this->rules);
return ($beforecount < $aftercount);
}
$value = str_replace($matches[1], '', $value);
}
+ // Switch out the brackets so that they don't get messed up when we explode
+ $brackets = array();
+ $bracketcount = 0;
+ while (preg_match('#\([^\)\(]+\)#', $value, $matches)) {
+ $key = "##BRACKET-{$bracketcount}##";
+ $bracketcount++;
+ $brackets[$key] = $matches[0];
+ $value = str_replace($matches[0], $key, $value);
+ }
+
$important = (stripos($value, '!important') !== false);
if ($important) {
// Great some genius put !important in the background shorthand property
$value = preg_replace('#\s+#', ' ', $value);
$bits = explode(' ', $value);
+ foreach ($bits as $key => $bit) {
+ $bits[$key] = self::replace_bracket_placeholders($bit, $brackets);
+ }
+ unset($bracketcount);
+ unset($brackets);
+
$repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
$attachments = array('scroll' , 'fixed', 'inherit');
$positions = array('top', 'left', 'bottom', 'right', 'center');
if (count($bits) > 0 && css_is_colour(reset($bits))) {
$color = array_shift($bits);
}
- $return[] = new css_style_backgroundcolor('background-color', $color);
$image = self::NULL_VALUE;
if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) {
$image = "url({$imageurl})";
}
}
- $return[] = new css_style_backgroundimage('background-image', $image);
$repeat = self::NULL_VALUE;
if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
$repeat = array_shift($bits);
}
- $return[] = new css_style_backgroundrepeat('background-repeat', $repeat);
$attachment = self::NULL_VALUE;
if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
// scroll , fixed, inherit
$attachment = array_shift($bits);
}
- $return[] = new css_style_backgroundattachment('background-attachment', $attachment);
$position = self::NULL_VALUE;
if (count($bits) > 0) {
$position = join(' ',$widthbits);
}
}
- $return[] = new css_style_backgroundposition('background-position', $position);
if (count($unknownbits)) {
foreach ($unknownbits as $bit) {
+ $bit = trim($bit);
if ($color === self::NULL_VALUE && css_is_colour($bit)) {
- $return[] = new css_style_backgroundcolor('background-color', $bit);
+ $color = $bit;
} else if ($repeat === self::NULL_VALUE && in_array($bit, $repeats)) {
- $return[] = new css_style_backgroundrepeat('background-repeat', $bit);
+ $repeat = $bit;
} else if ($attachment === self::NULL_VALUE && in_array($bit, $attachments)) {
- $return[] = new css_style_backgroundattachment('background-attachment', $bit);
+ $attachment = $bit;
+ } else if ($bit !== '') {
+ $return[] = css_style_background_advanced::init($bit);
}
}
}
+ if ($color === self::NULL_VALUE && $image === self::NULL_VALUE && $repeat === self::NULL_VALUE && $attachment === self::NULL_VALUE && $position === self::NULL_VALUE) {
+ // All primaries are null, return without doing anything else. There may be advanced madness there.
+ return $return;
+ }
+
+ $return[] = new css_style_backgroundcolor('background-color', $color);
+ $return[] = new css_style_backgroundimage('background-image', $image);
+ $return[] = new css_style_backgroundrepeat('background-repeat', $repeat);
+ $return[] = new css_style_backgroundattachment('background-attachment', $attachment);
+ $return[] = new css_style_backgroundposition('background-position', $position);
+
if ($important) {
foreach ($return as $style) {
$style->set_important();
return $return;
}
+ /**
+ * Static helper method to switch in bracket replacements
+ *
+ * @param string $value
+ * @param array $placeholders
+ * @return string
+ */
+ protected static function replace_bracket_placeholders($value, array $placeholders) {
+ while (preg_match('/##BRACKET-\d+##/', $value, $matches)) {
+ $value = str_replace($matches[0], $placeholders[$matches[0]], $value);
+ }
+ return $value;
+ }
+
/**
* Consolidates background styles into a single background style
*
}
}
+/**
+ * A advanced background style that allows multiple values to preserve unknown entities
+ *
+ * @package core_css
+ * @category css
+ * @copyright 2012 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class css_style_background_advanced extends css_style_generic {
+ /**
+ * Creates a new background colour style
+ *
+ * @param string $value The value of the style
+ * @return css_style_backgroundimage
+ */
+ public static function init($value) {
+ $value = preg_replace('#\s+#', ' ', $value);
+ return new css_style_background_advanced('background', $value);
+ }
+
+ /**
+ * Returns true because the advanced background image supports multiple values.
+ * e.g. -webkit-linear-gradient and -moz-linear-gradient.
+ *
+ * @return boolean
+ */
+ public function allows_multiple_values() {
+ return true;
+ }
+}
+
/**
* A background colour style.
*
}
return trim($value);
}
-}
\ No newline at end of file
+}
*/
class css_optimiser_testcase extends advanced_testcase {
+ protected $optimiser;
+
+ public function get_optimiser() {
+ if (!$this->optimiser instanceof css_optimiser) {
+ $this->optimiser = new css_optimiser;
+ }
+ return $this->optimiser;
+ }
+
/**
* Sets up the test class
*/
public function test_process() {
$optimiser = new css_optimiser;
- $this->check_background($optimiser);
- $this->check_borders($optimiser);
- $this->check_colors($optimiser);
- $this->check_margins($optimiser);
- $this->check_padding($optimiser);
- $this->check_widths($optimiser);
- $this->check_cursor($optimiser);
- $this->check_vertical_align($optimiser);
-
- $this->try_broken_css_found_in_moodle($optimiser);
- $this->try_invalid_css_handling($optimiser);
- $this->try_bulk_processing($optimiser);
- $this->try_break_things($optimiser);
- $this->try_media_rules($optimiser);
- $this->try_keyframe_css_animation($optimiser);
+ $this->try_broken_css_found_in_moodle();
+ $this->try_invalid_css_handling();
+ $this->try_bulk_processing();
+ $this->try_break_things();
+ $this->try_media_rules();
+ $this->try_keyframe_css_animation();
}
/**
*
* @param css_optimiser $optimiser
*/
- protected function check_background(css_optimiser $optimiser) {
+ public function test_background() {
+ $optimiser = $this->get_optimiser();
$cssin = '.test {background-color: #123456;}';
$cssout = '.test{background-color:#123456;}';
$cssin = '.userenrolment {background: inherit !important;background-image:url(test.png);}';
$cssout = '.userenrolment{background:inherit !important;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
+
+ $css = '#filesskin .yui3-widget-hd{background:#CCC;background:-webkit-gradient(linear, left top, left bottom, from(#FFFFFF), to(#CCCCCC));background:-moz-linear-gradient(top, #FFFFFF, #CCCCCC);}';
+ $this->assertEquals($css, $optimiser->process($css));
}
/**
* Border tests
* @param css_optimiser $optimiser
*/
- protected function check_borders(css_optimiser $optimiser) {
+ public function test_borders() {
+ $optimiser = $this->get_optimiser();
+
$cssin = '.test {border: 1px solid #654321} .test {border-bottom-color: #123456}';
$cssout = '.test{border:1px solid;border-color:#654321 #654321 #123456;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
* Test colour styles
* @param css_optimiser $optimiser
*/
- protected function check_colors(css_optimiser $optimiser) {
+ public function test_colors() {
+ $optimiser = $this->get_optimiser();
+
$css = '.css{}';
$this->assertEquals($css, $optimiser->process($css));
$this->assertEquals($css, $optimiser->process($css));
}
- protected function check_widths(css_optimiser $optimiser) {
+ public function test_widths() {
+ $optimiser = $this->get_optimiser();
+
$cssin = '.css {width:0}';
$cssout = '.css{width:0;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
* Test margin styles
* @param css_optimiser $optimiser
*/
- protected function check_margins(css_optimiser $optimiser) {
+ public function test_margins() {
+ $optimiser = $this->get_optimiser();
+
$cssin = '.one {margin: 1px 2px 3px 4px}';
$cssout = '.one{margin:1px 2px 3px 4px;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
$this->assertEquals($cssout, $optimiser->process($cssin));
$cssin = '.one, .two, .one.two, .one .two {margin:0;} .one.two {margin:0 7px;}';
- $cssout = '.one, .two, .one .two{margin:0;} .one.two{margin:0 7px;}';
+ $cssout = '.one, .two{margin:0;} .one.two{margin:0 7px;} .one .two{margin:0;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
$cssin = '.block {margin-top: 0px !important;margin-bottom: 0px !important;}';
*
* @param css_optimiser $optimiser
*/
- protected function check_padding(css_optimiser $optimiser) {
+ public function test_padding() {
+ $optimiser = $this->get_optimiser();
+
$cssin = '.one {padding: 1px 2px 3px 4px}';
$cssout = '.one{padding:1px 2px 3px 4px;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
$this->assertEquals($cssout, $optimiser->process($cssin));
$cssin = '.one, .two, .one.two, .one .two {padding:0;} .one.two {padding:0 7px;}';
- $cssout = '.one, .two, .one .two{padding:0;} .one.two{padding:0 7px;}';
+ $cssout = '.one, .two{padding:0;} .one.two{padding:0 7px;} .one .two{padding:0;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
$cssin = '.block {padding-top: 0px !important;padding-bottom: 0px !important;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
}
- protected function check_cursor(css_optimiser $optimiser) {
+ public function test_cursor() {
+ $optimiser = $this->get_optimiser();
+
// Valid cursor
$cssin = '.one {cursor: pointer;}';
$cssout = '.one{cursor:pointer;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
}
- protected function check_vertical_align(css_optimiser $optimiser) {
+ public function test_vertical_align() {
+ $optimiser = $this->get_optimiser();
+
// Valid vertical aligns
$cssin = '.one {vertical-align: baseline;}';
$cssout = '.one{vertical-align:baseline;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
}
- protected function check_float(css_optimiser $optimiser) {
+ public function test_float() {
+ $optimiser = $this->get_optimiser();
+
// Valid vertical aligns
$cssin = '.one {float: inherit;}';
$cssout = '.one{float:inherit;}';
*
* @param css_optimiser $optimiser
*/
- protected function try_invalid_css_handling(css_optimiser $optimiser) {
+ protected function try_invalid_css_handling() {
+ $optimiser = $this->get_optimiser();
$cssin = array(
'.one{}',
* Try to break some things
* @param css_optimiser $optimiser
*/
- protected function try_break_things(css_optimiser $optimiser) {
+ protected function try_break_things() {
+ $optimiser = $this->get_optimiser();
+
// Wildcard test
$cssin = '* {color: black;}';
$cssout = '*{color:#000;}';
* A bulk processing test
* @param css_optimiser $optimiser
*/
- protected function try_bulk_processing(css_optimiser $optimiser) {
+ protected function try_bulk_processing() {
global $CFG;
$cssin = <<<CSS
.test .one {
.test #one{margin:30px;}
#new.style{color:#000;}
-
@media print {
#test .one{margin:40px;color:#123456;}
#test #one{margin:45px;}
}
-
@media print,screen {
#test .one{color:#654321;}
}
CSS;
$CFG->cssoptimiserpretty = 1;
- $this->assertEquals($optimiser->process($cssin), $cssout);
+ $this->assertEquals($this->get_optimiser()->process($cssin), $cssout);
}
/**
*
* @param css_optimiser $optimiser
*/
- public function try_broken_css_found_in_moodle(css_optimiser $optimiser) {
+ public function try_broken_css_found_in_moodle() {
+ $optimiser = $this->get_optimiser();
+
// Notice how things are out of order here but that they get corrected
$cssin = '.test {background:url([[pix:theme|pageheaderbgred]]) top center no-repeat}';
$cssout = '.test{background:url([[pix:theme|pageheaderbgred]]) no-repeat top center;}';
* Test keyframe declarations
* @param css_optimiser $optimiser
*/
- public function try_keyframe_css_animation(css_optimiser $optimiser) {
+ public function try_keyframe_css_animation() {
+ $optimiser = $this->get_optimiser();
+
$css = '.dndupload-arrow{width:56px;height:47px;position:absolute;animation:mymove 5s infinite;-moz-animation:mymove 5s infinite;-webkit-animation:mymove 5s infinite;background:url(\'[[pix:theme|fp/dnd_arrow]]\') no-repeat center;margin-left:-28px;}';
$this->assertEquals($css, $optimiser->process($css));
.testtwo{color:#888;}
.dndupload-arrow{width:56px;height:47px;position:absolute;animation:mymove 5s infinite;-moz-animation:mymove 5s infinite;-webkit-animation:mymove 5s infinite;background:url('[[pix:theme|fp/dnd_arrow]]') no-repeat center;margin-left:-28px;}
-
@media print {
.test{background-color:#333;}
}
-
@keyframes mymove {0%{top:10px;}12%{top:40px;}30%{top:20px;}65%{top:35px;}100%{top:9px;}}
@-moz-keyframes mymove {0%{top:10px;}12%{top:40px;}30%{top:20px;}65%{top:35px;}100%{top:9px;}}
@-webkit-keyframes mymove {0%{top:10px;}12%{top:40px;}30%{top:20px;}65%{top:35px;}100%{top:9px;}}
CSS;
$cssout = <<<CSS
-.dndupload-target,
-.filemanager.fp-select .fp-select-loading,
-.filemanager.fp-select.loading form{display:none;}
-.dndsupported .dndupload-ready .dndupload-target,
-.dndupload-uploading .dndupload-uploadinprogress,
-.filemanager.fp-select.loading .fp-select-loading{display:block;}
+.dndupload-target{display:none;}
+.dndsupported .dndupload-ready .dndupload-target{display:block;}
.dndupload-uploadinprogress{display:none;text-align:center;}
+.dndupload-uploading .dndupload-uploadinprogress{display:block;}
.dndupload-arrow{background:url('[[pix:theme|fp/dnd_arrow]]') no-repeat center;width:56px;height:47px;position:absolute;margin-left:-28px;animation:mymove 5s infinite;-moz-animation:mymove 5s infinite;-webkit-animation:mymove 5s infinite;}
-
+.filemanager.fp-select .fp-select-loading{display:none;}
+.filemanager.fp-select.loading .fp-select-loading{display:block;}
+.filemanager.fp-select.loading form{display:none;}
@keyframes mymove {0%{top:10px;}12%{top:40px;}30%{top:20px;}65%{top:35px;}100%{top:9px;}}
@-moz-keyframes mymove {0%{top:10px;}12%{top:40px;}30%{top:20px;}65%{top:35px;}100%{top:9px;}}
* Test media declarations
* @param css_optimiser $optimiser
*/
- public function try_media_rules(css_optimiser $optimiser) {
+ public function try_media_rules() {
+ $optimiser = $this->get_optimiser();
+
$cssin = "@media print {\n .test{background-color:#333;}\n}";
$cssout = "@media print {\n .test{background-color:#333;}\n}";
$this->assertEquals($cssout, $optimiser->process($cssin));
$this->assertEquals($cssout, $optimiser->process($cssin));
$cssin = "@media screen and (min-width:30px) {\n #region-main-box{background-color:#000;}\n}\n@media screen and (min-width:31px) {\n #region-main-box{background-color:#FFF;}\n}";
- $cssout = "@media screen and (min-width:30px) {\n #region-main-box{background-color:#000;}\n}\n\n@media screen and (min-width:31px) {\n #region-main-box{background-color:#FFF;}\n}";
+ $cssout = "@media screen and (min-width:30px) {\n #region-main-box{background-color:#000;}\n}\n@media screen and (min-width:31px) {\n #region-main-box{background-color:#FFF;}\n}";
+ $this->assertEquals($cssout, $optimiser->process($cssin));
+ }
+
+
+ public function test_css_optimisation_ordering() {
+ $optimiser = $this->get_optimiser();
+
+ $css = '.test{display:none;} .dialogue{display:block;} .dialogue-hidden{display:none;}';
+ $this->assertEquals($css, $optimiser->process($css));
+
+ $cssin = '.test{display:none;} .dialogue-hidden{display:none;} .dialogue{display:block;}';
+ $cssout = '.test, .dialogue-hidden{display:none;} .dialogue{display:block;}';
$this->assertEquals($cssout, $optimiser->process($cssin));
}
}
\ No newline at end of file