html-minifier-next 5.1.3 → 5.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -3806,7 +3822,19 @@ async function minifyHTML(value, options, partialMarkup) {
3806
3822
  // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there;
3807
3823
  // for all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
3808
3824
  if (value.indexOf('<!-- htmlmin:ignore -->') !== -1) {
3809
- value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
3825
+ // Use `indexOf`-based O(n) loop instead of a global regex with [\s\S]*? to avoid O()
3826
+ // backtracking on adversarial HTML with many `<!--` prefixes but no closing marker
3827
+ const ignoreMarker = '<!-- htmlmin:ignore -->';
3828
+ const ignoreMarkerLen = ignoreMarker.length;
3829
+ let ignoreResult = '';
3830
+ let ignorePos = 0;
3831
+ while (ignorePos < value.length) {
3832
+ const ignoreStart = value.indexOf(ignoreMarker, ignorePos);
3833
+ if (ignoreStart === -1) { ignoreResult += value.slice(ignorePos); break; }
3834
+ ignoreResult += value.slice(ignorePos, ignoreStart);
3835
+ const ignoreEnd = value.indexOf(ignoreMarker, ignoreStart + ignoreMarkerLen);
3836
+ if (ignoreEnd === -1) { ignoreResult += value.slice(ignoreStart); break; }
3837
+ const group1 = value.slice(ignoreStart + ignoreMarkerLen, ignoreEnd);
3810
3838
  if (!uidIgnore) {
3811
3839
  uidIgnore = uniqueId(value);
3812
3840
  const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
@@ -3820,8 +3848,10 @@ async function minifyHTML(value, options, partialMarkup) {
3820
3848
  }
3821
3849
  const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
3822
3850
  ignoredMarkupChunks.push(group1);
3823
- return token;
3824
- });
3851
+ ignoreResult += token;
3852
+ ignorePos = ignoreEnd + ignoreMarkerLen;
3853
+ }
3854
+ value = ignoreResult;
3825
3855
  }
3826
3856
 
3827
3857
  // 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"}
package/package.json CHANGED
@@ -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.4"
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);