strips space between tag attributes
authoralexlamsl <alexlamsl@gmail.com>
Wed, 20 Jan 2016 12:21:47 +0000 (20:21 +0800)
committeralexlamsl <alexlamsl@gmail.com>
Wed, 20 Jan 2016 12:21:47 +0000 (20:21 +0800)
README.md
benchmark.conf
src/htmlminifier.js
src/htmlparser.js
tests/minifier.js

index ad0b2b7..746ce3f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Pe
 | `collapseInlineTagWhitespace`  | Don't leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
 | `preserveLineBreaks`           | Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break. Must be used in conjunction with `collapseWhitespace=true` | `false` |
 | `collapseBooleanAttributes`    | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier/#collapse_boolean_attributes) | `false` |
+| `removeTagWhitespace`          | Remove space between attributes whenever possible. | `false` |
 | `removeAttributeQuotes`        | [Remove quotes around attributes when possible.](http://perfectionkills.com/experimenting-with-html-minifier/#remove_attribute_quotes) | `false` |
 | `removeRedundantAttributes`    | [Remove attributes when value matches default.](http://perfectionkills.com/experimenting-with-html-minifier/#remove_redundant_attributes) | `false` |
 | `preventAttributesEscaping`    | Prevents the escaping of the values of attributes. | `false` |
@@ -61,7 +62,7 @@ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Pe
 | `customAttrAssign`             | Arrays of regex'es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[ ]` |
 | `customAttrSurround`           | Arrays of regex'es that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[ ]` |
 | `customAttrCollapse`           | Regex that specifies custom attribute to strip newlines from (e.g. `/ng\-class/`) | |
-| `quoteCharacter`               | Type of quote to use for attribute values (' or ") | |
+| `quoteCharacter`               | Type of quote to use for attribute values (' or ") | |
 
 ## Special cases
 
index 7d63462..9c21889 100644 (file)
@@ -5,6 +5,7 @@
   "collapseWhitespace": true,
   "conservativeCollapse": false,
   "collapseBooleanAttributes": true,
+  "removeTagWhitespace": true,
   "removeAttributeQuotes": true,
   "removeRedundantAttributes": true,
   "useShortDoctype": true,
index 8d490ec..cb82093 100644 (file)
         }
       }
       emittedAttrValue = attrQuote + attrValue + attrQuote;
+      if (!isLast && !options.removeTagWhitespace) {
+        emittedAttrValue += ' ';
+      }
     }
     // make sure trailing slash is not interpreted as HTML self-closing tag
-    else if (isLast && (hasUnarySlash || /\/$/.test(attrValue))) {
-      emittedAttrValue = attrValue + ' ';
+    else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
+      emittedAttrValue = attrValue;
     }
     else {
-      emittedAttrValue = attrValue;
+      emittedAttrValue = attrValue + ' ';
     }
 
     if (attrValue === undefined || (options.collapseBooleanAttributes &&
         isBooleanAttribute(attrName, attrValue))) {
       attrFragment = attrName;
+      if (!isLast) {
+        attrFragment += ' ';
+      }
     }
     else {
       attrFragment = attrName + attr.customAssign + emittedAttrValue;
     }
 
-    return (' ' + attr.customOpen + attrFragment + attr.customClose);
+    return attr.customOpen + attrFragment + attr.customClose;
   }
 
   function setDefaultTesters(options) {
           lint.testElement(tag);
         }
 
+        var parts = [ ];
         var token, isLast = true;
-        var insert = buffer.length;
         for (var i = attrs.length; --i >= 0; ) {
           if (lint) {
             lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].value);
           token = normalizeAttribute(attrs[i], attrs, tag, hasUnarySlash, i, options, isLast);
           if (token) {
             isLast = false;
-            buffer.splice(insert, 0, token);
+            parts.unshift(token);
           }
         }
+        if (parts.length > 0) {
+          buffer.push(' ');
+          buffer.push.apply(buffer, parts);
+        }
 
         buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
       },
index c1e07c9..d9c1c61 100644 (file)
@@ -91,7 +91,9 @@
         // Capture the custom attribute opening and closing markup surrounding the standard attribute rules
         attrClauses[i] = '(?:\\s*'
           + handler.customAttrSurround[i][0].source
+          + '\\s*'
           + startTagAttrs.source
+          + '\\s*'
           + handler.customAttrSurround[i][1].source
           + ')';
       }
       var attrClauses = [];
       for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
         attrClauses[i] = '(?:'
-          + '(' + handler.customAttrSurround[i][0].source + ')'
+          + '(' + handler.customAttrSurround[i][0].source + ')\\s*'
           + singleAttr.source
-          + '(' + handler.customAttrSurround[i][1].source + ')'
+          + '\\s*(' + handler.customAttrSurround[i][1].source + ')'
           + ')';
       }
       attrClauses.unshift('(?:' + singleAttr.source + ')');
index 18533b8..e625fd0 100644 (file)
     input = '<input {{#unless value}}checked="checked"{{/unless}}>';
     equal(minify(input, customAttrOptions), input);
 
-    input = '<input {{#if value1}}data-attr="example"{{/if}} {{#unless value2}}checked="checked"{{/unless}}>';
+    input = '<input {{#if value1}}data-attr="example" {{/if}}{{#unless value2}}checked="checked"{{/unless}}>';
     equal(minify(input, customAttrOptions), input);
 
     input = '<input checked="checked">';
     input = '<input {{#if value}}checked="checked"{{/if}}>';
     equal(minify(input, customAttrOptions), '<input {{#if value}}checked{{/if}}>');
 
-    input = '<input {{#if value1}}checked="checked"{{/if}} {{#if value2}}data-attr="foo"{{/if}}>';
-    equal(minify(input, customAttrOptions), '<input {{#if value1}}checked{{/if}} {{#if value2}}data-attr=foo{{/if}}>');
+    input = '<input {{#if value1}}checked="checked"{{/if}} {{#if value2}}data-attr="foo"{{/if}}/>';
+    equal(minify(input, customAttrOptions), '<input {{#if value1}}checked {{/if}}{{#if value2}}data-attr=foo{{/if}}>');
+
+    customAttrOptions.keepClosingSlash = true;
+    equal(minify(input, customAttrOptions), '<input {{#if value1}}checked {{/if}}{{#if value2}}data-attr=foo {{/if}}/>');
   });
 
   test('preserving custom attribute-joining markup', function() {
     equal(minify('<script>alert(\'-->\')<\/script>', options), '<script>alert(\'-->\')\n<\/script>');
 
     equal(minify('<a title="x"href=" ">foo</a>', options), '<a title="x" href="">foo\n</a>');
-    equal(minify('<p id=""class=""title="">x', options), '<p id="" class=""\n title="">x</p>');
+    equal(minify('<p id=""class=""title="">x', options), '<p id="" class="" \ntitle="">x</p>');
     equal(minify('<p x="x\'"">x</p>', options), '<p x="x\'">x</p>', 'trailing quote should be ignored');
     equal(minify('<a href="#"><p>Click me</p></a>', options), '<a href="#"><p>Click me\n</p></a>');
     equal(minify('<span><button>Hit me</button></span>', options), '<span><button>Hit me\n</button></span>');
     equal(minify('<object type="image/svg+xml" data="image.svg"><div>[fallback image]</div></object>', options),
-      '<object\n type="image/svg+xml"\n data="image.svg"><div>\n[fallback image]</div>\n</object>'
+      '<object \ntype="image/svg+xml" \ndata="image.svg"><div>\n[fallback image]</div>\n</object>'
     );
 
     equal(minify('<ng-include src="x"></ng-include>', options), '<ng-include src="x">\n</ng-include>');
     equal(minify('<ng:include src="x"></ng:include>', options), '<ng:include src="x">\n</ng:include>');
     equal(minify('<ng-include src="\'views/partial-notification.html\'"></ng-include><div ng-view=""></div>', options),
-      '<ng-include\n src="\'views/partial-notification.html\'">\n</ng-include><div\n ng-view=""></div>'
+      '<ng-include \nsrc="\'views/partial-notification.html\'">\n</ng-include><div \nng-view=""></div>'
     );
     equal(minify('<some-tag-1></some-tag-1><some-tag-2></some-tag-2>', options),
       '<some-tag-1>\n</some-tag-1>\n<some-tag-2>\n</some-tag-2>'
     );
     equal(minify('[\']["]', options), '[\']["]');
     equal(minify('<a href="test.html"><div>hey</div></a>', options), '<a href="test.html">\n<div>hey</div></a>');
-    equal(minify(':) <a href="http://example.com">link</a>', options), ':) <a\n href="http://example.com">\nlink</a>');
+    equal(minify(':) <a href="http://example.com">link</a>', options), ':) <a \nhref="http://example.com">\nlink</a>');
 
     equal(minify('<a href>ok</a>', options), '<a href>ok</a>');
   });
     equal(minify('<div class="bar">foo</div>', { quoteCharacter: 'm' }), '<div class="bar">foo</div>');
   });
 
+  test('remove space between attributes', function() {
+    var input, output;
+    var options = {
+      collapseBooleanAttributes: true,
+      keepClosingSlash: true,
+      removeAttributeQuotes: true,
+      removeTagWhitespace: true
+    };
+
+    input = '<input data-attr="example" value="hello world!" checked="checked">';
+    output = '<input data-attr=example value="hello world!"checked>';
+    equal(minify(input, options), output);
+
+    input = '<input checked="checked" value="hello world!" data-attr="example">';
+    output = '<input checked value="hello world!"data-attr=example>';
+    equal(minify(input, options), output);
+
+    input = '<input checked="checked" data-attr="example" value="hello world!">';
+    output = '<input checked data-attr=example value="hello world!">';
+    equal(minify(input, options), output);
+
+    input = '<input data-attr="example" value="hello world!" checked="checked"/>';
+    output = '<input data-attr=example value="hello world!"checked/>';
+    equal(minify(input, options), output);
+
+    input = '<input checked="checked" value="hello world!" data-attr="example"/>';
+    output = '<input checked value="hello world!"data-attr=example />';
+    equal(minify(input, options), output);
+
+    input = '<input checked="checked" data-attr="example" value="hello world!"/>';
+    output = '<input checked data-attr=example value="hello world!"/>';
+    equal(minify(input, options), output);
+  });
+
 })(typeof exports === 'undefined' ? window : exports);