html-minifier-next 4.16.4 → 4.17.1

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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next) [![Build status](https://github.com/j9t/html-minifier-next/workflows/Tests/badge.svg)](https://github.com/j9t/html-minifier-next/actions) [![Socket](https://badge.socket.dev/npm/package/html-minifier-next)](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). HMN offers additional features, but is backwards-compatible with both. The project was set up because as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for a few years. As the project seems maintainable [to me, [Jens](https://meiert.com/), an HTML optimizer]—even more so with community support—, it’s being [updated, extended, and documented](https://github.com/j9t/html-minifier-next/blob/main/CHANGELOG.md) further in this place.
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 line app:
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 also use a configuration file to specify options. The file can be either JSON format or a JavaScript module that exports the configuration object:
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="blah" id="moo">foo</p>', {
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=blah id=moo>foo</p>”
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="blah" id="moo">foo</p>', {
104
- removeAttributeQuotes: true,
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#how_it_works), [description of each option](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).
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`: Aggressive minification for maximum file size reduction. Includes relevant conservative options plus attribute quote removal, optional tag removal, and more.
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#collapse_boolean_attributes) | `false` |
150
- | `collapseInlineTagWhitespace`<br>`--collapse-inline-tag-whitespace` | Don’t leave any spaces between `display: inline;` elements when collapsing—use with `collapseWhitespace: true` | `false` |
151
- | `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` |
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` | Arrays of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`) | `[]` |
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` | Arrays of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
158
- | `customEventAttributes`<br>`--custom-event-attributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g., `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
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 `style` attributes (uses [Lightning CSS](https://lightningcss.dev/)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
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#remove_attribute_quotes) | `false` |
181
- | `removeComments`<br>`--remove-comments` | [Strip HTML comments](https://perfectionkills.com/experimenting-with-html-minifier#remove_comments) | `false` |
182
- | `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)`) |
183
- | `removeEmptyElements`<br>`--remove-empty-elements` | [Remove all elements with empty contents](https://perfectionkills.com/experimenting-with-html-minifier#remove_empty_elements) | `false` |
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#remove_optional_tags) | `false` |
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 (HTML) doctype](https://perfectionkills.com/experimenting-with-html-minifier#use_short_doctype) | `false` |
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 `<style>` elements and `style` attributes. Lightning CSS provides excellent minification by default.
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,38 +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>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[![npm last update](https://img.shields.io/npm/last-update/htmlnano)](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[![npm last update](https://img.shields.io/npm/last-update/@swc/html)](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[![npm last update](https://img.shields.io/npm/last-update/@minify-html/node)](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[![npm last update](https://img.shields.io/npm/last-update/minimize)](https://socket.dev/npm/package/minimize) | [html­com­pressor.­com](https://htmlcompressor.com/) |
359
345
  | --- | --- | --- | --- | --- | --- | --- | --- |
360
- | [A List Apart](https://alistapart.com/) | 59 | **50** | 51 | 52 | 51 | 54 | 52 |
361
- | [Apple](https://www.apple.com/) | 211 | **176** | 187 | 189 | 190 | 191 | 192 |
362
- | [BBC](https://www.bbc.co.uk/) | 712 | **649** | 669 | 669 | 671 | 707 | n/a |
363
- | [CERN](https://home.cern/) | 152 | **83** | 91 | 91 | 91 | 93 | 96 |
364
- | [CSS-Tricks](https://css-tricks.com/) | 162 | **119** | 127 | 143 | 143 | 148 | 144 |
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/) | 54 | **45** | 49 | 47 | 48 | 49 | 49 |
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/) | 1576 | 1466 | **1412** | 1500 | 1511 | 1522 | n/a |
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/) | 225 | **217** | 238 | 223 | 225 | 244 | 225 |
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/) | 2374 | **2094** | 2186 | 2211 | 2213 | 2361 | n/a |
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/) | 50 | **33** | 36 | 36 | 36 | 37 | 37 |
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 |
376
363
  | [Mastodon](https://mastodon.social/explore) | 37 | **28** | 32 | 35 | 35 | 36 | 36 |
377
364
  | [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | 64 | 65 | 65 | 68 | 68 |
378
- | [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 | 202 | 203 |
379
- | [Mistral AI](https://mistral.ai/) | 361 | **319** | 324 | 326 | 327 | 357 | n/a |
380
- | [Mozilla](https://www.mozilla.org/) | 45 | **31** | 34 | 34 | 34 | 35 | 35 |
381
- | [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 68 | **55** | 74 | 75 | 77 | 76 |
382
- | [SitePoint](https://www.sitepoint.com/) | 482 | **351** | 422 | 456 | 460 | 478 | n/a |
383
- | [Startup-Verband](https://startupverband.de/) | 42 | **29** | 30 | 30 | 30 | 31 | 30 |
384
- | [TetraLogical](https://tetralogical.com/) | 44 | 38 | **35** | 38 | 39 | 39 | 39 |
385
- | [TPGi](https://www.tpgi.com/) | 174 | **158** | 159 | 163 | 165 | 171 | 171 |
386
- | [United Nations](https://www.un.org/en/) | 152 | **112** | 121 | 125 | 125 | 130 | 123 |
387
- | [Vivaldi](https://vivaldi.com/) | 92 | **74** | n/a | 79 | 81 | 83 | 81 |
388
- | [W3C](https://www.w3.org/) | 51 | **36** | 39 | 38 | 38 | 41 | 39 |
389
- | **Average processing time** | | 96 ms (29/29) | 160 ms (28/29) | 52 ms (29/29) | **14 ms (29/29)** | 282 ms (29/29) | 1766 ms (23/29) |
390
-
391
- (Last updated: Dec 27, 2025)
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)
392
379
  <!-- End auto-generated -->
393
380
 
394
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.
@@ -403,7 +390,7 @@ Notes: Minimize does not minify CSS and JS. [HTML Minifier Terser](https://githu
403
390
  html-minifier-next --collapse-whitespace --remove-comments --minify-js --input-dir=. --output-dir=example
404
391
  ```
405
392
 
406
- Another example, using npx:
393
+ Example using npx:
407
394
 
408
395
  ```shell
409
396
  npx html-minifier-next --input-dir=test --file-ext html --preset comprehensive --output-dir example
@@ -551,7 +538,7 @@ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
551
538
 
552
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.
553
540
 
554
- #### Escaping patterns in different contexts
541
+ ##### Escaping patterns in different contexts
555
542
 
556
543
  The escaping requirements for `ignoreCustomFragments` patterns differ depending on how you’re using HMN:
557
544
 
package/cli.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
4
  * html-minifier-next CLI tool
4
5
  *
5
6
  * The MIT License (MIT)
6
7
  *
7
- * Copyright (c) 2014-2016 Zoltan Frombach
8
+ * Copyright 20142016 Zoltan Frombach
9
+ *
10
+ * Copyright Juriy “kangax” Zaytsev
11
+ *
12
+ * Copyright 2025 Jens Oliver Meiert
8
13
  *
9
14
  * Permission is hereby granted, free of charge, to any person obtaining a copy of
10
15
  * this software and associated documentation files (the "Software"), to deal in
@@ -33,7 +38,6 @@ import { createRequire } from 'module';
33
38
  import { camelCase, paramCase } from 'change-case';
34
39
  import { Command } from 'commander';
35
40
  // Lazy-load HMN to reduce CLI cold-start overhead
36
- // import { minify } from './src/htmlminifier.js';
37
41
  import { getPreset, getPresetNames } from './src/presets.js';
38
42
 
39
43
  const require = createRequire(import.meta.url);
@@ -57,7 +61,7 @@ process.stdout.on('error', (err) => {
57
61
  });
58
62
 
59
63
  /**
60
- * JSON does not support regexes, so, e.g., JSON.parse() will not create
64
+ * JSON does not support regexes, so, e.g., `JSON.parse()` will not create
61
65
  * a RegExp from the JSON value `[ "/matchString/" ]`, which is
62
66
  * technically just an array containing a string that begins and end with
63
67
  * a forward slash. To get a RegExp from a JSON string, it must be
@@ -121,53 +125,53 @@ const mainOptions = {
121
125
  caseSensitive: 'Treat attributes in case-sensitive manner (useful for custom HTML elements)',
122
126
  collapseAttributeWhitespace: 'Trim and collapse whitespace characters within attribute values',
123
127
  collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
124
- collapseInlineTagWhitespace: 'Don’t leave any spaces between “display: inline;” elements when collapsing—use with “--collapse-whitespace',
128
+ collapseInlineTagWhitespace: 'Collapse whitespace more aggressively between inline elements—use with `--collapse-whitespace`',
125
129
  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 “--collapse-whitespace',
130
+ conservativeCollapse: 'Always collapse to one space (never remove it entirely)—use with `--collapse-whitespace`',
127
131
  continueOnMinifyError: 'Abort on minification errors',
128
132
  continueOnParseError: 'Handle parse errors instead of aborting',
129
- customAttrAssign: ['Arrays of regexes that allow to support custom attribute assign expressions (e.g., “<div flex?="{{mode != cover}}"></div>”)', parseJSONRegExpArray],
133
+ customAttrAssign: ['Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`)', parseJSONRegExpArray],
130
134
  customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g., /ng-class/)', parseRegExp],
131
- customAttrSurround: ['Arrays of regexes that allow to support custom attribute surround expressions (e.g., “<input {{#if value}}checked="checked"{{/if}}>”)', parseJSONRegExpArray],
132
- customEventAttributes: ['Arrays of regexes that allow to support custom event attributes for minifyJS (e.g., ng-click)', parseJSONRegExpArray],
135
+ customAttrSurround: ['Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`)', parseJSONRegExpArray],
136
+ customEventAttributes: ['Array of regexes that allow to support custom event attributes for minifyJS (e.g., `ng-click`)', parseJSONRegExpArray],
133
137
  customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseValidInt('customFragmentQuantifierLimit')],
134
138
  decodeEntities: 'Use direct Unicode characters whenever possible',
135
139
  html5: 'Don’t parse input according to the HTML specification (not recommended for modern HTML)',
136
140
  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., “<?php … ?>”, {{ … }})', parseJSONRegExpArray],
141
+ ignoreCustomFragments: ['Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`)', parseJSONRegExpArray],
138
142
  includeAutoGeneratedTags: 'Don’t insert elements generated by HTML parser',
139
143
  inlineCustomElements: ['Array of names of custom elements which are inline', parseJSONArray],
140
144
  keepClosingSlash: 'Keep the trailing slash on void elements',
141
145
  maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseValidInt('maxInputLength')],
142
146
  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 "style" elements and "style" attributes (uses Lightning CSS)', parseJSON],
144
- minifyJS: ['Minify JavaScript in "script" elements and event attributes (uses Terser or SWC; pass "{"engine": "swc"}" for SWC)', parseJSON],
147
+ minifyCSS: ['Minify CSS in `style` elements and attributes (uses Lightning CSS)', parseJSON],
148
+ minifyJS: ['Minify JavaScript in `script` elements and event attributes (uses Terser or SWC; pass `{"engine": "swc"}` for SWC)', parseJSON],
145
149
  minifySVG: ['Minify SVG elements and attributes (numeric precision, default attributes, colors)', parseJSON],
146
150
  minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
147
151
  noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
148
152
  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 “--collapse-whitespace',
153
+ preserveLineBreaks: 'Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with `--collapse-whitespace`',
150
154
  preventAttributesEscaping: 'Prevents the escaping of the values of attributes',
151
155
  processConditionalComments: 'Process contents of conditional comments through minifier',
152
- 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],
156
+ 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
157
  quoteCharacter: ['Type of quote to use for attribute values (“\'” or “"”)', parseString],
154
158
  removeAttributeQuotes: 'Remove quotes around attributes when possible',
155
159
  removeComments: 'Strip HTML comments',
156
160
  removeEmptyAttributes: 'Remove all attributes with whitespace-only values',
157
161
  removeEmptyElements: 'Remove all elements with empty contents',
158
- removeEmptyElementsExcept: ['Array of elements to preserve when “--remove-empty-elements is enabled (e.g., td”, ["td", "<span aria-hidden=\'true\'>"])', parseJSONArray],
162
+ removeEmptyElementsExcept: ['Array of elements to preserve when `--remove-empty-elements` is enabled (e.g., `td`, `["td", "<span aria-hidden=\'true\'>"]`)', parseJSONArray],
159
163
  removeOptionalTags: 'Remove unrequired tags',
160
164
  removeRedundantAttributes: 'Remove attributes when value matches default',
161
- removeScriptTypeAttributes: 'Remove type="text/javascript" from script elements; other type attribute values are left intact',
162
- removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link elements; other type attribute values are left intact',
165
+ removeScriptTypeAttributes: 'Remove `type="text/javascript"` from `script` elements; other `type` attribute values are left intact',
166
+ removeStyleLinkTypeAttributes: 'Remove `type="text/css"` from `style` and `link` elements; other `type` attribute values are left intact',
163
167
  removeTagWhitespace: 'Remove space between attributes whenever possible; note that this will result in invalid HTML',
164
168
  sortAttributes: 'Sort attributes by frequency',
165
169
  sortClassName: 'Sort style classes by frequency',
166
- trimCustomFragments: 'Trim whitespace around “ignoreCustomFragments”',
167
- useShortDoctype: 'Replaces the doctype with the short (HTML) doctype'
170
+ trimCustomFragments: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`)',
171
+ useShortDoctype: 'Replaces the doctype with the short HTML doctype'
168
172
  };
169
173
 
170
- // Configure command line flags
174
+ // Configure command-line flags
171
175
  const mainOptionKeys = Object.keys(mainOptions);
172
176
  mainOptionKeys.forEach(function (key) {
173
177
  const option = mainOptions[key];
@@ -183,7 +187,7 @@ mainOptionKeys.forEach(function (key) {
183
187
  });
184
188
  program.option('-o --output <file>', 'Specify output file (reads from file arguments or STDIN; outputs to STDOUT if not specified)');
185
189
  program.option('-v --verbose', 'Show detailed processing information');
186
- program.option('-d --dry', 'Dry run: process and report statistics without writing output');
190
+ program.option('-d --dry', 'Dry run: Process and report statistics without writing output');
187
191
 
188
192
  // Lazy import wrapper for HMN
189
193
  let minifyFnPromise;
@@ -219,7 +223,7 @@ async function loadConfigFromPath(configPath) {
219
223
  // Try CJS require
220
224
  try {
221
225
  const result = require(abs);
222
- // Handle ESM interop: if `require()` loads an ESM file, it may return `{__esModule: true, default: …}`
226
+ // Handle ESM interop: If `require()` loads an ESM file, it may return `{__esModule: true, default: …}`
223
227
  return (result && result.__esModule && result.default) ? result.default : result;
224
228
  } catch (cjsErr) {
225
229
  // Try ESM import
@@ -252,7 +256,7 @@ function normalizeConfig(config) {
252
256
  }
253
257
  });
254
258
 
255
- // Handle fileExt in config file
259
+ // Handle `fileExt` in config file
256
260
  if ('fileExt' in normalized) {
257
261
  // Support both string (`html,htm`) and array (`["html", "htm"]`) formats
258
262
  if (Array.isArray(normalized.fileExt)) {
@@ -321,7 +325,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
321
325
  if (presetName) {
322
326
  const preset = getPreset(presetName);
323
327
  if (!preset) {
324
- fatal(`Unknown preset "${presetName}". Available presets: ${getPresetNames().join(', ')}`);
328
+ fatal(`Unknown preset “${presetName}”. Available presets: ${getPresetNames().join(', ')}`);
325
329
  }
326
330
  Object.assign(options, preset);
327
331
  }
@@ -445,8 +449,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
445
449
 
446
450
  return ignorePatterns.some(pattern => {
447
451
  // Support both exact directory names and relative paths
448
- return dirName === pattern || relativePath === pattern ||
449
- relativePath.startsWith(pattern + '/');
452
+ return dirName === pattern || relativePath === pattern || relativePath.startsWith(pattern + '/');
450
453
  });
451
454
  }
452
455
 
@@ -697,7 +700,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
697
700
  }
698
701
 
699
702
  // Resolve base directory for consistent path comparisons
700
- const inputDirResolved = await fs.promises.realpath(inputDir).catch(() => inputDir);
703
+ const inputDirResolved = inputReal || inputDir;
701
704
 
702
705
  if (showProgress) {
703
706
  // Start with indeterminate progress, count in background