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 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
- ```bash
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
- ```bash
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 swc for faster minification
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 swc**, install it as a dependency:
258
+ **To use SWC**, install it as a dependency:
252
259
 
253
- ```bash
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.—Minimize does not minify CSS or JS.)
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>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-terser)](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[![npm last update](https://img.shields.io/npm/last-update/htmlnano)](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[![npm last update](https://img.shields.io/npm/last-update/@swc/html)](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[![npm last update](https://img.shields.io/npm/last-update/@minify-html/node)](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[![npm last update](https://img.shields.io/npm/last-update/minimize)](https://socket.dev/npm/package/minimize) | [html­com­pressor.­com](https://htmlcompressor.com/) |
297
359
  | --- | --- | --- | --- | --- | --- | --- | --- | --- |
298
- | [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
299
- | [Apple](https://www.apple.com/) | 266 | **207** | **207** | 236 | 239 | 240 | 242 | 243 |
300
- | [BBC](https://www.bbc.co.uk/) | 730 | **663** | 673 | 685 | 686 | 687 | 724 | n/a |
301
- | [CERN](https://home.cern/) | 152 | **83** | 84 | 91 | 91 | 91 | 93 | 96 |
302
- | [CSS-Tricks](https://css-tricks.com/) | 162 | 121 | **120** | 127 | 143 | 143 | 148 | 145 |
303
- | [ECMAScript](https://tc39.es/ecma262/) | 7250 | **6352** | **6352** | 6573 | 6455 | 6578 | 6626 | n/a |
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 | **45** | 46 | 49 | 48 | 48 | 50 | 50 |
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/) | 1562 | 1455 | 1460 | **1401** | 1487 | 1498 | 1509 | n/a |
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/) | 224 | **214** | 216 | 237 | 222 | 224 | 243 | 224 |
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/) | 2344 | **2062** | 2064 | 2159 | 2184 | 2188 | 2331 | n/a |
312
- | [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 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/) | 222 | 192 | **190** | 207 | 207 | 207 | 217 | 219 |
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/) | 109 | **62** | **62** | 64 | 65 | 65 | 68 | 68 |
317
- | [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | **195** | 202 | 200 | 200 | 202 | 202 |
318
- | [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 74 | 74 | **55** | 74 | 75 | 77 | 76 |
319
- | [SitePoint](https://www.sitepoint.com/) | 501 | **370** | **370** | 442 | 475 | 480 | 498 | n/a |
320
- | [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 39 | 39 | 39 |
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** | 114 | 121 | 125 | 125 | 131 | 124 |
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** | | 264 ms (26/26) | 365 ms (26/26) | 162 ms (26/26) | 56 ms (26/26) | **16 ms (26/26)** | 323 ms (26/26) | 1373 ms (21/26) |
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 21, 2025)
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
- ```bash
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
- ```bash
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
- ```bash
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
- ```bash
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
- ```bash
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 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],
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',
@@ -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;