html-minifier-next 5.1.1 → 5.1.3
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.
- package/dist/htmlminifier.cjs +519 -397
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +2 -6
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +1 -1
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/htmlminifier.js +240 -197
- package/src/htmlparser.js +14 -6
- package/src/lib/attributes.js +161 -91
- package/src/lib/options.js +3 -3
- package/src/lib/utils.js +4 -4
- package/src/lib/whitespace.js +5 -2
package/dist/htmlminifier.cjs
CHANGED
|
@@ -2,6 +2,106 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
// Stringify for options signatures (sorted keys, shallow, nested objects)
|
|
6
|
+
|
|
7
|
+
function stableStringify(obj) {
|
|
8
|
+
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
9
|
+
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
10
|
+
const keys = Object.keys(obj).sort();
|
|
11
|
+
let out = '{';
|
|
12
|
+
for (let i = 0; i < keys.length; i++) {
|
|
13
|
+
const k = keys[i];
|
|
14
|
+
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
15
|
+
}
|
|
16
|
+
return out + '}';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// LRU cache for strings and promises
|
|
20
|
+
|
|
21
|
+
class LRU {
|
|
22
|
+
constructor(limit = 200) {
|
|
23
|
+
this.limit = limit;
|
|
24
|
+
this.map = new Map();
|
|
25
|
+
}
|
|
26
|
+
get(key) {
|
|
27
|
+
if (this.map.has(key)) {
|
|
28
|
+
const v = this.map.get(key);
|
|
29
|
+
this.map.delete(key);
|
|
30
|
+
this.map.set(key, v);
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
set(key, value) {
|
|
36
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
37
|
+
this.map.set(key, value);
|
|
38
|
+
if (this.map.size > this.limit) {
|
|
39
|
+
const first = this.map.keys().next().value;
|
|
40
|
+
this.map.delete(first);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
delete(key) { this.map.delete(key); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Unique ID generator
|
|
47
|
+
|
|
48
|
+
function uniqueId(value) {
|
|
49
|
+
let id;
|
|
50
|
+
do {
|
|
51
|
+
id = 'u' + crypto.randomUUID().replace(/-/g, '');
|
|
52
|
+
} while (~value.indexOf(id));
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Identity and transform functions
|
|
57
|
+
|
|
58
|
+
function identity(value) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isThenable(value) {
|
|
63
|
+
return value != null && typeof value === 'object' && typeof value.then === 'function';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function lowercase(value) {
|
|
67
|
+
return value.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Replace async helper
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Asynchronously replace matches in a string
|
|
74
|
+
* @param {string} str - Input string
|
|
75
|
+
* @param {RegExp} regex - Regular expression with global flag
|
|
76
|
+
* @param {Function} asyncFn - Async function to process each match
|
|
77
|
+
* @returns {Promise<string>} Processed string
|
|
78
|
+
*/
|
|
79
|
+
async function replaceAsync(str, regex, asyncFn) {
|
|
80
|
+
const promises = [];
|
|
81
|
+
|
|
82
|
+
str.replace(regex, (match, ...args) => {
|
|
83
|
+
const promise = asyncFn(match, ...args);
|
|
84
|
+
promises.push(promise);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const data = await Promise.all(promises);
|
|
88
|
+
return str.replace(regex, () => data.shift());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// String patterns to RegExp conversion (for JSON config support)
|
|
92
|
+
|
|
93
|
+
function parseRegExp(value) {
|
|
94
|
+
if (typeof value === 'string') {
|
|
95
|
+
if (!value) return undefined; // Empty string = not configured
|
|
96
|
+
const match = value.match(/^\/(.+)\/([dgimsuvy]*)$/);
|
|
97
|
+
if (match) {
|
|
98
|
+
return new RegExp(match[1], match[2]);
|
|
99
|
+
}
|
|
100
|
+
return new RegExp(value);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
5
105
|
/*
|
|
6
106
|
* HTML Parser By John Resig (ejohn.org)
|
|
7
107
|
* Modified by Juriy “kangax” Zaytsev
|
|
@@ -9,6 +109,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
9
109
|
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
|
10
110
|
*/
|
|
11
111
|
|
|
112
|
+
|
|
12
113
|
/*
|
|
13
114
|
* Use like so:
|
|
14
115
|
*
|
|
@@ -232,7 +333,8 @@ class HTMLParser {
|
|
|
232
333
|
|
|
233
334
|
if (commentEnd >= 0) {
|
|
234
335
|
if (handler.comment) {
|
|
235
|
-
|
|
336
|
+
const result = handler.comment(fullHtml.substring(pos + 4, commentEnd));
|
|
337
|
+
if (isThenable(result)) await result;
|
|
236
338
|
}
|
|
237
339
|
advance(commentEnd + 3 - pos);
|
|
238
340
|
prevTag = '';
|
|
@@ -248,7 +350,8 @@ class HTMLParser {
|
|
|
248
350
|
|
|
249
351
|
if (conditionalEnd >= 0) {
|
|
250
352
|
if (handler.comment) {
|
|
251
|
-
|
|
353
|
+
const result = handler.comment(fullHtml.substring(pos + 2, conditionalEnd + 1), true /* Non-standard */);
|
|
354
|
+
if (isThenable(result)) await result;
|
|
252
355
|
}
|
|
253
356
|
advance(conditionalEnd + 2 - pos);
|
|
254
357
|
prevTag = '';
|
|
@@ -326,7 +429,8 @@ class HTMLParser {
|
|
|
326
429
|
}
|
|
327
430
|
|
|
328
431
|
if (handler.chars) {
|
|
329
|
-
|
|
432
|
+
const result = handler.chars(text, prevTag, nextTag, prevAttrs, nextAttrs);
|
|
433
|
+
if (isThenable(result)) await result;
|
|
330
434
|
}
|
|
331
435
|
prevTag = '';
|
|
332
436
|
prevAttrs = [];
|
|
@@ -345,7 +449,8 @@ class HTMLParser {
|
|
|
345
449
|
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
|
|
346
450
|
}
|
|
347
451
|
if (handler.chars) {
|
|
348
|
-
|
|
452
|
+
const result = handler.chars(text);
|
|
453
|
+
if (isThenable(result)) await result;
|
|
349
454
|
}
|
|
350
455
|
// Advance HTML past the matched special tag content and its closing tag
|
|
351
456
|
advance(m[0].length);
|
|
@@ -353,7 +458,8 @@ class HTMLParser {
|
|
|
353
458
|
} else {
|
|
354
459
|
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
355
460
|
if (handler.continueOnParseError && handler.chars && pos < fullLength) {
|
|
356
|
-
|
|
461
|
+
const result = handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
462
|
+
if (isThenable(result)) await result;
|
|
357
463
|
advance(1);
|
|
358
464
|
} else {
|
|
359
465
|
break;
|
|
@@ -365,7 +471,8 @@ class HTMLParser {
|
|
|
365
471
|
if (handler.continueOnParseError) {
|
|
366
472
|
// Skip the problematic character and continue
|
|
367
473
|
if (handler.chars) {
|
|
368
|
-
|
|
474
|
+
const result = handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
475
|
+
if (isThenable(result)) await result;
|
|
369
476
|
}
|
|
370
477
|
advance(1);
|
|
371
478
|
prevTag = '';
|
|
@@ -913,106 +1020,6 @@ function getPresetNames() {
|
|
|
913
1020
|
return Object.keys(presets);
|
|
914
1021
|
}
|
|
915
1022
|
|
|
916
|
-
// Stringify for options signatures (sorted keys, shallow, nested objects)
|
|
917
|
-
|
|
918
|
-
function stableStringify(obj) {
|
|
919
|
-
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
920
|
-
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
921
|
-
const keys = Object.keys(obj).sort();
|
|
922
|
-
let out = '{';
|
|
923
|
-
for (let i = 0; i < keys.length; i++) {
|
|
924
|
-
const k = keys[i];
|
|
925
|
-
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
926
|
-
}
|
|
927
|
-
return out + '}';
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// LRU cache for strings and promises
|
|
931
|
-
|
|
932
|
-
class LRU {
|
|
933
|
-
constructor(limit = 200) {
|
|
934
|
-
this.limit = limit;
|
|
935
|
-
this.map = new Map();
|
|
936
|
-
}
|
|
937
|
-
get(key) {
|
|
938
|
-
if (this.map.has(key)) {
|
|
939
|
-
const v = this.map.get(key);
|
|
940
|
-
this.map.delete(key);
|
|
941
|
-
this.map.set(key, v);
|
|
942
|
-
return v;
|
|
943
|
-
}
|
|
944
|
-
return undefined;
|
|
945
|
-
}
|
|
946
|
-
set(key, value) {
|
|
947
|
-
if (this.map.has(key)) this.map.delete(key);
|
|
948
|
-
this.map.set(key, value);
|
|
949
|
-
if (this.map.size > this.limit) {
|
|
950
|
-
const first = this.map.keys().next().value;
|
|
951
|
-
this.map.delete(first);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
delete(key) { this.map.delete(key); }
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Unique ID generator
|
|
958
|
-
|
|
959
|
-
function uniqueId(value) {
|
|
960
|
-
let id;
|
|
961
|
-
do {
|
|
962
|
-
id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
|
|
963
|
-
} while (~value.indexOf(id));
|
|
964
|
-
return id;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Identity and transform functions
|
|
968
|
-
|
|
969
|
-
function identity(value) {
|
|
970
|
-
return value;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function identityAsync(value) {
|
|
974
|
-
return Promise.resolve(value);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function lowercase(value) {
|
|
978
|
-
return value.toLowerCase();
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// Replace async helper
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Asynchronously replace matches in a string
|
|
985
|
-
* @param {string} str - Input string
|
|
986
|
-
* @param {RegExp} regex - Regular expression with global flag
|
|
987
|
-
* @param {Function} asyncFn - Async function to process each match
|
|
988
|
-
* @returns {Promise<string>} Processed string
|
|
989
|
-
*/
|
|
990
|
-
async function replaceAsync(str, regex, asyncFn) {
|
|
991
|
-
const promises = [];
|
|
992
|
-
|
|
993
|
-
str.replace(regex, (match, ...args) => {
|
|
994
|
-
const promise = asyncFn(match, ...args);
|
|
995
|
-
promises.push(promise);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
const data = await Promise.all(promises);
|
|
999
|
-
return str.replace(regex, () => data.shift());
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// String patterns to RegExp conversion (for JSON config support)
|
|
1003
|
-
|
|
1004
|
-
function parseRegExp(value) {
|
|
1005
|
-
if (typeof value === 'string') {
|
|
1006
|
-
if (!value) return undefined; // Empty string = not configured
|
|
1007
|
-
const match = value.match(/^\/(.+)\/([dgimsuvy]*)$/);
|
|
1008
|
-
if (match) {
|
|
1009
|
-
return new RegExp(match[1], match[2]);
|
|
1010
|
-
}
|
|
1011
|
-
return new RegExp(value);
|
|
1012
|
-
}
|
|
1013
|
-
return value;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
1023
|
// Regex patterns (to avoid repeated allocations in hot paths)
|
|
1017
1024
|
|
|
1018
1025
|
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
@@ -1385,12 +1392,15 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
1385
1392
|
|
|
1386
1393
|
// Collapse/trim whitespace for given tag
|
|
1387
1394
|
|
|
1395
|
+
const noCollapseWsTags = new Set(['script', 'style', 'pre', 'textarea']);
|
|
1396
|
+
const noTrimWsTags = new Set(['pre', 'textarea']);
|
|
1397
|
+
|
|
1388
1398
|
function canCollapseWhitespace(tag) {
|
|
1389
|
-
return
|
|
1399
|
+
return !noCollapseWsTags.has(tag);
|
|
1390
1400
|
}
|
|
1391
1401
|
|
|
1392
1402
|
function canTrimWhitespace(tag) {
|
|
1393
|
-
return
|
|
1403
|
+
return !noTrimWsTags.has(tag);
|
|
1394
1404
|
}
|
|
1395
1405
|
|
|
1396
1406
|
/**
|
|
@@ -1662,7 +1672,7 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1662
1672
|
options.removeComments ||
|
|
1663
1673
|
options.removeOptionalTags ||
|
|
1664
1674
|
options.minifyJS !== identity ||
|
|
1665
|
-
options.minifyCSS !==
|
|
1675
|
+
options.minifyCSS !== identity ||
|
|
1666
1676
|
options.minifyURLs !== identity ||
|
|
1667
1677
|
options.minifySVG
|
|
1668
1678
|
);
|
|
@@ -1689,7 +1699,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1689
1699
|
canTrimWhitespace,
|
|
1690
1700
|
...optionDefaults,
|
|
1691
1701
|
log: identity,
|
|
1692
|
-
minifyCSS:
|
|
1702
|
+
minifyCSS: identity,
|
|
1693
1703
|
minifyJS: identity,
|
|
1694
1704
|
minifyURLs: identity,
|
|
1695
1705
|
minifySVG: null
|
|
@@ -2220,31 +2230,45 @@ function isBooleanAttribute(attrName, attrValue) {
|
|
|
2220
2230
|
(attrValue === '' && emptyCollapsible.has(attrName));
|
|
2221
2231
|
}
|
|
2222
2232
|
|
|
2233
|
+
const uriTypeAttributes = new Map([
|
|
2234
|
+
['a', new Set(['href'])],
|
|
2235
|
+
['area', new Set(['href'])],
|
|
2236
|
+
['link', new Set(['href'])],
|
|
2237
|
+
['base', new Set(['href'])],
|
|
2238
|
+
['img', new Set(['src', 'longdesc', 'usemap'])],
|
|
2239
|
+
['object', new Set(['classid', 'codebase', 'data', 'usemap'])],
|
|
2240
|
+
['q', new Set(['cite'])],
|
|
2241
|
+
['blockquote', new Set(['cite'])],
|
|
2242
|
+
['ins', new Set(['cite'])],
|
|
2243
|
+
['del', new Set(['cite'])],
|
|
2244
|
+
['form', new Set(['action'])],
|
|
2245
|
+
['input', new Set(['src', 'usemap'])],
|
|
2246
|
+
['head', new Set(['profile'])],
|
|
2247
|
+
['script', new Set(['src', 'for'])]
|
|
2248
|
+
]);
|
|
2249
|
+
|
|
2223
2250
|
function isUriTypeAttribute(attrName, tag) {
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
2227
|
-
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
2228
|
-
(tag === 'q' && attrName === 'cite') ||
|
|
2229
|
-
(tag === 'blockquote' && attrName === 'cite') ||
|
|
2230
|
-
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
2231
|
-
(tag === 'form' && attrName === 'action') ||
|
|
2232
|
-
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
2233
|
-
(tag === 'head' && attrName === 'profile') ||
|
|
2234
|
-
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
2235
|
-
);
|
|
2251
|
+
const set = uriTypeAttributes.get(tag);
|
|
2252
|
+
return set ? set.has(attrName) : false;
|
|
2236
2253
|
}
|
|
2237
2254
|
|
|
2255
|
+
const numberTypeAttributes = new Map([
|
|
2256
|
+
['a', new Set(['tabindex'])],
|
|
2257
|
+
['area', new Set(['tabindex'])],
|
|
2258
|
+
['object', new Set(['tabindex'])],
|
|
2259
|
+
['button', new Set(['tabindex'])],
|
|
2260
|
+
['input', new Set(['maxlength', 'tabindex'])],
|
|
2261
|
+
['select', new Set(['size', 'tabindex'])],
|
|
2262
|
+
['textarea', new Set(['rows', 'cols', 'tabindex'])],
|
|
2263
|
+
['colgroup', new Set(['span'])],
|
|
2264
|
+
['col', new Set(['span'])],
|
|
2265
|
+
['th', new Set(['rowspan', 'colspan'])],
|
|
2266
|
+
['td', new Set(['rowspan', 'colspan'])]
|
|
2267
|
+
]);
|
|
2268
|
+
|
|
2238
2269
|
function isNumberTypeAttribute(attrName, tag) {
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
2242
|
-
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
2243
|
-
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
2244
|
-
(tag === 'colgroup' && attrName === 'span') ||
|
|
2245
|
-
(tag === 'col' && attrName === 'span') ||
|
|
2246
|
-
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
2247
|
-
);
|
|
2270
|
+
const set = numberTypeAttributes.get(tag);
|
|
2271
|
+
return set ? set.has(attrName) : false;
|
|
2248
2272
|
}
|
|
2249
2273
|
|
|
2250
2274
|
function isLinkType(tag, attrs, value) {
|
|
@@ -2313,7 +2337,9 @@ function hasAttrName(name, attrs) {
|
|
|
2313
2337
|
|
|
2314
2338
|
// Cleaners
|
|
2315
2339
|
|
|
2316
|
-
|
|
2340
|
+
// Returns the cleaned attribute value directly (sync) or as a Promise (async);
|
|
2341
|
+
// callers must handle both cases—use `isThenable()` to distinguish
|
|
2342
|
+
function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
2317
2343
|
// Apply early whitespace normalization if enabled
|
|
2318
2344
|
// Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
2319
2345
|
if (options.collapseAttributeWhitespace) {
|
|
@@ -2328,16 +2354,18 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2328
2354
|
|
|
2329
2355
|
if (isEventAttribute(attrName, options)) {
|
|
2330
2356
|
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
return attrValue;
|
|
2357
|
+
const result = options.minifyJS(attrValue, true);
|
|
2358
|
+
if (isThenable(result)) {
|
|
2359
|
+
return result.catch(err => {
|
|
2360
|
+
if (!options.continueOnMinifyError) throw err;
|
|
2361
|
+
options.log && options.log(err);
|
|
2362
|
+
return attrValue;
|
|
2363
|
+
});
|
|
2339
2364
|
}
|
|
2340
|
-
|
|
2365
|
+
return result;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
if (attrName === 'class') {
|
|
2341
2369
|
attrValue = trimWhitespace(attrValue);
|
|
2342
2370
|
if (options.sortClassNames) {
|
|
2343
2371
|
attrValue = options.sortClassNames(attrValue);
|
|
@@ -2345,47 +2373,63 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2345
2373
|
attrValue = collapseWhitespaceAll(attrValue);
|
|
2346
2374
|
}
|
|
2347
2375
|
return attrValue;
|
|
2348
|
-
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
if (isUriTypeAttribute(attrName, tag)) {
|
|
2349
2379
|
attrValue = trimWhitespace(attrValue);
|
|
2350
2380
|
if (isLinkType(tag, attrs, 'canonical')) {
|
|
2351
2381
|
return attrValue;
|
|
2352
2382
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
return
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2383
|
+
const result = options.minifyURLs(attrValue);
|
|
2384
|
+
if (isThenable(result)) {
|
|
2385
|
+
return result
|
|
2386
|
+
.then(out => typeof out === 'string' ? out : attrValue)
|
|
2387
|
+
.catch(err => {
|
|
2388
|
+
if (!options.continueOnMinifyError) throw err;
|
|
2389
|
+
options.log && options.log(err);
|
|
2390
|
+
return attrValue;
|
|
2391
|
+
});
|
|
2362
2392
|
}
|
|
2363
|
-
|
|
2393
|
+
return typeof result === 'string' ? result : attrValue;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
if (isNumberTypeAttribute(attrName, tag)) {
|
|
2364
2397
|
return trimWhitespace(attrValue);
|
|
2365
|
-
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
if (attrName === 'style') {
|
|
2366
2401
|
attrValue = trimWhitespace(attrValue);
|
|
2367
2402
|
if (attrValue) {
|
|
2368
2403
|
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
2369
2404
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
2370
2405
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2406
|
+
const originalAttrValue = attrValue;
|
|
2407
|
+
const cssResult = options.minifyCSS(attrValue, 'inline');
|
|
2408
|
+
if (isThenable(cssResult)) {
|
|
2409
|
+
return cssResult
|
|
2410
|
+
.then(minified => {
|
|
2411
|
+
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
2412
|
+
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
2413
|
+
if (minified && /^(?:[a-z-]+:[;\s]*)+$/i.test(minified)) return '';
|
|
2414
|
+
return minified;
|
|
2415
|
+
})
|
|
2416
|
+
.catch(err => {
|
|
2417
|
+
if (!options.continueOnMinifyError) throw err;
|
|
2418
|
+
options.log && options.log(err);
|
|
2419
|
+
return originalAttrValue;
|
|
2420
|
+
});
|
|
2383
2421
|
}
|
|
2422
|
+
// Sync path (`minifyCSS` disabled—identity function)
|
|
2423
|
+
if (cssResult && /^(?:[a-z-]+:[;\s]*)+$/i.test(cssResult)) return '';
|
|
2424
|
+
return cssResult != null ? cssResult : attrValue;
|
|
2384
2425
|
}
|
|
2385
2426
|
return attrValue;
|
|
2386
|
-
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
if (isSrcset(attrName, tag)) {
|
|
2387
2430
|
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
2388
|
-
|
|
2431
|
+
const candidates = trimWhitespace(attrValue).split(/\s*,\s*/);
|
|
2432
|
+
const processed = candidates.map(candidate => {
|
|
2389
2433
|
let url = candidate;
|
|
2390
2434
|
let descriptor = '';
|
|
2391
2435
|
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
@@ -2397,47 +2441,65 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2397
2441
|
descriptor = ' ' + num + suffix;
|
|
2398
2442
|
}
|
|
2399
2443
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
return
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2444
|
+
const out = options.minifyURLs(url);
|
|
2445
|
+
if (isThenable(out)) {
|
|
2446
|
+
return out
|
|
2447
|
+
.then(result => (typeof result === 'string' ? result : url) + descriptor)
|
|
2448
|
+
.catch(err => {
|
|
2449
|
+
if (!options.continueOnMinifyError) throw err;
|
|
2450
|
+
options.log && options.log(err);
|
|
2451
|
+
return url + descriptor;
|
|
2452
|
+
});
|
|
2409
2453
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2454
|
+
return (typeof out === 'string' ? out : url) + descriptor;
|
|
2455
|
+
});
|
|
2456
|
+
if (processed.some(isThenable)) {
|
|
2457
|
+
return Promise.all(processed).then(results => results.join(', '));
|
|
2458
|
+
}
|
|
2459
|
+
return processed.join(', ');
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
|
2463
|
+
return attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
|
|
2413
2464
|
// 0.90000 → 0.9
|
|
2414
2465
|
// 1.0 → 1
|
|
2415
2466
|
// 1.0001 → 1.0001 (unchanged)
|
|
2416
2467
|
return (+numString).toString();
|
|
2417
2468
|
});
|
|
2418
|
-
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
2419
2472
|
return collapseWhitespaceAll(attrValue);
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
}
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
2476
|
+
return trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
if (tag === 'script' && attrName === 'type') {
|
|
2480
|
+
return trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
if (isMediaQuery(tag, attrs, attrName)) {
|
|
2425
2484
|
attrValue = trimWhitespace(attrValue);
|
|
2426
2485
|
// Only minify actual media queries (those with features in parentheses)
|
|
2427
2486
|
// Skip simple media types like `all`, `screen`, `print` which are already minimal
|
|
2428
2487
|
if (!/[()]/.test(attrValue)) {
|
|
2429
2488
|
return attrValue;
|
|
2430
2489
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
throw err;
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2490
|
+
const originalAttrValue = attrValue;
|
|
2491
|
+
const cssResult = options.minifyCSS(attrValue, 'media');
|
|
2492
|
+
if (isThenable(cssResult)) {
|
|
2493
|
+
return cssResult.catch(err => {
|
|
2494
|
+
if (!options.continueOnMinifyError) throw err;
|
|
2495
|
+
options.log && options.log(err);
|
|
2496
|
+
return originalAttrValue;
|
|
2497
|
+
});
|
|
2439
2498
|
}
|
|
2440
|
-
|
|
2499
|
+
return cssResult != null ? cssResult : attrValue;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
2441
2503
|
// Recursively minify HTML content within `srcdoc` attribute
|
|
2442
2504
|
// Fast-path: Skip if nothing would change
|
|
2443
2505
|
if (!shouldMinifyInnerHTML(options)) {
|
|
@@ -2445,6 +2507,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2445
2507
|
}
|
|
2446
2508
|
return minifyHTMLSelf(attrValue, options, true);
|
|
2447
2509
|
}
|
|
2510
|
+
|
|
2448
2511
|
return attrValue;
|
|
2449
2512
|
}
|
|
2450
2513
|
|
|
@@ -2468,17 +2531,24 @@ function chooseAttributeQuote(attrValue, options) {
|
|
|
2468
2531
|
return apos < quot ? '\'' : '"';
|
|
2469
2532
|
}
|
|
2470
2533
|
|
|
2471
|
-
|
|
2534
|
+
// Returns the normalized attribute object directly (sync) or as a Promise (async);
|
|
2535
|
+
// callers must handle both cases—use `isThenable()` to distinguish
|
|
2536
|
+
function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
2472
2537
|
const attrName = options.name(attr.name);
|
|
2473
2538
|
let attrValue = attr.value;
|
|
2474
2539
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
}
|
|
2540
|
+
// Entity decoding requires a lazy import—async only when `&` is present
|
|
2541
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
2542
|
+
return getDecodeHTMLStrict().then(decode => {
|
|
2543
|
+
return normalizeAttrContinue(attrName, decode(attrValue), attr, attrs, tag, options, minifyHTML);
|
|
2544
|
+
});
|
|
2480
2545
|
}
|
|
2481
2546
|
|
|
2547
|
+
return normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// Internal: Handles attribute normalization after entity decoding (if any)
|
|
2551
|
+
function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML) {
|
|
2482
2552
|
if ((options.removeRedundantAttributes &&
|
|
2483
2553
|
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
2484
2554
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
@@ -2489,9 +2559,18 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2489
2559
|
}
|
|
2490
2560
|
|
|
2491
2561
|
if (attrValue) {
|
|
2492
|
-
|
|
2562
|
+
const cleaned = cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
2563
|
+
if (isThenable(cleaned)) {
|
|
2564
|
+
return cleaned.then(v => normalizeAttrFinish(attrName, v, attr, tag, options));
|
|
2565
|
+
}
|
|
2566
|
+
return normalizeAttrFinish(attrName, cleaned, attr, tag, options);
|
|
2493
2567
|
}
|
|
2494
2568
|
|
|
2569
|
+
return normalizeAttrFinish(attrName, attrValue, attr, tag, options);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Internal: Final checks and result assembly after value cleaning
|
|
2573
|
+
function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
|
|
2495
2574
|
if (options.removeEmptyAttributes &&
|
|
2496
2575
|
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
2497
2576
|
return;
|
|
@@ -3691,6 +3770,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3691
3770
|
let currentAttrs = [];
|
|
3692
3771
|
const stackNoTrimWhitespace = [];
|
|
3693
3772
|
const stackNoCollapseWhitespace = [];
|
|
3773
|
+
let preTextareaDepth = 0; // Count of `pre`/`textarea` entries in `stackNoTrimWhitespace`
|
|
3694
3774
|
let optionalStartTag = '';
|
|
3695
3775
|
let optionalEndTag = '';
|
|
3696
3776
|
let optionalEndTagEmitted = false;
|
|
@@ -3716,31 +3796,33 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3716
3796
|
let removeEmptyElementsExcept;
|
|
3717
3797
|
if (options.removeEmptyElementsExcept && !Array.isArray(options.removeEmptyElementsExcept)) {
|
|
3718
3798
|
if (options.log) {
|
|
3719
|
-
options.log('Warning:
|
|
3799
|
+
options.log('Warning: `removeEmptyElementsExcept` option must be an array, received: ' + typeof options.removeEmptyElementsExcept);
|
|
3720
3800
|
}
|
|
3721
3801
|
removeEmptyElementsExcept = [];
|
|
3722
3802
|
} else {
|
|
3723
3803
|
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept, options) || [];
|
|
3724
3804
|
}
|
|
3725
3805
|
|
|
3726
|
-
// Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there
|
|
3727
|
-
//
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3806
|
+
// Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there;
|
|
3807
|
+
// for all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
|
|
3808
|
+
if (value.indexOf('<!-- htmlmin:ignore -->') !== -1) {
|
|
3809
|
+
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
3810
|
+
if (!uidIgnore) {
|
|
3811
|
+
uidIgnore = uniqueId(value);
|
|
3812
|
+
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
3813
|
+
uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
|
|
3814
|
+
if (options.ignoreCustomComments) {
|
|
3815
|
+
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
3816
|
+
} else {
|
|
3817
|
+
options.ignoreCustomComments = [];
|
|
3818
|
+
}
|
|
3819
|
+
options.ignoreCustomComments.push(pattern);
|
|
3737
3820
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
});
|
|
3821
|
+
const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
|
3822
|
+
ignoredMarkupChunks.push(group1);
|
|
3823
|
+
return token;
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3744
3826
|
|
|
3745
3827
|
// Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
|
|
3746
3828
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
@@ -3851,7 +3933,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3851
3933
|
let charsIndex = buffer.length - 1;
|
|
3852
3934
|
if (buffer.length > 1) {
|
|
3853
3935
|
const item = buffer[buffer.length - 1];
|
|
3854
|
-
if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
|
3936
|
+
if (/^(?:<!|$)/.test(item) && (!uidIgnore || item.indexOf(uidIgnore) === -1)) {
|
|
3855
3937
|
charsIndex--;
|
|
3856
3938
|
}
|
|
3857
3939
|
}
|
|
@@ -3945,6 +4027,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3945
4027
|
if (!unary) {
|
|
3946
4028
|
if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
3947
4029
|
stackNoTrimWhitespace.push(tag);
|
|
4030
|
+
if (tag === 'pre' || tag === 'textarea') preTextareaDepth++;
|
|
3948
4031
|
}
|
|
3949
4032
|
if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
3950
4033
|
stackNoCollapseWhitespace.push(tag);
|
|
@@ -3974,11 +4057,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3974
4057
|
options.sortAttributes(tag, attrs);
|
|
3975
4058
|
}
|
|
3976
4059
|
|
|
4060
|
+
const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
|
|
4061
|
+
const normalizedAttrs = attrResults.some(isThenable) ? await Promise.all(attrResults) : attrResults;
|
|
3977
4062
|
const parts = [];
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
if (
|
|
3981
|
-
parts.push(buildAttr(
|
|
4063
|
+
let isLast = true;
|
|
4064
|
+
for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
|
|
4065
|
+
if (normalizedAttrs[i]) {
|
|
4066
|
+
parts.push(buildAttr(normalizedAttrs[i], hasUnarySlash, options, isLast, uidAttr));
|
|
3982
4067
|
isLast = false;
|
|
3983
4068
|
}
|
|
3984
4069
|
}
|
|
@@ -4014,6 +4099,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4014
4099
|
if (options.collapseWhitespace) {
|
|
4015
4100
|
if (stackNoTrimWhitespace.length) {
|
|
4016
4101
|
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
4102
|
+
if (tag === 'pre' || tag === 'textarea') preTextareaDepth--;
|
|
4017
4103
|
stackNoTrimWhitespace.pop();
|
|
4018
4104
|
}
|
|
4019
4105
|
} else {
|
|
@@ -4100,207 +4186,225 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4100
4186
|
}
|
|
4101
4187
|
}
|
|
4102
4188
|
},
|
|
4103
|
-
chars:
|
|
4189
|
+
chars: function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
4104
4190
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
4105
4191
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
4106
4192
|
prevAttrs = prevAttrs || [];
|
|
4107
4193
|
nextAttrs = nextAttrs || [];
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
//
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
if (
|
|
4123
|
-
|
|
4194
|
+
|
|
4195
|
+
// Detect whether any async work is actually needed for this text node
|
|
4196
|
+
const needsDecode = options.decodeEntities && text && !specialContentElements.has(currentTag) && text.indexOf('&') !== -1;
|
|
4197
|
+
const needsProcessScript = specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs));
|
|
4198
|
+
const needsMinifyJS = options.minifyJS !== identity && isExecutableScript(currentTag, currentAttrs);
|
|
4199
|
+
const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
|
|
4200
|
+
|
|
4201
|
+
// Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
|
|
4202
|
+
function charsCollapse(text) {
|
|
4203
|
+
// Trim outermost newline-based whitespace inside `pre`/`textarea` elements
|
|
4204
|
+
// This removes trailing newlines often added by template engines before closing tags
|
|
4205
|
+
// Only trims single trailing newlines (multiple newlines are likely intentional formatting)
|
|
4206
|
+
if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
4207
|
+
const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
|
|
4208
|
+
if (preTextareaDepth > 0) {
|
|
4209
|
+
// Trim trailing whitespace only if it ends with a single newline (not multiple)
|
|
4210
|
+
// Multiple newlines are likely intentional formatting, single newline is often a template artifact
|
|
4211
|
+
// Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
|
|
4212
|
+
if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
|
|
4213
|
+
text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
|
|
4214
|
+
}
|
|
4124
4215
|
}
|
|
4125
4216
|
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
}
|
|
4217
|
+
if (options.collapseWhitespace) {
|
|
4218
|
+
if (!stackNoTrimWhitespace.length) {
|
|
4219
|
+
if (prevTag === 'comment') {
|
|
4220
|
+
const prevComment = buffer[buffer.length - 1];
|
|
4221
|
+
if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
|
|
4222
|
+
if (!prevComment) {
|
|
4223
|
+
prevTag = charsPrevTag;
|
|
4224
|
+
}
|
|
4225
|
+
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
4226
|
+
const charsIndex = buffer.length - 2;
|
|
4227
|
+
buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
|
|
4228
|
+
text = trailingSpaces + text;
|
|
4229
|
+
return '';
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
4141
4232
|
}
|
|
4142
4233
|
}
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4234
|
+
if (prevTag) {
|
|
4235
|
+
if (prevTag === '/nobr' || prevTag === 'wbr') {
|
|
4236
|
+
if (/^\s/.test(text)) {
|
|
4237
|
+
let tagIndex = buffer.length - 1;
|
|
4238
|
+
while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
|
4239
|
+
tagIndex--;
|
|
4240
|
+
}
|
|
4241
|
+
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
4150
4242
|
}
|
|
4151
|
-
|
|
4243
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
4244
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
4152
4245
|
}
|
|
4153
|
-
}
|
|
4154
|
-
|
|
4246
|
+
}
|
|
4247
|
+
if (prevTag || nextTag) {
|
|
4248
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
|
|
4249
|
+
} else {
|
|
4250
|
+
text = collapseWhitespace(text, options, true, true);
|
|
4251
|
+
}
|
|
4252
|
+
if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
|
|
4253
|
+
trimTrailingWhitespace(buffer.length - 1, nextTag);
|
|
4155
4254
|
}
|
|
4156
4255
|
}
|
|
4157
|
-
if (prevTag
|
|
4158
|
-
text =
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4256
|
+
if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
|
|
4257
|
+
text = collapseWhitespace(text, options, false, false, true);
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
return text;
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
// Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
|
|
4264
|
+
function charsFinalize(text) {
|
|
4265
|
+
if (options.removeOptionalTags && text) {
|
|
4266
|
+
// `<html>` may be omitted if first thing inside is not a comment
|
|
4267
|
+
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
|
|
4268
|
+
if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
|
|
4269
|
+
removeStartTag();
|
|
4270
|
+
}
|
|
4271
|
+
optionalStartTag = '';
|
|
4272
|
+
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
4273
|
+
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
4274
|
+
if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
|
|
4275
|
+
removeEndTag();
|
|
4276
|
+
}
|
|
4277
|
+
// Don’t reset `optionalEndTag` if text is only whitespace and will be collapsed (not conservatively)
|
|
4278
|
+
if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
|
|
4279
|
+
optionalEndTag = '';
|
|
4280
|
+
optionalEndTagEmitted = false;
|
|
4161
4281
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4282
|
+
}
|
|
4283
|
+
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
4284
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
4285
|
+
// Escape any `&` symbols that start either:
|
|
4286
|
+
// 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
4287
|
+
// 2. or any other character reference (i.e., one that does end with `;`)
|
|
4288
|
+
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
4289
|
+
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
4290
|
+
if (text.indexOf('&') !== -1) {
|
|
4291
|
+
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
4292
|
+
}
|
|
4293
|
+
if (text.indexOf('<') !== -1) {
|
|
4294
|
+
text = text.replace(RE_ESCAPE_LT, '<');
|
|
4164
4295
|
}
|
|
4165
4296
|
}
|
|
4166
|
-
if (
|
|
4167
|
-
text =
|
|
4297
|
+
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
4298
|
+
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
4299
|
+
return ignoredCustomMarkupChunks[+index][0];
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
currentChars += text;
|
|
4303
|
+
if (text) {
|
|
4304
|
+
hasChars = true;
|
|
4168
4305
|
}
|
|
4306
|
+
buffer.push(text);
|
|
4169
4307
|
}
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
}
|
|
4176
|
-
if (isStyleElement(currentTag, currentAttrs)) {
|
|
4177
|
-
text = await options.minifyCSS(text);
|
|
4308
|
+
|
|
4309
|
+
// Fast path: All work is sync—skip async machinery entirely
|
|
4310
|
+
if (!needsDecode && !needsProcessScript && !needsMinifyJS && !needsMinifyCSS) {
|
|
4311
|
+
charsFinalize(charsCollapse(text));
|
|
4312
|
+
return;
|
|
4178
4313
|
}
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
if (
|
|
4183
|
-
|
|
4314
|
+
|
|
4315
|
+
// Slow path: At least one async step required
|
|
4316
|
+
return (async () => {
|
|
4317
|
+
if (needsDecode) {
|
|
4318
|
+
text = (await getDecodeHTML())(text);
|
|
4184
4319
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
|
|
4189
|
-
removeEndTag();
|
|
4320
|
+
text = charsCollapse(text);
|
|
4321
|
+
if (needsProcessScript) {
|
|
4322
|
+
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
4190
4323
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
optionalEndTag = '';
|
|
4194
|
-
optionalEndTagEmitted = false;
|
|
4195
|
-
}
|
|
4196
|
-
}
|
|
4197
|
-
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
4198
|
-
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
4199
|
-
// Escape any `&` symbols that start either:
|
|
4200
|
-
// 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
4201
|
-
// 2. or any other character reference (i.e., one that does end with `;`)
|
|
4202
|
-
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
4203
|
-
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
4204
|
-
if (text.indexOf('&') !== -1) {
|
|
4205
|
-
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
4324
|
+
if (needsMinifyJS) {
|
|
4325
|
+
text = await options.minifyJS(text);
|
|
4206
4326
|
}
|
|
4207
|
-
if (
|
|
4208
|
-
text =
|
|
4327
|
+
if (needsMinifyCSS) {
|
|
4328
|
+
text = await options.minifyCSS(text);
|
|
4209
4329
|
}
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
4213
|
-
return ignoredCustomMarkupChunks[+index][0];
|
|
4214
|
-
});
|
|
4215
|
-
}
|
|
4216
|
-
currentChars += text;
|
|
4217
|
-
if (text) {
|
|
4218
|
-
hasChars = true;
|
|
4219
|
-
}
|
|
4220
|
-
buffer.push(text);
|
|
4330
|
+
charsFinalize(text);
|
|
4331
|
+
})();
|
|
4221
4332
|
},
|
|
4222
|
-
comment:
|
|
4333
|
+
comment: function (text, nonStandard) {
|
|
4223
4334
|
const prefix = nonStandard ? '<!' : '<!--';
|
|
4224
4335
|
const suffix = nonStandard ? '>' : '-->';
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
if (
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4336
|
+
|
|
4337
|
+
// Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
|
|
4338
|
+
function commentFinalize(comment) {
|
|
4339
|
+
if (options.removeOptionalTags && comment) {
|
|
4340
|
+
// Preceding comments suppress tag omissions
|
|
4341
|
+
optionalStartTag = '';
|
|
4342
|
+
optionalEndTag = '';
|
|
4343
|
+
optionalEndTagEmitted = false;
|
|
4232
4344
|
}
|
|
4233
|
-
} else {
|
|
4234
|
-
text = prefix + text + suffix;
|
|
4235
|
-
}
|
|
4236
|
-
if (options.removeOptionalTags && text) {
|
|
4237
|
-
// Preceding comments suppress tag omissions
|
|
4238
|
-
optionalStartTag = '';
|
|
4239
|
-
optionalEndTag = '';
|
|
4240
|
-
optionalEndTagEmitted = false;
|
|
4241
|
-
}
|
|
4242
4345
|
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4346
|
+
// Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
|
|
4347
|
+
if (options.collapseWhitespace && comment && uidIgnorePlaceholderPattern) {
|
|
4348
|
+
if (uidIgnorePlaceholderPattern.test(comment)) {
|
|
4349
|
+
// Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
|
|
4350
|
+
if (buffer.length >= 2) {
|
|
4351
|
+
const prevText = buffer[buffer.length - 1];
|
|
4352
|
+
const prevComment = buffer[buffer.length - 2];
|
|
4353
|
+
|
|
4354
|
+
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
4355
|
+
if (prevText && /^\s+$/.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
4356
|
+
// Extract the index from both placeholders to check their content
|
|
4357
|
+
const currentMatch = comment.match(uidIgnorePlaceholderPattern);
|
|
4358
|
+
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
4359
|
+
|
|
4360
|
+
if (currentMatch && prevMatch) {
|
|
4361
|
+
const currentIndex = +currentMatch[1];
|
|
4362
|
+
const prevIndex = +prevMatch[1];
|
|
4363
|
+
|
|
4364
|
+
// Defensive bounds check to ensure indices are valid
|
|
4365
|
+
if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
|
|
4366
|
+
const currentContent = ignoredMarkupChunks[currentIndex];
|
|
4367
|
+
const prevContent = ignoredMarkupChunks[prevIndex];
|
|
4368
|
+
|
|
4369
|
+
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
4370
|
+
// Don’t collapse if either contains plain text, as that would change meaning
|
|
4371
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag name
|
|
4372
|
+
// regex below requires starting with a letter, so comments are intentionally
|
|
4373
|
+
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
4374
|
+
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
4375
|
+
// Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
|
|
4376
|
+
const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
4377
|
+
const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
4378
|
+
|
|
4379
|
+
// Only collapse if both matched valid element tags (not comments/text)
|
|
4380
|
+
// and both tags are block-level (inline elements need whitespace preserved)
|
|
4381
|
+
if (currentTagMatch && prevTagMatch) {
|
|
4382
|
+
const currentTag = options.name(currentTagMatch[1]);
|
|
4383
|
+
const prevTag = options.name(prevTagMatch[1]);
|
|
4384
|
+
|
|
4385
|
+
// Don’t collapse between inline elements
|
|
4386
|
+
if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
|
|
4387
|
+
// Collapse whitespace respecting context rules
|
|
4388
|
+
let collapsedText = prevText;
|
|
4389
|
+
|
|
4390
|
+
// Apply `collapseWhitespace` with appropriate context
|
|
4391
|
+
if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
|
|
4392
|
+
// Not in pre or other no-collapse context
|
|
4393
|
+
if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
|
|
4394
|
+
// Preserve line break as single newline
|
|
4395
|
+
collapsedText = '\n';
|
|
4396
|
+
} else if (options.conservativeCollapse) {
|
|
4397
|
+
// Conservative mode: Keep single space
|
|
4398
|
+
collapsedText = ' ';
|
|
4399
|
+
} else {
|
|
4400
|
+
// Aggressive mode: Remove all whitespace
|
|
4401
|
+
collapsedText = '';
|
|
4402
|
+
}
|
|
4299
4403
|
}
|
|
4300
|
-
}
|
|
4301
4404
|
|
|
4302
|
-
|
|
4303
|
-
|
|
4405
|
+
// Replace the whitespace in buffer
|
|
4406
|
+
buffer[buffer.length - 1] = collapsedText;
|
|
4407
|
+
}
|
|
4304
4408
|
}
|
|
4305
4409
|
}
|
|
4306
4410
|
}
|
|
@@ -4309,9 +4413,27 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4309
4413
|
}
|
|
4310
4414
|
}
|
|
4311
4415
|
}
|
|
4416
|
+
|
|
4417
|
+
buffer.push(comment);
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
// Only conditional comments require async work (recursive minification)
|
|
4421
|
+
if (isConditionalComment(text)) {
|
|
4422
|
+
return cleanConditionalComment(text, options, minifyHTML).then(cleaned => {
|
|
4423
|
+
commentFinalize(prefix + cleaned + suffix);
|
|
4424
|
+
});
|
|
4312
4425
|
}
|
|
4313
4426
|
|
|
4314
|
-
|
|
4427
|
+
if (options.removeComments) {
|
|
4428
|
+
if (isIgnoredComment(text, options)) {
|
|
4429
|
+
text = '<!--' + text + '-->';
|
|
4430
|
+
} else {
|
|
4431
|
+
text = '';
|
|
4432
|
+
}
|
|
4433
|
+
} else {
|
|
4434
|
+
text = prefix + text + suffix;
|
|
4435
|
+
}
|
|
4436
|
+
commentFinalize(text);
|
|
4315
4437
|
},
|
|
4316
4438
|
doctype: function (doctype) {
|
|
4317
4439
|
buffer.push(options.useShortDoctype
|