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.
@@ -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;AAuFlE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAomBC;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.3"
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,33 @@ 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
+ value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
935
+ if (!uidIgnore) {
936
+ uidIgnore = uniqueId(value);
937
+ const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
938
+ uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
939
+ if (options.ignoreCustomComments) {
940
+ options.ignoreCustomComments = options.ignoreCustomComments.slice();
941
+ } else {
942
+ options.ignoreCustomComments = [];
943
+ }
944
+ options.ignoreCustomComments.push(pattern);
942
945
  }
943
- options.ignoreCustomComments.push(pattern);
944
- }
945
- const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
946
- ignoredMarkupChunks.push(group1);
947
- return token;
948
- });
946
+ const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
947
+ ignoredMarkupChunks.push(group1);
948
+ return token;
949
+ });
950
+ }
949
951
 
950
952
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
951
953
  // This allows proper frequency analysis with access to ignored content via UID tokens
@@ -1180,9 +1182,8 @@ async function minifyHTML(value, options, partialMarkup) {
1180
1182
  options.sortAttributes(tag, attrs);
1181
1183
  }
1182
1184
 
1183
- const normalizedAttrs = await Promise.all(
1184
- attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML))
1185
- );
1185
+ const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
1186
+ const normalizedAttrs = attrResults.some(isThenable) ? await Promise.all(attrResults) : attrResults;
1186
1187
  const parts = [];
1187
1188
  let isLast = true;
1188
1189
  for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
@@ -1310,207 +1311,225 @@ async function minifyHTML(value, options, partialMarkup) {
1310
1311
  }
1311
1312
  }
1312
1313
  },
1313
- chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
1314
+ chars: function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
1314
1315
  prevTag = prevTag === '' ? 'comment' : prevTag;
1315
1316
  nextTag = nextTag === '' ? 'comment' : nextTag;
1316
1317
  prevAttrs = prevAttrs || [];
1317
1318
  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]*$/, '');
1319
+
1320
+ // Detect whether any async work is actually needed for this text node
1321
+ const needsDecode = options.decodeEntities && text && !specialContentElements.has(currentTag) && text.indexOf('&') !== -1;
1322
+ const needsProcessScript = specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs));
1323
+ const needsMinifyJS = options.minifyJS !== identity && isExecutableScript(currentTag, currentAttrs);
1324
+ const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
1325
+
1326
+ // Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
1327
+ function charsCollapse(text) {
1328
+ // Trim outermost newline-based whitespace inside `pre`/`textarea` elements
1329
+ // This removes trailing newlines often added by template engines before closing tags
1330
+ // Only trims single trailing newlines (multiple newlines are likely intentional formatting)
1331
+ if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
1332
+ const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
1333
+ if (preTextareaDepth > 0) {
1334
+ // Trim trailing whitespace only if it ends with a single newline (not multiple)
1335
+ // Multiple newlines are likely intentional formatting, single newline is often a template artifact
1336
+ // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
1337
+ if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
1338
+ text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
1339
+ }
1334
1340
  }
1335
1341
  }
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
- });
1342
+ if (options.collapseWhitespace) {
1343
+ if (!stackNoTrimWhitespace.length) {
1344
+ if (prevTag === 'comment') {
1345
+ const prevComment = buffer[buffer.length - 1];
1346
+ if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
1347
+ if (!prevComment) {
1348
+ prevTag = charsPrevTag;
1349
+ }
1350
+ if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
1351
+ const charsIndex = buffer.length - 2;
1352
+ buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
1353
+ text = trailingSpaces + text;
1354
+ return '';
1355
+ });
1356
+ }
1351
1357
  }
1352
1358
  }
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--;
1359
+ if (prevTag) {
1360
+ if (prevTag === '/nobr' || prevTag === 'wbr') {
1361
+ if (/^\s/.test(text)) {
1362
+ let tagIndex = buffer.length - 1;
1363
+ while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
1364
+ tagIndex--;
1365
+ }
1366
+ trimTrailingWhitespace(tagIndex - 1, 'br');
1360
1367
  }
1361
- trimTrailingWhitespace(tagIndex - 1, 'br');
1368
+ } else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1369
+ text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
1362
1370
  }
1363
- } else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1364
- text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
1371
+ }
1372
+ if (prevTag || nextTag) {
1373
+ text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
1374
+ } else {
1375
+ text = collapseWhitespace(text, options, true, true);
1376
+ }
1377
+ if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
1378
+ trimTrailingWhitespace(buffer.length - 1, nextTag);
1365
1379
  }
1366
1380
  }
1367
- if (prevTag || nextTag) {
1368
- text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
1369
- } else {
1370
- text = collapseWhitespace(text, options, true, true);
1381
+ if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
1382
+ text = collapseWhitespace(text, options, false, false, true);
1371
1383
  }
1372
- if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
1373
- trimTrailingWhitespace(buffer.length - 1, nextTag);
1384
+ }
1385
+ return text;
1386
+ }
1387
+
1388
+ // Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
1389
+ function charsFinalize(text) {
1390
+ if (options.removeOptionalTags && text) {
1391
+ // `<html>` may be omitted if first thing inside is not a comment
1392
+ // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
1393
+ if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
1394
+ removeStartTag();
1395
+ }
1396
+ optionalStartTag = '';
1397
+ // `</html>` or `</body>` may be omitted if not followed by comment
1398
+ // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
1399
+ if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
1400
+ removeEndTag();
1401
+ }
1402
+ // Don’t reset `optionalEndTag` if text is only whitespace and will be collapsed (not conservatively)
1403
+ if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
1404
+ optionalEndTag = '';
1405
+ optionalEndTagEmitted = false;
1374
1406
  }
1375
1407
  }
1376
- if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
1377
- text = collapseWhitespace(text, options, false, false, true);
1408
+ charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
1409
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
1410
+ // Escape any `&` symbols that start either:
1411
+ // 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
1412
+ // 2. or any other character reference (i.e., one that does end with `;`)
1413
+ // Note that `&` can be escaped as `&amp`, without the semicolon.
1414
+ // https://mathiasbynens.be/notes/ambiguous-ampersands
1415
+ if (text.indexOf('&') !== -1) {
1416
+ text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
1417
+ }
1418
+ if (text.indexOf('<') !== -1) {
1419
+ text = text.replace(RE_ESCAPE_LT, '&lt;');
1420
+ }
1378
1421
  }
1422
+ if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
1423
+ text = text.replace(uidPattern, function (match, prefix, index) {
1424
+ return ignoredCustomMarkupChunks[+index][0];
1425
+ });
1426
+ }
1427
+ currentChars += text;
1428
+ if (text) {
1429
+ hasChars = true;
1430
+ }
1431
+ buffer.push(text);
1379
1432
  }
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);
1433
+
1434
+ // Fast path: All work is sync—skip async machinery entirely
1435
+ if (!needsDecode && !needsProcessScript && !needsMinifyJS && !needsMinifyCSS) {
1436
+ charsFinalize(charsCollapse(text));
1437
+ return;
1388
1438
  }
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();
1439
+
1440
+ // Slow path: At least one async step required
1441
+ return (async () => {
1442
+ if (needsDecode) {
1443
+ text = (await getDecodeHTML())(text);
1400
1444
  }
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;
1445
+ text = charsCollapse(text);
1446
+ if (needsProcessScript) {
1447
+ text = await processScript(text, options, currentAttrs, minifyHTML);
1405
1448
  }
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');
1449
+ if (needsMinifyJS) {
1450
+ text = await options.minifyJS(text);
1416
1451
  }
1417
- if (text.indexOf('<') !== -1) {
1418
- text = text.replace(RE_ESCAPE_LT, '&lt;');
1452
+ if (needsMinifyCSS) {
1453
+ text = await options.minifyCSS(text);
1419
1454
  }
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);
1455
+ charsFinalize(text);
1456
+ })();
1431
1457
  },
1432
- comment: async function (text, nonStandard) {
1458
+ comment: function (text, nonStandard) {
1433
1459
  const prefix = nonStandard ? '<!' : '<!--';
1434
1460
  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 = '';
1461
+
1462
+ // Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
1463
+ function commentFinalize(comment) {
1464
+ if (options.removeOptionalTags && comment) {
1465
+ // Preceding comments suppress tag omissions
1466
+ optionalStartTag = '';
1467
+ optionalEndTag = '';
1468
+ optionalEndTagEmitted = false;
1442
1469
  }
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
1470
 
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 = '';
1471
+ // Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
1472
+ if (options.collapseWhitespace && comment && uidIgnorePlaceholderPattern) {
1473
+ if (uidIgnorePlaceholderPattern.test(comment)) {
1474
+ // Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
1475
+ if (buffer.length >= 2) {
1476
+ const prevText = buffer[buffer.length - 1];
1477
+ const prevComment = buffer[buffer.length - 2];
1478
+
1479
+ // Check if previous item is whitespace-only and item before that is ignore-placeholder
1480
+ if (prevText && /^\s+$/.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
1481
+ // Extract the index from both placeholders to check their content
1482
+ const currentMatch = comment.match(uidIgnorePlaceholderPattern);
1483
+ const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
1484
+
1485
+ if (currentMatch && prevMatch) {
1486
+ const currentIndex = +currentMatch[1];
1487
+ const prevIndex = +prevMatch[1];
1488
+
1489
+ // Defensive bounds check to ensure indices are valid
1490
+ if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
1491
+ const currentContent = ignoredMarkupChunks[currentIndex];
1492
+ const prevContent = ignoredMarkupChunks[prevIndex];
1493
+
1494
+ // Only collapse whitespace if both blocks contain HTML (start with `<`)
1495
+ // Don’t collapse if either contains plain text, as that would change meaning
1496
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag name
1497
+ // regex below requires starting with a letter, so comments are intentionally
1498
+ // excluded by the `currentTagMatch && prevTagMatch` guard
1499
+ if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
1500
+ // Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
1501
+ const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
1502
+ const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
1503
+
1504
+ // Only collapse if both matched valid element tags (not comments/text)
1505
+ // and both tags are block-level (inline elements need whitespace preserved)
1506
+ if (currentTagMatch && prevTagMatch) {
1507
+ const currentTag = options.name(currentTagMatch[1]);
1508
+ const prevTag = options.name(prevTagMatch[1]);
1509
+
1510
+ // Don’t collapse between inline elements
1511
+ if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
1512
+ // Collapse whitespace respecting context rules
1513
+ let collapsedText = prevText;
1514
+
1515
+ // Apply `collapseWhitespace` with appropriate context
1516
+ if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
1517
+ // Not in pre or other no-collapse context
1518
+ if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
1519
+ // Preserve line break as single newline
1520
+ collapsedText = '\n';
1521
+ } else if (options.conservativeCollapse) {
1522
+ // Conservative mode: Keep single space
1523
+ collapsedText = ' ';
1524
+ } else {
1525
+ // Aggressive mode: Remove all whitespace
1526
+ collapsedText = '';
1527
+ }
1509
1528
  }
1510
- }
1511
1529
 
1512
- // Replace the whitespace in buffer
1513
- buffer[buffer.length - 1] = collapsedText;
1530
+ // Replace the whitespace in buffer
1531
+ buffer[buffer.length - 1] = collapsedText;
1532
+ }
1514
1533
  }
1515
1534
  }
1516
1535
  }
@@ -1519,9 +1538,27 @@ async function minifyHTML(value, options, partialMarkup) {
1519
1538
  }
1520
1539
  }
1521
1540
  }
1541
+
1542
+ buffer.push(comment);
1522
1543
  }
1523
1544
 
1524
- buffer.push(text);
1545
+ // Only conditional comments require async work (recursive minification)
1546
+ if (isConditionalComment(text)) {
1547
+ return cleanConditionalComment(text, options, minifyHTML).then(cleaned => {
1548
+ commentFinalize(prefix + cleaned + suffix);
1549
+ });
1550
+ }
1551
+
1552
+ if (options.removeComments) {
1553
+ if (isIgnoredComment(text, options)) {
1554
+ text = '<!--' + text + '-->';
1555
+ } else {
1556
+ text = '';
1557
+ }
1558
+ } else {
1559
+ text = prefix + text + suffix;
1560
+ }
1561
+ commentFinalize(text);
1525
1562
  },
1526
1563
  doctype: function (doctype) {
1527
1564
  buffer.push(options.useShortDoctype