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 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>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[![npm last update](https://img.shields.io/npm/last-update/htmlnano)](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[![npm last update](https://img.shields.io/npm/last-update/@swc/html)](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[![npm last update](https://img.shields.io/npm/last-update/@minify-html/node)](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[![npm last update](https://img.shields.io/npm/last-update/minimize)](https://socket.dev/npm/package/minimize) | [html­com­pressor.­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 | **177** | 188 | 190 | 191 | 192 | 193 |
362
- | [BBC](https://www.bbc.co.uk/) | 675 | **614** | 633 | 634 | 635 | 669 | n/a |
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/) | 1579 | 1468 | **1414** | 1503 | 1514 | 1525 | n/a |
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 | 36 |
375
- | [Leanpub](https://leanpub.com/) | 231 | **202** | 216 | 216 | 217 | 227 | 229 |
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/) | 106 | **60** | 62 | 63 | 63 | 66 | 66 |
378
- | [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 | 203 | 204 |
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/) | 175 | **159** | 160 | 164 | 166 | 172 | 172 |
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/) | 50 | **36** | 39 | 38 | 38 | 41 | 39 |
388
- | **Average processing time** | | 120 ms (28/28) | 138 ms (27/28) | 41 ms (28/28) | **14 ms (28/28)** | 283 ms (28/28) | 1349 ms (24/28) |
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 26, 2025)
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.
@@ -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(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
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(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
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
- text = await replaceAsync(
1768
- text,
1769
- /(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
1770
- async function (match, prefix, dq, sq, unq, suffix) {
1771
- const quote = dq != null ? '"' : (sq != null ? "'" : '');
1772
- const url = dq ?? sq ?? unq ?? '';
1773
- try {
1774
- const out = await options.minifyURLs(url);
1775
- return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
1776
- } catch (err) {
1777
- if (!options.continueOnMinifyError) {
1778
- throw err;
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
- return cached;
1809
+ // Support both resolved values and in-flight promises
1810
+ return await cached;
1797
1811
  }
1798
1812
 
1799
- const transformCSS = await getLightningCSS();
1800
- const result = transformCSS({
1801
- filename: 'input.css',
1802
- code: Buffer.from(inputCSS),
1803
- minify: true,
1804
- errorRecovery: !!options.continueOnMinifyError,
1805
- ...lightningCssOptions
1806
- });
1807
-
1808
- const outputCSS = unwrapCSS(result.code.toString(), type);
1809
-
1810
- // If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
1811
- // This preserves:
1812
- // 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
1813
- // 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
1814
- // CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
1815
- const isCDATA = text.includes('<![CDATA[');
1816
- const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
1817
- const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
1818
- const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
1819
-
1820
- // Preserve if output is empty and input had template syntax or UIDs
1821
- // This catches cases where Lightning CSS removed content that should be preserved
1822
- const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
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, finalOutput);
1825
- return finalOutput;
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 doesnt look like a URL that needs processing
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
- return relateUrlInstance.relate(text);
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 attributes
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]?.[attrName] === attrValue;
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
- // Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
2206
- attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
2207
- // Leading whitespace (`offset === 0`)
2208
- if (offset === 0) return '';
2209
- // Trailing whitespace (match ends at string end)
2210
- if (offset + match.length === str.length) return '';
2211
- // Internal whitespace
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(200);
2744
- const jsMinifyCache = new LRU(200);
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(200);
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(200);
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 `&amp`, without the semi-colon.
3810
3866
  // https://mathiasbynens.be/notes/ambiguous-ampersands
3811
3867
  if (text.indexOf('&') !== -1) {
3812
- text = text.replace(/&((?: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, '&amp$1');
3868
+ text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
3813
3869
  }
3814
3870
  if (text.indexOf('<') !== -1) {
3815
- text = text.replace(/</g, '&lt;');
3871
+ text = text.replace(RE_ESCAPE_LT, '&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(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
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(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
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
- text = await replaceAsync(
6910
- text,
6911
- /(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
6912
- async function (match, prefix, dq, sq, unq, suffix) {
6913
- const quote = dq != null ? '"' : (sq != null ? "'" : '');
6914
- const url = dq ?? sq ?? unq ?? '';
6915
- try {
6916
- const out = await options.minifyURLs(url);
6917
- return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
6918
- } catch (err) {
6919
- if (!options.continueOnMinifyError) {
6920
- throw err;
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
- return cached;
6951
+ // Support both resolved values and in-flight promises
6952
+ return await cached;
6939
6953
  }
6940
6954
 
6941
- const transformCSS = await getLightningCSS();
6942
- const result = transformCSS({
6943
- filename: 'input.css',
6944
- code: Buffer.from(inputCSS),
6945
- minify: true,
6946
- errorRecovery: !!options.continueOnMinifyError,
6947
- ...lightningCssOptions
6948
- });
6949
-
6950
- const outputCSS = unwrapCSS(result.code.toString(), type);
6951
-
6952
- // If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
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
- // Preserve if output is empty and input had template syntax or UIDs
6963
- // This catches cases where Lightning CSS removed content that should be preserved
6964
- const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
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, finalOutput);
6967
- return finalOutput;
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 doesnt look like a URL that needs processing
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
- return relateUrlInstance.relate(text);
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 attributes
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]?.[attrName] === attrValue;
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
- // Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
7348
- attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
7349
- // Leading whitespace (`offset === 0`)
7350
- if (offset === 0) return '';
7351
- // Trailing whitespace (match ends at string end)
7352
- if (offset + match.length === str.length) return '';
7353
- // Internal whitespace
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(200);
7886
- const jsMinifyCache = new LRU(200);
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(200);
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(200);
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 `&amp`, without the semi-colon.
8952
9008
  // https://mathiasbynens.be/notes/ambiguous-ampersands
8953
9009
  if (text.indexOf('&') !== -1) {
8954
- text = text.replace(/&((?: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, '&amp$1');
9010
+ text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
8955
9011
  }
8956
9012
  if (text.indexOf('<') !== -1) {
8957
- text = text.replace(/</g, '&lt;');
9013
+ text = text.replace(RE_ESCAPE_LT, '&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":"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"}
1
+ {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAuBA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAED,mGAqBC;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,0IAkJC;AAsBD;;;;GAwCC;AAED,6GA4EC"}
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;AAK5C,+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"}
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;;;;;;;;;GASG;AACH,6CATW,OAAO,CAAC,eAAe,CAAC,0EAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAG9C,eAAe,CA6S3B"}
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":"AAkVA;;;;;;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"}
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
@@ -93,5 +93,5 @@
93
93
  "test:watch": "node --test --watch tests/*.spec.js"
94
94
  },
95
95
  "type": "module",
96
- "version": "4.16.1"
96
+ "version": "4.16.3"
97
97
  }
@@ -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(200);
95
- const jsMinifyCache = new LRU(200);
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(200);
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(200);
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 `&amp`, without the semi-colon.
1161
1164
  // https://mathiasbynens.be/notes/ambiguous-ampersands
1162
1165
  if (text.indexOf('&') !== -1) {
1163
- text = text.replace(/&((?: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, '&amp$1');
1166
+ text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
1164
1167
  }
1165
1168
  if (text.indexOf('<') !== -1) {
1166
- text = text.replace(/</g, '&lt;');
1169
+ text = text.replace(RE_ESCAPE_LT, '&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');
@@ -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 attributes
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]?.[attrName] === attrValue;
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
- // Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
230
- attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
231
- // Leading whitespace (`offset === 0`)
232
- if (offset === 0) return '';
233
- // Trailing whitespace (match ends at string end)
234
- if (offset + match.length === str.length) return '';
235
- // Internal whitespace
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)) {
@@ -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,
@@ -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
- text = await replaceAsync(
110
- text,
111
- /(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
112
- async function (match, prefix, dq, sq, unq, suffix) {
113
- const quote = dq != null ? '"' : (sq != null ? "'" : '');
114
- const url = dq ?? sq ?? unq ?? '';
115
- try {
116
- const out = await options.minifyURLs(url);
117
- return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
118
- } catch (err) {
119
- if (!options.continueOnMinifyError) {
120
- throw err;
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
- return cached;
145
+ // Support both resolved values and in-flight promises
146
+ return await cached;
139
147
  }
140
148
 
141
- const transformCSS = await getLightningCSS();
142
- const result = transformCSS({
143
- filename: 'input.css',
144
- code: Buffer.from(inputCSS),
145
- minify: true,
146
- errorRecovery: !!options.continueOnMinifyError,
147
- ...lightningCssOptions
148
- });
149
-
150
- const outputCSS = unwrapCSS(result.code.toString(), type);
151
-
152
- // If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
153
- // This preserves:
154
- // 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
155
- // 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
156
- // CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
157
- const isCDATA = text.includes('<![CDATA[');
158
- const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
159
- const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
160
- const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
161
-
162
- // Preserve if output is empty and input had template syntax or UIDs
163
- // This catches cases where Lightning CSS removed content that should be preserved
164
- const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
165
-
166
- cssMinifyCache.set(cssKey, finalOutput);
167
- return finalOutput;
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 doesnt look like a URL that needs processing
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
- return relateUrlInstance.relate(text);
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(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
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(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
359
+ const minified = value.replace(RE_NUMERIC_VALUE, (match) => {
359
360
  return minifyNumber(match, precision);
360
361
  });
361
362
  return minifyAttributeWhitespace(minified);