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.
- package/dist/htmlminifier.cjs +43 -11
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/htmlminifier.js +17 -3
- package/src/htmlparser.js +20 -4
- package/src/lib/options.js +6 -4
package/dist/htmlminifier.cjs
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
1737
|
-
const option =
|
|
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
|
-
|
|
3827
|
+
// Use `indexOf`-based O(n) loop instead of a global regex with [\s\S]*? to avoid O(n²)
|
|
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
|
-
|
|
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":"
|
|
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;
|
|
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,
|
|
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.
|
|
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.
|
|
98
|
+
"version": "5.1.5"
|
|
99
99
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -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
|
-
|
|
934
|
+
// Use `indexOf`-based O(n) loop instead of a global regex with [\s\S]*? to avoid O(n²)
|
|
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
|
-
|
|
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
|
|
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);
|
package/src/lib/options.js
CHANGED
|
@@ -66,19 +66,21 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
66
66
|
});
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
//
|
|
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
|
-
|
|
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(
|
|
81
|
-
const option =
|
|
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') {
|