Fixes #204 - media query merging.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Sat, 7 Feb 2015 19:45:37 +0000 (19:45 +0000)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Tue, 10 Feb 2015 20:43:27 +0000 (20:43 +0000)
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
lib/selectors/optimizers/advanced.js
test/media-queries-test.js [new file with mode: 0644]

index 1182412..7be034c 100644 (file)
@@ -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.
index 555ea81..f405e93 100644 (file)
@@ -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 (file)
index 0000000..ea1b317
--- /dev/null
@@ -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);