html-minifier 5.0.2 → 5.0.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/README.md +8 -5
- package/cli.js +89 -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 +19 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +6 -1
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +3 -1
- package/dist/types/lib/elements.d.ts.map +1 -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/dist/types/lib/unused-css.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +8 -1
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/htmlminifier.js +220 -69
- package/src/htmlparser.js +333 -53
- package/src/lib/attributes.js +108 -18
- package/src/lib/constants.js +21 -5
- package/src/lib/elements.js +15 -3
- package/src/lib/fragments.js +12 -3
- package/src/lib/options.js +5 -4
- package/src/lib/unused-css.js +85 -68
- package/src/lib/utils.js +19 -0
- package/src/lib/whitespace.js +2 -4
package/src/htmlminifier.js
CHANGED
|
@@ -2,7 +2,7 @@ import { HTMLParser } from './htmlparser.js';
|
|
|
2
2
|
import TokenChain from './tokenchain.js';
|
|
3
3
|
import { presets, getPreset, getPresetNames } from './presets.js';
|
|
4
4
|
|
|
5
|
-
import { LRU, decodeForeignText, describeDependencyFailure, escapeXML, findTagEnd, identity, isThenable, lowercase, uniqueId } from './lib/utils.js';
|
|
5
|
+
import { LRU, decodeForeignText, describeDependencyFailure, escapeXML, findTagEnd, identity, isThenable, lowercase, replaceNUL, uniqueId } from './lib/utils.js';
|
|
6
6
|
import { collectUsage } from './lib/unused-css.js';
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -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,
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
compactElements,
|
|
32
35
|
looseElements,
|
|
33
36
|
trailingElements,
|
|
37
|
+
specialOptionalEndTags,
|
|
34
38
|
pClosingParents,
|
|
35
39
|
namesForeignMixedCase,
|
|
36
40
|
integrationPointsForeign,
|
|
@@ -665,6 +669,7 @@ const cacheStatsOrEmpty = (cache) => cache ? cache.stats() : { gets: 0, hits: 0,
|
|
|
665
669
|
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
666
670
|
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
667
671
|
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
672
|
+
const RE_SCRIPT_START_TEXT = /<script[\t\n\f\r />]/i; // As script text, after `<!--`
|
|
668
673
|
const RE_MATH_TAG = /<(\/?)math(?=[\s/>])/gi;
|
|
669
674
|
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
670
675
|
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
@@ -826,14 +831,20 @@ function mergeConsecutiveScripts(html, hasMathML = false) {
|
|
|
826
831
|
}
|
|
827
832
|
if (boolConflict || a1.nonce !== a2.nonce || (!mergedAny && insideMath(m1.index))) break;
|
|
828
833
|
|
|
829
|
-
// Join with a semicolon, or with a newline where the first ends in a `//` comment
|
|
830
834
|
const content2 = html.slice(tagEnd2 + 1, close2.index);
|
|
835
|
+
// After `<!--`, `<script` opens a state `</script>` doesn’t end,
|
|
836
|
+
// which would swallow what follows the merged script
|
|
837
|
+
if (mergedContent.indexOf('<!--') !== -1 && RE_SCRIPT_START_TEXT.test(content2)) break;
|
|
838
|
+
|
|
839
|
+
// Join with a semicolon, after a newline where the first may end in a `//`, `<!--`,
|
|
840
|
+
// or `-->` comment, all of which browsers read as running to the end of the line,
|
|
841
|
+
// and before one where the second starts with `-->`, a comment only there
|
|
831
842
|
const c1 = mergedContent.trim();
|
|
832
843
|
const c2 = content2.trim();
|
|
833
844
|
if (c1 && c2) {
|
|
834
845
|
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
835
|
-
const separator = lastLine
|
|
836
|
-
mergedContent = c1 + separator + c2;
|
|
846
|
+
const separator = /\/\/|<!--|-->/.test(lastLine) ? '\n;' : (c1.endsWith(';') ? '' : ';');
|
|
847
|
+
mergedContent = c1 + separator + (c2.startsWith('-->') ? '\n' : '') + c2;
|
|
837
848
|
} else {
|
|
838
849
|
mergedContent = c1 || c2;
|
|
839
850
|
}
|
|
@@ -953,6 +964,11 @@ async function createSortFns(value, options, uidIgnore, ignoredMarkupChunks) {
|
|
|
953
964
|
removeTagWhitespace: false,
|
|
954
965
|
decodeEntities: false,
|
|
955
966
|
processScripts: false,
|
|
967
|
+
minifyCSS: identity,
|
|
968
|
+
minifyJS: identity,
|
|
969
|
+
minifyJSON: false,
|
|
970
|
+
minifySVG: null,
|
|
971
|
+
minifyURLs: identity,
|
|
956
972
|
// `ignoreCustomFragments` stays, and template syntax may not parse cleanly
|
|
957
973
|
continueOnParseError: true,
|
|
958
974
|
log: identity
|
|
@@ -1112,8 +1128,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1112
1128
|
/** @type {HTMLAttribute[]} */
|
|
1113
1129
|
const emptyAttrs = [];
|
|
1114
1130
|
|
|
1131
|
+
// While parsing, entries already in it change through `bufferSet`, `bufferRemove`,
|
|
1132
|
+
// or `bufferTruncate`, so that `trimSettledIndex` stays true
|
|
1115
1133
|
/** @type {string[]} */
|
|
1116
1134
|
const buffer = [];
|
|
1135
|
+
// Where the last trim walk began that came to text it couldn’t trim, past end tags alone: A
|
|
1136
|
+
// later walk that gets there finds the same, until an entry up to there changes
|
|
1137
|
+
let trimSettledIndex = -1;
|
|
1117
1138
|
// Parallel to `buffer`: whether the entry is a tag—the only place lines may break
|
|
1118
1139
|
const bufferTags = options.maxLineLength ? /** @type {boolean[]} */ ([]) : null;
|
|
1119
1140
|
/** @type {string} */
|
|
@@ -1124,8 +1145,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1124
1145
|
// Open elements under `removeEmptyElements`, so an element whose content
|
|
1125
1146
|
// went is removed, too: where its start tag sits in `buffer`, and whether
|
|
1126
1147
|
// anything kept went in since
|
|
1127
|
-
/** @type {Array<{tag: string, index: number, empty: boolean}>} */
|
|
1148
|
+
/** @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
1149
|
const openElements = [];
|
|
1150
|
+
// End tags not written, for the start tag that comes next (`text` is empty where one can’t be)
|
|
1151
|
+
/** @type {Array<{tag: string, text: string}>} */
|
|
1152
|
+
let impliedEnds = [];
|
|
1153
|
+
let impliedEndsIndex = -1;
|
|
1129
1154
|
// Under `selfCloseForeignElements`, the open `svg` and `math` elements and the integration points
|
|
1130
1155
|
// in them, whose children are HTML, which a slash does not close
|
|
1131
1156
|
/** @type {string[]} */
|
|
@@ -1149,7 +1174,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1149
1174
|
const stackNoTrimWhitespace = [];
|
|
1150
1175
|
/** @type {string[]} */
|
|
1151
1176
|
const stackNoCollapseWhitespace = [];
|
|
1152
|
-
let
|
|
1177
|
+
let preDepth = 0; // Count of `pre` entries in `stackNoTrimWhitespace`
|
|
1153
1178
|
// Where the last element whose whitespace may not be trimmed ended in `buffer`;
|
|
1154
1179
|
// what lies up to there stays as written, also where `canTrimWhitespace` decides
|
|
1155
1180
|
// by attributes the trim walk lacks
|
|
@@ -1411,8 +1436,21 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1411
1436
|
if (entry) removed = false;
|
|
1412
1437
|
}
|
|
1413
1438
|
|
|
1439
|
+
function bufferSet(/** @type {number} */ index, /** @type {string} */ entry) {
|
|
1440
|
+
buffer[index] = entry;
|
|
1441
|
+
if (index <= trimSettledIndex) trimSettledIndex = -1;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Entries after `index` move down by one, so a settled walk from there on is no longer one
|
|
1445
|
+
function bufferRemove(/** @type {number} */ index) {
|
|
1446
|
+
buffer.splice(index, 1);
|
|
1447
|
+
bufferTags?.splice(index, 1);
|
|
1448
|
+
if (index <= trimSettledIndex) trimSettledIndex = -1;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1414
1451
|
function bufferTruncate(/** @type {number} */ length) {
|
|
1415
1452
|
buffer.length = Math.max(0, length);
|
|
1453
|
+
if (length <= trimSettledIndex) trimSettledIndex = -1;
|
|
1416
1454
|
if (bufferTags) bufferTags.length = buffer.length;
|
|
1417
1455
|
if (transparentGapIndex >= buffer.length) transparentGapIndex = -1;
|
|
1418
1456
|
if (noTrimEndIndex >= buffer.length) noTrimEndIndex = -1;
|
|
@@ -1457,15 +1495,15 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1457
1495
|
return;
|
|
1458
1496
|
}
|
|
1459
1497
|
for (let i = before + 1; i <= first; i++) {
|
|
1460
|
-
|
|
1498
|
+
bufferSet(i, '');
|
|
1461
1499
|
}
|
|
1462
1500
|
// The line break goes on the side of the text before the removal, so that the text after
|
|
1463
1501
|
// it keeps its own for a removal that may follow
|
|
1464
1502
|
if (before < 0) {
|
|
1465
|
-
|
|
1503
|
+
bufferSet(after, next.replace(RE_BLANKS_LINE_BREAK_START, ''));
|
|
1466
1504
|
} else {
|
|
1467
|
-
|
|
1468
|
-
|
|
1505
|
+
bufferSet(before, prev.replace(RE_LINE_BREAK_BLANKS_END, ''));
|
|
1506
|
+
bufferSet(after, next.replace(RE_BLANKS_START, ''));
|
|
1469
1507
|
}
|
|
1470
1508
|
}
|
|
1471
1509
|
|
|
@@ -1480,18 +1518,18 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1480
1518
|
if (!textVerbatim) {
|
|
1481
1519
|
let index = buffer.length - 1;
|
|
1482
1520
|
while (index >= 0 && (buffer[index] === '' || RE_WS_ONLY.test(buffer[index] ?? ''))) {
|
|
1483
|
-
|
|
1521
|
+
bufferSet(index--, '');
|
|
1484
1522
|
}
|
|
1485
1523
|
if (index >= 0) {
|
|
1486
|
-
|
|
1524
|
+
bufferSet(index, (buffer[index] ?? '').replace(RE_WS_END, ''));
|
|
1487
1525
|
}
|
|
1488
1526
|
}
|
|
1489
1527
|
let index = 0;
|
|
1490
1528
|
while (index < buffer.length && (buffer[index] === '' || RE_WS_ONLY.test(buffer[index] ?? ''))) {
|
|
1491
|
-
|
|
1529
|
+
bufferSet(index++, '');
|
|
1492
1530
|
}
|
|
1493
1531
|
if (index < buffer.length) {
|
|
1494
|
-
|
|
1532
|
+
bufferSet(index, (buffer[index] ?? '').replace(RE_WS_START, ''));
|
|
1495
1533
|
}
|
|
1496
1534
|
}
|
|
1497
1535
|
|
|
@@ -1503,8 +1541,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1503
1541
|
return index;
|
|
1504
1542
|
}
|
|
1505
1543
|
|
|
1506
|
-
function removeStartTag() {
|
|
1507
|
-
|
|
1544
|
+
function removeStartTag(/** @type {boolean} */ capture = false) {
|
|
1545
|
+
const index = lastStartTagIndex();
|
|
1546
|
+
const text = capture ? buffer.slice(index).join('') : undefined;
|
|
1547
|
+
bufferTruncate(index);
|
|
1548
|
+
return text;
|
|
1508
1549
|
}
|
|
1509
1550
|
|
|
1510
1551
|
// Write the last start tag as self-closing, where it is the last thing written and holds no
|
|
@@ -1527,7 +1568,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1527
1568
|
}
|
|
1528
1569
|
const tagOpen = tagText.slice(0, -1);
|
|
1529
1570
|
// A slash right after an unquoted value would belong to the value
|
|
1530
|
-
|
|
1571
|
+
bufferSet(last, tagOpen + (RE_UNQUOTED_VALUE_END.test(tagOpen) ? ' />' : '/>'));
|
|
1531
1572
|
return true;
|
|
1532
1573
|
}
|
|
1533
1574
|
|
|
@@ -1549,6 +1590,26 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1549
1590
|
return lowerTag === 'svg' || lowerTag === 'math' || scope === undefined || scope === 'svg' || scope === 'math';
|
|
1550
1591
|
}
|
|
1551
1592
|
|
|
1593
|
+
// Browsers can close a formatting element with others left open inside it;
|
|
1594
|
+
// any other end tag that doesn’t match the top entry of a stack is stray
|
|
1595
|
+
function canCloseBelowTop(/** @type {string} */ tag, /** @type {string} */ lowerTag) {
|
|
1596
|
+
return formattingElements.has((lowerTag || tag).toLowerCase());
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Returns whether `tag` was open
|
|
1600
|
+
function removeClosedTag(/** @type {string[]} */ tags, /** @type {string} */ tag, /** @type {string} */ lowerTag) {
|
|
1601
|
+
if (tags[tags.length - 1] === tag) {
|
|
1602
|
+
tags.pop();
|
|
1603
|
+
return true;
|
|
1604
|
+
}
|
|
1605
|
+
const index = canCloseBelowTop(tag, lowerTag) ? tags.lastIndexOf(tag) : -1;
|
|
1606
|
+
if (index === -1) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
tags.splice(index, 1);
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1552
1613
|
function markOpenElementFilled() {
|
|
1553
1614
|
// Guarded, as reading index -1 is a slow property lookup, and this runs for every text node
|
|
1554
1615
|
if (openElements.length) {
|
|
@@ -1584,9 +1645,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1584
1645
|
// A line break the run already carries outranks the whitespace ahead of it; trimming
|
|
1585
1646
|
// the other side takes ASCII whitespace only, so a no-break space stays as is
|
|
1586
1647
|
if (options.preserveLineBreaks && /[\n\r]/.test((next.match(RE_WS_START) ?? [''])[0])) {
|
|
1587
|
-
|
|
1648
|
+
bufferSet(before, prev.replace(RE_WS_END, ''));
|
|
1588
1649
|
} else {
|
|
1589
|
-
|
|
1650
|
+
bufferSet(after, next.replace(RE_WS_START, ''));
|
|
1590
1651
|
}
|
|
1591
1652
|
}
|
|
1592
1653
|
|
|
@@ -1596,13 +1657,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1596
1657
|
while (index > 0 && !RE_END_TAG.test(buffer[index] ?? '')) {
|
|
1597
1658
|
index--;
|
|
1598
1659
|
}
|
|
1660
|
+
const text = buffer[index] ?? '';
|
|
1599
1661
|
// Text that follows the end tag is kept as is, so drop the tag alone. Whitespace kept
|
|
1600
1662
|
// verbatim—in `pre`, in `textarea`, or wherever `canTrimWhitespace` says so—counts as
|
|
1601
1663
|
// kept text too, no matter how whitespace is collapsed elsewhere.
|
|
1602
1664
|
const noTrim = insideNoTrim || stackNoTrimWhitespace.length > 0;
|
|
1603
|
-
if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(
|
|
1604
|
-
|
|
1605
|
-
bufferTags?.splice(index, 1);
|
|
1665
|
+
if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(text)) {
|
|
1666
|
+
bufferRemove(index);
|
|
1606
1667
|
// Collapsed whitespace can merge, kept whitespace can leave a line of only whitespace
|
|
1607
1668
|
if (!noTrim) {
|
|
1608
1669
|
if (options.collapseWhitespace) {
|
|
@@ -1611,9 +1672,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1611
1672
|
removeWhitespaceLine(index);
|
|
1612
1673
|
}
|
|
1613
1674
|
}
|
|
1614
|
-
return;
|
|
1675
|
+
return text;
|
|
1615
1676
|
}
|
|
1616
1677
|
bufferTruncate(index);
|
|
1678
|
+
return text;
|
|
1617
1679
|
}
|
|
1618
1680
|
|
|
1619
1681
|
// Look for trailing whitespace, bypassing inline tags
|
|
@@ -1621,10 +1683,18 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1621
1683
|
// Nothing to the left of the text is in play here, so only its trailing run can
|
|
1622
1684
|
// change—unless preserved line breaks bring the leading run into play, too
|
|
1623
1685
|
const trailingOnly = !options.preserveLineBreaks;
|
|
1686
|
+
const indexStart = index;
|
|
1624
1687
|
let prevTag = '';
|
|
1625
1688
|
let prevTagLower = '';
|
|
1626
1689
|
for (; index > noTrimEndIndex && canTrimWhitespace(prevTagLower, prevTag, emptyAttrs); index--) {
|
|
1690
|
+
if (index === trimSettledIndex) {
|
|
1691
|
+
trimSettledIndex = indexStart;
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1627
1694
|
const str = buffer[index] ?? '';
|
|
1695
|
+
if (!str) {
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1628
1698
|
const endTagName = matchPlainEndTag(str);
|
|
1629
1699
|
if (endTagName !== null) {
|
|
1630
1700
|
prevTag = endTagName;
|
|
@@ -1633,14 +1703,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1633
1703
|
}
|
|
1634
1704
|
// A tag ends the search, and so does text that survives collapsing. Text with no
|
|
1635
1705
|
// trailing run comes back from the call unchanged, so it can end the search on
|
|
1636
|
-
// the spot
|
|
1637
|
-
if (str.charCodeAt(str.length - 1) === 62 /* > */) {
|
|
1638
|
-
|
|
1639
|
-
}
|
|
1640
|
-
if (trailingOnly && str && !endsWithWhitespace(str)) {
|
|
1706
|
+
// the spot, and so can every later search that gets as far as this one began
|
|
1707
|
+
if (str.charCodeAt(str.length - 1) === 62 /* > */ || (trailingOnly && !endsWithWhitespace(str))) {
|
|
1708
|
+
trimSettledIndex = indexStart;
|
|
1641
1709
|
break;
|
|
1642
1710
|
}
|
|
1643
|
-
|
|
1711
|
+
bufferSet(index, collapseWhitespaceSmart(str, '', nextTag, emptyAttrs, emptyAttrs, options, inlineSets, !stackNoCollapseWhitespace.length && index > noCollapseEndIndex));
|
|
1644
1712
|
if (buffer[index]) {
|
|
1645
1713
|
break;
|
|
1646
1714
|
}
|
|
@@ -1696,7 +1764,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1696
1764
|
// Keep or trim the whitespace pending before or between elements that don’t render
|
|
1697
1765
|
function settleTransparentGap(/** @type {boolean} */ keep) {
|
|
1698
1766
|
if (!keep) {
|
|
1699
|
-
|
|
1767
|
+
bufferSet(transparentGapIndex, collapseWhitespace(buffer[transparentGapIndex] ?? '', options, false, true));
|
|
1700
1768
|
}
|
|
1701
1769
|
transparentGapIndex = -1;
|
|
1702
1770
|
}
|
|
@@ -1775,11 +1843,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1775
1843
|
|
|
1776
1844
|
// Whitespace collapsing phase (sync)
|
|
1777
1845
|
function charsCollapse(/** @type {string} */ text) {
|
|
1778
|
-
// Trim a single trailing newline inside `pre
|
|
1779
|
-
//
|
|
1846
|
+
// Trim a single trailing newline inside `pre`, often a template engine artifact (several are
|
|
1847
|
+
// likely intentional formatting); in `textarea`, it is part of the value
|
|
1780
1848
|
if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
1781
1849
|
const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
|
|
1782
|
-
if (
|
|
1850
|
+
if (preDepth > 0 && topTag?.toLowerCase() !== 'textarea') {
|
|
1783
1851
|
// Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
|
|
1784
1852
|
if (textNextTag && textNextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
|
|
1785
1853
|
text = text.replace(RE_LINE_BREAK_BLANKS_END, '');
|
|
@@ -1801,10 +1869,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1801
1869
|
}
|
|
1802
1870
|
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
1803
1871
|
const charsIndex = buffer.length - 2;
|
|
1804
|
-
|
|
1872
|
+
bufferSet(charsIndex, (buffer[charsIndex] ?? '').replace(/\s+$/, function (trailingSpaces) {
|
|
1805
1873
|
text = trailingSpaces + text;
|
|
1874
|
+
// It moved, and is judged again as part of this text
|
|
1875
|
+
if (currentChars.endsWith(trailingSpaces)) {
|
|
1876
|
+
currentChars = currentChars.slice(0, -trailingSpaces.length);
|
|
1877
|
+
}
|
|
1806
1878
|
return '';
|
|
1807
|
-
});
|
|
1879
|
+
}));
|
|
1808
1880
|
}
|
|
1809
1881
|
} else if (uidIgnorePlaceholderPattern && textNextTag !== 'comment') {
|
|
1810
1882
|
// UID placeholder followed by a real element—derive the effective `prevTag` from the
|
|
@@ -1835,7 +1907,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1835
1907
|
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
1836
1908
|
}
|
|
1837
1909
|
} else if (inlineSets.withinEither.has(textPrevTag)) {
|
|
1838
|
-
text = collapseWhitespace(text, options,
|
|
1910
|
+
text = collapseWhitespace(text, options, RE_ENDS_COLLAPSIBLE.test(currentChars), false, false, stackNoCollapseWhitespace.length > 0);
|
|
1839
1911
|
}
|
|
1840
1912
|
}
|
|
1841
1913
|
if (textPrevTag || textNextTag) {
|
|
@@ -1938,9 +2010,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1938
2010
|
}
|
|
1939
2011
|
if (text && options.insideSVG && options.minifySVG) {
|
|
1940
2012
|
// SVGO reads XML, so text is escaped for it, while raw text, which XML cannot read as
|
|
1941
|
-
// HTML does (as in `foreignObject`), passes SVGO as a placeholder
|
|
2013
|
+
// HTML does (as in `foreignObject`), passes SVGO as a placeholder—as does HTML text
|
|
2014
|
+
// with whitespace at an end, which SVGO trims (`trim()`, so no-break spaces, too)
|
|
2015
|
+
// outside SVG text elements
|
|
1942
2016
|
if (!rawText) {
|
|
1943
2017
|
text = escapeXML(text);
|
|
2018
|
+
if (!options.insideForeignContent && /^\s|\s$/.test(text)) {
|
|
2019
|
+
uidSVGRawText ||= uniqueId(value);
|
|
2020
|
+
svgRawTexts.push(text);
|
|
2021
|
+
text = '/*!' + uidSVGRawText + (svgRawTexts.length - 1) + '*/';
|
|
2022
|
+
}
|
|
1944
2023
|
} else if (text.indexOf('&') !== -1 || text.indexOf('<') !== -1 || text.indexOf(']]>') !== -1) {
|
|
1945
2024
|
uidSVGRawText ||= uniqueId(value);
|
|
1946
2025
|
svgRawTexts.push(text);
|
|
@@ -2024,9 +2103,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2024
2103
|
const firstTag = match ? RE_FIRST_HTML_TAG.exec(ignoredMarkupChunks[+(match[1] ?? 0)] ?? '')?.[1] : undefined;
|
|
2025
2104
|
if (firstTag && inlineElementsTransparent.has(resolveName(firstTag)) && inlineElementsTransparentAfter.has(charsPrevTag)) {
|
|
2026
2105
|
// Emptied first, as `precedesTransparentRun` stops at text
|
|
2027
|
-
buffer
|
|
2106
|
+
bufferSet(buffer.length - 1, '');
|
|
2028
2107
|
if (transparentGapIndex === buffer.length - 1) transparentGapIndex = -1;
|
|
2029
|
-
buffer
|
|
2108
|
+
bufferSet(buffer.length - 1, options.conservativeCollapse || precedesTransparentRun(true) ? ' ' : '');
|
|
2030
2109
|
}
|
|
2031
2110
|
}
|
|
2032
2111
|
// What an `htmlmin:ignore` block holds is unknown, so whitespace pending before it stays
|
|
@@ -2097,7 +2176,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2097
2176
|
}
|
|
2098
2177
|
}
|
|
2099
2178
|
|
|
2100
|
-
buffer
|
|
2179
|
+
bufferSet(buffer.length - 1, collapsedText);
|
|
2101
2180
|
}
|
|
2102
2181
|
}
|
|
2103
2182
|
}
|
|
@@ -2125,7 +2204,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2125
2204
|
const trims = Boolean(options.collapseWhitespace) &&
|
|
2126
2205
|
!options.conservativeCollapse && !options.preserveLineBreaks;
|
|
2127
2206
|
for (const { code, isModule } of bodies) {
|
|
2128
|
-
const body = trims ? code.trim() : code;
|
|
2207
|
+
const body = replaceNUL(trims ? code.trim() : code);
|
|
2129
2208
|
const key = (isModule ? 'm|' : '|') + body;
|
|
2130
2209
|
if (jsPrewarmed.has(key)) {
|
|
2131
2210
|
continue;
|
|
@@ -2190,7 +2269,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2190
2269
|
const builtAttr = buildAttr(normalizedAttr, tag, hasUnarySlash, optionsTag, i === lastKeptIndex, uidAttr, markers);
|
|
2191
2270
|
// One entry with the tag name, so that no line break can come between them
|
|
2192
2271
|
if (i === 0 && fragmentJoinsTag) {
|
|
2193
|
-
buffer[buffer.length - 1]
|
|
2272
|
+
bufferSet(buffer.length - 1, (buffer[buffer.length - 1] ?? '') + builtAttr);
|
|
2194
2273
|
} else {
|
|
2195
2274
|
bufferPush(builtAttr, true);
|
|
2196
2275
|
}
|
|
@@ -2202,7 +2281,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2202
2281
|
optionalStartTag = tag;
|
|
2203
2282
|
}
|
|
2204
2283
|
|
|
2205
|
-
buffer[buffer.length - 1]
|
|
2284
|
+
bufferSet(buffer.length - 1, (buffer[buffer.length - 1] ?? '') + tagText + (hasUnarySlash ? '/' : '') + '>');
|
|
2206
2285
|
|
|
2207
2286
|
impliedStartTag = autoGenerated && !options.includeAutoGeneratedTags ? tag : '';
|
|
2208
2287
|
if (impliedStartTag) {
|
|
@@ -2344,11 +2423,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2344
2423
|
currentTag = tag;
|
|
2345
2424
|
charsPrevTag = tag;
|
|
2346
2425
|
if (!inlineTextSet.has(lowerTag || tag)) {
|
|
2347
|
-
|
|
2426
|
+
// Replaced content counts as content for the whitespace after what holds it
|
|
2427
|
+
currentChars = inlineElementsToKeepWhitespace.has(lowerTag || tag) ? '|' : '';
|
|
2348
2428
|
}
|
|
2349
2429
|
hasChars = false;
|
|
2350
2430
|
currentAttrs = attrs;
|
|
2351
2431
|
|
|
2432
|
+
// What this start tag closed, which is left to the end tags if its element is removed
|
|
2433
|
+
const impliedEndsClosed = impliedEndsIndex === buffer.length ? impliedEnds : undefined;
|
|
2434
|
+
|
|
2435
|
+
// What this start tag made optional, for if the element then turns out to be removed
|
|
2436
|
+
/** @type {{tag: string, text: string} | undefined} */
|
|
2437
|
+
let omittedStartTag;
|
|
2438
|
+
/** @type {{tag: string, text: string} | undefined} */
|
|
2439
|
+
let omittedEndTag;
|
|
2352
2440
|
let optional = options.removeOptionalTags;
|
|
2353
2441
|
if (optional) {
|
|
2354
2442
|
const htmlTag = htmlElements.has(tag);
|
|
@@ -2359,7 +2447,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2359
2447
|
// `<colgroup>` may be omitted if first thing inside is `<col>`
|
|
2360
2448
|
// `<tbody>` may be omitted if first thing inside is `<tr>`
|
|
2361
2449
|
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
2362
|
-
removeStartTag();
|
|
2450
|
+
const text = removeStartTag(options.removeEmptyElements);
|
|
2451
|
+
if (text !== undefined) {
|
|
2452
|
+
omittedStartTag = { tag: optionalStartTag, text };
|
|
2453
|
+
}
|
|
2363
2454
|
}
|
|
2364
2455
|
optionalStartTag = '';
|
|
2365
2456
|
// End-tag-followed-by-start-tag omission rules
|
|
@@ -2367,7 +2458,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2367
2458
|
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag) && (tableClosesP || optionalEndTag !== 'p' || tag !== 'table')) {
|
|
2368
2459
|
// Only a start tag that gets written can stand in for the end tag
|
|
2369
2460
|
if (optionalEndTagEmitted && (!autoGenerated || options.includeAutoGeneratedTags)) {
|
|
2370
|
-
removeEndTag();
|
|
2461
|
+
const text = removeEndTag();
|
|
2462
|
+
if (options.removeEmptyElements) {
|
|
2463
|
+
omittedEndTag = { tag: optionalEndTag, text };
|
|
2464
|
+
}
|
|
2371
2465
|
}
|
|
2372
2466
|
// `<colgroup>` cannot be omitted if preceding `</colgroup>` is omitted
|
|
2373
2467
|
// `<tbody>` cannot be omitted if preceding `</tbody>`, `</thead>`, or `</tfoot>` is
|
|
@@ -2390,7 +2484,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2390
2484
|
const tagLower = lowerTag || tag;
|
|
2391
2485
|
if (!canTrimWhitespace(tagLower, tagOut, attrs) || stackNoTrimWhitespace.length) {
|
|
2392
2486
|
stackNoTrimWhitespace.push(tag);
|
|
2393
|
-
if (tagLower === 'pre'
|
|
2487
|
+
if (tagLower === 'pre') preDepth++;
|
|
2394
2488
|
}
|
|
2395
2489
|
if (options.collapseWhitespace && (!canCollapseWhitespace(tagLower, tagOut, attrs) || stackNoCollapseWhitespace.length)) {
|
|
2396
2490
|
stackNoCollapseWhitespace.push(tag);
|
|
@@ -2420,7 +2514,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2420
2514
|
markOpenElementFilled();
|
|
2421
2515
|
} else {
|
|
2422
2516
|
// A start tag not written can’t be removed with its element
|
|
2423
|
-
openElements.push({ tag, index: buffer.length - 1, empty: !autoGenerated || Boolean(options.includeAutoGeneratedTags) });
|
|
2517
|
+
openElements.push({ tag, index: buffer.length - 1, empty: !autoGenerated || Boolean(options.includeAutoGeneratedTags), omittedStartTag, omittedEndTag, impliedEnds: impliedEndsClosed });
|
|
2424
2518
|
}
|
|
2425
2519
|
}
|
|
2426
2520
|
|
|
@@ -2513,10 +2607,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2513
2607
|
const insideNoTrim = stackNoTrimWhitespace.length > 0;
|
|
2514
2608
|
let closesNoTrim = false;
|
|
2515
2609
|
if (stackNoTrimWhitespace.length) {
|
|
2516
|
-
if (
|
|
2610
|
+
if (removeClosedTag(stackNoTrimWhitespace, tag, lowerTag)) {
|
|
2517
2611
|
const tagLower = lowerTag || tag;
|
|
2518
|
-
if (tagLower === 'pre'
|
|
2519
|
-
stackNoTrimWhitespace.pop();
|
|
2612
|
+
if (tagLower === 'pre') preDepth--;
|
|
2520
2613
|
closesNoTrim = !stackNoTrimWhitespace.length;
|
|
2521
2614
|
}
|
|
2522
2615
|
} else if (options.collapseWhitespace) {
|
|
@@ -2526,9 +2619,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2526
2619
|
transparentDepth--;
|
|
2527
2620
|
}
|
|
2528
2621
|
let closesNoCollapse = false;
|
|
2529
|
-
if (stackNoCollapseWhitespace.length &&
|
|
2530
|
-
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
2531
|
-
stackNoCollapseWhitespace.pop();
|
|
2622
|
+
if (stackNoCollapseWhitespace.length && removeClosedTag(stackNoCollapseWhitespace, tag, lowerTag)) {
|
|
2532
2623
|
closesNoCollapse = !stackNoCollapseWhitespace.length;
|
|
2533
2624
|
}
|
|
2534
2625
|
|
|
@@ -2539,6 +2630,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2539
2630
|
const skipsEndTag = autoGenerated && (!options.includeAutoGeneratedTags || Boolean(holdsTable));
|
|
2540
2631
|
impliedStartTag = '';
|
|
2541
2632
|
|
|
2633
|
+
let startTagRemoved = false;
|
|
2542
2634
|
let isElementEmpty = false;
|
|
2543
2635
|
if (tag === currentTag) {
|
|
2544
2636
|
currentTag = '';
|
|
@@ -2546,7 +2638,19 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2546
2638
|
}
|
|
2547
2639
|
selfCloses &&= isElementEmpty;
|
|
2548
2640
|
// A stray end tag has no open element of its own
|
|
2549
|
-
|
|
2641
|
+
let openElement;
|
|
2642
|
+
if (options.removeEmptyElements && openElements.length) {
|
|
2643
|
+
if (openElements[openElements.length - 1]?.tag === tag) {
|
|
2644
|
+
openElement = openElements.pop();
|
|
2645
|
+
} else if (canCloseBelowTop(tag, lowerTag)) {
|
|
2646
|
+
const index = openElements.findLastIndex(entry => entry.tag === tag);
|
|
2647
|
+
if (index !== -1) {
|
|
2648
|
+
openElement = openElements.splice(index, 1)[0];
|
|
2649
|
+
// Closed around what is open inside it, so that holds this end tag now
|
|
2650
|
+
currentTag = '';
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2550
2654
|
// Emptied by removing what it held, as long as its start tag is still the last one written
|
|
2551
2655
|
const isElementEmptied = !isElementEmpty && Boolean(openElement?.empty) && lastStartTagIndex() === openElement?.index;
|
|
2552
2656
|
|
|
@@ -2554,15 +2658,17 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2554
2658
|
// `<html>`, `<head>`, or `<body>` may be omitted if the element is empty
|
|
2555
2659
|
if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
|
|
2556
2660
|
removeStartTag();
|
|
2661
|
+
startTagRemoved = true;
|
|
2557
2662
|
}
|
|
2558
2663
|
optionalStartTag = '';
|
|
2559
|
-
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
2560
|
-
// `</head>` may be omitted if not followed by space or comment
|
|
2561
|
-
// `</p>` may be omitted if no more content
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2664
|
+
// `</html>` or `</body>` may be omitted if not followed by comment;
|
|
2665
|
+
// `</head>` may be omitted if not followed by space or comment;
|
|
2666
|
+
// `</p>`, `</li>`, `</dd>`, and `</dt>` may be omitted if no more content
|
|
2667
|
+
// in parent, as long as the parent’s end tag closes these special elements
|
|
2668
|
+
// in browsers (`pClosingParents`); except for `</dt>` or `</thead>`,
|
|
2669
|
+
// end tags may be omitted if no more content in parent
|
|
2564
2670
|
// https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
2565
|
-
if (tag && optionalEndTag && optionalEndTagEmitted && !trailingElements.has(optionalEndTag) && (optionalEndTag
|
|
2671
|
+
if (tag && optionalEndTag && optionalEndTagEmitted && !trailingElements.has(optionalEndTag) && (!specialOptionalEndTags.has(optionalEndTag) || pClosingParents.has(tag))) {
|
|
2566
2672
|
removeEndTag(insideNoTrim);
|
|
2567
2673
|
}
|
|
2568
2674
|
optionalEndTag = optionalEndTags.has(tag) && !endsImpliedStart && !holdsTable ? tag : '';
|
|
@@ -2571,8 +2677,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2571
2677
|
|
|
2572
2678
|
let isElementRemoved = false;
|
|
2573
2679
|
// A formatting element closed by a block is reopened after it by browsers, so it has to stay
|
|
2574
|
-
if (options.removeEmptyElements && (isElementEmpty || isElementEmptied) && !options.insideForeignContent && canRemoveElement(tag, attrs) &&
|
|
2575
|
-
!(autoGenerated && formattingElements.has(tag))) {
|
|
2680
|
+
if (options.removeEmptyElements && (isElementEmpty || isElementEmptied) && !options.insideForeignContent && canRemoveElement(tag, attrs, options, tagOut) &&
|
|
2681
|
+
!(autoGenerated && formattingElements.has(tag)) && !openElement?.impliedEnds?.some(implied => !implied.text)) {
|
|
2576
2682
|
let preserve = false;
|
|
2577
2683
|
if (removeEmptyElementsExcept.length) {
|
|
2578
2684
|
const normalizedAttrs = attrs.map((/** @type {HTMLAttribute} */ attr) => ({ ...attr, name: resolveName(attr.name) }));
|
|
@@ -2580,11 +2686,37 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2580
2686
|
}
|
|
2581
2687
|
|
|
2582
2688
|
if (!preserve) {
|
|
2583
|
-
|
|
2689
|
+
// Once it is gone, the last start tag is another element’s
|
|
2690
|
+
if (!startTagRemoved) {
|
|
2691
|
+
removeStartTag();
|
|
2692
|
+
}
|
|
2584
2693
|
isElementRemoved = true;
|
|
2585
2694
|
optionalStartTag = '';
|
|
2586
2695
|
optionalEndTag = '';
|
|
2587
2696
|
optionalEndTagEmitted = false;
|
|
2697
|
+
// What it made optional is up to what comes next now
|
|
2698
|
+
if (openElement?.omittedEndTag) {
|
|
2699
|
+
bufferPush(openElement.omittedEndTag.text, true);
|
|
2700
|
+
optionalEndTag = openElement.omittedEndTag.tag;
|
|
2701
|
+
optionalEndTagEmitted = true;
|
|
2702
|
+
}
|
|
2703
|
+
if (openElement?.omittedStartTag) {
|
|
2704
|
+
bufferPush(openElement.omittedStartTag.text, true);
|
|
2705
|
+
optionalStartTag = openElement.omittedStartTag.tag;
|
|
2706
|
+
}
|
|
2707
|
+
if (openElement?.impliedEnds) {
|
|
2708
|
+
// Browsers reopen formatting elements after what closed them, which their end tags
|
|
2709
|
+
// would prevent and whitespace following an omitted end tag would change
|
|
2710
|
+
const written = openElement.impliedEnds.filter(implied => !formattingElements.has(implied.tag));
|
|
2711
|
+
for (const implied of written) {
|
|
2712
|
+
bufferPush(implied.text, true);
|
|
2713
|
+
}
|
|
2714
|
+
const last = written[written.length - 1];
|
|
2715
|
+
if (last && written.length === openElement.impliedEnds.length && optionalEndTags.has(last.tag)) {
|
|
2716
|
+
optionalEndTag = last.tag;
|
|
2717
|
+
optionalEndTagEmitted = true;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2588
2720
|
} else {
|
|
2589
2721
|
// Preserve the element—add closing tag
|
|
2590
2722
|
if (skipsEndTag) {
|
|
@@ -2595,7 +2727,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2595
2727
|
charsPrevTag = '/' + tag;
|
|
2596
2728
|
if (!inlineElements.has(lowerTag || tag)) {
|
|
2597
2729
|
currentChars = '';
|
|
2598
|
-
} else if (isElementEmpty) {
|
|
2730
|
+
} else if (isElementEmpty || !inlineTextSet.has(lowerTag || tag)) {
|
|
2599
2731
|
currentChars += '|';
|
|
2600
2732
|
}
|
|
2601
2733
|
}
|
|
@@ -2608,11 +2740,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2608
2740
|
charsPrevTag = '/' + tag;
|
|
2609
2741
|
if (!inlineElements.has(lowerTag || tag)) {
|
|
2610
2742
|
currentChars = '';
|
|
2611
|
-
} else if (isElementEmpty) {
|
|
2743
|
+
} else if (isElementEmpty || !inlineTextSet.has(lowerTag || tag)) {
|
|
2744
|
+
// What is not text counts as content for the whitespace after it
|
|
2612
2745
|
currentChars += '|';
|
|
2613
2746
|
}
|
|
2614
2747
|
}
|
|
2615
2748
|
|
|
2749
|
+
if (skipsEndTag && !isElementRemoved && options.removeEmptyElements) {
|
|
2750
|
+
if (impliedEndsIndex !== buffer.length) {
|
|
2751
|
+
impliedEnds = [];
|
|
2752
|
+
impliedEndsIndex = buffer.length;
|
|
2753
|
+
}
|
|
2754
|
+
impliedEnds.push({ tag, text: holdsTable ? '' : endTagText });
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2616
2757
|
if (closesNoTrim && !isElementRemoved) {
|
|
2617
2758
|
noTrimEndIndex = buffer.length - 1;
|
|
2618
2759
|
}
|
|
@@ -2678,6 +2819,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2678
2819
|
if (needsProcessScript) {
|
|
2679
2820
|
text = await processScript(text, options, currentTag, currentAttrs, minifyHTML);
|
|
2680
2821
|
}
|
|
2822
|
+
if (needsMinifyJS || needsMinifyCSS) {
|
|
2823
|
+
text = replaceNUL(text);
|
|
2824
|
+
}
|
|
2681
2825
|
if (needsMinifyJS) {
|
|
2682
2826
|
const prewarmed = jsPrewarmed?.get((isModuleScript ? 'm|' : '|') + text);
|
|
2683
2827
|
text = prewarmed ? await prewarmed : await options.minifyJS(text, false, isModuleScript);
|
|
@@ -2710,12 +2854,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2710
2854
|
strayEnd: function () {
|
|
2711
2855
|
removed = true;
|
|
2712
2856
|
},
|
|
2713
|
-
doctype: function (/** @type {string} */ doctype, /** @type {boolean} */ noQuirksMode) {
|
|
2857
|
+
doctype: function (/** @type {string} */ doctype, /** @type {boolean} */ noQuirksMode, /** @type {boolean} */ isIgnored) {
|
|
2714
2858
|
tableClosesP = noQuirksMode;
|
|
2859
|
+
if (isIgnored) {
|
|
2860
|
+
removed = true;
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2715
2863
|
bufferPush(options.useShortDoctype
|
|
2716
2864
|
? '<!doctype' +
|
|
2717
2865
|
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
2718
|
-
: collapseWhitespaceAll(
|
|
2866
|
+
: doctype.replace(RE_DOCTYPE_SEGMENT, (segment, quoted) => quoted ?? collapseWhitespaceAll(segment)), false);
|
|
2719
2867
|
}
|
|
2720
2868
|
});
|
|
2721
2869
|
|
|
@@ -2749,7 +2897,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2749
2897
|
for (let i = 0; i < buffer.length; i++) {
|
|
2750
2898
|
const segment = /** @type {string} */ (buffer[i]);
|
|
2751
2899
|
if (segment.indexOf(uidSVGRawText) !== -1) {
|
|
2752
|
-
|
|
2900
|
+
bufferSet(i, segment.replace(pattern, (/** @type {string} */ match, /** @type {string} */ index) => svgRawTexts[+index] ?? match));
|
|
2753
2901
|
}
|
|
2754
2902
|
}
|
|
2755
2903
|
}
|
|
@@ -3035,6 +3183,9 @@ export const minify = async function (value, options) {
|
|
|
3035
3183
|
let processedBase;
|
|
3036
3184
|
// `valueUnchanged` needs no budget of its own: Recursion is bounded by the
|
|
3037
3185
|
// stored snapshot, which is a finite tree of at most `SNAPSHOT_MAX_NODES`
|
|
3186
|
+
//
|
|
3187
|
+
// Note: The comparison takes about 8% of a call on tiny documents; only options
|
|
3188
|
+
// known never to change (e.g., deeply frozen ones) could skip it
|
|
3038
3189
|
if (cached && valueUnchanged(cached.snapshot, inputOptions)) {
|
|
3039
3190
|
processedBase = cached.processed;
|
|
3040
3191
|
} else {
|