html-minifier-next 5.1.3 → 5.1.5

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.
@@ -185,6 +185,23 @@ const preCompiledStackedTags = {
185
185
  // Cache for compiled attribute regexes per handler configuration
186
186
  const attrRegexCache = new WeakMap();
187
187
 
188
+ // O(n) helper: Strip all occurrences of `open…close` delimiters, keeping inner content
189
+ // Used instead of a regex replace to avoid O(n²) behavior on adversarial inputs
190
+ function stripDelimited(str, open, close) {
191
+ let result = '';
192
+ let i = 0;
193
+ while (i < str.length) {
194
+ const start = str.indexOf(open, i);
195
+ if (start === -1) { result += str.slice(i); break; }
196
+ result += str.slice(i, start);
197
+ const end = str.indexOf(close, start + open.length);
198
+ if (end === -1) { result += str.slice(start); break; }
199
+ result += str.slice(start + open.length, end);
200
+ i = end + close.length;
201
+ }
202
+ return result;
203
+ }
204
+
188
205
  function buildAttrRegex(handler) {
189
206
  let pattern = singleAttrIdentifier.source +
190
207
  '(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
@@ -256,9 +273,10 @@ class HTMLParser {
256
273
 
257
274
  // Sticky regex versions for position-based matching (avoids string slicing)
258
275
  const startTagOpenY = new RegExp(startTagOpen.source.slice(1), 'y');
276
+ // `\s*` with sticky flag is O(n) at worst—no retry from different positions possible
259
277
  const startTagCloseY = /\s*(\/?)>/y;
260
278
  const endTagY = new RegExp(endTag.source.slice(1), 'y');
261
- const doctypeY = /<!DOCTYPE\s?[^>]+>/iy;
279
+ const doctypeY = /<!DOCTYPE[^<>]+>/iy;
262
280
  const commentTestY = /<!--/y;
263
281
  const conditionalTestY = /<!\[/y;
264
282
 
@@ -444,9 +462,7 @@ class HTMLParser {
444
462
  if (m && m.index === 0) {
445
463
  let text = m[1];
446
464
  if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
447
- text = text
448
- .replace(/<!--([\s\S]*?)-->/g, '$1')
449
- .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
465
+ text = stripDelimited(stripDelimited(text, '<!--', '-->'), '<![CDATA[', ']]>');
450
466
  }
451
467
  if (handler.chars) {
452
468
  const result = handler.chars(text);
@@ -1722,19 +1738,21 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
1722
1738
  });
1723
1739
  };
1724
1740
 
1725
- // Apply preset first if specified (so user options can override preset values)
1741
+ // Merge preset with user options so all values go through normalization
1742
+ // User options take precedence over preset values
1743
+ let effectiveInput = inputOptions;
1726
1744
  if (inputOptions.preset) {
1727
1745
  const preset = getPreset(inputOptions.preset);
1728
1746
  if (preset) {
1729
- Object.assign(options, preset);
1747
+ effectiveInput = { ...preset, ...inputOptions };
1730
1748
  } else {
1731
1749
  const available = getPresetNames().join(', ');
1732
1750
  console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
1733
1751
  }
1734
1752
  }
1735
1753
 
1736
- Object.keys(inputOptions).forEach(function (key) {
1737
- const option = inputOptions[key];
1754
+ Object.keys(effectiveInput).forEach(function (key) {
1755
+ const option = effectiveInput[key];
1738
1756
 
1739
1757
  // Skip preset key—it’s already been processed
1740
1758
  if (key === 'preset') {
@@ -3806,7 +3824,19 @@ async function minifyHTML(value, options, partialMarkup) {
3806
3824
  // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there;
3807
3825
  // for all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
3808
3826
  if (value.indexOf('<!-- htmlmin:ignore -->') !== -1) {
3809
- value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
3827
+ // Use `indexOf`-based O(n) loop instead of a global regex with [\s\S]*? to avoid O()
3828
+ // backtracking on adversarial HTML with many `<!--` prefixes but no closing marker
3829
+ const ignoreMarker = '<!-- htmlmin:ignore -->';
3830
+ const ignoreMarkerLen = ignoreMarker.length;
3831
+ let ignoreResult = '';
3832
+ let ignorePos = 0;
3833
+ while (ignorePos < value.length) {
3834
+ const ignoreStart = value.indexOf(ignoreMarker, ignorePos);
3835
+ if (ignoreStart === -1) { ignoreResult += value.slice(ignorePos); break; }
3836
+ ignoreResult += value.slice(ignorePos, ignoreStart);
3837
+ const ignoreEnd = value.indexOf(ignoreMarker, ignoreStart + ignoreMarkerLen);
3838
+ if (ignoreEnd === -1) { ignoreResult += value.slice(ignoreStart); break; }
3839
+ const group1 = value.slice(ignoreStart + ignoreMarkerLen, ignoreEnd);
3810
3840
  if (!uidIgnore) {
3811
3841
  uidIgnore = uniqueId(value);
3812
3842
  const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
@@ -3820,8 +3850,10 @@ async function minifyHTML(value, options, partialMarkup) {
3820
3850
  }
3821
3851
  const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
3822
3852
  ignoredMarkupChunks.push(group1);
3823
- return token;
3824
- });
3853
+ ignoreResult += token;
3854
+ ignorePos = ignoreEnd + ignoreMarkerLen;
3855
+ }
3856
+ value = ignoreResult;
3825
3857
  }
3826
3858
 
3827
3859
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
@@ -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":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAgDA,4BAAkE;AAuFlE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAomBC;CACF"}
1
+ {"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAgDA,4BAAkE;AAwGlE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAmmBC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAYA,6DAUC;AAID;;;;;;;;;;;GAWG;AACH,6CAXW,OAAO,CAAC,eAAe,CAAC,mGAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAK9C,eAAe,CA6W3B"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAYA,6DAUC;AAID;;;;;;;;;;;GAWG;AACH,6CAXW,OAAO,CAAC,eAAe,CAAC,mGAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAK9C,eAAe,CA+W3B"}
package/package.json CHANGED
@@ -20,7 +20,7 @@
20
20
  "@rollup/plugin-node-resolve": "^16.0.3",
21
21
  "@swc/core": "^1.15.11",
22
22
  "eslint": "^10.0.0",
23
- "rollup": "^4.57.1",
23
+ "rollup": "^4.59.0",
24
24
  "rollup-plugin-polyfill-node": "^0.13.0",
25
25
  "typescript": "^5.9.3",
26
26
  "vite": "^7.3.1"
@@ -95,5 +95,5 @@
95
95
  },
96
96
  "type": "module",
97
97
  "types": "./dist/types/htmlminifier.d.ts",
98
- "version": "5.1.3"
98
+ "version": "5.1.5"
99
99
  }
@@ -931,7 +931,19 @@ async function minifyHTML(value, options, partialMarkup) {
931
931
  // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there;
932
932
  // for all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
933
933
  if (value.indexOf('<!-- htmlmin:ignore -->') !== -1) {
934
- value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
934
+ // Use `indexOf`-based O(n) loop instead of a global regex with [\s\S]*? to avoid O()
935
+ // backtracking on adversarial HTML with many `<!--` prefixes but no closing marker
936
+ const ignoreMarker = '<!-- htmlmin:ignore -->';
937
+ const ignoreMarkerLen = ignoreMarker.length;
938
+ let ignoreResult = '';
939
+ let ignorePos = 0;
940
+ while (ignorePos < value.length) {
941
+ const ignoreStart = value.indexOf(ignoreMarker, ignorePos);
942
+ if (ignoreStart === -1) { ignoreResult += value.slice(ignorePos); break; }
943
+ ignoreResult += value.slice(ignorePos, ignoreStart);
944
+ const ignoreEnd = value.indexOf(ignoreMarker, ignoreStart + ignoreMarkerLen);
945
+ if (ignoreEnd === -1) { ignoreResult += value.slice(ignoreStart); break; }
946
+ const group1 = value.slice(ignoreStart + ignoreMarkerLen, ignoreEnd);
935
947
  if (!uidIgnore) {
936
948
  uidIgnore = uniqueId(value);
937
949
  const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
@@ -945,8 +957,10 @@ async function minifyHTML(value, options, partialMarkup) {
945
957
  }
946
958
  const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
947
959
  ignoredMarkupChunks.push(group1);
948
- return token;
949
- });
960
+ ignoreResult += token;
961
+ ignorePos = ignoreEnd + ignoreMarkerLen;
962
+ }
963
+ value = ignoreResult;
950
964
  }
951
965
 
952
966
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
package/src/htmlparser.js CHANGED
@@ -82,6 +82,23 @@ const preCompiledStackedTags = {
82
82
  // Cache for compiled attribute regexes per handler configuration
83
83
  const attrRegexCache = new WeakMap();
84
84
 
85
+ // O(n) helper: Strip all occurrences of `open…close` delimiters, keeping inner content
86
+ // Used instead of a regex replace to avoid O(n²) behavior on adversarial inputs
87
+ function stripDelimited(str, open, close) {
88
+ let result = '';
89
+ let i = 0;
90
+ while (i < str.length) {
91
+ const start = str.indexOf(open, i);
92
+ if (start === -1) { result += str.slice(i); break; }
93
+ result += str.slice(i, start);
94
+ const end = str.indexOf(close, start + open.length);
95
+ if (end === -1) { result += str.slice(start); break; }
96
+ result += str.slice(start + open.length, end);
97
+ i = end + close.length;
98
+ }
99
+ return result;
100
+ }
101
+
85
102
  function buildAttrRegex(handler) {
86
103
  let pattern = singleAttrIdentifier.source +
87
104
  '(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
@@ -153,9 +170,10 @@ export class HTMLParser {
153
170
 
154
171
  // Sticky regex versions for position-based matching (avoids string slicing)
155
172
  const startTagOpenY = new RegExp(startTagOpen.source.slice(1), 'y');
173
+ // `\s*` with sticky flag is O(n) at worst—no retry from different positions possible
156
174
  const startTagCloseY = /\s*(\/?)>/y;
157
175
  const endTagY = new RegExp(endTag.source.slice(1), 'y');
158
- const doctypeY = /<!DOCTYPE\s?[^>]+>/iy;
176
+ const doctypeY = /<!DOCTYPE[^<>]+>/iy;
159
177
  const commentTestY = /<!--/y;
160
178
  const conditionalTestY = /<!\[/y;
161
179
 
@@ -343,9 +361,7 @@ export class HTMLParser {
343
361
  if (m && m.index === 0) {
344
362
  let text = m[1];
345
363
  if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
346
- text = text
347
- .replace(/<!--([\s\S]*?)-->/g, '$1')
348
- .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
364
+ text = stripDelimited(stripDelimited(text, '<!--', '-->'), '<![CDATA[', ']]>');
349
365
  }
350
366
  if (handler.chars) {
351
367
  const result = handler.chars(text);
@@ -66,19 +66,21 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
66
66
  });
67
67
  };
68
68
 
69
- // Apply preset first if specified (so user options can override preset values)
69
+ // Merge preset with user options so all values go through normalization
70
+ // User options take precedence over preset values
71
+ let effectiveInput = inputOptions;
70
72
  if (inputOptions.preset) {
71
73
  const preset = getPreset(inputOptions.preset);
72
74
  if (preset) {
73
- Object.assign(options, preset);
75
+ effectiveInput = { ...preset, ...inputOptions };
74
76
  } else {
75
77
  const available = getPresetNames().join(', ');
76
78
  console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
77
79
  }
78
80
  }
79
81
 
80
- Object.keys(inputOptions).forEach(function (key) {
81
- const option = inputOptions[key];
82
+ Object.keys(effectiveInput).forEach(function (key) {
83
+ const option = effectiveInput[key];
82
84
 
83
85
  // Skip preset key—it’s already been processed
84
86
  if (key === 'preset') {