htmlminifier.js 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484
  1. /*!
  2. * HTMLMinifier v0.6.9 (http://kangax.github.io/html-minifier/)
  3. * Copyright 2010-2014 Juriy "kangax" Zaytsev
  4. * Licensed under MIT (https://github.com/kangax/html-minifier/blob/gh-pages/LICENSE)
  5. */
  6. /*!
  7. * HTML Parser By John Resig (ejohn.org)
  8. * Modified by Juriy "kangax" Zaytsev
  9. * Original code by Erik Arvidsson, Mozilla Public License
  10. * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
  11. */
  12. /*
  13. * // Use like so:
  14. * HTMLParser(htmlString, {
  15. * start: function(tag, attrs, unary) {},
  16. * end: function(tag) {},
  17. * chars: function(text) {},
  18. * comment: function(text) {}
  19. * });
  20. *
  21. * // or to get an XML string:
  22. * HTMLtoXML(htmlString);
  23. *
  24. * // or to get an XML DOM Document
  25. * HTMLtoDOM(htmlString);
  26. *
  27. * // or to inject into an existing document/DOM node
  28. * HTMLtoDOM(htmlString, document);
  29. * HTMLtoDOM(htmlString, document.body);
  30. *
  31. */
  32. /* global ActiveXObject, DOMDocument */
  33. (function(global) {
  34. 'use strict';
  35. // Regular Expressions for parsing tags and attributes
  36. var singleAttrIdentifier = /([\w:.-]+)/,
  37. singleAttrAssign = /=/,
  38. singleAttrAssigns = [ singleAttrAssign ],
  39. singleAttrValues = [
  40. /"((?:\\.|[^"])*)"/.source, // attr value double quotes
  41. /'((?:\\.|[^'])*)'/.source, // attr value, single quotes
  42. /([^>\s]+)/.source // attr value, no quotes
  43. ],
  44. startTagOpen = /^<([\w:-]+)/,
  45. startTagClose = /\s*(\/?)>/,
  46. endTag = /^<\/([\w:-]+)[^>]*>/,
  47. endingSlash = /\/>$/,
  48. doctype = /^<!DOCTYPE [^>]+>/i;
  49. var IS_REGEX_CAPTURING_BROKEN = false;
  50. 'x'.replace(/x(.)?/g, function(m, g) {
  51. IS_REGEX_CAPTURING_BROKEN = g === '';
  52. });
  53. // Empty Elements - HTML 4.01
  54. var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,wbr');
  55. // Block Elements - HTML 4.01
  56. // 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');
  57. // Inline Elements - HTML 4.01
  58. 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');
  59. // Elements that you can, intentionally, leave open
  60. // (and which close themselves)
  61. var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source');
  62. // Attributes that have their values filled in disabled='disabled'
  63. var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
  64. // Special Elements (can contain anything)
  65. var special = makeMap('script,style,noscript');
  66. var reCache = {}, stackedTag, reStackedTag, tagMatch;
  67. function startTagForHandler( handler ) {
  68. var customStartTagAttrs;
  69. var startTagAttrs = new RegExp(
  70. '(?:\\s*[\\w:.-]+'
  71. + '(?:\\s*'
  72. + '(?:' + joinSingleAttrAssigns(handler) + ')'
  73. + '\\s*(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>\\s]+)'
  74. + ')?'
  75. + ')*'
  76. );
  77. if ( handler.customAttrSurround ) {
  78. var attrClauses = [];
  79. for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
  80. // Capture the custom attribute opening and closing markup surrounding the standard attribute rules
  81. attrClauses[i] = '(?:\\s*'
  82. + handler.customAttrSurround[i][0].source
  83. + startTagAttrs.source
  84. + handler.customAttrSurround[i][1].source
  85. + ')';
  86. }
  87. attrClauses.unshift(startTagAttrs.source);
  88. customStartTagAttrs = new RegExp(
  89. '((?:' + attrClauses.join('|') + ')*)'
  90. );
  91. }
  92. else {
  93. // No custom attribute wrappers specified, so just capture the standard attribute rules
  94. customStartTagAttrs = new RegExp('(' + startTagAttrs.source + ')');
  95. }
  96. return new RegExp(startTagOpen.source + customStartTagAttrs.source + startTagClose.source);
  97. }
  98. function attrForHandler( handler ) {
  99. var singleAttr = new RegExp(
  100. singleAttrIdentifier.source
  101. + '(?:\\s*'
  102. + '(' + joinSingleAttrAssigns( handler ) + ')'
  103. + '\\s*'
  104. + '(?:'
  105. + singleAttrValues.join('|')
  106. + ')'
  107. + ')?'
  108. );
  109. if ( handler.customAttrSurround ) {
  110. var attrClauses = [];
  111. for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
  112. attrClauses[i] = '(?:'
  113. + '(' + handler.customAttrSurround[i][0].source + ')'
  114. + singleAttr.source
  115. + '(' + handler.customAttrSurround[i][1].source + ')'
  116. + ')';
  117. }
  118. attrClauses.unshift('(?:' + singleAttr.source + ')');
  119. return new RegExp(attrClauses.join('|'), 'g');
  120. }
  121. else {
  122. return new RegExp(singleAttr.source, 'g');
  123. }
  124. }
  125. function joinSingleAttrAssigns( handler ) {
  126. return singleAttrAssigns.concat(
  127. handler.customAttrAssign || []
  128. ).map(function (assign) {
  129. return '(?:' + assign.source + ')';
  130. }).join('|');
  131. }
  132. var HTMLParser = global.HTMLParser = function( html, handler ) {
  133. var index, chars, match, stack = [], last = html, prevTag, nextTag;
  134. stack.last = function() {
  135. var last = this[ this.length - 1 ];
  136. return last && last.tag;
  137. };
  138. var startTag = startTagForHandler(handler);
  139. var attr = attrForHandler(handler);
  140. while ( html ) {
  141. chars = true;
  142. // Make sure we're not in a script or style element
  143. if ( !stack.last() || !special[ stack.last() ] ) {
  144. // Comment:
  145. if ( /^<!--/.test( html ) ) {
  146. index = html.indexOf('-->');
  147. if ( index >= 0 ) {
  148. if ( handler.comment ) {
  149. handler.comment( html.substring( 4, index ) );
  150. }
  151. html = html.substring( index + 3 );
  152. chars = false;
  153. }
  154. }
  155. // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
  156. if ( /^<!\[/.test( html ) ) {
  157. index = html.indexOf(']>');
  158. if (index >= 0) {
  159. if ( handler.comment ) {
  160. handler.comment( html.substring(2, index + 1 ), true /* non-standard */ );
  161. }
  162. html = html.substring( index + 2 );
  163. chars = false;
  164. }
  165. }
  166. // Ignored elements?
  167. else if ( /^<\?/.test( html ) ) {
  168. index = html.indexOf( '?>', 2 );
  169. if ( index >= 0 ) {
  170. if ( handler.chars ) {
  171. handler.chars( html.substring( 0, index + 2 ) );
  172. }
  173. html = html.substring( index + 2 );
  174. }
  175. }
  176. else if ( /^<%/.test( html ) ) {
  177. index = html.indexOf( '%>', 2 );
  178. if ( index >= 0 ) {
  179. if ( handler.chars ) {
  180. handler.chars(html.substring( 0, index + 2) );
  181. }
  182. html = html.substring( index + 2 );
  183. }
  184. }
  185. // Doctype:
  186. else if ( (match = doctype.exec( html )) ) {
  187. if ( handler.doctype ) {
  188. handler.doctype( match[0] );
  189. }
  190. html = html.substring( match[0].length );
  191. chars = false;
  192. }
  193. // End tag:
  194. else if ( /^<\//.test( html ) ) {
  195. match = html.match( endTag );
  196. if ( match ) {
  197. html = html.substring( match[0].length );
  198. match[0].replace( endTag, parseEndTag );
  199. prevTag = '/' + match[1].toLowerCase();
  200. chars = false;
  201. }
  202. // Start tag:
  203. }
  204. else if ( /^</.test( html ) ) {
  205. match = html.match( startTag );
  206. if ( match ) {
  207. html = html.substring( match[0].length );
  208. match[0].replace( startTag, parseStartTag );
  209. prevTag = match[1].toLowerCase();
  210. chars = false;
  211. }
  212. }
  213. if ( chars ) {
  214. index = html.indexOf('<');
  215. var text = index < 0 ? html : html.substring( 0, index );
  216. html = index < 0 ? '' : html.substring( index );
  217. // next tag
  218. tagMatch = html.match( startTag );
  219. if (tagMatch) {
  220. nextTag = tagMatch[1];
  221. }
  222. else {
  223. tagMatch = html.match( endTag );
  224. if (tagMatch) {
  225. nextTag = '/' + tagMatch[1];
  226. }
  227. else {
  228. nextTag = '';
  229. }
  230. }
  231. if ( handler.chars ) {
  232. handler.chars(text, prevTag, nextTag);
  233. }
  234. }
  235. }
  236. else {
  237. stackedTag = stack.last().toLowerCase();
  238. reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)<\/' + stackedTag + '[^>]*>', 'i'));
  239. html = html.replace(reStackedTag, function(all, text) {
  240. if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
  241. text = text
  242. .replace(/<!--([\s\S]*?)-->/g, '$1')
  243. .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
  244. }
  245. if ( handler.chars ) {
  246. handler.chars( text );
  247. }
  248. return '';
  249. });
  250. parseEndTag( '', stackedTag );
  251. }
  252. if ( html === last ) {
  253. throw 'Parse Error: ' + html;
  254. }
  255. last = html;
  256. }
  257. // Clean up any remaining tags
  258. parseEndTag();
  259. function parseStartTag( tag, tagName, rest, unary ) {
  260. var unarySlash = false;
  261. while ( !handler.html5 && stack.last() && inline[ stack.last() ]) {
  262. parseEndTag( '', stack.last() );
  263. }
  264. if ( closeSelf[ tagName ] && stack.last() === tagName ) {
  265. parseEndTag( '', tagName );
  266. }
  267. unary = empty[ tagName ] || !!unary;
  268. var attrs = [];
  269. rest.replace(attr, function () {
  270. var name, value, fallbackValue, customOpen, customClose, customAssign;
  271. var ncp = 7; // number of captured parts, scalar
  272. // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
  273. if (IS_REGEX_CAPTURING_BROKEN && arguments[0].indexOf('""') === -1) {
  274. if (arguments[3] === '') { arguments[3] = undefined; }
  275. if (arguments[4] === '') { arguments[4] = undefined; }
  276. if (arguments[5] === '') { arguments[5] = undefined; }
  277. }
  278. name = arguments[1];
  279. if ( name ) {
  280. customAssign = arguments[2];
  281. fallbackValue = arguments[3];
  282. value = fallbackValue || arguments[4] || arguments[5];
  283. }
  284. else if ( handler.customAttrSurround ) {
  285. for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
  286. name = arguments[i * ncp + 7];
  287. customAssign = arguments[i * ncp + 8];
  288. if ( name ) {
  289. fallbackValue = arguments[i * ncp + 9];
  290. value = fallbackValue
  291. || arguments[i * ncp + 10]
  292. || arguments[i * ncp + 11];
  293. customOpen = arguments[i * ncp + 6];
  294. customClose = arguments[i * ncp + 12];
  295. break;
  296. }
  297. }
  298. }
  299. if ( value === undefined ) {
  300. value = fillAttrs[name] ? name : fallbackValue;
  301. }
  302. attrs.push({
  303. name: name,
  304. value: value,
  305. escaped: value && value.replace(/(^|.)("+)/g, function(match) {
  306. return match.replace(/"/g, '&quot;');
  307. }),
  308. customAssign: customAssign || '=',
  309. customOpen: customOpen || '',
  310. customClose: customClose || ''
  311. });
  312. });
  313. if ( !unary ) {
  314. stack.push( { tag: tagName, attrs: attrs } );
  315. }
  316. else {
  317. unarySlash = tag.match( endingSlash );
  318. }
  319. if ( handler.start ) {
  320. handler.start( tagName, attrs, unary, unarySlash );
  321. }
  322. }
  323. function parseEndTag( tag, tagName ) {
  324. var pos;
  325. // If no tag name is provided, clean shop
  326. if ( !tagName ) {
  327. pos = 0;
  328. }
  329. else {
  330. // Find the closest opened tag of the same type
  331. var needle = tagName.toLowerCase();
  332. for ( pos = stack.length - 1; pos >= 0; pos-- ) {
  333. if ( stack[ pos ].tag.toLowerCase() === needle ) {
  334. break;
  335. }
  336. }
  337. }
  338. if ( pos >= 0 ) {
  339. // Close all the open elements, up the stack
  340. for ( var i = stack.length - 1; i >= pos; i-- ) {
  341. if ( handler.end ) {
  342. handler.end( stack[ i ].tag, stack[ i ].attrs );
  343. }
  344. }
  345. // Remove the open elements from the stack
  346. stack.length = pos;
  347. }
  348. }
  349. };
  350. global.HTMLtoXML = function( html ) {
  351. var results = '';
  352. new HTMLParser(html, {
  353. start: function( tag, attrs, unary ) {
  354. results += '<' + tag;
  355. for ( var i = 0; i < attrs.length; i++ ) {
  356. results += ' ' + attrs[i].name + '="' + attrs[i].escaped + '"';
  357. }
  358. results += (unary ? '/' : '') + '>';
  359. },
  360. end: function( tag ) {
  361. results += '</' + tag + '>';
  362. },
  363. chars: function( text ) {
  364. results += text;
  365. },
  366. comment: function( text ) {
  367. results += '<!--' + text + '-->';
  368. },
  369. ignore: function(text) {
  370. results += text;
  371. }
  372. });
  373. return results;
  374. };
  375. global.HTMLtoDOM = function( html, doc ) {
  376. // There can be only one of these elements
  377. var one = makeMap('html,head,body,title');
  378. // Enforce a structure for the document
  379. var structure = {
  380. link: 'head',
  381. base: 'head'
  382. };
  383. if ( !doc ) {
  384. if ( typeof DOMDocument !== 'undefined' ) {
  385. doc = new DOMDocument();
  386. }
  387. else if ( typeof document !== 'undefined' && document.implementation && document.implementation.createDocument ) {
  388. doc = document.implementation.createDocument('', '', null);
  389. }
  390. else if ( typeof ActiveX !== 'undefined' ) {
  391. doc = new ActiveXObject('Msxml.DOMDocument');
  392. }
  393. }
  394. else {
  395. doc = doc.ownerDocument ||
  396. doc.getOwnerDocument && doc.getOwnerDocument() ||
  397. doc;
  398. }
  399. var elems = [],
  400. documentElement = doc.documentElement ||
  401. doc.getDocumentElement && doc.getDocumentElement();
  402. // If we're dealing with an empty document then we
  403. // need to pre-populate it with the HTML document structure
  404. if ( !documentElement && doc.createElement ) {
  405. (function() {
  406. var html = doc.createElement('html');
  407. var head = doc.createElement('head');
  408. head.appendChild( doc.createElement('title') );
  409. html.appendChild( head );
  410. html.appendChild( doc.createElement('body') );
  411. doc.appendChild( html );
  412. })();
  413. }
  414. // Find all the unique elements
  415. if ( doc.getElementsByTagName ) {
  416. for ( var i in one ) {
  417. one[ i ] = doc.getElementsByTagName( i )[0];
  418. }
  419. }
  420. // If we're working with a document, inject contents into
  421. // the body element
  422. var curParentNode = one.body;
  423. new HTMLParser( html, {
  424. start: function( tagName, attrs, unary ) {
  425. // If it's a pre-built element, then we can ignore
  426. // its construction
  427. if ( one[ tagName ] ) {
  428. curParentNode = one[ tagName ];
  429. return;
  430. }
  431. var elem = doc.createElement( tagName );
  432. for ( var attr in attrs ) {
  433. elem.setAttribute( attrs[ attr ].name, attrs[ attr ].value );
  434. }
  435. if ( structure[ tagName ] && typeof one[ structure[ tagName ] ] !== 'boolean' ) {
  436. one[ structure[ tagName ] ].appendChild( elem );
  437. }
  438. else if ( curParentNode && curParentNode.appendChild ) {
  439. curParentNode.appendChild( elem );
  440. }
  441. if ( !unary ) {
  442. elems.push( elem );
  443. curParentNode = elem;
  444. }
  445. },
  446. end: function( /* tag */ ) {
  447. elems.length -= 1;
  448. // Init the new parentNode
  449. curParentNode = elems[ elems.length - 1 ];
  450. },
  451. chars: function( text ) {
  452. curParentNode.appendChild( doc.createTextNode( text ) );
  453. },
  454. comment: function( /*text*/ ) {
  455. // create comment node
  456. },
  457. ignore: function( /* text */ ) {
  458. // What to do here?
  459. }
  460. });
  461. return doc;
  462. };
  463. function makeMap(str) {
  464. var obj = {}, items = str.split(',');
  465. for ( var i = 0; i < items.length; i++ ) {
  466. obj[ items[i] ] = true;
  467. obj[ items[i].toUpperCase() ] = true;
  468. }
  469. return obj;
  470. }
  471. })(typeof exports === 'undefined' ? this : exports);
  472. /* global CleanCSS */
  473. (function(global) {
  474. 'use strict';
  475. var log, HTMLParser;
  476. if (global.console && global.console.log) {
  477. log = function(message) {
  478. // "preserving" `this`
  479. global.console.log(message);
  480. };
  481. }
  482. else {
  483. log = function() {};
  484. }
  485. if (global.HTMLParser) {
  486. HTMLParser = global.HTMLParser;
  487. }
  488. else if (typeof require === 'function') {
  489. HTMLParser = require('./htmlparser').HTMLParser;
  490. }
  491. var trimWhitespace = function(str) {
  492. if (typeof str !== 'string') {
  493. return str;
  494. }
  495. return str.replace(/^\s+/, '').replace(/\s+$/, '');
  496. };
  497. if (String.prototype.trim) {
  498. trimWhitespace = function(str) {
  499. if (typeof str !== 'string') {
  500. return str;
  501. }
  502. return str.trim();
  503. };
  504. }
  505. function collapseWhitespace(str) {
  506. return str ? str.replace(/[\t\n\r ]+/g, ' ') : str;
  507. }
  508. function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
  509. // array of non-empty element tags that will maintain a single space outside of them
  510. var tags = [
  511. 'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite',
  512. 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'q',
  513. 'rt', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong',
  514. 'sub', 'sup', 'svg', 'time', 'tt', 'u', 'var'
  515. ],
  516. lineBreakBefore = /^[\t ]*[\n\r]+[\t\n\r ]*/,
  517. lineBreakAfter = /[\t\n\r ]*[\n\r]+[\t ]*$/,
  518. preserveBefore = lineBreakBefore.test(str) ? '\n' : ' ',
  519. preserveAfter = lineBreakAfter.test(str) ? '\n' : ' ',
  520. lineBreakStamp = '[{htmlmin-lb}]';
  521. if (prevTag && prevTag !== 'img' && prevTag !== 'input' && (prevTag.substr(0,1) !== '/'
  522. || ( prevTag.substr(0,1) === '/' && tags.indexOf(prevTag.substr(1)) === -1))) {
  523. str = str.replace(/^\s+/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveBefore : '');
  524. }
  525. if (nextTag && nextTag !== 'img' && nextTag !== 'input' && (nextTag.substr(0,1) === '/'
  526. || ( nextTag.substr(0,1) !== '/' && tags.indexOf(nextTag) === -1))) {
  527. str = str.replace(/\s+$/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveAfter : '');
  528. }
  529. if (prevTag && nextTag) {
  530. if (options.preserveLineBreaks) {
  531. str = str
  532. .replace(lineBreakBefore, lineBreakStamp)
  533. .replace(lineBreakAfter, lineBreakStamp);
  534. }
  535. // strip non space whitespace then compress spaces to one
  536. return str
  537. .replace(/[\t\n\r]+/g, ' ').replace(/[ ]+/g, ' ')
  538. .replace(lineBreakStamp, '\n');
  539. }
  540. return str;
  541. }
  542. function isConditionalComment(text) {
  543. return ((/\[if[^\]]+\]/).test(text) || (/\s*((?:<!)?\[endif\])$/).test(text));
  544. }
  545. function isIgnoredComment(text, options) {
  546. if ((/^!/).test(text)) {
  547. return true;
  548. }
  549. if (options.ignoreCustomComments) {
  550. for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
  551. if (options.ignoreCustomComments[i].test(text)) {
  552. return true;
  553. }
  554. }
  555. }
  556. return false;
  557. }
  558. function isEventAttribute(attrName) {
  559. return (/^on[a-z]+/).test(attrName);
  560. }
  561. function canRemoveAttributeQuotes(value) {
  562. // http://mathiasbynens.be/notes/unquoted-attribute-values
  563. return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value) && !(/\/$/ ).test(value) &&
  564. // make sure trailing slash is not interpreted as HTML self-closing tag
  565. !(/\/$/).test(value);
  566. }
  567. function attributesInclude(attributes, attribute) {
  568. for (var i = attributes.length; i--; ) {
  569. if (attributes[i].name.toLowerCase() === attribute) {
  570. return true;
  571. }
  572. }
  573. return false;
  574. }
  575. function isAttributeRedundant(tag, attrName, attrValue, attrs) {
  576. attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
  577. return (
  578. (tag === 'script' &&
  579. attrName === 'language' &&
  580. attrValue === 'javascript') ||
  581. (tag === 'form' &&
  582. attrName === 'method' &&
  583. attrValue === 'get') ||
  584. (tag === 'input' &&
  585. attrName === 'type' &&
  586. attrValue === 'text') ||
  587. (tag === 'script' &&
  588. attrName === 'charset' &&
  589. !attributesInclude(attrs, 'src')) ||
  590. (tag === 'a' &&
  591. attrName === 'name' &&
  592. attributesInclude(attrs, 'id')) ||
  593. (tag === 'area' &&
  594. attrName === 'shape' &&
  595. attrValue === 'rect')
  596. );
  597. }
  598. function isScriptTypeAttribute(tag, attrName, attrValue) {
  599. return (
  600. tag === 'script' &&
  601. attrName === 'type' &&
  602. trimWhitespace(attrValue.toLowerCase()) === 'text/javascript'
  603. );
  604. }
  605. function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
  606. return (
  607. (tag === 'style' || tag === 'link') &&
  608. attrName === 'type' &&
  609. trimWhitespace(attrValue.toLowerCase()) === 'text/css'
  610. );
  611. }
  612. var enumeratedAttributeValues = {
  613. draggable: ['true', 'false'] // defaults to 'auto'
  614. };
  615. function isBooleanAttribute(attrName, attrValue) {
  616. 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);
  617. if (isSimpleBoolean) {
  618. return true;
  619. }
  620. var attrValueEnumeration = enumeratedAttributeValues[attrName.toLowerCase()];
  621. if (!attrValueEnumeration) {
  622. return false;
  623. }
  624. else {
  625. return (-1 === attrValueEnumeration.indexOf(attrValue.toLowerCase()));
  626. }
  627. }
  628. function isUriTypeAttribute(attrName, tag) {
  629. return (
  630. ((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') ||
  631. (tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) ||
  632. (tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) ||
  633. (tag === 'q' && attrName === 'cite') ||
  634. (tag === 'blockquote' && attrName === 'cite') ||
  635. ((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
  636. (tag === 'form' && attrName === 'action') ||
  637. (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
  638. (tag === 'head' && attrName === 'profile') ||
  639. (tag === 'script' && (attrName === 'src' || attrName === 'for'))
  640. );
  641. }
  642. function isNumberTypeAttribute(attrName, tag) {
  643. return (
  644. ((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') ||
  645. (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
  646. (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
  647. (tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) ||
  648. (tag === 'colgroup' && attrName === 'span') ||
  649. (tag === 'col' && attrName === 'span') ||
  650. ((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
  651. );
  652. }
  653. function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
  654. if (attrValue && isEventAttribute(attrName)) {
  655. attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, '');
  656. if (options.minifyJS) {
  657. var wrappedCode = '(function(){' + attrValue + '})()';
  658. var minified = minifyJS(wrappedCode, options.minifyJS);
  659. return minified.slice(12, minified.length - 4).replace(/"/g, '&quot;');
  660. }
  661. return attrValue;
  662. }
  663. else if (attrName === 'class') {
  664. return collapseWhitespace(trimWhitespace(attrValue));
  665. }
  666. else if (isUriTypeAttribute(attrName, tag)) {
  667. attrValue = trimWhitespace(attrValue);
  668. if (options.minifyURLs) {
  669. return minifyURLs(attrValue, options.minifyURLs);
  670. }
  671. return attrValue;
  672. }
  673. else if (isNumberTypeAttribute(attrName, tag)) {
  674. return trimWhitespace(attrValue);
  675. }
  676. else if (attrName === 'style') {
  677. attrValue = trimWhitespace(attrValue);
  678. if (attrValue) {
  679. attrValue = attrValue.replace(/\s*;\s*$/, '');
  680. }
  681. if (options.minifyCSS) {
  682. return minifyCSS(attrValue, options.minifyCSS);
  683. }
  684. return attrValue;
  685. }
  686. else if (isMetaViewport(tag, attrs) && attrName === 'content') {
  687. attrValue = attrValue.replace(/1\.0/g, '1').replace(/\s+/g, '');
  688. }
  689. else if (attrValue && options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
  690. attrValue = attrValue.replace(/\n+/g, '');
  691. }
  692. return attrValue;
  693. }
  694. function isMetaViewport(tag, attrs) {
  695. if (tag !== 'meta') {
  696. return false;
  697. }
  698. for (var i = 0, len = attrs.length; i < len; i++) {
  699. if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
  700. return true;
  701. }
  702. }
  703. }
  704. function cleanConditionalComment(comment) {
  705. return comment
  706. .replace(/^(\[[^\]]+\]>)\s*/, '$1')
  707. .replace(/\s*(<!\[endif\])$/, '$1');
  708. }
  709. function removeCDATASections(text) {
  710. return text
  711. // "/* <![CDATA[ */" or "// <![CDATA["
  712. .replace(/^(?:\s*\/\*\s*<!\[CDATA\[\s*\*\/|\s*\/\/\s*<!\[CDATA\[.*)/, '')
  713. // "/* ]]> */" or "// ]]>"
  714. .replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, '');
  715. }
  716. function processScript(text, options, currentAttrs) {
  717. for (var i = 0, len = currentAttrs.length; i < len; i++) {
  718. if (currentAttrs[i].name.toLowerCase() === 'type' &&
  719. options.processScripts.indexOf(currentAttrs[i].value) > -1) {
  720. return minify(text, options);
  721. }
  722. }
  723. return text;
  724. }
  725. var reStartDelimiter = {
  726. // account for js + html comments (e.g.: //<!--)
  727. script: /^\s*(?:\/\/)?\s*<!--.*\n?/,
  728. style: /^\s*<!--\s*/
  729. };
  730. var reEndDelimiter = {
  731. script: /\s*(?:\/\/)?\s*-->\s*$/,
  732. style: /\s*-->\s*$/
  733. };
  734. function removeComments(text, tag) {
  735. return text.replace(reStartDelimiter[tag], '').replace(reEndDelimiter[tag], '');
  736. }
  737. function isOptionalTag(tag) {
  738. return (/^(?:html|t?body|t?head|tfoot|tr|td|th|dt|dd|option|colgroup|source)$/).test(tag);
  739. }
  740. var reEmptyAttribute = new RegExp(
  741. '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
  742. '?:down|up|over|move|out)|key(?:press|down|up)))$');
  743. function canDeleteEmptyAttribute(tag, attrName, attrValue) {
  744. var isValueEmpty = !attrValue || (/^\s*$/).test(attrValue);
  745. if (isValueEmpty) {
  746. return (
  747. (tag === 'input' && attrName === 'value') ||
  748. reEmptyAttribute.test(attrName));
  749. }
  750. return false;
  751. }
  752. function canRemoveElement(tag, attrs) {
  753. if (tag === 'textarea') {
  754. return false;
  755. }
  756. if (tag === 'script') {
  757. for (var i = attrs.length - 1; i >= 0; i--) {
  758. if (attrs[i].name === 'src') {
  759. return false;
  760. }
  761. }
  762. }
  763. return true;
  764. }
  765. function canCollapseWhitespace(tag) {
  766. return !(/^(?:script|style|pre|textarea)$/.test(tag));
  767. }
  768. function canTrimWhitespace(tag) {
  769. return !(/^(?:pre|textarea)$/.test(tag));
  770. }
  771. function attrsToMarkup(attrs) {
  772. var markup = '';
  773. for (var i = 0, len = attrs.length; i < len; i++) {
  774. markup += (' ' + attrs[i].name + (isBooleanAttribute(attrs[i].value) ? '' : ('="' + attrs[i].value + '"')));
  775. }
  776. return markup;
  777. }
  778. function normalizeAttribute(attr, attrs, tag, unarySlash, index, options) {
  779. var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
  780. attrValue = attr.escaped,
  781. attrFragment,
  782. emittedAttrValue,
  783. isTerminalOfUnarySlash = unarySlash && index === attrs.length - 1;
  784. if ((options.removeRedundantAttributes &&
  785. isAttributeRedundant(tag, attrName, attrValue, attrs))
  786. ||
  787. (options.removeScriptTypeAttributes &&
  788. isScriptTypeAttribute(tag, attrName, attrValue))
  789. ||
  790. (options.removeStyleLinkTypeAttributes &&
  791. isStyleLinkTypeAttribute(tag, attrName, attrValue))) {
  792. return '';
  793. }
  794. attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
  795. if (attrValue !== undefined && !options.removeAttributeQuotes ||
  796. !canRemoveAttributeQuotes(attrValue) || isTerminalOfUnarySlash) {
  797. emittedAttrValue = '"' + attrValue + '"';
  798. }
  799. else {
  800. emittedAttrValue = attrValue;
  801. }
  802. if (options.removeEmptyAttributes &&
  803. canDeleteEmptyAttribute(tag, attrName, attrValue)) {
  804. return '';
  805. }
  806. if (attrValue === undefined || (options.collapseBooleanAttributes &&
  807. isBooleanAttribute(attrName, attrValue))) {
  808. attrFragment = attrName;
  809. }
  810. else {
  811. attrFragment = attrName + attr.customAssign + emittedAttrValue;
  812. }
  813. return (' ' + attr.customOpen + attrFragment + attr.customClose);
  814. }
  815. function setDefaultTesters(options) {
  816. var defaultTesters = ['canCollapseWhitespace','canTrimWhitespace'];
  817. for (var i = 0, len = defaultTesters.length; i < len; i++) {
  818. if (!options[defaultTesters[i]]) {
  819. options[defaultTesters[i]] = function() {
  820. return false;
  821. };
  822. }
  823. }
  824. }
  825. function minifyURLs(text, options) {
  826. if (typeof options !== 'object') {
  827. options = { };
  828. }
  829. try {
  830. // try to get global reference first
  831. var __RelateUrl = global.RelateUrl;
  832. if (typeof __RelateUrl === 'undefined' && typeof require === 'function') {
  833. __RelateUrl = require('relateurl');
  834. }
  835. // noop
  836. if (!__RelateUrl) {
  837. return text;
  838. }
  839. if (__RelateUrl.relate) {
  840. return __RelateUrl.relate(text, options);
  841. }
  842. else {
  843. return text;
  844. }
  845. }
  846. catch (err) {
  847. log(err);
  848. }
  849. return text;
  850. }
  851. function minifyJS(text, options) {
  852. if (typeof options !== 'object') {
  853. options = { };
  854. }
  855. options.fromString = true;
  856. options.output = { inline_script: true };
  857. try {
  858. // try to get global reference first
  859. var __UglifyJS = global.UglifyJS;
  860. if (typeof __UglifyJS === 'undefined' && typeof require === 'function') {
  861. __UglifyJS = require('uglify-js');
  862. }
  863. // noop
  864. if (!__UglifyJS) {
  865. return text;
  866. }
  867. if (__UglifyJS.minify) {
  868. return __UglifyJS.minify(text, options).code;
  869. }
  870. else if (__UglifyJS.parse) {
  871. var ast = __UglifyJS.parse(text);
  872. ast.figure_out_scope();
  873. var compressor = __UglifyJS.Compressor();
  874. var compressedAst = ast.transform(compressor);
  875. compressedAst.figure_out_scope();
  876. compressedAst.compute_char_frequency();
  877. if (options.mangle !== false) {
  878. compressedAst.mangle_names();
  879. }
  880. var stream = __UglifyJS.OutputStream(options.output);
  881. compressedAst.print(stream);
  882. return stream.toString();
  883. }
  884. else {
  885. return text;
  886. }
  887. }
  888. catch (err) {
  889. log(err);
  890. }
  891. return text;
  892. }
  893. function minifyCSS(text, options) {
  894. if (typeof options !== 'object') {
  895. options = { };
  896. }
  897. if (typeof options.noAdvanced === 'undefined') {
  898. options.noAdvanced = true;
  899. }
  900. try {
  901. if (typeof CleanCSS !== 'undefined') {
  902. return new CleanCSS(options).minify(text);
  903. }
  904. else if (typeof require === 'function') {
  905. var CleanCSSModule = require('clean-css');
  906. return new CleanCSSModule(options).minify(text);
  907. }
  908. }
  909. catch (err) {
  910. log(err);
  911. }
  912. return text;
  913. }
  914. function minify(value, options) {
  915. options = options || {};
  916. value = trimWhitespace(value);
  917. setDefaultTesters(options);
  918. var results = [ ],
  919. buffer = [ ],
  920. currentChars = '',
  921. currentTag = '',
  922. currentAttrs = [],
  923. stackNoTrimWhitespace = [],
  924. stackNoCollapseWhitespace = [],
  925. lint = options.lint,
  926. isIgnoring = false,
  927. t = new Date();
  928. if (options.removeIgnored) {
  929. value = value
  930. .replace(/<\?[^\?]+\?>/g, '')
  931. .replace(/<%[^%]+%>/g, '');
  932. }
  933. function _canCollapseWhitespace(tag, attrs) {
  934. return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
  935. }
  936. function _canTrimWhitespace(tag, attrs) {
  937. return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
  938. }
  939. new HTMLParser(value, {
  940. html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
  941. start: function( tag, attrs, unary, unarySlash ) {
  942. if (isIgnoring) {
  943. buffer.push('<' + tag, attrsToMarkup(attrs), unarySlash ? '/' : '', '>');
  944. return;
  945. }
  946. tag = options.caseSensitive ? tag : tag.toLowerCase();
  947. currentTag = tag;
  948. currentChars = '';
  949. currentAttrs = attrs;
  950. // set whitespace flags for nested tags (eg. <code> within a <pre>)
  951. if (options.collapseWhitespace) {
  952. if (!_canTrimWhitespace(tag, attrs)) {
  953. stackNoTrimWhitespace.push(tag);
  954. }
  955. if (!_canCollapseWhitespace(tag, attrs)) {
  956. stackNoCollapseWhitespace.push(tag);
  957. }
  958. }
  959. var openTag = '<' + tag;
  960. var closeTag = ((unarySlash && options.keepClosingSlash) ? '/' : '') + '>';
  961. if ( attrs.length === 0) {
  962. openTag += closeTag;
  963. }
  964. buffer.push(openTag);
  965. lint && lint.testElement(tag);
  966. var token;
  967. for ( var i = 0, len = attrs.length; i < len; i++ ) {
  968. lint && lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
  969. token = normalizeAttribute(attrs[i], attrs, tag, unarySlash, i, options);
  970. if ( i === len - 1 ) {
  971. token += closeTag;
  972. }
  973. buffer.push(token);
  974. }
  975. },
  976. end: function( tag, attrs ) {
  977. if (isIgnoring) {
  978. buffer.push('</' + tag + '>');
  979. return;
  980. }
  981. // check if current tag is in a whitespace stack
  982. if (options.collapseWhitespace) {
  983. if (stackNoTrimWhitespace.length &&
  984. tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
  985. stackNoTrimWhitespace.pop();
  986. }
  987. if (stackNoCollapseWhitespace.length &&
  988. tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
  989. stackNoCollapseWhitespace.pop();
  990. }
  991. }
  992. var isElementEmpty = currentChars === '' && tag === currentTag;
  993. if ((options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs))) {
  994. // remove last "element" from buffer, return
  995. for ( var i = buffer.length - 1; i >= 0; i-- ) {
  996. if ( /^<[^\/!]/.test(buffer[i]) ) {
  997. buffer.splice(i);
  998. break;
  999. }
  1000. }
  1001. return;
  1002. }
  1003. else if (options.removeOptionalTags && isOptionalTag(tag)) {
  1004. // noop, leave start tag in buffer
  1005. return;
  1006. }
  1007. else {
  1008. // push end tag to buffer
  1009. buffer.push('</' + (options.caseSensitive ? tag : tag.toLowerCase()) + '>');
  1010. results.push.apply(results, buffer);
  1011. }
  1012. // flush buffer
  1013. buffer.length = 0;
  1014. currentChars = '';
  1015. },
  1016. chars: function( text, prevTag, nextTag ) {
  1017. prevTag = prevTag === '' ? 'comment' : prevTag;
  1018. nextTag = nextTag === '' ? 'comment' : nextTag;
  1019. if (isIgnoring) {
  1020. buffer.push(text);
  1021. return;
  1022. }
  1023. if (currentTag === 'script' || currentTag === 'style') {
  1024. if (options.removeCommentsFromCDATA) {
  1025. text = removeComments(text, currentTag);
  1026. }
  1027. if (options.removeCDATASectionsFromCDATA) {
  1028. text = removeCDATASections(text);
  1029. }
  1030. if (options.processScripts) {
  1031. text = processScript(text, options, currentAttrs);
  1032. }
  1033. }
  1034. if (currentTag === 'script' && options.minifyJS) {
  1035. text = minifyJS(text, options.minifyJS);
  1036. }
  1037. if (currentTag === 'style' && options.minifyCSS) {
  1038. text = minifyCSS(text, options.minifyCSS);
  1039. }
  1040. if (options.collapseWhitespace) {
  1041. if (!stackNoTrimWhitespace.length) {
  1042. text = ((prevTag && prevTag !== 'comment') || (nextTag && nextTag !== 'comment')) ?
  1043. collapseWhitespaceSmart(text, prevTag, nextTag, options)
  1044. : trimWhitespace(text);
  1045. }
  1046. if (!stackNoCollapseWhitespace.length) {
  1047. text = !(prevTag && nextTag || nextTag === 'html') ? collapseWhitespace(text) : text;
  1048. }
  1049. }
  1050. currentChars = text;
  1051. lint && lint.testChars(text);
  1052. buffer.push(text);
  1053. },
  1054. comment: function( text, nonStandard ) {
  1055. var prefix = nonStandard ? '<!' : '<!--';
  1056. var suffix = nonStandard ? '>' : '-->';
  1057. if (/^\s*htmlmin:ignore/.test(text)) {
  1058. isIgnoring = !isIgnoring;
  1059. if (!options.removeComments) {
  1060. buffer.push('<!--' + text + '-->');
  1061. }
  1062. return;
  1063. }
  1064. if (options.removeComments) {
  1065. if (isConditionalComment(text)) {
  1066. text = prefix + cleanConditionalComment(text) + suffix;
  1067. }
  1068. else if (isIgnoredComment(text, options)) {
  1069. text = '<!--' + text + '-->';
  1070. }
  1071. else {
  1072. text = '';
  1073. }
  1074. }
  1075. else {
  1076. text = prefix + text + suffix;
  1077. }
  1078. buffer.push(text);
  1079. },
  1080. doctype: function(doctype) {
  1081. buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
  1082. },
  1083. customAttrAssign: options.customAttrAssign,
  1084. customAttrSurround: options.customAttrSurround
  1085. });
  1086. results.push.apply(results, buffer);
  1087. var str = joinResultSegments(results, options);
  1088. log('minified in: ' + (new Date() - t) + 'ms');
  1089. return str;
  1090. }
  1091. function joinResultSegments( results, options ) {
  1092. var str;
  1093. var maxLineLength = options.maxLineLength;
  1094. if ( maxLineLength ) {
  1095. var token;
  1096. var lines = [];
  1097. var line = '';
  1098. for ( var i = 0, len = results.length; i < len; i++ ) {
  1099. token = results[i];
  1100. if ( line.length + token.length < maxLineLength ) {
  1101. line += token;
  1102. }
  1103. else {
  1104. lines.push(line.replace(/^\n/, ''));
  1105. line = token;
  1106. }
  1107. }
  1108. lines.push(line);
  1109. str = lines.join('\n');
  1110. }
  1111. else {
  1112. str = results.join('');
  1113. }
  1114. return str;
  1115. }
  1116. // for CommonJS enviroments, export everything
  1117. if ( typeof exports !== 'undefined' ) {
  1118. exports.minify = minify;
  1119. }
  1120. else {
  1121. global.minify = minify;
  1122. }
  1123. }(this));
  1124. /*!
  1125. * HTMLLint (to be used in conjunction with HTMLMinifier)
  1126. *
  1127. * Copyright (c) 2010-2013 Juriy "kangax" Zaytsev
  1128. * Licensed under the MIT license.
  1129. *
  1130. */
  1131. (function(global) {
  1132. 'use strict';
  1133. function isPresentationalElement(tag) {
  1134. return (/^(?:big|small|hr|blink|marquee)$/).test(tag);
  1135. }
  1136. function isDeprecatedElement(tag) {
  1137. return (/^(?:applet|basefont|center|dir|font|isindex|strike)$/).test(tag);
  1138. }
  1139. function isEventAttribute(attrName) {
  1140. return (/^on[a-z]+/).test(attrName);
  1141. }
  1142. function isStyleAttribute(attrName) {
  1143. return ('style' === attrName.toLowerCase());
  1144. }
  1145. function isDeprecatedAttribute(tag, attrName) {
  1146. return (
  1147. (attrName === 'align' &&
  1148. (/^(?:caption|applet|iframe|img|imput|object|legend|table|hr|div|h[1-6]|p)$/).test(tag)) ||
  1149. (attrName === 'alink' && tag === 'body') ||
  1150. (attrName === 'alt' && tag === 'applet') ||
  1151. (attrName === 'archive' && tag === 'applet') ||
  1152. (attrName === 'background' && tag === 'body') ||
  1153. (attrName === 'bgcolor' && (/^(?:table|t[rdh]|body)$/).test(tag)) ||
  1154. (attrName === 'border' && (/^(?:img|object)$/).test(tag)) ||
  1155. (attrName === 'clear' && tag === 'br') ||
  1156. (attrName === 'code' && tag === 'applet') ||
  1157. (attrName === 'codebase' && tag === 'applet') ||
  1158. (attrName === 'color' && (/^(?:base(?:font)?)$/).test(tag)) ||
  1159. (attrName === 'compact' && (/^(?:dir|[dou]l|menu)$/).test(tag)) ||
  1160. (attrName === 'face' && (/^base(?:font)?$/).test(tag)) ||
  1161. (attrName === 'height' && (/^(?:t[dh]|applet)$/).test(tag)) ||
  1162. (attrName === 'hspace' && (/^(?:applet|img|object)$/).test(tag)) ||
  1163. (attrName === 'language' && tag === 'script') ||
  1164. (attrName === 'link' && tag === 'body') ||
  1165. (attrName === 'name' && tag === 'applet') ||
  1166. (attrName === 'noshade' && tag === 'hr') ||
  1167. (attrName === 'nowrap' && (/^t[dh]$/).test(tag)) ||
  1168. (attrName === 'object' && tag === 'applet') ||
  1169. (attrName === 'prompt' && tag === 'isindex') ||
  1170. (attrName === 'size' && (/^(?:hr|font|basefont)$/).test(tag)) ||
  1171. (attrName === 'start' && tag === 'ol') ||
  1172. (attrName === 'text' && tag === 'body') ||
  1173. (attrName === 'type' && (/^(?:li|ol|ul)$/).test(tag)) ||
  1174. (attrName === 'value' && tag === 'li') ||
  1175. (attrName === 'version' && tag === 'html') ||
  1176. (attrName === 'vlink' && tag === 'body') ||
  1177. (attrName === 'vspace' && (/^(?:applet|img|object)$/).test(tag)) ||
  1178. (attrName === 'width' && (/^(?:hr|td|th|applet|pre)$/).test(tag))
  1179. );
  1180. }
  1181. function isInaccessibleAttribute(attrName, attrValue) {
  1182. return (
  1183. attrName === 'href' &&
  1184. (/^\s*javascript\s*:\s*void\s*(\s+0|\(\s*0\s*\))\s*$/i).test(attrValue)
  1185. );
  1186. }
  1187. function Lint() {
  1188. this.log = [ ];
  1189. this._lastElement = null;
  1190. this._isElementRepeated = false;
  1191. }
  1192. Lint.prototype.testElement = function(tag) {
  1193. if (isDeprecatedElement(tag)) {
  1194. this.log.push(
  1195. 'Found <span class="deprecated-element">deprecated</span> <strong><code>&lt;' +
  1196. tag + '&gt;</code></strong> element'
  1197. );
  1198. }
  1199. else if (isPresentationalElement(tag)) {
  1200. this.log.push(
  1201. 'Found <span class="presentational-element">presentational</span> <strong><code>&lt;' +
  1202. tag + '&gt;</code></strong> element'
  1203. );
  1204. }
  1205. else {
  1206. this.checkRepeatingElement(tag);
  1207. }
  1208. };
  1209. Lint.prototype.checkRepeatingElement = function(tag) {
  1210. if (tag === 'br' && this._lastElement === 'br') {
  1211. this._isElementRepeated = true;
  1212. }
  1213. else if (this._isElementRepeated) {
  1214. this._reportRepeatingElement();
  1215. this._isElementRepeated = false;
  1216. }
  1217. this._lastElement = tag;
  1218. };
  1219. Lint.prototype._reportRepeatingElement = function() {
  1220. this.log.push('Found <code>&lt;br></code> sequence. Try replacing it with styling.');
  1221. };
  1222. Lint.prototype.testAttribute = function(tag, attrName, attrValue) {
  1223. if (isEventAttribute(attrName)) {
  1224. this.log.push(
  1225. 'Found <span class="event-attribute">event attribute</span> (<strong>' +
  1226. attrName + '</strong>) on <strong><code>&lt;' + tag + '&gt;</code></strong> element.'
  1227. );
  1228. }
  1229. else if (isDeprecatedAttribute(tag, attrName)) {
  1230. this.log.push(
  1231. 'Found <span class="deprecated-attribute">deprecated</span> <strong>' +
  1232. attrName + '</strong> attribute on <strong><code>&lt;' + tag + '&gt;</code></strong> element.'
  1233. );
  1234. }
  1235. else if (isStyleAttribute(attrName)) {
  1236. this.log.push(
  1237. 'Found <span class="style-attribute">style attribute</span> on <strong><code>&lt;' +
  1238. tag + '&gt;</code></strong> element.'
  1239. );
  1240. }
  1241. else if (isInaccessibleAttribute(attrName, attrValue)) {
  1242. this.log.push(
  1243. 'Found <span class="inaccessible-attribute">inaccessible attribute</span> ' +
  1244. '(on <strong><code>&lt;' + tag + '&gt;</code></strong> element).'
  1245. );
  1246. }
  1247. };
  1248. Lint.prototype.testChars = function(chars) {
  1249. this._lastElement = '';
  1250. if (/(&nbsp;\s*){2,}/.test(chars)) {
  1251. this.log.push('Found repeating <strong><code>&amp;nbsp;</code></strong> sequence. Try replacing it with styling.');
  1252. }
  1253. };
  1254. Lint.prototype.test = function(tag, attrName, attrValue) {
  1255. this.testElement(tag);
  1256. this.testAttribute(tag, attrName, attrValue);
  1257. };
  1258. Lint.prototype.populate = function(writeToElement) {
  1259. if (this._isElementRepeated) {
  1260. this._reportRepeatingElement();
  1261. }
  1262. if (this.log.length) {
  1263. if (writeToElement) {
  1264. writeToElement.innerHTML = '<ol><li>' + this.log.join('<li>') + '</ol>';
  1265. }
  1266. else {
  1267. var output = ' - ' +
  1268. this.log.join('\n - ')
  1269. .replace(/(<([^>]+)>)/ig, '')
  1270. .replace(/&lt;/g, '<')
  1271. .replace(/&gt;/g, '>');
  1272. console.log(output);
  1273. }
  1274. }
  1275. };
  1276. global.HTMLLint = Lint;
  1277. })(typeof exports === 'undefined' ? this : exports);