html-minifier-next 1.3.3 → 1.4.1

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.
@@ -1,1878 +0,0 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var CleanCSS = require('clean-css');
6
- var entities = require('entities');
7
- var RelateURL = require('relateurl');
8
- var terser = require('terser');
9
-
10
- async function replaceAsync(str, regex, asyncFn) {
11
- const promises = [];
12
-
13
- str.replace(regex, (match, ...args) => {
14
- const promise = asyncFn(match, ...args);
15
- promises.push(promise);
16
- });
17
-
18
- const data = await Promise.all(promises);
19
- return str.replace(regex, () => data.shift());
20
- }
21
-
22
- /*!
23
- * HTML Parser By John Resig (ejohn.org)
24
- * Modified by Juriy "kangax" Zaytsev
25
- * Original code by Erik Arvidsson, Mozilla Public License
26
- * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
27
- */
28
-
29
-
30
- class CaseInsensitiveSet extends Set {
31
- has(str) {
32
- return super.has(str.toLowerCase());
33
- }
34
- }
35
-
36
- // Regular Expressions for parsing tags and attributes
37
- const singleAttrIdentifier = /([^\s"'<>/=]+)/;
38
- const singleAttrAssigns = [/=/];
39
- const singleAttrValues = [
40
- // attr value double quotes
41
- /"([^"]*)"+/.source,
42
- // attr value, single quotes
43
- /'([^']*)'+/.source,
44
- // attr value, no quotes
45
- /([^ \t\n\f\r"'`=<>]+)/.source
46
- ];
47
- // https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
48
- const qnameCapture = (function () {
49
- // based on https://www.npmjs.com/package/ncname
50
- const combiningChar = '\\u0300-\\u0345\\u0360\\u0361\\u0483-\\u0486\\u0591-\\u05A1\\u05A3-\\u05B9\\u05BB-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u064B-\\u0652\\u0670\\u06D6-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0901-\\u0903\\u093C\\u093E-\\u094D\\u0951-\\u0954\\u0962\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u0A02\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A70\\u0A71\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B43\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B82\\u0B83\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C01-\\u0C03\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C82\\u0C83\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D43\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86-\\u0F8B\\u0F90-\\u0F95\\u0F97\\u0F99-\\u0FAD\\u0FB1-\\u0FB7\\u0FB9\\u20D0-\\u20DC\\u20E1\\u302A-\\u302F\\u3099\\u309A';
51
- const digit = '0-9\\u0660-\\u0669\\u06F0-\\u06F9\\u0966-\\u096F\\u09E6-\\u09EF\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0BE7-\\u0BEF\\u0C66-\\u0C6F\\u0CE6-\\u0CEF\\u0D66-\\u0D6F\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F29';
52
- const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
53
- const letter = 'A-Za-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u0131\\u0134-\\u013E\\u0141-\\u0148\\u014A-\\u017E\\u0180-\\u01C3\\u01CD-\\u01F0\\u01F4\\u01F5\\u01FA-\\u0217\\u0250-\\u02A8\\u02BB-\\u02C1\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03CE\\u03D0-\\u03D6\\u03DA\\u03DC\\u03DE\\u03E0\\u03E2-\\u03F3\\u0401-\\u040C\\u040E-\\u044F\\u0451-\\u045C\\u045E-\\u0481\\u0490-\\u04C4\\u04C7\\u04C8\\u04CB\\u04CC\\u04D0-\\u04EB\\u04EE-\\u04F5\\u04F8\\u04F9\\u0531-\\u0556\\u0559\\u0561-\\u0586\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0621-\\u063A\\u0641-\\u064A\\u0671-\\u06B7\\u06BA-\\u06BE\\u06C0-\\u06CE\\u06D0-\\u06D3\\u06D5\\u06E5\\u06E6\\u0905-\\u0939\\u093D\\u0958-\\u0961\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8B\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AE0\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B36-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB5\\u0BB7-\\u0BB9\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CDE\\u0CE0\\u0CE1\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D28\\u0D2A-\\u0D39\\u0D60\\u0D61\\u0E01-\\u0E2E\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E45\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD\\u0EAE\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0F40-\\u0F47\\u0F49-\\u0F69\\u10A0-\\u10C5\\u10D0-\\u10F6\\u1100\\u1102\\u1103\\u1105-\\u1107\\u1109\\u110B\\u110C\\u110E-\\u1112\\u113C\\u113E\\u1140\\u114C\\u114E\\u1150\\u1154\\u1155\\u1159\\u115F-\\u1161\\u1163\\u1165\\u1167\\u1169\\u116D\\u116E\\u1172\\u1173\\u1175\\u119E\\u11A8\\u11AB\\u11AE\\u11AF\\u11B7\\u11B8\\u11BA\\u11BC-\\u11C2\\u11EB\\u11F0\\u11F9\\u1E00-\\u1E9B\\u1EA0-\\u1EF9\\u1F00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2126\\u212A\\u212B\\u212E\\u2180-\\u2182\\u3007\\u3021-\\u3029\\u3041-\\u3094\\u30A1-\\u30FA\\u3105-\\u312C\\u4E00-\\u9FA5\\uAC00-\\uD7A3';
54
- const ncname = '[' + letter + '_][' + letter + digit + '\\.\\-_' + combiningChar + extender + ']*';
55
- return '((?:' + ncname + '\\:)?' + ncname + ')';
56
- })();
57
- const startTagOpen = new RegExp('^<' + qnameCapture);
58
- const startTagClose = /^\s*(\/?)>/;
59
- const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
60
- const doctype = /^<!DOCTYPE\s?[^>]+>/i;
61
-
62
- let IS_REGEX_CAPTURING_BROKEN = false;
63
- 'x'.replace(/x(.)?/g, function (m, g) {
64
- IS_REGEX_CAPTURING_BROKEN = g === '';
65
- });
66
-
67
- // Empty Elements
68
- const empty = new CaseInsensitiveSet(['area', 'base', 'basefont', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
69
-
70
- // Inline Elements
71
- const inline = new CaseInsensitiveSet(['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'noscript', 'object', 'q', 's', 'samp', 'script', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'tt', 'u', 'var']);
72
-
73
- // Elements that you can, intentionally, leave open
74
- // (and which close themselves)
75
- const closeSelf = new CaseInsensitiveSet(['colgroup', 'dd', 'dt', 'li', 'option', 'p', 'td', 'tfoot', 'th', 'thead', 'tr', 'source']);
76
-
77
- // Attributes that have their values filled in disabled='disabled'
78
- const fillAttrs = new CaseInsensitiveSet(['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected']);
79
-
80
- // Special Elements (can contain anything)
81
- const special = new CaseInsensitiveSet(['script', 'style']);
82
-
83
- // HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
84
- // Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
85
- const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base', 'blockquote', 'body', 'caption', 'col', 'colgroup', 'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'legend', 'li', 'menuitem', 'meta', 'ol', 'optgroup', 'option', 'param', 'rp', 'rt', 'source', 'style', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul']);
86
-
87
- const reCache = {};
88
-
89
- function attrForHandler(handler) {
90
- let pattern = singleAttrIdentifier.source +
91
- '(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
92
- '[ \\t\\n\\f\\r]*(?:' + singleAttrValues.join('|') + '))?';
93
- if (handler.customAttrSurround) {
94
- const attrClauses = [];
95
- for (let i = handler.customAttrSurround.length - 1; i >= 0; i--) {
96
- attrClauses[i] = '(?:' +
97
- '(' + handler.customAttrSurround[i][0].source + ')\\s*' +
98
- pattern +
99
- '\\s*(' + handler.customAttrSurround[i][1].source + ')' +
100
- ')';
101
- }
102
- attrClauses.push('(?:' + pattern + ')');
103
- pattern = '(?:' + attrClauses.join('|') + ')';
104
- }
105
- return new RegExp('^\\s*' + pattern);
106
- }
107
-
108
- function joinSingleAttrAssigns(handler) {
109
- return singleAttrAssigns.concat(
110
- handler.customAttrAssign || []
111
- ).map(function (assign) {
112
- return '(?:' + assign.source + ')';
113
- }).join('|');
114
- }
115
-
116
- class HTMLParser {
117
- constructor(html, handler) {
118
- this.html = html;
119
- this.handler = handler;
120
- }
121
-
122
- async parse() {
123
- let html = this.html;
124
- const handler = this.handler;
125
-
126
- const stack = []; let lastTag;
127
- const attribute = attrForHandler(handler);
128
- let last, prevTag, nextTag;
129
- while (html) {
130
- last = html;
131
- // Make sure we're not in a script or style element
132
- if (!lastTag || !special.has(lastTag)) {
133
- let textEnd = html.indexOf('<');
134
- if (textEnd === 0) {
135
- // Comment:
136
- if (/^<!--/.test(html)) {
137
- const commentEnd = html.indexOf('-->');
138
-
139
- if (commentEnd >= 0) {
140
- if (handler.comment) {
141
- await handler.comment(html.substring(4, commentEnd));
142
- }
143
- html = html.substring(commentEnd + 3);
144
- prevTag = '';
145
- continue;
146
- }
147
- }
148
-
149
- // https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
150
- if (/^<!\[/.test(html)) {
151
- const conditionalEnd = html.indexOf(']>');
152
-
153
- if (conditionalEnd >= 0) {
154
- if (handler.comment) {
155
- await handler.comment(html.substring(2, conditionalEnd + 1), true /* non-standard */);
156
- }
157
- html = html.substring(conditionalEnd + 2);
158
- prevTag = '';
159
- continue;
160
- }
161
- }
162
-
163
- // Doctype:
164
- const doctypeMatch = html.match(doctype);
165
- if (doctypeMatch) {
166
- if (handler.doctype) {
167
- handler.doctype(doctypeMatch[0]);
168
- }
169
- html = html.substring(doctypeMatch[0].length);
170
- prevTag = '';
171
- continue;
172
- }
173
-
174
- // End tag:
175
- const endTagMatch = html.match(endTag);
176
- if (endTagMatch) {
177
- html = html.substring(endTagMatch[0].length);
178
- await replaceAsync(endTagMatch[0], endTag, parseEndTag);
179
- prevTag = '/' + endTagMatch[1].toLowerCase();
180
- continue;
181
- }
182
-
183
- // Start tag:
184
- const startTagMatch = parseStartTag(html);
185
- if (startTagMatch) {
186
- html = startTagMatch.rest;
187
- await handleStartTag(startTagMatch);
188
- prevTag = startTagMatch.tagName.toLowerCase();
189
- continue;
190
- }
191
-
192
- // Treat `<` as text
193
- if (handler.continueOnParseError) {
194
- textEnd = html.indexOf('<', 1);
195
- }
196
- }
197
-
198
- let text;
199
- if (textEnd >= 0) {
200
- text = html.substring(0, textEnd);
201
- html = html.substring(textEnd);
202
- } else {
203
- text = html;
204
- html = '';
205
- }
206
-
207
- // next tag
208
- let nextTagMatch = parseStartTag(html);
209
- if (nextTagMatch) {
210
- nextTag = nextTagMatch.tagName;
211
- } else {
212
- nextTagMatch = html.match(endTag);
213
- if (nextTagMatch) {
214
- nextTag = '/' + nextTagMatch[1];
215
- } else {
216
- nextTag = '';
217
- }
218
- }
219
-
220
- if (handler.chars) {
221
- await handler.chars(text, prevTag, nextTag);
222
- }
223
- prevTag = '';
224
- } else {
225
- const stackedTag = lastTag.toLowerCase();
226
- const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'));
227
-
228
- html = await replaceAsync(html, reStackedTag, async (_, text) => {
229
- if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
230
- text = text
231
- .replace(/<!--([\s\S]*?)-->/g, '$1')
232
- .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
233
- }
234
-
235
- if (handler.chars) {
236
- await handler.chars(text);
237
- }
238
-
239
- return '';
240
- });
241
-
242
- await parseEndTag('</' + stackedTag + '>', stackedTag);
243
- }
244
-
245
- if (html === last) {
246
- throw new Error('Parse Error: ' + html);
247
- }
248
- }
249
-
250
- if (!handler.partialMarkup) {
251
- // Clean up any remaining tags
252
- await parseEndTag();
253
- }
254
-
255
- function parseStartTag(input) {
256
- const start = input.match(startTagOpen);
257
- if (start) {
258
- const match = {
259
- tagName: start[1],
260
- attrs: []
261
- };
262
- input = input.slice(start[0].length);
263
- let end, attr;
264
- while (!(end = input.match(startTagClose)) && (attr = input.match(attribute))) {
265
- input = input.slice(attr[0].length);
266
- match.attrs.push(attr);
267
- }
268
- if (end) {
269
- match.unarySlash = end[1];
270
- match.rest = input.slice(end[0].length);
271
- return match;
272
- }
273
- }
274
- }
275
-
276
- async function closeIfFound(tagName) {
277
- if (findTag(tagName) >= 0) {
278
- await parseEndTag('', tagName);
279
- return true;
280
- }
281
- }
282
-
283
- async function handleStartTag(match) {
284
- const tagName = match.tagName;
285
- let unarySlash = match.unarySlash;
286
-
287
- if (handler.html5) {
288
- if (lastTag === 'p' && nonPhrasing.has(tagName)) {
289
- await parseEndTag('', lastTag);
290
- } else if (tagName === 'tbody') {
291
- await closeIfFound('thead');
292
- } else if (tagName === 'tfoot') {
293
- if (!await closeIfFound('tbody')) {
294
- await closeIfFound('thead');
295
- }
296
- }
297
- if (tagName === 'col' && findTag('colgroup') < 0) {
298
- lastTag = 'colgroup';
299
- stack.push({ tag: lastTag, attrs: [] });
300
- if (handler.start) {
301
- await handler.start(lastTag, [], false, '');
302
- }
303
- }
304
- }
305
-
306
- if (!handler.html5 && !inline.has(tagName)) {
307
- while (lastTag && inline.has(lastTag)) {
308
- await parseEndTag('', lastTag);
309
- }
310
- }
311
-
312
- if (closeSelf.has(tagName) && lastTag === tagName) {
313
- await parseEndTag('', tagName);
314
- }
315
-
316
- const unary = empty.has(tagName) || (tagName === 'html' && lastTag === 'head') || !!unarySlash;
317
-
318
- const attrs = match.attrs.map(function (args) {
319
- let name, value, customOpen, customClose, customAssign, quote;
320
- const ncp = 7; // number of captured parts, scalar
321
-
322
- // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
323
- if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
324
- if (args[3] === '') { delete args[3]; }
325
- if (args[4] === '') { delete args[4]; }
326
- if (args[5] === '') { delete args[5]; }
327
- }
328
-
329
- function populate(index) {
330
- customAssign = args[index];
331
- value = args[index + 1];
332
- if (typeof value !== 'undefined') {
333
- return '"';
334
- }
335
- value = args[index + 2];
336
- if (typeof value !== 'undefined') {
337
- return '\'';
338
- }
339
- value = args[index + 3];
340
- if (typeof value === 'undefined' && fillAttrs.has(name)) {
341
- value = name;
342
- }
343
- return '';
344
- }
345
-
346
- let j = 1;
347
- if (handler.customAttrSurround) {
348
- for (let i = 0, l = handler.customAttrSurround.length; i < l; i++, j += ncp) {
349
- name = args[j + 1];
350
- if (name) {
351
- quote = populate(j + 2);
352
- customOpen = args[j];
353
- customClose = args[j + 6];
354
- break;
355
- }
356
- }
357
- }
358
-
359
- if (!name && (name = args[j])) {
360
- quote = populate(j + 1);
361
- }
362
-
363
- return {
364
- name,
365
- value,
366
- customAssign: customAssign || '=',
367
- customOpen: customOpen || '',
368
- customClose: customClose || '',
369
- quote: quote || ''
370
- };
371
- });
372
-
373
- if (!unary) {
374
- stack.push({ tag: tagName, attrs });
375
- lastTag = tagName;
376
- unarySlash = '';
377
- }
378
-
379
- if (handler.start) {
380
- await handler.start(tagName, attrs, unary, unarySlash);
381
- }
382
- }
383
-
384
- function findTag(tagName) {
385
- let pos;
386
- const needle = tagName.toLowerCase();
387
- for (pos = stack.length - 1; pos >= 0; pos--) {
388
- if (stack[pos].tag.toLowerCase() === needle) {
389
- break;
390
- }
391
- }
392
- return pos;
393
- }
394
-
395
- async function parseEndTag(tag, tagName) {
396
- let pos;
397
-
398
- // Find the closest opened tag of the same type
399
- if (tagName) {
400
- pos = findTag(tagName);
401
- } else { // If no tag name is provided, clean shop
402
- pos = 0;
403
- }
404
-
405
- if (pos >= 0) {
406
- // Close all the open elements, up the stack
407
- for (let i = stack.length - 1; i >= pos; i--) {
408
- if (handler.end) {
409
- handler.end(stack[i].tag, stack[i].attrs, i > pos || !tag);
410
- }
411
- }
412
-
413
- // Remove the open elements from the stack
414
- stack.length = pos;
415
- lastTag = pos && stack[pos - 1].tag;
416
- } else if (tagName.toLowerCase() === 'br') {
417
- if (handler.start) {
418
- await handler.start(tagName, [], true, '');
419
- }
420
- } else if (tagName.toLowerCase() === 'p') {
421
- if (handler.start) {
422
- await handler.start(tagName, [], false, '', true);
423
- }
424
- if (handler.end) {
425
- handler.end(tagName, []);
426
- }
427
- }
428
- }
429
- }
430
- }
431
-
432
- class Sorter {
433
- sort(tokens, fromIndex = 0) {
434
- for (let i = 0, len = this.keys.length; i < len; i++) {
435
- const key = this.keys[i];
436
- const token = key.slice(1);
437
-
438
- let index = tokens.indexOf(token, fromIndex);
439
-
440
- if (index !== -1) {
441
- do {
442
- if (index !== fromIndex) {
443
- tokens.splice(index, 1);
444
- tokens.splice(fromIndex, 0, token);
445
- }
446
- fromIndex++;
447
- } while ((index = tokens.indexOf(token, fromIndex)) !== -1);
448
-
449
- return this[key].sort(tokens, fromIndex);
450
- }
451
- }
452
- return tokens;
453
- }
454
- }
455
-
456
- class TokenChain {
457
- add(tokens) {
458
- tokens.forEach((token) => {
459
- const key = '$' + token;
460
- if (!this[key]) {
461
- this[key] = [];
462
- this[key].processed = 0;
463
- }
464
- this[key].push(tokens);
465
- });
466
- }
467
-
468
- createSorter() {
469
- const sorter = new Sorter();
470
-
471
- sorter.keys = Object.keys(this).sort((j, k) => {
472
- const m = this[j].length;
473
- const n = this[k].length;
474
- return m < n ? 1 : m > n ? -1 : j < k ? -1 : j > k ? 1 : 0;
475
- }).filter((key) => {
476
- if (this[key].processed < this[key].length) {
477
- const token = key.slice(1);
478
- const chain = new TokenChain();
479
-
480
- this[key].forEach((tokens) => {
481
- let index;
482
- while ((index = tokens.indexOf(token)) !== -1) {
483
- tokens.splice(index, 1);
484
- }
485
- tokens.forEach((token) => {
486
- this['$' + token].processed++;
487
- });
488
- chain.add(tokens.slice(0));
489
- });
490
- sorter[key] = chain.createSorter();
491
- return true;
492
- }
493
- return false;
494
- });
495
- return sorter;
496
- }
497
- }
498
-
499
- const trimWhitespace = str => str && str.replace(/^[ \n\r\t\f]+/, '').replace(/[ \n\r\t\f]+$/, '');
500
-
501
- function collapseWhitespaceAll(str) {
502
- // Non-breaking space is specifically handled inside the replacer function here:
503
- return str && str.replace(/[ \n\r\t\f\xA0]+/g, function (spaces) {
504
- return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 ');
505
- });
506
- }
507
-
508
- function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
509
- let lineBreakBefore = ''; let lineBreakAfter = '';
510
-
511
- if (options.preserveLineBreaks) {
512
- str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
513
- lineBreakBefore = '\n';
514
- return '';
515
- }).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function () {
516
- lineBreakAfter = '\n';
517
- return '';
518
- });
519
- }
520
-
521
- if (trimLeft) {
522
- // Non-breaking space is specifically handled inside the replacer function here:
523
- str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
524
- const conservative = !lineBreakBefore && options.conservativeCollapse;
525
- if (conservative && spaces === '\t') {
526
- return '\t';
527
- }
528
- return spaces.replace(/^[^\xA0]+/, '').replace(/(\xA0+)[^\xA0]+/g, '$1 ') || (conservative ? ' ' : '');
529
- });
530
- }
531
-
532
- if (trimRight) {
533
- // Non-breaking space is specifically handled inside the replacer function here:
534
- str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
535
- const conservative = !lineBreakAfter && options.conservativeCollapse;
536
- if (conservative && spaces === '\t') {
537
- return '\t';
538
- }
539
- return spaces.replace(/[^\xA0]+(\xA0+)/g, ' $1').replace(/[^\xA0]+$/, '') || (conservative ? ' ' : '');
540
- });
541
- }
542
-
543
- if (collapseAll) {
544
- // strip non space whitespace then compress spaces to one
545
- str = collapseWhitespaceAll(str);
546
- }
547
-
548
- return lineBreakBefore + str + lineBreakAfter;
549
- }
550
-
551
- // non-empty elements that will maintain whitespace around them
552
- const inlineHtmlElements = ['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr'];
553
- // non-empty elements that will maintain whitespace within them
554
- 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']);
555
- // self-closing elements that will maintain whitespace around them
556
- const selfClosingInlineTags = new Set(['comment', 'img', 'input', 'wbr']);
557
-
558
- function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineTags) {
559
- let trimLeft = prevTag && !selfClosingInlineTags.has(prevTag);
560
- if (trimLeft && !options.collapseInlineTagWhitespace) {
561
- trimLeft = prevTag.charAt(0) === '/' ? !inlineTags.has(prevTag.slice(1)) : !inlineTextTags.has(prevTag);
562
- }
563
- let trimRight = nextTag && !selfClosingInlineTags.has(nextTag);
564
- if (trimRight && !options.collapseInlineTagWhitespace) {
565
- trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags.has(nextTag.slice(1)) : !inlineTags.has(nextTag);
566
- }
567
- return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
568
- }
569
-
570
- function isConditionalComment(text) {
571
- return /^\[if\s[^\]]+]|\[endif]$/.test(text);
572
- }
573
-
574
- function isIgnoredComment(text, options) {
575
- for (let i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
576
- if (options.ignoreCustomComments[i].test(text)) {
577
- return true;
578
- }
579
- }
580
- return false;
581
- }
582
-
583
- function isEventAttribute(attrName, options) {
584
- const patterns = options.customEventAttributes;
585
- if (patterns) {
586
- for (let i = patterns.length; i--;) {
587
- if (patterns[i].test(attrName)) {
588
- return true;
589
- }
590
- }
591
- return false;
592
- }
593
- return /^on[a-z]{3,}$/.test(attrName);
594
- }
595
-
596
- function canRemoveAttributeQuotes(value) {
597
- // https://mathiasbynens.be/notes/unquoted-attribute-values
598
- return /^[^ \t\n\f\r"'`=<>]+$/.test(value);
599
- }
600
-
601
- function attributesInclude(attributes, attribute) {
602
- for (let i = attributes.length; i--;) {
603
- if (attributes[i].name.toLowerCase() === attribute) {
604
- return true;
605
- }
606
- }
607
- return false;
608
- }
609
-
610
- function isAttributeRedundant(tag, attrName, attrValue, attrs) {
611
- attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
612
-
613
- return (
614
- (tag === 'script' &&
615
- attrName === 'language' &&
616
- attrValue === 'javascript') ||
617
-
618
- (tag === 'form' &&
619
- attrName === 'method' &&
620
- attrValue === 'get') ||
621
-
622
- (tag === 'input' &&
623
- attrName === 'type' &&
624
- attrValue === 'text') ||
625
-
626
- (tag === 'script' &&
627
- attrName === 'charset' &&
628
- !attributesInclude(attrs, 'src')) ||
629
-
630
- (tag === 'a' &&
631
- attrName === 'name' &&
632
- attributesInclude(attrs, 'id')) ||
633
-
634
- (tag === 'area' &&
635
- attrName === 'shape' &&
636
- attrValue === 'rect')
637
- );
638
- }
639
-
640
- // https://mathiasbynens.be/demo/javascript-mime-type
641
- // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
642
- const executableScriptsMimetypes = new Set([
643
- 'text/javascript',
644
- 'text/ecmascript',
645
- 'text/jscript',
646
- 'application/javascript',
647
- 'application/x-javascript',
648
- 'application/ecmascript',
649
- 'module'
650
- ]);
651
-
652
- const keepScriptsMimetypes = new Set([
653
- 'module'
654
- ]);
655
-
656
- function isScriptTypeAttribute(attrValue = '') {
657
- attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
658
- return attrValue === '' || executableScriptsMimetypes.has(attrValue);
659
- }
660
-
661
- function keepScriptTypeAttribute(attrValue = '') {
662
- attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
663
- return keepScriptsMimetypes.has(attrValue);
664
- }
665
-
666
- function isExecutableScript(tag, attrs) {
667
- if (tag !== 'script') {
668
- return false;
669
- }
670
- for (let i = 0, len = attrs.length; i < len; i++) {
671
- const attrName = attrs[i].name.toLowerCase();
672
- if (attrName === 'type') {
673
- return isScriptTypeAttribute(attrs[i].value);
674
- }
675
- }
676
- return true;
677
- }
678
-
679
- function isStyleLinkTypeAttribute(attrValue = '') {
680
- attrValue = trimWhitespace(attrValue).toLowerCase();
681
- return attrValue === '' || attrValue === 'text/css';
682
- }
683
-
684
- function isStyleSheet(tag, attrs) {
685
- if (tag !== 'style') {
686
- return false;
687
- }
688
- for (let i = 0, len = attrs.length; i < len; i++) {
689
- const attrName = attrs[i].name.toLowerCase();
690
- if (attrName === 'type') {
691
- return isStyleLinkTypeAttribute(attrs[i].value);
692
- }
693
- }
694
- return true;
695
- }
696
-
697
- 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']);
698
- const isBooleanValue = new Set(['true', 'false']);
699
-
700
- function isBooleanAttribute(attrName, attrValue) {
701
- return isSimpleBoolean.has(attrName) || (attrName === 'draggable' && !isBooleanValue.has(attrValue));
702
- }
703
-
704
- function isUriTypeAttribute(attrName, tag) {
705
- return (
706
- (/^(?:a|area|link|base)$/.test(tag) && attrName === 'href') ||
707
- (tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
708
- (tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
709
- (tag === 'q' && attrName === 'cite') ||
710
- (tag === 'blockquote' && attrName === 'cite') ||
711
- ((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
712
- (tag === 'form' && attrName === 'action') ||
713
- (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
714
- (tag === 'head' && attrName === 'profile') ||
715
- (tag === 'script' && (attrName === 'src' || attrName === 'for'))
716
- );
717
- }
718
-
719
- function isNumberTypeAttribute(attrName, tag) {
720
- return (
721
- (/^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex') ||
722
- (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
723
- (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
724
- (tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
725
- (tag === 'colgroup' && attrName === 'span') ||
726
- (tag === 'col' && attrName === 'span') ||
727
- ((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
728
- );
729
- }
730
-
731
- function isLinkType(tag, attrs, value) {
732
- if (tag !== 'link') {
733
- return false;
734
- }
735
- for (let i = 0, len = attrs.length; i < len; i++) {
736
- if (attrs[i].name === 'rel' && attrs[i].value === value) {
737
- return true;
738
- }
739
- }
740
- }
741
-
742
- function isMediaQuery(tag, attrs, attrName) {
743
- return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
744
- }
745
-
746
- const srcsetTags = new Set(['img', 'source']);
747
-
748
- function isSrcset(attrName, tag) {
749
- return attrName === 'srcset' && srcsetTags.has(tag);
750
- }
751
-
752
- async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
753
- if (isEventAttribute(attrName, options)) {
754
- attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
755
- return options.minifyJS(attrValue, true);
756
- } else if (attrName === 'class') {
757
- attrValue = trimWhitespace(attrValue);
758
- if (options.sortClassName) {
759
- attrValue = options.sortClassName(attrValue);
760
- } else {
761
- attrValue = collapseWhitespaceAll(attrValue);
762
- }
763
- return attrValue;
764
- } else if (isUriTypeAttribute(attrName, tag)) {
765
- attrValue = trimWhitespace(attrValue);
766
- return isLinkType(tag, attrs, 'canonical') ? attrValue : options.minifyURLs(attrValue);
767
- } else if (isNumberTypeAttribute(attrName, tag)) {
768
- return trimWhitespace(attrValue);
769
- } else if (attrName === 'style') {
770
- attrValue = trimWhitespace(attrValue);
771
- if (attrValue) {
772
- if (/;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
773
- attrValue = attrValue.replace(/\s*;$/, ';');
774
- }
775
- attrValue = await options.minifyCSS(attrValue, 'inline');
776
- }
777
- return attrValue;
778
- } else if (isSrcset(attrName, tag)) {
779
- // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
780
- attrValue = trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(function (candidate) {
781
- let url = candidate;
782
- let descriptor = '';
783
- const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
784
- if (match) {
785
- url = url.slice(0, -match[0].length);
786
- const num = +match[1].slice(0, -1);
787
- const suffix = match[1].slice(-1);
788
- if (num !== 1 || suffix !== 'x') {
789
- descriptor = ' ' + num + suffix;
790
- }
791
- }
792
- return options.minifyURLs(url) + descriptor;
793
- }).join(', ');
794
- } else if (isMetaViewport(tag, attrs) && attrName === 'content') {
795
- attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
796
- // "0.90000" -> "0.9"
797
- // "1.0" -> "1"
798
- // "1.0001" -> "1.0001" (unchanged)
799
- return (+numString).toString();
800
- });
801
- } else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
802
- return collapseWhitespaceAll(attrValue);
803
- } else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
804
- attrValue = trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
805
- } else if (tag === 'script' && attrName === 'type') {
806
- attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
807
- } else if (isMediaQuery(tag, attrs, attrName)) {
808
- attrValue = trimWhitespace(attrValue);
809
- return options.minifyCSS(attrValue, 'media');
810
- }
811
- return attrValue;
812
- }
813
-
814
- function isMetaViewport(tag, attrs) {
815
- if (tag !== 'meta') {
816
- return false;
817
- }
818
- for (let i = 0, len = attrs.length; i < len; i++) {
819
- if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
820
- return true;
821
- }
822
- }
823
- }
824
-
825
- function isContentSecurityPolicy(tag, attrs) {
826
- if (tag !== 'meta') {
827
- return false;
828
- }
829
- for (let i = 0, len = attrs.length; i < len; i++) {
830
- if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
831
- return true;
832
- }
833
- }
834
- }
835
-
836
- function ignoreCSS(id) {
837
- return '/* clean-css ignore:start */' + id + '/* clean-css ignore:end */';
838
- }
839
-
840
- // Wrap CSS declarations for CleanCSS > 3.x
841
- // See https://github.com/jakubpawlowicz/clean-css/issues/418
842
- function wrapCSS(text, type) {
843
- switch (type) {
844
- case 'inline':
845
- return '*{' + text + '}';
846
- case 'media':
847
- return '@media ' + text + '{a{top:0}}';
848
- default:
849
- return text;
850
- }
851
- }
852
-
853
- function unwrapCSS(text, type) {
854
- let matches;
855
- switch (type) {
856
- case 'inline':
857
- matches = text.match(/^\*\{([\s\S]*)\}$/);
858
- break;
859
- case 'media':
860
- matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
861
- break;
862
- }
863
- return matches ? matches[1] : text;
864
- }
865
-
866
- async function cleanConditionalComment(comment, options) {
867
- return options.processConditionalComments
868
- ? await replaceAsync(comment, /^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, async function (match, prefix, text, suffix) {
869
- return prefix + await minifyHTML(text, options, true) + suffix;
870
- })
871
- : comment;
872
- }
873
-
874
- async function processScript(text, options, currentAttrs) {
875
- for (let i = 0, len = currentAttrs.length; i < len; i++) {
876
- if (currentAttrs[i].name.toLowerCase() === 'type' &&
877
- options.processScripts.indexOf(currentAttrs[i].value) > -1) {
878
- return await minifyHTML(text, options);
879
- }
880
- }
881
- return text;
882
- }
883
-
884
- // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
885
- // with the following deviations:
886
- // - retain <body> if followed by <noscript>
887
- // - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
888
- // - retain all tags which are adjacent to non-standard HTML tags
889
- const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
890
- 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']);
891
- const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
892
- const descriptionTags = new Set(['dt', 'dd']);
893
- 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']);
894
- const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
895
- const rubyTags = new Set(['rb', 'rt', 'rtc', 'rp']);
896
- const rtcTag = new Set(['rb', 'rtc', 'rp']);
897
- const optionTag = new Set(['option', 'optgroup']);
898
- const tableContentTags = new Set(['tbody', 'tfoot']);
899
- const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
900
- const cellTags = new Set(['td', 'th']);
901
- const topLevelTags = new Set(['html', 'head', 'body']);
902
- const compactTags = new Set(['html', 'body']);
903
- const looseTags = new Set(['head', 'colgroup', 'caption']);
904
- const trailingTags = new Set(['dt', 'thead']);
905
- 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']);
906
-
907
- function canRemoveParentTag(optionalStartTag, tag) {
908
- switch (optionalStartTag) {
909
- case 'html':
910
- case 'head':
911
- return true;
912
- case 'body':
913
- return !headerTags.has(tag);
914
- case 'colgroup':
915
- return tag === 'col';
916
- case 'tbody':
917
- return tag === 'tr';
918
- }
919
- return false;
920
- }
921
-
922
- function isStartTagMandatory(optionalEndTag, tag) {
923
- switch (tag) {
924
- case 'colgroup':
925
- return optionalEndTag === 'colgroup';
926
- case 'tbody':
927
- return tableSectionTags.has(optionalEndTag);
928
- }
929
- return false;
930
- }
931
-
932
- function canRemovePrecedingTag(optionalEndTag, tag) {
933
- switch (optionalEndTag) {
934
- case 'html':
935
- case 'head':
936
- case 'body':
937
- case 'colgroup':
938
- case 'caption':
939
- return true;
940
- case 'li':
941
- case 'optgroup':
942
- case 'tr':
943
- return tag === optionalEndTag;
944
- case 'dt':
945
- case 'dd':
946
- return descriptionTags.has(tag);
947
- case 'p':
948
- return pBlockTags.has(tag);
949
- case 'rb':
950
- case 'rt':
951
- case 'rp':
952
- return rubyTags.has(tag);
953
- case 'rtc':
954
- return rtcTag.has(tag);
955
- case 'option':
956
- return optionTag.has(tag);
957
- case 'thead':
958
- case 'tbody':
959
- return tableContentTags.has(tag);
960
- case 'tfoot':
961
- return tag === 'tbody';
962
- case 'td':
963
- case 'th':
964
- return cellTags.has(tag);
965
- }
966
- return false;
967
- }
968
-
969
- const reEmptyAttribute = new RegExp(
970
- '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
971
- '?:down|up|over|move|out)|key(?:press|down|up)))$');
972
-
973
- function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
974
- const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
975
- if (!isValueEmpty) {
976
- return false;
977
- }
978
- if (typeof options.removeEmptyAttributes === 'function') {
979
- return options.removeEmptyAttributes(attrName, tag);
980
- }
981
- return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
982
- }
983
-
984
- function hasAttrName(name, attrs) {
985
- for (let i = attrs.length - 1; i >= 0; i--) {
986
- if (attrs[i].name === name) {
987
- return true;
988
- }
989
- }
990
- return false;
991
- }
992
-
993
- function canRemoveElement(tag, attrs) {
994
- switch (tag) {
995
- case 'textarea':
996
- return false;
997
- case 'audio':
998
- case 'script':
999
- case 'video':
1000
- if (hasAttrName('src', attrs)) {
1001
- return false;
1002
- }
1003
- break;
1004
- case 'iframe':
1005
- if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
1006
- return false;
1007
- }
1008
- break;
1009
- case 'object':
1010
- if (hasAttrName('data', attrs)) {
1011
- return false;
1012
- }
1013
- break;
1014
- case 'applet':
1015
- if (hasAttrName('code', attrs)) {
1016
- return false;
1017
- }
1018
- break;
1019
- }
1020
- return true;
1021
- }
1022
-
1023
- function canCollapseWhitespace(tag) {
1024
- return !/^(?:script|style|pre|textarea)$/.test(tag);
1025
- }
1026
-
1027
- function canTrimWhitespace(tag) {
1028
- return !/^(?:pre|textarea)$/.test(tag);
1029
- }
1030
-
1031
- async function normalizeAttr(attr, attrs, tag, options) {
1032
- const attrName = options.name(attr.name);
1033
- let attrValue = attr.value;
1034
-
1035
- if (options.decodeEntities && attrValue) {
1036
- attrValue = entities.decodeHTMLStrict(attrValue);
1037
- }
1038
-
1039
- if ((options.removeRedundantAttributes &&
1040
- isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
1041
- (options.removeScriptTypeAttributes && tag === 'script' &&
1042
- attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
1043
- (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
1044
- attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
1045
- return;
1046
- }
1047
-
1048
- if (attrValue) {
1049
- attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs);
1050
- }
1051
-
1052
- if (options.removeEmptyAttributes &&
1053
- canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
1054
- return;
1055
- }
1056
-
1057
- if (options.decodeEntities && attrValue) {
1058
- attrValue = attrValue.replace(/&(#?[0-9a-zA-Z]+;)/g, '&amp;$1');
1059
- }
1060
-
1061
- return {
1062
- attr,
1063
- name: attrName,
1064
- value: attrValue
1065
- };
1066
- }
1067
-
1068
- function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
1069
- const attrName = normalized.name;
1070
- let attrValue = normalized.value;
1071
- const attr = normalized.attr;
1072
- let attrQuote = attr.quote;
1073
- let attrFragment;
1074
- let emittedAttrValue;
1075
-
1076
- if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
1077
- ~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) {
1078
- if (!options.preventAttributesEscaping) {
1079
- if (typeof options.quoteCharacter === 'undefined') {
1080
- const apos = (attrValue.match(/'/g) || []).length;
1081
- const quot = (attrValue.match(/"/g) || []).length;
1082
- attrQuote = apos < quot ? '\'' : '"';
1083
- } else {
1084
- attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
1085
- }
1086
- if (attrQuote === '"') {
1087
- attrValue = attrValue.replace(/"/g, '&#34;');
1088
- } else {
1089
- attrValue = attrValue.replace(/'/g, '&#39;');
1090
- }
1091
- }
1092
- emittedAttrValue = attrQuote + attrValue + attrQuote;
1093
- if (!isLast && !options.removeTagWhitespace) {
1094
- emittedAttrValue += ' ';
1095
- }
1096
- } else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
1097
- // make sure trailing slash is not interpreted as HTML self-closing tag
1098
- emittedAttrValue = attrValue;
1099
- } else {
1100
- emittedAttrValue = attrValue + ' ';
1101
- }
1102
-
1103
- if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
1104
- isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase()))) {
1105
- attrFragment = attrName;
1106
- if (!isLast) {
1107
- attrFragment += ' ';
1108
- }
1109
- } else {
1110
- attrFragment = attrName + attr.customAssign + emittedAttrValue;
1111
- }
1112
-
1113
- return attr.customOpen + attrFragment + attr.customClose;
1114
- }
1115
-
1116
- function identity(value) {
1117
- return value;
1118
- }
1119
-
1120
- function identityAsync(value) {
1121
- return Promise.resolve(value);
1122
- }
1123
-
1124
- const processOptions = (inputOptions) => {
1125
- const options = {
1126
- name: function (name) {
1127
- return name.toLowerCase();
1128
- },
1129
- canCollapseWhitespace,
1130
- canTrimWhitespace,
1131
- html5: true,
1132
- ignoreCustomComments: [
1133
- /^!/,
1134
- /^\s*#/
1135
- ],
1136
- ignoreCustomFragments: [
1137
- /<%[\s\S]*?%>/,
1138
- /<\?[\s\S]*?\?>/
1139
- ],
1140
- includeAutoGeneratedTags: true,
1141
- log: identity,
1142
- minifyCSS: identityAsync,
1143
- minifyJS: identity,
1144
- minifyURLs: identity
1145
- };
1146
-
1147
- Object.keys(inputOptions).forEach(function (key) {
1148
- const option = inputOptions[key];
1149
-
1150
- if (key === 'caseSensitive') {
1151
- if (option) {
1152
- options.name = identity;
1153
- }
1154
- } else if (key === 'log') {
1155
- if (typeof option === 'function') {
1156
- options.log = option;
1157
- }
1158
- } else if (key === 'minifyCSS' && typeof option !== 'function') {
1159
- if (!option) {
1160
- return;
1161
- }
1162
-
1163
- const cleanCssOptions = typeof option === 'object' ? option : {};
1164
-
1165
- options.minifyCSS = async function (text, type) {
1166
- text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function (match, prefix, quote, url, suffix) {
1167
- return prefix + quote + options.minifyURLs(url) + quote + suffix;
1168
- });
1169
-
1170
- const inputCSS = wrapCSS(text, type);
1171
-
1172
- return new Promise((resolve) => {
1173
- new CleanCSS(cleanCssOptions).minify(inputCSS, (_err, output) => {
1174
- if (output.errors.length > 0) {
1175
- output.errors.forEach(options.log);
1176
- resolve(text);
1177
- }
1178
-
1179
- const outputCSS = unwrapCSS(output.styles, type);
1180
- resolve(outputCSS);
1181
- });
1182
- });
1183
- };
1184
- } else if (key === 'minifyJS' && typeof option !== 'function') {
1185
- if (!option) {
1186
- return;
1187
- }
1188
-
1189
- const terserOptions = typeof option === 'object' ? option : {};
1190
-
1191
- terserOptions.parse = {
1192
- ...terserOptions.parse,
1193
- bare_returns: false
1194
- };
1195
-
1196
- options.minifyJS = async function (text, inline) {
1197
- const start = text.match(/^\s*<!--.*/);
1198
- const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
1199
-
1200
- terserOptions.parse.bare_returns = inline;
1201
-
1202
- try {
1203
- const result = await terser.minify(code, terserOptions);
1204
- return result.code.replace(/;$/, '');
1205
- } catch (error) {
1206
- options.log(error);
1207
- return text;
1208
- }
1209
- };
1210
- } else if (key === 'minifyURLs' && typeof option !== 'function') {
1211
- if (!option) {
1212
- return;
1213
- }
1214
-
1215
- let relateUrlOptions = option;
1216
-
1217
- if (typeof option === 'string') {
1218
- relateUrlOptions = { site: option };
1219
- } else if (typeof option !== 'object') {
1220
- relateUrlOptions = {};
1221
- }
1222
-
1223
- options.minifyURLs = function (text) {
1224
- try {
1225
- return RelateURL.relate(text, relateUrlOptions);
1226
- } catch (err) {
1227
- options.log(err);
1228
- return text;
1229
- }
1230
- };
1231
- } else {
1232
- options[key] = option;
1233
- }
1234
- });
1235
- return options;
1236
- };
1237
-
1238
- function uniqueId(value) {
1239
- let id;
1240
- do {
1241
- id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
1242
- } while (~value.indexOf(id));
1243
- return id;
1244
- }
1245
-
1246
- const specialContentTags = new Set(['script', 'style']);
1247
-
1248
- async function createSortFns(value, options, uidIgnore, uidAttr) {
1249
- const attrChains = options.sortAttributes && Object.create(null);
1250
- const classChain = options.sortClassName && new TokenChain();
1251
-
1252
- function attrNames(attrs) {
1253
- return attrs.map(function (attr) {
1254
- return options.name(attr.name);
1255
- });
1256
- }
1257
-
1258
- function shouldSkipUID(token, uid) {
1259
- return !uid || token.indexOf(uid) === -1;
1260
- }
1261
-
1262
- function shouldSkipUIDs(token) {
1263
- return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
1264
- }
1265
-
1266
- async function scan(input) {
1267
- let currentTag, currentType;
1268
- const parser = new HTMLParser(input, {
1269
- start: function (tag, attrs) {
1270
- if (attrChains) {
1271
- if (!attrChains[tag]) {
1272
- attrChains[tag] = new TokenChain();
1273
- }
1274
- attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs));
1275
- }
1276
- for (let i = 0, len = attrs.length; i < len; i++) {
1277
- const attr = attrs[i];
1278
- if (classChain && attr.value && options.name(attr.name) === 'class') {
1279
- classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs));
1280
- } else if (options.processScripts && attr.name.toLowerCase() === 'type') {
1281
- currentTag = tag;
1282
- currentType = attr.value;
1283
- }
1284
- }
1285
- },
1286
- end: function () {
1287
- currentTag = '';
1288
- },
1289
- chars: async function (text) {
1290
- if (options.processScripts && specialContentTags.has(currentTag) &&
1291
- options.processScripts.indexOf(currentType) > -1) {
1292
- await scan(text);
1293
- }
1294
- }
1295
- });
1296
-
1297
- await parser.parse();
1298
- }
1299
-
1300
- const log = options.log;
1301
- options.log = identity;
1302
- options.sortAttributes = false;
1303
- options.sortClassName = false;
1304
- await scan(await minifyHTML(value, options));
1305
- options.log = log;
1306
- if (attrChains) {
1307
- const attrSorters = Object.create(null);
1308
- for (const tag in attrChains) {
1309
- attrSorters[tag] = attrChains[tag].createSorter();
1310
- }
1311
- options.sortAttributes = function (tag, attrs) {
1312
- const sorter = attrSorters[tag];
1313
- if (sorter) {
1314
- const attrMap = Object.create(null);
1315
- const names = attrNames(attrs);
1316
- names.forEach(function (name, index) {
1317
- (attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
1318
- });
1319
- sorter.sort(names).forEach(function (name, index) {
1320
- attrs[index] = attrMap[name].shift();
1321
- });
1322
- }
1323
- };
1324
- }
1325
- if (classChain) {
1326
- const sorter = classChain.createSorter();
1327
- options.sortClassName = function (value) {
1328
- return sorter.sort(value.split(/[ \n\f\r]+/)).join(' ');
1329
- };
1330
- }
1331
- }
1332
-
1333
- async function minifyHTML(value, options, partialMarkup) {
1334
- // Check input length limitation to prevent ReDoS attacks
1335
- if (options.maxInputLength && value.length > options.maxInputLength) {
1336
- throw new Error(`Input length (${value.length}) exceeds maximum allowed length (${options.maxInputLength})`);
1337
- }
1338
-
1339
- if (options.collapseWhitespace) {
1340
- value = collapseWhitespace(value, options, true, true);
1341
- }
1342
-
1343
- const buffer = [];
1344
- let charsPrevTag;
1345
- let currentChars = '';
1346
- let hasChars;
1347
- let currentTag = '';
1348
- let currentAttrs = [];
1349
- const stackNoTrimWhitespace = [];
1350
- const stackNoCollapseWhitespace = [];
1351
- let optionalStartTag = '';
1352
- let optionalEndTag = '';
1353
- const ignoredMarkupChunks = [];
1354
- const ignoredCustomMarkupChunks = [];
1355
- let uidIgnore;
1356
- let uidAttr;
1357
- let uidPattern;
1358
- let inlineTags = new Set([...inlineHtmlElements, ...(options.inlineCustomElements ?? [])]);
1359
-
1360
- // temporarily replace ignored chunks with comments,
1361
- // so that we don't have to worry what's there.
1362
- // for all we care there might be
1363
- // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
1364
- value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
1365
- if (!uidIgnore) {
1366
- uidIgnore = uniqueId(value);
1367
- const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
1368
- if (options.ignoreCustomComments) {
1369
- options.ignoreCustomComments = options.ignoreCustomComments.slice();
1370
- } else {
1371
- options.ignoreCustomComments = [];
1372
- }
1373
- options.ignoreCustomComments.push(pattern);
1374
- }
1375
- const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
1376
- ignoredMarkupChunks.push(group1);
1377
- return token;
1378
- });
1379
-
1380
- const customFragments = options.ignoreCustomFragments.map(function (re) {
1381
- return re.source;
1382
- });
1383
- if (customFragments.length) {
1384
- // Warn about potential ReDoS if custom fragments use unlimited quantifiers
1385
- for (let i = 0; i < customFragments.length; i++) {
1386
- if (/[*+]/.test(customFragments[i])) {
1387
- options.log('Warning: Custom fragment contains unlimited quantifiers (* or +) which may cause ReDoS vulnerability');
1388
- break;
1389
- }
1390
- }
1391
-
1392
- // Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS
1393
- const maxQuantifier = options.customFragmentQuantifierLimit || 200;
1394
- const whitespacePattern = `\\s{0,${maxQuantifier}}`;
1395
-
1396
- // Use bounded quantifiers to prevent ReDoS - this approach prevents exponential backtracking
1397
- const reCustomIgnore = new RegExp(
1398
- whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
1399
- 'g'
1400
- );
1401
- // Temporarily replace custom ignored fragments with unique attributes
1402
- value = value.replace(reCustomIgnore, function (match) {
1403
- if (!uidAttr) {
1404
- uidAttr = uniqueId(value);
1405
- uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
1406
-
1407
- if (options.minifyCSS) {
1408
- options.minifyCSS = (function (fn) {
1409
- return function (text, type) {
1410
- text = text.replace(uidPattern, function (match, prefix, index) {
1411
- const chunks = ignoredCustomMarkupChunks[+index];
1412
- return chunks[1] + uidAttr + index + uidAttr + chunks[2];
1413
- });
1414
-
1415
- const ids = [];
1416
- new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function (warning) {
1417
- const match = uidPattern.exec(warning);
1418
- if (match) {
1419
- const id = uidAttr + match[2] + uidAttr;
1420
- text = text.replace(id, ignoreCSS(id));
1421
- ids.push(id);
1422
- }
1423
- });
1424
-
1425
- return fn(text, type).then(chunk => {
1426
- ids.forEach(function (id) {
1427
- chunk = chunk.replace(ignoreCSS(id), id);
1428
- });
1429
-
1430
- return chunk;
1431
- });
1432
- };
1433
- })(options.minifyCSS);
1434
- }
1435
-
1436
- if (options.minifyJS) {
1437
- options.minifyJS = (function (fn) {
1438
- return function (text, type) {
1439
- return fn(text.replace(uidPattern, function (match, prefix, index) {
1440
- const chunks = ignoredCustomMarkupChunks[+index];
1441
- return chunks[1] + uidAttr + index + uidAttr + chunks[2];
1442
- }), type);
1443
- };
1444
- })(options.minifyJS);
1445
- }
1446
- }
1447
-
1448
- const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
1449
- ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
1450
- return '\t' + token + '\t';
1451
- });
1452
- }
1453
-
1454
- if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
1455
- (options.sortClassName && typeof options.sortClassName !== 'function')) {
1456
- await createSortFns(value, options, uidIgnore, uidAttr);
1457
- }
1458
-
1459
- function _canCollapseWhitespace(tag, attrs) {
1460
- return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
1461
- }
1462
-
1463
- function _canTrimWhitespace(tag, attrs) {
1464
- return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
1465
- }
1466
-
1467
- function removeStartTag() {
1468
- let index = buffer.length - 1;
1469
- while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
1470
- index--;
1471
- }
1472
- buffer.length = Math.max(0, index);
1473
- }
1474
-
1475
- function removeEndTag() {
1476
- let index = buffer.length - 1;
1477
- while (index > 0 && !/^<\//.test(buffer[index])) {
1478
- index--;
1479
- }
1480
- buffer.length = Math.max(0, index);
1481
- }
1482
-
1483
- // look for trailing whitespaces, bypass any inline tags
1484
- function trimTrailingWhitespace(index, nextTag) {
1485
- for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
1486
- const str = buffer[index];
1487
- const match = str.match(/^<\/([\w:-]+)>$/);
1488
- if (match) {
1489
- endTag = match[1];
1490
- } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineTags))) {
1491
- break;
1492
- }
1493
- }
1494
- }
1495
-
1496
- // look for trailing whitespaces from previously processed text
1497
- // which may not be trimmed due to a following comment or an empty
1498
- // element which has now been removed
1499
- function squashTrailingWhitespace(nextTag) {
1500
- let charsIndex = buffer.length - 1;
1501
- if (buffer.length > 1) {
1502
- const item = buffer[buffer.length - 1];
1503
- if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
1504
- charsIndex--;
1505
- }
1506
- }
1507
- trimTrailingWhitespace(charsIndex, nextTag);
1508
- }
1509
-
1510
- const parser = new HTMLParser(value, {
1511
- partialMarkup,
1512
- continueOnParseError: options.continueOnParseError,
1513
- customAttrAssign: options.customAttrAssign,
1514
- customAttrSurround: options.customAttrSurround,
1515
- html5: options.html5,
1516
-
1517
- start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
1518
- if (tag.toLowerCase() === 'svg') {
1519
- options = Object.create(options);
1520
- options.caseSensitive = true;
1521
- options.keepClosingSlash = true;
1522
- options.name = identity;
1523
- }
1524
- tag = options.name(tag);
1525
- currentTag = tag;
1526
- charsPrevTag = tag;
1527
- if (!inlineTextTags.has(tag)) {
1528
- currentChars = '';
1529
- }
1530
- hasChars = false;
1531
- currentAttrs = attrs;
1532
-
1533
- let optional = options.removeOptionalTags;
1534
- if (optional) {
1535
- const htmlTag = htmlTags.has(tag);
1536
- // <html> may be omitted if first thing inside is not comment
1537
- // <head> may be omitted if first thing inside is an element
1538
- // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
1539
- // <colgroup> may be omitted if first thing inside is <col>
1540
- // <tbody> may be omitted if first thing inside is <tr>
1541
- if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
1542
- removeStartTag();
1543
- }
1544
- optionalStartTag = '';
1545
- // end-tag-followed-by-start-tag omission rules
1546
- if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
1547
- removeEndTag();
1548
- // <colgroup> cannot be omitted if preceding </colgroup> is omitted
1549
- // <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted
1550
- optional = !isStartTagMandatory(optionalEndTag, tag);
1551
- }
1552
- optionalEndTag = '';
1553
- }
1554
-
1555
- // set whitespace flags for nested tags (eg. <code> within a <pre>)
1556
- if (options.collapseWhitespace) {
1557
- if (!stackNoTrimWhitespace.length) {
1558
- squashTrailingWhitespace(tag);
1559
- }
1560
- if (!unary) {
1561
- if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
1562
- stackNoTrimWhitespace.push(tag);
1563
- }
1564
- if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
1565
- stackNoCollapseWhitespace.push(tag);
1566
- }
1567
- }
1568
- }
1569
-
1570
- const openTag = '<' + tag;
1571
- const hasUnarySlash = unarySlash && options.keepClosingSlash;
1572
-
1573
- buffer.push(openTag);
1574
-
1575
- if (options.sortAttributes) {
1576
- options.sortAttributes(tag, attrs);
1577
- }
1578
-
1579
- const parts = [];
1580
- for (let i = attrs.length, isLast = true; --i >= 0;) {
1581
- const normalized = await normalizeAttr(attrs[i], attrs, tag, options);
1582
- if (normalized) {
1583
- parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr));
1584
- isLast = false;
1585
- }
1586
- }
1587
- if (parts.length > 0) {
1588
- buffer.push(' ');
1589
- buffer.push.apply(buffer, parts);
1590
- } else if (optional && optionalStartTags.has(tag)) {
1591
- // start tag must never be omitted if it has any attributes
1592
- optionalStartTag = tag;
1593
- }
1594
-
1595
- buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
1596
-
1597
- if (autoGenerated && !options.includeAutoGeneratedTags) {
1598
- removeStartTag();
1599
- optionalStartTag = '';
1600
- }
1601
- },
1602
- end: function (tag, attrs, autoGenerated) {
1603
- if (tag.toLowerCase() === 'svg') {
1604
- options = Object.getPrototypeOf(options);
1605
- }
1606
- tag = options.name(tag);
1607
-
1608
- // check if current tag is in a whitespace stack
1609
- if (options.collapseWhitespace) {
1610
- if (stackNoTrimWhitespace.length) {
1611
- if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
1612
- stackNoTrimWhitespace.pop();
1613
- }
1614
- } else {
1615
- squashTrailingWhitespace('/' + tag);
1616
- }
1617
- if (stackNoCollapseWhitespace.length &&
1618
- tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
1619
- stackNoCollapseWhitespace.pop();
1620
- }
1621
- }
1622
-
1623
- let isElementEmpty = false;
1624
- if (tag === currentTag) {
1625
- currentTag = '';
1626
- isElementEmpty = !hasChars;
1627
- }
1628
-
1629
- if (options.removeOptionalTags) {
1630
- // <html>, <head> or <body> may be omitted if the element is empty
1631
- if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
1632
- removeStartTag();
1633
- }
1634
- optionalStartTag = '';
1635
- // </html> or </body> may be omitted if not followed by comment
1636
- // </head> may be omitted if not followed by space or comment
1637
- // </p> may be omitted if no more content in non-</a> parent
1638
- // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
1639
- if (htmlTags.has(tag) && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
1640
- removeEndTag();
1641
- }
1642
- optionalEndTag = optionalEndTags.has(tag) ? tag : '';
1643
- }
1644
-
1645
- if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
1646
- // remove last "element" from buffer
1647
- removeStartTag();
1648
- optionalStartTag = '';
1649
- optionalEndTag = '';
1650
- } else {
1651
- if (autoGenerated && !options.includeAutoGeneratedTags) {
1652
- optionalEndTag = '';
1653
- } else {
1654
- buffer.push('</' + tag + '>');
1655
- }
1656
- charsPrevTag = '/' + tag;
1657
- if (!inlineTags.has(tag)) {
1658
- currentChars = '';
1659
- } else if (isElementEmpty) {
1660
- currentChars += '|';
1661
- }
1662
- }
1663
- },
1664
- chars: async function (text, prevTag, nextTag) {
1665
- prevTag = prevTag === '' ? 'comment' : prevTag;
1666
- nextTag = nextTag === '' ? 'comment' : nextTag;
1667
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
1668
- text = entities.decodeHTML(text);
1669
- }
1670
- if (options.collapseWhitespace) {
1671
- if (!stackNoTrimWhitespace.length) {
1672
- if (prevTag === 'comment') {
1673
- const prevComment = buffer[buffer.length - 1];
1674
- if (prevComment.indexOf(uidIgnore) === -1) {
1675
- if (!prevComment) {
1676
- prevTag = charsPrevTag;
1677
- }
1678
- if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
1679
- const charsIndex = buffer.length - 2;
1680
- buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
1681
- text = trailingSpaces + text;
1682
- return '';
1683
- });
1684
- }
1685
- }
1686
- }
1687
- if (prevTag) {
1688
- if (prevTag === '/nobr' || prevTag === 'wbr') {
1689
- if (/^\s/.test(text)) {
1690
- let tagIndex = buffer.length - 1;
1691
- while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
1692
- tagIndex--;
1693
- }
1694
- trimTrailingWhitespace(tagIndex - 1, 'br');
1695
- }
1696
- } else if (inlineTextTags.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1697
- text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
1698
- }
1699
- }
1700
- if (prevTag || nextTag) {
1701
- text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineTags);
1702
- } else {
1703
- text = collapseWhitespace(text, options, true, true);
1704
- }
1705
- if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
1706
- trimTrailingWhitespace(buffer.length - 1, nextTag);
1707
- }
1708
- }
1709
- if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
1710
- text = collapseWhitespace(text, options, false, false, true);
1711
- }
1712
- }
1713
- if (options.processScripts && specialContentTags.has(currentTag)) {
1714
- text = await processScript(text, options, currentAttrs);
1715
- }
1716
- if (isExecutableScript(currentTag, currentAttrs)) {
1717
- text = await options.minifyJS(text);
1718
- }
1719
- if (isStyleSheet(currentTag, currentAttrs)) {
1720
- text = await options.minifyCSS(text);
1721
- }
1722
- if (options.removeOptionalTags && text) {
1723
- // <html> may be omitted if first thing inside is not comment
1724
- // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
1725
- if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
1726
- removeStartTag();
1727
- }
1728
- optionalStartTag = '';
1729
- // </html> or </body> may be omitted if not followed by comment
1730
- // </head>, </colgroup> or </caption> may be omitted if not followed by space or comment
1731
- if (compactTags.has(optionalEndTag) || (looseTags.has(optionalEndTag) && !/^\s/.test(text))) {
1732
- removeEndTag();
1733
- }
1734
- optionalEndTag = '';
1735
- }
1736
- charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
1737
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
1738
- // Escape any `&` symbols that start either:
1739
- // 1) a legacy named character reference (i.e. one that doesn't end with `;`)
1740
- // 2) or any other character reference (i.e. one that does end with `;`)
1741
- // Note that `&` can be escaped as `&amp`, without the semi-colon.
1742
- // https://mathiasbynens.be/notes/ambiguous-ampersands
1743
- 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;');
1744
- }
1745
- if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
1746
- text = text.replace(uidPattern, function (match, prefix, index) {
1747
- return ignoredCustomMarkupChunks[+index][0];
1748
- });
1749
- }
1750
- currentChars += text;
1751
- if (text) {
1752
- hasChars = true;
1753
- }
1754
- buffer.push(text);
1755
- },
1756
- comment: async function (text, nonStandard) {
1757
- const prefix = nonStandard ? '<!' : '<!--';
1758
- const suffix = nonStandard ? '>' : '-->';
1759
- if (isConditionalComment(text)) {
1760
- text = prefix + await cleanConditionalComment(text, options) + suffix;
1761
- } else if (options.removeComments) {
1762
- if (isIgnoredComment(text, options)) {
1763
- text = '<!--' + text + '-->';
1764
- } else {
1765
- text = '';
1766
- }
1767
- } else {
1768
- text = prefix + text + suffix;
1769
- }
1770
- if (options.removeOptionalTags && text) {
1771
- // preceding comments suppress tag omissions
1772
- optionalStartTag = '';
1773
- optionalEndTag = '';
1774
- }
1775
- buffer.push(text);
1776
- },
1777
- doctype: function (doctype) {
1778
- buffer.push(options.useShortDoctype
1779
- ? '<!doctype' +
1780
- (options.removeTagWhitespace ? '' : ' ') + 'html>'
1781
- : collapseWhitespaceAll(doctype));
1782
- }
1783
- });
1784
-
1785
- await parser.parse();
1786
-
1787
- if (options.removeOptionalTags) {
1788
- // <html> may be omitted if first thing inside is not comment
1789
- // <head> or <body> may be omitted if empty
1790
- if (topLevelTags.has(optionalStartTag)) {
1791
- removeStartTag();
1792
- }
1793
- // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
1794
- if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
1795
- removeEndTag();
1796
- }
1797
- }
1798
- if (options.collapseWhitespace) {
1799
- squashTrailingWhitespace('br');
1800
- }
1801
-
1802
- return joinResultSegments(buffer, options, uidPattern
1803
- ? function (str) {
1804
- return str.replace(uidPattern, function (match, prefix, index, suffix) {
1805
- let chunk = ignoredCustomMarkupChunks[+index][0];
1806
- if (options.collapseWhitespace) {
1807
- if (prefix !== '\t') {
1808
- chunk = prefix + chunk;
1809
- }
1810
- if (suffix !== '\t') {
1811
- chunk += suffix;
1812
- }
1813
- return collapseWhitespace(chunk, {
1814
- preserveLineBreaks: options.preserveLineBreaks,
1815
- conservativeCollapse: !options.trimCustomFragments
1816
- }, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
1817
- }
1818
- return chunk;
1819
- });
1820
- }
1821
- : identity, uidIgnore
1822
- ? function (str) {
1823
- return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (match, index) {
1824
- return ignoredMarkupChunks[+index];
1825
- });
1826
- }
1827
- : identity);
1828
- }
1829
-
1830
- function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
1831
- let str;
1832
- const maxLineLength = options.maxLineLength;
1833
- const noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
1834
-
1835
- if (maxLineLength) {
1836
- let line = ''; const lines = [];
1837
- while (results.length) {
1838
- const len = line.length;
1839
- const end = results[0].indexOf('\n');
1840
- const isClosingTag = Boolean(results[0].match(endTag));
1841
- const shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
1842
-
1843
- if (end < 0) {
1844
- line += restoreIgnore(restoreCustom(results.shift()));
1845
- } else {
1846
- line += restoreIgnore(restoreCustom(results[0].slice(0, end)));
1847
- results[0] = results[0].slice(end + 1);
1848
- }
1849
- if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
1850
- lines.push(line.slice(0, len));
1851
- line = line.slice(len);
1852
- } else if (end >= 0) {
1853
- lines.push(line);
1854
- line = '';
1855
- }
1856
- }
1857
- if (line) {
1858
- lines.push(line);
1859
- }
1860
- str = lines.join('\n');
1861
- } else {
1862
- str = restoreIgnore(restoreCustom(results.join('')));
1863
- }
1864
- return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
1865
- }
1866
-
1867
- const minify = async function (value, options) {
1868
- const start = Date.now();
1869
- options = processOptions(options || {});
1870
- const result = await minifyHTML(value, options);
1871
- options.log('minified in: ' + (Date.now() - start) + 'ms');
1872
- return result;
1873
- };
1874
-
1875
- var htmlminifier = { minify };
1876
-
1877
- exports.default = htmlminifier;
1878
- exports.minify = minify;