html-minifier-next 2.1.3 → 2.1.5

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
@@ -1,11 +1,11 @@
1
1
  # HTML Minifier Next
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next)
4
- [![Build status](https://github.com/j9t/html-minifier-next/workflows/CI/badge.svg)](https://github.com/j9t/html-minifier-next/actions?workflow=CI)
4
+ [![Build status](https://github.com/j9t/html-minifier-next/workflows/Tests/badge.svg)](https://github.com/j9t/html-minifier-next/actions)
5
5
 
6
- HTML Minifier Next is a highly **configurable, well-tested, JavaScript-based HTML minifier**.
6
+ HTML Minifier Next (HMN) is a highly **configurable, well-tested, JavaScript-based HTML minifier**.
7
7
 
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.
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) (HMN offers additional features, but is compatible with both). 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’s being updated and documented further in this place.
9
9
 
10
10
  ## Installation
11
11
 
@@ -127,27 +127,29 @@ For lint-like capabilities take a look at [HTMLLint](https://github.com/kangax/h
127
127
 
128
128
  ## Minification comparison
129
129
 
130
- How does HTML Minifier Next compare to other solutions, like [minimize](https://github.com/Swaagie/minimize), [htmlcompressor.com](http://htmlcompressor.com/), [htmlnano](https://github.com/posthtml/htmlnano), and [minify-html](https://github.com/wilsonzlin/minify-html)?
130
+ How does HTML Minifier Next compare to other solutions, like [minimize](https://github.com/Swaagie/minimize), [htmlcompressor.com](http://htmlcompressor.com/), [htmlnano](https://github.com/posthtml/htmlnano), and [minify-html](https://github.com/wilsonzlin/minify-html)? (All with the most aggressive settings, but without [hyper-optimization](https://meiert.com/blog/the-ways-of-writing-html/#toc-hyper-optimized).)
131
131
 
132
- | Site | Original size (KB) | HTML Minifier Next | minimize | htmlcompressor.com | htmlnano | minify-html |
132
+ | Site | Original size (KB) | HTML Minifier Next | minimize | html­compressor.com | htmlnano | minify-html |
133
133
  | --- | --- | --- | --- | --- | --- | --- |
134
- | [A List Apart](https://alistapart.com/) | 64 | **54** | 59 | 57 | 59 | 56 |
135
- | [Amazon](https://www.amazon.com/) | 718 | **645** | 704 | n/a | 667 | 683 |
136
- | [BBC](https://www.bbc.co.uk/) | 739 | **678** | 733 | n/a | 709 | 718 |
137
- | [CSS-Tricks](https://css-tricks.com/) | 165 | **123** | 151 | 148 | 136 | 148 |
138
- | [ECMAScript](https://tc39.es/ecma262/) | 7205 | **6365** | 6585 | n/a | 6871 | 6538 |
139
- | [EFF](https://www.eff.org/) | 58 | **49** | 53 | 53 | 55 | 52 |
140
- | [Eloquent JavaScript](https://eloquentjavascript.net/) | 6 | **5** | 6 | 5 | 6 | 5 |
141
- | [FAZ](https://www.faz.net/aktuell/) | 1814 | **1696** | 1730 | n/a | 1623 | 1735 |
142
- | [Frontend Dogma](https://frontenddogma.com/) | 118 | **113** | 127 | 117 | 131 | 117 |
143
- | [Google](https://www.google.com/) | 50 | **46** | 50 | 50 | 48 | 49 |
144
- | [HTML Minifier Next](https://github.com/j9t/html-minifier-next) | 393 | **274** | 371 | n/a | 339 | 359 |
145
- | [Mastodon](https://mastodon.social/explore) | 35 | **26** | 34 | 34 | 31 | 33 |
146
- | [meiert.com](https://meiert.com/) | 25 | **24** | 26 | 25 | 26 | 25 |
147
- | [NBC](https://www.nbc.com/) | 584 | **533** | 576 | n/a | 559 | 571 |
148
- | [New York Times](https://www.nytimes.com/) | 864 | **738** | 852 | n/a | 801 | 848 |
149
- | [United Nations](https://www.un.org/) | 9 | **7** | 8 | 8 | 8 | 7 |
150
- | [W3C](https://www.w3.org/) | 50 | **36** | 41 | 39 | 40 | 41 |
134
+ | [A List Apart](https://alistapart.com/) | 63 | **54** | 58 | 57 | 55 | 56 |
135
+ | [Amazon](https://www.amazon.com/) | 5 | **3** | **3** | **3** | **3** | n/a |
136
+ | [Apple](https://www.apple.com/) | 212 | **171** | 198 | 195 | 189 | 194 |
137
+ | [BBC](https://www.bbc.co.uk/) | 742 | **683** | 736 | n/a | 698 | 699 |
138
+ | [CSS-Tricks](https://css-tricks.com/) | 11 | **6** | 11 | 10 | 7 | n/a |
139
+ | [ECMAScript](https://tc39.es/ecma262/) | 7205 | **6315** | 6585 | n/a | 6532 | 6538 |
140
+ | [EFF](https://www.eff.org/) | 60 | **51** | 55 | 55 | 54 | 53 |
141
+ | [FAZ](https://www.faz.net/aktuell/) | 1847 | 1728 | 1764 | n/a | **1630** | n/a |
142
+ | [Frontend Dogma](https://frontenddogma.com/) | 117 | **112** | 126 | 117 | 124 | 117 |
143
+ | [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | **16** | n/a |
144
+ | [Ground News](https://ground.news/) | 1509 | **1287** | 1495 | n/a | 1386 | n/a |
145
+ | [HTML](https://html.spec.whatwg.org/multipage/) | 149 | **146** | 155 | 148 | 153 | 148 |
146
+ | [Leanpub](https://leanpub.com/) | 1361 | **1143** | 1355 | n/a | 1150 | n/a |
147
+ | [Mastodon](https://mastodon.social/explore) | 35 | **25** | 33 | 33 | 30 | 33 |
148
+ | [MDN](https://developer.mozilla.org/en-US/) | 104 | **62** | 67 | 68 | 64 | n/a |
149
+ | [Middle East Eye](https://www.middleeasteye.net/) | 221 | **194** | 201 | 202 | 201 | 199 |
150
+ | [SitePoint](https://www.sitepoint.com/) | 469 | **338** | 466 | n/a | 408 | 449 |
151
+ | [United Nations](https://www.un.org/en/) | 153 | **116** | 132 | 125 | 123 | 127 |
152
+ | [W3C](https://www.w3.org/) | 49 | **35** | 40 | 38 | 37 | 38 |
151
153
 
152
154
  ## Options quick reference
153
155
 
@@ -220,9 +222,9 @@ SVG elements are automatically recognized, and when they are minified, both case
220
222
 
221
223
  HTML Minifier Next **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.
222
224
 
223
- 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>`)
225
+ _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>`)_
224
226
 
225
- HTML Minifier Next 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.
227
+ HMN 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.
226
228
 
227
229
  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/).
228
230
 
package/cli.js CHANGED
@@ -33,7 +33,6 @@ import { Command } from 'commander';
33
33
  import { minify } from './src/htmlminifier.js';
34
34
 
35
35
  const require = createRequire(import.meta.url);
36
-
37
36
  const pkg = require('./package.json');
38
37
 
39
38
  const program = new Command();
@@ -21,7 +21,7 @@ async function replaceAsync(str, regex, asyncFn) {
21
21
 
22
22
  /*!
23
23
  * HTML Parser By John Resig (ejohn.org)
24
- * Modified by Juriy "kangax" Zaytsev
24
+ * Modified by Juriy kangax Zaytsev
25
25
  * Original code by Erik Arvidsson, Mozilla Public License
26
26
  * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
27
27
  */
@@ -132,7 +132,7 @@ class HTMLParser {
132
132
  if (!lastTag || !special.has(lastTag)) {
133
133
  let textEnd = html.indexOf('<');
134
134
  if (textEnd === 0) {
135
- // Comment:
135
+ // Comment
136
136
  if (/^<!--/.test(html)) {
137
137
  const commentEnd = html.indexOf('-->');
138
138
 
@@ -160,7 +160,7 @@ class HTMLParser {
160
160
  }
161
161
  }
162
162
 
163
- // Doctype:
163
+ // Doctype
164
164
  const doctypeMatch = html.match(doctype);
165
165
  if (doctypeMatch) {
166
166
  if (handler.doctype) {
@@ -171,7 +171,7 @@ class HTMLParser {
171
171
  continue;
172
172
  }
173
173
 
174
- // End tag:
174
+ // End tag
175
175
  const endTagMatch = html.match(endTag);
176
176
  if (endTagMatch) {
177
177
  html = html.substring(endTagMatch[0].length);
@@ -180,7 +180,7 @@ class HTMLParser {
180
180
  continue;
181
181
  }
182
182
 
183
- // Start tag:
183
+ // Start tag
184
184
  const startTagMatch = parseStartTag(html);
185
185
  if (startTagMatch) {
186
186
  html = startTagMatch.rest;
@@ -273,11 +273,41 @@ class HTMLParser {
273
273
  }
274
274
  }
275
275
 
276
- async function closeIfFound(tagName) {
277
- if (findTag(tagName) >= 0) {
278
- await parseEndTag('', tagName);
276
+ function findTagInCurrentTable(tagName) {
277
+ let pos;
278
+ const needle = tagName.toLowerCase();
279
+ for (pos = stack.length - 1; pos >= 0; pos--) {
280
+ const currentTag = stack[pos].tag.toLowerCase();
281
+ if (currentTag === needle) {
282
+ return pos;
283
+ }
284
+ // Stop searching if we hit a table boundary
285
+ if (currentTag === 'table') {
286
+ break;
287
+ }
288
+ }
289
+ return -1;
290
+ }
291
+
292
+ async function parseEndTagAt(pos) {
293
+ // Close all open elements up to pos (mirrors parseEndTag’s core branch)
294
+ for (let i = stack.length - 1; i >= pos; i--) {
295
+ if (handler.end) {
296
+ await handler.end(stack[i].tag, stack[i].attrs, true);
297
+ }
298
+ }
299
+ stack.length = pos;
300
+ lastTag = pos && stack[pos - 1].tag;
301
+ }
302
+
303
+ async function closeIfFoundInCurrentTable(tagName) {
304
+ const pos = findTagInCurrentTable(tagName);
305
+ if (pos >= 0) {
306
+ // Close at the specific index to avoid re-searching
307
+ await parseEndTagAt(pos);
279
308
  return true;
280
309
  }
310
+ return false;
281
311
  }
282
312
 
283
313
  async function handleStartTag(match) {
@@ -288,10 +318,15 @@ class HTMLParser {
288
318
  if (lastTag === 'p' && nonPhrasing.has(tagName)) {
289
319
  await parseEndTag('', lastTag);
290
320
  } else if (tagName === 'tbody') {
291
- await closeIfFound('thead');
321
+ await closeIfFoundInCurrentTable('thead');
292
322
  } else if (tagName === 'tfoot') {
293
- if (!await closeIfFound('tbody')) {
294
- await closeIfFound('thead');
323
+ if (!await closeIfFoundInCurrentTable('tbody')) {
324
+ await closeIfFoundInCurrentTable('thead');
325
+ }
326
+ } else if (tagName === 'thead') {
327
+ // If a `tbody` or `tfoot` is open in the current table, close it
328
+ if (!await closeIfFoundInCurrentTable('tbody')) {
329
+ await closeIfFoundInCurrentTable('tfoot');
295
330
  }
296
331
  }
297
332
  if (tagName === 'col' && findTag('colgroup') < 0) {
@@ -60211,7 +60211,7 @@ async function replaceAsync(str, regex, asyncFn) {
60211
60211
 
60212
60212
  /*!
60213
60213
  * HTML Parser By John Resig (ejohn.org)
60214
- * Modified by Juriy "kangax" Zaytsev
60214
+ * Modified by Juriy kangax Zaytsev
60215
60215
  * Original code by Erik Arvidsson, Mozilla Public License
60216
60216
  * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
60217
60217
  */
@@ -60322,7 +60322,7 @@ class HTMLParser {
60322
60322
  if (!lastTag || !special.has(lastTag)) {
60323
60323
  let textEnd = html.indexOf('<');
60324
60324
  if (textEnd === 0) {
60325
- // Comment:
60325
+ // Comment
60326
60326
  if (/^<!--/.test(html)) {
60327
60327
  const commentEnd = html.indexOf('-->');
60328
60328
 
@@ -60350,7 +60350,7 @@ class HTMLParser {
60350
60350
  }
60351
60351
  }
60352
60352
 
60353
- // Doctype:
60353
+ // Doctype
60354
60354
  const doctypeMatch = html.match(doctype);
60355
60355
  if (doctypeMatch) {
60356
60356
  if (handler.doctype) {
@@ -60361,7 +60361,7 @@ class HTMLParser {
60361
60361
  continue;
60362
60362
  }
60363
60363
 
60364
- // End tag:
60364
+ // End tag
60365
60365
  const endTagMatch = html.match(endTag);
60366
60366
  if (endTagMatch) {
60367
60367
  html = html.substring(endTagMatch[0].length);
@@ -60370,7 +60370,7 @@ class HTMLParser {
60370
60370
  continue;
60371
60371
  }
60372
60372
 
60373
- // Start tag:
60373
+ // Start tag
60374
60374
  const startTagMatch = parseStartTag(html);
60375
60375
  if (startTagMatch) {
60376
60376
  html = startTagMatch.rest;
@@ -60463,11 +60463,41 @@ class HTMLParser {
60463
60463
  }
60464
60464
  }
60465
60465
 
60466
- async function closeIfFound(tagName) {
60467
- if (findTag(tagName) >= 0) {
60468
- await parseEndTag('', tagName);
60466
+ function findTagInCurrentTable(tagName) {
60467
+ let pos;
60468
+ const needle = tagName.toLowerCase();
60469
+ for (pos = stack.length - 1; pos >= 0; pos--) {
60470
+ const currentTag = stack[pos].tag.toLowerCase();
60471
+ if (currentTag === needle) {
60472
+ return pos;
60473
+ }
60474
+ // Stop searching if we hit a table boundary
60475
+ if (currentTag === 'table') {
60476
+ break;
60477
+ }
60478
+ }
60479
+ return -1;
60480
+ }
60481
+
60482
+ async function parseEndTagAt(pos) {
60483
+ // Close all open elements up to pos (mirrors parseEndTag’s core branch)
60484
+ for (let i = stack.length - 1; i >= pos; i--) {
60485
+ if (handler.end) {
60486
+ await handler.end(stack[i].tag, stack[i].attrs, true);
60487
+ }
60488
+ }
60489
+ stack.length = pos;
60490
+ lastTag = pos && stack[pos - 1].tag;
60491
+ }
60492
+
60493
+ async function closeIfFoundInCurrentTable(tagName) {
60494
+ const pos = findTagInCurrentTable(tagName);
60495
+ if (pos >= 0) {
60496
+ // Close at the specific index to avoid re-searching
60497
+ await parseEndTagAt(pos);
60469
60498
  return true;
60470
60499
  }
60500
+ return false;
60471
60501
  }
60472
60502
 
60473
60503
  async function handleStartTag(match) {
@@ -60478,10 +60508,15 @@ class HTMLParser {
60478
60508
  if (lastTag === 'p' && nonPhrasing.has(tagName)) {
60479
60509
  await parseEndTag('', lastTag);
60480
60510
  } else if (tagName === 'tbody') {
60481
- await closeIfFound('thead');
60511
+ await closeIfFoundInCurrentTable('thead');
60482
60512
  } else if (tagName === 'tfoot') {
60483
- if (!await closeIfFound('tbody')) {
60484
- await closeIfFound('thead');
60513
+ if (!await closeIfFoundInCurrentTable('tbody')) {
60514
+ await closeIfFoundInCurrentTable('thead');
60515
+ }
60516
+ } else if (tagName === 'thead') {
60517
+ // If a `tbody` or `tfoot` is open in the current table, close it
60518
+ if (!await closeIfFoundInCurrentTable('tbody')) {
60519
+ await closeIfFoundInCurrentTable('tfoot');
60485
60520
  }
60486
60521
  }
60487
60522
  if (tagName === 'col' && findTag('colgroup') < 0) {
@@ -60217,7 +60217,7 @@
60217
60217
 
60218
60218
  /*!
60219
60219
  * HTML Parser By John Resig (ejohn.org)
60220
- * Modified by Juriy "kangax" Zaytsev
60220
+ * Modified by Juriy kangax Zaytsev
60221
60221
  * Original code by Erik Arvidsson, Mozilla Public License
60222
60222
  * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
60223
60223
  */
@@ -60328,7 +60328,7 @@
60328
60328
  if (!lastTag || !special.has(lastTag)) {
60329
60329
  let textEnd = html.indexOf('<');
60330
60330
  if (textEnd === 0) {
60331
- // Comment:
60331
+ // Comment
60332
60332
  if (/^<!--/.test(html)) {
60333
60333
  const commentEnd = html.indexOf('-->');
60334
60334
 
@@ -60356,7 +60356,7 @@
60356
60356
  }
60357
60357
  }
60358
60358
 
60359
- // Doctype:
60359
+ // Doctype
60360
60360
  const doctypeMatch = html.match(doctype);
60361
60361
  if (doctypeMatch) {
60362
60362
  if (handler.doctype) {
@@ -60367,7 +60367,7 @@
60367
60367
  continue;
60368
60368
  }
60369
60369
 
60370
- // End tag:
60370
+ // End tag
60371
60371
  const endTagMatch = html.match(endTag);
60372
60372
  if (endTagMatch) {
60373
60373
  html = html.substring(endTagMatch[0].length);
@@ -60376,7 +60376,7 @@
60376
60376
  continue;
60377
60377
  }
60378
60378
 
60379
- // Start tag:
60379
+ // Start tag
60380
60380
  const startTagMatch = parseStartTag(html);
60381
60381
  if (startTagMatch) {
60382
60382
  html = startTagMatch.rest;
@@ -60469,11 +60469,41 @@
60469
60469
  }
60470
60470
  }
60471
60471
 
60472
- async function closeIfFound(tagName) {
60473
- if (findTag(tagName) >= 0) {
60474
- await parseEndTag('', tagName);
60472
+ function findTagInCurrentTable(tagName) {
60473
+ let pos;
60474
+ const needle = tagName.toLowerCase();
60475
+ for (pos = stack.length - 1; pos >= 0; pos--) {
60476
+ const currentTag = stack[pos].tag.toLowerCase();
60477
+ if (currentTag === needle) {
60478
+ return pos;
60479
+ }
60480
+ // Stop searching if we hit a table boundary
60481
+ if (currentTag === 'table') {
60482
+ break;
60483
+ }
60484
+ }
60485
+ return -1;
60486
+ }
60487
+
60488
+ async function parseEndTagAt(pos) {
60489
+ // Close all open elements up to pos (mirrors parseEndTag’s core branch)
60490
+ for (let i = stack.length - 1; i >= pos; i--) {
60491
+ if (handler.end) {
60492
+ await handler.end(stack[i].tag, stack[i].attrs, true);
60493
+ }
60494
+ }
60495
+ stack.length = pos;
60496
+ lastTag = pos && stack[pos - 1].tag;
60497
+ }
60498
+
60499
+ async function closeIfFoundInCurrentTable(tagName) {
60500
+ const pos = findTagInCurrentTable(tagName);
60501
+ if (pos >= 0) {
60502
+ // Close at the specific index to avoid re-searching
60503
+ await parseEndTagAt(pos);
60475
60504
  return true;
60476
60505
  }
60506
+ return false;
60477
60507
  }
60478
60508
 
60479
60509
  async function handleStartTag(match) {
@@ -60484,10 +60514,15 @@
60484
60514
  if (lastTag === 'p' && nonPhrasing.has(tagName)) {
60485
60515
  await parseEndTag('', lastTag);
60486
60516
  } else if (tagName === 'tbody') {
60487
- await closeIfFound('thead');
60517
+ await closeIfFoundInCurrentTable('thead');
60488
60518
  } else if (tagName === 'tfoot') {
60489
- if (!await closeIfFound('tbody')) {
60490
- await closeIfFound('thead');
60519
+ if (!await closeIfFoundInCurrentTable('tbody')) {
60520
+ await closeIfFoundInCurrentTable('thead');
60521
+ }
60522
+ } else if (tagName === 'thead') {
60523
+ // If a `tbody` or `tfoot` is open in the current table, close it
60524
+ if (!await closeIfFoundInCurrentTable('tbody')) {
60525
+ await closeIfFoundInCurrentTable('tfoot');
60491
60526
  }
60492
60527
  }
60493
60528
  if (tagName === 'col' && findTag('colgroup') < 0) {