html-minifier-next 4.16.4 → 4.17.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.
- package/README.md +57 -70
- package/cli.js +29 -26
- package/dist/htmlminifier.cjs +280 -139
- package/dist/htmlminifier.esm.bundle.js +280 -139
- 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 +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +16 -15
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +2 -2
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -2
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/htmlminifier.js +49 -47
- package/src/htmlparser.js +44 -13
- package/src/lib/attributes.js +72 -30
- package/src/lib/constants.js +46 -39
- package/src/lib/content.js +0 -1
- package/src/lib/elements.js +15 -15
- package/src/lib/options.js +26 -9
- package/src/lib/svg.js +14 -14
- package/src/lib/whitespace.js +53 -4
- package/src/presets.js +4 -5
- package/src/tokenchain.js +2 -2
- package/src/lib/index.js +0 -20
|
@@ -2617,7 +2617,7 @@ function decodeHTMLStrict(htmlString) {
|
|
|
2617
2617
|
return htmlDecoder(htmlString, DecodingMode.Strict);
|
|
2618
2618
|
}
|
|
2619
2619
|
|
|
2620
|
-
|
|
2620
|
+
/*
|
|
2621
2621
|
* HTML Parser By John Resig (ejohn.org)
|
|
2622
2622
|
* Modified by Juriy “kangax” Zaytsev
|
|
2623
2623
|
* Original code by Erik Arvidsson, Mozilla Public License
|
|
@@ -2628,10 +2628,10 @@ function decodeHTMLStrict(htmlString) {
|
|
|
2628
2628
|
* Use like so:
|
|
2629
2629
|
*
|
|
2630
2630
|
* HTMLParser(htmlString, {
|
|
2631
|
-
*
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2631
|
+
* start: function(tag, attrs, unary) {},
|
|
2632
|
+
* end: function(tag) {},
|
|
2633
|
+
* chars: function(text) {},
|
|
2634
|
+
* comment: function(text) {}
|
|
2635
2635
|
* });
|
|
2636
2636
|
*/
|
|
2637
2637
|
|
|
@@ -2654,7 +2654,7 @@ const singleAttrValues = [
|
|
|
2654
2654
|
];
|
|
2655
2655
|
// https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
|
|
2656
2656
|
const qnameCapture = (function () {
|
|
2657
|
-
//
|
|
2657
|
+
// https://www.npmjs.com/package/ncname
|
|
2658
2658
|
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';
|
|
2659
2659
|
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';
|
|
2660
2660
|
const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
|
|
@@ -2694,7 +2694,7 @@ const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base
|
|
|
2694
2694
|
const reCache = {};
|
|
2695
2695
|
|
|
2696
2696
|
// Pre-compiled regexes for common special elements (`script`, `style`, `noscript`)
|
|
2697
|
-
// These are used frequently and pre-compiling them avoids regex creation overhead
|
|
2697
|
+
// These are used frequently, and pre-compiling them avoids regex creation overhead
|
|
2698
2698
|
const preCompiledStackedTags = {
|
|
2699
2699
|
'script': /([\s\S]*?)<\/script[^>]*>/i,
|
|
2700
2700
|
'style': /([\s\S]*?)<\/style[^>]*>/i,
|
|
@@ -2757,6 +2757,7 @@ class HTMLParser {
|
|
|
2757
2757
|
// Use cached attribute regex for this handler configuration
|
|
2758
2758
|
const attribute = getAttrRegexForHandler(handler);
|
|
2759
2759
|
let prevTag = undefined, nextTag = undefined;
|
|
2760
|
+
let prevAttrs = [], nextAttrs = [];
|
|
2760
2761
|
|
|
2761
2762
|
// Index-based parsing
|
|
2762
2763
|
let pos = 0;
|
|
@@ -2800,6 +2801,7 @@ class HTMLParser {
|
|
|
2800
2801
|
}
|
|
2801
2802
|
advance(commentEnd + 3);
|
|
2802
2803
|
prevTag = '';
|
|
2804
|
+
prevAttrs = [];
|
|
2803
2805
|
continue;
|
|
2804
2806
|
}
|
|
2805
2807
|
}
|
|
@@ -2814,6 +2816,7 @@ class HTMLParser {
|
|
|
2814
2816
|
}
|
|
2815
2817
|
advance(conditionalEnd + 2);
|
|
2816
2818
|
prevTag = '';
|
|
2819
|
+
prevAttrs = [];
|
|
2817
2820
|
continue;
|
|
2818
2821
|
}
|
|
2819
2822
|
}
|
|
@@ -2826,6 +2829,7 @@ class HTMLParser {
|
|
|
2826
2829
|
}
|
|
2827
2830
|
advance(doctypeMatch[0].length);
|
|
2828
2831
|
prevTag = '';
|
|
2832
|
+
prevAttrs = [];
|
|
2829
2833
|
continue;
|
|
2830
2834
|
}
|
|
2831
2835
|
|
|
@@ -2835,6 +2839,7 @@ class HTMLParser {
|
|
|
2835
2839
|
advance(endTagMatch[0].length);
|
|
2836
2840
|
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
2837
2841
|
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
2842
|
+
prevAttrs = [];
|
|
2838
2843
|
continue;
|
|
2839
2844
|
}
|
|
2840
2845
|
|
|
@@ -2867,19 +2872,24 @@ class HTMLParser {
|
|
|
2867
2872
|
let nextTagMatch = parseStartTag(nextHtml);
|
|
2868
2873
|
if (nextTagMatch) {
|
|
2869
2874
|
nextTag = nextTagMatch.tagName;
|
|
2875
|
+
// Extract minimal attribute info for whitespace logic (just name/value pairs)
|
|
2876
|
+
nextAttrs = extractAttrInfo(nextTagMatch.attrs);
|
|
2870
2877
|
} else {
|
|
2871
2878
|
nextTagMatch = nextHtml.match(endTag);
|
|
2872
2879
|
if (nextTagMatch) {
|
|
2873
2880
|
nextTag = '/' + nextTagMatch[1];
|
|
2881
|
+
nextAttrs = [];
|
|
2874
2882
|
} else {
|
|
2875
2883
|
nextTag = '';
|
|
2884
|
+
nextAttrs = [];
|
|
2876
2885
|
}
|
|
2877
2886
|
}
|
|
2878
2887
|
|
|
2879
2888
|
if (handler.chars) {
|
|
2880
|
-
await handler.chars(text, prevTag, nextTag);
|
|
2889
|
+
await handler.chars(text, prevTag, nextTag, prevAttrs, nextAttrs);
|
|
2881
2890
|
}
|
|
2882
2891
|
prevTag = '';
|
|
2892
|
+
prevAttrs = [];
|
|
2883
2893
|
} else {
|
|
2884
2894
|
const stackedTag = lastTag.toLowerCase();
|
|
2885
2895
|
// Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
|
|
@@ -2902,7 +2912,7 @@ class HTMLParser {
|
|
|
2902
2912
|
} else {
|
|
2903
2913
|
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
2904
2914
|
if (handler.continueOnParseError && handler.chars && html) {
|
|
2905
|
-
await handler.chars(html[0], prevTag, '');
|
|
2915
|
+
await handler.chars(html[0], prevTag, '', prevAttrs, []);
|
|
2906
2916
|
advance(1);
|
|
2907
2917
|
} else {
|
|
2908
2918
|
break;
|
|
@@ -2914,10 +2924,11 @@ class HTMLParser {
|
|
|
2914
2924
|
if (handler.continueOnParseError) {
|
|
2915
2925
|
// Skip the problematic character and continue
|
|
2916
2926
|
if (handler.chars) {
|
|
2917
|
-
await handler.chars(fullHtml[pos], prevTag, '');
|
|
2927
|
+
await handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
2918
2928
|
}
|
|
2919
2929
|
advance(1);
|
|
2920
2930
|
prevTag = '';
|
|
2931
|
+
prevAttrs = [];
|
|
2921
2932
|
continue;
|
|
2922
2933
|
}
|
|
2923
2934
|
const loc = getLineColumn(pos);
|
|
@@ -2936,6 +2947,23 @@ class HTMLParser {
|
|
|
2936
2947
|
await parseEndTag();
|
|
2937
2948
|
}
|
|
2938
2949
|
|
|
2950
|
+
// Helper to extract minimal attribute info (name/value pairs) from raw attribute matches
|
|
2951
|
+
// Used for whitespace collapsing logic—doesn’t need full processing
|
|
2952
|
+
function extractAttrInfo(rawAttrs) {
|
|
2953
|
+
if (!rawAttrs || !rawAttrs.length) return [];
|
|
2954
|
+
|
|
2955
|
+
const numCustomParts = handler.customAttrSurround ? handler.customAttrSurround.length * NCP : 0;
|
|
2956
|
+
const baseIndex = 1 + numCustomParts;
|
|
2957
|
+
|
|
2958
|
+
return rawAttrs.map(args => {
|
|
2959
|
+
// Extract attribute name (always at `baseIndex`)
|
|
2960
|
+
const name = args[baseIndex];
|
|
2961
|
+
// Extract value from double-quoted (`baseIndex + 2`), single-quoted (`baseIndex + 3`), or unquoted (`baseIndex + 4`)
|
|
2962
|
+
const value = args[baseIndex + 2] ?? args[baseIndex + 3] ?? args[baseIndex + 4];
|
|
2963
|
+
return { name: name?.toLowerCase(), value };
|
|
2964
|
+
}).filter(attr => attr.name); // Filter out invalid entries
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2939
2967
|
function parseStartTag(input) {
|
|
2940
2968
|
const start = input.match(startTagOpen);
|
|
2941
2969
|
if (start) {
|
|
@@ -2948,7 +2976,7 @@ class HTMLParser {
|
|
|
2948
2976
|
input = input.slice(consumed);
|
|
2949
2977
|
let end, attr;
|
|
2950
2978
|
|
|
2951
|
-
// Safety limit:
|
|
2979
|
+
// Safety limit: Max length of input to check for attributes
|
|
2952
2980
|
// Protects against catastrophic backtracking on massive attribute values
|
|
2953
2981
|
const MAX_ATTR_PARSE_LENGTH = 20000; // 20 KB should be enough for any reasonable tag
|
|
2954
2982
|
|
|
@@ -3048,7 +3076,7 @@ class HTMLParser {
|
|
|
3048
3076
|
}
|
|
3049
3077
|
|
|
3050
3078
|
async function parseEndTagAt(pos) {
|
|
3051
|
-
// Close all open elements up to pos (mirrors parseEndTag
|
|
3079
|
+
// Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch)
|
|
3052
3080
|
for (let i = stack.length - 1; i >= pos; i--) {
|
|
3053
3081
|
if (handler.end) {
|
|
3054
3082
|
await handler.end(stack[i].tag, stack[i].attrs, true);
|
|
@@ -3116,7 +3144,7 @@ class HTMLParser {
|
|
|
3116
3144
|
const attrs = match.attrs.map(function (args) {
|
|
3117
3145
|
let name, value, customOpen, customClose, customAssign, quote;
|
|
3118
3146
|
|
|
3119
|
-
// Hackish workaround for
|
|
3147
|
+
// Hackish workaround for Firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
3120
3148
|
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
3121
3149
|
if (args[3] === '') { delete args[3]; }
|
|
3122
3150
|
if (args[4] === '') { delete args[4]; }
|
|
@@ -3173,6 +3201,9 @@ class HTMLParser {
|
|
|
3173
3201
|
unarySlash = '';
|
|
3174
3202
|
}
|
|
3175
3203
|
|
|
3204
|
+
// Store attributes for `prevAttrs` tracking (used in whitespace collapsing)
|
|
3205
|
+
prevAttrs = attrs;
|
|
3206
|
+
|
|
3176
3207
|
if (handler.start) {
|
|
3177
3208
|
await handler.start(tagName, attrs, unary, unarySlash);
|
|
3178
3209
|
}
|
|
@@ -3268,7 +3299,7 @@ class Sorter {
|
|
|
3268
3299
|
|
|
3269
3300
|
class TokenChain {
|
|
3270
3301
|
constructor() {
|
|
3271
|
-
// Use
|
|
3302
|
+
// Use map instead of object properties for better performance
|
|
3272
3303
|
this.map = new Map();
|
|
3273
3304
|
}
|
|
3274
3305
|
|
|
@@ -3285,7 +3316,7 @@ class TokenChain {
|
|
|
3285
3316
|
const sorter = new Sorter();
|
|
3286
3317
|
sorter.sorterMap = new Map();
|
|
3287
3318
|
|
|
3288
|
-
// Convert
|
|
3319
|
+
// Convert map entries to array and sort by frequency (descending), then alphabetically
|
|
3289
3320
|
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
3290
3321
|
const m = a[1].arrays.length;
|
|
3291
3322
|
const n = b[1].arrays.length;
|
|
@@ -3334,11 +3365,11 @@ class TokenChain {
|
|
|
3334
3365
|
}
|
|
3335
3366
|
|
|
3336
3367
|
/**
|
|
3337
|
-
* Preset configurations
|
|
3368
|
+
* Preset configurations
|
|
3338
3369
|
*
|
|
3339
3370
|
* Presets provide curated option sets for common use cases:
|
|
3340
|
-
* - conservative
|
|
3341
|
-
* - comprehensive
|
|
3371
|
+
* - `conservative`: Safe minification suitable for most projects
|
|
3372
|
+
* - `comprehensive`: Aggressive minification for maximum file size reduction
|
|
3342
3373
|
*/
|
|
3343
3374
|
|
|
3344
3375
|
const presets = {
|
|
@@ -3359,7 +3390,6 @@ const presets = {
|
|
|
3359
3390
|
comprehensive: {
|
|
3360
3391
|
caseSensitive: true,
|
|
3361
3392
|
collapseBooleanAttributes: true,
|
|
3362
|
-
collapseInlineTagWhitespace: true,
|
|
3363
3393
|
collapseWhitespace: true,
|
|
3364
3394
|
continueOnParseError: true,
|
|
3365
3395
|
decodeEntities: true,
|
|
@@ -3384,7 +3414,7 @@ const presets = {
|
|
|
3384
3414
|
|
|
3385
3415
|
/**
|
|
3386
3416
|
* Get preset configuration by name
|
|
3387
|
-
* @param {string} name - Preset name (
|
|
3417
|
+
* @param {string} name - Preset name (“conservative” or “comprehensive”)
|
|
3388
3418
|
* @returns {object|null} Preset options object or null if not found
|
|
3389
3419
|
*/
|
|
3390
3420
|
function getPreset(name) {
|
|
@@ -3483,7 +3513,7 @@ async function replaceAsync(str, regex, asyncFn) {
|
|
|
3483
3513
|
return str.replace(regex, () => data.shift());
|
|
3484
3514
|
}
|
|
3485
3515
|
|
|
3486
|
-
//
|
|
3516
|
+
// Regex patterns (to avoid repeated allocations in hot paths)
|
|
3487
3517
|
|
|
3488
3518
|
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
3489
3519
|
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
@@ -3504,7 +3534,7 @@ const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
|
3504
3534
|
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
3505
3535
|
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
3506
3536
|
|
|
3507
|
-
// Inline element
|
|
3537
|
+
// Inline element sets for whitespace handling
|
|
3508
3538
|
|
|
3509
3539
|
// Non-empty elements that will maintain whitespace around them
|
|
3510
3540
|
const inlineElementsToKeepWhitespaceAround = new Set(['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', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr']);
|
|
@@ -3515,6 +3545,9 @@ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b
|
|
|
3515
3545
|
// Elements that will always maintain whitespace around them
|
|
3516
3546
|
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
3517
3547
|
|
|
3548
|
+
// Form control elements (for conditional whitespace collapsing)
|
|
3549
|
+
const formControlElements = new Set(['input', 'button', 'select', 'textarea', 'output', 'meter', 'progress']);
|
|
3550
|
+
|
|
3518
3551
|
// Default attribute values
|
|
3519
3552
|
|
|
3520
3553
|
// Default attribute values (could apply to any element)
|
|
@@ -3554,14 +3587,17 @@ const tagDefaults = {
|
|
|
3554
3587
|
// Script MIME types
|
|
3555
3588
|
|
|
3556
3589
|
// https://mathiasbynens.be/demo/javascript-mime-type
|
|
3557
|
-
// https://developer.mozilla.org/en/docs/Web/HTML/
|
|
3590
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
|
|
3558
3591
|
const executableScriptsMimetypes = new Set([
|
|
3559
3592
|
'text/javascript',
|
|
3593
|
+
'text/x-javascript',
|
|
3560
3594
|
'text/ecmascript',
|
|
3595
|
+
'text/x-ecmascript',
|
|
3561
3596
|
'text/jscript',
|
|
3562
3597
|
'application/javascript',
|
|
3563
3598
|
'application/x-javascript',
|
|
3564
3599
|
'application/ecmascript',
|
|
3600
|
+
'application/x-ecmascript',
|
|
3565
3601
|
'module'
|
|
3566
3602
|
]);
|
|
3567
3603
|
|
|
@@ -3569,15 +3605,15 @@ const keepScriptsMimetypes = new Set([
|
|
|
3569
3605
|
'module'
|
|
3570
3606
|
]);
|
|
3571
3607
|
|
|
3572
|
-
// Boolean attribute
|
|
3608
|
+
// Boolean attribute sets
|
|
3573
3609
|
|
|
3574
3610
|
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']);
|
|
3575
3611
|
|
|
3576
3612
|
const isBooleanValue = new Set(['true', 'false']);
|
|
3577
3613
|
|
|
3578
|
-
// `srcset`
|
|
3614
|
+
// `srcset` elements
|
|
3579
3615
|
|
|
3580
|
-
const
|
|
3616
|
+
const srcsetElements = new Set(['img', 'source']);
|
|
3581
3617
|
|
|
3582
3618
|
// JSON script types
|
|
3583
3619
|
|
|
@@ -3593,7 +3629,7 @@ const jsonScriptTypes = new Set([
|
|
|
3593
3629
|
'speculationrules',
|
|
3594
3630
|
]);
|
|
3595
3631
|
|
|
3596
|
-
// Tag omission rules and element
|
|
3632
|
+
// Tag omission rules and element sets
|
|
3597
3633
|
|
|
3598
3634
|
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
|
|
3599
3635
|
// - retain `<body>` if followed by `<noscript>`
|
|
@@ -3604,35 +3640,35 @@ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody'])
|
|
|
3604
3640
|
|
|
3605
3641
|
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']);
|
|
3606
3642
|
|
|
3607
|
-
const
|
|
3643
|
+
const headerElements = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
|
|
3608
3644
|
|
|
3609
|
-
const
|
|
3645
|
+
const descriptionElements = new Set(['dt', 'dd']);
|
|
3610
3646
|
|
|
3611
|
-
const
|
|
3647
|
+
const pBlockElements = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
|
|
3612
3648
|
|
|
3613
|
-
const
|
|
3649
|
+
const pInlineElements = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
|
|
3614
3650
|
|
|
3615
3651
|
const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
|
|
3616
3652
|
|
|
3617
3653
|
const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
|
|
3618
3654
|
|
|
3619
|
-
const
|
|
3655
|
+
const optionElements = new Set(['option', 'optgroup']);
|
|
3620
3656
|
|
|
3621
|
-
const
|
|
3657
|
+
const tableContentElements = new Set(['tbody', 'tfoot']);
|
|
3622
3658
|
|
|
3623
|
-
const
|
|
3659
|
+
const tableSectionElements = new Set(['thead', 'tbody', 'tfoot']);
|
|
3624
3660
|
|
|
3625
|
-
const
|
|
3661
|
+
const cellElements = new Set(['td', 'th']);
|
|
3626
3662
|
|
|
3627
|
-
const
|
|
3663
|
+
const topLevelElements = new Set(['html', 'head', 'body']);
|
|
3628
3664
|
|
|
3629
|
-
const
|
|
3665
|
+
const compactElements = new Set(['html', 'body']);
|
|
3630
3666
|
|
|
3631
|
-
const
|
|
3667
|
+
const looseElements = new Set(['head', 'colgroup', 'caption']);
|
|
3632
3668
|
|
|
3633
|
-
const
|
|
3669
|
+
const trailingElements = new Set(['dt', 'thead']);
|
|
3634
3670
|
|
|
3635
|
-
const
|
|
3671
|
+
const htmlElements = 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', 'search', 'section', 'select', 'selectedcontent', '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']);
|
|
3636
3672
|
|
|
3637
3673
|
// Empty attribute regex
|
|
3638
3674
|
|
|
@@ -3642,7 +3678,7 @@ const reEmptyAttribute = new RegExp(
|
|
|
3642
3678
|
|
|
3643
3679
|
// Special content elements
|
|
3644
3680
|
|
|
3645
|
-
const
|
|
3681
|
+
const specialContentElements = new Set(['script', 'style']);
|
|
3646
3682
|
|
|
3647
3683
|
// Imports
|
|
3648
3684
|
|
|
@@ -3706,7 +3742,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
3706
3742
|
}
|
|
3707
3743
|
|
|
3708
3744
|
if (trimLeft) {
|
|
3709
|
-
//
|
|
3745
|
+
// No-break space is specifically handled inside the replacer function
|
|
3710
3746
|
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
3711
3747
|
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
3712
3748
|
if (conservative && spaces === '\t') {
|
|
@@ -3717,7 +3753,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
3717
3753
|
}
|
|
3718
3754
|
|
|
3719
3755
|
if (trimRight) {
|
|
3720
|
-
//
|
|
3756
|
+
// No-break space is specifically handled inside the replacer function
|
|
3721
3757
|
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
3722
3758
|
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
3723
3759
|
if (conservative && spaces === '\t') {
|
|
@@ -3741,11 +3777,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
3741
3777
|
|
|
3742
3778
|
// Collapse whitespace smartly based on surrounding tags
|
|
3743
3779
|
|
|
3744
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
3780
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
|
|
3781
|
+
const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
|
|
3782
|
+
const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
|
|
3783
|
+
|
|
3784
|
+
// Helper: Check if an input element has `type="hidden"`
|
|
3785
|
+
const isHiddenInput = (tagName, attrs) => {
|
|
3786
|
+
if (tagName !== 'input' || !attrs || !attrs.length) return false;
|
|
3787
|
+
const typeAttr = attrs.find(attr => attr.name === 'type');
|
|
3788
|
+
return typeAttr && typeAttr.value === 'hidden';
|
|
3789
|
+
};
|
|
3790
|
+
|
|
3791
|
+
// Check if prev/next are non-rendering (hidden) elements
|
|
3792
|
+
const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
|
|
3793
|
+
const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
|
|
3794
|
+
|
|
3745
3795
|
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
3796
|
+
|
|
3797
|
+
// Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
|
|
3798
|
+
// This happens even in basic `collapseWhitespace` mode (safe optimization)
|
|
3799
|
+
if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
|
|
3800
|
+
trimLeft = true;
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
// Aggressive mode: Collapse between all form controls (pure whitespace only)
|
|
3804
|
+
const isPureWhitespace = str && !/\S/.test(str);
|
|
3805
|
+
if (!trimLeft && prevTagName && nextTagName &&
|
|
3806
|
+
options.collapseInlineTagWhitespace &&
|
|
3807
|
+
isPureWhitespace &&
|
|
3808
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
3809
|
+
trimLeft = true;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3746
3812
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
3747
3813
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
3748
3814
|
}
|
|
3815
|
+
|
|
3749
3816
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
3750
3817
|
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
3751
3818
|
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
@@ -3753,10 +3820,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
3753
3820
|
trimLeft = false;
|
|
3754
3821
|
}
|
|
3755
3822
|
}
|
|
3823
|
+
|
|
3756
3824
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
3825
|
+
|
|
3826
|
+
// Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
|
|
3827
|
+
if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
|
|
3828
|
+
trimRight = true;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
// Aggressive mode: Same as `trimLeft`
|
|
3832
|
+
if (!trimRight && prevTagName && nextTagName &&
|
|
3833
|
+
options.collapseInlineTagWhitespace &&
|
|
3834
|
+
isPureWhitespace &&
|
|
3835
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
3836
|
+
trimRight = true;
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3757
3839
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
3758
3840
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
3759
3841
|
}
|
|
3842
|
+
|
|
3760
3843
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
3761
3844
|
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
3762
3845
|
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
@@ -3764,6 +3847,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
3764
3847
|
trimRight = false;
|
|
3765
3848
|
}
|
|
3766
3849
|
}
|
|
3850
|
+
|
|
3767
3851
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
3768
3852
|
}
|
|
3769
3853
|
|
|
@@ -6314,7 +6398,6 @@ var RelateURL = /*@__PURE__*/getDefaultExportFromCjs(libExports);
|
|
|
6314
6398
|
|
|
6315
6399
|
// Wrap CSS declarations for inline styles and media queries
|
|
6316
6400
|
// This ensures proper context for CSS minification
|
|
6317
|
-
|
|
6318
6401
|
function wrapCSS(text, type) {
|
|
6319
6402
|
switch (type) {
|
|
6320
6403
|
case 'inline':
|
|
@@ -6396,7 +6479,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
6396
6479
|
|
|
6397
6480
|
/**
|
|
6398
6481
|
* Lightweight SVG optimizations:
|
|
6399
|
-
*
|
|
6400
6482
|
* - Numeric precision reduction for coordinates and path data
|
|
6401
6483
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
6402
6484
|
* - Default attribute removal (safe, well-documented defaults)
|
|
@@ -6490,7 +6572,7 @@ function minifyNumber(num, precision = 3) {
|
|
|
6490
6572
|
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
6491
6573
|
|
|
6492
6574
|
// Check cache
|
|
6493
|
-
// (Note:
|
|
6575
|
+
// (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
6494
6576
|
// This is intentional to avoid parsing overhead.
|
|
6495
6577
|
// Real-world SVG files from export tools typically use consistent formats.)
|
|
6496
6578
|
const cacheKey = `${num}:${precision}`;
|
|
@@ -6529,15 +6611,15 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
6529
6611
|
|
|
6530
6612
|
// Remove unnecessary spaces around path commands
|
|
6531
6613
|
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
6532
|
-
// M 10 20 → M10 20
|
|
6614
|
+
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`
|
|
6533
6615
|
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
6534
6616
|
|
|
6535
6617
|
// Safe to remove space before command letter when preceded by a number
|
|
6536
|
-
// 0 L → 0L
|
|
6618
|
+
// `0 L` → `0L`, `20 M` → `20M`
|
|
6537
6619
|
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
6538
6620
|
|
|
6539
6621
|
// Safe to remove space before negative number when preceded by a number
|
|
6540
|
-
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
6622
|
+
// `10 -20` → `10-20` (numbers are separated by the minus sign)
|
|
6541
6623
|
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
6542
6624
|
|
|
6543
6625
|
return result;
|
|
@@ -6546,9 +6628,9 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
6546
6628
|
/**
|
|
6547
6629
|
* Minify whitespace in numeric attribute values
|
|
6548
6630
|
* Examples:
|
|
6549
|
-
*
|
|
6550
|
-
*
|
|
6551
|
-
*
|
|
6631
|
+
* - “10 , 20" → "10,20"
|
|
6632
|
+
* - "translate( 10 20 )" → "translate(10 20)"
|
|
6633
|
+
* - "100, 10 40, 198" → "100,10 40,198"
|
|
6552
6634
|
*
|
|
6553
6635
|
* @param {string} value - Attribute value to minify
|
|
6554
6636
|
* @returns {string} Minified value
|
|
@@ -6581,8 +6663,7 @@ function minifyColor(color) {
|
|
|
6581
6663
|
|
|
6582
6664
|
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
6583
6665
|
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
6584
|
-
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
6585
|
-
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
6666
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
6586
6667
|
return trimmed;
|
|
6587
6668
|
}
|
|
6588
6669
|
|
|
@@ -6590,7 +6671,7 @@ function minifyColor(color) {
|
|
|
6590
6671
|
const lower = trimmed.toLowerCase();
|
|
6591
6672
|
|
|
6592
6673
|
// Shorten 6-digit hex to 3-digit when possible
|
|
6593
|
-
//
|
|
6674
|
+
// `#aabbcc` → `#abc`, `#000000` → `#000`
|
|
6594
6675
|
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
6595
6676
|
if (hexMatch) {
|
|
6596
6677
|
const hex = hexMatch[1];
|
|
@@ -6610,7 +6691,7 @@ function minifyColor(color) {
|
|
|
6610
6691
|
return NAMED_COLORS[lower] || lower;
|
|
6611
6692
|
}
|
|
6612
6693
|
|
|
6613
|
-
// Convert rgb(
|
|
6694
|
+
// Convert rgb() to hex
|
|
6614
6695
|
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
6615
6696
|
if (rgbMatch) {
|
|
6616
6697
|
const r = parseInt(rgbMatch[1], 10);
|
|
@@ -6641,14 +6722,14 @@ function minifyColor(color) {
|
|
|
6641
6722
|
const NUMERIC_ATTRS = new Set([
|
|
6642
6723
|
'd', // Path data
|
|
6643
6724
|
'points', // Polygon/polyline points
|
|
6644
|
-
'viewBox', // viewBox coordinates
|
|
6725
|
+
'viewBox', // `viewBox` coordinates
|
|
6645
6726
|
'transform', // Transform functions
|
|
6646
6727
|
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
6647
6728
|
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
6648
6729
|
'width', 'height', // Dimensions
|
|
6649
6730
|
'dx', 'dy', // Text offsets
|
|
6650
6731
|
'offset', // Gradient offset
|
|
6651
|
-
'startOffset', // textPath
|
|
6732
|
+
'startOffset', // `textPath`
|
|
6652
6733
|
'pathLength', // Path length
|
|
6653
6734
|
'stdDeviation', // Filter params
|
|
6654
6735
|
'baseFrequency', // Turbulence
|
|
@@ -6832,8 +6913,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6832
6913
|
/**
|
|
6833
6914
|
* @param {Partial<MinifierOptions>} inputOptions - User-provided options
|
|
6834
6915
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
6835
|
-
* @param {Function} deps.getLightningCSS - Function to lazily load
|
|
6836
|
-
* @param {Function} deps.getTerser - Function to lazily load
|
|
6916
|
+
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
6917
|
+
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
6837
6918
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
6838
6919
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
6839
6920
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
@@ -6870,7 +6951,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6870
6951
|
if (typeof value === 'string') {
|
|
6871
6952
|
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
6872
6953
|
}
|
|
6873
|
-
return value; // Already a RegExp or
|
|
6954
|
+
return value; // Already a RegExp or another type
|
|
6874
6955
|
};
|
|
6875
6956
|
|
|
6876
6957
|
const parseRegExpArray = (arr) => {
|
|
@@ -6890,9 +6971,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6890
6971
|
});
|
|
6891
6972
|
};
|
|
6892
6973
|
|
|
6974
|
+
// Apply preset first if specified (so user options can override preset values)
|
|
6975
|
+
if (inputOptions.preset) {
|
|
6976
|
+
const preset = getPreset(inputOptions.preset);
|
|
6977
|
+
if (preset) {
|
|
6978
|
+
Object.assign(options, preset);
|
|
6979
|
+
} else {
|
|
6980
|
+
const available = getPresetNames().join(', ');
|
|
6981
|
+
console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
|
|
6893
6985
|
Object.keys(inputOptions).forEach(function (key) {
|
|
6894
6986
|
const option = inputOptions[key];
|
|
6895
6987
|
|
|
6988
|
+
// Skip preset key—it’s already been processed
|
|
6989
|
+
if (key === 'preset') {
|
|
6990
|
+
return;
|
|
6991
|
+
}
|
|
6992
|
+
|
|
6896
6993
|
if (key === 'caseSensitive') {
|
|
6897
6994
|
if (option) {
|
|
6898
6995
|
options.name = identity;
|
|
@@ -7007,7 +7104,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7007
7104
|
// Validate engine
|
|
7008
7105
|
const supportedEngines = ['terser', 'swc'];
|
|
7009
7106
|
if (!supportedEngines.includes(engine)) {
|
|
7010
|
-
throw new Error(`Unsupported JS minifier engine:
|
|
7107
|
+
throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
|
|
7011
7108
|
}
|
|
7012
7109
|
|
|
7013
7110
|
// Extract engine-specific options (excluding `engine` field itself)
|
|
@@ -7114,14 +7211,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7114
7211
|
relateUrlOptions = {};
|
|
7115
7212
|
}
|
|
7116
7213
|
|
|
7117
|
-
// Cache
|
|
7214
|
+
// Cache relateurl instance for reuse (expensive to create)
|
|
7118
7215
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
7119
7216
|
|
|
7120
7217
|
// Create instance-specific cache (results depend on site configuration)
|
|
7121
7218
|
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
7122
7219
|
|
|
7123
7220
|
options.minifyURLs = function (text) {
|
|
7124
|
-
// Fast-path: Skip if text doesn
|
|
7221
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
7125
7222
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
7126
7223
|
if (!/[/:?#\s]/.test(text)) {
|
|
7127
7224
|
return text;
|
|
@@ -7153,17 +7250,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7153
7250
|
};
|
|
7154
7251
|
} else if (key === 'minifySVG') {
|
|
7155
7252
|
// Process SVG minification options
|
|
7156
|
-
// Unlike minifyCSS
|
|
7253
|
+
// Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
|
|
7157
7254
|
// The actual minification is applied inline during attribute processing
|
|
7158
7255
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
7159
7256
|
} else if (key === 'customAttrCollapse') {
|
|
7160
|
-
// Single
|
|
7257
|
+
// Single regex pattern
|
|
7161
7258
|
options[key] = parseRegExp(option);
|
|
7162
7259
|
} else if (key === 'customAttrSurround') {
|
|
7163
7260
|
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
7164
7261
|
options[key] = parseNestedRegExpArray(option);
|
|
7165
7262
|
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
7166
|
-
// Array of
|
|
7263
|
+
// Array of regex patterns
|
|
7167
7264
|
options[key] = parseRegExpArray(option);
|
|
7168
7265
|
} else {
|
|
7169
7266
|
options[key] = option;
|
|
@@ -7228,8 +7325,7 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
7228
7325
|
const tagHasDefaults = tag in tagDefaults;
|
|
7229
7326
|
|
|
7230
7327
|
// Check for legacy attribute rules (element- and attribute-specific)
|
|
7231
|
-
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
|
|
7232
|
-
(tag === 'a' && attrName === 'name');
|
|
7328
|
+
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) || (tag === 'a' && attrName === 'name');
|
|
7233
7329
|
|
|
7234
7330
|
// If none of these conditions apply, attribute cannot be redundant
|
|
7235
7331
|
if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
|
|
@@ -7287,7 +7383,7 @@ function isStyleLinkTypeAttribute(attrValue = '') {
|
|
|
7287
7383
|
return attrValue === '' || attrValue === 'text/css';
|
|
7288
7384
|
}
|
|
7289
7385
|
|
|
7290
|
-
function
|
|
7386
|
+
function isStyleElement(tag, attrs) {
|
|
7291
7387
|
if (tag !== 'style') {
|
|
7292
7388
|
return false;
|
|
7293
7389
|
}
|
|
@@ -7344,11 +7440,11 @@ function isLinkType(tag, attrs, value) {
|
|
|
7344
7440
|
}
|
|
7345
7441
|
|
|
7346
7442
|
function isMediaQuery(tag, attrs, attrName) {
|
|
7347
|
-
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') ||
|
|
7443
|
+
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
|
|
7348
7444
|
}
|
|
7349
7445
|
|
|
7350
7446
|
function isSrcset(attrName, tag) {
|
|
7351
|
-
return attrName === 'srcset' &&
|
|
7447
|
+
return attrName === 'srcset' && srcsetElements.has(tag);
|
|
7352
7448
|
}
|
|
7353
7449
|
|
|
7354
7450
|
function isMetaViewport(tag, attrs) {
|
|
@@ -7356,7 +7452,7 @@ function isMetaViewport(tag, attrs) {
|
|
|
7356
7452
|
return false;
|
|
7357
7453
|
}
|
|
7358
7454
|
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
7359
|
-
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
7455
|
+
if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
|
|
7360
7456
|
return true;
|
|
7361
7457
|
}
|
|
7362
7458
|
}
|
|
@@ -7376,7 +7472,7 @@ function isContentSecurityPolicy(tag, attrs) {
|
|
|
7376
7472
|
}
|
|
7377
7473
|
|
|
7378
7474
|
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
7379
|
-
const isValueEmpty = !attrValue ||
|
|
7475
|
+
const isValueEmpty = !attrValue || attrValue.trim() === '';
|
|
7380
7476
|
if (!isValueEmpty) {
|
|
7381
7477
|
return false;
|
|
7382
7478
|
}
|
|
@@ -7399,7 +7495,7 @@ function hasAttrName(name, attrs) {
|
|
|
7399
7495
|
|
|
7400
7496
|
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
7401
7497
|
// Apply early whitespace normalization if enabled
|
|
7402
|
-
// Preserves special spaces (
|
|
7498
|
+
// Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
7403
7499
|
if (options.collapseAttributeWhitespace) {
|
|
7404
7500
|
// Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
|
|
7405
7501
|
if (RE_ATTR_WS_CHECK.test(attrValue)) {
|
|
@@ -7455,7 +7551,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
7455
7551
|
try {
|
|
7456
7552
|
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
7457
7553
|
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
7458
|
-
//
|
|
7554
|
+
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
7459
7555
|
if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
|
|
7460
7556
|
attrValue = '';
|
|
7461
7557
|
}
|
|
@@ -7575,13 +7671,13 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7575
7671
|
}
|
|
7576
7672
|
|
|
7577
7673
|
if ((options.removeRedundantAttributes &&
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7674
|
+
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
7675
|
+
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
7676
|
+
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
7677
|
+
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
7678
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
7679
|
+
(options.insideSVG && options.minifySVG &&
|
|
7680
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
7585
7681
|
return;
|
|
7586
7682
|
}
|
|
7587
7683
|
|
|
@@ -7590,7 +7686,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7590
7686
|
}
|
|
7591
7687
|
|
|
7592
7688
|
if (options.removeEmptyAttributes &&
|
|
7593
|
-
|
|
7689
|
+
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
7594
7690
|
return;
|
|
7595
7691
|
}
|
|
7596
7692
|
|
|
@@ -7613,19 +7709,35 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7613
7709
|
let attrFragment;
|
|
7614
7710
|
let emittedAttrValue;
|
|
7615
7711
|
|
|
7616
|
-
|
|
7617
|
-
|
|
7712
|
+
// Determine if we need to add/keep quotes
|
|
7713
|
+
const shouldAddQuotes = typeof attrValue !== 'undefined' && (
|
|
7714
|
+
// If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
|
|
7715
|
+
(options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
|
|
7716
|
+
// If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
|
|
7717
|
+
(!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
|
|
7718
|
+
// Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
|
|
7719
|
+
// which can create ambiguous/invalid HTML—add quotes to be safe
|
|
7720
|
+
(options.removeTagWhitespace && attrQuote === '' && !isLast)))
|
|
7721
|
+
);
|
|
7722
|
+
|
|
7723
|
+
if (shouldAddQuotes) {
|
|
7618
7724
|
// Determine the appropriate quote character
|
|
7619
7725
|
if (!options.preventAttributesEscaping) {
|
|
7620
|
-
// Normal mode:
|
|
7621
|
-
|
|
7726
|
+
// Normal mode: Choose optimal quote type to minimize escaping
|
|
7727
|
+
// unless we’re preserving original quotes and they don’t need escaping
|
|
7728
|
+
const needsEscaping = (attrQuote === '"' && attrValue.indexOf('"') !== -1) || (attrQuote === "'" && attrValue.indexOf("'") !== -1);
|
|
7729
|
+
|
|
7730
|
+
if (options.removeAttributeQuotes || typeof options.quoteCharacter !== 'undefined' || needsEscaping || attrQuote === '') {
|
|
7731
|
+
attrQuote = chooseAttributeQuote(attrValue, options);
|
|
7732
|
+
}
|
|
7733
|
+
|
|
7622
7734
|
if (attrQuote === '"') {
|
|
7623
7735
|
attrValue = attrValue.replace(/"/g, '"');
|
|
7624
7736
|
} else {
|
|
7625
7737
|
attrValue = attrValue.replace(/'/g, ''');
|
|
7626
7738
|
}
|
|
7627
7739
|
} else {
|
|
7628
|
-
// `preventAttributesEscaping` mode:
|
|
7740
|
+
// `preventAttributesEscaping` mode: Choose safe quotes but don't escape
|
|
7629
7741
|
// except when both quote types are present—then escape to prevent invalid HTML
|
|
7630
7742
|
const hasDoubleQuote = attrValue.indexOf('"') !== -1;
|
|
7631
7743
|
const hasSingleQuote = attrValue.indexOf("'") !== -1;
|
|
@@ -7644,8 +7756,18 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7644
7756
|
attrQuote = "'";
|
|
7645
7757
|
} else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
|
|
7646
7758
|
attrQuote = '"';
|
|
7647
|
-
//
|
|
7648
|
-
} else if (attrQuote
|
|
7759
|
+
// If no quote character yet (empty string), choose based on content
|
|
7760
|
+
} else if (attrQuote === '') {
|
|
7761
|
+
if (hasSingleQuote && !hasDoubleQuote) {
|
|
7762
|
+
attrQuote = '"';
|
|
7763
|
+
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
7764
|
+
attrQuote = "'";
|
|
7765
|
+
} else {
|
|
7766
|
+
attrQuote = '"';
|
|
7767
|
+
}
|
|
7768
|
+
// Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string):
|
|
7769
|
+
// Choose safe default based on value content
|
|
7770
|
+
} else if (attrQuote !== '"' && attrQuote !== "'") {
|
|
7649
7771
|
if (hasSingleQuote && !hasDoubleQuote) {
|
|
7650
7772
|
attrQuote = '"';
|
|
7651
7773
|
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
@@ -7655,7 +7777,22 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7655
7777
|
}
|
|
7656
7778
|
}
|
|
7657
7779
|
} else {
|
|
7658
|
-
|
|
7780
|
+
// `quoteCharacter` is explicitly set
|
|
7781
|
+
const preferredQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
7782
|
+
// Safety check: If the preferred quote conflicts with value content, switch to the opposite quote
|
|
7783
|
+
if ((preferredQuote === '"' && hasDoubleQuote && !hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && !hasDoubleQuote)) {
|
|
7784
|
+
attrQuote = preferredQuote === '"' ? "'" : '"';
|
|
7785
|
+
} else if ((preferredQuote === '"' && hasDoubleQuote && hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && hasDoubleQuote)) {
|
|
7786
|
+
// Both quote types present: Fall back to escaping despite `preventAttributesEscaping`
|
|
7787
|
+
attrQuote = preferredQuote;
|
|
7788
|
+
if (attrQuote === '"') {
|
|
7789
|
+
attrValue = attrValue.replace(/"/g, '"');
|
|
7790
|
+
} else {
|
|
7791
|
+
attrValue = attrValue.replace(/'/g, ''');
|
|
7792
|
+
}
|
|
7793
|
+
} else {
|
|
7794
|
+
attrQuote = preferredQuote;
|
|
7795
|
+
}
|
|
7659
7796
|
}
|
|
7660
7797
|
}
|
|
7661
7798
|
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
@@ -7663,15 +7800,17 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7663
7800
|
emittedAttrValue += ' ';
|
|
7664
7801
|
}
|
|
7665
7802
|
} else if (isLast && !hasUnarySlash) {
|
|
7666
|
-
// Last attribute in a non-self-closing tag:
|
|
7803
|
+
// Last attribute in a non-self-closing tag:
|
|
7804
|
+
// No space needed
|
|
7667
7805
|
emittedAttrValue = attrValue;
|
|
7668
7806
|
} else {
|
|
7669
|
-
// Not last attribute, or is a self-closing tag:
|
|
7807
|
+
// Not last attribute, or is a self-closing tag:
|
|
7808
|
+
// Unquoted values must have space after them to delimit from next attribute
|
|
7670
7809
|
emittedAttrValue = attrValue + ' ';
|
|
7671
7810
|
}
|
|
7672
7811
|
|
|
7673
7812
|
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
7674
|
-
|
|
7813
|
+
isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
|
|
7675
7814
|
attrFragment = attrName;
|
|
7676
7815
|
if (!isLast) {
|
|
7677
7816
|
attrFragment += ' ';
|
|
@@ -7694,7 +7833,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
|
|
|
7694
7833
|
case 'head':
|
|
7695
7834
|
return true;
|
|
7696
7835
|
case 'body':
|
|
7697
|
-
return !
|
|
7836
|
+
return !headerElements.has(tag);
|
|
7698
7837
|
case 'colgroup':
|
|
7699
7838
|
return tag === 'col';
|
|
7700
7839
|
case 'tbody':
|
|
@@ -7708,7 +7847,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
|
|
|
7708
7847
|
case 'colgroup':
|
|
7709
7848
|
return optionalEndTag === 'colgroup';
|
|
7710
7849
|
case 'tbody':
|
|
7711
|
-
return
|
|
7850
|
+
return tableSectionElements.has(optionalEndTag);
|
|
7712
7851
|
}
|
|
7713
7852
|
return false;
|
|
7714
7853
|
}
|
|
@@ -7727,9 +7866,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
7727
7866
|
return tag === optionalEndTag;
|
|
7728
7867
|
case 'dt':
|
|
7729
7868
|
case 'dd':
|
|
7730
|
-
return
|
|
7869
|
+
return descriptionElements.has(tag);
|
|
7731
7870
|
case 'p':
|
|
7732
|
-
return
|
|
7871
|
+
return pBlockElements.has(tag);
|
|
7733
7872
|
case 'rb':
|
|
7734
7873
|
case 'rt':
|
|
7735
7874
|
case 'rp':
|
|
@@ -7737,15 +7876,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
7737
7876
|
case 'rtc':
|
|
7738
7877
|
return rubyRtcEndTagOmission.has(tag);
|
|
7739
7878
|
case 'option':
|
|
7740
|
-
return
|
|
7879
|
+
return optionElements.has(tag);
|
|
7741
7880
|
case 'thead':
|
|
7742
7881
|
case 'tbody':
|
|
7743
|
-
return
|
|
7882
|
+
return tableContentElements.has(tag);
|
|
7744
7883
|
case 'tfoot':
|
|
7745
7884
|
return tag === 'tbody';
|
|
7746
7885
|
case 'td':
|
|
7747
7886
|
case 'th':
|
|
7748
|
-
return
|
|
7887
|
+
return cellElements.has(tag);
|
|
7749
7888
|
}
|
|
7750
7889
|
return false;
|
|
7751
7890
|
}
|
|
@@ -7848,7 +7987,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
7848
7987
|
if (typeof item === 'string') {
|
|
7849
7988
|
const spec = parseElementSpec(item, options);
|
|
7850
7989
|
if (!spec && options.log) {
|
|
7851
|
-
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification:
|
|
7990
|
+
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: “' + item + '”');
|
|
7852
7991
|
}
|
|
7853
7992
|
return spec;
|
|
7854
7993
|
}
|
|
@@ -8361,7 +8500,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8361
8500
|
}
|
|
8362
8501
|
|
|
8363
8502
|
// Pre-compile regex patterns for reuse (performance optimization)
|
|
8364
|
-
// These must be declared before scan() since scan uses them
|
|
8503
|
+
// These must be declared before `scan()` since scan uses them
|
|
8365
8504
|
const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
|
|
8366
8505
|
const whitespaceSplitPatternSort = /[ \n\f\r]+/;
|
|
8367
8506
|
|
|
@@ -8393,9 +8532,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8393
8532
|
chars: async function (text) {
|
|
8394
8533
|
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
8395
8534
|
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
8396
|
-
if (options.processScripts &&
|
|
8397
|
-
|
|
8398
|
-
|
|
8535
|
+
if (options.processScripts && specialContentElements.has(currentTag) &&
|
|
8536
|
+
options.processScripts.indexOf(currentType) > -1 &&
|
|
8537
|
+
currentType === 'text/html') {
|
|
8399
8538
|
await scan(text);
|
|
8400
8539
|
}
|
|
8401
8540
|
},
|
|
@@ -8418,7 +8557,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8418
8557
|
// For the first pass, create a copy of options and disable aggressive minification.
|
|
8419
8558
|
// Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
|
|
8420
8559
|
// This is safe because `createSortFns` is called before custom fragment UID markers (`uidAttr`) are added.
|
|
8421
|
-
// Note: `htmlmin:ignore` UID markers (uidIgnore) already exist and are expanded for analysis.
|
|
8560
|
+
// Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
|
|
8422
8561
|
const firstPassOptions = Object.assign({}, options, {
|
|
8423
8562
|
// Disable sorting for the analysis pass
|
|
8424
8563
|
sortAttributes: false,
|
|
@@ -8437,7 +8576,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8437
8576
|
});
|
|
8438
8577
|
|
|
8439
8578
|
// Temporarily enable `continueOnParseError` for the `scan()` function call below.
|
|
8440
|
-
// Note: `firstPassOptions` already has `continueOnParseError: true` for the minifyHTML call.
|
|
8579
|
+
// Note: `firstPassOptions` already has `continueOnParseError: true` for the `minifyHTML` call.
|
|
8441
8580
|
const originalContinueOnParseError = options.continueOnParseError;
|
|
8442
8581
|
options.continueOnParseError = true;
|
|
8443
8582
|
|
|
@@ -8450,7 +8589,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8450
8589
|
: null;
|
|
8451
8590
|
|
|
8452
8591
|
try {
|
|
8453
|
-
// Expand UID tokens back to original content for frequency analysis
|
|
8592
|
+
// Expand UID tokens back to the original content for frequency analysis
|
|
8454
8593
|
let expandedValue = value;
|
|
8455
8594
|
if (uidReplacePattern) {
|
|
8456
8595
|
expandedValue = value.replace(uidReplacePattern, function (match, index) {
|
|
@@ -8499,7 +8638,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8499
8638
|
attrOrderCache.set(cacheKey, sortedNames);
|
|
8500
8639
|
}
|
|
8501
8640
|
|
|
8502
|
-
// Apply the sorted order to attrs
|
|
8641
|
+
// Apply the sorted order to `attrs`
|
|
8503
8642
|
const attrMap = Object.create(null);
|
|
8504
8643
|
names.forEach(function (name, index) {
|
|
8505
8644
|
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
@@ -8586,7 +8725,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8586
8725
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
8587
8726
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
8588
8727
|
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
8589
|
-
// Fast path: Reuse base
|
|
8728
|
+
// Fast path: Reuse base sets if no custom elements
|
|
8590
8729
|
const inlineTextSet = normalizedCustomElements.length
|
|
8591
8730
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
8592
8731
|
: inlineElementsToKeepWhitespaceWithin;
|
|
@@ -8606,7 +8745,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8606
8745
|
}
|
|
8607
8746
|
|
|
8608
8747
|
// Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there.
|
|
8609
|
-
// For all we care there might be completely-horribly-broken-alien-non-html-
|
|
8748
|
+
// For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
|
|
8610
8749
|
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
8611
8750
|
if (!uidIgnore) {
|
|
8612
8751
|
uidIgnore = uniqueId(value);
|
|
@@ -8627,7 +8766,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8627
8766
|
// Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
|
|
8628
8767
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
8629
8768
|
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
8630
|
-
|
|
8769
|
+
(options.sortClassName && typeof options.sortClassName !== 'function')) {
|
|
8631
8770
|
await createSortFns(value, options, uidIgnore, null, ignoredMarkupChunks);
|
|
8632
8771
|
}
|
|
8633
8772
|
|
|
@@ -8689,11 +8828,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8689
8828
|
});
|
|
8690
8829
|
}
|
|
8691
8830
|
|
|
8692
|
-
function
|
|
8831
|
+
function canCollapseWhitespace$1(tag, attrs) {
|
|
8693
8832
|
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
8694
8833
|
}
|
|
8695
8834
|
|
|
8696
|
-
function
|
|
8835
|
+
function canTrimWhitespace$1(tag, attrs) {
|
|
8697
8836
|
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
8698
8837
|
}
|
|
8699
8838
|
|
|
@@ -8715,12 +8854,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8715
8854
|
|
|
8716
8855
|
// Look for trailing whitespaces, bypass any inline tags
|
|
8717
8856
|
function trimTrailingWhitespace(index, nextTag) {
|
|
8718
|
-
for (let endTag = null; index >= 0 &&
|
|
8857
|
+
for (let endTag = null; index >= 0 && canTrimWhitespace$1(endTag); index--) {
|
|
8719
8858
|
const str = buffer[index];
|
|
8720
8859
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
8721
8860
|
if (match) {
|
|
8722
8861
|
endTag = match[1];
|
|
8723
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
8862
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, [], [], options, inlineElements, inlineTextSet))) {
|
|
8724
8863
|
break;
|
|
8725
8864
|
}
|
|
8726
8865
|
}
|
|
@@ -8769,10 +8908,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8769
8908
|
|
|
8770
8909
|
let optional = options.removeOptionalTags;
|
|
8771
8910
|
if (optional) {
|
|
8772
|
-
const htmlTag =
|
|
8911
|
+
const htmlTag = htmlElements.has(tag);
|
|
8773
8912
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
8774
8913
|
// `<head>` may be omitted if first thing inside is an element
|
|
8775
|
-
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`,
|
|
8914
|
+
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
|
|
8776
8915
|
// `<colgroup>` may be omitted if first thing inside is `<col>`
|
|
8777
8916
|
// `<tbody>` may be omitted if first thing inside is `<tr>`
|
|
8778
8917
|
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
@@ -8789,16 +8928,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8789
8928
|
optionalEndTag = '';
|
|
8790
8929
|
}
|
|
8791
8930
|
|
|
8792
|
-
// Set whitespace flags for nested tags (e.g.,
|
|
8931
|
+
// Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
|
|
8793
8932
|
if (options.collapseWhitespace) {
|
|
8794
8933
|
if (!stackNoTrimWhitespace.length) {
|
|
8795
8934
|
squashTrailingWhitespace(tag);
|
|
8796
8935
|
}
|
|
8797
8936
|
if (!unary) {
|
|
8798
|
-
if (!
|
|
8937
|
+
if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
8799
8938
|
stackNoTrimWhitespace.push(tag);
|
|
8800
8939
|
}
|
|
8801
|
-
if (!
|
|
8940
|
+
if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
8802
8941
|
stackNoCollapseWhitespace.push(tag);
|
|
8803
8942
|
}
|
|
8804
8943
|
}
|
|
@@ -8854,7 +8993,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8854
8993
|
squashTrailingWhitespace('/' + tag);
|
|
8855
8994
|
}
|
|
8856
8995
|
if (stackNoCollapseWhitespace.length &&
|
|
8857
|
-
|
|
8996
|
+
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
8858
8997
|
stackNoCollapseWhitespace.pop();
|
|
8859
8998
|
}
|
|
8860
8999
|
}
|
|
@@ -8867,7 +9006,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8867
9006
|
|
|
8868
9007
|
if (options.removeOptionalTags) {
|
|
8869
9008
|
// `<html>`, `<head>` or `<body>` may be omitted if the element is empty
|
|
8870
|
-
if (isElementEmpty &&
|
|
9009
|
+
if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
|
|
8871
9010
|
removeStartTag();
|
|
8872
9011
|
}
|
|
8873
9012
|
optionalStartTag = '';
|
|
@@ -8875,7 +9014,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8875
9014
|
// `</head>` may be omitted if not followed by space or comment
|
|
8876
9015
|
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
8877
9016
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
8878
|
-
if (tag && optionalEndTag && !
|
|
9017
|
+
if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
|
|
8879
9018
|
removeEndTag();
|
|
8880
9019
|
}
|
|
8881
9020
|
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
@@ -8922,10 +9061,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8922
9061
|
}
|
|
8923
9062
|
}
|
|
8924
9063
|
},
|
|
8925
|
-
chars: async function (text, prevTag, nextTag) {
|
|
9064
|
+
chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
8926
9065
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
8927
9066
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
8928
|
-
|
|
9067
|
+
prevAttrs = prevAttrs || [];
|
|
9068
|
+
nextAttrs = nextAttrs || [];
|
|
9069
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
8929
9070
|
if (text.indexOf('&') !== -1) {
|
|
8930
9071
|
text = decodeHTML(text);
|
|
8931
9072
|
}
|
|
@@ -8961,7 +9102,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8961
9102
|
}
|
|
8962
9103
|
}
|
|
8963
9104
|
if (prevTag || nextTag) {
|
|
8964
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
9105
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
|
|
8965
9106
|
} else {
|
|
8966
9107
|
text = collapseWhitespace(text, options, true, true);
|
|
8967
9108
|
}
|
|
@@ -8973,13 +9114,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8973
9114
|
text = collapseWhitespace(text, options, false, false, true);
|
|
8974
9115
|
}
|
|
8975
9116
|
}
|
|
8976
|
-
if (
|
|
9117
|
+
if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
|
|
8977
9118
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
8978
9119
|
}
|
|
8979
9120
|
if (isExecutableScript(currentTag, currentAttrs)) {
|
|
8980
9121
|
text = await options.minifyJS(text);
|
|
8981
9122
|
}
|
|
8982
|
-
if (
|
|
9123
|
+
if (isStyleElement(currentTag, currentAttrs)) {
|
|
8983
9124
|
text = await options.minifyCSS(text);
|
|
8984
9125
|
}
|
|
8985
9126
|
if (options.removeOptionalTags && text) {
|
|
@@ -8991,7 +9132,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8991
9132
|
optionalStartTag = '';
|
|
8992
9133
|
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
8993
9134
|
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
8994
|
-
if (
|
|
9135
|
+
if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
8995
9136
|
removeEndTag();
|
|
8996
9137
|
}
|
|
8997
9138
|
// Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
|
|
@@ -9000,11 +9141,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9000
9141
|
}
|
|
9001
9142
|
}
|
|
9002
9143
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
9003
|
-
if (options.decodeEntities && text && !
|
|
9144
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
9004
9145
|
// Escape any `&` symbols that start either:
|
|
9005
|
-
// 1) a legacy
|
|
9146
|
+
// 1) a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
9006
9147
|
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
9007
|
-
// Note that `&` can be escaped as `&`, without the
|
|
9148
|
+
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
9008
9149
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
9009
9150
|
if (text.indexOf('&') !== -1) {
|
|
9010
9151
|
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
@@ -9069,7 +9210,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9069
9210
|
|
|
9070
9211
|
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
9071
9212
|
// Don’t collapse if either contains plain text, as that would change meaning
|
|
9072
|
-
// Note: This check will match HTML comments (`<!-- … -->`), but the tag
|
|
9213
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag name
|
|
9073
9214
|
// regex below requires starting with a letter, so comments are intentionally
|
|
9074
9215
|
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
9075
9216
|
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
@@ -9130,11 +9271,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9130
9271
|
if (options.removeOptionalTags) {
|
|
9131
9272
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
9132
9273
|
// `<head>` or `<body>` may be omitted if empty
|
|
9133
|
-
if (
|
|
9274
|
+
if (topLevelElements.has(optionalStartTag)) {
|
|
9134
9275
|
removeStartTag();
|
|
9135
9276
|
}
|
|
9136
9277
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
9137
|
-
if (optionalEndTag && !
|
|
9278
|
+
if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
|
|
9138
9279
|
removeEndTag();
|
|
9139
9280
|
}
|
|
9140
9281
|
}
|