function trimWhitespace(str) {
return (str.trim ? str.trim() : str.replace(/^\s+/, '').replace(/\s+$/, ''));
}
+ function collapseWhitespace(str) {
+ return str.replace(/\s+/g, ' ');
+ }
function canRemoveAttributeQuotes(value) {
// http://www.w3.org/TR/html4/intro/sgmltut.html#attributes
function cleanAttributeValue(tag, attrName, attrValue) {
if (/^on[a-z]+/.test(attrName)) {
- return attrValue.replace(/^(['"])?javascript:/i, '$1');
+ return attrValue.replace(/^\s*javascript:/i, '');
}
- if (attrName.toLowerCase() === 'class') {
+ if (attrName === 'class') {
// trim and collapse whitesapce
- return attrValue.replace(/^(["'])?\s+/, '$1').replace(/\s+(["'])?$/, '$1').replace(/\s+/g, ' ');
+ return collapseWhitespace(trimWhitespace(attrValue));
}
return attrValue;
}
return false;
}
+ function canRemoveElement(tag) {
+ return tag !== 'textarea';
+ }
+
function normalizeAttribute(attr, attrs, tag, options) {
- var attrName = attr.name.toLowerCase();
- var attrValue = attr.escaped;
- var attrFragment;
+ var attrName = attr.name.toLowerCase(),
+ attrValue = attr.escaped,
+ attrFragment;
if (options.shouldRemoveRedundantAttributes &&
isAttributeRedundant(tag, attrName, attrValue, attrs)) {
options = options || { };
value = trimWhitespace(value);
- var results = [];
- var t = new Date();
+ var results = [ ],
+ buffer = [ ],
+ currentChars = '',
+ currentTag = '',
+ t = new Date();
HTMLParser(value, {
start: function( tag, attrs, unary ) {
tag = tag.toLowerCase();
+ currentTag = tag;
- results.push('<', tag);
+ buffer.push('<', tag);
for ( var i = 0, len = attrs.length; i < len; i++ ) {
- results.push(normalizeAttribute(attrs[i], attrs, tag, options));
+ buffer.push(normalizeAttribute(attrs[i], attrs, tag, options));
}
- results.push('>');
+ buffer.push('>');
},
end: function( tag ) {
- results.push('</', tag.toLowerCase(), '>');
+ var isElementEmpty = currentChars === '' && tag === currentTag;
+ if (options.shouldRemoveEmptyElements && isElementEmpty && canRemoveElement(tag)) {
+ // noop
+ }
+ else {
+ buffer.push('</', tag.toLowerCase(), '>');
+ results.push.apply(results, buffer);
+ }
+ buffer.length = 0;
+ currentChars = '';
},
chars: function( text ) {
- results.push(options.shouldCollapseWhitespace ? trimWhitespace(text) : text);
+ currentChars = text;
+ buffer.push(options.shouldCollapseWhitespace ? trimWhitespace(text) : text);
},
comment: function( text ) {
- results.push(options.shouldRemoveComments ? '' : ('<!--' + text + '-->'));
+ buffer.push(options.shouldRemoveComments ? '' : ('<!--' + text + '-->'));
},
doctype: function(doctype) {
- results.push(options.shouldUseShortDoctype ? '<!DOCTYPE html>' : doctype.replace(/\s+/g, ' '));
+ buffer.push(options.shouldUseShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
}
});
-
+
+ results.push.apply(results, buffer);
+
var str = results.join('');
log('minified in: ' + (new Date() - t) + 'ms');
var minify = global.minify;
+ test('parsing non-trivial markup', function() {
+ equals(minify('<p title="</p>">x</p>'), '<p title="</p>">x</p>');
+ equals(minify('<p title=" <!-- hello world --> ">x</p>'), '<p title=" <!-- hello world --> ">x</p>');
+ equals(minify('<p title=" <![CDATA[ \n\n foobar baz ]]> ">x</p>'), '<p title=" <![CDATA[ \n\n foobar baz ]]> ">x</p>');
+ equals(minify('<p foo-bar=baz>xxx</p>'), '<p foo-bar="baz">xxx</p>');
+ equals(minify('<p foo:bar=baz>xxx</p>'), '<p foo:bar="baz">xxx</p>');
+
+ var input = '<div><div><div><div><div><div><div><div><div><div>'+
+ 'i\'m 10 levels deep'+
+ '</div></div></div></div></div></div></div></div></div></div>';
+
+ equals(minify(input), input);
+
+ equals(minify('<script>alert(\'<!--\')<\/script>'), '<script>alert(\'<!--\')<\/script>');
+ equals(minify('<script>alert(\'<!-- foo -->\')<\/script>'), '<script>alert(\'<!-- foo -->\')<\/script>');
+ equals(minify('<script>alert(\'-->\')<\/script>'), '<script>alert(\'-->\')<\/script>');
+ });
+
test('`minifiy` exists', function() {
ok(minify);
});
equals(minify(input, { shouldCollapseWhitespace: true }), output);
});
+ test('removing empty elements', function() {
+ equals(minify('<p>x</p>', { shouldRemoveEmptyElements: true }), '<p>x</p>');
+ equals(minify('<p></p>', { shouldRemoveEmptyElements: true }), '');
+
+ var input = '<p>foo<span>bar</span><span></span></p>';
+ var output = '<p>foo<span>bar</span></p>';
+
+ equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+
+ input = '<a href="http://example/com" title="hello world"></a>';
+ output = '';
+
+ equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+
+ input = '<textarea cols="10" rows="10"></textarea>';
+ output = '<textarea cols="10" rows="10"></textarea>';
+
+ equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+
+ input = '<div>hello<span>world</span></div>';
+ output = '<div>hello<span>world</span></div>';
+
+ equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+ });
+
})(this);
</script>
+
</body>
</html>
\ No newline at end of file