html-minifier 5.0.1 → 5.0.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/README.md +12 -7
- package/cli.js +91 -32
- 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 +3 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +5 -1
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts.map +1 -1
- package/dist/types/lib/fragments.d.ts +13 -1
- package/dist/types/lib/fragments.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +1 -0
- package/dist/types/lib/options.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/htmlminifier.js +182 -72
- package/src/htmlparser.js +309 -24
- package/src/lib/attributes.js +108 -20
- package/src/lib/constants.js +16 -5
- package/src/lib/elements.js +7 -0
- package/src/lib/fragments.js +274 -1
- package/src/lib/options.js +5 -4
package/src/htmlminifier.js
CHANGED
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
RE_WS_ONLY,
|
|
18
18
|
RE_WS_NBSP_END,
|
|
19
19
|
RE_STYLE_ELEMENT,
|
|
20
|
+
RE_ENDS_COLLAPSIBLE,
|
|
21
|
+
RE_DOCTYPE_SEGMENT,
|
|
20
22
|
formattingElements,
|
|
23
|
+
inlineElementsToKeepWhitespace,
|
|
21
24
|
inlineElementsToKeepWhitespaceAround,
|
|
22
25
|
inlineElementsToKeepWhitespaceWithin,
|
|
23
26
|
inlineElementsTransparent,
|
|
@@ -78,7 +81,7 @@ import {
|
|
|
78
81
|
} from './lib/content.js';
|
|
79
82
|
|
|
80
83
|
import { processOptions } from './lib/options.js';
|
|
81
|
-
import { toFragment, replaceCustomFragments } from './lib/fragments.js';
|
|
84
|
+
import { toFragment, replaceCustomFragments, isMinifiedCodeSafe } from './lib/fragments.js';
|
|
82
85
|
|
|
83
86
|
/** @import { ProcessedOptions } from './lib/options.js' */
|
|
84
87
|
|
|
@@ -826,13 +829,13 @@ function mergeConsecutiveScripts(html, hasMathML = false) {
|
|
|
826
829
|
}
|
|
827
830
|
if (boolConflict || a1.nonce !== a2.nonce || (!mergedAny && insideMath(m1.index))) break;
|
|
828
831
|
|
|
829
|
-
// Join with a semicolon,
|
|
832
|
+
// Join with a semicolon, after a newline where the first may end in a `//` comment
|
|
830
833
|
const content2 = html.slice(tagEnd2 + 1, close2.index);
|
|
831
834
|
const c1 = mergedContent.trim();
|
|
832
835
|
const c2 = content2.trim();
|
|
833
836
|
if (c1 && c2) {
|
|
834
837
|
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
835
|
-
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
838
|
+
const separator = lastLine.includes('//') ? '\n;' : (c1.endsWith(';') ? '' : ';');
|
|
836
839
|
mergedContent = c1 + separator + c2;
|
|
837
840
|
} else {
|
|
838
841
|
mergedContent = c1 || c2;
|
|
@@ -1124,8 +1127,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1124
1127
|
// Open elements under `removeEmptyElements`, so an element whose content
|
|
1125
1128
|
// went is removed, too: where its start tag sits in `buffer`, and whether
|
|
1126
1129
|
// anything kept went in since
|
|
1127
|
-
/** @type {Array<{tag: string, index: number, empty: boolean}>} */
|
|
1130
|
+
/** @type {Array<{tag: string, index: number, empty: boolean, omittedStartTag?: {tag: string, text: string} | undefined, omittedEndTag?: {tag: string, text: string} | undefined, impliedEnds?: Array<{tag: string, text: string}> | undefined}>} */
|
|
1128
1131
|
const openElements = [];
|
|
1132
|
+
// End tags not written, for the start tag that comes next (`text` is empty where one can’t be)
|
|
1133
|
+
/** @type {Array<{tag: string, text: string}>} */
|
|
1134
|
+
let impliedEnds = [];
|
|
1135
|
+
let impliedEndsIndex = -1;
|
|
1129
1136
|
// Under `selfCloseForeignElements`, the open `svg` and `math` elements and the integration points
|
|
1130
1137
|
// in them, whose children are HTML, which a slash does not close
|
|
1131
1138
|
/** @type {string[]} */
|
|
@@ -1149,7 +1156,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1149
1156
|
const stackNoTrimWhitespace = [];
|
|
1150
1157
|
/** @type {string[]} */
|
|
1151
1158
|
const stackNoCollapseWhitespace = [];
|
|
1152
|
-
let
|
|
1159
|
+
let preDepth = 0; // Count of `pre` entries in `stackNoTrimWhitespace`
|
|
1153
1160
|
// Where the last element whose whitespace may not be trimmed ended in `buffer`;
|
|
1154
1161
|
// what lies up to there stays as written, also where `canTrimWhitespace` decides
|
|
1155
1162
|
// by attributes the trim walk lacks
|
|
@@ -1263,31 +1270,32 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1263
1270
|
uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
|
|
1264
1271
|
uidAttrExactPattern = new RegExp('^' + uidAttr + '(\\d+)' + uidAttr + '$');
|
|
1265
1272
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1273
|
+
const uidPlaceholderPattern = new RegExp(uidAttr + '\\d+' + uidAttr, 'g');
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* @param {(text: string, ...args: any[]) => any} fn
|
|
1277
|
+
* @param {boolean} isJS
|
|
1278
|
+
* @returns {any}
|
|
1279
|
+
*/
|
|
1280
|
+
const wrapCodeMinifier = (fn, isJS) => (/** @type {string} */ text, /** @type {any[]} */ ...args) => {
|
|
1281
|
+
if (!text.includes(/** @type {string} */ (uidAttr))) return fn(text, ...args);
|
|
1282
|
+
|
|
1283
|
+
const source = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1284
|
+
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1285
|
+
customMarkupInCode.add(+index);
|
|
1286
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1287
|
+
});
|
|
1288
|
+
const result = fn(source, ...args);
|
|
1289
|
+
const finish = (/** @type {string} */ output) => {
|
|
1290
|
+
if (isMinifiedCodeSafe(source, output, uidPlaceholderPattern, isJS)) return output;
|
|
1291
|
+
options.log('Keeping CSS or JavaScript as written, as minifying could break or lose a custom fragment');
|
|
1292
|
+
return source;
|
|
1293
|
+
};
|
|
1294
|
+
return isThenable(result) ? Promise.resolve(result).then(finish) : finish(result);
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
if (options.minifyCSS !== identity) options.minifyCSS = wrapCodeMinifier(options.minifyCSS, false);
|
|
1298
|
+
if (options.minifyJS !== identity) options.minifyJS = wrapCodeMinifier(options.minifyJS, true);
|
|
1291
1299
|
}
|
|
1292
1300
|
|
|
1293
1301
|
const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
|
|
@@ -1502,8 +1510,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1502
1510
|
return index;
|
|
1503
1511
|
}
|
|
1504
1512
|
|
|
1505
|
-
function removeStartTag() {
|
|
1506
|
-
|
|
1513
|
+
function removeStartTag(/** @type {boolean} */ capture = false) {
|
|
1514
|
+
const index = lastStartTagIndex();
|
|
1515
|
+
const text = capture ? buffer.slice(index).join('') : undefined;
|
|
1516
|
+
bufferTruncate(index);
|
|
1517
|
+
return text;
|
|
1507
1518
|
}
|
|
1508
1519
|
|
|
1509
1520
|
// Write the last start tag as self-closing, where it is the last thing written and holds no
|
|
@@ -1548,6 +1559,26 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1548
1559
|
return lowerTag === 'svg' || lowerTag === 'math' || scope === undefined || scope === 'svg' || scope === 'math';
|
|
1549
1560
|
}
|
|
1550
1561
|
|
|
1562
|
+
// Browsers can close a formatting element with others left open inside it;
|
|
1563
|
+
// any other end tag that doesn’t match the top entry of a stack is stray
|
|
1564
|
+
function canCloseBelowTop(/** @type {string} */ tag, /** @type {string} */ lowerTag) {
|
|
1565
|
+
return formattingElements.has((lowerTag || tag).toLowerCase());
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Returns whether `tag` was open
|
|
1569
|
+
function removeClosedTag(/** @type {string[]} */ tags, /** @type {string} */ tag, /** @type {string} */ lowerTag) {
|
|
1570
|
+
if (tags[tags.length - 1] === tag) {
|
|
1571
|
+
tags.pop();
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
const index = canCloseBelowTop(tag, lowerTag) ? tags.lastIndexOf(tag) : -1;
|
|
1575
|
+
if (index === -1) {
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
tags.splice(index, 1);
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1551
1582
|
function markOpenElementFilled() {
|
|
1552
1583
|
// Guarded, as reading index -1 is a slow property lookup, and this runs for every text node
|
|
1553
1584
|
if (openElements.length) {
|
|
@@ -1595,11 +1626,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1595
1626
|
while (index > 0 && !RE_END_TAG.test(buffer[index] ?? '')) {
|
|
1596
1627
|
index--;
|
|
1597
1628
|
}
|
|
1629
|
+
const text = buffer[index] ?? '';
|
|
1598
1630
|
// Text that follows the end tag is kept as is, so drop the tag alone. Whitespace kept
|
|
1599
1631
|
// verbatim—in `pre`, in `textarea`, or wherever `canTrimWhitespace` says so—counts as
|
|
1600
1632
|
// kept text too, no matter how whitespace is collapsed elsewhere.
|
|
1601
1633
|
const noTrim = insideNoTrim || stackNoTrimWhitespace.length > 0;
|
|
1602
|
-
if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(
|
|
1634
|
+
if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(text)) {
|
|
1603
1635
|
buffer.splice(index, 1);
|
|
1604
1636
|
bufferTags?.splice(index, 1);
|
|
1605
1637
|
// Collapsed whitespace can merge, kept whitespace can leave a line of only whitespace
|
|
@@ -1610,9 +1642,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1610
1642
|
removeWhitespaceLine(index);
|
|
1611
1643
|
}
|
|
1612
1644
|
}
|
|
1613
|
-
return;
|
|
1645
|
+
return text;
|
|
1614
1646
|
}
|
|
1615
1647
|
bufferTruncate(index);
|
|
1648
|
+
return text;
|
|
1616
1649
|
}
|
|
1617
1650
|
|
|
1618
1651
|
// Look for trailing whitespace, bypassing inline tags
|
|
@@ -1620,11 +1653,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1620
1653
|
// Nothing to the left of the text is in play here, so only its trailing run can
|
|
1621
1654
|
// change—unless preserved line breaks bring the leading run into play, too
|
|
1622
1655
|
const trailingOnly = !options.preserveLineBreaks;
|
|
1623
|
-
|
|
1656
|
+
let prevTag = '';
|
|
1657
|
+
let prevTagLower = '';
|
|
1658
|
+
for (; index > noTrimEndIndex && canTrimWhitespace(prevTagLower, prevTag, emptyAttrs); index--) {
|
|
1624
1659
|
const str = buffer[index] ?? '';
|
|
1625
1660
|
const endTagName = matchPlainEndTag(str);
|
|
1626
1661
|
if (endTagName !== null) {
|
|
1627
1662
|
prevTag = endTagName;
|
|
1663
|
+
prevTagLower = prevTag.toLowerCase();
|
|
1628
1664
|
continue;
|
|
1629
1665
|
}
|
|
1630
1666
|
// A tag ends the search, and so does text that survives collapsing. Text with no
|
|
@@ -1771,11 +1807,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1771
1807
|
|
|
1772
1808
|
// Whitespace collapsing phase (sync)
|
|
1773
1809
|
function charsCollapse(/** @type {string} */ text) {
|
|
1774
|
-
// Trim a single trailing newline inside `pre
|
|
1775
|
-
//
|
|
1810
|
+
// Trim a single trailing newline inside `pre`, often a template engine artifact (several are
|
|
1811
|
+
// likely intentional formatting); in `textarea`, it is part of the value
|
|
1776
1812
|
if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
1777
1813
|
const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
|
|
1778
|
-
if (
|
|
1814
|
+
if (preDepth > 0 && topTag?.toLowerCase() !== 'textarea') {
|
|
1779
1815
|
// Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
|
|
1780
1816
|
if (textNextTag && textNextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
|
|
1781
1817
|
text = text.replace(RE_LINE_BREAK_BLANKS_END, '');
|
|
@@ -1799,6 +1835,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1799
1835
|
const charsIndex = buffer.length - 2;
|
|
1800
1836
|
buffer[charsIndex] = (buffer[charsIndex] ?? '').replace(/\s+$/, function (trailingSpaces) {
|
|
1801
1837
|
text = trailingSpaces + text;
|
|
1838
|
+
// It moved, and is judged again as part of this text
|
|
1839
|
+
if (currentChars.endsWith(trailingSpaces)) {
|
|
1840
|
+
currentChars = currentChars.slice(0, -trailingSpaces.length);
|
|
1841
|
+
}
|
|
1802
1842
|
return '';
|
|
1803
1843
|
});
|
|
1804
1844
|
}
|
|
@@ -1831,7 +1871,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1831
1871
|
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
1832
1872
|
}
|
|
1833
1873
|
} else if (inlineSets.withinEither.has(textPrevTag)) {
|
|
1834
|
-
text = collapseWhitespace(text, options,
|
|
1874
|
+
text = collapseWhitespace(text, options, RE_ENDS_COLLAPSIBLE.test(currentChars), false, false, stackNoCollapseWhitespace.length > 0);
|
|
1835
1875
|
}
|
|
1836
1876
|
}
|
|
1837
1877
|
if (textPrevTag || textNextTag) {
|
|
@@ -2146,8 +2186,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2146
2186
|
* @param {string | boolean | undefined} hasUnarySlash
|
|
2147
2187
|
* @param {boolean | undefined} optional
|
|
2148
2188
|
* @param {ProcessedOptions | null} restoreOptions - Context a unary tag pushed, or null
|
|
2189
|
+
* @param {ProcessedOptions} optionsTag - Context the element’s own attributes are in
|
|
2149
2190
|
*/
|
|
2150
|
-
function emitStartTag(normalizedAttrs, tag, unary, autoGenerated, fragmentJoinsTag, hasUnarySlash, optional, restoreOptions) {
|
|
2191
|
+
function emitStartTag(normalizedAttrs, tag, unary, autoGenerated, fragmentJoinsTag, hasUnarySlash, optional, restoreOptions, optionsTag) {
|
|
2151
2192
|
// Find the last kept attribute, then emit in order, with no intermediate array
|
|
2152
2193
|
let lastKeptIndex = -1;
|
|
2153
2194
|
for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
|
|
@@ -2169,7 +2210,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2169
2210
|
if (uidAttr && normalizedAttr.name.includes(uidAttr)) {
|
|
2170
2211
|
markTagFragments(normalizedAttr.name);
|
|
2171
2212
|
}
|
|
2172
|
-
tagText += buildAttr(normalizedAttr, hasUnarySlash,
|
|
2213
|
+
tagText += buildAttr(normalizedAttr, tag, hasUnarySlash, optionsTag, i === lastKeptIndex, uidAttr, markers);
|
|
2173
2214
|
}
|
|
2174
2215
|
}
|
|
2175
2216
|
} else {
|
|
@@ -2182,7 +2223,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2182
2223
|
if (uidAttr && normalizedAttr.name.includes(uidAttr)) {
|
|
2183
2224
|
markTagFragments(normalizedAttr.name);
|
|
2184
2225
|
}
|
|
2185
|
-
const builtAttr = buildAttr(normalizedAttr, hasUnarySlash,
|
|
2226
|
+
const builtAttr = buildAttr(normalizedAttr, tag, hasUnarySlash, optionsTag, i === lastKeptIndex, uidAttr, markers);
|
|
2186
2227
|
// One entry with the tag name, so that no line break can come between them
|
|
2187
2228
|
if (i === 0 && fragmentJoinsTag) {
|
|
2188
2229
|
buffer[buffer.length - 1] += builtAttr;
|
|
@@ -2338,12 +2379,21 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2338
2379
|
}
|
|
2339
2380
|
currentTag = tag;
|
|
2340
2381
|
charsPrevTag = tag;
|
|
2341
|
-
if (!inlineTextSet.has(tag)) {
|
|
2342
|
-
|
|
2382
|
+
if (!inlineTextSet.has(lowerTag || tag)) {
|
|
2383
|
+
// Replaced content counts as content for the whitespace after what holds it
|
|
2384
|
+
currentChars = inlineElementsToKeepWhitespace.has(lowerTag || tag) ? '|' : '';
|
|
2343
2385
|
}
|
|
2344
2386
|
hasChars = false;
|
|
2345
2387
|
currentAttrs = attrs;
|
|
2346
2388
|
|
|
2389
|
+
// What this start tag closed, which is left to the end tags if its element is removed
|
|
2390
|
+
const impliedEndsClosed = impliedEndsIndex === buffer.length ? impliedEnds : undefined;
|
|
2391
|
+
|
|
2392
|
+
// What this start tag made optional, for if the element then turns out to be removed
|
|
2393
|
+
/** @type {{tag: string, text: string} | undefined} */
|
|
2394
|
+
let omittedStartTag;
|
|
2395
|
+
/** @type {{tag: string, text: string} | undefined} */
|
|
2396
|
+
let omittedEndTag;
|
|
2347
2397
|
let optional = options.removeOptionalTags;
|
|
2348
2398
|
if (optional) {
|
|
2349
2399
|
const htmlTag = htmlElements.has(tag);
|
|
@@ -2354,7 +2404,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2354
2404
|
// `<colgroup>` may be omitted if first thing inside is `<col>`
|
|
2355
2405
|
// `<tbody>` may be omitted if first thing inside is `<tr>`
|
|
2356
2406
|
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
2357
|
-
removeStartTag();
|
|
2407
|
+
const text = removeStartTag(options.removeEmptyElements);
|
|
2408
|
+
if (text !== undefined) {
|
|
2409
|
+
omittedStartTag = { tag: optionalStartTag, text };
|
|
2410
|
+
}
|
|
2358
2411
|
}
|
|
2359
2412
|
optionalStartTag = '';
|
|
2360
2413
|
// End-tag-followed-by-start-tag omission rules
|
|
@@ -2362,7 +2415,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2362
2415
|
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag) && (tableClosesP || optionalEndTag !== 'p' || tag !== 'table')) {
|
|
2363
2416
|
// Only a start tag that gets written can stand in for the end tag
|
|
2364
2417
|
if (optionalEndTagEmitted && (!autoGenerated || options.includeAutoGeneratedTags)) {
|
|
2365
|
-
removeEndTag();
|
|
2418
|
+
const text = removeEndTag();
|
|
2419
|
+
if (options.removeEmptyElements) {
|
|
2420
|
+
omittedEndTag = { tag: optionalEndTag, text };
|
|
2421
|
+
}
|
|
2366
2422
|
}
|
|
2367
2423
|
// `<colgroup>` cannot be omitted if preceding `</colgroup>` is omitted
|
|
2368
2424
|
// `<tbody>` cannot be omitted if preceding `</tbody>`, `</thead>`, or `</tfoot>` is
|
|
@@ -2376,17 +2432,18 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2376
2432
|
// Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`); verbatim
|
|
2377
2433
|
// whitespace is tracked without `collapseWhitespace` too, as `removeWhitespaceLine` spares it
|
|
2378
2434
|
if (options.collapseWhitespace && !stackNoTrimWhitespace.length) {
|
|
2379
|
-
squashTrailingWhitespace(tag);
|
|
2435
|
+
squashTrailingWhitespace(lowerTag || tag);
|
|
2380
2436
|
}
|
|
2381
|
-
if (options.collapseWhitespace && !unary && inlineElementsTransparent.has(tag)) {
|
|
2437
|
+
if (options.collapseWhitespace && !unary && inlineElementsTransparent.has(lowerTag || tag)) {
|
|
2382
2438
|
transparentDepth++;
|
|
2383
2439
|
}
|
|
2384
2440
|
if (!unary) {
|
|
2385
|
-
|
|
2441
|
+
const tagLower = lowerTag || tag;
|
|
2442
|
+
if (!canTrimWhitespace(tagLower, tagOut, attrs) || stackNoTrimWhitespace.length) {
|
|
2386
2443
|
stackNoTrimWhitespace.push(tag);
|
|
2387
|
-
if (
|
|
2444
|
+
if (tagLower === 'pre') preDepth++;
|
|
2388
2445
|
}
|
|
2389
|
-
if (options.collapseWhitespace && (!canCollapseWhitespace(
|
|
2446
|
+
if (options.collapseWhitespace && (!canCollapseWhitespace(tagLower, tagOut, attrs) || stackNoCollapseWhitespace.length)) {
|
|
2390
2447
|
stackNoCollapseWhitespace.push(tag);
|
|
2391
2448
|
}
|
|
2392
2449
|
}
|
|
@@ -2403,7 +2460,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2403
2460
|
// SVGO reads the whole SVG block as XML, where an element closes itself or not, so
|
|
2404
2461
|
// every unary element in there is written with a slash
|
|
2405
2462
|
const needsXMLSlash = unary && Boolean(options.insideSVG) && Boolean(options.minifySVG);
|
|
2406
|
-
|
|
2463
|
+
// `foreignObject` and `annotation-xml` hold HTML, but are foreign elements themselves
|
|
2464
|
+
const optionsTag = useNameParentForTag ? optionsParent : options;
|
|
2465
|
+
const hasUnarySlash = needsXMLSlash || (unarySlash && optionsTag.keepClosingSlash);
|
|
2407
2466
|
|
|
2408
2467
|
bufferPush(openTag, true);
|
|
2409
2468
|
|
|
@@ -2412,7 +2471,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2412
2471
|
markOpenElementFilled();
|
|
2413
2472
|
} else {
|
|
2414
2473
|
// A start tag not written can’t be removed with its element
|
|
2415
|
-
openElements.push({ tag, index: buffer.length - 1, empty: !autoGenerated || Boolean(options.includeAutoGeneratedTags) });
|
|
2474
|
+
openElements.push({ tag, index: buffer.length - 1, empty: !autoGenerated || Boolean(options.includeAutoGeneratedTags), omittedStartTag, omittedEndTag, impliedEnds: impliedEndsClosed });
|
|
2416
2475
|
}
|
|
2417
2476
|
}
|
|
2418
2477
|
|
|
@@ -2450,10 +2509,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2450
2509
|
const restoreOptions = unary && pushedContext ? optionsParent : null;
|
|
2451
2510
|
if (anyThenable) {
|
|
2452
2511
|
return Promise.all(attrResults).then(resolved => {
|
|
2453
|
-
emitStartTag(resolved, tag, unary, autoGenerated, fragmentJoinsTag, hasUnarySlash, optional, restoreOptions);
|
|
2512
|
+
emitStartTag(resolved, tag, unary, autoGenerated, fragmentJoinsTag, hasUnarySlash, optional, restoreOptions, optionsTag);
|
|
2454
2513
|
});
|
|
2455
2514
|
}
|
|
2456
|
-
emitStartTag(/** @type {Array<{name: string, value: string | undefined, attr: HTMLAttribute} | undefined>} */ (attrResults), tag, unary, autoGenerated, fragmentJoinsTag, hasUnarySlash, optional, restoreOptions);
|
|
2515
|
+
emitStartTag(/** @type {Array<{name: string, value: string | undefined, attr: HTMLAttribute} | undefined>} */ (attrResults), tag, unary, autoGenerated, fragmentJoinsTag, hasUnarySlash, optional, restoreOptions, optionsTag);
|
|
2457
2516
|
return undefined;
|
|
2458
2517
|
},
|
|
2459
2518
|
end: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ autoGenerated, /** @type {string | undefined} */ rest, /** @type {boolean | undefined} */ holdsTable) {
|
|
@@ -2505,21 +2564,19 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2505
2564
|
const insideNoTrim = stackNoTrimWhitespace.length > 0;
|
|
2506
2565
|
let closesNoTrim = false;
|
|
2507
2566
|
if (stackNoTrimWhitespace.length) {
|
|
2508
|
-
if (
|
|
2509
|
-
|
|
2510
|
-
|
|
2567
|
+
if (removeClosedTag(stackNoTrimWhitespace, tag, lowerTag)) {
|
|
2568
|
+
const tagLower = lowerTag || tag;
|
|
2569
|
+
if (tagLower === 'pre') preDepth--;
|
|
2511
2570
|
closesNoTrim = !stackNoTrimWhitespace.length;
|
|
2512
2571
|
}
|
|
2513
2572
|
} else if (options.collapseWhitespace) {
|
|
2514
|
-
squashTrailingWhitespace('/' + tag);
|
|
2573
|
+
squashTrailingWhitespace('/' + (lowerTag || tag));
|
|
2515
2574
|
}
|
|
2516
|
-
if (transparentDepth && inlineElementsTransparent.has(tag)) {
|
|
2575
|
+
if (transparentDepth && inlineElementsTransparent.has(lowerTag || tag)) {
|
|
2517
2576
|
transparentDepth--;
|
|
2518
2577
|
}
|
|
2519
2578
|
let closesNoCollapse = false;
|
|
2520
|
-
if (stackNoCollapseWhitespace.length &&
|
|
2521
|
-
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
2522
|
-
stackNoCollapseWhitespace.pop();
|
|
2579
|
+
if (stackNoCollapseWhitespace.length && removeClosedTag(stackNoCollapseWhitespace, tag, lowerTag)) {
|
|
2523
2580
|
closesNoCollapse = !stackNoCollapseWhitespace.length;
|
|
2524
2581
|
}
|
|
2525
2582
|
|
|
@@ -2530,6 +2587,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2530
2587
|
const skipsEndTag = autoGenerated && (!options.includeAutoGeneratedTags || Boolean(holdsTable));
|
|
2531
2588
|
impliedStartTag = '';
|
|
2532
2589
|
|
|
2590
|
+
let startTagRemoved = false;
|
|
2533
2591
|
let isElementEmpty = false;
|
|
2534
2592
|
if (tag === currentTag) {
|
|
2535
2593
|
currentTag = '';
|
|
@@ -2537,7 +2595,19 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2537
2595
|
}
|
|
2538
2596
|
selfCloses &&= isElementEmpty;
|
|
2539
2597
|
// A stray end tag has no open element of its own
|
|
2540
|
-
|
|
2598
|
+
let openElement;
|
|
2599
|
+
if (options.removeEmptyElements && openElements.length) {
|
|
2600
|
+
if (openElements[openElements.length - 1]?.tag === tag) {
|
|
2601
|
+
openElement = openElements.pop();
|
|
2602
|
+
} else if (canCloseBelowTop(tag, lowerTag)) {
|
|
2603
|
+
const index = openElements.findLastIndex(entry => entry.tag === tag);
|
|
2604
|
+
if (index !== -1) {
|
|
2605
|
+
openElement = openElements.splice(index, 1)[0];
|
|
2606
|
+
// Closed around what is open inside it, so that holds this end tag now
|
|
2607
|
+
currentTag = '';
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2541
2611
|
// Emptied by removing what it held, as long as its start tag is still the last one written
|
|
2542
2612
|
const isElementEmptied = !isElementEmpty && Boolean(openElement?.empty) && lastStartTagIndex() === openElement?.index;
|
|
2543
2613
|
|
|
@@ -2545,6 +2615,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2545
2615
|
// `<html>`, `<head>`, or `<body>` may be omitted if the element is empty
|
|
2546
2616
|
if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
|
|
2547
2617
|
removeStartTag();
|
|
2618
|
+
startTagRemoved = true;
|
|
2548
2619
|
}
|
|
2549
2620
|
optionalStartTag = '';
|
|
2550
2621
|
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
@@ -2563,7 +2634,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2563
2634
|
let isElementRemoved = false;
|
|
2564
2635
|
// A formatting element closed by a block is reopened after it by browsers, so it has to stay
|
|
2565
2636
|
if (options.removeEmptyElements && (isElementEmpty || isElementEmptied) && !options.insideForeignContent && canRemoveElement(tag, attrs) &&
|
|
2566
|
-
!(autoGenerated && formattingElements.has(tag))) {
|
|
2637
|
+
!(autoGenerated && formattingElements.has(tag)) && !openElement?.impliedEnds?.some(implied => !implied.text)) {
|
|
2567
2638
|
let preserve = false;
|
|
2568
2639
|
if (removeEmptyElementsExcept.length) {
|
|
2569
2640
|
const normalizedAttrs = attrs.map((/** @type {HTMLAttribute} */ attr) => ({ ...attr, name: resolveName(attr.name) }));
|
|
@@ -2571,11 +2642,37 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2571
2642
|
}
|
|
2572
2643
|
|
|
2573
2644
|
if (!preserve) {
|
|
2574
|
-
|
|
2645
|
+
// Once it is gone, the last start tag is another element’s
|
|
2646
|
+
if (!startTagRemoved) {
|
|
2647
|
+
removeStartTag();
|
|
2648
|
+
}
|
|
2575
2649
|
isElementRemoved = true;
|
|
2576
2650
|
optionalStartTag = '';
|
|
2577
2651
|
optionalEndTag = '';
|
|
2578
2652
|
optionalEndTagEmitted = false;
|
|
2653
|
+
// What it made optional is up to what comes next now
|
|
2654
|
+
if (openElement?.omittedEndTag) {
|
|
2655
|
+
bufferPush(openElement.omittedEndTag.text, true);
|
|
2656
|
+
optionalEndTag = openElement.omittedEndTag.tag;
|
|
2657
|
+
optionalEndTagEmitted = true;
|
|
2658
|
+
}
|
|
2659
|
+
if (openElement?.omittedStartTag) {
|
|
2660
|
+
bufferPush(openElement.omittedStartTag.text, true);
|
|
2661
|
+
optionalStartTag = openElement.omittedStartTag.tag;
|
|
2662
|
+
}
|
|
2663
|
+
if (openElement?.impliedEnds) {
|
|
2664
|
+
// Browsers reopen formatting elements after what closed them, which their end tags
|
|
2665
|
+
// would prevent and whitespace following an omitted end tag would change
|
|
2666
|
+
const written = openElement.impliedEnds.filter(implied => !formattingElements.has(implied.tag));
|
|
2667
|
+
for (const implied of written) {
|
|
2668
|
+
bufferPush(implied.text, true);
|
|
2669
|
+
}
|
|
2670
|
+
const last = written[written.length - 1];
|
|
2671
|
+
if (last && written.length === openElement.impliedEnds.length && optionalEndTags.has(last.tag)) {
|
|
2672
|
+
optionalEndTag = last.tag;
|
|
2673
|
+
optionalEndTagEmitted = true;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2579
2676
|
} else {
|
|
2580
2677
|
// Preserve the element—add closing tag
|
|
2581
2678
|
if (skipsEndTag) {
|
|
@@ -2584,9 +2681,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2584
2681
|
bufferPush(endTagText, true);
|
|
2585
2682
|
}
|
|
2586
2683
|
charsPrevTag = '/' + tag;
|
|
2587
|
-
if (!inlineElements.has(tag)) {
|
|
2684
|
+
if (!inlineElements.has(lowerTag || tag)) {
|
|
2588
2685
|
currentChars = '';
|
|
2589
|
-
} else if (isElementEmpty) {
|
|
2686
|
+
} else if (isElementEmpty || !inlineTextSet.has(lowerTag || tag)) {
|
|
2590
2687
|
currentChars += '|';
|
|
2591
2688
|
}
|
|
2592
2689
|
}
|
|
@@ -2597,13 +2694,22 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2597
2694
|
bufferPush(endTagText, true);
|
|
2598
2695
|
}
|
|
2599
2696
|
charsPrevTag = '/' + tag;
|
|
2600
|
-
if (!inlineElements.has(tag)) {
|
|
2697
|
+
if (!inlineElements.has(lowerTag || tag)) {
|
|
2601
2698
|
currentChars = '';
|
|
2602
|
-
} else if (isElementEmpty) {
|
|
2699
|
+
} else if (isElementEmpty || !inlineTextSet.has(lowerTag || tag)) {
|
|
2700
|
+
// What is not text counts as content for the whitespace after it
|
|
2603
2701
|
currentChars += '|';
|
|
2604
2702
|
}
|
|
2605
2703
|
}
|
|
2606
2704
|
|
|
2705
|
+
if (skipsEndTag && !isElementRemoved && options.removeEmptyElements) {
|
|
2706
|
+
if (impliedEndsIndex !== buffer.length) {
|
|
2707
|
+
impliedEnds = [];
|
|
2708
|
+
impliedEndsIndex = buffer.length;
|
|
2709
|
+
}
|
|
2710
|
+
impliedEnds.push({ tag, text: holdsTable ? '' : endTagText });
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2607
2713
|
if (closesNoTrim && !isElementRemoved) {
|
|
2608
2714
|
noTrimEndIndex = buffer.length - 1;
|
|
2609
2715
|
}
|
|
@@ -2701,12 +2807,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2701
2807
|
strayEnd: function () {
|
|
2702
2808
|
removed = true;
|
|
2703
2809
|
},
|
|
2704
|
-
doctype: function (/** @type {string} */ doctype, /** @type {boolean} */ noQuirksMode) {
|
|
2810
|
+
doctype: function (/** @type {string} */ doctype, /** @type {boolean} */ noQuirksMode, /** @type {boolean} */ isIgnored) {
|
|
2705
2811
|
tableClosesP = noQuirksMode;
|
|
2812
|
+
if (isIgnored) {
|
|
2813
|
+
removed = true;
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2706
2816
|
bufferPush(options.useShortDoctype
|
|
2707
2817
|
? '<!doctype' +
|
|
2708
2818
|
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
2709
|
-
: collapseWhitespaceAll(
|
|
2819
|
+
: doctype.replace(RE_DOCTYPE_SEGMENT, (segment, quoted) => quoted ?? collapseWhitespaceAll(segment)), false);
|
|
2710
2820
|
}
|
|
2711
2821
|
});
|
|
2712
2822
|
|