Fixes #188 - Convert CleanCSS to a constructor
authorStephen Mathieson <smath23@gmail.com>
Thu, 5 Dec 2013 17:07:56 +0000 (12:07 -0500)
committerStephen Mathieson <smath23@gmail.com>
Thu, 5 Dec 2013 17:52:30 +0000 (12:52 -0500)
This allows for adding custom methods on the CleanCSS prototype.

lib/clean.js
test/module-test.js

index fdbf67a..385f6e0 100644 (file)
@@ -22,312 +22,316 @@ var UrlsProcessor = require('./text/urls');
 
 var SelectorsOptimizer = require('./selectors/optimizer');
 
-module.exports = function(options) {
-  var lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
-  var stats = {};
-  var context = {
-    errors: [],
-    warnings: []
-  };
-
+var CleanCSS = module.exports = function CleanCSS(options) {
   options = options || {};
+
+  // back compat
+  if (!(this instanceof CleanCSS))
+    return new CleanCSS(options);
+
   options.keepBreaks = options.keepBreaks || false;
 
   //active by default
-  if (options.processImport === undefined)
+  if (undefined === options.processImport)
     options.processImport = true;
 
-  var minify = function(data) {
-    var startedAt;
-    if (options.debug) {
-      startedAt = process.hrtime();
-      stats.originalSize = data.length;
-    }
-
-    var replace = function() {
-      if (typeof arguments[0] == 'function')
-        arguments[0]();
-      else
-        data = data.replace.apply(data, arguments);
-    };
-
-    // replace function
-    if (options.benchmark) {
-      var originalReplace = replace;
-      replace = function(pattern, replacement) {
-        var name = typeof pattern == 'function' ?
-          /function (\w+)\(/.exec(pattern.toString())[1] :
-          pattern;
-
-        var start = process.hrtime();
-        originalReplace(pattern, replacement);
-
-        var itTook = process.hrtime(start);
-        console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000);
-      };
-    }
-
-    var commentsProcessor = new CommentsProcessor(
-      'keepSpecialComments' in options ? options.keepSpecialComments : '*',
-      options.keepBreaks,
-      lineBreak
-    );
-    var expressionsProcessor = new ExpressionsProcessor();
-    var freeTextProcessor = new FreeTextProcessor();
-    var urlsProcessor = new UrlsProcessor();
-    var importInliner = new ImportInliner(context);
-
-    if (options.processImport) {
-      // inline all imports
-      replace(function inlineImports() {
-        data = importInliner.process(data, {
-          root: options.root || process.cwd(),
-          relativeTo: options.relativeTo
-        });
-      });
-    }
-
-    replace(function escapeComments() {
-      data = commentsProcessor.escape(data);
-    });
-
-    // replace all escaped line breaks
-    replace(/\\(\r\n|\n)/gm, '');
-
-    // strip parentheses in urls if possible (no spaces inside)
-    replace(/url\((['"])([^\)]+)['"]\)/g, function(match, quote, url) {
-      var unsafeDataURI = url.indexOf('data:') === 0 && url.match(/data:\w+\/[^;]+;base64,/) === null;
-      if (url.match(/[ \t]/g) !== null || unsafeDataURI)
-        return 'url(' + quote + url + quote + ')';
-      else
-        return 'url(' + url + ')';
-    });
-
-    // strip parentheses in animation & font names
-    replace(/(animation|animation\-name|font|font\-family):([^;}]+)/g, function(match, propertyName, fontDef) {
-      return propertyName + ':' + fontDef.replace(/['"]([\w\-]+)['"]/g, '$1');
-    });
-
-    // strip parentheses in @keyframes
-    replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) {
-      prefix = prefix || '';
-      return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, ''));
-    });
-
-    // IE shorter filters, but only if single (IE 7 issue)
-    replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) {
-      return filter.toLowerCase() + args + suffix;
-    });
-
-    replace(function escapeExpressions() {
-      data = expressionsProcessor.escape(data);
-    });
-
-    // strip parentheses in attribute values
-    replace(/\[([^\]]+)\]/g, function(match, content) {
-      var eqIndex = content.indexOf('=');
-      var singleQuoteIndex = content.indexOf('\'');
-      var doubleQuoteIndex = content.indexOf('"');
-      if (eqIndex < 0 && singleQuoteIndex < 0 && doubleQuoteIndex < 0)
-        return match;
-      if (singleQuoteIndex === 0 || doubleQuoteIndex === 0)
-        return match;
-
-      var key = content.substring(0, eqIndex);
-      var value = content.substring(eqIndex + 1, content.length);
-
-      if (/^['"](?:[a-zA-Z][a-zA-Z\d\-_]+)['"]$/.test(value))
-        return '[' + key + '=' + value.substring(1, value.length - 1) + ']';
-      else
-        return match;
-    });
-
-    replace(function escapeFreeText() {
-      data = freeTextProcessor.escape(data);
-    });
-
-    replace(function escapeUrls() {
-      data = urlsProcessor.escape(data);
-    });
-
-    // line breaks
-    replace(/[\r]?\n/g, ' ');
-
-    // multiple whitespace
-    replace(/[\t ]+/g, ' ');
-
-    // multiple semicolons (with optional whitespace)
-    replace(/;[ ]?;+/g, ';');
-
-    // multiple line breaks to one
-    replace(/ (?:\r\n|\n)/g, lineBreak);
-    replace(/(?:\r\n|\n)+/g, lineBreak);
-
-    // remove spaces around selectors
-    replace(/ ([+~>]) /g, '$1');
-
-    // remove extra spaces inside content
-    replace(/([!\(\{\}:;=,\n]) /g, '$1');
-    replace(/ ([!\)\{\};=,\n])/g, '$1');
-    replace(/(?:\r\n|\n)\}/g, '}');
-    replace(/([\{;,])(?:\r\n|\n)/g, '$1');
-    replace(/ :([^\{\};]+)([;}])/g, ':$1$2');
-
-    // restore spaces inside IE filters (IE 7 issue)
-    replace(/progid:[^(]+\(([^\)]+)/g, function(match) {
-      return match.replace(/,/g, ', ');
-    });
-
-    // trailing semicolons
-    replace(/;\}/g, '}');
-
-    replace(function hsl2Hex() {
-      data = new ColorHSLToHex(data).process();
-    });
-
-    replace(function rgb2Hex() {
-      data = new ColorRGBToHex(data).process();
-    });
-
-    replace(function longToShortHex() {
-      data = new ColorLongToShortHex(data).process();
-    });
-
-    replace(function shortenColors() {
-      data = new ColorShortener(data).process();
-    });
-
-    // replace font weight with numerical value
-    replace(/(font\-weight|font):(normal|bold)([ ;\}!])(\w*)/g, function(match, property, weight, suffix, next) {
-      if (suffix == ' ' && next.length > 0 && !/[.\d]/.test(next))
-        return match;
-
-      if (weight == 'normal')
-        return property + ':400' + suffix + next;
-      else if (weight == 'bold')
-        return property + ':700' + suffix + next;
-      else
-        return match;
-    });
-
-    // zero + unit to zero
-    replace(/(\s|:|,)0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0');
-    replace(/rect\(0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, 'rect(0');
-
-    // round pixels to 2nd decimal place
-    replace(/\.(\d{3,})px/g, function(match, decimalPlaces) {
-      return '.' + Math.round(parseFloat('.' + decimalPlaces) * 100) + 'px';
-    });
-
-    // fraction zeros removal
-    replace(/\.([1-9]*)0+(\D)/g, function(match, nonZeroPart, suffix) {
-      return (nonZeroPart ? '.' : '') + nonZeroPart + suffix;
-    });
-
-    // restore 0% in hsl/hsla
-    replace(/(hsl|hsla)\(([^\)]+)\)/g, function(match, colorFunction, colorDef) {
-      var tokens = colorDef.split(',');
-      if (tokens[1] == '0')
-        tokens[1] = '0%';
-      if (tokens[2] == '0')
-        tokens[2] = '0%';
-      return colorFunction + '(' + tokens.join(',') + ')';
-    });
-
-    // none to 0
-    replace(/(border|border-top|border-right|border-bottom|border-left|outline):none/g, '$1:0');
-
-    // background:none to background:0 0
-    replace(/background:(?:none|transparent)([;}])/g, 'background:0 0$1');
-
-    // multiple zeros into one
-    replace(/box-shadow:0 0 0 0([^\.])/g, 'box-shadow:0 0$1');
-    replace(/:0 0 0 0([^\.])/g, ':0$1');
-    replace(/([: ,=\-])0\.(\d)/g, '$1.$2');
-
-    replace(function shorthandNotations() {
-      data = new ShorthandNotations(data).process();
-    });
-
-    // restore rect(...) zeros syntax for 4 zeros
-    replace(/rect\(\s?0(\s|,)0[ ,]0[ ,]0\s?\)/g, 'rect(0$10$10$10)');
+  this.options = options;
+  this.stats = {};
+  this.context = {
+    errors: [],
+    warnings: []
+  };
+  this.errors = this.context.errors;
+  this.warnings = this.context.warnings;
+  this.lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
+};
 
-    // remove universal selector when not needed (*#id, *.class etc)
-    replace(/\*([\.#:\[])/g, '$1');
+CleanCSS.prototype.minify = function(data) {
+  var startedAt;
+  var stats = this.stats;
+  var options = this.options;
+  var context = this.context;
+  var lineBreak = this.lineBreak;
+
+  if (options.debug) {
+    startedAt = process.hrtime();
+    stats.originalSize = data.length;
+  }
+
+  var replace = function() {
+    if (typeof arguments[0] == 'function')
+      arguments[0]();
+    else
+      data = data.replace.apply(data, arguments);
+  };
 
-    // Restore spaces inside calc back
-    replace(/calc\([^\}]+\}/g, function(match) {
-      return match.replace(/\+/g, ' + ');
-    });
+  // replace function
+  if (options.benchmark) {
+    var originalReplace = replace;
+    replace = function(pattern, replacement) {
+      var name = typeof pattern == 'function' ?
+        /function (\w+)\(/.exec(pattern.toString())[1] :
+        pattern;
 
-    // remove space after (rgba|hsla) declaration - see #165
-    replace(/(rgba|hsla)\(([^\)]+)\) /g, '$1($2)');
+      var start = process.hrtime();
+      originalReplace(pattern, replacement);
 
-    if (!options.noAdvanced) {
-      replace(function optimizeSelectors() {
-        data = new SelectorsOptimizer(data, context, {
-          keepBreaks: options.keepBreaks,
-          lineBreak: lineBreak,
-          selectorsMergeMode: options.selectorsMergeMode
-        }).process();
+      var itTook = process.hrtime(start);
+      console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000);
+    };
+  }
+
+  var commentsProcessor = new CommentsProcessor(
+    'keepSpecialComments' in options ? options.keepSpecialComments : '*',
+    options.keepBreaks,
+    lineBreak
+  );
+  var expressionsProcessor = new ExpressionsProcessor();
+  var freeTextProcessor = new FreeTextProcessor();
+  var urlsProcessor = new UrlsProcessor();
+  var importInliner = new ImportInliner(context);
+
+  if (options.processImport) {
+    // inline all imports
+    replace(function inlineImports() {
+      data = importInliner.process(data, {
+        root: options.root || process.cwd(),
+        relativeTo: options.relativeTo
       });
-    }
-
-    replace(function restoreUrls() {
-      data = urlsProcessor.restore(data);
     });
-    replace(function rebaseUrls() {
-      data = options.noRebase ? data : new UrlRebase(options, context).process(data);
+  }
+
+  replace(function escapeComments() {
+    data = commentsProcessor.escape(data);
+  });
+
+  // replace all escaped line breaks
+  replace(/\\(\r\n|\n)/gm, '');
+
+  // strip parentheses in urls if possible (no spaces inside)
+  replace(/url\((['"])([^\)]+)['"]\)/g, function(match, quote, url) {
+    var unsafeDataURI = url.indexOf('data:') === 0 && url.match(/data:\w+\/[^;]+;base64,/) === null;
+    if (url.match(/[ \t]/g) !== null || unsafeDataURI)
+      return 'url(' + quote + url + quote + ')';
+    else
+      return 'url(' + url + ')';
+  });
+
+  // strip parentheses in animation & font names
+  replace(/(animation|animation\-name|font|font\-family):([^;}]+)/g, function(match, propertyName, fontDef) {
+    return propertyName + ':' + fontDef.replace(/['"]([\w\-]+)['"]/g, '$1');
+  });
+
+  // strip parentheses in @keyframes
+  replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) {
+    prefix = prefix || '';
+    return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, ''));
+  });
+
+  // IE shorter filters, but only if single (IE 7 issue)
+  replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) {
+    return filter.toLowerCase() + args + suffix;
+  });
+
+  replace(function escapeExpressions() {
+    data = expressionsProcessor.escape(data);
+  });
+
+  // strip parentheses in attribute values
+  replace(/\[([^\]]+)\]/g, function(match, content) {
+    var eqIndex = content.indexOf('=');
+    var singleQuoteIndex = content.indexOf('\'');
+    var doubleQuoteIndex = content.indexOf('"');
+    if (eqIndex < 0 && singleQuoteIndex < 0 && doubleQuoteIndex < 0)
+      return match;
+    if (singleQuoteIndex === 0 || doubleQuoteIndex === 0)
+      return match;
+
+    var key = content.substring(0, eqIndex);
+    var value = content.substring(eqIndex + 1, content.length);
+
+    if (/^['"](?:[a-zA-Z][a-zA-Z\d\-_]+)['"]$/.test(value))
+      return '[' + key + '=' + value.substring(1, value.length - 1) + ']';
+    else
+      return match;
+  });
+
+  replace(function escapeFreeText() {
+    data = freeTextProcessor.escape(data);
+  });
+
+  replace(function escapeUrls() {
+    data = urlsProcessor.escape(data);
+  });
+
+  // line breaks
+  replace(/[\r]?\n/g, ' ');
+
+  // multiple whitespace
+  replace(/[\t ]+/g, ' ');
+
+  // multiple semicolons (with optional whitespace)
+  replace(/;[ ]?;+/g, ';');
+
+  // multiple line breaks to one
+  replace(/ (?:\r\n|\n)/g, lineBreak);
+  replace(/(?:\r\n|\n)+/g, lineBreak);
+
+  // remove spaces around selectors
+  replace(/ ([+~>]) /g, '$1');
+
+  // remove extra spaces inside content
+  replace(/([!\(\{\}:;=,\n]) /g, '$1');
+  replace(/ ([!\)\{\};=,\n])/g, '$1');
+  replace(/(?:\r\n|\n)\}/g, '}');
+  replace(/([\{;,])(?:\r\n|\n)/g, '$1');
+  replace(/ :([^\{\};]+)([;}])/g, ':$1$2');
+
+  // restore spaces inside IE filters (IE 7 issue)
+  replace(/progid:[^(]+\(([^\)]+)/g, function(match) {
+    return match.replace(/,/g, ', ');
+  });
+
+  // trailing semicolons
+  replace(/;\}/g, '}');
+
+  replace(function hsl2Hex() {
+    data = new ColorHSLToHex(data).process();
+  });
+
+  replace(function rgb2Hex() {
+    data = new ColorRGBToHex(data).process();
+  });
+
+  replace(function longToShortHex() {
+    data = new ColorLongToShortHex(data).process();
+  });
+
+  replace(function shortenColors() {
+    data = new ColorShortener(data).process();
+  });
+
+  // replace font weight with numerical value
+  replace(/(font\-weight|font):(normal|bold)([ ;\}!])(\w*)/g, function(match, property, weight, suffix, next) {
+    if (suffix == ' ' && next.length > 0 && !/[.\d]/.test(next))
+      return match;
+
+    if (weight == 'normal')
+      return property + ':400' + suffix + next;
+    else if (weight == 'bold')
+      return property + ':700' + suffix + next;
+    else
+      return match;
+  });
+
+  // zero + unit to zero
+  replace(/(\s|:|,)0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0');
+  replace(/rect\(0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, 'rect(0');
+
+  // round pixels to 2nd decimal place
+  replace(/\.(\d{3,})px/g, function(match, decimalPlaces) {
+    return '.' + Math.round(parseFloat('.' + decimalPlaces) * 100) + 'px';
+  });
+
+  // fraction zeros removal
+  replace(/\.([1-9]*)0+(\D)/g, function(match, nonZeroPart, suffix) {
+    return (nonZeroPart ? '.' : '') + nonZeroPart + suffix;
+  });
+
+  // restore 0% in hsl/hsla
+  replace(/(hsl|hsla)\(([^\)]+)\)/g, function(match, colorFunction, colorDef) {
+    var tokens = colorDef.split(',');
+    if (tokens[1] == '0')
+      tokens[1] = '0%';
+    if (tokens[2] == '0')
+      tokens[2] = '0%';
+    return colorFunction + '(' + tokens.join(',') + ')';
+  });
+
+  // none to 0
+  replace(/(border|border-top|border-right|border-bottom|border-left|outline):none/g, '$1:0');
+
+  // background:none to background:0 0
+  replace(/background:(?:none|transparent)([;}])/g, 'background:0 0$1');
+
+  // multiple zeros into one
+  replace(/box-shadow:0 0 0 0([^\.])/g, 'box-shadow:0 0$1');
+  replace(/:0 0 0 0([^\.])/g, ':0$1');
+  replace(/([: ,=\-])0\.(\d)/g, '$1.$2');
+
+  replace(function shorthandNotations() {
+    data = new ShorthandNotations(data).process();
+  });
+
+  // restore rect(...) zeros syntax for 4 zeros
+  replace(/rect\(\s?0(\s|,)0[ ,]0[ ,]0\s?\)/g, 'rect(0$10$10$10)');
+
+  // remove universal selector when not needed (*#id, *.class etc)
+  replace(/\*([\.#:\[])/g, '$1');
+
+  // Restore spaces inside calc back
+  replace(/calc\([^\}]+\}/g, function(match) {
+    return match.replace(/\+/g, ' + ');
+  });
+
+  // remove space after (rgba|hsla) declaration - see #165
+  replace(/(rgba|hsla)\(([^\)]+)\) /g, '$1($2)');
+
+  if (!options.noAdvanced) {
+    replace(function optimizeSelectors() {
+      data = new SelectorsOptimizer(data, context, {
+        keepBreaks: options.keepBreaks,
+        lineBreak: lineBreak,
+        selectorsMergeMode: options.selectorsMergeMode
+      }).process();
     });
-    replace(function restoreFreeText() {
-      data = freeTextProcessor.restore(data);
-    });
-    replace(function restoreComments() {
-      data = commentsProcessor.restore(data);
-    });
-    replace(function restoreExpressions() {
-      data = expressionsProcessor.restore(data);
+  }
+
+  replace(function restoreUrls() {
+    data = urlsProcessor.restore(data);
+  });
+  replace(function rebaseUrls() {
+    data = options.noRebase ? data : new UrlRebase(options, context).process(data);
+  });
+  replace(function restoreFreeText() {
+    data = freeTextProcessor.restore(data);
+  });
+  replace(function restoreComments() {
+    data = commentsProcessor.restore(data);
+  });
+  replace(function restoreExpressions() {
+    data = expressionsProcessor.restore(data);
+  });
+
+  // move first charset to the beginning
+  replace(function moveCharset() {
+    // get first charset in stylesheet
+    var match = data.match(/@charset [^;]+;/);
+    var firstCharset = match ? match[0] : null;
+    if (!firstCharset)
+      return;
+
+    // reattach first charset and remove all subsequent
+    data = firstCharset +
+      (options.keepBreaks ? lineBreak : '') +
+      data.replace(new RegExp('@charset [^;]+;(' + lineBreak + ')?', 'g'), '').trim();
+  });
+
+  if (options.noAdvanced) {
+    replace(function removeEmptySelectors() {
+      data = new EmptyRemoval(data).process();
     });
+  }
 
-    // move first charset to the beginning
-    replace(function moveCharset() {
-      // get first charset in stylesheet
-      var match = data.match(/@charset [^;]+;/);
-      var firstCharset = match ? match[0] : null;
-      if (!firstCharset)
-        return;
-
-      // reattach first charset and remove all subsequent
-      data = firstCharset +
-        (options.keepBreaks ? lineBreak : '') +
-        data.replace(new RegExp('@charset [^;]+;(' + lineBreak + ')?', 'g'), '').trim();
-    });
-
-    if (options.noAdvanced) {
-      replace(function removeEmptySelectors() {
-        data = new EmptyRemoval(data).process();
-      });
-    }
+  // trim spaces at beginning and end
+  data = data.trim();
 
-    // trim spaces at beginning and end
-    data = data.trim();
+  if (options.debug) {
+    var elapsed = process.hrtime(startedAt);
+    stats.timeSpent = ~~(elapsed[0] * 1e3 + elapsed[1] / 1e6);
+    stats.efficiency = 1 - data.length / stats.originalSize;
+    stats.minifiedSize = data.length;
+  }
 
-    if (options.debug) {
-      var elapsed = process.hrtime(startedAt);
-      stats.timeSpent = ~~(elapsed[0] * 1e3 + elapsed[1] / 1e6);
-      stats.efficiency = 1 - data.length / stats.originalSize;
-      stats.minifiedSize = data.length;
-    }
-
-    return data;
-  };
-
-  return {
-    errors: context.errors,
-    lineBreak: lineBreak,
-    options: options,
-    minify: minify,
-    stats: stats,
-    warnings: context.warnings
-  };
+  return data;
 };
index 4e83108..e50a3b1 100644 (file)
@@ -5,12 +5,41 @@ var CleanCSS = require('../index');
 vows.describe('module tests').addBatch({
   'imported as a function': {
     topic: function() {
-      return new CleanCSS().minify;
+      var css = new CleanCSS();
+      return css.minify.bind(css);
     },
     'should minify CSS correctly': function(minify) {
       assert.equal(minify('a{  color: #f00;  }'), 'a{color:red}');
     }
   },
+  'initialization without new (back-compat)': {
+    topic: function() {
+      return CleanCSS();
+    },
+    'should have stats, errors, etc.': function(css) {
+      assert.isObject(css.stats);
+      assert.isArray(css.errors);
+      assert.isArray(css.warnings);
+      assert.isString(css.lineBreak);
+    },
+    'should minify CSS correctly': function(css) {
+      assert.equal(css.minify('a{  color: #f00;  }'), 'a{color:red}');
+    }
+  },
+  'extended via prototype': {
+    topic: function() {
+      CleanCSS.prototype.foo = function(data, callback) {
+        callback(null, this.minify(data));
+      };
+      new CleanCSS().foo('a{  color: #f00;  }', this.callback);
+    },
+    'should minify CSS correctly': function(error, minified) {
+      assert.equal(minified, 'a{color:red}');
+    },
+    teardown: function() {
+      delete CleanCSS.prototype.foo;
+    }
+  },
   'no debug': {
     topic: function() {
       var minifier = new CleanCSS();