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.
- package/dist/htmlminifier.cjs +504 -375
- 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 +245 -194
- package/src/htmlparser.js +34 -10
- 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;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):
|
|
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,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:
|
|
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
|
+
// 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
|
-
|
|
958
|
+
const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
|
959
|
+
ignoredMarkupChunks.push(group1);
|
|
960
|
+
ignoreResult += token;
|
|
961
|
+
ignorePos = ignoreEnd + ignoreMarkerLen;
|
|
944
962
|
}
|
|
945
|
-
|
|
946
|
-
|
|
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
|
|
1184
|
-
|
|
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:
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
//
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1382
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
1383
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
1362
1384
|
}
|
|
1363
|
-
}
|
|
1364
|
-
|
|
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
|
|
1368
|
-
text =
|
|
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
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
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 `&`, without the semicolon.
|
|
1428
|
+
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
1429
|
+
if (text.indexOf('&') !== -1) {
|
|
1430
|
+
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
1431
|
+
}
|
|
1432
|
+
if (text.indexOf('<') !== -1) {
|
|
1433
|
+
text = text.replace(RE_ESCAPE_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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
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();
|
|
1453
|
+
|
|
1454
|
+
// Slow path: At least one async step required
|
|
1455
|
+
return (async () => {
|
|
1456
|
+
if (needsDecode) {
|
|
1457
|
+
text = (await getDecodeHTML())(text);
|
|
1400
1458
|
}
|
|
1401
|
-
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
optionalEndTagEmitted = false;
|
|
1459
|
+
text = charsCollapse(text);
|
|
1460
|
+
if (needsProcessScript) {
|
|
1461
|
+
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
1405
1462
|
}
|
|
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');
|
|
1463
|
+
if (needsMinifyJS) {
|
|
1464
|
+
text = await options.minifyJS(text);
|
|
1416
1465
|
}
|
|
1417
|
-
if (
|
|
1418
|
-
text =
|
|
1466
|
+
if (needsMinifyCSS) {
|
|
1467
|
+
text = await options.minifyCSS(text);
|
|
1419
1468
|
}
|
|
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);
|
|
1469
|
+
charsFinalize(text);
|
|
1470
|
+
})();
|
|
1431
1471
|
},
|
|
1432
|
-
comment:
|
|
1472
|
+
comment: function (text, nonStandard) {
|
|
1433
1473
|
const prefix = nonStandard ? '<!' : '<!--';
|
|
1434
1474
|
const suffix = nonStandard ? '>' : '-->';
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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
|