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