From c0e0d13cc42b3122420627074f87e0015c9ae6b7 Mon Sep 17 00:00:00 2001 From: Stephen Mathieson Date: Thu, 5 Dec 2013 12:07:56 -0500 Subject: [PATCH] Fixes #188 - Convert CleanCSS to a constructor This allows for adding custom methods on the CleanCSS prototype. --- lib/clean.js | 586 ++++++++++++++++++++++---------------------- test/module-test.js | 31 ++- 2 files changed, 325 insertions(+), 292 deletions(-) diff --git a/lib/clean.js b/lib/clean.js index fdbf67a8..385f6e04 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -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; }; diff --git a/test/module-test.js b/test/module-test.js index 4e831088..e50a3b18 100644 --- a/test/module-test.js +++ b/test/module-test.js @@ -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(); -- 2.34.1