html-minifier-next 4.16.3 → 4.17.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 +110 -70
- package/cli.js +27 -25
- package/dist/htmlminifier.cjs +264 -138
- package/dist/htmlminifier.esm.bundle.js +264 -138
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +16 -15
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +2 -2
- package/dist/types/lib/whitespace.d.ts +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -1
- package/package.json +13 -8
- package/src/htmlminifier.js +49 -47
- package/src/htmlparser.js +44 -13
- package/src/lib/attributes.js +72 -30
- package/src/lib/constants.js +46 -39
- package/src/lib/content.js +0 -1
- package/src/lib/elements.js +15 -15
- package/src/lib/options.js +9 -9
- package/src/lib/svg.js +14 -14
- package/src/lib/whitespace.js +53 -4
- package/src/presets.js +4 -4
- package/src/tokenchain.js +2 -2
- package/src/lib/index.js +0 -20
- package/src/utils.js +0 -11
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/html-minifier-next) [](https://github.com/j9t/html-minifier-next/actions) [](https://socket.dev/npm/package/html-minifier-next)
|
|
4
4
|
|
|
5
|
-
HTML Minifier Next (HMN) is a **super-configurable, well-tested, JavaScript-based HTML minifier
|
|
5
|
+
HTML Minifier Next (HMN) is a **super-configurable, well-tested, JavaScript-based HTML minifier** that can also handle in-document CSS, JavaScript, and SVG minification.
|
|
6
6
|
|
|
7
|
-
The project was based on [HTML Minifier Terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy “kangax” Zaytsev’s HTML Minifier](https://github.com/kangax/html-minifier)
|
|
7
|
+
The project was based on [HTML Minifier Terser (HMT)](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy “kangax” Zaytsev’s HTML Minifier (HM)](https://github.com/kangax/html-minifier); as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for several years. HMN offers additional features and has been optimized for speed. While an independent project, it is still backwards-compatible with HMT and HM.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
From npm for use as a command
|
|
11
|
+
From npm for use as a command-line app:
|
|
12
12
|
|
|
13
13
|
```shell
|
|
14
14
|
npm i -g html-minifier-next
|
|
@@ -39,15 +39,14 @@ Use `html-minifier-next --help` to check all available options:
|
|
|
39
39
|
| `--output-dir <dir>` | Specify an output directory | `--output-dir=dist` |
|
|
40
40
|
| `--file-ext <extensions>` | Specify file extension(s) to process (comma-separated, overrides config file setting) | `--file-ext=html`, `--file-ext=html,htm,php`, `--file-ext="html, htm, php"` |
|
|
41
41
|
| `-o <file>`, `--output <file>` | Specify output file (reads from file arguments or STDIN) | File to file: `html-minifier-next input.html -o output.html`<br>Pipe to file: `cat input.html \| html-minifier-next -o output.html`<br>File to STDOUT: `html-minifier-next input.html` |
|
|
42
|
-
| `-c <file>`, `--config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
|
|
43
42
|
| `--preset <name>` | Use a preset configuration (conservative or comprehensive) | `--preset=conservative` |
|
|
43
|
+
| `-c <file>`, `--config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
|
|
44
44
|
| `-v`, `--verbose` | Show detailed processing information (active options, file statistics) | `html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
|
|
45
45
|
| `-d`, `--dry` | Dry run: Process and report statistics without writing output | `html-minifier-next input.html --dry --collapse-whitespace` |
|
|
46
|
-
| `-V`, `--version` | Output the version number | `html-minifier-next --version` |
|
|
47
46
|
|
|
48
47
|
### Configuration file
|
|
49
48
|
|
|
50
|
-
You can
|
|
49
|
+
You can use a configuration file to specify options. The file can be either JSON format or a JavaScript module that exports the configuration object:
|
|
51
50
|
|
|
52
51
|
**JSON configuration example:**
|
|
53
52
|
|
|
@@ -71,16 +70,6 @@ module.exports = {
|
|
|
71
70
|
};
|
|
72
71
|
```
|
|
73
72
|
|
|
74
|
-
**Using a configuration file:**
|
|
75
|
-
|
|
76
|
-
```shell
|
|
77
|
-
# Specify config file
|
|
78
|
-
html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
|
|
79
|
-
|
|
80
|
-
# CLI arguments override config file settings
|
|
81
|
-
html-minifier-next --config-file=html-minifier.json --file-ext=xml --input-dir=src --output-dir=dist
|
|
82
|
-
```
|
|
83
|
-
|
|
84
73
|
### Node.js
|
|
85
74
|
|
|
86
75
|
ESM with Node.js ≥16.14:
|
|
@@ -88,35 +77,32 @@ ESM with Node.js ≥16.14:
|
|
|
88
77
|
```js
|
|
89
78
|
import { minify } from 'html-minifier-next';
|
|
90
79
|
|
|
91
|
-
const result = await minify('<p title="
|
|
80
|
+
const result = await minify('<p title="example" id="moo">foo</p>', {
|
|
92
81
|
removeAttributeQuotes: true,
|
|
82
|
+
removeOptionalTags: true
|
|
93
83
|
});
|
|
94
|
-
console.log(result); // “<p title=
|
|
84
|
+
console.log(result); // “<p title=example id=moo>foo”
|
|
95
85
|
```
|
|
96
86
|
|
|
97
87
|
CommonJS:
|
|
98
88
|
|
|
99
89
|
```js
|
|
100
|
-
const { minify } = require('html-minifier-next');
|
|
90
|
+
const { minify, getPreset } = require('html-minifier-next');
|
|
101
91
|
|
|
102
92
|
(async () => {
|
|
103
|
-
const result = await minify('<p title="
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
console.log(result);
|
|
93
|
+
const result = await minify('<p title="example" id="moo">foo</p>', getPreset('comprehensive'));
|
|
94
|
+
console.log(result); // “<p id=moo title=example>foo”
|
|
107
95
|
})();
|
|
108
96
|
```
|
|
109
97
|
|
|
110
|
-
See [the original blog post](https://perfectionkills.com/experimenting-with-html-minifier) for details of [how it works](https://perfectionkills.com/experimenting-with-html-minifier
|
|
111
|
-
|
|
112
|
-
For lint-like capabilities, take a look at [HTMLLint](https://github.com/kangax/html-lint).
|
|
98
|
+
See [the original blog post](https://perfectionkills.com/experimenting-with-html-minifier/) for details of [how it works](https://perfectionkills.com/experimenting-with-html-minifier/#how_it_works), [descriptions of most options](https://perfectionkills.com/experimenting-with-html-minifier/#options), [testing results](https://perfectionkills.com/experimenting-with-html-minifier/#field_testing), and [conclusions](https://perfectionkills.com/experimenting-with-html-minifier/#cost_and_benefits).
|
|
113
99
|
|
|
114
100
|
## Presets
|
|
115
101
|
|
|
116
102
|
HTML Minifier Next provides presets for common use cases. Presets are pre-configured option sets that can be used as a starting point:
|
|
117
103
|
|
|
118
104
|
* `conservative`: Safe minification suitable for most projects. Includes whitespace collapsing, comment removal, and doctype normalization.
|
|
119
|
-
* `comprehensive`:
|
|
105
|
+
* `comprehensive`: More aggressive minification for better file size reduction. Includes relevant conservative options plus attribute quote removal, optional tag removal, and more.
|
|
120
106
|
|
|
121
107
|
To review the specific options set, [presets.js](https://github.com/j9t/html-minifier-next/blob/main/src/presets.js) lists them in an accessible manner.
|
|
122
108
|
|
|
@@ -146,16 +132,16 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
146
132
|
| --- | --- | --- |
|
|
147
133
|
| `caseSensitive`<br>`--case-sensitive` | Treat attributes in case-sensitive manner (useful for custom HTML elements) | `false` |
|
|
148
134
|
| `collapseAttributeWhitespace`<br>`--collapse-attribute-whitespace` | Trim and collapse whitespace characters within attribute values | `false` |
|
|
149
|
-
| `collapseBooleanAttributes`<br>`--collapse-boolean-attributes` | [Omit attribute values from boolean attributes](https://perfectionkills.com/experimenting-with-html-minifier
|
|
150
|
-
| `collapseInlineTagWhitespace`<br>`--collapse-inline-tag-whitespace` |
|
|
151
|
-
| `collapseWhitespace`<br>`--collapse-whitespace` | [Collapse whitespace that contributes to text nodes in a document tree](https://perfectionkills.com/experimenting-with-html-minifier
|
|
135
|
+
| `collapseBooleanAttributes`<br>`--collapse-boolean-attributes` | [Omit attribute values from boolean attributes](https://perfectionkills.com/experimenting-with-html-minifier/#collapse_boolean_attributes) | `false` |
|
|
136
|
+
| `collapseInlineTagWhitespace`<br>`--collapse-inline-tag-whitespace` | Collapse whitespace more aggressively between inline elements—use with `collapseWhitespace: true` | `false` |
|
|
137
|
+
| `collapseWhitespace`<br>`--collapse-whitespace` | [Collapse whitespace that contributes to text nodes in a document tree](https://perfectionkills.com/experimenting-with-html-minifier/#collapse_whitespace) | `false` |
|
|
152
138
|
| `conservativeCollapse`<br>`--conservative-collapse` | Always collapse to one space (never remove it entirely)—use with `collapseWhitespace: true` | `false` |
|
|
153
139
|
| `continueOnMinifyError`<br>`--no-continue-on-minify-error` | Continue on minification errors; when `false`, minification errors throw and abort processing | `true` |
|
|
154
140
|
| `continueOnParseError`<br>`--continue-on-parse-error` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting | `false` |
|
|
155
|
-
| `customAttrAssign`<br>`--custom-attr-assign` |
|
|
156
|
-
| `customAttrCollapse`<br>`--custom-attr-collapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | |
|
|
157
|
-
| `customAttrSurround`<br>`--custom-attr-surround` |
|
|
158
|
-
| `customEventAttributes`<br>`--custom-event-attributes` |
|
|
141
|
+
| `customAttrAssign`<br>`--custom-attr-assign` | Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`) | `[]` |
|
|
142
|
+
| `customAttrCollapse`<br>`--custom-attr-collapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | `undefined` |
|
|
143
|
+
| `customAttrSurround`<br>`--custom-attr-surround` | Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
|
|
144
|
+
| `customEventAttributes`<br>`--custom-event-attributes` | Array of regexes that allow to support custom event attributes for `minifyJS` (e.g., `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
|
|
159
145
|
| `customFragmentQuantifierLimit`<br>`--custom-fragment-quantifier-limit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
|
|
160
146
|
| `decodeEntities`<br>`--decode-entities` | Use direct Unicode characters whenever possible | `false` |
|
|
161
147
|
| `html5`<br>`--no-html5` | Parse input according to the HTML specification; when `false`, enforces legacy inline/block nesting rules that may restructure modern HTML | `true` |
|
|
@@ -165,8 +151,8 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
165
151
|
| `inlineCustomElements`<br>`--inline-custom-elements` | Array of names of custom elements which are inline | `[]` |
|
|
166
152
|
| `keepClosingSlash`<br>`--keep-closing-slash` | Keep the trailing slash on void elements | `false` |
|
|
167
153
|
| `maxInputLength`<br>`--max-input-length` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
|
|
168
|
-
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points | |
|
|
169
|
-
| `minifyCSS`<br>`--minify-css` | Minify CSS in `style` elements and
|
|
154
|
+
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points | `undefined` |
|
|
155
|
+
| `minifyCSS`<br>`--minify-css` | Minify CSS in `style` elements and attributes (uses [Lightning CSS](https://lightningcss.dev/)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
|
|
170
156
|
| `minifyJS`<br>`--minify-js` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://github.com/terser/terser) or [SWC](https://swc.rs/)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
|
|
171
157
|
| `minifySVG`<br>`--minify-svg` | Minify SVG elements and attributes (numeric precision, default attributes, colors) | `false` (could be `true`, `Object`) |
|
|
172
158
|
| `minifyURLs`<br>`--minify-urls` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `String`, `Object`, `Function(text)`, `async Function(text)`) |
|
|
@@ -177,20 +163,20 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
177
163
|
| `processConditionalComments`<br>`--process-conditional-comments` | Process contents of conditional comments through minifier | `false` |
|
|
178
164
|
| `processScripts`<br>`--process-scripts` | Array of strings corresponding to types of `script` elements to process through minifier (e.g., `text/ng-template`, `text/x-handlebars-template`, etc.) | `[]` |
|
|
179
165
|
| `quoteCharacter`<br>`--quote-character` | Type of quote to use for attribute values (`'` or `"`) | Auto-detected (uses the quote requiring less escaping; defaults to `"` when equal) |
|
|
180
|
-
| `removeAttributeQuotes`<br>`--remove-attribute-quotes` | [Remove quotes around attributes when possible](https://perfectionkills.com/experimenting-with-html-minifier
|
|
181
|
-
| `removeComments`<br>`--remove-comments` | [Strip HTML comments](https://perfectionkills.com/experimenting-with-html-minifier
|
|
182
|
-
| `removeEmptyAttributes`<br>`--remove-empty-attributes` | [Remove all attributes with whitespace-only values](https://perfectionkills.com/experimenting-with-html-minifier
|
|
183
|
-
| `removeEmptyElements`<br>`--remove-empty-elements` | [Remove all elements with empty contents](https://perfectionkills.com/experimenting-with-html-minifier
|
|
166
|
+
| `removeAttributeQuotes`<br>`--remove-attribute-quotes` | [Remove quotes around attributes when possible](https://perfectionkills.com/experimenting-with-html-minifier/#remove_attribute_quotes) | `false` |
|
|
167
|
+
| `removeComments`<br>`--remove-comments` | [Strip HTML comments](https://perfectionkills.com/experimenting-with-html-minifier/#remove_comments) | `false` |
|
|
168
|
+
| `removeEmptyAttributes`<br>`--remove-empty-attributes` | [Remove all attributes with whitespace-only values](https://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_or_blank_attributes) | `false` (could be `true`, `Function(attrName, tag)`) |
|
|
169
|
+
| `removeEmptyElements`<br>`--remove-empty-elements` | [Remove all elements with empty contents](https://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_elements) | `false` |
|
|
184
170
|
| `removeEmptyElementsExcept`<br>`--remove-empty-elements-except` | Array of elements to preserve when `removeEmptyElements` is enabled; accepts simple tag names (e.g., `["td"]`) or HTML-like markup with attributes (e.g., `["<span aria-hidden='true'>"]`); supports double quotes, single quotes, and unquoted attribute values | `[]` |
|
|
185
|
-
| `removeOptionalTags`<br>`--remove-optional-tags` | [Remove optional tags](https://perfectionkills.com/experimenting-with-html-minifier
|
|
171
|
+
| `removeOptionalTags`<br>`--remove-optional-tags` | [Remove optional tags](https://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags) | `false` |
|
|
186
172
|
| `removeRedundantAttributes`<br>`--remove-redundant-attributes` | [Remove attributes when value matches default](https://meiert.com/blog/optional-html/#toc-attribute-values) | `false` |
|
|
187
173
|
| `removeScriptTypeAttributes`<br>`--remove-script-type-attributes` | Remove `type="text/javascript"` from `script` elements; other `type` attribute values are left intact | `false` |
|
|
188
174
|
| `removeStyleLinkTypeAttributes`<br>`--remove-style-link-type-attributes` | Remove `type="text/css"` from `style` and `link` elements; other `type` attribute values are left intact | `false` |
|
|
189
175
|
| `removeTagWhitespace`<br>`--remove-tag-whitespace` | Remove space between attributes whenever possible; **note that this will result in invalid HTML** | `false` |
|
|
190
176
|
| `sortAttributes`<br>`--sort-attributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
191
177
|
| `sortClassName`<br>`--sort-class-name` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
192
|
-
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around `ignoreCustomFragments` | `false` |
|
|
193
|
-
| `useShortDoctype`<br>`--use-short-doctype` | [Replaces the doctype with the short
|
|
178
|
+
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`) | `false` |
|
|
179
|
+
| `useShortDoctype`<br>`--use-short-doctype` | [Replaces the doctype with the short HTML doctype](https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype) | `false` |
|
|
194
180
|
|
|
195
181
|
### Sorting attributes and style classes
|
|
196
182
|
|
|
@@ -198,7 +184,7 @@ Minifier options like `sortAttributes` and `sortClassName` won’t impact the pl
|
|
|
198
184
|
|
|
199
185
|
### CSS minification
|
|
200
186
|
|
|
201
|
-
When `minifyCSS` is set to `true`, HTML Minifier Next uses [Lightning CSS](https://lightningcss.dev/) to minify CSS in
|
|
187
|
+
When `minifyCSS` is set to `true`, HTML Minifier Next uses [Lightning CSS](https://lightningcss.dev/) to minify CSS in `style` elements and attributes. Lightning CSS provides excellent minification by default.
|
|
202
188
|
|
|
203
189
|
You can pass Lightning CSS configuration options by providing an object:
|
|
204
190
|
|
|
@@ -357,39 +343,39 @@ How does HTML Minifier Next compare to other minifiers? (All minification with t
|
|
|
357
343
|
<!-- Auto-generated benchmarks, don’t edit -->
|
|
358
344
|
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next) ([config](https://github.com/j9t/html-minifier-next/blob/main/benchmarks/html-minifier.json))<br>[](https://socket.dev/npm/package/html-minifier-next) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
359
345
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
360
|
-
| [A List Apart](https://alistapart.com/) | 59 | **
|
|
361
|
-
| [Apple](https://www.apple.com/) |
|
|
362
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
363
|
-
| [CERN](https://home.cern/) |
|
|
364
|
-
| [CSS-Tricks](https://css-tricks.com/) |
|
|
346
|
+
| [A List Apart](https://alistapart.com/) | 59 | **49** | 51 | 52 | 51 | 54 | 52 |
|
|
347
|
+
| [Apple](https://www.apple.com/) | 259 | **201** | 230 | 234 | 234 | 236 | 237 |
|
|
348
|
+
| [BBC](https://www.bbc.co.uk/) | 647 | **586** | 608 | 608 | 609 | 642 | n/a |
|
|
349
|
+
| [CERN](https://home.cern/) | 151 | **83** | 91 | 91 | 91 | 93 | 96 |
|
|
350
|
+
| [CSS-Tricks](https://css-tricks.com/) | 161 | **119** | 127 | 142 | 142 | 147 | 144 |
|
|
365
351
|
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | **6401** | 6573 | 6455 | 6578 | 6626 | n/a |
|
|
366
352
|
| [EDRi](https://edri.org/) | 80 | **59** | 70 | 70 | 71 | 75 | 73 |
|
|
367
|
-
| [EFF](https://www.eff.org/) |
|
|
353
|
+
| [EFF](https://www.eff.org/) | 55 | **46** | 49 | 48 | 48 | 50 | 50 |
|
|
368
354
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
369
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
355
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1579 | 1468 | **1416** | 1503 | 1515 | 1526 | n/a |
|
|
370
356
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 153 | **122** | 126 | 126 | 126 | 132 | 127 |
|
|
371
|
-
| [Frontend Dogma](https://frontenddogma.com/) |
|
|
357
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 227 | **219** | 240 | 225 | 227 | 245 | 226 |
|
|
372
358
|
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 18 | 18 |
|
|
373
|
-
| [Ground News](https://ground.news/) |
|
|
359
|
+
| [Ground News](https://ground.news/) | 2626 | **2321** | 2417 | 2444 | 2446 | 2613 | n/a |
|
|
374
360
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | 153 | **147** | 149 | 155 | 149 |
|
|
375
|
-
| [Igalia](https://www.igalia.com/) |
|
|
376
|
-
| [Leanpub](https://leanpub.com/) |
|
|
361
|
+
| [Igalia](https://www.igalia.com/) | 48 | **33** | 35 | 35 | 35 | 36 | 36 |
|
|
362
|
+
| [Leanpub](https://leanpub.com/) | 245 | **214** | 228 | 228 | 229 | 241 | 242 |
|
|
377
363
|
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | 32 | 35 | 35 | 36 | 36 |
|
|
378
364
|
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | 64 | 65 | 65 | 68 | 68 |
|
|
379
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
380
|
-
| [Mistral AI](https://mistral.ai/) |
|
|
381
|
-
| [Mozilla](https://www.mozilla.org/) | 45 | **31** |
|
|
382
|
-
| [Nielsen Norman Group](https://www.nngroup.com/) |
|
|
383
|
-
| [SitePoint](https://www.sitepoint.com/) |
|
|
384
|
-
| [Startup-Verband](https://startupverband.de/) |
|
|
385
|
-
| [TetraLogical](https://tetralogical.com/) | 44 |
|
|
386
|
-
| [TPGi](https://www.tpgi.com/) |
|
|
387
|
-
| [United Nations](https://www.un.org/en/) | 152 | **
|
|
388
|
-
| [Vivaldi](https://vivaldi.com/) |
|
|
389
|
-
| [W3C](https://www.w3.org/) |
|
|
390
|
-
| **Average processing time** | |
|
|
391
|
-
|
|
392
|
-
(Last updated:
|
|
365
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 222 | **196** | 202 | 200 | 200 | 202 | 202 |
|
|
366
|
+
| [Mistral AI](https://mistral.ai/) | 356 | **313** | 318 | 322 | 323 | 352 | n/a |
|
|
367
|
+
| [Mozilla](https://www.mozilla.org/) | 45 | **31** | 35 | 34 | 34 | 35 | 35 |
|
|
368
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 93 | 70 | **57** | 75 | 77 | 78 | 77 |
|
|
369
|
+
| [SitePoint](https://www.sitepoint.com/) | 478 | **347** | 419 | 452 | 457 | 475 | n/a |
|
|
370
|
+
| [Startup-Verband](https://startupverband.de/) | 43 | **30** | 31 | **30** | 31 | 31 | 31 |
|
|
371
|
+
| [TetraLogical](https://tetralogical.com/) | 44 | 39 | **36** | 38 | 39 | 39 | 39 |
|
|
372
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **159** | 160 | 164 | 166 | 172 | 172 |
|
|
373
|
+
| [United Nations](https://www.un.org/en/) | 152 | **113** | 122 | 126 | 126 | 131 | 124 |
|
|
374
|
+
| [Vivaldi](https://vivaldi.com/) | 93 | **74** | n/a | 79 | 81 | 84 | 82 |
|
|
375
|
+
| [W3C](https://www.w3.org/) | 50 | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
376
|
+
| **Average processing time** | | 97 ms (30/30) | 157 ms (29/30) | 51 ms (30/30) | **15 ms (30/30)** | 289 ms (30/30) | 1288 ms (24/30) |
|
|
377
|
+
|
|
378
|
+
(Last updated: Jan 8, 2026)
|
|
393
379
|
<!-- End auto-generated -->
|
|
394
380
|
|
|
395
381
|
Notes: Minimize does not minify CSS and JS. [HTML Minifier Terser](https://github.com/terser/html-minifier-terser) is currently not included due to issues around whitespace collapsing and removal of code using modern CSS features, issues which appeared to distort the data.
|
|
@@ -404,7 +390,7 @@ Notes: Minimize does not minify CSS and JS. [HTML Minifier Terser](https://githu
|
|
|
404
390
|
html-minifier-next --collapse-whitespace --remove-comments --minify-js --input-dir=. --output-dir=example
|
|
405
391
|
```
|
|
406
392
|
|
|
407
|
-
|
|
393
|
+
Example using npx:
|
|
408
394
|
|
|
409
395
|
```shell
|
|
410
396
|
npx html-minifier-next --input-dir=test --file-ext html --preset comprehensive --output-dir example
|
|
@@ -552,6 +538,44 @@ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
|
552
538
|
|
|
553
539
|
**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.
|
|
554
540
|
|
|
541
|
+
##### Escaping patterns in different contexts
|
|
542
|
+
|
|
543
|
+
The escaping requirements for `ignoreCustomFragments` patterns differ depending on how you’re using HMN:
|
|
544
|
+
|
|
545
|
+
**Config file (JSON):**
|
|
546
|
+
|
|
547
|
+
```json
|
|
548
|
+
{
|
|
549
|
+
"ignoreCustomFragments": ["\\{%[\\s\\S]{0,1000}?%\\}", "\\{\\{[\\s\\S]{0,500}?\\}\\}"]
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Programmatic (JavaScript/Node.js):**
|
|
554
|
+
|
|
555
|
+
```javascript
|
|
556
|
+
ignoreCustomFragments: [/\{%[\s\S]{0,1000}?%\}/, /\{\{[\s\S]{0,500}?\}\}/]
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**CLI (via config file—recommended):**
|
|
560
|
+
|
|
561
|
+
```shell
|
|
562
|
+
html-minifier-next --config-file=config.json input.html
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**CLI (inline—not recommended due to complex escaping):**
|
|
566
|
+
|
|
567
|
+
```shell
|
|
568
|
+
html-minifier-next --ignore-custom-fragments '[\\\"\\\\{%[\\\\s\\\\S]{0,1000}?%\\\\}\\\"]' input.html
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
For CLI usage, using a config file is strongly recommended to avoid complex shell and JSON escaping.
|
|
572
|
+
|
|
573
|
+
**[Web demo:](https://j9t.github.io/html-minifier-next/)**
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
\{%[\s\S]{0,1000}?%\} \{\{[\s\S]{0,500}?\}\}
|
|
577
|
+
```
|
|
578
|
+
|
|
555
579
|
## Running HTML Minifier Next locally
|
|
556
580
|
|
|
557
581
|
### Local server
|
|
@@ -572,6 +596,22 @@ npm run benchmarks
|
|
|
572
596
|
|
|
573
597
|
(In case of dependency conflicts, run `npm i` with the `--legacy-peer-deps` flag.)
|
|
574
598
|
|
|
599
|
+
### Regression tests
|
|
600
|
+
|
|
601
|
+
```shell
|
|
602
|
+
cd benchmarks;
|
|
603
|
+
npm i;
|
|
604
|
+
npm run backtest
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
The backtest tool tracks minification performance across Git history. Results are saved in `benchmarks/backtest/` as CSV and JSON files.
|
|
608
|
+
|
|
609
|
+
Parameters:
|
|
610
|
+
|
|
611
|
+
* No argument: Tests last 50 commits (default)
|
|
612
|
+
* `COUNT`: Tests last `COUNT` commits (e.g., `npm run backtest 100`)
|
|
613
|
+
* `COUNT/STEP`: Tests last `COUNT` commits, sampling every `STEP`th commit (e.g., `npm run backtest 500/10` tests 50 commits)
|
|
614
|
+
|
|
575
615
|
## Acknowledgements
|
|
576
616
|
|
|
577
617
|
With many thanks to all the previous authors of HTML Minifier, especially [Juriy “kangax” Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, particularly [Daniel Ruf](https://github.com/DanielRuf) and [Jonas Geiler](https://github.com/jonasgeiler).
|
package/cli.js
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The MIT License (MIT)
|
|
6
6
|
*
|
|
7
|
-
* Copyright
|
|
7
|
+
* Copyright 2014–2016 Zoltan Frombach
|
|
8
|
+
*
|
|
9
|
+
* Copyright Juriy “kangax” Zaytsev
|
|
10
|
+
*
|
|
11
|
+
* Copyright 2025 Jens Oliver Meiert
|
|
8
12
|
*
|
|
9
13
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
10
14
|
* this software and associated documentation files (the "Software"), to deal in
|
|
@@ -33,7 +37,6 @@ import { createRequire } from 'module';
|
|
|
33
37
|
import { camelCase, paramCase } from 'change-case';
|
|
34
38
|
import { Command } from 'commander';
|
|
35
39
|
// Lazy-load HMN to reduce CLI cold-start overhead
|
|
36
|
-
// import { minify } from './src/htmlminifier.js';
|
|
37
40
|
import { getPreset, getPresetNames } from './src/presets.js';
|
|
38
41
|
|
|
39
42
|
const require = createRequire(import.meta.url);
|
|
@@ -57,7 +60,7 @@ process.stdout.on('error', (err) => {
|
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* JSON does not support regexes, so, e.g., JSON.parse() will not create
|
|
63
|
+
* JSON does not support regexes, so, e.g., `JSON.parse()` will not create
|
|
61
64
|
* a RegExp from the JSON value `[ "/matchString/" ]`, which is
|
|
62
65
|
* technically just an array containing a string that begins and end with
|
|
63
66
|
* a forward slash. To get a RegExp from a JSON string, it must be
|
|
@@ -121,53 +124,53 @@ const mainOptions = {
|
|
|
121
124
|
caseSensitive: 'Treat attributes in case-sensitive manner (useful for custom HTML elements)',
|
|
122
125
|
collapseAttributeWhitespace: 'Trim and collapse whitespace characters within attribute values',
|
|
123
126
|
collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
|
|
124
|
-
collapseInlineTagWhitespace: '
|
|
127
|
+
collapseInlineTagWhitespace: 'Collapse whitespace more aggressively between inline elements—use with `--collapse-whitespace`',
|
|
125
128
|
collapseWhitespace: 'Collapse whitespace that contributes to text nodes in a document tree',
|
|
126
|
-
conservativeCollapse: 'Always collapse to one space (never remove it entirely)—use with
|
|
129
|
+
conservativeCollapse: 'Always collapse to one space (never remove it entirely)—use with `--collapse-whitespace`',
|
|
127
130
|
continueOnMinifyError: 'Abort on minification errors',
|
|
128
131
|
continueOnParseError: 'Handle parse errors instead of aborting',
|
|
129
|
-
customAttrAssign: ['
|
|
132
|
+
customAttrAssign: ['Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`)', parseJSONRegExpArray],
|
|
130
133
|
customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g., /ng-class/)', parseRegExp],
|
|
131
|
-
customAttrSurround: ['
|
|
132
|
-
customEventAttributes: ['
|
|
134
|
+
customAttrSurround: ['Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`)', parseJSONRegExpArray],
|
|
135
|
+
customEventAttributes: ['Array of regexes that allow to support custom event attributes for minifyJS (e.g., `ng-click`)', parseJSONRegExpArray],
|
|
133
136
|
customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseValidInt('customFragmentQuantifierLimit')],
|
|
134
137
|
decodeEntities: 'Use direct Unicode characters whenever possible',
|
|
135
138
|
html5: 'Don’t parse input according to the HTML specification (not recommended for modern HTML)',
|
|
136
139
|
ignoreCustomComments: ['Array of regexes that allow to ignore certain comments, when matched', parseJSONRegExpArray],
|
|
137
|
-
ignoreCustomFragments: ['Array of regexes that allow to ignore certain fragments, when matched (e.g.,
|
|
140
|
+
ignoreCustomFragments: ['Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`)', parseJSONRegExpArray],
|
|
138
141
|
includeAutoGeneratedTags: 'Don’t insert elements generated by HTML parser',
|
|
139
142
|
inlineCustomElements: ['Array of names of custom elements which are inline', parseJSONArray],
|
|
140
143
|
keepClosingSlash: 'Keep the trailing slash on void elements',
|
|
141
144
|
maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseValidInt('maxInputLength')],
|
|
142
145
|
maxLineLength: ['Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points', parseValidInt('maxLineLength')],
|
|
143
|
-
minifyCSS: ['Minify CSS in
|
|
144
|
-
minifyJS: ['Minify JavaScript in
|
|
146
|
+
minifyCSS: ['Minify CSS in `style` elements and attributes (uses Lightning CSS)', parseJSON],
|
|
147
|
+
minifyJS: ['Minify JavaScript in `script` elements and event attributes (uses Terser or SWC; pass `{"engine": "swc"}` for SWC)', parseJSON],
|
|
145
148
|
minifySVG: ['Minify SVG elements and attributes (numeric precision, default attributes, colors)', parseJSON],
|
|
146
149
|
minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
|
|
147
150
|
noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
|
|
148
151
|
partialMarkup: 'Treat input as a partial HTML fragment, preserving stray end tags and unclosed tags',
|
|
149
|
-
preserveLineBreaks: 'Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with
|
|
152
|
+
preserveLineBreaks: 'Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with `--collapse-whitespace`',
|
|
150
153
|
preventAttributesEscaping: 'Prevents the escaping of the values of attributes',
|
|
151
154
|
processConditionalComments: 'Process contents of conditional comments through minifier',
|
|
152
|
-
processScripts: ['Array of strings corresponding to types of
|
|
155
|
+
processScripts: ['Array of strings corresponding to types of `script` elements to process through minifier (e.g., `text/ng-template`, `text/x-handlebars-template`, etc.)', parseJSONArray],
|
|
153
156
|
quoteCharacter: ['Type of quote to use for attribute values (“\'” or “"”)', parseString],
|
|
154
157
|
removeAttributeQuotes: 'Remove quotes around attributes when possible',
|
|
155
158
|
removeComments: 'Strip HTML comments',
|
|
156
159
|
removeEmptyAttributes: 'Remove all attributes with whitespace-only values',
|
|
157
160
|
removeEmptyElements: 'Remove all elements with empty contents',
|
|
158
|
-
removeEmptyElementsExcept: ['Array of elements to preserve when
|
|
161
|
+
removeEmptyElementsExcept: ['Array of elements to preserve when `--remove-empty-elements` is enabled (e.g., `td`, `["td", "<span aria-hidden=\'true\'>"]`)', parseJSONArray],
|
|
159
162
|
removeOptionalTags: 'Remove unrequired tags',
|
|
160
163
|
removeRedundantAttributes: 'Remove attributes when value matches default',
|
|
161
|
-
removeScriptTypeAttributes: 'Remove
|
|
162
|
-
removeStyleLinkTypeAttributes: 'Remove
|
|
164
|
+
removeScriptTypeAttributes: 'Remove `type="text/javascript"` from `script` elements; other `type` attribute values are left intact',
|
|
165
|
+
removeStyleLinkTypeAttributes: 'Remove `type="text/css"` from `style` and `link` elements; other `type` attribute values are left intact',
|
|
163
166
|
removeTagWhitespace: 'Remove space between attributes whenever possible; note that this will result in invalid HTML',
|
|
164
167
|
sortAttributes: 'Sort attributes by frequency',
|
|
165
168
|
sortClassName: 'Sort style classes by frequency',
|
|
166
|
-
trimCustomFragments: 'Trim whitespace around
|
|
167
|
-
useShortDoctype: 'Replaces the doctype with the short
|
|
169
|
+
trimCustomFragments: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`)',
|
|
170
|
+
useShortDoctype: 'Replaces the doctype with the short HTML doctype'
|
|
168
171
|
};
|
|
169
172
|
|
|
170
|
-
// Configure command
|
|
173
|
+
// Configure command-line flags
|
|
171
174
|
const mainOptionKeys = Object.keys(mainOptions);
|
|
172
175
|
mainOptionKeys.forEach(function (key) {
|
|
173
176
|
const option = mainOptions[key];
|
|
@@ -183,7 +186,7 @@ mainOptionKeys.forEach(function (key) {
|
|
|
183
186
|
});
|
|
184
187
|
program.option('-o --output <file>', 'Specify output file (reads from file arguments or STDIN; outputs to STDOUT if not specified)');
|
|
185
188
|
program.option('-v --verbose', 'Show detailed processing information');
|
|
186
|
-
program.option('-d --dry', 'Dry run:
|
|
189
|
+
program.option('-d --dry', 'Dry run: Process and report statistics without writing output');
|
|
187
190
|
|
|
188
191
|
// Lazy import wrapper for HMN
|
|
189
192
|
let minifyFnPromise;
|
|
@@ -219,7 +222,7 @@ async function loadConfigFromPath(configPath) {
|
|
|
219
222
|
// Try CJS require
|
|
220
223
|
try {
|
|
221
224
|
const result = require(abs);
|
|
222
|
-
// Handle ESM interop:
|
|
225
|
+
// Handle ESM interop: If `require()` loads an ESM file, it may return `{__esModule: true, default: …}`
|
|
223
226
|
return (result && result.__esModule && result.default) ? result.default : result;
|
|
224
227
|
} catch (cjsErr) {
|
|
225
228
|
// Try ESM import
|
|
@@ -252,7 +255,7 @@ function normalizeConfig(config) {
|
|
|
252
255
|
}
|
|
253
256
|
});
|
|
254
257
|
|
|
255
|
-
// Handle fileExt in config file
|
|
258
|
+
// Handle `fileExt` in config file
|
|
256
259
|
if ('fileExt' in normalized) {
|
|
257
260
|
// Support both string (`html,htm`) and array (`["html", "htm"]`) formats
|
|
258
261
|
if (Array.isArray(normalized.fileExt)) {
|
|
@@ -445,8 +448,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
445
448
|
|
|
446
449
|
return ignorePatterns.some(pattern => {
|
|
447
450
|
// Support both exact directory names and relative paths
|
|
448
|
-
return dirName === pattern || relativePath === pattern ||
|
|
449
|
-
relativePath.startsWith(pattern + '/');
|
|
451
|
+
return dirName === pattern || relativePath === pattern || relativePath.startsWith(pattern + '/');
|
|
450
452
|
});
|
|
451
453
|
}
|
|
452
454
|
|
|
@@ -697,7 +699,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
697
699
|
}
|
|
698
700
|
|
|
699
701
|
// Resolve base directory for consistent path comparisons
|
|
700
|
-
const inputDirResolved =
|
|
702
|
+
const inputDirResolved = inputReal || inputDir;
|
|
701
703
|
|
|
702
704
|
if (showProgress) {
|
|
703
705
|
// Start with indeterminate progress, count in background
|