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 +32 -44
- package/cli.js +1 -1
- package/dist/htmlminifier.cjs +81 -40
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/htmlminifier.js +81 -40
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
package/dist/htmlminifier.cjs
CHANGED
|
@@ -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
|
-
//
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
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
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
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
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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":"
|
|
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.
|
|
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": "^
|
|
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.
|
|
23
|
-
"eslint": "^10.
|
|
24
|
-
"rollup": "^4.60.
|
|
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.
|
|
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.
|
|
100
|
+
"version": "6.2.8"
|
|
101
101
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -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
|
-
//
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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;
|