html-minifier-next 1.3.2 → 1.4.0

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
@@ -40,14 +40,16 @@ html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --fil
40
40
  # Process multiple file extensions (CLI method)
41
41
  html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html,htm,php
42
42
 
43
- # Using configuration file with fileExt setting
43
+ # Using configuration file that sets `fileExt` (e.g., `"fileExt": "html,htm"`)
44
44
  html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
45
45
 
46
46
  # Process all files (default behavior)
47
47
  html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
48
+ # Note: When processing all files, non-HTML files will also be read as UTF‑8 and passed to the minifier.
49
+ # Consider restricting with “--file-ext” to avoid touching binaries (e.g., images, archives).
48
50
  ```
49
51
 
50
- ### CLI Options
52
+ ### CLI options
51
53
 
52
54
  Use `html-minifier-next --help` to check all available options:
53
55
 
@@ -59,9 +61,9 @@ Use `html-minifier-next --help` to check all available options:
59
61
  | `-o --output <file>` | Specify output file (single file mode) | `-o minified.html` |
60
62
  | `-c --config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
61
63
 
62
- ### Configuration Files
64
+ ### Configuration file
63
65
 
64
- You can also use a configuration file to specify options:
66
+ You can also use a configuration file to specify options. The file can be either JSON format or a JavaScript module that exports the configuration object:
65
67
 
66
68
  **JSON configuration example:**
67
69
 
@@ -73,6 +75,16 @@ You can also use a configuration file to specify options:
73
75
  }
74
76
  ```
75
77
 
78
+ **JavaScript module configuration example:**
79
+
80
+ ```js
81
+ module.exports = {
82
+ collapseWhitespace: true,
83
+ removeComments: true,
84
+ fileExt: "html,htm"
85
+ };
86
+ ```
87
+
76
88
  **Using a configuration file:**
77
89
 
78
90
  ```bash
@@ -85,13 +97,28 @@ html-minifier-next --config-file=html-minifier.json --file-ext=xml --input-dir=s
85
97
 
86
98
  ### Node.js
87
99
 
100
+ ESM with Node.js ≥16.14:
101
+
88
102
  ```js
89
- const { minify } = require('html-minifier-next');
103
+ import { minify } from 'html-minifier-next';
90
104
 
91
105
  const result = await minify('<p title="blah" id="moo">foo</p>', {
92
106
  removeAttributeQuotes: true,
93
107
  });
94
- result; // “<p title=blah id=moo>foo</p>”
108
+ console.log(result); // “<p title=blah id=moo>foo</p>”
109
+ ```
110
+
111
+ CommonJS:
112
+
113
+ ```js
114
+ const { minify } = require('html-minifier-next');
115
+
116
+ (async () => {
117
+ const result = await minify('<p title="blah" id="moo">foo</p>', {
118
+ removeAttributeQuotes: true,
119
+ });
120
+ console.log(result);
121
+ })();
95
122
  ```
96
123
 
97
124
  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).
@@ -102,24 +129,24 @@ For lint-like capabilities take a look at [HTMLLint](https://github.com/kangax/h
102
129
 
103
130
  How does HTML Minifier compare to other solutions, like [minimize](https://github.com/Swaagie/minimize) or [htmlcompressor.com](http://htmlcompressor.com/)?
104
131
 
105
- | Site | Original size (KB) | HTML Minifier | minimize | htmlcompressor.com |
106
- | --- | --- |---------------| --- | --- |
132
+ | Site | Original size (KB) | HTMLMinifier | minimize | htmlcompressor.com |
133
+ | --- | --- | --- | --- | --- |
107
134
  | [A List Apart](https://alistapart.com/) | 64 | **54** | 59 | 57 |
108
- | [Amazon](https://www.amazon.com/) | 206 | **195** | 203 | 200 |
109
- | [BBC](https://www.bbc.co.uk/) | 767 | **703** | 761 | n/a |
110
- | [CSS-Tricks](https://css-tricks.com/) | 166 | **124** | 152 | 148 |
111
- | [ECMAScript](https://tc39.es/ecma262/) | 7204 | **6361** | 6581 | n/a |
112
- | [EFF](https://www.eff.org/) | 57 | **48** | 52 | 52 |
113
- | [FAZ](https://www.faz.net/aktuell/) | 1767 | **1641** | 1679 | n/a |
114
- | [Frontend Dogma](https://frontenddogma.com/) | 119 | **114** | 128 | 118 |
115
- | [Google](https://www.google.com/) | 51 | **46** | 50 | 50 |
116
- | [HTML Minifier](https://github.com/kangax/html-minifier) | 373 | **250** | 349 | n/a |
117
- | [Mastodon](https://mastodon.social/explore) | 37 | **28** | 36 | 36 |
118
- | [NBC](https://www.nbc.com/) | 601 | **549** | 593 | n/a |
119
- | [New York Times](https://www.nytimes.com/) | 822 | **701** | 811 | n/a |
135
+ | [Amazon](https://www.amazon.com/) | 707 | **635** | 693 | n/a |
136
+ | [BBC](https://www.bbc.co.uk/) | 700 | **642** | 694 | n/a |
137
+ | [CSS-Tricks](https://css-tricks.com/) | 167 | **124** | 153 | 149 |
138
+ | [ECMAScript](https://tc39.es/ecma262/) | 7205 | **6365** | 6585 | n/a |
139
+ | [EFF](https://www.eff.org/) | 58 | **49** | 52 | 52 |
140
+ | [Eloquent JavaScript](https://eloquentjavascript.net/) | 6 | **5** | 6 | 5 |
141
+ | [FAZ](https://www.faz.net/aktuell/) | 1848 | **1727** | 1763 | n/a |
142
+ | [Frontend Dogma](https://frontenddogma.com/) | 118 | **113** | 127 | 117 |
143
+ | [Google](https://www.google.com/) | 50 | **46** | 50 | 50 |
144
+ | [HTMLMinifier](https://github.com/kangax/html-minifier) | 371 | **249** | 347 | n/a |
145
+ | [Mastodon](https://mastodon.social/explore) | 35 | **26** | 34 | 34 |
146
+ | [NBC](https://www.nbc.com/) | 579 | **528** | 572 | n/a |
147
+ | [New York Times](https://www.nytimes.com/) | 733 | **625** | 722 | n/a |
120
148
  | [United Nations](https://www.un.org/) | 9 | **7** | 8 | 8 |
121
- | [W3C](https://www.w3.org/) | 50 | **36** | 41 | 39 |
122
- | [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | 225 | **204** | 215 | 215 |
149
+ | [W3C](https://www.w3.org/) | 51 | **36** | 42 | 40 |
123
150
 
124
151
  ## Options quick reference
125
152
 
@@ -139,7 +166,6 @@ Most of the options are disabled by default.
139
166
  | `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
140
167
  | `customEventAttributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g. `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
141
168
  | `decodeEntities` | Use direct Unicode characters whenever possible | `false` |
142
- | `fileExt` | File extensions to process | `[]` (process all files) |
143
169
  | `html5` | Parse input according to HTML5 specifications | `true` |
144
170
  | `ignoreCustomComments` | Array of regexes that allow to ignore certain comments, when matched | `[ /^!/, /^\s*#/ ]` |
145
171
  | `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
package/cli.js CHANGED
@@ -197,7 +197,7 @@ program.option('-c --config-file <file>', 'Use config file', function (configPat
197
197
  });
198
198
  program.option('--input-dir <dir>', 'Specify an input directory');
199
199
  program.option('--output-dir <dir>', 'Specify an output directory');
200
- program.option('--file-ext <text>', 'Specify file extension(s) to be read, e.g. “html” or “html,htm”');
200
+ program.option('--file-ext <extensions>', 'Specify file extension(s) to process (comma-separated), e.g., “html” or “html,htm,php”');
201
201
 
202
202
  let content;
203
203
  program.arguments('[files...]').action(function (files) {
@@ -250,17 +250,16 @@ function processFile(inputFile, outputFile) {
250
250
  }
251
251
 
252
252
  function parseFileExtensions(fileExt) {
253
- if (!fileExt) {
254
- return [];
255
- }
256
- return fileExt
253
+ if (!fileExt) return [];
254
+ const list = fileExt
257
255
  .split(',')
258
256
  .map(ext => ext.trim().replace(/^\.+/, '').toLowerCase())
259
257
  .filter(ext => ext.length > 0);
258
+ return [...new Set(list)];
260
259
  }
261
260
 
262
261
  function shouldProcessFile(filename, fileExtensions) {
263
- if (fileExtensions.length === 0) {
262
+ if (!fileExtensions || fileExtensions.length === 0) {
264
263
  return true; // No extensions specified, process all files
265
264
  }
266
265
 
@@ -268,8 +267,11 @@ function shouldProcessFile(filename, fileExtensions) {
268
267
  return fileExtensions.includes(fileExt);
269
268
  }
270
269
 
271
- function processDirectory(inputDir, outputDir, fileExt) {
272
- const extensions = parseFileExtensions(fileExt);
270
+ function processDirectory(inputDir, outputDir, extensions) {
271
+ // If first call provided a string, normalize once; otherwise assume pre-parsed array
272
+ if (typeof extensions === 'string') {
273
+ extensions = parseFileExtensions(extensions);
274
+ }
273
275
 
274
276
  fs.readdir(inputDir, function (err, files) {
275
277
  if (err) {
@@ -284,7 +286,7 @@ function processDirectory(inputDir, outputDir, fileExt) {
284
286
  if (err) {
285
287
  fatal('Cannot read ' + inputFile + '\n' + err.message);
286
288
  } else if (stat.isDirectory()) {
287
- processDirectory(inputFile, outputFile, fileExt);
289
+ processDirectory(inputFile, outputFile, extensions);
288
290
  } else if (shouldProcessFile(file, extensions)) {
289
291
  mkdir(outputDir, function () {
290
292
  processFile(inputFile, outputFile);
@@ -319,8 +321,9 @@ const writeMinify = async () => {
319
321
 
320
322
  const { inputDir, outputDir, fileExt } = programOptions;
321
323
 
322
- // Resolve file extensions: CLI argument takes priority over config file
323
- const resolvedFileExt = fileExt || config.fileExt;
324
+ // Resolve file extensions: CLI argument takes priority over config file, even if empty string
325
+ const hasCliFileExt = program.getOptionValueSource('fileExt') === 'cli';
326
+ const resolvedFileExt = hasCliFileExt ? fileExt : config.fileExt;
324
327
 
325
328
  if (inputDir || outputDir) {
326
329
  if (!inputDir) {
package/package.json CHANGED
@@ -88,5 +88,5 @@
88
88
  "test:watch": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --watch"
89
89
  },
90
90
  "type": "module",
91
- "version": "1.3.2"
91
+ "version": "1.4.0"
92
92
  }
@@ -260,7 +260,7 @@ function isSrcset(attrName, tag) {
260
260
  return attrName === 'srcset' && srcsetTags.has(tag);
261
261
  }
262
262
 
263
- async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
263
+ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
264
264
  if (isEventAttribute(attrName, options)) {
265
265
  attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
266
266
  return options.minifyJS(attrValue, true);
@@ -318,6 +318,13 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
318
318
  } else if (isMediaQuery(tag, attrs, attrName)) {
319
319
  attrValue = trimWhitespace(attrValue);
320
320
  return options.minifyCSS(attrValue, 'media');
321
+ } else if (tag === 'iframe' && attrName === 'srcdoc') {
322
+ // Recursively minify HTML content within srcdoc attribute
323
+ // Fast-path: skip if nothing would change
324
+ if (!shouldMinifyInnerHTML(options)) {
325
+ return attrValue;
326
+ }
327
+ return minifyHTMLSelf(attrValue, options, true);
321
328
  }
322
329
  return attrValue;
323
330
  }
@@ -557,7 +564,7 @@ async function normalizeAttr(attr, attrs, tag, options) {
557
564
  }
558
565
 
559
566
  if (attrValue) {
560
- attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs);
567
+ attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
561
568
  }
562
569
 
563
570
  if (options.removeEmptyAttributes &&
@@ -632,6 +639,17 @@ function identityAsync(value) {
632
639
  return Promise.resolve(value);
633
640
  }
634
641
 
642
+ function shouldMinifyInnerHTML(options) {
643
+ return Boolean(
644
+ options.collapseWhitespace ||
645
+ options.removeComments ||
646
+ options.removeOptionalTags ||
647
+ options.minifyJS !== identity ||
648
+ options.minifyCSS !== identityAsync ||
649
+ options.minifyURLs !== identity
650
+ );
651
+ }
652
+
635
653
  const processOptions = (inputOptions) => {
636
654
  const options = {
637
655
  name: function (name) {