html-minifier-next 4.16.2 → 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
@@ -359,24 +359,24 @@ How does HTML Minifier Next compare to other minifiers? (All minification with t
359
359
  | --- | --- | --- | --- | --- | --- | --- | --- |
360
360
  | [A List Apart](https://alistapart.com/) | 59 | **50** | 51 | 52 | 51 | 54 | 52 |
361
361
  | [Apple](https://www.apple.com/) | 211 | **176** | 187 | 189 | 190 | 191 | 192 |
362
- | [BBC](https://www.bbc.co.uk/) | 674 | **613** | 633 | 633 | 634 | 669 | n/a |
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/) | 1570 | 1458 | **1405** | 1494 | 1505 | 1516 | 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
- | [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** | 238 | 224 | 225 | 244 | 225 |
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/) | 2259 | **1978** | 2077 | 2107 | 2109 | 2246 | n/a |
373
+ | [Ground News](https://ground.news/) | 2291 | **2005** | 2106 | 2136 | 2139 | 2278 | n/a |
374
374
  | [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | 153 | **147** | 149 | 155 | 149 |
375
375
  | [Igalia](https://www.igalia.com/) | 50 | **33** | 36 | 36 | 36 | 37 | 37 |
376
376
  | [Leanpub](https://leanpub.com/) | 235 | **205** | 220 | 219 | 220 | 230 | 232 |
377
377
  | [Mastodon](https://mastodon.social/explore) | 37 | **28** | 32 | 35 | 35 | 36 | 36 |
378
378
  | [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | 64 | 65 | 65 | 68 | 68 |
379
- | [Middle East Eye](https://www.middleeasteye.net/) | 223 | **196** | 203 | 201 | 201 | 202 | 203 |
379
+ | [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 | 202 | 203 |
380
380
  | [Mistral AI](https://mistral.ai/) | 361 | **319** | 324 | 326 | 327 | 357 | n/a |
381
381
  | [Mozilla](https://www.mozilla.org/) | 45 | **31** | 34 | 34 | 34 | 35 | 35 |
382
382
  | [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 68 | **55** | 74 | 75 | 77 | 76 |
@@ -386,8 +386,8 @@ How does HTML Minifier Next compare to other minifiers? (All minification with t
386
386
  | [TPGi](https://www.tpgi.com/) | 174 | **158** | 159 | 163 | 165 | 171 | 171 |
387
387
  | [United Nations](https://www.un.org/en/) | 152 | **112** | 121 | 125 | 125 | 130 | 123 |
388
388
  | [Vivaldi](https://vivaldi.com/) | 92 | **74** | n/a | 79 | 81 | 83 | 81 |
389
- | [W3C](https://www.w3.org/) | 50 | **36** | 39 | 38 | 38 | 41 | 39 |
390
- | **Average processing time** | | 120 ms (30/30) | 143 ms (29/30) | 49 ms (30/30) | **13 ms (30/30)** | 270 ms (30/30) | 1252 ms (24/30) |
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) |
391
391
 
392
392
  (Last updated: Dec 27, 2025)
393
393
  <!-- End auto-generated -->
@@ -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();
@@ -1968,16 +1975,33 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
1968
1975
  // Cache RelateURL instance for reuse (expensive to create)
1969
1976
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
1970
1977
 
1978
+ // Create instance-specific cache (results depend on site configuration)
1979
+ const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
1980
+
1971
1981
  options.minifyURLs = function (text) {
1972
- // 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
1973
1983
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
1974
1984
  if (!/[/:?#\s]/.test(text)) {
1975
1985
  return text;
1976
1986
  }
1977
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
+
1978
1996
  try {
1979
- 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;
1980
2003
  } catch (err) {
2004
+ // Don’t cache errors
1981
2005
  if (!options.continueOnMinifyError) {
1982
2006
  throw err;
1983
2007
  }
@@ -2052,9 +2076,28 @@ function attributesInclude(attributes, attribute) {
2052
2076
  }
2053
2077
 
2054
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
2055
2098
  attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
2056
2099
 
2057
- // Legacy attributes
2100
+ // Legacy attribute checks
2058
2101
  if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
2059
2102
  return true;
2060
2103
  }
@@ -2066,12 +2109,12 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
2066
2109
  }
2067
2110
 
2068
2111
  // Check general defaults
2069
- if (generalDefaults[attrName] === attrValue) {
2112
+ if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
2070
2113
  return true;
2071
2114
  }
2072
2115
 
2073
2116
  // Check tag-specific defaults
2074
- return tagDefaults[tag]?.[attrName] === attrValue;
2117
+ return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
2075
2118
  }
2076
2119
 
2077
2120
  function isScriptTypeAttribute(attrValue = '') {
@@ -2216,15 +2259,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
2216
2259
  // Apply early whitespace normalization if enabled
2217
2260
  // Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
2218
2261
  if (options.collapseAttributeWhitespace) {
2219
- // Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
2220
- attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
2221
- // Leading whitespace (`offset === 0`)
2222
- if (offset === 0) return '';
2223
- // Trailing whitespace (match ends at string end)
2224
- if (offset + match.length === str.length) return '';
2225
- // Internal whitespace
2226
- return ' ';
2227
- });
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
+ }
2228
2269
  }
2229
2270
 
2230
2271
  if (isEventAttribute(attrName, options)) {
@@ -2756,6 +2797,7 @@ async function getSwc() {
2756
2797
 
2757
2798
  const cssMinifyCache = new LRU(500);
2758
2799
  const jsMinifyCache = new LRU(500);
2800
+ const urlMinifyCache = new LRU(500);
2759
2801
 
2760
2802
  // Type definitions
2761
2803
 
@@ -3823,10 +3865,10 @@ async function minifyHTML(value, options, partialMarkup) {
3823
3865
  // Note that `&` can be escaped as `&amp`, without the semi-colon.
3824
3866
  // https://mathiasbynens.be/notes/ambiguous-ampersands
3825
3867
  if (text.indexOf('&') !== -1) {
3826
- 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');
3827
3869
  }
3828
3870
  if (text.indexOf('<') !== -1) {
3829
- text = text.replace(/</g, '&lt;');
3871
+ text = text.replace(RE_ESCAPE_LT, '&lt;');
3830
3872
  }
3831
3873
  }
3832
3874
  if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
@@ -4035,7 +4077,8 @@ const minify = async function (value, options) {
4035
4077
  getTerser,
4036
4078
  getSwc,
4037
4079
  cssMinifyCache,
4038
- jsMinifyCache
4080
+ jsMinifyCache,
4081
+ urlMinifyCache
4039
4082
  });
4040
4083
  const result = await minifyHTML(value, options);
4041
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();
@@ -7110,16 +7117,33 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7110
7117
  // Cache RelateURL instance for reuse (expensive to create)
7111
7118
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
7112
7119
 
7120
+ // Create instance-specific cache (results depend on site configuration)
7121
+ const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
7122
+
7113
7123
  options.minifyURLs = function (text) {
7114
- // 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
7115
7125
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
7116
7126
  if (!/[/:?#\s]/.test(text)) {
7117
7127
  return text;
7118
7128
  }
7119
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
+
7120
7138
  try {
7121
- 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;
7122
7145
  } catch (err) {
7146
+ // Don’t cache errors
7123
7147
  if (!options.continueOnMinifyError) {
7124
7148
  throw err;
7125
7149
  }
@@ -7194,9 +7218,28 @@ function attributesInclude(attributes, attribute) {
7194
7218
  }
7195
7219
 
7196
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
7197
7240
  attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
7198
7241
 
7199
- // Legacy attributes
7242
+ // Legacy attribute checks
7200
7243
  if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
7201
7244
  return true;
7202
7245
  }
@@ -7208,12 +7251,12 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
7208
7251
  }
7209
7252
 
7210
7253
  // Check general defaults
7211
- if (generalDefaults[attrName] === attrValue) {
7254
+ if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
7212
7255
  return true;
7213
7256
  }
7214
7257
 
7215
7258
  // Check tag-specific defaults
7216
- return tagDefaults[tag]?.[attrName] === attrValue;
7259
+ return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
7217
7260
  }
7218
7261
 
7219
7262
  function isScriptTypeAttribute(attrValue = '') {
@@ -7358,15 +7401,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
7358
7401
  // Apply early whitespace normalization if enabled
7359
7402
  // Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
7360
7403
  if (options.collapseAttributeWhitespace) {
7361
- // Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
7362
- attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
7363
- // Leading whitespace (`offset === 0`)
7364
- if (offset === 0) return '';
7365
- // Trailing whitespace (match ends at string end)
7366
- if (offset + match.length === str.length) return '';
7367
- // Internal whitespace
7368
- return ' ';
7369
- });
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
+ }
7370
7411
  }
7371
7412
 
7372
7413
  if (isEventAttribute(attrName, options)) {
@@ -7898,6 +7939,7 @@ async function getSwc() {
7898
7939
 
7899
7940
  const cssMinifyCache = new LRU(500);
7900
7941
  const jsMinifyCache = new LRU(500);
7942
+ const urlMinifyCache = new LRU(500);
7901
7943
 
7902
7944
  // Type definitions
7903
7945
 
@@ -8965,10 +9007,10 @@ async function minifyHTML(value, options, partialMarkup) {
8965
9007
  // Note that `&` can be escaped as `&amp`, without the semi-colon.
8966
9008
  // https://mathiasbynens.be/notes/ambiguous-ampersands
8967
9009
  if (text.indexOf('&') !== -1) {
8968
- 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');
8969
9011
  }
8970
9012
  if (text.indexOf('<') !== -1) {
8971
- text = text.replace(/</g, '&lt;');
9013
+ text = text.replace(RE_ESCAPE_LT, '&lt;');
8972
9014
  }
8973
9015
  }
8974
9016
  if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
@@ -9177,7 +9219,8 @@ const minify$1 = async function (value, options) {
9177
9219
  getTerser,
9178
9220
  getSwc,
9179
9221
  cssMinifyCache,
9180
- jsMinifyCache
9222
+ jsMinifyCache,
9223
+ urlMinifyCache
9181
9224
  });
9182
9225
  const result = await minifyHTML(value, options);
9183
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,CA2T3B"}
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.2"
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,
@@ -93,6 +95,7 @@ async function getSwc() {
93
95
 
94
96
  const cssMinifyCache = new LRU(500);
95
97
  const jsMinifyCache = new LRU(500);
98
+ const urlMinifyCache = new LRU(500);
96
99
 
97
100
  // Type definitions
98
101
 
@@ -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();
@@ -310,16 +311,33 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
310
311
  // Cache RelateURL instance for reuse (expensive to create)
311
312
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
312
313
 
314
+ // Create instance-specific cache (results depend on site configuration)
315
+ const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
316
+
313
317
  options.minifyURLs = function (text) {
314
- // 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
315
319
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
316
320
  if (!/[/:?#\s]/.test(text)) {
317
321
  return text;
318
322
  }
319
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
+
320
332
  try {
321
- 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;
322
339
  } catch (err) {
340
+ // Don’t cache errors
323
341
  if (!options.continueOnMinifyError) {
324
342
  throw err;
325
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);