html-minifier-next 4.16.4 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -70
- package/cli.js +27 -25
- package/dist/htmlminifier.cjs +264 -138
- package/dist/htmlminifier.esm.bundle.js +264 -138
- 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/whitespace.d.ts +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -1
- package/package.json +8 -7
- 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 +9 -9
- package/src/lib/svg.js +14 -14
- package/src/lib/whitespace.js +53 -4
- package/src/presets.js +4 -4
- 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 = {
|
|
@@ -3384,7 +3415,7 @@ const presets = {
|
|
|
3384
3415
|
|
|
3385
3416
|
/**
|
|
3386
3417
|
* Get preset configuration by name
|
|
3387
|
-
* @param {string} name - Preset name (
|
|
3418
|
+
* @param {string} name - Preset name (“conservative” or “comprehensive”)
|
|
3388
3419
|
* @returns {object|null} Preset options object or null if not found
|
|
3389
3420
|
*/
|
|
3390
3421
|
function getPreset(name) {
|
|
@@ -3483,7 +3514,7 @@ async function replaceAsync(str, regex, asyncFn) {
|
|
|
3483
3514
|
return str.replace(regex, () => data.shift());
|
|
3484
3515
|
}
|
|
3485
3516
|
|
|
3486
|
-
//
|
|
3517
|
+
// Regex patterns (to avoid repeated allocations in hot paths)
|
|
3487
3518
|
|
|
3488
3519
|
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
3489
3520
|
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
@@ -3504,7 +3535,7 @@ const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
|
3504
3535
|
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
3505
3536
|
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
3506
3537
|
|
|
3507
|
-
// Inline element
|
|
3538
|
+
// Inline element sets for whitespace handling
|
|
3508
3539
|
|
|
3509
3540
|
// Non-empty elements that will maintain whitespace around them
|
|
3510
3541
|
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 +3546,9 @@ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b
|
|
|
3515
3546
|
// Elements that will always maintain whitespace around them
|
|
3516
3547
|
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
3517
3548
|
|
|
3549
|
+
// Form control elements (for conditional whitespace collapsing)
|
|
3550
|
+
const formControlElements = new Set(['input', 'button', 'select', 'textarea', 'output', 'meter', 'progress']);
|
|
3551
|
+
|
|
3518
3552
|
// Default attribute values
|
|
3519
3553
|
|
|
3520
3554
|
// Default attribute values (could apply to any element)
|
|
@@ -3554,14 +3588,17 @@ const tagDefaults = {
|
|
|
3554
3588
|
// Script MIME types
|
|
3555
3589
|
|
|
3556
3590
|
// https://mathiasbynens.be/demo/javascript-mime-type
|
|
3557
|
-
// https://developer.mozilla.org/en/docs/Web/HTML/
|
|
3591
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
|
|
3558
3592
|
const executableScriptsMimetypes = new Set([
|
|
3559
3593
|
'text/javascript',
|
|
3594
|
+
'text/x-javascript',
|
|
3560
3595
|
'text/ecmascript',
|
|
3596
|
+
'text/x-ecmascript',
|
|
3561
3597
|
'text/jscript',
|
|
3562
3598
|
'application/javascript',
|
|
3563
3599
|
'application/x-javascript',
|
|
3564
3600
|
'application/ecmascript',
|
|
3601
|
+
'application/x-ecmascript',
|
|
3565
3602
|
'module'
|
|
3566
3603
|
]);
|
|
3567
3604
|
|
|
@@ -3569,15 +3606,15 @@ const keepScriptsMimetypes = new Set([
|
|
|
3569
3606
|
'module'
|
|
3570
3607
|
]);
|
|
3571
3608
|
|
|
3572
|
-
// Boolean attribute
|
|
3609
|
+
// Boolean attribute sets
|
|
3573
3610
|
|
|
3574
3611
|
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
3612
|
|
|
3576
3613
|
const isBooleanValue = new Set(['true', 'false']);
|
|
3577
3614
|
|
|
3578
|
-
// `srcset`
|
|
3615
|
+
// `srcset` elements
|
|
3579
3616
|
|
|
3580
|
-
const
|
|
3617
|
+
const srcsetElements = new Set(['img', 'source']);
|
|
3581
3618
|
|
|
3582
3619
|
// JSON script types
|
|
3583
3620
|
|
|
@@ -3593,7 +3630,7 @@ const jsonScriptTypes = new Set([
|
|
|
3593
3630
|
'speculationrules',
|
|
3594
3631
|
]);
|
|
3595
3632
|
|
|
3596
|
-
// Tag omission rules and element
|
|
3633
|
+
// Tag omission rules and element sets
|
|
3597
3634
|
|
|
3598
3635
|
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
|
|
3599
3636
|
// - retain `<body>` if followed by `<noscript>`
|
|
@@ -3604,35 +3641,35 @@ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody'])
|
|
|
3604
3641
|
|
|
3605
3642
|
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
3643
|
|
|
3607
|
-
const
|
|
3644
|
+
const headerElements = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
|
|
3608
3645
|
|
|
3609
|
-
const
|
|
3646
|
+
const descriptionElements = new Set(['dt', 'dd']);
|
|
3610
3647
|
|
|
3611
|
-
const
|
|
3648
|
+
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
3649
|
|
|
3613
|
-
const
|
|
3650
|
+
const pInlineElements = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
|
|
3614
3651
|
|
|
3615
3652
|
const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
|
|
3616
3653
|
|
|
3617
3654
|
const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
|
|
3618
3655
|
|
|
3619
|
-
const
|
|
3656
|
+
const optionElements = new Set(['option', 'optgroup']);
|
|
3620
3657
|
|
|
3621
|
-
const
|
|
3658
|
+
const tableContentElements = new Set(['tbody', 'tfoot']);
|
|
3622
3659
|
|
|
3623
|
-
const
|
|
3660
|
+
const tableSectionElements = new Set(['thead', 'tbody', 'tfoot']);
|
|
3624
3661
|
|
|
3625
|
-
const
|
|
3662
|
+
const cellElements = new Set(['td', 'th']);
|
|
3626
3663
|
|
|
3627
|
-
const
|
|
3664
|
+
const topLevelElements = new Set(['html', 'head', 'body']);
|
|
3628
3665
|
|
|
3629
|
-
const
|
|
3666
|
+
const compactElements = new Set(['html', 'body']);
|
|
3630
3667
|
|
|
3631
|
-
const
|
|
3668
|
+
const looseElements = new Set(['head', 'colgroup', 'caption']);
|
|
3632
3669
|
|
|
3633
|
-
const
|
|
3670
|
+
const trailingElements = new Set(['dt', 'thead']);
|
|
3634
3671
|
|
|
3635
|
-
const
|
|
3672
|
+
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
3673
|
|
|
3637
3674
|
// Empty attribute regex
|
|
3638
3675
|
|
|
@@ -3642,7 +3679,7 @@ const reEmptyAttribute = new RegExp(
|
|
|
3642
3679
|
|
|
3643
3680
|
// Special content elements
|
|
3644
3681
|
|
|
3645
|
-
const
|
|
3682
|
+
const specialContentElements = new Set(['script', 'style']);
|
|
3646
3683
|
|
|
3647
3684
|
// Imports
|
|
3648
3685
|
|
|
@@ -3706,7 +3743,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
3706
3743
|
}
|
|
3707
3744
|
|
|
3708
3745
|
if (trimLeft) {
|
|
3709
|
-
//
|
|
3746
|
+
// No-break space is specifically handled inside the replacer function
|
|
3710
3747
|
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
3711
3748
|
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
3712
3749
|
if (conservative && spaces === '\t') {
|
|
@@ -3717,7 +3754,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
3717
3754
|
}
|
|
3718
3755
|
|
|
3719
3756
|
if (trimRight) {
|
|
3720
|
-
//
|
|
3757
|
+
// No-break space is specifically handled inside the replacer function
|
|
3721
3758
|
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
3722
3759
|
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
3723
3760
|
if (conservative && spaces === '\t') {
|
|
@@ -3741,11 +3778,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
3741
3778
|
|
|
3742
3779
|
// Collapse whitespace smartly based on surrounding tags
|
|
3743
3780
|
|
|
3744
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
3781
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
|
|
3782
|
+
const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
|
|
3783
|
+
const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
|
|
3784
|
+
|
|
3785
|
+
// Helper: Check if an input element has `type="hidden"`
|
|
3786
|
+
const isHiddenInput = (tagName, attrs) => {
|
|
3787
|
+
if (tagName !== 'input' || !attrs || !attrs.length) return false;
|
|
3788
|
+
const typeAttr = attrs.find(attr => attr.name === 'type');
|
|
3789
|
+
return typeAttr && typeAttr.value === 'hidden';
|
|
3790
|
+
};
|
|
3791
|
+
|
|
3792
|
+
// Check if prev/next are non-rendering (hidden) elements
|
|
3793
|
+
const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
|
|
3794
|
+
const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
|
|
3795
|
+
|
|
3745
3796
|
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
3797
|
+
|
|
3798
|
+
// Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
|
|
3799
|
+
// This happens even in basic `collapseWhitespace` mode (safe optimization)
|
|
3800
|
+
if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
|
|
3801
|
+
trimLeft = true;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
// Aggressive mode: Collapse between all form controls (pure whitespace only)
|
|
3805
|
+
const isPureWhitespace = str && !/\S/.test(str);
|
|
3806
|
+
if (!trimLeft && prevTagName && nextTagName &&
|
|
3807
|
+
options.collapseInlineTagWhitespace &&
|
|
3808
|
+
isPureWhitespace &&
|
|
3809
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
3810
|
+
trimLeft = true;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3746
3813
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
3747
3814
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
3748
3815
|
}
|
|
3816
|
+
|
|
3749
3817
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
3750
3818
|
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
3751
3819
|
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
@@ -3753,10 +3821,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
3753
3821
|
trimLeft = false;
|
|
3754
3822
|
}
|
|
3755
3823
|
}
|
|
3824
|
+
|
|
3756
3825
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
3826
|
+
|
|
3827
|
+
// Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
|
|
3828
|
+
if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
|
|
3829
|
+
trimRight = true;
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
// Aggressive mode: Same as `trimLeft`
|
|
3833
|
+
if (!trimRight && prevTagName && nextTagName &&
|
|
3834
|
+
options.collapseInlineTagWhitespace &&
|
|
3835
|
+
isPureWhitespace &&
|
|
3836
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
3837
|
+
trimRight = true;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3757
3840
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
3758
3841
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
3759
3842
|
}
|
|
3843
|
+
|
|
3760
3844
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
3761
3845
|
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
3762
3846
|
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
@@ -3764,6 +3848,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
3764
3848
|
trimRight = false;
|
|
3765
3849
|
}
|
|
3766
3850
|
}
|
|
3851
|
+
|
|
3767
3852
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
3768
3853
|
}
|
|
3769
3854
|
|
|
@@ -6314,7 +6399,6 @@ var RelateURL = /*@__PURE__*/getDefaultExportFromCjs(libExports);
|
|
|
6314
6399
|
|
|
6315
6400
|
// Wrap CSS declarations for inline styles and media queries
|
|
6316
6401
|
// This ensures proper context for CSS minification
|
|
6317
|
-
|
|
6318
6402
|
function wrapCSS(text, type) {
|
|
6319
6403
|
switch (type) {
|
|
6320
6404
|
case 'inline':
|
|
@@ -6396,7 +6480,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
6396
6480
|
|
|
6397
6481
|
/**
|
|
6398
6482
|
* Lightweight SVG optimizations:
|
|
6399
|
-
*
|
|
6400
6483
|
* - Numeric precision reduction for coordinates and path data
|
|
6401
6484
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
6402
6485
|
* - Default attribute removal (safe, well-documented defaults)
|
|
@@ -6490,7 +6573,7 @@ function minifyNumber(num, precision = 3) {
|
|
|
6490
6573
|
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
6491
6574
|
|
|
6492
6575
|
// Check cache
|
|
6493
|
-
// (Note:
|
|
6576
|
+
// (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
6494
6577
|
// This is intentional to avoid parsing overhead.
|
|
6495
6578
|
// Real-world SVG files from export tools typically use consistent formats.)
|
|
6496
6579
|
const cacheKey = `${num}:${precision}`;
|
|
@@ -6529,15 +6612,15 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
6529
6612
|
|
|
6530
6613
|
// Remove unnecessary spaces around path commands
|
|
6531
6614
|
// 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
|
|
6615
|
+
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`
|
|
6533
6616
|
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
6534
6617
|
|
|
6535
6618
|
// Safe to remove space before command letter when preceded by a number
|
|
6536
|
-
// 0 L → 0L
|
|
6619
|
+
// `0 L` → `0L`, `20 M` → `20M`
|
|
6537
6620
|
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
6538
6621
|
|
|
6539
6622
|
// Safe to remove space before negative number when preceded by a number
|
|
6540
|
-
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
6623
|
+
// `10 -20` → `10-20` (numbers are separated by the minus sign)
|
|
6541
6624
|
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
6542
6625
|
|
|
6543
6626
|
return result;
|
|
@@ -6546,9 +6629,9 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
6546
6629
|
/**
|
|
6547
6630
|
* Minify whitespace in numeric attribute values
|
|
6548
6631
|
* Examples:
|
|
6549
|
-
*
|
|
6550
|
-
*
|
|
6551
|
-
*
|
|
6632
|
+
* - “10 , 20" → "10,20"
|
|
6633
|
+
* - "translate( 10 20 )" → "translate(10 20)"
|
|
6634
|
+
* - "100, 10 40, 198" → "100,10 40,198"
|
|
6552
6635
|
*
|
|
6553
6636
|
* @param {string} value - Attribute value to minify
|
|
6554
6637
|
* @returns {string} Minified value
|
|
@@ -6581,8 +6664,7 @@ function minifyColor(color) {
|
|
|
6581
6664
|
|
|
6582
6665
|
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
6583
6666
|
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
6584
|
-
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
6585
|
-
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
6667
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
6586
6668
|
return trimmed;
|
|
6587
6669
|
}
|
|
6588
6670
|
|
|
@@ -6590,7 +6672,7 @@ function minifyColor(color) {
|
|
|
6590
6672
|
const lower = trimmed.toLowerCase();
|
|
6591
6673
|
|
|
6592
6674
|
// Shorten 6-digit hex to 3-digit when possible
|
|
6593
|
-
//
|
|
6675
|
+
// `#aabbcc` → `#abc`, `#000000` → `#000`
|
|
6594
6676
|
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
6595
6677
|
if (hexMatch) {
|
|
6596
6678
|
const hex = hexMatch[1];
|
|
@@ -6610,7 +6692,7 @@ function minifyColor(color) {
|
|
|
6610
6692
|
return NAMED_COLORS[lower] || lower;
|
|
6611
6693
|
}
|
|
6612
6694
|
|
|
6613
|
-
// Convert rgb(
|
|
6695
|
+
// Convert rgb() to hex
|
|
6614
6696
|
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
6615
6697
|
if (rgbMatch) {
|
|
6616
6698
|
const r = parseInt(rgbMatch[1], 10);
|
|
@@ -6641,14 +6723,14 @@ function minifyColor(color) {
|
|
|
6641
6723
|
const NUMERIC_ATTRS = new Set([
|
|
6642
6724
|
'd', // Path data
|
|
6643
6725
|
'points', // Polygon/polyline points
|
|
6644
|
-
'viewBox', // viewBox coordinates
|
|
6726
|
+
'viewBox', // `viewBox` coordinates
|
|
6645
6727
|
'transform', // Transform functions
|
|
6646
6728
|
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
6647
6729
|
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
6648
6730
|
'width', 'height', // Dimensions
|
|
6649
6731
|
'dx', 'dy', // Text offsets
|
|
6650
6732
|
'offset', // Gradient offset
|
|
6651
|
-
'startOffset', // textPath
|
|
6733
|
+
'startOffset', // `textPath`
|
|
6652
6734
|
'pathLength', // Path length
|
|
6653
6735
|
'stdDeviation', // Filter params
|
|
6654
6736
|
'baseFrequency', // Turbulence
|
|
@@ -6832,8 +6914,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6832
6914
|
/**
|
|
6833
6915
|
* @param {Partial<MinifierOptions>} inputOptions - User-provided options
|
|
6834
6916
|
* @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
|
|
6917
|
+
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
6918
|
+
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
6837
6919
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
6838
6920
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
6839
6921
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
@@ -6870,7 +6952,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6870
6952
|
if (typeof value === 'string') {
|
|
6871
6953
|
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
6872
6954
|
}
|
|
6873
|
-
return value; // Already a RegExp or
|
|
6955
|
+
return value; // Already a RegExp or another type
|
|
6874
6956
|
};
|
|
6875
6957
|
|
|
6876
6958
|
const parseRegExpArray = (arr) => {
|
|
@@ -7007,7 +7089,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7007
7089
|
// Validate engine
|
|
7008
7090
|
const supportedEngines = ['terser', 'swc'];
|
|
7009
7091
|
if (!supportedEngines.includes(engine)) {
|
|
7010
|
-
throw new Error(`Unsupported JS minifier engine:
|
|
7092
|
+
throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
|
|
7011
7093
|
}
|
|
7012
7094
|
|
|
7013
7095
|
// Extract engine-specific options (excluding `engine` field itself)
|
|
@@ -7114,14 +7196,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7114
7196
|
relateUrlOptions = {};
|
|
7115
7197
|
}
|
|
7116
7198
|
|
|
7117
|
-
// Cache
|
|
7199
|
+
// Cache relateurl instance for reuse (expensive to create)
|
|
7118
7200
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
7119
7201
|
|
|
7120
7202
|
// Create instance-specific cache (results depend on site configuration)
|
|
7121
7203
|
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
7122
7204
|
|
|
7123
7205
|
options.minifyURLs = function (text) {
|
|
7124
|
-
// Fast-path: Skip if text doesn
|
|
7206
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
7125
7207
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
7126
7208
|
if (!/[/:?#\s]/.test(text)) {
|
|
7127
7209
|
return text;
|
|
@@ -7153,17 +7235,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7153
7235
|
};
|
|
7154
7236
|
} else if (key === 'minifySVG') {
|
|
7155
7237
|
// Process SVG minification options
|
|
7156
|
-
// Unlike minifyCSS
|
|
7238
|
+
// Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
|
|
7157
7239
|
// The actual minification is applied inline during attribute processing
|
|
7158
7240
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
7159
7241
|
} else if (key === 'customAttrCollapse') {
|
|
7160
|
-
// Single
|
|
7242
|
+
// Single regex pattern
|
|
7161
7243
|
options[key] = parseRegExp(option);
|
|
7162
7244
|
} else if (key === 'customAttrSurround') {
|
|
7163
7245
|
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
7164
7246
|
options[key] = parseNestedRegExpArray(option);
|
|
7165
7247
|
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
7166
|
-
// Array of
|
|
7248
|
+
// Array of regex patterns
|
|
7167
7249
|
options[key] = parseRegExpArray(option);
|
|
7168
7250
|
} else {
|
|
7169
7251
|
options[key] = option;
|
|
@@ -7228,8 +7310,7 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
7228
7310
|
const tagHasDefaults = tag in tagDefaults;
|
|
7229
7311
|
|
|
7230
7312
|
// Check for legacy attribute rules (element- and attribute-specific)
|
|
7231
|
-
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
|
|
7232
|
-
(tag === 'a' && attrName === 'name');
|
|
7313
|
+
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) || (tag === 'a' && attrName === 'name');
|
|
7233
7314
|
|
|
7234
7315
|
// If none of these conditions apply, attribute cannot be redundant
|
|
7235
7316
|
if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
|
|
@@ -7287,7 +7368,7 @@ function isStyleLinkTypeAttribute(attrValue = '') {
|
|
|
7287
7368
|
return attrValue === '' || attrValue === 'text/css';
|
|
7288
7369
|
}
|
|
7289
7370
|
|
|
7290
|
-
function
|
|
7371
|
+
function isStyleElement(tag, attrs) {
|
|
7291
7372
|
if (tag !== 'style') {
|
|
7292
7373
|
return false;
|
|
7293
7374
|
}
|
|
@@ -7344,11 +7425,11 @@ function isLinkType(tag, attrs, value) {
|
|
|
7344
7425
|
}
|
|
7345
7426
|
|
|
7346
7427
|
function isMediaQuery(tag, attrs, attrName) {
|
|
7347
|
-
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') ||
|
|
7428
|
+
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
|
|
7348
7429
|
}
|
|
7349
7430
|
|
|
7350
7431
|
function isSrcset(attrName, tag) {
|
|
7351
|
-
return attrName === 'srcset' &&
|
|
7432
|
+
return attrName === 'srcset' && srcsetElements.has(tag);
|
|
7352
7433
|
}
|
|
7353
7434
|
|
|
7354
7435
|
function isMetaViewport(tag, attrs) {
|
|
@@ -7356,7 +7437,7 @@ function isMetaViewport(tag, attrs) {
|
|
|
7356
7437
|
return false;
|
|
7357
7438
|
}
|
|
7358
7439
|
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
7359
|
-
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
7440
|
+
if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
|
|
7360
7441
|
return true;
|
|
7361
7442
|
}
|
|
7362
7443
|
}
|
|
@@ -7376,7 +7457,7 @@ function isContentSecurityPolicy(tag, attrs) {
|
|
|
7376
7457
|
}
|
|
7377
7458
|
|
|
7378
7459
|
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
7379
|
-
const isValueEmpty = !attrValue ||
|
|
7460
|
+
const isValueEmpty = !attrValue || attrValue.trim() === '';
|
|
7380
7461
|
if (!isValueEmpty) {
|
|
7381
7462
|
return false;
|
|
7382
7463
|
}
|
|
@@ -7399,7 +7480,7 @@ function hasAttrName(name, attrs) {
|
|
|
7399
7480
|
|
|
7400
7481
|
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
7401
7482
|
// Apply early whitespace normalization if enabled
|
|
7402
|
-
// Preserves special spaces (
|
|
7483
|
+
// Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
7403
7484
|
if (options.collapseAttributeWhitespace) {
|
|
7404
7485
|
// Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
|
|
7405
7486
|
if (RE_ATTR_WS_CHECK.test(attrValue)) {
|
|
@@ -7455,7 +7536,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
7455
7536
|
try {
|
|
7456
7537
|
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
7457
7538
|
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
7458
|
-
//
|
|
7539
|
+
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
7459
7540
|
if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
|
|
7460
7541
|
attrValue = '';
|
|
7461
7542
|
}
|
|
@@ -7575,13 +7656,13 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7575
7656
|
}
|
|
7576
7657
|
|
|
7577
7658
|
if ((options.removeRedundantAttributes &&
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7659
|
+
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
7660
|
+
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
7661
|
+
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
7662
|
+
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
7663
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
7664
|
+
(options.insideSVG && options.minifySVG &&
|
|
7665
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
7585
7666
|
return;
|
|
7586
7667
|
}
|
|
7587
7668
|
|
|
@@ -7590,7 +7671,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7590
7671
|
}
|
|
7591
7672
|
|
|
7592
7673
|
if (options.removeEmptyAttributes &&
|
|
7593
|
-
|
|
7674
|
+
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
7594
7675
|
return;
|
|
7595
7676
|
}
|
|
7596
7677
|
|
|
@@ -7613,19 +7694,35 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7613
7694
|
let attrFragment;
|
|
7614
7695
|
let emittedAttrValue;
|
|
7615
7696
|
|
|
7616
|
-
|
|
7617
|
-
|
|
7697
|
+
// Determine if we need to add/keep quotes
|
|
7698
|
+
const shouldAddQuotes = typeof attrValue !== 'undefined' && (
|
|
7699
|
+
// If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
|
|
7700
|
+
(options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
|
|
7701
|
+
// If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
|
|
7702
|
+
(!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
|
|
7703
|
+
// Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
|
|
7704
|
+
// which can create ambiguous/invalid HTML—add quotes to be safe
|
|
7705
|
+
(options.removeTagWhitespace && attrQuote === '' && !isLast)))
|
|
7706
|
+
);
|
|
7707
|
+
|
|
7708
|
+
if (shouldAddQuotes) {
|
|
7618
7709
|
// Determine the appropriate quote character
|
|
7619
7710
|
if (!options.preventAttributesEscaping) {
|
|
7620
|
-
// Normal mode:
|
|
7621
|
-
|
|
7711
|
+
// Normal mode: Choose optimal quote type to minimize escaping
|
|
7712
|
+
// unless we’re preserving original quotes and they don’t need escaping
|
|
7713
|
+
const needsEscaping = (attrQuote === '"' && attrValue.indexOf('"') !== -1) || (attrQuote === "'" && attrValue.indexOf("'") !== -1);
|
|
7714
|
+
|
|
7715
|
+
if (options.removeAttributeQuotes || typeof options.quoteCharacter !== 'undefined' || needsEscaping || attrQuote === '') {
|
|
7716
|
+
attrQuote = chooseAttributeQuote(attrValue, options);
|
|
7717
|
+
}
|
|
7718
|
+
|
|
7622
7719
|
if (attrQuote === '"') {
|
|
7623
7720
|
attrValue = attrValue.replace(/"/g, '"');
|
|
7624
7721
|
} else {
|
|
7625
7722
|
attrValue = attrValue.replace(/'/g, ''');
|
|
7626
7723
|
}
|
|
7627
7724
|
} else {
|
|
7628
|
-
// `preventAttributesEscaping` mode:
|
|
7725
|
+
// `preventAttributesEscaping` mode: Choose safe quotes but don't escape
|
|
7629
7726
|
// except when both quote types are present—then escape to prevent invalid HTML
|
|
7630
7727
|
const hasDoubleQuote = attrValue.indexOf('"') !== -1;
|
|
7631
7728
|
const hasSingleQuote = attrValue.indexOf("'") !== -1;
|
|
@@ -7644,8 +7741,18 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7644
7741
|
attrQuote = "'";
|
|
7645
7742
|
} else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
|
|
7646
7743
|
attrQuote = '"';
|
|
7647
|
-
//
|
|
7648
|
-
} else if (attrQuote
|
|
7744
|
+
// If no quote character yet (empty string), choose based on content
|
|
7745
|
+
} else if (attrQuote === '') {
|
|
7746
|
+
if (hasSingleQuote && !hasDoubleQuote) {
|
|
7747
|
+
attrQuote = '"';
|
|
7748
|
+
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
7749
|
+
attrQuote = "'";
|
|
7750
|
+
} else {
|
|
7751
|
+
attrQuote = '"';
|
|
7752
|
+
}
|
|
7753
|
+
// Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string):
|
|
7754
|
+
// Choose safe default based on value content
|
|
7755
|
+
} else if (attrQuote !== '"' && attrQuote !== "'") {
|
|
7649
7756
|
if (hasSingleQuote && !hasDoubleQuote) {
|
|
7650
7757
|
attrQuote = '"';
|
|
7651
7758
|
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
@@ -7655,7 +7762,22 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7655
7762
|
}
|
|
7656
7763
|
}
|
|
7657
7764
|
} else {
|
|
7658
|
-
|
|
7765
|
+
// `quoteCharacter` is explicitly set
|
|
7766
|
+
const preferredQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
7767
|
+
// Safety check: If the preferred quote conflicts with value content, switch to the opposite quote
|
|
7768
|
+
if ((preferredQuote === '"' && hasDoubleQuote && !hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && !hasDoubleQuote)) {
|
|
7769
|
+
attrQuote = preferredQuote === '"' ? "'" : '"';
|
|
7770
|
+
} else if ((preferredQuote === '"' && hasDoubleQuote && hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && hasDoubleQuote)) {
|
|
7771
|
+
// Both quote types present: Fall back to escaping despite `preventAttributesEscaping`
|
|
7772
|
+
attrQuote = preferredQuote;
|
|
7773
|
+
if (attrQuote === '"') {
|
|
7774
|
+
attrValue = attrValue.replace(/"/g, '"');
|
|
7775
|
+
} else {
|
|
7776
|
+
attrValue = attrValue.replace(/'/g, ''');
|
|
7777
|
+
}
|
|
7778
|
+
} else {
|
|
7779
|
+
attrQuote = preferredQuote;
|
|
7780
|
+
}
|
|
7659
7781
|
}
|
|
7660
7782
|
}
|
|
7661
7783
|
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
@@ -7663,15 +7785,17 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
7663
7785
|
emittedAttrValue += ' ';
|
|
7664
7786
|
}
|
|
7665
7787
|
} else if (isLast && !hasUnarySlash) {
|
|
7666
|
-
// Last attribute in a non-self-closing tag:
|
|
7788
|
+
// Last attribute in a non-self-closing tag:
|
|
7789
|
+
// No space needed
|
|
7667
7790
|
emittedAttrValue = attrValue;
|
|
7668
7791
|
} else {
|
|
7669
|
-
// Not last attribute, or is a self-closing tag:
|
|
7792
|
+
// Not last attribute, or is a self-closing tag:
|
|
7793
|
+
// Unquoted values must have space after them to delimit from next attribute
|
|
7670
7794
|
emittedAttrValue = attrValue + ' ';
|
|
7671
7795
|
}
|
|
7672
7796
|
|
|
7673
7797
|
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
7674
|
-
|
|
7798
|
+
isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
|
|
7675
7799
|
attrFragment = attrName;
|
|
7676
7800
|
if (!isLast) {
|
|
7677
7801
|
attrFragment += ' ';
|
|
@@ -7694,7 +7818,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
|
|
|
7694
7818
|
case 'head':
|
|
7695
7819
|
return true;
|
|
7696
7820
|
case 'body':
|
|
7697
|
-
return !
|
|
7821
|
+
return !headerElements.has(tag);
|
|
7698
7822
|
case 'colgroup':
|
|
7699
7823
|
return tag === 'col';
|
|
7700
7824
|
case 'tbody':
|
|
@@ -7708,7 +7832,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
|
|
|
7708
7832
|
case 'colgroup':
|
|
7709
7833
|
return optionalEndTag === 'colgroup';
|
|
7710
7834
|
case 'tbody':
|
|
7711
|
-
return
|
|
7835
|
+
return tableSectionElements.has(optionalEndTag);
|
|
7712
7836
|
}
|
|
7713
7837
|
return false;
|
|
7714
7838
|
}
|
|
@@ -7727,9 +7851,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
7727
7851
|
return tag === optionalEndTag;
|
|
7728
7852
|
case 'dt':
|
|
7729
7853
|
case 'dd':
|
|
7730
|
-
return
|
|
7854
|
+
return descriptionElements.has(tag);
|
|
7731
7855
|
case 'p':
|
|
7732
|
-
return
|
|
7856
|
+
return pBlockElements.has(tag);
|
|
7733
7857
|
case 'rb':
|
|
7734
7858
|
case 'rt':
|
|
7735
7859
|
case 'rp':
|
|
@@ -7737,15 +7861,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
7737
7861
|
case 'rtc':
|
|
7738
7862
|
return rubyRtcEndTagOmission.has(tag);
|
|
7739
7863
|
case 'option':
|
|
7740
|
-
return
|
|
7864
|
+
return optionElements.has(tag);
|
|
7741
7865
|
case 'thead':
|
|
7742
7866
|
case 'tbody':
|
|
7743
|
-
return
|
|
7867
|
+
return tableContentElements.has(tag);
|
|
7744
7868
|
case 'tfoot':
|
|
7745
7869
|
return tag === 'tbody';
|
|
7746
7870
|
case 'td':
|
|
7747
7871
|
case 'th':
|
|
7748
|
-
return
|
|
7872
|
+
return cellElements.has(tag);
|
|
7749
7873
|
}
|
|
7750
7874
|
return false;
|
|
7751
7875
|
}
|
|
@@ -7848,7 +7972,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
7848
7972
|
if (typeof item === 'string') {
|
|
7849
7973
|
const spec = parseElementSpec(item, options);
|
|
7850
7974
|
if (!spec && options.log) {
|
|
7851
|
-
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification:
|
|
7975
|
+
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: “' + item + '”');
|
|
7852
7976
|
}
|
|
7853
7977
|
return spec;
|
|
7854
7978
|
}
|
|
@@ -8361,7 +8485,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8361
8485
|
}
|
|
8362
8486
|
|
|
8363
8487
|
// Pre-compile regex patterns for reuse (performance optimization)
|
|
8364
|
-
// These must be declared before scan() since scan uses them
|
|
8488
|
+
// These must be declared before `scan()` since scan uses them
|
|
8365
8489
|
const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
|
|
8366
8490
|
const whitespaceSplitPatternSort = /[ \n\f\r]+/;
|
|
8367
8491
|
|
|
@@ -8393,9 +8517,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8393
8517
|
chars: async function (text) {
|
|
8394
8518
|
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
8395
8519
|
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
8396
|
-
if (options.processScripts &&
|
|
8397
|
-
|
|
8398
|
-
|
|
8520
|
+
if (options.processScripts && specialContentElements.has(currentTag) &&
|
|
8521
|
+
options.processScripts.indexOf(currentType) > -1 &&
|
|
8522
|
+
currentType === 'text/html') {
|
|
8399
8523
|
await scan(text);
|
|
8400
8524
|
}
|
|
8401
8525
|
},
|
|
@@ -8418,7 +8542,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8418
8542
|
// For the first pass, create a copy of options and disable aggressive minification.
|
|
8419
8543
|
// Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
|
|
8420
8544
|
// 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.
|
|
8545
|
+
// Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
|
|
8422
8546
|
const firstPassOptions = Object.assign({}, options, {
|
|
8423
8547
|
// Disable sorting for the analysis pass
|
|
8424
8548
|
sortAttributes: false,
|
|
@@ -8437,7 +8561,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8437
8561
|
});
|
|
8438
8562
|
|
|
8439
8563
|
// Temporarily enable `continueOnParseError` for the `scan()` function call below.
|
|
8440
|
-
// Note: `firstPassOptions` already has `continueOnParseError: true` for the minifyHTML call.
|
|
8564
|
+
// Note: `firstPassOptions` already has `continueOnParseError: true` for the `minifyHTML` call.
|
|
8441
8565
|
const originalContinueOnParseError = options.continueOnParseError;
|
|
8442
8566
|
options.continueOnParseError = true;
|
|
8443
8567
|
|
|
@@ -8450,7 +8574,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8450
8574
|
: null;
|
|
8451
8575
|
|
|
8452
8576
|
try {
|
|
8453
|
-
// Expand UID tokens back to original content for frequency analysis
|
|
8577
|
+
// Expand UID tokens back to the original content for frequency analysis
|
|
8454
8578
|
let expandedValue = value;
|
|
8455
8579
|
if (uidReplacePattern) {
|
|
8456
8580
|
expandedValue = value.replace(uidReplacePattern, function (match, index) {
|
|
@@ -8499,7 +8623,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8499
8623
|
attrOrderCache.set(cacheKey, sortedNames);
|
|
8500
8624
|
}
|
|
8501
8625
|
|
|
8502
|
-
// Apply the sorted order to attrs
|
|
8626
|
+
// Apply the sorted order to `attrs`
|
|
8503
8627
|
const attrMap = Object.create(null);
|
|
8504
8628
|
names.forEach(function (name, index) {
|
|
8505
8629
|
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
@@ -8586,7 +8710,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8586
8710
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
8587
8711
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
8588
8712
|
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
8589
|
-
// Fast path: Reuse base
|
|
8713
|
+
// Fast path: Reuse base sets if no custom elements
|
|
8590
8714
|
const inlineTextSet = normalizedCustomElements.length
|
|
8591
8715
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
8592
8716
|
: inlineElementsToKeepWhitespaceWithin;
|
|
@@ -8606,7 +8730,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8606
8730
|
}
|
|
8607
8731
|
|
|
8608
8732
|
// 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-
|
|
8733
|
+
// For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
|
|
8610
8734
|
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
8611
8735
|
if (!uidIgnore) {
|
|
8612
8736
|
uidIgnore = uniqueId(value);
|
|
@@ -8627,7 +8751,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8627
8751
|
// Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
|
|
8628
8752
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
8629
8753
|
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
8630
|
-
|
|
8754
|
+
(options.sortClassName && typeof options.sortClassName !== 'function')) {
|
|
8631
8755
|
await createSortFns(value, options, uidIgnore, null, ignoredMarkupChunks);
|
|
8632
8756
|
}
|
|
8633
8757
|
|
|
@@ -8689,11 +8813,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8689
8813
|
});
|
|
8690
8814
|
}
|
|
8691
8815
|
|
|
8692
|
-
function
|
|
8816
|
+
function canCollapseWhitespace$1(tag, attrs) {
|
|
8693
8817
|
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
8694
8818
|
}
|
|
8695
8819
|
|
|
8696
|
-
function
|
|
8820
|
+
function canTrimWhitespace$1(tag, attrs) {
|
|
8697
8821
|
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
8698
8822
|
}
|
|
8699
8823
|
|
|
@@ -8715,12 +8839,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8715
8839
|
|
|
8716
8840
|
// Look for trailing whitespaces, bypass any inline tags
|
|
8717
8841
|
function trimTrailingWhitespace(index, nextTag) {
|
|
8718
|
-
for (let endTag = null; index >= 0 &&
|
|
8842
|
+
for (let endTag = null; index >= 0 && canTrimWhitespace$1(endTag); index--) {
|
|
8719
8843
|
const str = buffer[index];
|
|
8720
8844
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
8721
8845
|
if (match) {
|
|
8722
8846
|
endTag = match[1];
|
|
8723
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
8847
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, [], [], options, inlineElements, inlineTextSet))) {
|
|
8724
8848
|
break;
|
|
8725
8849
|
}
|
|
8726
8850
|
}
|
|
@@ -8769,10 +8893,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8769
8893
|
|
|
8770
8894
|
let optional = options.removeOptionalTags;
|
|
8771
8895
|
if (optional) {
|
|
8772
|
-
const htmlTag =
|
|
8896
|
+
const htmlTag = htmlElements.has(tag);
|
|
8773
8897
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
8774
8898
|
// `<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>`,
|
|
8899
|
+
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
|
|
8776
8900
|
// `<colgroup>` may be omitted if first thing inside is `<col>`
|
|
8777
8901
|
// `<tbody>` may be omitted if first thing inside is `<tr>`
|
|
8778
8902
|
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
@@ -8789,16 +8913,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8789
8913
|
optionalEndTag = '';
|
|
8790
8914
|
}
|
|
8791
8915
|
|
|
8792
|
-
// Set whitespace flags for nested tags (e.g.,
|
|
8916
|
+
// Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
|
|
8793
8917
|
if (options.collapseWhitespace) {
|
|
8794
8918
|
if (!stackNoTrimWhitespace.length) {
|
|
8795
8919
|
squashTrailingWhitespace(tag);
|
|
8796
8920
|
}
|
|
8797
8921
|
if (!unary) {
|
|
8798
|
-
if (!
|
|
8922
|
+
if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
8799
8923
|
stackNoTrimWhitespace.push(tag);
|
|
8800
8924
|
}
|
|
8801
|
-
if (!
|
|
8925
|
+
if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
8802
8926
|
stackNoCollapseWhitespace.push(tag);
|
|
8803
8927
|
}
|
|
8804
8928
|
}
|
|
@@ -8854,7 +8978,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8854
8978
|
squashTrailingWhitespace('/' + tag);
|
|
8855
8979
|
}
|
|
8856
8980
|
if (stackNoCollapseWhitespace.length &&
|
|
8857
|
-
|
|
8981
|
+
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
8858
8982
|
stackNoCollapseWhitespace.pop();
|
|
8859
8983
|
}
|
|
8860
8984
|
}
|
|
@@ -8867,7 +8991,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8867
8991
|
|
|
8868
8992
|
if (options.removeOptionalTags) {
|
|
8869
8993
|
// `<html>`, `<head>` or `<body>` may be omitted if the element is empty
|
|
8870
|
-
if (isElementEmpty &&
|
|
8994
|
+
if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
|
|
8871
8995
|
removeStartTag();
|
|
8872
8996
|
}
|
|
8873
8997
|
optionalStartTag = '';
|
|
@@ -8875,7 +8999,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8875
8999
|
// `</head>` may be omitted if not followed by space or comment
|
|
8876
9000
|
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
8877
9001
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
8878
|
-
if (tag && optionalEndTag && !
|
|
9002
|
+
if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
|
|
8879
9003
|
removeEndTag();
|
|
8880
9004
|
}
|
|
8881
9005
|
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
@@ -8922,10 +9046,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8922
9046
|
}
|
|
8923
9047
|
}
|
|
8924
9048
|
},
|
|
8925
|
-
chars: async function (text, prevTag, nextTag) {
|
|
9049
|
+
chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
8926
9050
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
8927
9051
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
8928
|
-
|
|
9052
|
+
prevAttrs = prevAttrs || [];
|
|
9053
|
+
nextAttrs = nextAttrs || [];
|
|
9054
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
8929
9055
|
if (text.indexOf('&') !== -1) {
|
|
8930
9056
|
text = decodeHTML(text);
|
|
8931
9057
|
}
|
|
@@ -8961,7 +9087,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8961
9087
|
}
|
|
8962
9088
|
}
|
|
8963
9089
|
if (prevTag || nextTag) {
|
|
8964
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
9090
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
|
|
8965
9091
|
} else {
|
|
8966
9092
|
text = collapseWhitespace(text, options, true, true);
|
|
8967
9093
|
}
|
|
@@ -8973,13 +9099,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8973
9099
|
text = collapseWhitespace(text, options, false, false, true);
|
|
8974
9100
|
}
|
|
8975
9101
|
}
|
|
8976
|
-
if (
|
|
9102
|
+
if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
|
|
8977
9103
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
8978
9104
|
}
|
|
8979
9105
|
if (isExecutableScript(currentTag, currentAttrs)) {
|
|
8980
9106
|
text = await options.minifyJS(text);
|
|
8981
9107
|
}
|
|
8982
|
-
if (
|
|
9108
|
+
if (isStyleElement(currentTag, currentAttrs)) {
|
|
8983
9109
|
text = await options.minifyCSS(text);
|
|
8984
9110
|
}
|
|
8985
9111
|
if (options.removeOptionalTags && text) {
|
|
@@ -8991,7 +9117,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8991
9117
|
optionalStartTag = '';
|
|
8992
9118
|
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
8993
9119
|
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
8994
|
-
if (
|
|
9120
|
+
if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
8995
9121
|
removeEndTag();
|
|
8996
9122
|
}
|
|
8997
9123
|
// Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
|
|
@@ -9000,11 +9126,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9000
9126
|
}
|
|
9001
9127
|
}
|
|
9002
9128
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
9003
|
-
if (options.decodeEntities && text && !
|
|
9129
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
9004
9130
|
// Escape any `&` symbols that start either:
|
|
9005
|
-
// 1) a legacy
|
|
9131
|
+
// 1) a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
9006
9132
|
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
9007
|
-
// Note that `&` can be escaped as `&`, without the
|
|
9133
|
+
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
9008
9134
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
9009
9135
|
if (text.indexOf('&') !== -1) {
|
|
9010
9136
|
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
@@ -9069,7 +9195,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9069
9195
|
|
|
9070
9196
|
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
9071
9197
|
// Don’t collapse if either contains plain text, as that would change meaning
|
|
9072
|
-
// Note: This check will match HTML comments (`<!-- … -->`), but the tag
|
|
9198
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag name
|
|
9073
9199
|
// regex below requires starting with a letter, so comments are intentionally
|
|
9074
9200
|
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
9075
9201
|
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
@@ -9130,11 +9256,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9130
9256
|
if (options.removeOptionalTags) {
|
|
9131
9257
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
9132
9258
|
// `<head>` or `<body>` may be omitted if empty
|
|
9133
|
-
if (
|
|
9259
|
+
if (topLevelElements.has(optionalStartTag)) {
|
|
9134
9260
|
removeStartTag();
|
|
9135
9261
|
}
|
|
9136
9262
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
9137
|
-
if (optionalEndTag && !
|
|
9263
|
+
if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
|
|
9138
9264
|
removeEndTag();
|
|
9139
9265
|
}
|
|
9140
9266
|
}
|