html-minifier-next 4.18.0 → 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.
@@ -43,16 +43,16 @@ const singleAttrValues = [
43
43
  // https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
44
44
  const qnameCapture = (function () {
45
45
  // https://www.npmjs.com/package/ncname
46
- 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';
47
- 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';
48
- const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
49
- 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';
46
+ 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';
47
+ 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';
48
+ const extender = '\xB7\u02D0\u02D1\u0387\u0640\u0E46\u0EC6\u3005\u3031-\u3035\u309D\u309E\u30FC-\u30FE';
49
+ 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';
50
50
  const ncname = '[' + letter + '_][' + letter + digit + '\\.\\-_' + combiningChar + extender + ']*';
51
51
  return '((?:' + ncname + '\\:)?' + ncname + ')';
52
52
  })();
53
53
  const startTagOpen = new RegExp('^<' + qnameCapture);
54
54
  const startTagClose = /^\s*(\/?)>/;
55
- const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
55
+ const endTag = new RegExp('^</' + qnameCapture + '[^>]*>');
56
56
  const doctype = /^<!DOCTYPE\s?[^>]+>/i;
57
57
 
58
58
  let IS_REGEX_CAPTURING_BROKEN = false;
@@ -151,9 +151,6 @@ class HTMLParser {
151
151
  let pos = 0;
152
152
  let lastPos;
153
153
 
154
- // Helper to get remaining HTML from current position
155
- const remaining = () => fullHtml.slice(pos);
156
-
157
154
  // Helper to advance position
158
155
  const advance = (n) => { pos += n; };
159
156
 
@@ -172,22 +169,32 @@ class HTMLParser {
172
169
  return { line, column };
173
170
  };
174
171
 
172
+ // Helper to safely extract substring when needed for regex operations
173
+ const sliceFromPos = (startPos, len) => {
174
+ const endPos = fullLength;
175
+ return fullHtml.slice(startPos, endPos);
176
+ };
177
+
175
178
  while (pos < fullLength) {
176
179
  lastPos = pos;
177
- const html = remaining();
180
+
178
181
  // Make sure we’re not in a `script` or `style` element
179
182
  if (!lastTag || !special.has(lastTag)) {
180
- let textEnd = html.indexOf('<');
181
- if (textEnd === 0) {
183
+ const textEnd = fullHtml.indexOf('<', pos);
184
+
185
+ if (textEnd === pos) {
186
+ // We found a tag at current position
187
+ const remaining = sliceFromPos(pos);
188
+
182
189
  // Comment
183
- if (/^<!--/.test(html)) {
184
- const commentEnd = html.indexOf('-->');
190
+ if (/^<!--/.test(remaining)) {
191
+ const commentEnd = fullHtml.indexOf('-->', pos + 4);
185
192
 
186
193
  if (commentEnd >= 0) {
187
194
  if (handler.comment) {
188
- await handler.comment(html.substring(4, commentEnd));
195
+ await handler.comment(fullHtml.substring(pos + 4, commentEnd));
189
196
  }
190
- advance(commentEnd + 3);
197
+ advance(commentEnd + 3 - pos);
191
198
  prevTag = '';
192
199
  prevAttrs = [];
193
200
  continue;
@@ -195,14 +202,14 @@ class HTMLParser {
195
202
  }
196
203
 
197
204
  // https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
198
- if (/^<!\[/.test(html)) {
199
- const conditionalEnd = html.indexOf(']>');
205
+ if (/^<!\[/.test(remaining)) {
206
+ const conditionalEnd = fullHtml.indexOf(']>', pos + 3);
200
207
 
201
208
  if (conditionalEnd >= 0) {
202
209
  if (handler.comment) {
203
- await handler.comment(html.substring(2, conditionalEnd + 1), true /* Non-standard */);
210
+ await handler.comment(fullHtml.substring(pos + 2, conditionalEnd + 1), true /* Non-standard */);
204
211
  }
205
- advance(conditionalEnd + 2);
212
+ advance(conditionalEnd + 2 - pos);
206
213
  prevTag = '';
207
214
  prevAttrs = [];
208
215
  continue;
@@ -210,8 +217,8 @@ class HTMLParser {
210
217
  }
211
218
 
212
219
  // Doctype
213
- const doctypeMatch = html.match(doctype);
214
- if (doctypeMatch) {
220
+ if (doctype.test(remaining)) {
221
+ const doctypeMatch = remaining.match(doctype);
215
222
  if (handler.doctype) {
216
223
  handler.doctype(doctypeMatch[0]);
217
224
  }
@@ -222,8 +229,8 @@ class HTMLParser {
222
229
  }
223
230
 
224
231
  // End tag
225
- const endTagMatch = html.match(endTag);
226
- if (endTagMatch) {
232
+ if (endTag.test(remaining)) {
233
+ const endTagMatch = remaining.match(endTag);
227
234
  advance(endTagMatch[0].length);
228
235
  await parseEndTag(endTagMatch[0], endTagMatch[1]);
229
236
  prevTag = '/' + endTagMatch[1].toLowerCase();
@@ -232,7 +239,7 @@ class HTMLParser {
232
239
  }
233
240
 
234
241
  // Start tag
235
- const startTagMatch = parseStartTag(html);
242
+ const startTagMatch = parseStartTag(remaining, pos);
236
243
  if (startTagMatch) {
237
244
  advance(startTagMatch.advance);
238
245
  await handleStartTag(startTagMatch);
@@ -241,31 +248,29 @@ class HTMLParser {
241
248
  }
242
249
 
243
250
  // Treat `<` as text
244
- if (handler.continueOnParseError) {
245
- textEnd = html.indexOf('<', 1);
246
- }
251
+ if (handler.continueOnParseError) ;
247
252
  }
248
253
 
249
254
  let text;
250
255
  if (textEnd >= 0) {
251
- text = html.substring(0, textEnd);
252
- advance(textEnd);
256
+ text = fullHtml.substring(pos, textEnd);
257
+ advance(textEnd - pos);
253
258
  } else {
254
- text = html;
255
- advance(html.length);
259
+ text = fullHtml.substring(pos);
260
+ advance(fullLength - pos);
256
261
  }
257
262
 
258
- // Next tag
259
- const nextHtml = remaining();
260
- let nextTagMatch = parseStartTag(nextHtml);
263
+ // Next tag for whitespace processing context
264
+ const remainingAfterText = sliceFromPos(pos);
265
+ let nextTagMatch = parseStartTag(remainingAfterText, pos);
261
266
  if (nextTagMatch) {
262
267
  nextTag = nextTagMatch.tagName;
263
268
  // Extract minimal attribute info for whitespace logic (just name/value pairs)
264
269
  nextAttrs = extractAttrInfo(nextTagMatch.attrs);
265
270
  } else {
266
- nextTagMatch = nextHtml.match(endTag);
267
- if (nextTagMatch) {
268
- nextTag = '/' + nextTagMatch[1];
271
+ const endTagMatch = remainingAfterText.match(endTag);
272
+ if (endTagMatch) {
273
+ nextTag = '/' + endTagMatch[1];
269
274
  nextAttrs = [];
270
275
  } else {
271
276
  nextTag = '';
@@ -281,10 +286,11 @@ class HTMLParser {
281
286
  } else {
282
287
  const stackedTag = lastTag.toLowerCase();
283
288
  // Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
284
- const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'));
289
+ const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
285
290
 
286
- const m = reStackedTag.exec(html);
287
- if (m) {
291
+ const remaining = sliceFromPos(pos);
292
+ const m = reStackedTag.exec(remaining);
293
+ if (m && m.index === 0) {
288
294
  let text = m[1];
289
295
  if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
290
296
  text = text
@@ -295,12 +301,12 @@ class HTMLParser {
295
301
  await handler.chars(text);
296
302
  }
297
303
  // Advance HTML past the matched special tag content and its closing tag
298
- advance(m.index + m[0].length);
304
+ advance(m[0].length);
299
305
  await parseEndTag('</' + stackedTag + '>', stackedTag);
300
306
  } else {
301
307
  // No closing tag found; to avoid infinite loop, break similarly to previous behavior
302
- if (handler.continueOnParseError && handler.chars && html) {
303
- await handler.chars(html[0], prevTag, '', prevAttrs, []);
308
+ if (handler.continueOnParseError && handler.chars && pos < fullLength) {
309
+ await handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
304
310
  advance(1);
305
311
  } else {
306
312
  break;
@@ -320,7 +326,7 @@ class HTMLParser {
320
326
  continue;
321
327
  }
322
328
  const loc = getLineColumn(pos);
323
- // Include some context before the error position so the snippet contains the offending markup plus preceding characters (e.g., invalid<tag)
329
+ // Include some context before the error position so the snippet contains the offending markup plus preceding characters (e.g., `invalid<tag`)
324
330
  const CONTEXT_BEFORE = 50;
325
331
  const startPos = Math.max(0, pos - CONTEXT_BEFORE);
326
332
  const snippet = fullHtml.slice(startPos, startPos + 200).replace(/\n/g, ' ');
@@ -352,8 +358,8 @@ class HTMLParser {
352
358
  }).filter(attr => attr.name); // Filter out invalid entries
353
359
  }
354
360
 
355
- function parseStartTag(input) {
356
- const start = input.match(startTagOpen);
361
+ function parseStartTag(remaining, startPos) {
362
+ const start = remaining.match(startTagOpen);
357
363
  if (start) {
358
364
  const match = {
359
365
  tagName: start[1],
@@ -361,7 +367,7 @@ class HTMLParser {
361
367
  advance: 0
362
368
  };
363
369
  let consumed = start[0].length;
364
- input = input.slice(consumed);
370
+ let currentPos = startPos + consumed;
365
371
  let end, attr;
366
372
 
367
373
  // Safety limit: Max length of input to check for attributes
@@ -370,16 +376,20 @@ class HTMLParser {
370
376
 
371
377
  while (true) {
372
378
  // Check for closing tag first
373
- end = input.match(startTagClose);
379
+ const remainingForEnd = sliceFromPos(currentPos);
380
+ end = remainingForEnd.match(startTagClose);
374
381
  if (end) {
375
382
  break;
376
383
  }
377
384
 
378
385
  // Limit the input length we pass to the regex to prevent catastrophic backtracking
379
- const isLimited = input.length > MAX_ATTR_PARSE_LENGTH;
380
- const searchInput = isLimited ? input.slice(0, MAX_ATTR_PARSE_LENGTH) : input;
386
+ const remainingLen = fullLength - currentPos;
387
+ const isLimited = remainingLen > MAX_ATTR_PARSE_LENGTH;
388
+ const extractEndPos = isLimited ? currentPos + MAX_ATTR_PARSE_LENGTH : fullLength;
381
389
 
382
- attr = searchInput.match(attribute);
390
+ // Create a temporary substring only for attribute parsing (this is limited and necessary for regex)
391
+ const searchStr = fullHtml.substring(currentPos, extractEndPos);
392
+ attr = searchStr.match(attribute);
383
393
 
384
394
  // If we limited the input and got a match, check if the value might be truncated
385
395
  if (attr && isLimited) {
@@ -388,32 +398,31 @@ class HTMLParser {
388
398
  // If the match ends near the limit, the value might be truncated
389
399
  if (attrEnd > MAX_ATTR_PARSE_LENGTH - 100) {
390
400
  // Manually extract this attribute to handle potentially huge value
391
- const manualMatch = input.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
401
+ const manualMatch = searchStr.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
392
402
  if (manualMatch) {
393
- const quoteChar = input[manualMatch[0].length];
403
+ const quoteChar = searchStr[manualMatch[0].length];
394
404
  if (quoteChar === '"' || quoteChar === "'") {
395
- const closeQuote = input.indexOf(quoteChar, manualMatch[0].length + 1);
405
+ const closeQuote = searchStr.indexOf(quoteChar, manualMatch[0].length + 1);
396
406
  if (closeQuote !== -1) {
397
- const fullAttr = input.slice(0, closeQuote + 1);
407
+ const fullAttrLen = closeQuote + 1;
398
408
  const numCustomParts = handler.customAttrSurround
399
409
  ? handler.customAttrSurround.length * NCP
400
410
  : 0;
401
411
  const baseIndex = 1 + numCustomParts;
402
412
 
403
413
  attr = [];
404
- attr[0] = fullAttr;
414
+ attr[0] = searchStr.substring(0, fullAttrLen);
405
415
  attr[baseIndex] = manualMatch[1]; // Attribute name
406
- attr[baseIndex + 1] = '='; // `customAssign` (falls back to “=” for huge attributes)
407
- const value = input.slice(manualMatch[0].length + 1, closeQuote);
416
+ attr[baseIndex + 1] = '='; // `customAssign` (falls back to "=" for huge attributes)
417
+ const value = searchStr.substring(manualMatch[0].length + 1, closeQuote);
408
418
  // Place value at correct index based on quote type
409
419
  if (quoteChar === '"') {
410
420
  attr[baseIndex + 2] = value; // Double-quoted value
411
421
  } else {
412
422
  attr[baseIndex + 3] = value; // Single-quoted value
413
423
  }
414
- const attrLen = fullAttr.length;
415
- input = input.slice(attrLen);
416
- consumed += attrLen;
424
+ currentPos += fullAttrLen;
425
+ consumed += fullAttrLen;
417
426
  match.attrs.push(attr);
418
427
  continue;
419
428
  }
@@ -426,18 +435,55 @@ class HTMLParser {
426
435
  }
427
436
  }
428
437
 
438
+ if (!attr && isLimited) {
439
+ // If we limited the input and got no match, try manual extraction
440
+ // This handles cases where quoted attributes exceed `MAX_ATTR_PARSE_LENGTH`
441
+ const manualMatch = searchStr.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
442
+ if (manualMatch) {
443
+ const quoteChar = searchStr[manualMatch[0].length];
444
+ if (quoteChar === '"' || quoteChar === "'") {
445
+ // Search in the full HTML (not limited substring) for closing quote
446
+ const closeQuote = fullHtml.indexOf(quoteChar, currentPos + manualMatch[0].length + 1);
447
+ if (closeQuote !== -1) {
448
+ const fullAttrLen = closeQuote - currentPos + 1;
449
+ const numCustomParts = handler.customAttrSurround
450
+ ? handler.customAttrSurround.length * NCP
451
+ : 0;
452
+ const baseIndex = 1 + numCustomParts;
453
+
454
+ attr = [];
455
+ attr[0] = fullHtml.substring(currentPos, closeQuote + 1);
456
+ attr[baseIndex] = manualMatch[1]; // Attribute name
457
+ attr[baseIndex + 1] = '='; // customAssign
458
+ const value = fullHtml.substring(currentPos + manualMatch[0].length + 1, closeQuote);
459
+ // Place value at correct index based on quote type
460
+ if (quoteChar === '"') {
461
+ attr[baseIndex + 2] = value; // Double-quoted value
462
+ } else {
463
+ attr[baseIndex + 3] = value; // Single-quoted value
464
+ }
465
+ currentPos += fullAttrLen;
466
+ consumed += fullAttrLen;
467
+ match.attrs.push(attr);
468
+ continue;
469
+ }
470
+ }
471
+ }
472
+ }
473
+
429
474
  if (!attr) {
430
475
  break;
431
476
  }
432
477
 
433
478
  const attrLen = attr[0].length;
434
- input = input.slice(attrLen);
479
+ currentPos += attrLen;
435
480
  consumed += attrLen;
436
481
  match.attrs.push(attr);
437
482
  }
438
483
 
439
484
  // Check for closing tag
440
- end = input.match(startTagClose);
485
+ const remainingForClose = sliceFromPos(currentPos);
486
+ end = remainingForClose.match(startTagClose);
441
487
  if (end) {
442
488
  match.unarySlash = end[1];
443
489
  consumed += end[0].length;
@@ -634,11 +680,11 @@ class HTMLParser {
634
680
  if (handler.end) {
635
681
  handler.end(tagName, [], false);
636
682
  }
637
- } else if (tagName.toLowerCase() === 'br') {
683
+ } else if (tagName && tagName.toLowerCase() === 'br') {
638
684
  if (handler.start) {
639
685
  await handler.start(tagName, [], true, '');
640
686
  }
641
- } else if (tagName.toLowerCase() === 'p') {
687
+ } else if (tagName && tagName.toLowerCase() === 'p') {
642
688
  if (handler.start) {
643
689
  await handler.start(tagName, [], false, '', true);
644
690
  }
@@ -1789,10 +1835,9 @@ function shouldMinifyInnerHTML(options) {
1789
1835
  * @param {Function} deps.getSwc - Function to lazily load @swc/core
1790
1836
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
1791
1837
  * @param {LRU} deps.jsMinifyCache - JS minification cache
1792
- * @param {LRU} deps.urlMinifyCache - URL minification cache
1793
1838
  * @returns {MinifierOptions} Normalized options with defaults applied
1794
1839
  */
1795
- const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache, urlMinifyCache } = {}) => {
1840
+ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
1796
1841
  const options = {
1797
1842
  name: function (name) {
1798
1843
  return name.toLowerCase();
@@ -2086,7 +2131,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
2086
2131
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
2087
2132
 
2088
2133
  // Create instance-specific cache (results depend on site configuration)
2089
- const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
2134
+ const instanceCache = new LRU(500);
2090
2135
 
2091
2136
  options.minifyURLs = function (text) {
2092
2137
  // Fast-path: Skip if text doesn’t look like a URL that needs processing
@@ -2095,20 +2140,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
2095
2140
  return text;
2096
2141
  }
2097
2142
 
2098
- // Check instance-specific cache
2099
- if (instanceCache) {
2100
- const cached = instanceCache.get(text);
2101
- if (cached !== undefined) {
2102
- return cached;
2103
- }
2143
+ // Check cache
2144
+ const cached = instanceCache.get(text);
2145
+ if (cached !== undefined) {
2146
+ return cached;
2104
2147
  }
2105
2148
 
2106
2149
  try {
2107
2150
  const result = relateUrlInstance.relate(text);
2108
- // Cache successful results
2109
- if (instanceCache) {
2110
- instanceCache.set(text, result);
2111
- }
2151
+ instanceCache.set(text, result);
2112
2152
  return result;
2113
2153
  } catch (err) {
2114
2154
  // Don’t cache errors
@@ -2947,10 +2987,9 @@ async function getSwc() {
2947
2987
  return swcPromise;
2948
2988
  }
2949
2989
 
2950
- // Minification caches
2951
- const cssMinifyCache = new LRU(500);
2952
- const jsMinifyCache = new LRU(500);
2953
- const urlMinifyCache = new LRU(500);
2990
+ // Minification caches (initialized on first use with configurable sizes)
2991
+ let cssMinifyCache = null;
2992
+ let jsMinifyCache = null;
2954
2993
 
2955
2994
  // Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
2956
2995
  const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
@@ -3099,6 +3138,24 @@ function mergeConsecutiveScripts(html) {
3099
3138
  *
3100
3139
  * Default: Built-in `canTrimWhitespace` function
3101
3140
  *
3141
+ * @prop {number} [cacheCSS]
3142
+ * The maximum number of entries for the CSS minification cache. Higher values
3143
+ * improve performance for inputs with repeated CSS (e.g., batch processing).
3144
+ * - Cache is created on first `minify()` call and persists for the process lifetime
3145
+ * - Cache size is locked after first call—subsequent calls reuse the same cache
3146
+ * - Explicit `0` values are coerced to `1` (minimum functional cache size)
3147
+ *
3148
+ * Default: `500` (or `1000` when `CI=true` environment variable is set)
3149
+ *
3150
+ * @prop {number} [cacheJS]
3151
+ * The maximum number of entries for the JavaScript minification cache. Higher
3152
+ * values improve performance for inputs with repeated JavaScript.
3153
+ * - Cache is created on first `minify()` call and persists for the process lifetime
3154
+ * - Cache size is locked after first call—subsequent calls reuse the same cache
3155
+ * - Explicit `0` values are coerced to `1` (minimum functional cache size)
3156
+ *
3157
+ * Default: `500` (or `1000` when `CI=true` environment variable is set)
3158
+ *
3102
3159
  * @prop {boolean} [caseSensitive]
3103
3160
  * When true, tag and attribute names are treated as case-sensitive.
3104
3161
  * Useful for custom HTML tags.
@@ -4357,6 +4414,49 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
4357
4414
  return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
4358
4415
  }
4359
4416
 
4417
+ /**
4418
+ * Initialize minification caches with configurable sizes.
4419
+ *
4420
+ * Important behavior notes:
4421
+ * - Caches are created on the first `minify()` call and persist for the lifetime of the process
4422
+ * - Cache sizes are locked after first initialization—subsequent calls use the same caches
4423
+ * even if different `cacheCSS`/`cacheJS` options are provided
4424
+ * - The first call’s options determine the cache sizes for subsequent calls
4425
+ * - Explicit `0` values are coerced to `1` (minimum functional cache size)
4426
+ */
4427
+ function initCaches(options) {
4428
+ // Only create caches once (on first call)—sizes are locked after this
4429
+ if (!cssMinifyCache) {
4430
+ // Determine default size based on environment
4431
+ const defaultSize = process.env.CI === 'true' ? 1000 : 500;
4432
+
4433
+ // Helper to parse env var—returns parsed number (including 0) or undefined if absent, invalid, or negative
4434
+ const parseEnvCacheSize = (envVar) => {
4435
+ if (envVar === undefined) return undefined;
4436
+ const parsed = Number(envVar);
4437
+ if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) {
4438
+ return undefined;
4439
+ }
4440
+ return parsed;
4441
+ };
4442
+
4443
+ // Get cache sizes with precedence: Options > env > default
4444
+ const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
4445
+ : (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
4446
+ const jsSize = options.cacheJS !== undefined ? options.cacheJS
4447
+ : (parseEnvCacheSize(process.env.HMN_CACHE_JS) ?? defaultSize);
4448
+
4449
+ // Coerce `0` to `1` (minimum functional cache size) to avoid immediate eviction
4450
+ const cssFinalSize = cssSize === 0 ? 1 : cssSize;
4451
+ const jsFinalSize = jsSize === 0 ? 1 : jsSize;
4452
+
4453
+ cssMinifyCache = new LRU(cssFinalSize);
4454
+ jsMinifyCache = new LRU(jsFinalSize);
4455
+ }
4456
+
4457
+ return { cssMinifyCache, jsMinifyCache };
4458
+ }
4459
+
4360
4460
  /**
4361
4461
  * @param {string} value
4362
4462
  * @param {MinifierOptions} [options]
@@ -4364,13 +4464,15 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
4364
4464
  */
4365
4465
  const minify = async function (value, options) {
4366
4466
  const start = Date.now();
4467
+
4468
+ // Initialize caches on first use with configurable sizes
4469
+ const caches = initCaches(options || {});
4470
+
4367
4471
  options = processOptions(options || {}, {
4368
4472
  getLightningCSS,
4369
4473
  getTerser,
4370
4474
  getSwc,
4371
- cssMinifyCache,
4372
- jsMinifyCache,
4373
- urlMinifyCache
4475
+ ...caches
4374
4476
  });
4375
4477
  let result = await minifyHTML(value, options);
4376
4478