Attributes can be wrapped with custom markup
authorDuncan Beevers <duncan@dweebd.com>
Sat, 5 Jul 2014 19:18:38 +0000 (14:18 -0500)
committerDuncan Beevers <duncan@dweebd.com>
Sat, 5 Jul 2014 22:31:31 +0000 (17:31 -0500)
Allows for recognizing attributes wrapped by handlebars conditionals

<input {{#if condition}}checked{{/if}}>

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

index 5e160d3..060447e 100644 (file)
       attrFragment = attrName + '=' + attrValue;
     }
 
-    return (' ' + attrFragment);
+    return (' ' + attr.customOpen + attrFragment + attr.customClose);
   }
 
   function setDefaultTesters(options) {
       },
       doctype: function(doctype) {
         buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
-      }
+      },
+      customAttrOpen: options.customAttrOpen,
+      customAttrClose: options.customAttrClose
     });
 
     results.push.apply(results, buffer);
index d6e19c6..cecd65a 100644 (file)
   'use strict';
 
   // Regular Expressions for parsing tags and attributes
-  var startTag = /^<([\w:-]+)((?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
+  var startTagOpen = /^<([\w:-]+)/,
+      startTagAttrs = /(?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*/,
+      startTagClose = /\s*(\/?)>/,
       endTag = /^<\/([\w:-]+)[^>]*>/,
       endingSlash = /\/>$/,
-      attr = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
+      singleAttr = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/,
       doctype = /^<!DOCTYPE [^>]+>/i,
       startIgnore = /<(%|\?)/,
       endIgnore = /(%|\?)>/;
       return this[ this.length - 1 ];
     };
 
+    var startTag, attr;
+    var customStartTagAttrs;
+    if (handler.customAttrOpen && handler.customAttrClose) {
+      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'
+      );
+    }
+    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);
+
     while ( html ) {
       chars = true;
 
       if ( handler.start ) {
         var attrs = [];
 
-        rest.replace(attr, function(match, name) {
-          var value = arguments[2] ? arguments[2] :
-            arguments[3] ? arguments[3] :
-            arguments[4] ? arguments[4] :
-            fillAttrs[name] ? name : arguments[2];
+        rest.replace(attr, function() {
+          var name, value, fallbackValue, customOpen, customClose;
+
+          if (arguments.length === 7) {
+            name = arguments[1];
+            fallbackValue = arguments[2];
+            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];
+          }
+
+          if ( value === undefined ) {
+            value = fillAttrs[name] ? name : fallbackValue;
+          }
+
           attrs.push({
             name: name,
             value: value,
-            escaped: value && value.replace(/(^|[^\\])"/g, '$1&quot;')
+            escaped: value && value.replace(/(^|[^\\])"/g, '$1&quot;'),
+            customOpen:  customOpen || '',
+            customClose: customClose || ''
           });
         });
 
index 7242e45..31f3a95 100644 (file)
     equal(minify(input, { removeAttributeQuotes: true }), '<p class=foo|bar:baz></p>');
   });
 
+  test('preserving custom attribute-wrapping markup', function() {
+    var customAttrOptions = {
+      customAttrOpen: /\{\{#if\s+\w+\}\}/,
+      customAttrClose: /\{\{\/if\}\}/
+    };
+
+    input = '<input {{#if value}}checked="checked"{{/if}}>';
+    equal(minify(input, customAttrOptions), '<input {{#if value}}checked="checked"{{/if}}>');
+
+    input = '<input checked="checked">';
+    equal(minify(input, customAttrOptions), '<input checked="checked">');
+  });
+
   test('collapsing whitespace', function() {
     input = '<script type="text/javascript">  \n\t   alert(1) \n\n\n  \t <\/script>';
     output = '<script type="text/javascript">alert(1)<\/script>';