Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
html-minifier/src/htmlminifier.js /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1344 lines (1240 sloc)
44.8 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use strict'; | |
| var CleanCSS = require('clean-css'); | |
| var decode = require('he').decode; | |
| var HTMLParser = require('./htmlparser').HTMLParser; | |
| var RelateUrl = require('relateurl'); | |
| var TokenChain = require('./tokenchain'); | |
| var UglifyJS = require('uglify-js'); | |
| var utils = require('./utils'); | |
| function trimWhitespace(str) { | |
| return str && str.replace(/^[ \n\r\t\f]+/, '').replace(/[ \n\r\t\f]+$/, ''); | |
| } | |
| function collapseWhitespaceAll(str) { | |
| // Non-breaking space is specifically handled inside the replacer function here: | |
| return str && str.replace(/[ \n\r\t\f\xA0]+/g, function(spaces) { | |
| return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 '); | |
| }); | |
| } | |
| function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) { | |
| var lineBreakBefore = '', lineBreakAfter = ''; | |
| if (options.preserveLineBreaks) { | |
| str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function() { | |
| lineBreakBefore = '\n'; | |
| return ''; | |
| }).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function() { | |
| lineBreakAfter = '\n'; | |
| return ''; | |
| }); | |
| } | |
| if (trimLeft) { | |
| // Non-breaking space is specifically handled inside the replacer function here: | |
| str = str.replace(/^[ \n\r\t\f\xA0]+/, function(spaces) { | |
| var conservative = !lineBreakBefore && options.conservativeCollapse; | |
| if (conservative && spaces === '\t') { | |
| return '\t'; | |
| } | |
| return spaces.replace(/^[^\xA0]+/, '').replace(/(\xA0+)[^\xA0]+/g, '$1 ') || (conservative ? ' ' : ''); | |
| }); | |
| } | |
| if (trimRight) { | |
| // Non-breaking space is specifically handled inside the replacer function here: | |
| str = str.replace(/[ \n\r\t\f\xA0]+$/, function(spaces) { | |
| var conservative = !lineBreakAfter && options.conservativeCollapse; | |
| if (conservative && spaces === '\t') { | |
| return '\t'; | |
| } | |
| return spaces.replace(/[^\xA0]+(\xA0+)/g, ' $1').replace(/[^\xA0]+$/, '') || (conservative ? ' ' : ''); | |
| }); | |
| } | |
| if (collapseAll) { | |
| // strip non space whitespace then compress spaces to one | |
| str = collapseWhitespaceAll(str); | |
| } | |
| return lineBreakBefore + str + lineBreakAfter; | |
| } | |
| var createMapFromString = utils.createMapFromString; | |
| // non-empty tags that will maintain whitespace around them | |
| var inlineTags = createMapFromString('a,abbr,acronym,b,bdi,bdo,big,button,cite,code,del,dfn,em,font,i,ins,kbd,label,mark,math,nobr,object,q,rp,rt,rtc,ruby,s,samp,select,small,span,strike,strong,sub,sup,svg,textarea,time,tt,u,var'); | |
| // non-empty tags that will maintain whitespace within them | |
| var inlineTextTags = createMapFromString('a,abbr,acronym,b,big,del,em,font,i,ins,kbd,mark,nobr,rp,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var'); | |
| // self-closing tags that will maintain whitespace around them | |
| var selfClosingInlineTags = createMapFromString('comment,img,input,wbr'); | |
| function collapseWhitespaceSmart(str, prevTag, nextTag, options) { | |
| var trimLeft = prevTag && !selfClosingInlineTags(prevTag); | |
| if (trimLeft && !options.collapseInlineTagWhitespace) { | |
| trimLeft = prevTag.charAt(0) === '/' ? !inlineTags(prevTag.slice(1)) : !inlineTextTags(prevTag); | |
| } | |
| var trimRight = nextTag && !selfClosingInlineTags(nextTag); | |
| if (trimRight && !options.collapseInlineTagWhitespace) { | |
| trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags(nextTag.slice(1)) : !inlineTags(nextTag); | |
| } | |
| return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag); | |
| } | |
| function isConditionalComment(text) { | |
| return /^\[if\s[^\]]+]|\[endif]$/.test(text); | |
| } | |
| function isIgnoredComment(text, options) { | |
| for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) { | |
| if (options.ignoreCustomComments[i].test(text)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function isEventAttribute(attrName, options) { | |
| var patterns = options.customEventAttributes; | |
| if (patterns) { | |
| for (var i = patterns.length; i--;) { | |
| if (patterns[i].test(attrName)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| return /^on[a-z]{3,}$/.test(attrName); | |
| } | |
| function canRemoveAttributeQuotes(value) { | |
| // https://mathiasbynens.be/notes/unquoted-attribute-values | |
| return /^[^ \t\n\f\r"'`=<>]+$/.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 = 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' | |
| ); | |
| } | |
| // https://mathiasbynens.be/demo/javascript-mime-type | |
| // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type | |
| var executableScriptsMimetypes = utils.createMap([ | |
| 'text/javascript', | |
| 'text/ecmascript', | |
| 'text/jscript', | |
| 'application/javascript', | |
| 'application/x-javascript', | |
| 'application/ecmascript' | |
| ]); | |
| function isScriptTypeAttribute(attrValue) { | |
| attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase(); | |
| return attrValue === '' || executableScriptsMimetypes(attrValue); | |
| } | |
| function isExecutableScript(tag, attrs) { | |
| if (tag !== 'script') { | |
| return false; | |
| } | |
| for (var i = 0, len = attrs.length; i < len; i++) { | |
| var attrName = attrs[i].name.toLowerCase(); | |
| if (attrName === 'type') { | |
| return isScriptTypeAttribute(attrs[i].value); | |
| } | |
| } | |
| return true; | |
| } | |
| function isStyleLinkTypeAttribute(attrValue) { | |
| attrValue = trimWhitespace(attrValue).toLowerCase(); | |
| return attrValue === '' || attrValue === 'text/css'; | |
| } | |
| function isStyleSheet(tag, attrs) { | |
| if (tag !== 'style') { | |
| return false; | |
| } | |
| for (var i = 0, len = attrs.length; i < len; i++) { | |
| var attrName = attrs[i].name.toLowerCase(); | |
| if (attrName === 'type') { | |
| return isStyleLinkTypeAttribute(attrs[i].value); | |
| } | |
| } | |
| return true; | |
| } | |
| var isSimpleBoolean = createMapFromString('allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible'); | |
| var isBooleanValue = createMapFromString('true,false'); | |
| function isBooleanAttribute(attrName, attrValue) { | |
| return isSimpleBoolean(attrName) || attrName === 'draggable' && !isBooleanValue(attrValue); | |
| } | |
| 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 isLinkType(tag, attrs, value) { | |
| if (tag !== 'link') { | |
| return false; | |
| } | |
| for (var i = 0, len = attrs.length; i < len; i++) { | |
| if (attrs[i].name === 'rel' && attrs[i].value === value) { | |
| return true; | |
| } | |
| } | |
| } | |
| function isMediaQuery(tag, attrs, attrName) { | |
| return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs)); | |
| } | |
| var srcsetTags = createMapFromString('img,source'); | |
| function isSrcset(attrName, tag) { | |
| return attrName === 'srcset' && srcsetTags(tag); | |
| } | |
| function cleanAttributeValue(tag, attrName, attrValue, options, attrs) { | |
| if (isEventAttribute(attrName, options)) { | |
| attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, ''); | |
| return options.minifyJS(attrValue, true); | |
| } | |
| else if (attrName === 'class') { | |
| attrValue = trimWhitespace(attrValue); | |
| if (options.sortClassName) { | |
| attrValue = options.sortClassName(attrValue); | |
| } | |
| else { | |
| attrValue = collapseWhitespaceAll(attrValue); | |
| } | |
| return attrValue; | |
| } | |
| else if (isUriTypeAttribute(attrName, tag)) { | |
| attrValue = trimWhitespace(attrValue); | |
| return isLinkType(tag, attrs, 'canonical') ? attrValue : options.minifyURLs(attrValue); | |
| } | |
| else if (isNumberTypeAttribute(attrName, tag)) { | |
| return trimWhitespace(attrValue); | |
| } | |
| else if (attrName === 'style') { | |
| attrValue = trimWhitespace(attrValue); | |
| if (attrValue) { | |
| if (/;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) { | |
| attrValue = attrValue.replace(/\s*;$/, ';'); | |
| } | |
| attrValue = options.minifyCSS(attrValue, 'inline'); | |
| } | |
| return attrValue; | |
| } | |
| else if (isSrcset(attrName, tag)) { | |
| // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset | |
| attrValue = trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(function(candidate) { | |
| var url = candidate; | |
| var descriptor = ''; | |
| var match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/); | |
| if (match) { | |
| url = url.slice(0, -match[0].length); | |
| var num = +match[1].slice(0, -1); | |
| var suffix = match[1].slice(-1); | |
| if (num !== 1 || suffix !== 'x') { | |
| descriptor = ' ' + num + suffix; | |
| } | |
| } | |
| return options.minifyURLs(url) + descriptor; | |
| }).join(', '); | |
| } | |
| else if (isMetaViewport(tag, attrs) && attrName === 'content') { | |
| attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function(numString) { | |
| // "0.90000" -> "0.9" | |
| // "1.0" -> "1" | |
| // "1.0001" -> "1.0001" (unchanged) | |
| return (+numString).toString(); | |
| }); | |
| } | |
| else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') { | |
| return collapseWhitespaceAll(attrValue); | |
| } | |
| else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) { | |
| attrValue = attrValue.replace(/\n+|\r+|\s{2,}/g, ''); | |
| } | |
| else if (tag === 'script' && attrName === 'type') { | |
| attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';')); | |
| } | |
| else if (isMediaQuery(tag, attrs, attrName)) { | |
| attrValue = trimWhitespace(attrValue); | |
| return options.minifyCSS(attrValue, 'media'); | |
| } | |
| return attrValue; | |
| } | |
| function isMetaViewport(tag, attrs) { | |
| if (tag !== 'meta') { | |
| return false; | |
| } | |
| for (var i = 0, len = attrs.length; i < len; i++) { | |
| if (attrs[i].name === 'name' && attrs[i].value === 'viewport') { | |
| return true; | |
| } | |
| } | |
| } | |
| function isContentSecurityPolicy(tag, attrs) { | |
| if (tag !== 'meta') { | |
| return false; | |
| } | |
| for (var i = 0, len = attrs.length; i < len; i++) { | |
| if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') { | |
| return true; | |
| } | |
| } | |
| } | |
| function ignoreCSS(id) { | |
| return '/* clean-css ignore:start */' + id + '/* clean-css ignore:end */'; | |
| } | |
| // Wrap CSS declarations for CleanCSS > 3.x | |
| // See https://github.com/jakubpawlowicz/clean-css/issues/418 | |
| function wrapCSS(text, type) { | |
| switch (type) { | |
| case 'inline': | |
| return '*{' + text + '}'; | |
| case 'media': | |
| return '@media ' + text + '{a{top:0}}'; | |
| default: | |
| return text; | |
| } | |
| } | |
| function unwrapCSS(text, type) { | |
| var matches; | |
| switch (type) { | |
| case 'inline': | |
| matches = text.match(/^\*\{([\s\S]*)\}$/); | |
| break; | |
| case 'media': | |
| matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/); | |
| break; | |
| } | |
| return matches ? matches[1] : text; | |
| } | |
| function cleanConditionalComment(comment, options) { | |
| return options.processConditionalComments ? comment.replace(/^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, function(match, prefix, text, suffix) { | |
| return prefix + minify(text, options, true) + suffix; | |
| }) : comment; | |
| } | |
| function processScript(text, options, currentAttrs) { | |
| for (var i = 0, len = currentAttrs.length; i < len; i++) { | |
| if (currentAttrs[i].name.toLowerCase() === 'type' && | |
| options.processScripts.indexOf(currentAttrs[i].value) > -1) { | |
| return minify(text, options); | |
| } | |
| } | |
| return text; | |
| } | |
| // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags | |
| // with the following deviations: | |
| // - retain <body> if followed by <noscript> | |
| // - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags | |
| // - retain all tags which are adjacent to non-standard HTML tags | |
| var optionalStartTags = createMapFromString('html,head,body,colgroup,tbody'); | |
| var optionalEndTags = createMapFromString('html,head,body,li,dt,dd,p,rb,rt,rtc,rp,optgroup,option,colgroup,caption,thead,tbody,tfoot,tr,td,th'); | |
| var headerTags = createMapFromString('meta,link,script,style,template,noscript'); | |
| var descriptionTags = createMapFromString('dt,dd'); | |
| var pBlockTags = createMapFromString('address,article,aside,blockquote,details,div,dl,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,main,menu,nav,ol,p,pre,section,table,ul'); | |
| var pInlineTags = createMapFromString('a,audio,del,ins,map,noscript,video'); | |
| var rubyTags = createMapFromString('rb,rt,rtc,rp'); | |
| var rtcTag = createMapFromString('rb,rtc,rp'); | |
| var optionTag = createMapFromString('option,optgroup'); | |
| var tableContentTags = createMapFromString('tbody,tfoot'); | |
| var tableSectionTags = createMapFromString('thead,tbody,tfoot'); | |
| var cellTags = createMapFromString('td,th'); | |
| var topLevelTags = createMapFromString('html,head,body'); | |
| var compactTags = createMapFromString('html,body'); | |
| var looseTags = createMapFromString('head,colgroup,caption'); | |
| var trailingTags = createMapFromString('dt,thead'); | |
| var htmlTags = createMapFromString('a,abbr,acronym,address,applet,area,article,aside,audio,b,base,basefont,bdi,bdo,bgsound,big,blink,blockquote,body,br,button,canvas,caption,center,cite,code,col,colgroup,command,content,data,datalist,dd,del,details,dfn,dialog,dir,div,dl,dt,element,em,embed,fieldset,figcaption,figure,font,footer,form,frame,frameset,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,image,img,input,ins,isindex,kbd,keygen,label,legend,li,link,listing,main,map,mark,marquee,menu,menuitem,meta,meter,multicol,nav,nobr,noembed,noframes,noscript,object,ol,optgroup,option,output,p,param,picture,plaintext,pre,progress,q,rb,rp,rt,rtc,ruby,s,samp,script,section,select,shadow,small,source,spacer,span,strike,strong,style,sub,summary,sup,table,tbody,td,template,textarea,tfoot,th,thead,time,title,tr,track,tt,u,ul,var,video,wbr,xmp'); | |
| function canRemoveParentTag(optionalStartTag, tag) { | |
| switch (optionalStartTag) { | |
| case 'html': | |
| case 'head': | |
| return true; | |
| case 'body': | |
| return !headerTags(tag); | |
| case 'colgroup': | |
| return tag === 'col'; | |
| case 'tbody': | |
| return tag === 'tr'; | |
| } | |
| return false; | |
| } | |
| function isStartTagMandatory(optionalEndTag, tag) { | |
| switch (tag) { | |
| case 'colgroup': | |
| return optionalEndTag === 'colgroup'; | |
| case 'tbody': | |
| return tableSectionTags(optionalEndTag); | |
| } | |
| return false; | |
| } | |
| function canRemovePrecedingTag(optionalEndTag, tag) { | |
| switch (optionalEndTag) { | |
| case 'html': | |
| case 'head': | |
| case 'body': | |
| case 'colgroup': | |
| case 'caption': | |
| return true; | |
| case 'li': | |
| case 'optgroup': | |
| case 'tr': | |
| return tag === optionalEndTag; | |
| case 'dt': | |
| case 'dd': | |
| return descriptionTags(tag); | |
| case 'p': | |
| return pBlockTags(tag); | |
| case 'rb': | |
| case 'rt': | |
| case 'rp': | |
| return rubyTags(tag); | |
| case 'rtc': | |
| return rtcTag(tag); | |
| case 'option': | |
| return optionTag(tag); | |
| case 'thead': | |
| case 'tbody': | |
| return tableContentTags(tag); | |
| case 'tfoot': | |
| return tag === 'tbody'; | |
| case 'td': | |
| case 'th': | |
| return cellTags(tag); | |
| } | |
| return false; | |
| } | |
| 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, options) { | |
| var isValueEmpty = !attrValue || /^\s*$/.test(attrValue); | |
| if (!isValueEmpty) { | |
| return false; | |
| } | |
| if (typeof options.removeEmptyAttributes === 'function') { | |
| return options.removeEmptyAttributes(attrName, tag); | |
| } | |
| return tag === 'input' && attrName === 'value' || reEmptyAttribute.test(attrName); | |
| } | |
| function hasAttrName(name, attrs) { | |
| for (var i = attrs.length - 1; i >= 0; i--) { | |
| if (attrs[i].name === name) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function canRemoveElement(tag, attrs) { | |
| switch (tag) { | |
| case 'textarea': | |
| return false; | |
| case 'audio': | |
| case 'script': | |
| case 'video': | |
| if (hasAttrName('src', attrs)) { | |
| return false; | |
| } | |
| break; | |
| case 'iframe': | |
| if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) { | |
| return false; | |
| } | |
| break; | |
| case 'object': | |
| if (hasAttrName('data', attrs)) { | |
| return false; | |
| } | |
| break; | |
| case 'applet': | |
| if (hasAttrName('code', attrs)) { | |
| return false; | |
| } | |
| break; | |
| } | |
| return true; | |
| } | |
| function canCollapseWhitespace(tag) { | |
| return !/^(?:script|style|pre|textarea)$/.test(tag); | |
| } | |
| function canTrimWhitespace(tag) { | |
| return !/^(?:pre|textarea)$/.test(tag); | |
| } | |
| function normalizeAttr(attr, attrs, tag, options) { | |
| var attrName = options.name(attr.name), | |
| attrValue = attr.value; | |
| if (options.decodeEntities && attrValue) { | |
| attrValue = decode(attrValue, { isAttributeValue: true }); | |
| } | |
| if (options.removeRedundantAttributes && | |
| isAttributeRedundant(tag, attrName, attrValue, attrs) || | |
| options.removeScriptTypeAttributes && tag === 'script' && | |
| attrName === 'type' && isScriptTypeAttribute(attrValue) || | |
| options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') && | |
| attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) { | |
| return; | |
| } | |
| if (attrValue) { | |
| attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs); | |
| } | |
| if (options.removeEmptyAttributes && | |
| canDeleteEmptyAttribute(tag, attrName, attrValue, options)) { | |
| return; | |
| } | |
| if (options.decodeEntities && attrValue) { | |
| attrValue = attrValue.replace(/&(#?[0-9a-zA-Z]+;)/g, '&$1'); | |
| } | |
| return { | |
| attr: attr, | |
| name: attrName, | |
| value: attrValue | |
| }; | |
| } | |
| function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) { | |
| var attrName = normalized.name, | |
| attrValue = normalized.value, | |
| attr = normalized.attr, | |
| attrQuote = attr.quote, | |
| attrFragment, | |
| emittedAttrValue; | |
| if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes || | |
| ~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) { | |
| if (!options.preventAttributesEscaping) { | |
| if (typeof options.quoteCharacter === 'undefined') { | |
| var apos = (attrValue.match(/'/g) || []).length; | |
| var quot = (attrValue.match(/"/g) || []).length; | |
| attrQuote = apos < quot ? '\'' : '"'; | |
| } | |
| else { | |
| attrQuote = options.quoteCharacter === '\'' ? '\'' : '"'; | |
| } | |
| if (attrQuote === '"') { | |
| attrValue = attrValue.replace(/"/g, '"'); | |
| } | |
| else { | |
| attrValue = attrValue.replace(/'/g, '''); | |
| } | |
| } | |
| 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 { | |
| emittedAttrValue = attrValue + ' '; | |
| } | |
| if (typeof attrValue === 'undefined' || options.collapseBooleanAttributes && | |
| isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase())) { | |
| attrFragment = attrName; | |
| if (!isLast) { | |
| attrFragment += ' '; | |
| } | |
| } | |
| else { | |
| attrFragment = attrName + attr.customAssign + emittedAttrValue; | |
| } | |
| return attr.customOpen + attrFragment + attr.customClose; | |
| } | |
| function identity(value) { | |
| return value; | |
| } | |
| function processOptions(values) { | |
| var options = { | |
| name: function(name) { | |
| return name.toLowerCase(); | |
| }, | |
| canCollapseWhitespace: canCollapseWhitespace, | |
| canTrimWhitespace: canTrimWhitespace, | |
| html5: true, | |
| ignoreCustomComments: [/^!/], | |
| ignoreCustomFragments: [ | |
| /<%[\s\S]*?%>/, | |
| /<\?[\s\S]*?\?>/ | |
| ], | |
| includeAutoGeneratedTags: true, | |
| log: identity, | |
| minifyCSS: identity, | |
| minifyJS: identity, | |
| minifyURLs: identity | |
| }; | |
| Object.keys(values).forEach(function(key) { | |
| var value = values[key]; | |
| if (key === 'caseSensitive') { | |
| if (value) { | |
| options.name = identity; | |
| } | |
| } | |
| else if (key === 'log') { | |
| if (typeof value === 'function') { | |
| options.log = value; | |
| } | |
| } | |
| else if (key === 'minifyCSS' && typeof value !== 'function') { | |
| if (!value) { | |
| return; | |
| } | |
| if (typeof value !== 'object') { | |
| value = {}; | |
| } | |
| options.minifyCSS = function(text, type) { | |
| text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function(match, prefix, quote, url, suffix) { | |
| return prefix + quote + options.minifyURLs(url) + quote + suffix; | |
| }); | |
| var cleanCssOutput = new CleanCSS(value).minify(wrapCSS(text, type)); | |
| if (cleanCssOutput.errors.length > 0) { | |
| cleanCssOutput.errors.forEach(options.log); | |
| return text; | |
| } | |
| return unwrapCSS(cleanCssOutput.styles, type); | |
| }; | |
| } | |
| else if (key === 'minifyJS' && typeof value !== 'function') { | |
| if (!value) { | |
| return; | |
| } | |
| if (typeof value !== 'object') { | |
| value = {}; | |
| } | |
| (value.parse || (value.parse = {})).bare_returns = false; | |
| options.minifyJS = function(text, inline) { | |
| var start = text.match(/^\s*<!--.*/); | |
| var code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text; | |
| value.parse.bare_returns = inline; | |
| var result = UglifyJS.minify(code, value); | |
| if (result.error) { | |
| options.log(result.error); | |
| return text; | |
| } | |
| return result.code.replace(/;$/, ''); | |
| }; | |
| } | |
| else if (key === 'minifyURLs' && typeof value !== 'function') { | |
| if (!value) { | |
| return; | |
| } | |
| if (typeof value === 'string') { | |
| value = { site: value }; | |
| } | |
| else if (typeof value !== 'object') { | |
| value = {}; | |
| } | |
| options.minifyURLs = function(text) { | |
| try { | |
| return RelateUrl.relate(text, value); | |
| } | |
| catch (err) { | |
| options.log(err); | |
| return text; | |
| } | |
| }; | |
| } | |
| else { | |
| options[key] = value; | |
| } | |
| }); | |
| return options; | |
| } | |
| function uniqueId(value) { | |
| var id; | |
| do { | |
| id = Math.random().toString(36).replace(/^0\.[0-9]*/, ''); | |
| } while (~value.indexOf(id)); | |
| return id; | |
| } | |
| var specialContentTags = createMapFromString('script,style'); | |
| function createSortFns(value, options, uidIgnore, uidAttr) { | |
| var attrChains = options.sortAttributes && Object.create(null); | |
| var classChain = options.sortClassName && new TokenChain(); | |
| function attrNames(attrs) { | |
| return attrs.map(function(attr) { | |
| return options.name(attr.name); | |
| }); | |
| } | |
| function shouldSkipUID(token, uid) { | |
| return !uid || token.indexOf(uid) === -1; | |
| } | |
| function shouldSkipUIDs(token) { | |
| return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr); | |
| } | |
| function scan(input) { | |
| var currentTag, currentType; | |
| new HTMLParser(input, { | |
| start: function(tag, attrs) { | |
| if (attrChains) { | |
| if (!attrChains[tag]) { | |
| attrChains[tag] = new TokenChain(); | |
| } | |
| attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs)); | |
| } | |
| for (var i = 0, len = attrs.length; i < len; i++) { | |
| var attr = attrs[i]; | |
| if (classChain && attr.value && options.name(attr.name) === 'class') { | |
| classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs)); | |
| } | |
| else if (options.processScripts && attr.name.toLowerCase() === 'type') { | |
| currentTag = tag; | |
| currentType = attr.value; | |
| } | |
| } | |
| }, | |
| end: function() { | |
| currentTag = ''; | |
| }, | |
| chars: function(text) { | |
| if (options.processScripts && specialContentTags(currentTag) && | |
| options.processScripts.indexOf(currentType) > -1) { | |
| scan(text); | |
| } | |
| } | |
| }); | |
| } | |
| var log = options.log; | |
| options.log = identity; | |
| options.sortAttributes = false; | |
| options.sortClassName = false; | |
| scan(minify(value, options)); | |
| options.log = log; | |
| if (attrChains) { | |
| var attrSorters = Object.create(null); | |
| for (var tag in attrChains) { | |
| attrSorters[tag] = attrChains[tag].createSorter(); | |
| } | |
| options.sortAttributes = function(tag, attrs) { | |
| var sorter = attrSorters[tag]; | |
| if (sorter) { | |
| var attrMap = Object.create(null); | |
| var names = attrNames(attrs); | |
| names.forEach(function(name, index) { | |
| (attrMap[name] || (attrMap[name] = [])).push(attrs[index]); | |
| }); | |
| sorter.sort(names).forEach(function(name, index) { | |
| attrs[index] = attrMap[name].shift(); | |
| }); | |
| } | |
| }; | |
| } | |
| if (classChain) { | |
| var sorter = classChain.createSorter(); | |
| options.sortClassName = function(value) { | |
| return sorter.sort(value.split(/[ \n\f\r]+/)).join(' '); | |
| }; | |
| } | |
| } | |
| function minify(value, options, partialMarkup) { | |
| if (options.collapseWhitespace) { | |
| value = collapseWhitespace(value, options, true, true); | |
| } | |
| var buffer = [], | |
| charsPrevTag, | |
| currentChars = '', | |
| hasChars, | |
| currentTag = '', | |
| currentAttrs = [], | |
| stackNoTrimWhitespace = [], | |
| stackNoCollapseWhitespace = [], | |
| optionalStartTag = '', | |
| optionalEndTag = '', | |
| ignoredMarkupChunks = [], | |
| ignoredCustomMarkupChunks = [], | |
| uidIgnore, | |
| uidAttr, | |
| uidPattern; | |
| // temporarily replace ignored chunks with comments, | |
| // so that we don't have to worry what's there. | |
| // for all we care there might be | |
| // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content | |
| value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function(match, group1) { | |
| if (!uidIgnore) { | |
| uidIgnore = uniqueId(value); | |
| var pattern = new RegExp('^' + uidIgnore + '([0-9]+)$'); | |
| if (options.ignoreCustomComments) { | |
| options.ignoreCustomComments = options.ignoreCustomComments.slice(); | |
| } | |
| else { | |
| options.ignoreCustomComments = []; | |
| } | |
| options.ignoreCustomComments.push(pattern); | |
| } | |
| var token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->'; | |
| ignoredMarkupChunks.push(group1); | |
| return token; | |
| }); | |
| var customFragments = options.ignoreCustomFragments.map(function(re) { | |
| return re.source; | |
| }); | |
| if (customFragments.length) { | |
| var reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g'); | |
| // temporarily replace custom ignored fragments with unique attributes | |
| value = value.replace(reCustomIgnore, function(match) { | |
| if (!uidAttr) { | |
| uidAttr = uniqueId(value); | |
| uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g'); | |
| if (options.minifyCSS) { | |
| options.minifyCSS = (function(fn) { | |
| return function(text, type) { | |
| text = text.replace(uidPattern, function(match, prefix, index) { | |
| var chunks = ignoredCustomMarkupChunks[+index]; | |
| return chunks[1] + uidAttr + index + uidAttr + chunks[2]; | |
| }); | |
| var ids = []; | |
| new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function(warning) { | |
| var match = uidPattern.exec(warning); | |
| if (match) { | |
| var id = uidAttr + match[2] + uidAttr; | |
| text = text.replace(id, ignoreCSS(id)); | |
| ids.push(id); | |
| } | |
| }); | |
| text = fn(text, type); | |
| ids.forEach(function(id) { | |
| text = text.replace(ignoreCSS(id), id); | |
| }); | |
| return text; | |
| }; | |
| })(options.minifyCSS); | |
| } | |
| if (options.minifyJS) { | |
| options.minifyJS = (function(fn) { | |
| return function(text, type) { | |
| return fn(text.replace(uidPattern, function(match, prefix, index) { | |
| var chunks = ignoredCustomMarkupChunks[+index]; | |
| return chunks[1] + uidAttr + index + uidAttr + chunks[2]; | |
| }), type); | |
| }; | |
| })(options.minifyJS); | |
| } | |
| } | |
| var token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr; | |
| ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match)); | |
| return '\t' + token + '\t'; | |
| }); | |
| } | |
| if (options.sortAttributes && typeof options.sortAttributes !== 'function' || | |
| options.sortClassName && typeof options.sortClassName !== 'function') { | |
| createSortFns(value, options, uidIgnore, uidAttr); | |
| } | |
| function _canCollapseWhitespace(tag, attrs) { | |
| return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace); | |
| } | |
| function _canTrimWhitespace(tag, attrs) { | |
| return options.canTrimWhitespace(tag, attrs, canTrimWhitespace); | |
| } | |
| function removeStartTag() { | |
| var index = buffer.length - 1; | |
| while (index > 0 && !/^<[^/!]/.test(buffer[index])) { | |
| index--; | |
| } | |
| buffer.length = Math.max(0, index); | |
| } | |
| function removeEndTag() { | |
| var index = buffer.length - 1; | |
| while (index > 0 && !/^<\//.test(buffer[index])) { | |
| index--; | |
| } | |
| buffer.length = Math.max(0, index); | |
| } | |
| // look for trailing whitespaces, bypass any inline tags | |
| function trimTrailingWhitespace(index, nextTag) { | |
| for (var endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) { | |
| var str = buffer[index]; | |
| var match = str.match(/^<\/([\w:-]+)>$/); | |
| if (match) { | |
| endTag = match[1]; | |
| } | |
| else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options))) { | |
| break; | |
| } | |
| } | |
| } | |
| // look for trailing whitespaces from previously processed text | |
| // which may not be trimmed due to a following comment or an empty | |
| // element which has now been removed | |
| function squashTrailingWhitespace(nextTag) { | |
| var charsIndex = buffer.length - 1; | |
| if (buffer.length > 1) { | |
| var item = buffer[buffer.length - 1]; | |
| if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) { | |
| charsIndex--; | |
| } | |
| } | |
| trimTrailingWhitespace(charsIndex, nextTag); | |
| } | |
| new HTMLParser(value, { | |
| partialMarkup: partialMarkup, | |
| continueOnParseError: options.continueOnParseError, | |
| customAttrAssign: options.customAttrAssign, | |
| customAttrSurround: options.customAttrSurround, | |
| html5: options.html5, | |
| start: function(tag, attrs, unary, unarySlash, autoGenerated) { | |
| if (tag.toLowerCase() === 'svg') { | |
| options = Object.create(options); | |
| options.caseSensitive = true; | |
| options.keepClosingSlash = true; | |
| options.name = identity; | |
| } | |
| tag = options.name(tag); | |
| currentTag = tag; | |
| charsPrevTag = tag; | |
| if (!inlineTextTags(tag)) { | |
| currentChars = ''; | |
| } | |
| hasChars = false; | |
| currentAttrs = attrs; | |
| var optional = options.removeOptionalTags; | |
| if (optional) { | |
| var htmlTag = htmlTags(tag); | |
| // <html> may be omitted if first thing inside is not comment | |
| // <head> may be omitted if first thing inside is an element | |
| // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template> | |
| // <colgroup> may be omitted if first thing inside is <col> | |
| // <tbody> may be omitted if first thing inside is <tr> | |
| if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) { | |
| removeStartTag(); | |
| } | |
| optionalStartTag = ''; | |
| // end-tag-followed-by-start-tag omission rules | |
| if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) { | |
| removeEndTag(); | |
| // <colgroup> cannot be omitted if preceding </colgroup> is omitted | |
| // <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted | |
| optional = !isStartTagMandatory(optionalEndTag, tag); | |
| } | |
| optionalEndTag = ''; | |
| } | |
| // set whitespace flags for nested tags (eg. <code> within a <pre>) | |
| if (options.collapseWhitespace) { | |
| if (!stackNoTrimWhitespace.length) { | |
| squashTrailingWhitespace(tag); | |
| } | |
| if (!unary) { | |
| if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) { | |
| stackNoTrimWhitespace.push(tag); | |
| } | |
| if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) { | |
| stackNoCollapseWhitespace.push(tag); | |
| } | |
| } | |
| } | |
| var openTag = '<' + tag; | |
| var hasUnarySlash = unarySlash && options.keepClosingSlash; | |
| buffer.push(openTag); | |
| if (options.sortAttributes) { | |
| options.sortAttributes(tag, attrs); | |
| } | |
| var parts = []; | |
| for (var i = attrs.length, isLast = true; --i >= 0;) { | |
| var normalized = normalizeAttr(attrs[i], attrs, tag, options); | |
| if (normalized) { | |
| parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr)); | |
| isLast = false; | |
| } | |
| } | |
| if (parts.length > 0) { | |
| buffer.push(' '); | |
| buffer.push.apply(buffer, parts); | |
| } | |
| // start tag must never be omitted if it has any attributes | |
| else if (optional && optionalStartTags(tag)) { | |
| optionalStartTag = tag; | |
| } | |
| buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>'); | |
| if (autoGenerated && !options.includeAutoGeneratedTags) { | |
| removeStartTag(); | |
| optionalStartTag = ''; | |
| } | |
| }, | |
| end: function(tag, attrs, autoGenerated) { | |
| if (tag.toLowerCase() === 'svg') { | |
| options = Object.getPrototypeOf(options); | |
| } | |
| tag = options.name(tag); | |
| // check if current tag is in a whitespace stack | |
| if (options.collapseWhitespace) { | |
| if (stackNoTrimWhitespace.length) { | |
| if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) { | |
| stackNoTrimWhitespace.pop(); | |
| } | |
| } | |
| else { | |
| squashTrailingWhitespace('/' + tag); | |
| } | |
| if (stackNoCollapseWhitespace.length && | |
| tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) { | |
| stackNoCollapseWhitespace.pop(); | |
| } | |
| } | |
| var isElementEmpty = false; | |
| if (tag === currentTag) { | |
| currentTag = ''; | |
| isElementEmpty = !hasChars; | |
| } | |
| if (options.removeOptionalTags) { | |
| // <html>, <head> or <body> may be omitted if the element is empty | |
| if (isElementEmpty && topLevelTags(optionalStartTag)) { | |
| removeStartTag(); | |
| } | |
| optionalStartTag = ''; | |
| // </html> or </body> may be omitted if not followed by comment | |
| // </head> may be omitted if not followed by space or comment | |
| // </p> may be omitted if no more content in non-</a> parent | |
| // except for </dt> or </thead>, end tags may be omitted if no more content in parent element | |
| if (htmlTags(tag) && optionalEndTag && !trailingTags(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags(tag))) { | |
| removeEndTag(); | |
| } | |
| optionalEndTag = optionalEndTags(tag) ? tag : ''; | |
| } | |
| if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) { | |
| // remove last "element" from buffer | |
| removeStartTag(); | |
| optionalStartTag = ''; | |
| optionalEndTag = ''; | |
| } | |
| else { | |
| if (autoGenerated && !options.includeAutoGeneratedTags) { | |
| optionalEndTag = ''; | |
| } | |
| else { | |
| buffer.push('</' + tag + '>'); | |
| } | |
| charsPrevTag = '/' + tag; | |
| if (!inlineTags(tag)) { | |
| currentChars = ''; | |
| } | |
| else if (isElementEmpty) { | |
| currentChars += '|'; | |
| } | |
| } | |
| }, | |
| chars: function(text, prevTag, nextTag) { | |
| prevTag = prevTag === '' ? 'comment' : prevTag; | |
| nextTag = nextTag === '' ? 'comment' : nextTag; | |
| if (options.decodeEntities && text && !specialContentTags(currentTag)) { | |
| text = decode(text); | |
| } | |
| if (options.collapseWhitespace) { | |
| if (!stackNoTrimWhitespace.length) { | |
| if (prevTag === 'comment') { | |
| var prevComment = buffer[buffer.length - 1]; | |
| if (prevComment.indexOf(uidIgnore) === -1) { | |
| if (!prevComment) { | |
| prevTag = charsPrevTag; | |
| } | |
| if (buffer.length > 1 && (!prevComment || !options.conservativeCollapse && / $/.test(currentChars))) { | |
| var charsIndex = buffer.length - 2; | |
| buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function(trailingSpaces) { | |
| text = trailingSpaces + text; | |
| return ''; | |
| }); | |
| } | |
| } | |
| } | |
| if (prevTag) { | |
| if (prevTag === '/nobr' || prevTag === 'wbr') { | |
| if (/^\s/.test(text)) { | |
| var tagIndex = buffer.length - 1; | |
| while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) { | |
| tagIndex--; | |
| } | |
| trimTrailingWhitespace(tagIndex - 1, 'br'); | |
| } | |
| } | |
| else if (inlineTextTags(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) { | |
| text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars)); | |
| } | |
| } | |
| if (prevTag || nextTag) { | |
| text = collapseWhitespaceSmart(text, prevTag, nextTag, options); | |
| } | |
| else { | |
| text = collapseWhitespace(text, options, true, true); | |
| } | |
| if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') { | |
| trimTrailingWhitespace(buffer.length - 1, nextTag); | |
| } | |
| } | |
| if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) { | |
| text = collapseWhitespace(text, options, false, false, true); | |
| } | |
| } | |
| if (options.processScripts && specialContentTags(currentTag)) { | |
| text = processScript(text, options, currentAttrs); | |
| } | |
| if (isExecutableScript(currentTag, currentAttrs)) { | |
| text = options.minifyJS(text); | |
| } | |
| if (isStyleSheet(currentTag, currentAttrs)) { | |
| text = options.minifyCSS(text); | |
| } | |
| if (options.removeOptionalTags && text) { | |
| // <html> may be omitted if first thing inside is not comment | |
| // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template> | |
| if (optionalStartTag === 'html' || optionalStartTag === 'body' && !/^\s/.test(text)) { | |
| removeStartTag(); | |
| } | |
| optionalStartTag = ''; | |
| // </html> or </body> may be omitted if not followed by comment | |
| // </head>, </colgroup> or </caption> may be omitted if not followed by space or comment | |
| if (compactTags(optionalEndTag) || looseTags(optionalEndTag) && !/^\s/.test(text)) { | |
| removeEndTag(); | |
| } | |
| optionalEndTag = ''; | |
| } | |
| charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment'; | |
| if (options.decodeEntities && text && !specialContentTags(currentTag)) { | |
| // Escape any `&` symbols that start either: | |
| // 1) a legacy named character reference (i.e. one that doesn't end with `;`) | |
| // 2) or any other character reference (i.e. one that does end with `;`) | |
| // Note that `&` can be escaped as `&`, without the semi-colon. | |
| // https://mathiasbynens.be/notes/ambiguous-ampersands | |
| text = text.replace(/&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g, '&$1').replace(/</g, '<'); | |
| } | |
| if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) { | |
| text = text.replace(uidPattern, function(match, prefix, index) { | |
| return ignoredCustomMarkupChunks[+index][0]; | |
| }); | |
| } | |
| currentChars += text; | |
| if (text) { | |
| hasChars = true; | |
| } | |
| buffer.push(text); | |
| }, | |
| comment: function(text, nonStandard) { | |
| var prefix = nonStandard ? '<!' : '<!--'; | |
| var suffix = nonStandard ? '>' : '-->'; | |
| if (isConditionalComment(text)) { | |
| text = prefix + cleanConditionalComment(text, options) + suffix; | |
| } | |
| else if (options.removeComments) { | |
| if (isIgnoredComment(text, options)) { | |
| text = '<!--' + text + '-->'; | |
| } | |
| else { | |
| text = ''; | |
| } | |
| } | |
| else { | |
| text = prefix + text + suffix; | |
| } | |
| if (options.removeOptionalTags && text) { | |
| // preceding comments suppress tag omissions | |
| optionalStartTag = ''; | |
| optionalEndTag = ''; | |
| } | |
| buffer.push(text); | |
| }, | |
| doctype: function(doctype) { | |
| buffer.push(options.useShortDoctype ? '<!doctype' + | |
| (options.removeTagWhitespace ? '' : ' ') + 'html>' : | |
| collapseWhitespaceAll(doctype)); | |
| } | |
| }); | |
| if (options.removeOptionalTags) { | |
| // <html> may be omitted if first thing inside is not comment | |
| // <head> or <body> may be omitted if empty | |
| if (topLevelTags(optionalStartTag)) { | |
| removeStartTag(); | |
| } | |
| // except for </dt> or </thead>, end tags may be omitted if no more content in parent element | |
| if (optionalEndTag && !trailingTags(optionalEndTag)) { | |
| removeEndTag(); | |
| } | |
| } | |
| if (options.collapseWhitespace) { | |
| squashTrailingWhitespace('br'); | |
| } | |
| return joinResultSegments(buffer, options, uidPattern ? function(str) { | |
| return str.replace(uidPattern, function(match, prefix, index, suffix) { | |
| var chunk = ignoredCustomMarkupChunks[+index][0]; | |
| if (options.collapseWhitespace) { | |
| if (prefix !== '\t') { | |
| chunk = prefix + chunk; | |
| } | |
| if (suffix !== '\t') { | |
| chunk += suffix; | |
| } | |
| return collapseWhitespace(chunk, { | |
| preserveLineBreaks: options.preserveLineBreaks, | |
| conservativeCollapse: !options.trimCustomFragments | |
| }, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk)); | |
| } | |
| return chunk; | |
| }); | |
| } : identity, uidIgnore ? function(str) { | |
| return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function(match, index) { | |
| return ignoredMarkupChunks[+index]; | |
| }); | |
| } : identity); | |
| } | |
| function joinResultSegments(results, options, restoreCustom, restoreIgnore) { | |
| var str; | |
| var maxLineLength = options.maxLineLength; | |
| if (maxLineLength) { | |
| var line = '', lines = []; | |
| while (results.length) { | |
| var len = line.length; | |
| var end = results[0].indexOf('\n'); | |
| if (end < 0) { | |
| line += restoreIgnore(restoreCustom(results.shift())); | |
| } | |
| else { | |
| line += restoreIgnore(restoreCustom(results[0].slice(0, end))); | |
| results[0] = results[0].slice(end + 1); | |
| } | |
| if (len > 0 && line.length > maxLineLength) { | |
| lines.push(line.slice(0, len)); | |
| line = line.slice(len); | |
| } | |
| else if (end >= 0) { | |
| lines.push(line); | |
| line = ''; | |
| } | |
| } | |
| if (line) { | |
| lines.push(line); | |
| } | |
| str = lines.join('\n'); | |
| } | |
| else { | |
| str = restoreIgnore(restoreCustom(results.join(''))); | |
| } | |
| return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str; | |
| } | |
| exports.minify = function(value, options) { | |
| var start = Date.now(); | |
| options = processOptions(options || {}); | |
| var result = minify(value, options); | |
| options.log('minified in: ' + (Date.now() - start) + 'ms'); | |
| return result; | |
| }; |