Unifies all stringifying code.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 8 Apr 2015 07:26:08 +0000 (08:26 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Sun, 12 Apr 2015 11:15:29 +0000 (12:15 +0100)
Instead of having 3 of them (simple, source maps, and advanced
optimizations helpers), we have one now with a slightly different
storing logic for source maps.

15 files changed:
lib/clean.js
lib/selectors/extractor.js
lib/selectors/optimizer.js
lib/selectors/optimizers/advanced.js
lib/selectors/source-map-stringifier.js [deleted file]
lib/selectors/stringifier.js [deleted file]
lib/stringifier/helpers.js [new file with mode: 0644]
lib/stringifier/one-time.js [new file with mode: 0644]
lib/stringifier/simple.js [new file with mode: 0644]
lib/stringifier/source-maps.js [new file with mode: 0644]
lib/utils/stringify-tokens.js [deleted file]
test/fixtures/font-awesome-min.css
test/fixtures/issue-117-snippet-min.css
test/fixtures/issue-241-min.css
test/selectors/optimizer-test.js

index 73a6bc3..2fa5563 100644 (file)
@@ -8,8 +8,9 @@
 var ImportInliner = require('./imports/inliner');
 var UrlRebase = require('./images/url-rebase');
 var SelectorsOptimizer = require('./selectors/optimizer');
-var Stringifier = require('./selectors/stringifier');
-var SourceMapStringifier = require('./selectors/source-map-stringifier');
+
+var simpleStringify = require('./stringifier/simple');
+var sourceMapStringify = require('./stringifier/source-maps');
 
 var CommentsProcessor = require('./text/comments-processor');
 var ExpressionsProcessor = require('./text/expressions-processor');
@@ -162,7 +163,7 @@ function minify(context, data) {
 
   var urlRebase = new UrlRebase(context);
   var selectorsOptimizer = new SelectorsOptimizer(options, context);
-  var stringifierClass = options.sourceMap ? SourceMapStringifier : Stringifier;
+  var stringify = options.sourceMap ? sourceMapStringify : simpleStringify;
 
   var run = function (processor, action) {
     data = typeof processor == 'function' ?
@@ -179,15 +180,13 @@ function minify(context, data) {
   run(freeTextProcessor, 'escape');
 
   run(function() {
-    var stringifier = new stringifierClass(options, function (data) {
+    return selectorsOptimizer.process(data, stringify, function (data) {
       data = freeTextProcessor.restore(data);
       data = urlsProcessor.restore(data);
       data = options.rebase ? urlRebase.process(data) : data;
       data = expressionsProcessor.restore(data);
       return commentsProcessor.restore(data);
     }, sourceMapTracker);
-
-    return selectorsOptimizer.process(data, stringifier);
   });
 
   return data;
index 877fc68..08ae0ca 100644 (file)
@@ -2,14 +2,14 @@
 // IMPORTANT: Mind Token class and this code is not related!
 // Properties will be tokenized in one step, see #429
 
-var stringifySelector = require('../utils/stringify-tokens').selector;
-var stringifyValue = require('../utils/stringify-tokens').value;
+var stringifySelectors = require('../stringifier/one-time').selectors;
+var stringifyValue = require('../stringifier/one-time').value;
 
 function extract(token) {
   var properties = [];
 
   if (token[0] == 'selector') {
-    var inSimpleSelector = !/[\.\+#>~\s]/.test(stringifySelector(token[1]));
+    var inSimpleSelector = !/[\.\+#>~\s]/.test(stringifySelectors(token[1]));
     for (var i = 0, l = token[2].length; i < l; i++) {
       var property = token[2][i];
 
@@ -20,7 +20,7 @@ function extract(token) {
       if (name.length === 0)
         continue;
 
-      var value = stringifyValue(token[2][i]);
+      var value = stringifyValue(token[2], i);
 
       properties.push([
         name,
index a7fa21f..3cfc6f8 100644 (file)
@@ -8,7 +8,7 @@ function SelectorsOptimizer(options, context) {
   this.context = context || {};
 }
 
-SelectorsOptimizer.prototype.process = function (data, stringifier) {
+SelectorsOptimizer.prototype.process = function (data, stringify, restoreCallback, sourceMapTracker) {
   var tokens = new Tokenizer(this.context, this.options.sourceMap).toTokens(data);
 
   addOptimizationMetadata(tokens);
@@ -17,7 +17,7 @@ SelectorsOptimizer.prototype.process = function (data, stringifier) {
   if (this.options.advanced)
     new AdvancedOptimizer(this.options, this.context).optimize(tokens);
 
-  return stringifier.toString(tokens);
+  return stringify(tokens, this.options, restoreCallback, sourceMapTracker);
 };
 
 module.exports = SelectorsOptimizer;
index 4502de8..d4b0452 100644 (file)
@@ -4,8 +4,8 @@ var CleanUp = require('./clean-up');
 var extractProperties = require('../extractor');
 var canReorder = require('../reorderable').canReorder;
 var canReorderSingle = require('../reorderable').canReorderSingle;
-var stringifyBody = require('../../utils/stringify-tokens').body;
-var stringifySelector = require('../../utils/stringify-tokens').selector;
+var stringifyBody = require('../../stringifier/one-time').body;
+var stringifySelectors = require('../../stringifier/one-time').selectors;
 
 function AdvancedOptimizer(options) {
   this.options = options;
@@ -34,7 +34,7 @@ AdvancedOptimizer.prototype.removeDuplicates = function (tokens) {
     if (token[0] != 'selector')
       continue;
 
-    id = stringifySelector(token[1]);
+    id = stringifySelectors(token[1]);
 
     if (matched[id] && matched[id].length == 1)
       moreThanOnce.push(id);
@@ -72,13 +72,13 @@ AdvancedOptimizer.prototype.mergeAdjacent = function (tokens) {
       continue;
     }
 
-    if (lastToken[0] == 'selector' && stringifySelector(token[1]) == stringifySelector(lastToken[1])) {
+    if (lastToken[0] == 'selector' && stringifySelectors(token[1]) == stringifySelectors(lastToken[1])) {
       var joinAt = [lastToken[2].length];
       Array.prototype.push.apply(lastToken[2], token[2]);
       optimizeProperties(token[1], lastToken[2], joinAt, this.options);
       token[2] = [];
     } else if (lastToken[0] == 'selector' && stringifyBody(token[2]) == stringifyBody(lastToken[2]) &&
-        !this.isSpecial(stringifySelector(token[1])) && !this.isSpecial(stringifySelector(lastToken[1]))) {
+        !this.isSpecial(stringifySelectors(token[1])) && !this.isSpecial(stringifySelectors(lastToken[1]))) {
       lastToken[1] = CleanUp.selectors(lastToken[1].concat(token[1]), false, adjacentSpace);
       token[2] = [];
     } else {
@@ -99,7 +99,7 @@ AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) {
     if (token[2].length === 0)
       continue;
 
-    var selectorAsString = stringifySelector(token[1]);
+    var selectorAsString = stringifySelectors(token[1]);
     var isComplexAndNotSpecial = token[1].length > 1 && !this.isSpecial(selectorAsString);
     var selectors = isComplexAndNotSpecial ?
       [selectorAsString].concat(token[1]) :
@@ -251,7 +251,7 @@ AdvancedOptimizer.prototype.mergeNonAdjacentBySelector = function (tokens) {
     if (tokens[i][2].length === 0)
       continue;
 
-    var selector = stringifySelector(tokens[i][1]);
+    var selector = stringifySelectors(tokens[i][1]);
     allSelectors[selector] = [i].concat(allSelectors[selector] || []);
 
     if (allSelectors[selector].length == 2)
@@ -317,11 +317,11 @@ AdvancedOptimizer.prototype.mergeNonAdjacentByBody = function (tokens) {
     if (token[0] != 'selector')
       continue;
 
-    if (token[2].length > 0 && unsafeSelector(stringifySelector(token[1])))
+    if (token[2].length > 0 && unsafeSelector(stringifySelectors(token[1])))
       candidates = {};
 
     var oldToken = candidates[stringifyBody(token[2])];
-    if (oldToken && !this.isSpecial(stringifySelector(token[1])) && !this.isSpecial(stringifySelector(oldToken[1]))) {
+    if (oldToken && !this.isSpecial(stringifySelectors(token[1])) && !this.isSpecial(stringifySelectors(oldToken[1]))) {
       token[1] = CleanUp.selectors(oldToken[1].concat(token[1]), false, adjacentSpace);
 
       oldToken[2] = [];
@@ -383,7 +383,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
   function cacheId(cachedTokens) {
     var id = [];
     for (var i = 0, l = cachedTokens.length; i < l; i++) {
-      id.push(stringifySelector(cachedTokens[i][1]));
+      id.push(stringifySelectors(cachedTokens[i][1]));
     }
     return id.join(ID_JOIN_CHARACTER);
   }
@@ -393,7 +393,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
     var mergeableTokens = [];
 
     for (var i = sourceTokens.length - 1; i >= 0; i--) {
-      if (self.isSpecial(stringifySelector(sourceTokens[i][1])))
+      if (self.isSpecial(stringifySelectors(sourceTokens[i][1])))
         continue;
 
       mergeableTokens.unshift(sourceTokens[i]);
@@ -457,7 +457,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
   function sizeDifference(tokensVariant, propertySize, propertiesCount) {
     var allSelectorsSize = 0;
     for (var i = tokensVariant.length - 1; i >= 0; i--) {
-      allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? stringifySelector(tokensVariant[i][1]).length : -1;
+      allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? stringifySelectors(tokensVariant[i][1]).length : -1;
     }
     return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1;
   }
@@ -664,13 +664,21 @@ AdvancedOptimizer.prototype.mergeMediaQueries = function (tokens) {
 AdvancedOptimizer.prototype.removeEmpty = function (tokens) {
   for (var i = 0, l = tokens.length; i < l; i++) {
     var token = tokens[i];
+    var isEmpty = false;
 
-    if (token[0] == 'selector' && (token[1].length === 0 || token[2].length === 0)) {
+    switch (token[0]) {
+      case 'selector':
+        isEmpty = token[1].length === 0 || token[2].length === 0;
+        break;
+      case 'block':
+        this.removeEmpty(token[2]);
+        isEmpty = token[2].length === 0;
+    }
+
+    if (isEmpty) {
       tokens.splice(i, 1);
       i--;
       l--;
-    } else if (token[1] == 'block') {
-      this.removeEmpty(token[2]);
     }
   }
 };
diff --git a/lib/selectors/source-map-stringifier.js b/lib/selectors/source-map-stringifier.js
deleted file mode 100644 (file)
index 5851919..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-var SourceMapGenerator = require('source-map').SourceMapGenerator;
-
-var lineBreak = require('os').EOL;
-var unknownSource = '$stdin';
-
-function hasMoreProperties(elements, index) {
-  for (var i = index, l = elements.length; i < l; i++) {
-    if (typeof elements[i] != 'string')
-      return true;
-  }
-
-  return false;
-}
-
-function Rebuilder(options, restoreCallback, inputMapTracker) {
-  this.column = 0;
-  this.line = 1;
-  this.output = [];
-  this.keepBreaks = options.keepBreaks;
-  this.sourceMapInlineSources = options.sourceMapInlineSources;
-  this.restore = restoreCallback;
-  this.inputMapTracker = inputMapTracker;
-  this.outputMap = new SourceMapGenerator();
-}
-
-Rebuilder.prototype.rebuildSelectors = function (elements) {
-  for (var i = 0, l = elements.length; i < l; i++) {
-    var element = elements[i];
-    this.store(element);
-
-    if (i < l - 1)
-      this.store(',');
-  }
-};
-
-Rebuilder.prototype.rebuildBody = function (elements) {
-  for (var i = 0, l = elements.length; i < l; i++) {
-    var element = elements[i];
-
-    if (typeof element == 'string') {
-      this.store(element);
-      continue;
-    }
-
-    for (var j = 0, m = element.length; j < m; j++) {
-      this.store(element[j]);
-
-      if (j == m - 1 && element[0][1])
-        this.store('!important');
-
-      if (j === 0) {
-        this.store(':');
-      } else if (j < m - 1) {
-        this.store(' ');
-      } else if (i < l - 1 && hasMoreProperties(elements, i + 1)) {
-        this.store(';');
-      }
-    }
-  }
-};
-
-Rebuilder.prototype.store = function (element) {
-  // handles defaults and values like `,` or `/` which do not have mapping
-  if (Array.isArray(element) && element.length == 1)
-    element = element[0];
-
-  var fromString = typeof element == 'string';
-  var value = fromString ? element : element[0];
-
-  if (value.indexOf('_') > -1)
-    value = this.restore(value);
-
-  this.track(value, fromString ? null : element);
-  this.output.push(value);
-};
-
-Rebuilder.prototype.rebuildList = function (tokens, isFlatBlock) {
-  var joinCharacter = isFlatBlock ? ';' : (this.keepBreaks ? lineBreak : '');
-
-  for (var i = 0, l = tokens.length; i < l; i++) {
-    var token = tokens[i];
-
-    switch (token[0]) {
-      case 'at-rule':
-      case 'text':
-        this.store(token[1][0]);
-        break;
-      case 'block':
-        this.rebuildSelectors([token[1]]);
-        this.store('{');
-        this.rebuildList(token[2], false);
-        this.store('}');
-        this.store(joinCharacter);
-        break;
-      case 'flat-block':
-        this.rebuildSelectors([token[1]]);
-        this.store('{');
-        this.rebuildBody(token[2]);
-        this.store('}');
-        this.store(joinCharacter);
-        break;
-      default:
-        this.rebuildSelectors(token[1]);
-        this.store('{');
-        this.rebuildBody(token[2]);
-        this.store('}');
-        this.store(joinCharacter);
-    }
-  }
-};
-
-Rebuilder.prototype.track = function (value, element) {
-  if (element)
-    this.trackMetadata(element);
-
-  var parts = value.split('\n');
-  this.line += parts.length - 1;
-  this.column = parts.length > 1 ? 0 : (this.column + parts.pop().length);
-};
-
-Rebuilder.prototype.trackMetadata = function (element) {
-  var sourceAt = element.length - 1;
-  if (typeof element[sourceAt] == 'object')
-    sourceAt--;
-
-  var source = element[sourceAt] || unknownSource;
-
-  this.outputMap.addMapping({
-    generated: {
-      line: this.line,
-      column: this.column
-    },
-    source: source,
-    original: {
-      line: element[sourceAt - 2],
-      column: element[sourceAt - 1]
-    }
-  });
-
-  if (element[sourceAt + 1])
-    this.outputMap.setSourceContent(source, element[sourceAt + 1][element[sourceAt]]);
-};
-
-function SourceMapStringifier(options, restoreCallback, inputMapTracker) {
-  this.rebuilder = new Rebuilder(options, restoreCallback, inputMapTracker);
-}
-
-SourceMapStringifier.prototype.toString = function (tokens) {
-  this.rebuilder.rebuildList(tokens);
-
-  return {
-    sourceMap: this.rebuilder.outputMap,
-    styles: this.rebuilder.output.join('').trim()
-  };
-};
-
-module.exports = SourceMapStringifier;
diff --git a/lib/selectors/stringifier.js b/lib/selectors/stringifier.js
deleted file mode 100644 (file)
index ce57f93..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-var lineBreak = require('os').EOL;
-
-function Stringifier(options, restoreCallback) {
-  this.keepBreaks = options.keepBreaks;
-  this.restoreCallback = restoreCallback;
-}
-
-function selectorRebuilder(elements) {
-  var merged = '';
-
-  for (var i = 0, l = elements.length; i < l; i++) {
-    merged += elements[i] + (i < l - 1 ? ',' : '');
-  }
-
-  return merged;
-}
-
-function bodyRebuilder(elements) {
-  var merged = '';
-  var element, important, lastSemicolonAt, value, valueLastChar, shouldSkipSpaceAfter;
-
-  for (var i = 0, l = elements.length; i < l; i++) {
-    element = elements[i];
-
-    if (typeof element == 'string' && element.indexOf('__ESCAPED_') === 0) {
-      merged += element;
-
-      if (i === l - 1) {
-        lastSemicolonAt = merged.lastIndexOf(';');
-        merged = merged.substring(0, lastSemicolonAt) + merged.substring(lastSemicolonAt + 1);
-      }
-    } else {
-      important = element[0][1];
-      merged += element[0][0] + ':';
-
-      for (var j = 1, m = element.length; j < m; j++) {
-        value = element[j][0];
-        valueLastChar = value[value.length - 1];
-
-        if (value == ',' || value == '/') {
-          if (merged[merged.length - 1] == ')')
-            merged += value;
-          else
-            merged = merged.substring(0, merged.length - 1) + value;
-        } else {
-          shouldSkipSpaceAfter = j == m - 1 || valueLastChar == ')' && value.indexOf('progid') == -1;
-          merged += value + (shouldSkipSpaceAfter ? '' : ' ');
-        }
-      }
-
-      if (important)
-        merged += '!important';
-
-      merged += (i < l - 1 ? ';' : '');
-    }
-  }
-
-  return merged;
-}
-
-function rebuild(tokens, keepBreaks, isFlatBlock) {
-  var joinCharacter = isFlatBlock ? ';' : (keepBreaks ? lineBreak : '');
-  var parts = [];
-  var body;
-  var selector;
-
-  for (var i = 0, l = tokens.length; i < l; i++) {
-    var token = tokens[i];
-
-    switch (token[0]) {
-      case 'at-rule':
-      case 'text':
-        parts.push(token[1][0]);
-        break;
-      case 'block':
-        body = rebuild(token[2], keepBreaks, false);
-        if (body.length > 0)
-          parts.push(token[1][0] + '{' + body + '}');
-        break;
-      case 'flat-block':
-        body = bodyRebuilder(token[2]);
-        if (body.length > 0)
-          parts.push(token[1][0] + '{' + body + '}');
-        break;
-      default:
-        selector = selectorRebuilder(token[1]);
-        body = bodyRebuilder(token[2]);
-        parts.push(selector + '{' + body + '}');
-    }
-  }
-
-  return parts.join(joinCharacter);
-}
-
-Stringifier.prototype.toString = function (tokens) {
-  var rebuilt = rebuild(tokens, this.keepBreaks, false);
-
-  return {
-    styles: this.restoreCallback(rebuilt).trim()
-  };
-};
-
-module.exports = Stringifier;
diff --git a/lib/stringifier/helpers.js b/lib/stringifier/helpers.js
new file mode 100644 (file)
index 0000000..d25fede
--- /dev/null
@@ -0,0 +1,136 @@
+var lineBreak = require('os').EOL;
+
+function hasMoreProperties(tokens, index) {
+  for (var i = index, l = tokens.length; i < l; i++) {
+    if (typeof tokens[i] != 'string')
+      return true;
+  }
+
+  return false;
+}
+
+function afterClosingBrace(token, valueIndex) {
+  return token[valueIndex][0][token[valueIndex][0].length - 1] == ')';
+}
+
+function afterComma(token, valueIndex) {
+  return token[valueIndex][0] == ',';
+}
+
+function afterSlash(token, valueIndex) {
+  return token[valueIndex][0] == '/';
+}
+
+function beforeComma(token, valueIndex) {
+  return token[valueIndex + 1] && token[valueIndex + 1][0] == ',';
+}
+
+function beforeSlash(token, valueIndex) {
+  return token[valueIndex + 1] && token[valueIndex + 1][0] == '/';
+}
+
+function inFilter(token) {
+  return token[0][0] == 'filter' || token[0][0] == '-ms-filter';
+}
+
+function inSpecialContext(token, valueIndex) {
+  return afterClosingBrace(token, valueIndex) ||
+    beforeSlash(token, valueIndex) ||
+    afterSlash(token, valueIndex) ||
+    beforeComma(token, valueIndex) ||
+    afterComma(token, valueIndex);
+}
+
+function selectors(tokens, context) {
+  var store = context.store;
+
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    store(tokens[i], context);
+
+    if (i < l - 1)
+      store(',', context);
+  }
+}
+
+function body(tokens, context) {
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    property(tokens, i, i == l - 1, context);
+  }
+}
+
+function property(tokens, position, isLast, context) {
+  var store = context.store;
+  var token = tokens[position];
+
+  if (typeof token == 'string') {
+    store(token, context);
+  } else {
+    store(token[0], context);
+    store(':', context);
+    value(tokens, position, isLast, context);
+  }
+}
+
+function value(tokens, position, isLast, context) {
+  var store = context.store;
+  var token = tokens[position];
+  var isImportant = token[0][1];
+
+  for (var j = 1, m = token.length; j < m; j++) {
+    store(token[j], context);
+
+    if (j == m - 1 && isImportant)
+      store('!important', context);
+
+    if (j < m - 1 && (inFilter(token) || !inSpecialContext(token, j))) {
+      store(' ', context);
+    } else if (j == m - 1 && !isLast && hasMoreProperties(tokens, position + 1)) {
+      store(';', context);
+    }
+  }
+}
+
+function all(tokens, context) {
+  var joinCharacter = context.keepBreaks ? lineBreak : '';
+  var store = context.store;
+
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    var token = tokens[i];
+
+    switch (token[0]) {
+      case 'at-rule':
+      case 'text':
+        store(token[1][0], context);
+        store(joinCharacter, context);
+        break;
+      case 'block':
+        selectors([token[1]], context);
+        store('{', context);
+        all(token[2], context);
+        store('}', context);
+        store(joinCharacter, context);
+        break;
+      case 'flat-block':
+        selectors([token[1]], context);
+        store('{', context);
+        body(token[2], context);
+        store('}', context);
+        store(joinCharacter, context);
+        break;
+      default:
+        selectors(token[1], context);
+        store('{', context);
+        body(token[2], context);
+        store('}', context);
+        store(joinCharacter, context);
+    }
+  }
+}
+
+module.exports = {
+  all: all,
+  body: body,
+  property: property,
+  selectors: selectors,
+  value: value
+};
diff --git a/lib/stringifier/one-time.js b/lib/stringifier/one-time.js
new file mode 100644 (file)
index 0000000..2e8d8da
--- /dev/null
@@ -0,0 +1,36 @@
+var helpers = require('./helpers');
+
+function store(token, context) {
+  context.output.push(typeof token == 'string' ? token : token[0]);
+}
+
+function context() {
+  return {
+    output: [],
+    store: store
+  };
+}
+
+function body(tokens) {
+  var fakeContext = context();
+  helpers.body(tokens, fakeContext);
+  return fakeContext.output.join('');
+}
+
+function selectors(tokens) {
+  var fakeContext = context();
+  helpers.selectors(tokens, fakeContext);
+  return fakeContext.output.join('');
+}
+
+function value(tokens, position) {
+  var fakeContext = context();
+  helpers.value(tokens, position, true, fakeContext);
+  return fakeContext.output.join('');
+}
+
+module.exports = {
+  body: body,
+  selectors: selectors,
+  value: value
+};
diff --git a/lib/stringifier/simple.js b/lib/stringifier/simple.js
new file mode 100644 (file)
index 0000000..80a32db
--- /dev/null
@@ -0,0 +1,21 @@
+var all = require('./helpers').all;
+
+function store(token, context) {
+  context.output.push(typeof token == 'string' ? token : token[0]);
+}
+
+function stringify(tokens, options, restoreCallback) {
+  var context = {
+    keepBreaks: options.keepBreaks,
+    output: [],
+    store: store
+  };
+
+  all(tokens, context, false);
+
+  return {
+    styles: restoreCallback(context.output.join('')).trim()
+  };
+}
+
+module.exports = stringify;
diff --git a/lib/stringifier/source-maps.js b/lib/stringifier/source-maps.js
new file mode 100644 (file)
index 0000000..dc26fd7
--- /dev/null
@@ -0,0 +1,74 @@
+var SourceMapGenerator = require('source-map').SourceMapGenerator;
+var all = require('./helpers').all;
+
+var unknownSource = '$stdin';
+
+function store(element, context) {
+  // handles defaults and values like `,` or `/` which do not have mapping
+  if (Array.isArray(element) && element.length == 1)
+    element = element[0];
+
+  var fromString = typeof element == 'string';
+  var value = fromString ? element : element[0];
+
+  if (value.indexOf('_') > -1)
+    value = context.restore(value);
+
+  track(value, fromString ? null : element, context);
+  context.output.push(value);
+}
+
+function track(value, element, context) {
+  if (element)
+    trackMetadata(element, context);
+
+  var parts = value.split('\n');
+  context.line += parts.length - 1;
+  context.column = parts.length > 1 ? 0 : (context.column + parts.pop().length);
+}
+
+function trackMetadata(element, context) {
+  var sourceAt = element.length - 1;
+  if (typeof element[sourceAt] == 'object')
+    sourceAt--;
+
+  var source = element[sourceAt] || unknownSource;
+
+  context.outputMap.addMapping({
+    generated: {
+      line: context.line,
+      column: context.column
+    },
+    source: source,
+    original: {
+      line: element[sourceAt - 2],
+      column: element[sourceAt - 1]
+    }
+  });
+
+  if (element[sourceAt + 1])
+    context.outputMap.setSourceContent(source, element[sourceAt + 1][element[sourceAt]]);
+}
+
+function stringify(tokens, options, restoreCallback, inputMapTracker) {
+  var context = {
+    column: 0,
+    inputMapTracker: inputMapTracker,
+    keepBreaks: options.keepBreaks,
+    line: 1,
+    output: [],
+    outputMap: new SourceMapGenerator(),
+    restore: restoreCallback,
+    sourceMapInlineSources: options.sourceMapInlineSources,
+    store: store
+  };
+
+  all(tokens, context, false);
+
+  return {
+    sourceMap: context.outputMap,
+    styles: context.output.join('').trim()
+  };
+}
+
+module.exports = stringify;
diff --git a/lib/utils/stringify-tokens.js b/lib/utils/stringify-tokens.js
deleted file mode 100644 (file)
index b77a0b0..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-function stringifyValue(property) {
-  var result = '';
-  for (var i = 1, l = property.length; i < l; i++) {
-    result += property[i][0] + (i < l - 1 ? ' ' : '');
-  }
-
-  return result;
-}
-
-function stringifyBody(properties) {
-  var result = '';
-  for (var i = 0, l = properties.length; i < l; i++) {
-    var property = properties[i];
-
-    result += property[0][0] + ':';
-    for (var j = 1, m = property.length; j < m; j++) {
-      result += property[j][0] + (j < m - 1 ? ' ' : '');
-    }
-
-    result += (i < l - 1 ? ';' : '');
-  }
-
-  return result;
-}
-
-function stringifySelector(list) {
-  var result = '';
-  for (var i = 0, l = list.length; i < l; i++) {
-    result += list[i][0] + (i < l - 1 ? ',' : '');
-  }
-
-  return result;
-}
-
-module.exports = {
-  value: stringifyValue,
-  body: stringifyBody,
-  selector: stringifySelector
-};
index 32ad134..104c8a4 100644 (file)
@@ -54,19 +54,25 @@ ul.icons li [class*=" icon-"],ul.icons li [class^=icon-]{width:.75em}
 .btn.btn-large [class*=" icon-"].pull-right.icon-2x,.btn.btn-large [class^=icon-].pull-right.icon-2x{margin-left:.2em}
 .icon-spin{-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}
 @-moz-keyframes spin{0%{-moz-transform:rotate(0)}
-100%{-moz-transform:rotate(359deg)}}
+100%{-moz-transform:rotate(359deg)}
+}
 @-webkit-keyframes spin{0%{-webkit-transform:rotate(0)}
-100%{-webkit-transform:rotate(359deg)}}
+100%{-webkit-transform:rotate(359deg)}
+}
 @-o-keyframes spin{0%{-o-transform:rotate(0)}
-100%{-o-transform:rotate(359deg)}}
+100%{-o-transform:rotate(359deg)}
+}
 @-ms-keyframes spin{0%{-ms-transform:rotate(0)}
-100%{-ms-transform:rotate(359deg)}}
+100%{-ms-transform:rotate(359deg)}
+}
 @keyframes spin{0%{transform:rotate(0)}
-100%{transform:rotate(359deg)}}
+100%{transform:rotate(359deg)}
+}
 @-moz-document url-prefix(){.icon-spin{height:.9em}
 .btn .icon-spin{height:auto}
 .icon-spin.icon-large{height:1.25em}
-.btn .icon-spin.icon-large{height:.75em}}
+.btn .icon-spin.icon-large{height:.75em}
+}
 .icon-glass:before{content:"\f000"}
 .icon-music:before{content:"\f001"}
 .icon-search:before{content:"\f002"}
@@ -315,4 +321,4 @@ ul.icons li [class*=" icon-"],ul.icons li [class^=icon-]{width:.75em}
 .icon-reply:before{content:"\f112"}
 .icon-github-alt:before{content:"\f113"}
 .icon-folder-close-alt:before{content:"\f114"}
-.icon-folder-open-alt:before{content:"\f115"}
\ No newline at end of file
+.icon-folder-open-alt:before{content:"\f115"}
index bb80449..98320d1 100644 (file)
@@ -1,3 +1,4 @@
 @media only print{a,a:visited{text-decoration:underline}
 a[href]:after{content:" (" attr(href)")"}
-abbr[title]:after{content:" (" attr(title)")"}}
\ No newline at end of file
+abbr[title]:after{content:" (" attr(title)")"}
+}
index 2ca7642..1fd155f 100644 (file)
@@ -1,2 +1,3 @@
 .c4:not(.c1,.c2){width:1px}
-@media (min-width:1px){.c3{width:1px}}
+@media (min-width:1px){.c3{width:1px}
+}
index fd8e59a..06f22f2 100644 (file)
@@ -1,12 +1,14 @@
 var vows = require('vows');
 var assert = require('assert');
 var SelectorsOptimizer = require('../../lib/selectors/optimizer');
-var Stringifier = require('../../lib/selectors/stringifier');
+var stringify = require('../../lib/stringifier/simple');
 var Compatibility = require('../../lib/utils/compatibility');
 var SourceTracker = require('../../lib/utils/source-tracker');
 
 function optimizerContext(group, specs, options) {
-  var stringifier = new Stringifier(false, function (data) { return data; });
+  function restoreCallback(data) {
+    return data;
+  }
 
   var context = {};
   options = options || {};
@@ -20,7 +22,7 @@ function optimizerContext(group, specs, options) {
 
   function optimized(target) {
     return function (source) {
-      assert.equal(new SelectorsOptimizer(options, outerContext).process(source, stringifier).styles, target);
+      assert.equal(new SelectorsOptimizer(options, outerContext).process(source, stringify, restoreCallback).styles, target);
     };
   }