From 6ed784a4aae91e5d7f48cf189c24095dfb8062cd Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Sat, 7 Feb 2015 19:45:37 +0000 Subject: [PATCH] Fixes #204 - media query merging. It adds media query merging with the following set of rules: * adjacent media with same conditions can be merged; * non-adjacent media with same conditions can be merged only if any of moved properties is not redefined in-between, or a property is redefined with the exactly same value. --- History.md | 1 + lib/selectors/optimizers/advanced.js | 83 +++++++++++++++++++++++++ test/media-queries-test.js | 91 ++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 test/media-queries-test.js diff --git a/History.md b/History.md index 1182412f..7be034c6 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,7 @@ * Adds better non-adjacent selector merging when body is the same. * Fixed issue [#158](https://github.com/GoalSmashers/clean-css/issues/158) - adds body-based selectors reduction. * Fixed issue [#182](https://github.com/GoalSmashers/clean-css/issues/182) - removing space after closing brace. +* Fixed issue [#204](https://github.com/GoalSmashers/clean-css/issues/204) - `@media` merging. * Fixed issue [#351](https://github.com/GoalSmashers/clean-css/issues/351) - remote `@import`s after content. * Fixed issue [#357](https://github.com/GoalSmashers/clean-css/issues/357) - non-standard but valid URLs. * Fixed issue [#416](https://github.com/GoalSmashers/clean-css/issues/416) - accepts hash as `minify` argument. diff --git a/lib/selectors/optimizers/advanced.js b/lib/selectors/optimizers/advanced.js index 555ea812..f405e939 100644 --- a/lib/selectors/optimizers/advanced.js +++ b/lib/selectors/optimizers/advanced.js @@ -347,6 +347,84 @@ AdvancedOptimizer.prototype.mergeNonAdjacentByBody = function (tokens) { } }; +AdvancedOptimizer.prototype.mergeMediaQueries = function (tokens) { + var candidates = {}; + var reduced = []; + + function allProperties(token) { + var properties = []; + + if (token.kind == 'selector') { + for (var i = token.metadata.bodiesList.length - 1; i >= 0; i--) { + var property = token.metadata.bodiesList[i]; + var splitAt = property.indexOf(':'); + properties.push([ + property.substring(0, splitAt), + property.substring(splitAt + 1) + ]); + } + } else if (token.kind == 'block') { + for (var j = token.body.length - 1; j >= 0; j--) { + properties = properties.concat(allProperties(token.body[j])); + } + } + + return properties; + } + + function breakingMove(moved, traversed) { + for (var i = traversed.length - 1; i >= 0; i--) { + for (var j = moved.length - 1; j >= 0; j--) { + var traversedName = traversed[i][0]; + var traversedValue = traversed[i][1]; + var movedName = moved[j][0]; + var movedValue = moved[j][1]; + + if (traversedName == movedName && traversedValue != movedValue) + return true; + } + } + } + + for (var i = tokens.length - 1; i >= 0; i--) { + var token = tokens[i]; + if (token.kind != 'block' || token.isFlatBlock === true) + continue; + + var candidate = candidates[token.value]; + if (!candidate) { + candidate = []; + candidates[token.value] = candidate; + } + + candidate.push(i); + } + + for (var name in candidates) { + var positions = candidates[name]; + + positionLoop: + for (var j = positions.length - 1; j > 0; j--) { + var source = tokens[positions[j]]; + var target = tokens[positions[j - 1]]; + var movedProperties = allProperties(source); + + for (var k = positions[j] + 1; k < positions[j - 1]; k++) { + var traversedProperties = allProperties(tokens[k]); + if (breakingMove(movedProperties, traversedProperties)) + continue positionLoop; + } + + target.body = source.body.concat(target.body); + source.body = []; + + reduced.push(target); + } + } + + return reduced; +}; + function optimizeProperties(tokens, propertyOptimizer) { for (var i = 0, l = tokens.length; i < l; i++) { var token = tokens[i]; @@ -382,6 +460,11 @@ AdvancedOptimizer.prototype.optimize = function (tokens) { self.mergeNonAdjacentBySelector(tokens); self.mergeNonAdjacentByBody(tokens); + + var reduced = self.mergeMediaQueries(tokens); + for (var i = reduced.length - 1; i >= 0; i--) { + _optimize(reduced[i].body); + } } _optimize(tokens); diff --git a/test/media-queries-test.js b/test/media-queries-test.js new file mode 100644 index 00000000..ea1b3179 --- /dev/null +++ b/test/media-queries-test.js @@ -0,0 +1,91 @@ +var vows = require('vows'); +var assert = require('assert'); +var CleanCSS = require('../index'); + +vows.describe('media queries') + .addBatch({ + 'different ones': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media print{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}}@media print{div{display:block}}'); + } + }, + 'other than @media': { + topic: new CleanCSS().minify('@font-face{font-family:A}@font-face{font-family:B}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@font-face{font-family:A}@font-face{font-family:B}'); + } + } + }) + .addBatch({ + 'same two adjacent': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}div{display:block}}'); + } + }, + 'same three adjacent': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media screen{div{display:block}}@media screen{body{width:100%}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}div{display:block}body{width:100%}}'); + } + }, + 'same two with selectors in between': { + topic: new CleanCSS().minify('@media screen{a{color:red}}body{width:100%}.one{height:100px}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, 'body{width:100%}.one{height:100px}@media screen{a{color:red}div{display:block}}'); + } + }, + 'same two with other @media in between': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media (min-width:1024px){body{width:100%}}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media (min-width:1024px){body{width:100%}}@media screen{a{color:red}div{display:block}}'); + } + }, + 'same two with breaking properties in between': { + topic: new CleanCSS().minify('@media screen{a{color:red}}.one{color:#00f}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}}.one{color:#00f}@media screen{div{display:block}}'); + } + }, + 'same two with breaking @media in between': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media (min-width:1024px){.one{color:#00f}}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}}@media (min-width:1024px){.one{color:#00f}}@media screen{div{display:block}}'); + } + }, + 'same two with breaking nested @media in between': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media (min-width:1024px){@media screen{.one{color:#00f}}}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}}@media (min-width:1024px){@media screen{.one{color:#00f}}}@media screen{div{display:block}}'); + } + }, + 'intermixed': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media (min-width:1024px){p{width:100%}}@media screen{div{display:block}}@media (min-width:1024px){body{height:100%}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red}div{display:block}}@media (min-width:1024px){p{width:100%}body{height:100%}}'); + } + }, + 'same two with same values as moved in between': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media (min-width:1024px){.one{color:red}}@media screen{div{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media (min-width:1024px){.one{color:red}}@media screen{a{color:red}div{display:block}}'); + } + } + }) + .addBatch({ + 'further optimizations': { + topic: new CleanCSS().minify('@media screen{a{color:red}}@media screen{a{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '@media screen{a{color:red;display:block}}'); + } + } + }) + .addBatch({ + 'with comments': { + topic: new CleanCSS().minify('@media screen{a{color:red}}/*! a comment */@media screen{a{display:block}}'), + 'get merged': function(minified) { + assert.equal(minified.styles, '/*! a comment */@media screen{a{color:red;display:block}}'); + } + } + }).export(module); -- 2.34.1