Adds a better non-adjacent optimizer.
authorJakub Pawlowicz <jakub@goalsmashers.com>
Sat, 22 Feb 2014 22:23:20 +0000 (22:23 +0000)
committerJakub Pawlowicz <jakub@goalsmashers.com>
Sun, 23 Feb 2014 16:19:42 +0000 (16:19 +0000)
* Makes code easier to understand by splitting simple and complex scenarios.
* Adds compatibility with upcoming new property optimizer.

History.md
lib/selectors/optimizer.js

index 6b1ad36..93078ba 100644 (file)
@@ -1,3 +1,8 @@
+[2.2.0 / 2014-xx-xx (UNRELEASED)](https://github.com/GoalSmashers/clean-css/compare/v2.1.1...HEAD)
+==================
+
+* Adds a better non-adjacent optimizer compatible with the upcoming new property optimizer.
+
 [2.1.1 / 2014-02-18](https://github.com/GoalSmashers/clean-css/compare/v2.1.0...v2.1.1)
 ==================
 
index fe394cf..c55c155 100644 (file)
@@ -123,108 +123,149 @@ module.exports = function Optimizer(data, context, options) {
   };
 
   var reduceNonAdjacent = function(tokens) {
-    var matched = {};
-    var matchedMoreThanOnce = [];
-    var partiallyReduced = [];
-    var reduced = false;
-    var token, selector, selectors;
+    var candidates = {};
+    var moreThanOnce = [];
 
-    for (var i = 0, l = tokens.length; i < l; i++) {
-      token = tokens[i];
-      selector = token.selector;
+    for (var i = tokens.length - 1; i >= 0; i--) {
+      var token = tokens[i];
 
       if (typeof token == 'string' || token.block)
         continue;
 
-      selectors = selector.indexOf(',') > 0 ?
-        selector.split(',').concat(selector) :
-        [selector];
+      var complexSelector = token.selector;
+      var selectors = complexSelector.split(','); // simplification, as :not() can have commas too
+
+      if (selectors.length > 1)
+        selectors.unshift(complexSelector);
 
       for (var j = 0, m = selectors.length; j < m; j++) {
-        var sel = selectors[j];
-        var alreadyMatched = matched[sel];
-        if (alreadyMatched) {
-          if (alreadyMatched.length == 1)
-            matchedMoreThanOnce.push(sel);
-          alreadyMatched.push(i);
-        } else {
-          matched[sel] = [i];
-        }
+        var selector = selectors[j];
+
+        if (!candidates[selector])
+          candidates[selector] = [];
+        else
+          moreThanOnce.push(selector);
+
+        candidates[selector].push({
+          where: i,
+          partial: selector != complexSelector
+        });
       }
     }
 
-    matchedMoreThanOnce.forEach(function(selector) {
-      var matchPositions = matched[selector];
+    _reduceSimpleNonAdjacentCases(tokens, moreThanOnce, candidates);
+    _reduceComplexNonAdjacentCases(tokens, candidates);
+  };
+
+  var _reduceSimpleNonAdjacentCases = function(tokens, matches, positions) {
+    for (var i = 0, l = matches.length; i < l; i++) {
+      var selector = matches[i];
+      var data = positions[selector];
       var bodies = [];
       var joinsAt = [];
-      var j;
+      var processedTokens = [];
+
+      for (var j = data.length - 1, m = 0; j >= 0; j--) {
+        if (data[j].partial && bodies.length === 0)
+          continue;
 
-      for (j = 0, m = matchPositions.length; j < m; j++) {
-        var body = tokens[matchPositions[j]].body;
+        var where = data[j].where;
+        var token = tokens[where];
+        var body = token.body;
         bodies.push(body);
-        joinsAt.push((joinsAt[j - 1] || 0) + body.split(';').length);
+        processedTokens.push(where);
+      }
+
+      for (j = 0, m = bodies.length; j < m; j++) {
+        if (bodies[j].length > 0)
+          joinsAt.push((joinsAt[j - 1] || 0) + bodies[j].split(';').length);
       }
 
       var optimizedBody = propertyOptimizer.process(bodies.join(';'), joinsAt);
-      var optimizedTokens = optimizedBody.split(';');
+      var optimizedProperties = optimizedBody.split(';');
 
-      j = optimizedTokens.length - 1;
-      var currentMatch = matchPositions.length - 1;
+      var processedCount = processedTokens.length;
+      var propertyIdx = optimizedProperties.length - 1;
+      var tokenIdx = processedCount - 1;
 
-      while (currentMatch >= 0) {
-        if (bodies[currentMatch].indexOf(optimizedTokens[j]) > -1 && j > -1) {
-          j -= 1;
+      while (tokenIdx >= 0) {
+        if ((tokenIdx === 0 || bodies[tokenIdx].indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) {
+          propertyIdx--;
           continue;
         }
 
-        var tokenIndex = matchPositions[currentMatch];
-        var token = tokens[tokenIndex];
-        var newBody = optimizedTokens.splice(j + 1);
-        var reducedBody = [];
-        for (var k = 0, n = newBody.length; k < n; k++) {
-          if (newBody[k].length > 0)
-            reducedBody.push(newBody[k]);
-        }
+        var newBody = optimizedProperties.splice(propertyIdx + 1);
+        if (!data[processedCount - tokenIdx - 1].partial)
+          tokens[processedTokens[tokenIdx]].body = newBody.join(';');
+
+        tokenIdx--;
+      }
+    }
+  };
 
-        if (token.selector == selector) {
-          var joinedBody = reducedBody.join(';');
-          reduced = reduced || (token.body != joinedBody);
-          token.body = joinedBody;
-        } else {
-          token._partials = token._partials || [];
-          token._partials.push(reducedBody.join(';'));
+  var _reduceComplexNonAdjacentCases = function(tokens, positions) {
+    for (var i = 0, matches = Object.keys(positions), l = matches.length; i < l; i++) {
+      var complexSelector = matches[i];
+      var allSame = true;
 
-          if (partiallyReduced.indexOf(tokenIndex) == -1)
-            partiallyReduced.push(tokenIndex);
-        }
+      if (complexSelector.indexOf(',') == -1) // another assumption which is wrong in case of :not() selector
+        continue;
 
-        currentMatch -= 1;
-      }
-    });
+      var intoPosition = positions[complexSelector].pop().where;
+      var intoToken = tokens[intoPosition];
+
+      var selectors = complexSelector.split(',');
+      var reducedBodies = [];
+
+      for (var j = 0, m = selectors.length; j < m; j++) {
+        var selector = selectors[j];
+        var data = positions[selector];
+        var bodies = [];
+        var joinsAt = [];
+        var processedTokens = [];
+
+        for (var k = data.length - 1, n = 0; k >= 0; k--) {
+          var where = data[k].where;
+          if (where < intoPosition)
+            continue;
+
+          var token = tokens[where];
+          var body = token.body;
+          bodies.push(body);
+          processedTokens.push(where);
+        }
 
-    // process those tokens which were partially reduced
-    // i.e. at least one of token's selectors saw reduction
-    // if all selectors were reduced to same value we can override it
-    for (i = 0, l = partiallyReduced.length; i < l; i++) {
-      token = tokens[partiallyReduced[i]];
-
-      if (token.body != token._partials[0] && token._partials.length == token.selector.split(',').length) {
-        var newBody = token._partials[0];
-        for (var k = 1, n = token._partials.length; k < n; k++) {
-          if (token._partials[k] != newBody)
-            break;
+        for (k = 0, n = bodies.length; k < n; k++) {
+          if (bodies[k].length > 0)
+            joinsAt.push((joinsAt[k - 1] || 0) + bodies[k].split(';').length);
         }
 
-        if (k == n) {
-          token.body = newBody;
-          reduced = reduced || true;
+        var optimizedBody = propertyOptimizer.process(bodies.join(';'), joinsAt);
+        var optimizedProperties = optimizedBody.split(';');
+
+        var processedCount = processedTokens.length;
+        var propertyIdx = optimizedProperties.length - 1;
+        var tokenIdx = processedCount - 1;
+
+        while (tokenIdx >= 0) {
+          if ((tokenIdx === 0 || bodies[tokenIdx].indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) {
+            propertyIdx--;
+            continue;
+          }
+
+          var newBody = optimizedProperties.splice(propertyIdx + 1);
+          if (tokenIdx === 0)
+            reducedBodies.push(newBody.join(';'));
+
+          tokenIdx--;
         }
+
+        allSame = allSame && reducedBodies[reducedBodies.length - 1] == reducedBodies[0];
       }
 
-      delete token._partials;
+      if (allSame)
+        intoToken.body = reducedBodies[0];
     }
-
-    minificationsMade.unshift(reduced);
   };
 
   var optimize = function(tokens) {