Reworks simple & advanced optimisations to use metadata.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Thu, 23 Oct 2014 21:46:21 +0000 (22:46 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 8 Dec 2014 09:39:14 +0000 (09:39 +0000)
* We can avoid merging, splitting, and mapping by using token metadata directly.
* Unfortunately it means metadata has to be updated as we go.

lib/properties/optimizer.js
lib/properties/token.js
lib/selectors/optimizer.js
lib/selectors/optimizers/advanced.js
lib/selectors/optimizers/clean-up.js
lib/selectors/optimizers/simple.js
lib/selectors/tokenizer.js
test/selectors/tokenizer-test.js

index 7f61761..c44eb32 100644 (file)
@@ -237,19 +237,23 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) {
   };
 
   var rebuild = function(tokens) {
-    var flat = [];
+    var tokenized = [];
+    var list = [];
     var eligibleForCompacting = false;
 
     for (var i = 0, l = tokens.length; i < l; i++) {
       if (!eligibleForCompacting && processableInfo.implementedFor.test(tokens[i][0]))
         eligibleForCompacting = true;
 
-      flat.push({ value: tokens[i][0] + ':' + tokens[i][1] });
+      var property = tokens[i][0] + ':' + tokens[i][1];
+      tokenized.push({ value: property });
+      list.push(property);
     }
 
     return {
-      value: flat,
-      compactFurther: eligibleForCompacting
+      compactFurther: eligibleForCompacting,
+      list: list,
+      tokenized: tokenized
     };
   };
 
@@ -272,8 +276,8 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) {
       var rebuilt = rebuild(optimized);
 
       return compactProperties && rebuilt.compactFurther ?
-        compact(rebuilt.value) :
-        rebuilt.value;
+        compact(rebuilt.tokenized) :
+        rebuilt;
     }
   };
 };
index bb4b8c7..333d43f 100644 (file)
@@ -110,7 +110,8 @@ module.exports = (function() {
         tokens = [tokens];
       }
 
-      var result = [];
+      var tokenized = [];
+      var list = [];
 
       // This step takes care of putting together the components of shorthands
       // NOTE: this is necessary to do for every shorthand, otherwise we couldn't remove their default values
@@ -124,10 +125,15 @@ module.exports = (function() {
           continue;
         }
 
-        result.push({ value: t.prop + ':' + t.value + (t.isImportant ? important : '') });
+        var property = t.prop + ':' + t.value + (t.isImportant ? important : '');
+        tokenized.push({ value: property });
+        list.push(property);
       }
 
-      return result;
+      return {
+        list: list,
+        tokenized: tokenized
+      };
     };
 
     // Gets the final (detokenized) length of the given tokens
index a65b3bc..0454bd8 100644 (file)
@@ -32,7 +32,7 @@ function rebuild(tokens, keepBreaks, isFlatBlock) {
       continue;
     }
 
-    // TODO: broken due to joining/splitting
+    // FIXME: broken due to joining/splitting
     if (token.body && (token.body.length === 0 || (token.body.length == 1 && token.body[0].value === '')))
       continue;
 
@@ -53,7 +53,7 @@ function rebuild(tokens, keepBreaks, isFlatBlock) {
 }
 
 SelectorsOptimizer.prototype.process = function (data) {
-  var tokens = new Tokenizer(this.context).toTokens(data);
+  var tokens = new Tokenizer(this.context, this.options.advanced).toTokens(data);
 
   new SimpleOptimizer(this.options).optimize(tokens);
   if (this.options.advanced)
index 142b1ed..fd3f998 100644 (file)
@@ -1,6 +1,5 @@
 var PropertyOptimizer = require('../../properties/optimizer');
 var CleanUp = require('./clean-up');
-var Splitter = require('../../utils/splitter');
 
 function AdvancedOptimizer(options, context) {
   this.options = options;
@@ -8,7 +7,17 @@ function AdvancedOptimizer(options, context) {
   this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context);
 }
 
-function valueMapper(object) { return object.value; }
+function changeBodyOf(token, newBody) {
+  token.body = newBody.tokenized;
+  token.metadata.body = newBody.list.join(';');
+  token.metadata.bodiesList = newBody.list;
+}
+
+function changeSelectorOf(token, newSelectors) {
+  token.value = newSelectors.tokenized;
+  token.metadata.selector = newSelectors.list.join(',');
+  token.metadata.selectorsList = newSelectors.list;
+}
 
 AdvancedOptimizer.prototype.isSpecial = function (selector) {
   return this.options.compatibility.selectors.special.test(selector);
@@ -23,7 +32,7 @@ AdvancedOptimizer.prototype.removeDuplicates = function (tokens) {
     if (token.kind != 'selector')
       continue;
 
-    var id = token.body.map(valueMapper).join(';') + '@' + token.value.map(valueMapper).join(',');
+    var id = token.metadata.body + '@' + token.metadata.selector;
     var alreadyMatched = matched[id];
 
     if (alreadyMatched) {
@@ -55,14 +64,19 @@ AdvancedOptimizer.prototype.mergeAdjacent = function (tokens) {
     if (token.kind != 'selector')
       continue;
 
-    // TODO: broken due to joining/splitting
-    if (lastToken.kind == 'selector' && token.value.map(valueMapper).join(',') == lastToken.value.map(valueMapper).join(',')) {
+    if (lastToken.kind == 'selector' && token.metadata.selector == lastToken.metadata.selector) {
       var joinAt = [lastToken.body.length];
-      lastToken.body = this.propertyOptimizer.process(token.value, lastToken.body.concat(token.body), joinAt, true);
+      changeBodyOf(
+        lastToken,
+        this.propertyOptimizer.process(token.value, lastToken.body.concat(token.body), joinAt, true)
+      );
       forRemoval.push(i);
-      // TODO: broken due to joining/splitting
-    } else if (lastToken.body && token.body.map(valueMapper).join(';') == lastToken.body.map(valueMapper).join(';') && !this.isSpecial(token.value.map(valueMapper).join(',')) && !this.isSpecial(lastToken.value.map(valueMapper).join(','), this.options)) {
-      lastToken.value = CleanUp.selectors(lastToken.value.concat(token.value));
+    } else if (lastToken.body && token.metadata.body == lastToken.metadata.body &&
+        !this.isSpecial(token.metadata.selector) && !this.isSpecial(lastToken.metadata.selector)) {
+      changeSelectorOf(
+        lastToken,
+        CleanUp.selectors(lastToken.value.concat(token.value))
+      );
       forRemoval.push(i);
     } else {
       lastToken = token;
@@ -86,10 +100,9 @@ AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) {
     if (token.kind != 'selector')
       continue;
 
-    var complexSelector = token.value;
-    var selectors = complexSelector.length > 1 && !this.isSpecial(complexSelector.map(valueMapper).join(','), this.options) ?
-      [complexSelector.map(valueMapper).join(',')].concat(complexSelector.map(valueMapper)) :
-      [complexSelector.map(valueMapper).join(',')];
+    var selectors = token.value.length > 1 && !this.isSpecial(token.metadata.selector) ?
+      [token.metadata.selector].concat(token.metadata.selectorsList) :
+      [token.metadata.selector];
 
     for (var j = 0, m = selectors.length; j < m; j++) {
       var selector = selectors[j];
@@ -99,10 +112,10 @@ AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) {
       else
         moreThanOnce.push(selector);
 
-      // TODO: broken due to joining/splitting
       candidates[selector].push({
         where: i,
-        partial: selector != complexSelector.map(valueMapper).join(',')
+        partial: selector != token.metadata.selector,
+        list: token.metadata.selectorsList
       });
     }
   }
@@ -130,7 +143,7 @@ AdvancedOptimizer.prototype.reduceSimpleNonAdjacentCases = function (tokens, mat
       },
       callback: function (token, newBody, processedCount, tokenIdx) {
         if (!data[processedCount - tokenIdx - 1].partial) {
-          token.body = newBody;
+          changeBodyOf(token, newBody);
           reduced = true;
         }
       }
@@ -152,10 +165,9 @@ AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, po
     var intoPosition = into[into.length - 1].where;
     var intoToken = tokens[intoPosition];
 
-    // TODO: broken due to joining/splitting
     var selectors = this.isSpecial(complexSelector) ?
       [complexSelector] :
-      new Splitter(',').split(complexSelector);
+      into[0].list;
     var reducedBodies = [];
 
     for (var j = 0, m = selectors.length; j < m; j++) {
@@ -172,17 +184,15 @@ AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, po
         },
         callback: function (token, newBody, processedCount, tokenIdx) {
           if (tokenIdx === 0)
-            reducedBodies.push(newBody.map(valueMapper).join(';'));
+            reducedBodies.push(newBody);
         }
       });
 
-      if (reducedBodies[reducedBodies.length - 1] != reducedBodies[0])
+      if (reducedBodies[reducedBodies.length - 1].list.length != reducedBodies[0].list.length)
         continue allSelectors;
     }
 
-    intoToken.body = reducedBodies[0].split(';').map(function (property) {
-      return { value: property };
-    });
+    intoToken.body = reducedBodies[0].tokenized;
     reduced = true;
   }
 
@@ -191,8 +201,8 @@ AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, po
 
 AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, options) {
   var bodies = [];
+  var bodiesAsList = [];
   var joinsAt = [];
-  var splitBodies = [];
   var processedTokens = [];
 
   for (var j = data.length - 1, m = 0; j >= 0; j--) {
@@ -201,32 +211,33 @@ AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, o
 
     var where = data[j].where;
     var token = tokens[where];
-    var body = token.body;
 
-    bodies = bodies.concat(body);
-    splitBodies.push(body.map(valueMapper));
+    bodies = bodies.concat(token.body);
+    bodiesAsList.push(token.metadata.bodiesList);
     processedTokens.push(where);
   }
 
-  for (j = 0, m = splitBodies.length; j < m; j++) {
-    if (splitBodies[j].length > 0)
-      joinsAt.push((joinsAt[j - 1] || 0) + splitBodies[j].length);
+  for (j = 0, m = bodiesAsList.length; j < m; j++) {
+    if (bodiesAsList[j].length > 0)
+      joinsAt.push((joinsAt[j - 1] || 0) + bodiesAsList[j].length);
   }
 
   var optimizedBody = this.propertyOptimizer.process(selector, bodies, joinsAt, false);
-  var optimizedProperties = optimizedBody;
 
   var processedCount = processedTokens.length;
-  var propertyIdx = optimizedProperties.length - 1;
+  var propertyIdx = optimizedBody.tokenized.length - 1;
   var tokenIdx = processedCount - 1;
 
   while (tokenIdx >= 0) {
-     if ((tokenIdx === 0 || (optimizedProperties[propertyIdx] && splitBodies[tokenIdx].indexOf(optimizedProperties[propertyIdx].value) > -1)) && propertyIdx > -1) {
+     if ((tokenIdx === 0 || (optimizedBody.tokenized[propertyIdx] && bodiesAsList[tokenIdx].indexOf(optimizedBody.tokenized[propertyIdx].value) > -1)) && propertyIdx > -1) {
       propertyIdx--;
       continue;
     }
 
-    var newBody = optimizedProperties.splice(propertyIdx + 1);
+    var newBody = {
+      list: optimizedBody.list.splice(propertyIdx + 1),
+      tokenized: optimizedBody.tokenized.splice(propertyIdx + 1)
+    };
     options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx);
 
     tokenIdx--;
@@ -238,7 +249,10 @@ function optimizeProperties(tokens, propertyOptimizer) {
     var token = tokens[i];
 
     if (token.kind == 'selector') {
-      token.body = propertyOptimizer.process(token.value, token.body, false, true);
+      changeBodyOf(
+        token,
+        propertyOptimizer.process(token.value, token.body, false, true)
+      );
     } else if (token.kind == 'block') {
       optimizeProperties(token.body, propertyOptimizer);
     }
index dbe5fb0..55ef9e8 100644 (file)
@@ -23,9 +23,12 @@ var CleanUp = {
         plain.push(reduced);
     }
 
-    return plain.sort().map(function (selector) {
-      return { value: selector };
-    });
+    var sorted = plain.sort();
+
+    return {
+      list: sorted,
+      tokenized: sorted.map(function (selector) { return { value: selector }; })
+    };
   },
 
   block: function (block) {
index 28e3320..a4a6610 100644 (file)
@@ -23,21 +23,29 @@ function SimpleOptimizer(options) {
     options.roundingPrecision;
   options.precision.multiplier = Math.pow(10, options.precision.value);
   options.precision.regexp = new RegExp('(\\d*\\.\\d{' + (options.precision.value + 1) + ',})px', 'g');
+
+  options.updateMetadata = this.options.advanced;
 }
 
-function removeUnsupported(token, compatibility) {
-  if (compatibility.selectors.ie7Hack)
-    return;
+function removeUnsupported(selectors, options) {
+  if (options.compatibility.selectors.ie7Hack)
+    return false;
 
   var supported = [];
-  for (var i = 0, l = token.value.length; i < l; i++) {
-    var selector = token.value[i];
+  var values = [];
+  for (var i = 0, l = selectors.length; i < l; i++) {
+    var selector = selectors[i];
 
-    if (selector.value.indexOf('*+html ') === -1 && selector.value.indexOf('*:first-child+html ') === -1)
+    if (selector.value.indexOf('*+html ') === -1 && selector.value.indexOf('*:first-child+html ') === -1) {
       supported.push(selector);
+      values.push(selector.value);
+    }
   }
 
-  token.value = supported;
+  return {
+    tokenized: supported,
+    list: values
+  };
 }
 
 var valueMinifiers = {
@@ -171,6 +179,8 @@ function colorMininifier(property, value, compatibility) {
 
 function reduce(body, options) {
   var reduced = [];
+  var properties = [];
+  var newProperty;
 
   for (var i = 0, l = body.length; i < l; i++) {
     var token = body[i].value;
@@ -199,15 +209,21 @@ function reduce(body, options) {
     value = multipleZerosMinifier(property, value);
     value = colorMininifier(property, value, options.compatibility);
 
-    reduced.push({ value: property + ':' + value + (important ? '!important' : '') });
+    newProperty = property + ':' + value + (important ? '!important' : '');
+    reduced.push({ value: newProperty });
+    properties.push(newProperty);
   }
 
-  return reduced;
+  return {
+    tokenized: reduced,
+    list: properties
+  };
 }
 
 SimpleOptimizer.prototype.optimize = function(tokens) {
   var self = this;
   var hasCharset = false;
+  var options = this.options;
 
   function _optimize(tokens) {
     for (var i = 0, l = tokens.length; i < l; i++) {
@@ -217,20 +233,30 @@ SimpleOptimizer.prototype.optimize = function(tokens) {
         break;
 
       if (token.kind == 'selector') {
-        token.value = CleanUp.selectors(token.value);
+        var newSelectors = removeUnsupported(CleanUp.selectors(token.value).tokenized, options);
+        if (newSelectors)
+          token.value = newSelectors.tokenized;
 
-        removeUnsupported(token, self.options.compatibility);
         if (token.value.length === 0) {
           tokens.splice(i, 1);
           i--;
           continue;
         }
-
-        token.body = reduce(token.body, self.options);
+        var newBody = reduce(token.body, self.options);
+        token.body = newBody.tokenized;
+
+        if (options.updateMetadata) {
+          token.metadata.body = newBody.list.join(';');
+          token.metadata.bodiesList = newBody.list;
+          if (newSelectors) {
+            token.metadata.selector = newSelectors.list.join(',');
+            token.metadata.selectorsList = newSelectors.list;
+          }
+        }
       } else if (token.kind == 'block') {
         token.value = CleanUp.block(token.value);
         if (token.isFlatBlock)
-          token.body = reduce(token.body, self.options);
+          token.body = reduce(token.body, self.options).tokenized;
         else
           _optimize(token.body);
       } else if (token.kind == 'text') {
index 0bd21d3..6eeb75a 100644 (file)
@@ -6,8 +6,9 @@ var WHITESPACE = /\s/g;
 var MULTI_WHITESPACE = /\s{2,}/g;
 var WHITESPACE_COMMA = / ?, ?/g;
 
-function Tokenizer(minifyContext) {
+function Tokenizer(minifyContext, addMetadata) {
   this.minifyContext = minifyContext;
+  this.addMetadata = addMetadata;
 }
 
 Tokenizer.prototype.toTokens = function (data) {
@@ -22,7 +23,8 @@ Tokenizer.prototype.toTokens = function (data) {
     mode: 'top',
     chunker: chunker,
     chunk: chunker.next(),
-    outer: this.minifyContext
+    outer: this.minifyContext,
+    addMetadata: this.addMetadata
   };
 
   return tokenize(context);
@@ -31,8 +33,10 @@ Tokenizer.prototype.toTokens = function (data) {
 function valueMapper(property) { return { value: property }; }
 
 function extractProperties(string) {
-  var properties = [];
+  var tokenized = [];
+  var list = [];
   var buffer = [];
+  var property;
   var isWhitespace;
   var wasWhitespace;
   var isSpecial;
@@ -46,8 +50,11 @@ function extractProperties(string) {
     if (current === ';') {
       if (wasWhitespace && buffer[buffer.length - 1] === ' ')
         buffer.pop();
-      if (buffer.length > 0)
-        properties.push({ value: buffer.join('') });
+      if (buffer.length > 0) {
+        property = buffer.join('');
+        tokenized.push({ value: property });
+        list.push(property);
+      }
       buffer = [];
     } else {
       isWhitespace = current === ' ' || current === '\t' || current === '\n';
@@ -73,10 +80,16 @@ function extractProperties(string) {
 
   if (wasWhitespace && buffer[buffer.length - 1] === ' ')
     buffer.pop();
-  if (buffer.length > 0)
-    properties.push({ value: buffer.join('') });
+  if (buffer.length > 0) {
+    property = buffer.join('');
+    tokenized.push({ value: property });
+    list.push(property);
+  }
 
-  return properties;
+  return {
+    list: list,
+    tokenized: tokenized
+  };
 }
 
 function extractSelectors(string) {
@@ -86,7 +99,11 @@ function extractSelectors(string) {
     .replace(WHITESPACE_COMMA, ',')
     .trim();
 
-  return new Splitter(',').split(extracted).map(valueMapper);
+  var selectors = new Splitter(',').split(extracted);
+  return {
+    list: selectors,
+    tokenized: selectors.map(valueMapper)
+  };
 }
 
 function extractBlock(string) {
@@ -192,7 +209,7 @@ function tokenize(context) {
         var specialBody = tokenize(context);
 
         if (typeof specialBody == 'string')
-          specialBody = extractProperties(specialBody);
+          specialBody = extractProperties(specialBody).tokenized;
 
         context.mode = oldMode;
 
@@ -205,16 +222,29 @@ function tokenize(context) {
 
       context.cursor = nextEnd + 2;
     } else if (what == 'bodyStart') {
-      var selector = extractSelectors(chunk.substring(context.cursor, nextSpecial));
+      var selectorData = extractSelectors(chunk.substring(context.cursor, nextSpecial));
 
       oldMode = context.mode;
       context.cursor = nextSpecial + 1;
       context.mode = 'body';
-      var body = extractProperties(tokenize(context));
+      var bodyData = extractProperties(tokenize(context));
 
       context.mode = oldMode;
 
-      tokenized.push({ kind: 'selector', value: selector, body: body });
+      var newToken = {
+        kind: 'selector',
+        value: selectorData.tokenized,
+        body: bodyData.tokenized
+      };
+      if (context.addMetadata) {
+        newToken.metadata = {
+          body: bodyData.list.join(','),
+          bodiesList: bodyData.list,
+          selector: selectorData.list.join(','),
+          selectorsList: selectorData.list
+        };
+      }
+      tokenized.push(newToken);
     } else if (what == 'bodyEnd') {
       // extra closing brace at the top level can be safely ignored
       if (context.mode == 'top') {
index b5e1130..fc3e57f 100644 (file)
@@ -2,12 +2,12 @@ var vows = require('vows');
 var assert = require('assert');
 var Tokenizer = require('../../lib/selectors/tokenizer');
 
-function tokenizerContext(name, specs) {
+function tokenizerContext(name, specs, addMetadata) {
   var ctx = {};
 
   function tokenized(target) {
     return function (source) {
-      var tokenized = new Tokenizer({}).toTokens(source);
+      var tokenized = new Tokenizer({}, addMetadata).toTokens(source);
       assert.deepEqual(target, tokenized);
     };
   }
@@ -204,4 +204,71 @@ vows.describe(Tokenizer)
       ]
     })
   )
+  .addBatch(
+    tokenizerContext('metadata', {
+      'no content': [
+        '',
+        []
+      ],
+      'an escaped content': [
+        '__ESCAPED_COMMENT_CLEAN_CSS0__',
+        [{ kind: 'text', value: '__ESCAPED_COMMENT_CLEAN_CSS0__' }]
+      ],
+      'an empty selector': [
+        'a{}',
+        [{
+          kind: 'selector',
+          value: [{ value: 'a' }],
+          body: [],
+          metadata: {
+            body: '',
+            bodiesList: [],
+            selector: 'a',
+            selectorsList: ['a']
+          }
+        }]
+      ],
+      'a double selector': [
+        'a,\n\ndiv.class > p {color:red}',
+        [{
+          kind: 'selector',
+          value: [{ value: 'a' }, { value: 'div.class > p' }],
+          body: [{ value: 'color:red' }],
+          metadata: {
+            body: 'color:red',
+            bodiesList: ['color:red'],
+            selector: 'a,div.class > p',
+            selectorsList: ['a', 'div.class > p']
+          }
+        }],
+      ],
+      'two selectors': [
+        'a{color:red}div{color:blue}',
+        [
+          {
+            kind: 'selector',
+            value: [{ value: 'a' }],
+            body: [{ value: 'color:red' }],
+            metadata: {
+              body: 'color:red',
+              bodiesList: ['color:red'],
+              selector: 'a',
+              selectorsList: ['a']
+            }
+          },
+          {
+            kind: 'selector',
+            value: [{ value: 'div' }],
+            body: [{ value: 'color:blue' }],
+            metadata: {
+              body: 'color:blue',
+              bodiesList: ['color:blue'],
+              selector: 'div',
+              selectorsList: ['div']
+            }
+          }
+        ]
+      ]
+    }, true)
+  )
   .export(module);