html-minifier-next 6.2.6 → 6.2.8

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
@@ -4,17 +4,11 @@
4
4
 
5
5
  Your web page optimization precision tool: HTML Minifier Next (HMN) is a **super-configurable, well-tested, JavaScript-based HTML minifier** able to also handle in-document CSS, JavaScript, and SVG minification.
6
6
 
7
- The project was based on [HTML Minifier Terser (HMT)](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy “kangax” Zaytsev’s HTML Minifier (HM)](https://github.com/kangax/html-minifier); as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for several years. HMN offers additional features and has been optimized for speed. While an independent project, it is still backwards-compatible with HMT and HM.
7
+ The project was based on [HTML Minifier Terser (HMT)](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy “kangax” Zaytsev’s HTML Minifier (HM)](https://github.com/kangax/html-minifier). It is now **the official successor to HTML Minifier**. HMN is maintained, easier to use, offers new features, and has been optimized for speed.
8
8
 
9
9
  ## Installation
10
10
 
11
- From npm for use as a command-line app:
12
-
13
- ```shell
14
- npm i -g html-minifier-next
15
- ```
16
-
17
- Directly with npx:
11
+ For use as a command-line app, use npx:
18
12
 
19
13
  ```shell
20
14
  npx html-minifier-next --help
@@ -22,30 +16,30 @@ npx html-minifier-next --help
22
16
 
23
17
  (For immediate, zero-config use in the current folder: `npx html-minifier-next --zero`)
24
18
 
25
- From npm for programmatic use:
19
+ For programmatic use, install as a development dependency:
26
20
 
27
21
  ```shell
28
- npm i html-minifier-next
22
+ npm i -D html-minifier-next
29
23
  ```
30
24
 
31
25
  ## General usage
32
26
 
33
27
  ### CLI
34
28
 
35
- Use `html-minifier-next --help` to check all available options:
29
+ Use `npx html-minifier-next --help` to check all available options:
36
30
 
37
31
  | Option | Description | Example |
38
32
  | --- | --- | --- |
39
- | `--zero`, `-z` | Minify all HTML files in the current folder and its subfolders in place (except node_modules), using comprehensive settings (standalone—flag is ignored when combined with other options) | `html-minifier-next --zero` |
33
+ | `--zero`, `-z` | Minify all HTML files in the current folder and its subfolders in place (except node_modules), using comprehensive settings (standalone—flag is ignored when combined with other options) | `npx html-minifier-next --zero` |
40
34
  | `--input-dir <dir>`, `-I <dir>` | Specify an input directory | `--input-dir=src` |
41
35
  | `--ignore-dir <patterns>`, `-X <patterns>` | Exclude directories—relative to input directory—from processing (comma-separated, overrides config file setting) | `--ignore-dir=libs`, `--ignore-dir=libs,vendor,node_modules` |
42
36
  | `--output-dir <dir>`, `-O <dir>` | Specify an output directory | `--output-dir=dist` |
43
- | `--output <file>`, `-o <file>` | Specify output file (reads from file arguments or STDIN) | File to file: `html-minifier-next input.html -o output.html`<br>Pipe to file: `cat input.html \| html-minifier-next -o output.html`<br>File to STDOUT: `html-minifier-next input.html` |
37
+ | `--output <file>`, `-o <file>` | Specify output file (reads from file arguments or STDIN) | File to file: `npx html-minifier-next input.html -o output.html`<br>Pipe to file: `cat input.html \| npx html-minifier-next -o output.html`<br>File to STDOUT: `npx html-minifier-next input.html` |
44
38
  | `--file-ext <extensions>`, `-f <extensions>` | Specify file extension(s) to process (comma-separated, overrides config file setting); defaults to `html,htm,shtml,shtm`; use `*` for all files | `--file-ext=html,php`, `--file-ext='*'` |
45
39
  | `--preset <name>`, `-p <name>` | Use a preset configuration (conservative or comprehensive) | `--preset=conservative` |
46
40
  | `--config-file <file>`, `-c <file>` | Use a configuration file | `--config-file=html-minifier.json` |
47
- | `--verbose`, `-v` | Show detailed processing information (active options, file statistics) | `html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
48
- | `--dry`, `-d` | Dry run: Process and report statistics without writing output | `html-minifier-next input.html --dry --collapse-whitespace` |
41
+ | `--verbose`, `-v` | Show detailed processing information (active options, file statistics) | `npx html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
42
+ | `--dry`, `-d` | Dry run: Process and report statistics without writing output | `npx html-minifier-next input.html --dry --collapse-whitespace` |
49
43
 
50
44
  ### Configuration file
51
45
 
@@ -113,14 +107,14 @@ To review the specific options set, [presets.js](https://github.com/j9t/html-min
113
107
 
114
108
  ```shell
115
109
  # Via CLI flag
116
- html-minifier-next --preset conservative input.html
110
+ npx html-minifier-next --preset conservative input.html
117
111
 
118
112
  # Via config file
119
- html-minifier-next --config-file=html-minifier.json input.html
113
+ npx html-minifier-next --config-file=html-minifier.json input.html
120
114
  # where html-minifier.json contains: { "preset": "conservative" }
121
115
 
122
116
  # Override preset options
123
- html-minifier-next --preset conservative --remove-empty-attributes input.html
117
+ npx html-minifier-next --preset conservative --remove-empty-attributes input.html
124
118
  ```
125
119
 
126
120
  **Priority order:** Presets are applied first, then config file options, then CLI flags. This allows you to start with a preset and customize as needed.
@@ -246,14 +240,12 @@ const result = await minify(html, {
246
240
  * `terser` (default): The standard JavaScript minifier with excellent compression
247
241
  * [`swc`](https://swc.rs/): Rust-based minifier that’s significantly faster than Terser (requires separate installation)
248
242
 
249
- **To use SWC**, install it as a dependency:
243
+ **To use SWC**, install it as a development dependency:
250
244
 
251
245
  ```shell
252
- npm i @swc/core
246
+ npm i -D @swc/core
253
247
  ```
254
248
 
255
- (Build-only users may want to install it as a dev dependency: `npm i -D @swc/core`.)
256
-
257
249
  **Important:** Inline event handlers (e.g., `onclick="return false"`) always use Terser regardless of the `engine` setting, as SWC doesn’t support bare return statements. This is handled automatically—you don’t need to do anything special.
258
250
 
259
251
  You can pass engine-specific configuration options:
@@ -340,7 +332,7 @@ const result = await minify(html, {
340
332
  **Via CLI flags:**
341
333
 
342
334
  ```shell
343
- html-minifier-next --minify-css --cache-css 750 --minify-js --cache-js 250 --minify-svg --cache-svg 100 input.html
335
+ npx html-minifier-next --minify-css --cache-css 750 --minify-js --cache-js 250 --minify-svg --cache-svg 100 input.html
344
336
  ```
345
337
 
346
338
  **Via environment variables:**
@@ -349,7 +341,7 @@ html-minifier-next --minify-css --cache-css 750 --minify-js --cache-js 250 --min
349
341
  export HMN_CACHE_CSS=750
350
342
  export HMN_CACHE_JS=250
351
343
  export HMN_CACHE_SVG=100
352
- html-minifier-next --minify-css --minify-js --minify-svg input.html
344
+ npx html-minifier-next --minify-css --minify-js --minify-svg input.html
353
345
  ```
354
346
 
355
347
  **Configuration file:**
@@ -390,12 +382,8 @@ Please see [**the Minifier Benchmarks project**](https://github.com/j9t/minifier
390
382
  **Sample command line:**
391
383
 
392
384
  ```shell
393
- html-minifier-next --collapse-whitespace --remove-comments --minify-js --input-dir=. --output-dir=example
394
- ```
385
+ npx html-minifier-next --collapse-whitespace --remove-comments --minify-js --input-dir=. --output-dir=example
395
386
 
396
- Example using npx:
397
-
398
- ```shell
399
387
  npx html-minifier-next --input-dir=test --preset comprehensive --output-dir example
400
388
  ```
401
389
 
@@ -403,39 +391,39 @@ npx html-minifier-next --input-dir=test --preset comprehensive --output-dir exam
403
391
 
404
392
  ```shell
405
393
  # Process default extensions (html, htm, shtml, shtm)
406
- html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
394
+ npx html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
407
395
 
408
396
  # Process only specific extensions
409
- html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html,php
397
+ npx html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html,php
410
398
 
411
399
  # Using configuration file that sets `fileExt` (e.g., `"fileExt": "html,php"`)
412
- html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
400
+ npx html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
413
401
 
414
402
  # Process all files (explicit wildcard)
415
- html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext='*'
403
+ npx html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext='*'
416
404
  ```
417
405
 
418
406
  **Exclude directories from processing:**
419
407
 
420
408
  ```shell
421
409
  # Ignore a single directory
422
- html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs
410
+ npx html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs
423
411
 
424
412
  # Ignore multiple directories
425
- html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs,vendor,node_modules
413
+ npx html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs,vendor,node_modules
426
414
 
427
415
  # Ignore by relative path (only ignores src/static/libs, not other “libs” directories)
428
- html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=static/libs
416
+ npx html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=static/libs
429
417
  ```
430
418
 
431
419
  **Dry run mode (preview outcome without writing files):**
432
420
 
433
421
  ```shell
434
422
  # Preview with output file
435
- html-minifier-next input.html -o output.html --dry --collapse-whitespace
423
+ npx html-minifier-next input.html -o output.html --dry --collapse-whitespace
436
424
 
437
425
  # Preview directory processing with statistics per file and total
438
- html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
426
+ npx html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
439
427
  # Output: [DRY RUN] Would process directory: src → dist
440
428
  # index.html: 1,234 → 892 bytes (-342, 27.7%)
441
429
  # about.html: 2,100 → 1,654 bytes (-446, 21.2%)
@@ -447,7 +435,7 @@ html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
447
435
 
448
436
  ```shell
449
437
  # Show processing details while minifying
450
- html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace
438
+ npx html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace
451
439
  # Output: CLI options: collapseWhitespace
452
440
  # ✓ src/index.html: 1,234 → 892 bytes (-342, 27.7%)
453
441
  # ✓ src/about.html: 2,100 → 1,654 bytes (-446, 21.2%)
@@ -455,7 +443,7 @@ html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whites
455
443
  # Total: 3,334 → 2,546 bytes (-788, 23.6%)
456
444
 
457
445
  # `--dry` automatically enables verbose output
458
- html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
446
+ npx html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
459
447
  ```
460
448
 
461
449
  ## Special cases
@@ -560,13 +548,13 @@ ignoreCustomFragments: [/\{%[\s\S]{0,1000}?%\}/, /\{\{[\s\S]{0,500}?\}\}/]
560
548
  **CLI (via config file—recommended):**
561
549
 
562
550
  ```shell
563
- html-minifier-next --config-file=config.json input.html
551
+ npx html-minifier-next --config-file=config.json input.html
564
552
  ```
565
553
 
566
554
  **CLI (inline—not recommended due to complex escaping):**
567
555
 
568
556
  ```shell
569
- html-minifier-next --ignore-custom-fragments '[\\\"\\\\{%[\\\\s\\\\S]{0,1000}?%\\\\}\\\"]' input.html
557
+ npx html-minifier-next --ignore-custom-fragments '[\\\"\\\\{%[\\\\s\\\\S]{0,1000}?%\\\\}\\\"]' input.html
570
558
  ```
571
559
 
572
560
  For CLI usage, using a config file is strongly recommended to avoid complex shell and JSON escaping.
@@ -577,7 +565,7 @@ For CLI usage, using a config file is strongly recommended to avoid complex shel
577
565
  \{%[\s\S]{0,1000}?%\} \{\{[\s\S]{0,500}?\}\}
578
566
  ```
579
567
 
580
- ## Running HTML Minifier Next locally
568
+ ## Working on HTML Minifier Next
581
569
 
582
570
  ### Local server
583
571
 
@@ -609,6 +597,6 @@ With many thanks to the previous authors of and contributors to HTML Minifier, e
609
597
 
610
598
  You might like some of my other work:
611
599
 
612
- * Optimization tools: HTML Minifier Next · [ObsoHTML](https://github.com/j9t/obsohtml) · [Image Guard](https://github.com/j9t/image-guard) · [Compressor.js Next](https://github.com/j9t/compressorjs-next) · [.htaccess Punk](https://github.com/j9t/htaccess-punk)
600
+ * Optimization tools: [hihtml](https://github.com/j9t/hihtml) · HTML Minifier Next · [ObsoHTML](https://github.com/j9t/obsohtml) · [Image Guard](https://github.com/j9t/image-guard) · [Compressor.js Next](https://github.com/j9t/compressorjs-next) · [.htaccess Punk](https://github.com/j9t/htaccess-punk)
613
601
  * Defense tools: [IA Defensa](https://iadefensa.com/solutions/)
614
602
  * Resources for quality web development: [Articles](https://meiert.com/topics/development/) · [Books](https://meiert.com/topics/books/) (including [_On Web Development_](https://meiert.com/blog/on-web-development-2/)) · [News](https://frontenddogma.com/) · [Terminology](https://webglossary.info/)
package/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * html-minifier-next CLI tool
5
5
  *
6
- * The MIT License (MIT)
6
+ * MIT License
7
7
  *
8
8
  * Copyright 2014–2016 Zoltan Frombach
9
9
  * Copyright Juriy “kangax” Zaytsev
@@ -3891,6 +3891,7 @@ async function minifyHTML(value, options, partialMarkup) {
3891
3891
  let uidIgnorePlaceholderPattern;
3892
3892
  let uidAttr;
3893
3893
  let uidPattern;
3894
+ let uidAttrLeadingPattern;
3894
3895
  // Create inline tags/text sets with custom elements
3895
3896
  const customElementsInput = options.inlineCustomElements ?? [];
3896
3897
  const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
@@ -3972,46 +3973,55 @@ async function minifyHTML(value, options, partialMarkup) {
3972
3973
  const maxQuantifier = options.customFragmentQuantifierLimit || 200;
3973
3974
  const whitespacePattern = `\\s{0,${maxQuantifier}}`;
3974
3975
 
3975
- // Use bounded quantifiers to prevent ReDoS—this approach prevents exponential backtracking
3976
- const reCustomIgnore = new RegExp(
3977
- whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
3978
- 'g'
3979
- );
3980
- // Temporarily replace custom ignored fragments with unique attributes
3981
- value = value.replace(reCustomIgnore, function (match) {
3982
- if (!uidAttr) {
3983
- uidAttr = uniqueId(value);
3984
- uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
3985
-
3986
- if (options.minifyCSS) {
3987
- options.minifyCSS = (function (fn) {
3988
- return function (text, type) {
3989
- text = text.replace(uidPattern, function (match, prefix, index) {
3990
- const chunks = ignoredCustomMarkupChunks[+index];
3991
- return chunks[1] + uidAttr + index + uidAttr + chunks[2];
3992
- });
3993
-
3994
- return fn(text, type);
3995
- };
3996
- })(options.minifyCSS);
3997
- }
3976
+ // Fast path: The padded replace below is costly on large inputs because the
3977
+ // `\s{0,N}` padding makes the regex engine consume and backtrack at every
3978
+ // whitespace run; since the padding is optional and the fragment group requires
3979
+ // at least one match, a single unpadded probe that finds nothing proves the
3980
+ // replace would be a no-op—so skip it entirely
3981
+ const reFragmentProbe = new RegExp('(?:' + customFragments.join('|') + ')');
3982
+ if (reFragmentProbe.test(value)) {
3983
+ // Use bounded quantifiers to prevent ReDoS—this approach prevents exponential backtracking
3984
+ const reCustomIgnore = new RegExp(
3985
+ whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
3986
+ 'g'
3987
+ );
3988
+ // Temporarily replace custom ignored fragments with unique attributes
3989
+ value = value.replace(reCustomIgnore, function (match) {
3990
+ if (!uidAttr) {
3991
+ uidAttr = uniqueId(value);
3992
+ uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
3993
+ uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
3994
+
3995
+ if (options.minifyCSS) {
3996
+ options.minifyCSS = (function (fn) {
3997
+ return function (text, type) {
3998
+ text = text.replace(uidPattern, function (match, prefix, index) {
3999
+ const chunks = ignoredCustomMarkupChunks[+index];
4000
+ return chunks[1] + uidAttr + index + uidAttr + chunks[2];
4001
+ });
4002
+
4003
+ return fn(text, type);
4004
+ };
4005
+ })(options.minifyCSS);
4006
+ }
3998
4007
 
3999
- if (options.minifyJS) {
4000
- options.minifyJS = (function (fn) {
4001
- return function (text, inline, isModule) {
4002
- return fn(text.replace(uidPattern, function (match, prefix, index) {
4003
- const chunks = ignoredCustomMarkupChunks[+index];
4004
- return chunks[1] + uidAttr + index + uidAttr + chunks[2];
4005
- }), inline, isModule);
4006
- };
4007
- })(options.minifyJS);
4008
+ if (options.minifyJS) {
4009
+ options.minifyJS = (function (fn) {
4010
+ return function (text, inline, isModule) {
4011
+ return fn(text.replace(uidPattern, function (match, prefix, index) {
4012
+ const chunks = ignoredCustomMarkupChunks[+index];
4013
+ return chunks[1] + uidAttr + index + uidAttr + chunks[2];
4014
+ }), inline, isModule);
4015
+ };
4016
+ })(options.minifyJS);
4017
+ }
4008
4018
  }
4009
- }
4010
4019
 
4011
- const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
4012
- ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
4013
- return '\t' + token + '\t';
4014
- });
4020
+ const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
4021
+ ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
4022
+ return '\t' + token + '\t';
4023
+ });
4024
+ }
4015
4025
  }
4016
4026
 
4017
4027
  function canCollapseWhitespace$1(tag, attrs) {
@@ -4417,15 +4427,28 @@ async function minifyHTML(value, options, partialMarkup) {
4417
4427
  // Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
4418
4428
  function charsFinalize(text) {
4419
4429
  if (options.removeOptionalTags && text) {
4430
+ // UID-attr tokens are padded with `\t`, which would falsely look like leading whitespace;
4431
+ // resolve single-token text to its actual content for the space/comment checks below
4432
+ let effectiveText = text;
4433
+ if (uidAttrLeadingPattern && text.includes(uidAttr)) {
4434
+ const uidMatch = uidAttrLeadingPattern.exec(text);
4435
+ if (uidMatch) {
4436
+ const idx = +uidMatch[1];
4437
+ const chunks = idx < ignoredCustomMarkupChunks.length ? ignoredCustomMarkupChunks[idx] : null;
4438
+ if (chunks != null) {
4439
+ effectiveText = chunks[0];
4440
+ }
4441
+ }
4442
+ }
4420
4443
  // `<html>` may be omitted if first thing inside is not a comment
4421
4444
  // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
4422
- if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
4445
+ if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(effectiveText))) {
4423
4446
  removeStartTag();
4424
4447
  }
4425
4448
  optionalStartTag = '';
4426
4449
  // `</html>` or `</body>` may be omitted if not followed by comment
4427
4450
  // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
4428
- if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
4451
+ if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(effectiveText)))) {
4429
4452
  removeEndTag();
4430
4453
  }
4431
4454
  // Don’t reset `optionalEndTag` if text is only whitespace and will be collapsed (not conservatively)
@@ -4491,7 +4514,25 @@ async function minifyHTML(value, options, partialMarkup) {
4491
4514
  // Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
4492
4515
  function commentFinalize(comment) {
4493
4516
  if (options.removeOptionalTags && comment) {
4494
- // Preceding comments suppress tag omissions
4517
+ if (uidIgnorePlaceholderPattern) {
4518
+ const match = uidIgnorePlaceholderPattern.exec(comment);
4519
+ if (match) {
4520
+ // UID placeholders represent real HTML content, not true HTML comments;
4521
+ // if there’s a pending optional end tag and the ignored content isn’t itself
4522
+ // a comment (which per the HTML spec prevents omission), resolve it now,
4523
+ // before the UID is pushed to the buffer
4524
+ const idx = +match[1];
4525
+ const content = idx < ignoredMarkupChunks.length ? ignoredMarkupChunks[idx] : null;
4526
+ if (optionalEndTag && optionalEndTagEmitted && content != null && !/^\s*<!--/.test(content)) {
4527
+ const firstTagMatch = content.match(/^\s*<([a-zA-Z][^\s/>]*)/);
4528
+ const firstTag = firstTagMatch ? options.name(firstTagMatch[1]) : '';
4529
+ if (canRemovePrecedingTag(optionalEndTag, firstTag)) {
4530
+ removeEndTag();
4531
+ }
4532
+ }
4533
+ }
4534
+ }
4535
+ // Comments (real or placeholder) always suppress optional start tag omissions
4495
4536
  optionalStartTag = '';
4496
4537
  optionalEndTag = '';
4497
4538
  optionalEndTagEmitted = false;
@@ -1 +1 @@
1
- {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AA2wDO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAwB3B;;;;;;;;;;;;UA3/CS,MAAM;;;;;;;;;;;;;;;;;;mCAaA,MAAM,SAAS,aAAa,EAAE,yBAAyB,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;+BAM3F,MAAM,GAAG,IAAI,SAAS,aAAa,EAAE,GAAG,SAAS,qBAAqB,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA6JtG,OAAO,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2HA2BiF,MAAM,SAAS,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;;;;;;iBASxG,QAAQ,GAAG,KAAK;gBAAgC,MAAM,WAAW,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;eAa/H,MAAM;gBAAY,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;;;;;;;mBAiBzE,MAAM,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kDA+DF,MAAM,OAAO,MAAM,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCA2EpC,MAAM,SAAS,aAAa,EAAE,KAAK,IAAI;;;;;;;;;wCAQrC,MAAM,KAAK,MAAM;;;;;;;;;;;;;;;;;wBAtqBK,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
1
+ {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AAozDO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAwB3B;;;;;;;;;;;;UApiDS,MAAM;;;;;;;;;;;;;;;;;;mCAaA,MAAM,SAAS,aAAa,EAAE,yBAAyB,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;+BAM3F,MAAM,GAAG,IAAI,SAAS,aAAa,EAAE,GAAG,SAAS,qBAAqB,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA6JtG,OAAO,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2HA2BiF,MAAM,SAAS,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;;;;;;iBASxG,QAAQ,GAAG,KAAK;gBAAgC,MAAM,WAAW,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;eAa/H,MAAM;gBAAY,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;;;;;;;mBAiBzE,MAAM,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kDA+DF,MAAM,OAAO,MAAM,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCA2EpC,MAAM,SAAS,aAAa,EAAE,KAAK,IAAI;;;;;;;;;wCAQrC,MAAM,KAAK,MAAM;;;;;;;;;;;;;;;;;wBAtqBK,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
package/package.json CHANGED
@@ -10,21 +10,21 @@
10
10
  "entities": "^8.0.0",
11
11
  "lightningcss": "^1.32.0",
12
12
  "svgo": "^4.0.1",
13
- "terser": "^5.46.2"
13
+ "terser": "^5.47.1"
14
14
  },
15
15
  "description": "Super-configurable and well-tested web page minifier (enhanced successor of HTML Minifier)",
16
16
  "devDependencies": {
17
- "@commitlint/cli": "^20.5.3",
17
+ "@commitlint/cli": "^21.0.1",
18
18
  "@eslint/js": "^10.0.1",
19
19
  "@rollup/plugin-commonjs": "^29.0.2",
20
20
  "@rollup/plugin-json": "^6.1.0",
21
21
  "@rollup/plugin-node-resolve": "^16.0.3",
22
- "@swc/core": "^1.15.33",
23
- "eslint": "^10.3.0",
24
- "rollup": "^4.60.2",
22
+ "@swc/core": "^1.15.40",
23
+ "eslint": "^10.4.0",
24
+ "rollup": "^4.60.4",
25
25
  "rollup-plugin-polyfill-node": "^0.13.0",
26
26
  "typescript": "^6.0.3",
27
- "vite": "^8.0.10"
27
+ "vite": "^8.0.14"
28
28
  },
29
29
  "exports": {
30
30
  ".": {
@@ -97,5 +97,5 @@
97
97
  },
98
98
  "type": "module",
99
99
  "types": "./dist/types/htmlminifier.d.ts",
100
- "version": "6.2.6"
100
+ "version": "6.2.8"
101
101
  }
@@ -949,6 +949,7 @@ async function minifyHTML(value, options, partialMarkup) {
949
949
  let uidIgnorePlaceholderPattern;
950
950
  let uidAttr;
951
951
  let uidPattern;
952
+ let uidAttrLeadingPattern;
952
953
  // Create inline tags/text sets with custom elements
953
954
  const customElementsInput = options.inlineCustomElements ?? [];
954
955
  const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
@@ -1030,46 +1031,55 @@ async function minifyHTML(value, options, partialMarkup) {
1030
1031
  const maxQuantifier = options.customFragmentQuantifierLimit || 200;
1031
1032
  const whitespacePattern = `\\s{0,${maxQuantifier}}`;
1032
1033
 
1033
- // Use bounded quantifiers to prevent ReDoS—this approach prevents exponential backtracking
1034
- const reCustomIgnore = new RegExp(
1035
- whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
1036
- 'g'
1037
- );
1038
- // Temporarily replace custom ignored fragments with unique attributes
1039
- value = value.replace(reCustomIgnore, function (match) {
1040
- if (!uidAttr) {
1041
- uidAttr = uniqueId(value);
1042
- uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
1043
-
1044
- if (options.minifyCSS) {
1045
- options.minifyCSS = (function (fn) {
1046
- return function (text, type) {
1047
- text = text.replace(uidPattern, function (match, prefix, index) {
1048
- const chunks = ignoredCustomMarkupChunks[+index];
1049
- return chunks[1] + uidAttr + index + uidAttr + chunks[2];
1050
- });
1051
-
1052
- return fn(text, type);
1053
- };
1054
- })(options.minifyCSS);
1055
- }
1034
+ // Fast path: The padded replace below is costly on large inputs because the
1035
+ // `\s{0,N}` padding makes the regex engine consume and backtrack at every
1036
+ // whitespace run; since the padding is optional and the fragment group requires
1037
+ // at least one match, a single unpadded probe that finds nothing proves the
1038
+ // replace would be a no-op—so skip it entirely
1039
+ const reFragmentProbe = new RegExp('(?:' + customFragments.join('|') + ')');
1040
+ if (reFragmentProbe.test(value)) {
1041
+ // Use bounded quantifiers to prevent ReDoS—this approach prevents exponential backtracking
1042
+ const reCustomIgnore = new RegExp(
1043
+ whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
1044
+ 'g'
1045
+ );
1046
+ // Temporarily replace custom ignored fragments with unique attributes
1047
+ value = value.replace(reCustomIgnore, function (match) {
1048
+ if (!uidAttr) {
1049
+ uidAttr = uniqueId(value);
1050
+ uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
1051
+ uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
1052
+
1053
+ if (options.minifyCSS) {
1054
+ options.minifyCSS = (function (fn) {
1055
+ return function (text, type) {
1056
+ text = text.replace(uidPattern, function (match, prefix, index) {
1057
+ const chunks = ignoredCustomMarkupChunks[+index];
1058
+ return chunks[1] + uidAttr + index + uidAttr + chunks[2];
1059
+ });
1060
+
1061
+ return fn(text, type);
1062
+ };
1063
+ })(options.minifyCSS);
1064
+ }
1056
1065
 
1057
- if (options.minifyJS) {
1058
- options.minifyJS = (function (fn) {
1059
- return function (text, inline, isModule) {
1060
- return fn(text.replace(uidPattern, function (match, prefix, index) {
1061
- const chunks = ignoredCustomMarkupChunks[+index];
1062
- return chunks[1] + uidAttr + index + uidAttr + chunks[2];
1063
- }), inline, isModule);
1064
- };
1065
- })(options.minifyJS);
1066
+ if (options.minifyJS) {
1067
+ options.minifyJS = (function (fn) {
1068
+ return function (text, inline, isModule) {
1069
+ return fn(text.replace(uidPattern, function (match, prefix, index) {
1070
+ const chunks = ignoredCustomMarkupChunks[+index];
1071
+ return chunks[1] + uidAttr + index + uidAttr + chunks[2];
1072
+ }), inline, isModule);
1073
+ };
1074
+ })(options.minifyJS);
1075
+ }
1066
1076
  }
1067
- }
1068
1077
 
1069
- const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
1070
- ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
1071
- return '\t' + token + '\t';
1072
- });
1078
+ const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
1079
+ ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
1080
+ return '\t' + token + '\t';
1081
+ });
1082
+ }
1073
1083
  }
1074
1084
 
1075
1085
  function canCollapseWhitespace(tag, attrs) {
@@ -1475,15 +1485,28 @@ async function minifyHTML(value, options, partialMarkup) {
1475
1485
  // Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
1476
1486
  function charsFinalize(text) {
1477
1487
  if (options.removeOptionalTags && text) {
1488
+ // UID-attr tokens are padded with `\t`, which would falsely look like leading whitespace;
1489
+ // resolve single-token text to its actual content for the space/comment checks below
1490
+ let effectiveText = text;
1491
+ if (uidAttrLeadingPattern && text.includes(uidAttr)) {
1492
+ const uidMatch = uidAttrLeadingPattern.exec(text);
1493
+ if (uidMatch) {
1494
+ const idx = +uidMatch[1];
1495
+ const chunks = idx < ignoredCustomMarkupChunks.length ? ignoredCustomMarkupChunks[idx] : null;
1496
+ if (chunks != null) {
1497
+ effectiveText = chunks[0];
1498
+ }
1499
+ }
1500
+ }
1478
1501
  // `<html>` may be omitted if first thing inside is not a comment
1479
1502
  // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
1480
- if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
1503
+ if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(effectiveText))) {
1481
1504
  removeStartTag();
1482
1505
  }
1483
1506
  optionalStartTag = '';
1484
1507
  // `</html>` or `</body>` may be omitted if not followed by comment
1485
1508
  // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
1486
- if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
1509
+ if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(effectiveText)))) {
1487
1510
  removeEndTag();
1488
1511
  }
1489
1512
  // Don’t reset `optionalEndTag` if text is only whitespace and will be collapsed (not conservatively)
@@ -1549,7 +1572,25 @@ async function minifyHTML(value, options, partialMarkup) {
1549
1572
  // Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
1550
1573
  function commentFinalize(comment) {
1551
1574
  if (options.removeOptionalTags && comment) {
1552
- // Preceding comments suppress tag omissions
1575
+ if (uidIgnorePlaceholderPattern) {
1576
+ const match = uidIgnorePlaceholderPattern.exec(comment);
1577
+ if (match) {
1578
+ // UID placeholders represent real HTML content, not true HTML comments;
1579
+ // if there’s a pending optional end tag and the ignored content isn’t itself
1580
+ // a comment (which per the HTML spec prevents omission), resolve it now,
1581
+ // before the UID is pushed to the buffer
1582
+ const idx = +match[1];
1583
+ const content = idx < ignoredMarkupChunks.length ? ignoredMarkupChunks[idx] : null;
1584
+ if (optionalEndTag && optionalEndTagEmitted && content != null && !/^\s*<!--/.test(content)) {
1585
+ const firstTagMatch = content.match(/^\s*<([a-zA-Z][^\s/>]*)/);
1586
+ const firstTag = firstTagMatch ? options.name(firstTagMatch[1]) : '';
1587
+ if (canRemovePrecedingTag(optionalEndTag, firstTag)) {
1588
+ removeEndTag();
1589
+ }
1590
+ }
1591
+ }
1592
+ }
1593
+ // Comments (real or placeholder) always suppress optional start tag omissions
1553
1594
  optionalStartTag = '';
1554
1595
  optionalEndTag = '';
1555
1596
  optionalEndTagEmitted = false;