htmlminifier.js 46 KB

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