Multiple attribute-surrounding expressions
authorDuncan Beevers <duncan@dweebd.com>
Tue, 8 Jul 2014 18:56:42 +0000 (13:56 -0500)
committerDuncan Beevers <duncan@dweebd.com>
Wed, 9 Jul 2014 07:03:53 +0000 (02:03 -0500)
customAttrOpen / customAttrClose replaced by customAttrSurround

customAttrSurround is an array of tuples.
Each tuple represents an open/close regexp pair.

src/htmlminifier.js
src/htmlparser.js
tests/minifier.js

index 060447e..abae81c 100644 (file)
       doctype: function(doctype) {
         buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
       },
-      customAttrOpen: options.customAttrOpen,
-      customAttrClose: options.customAttrClose
+      customAttrSurround: options.customAttrSurround
     });
 
     results.push.apply(results, buffer);
index cecd65a..35435fd 100644 (file)
 
   var reCache = {}, stackedTag, reStackedTag, tagMatch;
 
-  var HTMLParser = global.HTMLParser = function( html, handler ) {
-    var index, chars, match, stack = [], last = html, prevTag, nextTag;
-    stack.last = function() {
-      return this[ this.length - 1 ];
-    };
-
-    var startTag, attr;
+  function startTagForHandler( handler ) {
     var customStartTagAttrs;
-    if (handler.customAttrOpen && handler.customAttrClose) {
+
+    if ( handler.customAttrSurround ) {
+      var attrClauses = [];
+
+      for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
+        // Capture the custom attribute opening and closing markup surrounding the standard attribute rules
+        attrClauses[i] = '(?:\\s*'
+          + handler.customAttrSurround[i][0].source
+          + startTagAttrs.source
+          + handler.customAttrSurround[i][1].source
+          + ')';
+      }
+      attrClauses.unshift(startTagAttrs.source);
+
       customStartTagAttrs = new RegExp(
-        '(' +
-          '(?:' +
-            // Capture the custom attribute opening and closing markup surrounding the standard attribute rules
-            '(?:\\s*' + handler.customAttrOpen.source + startTagAttrs.source + handler.customAttrClose.source + ')' +
-            '|' +
-            // Or capture just the standard attribute rules
-            startTagAttrs.source +
-          ')*' +
-        ')'
-      );
-      attr = new RegExp(
-        '(?:' +
-          '(' + handler.customAttrOpen.source + ')' +
-          singleAttr.source +
-          '(' + handler.customAttrClose.source + ')' +
-        ')|(?:' + singleAttr.source + ')', 'g'
+        '((?:' + attrClauses.join('|') + ')*)'
       );
     }
     else {
       // No custom attribute wrappers specified, so just capture the standard attribute rules
       customStartTagAttrs = new RegExp('(' + startTagAttrs.source + ')');
-      attr = new RegExp(singleAttr.source, 'g');
     }
 
-    startTag = new RegExp(startTagOpen.source + customStartTagAttrs.source + startTagClose.source);
+    return new RegExp(startTagOpen.source + customStartTagAttrs.source + startTagClose.source);
+  }
+
+  function attrForHandler( handler ) {
+    if ( handler.customAttrSurround ) {
+      var attrClauses = [];
+      for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
+        attrClauses[i] = '(?:'
+          + '(' + handler.customAttrSurround[i][0].source + ')'
+          + singleAttr.source
+          + '(' + handler.customAttrSurround[i][1].source + ')'
+          + ')';
+      }
+      attrClauses.unshift('(?:' + singleAttr.source + ')');
+
+      return new RegExp(attrClauses.join('|'), 'g');
+    }
+    else {
+      return new RegExp(singleAttr.source, 'g');
+    }
+  }
+
+
+  var HTMLParser = global.HTMLParser = function( html, handler ) {
+    var index, chars, match, stack = [], last = html, prevTag, nextTag;
+    stack.last = function() {
+      return this[ this.length - 1 ];
+    };
+
+    var startTag = startTagForHandler(handler);
+    var attr = attrForHandler(handler);
 
     while ( html ) {
       chars = true;
       if ( handler.start ) {
         var attrs = [];
 
-        rest.replace(attr, function() {
+        rest.replace(attr, function () {
           var name, value, fallbackValue, customOpen, customClose;
 
-          if (arguments.length === 7) {
-            name = arguments[1];
+          name = arguments[1];
+          if ( name ) {
             fallbackValue = arguments[2];
-            value = fallbackValue
-              || arguments[3]
-              || arguments[4];
+            value = fallbackValue || arguments[3] || arguments[4];
           }
-          else {
-            name = arguments[2] || arguments[7];
-            fallbackValue = arguments[3];
-            value = fallbackValue
-              || arguments[4]
-              || arguments[5]
-              || arguments[8]
-              || arguments[9]
-              || arguments[10];
-            customOpen = arguments[1];
-            customClose = arguments[6];
+          else if ( handler.customAttrSurround ) {
+            for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
+              name = arguments[i * 6 + 6];
+              if ( name ) {
+                fallbackValue = arguments[i * 6 + 7];
+                value = fallbackValue
+                  || arguments[i * 6 + 8]
+                  || arguments[i * 6 + 9];
+                customOpen = arguments[i * 6 + 5];
+                customClose = arguments[i * 6 + 10];
+                break;
+              }
+            }
           }
 
           if ( value === undefined ) {
index 31f3a95..c64b556 100644 (file)
   });
 
   test('preserving custom attribute-wrapping markup', function() {
-    var customAttrOptions = {
-      customAttrOpen: /\{\{#if\s+\w+\}\}/,
-      customAttrClose: /\{\{\/if\}\}/
+    var customAttrOptions;
+
+    // With a single rule
+    customAttrOptions = {
+      customAttrSurround: [ [ /\{\{#if\s+\w+\}\}/, /\{\{\/if\}\}/ ] ]
+    };
+
+    input = '<input {{#if value}}checked="checked"{{/if}}>';
+    equal(minify(input, customAttrOptions), input);
+
+    input = '<input checked="checked">';
+    equal(minify(input, customAttrOptions), input);
+
+    // With multiple rules
+    customAttrOptions = {
+      customAttrSurround: [
+        [ /\{\{#if\s+\w+\}\}/, /\{\{\/if\}\}/ ],
+        [ /\{\{#unless\s+\w+\}\}/, /\{\{\/unless\}\}/ ]
+      ]
     };
 
     input = '<input {{#if value}}checked="checked"{{/if}}>';
-    equal(minify(input, customAttrOptions), '<input {{#if value}}checked="checked"{{/if}}>');
+    equal(minify(input, customAttrOptions), input);
+
+    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}}>';
+    equal(minify(input, customAttrOptions), input);
 
     input = '<input checked="checked">';
-    equal(minify(input, customAttrOptions), '<input checked="checked">');
+    equal(minify(input, customAttrOptions), input);
+
+    // With multiple rules and richer options
+    customAttrOptions = {
+      customAttrSurround: [
+        [ /\{\{#if\s+\w+\}\}/, /\{\{\/if\}\}/ ],
+        [ /\{\{#unless\s+\w+\}\}/, /\{\{\/unless\}\}/ ]
+      ],
+      collapseBooleanAttributes: true,
+      removeAttributeQuotes: true
+    };
+
+    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}}>');
   });
 
   test('collapsing whitespace', function() {