Improves selector optimizer.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 29 Sep 2014 12:01:20 +0000 (13:01 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 10 Oct 2014 20:22:44 +0000 (21:22 +0100)
* Turns to prototypal OO.
* Adds specs.
* Extracts Simple & Advanced optimizers into separate modules.
* Adds nasty workarounds as property optimizer is incompatible with new tokenizer output.

lib/selectors/optimizer.js
lib/selectors/optimizers/advanced.js [new file with mode: 0644]
lib/selectors/optimizers/clean-up.js [new file with mode: 0644]
lib/selectors/optimizers/simple.js [new file with mode: 0644]
test/integration-test.js
test/selectors/optimizer-test.js [new file with mode: 0644]
test/selectors/optimizers/simple-test.js [new file with mode: 0644]

index c5e6b6e..bfc31c8 100644 (file)
 var Tokenizer = require('./tokenizer');
-var PropertyOptimizer = require('../properties/optimizer');
-
-module.exports = function Optimizer(options, context) {
-  var specialSelectors = {
-    '*': /\-(moz|ms|o|webkit)\-/,
-    'ie8': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/,
-    'ie7': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/
-  };
-
-  var minificationsMade = [];
-
-  var propertyOptimizer = new PropertyOptimizer(options.compatibility, options.aggressiveMerging, context);
-
-  var cleanUpSelector = function(selectors) {
-    if (selectors.indexOf(',') == -1)
-      return selectors;
-
-    var plain = [];
-    var cursor = 0;
-    var lastComma = 0;
-    var noBrackets = selectors.indexOf('(') == -1;
-    var withinBrackets = function(idx) {
-      if (noBrackets)
-        return false;
-
-      var previousOpening = selectors.lastIndexOf('(', idx);
-      var previousClosing = selectors.lastIndexOf(')', idx);
-
-      if (previousOpening == -1)
-        return false;
-      if (previousClosing > 0 && previousClosing < idx)
-        return false;
-
-      return true;
-    };
-
-    while (true) {
-      var nextComma = selectors.indexOf(',', cursor + 1);
-      var selector;
-
-      if (nextComma === -1) {
-        nextComma = selectors.length;
-      } else if (withinBrackets(nextComma)) {
-        cursor = nextComma + 1;
-        continue;
-      }
-      selector = selectors.substring(lastComma, nextComma);
-      lastComma = cursor = nextComma + 1;
-
-      if (plain.indexOf(selector) == -1)
-        plain.push(selector);
-
-      if (nextComma === selectors.length)
-        break;
-    }
-
-    return plain.sort().join(',');
-  };
-
-  var isSpecial = function(selector) {
-    return specialSelectors[options.compatibility || '*'].test(selector);
-  };
-
-  var removeDuplicates = function(tokens) {
-    var matched = {};
-    var forRemoval = [];
-
-    for (var i = 0, l = tokens.length; i < l; i++) {
-      var token = tokens[i];
-      if (typeof token == 'string' || token.block)
-        continue;
-
-      var id = token.body + '@' + token.selector;
-      var alreadyMatched = matched[id];
-
-      if (alreadyMatched) {
-        forRemoval.push(alreadyMatched[0]);
-        alreadyMatched.unshift(i);
-      } else {
-        matched[id] = [i];
-      }
-    }
-
-    forRemoval = forRemoval.sort(function(a, b) {
-      return a > b ? 1 : -1;
-    });
-
-    for (var j = 0, n = forRemoval.length; j < n; j++) {
-      tokens.splice(forRemoval[j] - j, 1);
-    }
-
-    minificationsMade.unshift(forRemoval.length > 0);
-  };
-
-  var mergeAdjacent = function(tokens) {
-    var forRemoval = [];
-    var lastToken = { selector: null, body: null };
-
-    for (var i = 0, l = tokens.length; i < l; i++) {
-      var token = tokens[i];
-
-      if (typeof token == 'string' || token.block)
-        continue;
-
-      if (token.selector == lastToken.selector) {
-        var joinAt = [lastToken.body.split(';').length];
-        lastToken.body = propertyOptimizer.process(lastToken.body + ';' + token.body, joinAt, false, token.selector);
-        forRemoval.push(i);
-      } else if (token.body == lastToken.body && !isSpecial(token.selector) && !isSpecial(lastToken.selector)) {
-        lastToken.selector = cleanUpSelector(lastToken.selector + ',' + token.selector);
-        forRemoval.push(i);
-      } else {
-        lastToken = token;
-      }
-    }
-
-    for (var j = 0, m = forRemoval.length; j < m; j++) {
-      tokens.splice(forRemoval[j] - j, 1);
-    }
-
-    minificationsMade.unshift(forRemoval.length > 0);
-  };
-
-  var reduceNonAdjacent = function(tokens) {
-    var candidates = {};
-    var moreThanOnce = [];
-
-    for (var i = tokens.length - 1; i >= 0; i--) {
-      var token = tokens[i];
-
-      if (typeof token == 'string' || token.block)
-        continue;
-
-      var complexSelector = token.selector;
-      var selectors = complexSelector.indexOf(',') > -1 && !isSpecial(complexSelector) ?
-        complexSelector.split(',').concat(complexSelector) : // simplification, as :not() can have commas too
-        [complexSelector];
-
-      for (var j = 0, m = selectors.length; j < m; j++) {
-        var selector = selectors[j];
-
-        if (!candidates[selector])
-          candidates[selector] = [];
-        else
-          moreThanOnce.push(selector);
-
-        candidates[selector].push({
-          where: i,
-          partial: selector != complexSelector
-        });
-      }
-    }
-
-    var reducedInSimple = _reduceSimpleNonAdjacentCases(tokens, moreThanOnce, candidates);
-    var reducedInComplex = _reduceComplexNonAdjacentCases(tokens, candidates);
-
-    minificationsMade.unshift(reducedInSimple || reducedInComplex);
-  };
-
-  var _reduceSimpleNonAdjacentCases = function(tokens, matches, positions) {
-    var reduced = false;
-
-    for (var i = 0, l = matches.length; i < l; i++) {
-      var selector = matches[i];
-      var data = positions[selector];
-
-      if (data.length < 2)
-        continue;
-
-      /* jshint loopfunc: true */
-      _reduceSelector(tokens, selector, data, {
-        filterOut: function(idx, bodies) {
-          return data[idx].partial && bodies.length === 0;
-        },
-        callback: function(token, newBody, processedCount, tokenIdx) {
-          if (!data[processedCount - tokenIdx - 1].partial) {
-            token.body = newBody.join(';');
-            reduced = true;
-          }
-        }
-      });
-    }
-
-    return reduced;
-  };
-
-  var _reduceComplexNonAdjacentCases = function(tokens, positions) {
-    var reduced = false;
-
-    allSelectors:
-    for (var complexSelector in positions) {
-      if (complexSelector.indexOf(',') == -1) // simplification, as :not() can have commas too
-        continue;
-
-      var intoPosition = positions[complexSelector].pop().where;
-      var intoToken = tokens[intoPosition];
-
-      var selectors = isSpecial(complexSelector) ?
-        [complexSelector] :
-        complexSelector.split(',');
-      var reducedBodies = [];
-
-      for (var j = 0, m = selectors.length; j < m; j++) {
-        var selector = selectors[j];
-        var data = positions[selector];
-
-        if (data.length < 2)
-          continue allSelectors;
-
-        /* jshint loopfunc: true */
-        _reduceSelector(tokens, selector, data, {
-          filterOut: function(idx) {
-            return data[idx].where < intoPosition;
-          },
-          callback: function(token, newBody, processedCount, tokenIdx) {
-            if (tokenIdx === 0)
-              reducedBodies.push(newBody.join(';'));
-          }
-        });
-
-        if (reducedBodies[reducedBodies.length - 1] != reducedBodies[0])
-          continue allSelectors;
-      }
-
-      intoToken.body = reducedBodies[0];
-      reduced = true;
-    }
-
-    return reduced;
-  };
-
-  var _reduceSelector = function(tokens, selector, data, options) {
-    var bodies = [];
-    var joinsAt = [];
-    var splitBodies = [];
-    var processedTokens = [];
-
-    for (var j = data.length - 1, m = 0; j >= 0; j--) {
-      if (options.filterOut(j, bodies))
-        continue;
-
-      var where = data[j].where;
-      var token = tokens[where];
-      var body = token.body;
-      bodies.push(body);
-      splitBodies.push(body.split(';'));
-      processedTokens.push(where);
-    }
-
-    for (j = 0, m = bodies.length; j < m; j++) {
-      if (bodies[j].length > 0)
-        joinsAt.push((joinsAt[j - 1] || 0) + splitBodies[j].length);
-    }
-
-    var optimizedBody = propertyOptimizer.process(bodies.join(';'), joinsAt, true, selector);
-    var optimizedProperties = optimizedBody.split(';');
-
-    var processedCount = processedTokens.length;
-    var propertyIdx = optimizedProperties.length - 1;
-    var tokenIdx = processedCount - 1;
-
-    while (tokenIdx >= 0) {
-      if ((tokenIdx === 0 || splitBodies[tokenIdx].indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) {
-        propertyIdx--;
-        continue;
-      }
-
-      var newBody = optimizedProperties.splice(propertyIdx + 1);
-      options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx);
-
-      tokenIdx--;
-    }
-  };
-
-  var optimize = function(tokens) {
-    var noChanges = function() {
-      return minificationsMade.length > 4 &&
-        minificationsMade[0] === false &&
-        minificationsMade[1] === false;
-    };
-
-    tokens = Array.isArray(tokens) ? tokens : [tokens];
-    for (var i = 0, l = tokens.length; i < l; i++) {
-      var token = tokens[i];
-
-      if (token.selector) {
-        token.selector = cleanUpSelector(token.selector);
-        token.body = propertyOptimizer.process(token.body, false, false, token.selector);
-      } else if (token.block) {
-        optimize(token.body);
-      }
-    }
-
-    // Run until 2 last operations do not yield any changes
-    minificationsMade = [];
-    while (true) {
-      if (noChanges())
-        break;
-      removeDuplicates(tokens);
-
-      if (noChanges())
-        break;
-      mergeAdjacent(tokens);
-
-      if (noChanges())
-        break;
-      reduceNonAdjacent(tokens);
-    }
-  };
-
-  var rebuild = function(tokens) {
-    var rebuilt = [];
-
-    tokens = Array.isArray(tokens) ? tokens : [tokens];
-    for (var i = 0, l = tokens.length; i < l; i++) {
-      var token = tokens[i];
-
-      if (typeof token == 'string') {
-        rebuilt.push(token);
-        continue;
-      }
-
-      var name = token.block || token.selector;
-      var body = token.block ? rebuild(token.body) : token.body;
-
-      if (body.length > 0)
-        rebuilt.push(name + '{' + body + '}');
-    }
-
-    return rebuilt.join(options.keepBreaks ? options.lineBreak : '');
-  };
-
-  return {
-    process: function(data) {
-      var tokenized = new Tokenizer(context).toTokens(data);
-      // optimize(tokenized);
-      return rebuild(tokenized);
-    }
-  };
+var SimpleOptimizer = require('./optimizers/simple');
+var AdvancedOptimizer = require('./optimizers/advanced');
+
+var lineBreak = require('os').EOL;
+
+function SelectorsOptimizer(options, context) {
+  this.options = options || {};
+  this.context = context || {};
+}
+
+function rebuild(tokens, keepBreaks) {
+  return tokens
+    .map(function (token) {
+      if (typeof token === 'string')
+        return token;
+      // TODO: broken due to joining/splitting
+      if (token.body.length === 0 || (token.body.length == 1 && token.body[0] === ''))
+        return '';
+
+      return token.block ?
+        token.block + '{' + rebuild(token.body, keepBreaks) + '}' :
+        token.selector.join(',') + '{' + token.body.join(';') + '}';
+    })
+    .join(keepBreaks ? lineBreak : '')
+    .trim();
+}
+
+SelectorsOptimizer.prototype.process = function (data) {
+  var tokens = new Tokenizer(this.context).toTokens(data);
+
+  new SimpleOptimizer(this.options, this.context).optimize(tokens);
+  if (!this.options.noAdvanced)
+    new AdvancedOptimizer(this.options, this.context).optimize(tokens);
+
+  return rebuild(tokens, this.options.keepBreaks);
 };
+
+module.exports = SelectorsOptimizer;
diff --git a/lib/selectors/optimizers/advanced.js b/lib/selectors/optimizers/advanced.js
new file mode 100644 (file)
index 0000000..5526774
--- /dev/null
@@ -0,0 +1,280 @@
+var PropertyOptimizer = require('../../properties/optimizer');
+var CleanUp = require('./clean-up');
+
+var specialSelectors = {
+  '*': /\-(moz|ms|o|webkit)\-/,
+  'ie8': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/,
+  'ie7': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/
+};
+
+function AdvancedOptimizer(options, context) {
+  this.options = options;
+  this.minificationsMade = [];
+  this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context);
+}
+
+AdvancedOptimizer.prototype.isSpecial = function (selector) {
+  return specialSelectors[this.options.compatibility || '*'].test(selector);
+};
+
+AdvancedOptimizer.prototype.removeDuplicates = function (tokens) {
+  var matched = {};
+  var forRemoval = [];
+
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    var token = tokens[i];
+    if (typeof token == 'string' || token.block)
+      continue;
+
+    var id = token.body.join(';') + '@' + token.selector.join(',');
+    var alreadyMatched = matched[id];
+
+    if (alreadyMatched) {
+      forRemoval.push(alreadyMatched[0]);
+      alreadyMatched.unshift(i);
+    } else {
+      matched[id] = [i];
+    }
+  }
+
+  forRemoval = forRemoval.sort(function(a, b) {
+    return a > b ? 1 : -1;
+  });
+
+  for (var j = 0, n = forRemoval.length; j < n; j++) {
+    tokens.splice(forRemoval[j] - j, 1);
+  }
+
+  this.minificationsMade.unshift(forRemoval.length > 0);
+};
+
+AdvancedOptimizer.prototype.mergeAdjacent = function (tokens) {
+  var forRemoval = [];
+  var lastToken = { selector: null, body: null };
+
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    var token = tokens[i];
+
+    if (typeof token == 'string' || token.block)
+      continue;
+
+    // TODO: broken due to joining/splitting
+    if (lastToken.selector && token.selector.join(',') == lastToken.selector.join(',')) {
+      var joinAt = [lastToken.body.length];
+      lastToken.body = lastToken.body.concat(token.body);
+      // TODO: broken due to joining/splitting
+      lastToken.body = this.propertyOptimizer.process(lastToken.body.concat(token.body).join(';'), joinAt, false, token.selector.join(',')).split(';');
+      forRemoval.push(i);
+      // TODO: broken due to joining/splitting
+    } else if (lastToken.body && token.body.join(';') == lastToken.body.join(';') && !this.isSpecial(token.selector.join(',')) && !this.isSpecial(lastToken.selector.join(','), this.options)) {
+      lastToken.selector = CleanUp.selectors(lastToken.selector.concat(token.selector));
+      forRemoval.push(i);
+    } else {
+      lastToken = token;
+    }
+  }
+
+  for (var j = 0, m = forRemoval.length; j < m; j++) {
+    tokens.splice(forRemoval[j] - j, 1);
+  }
+
+  this.minificationsMade.unshift(forRemoval.length > 0);
+};
+
+AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) {
+  var candidates = {};
+  var moreThanOnce = [];
+
+  for (var i = tokens.length - 1; i >= 0; i--) {
+    var token = tokens[i];
+
+    if (typeof token == 'string' || token.block)
+      continue;
+
+    var complexSelector = token.selector;
+    var selectors = complexSelector.length > 1 && !this.isSpecial(complexSelector, this.options) ?
+      complexSelector :
+      [complexSelector];
+
+    for (var j = 0, m = selectors.length; j < m; j++) {
+      // TODO: broken due to joining/splitting
+      var selector = selectors[j];
+
+      if (!candidates[selector])
+        candidates[selector] = [];
+      else
+        moreThanOnce.push(selector);
+
+      // TODO: broken due to joining/splitting
+      candidates[selector].push({
+        where: i,
+        partial: selector != complexSelector.join(',')
+      });
+    }
+  }
+
+  var reducedInSimple = this.reduceSimpleNonAdjacentCases(tokens, moreThanOnce, candidates);
+  var reducedInComplex = this.reduceComplexNonAdjacentCases(tokens, candidates);
+
+  this.minificationsMade.unshift(reducedInSimple || reducedInComplex);
+};
+
+AdvancedOptimizer.prototype.reduceSimpleNonAdjacentCases = function (tokens, matches, positions) {
+  var reduced = false;
+
+  for (var i = 0, l = matches.length; i < l; i++) {
+    var selector = matches[i];
+    var data = positions[selector];
+
+    if (data.length < 2)
+      continue;
+
+    /* jshint loopfunc: true */
+    this.reduceSelector(tokens, selector, data, {
+      filterOut: function (idx, bodies) {
+        return data[idx].partial && bodies.length === 0;
+      },
+      callback: function (token, newBody, processedCount, tokenIdx) {
+        if (!data[processedCount - tokenIdx - 1].partial) {
+          token.body = newBody;
+          reduced = true;
+        }
+      }
+    });
+  }
+
+  return reduced;
+};
+
+AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, positions) {
+  var reduced = false;
+
+  allSelectors:
+  for (var complexSelector in positions) {
+    if (positions[complexSelector].length == 1)
+      continue;
+
+    var into = positions[complexSelector];
+    var intoPosition = into[into.length - 1].where;
+    var intoToken = tokens[intoPosition];
+
+    // TODO: broken due to joining/splitting
+    // var selectors = this.isSpecial(complexSelector) ?
+    //   [complexSelector] :
+    //   complexSelector;
+    var selectors = complexSelector.split(',');
+    var reducedBodies = [];
+
+    for (var j = 0, m = selectors.length; j < m; j++) {
+      var selector = selectors[j];
+      var data = positions[selector];
+
+      if (data.length < 2)
+        continue allSelectors;
+
+      /* jshint loopfunc: true */
+      this.reduceSelector(tokens, selector, data, {
+        filterOut: function (idx) {
+          return data[idx].where < intoPosition;
+        },
+        callback: function (token, newBody, processedCount, tokenIdx) {
+          if (tokenIdx === 0)
+            reducedBodies.push(newBody);
+        }
+      });
+
+      if (reducedBodies[reducedBodies.length - 1] != reducedBodies[0])
+        continue allSelectors;
+    }
+
+    intoToken.body = reducedBodies[0];
+    reduced = true;
+  }
+
+  return reduced;
+};
+
+AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, options) {
+  var bodies = [];
+  var joinsAt = [];
+  var splitBodies = [];
+  var processedTokens = [];
+
+  for (var j = data.length - 1, m = 0; j >= 0; j--) {
+    if (options.filterOut(j, bodies))
+      continue;
+
+    var where = data[j].where;
+    var token = tokens[where];
+    var body = token.body;
+    // TODO: broken due to joining/splitting
+    bodies.push(body.join(';'));
+    splitBodies.push(body);
+    processedTokens.push(where);
+  }
+
+  for (j = 0, m = bodies.length; j < m; j++) {
+    if (bodies[j].length > 0)
+      joinsAt.push((joinsAt[j - 1] || 0) + splitBodies[j].length);
+  }
+
+  // TODO: broken due to joining/splitting
+  var optimizedBody = this.propertyOptimizer.process(bodies.join(';'), joinsAt, true, selector).split(';');
+  var optimizedProperties = optimizedBody;
+
+  var processedCount = processedTokens.length;
+  var propertyIdx = optimizedProperties.length - 1;
+  var tokenIdx = processedCount - 1;
+
+  while (tokenIdx >= 0) {
+    if ((tokenIdx === 0 || splitBodies[tokenIdx].join(';').indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) {
+      propertyIdx--;
+      continue;
+    }
+
+    var newBody = optimizedProperties.splice(propertyIdx + 1);
+    options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx);
+
+    tokenIdx--;
+  }
+};
+
+AdvancedOptimizer.prototype.noChanges = function () {
+  return this.minificationsMade.length > 4 &&
+    this.minificationsMade[0] === false &&
+    this.minificationsMade[1] === false;
+};
+
+function optimizeProperties(tokens, propertyOptimizer) {
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    var token = tokens[i];
+
+    if (token.selector) {
+      // TODO: broken due to joining/splitting
+      token.body = propertyOptimizer.process(token.body.join(';'), false, false, token.selector.join(',')).split(';');
+    } else if (token.block) {
+      optimizeProperties(token.body, propertyOptimizer);
+    }
+  }
+}
+
+AdvancedOptimizer.prototype.optimize = function (tokens) {
+  optimizeProperties(tokens, this.propertyOptimizer);
+
+  // Run until 2 last operations do not yield any changes
+  while (true) {
+    if (this.noChanges())
+      break;
+    this.removeDuplicates(tokens);
+
+    if (this.noChanges())
+      break;
+    this.mergeAdjacent(tokens);
+
+    if (this.noChanges())
+      break;
+    this.reduceNonAdjacent(tokens);
+  }
+};
+
+module.exports = AdvancedOptimizer;
diff --git a/lib/selectors/optimizers/clean-up.js b/lib/selectors/optimizers/clean-up.js
new file mode 100644 (file)
index 0000000..27b9d8b
--- /dev/null
@@ -0,0 +1,28 @@
+var CleanUp = {
+  selectors: function (selectors) {
+    var plain = [];
+
+    for (var i = 0, l = selectors.length; i < l; i++) {
+      var reduced = selectors[i]
+        .trim()
+        .replace(/\s*([>\+\~])\s*/g, '$1')
+        .replace(/\*([:#\.\[])/g, '$1')
+        .replace(/\[([^\]]+)\]/g, function (match, value) { return '[' + value.replace(/\s/g, '') + ']'; })
+        .replace(/^(\:first\-child)?\+html/, '*$1+html');
+
+      if (plain.indexOf(reduced) == -1)
+        plain.push(reduced);
+    }
+
+    return plain.sort();
+  },
+
+  block: function (block) {
+    return block
+      .replace(/(\s{2,}|\s)/g, ' ')
+      .replace(/(,|:|\() /g, '$1')
+      .replace(/ \)/g, ')');
+  }
+};
+
+module.exports = CleanUp;
diff --git a/lib/selectors/optimizers/simple.js b/lib/selectors/optimizers/simple.js
new file mode 100644 (file)
index 0000000..5ca68f7
--- /dev/null
@@ -0,0 +1,26 @@
+var PropertyOptimizer = require('../../properties/optimizer');
+var CleanUp = require('./clean-up');
+
+function SimpleOptimizer(options, context) {
+  this.options = options;
+  this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context);
+}
+
+function minify(tokens) {
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    var token = tokens[i];
+
+    if (token.selector) {
+      token.selector = CleanUp.selectors(token.selector);
+    } else if (token.block) {
+      token.block = CleanUp.block(token.block);
+      minify(token.body);
+    }
+  }
+}
+
+SimpleOptimizer.prototype.optimize = function(tokens) {
+  minify(tokens);
+};
+
+module.exports = SimpleOptimizer;
index 3e631b3..c65f356 100644 (file)
@@ -238,26 +238,6 @@ vows.describe('integration tests').addBatch({
     ]
   }, { keepBreaks: true, keepSpecialComments: 0 }),
   'selectors': cssContext({
-    'remove spaces around selectors': [
-      'div + span >   em{display:block}',
-      'div+span>em{display:block}'
-    ],
-    'not remove spaces for pseudo-classes': [
-      'div :first-child{display:block}',
-      'div :first-child{display:block}'
-    ],
-    'strip universal selector from id and class selectors': [
-      '* > *#id > *.class{display:block}',
-      '*>#id>.class{display:block}'
-    ],
-    'strip universal selector from attribute selectors': [
-      '*:first-child > *[data-id]{display:block}',
-      ':first-child>[data-id]{display:block}'
-    ],
-    'not strip standalone universal selector': [
-      'label ~ * + span{display:block}',
-      'label~*+span{display:block}'
-    ],
     'not expand + in selectors mixed with calc methods': [
       'div{width:calc(50% + 3em)}div + div{width:100%}div:hover{width:calc(50% + 4em)}* > div {border:1px solid #f0f}',
       'div{width:calc(50% + 3em)}div+div{width:100%}div:hover{width:calc(50% + 4em)}*>div{border:1px solid #f0f}'
diff --git a/test/selectors/optimizer-test.js b/test/selectors/optimizer-test.js
new file mode 100644 (file)
index 0000000..18c2560
--- /dev/null
@@ -0,0 +1,189 @@
+var vows = require('vows');
+var assert = require('assert');
+var SelectorsOptimizer = require('../../lib/selectors/optimizer');
+
+function optimizerContext(group, specs, options) {
+  var context = {};
+
+  function optimized(target) {
+    return function (source) {
+      assert.equal(new SelectorsOptimizer(options).process(source), target);
+    };
+  }
+
+  for (var name in specs) {
+    context[group + ' - ' + name] = {
+      topic: specs[name][0],
+      optimized: optimized(specs[name][1])
+    };
+  }
+
+  return context;
+}
+
+vows.describe(SelectorsOptimizer)
+  .addBatch(
+    optimizerContext('selectors', {
+      'whitespace - heading & trailing': [
+        ' a {color:red}',
+        'a{color:red}'
+      ],
+      'whitespace - descendant selector': [
+        'div > a{color:red}',
+        'div>a{color:red}'
+      ],
+      'whitespace - next selector': [
+        'div + a{color:red}',
+        'div+a{color:red}'
+      ],
+      'whitespace - sibling selector': [
+        'div  ~ a{color:red}',
+        'div~a{color:red}'
+      ],
+      'whitespace - pseudo classes': [
+        'div  :first-child{color:red}',
+        'div :first-child{color:red}'
+      ],
+      'whitespace - line breaks': [
+        '\r\ndiv\n{color:red}',
+        'div{color:red}'
+      ],
+      'whitespace - tabs': [
+        'div\t\t{color:red}',
+        'div{color:red}'
+      ],
+      'universal selector - id, class, and property': [
+        '* > *#id > *.class > *[property]{color:red}',
+        '*>#id>.class>[property]{color:red}'
+      ],
+      'universal selector - pseudo': [
+        '*:first-child{color:red}',
+        ':first-child{color:red}'
+      ],
+      'universal selector - standalone': [
+        'label ~ * + span{color:red}',
+        'label~*+span{color:red}'
+      ],
+      'order': [
+        'b,div,a{color:red}',
+        'a,b,div{color:red}'
+      ],
+      'duplicates': [
+        'a,div,.class,.class,a ,div > a{color:red}',
+        '.class,a,div,div>a{color:red}'
+      ],
+      'mixed': [
+        ' label   ~  \n*  +  span , div>*.class, section\n\n{color:red}',
+        'div>.class,label~*+span,section{color:red}'
+      ],
+      'calc': [
+        'a{width:-moz-calc(100% - 1em);width:calc(100% - 1em)}',
+        'a{width:-moz-calc(100% - 1em);width:calc(100% - 1em)}'
+      ]
+    })
+  )
+  .addBatch(
+    optimizerContext('properties', {
+      'empty body': [
+        'a{}',
+        ''
+      ],
+      'whitespace body': [
+        'a{   \n }',
+        ''
+      ],
+    })
+  )
+  .addBatch(
+    optimizerContext('@media', {
+      'empty': [
+        '@media (min-width:980px){}',
+        ''
+      ],
+      'whitespace': [
+        ' @media   ( min-width:  980px ){}',
+        ''
+      ],
+      'body': [
+        '@media (min-width:980px){\na\n{color:red}}',
+        '@media (min-width:980px){a{color:red}}'
+      ],
+      'multiple': [
+        '@media screen, print, (min-width:980px){a{color:red}}',
+        '@media screen,print,(min-width:980px){a{color:red}}'
+      ],
+      'nested once': [
+        '@media screen { @media print { a{color:red} } }',
+        '@media screen{@media print{a{color:red}}}'
+      ],
+      'nested twice': [
+        '@media screen { @media print { @media (min-width:980px) { a{color:red} } } }',
+        '@media screen{@media print{@media (min-width:980px){a{color:red}}}}'
+      ]
+    })
+  )
+  .addBatch(
+    optimizerContext('advanced on & aggressive merging on', {
+      'repeated' : [
+        'a{color:red;color:red}',
+        'a{color:red}'
+      ],
+      'duplicates - same context': [
+        'a{color:red}div{color:blue}a{color:red}',
+        'div{color:blue}a{color:red}'
+      ],
+      'duplicates - different contexts': [
+        'a{color:red}div{color:blue}@media screen{a{color:red}}',
+        'a{color:red}div{color:blue}@media screen{a{color:red}}'
+      ],
+      'adjacent': [
+        'a{color:red}a{display:block;width:100px}div{color:#fff}',
+        'a{color:red;display:block;width:100px}div{color:#fff}'
+      ],
+      'non-adjacent': [
+        'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}',
+        'a{display:block}.one{font-size:12px}a{color:#fff;margin:2px}'
+      ],
+      'non-adjacent with multi selectors': [
+        'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}',
+        'a{margin:0}.one{color:red}a,p{color:red;padding:0}'
+      ]
+    }, { noAdvanced: false, aggressiveMerging: true })
+  )
+  .addBatch(
+    optimizerContext('advanced on & aggressive merging off', {
+      'repeated' : [
+        'a{color:red;color:red}',
+        'a{color:red}'
+      ],
+      'non-adjacent with multi selectors': [
+        'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}',
+        'a{padding:10px;margin:0}.one{color:red}a,p{color:red;padding:0}'
+      ]
+    }, { noAdvanced: false, aggressiveMerging: false })
+  )
+  .addBatch(
+    optimizerContext('advanced off', {
+      'repeated' : [
+        'a{color:red;color:red}',
+        'a{color:red;color:red}'
+      ],
+      'duplicates - same context': [
+        'a{color:red}div{color:blue}a{color:red}',
+        'a{color:red}div{color:blue}a{color:red}'
+      ],
+      'duplicates - different contexts': [
+        'a{color:red}div{color:blue}@media screen{a{color:red}}',
+        'a{color:red}div{color:blue}@media screen{a{color:red}}'
+      ],
+      'adjacent': [
+        'a{color:red}a{display:block;width:100px}div{color:#fff}',
+        'a{color:red}a{display:block;width:100px}div{color:#fff}'
+      ],
+      'non-adjacent': [
+        'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}',
+        'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}'
+      ]
+    }, { noAdvanced: true })
+  )
+  .export(module);
diff --git a/test/selectors/optimizers/simple-test.js b/test/selectors/optimizers/simple-test.js
new file mode 100644 (file)
index 0000000..de1717b
--- /dev/null
@@ -0,0 +1,46 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var Tokenizer = require('../../../lib/selectors/tokenizer');
+var SimpleOptimizer = require('../../../lib/selectors/optimizers/simple');
+
+function selectorContext(specs) {
+  var context = {};
+
+  function optimized(selectors) {
+    return function (source) {
+      var tokens = new Tokenizer().toTokens(source);
+      new SimpleOptimizer({}).optimize(tokens);
+
+      assert.deepEqual(tokens[0].selector, selectors);
+    };
+  }
+
+  for (var name in specs) {
+    context['selector - ' + name] = {
+      topic: specs[name][0],
+      optimized: optimized(specs[name][1])
+    };
+  }
+
+  return context;
+}
+
+vows.describe(SimpleOptimizer)
+  .addBatch(
+    selectorContext({
+      'optimized': [
+        'a{}',
+        ['a']
+      ],
+      'whitespace': [
+        ' div  > span{}',
+        ['div>span']
+      ],
+      'line breaks': [
+        ' div  >\n\r\n span{}',
+        ['div>span']
+      ]
+    })
+  )
+  .export(module);