html-minifier-next 5.1.2 → 5.1.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AA8CA,4BAAkE;AAuFlE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBA8lBC;CACF"}
1
+ {"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAgDA,4BAAkE;AAwGlE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAmmBC;CACF"}
@@ -19,12 +19,8 @@ export function isMetaViewport(tag: any, attrs: any): boolean;
19
19
  export function isContentSecurityPolicy(tag: any, attrs: any): boolean;
20
20
  export function canDeleteEmptyAttribute(tag: any, attrName: any, attrValue: any, options: any): any;
21
21
  export function hasAttrName(name: any, attrs: any): boolean;
22
- export function cleanAttributeValue(tag: any, attrName: any, attrValue: any, options: any, attrs: any, minifyHTMLSelf: any): Promise<any>;
23
- export function normalizeAttr(attr: any, attrs: any, tag: any, options: any, minifyHTML: any): Promise<{
24
- attr: any;
25
- name: any;
26
- value: any;
27
- }>;
22
+ export function cleanAttributeValue(tag: any, attrName: any, attrValue: any, options: any, attrs: any, minifyHTMLSelf: any): any;
23
+ export function normalizeAttr(attr: any, attrs: any, tag: any, options: any, minifyHTML: any): any;
28
24
  export function buildAttr(normalized: any, hasUnarySlash: any, options: any, isLast: any, uidAttr: any): any;
29
25
  /**
30
26
  * Remove duplicate attributes from an attribute list.
@@ -1 +1 @@
1
- {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAmCA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAgCD,mGAuCC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,8DAWC;AAED,2EAIC;AAmBD,qEAGC;AAgBD,wEAGC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,0IAqIC;AAsBD;;;;GAsCC;AAED,6GAuHC;AA3hBD;;;;;;;GAOG;AACH,mEAHW,OAAO,SAuBjB"}
1
+ {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAoCA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAgCD,mGAuCC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,8DAWC;AAED,2EAIC;AAmBD,qEAGC;AAgBD,wEAGC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAMD,iIA0KC;AAwBD,mGAYC;AA0CD,6GAuHC;AAllBD;;;;;;;GAOG;AACH,mEAHW,OAAO,SAuBjB"}
@@ -9,7 +9,7 @@ export class LRU {
9
9
  }
10
10
  export function uniqueId(value: any): string;
11
11
  export function identity(value: any): any;
12
- export function identityAsync(value: any): Promise<any>;
12
+ export function isThenable(value: any): boolean;
13
13
  export function lowercase(value: any): any;
14
14
  /**
15
15
  * Asynchronously replace matches in a string
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/lib/utils.js"],"names":[],"mappings":"AAEA,+CAUC;AAID;IACE,4BAGC;IAFC,cAAkB;IAClB,mBAAoB;IAEtB,mBAQC;IACD,gCAOC;IACD,uBAAqC;CACtC;AAID,6CAMC;AAID,0CAEC;AAED,wDAEC;AAED,2CAEC;AAID;;;;;;GAMG;AACH,kCALW,MAAM,SACN,MAAM,sBAEJ,OAAO,CAAC,MAAM,CAAC,CAY3B;AAID,6CAUC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/lib/utils.js"],"names":[],"mappings":"AAEA,+CAUC;AAID;IACE,4BAGC;IAFC,cAAkB;IAClB,mBAAoB;IAEtB,mBAQC;IACD,gCAOC;IACD,uBAAqC;CACtC;AAID,6CAMC;AAID,0CAEC;AAED,gDAEC;AAED,2CAEC;AAID;;;;;;GAMG;AACH,kCALW,MAAM,SACN,MAAM,sBAEJ,OAAO,CAAC,MAAM,CAAC,CAY3B;AAID,6CAUC"}
package/package.json CHANGED
@@ -95,5 +95,5 @@
95
95
  },
96
96
  "type": "module",
97
97
  "types": "./dist/types/htmlminifier.d.ts",
98
- "version": "5.1.2"
98
+ "version": "5.1.4"
99
99
  }
@@ -4,7 +4,7 @@ import { HTMLParser, endTag } from './htmlparser.js';
4
4
  import TokenChain from './tokenchain.js';
5
5
  import { presets, getPreset, getPresetNames } from './presets.js';
6
6
 
7
- import { LRU, identity, lowercase, uniqueId } from './lib/utils.js';
7
+ import { LRU, identity, isThenable, lowercase, uniqueId } from './lib/utils.js';
8
8
 
9
9
  import {
10
10
  RE_LEGACY_ENTITIES,
@@ -921,31 +921,47 @@ async function minifyHTML(value, options, partialMarkup) {
921
921
  let removeEmptyElementsExcept;
922
922
  if (options.removeEmptyElementsExcept && !Array.isArray(options.removeEmptyElementsExcept)) {
923
923
  if (options.log) {
924
- options.log('Warning: removeEmptyElementsExcept option must be an array, received: ' + typeof options.removeEmptyElementsExcept);
924
+ options.log('Warning: `removeEmptyElementsExcept` option must be an array, received: ' + typeof options.removeEmptyElementsExcept);
925
925
  }
926
926
  removeEmptyElementsExcept = [];
927
927
  } else {
928
928
  removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept, options) || [];
929
929
  }
930
930
 
931
- // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there.
932
- // For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
933
- value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
934
- if (!uidIgnore) {
935
- uidIgnore = uniqueId(value);
936
- const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
937
- uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
938
- if (options.ignoreCustomComments) {
939
- options.ignoreCustomComments = options.ignoreCustomComments.slice();
940
- } else {
941
- options.ignoreCustomComments = [];
931
+ // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there;
932
+ // for all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
933
+ if (value.indexOf('<!-- htmlmin:ignore -->') !== -1) {
934
+ // Use `indexOf`-based O(n) loop instead of a global regex with [\s\S]*? to avoid O(n²)
935
+ // backtracking on adversarial HTML with many `<!--` prefixes but no closing marker
936
+ const ignoreMarker = '<!-- htmlmin:ignore -->';
937
+ const ignoreMarkerLen = ignoreMarker.length;
938
+ let ignoreResult = '';
939
+ let ignorePos = 0;
940
+ while (ignorePos < value.length) {
941
+ const ignoreStart = value.indexOf(ignoreMarker, ignorePos);
942
+ if (ignoreStart === -1) { ignoreResult += value.slice(ignorePos); break; }
943
+ ignoreResult += value.slice(ignorePos, ignoreStart);
944
+ const ignoreEnd = value.indexOf(ignoreMarker, ignoreStart + ignoreMarkerLen);
945
+ if (ignoreEnd === -1) { ignoreResult += value.slice(ignoreStart); break; }
946
+ const group1 = value.slice(ignoreStart + ignoreMarkerLen, ignoreEnd);
947
+ if (!uidIgnore) {
948
+ uidIgnore = uniqueId(value);
949
+ const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
950
+ uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
951
+ if (options.ignoreCustomComments) {
952
+ options.ignoreCustomComments = options.ignoreCustomComments.slice();
953
+ } else {
954
+ options.ignoreCustomComments = [];
955
+ }
956
+ options.ignoreCustomComments.push(pattern);
942
957
  }
943
- options.ignoreCustomComments.push(pattern);
958
+ const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
959
+ ignoredMarkupChunks.push(group1);
960
+ ignoreResult += token;
961
+ ignorePos = ignoreEnd + ignoreMarkerLen;
944
962
  }
945
- const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
946
- ignoredMarkupChunks.push(group1);
947
- return token;
948
- });
963
+ value = ignoreResult;
964
+ }
949
965
 
950
966
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
951
967
  // This allows proper frequency analysis with access to ignored content via UID tokens
@@ -1180,9 +1196,8 @@ async function minifyHTML(value, options, partialMarkup) {
1180
1196
  options.sortAttributes(tag, attrs);
1181
1197
  }
1182
1198
 
1183
- const normalizedAttrs = await Promise.all(
1184
- attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML))
1185
- );
1199
+ const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
1200
+ const normalizedAttrs = attrResults.some(isThenable) ? await Promise.all(attrResults) : attrResults;
1186
1201
  const parts = [];
1187
1202
  let isLast = true;
1188
1203
  for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
@@ -1310,207 +1325,225 @@ async function minifyHTML(value, options, partialMarkup) {
1310
1325
  }
1311
1326
  }
1312
1327
  },
1313
- chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
1328
+ chars: function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
1314
1329
  prevTag = prevTag === '' ? 'comment' : prevTag;
1315
1330
  nextTag = nextTag === '' ? 'comment' : nextTag;
1316
1331
  prevAttrs = prevAttrs || [];
1317
1332
  nextAttrs = nextAttrs || [];
1318
- if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
1319
- if (text.indexOf('&') !== -1) {
1320
- text = (await getDecodeHTML())(text);
1321
- }
1322
- }
1323
- // Trim outermost newline-based whitespace inside `pre`/`textarea` elements
1324
- // This removes trailing newlines often added by template engines before closing tags
1325
- // Only trims single trailing newlines (multiple newlines are likely intentional formatting)
1326
- if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
1327
- const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
1328
- if (preTextareaDepth > 0) {
1329
- // Trim trailing whitespace only if it ends with a single newline (not multiple)
1330
- // Multiple newlines are likely intentional formatting, single newline is often a template artifact
1331
- // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
1332
- if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
1333
- text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
1333
+
1334
+ // Detect whether any async work is actually needed for this text node
1335
+ const needsDecode = options.decodeEntities && text && !specialContentElements.has(currentTag) && text.indexOf('&') !== -1;
1336
+ const needsProcessScript = specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs));
1337
+ const needsMinifyJS = options.minifyJS !== identity && isExecutableScript(currentTag, currentAttrs);
1338
+ const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
1339
+
1340
+ // Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
1341
+ function charsCollapse(text) {
1342
+ // Trim outermost newline-based whitespace inside `pre`/`textarea` elements
1343
+ // This removes trailing newlines often added by template engines before closing tags
1344
+ // Only trims single trailing newlines (multiple newlines are likely intentional formatting)
1345
+ if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
1346
+ const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
1347
+ if (preTextareaDepth > 0) {
1348
+ // Trim trailing whitespace only if it ends with a single newline (not multiple)
1349
+ // Multiple newlines are likely intentional formatting, single newline is often a template artifact
1350
+ // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
1351
+ if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
1352
+ text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
1353
+ }
1334
1354
  }
1335
1355
  }
1336
- }
1337
- if (options.collapseWhitespace) {
1338
- if (!stackNoTrimWhitespace.length) {
1339
- if (prevTag === 'comment') {
1340
- const prevComment = buffer[buffer.length - 1];
1341
- if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
1342
- if (!prevComment) {
1343
- prevTag = charsPrevTag;
1344
- }
1345
- if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
1346
- const charsIndex = buffer.length - 2;
1347
- buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
1348
- text = trailingSpaces + text;
1349
- return '';
1350
- });
1356
+ if (options.collapseWhitespace) {
1357
+ if (!stackNoTrimWhitespace.length) {
1358
+ if (prevTag === 'comment') {
1359
+ const prevComment = buffer[buffer.length - 1];
1360
+ if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
1361
+ if (!prevComment) {
1362
+ prevTag = charsPrevTag;
1363
+ }
1364
+ if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
1365
+ const charsIndex = buffer.length - 2;
1366
+ buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
1367
+ text = trailingSpaces + text;
1368
+ return '';
1369
+ });
1370
+ }
1351
1371
  }
1352
1372
  }
1353
- }
1354
- if (prevTag) {
1355
- if (prevTag === '/nobr' || prevTag === 'wbr') {
1356
- if (/^\s/.test(text)) {
1357
- let tagIndex = buffer.length - 1;
1358
- while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
1359
- tagIndex--;
1373
+ if (prevTag) {
1374
+ if (prevTag === '/nobr' || prevTag === 'wbr') {
1375
+ if (/^\s/.test(text)) {
1376
+ let tagIndex = buffer.length - 1;
1377
+ while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
1378
+ tagIndex--;
1379
+ }
1380
+ trimTrailingWhitespace(tagIndex - 1, 'br');
1360
1381
  }
1361
- trimTrailingWhitespace(tagIndex - 1, 'br');
1382
+ } else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1383
+ text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
1362
1384
  }
1363
- } else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1364
- text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
1385
+ }
1386
+ if (prevTag || nextTag) {
1387
+ text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
1388
+ } else {
1389
+ text = collapseWhitespace(text, options, true, true);
1390
+ }
1391
+ if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
1392
+ trimTrailingWhitespace(buffer.length - 1, nextTag);
1365
1393
  }
1366
1394
  }
1367
- if (prevTag || nextTag) {
1368
- text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
1369
- } else {
1370
- text = collapseWhitespace(text, options, true, true);
1395
+ if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
1396
+ text = collapseWhitespace(text, options, false, false, true);
1371
1397
  }
1372
- if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
1373
- trimTrailingWhitespace(buffer.length - 1, nextTag);
1398
+ }
1399
+ return text;
1400
+ }
1401
+
1402
+ // Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
1403
+ function charsFinalize(text) {
1404
+ if (options.removeOptionalTags && text) {
1405
+ // `<html>` may be omitted if first thing inside is not a comment
1406
+ // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
1407
+ if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
1408
+ removeStartTag();
1409
+ }
1410
+ optionalStartTag = '';
1411
+ // `</html>` or `</body>` may be omitted if not followed by comment
1412
+ // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
1413
+ if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
1414
+ removeEndTag();
1415
+ }
1416
+ // Don’t reset `optionalEndTag` if text is only whitespace and will be collapsed (not conservatively)
1417
+ if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
1418
+ optionalEndTag = '';
1419
+ optionalEndTagEmitted = false;
1374
1420
  }
1375
1421
  }
1376
- if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
1377
- text = collapseWhitespace(text, options, false, false, true);
1422
+ charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
1423
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
1424
+ // Escape any `&` symbols that start either:
1425
+ // 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
1426
+ // 2. or any other character reference (i.e., one that does end with `;`)
1427
+ // Note that `&` can be escaped as `&amp`, without the semicolon.
1428
+ // https://mathiasbynens.be/notes/ambiguous-ampersands
1429
+ if (text.indexOf('&') !== -1) {
1430
+ text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
1431
+ }
1432
+ if (text.indexOf('<') !== -1) {
1433
+ text = text.replace(RE_ESCAPE_LT, '&lt;');
1434
+ }
1378
1435
  }
1436
+ if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
1437
+ text = text.replace(uidPattern, function (match, prefix, index) {
1438
+ return ignoredCustomMarkupChunks[+index][0];
1439
+ });
1440
+ }
1441
+ currentChars += text;
1442
+ if (text) {
1443
+ hasChars = true;
1444
+ }
1445
+ buffer.push(text);
1379
1446
  }
1380
- if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
1381
- text = await processScript(text, options, currentAttrs, minifyHTML);
1382
- }
1383
- if (isExecutableScript(currentTag, currentAttrs)) {
1384
- text = await options.minifyJS(text);
1385
- }
1386
- if (isStyleElement(currentTag, currentAttrs)) {
1387
- text = await options.minifyCSS(text);
1447
+
1448
+ // Fast path: All work is sync—skip async machinery entirely
1449
+ if (!needsDecode && !needsProcessScript && !needsMinifyJS && !needsMinifyCSS) {
1450
+ charsFinalize(charsCollapse(text));
1451
+ return;
1388
1452
  }
1389
- if (options.removeOptionalTags && text) {
1390
- // `<html>` may be omitted if first thing inside is not a comment
1391
- // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
1392
- if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
1393
- removeStartTag();
1394
- }
1395
- optionalStartTag = '';
1396
- // `</html>` or `</body>` may be omitted if not followed by comment
1397
- // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
1398
- if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
1399
- removeEndTag();
1453
+
1454
+ // Slow path: At least one async step required
1455
+ return (async () => {
1456
+ if (needsDecode) {
1457
+ text = (await getDecodeHTML())(text);
1400
1458
  }
1401
- // Don't reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
1402
- if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
1403
- optionalEndTag = '';
1404
- optionalEndTagEmitted = false;
1459
+ text = charsCollapse(text);
1460
+ if (needsProcessScript) {
1461
+ text = await processScript(text, options, currentAttrs, minifyHTML);
1405
1462
  }
1406
- }
1407
- charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
1408
- if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
1409
- // Escape any `&` symbols that start either:
1410
- // 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
1411
- // 2. or any other character reference (i.e., one that does end with `;`)
1412
- // Note that `&` can be escaped as `&amp`, without the semicolon.
1413
- // https://mathiasbynens.be/notes/ambiguous-ampersands
1414
- if (text.indexOf('&') !== -1) {
1415
- text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
1463
+ if (needsMinifyJS) {
1464
+ text = await options.minifyJS(text);
1416
1465
  }
1417
- if (text.indexOf('<') !== -1) {
1418
- text = text.replace(RE_ESCAPE_LT, '&lt;');
1466
+ if (needsMinifyCSS) {
1467
+ text = await options.minifyCSS(text);
1419
1468
  }
1420
- }
1421
- if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
1422
- text = text.replace(uidPattern, function (match, prefix, index) {
1423
- return ignoredCustomMarkupChunks[+index][0];
1424
- });
1425
- }
1426
- currentChars += text;
1427
- if (text) {
1428
- hasChars = true;
1429
- }
1430
- buffer.push(text);
1469
+ charsFinalize(text);
1470
+ })();
1431
1471
  },
1432
- comment: async function (text, nonStandard) {
1472
+ comment: function (text, nonStandard) {
1433
1473
  const prefix = nonStandard ? '<!' : '<!--';
1434
1474
  const suffix = nonStandard ? '>' : '-->';
1435
- if (isConditionalComment(text)) {
1436
- text = prefix + await cleanConditionalComment(text, options, minifyHTML) + suffix;
1437
- } else if (options.removeComments) {
1438
- if (isIgnoredComment(text, options)) {
1439
- text = '<!--' + text + '-->';
1440
- } else {
1441
- text = '';
1475
+
1476
+ // Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
1477
+ function commentFinalize(comment) {
1478
+ if (options.removeOptionalTags && comment) {
1479
+ // Preceding comments suppress tag omissions
1480
+ optionalStartTag = '';
1481
+ optionalEndTag = '';
1482
+ optionalEndTagEmitted = false;
1442
1483
  }
1443
- } else {
1444
- text = prefix + text + suffix;
1445
- }
1446
- if (options.removeOptionalTags && text) {
1447
- // Preceding comments suppress tag omissions
1448
- optionalStartTag = '';
1449
- optionalEndTag = '';
1450
- optionalEndTagEmitted = false;
1451
- }
1452
1484
 
1453
- // Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
1454
- if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
1455
- if (uidIgnorePlaceholderPattern.test(text)) {
1456
- // Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
1457
- if (buffer.length >= 2) {
1458
- const prevText = buffer[buffer.length - 1];
1459
- const prevComment = buffer[buffer.length - 2];
1460
-
1461
- // Check if previous item is whitespace-only and item before that is ignore-placeholder
1462
- if (prevText && /^\s+$/.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
1463
- // Extract the index from both placeholders to check their content
1464
- const currentMatch = text.match(uidIgnorePlaceholderPattern);
1465
- const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
1466
-
1467
- if (currentMatch && prevMatch) {
1468
- const currentIndex = +currentMatch[1];
1469
- const prevIndex = +prevMatch[1];
1470
-
1471
- // Defensive bounds check to ensure indices are valid
1472
- if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
1473
- const currentContent = ignoredMarkupChunks[currentIndex];
1474
- const prevContent = ignoredMarkupChunks[prevIndex];
1475
-
1476
- // Only collapse whitespace if both blocks contain HTML (start with `<`)
1477
- // Don’t collapse if either contains plain text, as that would change meaning
1478
- // Note: This check will match HTML comments (`<!-- … -->`), but the tag name
1479
- // regex below requires starting with a letter, so comments are intentionally
1480
- // excluded by the `currentTagMatch && prevTagMatch` guard
1481
- if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
1482
- // Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
1483
- const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
1484
- const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
1485
-
1486
- // Only collapse if both matched valid element tags (not comments/text)
1487
- // and both tags are block-level (inline elements need whitespace preserved)
1488
- if (currentTagMatch && prevTagMatch) {
1489
- const currentTag = options.name(currentTagMatch[1]);
1490
- const prevTag = options.name(prevTagMatch[1]);
1491
-
1492
- // Don’t collapse between inline elements
1493
- if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
1494
- // Collapse whitespace respecting context rules
1495
- let collapsedText = prevText;
1496
-
1497
- // Apply `collapseWhitespace` with appropriate context
1498
- if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
1499
- // Not in pre or other no-collapse context
1500
- if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
1501
- // Preserve line break as single newline
1502
- collapsedText = '\n';
1503
- } else if (options.conservativeCollapse) {
1504
- // Conservative mode: keep single space
1505
- collapsedText = ' ';
1506
- } else {
1507
- // Aggressive mode: remove all whitespace
1508
- collapsedText = '';
1485
+ // Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
1486
+ if (options.collapseWhitespace && comment && uidIgnorePlaceholderPattern) {
1487
+ if (uidIgnorePlaceholderPattern.test(comment)) {
1488
+ // Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
1489
+ if (buffer.length >= 2) {
1490
+ const prevText = buffer[buffer.length - 1];
1491
+ const prevComment = buffer[buffer.length - 2];
1492
+
1493
+ // Check if previous item is whitespace-only and item before that is ignore-placeholder
1494
+ if (prevText && /^\s+$/.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
1495
+ // Extract the index from both placeholders to check their content
1496
+ const currentMatch = comment.match(uidIgnorePlaceholderPattern);
1497
+ const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
1498
+
1499
+ if (currentMatch && prevMatch) {
1500
+ const currentIndex = +currentMatch[1];
1501
+ const prevIndex = +prevMatch[1];
1502
+
1503
+ // Defensive bounds check to ensure indices are valid
1504
+ if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
1505
+ const currentContent = ignoredMarkupChunks[currentIndex];
1506
+ const prevContent = ignoredMarkupChunks[prevIndex];
1507
+
1508
+ // Only collapse whitespace if both blocks contain HTML (start with `<`)
1509
+ // Don’t collapse if either contains plain text, as that would change meaning
1510
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag name
1511
+ // regex below requires starting with a letter, so comments are intentionally
1512
+ // excluded by the `currentTagMatch && prevTagMatch` guard
1513
+ if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
1514
+ // Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
1515
+ const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
1516
+ const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
1517
+
1518
+ // Only collapse if both matched valid element tags (not comments/text)
1519
+ // and both tags are block-level (inline elements need whitespace preserved)
1520
+ if (currentTagMatch && prevTagMatch) {
1521
+ const currentTag = options.name(currentTagMatch[1]);
1522
+ const prevTag = options.name(prevTagMatch[1]);
1523
+
1524
+ // Don’t collapse between inline elements
1525
+ if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
1526
+ // Collapse whitespace respecting context rules
1527
+ let collapsedText = prevText;
1528
+
1529
+ // Apply `collapseWhitespace` with appropriate context
1530
+ if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
1531
+ // Not in pre or other no-collapse context
1532
+ if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
1533
+ // Preserve line break as single newline
1534
+ collapsedText = '\n';
1535
+ } else if (options.conservativeCollapse) {
1536
+ // Conservative mode: Keep single space
1537
+ collapsedText = ' ';
1538
+ } else {
1539
+ // Aggressive mode: Remove all whitespace
1540
+ collapsedText = '';
1541
+ }
1509
1542
  }
1510
- }
1511
1543
 
1512
- // Replace the whitespace in buffer
1513
- buffer[buffer.length - 1] = collapsedText;
1544
+ // Replace the whitespace in buffer
1545
+ buffer[buffer.length - 1] = collapsedText;
1546
+ }
1514
1547
  }
1515
1548
  }
1516
1549
  }
@@ -1519,9 +1552,27 @@ async function minifyHTML(value, options, partialMarkup) {
1519
1552
  }
1520
1553
  }
1521
1554
  }
1555
+
1556
+ buffer.push(comment);
1522
1557
  }
1523
1558
 
1524
- buffer.push(text);
1559
+ // Only conditional comments require async work (recursive minification)
1560
+ if (isConditionalComment(text)) {
1561
+ return cleanConditionalComment(text, options, minifyHTML).then(cleaned => {
1562
+ commentFinalize(prefix + cleaned + suffix);
1563
+ });
1564
+ }
1565
+
1566
+ if (options.removeComments) {
1567
+ if (isIgnoredComment(text, options)) {
1568
+ text = '<!--' + text + '-->';
1569
+ } else {
1570
+ text = '';
1571
+ }
1572
+ } else {
1573
+ text = prefix + text + suffix;
1574
+ }
1575
+ commentFinalize(text);
1525
1576
  },
1526
1577
  doctype: function (doctype) {
1527
1578
  buffer.push(options.useShortDoctype