html-minifier-next 5.1.2 → 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 +471 -372
- 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/package.json +1 -1
- package/src/htmlminifier.js +232 -195
- package/src/htmlparser.js +14 -6
- package/src/lib/attributes.js +126 -70
- package/src/lib/options.js +3 -3
- package/src/lib/utils.js +4 -4
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]+/;
|
|
@@ -1665,7 +1672,7 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1665
1672
|
options.removeComments ||
|
|
1666
1673
|
options.removeOptionalTags ||
|
|
1667
1674
|
options.minifyJS !== identity ||
|
|
1668
|
-
options.minifyCSS !==
|
|
1675
|
+
options.minifyCSS !== identity ||
|
|
1669
1676
|
options.minifyURLs !== identity ||
|
|
1670
1677
|
options.minifySVG
|
|
1671
1678
|
);
|
|
@@ -1692,7 +1699,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1692
1699
|
canTrimWhitespace,
|
|
1693
1700
|
...optionDefaults,
|
|
1694
1701
|
log: identity,
|
|
1695
|
-
minifyCSS:
|
|
1702
|
+
minifyCSS: identity,
|
|
1696
1703
|
minifyJS: identity,
|
|
1697
1704
|
minifyURLs: identity,
|
|
1698
1705
|
minifySVG: null
|
|
@@ -2330,7 +2337,9 @@ function hasAttrName(name, attrs) {
|
|
|
2330
2337
|
|
|
2331
2338
|
// Cleaners
|
|
2332
2339
|
|
|
2333
|
-
|
|
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) {
|
|
2334
2343
|
// Apply early whitespace normalization if enabled
|
|
2335
2344
|
// Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
2336
2345
|
if (options.collapseAttributeWhitespace) {
|
|
@@ -2345,16 +2354,18 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2345
2354
|
|
|
2346
2355
|
if (isEventAttribute(attrName, options)) {
|
|
2347
2356
|
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
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
|
+
});
|
|
2356
2364
|
}
|
|
2357
|
-
|
|
2365
|
+
return result;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
if (attrName === 'class') {
|
|
2358
2369
|
attrValue = trimWhitespace(attrValue);
|
|
2359
2370
|
if (options.sortClassNames) {
|
|
2360
2371
|
attrValue = options.sortClassNames(attrValue);
|
|
@@ -2362,47 +2373,63 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2362
2373
|
attrValue = collapseWhitespaceAll(attrValue);
|
|
2363
2374
|
}
|
|
2364
2375
|
return attrValue;
|
|
2365
|
-
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
if (isUriTypeAttribute(attrName, tag)) {
|
|
2366
2379
|
attrValue = trimWhitespace(attrValue);
|
|
2367
2380
|
if (isLinkType(tag, attrs, 'canonical')) {
|
|
2368
2381
|
return attrValue;
|
|
2369
2382
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
return
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
+
});
|
|
2379
2392
|
}
|
|
2380
|
-
|
|
2393
|
+
return typeof result === 'string' ? result : attrValue;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
if (isNumberTypeAttribute(attrName, tag)) {
|
|
2381
2397
|
return trimWhitespace(attrValue);
|
|
2382
|
-
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
if (attrName === 'style') {
|
|
2383
2401
|
attrValue = trimWhitespace(attrValue);
|
|
2384
2402
|
if (attrValue) {
|
|
2385
2403
|
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
2386
2404
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
2387
2405
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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
|
+
});
|
|
2400
2421
|
}
|
|
2422
|
+
// Sync path (`minifyCSS` disabled—identity function)
|
|
2423
|
+
if (cssResult && /^(?:[a-z-]+:[;\s]*)+$/i.test(cssResult)) return '';
|
|
2424
|
+
return cssResult != null ? cssResult : attrValue;
|
|
2401
2425
|
}
|
|
2402
2426
|
return attrValue;
|
|
2403
|
-
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
if (isSrcset(attrName, tag)) {
|
|
2404
2430
|
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
2405
|
-
|
|
2431
|
+
const candidates = trimWhitespace(attrValue).split(/\s*,\s*/);
|
|
2432
|
+
const processed = candidates.map(candidate => {
|
|
2406
2433
|
let url = candidate;
|
|
2407
2434
|
let descriptor = '';
|
|
2408
2435
|
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
@@ -2414,47 +2441,65 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2414
2441
|
descriptor = ' ' + num + suffix;
|
|
2415
2442
|
}
|
|
2416
2443
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
return
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
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
|
+
});
|
|
2426
2453
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
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) {
|
|
2430
2464
|
// 0.90000 → 0.9
|
|
2431
2465
|
// 1.0 → 1
|
|
2432
2466
|
// 1.0001 → 1.0001 (unchanged)
|
|
2433
2467
|
return (+numString).toString();
|
|
2434
2468
|
});
|
|
2435
|
-
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
2436
2472
|
return collapseWhitespaceAll(attrValue);
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
}
|
|
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)) {
|
|
2442
2484
|
attrValue = trimWhitespace(attrValue);
|
|
2443
2485
|
// Only minify actual media queries (those with features in parentheses)
|
|
2444
2486
|
// Skip simple media types like `all`, `screen`, `print` which are already minimal
|
|
2445
2487
|
if (!/[()]/.test(attrValue)) {
|
|
2446
2488
|
return attrValue;
|
|
2447
2489
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
throw err;
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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
|
+
});
|
|
2456
2498
|
}
|
|
2457
|
-
|
|
2499
|
+
return cssResult != null ? cssResult : attrValue;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
2458
2503
|
// Recursively minify HTML content within `srcdoc` attribute
|
|
2459
2504
|
// Fast-path: Skip if nothing would change
|
|
2460
2505
|
if (!shouldMinifyInnerHTML(options)) {
|
|
@@ -2462,6 +2507,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2462
2507
|
}
|
|
2463
2508
|
return minifyHTMLSelf(attrValue, options, true);
|
|
2464
2509
|
}
|
|
2510
|
+
|
|
2465
2511
|
return attrValue;
|
|
2466
2512
|
}
|
|
2467
2513
|
|
|
@@ -2485,17 +2531,24 @@ function chooseAttributeQuote(attrValue, options) {
|
|
|
2485
2531
|
return apos < quot ? '\'' : '"';
|
|
2486
2532
|
}
|
|
2487
2533
|
|
|
2488
|
-
|
|
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) {
|
|
2489
2537
|
const attrName = options.name(attr.name);
|
|
2490
2538
|
let attrValue = attr.value;
|
|
2491
2539
|
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
}
|
|
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
|
+
});
|
|
2497
2545
|
}
|
|
2498
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) {
|
|
2499
2552
|
if ((options.removeRedundantAttributes &&
|
|
2500
2553
|
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
2501
2554
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
@@ -2506,9 +2559,18 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2506
2559
|
}
|
|
2507
2560
|
|
|
2508
2561
|
if (attrValue) {
|
|
2509
|
-
|
|
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);
|
|
2510
2567
|
}
|
|
2511
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) {
|
|
2512
2574
|
if (options.removeEmptyAttributes &&
|
|
2513
2575
|
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
2514
2576
|
return;
|
|
@@ -3734,31 +3796,33 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3734
3796
|
let removeEmptyElementsExcept;
|
|
3735
3797
|
if (options.removeEmptyElementsExcept && !Array.isArray(options.removeEmptyElementsExcept)) {
|
|
3736
3798
|
if (options.log) {
|
|
3737
|
-
options.log('Warning:
|
|
3799
|
+
options.log('Warning: `removeEmptyElementsExcept` option must be an array, received: ' + typeof options.removeEmptyElementsExcept);
|
|
3738
3800
|
}
|
|
3739
3801
|
removeEmptyElementsExcept = [];
|
|
3740
3802
|
} else {
|
|
3741
3803
|
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept, options) || [];
|
|
3742
3804
|
}
|
|
3743
3805
|
|
|
3744
|
-
// Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there
|
|
3745
|
-
//
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
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);
|
|
3755
3820
|
}
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
});
|
|
3821
|
+
const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
|
3822
|
+
ignoredMarkupChunks.push(group1);
|
|
3823
|
+
return token;
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3762
3826
|
|
|
3763
3827
|
// Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
|
|
3764
3828
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
@@ -3993,9 +4057,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3993
4057
|
options.sortAttributes(tag, attrs);
|
|
3994
4058
|
}
|
|
3995
4059
|
|
|
3996
|
-
const
|
|
3997
|
-
|
|
3998
|
-
);
|
|
4060
|
+
const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
|
|
4061
|
+
const normalizedAttrs = attrResults.some(isThenable) ? await Promise.all(attrResults) : attrResults;
|
|
3999
4062
|
const parts = [];
|
|
4000
4063
|
let isLast = true;
|
|
4001
4064
|
for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
|
|
@@ -4123,207 +4186,225 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4123
4186
|
}
|
|
4124
4187
|
}
|
|
4125
4188
|
},
|
|
4126
|
-
chars:
|
|
4189
|
+
chars: function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
4127
4190
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
4128
4191
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
4129
4192
|
prevAttrs = prevAttrs || [];
|
|
4130
4193
|
nextAttrs = nextAttrs || [];
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
//
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
if (
|
|
4146
|
-
|
|
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
|
+
}
|
|
4147
4215
|
}
|
|
4148
4216
|
}
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
}
|
|
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
|
+
}
|
|
4164
4232
|
}
|
|
4165
4233
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
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');
|
|
4173
4242
|
}
|
|
4174
|
-
|
|
4243
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
4244
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
4175
4245
|
}
|
|
4176
|
-
}
|
|
4177
|
-
|
|
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);
|
|
4178
4254
|
}
|
|
4179
4255
|
}
|
|
4180
|
-
if (prevTag
|
|
4181
|
-
text =
|
|
4182
|
-
} else {
|
|
4183
|
-
text = collapseWhitespace(text, options, true, true);
|
|
4256
|
+
if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
|
|
4257
|
+
text = collapseWhitespace(text, options, false, false, true);
|
|
4184
4258
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
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;
|
|
4187
4281
|
}
|
|
4188
4282
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
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, '<');
|
|
4295
|
+
}
|
|
4191
4296
|
}
|
|
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;
|
|
4305
|
+
}
|
|
4306
|
+
buffer.push(text);
|
|
4192
4307
|
}
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
}
|
|
4199
|
-
if (isStyleElement(currentTag, currentAttrs)) {
|
|
4200
|
-
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;
|
|
4201
4313
|
}
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
if (
|
|
4206
|
-
|
|
4207
|
-
}
|
|
4208
|
-
optionalStartTag = '';
|
|
4209
|
-
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
4210
|
-
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
4211
|
-
if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
|
|
4212
|
-
removeEndTag();
|
|
4314
|
+
|
|
4315
|
+
// Slow path: At least one async step required
|
|
4316
|
+
return (async () => {
|
|
4317
|
+
if (needsDecode) {
|
|
4318
|
+
text = (await getDecodeHTML())(text);
|
|
4213
4319
|
}
|
|
4214
|
-
|
|
4215
|
-
if (
|
|
4216
|
-
|
|
4217
|
-
optionalEndTagEmitted = false;
|
|
4320
|
+
text = charsCollapse(text);
|
|
4321
|
+
if (needsProcessScript) {
|
|
4322
|
+
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
4218
4323
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
4222
|
-
// Escape any `&` symbols that start either:
|
|
4223
|
-
// 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
4224
|
-
// 2. or any other character reference (i.e., one that does end with `;`)
|
|
4225
|
-
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
4226
|
-
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
4227
|
-
if (text.indexOf('&') !== -1) {
|
|
4228
|
-
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
4324
|
+
if (needsMinifyJS) {
|
|
4325
|
+
text = await options.minifyJS(text);
|
|
4229
4326
|
}
|
|
4230
|
-
if (
|
|
4231
|
-
text =
|
|
4327
|
+
if (needsMinifyCSS) {
|
|
4328
|
+
text = await options.minifyCSS(text);
|
|
4232
4329
|
}
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
4236
|
-
return ignoredCustomMarkupChunks[+index][0];
|
|
4237
|
-
});
|
|
4238
|
-
}
|
|
4239
|
-
currentChars += text;
|
|
4240
|
-
if (text) {
|
|
4241
|
-
hasChars = true;
|
|
4242
|
-
}
|
|
4243
|
-
buffer.push(text);
|
|
4330
|
+
charsFinalize(text);
|
|
4331
|
+
})();
|
|
4244
4332
|
},
|
|
4245
|
-
comment:
|
|
4333
|
+
comment: function (text, nonStandard) {
|
|
4246
4334
|
const prefix = nonStandard ? '<!' : '<!--';
|
|
4247
4335
|
const suffix = nonStandard ? '>' : '-->';
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
if (
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
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;
|
|
4255
4344
|
}
|
|
4256
|
-
} else {
|
|
4257
|
-
text = prefix + text + suffix;
|
|
4258
|
-
}
|
|
4259
|
-
if (options.removeOptionalTags && text) {
|
|
4260
|
-
// Preceding comments suppress tag omissions
|
|
4261
|
-
optionalStartTag = '';
|
|
4262
|
-
optionalEndTag = '';
|
|
4263
|
-
optionalEndTagEmitted = false;
|
|
4264
|
-
}
|
|
4265
4345
|
|
|
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
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
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
|
+
}
|
|
4322
4403
|
}
|
|
4323
|
-
}
|
|
4324
4404
|
|
|
4325
|
-
|
|
4326
|
-
|
|
4405
|
+
// Replace the whitespace in buffer
|
|
4406
|
+
buffer[buffer.length - 1] = collapsedText;
|
|
4407
|
+
}
|
|
4327
4408
|
}
|
|
4328
4409
|
}
|
|
4329
4410
|
}
|
|
@@ -4332,9 +4413,27 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4332
4413
|
}
|
|
4333
4414
|
}
|
|
4334
4415
|
}
|
|
4416
|
+
|
|
4417
|
+
buffer.push(comment);
|
|
4335
4418
|
}
|
|
4336
4419
|
|
|
4337
|
-
|
|
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
|
+
});
|
|
4425
|
+
}
|
|
4426
|
+
|
|
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);
|
|
4338
4437
|
},
|
|
4339
4438
|
doctype: function (doctype) {
|
|
4340
4439
|
buffer.push(options.useShortDoctype
|