clean-css
Advanced tools
Comparing version
@@ -0,1 +1,9 @@ | ||
0.4.0 / 2012-06-04 | ||
================== | ||
* Speed improvements (up to 4x) thanks to rewrite of comments and CSS' content processing. | ||
* Stripping empty CSS tags is now optional (see ./bin/cleancss for details). | ||
* Improved debugging mode (see ./test/bench.js) | ||
* Added `make bench` for a one-pass benchmark. | ||
0.3.3 / 2012-05-27 | ||
@@ -2,0 +10,0 @@ ================== |
165
lib/clean.js
@@ -11,5 +11,13 @@ var util = require('util'); | ||
specialComments: [], | ||
contentBlocks: [], | ||
process: function(data, options) { | ||
var specialComments = [], | ||
contentBlocks = []; | ||
var self = this, | ||
replace = function(pattern, replacement) { | ||
if (typeof arguments[0] == 'function') | ||
arguments[0](); | ||
else | ||
data = data.replace.apply(data, arguments); | ||
}; | ||
@@ -19,48 +27,24 @@ options = options || {}; | ||
// replace function | ||
var replace = function(pattern, replacement) { | ||
if (options.debug) { // for debugging purposes only | ||
console.time(pattern); | ||
data = data.replace(pattern, replacement); | ||
console.timeEnd(pattern); | ||
var end = new Date().getTime(); | ||
} else { | ||
data = data.replace(pattern, replacement); | ||
} | ||
}; | ||
if (options.debug) { | ||
var originalReplace = replace; | ||
replace = function(pattern, replacement) { | ||
var name = typeof pattern == 'function' ? | ||
/function (\w+)\(/.exec(pattern.toString())[1] : | ||
pattern; | ||
console.time(name); | ||
originalReplace(pattern, replacement); | ||
console.timeEnd(name); | ||
}; | ||
} | ||
// strip comments one by one | ||
for (var end = 0; end < data.length; ) { | ||
var start = data.indexOf('/*', end); | ||
end = data.indexOf('*/', start); | ||
if (start == -1 || end == -1) break; | ||
replace(function stripComments() { | ||
data = self.stripComments(data); | ||
}); | ||
if (data[start + 2] == '!') { | ||
// in case of special comments, replace them with a placeholder | ||
specialComments.push(data.substring(start, end + 2)); | ||
data = data.substring(0, start) + '__CSSCOMMENT__' + data.substring(end + 2); | ||
} else { | ||
data = data.substring(0, start) + data.substring(end + 2); | ||
} | ||
end = start; | ||
} | ||
// replace content: with a placeholder | ||
for (var end = 0; end < data.length; ) { | ||
var start = data.indexOf('content', end); | ||
if (start == -1) break; | ||
replace(function stripContent() { | ||
data = self.stripContent(data); | ||
}); | ||
var wrapper = /[^ :]/.exec(data.substring(start + 7))[0]; | ||
if (/['"]/.test(wrapper) == false) { | ||
end = start + 7; | ||
continue; | ||
} | ||
var firstIndex = data.indexOf(wrapper, start); | ||
var lastIndex = data.indexOf(wrapper, firstIndex + 1); | ||
contentBlocks.push(data.substring(firstIndex, lastIndex + 1)); | ||
data = data.substring(0, firstIndex) + '__CSSCONTENT__' + data.substring(lastIndex + 1); | ||
end = lastIndex + 1; | ||
} | ||
replace(/;\s*;+/g, ';') // whitespace between semicolons & multiple semicolons | ||
@@ -112,3 +96,3 @@ replace(/\n/g, '') // line breaks | ||
replace(/([: ,=\-])0\.(\d)/g, '$1.$2') | ||
replace(/[^}]+?{\s*?}/g, '') // empty elements | ||
if (options.removeEmpty) replace(/[^}]+?{\s*?}/g, '') // empty elements | ||
if (data.indexOf('charset') > 0) replace(/(.+)(@charset [^;]+;)/, '$2$1') // move first charset to the beginning | ||
@@ -124,6 +108,95 @@ replace(/(.)(@charset [^;]+;)/g, '$1') // remove all extra charsets that are not at the beginning | ||
}); | ||
replace(/__CSSCOMMENT__/g, function() { return specialComments.shift(); }); | ||
replace(/__CSSCONTENT__/g, function() { return contentBlocks.shift(); }); | ||
replace(/__CSSCOMMENT__/g, function() { return self.specialComments.shift(); }); | ||
replace(/__CSSCONTENT__/g, function() { return self.contentBlocks.shift(); }); | ||
return data.trim() // trim spaces at beginning and end | ||
}, | ||
// Strips special comments (/*! ... */) by replacing them by __CSSCOMMENT__ marker | ||
// for further restoring. Plain comments are removed. It's done by scanning datq using | ||
// String#indexOf scanning instead of regexps to speed up the process. | ||
stripComments: function(data) { | ||
var tempData = [], | ||
nextStart = 0, | ||
nextEnd = 0, | ||
cursor = 0; | ||
for (; nextEnd < data.length; ) { | ||
nextStart = data.indexOf('/*', nextEnd); | ||
nextEnd = data.indexOf('*/', nextStart); | ||
if (nextStart == -1 || nextEnd == -1) break; | ||
tempData.push(data.substring(cursor, nextStart)) | ||
if (data[nextStart + 2] == '!') { | ||
// in case of special comments, replace them with a placeholder | ||
this.specialComments.push(data.substring(nextStart, nextEnd + 2)); | ||
tempData.push('__CSSCOMMENT__'); | ||
} | ||
cursor = nextEnd + 2; | ||
} | ||
return tempData.length > 0 ? | ||
tempData.join('') + data.substring(cursor, data.length) : | ||
data; | ||
}, | ||
// Strips content tags by replacing them by __CSSCONTENT__ marker | ||
// for further restoring. It's done via string scanning instead of | ||
// regexps to speed up the process. | ||
stripContent: function(data) { | ||
var tempData = [], | ||
nextStart = 0, | ||
nextEnd = 0, | ||
tempStart = 0, | ||
cursor = 0, | ||
matchedParenthesis = null; | ||
// Finds either first (matchedParenthesis == null) or second matching parenthesis | ||
// so we can determine boundaries of content block. | ||
var nextParenthesis = function(pos) { | ||
var min, | ||
max = data.length; | ||
if (matchedParenthesis) { | ||
min = data.indexOf(matchedParenthesis, pos); | ||
if (min == -1) min = max; | ||
} else { | ||
var next1 = data.indexOf("'", pos); | ||
var next2 = data.indexOf('"', pos); | ||
if (next1 == -1) next1 = max; | ||
if (next2 == -1) next2 = max; | ||
min = next1 > next2 ? next2 : next1; | ||
} | ||
if (min == max) return -1; | ||
if (matchedParenthesis) { | ||
matchedParenthesis = null; | ||
return min; | ||
} else { | ||
// check if there's anything else between pos and min that doesn't match ':' or whitespace | ||
if (/[^:\s]/.test(data.substring(pos, min))) return -1; | ||
matchedParenthesis = data.charAt(min); | ||
return min + 1; | ||
} | ||
}; | ||
for (; nextEnd < data.length; ) { | ||
nextStart = data.indexOf('content', nextEnd); | ||
if (nextStart == -1) break; | ||
nextStart = nextParenthesis(nextStart + 7); | ||
nextEnd = nextParenthesis(nextStart); | ||
if (nextStart == -1 || nextEnd == -1) break; | ||
tempData.push(data.substring(cursor, nextStart - 1)); | ||
tempData.push('__CSSCONTENT__'); | ||
this.contentBlocks.push(data.substring(nextStart - 1, nextEnd + 1)); | ||
cursor = nextEnd + 1; | ||
} | ||
return tempData.length > 0 ? | ||
tempData.join('') + data.substring(cursor, data.length) : | ||
data; | ||
} | ||
@@ -130,0 +203,0 @@ }; |
@@ -11,3 +11,3 @@ { | ||
}, | ||
"version": "0.3.3", | ||
"version": "0.4.0", | ||
"main": "index.js", | ||
@@ -14,0 +14,0 @@ "bin": { |
@@ -20,3 +20,3 @@ ## What is clean-css? ## | ||
cleancss -o public-min.css public.css | ||
To minify the same **public.css** into standard output skip the -o parameter: | ||
@@ -29,3 +29,3 @@ | ||
cat one.css two.css three.css | cleancss -o merged-and-minified.css | ||
Or even gzip it at once: | ||
@@ -38,3 +38,3 @@ | ||
var cleanCSS = require('clean-css'); | ||
var source = "a{font-weight:bold;}"; | ||
@@ -49,4 +49,9 @@ var minimized = cleanCSS.process(source); | ||
### Acknowledgments ### | ||
* Vincent Voyer (@vvo) for a patch with better empty element regex and for inspiring us to do many performance improvements in 0.4 release. | ||
* Jan Michael Alonzo (@jmalonzo) for a patch removing node's old 'sys' package. | ||
## License ## | ||
Clean-css is released under the MIT license. |
@@ -22,3 +22,3 @@ var vows = require('vows'), | ||
context[testName]['minimizing ' + testName + '.css'] = function(data) { | ||
assert.equal(cleanCSS.process(data.plain), data.minimized) | ||
assert.equal(cleanCSS.process(data.plain, { removeEmpty: true }), data.minimized) | ||
}; | ||
@@ -25,0 +25,0 @@ }); |
@@ -1,11 +0,6 @@ | ||
var | ||
path = require('path'), | ||
cleanCSS = require('../index'), | ||
bigcss = require('fs').readFileSync(path.join(__dirname, 'data', 'big.css'), 'utf8'), | ||
runs = ~~process.argv[2] || 10; | ||
var cleanCSS = require('../index'), | ||
bigcss = require('fs').readFileSync(require('path').join(__dirname, 'data', 'big.css'), 'utf8'); | ||
for(var i=0; i<runs; i++) { | ||
console.time('complete minification'); | ||
cleanCSS.process(bigcss, { debug: true }); | ||
console.timeEnd('complete minification'); | ||
} | ||
console.time('complete minification'); | ||
cleanCSS.process(bigcss, { debug: true }); | ||
console.timeEnd('complete minification'); |
@@ -5,10 +5,10 @@ var vows = require('vows'), | ||
var cssContext = function(groups) { | ||
var cssContext = function(groups, options) { | ||
var context = {}; | ||
var clean = function(cleanedCSS) { | ||
return function(css) { | ||
assert.equal(cleanCSS.process(css), cleanedCSS); | ||
assert.equal(cleanCSS.process(css, options), cleanedCSS); | ||
} | ||
}; | ||
for (var g in groups) { | ||
@@ -20,3 +20,3 @@ var transformation = groups[g]; | ||
} | ||
for (var i = 0, c = transformation.length; i < c; i++) { | ||
@@ -29,3 +29,3 @@ context[g + ' #' + (i + 1)] = { | ||
} | ||
return context; | ||
@@ -102,16 +102,2 @@ }; | ||
}), | ||
'empty elements': cssContext({ | ||
'single': [ | ||
' div p { \n}', | ||
'' | ||
], | ||
'between non-empty': [ | ||
'div {color:#fff} a{ } p{ line-height:1.35em}', | ||
'div{color:#fff}p{line-height:1.35em}' | ||
], | ||
'just a semicolon': [ | ||
'div { ; }', | ||
'' | ||
] | ||
}), | ||
'selectors': cssContext({ | ||
@@ -171,3 +157,7 @@ 'remove spaces around selectors': [ | ||
'text content': cssContext({ | ||
'normal': 'a{content:"."}', | ||
'normal #1': 'a{content:"."}', | ||
'normal #2': [ | ||
'a:before{content : "test\'s test"; }', | ||
'a:before{content:"test\'s test"}' | ||
], | ||
'open quote': [ | ||
@@ -311,3 +301,23 @@ 'a{content : open-quote;opacity:1}', | ||
] | ||
}), | ||
'empty elements': cssContext({ | ||
'single': [ | ||
' div p { \n}', | ||
'' | ||
], | ||
'between non-empty': [ | ||
'div {color:#fff} a{ } p{ line-height:1.35em}', | ||
'div{color:#fff}p{line-height:1.35em}' | ||
], | ||
'just a semicolon': [ | ||
'div { ; }', | ||
'' | ||
] | ||
}, { removeEmpty: true }), | ||
'skip empty elements': cssContext({ | ||
'empty #1': 'a{}', | ||
'empty #2': 'div>a{}', | ||
'empty #3': 'div:nth-child(2n){}', | ||
'empty #3': 'a{color:#fff}div{}p{line-height:2em}' | ||
}) | ||
}).export(module); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
703701
68.9%20
5.26%13983
0.48%53
10.42%0
-100%