diff --git a/lib/csslib.php b/lib/csslib.php index 0b4523fc291..8058e1952e6 100644 --- a/lib/csslib.php +++ b/lib/csslib.php @@ -776,6 +776,16 @@ class css_optimiser { $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; @@ -785,14 +795,12 @@ class css_optimiser { $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. @@ -800,33 +808,50 @@ class css_optimiser { // 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(); + } } /** @@ -1189,7 +1214,7 @@ abstract class css_writer { $output = ''; if ($typestring !== 'all') { - $output .= $nl.$nl."@media {$typestring} {".$nl; + $output .= "\n@media {$typestring} {".$nl; self::increase_indent(); } foreach ($rules as $rule) { @@ -1213,7 +1238,7 @@ abstract class css_writer { 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(); } @@ -1796,17 +1821,24 @@ abstract class css_rule_collection { 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); } @@ -3685,6 +3717,16 @@ class css_style_background extends css_style { $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 @@ -3694,6 +3736,12 @@ class css_style_background extends css_style { $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'); @@ -3705,7 +3753,6 @@ class css_style_background extends css_style { 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))) { @@ -3714,20 +3761,17 @@ class css_style_background extends css_style { $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) { @@ -3743,20 +3787,33 @@ class css_style_background extends css_style { $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(); @@ -3766,6 +3823,20 @@ class css_style_background extends css_style { 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 * @@ -3884,6 +3955,37 @@ class css_style_background extends css_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. * @@ -4609,4 +4711,4 @@ class css_style_float extends css_style_generic { } return trim($value); } -} \ No newline at end of file +} diff --git a/lib/tests/cssslib_test.php b/lib/tests/cssslib_test.php index 6677b7b7069..c5f12233b8b 100644 --- a/lib/tests/cssslib_test.php +++ b/lib/tests/cssslib_test.php @@ -39,6 +39,15 @@ require_once($CFG->libdir . '/csslib.php'); */ 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 */ @@ -59,21 +68,12 @@ class css_optimiser_testcase extends advanced_testcase { 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(); } /** @@ -105,7 +105,8 @@ class css_optimiser_testcase extends advanced_testcase { * * @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;}'; @@ -209,13 +210,18 @@ class css_optimiser_testcase extends advanced_testcase { $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)); @@ -305,7 +311,9 @@ class css_optimiser_testcase extends advanced_testcase { * 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)); @@ -397,7 +405,9 @@ class css_optimiser_testcase extends advanced_testcase { $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)); @@ -427,7 +437,9 @@ class css_optimiser_testcase extends advanced_testcase { * 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)); @@ -449,7 +461,7 @@ class css_optimiser_testcase extends advanced_testcase { $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;}'; @@ -470,7 +482,9 @@ class css_optimiser_testcase extends advanced_testcase { * * @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)); @@ -504,7 +518,7 @@ class css_optimiser_testcase extends advanced_testcase { $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;}'; @@ -520,7 +534,9 @@ class css_optimiser_testcase extends advanced_testcase { $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;}'; @@ -542,7 +558,9 @@ class css_optimiser_testcase extends advanced_testcase { $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;}'; @@ -563,7 +581,9 @@ class css_optimiser_testcase extends advanced_testcase { $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;}'; @@ -589,7 +609,8 @@ class css_optimiser_testcase extends advanced_testcase { * * @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{}', @@ -655,7 +676,9 @@ class css_optimiser_testcase extends advanced_testcase { * 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;}'; @@ -717,7 +740,7 @@ class css_optimiser_testcase extends advanced_testcase { * A bulk processing test * @param css_optimiser $optimiser */ - protected function try_bulk_processing(css_optimiser $optimiser) { + protected function try_bulk_processing() { global $CFG; $cssin = <<cssoptimiserpretty = 1; - $this->assertEquals($optimiser->process($cssin), $cssout); + $this->assertEquals($this->get_optimiser()->process($cssin), $cssout); } /** @@ -874,7 +895,9 @@ CSS; * * @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;}'; @@ -916,7 +939,9 @@ CSS; * 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)); @@ -954,11 +979,9 @@ 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;}} @@ -984,15 +1007,14 @@ CSS; CSS; $cssout = <<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)); @@ -1035,7 +1059,19 @@ CSS; $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