Fix some of the bugs in element removal mechanism. Write more tests for it.
authorJuriy Zaytsev <kangax@gmail.com>
Wed, 10 Feb 2010 17:34:24 +0000 (12:34 -0500)
committerJuriy Zaytsev <kangax@gmail.com>
Wed, 10 Feb 2010 17:34:24 +0000 (12:34 -0500)
src/htmlminifier.js
tests/index.html

index 788c6c3..c79ac00 100644 (file)
@@ -14,6 +14,9 @@
   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');
index 92b5e3a..be5fc4d 100644 (file)
         
         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