html-minifier-next 1.2.0 → 1.3.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 +76 -37
- package/cli.js +37 -5
- package/package.json +9 -4
- package/src/htmlminifier.js +38 -36
- package/src/htmlparser.js +9 -9
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# HTML Minifier Next
|
|
1
|
+
# HTML Minifier Next
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/html-minifier-next)
|
|
4
|
-
|
|
4
|
+
[](https://github.com/j9t/html-minifier-next/actions?workflow=CI)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
HTML Minifier is a highly **configurable, well-tested, JavaScript-based HTML minifier**.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The project has been based on [Terser’s html-minifier-terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy Zaytsev’s html-minifier](https://github.com/kangax/html-minifier). It was set up because as of 2025, both html-minifier-terser and html-minifier have been unmaintained for some time. As the project seems maintainable [to me, [Jens](https://meiert.com/)]—even more so with community support—, it will be updated and documented further in this place.
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
@@ -25,14 +25,64 @@ npm i html-minifier-next
|
|
|
25
25
|
|
|
26
26
|
**Note** that almost all options are disabled by default. Experiment and find what works best for you and your project.
|
|
27
27
|
|
|
28
|
-
For command line usage please see `html-minifier-next --help` for a list of available options.
|
|
29
|
-
|
|
30
28
|
**Sample command line:**
|
|
31
29
|
|
|
32
30
|
```bash
|
|
33
31
|
html-minifier-next --collapse-whitespace --remove-comments --minify-js true --input-dir=. --output-dir=example
|
|
34
32
|
```
|
|
35
33
|
|
|
34
|
+
**Process specific file extensions:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Process only HTML files (CLI method)
|
|
38
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html
|
|
39
|
+
|
|
40
|
+
# Process multiple file extensions (CLI method)
|
|
41
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html,htm,php
|
|
42
|
+
|
|
43
|
+
# Using configuration file with fileExt setting
|
|
44
|
+
html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
|
|
45
|
+
|
|
46
|
+
# Process all files (default behavior)
|
|
47
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### CLI Options
|
|
51
|
+
|
|
52
|
+
Use `html-minifier-next --help` to check all available options:
|
|
53
|
+
|
|
54
|
+
| Option | Description | Example |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `--input-dir <dir>` | Specify an input directory | `--input-dir=src` |
|
|
57
|
+
| `--output-dir <dir>` | Specify an output directory | `--output-dir=dist` |
|
|
58
|
+
| `--file-ext <extensions>` | Specify file extension(s) to process (overrides config file setting) | `--file-ext=html` or `--file-ext=html,htm,php` |
|
|
59
|
+
| `-o --output <file>` | Specify output file (single file mode) | `-o minified.html` |
|
|
60
|
+
| `-c --config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
|
|
61
|
+
|
|
62
|
+
### Configuration Files
|
|
63
|
+
|
|
64
|
+
You can also use a configuration file to specify options:
|
|
65
|
+
|
|
66
|
+
**JSON configuration example:**
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"collapseWhitespace": true,
|
|
71
|
+
"removeComments": true,
|
|
72
|
+
"fileExt": "html,htm"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Using a configuration file:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Specify config file
|
|
80
|
+
html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
|
|
81
|
+
|
|
82
|
+
# CLI arguments override config file settings
|
|
83
|
+
html-minifier-next --config-file=html-minifier.json --file-ext=xml --input-dir=src --output-dir=dist
|
|
84
|
+
```
|
|
85
|
+
|
|
36
86
|
### Node.js
|
|
37
87
|
|
|
38
88
|
```js
|
|
@@ -41,7 +91,7 @@ const { minify } = require('html-minifier-next');
|
|
|
41
91
|
const result = await minify('<p title="blah" id="moo">foo</p>', {
|
|
42
92
|
removeAttributeQuotes: true,
|
|
43
93
|
});
|
|
44
|
-
result; //
|
|
94
|
+
result; // “<p title=blah id=moo>foo</p>”
|
|
45
95
|
```
|
|
46
96
|
|
|
47
97
|
See [the original blog post](http://perfectionkills.com/experimenting-with-html-minifier) for details of [how it works](http://perfectionkills.com/experimenting-with-html-minifier#how_it_works), [description of each option](http://perfectionkills.com/experimenting-with-html-minifier#options), [testing results](http://perfectionkills.com/experimenting-with-html-minifier#field_testing), and [conclusions](http://perfectionkills.com/experimenting-with-html-minifier#cost_and_benefits).
|
|
@@ -50,10 +100,10 @@ For lint-like capabilities take a look at [HTMLLint](https://github.com/kangax/h
|
|
|
50
100
|
|
|
51
101
|
## Minification comparison
|
|
52
102
|
|
|
53
|
-
How does
|
|
103
|
+
How does HTML Minifier compare to other solutions, like [minimize](https://github.com/Swaagie/minimize) or [htmlcompressor.com](http://htmlcompressor.com/)?
|
|
54
104
|
|
|
55
|
-
| Site | Original size (KB) |
|
|
56
|
-
| --- | ---
|
|
105
|
+
| Site | Original size (KB) | HTML Minifier | minimize | htmlcompressor.com |
|
|
106
|
+
| --- | --- |---------------| --- | --- |
|
|
57
107
|
| [A List Apart](https://alistapart.com/) | 64 | **54** | 59 | 57 |
|
|
58
108
|
| [Amazon](https://www.amazon.com/) | 206 | **195** | 203 | 200 |
|
|
59
109
|
| [BBC](https://www.bbc.co.uk/) | 767 | **703** | 761 | n/a |
|
|
@@ -63,7 +113,7 @@ How does HTMLMinifier compare to other solutions, like [minimize](https://github
|
|
|
63
113
|
| [FAZ](https://www.faz.net/aktuell/) | 1767 | **1641** | 1679 | n/a |
|
|
64
114
|
| [Frontend Dogma](https://frontenddogma.com/) | 119 | **114** | 128 | 118 |
|
|
65
115
|
| [Google](https://www.google.com/) | 51 | **46** | 50 | 50 |
|
|
66
|
-
| [
|
|
116
|
+
| [HTML Minifier](https://github.com/kangax/html-minifier) | 373 | **250** | 349 | n/a |
|
|
67
117
|
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | 36 | 36 |
|
|
68
118
|
| [NBC](https://www.nbc.com/) | 601 | **549** | 593 | n/a |
|
|
69
119
|
| [New York Times](https://www.nytimes.com/) | 822 | **701** | 811 | n/a |
|
|
@@ -77,22 +127,23 @@ Most of the options are disabled by default.
|
|
|
77
127
|
|
|
78
128
|
| Option | Description | Default |
|
|
79
129
|
| --- | --- | --- |
|
|
80
|
-
| `caseSensitive` | Treat attributes in case
|
|
130
|
+
| `caseSensitive` | Treat attributes in case-sensitive manner (useful for custom HTML elements) | `false` |
|
|
81
131
|
| `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` |
|
|
82
132
|
| `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
|
|
83
133
|
| `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
|
|
84
134
|
| `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` |
|
|
85
135
|
| `conservativeCollapse` | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` |
|
|
86
136
|
| `continueOnParseError` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting. | `false` |
|
|
87
|
-
| `customAttrAssign` | Arrays of regex’es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[
|
|
137
|
+
| `customAttrAssign` | Arrays of regex’es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[]` |
|
|
88
138
|
| `customAttrCollapse` | Regex that specifies custom attribute to strip newlines from (e.g. `/ng-class/`) | |
|
|
89
|
-
| `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[
|
|
139
|
+
| `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
|
|
90
140
|
| `customEventAttributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g. `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
|
|
91
141
|
| `decodeEntities` | Use direct Unicode characters whenever possible | `false` |
|
|
142
|
+
| `fileExt` | File extensions to process | `[]` (process all files) |
|
|
92
143
|
| `html5` | Parse input according to HTML5 specifications | `true` |
|
|
93
144
|
| `ignoreCustomComments` | Array of regexes that allow to ignore certain comments, when matched | `[ /^!/, /^\s*#/ ]` |
|
|
94
145
|
| `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
|
|
95
|
-
| `includeAutoGeneratedTags` | Insert
|
|
146
|
+
| `includeAutoGeneratedTags` | Insert elements generated by HTML parser | `true` |
|
|
96
147
|
| `inlineCustomElements` | Array of names of custom elements which are inline | `[]` |
|
|
97
148
|
| `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` |
|
|
98
149
|
| `maxInputLength` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
|
|
@@ -104,7 +155,7 @@ Most of the options are disabled by default.
|
|
|
104
155
|
| `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` |
|
|
105
156
|
| `preventAttributesEscaping` | Prevents the escaping of the values of attributes | `false` |
|
|
106
157
|
| `processConditionalComments` | Process contents of conditional comments through minifier | `false` |
|
|
107
|
-
| `processScripts` | Array of strings corresponding to types of script elements to process through minifier (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.) | `[
|
|
158
|
+
| `processScripts` | Array of strings corresponding to types of script elements to process through minifier (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.) | `[]` |
|
|
108
159
|
| `quoteCharacter` | Type of quote to use for attribute values (“'” or “"”) | |
|
|
109
160
|
| `removeAttributeQuotes` | [Remove quotes around attributes when possible](http://perfectionkills.com/experimenting-with-html-minifier#remove_attribute_quotes) | `false` |
|
|
110
161
|
| `removeComments` | [Strip HTML comments](http://perfectionkills.com/experimenting-with-html-minifier#remove_comments) | `false` |
|
|
@@ -112,8 +163,8 @@ Most of the options are disabled by default.
|
|
|
112
163
|
| `removeEmptyElements` | [Remove all elements with empty contents](http://perfectionkills.com/experimenting-with-html-minifier#remove_empty_elements) | `false` |
|
|
113
164
|
| `removeOptionalTags` | [Remove optional tags](http://perfectionkills.com/experimenting-with-html-minifier#remove_optional_tags) | `false` |
|
|
114
165
|
| `removeRedundantAttributes` | [Remove attributes when value matches default.](http://perfectionkills.com/experimenting-with-html-minifier#remove_redundant_attributes) | `false` |
|
|
115
|
-
| `removeScriptTypeAttributes` | Remove `type="text/javascript"` from `script`
|
|
116
|
-
| `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link`
|
|
166
|
+
| `removeScriptTypeAttributes` | Remove `type="text/javascript"` from `script` elements. Other `type` attribute values are left intact | `false` |
|
|
167
|
+
| `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` elements. Other `type` attribute values are left intact | `false` |
|
|
117
168
|
| `removeTagWhitespace` | Remove space between attributes whenever possible. **Note that this will result in invalid HTML!** | `false` |
|
|
118
169
|
| `sortAttributes` | [Sort attributes by frequency](#sorting-attributes--style-classes) | `false` |
|
|
119
170
|
| `sortClassName` | [Sort style classes by frequency](#sorting-attributes--style-classes) | `false` |
|
|
@@ -132,31 +183,19 @@ If you have chunks of markup you would like preserved, you can wrap them `<!-- h
|
|
|
132
183
|
|
|
133
184
|
### Minifying JSON-LD
|
|
134
185
|
|
|
135
|
-
You can minify script
|
|
186
|
+
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.
|
|
136
187
|
|
|
137
|
-
### Preserving SVG
|
|
188
|
+
### Preserving SVG elements
|
|
138
189
|
|
|
139
|
-
SVG
|
|
190
|
+
SVG elements are automatically recognized, and when they are minified, both case-sensitivity and closing-slashes are preserved, regardless of the minification settings used for the rest of the file.
|
|
140
191
|
|
|
141
192
|
### Working with invalid markup
|
|
142
193
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Input markup (e.g. `<p id="">foo`)
|
|
146
|
-
|
|
147
|
-
↓
|
|
148
|
-
|
|
149
|
-
Internal representation of markup in a form of tree (e.g. `{ tag: "p", attr: "id", children: ["foo"] }`)
|
|
150
|
-
|
|
151
|
-
↓
|
|
152
|
-
|
|
153
|
-
Transformation of internal representation (e.g. removal of `id` attribute)
|
|
154
|
-
|
|
155
|
-
↓
|
|
194
|
+
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.
|
|
156
195
|
|
|
157
|
-
Output of resulting markup (e.g. `<p>foo</p>`)
|
|
196
|
+
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>`)
|
|
158
197
|
|
|
159
|
-
|
|
198
|
+
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.
|
|
160
199
|
|
|
161
200
|
## Security
|
|
162
201
|
|
|
@@ -233,4 +272,4 @@ npm run serve
|
|
|
233
272
|
|
|
234
273
|
## Acknowledgements
|
|
235
274
|
|
|
236
|
-
With many thanks to all the previous authors of HTML Minifier, especially [Juriy Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, like [Daniel Ruf](https://github.com/DanielRuf).
|
|
275
|
+
With many thanks to all the previous authors of HTML Minifier, especially [Juriy Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, like [Daniel Ruf](https://github.com/DanielRuf).
|
package/cli.js
CHANGED
|
@@ -141,7 +141,7 @@ const mainOptions = {
|
|
|
141
141
|
useShortDoctype: 'Replaces the doctype with the short (HTML5) doctype'
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
-
//
|
|
144
|
+
// Configure command line flags
|
|
145
145
|
const mainOptionKeys = Object.keys(mainOptions);
|
|
146
146
|
mainOptionKeys.forEach(function (key) {
|
|
147
147
|
const option = mainOptions[key];
|
|
@@ -186,10 +186,18 @@ program.option('-c --config-file <file>', 'Use config file', function (configPat
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
});
|
|
189
|
+
|
|
190
|
+
// Handle fileExt in config file
|
|
191
|
+
if ('fileExt' in config) {
|
|
192
|
+
// Support both string (`html,htm`) and array (`["html", "htm"]`) formats
|
|
193
|
+
if (Array.isArray(config.fileExt)) {
|
|
194
|
+
config.fileExt = config.fileExt.join(',');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
189
197
|
});
|
|
190
198
|
program.option('--input-dir <dir>', 'Specify an input directory');
|
|
191
199
|
program.option('--output-dir <dir>', 'Specify an output directory');
|
|
192
|
-
program.option('--file-ext <text>', 'Specify
|
|
200
|
+
program.option('--file-ext <text>', 'Specify file extension(s) to be read, e.g. “html” or “html,htm”');
|
|
193
201
|
|
|
194
202
|
let content;
|
|
195
203
|
program.arguments('[files...]').action(function (files) {
|
|
@@ -241,7 +249,28 @@ function processFile(inputFile, outputFile) {
|
|
|
241
249
|
});
|
|
242
250
|
}
|
|
243
251
|
|
|
252
|
+
function parseFileExtensions(fileExt) {
|
|
253
|
+
if (!fileExt) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
return fileExt
|
|
257
|
+
.split(',')
|
|
258
|
+
.map(ext => ext.trim().replace(/^\.+/, '').toLowerCase())
|
|
259
|
+
.filter(ext => ext.length > 0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function shouldProcessFile(filename, fileExtensions) {
|
|
263
|
+
if (fileExtensions.length === 0) {
|
|
264
|
+
return true; // No extensions specified, process all files
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const fileExt = path.extname(filename).replace(/^\.+/, '').toLowerCase();
|
|
268
|
+
return fileExtensions.includes(fileExt);
|
|
269
|
+
}
|
|
270
|
+
|
|
244
271
|
function processDirectory(inputDir, outputDir, fileExt) {
|
|
272
|
+
const extensions = parseFileExtensions(fileExt);
|
|
273
|
+
|
|
245
274
|
fs.readdir(inputDir, function (err, files) {
|
|
246
275
|
if (err) {
|
|
247
276
|
fatal('Cannot read directory ' + inputDir + '\n' + err.message);
|
|
@@ -256,7 +285,7 @@ function processDirectory(inputDir, outputDir, fileExt) {
|
|
|
256
285
|
fatal('Cannot read ' + inputFile + '\n' + err.message);
|
|
257
286
|
} else if (stat.isDirectory()) {
|
|
258
287
|
processDirectory(inputFile, outputFile, fileExt);
|
|
259
|
-
} else if (
|
|
288
|
+
} else if (shouldProcessFile(file, extensions)) {
|
|
260
289
|
mkdir(outputDir, function () {
|
|
261
290
|
processFile(inputFile, outputFile);
|
|
262
291
|
});
|
|
@@ -290,13 +319,16 @@ const writeMinify = async () => {
|
|
|
290
319
|
|
|
291
320
|
const { inputDir, outputDir, fileExt } = programOptions;
|
|
292
321
|
|
|
322
|
+
// Resolve file extensions: CLI argument takes priority over config file
|
|
323
|
+
const resolvedFileExt = fileExt || config.fileExt;
|
|
324
|
+
|
|
293
325
|
if (inputDir || outputDir) {
|
|
294
326
|
if (!inputDir) {
|
|
295
327
|
fatal('The option output-dir needs to be used with the option input-dir. If you are working with a single file, use -o.');
|
|
296
328
|
} else if (!outputDir) {
|
|
297
329
|
fatal('You need to specify where to write the output files with the option --output-dir');
|
|
298
330
|
}
|
|
299
|
-
processDirectory(inputDir, outputDir,
|
|
331
|
+
processDirectory(inputDir, outputDir, resolvedFileExt);
|
|
300
332
|
} else if (content) { // Minifying one or more files specified on the CMD line
|
|
301
333
|
writeMinify();
|
|
302
334
|
} else { // Minifying input coming from STDIN
|
|
@@ -305,4 +337,4 @@ if (inputDir || outputDir) {
|
|
|
305
337
|
process.stdin.on('data', function (data) {
|
|
306
338
|
content += data;
|
|
307
339
|
}).on('end', writeMinify);
|
|
308
|
-
}
|
|
340
|
+
}
|
package/package.json
CHANGED
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"@rollup/plugin-terser": "^0.4.4",
|
|
23
23
|
"alpinejs": "^3.14.9",
|
|
24
24
|
"commitlint-config-non-conventional": "^1.0.1",
|
|
25
|
-
"eslint": "^9.
|
|
25
|
+
"eslint": "^9.33.0",
|
|
26
26
|
"husky": "^9.1.7",
|
|
27
27
|
"is-ci": "^4.1.0",
|
|
28
28
|
"jest": "^30.0.5",
|
|
29
29
|
"lint-staged": "^16.1.5",
|
|
30
|
-
"rollup": "^4.
|
|
30
|
+
"rollup": "^4.46.2",
|
|
31
31
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
32
|
-
"vite": "^7.
|
|
32
|
+
"vite": "^7.1.2"
|
|
33
33
|
},
|
|
34
34
|
"exports": {
|
|
35
35
|
".": {
|
|
@@ -71,6 +71,11 @@
|
|
|
71
71
|
"main": "./dist/htmlminifier.cjs",
|
|
72
72
|
"module": "./src/htmlminifier.js",
|
|
73
73
|
"name": "html-minifier-next",
|
|
74
|
+
"overrides": {
|
|
75
|
+
"glob": "^10.0.0",
|
|
76
|
+
"inflight": "npm:@nodelib/fs.stat@^3.0.0"
|
|
77
|
+
},
|
|
78
|
+
"overrides_comment": "@@ Remove when Jest fixes deprecated glob@7.2.3 and inflight dependencies",
|
|
74
79
|
"repository": "https://github.com/j9t/html-minifier-next.git",
|
|
75
80
|
"scripts": {
|
|
76
81
|
"build": "rollup -c",
|
|
@@ -83,5 +88,5 @@
|
|
|
83
88
|
"test:watch": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --watch"
|
|
84
89
|
},
|
|
85
90
|
"type": "module",
|
|
86
|
-
"version": "1.2
|
|
91
|
+
"version": "1.3.2"
|
|
87
92
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -52,28 +52,28 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (collapseAll) {
|
|
55
|
-
//
|
|
55
|
+
// Strip non-space whitespace then compress spaces to one
|
|
56
56
|
str = collapseWhitespaceAll(str);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
return lineBreakBefore + str + lineBreakAfter;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
//
|
|
67
|
-
const
|
|
62
|
+
// Non-empty elements that will maintain whitespace around them
|
|
63
|
+
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'];
|
|
64
|
+
// Non-empty elements that will maintain whitespace within them
|
|
65
|
+
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']);
|
|
66
|
+
// Elements that will always maintain whitespace around them
|
|
67
|
+
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
68
68
|
|
|
69
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options,
|
|
70
|
-
let trimLeft = prevTag && !
|
|
69
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
70
|
+
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
71
71
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
72
|
-
trimLeft = prevTag.charAt(0) === '/' ? !
|
|
72
|
+
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
73
73
|
}
|
|
74
|
-
let trimRight = nextTag && !
|
|
74
|
+
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
75
75
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
76
|
-
trimRight = nextTag.charAt(0) === '/' ? !
|
|
76
|
+
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
77
77
|
}
|
|
78
78
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
79
79
|
}
|
|
@@ -395,7 +395,7 @@ async function processScript(text, options, currentAttrs) {
|
|
|
395
395
|
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
396
396
|
// with the following deviations:
|
|
397
397
|
// - retain <body> if followed by <noscript>
|
|
398
|
-
// - </rb>, </rt>, </rtc>, </rp
|
|
398
|
+
// - </rb>, </rt>, </rtc>, </rp>, and </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
|
|
399
399
|
// - retain all tags which are adjacent to non-standard HTML tags
|
|
400
400
|
const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
|
|
401
401
|
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']);
|
|
@@ -605,7 +605,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
605
605
|
emittedAttrValue += ' ';
|
|
606
606
|
}
|
|
607
607
|
} else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
|
|
608
|
-
//
|
|
608
|
+
// Make sure trailing slash is not interpreted as HTML self-closing tag
|
|
609
609
|
emittedAttrValue = attrValue;
|
|
610
610
|
} else {
|
|
611
611
|
emittedAttrValue = attrValue + ' ';
|
|
@@ -866,14 +866,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
866
866
|
let uidIgnore;
|
|
867
867
|
let uidAttr;
|
|
868
868
|
let uidPattern;
|
|
869
|
-
// Create inline tags
|
|
870
|
-
const
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
//
|
|
869
|
+
// Create inline tags/text sets with custom elements
|
|
870
|
+
const customElementsInput = options.inlineCustomElements ?? [];
|
|
871
|
+
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
872
|
+
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
873
|
+
const inlineTextSet = new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements]);
|
|
874
|
+
const inlineElements = new Set([...inlineElementsToKeepWhitespaceAround, ...normalizedCustomElements]);
|
|
875
|
+
|
|
876
|
+
// Temporarily replace ignored chunks with comments,
|
|
877
|
+
// so that we don’t have to worry what’s there.
|
|
878
|
+
// For all we care there might be
|
|
877
879
|
// completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
|
878
880
|
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
879
881
|
if (!uidIgnore) {
|
|
@@ -994,20 +996,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
994
996
|
buffer.length = Math.max(0, index);
|
|
995
997
|
}
|
|
996
998
|
|
|
997
|
-
//
|
|
999
|
+
// Look for trailing whitespaces, bypass any inline tags
|
|
998
1000
|
function trimTrailingWhitespace(index, nextTag) {
|
|
999
1001
|
for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
|
|
1000
1002
|
const str = buffer[index];
|
|
1001
1003
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
1002
1004
|
if (match) {
|
|
1003
1005
|
endTag = match[1];
|
|
1004
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options,
|
|
1006
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
1005
1007
|
break;
|
|
1006
1008
|
}
|
|
1007
1009
|
}
|
|
1008
1010
|
}
|
|
1009
1011
|
|
|
1010
|
-
//
|
|
1012
|
+
// Look for trailing whitespaces from previously processed text
|
|
1011
1013
|
// which may not be trimmed due to a following comment or an empty
|
|
1012
1014
|
// element which has now been removed
|
|
1013
1015
|
function squashTrailingWhitespace(nextTag) {
|
|
@@ -1038,7 +1040,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1038
1040
|
tag = options.name(tag);
|
|
1039
1041
|
currentTag = tag;
|
|
1040
1042
|
charsPrevTag = tag;
|
|
1041
|
-
if (!
|
|
1043
|
+
if (!inlineTextSet.has(tag)) {
|
|
1042
1044
|
currentChars = '';
|
|
1043
1045
|
}
|
|
1044
1046
|
hasChars = false;
|
|
@@ -1056,7 +1058,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1056
1058
|
removeStartTag();
|
|
1057
1059
|
}
|
|
1058
1060
|
optionalStartTag = '';
|
|
1059
|
-
//
|
|
1061
|
+
// End-tag-followed-by-start-tag omission rules
|
|
1060
1062
|
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
|
1061
1063
|
removeEndTag();
|
|
1062
1064
|
// <colgroup> cannot be omitted if preceding </colgroup> is omitted
|
|
@@ -1066,7 +1068,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1066
1068
|
optionalEndTag = '';
|
|
1067
1069
|
}
|
|
1068
1070
|
|
|
1069
|
-
//
|
|
1071
|
+
// Set whitespace flags for nested tags (eg. <code> within a <pre>)
|
|
1070
1072
|
if (options.collapseWhitespace) {
|
|
1071
1073
|
if (!stackNoTrimWhitespace.length) {
|
|
1072
1074
|
squashTrailingWhitespace(tag);
|
|
@@ -1102,7 +1104,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1102
1104
|
buffer.push(' ');
|
|
1103
1105
|
buffer.push.apply(buffer, parts);
|
|
1104
1106
|
} else if (optional && optionalStartTags.has(tag)) {
|
|
1105
|
-
//
|
|
1107
|
+
// Start tag must never be omitted if it has any attributes
|
|
1106
1108
|
optionalStartTag = tag;
|
|
1107
1109
|
}
|
|
1108
1110
|
|
|
@@ -1119,7 +1121,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1119
1121
|
}
|
|
1120
1122
|
tag = options.name(tag);
|
|
1121
1123
|
|
|
1122
|
-
//
|
|
1124
|
+
// Check if current tag is in a whitespace stack
|
|
1123
1125
|
if (options.collapseWhitespace) {
|
|
1124
1126
|
if (stackNoTrimWhitespace.length) {
|
|
1125
1127
|
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
@@ -1157,7 +1159,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1157
1159
|
}
|
|
1158
1160
|
|
|
1159
1161
|
if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
|
1160
|
-
//
|
|
1162
|
+
// Remove last “element” from buffer
|
|
1161
1163
|
removeStartTag();
|
|
1162
1164
|
optionalStartTag = '';
|
|
1163
1165
|
optionalEndTag = '';
|
|
@@ -1168,7 +1170,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1168
1170
|
buffer.push('</' + tag + '>');
|
|
1169
1171
|
}
|
|
1170
1172
|
charsPrevTag = '/' + tag;
|
|
1171
|
-
if (!
|
|
1173
|
+
if (!inlineElements.has(tag)) {
|
|
1172
1174
|
currentChars = '';
|
|
1173
1175
|
} else if (isElementEmpty) {
|
|
1174
1176
|
currentChars += '|';
|
|
@@ -1207,12 +1209,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1207
1209
|
}
|
|
1208
1210
|
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
1209
1211
|
}
|
|
1210
|
-
} else if (
|
|
1212
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
1211
1213
|
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
1212
1214
|
}
|
|
1213
1215
|
}
|
|
1214
1216
|
if (prevTag || nextTag) {
|
|
1215
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options,
|
|
1217
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
1216
1218
|
} else {
|
|
1217
1219
|
text = collapseWhitespace(text, options, true, true);
|
|
1218
1220
|
}
|
|
@@ -1282,7 +1284,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1282
1284
|
text = prefix + text + suffix;
|
|
1283
1285
|
}
|
|
1284
1286
|
if (options.removeOptionalTags && text) {
|
|
1285
|
-
//
|
|
1287
|
+
// Preceding comments suppress tag omissions
|
|
1286
1288
|
optionalStartTag = '';
|
|
1287
1289
|
optionalEndTag = '';
|
|
1288
1290
|
}
|
|
@@ -1386,4 +1388,4 @@ export const minify = async function (value, options) {
|
|
|
1386
1388
|
return result;
|
|
1387
1389
|
};
|
|
1388
1390
|
|
|
1389
|
-
export default { minify };
|
|
1391
|
+
export default { minify };
|
package/src/htmlparser.js
CHANGED
|
@@ -131,7 +131,7 @@ export class HTMLParser {
|
|
|
131
131
|
let last, prevTag, nextTag;
|
|
132
132
|
while (html) {
|
|
133
133
|
last = html;
|
|
134
|
-
// Make sure we
|
|
134
|
+
// Make sure we’re not in a `script` or `style` element
|
|
135
135
|
if (!lastTag || !special.has(lastTag)) {
|
|
136
136
|
let textEnd = html.indexOf('<');
|
|
137
137
|
if (textEnd === 0) {
|
|
@@ -207,7 +207,7 @@ export class HTMLParser {
|
|
|
207
207
|
html = '';
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
//
|
|
210
|
+
// Next tag
|
|
211
211
|
let nextTagMatch = parseStartTag(html);
|
|
212
212
|
if (nextTagMatch) {
|
|
213
213
|
nextTag = nextTagMatch.tagName;
|
|
@@ -320,9 +320,9 @@ export class HTMLParser {
|
|
|
320
320
|
|
|
321
321
|
const attrs = match.attrs.map(function (args) {
|
|
322
322
|
let name, value, customOpen, customClose, customAssign, quote;
|
|
323
|
-
const ncp = 7; //
|
|
323
|
+
const ncp = 7; // Number of captured parts, scalar
|
|
324
324
|
|
|
325
|
-
//
|
|
325
|
+
// Hackish workaround for FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
326
326
|
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
327
327
|
if (args[3] === '') { delete args[3]; }
|
|
328
328
|
if (args[4] === '') { delete args[4]; }
|
|
@@ -512,13 +512,13 @@ export const HTMLtoDOM = (html, doc) => {
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
// If we
|
|
516
|
-
// the body element
|
|
515
|
+
// If we’re working with a document, inject contents into
|
|
516
|
+
// the `body` element
|
|
517
517
|
let curParentNode = one.body;
|
|
518
518
|
|
|
519
519
|
const parser = new HTMLParser(html, {
|
|
520
520
|
start: function (tagName, attrs, unary) {
|
|
521
|
-
// If it
|
|
521
|
+
// If it’s a pre-built element, then we can ignore
|
|
522
522
|
// its construction
|
|
523
523
|
if (one[tagName]) {
|
|
524
524
|
curParentNode = one[tagName];
|
|
@@ -552,7 +552,7 @@ export const HTMLtoDOM = (html, doc) => {
|
|
|
552
552
|
curParentNode.appendChild(doc.createTextNode(text));
|
|
553
553
|
},
|
|
554
554
|
comment: function (/* text */) {
|
|
555
|
-
//
|
|
555
|
+
// Create comment node
|
|
556
556
|
},
|
|
557
557
|
ignore: function (/* text */) {
|
|
558
558
|
// What to do here?
|
|
@@ -562,4 +562,4 @@ export const HTMLtoDOM = (html, doc) => {
|
|
|
562
562
|
parser.parse();
|
|
563
563
|
|
|
564
564
|
return doc;
|
|
565
|
-
};
|
|
565
|
+
};
|