Add MIT license and mention it in htmlminifier.js;
authorJuriy Zaytsev <kangax@gmail.com>
Wed, 10 Feb 2010 19:34:08 +0000 (14:34 -0500)
committerJuriy Zaytsev <kangax@gmail.com>
Wed, 10 Feb 2010 19:34:08 +0000 (14:34 -0500)
Add checkbox to remove empty elements (off by default);
Add checkbox to remove comments from STYLE and SCRIPT elements;
Collapse whitespace in textnodes, when it's safe to do so;
Rename options to get rid of "should" prefix.

index.html
master.js
src/htmlminifier.js
src/license.txt [new file with mode: 0644]
tests/index.html

index bf65a43..21634bc 100644 (file)
@@ -17,7 +17,9 @@
           <ul>
             <li>
               <input type="checkbox" id="remove-comments" checked>
-              <label for="remove-comments">Remove comments</label>
+              <label for="remove-comments">Remove comments (</label>
+              <input type="checkbox" id="remove-comments-from-cdata" checked>
+              <label for="remove-comments-from-cdata">also from scripts and styles</label> )
             </li>
             <li>
               <input type="checkbox" id="collapse-whitespace" checked>
                 </span>
               </label>
             </li>
+            <li>
+              <input type="checkbox" id="remove-empty-elements">
+              <label for="remove-empty-elements">
+                Remove empty elements
+                <br>
+                <span class="quiet short">
+                  All except <code>textarea</code>
+                </span>
+              </label>
+            </li>
           </ul>
         </div>
       </div>
       <div id="todo">
         TODO:
         <ul>
-          <li>Write unit tests for minifier (!)</li>
-          <li>Detect empty elements (not just attributes)</li>
-          <li>Detect repeating attributes (e.g. multiple styles)</li>
+          <li>Write more unit tests (~60 so far)</li>
+          <li>Detect repeating attributes (e.g. multiple styles, classes, etc.)</li>
           <li>Strip whitespace from attributes where allowed</li>
           <li>Report deprecated (or presentational) attributes (e.g.: <code>&lt;td width="..." height="..."></code>)</li>
           <li>Add option to collapse all whitespace to 1 character, instead of completely removing it (to preserve empty text nodes)</li>
           <li>Figure out when it is safe to remove optional closing tags, so that it doesn't affect document tree</li>
           <li>Do not strip IE conditional comments</li>
           <li>Remove as many empty/blank attributes as possible (not just core ones)</li>
-          <li>Add option to remove comments/CDATA sections from scripts/styles</li>
+          <li>Add option to remove CDATA sections from scripts/styles</li>
           <li>Parser trips over xml declarations (need to ignore or strip them)</li>
           <li>Generate a report of all applied transformations</li>
         </ul>
index ec38a35..3601abc 100644 (file)
--- a/master.js
+++ b/master.js
   
   function getOptions() {
     return {
-      shouldRemoveComments:             byId('remove-comments').checked,
-      shouldCollapseWhitespace:         byId('collapse-whitespace').checked,
-      shouldCollapseBooleanAttributes:  byId('collapse-boolean-attributes').checked,
-      shouldRemoveAttributeQuotes:      byId('remove-attribute-quotes').checked,
-      shouldRemoveRedundantAttributes:  byId('remove-redundant-attributes').checked,
-      shouldUseShortDoctype:            byId('use-short-doctype').checked,
-      shouldRemoveEmptyAttributes:      byId('remove-empty-attributes').checked
+      removeComments:             byId('remove-comments').checked,
+      removeCommentsFromCDATA:    byId('remove-comments-from-cdata').checked,
+      collapseWhitespace:         byId('collapse-whitespace').checked,
+      collapseBooleanAttributes:  byId('collapse-boolean-attributes').checked,
+      removeAttributeQuotes:      byId('remove-attribute-quotes').checked,
+      removeRedundantAttributes:  byId('remove-redundant-attributes').checked,
+      useShortDoctype:            byId('use-short-doctype').checked,
+      removeEmptyAttributes:      byId('remove-empty-attributes').checked,
+      removeEmptyElements:        byId('remove-empty-elements').checked
     };
   }
   
index c79ac00..2216942 100644 (file)
@@ -1,3 +1,12 @@
+/*!
+ * HTML Minifier v0.2
+ * http://kangax.github.com/html-minifier/
+ *
+ * Copyright (c) 2010 Juriy "kangax" Zaytsev
+ * Licensed under the MIT license.
+ *
+ */
 (function(global){
   
   var log;
     return tag !== 'textarea';
   }
   
+  function canCollapseWhitespace(tag) {
+    return !(/^(?:script|style|pre|textarea)$/.test(tag));
+  }
+  
+  function canTrimWhitespace(tag) {
+    return !(/^(?:pre|textarea)$/.test(tag));
+  }
+  
   function normalizeAttribute(attr, attrs, tag, options) {
     
     var attrName = attr.name.toLowerCase(),
         attrValue = attr.escaped,
         attrFragment;
     
-    if (options.shouldRemoveRedundantAttributes && 
+    if (options.removeRedundantAttributes && 
         isAttributeRedundant(tag, attrName, attrValue, attrs)) {
       return '';
     }
     
     attrValue = cleanAttributeValue(tag, attrName, attrValue);
     
-    if (!options.shouldRemoveAttributeQuotes || 
+    if (!options.removeAttributeQuotes || 
         !canRemoveAttributeQuotes(attrValue)) {
       attrValue = '"' + attrValue + '"';
     }
     
-    if (options.shouldRemoveEmptyAttributes &&
+    if (options.removeEmptyAttributes &&
         canDeleteEmptyAttribute(tag, attrName, attrValue)) {
       return '';
     }
 
-    if (options.shouldCollapseBooleanAttributes && 
+    if (options.collapseBooleanAttributes && 
         isBooleanAttribute(attrName)) {
       attrFragment = attrName;
     }
       },
       end: function( tag ) {
         var isElementEmpty = currentChars === '' && tag === currentTag;
-        if (options.shouldRemoveEmptyElements && isElementEmpty && canRemoveElement(tag)) {
+        if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag)) {
           // noop
         }
         else {
         currentChars = '';
       },
       chars: function( text ) {
+        if (options.removeCommentsFromCDATA &&
+            (currentTag === 'script' || currentTag === 'style')) {
+          text = text.replace(/^\s*<!--/, '').replace(/-->\s*$/, '');
+        }
+        if (options.collapseWhitespace) {
+          if (canTrimWhitespace(currentTag)) {
+            text = trimWhitespace(text);
+          }
+          if (canCollapseWhitespace(currentTag)) {
+            text = collapseWhitespace(text);
+          }
+        }
         currentChars = text;
-        buffer.push(options.shouldCollapseWhitespace ? trimWhitespace(text) : text);
+        buffer.push(text);
       },
       comment: function( text ) {
-        buffer.push(options.shouldRemoveComments ? '' : ('<!--' + text + '-->'));
+        buffer.push(options.removeComments ? '' : ('<!--' + text + '-->'));
       },
       doctype: function(doctype) {
-        buffer.push(options.shouldUseShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
+        buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
       }
     });  
     
diff --git a/src/license.txt b/src/license.txt
new file mode 100644 (file)
index 0000000..a299981
--- /dev/null
@@ -0,0 +1,22 @@
+Copyright (c) 2010 Juriy "kangax" Zaytsev
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
index be5fc4d..dd970ba 100644 (file)
         
         test('doctype normalization', function() {
           var html401doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n    "http://www.w3.org/TR/html4/strict.dtd">';
-          var actual = minify(html401doctype, { shouldUseShortDoctype: true });
+          var actual = minify(html401doctype, { useShortDoctype: true });
           var expected = '<!DOCTYPE html>';
           
           equals(actual, expected);
           
-          equals(minify('<!DOCTYPE html>', { shouldUseShortDoctype: true }), '<!DOCTYPE html>');
+          equals(minify('<!DOCTYPE html>', { useShortDoctype: true }), '<!DOCTYPE html>');
           
-          actual = minify(html401doctype, { shouldUseShortDoctype: false });
+          actual = minify(html401doctype, { useShortDoctype: false });
           expected = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">';
           
           equals(actual, expected);
         
         test('removing comments', function(){
           var input = '<!-- test -->';
-          equals(minify(input, { shouldRemoveComments: true }), '');
+          equals(minify(input, { removeComments: true }), '');
           
           var input = '<!-- foo --><div>baz</div><!-- bar\n\n moo -->';
-          equals(minify(input, { shouldRemoveComments: true }), '<div>baz</div>');
-          equals(minify(input, { shouldRemoveComments: false }), input);
+          equals(minify(input, { removeComments: true }), '<div>baz</div>');
+          equals(minify(input, { removeComments: false }), input);
           
           var input = '<p title="<!-- comment in attribute -->">foo</p>';
-          equals(minify(input, { shouldRemoveComments: true }), input);
+          equals(minify(input, { removeComments: true }), input);
           
           var input = '<script><!-- alert(1) --><\/script>';
-          equals(minify(input, { shouldRemoveComments: true }), input);
+          equals(minify(input, { removeComments: true }), input);
           
           var input = '<STYLE><!-- alert(1) --><\/STYLE>';
-          equals(minify(input, { shouldRemoveComments: true }), '<style><!-- alert(1) --><\/style>');
+          equals(minify(input, { removeComments: true }), '<style><!-- alert(1) --><\/style>');
+        });
+        
+        test('remove comments from scripts/styles', function(){
+          var input = '<script><!--alert(1)--><\/script>';
+          var output = '<script>alert(1)<\/script>';
+          equals(minify(input, { removeCommentsFromCDATA: true }), output);
+          
+          input = '<style type="text/css"><!-- p { color: red } --><\/style>';
+          output = '<style type="text/css"> p { color: red } <\/style>';
+          equals(minify(input, { removeCommentsFromCDATA: true }), output);
+          
+          input = '<script type="text/javascript">  <!-- alert("-->"); -->\n\n   <\/script>';
+          output = '<script type="text/javascript"> alert("-->"); <\/script>';
+          equals(minify(input, { removeCommentsFromCDATA: true }), output);
         });
         
         test('empty attributes', function(){
           var input = '<p id="" class="" STYLE=" " title="\n" lang="" dir="">x</p>';
-          equals(minify(input, { shouldRemoveEmptyAttributes: true }), '<p>x</p>');
+          equals(minify(input, { removeEmptyAttributes: true }), '<p>x</p>');
           
           input = '<p onclick=""   ondblclick=" " onmousedown="" ONMOUSEUP="" onmouseover=" " onmousemove="" onmouseout="" '+
                   'onkeypress=\n\n  "\n     " onkeydown=\n"" onkeyup\n="">x</p>';
-          equals(minify(input, { shouldRemoveEmptyAttributes: true }), '<p>x</p>');
+          equals(minify(input, { removeEmptyAttributes: true }), '<p>x</p>');
           
           input = '<input onfocus="" onblur="" onchange=" " value=" boo ">';
-          equals(minify(input, { shouldRemoveEmptyAttributes: true }), '<input value=" boo ">');
+          equals(minify(input, { removeEmptyAttributes: true }), '<input value=" boo ">');
           
           input = '<input value="" name="foo">';
-          equals(minify(input, { shouldRemoveEmptyAttributes: true }), '<input name="foo">');
+          equals(minify(input, { removeEmptyAttributes: true }), '<input name="foo">');
         });
         
         test('cleaning attributes', function(){
           var input = '<p onclick="javascript:alert(1)">x</p>';
-          equals(minify(input, { shouldCleanAttributes: true }), '<p onclick="alert(1)">x</p>');
+          equals(minify(input, { cleanAttributes: true }), '<p onclick="alert(1)">x</p>');
           
           input = '<p onclick="javascript:x">x</p>';
-          equals(minify(input, { shouldCleanAttributes: true, shouldRemoveAttributeQuotes: true }), '<p onclick=x>x</p>');
+          equals(minify(input, { cleanAttributes: true, removeAttributeQuotes: true }), '<p onclick=x>x</p>');
           
           input = '<p class=" foo bar  ">foo bar baz</p>';
-          equals(minify(input, { shouldCleanAttributes: true }), '<p class="foo bar">foo bar baz</p>');
+          equals(minify(input, { cleanAttributes: true }), '<p class="foo bar">foo bar baz</p>');
           
           input = '<p class=" foo      ">foo bar baz</p>';
-          equals(minify(input, { shouldCleanAttributes: true }), '<p class="foo">foo bar baz</p>');
-          equals(minify(input, { shouldCleanAttributes: true, shouldRemoveAttributeQuotes: true }), '<p class=foo>foo bar baz</p>');
+          equals(minify(input, { cleanAttributes: true }), '<p class="foo">foo bar baz</p>');
+          equals(minify(input, { cleanAttributes: true, removeAttributeQuotes: true }), '<p class=foo>foo bar baz</p>');
           
           input = '<p class="\n  \n foo   \n\n\t  \t\n   ">foo bar baz</p>';
-          equals(minify(input, { shouldCleanAttributes: true }), '<p class="foo">foo bar baz</p>');
+          equals(minify(input, { cleanAttributes: true }), '<p class="foo">foo bar baz</p>');
           
           input = '<p class="\n  \n foo   \n\n\t  \t\n  class1 class-23 ">foo bar baz</p>';
-          equals(minify(input, { shouldCleanAttributes: true }), '<p class="foo class1 class-23">foo bar baz</p>');
+          equals(minify(input, { cleanAttributes: true }), '<p class="foo class1 class-23">foo bar baz</p>');
         });
         
         test('removing attribute quotes', function(){
           var input = '<p title="blah" class="a23B-foo.bar_baz:qux" id="moo">foo</p>';
-          equals(minify(input, { shouldRemoveAttributeQuotes: true }), '<p title=blah class=a23B-foo.bar_baz:qux id=moo>foo</p>');
+          equals(minify(input, { removeAttributeQuotes: true }), '<p title=blah class=a23B-foo.bar_baz:qux id=moo>foo</p>');
           
           input = '<input value="hello world">';
-          equals(minify(input, { shouldRemoveAttributeQuotes: true }), '<input value="hello world">');
+          equals(minify(input, { removeAttributeQuotes: true }), '<input value="hello world">');
           
           input = '<a href="#" title="foo#bar">x</a>';
-          equals(minify(input, { shouldRemoveAttributeQuotes: true }), '<a href="#" title="foo#bar">x</a>');
+          equals(minify(input, { removeAttributeQuotes: true }), '<a href="#" title="foo#bar">x</a>');
           
           input = '<a href="http://example.com" title="blah">\nfoo\n\n</a>';
-          equals(minify(input, { shouldRemoveAttributeQuotes: true }), '<a href="http://example.com" title=blah>\nfoo\n\n</a>');
+          equals(minify(input, { removeAttributeQuotes: true }), '<a href="http://example.com" title=blah>\nfoo\n\n</a>');
         });
         
         test('collapsing whitespace', function() {
           var input = '<script type="text/javascript">  \n\t   alert(1) \n\n\n  \t <\/script>', 
               output = '<script type="text/javascript">alert(1)<\/script>';
-              
-          equals(minify(input, { shouldCollapseWhitespace: true }), output);
+          equals(minify(input, { collapseWhitespace: true }), output);
           
           input = '<p>foo</p>    <p> bar</p>\n\n   \n\t\t  <div title="quz">baz  </div>';
           output = '<p>foo</p><p>bar</p><div title="quz">baz</div>';
+          equals(minify(input, { collapseWhitespace: true }), output);
+          
+          input = '<p> foo    bar</p>';
+          output = '<p>foo bar</p>';
+          equals(minify(input, { collapseWhitespace: true }), output);
+          
+          input = '<p> foo    <span>  blah    22 </span> bar <img src=""></p>';
+          output = '<p>foo<span>blah 22</span>bar<img src=""></p>';
+          equals(minify(input, { collapseWhitespace: true }), output);
+          
+          input = '<textarea> foo bar     baz \n\n   x \t    y </textarea>';
+          output = '<textarea> foo bar     baz \n\n   x \t    y </textarea>';
+          equals(minify(input, { collapseWhitespace: true }), output);
+          
+          input = '<pre title="some title...">   hello     world </pre>';
+          output = '<pre title="some title...">   hello     world </pre>';
+          equals(minify(input, { collapseWhitespace: true }), output);
           
-          equals(minify(input, { shouldCollapseWhitespace: true }), output);
+          input = '<script>alert("foo     bar")    <\/script>';
+          output = '<script>alert("foo     bar")<\/script>';
+          equals(minify(input, { collapseWhitespace: true }), output);
         });
         
         test('removing empty elements', function() {
-          equals(minify('<p>x</p>', { shouldRemoveEmptyElements: true }), '<p>x</p>');
-          equals(minify('<p></p>', { shouldRemoveEmptyElements: true }), '');
+          equals(minify('<p>x</p>', { removeEmptyElements: true }), '<p>x</p>');
+          equals(minify('<p></p>', { removeEmptyElements: 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);
+          equals(minify(input, { removeEmptyElements: true }), output);
           
           input = '<a href="http://example/com" title="hello world"></a>';
           output = '';
-          
-          equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+          equals(minify(input, { removeEmptyElements: true }), output);
           
           input = '<textarea cols="10" rows="10"></textarea>';
           output = '<textarea cols="10" rows="10"></textarea>';
-          
-          equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+          equals(minify(input, { removeEmptyElements: true }), output);
           
           input = '<div>hello<span>world</span></div>';
           output = '<div>hello<span>world</span></div>';
-          
-          equals(minify(input, { shouldRemoveEmptyElements: true }), output);
+          equals(minify(input, { removeEmptyElements: true }), output);
         });
         
       })(this);
     </script>
-    
   </body>
 </html>
\ No newline at end of file