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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"
|
|
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":"
|
|
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):
|
|
23
|
-
export function normalizeAttr(attr: any, attrs: any, tag: any, options: any, minifyHTML: any):
|
|
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":"
|
|
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
|
|
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,
|
|
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
package/src/htmlminifier.js
CHANGED
|
@@ -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:
|
|
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
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
|
1184
|
-
|
|
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:
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
//
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1368
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
1369
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
1362
1370
|
}
|
|
1363
|
-
}
|
|
1364
|
-
|
|
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
|
|
1368
|
-
text =
|
|
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
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
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 `&`, without the semicolon.
|
|
1414
|
+
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
1415
|
+
if (text.indexOf('&') !== -1) {
|
|
1416
|
+
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
1417
|
+
}
|
|
1418
|
+
if (text.indexOf('<') !== -1) {
|
|
1419
|
+
text = text.replace(RE_ESCAPE_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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
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
|
-
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
optionalEndTagEmitted = false;
|
|
1445
|
+
text = charsCollapse(text);
|
|
1446
|
+
if (needsProcessScript) {
|
|
1447
|
+
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
1405
1448
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
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 `&`, without the semicolon.
|
|
1413
|
-
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
1414
|
-
if (text.indexOf('&') !== -1) {
|
|
1415
|
-
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
1449
|
+
if (needsMinifyJS) {
|
|
1450
|
+
text = await options.minifyJS(text);
|
|
1416
1451
|
}
|
|
1417
|
-
if (
|
|
1418
|
-
text =
|
|
1452
|
+
if (needsMinifyCSS) {
|
|
1453
|
+
text = await options.minifyCSS(text);
|
|
1419
1454
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
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:
|
|
1458
|
+
comment: function (text, nonStandard) {
|
|
1433
1459
|
const prefix = nonStandard ? '<!' : '<!--';
|
|
1434
1460
|
const suffix = nonStandard ? '>' : '-->';
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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
|