Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ As of version 2.0.0, all notable changes to HTML Minifier Next (HMN) are documen

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.3.0] - 2026-08-27

### Added

* Added warnings for options that rely on another option, which used to pass silently

### Changed

* Documented which options rely on another option to take effect

## [8.2.2] - 2026-08-26

### Changed
Expand Down
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
| `customAttrAssign`<br>`--custom-attr-assign` | Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`) | `[]` |
| `customAttrCollapse`<br>`--custom-attr-collapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | `undefined` |
| `customAttrSurround`<br>`--custom-attr-surround` | Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
| `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,}$/ ]` |
| `customEventAttributes`<br>`--custom-event-attributes` | Array of regexes that allow to support custom event attributes (e.g., `ng-click`)—use with `minifyJS` | `[ /^on[a-z]{3,}$/ ]` |
| `decodeEntities`<br>`--decode-entities` | Use direct Unicode characters whenever possible | `false` |
| `ignoreCustomComments`<br>`--ignore-custom-comments` | Array of regexes that allow to ignore matching comments | `[ /^!/, /^\s*#/ ]` |
| `ignoreCustomFragments`<br>`--ignore-custom-fragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
| `includeAutoGeneratedTags`<br>`--include-auto-generated-tags` | Insert elements generated by HTML parser | `false` |
| `inlineCustomElements`<br>`--inline-custom-elements` | Array of names of custom elements which are inline, for whitespace handling | `[]` |
| `inlineCustomElements`<br>`--inline-custom-elements` | Array of names of custom elements which are inline, for whitespace handling—use with `collapseWhitespace` | `[]` |
| `keepClosingSlash`<br>`--keep-closing-slash` | Keep the trailing slash on void elements and other start tags that carry one, and read it as closing the element | `false` |
| `maxInputLength`<br>`--max-input-length` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points | `undefined` |
Expand All @@ -159,7 +159,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
| `minifyJS`<br>`--minify-js` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://terser.org/) or [SWC](https://swc.rs/)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
| `minifySVG`<br>`--minify-svg` | Minify SVG elements (uses [SVGO](https://svgo.dev/)) | `false` (could be `true`, `Object`) |
| `minifyURLs`<br>`--minify-urls` | Minify URLs in various attributes | `false` (could be `true`, `String`, `Object`, `Function(text)`) |
| `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never add a newline before a tag that closes an element | `false` |
| `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never add a newline before a tag that closes an element—use with `maxLineLength` | `false` |
| `partialMarkup`<br>`--partial-markup` | Treat input as a partial HTML fragment, preserving stray end tags (closing tags without opening tags) and preventing auto-closing of unclosed tags at end of input | `false` |
| `preserveLineBreaks`<br>`--preserve-line-breaks` | Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with [`collapseWhitespace`](#combining-whitespace-options) | `false` |
| `preventAttributesEscaping`<br>`--prevent-attributes-escaping` | Prevents the escaping of the values of attributes | `false` |
Expand All @@ -170,15 +170,15 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
| `removeDefaultTypeAttributes`<br>`--remove-default-type-attributes` | Remove default `type` attributes from `style`/`link` (e.g., `type="text/css"`) and `script` (e.g., `type="text/javascript"`) elements; other `type` attribute values are left intact | `false` |
| `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)`) |
| `removeEmptyElements`<br>`--remove-empty-elements` | [Remove all elements with empty contents](https://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_elements) | `false` |
| `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 | `[]` |
| `removeEmptyElementsExcept`<br>`--remove-empty-elements-except` | Array of elements to preserve—use with `removeEmptyElements`; 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 | `[]` |
| `removeOptionalTags`<br>`--remove-optional-tags` | [Remove optional tags](https://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags) | `false` |
| `removeRedundantAttributes`<br>`--remove-redundant-attributes` | [Remove attributes when value matches default](https://meiert.com/blog/optional-html/#toc-attribute-values) | `false` |
| `removeTagWhitespace`<br>`--remove-tag-whitespace` | Remove space between attributes whenever possible; **note that this will result in invalid HTML** | `false` |
| `removeUnusedCSS`<br>`--remove-unused-css` | [Remove unused CSS rules](#unused-css-removal) from `style` elements—use with `minifyCSS`; **note that this can change how a document renders** | `false` (could be `true`, `{ safelist, scripts }`) |
| `sortAttributes`<br>`--sort-attributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
| `sortClassNames`<br>`--sort-class-names` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
| `strictCustomFragments`<br>`--strict-custom-fragments` | [Reject `ignoreCustomFragments` patterns that risk catastrophic backtracking](#redos-protection) (rather than warning about them) | `false` |
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`) | `false` |
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`)—use with `collapseWhitespace` | `false` |
| `useShortDoctype`<br>`--use-short-doctype` | [Replaces the doctype with the short HTML doctype](https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype) | `false` |

### API-only options
Expand All @@ -191,6 +191,28 @@ A few options take functions and are therefore only available programmatically,
| `canTrimWhitespace` | `Function(tag, attrs, defaultFn)` that determines whether leading and trailing whitespace around an element may be trimmed | Built-in handling |
| `log` | `Function(message)` called with warnings and errors, including minification errors swallowed by `continueOnMinifyError` (e.g., pass `console.error` to surface them); the CLI wires this up under `--verbose` and `--dry` | No-op (errors are silent) |

### Options that rely on another option

Some options modify what another option does, and do nothing when that other option is off. Setting one on its own is reported through [the `log` hook](#api-only-options) (and, in the CLI, on STDERR), once per message per run:

```text
HTML Minifier Next: Ignoring `conservativeCollapse`—use with `collapseWhitespace` (`--collapse-whitespace`)
```

| Option | Needs |
| --- | --- |
| `collapseInlineTagWhitespace` | `collapseWhitespace` |
| `conservativeCollapse` | `collapseWhitespace` |
| `customEventAttributes` | `minifyJS` |
| `inlineCustomElements` | `collapseWhitespace` |
| `noNewlinesBeforeTagClose` | `maxLineLength` |
| `preserveLineBreaks` | `collapseWhitespace` |
| `removeEmptyElementsExcept` | `removeEmptyElements` |
| `removeUnusedCSS` | `minifyCSS`, and not [a function of your own](#unused-css-removal) |
| `trimCustomFragments` | `collapseWhitespace` |

Passing the option `false`, or an empty array, asks for nothing and is not reported. `cacheCSS`, `cacheJS`, and `cacheSVG` are not listed: They size a cache rather than transform markup, and don’t change output.

### Combining whitespace options

`collapseInlineTagWhitespace`, `conservativeCollapse`, and `preserveLineBreaks` are modifiers: They do nothing on their own, and only take effect when `collapseWhitespace` is enabled.
Expand Down Expand Up @@ -673,7 +695,7 @@ For CLI usage, using a config file is strongly recommended to avoid complex shel

**[Web demo:](https://j9t.github.io/html-minifier-next/)**

```
```text
\{%[\s\S]{0,1000}?%\} \{\{[\s\S]{0,500}?\}\}
```

Expand Down
5 changes: 1 addition & 4 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ import os from 'os';
import readline from 'readline';
import { Command, Option } from 'commander';

// Simple case conversion for CLI option names (ASCII-only, no Unicode needed)
/** @param {string} str */
const paramCase = (str) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
/** @param {string} str */
const camelCase = (str) => paramCase(str).replace(/-([a-z])/g, (_, c) => c.toUpperCase());

Expand All @@ -54,7 +51,7 @@ const commanderOptionKey = (key) => {

// Lazy-load HMN to reduce CLI cold-start overhead
import { getPreset, getPresetNames } from './src/presets.js';
import { parseRegExp } from './src/lib/utils.js';
import { paramCase, parseRegExp } from './src/lib/utils.js';
import { optionDefinitions } from './src/lib/option-definitions.js';

const pkg = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
Expand Down
7 changes: 6 additions & 1 deletion demo/default.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import HTMLMinifier, { getPreset } from '../src/htmlminifier.js';
import { optionDefinitions } from '../src/lib/option-definitions.js';
import { paramCase } from '../src/lib/utils.js';
import { getOptions } from './get-options.js';
import pkg from '../package.json' with { type: 'json' };

Expand All @@ -9,11 +10,15 @@ const escapeHtml = (str) => str
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');

// Option names by CLI flag, so that a flag in a description is shown as the option it
// names—recasing the flag would lose the case of acronyms (`minifyCSS`, `minifyJS`)
const optionNamesByFlag = new Map(Object.keys(optionDefinitions).map(key => [paramCase(key), key]));

// Convert CLI-style descriptions to demo-style HTML
// - Backticks → `<code>` elements (with HTML escaping)
// - `--kebab-case` → `camelCase` (for option cross-references)
const toHtml = (desc = '') => String(desc)
.replace(/`--([a-z-]+)`/g, (_, kebab) => `<code>${kebab.replace(/-([a-z])/g, (__, c) => c.toUpperCase())}</code>`)
.replace(/`--([a-z-]+)`/g, (_, kebab) => `<code>${optionNamesByFlag.get(kebab) ?? kebab}</code>`)
.replace(/`([^`]+)`/g, (_, content) => `<code>${escapeHtml(content)}</code>`);

// Demo-specific UI configuration
Expand Down
10 changes: 5 additions & 5 deletions html-minifier-next.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
}
},
"customEventAttributes": {
"description": "Array of regexes that allow to support custom event attributes for minifyJS (e.g., `ng-click`)",
"description": "Array of regexes that allow to support custom event attributes (e.g., `ng-click`)—use with `--minify-js`",
"type": [
"string",
"array"
Expand Down Expand Up @@ -146,7 +146,7 @@
"type": "boolean"
},
"inlineCustomElements": {
"description": "Array of names of custom elements which are inline, for whitespace handling",
"description": "Array of names of custom elements which are inline, for whitespace handling—use with `--collapse-whitespace`",
"type": [
"string",
"array"
Expand Down Expand Up @@ -203,7 +203,7 @@
]
},
"noNewlinesBeforeTagClose": {
"description": "Never add a newline before a tag that closes an element",
"description": "Never add a newline before a tag that closes an element—use with `--max-line-length`",
"type": "boolean"
},
"partialMarkup": {
Expand Down Expand Up @@ -253,7 +253,7 @@
"type": "boolean"
},
"removeEmptyElementsExcept": {
"description": "Array of elements to preserve when `--remove-empty-elements` is enabled (e.g., `td`, `<span aria-hidden=\"true\">`)",
"description": "Array of elements to preserve (e.g., `td`, `<span aria-hidden=\"true\">`)—use with `--remove-empty-elements`",
"type": [
"string",
"array"
Expand Down Expand Up @@ -290,7 +290,7 @@
"type": "boolean"
},
"trimCustomFragments": {
"description": "Trim whitespace around custom fragments (`--ignore-custom-fragments`)",
"description": "Trim whitespace around custom fragments (`--ignore-custom-fragments`)—use with `--collapse-whitespace`",
"type": "boolean"
},
"strictCustomFragments": {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,5 @@
},
"type": "module",
"types": "dist/types/htmlminifier.d.ts",
"version": "8.2.2"
"version": "8.3.0"
}
10 changes: 5 additions & 5 deletions src/lib/option-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const optionDefinitions = {
type: 'regexpArray'
},
customEventAttributes: {
description: 'Array of regexes that allow to support custom event attributes for minifyJS (e.g., `ng-click`)',
description: 'Array of regexes that allow to support custom event attributes (e.g., `ng-click`)—use with `--minify-js`',
type: 'regexpArray'
},
decodeEntities: {
Expand All @@ -87,7 +87,7 @@ const optionDefinitions = {
type: 'boolean'
},
inlineCustomElements: {
description: 'Array of names of custom elements which are inline, for whitespace handling',
description: 'Array of names of custom elements which are inline, for whitespace handling—use with `--collapse-whitespace`',
type: 'jsonArray'
},
keepClosingSlash: {
Expand Down Expand Up @@ -123,7 +123,7 @@ const optionDefinitions = {
type: 'json'
},
noNewlinesBeforeTagClose: {
description: 'Never add a newline before a tag that closes an element',
description: 'Never add a newline before a tag that closes an element—use with `--max-line-length`',
type: 'boolean'
},
partialMarkup: {
Expand Down Expand Up @@ -167,7 +167,7 @@ const optionDefinitions = {
type: 'boolean'
},
removeEmptyElementsExcept: {
description: 'Array of elements to preserve when `--remove-empty-elements` is enabled (e.g., `td`, `<span aria-hidden="true">`)',
description: 'Array of elements to preserve (e.g., `td`, `<span aria-hidden="true">`)—use with `--remove-empty-elements`',
type: 'jsonArray'
},
removeOptionalTags: {
Expand Down Expand Up @@ -195,7 +195,7 @@ const optionDefinitions = {
type: 'boolean'
},
trimCustomFragments: {
description: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`)',
description: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`)—use with `--collapse-whitespace`',
type: 'boolean'
},
strictCustomFragments: {
Expand Down
Loading