html-minifier-next 4.14.1 → 4.15.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 +99 -31
- package/cli.js +3 -2
- package/dist/htmlminifier.cjs +323 -4
- package/dist/htmlminifier.esm.bundle.js +323 -4
- package/dist/types/htmlminifier.d.ts +15 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts +23 -0
- package/dist/types/lib/svg.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/htmlminifier.js +11 -0
- package/src/htmlparser.js +1 -1
- package/src/lib/attributes.js +15 -1
- package/src/lib/options.js +10 -2
- package/src/lib/svg.js +272 -0
- package/src/lib/whitespace.js +16 -1
- package/src/presets.js +1 -0
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ From npm for use as a command line app:
|
|
|
14
14
|
npm i -g html-minifier-next
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Directly with npx (no installation required):
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
npx html-minifier-next --help
|
|
21
|
+
```
|
|
22
|
+
|
|
17
23
|
From npm for programmatic use:
|
|
18
24
|
|
|
19
25
|
```shell
|
|
@@ -67,7 +73,7 @@ module.exports = {
|
|
|
67
73
|
|
|
68
74
|
**Using a configuration file:**
|
|
69
75
|
|
|
70
|
-
```
|
|
76
|
+
```shell
|
|
71
77
|
# Specify config file
|
|
72
78
|
html-minifier-next --config-file=html-minifier.json --input-dir=src --output-dir=dist
|
|
73
79
|
|
|
@@ -116,7 +122,7 @@ To review the specific options set, [presets.js](https://github.com/j9t/html-min
|
|
|
116
122
|
|
|
117
123
|
**Using presets:**
|
|
118
124
|
|
|
119
|
-
```
|
|
125
|
+
```shell
|
|
120
126
|
# Via CLI flag
|
|
121
127
|
html-minifier-next --preset conservative input.html
|
|
122
128
|
|
|
@@ -162,6 +168,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
162
168
|
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points | |
|
|
163
169
|
| `minifyCSS`<br>`--minify-css` | Minify CSS in `style` elements and `style` attributes (uses [Lightning CSS](https://lightningcss.dev/)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
|
|
164
170
|
| `minifyJS`<br>`--minify-js` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://github.com/terser/terser) or [SWC](https://swc.rs/)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
|
|
171
|
+
| `minifySVG`<br>`--minify-svg` | Minify SVG elements and attributes (numeric precision, default attributes, colors) | `false` (could be `true`, `Object`) |
|
|
165
172
|
| `minifyURLs`<br>`--minify-urls` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `String`, `Object`, `Function(text)`, `async Function(text)`) |
|
|
166
173
|
| `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never add a newline before a tag that closes an element | `false` |
|
|
167
174
|
| `partialMarkup`<br>`--partial-markup` | Treat input as a partial HTML fragment, preserving stray end tags (closing tags without opening tags) and preventing auto-closing of unclosed tags at end of input | `false` |
|
|
@@ -237,7 +244,7 @@ You can choose between different JS minifiers using the `engine` field:
|
|
|
237
244
|
```js
|
|
238
245
|
const result = await minify(html, {
|
|
239
246
|
minifyJS: {
|
|
240
|
-
engine: 'swc', // Use
|
|
247
|
+
engine: 'swc', // Use SWC for faster minification
|
|
241
248
|
// SWC-specific options here
|
|
242
249
|
}
|
|
243
250
|
});
|
|
@@ -248,12 +255,14 @@ const result = await minify(html, {
|
|
|
248
255
|
* `terser` (default): The standard JavaScript minifier with excellent compression
|
|
249
256
|
* [`swc`](https://swc.rs/): Rust-based minifier that’s significantly faster than Terser (requires separate installation)
|
|
250
257
|
|
|
251
|
-
**To use
|
|
258
|
+
**To use SWC**, install it as a dependency:
|
|
252
259
|
|
|
253
|
-
```
|
|
260
|
+
```shell
|
|
254
261
|
npm i @swc/core
|
|
255
262
|
```
|
|
256
263
|
|
|
264
|
+
(Build-only users may want to install it as a dev dependency: `npm i -D @swc/core`.)
|
|
265
|
+
|
|
257
266
|
**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
267
|
|
|
259
268
|
You can pass engine-specific configuration options:
|
|
@@ -288,42 +297,95 @@ const result = await minify(html, {
|
|
|
288
297
|
});
|
|
289
298
|
```
|
|
290
299
|
|
|
300
|
+
### SVG minification
|
|
301
|
+
|
|
302
|
+
When `minifySVG` is set to `true`, HTML Minifier Next applies SVG-specific optimizations to SVG elements and their attributes. These optimizations are lightweight, fast, and safe:
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
const result = await minify(html, {
|
|
306
|
+
minifySVG: true // Enable with default settings
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
What gets optimized:
|
|
311
|
+
|
|
312
|
+
1. Numeric precision reduction: Coordinates and path data are rounded to 3 decimal places by default
|
|
313
|
+
- `<path d="M 0.00000000 0.00000000"/>` → `<path d="M 0 0"/>`
|
|
314
|
+
- `<circle cx="10.500000" cy="20.300000" r="5.000"/>` → `<circle cx="10.5" cy="20.3" r="5"/>`
|
|
315
|
+
|
|
316
|
+
2. Whitespace removal: Excess whitespace in numeric attribute values is removed
|
|
317
|
+
- `transform="translate( 10 , 20 )"` → `transform="translate(10,20)"`
|
|
318
|
+
- `points="100, 10 40, 198"` → `points="100,10 40,198"`
|
|
319
|
+
|
|
320
|
+
3. Color minification: Hex colors are shortened and RGB values converted to hex
|
|
321
|
+
- `fill="#000000"` → `fill="#000"`
|
|
322
|
+
- `fill="rgb(255,255,255)"` → `fill="#fff"`
|
|
323
|
+
|
|
324
|
+
4. Default attribute removal: Well-documented SVG default attributes are removed
|
|
325
|
+
- `fill-opacity="1"` → removed
|
|
326
|
+
- `stroke-linecap="butt"` → removed
|
|
327
|
+
|
|
328
|
+
You can customize the optimization behavior by providing an options object:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
const result = await minify(html, {
|
|
332
|
+
minifySVG: {
|
|
333
|
+
precision: 2, // Use 2 decimal places instead of 3
|
|
334
|
+
removeDefaults: true, // Remove default attributes (default: true)
|
|
335
|
+
minifyColors: true // Minify color values (default: true)
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Available options:
|
|
341
|
+
|
|
342
|
+
* `precision`: Number of decimal places for coordinates and path data (default: `3`)
|
|
343
|
+
* `removeDefaults`: Remove attributes with default values (default: `true`)
|
|
344
|
+
* `minifyColors`: Minify color values with hex shortening and RGB-to-hex conversion (default: `true`)
|
|
345
|
+
|
|
346
|
+
**Important:**
|
|
347
|
+
|
|
348
|
+
* SVG minification only applies within `<svg>` elements
|
|
349
|
+
* Case sensitivity and self-closing slashes are automatically preserved in SVG (regardless of global settings)
|
|
350
|
+
* For maximum compression, use `minifySVG` together with `collapseWhitespace` and other options
|
|
351
|
+
* This is a lightweight, built-in implementation; for more aggressive SVG optimization, consider using [SVGO](https://github.com/svg/svgo) as a separate build step
|
|
352
|
+
|
|
291
353
|
## Minification comparison
|
|
292
354
|
|
|
293
|
-
How does HTML Minifier Next compare to other minifiers? (All with the most aggressive settings—though without [hyper-optimization](https://meiert.com/blog/the-ways-of-writing-html/#toc-hyper-optimized)—and against some large documents
|
|
355
|
+
How does HTML Minifier Next compare to other minifiers? (All minification with the most aggressive settings—though without [hyper-optimization](https://meiert.com/blog/the-ways-of-writing-html/#toc-hyper-optimized)—and against some large documents. Note that HTML Minifier Terser cannot currently—Dec 2025—handle modern CSS syntax like CSS nesting and removes such code, which can make it appear more effective than it is. Minimize does not minify CSS and JS.)
|
|
294
356
|
|
|
295
357
|
<!-- Auto-generated benchmarks, don’t edit -->
|
|
296
358
|
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next) ([config](https://github.com/j9t/html-minifier-next/blob/main/benchmarks/html-minifier.json))<br>[](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
297
359
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
298
|
-
| [A List Apart](https://alistapart.com/) | 59 | **
|
|
299
|
-
| [Apple](https://www.apple.com/) | 266 | **
|
|
300
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
301
|
-
| [CERN](https://home.cern/) | 152 | **83** | 84 | 91 | 91 |
|
|
302
|
-
| [CSS-Tricks](https://css-tricks.com/) | 162 |
|
|
303
|
-
| [ECMAScript](https://tc39.es/ecma262/) | 7250 |
|
|
360
|
+
| [A List Apart](https://alistapart.com/) | 59 | **50** | **50** | 51 | 52 | 51 | 54 | 52 |
|
|
361
|
+
| [Apple](https://www.apple.com/) | 266 | **206** | 207 | 236 | 239 | 240 | 242 | 243 |
|
|
362
|
+
| [BBC](https://www.bbc.co.uk/) | 647 | **587** | 598 | 607 | 607 | 608 | 641 | n/a |
|
|
363
|
+
| [CERN](https://home.cern/) | 152 | **83** | 84 | 91 | 91 | 92 | 93 | 96 |
|
|
364
|
+
| [CSS-Tricks](https://css-tricks.com/) | 162 | **119** | 120 | 127 | 143 | 143 | 148 | 145 |
|
|
365
|
+
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | 6402 | **6354** | 6573 | 6455 | 6578 | 6626 | n/a |
|
|
304
366
|
| [EDRi](https://edri.org/) | 80 | **59** | 60 | 70 | 70 | 71 | 75 | 73 |
|
|
305
|
-
| [EFF](https://www.eff.org/) | 55 | **
|
|
367
|
+
| [EFF](https://www.eff.org/) | 55 | **46** | 47 | 49 | 48 | 49 | 50 | 50 |
|
|
306
368
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
307
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
369
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1559 | 1451 | 1460 | **1398** | 1484 | 1495 | 1506 | n/a |
|
|
308
370
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 152 | **122** | **122** | 126 | 125 | 125 | 132 | 127 |
|
|
309
|
-
| [Frontend Dogma](https://frontenddogma.com/) |
|
|
371
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** | **217** | 238 | 223 | 225 | 243 | 224 |
|
|
310
372
|
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 17 | 18 | 18 |
|
|
311
|
-
| [Ground News](https://ground.news/) |
|
|
312
|
-
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 |
|
|
373
|
+
| [Ground News](https://ground.news/) | 2104 | **1841** | 1843 | 1936 | 1959 | 1964 | 2091 | n/a |
|
|
374
|
+
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
313
375
|
| [Igalia](https://www.igalia.com/) | 50 | **34** | **34** | 36 | 36 | 36 | 37 | 37 |
|
|
314
|
-
| [Leanpub](https://leanpub.com/) |
|
|
376
|
+
| [Leanpub](https://leanpub.com/) | 229 | 200 | **198** | 214 | 214 | 215 | 225 | 227 |
|
|
315
377
|
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | **28** | 32 | 35 | 35 | 36 | 36 |
|
|
316
|
-
| [MDN](https://developer.mozilla.org/en-US/) |
|
|
317
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
318
|
-
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 |
|
|
319
|
-
| [SitePoint](https://www.sitepoint.com/) |
|
|
320
|
-
| [TetraLogical](https://tetralogical.com/) | 44 |
|
|
378
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 108 | **62** | **62** | 64 | 64 | 65 | 67 | 67 |
|
|
379
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 221 | **195** | **195** | 201 | 199 | 199 | 201 | 202 |
|
|
380
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 68 | 74 | **55** | 74 | 75 | 77 | 76 |
|
|
381
|
+
| [SitePoint](https://www.sitepoint.com/) | 491 | **360** | **360** | 431 | 465 | 470 | 488 | n/a |
|
|
382
|
+
| [TetraLogical](https://tetralogical.com/) | 44 | 39 | 38 | **35** | 38 | 39 | 39 | 39 |
|
|
321
383
|
| [TPGi](https://www.tpgi.com/) | 175 | **159** | 161 | 160 | 164 | 166 | 172 | 172 |
|
|
322
|
-
| [United Nations](https://www.un.org/en/) | 152 | **113** |
|
|
384
|
+
| [United Nations](https://www.un.org/en/) | 152 | **113** | 115 | 122 | 125 | 125 | 131 | 124 |
|
|
323
385
|
| [W3C](https://www.w3.org/) | 50 | **36** | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
324
|
-
| **Average processing time** | |
|
|
386
|
+
| **Average processing time** | | 246 ms (26/26) | 343 ms (26/26) | 154 ms (26/26) | 54 ms (26/26) | **15 ms (26/26)** | 312 ms (26/26) | 1379 ms (21/26) |
|
|
325
387
|
|
|
326
|
-
(Last updated: Dec
|
|
388
|
+
(Last updated: Dec 22, 2025)
|
|
327
389
|
<!-- End auto-generated -->
|
|
328
390
|
|
|
329
391
|
## Examples
|
|
@@ -332,13 +394,19 @@ How does HTML Minifier Next compare to other minifiers? (All with the most aggre
|
|
|
332
394
|
|
|
333
395
|
**Sample command line:**
|
|
334
396
|
|
|
335
|
-
```
|
|
397
|
+
```shell
|
|
336
398
|
html-minifier-next --collapse-whitespace --remove-comments --minify-js --input-dir=. --output-dir=example
|
|
337
399
|
```
|
|
338
400
|
|
|
401
|
+
Another example, using npx:
|
|
402
|
+
|
|
403
|
+
```shell
|
|
404
|
+
npx html-minifier-next --input-dir=test --file-ext html --preset comprehensive --output-dir example
|
|
405
|
+
```
|
|
406
|
+
|
|
339
407
|
**Process specific files and directories:**
|
|
340
408
|
|
|
341
|
-
```
|
|
409
|
+
```shell
|
|
342
410
|
# Process only HTML files
|
|
343
411
|
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html
|
|
344
412
|
|
|
@@ -356,7 +424,7 @@ html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
|
|
|
356
424
|
|
|
357
425
|
**Exclude directories from processing:**
|
|
358
426
|
|
|
359
|
-
```
|
|
427
|
+
```shell
|
|
360
428
|
# Ignore a single directory
|
|
361
429
|
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs
|
|
362
430
|
|
|
@@ -369,7 +437,7 @@ html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ign
|
|
|
369
437
|
|
|
370
438
|
**Dry run mode (preview outcome without writing files):**
|
|
371
439
|
|
|
372
|
-
```
|
|
440
|
+
```shell
|
|
373
441
|
# Preview with output file
|
|
374
442
|
html-minifier-next input.html -o output.html --dry --collapse-whitespace
|
|
375
443
|
|
|
@@ -384,7 +452,7 @@ html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
|
|
|
384
452
|
|
|
385
453
|
**Verbose mode (show detailed processing information):**
|
|
386
454
|
|
|
387
|
-
```
|
|
455
|
+
```shell
|
|
388
456
|
# Show processing details while minifying
|
|
389
457
|
html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace
|
|
390
458
|
# Output: Options: collapseWhitespace, html5, includeAutoGeneratedTags
|
package/cli.js
CHANGED
|
@@ -140,8 +140,9 @@ const mainOptions = {
|
|
|
140
140
|
keepClosingSlash: 'Keep the trailing slash on void elements',
|
|
141
141
|
maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseValidInt('maxInputLength')],
|
|
142
142
|
maxLineLength: ['Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points', parseValidInt('maxLineLength')],
|
|
143
|
-
minifyCSS: ['Minify CSS in
|
|
144
|
-
minifyJS: ['Minify JavaScript in
|
|
143
|
+
minifyCSS: ['Minify CSS in "style" elements and "style" attributes (uses Lightning CSS)', parseJSON],
|
|
144
|
+
minifyJS: ['Minify JavaScript in "script" elements and event attributes (uses Terser or SWC; pass "{"engine": "swc"}" for SWC)', parseJSON],
|
|
145
|
+
minifySVG: ['Minify SVG elements and attributes (numeric precision, default attributes, colors)', parseJSON],
|
|
145
146
|
minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
|
|
146
147
|
noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
|
|
147
148
|
partialMarkup: 'Treat input as a partial HTML fragment, preserving stray end tags and unclosed tags',
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -192,7 +192,7 @@ class HTMLParser {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
// https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
195
|
+
// https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
196
196
|
if (/^<!\[/.test(html)) {
|
|
197
197
|
const conditionalEnd = html.indexOf(']>');
|
|
198
198
|
|
|
@@ -761,6 +761,7 @@ const presets = {
|
|
|
761
761
|
decodeEntities: true,
|
|
762
762
|
minifyCSS: true,
|
|
763
763
|
minifyJS: true,
|
|
764
|
+
minifySVG: true,
|
|
764
765
|
minifyURLs: true,
|
|
765
766
|
noNewlinesBeforeTagClose: true,
|
|
766
767
|
processConditionalComments: true,
|
|
@@ -1135,10 +1136,24 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
1135
1136
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
1136
1137
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
1137
1138
|
}
|
|
1139
|
+
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
1140
|
+
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
1141
|
+
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
1142
|
+
if (inlineElementsToKeepWhitespaceWithin.has(tagName)) {
|
|
1143
|
+
trimLeft = false;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1138
1146
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
1139
1147
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
1140
1148
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
1141
1149
|
}
|
|
1150
|
+
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
1151
|
+
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
1152
|
+
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
1153
|
+
if (inlineElementsToKeepWhitespaceWithin.has(tagName)) {
|
|
1154
|
+
trimRight = false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1142
1157
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
1143
1158
|
}
|
|
1144
1159
|
|
|
@@ -1239,6 +1254,279 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
1239
1254
|
return text;
|
|
1240
1255
|
}
|
|
1241
1256
|
|
|
1257
|
+
/**
|
|
1258
|
+
* Lightweight SVG optimizations:
|
|
1259
|
+
*
|
|
1260
|
+
* - Numeric precision reduction for coordinates and path data
|
|
1261
|
+
* - Whitespace removal in attribute values (numeric sequences)
|
|
1262
|
+
* - Default attribute removal (safe, well-documented defaults)
|
|
1263
|
+
* - Color minification (hex shortening, rgb() to hex)
|
|
1264
|
+
*/
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Default SVG attribute values that can be safely removed
|
|
1268
|
+
* Only includes well-documented, widely-supported defaults
|
|
1269
|
+
*/
|
|
1270
|
+
const SVG_DEFAULT_ATTRS = {
|
|
1271
|
+
// Fill and stroke defaults
|
|
1272
|
+
fill: value => value === 'black' || value === '#000' || value === '#000000',
|
|
1273
|
+
'fill-opacity': value => value === '1',
|
|
1274
|
+
'fill-rule': value => value === 'nonzero',
|
|
1275
|
+
stroke: value => value === 'none',
|
|
1276
|
+
'stroke-dasharray': value => value === 'none',
|
|
1277
|
+
'stroke-dashoffset': value => value === '0',
|
|
1278
|
+
'stroke-linecap': value => value === 'butt',
|
|
1279
|
+
'stroke-linejoin': value => value === 'miter',
|
|
1280
|
+
'stroke-miterlimit': value => value === '4',
|
|
1281
|
+
'stroke-opacity': value => value === '1',
|
|
1282
|
+
'stroke-width': value => value === '1',
|
|
1283
|
+
|
|
1284
|
+
// Text and font defaults
|
|
1285
|
+
'font-family': value => value === 'inherit',
|
|
1286
|
+
'font-size': value => value === 'medium',
|
|
1287
|
+
'font-style': value => value === 'normal',
|
|
1288
|
+
'font-variant': value => value === 'normal',
|
|
1289
|
+
'font-weight': value => value === 'normal',
|
|
1290
|
+
'letter-spacing': value => value === 'normal',
|
|
1291
|
+
'text-decoration': value => value === 'none',
|
|
1292
|
+
'text-anchor': value => value === 'start',
|
|
1293
|
+
|
|
1294
|
+
// Other common defaults
|
|
1295
|
+
opacity: value => value === '1',
|
|
1296
|
+
visibility: value => value === 'visible',
|
|
1297
|
+
display: value => value === 'inline',
|
|
1298
|
+
overflow: value => value === 'visible'
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Minify numeric value by removing trailing zeros and unnecessary decimals
|
|
1303
|
+
* @param {string} num - Numeric string to minify
|
|
1304
|
+
* @param {number} precision - Maximum decimal places to keep
|
|
1305
|
+
* @returns {string} Minified numeric string
|
|
1306
|
+
*/
|
|
1307
|
+
function minifyNumber(num, precision = 3) {
|
|
1308
|
+
const parsed = parseFloat(num);
|
|
1309
|
+
|
|
1310
|
+
// Handle special cases
|
|
1311
|
+
if (isNaN(parsed)) return num;
|
|
1312
|
+
if (parsed === 0) return '0';
|
|
1313
|
+
if (!isFinite(parsed)) return num;
|
|
1314
|
+
|
|
1315
|
+
// Convert to fixed precision, then remove trailing zeros
|
|
1316
|
+
const fixed = parsed.toFixed(precision);
|
|
1317
|
+
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
1318
|
+
|
|
1319
|
+
return trimmed || '0';
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Minify SVG path data by reducing numeric precision
|
|
1324
|
+
* @param {string} pathData - SVG path data string
|
|
1325
|
+
* @param {number} precision - Decimal precision for coordinates
|
|
1326
|
+
* @returns {string} Minified path data
|
|
1327
|
+
*/
|
|
1328
|
+
function minifyPathData(pathData, precision = 3) {
|
|
1329
|
+
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
1330
|
+
|
|
1331
|
+
// Match numbers (including scientific notation and negative values)
|
|
1332
|
+
// Regex: optional minus, digits, optional decimal point and more digits, optional exponent
|
|
1333
|
+
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
1334
|
+
return minifyNumber(match, precision);
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Minify whitespace in numeric attribute values
|
|
1340
|
+
* Examples:
|
|
1341
|
+
* "10 , 20" → "10,20"
|
|
1342
|
+
* "translate( 10 20 )" → "translate(10 20)"
|
|
1343
|
+
* "100, 10 40, 198" → "100,10 40,198"
|
|
1344
|
+
*
|
|
1345
|
+
* @param {string} value - Attribute value to minify
|
|
1346
|
+
* @returns {string} Minified value
|
|
1347
|
+
*/
|
|
1348
|
+
function minifyAttributeWhitespace(value) {
|
|
1349
|
+
if (!value || typeof value !== 'string') return value;
|
|
1350
|
+
|
|
1351
|
+
return value
|
|
1352
|
+
// Remove spaces around commas
|
|
1353
|
+
.replace(/\s*,\s*/g, ',')
|
|
1354
|
+
// Remove spaces around parentheses
|
|
1355
|
+
.replace(/\(\s+/g, '(')
|
|
1356
|
+
.replace(/\s+\)/g, ')')
|
|
1357
|
+
// Collapse multiple spaces to single space
|
|
1358
|
+
.replace(/\s+/g, ' ')
|
|
1359
|
+
// Trim leading/trailing whitespace
|
|
1360
|
+
.trim();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Minify color values (hex shortening, rgb to hex conversion)
|
|
1365
|
+
* @param {string} color - Color value to minify
|
|
1366
|
+
* @returns {string} Minified color value
|
|
1367
|
+
*/
|
|
1368
|
+
function minifyColor(color) {
|
|
1369
|
+
if (!color || typeof color !== 'string') return color;
|
|
1370
|
+
|
|
1371
|
+
const trimmed = color.trim().toLowerCase();
|
|
1372
|
+
|
|
1373
|
+
// Shorten 6-digit hex to 3-digit when possible
|
|
1374
|
+
// #aabbcc → #abc, #000000 → #000
|
|
1375
|
+
const hexMatch = trimmed.match(/^#([0-9a-f]{6})$/);
|
|
1376
|
+
if (hexMatch) {
|
|
1377
|
+
const hex = hexMatch[1];
|
|
1378
|
+
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
1379
|
+
return '#' + hex[0] + hex[2] + hex[4];
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Convert rgb(255,255,255) to hex
|
|
1384
|
+
const rgbMatch = trimmed.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
1385
|
+
if (rgbMatch) {
|
|
1386
|
+
const r = parseInt(rgbMatch[1], 10);
|
|
1387
|
+
const g = parseInt(rgbMatch[2], 10);
|
|
1388
|
+
const b = parseInt(rgbMatch[3], 10);
|
|
1389
|
+
|
|
1390
|
+
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
1391
|
+
const toHex = (n) => {
|
|
1392
|
+
const h = n.toString(16);
|
|
1393
|
+
return h.length === 1 ? '0' + h : h;
|
|
1394
|
+
};
|
|
1395
|
+
const hexColor = '#' + toHex(r) + toHex(g) + toHex(b);
|
|
1396
|
+
|
|
1397
|
+
// Try to shorten if possible
|
|
1398
|
+
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
1399
|
+
return '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
1400
|
+
}
|
|
1401
|
+
return hexColor;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
return color;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Attributes that contain numeric sequences or path data
|
|
1409
|
+
const NUMERIC_ATTRS = new Set([
|
|
1410
|
+
'd', // Path data
|
|
1411
|
+
'points', // Polygon/polyline points
|
|
1412
|
+
'viewBox', // viewBox coordinates
|
|
1413
|
+
'transform', // Transform functions
|
|
1414
|
+
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
1415
|
+
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
1416
|
+
'width', 'height', // Dimensions
|
|
1417
|
+
'dx', 'dy', // Text offsets
|
|
1418
|
+
'offset', // Gradient offset
|
|
1419
|
+
'startOffset', // textPath
|
|
1420
|
+
'pathLength', // Path length
|
|
1421
|
+
'stdDeviation', // Filter params
|
|
1422
|
+
'baseFrequency', // Turbulence
|
|
1423
|
+
'k1', 'k2', 'k3', 'k4' // Composite filter
|
|
1424
|
+
]);
|
|
1425
|
+
|
|
1426
|
+
// Attributes that contain color values
|
|
1427
|
+
const COLOR_ATTRS = new Set([
|
|
1428
|
+
'fill',
|
|
1429
|
+
'stroke',
|
|
1430
|
+
'stop-color',
|
|
1431
|
+
'flood-color',
|
|
1432
|
+
'lighting-color'
|
|
1433
|
+
]);
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Check if an attribute should be removed based on default value
|
|
1437
|
+
* @param {string} name - Attribute name
|
|
1438
|
+
* @param {string} value - Attribute value
|
|
1439
|
+
* @returns {boolean} True if attribute can be removed
|
|
1440
|
+
*/
|
|
1441
|
+
function isDefaultAttribute(name, value) {
|
|
1442
|
+
const checker = SVG_DEFAULT_ATTRS[name];
|
|
1443
|
+
if (!checker) return false;
|
|
1444
|
+
|
|
1445
|
+
// Special case: Don’t remove `fill="black"` if stroke exists without fill
|
|
1446
|
+
// This would change the rendering (stroke-only shapes would gain black fill)
|
|
1447
|
+
if (name === 'fill' && checker(value)) {
|
|
1448
|
+
// This check would require looking at other attributes on the same element
|
|
1449
|
+
// For safety, we’ll keep this conservative and not remove `fill="black"`
|
|
1450
|
+
// in the initial implementation. Can be refined later.
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return checker(value);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Minify SVG attribute value based on attribute name
|
|
1459
|
+
* @param {string} name - Attribute name
|
|
1460
|
+
* @param {string} value - Attribute value
|
|
1461
|
+
* @param {Object} options - Minification options
|
|
1462
|
+
* @returns {string} Minified attribute value
|
|
1463
|
+
*/
|
|
1464
|
+
function minifySVGAttributeValue(name, value, options = {}) {
|
|
1465
|
+
if (!value || typeof value !== 'string') return value;
|
|
1466
|
+
|
|
1467
|
+
const { precision = 3, minifyColors = true } = options;
|
|
1468
|
+
|
|
1469
|
+
// Path data gets special treatment
|
|
1470
|
+
if (name === 'd') {
|
|
1471
|
+
return minifyPathData(value, precision);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Numeric attributes get precision reduction and whitespace minification
|
|
1475
|
+
if (NUMERIC_ATTRS.has(name)) {
|
|
1476
|
+
const minified = value.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
1477
|
+
return minifyNumber(match, precision);
|
|
1478
|
+
});
|
|
1479
|
+
return minifyAttributeWhitespace(minified);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// Color attributes get color minification
|
|
1483
|
+
if (minifyColors && COLOR_ATTRS.has(name)) {
|
|
1484
|
+
return minifyColor(value);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return value;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Check if an SVG attribute can be removed
|
|
1492
|
+
* @param {string} name - Attribute name
|
|
1493
|
+
* @param {string} value - Attribute value
|
|
1494
|
+
* @param {Object} options - Minification options
|
|
1495
|
+
* @returns {boolean} True if attribute should be removed
|
|
1496
|
+
*/
|
|
1497
|
+
function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
1498
|
+
const { removeDefaults = true } = options;
|
|
1499
|
+
|
|
1500
|
+
if (!removeDefaults) return false;
|
|
1501
|
+
|
|
1502
|
+
return isDefaultAttribute(name, value);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Get default SVG minification options
|
|
1507
|
+
* @param {Object} userOptions - User-provided options
|
|
1508
|
+
* @returns {Object} Complete options object with defaults
|
|
1509
|
+
*/
|
|
1510
|
+
function getSVGMinifierOptions(userOptions) {
|
|
1511
|
+
if (typeof userOptions === 'boolean') {
|
|
1512
|
+
return userOptions ? {
|
|
1513
|
+
precision: 3,
|
|
1514
|
+
removeDefaults: true,
|
|
1515
|
+
minifyColors: true
|
|
1516
|
+
} : null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (typeof userOptions === 'object' && userOptions !== null) {
|
|
1520
|
+
return {
|
|
1521
|
+
precision: userOptions.precision ?? 3,
|
|
1522
|
+
removeDefaults: userOptions.removeDefaults ?? true,
|
|
1523
|
+
minifyColors: userOptions.minifyColors ?? true
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1242
1530
|
// Imports
|
|
1243
1531
|
|
|
1244
1532
|
|
|
@@ -1251,7 +1539,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1251
1539
|
options.removeOptionalTags ||
|
|
1252
1540
|
options.minifyJS !== identity ||
|
|
1253
1541
|
options.minifyCSS !== identityAsync ||
|
|
1254
|
-
options.minifyURLs !== identity
|
|
1542
|
+
options.minifyURLs !== identity ||
|
|
1543
|
+
options.minifySVG
|
|
1255
1544
|
);
|
|
1256
1545
|
}
|
|
1257
1546
|
|
|
@@ -1288,7 +1577,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1288
1577
|
log: identity,
|
|
1289
1578
|
minifyCSS: identityAsync,
|
|
1290
1579
|
minifyJS: identity,
|
|
1291
|
-
minifyURLs: identity
|
|
1580
|
+
minifyURLs: identity,
|
|
1581
|
+
minifySVG: null
|
|
1292
1582
|
};
|
|
1293
1583
|
|
|
1294
1584
|
Object.keys(inputOptions).forEach(function (key) {
|
|
@@ -1521,6 +1811,11 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1521
1811
|
return text;
|
|
1522
1812
|
}
|
|
1523
1813
|
};
|
|
1814
|
+
} else if (key === 'minifySVG') {
|
|
1815
|
+
// Process SVG minification options
|
|
1816
|
+
// Unlike minifyCSS/minifyJS, this is a simple options object, not a function
|
|
1817
|
+
// The actual minification is applied inline during attribute processing
|
|
1818
|
+
options.minifySVG = getSVGMinifierOptions(option);
|
|
1524
1819
|
} else {
|
|
1525
1820
|
options[key] = option;
|
|
1526
1821
|
}
|
|
@@ -1859,6 +2154,17 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
1859
2154
|
return attrValue;
|
|
1860
2155
|
}
|
|
1861
2156
|
return minifyHTMLSelf(attrValue, options, true);
|
|
2157
|
+
} else if (options.insideSVG && options.minifySVG) {
|
|
2158
|
+
// Apply SVG-specific attribute minification when inside SVG elements
|
|
2159
|
+
try {
|
|
2160
|
+
return minifySVGAttributeValue(attrName, attrValue, options.minifySVG);
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
if (!options.continueOnMinifyError) {
|
|
2163
|
+
throw err;
|
|
2164
|
+
}
|
|
2165
|
+
options.log && options.log(err);
|
|
2166
|
+
return attrValue;
|
|
2167
|
+
}
|
|
1862
2168
|
}
|
|
1863
2169
|
return attrValue;
|
|
1864
2170
|
}
|
|
@@ -1899,7 +2205,9 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
1899
2205
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
1900
2206
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
1901
2207
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
1902
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))
|
|
2208
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
2209
|
+
(options.insideSVG && options.minifySVG &&
|
|
2210
|
+
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
1903
2211
|
return;
|
|
1904
2212
|
}
|
|
1905
2213
|
|
|
@@ -2469,6 +2777,16 @@ const jsMinifyCache = new LRU(200);
|
|
|
2469
2777
|
*
|
|
2470
2778
|
* Default: `false`
|
|
2471
2779
|
*
|
|
2780
|
+
* @prop {boolean | {precision?: number, removeDefaults?: boolean, minifyColors?: boolean}} [minifySVG]
|
|
2781
|
+
* When true, enables SVG-specific optimizations for SVG elements and attributes.
|
|
2782
|
+
* If an object is provided, it can include:
|
|
2783
|
+
* - `precision`: Number of decimal places for numeric values (coordinates, path data, etc.). Default: `3`
|
|
2784
|
+
* - `removeDefaults`: Remove attributes with default values (e.g., `fill="black"`). Default: `true`
|
|
2785
|
+
* - `minifyColors`: Minify color values (hex shortening, rgb to hex conversion). Default: `true`
|
|
2786
|
+
* If disabled, SVG content is minified using standard HTML rules only.
|
|
2787
|
+
*
|
|
2788
|
+
* Default: `false`
|
|
2789
|
+
*
|
|
2472
2790
|
* @prop {(name: string) => string} [name]
|
|
2473
2791
|
* Function used to normalise tag/attribute names. By default, this lowercases
|
|
2474
2792
|
* names, unless `caseSensitive` is enabled.
|
|
@@ -3063,6 +3381,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3063
3381
|
options.caseSensitive = true;
|
|
3064
3382
|
options.keepClosingSlash = true;
|
|
3065
3383
|
options.name = identity;
|
|
3384
|
+
options.insideSVG = lowerTag === 'svg';
|
|
3066
3385
|
}
|
|
3067
3386
|
tag = options.name(tag);
|
|
3068
3387
|
currentTag = tag;
|