html-minifier-next 1.4.0 → 1.4.2

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
@@ -57,7 +57,7 @@ Use `html-minifier-next --help` to check all available options:
57
57
  | --- | --- | --- |
58
58
  | `--input-dir <dir>` | Specify an input directory | `--input-dir=src` |
59
59
  | `--output-dir <dir>` | Specify an output directory | `--output-dir=dist` |
60
- | `--file-ext <extensions>` | Specify file extension(s) to process (overrides config file setting) | `--file-ext=html` or `--file-ext=html,htm,php` |
60
+ | `--file-ext <extensions>` | Specify file extension(s) to process (overrides config file setting) | `--file-ext=html`, `--file-ext=html,htm,php`, `--file-ext="html, htm, php"` |
61
61
  | `-o --output <file>` | Specify output file (single file mode) | `-o minified.html` |
62
62
  | `-c --config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
63
63
 
@@ -157,59 +157,59 @@ Most of the options are disabled by default.
157
157
  | `caseSensitive` | Treat attributes in case-sensitive manner (useful for custom HTML elements) | `false` |
158
158
  | `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` |
159
159
  | `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
160
- | `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
161
- | `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` |
162
- | `conservativeCollapse` | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` |
163
- | `continueOnParseError` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting. | `false` |
164
- | `customAttrAssign` | Arrays of regex’es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[]` |
165
- | `customAttrCollapse` | Regex that specifies custom attribute to strip newlines from (e.g. `/ng-class/`) | |
166
- | `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
167
- | `customEventAttributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g. `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
160
+ | `collapseInlineTagWhitespace` | Don’t leave any spaces between `display: inline;` elements when collapsing—use with `collapseWhitespace=true` | `false` |
161
+ | `collapseWhitespace` | [Collapse whitespace that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` |
162
+ | `conservativeCollapse` | Always collapse to 1 space (never remove it entirely)—use with `collapseWhitespace=true` | `false` |
163
+ | `continueOnParseError` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting | `false` |
164
+ | `customAttrAssign` | Arrays of regexes that allow to support custom attribute assign expressions (e.g., `'<div flex?="{{mode != cover}}"></div>'`) | `[]` |
165
+ | `customAttrCollapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | |
166
+ | `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
167
+ | `customEventAttributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g., `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
168
168
  | `decodeEntities` | Use direct Unicode characters whenever possible | `false` |
169
- | `html5` | Parse input according to HTML5 specifications | `true` |
169
+ | `html5` | Parse input according to the HTML specification | `true` |
170
170
  | `ignoreCustomComments` | Array of regexes that allow to ignore certain comments, when matched | `[ /^!/, /^\s*#/ ]` |
171
- | `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
171
+ | `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php ?>`, `{{ }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
172
172
  | `includeAutoGeneratedTags` | Insert elements generated by HTML parser | `true` |
173
173
  | `inlineCustomElements` | Array of names of custom elements which are inline | `[]` |
174
- | `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` |
174
+ | `keepClosingSlash` | Keep the trailing slash on void elements | `false` |
175
175
  | `maxInputLength` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
176
- | `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points |
177
- | `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
178
- | `minifyJS` | Minify JavaScript in script elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
176
+ | `maxLineLength` | Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points | |
177
+ | `minifyCSS` | Minify CSS in `style` elements and `style` attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
178
+ | `minifyJS` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
179
179
  | `minifyURLs` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `String`, `Object`, `Function(text)`) |
180
180
  | `noNewlinesBeforeTagClose` | Never add a newline before a tag that closes an element | `false` |
181
- | `preserveLineBreaks` | Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break. Must be used in conjunction with `collapseWhitespace=true` | `false` |
181
+ | `preserveLineBreaks` | Always collapse to 1 line break (never remove it entirely) when whitespace between tags includes a line break—use with `collapseWhitespace=true` | `false` |
182
182
  | `preventAttributesEscaping` | Prevents the escaping of the values of attributes | `false` |
183
183
  | `processConditionalComments` | Process contents of conditional comments through minifier | `false` |
184
- | `processScripts` | Array of strings corresponding to types of script elements to process through minifier (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.) | `[]` |
185
- | `quoteCharacter` | Type of quote to use for attribute values (' or ") | |
184
+ | `processScripts` | Array of strings corresponding to types of `script` elements to process through minifier (e.g., `text/ng-template`, `text/x-handlebars-template`, etc.) | `[]` |
185
+ | `quoteCharacter` | Type of quote to use for attribute values (`'` or `"`) | |
186
186
  | `removeAttributeQuotes` | [Remove quotes around attributes when possible](http://perfectionkills.com/experimenting-with-html-minifier#remove_attribute_quotes) | `false` |
187
187
  | `removeComments` | [Strip HTML comments](http://perfectionkills.com/experimenting-with-html-minifier#remove_comments) | `false` |
188
188
  | `removeEmptyAttributes` | [Remove all attributes with whitespace-only values](http://perfectionkills.com/experimenting-with-html-minifier#remove_empty_or_blank_attributes) | `false` (could be `true`, `Function(attrName, tag)`) |
189
189
  | `removeEmptyElements` | [Remove all elements with empty contents](http://perfectionkills.com/experimenting-with-html-minifier#remove_empty_elements) | `false` |
190
190
  | `removeOptionalTags` | [Remove optional tags](http://perfectionkills.com/experimenting-with-html-minifier#remove_optional_tags) | `false` |
191
191
  | `removeRedundantAttributes` | [Remove attributes when value matches default.](http://perfectionkills.com/experimenting-with-html-minifier#remove_redundant_attributes) | `false` |
192
- | `removeScriptTypeAttributes` | Remove `type="text/javascript"` from `script` elements. Other `type` attribute values are left intact | `false` |
193
- | `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` elements. Other `type` attribute values are left intact | `false` |
194
- | `removeTagWhitespace` | Remove space between attributes whenever possible. **Note that this will result in invalid HTML!** | `false` |
195
- | `sortAttributes` | [Sort attributes by frequency](#sorting-attributes--style-classes) | `false` |
196
- | `sortClassName` | [Sort style classes by frequency](#sorting-attributes--style-classes) | `false` |
197
- | `trimCustomFragments` | Trim white space around `ignoreCustomFragments`. | `false` |
198
- | `useShortDoctype` | [Replaces the `doctype` with the short (HTML5) doctype](http://perfectionkills.com/experimenting-with-html-minifier#use_short_doctype) | `false` |
192
+ | `removeScriptTypeAttributes` | Remove `type="text/javascript"` from `script` elements; other `type` attribute values are left intact | `false` |
193
+ | `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` elements; other `type` attribute values are left intact | `false` |
194
+ | `removeTagWhitespace` | Remove space between attributes whenever possible; **note that this will result in invalid HTML** | `false` |
195
+ | `sortAttributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
196
+ | `sortClassName` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
197
+ | `trimCustomFragments` | Trim whitespace around `ignoreCustomFragments` | `false` |
198
+ | `useShortDoctype` | [Replaces the doctype with the short (HTML) doctype](http://perfectionkills.com/experimenting-with-html-minifier#use_short_doctype) | `false` |
199
199
 
200
- ### Sorting attributes / style classes
200
+ ### Sorting attributes and style classes
201
201
 
202
- Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain-text size of the output. However, they form long repetitive chains of characters that should improve compression ratio of gzip used in HTTP compression.
202
+ Minifier options like `sortAttributes` and `sortClassName` won’t impact the plaintext size of the output. However, they form long, repetitive character chains that should improve the compression ratio of gzip used for HTTP.
203
203
 
204
204
  ## Special cases
205
205
 
206
206
  ### Ignoring chunks of markup
207
207
 
208
- If you have chunks of markup you would like preserved, you can wrap them `<!-- htmlmin:ignore -->`.
208
+ If you have chunks of markup you would like preserved, you can wrap them with `<!-- htmlmin:ignore -->`.
209
209
 
210
210
  ### Minifying JSON-LD
211
211
 
212
- You can minify script elements with JSON-LD by setting the option `{ processScripts: ['application/ld+json'] }`. Note that this minification is very rudimentary, it is mainly useful for removing newlines and excessive whitespace.
212
+ You can minify `script` elements with JSON-LD by setting `{ processScripts: ['application/ld+json'] }`. Note that this minification is rudimentary; it’s mainly useful for removing newlines and excessive whitespace.
213
213
 
214
214
  ### Preserving SVG elements
215
215
 
@@ -219,9 +219,11 @@ SVG elements are automatically recognized, and when they are minified, both case
219
219
 
220
220
  HTML Minifier **can’t work with invalid or partial chunks of markup**. This is because it parses markup into a tree structure, then modifies it (removing anything that was specified for removal, ignoring anything that was specified to be ignored, etc.), then it creates a markup out of that tree and returns it.
221
221
 
222
- Input markup (e.g. `<p id="">foo`) → Internal representation of markup in a form of tree (e.g. `{ tag: "p", attr: "id", children: ["foo"] }`) → Transformation of internal representation (e.g. removal of `id` attribute) → Output of resulting markup (e.g. `<p>foo</p>`)
222
+ Input markup (e.g., `<p id="">foo`) → Internal representation of markup in a form of tree (e.g., `{ tag: "p", attr: "id", children: ["foo"] }`) → Transformation of internal representation (e.g., removal of `id` attribute) → Output of resulting markup (e.g., `<p>foo</p>`)
223
223
 
224
- HTML Minifier can’t know that original markup was only half of the tree; it does its best to try to parse it as a full tree and it loses information about tree being malformed or partial in the beginning. As a result, it can’t create a partial/malformed tree at the time of the output.
224
+ HTML Minifier can’t know that the original markup represented only part of the tree. It parses a complete tree and, in doing so, loses information about the input being malformed or partial. As a result, it can’t emit a partial or malformed tree.
225
+
226
+ To validate HTML markup, use [the W3C validator](https://validator.w3.org/) or one of [several validator packages](https://meiert.com/blog/html-validator-packages/).
225
227
 
226
228
  ## Security
227
229
 
@@ -237,8 +239,6 @@ This minifier includes protection against regular expression denial of service (
237
239
 
238
240
  **Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities.
239
241
 
240
- (Further improvements are needed. Contributions welcome.)
241
-
242
242
  #### Custom fragment examples
243
243
 
244
244
  **Safe patterns** (recommended):
package/cli.js CHANGED
@@ -97,48 +97,48 @@ function parseJSONRegExpArray(value) {
97
97
  const parseString = value => value;
98
98
 
99
99
  const mainOptions = {
100
- caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)',
100
+ caseSensitive: 'Treat attributes in case-sensitive manner (useful for custom HTML elements)',
101
101
  collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
102
102
  customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseInt],
103
- collapseInlineTagWhitespace: 'Collapse white space around inline tag',
104
- collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.',
105
- conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)',
103
+ collapseInlineTagWhitespace: 'Don’t leave any spaces between “display: inline;” elements when collapsing—use with “collapseWhitespace=true”',
104
+ collapseWhitespace: 'Collapse whitespace that contributes to text nodes in a document tree',
105
+ conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)—use with “collapseWhitespace=true”',
106
106
  continueOnParseError: 'Handle parse errors instead of aborting',
107
- customAttrAssign: ['Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'<div flex?="{{mode != cover}}"></div>\')', parseJSONRegExpArray],
108
- customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g. /ng-class/)', parseRegExp],
109
- customAttrSurround: ['Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. <input {{#if value}}checked="checked"{{/if}}>)', parseJSONRegExpArray],
110
- customEventAttributes: ['Arrays of regex\'es that allow to support custom event attributes for minifyJS (e.g. ng-click)', parseJSONRegExpArray],
107
+ customAttrAssign: ['Arrays of regexes that allow to support custom attribute assign expressions (e.g., “<div flex?="{{mode != cover}}"></div>”)', parseJSONRegExpArray],
108
+ customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g., /ng-class/)', parseRegExp],
109
+ customAttrSurround: ['Arrays of regexes that allow to support custom attribute surround expressions (e.g., “<input {{#if value}}checked="checked"{{/if}}>”)', parseJSONRegExpArray],
110
+ customEventAttributes: ['Arrays of regexes that allow to support custom event attributes for minifyJS (e.g., ng-click)', parseJSONRegExpArray],
111
111
  decodeEntities: 'Use direct Unicode characters whenever possible',
112
- html5: 'Parse input according to HTML5 specifications',
113
- ignoreCustomComments: ['Array of regex\'es that allow to ignore certain comments, when matched', parseJSONRegExpArray],
114
- ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. <?php ... ?>, {{ ... }})', parseJSONRegExpArray],
115
- includeAutoGeneratedTags: 'Insert tags generated by HTML parser',
112
+ html5: 'Parse input according to the HTML specification',
113
+ ignoreCustomComments: ['Array of regexes that allow to ignore certain comments, when matched', parseJSONRegExpArray],
114
+ ignoreCustomFragments: ['Array of regexes that allow to ignore certain fragments, when matched (e.g., “<?php ?>”, {{ }})', parseJSONRegExpArray],
115
+ includeAutoGeneratedTags: 'Insert elements generated by HTML parser',
116
116
  inlineCustomElements: ['Array of names of custom elements which are inline', parseJSONArray],
117
- keepClosingSlash: 'Keep the trailing slash on singleton elements',
117
+ keepClosingSlash: 'Keep the trailing slash on void elements',
118
118
  maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseInt],
119
- maxLineLength: ['Max line length', parseInt],
120
- minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
121
- minifyJS: ['Minify Javascript in script elements and on* attributes', parseJSON],
119
+ maxLineLength: ['Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points', parseInt],
120
+ minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
121
+ minifyJS: ['Minify JavaScript in script elements and event attributes (uses Terser)', parseJSON],
122
122
  minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
123
123
  noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
124
- preserveLineBreaks: 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break.',
125
- preventAttributesEscaping: 'Prevents the escaping of the values of attributes.',
124
+ preserveLineBreaks: 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags includes a line break—use with “collapseWhitespace=true”',
125
+ preventAttributesEscaping: 'Prevents the escaping of the values of attributes',
126
126
  processConditionalComments: 'Process contents of conditional comments through minifier',
127
- 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],
128
- quoteCharacter: ['Type of quote to use for attribute values (\' or ")', parseString],
129
- removeAttributeQuotes: 'Remove quotes around attributes when possible.',
127
+ 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],
128
+ quoteCharacter: ['Type of quote to use for attribute values (“\' or “\")', parseString],
129
+ removeAttributeQuotes: 'Remove quotes around attributes when possible',
130
130
  removeComments: 'Strip HTML comments',
131
131
  removeEmptyAttributes: 'Remove all attributes with whitespace-only values',
132
132
  removeEmptyElements: 'Remove all elements with empty contents',
133
133
  removeOptionalTags: 'Remove unrequired tags',
134
- removeRedundantAttributes: 'Remove attributes when value matches default.',
135
- removeScriptTypeAttributes: 'Removes the following attributes from script tags: text/javascript, text/ecmascript, text/jscript, application/javascript, application/x-javascript, application/ecmascript. Other type attribute values are left intact',
136
- removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link tags. Other type attribute values are left intact.',
137
- removeTagWhitespace: 'Remove space between attributes whenever possible',
134
+ removeRedundantAttributes: 'Remove attributes when value matches default',
135
+ removeScriptTypeAttributes: 'Remove “type="text/javascript"” from “script” elements; other type attribute values are left intact',
136
+ removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link elements; other type attribute values are left intact',
137
+ removeTagWhitespace: 'Remove space between attributes whenever possible; note that this will result in invalid HTML',
138
138
  sortAttributes: 'Sort attributes by frequency',
139
139
  sortClassName: 'Sort style classes by frequency',
140
- trimCustomFragments: 'Trim white space around ignoreCustomFragments.',
141
- useShortDoctype: 'Replaces the doctype with the short (HTML5) doctype'
140
+ trimCustomFragments: 'Trim whitespace around ignoreCustomFragments',
141
+ useShortDoctype: 'Replaces the doctype with the short (HTML) doctype'
142
142
  };
143
143
 
144
144
  // Configure command line flags
@@ -33,7 +33,7 @@ class CaseInsensitiveSet extends Set {
33
33
  }
34
34
  }
35
35
 
36
- // Regular Expressions for parsing tags and attributes
36
+ // Regular expressions for parsing tags and attributes
37
37
  const singleAttrIdentifier = /([^\s"'<>/=]+)/;
38
38
  const singleAttrAssigns = [/=/];
39
39
  const singleAttrValues = [
@@ -64,10 +64,10 @@ let IS_REGEX_CAPTURING_BROKEN = false;
64
64
  IS_REGEX_CAPTURING_BROKEN = g === '';
65
65
  });
66
66
 
67
- // Empty Elements
67
+ // Empty elements
68
68
  const empty = new CaseInsensitiveSet(['area', 'base', 'basefont', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
69
69
 
70
- // Inline Elements
70
+ // Inline elements
71
71
  const inline = new CaseInsensitiveSet(['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'noscript', 'object', 'q', 's', 'samp', 'script', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'tt', 'u', 'var']);
72
72
 
73
73
  // Elements that you can, intentionally, leave open
@@ -77,10 +77,10 @@ const closeSelf = new CaseInsensitiveSet(['colgroup', 'dd', 'dt', 'li', 'option'
77
77
  // Attributes that have their values filled in disabled='disabled'
78
78
  const fillAttrs = new CaseInsensitiveSet(['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected']);
79
79
 
80
- // Special Elements (can contain anything)
80
+ // Special elements (can contain anything)
81
81
  const special = new CaseInsensitiveSet(['script', 'style']);
82
82
 
83
- // HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
83
+ // HTML5 elements https://html.spec.whatwg.org/multipage/indices.html#elements-3
84
84
  // Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
85
85
  const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base', 'blockquote', 'body', 'caption', 'col', 'colgroup', 'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'legend', 'li', 'menuitem', 'meta', 'ol', 'optgroup', 'option', 'param', 'rp', 'rt', 'source', 'style', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul']);
86
86
 
@@ -128,7 +128,7 @@ class HTMLParser {
128
128
  let last, prevTag, nextTag;
129
129
  while (html) {
130
130
  last = html;
131
- // Make sure we're not in a script or style element
131
+ // Make sure were not in a `script` or `style` element
132
132
  if (!lastTag || !special.has(lastTag)) {
133
133
  let textEnd = html.indexOf('<');
134
134
  if (textEnd === 0) {
@@ -204,7 +204,7 @@ class HTMLParser {
204
204
  html = '';
205
205
  }
206
206
 
207
- // next tag
207
+ // Next tag
208
208
  let nextTagMatch = parseStartTag(html);
209
209
  if (nextTagMatch) {
210
210
  nextTag = nextTagMatch.tagName;
@@ -317,9 +317,9 @@ class HTMLParser {
317
317
 
318
318
  const attrs = match.attrs.map(function (args) {
319
319
  let name, value, customOpen, customClose, customAssign, quote;
320
- const ncp = 7; // number of captured parts, scalar
320
+ const ncp = 7; // Number of captured parts, scalar
321
321
 
322
- // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
322
+ // Hackish workaround for FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
323
323
  if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
324
324
  if (args[3] === '') { delete args[3]; }
325
325
  if (args[4] === '') { delete args[4]; }
@@ -541,28 +541,28 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
541
541
  }
542
542
 
543
543
  if (collapseAll) {
544
- // strip non space whitespace then compress spaces to one
544
+ // Strip non-space whitespace then compress spaces to one
545
545
  str = collapseWhitespaceAll(str);
546
546
  }
547
547
 
548
548
  return lineBreakBefore + str + lineBreakAfter;
549
549
  }
550
550
 
551
- // non-empty elements that will maintain whitespace around them
552
- const inlineHtmlElements = ['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr'];
553
- // non-empty elements that will maintain whitespace within them
554
- const inlineTextTags = new Set(['a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
555
- // self-closing elements that will maintain whitespace around them
556
- const selfClosingInlineTags = new Set(['comment', 'img', 'input', 'wbr']);
551
+ // Non-empty elements that will maintain whitespace around them
552
+ const inlineElementsToKeepWhitespaceAround = ['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr'];
553
+ // Non-empty elements that will maintain whitespace within them
554
+ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
555
+ // Elements that will always maintain whitespace around them
556
+ const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
557
557
 
558
- function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineTags) {
559
- let trimLeft = prevTag && !selfClosingInlineTags.has(prevTag);
558
+ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
559
+ let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
560
560
  if (trimLeft && !options.collapseInlineTagWhitespace) {
561
- trimLeft = prevTag.charAt(0) === '/' ? !inlineTags.has(prevTag.slice(1)) : !inlineTextTags.has(prevTag);
561
+ trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
562
562
  }
563
- let trimRight = nextTag && !selfClosingInlineTags.has(nextTag);
563
+ let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
564
564
  if (trimRight && !options.collapseInlineTagWhitespace) {
565
- trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags.has(nextTag.slice(1)) : !inlineTags.has(nextTag);
565
+ trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
566
566
  }
567
567
  return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
568
568
  }
@@ -749,7 +749,7 @@ function isSrcset(attrName, tag) {
749
749
  return attrName === 'srcset' && srcsetTags.has(tag);
750
750
  }
751
751
 
752
- async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
752
+ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
753
753
  if (isEventAttribute(attrName, options)) {
754
754
  attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
755
755
  return options.minifyJS(attrValue, true);
@@ -807,6 +807,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
807
807
  } else if (isMediaQuery(tag, attrs, attrName)) {
808
808
  attrValue = trimWhitespace(attrValue);
809
809
  return options.minifyCSS(attrValue, 'media');
810
+ } else if (tag === 'iframe' && attrName === 'srcdoc') {
811
+ // Recursively minify HTML content within srcdoc attribute
812
+ // Fast-path: skip if nothing would change
813
+ if (!shouldMinifyInnerHTML(options)) {
814
+ return attrValue;
815
+ }
816
+ return minifyHTMLSelf(attrValue, options, true);
810
817
  }
811
818
  return attrValue;
812
819
  }
@@ -884,7 +891,7 @@ async function processScript(text, options, currentAttrs) {
884
891
  // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
885
892
  // with the following deviations:
886
893
  // - retain <body> if followed by <noscript>
887
- // - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
894
+ // - </rb>, </rt>, </rtc>, </rp>, and </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
888
895
  // - retain all tags which are adjacent to non-standard HTML tags
889
896
  const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
890
897
  const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
@@ -1046,7 +1053,7 @@ async function normalizeAttr(attr, attrs, tag, options) {
1046
1053
  }
1047
1054
 
1048
1055
  if (attrValue) {
1049
- attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs);
1056
+ attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
1050
1057
  }
1051
1058
 
1052
1059
  if (options.removeEmptyAttributes &&
@@ -1094,7 +1101,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
1094
1101
  emittedAttrValue += ' ';
1095
1102
  }
1096
1103
  } else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
1097
- // make sure trailing slash is not interpreted as HTML self-closing tag
1104
+ // Make sure trailing slash is not interpreted as HTML self-closing tag
1098
1105
  emittedAttrValue = attrValue;
1099
1106
  } else {
1100
1107
  emittedAttrValue = attrValue + ' ';
@@ -1121,6 +1128,17 @@ function identityAsync(value) {
1121
1128
  return Promise.resolve(value);
1122
1129
  }
1123
1130
 
1131
+ function shouldMinifyInnerHTML(options) {
1132
+ return Boolean(
1133
+ options.collapseWhitespace ||
1134
+ options.removeComments ||
1135
+ options.removeOptionalTags ||
1136
+ options.minifyJS !== identity ||
1137
+ options.minifyCSS !== identityAsync ||
1138
+ options.minifyURLs !== identity
1139
+ );
1140
+ }
1141
+
1124
1142
  const processOptions = (inputOptions) => {
1125
1143
  const options = {
1126
1144
  name: function (name) {
@@ -1355,11 +1373,16 @@ async function minifyHTML(value, options, partialMarkup) {
1355
1373
  let uidIgnore;
1356
1374
  let uidAttr;
1357
1375
  let uidPattern;
1358
- let inlineTags = new Set([...inlineHtmlElements, ...(options.inlineCustomElements ?? [])]);
1359
-
1360
- // temporarily replace ignored chunks with comments,
1361
- // so that we don't have to worry what's there.
1362
- // for all we care there might be
1376
+ // Create inline tags/text sets with custom elements
1377
+ const customElementsInput = options.inlineCustomElements ?? [];
1378
+ const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
1379
+ const normalizedCustomElements = customElementsArr.map(name => options.name(name));
1380
+ const inlineTextSet = new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements]);
1381
+ const inlineElements = new Set([...inlineElementsToKeepWhitespaceAround, ...normalizedCustomElements]);
1382
+
1383
+ // Temporarily replace ignored chunks with comments,
1384
+ // so that we don’t have to worry what’s there.
1385
+ // For all we care there might be
1363
1386
  // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
1364
1387
  value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
1365
1388
  if (!uidIgnore) {
@@ -1480,20 +1503,20 @@ async function minifyHTML(value, options, partialMarkup) {
1480
1503
  buffer.length = Math.max(0, index);
1481
1504
  }
1482
1505
 
1483
- // look for trailing whitespaces, bypass any inline tags
1506
+ // Look for trailing whitespaces, bypass any inline tags
1484
1507
  function trimTrailingWhitespace(index, nextTag) {
1485
1508
  for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
1486
1509
  const str = buffer[index];
1487
1510
  const match = str.match(/^<\/([\w:-]+)>$/);
1488
1511
  if (match) {
1489
1512
  endTag = match[1];
1490
- } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineTags))) {
1513
+ } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
1491
1514
  break;
1492
1515
  }
1493
1516
  }
1494
1517
  }
1495
1518
 
1496
- // look for trailing whitespaces from previously processed text
1519
+ // Look for trailing whitespaces from previously processed text
1497
1520
  // which may not be trimmed due to a following comment or an empty
1498
1521
  // element which has now been removed
1499
1522
  function squashTrailingWhitespace(nextTag) {
@@ -1524,7 +1547,7 @@ async function minifyHTML(value, options, partialMarkup) {
1524
1547
  tag = options.name(tag);
1525
1548
  currentTag = tag;
1526
1549
  charsPrevTag = tag;
1527
- if (!inlineTextTags.has(tag)) {
1550
+ if (!inlineTextSet.has(tag)) {
1528
1551
  currentChars = '';
1529
1552
  }
1530
1553
  hasChars = false;
@@ -1542,7 +1565,7 @@ async function minifyHTML(value, options, partialMarkup) {
1542
1565
  removeStartTag();
1543
1566
  }
1544
1567
  optionalStartTag = '';
1545
- // end-tag-followed-by-start-tag omission rules
1568
+ // End-tag-followed-by-start-tag omission rules
1546
1569
  if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
1547
1570
  removeEndTag();
1548
1571
  // <colgroup> cannot be omitted if preceding </colgroup> is omitted
@@ -1552,7 +1575,7 @@ async function minifyHTML(value, options, partialMarkup) {
1552
1575
  optionalEndTag = '';
1553
1576
  }
1554
1577
 
1555
- // set whitespace flags for nested tags (eg. <code> within a <pre>)
1578
+ // Set whitespace flags for nested tags (eg. <code> within a <pre>)
1556
1579
  if (options.collapseWhitespace) {
1557
1580
  if (!stackNoTrimWhitespace.length) {
1558
1581
  squashTrailingWhitespace(tag);
@@ -1588,7 +1611,7 @@ async function minifyHTML(value, options, partialMarkup) {
1588
1611
  buffer.push(' ');
1589
1612
  buffer.push.apply(buffer, parts);
1590
1613
  } else if (optional && optionalStartTags.has(tag)) {
1591
- // start tag must never be omitted if it has any attributes
1614
+ // Start tag must never be omitted if it has any attributes
1592
1615
  optionalStartTag = tag;
1593
1616
  }
1594
1617
 
@@ -1605,7 +1628,7 @@ async function minifyHTML(value, options, partialMarkup) {
1605
1628
  }
1606
1629
  tag = options.name(tag);
1607
1630
 
1608
- // check if current tag is in a whitespace stack
1631
+ // Check if current tag is in a whitespace stack
1609
1632
  if (options.collapseWhitespace) {
1610
1633
  if (stackNoTrimWhitespace.length) {
1611
1634
  if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
@@ -1643,7 +1666,7 @@ async function minifyHTML(value, options, partialMarkup) {
1643
1666
  }
1644
1667
 
1645
1668
  if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
1646
- // remove last "element" from buffer
1669
+ // Remove last element from buffer
1647
1670
  removeStartTag();
1648
1671
  optionalStartTag = '';
1649
1672
  optionalEndTag = '';
@@ -1654,7 +1677,7 @@ async function minifyHTML(value, options, partialMarkup) {
1654
1677
  buffer.push('</' + tag + '>');
1655
1678
  }
1656
1679
  charsPrevTag = '/' + tag;
1657
- if (!inlineTags.has(tag)) {
1680
+ if (!inlineElements.has(tag)) {
1658
1681
  currentChars = '';
1659
1682
  } else if (isElementEmpty) {
1660
1683
  currentChars += '|';
@@ -1693,12 +1716,12 @@ async function minifyHTML(value, options, partialMarkup) {
1693
1716
  }
1694
1717
  trimTrailingWhitespace(tagIndex - 1, 'br');
1695
1718
  }
1696
- } else if (inlineTextTags.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1719
+ } else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
1697
1720
  text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
1698
1721
  }
1699
1722
  }
1700
1723
  if (prevTag || nextTag) {
1701
- text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineTags);
1724
+ text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
1702
1725
  } else {
1703
1726
  text = collapseWhitespace(text, options, true, true);
1704
1727
  }
@@ -1768,7 +1791,7 @@ async function minifyHTML(value, options, partialMarkup) {
1768
1791
  text = prefix + text + suffix;
1769
1792
  }
1770
1793
  if (options.removeOptionalTags && text) {
1771
- // preceding comments suppress tag omissions
1794
+ // Preceding comments suppress tag omissions
1772
1795
  optionalStartTag = '';
1773
1796
  optionalEndTag = '';
1774
1797
  }