From: Nikola Kotur Date: Thu, 13 Feb 2014 23:14:59 +0000 (+0100) Subject: Add an option to keep closing slash in singleton tags X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=2f03f301e876421ca08facbc9be9606117d3f7a4;p=html-minifier.git Add an option to keep closing slash in singleton tags If ``keepClosingSlash`` is set to ``true`` in options, closing singleton slash will be preserved. This is important for HTML5 being minified, since in HTML5 closing slash is not optional and affects the DOM significantly. Closes #76. --- diff --git a/dist/all.js b/dist/all.js index 3f76936..e2c8a08 100644 --- a/dist/all.js +++ b/dist/all.js @@ -29,6 +29,7 @@ // Regular Expressions for parsing tags and attributes var startTag = /^<([\w:-]+)((?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([\w:-]+)[^>]*>/, + endingSlash = /\/>$/, attr = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g, doctype = /^]+>/i, startIgnore = /<(%|\?)/, @@ -175,6 +176,7 @@ parseEndTag(); function parseStartTag( tag, tagName, rest, unary ) { + var unarySlash = false; while ( !handler.html5 && stack.last() && inline[ stack.last() ]) { parseEndTag( "", stack.last() ); @@ -186,8 +188,11 @@ unary = empty[ tagName ] || !!unary; - if ( !unary ) + if ( !unary ) { stack.push( tagName ); + } else { + unarySlash = tag.match( endingSlash ); + } if ( handler.start ) { var attrs = []; @@ -205,7 +210,7 @@ }); if ( handler.start ) - handler.start( tagName, attrs, unary ); + handler.start( tagName, attrs, unary, unarySlash ); } } @@ -686,7 +691,7 @@ HTMLParser(value, { html5: typeof options.html5 !== 'undefined' ? options.html5 : true, - start: function( tag, attrs ) { + start: function( tag, attrs, unary, unarySlash ) { tag = tag.toLowerCase(); currentTag = tag; currentChars = ''; @@ -710,8 +715,7 @@ lint && lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped); buffer.push(normalizeAttribute(attrs[i], attrs, tag, options)); } - - buffer.push('>'); + buffer.push(((unarySlash && options.keepClosingSlash) ? '/' : '') + '>'); }, end: function( tag ) { // check if current tag is in a whitespace stack diff --git a/dist/all.min.js b/dist/all.min.js index f0c182d..8f0fd76 100644 --- a/dist/all.min.js +++ b/dist/all.min.js @@ -1,25 +1 @@ -/*! - * HTML Parser By John Resig (ejohn.org) - * Modified by Juriy "kangax" Zaytsev - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * HTMLParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * // or to get an XML string: - * HTMLtoXML(htmlString); - * - * // or to get an XML DOM Document - * HTMLtoDOM(htmlString); - * - * // or to inject into an existing document/DOM node - * HTMLtoDOM(htmlString, document); - * HTMLtoDOM(htmlString, document.body); - * - */(function(e){function m(e){var t={},n=e.split(",");for(var r=0;r\s]+))?)*)\s*(\/?)>/,n=/^<\/([\w:-]+)[^>]*>/,r=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,i=/^]+>/i,s=/<(%|\?)/;endIgnore=/(%|\?)>/;var o=m("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed"),u=m("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"),a=m("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),f=m("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"),l=m("script,style"),c={},h,p,d,v=e.HTMLParser=function(e,v){function T(e,t,n,i){while(!v.html5&&b.last()&&u[b.last()])N("",b.last());a[t]&&b.last()===t&&N("",t),i=o[t]||!!i,i||b.push(t);if(v.start){var s=[];n.replace(r,function(e,t){var n=arguments[2]?arguments[2]:arguments[3]?arguments[3]:arguments[4]?arguments[4]:f[t]?t:"";s.push({name:t,value:n,escaped:n.replace(/(^|[^\\])"/g,"$1"")})}),v.start&&v.start(t,s,i)}}function N(e,t){var n;if(!t)n=0;else for(n=b.length-1;n>=0;n--)if(b[n]===t)break;if(n>=0){for(var r=b.length-1;r>=n;r--)v.end&&v.end(b[r]);b.length=n}}var m,g,y,b=[],w=e,E,S;b.last=function(){return this[this.length-1]};while(e){g=!0;if(!b.last()||!l[b.last()]){e.indexOf(""),m>=0&&(v.comment&&v.comment(e.substring(4,m)),e=e.substring(m+3),g=!1)):e.search(s)===0?(m=e.search(endIgnore),m>=0&&(v.ignore&&v.ignore(e.substring(0,m+2)),e=e.substring(m+2),g=!1)):(y=i.exec(e))?(v.doctype&&v.doctype(y[0]),e=e.substring(y[0].length),g=!1):e.indexOf("]*>","i")),e=e.replace(p,function(e,t){return h!=="script"&&h!=="style"&&(t=t.replace(//g,"$1").replace(//g,"$1")),v.chars&&v.chars(t),""}),N("",h);if(e===w)throw"Parse Error: "+e;w=e}N()};e.HTMLtoXML=function(e){var t="";return v(e,{start:function(e,n,r){t+="<"+e;for(var i=0;i"},end:function(e){t+=""},chars:function(e){t+=e},comment:function(e){t+=""},ignore:function(e){t+=e}}),t},e.HTMLtoDOM=function(e,t){var n=m("html,head,body,title"),r={link:"head",base:"head"};t?t=t.ownerDocument||t.getOwnerDocument&&t.getOwnerDocument()||t:typeof DOMDocument!="undefined"?t=new DOMDocument:typeof document!="undefined"&&document.implementation&&document.implementation.createDocument?t=document.implementation.createDocument("","",null):typeof ActiveX!="undefined"&&(t=new ActiveXObject("Msxml.DOMDocument"));var i=[],s=t.documentElement||t.getDocumentElement&&t.getDocumentElement();!s&&t.createElement&&function(){var e=t.createElement("html"),n=t.createElement("head");n.appendChild(t.createElement("title")),e.appendChild(n),e.appendChild(t.createElement("body")),t.appendChild(e)}();if(t.getElementsByTagName)for(var o in n)n[o]=t.getElementsByTagName(o)[0];var u=n.body;return v(e,{start:function(e,s,o){if(n[e]){u=n[e];return}var a=t.createElement(e);for(var f in s)a.setAttribute(s[f].name,s[f].value);r[e]&&typeof n[r[e]]!="boolean"?n[r[e]].appendChild(a):u&&u.appendChild&&u.appendChild(a),o||(i.push(a),u=a)},end:function(){i.length-=1,u=i[i.length-1]},chars:function(e){u.appendChild(t.createTextNode(e))},comment:function(){},ignore:function(){}}),t}})(typeof exports=="undefined"?this:exports),function(e){function i(e){return e.replace(/\s+/g," ")}function s(e,t,n){var r=["a","abbr","acronym","b","bdi","bdo","big","button","cite","code","del","dfn","em","font","i","ins","kbd","mark","q","rt","rp","s","samp","small","span","strike","strong","sub","sup","time","tt","u","var"];return t&&t!=="img"&&(t.substr(0,1)!=="/"||t.substr(0,1)==="/"&&r.indexOf(t.substr(1))===-1)&&(e=e.replace(/^\s+/,"")),n&&n!=="img"&&(n.substr(0,1)==="/"||n.substr(0,1)!=="/"&&r.indexOf(n)===-1)&&(e=e.replace(/\s+$/,"")),t&&n?e.replace(/[\t\n\r]+/g," ").replace(/[ ]+/g," "):e}function o(e){return/\[if[^\]]+\]/.test(e)||/\s*(]+$/.test(e)&&!/\/$/.test(e)&&!/\/$/.test(e)}function l(e,t){for(var n=e.length;n--;)if(e[n].name.toLowerCase()===t)return!0;return!1}function c(e,t,n,i){return n=r(n.toLowerCase()),e==="script"&&t==="language"&&n==="javascript"||e==="form"&&t==="method"&&n==="get"||e==="input"&&t==="type"&&n==="text"||e==="script"&&t==="charset"&&!l(i,"src")||e==="a"&&t==="name"&&l(i,"id")||e==="area"&&t==="shape"&&n==="rect"}function h(e,t,n){return e==="script"&&t==="type"&&r(n.toLowerCase())==="text/javascript"}function p(e,t,n){return(e==="style"||e==="link")&&t==="type"&&r(n.toLowerCase())==="text/css"}function d(e){return/^(?:allowfullscreen|async|autofocus|checked|compact|declare|default|defer|disabled|formnovalidate|hidden|inert|ismap|itemscope|multiple|muted|nohref|noresize|noshade|novalidate|nowrap|open|readonly|required|reversed|seamless|selected|sortable|truespeed|typemustmatch)$/.test(e)}function v(e,t){return/^(?:a|area|link|base)$/.test(t)&&e==="href"||t==="img"&&/^(?:src|longdesc|usemap)$/.test(e)||t==="object"&&/^(?:classid|codebase|data|usemap)$/.test(e)||t==="q"&&e==="cite"||t==="blockquote"&&e==="cite"||(t==="ins"||t==="del")&&e==="cite"||t==="form"&&e==="action"||t==="input"&&(e==="src"||e==="usemap")||t==="head"&&e==="profile"||t==="script"&&(e==="src"||e==="for")}function m(e,t){return/^(?:a|area|object|button)$/.test(t)&&e==="tabindex"||t==="input"&&(e==="maxlength"||e==="tabindex")||t==="select"&&(e==="size"||e==="tabindex")||t==="textarea"&&/^(?:rows|cols|tabindex)$/.test(e)||t==="colgroup"&&e==="span"||t==="col"&&e==="span"||(t==="th"||t==="td")&&(e==="rowspan"||e==="colspan")}function g(e,t,n){return a(t)?r(n).replace(/^javascript:\s*/i,"").replace(/\s*;$/,""):t==="class"?i(r(n)):v(t,e)||m(t,e)?r(n):t==="style"?r(n).replace(/\s*;\s*$/,""):n}function y(e){return e.replace(/^(\[[^\]]+\]>)\s*/,"$1").replace(/\s*(\s*\*\/|\/\/\s*\]\]>)\s*$/,"")}function S(e,t){return e.replace(w[t],"").replace(E[t],"")}function x(e){return/^(?:html|t?body|t?head|tfoot|tr|td|th|option|source)$/.test(e)}function N(e,t,n){var r=/^(["'])?\s*\1$/.test(n);return r?e==="input"&&t==="value"||T.test(t):!1}function C(e){return e!=="textarea"}function k(e){return!/^(?:script|style|pre|textarea)$/.test(e)}function L(e){return!/^(?:pre|textarea)$/.test(e)}function A(e,t,n,r){var i=r.caseSensitive?e.name:e.name.toLowerCase(),s=e.escaped,o;if(r.removeRedundantAttributes&&c(n,i,s,t)||r.removeScriptTypeAttributes&&h(n,i,s)||r.removeStyleLinkTypeAttributes&&p(n,i,s))return"";s=g(n,i,s);if(!r.removeAttributeQuotes||!f(s))s='"'+s+'"';return r.removeEmptyAttributes&&N(n,i,s)?"":(r.collapseBooleanAttributes&&d(i)?o=i:o=i+"="+s," "+o)}function O(e){var t=["canCollapseWhitespace","canTrimWhitespace"];for(var n=0,r=t.length;n")},end:function(e){a.collapseWhitespace&&(d.length&&e===d[d.length-1]&&d.pop(),v.length&&e===v[v.length-1]&&v.pop());var t=c===""&&e===h;if(a.removeEmptyElements&&t&&C(e)){l.splice(l.lastIndexOf("<"));return}if(a.removeOptionalTags&&x(e))return;l.push(""),f.push.apply(f,l),l.length=0,c=""},chars:function(e,t,n){if(h==="script"||h==="style")a.removeCommentsFromCDATA&&(e=S(e,h)),a.removeCDATASectionsFromCDATA&&(e=b(e));a.collapseWhitespace&&(d.length||(e=t||n?s(e,t,n):r(e)),v.length||(e=i(e))),c=e,m&&m.testChars(e),l.push(e)},comment:function(e){a.removeComments?o(e)?e="":u(e)?e="":e="":e="",l.push(e)},ignore:function(e){l.push(a.removeIgnored?"":e)},doctype:function(e){l.push(a.useShortDoctype?"":i(e))}}),f.push.apply(f,l);var T=f.join("");return t("minified in: "+(new Date-g)+"ms"),T}var t,n;e.console&&e.console.log?t=function(t){e.console.log(t)}:t=function(){},e.HTMLParser?n=e.HTMLParser:typeof require=="function"&&(n=require("./htmlparser").HTMLParser);var r=function(e){return e.replace(/^\s+/,"").replace(/\s+$/,"")};String.prototype.trim&&(r=function(e){return e.trim()});var w={script:/^\s*(?:\/\/)?\s*\s*$/,style:/\s*-->\s*$/},T=new RegExp("^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(?:down|up|over|move|out)|key(?:press|down|up)))$");typeof exports!="undefined"?exports.minify=M:e.minify=M}(this),function(e){function t(e){return/^(?:b|i|big|small|hr|blink|marquee)$/.test(e)}function n(e){return/^(?:applet|basefont|center|dir|font|isindex|s|strike|u)$/.test(e)}function r(e){return/^on[a-z]+/.test(e)}function i(e){return"style"===e.toLowerCase()}function s(e,t){return t==="align"&&/^(?:caption|applet|iframe|img|imput|object|legend|table|hr|div|h[1-6]|p)$/.test(e)||t==="alink"&&e==="body"||t==="alt"&&e==="applet"||t==="archive"&&e==="applet"||t==="background"&&e==="body"||t==="bgcolor"&&/^(?:table|t[rdh]|body)$/.test(e)||t==="border"&&/^(?:img|object)$/.test(e)||t==="clear"&&e==="br"||t==="code"&&e==="applet"||t==="codebase"&&e==="applet"||t==="color"&&/^(?:base(?:font)?)$/.test(e)||t==="compact"&&/^(?:dir|[dou]l|menu)$/.test(e)||t==="face"&&/^base(?:font)?$/.test(e)||t==="height"&&/^(?:t[dh]|applet)$/.test(e)||t==="hspace"&&/^(?:applet|img|object)$/.test(e)||t==="language"&&e==="script"||t==="link"&&e==="body"||t==="name"&&e==="applet"||t==="noshade"&&e==="hr"||t==="nowrap"&&/^t[dh]$/.test(e)||t==="object"&&e==="applet"||t==="prompt"&&e==="isindex"||t==="size"&&/^(?:hr|font|basefont)$/.test(e)||t==="start"&&e==="ol"||t==="text"&&e==="body"||t==="type"&&/^(?:li|ol|ul)$/.test(e)||t==="value"&&e==="li"||t==="version"&&e==="html"||t==="vlink"&&e==="body"||t==="vspace"&&/^(?:applet|img|object)$/.test(e)||t==="width"&&/^(?:hr|td|th|applet|pre)$/.test(e)}function o(e,t){return e==="href"&&/^\s*javascript\s*:\s*void\s*(\s+0|\(\s*0\s*\))\s*$/i.test(t)}function u(){this.log=[],this._lastElement=null,this._isElementRepeated=!1}u.prototype.testElement=function(e){n(e)?this.log.push('
  • Found deprecated <'+e+"> element
  • "):t(e)?this.log.push('
  • Found presentational <'+e+"> element
  • "):this.checkRepeatingElement(e)},u.prototype.checkRepeatingElement=function(e){e==="br"&&this._lastElement==="br"?this._isElementRepeated=!0:this._isElementRepeated&&(this._reportRepeatingElement(),this._isElementRepeated=!1),this._lastElement=e},u.prototype._reportRepeatingElement=function(){this.log.push("
  • Found <br> sequence. Try replacing it with styling.
  • ")},u.prototype.testAttribute=function(e,t,n){r(t)?this.log.push('
  • Found event attribute (',t,") on <"+e+"> element
  • "):s(e,t)?this.log.push('
  • Found deprecated '+t+" attribute on <",e,"> element
  • "):i(t)?this.log.push('
  • Found style attribute on <',e,"> element
  • "):o(t,n)&&this.log.push('
  • Found inaccessible attribute (on <',e,"> element)
  • ")},u.prototype.testChars=function(e){this._lastElement="",/( \s*){2,}/.test(e)&&this.log.push("
  • Found repeating &nbsp; sequence. Try replacing it with styling.
  • ")},u.prototype.test=function(e,t,n){this.testElement(e),this.testAttribute(e,t,n)},u.prototype.populate=function(e){this._isElementRepeated&&this._reportRepeatingElement();var t;this.log.length&&e&&(t="
      "+this.log.join("")+"
    ",e.innerHTML=t)},e.HTMLLint=u}(typeof exports=="undefined"?this:exports); \ No newline at end of file +(function(global){var startTag=/^<([\w:-]+)((?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,endTag=/^<\/([\w:-]+)[^>]*>/,endingSlash=/\/>$/,attr=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,doctype=/^]+>/i,startIgnore=/<(%|\?)/,endIgnore=/(%|\?)>/;var empty=makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");var inline=makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");var closeSelf=makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source");var fillAttrs=makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");var special=makeMap("script,style");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]};while(html){chars=true;if(!stack.last()||!special[stack.last()]){if(html.indexOf("");if(index>=0){if(handler.comment)handler.comment(html.substring(4,index));html=html.substring(index+3);chars=false}}else if(html.search(startIgnore)===0){index=html.search(endIgnore);if(index>=0){handler.ignore&&handler.ignore(html.substring(0,index+2));html=html.substring(index+2);chars=false}}else if(match=doctype.exec(html)){if(handler.doctype)handler.doctype(match[0]);html=html.substring(match[0].length);chars=false}else if(html.indexOf("]*>","i"));html=html.replace(reStackedTag,function(all,text){if(stackedTag!=="script"&&stackedTag!=="style"){text=text.replace(//g,"$1").replace(//g,"$1")}if(handler.chars)handler.chars(text);return""});parseEndTag("",stackedTag)}if(html===last)throw"Parse Error: "+html;last=html}parseEndTag();function parseStartTag(tag,tagName,rest,unary){var unarySlash=false;while(!handler.html5&&stack.last()&&inline[stack.last()]){parseEndTag("",stack.last())}if(closeSelf[tagName]&&stack.last()===tagName){parseEndTag("",tagName)}unary=empty[tagName]||!!unary;if(!unary){stack.push(tagName)}else{unarySlash=tag.match(endingSlash)}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:"";attrs.push({name:name,value:value,escaped:value.replace(/(^|[^\\])"/g,"$1"")})});if(handler.start)handler.start(tagName,attrs,unary,unarySlash)}}function parseEndTag(tag,tagName){var pos;if(!tagName)pos=0;else for(pos=stack.length-1;pos>=0;pos--)if(stack[pos]===tagName)break;if(pos>=0){for(var i=stack.length-1;i>=pos;i--)if(handler.end)handler.end(stack[i]);stack.length=pos}}};global.HTMLtoXML=function(html){var results="";HTMLParser(html,{start:function(tag,attrs,unary){results+="<"+tag;for(var i=0;i"},end:function(tag){results+=""},chars:function(text){results+=text},comment:function(text){results+=""},ignore:function(text){results+=text}});return results};global.HTMLtoDOM=function(html,doc){var one=makeMap("html,head,body,title");var structure={link:"head",base:"head"};if(!doc){if(typeof DOMDocument!=="undefined")doc=new DOMDocument;else if(typeof document!=="undefined"&&document.implementation&&document.implementation.createDocument)doc=document.implementation.createDocument("","",null);else if(typeof ActiveX!=="undefined")doc=new ActiveXObject("Msxml.DOMDocument")}else doc=doc.ownerDocument||doc.getOwnerDocument&&doc.getOwnerDocument()||doc;var elems=[],documentElement=doc.documentElement||doc.getDocumentElement&&doc.getDocumentElement();if(!documentElement&&doc.createElement)(function(){var html=doc.createElement("html");var head=doc.createElement("head");head.appendChild(doc.createElement("title"));html.appendChild(head);html.appendChild(doc.createElement("body"));doc.appendChild(html)})();if(doc.getElementsByTagName)for(var i in one)one[i]=doc.getElementsByTagName(i)[0];var curParentNode=one.body;HTMLParser(html,{start:function(tagName,attrs,unary){if(one[tagName]){curParentNode=one[tagName];return}var elem=doc.createElement(tagName);for(var attr in attrs)elem.setAttribute(attrs[attr].name,attrs[attr].value);if(structure[tagName]&&typeof one[structure[tagName]]!=="boolean")one[structure[tagName]].appendChild(elem);else if(curParentNode&&curParentNode.appendChild)curParentNode.appendChild(elem);if(!unary){elems.push(elem);curParentNode=elem}},end:function(){elems.length-=1;curParentNode=elems[elems.length-1]},chars:function(text){curParentNode.appendChild(doc.createTextNode(text))},comment:function(){},ignore:function(){}});return doc};function makeMap(str){var obj={},items=str.split(",");for(var i=0;i]+$/.test(value)&&!/\/$/.test(value)&&!/\/$/.test(value)}function attributesInclude(attributes,attribute){for(var i=attributes.length;i--;){if(attributes[i].name.toLowerCase()===attribute){return true}}return false}function isAttributeRedundant(tag,attrName,attrValue,attrs){attrValue=trimWhitespace(attrValue.toLowerCase());return tag==="script"&&attrName==="language"&&attrValue==="javascript"||tag==="form"&&attrName==="method"&&attrValue==="get"||tag==="input"&&attrName==="type"&&attrValue==="text"||tag==="script"&&attrName==="charset"&&!attributesInclude(attrs,"src")||tag==="a"&&attrName==="name"&&attributesInclude(attrs,"id")||tag==="area"&&attrName==="shape"&&attrValue==="rect"}function isScriptTypeAttribute(tag,attrName,attrValue){return tag==="script"&&attrName==="type"&&trimWhitespace(attrValue.toLowerCase())==="text/javascript"}function isStyleLinkTypeAttribute(tag,attrName,attrValue){return(tag==="style"||tag==="link")&&attrName==="type"&&trimWhitespace(attrValue.toLowerCase())==="text/css"}function isBooleanAttribute(attrName){return/^(?:allowfullscreen|async|autofocus|checked|compact|declare|default|defer|disabled|formnovalidate|hidden|inert|ismap|itemscope|multiple|muted|nohref|noresize|noshade|novalidate|nowrap|open|readonly|required|reversed|seamless|selected|sortable|truespeed|typemustmatch)$/.test(attrName)}function isUriTypeAttribute(attrName,tag){return/^(?:a|area|link|base)$/.test(tag)&&attrName==="href"||tag==="img"&&/^(?:src|longdesc|usemap)$/.test(attrName)||tag==="object"&&/^(?:classid|codebase|data|usemap)$/.test(attrName)||tag==="q"&&attrName==="cite"||tag==="blockquote"&&attrName==="cite"||(tag==="ins"||tag==="del")&&attrName==="cite"||tag==="form"&&attrName==="action"||tag==="input"&&(attrName==="src"||attrName==="usemap")||tag==="head"&&attrName==="profile"||tag==="script"&&(attrName==="src"||attrName==="for")}function isNumberTypeAttribute(attrName,tag){return/^(?:a|area|object|button)$/.test(tag)&&attrName==="tabindex"||tag==="input"&&(attrName==="maxlength"||attrName==="tabindex")||tag==="select"&&(attrName==="size"||attrName==="tabindex")||tag==="textarea"&&/^(?:rows|cols|tabindex)$/.test(attrName)||tag==="colgroup"&&attrName==="span"||tag==="col"&&attrName==="span"||(tag==="th"||tag==="td")&&(attrName==="rowspan"||attrName==="colspan")}function cleanAttributeValue(tag,attrName,attrValue){if(isEventAttribute(attrName)){return trimWhitespace(attrValue).replace(/^javascript:\s*/i,"").replace(/\s*;$/,"")}else if(attrName==="class"){return collapseWhitespace(trimWhitespace(attrValue))}else if(isUriTypeAttribute(attrName,tag)||isNumberTypeAttribute(attrName,tag)){return trimWhitespace(attrValue)}else if(attrName==="style"){return trimWhitespace(attrValue).replace(/\s*;\s*$/,"")}return attrValue}function cleanConditionalComment(comment){return comment.replace(/^(\[[^\]]+\]>)\s*/,"$1").replace(/\s*(\s*\*\/|\/\/\s*\]\]>)\s*$/,"")}var reStartDelimiter={script:/^\s*(?:\/\/)?\s*\s*$/,style:/\s*-->\s*$/};function removeComments(text,tag){return text.replace(reStartDelimiter[tag],"").replace(reEndDelimiter[tag],"")}function isOptionalTag(tag){return/^(?:html|t?body|t?head|tfoot|tr|td|th|option|source)$/.test(tag)}var reEmptyAttribute=new RegExp("^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse("+"?:down|up|over|move|out)|key(?:press|down|up)))$");function canDeleteEmptyAttribute(tag,attrName,attrValue){var isValueEmpty=/^(["'])?\s*\1$/.test(attrValue);if(isValueEmpty){return tag==="input"&&attrName==="value"||reEmptyAttribute.test(attrName)}return false}function canRemoveElement(tag){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=options.caseSensitive?attr.name:attr.name.toLowerCase(),attrValue=attr.escaped,attrFragment;if(options.removeRedundantAttributes&&isAttributeRedundant(tag,attrName,attrValue,attrs)||options.removeScriptTypeAttributes&&isScriptTypeAttribute(tag,attrName,attrValue)||options.removeStyleLinkTypeAttributes&&isStyleLinkTypeAttribute(tag,attrName,attrValue)){return""}attrValue=cleanAttributeValue(tag,attrName,attrValue);if(!options.removeAttributeQuotes||!canRemoveAttributeQuotes(attrValue)){attrValue='"'+attrValue+'"'}if(options.removeEmptyAttributes&&canDeleteEmptyAttribute(tag,attrName,attrValue)){return""}if(options.collapseBooleanAttributes&&isBooleanAttribute(attrName)){attrFragment=attrName}else{attrFragment=attrName+"="+attrValue}return" "+attrFragment}function setDefaultTesters(options){var defaultTesters=["canCollapseWhitespace","canTrimWhitespace"];for(var i=0,len=defaultTesters.length;i")},end:function(tag){if(options.collapseWhitespace){if(stackNoTrimWhitespace.length&&tag===stackNoTrimWhitespace[stackNoTrimWhitespace.length-1]){stackNoTrimWhitespace.pop()}if(stackNoCollapseWhitespace.length&&tag===stackNoCollapseWhitespace[stackNoCollapseWhitespace.length-1]){stackNoCollapseWhitespace.pop()}}var isElementEmpty=currentChars===""&&tag===currentTag;if(options.removeEmptyElements&&isElementEmpty&&canRemoveElement(tag)){buffer.splice(buffer.lastIndexOf("<"));return}else if(options.removeOptionalTags&&isOptionalTag(tag)){return}else{buffer.push("");results.push.apply(results,buffer)}buffer.length=0;currentChars=""},chars:function(text,prevTag,nextTag){if(currentTag==="script"||currentTag==="style"){if(options.removeCommentsFromCDATA){text=removeComments(text,currentTag)}if(options.removeCDATASectionsFromCDATA){text=removeCDATASections(text)}}if(options.collapseWhitespace){if(!stackNoTrimWhitespace.length){text=prevTag||nextTag?collapseWhitespaceSmart(text,prevTag,nextTag):trimWhitespace(text)}if(!stackNoCollapseWhitespace.length){text=collapseWhitespace(text)}}currentChars=text;lint&&lint.testChars(text);buffer.push(text)},comment:function(text){if(options.removeComments){if(isConditionalComment(text)){text=""}else if(isIgnoredComment(text)){text=""}else{text=""}}else{text=""}buffer.push(text)},ignore:function(text){buffer.push(options.removeIgnored?"":text)},doctype:function(doctype){buffer.push(options.useShortDoctype?"":collapseWhitespace(doctype))}});results.push.apply(results,buffer);var str=results.join("");log("minified in: "+(new Date-t)+"ms");return str}if(typeof exports!=="undefined"){exports.minify=minify}else{global.minify=minify}})(this);(function(global){function isPresentationalElement(tag){return/^(?:b|i|big|small|hr|blink|marquee)$/.test(tag)}function isDeprecatedElement(tag){return/^(?:applet|basefont|center|dir|font|isindex|s|strike|u)$/.test(tag)}function isEventAttribute(attrName){return/^on[a-z]+/.test(attrName)}function isStyleAttribute(attrName){return"style"===attrName.toLowerCase()}function isDeprecatedAttribute(tag,attrName){return attrName==="align"&&/^(?:caption|applet|iframe|img|imput|object|legend|table|hr|div|h[1-6]|p)$/.test(tag)||attrName==="alink"&&tag==="body"||attrName==="alt"&&tag==="applet"||attrName==="archive"&&tag==="applet"||attrName==="background"&&tag==="body"||attrName==="bgcolor"&&/^(?:table|t[rdh]|body)$/.test(tag)||attrName==="border"&&/^(?:img|object)$/.test(tag)||attrName==="clear"&&tag==="br"||attrName==="code"&&tag==="applet"||attrName==="codebase"&&tag==="applet"||attrName==="color"&&/^(?:base(?:font)?)$/.test(tag)||attrName==="compact"&&/^(?:dir|[dou]l|menu)$/.test(tag)||attrName==="face"&&/^base(?:font)?$/.test(tag)||attrName==="height"&&/^(?:t[dh]|applet)$/.test(tag)||attrName==="hspace"&&/^(?:applet|img|object)$/.test(tag)||attrName==="language"&&tag==="script"||attrName==="link"&&tag==="body"||attrName==="name"&&tag==="applet"||attrName==="noshade"&&tag==="hr"||attrName==="nowrap"&&/^t[dh]$/.test(tag)||attrName==="object"&&tag==="applet"||attrName==="prompt"&&tag==="isindex"||attrName==="size"&&/^(?:hr|font|basefont)$/.test(tag)||attrName==="start"&&tag==="ol"||attrName==="text"&&tag==="body"||attrName==="type"&&/^(?:li|ol|ul)$/.test(tag)||attrName==="value"&&tag==="li"||attrName==="version"&&tag==="html"||attrName==="vlink"&&tag==="body"||attrName==="vspace"&&/^(?:applet|img|object)$/.test(tag)||attrName==="width"&&/^(?:hr|td|th|applet|pre)$/.test(tag)}function isInaccessibleAttribute(attrName,attrValue){return attrName==="href"&&/^\s*javascript\s*:\s*void\s*(\s+0|\(\s*0\s*\))\s*$/i.test(attrValue)}function Lint(){this.log=[];this._lastElement=null;this._isElementRepeated=false}Lint.prototype.testElement=function(tag){if(isDeprecatedElement(tag)){this.log.push('
  • Found deprecated <'+tag+"> element
  • ")}else if(isPresentationalElement(tag)){this.log.push('
  • Found presentational <'+tag+"> element
  • ")}else{this.checkRepeatingElement(tag)}};Lint.prototype.checkRepeatingElement=function(tag){if(tag==="br"&&this._lastElement==="br"){this._isElementRepeated=true}else if(this._isElementRepeated){this._reportRepeatingElement();this._isElementRepeated=false}this._lastElement=tag};Lint.prototype._reportRepeatingElement=function(){this.log.push("
  • Found <br> sequence. Try replacing it with styling.
  • ")};Lint.prototype.testAttribute=function(tag,attrName,attrValue){if(isEventAttribute(attrName)){this.log.push('
  • Found event attribute (',attrName,") on <"+tag+"> element
  • ")}else if(isDeprecatedAttribute(tag,attrName)){this.log.push('
  • Found deprecated '+attrName+" attribute on <",tag,"> element
  • ")}else if(isStyleAttribute(attrName)){this.log.push('
  • Found style attribute on <',tag,"> element
  • ")}else if(isInaccessibleAttribute(attrName,attrValue)){this.log.push('
  • Found inaccessible attribute '+"(on <",tag,"> element)
  • ")}};Lint.prototype.testChars=function(chars){this._lastElement="";if(/( \s*){2,}/.test(chars)){this.log.push("
  • Found repeating &nbsp; sequence. Try replacing it with styling.
  • ")}};Lint.prototype.test=function(tag,attrName,attrValue){this.testElement(tag);this.testAttribute(tag,attrName,attrValue)};Lint.prototype.populate=function(writeToElement){if(this._isElementRepeated){this._reportRepeatingElement()}var report;if(this.log.length&&writeToElement){report="
      "+this.log.join("")+"
    ";writeToElement.innerHTML=report}};global.HTMLLint=Lint})(typeof exports==="undefined"?this:exports); \ No newline at end of file diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 834e900..ca7d946 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -320,7 +320,7 @@ HTMLParser(value, { html5: typeof options.html5 !== 'undefined' ? options.html5 : true, - start: function( tag, attrs ) { + start: function( tag, attrs, unary, unarySlash ) { tag = tag.toLowerCase(); currentTag = tag; currentChars = ''; @@ -344,8 +344,7 @@ lint && lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped); buffer.push(normalizeAttribute(attrs[i], attrs, tag, options)); } - - buffer.push('>'); + buffer.push(((unarySlash && options.keepClosingSlash) ? '/' : '') + '>'); }, end: function( tag ) { // check if current tag is in a whitespace stack diff --git a/src/htmlparser.js b/src/htmlparser.js index c2f4449..c55f12d 100644 --- a/src/htmlparser.js +++ b/src/htmlparser.js @@ -29,6 +29,7 @@ // Regular Expressions for parsing tags and attributes var startTag = /^<([\w:-]+)((?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([\w:-]+)[^>]*>/, + endingSlash = /\/>$/, attr = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g, doctype = /^]+>/i, startIgnore = /<(%|\?)/, @@ -175,6 +176,7 @@ parseEndTag(); function parseStartTag( tag, tagName, rest, unary ) { + var unarySlash = false; while ( !handler.html5 && stack.last() && inline[ stack.last() ]) { parseEndTag( "", stack.last() ); @@ -186,8 +188,11 @@ unary = empty[ tagName ] || !!unary; - if ( !unary ) + if ( !unary ) { stack.push( tagName ); + } else { + unarySlash = tag.match( endingSlash ); + } if ( handler.start ) { var attrs = []; @@ -205,7 +210,7 @@ }); if ( handler.start ) - handler.start( tagName, attrs, unary ); + handler.start( tagName, attrs, unary, unarySlash ); } } diff --git a/tests/minifier.js b/tests/minifier.js index c3c34d4..c62d3d0 100644 --- a/tests/minifier.js +++ b/tests/minifier.js @@ -69,8 +69,10 @@ test('space normalization between attributes', function() { equal(minify('

    foo

    '), '

    foo

    '); + equal(minify(''), '') equal(minify('

    foo

    '), '

    foo

    '); equal(minify('

    foo

    '), '

    foo

    '); + equal(minify(''), '') equal(minify(''), ''); }); @@ -617,6 +619,10 @@ equal(minify(input, { collapseBooleanAttributes: true }), ''); }); + test('keeping trailing slashes in tags', function(){ + equal(minify('', { keepClosingSlash: true }), ''); + }); + test('removing optional tags', function(){ input = 'hello

    foobar

    '; output = 'hello

    foobar

    ';