html-minifier-next 4.17.2 → 4.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -21
- package/cli.js +3 -0
- package/dist/htmlminifier.cjs +344 -99
- package/dist/htmlminifier.esm.bundle.js +344 -99
- package/dist/types/htmlminifier.d.ts +28 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +1 -0
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +1 -2
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +206 -12
- package/src/htmlparser.js +111 -63
- package/src/lib/attributes.js +4 -1
- package/src/lib/constants.js +6 -0
- package/src/lib/options.js +8 -14
- package/src/lib/svg.js +15 -8
- package/src/presets.js +1 -0
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
|
}
|
|
@@ -781,6 +827,7 @@ const presets = {
|
|
|
781
827
|
collapseWhitespace: true,
|
|
782
828
|
continueOnParseError: true,
|
|
783
829
|
decodeEntities: true,
|
|
830
|
+
mergeScripts: true,
|
|
784
831
|
minifyCSS: true,
|
|
785
832
|
minifyJS: true,
|
|
786
833
|
minifySVG: true,
|
|
@@ -999,6 +1046,11 @@ const isSimpleBoolean = new Set(['allowfullscreen', 'async', 'autofocus', 'autop
|
|
|
999
1046
|
|
|
1000
1047
|
const isBooleanValue = new Set(['true', 'false']);
|
|
1001
1048
|
|
|
1049
|
+
// Attributes where empty value can be collapsed to just the attribute name
|
|
1050
|
+
// `crossorigin=""` → `crossorigin` (empty string equals anonymous mode)
|
|
1051
|
+
// `contenteditable=""` → `contenteditable` (empty string equals `true`)
|
|
1052
|
+
const emptyCollapsible = new Set(['crossorigin', 'contenteditable']);
|
|
1053
|
+
|
|
1002
1054
|
// `srcset` elements
|
|
1003
1055
|
|
|
1004
1056
|
const srcsetElements = new Set(['img', 'source']);
|
|
@@ -1448,7 +1500,8 @@ function minifyNumber(num, precision = 3) {
|
|
|
1448
1500
|
const fixed = parsed.toFixed(precision);
|
|
1449
1501
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
1450
1502
|
|
|
1451
|
-
|
|
1503
|
+
// Remove leading zero before decimal point (e.g., `0.5` → `.5`, `-0.3` → `-.3`)
|
|
1504
|
+
const result = (trimmed || '0').replace(/^(-?)0\./, '$1.');
|
|
1452
1505
|
numberCache.set(cacheKey, result);
|
|
1453
1506
|
return result;
|
|
1454
1507
|
}
|
|
@@ -1468,17 +1521,23 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
1468
1521
|
});
|
|
1469
1522
|
|
|
1470
1523
|
// Remove unnecessary spaces around path commands
|
|
1471
|
-
// Safe to remove space after a command letter when it’s followed by a number
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1524
|
+
// Safe to remove space after a command letter when it’s followed by a number
|
|
1525
|
+
// (which may be negative or start with a decimal point)
|
|
1526
|
+
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`, `M .5 .3` → `M.5.3`
|
|
1527
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\.?\d)/g, '$1');
|
|
1474
1528
|
|
|
1475
1529
|
// Safe to remove space before command letter when preceded by a number
|
|
1476
|
-
// `0 L` → `0L`, `20 M` → `20M`
|
|
1477
|
-
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
1530
|
+
// `0 L` → `0L`, `20 M` → `20M`, `.5 L` → `.5L`
|
|
1531
|
+
result = result.replace(/([\d.])\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
1478
1532
|
|
|
1479
1533
|
// Safe to remove space before negative number when preceded by a number
|
|
1480
|
-
// `10 -20` → `10-20` (
|
|
1481
|
-
result = result.replace(/(\d)\s+(
|
|
1534
|
+
// `10 -20` → `10-20`, `.5 -.3` → `.5-.3` (minus sign is always a separator)
|
|
1535
|
+
result = result.replace(/([\d.])\s+(-)/g, '$1$2');
|
|
1536
|
+
|
|
1537
|
+
// Safe to remove space between two decimal numbers (decimal point acts as separator)
|
|
1538
|
+
// `.5 .3` → `.5.3` (only when previous char is `.`, indicating a complete decimal)
|
|
1539
|
+
// Note: `0 .3` must not become `0.3` (that would change two numbers into one)
|
|
1540
|
+
result = result.replace(/(\.\d*)\s+(\.)/g, '$1$2');
|
|
1482
1541
|
|
|
1483
1542
|
return result;
|
|
1484
1543
|
}
|
|
@@ -1776,10 +1835,9 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1776
1835
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
1777
1836
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
1778
1837
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
1779
|
-
* @param {LRU} deps.urlMinifyCache - URL minification cache
|
|
1780
1838
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
1781
1839
|
*/
|
|
1782
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache
|
|
1840
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
1783
1841
|
const options = {
|
|
1784
1842
|
name: function (name) {
|
|
1785
1843
|
return name.toLowerCase();
|
|
@@ -2073,7 +2131,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
2073
2131
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
2074
2132
|
|
|
2075
2133
|
// Create instance-specific cache (results depend on site configuration)
|
|
2076
|
-
const instanceCache =
|
|
2134
|
+
const instanceCache = new LRU(500);
|
|
2077
2135
|
|
|
2078
2136
|
options.minifyURLs = function (text) {
|
|
2079
2137
|
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
@@ -2082,20 +2140,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
2082
2140
|
return text;
|
|
2083
2141
|
}
|
|
2084
2142
|
|
|
2085
|
-
// Check
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
return cached;
|
|
2090
|
-
}
|
|
2143
|
+
// Check cache
|
|
2144
|
+
const cached = instanceCache.get(text);
|
|
2145
|
+
if (cached !== undefined) {
|
|
2146
|
+
return cached;
|
|
2091
2147
|
}
|
|
2092
2148
|
|
|
2093
2149
|
try {
|
|
2094
2150
|
const result = relateUrlInstance.relate(text);
|
|
2095
|
-
|
|
2096
|
-
if (instanceCache) {
|
|
2097
|
-
instanceCache.set(text, result);
|
|
2098
|
-
}
|
|
2151
|
+
instanceCache.set(text, result);
|
|
2099
2152
|
return result;
|
|
2100
2153
|
} catch (err) {
|
|
2101
2154
|
// Don’t cache errors
|
|
@@ -2255,7 +2308,9 @@ function isStyleElement(tag, attrs) {
|
|
|
2255
2308
|
}
|
|
2256
2309
|
|
|
2257
2310
|
function isBooleanAttribute(attrName, attrValue) {
|
|
2258
|
-
return isSimpleBoolean.has(attrName) ||
|
|
2311
|
+
return isSimpleBoolean.has(attrName) ||
|
|
2312
|
+
(attrName === 'draggable' && !isBooleanValue.has(attrValue)) ||
|
|
2313
|
+
(attrValue === '' && emptyCollapsible.has(attrName));
|
|
2259
2314
|
}
|
|
2260
2315
|
|
|
2261
2316
|
function isUriTypeAttribute(attrName, tag) {
|
|
@@ -2932,11 +2987,125 @@ async function getSwc() {
|
|
|
2932
2987
|
return swcPromise;
|
|
2933
2988
|
}
|
|
2934
2989
|
|
|
2935
|
-
// Minification caches
|
|
2990
|
+
// Minification caches (initialized on first use with configurable sizes)
|
|
2991
|
+
let cssMinifyCache = null;
|
|
2992
|
+
let jsMinifyCache = null;
|
|
2993
|
+
|
|
2994
|
+
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
2995
|
+
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
2996
|
+
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
2997
|
+
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
2998
|
+
|
|
2999
|
+
// Pre-compiled patterns for buffer scanning
|
|
3000
|
+
const RE_START_TAG = /^<[^/!]/;
|
|
3001
|
+
const RE_END_TAG = /^<\//;
|
|
3002
|
+
|
|
3003
|
+
// Script merging
|
|
3004
|
+
|
|
3005
|
+
/**
|
|
3006
|
+
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
3007
|
+
* Only merges scripts that are compatible:
|
|
3008
|
+
* - Both inline (no `src` attribute)
|
|
3009
|
+
* - Same `type` (or both default JavaScript)
|
|
3010
|
+
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
3011
|
+
*
|
|
3012
|
+
* Limitation: This function uses regex-based matching (`pattern` variable below),
|
|
3013
|
+
* which can produce incorrect results if a script’s content contains a literal
|
|
3014
|
+
* `</script>` string (e.g., `document.write('<script>…</script>')`). In valid
|
|
3015
|
+
* HTML, such strings should be escaped as `<\/script>` or split like
|
|
3016
|
+
* `'</scr' + 'ipt>'`, so this limitation rarely affects real-world code. The
|
|
3017
|
+
* earlier `minifyJS` step (if enabled) typically handles this escaping already.
|
|
3018
|
+
*
|
|
3019
|
+
* @param {string} html - The HTML string to process
|
|
3020
|
+
* @returns {string} HTML with consecutive scripts merged
|
|
3021
|
+
*/
|
|
3022
|
+
function mergeConsecutiveScripts(html) {
|
|
3023
|
+
// `pattern`: Regex to match consecutive `</script>` followed by `<script…>`.
|
|
3024
|
+
// See function JSDoc above for known limitations with literal `</script>` in content.
|
|
3025
|
+
// Captures:
|
|
3026
|
+
// 1. first script attrs
|
|
3027
|
+
// 2. first script content
|
|
3028
|
+
// 3. whitespace between
|
|
3029
|
+
// 4. second script attrs
|
|
3030
|
+
// 5. second script content
|
|
3031
|
+
const pattern = /<script([^>]*)>([\s\S]*?)<\/script>([\s]*)<script([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
3032
|
+
|
|
3033
|
+
let result = html;
|
|
3034
|
+
let changed = true;
|
|
3035
|
+
|
|
3036
|
+
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
3037
|
+
while (changed) {
|
|
3038
|
+
changed = false;
|
|
3039
|
+
result = result.replace(pattern, (match, attrs1, content1, whitespace, attrs2, content2) => {
|
|
3040
|
+
// Parse attributes from both script tags (uses pre-compiled RE_SCRIPT_ATTRS)
|
|
3041
|
+
const parseAttrs = (attrStr) => {
|
|
3042
|
+
const attrs = {};
|
|
3043
|
+
RE_SCRIPT_ATTRS.lastIndex = 0; // Reset for reuse
|
|
3044
|
+
let m;
|
|
3045
|
+
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
3046
|
+
const name = m[1].toLowerCase();
|
|
3047
|
+
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
3048
|
+
attrs[name] = value;
|
|
3049
|
+
}
|
|
3050
|
+
return attrs;
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
const a1 = parseAttrs(attrs1);
|
|
3054
|
+
const a2 = parseAttrs(attrs2);
|
|
3055
|
+
|
|
3056
|
+
// Check for `src`—cannot merge external scripts
|
|
3057
|
+
if ('src' in a1 || 'src' in a2) {
|
|
3058
|
+
return match;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// Check `type` compatibility (both must be same, or both default JS)
|
|
3062
|
+
const type1 = a1.type || '';
|
|
3063
|
+
const type2 = a2.type || '';
|
|
3064
|
+
|
|
3065
|
+
if (DEFAULT_JS_TYPES.has(type1) && DEFAULT_JS_TYPES.has(type2)) ; else if (type1 === type2) ; else {
|
|
3066
|
+
// Incompatible types
|
|
3067
|
+
return match;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// Check for conflicting boolean attributes (uses pre-compiled SCRIPT_BOOL_ATTRS)
|
|
3071
|
+
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
3072
|
+
const has1 = attr in a1;
|
|
3073
|
+
const has2 = attr in a2;
|
|
3074
|
+
if (has1 !== has2) {
|
|
3075
|
+
// One has it, one doesn't - incompatible
|
|
3076
|
+
return match;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// Check `nonce`—must be same or both absent
|
|
3081
|
+
if (a1.nonce !== a2.nonce) {
|
|
3082
|
+
return match;
|
|
3083
|
+
}
|
|
2936
3084
|
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
3085
|
+
// Scripts are compatible—merge them
|
|
3086
|
+
changed = true;
|
|
3087
|
+
|
|
3088
|
+
// Combine content—use semicolon normally, newline only for trailing `//` comments
|
|
3089
|
+
const c1 = content1.trim();
|
|
3090
|
+
const c2 = content2.trim();
|
|
3091
|
+
let mergedContent;
|
|
3092
|
+
if (c1 && c2) {
|
|
3093
|
+
// Check if last line of c1 contains `//` (single-line comment)
|
|
3094
|
+
// If so, use newline to terminate it; otherwise use semicolon (if not already present)
|
|
3095
|
+
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
3096
|
+
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
3097
|
+
mergedContent = c1 + separator + c2;
|
|
3098
|
+
} else {
|
|
3099
|
+
mergedContent = c1 || c2;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// Use first script’s attributes (they should be compatible)
|
|
3103
|
+
return `<script${attrs1}>${mergedContent}</script>`;
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
return result;
|
|
3108
|
+
}
|
|
2940
3109
|
|
|
2941
3110
|
// Type definitions
|
|
2942
3111
|
|
|
@@ -2969,6 +3138,24 @@ const urlMinifyCache = new LRU(500);
|
|
|
2969
3138
|
*
|
|
2970
3139
|
* Default: Built-in `canTrimWhitespace` function
|
|
2971
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
|
+
*
|
|
2972
3159
|
* @prop {boolean} [caseSensitive]
|
|
2973
3160
|
* When true, tag and attribute names are treated as case-sensitive.
|
|
2974
3161
|
* Useful for custom HTML tags.
|
|
@@ -3118,6 +3305,13 @@ const urlMinifyCache = new LRU(500);
|
|
|
3118
3305
|
*
|
|
3119
3306
|
* Default: No limit
|
|
3120
3307
|
*
|
|
3308
|
+
* @prop {boolean} [mergeScripts]
|
|
3309
|
+
* When true, consecutive inline `<script>` elements are merged into one.
|
|
3310
|
+
* Only merges compatible scripts (same `type`, matching `async`/`defer`/
|
|
3311
|
+
* `nomodule`/`nonce` attributes). Does not merge external scripts (with `src`).
|
|
3312
|
+
*
|
|
3313
|
+
* Default: `false`
|
|
3314
|
+
*
|
|
3121
3315
|
* @prop {boolean | Partial<import("lightningcss").TransformOptions<import("lightningcss").CustomAtRules>> | ((text: string, type?: string) => Promise<string> | string)} [minifyCSS]
|
|
3122
3316
|
* When true, enables CSS minification for inline `<style>` tags or
|
|
3123
3317
|
* `style` attributes. If an object is provided, it is passed to
|
|
@@ -3696,7 +3890,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3696
3890
|
|
|
3697
3891
|
function removeStartTag() {
|
|
3698
3892
|
let index = buffer.length - 1;
|
|
3699
|
-
while (index > 0 &&
|
|
3893
|
+
while (index > 0 && !RE_START_TAG.test(buffer[index])) {
|
|
3700
3894
|
index--;
|
|
3701
3895
|
}
|
|
3702
3896
|
buffer.length = Math.max(0, index);
|
|
@@ -3704,7 +3898,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3704
3898
|
|
|
3705
3899
|
function removeEndTag() {
|
|
3706
3900
|
let index = buffer.length - 1;
|
|
3707
|
-
while (index > 0 &&
|
|
3901
|
+
while (index > 0 && !RE_END_TAG.test(buffer[index])) {
|
|
3708
3902
|
index--;
|
|
3709
3903
|
}
|
|
3710
3904
|
buffer.length = Math.max(0, index);
|
|
@@ -4015,8 +4209,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4015
4209
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
4016
4210
|
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
4017
4211
|
// Escape any `&` symbols that start either:
|
|
4018
|
-
// 1
|
|
4019
|
-
// 2
|
|
4212
|
+
// 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
4213
|
+
// 2. or any other character reference (i.e., one that does end with `;`)
|
|
4020
4214
|
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
4021
4215
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
4022
4216
|
if (text.indexOf('&') !== -1) {
|
|
@@ -4220,6 +4414,49 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4220
4414
|
return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
|
|
4221
4415
|
}
|
|
4222
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
|
+
|
|
4223
4460
|
/**
|
|
4224
4461
|
* @param {string} value
|
|
4225
4462
|
* @param {MinifierOptions} [options]
|
|
@@ -4227,15 +4464,23 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4227
4464
|
*/
|
|
4228
4465
|
const minify = async function (value, options) {
|
|
4229
4466
|
const start = Date.now();
|
|
4467
|
+
|
|
4468
|
+
// Initialize caches on first use with configurable sizes
|
|
4469
|
+
const caches = initCaches(options || {});
|
|
4470
|
+
|
|
4230
4471
|
options = processOptions(options || {}, {
|
|
4231
4472
|
getLightningCSS,
|
|
4232
4473
|
getTerser,
|
|
4233
4474
|
getSwc,
|
|
4234
|
-
|
|
4235
|
-
jsMinifyCache,
|
|
4236
|
-
urlMinifyCache
|
|
4475
|
+
...caches
|
|
4237
4476
|
});
|
|
4238
|
-
|
|
4477
|
+
let result = await minifyHTML(value, options);
|
|
4478
|
+
|
|
4479
|
+
// Post-processing: Merge consecutive inline scripts if enabled
|
|
4480
|
+
if (options.mergeScripts) {
|
|
4481
|
+
result = mergeConsecutiveScripts(result);
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4239
4484
|
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
4240
4485
|
return result;
|
|
4241
4486
|
};
|