htmlminifier.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329
  1. 'use strict';
  2. var CleanCSS = require('clean-css');
  3. var decode = require('he').decode;
  4. var HTMLParser = require('./htmlparser').HTMLParser;
  5. var RelateUrl = require('relateurl');
  6. var TokenChain = require('./tokenchain');
  7. var UglifyJS = require('uglify-js');
  8. var utils = require('./utils');
  9. function trimWhitespace(str) {
  10. return str && str.replace(/^[ \n\r\t\f]+/, '').replace(/[ \n\r\t\f]+$/, '');
  11. }
  12. function collapseWhitespaceAll(str) {
  13. // Non-breaking space is specifically handled inside the replacer function here:
  14. return str && str.replace(/[ \n\r\t\f\xA0]+/g, function(spaces) {
  15. return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 ');
  16. });
  17. }
  18. function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
  19. var lineBreakBefore = '', lineBreakAfter = '';
  20. if (options.preserveLineBreaks) {
  21. str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function() {
  22. lineBreakBefore = '\n';
  23. return '';
  24. }).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function() {
  25. lineBreakAfter = '\n';
  26. return '';
  27. });
  28. }
  29. if (trimLeft) {
  30. // Non-breaking space is specifically handled inside the replacer function here:
  31. str = str.replace(/^[ \n\r\t\f\xA0]+/, function(spaces) {
  32. var conservative = !lineBreakBefore && options.conservativeCollapse;
  33. if (conservative && spaces === '\t') {
  34. return '\t';
  35. }
  36. return spaces.replace(/^[^\xA0]+/, '').replace(/(\xA0+)[^\xA0]+/g, '$1 ') || (conservative ? ' ' : '');
  37. });
  38. }
  39. if (trimRight) {
  40. // Non-breaking space is specifically handled inside the replacer function here:
  41. str = str.replace(/[ \n\r\t\f\xA0]+$/, function(spaces) {
  42. var conservative = !lineBreakAfter && options.conservativeCollapse;
  43. if (conservative && spaces === '\t') {
  44. return '\t';
  45. }
  46. return spaces.replace(/[^\xA0]+(\xA0+)/g, ' $1').replace(/[^\xA0]+$/, '') || (conservative ? ' ' : '');
  47. });
  48. }
  49. if (collapseAll) {
  50. // strip non space whitespace then compress spaces to one
  51. str = collapseWhitespaceAll(str);
  52. }
  53. return lineBreakBefore + str + lineBreakAfter;
  54. }
  55. var createMapFromString = utils.createMapFromString;
  56. // non-empty tags that will maintain whitespace around them
  57. 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');
  58. // non-empty tags that will maintain whitespace within them
  59. 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');
  60. // self-closing tags that will maintain whitespace around them
  61. var selfClosingInlineTags = createMapFromString('comment,img,input,wbr');
  62. function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
  63. var trimLeft = prevTag && !selfClosingInlineTags(prevTag);
  64. if (trimLeft && !options.collapseInlineTagWhitespace) {
  65. trimLeft = prevTag.charAt(0) === '/' ? !inlineTags(prevTag.slice(1)) : !inlineTextTags(prevTag);
  66. }
  67. var trimRight = nextTag && !selfClosingInlineTags(nextTag);
  68. if (trimRight && !options.collapseInlineTagWhitespace) {
  69. trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags(nextTag.slice(1)) : !inlineTags(nextTag);
  70. }
  71. return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
  72. }
  73. function isConditionalComment(text) {
  74. return /^\[if\s[^\]]+]|\[endif]$/.test(text);
  75. }
  76. function isIgnoredComment(text, options) {
  77. for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
  78. if (options.ignoreCustomComments[i].test(text)) {
  79. return true;
  80. }
  81. }
  82. return false;
  83. }
  84. function isEventAttribute(attrName, options) {
  85. var patterns = options.customEventAttributes;
  86. if (patterns) {
  87. for (var i = patterns.length; i--;) {
  88. if (patterns[i].test(attrName)) {
  89. return true;
  90. }
  91. }
  92. return false;
  93. }
  94. return /^on[a-z]{3,}$/.test(attrName);
  95. }
  96. function canRemoveAttributeQuotes(value) {
  97. // https://mathiasbynens.be/notes/unquoted-attribute-values
  98. return /^[^ \t\n\f\r"'`=<>]+$/.test(value);
  99. }
  100. function attributesInclude(attributes, attribute) {
  101. for (var i = attributes.length; i--;) {
  102. if (attributes[i].name.toLowerCase() === attribute) {
  103. return true;
  104. }
  105. }
  106. return false;
  107. }
  108. function isAttributeRedundant(tag, attrName, attrValue, attrs) {
  109. attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
  110. return (
  111. tag === 'script' &&
  112. attrName === 'language' &&
  113. attrValue === 'javascript' ||
  114. tag === 'form' &&
  115. attrName === 'method' &&
  116. attrValue === 'get' ||
  117. tag === 'input' &&
  118. attrName === 'type' &&
  119. attrValue === 'text' ||
  120. tag === 'script' &&
  121. attrName === 'charset' &&
  122. !attributesInclude(attrs, 'src') ||
  123. tag === 'a' &&
  124. attrName === 'name' &&
  125. attributesInclude(attrs, 'id') ||
  126. tag === 'area' &&
  127. attrName === 'shape' &&
  128. attrValue === 'rect'
  129. );
  130. }
  131. // https://mathiasbynens.be/demo/javascript-mime-type
  132. // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
  133. var executableScriptsMimetypes = utils.createMap([
  134. 'text/javascript',
  135. 'text/ecmascript',
  136. 'text/jscript',
  137. 'application/javascript',
  138. 'application/x-javascript',
  139. 'application/ecmascript'
  140. ]);
  141. function isScriptTypeAttribute(attrValue) {
  142. attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
  143. return attrValue === '' || executableScriptsMimetypes(attrValue);
  144. }
  145. function isExecutableScript(tag, attrs) {
  146. if (tag !== 'script') {
  147. return false;
  148. }
  149. for (var i = 0, len = attrs.length; i < len; i++) {
  150. var attrName = attrs[i].name.toLowerCase();
  151. if (attrName === 'type') {
  152. return isScriptTypeAttribute(attrs[i].value);
  153. }
  154. }
  155. return true;
  156. }
  157. function isStyleLinkTypeAttribute(attrValue) {
  158. attrValue = trimWhitespace(attrValue).toLowerCase();
  159. return attrValue === '' || attrValue === 'text/css';
  160. }
  161. function isStyleSheet(tag, attrs) {
  162. if (tag !== 'style') {
  163. return false;
  164. }
  165. for (var i = 0, len = attrs.length; i < len; i++) {
  166. var attrName = attrs[i].name.toLowerCase();
  167. if (attrName === 'type') {
  168. return isStyleLinkTypeAttribute(attrs[i].value);
  169. }
  170. }
  171. return true;
  172. }
  173. 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');
  174. var isBooleanValue = createMapFromString('true,false');
  175. function isBooleanAttribute(attrName, attrValue) {
  176. return isSimpleBoolean(attrName) || attrName === 'draggable' && !isBooleanValue(attrValue);
  177. }
  178. function isUriTypeAttribute(attrName, tag) {
  179. return (
  180. /^(?:a|area|link|base)$/.test(tag) && attrName === 'href' ||
  181. tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName) ||
  182. tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName) ||
  183. tag === 'q' && attrName === 'cite' ||
  184. tag === 'blockquote' && attrName === 'cite' ||
  185. (tag === 'ins' || tag === 'del') && attrName === 'cite' ||
  186. tag === 'form' && attrName === 'action' ||
  187. tag === 'input' && (attrName === 'src' || attrName === 'usemap') ||
  188. tag === 'head' && attrName === 'profile' ||
  189. tag === 'script' && (attrName === 'src' || attrName === 'for')
  190. );
  191. }
  192. function isNumberTypeAttribute(attrName, tag) {
  193. return (
  194. /^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex' ||
  195. tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex') ||
  196. tag === 'select' && (attrName === 'size' || attrName === 'tabindex') ||
  197. tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName) ||
  198. tag === 'colgroup' && attrName === 'span' ||
  199. tag === 'col' && attrName === 'span' ||
  200. (tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan')
  201. );
  202. }
  203. function isLinkType(tag, attrs, value) {
  204. if (tag !== 'link') {
  205. return false;
  206. }
  207. for (var i = 0, len = attrs.length; i < len; i++) {
  208. if (attrs[i].name === 'rel' && attrs[i].value === value) {
  209. return true;
  210. }
  211. }
  212. }
  213. function isMediaQuery(tag, attrs, attrName) {
  214. return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
  215. }
  216. var srcsetTags = createMapFromString('img,source');
  217. function isSrcset(attrName, tag) {
  218. return attrName === 'srcset' && srcsetTags(tag);
  219. }
  220. function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
  221. if (isEventAttribute(attrName, options)) {
  222. attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
  223. return options.minifyJS(attrValue, true);
  224. }
  225. else if (attrName === 'class') {
  226. attrValue = trimWhitespace(attrValue);
  227. if (options.sortClassName) {
  228. attrValue = options.sortClassName(attrValue);
  229. }
  230. else {
  231. attrValue = collapseWhitespaceAll(attrValue);
  232. }
  233. return attrValue;
  234. }
  235. else if (isUriTypeAttribute(attrName, tag)) {
  236. attrValue = trimWhitespace(attrValue);
  237. return isLinkType(tag, attrs, 'canonical') ? attrValue : options.minifyURLs(attrValue);
  238. }
  239. else if (isNumberTypeAttribute(attrName, tag)) {
  240. return trimWhitespace(attrValue);
  241. }
  242. else if (attrName === 'style') {
  243. attrValue = trimWhitespace(attrValue);
  244. if (attrValue) {
  245. if (/;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
  246. attrValue = attrValue.replace(/\s*;$/, ';');
  247. }
  248. attrValue = options.minifyCSS(attrValue, 'inline');
  249. }
  250. return attrValue;
  251. }
  252. else if (isSrcset(attrName, tag)) {
  253. // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
  254. attrValue = trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(function(candidate) {
  255. var url = candidate;
  256. var descriptor = '';
  257. var match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
  258. if (match) {
  259. url = url.slice(0, -match[0].length);
  260. var num = +match[1].slice(0, -1);
  261. var suffix = match[1].slice(-1);
  262. if (num !== 1 || suffix !== 'x') {
  263. descriptor = ' ' + num + suffix;
  264. }
  265. }
  266. return options.minifyURLs(url) + descriptor;
  267. }).join(', ');
  268. }
  269. else if (isMetaViewport(tag, attrs) && attrName === 'content') {
  270. attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function(numString) {
  271. // "0.90000" -> "0.9"
  272. // "1.0" -> "1"
  273. // "1.0001" -> "1.0001" (unchanged)
  274. return (+numString).toString();
  275. });
  276. }
  277. else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
  278. attrValue = attrValue.replace(/\n+|\r+|\s{2,}/g, '');
  279. }
  280. else if (tag === 'script' && attrName === 'type') {
  281. attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
  282. }
  283. else if (isMediaQuery(tag, attrs, attrName)) {
  284. attrValue = trimWhitespace(attrValue);
  285. return options.minifyCSS(attrValue, 'media');
  286. }
  287. return attrValue;
  288. }
  289. function isMetaViewport(tag, attrs) {
  290. if (tag !== 'meta') {
  291. return false;
  292. }
  293. for (var i = 0, len = attrs.length; i < len; i++) {
  294. if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
  295. return true;
  296. }
  297. }
  298. }
  299. function ignoreCSS(id) {
  300. return '/* clean-css ignore:start */' + id + '/* clean-css ignore:end */';
  301. }
  302. // Wrap CSS declarations for CleanCSS > 3.x
  303. // See https://github.com/jakubpawlowicz/clean-css/issues/418
  304. function wrapCSS(text, type) {
  305. switch (type) {
  306. case 'inline':
  307. return '*{' + text + '}';
  308. case 'media':
  309. return '@media ' + text + '{a{top:0}}';
  310. default:
  311. return text;
  312. }
  313. }
  314. function unwrapCSS(text, type) {
  315. var matches;
  316. switch (type) {
  317. case 'inline':
  318. matches = text.match(/^\*\{([\s\S]*)\}$/);
  319. break;
  320. case 'media':
  321. matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
  322. break;
  323. }
  324. return matches ? matches[1] : text;
  325. }
  326. function cleanConditionalComment(comment, options) {
  327. return options.processConditionalComments ? comment.replace(/^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, function(match, prefix, text, suffix) {
  328. return prefix + minify(text, options, true) + suffix;
  329. }) : comment;
  330. }
  331. function processScript(text, options, currentAttrs) {
  332. for (var i = 0, len = currentAttrs.length; i < len; i++) {
  333. if (currentAttrs[i].name.toLowerCase() === 'type' &&
  334. options.processScripts.indexOf(currentAttrs[i].value) > -1) {
  335. return minify(text, options);
  336. }
  337. }
  338. return text;
  339. }
  340. // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
  341. // with the following deviations:
  342. // - retain <body> if followed by <noscript>
  343. // - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
  344. // - retain all tags which are adjacent to non-standard HTML tags
  345. var optionalStartTags = createMapFromString('html,head,body,colgroup,tbody');
  346. var optionalEndTags = createMapFromString('html,head,body,li,dt,dd,p,rb,rt,rtc,rp,optgroup,option,colgroup,caption,thead,tbody,tfoot,tr,td,th');
  347. var headerTags = createMapFromString('meta,link,script,style,template,noscript');
  348. var descriptionTags = createMapFromString('dt,dd');
  349. 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');
  350. var pInlineTags = createMapFromString('a,audio,del,ins,map,noscript,video');
  351. var rubyTags = createMapFromString('rb,rt,rtc,rp');
  352. var rtcTag = createMapFromString('rb,rtc,rp');
  353. var optionTag = createMapFromString('option,optgroup');
  354. var tableContentTags = createMapFromString('tbody,tfoot');
  355. var tableSectionTags = createMapFromString('thead,tbody,tfoot');
  356. var cellTags = createMapFromString('td,th');
  357. var topLevelTags = createMapFromString('html,head,body');
  358. var compactTags = createMapFromString('html,body');
  359. var looseTags = createMapFromString('head,colgroup,caption');
  360. var trailingTags = createMapFromString('dt,thead');
  361. 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');
  362. function canRemoveParentTag(optionalStartTag, tag) {
  363. switch (optionalStartTag) {
  364. case 'html':
  365. case 'head':
  366. return true;
  367. case 'body':
  368. return !headerTags(tag);
  369. case 'colgroup':
  370. return tag === 'col';
  371. case 'tbody':
  372. return tag === 'tr';
  373. }
  374. return false;
  375. }
  376. function isStartTagMandatory(optionalEndTag, tag) {
  377. switch (tag) {
  378. case 'colgroup':
  379. return optionalEndTag === 'colgroup';
  380. case 'tbody':
  381. return tableSectionTags(optionalEndTag);
  382. }
  383. return false;
  384. }
  385. function canRemovePrecedingTag(optionalEndTag, tag) {
  386. switch (optionalEndTag) {
  387. case 'html':
  388. case 'head':
  389. case 'body':
  390. case 'colgroup':
  391. case 'caption':
  392. return true;
  393. case 'li':
  394. case 'optgroup':
  395. case 'tr':
  396. return tag === optionalEndTag;
  397. case 'dt':
  398. case 'dd':
  399. return descriptionTags(tag);
  400. case 'p':
  401. return pBlockTags(tag);
  402. case 'rb':
  403. case 'rt':
  404. case 'rp':
  405. return rubyTags(tag);
  406. case 'rtc':
  407. return rtcTag(tag);
  408. case 'option':
  409. return optionTag(tag);
  410. case 'thead':
  411. case 'tbody':
  412. return tableContentTags(tag);
  413. case 'tfoot':
  414. return tag === 'tbody';
  415. case 'td':
  416. case 'th':
  417. return cellTags(tag);
  418. }
  419. return false;
  420. }
  421. var reEmptyAttribute = new RegExp(
  422. '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
  423. '?:down|up|over|move|out)|key(?:press|down|up)))$');
  424. function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
  425. var isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
  426. if (!isValueEmpty) {
  427. return false;
  428. }
  429. if (typeof options.removeEmptyAttributes === 'function') {
  430. return options.removeEmptyAttributes(attrName, tag);
  431. }
  432. return tag === 'input' && attrName === 'value' || reEmptyAttribute.test(attrName);
  433. }
  434. function hasAttrName(name, attrs) {
  435. for (var i = attrs.length - 1; i >= 0; i--) {
  436. if (attrs[i].name === name) {
  437. return true;
  438. }
  439. }
  440. return false;
  441. }
  442. function canRemoveElement(tag, attrs) {
  443. switch (tag) {
  444. case 'textarea':
  445. return false;
  446. case 'audio':
  447. case 'script':
  448. case 'video':
  449. if (hasAttrName('src', attrs)) {
  450. return false;
  451. }
  452. break;
  453. case 'iframe':
  454. if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
  455. return false;
  456. }
  457. break;
  458. case 'object':
  459. if (hasAttrName('data', attrs)) {
  460. return false;
  461. }
  462. break;
  463. case 'applet':
  464. if (hasAttrName('code', attrs)) {
  465. return false;
  466. }
  467. break;
  468. }
  469. return true;
  470. }
  471. function canCollapseWhitespace(tag) {
  472. return !/^(?:script|style|pre|textarea)$/.test(tag);
  473. }
  474. function canTrimWhitespace(tag) {
  475. return !/^(?:pre|textarea)$/.test(tag);
  476. }
  477. function normalizeAttr(attr, attrs, tag, options) {
  478. var attrName = options.name(attr.name),
  479. attrValue = attr.value;
  480. if (options.decodeEntities && attrValue) {
  481. attrValue = decode(attrValue, { isAttributeValue: true });
  482. }
  483. if (options.removeRedundantAttributes &&
  484. isAttributeRedundant(tag, attrName, attrValue, attrs) ||
  485. options.removeScriptTypeAttributes && tag === 'script' &&
  486. attrName === 'type' && isScriptTypeAttribute(attrValue) ||
  487. options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
  488. attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) {
  489. return;
  490. }
  491. if (attrValue) {
  492. attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
  493. }
  494. if (options.removeEmptyAttributes &&
  495. canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
  496. return;
  497. }
  498. if (options.decodeEntities && attrValue) {
  499. attrValue = attrValue.replace(/&(#?[0-9a-zA-Z]+;)/g, '&amp;$1');
  500. }
  501. return {
  502. attr: attr,
  503. name: attrName,
  504. value: attrValue
  505. };
  506. }
  507. function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
  508. var attrName = normalized.name,
  509. attrValue = normalized.value,
  510. attr = normalized.attr,
  511. attrQuote = attr.quote,
  512. attrFragment,
  513. emittedAttrValue;
  514. if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
  515. ~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) {
  516. if (!options.preventAttributesEscaping) {
  517. if (typeof options.quoteCharacter === 'undefined') {
  518. var apos = (attrValue.match(/'/g) || []).length;
  519. var quot = (attrValue.match(/"/g) || []).length;
  520. attrQuote = apos < quot ? '\'' : '"';
  521. }
  522. else {
  523. attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
  524. }
  525. if (attrQuote === '"') {
  526. attrValue = attrValue.replace(/"/g, '&#34;');
  527. }
  528. else {
  529. attrValue = attrValue.replace(/'/g, '&#39;');
  530. }
  531. }
  532. emittedAttrValue = attrQuote + attrValue + attrQuote;
  533. if (!isLast && !options.removeTagWhitespace) {
  534. emittedAttrValue += ' ';
  535. }
  536. }
  537. // make sure trailing slash is not interpreted as HTML self-closing tag
  538. else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
  539. emittedAttrValue = attrValue;
  540. }
  541. else {
  542. emittedAttrValue = attrValue + ' ';
  543. }
  544. if (typeof attrValue === 'undefined' || options.collapseBooleanAttributes &&
  545. isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase())) {
  546. attrFragment = attrName;
  547. if (!isLast) {
  548. attrFragment += ' ';
  549. }
  550. }
  551. else {
  552. attrFragment = attrName + attr.customAssign + emittedAttrValue;
  553. }
  554. return attr.customOpen + attrFragment + attr.customClose;
  555. }
  556. function identity(value) {
  557. return value;
  558. }
  559. function processOptions(values) {
  560. var options = {
  561. name: function(name) {
  562. return name.toLowerCase();
  563. },
  564. canCollapseWhitespace: canCollapseWhitespace,
  565. canTrimWhitespace: canTrimWhitespace,
  566. html5: true,
  567. ignoreCustomComments: [/^!/],
  568. ignoreCustomFragments: [
  569. /<%[\s\S]*?%>/,
  570. /<\?[\s\S]*?\?>/
  571. ],
  572. includeAutoGeneratedTags: true,
  573. log: identity,
  574. minifyCSS: identity,
  575. minifyJS: identity,
  576. minifyURLs: identity
  577. };
  578. Object.keys(values).forEach(function(key) {
  579. var value = values[key];
  580. if (key === 'caseSensitive') {
  581. if (value) {
  582. options.name = identity;
  583. }
  584. }
  585. else if (key === 'log') {
  586. if (typeof value === 'function') {
  587. options.log = value;
  588. }
  589. }
  590. else if (key === 'minifyCSS' && typeof value !== 'function') {
  591. if (!value) {
  592. return;
  593. }
  594. if (typeof value !== 'object') {
  595. value = {};
  596. }
  597. options.minifyCSS = function(text, type) {
  598. text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function(match, prefix, quote, url, suffix) {
  599. return prefix + quote + options.minifyURLs(url) + quote + suffix;
  600. });
  601. var cleanCssOutput = new CleanCSS(value).minify(wrapCSS(text, type));
  602. if (cleanCssOutput.errors.length > 0) {
  603. cleanCssOutput.errors.forEach(options.log);
  604. return text;
  605. }
  606. return unwrapCSS(cleanCssOutput.styles, type);
  607. };
  608. }
  609. else if (key === 'minifyJS' && typeof value !== 'function') {
  610. if (!value) {
  611. return;
  612. }
  613. if (typeof value !== 'object') {
  614. value = {};
  615. }
  616. (value.parse || (value.parse = {})).bare_returns = false;
  617. options.minifyJS = function(text, inline) {
  618. var start = text.match(/^\s*<!--.*/);
  619. var code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
  620. value.parse.bare_returns = inline;
  621. var result = UglifyJS.minify(code, value);
  622. if (result.error) {
  623. options.log(result.error);
  624. return text;
  625. }
  626. return result.code.replace(/;$/, '');
  627. };
  628. }
  629. else if (key === 'minifyURLs' && typeof value !== 'function') {
  630. if (!value) {
  631. return;
  632. }
  633. if (typeof value === 'string') {
  634. value = { site: value };
  635. }
  636. else if (typeof value !== 'object') {
  637. value = {};
  638. }
  639. options.minifyURLs = function(text) {
  640. try {
  641. return RelateUrl.relate(text, value);
  642. }
  643. catch (err) {
  644. options.log(err);
  645. return text;
  646. }
  647. };
  648. }
  649. else {
  650. options[key] = value;
  651. }
  652. });
  653. return options;
  654. }
  655. function uniqueId(value) {
  656. var id;
  657. do {
  658. id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
  659. } while (~value.indexOf(id));
  660. return id;
  661. }
  662. var specialContentTags = createMapFromString('script,style');
  663. function createSortFns(value, options, uidIgnore, uidAttr) {
  664. var attrChains = options.sortAttributes && Object.create(null);
  665. var classChain = options.sortClassName && new TokenChain();
  666. function attrNames(attrs) {
  667. return attrs.map(function(attr) {
  668. return options.name(attr.name);
  669. });
  670. }
  671. function shouldSkipUID(token, uid) {
  672. return !uid || token.indexOf(uid) === -1;
  673. }
  674. function shouldSkipUIDs(token) {
  675. return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
  676. }
  677. function scan(input) {
  678. var currentTag, currentType;
  679. new HTMLParser(input, {
  680. start: function(tag, attrs) {
  681. if (attrChains) {
  682. if (!attrChains[tag]) {
  683. attrChains[tag] = new TokenChain();
  684. }
  685. attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs));
  686. }
  687. for (var i = 0, len = attrs.length; i < len; i++) {
  688. var attr = attrs[i];
  689. if (classChain && attr.value && options.name(attr.name) === 'class') {
  690. classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs));
  691. }
  692. else if (options.processScripts && attr.name.toLowerCase() === 'type') {
  693. currentTag = tag;
  694. currentType = attr.value;
  695. }
  696. }
  697. },
  698. end: function() {
  699. currentTag = '';
  700. },
  701. chars: function(text) {
  702. if (options.processScripts && specialContentTags(currentTag) &&
  703. options.processScripts.indexOf(currentType) > -1) {
  704. scan(text);
  705. }
  706. }
  707. });
  708. }
  709. var log = options.log;
  710. options.log = identity;
  711. options.sortAttributes = false;
  712. options.sortClassName = false;
  713. scan(minify(value, options));
  714. options.log = log;
  715. if (attrChains) {
  716. var attrSorters = Object.create(null);
  717. for (var tag in attrChains) {
  718. attrSorters[tag] = attrChains[tag].createSorter();
  719. }
  720. options.sortAttributes = function(tag, attrs) {
  721. var sorter = attrSorters[tag];
  722. if (sorter) {
  723. var attrMap = Object.create(null);
  724. var names = attrNames(attrs);
  725. names.forEach(function(name, index) {
  726. (attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
  727. });
  728. sorter.sort(names).forEach(function(name, index) {
  729. attrs[index] = attrMap[name].shift();
  730. });
  731. }
  732. };
  733. }
  734. if (classChain) {
  735. var sorter = classChain.createSorter();
  736. options.sortClassName = function(value) {
  737. return sorter.sort(value.split(/[ \n\f\r]+/)).join(' ');
  738. };
  739. }
  740. }
  741. function minify(value, options, partialMarkup) {
  742. if (options.collapseWhitespace) {
  743. value = collapseWhitespace(value, options, true, true);
  744. }
  745. var buffer = [],
  746. charsPrevTag,
  747. currentChars = '',
  748. hasChars,
  749. currentTag = '',
  750. currentAttrs = [],
  751. stackNoTrimWhitespace = [],
  752. stackNoCollapseWhitespace = [],
  753. optionalStartTag = '',
  754. optionalEndTag = '',
  755. ignoredMarkupChunks = [],
  756. ignoredCustomMarkupChunks = [],
  757. uidIgnore,
  758. uidAttr,
  759. uidPattern;
  760. // temporarily replace ignored chunks with comments,
  761. // so that we don't have to worry what's there.
  762. // for all we care there might be
  763. // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
  764. value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function(match, group1) {
  765. if (!uidIgnore) {
  766. uidIgnore = uniqueId(value);
  767. var pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
  768. if (options.ignoreCustomComments) {
  769. options.ignoreCustomComments = options.ignoreCustomComments.slice();
  770. }
  771. else {
  772. options.ignoreCustomComments = [];
  773. }
  774. options.ignoreCustomComments.push(pattern);
  775. }
  776. var token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
  777. ignoredMarkupChunks.push(group1);
  778. return token;
  779. });
  780. var customFragments = options.ignoreCustomFragments.map(function(re) {
  781. return re.source;
  782. });
  783. if (customFragments.length) {
  784. var reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
  785. // temporarily replace custom ignored fragments with unique attributes
  786. value = value.replace(reCustomIgnore, function(match) {
  787. if (!uidAttr) {
  788. uidAttr = uniqueId(value);
  789. uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)(\\s*)', 'g');
  790. if (options.minifyCSS) {
  791. options.minifyCSS = (function(fn) {
  792. return function(text, type) {
  793. text = text.replace(uidPattern, function(match, prefix, index) {
  794. var chunks = ignoredCustomMarkupChunks[+index];
  795. return chunks[1] + uidAttr + index + chunks[2];
  796. });
  797. var ids = [];
  798. new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function(warning) {
  799. var match = uidPattern.exec(warning);
  800. if (match) {
  801. var id = uidAttr + match[2];
  802. text = text.replace(id, ignoreCSS(id));
  803. ids.push(id);
  804. }
  805. });
  806. text = fn(text, type);
  807. ids.forEach(function(id) {
  808. text = text.replace(ignoreCSS(id), id);
  809. });
  810. return text;
  811. };
  812. })(options.minifyCSS);
  813. }
  814. if (options.minifyJS) {
  815. options.minifyJS = (function(fn) {
  816. return function(text, type) {
  817. return fn(text.replace(uidPattern, function(match, prefix, index) {
  818. var chunks = ignoredCustomMarkupChunks[+index];
  819. return chunks[1] + uidAttr + index + chunks[2];
  820. }), type);
  821. };
  822. })(options.minifyJS);
  823. }
  824. }
  825. var token = uidAttr + ignoredCustomMarkupChunks.length;
  826. ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
  827. return '\t' + token + '\t';
  828. });
  829. }
  830. if (options.sortAttributes && typeof options.sortAttributes !== 'function' ||
  831. options.sortClassName && typeof options.sortClassName !== 'function') {
  832. createSortFns(value, options, uidIgnore, uidAttr);
  833. }
  834. function _canCollapseWhitespace(tag, attrs) {
  835. return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
  836. }
  837. function _canTrimWhitespace(tag, attrs) {
  838. return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
  839. }
  840. function removeStartTag() {
  841. var index = buffer.length - 1;
  842. while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
  843. index--;
  844. }
  845. buffer.length = Math.max(0, index);
  846. }
  847. function removeEndTag() {
  848. var index = buffer.length - 1;
  849. while (index > 0 && !/^<\//.test(buffer[index])) {
  850. index--;
  851. }
  852. buffer.length = Math.max(0, index);
  853. }
  854. // look for trailing whitespaces, bypass any inline tags
  855. function trimTrailingWhitespace(index, nextTag) {
  856. for (var endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
  857. var str = buffer[index];
  858. var match = str.match(/^<\/([\w:-]+)>$/);
  859. if (match) {
  860. endTag = match[1];
  861. }
  862. else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options))) {
  863. break;
  864. }
  865. }
  866. }
  867. // look for trailing whitespaces from previously processed text
  868. // which may not be trimmed due to a following comment or an empty
  869. // element which has now been removed
  870. function squashTrailingWhitespace(nextTag) {
  871. var charsIndex = buffer.length - 1;
  872. if (buffer.length > 1) {
  873. var item = buffer[buffer.length - 1];
  874. if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
  875. charsIndex--;
  876. }
  877. }
  878. trimTrailingWhitespace(charsIndex, nextTag);
  879. }
  880. new HTMLParser(value, {
  881. partialMarkup: partialMarkup,
  882. html5: options.html5,
  883. start: function(tag, attrs, unary, unarySlash, autoGenerated) {
  884. if (tag.toLowerCase() === 'svg') {
  885. options = Object.create(options);
  886. options.caseSensitive = true;
  887. options.keepClosingSlash = true;
  888. options.name = identity;
  889. }
  890. tag = options.name(tag);
  891. currentTag = tag;
  892. charsPrevTag = tag;
  893. if (!inlineTextTags(tag)) {
  894. currentChars = '';
  895. }
  896. hasChars = false;
  897. currentAttrs = attrs;
  898. var optional = options.removeOptionalTags;
  899. if (optional) {
  900. var htmlTag = htmlTags(tag);
  901. // <html> may be omitted if first thing inside is not comment
  902. // <head> may be omitted if first thing inside is an element
  903. // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
  904. // <colgroup> may be omitted if first thing inside is <col>
  905. // <tbody> may be omitted if first thing inside is <tr>
  906. if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
  907. removeStartTag();
  908. }
  909. optionalStartTag = '';
  910. // end-tag-followed-by-start-tag omission rules
  911. if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
  912. removeEndTag();
  913. // <colgroup> cannot be omitted if preceding </colgroup> is omitted
  914. // <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted
  915. optional = !isStartTagMandatory(optionalEndTag, tag);
  916. }
  917. optionalEndTag = '';
  918. }
  919. // set whitespace flags for nested tags (eg. <code> within a <pre>)
  920. if (options.collapseWhitespace) {
  921. if (!stackNoTrimWhitespace.length) {
  922. squashTrailingWhitespace(tag);
  923. }
  924. if (!unary) {
  925. if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
  926. stackNoTrimWhitespace.push(tag);
  927. }
  928. if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
  929. stackNoCollapseWhitespace.push(tag);
  930. }
  931. }
  932. }
  933. var openTag = '<' + tag;
  934. var hasUnarySlash = unarySlash && options.keepClosingSlash;
  935. buffer.push(openTag);
  936. if (options.sortAttributes) {
  937. options.sortAttributes(tag, attrs);
  938. }
  939. var parts = [];
  940. for (var i = attrs.length, isLast = true; --i >= 0;) {
  941. var normalized = normalizeAttr(attrs[i], attrs, tag, options);
  942. if (normalized) {
  943. parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr));
  944. isLast = false;
  945. }
  946. }
  947. if (parts.length > 0) {
  948. buffer.push(' ');
  949. buffer.push.apply(buffer, parts);
  950. }
  951. // start tag must never be omitted if it has any attributes
  952. else if (optional && optionalStartTags(tag)) {
  953. optionalStartTag = tag;
  954. }
  955. buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
  956. if (autoGenerated && !options.includeAutoGeneratedTags) {
  957. removeStartTag();
  958. optionalStartTag = '';
  959. }
  960. },
  961. end: function(tag, attrs, autoGenerated) {
  962. if (tag.toLowerCase() === 'svg') {
  963. options = Object.getPrototypeOf(options);
  964. }
  965. tag = options.name(tag);
  966. // check if current tag is in a whitespace stack
  967. if (options.collapseWhitespace) {
  968. if (stackNoTrimWhitespace.length) {
  969. if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
  970. stackNoTrimWhitespace.pop();
  971. }
  972. }
  973. else {
  974. squashTrailingWhitespace('/' + tag);
  975. }
  976. if (stackNoCollapseWhitespace.length &&
  977. tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
  978. stackNoCollapseWhitespace.pop();
  979. }
  980. }
  981. var isElementEmpty = false;
  982. if (tag === currentTag) {
  983. currentTag = '';
  984. isElementEmpty = !hasChars;
  985. }
  986. if (options.removeOptionalTags) {
  987. // <html>, <head> or <body> may be omitted if the element is empty
  988. if (isElementEmpty && topLevelTags(optionalStartTag)) {
  989. removeStartTag();
  990. }
  991. optionalStartTag = '';
  992. // </html> or </body> may be omitted if not followed by comment
  993. // </head> may be omitted if not followed by space or comment
  994. // </p> may be omitted if no more content in non-</a> parent
  995. // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
  996. if (htmlTags(tag) && optionalEndTag && !trailingTags(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags(tag))) {
  997. removeEndTag();
  998. }
  999. optionalEndTag = optionalEndTags(tag) ? tag : '';
  1000. }
  1001. if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
  1002. // remove last "element" from buffer
  1003. removeStartTag();
  1004. optionalStartTag = '';
  1005. optionalEndTag = '';
  1006. }
  1007. else {
  1008. if (autoGenerated && !options.includeAutoGeneratedTags) {
  1009. optionalEndTag = '';
  1010. }
  1011. else {
  1012. buffer.push('</' + tag + '>');
  1013. }
  1014. charsPrevTag = '/' + tag;
  1015. if (!inlineTags(tag)) {
  1016. currentChars = '';
  1017. }
  1018. else if (isElementEmpty) {
  1019. currentChars += '|';
  1020. }
  1021. }
  1022. },
  1023. chars: function(text, prevTag, nextTag) {
  1024. prevTag = prevTag === '' ? 'comment' : prevTag;
  1025. nextTag = nextTag === '' ? 'comment' : nextTag;
  1026. if (options.decodeEntities && text && !specialContentTags(currentTag)) {
  1027. text = decode(text);
  1028. }
  1029. if (options.collapseWhitespace) {
  1030. if (!stackNoTrimWhitespace.length) {
  1031. if (prevTag === 'comment') {
  1032. var prevComment = buffer[buffer.length - 1];
  1033. if (prevComment.indexOf(uidIgnore) === -1) {
  1034. if (!prevComment) {
  1035. prevTag = charsPrevTag;
  1036. }
  1037. if (buffer.length > 1 && (!prevComment || !options.conservativeCollapse && / $/.test(currentChars))) {
  1038. var charsIndex = buffer.length - 2;
  1039. buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function(trailingSpaces) {
  1040. text = trailingSpaces + text;
  1041. return '';
  1042. });
  1043. }
  1044. }
  1045. }
  1046. if (prevTag) {
  1047. if (prevTag === '/nobr' || prevTag === 'wbr') {
  1048. if (/^\s/.test(text)) {
  1049. var tagIndex = buffer.length - 1;
  1050. while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
  1051. tagIndex--;
  1052. }
  1053. trimTrailingWhitespace(tagIndex - 1, 'br');
  1054. }
  1055. }
  1056. else if (inlineTextTags(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
  1057. text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
  1058. }
  1059. }
  1060. if (prevTag || nextTag) {
  1061. text = collapseWhitespaceSmart(text, prevTag, nextTag, options);
  1062. }
  1063. else {
  1064. text = collapseWhitespace(text, options, true, true);
  1065. }
  1066. if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
  1067. trimTrailingWhitespace(buffer.length - 1, nextTag);
  1068. }
  1069. }
  1070. if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
  1071. text = collapseWhitespace(text, options, false, false, true);
  1072. }
  1073. }
  1074. if (options.processScripts && specialContentTags(currentTag)) {
  1075. text = processScript(text, options, currentAttrs);
  1076. }
  1077. if (isExecutableScript(currentTag, currentAttrs)) {
  1078. text = options.minifyJS(text);
  1079. }
  1080. if (isStyleSheet(currentTag, currentAttrs)) {
  1081. text = options.minifyCSS(text);
  1082. }
  1083. if (options.removeOptionalTags && text) {
  1084. // <html> may be omitted if first thing inside is not comment
  1085. // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
  1086. if (optionalStartTag === 'html' || optionalStartTag === 'body' && !/^\s/.test(text)) {
  1087. removeStartTag();
  1088. }
  1089. optionalStartTag = '';
  1090. // </html> or </body> may be omitted if not followed by comment
  1091. // </head>, </colgroup> or </caption> may be omitted if not followed by space or comment
  1092. if (compactTags(optionalEndTag) || looseTags(optionalEndTag) && !/^\s/.test(text)) {
  1093. removeEndTag();
  1094. }
  1095. optionalEndTag = '';
  1096. }
  1097. charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
  1098. if (options.decodeEntities && text && !specialContentTags(currentTag)) {
  1099. // Escape any `&` symbols that start either:
  1100. // 1) a legacy named character reference (i.e. one that doesn't end with `;`)
  1101. // 2) or any other character reference (i.e. one that does end with `;`)
  1102. // Note that `&` can be escaped as `&amp`, without the semi-colon.
  1103. // https://mathiasbynens.be/notes/ambiguous-ampersands
  1104. 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, '&amp$1').replace(/</g, '&lt;');
  1105. }
  1106. if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
  1107. text = text.replace(uidPattern, function(match, prefix, index) {
  1108. return ignoredCustomMarkupChunks[+index][0];
  1109. });
  1110. }
  1111. currentChars += text;
  1112. if (text) {
  1113. hasChars = true;
  1114. }
  1115. buffer.push(text);
  1116. },
  1117. comment: function(text, nonStandard) {
  1118. var prefix = nonStandard ? '<!' : '<!--';
  1119. var suffix = nonStandard ? '>' : '-->';
  1120. if (isConditionalComment(text)) {
  1121. text = prefix + cleanConditionalComment(text, options) + suffix;
  1122. }
  1123. else if (options.removeComments) {
  1124. if (isIgnoredComment(text, options)) {
  1125. text = '<!--' + text + '-->';
  1126. }
  1127. else {
  1128. text = '';
  1129. }
  1130. }
  1131. else {
  1132. text = prefix + text + suffix;
  1133. }
  1134. if (options.removeOptionalTags && text) {
  1135. // preceding comments suppress tag omissions
  1136. optionalStartTag = '';
  1137. optionalEndTag = '';
  1138. }
  1139. buffer.push(text);
  1140. },
  1141. doctype: function(doctype) {
  1142. buffer.push(options.useShortDoctype ? '<!doctype' +
  1143. (options.removeTagWhitespace ? '' : ' ') + 'html>' :
  1144. collapseWhitespaceAll(doctype));
  1145. },
  1146. customAttrAssign: options.customAttrAssign,
  1147. customAttrSurround: options.customAttrSurround
  1148. });
  1149. if (options.removeOptionalTags) {
  1150. // <html> may be omitted if first thing inside is not comment
  1151. // <head> or <body> may be omitted if empty
  1152. if (topLevelTags(optionalStartTag)) {
  1153. removeStartTag();
  1154. }
  1155. // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
  1156. if (optionalEndTag && !trailingTags(optionalEndTag)) {
  1157. removeEndTag();
  1158. }
  1159. }
  1160. if (options.collapseWhitespace) {
  1161. squashTrailingWhitespace('br');
  1162. }
  1163. return joinResultSegments(buffer, options, uidPattern ? function(str) {
  1164. return str.replace(uidPattern, function(match, prefix, index, suffix) {
  1165. var chunk = ignoredCustomMarkupChunks[+index][0];
  1166. if (options.collapseWhitespace) {
  1167. if (prefix !== '\t') {
  1168. chunk = prefix + chunk;
  1169. }
  1170. if (suffix !== '\t') {
  1171. chunk += suffix;
  1172. }
  1173. return collapseWhitespace(chunk, {
  1174. preserveLineBreaks: options.preserveLineBreaks,
  1175. conservativeCollapse: !options.trimCustomFragments
  1176. }, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
  1177. }
  1178. return chunk;
  1179. });
  1180. } : identity, uidIgnore ? function(str) {
  1181. return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function(match, index) {
  1182. return ignoredMarkupChunks[+index];
  1183. });
  1184. } : identity);
  1185. }
  1186. function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
  1187. var str;
  1188. var maxLineLength = options.maxLineLength;
  1189. if (maxLineLength) {
  1190. var line = '', lines = [];
  1191. while (results.length) {
  1192. var len = line.length;
  1193. var end = results[0].indexOf('\n');
  1194. if (end < 0) {
  1195. line += restoreIgnore(restoreCustom(results.shift()));
  1196. }
  1197. else {
  1198. line += restoreIgnore(restoreCustom(results[0].slice(0, end)));
  1199. results[0] = results[0].slice(end + 1);
  1200. }
  1201. if (len > 0 && line.length > maxLineLength) {
  1202. lines.push(line.slice(0, len));
  1203. line = line.slice(len);
  1204. }
  1205. else if (end >= 0) {
  1206. lines.push(line);
  1207. line = '';
  1208. }
  1209. }
  1210. if (line) {
  1211. lines.push(line);
  1212. }
  1213. str = lines.join('\n');
  1214. }
  1215. else {
  1216. str = restoreIgnore(restoreCustom(results.join('')));
  1217. }
  1218. return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
  1219. }
  1220. exports.minify = function(value, options) {
  1221. var start = Date.now();
  1222. options = processOptions(options || {});
  1223. var result = minify(value, options);
  1224. options.log('minified in: ' + (Date.now() - start) + 'ms');
  1225. return result;
  1226. };