Extracts 'restructure' optimization into a module.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Sun, 21 Jun 2015 13:35:19 +0000 (14:35 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Sun, 21 Jun 2015 13:35:19 +0000 (14:35 +0100)
lib/selectors/advanced.js
lib/selectors/restructure.js [new file with mode: 0644]
test/selectors/advanced-test.js
test/selectors/restructure-test.js [new file with mode: 0644]

index 5d3e66b..4f55c39 100644 (file)
 var optimizeProperties = require('../properties/optimizer');
-var CleanUp = require('./clean-up');
 
 var extractProperties = require('./extractor');
 var canReorder = require('./reorderable').canReorder;
-var canReorderSingle = require('./reorderable').canReorderSingle;
 var stringifyAll = require('../stringifier/one-time').all;
-var stringifyBody = require('../stringifier/one-time').body;
-var stringifySelectors = require('../stringifier/one-time').selectors;
 
 var removeDuplicates = require('./remove-duplicates');
 var mergeAdjacent = require('./merge-adjacent');
 var reduceNonAdjacent = require('./reduce-non-adjacent');
 var mergeNonAdjacentBySelector = require('./merge-non-adjacent-by-selector');
 var mergeNonAdjacentByBody = require('./merge-non-adjacent-by-body');
+var restructure = require('./restructure');
 
 function AdvancedOptimizer(options, context) {
   this.options = options;
   this.validator = context.validator;
 }
 
-function naturalSorter(a, b) {
-  return a > b;
-}
-
-AdvancedOptimizer.prototype.isSpecial = function (selector) {
-  return this.options.compatibility.selectors.special.test(selector);
-};
-
-AdvancedOptimizer.prototype.restructure = function (tokens) {
-  var movableTokens = {};
-  var movedProperties = [];
-  var multiPropertyMoveCache = {};
-  var movedToBeDropped = [];
-  var self = this;
-  var maxCombinationsLevel = 2;
-  var ID_JOIN_CHARACTER = '%';
-
-  function sendToMultiPropertyMoveCache(position, movedProperty, allFits) {
-    for (var i = allFits.length - 1; i >= 0; i--) {
-      var fit = allFits[i][0];
-      var id = addToCache(movedProperty, fit);
-
-      if (multiPropertyMoveCache[id].length > 1 && processMultiPropertyMove(position, multiPropertyMoveCache[id])) {
-        removeAllMatchingFromCache(id);
-        break;
-      }
-    }
-  }
-
-  function addToCache(movedProperty, fit) {
-    var id = cacheId(fit);
-    multiPropertyMoveCache[id] = multiPropertyMoveCache[id] || [];
-    multiPropertyMoveCache[id].push([movedProperty, fit]);
-    return id;
-  }
-
-  function removeAllMatchingFromCache(matchId) {
-    var matchSelectors = matchId.split(ID_JOIN_CHARACTER);
-    var forRemoval = [];
-    var i;
-
-    for (var id in multiPropertyMoveCache) {
-      var selectors = id.split(ID_JOIN_CHARACTER);
-      for (i = selectors.length - 1; i >= 0; i--) {
-        if (matchSelectors.indexOf(selectors[i]) > -1) {
-          forRemoval.push(id);
-          break;
-        }
-      }
-    }
-
-    for (i = forRemoval.length - 1; i >= 0; i--) {
-      delete multiPropertyMoveCache[forRemoval[i]];
-    }
-  }
-
-  function cacheId(cachedTokens) {
-    var id = [];
-    for (var i = 0, l = cachedTokens.length; i < l; i++) {
-      id.push(stringifySelectors(cachedTokens[i][1]));
-    }
-    return id.join(ID_JOIN_CHARACTER);
-  }
-
-  function tokensToMerge(sourceTokens) {
-    var uniqueTokensWithBody = [];
-    var mergeableTokens = [];
-
-    for (var i = sourceTokens.length - 1; i >= 0; i--) {
-      if (self.isSpecial(stringifySelectors(sourceTokens[i][1])))
-        continue;
-
-      mergeableTokens.unshift(sourceTokens[i]);
-      if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1)
-        uniqueTokensWithBody.push(sourceTokens[i]);
-    }
-
-    return uniqueTokensWithBody.length > 1 ?
-      mergeableTokens :
-      [];
-  }
-
-  function shortenIfPossible(position, movedProperty) {
-    var name = movedProperty[0];
-    var value = movedProperty[1];
-    var key = movedProperty[4];
-    var valueSize = name.length + value.length + 1;
-    var allSelectors = [];
-    var qualifiedTokens = [];
-
-    var mergeableTokens = tokensToMerge(movableTokens[key]);
-    if (mergeableTokens.length < 2)
-      return;
-
-    var allFits = findAllFits(mergeableTokens, valueSize, 1);
-    var bestFit = allFits[0];
-    if (bestFit[1] > 0)
-      return sendToMultiPropertyMoveCache(position, movedProperty, allFits);
-
-    for (var i = bestFit[0].length - 1; i >=0; i--) {
-      allSelectors = bestFit[0][i][1].concat(allSelectors);
-      qualifiedTokens.unshift(bestFit[0][i]);
-    }
-
-    allSelectors = CleanUp.selectorDuplicates(allSelectors);
-    dropAsNewTokenAt(position, [movedProperty], allSelectors, qualifiedTokens);
-  }
-
-  function fitSorter(fit1, fit2) {
-    return fit1[1] > fit2[1];
-  }
-
-  function findAllFits(mergeableTokens, propertySize, propertiesCount) {
-    var combinations = allCombinations(mergeableTokens, propertySize, propertiesCount, maxCombinationsLevel - 1);
-    return combinations.sort(fitSorter);
-  }
-
-  function allCombinations(tokensVariant, propertySize, propertiesCount, level) {
-    var differenceVariants = [[tokensVariant, sizeDifference(tokensVariant, propertySize, propertiesCount)]];
-    if (tokensVariant.length > 2 && level > 0) {
-      for (var i = tokensVariant.length - 1; i >= 0; i--) {
-        var subVariant = Array.prototype.slice.call(tokensVariant, 0);
-        subVariant.splice(i, 1);
-        differenceVariants = differenceVariants.concat(allCombinations(subVariant, propertySize, propertiesCount, level - 1));
-      }
-    }
-
-    return differenceVariants;
-  }
-
-  function sizeDifference(tokensVariant, propertySize, propertiesCount) {
-    var allSelectorsSize = 0;
-    for (var i = tokensVariant.length - 1; i >= 0; i--) {
-      allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? stringifySelectors(tokensVariant[i][1]).length : -1;
-    }
-    return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1;
-  }
-
-  function dropAsNewTokenAt(position, properties, allSelectors, mergeableTokens) {
-    var i, j, k, m;
-    var allProperties = [];
-
-    for (i = mergeableTokens.length - 1; i >= 0; i--) {
-      var mergeableToken = mergeableTokens[i];
-
-      for (j = mergeableToken[2].length - 1; j >= 0; j--) {
-        var mergeableProperty = mergeableToken[2][j];
-
-        for (k = 0, m = properties.length; k < m; k++) {
-          var property = properties[k];
-
-          var mergeablePropertyName = mergeableProperty[0][0];
-          var propertyName = property[0];
-          var propertyBody = property[4];
-          if (mergeablePropertyName == propertyName && stringifyBody([mergeableProperty]) == propertyBody) {
-            mergeableToken[2].splice(j, 1);
-            break;
-          }
-        }
-      }
-    }
-
-    for (i = properties.length - 1; i >= 0; i--) {
-      allProperties.push(properties[i][3]);
-    }
-
-    var newToken = ['selector', allSelectors, allProperties];
-    tokens.splice(position, 0, newToken);
-  }
-
-  function dropPropertiesAt(position, movedProperty) {
-    var key = movedProperty[4];
-    var toMove = movableTokens[key];
-
-    if (toMove && toMove.length > 1) {
-      if (!shortenMultiMovesIfPossible(position, movedProperty))
-        shortenIfPossible(position, movedProperty);
-    }
-  }
-
-  function shortenMultiMovesIfPossible(position, movedProperty) {
-    var candidates = [];
-    var propertiesAndMergableTokens = [];
-    var key = movedProperty[4];
-    var j, k;
-
-    var mergeableTokens = tokensToMerge(movableTokens[key]);
-    if (mergeableTokens.length < 2)
-      return;
-
-    movableLoop:
-    for (var value in movableTokens) {
-      var tokensList = movableTokens[value];
-
-      for (j = mergeableTokens.length - 1; j >= 0; j--) {
-        if (tokensList.indexOf(mergeableTokens[j]) == -1)
-          continue movableLoop;
-      }
-
-      candidates.push(value);
-    }
-
-    if (candidates.length < 2)
-      return false;
-
-    for (j = candidates.length - 1; j >= 0; j--) {
-      for (k = movedProperties.length - 1; k >= 0; k--) {
-        if (movedProperties[k][4] == candidates[j]) {
-          propertiesAndMergableTokens.unshift([movedProperties[k], mergeableTokens]);
-          break;
-        }
-      }
-    }
-
-    return processMultiPropertyMove(position, propertiesAndMergableTokens);
-  }
-
-  function processMultiPropertyMove(position, propertiesAndMergableTokens) {
-    var valueSize = 0;
-    var properties = [];
-    var property;
-
-    for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) {
-      property = propertiesAndMergableTokens[i][0];
-      var fullValue = property[4];
-      valueSize += fullValue.length + (i > 0 ? 1 : 0);
-
-      properties.push(property);
-    }
-
-    var mergeableTokens = propertiesAndMergableTokens[0][1];
-    var bestFit = findAllFits(mergeableTokens, valueSize, properties.length)[0];
-    if (bestFit[1] > 0)
-      return false;
-
-    var allSelectors = [];
-    var qualifiedTokens = [];
-    for (i = bestFit[0].length - 1; i >= 0; i--) {
-      allSelectors = bestFit[0][i][1].concat(allSelectors);
-      qualifiedTokens.unshift(bestFit[0][i]);
-    }
-
-    allSelectors = CleanUp.selectorDuplicates(allSelectors);
-    dropAsNewTokenAt(position, properties, allSelectors, qualifiedTokens);
-
-    for (i = properties.length - 1; i >= 0; i--) {
-      property = properties[i];
-      var index = movedProperties.indexOf(property);
-
-      delete movableTokens[property[4]];
-
-      if (index > -1 && movedToBeDropped.indexOf(index) == -1)
-        movedToBeDropped.push(index);
-    }
-
-    return true;
-  }
-
-  function boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) {
-    var propertyName = property[0];
-    var movedPropertyName = movedProperty[0];
-    if (propertyName != movedPropertyName)
-      return false;
-
-    var key = movedProperty[4];
-    var toMove = movableTokens[key];
-    return toMove && toMove.indexOf(token) > -1;
-  }
-
-  for (var i = tokens.length - 1; i >= 0; i--) {
-    var token = tokens[i];
-    var isSelector;
-    var j, k, m;
-
-    if (token[0] == 'selector') {
-      isSelector = true;
-    } else if (token[0] == 'block') {
-      isSelector = false;
-    } else {
-      continue;
-    }
-
-    // We cache movedProperties.length as it may change in the loop
-    var movedCount = movedProperties.length;
-
-    var properties = extractProperties(token);
-    movedToBeDropped = [];
-
-    var unmovableInCurrentToken = [];
-    for (j = properties.length - 1; j >= 0; j--) {
-      for (k = j - 1; k >= 0; k--) {
-        if (!canReorderSingle(properties[j], properties[k])) {
-          unmovableInCurrentToken.push(j);
-          break;
-        }
-      }
-    }
-
-    for (j = 0, m = properties.length; j < m; j++) {
-      var property = properties[j];
-      var movedSameProperty = false;
-
-      for (k = 0; k < movedCount; k++) {
-        var movedProperty = movedProperties[k];
-
-        if (movedToBeDropped.indexOf(k) == -1 && !canReorderSingle(property, movedProperty) && !boundToAnotherPropertyInCurrrentToken(property, movedProperty, token)) {
-          dropPropertiesAt(i + 1, movedProperty, token);
-
-          if (movedToBeDropped.indexOf(k) == -1) {
-            movedToBeDropped.push(k);
-            delete movableTokens[movedProperty[4]];
-          }
-        }
-
-        if (!movedSameProperty)
-          movedSameProperty = property[0] == movedProperty[0] && property[1] == movedProperty[1];
-      }
-
-      if (!isSelector || unmovableInCurrentToken.indexOf(j) > -1)
-        continue;
-
-      var key = property[4];
-      movableTokens[key] = movableTokens[key] || [];
-      movableTokens[key].push(token);
-
-      if (!movedSameProperty)
-        movedProperties.push(property);
-    }
-
-    movedToBeDropped = movedToBeDropped.sort(naturalSorter);
-    for (j = 0, m = movedToBeDropped.length; j < m; j++) {
-      var dropAt = movedToBeDropped[j] - j;
-      movedProperties.splice(dropAt, 1);
-    }
-  }
-
-  var position = tokens[0] && tokens[0][0] == 'at-rule' && tokens[0][1][0].indexOf('@charset') === 0 ? 1 : 0;
-  for (; position < tokens.length - 1; position++) {
-    var isImportRule = tokens[position][0] === 'at-rule' && tokens[position][1][0].indexOf('@import') === 0;
-    var isEscapedCommentSpecial = tokens[position][0] === 'text' && tokens[position][1][0].indexOf('__ESCAPED_COMMENT_SPECIAL') === 0;
-    if (!(isImportRule || isEscapedCommentSpecial))
-      break;
-  }
-
-  for (i = 0; i < movedProperties.length; i++) {
-    dropPropertiesAt(position, movedProperties[i]);
-  }
-};
-
 AdvancedOptimizer.prototype.removeDuplicateMediaQueries = function (tokens) {
   var candidates = {};
 
@@ -488,7 +136,7 @@ AdvancedOptimizer.prototype.optimize = function (tokens) {
     mergeNonAdjacentByBody(tokens, self.options);
 
     if (self.options.restructuring && withRestructuring) {
-      self.restructure(tokens);
+      restructure(tokens, self.options);
       mergeAdjacent(tokens, self.options, self.validator);
     }
 
diff --git a/lib/selectors/restructure.js b/lib/selectors/restructure.js
new file mode 100644 (file)
index 0000000..2fe72fb
--- /dev/null
@@ -0,0 +1,352 @@
+var extractProperties = require('./extractor');
+var canReorderSingle = require('./reorderable').canReorderSingle;
+var stringifyBody = require('../stringifier/one-time').body;
+var stringifySelectors = require('../stringifier/one-time').selectors;
+var cleanUpSelectorDuplicates = require('./clean-up').selectorDuplicates;
+var isSpecial = require('./is-special');
+
+function naturalSorter(a, b) {
+  return a > b;
+}
+
+function restructure(tokens, options) {
+  var movableTokens = {};
+  var movedProperties = [];
+  var multiPropertyMoveCache = {};
+  var movedToBeDropped = [];
+  var maxCombinationsLevel = 2;
+  var ID_JOIN_CHARACTER = '%';
+
+  function sendToMultiPropertyMoveCache(position, movedProperty, allFits) {
+    for (var i = allFits.length - 1; i >= 0; i--) {
+      var fit = allFits[i][0];
+      var id = addToCache(movedProperty, fit);
+
+      if (multiPropertyMoveCache[id].length > 1 && processMultiPropertyMove(position, multiPropertyMoveCache[id])) {
+        removeAllMatchingFromCache(id);
+        break;
+      }
+    }
+  }
+
+  function addToCache(movedProperty, fit) {
+    var id = cacheId(fit);
+    multiPropertyMoveCache[id] = multiPropertyMoveCache[id] || [];
+    multiPropertyMoveCache[id].push([movedProperty, fit]);
+    return id;
+  }
+
+  function removeAllMatchingFromCache(matchId) {
+    var matchSelectors = matchId.split(ID_JOIN_CHARACTER);
+    var forRemoval = [];
+    var i;
+
+    for (var id in multiPropertyMoveCache) {
+      var selectors = id.split(ID_JOIN_CHARACTER);
+      for (i = selectors.length - 1; i >= 0; i--) {
+        if (matchSelectors.indexOf(selectors[i]) > -1) {
+          forRemoval.push(id);
+          break;
+        }
+      }
+    }
+
+    for (i = forRemoval.length - 1; i >= 0; i--) {
+      delete multiPropertyMoveCache[forRemoval[i]];
+    }
+  }
+
+  function cacheId(cachedTokens) {
+    var id = [];
+    for (var i = 0, l = cachedTokens.length; i < l; i++) {
+      id.push(stringifySelectors(cachedTokens[i][1]));
+    }
+    return id.join(ID_JOIN_CHARACTER);
+  }
+
+  function tokensToMerge(sourceTokens) {
+    var uniqueTokensWithBody = [];
+    var mergeableTokens = [];
+
+    for (var i = sourceTokens.length - 1; i >= 0; i--) {
+      if (isSpecial(options, stringifySelectors(sourceTokens[i][1])))
+        continue;
+
+      mergeableTokens.unshift(sourceTokens[i]);
+      if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1)
+        uniqueTokensWithBody.push(sourceTokens[i]);
+    }
+
+    return uniqueTokensWithBody.length > 1 ?
+      mergeableTokens :
+      [];
+  }
+
+  function shortenIfPossible(position, movedProperty) {
+    var name = movedProperty[0];
+    var value = movedProperty[1];
+    var key = movedProperty[4];
+    var valueSize = name.length + value.length + 1;
+    var allSelectors = [];
+    var qualifiedTokens = [];
+
+    var mergeableTokens = tokensToMerge(movableTokens[key]);
+    if (mergeableTokens.length < 2)
+      return;
+
+    var allFits = findAllFits(mergeableTokens, valueSize, 1);
+    var bestFit = allFits[0];
+    if (bestFit[1] > 0)
+      return sendToMultiPropertyMoveCache(position, movedProperty, allFits);
+
+    for (var i = bestFit[0].length - 1; i >=0; i--) {
+      allSelectors = bestFit[0][i][1].concat(allSelectors);
+      qualifiedTokens.unshift(bestFit[0][i]);
+    }
+
+    allSelectors = cleanUpSelectorDuplicates(allSelectors);
+    dropAsNewTokenAt(position, [movedProperty], allSelectors, qualifiedTokens);
+  }
+
+  function fitSorter(fit1, fit2) {
+    return fit1[1] > fit2[1];
+  }
+
+  function findAllFits(mergeableTokens, propertySize, propertiesCount) {
+    var combinations = allCombinations(mergeableTokens, propertySize, propertiesCount, maxCombinationsLevel - 1);
+    return combinations.sort(fitSorter);
+  }
+
+  function allCombinations(tokensVariant, propertySize, propertiesCount, level) {
+    var differenceVariants = [[tokensVariant, sizeDifference(tokensVariant, propertySize, propertiesCount)]];
+    if (tokensVariant.length > 2 && level > 0) {
+      for (var i = tokensVariant.length - 1; i >= 0; i--) {
+        var subVariant = Array.prototype.slice.call(tokensVariant, 0);
+        subVariant.splice(i, 1);
+        differenceVariants = differenceVariants.concat(allCombinations(subVariant, propertySize, propertiesCount, level - 1));
+      }
+    }
+
+    return differenceVariants;
+  }
+
+  function sizeDifference(tokensVariant, propertySize, propertiesCount) {
+    var allSelectorsSize = 0;
+    for (var i = tokensVariant.length - 1; i >= 0; i--) {
+      allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? stringifySelectors(tokensVariant[i][1]).length : -1;
+    }
+    return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1;
+  }
+
+  function dropAsNewTokenAt(position, properties, allSelectors, mergeableTokens) {
+    var i, j, k, m;
+    var allProperties = [];
+
+    for (i = mergeableTokens.length - 1; i >= 0; i--) {
+      var mergeableToken = mergeableTokens[i];
+
+      for (j = mergeableToken[2].length - 1; j >= 0; j--) {
+        var mergeableProperty = mergeableToken[2][j];
+
+        for (k = 0, m = properties.length; k < m; k++) {
+          var property = properties[k];
+
+          var mergeablePropertyName = mergeableProperty[0][0];
+          var propertyName = property[0];
+          var propertyBody = property[4];
+          if (mergeablePropertyName == propertyName && stringifyBody([mergeableProperty]) == propertyBody) {
+            mergeableToken[2].splice(j, 1);
+            break;
+          }
+        }
+      }
+    }
+
+    for (i = properties.length - 1; i >= 0; i--) {
+      allProperties.push(properties[i][3]);
+    }
+
+    var newToken = ['selector', allSelectors, allProperties];
+    tokens.splice(position, 0, newToken);
+  }
+
+  function dropPropertiesAt(position, movedProperty) {
+    var key = movedProperty[4];
+    var toMove = movableTokens[key];
+
+    if (toMove && toMove.length > 1) {
+      if (!shortenMultiMovesIfPossible(position, movedProperty))
+        shortenIfPossible(position, movedProperty);
+    }
+  }
+
+  function shortenMultiMovesIfPossible(position, movedProperty) {
+    var candidates = [];
+    var propertiesAndMergableTokens = [];
+    var key = movedProperty[4];
+    var j, k;
+
+    var mergeableTokens = tokensToMerge(movableTokens[key]);
+    if (mergeableTokens.length < 2)
+      return;
+
+    movableLoop:
+    for (var value in movableTokens) {
+      var tokensList = movableTokens[value];
+
+      for (j = mergeableTokens.length - 1; j >= 0; j--) {
+        if (tokensList.indexOf(mergeableTokens[j]) == -1)
+          continue movableLoop;
+      }
+
+      candidates.push(value);
+    }
+
+    if (candidates.length < 2)
+      return false;
+
+    for (j = candidates.length - 1; j >= 0; j--) {
+      for (k = movedProperties.length - 1; k >= 0; k--) {
+        if (movedProperties[k][4] == candidates[j]) {
+          propertiesAndMergableTokens.unshift([movedProperties[k], mergeableTokens]);
+          break;
+        }
+      }
+    }
+
+    return processMultiPropertyMove(position, propertiesAndMergableTokens);
+  }
+
+  function processMultiPropertyMove(position, propertiesAndMergableTokens) {
+    var valueSize = 0;
+    var properties = [];
+    var property;
+
+    for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) {
+      property = propertiesAndMergableTokens[i][0];
+      var fullValue = property[4];
+      valueSize += fullValue.length + (i > 0 ? 1 : 0);
+
+      properties.push(property);
+    }
+
+    var mergeableTokens = propertiesAndMergableTokens[0][1];
+    var bestFit = findAllFits(mergeableTokens, valueSize, properties.length)[0];
+    if (bestFit[1] > 0)
+      return false;
+
+    var allSelectors = [];
+    var qualifiedTokens = [];
+    for (i = bestFit[0].length - 1; i >= 0; i--) {
+      allSelectors = bestFit[0][i][1].concat(allSelectors);
+      qualifiedTokens.unshift(bestFit[0][i]);
+    }
+
+    allSelectors = cleanUpSelectorDuplicates(allSelectors);
+    dropAsNewTokenAt(position, properties, allSelectors, qualifiedTokens);
+
+    for (i = properties.length - 1; i >= 0; i--) {
+      property = properties[i];
+      var index = movedProperties.indexOf(property);
+
+      delete movableTokens[property[4]];
+
+      if (index > -1 && movedToBeDropped.indexOf(index) == -1)
+        movedToBeDropped.push(index);
+    }
+
+    return true;
+  }
+
+  function boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) {
+    var propertyName = property[0];
+    var movedPropertyName = movedProperty[0];
+    if (propertyName != movedPropertyName)
+      return false;
+
+    var key = movedProperty[4];
+    var toMove = movableTokens[key];
+    return toMove && toMove.indexOf(token) > -1;
+  }
+
+  for (var i = tokens.length - 1; i >= 0; i--) {
+    var token = tokens[i];
+    var isSelector;
+    var j, k, m;
+
+    if (token[0] == 'selector') {
+      isSelector = true;
+    } else if (token[0] == 'block') {
+      isSelector = false;
+    } else {
+      continue;
+    }
+
+    // We cache movedProperties.length as it may change in the loop
+    var movedCount = movedProperties.length;
+
+    var properties = extractProperties(token);
+    movedToBeDropped = [];
+
+    var unmovableInCurrentToken = [];
+    for (j = properties.length - 1; j >= 0; j--) {
+      for (k = j - 1; k >= 0; k--) {
+        if (!canReorderSingle(properties[j], properties[k])) {
+          unmovableInCurrentToken.push(j);
+          break;
+        }
+      }
+    }
+
+    for (j = 0, m = properties.length; j < m; j++) {
+      var property = properties[j];
+      var movedSameProperty = false;
+
+      for (k = 0; k < movedCount; k++) {
+        var movedProperty = movedProperties[k];
+
+        if (movedToBeDropped.indexOf(k) == -1 && !canReorderSingle(property, movedProperty) && !boundToAnotherPropertyInCurrrentToken(property, movedProperty, token)) {
+          dropPropertiesAt(i + 1, movedProperty, token);
+
+          if (movedToBeDropped.indexOf(k) == -1) {
+            movedToBeDropped.push(k);
+            delete movableTokens[movedProperty[4]];
+          }
+        }
+
+        if (!movedSameProperty)
+          movedSameProperty = property[0] == movedProperty[0] && property[1] == movedProperty[1];
+      }
+
+      if (!isSelector || unmovableInCurrentToken.indexOf(j) > -1)
+        continue;
+
+      var key = property[4];
+      movableTokens[key] = movableTokens[key] || [];
+      movableTokens[key].push(token);
+
+      if (!movedSameProperty)
+        movedProperties.push(property);
+    }
+
+    movedToBeDropped = movedToBeDropped.sort(naturalSorter);
+    for (j = 0, m = movedToBeDropped.length; j < m; j++) {
+      var dropAt = movedToBeDropped[j] - j;
+      movedProperties.splice(dropAt, 1);
+    }
+  }
+
+  var position = tokens[0] && tokens[0][0] == 'at-rule' && tokens[0][1][0].indexOf('@charset') === 0 ? 1 : 0;
+  for (; position < tokens.length - 1; position++) {
+    var isImportRule = tokens[position][0] === 'at-rule' && tokens[position][1][0].indexOf('@import') === 0;
+    var isEscapedCommentSpecial = tokens[position][0] === 'text' && tokens[position][1][0].indexOf('__ESCAPED_COMMENT_SPECIAL') === 0;
+    if (!(isImportRule || isEscapedCommentSpecial))
+      break;
+  }
+
+  for (i = 0; i < movedProperties.length; i++) {
+    dropPropertiesAt(position, movedProperties[i]);
+  }
+}
+
+module.exports = restructure;
index 38ce094..f5409bd 100644 (file)
@@ -2,142 +2,6 @@ var vows = require('vows');
 var optimizerContext = require('../test-helper').optimizerContext;
 
 vows.describe('advanced optimizer')
-  .addBatch(
-    optimizerContext('selectors - restructuring', {
-      'up until changed': [
-        'a{color:#000}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        'a{color:#000}.two,div{color:red}.one{display:block}.two{display:inline}'
-      ],
-      'up until top': [
-        'a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'up until top with charset': [
-        '@charset "utf-8";a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '@charset "utf-8";.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'two at once': [
-        '.one,.two,.three{color:red;display:block}div{margin:0}.four,.five,.six{color:red;display:block}',
-        '.five,.four,.one,.six,.three,.two{color:red;display:block}div{margin:0}'
-      ],
-      'down until changed': [
-        '.one{padding:0}.two{margin:0}.one{margin-bottom:3px}',
-        '.two{margin:0}.one{padding:0;margin-bottom:3px}'
-      ],
-      'over shorthands': [
-        'div{margin-top:0}.one{margin:0}.two{display:block;margin-top:0}',
-        '.two,div{margin-top:0}.one{margin:0}.two{display:block}'
-      ],
-      'over shorthands with flush': [
-        'div{margin-top:0}.one{margin:5px}.two{display:block;margin-top:0}.three{color:red}.four{margin-top:0}',
-        'div{margin-top:0}.one{margin:5px}.four,.two{margin-top:0}.two{display:block}.three{color:red}'
-      ],
-      'over shorthand - border': [
-        '.one{border-color:red}.two{border:1px solid}.three{color:#fff;border-color:red}',
-        '.one{border-color:red}.two{border:1px solid}.three{color:#fff;border-color:red}'
-      ],
-      'granuar over granular': [
-        'div{margin-top:0}.one{margin-bottom:2px}.two{display:block;margin-top:0}',
-        '.two,div{margin-top:0}.one{margin-bottom:2px}.two{display:block}'
-      ],
-      'shorthand over granular with different value': [
-        'div{margin:0}.one{margin-bottom:1px}.two{display:block;margin:0}',
-        'div{margin:0}.one{margin-bottom:1px}.two{display:block;margin:0}'
-      ],
-      'shorthand over granular with different value for simple tags': [
-        'div{margin:0}body{margin-bottom:1px}p{display:block;margin:0}',
-        'div,p{margin:0}body{margin-bottom:1px}p{display:block}'
-      ],
-      'shorthand over granular with different value for simple tags when tag match': [
-        'div{margin:0}body,p{margin-bottom:1px}p{display:block;margin:0}',
-        'div{margin:0}body,p{margin-bottom:1px}p{display:block;margin:0}'
-      ],
-      'shorthand over granular with same value': [
-        'div{margin:0}.one{margin-bottom:0}.two{display:block;margin:0}',
-        '.two,div{margin:0}.one{margin-bottom:0}.two{display:block}'
-      ],
-      'dropping longer content at a right place': [
-        '.one,a:hover{color:red}a:hover{color:#000;display:block;border-color:#000}.longer-name{color:#000;border-color:#000}',
-        '.one,a:hover{color:red}.longer-name,a:hover{color:#000;border-color:#000}a:hover{display:block}'
-      ],
-      'over media without overriding': [
-        'div{margin:0}@media{.one{color:red}}.two{display:block;margin:0}',
-        '.two,div{margin:0}@media{.one{color:red}}.two{display:block}'
-      ],
-      'over media with overriding by different value': [
-        'div{margin:0}@media{.one{margin:10px}}.two{display:block;margin:0}',
-        'div{margin:0}@media{.one{margin:10px}}.two{display:block;margin:0}'
-      ],
-      'over media with overriding by same value': [
-        'div{margin:0}@media{.one{margin:0}}.two{display:block;margin:0}',
-        '.two,div{margin:0}@media{.one{margin:0}}.two{display:block}'
-      ],
-      'over media with overriding by a granular': [
-        'div{margin:0}@media{.one{margin-bottom:0}}.two{display:block;margin:0}',
-        '.two,div{margin:0}@media{.one{margin-bottom:0}}.two{display:block}'
-      ],
-      'over media with overriding by a different granular': [
-        'div{margin-top:0}@media{.one{margin-bottom:0}}.two{display:block;margin-top:0}',
-        '.two,div{margin-top:0}@media{.one{margin-bottom:0}}.two{display:block}'
-      ],
-      'over media with a new property': [
-        'div{margin-top:0}@media{.one{margin-top:0}}.two{display:block;margin:0}',
-        'div{margin-top:0}@media{.one{margin-top:0}}.two{display:block;margin:0}'
-      ],
-      'over a property in the same selector': [
-        'div{background-size:100%}a{background:no-repeat;background-size:100%}',
-        'div{background-size:100%}a{background:no-repeat;background-size:100%}'
-      ],
-      'multiple granular up to a shorthand': [
-        '.one{border:1px solid #bbb}.two{border-color:#666}.three{border-width:1px;border-style:solid}',
-        '.one{border:1px solid #bbb}.two{border-color:#666}.three{border-width:1px;border-style:solid}'
-      ],
-      'multiple granular - complex case': [
-        '.one{background:red;padding:8px 16px}.two{padding-left:16px;padding-right:16px}.three{padding-top:20px}.four{border-left:1px solid #000;border-right:1px solid #000;border-bottom:1px solid #000}.five{background-color:#fff;background-image:-moz-linear-gradient();background-image:-ms-linear-gradient();background-image:-webkit-gradient();background-image:-webkit-linear-gradient()}',
-        '.one{background:red;padding:8px 16px}.two{padding-left:16px;padding-right:16px}.three{padding-top:20px}.four{border-left:1px solid #000;border-right:1px solid #000;border-bottom:1px solid #000}.five{background-color:#fff;background-image:-moz-linear-gradient();background-image:-ms-linear-gradient();background-image:-webkit-gradient();background-image:-webkit-linear-gradient()}'
-      ],
-      'multiple granular - special': [
-        'input:-ms-input-placeholder{color:red;text-align:center}input::placeholder{color:red;text-align:center}',
-        'input:-ms-input-placeholder{color:red;text-align:center}input::placeholder{color:red;text-align:center}'
-      ],
-      'moving one already being moved with different value': [
-        '.one{color:red}.two{display:block}.three{color:red;display:inline}.four{display:inline-block}.five{color:#000}',
-        '.one,.three{color:red}.two{display:block}.three{display:inline}.four{display:inline-block}.five{color:#000}'
-      ],
-      'not in keyframes': [
-        '@keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}',
-        '@keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}'
-      ],
-      'not in vendored keyframes': [
-        '@-moz-keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}',
-        '@-moz-keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}'
-      ],
-      'with one important comment': [
-        '/*! comment */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '/*! comment */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'with many important comments': [
-        '/*! comment 1 *//*! comment 2 */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '/*! comment 1 *//*! comment 2 */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'with important comment and charset': [
-        '@charset "utf-8";/*! comment */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '@charset "utf-8";/*! comment */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'with charset and import': [
-        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'with charset and import and comments': [
-        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);/*! comment */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
-        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);/*! comment */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
-      ],
-      'with vendor prefixed value group': [
-        'a{-moz-box-sizing:content-box;box-sizing:content-box}div{color:red}p{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}',
-        'a{box-sizing:content-box}a,p{-moz-box-sizing:content-box}div{color:red}p{-webkit-box-sizing:content-box;box-sizing:content-box}'
-      ]
-    }, { advanced: true })
-  )
   .addBatch(
     optimizerContext('@media', {
       'empty': [
diff --git a/test/selectors/restructure-test.js b/test/selectors/restructure-test.js
new file mode 100644 (file)
index 0000000..59cf0aa
--- /dev/null
@@ -0,0 +1,149 @@
+var vows = require('vows');
+var optimizerContext = require('../test-helper').optimizerContext;
+
+vows.describe('restructure')
+  .addBatch(
+    optimizerContext('advanced on', {
+      'up until changed': [
+        'a{color:#000}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        'a{color:#000}.two,div{color:red}.one{display:block}.two{display:inline}'
+      ],
+      'up until top': [
+        'a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'up until top with charset': [
+        '@charset "utf-8";a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '@charset "utf-8";.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'two at once': [
+        '.one,.two,.three{color:red;display:block}div{margin:0}.four,.five,.six{color:red;display:block}',
+        '.five,.four,.one,.six,.three,.two{color:red;display:block}div{margin:0}'
+      ],
+      'down until changed': [
+        '.one{padding:0}.two{margin:0}.one{margin-bottom:3px}',
+        '.two{margin:0}.one{padding:0;margin-bottom:3px}'
+      ],
+      'over shorthands': [
+        'div{margin-top:0}.one{margin:0}.two{display:block;margin-top:0}',
+        '.two,div{margin-top:0}.one{margin:0}.two{display:block}'
+      ],
+      'over shorthands with flush': [
+        'div{margin-top:0}.one{margin:5px}.two{display:block;margin-top:0}.three{color:red}.four{margin-top:0}',
+        'div{margin-top:0}.one{margin:5px}.four,.two{margin-top:0}.two{display:block}.three{color:red}'
+      ],
+      'over shorthand - border': [
+        '.one{border-color:red}.two{border:1px solid}.three{color:#fff;border-color:red}',
+        '.one{border-color:red}.two{border:1px solid}.three{color:#fff;border-color:red}'
+      ],
+      'granuar over granular': [
+        'div{margin-top:0}.one{margin-bottom:2px}.two{display:block;margin-top:0}',
+        '.two,div{margin-top:0}.one{margin-bottom:2px}.two{display:block}'
+      ],
+      'shorthand over granular with different value': [
+        'div{margin:0}.one{margin-bottom:1px}.two{display:block;margin:0}',
+        'div{margin:0}.one{margin-bottom:1px}.two{display:block;margin:0}'
+      ],
+      'shorthand over granular with different value for simple tags': [
+        'div{margin:0}body{margin-bottom:1px}p{display:block;margin:0}',
+        'div,p{margin:0}body{margin-bottom:1px}p{display:block}'
+      ],
+      'shorthand over granular with different value for simple tags when tag match': [
+        'div{margin:0}body,p{margin-bottom:1px}p{display:block;margin:0}',
+        'div{margin:0}body,p{margin-bottom:1px}p{display:block;margin:0}'
+      ],
+      'shorthand over granular with same value': [
+        'div{margin:0}.one{margin-bottom:0}.two{display:block;margin:0}',
+        '.two,div{margin:0}.one{margin-bottom:0}.two{display:block}'
+      ],
+      'dropping longer content at a right place': [
+        '.one,a:hover{color:red}a:hover{color:#000;display:block;border-color:#000}.longer-name{color:#000;border-color:#000}',
+        '.one,a:hover{color:red}.longer-name,a:hover{color:#000;border-color:#000}a:hover{display:block}'
+      ],
+      'over media without overriding': [
+        'div{margin:0}@media{.one{color:red}}.two{display:block;margin:0}',
+        '.two,div{margin:0}@media{.one{color:red}}.two{display:block}'
+      ],
+      'over media with overriding by different value': [
+        'div{margin:0}@media{.one{margin:10px}}.two{display:block;margin:0}',
+        'div{margin:0}@media{.one{margin:10px}}.two{display:block;margin:0}'
+      ],
+      'over media with overriding by same value': [
+        'div{margin:0}@media{.one{margin:0}}.two{display:block;margin:0}',
+        '.two,div{margin:0}@media{.one{margin:0}}.two{display:block}'
+      ],
+      'over media with overriding by a granular': [
+        'div{margin:0}@media{.one{margin-bottom:0}}.two{display:block;margin:0}',
+        '.two,div{margin:0}@media{.one{margin-bottom:0}}.two{display:block}'
+      ],
+      'over media with overriding by a different granular': [
+        'div{margin-top:0}@media{.one{margin-bottom:0}}.two{display:block;margin-top:0}',
+        '.two,div{margin-top:0}@media{.one{margin-bottom:0}}.two{display:block}'
+      ],
+      'over media with a new property': [
+        'div{margin-top:0}@media{.one{margin-top:0}}.two{display:block;margin:0}',
+        'div{margin-top:0}@media{.one{margin-top:0}}.two{display:block;margin:0}'
+      ],
+      'over a property in the same selector': [
+        'div{background-size:100%}a{background:no-repeat;background-size:100%}',
+        'div{background-size:100%}a{background:no-repeat;background-size:100%}'
+      ],
+      'multiple granular up to a shorthand': [
+        '.one{border:1px solid #bbb}.two{border-color:#666}.three{border-width:1px;border-style:solid}',
+        '.one{border:1px solid #bbb}.two{border-color:#666}.three{border-width:1px;border-style:solid}'
+      ],
+      'multiple granular - complex case': [
+        '.one{background:red;padding:8px 16px}.two{padding-left:16px;padding-right:16px}.three{padding-top:20px}.four{border-left:1px solid #000;border-right:1px solid #000;border-bottom:1px solid #000}.five{background-color:#fff;background-image:-moz-linear-gradient();background-image:-ms-linear-gradient();background-image:-webkit-gradient();background-image:-webkit-linear-gradient()}',
+        '.one{background:red;padding:8px 16px}.two{padding-left:16px;padding-right:16px}.three{padding-top:20px}.four{border-left:1px solid #000;border-right:1px solid #000;border-bottom:1px solid #000}.five{background-color:#fff;background-image:-moz-linear-gradient();background-image:-ms-linear-gradient();background-image:-webkit-gradient();background-image:-webkit-linear-gradient()}'
+      ],
+      'multiple granular - special': [
+        'input:-ms-input-placeholder{color:red;text-align:center}input::placeholder{color:red;text-align:center}',
+        'input:-ms-input-placeholder{color:red;text-align:center}input::placeholder{color:red;text-align:center}'
+      ],
+      'moving one already being moved with different value': [
+        '.one{color:red}.two{display:block}.three{color:red;display:inline}.four{display:inline-block}.five{color:#000}',
+        '.one,.three{color:red}.two{display:block}.three{display:inline}.four{display:inline-block}.five{color:#000}'
+      ],
+      'not in keyframes': [
+        '@keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}',
+        '@keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}'
+      ],
+      'not in vendored keyframes': [
+        '@-moz-keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}',
+        '@-moz-keyframes test{0%{transform:scale3d(1,1,1);opacity:1}100%{transform:scale3d(.5,.5,.5);opacity:1}}'
+      ],
+      'with one important comment': [
+        '/*! comment */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '/*! comment */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'with many important comments': [
+        '/*! comment 1 *//*! comment 2 */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '/*! comment 1 *//*! comment 2 */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'with important comment and charset': [
+        '@charset "utf-8";/*! comment */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '@charset "utf-8";/*! comment */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'with charset and import': [
+        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'with charset and import and comments': [
+        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);/*! comment */a{width:100px}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        '@charset "UTF-8";@import url(http://fonts.googleapis.com/css?family=Lora:400,700);/*! comment */.two,div{color:red}a{width:100px}.one{display:block}.two{display:inline}'
+      ],
+      'with vendor prefixed value group': [
+        'a{-moz-box-sizing:content-box;box-sizing:content-box}div{color:red}p{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}',
+        'a{box-sizing:content-box}a,p{-moz-box-sizing:content-box}div{color:red}p{-webkit-box-sizing:content-box;box-sizing:content-box}'
+      ]
+    })
+  )
+  .addBatch(
+    optimizerContext('advanced off', {
+      'up until changed': [
+        'a{color:#000}div{color:red}.one{display:block}.two{display:inline;color:red}',
+        'a{color:#000}div{color:red}.one{display:block}.two{display:inline;color:red}'
+      ]
+    }, { advanced: false })
+  )
+  .export(module);