Implement maxLineLength option
authorDuncan Beevers <duncan@dweebd.com>
Wed, 9 Jul 2014 22:51:11 +0000 (17:51 -0500)
committerDuncan Beevers <duncan@dweebd.com>
Fri, 11 Jul 2014 18:19:53 +0000 (13:19 -0500)
README.md
src/htmlminifier.js
tests/minifier.js

index 88d6d46..ab533f9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ How does HTMLMinifier compare to [another solution](http://www.willpeavy.com/min
 | `minifyCSS`                    | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/GoalSmashers/clean-css))  | `false` (could be `true`, `false`, `Object` (options)) |
 | `ignoreCustomComments`             | Array of regex'es that allow to ignore certain comments, when matched  | `[ ]` |
 | `processScripts`                   | Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.) | `[ ]` |
+| `maxLineLength`                | Specify a maximum line length. Compressed output will be split by newlines at valid html split-points. |
 
 Chunks of markup can be ignored by wrapping them with `<!-- htmlmin:ignore -->`.
 
index cb24628..a59176d 100644 (file)
       start: function( tag, attrs, unary, unarySlash ) {
 
         if (isIgnoring) {
-          buffer.push('<', tag, attrsToMarkup(attrs), unarySlash ? '/' : '', '>');
+          buffer.push('<' + tag, attrsToMarkup(attrs), unarySlash ? '/' : '', '>');
           return;
         }
 
           }
         }
 
-        buffer.push('<', tag);
+        var openTag = '<' + tag;
+        var closeTag = ((unarySlash && options.keepClosingSlash) ? '/' : '') + '>';
+        if ( attrs.length === 0) {
+          openTag += closeTag;
+        }
+
+        buffer.push(openTag);
 
         lint && lint.testElement(tag);
 
+        var token;
         for ( var i = 0, len = attrs.length; i < len; i++ ) {
           lint && lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
-          buffer.push(normalizeAttribute(attrs[i], attrs, tag, options));
+          token = normalizeAttribute(attrs[i], attrs, tag, options);
+          if ( i === len - 1 ) {
+            token += closeTag;
+          }
+          buffer.push(token);
         }
-        buffer.push(((unarySlash && options.keepClosingSlash) ? '/' : '') + '>');
       },
       end: function( tag ) {
 
         if (isIgnoring) {
-          buffer.push('</', tag, '>');
+          buffer.push('</' + tag + '>');
           return;
         }
 
         var isElementEmpty = currentChars === '' && tag === currentTag;
         if ((options.removeEmptyElements && isElementEmpty && canRemoveElement(tag))) {
           // remove last "element" from buffer, return
-          buffer.splice(buffer.lastIndexOf('<'));
+          for ( var i = buffer.length - 1; i >= 0; i-- ) {
+            if ( /^<[^\/!]/.test(buffer[i]) ) {
+              buffer.splice(i);
+              break;
+            }
+          }
           return;
         }
         else if (options.removeOptionalTags && isOptionalTag(tag)) {
         }
         else {
           // push end tag to buffer
-          buffer.push('</', options.caseSensitive ? tag : tag.toLowerCase(), '>');
+          buffer.push('</' + (options.caseSensitive ? tag : tag.toLowerCase()) + '>');
           results.push.apply(results, buffer);
         }
         // flush buffer
     });
 
     results.push.apply(results, buffer);
-    var str = results.join('');
+    var str = joinResultSegments(results, options);
     log('minified in: ' + (new Date() - t) + 'ms');
     return str;
   }
 
+  function joinResultSegments( results, options ) {
+    var str;
+    var maxLineLength = options.maxLineLength;
+    if ( maxLineLength ) {
+      var token;
+      var lines = [];
+      var line = '';
+      for ( var i = 0, len = results.length; i < len; i++ ) {
+        token = results[i];
+        if ( line.length + token.length < maxLineLength ) {
+          line += token;
+        }
+        else {
+          lines.push(line.replace(/^\n/, ''));
+          line = token;
+        }
+      }
+      lines.push(line);
+
+      str = lines.join('\n');
+    }
+    else {
+      str = results.join('');
+    }
+
+    return str;
+  }
+
   // for CommonJS enviroments, export everything
   if ( typeof exports !== 'undefined' ) {
     exports.minify = minify;
index efb61ef..314c6fe 100644 (file)
     equal(minify(input), '<script src="x"></script><noscript>x</noscript>');
   });
 
+  test('max line length', function() {
+    var options = { maxLineLength: 25 };
+
+    input = '<div data-attr="foo"></div>';
+    equal(minify(input, options), '<div data-attr="foo">\n</div>');
+
+    input = '<code>    hello   world  \n    world   hello  </code>';
+    equal(minify(input, options), '<code>\n    hello   world  \n    world   hello  \n</code>');
+
+    equal(minify('<p title="</p>">x</p>'), '<p title="</p>">x</p>');
+    equal(minify('<p title=" <!-- hello world --> ">x</p>'), '<p title=" <!-- hello world --> ">x</p>');
+    equal(minify('<p title=" <![CDATA[ \n\n foobar baz ]]> ">x</p>'), '<p title=" <![CDATA[ \n\n foobar baz ]]> ">x</p>');
+    equal(minify('<p foo-bar=baz>xxx</p>'), '<p foo-bar="baz">xxx</p>');
+    equal(minify('<p foo:bar=baz>xxx</p>'), '<p foo:bar="baz">xxx</p>');
+
+    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>';
+
+    equal(minify(input), input);
+
+    equal(minify('<script>alert(\'<!--\')<\/script>', options), '<script>alert(\'<!--\')\n<\/script>');
+    equal(minify('<script>alert(\'<!-- foo -->\')<\/script>', options), '<script>\nalert(\'<!-- foo -->\')\n<\/script>');
+    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 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>'
+    );
+
+    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>'
+    );
+    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>ok</a>', options), '<a href>ok</a>');
+  });
+
 })(typeof exports === 'undefined' ? window : exports);