html-minifier-next 4.16.1 → 4.16.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 +13 -11
- package/dist/htmlminifier.cjs +124 -67
- package/dist/htmlminifier.esm.bundle.js +123 -66
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +6 -0
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +2 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +11 -7
- package/src/lib/attributes.js +32 -12
- package/src/lib/constants.js +12 -0
- package/src/lib/options.js +79 -47
- package/src/lib/svg.js +3 -2
package/README.md
CHANGED
|
@@ -358,36 +358,38 @@ How does HTML Minifier Next compare to other minifiers? (All minification with t
|
|
|
358
358
|
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next) ([config](https://github.com/j9t/html-minifier-next/blob/main/benchmarks/html-minifier.json))<br>[](https://socket.dev/npm/package/html-minifier-next) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
359
359
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
360
360
|
| [A List Apart](https://alistapart.com/) | 59 | **50** | 51 | 52 | 51 | 54 | 52 |
|
|
361
|
-
| [Apple](https://www.apple.com/) | 211 | **
|
|
362
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
361
|
+
| [Apple](https://www.apple.com/) | 211 | **176** | 187 | 189 | 190 | 191 | 192 |
|
|
362
|
+
| [BBC](https://www.bbc.co.uk/) | 686 | **624** | 643 | 644 | 645 | 680 | n/a |
|
|
363
363
|
| [CERN](https://home.cern/) | 152 | **83** | 91 | 91 | 91 | 93 | 96 |
|
|
364
364
|
| [CSS-Tricks](https://css-tricks.com/) | 162 | **119** | 127 | 143 | 143 | 148 | 144 |
|
|
365
365
|
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | **6401** | 6573 | 6455 | 6578 | 6626 | n/a |
|
|
366
366
|
| [EDRi](https://edri.org/) | 80 | **59** | 70 | 70 | 71 | 75 | 73 |
|
|
367
367
|
| [EFF](https://www.eff.org/) | 54 | **45** | 49 | 47 | 48 | 49 | 49 |
|
|
368
368
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
369
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
369
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1587 | 1476 | **1421** | 1511 | 1522 | 1533 | n/a |
|
|
370
370
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 153 | **122** | 126 | 126 | 126 | 132 | 127 |
|
|
371
371
|
| [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** | 238 | 223 | 225 | 244 | 225 |
|
|
372
372
|
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 18 | 18 |
|
|
373
|
+
| [Ground News](https://ground.news/) | 2291 | **2005** | 2106 | 2136 | 2139 | 2278 | n/a |
|
|
373
374
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | 153 | **147** | 149 | 155 | 149 |
|
|
374
|
-
| [Igalia](https://www.igalia.com/) | 50 | **33** | 36 | 36 | 36 | 37 |
|
|
375
|
-
| [Leanpub](https://leanpub.com/) |
|
|
375
|
+
| [Igalia](https://www.igalia.com/) | 50 | **33** | 36 | 36 | 36 | 37 | 37 |
|
|
376
|
+
| [Leanpub](https://leanpub.com/) | 235 | **205** | 220 | 219 | 220 | 230 | 232 |
|
|
376
377
|
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | 32 | 35 | 35 | 36 | 36 |
|
|
377
|
-
| [MDN](https://developer.mozilla.org/en-US/) |
|
|
378
|
-
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 |
|
|
378
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | 64 | 65 | 65 | 68 | 68 |
|
|
379
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 | 202 | 203 |
|
|
379
380
|
| [Mistral AI](https://mistral.ai/) | 361 | **319** | 324 | 326 | 327 | 357 | n/a |
|
|
380
381
|
| [Mozilla](https://www.mozilla.org/) | 45 | **31** | 34 | 34 | 34 | 35 | 35 |
|
|
381
382
|
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 68 | **55** | 74 | 75 | 77 | 76 |
|
|
383
|
+
| [SitePoint](https://www.sitepoint.com/) | 482 | **351** | 422 | 456 | 460 | 478 | n/a |
|
|
382
384
|
| [Startup-Verband](https://startupverband.de/) | 42 | **29** | 30 | 30 | 30 | 31 | 30 |
|
|
383
385
|
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | **35** | 38 | 39 | 39 | 39 |
|
|
384
|
-
| [TPGi](https://www.tpgi.com/) |
|
|
386
|
+
| [TPGi](https://www.tpgi.com/) | 174 | **158** | 159 | 163 | 165 | 171 | 171 |
|
|
385
387
|
| [United Nations](https://www.un.org/en/) | 152 | **112** | 121 | 125 | 125 | 130 | 123 |
|
|
386
388
|
| [Vivaldi](https://vivaldi.com/) | 92 | **74** | n/a | 79 | 81 | 83 | 81 |
|
|
387
|
-
| [W3C](https://www.w3.org/) |
|
|
388
|
-
| **Average processing time** | |
|
|
389
|
+
| [W3C](https://www.w3.org/) | 51 | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
390
|
+
| **Average processing time** | | 100 ms (30/30) | 157 ms (29/30) | 52 ms (30/30) | **14 ms (30/30)** | 278 ms (30/30) | 1385 ms (24/30) |
|
|
389
391
|
|
|
390
|
-
(Last updated: Dec
|
|
392
|
+
(Last updated: Dec 27, 2025)
|
|
391
393
|
<!-- End auto-generated -->
|
|
392
394
|
|
|
393
395
|
Notes: Minimize does not minify CSS and JS. [HTML Minifier Terser](https://github.com/terser/html-minifier-terser) is currently not included due to issues around whitespace collapsing and removal of code using modern CSS features, issues which appeared to distort the data.
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -885,6 +885,12 @@ const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
|
885
885
|
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
886
886
|
const RE_TRAILING_SEMICOLON = /;$/;
|
|
887
887
|
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
888
|
+
const RE_LEGACY_ENTITIES = /&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g;
|
|
889
|
+
const RE_ESCAPE_LT = /</g;
|
|
890
|
+
const RE_ATTR_WS_CHECK = /[ \n\r\t\f]/;
|
|
891
|
+
const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
892
|
+
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
893
|
+
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
888
894
|
|
|
889
895
|
// Inline element Sets for whitespace handling
|
|
890
896
|
|
|
@@ -1375,7 +1381,7 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
1375
1381
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
1376
1382
|
|
|
1377
1383
|
// First, minify all numbers
|
|
1378
|
-
let result = pathData.replace(
|
|
1384
|
+
let result = pathData.replace(RE_NUMERIC_VALUE, (match) => {
|
|
1379
1385
|
return minifyNumber(match, precision);
|
|
1380
1386
|
});
|
|
1381
1387
|
|
|
@@ -1602,7 +1608,7 @@ function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
1602
1608
|
|
|
1603
1609
|
// Numeric attributes get precision reduction and whitespace minification
|
|
1604
1610
|
if (NUMERIC_ATTRS.has(name)) {
|
|
1605
|
-
const minified = value.replace(
|
|
1611
|
+
const minified = value.replace(RE_NUMERIC_VALUE, (match) => {
|
|
1606
1612
|
return minifyNumber(match, precision);
|
|
1607
1613
|
});
|
|
1608
1614
|
return minifyAttributeWhitespace(minified);
|
|
@@ -1689,9 +1695,10 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1689
1695
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
1690
1696
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
1691
1697
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
1698
|
+
* @param {LRU} deps.urlMinifyCache - URL minification cache
|
|
1692
1699
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
1693
1700
|
*/
|
|
1694
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
1701
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache, urlMinifyCache } = {}) => {
|
|
1695
1702
|
const options = {
|
|
1696
1703
|
name: function (name) {
|
|
1697
1704
|
return name.toLowerCase();
|
|
@@ -1764,24 +1771,30 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1764
1771
|
if (!text || !text.trim()) {
|
|
1765
1772
|
return text;
|
|
1766
1773
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1774
|
+
|
|
1775
|
+
// Optimization: Only process URLs if minification is enabled (not identity function)
|
|
1776
|
+
// This avoids expensive `replaceAsync` when URL minification is disabled
|
|
1777
|
+
if (options.minifyURLs !== identity) {
|
|
1778
|
+
text = await replaceAsync(
|
|
1779
|
+
text,
|
|
1780
|
+
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
1781
|
+
async function (match, prefix, dq, sq, unq, suffix) {
|
|
1782
|
+
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
1783
|
+
const url = dq ?? sq ?? unq ?? '';
|
|
1784
|
+
try {
|
|
1785
|
+
const out = await options.minifyURLs(url);
|
|
1786
|
+
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
if (!options.continueOnMinifyError) {
|
|
1789
|
+
throw err;
|
|
1790
|
+
}
|
|
1791
|
+
options.log && options.log(err);
|
|
1792
|
+
return match;
|
|
1779
1793
|
}
|
|
1780
|
-
options.log && options.log(err);
|
|
1781
|
-
return match;
|
|
1782
1794
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1785
1798
|
// Cache key: Wrapped content, type, options signature
|
|
1786
1799
|
const inputCSS = wrapCSS(text, type);
|
|
1787
1800
|
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
@@ -1793,36 +1806,44 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1793
1806
|
try {
|
|
1794
1807
|
const cached = cssMinifyCache.get(cssKey);
|
|
1795
1808
|
if (cached) {
|
|
1796
|
-
|
|
1809
|
+
// Support both resolved values and in-flight promises
|
|
1810
|
+
return await cached;
|
|
1797
1811
|
}
|
|
1798
1812
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1813
|
+
// In-flight promise caching: Prevent duplicate concurrent minifications
|
|
1814
|
+
// of the same CSS content (same pattern as JS minification)
|
|
1815
|
+
const inFlight = (async () => {
|
|
1816
|
+
const transformCSS = await getLightningCSS();
|
|
1817
|
+
// Note: `Buffer.from()` is required—Lightning CSS API expects Uint8Array
|
|
1818
|
+
const result = transformCSS({
|
|
1819
|
+
filename: 'input.css',
|
|
1820
|
+
code: Buffer.from(inputCSS),
|
|
1821
|
+
minify: true,
|
|
1822
|
+
errorRecovery: !!options.continueOnMinifyError,
|
|
1823
|
+
...lightningCssOptions
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
1827
|
+
|
|
1828
|
+
// If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
|
|
1829
|
+
// This preserves:
|
|
1830
|
+
// 1. Template code like `<?php ?>`, `<%= ?>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
1831
|
+
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
1832
|
+
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
1833
|
+
const isCDATA = text.includes('<![CDATA[');
|
|
1834
|
+
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
1835
|
+
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
1836
|
+
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
1837
|
+
|
|
1838
|
+
// Preserve if output is empty and input had template syntax or UIDs
|
|
1839
|
+
// This catches cases where Lightning CSS removed content that should be preserved
|
|
1840
|
+
return (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
1841
|
+
})();
|
|
1823
1842
|
|
|
1824
|
-
cssMinifyCache.set(cssKey,
|
|
1825
|
-
|
|
1843
|
+
cssMinifyCache.set(cssKey, inFlight);
|
|
1844
|
+
const resolved = await inFlight;
|
|
1845
|
+
cssMinifyCache.set(cssKey, resolved);
|
|
1846
|
+
return resolved;
|
|
1826
1847
|
} catch (err) {
|
|
1827
1848
|
cssMinifyCache.delete(cssKey);
|
|
1828
1849
|
if (!options.continueOnMinifyError) {
|
|
@@ -1954,16 +1975,33 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1954
1975
|
// Cache RelateURL instance for reuse (expensive to create)
|
|
1955
1976
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
1956
1977
|
|
|
1978
|
+
// Create instance-specific cache (results depend on site configuration)
|
|
1979
|
+
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
1980
|
+
|
|
1957
1981
|
options.minifyURLs = function (text) {
|
|
1958
|
-
// Fast-path: Skip if text doesn
|
|
1982
|
+
// Fast-path: Skip if text doesn't look like a URL that needs processing
|
|
1959
1983
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
1960
1984
|
if (!/[/:?#\s]/.test(text)) {
|
|
1961
1985
|
return text;
|
|
1962
1986
|
}
|
|
1963
1987
|
|
|
1988
|
+
// Check instance-specific cache
|
|
1989
|
+
if (instanceCache) {
|
|
1990
|
+
const cached = instanceCache.get(text);
|
|
1991
|
+
if (cached !== undefined) {
|
|
1992
|
+
return cached;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1964
1996
|
try {
|
|
1965
|
-
|
|
1997
|
+
const result = relateUrlInstance.relate(text);
|
|
1998
|
+
// Cache successful results
|
|
1999
|
+
if (instanceCache) {
|
|
2000
|
+
instanceCache.set(text, result);
|
|
2001
|
+
}
|
|
2002
|
+
return result;
|
|
1966
2003
|
} catch (err) {
|
|
2004
|
+
// Don’t cache errors
|
|
1967
2005
|
if (!options.continueOnMinifyError) {
|
|
1968
2006
|
throw err;
|
|
1969
2007
|
}
|
|
@@ -2038,9 +2076,28 @@ function attributesInclude(attributes, attribute) {
|
|
|
2038
2076
|
}
|
|
2039
2077
|
|
|
2040
2078
|
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
2079
|
+
// Fast-path: Check if this element–attribute combination can possibly be redundant
|
|
2080
|
+
// before doing expensive string operations
|
|
2081
|
+
|
|
2082
|
+
// Check if attribute name is in general defaults
|
|
2083
|
+
const hasGeneralDefault = attrName in generalDefaults;
|
|
2084
|
+
|
|
2085
|
+
// Check if element has any default attributes
|
|
2086
|
+
const tagHasDefaults = tag in tagDefaults;
|
|
2087
|
+
|
|
2088
|
+
// Check for legacy attribute rules (element- and attribute-specific)
|
|
2089
|
+
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
|
|
2090
|
+
(tag === 'a' && attrName === 'name');
|
|
2091
|
+
|
|
2092
|
+
// If none of these conditions apply, attribute cannot be redundant
|
|
2093
|
+
if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Now we know we need to check the value, so normalize it
|
|
2041
2098
|
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
2042
2099
|
|
|
2043
|
-
// Legacy
|
|
2100
|
+
// Legacy attribute checks
|
|
2044
2101
|
if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
|
|
2045
2102
|
return true;
|
|
2046
2103
|
}
|
|
@@ -2052,12 +2109,12 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
2052
2109
|
}
|
|
2053
2110
|
|
|
2054
2111
|
// Check general defaults
|
|
2055
|
-
if (generalDefaults[attrName] === attrValue) {
|
|
2112
|
+
if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
|
|
2056
2113
|
return true;
|
|
2057
2114
|
}
|
|
2058
2115
|
|
|
2059
2116
|
// Check tag-specific defaults
|
|
2060
|
-
return tagDefaults[tag]
|
|
2117
|
+
return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
|
|
2061
2118
|
}
|
|
2062
2119
|
|
|
2063
2120
|
function isScriptTypeAttribute(attrValue = '') {
|
|
@@ -2202,15 +2259,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2202
2259
|
// Apply early whitespace normalization if enabled
|
|
2203
2260
|
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
2204
2261
|
if (options.collapseAttributeWhitespace) {
|
|
2205
|
-
//
|
|
2206
|
-
|
|
2207
|
-
//
|
|
2208
|
-
|
|
2209
|
-
//
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
return ' ';
|
|
2213
|
-
});
|
|
2262
|
+
// Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
|
|
2263
|
+
if (RE_ATTR_WS_CHECK.test(attrValue)) {
|
|
2264
|
+
// Two-pass approach (faster than single-pass with callback)
|
|
2265
|
+
// First: Collapse internal whitespace sequences to single space
|
|
2266
|
+
// Second: Trim leading/trailing whitespace
|
|
2267
|
+
attrValue = attrValue.replace(RE_ATTR_WS_COLLAPSE, ' ').replace(RE_ATTR_WS_TRIM, '');
|
|
2268
|
+
}
|
|
2214
2269
|
}
|
|
2215
2270
|
|
|
2216
2271
|
if (isEventAttribute(attrName, options)) {
|
|
@@ -2740,8 +2795,9 @@ async function getSwc() {
|
|
|
2740
2795
|
|
|
2741
2796
|
// Minification caches
|
|
2742
2797
|
|
|
2743
|
-
const cssMinifyCache = new LRU(
|
|
2744
|
-
const jsMinifyCache = new LRU(
|
|
2798
|
+
const cssMinifyCache = new LRU(500);
|
|
2799
|
+
const jsMinifyCache = new LRU(500);
|
|
2800
|
+
const urlMinifyCache = new LRU(500);
|
|
2745
2801
|
|
|
2746
2802
|
// Type definitions
|
|
2747
2803
|
|
|
@@ -3284,7 +3340,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3284
3340
|
attrSorters[tag] = attrChains[tag].createSorter();
|
|
3285
3341
|
}
|
|
3286
3342
|
// Memoize sorted attribute orders—attribute sets often repeat in templates
|
|
3287
|
-
const attrOrderCache = new LRU(
|
|
3343
|
+
const attrOrderCache = new LRU(500);
|
|
3288
3344
|
|
|
3289
3345
|
options.sortAttributes = function (tag, attrs) {
|
|
3290
3346
|
const sorter = attrSorters[tag];
|
|
@@ -3315,7 +3371,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3315
3371
|
if (classChain) {
|
|
3316
3372
|
const sorter = classChain.createSorter();
|
|
3317
3373
|
// Memoize `sortClassName` results—class lists often repeat in templates
|
|
3318
|
-
const classNameCache = new LRU(
|
|
3374
|
+
const classNameCache = new LRU(500);
|
|
3319
3375
|
|
|
3320
3376
|
options.sortClassName = function (value) {
|
|
3321
3377
|
// Fast path: Single class (no spaces) needs no sorting
|
|
@@ -3809,10 +3865,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3809
3865
|
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
3810
3866
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
3811
3867
|
if (text.indexOf('&') !== -1) {
|
|
3812
|
-
text = text.replace(
|
|
3868
|
+
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
3813
3869
|
}
|
|
3814
3870
|
if (text.indexOf('<') !== -1) {
|
|
3815
|
-
text = text.replace(
|
|
3871
|
+
text = text.replace(RE_ESCAPE_LT, '<');
|
|
3816
3872
|
}
|
|
3817
3873
|
}
|
|
3818
3874
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
@@ -4021,7 +4077,8 @@ const minify = async function (value, options) {
|
|
|
4021
4077
|
getTerser,
|
|
4022
4078
|
getSwc,
|
|
4023
4079
|
cssMinifyCache,
|
|
4024
|
-
jsMinifyCache
|
|
4080
|
+
jsMinifyCache,
|
|
4081
|
+
urlMinifyCache
|
|
4025
4082
|
});
|
|
4026
4083
|
const result = await minifyHTML(value, options);
|
|
4027
4084
|
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
@@ -3497,6 +3497,12 @@ const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
|
3497
3497
|
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
3498
3498
|
const RE_TRAILING_SEMICOLON = /;$/;
|
|
3499
3499
|
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
3500
|
+
const RE_LEGACY_ENTITIES = /&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g;
|
|
3501
|
+
const RE_ESCAPE_LT = /</g;
|
|
3502
|
+
const RE_ATTR_WS_CHECK = /[ \n\r\t\f]/;
|
|
3503
|
+
const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
3504
|
+
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
3505
|
+
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
3500
3506
|
|
|
3501
3507
|
// Inline element Sets for whitespace handling
|
|
3502
3508
|
|
|
@@ -6517,7 +6523,7 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
6517
6523
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
6518
6524
|
|
|
6519
6525
|
// First, minify all numbers
|
|
6520
|
-
let result = pathData.replace(
|
|
6526
|
+
let result = pathData.replace(RE_NUMERIC_VALUE, (match) => {
|
|
6521
6527
|
return minifyNumber(match, precision);
|
|
6522
6528
|
});
|
|
6523
6529
|
|
|
@@ -6744,7 +6750,7 @@ function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
6744
6750
|
|
|
6745
6751
|
// Numeric attributes get precision reduction and whitespace minification
|
|
6746
6752
|
if (NUMERIC_ATTRS.has(name)) {
|
|
6747
|
-
const minified = value.replace(
|
|
6753
|
+
const minified = value.replace(RE_NUMERIC_VALUE, (match) => {
|
|
6748
6754
|
return minifyNumber(match, precision);
|
|
6749
6755
|
});
|
|
6750
6756
|
return minifyAttributeWhitespace(minified);
|
|
@@ -6831,9 +6837,10 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6831
6837
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
6832
6838
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
6833
6839
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
6840
|
+
* @param {LRU} deps.urlMinifyCache - URL minification cache
|
|
6834
6841
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
6835
6842
|
*/
|
|
6836
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
6843
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache, urlMinifyCache } = {}) => {
|
|
6837
6844
|
const options = {
|
|
6838
6845
|
name: function (name) {
|
|
6839
6846
|
return name.toLowerCase();
|
|
@@ -6906,24 +6913,30 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6906
6913
|
if (!text || !text.trim()) {
|
|
6907
6914
|
return text;
|
|
6908
6915
|
}
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6916
|
+
|
|
6917
|
+
// Optimization: Only process URLs if minification is enabled (not identity function)
|
|
6918
|
+
// This avoids expensive `replaceAsync` when URL minification is disabled
|
|
6919
|
+
if (options.minifyURLs !== identity) {
|
|
6920
|
+
text = await replaceAsync(
|
|
6921
|
+
text,
|
|
6922
|
+
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
6923
|
+
async function (match, prefix, dq, sq, unq, suffix) {
|
|
6924
|
+
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
6925
|
+
const url = dq ?? sq ?? unq ?? '';
|
|
6926
|
+
try {
|
|
6927
|
+
const out = await options.minifyURLs(url);
|
|
6928
|
+
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
6929
|
+
} catch (err) {
|
|
6930
|
+
if (!options.continueOnMinifyError) {
|
|
6931
|
+
throw err;
|
|
6932
|
+
}
|
|
6933
|
+
options.log && options.log(err);
|
|
6934
|
+
return match;
|
|
6921
6935
|
}
|
|
6922
|
-
options.log && options.log(err);
|
|
6923
|
-
return match;
|
|
6924
6936
|
}
|
|
6925
|
-
|
|
6926
|
-
|
|
6937
|
+
);
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6927
6940
|
// Cache key: Wrapped content, type, options signature
|
|
6928
6941
|
const inputCSS = wrapCSS(text, type);
|
|
6929
6942
|
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
@@ -6935,36 +6948,44 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6935
6948
|
try {
|
|
6936
6949
|
const cached = cssMinifyCache.get(cssKey);
|
|
6937
6950
|
if (cached) {
|
|
6938
|
-
|
|
6951
|
+
// Support both resolved values and in-flight promises
|
|
6952
|
+
return await cached;
|
|
6939
6953
|
}
|
|
6940
6954
|
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
// This preserves:
|
|
6954
|
-
// 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
6955
|
-
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
6956
|
-
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
6957
|
-
const isCDATA = text.includes('<![CDATA[');
|
|
6958
|
-
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
6959
|
-
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
6960
|
-
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
6955
|
+
// In-flight promise caching: Prevent duplicate concurrent minifications
|
|
6956
|
+
// of the same CSS content (same pattern as JS minification)
|
|
6957
|
+
const inFlight = (async () => {
|
|
6958
|
+
const transformCSS = await getLightningCSS();
|
|
6959
|
+
// Note: `Buffer.from()` is required—Lightning CSS API expects Uint8Array
|
|
6960
|
+
const result = transformCSS({
|
|
6961
|
+
filename: 'input.css',
|
|
6962
|
+
code: Buffer.from(inputCSS),
|
|
6963
|
+
minify: true,
|
|
6964
|
+
errorRecovery: !!options.continueOnMinifyError,
|
|
6965
|
+
...lightningCssOptions
|
|
6966
|
+
});
|
|
6961
6967
|
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6968
|
+
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
6969
|
+
|
|
6970
|
+
// If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
|
|
6971
|
+
// This preserves:
|
|
6972
|
+
// 1. Template code like `<?php ?>`, `<%= ?>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
6973
|
+
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
6974
|
+
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
6975
|
+
const isCDATA = text.includes('<![CDATA[');
|
|
6976
|
+
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
6977
|
+
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
6978
|
+
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
6979
|
+
|
|
6980
|
+
// Preserve if output is empty and input had template syntax or UIDs
|
|
6981
|
+
// This catches cases where Lightning CSS removed content that should be preserved
|
|
6982
|
+
return (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
6983
|
+
})();
|
|
6965
6984
|
|
|
6966
|
-
cssMinifyCache.set(cssKey,
|
|
6967
|
-
|
|
6985
|
+
cssMinifyCache.set(cssKey, inFlight);
|
|
6986
|
+
const resolved = await inFlight;
|
|
6987
|
+
cssMinifyCache.set(cssKey, resolved);
|
|
6988
|
+
return resolved;
|
|
6968
6989
|
} catch (err) {
|
|
6969
6990
|
cssMinifyCache.delete(cssKey);
|
|
6970
6991
|
if (!options.continueOnMinifyError) {
|
|
@@ -7096,16 +7117,33 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
7096
7117
|
// Cache RelateURL instance for reuse (expensive to create)
|
|
7097
7118
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
7098
7119
|
|
|
7120
|
+
// Create instance-specific cache (results depend on site configuration)
|
|
7121
|
+
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
7122
|
+
|
|
7099
7123
|
options.minifyURLs = function (text) {
|
|
7100
|
-
// Fast-path: Skip if text doesn
|
|
7124
|
+
// Fast-path: Skip if text doesn't look like a URL that needs processing
|
|
7101
7125
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
7102
7126
|
if (!/[/:?#\s]/.test(text)) {
|
|
7103
7127
|
return text;
|
|
7104
7128
|
}
|
|
7105
7129
|
|
|
7130
|
+
// Check instance-specific cache
|
|
7131
|
+
if (instanceCache) {
|
|
7132
|
+
const cached = instanceCache.get(text);
|
|
7133
|
+
if (cached !== undefined) {
|
|
7134
|
+
return cached;
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
|
|
7106
7138
|
try {
|
|
7107
|
-
|
|
7139
|
+
const result = relateUrlInstance.relate(text);
|
|
7140
|
+
// Cache successful results
|
|
7141
|
+
if (instanceCache) {
|
|
7142
|
+
instanceCache.set(text, result);
|
|
7143
|
+
}
|
|
7144
|
+
return result;
|
|
7108
7145
|
} catch (err) {
|
|
7146
|
+
// Don’t cache errors
|
|
7109
7147
|
if (!options.continueOnMinifyError) {
|
|
7110
7148
|
throw err;
|
|
7111
7149
|
}
|
|
@@ -7180,9 +7218,28 @@ function attributesInclude(attributes, attribute) {
|
|
|
7180
7218
|
}
|
|
7181
7219
|
|
|
7182
7220
|
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
7221
|
+
// Fast-path: Check if this element–attribute combination can possibly be redundant
|
|
7222
|
+
// before doing expensive string operations
|
|
7223
|
+
|
|
7224
|
+
// Check if attribute name is in general defaults
|
|
7225
|
+
const hasGeneralDefault = attrName in generalDefaults;
|
|
7226
|
+
|
|
7227
|
+
// Check if element has any default attributes
|
|
7228
|
+
const tagHasDefaults = tag in tagDefaults;
|
|
7229
|
+
|
|
7230
|
+
// Check for legacy attribute rules (element- and attribute-specific)
|
|
7231
|
+
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
|
|
7232
|
+
(tag === 'a' && attrName === 'name');
|
|
7233
|
+
|
|
7234
|
+
// If none of these conditions apply, attribute cannot be redundant
|
|
7235
|
+
if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
|
|
7236
|
+
return false;
|
|
7237
|
+
}
|
|
7238
|
+
|
|
7239
|
+
// Now we know we need to check the value, so normalize it
|
|
7183
7240
|
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
7184
7241
|
|
|
7185
|
-
// Legacy
|
|
7242
|
+
// Legacy attribute checks
|
|
7186
7243
|
if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
|
|
7187
7244
|
return true;
|
|
7188
7245
|
}
|
|
@@ -7194,12 +7251,12 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
7194
7251
|
}
|
|
7195
7252
|
|
|
7196
7253
|
// Check general defaults
|
|
7197
|
-
if (generalDefaults[attrName] === attrValue) {
|
|
7254
|
+
if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
|
|
7198
7255
|
return true;
|
|
7199
7256
|
}
|
|
7200
7257
|
|
|
7201
7258
|
// Check tag-specific defaults
|
|
7202
|
-
return tagDefaults[tag]
|
|
7259
|
+
return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
|
|
7203
7260
|
}
|
|
7204
7261
|
|
|
7205
7262
|
function isScriptTypeAttribute(attrValue = '') {
|
|
@@ -7344,15 +7401,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
7344
7401
|
// Apply early whitespace normalization if enabled
|
|
7345
7402
|
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
7346
7403
|
if (options.collapseAttributeWhitespace) {
|
|
7347
|
-
//
|
|
7348
|
-
|
|
7349
|
-
//
|
|
7350
|
-
|
|
7351
|
-
//
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
return ' ';
|
|
7355
|
-
});
|
|
7404
|
+
// Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
|
|
7405
|
+
if (RE_ATTR_WS_CHECK.test(attrValue)) {
|
|
7406
|
+
// Two-pass approach (faster than single-pass with callback)
|
|
7407
|
+
// First: Collapse internal whitespace sequences to single space
|
|
7408
|
+
// Second: Trim leading/trailing whitespace
|
|
7409
|
+
attrValue = attrValue.replace(RE_ATTR_WS_COLLAPSE, ' ').replace(RE_ATTR_WS_TRIM, '');
|
|
7410
|
+
}
|
|
7356
7411
|
}
|
|
7357
7412
|
|
|
7358
7413
|
if (isEventAttribute(attrName, options)) {
|
|
@@ -7882,8 +7937,9 @@ async function getSwc() {
|
|
|
7882
7937
|
|
|
7883
7938
|
// Minification caches
|
|
7884
7939
|
|
|
7885
|
-
const cssMinifyCache = new LRU(
|
|
7886
|
-
const jsMinifyCache = new LRU(
|
|
7940
|
+
const cssMinifyCache = new LRU(500);
|
|
7941
|
+
const jsMinifyCache = new LRU(500);
|
|
7942
|
+
const urlMinifyCache = new LRU(500);
|
|
7887
7943
|
|
|
7888
7944
|
// Type definitions
|
|
7889
7945
|
|
|
@@ -8426,7 +8482,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8426
8482
|
attrSorters[tag] = attrChains[tag].createSorter();
|
|
8427
8483
|
}
|
|
8428
8484
|
// Memoize sorted attribute orders—attribute sets often repeat in templates
|
|
8429
|
-
const attrOrderCache = new LRU(
|
|
8485
|
+
const attrOrderCache = new LRU(500);
|
|
8430
8486
|
|
|
8431
8487
|
options.sortAttributes = function (tag, attrs) {
|
|
8432
8488
|
const sorter = attrSorters[tag];
|
|
@@ -8457,7 +8513,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
8457
8513
|
if (classChain) {
|
|
8458
8514
|
const sorter = classChain.createSorter();
|
|
8459
8515
|
// Memoize `sortClassName` results—class lists often repeat in templates
|
|
8460
|
-
const classNameCache = new LRU(
|
|
8516
|
+
const classNameCache = new LRU(500);
|
|
8461
8517
|
|
|
8462
8518
|
options.sortClassName = function (value) {
|
|
8463
8519
|
// Fast path: Single class (no spaces) needs no sorting
|
|
@@ -8951,10 +9007,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8951
9007
|
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
8952
9008
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
8953
9009
|
if (text.indexOf('&') !== -1) {
|
|
8954
|
-
text = text.replace(
|
|
9010
|
+
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
8955
9011
|
}
|
|
8956
9012
|
if (text.indexOf('<') !== -1) {
|
|
8957
|
-
text = text.replace(
|
|
9013
|
+
text = text.replace(RE_ESCAPE_LT, '<');
|
|
8958
9014
|
}
|
|
8959
9015
|
}
|
|
8960
9016
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
@@ -9163,7 +9219,8 @@ const minify$1 = async function (value, options) {
|
|
|
9163
9219
|
getTerser,
|
|
9164
9220
|
getSwc,
|
|
9165
9221
|
cssMinifyCache,
|
|
9166
|
-
jsMinifyCache
|
|
9222
|
+
jsMinifyCache,
|
|
9223
|
+
urlMinifyCache
|
|
9167
9224
|
});
|
|
9168
9225
|
const result = await minifyHTML(value, options);
|
|
9169
9226
|
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
@@ -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":"AA01CO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAe3B;;;;;;;;;;;;UA9vCS,MAAM;YACN,MAAM;YACN,MAAM;mBACN,MAAM;iBACN,MAAM;kBACN,MAAM;;;;;;;;;;;;;4BAQN,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;wBAMjG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;oBAMhH,OAAO;;;;;;;;;kCAON,OAAO;;;;;;;;gCAQR,OAAO;;;;;;;;kCAOP,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;2BAOP,OAAO;;;;;;;;4BAOP,OAAO;;;;;;;2BAOP,OAAO;;;;;;;;uBAMP,MAAM,EAAE;;;;;;yBAOR,MAAM;;;;;;yBAKN,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;;;;;;;4BAKlB,MAAM,EAAE;;;;;;;oCAMR,MAAM;;;;;;;qBAMN,OAAO;;;;;;;YAMP,OAAO;;;;;;;;2BAMP,MAAM,EAAE;;;;;;;;;4BAOR,MAAM,EAAE;;;;;;;+BAQR,OAAO;;;;;;;2BAMP,SAAS,CAAC,MAAM,CAAC;;;;;;uBAMjB,OAAO;;;;;;;;UAKP,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;;;;qBAO1B,MAAM;;;;;;;oBAON,MAAM;;;;;;;;;;gBAMN,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;;;;;eAS9J,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG;QAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;iBAa3J,OAAO,GAAG,MAAM,GAAG,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;;gBAS7F,OAAO,GAAG;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAC;;;;;;;;WAUhF,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;;;;;+BAOxB,OAAO;;;;;;;;;;oBAMP,OAAO;;;;;;;;yBASP,OAAO;;;;;;;gCAOP,OAAO;;;;;;;;iCAMP,OAAO;;;;;;;;;;qBAOP,MAAM,EAAE;;;;;;;qBASR,IAAI,GAAG,GAAG;;;;;;;4BAMV,OAAO;;;;;;;;qBAMP,OAAO;;;;;;;;;4BAOP,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;;;;;;;;0BAQtD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;gCAOP,MAAM,EAAE;;;;;;;;yBAyBR,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;iCAOP,OAAO;;;;;;;oCAMP,OAAO;;;;;;;;;;0BAMP,OAAO;;;;;;;;;qBASP,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;;;;;;;;;oBAQzD,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;;;;;;0BAQrC,OAAO;;;;;;;sBAOP,OAAO;;wBAnekC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
|
|
@@ -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":"AA0BA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAED,mGAwCC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,4DAWC;AAED,2EAEC;AAED,qEAaC;AAED,wEAUC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,0IAgJC;AAsBD;;;;GAwCC;AAED,6GA4EC"}
|
|
@@ -10,6 +10,12 @@ export const RE_EVENT_ATTR_DEFAULT: RegExp;
|
|
|
10
10
|
export const RE_CAN_REMOVE_ATTR_QUOTES: RegExp;
|
|
11
11
|
export const RE_TRAILING_SEMICOLON: RegExp;
|
|
12
12
|
export const RE_AMP_ENTITY: RegExp;
|
|
13
|
+
export const RE_LEGACY_ENTITIES: RegExp;
|
|
14
|
+
export const RE_ESCAPE_LT: RegExp;
|
|
15
|
+
export const RE_ATTR_WS_CHECK: RegExp;
|
|
16
|
+
export const RE_ATTR_WS_COLLAPSE: RegExp;
|
|
17
|
+
export const RE_ATTR_WS_TRIM: RegExp;
|
|
18
|
+
export const RE_NUMERIC_VALUE: RegExp;
|
|
13
19
|
export const inlineElementsToKeepWhitespaceAround: Set<string>;
|
|
14
20
|
export const inlineElementsToKeepWhitespaceWithin: Set<string>;
|
|
15
21
|
export const inlineElementsToKeepWhitespace: Set<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"AAEA,iCAAoC;AACpC,+BAAkC;AAClC,oCAA2C;AAC3C,2CAAmD;AACnD,wCAA8C;AAC9C,4CAAkD;AAClD,4CAA2C;AAC3C,4CAA0D;AAC1D,2CAA8C;AAC9C,+CAA0D;AAC1D,2CAAmC;AACnC,mCAA4C;
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"AAEA,iCAAoC;AACpC,+BAAkC;AAClC,oCAA2C;AAC3C,2CAAmD;AACnD,wCAA8C;AAC9C,4CAAkD;AAClD,4CAA2C;AAC3C,4CAA0D;AAC1D,2CAA8C;AAC9C,+CAA0D;AAC1D,2CAAmC;AACnC,mCAA4C;AAC5C,wCAAwqB;AACxqB,kCAA0B;AAC1B,sCAAuC;AACvC,yCAA4C;AAC5C,qCAAuD;AACvD,sCAAmE;AAKnE,+DAAgb;AAGhb,+DAA6O;AAG7O,yDAAmF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CnF,qDAQG;AAEH,+CAEG;AAcH,0CAUG;AApBH,0CAAwhB;AAExhB,yCAAkD;AAIlD,qCAA8C;AAuB9C,4CAAiF;AAEjF,0CAAoM;AAEpM,qCAAwF;AAExF,0CAA8C;AAE9C,qCAA6S;AAE7S,sCAAsF;AAEtF,6CAA8D;AAE9D,gDAAqD;AAErD,oCAAkD;AAElD,2CAAqD;AAErD,2CAA8D;AAE9D,mCAAuC;AAEvC,uCAAuD;AAEvD,sCAA8C;AAE9C,oCAA2D;AAE3D,uCAA8C;AAE9C,mCAA+wC;AAI/wC,sCAEsD;AAItD,6CAAwD"}
|
|
@@ -7,9 +7,10 @@ export function shouldMinifyInnerHTML(options: any): boolean;
|
|
|
7
7
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
8
8
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
9
9
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
10
|
+
* @param {LRU} deps.urlMinifyCache - URL minification cache
|
|
10
11
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
11
12
|
*/
|
|
12
|
-
export function processOptions(inputOptions: Partial<MinifierOptions>, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache }?: {
|
|
13
|
+
export function processOptions(inputOptions: Partial<MinifierOptions>, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache, urlMinifyCache }?: {
|
|
13
14
|
getLightningCSS: Function;
|
|
14
15
|
getTerser: Function;
|
|
15
16
|
getSwc: Function;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAWA,6DAUC;AAID
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAWA,6DAUC;AAID;;;;;;;;;;GAUG;AACH,6CAVW,OAAO,CAAC,eAAe,CAAC,0FAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAI9C,eAAe,CA4U3B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../../src/lib/svg.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../../src/lib/svg.js"],"names":[],"mappings":"AAmVA;;;;;;GAMG;AACH,8CALW,MAAM,SACN,MAAM,kBAEJ,MAAM,CA0BlB;AAED;;;;;;;GAOG;AACH,8CANW,MAAM,QACN,MAAM,SACN,MAAM,kBAEJ,OAAO,CAanB;AAED;;;;GAIG;AACH,6DAkBC"}
|
package/package.json
CHANGED
package/src/htmlminifier.js
CHANGED
|
@@ -8,6 +8,8 @@ import { presets, getPreset, getPresetNames } from './presets.js';
|
|
|
8
8
|
import { LRU, identity, uniqueId } from './lib/utils.js';
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
RE_LEGACY_ENTITIES,
|
|
12
|
+
RE_ESCAPE_LT,
|
|
11
13
|
inlineElementsToKeepWhitespaceAround,
|
|
12
14
|
inlineElementsToKeepWhitespaceWithin,
|
|
13
15
|
specialContentTags,
|
|
@@ -91,8 +93,9 @@ async function getSwc() {
|
|
|
91
93
|
|
|
92
94
|
// Minification caches
|
|
93
95
|
|
|
94
|
-
const cssMinifyCache = new LRU(
|
|
95
|
-
const jsMinifyCache = new LRU(
|
|
96
|
+
const cssMinifyCache = new LRU(500);
|
|
97
|
+
const jsMinifyCache = new LRU(500);
|
|
98
|
+
const urlMinifyCache = new LRU(500);
|
|
96
99
|
|
|
97
100
|
// Type definitions
|
|
98
101
|
|
|
@@ -635,7 +638,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
635
638
|
attrSorters[tag] = attrChains[tag].createSorter();
|
|
636
639
|
}
|
|
637
640
|
// Memoize sorted attribute orders—attribute sets often repeat in templates
|
|
638
|
-
const attrOrderCache = new LRU(
|
|
641
|
+
const attrOrderCache = new LRU(500);
|
|
639
642
|
|
|
640
643
|
options.sortAttributes = function (tag, attrs) {
|
|
641
644
|
const sorter = attrSorters[tag];
|
|
@@ -666,7 +669,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
666
669
|
if (classChain) {
|
|
667
670
|
const sorter = classChain.createSorter();
|
|
668
671
|
// Memoize `sortClassName` results—class lists often repeat in templates
|
|
669
|
-
const classNameCache = new LRU(
|
|
672
|
+
const classNameCache = new LRU(500);
|
|
670
673
|
|
|
671
674
|
options.sortClassName = function (value) {
|
|
672
675
|
// Fast path: Single class (no spaces) needs no sorting
|
|
@@ -1160,10 +1163,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1160
1163
|
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
1161
1164
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
1162
1165
|
if (text.indexOf('&') !== -1) {
|
|
1163
|
-
text = text.replace(
|
|
1166
|
+
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
1164
1167
|
}
|
|
1165
1168
|
if (text.indexOf('<') !== -1) {
|
|
1166
|
-
text = text.replace(
|
|
1169
|
+
text = text.replace(RE_ESCAPE_LT, '<');
|
|
1167
1170
|
}
|
|
1168
1171
|
}
|
|
1169
1172
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
@@ -1372,7 +1375,8 @@ export const minify = async function (value, options) {
|
|
|
1372
1375
|
getTerser,
|
|
1373
1376
|
getSwc,
|
|
1374
1377
|
cssMinifyCache,
|
|
1375
|
-
jsMinifyCache
|
|
1378
|
+
jsMinifyCache,
|
|
1379
|
+
urlMinifyCache
|
|
1376
1380
|
});
|
|
1377
1381
|
const result = await minifyHTML(value, options);
|
|
1378
1382
|
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
package/src/lib/attributes.js
CHANGED
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
RE_EVENT_ATTR_DEFAULT,
|
|
7
7
|
RE_CAN_REMOVE_ATTR_QUOTES,
|
|
8
8
|
RE_AMP_ENTITY,
|
|
9
|
+
RE_ATTR_WS_CHECK,
|
|
10
|
+
RE_ATTR_WS_COLLAPSE,
|
|
11
|
+
RE_ATTR_WS_TRIM,
|
|
9
12
|
generalDefaults,
|
|
10
13
|
tagDefaults,
|
|
11
14
|
executableScriptsMimetypes,
|
|
@@ -62,9 +65,28 @@ function attributesInclude(attributes, attribute) {
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
68
|
+
// Fast-path: Check if this element–attribute combination can possibly be redundant
|
|
69
|
+
// before doing expensive string operations
|
|
70
|
+
|
|
71
|
+
// Check if attribute name is in general defaults
|
|
72
|
+
const hasGeneralDefault = attrName in generalDefaults;
|
|
73
|
+
|
|
74
|
+
// Check if element has any default attributes
|
|
75
|
+
const tagHasDefaults = tag in tagDefaults;
|
|
76
|
+
|
|
77
|
+
// Check for legacy attribute rules (element- and attribute-specific)
|
|
78
|
+
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
|
|
79
|
+
(tag === 'a' && attrName === 'name');
|
|
80
|
+
|
|
81
|
+
// If none of these conditions apply, attribute cannot be redundant
|
|
82
|
+
if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Now we know we need to check the value, so normalize it
|
|
65
87
|
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
66
88
|
|
|
67
|
-
// Legacy
|
|
89
|
+
// Legacy attribute checks
|
|
68
90
|
if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
|
|
69
91
|
return true;
|
|
70
92
|
}
|
|
@@ -76,12 +98,12 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
// Check general defaults
|
|
79
|
-
if (generalDefaults[attrName] === attrValue) {
|
|
101
|
+
if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
|
|
80
102
|
return true;
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
// Check tag-specific defaults
|
|
84
|
-
return tagDefaults[tag]
|
|
106
|
+
return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
function isScriptTypeAttribute(attrValue = '') {
|
|
@@ -226,15 +248,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
226
248
|
// Apply early whitespace normalization if enabled
|
|
227
249
|
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
228
250
|
if (options.collapseAttributeWhitespace) {
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return ' ';
|
|
237
|
-
});
|
|
251
|
+
// Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
|
|
252
|
+
if (RE_ATTR_WS_CHECK.test(attrValue)) {
|
|
253
|
+
// Two-pass approach (faster than single-pass with callback)
|
|
254
|
+
// First: Collapse internal whitespace sequences to single space
|
|
255
|
+
// Second: Trim leading/trailing whitespace
|
|
256
|
+
attrValue = attrValue.replace(RE_ATTR_WS_COLLAPSE, ' ').replace(RE_ATTR_WS_TRIM, '');
|
|
257
|
+
}
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
if (isEventAttribute(attrName, options)) {
|
package/src/lib/constants.js
CHANGED
|
@@ -12,6 +12,12 @@ const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
|
12
12
|
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
13
13
|
const RE_TRAILING_SEMICOLON = /;$/;
|
|
14
14
|
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
15
|
+
const RE_LEGACY_ENTITIES = /&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g;
|
|
16
|
+
const RE_ESCAPE_LT = /</g;
|
|
17
|
+
const RE_ATTR_WS_CHECK = /[ \n\r\t\f]/;
|
|
18
|
+
const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
19
|
+
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
20
|
+
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
15
21
|
|
|
16
22
|
// Inline element Sets for whitespace handling
|
|
17
23
|
|
|
@@ -169,6 +175,12 @@ export {
|
|
|
169
175
|
RE_CAN_REMOVE_ATTR_QUOTES,
|
|
170
176
|
RE_TRAILING_SEMICOLON,
|
|
171
177
|
RE_AMP_ENTITY,
|
|
178
|
+
RE_LEGACY_ENTITIES,
|
|
179
|
+
RE_ESCAPE_LT,
|
|
180
|
+
RE_ATTR_WS_CHECK,
|
|
181
|
+
RE_ATTR_WS_COLLAPSE,
|
|
182
|
+
RE_ATTR_WS_TRIM,
|
|
183
|
+
RE_NUMERIC_VALUE,
|
|
172
184
|
|
|
173
185
|
// Inline element Sets
|
|
174
186
|
inlineElementsToKeepWhitespaceAround,
|
package/src/lib/options.js
CHANGED
|
@@ -31,9 +31,10 @@ function shouldMinifyInnerHTML(options) {
|
|
|
31
31
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
32
32
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
33
33
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
34
|
+
* @param {LRU} deps.urlMinifyCache - URL minification cache
|
|
34
35
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
35
36
|
*/
|
|
36
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
37
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache, urlMinifyCache } = {}) => {
|
|
37
38
|
const options = {
|
|
38
39
|
name: function (name) {
|
|
39
40
|
return name.toLowerCase();
|
|
@@ -106,24 +107,30 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
106
107
|
if (!text || !text.trim()) {
|
|
107
108
|
return text;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
|
|
111
|
+
// Optimization: Only process URLs if minification is enabled (not identity function)
|
|
112
|
+
// This avoids expensive `replaceAsync` when URL minification is disabled
|
|
113
|
+
if (options.minifyURLs !== identity) {
|
|
114
|
+
text = await replaceAsync(
|
|
115
|
+
text,
|
|
116
|
+
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
117
|
+
async function (match, prefix, dq, sq, unq, suffix) {
|
|
118
|
+
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
119
|
+
const url = dq ?? sq ?? unq ?? '';
|
|
120
|
+
try {
|
|
121
|
+
const out = await options.minifyURLs(url);
|
|
122
|
+
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (!options.continueOnMinifyError) {
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
options.log && options.log(err);
|
|
128
|
+
return match;
|
|
121
129
|
}
|
|
122
|
-
options.log && options.log(err);
|
|
123
|
-
return match;
|
|
124
130
|
}
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
// Cache key: Wrapped content, type, options signature
|
|
128
135
|
const inputCSS = wrapCSS(text, type);
|
|
129
136
|
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
@@ -135,36 +142,44 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
135
142
|
try {
|
|
136
143
|
const cached = cssMinifyCache.get(cssKey);
|
|
137
144
|
if (cached) {
|
|
138
|
-
|
|
145
|
+
// Support both resolved values and in-flight promises
|
|
146
|
+
return await cached;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
149
|
+
// In-flight promise caching: Prevent duplicate concurrent minifications
|
|
150
|
+
// of the same CSS content (same pattern as JS minification)
|
|
151
|
+
const inFlight = (async () => {
|
|
152
|
+
const transformCSS = await getLightningCSS();
|
|
153
|
+
// Note: `Buffer.from()` is required—Lightning CSS API expects Uint8Array
|
|
154
|
+
const result = transformCSS({
|
|
155
|
+
filename: 'input.css',
|
|
156
|
+
code: Buffer.from(inputCSS),
|
|
157
|
+
minify: true,
|
|
158
|
+
errorRecovery: !!options.continueOnMinifyError,
|
|
159
|
+
...lightningCssOptions
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
163
|
+
|
|
164
|
+
// If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
|
|
165
|
+
// This preserves:
|
|
166
|
+
// 1. Template code like `<?php ?>`, `<%= ?>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
167
|
+
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
168
|
+
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
169
|
+
const isCDATA = text.includes('<![CDATA[');
|
|
170
|
+
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
171
|
+
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
172
|
+
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
173
|
+
|
|
174
|
+
// Preserve if output is empty and input had template syntax or UIDs
|
|
175
|
+
// This catches cases where Lightning CSS removed content that should be preserved
|
|
176
|
+
return (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
177
|
+
})();
|
|
178
|
+
|
|
179
|
+
cssMinifyCache.set(cssKey, inFlight);
|
|
180
|
+
const resolved = await inFlight;
|
|
181
|
+
cssMinifyCache.set(cssKey, resolved);
|
|
182
|
+
return resolved;
|
|
168
183
|
} catch (err) {
|
|
169
184
|
cssMinifyCache.delete(cssKey);
|
|
170
185
|
if (!options.continueOnMinifyError) {
|
|
@@ -296,16 +311,33 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
296
311
|
// Cache RelateURL instance for reuse (expensive to create)
|
|
297
312
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
298
313
|
|
|
314
|
+
// Create instance-specific cache (results depend on site configuration)
|
|
315
|
+
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
316
|
+
|
|
299
317
|
options.minifyURLs = function (text) {
|
|
300
|
-
// Fast-path: Skip if text doesn
|
|
318
|
+
// Fast-path: Skip if text doesn't look like a URL that needs processing
|
|
301
319
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
302
320
|
if (!/[/:?#\s]/.test(text)) {
|
|
303
321
|
return text;
|
|
304
322
|
}
|
|
305
323
|
|
|
324
|
+
// Check instance-specific cache
|
|
325
|
+
if (instanceCache) {
|
|
326
|
+
const cached = instanceCache.get(text);
|
|
327
|
+
if (cached !== undefined) {
|
|
328
|
+
return cached;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
306
332
|
try {
|
|
307
|
-
|
|
333
|
+
const result = relateUrlInstance.relate(text);
|
|
334
|
+
// Cache successful results
|
|
335
|
+
if (instanceCache) {
|
|
336
|
+
instanceCache.set(text, result);
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
308
339
|
} catch (err) {
|
|
340
|
+
// Don’t cache errors
|
|
309
341
|
if (!options.continueOnMinifyError) {
|
|
310
342
|
throw err;
|
|
311
343
|
}
|
package/src/lib/svg.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { LRU } from './utils.js';
|
|
13
|
+
import { RE_NUMERIC_VALUE } from './constants.js';
|
|
13
14
|
|
|
14
15
|
// Cache for minified numbers
|
|
15
16
|
const numberCache = new LRU(100);
|
|
@@ -128,7 +129,7 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
128
129
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
129
130
|
|
|
130
131
|
// First, minify all numbers
|
|
131
|
-
let result = pathData.replace(
|
|
132
|
+
let result = pathData.replace(RE_NUMERIC_VALUE, (match) => {
|
|
132
133
|
return minifyNumber(match, precision);
|
|
133
134
|
});
|
|
134
135
|
|
|
@@ -355,7 +356,7 @@ export function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
355
356
|
|
|
356
357
|
// Numeric attributes get precision reduction and whitespace minification
|
|
357
358
|
if (NUMERIC_ATTRS.has(name)) {
|
|
358
|
-
const minified = value.replace(
|
|
359
|
+
const minified = value.replace(RE_NUMERIC_VALUE, (match) => {
|
|
359
360
|
return minifyNumber(match, precision);
|
|
360
361
|
});
|
|
361
362
|
return minifyAttributeWhitespace(minified);
|