html-minifier-next 4.18.0 → 4.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -2
- package/README.md +81 -22
- package/cli.js +2 -2
- package/dist/htmlminifier.cjs +186 -85
- package/dist/htmlminifier.esm.bundle.js +400 -120
- package/dist/types/htmlminifier.d.ts +20 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.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/package.json +2 -2
- package/src/htmlminifier.js +68 -7
- package/src/htmlparser.js +111 -63
- package/src/lib/options.js +8 -14
package/dist/htmlminifier.cjs
CHANGED
|
@@ -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 = '
|
|
47
|
-
const digit = '0-9
|
|
48
|
-
const extender = '
|
|
49
|
-
const letter = 'A-Za-z
|
|
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('
|
|
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
|
-
|
|
180
|
+
|
|
178
181
|
// Make sure we’re not in a `script` or `style` element
|
|
179
182
|
if (!lastTag || !special.has(lastTag)) {
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
184
|
-
const commentEnd =
|
|
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(
|
|
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(
|
|
199
|
-
const conditionalEnd =
|
|
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(
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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(
|
|
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 =
|
|
252
|
-
advance(textEnd);
|
|
256
|
+
text = fullHtml.substring(pos, textEnd);
|
|
257
|
+
advance(textEnd - pos);
|
|
253
258
|
} else {
|
|
254
|
-
text =
|
|
255
|
-
advance(
|
|
259
|
+
text = fullHtml.substring(pos);
|
|
260
|
+
advance(fullLength - pos);
|
|
256
261
|
}
|
|
257
262
|
|
|
258
|
-
// Next tag
|
|
259
|
-
const
|
|
260
|
-
let nextTagMatch = parseStartTag(
|
|
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
|
-
|
|
267
|
-
if (
|
|
268
|
-
nextTag = '/' +
|
|
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]*?)
|
|
289
|
+
const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
|
|
285
290
|
|
|
286
|
-
const
|
|
287
|
-
|
|
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
|
|
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 &&
|
|
303
|
-
await handler.chars(
|
|
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.,
|
|
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(
|
|
356
|
-
const start =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
380
|
-
const
|
|
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
|
-
|
|
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 =
|
|
401
|
+
const manualMatch = searchStr.match(/^\s*([^\s"'<>/=]+)\s*=\s*/);
|
|
392
402
|
if (manualMatch) {
|
|
393
|
-
const quoteChar =
|
|
403
|
+
const quoteChar = searchStr[manualMatch[0].length];
|
|
394
404
|
if (quoteChar === '"' || quoteChar === "'") {
|
|
395
|
-
const closeQuote =
|
|
405
|
+
const closeQuote = searchStr.indexOf(quoteChar, manualMatch[0].length + 1);
|
|
396
406
|
if (closeQuote !== -1) {
|
|
397
|
-
const
|
|
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] =
|
|
414
|
+
attr[0] = searchStr.substring(0, fullAttrLen);
|
|
405
415
|
attr[baseIndex] = manualMatch[1]; // Attribute name
|
|
406
|
-
attr[baseIndex + 1] = '='; // `customAssign` (falls back to
|
|
407
|
-
const value =
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
479
|
+
currentPos += attrLen;
|
|
435
480
|
consumed += attrLen;
|
|
436
481
|
match.attrs.push(attr);
|
|
437
482
|
}
|
|
438
483
|
|
|
439
484
|
// Check for closing tag
|
|
440
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2952
|
-
|
|
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`
|
|
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`
|
|
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,48 @@ 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
|
+
const defaultSize = 500;
|
|
4431
|
+
|
|
4432
|
+
// Helper to parse env var—returns parsed number (including 0) or undefined if absent, invalid, or negative
|
|
4433
|
+
const parseEnvCacheSize = (envVar) => {
|
|
4434
|
+
if (envVar === undefined) return undefined;
|
|
4435
|
+
const parsed = Number(envVar);
|
|
4436
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) {
|
|
4437
|
+
return undefined;
|
|
4438
|
+
}
|
|
4439
|
+
return parsed;
|
|
4440
|
+
};
|
|
4441
|
+
|
|
4442
|
+
// Get cache sizes with precedence: Options > env > default
|
|
4443
|
+
const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
|
|
4444
|
+
: (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
|
|
4445
|
+
const jsSize = options.cacheJS !== undefined ? options.cacheJS
|
|
4446
|
+
: (parseEnvCacheSize(process.env.HMN_CACHE_JS) ?? defaultSize);
|
|
4447
|
+
|
|
4448
|
+
// Coerce `0` to `1` (minimum functional cache size) to avoid immediate eviction
|
|
4449
|
+
const cssFinalSize = cssSize === 0 ? 1 : cssSize;
|
|
4450
|
+
const jsFinalSize = jsSize === 0 ? 1 : jsSize;
|
|
4451
|
+
|
|
4452
|
+
cssMinifyCache = new LRU(cssFinalSize);
|
|
4453
|
+
jsMinifyCache = new LRU(jsFinalSize);
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
return { cssMinifyCache, jsMinifyCache };
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4360
4459
|
/**
|
|
4361
4460
|
* @param {string} value
|
|
4362
4461
|
* @param {MinifierOptions} [options]
|
|
@@ -4364,13 +4463,15 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4364
4463
|
*/
|
|
4365
4464
|
const minify = async function (value, options) {
|
|
4366
4465
|
const start = Date.now();
|
|
4466
|
+
|
|
4467
|
+
// Initialize caches on first use with configurable sizes
|
|
4468
|
+
const caches = initCaches(options || {});
|
|
4469
|
+
|
|
4367
4470
|
options = processOptions(options || {}, {
|
|
4368
4471
|
getLightningCSS,
|
|
4369
4472
|
getTerser,
|
|
4370
4473
|
getSwc,
|
|
4371
|
-
|
|
4372
|
-
jsMinifyCache,
|
|
4373
|
-
urlMinifyCache
|
|
4474
|
+
...caches
|
|
4374
4475
|
});
|
|
4375
4476
|
let result = await minifyHTML(value, options);
|
|
4376
4477
|
|