html-minifier-next 4.15.1 → 4.16.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 +37 -32
- package/dist/htmlminifier.cjs +196 -19
- package/dist/htmlminifier.esm.bundle.js +196 -19
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts +2 -1
- package/dist/types/lib/svg.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/attributes.js +1 -1
- package/src/lib/options.js +34 -0
- package/src/lib/svg.js +162 -18
package/README.md
CHANGED
|
@@ -352,42 +352,47 @@ Available options:
|
|
|
352
352
|
|
|
353
353
|
## Minification comparison
|
|
354
354
|
|
|
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 a wide range of pages.
|
|
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 a wide range of pages.)
|
|
356
356
|
|
|
357
357
|
<!-- Auto-generated benchmarks, don’t edit -->
|
|
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) | [
|
|
359
|
-
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
360
|
-
| [A List Apart](https://alistapart.com/) | 59 | **50** |
|
|
361
|
-
| [Apple](https://www.apple.com/) |
|
|
362
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
363
|
-
| [CERN](https://home.cern/) | 152 | **83** |
|
|
364
|
-
| [CSS-Tricks](https://css-tricks.com/) | 162 | **119** |
|
|
365
|
-
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | 6401
|
|
366
|
-
| [EDRi](https://edri.org/) | 80 | **59** |
|
|
367
|
-
| [EFF](https://www.eff.org/) |
|
|
368
|
-
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** |
|
|
369
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
370
|
-
| [French Tech](https://lafrenchtech.gouv.fr/) |
|
|
371
|
-
| [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** |
|
|
372
|
-
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 |
|
|
373
|
-
| [Ground News](https://ground.news/) |
|
|
374
|
-
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 |
|
|
375
|
-
| [Igalia](https://www.igalia.com/) | 50 | **33** |
|
|
376
|
-
| [Leanpub](https://leanpub.com/) |
|
|
377
|
-
| [Mastodon](https://mastodon.social/explore) | 37 | **28** |
|
|
378
|
-
| [MDN](https://developer.mozilla.org/en-US/) |
|
|
379
|
-
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **
|
|
380
|
-
| [
|
|
381
|
-
| [
|
|
382
|
-
| [
|
|
383
|
-
| [
|
|
384
|
-
| [
|
|
385
|
-
| [
|
|
386
|
-
|
|
|
387
|
-
|
|
388
|
-
(
|
|
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) | [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/) |
|
|
359
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
360
|
+
| [A List Apart](https://alistapart.com/) | 59 | **50** | 51 | 52 | 51 | 54 | 52 |
|
|
361
|
+
| [Apple](https://www.apple.com/) | 260 | **203** | 231 | 235 | 236 | 238 | 238 |
|
|
362
|
+
| [BBC](https://www.bbc.co.uk/) | 704 | **641** | 661 | 661 | 662 | 698 | n/a |
|
|
363
|
+
| [CERN](https://home.cern/) | 152 | **83** | 91 | 91 | 91 | 93 | 96 |
|
|
364
|
+
| [CSS-Tricks](https://css-tricks.com/) | 162 | **119** | 128 | 143 | 143 | 148 | 145 |
|
|
365
|
+
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | **6401** | 6573 | 6455 | 6578 | 6626 | n/a |
|
|
366
|
+
| [EDRi](https://edri.org/) | 80 | **59** | 70 | 70 | 71 | 75 | 73 |
|
|
367
|
+
| [EFF](https://www.eff.org/) | 54 | **45** | 48 | 47 | 48 | 49 | 49 |
|
|
368
|
+
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
369
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1537 | 1429 | **1378** | 1462 | 1473 | 1484 | n/a |
|
|
370
|
+
| [French Tech](https://lafrenchtech.gouv.fr/) | 153 | **122** | 126 | 126 | 126 | 132 | 127 |
|
|
371
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** | 238 | 224 | 225 | 244 | 225 |
|
|
372
|
+
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 18 | 18 |
|
|
373
|
+
| [Ground News](https://ground.news/) | 2437 | **2150** | 2246 | 2272 | 2275 | 2424 | n/a |
|
|
374
|
+
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | 153 | **147** | 149 | 155 | 149 |
|
|
375
|
+
| [Igalia](https://www.igalia.com/) | 50 | **33** | 36 | 36 | 36 | 37 | 36 |
|
|
376
|
+
| [Leanpub](https://leanpub.com/) | 233 | **203** | 217 | 217 | 218 | 228 | 230 |
|
|
377
|
+
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | 32 | 35 | 35 | 36 | 36 |
|
|
378
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 106 | **60** | 62 | 63 | 63 | 66 | 66 |
|
|
379
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 | 203 | 204 |
|
|
380
|
+
| [Mistral AI](https://mistral.ai/) | 361 | **319** | 324 | 326 | 327 | 357 | n/a |
|
|
381
|
+
| [Mozilla](https://www.mozilla.org/) | 45 | **31** | 34 | 34 | 34 | 35 | 35 |
|
|
382
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 68 | **55** | 74 | 75 | 77 | 76 |
|
|
383
|
+
| [SitePoint](https://www.sitepoint.com/) | 482 | **351** | 422 | 456 | 460 | 478 | n/a |
|
|
384
|
+
| [Startup-Verband](https://startupverband.de/) | 42 | **29** | 30 | 30 | 30 | 31 | 30 |
|
|
385
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **159** | 160 | 164 | 166 | 172 | 172 |
|
|
386
|
+
| [United Nations](https://www.un.org/en/) | 152 | **112** | 121 | 125 | 125 | 130 | 123 |
|
|
387
|
+
| [Vivaldi](https://vivaldi.com/) | 92 | **74** | n/a | 79 | 81 | 83 | 81 |
|
|
388
|
+
| [W3C](https://www.w3.org/) | 50 | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
389
|
+
| **Average processing time** | | 132 ms (29/29) | 169 ms (28/29) | 54 ms (29/29) | **14 ms (29/29)** | 293 ms (29/29) | 1355 ms (23/29) |
|
|
390
|
+
|
|
391
|
+
(Last updated: Dec 26, 2025)
|
|
389
392
|
<!-- End auto-generated -->
|
|
390
393
|
|
|
394
|
+
Notes: Minimize does not minify CSS and JS. [HTML Minifier Terser](https://github.com/terser/html-minifier-terser) is currently not included due to issues around whitespace collapsing and removal of code using modern CSS features, issues which appeared to distort the data.
|
|
395
|
+
|
|
391
396
|
## Examples
|
|
392
397
|
|
|
393
398
|
### CLI
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -1260,9 +1260,32 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
1260
1260
|
* - Numeric precision reduction for coordinates and path data
|
|
1261
1261
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
1262
1262
|
* - Default attribute removal (safe, well-documented defaults)
|
|
1263
|
-
* - Color minification (hex shortening, rgb() to hex)
|
|
1263
|
+
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
1264
|
+
* - Identity transform removal
|
|
1265
|
+
* - Path data space optimization
|
|
1264
1266
|
*/
|
|
1265
1267
|
|
|
1268
|
+
|
|
1269
|
+
// Cache for minified numbers
|
|
1270
|
+
const numberCache = new LRU(100);
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Named colors that are shorter than their hex equivalents
|
|
1274
|
+
* Only includes cases where using the name saves bytes
|
|
1275
|
+
*/
|
|
1276
|
+
const NAMED_COLORS = {
|
|
1277
|
+
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
1278
|
+
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
1279
|
+
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
1280
|
+
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
1281
|
+
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
1282
|
+
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
1283
|
+
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
1284
|
+
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
1285
|
+
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
1286
|
+
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1266
1289
|
/**
|
|
1267
1290
|
* Default SVG attribute values that can be safely removed
|
|
1268
1291
|
* Only includes well-documented, widely-supported defaults
|
|
@@ -1295,7 +1318,22 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
1295
1318
|
opacity: value => value === '1',
|
|
1296
1319
|
visibility: value => value === 'visible',
|
|
1297
1320
|
display: value => value === 'inline',
|
|
1298
|
-
|
|
1321
|
+
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
1322
|
+
|
|
1323
|
+
// Clipping and masking defaults
|
|
1324
|
+
'clip-rule': value => value === 'nonzero',
|
|
1325
|
+
'clip-path': value => value === 'none',
|
|
1326
|
+
mask: value => value === 'none',
|
|
1327
|
+
|
|
1328
|
+
// Marker defaults
|
|
1329
|
+
'marker-start': value => value === 'none',
|
|
1330
|
+
'marker-mid': value => value === 'none',
|
|
1331
|
+
'marker-end': value => value === 'none',
|
|
1332
|
+
|
|
1333
|
+
// Filter and color defaults
|
|
1334
|
+
filter: value => value === 'none',
|
|
1335
|
+
'color-interpolation': value => value === 'sRGB',
|
|
1336
|
+
'color-interpolation-filters': value => value === 'linearRGB'
|
|
1299
1337
|
};
|
|
1300
1338
|
|
|
1301
1339
|
/**
|
|
@@ -1305,6 +1343,20 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
1305
1343
|
* @returns {string} Minified numeric string
|
|
1306
1344
|
*/
|
|
1307
1345
|
function minifyNumber(num, precision = 3) {
|
|
1346
|
+
// Fast path for common values (avoids parsing and caching)
|
|
1347
|
+
if (num === '0' || num === '1') return num;
|
|
1348
|
+
// Common decimal variants that tools export
|
|
1349
|
+
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
1350
|
+
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
1351
|
+
|
|
1352
|
+
// Check cache
|
|
1353
|
+
// (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
1354
|
+
// This is intentional to avoid parsing overhead.
|
|
1355
|
+
// Real-world SVG files from export tools typically use consistent formats.)
|
|
1356
|
+
const cacheKey = `${num}:${precision}`;
|
|
1357
|
+
const cached = numberCache.get(cacheKey);
|
|
1358
|
+
if (cached !== undefined) return cached;
|
|
1359
|
+
|
|
1308
1360
|
const parsed = parseFloat(num);
|
|
1309
1361
|
|
|
1310
1362
|
// Handle special cases
|
|
@@ -1316,11 +1368,13 @@ function minifyNumber(num, precision = 3) {
|
|
|
1316
1368
|
const fixed = parsed.toFixed(precision);
|
|
1317
1369
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
1318
1370
|
|
|
1319
|
-
|
|
1371
|
+
const result = trimmed || '0';
|
|
1372
|
+
numberCache.set(cacheKey, result);
|
|
1373
|
+
return result;
|
|
1320
1374
|
}
|
|
1321
1375
|
|
|
1322
1376
|
/**
|
|
1323
|
-
* Minify SVG path data by reducing numeric precision
|
|
1377
|
+
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
1324
1378
|
* @param {string} pathData - SVG path data string
|
|
1325
1379
|
* @param {number} precision - Decimal precision for coordinates
|
|
1326
1380
|
* @returns {string} Minified path data
|
|
@@ -1328,11 +1382,25 @@ function minifyNumber(num, precision = 3) {
|
|
|
1328
1382
|
function minifyPathData(pathData, precision = 3) {
|
|
1329
1383
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
1330
1384
|
|
|
1331
|
-
//
|
|
1332
|
-
|
|
1333
|
-
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
1385
|
+
// First, minify all numbers
|
|
1386
|
+
let result = pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
1334
1387
|
return minifyNumber(match, precision);
|
|
1335
1388
|
});
|
|
1389
|
+
|
|
1390
|
+
// Remove unnecessary spaces around path commands
|
|
1391
|
+
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
1392
|
+
// M 10 20 → M10 20, L -5 -3 → L-5-3
|
|
1393
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
1394
|
+
|
|
1395
|
+
// Safe to remove space before command letter when preceded by a number
|
|
1396
|
+
// 0 L → 0L, 20 M → 20M
|
|
1397
|
+
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
1398
|
+
|
|
1399
|
+
// Safe to remove space before negative number when preceded by a number
|
|
1400
|
+
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
1401
|
+
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
1402
|
+
|
|
1403
|
+
return result;
|
|
1336
1404
|
}
|
|
1337
1405
|
|
|
1338
1406
|
/**
|
|
@@ -1361,27 +1429,49 @@ function minifyAttributeWhitespace(value) {
|
|
|
1361
1429
|
}
|
|
1362
1430
|
|
|
1363
1431
|
/**
|
|
1364
|
-
* Minify color values (hex shortening, rgb to hex conversion)
|
|
1432
|
+
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
1433
|
+
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
1365
1434
|
* @param {string} color - Color value to minify
|
|
1366
1435
|
* @returns {string} Minified color value
|
|
1367
1436
|
*/
|
|
1368
1437
|
function minifyColor(color) {
|
|
1369
1438
|
if (!color || typeof color !== 'string') return color;
|
|
1370
1439
|
|
|
1371
|
-
const trimmed = color.trim()
|
|
1440
|
+
const trimmed = color.trim();
|
|
1441
|
+
|
|
1442
|
+
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
1443
|
+
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
1444
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
1445
|
+
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
1446
|
+
return trimmed;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Now safe to lowercase for color matching
|
|
1450
|
+
const lower = trimmed.toLowerCase();
|
|
1372
1451
|
|
|
1373
1452
|
// Shorten 6-digit hex to 3-digit when possible
|
|
1374
1453
|
// #aabbcc → #abc, #000000 → #000
|
|
1375
|
-
const hexMatch =
|
|
1454
|
+
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
1376
1455
|
if (hexMatch) {
|
|
1377
1456
|
const hex = hexMatch[1];
|
|
1378
1457
|
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
1379
|
-
|
|
1458
|
+
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
1459
|
+
// Try to use named color if shorter
|
|
1460
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
1380
1461
|
}
|
|
1462
|
+
// Can’t shorten, but check for named color
|
|
1463
|
+
return NAMED_COLORS[lower] || lower;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Match 3-digit hex colors
|
|
1467
|
+
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
1468
|
+
if (hex3Match) {
|
|
1469
|
+
// Check if there’s a shorter named color
|
|
1470
|
+
return NAMED_COLORS[lower] || lower;
|
|
1381
1471
|
}
|
|
1382
1472
|
|
|
1383
1473
|
// Convert rgb(255,255,255) to hex
|
|
1384
|
-
const rgbMatch =
|
|
1474
|
+
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
1385
1475
|
if (rgbMatch) {
|
|
1386
1476
|
const r = parseInt(rgbMatch[1], 10);
|
|
1387
1477
|
const g = parseInt(rgbMatch[2], 10);
|
|
@@ -1396,13 +1486,15 @@ function minifyColor(color) {
|
|
|
1396
1486
|
|
|
1397
1487
|
// Try to shorten if possible
|
|
1398
1488
|
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
1399
|
-
|
|
1489
|
+
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
1490
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
1400
1491
|
}
|
|
1401
|
-
return hexColor;
|
|
1492
|
+
return NAMED_COLORS[hexColor] || hexColor;
|
|
1402
1493
|
}
|
|
1403
1494
|
}
|
|
1404
1495
|
|
|
1405
|
-
return
|
|
1496
|
+
// Not a recognized color format, return as-is (preserves case)
|
|
1497
|
+
return trimmed;
|
|
1406
1498
|
}
|
|
1407
1499
|
|
|
1408
1500
|
// Attributes that contain numeric sequences or path data
|
|
@@ -1432,13 +1524,58 @@ const COLOR_ATTRS = new Set([
|
|
|
1432
1524
|
'lighting-color'
|
|
1433
1525
|
]);
|
|
1434
1526
|
|
|
1527
|
+
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
1528
|
+
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
1529
|
+
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
1530
|
+
|
|
1531
|
+
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
1532
|
+
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
1533
|
+
|
|
1534
|
+
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
1535
|
+
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
1536
|
+
|
|
1537
|
+
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
1538
|
+
// Note: `cx` and `cy` must be valid numbers if present
|
|
1539
|
+
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
1540
|
+
|
|
1541
|
+
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
1542
|
+
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
1543
|
+
|
|
1544
|
+
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
1545
|
+
const IDENTITY_MATRIX_RE = new RegExp(`^matrix\\s*\\(\\s*1(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}1(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*\\)$`, 'i');
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Check if a transform attribute has no effect (identity transform)
|
|
1549
|
+
* @param {string} transform - Transform attribute value
|
|
1550
|
+
* @returns {boolean} True if transform is an identity (has no effect)
|
|
1551
|
+
*/
|
|
1552
|
+
function isIdentityTransform(transform) {
|
|
1553
|
+
if (!transform || typeof transform !== 'string') return false;
|
|
1554
|
+
|
|
1555
|
+
const trimmed = transform.trim();
|
|
1556
|
+
|
|
1557
|
+
// Check for common identity transforms using pre-compiled regexes
|
|
1558
|
+
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
1559
|
+
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
1560
|
+
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
1561
|
+
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
1562
|
+
IDENTITY_MATRIX_RE.test(trimmed);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1435
1565
|
/**
|
|
1436
1566
|
* Check if an attribute should be removed based on default value
|
|
1567
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
1437
1568
|
* @param {string} name - Attribute name
|
|
1438
1569
|
* @param {string} value - Attribute value
|
|
1439
1570
|
* @returns {boolean} True if attribute can be removed
|
|
1440
1571
|
*/
|
|
1441
|
-
function isDefaultAttribute(name, value) {
|
|
1572
|
+
function isDefaultAttribute(tag, name, value) {
|
|
1573
|
+
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
1574
|
+
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
1575
|
+
if (name === 'overflow' && value === 'visible') {
|
|
1576
|
+
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1442
1579
|
const checker = SVG_DEFAULT_ATTRS[name];
|
|
1443
1580
|
if (!checker) return false;
|
|
1444
1581
|
|
|
@@ -1489,17 +1626,23 @@ function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
1489
1626
|
|
|
1490
1627
|
/**
|
|
1491
1628
|
* Check if an SVG attribute can be removed
|
|
1629
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
1492
1630
|
* @param {string} name - Attribute name
|
|
1493
1631
|
* @param {string} value - Attribute value
|
|
1494
1632
|
* @param {Object} options - Minification options
|
|
1495
1633
|
* @returns {boolean} True if attribute should be removed
|
|
1496
1634
|
*/
|
|
1497
|
-
function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
1635
|
+
function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
1498
1636
|
const { removeDefaults = true } = options;
|
|
1499
1637
|
|
|
1500
1638
|
if (!removeDefaults) return false;
|
|
1501
1639
|
|
|
1502
|
-
|
|
1640
|
+
// Check for identity transforms
|
|
1641
|
+
if (name === 'transform' && isIdentityTransform(value)) {
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return isDefaultAttribute(tag, name, value);
|
|
1503
1646
|
}
|
|
1504
1647
|
|
|
1505
1648
|
/**
|
|
@@ -1581,6 +1724,31 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1581
1724
|
minifySVG: null
|
|
1582
1725
|
};
|
|
1583
1726
|
|
|
1727
|
+
// Helper to convert string patterns to RegExp (for JSON config support)
|
|
1728
|
+
const parseRegExp = (value) => {
|
|
1729
|
+
if (typeof value === 'string') {
|
|
1730
|
+
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
1731
|
+
}
|
|
1732
|
+
return value; // Already a RegExp or other type
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
const parseRegExpArray = (arr) => {
|
|
1736
|
+
return Array.isArray(arr) ? arr.map(parseRegExp) : arr;
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
// Helper for nested arrays (e.g., `customAttrSurround: [[start, end], …]`)
|
|
1740
|
+
const parseNestedRegExpArray = (arr) => {
|
|
1741
|
+
if (!Array.isArray(arr)) return arr;
|
|
1742
|
+
return arr.map(item => {
|
|
1743
|
+
// If item is an array (a pair), recursively convert each element
|
|
1744
|
+
if (Array.isArray(item)) {
|
|
1745
|
+
return item.map(parseRegExp);
|
|
1746
|
+
}
|
|
1747
|
+
// Otherwise, convert single item
|
|
1748
|
+
return parseRegExp(item);
|
|
1749
|
+
});
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1584
1752
|
Object.keys(inputOptions).forEach(function (key) {
|
|
1585
1753
|
const option = inputOptions[key];
|
|
1586
1754
|
|
|
@@ -1816,6 +1984,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1816
1984
|
// Unlike minifyCSS/minifyJS, this is a simple options object, not a function
|
|
1817
1985
|
// The actual minification is applied inline during attribute processing
|
|
1818
1986
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
1987
|
+
} else if (key === 'customAttrCollapse') {
|
|
1988
|
+
// Single RegExp pattern
|
|
1989
|
+
options[key] = parseRegExp(option);
|
|
1990
|
+
} else if (key === 'customAttrSurround') {
|
|
1991
|
+
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
1992
|
+
options[key] = parseNestedRegExpArray(option);
|
|
1993
|
+
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
1994
|
+
// Array of RegExp patterns
|
|
1995
|
+
options[key] = parseRegExpArray(option);
|
|
1819
1996
|
} else {
|
|
1820
1997
|
options[key] = option;
|
|
1821
1998
|
}
|
|
@@ -2207,7 +2384,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2207
2384
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
2208
2385
|
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
2209
2386
|
(options.insideSVG && options.minifySVG &&
|
|
2210
|
-
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
2387
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
2211
2388
|
return;
|
|
2212
2389
|
}
|
|
2213
2390
|
|
|
@@ -6402,9 +6402,32 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
6402
6402
|
* - Numeric precision reduction for coordinates and path data
|
|
6403
6403
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
6404
6404
|
* - Default attribute removal (safe, well-documented defaults)
|
|
6405
|
-
* - Color minification (hex shortening, rgb() to hex)
|
|
6405
|
+
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
6406
|
+
* - Identity transform removal
|
|
6407
|
+
* - Path data space optimization
|
|
6406
6408
|
*/
|
|
6407
6409
|
|
|
6410
|
+
|
|
6411
|
+
// Cache for minified numbers
|
|
6412
|
+
const numberCache = new LRU(100);
|
|
6413
|
+
|
|
6414
|
+
/**
|
|
6415
|
+
* Named colors that are shorter than their hex equivalents
|
|
6416
|
+
* Only includes cases where using the name saves bytes
|
|
6417
|
+
*/
|
|
6418
|
+
const NAMED_COLORS = {
|
|
6419
|
+
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
6420
|
+
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
6421
|
+
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
6422
|
+
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
6423
|
+
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
6424
|
+
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
6425
|
+
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
6426
|
+
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
6427
|
+
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
6428
|
+
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
6429
|
+
};
|
|
6430
|
+
|
|
6408
6431
|
/**
|
|
6409
6432
|
* Default SVG attribute values that can be safely removed
|
|
6410
6433
|
* Only includes well-documented, widely-supported defaults
|
|
@@ -6437,7 +6460,22 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
6437
6460
|
opacity: value => value === '1',
|
|
6438
6461
|
visibility: value => value === 'visible',
|
|
6439
6462
|
display: value => value === 'inline',
|
|
6440
|
-
|
|
6463
|
+
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
6464
|
+
|
|
6465
|
+
// Clipping and masking defaults
|
|
6466
|
+
'clip-rule': value => value === 'nonzero',
|
|
6467
|
+
'clip-path': value => value === 'none',
|
|
6468
|
+
mask: value => value === 'none',
|
|
6469
|
+
|
|
6470
|
+
// Marker defaults
|
|
6471
|
+
'marker-start': value => value === 'none',
|
|
6472
|
+
'marker-mid': value => value === 'none',
|
|
6473
|
+
'marker-end': value => value === 'none',
|
|
6474
|
+
|
|
6475
|
+
// Filter and color defaults
|
|
6476
|
+
filter: value => value === 'none',
|
|
6477
|
+
'color-interpolation': value => value === 'sRGB',
|
|
6478
|
+
'color-interpolation-filters': value => value === 'linearRGB'
|
|
6441
6479
|
};
|
|
6442
6480
|
|
|
6443
6481
|
/**
|
|
@@ -6447,6 +6485,20 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
6447
6485
|
* @returns {string} Minified numeric string
|
|
6448
6486
|
*/
|
|
6449
6487
|
function minifyNumber(num, precision = 3) {
|
|
6488
|
+
// Fast path for common values (avoids parsing and caching)
|
|
6489
|
+
if (num === '0' || num === '1') return num;
|
|
6490
|
+
// Common decimal variants that tools export
|
|
6491
|
+
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
6492
|
+
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
6493
|
+
|
|
6494
|
+
// Check cache
|
|
6495
|
+
// (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
6496
|
+
// This is intentional to avoid parsing overhead.
|
|
6497
|
+
// Real-world SVG files from export tools typically use consistent formats.)
|
|
6498
|
+
const cacheKey = `${num}:${precision}`;
|
|
6499
|
+
const cached = numberCache.get(cacheKey);
|
|
6500
|
+
if (cached !== undefined) return cached;
|
|
6501
|
+
|
|
6450
6502
|
const parsed = parseFloat(num);
|
|
6451
6503
|
|
|
6452
6504
|
// Handle special cases
|
|
@@ -6458,11 +6510,13 @@ function minifyNumber(num, precision = 3) {
|
|
|
6458
6510
|
const fixed = parsed.toFixed(precision);
|
|
6459
6511
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
6460
6512
|
|
|
6461
|
-
|
|
6513
|
+
const result = trimmed || '0';
|
|
6514
|
+
numberCache.set(cacheKey, result);
|
|
6515
|
+
return result;
|
|
6462
6516
|
}
|
|
6463
6517
|
|
|
6464
6518
|
/**
|
|
6465
|
-
* Minify SVG path data by reducing numeric precision
|
|
6519
|
+
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
6466
6520
|
* @param {string} pathData - SVG path data string
|
|
6467
6521
|
* @param {number} precision - Decimal precision for coordinates
|
|
6468
6522
|
* @returns {string} Minified path data
|
|
@@ -6470,11 +6524,25 @@ function minifyNumber(num, precision = 3) {
|
|
|
6470
6524
|
function minifyPathData(pathData, precision = 3) {
|
|
6471
6525
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
6472
6526
|
|
|
6473
|
-
//
|
|
6474
|
-
|
|
6475
|
-
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
6527
|
+
// First, minify all numbers
|
|
6528
|
+
let result = pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
6476
6529
|
return minifyNumber(match, precision);
|
|
6477
6530
|
});
|
|
6531
|
+
|
|
6532
|
+
// Remove unnecessary spaces around path commands
|
|
6533
|
+
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
6534
|
+
// M 10 20 → M10 20, L -5 -3 → L-5-3
|
|
6535
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
6536
|
+
|
|
6537
|
+
// Safe to remove space before command letter when preceded by a number
|
|
6538
|
+
// 0 L → 0L, 20 M → 20M
|
|
6539
|
+
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
6540
|
+
|
|
6541
|
+
// Safe to remove space before negative number when preceded by a number
|
|
6542
|
+
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
6543
|
+
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
6544
|
+
|
|
6545
|
+
return result;
|
|
6478
6546
|
}
|
|
6479
6547
|
|
|
6480
6548
|
/**
|
|
@@ -6503,27 +6571,49 @@ function minifyAttributeWhitespace(value) {
|
|
|
6503
6571
|
}
|
|
6504
6572
|
|
|
6505
6573
|
/**
|
|
6506
|
-
* Minify color values (hex shortening, rgb to hex conversion)
|
|
6574
|
+
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
6575
|
+
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
6507
6576
|
* @param {string} color - Color value to minify
|
|
6508
6577
|
* @returns {string} Minified color value
|
|
6509
6578
|
*/
|
|
6510
6579
|
function minifyColor(color) {
|
|
6511
6580
|
if (!color || typeof color !== 'string') return color;
|
|
6512
6581
|
|
|
6513
|
-
const trimmed = color.trim()
|
|
6582
|
+
const trimmed = color.trim();
|
|
6583
|
+
|
|
6584
|
+
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
6585
|
+
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
6586
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
6587
|
+
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
6588
|
+
return trimmed;
|
|
6589
|
+
}
|
|
6590
|
+
|
|
6591
|
+
// Now safe to lowercase for color matching
|
|
6592
|
+
const lower = trimmed.toLowerCase();
|
|
6514
6593
|
|
|
6515
6594
|
// Shorten 6-digit hex to 3-digit when possible
|
|
6516
6595
|
// #aabbcc → #abc, #000000 → #000
|
|
6517
|
-
const hexMatch =
|
|
6596
|
+
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
6518
6597
|
if (hexMatch) {
|
|
6519
6598
|
const hex = hexMatch[1];
|
|
6520
6599
|
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
6521
|
-
|
|
6600
|
+
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
6601
|
+
// Try to use named color if shorter
|
|
6602
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
6522
6603
|
}
|
|
6604
|
+
// Can’t shorten, but check for named color
|
|
6605
|
+
return NAMED_COLORS[lower] || lower;
|
|
6606
|
+
}
|
|
6607
|
+
|
|
6608
|
+
// Match 3-digit hex colors
|
|
6609
|
+
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
6610
|
+
if (hex3Match) {
|
|
6611
|
+
// Check if there’s a shorter named color
|
|
6612
|
+
return NAMED_COLORS[lower] || lower;
|
|
6523
6613
|
}
|
|
6524
6614
|
|
|
6525
6615
|
// Convert rgb(255,255,255) to hex
|
|
6526
|
-
const rgbMatch =
|
|
6616
|
+
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
6527
6617
|
if (rgbMatch) {
|
|
6528
6618
|
const r = parseInt(rgbMatch[1], 10);
|
|
6529
6619
|
const g = parseInt(rgbMatch[2], 10);
|
|
@@ -6538,13 +6628,15 @@ function minifyColor(color) {
|
|
|
6538
6628
|
|
|
6539
6629
|
// Try to shorten if possible
|
|
6540
6630
|
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
6541
|
-
|
|
6631
|
+
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
6632
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
6542
6633
|
}
|
|
6543
|
-
return hexColor;
|
|
6634
|
+
return NAMED_COLORS[hexColor] || hexColor;
|
|
6544
6635
|
}
|
|
6545
6636
|
}
|
|
6546
6637
|
|
|
6547
|
-
return
|
|
6638
|
+
// Not a recognized color format, return as-is (preserves case)
|
|
6639
|
+
return trimmed;
|
|
6548
6640
|
}
|
|
6549
6641
|
|
|
6550
6642
|
// Attributes that contain numeric sequences or path data
|
|
@@ -6574,13 +6666,58 @@ const COLOR_ATTRS = new Set([
|
|
|
6574
6666
|
'lighting-color'
|
|
6575
6667
|
]);
|
|
6576
6668
|
|
|
6669
|
+
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
6670
|
+
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
6671
|
+
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
6672
|
+
|
|
6673
|
+
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
6674
|
+
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
6675
|
+
|
|
6676
|
+
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
6677
|
+
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
6678
|
+
|
|
6679
|
+
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
6680
|
+
// Note: `cx` and `cy` must be valid numbers if present
|
|
6681
|
+
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
6682
|
+
|
|
6683
|
+
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
6684
|
+
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
6685
|
+
|
|
6686
|
+
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
6687
|
+
const IDENTITY_MATRIX_RE = new RegExp(`^matrix\\s*\\(\\s*1(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}1(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*\\)$`, 'i');
|
|
6688
|
+
|
|
6689
|
+
/**
|
|
6690
|
+
* Check if a transform attribute has no effect (identity transform)
|
|
6691
|
+
* @param {string} transform - Transform attribute value
|
|
6692
|
+
* @returns {boolean} True if transform is an identity (has no effect)
|
|
6693
|
+
*/
|
|
6694
|
+
function isIdentityTransform(transform) {
|
|
6695
|
+
if (!transform || typeof transform !== 'string') return false;
|
|
6696
|
+
|
|
6697
|
+
const trimmed = transform.trim();
|
|
6698
|
+
|
|
6699
|
+
// Check for common identity transforms using pre-compiled regexes
|
|
6700
|
+
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
6701
|
+
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
6702
|
+
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
6703
|
+
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
6704
|
+
IDENTITY_MATRIX_RE.test(trimmed);
|
|
6705
|
+
}
|
|
6706
|
+
|
|
6577
6707
|
/**
|
|
6578
6708
|
* Check if an attribute should be removed based on default value
|
|
6709
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
6579
6710
|
* @param {string} name - Attribute name
|
|
6580
6711
|
* @param {string} value - Attribute value
|
|
6581
6712
|
* @returns {boolean} True if attribute can be removed
|
|
6582
6713
|
*/
|
|
6583
|
-
function isDefaultAttribute(name, value) {
|
|
6714
|
+
function isDefaultAttribute(tag, name, value) {
|
|
6715
|
+
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
6716
|
+
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
6717
|
+
if (name === 'overflow' && value === 'visible') {
|
|
6718
|
+
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
6719
|
+
}
|
|
6720
|
+
|
|
6584
6721
|
const checker = SVG_DEFAULT_ATTRS[name];
|
|
6585
6722
|
if (!checker) return false;
|
|
6586
6723
|
|
|
@@ -6631,17 +6768,23 @@ function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
6631
6768
|
|
|
6632
6769
|
/**
|
|
6633
6770
|
* Check if an SVG attribute can be removed
|
|
6771
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
6634
6772
|
* @param {string} name - Attribute name
|
|
6635
6773
|
* @param {string} value - Attribute value
|
|
6636
6774
|
* @param {Object} options - Minification options
|
|
6637
6775
|
* @returns {boolean} True if attribute should be removed
|
|
6638
6776
|
*/
|
|
6639
|
-
function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
6777
|
+
function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
6640
6778
|
const { removeDefaults = true } = options;
|
|
6641
6779
|
|
|
6642
6780
|
if (!removeDefaults) return false;
|
|
6643
6781
|
|
|
6644
|
-
|
|
6782
|
+
// Check for identity transforms
|
|
6783
|
+
if (name === 'transform' && isIdentityTransform(value)) {
|
|
6784
|
+
return true;
|
|
6785
|
+
}
|
|
6786
|
+
|
|
6787
|
+
return isDefaultAttribute(tag, name, value);
|
|
6645
6788
|
}
|
|
6646
6789
|
|
|
6647
6790
|
/**
|
|
@@ -6723,6 +6866,31 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6723
6866
|
minifySVG: null
|
|
6724
6867
|
};
|
|
6725
6868
|
|
|
6869
|
+
// Helper to convert string patterns to RegExp (for JSON config support)
|
|
6870
|
+
const parseRegExp = (value) => {
|
|
6871
|
+
if (typeof value === 'string') {
|
|
6872
|
+
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
6873
|
+
}
|
|
6874
|
+
return value; // Already a RegExp or other type
|
|
6875
|
+
};
|
|
6876
|
+
|
|
6877
|
+
const parseRegExpArray = (arr) => {
|
|
6878
|
+
return Array.isArray(arr) ? arr.map(parseRegExp) : arr;
|
|
6879
|
+
};
|
|
6880
|
+
|
|
6881
|
+
// Helper for nested arrays (e.g., `customAttrSurround: [[start, end], …]`)
|
|
6882
|
+
const parseNestedRegExpArray = (arr) => {
|
|
6883
|
+
if (!Array.isArray(arr)) return arr;
|
|
6884
|
+
return arr.map(item => {
|
|
6885
|
+
// If item is an array (a pair), recursively convert each element
|
|
6886
|
+
if (Array.isArray(item)) {
|
|
6887
|
+
return item.map(parseRegExp);
|
|
6888
|
+
}
|
|
6889
|
+
// Otherwise, convert single item
|
|
6890
|
+
return parseRegExp(item);
|
|
6891
|
+
});
|
|
6892
|
+
};
|
|
6893
|
+
|
|
6726
6894
|
Object.keys(inputOptions).forEach(function (key) {
|
|
6727
6895
|
const option = inputOptions[key];
|
|
6728
6896
|
|
|
@@ -6958,6 +7126,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6958
7126
|
// Unlike minifyCSS/minifyJS, this is a simple options object, not a function
|
|
6959
7127
|
// The actual minification is applied inline during attribute processing
|
|
6960
7128
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
7129
|
+
} else if (key === 'customAttrCollapse') {
|
|
7130
|
+
// Single RegExp pattern
|
|
7131
|
+
options[key] = parseRegExp(option);
|
|
7132
|
+
} else if (key === 'customAttrSurround') {
|
|
7133
|
+
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
7134
|
+
options[key] = parseNestedRegExpArray(option);
|
|
7135
|
+
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
7136
|
+
// Array of RegExp patterns
|
|
7137
|
+
options[key] = parseRegExpArray(option);
|
|
6961
7138
|
} else {
|
|
6962
7139
|
options[key] = option;
|
|
6963
7140
|
}
|
|
@@ -7349,7 +7526,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7349
7526
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
7350
7527
|
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
7351
7528
|
(options.insideSVG && options.minifySVG &&
|
|
7352
|
-
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
7529
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
7353
7530
|
return;
|
|
7354
7531
|
}
|
|
7355
7532
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAWA,6DAUC;AAID;;;;;;;;;GASG;AACH,6CATW,OAAO,CAAC,eAAe,CAAC,0EAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAG9C,eAAe,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAWA,6DAUC;AAID;;;;;;;;;GASG;AACH,6CATW,OAAO,CAAC,eAAe,CAAC,0EAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAG9C,eAAe,CA6S3B"}
|
package/dist/types/lib/svg.d.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
export function minifySVGAttributeValue(name: string, value: string, options?: any): string;
|
|
9
9
|
/**
|
|
10
10
|
* Check if an SVG attribute can be removed
|
|
11
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
11
12
|
* @param {string} name - Attribute name
|
|
12
13
|
* @param {string} value - Attribute value
|
|
13
14
|
* @param {Object} options - Minification options
|
|
14
15
|
* @returns {boolean} True if attribute should be removed
|
|
15
16
|
*/
|
|
16
|
-
export function shouldRemoveSVGAttribute(name: string, value: string, options?: any): boolean;
|
|
17
|
+
export function shouldRemoveSVGAttribute(tag: string, name: string, value: string, options?: any): boolean;
|
|
17
18
|
/**
|
|
18
19
|
* Get default SVG minification options
|
|
19
20
|
* @param {Object} userOptions - User-provided options
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../../src/lib/svg.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../../src/lib/svg.js"],"names":[],"mappings":"AAkVA;;;;;;GAMG;AACH,8CALW,MAAM,SACN,MAAM,kBAEJ,MAAM,CA0BlB;AAED;;;;;;;GAOG;AACH,8CANW,MAAM,QACN,MAAM,SACN,MAAM,kBAEJ,OAAO,CAanB;AAED;;;;GAIG;AACH,6DAkBC"}
|
package/package.json
CHANGED
package/src/lib/attributes.js
CHANGED
|
@@ -400,7 +400,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
400
400
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
401
401
|
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
402
402
|
(options.insideSVG && options.minifySVG &&
|
|
403
|
-
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
403
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
404
404
|
return;
|
|
405
405
|
}
|
|
406
406
|
|
package/src/lib/options.js
CHANGED
|
@@ -58,6 +58,31 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
58
58
|
minifySVG: null
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
// Helper to convert string patterns to RegExp (for JSON config support)
|
|
62
|
+
const parseRegExp = (value) => {
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
65
|
+
}
|
|
66
|
+
return value; // Already a RegExp or other type
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const parseRegExpArray = (arr) => {
|
|
70
|
+
return Array.isArray(arr) ? arr.map(parseRegExp) : arr;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Helper for nested arrays (e.g., `customAttrSurround: [[start, end], …]`)
|
|
74
|
+
const parseNestedRegExpArray = (arr) => {
|
|
75
|
+
if (!Array.isArray(arr)) return arr;
|
|
76
|
+
return arr.map(item => {
|
|
77
|
+
// If item is an array (a pair), recursively convert each element
|
|
78
|
+
if (Array.isArray(item)) {
|
|
79
|
+
return item.map(parseRegExp);
|
|
80
|
+
}
|
|
81
|
+
// Otherwise, convert single item
|
|
82
|
+
return parseRegExp(item);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
61
86
|
Object.keys(inputOptions).forEach(function (key) {
|
|
62
87
|
const option = inputOptions[key];
|
|
63
88
|
|
|
@@ -293,6 +318,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
293
318
|
// Unlike minifyCSS/minifyJS, this is a simple options object, not a function
|
|
294
319
|
// The actual minification is applied inline during attribute processing
|
|
295
320
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
321
|
+
} else if (key === 'customAttrCollapse') {
|
|
322
|
+
// Single RegExp pattern
|
|
323
|
+
options[key] = parseRegExp(option);
|
|
324
|
+
} else if (key === 'customAttrSurround') {
|
|
325
|
+
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
326
|
+
options[key] = parseNestedRegExpArray(option);
|
|
327
|
+
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
328
|
+
// Array of RegExp patterns
|
|
329
|
+
options[key] = parseRegExpArray(option);
|
|
296
330
|
} else {
|
|
297
331
|
options[key] = option;
|
|
298
332
|
}
|
package/src/lib/svg.js
CHANGED
|
@@ -4,9 +4,33 @@
|
|
|
4
4
|
* - Numeric precision reduction for coordinates and path data
|
|
5
5
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
6
6
|
* - Default attribute removal (safe, well-documented defaults)
|
|
7
|
-
* - Color minification (hex shortening, rgb() to hex)
|
|
7
|
+
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
8
|
+
* - Identity transform removal
|
|
9
|
+
* - Path data space optimization
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
import { LRU } from './utils.js';
|
|
13
|
+
|
|
14
|
+
// Cache for minified numbers
|
|
15
|
+
const numberCache = new LRU(100);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Named colors that are shorter than their hex equivalents
|
|
19
|
+
* Only includes cases where using the name saves bytes
|
|
20
|
+
*/
|
|
21
|
+
const NAMED_COLORS = {
|
|
22
|
+
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
23
|
+
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
24
|
+
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
25
|
+
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
26
|
+
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
27
|
+
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
28
|
+
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
29
|
+
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
30
|
+
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
31
|
+
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
32
|
+
};
|
|
33
|
+
|
|
10
34
|
/**
|
|
11
35
|
* Default SVG attribute values that can be safely removed
|
|
12
36
|
* Only includes well-documented, widely-supported defaults
|
|
@@ -39,7 +63,22 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
39
63
|
opacity: value => value === '1',
|
|
40
64
|
visibility: value => value === 'visible',
|
|
41
65
|
display: value => value === 'inline',
|
|
42
|
-
|
|
66
|
+
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
67
|
+
|
|
68
|
+
// Clipping and masking defaults
|
|
69
|
+
'clip-rule': value => value === 'nonzero',
|
|
70
|
+
'clip-path': value => value === 'none',
|
|
71
|
+
mask: value => value === 'none',
|
|
72
|
+
|
|
73
|
+
// Marker defaults
|
|
74
|
+
'marker-start': value => value === 'none',
|
|
75
|
+
'marker-mid': value => value === 'none',
|
|
76
|
+
'marker-end': value => value === 'none',
|
|
77
|
+
|
|
78
|
+
// Filter and color defaults
|
|
79
|
+
filter: value => value === 'none',
|
|
80
|
+
'color-interpolation': value => value === 'sRGB',
|
|
81
|
+
'color-interpolation-filters': value => value === 'linearRGB'
|
|
43
82
|
};
|
|
44
83
|
|
|
45
84
|
/**
|
|
@@ -49,6 +88,20 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
49
88
|
* @returns {string} Minified numeric string
|
|
50
89
|
*/
|
|
51
90
|
function minifyNumber(num, precision = 3) {
|
|
91
|
+
// Fast path for common values (avoids parsing and caching)
|
|
92
|
+
if (num === '0' || num === '1') return num;
|
|
93
|
+
// Common decimal variants that tools export
|
|
94
|
+
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
95
|
+
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
96
|
+
|
|
97
|
+
// Check cache
|
|
98
|
+
// (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
99
|
+
// This is intentional to avoid parsing overhead.
|
|
100
|
+
// Real-world SVG files from export tools typically use consistent formats.)
|
|
101
|
+
const cacheKey = `${num}:${precision}`;
|
|
102
|
+
const cached = numberCache.get(cacheKey);
|
|
103
|
+
if (cached !== undefined) return cached;
|
|
104
|
+
|
|
52
105
|
const parsed = parseFloat(num);
|
|
53
106
|
|
|
54
107
|
// Handle special cases
|
|
@@ -60,11 +113,13 @@ function minifyNumber(num, precision = 3) {
|
|
|
60
113
|
const fixed = parsed.toFixed(precision);
|
|
61
114
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
62
115
|
|
|
63
|
-
|
|
116
|
+
const result = trimmed || '0';
|
|
117
|
+
numberCache.set(cacheKey, result);
|
|
118
|
+
return result;
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
/**
|
|
67
|
-
* Minify SVG path data by reducing numeric precision
|
|
122
|
+
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
68
123
|
* @param {string} pathData - SVG path data string
|
|
69
124
|
* @param {number} precision - Decimal precision for coordinates
|
|
70
125
|
* @returns {string} Minified path data
|
|
@@ -72,11 +127,25 @@ function minifyNumber(num, precision = 3) {
|
|
|
72
127
|
function minifyPathData(pathData, precision = 3) {
|
|
73
128
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
74
129
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
130
|
+
// First, minify all numbers
|
|
131
|
+
let result = pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
78
132
|
return minifyNumber(match, precision);
|
|
79
133
|
});
|
|
134
|
+
|
|
135
|
+
// Remove unnecessary spaces around path commands
|
|
136
|
+
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
137
|
+
// M 10 20 → M10 20, L -5 -3 → L-5-3
|
|
138
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
139
|
+
|
|
140
|
+
// Safe to remove space before command letter when preceded by a number
|
|
141
|
+
// 0 L → 0L, 20 M → 20M
|
|
142
|
+
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
143
|
+
|
|
144
|
+
// Safe to remove space before negative number when preceded by a number
|
|
145
|
+
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
146
|
+
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
147
|
+
|
|
148
|
+
return result;
|
|
80
149
|
}
|
|
81
150
|
|
|
82
151
|
/**
|
|
@@ -105,27 +174,49 @@ function minifyAttributeWhitespace(value) {
|
|
|
105
174
|
}
|
|
106
175
|
|
|
107
176
|
/**
|
|
108
|
-
* Minify color values (hex shortening, rgb to hex conversion)
|
|
177
|
+
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
178
|
+
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
109
179
|
* @param {string} color - Color value to minify
|
|
110
180
|
* @returns {string} Minified color value
|
|
111
181
|
*/
|
|
112
182
|
function minifyColor(color) {
|
|
113
183
|
if (!color || typeof color !== 'string') return color;
|
|
114
184
|
|
|
115
|
-
const trimmed = color.trim()
|
|
185
|
+
const trimmed = color.trim();
|
|
186
|
+
|
|
187
|
+
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
188
|
+
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
189
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
190
|
+
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
191
|
+
return trimmed;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Now safe to lowercase for color matching
|
|
195
|
+
const lower = trimmed.toLowerCase();
|
|
116
196
|
|
|
117
197
|
// Shorten 6-digit hex to 3-digit when possible
|
|
118
198
|
// #aabbcc → #abc, #000000 → #000
|
|
119
|
-
const hexMatch =
|
|
199
|
+
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
120
200
|
if (hexMatch) {
|
|
121
201
|
const hex = hexMatch[1];
|
|
122
202
|
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
123
|
-
|
|
203
|
+
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
204
|
+
// Try to use named color if shorter
|
|
205
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
124
206
|
}
|
|
207
|
+
// Can’t shorten, but check for named color
|
|
208
|
+
return NAMED_COLORS[lower] || lower;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Match 3-digit hex colors
|
|
212
|
+
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
213
|
+
if (hex3Match) {
|
|
214
|
+
// Check if there’s a shorter named color
|
|
215
|
+
return NAMED_COLORS[lower] || lower;
|
|
125
216
|
}
|
|
126
217
|
|
|
127
218
|
// Convert rgb(255,255,255) to hex
|
|
128
|
-
const rgbMatch =
|
|
219
|
+
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
129
220
|
if (rgbMatch) {
|
|
130
221
|
const r = parseInt(rgbMatch[1], 10);
|
|
131
222
|
const g = parseInt(rgbMatch[2], 10);
|
|
@@ -140,13 +231,15 @@ function minifyColor(color) {
|
|
|
140
231
|
|
|
141
232
|
// Try to shorten if possible
|
|
142
233
|
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
143
|
-
|
|
234
|
+
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
235
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
144
236
|
}
|
|
145
|
-
return hexColor;
|
|
237
|
+
return NAMED_COLORS[hexColor] || hexColor;
|
|
146
238
|
}
|
|
147
239
|
}
|
|
148
240
|
|
|
149
|
-
return
|
|
241
|
+
// Not a recognized color format, return as-is (preserves case)
|
|
242
|
+
return trimmed;
|
|
150
243
|
}
|
|
151
244
|
|
|
152
245
|
// Attributes that contain numeric sequences or path data
|
|
@@ -176,13 +269,58 @@ const COLOR_ATTRS = new Set([
|
|
|
176
269
|
'lighting-color'
|
|
177
270
|
]);
|
|
178
271
|
|
|
272
|
+
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
273
|
+
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
274
|
+
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
275
|
+
|
|
276
|
+
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
277
|
+
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
278
|
+
|
|
279
|
+
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
280
|
+
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
281
|
+
|
|
282
|
+
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
283
|
+
// Note: `cx` and `cy` must be valid numbers if present
|
|
284
|
+
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
285
|
+
|
|
286
|
+
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
287
|
+
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
288
|
+
|
|
289
|
+
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
290
|
+
const IDENTITY_MATRIX_RE = new RegExp(`^matrix\\s*\\(\\s*1(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}1(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*${SEP}0(?:\\.0+)?\\s*\\)$`, 'i');
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if a transform attribute has no effect (identity transform)
|
|
294
|
+
* @param {string} transform - Transform attribute value
|
|
295
|
+
* @returns {boolean} True if transform is an identity (has no effect)
|
|
296
|
+
*/
|
|
297
|
+
function isIdentityTransform(transform) {
|
|
298
|
+
if (!transform || typeof transform !== 'string') return false;
|
|
299
|
+
|
|
300
|
+
const trimmed = transform.trim();
|
|
301
|
+
|
|
302
|
+
// Check for common identity transforms using pre-compiled regexes
|
|
303
|
+
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
304
|
+
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
305
|
+
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
306
|
+
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
307
|
+
IDENTITY_MATRIX_RE.test(trimmed);
|
|
308
|
+
}
|
|
309
|
+
|
|
179
310
|
/**
|
|
180
311
|
* Check if an attribute should be removed based on default value
|
|
312
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
181
313
|
* @param {string} name - Attribute name
|
|
182
314
|
* @param {string} value - Attribute value
|
|
183
315
|
* @returns {boolean} True if attribute can be removed
|
|
184
316
|
*/
|
|
185
|
-
function isDefaultAttribute(name, value) {
|
|
317
|
+
function isDefaultAttribute(tag, name, value) {
|
|
318
|
+
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
319
|
+
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
320
|
+
if (name === 'overflow' && value === 'visible') {
|
|
321
|
+
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
322
|
+
}
|
|
323
|
+
|
|
186
324
|
const checker = SVG_DEFAULT_ATTRS[name];
|
|
187
325
|
if (!checker) return false;
|
|
188
326
|
|
|
@@ -233,17 +371,23 @@ export function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
233
371
|
|
|
234
372
|
/**
|
|
235
373
|
* Check if an SVG attribute can be removed
|
|
374
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
236
375
|
* @param {string} name - Attribute name
|
|
237
376
|
* @param {string} value - Attribute value
|
|
238
377
|
* @param {Object} options - Minification options
|
|
239
378
|
* @returns {boolean} True if attribute should be removed
|
|
240
379
|
*/
|
|
241
|
-
export function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
380
|
+
export function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
242
381
|
const { removeDefaults = true } = options;
|
|
243
382
|
|
|
244
383
|
if (!removeDefaults) return false;
|
|
245
384
|
|
|
246
|
-
|
|
385
|
+
// Check for identity transforms
|
|
386
|
+
if (name === 'transform' && isIdentityTransform(value)) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return isDefaultAttribute(tag, name, value);
|
|
247
391
|
}
|
|
248
392
|
|
|
249
393
|
/**
|