html-minifier-next 1.0.0

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