html-minifier-next 4.17.2 → 4.19.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 +93 -21
- package/cli.js +3 -0
- package/dist/htmlminifier.cjs +344 -99
- package/dist/htmlminifier.esm.bundle.js +344 -99
- package/dist/types/htmlminifier.d.ts +28 -0
- 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.map +1 -1
- package/dist/types/lib/constants.d.ts +1 -0
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +1 -2
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +206 -12
- package/src/htmlparser.js +111 -63
- package/src/lib/attributes.js +4 -1
- package/src/lib/constants.js +6 -0
- package/src/lib/options.js +8 -14
- package/src/lib/svg.js +15 -8
- package/src/presets.js +1 -0
|
@@ -2655,16 +2655,16 @@ const singleAttrValues = [
|
|
|
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
|
-
const combiningChar = '
|
|
2659
|
-
const digit = '0-9
|
|
2660
|
-
const extender = '
|
|
2661
|
-
const letter = 'A-Za-z
|
|
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
|
+
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
|
+
const extender = '\xB7\u02D0\u02D1\u0387\u0640\u0E46\u0EC6\u3005\u3031-\u3035\u309D\u309E\u30FC-\u30FE';
|
|
2661
|
+
const letter = 'A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u0131\u0134-\u013E\u0141-\u0148\u014A-\u017E\u0180-\u01C3\u01CD-\u01F0\u01F4\u01F5\u01FA-\u0217\u0250-\u02A8\u02BB-\u02C1\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03D6\u03DA\u03DC\u03DE\u03E0\u03E2-\u03F3\u0401-\u040C\u040E-\u044F\u0451-\u045C\u045E-\u0481\u0490-\u04C4\u04C7\u04C8\u04CB\u04CC\u04D0-\u04EB\u04EE-\u04F5\u04F8\u04F9\u0531-\u0556\u0559\u0561-\u0586\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0641-\u064A\u0671-\u06B7\u06BA-\u06BE\u06C0-\u06CE\u06D0-\u06D3\u06D5\u06E5\u06E6\u0905-\u0939\u093D\u0958-\u0961\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8B\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AE0\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B36-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CDE\u0CE0\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D60\u0D61\u0E01-\u0E2E\u0E30\u0E32\u0E33\u0E40-\u0E45\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD\u0EAE\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0F40-\u0F47\u0F49-\u0F69\u10A0-\u10C5\u10D0-\u10F6\u1100\u1102\u1103\u1105-\u1107\u1109\u110B\u110C\u110E-\u1112\u113C\u113E\u1140\u114C\u114E\u1150\u1154\u1155\u1159\u115F-\u1161\u1163\u1165\u1167\u1169\u116D\u116E\u1172\u1173\u1175\u119E\u11A8\u11AB\u11AE\u11AF\u11B7\u11B8\u11BA\u11BC-\u11C2\u11EB\u11F0\u11F9\u1E00-\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2126\u212A\u212B\u212E\u2180-\u2182\u3007\u3021-\u3029\u3041-\u3094\u30A1-\u30FA\u3105-\u312C\u4E00-\u9FA5\uAC00-\uD7A3';
|
|
2662
2662
|
const ncname = '[' + letter + '_][' + letter + digit + '\\.\\-_' + combiningChar + extender + ']*';
|
|
2663
2663
|
return '((?:' + ncname + '\\:)?' + ncname + ')';
|
|
2664
2664
|
})();
|
|
2665
2665
|
const startTagOpen = new RegExp('^<' + qnameCapture);
|
|
2666
2666
|
const startTagClose = /^\s*(\/?)>/;
|
|
2667
|
-
const endTag = new RegExp('
|
|
2667
|
+
const endTag = new RegExp('^</' + qnameCapture + '[^>]*>');
|
|
2668
2668
|
const doctype = /^<!DOCTYPE\s?[^>]+>/i;
|
|
2669
2669
|
|
|
2670
2670
|
let IS_REGEX_CAPTURING_BROKEN = false;
|
|
@@ -2763,9 +2763,6 @@ class HTMLParser {
|
|
|
2763
2763
|
let pos = 0;
|
|
2764
2764
|
let lastPos;
|
|
2765
2765
|
|
|
2766
|
-
// Helper to get remaining HTML from current position
|
|
2767
|
-
const remaining = () => fullHtml.slice(pos);
|
|
2768
|
-
|
|
2769
2766
|
// Helper to advance position
|
|
2770
2767
|
const advance = (n) => { pos += n; };
|
|
2771
2768
|
|
|
@@ -2784,22 +2781,32 @@ class HTMLParser {
|
|
|
2784
2781
|
return { line, column };
|
|
2785
2782
|
};
|
|
2786
2783
|
|
|
2784
|
+
// Helper to safely extract substring when needed for regex operations
|
|
2785
|
+
const sliceFromPos = (startPos, len) => {
|
|
2786
|
+
const endPos = fullLength;
|
|
2787
|
+
return fullHtml.slice(startPos, endPos);
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2787
2790
|
while (pos < fullLength) {
|
|
2788
2791
|
lastPos = pos;
|
|
2789
|
-
|
|
2792
|
+
|
|
2790
2793
|
// Make sure we’re not in a `script` or `style` element
|
|
2791
2794
|
if (!lastTag || !special.has(lastTag)) {
|
|
2792
|
-
|
|
2793
|
-
|
|
2795
|
+
const textEnd = fullHtml.indexOf('<', pos);
|
|
2796
|
+
|
|
2797
|
+
if (textEnd === pos) {
|
|
2798
|
+
// We found a tag at current position
|
|
2799
|
+
const remaining = sliceFromPos(pos);
|
|
2800
|
+
|
|
2794
2801
|
// Comment
|
|
2795
|
-
if (/^<!--/.test(
|
|
2796
|
-
const commentEnd =
|
|
2802
|
+
if (/^<!--/.test(remaining)) {
|
|
2803
|
+
const commentEnd = fullHtml.indexOf('-->', pos + 4);
|
|
2797
2804
|
|
|
2798
2805
|
if (commentEnd >= 0) {
|
|
2799
2806
|
if (handler.comment) {
|
|
2800
|
-
await handler.comment(
|
|
2807
|
+
await handler.comment(fullHtml.substring(pos + 4, commentEnd));
|
|
2801
2808
|
}
|
|
2802
|
-
advance(commentEnd + 3);
|
|
2809
|
+
advance(commentEnd + 3 - pos);
|
|
2803
2810
|
prevTag = '';
|
|
2804
2811
|
prevAttrs = [];
|
|
2805
2812
|
continue;
|
|
@@ -2807,14 +2814,14 @@ class HTMLParser {
|
|
|
2807
2814
|
}
|
|
2808
2815
|
|
|
2809
2816
|
// https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
2810
|
-
if (/^<!\[/.test(
|
|
2811
|
-
const conditionalEnd =
|
|
2817
|
+
if (/^<!\[/.test(remaining)) {
|
|
2818
|
+
const conditionalEnd = fullHtml.indexOf(']>', pos + 3);
|
|
2812
2819
|
|
|
2813
2820
|
if (conditionalEnd >= 0) {
|
|
2814
2821
|
if (handler.comment) {
|
|
2815
|
-
await handler.comment(
|
|
2822
|
+
await handler.comment(fullHtml.substring(pos + 2, conditionalEnd + 1), true /* Non-standard */);
|
|
2816
2823
|
}
|
|
2817
|
-
advance(conditionalEnd + 2);
|
|
2824
|
+
advance(conditionalEnd + 2 - pos);
|
|
2818
2825
|
prevTag = '';
|
|
2819
2826
|
prevAttrs = [];
|
|
2820
2827
|
continue;
|
|
@@ -2822,8 +2829,8 @@ class HTMLParser {
|
|
|
2822
2829
|
}
|
|
2823
2830
|
|
|
2824
2831
|
// Doctype
|
|
2825
|
-
|
|
2826
|
-
|
|
2832
|
+
if (doctype.test(remaining)) {
|
|
2833
|
+
const doctypeMatch = remaining.match(doctype);
|
|
2827
2834
|
if (handler.doctype) {
|
|
2828
2835
|
handler.doctype(doctypeMatch[0]);
|
|
2829
2836
|
}
|
|
@@ -2834,8 +2841,8 @@ class HTMLParser {
|
|
|
2834
2841
|
}
|
|
2835
2842
|
|
|
2836
2843
|
// End tag
|
|
2837
|
-
|
|
2838
|
-
|
|
2844
|
+
if (endTag.test(remaining)) {
|
|
2845
|
+
const endTagMatch = remaining.match(endTag);
|
|
2839
2846
|
advance(endTagMatch[0].length);
|
|
2840
2847
|
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
2841
2848
|
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
@@ -2844,7 +2851,7 @@ class HTMLParser {
|
|
|
2844
2851
|
}
|
|
2845
2852
|
|
|
2846
2853
|
// Start tag
|
|
2847
|
-
const startTagMatch = parseStartTag(
|
|
2854
|
+
const startTagMatch = parseStartTag(remaining, pos);
|
|
2848
2855
|
if (startTagMatch) {
|
|
2849
2856
|
advance(startTagMatch.advance);
|
|
2850
2857
|
await handleStartTag(startTagMatch);
|
|
@@ -2853,31 +2860,29 @@ class HTMLParser {
|
|
|
2853
2860
|
}
|
|
2854
2861
|
|
|
2855
2862
|
// Treat `<` as text
|
|
2856
|
-
if (handler.continueOnParseError)
|
|
2857
|
-
textEnd = html.indexOf('<', 1);
|
|
2858
|
-
}
|
|
2863
|
+
if (handler.continueOnParseError) ;
|
|
2859
2864
|
}
|
|
2860
2865
|
|
|
2861
2866
|
let text;
|
|
2862
2867
|
if (textEnd >= 0) {
|
|
2863
|
-
text =
|
|
2864
|
-
advance(textEnd);
|
|
2868
|
+
text = fullHtml.substring(pos, textEnd);
|
|
2869
|
+
advance(textEnd - pos);
|
|
2865
2870
|
} else {
|
|
2866
|
-
text =
|
|
2867
|
-
advance(
|
|
2871
|
+
text = fullHtml.substring(pos);
|
|
2872
|
+
advance(fullLength - pos);
|
|
2868
2873
|
}
|
|
2869
2874
|
|
|
2870
|
-
// Next tag
|
|
2871
|
-
const
|
|
2872
|
-
let nextTagMatch = parseStartTag(
|
|
2875
|
+
// Next tag for whitespace processing context
|
|
2876
|
+
const remainingAfterText = sliceFromPos(pos);
|
|
2877
|
+
let nextTagMatch = parseStartTag(remainingAfterText, pos);
|
|
2873
2878
|
if (nextTagMatch) {
|
|
2874
2879
|
nextTag = nextTagMatch.tagName;
|
|
2875
2880
|
// Extract minimal attribute info for whitespace logic (just name/value pairs)
|
|
2876
2881
|
nextAttrs = extractAttrInfo(nextTagMatch.attrs);
|
|
2877
2882
|
} else {
|
|
2878
|
-
|
|
2879
|
-
if (
|
|
2880
|
-
nextTag = '/' +
|
|
2883
|
+
const endTagMatch = remainingAfterText.match(endTag);
|
|
2884
|
+
if (endTagMatch) {
|
|
2885
|
+
nextTag = '/' + endTagMatch[1];
|
|
2881
2886
|
nextAttrs = [];
|
|
2882
2887
|
} else {
|
|
2883
2888
|
nextTag = '';
|
|
@@ -2893,10 +2898,11 @@ class HTMLParser {
|
|
|
2893
2898
|
} else {
|
|
2894
2899
|
const stackedTag = lastTag.toLowerCase();
|
|
2895
2900
|
// Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
|
|
2896
|
-
const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)
|
|
2901
|
+
const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
|
|
2897
2902
|
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2903
|
+
const remaining = sliceFromPos(pos);
|
|
2904
|
+
const m = reStackedTag.exec(remaining);
|
|
2905
|
+
if (m && m.index === 0) {
|
|
2900
2906
|
let text = m[1];
|
|
2901
2907
|
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
|
|
2902
2908
|
text = text
|
|
@@ -2907,12 +2913,12 @@ class HTMLParser {
|
|
|
2907
2913
|
await handler.chars(text);
|
|
2908
2914
|
}
|
|
2909
2915
|
// Advance HTML past the matched special tag content and its closing tag
|
|
2910
|
-
advance(m
|
|
2916
|
+
advance(m[0].length);
|
|
2911
2917
|
await parseEndTag('</' + stackedTag + '>', stackedTag);
|
|
2912
2918
|
} else {
|
|
2913
2919
|
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
2914
|
-
if (handler.continueOnParseError && handler.chars &&
|
|
2915
|
-
await handler.chars(
|
|
2920
|
+
if (handler.continueOnParseError && handler.chars && pos < fullLength) {
|
|
2921
|
+
await handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
2916
2922
|
advance(1);
|
|
2917
2923
|
} else {
|
|
2918
2924
|
break;
|
|
@@ -2932,7 +2938,7 @@ class HTMLParser {
|
|
|
2932
2938
|
continue;
|
|
2933
2939
|
}
|
|
2934
2940
|
const loc = getLineColumn(pos);
|
|
2935
|
-
// Include some context before the error position so the snippet contains the offending markup plus preceding characters (e.g.,
|
|
2941
|
+
// Include some context before the error position so the snippet contains the offending markup plus preceding characters (e.g., `invalid<tag`)
|
|
2936
2942
|
const CONTEXT_BEFORE = 50;
|
|
2937
2943
|
const startPos = Math.max(0, pos - CONTEXT_BEFORE);
|
|
2938
2944
|
const snippet = fullHtml.slice(startPos, startPos + 200).replace(/\n/g, ' ');
|
|
@@ -2964,8 +2970,8 @@ class HTMLParser {
|
|
|
2964
2970
|
}).filter(attr => attr.name); // Filter out invalid entries
|
|
2965
2971
|
}
|
|
2966
2972
|
|
|
2967
|
-
function parseStartTag(
|
|
2968
|
-
const start =
|
|
2973
|
+
function parseStartTag(remaining, startPos) {
|
|
2974
|
+
const start = remaining.match(startTagOpen);
|
|
2969
2975
|
if (start) {
|
|
2970
2976
|
const match = {
|
|
2971
2977
|
tagName: start[1],
|
|
@@ -2973,7 +2979,7 @@ class HTMLParser {
|
|
|
2973
2979
|
advance: 0
|
|
2974
2980
|
};
|
|
2975
2981
|
let consumed = start[0].length;
|
|
2976
|
-
|
|
2982
|
+
let currentPos = startPos + consumed;
|
|
2977
2983
|
let end, attr;
|
|
2978
2984
|
|
|
2979
2985
|
// Safety limit: Max length of input to check for attributes
|
|
@@ -2982,16 +2988,20 @@ class HTMLParser {
|
|
|
2982
2988
|
|
|
2983
2989
|
while (true) {
|
|
2984
2990
|
// Check for closing tag first
|
|
2985
|
-
|
|
2991
|
+
const remainingForEnd = sliceFromPos(currentPos);
|
|
2992
|
+
end = remainingForEnd.match(startTagClose);
|
|
2986
2993
|
if (end) {
|
|
2987
2994
|
break;
|
|
2988
2995
|
}
|
|
2989
2996
|
|
|
2990
2997
|
// Limit the input length we pass to the regex to prevent catastrophic backtracking
|
|
2991
|
-
const
|
|
2992
|
-
const
|
|
2998
|
+
const remainingLen = fullLength - currentPos;
|
|
2999
|
+
const isLimited = remainingLen > MAX_ATTR_PARSE_LENGTH;
|
|
3000
|
+
const extractEndPos = isLimited ? currentPos + MAX_ATTR_PARSE_LENGTH : fullLength;
|
|
2993
3001
|
|
|
2994
|
-
|
|
3002
|
+
// Create a temporary substring only for attribute parsing (this is limited and necessary for regex)
|
|
3003
|
+
const searchStr = fullHtml.substring(currentPos, extractEndPos);
|
|
3004
|
+
attr = searchStr.match(attribute);
|
|
2995
3005
|
|
|
2996
3006
|
// If we limited the input and got a match, check if the value might be truncated
|
|
2997
3007
|
if (attr && isLimited) {
|
|
@@ -3000,32 +3010,31 @@ class HTMLParser {
|
|
|
3000
3010
|
// If the match ends near the limit, the value might be truncated
|
|
3001
3011
|
if (attrEnd > MAX_ATTR_PARSE_LENGTH - 100) {
|
|
3002
3012
|
// Manually extract this attribute to handle potentially huge value
|
|
3003
|
-
const manualMatch =
|
|
3013
|
+
const manualMatch = searchStr.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
|
|
3004
3014
|
if (manualMatch) {
|
|
3005
|
-
const quoteChar =
|
|
3015
|
+
const quoteChar = searchStr[manualMatch[0].length];
|
|
3006
3016
|
if (quoteChar === '"' || quoteChar === "'") {
|
|
3007
|
-
const closeQuote =
|
|
3017
|
+
const closeQuote = searchStr.indexOf(quoteChar, manualMatch[0].length + 1);
|
|
3008
3018
|
if (closeQuote !== -1) {
|
|
3009
|
-
const
|
|
3019
|
+
const fullAttrLen = closeQuote + 1;
|
|
3010
3020
|
const numCustomParts = handler.customAttrSurround
|
|
3011
3021
|
? handler.customAttrSurround.length * NCP
|
|
3012
3022
|
: 0;
|
|
3013
3023
|
const baseIndex = 1 + numCustomParts;
|
|
3014
3024
|
|
|
3015
3025
|
attr = [];
|
|
3016
|
-
attr[0] =
|
|
3026
|
+
attr[0] = searchStr.substring(0, fullAttrLen);
|
|
3017
3027
|
attr[baseIndex] = manualMatch[1]; // Attribute name
|
|
3018
|
-
attr[baseIndex + 1] = '='; // `customAssign` (falls back to
|
|
3019
|
-
const value =
|
|
3028
|
+
attr[baseIndex + 1] = '='; // `customAssign` (falls back to "=" for huge attributes)
|
|
3029
|
+
const value = searchStr.substring(manualMatch[0].length + 1, closeQuote);
|
|
3020
3030
|
// Place value at correct index based on quote type
|
|
3021
3031
|
if (quoteChar === '"') {
|
|
3022
3032
|
attr[baseIndex + 2] = value; // Double-quoted value
|
|
3023
3033
|
} else {
|
|
3024
3034
|
attr[baseIndex + 3] = value; // Single-quoted value
|
|
3025
3035
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
consumed += attrLen;
|
|
3036
|
+
currentPos += fullAttrLen;
|
|
3037
|
+
consumed += fullAttrLen;
|
|
3029
3038
|
match.attrs.push(attr);
|
|
3030
3039
|
continue;
|
|
3031
3040
|
}
|
|
@@ -3038,18 +3047,55 @@ class HTMLParser {
|
|
|
3038
3047
|
}
|
|
3039
3048
|
}
|
|
3040
3049
|
|
|
3050
|
+
if (!attr && isLimited) {
|
|
3051
|
+
// If we limited the input and got no match, try manual extraction
|
|
3052
|
+
// This handles cases where quoted attributes exceed `MAX_ATTR_PARSE_LENGTH`
|
|
3053
|
+
const manualMatch = searchStr.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
|
|
3054
|
+
if (manualMatch) {
|
|
3055
|
+
const quoteChar = searchStr[manualMatch[0].length];
|
|
3056
|
+
if (quoteChar === '"' || quoteChar === "'") {
|
|
3057
|
+
// Search in the full HTML (not limited substring) for closing quote
|
|
3058
|
+
const closeQuote = fullHtml.indexOf(quoteChar, currentPos + manualMatch[0].length + 1);
|
|
3059
|
+
if (closeQuote !== -1) {
|
|
3060
|
+
const fullAttrLen = closeQuote - currentPos + 1;
|
|
3061
|
+
const numCustomParts = handler.customAttrSurround
|
|
3062
|
+
? handler.customAttrSurround.length * NCP
|
|
3063
|
+
: 0;
|
|
3064
|
+
const baseIndex = 1 + numCustomParts;
|
|
3065
|
+
|
|
3066
|
+
attr = [];
|
|
3067
|
+
attr[0] = fullHtml.substring(currentPos, closeQuote + 1);
|
|
3068
|
+
attr[baseIndex] = manualMatch[1]; // Attribute name
|
|
3069
|
+
attr[baseIndex + 1] = '='; // customAssign
|
|
3070
|
+
const value = fullHtml.substring(currentPos + manualMatch[0].length + 1, closeQuote);
|
|
3071
|
+
// Place value at correct index based on quote type
|
|
3072
|
+
if (quoteChar === '"') {
|
|
3073
|
+
attr[baseIndex + 2] = value; // Double-quoted value
|
|
3074
|
+
} else {
|
|
3075
|
+
attr[baseIndex + 3] = value; // Single-quoted value
|
|
3076
|
+
}
|
|
3077
|
+
currentPos += fullAttrLen;
|
|
3078
|
+
consumed += fullAttrLen;
|
|
3079
|
+
match.attrs.push(attr);
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3041
3086
|
if (!attr) {
|
|
3042
3087
|
break;
|
|
3043
3088
|
}
|
|
3044
3089
|
|
|
3045
3090
|
const attrLen = attr[0].length;
|
|
3046
|
-
|
|
3091
|
+
currentPos += attrLen;
|
|
3047
3092
|
consumed += attrLen;
|
|
3048
3093
|
match.attrs.push(attr);
|
|
3049
3094
|
}
|
|
3050
3095
|
|
|
3051
3096
|
// Check for closing tag
|
|
3052
|
-
|
|
3097
|
+
const remainingForClose = sliceFromPos(currentPos);
|
|
3098
|
+
end = remainingForClose.match(startTagClose);
|
|
3053
3099
|
if (end) {
|
|
3054
3100
|
match.unarySlash = end[1];
|
|
3055
3101
|
consumed += end[0].length;
|
|
@@ -3246,11 +3292,11 @@ class HTMLParser {
|
|
|
3246
3292
|
if (handler.end) {
|
|
3247
3293
|
handler.end(tagName, [], false);
|
|
3248
3294
|
}
|
|
3249
|
-
} else if (tagName.toLowerCase() === 'br') {
|
|
3295
|
+
} else if (tagName && tagName.toLowerCase() === 'br') {
|
|
3250
3296
|
if (handler.start) {
|
|
3251
3297
|
await handler.start(tagName, [], true, '');
|
|
3252
3298
|
}
|
|
3253
|
-
} else if (tagName.toLowerCase() === 'p') {
|
|
3299
|
+
} else if (tagName && tagName.toLowerCase() === 'p') {
|
|
3254
3300
|
if (handler.start) {
|
|
3255
3301
|
await handler.start(tagName, [], false, '', true);
|
|
3256
3302
|
}
|
|
@@ -3393,6 +3439,7 @@ const presets = {
|
|
|
3393
3439
|
collapseWhitespace: true,
|
|
3394
3440
|
continueOnParseError: true,
|
|
3395
3441
|
decodeEntities: true,
|
|
3442
|
+
mergeScripts: true,
|
|
3396
3443
|
minifyCSS: true,
|
|
3397
3444
|
minifyJS: true,
|
|
3398
3445
|
minifySVG: true,
|
|
@@ -3611,6 +3658,11 @@ const isSimpleBoolean = new Set(['allowfullscreen', 'async', 'autofocus', 'autop
|
|
|
3611
3658
|
|
|
3612
3659
|
const isBooleanValue = new Set(['true', 'false']);
|
|
3613
3660
|
|
|
3661
|
+
// Attributes where empty value can be collapsed to just the attribute name
|
|
3662
|
+
// `crossorigin=""` → `crossorigin` (empty string equals anonymous mode)
|
|
3663
|
+
// `contenteditable=""` → `contenteditable` (empty string equals `true`)
|
|
3664
|
+
const emptyCollapsible = new Set(['crossorigin', 'contenteditable']);
|
|
3665
|
+
|
|
3614
3666
|
// `srcset` elements
|
|
3615
3667
|
|
|
3616
3668
|
const srcsetElements = new Set(['img', 'source']);
|
|
@@ -6590,7 +6642,8 @@ function minifyNumber(num, precision = 3) {
|
|
|
6590
6642
|
const fixed = parsed.toFixed(precision);
|
|
6591
6643
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
6592
6644
|
|
|
6593
|
-
|
|
6645
|
+
// Remove leading zero before decimal point (e.g., `0.5` → `.5`, `-0.3` → `-.3`)
|
|
6646
|
+
const result = (trimmed || '0').replace(/^(-?)0\./, '$1.');
|
|
6594
6647
|
numberCache.set(cacheKey, result);
|
|
6595
6648
|
return result;
|
|
6596
6649
|
}
|
|
@@ -6610,17 +6663,23 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
6610
6663
|
});
|
|
6611
6664
|
|
|
6612
6665
|
// Remove unnecessary spaces around path commands
|
|
6613
|
-
// Safe to remove space after a command letter when it’s followed by a number
|
|
6614
|
-
//
|
|
6615
|
-
|
|
6666
|
+
// Safe to remove space after a command letter when it’s followed by a number
|
|
6667
|
+
// (which may be negative or start with a decimal point)
|
|
6668
|
+
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`, `M .5 .3` → `M.5.3`
|
|
6669
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\.?\d)/g, '$1');
|
|
6616
6670
|
|
|
6617
6671
|
// Safe to remove space before command letter when preceded by a number
|
|
6618
|
-
// `0 L` → `0L`, `20 M` → `20M`
|
|
6619
|
-
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
6672
|
+
// `0 L` → `0L`, `20 M` → `20M`, `.5 L` → `.5L`
|
|
6673
|
+
result = result.replace(/([\d.])\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
6620
6674
|
|
|
6621
6675
|
// Safe to remove space before negative number when preceded by a number
|
|
6622
|
-
// `10 -20` → `10-20` (
|
|
6623
|
-
result = result.replace(/(\d)\s+(
|
|
6676
|
+
// `10 -20` → `10-20`, `.5 -.3` → `.5-.3` (minus sign is always a separator)
|
|
6677
|
+
result = result.replace(/([\d.])\s+(-)/g, '$1$2');
|
|
6678
|
+
|
|
6679
|
+
// Safe to remove space between two decimal numbers (decimal point acts as separator)
|
|
6680
|
+
// `.5 .3` → `.5.3` (only when previous char is `.`, indicating a complete decimal)
|
|
6681
|
+
// Note: `0 .3` must not become `0.3` (that would change two numbers into one)
|
|
6682
|
+
result = result.replace(/(\.\d*)\s+(\.)/g, '$1$2');
|
|
6624
6683
|
|
|
6625
6684
|
return result;
|
|
6626
6685
|
}
|
|
@@ -6918,10 +6977,9 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6918
6977
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
6919
6978
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
6920
6979
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
6921
|
-
* @param {LRU} deps.urlMinifyCache - URL minification cache
|
|
6922
6980
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
6923
6981
|
*/
|
|
6924
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache
|
|
6982
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
6925
6983
|
const options = {
|
|
6926
6984
|
name: function (name) {
|
|
6927
6985
|
return name.toLowerCase();
|
|
@@ -7215,7 +7273,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7215
7273
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
7216
7274
|
|
|
7217
7275
|
// Create instance-specific cache (results depend on site configuration)
|
|
7218
|
-
const instanceCache =
|
|
7276
|
+
const instanceCache = new LRU(500);
|
|
7219
7277
|
|
|
7220
7278
|
options.minifyURLs = function (text) {
|
|
7221
7279
|
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
@@ -7224,20 +7282,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7224
7282
|
return text;
|
|
7225
7283
|
}
|
|
7226
7284
|
|
|
7227
|
-
// Check
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
return cached;
|
|
7232
|
-
}
|
|
7285
|
+
// Check cache
|
|
7286
|
+
const cached = instanceCache.get(text);
|
|
7287
|
+
if (cached !== undefined) {
|
|
7288
|
+
return cached;
|
|
7233
7289
|
}
|
|
7234
7290
|
|
|
7235
7291
|
try {
|
|
7236
7292
|
const result = relateUrlInstance.relate(text);
|
|
7237
|
-
|
|
7238
|
-
if (instanceCache) {
|
|
7239
|
-
instanceCache.set(text, result);
|
|
7240
|
-
}
|
|
7293
|
+
instanceCache.set(text, result);
|
|
7241
7294
|
return result;
|
|
7242
7295
|
} catch (err) {
|
|
7243
7296
|
// Don’t cache errors
|
|
@@ -7397,7 +7450,9 @@ function isStyleElement(tag, attrs) {
|
|
|
7397
7450
|
}
|
|
7398
7451
|
|
|
7399
7452
|
function isBooleanAttribute(attrName, attrValue) {
|
|
7400
|
-
return isSimpleBoolean.has(attrName) ||
|
|
7453
|
+
return isSimpleBoolean.has(attrName) ||
|
|
7454
|
+
(attrName === 'draggable' && !isBooleanValue.has(attrValue)) ||
|
|
7455
|
+
(attrValue === '' && emptyCollapsible.has(attrName));
|
|
7401
7456
|
}
|
|
7402
7457
|
|
|
7403
7458
|
function isUriTypeAttribute(attrName, tag) {
|
|
@@ -8074,11 +8129,125 @@ async function getSwc() {
|
|
|
8074
8129
|
return swcPromise;
|
|
8075
8130
|
}
|
|
8076
8131
|
|
|
8077
|
-
// Minification caches
|
|
8132
|
+
// Minification caches (initialized on first use with configurable sizes)
|
|
8133
|
+
let cssMinifyCache = null;
|
|
8134
|
+
let jsMinifyCache = null;
|
|
8135
|
+
|
|
8136
|
+
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
8137
|
+
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
8138
|
+
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
8139
|
+
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
8140
|
+
|
|
8141
|
+
// Pre-compiled patterns for buffer scanning
|
|
8142
|
+
const RE_START_TAG = /^<[^/!]/;
|
|
8143
|
+
const RE_END_TAG = /^<\//;
|
|
8144
|
+
|
|
8145
|
+
// Script merging
|
|
8078
8146
|
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8147
|
+
/**
|
|
8148
|
+
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
8149
|
+
* Only merges scripts that are compatible:
|
|
8150
|
+
* - Both inline (no `src` attribute)
|
|
8151
|
+
* - Same `type` (or both default JavaScript)
|
|
8152
|
+
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
8153
|
+
*
|
|
8154
|
+
* Limitation: This function uses regex-based matching (`pattern` variable below),
|
|
8155
|
+
* which can produce incorrect results if a script’s content contains a literal
|
|
8156
|
+
* `</script>` string (e.g., `document.write('<script>…</script>')`). In valid
|
|
8157
|
+
* HTML, such strings should be escaped as `<\/script>` or split like
|
|
8158
|
+
* `'</scr' + 'ipt>'`, so this limitation rarely affects real-world code. The
|
|
8159
|
+
* earlier `minifyJS` step (if enabled) typically handles this escaping already.
|
|
8160
|
+
*
|
|
8161
|
+
* @param {string} html - The HTML string to process
|
|
8162
|
+
* @returns {string} HTML with consecutive scripts merged
|
|
8163
|
+
*/
|
|
8164
|
+
function mergeConsecutiveScripts(html) {
|
|
8165
|
+
// `pattern`: Regex to match consecutive `</script>` followed by `<script…>`.
|
|
8166
|
+
// See function JSDoc above for known limitations with literal `</script>` in content.
|
|
8167
|
+
// Captures:
|
|
8168
|
+
// 1. first script attrs
|
|
8169
|
+
// 2. first script content
|
|
8170
|
+
// 3. whitespace between
|
|
8171
|
+
// 4. second script attrs
|
|
8172
|
+
// 5. second script content
|
|
8173
|
+
const pattern = /<script([^>]*)>([\s\S]*?)<\/script>([\s]*)<script([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
8174
|
+
|
|
8175
|
+
let result = html;
|
|
8176
|
+
let changed = true;
|
|
8177
|
+
|
|
8178
|
+
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
8179
|
+
while (changed) {
|
|
8180
|
+
changed = false;
|
|
8181
|
+
result = result.replace(pattern, (match, attrs1, content1, whitespace, attrs2, content2) => {
|
|
8182
|
+
// Parse attributes from both script tags (uses pre-compiled RE_SCRIPT_ATTRS)
|
|
8183
|
+
const parseAttrs = (attrStr) => {
|
|
8184
|
+
const attrs = {};
|
|
8185
|
+
RE_SCRIPT_ATTRS.lastIndex = 0; // Reset for reuse
|
|
8186
|
+
let m;
|
|
8187
|
+
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
8188
|
+
const name = m[1].toLowerCase();
|
|
8189
|
+
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
8190
|
+
attrs[name] = value;
|
|
8191
|
+
}
|
|
8192
|
+
return attrs;
|
|
8193
|
+
};
|
|
8194
|
+
|
|
8195
|
+
const a1 = parseAttrs(attrs1);
|
|
8196
|
+
const a2 = parseAttrs(attrs2);
|
|
8197
|
+
|
|
8198
|
+
// Check for `src`—cannot merge external scripts
|
|
8199
|
+
if ('src' in a1 || 'src' in a2) {
|
|
8200
|
+
return match;
|
|
8201
|
+
}
|
|
8202
|
+
|
|
8203
|
+
// Check `type` compatibility (both must be same, or both default JS)
|
|
8204
|
+
const type1 = a1.type || '';
|
|
8205
|
+
const type2 = a2.type || '';
|
|
8206
|
+
|
|
8207
|
+
if (DEFAULT_JS_TYPES.has(type1) && DEFAULT_JS_TYPES.has(type2)) ; else if (type1 === type2) ; else {
|
|
8208
|
+
// Incompatible types
|
|
8209
|
+
return match;
|
|
8210
|
+
}
|
|
8211
|
+
|
|
8212
|
+
// Check for conflicting boolean attributes (uses pre-compiled SCRIPT_BOOL_ATTRS)
|
|
8213
|
+
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
8214
|
+
const has1 = attr in a1;
|
|
8215
|
+
const has2 = attr in a2;
|
|
8216
|
+
if (has1 !== has2) {
|
|
8217
|
+
// One has it, one doesn't - incompatible
|
|
8218
|
+
return match;
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
8221
|
+
|
|
8222
|
+
// Check `nonce`—must be same or both absent
|
|
8223
|
+
if (a1.nonce !== a2.nonce) {
|
|
8224
|
+
return match;
|
|
8225
|
+
}
|
|
8226
|
+
|
|
8227
|
+
// Scripts are compatible—merge them
|
|
8228
|
+
changed = true;
|
|
8229
|
+
|
|
8230
|
+
// Combine content—use semicolon normally, newline only for trailing `//` comments
|
|
8231
|
+
const c1 = content1.trim();
|
|
8232
|
+
const c2 = content2.trim();
|
|
8233
|
+
let mergedContent;
|
|
8234
|
+
if (c1 && c2) {
|
|
8235
|
+
// Check if last line of c1 contains `//` (single-line comment)
|
|
8236
|
+
// If so, use newline to terminate it; otherwise use semicolon (if not already present)
|
|
8237
|
+
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
8238
|
+
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
8239
|
+
mergedContent = c1 + separator + c2;
|
|
8240
|
+
} else {
|
|
8241
|
+
mergedContent = c1 || c2;
|
|
8242
|
+
}
|
|
8243
|
+
|
|
8244
|
+
// Use first script’s attributes (they should be compatible)
|
|
8245
|
+
return `<script${attrs1}>${mergedContent}</script>`;
|
|
8246
|
+
});
|
|
8247
|
+
}
|
|
8248
|
+
|
|
8249
|
+
return result;
|
|
8250
|
+
}
|
|
8082
8251
|
|
|
8083
8252
|
// Type definitions
|
|
8084
8253
|
|
|
@@ -8111,6 +8280,24 @@ const urlMinifyCache = new LRU(500);
|
|
|
8111
8280
|
*
|
|
8112
8281
|
* Default: Built-in `canTrimWhitespace` function
|
|
8113
8282
|
*
|
|
8283
|
+
* @prop {number} [cacheCSS]
|
|
8284
|
+
* The maximum number of entries for the CSS minification cache. Higher values
|
|
8285
|
+
* improve performance for inputs with repeated CSS (e.g., batch processing).
|
|
8286
|
+
* - Cache is created on first `minify()` call and persists for the process lifetime
|
|
8287
|
+
* - Cache size is locked after first call—subsequent calls reuse the same cache
|
|
8288
|
+
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
8289
|
+
*
|
|
8290
|
+
* Default: `500` (or `1000` when `CI=true` environment variable is set)
|
|
8291
|
+
*
|
|
8292
|
+
* @prop {number} [cacheJS]
|
|
8293
|
+
* The maximum number of entries for the JavaScript minification cache. Higher
|
|
8294
|
+
* values improve performance for inputs with repeated JavaScript.
|
|
8295
|
+
* - Cache is created on first `minify()` call and persists for the process lifetime
|
|
8296
|
+
* - Cache size is locked after first call—subsequent calls reuse the same cache
|
|
8297
|
+
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
8298
|
+
*
|
|
8299
|
+
* Default: `500` (or `1000` when `CI=true` environment variable is set)
|
|
8300
|
+
*
|
|
8114
8301
|
* @prop {boolean} [caseSensitive]
|
|
8115
8302
|
* When true, tag and attribute names are treated as case-sensitive.
|
|
8116
8303
|
* Useful for custom HTML tags.
|
|
@@ -8260,6 +8447,13 @@ const urlMinifyCache = new LRU(500);
|
|
|
8260
8447
|
*
|
|
8261
8448
|
* Default: No limit
|
|
8262
8449
|
*
|
|
8450
|
+
* @prop {boolean} [mergeScripts]
|
|
8451
|
+
* When true, consecutive inline `<script>` elements are merged into one.
|
|
8452
|
+
* Only merges compatible scripts (same `type`, matching `async`/`defer`/
|
|
8453
|
+
* `nomodule`/`nonce` attributes). Does not merge external scripts (with `src`).
|
|
8454
|
+
*
|
|
8455
|
+
* Default: `false`
|
|
8456
|
+
*
|
|
8263
8457
|
* @prop {boolean | Partial<import("lightningcss").TransformOptions<import("lightningcss").CustomAtRules>> | ((text: string, type?: string) => Promise<string> | string)} [minifyCSS]
|
|
8264
8458
|
* When true, enables CSS minification for inline `<style>` tags or
|
|
8265
8459
|
* `style` attributes. If an object is provided, it is passed to
|
|
@@ -8838,7 +9032,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8838
9032
|
|
|
8839
9033
|
function removeStartTag() {
|
|
8840
9034
|
let index = buffer.length - 1;
|
|
8841
|
-
while (index > 0 &&
|
|
9035
|
+
while (index > 0 && !RE_START_TAG.test(buffer[index])) {
|
|
8842
9036
|
index--;
|
|
8843
9037
|
}
|
|
8844
9038
|
buffer.length = Math.max(0, index);
|
|
@@ -8846,7 +9040,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8846
9040
|
|
|
8847
9041
|
function removeEndTag() {
|
|
8848
9042
|
let index = buffer.length - 1;
|
|
8849
|
-
while (index > 0 &&
|
|
9043
|
+
while (index > 0 && !RE_END_TAG.test(buffer[index])) {
|
|
8850
9044
|
index--;
|
|
8851
9045
|
}
|
|
8852
9046
|
buffer.length = Math.max(0, index);
|
|
@@ -9157,8 +9351,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
9157
9351
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
9158
9352
|
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
9159
9353
|
// Escape any `&` symbols that start either:
|
|
9160
|
-
// 1
|
|
9161
|
-
// 2
|
|
9354
|
+
// 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
9355
|
+
// 2. or any other character reference (i.e., one that does end with `;`)
|
|
9162
9356
|
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
9163
9357
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
9164
9358
|
if (text.indexOf('&') !== -1) {
|
|
@@ -9362,6 +9556,49 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
9362
9556
|
return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
|
|
9363
9557
|
}
|
|
9364
9558
|
|
|
9559
|
+
/**
|
|
9560
|
+
* Initialize minification caches with configurable sizes.
|
|
9561
|
+
*
|
|
9562
|
+
* Important behavior notes:
|
|
9563
|
+
* - Caches are created on the first `minify()` call and persist for the lifetime of the process
|
|
9564
|
+
* - Cache sizes are locked after first initialization—subsequent calls use the same caches
|
|
9565
|
+
* even if different `cacheCSS`/`cacheJS` options are provided
|
|
9566
|
+
* - The first call’s options determine the cache sizes for subsequent calls
|
|
9567
|
+
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
9568
|
+
*/
|
|
9569
|
+
function initCaches(options) {
|
|
9570
|
+
// Only create caches once (on first call)—sizes are locked after this
|
|
9571
|
+
if (!cssMinifyCache) {
|
|
9572
|
+
// Determine default size based on environment
|
|
9573
|
+
const defaultSize = process.env.CI === 'true' ? 1000 : 500;
|
|
9574
|
+
|
|
9575
|
+
// Helper to parse env var—returns parsed number (including 0) or undefined if absent, invalid, or negative
|
|
9576
|
+
const parseEnvCacheSize = (envVar) => {
|
|
9577
|
+
if (envVar === undefined) return undefined;
|
|
9578
|
+
const parsed = Number(envVar);
|
|
9579
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) {
|
|
9580
|
+
return undefined;
|
|
9581
|
+
}
|
|
9582
|
+
return parsed;
|
|
9583
|
+
};
|
|
9584
|
+
|
|
9585
|
+
// Get cache sizes with precedence: Options > env > default
|
|
9586
|
+
const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
|
|
9587
|
+
: (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
|
|
9588
|
+
const jsSize = options.cacheJS !== undefined ? options.cacheJS
|
|
9589
|
+
: (parseEnvCacheSize(process.env.HMN_CACHE_JS) ?? defaultSize);
|
|
9590
|
+
|
|
9591
|
+
// Coerce `0` to `1` (minimum functional cache size) to avoid immediate eviction
|
|
9592
|
+
const cssFinalSize = cssSize === 0 ? 1 : cssSize;
|
|
9593
|
+
const jsFinalSize = jsSize === 0 ? 1 : jsSize;
|
|
9594
|
+
|
|
9595
|
+
cssMinifyCache = new LRU(cssFinalSize);
|
|
9596
|
+
jsMinifyCache = new LRU(jsFinalSize);
|
|
9597
|
+
}
|
|
9598
|
+
|
|
9599
|
+
return { cssMinifyCache, jsMinifyCache };
|
|
9600
|
+
}
|
|
9601
|
+
|
|
9365
9602
|
/**
|
|
9366
9603
|
* @param {string} value
|
|
9367
9604
|
* @param {MinifierOptions} [options]
|
|
@@ -9369,15 +9606,23 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
9369
9606
|
*/
|
|
9370
9607
|
const minify$1 = async function (value, options) {
|
|
9371
9608
|
const start = Date.now();
|
|
9609
|
+
|
|
9610
|
+
// Initialize caches on first use with configurable sizes
|
|
9611
|
+
const caches = initCaches(options || {});
|
|
9612
|
+
|
|
9372
9613
|
options = processOptions(options || {}, {
|
|
9373
9614
|
getLightningCSS,
|
|
9374
9615
|
getTerser,
|
|
9375
9616
|
getSwc,
|
|
9376
|
-
|
|
9377
|
-
jsMinifyCache,
|
|
9378
|
-
urlMinifyCache
|
|
9617
|
+
...caches
|
|
9379
9618
|
});
|
|
9380
|
-
|
|
9619
|
+
let result = await minifyHTML(value, options);
|
|
9620
|
+
|
|
9621
|
+
// Post-processing: Merge consecutive inline scripts if enabled
|
|
9622
|
+
if (options.mergeScripts) {
|
|
9623
|
+
result = mergeConsecutiveScripts(result);
|
|
9624
|
+
}
|
|
9625
|
+
|
|
9381
9626
|
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
9382
9627
|
return result;
|
|
9383
9628
|
};
|