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.
@@ -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 = '\\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';
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('^<\\/' + qnameCapture + '[^>]*>');
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
- const html = remaining();
2792
+
2790
2793
  // Make sure we’re not in a `script` or `style` element
2791
2794
  if (!lastTag || !special.has(lastTag)) {
2792
- let textEnd = html.indexOf('<');
2793
- if (textEnd === 0) {
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(html)) {
2796
- const commentEnd = html.indexOf('-->');
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(html.substring(4, commentEnd));
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(html)) {
2811
- const conditionalEnd = html.indexOf(']>');
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(html.substring(2, conditionalEnd + 1), true /* Non-standard */);
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
- const doctypeMatch = html.match(doctype);
2826
- if (doctypeMatch) {
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
- const endTagMatch = html.match(endTag);
2838
- if (endTagMatch) {
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(html);
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 = html.substring(0, textEnd);
2864
- advance(textEnd);
2868
+ text = fullHtml.substring(pos, textEnd);
2869
+ advance(textEnd - pos);
2865
2870
  } else {
2866
- text = html;
2867
- advance(html.length);
2871
+ text = fullHtml.substring(pos);
2872
+ advance(fullLength - pos);
2868
2873
  }
2869
2874
 
2870
- // Next tag
2871
- const nextHtml = remaining();
2872
- let nextTagMatch = parseStartTag(nextHtml);
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
- nextTagMatch = nextHtml.match(endTag);
2879
- if (nextTagMatch) {
2880
- nextTag = '/' + nextTagMatch[1];
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]*?)</' + stackedTag + '[^>]*>', 'i'));
2901
+ const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
2897
2902
 
2898
- const m = reStackedTag.exec(html);
2899
- if (m) {
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.index + m[0].length);
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 && html) {
2915
- await handler.chars(html[0], prevTag, '', prevAttrs, []);
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., invalid<tag)
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(input) {
2968
- const start = input.match(startTagOpen);
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
- input = input.slice(consumed);
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
- end = input.match(startTagClose);
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 isLimited = input.length > MAX_ATTR_PARSE_LENGTH;
2992
- const searchInput = isLimited ? input.slice(0, MAX_ATTR_PARSE_LENGTH) : input;
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
- attr = searchInput.match(attribute);
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 = input.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
3013
+ const manualMatch = searchStr.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
3004
3014
  if (manualMatch) {
3005
- const quoteChar = input[manualMatch[0].length];
3015
+ const quoteChar = searchStr[manualMatch[0].length];
3006
3016
  if (quoteChar === '"' || quoteChar === "'") {
3007
- const closeQuote = input.indexOf(quoteChar, manualMatch[0].length + 1);
3017
+ const closeQuote = searchStr.indexOf(quoteChar, manualMatch[0].length + 1);
3008
3018
  if (closeQuote !== -1) {
3009
- const fullAttr = input.slice(0, closeQuote + 1);
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] = fullAttr;
3026
+ attr[0] = searchStr.substring(0, fullAttrLen);
3017
3027
  attr[baseIndex] = manualMatch[1]; // Attribute name
3018
- attr[baseIndex + 1] = '='; // `customAssign` (falls back to “=” for huge attributes)
3019
- const value = input.slice(manualMatch[0].length + 1, closeQuote);
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
- const attrLen = fullAttr.length;
3027
- input = input.slice(attrLen);
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
- input = input.slice(attrLen);
3091
+ currentPos += attrLen;
3047
3092
  consumed += attrLen;
3048
3093
  match.attrs.push(attr);
3049
3094
  }
3050
3095
 
3051
3096
  // Check for closing tag
3052
- end = input.match(startTagClose);
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
- const result = trimmed || '0';
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 (which may be negative)
6614
- // `M 10 20` `M10 20`, `L -5 -3` → `L-5-3`
6615
- result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
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` (numbers are separated by the minus sign)
6623
- result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
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, urlMinifyCache } = {}) => {
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 = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
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 instance-specific cache
7228
- if (instanceCache) {
7229
- const cached = instanceCache.get(text);
7230
- if (cached !== undefined) {
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
- // Cache successful results
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) || (attrName === 'draggable' && !isBooleanValue.has(attrValue));
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
- const cssMinifyCache = new LRU(500);
8080
- const jsMinifyCache = new LRU(500);
8081
- const urlMinifyCache = new LRU(500);
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 && !/^<[^/!]/.test(buffer[index])) {
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 && !/^<\//.test(buffer[index])) {
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) a legacy-named character reference (i.e., one that doesn’t end with `;`)
9161
- // 2) or any other character reference (i.e., one that does end with `;`)
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 `&amp`, 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
- cssMinifyCache,
9377
- jsMinifyCache,
9378
- urlMinifyCache
9617
+ ...caches
9379
9618
  });
9380
- const result = await minifyHTML(value, options);
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
  };