clean-css
Advanced tools
Comparing version 0.3.3 to 0.4.0
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
20
13983
53
0