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 +33 -33
- package/cli.js +27 -27
- package/dist/htmlminifier.cjs +66 -43
- package/dist/htmlminifier.esm.bundle.js +321 -89
- package/dist/htmlminifier.umd.bundle.js +321 -89
- package/dist/htmlminifier.umd.bundle.min.js +2 -2
- package/package.json +8 -7
- package/src/htmlparser.js +5 -5
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
|
|
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
|
|
161
|
-
| `collapseWhitespace` | [Collapse
|
|
162
|
-
| `conservativeCollapse` | Always collapse to 1 space (never remove it entirely)
|
|
163
|
-
| `continueOnParseError` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting
|
|
164
|
-
| `customAttrAssign` | Arrays of
|
|
165
|
-
| `customAttrCollapse` | Regex that specifies custom attribute to strip newlines from (e.g
|
|
166
|
-
| `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g
|
|
167
|
-
| `customEventAttributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
185
|
-
| `quoteCharacter` | Type of quote to use for attribute values (
|
|
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
|
|
193
|
-
| `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` elements
|
|
194
|
-
| `removeTagWhitespace` | Remove space between attributes whenever possible
|
|
195
|
-
| `sortAttributes` | [Sort attributes by frequency](#sorting-attributes
|
|
196
|
-
| `sortClassName` | [Sort style classes by frequency](#sorting-attributes
|
|
197
|
-
| `trimCustomFragments` | Trim
|
|
198
|
-
| `useShortDoctype` | [Replaces the
|
|
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
|
|
200
|
+
### Sorting attributes and style classes
|
|
201
201
|
|
|
202
|
-
Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain
|
|
202
|
+
Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain‑text 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
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
104
|
-
collapseWhitespace: 'Collapse
|
|
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
|
|
108
|
-
customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g
|
|
109
|
-
customAttrSurround: ['Arrays of
|
|
110
|
-
customEventAttributes: ['Arrays of
|
|
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
|
|
113
|
-
ignoreCustomComments: ['Array of
|
|
114
|
-
ignoreCustomFragments: ['Array of
|
|
115
|
-
includeAutoGeneratedTags: 'Insert
|
|
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
|
|
117
|
+
keepClosingSlash: 'Keep the trailing slash on void elements',
|
|
118
118
|
maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseInt],
|
|
119
|
-
maxLineLength: ['
|
|
120
|
-
minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
|
|
121
|
-
minifyJS: ['Minify
|
|
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
|
|
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
|
|
128
|
-
quoteCharacter: ['Type of quote to use for attribute values (
|
|
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: '
|
|
136
|
-
removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link
|
|
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
|
|
141
|
-
useShortDoctype: 'Replaces the doctype with the short (
|
|
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
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -33,7 +33,7 @@ class CaseInsensitiveSet extends Set {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Regular
|
|
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
|
|
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
|
|
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
|
|
80
|
+
// Special elements (can contain anything)
|
|
81
81
|
const special = new CaseInsensitiveSet(['script', 'style']);
|
|
82
82
|
|
|
83
|
-
// HTML5
|
|
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
|
|
131
|
+
// Make sure we’re 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
|
-
//
|
|
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; //
|
|
320
|
+
const ncp = 7; // Number of captured parts, scalar
|
|
321
321
|
|
|
322
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
552
|
-
const
|
|
553
|
-
//
|
|
554
|
-
const
|
|
555
|
-
//
|
|
556
|
-
const
|
|
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,
|
|
559
|
-
let trimLeft = 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) === '/' ? !
|
|
561
|
+
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
562
562
|
}
|
|
563
|
-
let trimRight = nextTag && !
|
|
563
|
+
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
564
564
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
565
|
-
trimRight = nextTag.charAt(0) === '/' ? !
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
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 (!
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 (!
|
|
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 (
|
|
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,
|
|
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
|
-
//
|
|
1794
|
+
// Preceding comments suppress tag omissions
|
|
1772
1795
|
optionalStartTag = '';
|
|
1773
1796
|
optionalEndTag = '';
|
|
1774
1797
|
}
|