html-minifier-next 1.0.0 → 1.1.0
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 +66 -2
- package/cli.js +3 -2
- package/package.json +14 -15
- package/src/htmlminifier.js +23 -2
- package/dist/htmlminifier.cjs +0 -1858
- package/dist/htmlminifier.esm.bundle.js +0 -59386
- package/dist/htmlminifier.umd.bundle.js +0 -59397
- package/dist/htmlminifier.umd.bundle.min.js +0 -9
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# HTML Minifier Next (HTMLMinifier)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/html-minifier-next)
|
|
3
4
|
<!-- [](https://github.com/j9t/html-minifier-next/actions?workflow=CI) -->
|
|
4
5
|
|
|
5
6
|
(This project is based on [Terser’s html-minifier-terser](https://github.com/terser/html-minifier-terser), which in turn is based on [Juriy Zaytsev’s html-minifier](https://github.com/kangax/html-minifier). It was set up because as of May 2025, both html-minifier-terser and html-minifier seem unmaintained. **This project is currently under test.** If it seems maintainable to me, [Jens](https://meiert.com/), even without community support, the project will be updated and documented further. The following documentation largely matches the original project.)
|
|
@@ -69,7 +70,7 @@ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Pe
|
|
|
69
70
|
| [W3C](https://www.w3.org/) | 51 | **36** | 42 | n/a |
|
|
70
71
|
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | 114 | **100** | 107 | n/a |
|
|
71
72
|
|
|
72
|
-
## Options
|
|
73
|
+
## Options quick reference
|
|
73
74
|
|
|
74
75
|
Most of the options are disabled by default.
|
|
75
76
|
|
|
@@ -77,6 +78,7 @@ Most of the options are disabled by default.
|
|
|
77
78
|
| --- | --- | --- |
|
|
78
79
|
| `caseSensitive` | Treat attributes in case sensitive manner (useful for custom HTML tags) | `false` |
|
|
79
80
|
| `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` |
|
|
81
|
+
| `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
|
|
80
82
|
| `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
|
|
81
83
|
| `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` |
|
|
82
84
|
| `conservativeCollapse` | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` |
|
|
@@ -91,6 +93,7 @@ Most of the options are disabled by default.
|
|
|
91
93
|
| `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
|
|
92
94
|
| `includeAutoGeneratedTags` | Insert tags generated by HTML parser | `true` |
|
|
93
95
|
| `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` |
|
|
96
|
+
| `maxInputLength` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
|
|
94
97
|
| `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points |
|
|
95
98
|
| `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
|
|
96
99
|
| `minifyJS` | Minify JavaScript in script elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
|
|
@@ -153,6 +156,63 @@ Output of resulting markup (e.g. `<p>foo</p>`)
|
|
|
153
156
|
|
|
154
157
|
HTMLMinifier can’t know that original markup was only half of the tree; it does its best to try to parse it as a full tree and it loses information about tree being malformed or partial in the beginning. As a result, it can’t create a partial/malformed tree at the time of the output.
|
|
155
158
|
|
|
159
|
+
## Security
|
|
160
|
+
|
|
161
|
+
### ReDoS protection
|
|
162
|
+
|
|
163
|
+
This minifier includes protection against regular expression denial of service (ReDoS) attacks:
|
|
164
|
+
|
|
165
|
+
* Custom fragment quantifier limits: The `customFragmentQuantifierLimit` option (default: 200) prevents exponential backtracking by replacing unlimited quantifiers (`*`, `+`) with bounded ones in regular expressions.
|
|
166
|
+
|
|
167
|
+
* Input length limits: The `maxInputLength` option allows you to set a maximum input size to prevent processing of excessively large inputs that could cause performance issues.
|
|
168
|
+
|
|
169
|
+
* Enhanced pattern detection: The minifier detects and warns about various ReDoS-prone patterns including nested quantifiers, alternation with quantifiers, and multiple unlimited quantifiers.
|
|
170
|
+
|
|
171
|
+
**Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities.
|
|
172
|
+
|
|
173
|
+
(Further improvements are needed. Contributions welcome.)
|
|
174
|
+
|
|
175
|
+
#### Custom fragment examples
|
|
176
|
+
|
|
177
|
+
**Safe patterns** (recommended):
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
ignoreCustomFragments: [
|
|
181
|
+
/<%[\s\S]{0,1000}?%>/, // JSP/ASP with explicit bounds
|
|
182
|
+
/<\?php[\s\S]{0,5000}?\?>/, // PHP with bounds
|
|
183
|
+
/\{\{[^}]{0,500}\}\}/ // Handlebars without nested braces
|
|
184
|
+
]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Potentially unsafe patterns** (will trigger warnings):
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
ignoreCustomFragments: [
|
|
191
|
+
/<%[\s\S]*?%>/, // Unlimited quantifiers
|
|
192
|
+
/<!--[\s\S]*?-->/, // Could cause issues with very long comments
|
|
193
|
+
/\{\{.*?\}\}/, // Nested unlimited quantifiers
|
|
194
|
+
/(script|style)[\s\S]*?/ // Multiple unlimited quantifiers
|
|
195
|
+
]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Template engine configurations:**
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// Handlebars/Mustache
|
|
202
|
+
ignoreCustomFragments: [/\{\{[\s\S]{0,1000}?\}\}/]
|
|
203
|
+
|
|
204
|
+
// Liquid (Jekyll)
|
|
205
|
+
ignoreCustomFragments: [/\{%[\s\S]{0,500}?%\}/, /\{\{[\s\S]{0,500}?\}\}/]
|
|
206
|
+
|
|
207
|
+
// Angular
|
|
208
|
+
ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
209
|
+
|
|
210
|
+
// Vue.js
|
|
211
|
+
ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Important:** When using custom `ignoreCustomFragments`, the minifier automatically applies bounded quantifiers to prevent ReDoS attacks, but you can also write safer patterns yourself using explicit bounds.
|
|
215
|
+
|
|
156
216
|
## Running benchmarks
|
|
157
217
|
|
|
158
218
|
Benchmarks for minified HTML:
|
|
@@ -167,4 +227,8 @@ npm run benchmark
|
|
|
167
227
|
|
|
168
228
|
```shell
|
|
169
229
|
npm run serve
|
|
170
|
-
```
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Acknowledgements
|
|
233
|
+
|
|
234
|
+
With many thanks to all the previous authors of HTML Minifier, especially [Juriy Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, like [Daniel Ruf](https://github.com/DanielRuf).
|
package/cli.js
CHANGED
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
import fs from 'fs';
|
|
29
29
|
import path from 'path';
|
|
30
30
|
import { createRequire } from 'module';
|
|
31
|
-
import { camelCase } from '
|
|
32
|
-
import { paramCase } from 'param-case';
|
|
31
|
+
import { camelCase, paramCase } from 'change-case';
|
|
33
32
|
import { Command } from 'commander';
|
|
34
33
|
import { minify } from './src/htmlminifier.js';
|
|
35
34
|
|
|
@@ -102,6 +101,7 @@ function parseString(value) {
|
|
|
102
101
|
const mainOptions = {
|
|
103
102
|
caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)',
|
|
104
103
|
collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
|
|
104
|
+
customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseInt],
|
|
105
105
|
collapseInlineTagWhitespace: 'Collapse white space around inline tag',
|
|
106
106
|
collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.',
|
|
107
107
|
conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)',
|
|
@@ -116,6 +116,7 @@ const mainOptions = {
|
|
|
116
116
|
ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. <?php ... ?>, {{ ... }})', parseJSONRegExpArray],
|
|
117
117
|
includeAutoGeneratedTags: 'Insert tags generated by HTML parser',
|
|
118
118
|
keepClosingSlash: 'Keep the trailing slash on singleton elements',
|
|
119
|
+
maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseInt],
|
|
119
120
|
maxLineLength: ['Max line length', parseInt],
|
|
120
121
|
minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
|
|
121
122
|
minifyJS: ['Minify Javascript in script elements and on* attributes', parseJSON],
|
package/package.json
CHANGED
|
@@ -5,32 +5,31 @@
|
|
|
5
5
|
},
|
|
6
6
|
"bugs": "https://github.com/j9t/html-minifier-next/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"
|
|
9
|
-
"clean-css": "~5.3.
|
|
10
|
-
"commander": "^
|
|
11
|
-
"entities": "^6.0.
|
|
12
|
-
"param-case": "^4.0.0",
|
|
8
|
+
"change-case": "^4.1.2",
|
|
9
|
+
"clean-css": "~5.3.3",
|
|
10
|
+
"commander": "^14.0.0",
|
|
11
|
+
"entities": "^6.0.1",
|
|
13
12
|
"relateurl": "^0.2.7",
|
|
14
|
-
"terser": "^5.
|
|
13
|
+
"terser": "^5.39.2"
|
|
15
14
|
},
|
|
16
15
|
"description": "Highly configurable, well-tested, JavaScript-based HTML minifier.",
|
|
17
16
|
"devDependencies": {
|
|
18
17
|
"@commitlint/cli": "^19.8.1",
|
|
19
|
-
"@jest/globals": "^29.
|
|
18
|
+
"@jest/globals": "^29.7.0",
|
|
20
19
|
"@rollup/plugin-commonjs": "^28.0.3",
|
|
21
|
-
"@rollup/plugin-json": "^6.
|
|
20
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
22
21
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
23
|
-
"@rollup/plugin-terser": "^0.4.
|
|
24
|
-
"alpinejs": "^3.
|
|
22
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
23
|
+
"alpinejs": "^3.14.9",
|
|
25
24
|
"commitlint-config-non-conventional": "^1.0.1",
|
|
26
25
|
"eslint": "^8.57.1",
|
|
27
26
|
"eslint-config-standard": "^17.1.0",
|
|
28
27
|
"husky": "^9.1.7",
|
|
29
28
|
"is-ci": "^4.1.0",
|
|
30
|
-
"jest": "^29.
|
|
31
|
-
"jest-environment-jsdom": "^29.
|
|
32
|
-
"lint-staged": "^16.
|
|
33
|
-
"rollup": "^
|
|
29
|
+
"jest": "^29.7.0",
|
|
30
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
31
|
+
"lint-staged": "^16.1.0",
|
|
32
|
+
"rollup": "^4.41.1",
|
|
34
33
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
35
34
|
"vite": "^6.3.5"
|
|
36
35
|
},
|
|
@@ -91,5 +90,5 @@
|
|
|
91
90
|
"test:web": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --verbose --environment=jsdom"
|
|
92
91
|
},
|
|
93
92
|
"type": "module",
|
|
94
|
-
"version": "1.
|
|
93
|
+
"version": "1.1.0"
|
|
95
94
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -844,6 +844,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr) {
|
|
|
844
844
|
}
|
|
845
845
|
|
|
846
846
|
async function minifyHTML(value, options, partialMarkup) {
|
|
847
|
+
// Check input length limitation to prevent ReDoS attacks
|
|
848
|
+
if (options.maxInputLength && value.length > options.maxInputLength) {
|
|
849
|
+
throw new Error(`Input length (${value.length}) exceeds maximum allowed length (${options.maxInputLength})`);
|
|
850
|
+
}
|
|
851
|
+
|
|
847
852
|
if (options.collapseWhitespace) {
|
|
848
853
|
value = collapseWhitespace(value, options, true, true);
|
|
849
854
|
}
|
|
@@ -888,8 +893,24 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
888
893
|
return re.source;
|
|
889
894
|
});
|
|
890
895
|
if (customFragments.length) {
|
|
891
|
-
|
|
892
|
-
|
|
896
|
+
// Warn about potential ReDoS if custom fragments use unlimited quantifiers
|
|
897
|
+
for (let i = 0; i < customFragments.length; i++) {
|
|
898
|
+
if (/[*+]/.test(customFragments[i])) {
|
|
899
|
+
options.log('Warning: Custom fragment contains unlimited quantifiers (* or +) which may cause ReDoS vulnerability');
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS
|
|
905
|
+
const maxQuantifier = options.customFragmentQuantifierLimit || 200;
|
|
906
|
+
const whitespacePattern = `\\s{0,${maxQuantifier}}`;
|
|
907
|
+
|
|
908
|
+
// Use bounded quantifiers to prevent ReDoS - this approach prevents exponential backtracking
|
|
909
|
+
const reCustomIgnore = new RegExp(
|
|
910
|
+
whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
|
|
911
|
+
'g'
|
|
912
|
+
);
|
|
913
|
+
// Temporarily replace custom ignored fragments with unique attributes
|
|
893
914
|
value = value.replace(reCustomIgnore, function (match) {
|
|
894
915
|
if (!uidAttr) {
|
|
895
916
|
uidAttr = uniqueId(value);
|