html-minifier-next 5.1.2 → 5.1.3
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/dist/htmlminifier.cjs +471 -372
- 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 +2 -6
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +1 -1
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +232 -195
- package/src/htmlparser.js +14 -6
- package/src/lib/attributes.js +126 -70
- package/src/lib/options.js +3 -3
- package/src/lib/utils.js +4 -4
package/src/htmlparser.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isThenable } from './lib/utils.js';
|
|
9
|
+
|
|
8
10
|
/*
|
|
9
11
|
* Use like so:
|
|
10
12
|
*
|
|
@@ -228,7 +230,8 @@ export class HTMLParser {
|
|
|
228
230
|
|
|
229
231
|
if (commentEnd >= 0) {
|
|
230
232
|
if (handler.comment) {
|
|
231
|
-
|
|
233
|
+
const result = handler.comment(fullHtml.substring(pos + 4, commentEnd));
|
|
234
|
+
if (isThenable(result)) await result;
|
|
232
235
|
}
|
|
233
236
|
advance(commentEnd + 3 - pos);
|
|
234
237
|
prevTag = '';
|
|
@@ -244,7 +247,8 @@ export class HTMLParser {
|
|
|
244
247
|
|
|
245
248
|
if (conditionalEnd >= 0) {
|
|
246
249
|
if (handler.comment) {
|
|
247
|
-
|
|
250
|
+
const result = handler.comment(fullHtml.substring(pos + 2, conditionalEnd + 1), true /* Non-standard */);
|
|
251
|
+
if (isThenable(result)) await result;
|
|
248
252
|
}
|
|
249
253
|
advance(conditionalEnd + 2 - pos);
|
|
250
254
|
prevTag = '';
|
|
@@ -324,7 +328,8 @@ export class HTMLParser {
|
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
if (handler.chars) {
|
|
327
|
-
|
|
331
|
+
const result = handler.chars(text, prevTag, nextTag, prevAttrs, nextAttrs);
|
|
332
|
+
if (isThenable(result)) await result;
|
|
328
333
|
}
|
|
329
334
|
prevTag = '';
|
|
330
335
|
prevAttrs = [];
|
|
@@ -343,7 +348,8 @@ export class HTMLParser {
|
|
|
343
348
|
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
|
|
344
349
|
}
|
|
345
350
|
if (handler.chars) {
|
|
346
|
-
|
|
351
|
+
const result = handler.chars(text);
|
|
352
|
+
if (isThenable(result)) await result;
|
|
347
353
|
}
|
|
348
354
|
// Advance HTML past the matched special tag content and its closing tag
|
|
349
355
|
advance(m[0].length);
|
|
@@ -351,7 +357,8 @@ export class HTMLParser {
|
|
|
351
357
|
} else {
|
|
352
358
|
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
353
359
|
if (handler.continueOnParseError && handler.chars && pos < fullLength) {
|
|
354
|
-
|
|
360
|
+
const result = handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
361
|
+
if (isThenable(result)) await result;
|
|
355
362
|
advance(1);
|
|
356
363
|
} else {
|
|
357
364
|
break;
|
|
@@ -363,7 +370,8 @@ export class HTMLParser {
|
|
|
363
370
|
if (handler.continueOnParseError) {
|
|
364
371
|
// Skip the problematic character and continue
|
|
365
372
|
if (handler.chars) {
|
|
366
|
-
|
|
373
|
+
const result = handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
374
|
+
if (isThenable(result)) await result;
|
|
367
375
|
}
|
|
368
376
|
advance(1);
|
|
369
377
|
prevTag = '';
|
package/src/lib/attributes.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './constants.js';
|
|
21
21
|
import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
|
|
22
22
|
import { shouldMinifyInnerHTML } from './options.js';
|
|
23
|
+
import { isThenable } from './utils.js';
|
|
23
24
|
|
|
24
25
|
// Lazy-load entities only when `decodeEntities` is enabled
|
|
25
26
|
|
|
@@ -298,7 +299,9 @@ function hasAttrName(name, attrs) {
|
|
|
298
299
|
|
|
299
300
|
// Cleaners
|
|
300
301
|
|
|
301
|
-
|
|
302
|
+
// Returns the cleaned attribute value directly (sync) or as a Promise (async);
|
|
303
|
+
// callers must handle both cases—use `isThenable()` to distinguish
|
|
304
|
+
function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
302
305
|
// Apply early whitespace normalization if enabled
|
|
303
306
|
// Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
304
307
|
if (options.collapseAttributeWhitespace) {
|
|
@@ -313,16 +316,18 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
313
316
|
|
|
314
317
|
if (isEventAttribute(attrName, options)) {
|
|
315
318
|
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return attrValue;
|
|
319
|
+
const result = options.minifyJS(attrValue, true);
|
|
320
|
+
if (isThenable(result)) {
|
|
321
|
+
return result.catch(err => {
|
|
322
|
+
if (!options.continueOnMinifyError) throw err;
|
|
323
|
+
options.log && options.log(err);
|
|
324
|
+
return attrValue;
|
|
325
|
+
});
|
|
324
326
|
}
|
|
325
|
-
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (attrName === 'class') {
|
|
326
331
|
attrValue = trimWhitespace(attrValue);
|
|
327
332
|
if (options.sortClassNames) {
|
|
328
333
|
attrValue = options.sortClassNames(attrValue);
|
|
@@ -330,47 +335,63 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
330
335
|
attrValue = collapseWhitespaceAll(attrValue);
|
|
331
336
|
}
|
|
332
337
|
return attrValue;
|
|
333
|
-
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (isUriTypeAttribute(attrName, tag)) {
|
|
334
341
|
attrValue = trimWhitespace(attrValue);
|
|
335
342
|
if (isLinkType(tag, attrs, 'canonical')) {
|
|
336
343
|
return attrValue;
|
|
337
344
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
const result = options.minifyURLs(attrValue);
|
|
346
|
+
if (isThenable(result)) {
|
|
347
|
+
return result
|
|
348
|
+
.then(out => typeof out === 'string' ? out : attrValue)
|
|
349
|
+
.catch(err => {
|
|
350
|
+
if (!options.continueOnMinifyError) throw err;
|
|
351
|
+
options.log && options.log(err);
|
|
352
|
+
return attrValue;
|
|
353
|
+
});
|
|
347
354
|
}
|
|
348
|
-
|
|
355
|
+
return typeof result === 'string' ? result : attrValue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (isNumberTypeAttribute(attrName, tag)) {
|
|
349
359
|
return trimWhitespace(attrValue);
|
|
350
|
-
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (attrName === 'style') {
|
|
351
363
|
attrValue = trimWhitespace(attrValue);
|
|
352
364
|
if (attrValue) {
|
|
353
365
|
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
354
366
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
355
367
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
const originalAttrValue = attrValue;
|
|
369
|
+
const cssResult = options.minifyCSS(attrValue, 'inline');
|
|
370
|
+
if (isThenable(cssResult)) {
|
|
371
|
+
return cssResult
|
|
372
|
+
.then(minified => {
|
|
373
|
+
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
374
|
+
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
375
|
+
if (minified && /^(?:[a-z-]+:[;\s]*)+$/i.test(minified)) return '';
|
|
376
|
+
return minified;
|
|
377
|
+
})
|
|
378
|
+
.catch(err => {
|
|
379
|
+
if (!options.continueOnMinifyError) throw err;
|
|
380
|
+
options.log && options.log(err);
|
|
381
|
+
return originalAttrValue;
|
|
382
|
+
});
|
|
368
383
|
}
|
|
384
|
+
// Sync path (`minifyCSS` disabled—identity function)
|
|
385
|
+
if (cssResult && /^(?:[a-z-]+:[;\s]*)+$/i.test(cssResult)) return '';
|
|
386
|
+
return cssResult != null ? cssResult : attrValue;
|
|
369
387
|
}
|
|
370
388
|
return attrValue;
|
|
371
|
-
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isSrcset(attrName, tag)) {
|
|
372
392
|
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
373
|
-
|
|
393
|
+
const candidates = trimWhitespace(attrValue).split(/\s*,\s*/);
|
|
394
|
+
const processed = candidates.map(candidate => {
|
|
374
395
|
let url = candidate;
|
|
375
396
|
let descriptor = '';
|
|
376
397
|
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
@@ -382,47 +403,65 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
382
403
|
descriptor = ' ' + num + suffix;
|
|
383
404
|
}
|
|
384
405
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
406
|
+
const out = options.minifyURLs(url);
|
|
407
|
+
if (isThenable(out)) {
|
|
408
|
+
return out
|
|
409
|
+
.then(result => (typeof result === 'string' ? result : url) + descriptor)
|
|
410
|
+
.catch(err => {
|
|
411
|
+
if (!options.continueOnMinifyError) throw err;
|
|
412
|
+
options.log && options.log(err);
|
|
413
|
+
return url + descriptor;
|
|
414
|
+
});
|
|
394
415
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
416
|
+
return (typeof out === 'string' ? out : url) + descriptor;
|
|
417
|
+
});
|
|
418
|
+
if (processed.some(isThenable)) {
|
|
419
|
+
return Promise.all(processed).then(results => results.join(', '));
|
|
420
|
+
}
|
|
421
|
+
return processed.join(', ');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
|
425
|
+
return attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
|
|
398
426
|
// 0.90000 → 0.9
|
|
399
427
|
// 1.0 → 1
|
|
400
428
|
// 1.0001 → 1.0001 (unchanged)
|
|
401
429
|
return (+numString).toString();
|
|
402
430
|
});
|
|
403
|
-
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
404
434
|
return collapseWhitespaceAll(attrValue);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
438
|
+
return trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (tag === 'script' && attrName === 'type') {
|
|
442
|
+
return trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (isMediaQuery(tag, attrs, attrName)) {
|
|
410
446
|
attrValue = trimWhitespace(attrValue);
|
|
411
447
|
// Only minify actual media queries (those with features in parentheses)
|
|
412
448
|
// Skip simple media types like `all`, `screen`, `print` which are already minimal
|
|
413
449
|
if (!/[()]/.test(attrValue)) {
|
|
414
450
|
return attrValue;
|
|
415
451
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
throw err;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
452
|
+
const originalAttrValue = attrValue;
|
|
453
|
+
const cssResult = options.minifyCSS(attrValue, 'media');
|
|
454
|
+
if (isThenable(cssResult)) {
|
|
455
|
+
return cssResult.catch(err => {
|
|
456
|
+
if (!options.continueOnMinifyError) throw err;
|
|
457
|
+
options.log && options.log(err);
|
|
458
|
+
return originalAttrValue;
|
|
459
|
+
});
|
|
424
460
|
}
|
|
425
|
-
|
|
461
|
+
return cssResult != null ? cssResult : attrValue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
426
465
|
// Recursively minify HTML content within `srcdoc` attribute
|
|
427
466
|
// Fast-path: Skip if nothing would change
|
|
428
467
|
if (!shouldMinifyInnerHTML(options)) {
|
|
@@ -430,6 +469,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
430
469
|
}
|
|
431
470
|
return minifyHTMLSelf(attrValue, options, true);
|
|
432
471
|
}
|
|
472
|
+
|
|
433
473
|
return attrValue;
|
|
434
474
|
}
|
|
435
475
|
|
|
@@ -453,17 +493,24 @@ function chooseAttributeQuote(attrValue, options) {
|
|
|
453
493
|
return apos < quot ? '\'' : '"';
|
|
454
494
|
}
|
|
455
495
|
|
|
456
|
-
|
|
496
|
+
// Returns the normalized attribute object directly (sync) or as a Promise (async);
|
|
497
|
+
// callers must handle both cases—use `isThenable()` to distinguish
|
|
498
|
+
function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
457
499
|
const attrName = options.name(attr.name);
|
|
458
500
|
let attrValue = attr.value;
|
|
459
501
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
502
|
+
// Entity decoding requires a lazy import—async only when `&` is present
|
|
503
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
504
|
+
return getDecodeHTMLStrict().then(decode => {
|
|
505
|
+
return normalizeAttrContinue(attrName, decode(attrValue), attr, attrs, tag, options, minifyHTML);
|
|
506
|
+
});
|
|
465
507
|
}
|
|
466
508
|
|
|
509
|
+
return normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Internal: Handles attribute normalization after entity decoding (if any)
|
|
513
|
+
function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML) {
|
|
467
514
|
if ((options.removeRedundantAttributes &&
|
|
468
515
|
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
469
516
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
@@ -474,9 +521,18 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
474
521
|
}
|
|
475
522
|
|
|
476
523
|
if (attrValue) {
|
|
477
|
-
|
|
524
|
+
const cleaned = cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
525
|
+
if (isThenable(cleaned)) {
|
|
526
|
+
return cleaned.then(v => normalizeAttrFinish(attrName, v, attr, tag, options));
|
|
527
|
+
}
|
|
528
|
+
return normalizeAttrFinish(attrName, cleaned, attr, tag, options);
|
|
478
529
|
}
|
|
479
530
|
|
|
531
|
+
return normalizeAttrFinish(attrName, attrValue, attr, tag, options);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Internal: Final checks and result assembly after value cleaning
|
|
535
|
+
function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
|
|
480
536
|
if (options.removeEmptyAttributes &&
|
|
481
537
|
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
482
538
|
return;
|
package/src/lib/options.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Imports
|
|
2
2
|
|
|
3
3
|
import { createUrlMinifier } from './urls.js';
|
|
4
|
-
import { LRU, stableStringify, identity, lowercase,
|
|
4
|
+
import { LRU, stableStringify, identity, lowercase, replaceAsync, parseRegExp } from './utils.js';
|
|
5
5
|
import { RE_TRAILING_SEMICOLON } from './constants.js';
|
|
6
6
|
import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
|
|
7
7
|
import { wrapCSS, unwrapCSS } from './content.js';
|
|
@@ -16,7 +16,7 @@ function shouldMinifyInnerHTML(options) {
|
|
|
16
16
|
options.removeComments ||
|
|
17
17
|
options.removeOptionalTags ||
|
|
18
18
|
options.minifyJS !== identity ||
|
|
19
|
-
options.minifyCSS !==
|
|
19
|
+
options.minifyCSS !== identity ||
|
|
20
20
|
options.minifyURLs !== identity ||
|
|
21
21
|
options.minifySVG
|
|
22
22
|
);
|
|
@@ -43,7 +43,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
43
43
|
canTrimWhitespace,
|
|
44
44
|
...optionDefaults,
|
|
45
45
|
log: identity,
|
|
46
|
-
minifyCSS:
|
|
46
|
+
minifyCSS: identity,
|
|
47
47
|
minifyJS: identity,
|
|
48
48
|
minifyURLs: identity,
|
|
49
49
|
minifySVG: null
|
package/src/lib/utils.js
CHANGED
|
@@ -44,7 +44,7 @@ class LRU {
|
|
|
44
44
|
function uniqueId(value) {
|
|
45
45
|
let id;
|
|
46
46
|
do {
|
|
47
|
-
id =
|
|
47
|
+
id = 'u' + crypto.randomUUID().replace(/-/g, '');
|
|
48
48
|
} while (~value.indexOf(id));
|
|
49
49
|
return id;
|
|
50
50
|
}
|
|
@@ -55,8 +55,8 @@ function identity(value) {
|
|
|
55
55
|
return value;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
return
|
|
58
|
+
function isThenable(value) {
|
|
59
|
+
return value != null && typeof value === 'object' && typeof value.then === 'function';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function lowercase(value) {
|
|
@@ -104,7 +104,7 @@ export { stableStringify };
|
|
|
104
104
|
export { LRU };
|
|
105
105
|
export { uniqueId };
|
|
106
106
|
export { identity };
|
|
107
|
-
export {
|
|
107
|
+
export { isThenable };
|
|
108
108
|
export { lowercase };
|
|
109
109
|
export { replaceAsync };
|
|
110
110
|
export { parseRegExp };
|