html-minifier
Version:
HTML minifier with lint-like capabilities.
1,570 lines (1,351 loc) • 47.5 kB
JavaScript
/*!
* HTMLMinifier v0.7.1 (http://kangax.github.io/html-minifier/)
* Copyright 2010-2015 Juriy "kangax" Zaytsev
* Licensed under MIT (https://github.com/kangax/html-minifier/blob/gh-pages/LICENSE)
*/
/*!
* 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);
*
*/
/* global ActiveXObject, DOMDocument */
(function(global) {
'use strict';
// Regular Expressions for parsing tags and attributes
var singleAttrIdentifier = /([\w:\.-]+)/,
singleAttrAssign = /=/,
singleAttrAssigns = [ singleAttrAssign ],
singleAttrValues = [
/"((?:\\.|[^"])*)"/.source, // attr value double quotes
/'((?:\\.|[^'])*)'/.source, // attr value, single quotes
/([^>\s]+)/.source // attr value, no quotes
],
startTagOpen = /^<([\w:-]+)/,
startTagClose = /\s*(\/?)>/,
endTag = /^<\/([\w:-]+)[^>]*>/,
endingSlash = /\/>$/,
doctype = /^<!DOCTYPE [^>]+>/i;
var IS_REGEX_CAPTURING_BROKEN = false;
'x'.replace(/x(.)?/g, function(m, g) {
IS_REGEX_CAPTURING_BROKEN = g === '';
});
// Empty Elements - HTML 4.01
var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,wbr');
// Block Elements - HTML 4.01
// var block = makeMap('address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul');
// Inline Elements - HTML 4.01
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,noscript,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,svg,textarea,tt,u,var');
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source');
// Attributes that have their values filled in disabled='disabled'
var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
// Special Elements (can contain anything)
var special = makeMap('script,style,noscript');
var reCache = {}, stackedTag, reStackedTag, tagMatch;
function startTagForHandler( handler ) {
var customStartTagAttrs;
var startTagAttrs = new RegExp(
'(?:\\s*[\\w:\\.-]+'
+ '(?:\\s*'
+ '(?:' + joinSingleAttrAssigns(handler) + ')'
+ '\\s*(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>\\s]+)'
+ ')?'
+ ')*'
);
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(
'((?:' + attrClauses.join('|') + ')*)'
);
}
else {
// No custom attribute wrappers specified, so just capture the standard attribute rules
customStartTagAttrs = new RegExp('(' + startTagAttrs.source + ')');
}
return new RegExp(startTagOpen.source + customStartTagAttrs.source + startTagClose.source);
}
function attrForHandler( handler ) {
var singleAttr = new RegExp(
singleAttrIdentifier.source
+ '(?:\\s*'
+ '(' + joinSingleAttrAssigns( handler ) + ')'
+ '\\s*'
+ '(?:'
+ singleAttrValues.join('|')
+ ')'
+ ')?'
);
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');
}
}
function joinSingleAttrAssigns( handler ) {
return singleAttrAssigns.concat(
handler.customAttrAssign || []
).map(function (assign) {
return '(?:' + assign.source + ')';
}).join('|');
}
var HTMLParser = global.HTMLParser = function( html, handler ) {
var index, chars, match, stack = [], last = html, prevTag, nextTag;
stack.last = function() {
var last = this[ this.length - 1 ];
return last && last.tag;
};
var startTag = startTagForHandler(handler);
var attr = attrForHandler(handler);
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !special[ stack.last() ] ) {
// Comment:
if ( /^<!--/.test( html ) ) {
index = html.indexOf('-->');
if ( index >= 0 ) {
if ( handler.comment ) {
handler.comment( html.substring( 4, index ) );
}
html = html.substring( index + 3 );
chars = false;
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if ( /^<!\[/.test( html ) ) {
index = html.indexOf(']>');
if (index >= 0) {
if ( handler.comment ) {
handler.comment( html.substring(2, index + 1 ), true /* non-standard */ );
}
html = html.substring( index + 2 );
chars = false;
}
}
// Ignored elements?
else if ( /^<\?/.test( html ) ) {
index = html.indexOf( '?>', 2 );
if ( index >= 0 ) {
if ( handler.chars ) {
handler.chars( html.substring( 0, index + 2 ) );
}
html = html.substring( index + 2 );
}
}
else if ( /^<%/.test( html ) ) {
index = html.indexOf( '%>', 2 );
if ( index >= 0 ) {
if ( handler.chars ) {
handler.chars(html.substring( 0, index + 2) );
}
html = html.substring( index + 2 );
}
}
// Doctype:
else if ( (match = doctype.exec( html )) ) {
if ( handler.doctype ) {
handler.doctype( match[0] );
}
html = html.substring( match[0].length );
chars = false;
}
// End tag:
else if ( /^<\//.test( html ) ) {
match = html.match( endTag );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( endTag, parseEndTag );
prevTag = '/' + match[1].toLowerCase();
chars = false;
}
}
// Start tag:
else if ( /^</.test( html ) ) {
match = html.match( startTag );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( startTag, parseStartTag );
prevTag = match[1].toLowerCase();
chars = false;
}
}
if ( chars ) {
index = html.indexOf('<');
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? '' : html.substring( index );
// next tag
tagMatch = html.match( startTag );
if (tagMatch) {
nextTag = tagMatch[1];
}
else {
tagMatch = html.match( endTag );
if (tagMatch) {
nextTag = '/' + tagMatch[1];
}
else {
nextTag = '';
}
}
if ( handler.chars ) {
handler.chars(text, prevTag, nextTag);
}
}
}
else {
stackedTag = stack.last().toLowerCase();
reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)<\/' + stackedTag + '[^>]*>', 'i'));
html = html.replace(reStackedTag, function(all, text) {
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
text = text
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
}
if ( handler.chars ) {
handler.chars( text );
}
return '';
});
parseEndTag( '', stackedTag );
}
if ( html === last ) {
throw 'Parse Error: ' + html;
}
last = html;
}
// Clean up any remaining tags
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;
var attrs = [];
rest.replace(attr, function () {
var name, value, fallbackValue, customOpen, customClose, customAssign, quote;
var ncp = 7; // number of captured parts, scalar
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && arguments[0].indexOf('""') === -1) {
if (arguments[3] === '') { arguments[3] = undefined; }
if (arguments[4] === '') { arguments[4] = undefined; }
if (arguments[5] === '') { arguments[5] = undefined; }
}
name = arguments[1];
if ( name ) {
customAssign = arguments[2];
fallbackValue = arguments[3];
value = fallbackValue || arguments[4] || arguments[5];
if (customAssign) {
quote = arguments[0].charAt(name.length + customAssign.length);
quote = (quote === '\'' || quote === '"') ? quote : '';
}
}
else if ( handler.customAttrSurround ) {
for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
name = arguments[i * ncp + 7];
customAssign = arguments[i * ncp + 8];
if ( name ) {
fallbackValue = arguments[i * ncp + 9];
value = fallbackValue
|| arguments[i * ncp + 10]
|| arguments[i * ncp + 11];
customOpen = arguments[i * ncp + 6];
customClose = arguments[i * ncp + 12];
break;
}
}
}
if ( value === undefined ) {
value = fillAttrs[name] ? name : fallbackValue;
}
attrs.push({
name: name,
value: value,
escaped: value && value.replace(/(^|.)("+)/g, function(match) {
return match.replace(/"/g, '"');
}),
customAssign: customAssign || '=',
customOpen: customOpen || '',
customClose: customClose || '',
quote: quote || ''
});
});
if ( !unary ) {
stack.push( { tag: tagName, attrs: attrs } );
}
else {
unarySlash = tag.match( endingSlash );
}
if ( handler.start ) {
handler.start( tagName, attrs, unary, unarySlash );
}
}
function parseEndTag( tag, tagName ) {
var pos;
// If no tag name is provided, clean shop
if ( !tagName ) {
pos = 0;
}
else {
// Find the closest opened tag of the same type
var needle = tagName.toLowerCase();
for ( pos = stack.length - 1; pos >= 0; pos-- ) {
if ( stack[ pos ].tag.toLowerCase() === needle ) {
break;
}
}
}
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( var i = stack.length - 1; i >= pos; i-- ) {
if ( handler.end ) {
handler.end( stack[ i ].tag, stack[ i ].attrs );
}
}
// Remove the open elements from the stack
stack.length = pos;
}
}
};
global.HTMLtoXML = function( html ) {
var results = '';
new HTMLParser(html, {
start: function( tag, attrs, unary ) {
results += '<' + tag;
for ( var i = 0; i < attrs.length; i++ ) {
results += ' ' + attrs[i].name + '="' + attrs[i].escaped + '"';
}
results += (unary ? '/' : '') + '>';
},
end: function( tag ) {
results += '</' + tag + '>';
},
chars: function( text ) {
results += text;
},
comment: function( text ) {
results += '<!--' + text + '-->';
},
ignore: function(text) {
results += text;
}
});
return results;
};
global.HTMLtoDOM = function( html, doc ) {
// There can be only one of these elements
var one = makeMap('html,head,body,title');
// Enforce a structure for the document
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 we're dealing with an empty document then we
// need to pre-populate it with the HTML document structure
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 );
})();
}
// Find all the unique elements
if ( doc.getElementsByTagName ) {
for ( var i in one ) {
one[ i ] = doc.getElementsByTagName( i )[0];
}
}
// If we're working with a document, inject contents into
// the body element
var curParentNode = one.body;
new HTMLParser( html, {
start: function( tagName, attrs, unary ) {
// If it's a pre-built element, then we can ignore
// its construction
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( /* tag */ ) {
elems.length -= 1;
// Init the new parentNode
curParentNode = elems[ elems.length - 1 ];
},
chars: function( text ) {
curParentNode.appendChild( doc.createTextNode( text ) );
},
comment: function( /*text*/ ) {
// create comment node
},
ignore: function( /* text */ ) {
// What to do here?
}
});
return doc;
};
function makeMap(str) {
var obj = {}, items = str.split(',');
for ( var i = 0; i < items.length; i++ ) {
obj[ items[i] ] = true;
obj[ items[i].toUpperCase() ] = true;
}
return obj;
}
})(typeof exports === 'undefined' ? this : exports);
/* global CleanCSS */
(function(global) {
'use strict';
var log, HTMLParser;
if (global.console && global.console.log) {
log = function(message) {
// "preserving" `this`
global.console.log(message);
};
}
else {
log = function() {};
}
if (global.HTMLParser) {
HTMLParser = global.HTMLParser;
}
else if (typeof require === 'function') {
HTMLParser = require('./htmlparser').HTMLParser;
}
var trimWhitespace = function(str) {
if (typeof str !== 'string') {
return str;
}
return str.replace(/^\s+/, '').replace(/\s+$/, '');
};
if (String.prototype.trim) {
trimWhitespace = function(str) {
if (typeof str !== 'string') {
return str;
}
return str.trim();
};
}
function collapseWhitespace(str) {
return str ? str.replace(/[\t\n\r ]+/g, ' ') : str;
}
function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
// array of non-empty element tags that will maintain a single space outside of them
var tags = [
'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', 'svg', 'time', 'tt', 'u', 'var'
],
lineBreakBefore = /^[\t ]*[\n\r]+[\t\n\r ]*/,
lineBreakAfter = /[\t\n\r ]*[\n\r]+[\t ]*$/,
preserveBefore = lineBreakBefore.test(str) ? '\n' : ' ',
preserveAfter = lineBreakAfter.test(str) ? '\n' : ' ',
lineBreakStamp = 'htmlmincollapsedlinebreak';
if (prevTag && prevTag !== 'img' && prevTag !== 'input' && (prevTag.substr(0, 1) !== '/'
|| (prevTag.substr(0, 1) === '/' && tags.indexOf(prevTag.substr(1)) === -1))) {
str = str.replace(/^\s+/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveBefore : '');
}
if (nextTag && nextTag !== 'img' && nextTag !== 'input' && (nextTag.substr(0, 1) === '/'
|| (nextTag.substr(0, 1) !== '/' && tags.indexOf(nextTag) === -1))) {
str = str.replace(/\s+$/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveAfter : '');
}
if (prevTag && nextTag) {
if (options.preserveLineBreaks) {
str = str
.replace(lineBreakBefore, lineBreakStamp)
.replace(lineBreakAfter, lineBreakStamp);
}
// strip non space whitespace then compress spaces to one
return str
.replace(/[\t\n\r]+/g, ' ').replace(/[ ]+/g, ' ')
.replace(new RegExp(lineBreakStamp, 'g'), '\n');
}
return str;
}
function isConditionalComment(text) {
return ((/\[if[^\]]+\]/).test(text) || (/\s*((?:<!)?\[endif\])$/).test(text));
}
function isIgnoredComment(text, options) {
if ((/^!/).test(text)) {
return true;
}
if (options.ignoreCustomComments) {
for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
if (options.ignoreCustomComments[i].test(text)) {
return true;
}
}
}
return false;
}
function isEventAttribute(attrName) {
return (/^on[a-z]+/).test(attrName);
}
function canRemoveAttributeQuotes(value) {
// http://mathiasbynens.be/notes/unquoted-attribute-values
return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value) && !(/\/$/).test(value) &&
// make sure trailing slash is not interpreted as HTML self-closing tag
!(/\/$/).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')
);
}
function isScriptTypeAttribute(tag, attrName, attrValue) {
return (
tag === 'script' &&
attrName === 'type' &&
trimWhitespace(attrValue.toLowerCase()) === 'text/javascript'
);
}
// https://mathiasbynens.be/demo/javascript-mime-type
// https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
var executableScriptsMimetypes = {
'text/javascript': 1,
'text/ecmascript': 1,
'text/jscript': 1,
'application/javascript': 1,
'application/x-javascript': 1,
'application/ecmascript': 1
};
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 attrs[i].value === '' ||
executableScriptsMimetypes[attrs[i].value] === 1;
}
}
return true;
}
function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
return (
(tag === 'style' || tag === 'link') &&
attrName === 'type' &&
trimWhitespace(attrValue.toLowerCase()) === 'text/css'
);
}
var enumeratedAttributeValues = {
draggable: ['true', 'false'] // defaults to 'auto'
};
function isBooleanAttribute(attrName, attrValue) {
var isSimpleBoolean = (/^(?: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|spellcheck|truespeed|typemustmatch|visible)$/i).test(attrName);
if (isSimpleBoolean) {
return true;
}
var attrValueEnumeration = enumeratedAttributeValues[attrName.toLowerCase()];
if (!attrValueEnumeration) {
return false;
}
else {
return (-1 === attrValueEnumeration.indexOf(attrValue.toLowerCase()));
}
}
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, options, attrs) {
if (attrValue && isEventAttribute(attrName)) {
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, '');
if (options.minifyJS) {
var wrappedCode = '(function(){' + attrValue + '})()';
var minified = minifyJS(wrappedCode, options.minifyJS);
return minified.slice(12, minified.length - 4).replace(/"/g, '"');
}
return attrValue;
}
else if (attrName === 'class') {
return collapseWhitespace(trimWhitespace(attrValue));
}
else if (isUriTypeAttribute(attrName, tag)) {
attrValue = trimWhitespace(attrValue);
if (options.minifyURLs) {
return minifyURLs(attrValue, options.minifyURLs);
}
return attrValue;
}
else if (isNumberTypeAttribute(attrName, tag)) {
return trimWhitespace(attrValue);
}
else if (attrName === 'style') {
attrValue = trimWhitespace(attrValue);
if (attrValue) {
attrValue = attrValue.replace(/\s*;\s*$/, '');
}
if (options.minifyCSS) {
return minifyCSS(attrValue, options.minifyCSS, true);
}
return attrValue;
}
else if (isMetaViewport(tag, attrs) && attrName === 'content') {
attrValue = attrValue.replace(/1\.0/g, '1').replace(/\s+/g, '');
}
else if (attrValue && options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
attrValue = attrValue.replace(/\n+/g, '');
}
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;
}
}
}
// Wrap CSS declarations for CleanCSS > 3.x
// See https://github.com/jakubpawlowicz/clean-css/issues/418
function wrapCSS(text) {
return '*{' + text + '}';
}
function unwrapCSS(text) {
var matches = text.match(/^\*\{([\s\S]*)\}$/m);
if (matches && matches[1]) {
return matches[1];
}
else {
return text;
}
}
function cleanConditionalComment(comment) {
return comment
.replace(/^(\[[^\]]+\]>)\s*/, '$1')
.replace(/\s*(<!\[endif\])$/, '$1');
}
function removeCDATASections(text) {
return text
// "/* <![CDATA[ */" or "// <![CDATA["
.replace(/^(?:\s*\/\*\s*<!\[CDATA\[\s*\*\/|\s*\/\/\s*<!\[CDATA\[.*)/, '')
// "/* ]]> */" or "// ]]>"
.replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, '');
}
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;
}
var reStartDelimiter = {
// account for js + html comments (e.g.: //<!--)
script: /^\s*(?:\/\/)?\s*<!--.*\n?/,
style: /^\s*<!--\s*/
};
var reEndDelimiter = {
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|dt|dd|option|colgroup|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 = !attrValue || (/^\s*$/).test(attrValue);
if (isValueEmpty) {
return (
(tag === 'input' && attrName === 'value') ||
reEmptyAttribute.test(attrName));
}
return false;
}
function canRemoveElement(tag, attrs) {
if (tag === 'textarea') {
return false;
}
if (tag === 'script') {
for (var i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name === 'src') {
return false;
}
}
}
return true;
}
function canCollapseWhitespace(tag) {
return !(/^(?:script|style|pre|textarea)$/.test(tag));
}
function canTrimWhitespace(tag) {
return !(/^(?:pre|textarea)$/.test(tag));
}
function attrsToMarkup(attrs) {
var markup = '';
for (var i = 0, len = attrs.length; i < len; i++) {
markup += (' ' + attrs[i].name + (isBooleanAttribute(attrs[i].value) ? '' : ('="' + attrs[i].value + '"')));
}
return markup;
}
function normalizeAttribute(attr, attrs, tag, unarySlash, index, options) {
var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
attrValue = options.preventAttributesEscaping ? attr.value : attr.escaped,
attrQuote = options.preventAttributesEscaping ? attr.quote : '"',
attrFragment,
emittedAttrValue,
isTerminalOfUnarySlash = unarySlash && index === attrs.length - 1;
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, options, attrs);
if (attrValue !== undefined && !options.removeAttributeQuotes ||
!canRemoveAttributeQuotes(attrValue) || isTerminalOfUnarySlash) {
emittedAttrValue = attrQuote + attrValue + attrQuote;
}
else {
emittedAttrValue = attrValue;
}
if (options.removeEmptyAttributes &&
canDeleteEmptyAttribute(tag, attrName, attrValue)) {
return '';
}
if (attrValue === undefined || (options.collapseBooleanAttributes &&
isBooleanAttribute(attrName, attrValue))) {
attrFragment = attrName;
}
else {
attrFragment = attrName + attr.customAssign + emittedAttrValue;
}
return (' ' + attr.customOpen + attrFragment + attr.customClose);
}
function setDefaultTesters(options) {
var defaultTesters = ['canCollapseWhitespace', 'canTrimWhitespace'];
for (var i = 0, len = defaultTesters.length; i < len; i++) {
if (!options[defaultTesters[i]]) {
options[defaultTesters[i]] = function() {
return false;
};
}
}
}
function minifyURLs(text, options) {
if (typeof options !== 'object') {
options = { };
}
try {
// try to get global reference first
var __RelateUrl = global.RelateUrl;
if (typeof __RelateUrl === 'undefined' && typeof require === 'function') {
__RelateUrl = require('relateurl');
}
// noop
if (!__RelateUrl) {
return text;
}
if (__RelateUrl.relate) {
return __RelateUrl.relate(text, options);
}
else {
return text;
}
}
catch (err) {
log(err);
}
return text;
}
function minifyJS(text, options) {
if (typeof options !== 'object') {
options = { };
}
options.fromString = true;
var outputOptions = options.output || {};
outputOptions.inline_script = true;
options.output = outputOptions;
try {
// try to get global reference first
var __UglifyJS = global.UglifyJS;
if (typeof __UglifyJS === 'undefined' && typeof require === 'function') {
__UglifyJS = require('uglify-js');
}
// noop
if (!__UglifyJS) {
return text;
}
if (__UglifyJS.minify) {
return __UglifyJS.minify(text, options).code;
}
else if (__UglifyJS.parse) {
var ast = __UglifyJS.parse(text);
ast.figure_out_scope();
var compressor = __UglifyJS.Compressor();
var compressedAst = ast.transform(compressor);
compressedAst.figure_out_scope();
compressedAst.compute_char_frequency();
if (options.mangle !== false) {
compressedAst.mangle_names();
}
var stream = __UglifyJS.OutputStream(options.output);
compressedAst.print(stream);
return stream.toString();
}
else {
return text;
}
}
catch (err) {
log(err);
}
return text;
}
function minifyCSS(text, options, inline) {
if (typeof options !== 'object') {
options = { };
}
if (typeof options.advanced === 'undefined') {
options.advanced = false;
}
try {
var cleanCSS;
if (typeof CleanCSS !== 'undefined') {
cleanCSS = new CleanCSS(options);
}
else if (typeof require === 'function') {
var CleanCSSModule = require('clean-css');
cleanCSS = new CleanCSSModule(options);
}
if (inline) {
return unwrapCSS(cleanCSS.minify(wrapCSS(text)).styles);
}
else {
return cleanCSS.minify(text).styles;
}
}
catch (err) {
log(err);
}
return text;
}
function minify(value, options) {
options = options || {};
var optionsStack = [];
value = trimWhitespace(value);
setDefaultTesters(options);
var results = [ ],
buffer = [ ],
currentChars = '',
currentTag = '',
currentAttrs = [],
stackNoTrimWhitespace = [],
stackNoCollapseWhitespace = [],
lint = options.lint,
isIgnoring = false,
t = new Date();
if (options.removeIgnored) {
value = value
.replace(/<\?[^\?]+\?>/g, '')
.replace(/<%[^%]+%>/g, '');
}
function _canCollapseWhitespace(tag, attrs) {
return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
}
function _canTrimWhitespace(tag, attrs) {
return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
}
new HTMLParser(value, {
html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
start: function(tag, attrs, unary, unarySlash) {
if (isIgnoring) {
buffer.push('<' + tag, attrsToMarkup(attrs), unarySlash ? '/' : '', '>');
return;
}
var lowerTag = tag.toLowerCase();
if (lowerTag === 'svg') {
optionsStack.push(options);
var nextOptions = {};
for (var key in options) {
nextOptions[key] = options[key];
}
nextOptions.keepClosingSlash = true;
nextOptions.caseSensitive = true;
options = nextOptions;
}
tag = options.caseSensitive ? tag : lowerTag;
currentTag = tag;
currentChars = '';
currentAttrs = attrs;
// set whitespace flags for nested tags (eg. <code> within a <pre>)
if (options.collapseWhitespace) {
if (!_canTrimWhitespace(tag, attrs)) {
stackNoTrimWhitespace.push(tag);
}
if (!_canCollapseWhitespace(tag, attrs)) {
stackNoCollapseWhitespace.push(tag);
}
}
var openTag = '<' + tag;
var closeTag = ((unarySlash && options.keepClosingSlash) ? '/' : '') + '>';
if (attrs.length === 0) {
openTag += closeTag;
}
buffer.push(openTag);
if (lint) {
lint.testElement(tag);
}
var token;
for (var i = 0, len = attrs.length; i < len; i++) {
if (lint) {
lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
}
token = normalizeAttribute(attrs[i], attrs, tag, unarySlash, i, options);
if (i === len - 1) {
token += closeTag;
}
buffer.push(token);
}
},
end: function(tag, attrs) {
if (isIgnoring) {
buffer.push('</' + tag + '>');
return;
}
var lowerTag = tag.toLowerCase();
if (lowerTag === 'svg') {
options = optionsStack.pop();
}
// check if current tag is in a whitespace stack
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, attrs))) {
// remove last "element" from buffer, return
for (var i = buffer.length - 1; i >= 0; i--) {
if (/^<[^\/!]/.test(buffer[i])) {
buffer.splice(i);
break;
}
}
return;
}
else if (options.removeOptionalTags && isOptionalTag(tag)) {
// noop, leave start tag in buffer
return;
}
else {
// push end tag to buffer
buffer.push('</' + (options.caseSensitive ? tag : lowerTag) + '>');
results.push.apply(results, buffer);
}
// flush buffer
buffer.length = 0;
currentChars = '';
},
chars: function(text, prevTag, nextTag) {
prevTag = prevTag === '' ? 'comment' : prevTag;
nextTag = nextTag === '' ? 'comment' : nextTag;
if (isIgnoring) {
buffer.push(text);
return;
}
if (currentTag === 'script' || currentTag === 'style') {
if (options.removeCommentsFromCDATA) {
text = removeComments(text, currentTag);
}
if (options.removeCDATASectionsFromCDATA) {
text = removeCDATASections(text);
}
if (options.processScripts) {
text = processScript(text, options, currentAttrs);
}
}
if (options.minifyJS && isExecutableScript(currentTag, currentAttrs)) {
text = minifyJS(text, options.minifyJS);
}
if (currentTag === 'style' && options.minifyCSS) {
text = minifyCSS(text, options.minifyCSS);
}
if (options.collapseWhitespace) {
if (!stackNoTrimWhitespace.length) {
text = ((prevTag && prevTag !== 'comment') || (nextTag && nextTag !== 'comment')) ?
collapseWhitespaceSmart(text, prevTag, nextTag, options)
: trimWhitespace(text);
}
if (!stackNoCollapseWhitespace.length) {
text = !(prevTag && nextTag || nextTag === 'html') ? collapseWhitespace(text) : text;
}
}
currentChars = text;
if (lint) {
lint.testChars(text);
}
buffer.push(text);
},
comment: function(text, nonStandard) {
var prefix = nonStandard ? '<!' : '<!--';
var suffix = nonStandard ? '>' : '-->';
if (/^\s*htmlmin:ignore/.test(text)) {
isIgnoring = !isIgnoring;
if (!options.removeComments) {
buffer.push('<!--' + text + '-->');
}
return;
}
if (options.removeComments) {
if (isConditionalComment(text)) {
text = prefix + cleanConditionalComment(text) + suffix;
}
else if (isIgnoredComment(text, options)) {
text = '<!--' + text + '-->';
}
else {
text = '';
}
}
else {
text = prefix + text + suffix;
}
buffer.push(text);
},
doctype: function(doctype) {
buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
},
customAttrAssign: options.customAttrAssign,
customAttrSurround: options.customAttrSurround
});
results.push.apply(results, buffer);
var str = joinResultSegments(results, options);
log('minified in: ' + (new Date() - t) + 'ms');
return str;
}
function joinResultSegments(results, options) {
var str;
var maxLineLength = options.maxLineLength;
if (maxLineLength) {
var token;
var lines = [];
var line = '';
for (var i = 0, len = results.length; i < len; i++) {
token = results[i];
if (line.length + token.length < maxLineLength) {
line += token;
}
else {
lines.push(line.replace(/^\n/, ''));
line = token;
}
}
lines.push(line);
str = lines.join('\n');
}
else {
str = results.join('');
}
return str;
}
// for CommonJS enviroments, export everything
if (typeof exports !== 'undefined') {
exports.minify = minify;
}
else {
global.minify = minify;
}
}(this));
/*!
* HTMLLint (to be used in conjunction with HTMLMinifier)
*
* Copyright (c) 2010-2013 Juriy "kangax" Zaytsev
* Licensed under the MIT license.
*
*/
(function(global) {
'use strict';
function isPresentationalElement(tag) {
return (/^(?:big|small|hr|blink|marquee)$/).test(tag);
}
function isDeprecatedElement(tag) {
return (/^(?:applet|basefont|center|dir|font|isindex|strike)$/).test(tag);
}
function isEventAttribute(attrName) {
return (/^on[a-z]+/).test(attrName);
}
function isStyleAttribute(attrName) {
return (attrName.toLowerCase() === 'style');
}
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 <span class="deprecated-element">deprecated</span> <strong><code><' +
tag + '></code></strong> element'
);
}
else if (isPresentationalElement(tag)) {
this.log.push(
'Found <span class="presentational-element">presentational</span> <strong><code><' +
tag + '></code></strong> 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 <code><br></code> sequence. Try replacing it with styling.');
};
Lint.prototype.testAttribute = function(tag, attrName, attrValue) {
if (isEventAttribute(attrName)) {
this.log.push(
'Found <span class="event-attribute">event attribute</span> (<strong>' +
attrName + '</strong>) on <strong><code><' + tag + '></code></strong> element.'
);
}
else if (isDeprecatedAttribute(tag, attrName)) {
this.log.push(
'Found <span class="deprecated-attribute">deprecated</span> <strong>' +
attrName + '</strong> attribute on <strong><code><' + tag + '></code></strong> element.'
);
}
else if (isStyleAttribute(attrName)) {
this.log.push(
'Found <span class="style-attribute">style attribute</span> on <strong><code><' +
tag + '></code></strong> element.'
);
}
else if (isInaccessibleAttribute(attrName, attrValue)) {
this.log.push(
'Found <span class="inaccessible-attribute">inaccessible attribute</span> ' +
'(on <strong><code><' + tag + '></code></strong> element).'
);
}
};
Lint.prototype.testChars = function(chars) {
this._lastElement = '';
if (/( \s*){2,}/.test(chars)) {
this.log.push('Found repeating <strong><code>&nbsp;</code></strong> 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();
}
if (this.log.length) {
if (writeToElement) {
writeToElement.innerHTML = '<ol><li>' + this.log.join('<li>') + '</ol>';
}
else {
var output = ' - ' +
this.log.join('\n - ')
.replace(/(<([^>]+)>)/ig, '')
.replace(/</g, '<')
.replace(/>/g, '>');
console.log(output);
}
}
};
global.HTMLLint = Lint;
})(typeof exports === 'undefined' ? this : exports);